From 7e275f8dc2b728ee44bd4c81352c8bf64d4c4ee7 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Tue, 10 Feb 2026 14:59:25 +1100 Subject: [PATCH 01/81] Gloas envelope consensus and more operations tests (#8781) - Implement new `process_execution_payload` (as `process_execution_payload_envelope`). - Implement new processing for deposit requests, including logic for adding new builders to the registry with index reuse. - Enable a bunch more operations EF tests (most of them except bid processing/payload attestations/etc which we don't have code for yet). Co-Authored-By: Michael Sproul --- .../src/envelope_processing.rs | 278 ++++++++++++++++++ consensus/state_processing/src/lib.rs | 1 + .../process_operations.rs | 143 ++++++++- .../verify_attestation.rs | 4 - .../signed_execution_payload_envelope.rs | 41 ++- consensus/types/src/state/beacon_state.rs | 75 ++++- consensus/types/src/validator/mod.rs | 4 +- consensus/types/src/validator/validator.rs | 8 + testing/ef_tests/check_all_files_accessed.py | 10 - testing/ef_tests/src/cases/operations.rs | 92 +++++- testing/ef_tests/src/handler.rs | 13 +- testing/ef_tests/tests/tests.rs | 8 +- 12 files changed, 643 insertions(+), 34 deletions(-) create mode 100644 consensus/state_processing/src/envelope_processing.rs diff --git a/consensus/state_processing/src/envelope_processing.rs b/consensus/state_processing/src/envelope_processing.rs new file mode 100644 index 0000000000..d46728dbbc --- /dev/null +++ b/consensus/state_processing/src/envelope_processing.rs @@ -0,0 +1,278 @@ +use crate::BlockProcessingError; +use crate::VerifySignatures; +use crate::per_block_processing::compute_timestamp_at_slot; +use crate::per_block_processing::process_operations::{ + process_consolidation_requests, process_deposit_requests_post_gloas, + process_withdrawal_requests, +}; +use safe_arith::{ArithError, SafeArith}; +use tree_hash::TreeHash; +use types::{ + BeaconState, BeaconStateError, BuilderIndex, BuilderPendingPayment, ChainSpec, EthSpec, + ExecutionBlockHash, Hash256, SignedExecutionPayloadEnvelope, Slot, +}; + +macro_rules! envelope_verify { + ($condition: expr, $result: expr) => { + if !$condition { + return Err($result); + } + }; +} + +#[derive(Debug, Clone)] +pub enum EnvelopeProcessingError { + /// Bad Signature + BadSignature, + BeaconStateError(BeaconStateError), + BlockProcessingError(BlockProcessingError), + ArithError(ArithError), + /// Envelope doesn't match latest beacon block header + LatestBlockHeaderMismatch { + envelope_root: Hash256, + block_header_root: Hash256, + }, + /// Envelope doesn't match latest beacon block slot + SlotMismatch { + envelope_slot: Slot, + parent_state_slot: Slot, + }, + /// The payload withdrawals don't match the state's payload withdrawals. + WithdrawalsRootMismatch { + state: Hash256, + payload: Hash256, + }, + // The builder index doesn't match the committed bid. + BuilderIndexMismatch { + committed_bid: BuilderIndex, + envelope: BuilderIndex, + }, + // The gas limit doesn't match the committed bid + GasLimitMismatch { + committed_bid: u64, + envelope: u64, + }, + // The block hash doesn't match the committed bid + BlockHashMismatch { + committed_bid: ExecutionBlockHash, + envelope: ExecutionBlockHash, + }, + // The parent hash doesn't match the previous execution payload + ParentHashMismatch { + state: ExecutionBlockHash, + envelope: ExecutionBlockHash, + }, + // The previous randao didn't match the payload + PrevRandaoMismatch { + committed_bid: Hash256, + envelope: Hash256, + }, + // The timestamp didn't match the payload + TimestampMismatch { + state: u64, + envelope: u64, + }, + // Invalid state root + InvalidStateRoot { + state: Hash256, + envelope: Hash256, + }, + // BitFieldError + BitFieldError(ssz::BitfieldError), + // Some kind of error calculating the builder payment index + BuilderPaymentIndexOutOfBounds(usize), + /// The envelope was deemed invalid by the execution engine. + ExecutionInvalid, +} + +impl From for EnvelopeProcessingError { + fn from(e: BeaconStateError) -> Self { + EnvelopeProcessingError::BeaconStateError(e) + } +} + +impl From for EnvelopeProcessingError { + fn from(e: BlockProcessingError) -> Self { + EnvelopeProcessingError::BlockProcessingError(e) + } +} + +impl From for EnvelopeProcessingError { + fn from(e: ArithError) -> Self { + EnvelopeProcessingError::ArithError(e) + } +} + +/// Processes a `SignedExecutionPayloadEnvelope` +/// +/// This function does all the state modifications inside `process_execution_payload()` +pub fn process_execution_payload_envelope( + state: &mut BeaconState, + parent_state_root: Option, + signed_envelope: &SignedExecutionPayloadEnvelope, + verify_signatures: VerifySignatures, + spec: &ChainSpec, +) -> Result<(), EnvelopeProcessingError> { + if verify_signatures.is_true() { + // Verify Signed Envelope Signature + if !signed_envelope.verify_signature_with_state(state, spec)? { + return Err(EnvelopeProcessingError::BadSignature); + } + } + + let envelope = &signed_envelope.message; + let payload = &envelope.payload; + let execution_requests = &envelope.execution_requests; + + // Cache latest block header state root + if state.latest_block_header().state_root == Hash256::default() { + let previous_state_root = parent_state_root + .map(Ok) + .unwrap_or_else(|| state.canonical_root())?; + state.latest_block_header_mut().state_root = previous_state_root; + } + + // Verify consistency with the beacon block + let latest_block_header_root = state.latest_block_header().tree_hash_root(); + envelope_verify!( + envelope.beacon_block_root == latest_block_header_root, + EnvelopeProcessingError::LatestBlockHeaderMismatch { + envelope_root: envelope.beacon_block_root, + block_header_root: latest_block_header_root, + } + ); + envelope_verify!( + envelope.slot == state.slot(), + EnvelopeProcessingError::SlotMismatch { + envelope_slot: envelope.slot, + parent_state_slot: state.slot(), + } + ); + + // Verify consistency with the committed bid + let committed_bid = state.latest_execution_payload_bid()?; + envelope_verify!( + envelope.builder_index == committed_bid.builder_index, + EnvelopeProcessingError::BuilderIndexMismatch { + committed_bid: committed_bid.builder_index, + envelope: envelope.builder_index, + } + ); + envelope_verify!( + committed_bid.prev_randao == payload.prev_randao, + EnvelopeProcessingError::PrevRandaoMismatch { + committed_bid: committed_bid.prev_randao, + envelope: payload.prev_randao, + } + ); + + // Verify consistency with expected withdrawals + // NOTE: we don't bother hashing here except in case of error, because we can just compare for + // equality directly. This equality check could be more straight-forward if the types were + // changed to match (currently we are comparing VariableList to List). This could happen + // coincidentally when we adopt ProgressiveList. + envelope_verify!( + payload.withdrawals.len() == state.payload_expected_withdrawals()?.len() + && payload + .withdrawals + .iter() + .eq(state.payload_expected_withdrawals()?.iter()), + EnvelopeProcessingError::WithdrawalsRootMismatch { + state: state.payload_expected_withdrawals()?.tree_hash_root(), + payload: payload.withdrawals.tree_hash_root(), + } + ); + + // Verify the gas limit + envelope_verify!( + committed_bid.gas_limit == payload.gas_limit, + EnvelopeProcessingError::GasLimitMismatch { + committed_bid: committed_bid.gas_limit, + envelope: payload.gas_limit, + } + ); + + // Verify the block hash + envelope_verify!( + committed_bid.block_hash == payload.block_hash, + EnvelopeProcessingError::BlockHashMismatch { + committed_bid: committed_bid.block_hash, + envelope: payload.block_hash, + } + ); + + // Verify consistency of the parent hash with respect to the previous execution payload + envelope_verify!( + payload.parent_hash == *state.latest_block_hash()?, + EnvelopeProcessingError::ParentHashMismatch { + state: *state.latest_block_hash()?, + envelope: payload.parent_hash, + } + ); + + // Verify timestamp + let state_timestamp = compute_timestamp_at_slot(state, state.slot(), spec)?; + envelope_verify!( + payload.timestamp == state_timestamp, + EnvelopeProcessingError::TimestampMismatch { + state: state_timestamp, + envelope: payload.timestamp, + } + ); + + // TODO(gloas): newPayload happens here in the spec, ensure we wire that up correctly + + process_deposit_requests_post_gloas(state, &execution_requests.deposits, spec)?; + + // TODO(gloas): gotta update these + process_withdrawal_requests(state, &execution_requests.withdrawals, spec)?; + process_consolidation_requests(state, &execution_requests.consolidations, spec)?; + + // Queue the builder payment + let payment_index = E::slots_per_epoch() + .safe_add(state.slot().as_u64().safe_rem(E::slots_per_epoch())?)? + as usize; + let payment_mut = state + .builder_pending_payments_mut()? + .get_mut(payment_index) + .ok_or(EnvelopeProcessingError::BuilderPaymentIndexOutOfBounds( + payment_index, + ))?; + + // We have re-ordered the blanking out of the pending payment to avoid a double-lookup. + // This is semantically equivalent to the ordering used by the spec because we have taken a + // clone of the payment prior to doing the write. + let payment_withdrawal = payment_mut.withdrawal.clone(); + *payment_mut = BuilderPendingPayment::default(); + + let amount = payment_withdrawal.amount; + if amount > 0 { + state + .builder_pending_withdrawals_mut()? + .push(payment_withdrawal) + .map_err(|e| EnvelopeProcessingError::BeaconStateError(e.into()))?; + } + + // Cache the execution payload hash + let availability_index = state + .slot() + .as_usize() + .safe_rem(E::slots_per_historical_root())?; + state + .execution_payload_availability_mut()? + .set(availability_index, true) + .map_err(EnvelopeProcessingError::BitFieldError)?; + *state.latest_block_hash_mut()? = payload.block_hash; + + // Verify the state root + let state_root = state.canonical_root()?; + envelope_verify!( + envelope.state_root == state_root, + EnvelopeProcessingError::InvalidStateRoot { + state: state_root, + envelope: envelope.state_root, + } + ); + + Ok(()) +} diff --git a/consensus/state_processing/src/lib.rs b/consensus/state_processing/src/lib.rs index 9b2696c6d5..e37c526579 100644 --- a/consensus/state_processing/src/lib.rs +++ b/consensus/state_processing/src/lib.rs @@ -20,6 +20,7 @@ pub mod all_caches; pub mod block_replayer; pub mod common; pub mod consensus_context; +pub mod envelope_processing; pub mod epoch_cache; pub mod genesis; pub mod per_block_processing; diff --git a/consensus/state_processing/src/per_block_processing/process_operations.rs b/consensus/state_processing/src/per_block_processing/process_operations.rs index 9db439b543..19109f1508 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -5,6 +5,7 @@ use crate::common::{ slash_validator, }; use crate::per_block_processing::errors::{BlockProcessingError, IntoWithIndex}; +use bls::{PublicKeyBytes, SignatureBytes}; use ssz_types::FixedVector; use typenum::U33; use types::consts::altair::{PARTICIPATION_FLAG_WEIGHTS, PROPOSER_WEIGHT, WEIGHT_DENOMINATOR}; @@ -38,9 +39,14 @@ pub fn process_operations>( process_bls_to_execution_changes(state, bls_to_execution_changes, verify_signatures, spec)?; } - if state.fork_name_unchecked().electra_enabled() { + if state.fork_name_unchecked().electra_enabled() && !state.fork_name_unchecked().gloas_enabled() + { state.update_pubkey_cache()?; - process_deposit_requests(state, &block_body.execution_requests()?.deposits, spec)?; + process_deposit_requests_pre_gloas( + state, + &block_body.execution_requests()?.deposits, + spec, + )?; process_withdrawal_requests(state, &block_body.execution_requests()?.withdrawals, spec)?; process_consolidation_requests( state, @@ -377,6 +383,31 @@ pub fn process_proposer_slashings( verify_proposer_slashing(proposer_slashing, state, verify_signatures, spec) .map_err(|e| e.into_with_index(i))?; + // [New in Gloas:EIP7732] + // Remove the BuilderPendingPayment corresponding to this proposal + // if it is still in the 2-epoch window. + if state.fork_name_unchecked().gloas_enabled() { + let slot = proposer_slashing.signed_header_1.message.slot; + let proposal_epoch = slot.epoch(E::slots_per_epoch()); + let slot_in_epoch = slot.as_usize().safe_rem(E::SlotsPerEpoch::to_usize())?; + + let payment_index = if proposal_epoch == state.current_epoch() { + Some(E::SlotsPerEpoch::to_usize().safe_add(slot_in_epoch)?) + } else if proposal_epoch == state.previous_epoch() { + Some(slot_in_epoch) + } else { + None + }; + + if let Some(index) = payment_index { + let payment = state + .builder_pending_payments_mut()? + .get_mut(index) + .ok_or(BlockProcessingError::BuilderPaymentIndexOutOfBounds(index))?; + *payment = BuilderPendingPayment::default(); + } + } + slash_validator( state, proposer_slashing.signed_header_1.message.proposer_index as usize, @@ -736,7 +767,7 @@ pub fn process_withdrawal_requests( Ok(()) } -pub fn process_deposit_requests( +pub fn process_deposit_requests_pre_gloas( state: &mut BeaconState, deposit_requests: &[DepositRequest], spec: &ChainSpec, @@ -763,6 +794,112 @@ pub fn process_deposit_requests( Ok(()) } +pub fn process_deposit_requests_post_gloas( + state: &mut BeaconState, + deposit_requests: &[DepositRequest], + spec: &ChainSpec, +) -> Result<(), BlockProcessingError> { + for request in deposit_requests { + process_deposit_request_post_gloas(state, request, spec)?; + } + + Ok(()) +} + +pub fn process_deposit_request_post_gloas( + state: &mut BeaconState, + deposit_request: &DepositRequest, + spec: &ChainSpec, +) -> Result<(), BlockProcessingError> { + // [New in Gloas:EIP7732] + // Regardless of the withdrawal credentials prefix, if a builder/validator + // already exists with this pubkey, apply the deposit to their balance + // TODO(gloas): this could be more efficient in the builder case, see: + // https://github.com/sigp/lighthouse/issues/8783 + let builder_index = state + .builders()? + .iter() + .enumerate() + .find(|(_, builder)| builder.pubkey == deposit_request.pubkey) + .map(|(i, _)| i as u64); + let is_builder = builder_index.is_some(); + + let validator_index = state.get_validator_index(&deposit_request.pubkey)?; + let is_validator = validator_index.is_some(); + + let is_builder_prefix = + is_builder_withdrawal_credential(deposit_request.withdrawal_credentials, spec); + + if is_builder || (is_builder_prefix && !is_validator) { + // Apply builder deposits immediately + apply_deposit_for_builder( + state, + builder_index, + deposit_request.pubkey, + deposit_request.withdrawal_credentials, + deposit_request.amount, + deposit_request.signature.clone(), + state.slot(), + spec, + )?; + return Ok(()); + } + + // Add validator deposits to the queue + let slot = state.slot(); + state.pending_deposits_mut()?.push(PendingDeposit { + pubkey: deposit_request.pubkey, + withdrawal_credentials: deposit_request.withdrawal_credentials, + amount: deposit_request.amount, + signature: deposit_request.signature.clone(), + slot, + })?; + + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +pub fn apply_deposit_for_builder( + state: &mut BeaconState, + builder_index_opt: Option, + pubkey: PublicKeyBytes, + withdrawal_credentials: Hash256, + amount: u64, + signature: SignatureBytes, + slot: Slot, + spec: &ChainSpec, +) -> Result<(), BlockProcessingError> { + match builder_index_opt { + None => { + // Verify the deposit signature (proof of possession) which is not checked by the deposit contract + let deposit_data = DepositData { + pubkey, + withdrawal_credentials, + amount, + signature, + }; + if is_valid_deposit_signature(&deposit_data, spec).is_ok() { + state.add_builder_to_registry( + pubkey, + withdrawal_credentials, + amount, + slot, + spec, + )?; + } + } + Some(builder_index) => { + state + .builders_mut()? + .get_mut(builder_index as usize) + .ok_or(BeaconStateError::UnknownBuilder(builder_index))? + .balance + .safe_add_assign(amount)?; + } + } + Ok(()) +} + // Make sure to build the pubkey cache before calling this function pub fn process_consolidation_requests( state: &mut BeaconState, diff --git a/consensus/state_processing/src/per_block_processing/verify_attestation.rs b/consensus/state_processing/src/per_block_processing/verify_attestation.rs index 00105f323c..64b7a31afb 100644 --- a/consensus/state_processing/src/per_block_processing/verify_attestation.rs +++ b/consensus/state_processing/src/per_block_processing/verify_attestation.rs @@ -52,8 +52,6 @@ pub fn verify_attestation_for_block_inclusion<'ctxt, E: EthSpec>( /// /// Returns a descriptive `Err` if the attestation is malformed or does not accurately reflect the /// prior blocks in `state`. -/// -/// Spec v0.12.1 pub fn verify_attestation_for_state<'ctxt, E: EthSpec>( state: &BeaconState, attestation: AttestationRef<'ctxt, E>, @@ -94,8 +92,6 @@ pub fn verify_attestation_for_state<'ctxt, E: EthSpec>( } /// Check target epoch and source checkpoint. -/// -/// Spec v0.12.1 fn verify_casper_ffg_vote( attestation: AttestationRef, state: &BeaconState, diff --git a/consensus/types/src/execution/signed_execution_payload_envelope.rs b/consensus/types/src/execution/signed_execution_payload_envelope.rs index ca48a9ec9b..b1d949f863 100644 --- a/consensus/types/src/execution/signed_execution_payload_envelope.rs +++ b/consensus/types/src/execution/signed_execution_payload_envelope.rs @@ -1,7 +1,8 @@ use crate::test_utils::TestRandom; use crate::{ - ChainSpec, Domain, Epoch, EthSpec, ExecutionBlockHash, ExecutionPayloadEnvelope, Fork, - ForkName, Hash256, SignedRoot, Slot, + BeaconState, BeaconStateError, ChainSpec, Domain, Epoch, EthSpec, ExecutionBlockHash, + ExecutionPayloadEnvelope, Fork, ForkName, Hash256, SignedRoot, Slot, + consts::gloas::BUILDER_INDEX_SELF_BUILD, }; use bls::{PublicKey, Signature}; use context_deserialize::context_deserialize; @@ -58,6 +59,42 @@ impl SignedExecutionPayloadEnvelope { self.signature.verify(pubkey, message) } + + /// Verify `self.signature` using keys drawn from the beacon state. + pub fn verify_signature_with_state( + &self, + state: &BeaconState, + spec: &ChainSpec, + ) -> Result { + let builder_index = self.message.builder_index; + + let pubkey_bytes = if builder_index == BUILDER_INDEX_SELF_BUILD { + let validator_index = state.latest_block_header().proposer_index; + state.get_validator(validator_index as usize)?.pubkey + } else { + state.get_builder(builder_index)?.pubkey + }; + + // TODO(gloas): Could use pubkey cache on state here, but it probably isn't worth + // it because this function is rarely used. Almost always the envelope should be signature + // verified prior to consensus code running. + let pubkey = pubkey_bytes.decompress()?; + + // Ensure the state's epoch matches the message's epoch before determining the Fork. + if self.epoch() != state.current_epoch() { + return Err(BeaconStateError::SignedEnvelopeIncorrectEpoch { + state_epoch: state.current_epoch(), + envelope_epoch: self.epoch(), + }); + } + + Ok(self.verify_signature( + &pubkey, + &state.fork(), + state.genesis_validators_root(), + spec, + )) + } } #[cfg(test)] diff --git a/consensus/types/src/state/beacon_state.rs b/consensus/types/src/state/beacon_state.rs index 2720745b01..1352ded79e 100644 --- a/consensus/types/src/state/beacon_state.rs +++ b/consensus/types/src/state/beacon_state.rs @@ -23,7 +23,7 @@ use tree_hash_derive::TreeHash; use typenum::Unsigned; use crate::{ - ExecutionBlockHash, ExecutionPayloadBid, Withdrawal, + Address, ExecutionBlockHash, ExecutionPayloadBid, Withdrawal, attestation::{ AttestationData, AttestationDuty, BeaconCommittee, Checkpoint, CommitteeIndex, PTC, ParticipationFlags, PendingAttestation, @@ -174,6 +174,8 @@ pub enum BeaconStateError { MerkleTreeError(merkle_proof::MerkleTreeError), PartialWithdrawalCountInvalid(usize), NonExecutionAddressWithdrawalCredential, + WithdrawalCredentialMissingVersion, + WithdrawalCredentialMissingAddress, NoCommitteeFound(CommitteeIndex), InvalidCommitteeIndex(CommitteeIndex), /// `Attestation.data.index` field is invalid in overloaded data index scenario. @@ -199,6 +201,10 @@ pub enum BeaconStateError { ProposerLookaheadOutOfBounds { i: usize, }, + SignedEnvelopeIncorrectEpoch { + state_epoch: Epoch, + envelope_epoch: Epoch, + }, InvalidIndicesCount, InvalidExecutionPayloadAvailabilityIndex(usize), } @@ -1920,6 +1926,15 @@ impl BeaconState { .ok_or(BeaconStateError::UnknownValidator(validator_index)) } + /// Safe indexer for the `builders` list. + /// + /// Will return an error pre-Gloas, or for out-of-bounds indices. + pub fn get_builder(&self, builder_index: BuilderIndex) -> Result<&Builder, BeaconStateError> { + self.builders()? + .get(builder_index as usize) + .ok_or(BeaconStateError::UnknownBuilder(builder_index)) + } + /// Add a validator to the registry and return the validator index that was allocated for it. pub fn add_validator_to_registry( &mut self, @@ -1966,6 +1981,64 @@ impl BeaconState { Ok(index) } + /// Add a builder to the registry and return the builder index that was allocated for it. + pub fn add_builder_to_registry( + &mut self, + pubkey: PublicKeyBytes, + withdrawal_credentials: Hash256, + amount: u64, + slot: Slot, + spec: &ChainSpec, + ) -> Result { + // We are not yet using the spec's `set_or_append_list`, but could consider it if it crops + // up elsewhere. It has been retconned into the spec to support index reuse but so far + // index reuse is only relevant for builders. + let builder_index = self.get_index_for_new_builder()?; + let builders = self.builders_mut()?; + + let version = *withdrawal_credentials + .as_slice() + .first() + .ok_or(BeaconStateError::WithdrawalCredentialMissingVersion)?; + let execution_address = withdrawal_credentials + .as_slice() + .get(12..) + .and_then(|bytes| Address::try_from(bytes).ok()) + .ok_or(BeaconStateError::WithdrawalCredentialMissingAddress)?; + + let builder = Builder { + pubkey, + version, + execution_address, + balance: amount, + deposit_epoch: slot.epoch(E::slots_per_epoch()), + withdrawable_epoch: spec.far_future_epoch, + }; + + if builder_index == builders.len() as u64 { + builders.push(builder)?; + } else { + *builders + .get_mut(builder_index as usize) + .ok_or(BeaconStateError::UnknownBuilder(builder_index))? = builder; + } + Ok(builder_index) + } + + // TODO(gloas): Optimize this function if we see a lot of registered builders on-chain. + // A cache here could be quite fiddly because this calculation depends on withdrawable epoch + // and balance - a cache for this would need to be updated whenever either of those fields + // changes. + pub fn get_index_for_new_builder(&self) -> Result { + let current_epoch = self.current_epoch(); + for (index, builder) in self.builders()?.iter().enumerate() { + if builder.withdrawable_epoch <= current_epoch && builder.balance == 0 { + return Ok(index as u64); + } + } + Ok(self.builders()?.len() as u64) + } + /// Safe copy-on-write accessor for the `validators` list. pub fn get_validator_cow( &mut self, diff --git a/consensus/types/src/validator/mod.rs b/consensus/types/src/validator/mod.rs index 8a67407821..23f7a2a0e1 100644 --- a/consensus/types/src/validator/mod.rs +++ b/consensus/types/src/validator/mod.rs @@ -4,6 +4,8 @@ mod validator_registration_data; mod validator_subscription; pub use proposer_preparation_data::ProposerPreparationData; -pub use validator::{Validator, is_compounding_withdrawal_credential}; +pub use validator::{ + Validator, is_builder_withdrawal_credential, is_compounding_withdrawal_credential, +}; pub use validator_registration_data::{SignedValidatorRegistrationData, ValidatorRegistrationData}; pub use validator_subscription::ValidatorSubscription; diff --git a/consensus/types/src/validator/validator.rs b/consensus/types/src/validator/validator.rs index 7898ab9073..5c5bfc761f 100644 --- a/consensus/types/src/validator/validator.rs +++ b/consensus/types/src/validator/validator.rs @@ -319,6 +319,14 @@ pub fn is_compounding_withdrawal_credential( .unwrap_or(false) } +pub fn is_builder_withdrawal_credential(withdrawal_credentials: Hash256, spec: &ChainSpec) -> bool { + withdrawal_credentials + .as_slice() + .first() + .map(|prefix_byte| *prefix_byte == spec.builder_withdrawal_prefix_byte) + .unwrap_or(false) +} + #[cfg(test)] mod tests { use super::*; diff --git a/testing/ef_tests/check_all_files_accessed.py b/testing/ef_tests/check_all_files_accessed.py index ed220fbe8c..8e5bd24d24 100755 --- a/testing/ef_tests/check_all_files_accessed.py +++ b/testing/ef_tests/check_all_files_accessed.py @@ -48,19 +48,9 @@ excluded_paths = [ "tests/.*/eip7732", "tests/.*/eip7805", # TODO(gloas): remove these ignores as more Gloas operations are implemented - "tests/.*/gloas/operations/attester_slashing/.*", "tests/.*/gloas/operations/block_header/.*", - "tests/.*/gloas/operations/bls_to_execution_change/.*", - "tests/.*/gloas/operations/consolidation_request/.*", - "tests/.*/gloas/operations/deposit/.*", - "tests/.*/gloas/operations/deposit_request/.*", - "tests/.*/gloas/operations/execution_payload/.*", "tests/.*/gloas/operations/execution_payload_bid/.*", "tests/.*/gloas/operations/payload_attestation/.*", - "tests/.*/gloas/operations/proposer_slashing/.*", - "tests/.*/gloas/operations/sync_aggregate/.*", - "tests/.*/gloas/operations/voluntary_exit/.*", - "tests/.*/gloas/operations/withdrawal_request/.*", # TODO(EIP-7732): remove these ignores as Gloas consensus is implemented "tests/.*/gloas/epoch_processing/.*", "tests/.*/gloas/finality/.*", diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index 9133378ac5..ef998a94ba 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -7,10 +7,12 @@ use ssz::Decode; use state_processing::common::update_progressive_balances_cache::initialize_progressive_balances_cache; use state_processing::epoch_cache::initialize_epoch_cache; use state_processing::per_block_processing::process_operations::{ - process_consolidation_requests, process_deposit_requests, process_withdrawal_requests, + process_consolidation_requests, process_deposit_requests_post_gloas, + process_deposit_requests_pre_gloas, process_withdrawal_requests, }; use state_processing::{ ConsensusContext, + envelope_processing::{EnvelopeProcessingError, process_execution_payload_envelope}, per_block_processing::{ VerifyBlockRoot, VerifySignatures, errors::BlockProcessingError, @@ -29,7 +31,7 @@ use types::{ BeaconBlockBodyCapella, BeaconBlockBodyDeneb, BeaconBlockBodyElectra, BeaconBlockBodyFulu, BeaconState, BlindedPayload, ConsolidationRequest, Deposit, DepositRequest, ExecutionPayload, ForkVersionDecode, FullPayload, ProposerSlashing, SignedBlsToExecutionChange, - SignedVoluntaryExit, SyncAggregate, WithdrawalRequest, + SignedExecutionPayloadEnvelope, SignedVoluntaryExit, SyncAggregate, WithdrawalRequest, }; #[derive(Debug, Clone, Default, Deserialize)] @@ -59,6 +61,8 @@ pub struct Operations> { } pub trait Operation: Debug + Sync + Sized { + type Error: Debug; + fn handler_name() -> String; fn filename() -> String { @@ -76,10 +80,12 @@ pub trait Operation: Debug + Sync + Sized { state: &mut BeaconState, spec: &ChainSpec, _: &Operations, - ) -> Result<(), BlockProcessingError>; + ) -> Result<(), Self::Error>; } impl Operation for Attestation { + type Error = BlockProcessingError; + fn handler_name() -> String { "attestation".into() } @@ -132,6 +138,8 @@ impl Operation for Attestation { } impl Operation for AttesterSlashing { + type Error = BlockProcessingError; + fn handler_name() -> String { "attester_slashing".into() } @@ -163,6 +171,8 @@ impl Operation for AttesterSlashing { } impl Operation for Deposit { + type Error = BlockProcessingError; + fn handler_name() -> String { "deposit".into() } @@ -187,6 +197,8 @@ impl Operation for Deposit { } impl Operation for ProposerSlashing { + type Error = BlockProcessingError; + fn handler_name() -> String { "proposer_slashing".into() } @@ -214,6 +226,8 @@ impl Operation for ProposerSlashing { } impl Operation for SignedVoluntaryExit { + type Error = BlockProcessingError; + fn handler_name() -> String { "voluntary_exit".into() } @@ -238,6 +252,8 @@ impl Operation for SignedVoluntaryExit { } impl Operation for BeaconBlock { + type Error = BlockProcessingError; + fn handler_name() -> String { "block_header".into() } @@ -269,6 +285,8 @@ impl Operation for BeaconBlock { } impl Operation for SyncAggregate { + type Error = BlockProcessingError; + fn handler_name() -> String { "sync_aggregate".into() } @@ -297,6 +315,8 @@ impl Operation for SyncAggregate { } impl Operation for BeaconBlockBody> { + type Error = BlockProcessingError; + fn handler_name() -> String { "execution_payload".into() } @@ -306,7 +326,7 @@ impl Operation for BeaconBlockBody> { } fn is_enabled_for_fork(fork_name: ForkName) -> bool { - fork_name.bellatrix_enabled() + fork_name.bellatrix_enabled() && !fork_name.gloas_enabled() } fn decode(path: &Path, fork_name: ForkName, _spec: &ChainSpec) -> Result { @@ -317,8 +337,7 @@ impl Operation for BeaconBlockBody> { ForkName::Deneb => BeaconBlockBody::Deneb(<_>::from_ssz_bytes(bytes)?), ForkName::Electra => BeaconBlockBody::Electra(<_>::from_ssz_bytes(bytes)?), ForkName::Fulu => BeaconBlockBody::Fulu(<_>::from_ssz_bytes(bytes)?), - // TODO(EIP-7732): See if we need to handle Gloas here - _ => panic!(), + _ => panic!("Not supported after Gloas"), }) }) } @@ -340,7 +359,10 @@ impl Operation for BeaconBlockBody> { } } } + impl Operation for BeaconBlockBody> { + type Error = BlockProcessingError; + fn handler_name() -> String { "execution_payload".into() } @@ -350,7 +372,7 @@ impl Operation for BeaconBlockBody> { } fn is_enabled_for_fork(fork_name: ForkName) -> bool { - fork_name.bellatrix_enabled() + fork_name.bellatrix_enabled() && !fork_name.gloas_enabled() } fn decode(path: &Path, fork_name: ForkName, _spec: &ChainSpec) -> Result { @@ -377,8 +399,7 @@ impl Operation for BeaconBlockBody> { let inner = >>::from_ssz_bytes(bytes)?; BeaconBlockBody::Fulu(inner.clone_as_blinded()) } - // TODO(EIP-7732): See if we need to handle Gloas here - _ => panic!(), + _ => panic!("Not supported after Gloas"), }) }) } @@ -401,7 +422,46 @@ impl Operation for BeaconBlockBody> { } } +impl Operation for SignedExecutionPayloadEnvelope { + type Error = EnvelopeProcessingError; + + fn handler_name() -> String { + "execution_payload".into() + } + + fn filename() -> String { + "signed_envelope.ssz_snappy".into() + } + + fn is_enabled_for_fork(fork_name: ForkName) -> bool { + fork_name.gloas_enabled() + } + + fn decode(path: &Path, _: ForkName, _spec: &ChainSpec) -> Result { + ssz_decode_file(path) + } + + fn apply_to( + &self, + state: &mut BeaconState, + spec: &ChainSpec, + extra: &Operations, + ) -> Result<(), Self::Error> { + let valid = extra + .execution_metadata + .as_ref() + .is_some_and(|e| e.execution_valid); + if valid { + process_execution_payload_envelope(state, None, self, VerifySignatures::True, spec) + } else { + Err(EnvelopeProcessingError::ExecutionInvalid) + } + } +} + impl Operation for WithdrawalsPayload { + type Error = BlockProcessingError; + fn handler_name() -> String { "withdrawals".into() } @@ -448,6 +508,8 @@ impl Operation for WithdrawalsPayload { } impl Operation for SignedBlsToExecutionChange { + type Error = BlockProcessingError; + fn handler_name() -> String { "bls_to_execution_change".into() } @@ -480,6 +542,8 @@ impl Operation for SignedBlsToExecutionChange { } impl Operation for WithdrawalRequest { + type Error = BlockProcessingError; + fn handler_name() -> String { "withdrawal_request".into() } @@ -504,6 +568,8 @@ impl Operation for WithdrawalRequest { } impl Operation for DepositRequest { + type Error = BlockProcessingError; + fn handler_name() -> String { "deposit_request".into() } @@ -522,11 +588,17 @@ impl Operation for DepositRequest { spec: &ChainSpec, _extra: &Operations, ) -> Result<(), BlockProcessingError> { - process_deposit_requests(state, std::slice::from_ref(self), spec) + if state.fork_name_unchecked().gloas_enabled() { + process_deposit_requests_post_gloas(state, std::slice::from_ref(self), spec) + } else { + process_deposit_requests_pre_gloas(state, std::slice::from_ref(self), spec) + } } } impl Operation for ConsolidationRequest { + type Error = BlockProcessingError; + fn handler_name() -> String { "consolidation_request".into() } diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index 39ddff46e7..9d11252edb 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -1135,11 +1135,20 @@ impl> Handler for OperationsHandler } fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { - // TODO(gloas): So far only withdrawals tests are enabled for Gloas. Self::Case::is_enabled_for_fork(fork_name) && (!fork_name.gloas_enabled() + || self.handler_name() == "attestation" + || self.handler_name() == "attester_slashing" + || self.handler_name() == "bls_to_execution_change" + || self.handler_name() == "consolidation_request" + || self.handler_name() == "deposit_request" + || self.handler_name() == "deposit" + || self.handler_name() == "execution_payload" + || self.handler_name() == "proposer_slashing" + || self.handler_name() == "sync_aggregate" + || self.handler_name() == "withdrawal_request" || self.handler_name() == "withdrawals" - || self.handler_name() == "attestation") + || self.handler_name() == "voluntary_exit") } } diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index b47d39a6fa..8a53a61929 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -87,6 +87,12 @@ fn operations_execution_payload_blinded() { OperationsHandler::>>::default().run(); } +#[test] +fn operations_execution_payload_envelope() { + OperationsHandler::>::default().run(); + OperationsHandler::>::default().run(); +} + #[test] fn operations_withdrawals() { OperationsHandler::>::default().run(); @@ -94,7 +100,7 @@ fn operations_withdrawals() { } #[test] -fn operations_withdrawal_reqeusts() { +fn operations_withdrawal_requests() { OperationsHandler::::default().run(); OperationsHandler::::default().run(); } From 8948159a4039c8f74032493705319de840810d2b Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Tue, 10 Feb 2026 17:41:57 +1100 Subject: [PATCH 02/81] Add AI assistant documentation and commands (#8785) * Add AI assistant documentation and commands Adds structured documentation for AI coding assistants: - CLAUDE.md / AGENTS.md: Lightweight entry points with critical rules - .ai/: Shared knowledge base (CODE_REVIEW.md, DEVELOPMENT.md, ISSUES.md) - .claude/commands/: Claude Code skills for review, issue, release - .github/copilot-instructions.md: GitHub Copilot instructions Supports Claude Code, OpenAI Codex, and GitHub Copilot with modular, pointer-based structure for maintainability. Includes guidelines for AI assistants to prompt developers about updating these docs after receiving feedback, creating a continuous improvement loop. * Add parallel development tip with git worktrees * Address review feedback - Add missing details to DEVELOPMENT.md: fork-specific testing, database backends, cross-compilation targets, make test-release - Simplify AGENTS.md to pointer to CLAUDE.md (Codex can read files) * Address review feedback - Add priority signaling: Critical vs Important vs Good Practices - Restore actionable file references (canonical_head.rs, test_utils.rs, etc.) - Add Rayon CPU oversubscription context - Add tracing span guidelines - Simplify AGENTS.md to pointer * Address review feedback and remove Copilot instructions - Restore anti-patterns section (over-engineering, unnecessary complexity) - Restore design principles (simplicity first, high cohesion) - Add architecture guidance (dependency bloat, schema migrations, backwards compat) - Improve natural language guidance for AI comments - Add try_read lock pattern - Remove copilot-instructions.md (can't follow file refs, untestable) --- .ai/CODE_REVIEW.md | 277 +++++++++++++++++++++++ .ai/DEVELOPMENT.md | 200 +++++++++++++++++ .ai/ISSUES.md | 130 +++++++++++ .claude/commands/issue.md | 49 +++++ .claude/commands/release.md | 85 ++++++++ .claude/commands/review.md | 57 +++++ AGENTS.md | 10 + CLAUDE.md | 425 +++++++++++------------------------- 8 files changed, 930 insertions(+), 303 deletions(-) create mode 100644 .ai/CODE_REVIEW.md create mode 100644 .ai/DEVELOPMENT.md create mode 100644 .ai/ISSUES.md create mode 100644 .claude/commands/issue.md create mode 100644 .claude/commands/release.md create mode 100644 .claude/commands/review.md create mode 100644 AGENTS.md diff --git a/.ai/CODE_REVIEW.md b/.ai/CODE_REVIEW.md new file mode 100644 index 0000000000..e4da3b22d5 --- /dev/null +++ b/.ai/CODE_REVIEW.md @@ -0,0 +1,277 @@ +# Lighthouse Code Review Guidelines + +Code review guidelines based on patterns from Lighthouse maintainers. + +## Core Principles + +- **Correctness** over clever code +- **Clarity** through good documentation and naming +- **Safety** through proper error handling and panic avoidance +- **Maintainability** for long-term health + +## Critical: Consensus Crate (`consensus/` excluding `types/`) + +**Extra scrutiny required** - bugs here cause consensus failures. + +### Requirements + +1. **Safe Math Only** + ```rust + // NEVER + let result = a + b; + + // ALWAYS + let result = a.saturating_add(b); + // or use safe_arith crate + let result = a.safe_add(b)?; + ``` + +2. **Zero Panics** + - No `.unwrap()`, `.expect()`, array indexing `[i]` + - Return `Result` or `Option` instead + +3. **Deterministic Behavior** + - Identical results across all platforms + - No undefined behavior + +## Panic Avoidance (All Code) + +```rust +// NEVER at runtime +let value = option.unwrap(); +let item = array[1]; + +// ALWAYS +let value = option.ok_or(Error::Missing)?; +let item = array.get(1)?; + +// Only acceptable during startup for CLI/config validation +let flag = matches.get_one::("flag") + .expect("Required due to clap validation"); +``` + +## Code Clarity + +### Variable Naming +```rust +// BAD - ambiguous +let bb = ...; +let bl = ...; + +// GOOD - clear +let beacon_block = ...; +let blob = ...; +``` + +### Comments +- Explain the "why" not just the "what" +- All `TODO` comments must link to a GitHub issue +- Remove dead/commented-out code + +## Error Handling + +### Don't Silently Swallow Errors +```rust +// BAD +self.store.get_info().unwrap_or(None) + +// GOOD +self.store.get_info().unwrap_or_else(|e| { + error!(self.log, "Failed to read info"; "error" => ?e); + None +}) +``` + +### Check Return Values +Ask: "What happens if this returns `Ok(Failed)`?" Don't ignore results that might indicate failure. + +## Performance & Concurrency + +### Lock Safety +- Document lock ordering requirements +- Keep lock scopes narrow +- Seek detailed review for lock-related changes +- Use `try_read` when falling back to an alternative is acceptable +- Use blocking `read` when alternative is more expensive (e.g., state reconstruction) + +### Async Patterns +```rust +// NEVER block in async context +async fn handler() { + expensive_computation(); // blocks runtime +} + +// ALWAYS spawn blocking +async fn handler() { + tokio::task::spawn_blocking(|| expensive_computation()).await?; +} +``` + +### Rayon +- Use scoped rayon pools from beacon processor +- Avoid global thread pool (causes CPU oversubscription) + +## Review Process + +### Focus on Actionable Issues + +**Limit to 3-5 key comments.** Prioritize: +1. Correctness issues - bugs, race conditions, panics +2. Missing test coverage - especially edge cases +3. Complex logic needing documentation +4. API design concerns + +**Don't comment on:** +- Minor style issues +- Things caught by CI (formatting, linting) +- Nice-to-haves that aren't important + +### Keep Comments Natural and Minimal + +**Tone**: Natural and conversational, not robotic. + +**Good review comment:** +``` +Missing test coverage for the None blobs path. The existing test at +`store_tests.rs:2874` still provides blobs. Should add a test passing +None to verify backfill handles this correctly. +``` + +**Good follow-up after author addresses comments:** +``` +LGTM, thanks! +``` +or +``` +Thanks for the updates, looks good! +``` + +**Avoid:** +- Checklists or structured formatting (✅ Item 1 fixed...) +- Repeating what was fixed (makes it obvious it's AI-generated) +- Headers, subsections, "Summary" sections +- Verbose multi-paragraph explanations + +### Use Natural Language + +``` +BAD (prescriptive): +"This violates coding standards which strictly prohibit runtime panics." + +GOOD (conversational): +"Should we avoid `.expect()` here? This gets called in hot paths and +we typically try to avoid runtime panics outside of startup." +``` + +### Verify Before Commenting + +- If CI passes, trust it - types/imports must exist +- Check the full diff, not just visible parts +- Ask for verification rather than asserting things are missing + +## Common Review Patterns + +### Fork-Specific Changes +- Verify production fork code path unchanged +- Check SSZ compatibility (field order) +- Verify rollback/error paths handle edge cases + +### API Design +- Constructor signatures should be consistent +- Avoid `Option` parameters when value is always required + +### Concurrency +- Lock ordering documented? +- Potential deadlocks? +- Race conditions? + +### Error Handling +- Errors logged? +- Edge cases handled? +- Context provided with errors? + +## Deep Review Techniques + +### Verify Against Specifications +- Read the actual spec in `./consensus-specs/` +- Compare formulas exactly +- Check constant values match spec definitions + +### Trace Data Flow End-to-End +For new config fields: +1. Config file - Does YAML contain the field? +2. Config struct - Is it parsed with serde attributes? +3. apply_to_chain_spec - Is it actually applied? +4. Runtime usage - Used correctly everywhere? + +### Check Error Handling Fallbacks +Examine every `.unwrap_or()`, `.unwrap_or_else()`: +- If the fallback triggers, does code behave correctly? +- Does it silently degrade or fail loudly? + +### Look for Incomplete Migrations +When a PR changes a pattern across the codebase: +- Search for old pattern - all occurrences updated? +- Check test files - often lag behind implementation + +## Architecture & Design + +### Avoid Dependency Bloat +- Question whether imports add unnecessary dependencies +- Consider feature flags for optional functionality +- Large imports when only primitives are needed may warrant a `core` or `primitives` feature + +### Schema Migrations +- Database schema changes require migrations +- Don't forget to add migration code when changing stored types +- Review pattern: "Needs a schema migration" + +### Backwards Compatibility +- Consider existing users when changing behavior +- Document breaking changes clearly +- Prefer additive changes when possible + +## Anti-Patterns to Avoid + +### Over-Engineering +- Don't add abstractions until needed +- Keep solutions simple and focused +- "Three similar lines of code is better than a premature abstraction" + +### Unnecessary Complexity +- Avoid feature flags for simple changes +- Don't add fallbacks for scenarios that can't happen +- Trust internal code and framework guarantees + +### Premature Optimization +- Optimize hot paths based on profiling, not assumptions +- Document performance considerations but don't over-optimize + +### Hiding Important Information +- Don't use generic variable names when specific ones are clearer +- Don't skip logging just to keep code shorter +- Don't omit error context + +## Design Principles + +### Simplicity First +Question every layer of abstraction: +- Is this `Arc` needed, or is the inner type already `Clone`? +- Is this `Mutex` needed, or can ownership be restructured? +- Is this wrapper type adding value or just indirection? + +If you can't articulate why a layer of abstraction exists, it probably shouldn't. + +### High Cohesion +Group related state and behavior together. If two fields are always set together, used together, and invalid without each other, they belong in a struct. + +## Before Approval Checklist + +- [ ] No panics: No `.unwrap()`, `.expect()`, unchecked array indexing +- [ ] Consensus safe: If touching consensus crate, all arithmetic is safe +- [ ] Errors logged: Not silently swallowed +- [ ] Clear naming: Variable names are unambiguous +- [ ] TODOs linked: All TODOs have GitHub issue links +- [ ] Tests present: Non-trivial changes have tests +- [ ] Lock safety: Lock ordering is safe and documented +- [ ] No blocking: Async code doesn't block runtime diff --git a/.ai/DEVELOPMENT.md b/.ai/DEVELOPMENT.md new file mode 100644 index 0000000000..1204f21ead --- /dev/null +++ b/.ai/DEVELOPMENT.md @@ -0,0 +1,200 @@ +# Lighthouse Development Guide + +Development patterns, commands, and architecture for AI assistants and contributors. + +## Development Commands + +**Important**: Always branch from `unstable` and target `unstable` when creating pull requests. + +### Building + +- `make install` - Build and install Lighthouse in release mode +- `make install-lcli` - Build and install `lcli` utility +- `cargo build --release` - Standard release build +- `cargo build --bin lighthouse --features "gnosis,slasher-lmdb"` - Build with specific features + +### Testing + +- `make test` - Full test suite in release mode +- `make test-release` - Run tests using nextest (faster parallel runner) +- `cargo nextest run -p ` - Run tests for specific package (preferred for iteration) +- `cargo nextest run -p ` - Run individual test +- `FORK_NAME=electra cargo nextest run -p beacon_chain` - Run tests for specific fork +- `make test-ef` - Ethereum Foundation test vectors + +**Fork-specific testing**: `beacon_chain` and `http_api` tests support fork-specific testing via `FORK_NAME` env var when `beacon_chain/fork_from_env` feature is enabled. + +**Note**: Full test suite takes ~20 minutes. Prefer targeted tests when iterating. + +### Linting + +- `make lint` - Run Clippy with project rules +- `make lint-full` - Comprehensive linting including tests +- `cargo fmt --all && make lint-fix` - Format and fix linting issues +- `cargo sort` - Sort dependencies (enforced on CI) + +## Architecture Overview + +Lighthouse is a modular Ethereum consensus client with two main components: + +### Beacon Node (`beacon_node/`) + +- Main consensus client syncing with Ethereum network +- Beacon chain state transition logic (`beacon_node/beacon_chain/`) +- Networking, storage, P2P communication +- HTTP API for validator clients +- Entry point: `beacon_node/src/lib.rs` + +### Validator Client (`validator_client/`) + +- Manages validator keystores and duties +- Block proposals, attestations, sync committee duties +- Slashing protection and doppelganger detection +- Entry point: `validator_client/src/lib.rs` + +### Key Subsystems + +| Subsystem | Location | Purpose | +|-----------|----------|---------| +| Consensus Types | `consensus/types/` | Core data structures, SSZ encoding | +| Storage | `beacon_node/store/` | Hot/cold database (LevelDB, RocksDB, REDB backends) | +| Networking | `beacon_node/lighthouse_network/` | Libp2p, gossipsub, discovery | +| Fork Choice | `consensus/fork_choice/` | Proto-array fork choice | +| Execution Layer | `beacon_node/execution_layer/` | EL client integration | +| Slasher | `slasher/` | Optional slashing detection | + +### Utilities + +- `account_manager/` - Validator account management +- `lcli/` - Command-line debugging utilities +- `database_manager/` - Database maintenance tools + +## Code Quality Standards + +### Panic Avoidance (Critical) + +**Panics should be avoided at all costs.** + +```rust +// NEVER at runtime +let value = some_result.unwrap(); +let item = array[1]; + +// ALWAYS prefer +let value = some_result?; +let item = array.get(1)?; + +// Only acceptable during startup +let config = matches.get_one::("flag") + .expect("Required due to clap validation"); +``` + +### Consensus Crate Safety (`consensus/` excluding `types/`) + +Extra scrutiny required - bugs here cause consensus failures. + +```rust +// NEVER standard arithmetic +let result = a + b; + +// ALWAYS safe math +let result = a.saturating_add(b); +// or +use safe_arith::SafeArith; +let result = a.safe_add(b)?; +``` + +Requirements: +- Use `saturating_*` or `checked_*` operations +- Zero panics - no `.unwrap()`, `.expect()`, or `array[i]` +- Deterministic behavior across all platforms + +### Error Handling + +- Return `Result` or `Option` instead of panicking +- Log errors, don't silently swallow them +- Provide context with errors + +### Async Patterns + +```rust +// NEVER block in async context +async fn handler() { + expensive_computation(); // blocks runtime +} + +// ALWAYS spawn blocking +async fn handler() { + tokio::task::spawn_blocking(|| expensive_computation()).await?; +} +``` + +### Concurrency + +- **Lock ordering**: Document lock ordering to avoid deadlocks. See [`canonical_head.rs:9-32`](beacon_node/beacon_chain/src/canonical_head.rs) for excellent example documenting three locks and safe acquisition order. +- Keep lock scopes narrow +- Seek detailed review for lock-related changes + +### Rayon Thread Pools + +Avoid using the rayon global thread pool - it causes CPU oversubscription when beacon processor has fully allocated all CPUs to workers. Use scoped rayon pools started by beacon processor for computationally intensive tasks. + +### Tracing Spans + +- Avoid spans on simple getter methods (performance overhead) +- Be cautious of span explosion with recursive functions +- Use spans per meaningful computation step, not every function +- **Never** use `span.enter()` or `span.entered()` in async tasks + +### Documentation + +- All `TODO` comments must link to a GitHub issue +- Prefer line comments (`//`) over block comments +- Keep comments concise, explain "why" not "what" + +## Logging Levels + +| Level | Use Case | +|-------|----------| +| `crit` | Lighthouse may not function - needs immediate attention | +| `error` | Moderate impact - expect user reports | +| `warn` | Unexpected but recoverable | +| `info` | High-level status - not excessive | +| `debug` | Developer events, expected errors | + +## Testing Patterns + +- **Unit tests**: Single component edge cases +- **Integration tests**: Use [`BeaconChainHarness`](beacon_node/beacon_chain/src/test_utils.rs) for end-to-end workflows +- **Sync components**: Use [`TestRig`](beacon_node/network/src/sync/tests/mod.rs) pattern with event-based testing +- **Mocking**: `mockall` for unit tests, `mockito` for HTTP APIs +- **Adapter pattern**: For testing `BeaconChain` dependent components, create adapter structs. See [`fetch_blobs/tests.rs`](beacon_node/beacon_chain/src/fetch_blobs/tests.rs) +- **Local testnet**: See `scripts/local_testnet/README.md` + +## Build Notes + +- Full builds take 5+ minutes - use large timeouts (300s+) +- Use `cargo check` for faster iteration +- MSRV documented in `Cargo.toml` + +### Cross-compilation + +- `make build-x86_64` - Cross-compile for x86_64 Linux +- `make build-aarch64` - Cross-compile for ARM64 Linux +- `make build-riscv64` - Cross-compile for RISC-V 64-bit Linux + +## Parallel Development + +For working on multiple branches simultaneously, use git worktrees: + +```bash +git worktree add -b my-feature ../lighthouse-my-feature unstable +``` + +This creates a separate working directory without needing multiple clones. To save disk space across worktrees, configure a shared target directory: + +```bash +# In .cargo/config.toml at your workspace root +[build] +target-dir = "/path/to/shared-target" +``` diff --git a/.ai/ISSUES.md b/.ai/ISSUES.md new file mode 100644 index 0000000000..ce79198b4d --- /dev/null +++ b/.ai/ISSUES.md @@ -0,0 +1,130 @@ +# GitHub Issue & PR Guidelines + +Guidelines for creating well-structured GitHub issues and PRs for Lighthouse. + +## Issue Structure + +### Start with Description + +Always begin with `## Description`: + +```markdown +## Description + +We presently prune all knowledge of non-canonical blocks once they conflict with +finalization. The pruning is not always immediate, fork choice currently prunes +once the number of nodes reaches a threshold of 256. + +It would be nice to develop a simple system for handling messages relating to +blocks that are non-canonical. +``` + +**Guidelines:** +- First paragraph: problem and brief solution +- Provide context about current behavior +- Link to related issues, PRs, or specs +- Be technical and specific + +### Steps to Resolve (when applicable) + +```markdown +## Steps to resolve + +I see two ways to fix this: a strict approach, and a pragmatic one. + +The strict approach would only check once the slot is finalized. This would have +0 false positives, but would be slower to detect missed blocks. + +The pragmatic approach might be to only process `BeaconState`s from the canonical +chain. I don't have a strong preference between approaches. +``` + +**Guidelines:** +- Don't be overly prescriptive - present options +- Mention relevant constraints +- It's okay to say "I don't have a strong preference" + +### Optional Sections + +- `## Additional Info` - Edge cases, related issues +- `## Metrics` - Performance data, observations +- `## Version` - For bug reports + +## Code References + +**Use GitHub permalinks with commit hashes** so code renders properly: + +``` +https://github.com/sigp/lighthouse/blob/261322c3e3ee/beacon_node/beacon_processor/src/lib.rs#L809 +``` + +Get commit hash: `git rev-parse unstable` + +For line ranges: `#L809-L825` + +## Writing Style + +### Be Natural and Concise +- Direct and objective +- Precise technical terminology +- Avoid AI-sounding language + +### Be Honest About Uncertainty +- Don't guess - ask questions +- Use tentative language when appropriate ("might", "I think") +- Present multiple options without picking one + +### Think About Trade-offs +- Present multiple approaches +- Discuss pros and cons +- Consider backward compatibility +- Note performance implications + +## Labels + +**Type:** `bug`, `enhancement`, `optimization`, `code-quality`, `security`, `RFC` + +**Component:** `database`, `HTTP-API`, `fork-choice`, `beacon-processor`, etc. + +**Effort:** `good first issue`, `low-hanging-fruit`, `major-task` + +## Pull Request Guidelines + +```markdown +## Description + +[What does this PR do? Why is it needed? Be concise and technical.] + +Closes #[issue-number] + +## Additional Info + +[Breaking changes, performance impacts, migration steps, etc.] +``` + +### Commit Messages + +Format: +- First line: Brief summary (imperative mood) +- Blank line +- Additional details if needed + +``` +Add custody info API for data columns + +Implements `/lighthouse/custody/info` endpoint that returns custody group +count, custodied columns, and earliest available data column slot. +``` + +## Anti-Patterns + +- Vague descriptions without details +- No code references when describing code +- Premature solutions without understanding the problem +- Making claims without validating against codebase + +## Good Examples + +- https://github.com/sigp/lighthouse/issues/6120 +- https://github.com/sigp/lighthouse/issues/4388 +- https://github.com/sigp/lighthouse/issues/8216 diff --git a/.claude/commands/issue.md b/.claude/commands/issue.md new file mode 100644 index 0000000000..85ff46fe22 --- /dev/null +++ b/.claude/commands/issue.md @@ -0,0 +1,49 @@ +# GitHub Issue Creation Task + +You are creating a GitHub issue for the Lighthouse project. + +## Required Reading + +**Before creating an issue, read `.ai/ISSUES.md`** for issue and PR writing guidelines. + +## Structure + +1. **Description** (required) + - First paragraph: problem and brief solution + - Context about current behavior + - Links to related issues, PRs, or specs + - Technical and specific + +2. **Steps to Resolve** (when applicable) + - Present options and considerations + - Don't be overly prescriptive + - Mention relevant constraints + +3. **Code References** + - Use GitHub permalinks with commit hashes + - Get hash: `git rev-parse unstable` + +## Style + +- Natural, concise, direct +- Avoid AI-sounding language +- Be honest about uncertainty +- Present trade-offs + +## Labels to Suggest + +- **Type**: bug, enhancement, optimization, code-quality +- **Component**: database, HTTP-API, fork-choice, beacon-processor +- **Effort**: good first issue, low-hanging-fruit, major-task + +## Output + +Provide the complete issue text ready to paste into GitHub. + +## After Feedback + +If the developer refines your issue/PR text or suggests a different format: + +1. **Apply their feedback** to the current issue +2. **Offer to update docs** - Ask: "Should I update `.ai/ISSUES.md` to capture this preference?" +3. **Document patterns** the team prefers that aren't yet in the guidelines diff --git a/.claude/commands/release.md b/.claude/commands/release.md new file mode 100644 index 0000000000..1694e90cc5 --- /dev/null +++ b/.claude/commands/release.md @@ -0,0 +1,85 @@ +# Release Notes Generation Task + +You are generating release notes for a new Lighthouse version. + +## Input Required + +- **Version number** (e.g., v8.1.0) +- **Base branch** (typically `stable` for previous release) +- **Release branch** (e.g., `release-v8.1`) +- **Release name** (Rick and Morty character - check existing to avoid duplicates) + +## Step 1: Gather Changes + +```bash +# Get commits between branches +git log --oneline origin/..origin/ + +# Check existing release names +gh release list --repo sigp/lighthouse --limit 50 +``` + +## Step 2: Analyze PRs + +For each PR: +1. Extract PR numbers from commit messages +2. Check for `backwards-incompat` label: + ```bash + gh pr view --repo sigp/lighthouse --json labels --jq '[.labels[].name] | join(",")' + ``` +3. Get PR details for context + +## Step 3: Categorize + +Group into sections (skip empty): +- **Breaking Changes** - schema changes, CLI changes, API changes +- **Performance Improvements** - user-noticeable optimizations +- **Validator Client Improvements** - VC-specific changes +- **Other Notable Changes** - new features, metrics +- **CLI Changes** - new/changed flags (note if BN or VC) +- **Bug Fixes** - significant user-facing fixes only + +## Step 4: Write Release Notes + +```markdown +## + +## Summary + +Lighthouse v includes . + +This is a upgrade for . + +##
+ +- **** (#<PR>): <User-facing description> + +## Update Priority + +| User Class | Beacon Node | Validator Client | +|:------------------|:------------|:-----------------| +| Staking Users | Low/Medium/High | Low/Medium/High | +| Non-Staking Users | Low/Medium/High | --- | + +## All Changes + +- <commit title> (#<PR>) + +## Binaries + +[See pre-built binaries documentation.](https://lighthouse-book.sigmaprime.io/installation_binaries.html) +``` + +## Guidelines + +- State **user impact**, not implementation details +- Avoid jargon users won't understand +- For CLI flags, mention if BN or VC +- Check PR descriptions for context + +## Step 5: Generate Announcements + +Create drafts for: +- **Email** - Formal, include priority table +- **Discord** - Tag @everyone, shorter +- **Twitter** - Single tweet, 2-3 highlights diff --git a/.claude/commands/review.md b/.claude/commands/review.md new file mode 100644 index 0000000000..7867716c79 --- /dev/null +++ b/.claude/commands/review.md @@ -0,0 +1,57 @@ +# Code Review Task + +You are reviewing code for the Lighthouse project. + +## Required Reading + +**Before reviewing, read `.ai/CODE_REVIEW.md`** for Lighthouse-specific safety requirements and review etiquette. + +## Focus Areas + +1. **Consensus Crate Safety** (if applicable) + - Safe math operations (saturating_*, checked_*) + - Zero panics + - Deterministic behavior + +2. **General Code Safety** + - No `.unwrap()` or `.expect()` at runtime + - No array indexing without bounds checks + - Proper error handling + +3. **Code Clarity** + - Clear variable names (avoid ambiguous abbreviations) + - Well-documented complex logic + - TODOs linked to GitHub issues + +4. **Error Handling** + - Errors are logged, not silently swallowed + - Edge cases are handled + - Return values are checked + +5. **Concurrency & Performance** + - Lock ordering is safe + - No blocking in async context + - Proper use of rayon thread pools + +## Output + +- Keep to 3-5 actionable comments +- Use natural, conversational language +- Provide specific line references +- Ask questions rather than making demands + +## After Review Discussion + +If the developer corrects your feedback or you learn something new: + +1. **Acknowledge and learn** - Note what you got wrong +2. **Offer to update docs** - Ask: "Should I update `.ai/CODE_REVIEW.md` with this lesson?" +3. **Format the lesson:** + ```markdown + ### Lesson: [Title] + **Issue:** [What went wrong] + **Feedback:** [What developer said] + **Learning:** [What to do differently] + ``` + +This keeps the review guidelines improving over time. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..4ab3ec9333 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,10 @@ +# Lighthouse AI Assistant Guide + +See [`CLAUDE.md`](CLAUDE.md) for AI assistant guidance. + +This file exists for OpenAI Codex compatibility. Codex can read files, so refer to `CLAUDE.md` for the full documentation including: + +- Quick reference commands +- Critical rules (panics, safe math, async) +- Project structure +- Pointers to detailed guides in `.ai/` diff --git a/CLAUDE.md b/CLAUDE.md index 3e9ab169f3..441c8e4274 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,332 +1,151 @@ -# CLAUDE.md +# Lighthouse AI Assistant Guide -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +This file provides guidance for AI assistants (Claude Code, Codex, etc.) working with Lighthouse. -## Development Commands +## Quick Reference -**Important**: Always branch from `unstable` and target `unstable` when creating pull requests. +```bash +# Build +make install # Build and install Lighthouse +cargo build --release # Standard release build -### Building and Installation +# Test (prefer targeted tests when iterating) +cargo nextest run -p <package> # Test specific package +cargo nextest run -p <package> <test> # Run individual test +make test # Full test suite (~20 min) -- `make install` - Build and install the main Lighthouse binary in release mode -- `make install-lcli` - Build and install the `lcli` utility binary -- `cargo build --release` - Standard Rust release build -- `cargo build --bin lighthouse --features "gnosis,slasher-lmdb"` - Build with specific features - -### Testing - -- `make test` - Run the full test suite in release mode (excludes EF tests, beacon_chain, slasher, network, http_api) -- `make test-release` - Run tests using nextest (faster parallel test runner) -- `make test-beacon-chain` - Run beacon chain tests for all supported forks -- `make test-slasher` - Run slasher tests with all database backend combinations -- `make test-ef` - Download and run Ethereum Foundation test vectors -- `make test-full` - Complete test suite including linting, EF tests, and execution engine tests -- `cargo nextest run -p <package_name>` - Run tests for a specific package -- `cargo nextest run -p <package_name> <test_name>` - Run individual test (preferred during development iteration) -- `FORK_NAME=electra cargo nextest run -p beacon_chain` - Run tests for specific fork - -**Note**: Full test suite takes ~20 minutes. When iterating, prefer running individual tests. - -### Linting and Code Quality - -- `make lint` - Run Clippy linter with project-specific rules -- `make lint-full` - Run comprehensive linting including tests (recommended for thorough checking) -- `make cargo-fmt` - Check code formatting with rustfmt -- `make check-benches` - Typecheck benchmark code -- `make audit` - Run security audit on dependencies - -### Cross-compilation - -- `make build-x86_64` - Cross-compile for x86_64 Linux -- `make build-aarch64` - Cross-compile for ARM64 Linux -- `make build-riscv64` - Cross-compile for RISC-V 64-bit Linux - -## Architecture Overview - -Lighthouse is a modular Ethereum consensus client with two main components: - -### Core Components - -**Beacon Node** (`beacon_node/`) - -- Main consensus client that syncs with the Ethereum network -- Contains the beacon chain state transition logic (`beacon_node/beacon_chain/`) -- Handles networking, storage, and P2P communication -- Provides HTTP API for validator clients and external tools -- Entry point: `beacon_node/src/lib.rs` - -**Validator Client** (`validator_client/`) - -- Manages validator keystores and performs validator duties -- Connects to beacon nodes via HTTP API -- Handles block proposals, attestations, and sync committee duties -- Includes slashing protection and doppelganger detection -- Entry point: `validator_client/src/lib.rs` - -### Key Subsystems - -**Consensus Types** (`consensus/types/`) - -- Core Ethereum consensus data structures (BeaconState, BeaconBlock, etc.) -- Ethereum specification implementations for different networks (mainnet, gnosis) -- SSZ encoding/decoding and state transition primitives - -**Storage** (`beacon_node/store/`) - -- Hot/cold database architecture for efficient beacon chain storage -- Supports multiple backends (LevelDB, RocksDB, REDB) -- Handles state pruning and historical data management - -**Networking** (`beacon_node/lighthouse_network/`, `beacon_node/network/`) - -- Libp2p-based P2P networking stack -- Gossipsub for message propagation -- Discovery v5 for peer discovery -- Request/response protocols for sync - -**Fork Choice** (`consensus/fork_choice/`, `consensus/proto_array/`) - -- Implements Ethereum's fork choice algorithm (proto-array) -- Manages chain reorganizations and finality - -**Execution Layer Integration** (`beacon_node/execution_layer/`) - -- Interfaces with execution clients -- Retrieves payloads from local execution layer or external block builders -- Handles payload validation and builder integration - -**Slasher** (`slasher/`) - -- Optional slashing detection service -- Supports LMDB, MDBX, and REDB database backends -- Can be enabled with `--slasher` flag - -### Utilities - -**Account Manager** (`account_manager/`) - CLI tool for managing validator accounts and keystores -**LCLI** (`lcli/`) - Lighthouse command-line utilities for debugging and testing -**Database Manager** (`database_manager/`) - Database maintenance and migration tools - -### Build System Notes - -- Uses Cargo workspace with 90+ member crates -- Supports multiple Ethereum specifications via feature flags (`gnosis`, `spec-minimal`) -- Cross-compilation support for Linux x86_64, ARM64, and RISC-V -- Multiple build profiles: `release`, `maxperf`, `reproducible` -- Feature-based compilation for different database backends and optional components - -### Network Support - -- **Mainnet**: Default production network -- **Gnosis**: Alternative network (requires `gnosis` feature) -- **Testnets**: Holesky, Sepolia via built-in network configs -- **Custom networks**: Via `--testnet-dir` flag - -### Key Configuration - -- Default data directory: `~/.lighthouse/{network}` -- Beacon node data: `~/.lighthouse/{network}/beacon` -- Validator data: `~/.lighthouse/{network}/validators` -- Configuration primarily via CLI flags and YAML files - -## Common Review Standards - -### CI/Testing Requirements - -- All checks must pass before merge -- Test coverage expected for significant changes -- Flaky tests are actively addressed and fixed -- New features often require corresponding tests -- `beacon_chain` and `http_api` tests support fork-specific testing using `FORK_NAME` env var when `beacon_chain/fork_from_env` feature is enabled - -### Code Quality Standards - -- Clippy warnings must be fixed promptly (multiple PRs show this pattern) -- Code formatting with `cargo fmt` enforced -- Must run `cargo sort` when adding dependencies - dependency order is enforced on CI -- Performance considerations for hot paths - -### Documentation and Context - -- PRs require clear descriptions of what and why -- Breaking changes need migration documentation -- API changes require documentation updates -- When CLI is updated, run `make cli-local` to generate updated help text in lighthouse book -- Comments appreciated for complex logic - -### Security and Safety - -- Careful review of consensus-critical code paths -- Error handling patterns must be comprehensive -- Input validation for external data - -## Development Patterns and Best Practices - -### Panics and Error Handling - -- **Panics should be avoided at all costs** -- Always prefer returning a `Result` or `Option` over causing a panic (e.g., prefer `array.get(1)?` over `array[1]`) -- Avoid `expect` or `unwrap` at runtime - only acceptable during startup when validating CLI flags or configurations -- If you must make assumptions about panics, use `.expect("Helpful message")` instead of `.unwrap()` and provide detailed reasoning in nearby comments -- Use proper error handling with `Result` types and graceful error propagation - -### Rayon Usage - -- Avoid using the rayon global thread pool as it results in CPU oversubscription when the beacon processor has fully allocated all CPUs to workers -- Use scoped rayon pools started by beacon processor for computational intensive tasks - -### Locks - -- Take great care to avoid deadlocks when working with fork choice locks - seek detailed review ([reference](beacon_node/beacon_chain/src/canonical_head.rs:9)) -- Keep lock scopes as narrow as possible to avoid blocking fast-responding functions like the networking stack - -### Async Patterns - -- Avoid blocking computations in async tasks -- Spawn a blocking task instead for CPU-intensive work - -### Tracing - -- Design spans carefully and avoid overuse of spans just to add context data to events -- Avoid using spans on simple getter methods as it can result in performance overhead -- Be cautious of span explosion with recursive functions -- Use spans per meaningful step or computationally critical step -- Avoid using `span.enter()` or `span.entered()` in async tasks - -### Database - -- Maintain schema continuity on `unstable` branch -- Database migrations must be backward compatible - -### Consensus Crate - -- Use safe math methods like `saturating_xxx` or `checked_xxx` -- Critical that this crate behaves deterministically and MUST not have undefined behavior - -### Testing Patterns - -- **Use appropriate test types for the right scenarios**: - - **Unit tests** for single component edge cases and isolated logic - - **Integration tests** using [`BeaconChainHarness`](beacon_node/beacon_chain/src/test_utils.rs:668) for end-to-end workflows -- **`BeaconChainHarness` guidelines**: - - Excellent for integration testing but slower than unit tests - - Prefer unit tests instead for testing edge cases of single components - - Reserve for testing component interactions and full workflows -- **Mocking strategies**: - - Use `mockall` crate for unit test mocking - - Use `mockito` for HTTP API mocking (see [`validator_test_rig`](testing/validator_test_rig/src/mock_beacon_node.rs:20) for examples) -- **Event-based testing for sync components**: - - Use [`TestRig`](beacon_node/network/src/sync/tests/mod.rs) pattern for testing sync components - - Sync components interact with the network and beacon chain via events (their public API), making event-based testing more suitable than using internal functions and mutating internal states - - Enables testing of complex state transitions and timing-sensitive scenarios -- **Testing `BeaconChain` dependent components**: - - `BeaconChain` is difficult to create for TDD - - Create intermediate adapter structs to enable easy mocking - - See [`beacon_node/beacon_chain/src/fetch_blobs/tests.rs`](beacon_node/beacon_chain/src/fetch_blobs/tests.rs) for the adapter pattern -- **Local testnet for manual/full E2E testing**: - - Use Kurtosis-based local testnet setup for comprehensive testing - - See [`scripts/local_testnet/README.md`](scripts/local_testnet/README.md) for setup instructions - -### TODOs and Comments - -- All `TODO` statements must be accompanied by a GitHub issue link -- Prefer line (`//`) comments to block comments (`/* ... */`) -- Use doc comments (`///`) before attributes for public items -- Keep documentation concise and clear - avoid verbose explanations -- Provide examples in doc comments for public APIs when helpful - -## Logging Guidelines - -Use appropriate log levels for different scenarios: - -- **`crit`**: Critical issues with major impact to Lighthouse functionality - Lighthouse may not function correctly without resolving. Needs immediate attention. -- **`error`**: Error cases that may have moderate impact to Lighthouse functionality. Expect to receive reports from users for this level. -- **`warn`**: Unexpected code paths that don't have major impact - fully recoverable. Expect user reports if excessive warning logs occur. -- **`info`**: High-level logs indicating beacon node status and block import status. Should not be used excessively. -- **`debug`**: Events lower level than info useful for developers. Can also log errors expected during normal operation that users don't need to action. - -## Code Examples - -### Safe Math in Consensus Crate - -```rust -// ❌ Avoid - could panic -let result = a + b; - -// ✅ Preferred -let result = a.saturating_add(b); -// or -use safe_arith::SafeArith; - -let result = a.safe_add(b)?; +# Lint +make lint # Run Clippy +cargo fmt --all && make lint-fix # Format and fix ``` -### Panics and Error Handling +## Before You Start + +Read the relevant guide for your task: + +| Task | Read This First | +|------|-----------------| +| **Code review** | `.ai/CODE_REVIEW.md` | +| **Creating issues/PRs** | `.ai/ISSUES.md` | +| **Development patterns** | `.ai/DEVELOPMENT.md` | + +## Critical Rules (consensus failures or crashes) + +### 1. No Panics at Runtime ```rust -// ❌ Avoid - could panic at runtime -let value = some_result.unwrap(); +// NEVER +let value = option.unwrap(); let item = array[1]; -// ✅ Preferred - proper error handling -let value = some_result.map_err(|e| CustomError::SomeVariant(e))?; +// ALWAYS +let value = option?; let item = array.get(1)?; - -// ✅ Acceptable during startup for CLI/config validation -let config_value = matches.get_one::<String>("required-flag") - .expect("Required flag must be present due to clap validation"); - -// ✅ If you must make runtime assumptions, use expect with explanation -let item = array.get(1).expect("Array always has at least 2 elements due to validation in constructor"); -// Detailed reasoning should be provided in nearby comments ``` -### TODO Format +Only acceptable during startup for CLI/config validation. + +### 2. Consensus Crate: Safe Math Only + +In `consensus/` (excluding `types/`), use saturating or checked arithmetic: ```rust -pub fn my_function(&mut self, _something: &[u8]) -> Result<String, Error> { - // TODO: Implement proper validation here - // https://github.com/sigp/lighthouse/issues/1234 -} +// NEVER +let result = a + b; + +// ALWAYS +let result = a.saturating_add(b); ``` -### Async Task Spawning for Blocking Work +## Important Rules (bugs or performance issues) + +### 3. Never Block Async ```rust -// ❌ Avoid - blocking in async context -async fn some_handler() { - let result = expensive_computation(); // blocks async runtime -} +// NEVER +async fn handler() { expensive_computation(); } -// ✅ Preferred -async fn some_handler() { - let result = tokio::task::spawn_blocking(|| { - expensive_computation() - }).await?; +// ALWAYS +async fn handler() { + tokio::task::spawn_blocking(|| expensive_computation()).await?; } ``` -### Tracing Span Usage +### 4. Lock Ordering -```rust -// ❌ Avoid - span on simple getter -#[instrument] -fn get_head_block_root(&self) -> Hash256 { - self.head_block_root -} +Document lock ordering to avoid deadlocks. See [`canonical_head.rs:9-32`](beacon_node/beacon_chain/src/canonical_head.rs) for the pattern. -// ✅ Preferred - span on meaningful operations -#[instrument(skip(self))] -async fn process_block(&self, block: Block) -> Result<(), Error> { - // meaningful computation -} +### 5. Rayon Thread Pools + +Use scoped rayon pools from beacon processor, not global pool. Global pool causes CPU oversubscription when beacon processor has allocated all CPUs. + +## Good Practices + +### 6. TODOs Need Issues + +All `TODO` comments must link to a GitHub issue. + +### 7. Clear Variable Names + +Avoid ambiguous abbreviations (`bb`, `bl`). Use `beacon_block`, `blob`. + +## Branch & PR Guidelines + +- Branch from `unstable`, target `unstable` for PRs +- Run `cargo sort` when adding dependencies +- Run `make cli-local` when updating CLI flags + +## Project Structure + +``` +beacon_node/ # Consensus client + beacon_chain/ # State transition logic + store/ # Database (hot/cold) + network/ # P2P networking + execution_layer/ # EL integration +validator_client/ # Validator duties +consensus/ + types/ # Core data structures + fork_choice/ # Proto-array ``` -## Build and Development Notes +See `.ai/DEVELOPMENT.md` for detailed architecture. -- Full builds and tests take 5+ minutes - use large timeouts (300s+) for any `cargo build`, `cargo nextest`, or `make` commands -- Use `cargo check` for faster iteration during development and always run after code changes -- Prefer targeted package tests (`cargo nextest run -p <package>`) and individual tests over full test suite when debugging specific issues -- Use `cargo fmt --all && make lint-fix` to format code and fix linting issues once a task is complete -- Always understand the broader codebase patterns before making changes -- Minimum Supported Rust Version (MSRV) is documented in `lighthouse/Cargo.toml` - ensure Rust version meets or exceeds this requirement +## Maintaining These Docs + +**These AI docs should evolve based on real interactions.** + +### After Code Reviews + +If a developer corrects your review feedback or points out something you missed: +- Ask: "Should I update `.ai/CODE_REVIEW.md` with this lesson?" +- Add to the "Common Review Patterns" or create a new "Lessons Learned" entry +- Include: what went wrong, what the feedback was, what to do differently + +### After PR/Issue Creation + +If a developer refines your PR description or issue format: +- Ask: "Should I update `.ai/ISSUES.md` to capture this?" +- Document the preferred style or format + +### After Development Work + +If you learn something about the codebase architecture or patterns: +- Ask: "Should I update `.ai/DEVELOPMENT.md` with this?" +- Add to relevant section or create new patterns + +### Format for Lessons + +```markdown +### Lesson: [Brief Title] + +**Context:** [What task were you doing?] +**Issue:** [What went wrong or was corrected?] +**Learning:** [What to do differently next time] +``` + +### When NOT to Update + +- Minor preference differences (not worth documenting) +- One-off edge cases unlikely to recur +- Already covered by existing documentation From a1176e77be5488d6a7b607257bca88db56b0541d Mon Sep 17 00:00:00 2001 From: Alexander Uvizhev <uvizhe@gmail.com> Date: Tue, 10 Feb 2026 11:13:25 +0300 Subject: [PATCH 03/81] Add insecure-dep test task to Makefile and CI (#8464) #8106 I added `insecure-deps` target to Makefile and a new step into `check-code` section of test-suite CI workflow that uses the former. That bash multiliner is not ideal, I'd prefer a cargo plugin instead but none exists. I also changed Cargo.toml to test that the new CI check works. Once we see a pipeline fails, I revert the change. Co-Authored-By: Alexander Uvizhev <uvizhe@gmail.com> Co-Authored-By: chonghe <44791194+chong-he@users.noreply.github.com> Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com> --- .github/workflows/test-suite.yml | 2 ++ Makefile | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 46fa15da86..72ea9d41ae 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -319,6 +319,8 @@ jobs: bins: cargo-audit,cargo-deny - name: Check formatting with cargo fmt run: make cargo-fmt + - name: Check dependencies for unencrypted HTTP links + run: make insecure-deps - name: Lint code for quality and style with Clippy run: make lint-full - name: Certify Cargo.lock freshness diff --git a/Makefile b/Makefile index 9d08c3ebe1..9e2b1d24c5 100644 --- a/Makefile +++ b/Makefile @@ -343,6 +343,12 @@ vendor: udeps: cargo +$(PINNED_NIGHTLY) udeps --tests --all-targets --release --features "$(TEST_FEATURES)" +# Checks Cargo.toml files for unencrypted HTTP links +insecure-deps: + @ BAD_LINKS=$$(find . -name Cargo.toml | xargs grep -n "http://" || true); \ + if [ -z "$$BAD_LINKS" ]; then echo "No insecure HTTP links found"; \ + else echo "$$BAD_LINKS"; echo "Using plain HTTP in Cargo.toml files is forbidden"; exit 1; fi + # Performs a `cargo` clean and cleans the `ef_tests` directory. clean: cargo clean From 56eb81a5e0a383a7a8789cfe12a6b684fc3aadf6 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi <eserilev@gmail.com> Date: Tue, 10 Feb 2026 13:52:52 -0800 Subject: [PATCH 04/81] Implement weak subjectivity safety checks (#7347) Closes #7273 https://github.com/ethereum/consensus-specs/pull/4179 Co-Authored-By: Eitan Seri-Levi <eserilev@ucsc.edu> Co-Authored-By: Eitan Seri- Levi <eserilev@gmail.com> Co-Authored-By: Michael Sproul <michaelsproul@users.noreply.github.com> Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com> --- beacon_node/beacon_chain/src/builder.rs | 29 ++++- beacon_node/beacon_chain/src/chain_config.rs | 3 + .../tests/payload_invalidation.rs | 9 +- beacon_node/beacon_chain/tests/store_tests.rs | 1 + beacon_node/beacon_chain/tests/tests.rs | 49 ++++++++- beacon_node/src/cli.rs | 10 ++ beacon_node/src/config.rs | 2 + beacon_node/src/lib.rs | 7 +- book/src/help_bn.md | 6 + consensus/types/src/state/beacon_state.rs | 104 ++++++++++++++++++ consensus/types/src/state/mod.rs | 2 +- lighthouse/tests/beacon_node.rs | 15 +++ 12 files changed, 222 insertions(+), 15 deletions(-) diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index e5b656adf8..cf6cb1598b 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -41,7 +41,7 @@ use std::sync::Arc; use std::time::Duration; use store::{Error as StoreError, HotColdDB, ItemStore, KeyValueStoreOp}; use task_executor::{ShutdownReason, TaskExecutor}; -use tracing::{debug, error, info}; +use tracing::{debug, error, info, warn}; use tree_hash::TreeHash; use types::data::CustodyIndex; use types::{ @@ -848,6 +848,33 @@ where )); } + // Check if the head snapshot is within the weak subjectivity period + let head_state = &head_snapshot.beacon_state; + let Ok(ws_period) = head_state.compute_weak_subjectivity_period(&self.spec) else { + return Err(format!( + "Unable to compute the weak subjectivity period at the head snapshot slot: {:?}", + head_state.slot() + )); + }; + if current_slot.epoch(E::slots_per_epoch()) + > head_state.slot().epoch(E::slots_per_epoch()) + ws_period + { + if self.chain_config.ignore_ws_check { + warn!( + head_slot=%head_state.slot(), + %current_slot, + "The current head state is outside the weak subjectivity period. You are currently running a node that is susceptible to long range attacks. \ + It is highly recommended to purge your db and checkpoint sync. For more information please \ + read this blog post: https://blog.ethereum.org/2014/11/25/proof-stake-learned-love-weak-subjectivity" + ) + } + return Err( + "The current head state is outside the weak subjectivity period. A node in this state is susceptible to long range attacks. You should purge your db and \ + checkpoint sync. For more information please read this blog post: https://blog.ethereum.org/2014/11/25/proof-stake-learned-love-weak-subjectivity \ + If you understand the risks, it is possible to ignore this error with the --ignore-ws-check flag.".to_string() + ); + } + let validator_pubkey_cache = self .validator_pubkey_cache .map(|mut validator_pubkey_cache| { diff --git a/beacon_node/beacon_chain/src/chain_config.rs b/beacon_node/beacon_chain/src/chain_config.rs index 711ffdc99c..ad923000e2 100644 --- a/beacon_node/beacon_chain/src/chain_config.rs +++ b/beacon_node/beacon_chain/src/chain_config.rs @@ -117,6 +117,8 @@ pub struct ChainConfig { /// On Holesky there is a block which is added to this set by default but which can be removed /// by using `--invalid-block-roots ""`. pub invalid_block_roots: HashSet<Hash256>, + /// When set to true, the beacon node can be started even if the head state is outside the weak subjectivity period. + pub ignore_ws_check: bool, /// Disable the getBlobs optimisation to fetch blobs from the EL mempool. pub disable_get_blobs: bool, /// The node's custody type, determining how many data columns to custody and sample. @@ -160,6 +162,7 @@ impl Default for ChainConfig { block_publishing_delay: None, data_column_publishing_delay: None, invalid_block_roots: HashSet::new(), + ignore_ws_check: false, disable_get_blobs: false, node_custody_type: NodeCustodyType::Fullnode, } diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index 1204412d65..f1e52de27b 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::{ INVALID_JUSTIFIED_PAYLOAD_SHUTDOWN_REASON, NotifyExecutionLayer, OverrideForkchoiceUpdate, StateSkipConfig, WhenSlotSkipped, canonical_head::{CachedHead, CanonicalHead}, - test_utils::{BeaconChainHarness, EphemeralHarnessType}, + test_utils::{BeaconChainHarness, EphemeralHarnessType, test_spec}, }; use execution_layer::{ ExecutionLayer, ForkchoiceState, PayloadAttributes, @@ -42,14 +42,11 @@ struct InvalidPayloadRig { impl InvalidPayloadRig { fn new() -> Self { - let spec = E::default_spec(); + let spec = test_spec::<E>(); Self::new_with_spec(spec) } - fn new_with_spec(mut spec: ChainSpec) -> Self { - spec.altair_fork_epoch = Some(Epoch::new(0)); - spec.bellatrix_fork_epoch = Some(Epoch::new(0)); - + fn new_with_spec(spec: ChainSpec) -> Self { let harness = BeaconChainHarness::builder(MainnetEthSpec) .spec(spec.into()) .chain_config(ChainConfig { diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index ea5f735bde..5410f26a5d 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -117,6 +117,7 @@ fn get_harness_import_all_data_columns( ) -> TestHarness { // Most tests expect to retain historic states, so we use this as the default. let chain_config = ChainConfig { + ignore_ws_check: true, reconstruct_historic_states: true, ..ChainConfig::default() }; diff --git a/beacon_node/beacon_chain/tests/tests.rs b/beacon_node/beacon_chain/tests/tests.rs index 1884429a6a..fb86a1a845 100644 --- a/beacon_node/beacon_chain/tests/tests.rs +++ b/beacon_node/beacon_chain/tests/tests.rs @@ -15,7 +15,8 @@ use state_processing::EpochProcessingError; use state_processing::{per_slot_processing, per_slot_processing::Error as SlotProcessingError}; use std::sync::LazyLock; use types::{ - BeaconState, BeaconStateError, BlockImportSource, Checkpoint, EthSpec, Hash256, MinimalEthSpec, + BeaconState, BeaconStateError, BlockImportSource, ChainSpec, Checkpoint, + DEFAULT_PRE_ELECTRA_WS_PERIOD, EthSpec, ForkName, Hash256, MainnetEthSpec, MinimalEthSpec, RelativeEpoch, Slot, }; @@ -38,6 +39,27 @@ fn get_harness(validator_count: usize) -> BeaconChainHarness<EphemeralHarnessTyp ) } +fn get_harness_with_spec( + validator_count: usize, + spec: &ChainSpec, +) -> BeaconChainHarness<EphemeralHarnessType<MainnetEthSpec>> { + let chain_config = ChainConfig { + reconstruct_historic_states: true, + ..Default::default() + }; + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec.clone().into()) + .chain_config(chain_config) + .keypairs(KEYPAIRS[0..validator_count].to_vec()) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + harness.advance_slot(); + + harness +} + fn get_harness_with_config( validator_count: usize, chain_config: ChainConfig, @@ -1083,3 +1105,28 @@ async fn pseudo_finalize_with_lagging_split_update() { let expect_true_migration = false; pseudo_finalize_test_generic(epochs_per_migration, expect_true_migration).await; } + +#[tokio::test] +async fn test_compute_weak_subjectivity_period() { + type E = MainnetEthSpec; + let expected_ws_period_pre_electra = DEFAULT_PRE_ELECTRA_WS_PERIOD; + let expected_ws_period_post_electra = 256; + + // test Base variant + let spec = ForkName::Altair.make_genesis_spec(E::default_spec()); + let harness = get_harness_with_spec(VALIDATOR_COUNT, &spec); + let head_state = harness.get_current_state(); + + let calculated_ws_period = head_state.compute_weak_subjectivity_period(&spec).unwrap(); + + assert_eq!(calculated_ws_period, expected_ws_period_pre_electra); + + // test Electra variant + let spec = ForkName::Electra.make_genesis_spec(E::default_spec()); + let harness = get_harness_with_spec(VALIDATOR_COUNT, &spec); + let head_state = harness.get_current_state(); + + let calculated_ws_period = head_state.compute_weak_subjectivity_period(&spec).unwrap(); + + assert_eq!(calculated_ws_period, expected_ws_period_post_electra); +} diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 9553fe60ba..5c3e8058d9 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -1404,6 +1404,16 @@ pub fn cli_app() -> Command { .help_heading(FLAG_HEADER) .display_order(0) ) + .arg( + Arg::new("ignore-ws-check") + .long("ignore-ws-check") + .help("Using this flag allows a node to run in a state that may expose it to long-range attacks. \ + For more information please read this blog post: https://blog.ethereum.org/2014/11/25/proof-stake-learned-love-weak-subjectivity \ + If you understand the risks, you can use this flag to disable the Weak Subjectivity check at startup.") + .action(ArgAction::SetTrue) + .help_heading(FLAG_HEADER) + .display_order(0) + ) .arg( Arg::new("builder-fallback-skips") .long("builder-fallback-skips") diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index 752cf10550..e6091d9213 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -780,6 +780,8 @@ pub fn get_config<E: EthSpec>( client_config.chain.paranoid_block_proposal = cli_args.get_flag("paranoid-block-proposal"); + client_config.chain.ignore_ws_check = cli_args.get_flag("ignore-ws-check"); + /* * Builder fallback configs. */ diff --git a/beacon_node/src/lib.rs b/beacon_node/src/lib.rs index 6db2150e5f..e33da17e26 100644 --- a/beacon_node/src/lib.rs +++ b/beacon_node/src/lib.rs @@ -22,14 +22,9 @@ use types::{ChainSpec, Epoch, EthSpec, ForkName}; pub type ProductionClient<E> = Client<Witness<SystemTimeSlotClock, E, BeaconNodeBackend<E>, BeaconNodeBackend<E>>>; -/// The beacon node `Client` that will be used in production. +/// The beacon node `Client` that is used in production. /// /// Generic over some `EthSpec`. -/// -/// ## Notes: -/// -/// Despite being titled `Production...`, this code is not ready for production. The name -/// demonstrates an intention, not a promise. pub struct ProductionBeaconNode<E: EthSpec>(ProductionClient<E>); impl<E: EthSpec> ProductionBeaconNode<E> { diff --git a/book/src/help_bn.md b/book/src/help_bn.md index d3aa27c8a7..beb74da376 100644 --- a/book/src/help_bn.md +++ b/book/src/help_bn.md @@ -509,6 +509,12 @@ Flags: --http-enable-tls Serves the RESTful HTTP API server over TLS. This feature is currently experimental. + --ignore-ws-check + Using this flag allows a node to run in a state that may expose it to + long-range attacks. For more information please read this blog post: + https://blog.ethereum.org/2014/11/25/proof-stake-learned-love-weak-subjectivity + If you understand the risks, you can use this flag to disable the Weak + Subjectivity check at startup. --import-all-attestations Import and aggregate all attestations, regardless of validator subscriptions. This will only import attestations from diff --git a/consensus/types/src/state/beacon_state.rs b/consensus/types/src/state/beacon_state.rs index 1352ded79e..1745908c40 100644 --- a/consensus/types/src/state/beacon_state.rs +++ b/consensus/types/src/state/beacon_state.rs @@ -55,9 +55,21 @@ use crate::{ }; pub const CACHED_EPOCHS: usize = 3; + +// Pre-electra WS calculations are not supported. On mainnet, pre-electra epochs are outside the weak subjectivity +// period. The default pre-electra WS value is set to 256 to allow for `basic-sim``, `fallback-sim`` test case `revert_minority_fork_on_resume` +// to pass. 256 is a small enough number to trigger the WS safety check pre-electra on mainnet. +pub const DEFAULT_PRE_ELECTRA_WS_PERIOD: u64 = 256; + const MAX_RANDOM_BYTE: u64 = (1 << 8) - 1; const MAX_RANDOM_VALUE: u64 = (1 << 16) - 1; +// `SAFETY_DECAY` is defined as the maximum percentage tolerable loss in the one-third +// safety margin of FFG finality. Thus, any attack exploiting the Weak Subjectivity Period has +// a safety margin of at least `1/3 - SAFETY_DECAY/100`. +// Spec: https://github.com/ethereum/consensus-specs/blob/1937aff86b41b5171a9bc3972515986f1bbbf303/specs/phase0/weak-subjectivity.md?plain=1#L50-L71 +const SAFETY_DECAY: u64 = 10; + pub type Validators<E> = List<Validator, <E as EthSpec>::ValidatorRegistryLimit>; pub type Balances<E> = List<u64, <E as EthSpec>::ValidatorRegistryLimit>; @@ -3007,6 +3019,26 @@ impl<E: EthSpec> BeaconState<E> { Ok(()) } + /// Returns the weak subjectivity period for `self` + pub fn compute_weak_subjectivity_period( + &self, + spec: &ChainSpec, + ) -> Result<Epoch, BeaconStateError> { + let total_active_balance = self.get_total_active_balance()?; + let fork_name = self.fork_name_unchecked(); + + if fork_name.electra_enabled() { + let balance_churn_limit = self.get_balance_churn_limit(spec)?; + compute_weak_subjectivity_period_electra( + total_active_balance, + balance_churn_limit, + spec, + ) + } else { + Ok(Epoch::new(DEFAULT_PRE_ELECTRA_WS_PERIOD)) + } + } + /// Get the payload timeliness committee for the given `slot`. /// /// Requires the committee cache to be initialized. @@ -3382,3 +3414,75 @@ impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for BeaconState<E> { )) } } + +/// Spec: https://github.com/ethereum/consensus-specs/blob/1937aff86b41b5171a9bc3972515986f1bbbf303/specs/electra/weak-subjectivity.md?plain=1#L30 +pub fn compute_weak_subjectivity_period_electra( + total_active_balance: u64, + balance_churn_limit: u64, + spec: &ChainSpec, +) -> Result<Epoch, BeaconStateError> { + let epochs_for_validator_set_churn = SAFETY_DECAY + .safe_mul(total_active_balance)? + .safe_div(balance_churn_limit.safe_mul(200)?)?; + let ws_period = spec + .min_validator_withdrawability_delay + .safe_add(epochs_for_validator_set_churn)?; + + Ok(ws_period) +} + +#[cfg(test)] +mod weak_subjectivity_tests { + use crate::state::beacon_state::compute_weak_subjectivity_period_electra; + use crate::{ChainSpec, Epoch, EthSpec, MainnetEthSpec}; + + const GWEI_PER_ETH: u64 = 1_000_000_000; + + #[test] + fn test_compute_weak_subjectivity_period_electra() { + let mut spec = MainnetEthSpec::default_spec(); + spec.altair_fork_epoch = Some(Epoch::new(0)); + spec.bellatrix_fork_epoch = Some(Epoch::new(0)); + spec.capella_fork_epoch = Some(Epoch::new(0)); + spec.deneb_fork_epoch = Some(Epoch::new(0)); + spec.electra_fork_epoch = Some(Epoch::new(0)); + + // A table of some expected values: + // https://github.com/ethereum/consensus-specs/blob/1937aff86b41b5171a9bc3972515986f1bbbf303/specs/electra/weak-subjectivity.md?plain=1#L44-L54 + // (total_active_balance, expected_ws_period) + let expected_values: Vec<(u64, u64)> = vec![ + (1_048_576 * GWEI_PER_ETH, 665), + (2_097_152 * GWEI_PER_ETH, 1_075), + (4_194_304 * GWEI_PER_ETH, 1_894), + (8_388_608 * GWEI_PER_ETH, 3_532), + (16_777_216 * GWEI_PER_ETH, 3_532), + (33_554_432 * GWEI_PER_ETH, 3_532), + // This value cross referenced w/ + // beacon_chain/tests/tests.rs:test_compute_weak_subjectivity_period + (1536 * GWEI_PER_ETH, 256), + ]; + + for (total_active_balance, expected_ws_period) in expected_values { + let balance_churn_limit = get_balance_churn_limit(total_active_balance, &spec); + + let calculated_ws_period = compute_weak_subjectivity_period_electra( + total_active_balance, + balance_churn_limit, + &spec, + ) + .unwrap(); + + assert_eq!(calculated_ws_period, expected_ws_period); + } + } + + // caclulate the balance_churn_limit without dealing with states + // and without initializing the active balance cache + fn get_balance_churn_limit(total_active_balance: u64, spec: &ChainSpec) -> u64 { + let churn = std::cmp::max( + spec.min_per_epoch_churn_limit_electra, + total_active_balance / spec.churn_limit_quotient, + ); + churn - (churn % spec.effective_balance_increment) + } +} diff --git a/consensus/types/src/state/mod.rs b/consensus/types/src/state/mod.rs index 309796d359..ea064fb7ac 100644 --- a/consensus/types/src/state/mod.rs +++ b/consensus/types/src/state/mod.rs @@ -17,7 +17,7 @@ pub use balance::Balance; pub use beacon_state::{ BeaconState, BeaconStateAltair, BeaconStateBase, BeaconStateBellatrix, BeaconStateCapella, BeaconStateDeneb, BeaconStateElectra, BeaconStateError, BeaconStateFulu, BeaconStateGloas, - BeaconStateHash, BeaconStateRef, CACHED_EPOCHS, + BeaconStateHash, BeaconStateRef, CACHED_EPOCHS, DEFAULT_PRE_ELECTRA_WS_PERIOD, }; pub use committee_cache::{ CommitteeCache, compute_committee_index_in_epoch, compute_committee_range_in_epoch, diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index a2fad31f65..322787736b 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -295,6 +295,21 @@ fn paranoid_block_proposal_on() { .with_config(|config| assert!(config.chain.paranoid_block_proposal)); } +#[test] +fn ignore_ws_check_enabled() { + CommandLineTest::new() + .flag("ignore-ws-check", None) + .run_with_zero_port() + .with_config(|config| assert!(config.chain.ignore_ws_check)); +} + +#[test] +fn ignore_ws_check_default() { + CommandLineTest::new() + .run_with_zero_port() + .with_config(|config| assert!(!config.chain.ignore_ws_check)); +} + #[test] fn reset_payload_statuses_default() { CommandLineTest::new() From e1d3dcc8dc19f137756f75c42c592964a07adfae Mon Sep 17 00:00:00 2001 From: Akihito Nakano <sora.akatsuki@gmail.com> Date: Wed, 11 Feb 2026 07:49:20 +0900 Subject: [PATCH 05/81] Penalize peers that send an invalid rpc request (#6986) Since https://github.com/sigp/lighthouse/pull/6847, invalid `BlocksByRange`/`BlobsByRange` requests, which do not comply with the spec, are [handled in the Handler](https://github.com/sigp/lighthouse/blob/3d16d1080f5b93193404967dcb5525fa68840ea0/beacon_node/lighthouse_network/src/rpc/handler.rs#L880-L911). Any peer that sends an invalid request is penalized and disconnected. However, other kinds of invalid rpc request, which result in decoding errors, are just dropped. No penalty is applied and the connection with the peer remains. I have added handling for the `ListenUpgradeError` event to notify the application of an `RPCError:InvalidData` error and disconnect to the peer that sent the invalid rpc request. I also added tests for handling invalid rpc requests. Co-Authored-By: ackintosh <sora.akatsuki@gmail.com> --- .../lighthouse_network/src/rpc/handler.rs | 17 +- .../lighthouse_network/src/rpc/protocol.rs | 10 +- .../lighthouse_network/tests/rpc_tests.rs | 160 +++++++++++++++++- 3 files changed, 179 insertions(+), 8 deletions(-) diff --git a/beacon_node/lighthouse_network/src/rpc/handler.rs b/beacon_node/lighthouse_network/src/rpc/handler.rs index 720895bbe7..9861119ac1 100644 --- a/beacon_node/lighthouse_network/src/rpc/handler.rs +++ b/beacon_node/lighthouse_network/src/rpc/handler.rs @@ -13,7 +13,8 @@ use futures::prelude::*; use libp2p::PeerId; use libp2p::swarm::handler::{ ConnectionEvent, ConnectionHandler, ConnectionHandlerEvent, DialUpgradeError, - FullyNegotiatedInbound, FullyNegotiatedOutbound, StreamUpgradeError, SubstreamProtocol, + FullyNegotiatedInbound, FullyNegotiatedOutbound, ListenUpgradeError, StreamUpgradeError, + SubstreamProtocol, }; use libp2p::swarm::{ConnectionId, Stream}; use logging::crit; @@ -888,6 +889,16 @@ where ConnectionEvent::DialUpgradeError(DialUpgradeError { info, error }) => { self.on_dial_upgrade_error(info, error) } + ConnectionEvent::ListenUpgradeError(ListenUpgradeError { + error: (proto, error), + .. + }) => { + self.events_out.push(HandlerEvent::Err(HandlerErr::Inbound { + id: self.current_inbound_substream_id, + proto, + error, + })); + } _ => { // NOTE: ConnectionEvent is a non exhaustive enum so updates should be based on // release notes more than compiler feedback @@ -924,7 +935,7 @@ where request.count() )), })); - return self.shutdown(None); + return; } } RequestType::BlobsByRange(request) => { @@ -940,7 +951,7 @@ where max_allowed, max_requested_blobs )), })); - return self.shutdown(None); + return; } } _ => {} diff --git a/beacon_node/lighthouse_network/src/rpc/protocol.rs b/beacon_node/lighthouse_network/src/rpc/protocol.rs index f0ac9d00f9..34d8efccd1 100644 --- a/beacon_node/lighthouse_network/src/rpc/protocol.rs +++ b/beacon_node/lighthouse_network/src/rpc/protocol.rs @@ -675,7 +675,7 @@ where E: EthSpec, { type Output = InboundOutput<TSocket, E>; - type Error = RPCError; + type Error = (Protocol, RPCError); type Future = BoxFuture<'static, Result<Self::Output, Self::Error>>; fn upgrade_inbound(self, socket: TSocket, protocol: ProtocolId) -> Self::Future { @@ -717,10 +717,12 @@ where ) .await { - Err(e) => Err(RPCError::from(e)), + Err(e) => Err((versioned_protocol.protocol(), RPCError::from(e))), Ok((Some(Ok(request)), stream)) => Ok((request, stream)), - Ok((Some(Err(e)), _)) => Err(e), - Ok((None, _)) => Err(RPCError::IncompleteStream), + Ok((Some(Err(e)), _)) => Err((versioned_protocol.protocol(), e)), + Ok((None, _)) => { + Err((versioned_protocol.protocol(), RPCError::IncompleteStream)) + } } } } diff --git a/beacon_node/lighthouse_network/tests/rpc_tests.rs b/beacon_node/lighthouse_network/tests/rpc_tests.rs index 53939687d3..debe30b34f 100644 --- a/beacon_node/lighthouse_network/tests/rpc_tests.rs +++ b/beacon_node/lighthouse_network/tests/rpc_tests.rs @@ -5,8 +5,12 @@ use crate::common::spec_with_all_forks_enabled; use crate::common::{Protocol, build_tracing_subscriber}; use bls::Signature; use fixed_bytes::FixedBytesExtended; +use libp2p::PeerId; use lighthouse_network::rpc::{RequestType, methods::*}; -use lighthouse_network::service::api_types::AppRequestId; +use lighthouse_network::service::api_types::{ + AppRequestId, BlobsByRangeRequestId, BlocksByRangeRequestId, ComponentsByRangeRequestId, + DataColumnsByRangeRequestId, DataColumnsByRangeRequester, RangeRequestId, SyncRequestId, +}; use lighthouse_network::{NetworkEvent, ReportSource, Response}; use ssz::Encode; use ssz_types::{RuntimeVariableList, VariableList}; @@ -1783,3 +1787,157 @@ fn test_active_requests() { } }) } + +// Test that when a node receives an invalid BlocksByRange request exceeding the maximum count, +// it bans the sender. +#[test] +fn test_request_too_large_blocks_by_range() { + let spec = Arc::new(spec_with_all_forks_enabled()); + + test_request_too_large( + AppRequestId::Sync(SyncRequestId::BlocksByRange(BlocksByRangeRequestId { + id: 1, + parent_request_id: ComponentsByRangeRequestId { + id: 1, + requester: RangeRequestId::RangeSync { + chain_id: 1, + batch_id: Epoch::new(1), + }, + }, + })), + RequestType::BlocksByRange(OldBlocksByRangeRequest::new( + 0, + spec.max_request_blocks(ForkName::Base) as u64 + 1, // exceeds the max request defined in the spec. + 1, + )), + ); +} + +// Test that when a node receives an invalid BlobsByRange request exceeding the maximum count, +// it bans the sender. +#[test] +fn test_request_too_large_blobs_by_range() { + let spec = Arc::new(spec_with_all_forks_enabled()); + + let max_request_blobs_count = spec.max_request_blob_sidecars(ForkName::Base) as u64 + / spec.max_blobs_per_block_within_fork(ForkName::Base); + test_request_too_large( + AppRequestId::Sync(SyncRequestId::BlobsByRange(BlobsByRangeRequestId { + id: 1, + parent_request_id: ComponentsByRangeRequestId { + id: 1, + requester: RangeRequestId::RangeSync { + chain_id: 1, + batch_id: Epoch::new(1), + }, + }, + })), + RequestType::BlobsByRange(BlobsByRangeRequest { + start_slot: 0, + count: max_request_blobs_count + 1, // exceeds the max request defined in the spec. + }), + ); +} + +// Test that when a node receives an invalid DataColumnsByRange request exceeding the columns count, +// it bans the sender. +#[test] +fn test_request_too_large_data_columns_by_range() { + test_request_too_large( + AppRequestId::Sync(SyncRequestId::DataColumnsByRange( + DataColumnsByRangeRequestId { + id: 1, + parent_request_id: DataColumnsByRangeRequester::ComponentsByRange( + ComponentsByRangeRequestId { + id: 1, + requester: RangeRequestId::RangeSync { + chain_id: 1, + batch_id: Epoch::new(1), + }, + }, + ), + peer: PeerId::random(), + }, + )), + RequestType::DataColumnsByRange(DataColumnsByRangeRequest { + start_slot: 0, + count: 0, + // exceeds the max request defined in the spec. + columns: vec![0; E::number_of_columns() + 1], + }), + ); +} + +fn test_request_too_large(app_request_id: AppRequestId, request: RequestType<E>) { + // Set up the logging. + let log_level = "debug"; + let enable_logging = true; + let _subscriber = build_tracing_subscriber(log_level, enable_logging); + let rt = Arc::new(Runtime::new().unwrap()); + let spec = Arc::new(spec_with_all_forks_enabled()); + + rt.block_on(async { + let (mut sender, mut receiver) = common::build_node_pair( + Arc::downgrade(&rt), + ForkName::Base, + spec, + Protocol::Tcp, + false, + None, + ) + .await; + + // Build the sender future + let sender_future = async { + loop { + match sender.next_event().await { + NetworkEvent::PeerConnectedOutgoing(peer_id) => { + debug!(?request, %peer_id, "Sending RPC request"); + sender + .send_request(peer_id, app_request_id, request.clone()) + .unwrap(); + } + NetworkEvent::ResponseReceived { + app_request_id, + response, + .. + } => { + debug!(?app_request_id, ?response, "Received response"); + } + NetworkEvent::RPCFailed { error, .. } => { + // This variant should be unreachable, as the receiver doesn't respond with an error when a request exceeds the limit. + debug!(?error, "RPC failed"); + unreachable!(); + } + NetworkEvent::PeerDisconnected(peer_id) => { + // The receiver should disconnect as a result of the invalid request. + debug!(%peer_id, "Peer disconnected"); + // End the test. + return; + } + _ => {} + } + } + } + .instrument(info_span!("Sender")); + + // Build the receiver future + let receiver_future = async { + loop { + if let NetworkEvent::RequestReceived { .. } = receiver.next_event().await { + // This event should be unreachable, as the handler drops the invalid request. + unreachable!(); + } + } + } + .instrument(info_span!("Receiver")); + + tokio::select! { + _ = sender_future => {} + _ = receiver_future => {} + _ = sleep(Duration::from_secs(30)) => { + panic!("Future timed out"); + } + } + }); +} From 889946c04b15798cb8314e284cbf866e76523632 Mon Sep 17 00:00:00 2001 From: Akihito Nakano <sora.akatsuki@gmail.com> Date: Wed, 11 Feb 2026 08:14:28 +0900 Subject: [PATCH 06/81] Remove pending requests from ready_requests (#6625) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: ackintosh <sora.akatsuki@gmail.com> Co-Authored-By: João Oliveira <hello@jxs.pt> --- .../src/rpc/rate_limiter.rs | 8 ++ .../src/rpc/self_limiter.rs | 100 ++++++++++++++++-- 2 files changed, 102 insertions(+), 6 deletions(-) diff --git a/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs b/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs index 8b364f506c..2407038bc3 100644 --- a/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs +++ b/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs @@ -77,6 +77,14 @@ impl Quota { max_tokens: n, } } + + #[cfg(test)] + pub const fn n_every_millis(n: NonZeroU64, millis: u64) -> Self { + Quota { + replenish_all_every: Duration::from_millis(millis), + max_tokens: n, + } + } } /// Manages rate limiting of requests per peer, with differentiated rates per protocol. diff --git a/beacon_node/lighthouse_network/src/rpc/self_limiter.rs b/beacon_node/lighthouse_network/src/rpc/self_limiter.rs index 90e2db9135..2a7ef955a1 100644 --- a/beacon_node/lighthouse_network/src/rpc/self_limiter.rs +++ b/beacon_node/lighthouse_network/src/rpc/self_limiter.rs @@ -4,6 +4,10 @@ use super::{ rate_limiter::{RPCRateLimiter as RateLimiter, RateLimitedErr}, }; use crate::rpc::rate_limiter::RateLimiterItem; +use futures::FutureExt; +use libp2p::{PeerId, swarm::NotifyHandler}; +use logging::crit; +use smallvec::SmallVec; use std::time::{SystemTime, UNIX_EPOCH}; use std::{ collections::{HashMap, VecDeque, hash_map::Entry}, @@ -11,11 +15,6 @@ use std::{ task::{Context, Poll}, time::Duration, }; - -use futures::FutureExt; -use libp2p::{PeerId, swarm::NotifyHandler}; -use logging::crit; -use smallvec::SmallVec; use tokio_util::time::DelayQueue; use tracing::debug; use types::{EthSpec, ForkContext}; @@ -234,9 +233,29 @@ impl<Id: ReqId, E: EthSpec> SelfRateLimiter<Id, E> { pub fn peer_disconnected(&mut self, peer_id: PeerId) -> Vec<(Id, Protocol)> { self.active_requests.remove(&peer_id); + let mut failed_requests = Vec::new(); + + self.ready_requests.retain(|(req_peer_id, rpc_send, _)| { + if let RPCSend::Request(request_id, req) = rpc_send { + if req_peer_id == &peer_id { + failed_requests.push((*request_id, req.protocol())); + // Remove the entry + false + } else { + // Keep the entry + true + } + } else { + debug_assert!( + false, + "Coding error: unexpected RPCSend variant {rpc_send:?}." + ); + false + } + }); + // It's not ideal to iterate this map, but the key is (PeerId, Protocol) and this map // should never really be large. So we iterate for simplicity - let mut failed_requests = Vec::new(); self.delayed_requests .retain(|(map_peer_id, protocol), queue| { if map_peer_id == &peer_id { @@ -252,6 +271,7 @@ impl<Id: ReqId, E: EthSpec> SelfRateLimiter<Id, E> { true } }); + failed_requests } @@ -549,4 +569,72 @@ mod tests { .contains_key(&(peer2, Protocol::Ping)) ); } + + /// Test that `peer_disconnected` returns the IDs of pending requests. + #[tokio::test] + async fn test_peer_disconnected_returns_failed_requests() { + const REPLENISH_DURATION: u64 = 50; + let fork_context = std::sync::Arc::new(ForkContext::new::<MainnetEthSpec>( + Slot::new(0), + Hash256::ZERO, + &MainnetEthSpec::default_spec(), + )); + let config = OutboundRateLimiterConfig(RateLimiterConfig { + ping_quota: Quota::n_every_millis(NonZeroU64::new(1).unwrap(), REPLENISH_DURATION), + ..Default::default() + }); + let mut limiter: SelfRateLimiter<AppRequestId, MainnetEthSpec> = + SelfRateLimiter::new(Some(config), fork_context).unwrap(); + let peer_id = PeerId::random(); + + for i in 1..=5u32 { + let result = limiter.allows( + peer_id, + AppRequestId::Sync(SyncRequestId::SingleBlock { + id: SingleLookupReqId { + req_id: i, + lookup_id: i, + }, + }), + RequestType::Ping(Ping { data: i as u64 }), + ); + + // Check that the limiter allows the first request while other requests are added to the queue. + if i == 1 { + assert!(result.is_ok()); + } else { + assert!(result.is_err()); + } + } + + // Wait until the tokens have been regenerated, then run `next_peer_request_ready`. + tokio::time::sleep(Duration::from_millis(REPLENISH_DURATION + 10)).await; + limiter.next_peer_request_ready(peer_id, Protocol::Ping); + + // Check that one of the pending requests has moved to ready_requests. + assert_eq!( + limiter + .delayed_requests + .get(&(peer_id, Protocol::Ping)) + .unwrap() + .len(), + 3 + ); + assert_eq!(limiter.ready_requests.len(), 1); + + let mut failed_requests = limiter.peer_disconnected(peer_id); + + // Check that the limiter returns the IDs of pending requests and that the IDs are ordered correctly. + assert_eq!(failed_requests.len(), 4); + for i in 2..=5u32 { + let (request_id, protocol) = failed_requests.remove(0); + assert!(matches!( + request_id, + AppRequestId::Sync(SyncRequestId::SingleBlock { + id: SingleLookupReqId { req_id, .. }, + }) if req_id == i + )); + assert_eq!(protocol, Protocol::Ping); + } + } } From 8d72cc34ebe3c94c0856a0c9534943179e11b39f Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:40:01 -0700 Subject: [PATCH 07/81] Add sync request metrics (#7790) Add error rates metrics on unstable to benchmark against tree-sync. In my branch there are frequent errors but mostly connections errors as the node is still finding it set of stable peers. These metrics are very useful and unstable can benefit from them ahead of tree-sync Add three new metrics: - sync_rpc_requests_success_total: Total count of sync RPC requests successes - sync_rpc_requests_error_total: Total count of sync RPC requests errors - sync_rpc_request_duration_sec: Time to complete a successful sync RPC requesst Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com> --- beacon_node/network/src/metrics.rs | 24 ++++++++ .../network/src/sync/network_context.rs | 37 +++---------- .../src/sync/network_context/requests.rs | 55 ++++++++++++++++--- 3 files changed, 80 insertions(+), 36 deletions(-) diff --git a/beacon_node/network/src/metrics.rs b/beacon_node/network/src/metrics.rs index cea06a28c8..0016f66c01 100644 --- a/beacon_node/network/src/metrics.rs +++ b/beacon_node/network/src/metrics.rs @@ -507,6 +507,30 @@ pub static SYNC_UNKNOWN_NETWORK_REQUESTS: LazyLock<Result<IntCounterVec>> = Lazy &["type"], ) }); +pub static SYNC_RPC_REQUEST_SUCCESSES: LazyLock<Result<IntCounterVec>> = LazyLock::new(|| { + try_create_int_counter_vec( + "sync_rpc_requests_success_total", + "Total count of sync RPC requests successes", + &["protocol"], + ) +}); +pub static SYNC_RPC_REQUEST_ERRORS: LazyLock<Result<IntCounterVec>> = LazyLock::new(|| { + try_create_int_counter_vec( + "sync_rpc_requests_error_total", + "Total count of sync RPC requests errors", + &["protocol", "error"], + ) +}); +pub static SYNC_RPC_REQUEST_TIME: LazyLock<Result<HistogramVec>> = LazyLock::new(|| { + try_create_histogram_vec_with_buckets( + "sync_rpc_request_duration_sec", + "Time to complete a successful sync RPC requesst", + Ok(vec![ + 0.001, 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 1.0, 2.0, + ]), + &["protocol"], + ) +}); /* * Block Delay Metrics diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index 7f4da9c0da..542625b8a3 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -1430,7 +1430,7 @@ impl<T: BeaconChainTypes> SyncNetworkContext<T> { } }) }); - self.on_rpc_response_result(id, "BlocksByRoot", resp, peer_id, |_| 1) + self.on_rpc_response_result(resp, peer_id) } pub(crate) fn on_single_blob_response( @@ -1459,7 +1459,7 @@ impl<T: BeaconChainTypes> SyncNetworkContext<T> { } }) }); - self.on_rpc_response_result(id, "BlobsByRoot", resp, peer_id, |_| 1) + self.on_rpc_response_result(resp, peer_id) } #[allow(clippy::type_complexity)] @@ -1472,7 +1472,7 @@ impl<T: BeaconChainTypes> SyncNetworkContext<T> { let resp = self .data_columns_by_root_requests .on_response(id, rpc_event); - self.on_rpc_response_result(id, "DataColumnsByRoot", resp, peer_id, |_| 1) + self.on_rpc_response_result(resp, peer_id) } #[allow(clippy::type_complexity)] @@ -1483,7 +1483,7 @@ impl<T: BeaconChainTypes> SyncNetworkContext<T> { rpc_event: RpcEvent<Arc<SignedBeaconBlock<T::EthSpec>>>, ) -> Option<RpcResponseResult<Vec<Arc<SignedBeaconBlock<T::EthSpec>>>>> { let resp = self.blocks_by_range_requests.on_response(id, rpc_event); - self.on_rpc_response_result(id, "BlocksByRange", resp, peer_id, |b| b.len()) + self.on_rpc_response_result(resp, peer_id) } #[allow(clippy::type_complexity)] @@ -1494,7 +1494,7 @@ impl<T: BeaconChainTypes> SyncNetworkContext<T> { rpc_event: RpcEvent<Arc<BlobSidecar<T::EthSpec>>>, ) -> Option<RpcResponseResult<Vec<Arc<BlobSidecar<T::EthSpec>>>>> { let resp = self.blobs_by_range_requests.on_response(id, rpc_event); - self.on_rpc_response_result(id, "BlobsByRangeRequest", resp, peer_id, |b| b.len()) + self.on_rpc_response_result(resp, peer_id) } #[allow(clippy::type_complexity)] @@ -1507,36 +1507,15 @@ impl<T: BeaconChainTypes> SyncNetworkContext<T> { let resp = self .data_columns_by_range_requests .on_response(id, rpc_event); - self.on_rpc_response_result(id, "DataColumnsByRange", resp, peer_id, |d| d.len()) + self.on_rpc_response_result(resp, peer_id) } - fn on_rpc_response_result<I: std::fmt::Display, R, F: FnOnce(&R) -> usize>( + /// Common handler for consistent scoring of RpcResponseError + fn on_rpc_response_result<R>( &mut self, - id: I, - method: &'static str, resp: Option<RpcResponseResult<R>>, peer_id: PeerId, - get_count: F, ) -> Option<RpcResponseResult<R>> { - match &resp { - None => {} - Some(Ok((v, _))) => { - debug!( - %id, - method, - count = get_count(v), - "Sync RPC request completed" - ); - } - Some(Err(e)) => { - debug!( - %id, - method, - error = ?e, - "Sync RPC request error" - ); - } - } if let Some(Err(RpcResponseError::VerifyError(e))) = &resp { self.report_peer(peer_id, PeerAction::LowToleranceError, e.into()); } diff --git a/beacon_node/network/src/sync/network_context/requests.rs b/beacon_node/network/src/sync/network_context/requests.rs index 3183c06d76..8f9540693e 100644 --- a/beacon_node/network/src/sync/network_context/requests.rs +++ b/beacon_node/network/src/sync/network_context/requests.rs @@ -1,10 +1,11 @@ +use std::time::Instant; use std::{collections::hash_map::Entry, hash::Hash}; use beacon_chain::validator_monitor::timestamp_now; use fnv::FnvHashMap; use lighthouse_network::PeerId; use strum::IntoStaticStr; -use tracing::Span; +use tracing::{Span, debug}; use types::{Hash256, Slot}; pub use blobs_by_range::BlobsByRangeRequestItems; @@ -18,7 +19,7 @@ pub use data_columns_by_root::{ use crate::metrics; -use super::{RpcEvent, RpcResponseResult}; +use super::{RpcEvent, RpcResponseError, RpcResponseResult}; mod blobs_by_range; mod blobs_by_root; @@ -51,6 +52,7 @@ struct ActiveRequest<T: ActiveRequestItems> { peer_id: PeerId, // Error if the request terminates before receiving max expected responses expect_max_responses: bool, + start_instant: Instant, span: Span, } @@ -60,7 +62,7 @@ enum State<T> { Errored, } -impl<K: Eq + Hash, T: ActiveRequestItems> ActiveRequests<K, T> { +impl<K: Copy + Eq + Hash + std::fmt::Display, T: ActiveRequestItems> ActiveRequests<K, T> { pub fn new(name: &'static str) -> Self { Self { requests: <_>::default(), @@ -83,6 +85,7 @@ impl<K: Eq + Hash, T: ActiveRequestItems> ActiveRequests<K, T> { state: State::Active(items), peer_id, expect_max_responses, + start_instant: Instant::now(), span, }, ); @@ -112,7 +115,7 @@ impl<K: Eq + Hash, T: ActiveRequestItems> ActiveRequests<K, T> { return None; }; - match rpc_event { + let result = match rpc_event { // Handler of a success ReqResp chunk. Adds the item to the request accumulator. // `ActiveRequestItems` validates the item before appending to its internal state. RpcEvent::Response(item, seen_timestamp) => { @@ -126,7 +129,7 @@ impl<K: Eq + Hash, T: ActiveRequestItems> ActiveRequests<K, T> { Ok(true) => { let items = items.consume(); request.state = State::CompletedEarly; - Some(Ok((items, seen_timestamp))) + Some(Ok((items, seen_timestamp, request.start_instant.elapsed()))) } // Received item, but we are still expecting more Ok(false) => None, @@ -163,7 +166,11 @@ impl<K: Eq + Hash, T: ActiveRequestItems> ActiveRequests<K, T> { } .into())) } else { - Some(Ok((items.consume(), timestamp_now()))) + Some(Ok(( + items.consume(), + timestamp_now(), + request.start_instant.elapsed(), + ))) } } // Items already returned, ignore stream termination @@ -188,7 +195,41 @@ impl<K: Eq + Hash, T: ActiveRequestItems> ActiveRequests<K, T> { State::Errored => None, } } - } + }; + + result.map(|result| match result { + Ok((items, seen_timestamp, duration)) => { + metrics::inc_counter_vec(&metrics::SYNC_RPC_REQUEST_SUCCESSES, &[self.name]); + metrics::observe_timer_vec(&metrics::SYNC_RPC_REQUEST_TIME, &[self.name], duration); + debug!( + %id, + method = self.name, + count = items.len(), + "Sync RPC request completed" + ); + + Ok((items, seen_timestamp)) + } + Err(e) => { + let err_str: &'static str = match &e { + RpcResponseError::RpcError(e) => e.into(), + RpcResponseError::VerifyError(e) => e.into(), + RpcResponseError::CustodyRequestError(_) => "CustodyRequestError", + RpcResponseError::BlockComponentCouplingError(_) => { + "BlockComponentCouplingError" + } + }; + metrics::inc_counter_vec(&metrics::SYNC_RPC_REQUEST_ERRORS, &[self.name, err_str]); + debug!( + %id, + method = self.name, + error = ?e, + "Sync RPC request error" + ); + + Err(e) + } + }) } pub fn active_requests_of_peer(&self, peer_id: &PeerId) -> Vec<&K> { From d7c78a7f89bd9f3a3330ef60d8a4e58959f73227 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Wed, 11 Feb 2026 16:45:48 -0700 Subject: [PATCH 08/81] rename --reconstruct-historic-states to --archive (#8795) Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com> --- beacon_node/beacon_chain/src/builder.rs | 10 +++----- beacon_node/beacon_chain/src/chain_config.rs | 4 +-- .../beacon_chain/src/historical_blocks.rs | 5 +--- .../tests/attestation_verification.rs | 4 +-- .../beacon_chain/tests/blob_verification.rs | 2 +- .../beacon_chain/tests/block_verification.rs | 2 +- .../beacon_chain/tests/column_verification.rs | 2 +- .../tests/payload_invalidation.rs | 2 +- beacon_node/beacon_chain/tests/rewards.rs | 4 +-- .../beacon_chain/tests/schema_stability.rs | 2 +- beacon_node/beacon_chain/tests/store_tests.rs | 25 ++++++++----------- beacon_node/beacon_chain/tests/tests.rs | 8 +++--- beacon_node/http_api/tests/tests.rs | 2 +- beacon_node/src/cli.rs | 9 ++++--- beacon_node/src/config.rs | 4 +-- book/src/advanced_checkpoint_sync.md | 2 +- book/src/api_validator_inclusion.md | 2 +- book/src/faq.md | 12 ++++----- book/src/help_bn.md | 7 +++--- lighthouse/tests/beacon_node.rs | 19 +++++++++----- testing/ef_tests/src/cases/fork_choice.rs | 2 +- testing/node_test_rig/src/lib.rs | 2 +- 22 files changed, 67 insertions(+), 64 deletions(-) diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index cf6cb1598b..f673519f5f 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -372,8 +372,8 @@ where // Initialize anchor info before attempting to write the genesis state. // Since v4.4.0 we will set the anchor with a dummy state upper limit in order to prevent - // historic states from being retained (unless `--reconstruct-historic-states` is set). - let retain_historic_states = self.chain_config.reconstruct_historic_states; + // historic states from being retained (unless `--archive` is set). + let retain_historic_states = self.chain_config.archive; let genesis_beacon_block = genesis_block(&mut beacon_state, &self.spec)?; self.pending_io_batch.push( store @@ -529,7 +529,7 @@ where // case it will be stored in the hot DB. In this case, we need to ensure the store's anchor // is initialised prior to storing the state, as the anchor is required for working out // hdiff storage strategies. - let retain_historic_states = self.chain_config.reconstruct_historic_states; + let retain_historic_states = self.chain_config.archive; self.pending_io_batch.push( store .init_anchor_info( @@ -1125,9 +1125,7 @@ where ); // Check for states to reconstruct (in the background). - if beacon_chain.config.reconstruct_historic_states - && beacon_chain.store.get_oldest_block_slot() == 0 - { + if beacon_chain.config.archive && beacon_chain.store.get_oldest_block_slot() == 0 { beacon_chain.store_migrator.process_reconstruction(); } diff --git a/beacon_node/beacon_chain/src/chain_config.rs b/beacon_node/beacon_chain/src/chain_config.rs index ad923000e2..e9cc4f24e9 100644 --- a/beacon_node/beacon_chain/src/chain_config.rs +++ b/beacon_node/beacon_chain/src/chain_config.rs @@ -38,7 +38,7 @@ pub struct ChainConfig { /// If `None`, there is no weak subjectivity verification. pub weak_subjectivity_checkpoint: Option<Checkpoint>, /// Determine whether to reconstruct historic states, usually after a checkpoint sync. - pub reconstruct_historic_states: bool, + pub archive: bool, /// The max size of a message that can be sent over the network. pub max_network_size: usize, /// Maximum percentage of the head committee weight at which to attempt re-orging the canonical head. @@ -130,7 +130,7 @@ impl Default for ChainConfig { Self { import_max_skip_slots: None, weak_subjectivity_checkpoint: None, - reconstruct_historic_states: false, + archive: false, max_network_size: 10 * 1_048_576, // 10M re_org_head_threshold: Some(DEFAULT_RE_ORG_HEAD_THRESHOLD), re_org_parent_threshold: Some(DEFAULT_RE_ORG_PARENT_THRESHOLD), diff --git a/beacon_node/beacon_chain/src/historical_blocks.rs b/beacon_node/beacon_chain/src/historical_blocks.rs index 45ae9d7b84..3a3c3739c7 100644 --- a/beacon_node/beacon_chain/src/historical_blocks.rs +++ b/beacon_node/beacon_chain/src/historical_blocks.rs @@ -305,10 +305,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> { // If backfill has completed and the chain is configured to reconstruct historic states, // send a message to the background migrator instructing it to begin reconstruction. // This can only happen if we have backfilled all the way to genesis. - if backfill_complete - && self.genesis_backfill_slot == Slot::new(0) - && self.config.reconstruct_historic_states - { + if backfill_complete && self.genesis_backfill_slot == Slot::new(0) && self.config.archive { self.store_migrator.process_reconstruction(); } diff --git a/beacon_node/beacon_chain/tests/attestation_verification.rs b/beacon_node/beacon_chain/tests/attestation_verification.rs index 208798dfdf..8aeb881aa4 100644 --- a/beacon_node/beacon_chain/tests/attestation_verification.rs +++ b/beacon_node/beacon_chain/tests/attestation_verification.rs @@ -54,7 +54,7 @@ fn get_harness(validator_count: usize) -> BeaconChainHarness<EphemeralHarnessTyp let harness = BeaconChainHarness::builder(MainnetEthSpec) .spec(spec) .chain_config(ChainConfig { - reconstruct_historic_states: true, + archive: true, ..ChainConfig::default() }) .keypairs(KEYPAIRS[0..validator_count].to_vec()) @@ -91,7 +91,7 @@ fn get_harness_capella_spec( let harness = BeaconChainHarness::builder(MainnetEthSpec) .spec(spec.clone()) .chain_config(ChainConfig { - reconstruct_historic_states: true, + archive: true, ..ChainConfig::default() }) .keypairs(validator_keypairs) diff --git a/beacon_node/beacon_chain/tests/blob_verification.rs b/beacon_node/beacon_chain/tests/blob_verification.rs index e39c53729f..ee61177b2a 100644 --- a/beacon_node/beacon_chain/tests/blob_verification.rs +++ b/beacon_node/beacon_chain/tests/blob_verification.rs @@ -29,7 +29,7 @@ fn get_harness( let harness = BeaconChainHarness::builder(MainnetEthSpec) .spec(spec) .chain_config(ChainConfig { - reconstruct_historic_states: true, + archive: true, ..ChainConfig::default() }) .keypairs(KEYPAIRS[0..validator_count].to_vec()) diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index 417d2811dd..d214ea6b15 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -119,7 +119,7 @@ fn get_harness( let harness = BeaconChainHarness::builder(MainnetEthSpec) .default_spec() .chain_config(ChainConfig { - reconstruct_historic_states: true, + archive: true, ..ChainConfig::default() }) .keypairs(KEYPAIRS[0..validator_count].to_vec()) diff --git a/beacon_node/beacon_chain/tests/column_verification.rs b/beacon_node/beacon_chain/tests/column_verification.rs index ca9893941a..9941c957e2 100644 --- a/beacon_node/beacon_chain/tests/column_verification.rs +++ b/beacon_node/beacon_chain/tests/column_verification.rs @@ -32,7 +32,7 @@ fn get_harness( let harness = BeaconChainHarness::builder(MainnetEthSpec) .spec(spec) .chain_config(ChainConfig { - reconstruct_historic_states: true, + archive: true, ..ChainConfig::default() }) .keypairs(KEYPAIRS[0..validator_count].to_vec()) diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index f1e52de27b..eb8e57a5d5 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -50,7 +50,7 @@ impl InvalidPayloadRig { let harness = BeaconChainHarness::builder(MainnetEthSpec) .spec(spec.into()) .chain_config(ChainConfig { - reconstruct_historic_states: true, + archive: true, ..ChainConfig::default() }) .deterministic_keypairs(VALIDATOR_COUNT) diff --git a/beacon_node/beacon_chain/tests/rewards.rs b/beacon_node/beacon_chain/tests/rewards.rs index 94ad97c963..bc7c98041f 100644 --- a/beacon_node/beacon_chain/tests/rewards.rs +++ b/beacon_node/beacon_chain/tests/rewards.rs @@ -29,7 +29,7 @@ static KEYPAIRS: LazyLock<Vec<Keypair>> = fn get_harness(spec: ChainSpec) -> BeaconChainHarness<EphemeralHarnessType<E>> { let chain_config = ChainConfig { - reconstruct_historic_states: true, + archive: true, ..Default::default() }; @@ -48,7 +48,7 @@ fn get_harness(spec: ChainSpec) -> BeaconChainHarness<EphemeralHarnessType<E>> { fn get_electra_harness(spec: ChainSpec) -> BeaconChainHarness<EphemeralHarnessType<E>> { let chain_config = ChainConfig { - reconstruct_historic_states: true, + archive: true, ..Default::default() }; diff --git a/beacon_node/beacon_chain/tests/schema_stability.rs b/beacon_node/beacon_chain/tests/schema_stability.rs index 3dc009366d..8200748ae6 100644 --- a/beacon_node/beacon_chain/tests/schema_stability.rs +++ b/beacon_node/beacon_chain/tests/schema_stability.rs @@ -70,7 +70,7 @@ async fn schema_stability() { let store = get_store(&datadir, store_config, spec.clone()); let chain_config = ChainConfig { - reconstruct_historic_states: true, + archive: true, ..ChainConfig::default() }; diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 5410f26a5d..6bea5f6013 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -100,7 +100,7 @@ fn get_harness( ) -> TestHarness { // Most tests expect to retain historic states, so we use this as the default. let chain_config = ChainConfig { - reconstruct_historic_states: true, + archive: true, ..ChainConfig::default() }; get_harness_generic( @@ -118,7 +118,7 @@ fn get_harness_import_all_data_columns( // Most tests expect to retain historic states, so we use this as the default. let chain_config = ChainConfig { ignore_ws_check: true, - reconstruct_historic_states: true, + archive: true, ..ChainConfig::default() }; get_harness_generic( @@ -2876,7 +2876,7 @@ async fn reproduction_unaligned_checkpoint_sync_pruned_payload() { slot_clock.set_slot(harness.get_current_slot().as_u64()); let chain_config = ChainConfig { - reconstruct_historic_states: true, + archive: true, ..ChainConfig::default() }; @@ -3030,9 +3030,9 @@ async fn weak_subjectivity_sync_test( slot_clock.set_slot(harness.get_current_slot().as_u64()); let chain_config = ChainConfig { - // Set reconstruct_historic_states to true from the start in the genesis case. This makes + // Set archive to true from the start in the genesis case. This makes // some of the later checks more uniform across the genesis/non-genesis cases. - reconstruct_historic_states: checkpoint_slot == 0, + archive: checkpoint_slot == 0, ..ChainConfig::default() }; @@ -3685,7 +3685,7 @@ async fn process_blocks_and_attestations_for_unaligned_checkpoint() { let temp = tempdir().unwrap(); let store = get_store(&temp); let chain_config = ChainConfig { - reconstruct_historic_states: false, + archive: false, ..ChainConfig::default() }; let harness = get_harness_generic( @@ -4110,16 +4110,13 @@ async fn revert_minority_fork_on_resume() { // version is correct. This is the easiest schema test to write without historic versions of // Lighthouse on-hand, but has the disadvantage that the min version needs to be adjusted manually // as old downgrades are deprecated. -async fn schema_downgrade_to_min_version( - store_config: StoreConfig, - reconstruct_historic_states: bool, -) { +async fn schema_downgrade_to_min_version(store_config: StoreConfig, archive: bool) { let num_blocks_produced = E::slots_per_epoch() * 4; let db_path = tempdir().unwrap(); let spec = test_spec::<E>(); let chain_config = ChainConfig { - reconstruct_historic_states, + archive, ..ChainConfig::default() }; @@ -4174,7 +4171,7 @@ async fn schema_downgrade_to_min_version( .build(); // Check chain dump for appropriate range depending on whether this is an archive node. - let chain_dump_start_slot = if reconstruct_historic_states { + let chain_dump_start_slot = if archive { Slot::new(0) } else { store.get_split_slot() @@ -5154,7 +5151,7 @@ async fn ancestor_state_root_prior_to_split() { ..StoreConfig::default() }; let chain_config = ChainConfig { - reconstruct_historic_states: false, + archive: false, ..ChainConfig::default() }; @@ -5247,7 +5244,7 @@ async fn replay_from_split_state() { ..StoreConfig::default() }; let chain_config = ChainConfig { - reconstruct_historic_states: false, + archive: false, ..ChainConfig::default() }; diff --git a/beacon_node/beacon_chain/tests/tests.rs b/beacon_node/beacon_chain/tests/tests.rs index fb86a1a845..b052ba66f1 100644 --- a/beacon_node/beacon_chain/tests/tests.rs +++ b/beacon_node/beacon_chain/tests/tests.rs @@ -33,7 +33,7 @@ fn get_harness(validator_count: usize) -> BeaconChainHarness<EphemeralHarnessTyp get_harness_with_config( validator_count, ChainConfig { - reconstruct_historic_states: true, + archive: true, ..Default::default() }, ) @@ -44,7 +44,7 @@ fn get_harness_with_spec( spec: &ChainSpec, ) -> BeaconChainHarness<EphemeralHarnessType<MainnetEthSpec>> { let chain_config = ChainConfig { - reconstruct_historic_states: true, + archive: true, ..Default::default() }; let harness = BeaconChainHarness::builder(MainnetEthSpec) @@ -85,7 +85,7 @@ fn get_harness_semi_supernode( let harness = BeaconChainHarness::builder(MinimalEthSpec) .default_spec() .chain_config(ChainConfig { - reconstruct_historic_states: true, + archive: true, ..Default::default() }) .keypairs(KEYPAIRS[0..validator_count].to_vec()) @@ -950,7 +950,7 @@ async fn pseudo_finalize_test_generic( let num_blocks_produced = MinimalEthSpec::slots_per_epoch() * 5; let chain_config = ChainConfig { - reconstruct_historic_states: true, + archive: true, epochs_per_migration, ..Default::default() }; diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index bef9fe6acd..a49362d815 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -135,7 +135,7 @@ impl ApiTester { let mut harness = BeaconChainHarness::builder(MainnetEthSpec) .spec(spec.clone()) .chain_config(ChainConfig { - reconstruct_historic_states: config.retain_historic_states, + archive: config.retain_historic_states, ..ChainConfig::default() }) .deterministic_keypairs(VALIDATOR_COUNT) diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 5c3e8058d9..61dccc9674 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -1246,9 +1246,12 @@ pub fn cli_app() -> Command { .display_order(0) ) .arg( - Arg::new("reconstruct-historic-states") - .long("reconstruct-historic-states") - .help("After a checkpoint sync, reconstruct historic states in the database. This requires syncing all the way back to genesis.") + Arg::new("archive") + .long("archive") + .alias("reconstruct-historic-states") + .help("Store all beacon states in the database. When checkpoint syncing, \ + states are reconstructed after backfill completes. This requires \ + syncing all the way back to genesis.") .action(ArgAction::SetTrue) .help_heading(FLAG_HEADER) .display_order(0) diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index e6091d9213..0a52bcef06 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -554,8 +554,8 @@ pub fn get_config<E: EthSpec>( ClientGenesis::DepositContract }; - if cli_args.get_flag("reconstruct-historic-states") { - client_config.chain.reconstruct_historic_states = true; + if cli_args.get_flag("archive") { + client_config.chain.archive = true; client_config.chain.genesis_backfill = true; } diff --git a/book/src/advanced_checkpoint_sync.md b/book/src/advanced_checkpoint_sync.md index 7c30598928..0682310dcd 100644 --- a/book/src/advanced_checkpoint_sync.md +++ b/book/src/advanced_checkpoint_sync.md @@ -102,7 +102,7 @@ lack of historic states. _You do not need these states to run a staking node_, b for historical API calls (as used by block explorers and researchers). To run an archived node, you can opt-in to reconstructing all of the historic states by providing the -`--reconstruct-historic-states` flag to the beacon node at any point (before, during or after sync). +`--archive` flag to the beacon node at any point (before, during or after sync). The database keeps track of three markers to determine the availability of historic blocks and states: diff --git a/book/src/api_validator_inclusion.md b/book/src/api_validator_inclusion.md index eef563dcdb..d86483e0ea 100644 --- a/book/src/api_validator_inclusion.md +++ b/book/src/api_validator_inclusion.md @@ -8,7 +8,7 @@ These endpoints are not stable or included in the Ethereum consensus standard AP they are subject to change or removal without a change in major release version. -In order to apply these APIs, you need to have historical states information in the database of your node. This means adding the flag `--reconstruct-historic-states` in the beacon node. Once the state reconstruction process is completed, you can apply these APIs to any epoch. +In order to apply these APIs, you need to have historical states information in the database of your node. This means adding the flag `--archive` in the beacon node. Once the state reconstruction process is completed, you can apply these APIs to any epoch. ## Endpoints diff --git a/book/src/faq.md b/book/src/faq.md index c9bc53533f..5ba2c3407f 100644 --- a/book/src/faq.md +++ b/book/src/faq.md @@ -167,19 +167,19 @@ This is a known [bug](https://github.com/sigp/lighthouse/issues/3707) that will ### <a name="bn-partial-history"></a> How can I construct only partial state history? -Lighthouse prunes finalized states by default. Nevertheless, it is quite often that users may be interested in the state history of a few epochs before finalization. To have access to these pruned states, Lighthouse typically requires a full reconstruction of states using the flag `--reconstruct-historic-states` (which will usually take a week). Partial state history can be achieved with some "tricks". Here are the general steps: +Lighthouse prunes finalized states by default. Nevertheless, it is quite often that users may be interested in the state history of a few epochs before finalization. To have access to these pruned states, Lighthouse typically requires a full reconstruction of states using the flag `--archive` (which will usually take a week). Partial state history can be achieved with some "tricks". Here are the general steps: 1. Delete the current database. You can do so with `--purge-db-force` or manually deleting the database from the data directory: `$datadir/beacon`. - 1. If you are interested in the states from the current slot and beyond, perform a checkpoint sync with the flag `--reconstruct-historic-states`, then you can skip the following and jump straight to Step 5 to check the database. + 1. If you are interested in the states from the current slot and beyond, perform a checkpoint sync with the flag `--archive`, then you can skip the following and jump straight to Step 5 to check the database. - If you are interested in the states before the current slot, identify the slot to perform a manual checkpoint sync. With the default configuration, this slot should be divisible by 2<sup>21</sup>, as this is where a full state snapshot is stored. With the flag `--reconstruct-historic-states`, the state upper limit will be adjusted to the next full snapshot slot, a slot that satisfies: `slot % 2**21 == 0`. In other words, to have the state history available before the current slot, we have to checkpoint sync 2<sup>21</sup> slots before the next full snapshot slot. + If you are interested in the states before the current slot, identify the slot to perform a manual checkpoint sync. With the default configuration, this slot should be divisible by 2<sup>21</sup>, as this is where a full state snapshot is stored. With the flag `--archive`, the state upper limit will be adjusted to the next full snapshot slot, a slot that satisfies: `slot % 2**21 == 0`. In other words, to have the state history available before the current slot, we have to checkpoint sync 2<sup>21</sup> slots before the next full snapshot slot. Example: Say the current mainnet is at slot `12000000`. As the next full state snapshot is at slot `12582912`, the slot that we want is slot `10485760`. You can calculate this (in Python) using `12000000 // 2**21 * 2**21`. 1. [Export](./advanced_checkpoint_sync.md#manual-checkpoint-sync) the blobs, block and state data for the slot identified in Step 2. This can be done from another beacon node that you have access to, or you could use any available public beacon API, e.g., [QuickNode](https://www.quicknode.com/docs/ethereum). - 1. Perform a [manual checkpoint sync](./advanced_checkpoint_sync.md#manual-checkpoint-sync) using the data from the previous step, and provide the flag `--reconstruct-historic-states`. + 1. Perform a [manual checkpoint sync](./advanced_checkpoint_sync.md#manual-checkpoint-sync) using the data from the previous step, and provide the flag `--archive`. 1. Check the database: @@ -193,9 +193,9 @@ Lighthouse prunes finalized states by default. Nevertheless, it is quite often t "state_upper_limit": "10485760", ``` -Lighthouse will now start to reconstruct historic states from slot `10485760`. At this point, if you do not want a full state reconstruction, you may remove the flag `--reconstruct-historic-states` (and restart). When the process is completed, you will have the state data from slot `10485760`. Going forward, Lighthouse will continue retaining all historical states newer than the snapshot. Eventually this can lead to increased disk usage, which presently can only be reduced by repeating the process starting from a more recent snapshot. +Lighthouse will now start to reconstruct historic states from slot `10485760`. At this point, if you do not want a full state reconstruction, you may remove the flag `--archive` (and restart). When the process is completed, you will have the state data from slot `10485760`. Going forward, Lighthouse will continue retaining all historical states newer than the snapshot. Eventually this can lead to increased disk usage, which presently can only be reduced by repeating the process starting from a more recent snapshot. -> Note: You may only be interested in very recent historic states. To do so, you may configure the full snapshot to be, for example, every 2<sup>11</sup> slots, see [database configuration](./advanced_database.md#hierarchical-state-diffs) for more details. This can be configured with the flag `--hierarchy-exponents 5,7,11` together with the flag `--reconstruct-historic-states`. This will affect the slot number in Step 2, while other steps remain the same. Note that this comes at the expense of a higher storage requirement. +> Note: You may only be interested in very recent historic states. To do so, you may configure the full snapshot to be, for example, every 2<sup>11</sup> slots, see [database configuration](./advanced_database.md#hierarchical-state-diffs) for more details. This can be configured with the flag `--hierarchy-exponents 5,7,11` together with the flag `--archive`. This will affect the slot number in Step 2, while other steps remain the same. Note that this comes at the expense of a higher storage requirement. > With `--hierarchy-exponents 5,7,11`, using the same example as above, the next full state snapshot is at slot `12001280`. So the slot to checkpoint sync from is: slot `11999232`. diff --git a/book/src/help_bn.md b/book/src/help_bn.md index beb74da376..cad21a3e78 100644 --- a/book/src/help_bn.md +++ b/book/src/help_bn.md @@ -439,6 +439,10 @@ Flags: intended for use by block builders, relays and developers. You should set a fee recipient on this BN and also consider adjusting the --prepare-payload-lookahead flag. + --archive + Store all beacon states in the database. When checkpoint syncing, + states are reconstructed after backfill completes. This requires + syncing all the way back to genesis. --builder-fallback-disable-checks This flag disables all checks related to chain health. This means the builder API will always be used for payload construction, regardless @@ -552,9 +556,6 @@ Flags: --purge-db-force If present, the chain database will be deleted without confirmation. Use with caution. - --reconstruct-historic-states - After a checkpoint sync, reconstruct historic states in the database. - This requires syncing all the way back to genesis. --reset-payload-statuses When present, Lighthouse will forget the payload statuses of any already-imported blocks. This can assist in the recovery from a diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index 322787736b..ded1f2b765 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -401,9 +401,9 @@ fn genesis_backfill_flag() { /// The genesis backfill flag should be enabled if historic states flag is set. #[test] -fn genesis_backfill_with_historic_flag() { +fn genesis_backfill_with_archive_flag() { CommandLineTest::new() - .flag("reconstruct-historic-states", None) + .flag("archive", None) .run_with_zero_port() .with_config(|config| assert!(config.chain.genesis_backfill)); } @@ -2030,17 +2030,24 @@ fn blob_prune_margin_epochs_on_startup_ten() { .with_config(|config| assert!(config.store.blob_prune_margin_epochs == 10)); } #[test] -fn reconstruct_historic_states_flag() { +fn archive_flag() { + CommandLineTest::new() + .flag("archive", None) + .run_with_zero_port() + .with_config(|config| assert!(config.chain.archive)); +} +#[test] +fn archive_flag_alias() { CommandLineTest::new() .flag("reconstruct-historic-states", None) .run_with_zero_port() - .with_config(|config| assert!(config.chain.reconstruct_historic_states)); + .with_config(|config| assert!(config.chain.archive)); } #[test] -fn no_reconstruct_historic_states_flag() { +fn no_archive_flag() { CommandLineTest::new() .run_with_zero_port() - .with_config(|config| assert!(!config.chain.reconstruct_historic_states)); + .with_config(|config| assert!(!config.chain.archive)); } #[test] fn epochs_per_migration_default() { diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index 45bed7c6cd..ca77dc8d79 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -448,7 +448,7 @@ impl<E: EthSpec> Tester<E> { .spec(spec.clone()) .keypairs(vec![]) .chain_config(ChainConfig { - reconstruct_historic_states: true, + archive: true, ..ChainConfig::default() }) .genesis_state_ephemeral_store(case.anchor_state.clone()) diff --git a/testing/node_test_rig/src/lib.rs b/testing/node_test_rig/src/lib.rs index e49d11ee1e..ece6001802 100644 --- a/testing/node_test_rig/src/lib.rs +++ b/testing/node_test_rig/src/lib.rs @@ -115,7 +115,7 @@ pub fn testing_client_config() -> ClientConfig { }; // Simulator tests expect historic states to be available for post-run checks. - client_config.chain.reconstruct_historic_states = true; + client_config.chain.archive = true; // Specify a constant count of beacon processor workers. Having this number // too low can cause annoying HTTP timeouts, especially on Github runners From 711971f269ec61fd8f4e108c26c1ddaa7176dd59 Mon Sep 17 00:00:00 2001 From: radik878 <radikpadik76@gmail.com> Date: Thu, 12 Feb 2026 01:45:50 +0200 Subject: [PATCH 09/81] fix: cache slot in check_block_relevancy to prevent TOCTOU (#8776) Co-Authored-By: radik878 <radikpadik76@gmail.com> --- beacon_node/beacon_chain/src/block_verification.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 9bb6757341..e0943d5d93 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -1798,10 +1798,12 @@ pub fn check_block_relevancy<T: BeaconChainTypes>( ) -> Result<Hash256, BlockError> { let block = signed_block.message(); + let present_slot = chain.slot()?; + // Do not process blocks from the future. - if block.slot() > chain.slot()? { + if block.slot() > present_slot { return Err(BlockError::FutureSlot { - present_slot: chain.slot()?, + present_slot, block_slot: block.slot(), }); } From b8072c5b7799f66ae7977e367b8ececdc2fb2682 Mon Sep 17 00:00:00 2001 From: Michael Sproul <michaelsproul@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:26:23 +1100 Subject: [PATCH 10/81] Gloas payload bid consensus (#8801) - [x] Consensus changes for execution payload bids - [x] EF tests for bids (and `block_header` -- no changes required). Co-Authored-By: Michael Sproul <michael@sigmaprime.io> --- .../src/per_block_processing.rs | 170 +++++++++++++++++- .../src/per_block_processing/errors.rs | 41 +++++ .../per_block_processing/signature_sets.rs | 66 ++++++- consensus/types/src/builder/builder.rs | 11 +- consensus/types/src/state/beacon_state.rs | 27 ++- testing/ef_tests/check_all_files_accessed.py | 2 - testing/ef_tests/src/cases/operations.rs | 39 +++- testing/ef_tests/src/handler.rs | 2 + testing/ef_tests/src/lib.rs | 12 +- testing/ef_tests/tests/tests.rs | 6 + 10 files changed, 358 insertions(+), 18 deletions(-) diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index 1de5083f6f..37639047fb 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -1,12 +1,17 @@ use crate::consensus_context::ConsensusContext; -use errors::{BlockOperationError, BlockProcessingError, HeaderInvalid}; +use errors::{ + BlockOperationError, BlockProcessingError, ExecutionPayloadBidInvalid, HeaderInvalid, +}; use rayon::prelude::*; use safe_arith::{ArithError, SafeArith}; -use signature_sets::{block_proposal_signature_set, get_pubkey_from_state, randao_signature_set}; +use signature_sets::{ + block_proposal_signature_set, execution_payload_bid_signature_set, + get_builder_pubkey_from_state, get_pubkey_from_state, randao_signature_set, +}; use std::borrow::Cow; use tree_hash::TreeHash; use typenum::Unsigned; -use types::*; +use types::{consts::gloas::BUILDER_INDEX_SELF_BUILD, *}; pub use self::verify_attester_slashing::{ get_slashable_indices, get_slashable_indices_modular, verify_attester_slashing, @@ -522,3 +527,162 @@ pub fn compute_timestamp_at_slot<E: EthSpec>( .safe_mul(spec.get_slot_duration().as_secs()) .and_then(|since_genesis| state.genesis_time().safe_add(since_genesis)) } + +pub fn can_builder_cover_bid<E: EthSpec>( + state: &BeaconState<E>, + builder_index: BuilderIndex, + builder: &Builder, + bid_amount: u64, + spec: &ChainSpec, +) -> Result<bool, BlockProcessingError> { + let builder_balance = builder.balance; + let pending_withdrawals_amount = + state.get_pending_balance_to_withdraw_for_builder(builder_index)?; + let min_balance = spec + .min_deposit_amount + .safe_add(pending_withdrawals_amount)?; + if builder_balance < min_balance { + Ok(false) + } else { + Ok(builder_balance.safe_sub(min_balance)? >= bid_amount) + } +} + +pub fn process_execution_payload_bid<E: EthSpec, Payload: AbstractExecPayload<E>>( + state: &mut BeaconState<E>, + block: BeaconBlockRef<'_, E, Payload>, + verify_signatures: VerifySignatures, + spec: &ChainSpec, +) -> Result<(), BlockProcessingError> { + // Verify the bid signature + let signed_bid = block.body().signed_execution_payload_bid()?; + + let bid = &signed_bid.message; + let amount = bid.value; + let builder_index = bid.builder_index; + + // For self-builds, amount must be zero regardless of withdrawal credential prefix + if builder_index == BUILDER_INDEX_SELF_BUILD { + block_verify!( + amount == 0, + ExecutionPayloadBidInvalid::SelfBuildNonZeroAmount.into() + ); + block_verify!( + signed_bid.signature.is_infinity(), + ExecutionPayloadBidInvalid::BadSignature.into() + ); + } else { + let builder = state.get_builder(builder_index)?; + + // Verify that the builder is active + block_verify!( + builder.is_active_at_finalized_epoch(state.finalized_checkpoint().epoch, spec), + ExecutionPayloadBidInvalid::BuilderNotActive(builder_index).into() + ); + + // Verify that the builder has funds to cover the bid + block_verify!( + can_builder_cover_bid(state, builder_index, builder, amount, spec)?, + ExecutionPayloadBidInvalid::InsufficientBalance { + builder_index, + builder_balance: builder.balance, + bid_value: amount, + } + .into() + ); + + if verify_signatures.is_true() { + block_verify!( + // We know this is NOT a self-build, so there MUST be a signature set (func does not + // return None). + execution_payload_bid_signature_set( + state, + |i| get_builder_pubkey_from_state(state, i), + signed_bid, + spec + )? + .ok_or(ExecutionPayloadBidInvalid::BadSignature)? + .verify(), + ExecutionPayloadBidInvalid::BadSignature.into() + ); + } + } + + // Verify commitments are under limit + let max_blobs_per_block = spec.max_blobs_per_block(state.current_epoch()) as usize; + block_verify!( + bid.blob_kzg_commitments.len() <= max_blobs_per_block, + ExecutionPayloadBidInvalid::ExcessBlobCommitments { + max: max_blobs_per_block, + bid: bid.blob_kzg_commitments.len(), + } + .into() + ); + + // Verify that the bid is for the current slot + block_verify!( + bid.slot == block.slot(), + ExecutionPayloadBidInvalid::SlotMismatch { + bid_slot: bid.slot, + block_slot: block.slot(), + } + .into() + ); + + // Verify that the bid is for the right parent block + let latest_block_hash = state.latest_block_hash()?; + block_verify!( + bid.parent_block_hash == *latest_block_hash, + ExecutionPayloadBidInvalid::ParentBlockHashMismatch { + state_block_hash: *latest_block_hash, + bid_parent_hash: bid.parent_block_hash, + } + .into() + ); + + block_verify!( + bid.parent_block_root == block.parent_root(), + ExecutionPayloadBidInvalid::ParentBlockRootMismatch { + block_parent_root: block.parent_root(), + bid_parent_root: bid.parent_block_root, + } + .into() + ); + + let expected_randao = *state.get_randao_mix(state.current_epoch())?; + block_verify!( + bid.prev_randao == expected_randao, + ExecutionPayloadBidInvalid::PrevRandaoMismatch { + expected: expected_randao, + bid: bid.prev_randao, + } + .into() + ); + + // Record the pending payment if there is some payment + if amount > 0 { + let pending_payment = BuilderPendingPayment { + weight: 0, + withdrawal: BuilderPendingWithdrawal { + fee_recipient: bid.fee_recipient, + amount, + builder_index, + }, + }; + + let payment_index = E::SlotsPerEpoch::to_usize() + .safe_add(bid.slot.as_usize().safe_rem(E::SlotsPerEpoch::to_usize())?)?; + + *state + .builder_pending_payments_mut()? + .get_mut(payment_index) + .ok_or(BlockProcessingError::BeaconStateError( + BeaconStateError::InvalidBuilderPendingPaymentsIndex(payment_index), + ))? = pending_payment; + } + + // Cache the execution bid + *state.latest_execution_payload_bid_mut()? = bid.clone(); + + Ok(()) +} diff --git a/consensus/state_processing/src/per_block_processing/errors.rs b/consensus/state_processing/src/per_block_processing/errors.rs index 5c1db9d732..53178a7a64 100644 --- a/consensus/state_processing/src/per_block_processing/errors.rs +++ b/consensus/state_processing/src/per_block_processing/errors.rs @@ -99,6 +99,9 @@ pub enum BlockProcessingError { IncorrectExpectedWithdrawalsVariant, MissingLastWithdrawal, PendingAttestationInElectra, + ExecutionPayloadBidInvalid { + reason: ExecutionPayloadBidInvalid, + }, /// Builder payment index out of bounds (Gloas) BuilderPaymentIndexOutOfBounds(usize), } @@ -157,6 +160,12 @@ impl From<milhouse::Error> for BlockProcessingError { } } +impl From<ExecutionPayloadBidInvalid> for BlockProcessingError { + fn from(reason: ExecutionPayloadBidInvalid) -> Self { + Self::ExecutionPayloadBidInvalid { reason } + } +} + impl From<BlockOperationError<HeaderInvalid>> for BlockProcessingError { fn from(e: BlockOperationError<HeaderInvalid>) -> BlockProcessingError { match e { @@ -452,6 +461,38 @@ pub enum ExitInvalid { PendingWithdrawalInQueue(u64), } +#[derive(Debug, PartialEq, Clone)] +pub enum ExecutionPayloadBidInvalid { + /// The validator set a non-zero amount for a self-build. + SelfBuildNonZeroAmount, + /// The signature is invalid. + BadSignature, + /// The builder is not active. + BuilderNotActive(u64), + /// The builder has insufficient balance to cover the bid + InsufficientBalance { + builder_index: u64, + builder_balance: u64, + bid_value: u64, + }, + /// Bid slot doesn't match block slot + SlotMismatch { bid_slot: Slot, block_slot: Slot }, + /// The bid's parent block hash doesn't match the state's latest block hash + ParentBlockHashMismatch { + state_block_hash: ExecutionBlockHash, + bid_parent_hash: ExecutionBlockHash, + }, + /// The bid's parent block root doesn't match the block's parent root + ParentBlockRootMismatch { + block_parent_root: Hash256, + bid_parent_root: Hash256, + }, + /// The bid's prev randao doesn't match the state. + PrevRandaoMismatch { expected: Hash256, bid: Hash256 }, + /// The bid contains more than the maximum number of kzg blob commitments. + ExcessBlobCommitments { max: usize, bid: usize }, +} + #[derive(Debug, PartialEq, Clone)] pub enum BlsExecutionChangeInvalid { /// The specified validator is not in the state's validator registry. diff --git a/consensus/state_processing/src/per_block_processing/signature_sets.rs b/consensus/state_processing/src/per_block_processing/signature_sets.rs index 0e936007ee..0cc591ba4c 100644 --- a/consensus/state_processing/src/per_block_processing/signature_sets.rs +++ b/consensus/state_processing/src/per_block_processing/signature_sets.rs @@ -9,11 +9,12 @@ use tree_hash::TreeHash; use typenum::Unsigned; use types::{ AbstractExecPayload, AttesterSlashingRef, BeaconBlockRef, BeaconState, BeaconStateError, - ChainSpec, DepositData, Domain, Epoch, EthSpec, Fork, Hash256, InconsistentFork, + BuilderIndex, ChainSpec, DepositData, Domain, Epoch, EthSpec, Fork, Hash256, InconsistentFork, IndexedAttestation, IndexedAttestationRef, ProposerSlashing, SignedAggregateAndProof, SignedBeaconBlock, SignedBeaconBlockHeader, SignedBlsToExecutionChange, - SignedContributionAndProof, SignedRoot, SignedVoluntaryExit, SigningData, Slot, SyncAggregate, - SyncAggregatorSelectionData, + SignedContributionAndProof, SignedExecutionPayloadBid, SignedRoot, SignedVoluntaryExit, + SigningData, Slot, SyncAggregate, SyncAggregatorSelectionData, + consts::gloas::BUILDER_INDEX_SELF_BUILD, }; pub type Result<T> = std::result::Result<T, Error>; @@ -28,6 +29,9 @@ pub enum Error { /// Attempted to find the public key of a validator that does not exist. You cannot distinguish /// between an error and an invalid block in this case. ValidatorUnknown(u64), + /// Attempted to find the public key of a builder that does not exist. You cannot distinguish + /// between an error and an invalid block in this case. + BuilderUnknown(BuilderIndex), /// Attempted to find the public key of a validator that does not exist. You cannot distinguish /// between an error and an invalid block in this case. ValidatorPubkeyUnknown(PublicKeyBytes), @@ -53,7 +57,7 @@ impl From<BeaconStateError> for Error { } } -/// Helper function to get a public key from a `state`. +/// Helper function to get a validator public key from a `state`. pub fn get_pubkey_from_state<E>( state: &BeaconState<E>, validator_index: usize, @@ -71,6 +75,25 @@ where .map(Cow::Owned) } +/// Helper function to get a builder public key from a `state`. +pub fn get_builder_pubkey_from_state<E>( + state: &BeaconState<E>, + builder_index: BuilderIndex, +) -> Option<Cow<'_, PublicKey>> +where + E: EthSpec, +{ + state + .builders() + .ok()? + .get(builder_index as usize) + .and_then(|b| { + let pk: Option<PublicKey> = b.pubkey.decompress().ok(); + pk + }) + .map(Cow::Owned) +} + /// A signature set that is valid if a block was signed by the expected block producer. pub fn block_proposal_signature_set<'a, E, F, Payload: AbstractExecPayload<E>>( state: &'a BeaconState<E>, @@ -332,6 +355,41 @@ where Ok(SignatureSet::multiple_pubkeys(signature, pubkeys, message)) } +pub fn execution_payload_bid_signature_set<'a, E, F>( + state: &'a BeaconState<E>, + get_builder_pubkey: F, + signed_execution_payload_bid: &'a SignedExecutionPayloadBid<E>, + spec: &'a ChainSpec, +) -> Result<Option<SignatureSet<'a>>> +where + E: EthSpec, + F: Fn(BuilderIndex) -> Option<Cow<'a, PublicKey>>, +{ + let execution_payload_bid = &signed_execution_payload_bid.message; + let builder_index = execution_payload_bid.builder_index; + if builder_index == BUILDER_INDEX_SELF_BUILD { + // No signatures to verify in case of a self-build, but consensus code MUST check that + // the signature is the point at infinity. + // See `process_execution_payload_bid`. + return Ok(None); + } + let domain = spec.get_domain( + state.current_epoch(), + Domain::BeaconBuilder, + &state.fork(), + state.genesis_validators_root(), + ); + + let pubkey = get_builder_pubkey(builder_index).ok_or(Error::BuilderUnknown(builder_index))?; + let message = execution_payload_bid.signing_root(domain); + + Ok(Some(SignatureSet::single_pubkey( + &signed_execution_payload_bid.signature, + pubkey, + message, + ))) +} + /// Returns the signature set for the given `attester_slashing` and corresponding `pubkeys`. pub fn attester_slashing_signature_sets<'a, E, F>( state: &'a BeaconState<E>, diff --git a/consensus/types/src/builder/builder.rs b/consensus/types/src/builder/builder.rs index 2bd50f42cc..7d494da3ee 100644 --- a/consensus/types/src/builder/builder.rs +++ b/consensus/types/src/builder/builder.rs @@ -1,5 +1,5 @@ use crate::test_utils::TestRandom; -use crate::{Address, Epoch, ForkName}; +use crate::{Address, ChainSpec, Epoch, ForkName}; use bls::PublicKeyBytes; use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; @@ -24,3 +24,12 @@ pub struct Builder { pub deposit_epoch: Epoch, pub withdrawable_epoch: Epoch, } + +impl Builder { + /// Check if a builder is active in a state with `finalized_epoch`. + /// + /// This implements `is_active_builder` from the spec. + pub fn is_active_at_finalized_epoch(&self, finalized_epoch: Epoch, spec: &ChainSpec) -> bool { + self.deposit_epoch < finalized_epoch && self.withdrawable_epoch == spec.far_future_epoch + } +} diff --git a/consensus/types/src/state/beacon_state.rs b/consensus/types/src/state/beacon_state.rs index 1745908c40..6838b588eb 100644 --- a/consensus/types/src/state/beacon_state.rs +++ b/consensus/types/src/state/beacon_state.rs @@ -9,7 +9,7 @@ use fixed_bytes::FixedBytesExtended; use int_to_bytes::{int_to_bytes4, int_to_bytes8}; use metastruct::{NumFields, metastruct}; use milhouse::{List, Vector}; -use safe_arith::{ArithError, SafeArith}; +use safe_arith::{ArithError, SafeArith, SafeArithIter}; use serde::{Deserialize, Deserializer, Serialize}; use ssz::{Decode, DecodeError, Encode, ssz_encode}; use ssz_derive::{Decode, Encode}; @@ -218,6 +218,7 @@ pub enum BeaconStateError { envelope_epoch: Epoch, }, InvalidIndicesCount, + InvalidBuilderPendingPaymentsIndex(usize), InvalidExecutionPayloadAvailabilityIndex(usize), } @@ -2749,6 +2750,30 @@ impl<E: EthSpec> BeaconState<E> { Ok(pending_balance) } + pub fn get_pending_balance_to_withdraw_for_builder( + &self, + builder_index: BuilderIndex, + ) -> Result<u64, BeaconStateError> { + let pending_withdrawals_total = self + .builder_pending_withdrawals()? + .iter() + .filter_map(|withdrawal| { + (withdrawal.builder_index == builder_index).then_some(withdrawal.amount) + }) + .safe_sum()?; + let pending_payments_total = self + .builder_pending_payments()? + .iter() + .filter_map(|payment| { + (payment.withdrawal.builder_index == builder_index) + .then_some(payment.withdrawal.amount) + }) + .safe_sum()?; + pending_withdrawals_total + .safe_add(pending_payments_total) + .map_err(Into::into) + } + // ******* Electra mutators ******* pub fn queue_excess_active_balance( diff --git a/testing/ef_tests/check_all_files_accessed.py b/testing/ef_tests/check_all_files_accessed.py index 8e5bd24d24..628ee83936 100755 --- a/testing/ef_tests/check_all_files_accessed.py +++ b/testing/ef_tests/check_all_files_accessed.py @@ -48,8 +48,6 @@ excluded_paths = [ "tests/.*/eip7732", "tests/.*/eip7805", # TODO(gloas): remove these ignores as more Gloas operations are implemented - "tests/.*/gloas/operations/block_header/.*", - "tests/.*/gloas/operations/execution_payload_bid/.*", "tests/.*/gloas/operations/payload_attestation/.*", # TODO(EIP-7732): remove these ignores as Gloas consensus is implemented "tests/.*/gloas/epoch_processing/.*", diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index ef998a94ba..2f08727045 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -16,7 +16,7 @@ use state_processing::{ per_block_processing::{ VerifyBlockRoot, VerifySignatures, errors::BlockProcessingError, - process_block_header, process_execution_payload, + process_block_header, process_execution_payload, process_execution_payload_bid, process_operations::{ altair_deneb, base, gloas, process_attester_slashings, process_bls_to_execution_changes, process_deposits, process_exits, @@ -51,6 +51,12 @@ pub struct WithdrawalsPayload<E: EthSpec> { payload: Option<ExecutionPayload<E>>, } +/// Newtype for testing execution payload bids. +#[derive(Debug, Clone, Deserialize)] +pub struct ExecutionPayloadBidBlock<E: EthSpec> { + block: BeaconBlock<E>, +} + #[derive(Debug, Clone)] pub struct Operations<E: EthSpec, O: Operation<E>> { metadata: Metadata, @@ -459,6 +465,37 @@ impl<E: EthSpec> Operation<E> for SignedExecutionPayloadEnvelope<E> { } } +impl<E: EthSpec> Operation<E> for ExecutionPayloadBidBlock<E> { + type Error = BlockProcessingError; + + fn handler_name() -> String { + "execution_payload_bid".into() + } + + fn filename() -> String { + "block.ssz_snappy".into() + } + + fn is_enabled_for_fork(fork_name: ForkName) -> bool { + fork_name.gloas_enabled() + } + + fn decode(path: &Path, _fork_name: ForkName, spec: &ChainSpec) -> Result<Self, Error> { + ssz_decode_file_with(path, |bytes| BeaconBlock::from_ssz_bytes(bytes, spec)) + .map(|block| ExecutionPayloadBidBlock { block }) + } + + fn apply_to( + &self, + state: &mut BeaconState<E>, + spec: &ChainSpec, + _: &Operations<E, Self>, + ) -> Result<(), BlockProcessingError> { + process_execution_payload_bid(state, self.block.to_ref(), VerifySignatures::True, spec)?; + Ok(()) + } +} + impl<E: EthSpec> Operation<E> for WithdrawalsPayload<E> { type Error = BlockProcessingError; diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index 9d11252edb..5a43642c88 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -1139,11 +1139,13 @@ impl<E: EthSpec + TypeName, O: Operation<E>> Handler for OperationsHandler<E, O> && (!fork_name.gloas_enabled() || self.handler_name() == "attestation" || self.handler_name() == "attester_slashing" + || self.handler_name() == "block_header" || self.handler_name() == "bls_to_execution_change" || self.handler_name() == "consolidation_request" || self.handler_name() == "deposit_request" || self.handler_name() == "deposit" || self.handler_name() == "execution_payload" + || self.handler_name() == "execution_payload_bid" || self.handler_name() == "proposer_slashing" || self.handler_name() == "sync_aggregate" || self.handler_name() == "withdrawal_request" diff --git a/testing/ef_tests/src/lib.rs b/testing/ef_tests/src/lib.rs index 8ec4860cab..49bea7d85f 100644 --- a/testing/ef_tests/src/lib.rs +++ b/testing/ef_tests/src/lib.rs @@ -1,11 +1,11 @@ pub use case_result::CaseResult; -pub use cases::WithdrawalsPayload; pub use cases::{ - Case, EffectiveBalanceUpdates, Eth1DataReset, FeatureName, HistoricalRootsUpdate, - HistoricalSummariesUpdate, InactivityUpdates, JustificationAndFinalization, - ParticipationFlagUpdates, ParticipationRecordUpdates, PendingBalanceDeposits, - PendingConsolidations, ProposerLookahead, RandaoMixesReset, RegistryUpdates, - RewardsAndPenalties, Slashings, SlashingsReset, SyncCommitteeUpdates, + Case, EffectiveBalanceUpdates, Eth1DataReset, ExecutionPayloadBidBlock, FeatureName, + HistoricalRootsUpdate, HistoricalSummariesUpdate, InactivityUpdates, + JustificationAndFinalization, ParticipationFlagUpdates, ParticipationRecordUpdates, + PendingBalanceDeposits, PendingConsolidations, ProposerLookahead, RandaoMixesReset, + RegistryUpdates, RewardsAndPenalties, Slashings, SlashingsReset, SyncCommitteeUpdates, + WithdrawalsPayload, }; pub use decode::log_file_access; pub use error::Error; diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index 8a53a61929..332f077984 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -93,6 +93,12 @@ fn operations_execution_payload_envelope() { OperationsHandler::<MainnetEthSpec, SignedExecutionPayloadEnvelope<_>>::default().run(); } +#[test] +fn operations_execution_payload_bid() { + OperationsHandler::<MinimalEthSpec, ExecutionPayloadBidBlock<_>>::default().run(); + OperationsHandler::<MainnetEthSpec, ExecutionPayloadBidBlock<_>>::default().run(); +} + #[test] fn operations_withdrawals() { OperationsHandler::<MinimalEthSpec, WithdrawalsPayload<_>>::default().run(); From 96bc5617d0210e502b1103ecd12ca97b0c4e64fa Mon Sep 17 00:00:00 2001 From: Sergey Yakovlev <selfuryon@pm.me> Date: Thu, 12 Feb 2026 21:33:00 +0200 Subject: [PATCH 11/81] fix: auto-populate ENR UDP port from discovery listen port (#8804) Co-Authored-By: Sergey Yakovlev <selfuryon@pm.me> --- .../lighthouse_network/src/discovery/enr.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/beacon_node/lighthouse_network/src/discovery/enr.rs b/beacon_node/lighthouse_network/src/discovery/enr.rs index 4c285ea86c..01a01d55ab 100644 --- a/beacon_node/lighthouse_network/src/discovery/enr.rs +++ b/beacon_node/lighthouse_network/src/discovery/enr.rs @@ -200,11 +200,23 @@ pub fn build_enr<E: EthSpec>( builder.ip6(*ip); } - if let Some(udp4_port) = config.enr_udp4_port { + // If the ENR port is not set, and we are listening over that ip version, use the listening + // discovery port instead. + if let Some(udp4_port) = config.enr_udp4_port.or_else(|| { + config + .listen_addrs() + .v4() + .and_then(|v4_addr| v4_addr.disc_port.try_into().ok()) + }) { builder.udp4(udp4_port.get()); } - if let Some(udp6_port) = config.enr_udp6_port { + if let Some(udp6_port) = config.enr_udp6_port.or_else(|| { + config + .listen_addrs() + .v6() + .and_then(|v6_addr| v6_addr.disc_port.try_into().ok()) + }) { builder.udp6(udp6_port.get()); } From 036ba1f221fd1f7a346a0c10a971e9f7582099f3 Mon Sep 17 00:00:00 2001 From: Mac L <mjladson@pm.me> Date: Fri, 13 Feb 2026 00:51:26 +0400 Subject: [PATCH 12/81] Add `network` feature to `eth2` (#8558) This reverts some of the changes from #8524 by adding back the typed network endpoints with an optional `network` feature. Without the `network` feature, these endpoints (and associated dependencies) will not be built. This means the `enr`, `multiaddr` and `libp2p-identity` dependencies have returned but are now optional Co-Authored-By: Mac L <mjladson@pm.me> --- Cargo.lock | 3 +++ beacon_node/beacon_chain/Cargo.toml | 2 +- beacon_node/execution_layer/Cargo.toml | 2 +- beacon_node/http_api/Cargo.toml | 2 +- beacon_node/http_api/src/lib.rs | 9 +++------ beacon_node/http_api/tests/tests.rs | 18 ++++-------------- common/eth2/Cargo.toml | 4 ++++ common/eth2/src/lib.rs | 8 ++++++-- common/eth2/src/types.rs | 11 ++++++++--- 9 files changed, 31 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 69204ccaec..7683e67624 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3120,13 +3120,16 @@ dependencies = [ "context_deserialize", "educe", "eip_3076", + "enr", "eth2_keystore", "ethereum_serde_utils", "ethereum_ssz", "ethereum_ssz_derive", "futures", "futures-util", + "libp2p-identity", "mediatype", + "multiaddr", "pretty_reqwest_error", "proto_array", "rand 0.9.2", diff --git a/beacon_node/beacon_chain/Cargo.toml b/beacon_node/beacon_chain/Cargo.toml index 5e1c41b830..eec8836ff4 100644 --- a/beacon_node/beacon_chain/Cargo.toml +++ b/beacon_node/beacon_chain/Cargo.toml @@ -19,7 +19,7 @@ alloy-primitives = { workspace = true } bitvec = { workspace = true } bls = { workspace = true } educe = { workspace = true } -eth2 = { workspace = true, features = ["lighthouse"] } +eth2 = { workspace = true, features = ["lighthouse", "network"] } eth2_network_config = { workspace = true } ethereum_hashing = { workspace = true } ethereum_serde_utils = { workspace = true } diff --git a/beacon_node/execution_layer/Cargo.toml b/beacon_node/execution_layer/Cargo.toml index c443e94574..a23ea948e4 100644 --- a/beacon_node/execution_layer/Cargo.toml +++ b/beacon_node/execution_layer/Cargo.toml @@ -13,7 +13,7 @@ arc-swap = "1.6.0" bls = { workspace = true } builder_client = { path = "../builder_client" } bytes = { workspace = true } -eth2 = { workspace = true, features = ["events", "lighthouse"] } +eth2 = { workspace = true, features = ["events", "lighthouse", "network"] } ethereum_serde_utils = { workspace = true } ethereum_ssz = { workspace = true } fixed_bytes = { workspace = true } diff --git a/beacon_node/http_api/Cargo.toml b/beacon_node/http_api/Cargo.toml index 6211ac6726..78e7af71f4 100644 --- a/beacon_node/http_api/Cargo.toml +++ b/beacon_node/http_api/Cargo.toml @@ -14,7 +14,7 @@ bytes = { workspace = true } context_deserialize = { workspace = true } directory = { workspace = true } either = { workspace = true } -eth2 = { workspace = true, features = ["lighthouse"] } +eth2 = { workspace = true, features = ["lighthouse", "network"] } ethereum_serde_utils = { workspace = true } ethereum_ssz = { workspace = true } execution_layer = { workspace = true } diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 095c52fb29..c4b2cded51 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -2140,12 +2140,9 @@ pub fn serve<T: BeaconChainTypes>( let discovery_addresses = enr.multiaddr_p2p_udp(); Ok(api_types::GenericResponse::from(api_types::IdentityData { peer_id: network_globals.local_peer_id().to_base58(), - enr: enr.to_base64(), - p2p_addresses: p2p_addresses.iter().map(|a| a.to_string()).collect(), - discovery_addresses: discovery_addresses - .iter() - .map(|a| a.to_string()) - .collect(), + enr, + p2p_addresses, + discovery_addresses, metadata: utils::from_meta_data::<T::EthSpec>( &network_globals.local_metadata, &chain.spec, diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index a49362d815..367a0e3f05 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -2855,19 +2855,9 @@ impl ApiTester { let expected = IdentityData { peer_id: self.local_enr.peer_id().to_string(), - enr: self.local_enr.to_base64(), - p2p_addresses: self - .local_enr - .multiaddr_p2p_tcp() - .iter() - .map(|a| a.to_string()) - .collect(), - discovery_addresses: self - .local_enr - .multiaddr_p2p_udp() - .iter() - .map(|a| a.to_string()) - .collect(), + enr: self.local_enr.clone(), + p2p_addresses: self.local_enr.multiaddr_p2p_tcp(), + discovery_addresses: self.local_enr.multiaddr_p2p_udp(), metadata: MetaData::V2(MetaDataV2 { seq_number: 0, attnets: "0x0000000000000000".to_string(), @@ -2896,7 +2886,7 @@ impl ApiTester { pub async fn test_get_node_peers_by_id(self) -> Self { let result = self .client - .get_node_peers_by_id(&self.external_peer_id.to_string()) + .get_node_peers_by_id(self.external_peer_id) .await .unwrap() .data; diff --git a/common/eth2/Cargo.toml b/common/eth2/Cargo.toml index da8aba5ded..974508492a 100644 --- a/common/eth2/Cargo.toml +++ b/common/eth2/Cargo.toml @@ -8,19 +8,23 @@ edition = { workspace = true } default = [] lighthouse = ["proto_array", "eth2_keystore", "eip_3076", "zeroize"] events = ["reqwest-eventsource", "futures", "futures-util"] +network = ["libp2p-identity", "enr", "multiaddr"] [dependencies] bls = { workspace = true } context_deserialize = { workspace = true } educe = { workspace = true } eip_3076 = { workspace = true, optional = true } +enr = { version = "0.13.0", features = ["ed25519"], optional = true } eth2_keystore = { workspace = true, optional = true } ethereum_serde_utils = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } futures = { workspace = true, optional = true } futures-util = { version = "0.3.8", optional = true } +libp2p-identity = { version = "0.2", features = ["peerid"], optional = true } mediatype = "0.19.13" +multiaddr = { version = "0.18.2", optional = true } pretty_reqwest_error = { workspace = true } proto_array = { workspace = true, optional = true } reqwest = { workspace = true } diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 10382b028a..95744a4137 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -35,6 +35,8 @@ use educe::Educe; use futures::Stream; #[cfg(feature = "events")] use futures_util::StreamExt; +#[cfg(feature = "network")] +use libp2p_identity::PeerId; use reqwest::{ Body, IntoUrl, RequestBuilder, Response, header::{HeaderMap, HeaderValue}, @@ -1939,6 +1941,7 @@ impl BeaconNodeHttpClient { } /// `GET node/identity` + #[cfg(feature = "network")] pub async fn get_node_identity(&self) -> Result<GenericResponse<IdentityData>, Error> { let mut path = self.eth_path(V1)?; @@ -1986,9 +1989,10 @@ impl BeaconNodeHttpClient { } /// `GET node/peers/{peer_id}` + #[cfg(feature = "network")] pub async fn get_node_peers_by_id( &self, - peer_id: &str, + peer_id: PeerId, ) -> Result<GenericResponse<PeerData>, Error> { let mut path = self.eth_path(V1)?; @@ -1996,7 +2000,7 @@ impl BeaconNodeHttpClient { .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("node") .push("peers") - .push(peer_id); + .push(&peer_id.to_string()); self.get(path).await } diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index 4acfe3a640..8b33a4dfb9 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -9,7 +9,11 @@ use crate::{ }; use bls::{PublicKeyBytes, SecretKey, Signature, SignatureBytes}; use context_deserialize::ContextDeserialize; +#[cfg(feature = "network")] +use enr::{CombinedKey, Enr}; use mediatype::{MediaType, MediaTypeList, names}; +#[cfg(feature = "network")] +use multiaddr::Multiaddr; use reqwest::header::HeaderMap; use serde::{Deserialize, Deserializer, Serialize}; use serde_utils::quoted_u64::Quoted; @@ -559,12 +563,13 @@ pub struct ChainHeadData { pub execution_optimistic: Option<bool>, } +#[cfg(feature = "network")] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct IdentityData { pub peer_id: String, - pub enr: String, - pub p2p_addresses: Vec<String>, - pub discovery_addresses: Vec<String>, + pub enr: Enr<CombinedKey>, + pub p2p_addresses: Vec<Multiaddr>, + pub discovery_addresses: Vec<Multiaddr>, pub metadata: MetaData, } From c59e4a0cee78d311ffe17d8045cbd82032b501c9 Mon Sep 17 00:00:00 2001 From: Mac L <mjladson@pm.me> Date: Fri, 13 Feb 2026 00:51:39 +0400 Subject: [PATCH 13/81] Disable `legacy-arith` by default in `consensus/types` (#8695) Currently, `consensus/types` cannot build with `no-default-features` since we use "legacy" standard arithmetic operations. - Remove the offending arithmetic to fix compilation. - Rename `legacy-arith` to `saturating-arith` and disable it by default. Co-Authored-By: Mac L <mjladson@pm.me> --- Cargo.toml | 2 +- beacon_node/beacon_chain/src/beacon_chain.rs | 2 +- .../beacon_chain/src/early_attester_cache.rs | 8 +- consensus/state_processing/Cargo.toml | 3 +- consensus/types/Cargo.toml | 7 +- consensus/types/src/core/slot_epoch.rs | 2 +- consensus/types/src/core/slot_epoch_macros.rs | 6 +- consensus/types/src/state/beacon_state.rs | 7 +- consensus/types/src/state/committee_cache.rs | 125 ++++++++++-------- consensus/types/src/state/mod.rs | 2 +- consensus/types/tests/committee_cache.rs | 4 +- 11 files changed, 91 insertions(+), 77 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 100a916c50..98e8c057b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -271,7 +271,7 @@ tracing_samplers = { path = "common/tracing_samplers" } tree_hash = "0.12.0" tree_hash_derive = "0.12.0" typenum = "1" -types = { path = "consensus/types" } +types = { path = "consensus/types", features = ["saturating-arith"] } url = "2" uuid = { version = "0.8", features = ["serde", "v4"] } validator_client = { path = "validator_client" } diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index ec79153785..4ae7871758 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -1665,7 +1665,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> { let validator_index = *validator_index as usize; committee_cache.get_attestation_duties(validator_index) }) - .collect(); + .collect::<Result<Vec<_>, _>>()?; Ok((duties, dependent_root)) }, diff --git a/beacon_node/beacon_chain/src/early_attester_cache.rs b/beacon_node/beacon_chain/src/early_attester_cache.rs index 8d9eb950f3..752e4d1a96 100644 --- a/beacon_node/beacon_chain/src/early_attester_cache.rs +++ b/beacon_node/beacon_chain/src/early_attester_cache.rs @@ -2,6 +2,7 @@ use crate::data_availability_checker::{AvailableBlock, AvailableBlockData}; use crate::{BeaconChainError as Error, metrics}; use parking_lot::RwLock; use proto_array::Block as ProtoBlock; +use safe_arith::SafeArith; use std::sync::Arc; use tracing::instrument; use types::*; @@ -59,12 +60,13 @@ impl CommitteeLengths { slots_per_epoch, committees_per_slot, committee_index as usize, - ); + )?; + let epoch_committee_count = committees_per_slot.safe_mul(slots_per_epoch)?; let range = compute_committee_range_in_epoch( - epoch_committee_count(committees_per_slot, slots_per_epoch), + epoch_committee_count, index_in_epoch, self.active_validator_indices_len, - ) + )? .ok_or(Error::EarlyAttesterCacheError)?; range diff --git a/consensus/state_processing/Cargo.toml b/consensus/state_processing/Cargo.toml index a08035d583..a83e443e80 100644 --- a/consensus/state_processing/Cargo.toml +++ b/consensus/state_processing/Cargo.toml @@ -5,9 +5,8 @@ authors = ["Paul Hauner <paul@paulhauner.com>", "Michael Sproul <michael@sigmapr edition = { workspace = true } [features] -default = ["legacy-arith"] +default = [] fake_crypto = ["bls/fake_crypto"] -legacy-arith = ["types/legacy-arith"] arbitrary-fuzz = [ "types/arbitrary-fuzz", "merkle_proof/arbitrary", diff --git a/consensus/types/Cargo.toml b/consensus/types/Cargo.toml index feea855c84..a4b879ddb2 100644 --- a/consensus/types/Cargo.toml +++ b/consensus/types/Cargo.toml @@ -8,9 +8,10 @@ authors = [ edition = { workspace = true } [features] -default = ["legacy-arith"] -# Allow saturating arithmetic on slots and epochs. Enabled by default, but deprecated. -legacy-arith = [] +default = [] +# Enable +, -, *, /, % operators for Slot and Epoch types. +# Operations saturate instead of wrapping. +saturating-arith = [] sqlite = ["dep:rusqlite"] arbitrary = [ "dep:arbitrary", diff --git a/consensus/types/src/core/slot_epoch.rs b/consensus/types/src/core/slot_epoch.rs index 97457701b1..837391546c 100644 --- a/consensus/types/src/core/slot_epoch.rs +++ b/consensus/types/src/core/slot_epoch.rs @@ -22,7 +22,7 @@ use crate::{ test_utils::TestRandom, }; -#[cfg(feature = "legacy-arith")] +#[cfg(feature = "saturating-arith")] use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Rem, Sub, SubAssign}; #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] diff --git a/consensus/types/src/core/slot_epoch_macros.rs b/consensus/types/src/core/slot_epoch_macros.rs index eee267355a..1b0c3bcfc1 100644 --- a/consensus/types/src/core/slot_epoch_macros.rs +++ b/consensus/types/src/core/slot_epoch_macros.rs @@ -117,7 +117,7 @@ macro_rules! impl_safe_arith { } // Deprecated: prefer `SafeArith` methods for new code. -#[cfg(feature = "legacy-arith")] +#[cfg(feature = "saturating-arith")] macro_rules! impl_math_between { ($main: ident, $other: ident) => { impl Add<$other> for $main { @@ -321,9 +321,9 @@ macro_rules! impl_common { impl_u64_eq_ord!($type); impl_safe_arith!($type, $type); impl_safe_arith!($type, u64); - #[cfg(feature = "legacy-arith")] + #[cfg(feature = "saturating-arith")] impl_math_between!($type, $type); - #[cfg(feature = "legacy-arith")] + #[cfg(feature = "saturating-arith")] impl_math_between!($type, u64); impl_math!($type); impl_display!($type); diff --git a/consensus/types/src/state/beacon_state.rs b/consensus/types/src/state/beacon_state.rs index 6838b588eb..6228e40ef8 100644 --- a/consensus/types/src/state/beacon_state.rs +++ b/consensus/types/src/state/beacon_state.rs @@ -876,7 +876,7 @@ impl<E: EthSpec> BeaconState<E> { relative_epoch: RelativeEpoch, ) -> Result<u64, BeaconStateError> { let cache = self.committee_cache(relative_epoch)?; - Ok(cache.epoch_committee_count() as u64) + Ok(cache.epoch_committee_count()? as u64) } /// Return the cached active validator indices at some epoch. @@ -2150,7 +2150,7 @@ impl<E: EthSpec> BeaconState<E> { ) -> Result<Option<AttestationDuty>, BeaconStateError> { let cache = self.committee_cache(relative_epoch)?; - Ok(cache.get_attestation_duties(validator_index)) + Ok(cache.get_attestation_duties(validator_index)?) } /// Check if the attestation is for the block proposed at the attestation slot. @@ -2909,7 +2909,6 @@ impl<E: EthSpec> BeaconState<E> { } } - #[allow(clippy::arithmetic_side_effects)] pub fn rebase_on(&mut self, base: &Self, spec: &ChainSpec) -> Result<(), BeaconStateError> { // Required for macros (which use type-hints internally). @@ -3218,7 +3217,6 @@ impl<E: EthSpec> BeaconState<E> { )) } - #[allow(clippy::arithmetic_side_effects)] pub fn apply_pending_mutations(&mut self) -> Result<(), BeaconStateError> { match self { Self::Base(inner) => { @@ -3321,7 +3319,6 @@ impl<E: EthSpec> BeaconState<E> { pub fn get_beacon_state_leaves(&self) -> Vec<Hash256> { let mut leaves = vec![]; - #[allow(clippy::arithmetic_side_effects)] match self { BeaconState::Base(state) => { map_beacon_state_base_fields!(state, |_, field| { diff --git a/consensus/types/src/state/committee_cache.rs b/consensus/types/src/state/committee_cache.rs index 39e9011ef4..4a28f3c689 100644 --- a/consensus/types/src/state/committee_cache.rs +++ b/consensus/types/src/state/committee_cache.rs @@ -1,9 +1,7 @@ -#![allow(clippy::arithmetic_side_effects)] - use std::{num::NonZeroUsize, ops::Range, sync::Arc}; use educe::Educe; -use safe_arith::SafeArith; +use safe_arith::{ArithError, SafeArith}; use serde::{Deserialize, Serialize}; use ssz::{Decode, DecodeError, Encode, four_byte_option_impl}; use ssz_derive::{Decode, Encode}; @@ -79,7 +77,13 @@ impl CommitteeCache { .saturating_sub(spec.min_seed_lookahead) .saturating_sub(1u64); - if reqd_randao_epoch < state.min_randao_epoch() || epoch > state.current_epoch() + 1 { + if reqd_randao_epoch < state.min_randao_epoch() + || epoch + > state + .current_epoch() + .safe_add(1) + .map_err(BeaconStateError::ArithError)? + { return Err(BeaconStateError::EpochOutOfBounds); } @@ -118,7 +122,7 @@ impl CommitteeCache { *shuffling_positions .get_mut(v) .ok_or(BeaconStateError::ShuffleIndexOutOfBounds(v))? = - NonZeroUsize::new(i + 1).into(); + NonZeroUsize::new(i.safe_add(1).map_err(BeaconStateError::ArithError)?).into(); } Ok(Arc::new(CommitteeCache { @@ -177,8 +181,9 @@ impl CommitteeCache { self.slots_per_epoch as usize, self.committees_per_slot as usize, index as usize, - ); - let committee = self.compute_committee(committee_index)?; + ) + .ok()?; + let committee = self.compute_committee(committee_index).ok()??; Some(BeaconCommittee { slot, @@ -212,8 +217,9 @@ impl CommitteeCache { .initialized_epoch .ok_or(BeaconStateError::CommitteeCacheUninitialized(None))?; + let capacity = self.epoch_committee_count()?; initialized_epoch.slot_iter(self.slots_per_epoch).try_fold( - Vec::with_capacity(self.epoch_committee_count()), + Vec::with_capacity(capacity), |mut vec, slot| { vec.append(&mut self.get_beacon_committees_at_slot(slot)?); Ok(vec) @@ -225,43 +231,53 @@ impl CommitteeCache { /// /// Returns `None` if the `validator_index` does not exist, does not have duties or `Self` is /// non-initialized. - pub fn get_attestation_duties(&self, validator_index: usize) -> Option<AttestationDuty> { - let i = self.shuffled_position(validator_index)?; + pub fn get_attestation_duties( + &self, + validator_index: usize, + ) -> Result<Option<AttestationDuty>, ArithError> { + let Some(i) = self.shuffled_position(validator_index) else { + return Ok(None); + }; - (0..self.epoch_committee_count()) - .map(|nth_committee| (nth_committee, self.compute_committee_range(nth_committee))) - .find(|(_, range)| { - if let Some(range) = range { - range.start <= i && range.end > i - } else { - false - } - }) - .and_then(|(nth_committee, range)| { - let (slot, index) = self.convert_to_slot_and_index(nth_committee as u64)?; - let range = range?; - let committee_position = i - range.start; - let committee_len = range.end - range.start; + for nth_committee in 0..self.epoch_committee_count()? { + let Some(range) = self.compute_committee_range(nth_committee)? else { + continue; + }; - Some(AttestationDuty { + if range.start <= i && range.end > i { + let Some((slot, index)) = self.convert_to_slot_and_index(nth_committee as u64)? + else { + return Ok(None); + }; + + let committee_position = i.safe_sub(range.start)?; + let committee_len = range.end.safe_sub(range.start)?; + + return Ok(Some(AttestationDuty { slot, index, committee_position, committee_len, committees_at_slot: self.committees_per_slot(), - }) - }) + })); + } + } + + Ok(None) } /// Convert an index addressing the list of all epoch committees into a slot and per-slot index. fn convert_to_slot_and_index( &self, global_committee_index: u64, - ) -> Option<(Slot, CommitteeIndex)> { - let epoch_start_slot = self.initialized_epoch?.start_slot(self.slots_per_epoch); - let slot_offset = global_committee_index / self.committees_per_slot; - let index = global_committee_index % self.committees_per_slot; - Some((epoch_start_slot.safe_add(slot_offset).ok()?, index)) + ) -> Result<Option<(Slot, CommitteeIndex)>, ArithError> { + let Some(epoch) = self.initialized_epoch else { + return Ok(None); + }; + let epoch_start_slot = epoch.start_slot(self.slots_per_epoch); + let slot_offset = global_committee_index.safe_div(self.committees_per_slot)?; + let index = global_committee_index.safe_rem(self.committees_per_slot)?; + Ok(Some((epoch_start_slot.safe_add(slot_offset)?, index))) } /// Returns the number of active validators in the initialized epoch. @@ -278,11 +294,8 @@ impl CommitteeCache { /// Always returns `usize::default()` for a non-initialized epoch. /// /// Spec v0.12.1 - pub fn epoch_committee_count(&self) -> usize { - epoch_committee_count( - self.committees_per_slot as usize, - self.slots_per_epoch as usize, - ) + pub fn epoch_committee_count(&self) -> Result<usize, ArithError> { + (self.committees_per_slot as usize).safe_mul(self.slots_per_epoch as usize) } /// Returns the number of committees per slot for this cache's epoch. @@ -293,19 +306,23 @@ impl CommitteeCache { /// Returns a slice of `self.shuffling` that represents the `index`'th committee in the epoch. /// /// Spec v0.12.1 - fn compute_committee(&self, index: usize) -> Option<&[usize]> { - self.shuffling.get(self.compute_committee_range(index)?) + fn compute_committee(&self, index: usize) -> Result<Option<&[usize]>, ArithError> { + if let Some(range) = self.compute_committee_range(index)? { + Ok(self.shuffling.get(range)) + } else { + Ok(None) + } } /// Returns a range of `self.shuffling` that represents the `index`'th committee in the epoch. /// - /// To avoid a divide-by-zero, returns `None` if `self.committee_count` is zero. + /// To avoid a divide-by-zero, returns `Ok(None)` if `self.committee_count` is zero. /// - /// Will also return `None` if the index is out of bounds. + /// Will also return `Ok(None)` if the index is out of bounds. /// /// Spec v0.12.1 - fn compute_committee_range(&self, index: usize) -> Option<Range<usize>> { - compute_committee_range_in_epoch(self.epoch_committee_count(), index, self.shuffling.len()) + fn compute_committee_range(&self, index: usize) -> Result<Option<Range<usize>>, ArithError> { + compute_committee_range_in_epoch(self.epoch_committee_count()?, index, self.shuffling.len()) } /// Returns the index of some validator in `self.shuffling`. @@ -329,8 +346,10 @@ pub fn compute_committee_index_in_epoch( slots_per_epoch: usize, committees_per_slot: usize, committee_index: usize, -) -> usize { - (slot.as_usize() % slots_per_epoch) * committees_per_slot + committee_index +) -> Result<usize, ArithError> { + (slot.as_usize().safe_rem(slots_per_epoch)?) + .safe_mul(committees_per_slot)? + .safe_add(committee_index) } /// Computes the range for slicing the shuffled indices to determine the members of a committee. @@ -341,20 +360,16 @@ pub fn compute_committee_range_in_epoch( epoch_committee_count: usize, index_in_epoch: usize, shuffling_len: usize, -) -> Option<Range<usize>> { +) -> Result<Option<Range<usize>>, ArithError> { if epoch_committee_count == 0 || index_in_epoch >= epoch_committee_count { - return None; + return Ok(None); } - let start = (shuffling_len * index_in_epoch) / epoch_committee_count; - let end = (shuffling_len * (index_in_epoch + 1)) / epoch_committee_count; + let start = (shuffling_len.safe_mul(index_in_epoch))?.safe_div(epoch_committee_count)?; + let end = + (shuffling_len.safe_mul(index_in_epoch.safe_add(1)?))?.safe_div(epoch_committee_count)?; - Some(start..end) -} - -/// Returns the total number of committees in an epoch. -pub fn epoch_committee_count(committees_per_slot: usize, slots_per_epoch: usize) -> usize { - committees_per_slot * slots_per_epoch + Ok(Some(start..end)) } /// Returns a list of all `validators` indices where the validator is active at the given diff --git a/consensus/types/src/state/mod.rs b/consensus/types/src/state/mod.rs index ea064fb7ac..096bb67167 100644 --- a/consensus/types/src/state/mod.rs +++ b/consensus/types/src/state/mod.rs @@ -21,7 +21,7 @@ pub use beacon_state::{ }; pub use committee_cache::{ CommitteeCache, compute_committee_index_in_epoch, compute_committee_range_in_epoch, - epoch_committee_count, get_active_validator_indices, + get_active_validator_indices, }; pub use epoch_cache::{EpochCache, EpochCacheError, EpochCacheKey}; pub use exit_cache::ExitCache; diff --git a/consensus/types/tests/committee_cache.rs b/consensus/types/tests/committee_cache.rs index 751ef05d29..0bb8aa1da2 100644 --- a/consensus/types/tests/committee_cache.rs +++ b/consensus/types/tests/committee_cache.rs @@ -33,9 +33,9 @@ fn default_values() { assert!(!cache.is_initialized_at(Epoch::new(0))); assert!(&cache.active_validator_indices().is_empty()); assert_eq!(cache.get_beacon_committee(Slot::new(0), 0), None); - assert_eq!(cache.get_attestation_duties(0), None); + assert_eq!(cache.get_attestation_duties(0), Ok(None)); assert_eq!(cache.active_validator_count(), 0); - assert_eq!(cache.epoch_committee_count(), 0); + assert_eq!(cache.epoch_committee_count(), Ok(0)); assert!(cache.get_beacon_committees_at_slot(Slot::new(0)).is_err()); } From f4a6b8d9b97bfcbf901423c109901561d3e7e928 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Thu, 12 Feb 2026 21:24:51 -0700 Subject: [PATCH 14/81] Tree-sync friendly lookup sync tests (#8592) - Step 0 of the tree-sync roadmap https://github.com/sigp/lighthouse/issues/7678 Current lookup sync tests are written in an explicit way that assume how the internals of lookup sync work. For example the test would do: - Emit unknown block parent message - Expect block request for X - Respond with successful block request - Expect block processing request for X - Response with successful processing request - etc.. This is unnecessarily verbose. And it will requires a complete re-write when something changes in the internals of lookup sync (has happened a few times, mostly for deneb and fulu). What we really want to assert is: - WHEN: we receive an unknown block parent message - THEN: Lookup sync can sync that block - ASSERT: Without penalizing peers, without unnecessary retries Keep all existing tests and add new cases but written in the new style described above. The logic to serve and respond to request is in this function `fn simulate` https://github.com/dapplion/lighthouse/blob/2288a3aeb11164bb1960dc803f41696c984c69ff/beacon_node/network/src/sync/tests/lookups.rs#L301 - It controls peer behavior based on a `CompleteStrategy` where you can set for example "respond to BlocksByRoot requests with empty" - It actually runs beacon processor messages running their clousures. Now sync tests actually import blocks, increasing the test coverage to the interaction of sync and the da_checker. - To achieve the above the tests create real blocks with the test harness. To make the tests as fast as before, I disabled crypto with `TestConfig` Along the way I found a couple bugs, which I documented on the diff. Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com> --- Cargo.lock | 1 + Makefile | 13 +- .../src/block_verification_types.rs | 15 - .../overflow_lru_cache.rs | 2 + beacon_node/beacon_chain/src/test_utils.rs | 6 +- beacon_node/beacon_processor/src/lib.rs | 15 +- .../test_utils/execution_block_generator.rs | 16 + .../src/test_utils/handle_rpc.rs | 9 + .../lighthouse_network/src/rpc/protocol.rs | 2 +- .../src/service/api_types.rs | 2 +- beacon_node/network/Cargo.toml | 2 + .../src/network_beacon_processor/mod.rs | 5 +- .../network_beacon_processor/sync_methods.rs | 2 +- .../src/network_beacon_processor/tests.rs | 20 +- .../network/src/sync/block_lookups/mod.rs | 75 +- .../sync/block_lookups/single_block_lookup.rs | 6 + beacon_node/network/src/sync/manager.rs | 33 +- .../network/src/sync/network_context.rs | 7 +- .../src/sync/network_context/custody.rs | 20 +- .../src/sync/range_sync/chain_collection.rs | 25 + .../network/src/sync/range_sync/range.rs | 5 + beacon_node/network/src/sync/tests/lookups.rs | 4258 ++++++++--------- beacon_node/network/src/sync/tests/mod.rs | 73 +- beacon_node/network/src/sync/tests/range.rs | 26 +- crypto/bls/src/impls/fake_crypto.rs | 25 +- crypto/kzg/Cargo.toml | 4 + crypto/kzg/src/lib.rs | 12 + 27 files changed, 2298 insertions(+), 2381 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7683e67624..5a63ab1e72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6081,6 +6081,7 @@ dependencies = [ "metrics", "operation_pool", "parking_lot", + "paste", "rand 0.8.5", "rand 0.9.2", "rand_chacha 0.3.1", diff --git a/Makefile b/Makefile index 9e2b1d24c5..0995a869f4 100644 --- a/Makefile +++ b/Makefile @@ -36,8 +36,12 @@ PROFILE ?= release RECENT_FORKS_BEFORE_GLOAS=electra fulu # List of all recent hard forks. This list is used to set env variables for http_api tests +# Include phase0 to test the code paths in sync that are pre blobs RECENT_FORKS=electra fulu gloas +# For network tests include phase0 to cover genesis syncing (blocks without blobs or columns) +TEST_NETWORK_FORKS=phase0 $(RECENT_FORKS_BEFORE_GLOAS) + # Extra flags for Cargo CARGO_INSTALL_EXTRA_FLAGS?= @@ -226,12 +230,15 @@ test-op-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-%,$(RECENT_FORKS_BEFORE_GLOAS)) +test-network: $(patsubst %,test-network-%,$(TEST_NETWORK_FORKS)) test-network-%: - env FORK_NAME=$* cargo nextest run --release \ - --features "fork_from_env,$(TEST_FEATURES)" \ + env FORK_NAME=$* cargo nextest run --no-fail-fast --release \ + --features "fork_from_env,fake_crypto,$(TEST_FEATURES)" \ -p network + env FORK_NAME=$* cargo nextest run --no-fail-fast --release \ + --features "fork_from_env,$(TEST_FEATURES)" \ + -p network crypto_on # Run the tests in the `slasher` crate for all supported database backends. test-slasher: diff --git a/beacon_node/beacon_chain/src/block_verification_types.rs b/beacon_node/beacon_chain/src/block_verification_types.rs index 6a028e6c98..f98cd40d08 100644 --- a/beacon_node/beacon_chain/src/block_verification_types.rs +++ b/beacon_node/beacon_chain/src/block_verification_types.rs @@ -287,21 +287,6 @@ pub struct BlockImportData<E: EthSpec> { pub consensus_context: ConsensusContext<E>, } -impl<E: EthSpec> BlockImportData<E> { - pub fn __new_for_test( - block_root: Hash256, - state: BeaconState<E>, - parent_block: SignedBeaconBlock<E, BlindedPayload<E>>, - ) -> Self { - Self { - block_root, - state, - parent_block, - consensus_context: ConsensusContext::new(Slot::new(0)), - } - } -} - /// Trait for common block operations. pub trait AsBlock<E: EthSpec> { fn slot(&self) -> Slot; 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 f7bd646f82..7260a4aca0 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 @@ -698,6 +698,8 @@ impl<T: BeaconChainTypes> DataAvailabilityCheckerInner<T> { pub fn remove_pre_execution_block(&self, block_root: &Hash256) { // The read lock is immediately dropped so we can safely remove the block from the cache. if let Some(BlockProcessStatus::NotValidated(_, _)) = self.get_cached_block(block_root) { + // If the block is execution invalid, this status is permanent and idempotent to this + // block_root. We drop its components (e.g. columns) because they will never be useful. self.critical.write().pop(block_root); } } diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index f816dbac53..096a0516fc 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -818,7 +818,11 @@ where } pub fn get_full_block(&self, block_root: &Hash256) -> RpcBlock<E> { - let block = self.chain.get_blinded_block(block_root).unwrap().unwrap(); + let block = self + .chain + .get_blinded_block(block_root) + .unwrap() + .unwrap_or_else(|| panic!("block root does not exist in harness {block_root:?}")); let full_block = self.chain.store.make_full_block(block_root, block).unwrap(); self.build_rpc_block_from_store_blobs(Some(*block_root), Arc::new(full_block)) } diff --git a/beacon_node/beacon_processor/src/lib.rs b/beacon_node/beacon_processor/src/lib.rs index d3e9133542..33a00bfa49 100644 --- a/beacon_node/beacon_processor/src/lib.rs +++ b/beacon_node/beacon_processor/src/lib.rs @@ -243,12 +243,15 @@ impl<E: EthSpec> From<ReadyWork> for WorkEvent<E> { }, }, ReadyWork::RpcBlock(QueuedRpcBlock { - beacon_block_root: _, + beacon_block_root, process_fn, ignore_fn: _, }) => Self { drop_during_sync: false, - work: Work::RpcBlock { process_fn }, + work: Work::RpcBlock { + process_fn, + beacon_block_root, + }, }, ReadyWork::IgnoredRpcBlock(IgnoredRpcBlock { process_fn }) => Self { drop_during_sync: false, @@ -389,6 +392,7 @@ pub enum Work<E: EthSpec> { GossipLightClientFinalityUpdate(BlockingFn), GossipLightClientOptimisticUpdate(BlockingFn), RpcBlock { + beacon_block_root: Hash256, process_fn: AsyncFn, }, RpcBlobs { @@ -479,7 +483,7 @@ pub enum WorkType { } impl<E: EthSpec> Work<E> { - fn str_id(&self) -> &'static str { + pub fn str_id(&self) -> &'static str { self.to_type().into() } @@ -1432,7 +1436,10 @@ impl<E: EthSpec> BeaconProcessor<E> { beacon_block_root: _, process_fn, } => task_spawner.spawn_async(process_fn), - Work::RpcBlock { process_fn } + Work::RpcBlock { + process_fn, + beacon_block_root: _, + } | Work::RpcBlobs { process_fn } | Work::RpcCustodyColumn(process_fn) | Work::ColumnReconstruction(process_fn) => task_spawner.spawn_async(process_fn), diff --git a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs index 6b247a4cd4..8591359f15 100644 --- a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs +++ b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs @@ -18,6 +18,7 @@ use ssz_types::VariableList; use std::cmp::max; use std::collections::HashMap; use std::sync::Arc; +use tracing::warn; use tree_hash::TreeHash; use tree_hash_derive::TreeHash; use types::{ @@ -537,6 +538,21 @@ impl<E: EthSpec> ExecutionBlockGenerator<E> { .contains_key(&forkchoice_state.finalized_block_hash); if unknown_head_block_hash || unknown_safe_block_hash || unknown_finalized_block_hash { + if unknown_head_block_hash { + warn!(?head_block_hash, "Received unknown head block hash"); + } + if unknown_safe_block_hash { + warn!( + safe_block_hash = ?forkchoice_state.safe_block_hash, + "Received unknown safe block hash" + ); + } + if unknown_finalized_block_hash { + warn!( + finalized_block_hash = ?forkchoice_state.finalized_block_hash, + "Received unknown finalized block hash" + ) + } return Ok(JsonForkchoiceUpdatedV1Response { payload_status: JsonPayloadStatusV1 { status: JsonPayloadStatusV1Status::Syncing, diff --git a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs index 2168ed8961..53eb3b5166 100644 --- a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs +++ b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs @@ -5,6 +5,7 @@ use crate::test_utils::{DEFAULT_CLIENT_VERSION, DEFAULT_MOCK_EL_PAYLOAD_VALUE_WE use serde::{Deserialize, de::DeserializeOwned}; use serde_json::Value as JsonValue; use std::sync::Arc; +use tracing::debug; pub const GENERIC_ERROR_CODE: i64 = -1234; pub const BAD_PARAMS_ERROR_CODE: i64 = -32602; @@ -28,6 +29,8 @@ pub async fn handle_rpc<E: EthSpec>( .ok_or_else(|| "missing/invalid params field".to_string()) .map_err(|s| (s, GENERIC_ERROR_CODE))?; + debug!(method, "Mock execution engine"); + match method { ETH_SYNCING => ctx .syncing_response @@ -517,6 +520,12 @@ pub async fn handle_rpc<E: EthSpec>( _ => unreachable!(), }; + debug!( + ?payload_attributes, + ?forkchoice_state, + "ENGINE_FORKCHOICE_UPDATED" + ); + // validate method called correctly according to fork time if let Some(pa) = payload_attributes.as_ref() { match ctx diff --git a/beacon_node/lighthouse_network/src/rpc/protocol.rs b/beacon_node/lighthouse_network/src/rpc/protocol.rs index 34d8efccd1..b75ca72eda 100644 --- a/beacon_node/lighthouse_network/src/rpc/protocol.rs +++ b/beacon_node/lighthouse_network/src/rpc/protocol.rs @@ -731,7 +731,7 @@ where } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, IntoStaticStr)] pub enum RequestType<E: EthSpec> { Status(StatusMessage), Goodbye(GoodbyeReason), diff --git a/beacon_node/lighthouse_network/src/service/api_types.rs b/beacon_node/lighthouse_network/src/service/api_types.rs index f1a4d87de7..d0323bab52 100644 --- a/beacon_node/lighthouse_network/src/service/api_types.rs +++ b/beacon_node/lighthouse_network/src/service/api_types.rs @@ -135,7 +135,7 @@ pub struct CustodyId { pub struct CustodyRequester(pub SingleLookupReqId); /// Application level requests sent to the network. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum AppRequestId { Sync(SyncRequestId), Router, diff --git a/beacon_node/network/Cargo.toml b/beacon_node/network/Cargo.toml index 78dc0c48a7..68c77252ab 100644 --- a/beacon_node/network/Cargo.toml +++ b/beacon_node/network/Cargo.toml @@ -8,6 +8,7 @@ edition = { workspace = true } # NOTE: This can be run via cargo build --bin lighthouse --features network/disable-backfill disable-backfill = [] fork_from_env = ["beacon_chain/fork_from_env"] +fake_crypto = ["bls/fake_crypto", "kzg/fake_crypto"] portable = ["beacon_chain/portable"] test_logger = [] @@ -57,6 +58,7 @@ k256 = "0.13.4" kzg = { workspace = true } libp2p = { workspace = true } matches = "0.1.8" +paste = { workspace = true } rand_08 = { package = "rand", version = "0.8.5" } rand_chacha = "0.9.0" rand_chacha_03 = { package = "rand_chacha", version = "0.3.1" } diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index fd67fcde82..e1adf860de 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -526,7 +526,10 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> { ); self.try_send(BeaconWorkEvent { drop_during_sync: false, - work: Work::RpcBlock { process_fn }, + work: Work::RpcBlock { + process_fn, + beacon_block_root: block_root, + }, }) } diff --git a/beacon_node/network/src/network_beacon_processor/sync_methods.rs b/beacon_node/network/src/network_beacon_processor/sync_methods.rs index a6b3ea9e4b..629a42c688 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -219,7 +219,7 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> { // to be sent from the peers if we already have them. let publish_blobs = false; self.fetch_engine_blobs_and_publish(signed_beacon_block, block_root, publish_blobs) - .await + .await; } _ => {} } diff --git a/beacon_node/network/src/network_beacon_processor/tests.rs b/beacon_node/network/src/network_beacon_processor/tests.rs index 49b1c0c262..aa03ee931d 100644 --- a/beacon_node/network/src/network_beacon_processor/tests.rs +++ b/beacon_node/network/src/network_beacon_processor/tests.rs @@ -940,20 +940,20 @@ async fn data_column_reconstruction_at_deadline() { .set_current_time(slot_start + Duration::from_millis(reconstruction_deadline_millis)); let min_columns_for_reconstruction = E::number_of_columns() / 2; + + // Enqueue all columns first - at deadline, reconstruction races with gossip drain for i in 0..min_columns_for_reconstruction { rig.enqueue_gossip_data_columns(i); - rig.assert_event_journal_completes(&[WorkType::GossipDataColumnSidecar]) - .await; } - // Since we're at the reconstruction deadline, reconstruction should be triggered immediately - rig.assert_event_journal_with_timeout( - &[WorkType::ColumnReconstruction.into()], - Duration::from_millis(50), - false, - false, - ) - .await; + // Expect all gossip events + reconstruction + let mut expected_events: Vec<WorkType> = (0..min_columns_for_reconstruction) + .map(|_| WorkType::GossipDataColumnSidecar) + .collect(); + expected_events.push(WorkType::ColumnReconstruction); + + rig.assert_event_journal_contains_ordered(&expected_events) + .await; } // Test the column reconstruction is delayed for columns that arrive for a previous slot. diff --git a/beacon_node/network/src/sync/block_lookups/mod.rs b/beacon_node/network/src/sync/block_lookups/mod.rs index 9065f05753..cbf65505ef 100644 --- a/beacon_node/network/src/sync/block_lookups/mod.rs +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -121,15 +121,24 @@ pub struct BlockLookups<T: BeaconChainTypes> { // TODO: Why not index lookups by block_root? single_block_lookups: FnvHashMap<SingleLookupId, SingleBlockLookup<T>>, + + /// Used for testing assertions + metrics: BlockLookupsMetrics, } #[cfg(test)] use lighthouse_network::service::api_types::Id; #[cfg(test)] -/// Tuple of `SingleLookupId`, requested block root, awaiting parent block root (if any), -/// and list of peers that claim to have imported this set of block components. -pub(crate) type BlockLookupSummary = (Id, Hash256, Option<Hash256>, Vec<PeerId>); +#[derive(Debug)] +pub(crate) struct BlockLookupSummary { + /// Lookup ID + pub id: Id, + /// Requested block root + pub block_root: Hash256, + /// List of peers that claim to have imported this set of block components. + pub peers: Vec<PeerId>, +} impl<T: BeaconChainTypes> BlockLookups<T> { pub fn new() -> Self { @@ -138,9 +147,15 @@ impl<T: BeaconChainTypes> BlockLookups<T> { IGNORED_CHAINS_CACHE_EXPIRY_SECONDS, )), single_block_lookups: Default::default(), + metrics: <_>::default(), } } + #[cfg(test)] + pub(crate) fn metrics(&self) -> &BlockLookupsMetrics { + &self.metrics + } + #[cfg(test)] pub(crate) fn insert_ignored_chain(&mut self, block_root: Hash256) { self.ignored_chains.insert(block_root); @@ -155,7 +170,11 @@ impl<T: BeaconChainTypes> BlockLookups<T> { pub(crate) fn active_single_lookups(&self) -> Vec<BlockLookupSummary> { self.single_block_lookups .iter() - .map(|(id, l)| (*id, l.block_root(), l.awaiting_parent(), l.all_peers())) + .map(|(id, l)| BlockLookupSummary { + id: *id, + block_root: l.block_root(), + peers: l.all_peers(), + }) .collect() } @@ -306,7 +325,7 @@ impl<T: BeaconChainTypes> BlockLookups<T> { // attributability. A peer can send us garbage blocks over blocks_by_root, and // then correct blocks via blocks_by_range. - self.drop_lookup_and_children(*lookup_id); + self.drop_lookup_and_children(*lookup_id, "chain_too_long"); } else { // Should never happen error!( @@ -414,6 +433,7 @@ impl<T: BeaconChainTypes> BlockLookups<T> { "Created block lookup" ); metrics::inc_counter(&metrics::SYNC_LOOKUP_CREATED); + self.metrics.created_lookups += 1; let result = lookup.continue_requests(cx); if self.on_lookup_result(id, result, "new_current_lookup", cx) { @@ -513,8 +533,11 @@ impl<T: BeaconChainTypes> BlockLookups<T> { /* Error responses */ pub fn peer_disconnected(&mut self, peer_id: &PeerId) { - for (_, lookup) in self.single_block_lookups.iter_mut() { + for (id, lookup) in self.single_block_lookups.iter_mut() { lookup.remove_peer(peer_id); + if lookup.has_no_peers() { + debug!(%id, "Lookup has no peers"); + } } } @@ -566,7 +589,8 @@ impl<T: BeaconChainTypes> BlockLookups<T> { let action = match result { BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(_)) - | BlockProcessingResult::Err(BlockError::DuplicateFullyImported(..)) => { + | BlockProcessingResult::Err(BlockError::DuplicateFullyImported(..)) + | BlockProcessingResult::Err(BlockError::GenesisBlock) => { // Successfully imported request_state.on_processing_success()?; Action::Continue @@ -747,6 +771,15 @@ impl<T: BeaconChainTypes> BlockLookups<T> { 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; @@ -779,14 +812,17 @@ impl<T: BeaconChainTypes> BlockLookups<T> { /// Drops `dropped_id` lookup and all its children recursively. Lookups awaiting a parent need /// the parent to make progress to resolve, therefore we must drop them if the parent is /// dropped. - pub fn drop_lookup_and_children(&mut self, dropped_id: SingleLookupId) { + pub fn drop_lookup_and_children(&mut self, dropped_id: SingleLookupId, reason: &'static str) { if let Some(dropped_lookup) = self.single_block_lookups.remove(&dropped_id) { debug!( id = ?dropped_id, block_root = ?dropped_lookup.block_root(), awaiting_parent = ?dropped_lookup.awaiting_parent(), + reason, "Dropping lookup" ); + metrics::inc_counter_vec(&metrics::SYNC_LOOKUP_DROPPED, &[reason]); + self.metrics.dropped_lookups += 1; let child_lookups = self .single_block_lookups @@ -796,7 +832,7 @@ impl<T: BeaconChainTypes> BlockLookups<T> { .collect::<Vec<_>>(); for id in child_lookups { - self.drop_lookup_and_children(id); + self.drop_lookup_and_children(id, reason); } } } @@ -814,8 +850,13 @@ impl<T: BeaconChainTypes> BlockLookups<T> { Ok(LookupResult::Pending) => true, // no action Ok(LookupResult::Completed) => { if let Some(lookup) = self.single_block_lookups.remove(&id) { - debug!(block = ?lookup.block_root(), id, "Dropping completed lookup"); + debug!( + block = ?lookup.block_root(), + id, + "Dropping completed lookup" + ); 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.update_metrics(); @@ -829,8 +870,7 @@ impl<T: BeaconChainTypes> BlockLookups<T> { Err(LookupRequestError::UnknownLookup) => false, Err(error) => { debug!(id, source, ?error, "Dropping lookup on request error"); - metrics::inc_counter_vec(&metrics::SYNC_LOOKUP_DROPPED, &[error.into()]); - self.drop_lookup_and_children(id); + self.drop_lookup_and_children(id, error.into()); self.update_metrics(); false } @@ -897,7 +937,7 @@ impl<T: BeaconChainTypes> BlockLookups<T> { %block_root, "Dropping lookup with no peers" ); - self.drop_lookup_and_children(lookup_id); + self.drop_lookup_and_children(lookup_id, "no_peers"); } } @@ -946,7 +986,7 @@ impl<T: BeaconChainTypes> BlockLookups<T> { } metrics::inc_counter(&metrics::SYNC_LOOKUPS_STUCK); - self.drop_lookup_and_children(ancestor_stuck_lookup.id); + self.drop_lookup_and_children(ancestor_stuck_lookup.id, "lookup_stuck"); } } @@ -1022,3 +1062,10 @@ impl<T: BeaconChainTypes> BlockLookups<T> { } } } + +#[derive(Default, Clone, Debug)] +pub(crate) struct BlockLookupsMetrics { + pub created_lookups: usize, + pub dropped_lookups: usize, + pub completed_lookups: usize, +} 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 43bfe29a84..919526c238 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 @@ -109,6 +109,12 @@ impl<T: BeaconChainTypes> SingleBlockLookup<T> { } } + /// Reset the status of all internal requests + pub fn reset_requests(&mut self) { + self.block_request_state = BlockRequestState::new(self.block_root); + self.component_requests = ComponentRequests::WaitingForBlock; + } + /// Return the slot of this lookup's block if it's currently cached as `AwaitingProcessing` pub fn peek_downloaded_block_slot(&self) -> Option<Slot> { self.block_request_state diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 096ed9c328..c1ab6221dd 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -70,6 +70,7 @@ use slot_clock::SlotClock; use std::ops::Sub; use std::sync::Arc; use std::time::Duration; +use strum::IntoStaticStr; use tokio::sync::mpsc; use tracing::{debug, error, info, trace}; use types::{ @@ -90,7 +91,7 @@ pub const SLOT_IMPORT_TOLERANCE: usize = 32; /// arbitrary number that covers a full slot, but allows recovery if sync get stuck for a few slots. const NOTIFIED_UNKNOWN_ROOT_EXPIRY_SECONDS: u64 = 30; -#[derive(Debug)] +#[derive(Debug, IntoStaticStr)] /// A message that can be sent to the sync manager thread. pub enum SyncMessage<E: EthSpec> { /// A useful peer has been discovered. @@ -323,17 +324,18 @@ impl<T: BeaconChainTypes> SyncManager<T> { } #[cfg(test)] - pub(crate) fn active_single_lookups(&self) -> Vec<super::block_lookups::BlockLookupSummary> { - self.block_lookups.active_single_lookups() + pub(crate) fn send_sync_message(&mut self, sync_message: SyncMessage<<T>::EthSpec>) { + self.network.send_sync_message(sync_message); } #[cfg(test)] - pub(crate) fn active_parent_lookups(&self) -> Vec<Vec<Hash256>> { - self.block_lookups - .active_parent_lookups() - .iter() - .map(|c| c.chain.clone()) - .collect() + pub(crate) fn block_lookups(&self) -> &BlockLookups<T> { + &self.block_lookups + } + + #[cfg(test)] + pub(crate) fn range_sync(&self) -> &RangeSync<T> { + &self.range_sync } #[cfg(test)] @@ -512,17 +514,18 @@ impl<T: BeaconChainTypes> SyncManager<T> { /// there is no way to guarantee that libp2p always emits a error along with /// the disconnect. fn peer_disconnect(&mut self, peer_id: &PeerId) { - // Inject a Disconnected error on all requests associated with the disconnected peer - // to retry all batches/lookups - for sync_request_id in self.network.peer_disconnected(peer_id) { - self.inject_error(*peer_id, sync_request_id, RPCError::Disconnected); - } - // Remove peer from all data structures self.range_sync.peer_disconnect(&mut self.network, peer_id); let _ = self.backfill_sync.peer_disconnected(peer_id); self.block_lookups.peer_disconnected(peer_id); + // Inject a Disconnected error on all requests associated with the disconnected peer + // to retry all batches/lookups. Only after removing the peer from the data structures to + // avoid sending retry requests to the disconnecting peer. + for sync_request_id in self.network.peer_disconnected(peer_id) { + self.inject_error(*peer_id, sync_request_id, RPCError::Disconnected); + } + // Regardless of the outcome, we update the sync status. self.update_sync_state(); } diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index 542625b8a3..7e2c0d9a94 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -17,7 +17,7 @@ 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::RpcBlock; +use beacon_chain::block_verification_types::{AsBlock, RpcBlock}; use beacon_chain::{BeaconChain, BeaconChainTypes, BlockProcessStatus, EngineState}; use custody::CustodyRequestResult; use fnv::FnvHashMap; @@ -1095,13 +1095,14 @@ impl<T: BeaconChainTypes> SyncNetworkContext<T> { })?; // Include only the blob indexes not yet imported (received through gossip) - let custody_indexes_to_fetch = self + let mut custody_indexes_to_fetch = self .chain .sampling_columns_for_epoch(current_epoch) .iter() .copied() .filter(|index| !custody_indexes_imported.contains(index)) .collect::<Vec<_>>(); + custody_indexes_to_fetch.sort_unstable(); if custody_indexes_to_fetch.is_empty() { // No indexes required, do not issue any request @@ -1595,7 +1596,7 @@ impl<T: BeaconChainTypes> SyncNetworkContext<T> { ) .map_err(|_| SendErrorProcessor::SendError)?; - debug!(block = ?block_root, id, "Sending block for processing"); + debug!(block = ?block_root, block_slot = %block.slot(), id, "Sending block for processing"); // Lookup sync event safety: If `beacon_processor.send_rpc_beacon_block` returns Ok() sync // must receive a single `SyncMessage::BlockComponentProcessed` with this process type beacon_processor diff --git a/beacon_node/network/src/sync/network_context/custody.rs b/beacon_node/network/src/sync/network_context/custody.rs index 61ae95ee70..de5d9b6e0b 100644 --- a/beacon_node/network/src/sync/network_context/custody.rs +++ b/beacon_node/network/src/sync/network_context/custody.rs @@ -198,7 +198,14 @@ impl<T: BeaconChainTypes> ActiveCustodyRequest<T> { cx: &mut SyncNetworkContext<T>, ) -> CustodyRequestResult<T::EthSpec> { let _guard = self.span.clone().entered(); - if self.column_requests.values().all(|r| r.is_downloaded()) { + let total_requests = self.column_requests.len(); + let completed_requests = self + .column_requests + .values() + .filter(|r| r.is_downloaded()) + .count(); + + if completed_requests >= total_requests { // All requests have completed successfully. let mut peers = HashMap::<PeerId, Vec<usize>>::new(); let mut seen_timestamps = vec![]; @@ -222,6 +229,7 @@ impl<T: BeaconChainTypes> ActiveCustodyRequest<T> { let active_request_count_by_peer = cx.active_request_count_by_peer(); let mut columns_to_request_by_peer = HashMap::<PeerId, Vec<ColumnIndex>>::new(); + let mut columns_without_peers = vec![]; let lookup_peers = self.lookup_peers.read(); // Create deterministic hasher per request to ensure consistent peer ordering within // this request (avoiding fragmentation) while varying selection across different requests @@ -256,6 +264,7 @@ impl<T: BeaconChainTypes> ActiveCustodyRequest<T> { return Err(Error::NoPeer(*column_index)); } else { // Do not issue requests if there is no custody peer on this column + columns_without_peers.push(*column_index); } } } @@ -270,10 +279,13 @@ impl<T: BeaconChainTypes> ActiveCustodyRequest<T> { lookup_peers = lookup_peers.len(), "Requesting {} columns from {} peers", columns_requested_count, peer_requests, ); - } else { + } else if !columns_without_peers.is_empty() { debug!( lookup_peers = lookup_peers.len(), - "No column peers found for look up", + total_requests, + completed_requests, + ?columns_without_peers, + "No column peers found for lookup", ); } @@ -288,7 +300,7 @@ impl<T: BeaconChainTypes> ActiveCustodyRequest<T> { }, // If peer is in the lookup peer set, it claims to have imported the block and // must have its columns in custody. In that case, set `true = enforce max_requests` - // and downscore if data_columns_by_root does not returned the expected custody + // and downscore if data_columns_by_root does not return the expected custody // columns. For the rest of peers, don't downscore if columns are missing. lookup_peers.contains(&peer_id), ) diff --git a/beacon_node/network/src/sync/range_sync/chain_collection.rs b/beacon_node/network/src/sync/range_sync/chain_collection.rs index 1d57ee6c3d..b91b88b55c 100644 --- a/beacon_node/network/src/sync/range_sync/chain_collection.rs +++ b/beacon_node/network/src/sync/range_sync/chain_collection.rs @@ -41,6 +41,13 @@ pub enum RangeSyncState { pub type SyncChainStatus = Result<Option<(RangeSyncType, Slot /* from */, Slot /* to */)>, &'static str>; +#[cfg(test)] +#[derive(Default, Debug)] +pub struct ChainCollectionMetrics { + pub chains_added: usize, + pub chains_removed: usize, +} + /// A collection of finalized and head chains currently being processed. pub struct ChainCollection<T: BeaconChainTypes> { /// The beacon chain for processing. @@ -51,6 +58,9 @@ pub struct ChainCollection<T: BeaconChainTypes> { head_chains: FnvHashMap<ChainId, SyncingChain<T>>, /// The current sync state of the process. state: RangeSyncState, + #[cfg(test)] + /// Used for testing assertions + metrics: ChainCollectionMetrics, } impl<T: BeaconChainTypes> ChainCollection<T> { @@ -60,12 +70,23 @@ impl<T: BeaconChainTypes> ChainCollection<T> { finalized_chains: FnvHashMap::default(), head_chains: FnvHashMap::default(), state: RangeSyncState::Idle, + #[cfg(test)] + metrics: <_>::default(), } } + #[cfg(test)] + pub(crate) fn metrics(&self) -> &ChainCollectionMetrics { + &self.metrics + } + /// Updates the Syncing state of the collection after a chain is removed. fn on_chain_removed(&mut self, id: &ChainId, was_syncing: bool, sync_type: RangeSyncType) { metrics::inc_counter_vec(&metrics::SYNCING_CHAINS_REMOVED, &[sync_type.as_str()]); + #[cfg(test)] + { + self.metrics.chains_removed += 1; + } self.update_metrics(); match self.state { @@ -510,6 +531,10 @@ impl<T: BeaconChainTypes> ChainCollection<T> { ); collection.insert(id, new_chain); metrics::inc_counter_vec(&metrics::SYNCING_CHAINS_ADDED, &[sync_type.as_str()]); + #[cfg(test)] + { + self.metrics.chains_added += 1; + } self.update_metrics(); } } diff --git a/beacon_node/network/src/sync/range_sync/range.rs b/beacon_node/network/src/sync/range_sync/range.rs index c9656ad1d0..86625444be 100644 --- a/beacon_node/network/src/sync/range_sync/range.rs +++ b/beacon_node/network/src/sync/range_sync/range.rs @@ -98,6 +98,11 @@ where self.failed_chains.keys().copied().collect() } + #[cfg(test)] + pub(crate) fn metrics(&self) -> &super::chain_collection::ChainCollectionMetrics { + self.chains.metrics() + } + pub fn state(&self) -> SyncChainStatus { self.chains.state() } diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index b6e96737d6..769a11d976 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -1,79 +1,171 @@ +use super::*; use crate::NetworkMessage; -use crate::network_beacon_processor::NetworkBeaconProcessor; -use crate::sync::block_lookups::{ - BlockLookupSummary, PARENT_DEPTH_TOLERANCE, SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS, -}; +use crate::network_beacon_processor::{InvalidBlockStorage, NetworkBeaconProcessor}; +use crate::sync::block_lookups::{BlockLookupSummary, PARENT_DEPTH_TOLERANCE}; use crate::sync::{ SyncMessage, manager::{BlockProcessType, BlockProcessingResult, SyncManager}, }; -use std::sync::Arc; -use std::time::Duration; - -use super::*; - -use crate::sync::block_lookups::common::ResponseType; -use beacon_chain::observed_data_sidecars::Observe; +use beacon_chain::blob_verification::KzgVerifiedBlob; +use beacon_chain::custody_context::NodeCustodyType; use beacon_chain::{ - AvailabilityPendingExecutedBlock, AvailabilityProcessingStatus, BlockError, - PayloadVerificationOutcome, PayloadVerificationStatus, - blob_verification::GossipVerifiedBlob, - block_verification_types::{AsBlock, BlockImportData}, - custody_context::NodeCustodyType, + AvailabilityProcessingStatus, BlockError, NotifyExecutionLayer, + block_verification_types::{AsBlock, AvailableBlockData}, data_availability_checker::Availability, test_utils::{ - BeaconChainHarness, EphemeralHarnessType, NumBlobs, generate_rand_block_and_blobs, - generate_rand_block_and_data_columns, test_spec, + AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, NumBlobs, + generate_rand_block_and_blobs, test_spec, }, - validator_monitor::timestamp_now, }; -use beacon_processor::WorkEvent; +use beacon_processor::{BeaconProcessorChannels, DuplicateCache, Work, WorkEvent}; +use educe::Educe; +use itertools::Itertools; use lighthouse_network::discovery::CombinedKey; use lighthouse_network::{ NetworkConfig, NetworkGlobals, PeerId, - rpc::{RPCError, RequestType, RpcErrorResponse}, - service::api_types::{ - AppRequestId, DataColumnsByRootRequestId, DataColumnsByRootRequester, Id, - SingleLookupReqId, SyncRequestId, - }, + rpc::{RPCError, RequestType}, + service::api_types::{AppRequestId, SyncRequestId}, types::SyncState, }; use slot_clock::{SlotClock, TestingSlotClock}; +use std::sync::Arc; +use std::time::Duration; use tokio::sync::mpsc; use tracing::info; use types::{ - BeaconState, BeaconStateBase, BlobSidecar, BlockImportSource, DataColumnSidecar, EthSpec, - ForkContext, ForkName, Hash256, MinimalEthSpec as E, SignedBeaconBlock, Slot, - data::ColumnIndex, - test_utils::{SeedableRng, TestRandom, XorShiftRng}, + BlobSidecar, BlockImportSource, ColumnIndex, DataColumnSidecar, EthSpec, ForkContext, ForkName, + Hash256, MinimalEthSpec as E, SignedBeaconBlock, Slot, + test_utils::{SeedableRng, XorShiftRng}, }; const D: Duration = Duration::new(0, 0); -const PARENT_FAIL_TOLERANCE: u8 = SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS; -type DCByRootIds = Vec<DCByRootId>; -type DCByRootId = (SyncRequestId, Vec<ColumnIndex>); -impl TestRig { - pub fn test_setup() -> Self { - Self::test_setup_with_custody_type(NodeCustodyType::Fullnode) +/// Configuration for how the test rig should respond to sync requests. +/// +/// Controls simulated peer behavior during lookup tests, including RPC errors, +/// invalid responses, and custom block processing results. Use builder methods +/// to configure specific failure scenarios. +#[derive(Default, Educe)] +#[educe(Debug)] +pub struct SimulateConfig { + return_rpc_error: Option<RPCError>, + return_wrong_blocks_n_times: usize, + return_wrong_sidecar_for_block_n_times: usize, + return_no_blocks_n_times: usize, + return_no_data_n_times: usize, + return_too_few_data_n_times: usize, + return_no_columns_on_indices_n_times: usize, + return_no_columns_on_indices: Vec<ColumnIndex>, + skip_by_range_routes: bool, + // Use a callable fn because BlockProcessingResult does not implement Clone + #[educe(Debug(ignore))] + process_result_conditional: + Option<Box<dyn Fn(Hash256) -> Option<BlockProcessingResult> + Send + Sync>>, + // Import a block directly before processing it (for simulating race conditions) + import_block_before_process: HashSet<Hash256>, +} + +impl SimulateConfig { + fn new() -> Self { + Self::default() } - pub fn test_setup_with_custody_type(node_custody_type: NodeCustodyType) -> Self { + fn happy_path() -> Self { + Self::default() + } + + fn return_no_blocks_always(mut self) -> Self { + self.return_no_blocks_n_times = usize::MAX; + self + } + + fn return_no_blocks_once(mut self) -> Self { + self.return_no_blocks_n_times = 1; + self + } + + fn return_no_data_once(mut self) -> Self { + self.return_no_data_n_times = 1; + self + } + + fn return_wrong_blocks_once(mut self) -> Self { + self.return_wrong_blocks_n_times = 1; + self + } + + fn return_wrong_sidecar_for_block_once(mut self) -> Self { + self.return_wrong_sidecar_for_block_n_times = 1; + self + } + + fn return_too_few_data_once(mut self) -> Self { + self.return_too_few_data_n_times = 1; + self + } + + fn return_no_columns_on_indices(mut self, indices: &[ColumnIndex], times: usize) -> Self { + self.return_no_columns_on_indices_n_times = times; + self.return_no_columns_on_indices = indices.to_vec(); + self + } + + fn return_rpc_error(mut self, error: RPCError) -> Self { + self.return_rpc_error = Some(error); + self + } + + fn no_range_sync(mut self) -> Self { + self.skip_by_range_routes = true; + self + } + + fn with_process_result<F>(mut self, f: F) -> Self + where + F: Fn() -> BlockProcessingResult + Send + Sync + 'static, + { + self.process_result_conditional = Some(Box::new(move |_| Some(f()))); + self + } + + fn with_import_block_before_process(mut self, block_root: Hash256) -> Self { + self.import_block_before_process.insert(block_root); + self + } +} + +fn genesis_fork() -> ForkName { + test_spec::<E>().fork_name_at_slot::<E>(Slot::new(0)) +} + +pub(crate) struct TestRigConfig { + fulu_test_type: FuluTestType, + /// Override the node custody type derived from `fulu_test_type` + node_custody_type_override: Option<NodeCustodyType>, +} + +impl TestRig { + pub(crate) fn new(test_rig_config: TestRigConfig) -> Self { // Use `fork_from_env` logic to set correct fork epochs - let spec = test_spec::<E>(); + let spec = Arc::new(test_spec::<E>()); + let clock = TestingSlotClock::new( + Slot::new(0), + Duration::from_secs(0), + Duration::from_secs(12), + ); // Initialise a new beacon chain let harness = BeaconChainHarness::<EphemeralHarnessType<E>>::builder(E) - .spec(Arc::new(spec)) + .spec(spec.clone()) .deterministic_keypairs(1) .fresh_ephemeral_store() .mock_execution_layer() - .testing_slot_clock(TestingSlotClock::new( - Slot::new(0), - Duration::from_secs(0), - Duration::from_secs(12), - )) - .node_custody_type(node_custody_type) + .testing_slot_clock(clock.clone()) + .node_custody_type( + test_rig_config + .node_custody_type_override + .unwrap_or_else(|| test_rig_config.fulu_test_type.we_node_custody_type()), + ) .build(); let chain = harness.chain.clone(); @@ -93,12 +185,23 @@ impl TestRig { network_config, chain.spec.clone(), )); - let (beacon_processor, beacon_processor_rx) = NetworkBeaconProcessor::null_for_testing( - globals, + + let BeaconProcessorChannels { + beacon_processor_tx, + beacon_processor_rx, + } = <_>::default(); + + let beacon_processor = NetworkBeaconProcessor { + beacon_processor_send: beacon_processor_tx, + duplicate_cache: DuplicateCache::default(), + chain: chain.clone(), + // TODO: What is this sender used for? + network_tx: mpsc::unbounded_channel().0, sync_tx, - chain.clone(), - harness.runtime.task_executor.clone(), - ); + network_globals: globals.clone(), + invalid_block_storage: InvalidBlockStorage::Disabled, + executor: harness.runtime.task_executor.clone(), + }; let fork_name = chain.spec.fork_name_at_slot::<E>(chain.slot().unwrap()); @@ -119,6 +222,7 @@ impl TestRig { network_rx, network_rx_queue: vec![], sync_rx, + sync_rx_queue: vec![], rng_08, rng, network_globals: beacon_processor.network_globals.clone(), @@ -132,36 +236,985 @@ impl TestRig { ), harness, fork_name, + network_blocks_by_root: <_>::default(), + network_blocks_by_slot: <_>::default(), + penalties: <_>::default(), + seen_lookups: <_>::default(), + requests: <_>::default(), + complete_strategy: <_>::default(), + initial_block_lookups_metrics: <_>::default(), + fulu_test_type: test_rig_config.fulu_test_type, } } - fn test_setup_after_deneb_before_fulu() -> Option<Self> { - let r = Self::test_setup(); - if r.after_deneb() && !r.fork_name.fulu_enabled() { - Some(r) + pub fn default() -> Self { + // Before Fulu, FuluTestType is irrelevant + Self::new(TestRigConfig { + fulu_test_type: FuluTestType::WeFullnodeThemSupernode, + node_custody_type_override: None, + }) + } + + pub fn with_custody_type(node_custody_type: NodeCustodyType) -> Self { + Self::new(TestRigConfig { + fulu_test_type: FuluTestType::WeFullnodeThemSupernode, + node_custody_type_override: Some(node_custody_type), + }) + } + + /// Runs the sync simulation until all event queues are empty. + /// + /// Processes events from sync_rx (sink), beacon processor, and network queues in fixed + /// priority order each tick. Handles completed work before pulling new requests. + async fn simulate(&mut self, complete_strategy: SimulateConfig) { + self.complete_strategy = complete_strategy; + self.log(&format!( + "Running simulate with config {:?}", + self.complete_strategy + )); + + let mut i = 0; + + loop { + i += 1; + + // Record current status + for BlockLookupSummary { + id, + block_root, + peers, + .. + } in self.active_single_lookups() + { + let lookup = self.seen_lookups.entry(id).or_insert(SeenLookup { + id, + block_root, + seen_peers: <_>::default(), + }); + for peer in peers { + lookup.seen_peers.insert(peer); + } + } + + // Drain all channels into queues + while let Ok(ev) = self.network_rx.try_recv() { + self.network_rx_queue.push(ev); + } + while let Ok(ev) = self.beacon_processor_rx.try_recv() { + self.beacon_processor_rx_queue.push(ev); + } + while let Ok(ev) = self.sync_rx.try_recv() { + self.sync_rx_queue.push(ev); + } + + // Process one event per tick in fixed priority: sink → processor → network + if !self.sync_rx_queue.is_empty() { + let sync_message = self.sync_rx_queue.remove(0); + self.log(&format!( + "Tick {i}: sync_rx event: {}", + Into::<&'static str>::into(&sync_message) + )); + self.sync_manager.handle_message(sync_message); + } else if !self.beacon_processor_rx_queue.is_empty() { + let event = self.beacon_processor_rx_queue.remove(0); + self.log(&format!("Tick {i}: beacon_processor event: {event:?}")); + match event.work { + Work::RpcBlock { + process_fn, + beacon_block_root, + } => { + // Import block before processing if configured (for simulating race conditions) + if self + .complete_strategy + .import_block_before_process + .contains(&beacon_block_root) + { + self.log(&format!( + "Importing block {} before processing (race condition simulation)", + beacon_block_root + )); + self.import_block_by_root(beacon_block_root).await; + } + + if let Some(f) = self.complete_strategy.process_result_conditional.as_ref() + && let Some(result) = f(beacon_block_root) + { + let id = self.lookup_by_root(beacon_block_root).id; + self.log(&format!( + "Sending custom process result to lookup id {id}: {result:?}" + )); + self.push_sync_message(SyncMessage::BlockComponentProcessed { + process_type: BlockProcessType::SingleBlock { id }, + result, + }); + } else { + process_fn.await + } + } + Work::RpcBlobs { process_fn } + | Work::RpcCustodyColumn(process_fn) + | Work::ChainSegment(process_fn) => process_fn.await, + Work::Reprocess(_) => {} // ignore + other => panic!("Unsupported Work event {}", other.str_id()), + } + } else if !self.network_rx_queue.is_empty() { + let event = self.network_rx_queue.remove(0); + self.log(&format!("Tick {i}: network_rx event: {event:?}")); + match event { + NetworkMessage::SendRequest { + peer_id, + request, + app_request_id, + } => { + self.simulate_on_request(peer_id, request, app_request_id); + } + NetworkMessage::ReportPeer { peer_id, msg, .. } => { + self.penalties.push(ReportedPenalty { peer_id, msg }); + } + _ => {} + } + } else { + break; + } + } + + self.log("No more events in simulation"); + self.log(&format!( + "Lookup metrics: {:?}", + self.sync_manager.block_lookups().metrics() + )); + self.log(&format!( + "Range sync metrics: {:?}", + self.sync_manager.range_sync().metrics() + )); + self.log(&format!( + "Max known slot: {}, Head slot: {}", + self.max_known_slot(), + self.head_slot() + )); + self.log(&format!("Penalties: {:?}", self.penalties)); + self.log(&format!( + "Total requests {}: {:?}", + self.requests.len(), + self.requests_count() + )) + } + + fn simulate_on_request( + &mut self, + peer_id: PeerId, + request: RequestType<E>, + app_req_id: AppRequestId, + ) { + self.requests.push((request.clone(), app_req_id)); + + if let AppRequestId::Sync(req_id) = app_req_id + && let Some(error) = self.complete_strategy.return_rpc_error.take() + { + self.log(&format!( + "Completing request {req_id:?} to {peer_id} with RPCError {error:?}" + )); + self.send_sync_message(SyncMessage::RpcError { + sync_request_id: req_id, + peer_id, + error, + }); + return; + } + + match (request, app_req_id) { + (RequestType::BlocksByRoot(req), AppRequestId::Sync(req_id)) => { + let blocks = + req.block_roots() + .iter() + .filter_map(|block_root| { + if self.complete_strategy.return_no_blocks_n_times > 0 { + self.complete_strategy.return_no_blocks_n_times -= 1; + None + } else if self.complete_strategy.return_wrong_blocks_n_times > 0 { + self.complete_strategy.return_wrong_blocks_n_times -= 1; + Some(Arc::new(self.rand_block())) + } else { + Some(self.network_blocks_by_root + .get(block_root) + .unwrap_or_else(|| { + panic!("Test consumer requested unknown block: {block_root:?}") + }) + .block_cloned()) + } + }) + .collect::<Vec<_>>(); + + 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() + .and_then(|d| d.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::<Vec<_>>(); + + 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; + return self.send_rpc_columns_response(req_id, peer_id, &[]); + } + + 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) + }) + }); + let columns_to_omit = if will_omit_columns + && self.complete_strategy.return_no_columns_on_indices_n_times > 0 + { + self.log(&format!("OMIT {:?}", req)); + self.complete_strategy.return_no_columns_on_indices_n_times -= 1; + self.complete_strategy.return_no_columns_on_indices.clone() + } else { + vec![] + }; + + let mut columns = req + .data_column_ids + .iter() + .flat_map(|id| { + let block_columns = self + .network_blocks_by_root + .get(&id.block_root) + .unwrap_or_else(|| { + panic!("Test consumer requested unknown block: {id:?}") + }) + .block_data() + .and_then(|d| d.data_columns()) + .unwrap_or_else(|| panic!("Block id {id:?} has no columns")); + id.columns + .iter() + .filter(|index| !columns_to_omit.contains(index)) + .map(move |index| { + block_columns + .iter() + .find(|c| *c.index() == *index) + .unwrap_or_else(|| { + panic!("Column {index:?} {:?} not found", id.block_root) + }) + .clone() + }) + }) + .collect::<Vec<_>>(); + + if self.complete_strategy.return_too_few_data_n_times > 0 { + self.complete_strategy.return_too_few_data_n_times -= 1; + columns.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 = 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; + } + self.send_rpc_columns_response(req_id, peer_id, &columns); + } + + (RequestType::BlocksByRange(req), AppRequestId::Sync(req_id)) => { + if self.complete_strategy.skip_by_range_routes { + return; + } + let blocks = (*req.start_slot()..req.start_slot() + req.count()) + .filter_map(|slot| { + self.network_blocks_by_slot + .get(&Slot::new(slot)) + .map(|block| block.block_cloned()) + }) + .collect::<Vec<_>>(); + + self.send_rpc_blocks_response(req_id, peer_id, &blocks); + } + + (RequestType::BlobsByRange(req), AppRequestId::Sync(req_id)) => { + if self.complete_strategy.skip_by_range_routes { + return; + } + + // Note: This function is permissive, blocks may have zero blobs and it won't + // error. Some caveats: + // - The genesis block never has blobs + // - Some blocks may not have blobs as the blob count is random + let blobs = (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().and_then(|d| d.blobs())) + .flat_map(|blobs| blobs.into_iter()) + .collect::<Vec<_>>(); + self.send_rpc_blobs_response(req_id, peer_id, &blobs); + } + + (RequestType::DataColumnsByRange(req), AppRequestId::Sync(req_id)) => { + if self.complete_strategy.skip_by_range_routes { + return; + } + // Note: This function is permissive, blocks may have zero columns and it won't + // error. Some caveats: + // - The genesis block never has columns + // - Some blocks may not have columns as the blob count is random + 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().and_then(|d| d.data_columns())) + .flat_map(|columns| { + columns + .into_iter() + .filter(|c| req.columns.contains(c.index())) + }) + .collect::<Vec<_>>(); + self.send_rpc_columns_response(req_id, peer_id, &columns); + } + + (RequestType::Status(_req), AppRequestId::Router) => { + // Ignore Status requests for now + } + + other => panic!("Request not supported: {app_req_id:?} {other:?}"), + } + } + + fn send_rpc_blocks_response( + &mut self, + sync_request_id: SyncRequestId, + peer_id: PeerId, + blocks: &[Arc<SignedBeaconBlock<E>>], + ) { + let slots = blocks.iter().map(|block| block.slot()).collect::<Vec<_>>(); + self.log(&format!( + "Completing request {sync_request_id:?} to {peer_id} with blocks {slots:?}" + )); + + for block in blocks { + self.push_sync_message(SyncMessage::RpcBlock { + sync_request_id, + peer_id, + beacon_block: Some(block.clone()), + seen_timestamp: D, + }); + } + self.push_sync_message(SyncMessage::RpcBlock { + sync_request_id, + peer_id, + beacon_block: None, + seen_timestamp: D, + }); + } + + fn send_rpc_blobs_response( + &mut self, + sync_request_id: SyncRequestId, + peer_id: PeerId, + blobs: &[Arc<BlobSidecar<E>>], + ) { + let slots = blobs + .iter() + .map(|block| block.slot()) + .unique() + .collect::<Vec<_>>(); + self.log(&format!( + "Completing request {sync_request_id:?} to {peer_id} with blobs {slots:?}" + )); + + for blob in blobs { + self.push_sync_message(SyncMessage::RpcBlob { + sync_request_id, + peer_id, + blob_sidecar: Some(blob.clone()), + seen_timestamp: D, + }); + } + self.push_sync_message(SyncMessage::RpcBlob { + sync_request_id, + peer_id, + blob_sidecar: None, + seen_timestamp: D, + }); + } + + fn send_rpc_columns_response( + &mut self, + sync_request_id: SyncRequestId, + peer_id: PeerId, + columns: &[Arc<DataColumnSidecar<E>>], + ) { + let slots = columns + .iter() + .map(|block| block.slot()) + .unique() + .collect::<Vec<_>>(); + let indices = columns + .iter() + .map(|column| *column.index()) + .unique() + .collect::<Vec<_>>(); + self.log(&format!( + "Completing request {sync_request_id:?} to {peer_id} with columns {slots:?} indices {indices:?}" + )); + + for column in columns { + self.push_sync_message(SyncMessage::RpcDataColumn { + sync_request_id, + peer_id, + data_column: Some(column.clone()), + seen_timestamp: D, + }); + } + self.push_sync_message(SyncMessage::RpcDataColumn { + sync_request_id, + peer_id, + data_column: None, + seen_timestamp: D, + }); + } + + // Preparation steps + + /// Returns the block root of the tip of the built chain + async fn build_chain(&mut self, block_count: usize) -> Hash256 { + let mut blocks = vec![]; + + // Initialise a new beacon chain + let external_harness = BeaconChainHarness::<EphemeralHarnessType<E>>::builder(E) + .spec(self.harness.spec.clone()) + .deterministic_keypairs(1) + .fresh_ephemeral_store() + .mock_execution_layer() + .testing_slot_clock(self.harness.chain.slot_clock.clone()) + // Make the external harness a supernode so all columns are available + .node_custody_type(NodeCustodyType::Supernode) + .build(); + // Ensure all blocks have data. Otherwise, the triggers for unknown blob parent and unknown + // data column parent fail. + external_harness + .execution_block_generator() + .set_min_blob_count(1); + + // Add genesis block for completeness + let genesis_block = external_harness.get_head_block(); + self.network_blocks_by_root + .insert(genesis_block.canonical_root(), genesis_block.clone()); + self.network_blocks_by_slot + .insert(genesis_block.slot(), genesis_block); + + for i in 0..block_count { + external_harness.advance_slot(); + let block_root = external_harness + .extend_chain( + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .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, + )); + 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()); + + blocks.last().expect("empty blocks").1 + } + + fn corrupt_last_block_signature(&mut self) { + let rpc_block = self.get_last_block().clone(); + let mut block = (*rpc_block.block_cloned()).clone(); + let blobs = rpc_block.block_data().and_then(|d| d.blobs()); + let columns = rpc_block.block_data().and_then(|d| d.data_columns()); + *block.signature_mut() = self.valid_signature(); + self.re_insert_block(Arc::new(block), blobs, columns); + } + + fn valid_signature(&mut self) -> bls::Signature { + let keypair = bls::Keypair::random(); + let msg = Hash256::random(); + keypair.sk.sign(msg) + } + + fn corrupt_last_blob_proposer_signature(&mut self) { + let rpc_block = self.get_last_block().clone(); + let block = rpc_block.block_cloned(); + let mut blobs = rpc_block + .block_data() + .and_then(|d| d.blobs()) + .expect("no blobs") + .into_iter() + .collect::<Vec<_>>(); + let columns = rpc_block.block_data().and_then(|d| d.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 rpc_block = self.get_last_block().clone(); + let block = rpc_block.block_cloned(); + let mut blobs = rpc_block + .block_data() + .and_then(|d| d.blobs()) + .expect("no blobs") + .into_iter() + .collect::<Vec<_>>(); + let columns = rpc_block.block_data().and_then(|d| d.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 rpc_block = self.get_last_block().clone(); + let block = rpc_block.block_cloned(); + let blobs = rpc_block.block_data().and_then(|d| d.blobs()); + let mut columns = rpc_block + .block_data() + .and_then(|d| d.data_columns()) + .expect("no columns"); + let first = columns.first_mut().expect("empty columns"); + Arc::make_mut(first) + .signed_block_header_mut() + .expect("not fulu") + .signature = self.valid_signature(); + self.re_insert_block(block, blobs, Some(columns)); + } + + fn corrupt_last_column_kzg_proof(&mut self) { + let rpc_block = self.get_last_block().clone(); + let block = rpc_block.block_cloned(); + let blobs = rpc_block.block_data().and_then(|d| d.blobs()); + let mut columns = rpc_block + .block_data() + .and_then(|d| d.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"); + *proof = kzg::KzgProof::empty(); + self.re_insert_block(block, blobs, Some(columns)); + } + + fn get_last_block(&self) -> &RpcBlock<E> { + let (_, last_block) = self + .network_blocks_by_root + .iter() + .max_by_key(|(_, block)| block.slot()) + .expect("no blocks"); + last_block + } + + fn re_insert_block( + &mut self, + block: Arc<SignedBeaconBlock<E>>, + blobs: Option<types::BlobSidecarList<E>>, + columns: Option<types::DataColumnSidecarList<E>>, + ) { + self.network_blocks_by_slot.clear(); + self.network_blocks_by_root.clear(); + let block_root = block.canonical_root(); + let block_slot = block.slot(); + let block_data = if let Some(columns) = columns { + Some(AvailableBlockData::new_with_data_columns(columns)) + } else if let Some(blobs) = blobs { + Some(AvailableBlockData::new_with_blobs(blobs)) + } else { + Some(AvailableBlockData::NoData) + }; + let rpc_block = RpcBlock::new( + block, + block_data, + &self.harness.chain.data_availability_checker, + self.harness.chain.spec.clone(), + ) + .unwrap(); + self.network_blocks_by_slot + .insert(block_slot, rpc_block.clone()); + self.network_blocks_by_root.insert(block_root, rpc_block); + } + + /// Trigger a lookup with the last created block + fn trigger_with_last_block(&mut self) { + let peer_id = match self.fulu_test_type.them_node_custody_type() { + NodeCustodyType::Fullnode => self.new_connected_peer(), + NodeCustodyType::Supernode | NodeCustodyType::SemiSupernode => { + self.new_connected_supernode_peer() + } + }; + let last_block = self.get_last_block().canonical_root(); + self.trigger_unknown_block_from_attestation(last_block, peer_id); + } + + fn block_at_slot(&self, slot: u64) -> Arc<SignedBeaconBlock<E>> { + self.network_blocks_by_slot + .get(&Slot::new(slot)) + .unwrap_or_else(|| panic!("No block for slot {slot}")) + .block_cloned() + } + + fn block_root_at_slot(&self, slot: u64) -> Hash256 { + self.block_at_slot(slot).canonical_root() + } + + fn trigger_with_block_at_slot(&mut self, slot: u64) { + let peer_id = self.new_connected_supernode_peer(); + let block = self.block_at_slot(slot); + self.trigger_unknown_block_from_attestation(block.canonical_root(), peer_id); + } + + async fn build_chain_and_trigger_last_block(&mut self, block_count: usize) { + self.build_chain(block_count).await; + self.trigger_with_last_block(); + } + + /// Import a block directly into the chain without going through lookup sync + async fn import_block_by_root(&mut self, block_root: Hash256) { + let rpc_block = self + .network_blocks_by_root + .get(&block_root) + .unwrap_or_else(|| panic!("No block for root {block_root}")) + .clone(); + + self.harness + .chain + .process_block( + block_root, + rpc_block, + NotifyExecutionLayer::Yes, + BlockImportSource::Gossip, + || Ok(()), + ) + .await + .unwrap(); + + self.harness.chain.recompute_head_at_current_slot().await; + } + + fn trigger_with_last_unknown_block_parent(&mut self) { + let peer_id = self.new_connected_supernode_peer(); + let last_block = self.get_last_block().block_cloned(); + self.trigger_unknown_parent_block(peer_id, last_block); + } + + fn trigger_with_last_unknown_blob_parent(&mut self) { + let peer_id = self.new_connected_supernode_peer(); + let blobs = self + .get_last_block() + .block_data() + .and_then(|d| d.blobs()) + .expect("no blobs"); + let blob = blobs.first().expect("empty blobs"); + self.trigger_unknown_parent_blob(peer_id, blob.clone()); + } + + fn trigger_with_last_unknown_data_column_parent(&mut self) { + let peer_id = self.new_connected_supernode_peer(); + let columns = self + .get_last_block() + .block_data() + .and_then(|d| d.data_columns()) + .expect("No data columns"); + let column = columns.first().expect("empty columns"); + self.trigger_unknown_parent_column(peer_id, column.clone()); + } + + // Post-test assertions + + fn head_slot(&self) -> Slot { + self.harness.chain.head().head_slot() + } + + fn assert_head_slot(&self, slot: u64) { + assert_eq!(self.head_slot(), Slot::new(slot), "Unexpected head slot"); + } + + fn max_known_slot(&self) -> Slot { + self.network_blocks_by_slot + .keys() + .max() + .copied() + .expect("no blocks") + } + + fn assert_penalties(&self, expected_penalties: &[&'static str]) { + let penalties = self + .penalties + .iter() + .map(|penalty| penalty.msg) + .collect::<Vec<_>>(); + if penalties != expected_penalties { + panic!( + "Expected penalties: {:#?} but got {:#?}", + expected_penalties, + self.penalties + .iter() + .map(|p| format!("{} for peer {}", p.msg, p.peer_id)) + .collect::<Vec<_>>() + ); + } + } + + fn assert_penalties_of_type(&self, expected_penalty: &'static str) { + if self.penalties.is_empty() { + panic!("No penalties but expected some of type {expected_penalty}"); + } + let non_matching_penalties = self + .penalties + .iter() + .filter(|penalty| penalty.msg != expected_penalty) + .collect::<Vec<_>>(); + if !non_matching_penalties.is_empty() { + panic!( + "Found non-matching penalties to {}: {:?}", + expected_penalty, non_matching_penalties + ); + } + } + + fn assert_no_penalties(&mut self) { + if !self.penalties.is_empty() { + panic!("Some downscore events: {:?}", self.penalties); + } + } + fn assert_failed_lookup_sync(&mut self) { + assert!(self.created_lookups() > 0, "no created lookups"); + assert_eq!(self.completed_lookups(), 0, "some completed lookups"); + assert_eq!( + self.dropped_lookups(), + self.created_lookups(), + "not all dropped. Current lookups {:?}", + self.active_single_lookups(), + ); + self.assert_empty_network(); + self.assert_no_active_lookups(); + } + + fn assert_successful_lookup_sync(&mut self) { + assert!(self.created_lookups() > 0, "no created lookups"); + assert_eq!(self.dropped_lookups(), 0, "some dropped lookups"); + assert_eq!( + self.completed_lookups(), + self.created_lookups(), + "not all lookups completed. Current lookups {:?}", + self.active_single_lookups(), + ); + self.assert_empty_network(); + self.assert_no_active_lookups(); + } + + /// There is a lookup created with the block that triggers the unknown message that can't be + /// completed because it has zero peers + fn assert_successful_lookup_sync_parent_trigger(&mut self) { + assert!(self.created_lookups() > 0, "no created lookups"); + assert_eq!( + self.completed_lookups() + 1, + self.created_lookups(), + "all completed" + ); + assert_eq!(self.dropped_lookups(), 0, "some dropped lookups"); + 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 + fn assert_successful_range_sync(&self) { + assert!( + self.range_sync_chains_added() > 0, + "No created range sync chains" + ); + assert_eq!( + self.range_sync_chains_added(), + self.range_sync_chains_removed(), + "Not all chains completed" + ); + } + + fn lookup_at_slot(&self, slot: u64) -> &SeenLookup { + let block_root = self.block_root_at_slot(slot); + self.seen_lookups + .values() + .find(|lookup| lookup.block_root == block_root) + .unwrap_or_else(|| panic!("No lookup for block_root {block_root} of slot {slot}")) + } + + fn assert_peers_at_lookup_of_slot(&self, slot: u64, expected_peers: usize) { + let lookup = self.lookup_at_slot(slot); + if lookup.seen_peers.len() != expected_peers { + panic!( + "Expected lookup of slot {slot} to have {expected_peers} peers but had {:?}", + lookup.seen_peers + ) + } + } + + /// Total count of unique lookups created + fn created_lookups(&self) -> usize { + // Subtract initial value to allow resetting metrics mid test + self.sync_manager.block_lookups().metrics().created_lookups + - self.initial_block_lookups_metrics.created_lookups + } + + /// Total count of lookups completed or dropped + fn dropped_lookups(&self) -> usize { + // Subtract initial value to allow resetting metrics mid test + self.sync_manager.block_lookups().metrics().dropped_lookups + - self.initial_block_lookups_metrics.dropped_lookups + } + + fn completed_lookups(&self) -> usize { + // Subtract initial value to allow resetting metrics mid test + self.sync_manager + .block_lookups() + .metrics() + .completed_lookups + - self.initial_block_lookups_metrics.completed_lookups + } + + fn capture_metrics_baseline(&mut self) { + self.initial_block_lookups_metrics = self.sync_manager.block_lookups().metrics().clone() + } + + /// Returns the last lookup seen with matching block_root + fn lookup_by_root(&self, block_root: Hash256) -> &SeenLookup { + self.seen_lookups + .values() + .filter(|lookup| lookup.block_root == block_root) + .max_by_key(|lookup| lookup.id) + .unwrap_or_else(|| panic!("No loookup for block_root {block_root}")) + } + + fn range_sync_chains_added(&self) -> usize { + self.sync_manager.range_sync().metrics().chains_added + } + + fn range_sync_chains_removed(&self) -> usize { + self.sync_manager.range_sync().metrics().chains_removed + } + + fn custody_columns(&self) -> &[ColumnIndex] { + self.harness + .chain + .data_availability_checker + .custody_context() + .custody_columns_for_epoch(None, &self.harness.spec) + } + + // Test setup + + fn new_after_deneb() -> Option<Self> { + genesis_fork().deneb_enabled().then(Self::default) + } + + fn new_after_deneb_before_fulu() -> Option<Self> { + let fork = genesis_fork(); + if fork.deneb_enabled() && !fork.fulu_enabled() { + Some(Self::default()) } else { None } } - pub fn test_setup_after_fulu() -> Option<Self> { - let r = Self::test_setup(); - if r.fork_name.fulu_enabled() { - Some(r) - } else { - None - } + pub fn new_fulu_peer_test(fulu_test_type: FuluTestType) -> Option<Self> { + genesis_fork().fulu_enabled().then(|| { + Self::new(TestRigConfig { + fulu_test_type, + node_custody_type_override: None, + }) + }) } pub fn log(&self, msg: &str) { info!(msg, "TEST_RIG"); } - pub fn after_deneb(&self) -> bool { + pub fn is_after_deneb(&self) -> bool { self.fork_name.deneb_enabled() } - pub fn after_fulu(&self) -> bool { + pub fn is_after_fulu(&self) -> bool { self.fork_name.fulu_enabled() } @@ -170,8 +1223,16 @@ impl TestRig { self.send_sync_message(SyncMessage::UnknownParentBlock(peer_id, block, block_root)) } - fn trigger_unknown_parent_blob(&mut self, peer_id: PeerId, blob: BlobSidecar<E>) { - self.send_sync_message(SyncMessage::UnknownParentBlob(peer_id, blob.into())); + fn trigger_unknown_parent_blob(&mut self, peer_id: PeerId, blob: Arc<BlobSidecar<E>>) { + self.send_sync_message(SyncMessage::UnknownParentBlob(peer_id, blob)); + } + + fn trigger_unknown_parent_column( + &mut self, + peer_id: PeerId, + column: Arc<DataColumnSidecar<E>>, + ) { + self.send_sync_message(SyncMessage::UnknownParentDataColumn(peer_id, column)); } fn trigger_unknown_block_from_attestation(&mut self, block_root: Hash256, peer_id: PeerId) { @@ -180,13 +1241,6 @@ impl TestRig { )); } - /// Drain all sync messages in the sync_rx attached to the beacon processor - fn drain_sync_rx(&mut self) { - while let Ok(sync_message) = self.sync_rx.try_recv() { - self.send_sync_message(sync_message); - } - } - fn rand_block(&mut self) -> SignedBeaconBlock<E> { self.rand_block_and_blobs(NumBlobs::None).0 } @@ -200,105 +1254,36 @@ impl TestRig { generate_rand_block_and_blobs::<E>(fork_name, num_blobs, rng) } - fn rand_block_and_data_columns( - &mut self, - ) -> (SignedBeaconBlock<E>, Vec<Arc<DataColumnSidecar<E>>>) { - let num_blobs = NumBlobs::Number(1); - generate_rand_block_and_data_columns::<E>( - self.fork_name, - num_blobs, - &mut self.rng, - &self.harness.spec, - ) - } - - pub fn rand_block_and_parent( - &mut self, - ) -> (SignedBeaconBlock<E>, SignedBeaconBlock<E>, Hash256, Hash256) { - let parent = self.rand_block(); - let parent_root = parent.canonical_root(); - let mut block = self.rand_block(); - *block.message_mut().parent_root_mut() = parent_root; - let block_root = block.canonical_root(); - (parent, block, parent_root, block_root) - } - pub fn send_sync_message(&mut self, sync_message: SyncMessage<E>) { self.sync_manager.handle_message(sync_message); } + pub fn push_sync_message(&mut self, sync_message: SyncMessage<E>) { + self.sync_manager.send_sync_message(sync_message); + } + fn active_single_lookups(&self) -> Vec<BlockLookupSummary> { - self.sync_manager.active_single_lookups() + self.sync_manager.block_lookups().active_single_lookups() } fn active_single_lookups_count(&self) -> usize { - self.sync_manager.active_single_lookups().len() - } - - fn active_parent_lookups(&self) -> Vec<Vec<Hash256>> { - self.sync_manager.active_parent_lookups() - } - - fn active_parent_lookups_count(&self) -> usize { - self.sync_manager.active_parent_lookups().len() - } - - fn active_range_sync_chain(&self) -> (RangeSyncType, Slot, Slot) { - self.sync_manager.get_range_sync_chains().unwrap().unwrap() + self.active_single_lookups().len() } fn assert_single_lookups_count(&self, count: usize) { assert_eq!( self.active_single_lookups_count(), count, - "Unexpected count of single lookups. Current lookups: {:?}", + "Unexpected count of single lookups. Current lookups: {:#?}", self.active_single_lookups() ); } - fn assert_parent_lookups_count(&self, count: usize) { - assert_eq!( - self.active_parent_lookups_count(), - count, - "Unexpected count of parent lookups. Parent lookups: {:?}. Current lookups: {:?}", - self.active_parent_lookups(), - self.active_single_lookups() - ); - } - - fn assert_lookup_is_active(&self, block_root: Hash256) { - let lookups = self.sync_manager.active_single_lookups(); - if !lookups.iter().any(|l| l.1 == block_root) { - panic!("Expected lookup {block_root} to be the only active: {lookups:?}"); - } - } - - fn assert_lookup_peers(&self, block_root: Hash256, mut expected_peers: Vec<PeerId>) { - let mut lookup = self - .sync_manager - .active_single_lookups() - .into_iter() - .find(|l| l.1 == block_root) - .unwrap_or_else(|| panic!("no lookup for {block_root}")); - lookup.3.sort(); - expected_peers.sort(); - assert_eq!( - lookup.3, expected_peers, - "unexpected peers on lookup {block_root}" - ); - } - fn insert_ignored_chain(&mut self, block_root: Hash256) { + self.log(&format!("Inserting block in ignored chains {block_root:?}")); self.sync_manager.insert_ignored_chain(block_root); } - fn assert_not_ignored_chain(&mut self, chain_hash: Hash256) { - let chains = self.sync_manager.get_ignored_chains(); - if chains.contains(&chain_hash) { - panic!("ignored chains contain {chain_hash:?}: {chains:?}"); - } - } - fn assert_ignored_chain(&mut self, chain_hash: Hash256) { let chains = self.sync_manager.get_ignored_chains(); if !chains.contains(&chain_hash) { @@ -306,16 +1291,8 @@ impl TestRig { } } - fn find_single_lookup_for(&self, block_root: Hash256) -> Id { - self.active_single_lookups() - .iter() - .find(|l| l.1 == block_root) - .unwrap_or_else(|| panic!("no single block lookup found for {block_root}")) - .0 - } - #[track_caller] - fn expect_no_active_single_lookups(&self) { + fn assert_no_active_single_lookups(&self) { assert!( self.active_single_lookups().is_empty(), "expect no single block lookups: {:?}", @@ -324,13 +1301,8 @@ impl TestRig { } #[track_caller] - fn expect_no_active_lookups(&self) { - self.expect_no_active_single_lookups(); - } - - fn expect_no_active_lookups_empty_network(&mut self) { - self.expect_no_active_lookups(); - self.expect_empty_network(); + fn assert_no_active_lookups(&self) { + self.assert_no_active_single_lookups(); } pub fn new_connected_peer(&mut self) -> PeerId { @@ -340,367 +1312,62 @@ impl TestRig { .peers .write() .__add_connected_peer_testing_only(false, &self.harness.spec, key); - self.log(&format!("Added new peer for testing {peer_id:?}")); + + // Assumes custody subnet count == column count + let custody_subnets = self + .network_globals + .peers + .read() + .peer_info(&peer_id) + .expect("Peer should be known") + .custody_subnets_iter() + .copied() + .collect::<Vec<_>>(); + let peer_custody_str = + if custody_subnets.len() == self.harness.spec.number_of_custody_groups as usize { + "all".to_owned() + } else { + format!("{custody_subnets:?}") + }; + + self.log(&format!( + "Added new peer for testing {peer_id:?}, custody: {peer_custody_str}" + )); peer_id } pub fn new_connected_supernode_peer(&mut self) -> PeerId { let key = self.determinstic_key(); - self.network_globals + let peer_id = self + .network_globals .peers .write() - .__add_connected_peer_testing_only(true, &self.harness.spec, key) + .__add_connected_peer_testing_only(true, &self.harness.spec, key); + self.log(&format!( + "Added new peer for testing {peer_id:?}, custody: supernode" + )); + peer_id } fn determinstic_key(&mut self) -> CombinedKey { k256::ecdsa::SigningKey::random(&mut self.rng_08).into() } - pub fn new_connected_peers_for_peerdas(&mut self) { - // Enough sampling peers with few columns - for _ in 0..100 { - self.new_connected_peer(); - } - // One supernode peer to ensure all columns have at least one peer - self.new_connected_supernode_peer(); - } - - fn parent_chain_processed_success( - &mut self, - chain_hash: Hash256, - blocks: &[Arc<SignedBeaconBlock<E>>], - ) { - // Send import events for all pending parent blocks - for _ in blocks { - self.parent_block_processed_imported(chain_hash); - } - // Send final import event for the block that triggered the lookup - self.single_block_component_processed_imported(chain_hash); - } - - /// Locate a parent lookup chain with tip hash `chain_hash` - fn find_oldest_parent_lookup(&self, chain_hash: Hash256) -> Hash256 { - let parent_chain = self - .active_parent_lookups() - .into_iter() - .find(|chain| chain.first() == Some(&chain_hash)) - .unwrap_or_else(|| { - panic!( - "No parent chain with chain_hash {chain_hash:?}: Parent lookups {:?} Single lookups {:?}", - self.active_parent_lookups(), - self.active_single_lookups(), - ) - }); - *parent_chain.last().unwrap() - } - - fn parent_block_processed(&mut self, chain_hash: Hash256, result: BlockProcessingResult) { - let id = self.find_single_lookup_for(self.find_oldest_parent_lookup(chain_hash)); - self.single_block_component_processed(id, result); - } - - fn parent_blob_processed(&mut self, chain_hash: Hash256, result: BlockProcessingResult) { - let id = self.find_single_lookup_for(self.find_oldest_parent_lookup(chain_hash)); - self.single_blob_component_processed(id, result); - } - - fn parent_block_processed_imported(&mut self, chain_hash: Hash256) { - self.parent_block_processed( - chain_hash, - BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(chain_hash)), - ); - } - - fn single_block_component_processed(&mut self, id: Id, result: BlockProcessingResult) { - self.send_sync_message(SyncMessage::BlockComponentProcessed { - process_type: BlockProcessType::SingleBlock { id }, - result, - }) - } - - fn single_block_component_processed_imported(&mut self, block_root: Hash256) { - let id = self.find_single_lookup_for(block_root); - self.single_block_component_processed( - id, - BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(block_root)), - ) - } - - fn single_blob_component_processed(&mut self, id: Id, result: BlockProcessingResult) { - self.send_sync_message(SyncMessage::BlockComponentProcessed { - process_type: BlockProcessType::SingleBlob { id }, - result, - }) - } - - fn parent_lookup_block_response( - &mut self, - id: SingleLookupReqId, - peer_id: PeerId, - beacon_block: Option<Arc<SignedBeaconBlock<E>>>, - ) { - self.log("parent_lookup_block_response"); - self.send_sync_message(SyncMessage::RpcBlock { - sync_request_id: SyncRequestId::SingleBlock { id }, - peer_id, - beacon_block, - seen_timestamp: D, - }); - } - - fn single_lookup_block_response( - &mut self, - id: SingleLookupReqId, - peer_id: PeerId, - beacon_block: Option<Arc<SignedBeaconBlock<E>>>, - ) { - self.log("single_lookup_block_response"); - self.send_sync_message(SyncMessage::RpcBlock { - sync_request_id: SyncRequestId::SingleBlock { id }, - peer_id, - beacon_block, - seen_timestamp: D, - }); - } - - fn parent_lookup_blob_response( - &mut self, - id: SingleLookupReqId, - peer_id: PeerId, - blob_sidecar: Option<Arc<BlobSidecar<E>>>, - ) { - self.log(&format!( - "parent_lookup_blob_response {:?}", - blob_sidecar.as_ref().map(|b| b.index) - )); - self.send_sync_message(SyncMessage::RpcBlob { - sync_request_id: SyncRequestId::SingleBlob { id }, - peer_id, - blob_sidecar, - seen_timestamp: D, - }); - } - - fn single_lookup_blob_response( - &mut self, - id: SingleLookupReqId, - peer_id: PeerId, - blob_sidecar: Option<Arc<BlobSidecar<E>>>, - ) { - self.send_sync_message(SyncMessage::RpcBlob { - sync_request_id: SyncRequestId::SingleBlob { id }, - peer_id, - blob_sidecar, - seen_timestamp: D, - }); - } - - fn complete_single_lookup_blob_download( - &mut self, - id: SingleLookupReqId, - peer_id: PeerId, - blobs: Vec<BlobSidecar<E>>, - ) { - for blob in blobs { - self.single_lookup_blob_response(id, peer_id, Some(blob.into())); - } - self.single_lookup_blob_response(id, peer_id, None); - } - - fn complete_single_lookup_blob_lookup_valid( - &mut self, - id: SingleLookupReqId, - peer_id: PeerId, - blobs: Vec<BlobSidecar<E>>, - import: bool, - ) { - let block_root = blobs.first().unwrap().block_root(); - let block_slot = blobs.first().unwrap().slot(); - self.complete_single_lookup_blob_download(id, peer_id, blobs); - self.expect_block_process(ResponseType::Blob); - self.single_blob_component_processed( - id.lookup_id, - if import { - BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(block_root)) - } else { - BlockProcessingResult::Ok(AvailabilityProcessingStatus::MissingComponents( - block_slot, block_root, - )) - }, - ); - } - - fn complete_lookup_block_download(&mut self, block: SignedBeaconBlock<E>) { - let block_root = block.canonical_root(); - let id = self.expect_block_lookup_request(block_root); - self.expect_empty_network(); - let peer_id = self.new_connected_peer(); - self.single_lookup_block_response(id, peer_id, Some(block.into())); - self.single_lookup_block_response(id, peer_id, None); - } - - fn complete_lookup_block_import_valid(&mut self, block_root: Hash256, import: bool) { - self.expect_block_process(ResponseType::Block); - let id = self.find_single_lookup_for(block_root); - self.single_block_component_processed( - id, - if import { - BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(block_root)) - } else { - BlockProcessingResult::Ok(AvailabilityProcessingStatus::MissingComponents( - Slot::new(0), - block_root, - )) - }, - ) - } - - fn complete_single_lookup_block_valid(&mut self, block: SignedBeaconBlock<E>, import: bool) { - let block_root = block.canonical_root(); - self.complete_lookup_block_download(block); - self.complete_lookup_block_import_valid(block_root, import) - } - - fn parent_lookup_failed(&mut self, id: SingleLookupReqId, peer_id: PeerId, error: RPCError) { - self.send_sync_message(SyncMessage::RpcError { - peer_id, - sync_request_id: SyncRequestId::SingleBlock { id }, - error, - }) - } - - fn parent_lookup_failed_unavailable(&mut self, id: SingleLookupReqId, peer_id: PeerId) { - self.parent_lookup_failed( - id, - peer_id, - RPCError::ErrorResponse( - RpcErrorResponse::ResourceUnavailable, - "older than deneb".into(), - ), - ); - } - - fn single_lookup_failed(&mut self, id: SingleLookupReqId, peer_id: PeerId, error: RPCError) { - self.send_sync_message(SyncMessage::RpcError { - peer_id, - sync_request_id: SyncRequestId::SingleBlock { id }, - error, - }) - } - - fn complete_valid_block_request( - &mut self, - id: SingleLookupReqId, - block: Arc<SignedBeaconBlock<E>>, - missing_components: bool, - ) { - // Complete download - let peer_id = PeerId::random(); - let slot = block.slot(); - let block_root = block.canonical_root(); - self.single_lookup_block_response(id, peer_id, Some(block)); - self.single_lookup_block_response(id, peer_id, None); - // Expect processing and resolve with import - self.expect_block_process(ResponseType::Block); - self.single_block_component_processed( - id.lookup_id, - if missing_components { - BlockProcessingResult::Ok(AvailabilityProcessingStatus::MissingComponents( - slot, block_root, - )) - } else { - BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(block_root)) - }, - ) - } - - fn complete_valid_custody_request( - &mut self, - ids: DCByRootIds, - data_columns: Vec<Arc<DataColumnSidecar<E>>>, - missing_components: bool, - ) { - let lookup_id = if let SyncRequestId::DataColumnsByRoot(DataColumnsByRootRequestId { - requester: DataColumnsByRootRequester::Custody(id), - .. - }) = ids.first().unwrap().0 - { - id.requester.0.lookup_id - } else { - panic!("not a custody requester") - }; - - let first_column = data_columns.first().cloned().unwrap(); - - for id in ids { - self.log(&format!("return valid data column for {id:?}")); - let indices = &id.1; - let columns_to_send = indices - .iter() - .map(|&i| data_columns[i as usize].clone()) - .collect::<Vec<_>>(); - self.complete_data_columns_by_root_request(id, &columns_to_send); - } - - // Expect work event - self.expect_rpc_custody_column_work_event(); - - // Respond with valid result - self.send_sync_message(SyncMessage::BlockComponentProcessed { - process_type: BlockProcessType::SingleCustodyColumn(lookup_id), - result: if missing_components { - BlockProcessingResult::Ok(AvailabilityProcessingStatus::MissingComponents( - first_column.slot(), - first_column.block_root(), - )) - } else { - BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported( - first_column.block_root(), - )) - }, - }); - } - - fn complete_data_columns_by_root_request( - &mut self, - (sync_request_id, _): DCByRootId, - data_columns: &[Arc<DataColumnSidecar<E>>], - ) { - let peer_id = PeerId::random(); - for data_column in data_columns { - // Send chunks - self.send_sync_message(SyncMessage::RpcDataColumn { - sync_request_id, - peer_id, - data_column: Some(data_column.clone()), - seen_timestamp: timestamp_now(), - }); - } - // Send stream termination - self.send_sync_message(SyncMessage::RpcDataColumn { - sync_request_id, - peer_id, - data_column: None, - seen_timestamp: timestamp_now(), - }); - } - - /// Return RPCErrors for all active requests of peer - fn rpc_error_all_active_requests(&mut self, disconnected_peer_id: PeerId) { - self.drain_network_rx(); - while let Ok(sync_request_id) = self.pop_received_network_event(|ev| match ev { - NetworkMessage::SendRequest { - peer_id, - app_request_id: AppRequestId::Sync(id), - .. - } if *peer_id == disconnected_peer_id => Some(*id), - _ => None, - }) { - self.send_sync_message(SyncMessage::RpcError { - peer_id: disconnected_peer_id, - sync_request_id, - error: RPCError::Disconnected, - }); + pub fn new_connected_peers_for_peerdas(&mut self) -> Vec<PeerId> { + match self.fulu_test_type.them_node_custody_type() { + NodeCustodyType::Fullnode => { + // Enough sampling peers with few columns + let mut peers = (0..100) + .map(|_| self.new_connected_peer()) + .collect::<Vec<_>>(); + // One supernode peer to ensure all columns have at least one peer + peers.push(self.new_connected_supernode_peer()); + peers + } + NodeCustodyType::Supernode | NodeCustodyType::SemiSupernode => { + let peer = self.new_connected_supernode_peer(); + vec![peer] + } } } @@ -708,6 +1375,22 @@ impl TestRig { self.send_sync_message(SyncMessage::Disconnect(peer_id)); } + fn get_connected_peers(&self) -> Vec<PeerId> { + self.network_globals + .peers + .read() + .peers() + .map(|(peer, _)| *peer) + .collect::<Vec<_>>() + } + + fn disconnect_all_peers(&mut self) { + for peer in self.get_connected_peers() { + self.log(&format!("Disconnecting peer {peer}")); + self.send_sync_message(SyncMessage::Disconnect(peer)); + } + } + fn drain_network_rx(&mut self) { while let Ok(event) = self.network_rx.try_recv() { self.network_rx_queue.push(event); @@ -764,7 +1447,7 @@ impl TestRig { } } - pub fn expect_empty_processor(&mut self) { + pub fn assert_empty_processor(&mut self) { self.drain_processor_rx(); if !self.beacon_processor_rx_queue.is_empty() { panic!( @@ -774,215 +1457,8 @@ impl TestRig { } } - fn find_block_lookup_request( - &mut self, - for_block: Hash256, - ) -> Result<SingleLookupReqId, String> { - self.pop_received_network_event(|ev| match ev { - NetworkMessage::SendRequest { - peer_id: _, - request: RequestType::BlocksByRoot(request), - app_request_id: AppRequestId::Sync(SyncRequestId::SingleBlock { id }), - } if request.block_roots().to_vec().contains(&for_block) => Some(*id), - _ => None, - }) - } - #[track_caller] - fn expect_block_lookup_request(&mut self, for_block: Hash256) -> SingleLookupReqId { - self.find_block_lookup_request(for_block) - .unwrap_or_else(|e| panic!("Expected block request for {for_block:?}: {e}")) - } - - fn find_blob_lookup_request( - &mut self, - for_block: Hash256, - ) -> Result<SingleLookupReqId, String> { - self.pop_received_network_event(|ev| match ev { - NetworkMessage::SendRequest { - peer_id: _, - request: RequestType::BlobsByRoot(request), - app_request_id: AppRequestId::Sync(SyncRequestId::SingleBlob { id }), - } if request - .blob_ids - .to_vec() - .iter() - .any(|r| r.block_root == for_block) => - { - Some(*id) - } - _ => None, - }) - } - - #[track_caller] - fn expect_blob_lookup_request(&mut self, for_block: Hash256) -> SingleLookupReqId { - self.find_blob_lookup_request(for_block) - .unwrap_or_else(|e| panic!("Expected blob request for {for_block:?}: {e}")) - } - - #[track_caller] - fn expect_block_parent_request(&mut self, for_block: Hash256) -> SingleLookupReqId { - self.pop_received_network_event(|ev| match ev { - NetworkMessage::SendRequest { - peer_id: _, - request: RequestType::BlocksByRoot(request), - app_request_id: AppRequestId::Sync(SyncRequestId::SingleBlock { id }), - } if request.block_roots().to_vec().contains(&for_block) => Some(*id), - _ => None, - }) - .unwrap_or_else(|e| panic!("Expected block parent request for {for_block:?}: {e}")) - } - - fn expect_no_requests_for(&mut self, block_root: Hash256) { - if let Ok(request) = self.find_block_lookup_request(block_root) { - panic!("Expected no block request for {block_root:?} found {request:?}"); - } - if let Ok(request) = self.find_blob_lookup_request(block_root) { - panic!("Expected no blob request for {block_root:?} found {request:?}"); - } - } - - #[track_caller] - fn expect_blob_parent_request(&mut self, for_block: Hash256) -> SingleLookupReqId { - self.pop_received_network_event(|ev| match ev { - NetworkMessage::SendRequest { - peer_id: _, - request: RequestType::BlobsByRoot(request), - app_request_id: AppRequestId::Sync(SyncRequestId::SingleBlob { id }), - } if request - .blob_ids - .to_vec() - .iter() - .all(|r| r.block_root == for_block) => - { - Some(*id) - } - _ => None, - }) - .unwrap_or_else(|e| panic!("Expected blob parent request for {for_block:?}: {e}")) - } - - /// Retrieves an unknown number of requests for data columns of `block_root`. Because peer ENRs - /// are random, and peer selection is random, the total number of batched requests is unknown. - fn expect_data_columns_by_root_requests( - &mut self, - block_root: Hash256, - count: usize, - ) -> DCByRootIds { - let mut requests: DCByRootIds = vec![]; - loop { - let req = self - .pop_received_network_event(|ev| match ev { - NetworkMessage::SendRequest { - peer_id: _, - request: RequestType::DataColumnsByRoot(request), - app_request_id: - AppRequestId::Sync(id @ SyncRequestId::DataColumnsByRoot { .. }), - } => { - let matching = request - .data_column_ids - .iter() - .find(|id| id.block_root == block_root)?; - - let indices = matching.columns.iter().copied().collect(); - Some((*id, indices)) - } - _ => None, - }) - .unwrap_or_else(|e| { - panic!("Expected more DataColumnsByRoot requests for {block_root:?}: {e}") - }); - requests.push(req); - - // Should never infinite loop because sync does not send requests for 0 columns - if requests.iter().map(|r| r.1.len()).sum::<usize>() >= count { - return requests; - } - } - } - - fn expect_only_data_columns_by_root_requests( - &mut self, - for_block: Hash256, - count: usize, - ) -> DCByRootIds { - let ids = self.expect_data_columns_by_root_requests(for_block, count); - self.expect_empty_network(); - ids - } - - #[track_caller] - fn expect_block_process(&mut self, response_type: ResponseType) { - match response_type { - ResponseType::Block => self - .pop_received_processor_event(|ev| { - (ev.work_type() == beacon_processor::WorkType::RpcBlock).then_some(()) - }) - .unwrap_or_else(|e| panic!("Expected block work event: {e}")), - ResponseType::Blob => self - .pop_received_processor_event(|ev| { - (ev.work_type() == beacon_processor::WorkType::RpcBlobs).then_some(()) - }) - .unwrap_or_else(|e| panic!("Expected blobs work event: {e}")), - ResponseType::CustodyColumn => self - .pop_received_processor_event(|ev| { - (ev.work_type() == beacon_processor::WorkType::RpcCustodyColumn).then_some(()) - }) - .unwrap_or_else(|e| panic!("Expected column work event: {e}")), - } - } - - fn expect_rpc_custody_column_work_event(&mut self) { - self.pop_received_processor_event(|ev| { - if ev.work_type() == beacon_processor::WorkType::RpcCustodyColumn { - Some(()) - } else { - None - } - }) - .unwrap_or_else(|e| panic!("Expected RPC custody column work: {e}")) - } - - #[allow(dead_code)] - fn expect_no_work_event(&mut self) { - self.drain_processor_rx(); - assert!(self.network_rx_queue.is_empty()); - } - - fn expect_no_penalty_for(&mut self, peer_id: PeerId) { - self.drain_network_rx(); - let downscore_events = self - .network_rx_queue - .iter() - .filter_map(|ev| match ev { - NetworkMessage::ReportPeer { - peer_id: p_id, msg, .. - } if p_id == &peer_id => Some(msg), - _ => None, - }) - .collect::<Vec<_>>(); - if !downscore_events.is_empty() { - panic!("Some downscore events for {peer_id}: {downscore_events:?}"); - } - } - - #[track_caller] - fn expect_parent_chain_process(&mut self) { - match self.beacon_processor_rx.try_recv() { - Ok(work) => { - // Parent chain sends blocks one by one - assert_eq!(work.work_type(), beacon_processor::WorkType::RpcBlock); - } - other => panic!( - "Expected rpc_block from chain segment process, found {:?}", - other - ), - } - } - - #[track_caller] - pub fn expect_empty_network(&mut self) { + pub fn assert_empty_network(&mut self) { self.drain_network_rx(); if !self.network_rx_queue.is_empty() { let n = self.network_rx_queue.len(); @@ -993,115 +1469,52 @@ impl TestRig { } } - #[track_caller] - fn expect_empty_beacon_processor(&mut self) { - match self.beacon_processor_rx.try_recv() { - Err(mpsc::error::TryRecvError::Empty) => {} // ok - Ok(event) => panic!("expected empty beacon processor: {:?}", event), - other => panic!("unexpected err {:?}", other), - } - } - - #[track_caller] - pub fn expect_penalty(&mut self, peer_id: PeerId, expect_penalty_msg: &'static str) { - let penalty_msg = self - .pop_received_network_event(|ev| match ev { - NetworkMessage::ReportPeer { - peer_id: p_id, msg, .. - } if p_id == &peer_id => Some(msg.to_owned()), - _ => None, - }) - .unwrap_or_else(|_| { - panic!( - "Expected '{expect_penalty_msg}' penalty for peer {peer_id}: {:#?}", - self.network_rx_queue - ) - }); - assert_eq!( - penalty_msg, expect_penalty_msg, - "Unexpected penalty msg for {peer_id}" - ); - self.log(&format!("Found expected penalty {penalty_msg}")); - } - - pub fn block_with_parent_and_blobs( + async fn import_block_to_da_checker( &mut self, - parent_root: Hash256, - num_blobs: NumBlobs, - ) -> (SignedBeaconBlock<E>, Vec<BlobSidecar<E>>) { - let (mut block, mut blobs) = self.rand_block_and_blobs(num_blobs); - *block.message_mut().parent_root_mut() = parent_root; - blobs.iter_mut().for_each(|blob| { - blob.signed_block_header = block.signed_block_header(); - }); - (block, blobs) - } - - pub fn rand_blockchain(&mut self, depth: usize) -> Vec<Arc<SignedBeaconBlock<E>>> { - let mut blocks = Vec::<Arc<SignedBeaconBlock<E>>>::with_capacity(depth); - for slot in 0..depth { - let parent = blocks - .last() - .map(|b| b.canonical_root()) - .unwrap_or_else(Hash256::random); - let mut block = self.rand_block(); - *block.message_mut().parent_root_mut() = parent; - *block.message_mut().slot_mut() = slot.into(); - blocks.push(block.into()); - } - self.log(&format!( - "Blockchain dump {:#?}", - blocks - .iter() - .map(|b| format!( - "block {} {} parent {}", - b.slot(), - b.canonical_root(), - b.parent_root() - )) - .collect::<Vec<_>>() - )); - blocks - } - - fn insert_block_to_da_checker(&mut self, block: Arc<SignedBeaconBlock<E>>) { - let state = BeaconState::Base(BeaconStateBase::random_for_test(&mut self.rng)); - let parent_block = self.rand_block(); - let import_data = BlockImportData::<E>::__new_for_test( - block.canonical_root(), - state, - parent_block.into(), - ); - let payload_verification_outcome = PayloadVerificationOutcome { - payload_verification_status: PayloadVerificationStatus::Verified, - is_valid_merge_transition_block: false, - }; - let executed_block = - AvailabilityPendingExecutedBlock::new(block, import_data, payload_verification_outcome); - match self - .harness + block: Arc<SignedBeaconBlock<E>>, + ) -> AvailabilityProcessingStatus { + // Simulate importing block from another source. Don't use GossipVerified as it checks with + // the clock, which does not match the timestamp in the payload. + let block_root = block.canonical_root(); + let rpc_block = RpcBlock::BlockOnly { block_root, block }; + self.harness .chain - .data_availability_checker - .put_executed_block(executed_block) - .unwrap() - { - Availability::Available(_) => panic!("block removed from da_checker, available"), - Availability::MissingComponents(block_root) => { + .process_block( + block_root, + rpc_block, + NotifyExecutionLayer::Yes, + BlockImportSource::Gossip, + || Ok(()), + ) + .await + .expect("Error processing block") + } + + async fn insert_block_to_da_chain_and_assert_missing_componens( + &mut self, + block: Arc<SignedBeaconBlock<E>>, + ) { + match self.import_block_to_da_checker(block).await { + AvailabilityProcessingStatus::Imported(_) => { + panic!("block removed from da_checker, available") + } + AvailabilityProcessingStatus::MissingComponents(_, block_root) => { self.log(&format!("inserted block to da_checker {block_root:?}")) } - }; + } } - fn insert_blob_to_da_checker(&mut self, blob: BlobSidecar<E>) { + fn insert_blob_to_da_checker(&mut self, blob: Arc<BlobSidecar<E>>) { match self .harness .chain .data_availability_checker - .put_gossip_verified_blobs( + .put_kzg_verified_blobs( blob.block_root(), - std::iter::once(GossipVerifiedBlob::<_, Observe>::__assumed_valid( - blob.into(), - )), + std::iter::once( + KzgVerifiedBlob::new(blob, &self.harness.chain.kzg, Duration::new(0, 0)) + .expect("Invalid blob"), + ), ) .unwrap() { @@ -1112,7 +1525,11 @@ impl TestRig { }; } - fn insert_block_to_availability_cache(&mut self, block: Arc<SignedBeaconBlock<E>>) { + fn insert_block_to_da_checker_as_pre_execution(&mut self, block: Arc<SignedBeaconBlock<E>>) { + self.log(&format!( + "Inserting block to availability_cache as pre_execution_block {:?}", + block.canonical_root() + )); self.harness .chain .data_availability_checker @@ -1121,6 +1538,9 @@ impl TestRig { } 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 @@ -1132,19 +1552,38 @@ impl TestRig { }); } - fn simulate_block_gossip_processing_becomes_valid_missing_components( + async fn simulate_block_gossip_processing_becomes_valid( &mut self, block: Arc<SignedBeaconBlock<E>>, ) { let block_root = block.canonical_root(); - self.insert_block_to_da_checker(block); + 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 { + *requests_count + .entry(Into::<&'static str>::into(request)) + .or_default() += 1; + } + requests_count + } } #[test] @@ -1161,1558 +1600,803 @@ fn stable_rng() { ); } -#[test] -fn test_single_block_lookup_happy_path() { - let mut rig = TestRig::test_setup(); - let block = rig.rand_block(); - let peer_id = rig.new_connected_peer(); - let block_root = block.canonical_root(); - // Trigger the request - rig.trigger_unknown_block_from_attestation(block_root, peer_id); - let id = rig.expect_block_lookup_request(block_root); +macro_rules! run_lookups_tests_for_depths { + ($($depth:literal),+ $(,)?) => { + paste::paste! { + $( + #[tokio::test] + async fn [<happy_path_unknown_attestation_depth_ $depth>]() { + happy_path_unknown_attestation($depth).await; + } - // The peer provides the correct block, should not be penalized. Now the block should be sent - // for processing. - rig.single_lookup_block_response(id, peer_id, Some(block.into())); - rig.expect_empty_network(); - rig.expect_block_process(ResponseType::Block); + #[tokio::test] + async fn [<happy_path_unknown_block_parent_depth_ $depth>]() { + happy_path_unknown_block_parent($depth).await; + } - // The request should still be active. - assert_eq!(rig.active_single_lookups_count(), 1); + #[tokio::test] + async fn [<happy_path_unknown_data_parent_depth_ $depth>]() { + happy_path_unknown_data_parent($depth).await; + } - // Send the stream termination. Peer should have not been penalized, and the request removed - // after processing. - rig.single_lookup_block_response(id, peer_id, None); - rig.single_block_component_processed_imported(block_root); - rig.expect_empty_network(); - rig.expect_no_active_lookups(); + #[tokio::test] + async fn [<happy_path_multiple_triggers_depth_ $depth>]() { + happy_path_multiple_triggers($depth).await; + } + + #[tokio::test] + async fn [<bad_peer_empty_block_response_depth_ $depth>]() { + bad_peer_empty_block_response($depth).await; + } + + #[tokio::test] + async fn [<bad_peer_empty_data_response_depth_ $depth>]() { + bad_peer_empty_data_response($depth).await; + } + + #[tokio::test] + async fn [<bad_peer_too_few_data_response_depth_ $depth>]() { + bad_peer_too_few_data_response($depth).await; + } + + #[tokio::test] + async fn [<bad_peer_wrong_block_response_depth_ $depth>]() { + bad_peer_wrong_block_response($depth).await; + } + + #[tokio::test] + async fn [<bad_peer_wrong_data_response_depth_ $depth>]() { + bad_peer_wrong_data_response($depth).await; + } + + #[tokio::test] + async fn [<bad_peer_rpc_failure_depth_ $depth>]() { + bad_peer_rpc_failure($depth).await; + } + + #[tokio::test] + async fn [<too_many_download_failures_depth_ $depth>]() { + too_many_download_failures($depth).await; + } + + #[tokio::test] + async fn [<too_many_processing_failures_depth_ $depth>]() { + too_many_processing_failures($depth).await; + } + + #[tokio::test] + async fn [<peer_disconnected_then_rpc_error_depth_ $depth>]() { + peer_disconnected_then_rpc_error($depth).await; + } + )+ + } + }; } -// Tests that if a peer does not respond with a block, we downscore and retry the block only -#[test] -fn test_single_block_lookup_empty_response() { - let mut r = TestRig::test_setup(); +run_lookups_tests_for_depths!(1, 2); - let block = r.rand_block(); - let block_root = block.canonical_root(); - let peer_id = r.new_connected_peer(); - - // Trigger the request - r.trigger_unknown_block_from_attestation(block_root, peer_id); - let id = r.expect_block_lookup_request(block_root); - - // The peer does not have the block. It should be penalized. - r.single_lookup_block_response(id, peer_id, None); - r.expect_penalty(peer_id, "NotEnoughResponsesReturned"); - // it should be retried - let id = r.expect_block_lookup_request(block_root); - // Send the right block this time. - r.single_lookup_block_response(id, peer_id, Some(block.into())); - r.expect_block_process(ResponseType::Block); - r.single_block_component_processed_imported(block_root); - r.expect_no_active_lookups(); +/// Assert that lookup sync succeeds with the happy case +async fn happy_path_unknown_attestation(depth: usize) { + let mut r = TestRig::default(); + // We get attestation for a block descendant (depth) blocks of current head + r.build_chain_and_trigger_last_block(depth).await; + // Complete the request with good peer behaviour + r.simulate(SimulateConfig::happy_path()).await; + r.assert_successful_lookup_sync(); } -#[test] -fn test_single_block_lookup_wrong_response() { - let mut rig = TestRig::test_setup(); - - let block_hash = Hash256::random(); - let peer_id = rig.new_connected_peer(); - - // Trigger the request - rig.trigger_unknown_block_from_attestation(block_hash, peer_id); - let id = rig.expect_block_lookup_request(block_hash); - - // Peer sends something else. It should be penalized. - let bad_block = rig.rand_block(); - rig.single_lookup_block_response(id, peer_id, Some(bad_block.into())); - rig.expect_penalty(peer_id, "UnrequestedBlockRoot"); - rig.expect_block_lookup_request(block_hash); // should be retried - - // Send the stream termination. This should not produce an additional penalty. - rig.single_lookup_block_response(id, peer_id, None); - rig.expect_empty_network(); +async fn happy_path_unknown_block_parent(depth: usize) { + let mut r = TestRig::default(); + 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: + // - 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 + // - 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(); + } } -#[test] -fn test_single_block_lookup_failure() { - let mut rig = TestRig::test_setup(); - - let block_hash = Hash256::random(); - let peer_id = rig.new_connected_peer(); - - // Trigger the request - rig.trigger_unknown_block_from_attestation(block_hash, peer_id); - let id = rig.expect_block_lookup_request(block_hash); - - // The request fails. RPC failures are handled elsewhere so we should not penalize the peer. - rig.single_lookup_failed(id, peer_id, RPCError::UnsupportedProtocol); - rig.expect_block_lookup_request(block_hash); - rig.expect_empty_network(); +/// Assert that sync completes from a GossipUnknownParentBlob / UnknownDataColumnParent +async fn happy_path_unknown_data_parent(depth: usize) { + let Some(mut r) = TestRig::new_after_deneb() else { + return; + }; + r.build_chain(depth).await; + if r.is_after_fulu() { + r.trigger_with_last_unknown_data_column_parent(); + } else if r.is_after_deneb() { + r.trigger_with_last_unknown_blob_parent(); + } + r.simulate(SimulateConfig::happy_path()).await; + r.assert_successful_lookup_sync_parent_trigger(); } -#[test] -fn test_single_block_lookup_peer_disconnected_then_rpc_error() { - let mut rig = TestRig::test_setup(); +/// Assert that multiple trigger types don't create extra lookups +async fn happy_path_multiple_triggers(depth: usize) { + let mut r = TestRig::default(); + // + 1, because the unknown parent trigger needs two new blocks + r.build_chain(depth + 1).await; + r.trigger_with_last_block(); + r.trigger_with_last_block(); + r.trigger_with_last_unknown_block_parent(); + r.trigger_with_last_unknown_block_parent(); + if r.is_after_fulu() { + r.trigger_with_last_unknown_data_column_parent(); + } else if r.is_after_deneb() { + r.trigger_with_last_unknown_blob_parent(); + } + r.simulate(SimulateConfig::happy_path()).await; + assert_eq!(r.created_lookups(), depth + 1, "Don't create extra lookups"); + r.assert_successful_lookup_sync(); +} - let block_hash = Hash256::random(); - let peer_id = rig.new_connected_peer(); +// Test bad behaviour of peers - // Trigger the request. - rig.trigger_unknown_block_from_attestation(block_hash, peer_id); - let id = rig.expect_block_lookup_request(block_hash); +/// Assert that if peer responds with no blocks, we downscore, and retry the same lookup +async fn bad_peer_empty_block_response(depth: usize) { + let mut r = TestRig::default(); + r.build_chain_and_trigger_last_block(depth).await; + // Simulate that peer returns empty response once, then good behaviour + r.simulate(SimulateConfig::new().return_no_blocks_once()) + .await; + // We register a penalty, retry and complete sync successfully + r.assert_penalties(&["NotEnoughResponsesReturned"]); + r.assert_successful_lookup_sync(); + // TODO(tree-sync) For post-deneb assert that the blobs are not re-fetched + // 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 +async fn bad_peer_empty_data_response(depth: usize) { + let Some(mut r) = TestRig::new_after_deneb() else { + return; + }; + r.build_chain_and_trigger_last_block(depth).await; + r.simulate(SimulateConfig::new().return_no_data_once()) + .await; + // We register a penalty, retry and complete sync successfully + r.assert_penalties(&["NotEnoughResponsesReturned"]); + r.assert_successful_lookup_sync(); + // 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 +async fn bad_peer_too_few_data_response(depth: usize) { + let Some(mut r) = TestRig::new_after_deneb() else { + return; + }; + r.build_chain_and_trigger_last_block(depth).await; + r.simulate(SimulateConfig::new().return_too_few_data_once()) + .await; + // We register a penalty, retry and complete sync successfully + r.assert_penalties(&["NotEnoughResponsesReturned"]); + r.assert_successful_lookup_sync(); + // TODO(tree-sync) Assert that a single lookup is created (no drops) +} + +/// Assert that if peer responds with bad blocks, we downscore, and retry the same lookup +async fn bad_peer_wrong_block_response(depth: usize) { + let mut r = TestRig::default(); + r.build_chain_and_trigger_last_block(depth).await; + r.simulate(SimulateConfig::new().return_wrong_blocks_once()) + .await; + r.assert_penalties(&["UnrequestedBlockRoot"]); + r.assert_successful_lookup_sync(); + + // 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 +async fn bad_peer_wrong_data_response(depth: usize) { + let Some(mut r) = TestRig::new_after_deneb() else { + return; + }; + 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"]); + r.assert_successful_lookup_sync(); + // TODO(tree-sync) Assert that a single lookup is created (no drops) +} + +/// Assert that on network error, we DON'T downscore, and retry the same lookup +async fn bad_peer_rpc_failure(depth: usize) { + let mut r = TestRig::default(); + r.build_chain_and_trigger_last_block(depth).await; + r.simulate(SimulateConfig::new().return_rpc_error(RPCError::UnsupportedProtocol)) + .await; + r.assert_no_penalties(); + r.assert_successful_lookup_sync(); +} + +// Test retry logic + +/// Assert that on too many download failures the lookup fails, but we can still sync +async fn too_many_download_failures(depth: usize) { + let mut r = TestRig::default(); + r.build_chain_and_trigger_last_block(depth).await; + // Simulate that a peer always returns empty + r.simulate(SimulateConfig::new().return_no_blocks_always()) + .await; + // We register multiple penalties, the lookup fails and sync does not progress + r.assert_penalties_of_type("NotEnoughResponsesReturned"); + r.assert_failed_lookup_sync(); + + // Trigger sync again for same block, and complete successfully. + // Asserts that the lookup is not on a blacklist + r.capture_metrics_baseline(); + r.trigger_with_last_block(); + r.simulate(SimulateConfig::happy_path()).await; + r.assert_successful_lookup_sync(); +} + +/// Assert that on too many processing failures the lookup fails, but we can still sync +async fn too_many_processing_failures(depth: usize) { + let mut r = TestRig::default(); + r.build_chain_and_trigger_last_block(depth).await; + // Simulate that a peer always returns empty + r.simulate( + SimulateConfig::new() + .with_process_result(|| BlockProcessingResult::Err(BlockError::BlockSlotLimitReached)), + ) + .await; + // We register multiple penalties, the lookup fails and sync does not progress + r.assert_penalties_of_type("lookup_block_processing_failure"); + r.assert_failed_lookup_sync(); + + // Trigger sync again for same block, and complete successfully. + // Asserts that the lookup is not on a blacklist + r.capture_metrics_baseline(); + r.trigger_with_last_block(); + r.simulate(SimulateConfig::happy_path()).await; + r.assert_successful_lookup_sync(); +} + +#[tokio::test] +/// Assert that multiple trigger types don't create extra lookups +async fn unknown_parent_does_not_add_peers_to_itself() { + let Some(mut r) = TestRig::new_after_deneb() else { + return; + }; + // 2, because the unknown parent trigger needs two new blocks + r.build_chain(2).await; + r.trigger_with_last_unknown_block_parent(); + r.trigger_with_last_unknown_block_parent(); + if r.is_after_fulu() { + r.trigger_with_last_unknown_data_column_parent(); + } else if r.is_after_deneb() { + r.trigger_with_last_unknown_blob_parent(); + } + r.simulate(SimulateConfig::happy_path()).await; + r.assert_peers_at_lookup_of_slot(2, 0); + r.assert_peers_at_lookup_of_slot(1, 3); + 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: + // - before fulu the block is cached, but we can't fetch blobs so it's stuck + // - 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_fulu() { + r.assert_successful_lookup_sync() + } else { + r.assert_successful_lookup_sync_parent_trigger(); + } +} + +#[tokio::test] +/// Assert that if the beacon processor returns Ignored, the lookup is 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; + // The block was not actually imported + r.assert_head_slot(0); + 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"); +} + +#[tokio::test] +/// 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(); + r.build_chain_and_trigger_last_block(1).await; + // Send a DuplicateFullyImported response, the lookup should complete successfully + r.simulate(SimulateConfig::new().with_process_result(|| { + BlockProcessingResult::Err(BlockError::DuplicateFullyImported(Hash256::ZERO)) + })) + .await; + // The block was not actually imported + r.assert_head_slot(0); + r.assert_successful_lookup_sync(); +} + +/// Assert that when peers disconnect the lookups are not dropped (kept with zero peers) +async fn peer_disconnected_then_rpc_error(depth: usize) { + let mut r = TestRig::default(); + r.build_chain_and_trigger_last_block(depth).await; + r.assert_single_lookups_count(1); // The peer disconnect event reaches sync before the rpc error. - rig.peer_disconnected(peer_id); + r.disconnect_all_peers(); // The lookup is not removed as it can still potentially make progress. - rig.assert_single_lookups_count(1); - // The request fails. - rig.single_lookup_failed(id, peer_id, RPCError::Disconnected); - rig.expect_block_lookup_request(block_hash); - // The request should be removed from the network context on disconnection. - rig.expect_empty_network(); + r.assert_single_lookups_count(1); + r.simulate(SimulateConfig::new().return_rpc_error(RPCError::Disconnected)) + .await; + + // Regardless of depth, only the initial lookup is created, because the peer disconnects before + // being able to download the block + assert_eq!(r.created_lookups(), 1, "no created lookups"); + assert_eq!(r.completed_lookups(), 0, "some completed lookups"); + assert_eq!(r.dropped_lookups(), 0, "some dropped lookups"); + r.assert_empty_network(); + r.assert_single_lookups_count(1); } -#[test] -fn test_single_block_lookup_becomes_parent_request() { - let mut rig = TestRig::test_setup(); - - let block = Arc::new(rig.rand_block()); - let block_root = block.canonical_root(); - let parent_root = block.parent_root(); - let peer_id = rig.new_connected_peer(); - - // Trigger the request - rig.trigger_unknown_block_from_attestation(block.canonical_root(), peer_id); - let id = rig.expect_block_parent_request(block_root); - - // The peer provides the correct block, should not be penalized. Now the block should be sent - // for processing. - rig.single_lookup_block_response(id, peer_id, Some(block.clone())); - rig.expect_empty_network(); - rig.expect_block_process(ResponseType::Block); - - // The request should still be active. - assert_eq!(rig.active_single_lookups_count(), 1); - - // Send the stream termination. Peer should have not been penalized, and the request moved to a - // parent request after processing. - rig.single_block_component_processed( - id.lookup_id, - BlockProcessingResult::Err(BlockError::ParentUnknown { - parent_root: block.parent_root(), - }), - ); - assert_eq!(rig.active_single_lookups_count(), 2); // 2 = current + parent - rig.expect_block_parent_request(parent_root); - rig.expect_empty_network(); - assert_eq!(rig.active_parent_lookups_count(), 1); -} - -#[test] -fn test_parent_lookup_happy_path() { - let mut rig = TestRig::test_setup(); - - let (parent, block, parent_root, block_root) = rig.rand_block_and_parent(); - let peer_id = rig.new_connected_peer(); - - // Trigger the request - rig.trigger_unknown_parent_block(peer_id, block.into()); - let id = rig.expect_block_parent_request(parent_root); - - // Peer sends the right block, it should be sent for processing. Peer should not be penalized. - rig.parent_lookup_block_response(id, peer_id, Some(parent.into())); - // No request of blobs because the block has not data - rig.expect_empty_network(); - rig.expect_block_process(ResponseType::Block); - rig.expect_empty_network(); - - // Add peer to child lookup to prevent it being dropped - rig.trigger_unknown_block_from_attestation(block_root, peer_id); - // Processing succeeds, now the rest of the chain should be sent for processing. - rig.parent_block_processed( - block_root, - BlockError::DuplicateFullyImported(block_root).into(), - ); - rig.expect_parent_chain_process(); - rig.parent_chain_processed_success(block_root, &[]); - rig.expect_no_active_lookups_empty_network(); -} - -#[test] -fn test_parent_lookup_wrong_response() { - let mut rig = TestRig::test_setup(); - - let (parent, block, parent_root, block_root) = rig.rand_block_and_parent(); - let peer_id = rig.new_connected_peer(); - - // Trigger the request - rig.trigger_unknown_parent_block(peer_id, block.into()); - let id1 = rig.expect_block_parent_request(parent_root); - - // Peer sends the wrong block, peer should be penalized and the block re-requested. - let bad_block = rig.rand_block(); - rig.parent_lookup_block_response(id1, peer_id, Some(bad_block.into())); - rig.expect_penalty(peer_id, "UnrequestedBlockRoot"); - let id2 = rig.expect_block_parent_request(parent_root); - - // Send the stream termination for the first request. This should not produce extra penalties. - rig.parent_lookup_block_response(id1, peer_id, None); - rig.expect_empty_network(); - - // Send the right block this time. - rig.parent_lookup_block_response(id2, peer_id, Some(parent.into())); - rig.expect_block_process(ResponseType::Block); - - // Add peer to child lookup to prevent it being dropped - rig.trigger_unknown_block_from_attestation(block_root, peer_id); - // Processing succeeds, now the rest of the chain should be sent for processing. - rig.parent_block_processed_imported(block_root); - rig.expect_parent_chain_process(); - rig.parent_chain_processed_success(block_root, &[]); - rig.expect_no_active_lookups_empty_network(); -} - -#[test] -fn test_parent_lookup_rpc_failure() { - let mut rig = TestRig::test_setup(); - - let (parent, block, parent_root, block_root) = rig.rand_block_and_parent(); - let peer_id = rig.new_connected_peer(); - - // Trigger the request - rig.trigger_unknown_parent_block(peer_id, block.into()); - let id = rig.expect_block_parent_request(parent_root); - - // The request fails. It should be tried again. - rig.parent_lookup_failed_unavailable(id, peer_id); - let id = rig.expect_block_parent_request(parent_root); - - // Send the right block this time. - rig.parent_lookup_block_response(id, peer_id, Some(parent.into())); - rig.expect_block_process(ResponseType::Block); - - // Add peer to child lookup to prevent it being dropped - rig.trigger_unknown_block_from_attestation(block_root, peer_id); - // Processing succeeds, now the rest of the chain should be sent for processing. - rig.parent_block_processed_imported(block_root); - rig.expect_parent_chain_process(); - rig.parent_chain_processed_success(block_root, &[]); - rig.expect_no_active_lookups_empty_network(); -} - -#[test] -fn test_parent_lookup_too_many_attempts() { - let mut rig = TestRig::test_setup(); - - let block = rig.rand_block(); - let parent_root = block.parent_root(); - let peer_id = rig.new_connected_peer(); - - // Trigger the request - rig.trigger_unknown_parent_block(peer_id, block.into()); - for i in 1..=PARENT_FAIL_TOLERANCE { - let id = rig.expect_block_parent_request(parent_root); - // Blobs are only requested in the first iteration as this test only retries blocks - - if i % 2 == 0 { - // make sure every error is accounted for - // The request fails. It should be tried again. - rig.parent_lookup_failed_unavailable(id, peer_id); - } else { - // Send a bad block this time. It should be tried again. - let bad_block = rig.rand_block(); - rig.parent_lookup_block_response(id, peer_id, Some(bad_block.into())); - // Send the stream termination - - // Note, previously we would send the same lookup id with a stream terminator, - // we'd ignore it because we'd intrepret it as an unrequested response, since - // we already got one response for the block. I'm not sure what the intent is - // for having this stream terminator line in this test at all. Receiving an invalid - // block and a stream terminator with the same Id now results in two failed attempts, - // I'm unsure if this is how it should behave? - // - rig.parent_lookup_block_response(id, peer_id, None); - rig.expect_penalty(peer_id, "UnrequestedBlockRoot"); - } +#[tokio::test] +/// Assert that when creating multiple lookups their parent-child relation is discovered and we add +/// peers recursively from child to parent. +async fn lookups_form_chain() { + let depth = 5; + let mut r = TestRig::default(); + r.build_chain(depth).await; + for slot in (1..=depth).rev() { + r.trigger_with_block_at_slot(slot as u64); } + // TODO(tree-sync): Assert that there are `depth` disjoint chains + r.simulate(SimulateConfig::happy_path()).await; + r.assert_successful_lookup_sync(); - rig.expect_no_active_lookups_empty_network(); + // Assert that the peers are added to ancestor lookups, + // - The lookup with max slot has 1 peer + // - The lookup with min slot has all the peers + for slot in 1..=(depth as u64) { + let lookup = r.lookup_by_root(r.block_root_at_slot(slot)); + assert_eq!( + lookup.seen_peers.len(), + 1 + depth - slot as usize, + "Unexpected peer count for lookup at slot {slot}" + ); + } } -#[test] -fn test_parent_lookup_too_many_download_attempts_no_blacklist() { - let mut rig = TestRig::test_setup(); +#[tokio::test] +/// 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(); + r.build_chain(PARENT_DEPTH_TOLERANCE + 1).await; + r.trigger_with_last_block(); + r.simulate(SimulateConfig::happy_path()).await; - let (parent, block, parent_root, block_root) = rig.rand_block_and_parent(); - let peer_id = rig.new_connected_peer(); - - // Trigger the request - rig.trigger_unknown_parent_block(peer_id, block.into()); - for i in 1..=PARENT_FAIL_TOLERANCE { - rig.assert_not_ignored_chain(block_root); - let id = rig.expect_block_parent_request(parent_root); - if i % 2 != 0 { - // The request fails. It should be tried again. - rig.parent_lookup_failed_unavailable(id, peer_id); - } else { - // Send a bad block this time. It should be tried again. - let bad_block = rig.rand_block(); - rig.parent_lookup_block_response(id, peer_id, Some(bad_block.into())); - rig.expect_penalty(peer_id, "UnrequestedBlockRoot"); - } - } - - rig.assert_not_ignored_chain(block_root); - rig.assert_not_ignored_chain(parent.canonical_root()); - rig.expect_no_active_lookups_empty_network(); -} - -#[test] -fn test_parent_lookup_too_many_processing_attempts_must_blacklist() { - const PROCESSING_FAILURES: u8 = PARENT_FAIL_TOLERANCE / 2 + 1; - let mut rig = TestRig::test_setup(); - let (parent, block, parent_root, block_root) = rig.rand_block_and_parent(); - let peer_id = rig.new_connected_peer(); - - // Trigger the request - rig.trigger_unknown_parent_block(peer_id, block.into()); - - rig.log("Fail downloading the block"); - for _ in 0..(PARENT_FAIL_TOLERANCE - PROCESSING_FAILURES) { - let id = rig.expect_block_parent_request(parent_root); - // The request fails. It should be tried again. - rig.parent_lookup_failed_unavailable(id, peer_id); - } - - rig.log("Now fail processing a block in the parent request"); - for _ in 0..PROCESSING_FAILURES { - let id = rig.expect_block_parent_request(parent_root); - // Blobs are only requested in the previous first iteration as this test only retries blocks - rig.assert_not_ignored_chain(block_root); - // send the right parent but fail processing - rig.parent_lookup_block_response(id, peer_id, Some(parent.clone().into())); - rig.parent_block_processed(block_root, BlockError::BlockSlotLimitReached.into()); - rig.parent_lookup_block_response(id, peer_id, None); - rig.expect_penalty(peer_id, "lookup_block_processing_failure"); - } - - rig.assert_not_ignored_chain(block_root); - rig.expect_no_active_lookups_empty_network(); -} - -#[test] -fn test_parent_lookup_too_deep_grow_ancestor() { - let mut rig = TestRig::test_setup(); - let mut blocks = rig.rand_blockchain(PARENT_DEPTH_TOLERANCE); - - let peer_id = rig.new_connected_peer(); - let trigger_block = blocks.pop().unwrap(); - let chain_hash = trigger_block.canonical_root(); - rig.trigger_unknown_parent_block(peer_id, trigger_block); - - for block in blocks.into_iter().rev() { - let id = rig.expect_block_parent_request(block.canonical_root()); - // the block - rig.parent_lookup_block_response(id, peer_id, Some(block.clone())); - // the stream termination - rig.parent_lookup_block_response(id, peer_id, None); - // the processing request - rig.expect_block_process(ResponseType::Block); - // the processing result - rig.parent_block_processed( - chain_hash, - BlockProcessingResult::Err(BlockError::ParentUnknown { - parent_root: block.parent_root(), - }), - ) - } - - // Should create a new syncing chain - rig.drain_sync_rx(); - assert_eq!( - rig.active_range_sync_chain(), - ( - RangeSyncType::Head, - Slot::new(0), - Slot::new(PARENT_DEPTH_TOLERANCE as u64 - 1) - ) - ); + r.assert_head_slot(PARENT_DEPTH_TOLERANCE as u64 + 1); + r.assert_no_penalties(); // Should not penalize peer, but network is not clear because of the blocks_by_range requests - rig.expect_no_penalty_for(peer_id); - rig.assert_ignored_chain(chain_hash); + // r.assert_ignored_chain(chain_hash); + // + // Assert that chain is in failed chains + // Assert that there were 0 lookups completed, 33 dropped + // Assert that there were 1 range sync chains + // Bound resources: + // - Limit amount of requests + // - Limit the types of sync used + assert_eq!(r.completed_lookups(), 0, "no completed lookups"); + assert_eq!( + r.dropped_lookups(), + PARENT_DEPTH_TOLERANCE, + "All lookups dropped" + ); + r.assert_successful_range_sync(); +} + +#[tokio::test] +async fn test_parent_lookup_too_deep_grow_ancestor_zero() { + let mut r = TestRig::default(); + r.build_chain(PARENT_DEPTH_TOLERANCE).await; + r.trigger_with_last_block(); + r.simulate(SimulateConfig::happy_path()).await; + + r.assert_head_slot(PARENT_DEPTH_TOLERANCE as u64); + r.assert_no_penalties(); + assert_eq!( + r.completed_lookups(), + PARENT_DEPTH_TOLERANCE, + "completed all lookups" + ); + assert_eq!(r.dropped_lookups(), 0, "no dropped lookups"); } // Regression test for https://github.com/sigp/lighthouse/pull/7118 // 8042 UPDATE: block was previously added to the failed_chains cache, now it's inserted into the -// ignored chains cache. The regression test still applies as the chaild lookup is not created -#[test] -fn test_child_lookup_not_created_for_ignored_chain_parent_after_processing() { - // GIVEN: A parent chain longer than PARENT_DEPTH_TOLERANCE. - let mut rig = TestRig::test_setup(); - let mut blocks = rig.rand_blockchain(PARENT_DEPTH_TOLERANCE + 1); - let peer_id = rig.new_connected_peer(); - - // The child of the trigger block to be used to extend the chain. - let trigger_block_child = blocks.pop().unwrap(); - // The trigger block that starts the lookup. - let trigger_block = blocks.pop().unwrap(); - let tip_root = trigger_block.canonical_root(); - - // Trigger the initial unknown parent block for the tip. - rig.trigger_unknown_parent_block(peer_id, trigger_block.clone()); - - // Simulate the lookup chain building up via `ParentUnknown` errors. - for block in blocks.into_iter().rev() { - let id = rig.expect_block_parent_request(block.canonical_root()); - rig.parent_lookup_block_response(id, peer_id, Some(block.clone())); - rig.parent_lookup_block_response(id, peer_id, None); - rig.expect_block_process(ResponseType::Block); - rig.parent_block_processed( - tip_root, - BlockProcessingResult::Err(BlockError::ParentUnknown { - parent_root: block.parent_root(), - }), - ); - } +// ignored chains cache. The regression test still applies as the child lookup is not created +#[tokio::test] +async fn test_child_lookup_not_created_for_ignored_chain_parent_after_processing() { + let mut r = TestRig::default(); + let depth = PARENT_DEPTH_TOLERANCE + 1; + r.build_chain(depth + 1).await; + r.trigger_with_block_at_slot(depth as u64); + r.simulate(SimulateConfig::new().no_range_sync()).await; // At this point, the chain should have been deemed too deep and pruned. // The tip root should have been inserted into ignored chains. - rig.assert_ignored_chain(tip_root); - rig.expect_no_penalty_for(peer_id); + // Ensure no blocks have been synced + r.assert_head_slot(0); + r.assert_no_active_lookups(); + r.assert_no_penalties(); + r.assert_ignored_chain(r.block_at_slot(depth as u64).canonical_root()); // WHEN: Trigger the extending block that points to the tip. - let trigger_block_child_root = trigger_block_child.canonical_root(); - rig.trigger_unknown_block_from_attestation(trigger_block_child_root, peer_id); - let id = rig.expect_block_lookup_request(trigger_block_child_root); - rig.single_lookup_block_response(id, peer_id, Some(trigger_block_child.clone())); - rig.single_lookup_block_response(id, peer_id, None); - rig.expect_block_process(ResponseType::Block); - rig.single_block_component_processed( - id.lookup_id, - BlockProcessingResult::Err(BlockError::ParentUnknown { - parent_root: tip_root, - }), - ); - + let peer = r.new_connected_peer(); + r.trigger_unknown_parent_block(peer, r.block_at_slot(depth as u64 + 1)); // THEN: The extending block should not create a lookup because the tip was inserted into // ignored chains. - rig.expect_no_active_lookups(); - rig.expect_no_penalty_for(peer_id); - rig.expect_empty_network(); + r.assert_no_active_lookups(); + r.assert_no_penalties(); + r.assert_empty_network(); } -#[test] -fn test_parent_lookup_too_deep_grow_tip() { - let mut rig = TestRig::test_setup(); - let blocks = rig.rand_blockchain(PARENT_DEPTH_TOLERANCE - 1); - let peer_id = rig.new_connected_peer(); - let tip = blocks.last().unwrap().clone(); - - for block in blocks.into_iter() { - let block_root = block.canonical_root(); - rig.trigger_unknown_block_from_attestation(block_root, peer_id); - let id = rig.expect_block_parent_request(block_root); - rig.single_lookup_block_response(id, peer_id, Some(block.clone())); - rig.single_lookup_block_response(id, peer_id, None); - rig.expect_block_process(ResponseType::Block); - rig.single_block_component_processed( - id.lookup_id, - BlockError::ParentUnknown { - parent_root: block.parent_root(), - } - .into(), - ); +#[tokio::test] +/// Assert that if a lookup chain (by appending tips) is too long we drop it +async fn test_parent_lookup_too_deep_grow_tip() { + let depth = PARENT_DEPTH_TOLERANCE + 1; + let mut r = TestRig::default(); + r.build_chain(depth).await; + for slot in (1..=depth).rev() { + r.trigger_with_block_at_slot(slot as u64); } + r.simulate(SimulateConfig::happy_path()).await; - // Should create a new syncing chain - rig.drain_sync_rx(); + // Even if the chain is longer than `PARENT_DEPTH_TOLERANCE` because the lookups are created all + // at once they chain by sections and it's possible that the oldest ancestors start processing + // before the full chain is connected. + assert!(r.created_lookups() > 0, "no created lookups"); assert_eq!( - rig.active_range_sync_chain(), - ( - RangeSyncType::Head, - Slot::new(0), - Slot::new(PARENT_DEPTH_TOLERANCE as u64 - 2) - ) + r.completed_lookups(), + r.created_lookups(), + "not all completed lookups" ); + assert_eq!(r.dropped_lookups(), 0, "some dropped lookups"); + r.assert_successful_lookup_sync(); // Should not penalize peer, but network is not clear because of the blocks_by_range requests - rig.expect_no_penalty_for(peer_id); - rig.assert_ignored_chain(tip.canonical_root()); + r.assert_no_penalties(); } -#[test] -fn test_lookup_peer_disconnected_no_peers_left_while_request() { - let mut rig = TestRig::test_setup(); - let peer_id = rig.new_connected_peer(); - let trigger_block = rig.rand_block(); - rig.trigger_unknown_parent_block(peer_id, trigger_block.into()); - rig.peer_disconnected(peer_id); - rig.rpc_error_all_active_requests(peer_id); - // Erroring all rpc requests and disconnecting the peer shouldn't remove the requests - // from the lookups map as they can still progress. - rig.assert_single_lookups_count(2); -} - -#[test] -fn test_lookup_disconnection_peer_left() { - let mut rig = TestRig::test_setup(); - let peer_ids = (0..2).map(|_| rig.new_connected_peer()).collect::<Vec<_>>(); - let disconnecting_peer = *peer_ids.first().unwrap(); - let block_root = Hash256::random(); - // lookup should have two peers associated with the same block - for peer_id in peer_ids.iter() { - rig.trigger_unknown_block_from_attestation(block_root, *peer_id); - } - // Disconnect the first peer only, which is the one handling the request - rig.peer_disconnected(disconnecting_peer); - rig.rpc_error_all_active_requests(disconnecting_peer); - rig.assert_single_lookups_count(1); -} - -#[test] -fn test_lookup_add_peers_to_parent() { - let mut r = TestRig::test_setup(); - let peer_id_1 = r.new_connected_peer(); - let peer_id_2 = r.new_connected_peer(); - let blocks = r.rand_blockchain(5); - let last_block_root = blocks.last().unwrap().canonical_root(); - // Create a chain of lookups - for block in &blocks { - r.trigger_unknown_parent_block(peer_id_1, block.clone()); - } - r.trigger_unknown_block_from_attestation(last_block_root, peer_id_2); - for block in blocks.iter().take(blocks.len() - 1) { - // Parent has the original unknown parent event peer + new peer - r.assert_lookup_peers(block.canonical_root(), vec![peer_id_1, peer_id_2]); - } - // Child lookup only has the unknown attestation peer - r.assert_lookup_peers(last_block_root, vec![peer_id_2]); -} - -#[test] -fn test_skip_creating_ignored_parent_lookup() { - let mut rig = TestRig::test_setup(); - let (_, block, parent_root, _) = rig.rand_block_and_parent(); - let peer_id = rig.new_connected_peer(); - rig.insert_ignored_chain(parent_root); - rig.trigger_unknown_parent_block(peer_id, block.into()); - rig.expect_no_penalty_for(peer_id); +#[tokio::test] +async fn test_skip_creating_ignored_parent_lookup() { + let mut r = TestRig::default(); + r.build_chain(2).await; + r.insert_ignored_chain(r.block_root_at_slot(1)); + r.trigger_with_last_block(); + r.simulate(SimulateConfig::happy_path()).await; + r.assert_no_penalties(); // Both current and parent lookup should not be created - rig.expect_no_active_lookups(); + r.assert_no_active_lookups(); } -#[test] -fn test_single_block_lookup_ignored_response() { - let mut rig = TestRig::test_setup(); +#[tokio::test] +/// Assert that if the oldest block in a chain is already imported (DuplicateFullyImported), +/// the remaining blocks in the chain are still processed successfully. This tests a race +/// condition where a block gets imported elsewhere while the lookup is processing. +/// +/// The processing sequence is: +/// - Block 3: UnknownParent (needs block 2) +/// - Block 2: UnknownParent (needs block 1) +/// - Block 1: About to be processed, but gets imported via gossip (race condition) +/// - Block 1: DuplicateFullyImported (already in chain from race) +/// - Block 2: Import ok (parent block 1 is available) +/// - Block 3: Import ok (parent block 2 is available) +async fn test_same_chain_race_condition() { + let mut r = TestRig::default(); + r.build_chain(3).await; - let block = rig.rand_block(); - let peer_id = rig.new_connected_peer(); + let block_1_root = r.block_root_at_slot(1); - // Trigger the request - rig.trigger_unknown_block_from_attestation(block.canonical_root(), peer_id); - let id = rig.expect_block_lookup_request(block.canonical_root()); + // Trigger a lookup with block 3. This creates a parent lookup chain that will + // request blocks 3 → 2 → 1. + r.trigger_with_block_at_slot(3); - // The peer provides the correct block, should not be penalized. Now the block should be sent - // for processing. - rig.single_lookup_block_response(id, peer_id, Some(block.into())); - rig.expect_empty_network(); - rig.expect_block_process(ResponseType::Block); + // Configure simulate to import block 1 right before it's processed by the lookup. + // This simulates the race condition where block 1 arrives via gossip at the same + // time the lookup is trying to process it. + r.simulate(SimulateConfig::new().with_import_block_before_process(block_1_root)) + .await; - // The request should still be active. - assert_eq!(rig.active_single_lookups_count(), 1); - - // Send the stream termination. Peer should have not been penalized, and the request removed - // after processing. - rig.single_lookup_block_response(id, peer_id, None); - // Send an Ignored response, the request should be dropped - rig.single_block_component_processed(id.lookup_id, BlockProcessingResult::Ignored); - rig.expect_no_active_lookups_empty_network(); + // The chain should complete successfully with head at slot 3, proving that + // the lookup correctly handled the DuplicateFullyImported for block 1 and + // continued processing blocks 2 and 3. + r.assert_head_slot(3); + r.assert_successful_lookup_sync(); } -#[test] -fn test_parent_lookup_ignored_response() { - let mut rig = TestRig::test_setup(); - - let (parent, block, parent_root, block_root) = rig.rand_block_and_parent(); - let peer_id = rig.new_connected_peer(); - - // Trigger the request - rig.trigger_unknown_parent_block(peer_id, block.clone().into()); - let id = rig.expect_block_parent_request(parent_root); - // Note: single block lookup for current `block` does not trigger any request because it does - // not have blobs, and the block is already cached - - // Peer sends the right block, it should be sent for processing. Peer should not be penalized. - rig.parent_lookup_block_response(id, peer_id, Some(parent.into())); - rig.expect_block_process(ResponseType::Block); - rig.expect_empty_network(); - - // Return an Ignored result. The request should be dropped - rig.parent_block_processed(block_root, BlockProcessingResult::Ignored); - rig.expect_empty_network(); - rig.expect_no_active_lookups(); -} - -/// This is a regression test. -#[test] -fn test_same_chain_race_condition() { - let mut rig = TestRig::test_setup(); - - // if we use one or two blocks it will match on the hash or the parent hash, so make a longer - // chain. - let depth = 4; - let mut blocks = rig.rand_blockchain(depth); - let peer_id = rig.new_connected_peer(); - let trigger_block = blocks.pop().unwrap(); - let chain_hash = trigger_block.canonical_root(); - rig.trigger_unknown_parent_block(peer_id, trigger_block.clone()); - - for (i, block) in blocks.clone().into_iter().rev().enumerate() { - let id = rig.expect_block_parent_request(block.canonical_root()); - // the block - rig.parent_lookup_block_response(id, peer_id, Some(block.clone())); - // the stream termination - rig.parent_lookup_block_response(id, peer_id, None); - // the processing request - rig.expect_block_process(ResponseType::Block); - // the processing result - if i + 2 == depth { - rig.log(&format!("Block {i} was removed and is already known")); - rig.parent_block_processed( - chain_hash, - BlockError::DuplicateFullyImported(block.canonical_root()).into(), - ) - } else { - rig.log(&format!("Block {i} ParentUnknown")); - rig.parent_block_processed( - chain_hash, - BlockProcessingResult::Err(BlockError::ParentUnknown { - parent_root: block.parent_root(), - }), - ) - } - } - - // Try to get this block again while the chain is being processed. We should not request it again. - let peer_id = rig.new_connected_peer(); - rig.trigger_unknown_parent_block(peer_id, trigger_block.clone()); - rig.expect_empty_network(); - - // Add a peer to the tip child lookup which has zero peers - rig.trigger_unknown_block_from_attestation(trigger_block.canonical_root(), peer_id); - - rig.log("Processing succeeds, now the rest of the chain should be sent for processing."); - for block in blocks.iter().skip(1).chain(&[trigger_block]) { - rig.expect_parent_chain_process(); - rig.single_block_component_processed_imported(block.canonical_root()); - } - rig.expect_no_active_lookups_empty_network(); -} - -#[test] -fn block_in_da_checker_skips_download() { - let Some(mut r) = TestRig::test_setup_after_deneb_before_fulu() else { +#[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 { return; }; - let (block, blobs) = r.rand_block_and_blobs(NumBlobs::Number(1)); - let block_root = block.canonical_root(); - let peer_id = r.new_connected_peer(); - r.insert_block_to_da_checker(block.into()); - r.trigger_unknown_block_from_attestation(block_root, peer_id); - // Should not trigger block request - let id = r.expect_blob_lookup_request(block_root); - r.expect_empty_network(); - // Resolve blob and expect lookup completed - r.complete_single_lookup_blob_lookup_valid(id, peer_id, blobs, true); - r.expect_no_active_lookups(); + // Add block to da_checker + // Complete test with happy path + // Assert that there were no requests for blocks + r.build_chain(1).await; + r.insert_block_to_da_chain_and_assert_missing_componens(r.block_at_slot(1)) + .await; + r.trigger_with_block_at_slot(1); + r.simulate(SimulateConfig::happy_path()).await; + r.assert_successful_lookup_sync(); + assert_eq!( + r.requests + .iter() + .filter(|(request, _)| matches!(request, RequestType::BlocksByRoot(_))) + .collect::<Vec<_>>(), + Vec::<&(RequestType<E>, AppRequestId)>::new(), + "There should be no block requests" + ); } -#[test] -fn block_in_processing_cache_becomes_invalid() { - let Some(mut r) = TestRig::test_setup_after_deneb_before_fulu() else { +#[tokio::test] +async fn block_in_processing_cache_becomes_invalid() { + let Some(mut r) = TestRig::new_after_deneb_before_fulu() else { return; }; - let (block, blobs) = r.rand_block_and_blobs(NumBlobs::Number(1)); - let block_root = block.canonical_root(); - let peer_id = r.new_connected_peer(); - r.insert_block_to_availability_cache(block.clone().into()); - r.trigger_unknown_block_from_attestation(block_root, peer_id); - // Should trigger blob request - let id = r.expect_blob_lookup_request(block_root); - // Should not trigger block request - r.expect_empty_network(); + 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_root); + r.simulate_block_gossip_processing_becomes_invalid(block.canonical_root()); // Should download block, then issue blobs request - r.complete_lookup_block_download(block); - // Should not trigger block or blob request - r.expect_empty_network(); - r.complete_lookup_block_import_valid(block_root, false); - // Resolve blob and expect lookup completed - r.complete_single_lookup_blob_lookup_valid(id, peer_id, blobs, true); - r.expect_no_active_lookups(); + r.simulate(SimulateConfig::happy_path()).await; + r.assert_successful_lookup_sync(); } -#[test] -fn block_in_processing_cache_becomes_valid_imported() { - let Some(mut r) = TestRig::test_setup_after_deneb_before_fulu() else { +#[tokio::test] +async fn block_in_processing_cache_becomes_valid_imported() { + let Some(mut r) = TestRig::new_after_deneb_before_fulu() else { return; }; - let (block, blobs) = r.rand_block_and_blobs(NumBlobs::Number(1)); - let block_root = block.canonical_root(); - let peer_id = r.new_connected_peer(); - r.insert_block_to_availability_cache(block.clone().into()); - r.trigger_unknown_block_from_attestation(block_root, peer_id); - // Should trigger blob request - let id = r.expect_blob_lookup_request(block_root); - // Should not trigger block request - r.expect_empty_network(); + 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_missing_components(block.into()); + r.simulate_block_gossip_processing_becomes_valid(block) + .await; // Should not trigger block or blob request - r.expect_empty_network(); + r.assert_empty_network(); // Resolve blob and expect lookup completed - r.complete_single_lookup_blob_lookup_valid(id, peer_id, blobs, true); - r.expect_no_active_lookups(); + r.assert_no_active_lookups(); } // IGNORE: wait for change that delays blob fetching to knowing the block -#[ignore] -#[test] -fn blobs_in_da_checker_skip_download() { - let Some(mut r) = TestRig::test_setup_after_deneb_before_fulu() else { +#[tokio::test] +async fn blobs_in_da_checker_skip_download() { + let Some(mut r) = TestRig::new_after_deneb_before_fulu() else { return; }; - let (block, blobs) = r.rand_block_and_blobs(NumBlobs::Number(1)); - let block_root = block.canonical_root(); - let peer_id = r.new_connected_peer(); - for blob in blobs { - r.insert_blob_to_da_checker(blob); + r.build_chain(1).await; + let block = r.get_last_block().clone(); + let blobs = block + .block_data() + .and_then(|d| d.blobs()) + .expect("block with no blobs"); + for blob in &blobs { + r.insert_blob_to_da_checker(blob.clone()); } - r.trigger_unknown_block_from_attestation(block_root, peer_id); - // Should download and process the block - r.complete_single_lookup_block_valid(block, true); - // Should not trigger blob request - r.expect_empty_network(); - r.expect_no_active_lookups(); + 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<_>>(), + Vec::<&(RequestType<E>, AppRequestId)>::new(), + "There should be no blob requests" + ); } -#[test] -fn custody_lookup_happy_path() { - let Some(mut r) = TestRig::test_setup_after_fulu() else { +macro_rules! fulu_peer_matrix_tests { + ( + [$($name:ident => $variant:expr),+ $(,)?] + ) => { + paste::paste! { + $( + #[tokio::test] + async fn [<custody_lookup_happy_path _ $name>]() { + custody_lookup_happy_path($variant).await; + } + + #[tokio::test] + async fn [<custody_lookup_some_custody_failures _ $name>]() { + custody_lookup_some_custody_failures($variant).await; + } + + #[tokio::test] + async fn [<custody_lookup_permanent_custody_failures _ $name>]() { + custody_lookup_permanent_custody_failures($variant).await; + } + )+ + } + }; +} + +fulu_peer_matrix_tests!( + [ + we_supernode_them_supernode => FuluTestType::WeSupernodeThemSupernode, + we_supernode_them_fullnodes => FuluTestType::WeSupernodeThemFullnodes, + we_fullnode_them_supernode => FuluTestType::WeFullnodeThemSupernode, + we_fullnode_them_fullnodes => FuluTestType::WeFullnodeThemFullnodes, + ] +); + +async fn custody_lookup_happy_path(test_type: FuluTestType) { + let Some(mut r) = TestRig::new_fulu_peer_test(test_type) else { return; }; - let spec = E::default_spec(); + r.build_chain(1).await; r.new_connected_peers_for_peerdas(); - let (block, data_columns) = r.rand_block_and_data_columns(); - let block_root = block.canonical_root(); - let peer_id = r.new_connected_peer(); - r.trigger_unknown_block_from_attestation(block_root, peer_id); - // Should not request blobs - let id = r.expect_block_lookup_request(block.canonical_root()); - r.complete_valid_block_request(id, block.into(), true); - // for each slot we download `samples_per_slot` columns - let sample_column_count = spec.samples_per_slot * spec.data_columns_per_group::<E>(); - let custody_ids = - r.expect_only_data_columns_by_root_requests(block_root, sample_column_count as usize); - r.complete_valid_custody_request(custody_ids, data_columns, false); - r.expect_no_active_lookups(); + r.trigger_with_last_block(); + r.simulate(SimulateConfig::happy_path()).await; + r.assert_no_penalties(); + r.assert_successful_lookup_sync(); } +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 custody_columns = r.custody_columns(); + r.simulate(SimulateConfig::new().return_no_columns_on_indices(&custody_columns[..4], 3)) + .await; + r.assert_penalties_of_type("NotEnoughResponsesReturned"); + r.assert_successful_lookup_sync(); +} + +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 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. + r.assert_penalties_of_type("NotEnoughResponsesReturned"); + r.assert_failed_lookup_sync(); +} + +// We supernode, diverse peers +// We not supernode, diverse peers + // TODO(das): Test retries of DataColumnByRoot: // - Expect request for column_index // - Respond with bad data // - Respond with stream terminator // ^ The stream terminator should be ignored and not close the next retry -mod deneb_only { - use super::*; - use beacon_chain::{ - block_verification_types::{AsBlock, RpcBlock}, - data_availability_checker::AvailabilityCheckError, - }; - use std::collections::VecDeque; - - struct DenebTester { - rig: TestRig, - block: Arc<SignedBeaconBlock<E>>, - blobs: Vec<Arc<BlobSidecar<E>>>, - parent_block_roots: Vec<Hash256>, - parent_block: VecDeque<Arc<SignedBeaconBlock<E>>>, - parent_blobs: VecDeque<Vec<Arc<BlobSidecar<E>>>>, - unknown_parent_block: Option<Arc<SignedBeaconBlock<E>>>, - unknown_parent_blobs: Option<Vec<Arc<BlobSidecar<E>>>>, - peer_id: PeerId, - block_req_id: Option<SingleLookupReqId>, - parent_block_req_id: Option<SingleLookupReqId>, - blob_req_id: Option<SingleLookupReqId>, - parent_blob_req_id: Option<SingleLookupReqId>, - slot: Slot, - block_root: Hash256, - } - - enum RequestTrigger { - AttestationUnknownBlock, - GossipUnknownParentBlock(usize), - GossipUnknownParentBlob(usize), - } - - impl RequestTrigger { - fn num_parents(&self) -> usize { - match self { - RequestTrigger::AttestationUnknownBlock => 0, - RequestTrigger::GossipUnknownParentBlock(num_parents) => *num_parents, - RequestTrigger::GossipUnknownParentBlob(num_parents) => *num_parents, - } - } - } - - impl DenebTester { - fn new(request_trigger: RequestTrigger) -> Option<Self> { - let Some(mut rig) = TestRig::test_setup_after_deneb_before_fulu() else { - return None; - }; - let (block, blobs) = rig.rand_block_and_blobs(NumBlobs::Random); - let mut block = Arc::new(block); - let mut blobs = blobs.into_iter().map(Arc::new).collect::<Vec<_>>(); - let slot = block.slot(); - - let num_parents = request_trigger.num_parents(); - let mut parent_block_chain = VecDeque::with_capacity(num_parents); - let mut parent_blobs_chain = VecDeque::with_capacity(num_parents); - let mut parent_block_roots = vec![]; - for _ in 0..num_parents { - // Set the current block as the parent. - let parent_root = block.canonical_root(); - let parent_block = block.clone(); - let parent_blobs = blobs.clone(); - parent_block_chain.push_front(parent_block); - parent_blobs_chain.push_front(parent_blobs); - parent_block_roots.push(parent_root); - - // Create the next block. - let (child_block, child_blobs) = - rig.block_with_parent_and_blobs(parent_root, NumBlobs::Random); - let mut child_block = Arc::new(child_block); - let mut child_blobs = child_blobs.into_iter().map(Arc::new).collect::<Vec<_>>(); - - // Update the new block to the current block. - std::mem::swap(&mut child_block, &mut block); - std::mem::swap(&mut child_blobs, &mut blobs); - } - let block_root = block.canonical_root(); - - let peer_id = rig.new_connected_peer(); - - // Trigger the request - let (block_req_id, blob_req_id, parent_block_req_id, parent_blob_req_id) = - match request_trigger { - RequestTrigger::AttestationUnknownBlock => { - rig.send_sync_message(SyncMessage::UnknownBlockHashFromAttestation( - peer_id, block_root, - )); - let block_req_id = rig.expect_block_lookup_request(block_root); - (Some(block_req_id), None, None, None) - } - RequestTrigger::GossipUnknownParentBlock { .. } => { - rig.send_sync_message(SyncMessage::UnknownParentBlock( - peer_id, - block.clone(), - block_root, - )); - - let parent_root = block.parent_root(); - let parent_block_req_id = rig.expect_block_parent_request(parent_root); - rig.expect_empty_network(); // expect no more requests - (None, None, Some(parent_block_req_id), None) - } - RequestTrigger::GossipUnknownParentBlob { .. } => { - let single_blob = blobs.first().cloned().unwrap(); - let parent_root = single_blob.block_parent_root(); - rig.send_sync_message(SyncMessage::UnknownParentBlob(peer_id, single_blob)); - - let parent_block_req_id = rig.expect_block_parent_request(parent_root); - rig.expect_empty_network(); // expect no more requests - (None, None, Some(parent_block_req_id), None) - } - }; - - Some(Self { - rig, - block, - blobs, - parent_block: parent_block_chain, - parent_blobs: parent_blobs_chain, - parent_block_roots, - unknown_parent_block: None, - unknown_parent_blobs: None, - peer_id, - block_req_id, - parent_block_req_id, - blob_req_id, - parent_blob_req_id, - slot, - block_root, - }) - } - - fn trigger_unknown_block_from_attestation(mut self) -> Self { - let block_root = self.block.canonical_root(); - self.rig - .trigger_unknown_block_from_attestation(block_root, self.peer_id); - self - } - - fn parent_block_response(mut self) -> Self { - self.rig.expect_empty_network(); - let block = self.parent_block.pop_front().unwrap().clone(); - let _ = self.unknown_parent_block.insert(block.clone()); - self.rig.parent_lookup_block_response( - self.parent_block_req_id.expect("parent request id"), - self.peer_id, - Some(block), - ); - - self.rig.assert_parent_lookups_count(1); - self - } - - fn parent_block_response_expect_blobs(mut self) -> Self { - self.rig.expect_empty_network(); - let block = self.parent_block.pop_front().unwrap().clone(); - let _ = self.unknown_parent_block.insert(block.clone()); - self.rig.parent_lookup_block_response( - self.parent_block_req_id.expect("parent request id"), - self.peer_id, - Some(block), - ); - - // Expect blobs request after sending block - let s = self.expect_parent_blobs_request(); - - s.rig.assert_parent_lookups_count(1); - s - } - - fn parent_blob_response(mut self) -> Self { - let blobs = self.parent_blobs.pop_front().unwrap(); - let _ = self.unknown_parent_blobs.insert(blobs.clone()); - for blob in &blobs { - self.rig.parent_lookup_blob_response( - self.parent_blob_req_id.expect("parent blob request id"), - self.peer_id, - Some(blob.clone()), - ); - assert_eq!(self.rig.active_parent_lookups_count(), 1); - } - self.rig.parent_lookup_blob_response( - self.parent_blob_req_id.expect("parent blob request id"), - self.peer_id, - None, - ); - - self - } - - fn block_response_triggering_process(self) -> Self { - let mut me = self.block_response_and_expect_blob_request(); - me.rig.expect_block_process(ResponseType::Block); - - // The request should still be active. - assert_eq!(me.rig.active_single_lookups_count(), 1); - me - } - - fn block_response_and_expect_blob_request(mut self) -> Self { - // The peer provides the correct block, should not be penalized. Now the block should be sent - // for processing. - self.rig.single_lookup_block_response( - self.block_req_id.expect("block request id"), - self.peer_id, - Some(self.block.clone()), - ); - // After responding with block the node will issue a blob request - let mut s = self.expect_blobs_request(); - - s.rig.expect_empty_network(); - - // The request should still be active. - s.rig.assert_lookup_is_active(s.block.canonical_root()); - s - } - - fn blobs_response(mut self) -> Self { - self.rig - .log(&format!("blobs response {}", self.blobs.len())); - for blob in &self.blobs { - self.rig.single_lookup_blob_response( - self.blob_req_id.expect("blob request id"), - self.peer_id, - Some(blob.clone()), - ); - self.rig - .assert_lookup_is_active(self.block.canonical_root()); - } - self.rig.single_lookup_blob_response( - self.blob_req_id.expect("blob request id"), - self.peer_id, - None, - ); - self - } - - fn blobs_response_was_valid(mut self) -> Self { - self.rig.expect_empty_network(); - if !self.blobs.is_empty() { - self.rig.expect_block_process(ResponseType::Blob); - } - self - } - - fn expect_empty_beacon_processor(mut self) -> Self { - self.rig.expect_empty_beacon_processor(); - self - } - - fn empty_block_response(mut self) -> Self { - self.rig.single_lookup_block_response( - self.block_req_id.expect("block request id"), - self.peer_id, - None, - ); - self - } - - fn empty_blobs_response(mut self) -> Self { - self.rig.single_lookup_blob_response( - self.blob_req_id.expect("blob request id"), - self.peer_id, - None, - ); - self - } - - fn empty_parent_blobs_response(mut self) -> Self { - self.rig.parent_lookup_blob_response( - self.parent_blob_req_id.expect("blob request id"), - self.peer_id, - None, - ); - self - } - - fn block_missing_components(mut self) -> Self { - self.rig.single_block_component_processed( - self.block_req_id.expect("block request id").lookup_id, - BlockProcessingResult::Ok(AvailabilityProcessingStatus::MissingComponents( - self.block.slot(), - self.block_root, - )), - ); - self.rig.expect_empty_network(); - self.rig.assert_single_lookups_count(1); - self - } - - fn blob_imported(mut self) -> Self { - self.rig.single_blob_component_processed( - self.blob_req_id.expect("blob request id").lookup_id, - BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(self.block_root)), - ); - self.rig.expect_empty_network(); - self.rig.assert_single_lookups_count(0); - self - } - - fn block_imported(mut self) -> Self { - // Missing blobs should be the request is not removed, the outstanding blobs request should - // mean we do not send a new request. - self.rig.single_block_component_processed( - self.block_req_id - .or(self.blob_req_id) - .expect("block request id") - .lookup_id, - BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(self.block_root)), - ); - self.rig.expect_empty_network(); - self.rig.assert_single_lookups_count(0); - self - } - - fn parent_block_imported(mut self) -> Self { - let parent_root = *self.parent_block_roots.first().unwrap(); - self.rig - .log(&format!("parent_block_imported {parent_root:?}")); - self.rig.parent_block_processed( - self.block_root, - BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(parent_root)), - ); - self.rig.expect_no_requests_for(parent_root); - self.rig.assert_parent_lookups_count(0); - self - } - - fn parent_block_missing_components(mut self) -> Self { - let parent_root = *self.parent_block_roots.first().unwrap(); - self.rig - .log(&format!("parent_block_missing_components {parent_root:?}")); - self.rig.parent_block_processed( - self.block_root, - BlockProcessingResult::Ok(AvailabilityProcessingStatus::MissingComponents( - Slot::new(0), - parent_root, - )), - ); - self.rig.expect_no_requests_for(parent_root); - self - } - - fn parent_blob_imported(mut self) -> Self { - let parent_root = *self.parent_block_roots.first().unwrap(); - self.rig - .log(&format!("parent_blob_imported {parent_root:?}")); - self.rig.parent_blob_processed( - self.block_root, - BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(parent_root)), - ); - - self.rig.expect_no_requests_for(parent_root); - self.rig.assert_parent_lookups_count(0); - self - } - - fn parent_block_unknown_parent(mut self) -> Self { - self.rig.log("parent_block_unknown_parent"); - let block = self.unknown_parent_block.take().unwrap(); - // Now this block is the one we expect requests from - self.block = block.clone(); - let block = RpcBlock::new( - block, - None, - &self.rig.harness.chain.data_availability_checker, - self.rig.harness.chain.spec.clone(), - ) - .unwrap(); - self.rig.parent_block_processed( - self.block_root, - BlockProcessingResult::Err(BlockError::ParentUnknown { - parent_root: block.parent_root(), - }), - ); - assert_eq!(self.rig.active_parent_lookups_count(), 1); - self - } - - fn invalid_parent_processed(mut self) -> Self { - self.rig.parent_block_processed( - self.block_root, - BlockProcessingResult::Err(BlockError::BlockSlotLimitReached), - ); - assert_eq!(self.rig.active_parent_lookups_count(), 1); - self - } - - fn invalid_block_processed(mut self) -> Self { - self.rig.single_block_component_processed( - self.block_req_id.expect("block request id").lookup_id, - BlockProcessingResult::Err(BlockError::BlockSlotLimitReached), - ); - self.rig.assert_single_lookups_count(1); - self - } - - fn invalid_blob_processed(mut self) -> Self { - self.rig.log("invalid_blob_processed"); - self.rig.single_blob_component_processed( - self.blob_req_id.expect("blob request id").lookup_id, - BlockProcessingResult::Err(BlockError::AvailabilityCheck( - AvailabilityCheckError::InvalidBlobs(kzg::Error::KzgVerificationFailed), - )), - ); - self.rig.assert_single_lookups_count(1); - self - } - - fn missing_components_from_block_request(mut self) -> Self { - self.rig.single_block_component_processed( - self.block_req_id.expect("block request id").lookup_id, - BlockProcessingResult::Ok(AvailabilityProcessingStatus::MissingComponents( - self.slot, - self.block_root, - )), - ); - // Add block to da_checker so blobs request can continue - self.rig.insert_block_to_da_checker(self.block.clone()); - - self.rig.assert_single_lookups_count(1); - self - } - - fn complete_current_block_and_blobs_lookup(self) -> Self { - self.expect_block_request() - .block_response_and_expect_blob_request() - .blobs_response() - // TODO: Should send blobs for processing - .expect_block_process() - .block_imported() - } - - fn log(self, msg: &str) -> Self { - self.rig.log(msg); - self - } - - fn parent_block_then_empty_parent_blobs(self) -> Self { - self.log( - " Return empty blobs for parent, block errors with missing components, downscore", - ) - .parent_block_response() - .expect_parent_blobs_request() - .empty_parent_blobs_response() - .expect_penalty("NotEnoughResponsesReturned") - .log("Re-request parent blobs, succeed and import parent") - .expect_parent_blobs_request() - .parent_blob_response() - .expect_block_process() - .parent_block_missing_components() - // Insert new peer into child request before completing parent - .trigger_unknown_block_from_attestation() - .parent_blob_imported() - } - - fn expect_penalty(mut self, expect_penalty_msg: &'static str) -> Self { - self.rig.expect_penalty(self.peer_id, expect_penalty_msg); - self - } - fn expect_no_penalty(mut self) -> Self { - self.rig.expect_empty_network(); - self - } - fn expect_no_penalty_and_no_requests(mut self) -> Self { - self.rig.expect_empty_network(); - self - } - fn expect_block_request(mut self) -> Self { - let id = self - .rig - .expect_block_lookup_request(self.block.canonical_root()); - self.block_req_id = Some(id); - self - } - fn expect_blobs_request(mut self) -> Self { - let id = self - .rig - .expect_blob_lookup_request(self.block.canonical_root()); - self.blob_req_id = Some(id); - self - } - fn expect_parent_block_request(mut self) -> Self { - let id = self - .rig - .expect_block_parent_request(self.block.parent_root()); - self.parent_block_req_id = Some(id); - self - } - fn expect_parent_blobs_request(mut self) -> Self { - let id = self - .rig - .expect_blob_parent_request(self.block.parent_root()); - self.parent_blob_req_id = Some(id); - self - } - fn expect_no_blobs_request(mut self) -> Self { - self.rig.expect_empty_network(); - self - } - fn expect_no_block_request(mut self) -> Self { - self.rig.expect_empty_network(); - self - } - fn invalidate_blobs_too_few(mut self) -> Self { - self.blobs.pop().expect("blobs"); - self - } - fn expect_block_process(mut self) -> Self { - self.rig.expect_block_process(ResponseType::Block); - self - } - fn expect_no_active_lookups(self) -> Self { - self.rig.expect_no_active_lookups(); - self - } - fn search_parent_dup(mut self) -> Self { - self.rig - .trigger_unknown_parent_block(self.peer_id, self.block.clone()); - self - } - } - - #[test] - fn single_block_and_blob_lookup_block_returned_first_attestation() { - let Some(tester) = DenebTester::new(RequestTrigger::AttestationUnknownBlock) else { - return; - }; - tester - .block_response_and_expect_blob_request() - .blobs_response() - .block_missing_components() // blobs not yet imported - .blobs_response_was_valid() - .blob_imported(); // now blobs resolve as imported - } - - #[test] - fn single_block_response_then_empty_blob_response_attestation() { - let Some(tester) = DenebTester::new(RequestTrigger::AttestationUnknownBlock) else { - return; - }; - tester - .block_response_and_expect_blob_request() - .missing_components_from_block_request() - .empty_blobs_response() - .expect_penalty("NotEnoughResponsesReturned") - .expect_blobs_request() - .expect_no_block_request(); - } - - #[test] - fn single_invalid_block_response_then_blob_response_attestation() { - let Some(tester) = DenebTester::new(RequestTrigger::AttestationUnknownBlock) else { - return; - }; - tester - .block_response_triggering_process() - .invalid_block_processed() - .expect_penalty("lookup_block_processing_failure") - .expect_block_request() - .expect_no_blobs_request() - .blobs_response() - // blobs not sent for processing until the block is processed - .expect_no_penalty_and_no_requests(); - } - - #[test] - fn single_block_response_then_invalid_blob_response_attestation() { - let Some(tester) = DenebTester::new(RequestTrigger::AttestationUnknownBlock) else { - return; - }; - tester - .block_response_triggering_process() - .missing_components_from_block_request() - .blobs_response() - .invalid_blob_processed() - .expect_penalty("lookup_blobs_processing_failure") - .expect_blobs_request() - .expect_no_block_request(); - } - - #[test] - fn single_block_response_then_too_few_blobs_response_attestation() { - let Some(tester) = DenebTester::new(RequestTrigger::AttestationUnknownBlock) else { - return; - }; - tester - .block_response_triggering_process() - .missing_components_from_block_request() - .invalidate_blobs_too_few() - .blobs_response() - .expect_penalty("NotEnoughResponsesReturned") - .expect_blobs_request() - .expect_no_block_request(); - } - - // Test peer returning block that has unknown parent, and a new lookup is created - #[test] - fn parent_block_unknown_parent() { - let Some(tester) = DenebTester::new(RequestTrigger::GossipUnknownParentBlock(1)) else { - return; - }; - tester - .expect_empty_beacon_processor() - .parent_block_response_expect_blobs() - .parent_blob_response() - .expect_block_process() - .parent_block_unknown_parent() - .expect_parent_block_request() - .expect_empty_beacon_processor(); - } - - // Test peer returning invalid (processing) block, expect retry - #[test] - fn parent_block_invalid_parent() { - let Some(tester) = DenebTester::new(RequestTrigger::GossipUnknownParentBlock(1)) else { - return; - }; - tester - .parent_block_response_expect_blobs() - .parent_blob_response() - .expect_block_process() - .invalid_parent_processed() - .expect_penalty("lookup_block_processing_failure") - .expect_parent_block_request() - .expect_empty_beacon_processor(); - } - - // Tests that if a peer does not respond with a block, we downscore and retry the block only - #[test] - fn empty_block_is_retried() { - let Some(tester) = DenebTester::new(RequestTrigger::AttestationUnknownBlock) else { - return; - }; - tester - .empty_block_response() - .expect_penalty("NotEnoughResponsesReturned") - .expect_block_request() - .expect_no_blobs_request() - .block_response_and_expect_blob_request() - .blobs_response() - .block_imported() - .expect_no_active_lookups(); - } - - #[test] - fn parent_block_then_empty_parent_blobs() { - let Some(tester) = DenebTester::new(RequestTrigger::GossipUnknownParentBlock(1)) else { - return; - }; - tester - .parent_block_then_empty_parent_blobs() - .log("resolve original block trigger blobs request and import") - // Should not have block request, it is cached - .expect_blobs_request() - // TODO: Should send blobs for processing - .block_imported() - .expect_no_active_lookups(); - } - - #[test] - fn parent_blob_unknown_parent() { - let Some(tester) = DenebTester::new(RequestTrigger::GossipUnknownParentBlob(1)) else { - return; - }; - tester - .expect_empty_beacon_processor() - .parent_block_response_expect_blobs() - .parent_blob_response() - .expect_block_process() - .parent_block_unknown_parent() - .expect_parent_block_request() - .expect_empty_beacon_processor(); - } - - #[test] - fn parent_blob_invalid_parent() { - let Some(tester) = DenebTester::new(RequestTrigger::GossipUnknownParentBlob(1)) else { - return; - }; - tester - .expect_empty_beacon_processor() - .parent_block_response_expect_blobs() - .parent_blob_response() - .expect_block_process() - .invalid_parent_processed() - .expect_penalty("lookup_block_processing_failure") - .expect_parent_block_request() - // blobs are not sent until block is processed - .expect_empty_beacon_processor(); - } - - #[test] - fn parent_block_and_blob_lookup_parent_returned_first_blob_trigger() { - let Some(tester) = DenebTester::new(RequestTrigger::GossipUnknownParentBlob(1)) else { - return; - }; - tester - .parent_block_response() - .expect_parent_blobs_request() - .parent_blob_response() - .expect_block_process() - .trigger_unknown_block_from_attestation() - .parent_block_imported() - .complete_current_block_and_blobs_lookup() - .expect_no_active_lookups(); - } - - #[test] - fn parent_block_then_empty_parent_blobs_blob_trigger() { - let Some(tester) = DenebTester::new(RequestTrigger::GossipUnknownParentBlob(1)) else { - return; - }; - tester - .parent_block_then_empty_parent_blobs() - .log("resolve original block trigger blobs request and import") - .complete_current_block_and_blobs_lookup() - .expect_no_active_lookups(); - } - - #[test] - fn parent_blob_unknown_parent_chain() { - let Some(tester) = DenebTester::new(RequestTrigger::GossipUnknownParentBlob(2)) else { - return; - }; - tester - .expect_empty_beacon_processor() - .parent_block_response_expect_blobs() - .parent_blob_response() - .expect_no_penalty() - .expect_block_process() - .parent_block_unknown_parent() - .expect_parent_block_request() - .expect_empty_beacon_processor() - .parent_block_response() - .expect_parent_blobs_request() - .parent_blob_response() - .expect_no_penalty() - .expect_block_process(); - } - - #[test] - fn unknown_parent_block_dup() { - let Some(tester) = DenebTester::new(RequestTrigger::GossipUnknownParentBlock(1)) else { - return; - }; - tester - .search_parent_dup() - .expect_no_blobs_request() - .expect_no_block_request(); - } - - #[test] - fn unknown_parent_blob_dup() { - let Some(tester) = DenebTester::new(RequestTrigger::GossipUnknownParentBlob(1)) else { - return; - }; - tester - .search_parent_dup() - .expect_no_blobs_request() - .expect_no_block_request(); - } - - // This test no longer applies, we don't issue requests for child lookups - // Keep for after updating rules on fetching blocks only first - #[ignore] - #[test] - fn no_peer_penalty_when_rpc_response_already_known_from_gossip() { - let Some(mut r) = TestRig::test_setup_after_deneb_before_fulu() else { - return; - }; - let (block, blobs) = r.rand_block_and_blobs(NumBlobs::Number(2)); - let block_root = block.canonical_root(); - let blob_0 = blobs[0].clone(); - let blob_1 = blobs[1].clone(); - let peer_a = r.new_connected_peer(); - let peer_b = r.new_connected_peer(); - // Send unknown parent block lookup - r.trigger_unknown_parent_block(peer_a, block.into()); - // Expect network request for blobs - let id = r.expect_blob_lookup_request(block_root); - // Peer responses with blob 0 - r.single_lookup_blob_response(id, peer_a, Some(blob_0.into())); - // Blob 1 is received via gossip unknown parent blob from a different peer - r.trigger_unknown_parent_blob(peer_b, blob_1.clone()); - // Original peer sends blob 1 via RPC - r.single_lookup_blob_response(id, peer_a, Some(blob_1.into())); - // Assert no downscore event for original peer - r.expect_no_penalty_for(peer_a); +// These `crypto_on` tests assert that the fake_crytpo feature works as expected. We run only the +// `crypto_on` tests without the fake_crypto feature and make sure that processing fails, = to +// assert that signatures and kzg proofs are checked +#[tokio::test] +async fn crypto_on_fail_with_invalid_block_signature() { + let mut r = TestRig::default(); + r.build_chain(1).await; + r.corrupt_last_block_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_block_processing_failure"); + } +} + +#[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 { + return; + }; + r.build_chain(1).await; + r.corrupt_last_column_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_custody_column_processing_failure"); + } +} + +#[tokio::test] +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(); + 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_custody_column_processing_failure"); } } diff --git a/beacon_node/network/src/sync/tests/mod.rs b/beacon_node/network/src/sync/tests/mod.rs index dcc7e3e49d..f00cf5841d 100644 --- a/beacon_node/network/src/sync/tests/mod.rs +++ b/beacon_node/network/src/sync/tests/mod.rs @@ -1,13 +1,19 @@ use crate::NetworkMessage; use crate::sync::SyncMessage; +use crate::sync::block_lookups::BlockLookupsMetrics; use crate::sync::manager::SyncManager; -use crate::sync::range_sync::RangeSyncType; +use crate::sync::tests::lookups::SimulateConfig; +use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::builder::Witness; +use beacon_chain::custody_context::NodeCustodyType; use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType}; use beacon_processor::WorkEvent; -use lighthouse_network::NetworkGlobals; +use lighthouse_network::rpc::RequestType; +use lighthouse_network::service::api_types::{AppRequestId, Id}; +use lighthouse_network::{NetworkGlobals, PeerId}; use rand_chacha::ChaCha20Rng; use slot_clock::ManualSlotClock; +use std::collections::{HashMap, HashSet}; use std::fs::OpenOptions; use std::io::Write; use std::sync::{Arc, Once}; @@ -16,7 +22,7 @@ use tokio::sync::mpsc; use tracing_subscriber::fmt::MakeWriter; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; -use types::{ForkName, MinimalEthSpec as E}; +use types::{ForkName, Hash256, MinimalEthSpec as E, Slot}; mod lookups; mod range; @@ -58,6 +64,8 @@ struct TestRig { network_rx_queue: Vec<NetworkMessage<E>>, /// Receiver for `SyncMessage` from the network sync_rx: mpsc::UnboundedReceiver<SyncMessage<E>>, + /// Stores all `SyncMessage`s received from `sync_rx` + sync_rx_queue: Vec<SyncMessage<E>>, /// To send `SyncMessage`. For sending RPC responses or block processing results to sync. sync_manager: SyncManager<T>, /// To manipulate sync state and peer connection status @@ -68,6 +76,65 @@ struct TestRig { rng_08: rand_chacha_03::ChaCha20Rng, rng: ChaCha20Rng, fork_name: ForkName, + /// Blocks that will be used in the test but may not be known to `harness` yet. + network_blocks_by_root: HashMap<Hash256, RpcBlock<E>>, + network_blocks_by_slot: HashMap<Slot, RpcBlock<E>>, + penalties: Vec<ReportedPenalty>, + /// All seen lookups through the test run + seen_lookups: HashMap<Id, SeenLookup>, + /// Registry of all requests done by the test + requests: Vec<(RequestType<E>, AppRequestId)>, + /// Persistent config on how to complete request + complete_strategy: SimulateConfig, + /// Metrics values to allow a reset + initial_block_lookups_metrics: BlockLookupsMetrics, + /// Fulu test type + fulu_test_type: FuluTestType, +} + +enum FuluTestType { + WeSupernodeThemSupernode, + WeSupernodeThemFullnodes, + WeFullnodeThemSupernode, + WeFullnodeThemFullnodes, +} + +impl FuluTestType { + fn we_node_custody_type(&self) -> NodeCustodyType { + match self { + Self::WeSupernodeThemSupernode | Self::WeSupernodeThemFullnodes => { + NodeCustodyType::Supernode + } + Self::WeFullnodeThemSupernode | Self::WeFullnodeThemFullnodes => { + NodeCustodyType::Fullnode + } + } + } + + fn them_node_custody_type(&self) -> NodeCustodyType { + match self { + Self::WeSupernodeThemSupernode | Self::WeFullnodeThemSupernode => { + NodeCustodyType::Supernode + } + Self::WeSupernodeThemFullnodes | Self::WeFullnodeThemFullnodes => { + NodeCustodyType::Fullnode + } + } + } +} + +#[derive(Debug)] +struct SeenLookup { + /// Lookup's Id + id: Id, + block_root: Hash256, + seen_peers: HashSet<PeerId>, +} + +#[derive(Debug)] +struct ReportedPenalty { + pub peer_id: PeerId, + pub msg: &'static str, } // Environment variable to read if `fork_from_env` feature is enabled. diff --git a/beacon_node/network/src/sync/tests/range.rs b/beacon_node/network/src/sync/tests/range.rs index 6f129bc8f0..67395ccd25 100644 --- a/beacon_node/network/src/sync/tests/range.rs +++ b/beacon_node/network/src/sync/tests/range.rs @@ -185,7 +185,7 @@ impl TestRig { } #[track_caller] - fn expect_chain_segments(&mut self, count: usize) { + fn assert_chain_segments(&mut self, count: usize) { for i in 0..count { self.pop_received_processor_event(|ev| { (ev.work_type() == beacon_processor::WorkType::ChainSegment).then_some(()) @@ -235,7 +235,7 @@ impl TestRig { panic!("Should have a BlocksByRange request, filter {request_filter:?}: {e:?}") }); - let by_range_data_requests = if self.after_fulu() { + let by_range_data_requests = if self.is_after_fulu() { let mut data_columns_requests = vec![]; while let Ok(data_columns_request) = self.pop_received_network_event(|ev| match ev { NetworkMessage::SendRequest { @@ -254,7 +254,7 @@ impl TestRig { panic!("Found zero DataColumnsByRange requests, filter {request_filter:?}"); } ByRangeDataRequestIds::PostPeerDAS(data_columns_requests) - } else if self.after_deneb() { + } else if self.is_after_deneb() { let (id, peer) = self .pop_received_network_event(|ev| match ev { NetworkMessage::SendRequest { @@ -489,7 +489,7 @@ fn build_rpc_block( fn head_chain_removed_while_finalized_syncing() { // NOTE: this is a regression test. // Added in PR https://github.com/sigp/lighthouse/pull/2821 - let mut rig = TestRig::test_setup(); + let mut rig = TestRig::default(); // Get a peer with an advanced head let head_peer = rig.add_head_peer(); @@ -514,11 +514,11 @@ fn head_chain_removed_while_finalized_syncing() { async fn state_update_while_purging() { // NOTE: this is a regression test. // Added in PR https://github.com/sigp/lighthouse/pull/2827 - let mut rig = TestRig::test_setup_with_custody_type(NodeCustodyType::SemiSupernode); + let mut rig = TestRig::with_custody_type(NodeCustodyType::SemiSupernode); // Create blocks on a separate harness // SemiSupernode ensures enough columns are stored for sampling + custody RPC block validation - let mut rig_2 = TestRig::test_setup_with_custody_type(NodeCustodyType::SemiSupernode); + let mut rig_2 = TestRig::with_custody_type(NodeCustodyType::SemiSupernode); // Need to create blocks that can be inserted into the fork-choice and fit the "known // conditions" below. let head_peer_block = rig_2.create_canonical_block().await; @@ -550,7 +550,7 @@ async fn state_update_while_purging() { #[test] fn pause_and_resume_on_ee_offline() { - let mut rig = TestRig::test_setup(); + let mut rig = TestRig::default(); // add some peers let peer1 = rig.add_head_peer(); @@ -559,7 +559,7 @@ fn pause_and_resume_on_ee_offline() { // send the response to the request rig.find_and_complete_blocks_by_range_request(filter().peer(peer1).epoch(0)); // the beacon processor shouldn't have received any work - rig.expect_empty_processor(); + rig.assert_empty_processor(); // while the ee is offline, more peers might arrive. Add a new finalized peer. let _peer2 = rig.add_finalized_peer(); @@ -570,14 +570,14 @@ fn pause_and_resume_on_ee_offline() { // epoch for the other batch. So we can either filter by epoch of by sync type. rig.find_and_complete_blocks_by_range_request(filter().epoch(0)); // the beacon processor shouldn't have received any work - rig.expect_empty_processor(); + rig.assert_empty_processor(); // make the beacon processor available again. // update_execution_engine_state implicitly calls resume // now resume range, we should have two processing requests in the beacon processor. rig.update_execution_engine_state(EngineState::Online); // The head chain and finalized chain (2) should be in the processing queue - rig.expect_chain_segments(2); + rig.assert_chain_segments(2); } /// To attempt to finalize the peer's status finalized checkpoint we synced to its finalized epoch + @@ -587,7 +587,7 @@ const EXTRA_SYNCED_EPOCHS: u64 = 2 + 1; #[test] fn finalized_sync_enough_global_custody_peers_few_chain_peers() { // Run for all forks - let mut r = TestRig::test_setup(); + let mut r = TestRig::default(); let advanced_epochs: u64 = 2; let remote_info = r.finalized_remote_info_advanced_by(advanced_epochs.into()); @@ -604,7 +604,7 @@ fn finalized_sync_enough_global_custody_peers_few_chain_peers() { #[test] fn finalized_sync_not_enough_custody_peers_on_start() { - let mut r = TestRig::test_setup(); + let mut r = TestRig::default(); // Only run post-PeerDAS if !r.fork_name.fulu_enabled() { return; @@ -621,7 +621,7 @@ fn finalized_sync_not_enough_custody_peers_on_start() { // Because we don't have enough peers on all columns we haven't sent any request. // NOTE: There's a small chance that this single peer happens to custody exactly the set we // expect, in that case the test will fail. Find a way to make the test deterministic. - r.expect_empty_network(); + r.assert_empty_network(); // Generate enough peers and supernodes to cover all custody columns let peer_count = 100; diff --git a/crypto/bls/src/impls/fake_crypto.rs b/crypto/bls/src/impls/fake_crypto.rs index e7eee05077..5fe0c3baab 100644 --- a/crypto/bls/src/impls/fake_crypto.rs +++ b/crypto/bls/src/impls/fake_crypto.rs @@ -49,7 +49,9 @@ impl TPublicKey for PublicKey { } fn serialize_uncompressed(&self) -> [u8; PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN] { - panic!("fake_crypto does not support uncompressed keys") + let mut bytes = [0; PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN]; + bytes[0..PUBLIC_KEY_BYTES_LEN].copy_from_slice(&self.0); + bytes } fn deserialize(bytes: &[u8]) -> Result<Self, Error> { @@ -58,8 +60,17 @@ impl TPublicKey for PublicKey { Ok(pubkey) } - fn deserialize_uncompressed(_: &[u8]) -> Result<Self, Error> { - panic!("fake_crypto does not support uncompressed keys") + fn deserialize_uncompressed(bytes: &[u8]) -> Result<Self, Error> { + if bytes.len() == PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN { + let mut pubkey = Self([0; PUBLIC_KEY_BYTES_LEN]); + pubkey.0.copy_from_slice(&bytes[0..PUBLIC_KEY_BYTES_LEN]); + Ok(pubkey) + } else { + Err(Error::InvalidByteLength { + got: bytes.len(), + expected: PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN, + }) + } } } @@ -97,7 +108,7 @@ pub struct Signature([u8; SIGNATURE_BYTES_LEN]); impl Signature { fn infinity() -> Self { - Self([0; SIGNATURE_BYTES_LEN]) + Self(INFINITY_SIGNATURE) } } @@ -213,7 +224,11 @@ impl TSecretKey<Signature, PublicKey> for SecretKey { } fn public_key(&self) -> PublicKey { - PublicKey::infinity() + let mut bytes = [0; PUBLIC_KEY_BYTES_LEN]; + bytes[0] = 0x01; + let to_copy = std::cmp::min(self.0.len(), bytes.len() - 1); + bytes[1..1 + to_copy].copy_from_slice(&self.0[..to_copy]); + PublicKey(bytes) } fn sign(&self, _msg: Hash256) -> Signature { diff --git a/crypto/kzg/Cargo.toml b/crypto/kzg/Cargo.toml index 5a36eb74f7..d2558663d5 100644 --- a/crypto/kzg/Cargo.toml +++ b/crypto/kzg/Cargo.toml @@ -5,6 +5,10 @@ authors = ["Pawan Dhananjay <pawandhananjay@gmail.com>"] edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = [] +fake_crypto = [] + [dependencies] arbitrary = { workspace = true } c-kzg = { workspace = true } diff --git a/crypto/kzg/src/lib.rs b/crypto/kzg/src/lib.rs index 0fe95b7723..66499dad8e 100644 --- a/crypto/kzg/src/lib.rs +++ b/crypto/kzg/src/lib.rs @@ -134,6 +134,9 @@ impl Kzg { kzg_commitment: KzgCommitment, kzg_proof: KzgProof, ) -> Result<(), Error> { + if cfg!(feature = "fake_crypto") { + return Ok(()); + } if !self.trusted_setup.verify_blob_kzg_proof( blob, &kzg_commitment.into(), @@ -155,6 +158,9 @@ impl Kzg { kzg_commitments: &[KzgCommitment], kzg_proofs: &[KzgProof], ) -> Result<(), Error> { + if cfg!(feature = "fake_crypto") { + return Ok(()); + } let commitments_bytes = kzg_commitments .iter() .map(|comm| Bytes48::from(*comm)) @@ -204,6 +210,9 @@ impl Kzg { y: &Bytes32, kzg_proof: KzgProof, ) -> Result<bool, Error> { + if cfg!(feature = "fake_crypto") { + return Ok(true); + } self.trusted_setup .verify_kzg_proof(&kzg_commitment.into(), z, y, &kzg_proof.into()) .map_err(Into::into) @@ -240,6 +249,9 @@ impl Kzg { indices: Vec<CellIndex>, kzg_commitments: &[Bytes48], ) -> Result<(), (Option<u64>, Error)> { + if cfg!(feature = "fake_crypto") { + return Ok(()); + } let mut column_groups: HashMap<u64, Vec<(CellRef, Bytes48, Bytes48)>> = HashMap::new(); let expected_len = cells.len(); From 26db016425aa1495d5ca0a33eeef98f430a3b35c Mon Sep 17 00:00:00 2001 From: Michael Sproul <michaelsproul@users.noreply.github.com> Date: Fri, 13 Feb 2026 16:24:26 +1100 Subject: [PATCH 15/81] Gloas consensus: epoch processing, block signature verification, more tests (#8808) - [x] Implement `process_builder_pending_payments` in epoch processing for Gloas. Enable the new EF tests for this sub-component as well. - [x] Update `include_all_signatures_except_proposal` for Gloas to safely include the execution payload bid signature (this was an omission in the previous bid PR). - [x] Enable Gloas for _all_ remaining EF tests by default. They all pass with the exception of the finality tests (see below). Co-Authored-By: Michael Sproul <michael@sigmaprime.io> Co-Authored-By: Eitan Seri- Levi <eserilev@gmail.com> --- .../src/per_block_processing.rs | 2 +- .../block_signature_verifier.rs | 22 ++++++ .../src/per_epoch_processing/single_pass.rs | 67 +++++++++++++++- .../src/per_slot_processing.rs | 19 +++++ testing/ef_tests/check_all_files_accessed.py | 8 +- .../ef_tests/src/cases/epoch_processing.rs | 21 +++++ testing/ef_tests/src/handler.rs | 77 ++++++++++++++----- testing/ef_tests/src/lib.rs | 4 +- testing/ef_tests/tests/tests.rs | 6 ++ 9 files changed, 192 insertions(+), 34 deletions(-) diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index 37639047fb..d9a41418cf 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -181,7 +181,7 @@ pub fn per_block_processing<E: EthSpec, Payload: AbstractExecPayload<E>>( let body = block.body(); if state.fork_name_unchecked().gloas_enabled() { withdrawals::gloas::process_withdrawals::<E>(state, spec)?; - // TODO(EIP-7732): process execution payload bid + process_execution_payload_bid(state, block, verify_signatures, spec)?; } else { if state.fork_name_unchecked().capella_enabled() { withdrawals::capella_electra::process_withdrawals::<E, Payload>( diff --git a/consensus/state_processing/src/per_block_processing/block_signature_verifier.rs b/consensus/state_processing/src/per_block_processing/block_signature_verifier.rs index 9aa44137d8..e82ce537fd 100644 --- a/consensus/state_processing/src/per_block_processing/block_signature_verifier.rs +++ b/consensus/state_processing/src/per_block_processing/block_signature_verifier.rs @@ -170,6 +170,7 @@ where self.include_exits(block)?; self.include_sync_aggregate(block)?; self.include_bls_to_execution_changes(block)?; + self.include_execution_payload_bid(block)?; Ok(()) } @@ -357,6 +358,27 @@ where Ok(()) } + /// Include the signature of the block's execution payload bid. + pub fn include_execution_payload_bid<Payload: AbstractExecPayload<E>>( + &mut self, + block: &'a SignedBeaconBlock<E, Payload>, + ) -> Result<()> { + if let Ok(signed_execution_payload_bid) = + block.message().body().signed_execution_payload_bid() + { + // TODO(gloas): if we implement a global builder pubkey cache we need to inject it here + if let Some(signature_set) = execution_payload_bid_signature_set( + self.state, + |builder_index| get_builder_pubkey_from_state(self.state, builder_index), + signed_execution_payload_bid, + self.spec, + )? { + self.sets.push(signature_set); + } + } + Ok(()) + } + /// Verify all the signatures that have been included in `self`, returning `true` if and only if /// all the signatures are valid. /// diff --git a/consensus/state_processing/src/per_epoch_processing/single_pass.rs b/consensus/state_processing/src/per_epoch_processing/single_pass.rs index 3e07803aa6..4eb1e36628 100644 --- a/consensus/state_processing/src/per_epoch_processing/single_pass.rs +++ b/consensus/state_processing/src/per_epoch_processing/single_pass.rs @@ -15,9 +15,9 @@ use std::collections::{BTreeSet, HashMap}; use tracing::instrument; use typenum::Unsigned; use types::{ - ActivationQueue, BeaconState, BeaconStateError, ChainSpec, Checkpoint, DepositData, Epoch, - EthSpec, ExitCache, ForkName, ParticipationFlags, PendingDeposit, ProgressiveBalancesCache, - RelativeEpoch, Validator, + ActivationQueue, BeaconState, BeaconStateError, BuilderPendingPayment, ChainSpec, Checkpoint, + DepositData, Epoch, EthSpec, ExitCache, ForkName, ParticipationFlags, PendingDeposit, + ProgressiveBalancesCache, RelativeEpoch, Validator, consts::altair::{ NUM_FLAG_INDICES, PARTICIPATION_FLAG_WEIGHTS, TIMELY_HEAD_FLAG_INDEX, TIMELY_TARGET_FLAG_INDEX, WEIGHT_DENOMINATOR, @@ -33,6 +33,7 @@ pub struct SinglePassConfig { pub pending_consolidations: bool, pub effective_balance_updates: bool, pub proposer_lookahead: bool, + pub builder_pending_payments: bool, } impl Default for SinglePassConfig { @@ -52,6 +53,7 @@ impl SinglePassConfig { pending_consolidations: true, effective_balance_updates: true, proposer_lookahead: true, + builder_pending_payments: true, } } @@ -65,6 +67,7 @@ impl SinglePassConfig { pending_consolidations: false, effective_balance_updates: false, proposer_lookahead: false, + builder_pending_payments: false, } } } @@ -455,6 +458,12 @@ pub fn process_epoch_single_pass<E: EthSpec>( )?; } + // Process builder pending payments outside the single-pass loop, as they depend on balances for + // multiple validators and cannot be computed accurately inside the loop. + if fork_name.gloas_enabled() && conf.builder_pending_payments { + process_builder_pending_payments(state, state_ctxt, spec)?; + } + // Finally, finish updating effective balance caches. We need this to happen *after* processing // of pending consolidations, which recomputes some effective balances. if conf.effective_balance_updates { @@ -503,6 +512,58 @@ pub fn process_proposer_lookahead<E: EthSpec>( Ok(()) } +/// Calculate the quorum threshold for builder payments based on total active balance. +fn get_builder_payment_quorum_threshold<E: EthSpec>( + state_ctxt: &StateContext, + spec: &ChainSpec, +) -> Result<u64, Error> { + let per_slot_balance = state_ctxt + .total_active_balance + .safe_div(E::slots_per_epoch())?; + let quorum = per_slot_balance.safe_mul(spec.builder_payment_threshold_numerator)?; + quorum + .safe_div(spec.builder_payment_threshold_denominator) + .map_err(Error::from) +} + +/// Processes the builder pending payments from the previous epoch. +fn process_builder_pending_payments<E: EthSpec>( + state: &mut BeaconState<E>, + state_ctxt: &StateContext, + spec: &ChainSpec, +) -> Result<(), Error> { + let quorum = get_builder_payment_quorum_threshold::<E>(state_ctxt, spec)?; + + // Collect qualifying payments and append to `builder_pending_withdrawals`. + // We use this pattern rather than a loop to avoid multiple borrows of the state's fields. + let new_pending_builder_withdrawals = state + .builder_pending_payments()? + .iter() + .take(E::SlotsPerEpoch::to_usize()) + .filter(|payment| payment.weight >= quorum) + .map(|payment| payment.withdrawal.clone()) + .collect::<Vec<_>>(); + for payment_withdrawal in new_pending_builder_withdrawals { + state + .builder_pending_withdrawals_mut()? + .push(payment_withdrawal)?; + } + + // NOTE: this could be a little more memory-efficient with some juggling to reuse parts + // of the persistent tree (could convert to list, use pop_front, convert back). + let updated_payments = state + .builder_pending_payments()? + .iter() + .skip(E::SlotsPerEpoch::to_usize()) + .cloned() + .chain((0..E::SlotsPerEpoch::to_usize()).map(|_| BuilderPendingPayment::default())) + .collect::<Vec<_>>(); + + *state.builder_pending_payments_mut()? = Vector::new(updated_payments)?; + + Ok(()) +} + fn process_single_inactivity_update( inactivity_score: &mut Cow<u64>, validator_info: &ValidatorInfo, diff --git a/consensus/state_processing/src/per_slot_processing.rs b/consensus/state_processing/src/per_slot_processing.rs index 0f8e5dc52d..f26ea567a2 100644 --- a/consensus/state_processing/src/per_slot_processing.rs +++ b/consensus/state_processing/src/per_slot_processing.rs @@ -14,6 +14,7 @@ pub enum Error { EpochProcessingError(EpochProcessingError), ArithError(ArithError), InconsistentStateFork(InconsistentFork), + BitfieldError(ssz::BitfieldError), } impl From<ArithError> for Error { @@ -22,6 +23,12 @@ impl From<ArithError> for Error { } } +impl From<ssz::BitfieldError> for Error { + fn from(e: ssz::BitfieldError) -> Self { + Self::BitfieldError(e) + } +} + /// Advances a state forward by one slot, performing per-epoch processing if required. /// /// If the root of the supplied `state` is known, then it can be passed as `state_root`. If @@ -48,6 +55,18 @@ pub fn per_slot_processing<E: EthSpec>( None }; + // Unset the next payload availability + if state.fork_name_unchecked().gloas_enabled() { + let next_slot_index = state + .slot() + .as_usize() + .safe_add(1)? + .safe_rem(E::slots_per_historical_root())?; + state + .execution_payload_availability_mut()? + .set(next_slot_index, false)?; + } + state.slot_mut().safe_add_assign(1)?; // Process fork upgrades here. Note that multiple upgrades can potentially run diff --git a/testing/ef_tests/check_all_files_accessed.py b/testing/ef_tests/check_all_files_accessed.py index 628ee83936..00638f7b1e 100755 --- a/testing/ef_tests/check_all_files_accessed.py +++ b/testing/ef_tests/check_all_files_accessed.py @@ -49,15 +49,9 @@ excluded_paths = [ "tests/.*/eip7805", # TODO(gloas): remove these ignores as more Gloas operations are implemented "tests/.*/gloas/operations/payload_attestation/.*", - # TODO(EIP-7732): remove these ignores as Gloas consensus is implemented - "tests/.*/gloas/epoch_processing/.*", - "tests/.*/gloas/finality/.*", + # TODO(gloas): remove these ignores as Gloas consensus is implemented "tests/.*/gloas/fork/.*", "tests/.*/gloas/fork_choice/.*", - "tests/.*/gloas/networking/.*", - "tests/.*/gloas/rewards/.*", - "tests/.*/gloas/sanity/.*", - "tests/.*/gloas/transition/.*", # Ignore MatrixEntry SSZ tests for now. "tests/.*/.*/ssz_static/MatrixEntry/.*", # TODO(gloas): Ignore Gloas light client stuff for now diff --git a/testing/ef_tests/src/cases/epoch_processing.rs b/testing/ef_tests/src/cases/epoch_processing.rs index f143643ec3..7a90fc70d0 100644 --- a/testing/ef_tests/src/cases/epoch_processing.rs +++ b/testing/ef_tests/src/cases/epoch_processing.rs @@ -79,6 +79,8 @@ pub struct InactivityUpdates; pub struct ParticipationFlagUpdates; #[derive(Debug)] pub struct ProposerLookahead; +#[derive(Debug)] +pub struct BuilderPendingPayments; type_name!( JustificationAndFinalization, @@ -100,6 +102,7 @@ type_name!(SyncCommitteeUpdates, "sync_committee_updates"); type_name!(InactivityUpdates, "inactivity_updates"); type_name!(ParticipationFlagUpdates, "participation_flag_updates"); type_name!(ProposerLookahead, "proposer_lookahead"); +type_name!(BuilderPendingPayments, "builder_pending_payments"); impl<E: EthSpec> EpochTransition<E> for JustificationAndFinalization { fn run(state: &mut BeaconState<E>, spec: &ChainSpec) -> Result<(), EpochProcessingError> { @@ -293,6 +296,20 @@ impl<E: EthSpec> EpochTransition<E> for ProposerLookahead { } } +impl<E: EthSpec> EpochTransition<E> for BuilderPendingPayments { + fn run(state: &mut BeaconState<E>, spec: &ChainSpec) -> Result<(), EpochProcessingError> { + process_epoch_single_pass( + state, + spec, + SinglePassConfig { + builder_pending_payments: true, + ..SinglePassConfig::disable_all() + }, + ) + .map(|_| ()) + } +} + impl<E: EthSpec, T: EpochTransition<E>> LoadCase for EpochProcessing<E, T> { fn load_from_dir(path: &Path, fork_name: ForkName) -> Result<Self, Error> { let spec = &testing_spec::<E>(fork_name); @@ -356,6 +373,10 @@ impl<E: EthSpec, T: EpochTransition<E>> Case for EpochProcessing<E, T> { return false; } + if !fork_name.gloas_enabled() && T::name() == "builder_pending_payments" { + return false; + } + true } diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index 5a43642c88..45bca21c6f 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -22,7 +22,7 @@ pub trait Handler { // Add forks here to exclude them from EF spec testing. Helpful for adding future or // unspecified forks. fn disabled_forks(&self) -> Vec<ForkName> { - vec![ForkName::Gloas] + vec![] } fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { @@ -395,11 +395,6 @@ where T::name().into() } - fn disabled_forks(&self) -> Vec<ForkName> { - // TODO(gloas): Can be removed once we enable Gloas on all tests - vec![] - } - fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { self.supported_forks.contains(&fork_name) } @@ -422,11 +417,6 @@ where fn handler_name(&self) -> String { BeaconState::<E>::name().into() } - - fn disabled_forks(&self) -> Vec<ForkName> { - // TODO(gloas): Can be removed once we enable Gloas on all tests - vec![] - } } impl<T, E> Handler for SszStaticWithSpecHandler<T, E> @@ -449,11 +439,6 @@ where T::name().into() } - fn disabled_forks(&self) -> Vec<ForkName> { - // TODO(gloas): Can be removed once we enable Gloas on all tests - vec![] - } - fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { self.supported_forks.contains(&fork_name) } @@ -552,6 +537,11 @@ impl<E: EthSpec + TypeName> Handler for RandomHandler<E> { fn handler_name(&self) -> String { "random".into() } + + fn disabled_forks(&self) -> Vec<ForkName> { + // TODO(gloas): remove once we have Gloas random tests + vec![ForkName::Gloas] + } } #[derive(Educe)] @@ -622,6 +612,11 @@ impl<E: EthSpec + TypeName> Handler for ForkHandler<E> { fn handler_name(&self) -> String { "fork".into() } + + fn disabled_forks(&self) -> Vec<ForkName> { + // TODO(gloas): remove once onboard_builders_from_pending_deposits is implemented + vec![ForkName::Gloas] + } } #[derive(Educe)] @@ -726,6 +721,11 @@ impl<E: EthSpec + TypeName> Handler for ForkChoiceHandler<E> { // run them with fake crypto. cfg!(not(feature = "fake_crypto")) } + + fn disabled_forks(&self) -> Vec<ForkName> { + // TODO(gloas): remove once we have Gloas fork choice tests + vec![ForkName::Gloas] + } } #[derive(Educe)] @@ -755,6 +755,11 @@ impl<E: EthSpec + TypeName> Handler for OptimisticSyncHandler<E> { fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { fork_name.bellatrix_enabled() && cfg!(not(feature = "fake_crypto")) } + + fn disabled_forks(&self) -> Vec<ForkName> { + // TODO(gloas): remove once we have Gloas optimistic sync tests + vec![ForkName::Gloas] + } } #[derive(Educe)] @@ -975,6 +980,11 @@ impl<E: EthSpec> Handler for KZGComputeCellsHandler<E> { fn handler_name(&self) -> String { "compute_cells".into() } + + fn disabled_forks(&self) -> Vec<ForkName> { + // TODO(gloas): remove once we have Gloas KZG tests + vec![ForkName::Gloas] + } } #[derive(Educe)] @@ -995,6 +1005,11 @@ impl<E: EthSpec> Handler for KZGComputeCellsAndKZGProofHandler<E> { fn handler_name(&self) -> String { "compute_cells_and_kzg_proofs".into() } + + fn disabled_forks(&self) -> Vec<ForkName> { + // TODO(gloas): remove once we have Gloas KZG tests + vec![ForkName::Gloas] + } } #[derive(Educe)] @@ -1015,6 +1030,11 @@ impl<E: EthSpec> Handler for KZGVerifyCellKZGProofBatchHandler<E> { fn handler_name(&self) -> String { "verify_cell_kzg_proof_batch".into() } + + fn disabled_forks(&self) -> Vec<ForkName> { + // TODO(gloas): remove once we have Gloas KZG tests + vec![ForkName::Gloas] + } } #[derive(Educe)] @@ -1035,6 +1055,11 @@ impl<E: EthSpec> Handler for KZGRecoverCellsAndKZGProofHandler<E> { fn handler_name(&self) -> String { "recover_cells_and_kzg_proofs".into() } + + fn disabled_forks(&self) -> Vec<ForkName> { + // TODO(gloas): remove once we have Gloas KZG tests + vec![ForkName::Gloas] + } } #[derive(Educe)] @@ -1059,6 +1084,11 @@ impl<E: EthSpec + TypeName> Handler for KzgInclusionMerkleProofValidityHandler<E fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { fork_name.deneb_enabled() } + + fn disabled_forks(&self) -> Vec<ForkName> { + // TODO(gloas): remove once we have Gloas KZG merkle proof tests + vec![ForkName::Gloas] + } } #[derive(Educe)] @@ -1083,6 +1113,11 @@ impl<E: EthSpec + TypeName> Handler for MerkleProofValidityHandler<E> { fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { fork_name.altair_enabled() } + + fn disabled_forks(&self) -> Vec<ForkName> { + // TODO(gloas): remove once we have Gloas light client tests + vec![ForkName::Gloas] + } } #[derive(Educe)] @@ -1108,6 +1143,11 @@ impl<E: EthSpec + TypeName> Handler for LightClientUpdateHandler<E> { // Enabled in Altair fork_name.altair_enabled() } + + fn disabled_forks(&self) -> Vec<ForkName> { + // TODO(gloas): remove once we have Gloas light client tests + vec![ForkName::Gloas] + } } #[derive(Educe)] @@ -1129,11 +1169,6 @@ impl<E: EthSpec + TypeName, O: Operation<E>> Handler for OperationsHandler<E, O> O::handler_name() } - fn disabled_forks(&self) -> Vec<ForkName> { - // TODO(gloas): Can be removed once we enable Gloas on all tests - vec![] - } - fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { Self::Case::is_enabled_for_fork(fork_name) && (!fork_name.gloas_enabled() diff --git a/testing/ef_tests/src/lib.rs b/testing/ef_tests/src/lib.rs index 49bea7d85f..94b19b6644 100644 --- a/testing/ef_tests/src/lib.rs +++ b/testing/ef_tests/src/lib.rs @@ -1,7 +1,7 @@ pub use case_result::CaseResult; pub use cases::{ - Case, EffectiveBalanceUpdates, Eth1DataReset, ExecutionPayloadBidBlock, FeatureName, - HistoricalRootsUpdate, HistoricalSummariesUpdate, InactivityUpdates, + BuilderPendingPayments, Case, EffectiveBalanceUpdates, Eth1DataReset, ExecutionPayloadBidBlock, + FeatureName, HistoricalRootsUpdate, HistoricalSummariesUpdate, InactivityUpdates, JustificationAndFinalization, ParticipationFlagUpdates, ParticipationRecordUpdates, PendingBalanceDeposits, PendingConsolidations, ProposerLookahead, RandaoMixesReset, RegistryUpdates, RewardsAndPenalties, Slashings, SlashingsReset, SyncCommitteeUpdates, diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index 332f077984..c3481a2405 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -954,6 +954,12 @@ fn epoch_processing_proposer_lookahead() { EpochProcessingHandler::<MainnetEthSpec, ProposerLookahead>::default().run(); } +#[test] +fn epoch_processing_builder_pending_payments() { + EpochProcessingHandler::<MinimalEthSpec, BuilderPendingPayments>::default().run(); + EpochProcessingHandler::<MainnetEthSpec, BuilderPendingPayments>::default().run(); +} + #[test] fn fork_upgrade() { ForkHandler::<MinimalEthSpec>::default().run(); From 68ad9758a3e596ff99825bdf2fe4afcbe1894c3a Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi <eserilev@ucsc.edu> Date: Fri, 13 Feb 2026 13:39:56 -0800 Subject: [PATCH 16/81] Gloas attestation verification (#8705) https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/p2p-interface.md#attestation-subnets Implements attestation verification logic for Gloas and adds a few gloas related tests. Note that a few of these tests rely on gloas test harness block production which hasn't been built out yet. So for now those tests are ignored. Co-Authored-By: Eitan Seri- Levi <eserilev@gmail.com> Co-Authored-By: Eitan Seri-Levi <eserilev@ucsc.edu> --- .../src/attestation_verification.rs | 68 ++++- .../tests/attestation_verification.rs | 259 ++++++++++++++++++ .../gossip_methods.rs | 19 ++ 3 files changed, 336 insertions(+), 10 deletions(-) diff --git a/beacon_node/beacon_chain/src/attestation_verification.rs b/beacon_node/beacon_chain/src/attestation_verification.rs index faa396966f..667bafe445 100644 --- a/beacon_node/beacon_chain/src/attestation_verification.rs +++ b/beacon_node/beacon_chain/src/attestation_verification.rs @@ -61,8 +61,9 @@ use tracing::{debug, error}; use tree_hash::TreeHash; use types::{ Attestation, AttestationData, AttestationRef, BeaconCommittee, - BeaconStateError::NoCommitteeFound, ChainSpec, CommitteeIndex, Epoch, EthSpec, Hash256, - IndexedAttestation, SelectionProof, SignedAggregateAndProof, SingleAttestation, Slot, SubnetId, + BeaconStateError::NoCommitteeFound, ChainSpec, CommitteeIndex, Epoch, EthSpec, ForkName, + Hash256, IndexedAttestation, SelectionProof, SignedAggregateAndProof, SingleAttestation, Slot, + SubnetId, }; pub use batch::{batch_verify_aggregated_attestations, batch_verify_unaggregated_attestations}; @@ -160,6 +161,12 @@ pub enum Error { /// /// The peer has sent an invalid message. CommitteeIndexNonZero(usize), + /// The validator index is set to an invalid value after Gloas. + /// + /// ## Peer scoring + /// + /// The peer has sent an invalid message. + CommitteeIndexInvalid, /// The `attestation.data.beacon_block_root` block is unknown. /// /// ## Peer scoring @@ -550,8 +557,12 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { } .tree_hash_root(); + let fork_name = chain + .spec + .fork_name_at_slot::<T::EthSpec>(attestation.data().slot); + // [New in Electra:EIP7549] - verify_committee_index(attestation)?; + verify_committee_index(attestation, fork_name)?; if chain .observed_attestations @@ -595,6 +606,17 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { // attestation and do not delay consideration for later. let head_block = verify_head_block_is_known(chain, attestation.data(), None)?; + // [New in Gloas]: If the attested block is from the same slot as the attestation, + // index must be 0. + if fork_name.gloas_enabled() + && head_block.slot == attestation.data().slot + && attestation.data().index != 0 + { + return Err(Error::CommitteeIndexNonZero( + attestation.data().index as usize, + )); + } + // Check the attestation target root is consistent with the head root. // // This check is not in the specification, however we guard against it since it opens us up @@ -871,7 +893,12 @@ impl<'a, T: BeaconChainTypes> IndexedUnaggregatedAttestation<'a, T> { let fork_name = chain .spec .fork_name_at_slot::<T::EthSpec>(attestation.data.slot); - if fork_name.electra_enabled() { + if fork_name.gloas_enabled() { + // [New in Gloas] + if attestation.data.index >= 2 { + return Err(Error::CommitteeIndexInvalid); + } + } else if fork_name.electra_enabled() { // [New in Electra:EIP7549] if attestation.data.index != 0 { return Err(Error::CommitteeIndexNonZero( @@ -890,6 +917,17 @@ impl<'a, T: BeaconChainTypes> IndexedUnaggregatedAttestation<'a, T> { chain.config.import_max_skip_slots, )?; + // [New in Gloas]: If the attested block is from the same slot as the attestation, + // index must be 0. + if fork_name.gloas_enabled() + && head_block.slot == attestation.data.slot + && attestation.data.index != 0 + { + return Err(Error::CommitteeIndexNonZero( + attestation.data.index as usize, + )); + } + // Check the attestation target root is consistent with the head root. verify_attestation_target_root::<T::EthSpec>(&head_block, &attestation.data)?; @@ -1404,7 +1442,10 @@ pub fn verify_signed_aggregate_signatures<T: BeaconChainTypes>( /// Verify that the `attestation` committee index is properly set for the attestation's fork. /// This function will only apply verification post-Electra. -pub fn verify_committee_index<E: EthSpec>(attestation: AttestationRef<E>) -> Result<(), Error> { +pub fn verify_committee_index<E: EthSpec>( + attestation: AttestationRef<E>, + fork_name: ForkName, +) -> Result<(), Error> { if let Ok(committee_bits) = attestation.committee_bits() { // Check to ensure that the attestation is for a single committee. let num_committee_bits = get_committee_indices::<E>(committee_bits); @@ -1414,11 +1455,18 @@ pub fn verify_committee_index<E: EthSpec>(attestation: AttestationRef<E>) -> Res )); } - // Ensure the attestation index is set to zero post Electra. - if attestation.data().index != 0 { - return Err(Error::CommitteeIndexNonZero( - attestation.data().index as usize, - )); + // Ensure the attestation index is valid for the fork. + let index = attestation.data().index; + if fork_name.gloas_enabled() { + // [New in Gloas]: index must be < 2. + if index >= 2 { + return Err(Error::CommitteeIndexInvalid); + } + } else { + // [New in Electra:EIP7549]: index must be 0. + if index != 0 { + return Err(Error::CommitteeIndexNonZero(index as usize)); + } } } Ok(()) diff --git a/beacon_node/beacon_chain/tests/attestation_verification.rs b/beacon_node/beacon_chain/tests/attestation_verification.rs index 8aeb881aa4..96071be89f 100644 --- a/beacon_node/beacon_chain/tests/attestation_verification.rs +++ b/beacon_node/beacon_chain/tests/attestation_verification.rs @@ -368,6 +368,13 @@ impl GossipTester { self.harness.chain.epoch().unwrap() } + pub fn is_gloas(&self) -> bool { + self.harness + .spec + .fork_name_at_slot::<E>(self.valid_attestation.data.slot) + .gloas_enabled() + } + pub fn earliest_valid_attestation_slot(&self) -> Slot { let offset = if self .harness @@ -522,6 +529,44 @@ impl GossipTester { self } + + /// Like `inspect_aggregate_err`, but only runs the check if gloas is enabled. + /// If gloas is not enabled, this is a no-op that returns self. + pub fn inspect_aggregate_err_if_gloas<G, I>( + self, + desc: &str, + get_attn: G, + inspect_err: I, + ) -> Self + where + G: Fn(&Self, &mut SignedAggregateAndProof<E>), + I: Fn(&Self, AttnError), + { + if self.is_gloas() { + self.inspect_aggregate_err(desc, get_attn, inspect_err) + } else { + self + } + } + + /// Like `inspect_unaggregate_err`, but only runs the check if gloas is enabled. + /// If gloas is not enabled, this is a no-op that returns self. + pub fn inspect_unaggregate_err_if_gloas<G, I>( + self, + desc: &str, + get_attn: G, + inspect_err: I, + ) -> Self + where + G: Fn(&Self, &mut SingleAttestation, &mut SubnetId, &ChainSpec), + I: Fn(&Self, AttnError), + { + if self.is_gloas() { + self.inspect_unaggregate_err(desc, get_attn, inspect_err) + } else { + self + } + } } /// Tests verification of `SignedAggregateAndProof` from the gossip network. #[tokio::test] @@ -854,6 +899,27 @@ async fn aggregated_gossip_verification() { )) }, ) + /* + * [New in Gloas]: attestation.data.index must be < 2 + */ + .inspect_aggregate_err_if_gloas( + "gloas: aggregate with index >= 2", + |_, a| match a.to_mut() { + SignedAggregateAndProofRefMut::Base(_) => { + panic!("Expected Electra attestation variant"); + } + SignedAggregateAndProofRefMut::Electra(att) => { + att.message.aggregate.data.index = 2; + } + }, + |_, err| { + assert!( + matches!(err, AttnError::CommitteeIndexInvalid), + "expected CommitteeIndexInvalid, got {:?}", + err + ) + }, + ) // NOTE: from here on, the tests are stateful, and rely on the valid attestation having // been seen. .import_valid_aggregate() @@ -1071,6 +1137,22 @@ async fn unaggregated_gossip_verification() { )) }, ) + /* + * [New in Gloas]: attestation.data.index must be < 2 + */ + .inspect_unaggregate_err_if_gloas( + "gloas: attestation with index >= 2", + |_, a, _, _| { + a.data.index = 2; + }, + |_, err| { + assert!( + matches!(err, AttnError::CommitteeIndexInvalid), + "expected CommitteeIndexInvalid, got {:?}", + err + ) + }, + ) // NOTE: from here on, the tests are stateful, and rely on the valid attestation having // been seen. .import_valid_unaggregate() @@ -1700,3 +1782,180 @@ async fn aggregated_attestation_verification_use_head_state_fork() { ); } } + +/// [New in Gloas]: Tests that unaggregated attestations with `data.index == 1` are rejected +/// when `head_block.slot == attestation.data.slot`. +/// +/// This test only runs when `FORK_NAME=gloas` is set with `fork_from_env` feature. +// TODO(EIP-7732): Enable this test once gloas block production works in test harness. +// `state.latest_execution_payload_header()` not available in Gloas. +#[ignore] +#[tokio::test] +async fn gloas_unaggregated_attestation_same_slot_index_must_be_zero() { + let harness = get_harness(VALIDATOR_COUNT); + + // Skip this test if not running with gloas fork + if !harness + .spec + .fork_name_at_epoch(Epoch::new(0)) + .gloas_enabled() + { + return; + } + + // Extend the chain out a few epochs so we have some chain depth to play with. + harness + .extend_chain( + MainnetEthSpec::slots_per_epoch() as usize * 3 - 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Produce a block in the current slot (this creates the same-slot scenario) + harness + .extend_chain( + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::SomeValidators(vec![]), + ) + .await; + + let current_slot = harness.chain.slot().expect("should get slot"); + let head = harness.chain.head_snapshot(); + + // Verify head block is in the current slot + assert_eq!( + head.beacon_block.slot(), + current_slot, + "head block should be in current slot for same-slot test" + ); + + // Produce an attestation for the current slot + let (mut attestation, _attester_sk, subnet_id) = + get_valid_unaggregated_attestation(&harness.chain); + + // Verify we have a same-slot scenario + let attested_block_slot = harness + .chain + .canonical_head + .fork_choice_read_lock() + .get_block(&attestation.data.beacon_block_root) + .expect("block should exist") + .slot; + assert_eq!( + attested_block_slot, attestation.data.slot, + "attested block slot should equal attestation slot for same-slot test" + ); + + // index == 1 should be rejected when head_block.slot == attestation.data.slot + attestation.data.index = 1; + let result = harness + .chain + .verify_unaggregated_attestation_for_gossip(&attestation, Some(subnet_id)); + assert!( + matches!(result, Err(AttnError::CommitteeIndexNonZero(_))), + "gloas: attestation with index == 1 when head_block.slot == attestation.data.slot should be rejected, got {:?}", + result.err() + ); +} + +/// [New in Gloas]: Tests that aggregated attestations with `data.index == 1` are rejected +/// when `head_block.slot == attestation.data.slot`. +/// +/// This test only runs when `FORK_NAME=gloas` is set with `fork_from_env` feature. +// TODO(EIP-7732): Enable this test once gloas block production works in test harness. +// `state.latest_execution_payload_header()` not available in Gloas. +#[ignore] +#[tokio::test] +async fn gloas_aggregated_attestation_same_slot_index_must_be_zero() { + let harness = get_harness(VALIDATOR_COUNT); + + // Skip this test if not running with gloas fork + if !harness + .spec + .fork_name_at_epoch(Epoch::new(0)) + .gloas_enabled() + { + return; + } + + // Extend the chain out a few epochs so we have some chain depth to play with. + harness + .extend_chain( + MainnetEthSpec::slots_per_epoch() as usize * 3 - 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Produce a block in the current slot (this creates the same-slot scenario) + harness + .extend_chain( + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::SomeValidators(vec![]), + ) + .await; + + let current_slot = harness.chain.slot().expect("should get slot"); + let head = harness.chain.head_snapshot(); + + // Verify head block is in the current slot + assert_eq!( + head.beacon_block.slot(), + current_slot, + "head block should be in current slot for same-slot test" + ); + + // Produce an attestation for the current slot + let (valid_attestation, _attester_sk, _subnet_id) = + get_valid_unaggregated_attestation(&harness.chain); + + // Verify we have a same-slot scenario + let attested_block_slot = harness + .chain + .canonical_head + .fork_choice_read_lock() + .get_block(&valid_attestation.data.beacon_block_root) + .expect("block should exist") + .slot; + assert_eq!( + attested_block_slot, valid_attestation.data.slot, + "attested block slot should equal attestation slot for same-slot test" + ); + + // Convert to aggregate + let committee = head + .beacon_state + .get_beacon_committee(current_slot, valid_attestation.committee_index) + .expect("should get committee"); + let fork_name = harness + .spec + .fork_name_at_slot::<E>(valid_attestation.data.slot); + let aggregate_attestation = + single_attestation_to_attestation(&valid_attestation, committee.committee, fork_name) + .unwrap(); + + let (mut valid_aggregate, _, _) = + get_valid_aggregated_attestation(&harness.chain, aggregate_attestation); + + // index == 1 should be rejected when head_block.slot == attestation.data.slot + match valid_aggregate.to_mut() { + SignedAggregateAndProofRefMut::Base(att) => { + att.message.aggregate.data.index = 1; + } + SignedAggregateAndProofRefMut::Electra(att) => { + att.message.aggregate.data.index = 1; + } + } + + let result = harness + .chain + .verify_aggregated_attestation_for_gossip(&valid_aggregate); + assert!( + matches!(result, Err(AttnError::CommitteeIndexNonZero(_))), + "gloas: aggregate with index == 1 when head_block.slot == attestation.data.slot should be rejected, got {:?}", + result.err() + ); +} 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 a4125f3df0..a9198f1943 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -2415,6 +2415,25 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> { "attn_comm_index_non_zero", ); } + AttnError::CommitteeIndexInvalid => { + /* + * The committee index is invalid after Gloas. + * + * The peer has published an invalid consensus message. + */ + debug!( + %peer_id, + block = ?beacon_block_root, + ?attestation_type, + "Committee index invalid" + ); + self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject); + self.gossip_penalize_peer( + peer_id, + PeerAction::LowToleranceError, + "attn_comm_index_invalid", + ); + } AttnError::UnknownHeadBlock { beacon_block_root } => { trace!( %peer_id, From a3a74d89881265ebe44299dbe7df7331561e012d Mon Sep 17 00:00:00 2001 From: Mac L <mjladson@pm.me> Date: Sat, 14 Feb 2026 12:26:25 +0400 Subject: [PATCH 17/81] Correctly compute process times during `ProcessHealth::observe` (#8793) I believe I found a bug where during computation of `pid_process_seconds_total` we add `children_system` twice. I assume that it was originally intended to add `children_system` and `children_user` once each Co-Authored-By: Mac L <mjladson@pm.me> --- common/health_metrics/src/observe.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/health_metrics/src/observe.rs b/common/health_metrics/src/observe.rs index 81bb8e6f7e..5bc3770301 100644 --- a/common/health_metrics/src/observe.rs +++ b/common/health_metrics/src/observe.rs @@ -121,7 +121,7 @@ impl Observe for ProcessHealth { pid_mem_shared_memory_size: process_mem.shared(), pid_process_seconds_total: process_times.busy().as_secs() + process_times.children_system().as_secs() - + process_times.children_system().as_secs(), + + process_times.children_user().as_secs(), }) } } From 1fe7a8ce77ada05de097352c5acb7646cd107852 Mon Sep 17 00:00:00 2001 From: Romeo <romeobourne211@gmail.com> Date: Mon, 16 Feb 2026 00:44:15 +0100 Subject: [PATCH 18/81] Implement inactivity scores ef tests unstable (#8807) fixes issue #8750 This PR enables the inactivity_scores reward EF tests from v1.7.0-alpha.2. - Enabled Tests: Added the inactivity_scores handler to the rewards test suite. - Fork Filtering: Updated the runner to execute these tests only on supported forks (Altair onwards), preventing directory-not-found errors on earlier forks. - CI Coverage: Removed exclusions in the file access check script to ensures all new test vectors are fully tracked. Co-Authored-By: romeoscript <romeobourne211@gmail.com> Co-Authored-By: Eitan Seri-Levi <eserilev@gmail.com> Co-Authored-By: Michael Sproul <michael@sigmaprime.io> --- testing/ef_tests/check_all_files_accessed.py | 2 -- testing/ef_tests/src/handler.rs | 9 +++++++++ testing/ef_tests/tests/tests.rs | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/testing/ef_tests/check_all_files_accessed.py b/testing/ef_tests/check_all_files_accessed.py index 00638f7b1e..b465a47296 100755 --- a/testing/ef_tests/check_all_files_accessed.py +++ b/testing/ef_tests/check_all_files_accessed.py @@ -70,8 +70,6 @@ excluded_paths = [ # Ignore full epoch tests for now (just test the sub-transitions). "tests/.*/.*/epoch_processing/.*/pre_epoch.ssz_snappy", "tests/.*/.*/epoch_processing/.*/post_epoch.ssz_snappy", - # Ignore inactivity_scores tests for now (should implement soon). - "tests/.*/.*/rewards/inactivity_scores/.*", # Ignore KZG tests that target internal kzg library functions "tests/.*/compute_verify_cell_kzg_proof_batch_challenge/.*", "tests/.*/compute_challenge/.*", diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index 45bca21c6f..625778c2dd 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -592,6 +592,15 @@ impl<E: EthSpec + TypeName> Handler for RewardsHandler<E> { fn handler_name(&self) -> String { self.handler_name.to_string() } + + fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { + if self.handler_name == "inactivity_scores" { + // These tests were added in v1.7.0-alpha.2 and are available for Altair and later. + fork_name.altair_enabled() + } else { + true + } + } } #[derive(Educe)] diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index c3481a2405..fcf7951c3e 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -1113,7 +1113,7 @@ fn kzg_inclusion_merkle_proof_validity() { #[test] fn rewards() { - for handler in &["basic", "leak", "random"] { + for handler in &["basic", "leak", "random", "inactivity_scores"] { RewardsHandler::<MinimalEthSpec>::new(handler).run(); RewardsHandler::<MainnetEthSpec>::new(handler).run(); } From 5563b7a1dd01fc27f80bc596fe7b8121d5057eee Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Sun, 15 Feb 2026 16:58:30 -0700 Subject: [PATCH 19/81] fix second_payload head in execution engine test (#8789) Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com> --- testing/execution_engine_integration/src/test_rig.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/execution_engine_integration/src/test_rig.rs b/testing/execution_engine_integration/src/test_rig.rs index 24d75f5a11..5c3061166e 100644 --- a/testing/execution_engine_integration/src/test_rig.rs +++ b/testing/execution_engine_integration/src/test_rig.rs @@ -563,7 +563,7 @@ impl<Engine: GenericExecutionEngine> TestRig<Engine> { * * Indicate that the payload is the head of the chain, providing payload attributes. */ - let head_block_hash = valid_payload.block_hash(); + let head_block_hash = second_payload.block_hash(); let finalized_block_hash = ExecutionBlockHash::zero(); // To save sending proposer preparation data, just set the fee recipient // to the fee recipient configured for EE A. From fcfd061fc2ddf43b74e9b78ac60d7c62694fb715 Mon Sep 17 00:00:00 2001 From: Mac L <mjladson@pm.me> Date: Mon, 16 Feb 2026 05:45:29 +0400 Subject: [PATCH 20/81] Fix eth2 compilation by feature gating `SseEventSource` (#8819) `eth2` is currently unable to be built without the `events` feature. Feature gates the `SseEventSource` match arm in the `status` function. Co-Authored-By: Mac L <mjladson@pm.me> --- common/eth2/src/error.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/common/eth2/src/error.rs b/common/eth2/src/error.rs index 671a617c9e..45f2599493 100644 --- a/common/eth2/src/error.rs +++ b/common/eth2/src/error.rs @@ -102,6 +102,7 @@ impl Error { None } } + #[cfg(feature = "events")] Error::SseEventSource(_) => None, Error::ServerMessage(msg) => StatusCode::try_from(msg.code).ok(), Error::ServerIndexedMessage(msg) => StatusCode::try_from(msg.code).ok(), From 48a2b2802da833c3cd626c0697835c076de1fae3 Mon Sep 17 00:00:00 2001 From: Michael Sproul <michaelsproul@users.noreply.github.com> Date: Mon, 16 Feb 2026 13:49:46 +1100 Subject: [PATCH 21/81] Delete `OnDiskConsensusContext` (#8824) Remove the `OnDiskConsensusContext` type which is no longer used at all after the merge of: - https://github.com/sigp/lighthouse/pull/8724 This type was not necessary since the merge of Lion's change which removed the on-disk storage: - https://github.com/sigp/lighthouse/pull/5891 Co-Authored-By: Michael Sproul <michael@sigmaprime.io> --- beacon_node/store/src/consensus_context.rs | 65 ---------------------- beacon_node/store/src/lib.rs | 2 - 2 files changed, 67 deletions(-) delete mode 100644 beacon_node/store/src/consensus_context.rs diff --git a/beacon_node/store/src/consensus_context.rs b/beacon_node/store/src/consensus_context.rs deleted file mode 100644 index 281106d9aa..0000000000 --- a/beacon_node/store/src/consensus_context.rs +++ /dev/null @@ -1,65 +0,0 @@ -use ssz_derive::{Decode, Encode}; -use state_processing::ConsensusContext; -use std::collections::HashMap; -use types::{EthSpec, Hash256, IndexedAttestation, Slot}; - -/// The consensus context is stored on disk as part of the data availability overflow cache. -/// -/// We use this separate struct to keep the on-disk format stable in the presence of changes to the -/// in-memory `ConsensusContext`. You MUST NOT change the fields of this struct without -/// superstructing it and implementing a schema migration. -#[derive(Debug, PartialEq, Clone, Encode, Decode)] -pub struct OnDiskConsensusContext<E: EthSpec> { - /// Slot to act as an identifier/safeguard - slot: Slot, - /// Proposer index of the block at `slot`. - proposer_index: Option<u64>, - /// Block root of the block at `slot`. - current_block_root: Option<Hash256>, - /// We keep the indexed attestations in the *in-memory* version of this struct so that we don't - /// need to regenerate them if roundtripping via this type *without* going to disk. - /// - /// They are not part of the on-disk format. - #[ssz(skip_serializing, skip_deserializing)] - indexed_attestations: HashMap<Hash256, IndexedAttestation<E>>, -} - -impl<E: EthSpec> OnDiskConsensusContext<E> { - pub fn from_consensus_context(ctxt: ConsensusContext<E>) -> Self { - // Match exhaustively on fields here so we are forced to *consider* updating the on-disk - // format when the `ConsensusContext` fields change. - let ConsensusContext { - slot, - previous_epoch: _, - current_epoch: _, - proposer_index, - current_block_root, - indexed_attestations, - } = ctxt; - OnDiskConsensusContext { - slot, - proposer_index, - current_block_root, - indexed_attestations, - } - } - - pub fn into_consensus_context(self) -> ConsensusContext<E> { - let OnDiskConsensusContext { - slot, - proposer_index, - current_block_root, - indexed_attestations, - } = self; - - let mut ctxt = ConsensusContext::new(slot); - - if let Some(proposer_index) = proposer_index { - ctxt = ctxt.set_proposer_index(proposer_index); - } - if let Some(block_root) = current_block_root { - ctxt = ctxt.set_current_block_root(block_root); - } - ctxt.set_indexed_attestations(indexed_attestations) - } -} diff --git a/beacon_node/store/src/lib.rs b/beacon_node/store/src/lib.rs index ee9cfce0ec..3363eb800c 100644 --- a/beacon_node/store/src/lib.rs +++ b/beacon_node/store/src/lib.rs @@ -9,7 +9,6 @@ //! tests for implementation examples. pub mod blob_sidecar_list_from_root; pub mod config; -pub mod consensus_context; pub mod errors; mod forwards_iter; pub mod hdiff; @@ -27,7 +26,6 @@ pub mod iter; pub use self::blob_sidecar_list_from_root::BlobSidecarListFromRoot; pub use self::config::StoreConfig; -pub use self::consensus_context::OnDiskConsensusContext; pub use self::hot_cold_store::{HotColdDB, HotStateSummary, Split}; pub use self::memory_store::MemoryStore; pub use crate::metadata::BlobInfo; From 945f6637c51b145558a68777c9b6feaf2c68caa2 Mon Sep 17 00:00:00 2001 From: Mac L <mjladson@pm.me> Date: Mon, 16 Feb 2026 20:05:54 +0400 Subject: [PATCH 22/81] Remove `reqwest` re-exports from `eth2` (#8829) Remove `reqwest` from being re-exported within `eth2` Co-Authored-By: Mac L <mjladson@pm.me> --- Cargo.lock | 5 +++++ beacon_node/builder_client/src/lib.rs | 4 ++-- beacon_node/http_api/Cargo.toml | 1 + beacon_node/http_api/src/lib.rs | 2 +- beacon_node/http_api/src/publish_blocks.rs | 10 ++++------ beacon_node/http_api/src/validator/mod.rs | 2 +- .../http_api/tests/broadcast_validation_tests.rs | 2 +- beacon_node/http_api/tests/status_tests.rs | 2 +- beacon_node/http_api/tests/tests.rs | 4 ++-- common/eth2/src/lib.rs | 4 +--- common/eth2/src/types.rs | 2 +- common/warp_utils/Cargo.toml | 1 + common/warp_utils/src/status_code.rs | 2 +- testing/node_test_rig/Cargo.toml | 1 + testing/node_test_rig/src/lib.rs | 3 ++- testing/validator_test_rig/Cargo.toml | 1 + testing/validator_test_rig/src/mock_beacon_node.rs | 3 ++- validator_client/src/lib.rs | 4 ++-- validator_client/validator_services/Cargo.toml | 1 + .../validator_services/src/block_service.rs | 3 ++- 20 files changed, 33 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5a63ab1e72..5a8e76a8a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4217,6 +4217,7 @@ dependencies = [ "parking_lot", "proto_array", "rand 0.9.2", + "reqwest", "safe_arith", "sensitive_url", "serde", @@ -6159,6 +6160,7 @@ dependencies = [ "environment", "eth2", "execution_layer", + "reqwest", "sensitive_url", "tempfile", "tokio", @@ -9688,6 +9690,7 @@ dependencies = [ "graffiti_file", "logging", "parking_lot", + "reqwest", "safe_arith", "slot_clock", "task_executor", @@ -9716,6 +9719,7 @@ dependencies = [ "eth2", "mockito", "regex", + "reqwest", "sensitive_url", "serde_json", "tracing", @@ -9810,6 +9814,7 @@ dependencies = [ "bytes", "eth2", "headers", + "reqwest", "safe_arith", "serde", "serde_array_query", diff --git a/beacon_node/builder_client/src/lib.rs b/beacon_node/builder_client/src/lib.rs index b17a824fd7..7dc0cbfc6d 100644 --- a/beacon_node/builder_client/src/lib.rs +++ b/beacon_node/builder_client/src/lib.rs @@ -10,10 +10,10 @@ use eth2::types::{ use eth2::types::{FullPayloadContents, SignedBlindedBeaconBlock}; use eth2::{ CONSENSUS_VERSION_HEADER, CONTENT_TYPE_HEADER, JSON_CONTENT_TYPE_HEADER, - SSZ_CONTENT_TYPE_HEADER, StatusCode, ok_or_error, success_or_error, + SSZ_CONTENT_TYPE_HEADER, ok_or_error, success_or_error, }; use reqwest::header::{ACCEPT, HeaderMap, HeaderValue}; -use reqwest::{IntoUrl, Response}; +use reqwest::{IntoUrl, Response, StatusCode}; use sensitive_url::SensitiveUrl; use serde::Serialize; use serde::de::DeserializeOwned; diff --git a/beacon_node/http_api/Cargo.toml b/beacon_node/http_api/Cargo.toml index 78e7af71f4..dd15a76c7a 100644 --- a/beacon_node/http_api/Cargo.toml +++ b/beacon_node/http_api/Cargo.toml @@ -33,6 +33,7 @@ operation_pool = { workspace = true } parking_lot = { workspace = true } proto_array = { workspace = true } rand = { workspace = true } +reqwest = { workspace = true } safe_arith = { workspace = true } sensitive_url = { workspace = true } serde = { workspace = true } diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index c4b2cded51..6824eab4fd 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -50,7 +50,6 @@ use builder_states::get_next_withdrawals; use bytes::Bytes; use context_deserialize::ContextDeserialize; use directory::DEFAULT_ROOT_DIR; -use eth2::StatusCode; use eth2::lighthouse::sync_state::SyncState; use eth2::types::{ self as api_types, BroadcastValidation, EndpointVersion, ForkChoice, ForkChoiceExtraData, @@ -69,6 +68,7 @@ use parking_lot::RwLock; pub use publish_blocks::{ ProvenancedBlock, publish_blinded_block, publish_block, reconstruct_block, }; +use reqwest::StatusCode; use serde::{Deserialize, Serialize}; use slot_clock::SlotClock; use ssz::Encode; diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index 7826ec55e1..bbf92a4dda 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -9,18 +9,16 @@ use beacon_chain::{ AvailabilityProcessingStatus, BeaconChain, BeaconChainError, BeaconChainTypes, BlockError, IntoGossipVerifiedBlock, NotifyExecutionLayer, build_blob_data_column_sidecars, }; -use eth2::{ - StatusCode, - types::{ - BlobsBundle, BroadcastValidation, ErrorMessage, ExecutionPayloadAndBlobs, - FullPayloadContents, PublishBlockRequest, SignedBlockContents, - }, +use eth2::types::{ + BlobsBundle, BroadcastValidation, ErrorMessage, ExecutionPayloadAndBlobs, FullPayloadContents, + PublishBlockRequest, SignedBlockContents, }; use execution_layer::{ProvenancedPayload, SubmitBlindedBlockResponse}; use futures::TryFutureExt; use lighthouse_network::PubsubMessage; use network::NetworkMessage; use rand::prelude::SliceRandom; +use reqwest::StatusCode; use std::marker::PhantomData; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; diff --git a/beacon_node/http_api/src/validator/mod.rs b/beacon_node/http_api/src/validator/mod.rs index b1ab4c648a..0704c52095 100644 --- a/beacon_node/http_api/src/validator/mod.rs +++ b/beacon_node/http_api/src/validator/mod.rs @@ -10,7 +10,6 @@ use beacon_chain::attestation_verification::VerifiedAttestation; use beacon_chain::validator_monitor::timestamp_now; use beacon_chain::{AttestationError, BeaconChain, BeaconChainError, BeaconChainTypes}; use bls::PublicKeyBytes; -use eth2::StatusCode; use eth2::types::{ Accept, BeaconCommitteeSubscription, EndpointVersion, Failure, GenericResponse, StandardLivenessResponseData, StateId as CoreStateId, ValidatorAggregateAttestationQuery, @@ -18,6 +17,7 @@ use eth2::types::{ }; use lighthouse_network::PubsubMessage; use network::{NetworkMessage, ValidatorSubscriptionMessage}; +use reqwest::StatusCode; use slot_clock::SlotClock; use std::sync::Arc; use tokio::sync::mpsc::{Sender, UnboundedSender}; diff --git a/beacon_node/http_api/tests/broadcast_validation_tests.rs b/beacon_node/http_api/tests/broadcast_validation_tests.rs index 357b78cf41..ef5c508595 100644 --- a/beacon_node/http_api/tests/broadcast_validation_tests.rs +++ b/beacon_node/http_api/tests/broadcast_validation_tests.rs @@ -4,11 +4,11 @@ use beacon_chain::{ GossipVerifiedBlock, IntoGossipVerifiedBlock, WhenSlotSkipped, test_utils::{AttestationStrategy, BlockStrategy}, }; -use eth2::reqwest::{Response, StatusCode}; use eth2::types::{BroadcastValidation, PublishBlockRequest}; use fixed_bytes::FixedBytesExtended; use http_api::test_utils::InteractiveTester; use http_api::{Config, ProvenancedBlock, publish_blinded_block, publish_block, reconstruct_block}; +use reqwest::{Response, StatusCode}; use std::collections::HashSet; use std::sync::Arc; use types::{ColumnIndex, Epoch, EthSpec, ForkName, Hash256, MainnetEthSpec, Slot}; diff --git a/beacon_node/http_api/tests/status_tests.rs b/beacon_node/http_api/tests/status_tests.rs index 556b75cb85..6bca9e51f6 100644 --- a/beacon_node/http_api/tests/status_tests.rs +++ b/beacon_node/http_api/tests/status_tests.rs @@ -3,9 +3,9 @@ use beacon_chain::{ BlockError, test_utils::{AttestationStrategy, BlockStrategy, LightClientStrategy, SyncCommitteeStrategy}, }; -use eth2::StatusCode; use execution_layer::{PayloadStatusV1, PayloadStatusV1Status}; use http_api::test_utils::InteractiveTester; +use reqwest::StatusCode; use types::{EthSpec, ExecPayload, ForkName, MinimalEthSpec, Slot, Uint256}; type E = MinimalEthSpec; diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 367a0e3f05..6787d1ab9e 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -10,9 +10,8 @@ use bls::{AggregateSignature, Keypair, PublicKeyBytes, Signature, SignatureBytes use eth2::{ BeaconNodeHttpClient, Error, Error::ServerMessage, - StatusCode, Timeouts, + Timeouts, mixin::{RequestAccept, ResponseForkName, ResponseOptional}, - reqwest::{RequestBuilder, Response}, types::{ BlockId as CoreBlockId, ForkChoiceNode, ProduceBlockV3Response, StateId as CoreStateId, *, }, @@ -34,6 +33,7 @@ use network::NetworkReceivers; use network_utils::enr_ext::EnrExt; use operation_pool::attestation_storage::CheckpointKey; use proto_array::ExecutionStatus; +use reqwest::{RequestBuilder, Response, StatusCode}; use sensitive_url::SensitiveUrl; use slot_clock::SlotClock; use ssz::BitList; diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 95744a4137..cdf63a3c67 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -22,8 +22,6 @@ pub use beacon_response::{ }; pub use self::error::{Error, ok_or_error, success_or_error}; -pub use reqwest; -pub use reqwest::{StatusCode, Url}; pub use sensitive_url::SensitiveUrl; use self::mixin::{RequestAccept, ResponseOptional}; @@ -38,7 +36,7 @@ use futures_util::StreamExt; #[cfg(feature = "network")] use libp2p_identity::PeerId; use reqwest::{ - Body, IntoUrl, RequestBuilder, Response, + Body, IntoUrl, RequestBuilder, Response, StatusCode, Url, header::{HeaderMap, HeaderValue}, }; #[cfg(feature = "events")] diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index 8b33a4dfb9..0c9c0b95f0 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -1604,7 +1604,7 @@ pub struct BroadcastValidationQuery { } pub mod serde_status_code { - use crate::StatusCode; + use reqwest::StatusCode; use serde::{Deserialize, Serialize, de::Error}; pub fn serialize<S>(status_code: &StatusCode, ser: S) -> Result<S::Ok, S::Error> diff --git a/common/warp_utils/Cargo.toml b/common/warp_utils/Cargo.toml index 32a540a69d..80bc247cbf 100644 --- a/common/warp_utils/Cargo.toml +++ b/common/warp_utils/Cargo.toml @@ -9,6 +9,7 @@ edition = { workspace = true } bytes = { workspace = true } eth2 = { workspace = true } headers = "0.3.2" +reqwest = { workspace = true } safe_arith = { workspace = true } serde = { workspace = true } serde_array_query = "0.1.0" diff --git a/common/warp_utils/src/status_code.rs b/common/warp_utils/src/status_code.rs index 1b05297359..a654b6d2c5 100644 --- a/common/warp_utils/src/status_code.rs +++ b/common/warp_utils/src/status_code.rs @@ -1,4 +1,4 @@ -use eth2::StatusCode; +use reqwest::StatusCode; use warp::Rejection; /// Convert from a "new" `http::StatusCode` to a `warp` compatible one. diff --git a/testing/node_test_rig/Cargo.toml b/testing/node_test_rig/Cargo.toml index 0d9db528da..21ec6fac12 100644 --- a/testing/node_test_rig/Cargo.toml +++ b/testing/node_test_rig/Cargo.toml @@ -10,6 +10,7 @@ beacon_node_fallback = { workspace = true } environment = { workspace = true } eth2 = { workspace = true } execution_layer = { workspace = true } +reqwest = { workspace = true } sensitive_url = { workspace = true } tempfile = { workspace = true } tokio = { workspace = true } diff --git a/testing/node_test_rig/src/lib.rs b/testing/node_test_rig/src/lib.rs index ece6001802..76a5b7ddb2 100644 --- a/testing/node_test_rig/src/lib.rs +++ b/testing/node_test_rig/src/lib.rs @@ -4,7 +4,8 @@ use beacon_node::ProductionBeaconNode; use environment::RuntimeContext; -use eth2::{BeaconNodeHttpClient, Timeouts, reqwest::ClientBuilder}; +use eth2::{BeaconNodeHttpClient, Timeouts}; +use reqwest::ClientBuilder; use sensitive_url::SensitiveUrl; use std::path::PathBuf; use std::time::Duration; diff --git a/testing/validator_test_rig/Cargo.toml b/testing/validator_test_rig/Cargo.toml index f28a423433..2057a9fdc8 100644 --- a/testing/validator_test_rig/Cargo.toml +++ b/testing/validator_test_rig/Cargo.toml @@ -7,6 +7,7 @@ edition = { workspace = true } eth2 = { workspace = true } mockito = { workspace = true } regex = { workspace = true } +reqwest = { workspace = true } sensitive_url = { workspace = true } serde_json = { workspace = true } tracing = { 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 ff1e772d54..1ecdd85f3b 100644 --- a/testing/validator_test_rig/src/mock_beacon_node.rs +++ b/testing/validator_test_rig/src/mock_beacon_node.rs @@ -1,7 +1,8 @@ use eth2::types::{GenericResponse, SyncingData}; -use eth2::{BeaconNodeHttpClient, StatusCode, Timeouts}; +use eth2::{BeaconNodeHttpClient, Timeouts}; use mockito::{Matcher, Mock, Server, ServerGuard}; use regex::Regex; +use reqwest::StatusCode; use sensitive_url::SensitiveUrl; use std::marker::PhantomData; use std::str::FromStr; diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index c0d561b175..f70d5830ec 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -19,11 +19,11 @@ use beacon_node_fallback::{ use clap::ArgMatches; use doppelganger_service::DoppelgangerService; use environment::RuntimeContext; -use eth2::{BeaconNodeHttpClient, StatusCode, Timeouts, reqwest::ClientBuilder}; +use eth2::{BeaconNodeHttpClient, Timeouts}; use initialized_validators::Error::UnableToOpenVotingKeystore; use lighthouse_validator_store::LighthouseValidatorStore; use parking_lot::RwLock; -use reqwest::Certificate; +use reqwest::{Certificate, ClientBuilder, StatusCode}; use slot_clock::SlotClock; use slot_clock::SystemTimeSlotClock; use std::fs::File; diff --git a/validator_client/validator_services/Cargo.toml b/validator_client/validator_services/Cargo.toml index c914940914..2582968265 100644 --- a/validator_client/validator_services/Cargo.toml +++ b/validator_client/validator_services/Cargo.toml @@ -13,6 +13,7 @@ futures = { workspace = true } graffiti_file = { workspace = true } logging = { workspace = true } parking_lot = { workspace = true } +reqwest = { workspace = true } safe_arith = { workspace = true } slot_clock = { workspace = true } task_executor = { workspace = true } diff --git a/validator_client/validator_services/src/block_service.rs b/validator_client/validator_services/src/block_service.rs index 625f8db7cb..df4c9b223c 100644 --- a/validator_client/validator_services/src/block_service.rs +++ b/validator_client/validator_services/src/block_service.rs @@ -1,9 +1,10 @@ use beacon_node_fallback::{ApiTopic, BeaconNodeFallback, Error as FallbackError, Errors}; use bls::PublicKeyBytes; +use eth2::BeaconNodeHttpClient; use eth2::types::GraffitiPolicy; -use eth2::{BeaconNodeHttpClient, StatusCode}; use graffiti_file::{GraffitiFile, determine_graffiti}; use logging::crit; +use reqwest::StatusCode; use slot_clock::SlotClock; use std::fmt::Debug; use std::future::Future; From eec0700f94ee551484b6a39d596900ff53a8aba9 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi <eserilev@ucsc.edu> Date: Mon, 16 Feb 2026 18:09:35 -0800 Subject: [PATCH 23/81] Gloas local block building MVP (#8754) The flow for local block building is 1. Create execution payload and bid 2. Construct beacon block 3. Sign beacon block and publish 4. Sign execution payload and publish This PR adds the beacon block v4 flow , GET payload envelope and POST payload envelope (local block building only). The spec for these endpoints can be found here: https://github.com/ethereum/beacon-APIs/pull/552 and is subject to change. We needed a way to store the unsigned execution payload envelope associated to the execution payload bid that was included in the block. I introduced a new cache that stores these unsigned execution payload envelopes. the GET payload envelope queries this cache directly so that a proposer, after publishing a block, can fetch the payload envelope + sign and publish it. I kept payload signing and publishing within the validators block service to keep things simple for now. The idea was to build out a block production MVP for devnet 0, try not to affect any non gloas code paths and build things out in such a way that it will be easy to deprecate pre-gloas code paths later on (for example block production v2 and v3). We will eventually need to track which beacon node was queried for the block so that we can later query it for the payload. But thats not needed for the devnet. Co-Authored-By: Eitan Seri- Levi <eserilev@gmail.com> Co-Authored-By: Michael Sproul <michael@sigmaprime.io> Co-Authored-By: Jimmy Chen <jchen.tc@gmail.com> Co-Authored-By: Eitan Seri-Levi <eserilev@ucsc.edu> --- beacon_node/beacon_chain/src/beacon_chain.rs | 257 +---- .../src/block_production/gloas.rs | 890 ++++++++++++++++++ .../beacon_chain/src/block_production/mod.rs | 223 +++++ beacon_node/beacon_chain/src/builder.rs | 1 + beacon_node/beacon_chain/src/errors.rs | 2 +- beacon_node/beacon_chain/src/lib.rs | 2 + .../src/pending_payload_envelopes.rs | 151 +++ beacon_node/client/src/notifier.rs | 10 + beacon_node/execution_layer/src/lib.rs | 60 +- .../src/test_utils/mock_builder.rs | 2 +- .../src/beacon/execution_payload_envelope.rs | 116 +++ beacon_node/http_api/src/beacon/mod.rs | 1 + beacon_node/http_api/src/beacon/states.rs | 1 - beacon_node/http_api/src/lib.rs | 63 +- beacon_node/http_api/src/produce_block.rs | 84 +- .../validator/execution_payload_envelope.rs | 110 +++ beacon_node/http_api/src/validator/mod.rs | 41 +- beacon_node/http_api/tests/tests.rs | 247 ++++- common/eth2/src/lib.rs | 274 +++++- common/eth2/src/types.rs | 39 +- .../src/envelope_processing.rs | 38 +- consensus/types/src/builder/builder_bid.rs | 2 +- .../types/src/core/application_domain.rs | 1 + consensus/types/src/core/chain_spec.rs | 4 +- .../validator/validator_registration_data.rs | 2 +- testing/ef_tests/src/cases/operations.rs | 10 +- .../lighthouse_validator_store/src/lib.rs | 45 +- validator_client/signing_method/src/lib.rs | 5 + .../signing_method/src/web3signer.rs | 4 + .../validator_services/src/block_service.rs | 305 ++++-- validator_client/validator_store/src/lib.rs | 12 +- 31 files changed, 2656 insertions(+), 346 deletions(-) create mode 100644 beacon_node/beacon_chain/src/block_production/gloas.rs create mode 100644 beacon_node/beacon_chain/src/block_production/mod.rs create mode 100644 beacon_node/beacon_chain/src/pending_payload_envelopes.rs create mode 100644 beacon_node/http_api/src/beacon/execution_payload_envelope.rs create mode 100644 beacon_node/http_api/src/validator/execution_payload_envelope.rs diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 4ae7871758..d0f5297f1b 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -31,7 +31,7 @@ use crate::errors::{BeaconChainError as Error, BlockProductionError}; use crate::events::ServerSentEventHandler; use crate::execution_payload::{NotifyExecutionLayer, PreparePayloadHandle, get_execution_payload}; use crate::fetch_blobs::EngineGetBlobsOutput; -use crate::fork_choice_signal::{ForkChoiceSignalRx, ForkChoiceSignalTx, ForkChoiceWaitResult}; +use crate::fork_choice_signal::{ForkChoiceSignalRx, ForkChoiceSignalTx}; use crate::graffiti_calculator::{GraffitiCalculator, GraffitiSettings}; use crate::kzg_utils::reconstruct_blobs; use crate::light_client_finality_update_verification::{ @@ -56,6 +56,7 @@ use crate::observed_block_producers::ObservedBlockProducers; use crate::observed_data_sidecars::ObservedDataSidecars; use crate::observed_operations::{ObservationOutcome, ObservedOperations}; use crate::observed_slashable::ObservedSlashable; +use crate::pending_payload_envelopes::PendingPayloadEnvelopes; use crate::persisted_beacon_chain::PersistedBeaconChain; use crate::persisted_custody::persist_custody_context; use crate::persisted_fork_choice::PersistedForkChoice; @@ -235,7 +236,7 @@ pub struct PrePayloadAttributes { /// /// The parent block number is not part of the payload attributes sent to the EL, but *is* /// sent to builders via SSE. - pub parent_block_number: u64, + pub parent_block_number: Option<u64>, /// The block root of the block being built upon (same block as fcU `headBlockHash`). pub parent_beacon_block_root: Hash256, } @@ -419,6 +420,9 @@ pub struct BeaconChain<T: BeaconChainTypes> { RwLock<ObservedDataSidecars<DataColumnSidecar<T::EthSpec>, T::EthSpec>>, /// Maintains a record of slashable message seen over the gossip network or RPC. pub observed_slashable: RwLock<ObservedSlashable<T::EthSpec>>, + /// Cache of pending execution payload envelopes for local block building. + /// Envelopes are stored here during block production and eventually published. + pub pending_payload_envelopes: RwLock<PendingPayloadEnvelopes<T::EthSpec>>, /// Maintains a record of which validators have submitted voluntary exits. pub observed_voluntary_exits: Mutex<ObservedOperations<SignedVoluntaryExit, T::EthSpec>>, /// Maintains a record of which validators we've seen proposer slashings for. @@ -4504,55 +4508,6 @@ impl<T: BeaconChainTypes> BeaconChain<T> { Ok(()) } - /// If configured, wait for the fork choice run at the start of the slot to complete. - #[instrument(level = "debug", skip_all)] - fn wait_for_fork_choice_before_block_production( - self: &Arc<Self>, - slot: Slot, - ) -> Result<(), BlockProductionError> { - if let Some(rx) = &self.fork_choice_signal_rx { - let current_slot = self - .slot() - .map_err(|_| BlockProductionError::UnableToReadSlot)?; - - let timeout = Duration::from_millis(self.config.fork_choice_before_proposal_timeout_ms); - - if slot == current_slot || slot == current_slot + 1 { - match rx.wait_for_fork_choice(slot, timeout) { - ForkChoiceWaitResult::Success(fc_slot) => { - debug!( - %slot, - fork_choice_slot = %fc_slot, - "Fork choice successfully updated before block production" - ); - } - ForkChoiceWaitResult::Behind(fc_slot) => { - warn!( - fork_choice_slot = %fc_slot, - %slot, - message = "this block may be orphaned", - "Fork choice notifier out of sync with block production" - ); - } - ForkChoiceWaitResult::TimeOut => { - warn!( - message = "this block may be orphaned", - "Timed out waiting for fork choice before proposal" - ); - } - } - } else { - error!( - %slot, - %current_slot, - message = "check clock sync, this block may be orphaned", - "Producing block at incorrect slot" - ); - } - } - Ok(()) - } - pub async fn produce_block_with_verification( self: &Arc<Self>, randao_reveal: Signature, @@ -4599,165 +4554,6 @@ impl<T: BeaconChainTypes> BeaconChain<T> { .await } - /// Load a beacon state from the database for block production. This is a long-running process - /// that should not be performed in an `async` context. - fn load_state_for_block_production( - self: &Arc<Self>, - slot: Slot, - ) -> Result<(BeaconState<T::EthSpec>, Option<Hash256>), BlockProductionError> { - let fork_choice_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_FORK_CHOICE_TIMES); - self.wait_for_fork_choice_before_block_production(slot)?; - drop(fork_choice_timer); - - let state_load_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_STATE_LOAD_TIMES); - - // Atomically read some values from the head whilst avoiding holding cached head `Arc` any - // longer than necessary. - let (head_slot, head_block_root, head_state_root) = { - let head = self.canonical_head.cached_head(); - ( - head.head_slot(), - head.head_block_root(), - head.head_state_root(), - ) - }; - let (state, state_root_opt) = if head_slot < slot { - // Attempt an aggressive re-org if configured and the conditions are right. - if let Some((re_org_state, re_org_state_root)) = - self.get_state_for_re_org(slot, head_slot, head_block_root) - { - info!( - %slot, - head_to_reorg = %head_block_root, - "Proposing block to re-org current head" - ); - (re_org_state, Some(re_org_state_root)) - } else { - // Fetch the head state advanced through to `slot`, which should be present in the - // state cache thanks to the state advance timer. - let (state_root, state) = self - .store - .get_advanced_hot_state(head_block_root, slot, head_state_root) - .map_err(BlockProductionError::FailedToLoadState)? - .ok_or(BlockProductionError::UnableToProduceAtSlot(slot))?; - (state, Some(state_root)) - } - } else { - warn!( - message = "this block is more likely to be orphaned", - %slot, - "Producing block that conflicts with head" - ); - let state = self - .state_at_slot(slot - 1, StateSkipConfig::WithStateRoots) - .map_err(|_| BlockProductionError::UnableToProduceAtSlot(slot))?; - - (state, None) - }; - - drop(state_load_timer); - - Ok((state, state_root_opt)) - } - - /// Fetch the beacon state to use for producing a block if a 1-slot proposer re-org is viable. - /// - /// This function will return `None` if proposer re-orgs are disabled. - #[instrument(skip_all, level = "debug")] - fn get_state_for_re_org( - &self, - slot: Slot, - head_slot: Slot, - canonical_head: Hash256, - ) -> Option<(BeaconState<T::EthSpec>, Hash256)> { - let re_org_head_threshold = self.config.re_org_head_threshold?; - let re_org_parent_threshold = self.config.re_org_parent_threshold?; - - 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() - .or_else(|| { - warn!(error = "unable to read slot clock", "Not attempting re-org"); - None - })?; - - // Attempt a proposer re-org if: - // - // 1. It seems we have time to propagate and still receive the proposer boost. - // 2. The current head block was seen late. - // 3. The `get_proposer_head` conditions from fork choice pass. - let proposing_on_time = - slot_delay < self.config.re_org_cutoff(self.spec.get_slot_duration()); - if !proposing_on_time { - debug!(reason = "not proposing on time", "Not attempting re-org"); - return None; - } - - let head_late = self.block_observed_after_attestation_deadline(canonical_head, head_slot); - if !head_late { - debug!(reason = "head not late", "Not attempting re-org"); - return None; - } - - // Is the current head weak and appropriate for re-orging? - let proposer_head_timer = - metrics::start_timer(&metrics::BLOCK_PRODUCTION_GET_PROPOSER_HEAD_TIMES); - let proposer_head = self - .canonical_head - .fork_choice_read_lock() - .get_proposer_head( - slot, - canonical_head, - re_org_head_threshold, - re_org_parent_threshold, - &self.config.re_org_disallowed_offsets, - self.config.re_org_max_epochs_since_finalization, - ) - .map_err(|e| match e { - ProposerHeadError::DoNotReOrg(reason) => { - debug!( - %reason, - "Not attempting re-org" - ); - } - ProposerHeadError::Error(e) => { - warn!( - error = ?e, - "Not attempting re-org" - ); - } - }) - .ok()?; - drop(proposer_head_timer); - let re_org_parent_block = proposer_head.parent_node.root; - - let (state_root, state) = self - .store - .get_advanced_hot_state_from_cache(re_org_parent_block, slot) - .or_else(|| { - warn!(reason = "no state in cache", "Not attempting re-org"); - None - })?; - - info!( - weak_head = ?canonical_head, - parent = ?re_org_parent_block, - head_weight = proposer_head.head_node.weight, - threshold_weight = proposer_head.re_org_head_weight_threshold, - "Attempting re-org due to weak head" - ); - - Some((state, state_root)) - } - /// Get the proposer index and `prev_randao` value for a proposal at slot `proposal_slot`. /// /// The `proposer_head` may be the head block of `cached_head` or its parent. An error will @@ -4840,15 +4636,25 @@ impl<T: BeaconChainTypes> BeaconChain<T> { return Ok(None); }; - // Get the `prev_randao` and parent block number. - let head_block_number = cached_head.head_block_number()?; - let (prev_randao, parent_block_number) = if proposer_head == head_parent_block_root { - ( - cached_head.parent_random()?, - head_block_number.saturating_sub(1), - ) + // TODO(gloas) not sure what to do here see this issue + // https://github.com/sigp/lighthouse/issues/8817 + let (prev_randao, parent_block_number) = if self + .spec + .fork_name_at_slot::<T::EthSpec>(proposal_slot) + .gloas_enabled() + { + (cached_head.head_random()?, None) } else { - (cached_head.head_random()?, head_block_number) + // Get the `prev_randao` and parent block number. + let head_block_number = cached_head.head_block_number()?; + if proposer_head == head_parent_block_root { + ( + cached_head.parent_random()?, + Some(head_block_number.saturating_sub(1)), + ) + } else { + (cached_head.head_random()?, Some(head_block_number)) + } }; Ok(Some(PrePayloadAttributes { @@ -5093,7 +4899,11 @@ impl<T: BeaconChainTypes> BeaconChain<T> { } /// Check if the block with `block_root` was observed after the attestation deadline of `slot`. - fn block_observed_after_attestation_deadline(&self, block_root: Hash256, slot: Slot) -> bool { + pub(crate) fn block_observed_after_attestation_deadline( + &self, + block_root: Hash256, + slot: Slot, + ) -> bool { let block_delays = self.block_times_cache.read().get_block_delays( block_root, self.slot_clock @@ -5860,7 +5670,11 @@ impl<T: BeaconChainTypes> BeaconChain<T> { execution_payload_value, ) } - BeaconState::Gloas(_) => return Err(BlockProductionError::GloasNotImplemented), + BeaconState::Gloas(_) => { + return Err(BlockProductionError::GloasNotImplemented( + "Attempting to produce gloas beacn block via non gloas code path".to_owned(), + )); + } }; let block = SignedBeaconBlock::from_block( @@ -6198,13 +6012,14 @@ impl<T: BeaconChainTypes> BeaconChain<T> { // Push a server-sent event (probably to a block builder or relay). if let Some(event_handler) = &self.event_handler && event_handler.has_payload_attributes_subscribers() + && let Some(parent_block_number) = pre_payload_attributes.parent_block_number { event_handler.register(EventKind::PayloadAttributes(ForkVersionedResponse { data: SseExtendedPayloadAttributes { proposal_slot: prepare_slot, proposer_index: proposer, parent_block_root: head_root, - parent_block_number: pre_payload_attributes.parent_block_number, + parent_block_number, parent_block_hash: forkchoice_update_params.head_hash.unwrap_or_default(), payload_attributes: payload_attributes.into(), }, diff --git a/beacon_node/beacon_chain/src/block_production/gloas.rs b/beacon_node/beacon_chain/src/block_production/gloas.rs new file mode 100644 index 0000000000..025cf21a73 --- /dev/null +++ b/beacon_node/beacon_chain/src/block_production/gloas.rs @@ -0,0 +1,890 @@ +use std::collections::HashMap; +use std::marker::PhantomData; +use std::sync::Arc; + +use bls::Signature; +use execution_layer::{ + BlockProposalContentsGloas, BuilderParams, PayloadAttributes, PayloadParameters, +}; +use operation_pool::CompactAttestationRef; +use ssz::Encode; +use state_processing::common::get_attesting_indices_from_state; +use state_processing::envelope_processing::{VerifyStateRoot, process_execution_payload_envelope}; +use state_processing::epoch_cache::initialize_epoch_cache; +use state_processing::per_block_processing::{ + compute_timestamp_at_slot, get_expected_withdrawals, verify_attestation_for_block_inclusion, +}; +use state_processing::{ + BlockSignatureStrategy, ConsensusContext, VerifyBlockRoot, VerifySignatures, +}; +use state_processing::{VerifyOperation, state_advance::complete_state_advance}; +use task_executor::JoinHandle; +use tracing::{Instrument, Span, debug, debug_span, error, instrument, trace, warn}; +use tree_hash::TreeHash; +use types::consts::gloas::BUILDER_INDEX_SELF_BUILD; +use types::{ + Address, Attestation, AttestationElectra, AttesterSlashing, AttesterSlashingElectra, + BeaconBlock, BeaconBlockBodyGloas, BeaconBlockGloas, BeaconState, BeaconStateError, + BuilderIndex, Deposit, Eth1Data, EthSpec, ExecutionBlockHash, ExecutionPayloadBid, + ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequests, FullPayload, Graffiti, + Hash256, PayloadAttestation, ProposerSlashing, RelativeEpoch, SignedBeaconBlock, + SignedBlsToExecutionChange, SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, + SignedVoluntaryExit, Slot, SyncAggregate, Withdrawal, Withdrawals, +}; + +use crate::{ + BeaconChain, BeaconChainError, BeaconChainTypes, BlockProductionError, + ProduceBlockVerification, graffiti_calculator::GraffitiSettings, metrics, +}; + +pub const BID_VALUE_SELF_BUILD: u64 = 0; +pub const EXECUTION_PAYMENT_TRUSTLESS_BUILD: u64 = 0; + +type ConsensusBlockValue = u64; +type BlockProductionResult<E> = (BeaconBlock<E, FullPayload<E>>, ConsensusBlockValue); + +pub type PreparePayloadResult<E> = Result<BlockProposalContentsGloas<E>, BlockProductionError>; +pub type PreparePayloadHandle<E> = JoinHandle<Option<PreparePayloadResult<E>>>; + +pub struct PartialBeaconBlock<E: EthSpec> { + slot: Slot, + proposer_index: u64, + parent_root: Hash256, + randao_reveal: Signature, + eth1_data: Eth1Data, + graffiti: Graffiti, + proposer_slashings: Vec<ProposerSlashing>, + attester_slashings: Vec<AttesterSlashingElectra<E>>, + attestations: Vec<AttestationElectra<E>>, + payload_attestations: Vec<PayloadAttestation<E>>, + deposits: Vec<Deposit>, + voluntary_exits: Vec<SignedVoluntaryExit>, + sync_aggregate: Option<SyncAggregate<E>>, + bls_to_execution_changes: Vec<SignedBlsToExecutionChange>, +} + +/// Data needed to construct an ExecutionPayloadEnvelope. +/// The envelope requires the beacon_block_root which can only be computed after the block exists. +pub struct ExecutionPayloadData<E: types::EthSpec> { + pub payload: ExecutionPayloadGloas<E>, + pub execution_requests: ExecutionRequests<E>, + pub builder_index: BuilderIndex, + pub slot: Slot, +} + +impl<T: BeaconChainTypes> BeaconChain<T> { + pub async fn produce_block_with_verification_gloas( + self: &Arc<Self>, + randao_reveal: Signature, + slot: Slot, + graffiti_settings: GraffitiSettings, + verification: ProduceBlockVerification, + _builder_boost_factor: Option<u64>, + ) -> Result<BlockProductionResult<T::EthSpec>, BlockProductionError> { + metrics::inc_counter(&metrics::BLOCK_PRODUCTION_REQUESTS); + let _complete_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_TIMES); + // Part 1/2 (blocking) + // + // Load the parent state from disk. + let chain = self.clone(); + let span = Span::current(); + let (state, state_root_opt) = self + .task_executor + .spawn_blocking_handle( + move || { + let _guard = + debug_span!(parent: span, "load_state_for_block_production").entered(); + chain.load_state_for_block_production(slot) + }, + "load_state_for_block_production", + ) + .ok_or(BlockProductionError::ShuttingDown)? + .await + .map_err(BlockProductionError::TokioJoin)??; + + // Part 2/2 (async, with some blocking components) + // + // Produce the block upon the state + self.produce_block_on_state_gloas( + state, + state_root_opt, + slot, + randao_reveal, + graffiti_settings, + verification, + ) + .await + } + + // TODO(gloas) need to implement builder boost factor logic + #[instrument(level = "debug", skip_all)] + pub async fn produce_block_on_state_gloas( + self: &Arc<Self>, + state: BeaconState<T::EthSpec>, + state_root_opt: Option<Hash256>, + produce_at_slot: Slot, + randao_reveal: Signature, + graffiti_settings: GraffitiSettings, + verification: ProduceBlockVerification, + ) -> Result<BlockProductionResult<T::EthSpec>, BlockProductionError> { + // Part 1/3 (blocking) + // + // Perform the state advance and block-packing functions. + let chain = self.clone(); + let graffiti = self + .graffiti_calculator + .get_graffiti(graffiti_settings) + .await; + let span = Span::current(); + let (partial_beacon_block, state) = self + .task_executor + .spawn_blocking_handle( + move || { + let _guard = + debug_span!(parent: span, "produce_partial_beacon_block_gloas").entered(); + chain.produce_partial_beacon_block_gloas( + state, + state_root_opt, + produce_at_slot, + randao_reveal, + graffiti, + ) + }, + "produce_partial_beacon_block_gloas", + ) + .ok_or(BlockProductionError::ShuttingDown)? + .await + .map_err(BlockProductionError::TokioJoin)??; + + // Part 2/3 (async) + // + // Produce the execution payload bid. + // TODO(gloas) this is strictly for building local bids + // We'll need to build out trustless/trusted bid paths. + let (execution_payload_bid, state, payload_data) = self + .clone() + .produce_execution_payload_bid( + state, + produce_at_slot, + BID_VALUE_SELF_BUILD, + BUILDER_INDEX_SELF_BUILD, + ) + .await?; + + // Part 3/3 (blocking) + // + // Complete the block with the execution payload bid. + let chain = self.clone(); + let span = Span::current(); + self.task_executor + .spawn_blocking_handle( + move || { + let _guard = + debug_span!(parent: span, "complete_partial_beacon_block_gloas").entered(); + chain.complete_partial_beacon_block_gloas( + partial_beacon_block, + execution_payload_bid, + payload_data, + state, + verification, + ) + }, + "complete_partial_beacon_block_gloas", + ) + .ok_or(BlockProductionError::ShuttingDown)? + .await + .map_err(BlockProductionError::TokioJoin)? + } + + #[allow(clippy::too_many_arguments)] + #[allow(clippy::type_complexity)] + fn produce_partial_beacon_block_gloas( + self: &Arc<Self>, + mut state: BeaconState<T::EthSpec>, + state_root_opt: Option<Hash256>, + produce_at_slot: Slot, + randao_reveal: Signature, + graffiti: Graffiti, + ) -> Result<(PartialBeaconBlock<T::EthSpec>, BeaconState<T::EthSpec>), BlockProductionError> + { + // It is invalid to try to produce a block using a state from a future slot. + if state.slot() > produce_at_slot { + return Err(BlockProductionError::StateSlotTooHigh { + produce_at_slot, + state_slot: state.slot(), + }); + } + + let slot_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_SLOT_PROCESS_TIMES); + + // Ensure the state has performed a complete transition into the required slot. + complete_state_advance(&mut state, state_root_opt, produce_at_slot, &self.spec)?; + + drop(slot_timer); + + state.build_committee_cache(RelativeEpoch::Current, &self.spec)?; + state.apply_pending_mutations()?; + + let parent_root = if state.slot() > 0 { + *state + .get_block_root(state.slot() - 1) + .map_err(|_| BlockProductionError::UnableToGetBlockRootFromState)? + } else { + state.latest_block_header().canonical_root() + }; + + let proposer_index = state.get_beacon_proposer_index(state.slot(), &self.spec)? as u64; + + let slashings_and_exits_span = debug_span!("get_slashings_and_exits").entered(); + let (mut proposer_slashings, mut attester_slashings, mut voluntary_exits) = + self.op_pool.get_slashings_and_exits(&state, &self.spec); + + drop(slashings_and_exits_span); + + let eth1_data = state.eth1_data().clone(); + + let deposits = vec![]; + + let bls_changes_span = debug_span!("get_bls_to_execution_changes").entered(); + let bls_to_execution_changes = self + .op_pool + .get_bls_to_execution_changes(&state, &self.spec); + drop(bls_changes_span); + + // Iterate through the naive aggregation pool and ensure all the attestations from there + // are included in the operation pool. + { + let _guard = debug_span!("import_naive_aggregation_pool").entered(); + let _unagg_import_timer = + metrics::start_timer(&metrics::BLOCK_PRODUCTION_UNAGGREGATED_TIMES); + for attestation in self.naive_aggregation_pool.read().iter() { + let import = |attestation: &Attestation<T::EthSpec>| { + let attesting_indices = + get_attesting_indices_from_state(&state, attestation.to_ref())?; + self.op_pool + .insert_attestation(attestation.clone(), attesting_indices) + }; + if let Err(e) = import(attestation) { + // Don't stop block production if there's an error, just create a log. + error!( + reason = ?e, + "Attestation did not transfer to op pool" + ); + } + } + }; + + let mut attestations = { + let _guard = debug_span!("pack_attestations").entered(); + let _attestation_packing_timer = + metrics::start_timer(&metrics::BLOCK_PRODUCTION_ATTESTATION_TIMES); + + // Epoch cache and total balance cache are required for op pool packing. + state.build_total_active_balance_cache(&self.spec)?; + initialize_epoch_cache(&mut state, &self.spec)?; + + let mut prev_filter_cache = HashMap::new(); + let prev_attestation_filter = |att: &CompactAttestationRef<T::EthSpec>| { + self.filter_op_pool_attestation(&mut prev_filter_cache, att, &state) + }; + let mut curr_filter_cache = HashMap::new(); + let curr_attestation_filter = |att: &CompactAttestationRef<T::EthSpec>| { + self.filter_op_pool_attestation(&mut curr_filter_cache, att, &state) + }; + + self.op_pool + .get_attestations( + &state, + prev_attestation_filter, + curr_attestation_filter, + &self.spec, + ) + .map_err(BlockProductionError::OpPoolError)? + }; + + // If paranoid mode is enabled re-check the signatures of every included message. + // This will be a lot slower but guards against bugs in block production and can be + // quickly rolled out without a release. + if self.config.paranoid_block_proposal { + let mut tmp_ctxt = ConsensusContext::new(state.slot()); + attestations.retain(|att| { + verify_attestation_for_block_inclusion( + &state, + att.to_ref(), + &mut tmp_ctxt, + VerifySignatures::True, + &self.spec, + ) + .map_err(|e| { + warn!( + err = ?e, + block_slot = %state.slot(), + attestation = ?att, + "Attempted to include an invalid attestation" + ); + }) + .is_ok() + }); + + proposer_slashings.retain(|slashing| { + slashing + .clone() + .validate(&state, &self.spec) + .map_err(|e| { + warn!( + err = ?e, + block_slot = %state.slot(), + ?slashing, + "Attempted to include an invalid proposer slashing" + ); + }) + .is_ok() + }); + + attester_slashings.retain(|slashing| { + slashing + .clone() + .validate(&state, &self.spec) + .map_err(|e| { + warn!( + err = ?e, + block_slot = %state.slot(), + ?slashing, + "Attempted to include an invalid attester slashing" + ); + }) + .is_ok() + }); + + voluntary_exits.retain(|exit| { + exit.clone() + .validate(&state, &self.spec) + .map_err(|e| { + warn!( + err = ?e, + block_slot = %state.slot(), + ?exit, + "Attempted to include an invalid proposer slashing" + ); + }) + .is_ok() + }); + + // TODO(gloas) verifiy payload attestation signature here as well + } + + let attester_slashings = attester_slashings + .into_iter() + .filter_map(|a| match a { + AttesterSlashing::Base(_) => None, + AttesterSlashing::Electra(a) => Some(a), + }) + .collect::<Vec<_>>(); + + let attestations = attestations + .into_iter() + .filter_map(|a| match a { + Attestation::Base(_) => None, + Attestation::Electra(a) => Some(a), + }) + .collect::<Vec<_>>(); + + let slot = state.slot(); + + let sync_aggregate = if matches!(&state, BeaconState::Base(_)) { + None + } else { + let sync_aggregate = self + .op_pool + .get_sync_aggregate(&state) + .map_err(BlockProductionError::OpPoolError)? + .unwrap_or_else(|| { + warn!( + slot = %state.slot(), + "Producing block with no sync contributions" + ); + SyncAggregate::new() + }); + Some(sync_aggregate) + }; + + Ok(( + PartialBeaconBlock { + slot, + proposer_index, + parent_root, + randao_reveal, + eth1_data, + graffiti, + proposer_slashings, + attester_slashings, + attestations, + deposits, + voluntary_exits, + sync_aggregate, + // TODO(gloas) need to implement payload attestations + payload_attestations: vec![], + bls_to_execution_changes, + }, + state, + )) + } + + #[allow(clippy::type_complexity)] + fn complete_partial_beacon_block_gloas( + &self, + partial_beacon_block: PartialBeaconBlock<T::EthSpec>, + signed_execution_payload_bid: SignedExecutionPayloadBid<T::EthSpec>, + payload_data: Option<ExecutionPayloadData<T::EthSpec>>, + mut state: BeaconState<T::EthSpec>, + verification: ProduceBlockVerification, + ) -> Result<(BeaconBlock<T::EthSpec, FullPayload<T::EthSpec>>, u64), BlockProductionError> { + let PartialBeaconBlock { + slot, + proposer_index, + parent_root, + randao_reveal, + eth1_data, + graffiti, + proposer_slashings, + attester_slashings, + attestations, + deposits, + voluntary_exits, + sync_aggregate, + payload_attestations, + bls_to_execution_changes, + } = partial_beacon_block; + + let beacon_block = match &state { + BeaconState::Base(_) + | BeaconState::Altair(_) + | BeaconState::Bellatrix(_) + | BeaconState::Capella(_) + | BeaconState::Deneb(_) + | BeaconState::Electra(_) + | BeaconState::Fulu(_) => { + return Err(BlockProductionError::InvalidBlockVariant( + "Cannot construct a block pre-Gloas".to_owned(), + )); + } + BeaconState::Gloas(_) => BeaconBlock::Gloas(BeaconBlockGloas { + slot, + proposer_index, + parent_root, + state_root: Hash256::ZERO, + body: BeaconBlockBodyGloas { + randao_reveal, + eth1_data, + graffiti, + proposer_slashings: proposer_slashings + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + attester_slashings: attester_slashings + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + attestations: attestations + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + deposits: deposits + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + voluntary_exits: voluntary_exits + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + sync_aggregate: sync_aggregate + .ok_or(BlockProductionError::MissingSyncAggregate)?, + bls_to_execution_changes: bls_to_execution_changes + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + signed_execution_payload_bid, + payload_attestations: payload_attestations + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + _phantom: PhantomData::<FullPayload<T::EthSpec>>, + }, + }), + }; + + let signed_beacon_block = SignedBeaconBlock::from_block( + beacon_block, + // The block is not signed here, that is the task of a validator client. + Signature::empty(), + ); + + let block_size = signed_beacon_block.ssz_bytes_len(); + debug!(%block_size, "Produced block on state"); + + metrics::observe(&metrics::BLOCK_SIZE, block_size as f64); + + if block_size > self.config.max_network_size { + return Err(BlockProductionError::BlockTooLarge(block_size)); + } + + let process_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_PROCESS_TIMES); + let signature_strategy = match verification { + ProduceBlockVerification::VerifyRandao => BlockSignatureStrategy::VerifyRandao, + ProduceBlockVerification::NoVerification => BlockSignatureStrategy::NoVerification, + }; + + // Use a context without block root or proposer index so that both are checked. + let mut ctxt = ConsensusContext::new(signed_beacon_block.slot()); + + let consensus_block_value = self + .compute_beacon_block_reward(signed_beacon_block.message(), &mut state) + .map(|reward| reward.total) + .unwrap_or(0); + + state_processing::per_block_processing( + &mut state, + &signed_beacon_block, + signature_strategy, + VerifyBlockRoot::True, + &mut ctxt, + &self.spec, + )?; + drop(process_timer); + + let state_root_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_STATE_ROOT_TIMES); + + let state_root = state.update_tree_hash_cache()?; + + drop(state_root_timer); + + let (mut block, _) = signed_beacon_block.deconstruct(); + *block.state_root_mut() = state_root; + + // Construct and cache the ExecutionPayloadEnvelope if we have payload data. + // For local building, we always have payload data. + // For trustless building, the builder will provide the envelope separately. + if let Some(payload_data) = payload_data { + let beacon_block_root = block.tree_hash_root(); + let execution_payload_envelope = ExecutionPayloadEnvelope { + payload: payload_data.payload, + execution_requests: payload_data.execution_requests, + builder_index: payload_data.builder_index, + beacon_block_root, + slot: payload_data.slot, + state_root: Hash256::ZERO, + }; + + let mut signed_envelope = SignedExecutionPayloadEnvelope { + message: execution_payload_envelope, + signature: Signature::empty(), + }; + + // TODO(gloas) add better error variant + // We skip state root verification here because the relevant state root + // cant be calculated until after the new block has been constructed. + process_execution_payload_envelope( + &mut state, + None, + &signed_envelope, + VerifySignatures::False, + VerifyStateRoot::False, + &self.spec, + ) + .map_err(|_| { + BlockProductionError::GloasNotImplemented( + "process_execution_payload_envelope failed".to_owned(), + ) + })?; + + signed_envelope.message.state_root = state.update_tree_hash_cache()?; + + // Cache the envelope for later retrieval by the validator for signing and publishing. + let envelope_slot = payload_data.slot; + // TODO(gloas) might be safer to cache by root instead of by slot. + // We should revisit this once this code path + beacon api spec matures + self.pending_payload_envelopes + .write() + .insert(envelope_slot, signed_envelope.message); + + debug!( + %beacon_block_root, + slot = %envelope_slot, + "Cached pending execution payload envelope" + ); + } + + metrics::inc_counter(&metrics::BLOCK_PRODUCTION_SUCCESSES); + + trace!( + parent = ?block.parent_root(), + attestations = block.body().attestations_len(), + slot = %block.slot(), + "Produced beacon block" + ); + + Ok((block, consensus_block_value)) + } + + // TODO(gloas) introduce `ProposerPreferences` so we can build out trustless + // bid building. Right now this only works for local building. + /// Produce an `ExecutionPayloadBid` for some `slot` upon the given `state`. + /// This function assumes we've already advanced `state`. + /// + /// Returns the signed bid, the state, and optionally the payload data needed to construct + /// the `ExecutionPayloadEnvelope` after the beacon block is created. + /// + /// For local building, payload data is always returned (`Some`). + /// For trustless building, the builder provides the envelope separately, so `None` is returned. + #[allow(clippy::type_complexity)] + #[instrument(level = "debug", skip_all)] + pub async fn produce_execution_payload_bid( + self: Arc<Self>, + mut state: BeaconState<T::EthSpec>, + produce_at_slot: Slot, + bid_value: u64, + builder_index: BuilderIndex, + ) -> Result< + ( + SignedExecutionPayloadBid<T::EthSpec>, + BeaconState<T::EthSpec>, + Option<ExecutionPayloadData<T::EthSpec>>, + ), + BlockProductionError, + > { + // TODO(gloas) For non local building, add sanity check on value + // The builder MUST have enough excess balance to fulfill this bid (i.e. `value`) and all pending payments. + + // TODO(gloas) add metrics for execution payload bid production + + let parent_root = if state.slot() > 0 { + *state + .get_block_root(state.slot() - 1) + .map_err(|_| BlockProductionError::UnableToGetBlockRootFromState)? + } else { + state.latest_block_header().canonical_root() + }; + + let proposer_index = state.get_beacon_proposer_index(state.slot(), &self.spec)? as u64; + + let pubkey = state + .validators() + .get(proposer_index as usize) + .map(|v| v.pubkey) + .ok_or(BlockProductionError::BeaconChain(Box::new( + BeaconChainError::ValidatorIndexUnknown(proposer_index as usize), + )))?; + + let builder_params = BuilderParams { + pubkey, + slot: state.slot(), + chain_health: self + .is_healthy(&parent_root) + .map_err(|e| BlockProductionError::BeaconChain(Box::new(e)))?, + }; + + // TODO(gloas) this should be BlockProductionVersion::V4 + // V3 is okay for now as long as we're not connected to a builder + // TODO(gloas) add builder boost factor + let prepare_payload_handle = get_execution_payload_gloas( + self.clone(), + &state, + parent_root, + proposer_index, + builder_params, + )?; + + let block_proposal_contents = prepare_payload_handle + .await + .map_err(BlockProductionError::TokioJoin)? + .ok_or(BlockProductionError::ShuttingDown)??; + + let BlockProposalContentsGloas { + payload, + payload_value: _, + execution_requests, + blob_kzg_commitments, + blobs_and_proofs: _, + } = block_proposal_contents; + + let state_root = state.update_tree_hash_cache()?; + + // TODO(gloas) since we are defaulting to local building, execution payment is 0 + // execution payment should only be set to > 0 for trusted building. + let bid = ExecutionPayloadBid::<T::EthSpec> { + parent_block_hash: state.latest_block_hash()?.to_owned(), + parent_block_root: state.get_latest_block_root(state_root), + block_hash: payload.block_hash, + prev_randao: payload.prev_randao, + fee_recipient: Address::ZERO, + gas_limit: payload.gas_limit, + builder_index, + slot: produce_at_slot, + value: bid_value, + execution_payment: EXECUTION_PAYMENT_TRUSTLESS_BUILD, + blob_kzg_commitments, + }; + + // Store payload data for envelope construction after block is created + let payload_data = ExecutionPayloadData { + payload, + execution_requests, + builder_index, + slot: produce_at_slot, + }; + + // TODO(gloas) this is only local building + // we'll need to implement builder signature for the trustless path + Ok(( + SignedExecutionPayloadBid { + message: bid, + // TODO(gloas) return better error variant here + signature: Signature::infinity().map_err(|_| { + BlockProductionError::GloasNotImplemented( + "Failed to generate infinity signature".to_owned(), + ) + })?, + }, + state, + // Local building always returns payload data. + // Trustless building would return None here. + Some(payload_data), + )) + } +} + +/// Gets an execution payload for inclusion in a block. +/// +/// ## Errors +/// +/// Will return an error when using a pre-Gloas `state`. Ensure to only run this function +/// after the Gloas fork. +/// +/// ## Specification +/// +/// Equivalent to the `get_execution_payload` function in the Validator Guide: +/// +/// https://github.com/ethereum/consensus-specs/blob/v1.1.5/specs/merge/validator.md#block-proposal +fn get_execution_payload_gloas<T: BeaconChainTypes>( + chain: Arc<BeaconChain<T>>, + state: &BeaconState<T::EthSpec>, + parent_beacon_block_root: Hash256, + proposer_index: u64, + builder_params: BuilderParams, +) -> Result<PreparePayloadHandle<T::EthSpec>, BlockProductionError> { + // Compute all required values from the `state` now to avoid needing to pass it into a spawned + // task. + let spec = &chain.spec; + let current_epoch = state.current_epoch(); + let timestamp = + compute_timestamp_at_slot(state, state.slot(), spec).map_err(BeaconStateError::from)?; + let random = *state.get_randao_mix(current_epoch)?; + + let latest_execution_block_hash = *state.latest_block_hash()?; + let latest_gas_limit = state.latest_execution_payload_bid()?.gas_limit; + + let withdrawals = + Withdrawals::<T::EthSpec>::from(get_expected_withdrawals(state, spec)?).into(); + + // Spawn a task to obtain the execution payload from the EL via a series of async calls. The + // `join_handle` can be used to await the result of the function. + let join_handle = chain + .task_executor + .clone() + .spawn_handle( + async move { + prepare_execution_payload::<T>( + &chain, + timestamp, + random, + proposer_index, + latest_execution_block_hash, + latest_gas_limit, + builder_params, + withdrawals, + parent_beacon_block_root, + ) + .await + } + .instrument(debug_span!("prepare_execution_payload")), + "prepare_execution_payload", + ) + .ok_or(BlockProductionError::ShuttingDown)?; + + Ok(join_handle) +} + +/// Prepares an execution payload for inclusion in a block. +/// +/// ## Errors +/// +/// Will return an error when using a pre-Gloas fork `state`. Ensure to only run this function +/// after the Gloas fork. +/// +/// ## Specification +/// +/// Equivalent to the `prepare_execution_payload` function in the Validator Guide: +/// +/// https://github.com/ethereum/consensus-specs/blob/v1.1.5/specs/merge/validator.md#block-proposal +#[allow(clippy::too_many_arguments)] +async fn prepare_execution_payload<T>( + chain: &Arc<BeaconChain<T>>, + timestamp: u64, + random: Hash256, + proposer_index: u64, + parent_block_hash: ExecutionBlockHash, + parent_gas_limit: u64, + builder_params: BuilderParams, + withdrawals: Vec<Withdrawal>, + parent_beacon_block_root: Hash256, +) -> Result<BlockProposalContentsGloas<T::EthSpec>, BlockProductionError> +where + T: BeaconChainTypes, +{ + let spec = &chain.spec; + let fork = spec.fork_name_at_slot::<T::EthSpec>(builder_params.slot); + let execution_layer = chain + .execution_layer + .as_ref() + .ok_or(BlockProductionError::ExecutionLayerMissing)?; + + // Try to obtain the fork choice update parameters from the cached head. + // + // Use a blocking task to interact with the `canonical_head` lock otherwise we risk blocking the + // core `tokio` executor. + let inner_chain = chain.clone(); + let forkchoice_update_params = chain + .spawn_blocking_handle( + move || { + inner_chain + .canonical_head + .cached_head() + .forkchoice_update_parameters() + }, + "prepare_execution_payload_forkchoice_update_params", + ) + .instrument(debug_span!("forkchoice_update_params")) + .await + .map_err(|e| BlockProductionError::BeaconChain(Box::new(e)))?; + + let suggested_fee_recipient = execution_layer + .get_suggested_fee_recipient(proposer_index) + .await; + let payload_attributes = PayloadAttributes::new( + timestamp, + random, + suggested_fee_recipient, + Some(withdrawals), + Some(parent_beacon_block_root), + ); + + let target_gas_limit = execution_layer.get_proposer_gas_limit(proposer_index).await; + let payload_parameters = PayloadParameters { + parent_hash: parent_block_hash, + parent_gas_limit, + proposer_gas_limit: target_gas_limit, + payload_attributes: &payload_attributes, + forkchoice_update_params: &forkchoice_update_params, + current_fork: fork, + }; + + let block_contents = execution_layer + .get_payload_gloas(payload_parameters) + .await + .map_err(BlockProductionError::GetPayloadFailed)?; + + Ok(block_contents) +} diff --git a/beacon_node/beacon_chain/src/block_production/mod.rs b/beacon_node/beacon_chain/src/block_production/mod.rs new file mode 100644 index 0000000000..76c8b77e93 --- /dev/null +++ b/beacon_node/beacon_chain/src/block_production/mod.rs @@ -0,0 +1,223 @@ +use std::{sync::Arc, time::Duration}; + +use proto_array::ProposerHeadError; +use slot_clock::SlotClock; +use tracing::{debug, error, info, instrument, warn}; +use types::{BeaconState, Hash256, Slot}; + +use crate::{ + BeaconChain, BeaconChainTypes, BlockProductionError, StateSkipConfig, + fork_choice_signal::ForkChoiceWaitResult, metrics, +}; + +mod gloas; + +impl<T: BeaconChainTypes> BeaconChain<T> { + /// Load a beacon state from the database for block production. This is a long-running process + /// that should not be performed in an `async` context. + pub(crate) fn load_state_for_block_production( + self: &Arc<Self>, + slot: Slot, + ) -> Result<(BeaconState<T::EthSpec>, Option<Hash256>), BlockProductionError> { + let fork_choice_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_FORK_CHOICE_TIMES); + self.wait_for_fork_choice_before_block_production(slot)?; + drop(fork_choice_timer); + + let state_load_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_STATE_LOAD_TIMES); + + // Atomically read some values from the head whilst avoiding holding cached head `Arc` any + // longer than necessary. + let (head_slot, head_block_root, head_state_root) = { + let head = self.canonical_head.cached_head(); + ( + head.head_slot(), + head.head_block_root(), + head.head_state_root(), + ) + }; + let (state, state_root_opt) = if head_slot < slot { + // Attempt an aggressive re-org if configured and the conditions are right. + if let Some((re_org_state, re_org_state_root)) = + self.get_state_for_re_org(slot, head_slot, head_block_root) + { + info!( + %slot, + head_to_reorg = %head_block_root, + "Proposing block to re-org current head" + ); + (re_org_state, Some(re_org_state_root)) + } else { + // Fetch the head state advanced through to `slot`, which should be present in the + // state cache thanks to the state advance timer. + let (state_root, state) = self + .store + .get_advanced_hot_state(head_block_root, slot, head_state_root) + .map_err(BlockProductionError::FailedToLoadState)? + .ok_or(BlockProductionError::UnableToProduceAtSlot(slot))?; + (state, Some(state_root)) + } + } else { + warn!( + message = "this block is more likely to be orphaned", + %slot, + "Producing block that conflicts with head" + ); + let state = self + .state_at_slot(slot - 1, StateSkipConfig::WithStateRoots) + .map_err(|_| BlockProductionError::UnableToProduceAtSlot(slot))?; + + (state, None) + }; + + drop(state_load_timer); + + Ok((state, state_root_opt)) + } + + /// If configured, wait for the fork choice run at the start of the slot to complete. + #[instrument(level = "debug", skip_all)] + fn wait_for_fork_choice_before_block_production( + self: &Arc<Self>, + slot: Slot, + ) -> Result<(), BlockProductionError> { + if let Some(rx) = &self.fork_choice_signal_rx { + let current_slot = self + .slot() + .map_err(|_| BlockProductionError::UnableToReadSlot)?; + + let timeout = Duration::from_millis(self.config.fork_choice_before_proposal_timeout_ms); + + if slot == current_slot || slot == current_slot + 1 { + match rx.wait_for_fork_choice(slot, timeout) { + ForkChoiceWaitResult::Success(fc_slot) => { + debug!( + %slot, + fork_choice_slot = %fc_slot, + "Fork choice successfully updated before block production" + ); + } + ForkChoiceWaitResult::Behind(fc_slot) => { + warn!( + fork_choice_slot = %fc_slot, + %slot, + message = "this block may be orphaned", + "Fork choice notifier out of sync with block production" + ); + } + ForkChoiceWaitResult::TimeOut => { + warn!( + message = "this block may be orphaned", + "Timed out waiting for fork choice before proposal" + ); + } + } + } else { + error!( + %slot, + %current_slot, + message = "check clock sync, this block may be orphaned", + "Producing block at incorrect slot" + ); + } + } + Ok(()) + } + + /// Fetch the beacon state to use for producing a block if a 1-slot proposer re-org is viable. + /// + /// This function will return `None` if proposer re-orgs are disabled. + #[instrument(skip_all, level = "debug")] + fn get_state_for_re_org( + &self, + slot: Slot, + head_slot: Slot, + canonical_head: Hash256, + ) -> Option<(BeaconState<T::EthSpec>, Hash256)> { + let re_org_head_threshold = self.config.re_org_head_threshold?; + let re_org_parent_threshold = self.config.re_org_parent_threshold?; + + 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() + .or_else(|| { + warn!(error = "unable to read slot clock", "Not attempting re-org"); + None + })?; + + // Attempt a proposer re-org if: + // + // 1. It seems we have time to propagate and still receive the proposer boost. + // 2. The current head block was seen late. + // 3. The `get_proposer_head` conditions from fork choice pass. + let proposing_on_time = + slot_delay < self.config.re_org_cutoff(self.spec.get_slot_duration()); + if !proposing_on_time { + debug!(reason = "not proposing on time", "Not attempting re-org"); + return None; + } + + let head_late = self.block_observed_after_attestation_deadline(canonical_head, head_slot); + if !head_late { + debug!(reason = "head not late", "Not attempting re-org"); + return None; + } + + // Is the current head weak and appropriate for re-orging? + let proposer_head_timer = + metrics::start_timer(&metrics::BLOCK_PRODUCTION_GET_PROPOSER_HEAD_TIMES); + let proposer_head = self + .canonical_head + .fork_choice_read_lock() + .get_proposer_head( + slot, + canonical_head, + re_org_head_threshold, + re_org_parent_threshold, + &self.config.re_org_disallowed_offsets, + self.config.re_org_max_epochs_since_finalization, + ) + .map_err(|e| match e { + ProposerHeadError::DoNotReOrg(reason) => { + debug!( + %reason, + "Not attempting re-org" + ); + } + ProposerHeadError::Error(e) => { + warn!( + error = ?e, + "Not attempting re-org" + ); + } + }) + .ok()?; + drop(proposer_head_timer); + let re_org_parent_block = proposer_head.parent_node.root; + + let (state_root, state) = self + .store + .get_advanced_hot_state_from_cache(re_org_parent_block, slot) + .or_else(|| { + warn!(reason = "no state in cache", "Not attempting re-org"); + None + })?; + + info!( + weak_head = ?canonical_head, + parent = ?re_org_parent_block, + head_weight = proposer_head.head_node.weight, + threshold_weight = proposer_head.re_org_head_weight_threshold, + "Attempting re-org due to weak head" + ); + + Some((state, state_root)) + } +} diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index f673519f5f..4c82c93ba3 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -1036,6 +1036,7 @@ where observed_column_sidecars: RwLock::new(ObservedDataSidecars::new(self.spec.clone())), observed_blob_sidecars: RwLock::new(ObservedDataSidecars::new(self.spec.clone())), observed_slashable: <_>::default(), + pending_payload_envelopes: <_>::default(), observed_voluntary_exits: <_>::default(), observed_proposer_slashings: <_>::default(), observed_attester_slashings: <_>::default(), diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index 816e75fd24..bcccc0ec12 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -319,7 +319,7 @@ pub enum BlockProductionError { MissingExecutionRequests, SszTypesError(ssz_types::Error), // TODO(gloas): Remove this once Gloas is implemented - GloasNotImplemented, + GloasNotImplemented(String), } easy_from_to!(BlockProcessingError, BlockProductionError); diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index e77739e2d5..3b03395a66 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -9,6 +9,7 @@ pub mod beacon_proposer_cache; mod beacon_snapshot; pub mod bellatrix_readiness; pub mod blob_verification; +mod block_production; pub mod block_reward; mod block_times_cache; mod block_verification; @@ -42,6 +43,7 @@ pub mod observed_block_producers; pub mod observed_data_sidecars; pub mod observed_operations; mod observed_slashable; +pub mod pending_payload_envelopes; pub mod persisted_beacon_chain; pub mod persisted_custody; mod persisted_fork_choice; diff --git a/beacon_node/beacon_chain/src/pending_payload_envelopes.rs b/beacon_node/beacon_chain/src/pending_payload_envelopes.rs new file mode 100644 index 0000000000..336ab5323f --- /dev/null +++ b/beacon_node/beacon_chain/src/pending_payload_envelopes.rs @@ -0,0 +1,151 @@ +//! Provides the `PendingPayloadEnvelopes` cache for storing execution payload envelopes +//! that have been produced during local block production. +//! +//! For local building, the envelope is created during block production. +//! This cache holds the envelopes temporarily until the validator fetches, signs, +//! and publishes the payload. + +use std::collections::HashMap; +use types::{EthSpec, ExecutionPayloadEnvelope, Slot}; + +/// Cache for pending execution payload envelopes awaiting publishing. +/// +/// Envelopes are keyed by slot and pruned based on slot age. +/// This cache is only used for local building. +pub struct PendingPayloadEnvelopes<E: EthSpec> { + /// Maximum number of slots to keep envelopes before pruning. + max_slot_age: u64, + /// The envelopes, keyed by slot. + envelopes: HashMap<Slot, ExecutionPayloadEnvelope<E>>, +} + +impl<E: EthSpec> Default for PendingPayloadEnvelopes<E> { + fn default() -> Self { + Self::new(Self::DEFAULT_MAX_SLOT_AGE) + } +} + +impl<E: EthSpec> PendingPayloadEnvelopes<E> { + /// Default maximum slot age before pruning (2 slots). + pub const DEFAULT_MAX_SLOT_AGE: u64 = 2; + + /// Create a new cache with the specified maximum slot age. + pub fn new(max_slot_age: u64) -> Self { + Self { + max_slot_age, + envelopes: HashMap::new(), + } + } + + /// Insert a pending envelope into the cache. + pub fn insert(&mut self, slot: Slot, envelope: ExecutionPayloadEnvelope<E>) { + // TODO(gloas): we may want to check for duplicates here, which shouldn't be allowed + self.envelopes.insert(slot, envelope); + } + + /// Get a pending envelope by slot. + pub fn get(&self, slot: Slot) -> Option<&ExecutionPayloadEnvelope<E>> { + self.envelopes.get(&slot) + } + + /// Remove and return a pending envelope by slot. + pub fn remove(&mut self, slot: Slot) -> Option<ExecutionPayloadEnvelope<E>> { + self.envelopes.remove(&slot) + } + + /// Check if an envelope exists for the given slot. + pub fn contains(&self, slot: Slot) -> bool { + self.envelopes.contains_key(&slot) + } + + /// Prune envelopes older than `current_slot - max_slot_age`. + /// + /// This removes stale envelopes from blocks that were never published. + // TODO(gloas) implement pruning + pub fn prune(&mut self, current_slot: Slot) { + let min_slot = current_slot.saturating_sub(self.max_slot_age); + self.envelopes.retain(|slot, _| *slot >= min_slot); + } + + /// Returns the number of pending envelopes in the cache. + pub fn len(&self) -> usize { + self.envelopes.len() + } + + /// Returns true if the cache is empty. + pub fn is_empty(&self) -> bool { + self.envelopes.is_empty() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use types::{ExecutionPayloadGloas, ExecutionRequests, Hash256, MainnetEthSpec}; + + type E = MainnetEthSpec; + + fn make_envelope(slot: Slot) -> ExecutionPayloadEnvelope<E> { + ExecutionPayloadEnvelope { + payload: ExecutionPayloadGloas::default(), + execution_requests: ExecutionRequests::default(), + builder_index: 0, + beacon_block_root: Hash256::ZERO, + slot, + state_root: Hash256::ZERO, + } + } + + #[test] + fn insert_and_get() { + let mut cache = PendingPayloadEnvelopes::<E>::default(); + let slot = Slot::new(1); + let envelope = make_envelope(slot); + + assert!(!cache.contains(slot)); + assert_eq!(cache.len(), 0); + + cache.insert(slot, envelope.clone()); + + assert!(cache.contains(slot)); + assert_eq!(cache.len(), 1); + assert_eq!(cache.get(slot), Some(&envelope)); + } + + #[test] + fn remove() { + let mut cache = PendingPayloadEnvelopes::<E>::default(); + let slot = Slot::new(1); + let envelope = make_envelope(slot); + + cache.insert(slot, envelope.clone()); + assert!(cache.contains(slot)); + + let removed = cache.remove(slot); + assert_eq!(removed, Some(envelope)); + assert!(!cache.contains(slot)); + assert_eq!(cache.len(), 0); + } + + #[test] + fn prune_old_envelopes() { + let mut cache = PendingPayloadEnvelopes::<E>::new(2); + + // Insert envelope at slot 5 + let slot_1 = Slot::new(5); + cache.insert(slot_1, make_envelope(slot_1)); + + // Insert envelope at slot 10 + let slot_2 = Slot::new(10); + cache.insert(slot_2, make_envelope(slot_2)); + + assert_eq!(cache.len(), 2); + + // Prune at slot 10 with max_slot_age=2, should keep slots >= 8 + cache.prune(Slot::new(10)); + + assert_eq!(cache.len(), 1); + assert!(!cache.contains(slot_1)); // slot 5 < 8, pruned + assert!(cache.contains(slot_2)); // slot 10 >= 8, kept + } +} diff --git a/beacon_node/client/src/notifier.rs b/beacon_node/client/src/notifier.rs index 3f01622c35..21a5abeb6c 100644 --- a/beacon_node/client/src/notifier.rs +++ b/beacon_node/client/src/notifier.rs @@ -431,6 +431,16 @@ async fn bellatrix_readiness_logging<T: BeaconChainTypes>( current_slot: Slot, beacon_chain: &BeaconChain<T>, ) { + // There is no execution payload in gloas blocks, so this will trigger + // bellatrix readiness logging in gloas if we dont skip the check below + if beacon_chain + .spec + .fork_name_at_slot::<T::EthSpec>(current_slot) + .gloas_enabled() + { + return; + } + let merge_completed = beacon_chain .canonical_head .cached_head() diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index 33b83aab09..ad2486a4ad 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -48,7 +48,6 @@ use tree_hash::TreeHash; use types::builder::BuilderBid; use types::execution::BlockProductionVersion; use types::kzg_ext::KzgCommitments; -use types::new_non_zero_usize; use types::{ AbstractExecPayload, BlobsList, ExecutionPayloadDeneb, ExecutionRequests, KzgProofs, SignedBlindedBeaconBlock, @@ -58,6 +57,7 @@ use types::{ ExecutionPayloadCapella, ExecutionPayloadElectra, ExecutionPayloadFulu, FullPayload, ProposerPreparationData, Slot, }; +use types::{ExecutionPayloadGloas, new_non_zero_usize}; mod block_hash; mod engine_api; @@ -168,6 +168,7 @@ pub enum Error { BeaconStateError(BeaconStateError), PayloadTypeMismatch, VerifyingVersionedHashes(versioned_hashes::Error), + Unexpected(String), } impl From<ssz_types::Error> for Error { @@ -204,6 +205,26 @@ pub enum BlockProposalContentsType<E: EthSpec> { Blinded(BlockProposalContents<E, BlindedPayload<E>>), } +pub struct BlockProposalContentsGloas<E: EthSpec> { + pub payload: ExecutionPayloadGloas<E>, + pub payload_value: Uint256, + pub blob_kzg_commitments: KzgCommitments<E>, + pub blobs_and_proofs: (BlobsList<E>, KzgProofs<E>), + pub execution_requests: ExecutionRequests<E>, +} + +impl<E: EthSpec> From<GetPayloadResponseGloas<E>> for BlockProposalContentsGloas<E> { + fn from(response: GetPayloadResponseGloas<E>) -> Self { + Self { + payload: response.execution_payload, + payload_value: response.block_value, + blob_kzg_commitments: response.blobs_bundle.commitments, + blobs_and_proofs: (response.blobs_bundle.blobs, response.blobs_bundle.proofs), + execution_requests: response.requests, + } + } +} + pub enum BlockProposalContents<E: EthSpec, Payload: AbstractExecPayload<E>> { Payload { payload: Payload, @@ -884,6 +905,43 @@ impl<E: EthSpec> ExecutionLayer<E> { .and_then(|entry| entry.gas_limit) } + /// Maps to the `engine_getPayload` JSON-RPC call for post-Gloas payload construction. + /// + /// However, it will attempt to call `self.prepare_payload` if it cannot find an existing + /// payload id for the given parameters. + /// + /// ## Fallback Behavior + /// + /// The result will be returned from the first node that returns successfully. No more nodes + /// will be contacted. + pub async fn get_payload_gloas( + &self, + payload_parameters: PayloadParameters<'_>, + ) -> Result<BlockProposalContentsGloas<E>, Error> { + let payload_response_type = self.get_full_payload_caching(payload_parameters).await?; + let GetPayloadResponseType::Full(payload_response) = payload_response_type else { + return Err(Error::Unexpected( + "get_payload_gloas should never return a blinded payload".to_owned(), + )); + }; + let GetPayloadResponse::Gloas(payload_response) = payload_response else { + return Err(Error::Unexpected( + "get_payload_gloas should always return a gloas `GetPayloadResponse` variant" + .to_owned(), + )); + }; + metrics::inc_counter_vec( + &metrics::EXECUTION_LAYER_GET_PAYLOAD_OUTCOME, + &[metrics::SUCCESS], + ); + metrics::inc_counter_vec( + &metrics::EXECUTION_LAYER_GET_PAYLOAD_SOURCE, + &[metrics::LOCAL], + ); + + Ok(payload_response.into()) + } + /// Maps to the `engine_getPayload` JSON-RPC call. /// /// However, it will attempt to call `self.prepare_payload` if it cannot find an existing diff --git a/beacon_node/execution_layer/src/test_utils/mock_builder.rs b/beacon_node/execution_layer/src/test_utils/mock_builder.rs index 464879288b..7b6c4e8310 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_builder.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_builder.rs @@ -245,7 +245,7 @@ impl<E: EthSpec> BidStuff<E> for BuilderBid<E> { } fn sign_builder_message(&mut self, sk: &SecretKey, spec: &ChainSpec) -> Signature { - let domain = spec.get_builder_domain(); + let domain = spec.get_builder_application_domain(); let message = self.signing_root(domain); sk.sign(message) } diff --git a/beacon_node/http_api/src/beacon/execution_payload_envelope.rs b/beacon_node/http_api/src/beacon/execution_payload_envelope.rs new file mode 100644 index 0000000000..81f2ea41ea --- /dev/null +++ b/beacon_node/http_api/src/beacon/execution_payload_envelope.rs @@ -0,0 +1,116 @@ +use crate::task_spawner::{Priority, TaskSpawner}; +use crate::utils::{ChainFilter, EthV1Filter, NetworkTxFilter, ResponseFilter, TaskSpawnerFilter}; +use beacon_chain::{BeaconChain, BeaconChainTypes}; +use bytes::Bytes; +use eth2::{CONTENT_TYPE_HEADER, SSZ_CONTENT_TYPE_HEADER}; +use lighthouse_network::PubsubMessage; +use network::NetworkMessage; +use ssz::Decode; +use std::sync::Arc; +use tokio::sync::mpsc::UnboundedSender; +use tracing::{info, warn}; +use types::SignedExecutionPayloadEnvelope; +use warp::{Filter, Rejection, Reply, reply::Response}; + +// POST beacon/execution_payload_envelope (SSZ) +pub(crate) fn post_beacon_execution_payload_envelope_ssz<T: BeaconChainTypes>( + eth_v1: EthV1Filter, + task_spawner_filter: TaskSpawnerFilter<T>, + chain_filter: ChainFilter<T>, + network_tx_filter: NetworkTxFilter<T>, +) -> ResponseFilter { + eth_v1 + .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) + .and(network_tx_filter) + .then( + |body_bytes: Bytes, + task_spawner: TaskSpawner<T::EthSpec>, + chain: Arc<BeaconChain<T>>, + network_tx: UnboundedSender<NetworkMessage<T::EthSpec>>| { + task_spawner.spawn_async_with_rejection(Priority::P0, async move { + let envelope = + SignedExecutionPayloadEnvelope::<T::EthSpec>::from_ssz_bytes(&body_bytes) + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!("invalid SSZ: {e:?}")) + })?; + publish_execution_payload_envelope(envelope, chain, &network_tx).await + }) + }, + ) + .boxed() +} + +// POST beacon/execution_payload_envelope +pub(crate) fn post_beacon_execution_payload_envelope<T: BeaconChainTypes>( + eth_v1: EthV1Filter, + task_spawner_filter: TaskSpawnerFilter<T>, + chain_filter: ChainFilter<T>, + network_tx_filter: NetworkTxFilter<T>, +) -> ResponseFilter { + eth_v1 + .and(warp::path("beacon")) + .and(warp::path("execution_payload_envelope")) + .and(warp::path::end()) + .and(warp::body::json()) + .and(task_spawner_filter.clone()) + .and(chain_filter.clone()) + .and(network_tx_filter.clone()) + .then( + |envelope: SignedExecutionPayloadEnvelope<T::EthSpec>, + task_spawner: TaskSpawner<T::EthSpec>, + chain: Arc<BeaconChain<T>>, + network_tx: UnboundedSender<NetworkMessage<T::EthSpec>>| { + task_spawner.spawn_async_with_rejection(Priority::P0, async move { + publish_execution_payload_envelope(envelope, chain, &network_tx).await + }) + }, + ) + .boxed() +} +/// Publishes a signed execution payload envelope to the network. +pub async fn publish_execution_payload_envelope<T: BeaconChainTypes>( + envelope: SignedExecutionPayloadEnvelope<T::EthSpec>, + chain: Arc<BeaconChain<T>>, + network_tx: &UnboundedSender<NetworkMessage<T::EthSpec>>, +) -> Result<Response, Rejection> { + let slot = envelope.message.slot; + let beacon_block_root = envelope.message.beacon_block_root; + + // TODO(gloas): Replace this check once we have gossip validation. + if !chain.spec.is_gloas_scheduled() { + return Err(warp_utils::reject::custom_bad_request( + "Execution payload envelopes are not supported before the Gloas fork".into(), + )); + } + + // TODO(gloas): We should probably add validation here i.e. BroadcastValidation::Gossip + info!( + %slot, + %beacon_block_root, + builder_index = envelope.message.builder_index, + "Publishing signed execution payload envelope to network" + ); + + // Publish to the network + crate::utils::publish_pubsub_message( + network_tx, + PubsubMessage::ExecutionPayload(Box::new(envelope)), + ) + .map_err(|_| { + warn!(%slot, "Failed to publish execution payload envelope to network"); + warp_utils::reject::custom_server_error( + "Unable to publish execution payload envelope to network".into(), + ) + })?; + + Ok(warp::reply().into_response()) +} diff --git a/beacon_node/http_api/src/beacon/mod.rs b/beacon_node/http_api/src/beacon/mod.rs index df5e6eee5c..9ec1c476f6 100644 --- a/beacon_node/http_api/src/beacon/mod.rs +++ b/beacon_node/http_api/src/beacon/mod.rs @@ -1,2 +1,3 @@ +pub mod execution_payload_envelope; pub mod pool; pub mod states; diff --git a/beacon_node/http_api/src/beacon/states.rs b/beacon_node/http_api/src/beacon/states.rs index 828efb86a7..50be7211d8 100644 --- a/beacon_node/http_api/src/beacon/states.rs +++ b/beacon_node/http_api/src/beacon/states.rs @@ -28,7 +28,6 @@ pub fn get_beacon_state_pending_consolidations<T: BeaconChainTypes>( beacon_states_path: BeaconStatesPath<T>, ) -> ResponseFilter { beacon_states_path - .clone() .and(warp::path("pending_consolidations")) .and(warp::path::end()) .then( diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 6824eab4fd..92a1ad934d 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_envelope::{ + post_beacon_execution_payload_envelope, post_beacon_execution_payload_envelope_ssz, +}; use crate::beacon::pool::*; use crate::light_client::{get_light_client_bootstrap, get_light_client_updates}; use crate::utils::{AnyVersionFilter, EthV1Filter}; @@ -92,6 +95,7 @@ use types::{ BeaconStateError, Checkpoint, ConfigAndPreset, Epoch, EthSpec, ForkName, Hash256, SignedBlindedBeaconBlock, Slot, }; +use validator::execution_payload_envelope::get_validator_execution_payload_envelope; use version::{ ResponseIncludesVersion, V1, V2, add_consensus_version_header, add_ssz_content_type_header, execution_optimistic_finalized_beacon_response, inconsistent_fork_rejection, @@ -1486,6 +1490,22 @@ pub fn serve<T: BeaconChainTypes>( let post_beacon_pool_bls_to_execution_changes = post_beacon_pool_bls_to_execution_changes(&network_tx_filter, &beacon_pool_path); + // POST beacon/execution_payload_envelope + let post_beacon_execution_payload_envelope = post_beacon_execution_payload_envelope( + 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( + eth_v1.clone(), + task_spawner_filter.clone(), + chain_filter.clone(), + network_tx_filter.clone(), + ); + let beacon_rewards_path = eth_v1 .clone() .and(warp::path("beacon")) @@ -2444,7 +2464,7 @@ pub fn serve<T: BeaconChainTypes>( // GET validator/duties/proposer/{epoch} let get_validator_duties_proposer = get_validator_duties_proposer( - eth_v1.clone().clone(), + eth_v1.clone(), chain_filter.clone(), not_while_syncing_filter.clone(), task_spawner_filter.clone(), @@ -2452,7 +2472,7 @@ pub fn serve<T: BeaconChainTypes>( // GET validator/blocks/{slot} let get_validator_blocks = get_validator_blocks( - any_version.clone().clone(), + any_version.clone(), chain_filter.clone(), not_while_syncing_filter.clone(), task_spawner_filter.clone(), @@ -2460,7 +2480,15 @@ pub fn serve<T: BeaconChainTypes>( // GET validator/blinded_blocks/{slot} let get_validator_blinded_blocks = get_validator_blinded_blocks( - eth_v1.clone().clone(), + eth_v1.clone(), + chain_filter.clone(), + not_while_syncing_filter.clone(), + task_spawner_filter.clone(), + ); + + // GET validator/execution_payload_envelope/{slot}/{builder_index} + let get_validator_execution_payload_envelope = get_validator_execution_payload_envelope( + eth_v1.clone(), chain_filter.clone(), not_while_syncing_filter.clone(), task_spawner_filter.clone(), @@ -2468,7 +2496,7 @@ pub fn serve<T: BeaconChainTypes>( // GET validator/attestation_data?slot,committee_index let get_validator_attestation_data = get_validator_attestation_data( - eth_v1.clone().clone(), + eth_v1.clone(), chain_filter.clone(), not_while_syncing_filter.clone(), task_spawner_filter.clone(), @@ -2476,7 +2504,7 @@ pub fn serve<T: BeaconChainTypes>( // GET validator/aggregate_attestation?attestation_data_root,slot let get_validator_aggregate_attestation = get_validator_aggregate_attestation( - any_version.clone().clone(), + any_version.clone(), chain_filter.clone(), not_while_syncing_filter.clone(), task_spawner_filter.clone(), @@ -2484,7 +2512,7 @@ pub fn serve<T: BeaconChainTypes>( // POST validator/duties/attester/{epoch} let post_validator_duties_attester = post_validator_duties_attester( - eth_v1.clone().clone(), + eth_v1.clone(), chain_filter.clone(), not_while_syncing_filter.clone(), task_spawner_filter.clone(), @@ -2492,7 +2520,7 @@ pub fn serve<T: BeaconChainTypes>( // POST validator/duties/sync/{epoch} let post_validator_duties_sync = post_validator_duties_sync( - eth_v1.clone().clone(), + eth_v1.clone(), chain_filter.clone(), not_while_syncing_filter.clone(), task_spawner_filter.clone(), @@ -2500,7 +2528,7 @@ pub fn serve<T: BeaconChainTypes>( // GET validator/sync_committee_contribution let get_validator_sync_committee_contribution = get_validator_sync_committee_contribution( - eth_v1.clone().clone(), + eth_v1.clone(), chain_filter.clone(), not_while_syncing_filter.clone(), task_spawner_filter.clone(), @@ -2508,7 +2536,7 @@ pub fn serve<T: BeaconChainTypes>( // POST validator/aggregate_and_proofs let post_validator_aggregate_and_proofs = post_validator_aggregate_and_proofs( - any_version.clone().clone(), + any_version.clone(), chain_filter.clone(), network_tx_filter.clone(), not_while_syncing_filter.clone(), @@ -2516,7 +2544,7 @@ pub fn serve<T: BeaconChainTypes>( ); let post_validator_contribution_and_proofs = post_validator_contribution_and_proofs( - eth_v1.clone().clone(), + eth_v1.clone(), chain_filter.clone(), network_tx_filter.clone(), not_while_syncing_filter.clone(), @@ -2526,7 +2554,7 @@ pub fn serve<T: BeaconChainTypes>( // POST validator/beacon_committee_subscriptions let post_validator_beacon_committee_subscriptions = post_validator_beacon_committee_subscriptions( - eth_v1.clone().clone(), + eth_v1.clone(), chain_filter.clone(), validator_subscription_tx_filter.clone(), task_spawner_filter.clone(), @@ -2534,7 +2562,7 @@ pub fn serve<T: BeaconChainTypes>( // POST validator/prepare_beacon_proposer let post_validator_prepare_beacon_proposer = post_validator_prepare_beacon_proposer( - eth_v1.clone().clone(), + eth_v1.clone(), chain_filter.clone(), network_tx_filter.clone(), not_while_syncing_filter.clone(), @@ -2543,13 +2571,13 @@ pub fn serve<T: BeaconChainTypes>( // POST validator/register_validator let post_validator_register_validator = post_validator_register_validator( - eth_v1.clone().clone(), + eth_v1.clone(), chain_filter.clone(), task_spawner_filter.clone(), ); // POST validator/sync_committee_subscriptions let post_validator_sync_committee_subscriptions = post_validator_sync_committee_subscriptions( - eth_v1.clone().clone(), + eth_v1.clone(), chain_filter.clone(), validator_subscription_tx_filter.clone(), task_spawner_filter.clone(), @@ -2557,7 +2585,7 @@ pub fn serve<T: BeaconChainTypes>( // POST validator/liveness/{epoch} let post_validator_liveness_epoch = post_validator_liveness_epoch( - eth_v1.clone().clone(), + eth_v1.clone(), chain_filter.clone(), task_spawner_filter.clone(), ); @@ -3336,6 +3364,7 @@ pub fn serve<T: BeaconChainTypes>( .uor(get_validator_duties_proposer) .uor(get_validator_blocks) .uor(get_validator_blinded_blocks) + .uor(get_validator_execution_payload_envelope) .uor(get_validator_attestation_data) .uor(get_validator_aggregate_attestation) .uor(get_validator_sync_committee_contribution) @@ -3374,7 +3403,8 @@ pub fn serve<T: BeaconChainTypes>( post_beacon_blocks_ssz .uor(post_beacon_blocks_v2_ssz) .uor(post_beacon_blinded_blocks_ssz) - .uor(post_beacon_blinded_blocks_v2_ssz), + .uor(post_beacon_blinded_blocks_v2_ssz) + .uor(post_beacon_execution_payload_envelope_ssz), ) .uor(post_beacon_blocks) .uor(post_beacon_blinded_blocks) @@ -3386,6 +3416,7 @@ pub fn serve<T: BeaconChainTypes>( .uor(post_beacon_pool_voluntary_exits) .uor(post_beacon_pool_sync_committees) .uor(post_beacon_pool_bls_to_execution_changes) + .uor(post_beacon_execution_payload_envelope) .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/produce_block.rs b/beacon_node/http_api/src/produce_block.rs index 6a549c91ef..607221686f 100644 --- a/beacon_node/http_api/src/produce_block.rs +++ b/beacon_node/http_api/src/produce_block.rs @@ -10,8 +10,8 @@ use beacon_chain::graffiti_calculator::GraffitiSettings; use beacon_chain::{ BeaconBlockResponseWrapper, BeaconChain, BeaconChainTypes, ProduceBlockVerification, }; -use eth2::beacon_response::ForkVersionedResponse; use eth2::types::{self as api_types, ProduceBlockV3Metadata, SkipRandaoVerification}; +use eth2::{beacon_response::ForkVersionedResponse, types::ProduceBlockV4Metadata}; use ssz::Encode; use std::sync::Arc; use tracing::instrument; @@ -43,6 +43,49 @@ pub fn get_randao_verification( Ok(randao_verification) } +#[instrument( + name = "lh_produce_block_v4", + skip_all, + fields(%slot) +)] +pub async fn produce_block_v4<T: BeaconChainTypes>( + accept_header: Option<api_types::Accept>, + chain: Arc<BeaconChain<T>>, + slot: Slot, + query: api_types::ValidatorBlocksQuery, +) -> Result<Response<Body>, warp::Rejection> { + let randao_reveal = query.randao_reveal.decompress().map_err(|e| { + warp_utils::reject::custom_bad_request(format!( + "randao reveal is not a valid BLS signature: {:?}", + e + )) + })?; + + let randao_verification = get_randao_verification(&query, randao_reveal.is_infinity())?; + let builder_boost_factor = if query.builder_boost_factor == Some(DEFAULT_BOOST_FACTOR) { + None + } else { + query.builder_boost_factor + }; + + let graffiti_settings = GraffitiSettings::new(query.graffiti, query.graffiti_policy); + + let (block, consensus_block_value) = chain + .produce_block_with_verification_gloas( + randao_reveal, + slot, + graffiti_settings, + randao_verification, + builder_boost_factor, + ) + .await + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!("failed to fetch a block: {:?}", e)) + })?; + + build_response_v4::<T>(block, consensus_block_value, accept_header, &chain.spec) +} + #[instrument( name = "lh_produce_block_v3", skip_all, @@ -87,6 +130,45 @@ pub async fn produce_block_v3<T: BeaconChainTypes>( build_response_v3(chain, block_response_type, accept_header) } +pub fn build_response_v4<T: BeaconChainTypes>( + block: BeaconBlock<T::EthSpec, FullPayload<T::EthSpec>>, + consensus_block_value: u64, + accept_header: Option<api_types::Accept>, + spec: &ChainSpec, +) -> Result<Response<Body>, warp::Rejection> { + let fork_name = block + .to_ref() + .fork_name(spec) + .map_err(inconsistent_fork_rejection)?; + let consensus_block_value_wei = + Uint256::from(consensus_block_value) * Uint256::from(1_000_000_000u64); + + let metadata = ProduceBlockV4Metadata { + consensus_version: fork_name, + consensus_block_value: consensus_block_value_wei, + }; + + match accept_header { + Some(api_types::Accept::Ssz) => Response::builder() + .status(200) + .body(block.as_ssz_bytes().into()) + .map(|res: Response<Body>| add_ssz_content_type_header(res)) + .map(|res: Response<Body>| add_consensus_version_header(res, fork_name)) + .map(|res| add_consensus_block_value_header(res, consensus_block_value_wei)) + .map_err(|e| -> warp::Rejection { + warp_utils::reject::custom_server_error(format!("failed to create response: {}", e)) + }), + _ => Ok(warp::reply::json(&ForkVersionedResponse { + version: fork_name, + metadata, + data: block, + }) + .into_response()) + .map(|res| add_consensus_version_header(res, fork_name)) + .map(|res| add_consensus_block_value_header(res, consensus_block_value_wei)), + } +} + pub fn build_response_v3<T: BeaconChainTypes>( chain: Arc<BeaconChain<T>>, block_response: BeaconBlockResponseWrapper<T::EthSpec>, diff --git a/beacon_node/http_api/src/validator/execution_payload_envelope.rs b/beacon_node/http_api/src/validator/execution_payload_envelope.rs new file mode 100644 index 0000000000..c40b375e49 --- /dev/null +++ b/beacon_node/http_api/src/validator/execution_payload_envelope.rs @@ -0,0 +1,110 @@ +use crate::task_spawner::{Priority, TaskSpawner}; +use crate::utils::{ + ChainFilter, EthV1Filter, NotWhileSyncingFilter, ResponseFilter, TaskSpawnerFilter, +}; +use beacon_chain::{BeaconChain, BeaconChainTypes}; +use eth2::beacon_response::{EmptyMetadata, ForkVersionedResponse}; +use eth2::types::Accept; +use ssz::Encode; +use std::sync::Arc; +use tracing::debug; +use types::Slot; +use warp::http::Response; +use warp::{Filter, Rejection}; + +// GET validator/execution_payload_envelope/{slot}/{builder_index} +pub fn get_validator_execution_payload_envelope<T: BeaconChainTypes>( + eth_v1: EthV1Filter, + chain_filter: ChainFilter<T>, + not_while_syncing_filter: NotWhileSyncingFilter, + task_spawner_filter: TaskSpawnerFilter<T>, +) -> ResponseFilter { + eth_v1 + .and(warp::path("validator")) + .and(warp::path("execution_payload_envelope")) + .and(warp::path::param::<Slot>().or_else(|_| async { + Err(warp_utils::reject::custom_bad_request( + "Invalid slot".to_string(), + )) + })) + .and(warp::path::param::<u64>().or_else(|_| async { + Err(warp_utils::reject::custom_bad_request( + "Invalid builder_index".to_string(), + )) + })) + .and(warp::path::end()) + .and(warp::header::optional::<Accept>("accept")) + .and(not_while_syncing_filter) + .and(task_spawner_filter) + .and(chain_filter) + .then( + |slot: Slot, + // TODO(gloas) we're only doing local building + // we'll need to implement builder index logic + // eventually. + _builder_index: u64, + accept_header: Option<Accept>, + not_synced_filter: Result<(), Rejection>, + task_spawner: TaskSpawner<T::EthSpec>, + chain: Arc<BeaconChain<T>>| { + task_spawner.spawn_async_with_rejection(Priority::P0, async move { + debug!(?slot, "Execution payload envelope request from HTTP API"); + + not_synced_filter?; + + // Get the envelope from the pending cache (local building only) + let envelope = chain + .pending_payload_envelopes + .read() + .get(slot) + .cloned() + .ok_or_else(|| { + warp_utils::reject::custom_not_found(format!( + "Execution payload envelope not available for slot {slot}" + )) + })?; + + let fork_name = chain.spec.fork_name_at_slot::<T::EthSpec>(slot); + + match accept_header { + Some(Accept::Ssz) => Response::builder() + .status(200) + .header("Content-Type", "application/octet-stream") + .header("Eth-Consensus-Version", fork_name.to_string()) + .body(envelope.as_ssz_bytes().into()) + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "Failed to build SSZ response: {e}" + )) + }), + _ => { + let json_response = ForkVersionedResponse { + version: fork_name, + metadata: EmptyMetadata {}, + data: envelope, + }; + Response::builder() + .status(200) + .header("Content-Type", "application/json") + .header("Eth-Consensus-Version", fork_name.to_string()) + .body( + serde_json::to_string(&json_response) + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "Failed to serialize response: {e}" + )) + })? + .into(), + ) + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "Failed to build JSON response: {e}" + )) + }) + } + } + }) + }, + ) + .boxed() +} diff --git a/beacon_node/http_api/src/validator/mod.rs b/beacon_node/http_api/src/validator/mod.rs index 0704c52095..df237d9f9b 100644 --- a/beacon_node/http_api/src/validator/mod.rs +++ b/beacon_node/http_api/src/validator/mod.rs @@ -1,4 +1,6 @@ -use crate::produce_block::{produce_blinded_block_v2, produce_block_v2, produce_block_v3}; +use crate::produce_block::{ + produce_blinded_block_v2, produce_block_v2, produce_block_v3, produce_block_v4, +}; use crate::task_spawner::{Priority, TaskSpawner}; use crate::utils::{ AnyVersionFilter, ChainFilter, EthV1Filter, NetworkTxFilter, NotWhileSyncingFilter, @@ -31,6 +33,8 @@ use types::{ use warp::{Filter, Rejection, Reply}; use warp_utils::reject::convert_rejection; +pub mod execution_payload_envelope; + /// 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`. pub fn pubkey_to_validator_index<T: BeaconChainTypes>( @@ -316,7 +320,11 @@ pub fn get_validator_blocks<T: BeaconChainTypes>( not_synced_filter?; - if endpoint_version == V3 { + // Use V4 block production for Gloas fork + let fork_name = chain.spec.fork_name_at_slot::<T::EthSpec>(slot); + if fork_name.gloas_enabled() { + produce_block_v4(accept_header, chain, slot, query).await + } else if endpoint_version == V3 { produce_block_v3(accept_header, chain, slot, query).await } else { produce_block_v2(accept_header, chain, slot, query).await @@ -662,15 +670,26 @@ pub fn post_validator_prepare_beacon_proposer<T: BeaconChainTypes>( ) .await; - chain - .prepare_beacon_proposer(current_slot) - .await - .map_err(|e| { - warp_utils::reject::custom_bad_request(format!( - "error updating proposer preparations: {:?}", - e - )) - })?; + // TODO(gloas): verify this is correct. We skip proposer preparation for + // GLOAS because the execution payload is no longer embedded in the beacon + // block (it's in the payload envelope), so the head block's + // execution_payload() is unavailable. + let next_slot = current_slot + 1; + if !chain + .spec + .fork_name_at_slot::<T::EthSpec>(next_slot) + .gloas_enabled() + { + chain + .prepare_beacon_proposer(current_slot) + .await + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!( + "error updating proposer preparations: {:?}", + e + )) + })?; + } if chain.spec.is_peer_das_scheduled() { let (finalized_beacon_state, _, _) = diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 6787d1ab9e..7e3eb8b980 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -6,14 +6,15 @@ use beacon_chain::{ AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, test_spec, }, }; -use bls::{AggregateSignature, Keypair, PublicKeyBytes, Signature, SignatureBytes}; +use bls::{AggregateSignature, Keypair, PublicKeyBytes, SecretKey, Signature, SignatureBytes}; use eth2::{ BeaconNodeHttpClient, Error, Error::ServerMessage, Timeouts, mixin::{RequestAccept, ResponseForkName, ResponseOptional}, types::{ - BlockId as CoreBlockId, ForkChoiceNode, ProduceBlockV3Response, StateId as CoreStateId, *, + BlockId as CoreBlockId, ForkChoiceNode, ProduceBlockV3Response, ProduceBlockV4Metadata, + StateId as CoreStateId, *, }, }; use execution_layer::expected_gas_limit; @@ -47,7 +48,8 @@ use tree_hash::TreeHash; use types::ApplicationDomain; use types::{ Domain, EthSpec, ExecutionBlockHash, Hash256, MainnetEthSpec, RelativeEpoch, SelectionProof, - SignedRoot, SingleAttestation, Slot, attestation::AttestationBase, + SignedExecutionPayloadEnvelope, SignedRoot, SingleAttestation, Slot, + attestation::AttestationBase, consts::gloas::BUILDER_INDEX_SELF_BUILD, }; type E = MainnetEthSpec; @@ -3726,6 +3728,229 @@ impl ApiTester { self } + /// Get the proposer secret key and randao reveal for the given slot. + async fn proposer_setup( + &self, + slot: Slot, + epoch: Epoch, + fork: &Fork, + genesis_validators_root: Hash256, + ) -> (SecretKey, SignatureBytes) { + let proposer_pubkey_bytes = self + .client + .get_validator_duties_proposer(epoch) + .await + .unwrap() + .data + .into_iter() + .find(|duty| duty.slot == slot) + .map(|duty| duty.pubkey) + .unwrap(); + let proposer_pubkey = (&proposer_pubkey_bytes).try_into().unwrap(); + + let sk = self + .validator_keypairs() + .iter() + .find(|kp| kp.pk == proposer_pubkey) + .map(|kp| kp.sk.clone()) + .unwrap(); + + let randao_reveal = { + let domain = + self.chain + .spec + .get_domain(epoch, Domain::Randao, fork, genesis_validators_root); + let message = epoch.signing_root(domain); + sk.sign(message).into() + }; + + (sk, randao_reveal) + } + + /// Assert block metadata and verify the envelope cache. + fn assert_v4_block_metadata( + &self, + block: &BeaconBlock<E>, + metadata: &ProduceBlockV4Metadata, + slot: Slot, + ) { + assert_eq!( + metadata.consensus_version, + block.to_ref().fork_name(&self.chain.spec).unwrap() + ); + assert!(!metadata.consensus_block_value.is_zero()); + + let block_root = block.tree_hash_root(); + let envelope = self + .chain + .pending_payload_envelopes + .read() + .get(slot) + .cloned() + .expect("envelope should exist in pending cache for local building"); + assert_eq!(envelope.beacon_block_root, block_root); + assert_eq!(envelope.slot, slot); + } + + /// Assert envelope fields match the expected block root and slot. + fn assert_envelope_fields( + &self, + envelope: &ExecutionPayloadEnvelope<E>, + block_root: Hash256, + slot: Slot, + ) { + assert_eq!(envelope.beacon_block_root, block_root); + assert_eq!(envelope.slot, slot); + assert_eq!(envelope.builder_index, BUILDER_INDEX_SELF_BUILD); + assert_ne!(envelope.state_root, Hash256::ZERO); + } + + /// Sign an execution payload envelope. + fn sign_envelope( + &self, + envelope: ExecutionPayloadEnvelope<E>, + sk: &SecretKey, + epoch: Epoch, + fork: &Fork, + genesis_validators_root: Hash256, + ) -> SignedExecutionPayloadEnvelope<E> { + let domain = + self.chain + .spec + .get_domain(epoch, Domain::BeaconBuilder, fork, genesis_validators_root); + let signing_root = envelope.signing_root(domain); + let signature = sk.sign(signing_root); + + SignedExecutionPayloadEnvelope { + message: envelope, + signature, + } + } + + /// Test V4 block production (JSON). Only runs if Gloas is scheduled. + pub async fn test_block_production_v4(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::<E>(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; + + let (response, metadata) = self + .client + .get_validator_blocks_v4::<E>(slot, &randao_reveal, None, None, None) + .await + .unwrap(); + let block = response.data; + + self.assert_v4_block_metadata(&block, &metadata, slot); + + let envelope = self + .client + .get_validator_execution_payload_envelope::<E>(slot, BUILDER_INDEX_SELF_BUILD) + .await + .unwrap() + .data; + + self.assert_envelope_fields(&envelope, block.tree_hash_root(), slot); + + let signed_block = block.sign(&sk, &fork, genesis_validators_root, &self.chain.spec); + let signed_block_request = + PublishBlockRequest::try_from(Arc::new(signed_block.clone())).unwrap(); + self.client + .post_beacon_blocks_v2(&signed_block_request, None) + .await + .unwrap(); + assert_eq!(self.chain.head_beacon_block(), Arc::new(signed_block)); + + let signed_envelope = + self.sign_envelope(envelope, &sk, epoch, &fork, genesis_validators_root); + self.client + .post_beacon_execution_payload_envelope(&signed_envelope, fork_name) + .await + .unwrap(); + + self.chain.slot_clock.set_slot(slot.as_u64() + 1); + } + + self + } + + /// Test V4 block production (SSZ). Only runs if Gloas is scheduled. + pub async fn test_block_production_v4_ssz(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::<E>(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; + + let (block, metadata) = self + .client + .get_validator_blocks_v4_ssz::<E>(slot, &randao_reveal, None, None, None) + .await + .unwrap(); + + self.assert_v4_block_metadata(&block, &metadata, slot); + + let envelope = self + .client + .get_validator_execution_payload_envelope_ssz::<E>(slot, BUILDER_INDEX_SELF_BUILD) + .await + .unwrap(); + + self.assert_envelope_fields(&envelope, block.tree_hash_root(), slot); + + let signed_block = block.sign(&sk, &fork, genesis_validators_root, &self.chain.spec); + let signed_block_request = + PublishBlockRequest::try_from(Arc::new(signed_block.clone())).unwrap(); + self.client + .post_beacon_blocks_v2_ssz(&signed_block_request, None) + .await + .unwrap(); + assert_eq!(self.chain.head_beacon_block(), Arc::new(signed_block)); + + 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) + .await + .unwrap(); + + self.chain.slot_clock.set_slot(slot.as_u64() + 1); + } + + self + } + pub async fn test_block_production_no_verify_randao(self) -> Self { for _ in 0..E::slots_per_epoch() { let slot = self.chain.slot().unwrap(); @@ -7459,6 +7684,22 @@ async fn block_production_v3_ssz_with_skip_slots() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn block_production_v4() { + ApiTester::new_with_hard_forks() + .await + .test_block_production_v4() + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn block_production_v4_ssz() { + ApiTester::new_with_hard_forks() + .await + .test_block_production_v4_ssz() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn blinded_block_production_full_payload_premerge() { ApiTester::new().await.test_blinded_block_production().await; diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index cdf63a3c67..a5b4f9afdd 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -42,7 +42,7 @@ use reqwest::{ #[cfg(feature = "events")] use reqwest_eventsource::{Event, RequestBuilderExt}; use serde::{Serialize, de::DeserializeOwned}; -use ssz::Encode; +use ssz::{Decode, Encode}; use std::fmt; use std::future::Future; use std::time::Duration; @@ -50,6 +50,7 @@ use std::time::Duration; pub const V1: EndpointVersion = EndpointVersion(1); pub const V2: EndpointVersion = EndpointVersion(2); pub const V3: EndpointVersion = EndpointVersion(3); +pub const V4: EndpointVersion = EndpointVersion(4); pub const CONSENSUS_VERSION_HEADER: &str = "Eth-Consensus-Version"; pub const EXECUTION_PAYLOAD_BLINDED_HEADER: &str = "Eth-Execution-Payload-Blinded"; @@ -2406,6 +2407,277 @@ impl BeaconNodeHttpClient { opt_response.ok_or(Error::StatusCode(StatusCode::NOT_FOUND)) } + /// returns `GET v4/validator/blocks/{slot}` URL path + pub async fn get_validator_blocks_v4_path( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + skip_randao_verification: SkipRandaoVerification, + builder_booster_factor: Option<u64>, + graffiti_policy: Option<GraffitiPolicy>, + ) -> Result<Url, Error> { + let mut path = self.eth_path(V4)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("blocks") + .push(&slot.to_string()); + + path.query_pairs_mut() + .append_pair("randao_reveal", &randao_reveal.to_string()); + + if let Some(graffiti) = graffiti { + path.query_pairs_mut() + .append_pair("graffiti", &graffiti.to_string()); + } + + if skip_randao_verification == SkipRandaoVerification::Yes { + path.query_pairs_mut() + .append_pair("skip_randao_verification", ""); + } + + if let Some(builder_booster_factor) = builder_booster_factor { + path.query_pairs_mut() + .append_pair("builder_boost_factor", &builder_booster_factor.to_string()); + } + + if let Some(GraffitiPolicy::AppendClientVersions) = graffiti_policy { + path.query_pairs_mut() + .append_pair("graffiti_policy", "AppendClientVersions"); + } + + Ok(path) + } + + /// `GET v4/validator/blocks/{slot}` + pub async fn get_validator_blocks_v4<E: EthSpec>( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + builder_booster_factor: Option<u64>, + graffiti_policy: Option<GraffitiPolicy>, + ) -> Result< + ( + ForkVersionedResponse<BeaconBlock<E>, ProduceBlockV4Metadata>, + ProduceBlockV4Metadata, + ), + Error, + > { + self.get_validator_blocks_v4_modular( + slot, + randao_reveal, + graffiti, + SkipRandaoVerification::No, + builder_booster_factor, + graffiti_policy, + ) + .await + } + + /// `GET v4/validator/blocks/{slot}` + pub async fn get_validator_blocks_v4_modular<E: EthSpec>( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + skip_randao_verification: SkipRandaoVerification, + builder_booster_factor: Option<u64>, + graffiti_policy: Option<GraffitiPolicy>, + ) -> Result< + ( + ForkVersionedResponse<BeaconBlock<E>, ProduceBlockV4Metadata>, + ProduceBlockV4Metadata, + ), + Error, + > { + let path = self + .get_validator_blocks_v4_path( + slot, + randao_reveal, + graffiti, + skip_randao_verification, + builder_booster_factor, + graffiti_policy, + ) + .await?; + + let opt_result = self + .get_response_with_response_headers( + path, + Accept::Json, + self.timeouts.get_validator_block, + |response, headers| async move { + let header_metadata = ProduceBlockV4Metadata::try_from(&headers) + .map_err(Error::InvalidHeaders)?; + let block_response = response + .json::<ForkVersionedResponse<BeaconBlock<E>, ProduceBlockV4Metadata>>() + .await?; + Ok((block_response, header_metadata)) + }, + ) + .await?; + + opt_result.ok_or(Error::StatusCode(StatusCode::NOT_FOUND)) + } + + /// `GET v4/validator/blocks/{slot}` in ssz format + pub async fn get_validator_blocks_v4_ssz<E: EthSpec>( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + builder_booster_factor: Option<u64>, + graffiti_policy: Option<GraffitiPolicy>, + ) -> Result<(BeaconBlock<E>, ProduceBlockV4Metadata), Error> { + self.get_validator_blocks_v4_modular_ssz::<E>( + slot, + randao_reveal, + graffiti, + SkipRandaoVerification::No, + builder_booster_factor, + graffiti_policy, + ) + .await + } + + /// `GET v4/validator/blocks/{slot}` in ssz format + pub async fn get_validator_blocks_v4_modular_ssz<E: EthSpec>( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + skip_randao_verification: SkipRandaoVerification, + builder_booster_factor: Option<u64>, + graffiti_policy: Option<GraffitiPolicy>, + ) -> Result<(BeaconBlock<E>, ProduceBlockV4Metadata), Error> { + let path = self + .get_validator_blocks_v4_path( + slot, + randao_reveal, + graffiti, + skip_randao_verification, + builder_booster_factor, + graffiti_policy, + ) + .await?; + + let opt_response = self + .get_response_with_response_headers( + path, + Accept::Ssz, + self.timeouts.get_validator_block, + |response, headers| async move { + let metadata = ProduceBlockV4Metadata::try_from(&headers) + .map_err(Error::InvalidHeaders)?; + let response_bytes = response.bytes().await?; + + let block = BeaconBlock::from_ssz_bytes_for_fork( + &response_bytes, + metadata.consensus_version, + ) + .map_err(Error::InvalidSsz)?; + + Ok((block, metadata)) + }, + ) + .await?; + + opt_response.ok_or(Error::StatusCode(StatusCode::NOT_FOUND)) + } + + /// `GET v1/validator/execution_payload_envelope/{slot}/{builder_index}` + pub async fn get_validator_execution_payload_envelope<E: EthSpec>( + &self, + slot: Slot, + builder_index: u64, + ) -> Result<ForkVersionedResponse<ExecutionPayloadEnvelope<E>>, Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("execution_payload_envelope") + .push(&slot.to_string()) + .push(&builder_index.to_string()); + + self.get(path).await + } + + /// `GET v1/validator/execution_payload_envelope/{slot}/{builder_index}` in SSZ format + pub async fn get_validator_execution_payload_envelope_ssz<E: EthSpec>( + &self, + slot: Slot, + builder_index: u64, + ) -> Result<ExecutionPayloadEnvelope<E>, Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("execution_payload_envelope") + .push(&slot.to_string()) + .push(&builder_index.to_string()); + + let opt_response = self + .get_bytes_opt_accept_header(path, Accept::Ssz, self.timeouts.get_validator_block) + .await?; + + let response_bytes = opt_response.ok_or(Error::StatusCode(StatusCode::NOT_FOUND))?; + + ExecutionPayloadEnvelope::from_ssz_bytes(&response_bytes).map_err(Error::InvalidSsz) + } + + /// `POST v1/beacon/execution_payload_envelope` + pub async fn post_beacon_execution_payload_envelope<E: EthSpec>( + &self, + envelope: &SignedExecutionPayloadEnvelope<E>, + 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_envelope"); + + self.post_generic_with_consensus_version( + path, + envelope, + Some(self.timeouts.proposal), + fork_name, + ) + .await?; + + Ok(()) + } + + /// `POST v1/beacon/execution_payload_envelope` in SSZ format + pub async fn post_beacon_execution_payload_envelope_ssz<E: EthSpec>( + &self, + envelope: &SignedExecutionPayloadEnvelope<E>, + 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_envelope"); + + self.post_generic_with_consensus_version_and_ssz_body( + path, + envelope.as_ssz_bytes(), + Some(self.timeouts.proposal), + fork_name, + ) + .await?; + + Ok(()) + } + /// `GET v2/validator/blocks/{slot}` in ssz format pub async fn get_validator_blocks_ssz<E: EthSpec>( &self, diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index 0c9c0b95f0..f8376d430c 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -1723,7 +1723,7 @@ pub type JsonProduceBlockV3Response<E> = pub enum FullBlockContents<E: EthSpec> { /// This is a full deneb variant with block and blobs. BlockContents(BlockContents<E>), - /// This variant is for all pre-deneb full blocks. + /// This variant is for all pre-deneb full blocks or post-gloas beacon block. Block(BeaconBlock<E>), } @@ -1751,6 +1751,20 @@ pub struct ProduceBlockV3Metadata { pub consensus_block_value: Uint256, } +/// Metadata about a `produce_block_v4` response which is returned in the body & headers. +#[derive(Debug, Deserialize, Serialize)] +pub struct ProduceBlockV4Metadata { + // The consensus version is serialized & deserialized by `ForkVersionedResponse`. + #[serde( + skip_serializing, + skip_deserializing, + default = "dummy_consensus_version" + )] + pub consensus_version: ForkName, + #[serde(with = "serde_utils::u256_dec")] + pub consensus_block_value: Uint256, +} + impl<E: EthSpec> FullBlockContents<E> { pub fn new(block: BeaconBlock<E>, blob_data: Option<(KzgProofs<E>, BlobsList<E>)>) -> Self { match blob_data { @@ -1907,6 +1921,27 @@ impl TryFrom<&HeaderMap> for ProduceBlockV3Metadata { } } +impl TryFrom<&HeaderMap> for ProduceBlockV4Metadata { + type Error = String; + + fn try_from(headers: &HeaderMap) -> Result<Self, Self::Error> { + let consensus_version = parse_required_header(headers, CONSENSUS_VERSION_HEADER, |s| { + s.parse::<ForkName>() + .map_err(|e| format!("invalid {CONSENSUS_VERSION_HEADER}: {e:?}")) + })?; + let consensus_block_value = + parse_required_header(headers, CONSENSUS_BLOCK_VALUE_HEADER, |s| { + Uint256::from_str_radix(s, 10) + .map_err(|e| format!("invalid {CONSENSUS_BLOCK_VALUE_HEADER}: {e:?}")) + })?; + + Ok(ProduceBlockV4Metadata { + consensus_version, + consensus_block_value, + }) + } +} + /// A wrapper over a [`SignedBeaconBlock`] or a [`SignedBlockContents`]. #[derive(Clone, Debug, PartialEq, Encode, Serialize)] #[serde(untagged)] @@ -1954,7 +1989,7 @@ impl<E: EthSpec> PublishBlockRequest<E> { /// SSZ decode with fork variant determined by `fork_name`. pub fn from_ssz_bytes(bytes: &[u8], fork_name: ForkName) -> Result<Self, DecodeError> { - if fork_name.deneb_enabled() { + if fork_name.deneb_enabled() && !fork_name.gloas_enabled() { let mut builder = ssz::SszDecoderBuilder::new(bytes); builder.register_anonymous_variable_length_item()?; builder.register_type::<KzgProofs<E>>()?; diff --git a/consensus/state_processing/src/envelope_processing.rs b/consensus/state_processing/src/envelope_processing.rs index d46728dbbc..c2cfeae5d3 100644 --- a/consensus/state_processing/src/envelope_processing.rs +++ b/consensus/state_processing/src/envelope_processing.rs @@ -20,6 +20,23 @@ macro_rules! envelope_verify { }; } +/// The strategy to be used when validating the payloads state root. +#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] +#[derive(PartialEq, Clone, Copy)] +pub enum VerifyStateRoot { + /// Validate state root. + True, + /// Do not validate state root. Use with caution. + /// This should only be used when first constructing the payload envelope. + False, +} + +impl VerifyStateRoot { + pub fn is_true(self) -> bool { + self == VerifyStateRoot::True + } +} + #[derive(Debug, Clone)] pub enum EnvelopeProcessingError { /// Bad Signature @@ -111,6 +128,7 @@ pub fn process_execution_payload_envelope<E: EthSpec>( parent_state_root: Option<Hash256>, signed_envelope: &SignedExecutionPayloadEnvelope<E>, verify_signatures: VerifySignatures, + verify_state_root: VerifyStateRoot, spec: &ChainSpec, ) -> Result<(), EnvelopeProcessingError> { if verify_signatures.is_true() { @@ -264,15 +282,17 @@ pub fn process_execution_payload_envelope<E: EthSpec>( .map_err(EnvelopeProcessingError::BitFieldError)?; *state.latest_block_hash_mut()? = payload.block_hash; - // Verify the state root - let state_root = state.canonical_root()?; - envelope_verify!( - envelope.state_root == state_root, - EnvelopeProcessingError::InvalidStateRoot { - state: state_root, - envelope: envelope.state_root, - } - ); + if verify_state_root.is_true() { + // Verify the state root + let state_root = state.canonical_root()?; + envelope_verify!( + envelope.state_root == state_root, + EnvelopeProcessingError::InvalidStateRoot { + state: state_root, + envelope: envelope.state_root, + } + ); + } Ok(()) } diff --git a/consensus/types/src/builder/builder_bid.rs b/consensus/types/src/builder/builder_bid.rs index 1018fadb64..e706b01283 100644 --- a/consensus/types/src/builder/builder_bid.rs +++ b/consensus/types/src/builder/builder_bid.rs @@ -196,7 +196,7 @@ impl<E: EthSpec> SignedBuilderBid<E> { .pubkey() .decompress() .map(|pubkey| { - let domain = spec.get_builder_domain(); + let domain = spec.get_builder_application_domain(); let message = self.message.signing_root(domain); self.signature.verify(&pubkey, message) }) diff --git a/consensus/types/src/core/application_domain.rs b/consensus/types/src/core/application_domain.rs index 5e33f2dfd5..ff55a91034 100644 --- a/consensus/types/src/core/application_domain.rs +++ b/consensus/types/src/core/application_domain.rs @@ -4,6 +4,7 @@ pub const APPLICATION_DOMAIN_BUILDER: u32 = 16777216; #[derive(Debug, PartialEq, Clone, Copy)] pub enum ApplicationDomain { + /// NOTE: This domain is only used for out-of-protocol block building, DO NOT use it for Gloas/ePBS. Builder, } diff --git a/consensus/types/src/core/chain_spec.rs b/consensus/types/src/core/chain_spec.rs index 6d25e3baf4..adf87dee94 100644 --- a/consensus/types/src/core/chain_spec.rs +++ b/consensus/types/src/core/chain_spec.rs @@ -549,7 +549,9 @@ impl ChainSpec { // This should be updated to include the current fork and the genesis validators root, but discussion is ongoing: // // https://github.com/ethereum/builder-specs/issues/14 - pub fn get_builder_domain(&self) -> Hash256 { + // + // NOTE: This domain is only used for out-of-protocol block building, DO NOT use it for Gloas/ePBS. + pub fn get_builder_application_domain(&self) -> Hash256 { self.compute_domain( Domain::ApplicationMask(ApplicationDomain::Builder), self.genesis_fork_version, diff --git a/consensus/types/src/validator/validator_registration_data.rs b/consensus/types/src/validator/validator_registration_data.rs index a0a1df7dc5..df2293cbae 100644 --- a/consensus/types/src/validator/validator_registration_data.rs +++ b/consensus/types/src/validator/validator_registration_data.rs @@ -31,7 +31,7 @@ impl SignedValidatorRegistrationData { .pubkey .decompress() .map(|pubkey| { - let domain = spec.get_builder_domain(); + let domain = spec.get_builder_application_domain(); let message = self.message.signing_root(domain); self.signature.verify(&pubkey, message) }) diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index 2f08727045..59d2bef24e 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -5,6 +5,7 @@ use crate::decode::{ssz_decode_file, ssz_decode_file_with, ssz_decode_state, yam use serde::Deserialize; use ssz::Decode; use state_processing::common::update_progressive_balances_cache::initialize_progressive_balances_cache; +use state_processing::envelope_processing::VerifyStateRoot; use state_processing::epoch_cache::initialize_epoch_cache; use state_processing::per_block_processing::process_operations::{ process_consolidation_requests, process_deposit_requests_post_gloas, @@ -458,7 +459,14 @@ impl<E: EthSpec> Operation<E> for SignedExecutionPayloadEnvelope<E> { .as_ref() .is_some_and(|e| e.execution_valid); if valid { - process_execution_payload_envelope(state, None, self, VerifySignatures::True, spec) + process_execution_payload_envelope( + state, + None, + self, + VerifySignatures::True, + VerifyStateRoot::True, + spec, + ) } else { Err(EnvelopeProcessingError::ExecutionInvalid) } diff --git a/validator_client/lighthouse_validator_store/src/lib.rs b/validator_client/lighthouse_validator_store/src/lib.rs index 7b6a582363..7806482ffb 100644 --- a/validator_client/lighthouse_validator_store/src/lib.rs +++ b/validator_client/lighthouse_validator_store/src/lib.rs @@ -20,12 +20,12 @@ use task_executor::TaskExecutor; use tracing::{error, info, instrument, warn}; use types::{ AbstractExecPayload, Address, AggregateAndProof, Attestation, BeaconBlock, BlindedPayload, - ChainSpec, ContributionAndProof, Domain, Epoch, EthSpec, Fork, Graffiti, Hash256, - SelectionProof, SignedAggregateAndProof, SignedBeaconBlock, SignedContributionAndProof, - SignedRoot, SignedValidatorRegistrationData, SignedVoluntaryExit, Slot, - SyncAggregatorSelectionData, SyncCommitteeContribution, SyncCommitteeMessage, - SyncSelectionProof, SyncSubnetId, ValidatorRegistrationData, VoluntaryExit, - graffiti::GraffitiString, + ChainSpec, ContributionAndProof, Domain, Epoch, EthSpec, ExecutionPayloadEnvelope, Fork, + FullPayload, Graffiti, Hash256, SelectionProof, SignedAggregateAndProof, SignedBeaconBlock, + SignedContributionAndProof, SignedExecutionPayloadEnvelope, SignedRoot, + SignedValidatorRegistrationData, SignedVoluntaryExit, Slot, SyncAggregatorSelectionData, + SyncCommitteeContribution, SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, + ValidatorRegistrationData, VoluntaryExit, graffiti::GraffitiString, }; use validator_store::{ DoppelgangerStatus, Error as ValidatorStoreError, ProposalData, SignedBlock, UnsignedBlock, @@ -954,7 +954,7 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore for LighthouseValidatorS &self, validator_registration_data: ValidatorRegistrationData, ) -> Result<SignedValidatorRegistrationData, Error> { - let domain_hash = self.spec.get_builder_domain(); + let domain_hash = self.spec.get_builder_application_domain(); let signing_root = validator_registration_data.signing_root(domain_hash); let signing_method = @@ -1242,4 +1242,35 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore for LighthouseValidatorS .get_builder_proposals_defaulting(validator.get_builder_proposals()), }) } + + /// Sign an `ExecutionPayloadEnvelope` for Gloas (local building). + /// The proposer acts as the builder and signs with the BeaconBuilder domain. + async fn sign_execution_payload_envelope( + &self, + validator_pubkey: PublicKeyBytes, + envelope: ExecutionPayloadEnvelope<E>, + ) -> Result<SignedExecutionPayloadEnvelope<E>, Error> { + let signing_context = self.signing_context( + Domain::BeaconBuilder, + envelope.slot.epoch(E::slots_per_epoch()), + ); + + // Execution payload envelope signing is not slashable, bypass doppelganger protection. + let signing_method = self.doppelganger_bypassed_signing_method(validator_pubkey)?; + + let signature = signing_method + .get_signature::<E, FullPayload<E>>( + SignableMessage::ExecutionPayloadEnvelope(&envelope), + signing_context, + &self.spec, + &self.task_executor, + ) + .await + .map_err(Error::SpecificError)?; + + Ok(SignedExecutionPayloadEnvelope { + message: envelope, + signature, + }) + } } diff --git a/validator_client/signing_method/src/lib.rs b/validator_client/signing_method/src/lib.rs index bf3cc6a17d..c132d86c17 100644 --- a/validator_client/signing_method/src/lib.rs +++ b/validator_client/signing_method/src/lib.rs @@ -49,6 +49,7 @@ pub enum SignableMessage<'a, E: EthSpec, Payload: AbstractExecPayload<E> = FullP SignedContributionAndProof(&'a ContributionAndProof<E>), ValidatorRegistration(&'a ValidatorRegistrationData), VoluntaryExit(&'a VoluntaryExit), + ExecutionPayloadEnvelope(&'a ExecutionPayloadEnvelope<E>), } impl<E: EthSpec, Payload: AbstractExecPayload<E>> SignableMessage<'_, E, Payload> { @@ -70,6 +71,7 @@ impl<E: EthSpec, Payload: AbstractExecPayload<E>> SignableMessage<'_, E, Payload SignableMessage::SignedContributionAndProof(c) => c.signing_root(domain), SignableMessage::ValidatorRegistration(v) => v.signing_root(domain), SignableMessage::VoluntaryExit(exit) => exit.signing_root(domain), + SignableMessage::ExecutionPayloadEnvelope(e) => e.signing_root(domain), } } } @@ -233,6 +235,9 @@ impl SigningMethod { Web3SignerObject::ValidatorRegistration(v) } SignableMessage::VoluntaryExit(e) => Web3SignerObject::VoluntaryExit(e), + SignableMessage::ExecutionPayloadEnvelope(e) => { + Web3SignerObject::ExecutionPayloadEnvelope(e) + } }; // Determine the Web3Signer message type. diff --git a/validator_client/signing_method/src/web3signer.rs b/validator_client/signing_method/src/web3signer.rs index 246d9e9e09..e6fc8f3ba2 100644 --- a/validator_client/signing_method/src/web3signer.rs +++ b/validator_client/signing_method/src/web3signer.rs @@ -19,6 +19,8 @@ pub enum MessageType { SyncCommitteeSelectionProof, SyncCommitteeContributionAndProof, ValidatorRegistration, + // TODO(gloas) verify w/ web3signer specs + ExecutionPayloadEnvelope, } #[derive(Debug, PartialEq, Copy, Clone, Serialize)] @@ -75,6 +77,7 @@ pub enum Web3SignerObject<'a, E: EthSpec, Payload: AbstractExecPayload<E>> { SyncAggregatorSelectionData(&'a SyncAggregatorSelectionData), ContributionAndProof(&'a ContributionAndProof<E>), ValidatorRegistration(&'a ValidatorRegistrationData), + ExecutionPayloadEnvelope(&'a ExecutionPayloadEnvelope<E>), } impl<'a, E: EthSpec, Payload: AbstractExecPayload<E>> Web3SignerObject<'a, E, Payload> { @@ -140,6 +143,7 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload<E>> Web3SignerObject<'a, E, Pa MessageType::SyncCommitteeContributionAndProof } Web3SignerObject::ValidatorRegistration(_) => MessageType::ValidatorRegistration, + Web3SignerObject::ExecutionPayloadEnvelope(_) => MessageType::ExecutionPayloadEnvelope, } } } diff --git a/validator_client/validator_services/src/block_service.rs b/validator_client/validator_services/src/block_service.rs index df4c9b223c..1535f50663 100644 --- a/validator_client/validator_services/src/block_service.rs +++ b/validator_client/validator_services/src/block_service.rs @@ -14,6 +14,7 @@ use std::time::Duration; use task_executor::TaskExecutor; use tokio::sync::mpsc; use tracing::{Instrument, debug, error, info, info_span, instrument, trace, warn}; +use types::consts::gloas::BUILDER_INDEX_SELF_BUILD; use types::{BlockType, ChainSpec, EthSpec, Graffiti, Slot}; use validator_store::{Error as ValidatorStoreError, SignedBlock, UnsignedBlock, ValidatorStore}; @@ -334,7 +335,7 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> BlockService<S, T> { #[instrument(skip_all, fields(%slot, ?validator_pubkey))] async fn sign_and_publish_block( &self, - proposer_fallback: ProposerFallback<T>, + proposer_fallback: &ProposerFallback<T>, slot: Slot, graffiti: Option<Graffiti>, validator_pubkey: &PublicKeyBytes, @@ -460,73 +461,145 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> BlockService<S, T> { info!(slot = slot.as_u64(), "Requesting unsigned block"); - // Request an SSZ block from all beacon nodes in order, returning on the first successful response. - // If all nodes fail, run a second pass falling back to JSON. - // - // Proposer nodes will always be tried last during each pass since it's likely that they don't have a - // great view of attestations on the network. - let ssz_block_response = proposer_fallback - .request_proposers_last(|beacon_node| async move { - let _get_timer = validator_metrics::start_timer_vec( - &validator_metrics::BLOCK_SERVICE_TIMES, - &[validator_metrics::BEACON_BLOCK_HTTP_GET], - ); - beacon_node - .get_validator_blocks_v3_ssz::<S::E>( - slot, - randao_reveal_ref, - graffiti.as_ref(), - builder_boost_factor, - self_ref.graffiti_policy, - ) - .await - }) - .await; + // Check if Gloas fork is active at this slot + let fork_name = self_ref.chain_spec.fork_name_at_slot::<S::E>(slot); - let block_response = match ssz_block_response { - Ok((ssz_block_response, _metadata)) => ssz_block_response, - Err(e) => { - warn!( - slot = slot.as_u64(), - error = %e, - "SSZ block production failed, falling back to JSON" - ); + let (block_proposer, unsigned_block) = if fork_name.gloas_enabled() { + // Use V4 block production for Gloas + // Request an SSZ block from all beacon nodes in order, returning on the first successful response. + // If all nodes fail, run a second pass falling back to JSON. + let ssz_block_response = proposer_fallback + .request_proposers_last(|beacon_node| async move { + let _get_timer = validator_metrics::start_timer_vec( + &validator_metrics::BLOCK_SERVICE_TIMES, + &[validator_metrics::BEACON_BLOCK_HTTP_GET], + ); + beacon_node + .get_validator_blocks_v4_ssz::<S::E>( + slot, + randao_reveal_ref, + graffiti.as_ref(), + builder_boost_factor, + self_ref.graffiti_policy, + ) + .await + }) + .await; - proposer_fallback - .request_proposers_last(|beacon_node| async move { - let _get_timer = validator_metrics::start_timer_vec( - &validator_metrics::BLOCK_SERVICE_TIMES, - &[validator_metrics::BEACON_BLOCK_HTTP_GET], - ); - let (json_block_response, _metadata) = beacon_node - .get_validator_blocks_v3::<S::E>( - slot, - randao_reveal_ref, - graffiti.as_ref(), - builder_boost_factor, - self_ref.graffiti_policy, - ) - .await - .map_err(|e| { - BlockError::Recoverable(format!( - "Error from beacon node when producing block: {:?}", - e - )) - })?; + let block_response = match ssz_block_response { + Ok((ssz_block_response, _metadata)) => ssz_block_response, + Err(e) => { + warn!( + slot = slot.as_u64(), + error = %e, + "SSZ V4 block production failed, falling back to JSON" + ); - Ok(json_block_response.data) - }) - .await - .map_err(BlockError::from)? - } - }; + proposer_fallback + .request_proposers_last(|beacon_node| async move { + let _get_timer = validator_metrics::start_timer_vec( + &validator_metrics::BLOCK_SERVICE_TIMES, + &[validator_metrics::BEACON_BLOCK_HTTP_GET], + ); + let (json_block_response, _metadata) = beacon_node + .get_validator_blocks_v4::<S::E>( + slot, + randao_reveal_ref, + graffiti.as_ref(), + builder_boost_factor, + self_ref.graffiti_policy, + ) + .await + .map_err(|e| { + BlockError::Recoverable(format!( + "Error from beacon node when producing block: {:?}", + e + )) + })?; - let (block_proposer, unsigned_block) = match block_response { - eth2::types::ProduceBlockV3Response::Full(block) => { - (block.block().proposer_index(), UnsignedBlock::Full(block)) - } - eth2::types::ProduceBlockV3Response::Blinded(block) => { - (block.proposer_index(), UnsignedBlock::Blinded(block)) + Ok(json_block_response.data) + }) + .await + .map_err(BlockError::from)? + } + }; + + // Gloas blocks don't have blobs (they're in the execution layer) + let block_contents = eth2::types::FullBlockContents::Block(block_response); + ( + block_contents.block().proposer_index(), + UnsignedBlock::Full(block_contents), + ) + } else { + // Use V3 block production for pre-Gloas forks + // Request an SSZ block from all beacon nodes in order, returning on the first successful response. + // If all nodes fail, run a second pass falling back to JSON. + // + // Proposer nodes will always be tried last during each pass since it's likely that they don't have a + // great view of attestations on the network. + let ssz_block_response = proposer_fallback + .request_proposers_last(|beacon_node| async move { + let _get_timer = validator_metrics::start_timer_vec( + &validator_metrics::BLOCK_SERVICE_TIMES, + &[validator_metrics::BEACON_BLOCK_HTTP_GET], + ); + beacon_node + .get_validator_blocks_v3_ssz::<S::E>( + slot, + randao_reveal_ref, + graffiti.as_ref(), + builder_boost_factor, + self_ref.graffiti_policy, + ) + .await + }) + .await; + + let block_response = match ssz_block_response { + Ok((ssz_block_response, _metadata)) => ssz_block_response, + Err(e) => { + warn!( + slot = slot.as_u64(), + error = %e, + "SSZ block production failed, falling back to JSON" + ); + + proposer_fallback + .request_proposers_last(|beacon_node| async move { + let _get_timer = validator_metrics::start_timer_vec( + &validator_metrics::BLOCK_SERVICE_TIMES, + &[validator_metrics::BEACON_BLOCK_HTTP_GET], + ); + let (json_block_response, _metadata) = beacon_node + .get_validator_blocks_v3::<S::E>( + slot, + randao_reveal_ref, + graffiti.as_ref(), + builder_boost_factor, + self_ref.graffiti_policy, + ) + .await + .map_err(|e| { + BlockError::Recoverable(format!( + "Error from beacon node when producing block: {:?}", + e + )) + })?; + + Ok(json_block_response.data) + }) + .await + .map_err(BlockError::from)? + } + }; + + match block_response { + eth2::types::ProduceBlockV3Response::Full(block) => { + (block.block().proposer_index(), UnsignedBlock::Full(block)) + } + eth2::types::ProduceBlockV3Response::Blinded(block) => { + (block.proposer_index(), UnsignedBlock::Blinded(block)) + } } }; @@ -539,7 +612,7 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> BlockService<S, T> { self_ref .sign_and_publish_block( - proposer_fallback, + &proposer_fallback, slot, graffiti, &validator_pubkey, @@ -547,6 +620,108 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> BlockService<S, T> { ) .await?; + // TODO(gloas) we only need to fetch, sign and publish the envelope in the local building case. + // Right now we always default to local building. Once we implement trustless/trusted builder logic + // we should check the bid for index == BUILDER_INDEX_SELF_BUILD + if fork_name.gloas_enabled() { + self_ref + .fetch_sign_and_publish_payload_envelope( + &proposer_fallback, + slot, + &validator_pubkey, + ) + .await?; + } + + Ok(()) + } + + /// Fetch, sign, and publish the execution payload envelope for Gloas. + /// This should be called after the block has been published. + /// + /// TODO(gloas): For multi-BN setups, we need to track which beacon node produced the block + /// and fetch the envelope from that same node. The envelope is cached per-BN, + /// so fetching from a different BN than the one that built the block will fail. + /// See: https://github.com/sigp/lighthouse/pull/8313 + #[instrument(skip_all)] + async fn fetch_sign_and_publish_payload_envelope( + &self, + _proposer_fallback: &ProposerFallback<T>, + slot: Slot, + validator_pubkey: &PublicKeyBytes, + ) -> Result<(), BlockError> { + info!(slot = slot.as_u64(), "Fetching execution payload envelope"); + + // Fetch the envelope from the beacon node. Use builder_index=BUILDER_INDEX_SELF_BUILD for local building. + // TODO(gloas): Use proposer_fallback once multi-BN is supported. + let envelope = self + .beacon_nodes + .first_success(|beacon_node| async move { + beacon_node + .get_validator_execution_payload_envelope_ssz::<S::E>( + slot, + BUILDER_INDEX_SELF_BUILD, + ) + .await + .map_err(|e| { + BlockError::Recoverable(format!( + "Error fetching execution payload envelope: {:?}", + e + )) + }) + }) + .await?; + + info!( + slot = slot.as_u64(), + beacon_block_root = %envelope.beacon_block_root, + "Received execution payload envelope, signing" + ); + + // Sign the envelope + let signed_envelope = self + .validator_store + .sign_execution_payload_envelope(*validator_pubkey, envelope) + .await + .map_err(|e| { + BlockError::Recoverable(format!( + "Error signing execution payload envelope: {:?}", + e + )) + })?; + + info!( + slot = slot.as_u64(), + "Signed execution payload envelope, publishing" + ); + + let fork_name = self.chain_spec.fork_name_at_slot::<S::E>(slot); + + // Publish the signed envelope + // TODO(gloas): Use proposer_fallback once multi-BN is supported. + self.beacon_nodes + .first_success(|beacon_node| { + let signed_envelope = signed_envelope.clone(); + async move { + beacon_node + .post_beacon_execution_payload_envelope_ssz(&signed_envelope, fork_name) + .await + .map_err(|e| { + BlockError::Recoverable(format!( + "Error publishing execution payload envelope: {:?}", + e + )) + }) + } + }) + .await?; + + info!( + slot = slot.as_u64(), + beacon_block_root = %signed_envelope.message.beacon_block_root, + "Successfully published signed execution payload envelope" + ); + Ok(()) } diff --git a/validator_client/validator_store/src/lib.rs b/validator_client/validator_store/src/lib.rs index 4fdbb8064c..87ab669e8d 100644 --- a/validator_client/validator_store/src/lib.rs +++ b/validator_client/validator_store/src/lib.rs @@ -5,8 +5,9 @@ use std::fmt::Debug; use std::future::Future; use std::sync::Arc; use types::{ - Address, Attestation, AttestationError, BlindedBeaconBlock, Epoch, EthSpec, Graffiti, Hash256, - SelectionProof, SignedAggregateAndProof, SignedBlindedBeaconBlock, SignedContributionAndProof, + Address, Attestation, AttestationError, BlindedBeaconBlock, Epoch, EthSpec, + ExecutionPayloadEnvelope, Graffiti, Hash256, SelectionProof, SignedAggregateAndProof, + SignedBlindedBeaconBlock, SignedContributionAndProof, SignedExecutionPayloadEnvelope, SignedValidatorRegistrationData, Slot, SyncCommitteeContribution, SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, ValidatorRegistrationData, }; @@ -178,6 +179,13 @@ pub trait ValidatorStore: Send + Sync { /// runs. fn prune_slashing_protection_db(&self, current_epoch: Epoch, first_run: bool); + /// Sign an `ExecutionPayloadEnvelope` for Gloas. + fn sign_execution_payload_envelope( + &self, + validator_pubkey: PublicKeyBytes, + envelope: ExecutionPayloadEnvelope<Self::E>, + ) -> impl Future<Output = Result<SignedExecutionPayloadEnvelope<Self::E>, Error<Self::Error>>> + Send; + /// Returns `ProposalData` for the provided `pubkey` if it exists in `InitializedValidators`. /// `ProposalData` fields include defaulting logic described in `get_fee_recipient_defaulting`, /// `get_gas_limit_defaulting`, and `get_builder_proposals_defaulting`. From 67b96731913e6a12aef8c585dbdb6c2eff4c2541 Mon Sep 17 00:00:00 2001 From: Michael Sproul <michaelsproul@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:50:44 +1100 Subject: [PATCH 24/81] Gloas payload attestation consensus (#8827) - Implement `process_payload_attestation` - Implement EF tests for payload attestations (allows simplification of handler now that we support all `operations` tests). - Update the `BlockSignatureVerifier` to signature-verify payload attestations Co-Authored-By: Michael Sproul <michael@sigmaprime.io> --- .../common/get_payload_attesting_indices.rs | 42 +++++++++++++++ consensus/state_processing/src/common/mod.rs | 4 ++ .../state_processing/src/consensus_context.rs | 32 +++++++++-- .../src/per_block_processing.rs | 3 ++ .../block_signature_verifier.rs | 46 +++++++++++++++- .../src/per_block_processing/errors.rs | 51 ++++++++++++++++++ .../is_valid_indexed_payload_attestation.rs | 32 +++++++++++ .../process_operations.rs | 54 ++++++++++++++++++- .../per_block_processing/signature_sets.rs | 42 +++++++++++++-- .../verify_payload_attestation.rs | 46 ++++++++++++++++ testing/ef_tests/check_all_files_accessed.py | 2 - testing/ef_tests/src/cases/operations.rs | 33 ++++++++++-- testing/ef_tests/src/handler.rs | 19 ------- testing/ef_tests/tests/tests.rs | 6 +++ 14 files changed, 378 insertions(+), 34 deletions(-) create mode 100644 consensus/state_processing/src/common/get_payload_attesting_indices.rs create mode 100644 consensus/state_processing/src/per_block_processing/is_valid_indexed_payload_attestation.rs create mode 100644 consensus/state_processing/src/per_block_processing/verify_payload_attestation.rs diff --git a/consensus/state_processing/src/common/get_payload_attesting_indices.rs b/consensus/state_processing/src/common/get_payload_attesting_indices.rs new file mode 100644 index 0000000000..407e4f1372 --- /dev/null +++ b/consensus/state_processing/src/common/get_payload_attesting_indices.rs @@ -0,0 +1,42 @@ +use crate::per_block_processing::errors::{ + BlockOperationError, PayloadAttestationInvalid as Invalid, +}; +use ssz_types::VariableList; +use types::{ + BeaconState, BeaconStateError, ChainSpec, EthSpec, IndexedPayloadAttestation, + PayloadAttestation, +}; + +pub fn get_indexed_payload_attestation<E: EthSpec>( + state: &BeaconState<E>, + payload_attestation: &PayloadAttestation<E>, + spec: &ChainSpec, +) -> Result<IndexedPayloadAttestation<E>, BlockOperationError<Invalid>> { + let attesting_indices = get_payload_attesting_indices(state, payload_attestation, spec)?; + + Ok(IndexedPayloadAttestation { + attesting_indices: VariableList::new(attesting_indices)?, + data: payload_attestation.data.clone(), + signature: payload_attestation.signature.clone(), + }) +} + +pub fn get_payload_attesting_indices<E: EthSpec>( + state: &BeaconState<E>, + payload_attestation: &PayloadAttestation<E>, + spec: &ChainSpec, +) -> Result<Vec<u64>, BeaconStateError> { + let slot = payload_attestation.data.slot; + let ptc = state.get_ptc(slot, spec)?; + let bits = &payload_attestation.aggregation_bits; + + let mut attesting_indices = vec![]; + for (i, index) in ptc.into_iter().enumerate() { + if let Ok(true) = bits.get(i) { + attesting_indices.push(index as u64); + } + } + attesting_indices.sort_unstable(); + + Ok(attesting_indices) +} diff --git a/consensus/state_processing/src/common/mod.rs b/consensus/state_processing/src/common/mod.rs index 0287748fd0..e550a6c48b 100644 --- a/consensus/state_processing/src/common/mod.rs +++ b/consensus/state_processing/src/common/mod.rs @@ -1,6 +1,7 @@ mod deposit_data_tree; mod get_attestation_participation; mod get_attesting_indices; +mod get_payload_attesting_indices; mod initiate_validator_exit; mod slash_validator; @@ -13,6 +14,9 @@ pub use get_attestation_participation::get_attestation_participation_flag_indice pub use get_attesting_indices::{ attesting_indices_base, attesting_indices_electra, get_attesting_indices_from_state, }; +pub use get_payload_attesting_indices::{ + get_indexed_payload_attestation, get_payload_attesting_indices, +}; pub use initiate_validator_exit::initiate_validator_exit; pub use slash_validator::slash_validator; diff --git a/consensus/state_processing/src/consensus_context.rs b/consensus/state_processing/src/consensus_context.rs index 07d554e303..bc7bd20384 100644 --- a/consensus/state_processing/src/consensus_context.rs +++ b/consensus/state_processing/src/consensus_context.rs @@ -1,11 +1,16 @@ use crate::EpochCacheError; -use crate::common::{attesting_indices_base, attesting_indices_electra}; -use crate::per_block_processing::errors::{AttestationInvalid, BlockOperationError}; +use crate::common::{ + attesting_indices_base, attesting_indices_electra, get_indexed_payload_attestation, +}; +use crate::per_block_processing::errors::{ + AttestationInvalid, BlockOperationError, PayloadAttestationInvalid, +}; use std::collections::{HashMap, hash_map::Entry}; use tree_hash::TreeHash; use types::{ AbstractExecPayload, AttestationRef, BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, - Hash256, IndexedAttestation, IndexedAttestationRef, SignedBeaconBlock, Slot, + Hash256, IndexedAttestation, IndexedAttestationRef, IndexedPayloadAttestation, + PayloadAttestation, SignedBeaconBlock, Slot, }; #[derive(Debug, PartialEq, Clone)] @@ -22,6 +27,8 @@ pub struct ConsensusContext<E: EthSpec> { pub current_block_root: Option<Hash256>, /// Cache of indexed attestations constructed during block processing. pub indexed_attestations: HashMap<Hash256, IndexedAttestation<E>>, + /// Cache of indexed payload attestations constructed during block processing. + pub indexed_payload_attestations: HashMap<Hash256, IndexedPayloadAttestation<E>>, } #[derive(Debug, PartialEq, Clone)] @@ -55,6 +62,7 @@ impl<E: EthSpec> ConsensusContext<E> { proposer_index: None, current_block_root: None, indexed_attestations: HashMap::new(), + indexed_payload_attestations: HashMap::new(), } } @@ -177,6 +185,24 @@ impl<E: EthSpec> ConsensusContext<E> { .map(|indexed_attestation| (*indexed_attestation).to_ref()) } + pub fn get_indexed_payload_attestation<'a>( + &'a mut self, + state: &BeaconState<E>, + payload_attestation: &'a PayloadAttestation<E>, + spec: &ChainSpec, + ) -> Result<&'a IndexedPayloadAttestation<E>, BlockOperationError<PayloadAttestationInvalid>> + { + let key = payload_attestation.tree_hash_root(); + match self.indexed_payload_attestations.entry(key) { + Entry::Occupied(occupied) => Ok(occupied.into_mut()), + Entry::Vacant(vacant) => { + let indexed_payload_attestation = + get_indexed_payload_attestation(state, payload_attestation, spec)?; + Ok(vacant.insert(indexed_payload_attestation)) + } + } + } + pub fn num_cached_indexed_attestations(&self) -> usize { self.indexed_attestations.len() } diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index d9a41418cf..037e1c7cc7 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -20,6 +20,7 @@ pub use self::verify_proposer_slashing::verify_proposer_slashing; pub use altair::sync_committee::process_sync_aggregate; pub use block_signature_verifier::{BlockSignatureVerifier, ParallelSignatureSets}; pub use is_valid_indexed_attestation::is_valid_indexed_attestation; +pub use is_valid_indexed_payload_attestation::is_valid_indexed_payload_attestation; pub use process_operations::process_operations; pub use verify_attestation::{ verify_attestation_for_block_inclusion, verify_attestation_for_state, @@ -37,6 +38,7 @@ pub mod builder; pub mod deneb; pub mod errors; mod is_valid_indexed_attestation; +mod is_valid_indexed_payload_attestation; pub mod process_operations; pub mod signature_sets; pub mod tests; @@ -45,6 +47,7 @@ mod verify_attester_slashing; mod verify_bls_to_execution_change; mod verify_deposit; mod verify_exit; +mod verify_payload_attestation; mod verify_proposer_slashing; pub mod withdrawals; diff --git a/consensus/state_processing/src/per_block_processing/block_signature_verifier.rs b/consensus/state_processing/src/per_block_processing/block_signature_verifier.rs index e82ce537fd..eea9c17a14 100644 --- a/consensus/state_processing/src/per_block_processing/block_signature_verifier.rs +++ b/consensus/state_processing/src/per_block_processing/block_signature_verifier.rs @@ -1,7 +1,9 @@ #![allow(clippy::arithmetic_side_effects)] use super::signature_sets::{Error as SignatureSetError, *}; -use crate::per_block_processing::errors::{AttestationInvalid, BlockOperationError}; +use crate::per_block_processing::errors::{ + AttestationInvalid, BlockOperationError, PayloadAttestationInvalid, +}; use crate::{ConsensusContext, ContextError}; use bls::{PublicKey, PublicKeyBytes, SignatureSet, verify_signature_sets}; use std::borrow::Cow; @@ -18,6 +20,8 @@ pub enum Error { SignatureInvalid, /// An attestation in the block was invalid. The block is invalid. AttestationValidationError(BlockOperationError<AttestationInvalid>), + /// A payload attestation in the block was invalid. The block is invalid. + PayloadAttestationValidationError(BlockOperationError<PayloadAttestationInvalid>), /// There was an error attempting to read from a `BeaconState`. Block /// validity was not determined. BeaconStateError(BeaconStateError), @@ -66,6 +70,12 @@ impl From<BlockOperationError<AttestationInvalid>> for Error { } } +impl From<BlockOperationError<PayloadAttestationInvalid>> for Error { + fn from(e: BlockOperationError<PayloadAttestationInvalid>) -> Error { + Error::PayloadAttestationValidationError(e) + } +} + /// Reads the BLS signatures and keys from a `SignedBeaconBlock`, storing them as a `Vec<SignatureSet>`. /// /// This allows for optimizations related to batch BLS operations (see the @@ -171,6 +181,7 @@ where self.include_sync_aggregate(block)?; self.include_bls_to_execution_changes(block)?; self.include_execution_payload_bid(block)?; + self.include_payload_attestations(block, ctxt)?; Ok(()) } @@ -296,6 +307,39 @@ where }) } + /// Includes all signatures in `self.block.body.payload_attestations` for verification. + pub fn include_payload_attestations<Payload: AbstractExecPayload<E>>( + &mut self, + block: &'a SignedBeaconBlock<E, Payload>, + ctxt: &mut ConsensusContext<E>, + ) -> Result<()> { + let Ok(payload_attestations) = block.message().body().payload_attestations() else { + // Nothing to do pre-Gloas. + return Ok(()); + }; + + self.sets.sets.reserve(payload_attestations.len()); + + payload_attestations + .iter() + .try_for_each(|payload_attestation| { + let indexed_payload_attestation = ctxt.get_indexed_payload_attestation( + self.state, + payload_attestation, + self.spec, + )?; + + self.sets.push(indexed_payload_attestation_signature_set( + self.state, + self.get_pubkey.clone(), + &payload_attestation.signature, + indexed_payload_attestation, + self.spec, + )?); + Ok(()) + }) + } + /// Includes all signatures in `self.block.body.voluntary_exits` for verification. pub fn include_exits<Payload: AbstractExecPayload<E>>( &mut self, diff --git a/consensus/state_processing/src/per_block_processing/errors.rs b/consensus/state_processing/src/per_block_processing/errors.rs index 53178a7a64..71083378db 100644 --- a/consensus/state_processing/src/per_block_processing/errors.rs +++ b/consensus/state_processing/src/per_block_processing/errors.rs @@ -41,6 +41,10 @@ pub enum BlockProcessingError { index: usize, reason: AttestationInvalid, }, + PayloadAttestationInvalid { + index: usize, + reason: PayloadAttestationInvalid, + }, DepositInvalid { index: usize, reason: DepositInvalid, @@ -217,6 +221,7 @@ impl_into_block_processing_error_with_index!( AttesterSlashingInvalid, IndexedAttestationInvalid, AttestationInvalid, + PayloadAttestationInvalid, DepositInvalid, ExitInvalid, BlsExecutionChangeInvalid @@ -422,6 +427,52 @@ pub enum IndexedAttestationInvalid { SignatureSetError(SignatureSetError), } +#[derive(Debug, PartialEq, Clone)] +pub enum PayloadAttestationInvalid { + /// Block root does not match the parent beacon block root. + BlockRootMismatch { + expected: Hash256, + found: Hash256, + }, + /// The attestation slot is not the previous slot. + SlotMismatch { + expected: Slot, + found: Slot, + }, + BadIndexedPayloadAttestation(IndexedPayloadAttestationInvalid), +} + +impl From<BlockOperationError<IndexedPayloadAttestationInvalid>> + for BlockOperationError<PayloadAttestationInvalid> +{ + fn from(e: BlockOperationError<IndexedPayloadAttestationInvalid>) -> Self { + match e { + BlockOperationError::Invalid(e) => BlockOperationError::invalid( + PayloadAttestationInvalid::BadIndexedPayloadAttestation(e), + ), + BlockOperationError::BeaconStateError(e) => BlockOperationError::BeaconStateError(e), + BlockOperationError::SignatureSetError(e) => BlockOperationError::SignatureSetError(e), + BlockOperationError::SszTypesError(e) => BlockOperationError::SszTypesError(e), + BlockOperationError::BitfieldError(e) => BlockOperationError::BitfieldError(e), + BlockOperationError::ConsensusContext(e) => BlockOperationError::ConsensusContext(e), + BlockOperationError::ArithError(e) => BlockOperationError::ArithError(e), + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum IndexedPayloadAttestationInvalid { + /// The number of indices is 0. + IndicesEmpty, + /// The validator indices were not in increasing order. + BadValidatorIndicesOrdering, + /// The indexed attestation aggregate signature was not valid. + BadSignature, + /// There was an error whilst attempting to get a set of signatures. The signatures may have + /// been invalid or an internal error occurred. + SignatureSetError(SignatureSetError), +} + #[derive(Debug, PartialEq, Clone)] pub enum DepositInvalid { /// The signature (proof-of-possession) does not match the given pubkey. diff --git a/consensus/state_processing/src/per_block_processing/is_valid_indexed_payload_attestation.rs b/consensus/state_processing/src/per_block_processing/is_valid_indexed_payload_attestation.rs new file mode 100644 index 0000000000..534f553247 --- /dev/null +++ b/consensus/state_processing/src/per_block_processing/is_valid_indexed_payload_attestation.rs @@ -0,0 +1,32 @@ +use super::errors::{BlockOperationError, IndexedPayloadAttestationInvalid as Invalid}; +use super::signature_sets::{get_pubkey_from_state, indexed_payload_attestation_signature_set}; +use crate::VerifySignatures; +use types::*; + +pub fn is_valid_indexed_payload_attestation<E: EthSpec>( + state: &BeaconState<E>, + indexed_payload_attestation: &IndexedPayloadAttestation<E>, + verify_signatures: VerifySignatures, + spec: &ChainSpec, +) -> Result<(), BlockOperationError<Invalid>> { + // Verify indices are non-empty and sorted (duplicates allowed) + let indices = &indexed_payload_attestation.attesting_indices; + verify!(!indices.is_empty(), Invalid::IndicesEmpty); + verify!(indices.is_sorted(), Invalid::BadValidatorIndicesOrdering); + + if verify_signatures.is_true() { + verify!( + indexed_payload_attestation_signature_set( + state, + |i| get_pubkey_from_state(state, i), + &indexed_payload_attestation.signature, + indexed_payload_attestation, + spec + )? + .verify(), + Invalid::BadSignature + ); + } + + Ok(()) +} diff --git a/consensus/state_processing/src/per_block_processing/process_operations.rs b/consensus/state_processing/src/per_block_processing/process_operations.rs index 19109f1508..b037c74484 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -5,6 +5,7 @@ use crate::common::{ slash_validator, }; use crate::per_block_processing::errors::{BlockProcessingError, IntoWithIndex}; +use crate::per_block_processing::verify_payload_attestation::verify_payload_attestation; use bls::{PublicKeyBytes, SignatureBytes}; use ssz_types::FixedVector; use typenum::U33; @@ -39,8 +40,15 @@ pub fn process_operations<E: EthSpec, Payload: AbstractExecPayload<E>>( process_bls_to_execution_changes(state, bls_to_execution_changes, verify_signatures, spec)?; } - if state.fork_name_unchecked().electra_enabled() && !state.fork_name_unchecked().gloas_enabled() - { + if state.fork_name_unchecked().gloas_enabled() { + process_payload_attestations( + state, + block_body.payload_attestations()?.iter(), + verify_signatures, + ctxt, + spec, + )?; + } else if state.fork_name_unchecked().electra_enabled() { state.update_pubkey_cache()?; process_deposit_requests_pre_gloas( state, @@ -1074,3 +1082,45 @@ pub fn process_consolidation_request<E: EthSpec>( Ok(()) } + +pub fn process_payload_attestation<E: EthSpec>( + state: &mut BeaconState<E>, + payload_attestation: &PayloadAttestation<E>, + att_index: usize, + verify_signatures: VerifySignatures, + ctxt: &mut ConsensusContext<E>, + spec: &ChainSpec, +) -> Result<(), BlockProcessingError> { + verify_payload_attestation(state, payload_attestation, ctxt, verify_signatures, spec) + .map_err(|e| e.into_with_index(att_index)) +} + +pub fn process_payload_attestations<'a, E: EthSpec, I>( + state: &mut BeaconState<E>, + payload_attestations: I, + verify_signatures: VerifySignatures, + ctxt: &mut ConsensusContext<E>, + spec: &ChainSpec, +) -> Result<(), BlockProcessingError> +where + I: Iterator<Item = &'a PayloadAttestation<E>>, +{ + // Presently the PTC cache requires the committee cache for `state.slot() - 1` which is either + // in the current or previous epoch. + // TODO(gloas): These requirements may change if we introduce a PTC cache. + state.build_committee_cache(RelativeEpoch::Current, spec)?; + state.build_committee_cache(RelativeEpoch::Previous, spec)?; + + payload_attestations + .enumerate() + .try_for_each(|(i, payload_attestation)| { + process_payload_attestation( + state, + payload_attestation, + i, + verify_signatures, + ctxt, + spec, + ) + }) +} diff --git a/consensus/state_processing/src/per_block_processing/signature_sets.rs b/consensus/state_processing/src/per_block_processing/signature_sets.rs index 0cc591ba4c..71ee1f8993 100644 --- a/consensus/state_processing/src/per_block_processing/signature_sets.rs +++ b/consensus/state_processing/src/per_block_processing/signature_sets.rs @@ -10,10 +10,10 @@ use typenum::Unsigned; use types::{ AbstractExecPayload, AttesterSlashingRef, BeaconBlockRef, BeaconState, BeaconStateError, BuilderIndex, ChainSpec, DepositData, Domain, Epoch, EthSpec, Fork, Hash256, InconsistentFork, - IndexedAttestation, IndexedAttestationRef, ProposerSlashing, SignedAggregateAndProof, - SignedBeaconBlock, SignedBeaconBlockHeader, SignedBlsToExecutionChange, - SignedContributionAndProof, SignedExecutionPayloadBid, SignedRoot, SignedVoluntaryExit, - SigningData, Slot, SyncAggregate, SyncAggregatorSelectionData, + IndexedAttestation, IndexedAttestationRef, IndexedPayloadAttestation, ProposerSlashing, + SignedAggregateAndProof, SignedBeaconBlock, SignedBeaconBlockHeader, + SignedBlsToExecutionChange, SignedContributionAndProof, SignedExecutionPayloadBid, SignedRoot, + SignedVoluntaryExit, SigningData, Slot, SyncAggregate, SyncAggregatorSelectionData, consts::gloas::BUILDER_INDEX_SELF_BUILD, }; @@ -355,6 +355,40 @@ where Ok(SignatureSet::multiple_pubkeys(signature, pubkeys, message)) } +pub fn indexed_payload_attestation_signature_set<'a, 'b, E, F>( + state: &'a BeaconState<E>, + get_pubkey: F, + signature: &'a AggregateSignature, + indexed_payload_attestation: &'b IndexedPayloadAttestation<E>, + spec: &'a ChainSpec, +) -> Result<SignatureSet<'a>> +where + E: EthSpec, + F: Fn(usize) -> Option<Cow<'a, PublicKey>>, +{ + let mut pubkeys = Vec::with_capacity(indexed_payload_attestation.attesting_indices.len()); + for &validator_idx in indexed_payload_attestation.attesting_indices.iter() { + pubkeys.push( + get_pubkey(validator_idx as usize).ok_or(Error::ValidatorUnknown(validator_idx))?, + ); + } + + let epoch = indexed_payload_attestation + .data + .slot + .epoch(E::slots_per_epoch()); + let domain = spec.get_domain( + epoch, + Domain::PTCAttester, + &state.fork(), + state.genesis_validators_root(), + ); + + let message = indexed_payload_attestation.data.signing_root(domain); + + Ok(SignatureSet::multiple_pubkeys(signature, pubkeys, message)) +} + pub fn execution_payload_bid_signature_set<'a, E, F>( state: &'a BeaconState<E>, get_builder_pubkey: F, diff --git a/consensus/state_processing/src/per_block_processing/verify_payload_attestation.rs b/consensus/state_processing/src/per_block_processing/verify_payload_attestation.rs new file mode 100644 index 0000000000..1c15e1c21c --- /dev/null +++ b/consensus/state_processing/src/per_block_processing/verify_payload_attestation.rs @@ -0,0 +1,46 @@ +use super::VerifySignatures; +use super::errors::{BlockOperationError, PayloadAttestationInvalid as Invalid}; +use crate::ConsensusContext; +use crate::per_block_processing::is_valid_indexed_payload_attestation; +use safe_arith::SafeArith; +use types::*; + +pub fn verify_payload_attestation<'ctxt, E: EthSpec>( + state: &mut BeaconState<E>, + payload_attestation: &'ctxt PayloadAttestation<E>, + ctxt: &'ctxt mut ConsensusContext<E>, + verify_signatures: VerifySignatures, + spec: &ChainSpec, +) -> Result<(), BlockOperationError<Invalid>> { + let data = &payload_attestation.data; + + // Check that the attestation is for the parent beacon block + verify!( + data.beacon_block_root == state.latest_block_header().parent_root, + Invalid::BlockRootMismatch { + expected: state.latest_block_header().parent_root, + found: data.beacon_block_root, + } + ); + + // Check that the attestation is for the previous slot + verify!( + data.slot.safe_add(1)? == state.slot(), + Invalid::SlotMismatch { + expected: state.slot().saturating_sub(Slot::new(1)), + found: data.slot, + } + ); + + let indexed_payload_attestation = + ctxt.get_indexed_payload_attestation(state, payload_attestation, spec)?; + + is_valid_indexed_payload_attestation( + state, + indexed_payload_attestation, + verify_signatures, + spec, + )?; + + Ok(()) +} diff --git a/testing/ef_tests/check_all_files_accessed.py b/testing/ef_tests/check_all_files_accessed.py index b465a47296..b5d7a787fe 100755 --- a/testing/ef_tests/check_all_files_accessed.py +++ b/testing/ef_tests/check_all_files_accessed.py @@ -47,8 +47,6 @@ excluded_paths = [ "bls12-381-tests/hash_to_G2", "tests/.*/eip7732", "tests/.*/eip7805", - # TODO(gloas): remove these ignores as more Gloas operations are implemented - "tests/.*/gloas/operations/payload_attestation/.*", # TODO(gloas): remove these ignores as Gloas consensus is implemented "tests/.*/gloas/fork/.*", "tests/.*/gloas/fork_choice/.*", diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index 59d2bef24e..ca0124e1aa 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -21,7 +21,7 @@ use state_processing::{ process_operations::{ altair_deneb, base, gloas, process_attester_slashings, process_bls_to_execution_changes, process_deposits, process_exits, - process_proposer_slashings, + process_payload_attestation, process_proposer_slashings, }, process_sync_aggregate, withdrawals, }, @@ -31,8 +31,9 @@ use types::{ Attestation, AttesterSlashing, BeaconBlock, BeaconBlockBody, BeaconBlockBodyBellatrix, BeaconBlockBodyCapella, BeaconBlockBodyDeneb, BeaconBlockBodyElectra, BeaconBlockBodyFulu, BeaconState, BlindedPayload, ConsolidationRequest, Deposit, DepositRequest, ExecutionPayload, - ForkVersionDecode, FullPayload, ProposerSlashing, SignedBlsToExecutionChange, - SignedExecutionPayloadEnvelope, SignedVoluntaryExit, SyncAggregate, WithdrawalRequest, + ForkVersionDecode, FullPayload, PayloadAttestation, ProposerSlashing, + SignedBlsToExecutionChange, SignedExecutionPayloadEnvelope, SignedVoluntaryExit, SyncAggregate, + WithdrawalRequest, }; #[derive(Debug, Clone, Default, Deserialize)] @@ -667,6 +668,32 @@ impl<E: EthSpec> Operation<E> for ConsolidationRequest { } } +impl<E: EthSpec> Operation<E> for PayloadAttestation<E> { + type Error = BlockProcessingError; + + fn handler_name() -> String { + "payload_attestation".into() + } + + fn is_enabled_for_fork(fork_name: ForkName) -> bool { + fork_name.gloas_enabled() + } + + fn decode(path: &Path, _fork_name: ForkName, _spec: &ChainSpec) -> Result<Self, Error> { + ssz_decode_file(path) + } + + fn apply_to( + &self, + state: &mut BeaconState<E>, + spec: &ChainSpec, + _extra: &Operations<E, Self>, + ) -> Result<(), BlockProcessingError> { + let mut ctxt = ConsensusContext::new(state.slot()); + process_payload_attestation(state, self, 0, VerifySignatures::True, &mut ctxt, spec) + } +} + impl<E: EthSpec, O: Operation<E>> LoadCase for Operations<E, O> { fn load_from_dir(path: &Path, fork_name: ForkName) -> Result<Self, Error> { let spec = &testing_spec::<E>(fork_name); diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index 625778c2dd..35972ce72c 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -1177,25 +1177,6 @@ impl<E: EthSpec + TypeName, O: Operation<E>> Handler for OperationsHandler<E, O> fn handler_name(&self) -> String { O::handler_name() } - - fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { - Self::Case::is_enabled_for_fork(fork_name) - && (!fork_name.gloas_enabled() - || self.handler_name() == "attestation" - || self.handler_name() == "attester_slashing" - || self.handler_name() == "block_header" - || self.handler_name() == "bls_to_execution_change" - || self.handler_name() == "consolidation_request" - || self.handler_name() == "deposit_request" - || self.handler_name() == "deposit" - || self.handler_name() == "execution_payload" - || self.handler_name() == "execution_payload_bid" - || self.handler_name() == "proposer_slashing" - || self.handler_name() == "sync_aggregate" - || self.handler_name() == "withdrawal_request" - || self.handler_name() == "withdrawals" - || self.handler_name() == "voluntary_exit") - } } #[derive(Educe)] diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index fcf7951c3e..3893df2ef7 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -99,6 +99,12 @@ fn operations_execution_payload_bid() { OperationsHandler::<MainnetEthSpec, ExecutionPayloadBidBlock<_>>::default().run(); } +#[test] +fn operations_payload_attestation() { + OperationsHandler::<MinimalEthSpec, PayloadAttestation<_>>::default().run(); + OperationsHandler::<MainnetEthSpec, PayloadAttestation<_>>::default().run(); +} + #[test] fn operations_withdrawals() { OperationsHandler::<MinimalEthSpec, WithdrawalsPayload<_>>::default().run(); From 41291a8aecd9813243d1cef96f54ad8bc50cbe5b Mon Sep 17 00:00:00 2001 From: Michael Sproul <michaelsproul@users.noreply.github.com> Date: Tue, 17 Feb 2026 17:23:25 +1100 Subject: [PATCH 25/81] Gloas fork upgrade consensus (#8833) - Implement and optimise `upgrade_to_gloas` - Enable EF tests for `fork_ugprade` Co-Authored-By: Michael Sproul <michael@sigmaprime.io> --- .../process_operations.rs | 2 +- .../state_processing/src/upgrade/gloas.rs | 84 ++++++++++++++++++- testing/ef_tests/check_all_files_accessed.py | 1 - testing/ef_tests/src/handler.rs | 5 -- 4 files changed, 83 insertions(+), 9 deletions(-) diff --git a/consensus/state_processing/src/per_block_processing/process_operations.rs b/consensus/state_processing/src/per_block_processing/process_operations.rs index b037c74484..9743812632 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -876,7 +876,7 @@ pub fn apply_deposit_for_builder<E: EthSpec>( signature: SignatureBytes, slot: Slot, spec: &ChainSpec, -) -> Result<(), BlockProcessingError> { +) -> Result<(), BeaconStateError> { match builder_index_opt { None => { // Verify the deposit signature (proof of possession) which is not checked by the deposit contract diff --git a/consensus/state_processing/src/upgrade/gloas.rs b/consensus/state_processing/src/upgrade/gloas.rs index 0e0f39fa02..7a88383ab0 100644 --- a/consensus/state_processing/src/upgrade/gloas.rs +++ b/consensus/state_processing/src/upgrade/gloas.rs @@ -1,10 +1,14 @@ +use crate::per_block_processing::{ + is_valid_deposit_signature, process_operations::apply_deposit_for_builder, +}; use milhouse::{List, Vector}; use ssz_types::BitVector; +use std::collections::HashSet; use std::mem; use typenum::Unsigned; use types::{ BeaconState, BeaconStateError as Error, BeaconStateGloas, BuilderPendingPayment, ChainSpec, - EthSpec, ExecutionPayloadBid, Fork, + DepositData, EthSpec, ExecutionPayloadBid, Fork, is_builder_withdrawal_credential, }; /// Transform a `Fulu` state into a `Gloas` state. @@ -30,7 +34,7 @@ pub fn upgrade_state_to_gloas<E: EthSpec>( // // Fixed size vectors get cloned because replacing them would require the same size // allocation as cloning. - let post = BeaconState::Gloas(BeaconStateGloas { + let mut post = BeaconState::Gloas(BeaconStateGloas { // Versioning genesis_time: pre.genesis_time, genesis_validators_root: pre.genesis_validators_root, @@ -114,5 +118,81 @@ pub fn upgrade_state_to_gloas<E: EthSpec>( slashings_cache: mem::take(&mut pre.slashings_cache), epoch_cache: mem::take(&mut pre.epoch_cache), }); + // [New in Gloas:EIP7732] + onboard_builders_from_pending_deposits(&mut post, spec)?; + Ok(post) } + +/// Applies any pending deposit for builders, effectively onboarding builders at the fork. +fn onboard_builders_from_pending_deposits<E: EthSpec>( + state: &mut BeaconState<E>, + spec: &ChainSpec, +) -> Result<(), Error> { + // Rather than tracking all `validator_pubkeys` in one place as the spec does, we keep a + // hashset for *just* the new validator pubkeys, and use the state's efficient + // `get_validator_index` function instead of an O(n) iteration over the full validator list. + let mut new_validator_pubkeys = HashSet::new(); + + // Clone pending deposits to avoid borrow conflicts when mutating state. + let current_pending_deposits = state.pending_deposits()?.clone(); + + let mut pending_deposits = List::empty(); + + for deposit in ¤t_pending_deposits { + // Deposits for existing validators stay in the pending queue. + if new_validator_pubkeys.contains(&deposit.pubkey) + || state.get_validator_index(&deposit.pubkey)?.is_some() + { + pending_deposits.push(deposit.clone())?; + continue; + } + + // Re-scan builder list each iteration because `apply_deposit_for_builder` may add + // new builders to the registry. + // TODO(gloas): this linear scan could be optimized, see: + // https://github.com/sigp/lighthouse/issues/8783 + let builder_index = state + .builders()? + .iter() + .position(|b| b.pubkey == deposit.pubkey); + + let has_builder_credentials = + is_builder_withdrawal_credential(deposit.withdrawal_credentials, spec); + + if builder_index.is_some() || has_builder_credentials { + let builder_index_opt = builder_index.map(|i| i as u64); + apply_deposit_for_builder( + state, + builder_index_opt, + deposit.pubkey, + deposit.withdrawal_credentials, + deposit.amount, + deposit.signature.clone(), + deposit.slot, + spec, + )?; + continue; + } + + // If there is a pending deposit for a new validator that has a valid signature, + // track the pubkey so that subsequent builder deposits for the same pubkey stay + // in pending (applied to the validator later) rather than creating a builder. + // Deposits with invalid signatures are dropped since they would fail in + // apply_pending_deposit anyway. + let deposit_data = DepositData { + pubkey: deposit.pubkey, + withdrawal_credentials: deposit.withdrawal_credentials, + amount: deposit.amount, + signature: deposit.signature.clone(), + }; + if is_valid_deposit_signature(&deposit_data, spec).is_ok() { + new_validator_pubkeys.insert(deposit.pubkey); + pending_deposits.push(deposit.clone())?; + } + } + + *state.pending_deposits_mut()? = pending_deposits; + + Ok(()) +} diff --git a/testing/ef_tests/check_all_files_accessed.py b/testing/ef_tests/check_all_files_accessed.py index b5d7a787fe..782b554ff1 100755 --- a/testing/ef_tests/check_all_files_accessed.py +++ b/testing/ef_tests/check_all_files_accessed.py @@ -48,7 +48,6 @@ excluded_paths = [ "tests/.*/eip7732", "tests/.*/eip7805", # TODO(gloas): remove these ignores as Gloas consensus is implemented - "tests/.*/gloas/fork/.*", "tests/.*/gloas/fork_choice/.*", # Ignore MatrixEntry SSZ tests for now. "tests/.*/.*/ssz_static/MatrixEntry/.*", diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index 35972ce72c..da3c5533b6 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -621,11 +621,6 @@ impl<E: EthSpec + TypeName> Handler for ForkHandler<E> { fn handler_name(&self) -> String { "fork".into() } - - fn disabled_forks(&self) -> Vec<ForkName> { - // TODO(gloas): remove once onboard_builders_from_pending_deposits is implemented - vec![ForkName::Gloas] - } } #[derive(Educe)] From 4625cb6ab62d78ef13ff286b8243a52aaa60bfa4 Mon Sep 17 00:00:00 2001 From: Jimmy Chen <jchen.tc@gmail.com> Date: Tue, 17 Feb 2026 20:22:16 +1100 Subject: [PATCH 26/81] Gloas local block building cleanup (#8834) Continuation of #8754, some small cleanups and address TODOs Co-Authored-By: Jimmy Chen <jchen.tc@gmail.com> --- beacon_node/beacon_chain/src/beacon_chain.rs | 4 +- .../src/block_production/gloas.rs | 62 +++++-------------- .../src/data_column_verification.rs | 1 - beacon_node/beacon_chain/src/errors.rs | 3 + beacon_node/execution_layer/src/lib.rs | 1 + 5 files changed, 23 insertions(+), 48 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index d0f5297f1b..9f62bf11f5 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -5262,7 +5262,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> { err = ?e, block_slot = %state.slot(), ?exit, - "Attempted to include an invalid proposer slashing" + "Attempted to include an invalid voluntary exit" ); }) .is_ok() @@ -5672,7 +5672,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> { } BeaconState::Gloas(_) => { return Err(BlockProductionError::GloasNotImplemented( - "Attempting to produce gloas beacn block via non gloas code path".to_owned(), + "Attempting to produce gloas beacon block via non gloas code path".to_owned(), )); } }; diff --git a/beacon_node/beacon_chain/src/block_production/gloas.rs b/beacon_node/beacon_chain/src/block_production/gloas.rs index 025cf21a73..607090c59d 100644 --- a/beacon_node/beacon_chain/src/block_production/gloas.rs +++ b/beacon_node/beacon_chain/src/block_production/gloas.rs @@ -59,7 +59,7 @@ pub struct PartialBeaconBlock<E: EthSpec> { payload_attestations: Vec<PayloadAttestation<E>>, deposits: Vec<Deposit>, voluntary_exits: Vec<SignedVoluntaryExit>, - sync_aggregate: Option<SyncAggregate<E>>, + sync_aggregate: SyncAggregate<E>, bls_to_execution_changes: Vec<SignedBlsToExecutionChange>, } @@ -364,13 +364,13 @@ impl<T: BeaconChainTypes> BeaconChain<T> { err = ?e, block_slot = %state.slot(), ?exit, - "Attempted to include an invalid proposer slashing" + "Attempted to include an invalid voluntary exit" ); }) .is_ok() }); - // TODO(gloas) verifiy payload attestation signature here as well + // TODO(gloas) verify payload attestation signature here as well } let attester_slashings = attester_slashings @@ -391,22 +391,17 @@ impl<T: BeaconChainTypes> BeaconChain<T> { let slot = state.slot(); - let sync_aggregate = if matches!(&state, BeaconState::Base(_)) { - None - } else { - let sync_aggregate = self - .op_pool - .get_sync_aggregate(&state) - .map_err(BlockProductionError::OpPoolError)? - .unwrap_or_else(|| { - warn!( - slot = %state.slot(), - "Producing block with no sync contributions" - ); - SyncAggregate::new() - }); - Some(sync_aggregate) - }; + let sync_aggregate = self + .op_pool + .get_sync_aggregate(&state) + .map_err(BlockProductionError::OpPoolError)? + .unwrap_or_else(|| { + warn!( + slot = %state.slot(), + "Producing block with no sync contributions" + ); + SyncAggregate::new() + }); Ok(( PartialBeaconBlock { @@ -492,8 +487,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> { voluntary_exits: voluntary_exits .try_into() .map_err(BlockProductionError::SszTypesError)?, - sync_aggregate: sync_aggregate - .ok_or(BlockProductionError::MissingSyncAggregate)?, + sync_aggregate, bls_to_execution_changes: bls_to_execution_changes .try_into() .map_err(BlockProductionError::SszTypesError)?, @@ -573,7 +567,6 @@ impl<T: BeaconChainTypes> BeaconChain<T> { signature: Signature::empty(), }; - // TODO(gloas) add better error variant // We skip state root verification here because the relevant state root // cant be calculated until after the new block has been constructed. process_execution_payload_envelope( @@ -584,11 +577,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> { VerifyStateRoot::False, &self.spec, ) - .map_err(|_| { - BlockProductionError::GloasNotImplemented( - "process_execution_payload_envelope failed".to_owned(), - ) - })?; + .map_err(BlockProductionError::EnvelopeProcessingError)?; signed_envelope.message.state_root = state.update_tree_hash_cache()?; @@ -731,12 +720,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> { Ok(( SignedExecutionPayloadBid { message: bid, - // TODO(gloas) return better error variant here - signature: Signature::infinity().map_err(|_| { - BlockProductionError::GloasNotImplemented( - "Failed to generate infinity signature".to_owned(), - ) - })?, + signature: Signature::infinity().map_err(BlockProductionError::BlsError)?, }, state, // Local building always returns payload data. @@ -752,12 +736,6 @@ impl<T: BeaconChainTypes> BeaconChain<T> { /// /// Will return an error when using a pre-Gloas `state`. Ensure to only run this function /// after the Gloas fork. -/// -/// ## Specification -/// -/// Equivalent to the `get_execution_payload` function in the Validator Guide: -/// -/// https://github.com/ethereum/consensus-specs/blob/v1.1.5/specs/merge/validator.md#block-proposal fn get_execution_payload_gloas<T: BeaconChainTypes>( chain: Arc<BeaconChain<T>>, state: &BeaconState<T::EthSpec>, @@ -813,12 +791,6 @@ fn get_execution_payload_gloas<T: BeaconChainTypes>( /// /// Will return an error when using a pre-Gloas fork `state`. Ensure to only run this function /// after the Gloas fork. -/// -/// ## Specification -/// -/// Equivalent to the `prepare_execution_payload` function in the Validator Guide: -/// -/// https://github.com/ethereum/consensus-specs/blob/v1.1.5/specs/merge/validator.md#block-proposal #[allow(clippy::too_many_arguments)] async fn prepare_execution_payload<T>( chain: &Arc<BeaconChain<T>>, diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index cf3385ec5b..08acfdffa4 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -496,7 +496,6 @@ where Ok(()) } -// TODO(gloas) make sure the gloas variant uses the same span name #[instrument( skip_all, name = "validate_data_column_sidecar_for_gossip", diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index bcccc0ec12..6c8f0d2794 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -16,6 +16,7 @@ use milhouse::Error as MilhouseError; use operation_pool::OpPoolError; use safe_arith::ArithError; use ssz_types::Error as SszTypesError; +use state_processing::envelope_processing::EnvelopeProcessingError; use state_processing::{ BlockProcessingError, BlockReplayError, EpochProcessingError, SlotProcessingError, block_signature_verifier::Error as BlockSignatureVerifierError, @@ -318,6 +319,8 @@ pub enum BlockProductionError { FailedToBuildBlobSidecars(String), MissingExecutionRequests, SszTypesError(ssz_types::Error), + EnvelopeProcessingError(EnvelopeProcessingError), + BlsError(bls::Error), // TODO(gloas): Remove this once Gloas is implemented GloasNotImplemented(String), } diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index ad2486a4ad..157fe152ef 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -914,6 +914,7 @@ impl<E: EthSpec> ExecutionLayer<E> { /// /// The result will be returned from the first node that returns successfully. No more nodes /// will be contacted. + #[instrument(level = "debug", skip_all)] pub async fn get_payload_gloas( &self, payload_parameters: PayloadParameters<'_>, From 2f7a1f3ae8b450f8042001a67ae88dd675251e3c Mon Sep 17 00:00:00 2001 From: Jimmy Chen <jchen.tc@gmail.com> Date: Wed, 18 Feb 2026 11:38:11 +1100 Subject: [PATCH 27/81] Support pinning nightly ef test runs (#8738) Co-Authored-By: Jimmy Chen <jchen.tc@gmail.com> Co-Authored-By: Michael Sproul <michael@sigmaprime.io> --- testing/ef_tests/download_test_vectors.sh | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/testing/ef_tests/download_test_vectors.sh b/testing/ef_tests/download_test_vectors.sh index 21f74e817f..ff5b61bb47 100755 --- a/testing/ef_tests/download_test_vectors.sh +++ b/testing/ef_tests/download_test_vectors.sh @@ -4,7 +4,7 @@ set -Eeuo pipefail TESTS=("general" "minimal" "mainnet") version=${1} -if [[ "$version" == "nightly" ]]; then +if [[ "$version" == "nightly" || "$version" =~ ^nightly-[0-9]+$ ]]; then if [[ -z "${GITHUB_TOKEN:-}" ]]; then echo "Error GITHUB_TOKEN is not set" exit 1 @@ -21,9 +21,13 @@ if [[ "$version" == "nightly" ]]; then api="https://api.github.com" auth_header="Authorization: token ${GITHUB_TOKEN}" - run_id=$(curl -s -H "${auth_header}" \ - "${api}/repos/${repo}/actions/workflows/generate_vectors.yml/runs?branch=dev&status=success&per_page=1" | - jq -r '.workflow_runs[0].id') + if [[ "$version" == "nightly" ]]; then + run_id=$(curl --fail -s -H "${auth_header}" \ + "${api}/repos/${repo}/actions/workflows/nightly-reftests.yml/runs?branch=master&status=success&per_page=1" | + jq -r '.workflow_runs[0].id') + else + run_id="${version#nightly-}" + fi if [[ "${run_id}" == "null" || -z "${run_id}" ]]; then echo "No successful nightly workflow run found" @@ -31,7 +35,7 @@ if [[ "$version" == "nightly" ]]; then fi echo "Downloading nightly test vectors for run: ${run_id}" - curl -s -H "${auth_header}" "${api}/repos/${repo}/actions/runs/${run_id}/artifacts" | + curl --fail -H "${auth_header}" "${api}/repos/${repo}/actions/runs/${run_id}/artifacts" | jq -c '.artifacts[] | {name, url: .archive_download_url}' | while read -r artifact; do name=$(echo "${artifact}" | jq -r .name) From 9065e4a56e7327d7c084e996d5ee071a0eec038b Mon Sep 17 00:00:00 2001 From: 0xMushow <105550256+0xMushow@users.noreply.github.com> Date: Thu, 4 Dec 2025 14:35:12 +0100 Subject: [PATCH 28/81] fix(beacon_node): add pruning of observed_column_sidecars (#8531) None I noticed that `observed_column_sidecars` is missing its prune call in the finalization handler, which results in a memory leak on long-running nodes (very slow (**7MB/day**)) : https://github.com/sigp/lighthouse/blob/13dfa9200f822c41ccd81b95a3f052df54c888e9/beacon_node/beacon_chain/src/canonical_head.rs#L940-L959 Both caches use the same generic type `ObservedDataSidecars<T>:` https://github.com/sigp/lighthouse/blob/22ec4b327186c4a4a87d2c8c745caf3b36cb6dd6/beacon_node/beacon_chain/src/beacon_chain.rs#L413-L416 The type's documentation explicitly requires manual pruning: > "*The cache supports pruning based upon the finalized epoch. It does not automatically prune, you must call Self::prune manually.*" https://github.com/sigp/lighthouse/blob/b4704eab4ac8edf0ea0282ed9a5758b784038dd2/beacon_node/beacon_chain/src/observed_data_sidecars.rs#L66-L74 Currently: - `observed_blob_sidecars` => pruned - `observed_column_sidecars` => **NOT** pruned Without pruning, the underlying HashMap accumulates entries indefinitely, causing continuous memory growth until the node restarts. Co-Authored-By: Antoine James <antoine@ethereum.org> --- beacon_node/beacon_chain/src/canonical_head.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/beacon_node/beacon_chain/src/canonical_head.rs b/beacon_node/beacon_chain/src/canonical_head.rs index db071db166..1a08ac3f88 100644 --- a/beacon_node/beacon_chain/src/canonical_head.rs +++ b/beacon_node/beacon_chain/src/canonical_head.rs @@ -918,6 +918,13 @@ impl<T: BeaconChainTypes> BeaconChain<T> { .start_slot(T::EthSpec::slots_per_epoch()), ); + self.observed_column_sidecars.write().prune( + new_view + .finalized_checkpoint + .epoch + .start_slot(T::EthSpec::slots_per_epoch()), + ); + self.observed_slashable.write().prune( new_view .finalized_checkpoint From d4ec006a3419f15041a02792b1981e68645c501d Mon Sep 17 00:00:00 2001 From: Jimmy Chen <jchen.tc@gmail.com> Date: Wed, 18 Feb 2026 14:01:22 +1100 Subject: [PATCH 29/81] Update `time` to fix `cargo audit` failure (#8764) --- Cargo.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8748be726c..7d75f5c197 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6323,9 +6323,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-integer" @@ -8899,30 +8899,30 @@ dependencies = [ [[package]] name = "time" -version = "0.3.44" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", From c4ff9b137c9a2cb8daf7a1cf6b708dc4b0011659 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Tue, 17 Feb 2026 20:26:06 -0700 Subject: [PATCH 30/81] Add critical instructions and hooks for Claude Code (#8715) Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com> --- .claude/settings.json | 15 +++++++++++++++ .githooks/pre-commit | 5 +++++ CLAUDE.md | 8 ++++++++ Makefile | 6 ++++++ 4 files changed, 34 insertions(+) create mode 100644 .claude/settings.json create mode 100755 .githooks/pre-commit diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000000..ae426dd254 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "echo '\n[Reminder] Run: cargo fmt --all && make lint-fix'" + } + ] + } + ] + } +} diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000000..42a5ca79e0 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,5 @@ +#!/bin/sh +# Pre-commit hook: runs cargo fmt --check +# Install with: make install-hooks + +exec cargo fmt --check diff --git a/CLAUDE.md b/CLAUDE.md index 441c8e4274..79ed344e35 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,6 +2,14 @@ This file provides guidance for AI assistants (Claude Code, Codex, etc.) working with Lighthouse. +## CRITICAL - Always Follow + +After completing ANY code changes: +1. **MUST** run `cargo fmt --all && make lint-fix` to format and fix linting issues +2. **MUST** run `cargo check` to verify compilation before considering task complete + +Run `make install-hooks` if you have not already to install git hooks. Never skip git hooks. If cargo is not available install the toolchain. + ## Quick Reference ```bash diff --git a/Makefile b/Makefile index 0995a869f4..9786c17cc9 100644 --- a/Makefile +++ b/Makefile @@ -361,3 +361,9 @@ clean: cargo clean make -C $(EF_TESTS) clean make -C $(STATE_TRANSITION_VECTORS) clean + +# Installs git hooks from .githooks/ directory +install-hooks: + @ln -sf ../../.githooks/pre-commit .git/hooks/pre-commit + @chmod +x .githooks/pre-commit + @echo "Git hooks installed. Pre-commit hook runs 'cargo fmt --check'." From c61665b3a1efc6b353b57be37816e69825f2bab6 Mon Sep 17 00:00:00 2001 From: Akihito Nakano <sora.akatsuki@gmail.com> Date: Wed, 11 Feb 2026 07:49:20 +0900 Subject: [PATCH 31/81] Penalize peers that send an invalid rpc request (#6986) Since https://github.com/sigp/lighthouse/pull/6847, invalid `BlocksByRange`/`BlobsByRange` requests, which do not comply with the spec, are [handled in the Handler](https://github.com/sigp/lighthouse/blob/3d16d1080f5b93193404967dcb5525fa68840ea0/beacon_node/lighthouse_network/src/rpc/handler.rs#L880-L911). Any peer that sends an invalid request is penalized and disconnected. However, other kinds of invalid rpc request, which result in decoding errors, are just dropped. No penalty is applied and the connection with the peer remains. I have added handling for the `ListenUpgradeError` event to notify the application of an `RPCError:InvalidData` error and disconnect to the peer that sent the invalid rpc request. I also added tests for handling invalid rpc requests. Co-Authored-By: ackintosh <sora.akatsuki@gmail.com> --- .../lighthouse_network/src/rpc/handler.rs | 17 +- .../lighthouse_network/src/rpc/protocol.rs | 10 +- .../lighthouse_network/tests/rpc_tests.rs | 160 +++++++++++++++++- 3 files changed, 179 insertions(+), 8 deletions(-) diff --git a/beacon_node/lighthouse_network/src/rpc/handler.rs b/beacon_node/lighthouse_network/src/rpc/handler.rs index 720895bbe7..9861119ac1 100644 --- a/beacon_node/lighthouse_network/src/rpc/handler.rs +++ b/beacon_node/lighthouse_network/src/rpc/handler.rs @@ -13,7 +13,8 @@ use futures::prelude::*; use libp2p::PeerId; use libp2p::swarm::handler::{ ConnectionEvent, ConnectionHandler, ConnectionHandlerEvent, DialUpgradeError, - FullyNegotiatedInbound, FullyNegotiatedOutbound, StreamUpgradeError, SubstreamProtocol, + FullyNegotiatedInbound, FullyNegotiatedOutbound, ListenUpgradeError, StreamUpgradeError, + SubstreamProtocol, }; use libp2p::swarm::{ConnectionId, Stream}; use logging::crit; @@ -888,6 +889,16 @@ where ConnectionEvent::DialUpgradeError(DialUpgradeError { info, error }) => { self.on_dial_upgrade_error(info, error) } + ConnectionEvent::ListenUpgradeError(ListenUpgradeError { + error: (proto, error), + .. + }) => { + self.events_out.push(HandlerEvent::Err(HandlerErr::Inbound { + id: self.current_inbound_substream_id, + proto, + error, + })); + } _ => { // NOTE: ConnectionEvent is a non exhaustive enum so updates should be based on // release notes more than compiler feedback @@ -924,7 +935,7 @@ where request.count() )), })); - return self.shutdown(None); + return; } } RequestType::BlobsByRange(request) => { @@ -940,7 +951,7 @@ where max_allowed, max_requested_blobs )), })); - return self.shutdown(None); + return; } } _ => {} diff --git a/beacon_node/lighthouse_network/src/rpc/protocol.rs b/beacon_node/lighthouse_network/src/rpc/protocol.rs index f0ac9d00f9..34d8efccd1 100644 --- a/beacon_node/lighthouse_network/src/rpc/protocol.rs +++ b/beacon_node/lighthouse_network/src/rpc/protocol.rs @@ -675,7 +675,7 @@ where E: EthSpec, { type Output = InboundOutput<TSocket, E>; - type Error = RPCError; + type Error = (Protocol, RPCError); type Future = BoxFuture<'static, Result<Self::Output, Self::Error>>; fn upgrade_inbound(self, socket: TSocket, protocol: ProtocolId) -> Self::Future { @@ -717,10 +717,12 @@ where ) .await { - Err(e) => Err(RPCError::from(e)), + Err(e) => Err((versioned_protocol.protocol(), RPCError::from(e))), Ok((Some(Ok(request)), stream)) => Ok((request, stream)), - Ok((Some(Err(e)), _)) => Err(e), - Ok((None, _)) => Err(RPCError::IncompleteStream), + Ok((Some(Err(e)), _)) => Err((versioned_protocol.protocol(), e)), + Ok((None, _)) => { + Err((versioned_protocol.protocol(), RPCError::IncompleteStream)) + } } } } diff --git a/beacon_node/lighthouse_network/tests/rpc_tests.rs b/beacon_node/lighthouse_network/tests/rpc_tests.rs index 553cfa6f0d..137136e97e 100644 --- a/beacon_node/lighthouse_network/tests/rpc_tests.rs +++ b/beacon_node/lighthouse_network/tests/rpc_tests.rs @@ -5,8 +5,12 @@ use crate::common::spec_with_all_forks_enabled; use crate::common::{Protocol, build_tracing_subscriber}; use bls::Signature; use fixed_bytes::FixedBytesExtended; +use libp2p::PeerId; use lighthouse_network::rpc::{RequestType, methods::*}; -use lighthouse_network::service::api_types::AppRequestId; +use lighthouse_network::service::api_types::{ + AppRequestId, BlobsByRangeRequestId, BlocksByRangeRequestId, ComponentsByRangeRequestId, + DataColumnsByRangeRequestId, DataColumnsByRangeRequester, RangeRequestId, SyncRequestId, +}; use lighthouse_network::{NetworkEvent, ReportSource, Response}; use ssz::Encode; use ssz_types::{RuntimeVariableList, VariableList}; @@ -1785,3 +1789,157 @@ fn test_active_requests() { } }) } + +// Test that when a node receives an invalid BlocksByRange request exceeding the maximum count, +// it bans the sender. +#[test] +fn test_request_too_large_blocks_by_range() { + let spec = Arc::new(spec_with_all_forks_enabled()); + + test_request_too_large( + AppRequestId::Sync(SyncRequestId::BlocksByRange(BlocksByRangeRequestId { + id: 1, + parent_request_id: ComponentsByRangeRequestId { + id: 1, + requester: RangeRequestId::RangeSync { + chain_id: 1, + batch_id: Epoch::new(1), + }, + }, + })), + RequestType::BlocksByRange(OldBlocksByRangeRequest::new( + 0, + spec.max_request_blocks(ForkName::Base) as u64 + 1, // exceeds the max request defined in the spec. + 1, + )), + ); +} + +// Test that when a node receives an invalid BlobsByRange request exceeding the maximum count, +// it bans the sender. +#[test] +fn test_request_too_large_blobs_by_range() { + let spec = Arc::new(spec_with_all_forks_enabled()); + + let max_request_blobs_count = spec.max_request_blob_sidecars(ForkName::Base) as u64 + / spec.max_blobs_per_block_within_fork(ForkName::Base); + test_request_too_large( + AppRequestId::Sync(SyncRequestId::BlobsByRange(BlobsByRangeRequestId { + id: 1, + parent_request_id: ComponentsByRangeRequestId { + id: 1, + requester: RangeRequestId::RangeSync { + chain_id: 1, + batch_id: Epoch::new(1), + }, + }, + })), + RequestType::BlobsByRange(BlobsByRangeRequest { + start_slot: 0, + count: max_request_blobs_count + 1, // exceeds the max request defined in the spec. + }), + ); +} + +// Test that when a node receives an invalid DataColumnsByRange request exceeding the columns count, +// it bans the sender. +#[test] +fn test_request_too_large_data_columns_by_range() { + test_request_too_large( + AppRequestId::Sync(SyncRequestId::DataColumnsByRange( + DataColumnsByRangeRequestId { + id: 1, + parent_request_id: DataColumnsByRangeRequester::ComponentsByRange( + ComponentsByRangeRequestId { + id: 1, + requester: RangeRequestId::RangeSync { + chain_id: 1, + batch_id: Epoch::new(1), + }, + }, + ), + peer: PeerId::random(), + }, + )), + RequestType::DataColumnsByRange(DataColumnsByRangeRequest { + start_slot: 0, + count: 0, + // exceeds the max request defined in the spec. + columns: vec![0; E::number_of_columns() + 1], + }), + ); +} + +fn test_request_too_large(app_request_id: AppRequestId, request: RequestType<E>) { + // Set up the logging. + let log_level = "debug"; + let enable_logging = true; + let _subscriber = build_tracing_subscriber(log_level, enable_logging); + let rt = Arc::new(Runtime::new().unwrap()); + let spec = Arc::new(spec_with_all_forks_enabled()); + + rt.block_on(async { + let (mut sender, mut receiver) = common::build_node_pair( + Arc::downgrade(&rt), + ForkName::Base, + spec, + Protocol::Tcp, + false, + None, + ) + .await; + + // Build the sender future + let sender_future = async { + loop { + match sender.next_event().await { + NetworkEvent::PeerConnectedOutgoing(peer_id) => { + debug!(?request, %peer_id, "Sending RPC request"); + sender + .send_request(peer_id, app_request_id, request.clone()) + .unwrap(); + } + NetworkEvent::ResponseReceived { + app_request_id, + response, + .. + } => { + debug!(?app_request_id, ?response, "Received response"); + } + NetworkEvent::RPCFailed { error, .. } => { + // This variant should be unreachable, as the receiver doesn't respond with an error when a request exceeds the limit. + debug!(?error, "RPC failed"); + unreachable!(); + } + NetworkEvent::PeerDisconnected(peer_id) => { + // The receiver should disconnect as a result of the invalid request. + debug!(%peer_id, "Peer disconnected"); + // End the test. + return; + } + _ => {} + } + } + } + .instrument(info_span!("Sender")); + + // Build the receiver future + let receiver_future = async { + loop { + if let NetworkEvent::RequestReceived { .. } = receiver.next_event().await { + // This event should be unreachable, as the handler drops the invalid request. + unreachable!(); + } + } + } + .instrument(info_span!("Receiver")); + + tokio::select! { + _ = sender_future => {} + _ = receiver_future => {} + _ = sleep(Duration::from_secs(30)) => { + panic!("Future timed out"); + } + } + }); +} From 691c8cf8e69d4d40c4969c2bc493dc1eed9af99f Mon Sep 17 00:00:00 2001 From: Jimmy Chen <jchen.tc@gmail.com> Date: Wed, 18 Feb 2026 15:16:57 +1100 Subject: [PATCH 32/81] Fix duplicate data columns in DataColumnsByRange responses (#8843) Co-Authored-By: Jimmy Chen <jchen.tc@gmail.com> --- .../network_beacon_processor/rpc_methods.rs | 5 +- .../src/network_beacon_processor/tests.rs | 116 ++++++++++++++++++ 2 files changed, 120 insertions(+), 1 deletion(-) diff --git a/beacon_node/network/src/network_beacon_processor/rpc_methods.rs b/beacon_node/network/src/network_beacon_processor/rpc_methods.rs index 5edd661bb6..279870d444 100644 --- a/beacon_node/network/src/network_beacon_processor/rpc_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/rpc_methods.rs @@ -977,7 +977,10 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> { }; // remove all skip slots i.e. duplicated roots - Ok(block_roots.into_iter().unique().collect::<Vec<_>>()) + Ok(block_roots + .into_iter() + .unique_by(|(root, _)| *root) + .collect::<Vec<_>>()) } /// Handle a `BlobsByRange` request from the peer. diff --git a/beacon_node/network/src/network_beacon_processor/tests.rs b/beacon_node/network/src/network_beacon_processor/tests.rs index 49b1c0c262..32ca84453a 100644 --- a/beacon_node/network/src/network_beacon_processor/tests.rs +++ b/beacon_node/network/src/network_beacon_processor/tests.rs @@ -120,6 +120,39 @@ impl TestRig { .await } + pub async fn new_with_skip_slots(chain_length: u64, skip_slots: &HashSet<u64>) -> Self { + let mut spec = test_spec::<E>(); + spec.shard_committee_period = 2; + let spec = Arc::new(spec); + let beacon_processor_config = BeaconProcessorConfig::default(); + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec.clone()) + .deterministic_keypairs(VALIDATOR_COUNT) + .fresh_ephemeral_store() + .mock_execution_layer() + .node_custody_type(NodeCustodyType::Fullnode) + .chain_config(<_>::default()) + .build(); + + harness.advance_slot(); + + for slot in 1..=chain_length { + if !skip_slots.contains(&slot) { + harness + .extend_chain( + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + } + + harness.advance_slot(); + } + + Self::from_harness(harness, beacon_processor_config, spec).await + } + pub async fn new_parametric( chain_length: u64, beacon_processor_config: BeaconProcessorConfig, @@ -150,6 +183,14 @@ impl TestRig { harness.advance_slot(); } + Self::from_harness(harness, beacon_processor_config, spec).await + } + + async fn from_harness( + harness: BeaconChainHarness<T>, + beacon_processor_config: BeaconProcessorConfig, + spec: Arc<ChainSpec>, + ) -> Self { let head = harness.chain.head_snapshot(); assert_eq!( @@ -1986,3 +2027,78 @@ async fn test_data_columns_by_range_request_only_returns_requested_columns() { "Should have received at least some data columns" ); } + +/// Test that DataColumnsByRange does not return duplicate data columns for skip slots. +/// +/// When skip slots occur, `forwards_iter_block_roots` returns the same block root for +/// consecutive slots. The deduplication in `get_block_roots_from_store` must use +/// `unique_by` on the root (not the full `(root, slot)` tuple) to avoid serving +/// duplicate data columns for the same block. +#[tokio::test] +async fn test_data_columns_by_range_no_duplicates_with_skip_slots() { + if test_spec::<E>().fulu_fork_epoch.is_none() { + return; + }; + + // Build a chain of 128 slots (4 epochs) with skip slots at positions 5 and 6. + // After 4 epochs, finalized_epoch=2 (finalized_slot=64). Requesting slots 0-9 + // satisfies req_start_slot + req_count <= finalized_slot (10 <= 64), which routes + // through `get_block_roots_from_store` — the code path with the bug. + let skip_slots: HashSet<u64> = [5, 6].into_iter().collect(); + let mut rig = TestRig::new_with_skip_slots(128, &skip_slots).await; + + let all_custody_columns = rig.chain.custody_columns_for_epoch(Some(Epoch::new(0))); + let requested_column = vec![all_custody_columns[0]]; + + // Request a range that spans the skip slots (slots 0 through 9). + let start_slot = 0; + let slot_count = 10; + + rig.network_beacon_processor + .send_data_columns_by_range_request( + PeerId::random(), + InboundRequestId::new_unchecked(42, 24), + DataColumnsByRangeRequest { + start_slot, + count: slot_count, + columns: requested_column.clone(), + }, + ) + .unwrap(); + + // Collect block roots from all data column responses. + let mut block_roots: Vec<Hash256> = Vec::new(); + + while let Some(next) = rig.network_rx.recv().await { + if let NetworkMessage::SendResponse { + peer_id: _, + response: Response::DataColumnsByRange(data_column), + inbound_request_id: _, + } = next + { + if let Some(column) = data_column { + block_roots.push(column.block_root()); + } else { + break; + } + } else { + panic!("unexpected message {:?}", next); + } + } + + assert!( + !block_roots.is_empty(), + "Should have received at least some data columns" + ); + + // Before the fix, skip slots caused the same block root to appear multiple times + // (once per skip slot) because .unique() on (Hash256, Slot) tuples didn't deduplicate. + let unique_roots: HashSet<_> = block_roots.iter().collect(); + assert_eq!( + block_roots.len(), + unique_roots.len(), + "Response contained duplicate block roots: got {} columns but only {} unique roots", + block_roots.len(), + unique_roots.len(), + ); +} From c5b4580e37b44a57605a07a9acdd5057c1b06010 Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay <pawandhananjay@gmail.com> Date: Wed, 18 Feb 2026 09:47:07 +0530 Subject: [PATCH 33/81] Return correct variant for snappy errors (#8841) N/A Handle snappy crate errors as InvalidData instead of IoError. Co-Authored-By: Pawan Dhananjay <pawandhananjay@gmail.com> --- .../lighthouse_network/src/rpc/codec.rs | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/beacon_node/lighthouse_network/src/rpc/codec.rs b/beacon_node/lighthouse_network/src/rpc/codec.rs index 36d9726dd9..d1a3182fad 100644 --- a/beacon_node/lighthouse_network/src/rpc/codec.rs +++ b/beacon_node/lighthouse_network/src/rpc/codec.rs @@ -457,6 +457,9 @@ fn handle_error<T>( Ok(None) } } + // All snappy errors from the snap crate bubble up as `Other` kind errors + // that imply invalid response + ErrorKind::Other => Err(RPCError::InvalidData(err.to_string())), _ => Err(RPCError::from(err)), } } @@ -2317,4 +2320,43 @@ mod tests { RPCError::InvalidData(_) )); } + + /// Test invalid snappy response. + #[test] + fn test_invalid_snappy_response() { + let spec = spec_with_all_forks_enabled(); + let fork_ctx = Arc::new(fork_context(ForkName::latest(), &spec)); + let max_packet_size = spec.max_payload_size as usize; // 10 MiB. + + let protocol = ProtocolId::new(SupportedProtocol::BlocksByRangeV2, Encoding::SSZSnappy); + + let mut codec = SSZSnappyOutboundCodec::<Spec>::new( + protocol.clone(), + max_packet_size, + fork_ctx.clone(), + ); + + let mut payload = BytesMut::new(); + payload.extend_from_slice(&[0u8]); + let deneb_epoch = spec.deneb_fork_epoch.unwrap(); + payload.extend_from_slice(&fork_ctx.context_bytes(deneb_epoch)); + + // Claim the MAXIMUM allowed size (10 MiB) + let claimed_size = max_packet_size; + let mut uvi_codec: Uvi<usize> = Uvi::default(); + uvi_codec.encode(claimed_size, &mut payload).unwrap(); + payload.extend_from_slice(&[0xBB; 16]); // Junk snappy. + + let result = codec.decode(&mut payload); + + assert!(result.is_err(), "Expected decode to fail"); + + // IoError = reached snappy decode (allocation happened). + let err = result.unwrap_err(); + assert!( + matches!(err, RPCError::InvalidData(_)), + "Should return invalid data variant {}", + err + ); + } } From be799cb2ad2fb0243cdc2a2f368091ffee29fe8e Mon Sep 17 00:00:00 2001 From: Michael Sproul <michaelsproul@users.noreply.github.com> Date: Wed, 18 Feb 2026 16:28:17 +1100 Subject: [PATCH 34/81] Validator client head monitor timeout fix (#8846) Fix a bug in v8.1.0 whereby the VC times out continuously with: > Feb 18 02:03:48.030 WARN Head service failed retrying starting next slot error: "Head monitoring stream error, node: 0, error: SseClient(Transport(reqwest::Error { kind: Decode, source: reqwest::Error { kind: Body, source: TimedOut } }))" - Remove the existing timeout for the events API by using `Duration::MAX`. This is necessary as the client is configured with a default timeout. This is the only way to override/remove it. - DO NOT add a `read_timeout` (yet), as this would need to be configured on a per-client basis. We do not want to create a new Client for every call as the early commits on this branch were doing, as this would bypass the TLS cert config, and is also wasteful. Co-Authored-By: hopinheimer <knmanas6@gmail.com> Co-Authored-By: Michael Sproul <michael@sigmaprime.io> Co-Authored-By: Michael Sproul <michaelsproul@users.noreply.github.com> --- common/eth2/src/lib.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 10382b028a..76b05130d7 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -76,8 +76,6 @@ const HTTP_GET_BEACON_BLOCK_SSZ_TIMEOUT_QUOTIENT: u32 = 4; const HTTP_GET_DEBUG_BEACON_STATE_QUOTIENT: u32 = 4; const HTTP_GET_DEPOSIT_SNAPSHOT_QUOTIENT: u32 = 4; const HTTP_GET_VALIDATOR_BLOCK_TIMEOUT_QUOTIENT: u32 = 4; -// Generally the timeout for events should be longer than a slot. -const HTTP_GET_EVENTS_TIMEOUT_MULTIPLIER: u32 = 50; const HTTP_DEFAULT_TIMEOUT_QUOTIENT: u32 = 4; /// A struct to define a variety of different timeouts for different validator tasks to ensure @@ -98,7 +96,6 @@ pub struct Timeouts { pub get_debug_beacon_states: Duration, pub get_deposit_snapshot: Duration, pub get_validator_block: Duration, - pub events: Duration, pub default: Duration, } @@ -119,7 +116,6 @@ impl Timeouts { get_debug_beacon_states: timeout, get_deposit_snapshot: timeout, get_validator_block: timeout, - events: HTTP_GET_EVENTS_TIMEOUT_MULTIPLIER * timeout, default: timeout, } } @@ -142,7 +138,6 @@ impl Timeouts { get_debug_beacon_states: base_timeout / HTTP_GET_DEBUG_BEACON_STATE_QUOTIENT, get_deposit_snapshot: base_timeout / HTTP_GET_DEPOSIT_SNAPSHOT_QUOTIENT, get_validator_block: base_timeout / HTTP_GET_VALIDATOR_BLOCK_TIMEOUT_QUOTIENT, - events: HTTP_GET_EVENTS_TIMEOUT_MULTIPLIER * base_timeout, default: base_timeout / HTTP_DEFAULT_TIMEOUT_QUOTIENT, } } @@ -2805,10 +2800,14 @@ impl BeaconNodeHttpClient { .join(","); path.query_pairs_mut().append_pair("topics", &topic_string); + // Do not use a timeout for the events endpoint. Using a regular timeout will trigger a + // timeout every `timeout` seconds, regardless of any data streamed from the endpoint. + // In future we could add a read_timeout, but that can only be configured globally on the + // Client. let mut es = self .client .get(path) - .timeout(self.timeouts.events) + .timeout(Duration::MAX) .eventsource() .map_err(Error::SseEventSource)?; // If we don't await `Event::Open` here, then the consumer From 54b35761452d73147a80665614230dd2b5dc2951 Mon Sep 17 00:00:00 2001 From: Jimmy Chen <jchen.tc@gmail.com> Date: Wed, 18 Feb 2026 20:31:57 +1100 Subject: [PATCH 35/81] Update agent review instructions on large PRs (#8845) Co-Authored-By: Jimmy Chen <jchen.tc@gmail.com> --- .ai/CODE_REVIEW.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.ai/CODE_REVIEW.md b/.ai/CODE_REVIEW.md index e4da3b22d5..2ce60c80fd 100644 --- a/.ai/CODE_REVIEW.md +++ b/.ai/CODE_REVIEW.md @@ -190,6 +190,14 @@ we typically try to avoid runtime panics outside of startup." - Edge cases handled? - Context provided with errors? +## Large PR Strategy + +Large PRs (10+ files) make it easy to miss subtle bugs in individual files. + +- **Group files by subsystem** (networking, store, types, etc.) and review each group, but pay extra attention to changes that cross subsystem boundaries. +- **Review shared type/interface changes first** — changes to function signatures, return types, or struct definitions ripple through all callers. When reviewing a large PR, identify these first and trace their impact across the codebase. Downstream code may silently change behavior even if it looks untouched. +- **Flag missing test coverage for changed behavior** — if a code path's semantics change (even subtly), check that tests exercise it. If not, flag the gap. + ## Deep Review Techniques ### Verify Against Specifications @@ -275,3 +283,4 @@ Group related state and behavior together. If two fields are always set together - [ ] Tests present: Non-trivial changes have tests - [ ] Lock safety: Lock ordering is safe and documented - [ ] No blocking: Async code doesn't block runtime + From fab77f4fc9fc8fc5d9e9b9d82999cdd015d14859 Mon Sep 17 00:00:00 2001 From: Michael Sproul <michaelsproul@users.noreply.github.com> Date: Thu, 19 Feb 2026 08:55:13 +1100 Subject: [PATCH 36/81] Skip payload_invalidation tests prior to Bellatrix (#8856) Fix the failure of the beacon-chain tests for phase0/altair, which now only runs nightly. Just skip the payload invalidation tests, they don't make any sense prior to Bellatrix anyway. Co-Authored-By: Michael Sproul <michael@sigmaprime.io> --- .../tests/payload_invalidation.rs | 68 ++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index eb8e57a5d5..7fd70f0e77 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::{ INVALID_JUSTIFIED_PAYLOAD_SHUTDOWN_REASON, NotifyExecutionLayer, OverrideForkchoiceUpdate, StateSkipConfig, WhenSlotSkipped, canonical_head::{CachedHead, CanonicalHead}, - test_utils::{BeaconChainHarness, EphemeralHarnessType, test_spec}, + test_utils::{BeaconChainHarness, EphemeralHarnessType, fork_name_from_env, test_spec}, }; use execution_layer::{ ExecutionLayer, ForkchoiceState, PayloadAttributes, @@ -389,6 +389,9 @@ impl InvalidPayloadRig { /// Simple test of the different import types. #[tokio::test] async fn valid_invalid_syncing() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let mut rig = InvalidPayloadRig::new(); rig.move_to_terminal_block(); @@ -404,6 +407,9 @@ async fn valid_invalid_syncing() { /// `latest_valid_hash`. #[tokio::test] async fn invalid_payload_invalidates_parent() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let mut rig = InvalidPayloadRig::new().enable_attestations(); rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; // Import a valid transition block. @@ -460,6 +466,9 @@ async fn immediate_forkchoice_update_invalid_test( #[tokio::test] async fn immediate_forkchoice_update_payload_invalid() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } immediate_forkchoice_update_invalid_test(|latest_valid_hash| Payload::Invalid { latest_valid_hash, }) @@ -468,11 +477,17 @@ async fn immediate_forkchoice_update_payload_invalid() { #[tokio::test] async fn immediate_forkchoice_update_payload_invalid_block_hash() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } immediate_forkchoice_update_invalid_test(|_| Payload::InvalidBlockHash).await } #[tokio::test] async fn immediate_forkchoice_update_payload_invalid_terminal_block() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } immediate_forkchoice_update_invalid_test(|_| Payload::Invalid { latest_valid_hash: Some(ExecutionBlockHash::zero()), }) @@ -482,6 +497,9 @@ async fn immediate_forkchoice_update_payload_invalid_terminal_block() { /// Ensure the client tries to exit when the justified checkpoint is invalidated. #[tokio::test] async fn justified_checkpoint_becomes_invalid() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let mut rig = InvalidPayloadRig::new().enable_attestations(); rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; // Import a valid transition block. @@ -524,6 +542,9 @@ async fn justified_checkpoint_becomes_invalid() { /// Ensure that a `latest_valid_hash` for a pre-finality block only reverts a single block. #[tokio::test] async fn pre_finalized_latest_valid_hash() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let num_blocks = E::slots_per_epoch() * 4; let finalized_epoch = 2; @@ -571,6 +592,9 @@ async fn pre_finalized_latest_valid_hash() { /// - Will not validate `latest_valid_root` and its ancestors. #[tokio::test] async fn latest_valid_hash_will_not_validate() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } const LATEST_VALID_SLOT: u64 = 3; let mut rig = InvalidPayloadRig::new().enable_attestations(); @@ -618,6 +642,9 @@ async fn latest_valid_hash_will_not_validate() { /// Check behaviour when the `latest_valid_hash` is a junk value. #[tokio::test] async fn latest_valid_hash_is_junk() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let num_blocks = E::slots_per_epoch() * 5; let finalized_epoch = 3; @@ -659,6 +686,9 @@ async fn latest_valid_hash_is_junk() { /// Check that descendants of invalid blocks are also invalidated. #[tokio::test] async fn invalidates_all_descendants() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let num_blocks = E::slots_per_epoch() * 4 + E::slots_per_epoch() / 2; let finalized_epoch = 2; let finalized_slot = E::slots_per_epoch() * 2; @@ -766,6 +796,9 @@ async fn invalidates_all_descendants() { /// Check that the head will switch after the canonical branch is invalidated. #[tokio::test] async fn switches_heads() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let num_blocks = E::slots_per_epoch() * 4 + E::slots_per_epoch() / 2; let finalized_epoch = 2; let finalized_slot = E::slots_per_epoch() * 2; @@ -869,6 +902,9 @@ async fn switches_heads() { #[tokio::test] async fn invalid_during_processing() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let mut rig = InvalidPayloadRig::new(); rig.move_to_terminal_block(); @@ -901,6 +937,9 @@ async fn invalid_during_processing() { #[tokio::test] async fn invalid_after_optimistic_sync() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let mut rig = InvalidPayloadRig::new().enable_attestations(); rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; // Import a valid transition block. @@ -939,6 +978,9 @@ async fn invalid_after_optimistic_sync() { #[tokio::test] async fn manually_validate_child() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let mut rig = InvalidPayloadRig::new().enable_attestations(); rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; // Import a valid transition block. @@ -957,6 +999,9 @@ async fn manually_validate_child() { #[tokio::test] async fn manually_validate_parent() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let mut rig = InvalidPayloadRig::new().enable_attestations(); rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; // Import a valid transition block. @@ -975,6 +1020,9 @@ async fn manually_validate_parent() { #[tokio::test] async fn payload_preparation() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let mut rig = InvalidPayloadRig::new(); rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; @@ -1036,6 +1084,9 @@ async fn payload_preparation() { #[tokio::test] async fn invalid_parent() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let mut rig = InvalidPayloadRig::new(); rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; // Import a valid transition block. @@ -1108,6 +1159,9 @@ async fn invalid_parent() { /// Tests to ensure that we will still send a proposer preparation #[tokio::test] async fn payload_preparation_before_transition_block() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let rig = InvalidPayloadRig::new(); let el = rig.execution_layer(); @@ -1180,6 +1234,9 @@ async fn payload_preparation_before_transition_block() { #[tokio::test] async fn attesting_to_optimistic_head() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let mut rig = InvalidPayloadRig::new(); rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; // Import a valid transition block. @@ -1392,6 +1449,9 @@ impl InvalidHeadSetup { #[tokio::test] async fn recover_from_invalid_head_by_importing_blocks() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let InvalidHeadSetup { rig, fork_block, @@ -1437,6 +1497,9 @@ async fn recover_from_invalid_head_by_importing_blocks() { #[tokio::test] async fn recover_from_invalid_head_after_persist_and_reboot() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let InvalidHeadSetup { rig, fork_block: _, @@ -1479,6 +1542,9 @@ async fn recover_from_invalid_head_after_persist_and_reboot() { #[tokio::test] async fn weights_after_resetting_optimistic_status() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let mut rig = InvalidPayloadRig::new().enable_attestations(); rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; // Import a valid transition block. From 5e2d296de619d582ceff214584d8056776c05fd7 Mon Sep 17 00:00:00 2001 From: chonghe <44791194+chong-he@users.noreply.github.com> Date: Thu, 19 Feb 2026 05:55:16 +0800 Subject: [PATCH 37/81] Validator manager import to allow overriding fields with CLI flag (#7684) * #7651 Co-Authored-By: Tan Chee Keong <tanck@sigmaprime.io> Co-Authored-By: chonghe <44791194+chong-he@users.noreply.github.com> Co-Authored-By: Lion - dapplion <35266934+dapplion@users.noreply.github.com> --- book/src/help_vm_import.md | 3 + lighthouse/tests/validator_manager.rs | 65 +++++++++ validator_manager/src/import_validators.rs | 161 +++++++++++++++------ 3 files changed, 183 insertions(+), 46 deletions(-) diff --git a/book/src/help_vm_import.md b/book/src/help_vm_import.md index 3c768f6705..09c1b74f4d 100644 --- a/book/src/help_vm_import.md +++ b/book/src/help_vm_import.md @@ -24,6 +24,9 @@ Options: --debug-level <LEVEL> Specifies the verbosity level used when emitting logs to the terminal. [default: info] [possible values: info, debug, trace, warn, error] + --enabled <enabled> + When provided, the imported validator will be enabled or disabled. + [possible values: true, false] --gas-limit <UINT64> When provided, the imported validator will use this gas limit. It is recommended to leave this as the default value by not specifying this diff --git a/lighthouse/tests/validator_manager.rs b/lighthouse/tests/validator_manager.rs index d6d720a561..9bad1cdc91 100644 --- a/lighthouse/tests/validator_manager.rs +++ b/lighthouse/tests/validator_manager.rs @@ -16,6 +16,7 @@ use validator_manager::{ list_validators::ListConfig, move_validators::{MoveConfig, PasswordSource, Validators}, }; +use zeroize::Zeroizing; const EXAMPLE_ETH1_ADDRESS: &str = "0x00000000219ab540356cBB839Cbe05303d7705Fa"; @@ -280,6 +281,40 @@ pub fn validator_import_using_both_file_flags() { .assert_failed(); } +#[test] +pub fn validator_import_keystore_file_without_password_flag_should_fail() { + CommandLineTest::validators_import() + .flag("--vc-token", Some("./token.json")) + .flag("--keystore-file", Some("./keystore.json")) + .assert_failed(); +} + +#[test] +pub fn validator_import_keystore_file_with_password_flag_should_pass() { + CommandLineTest::validators_import() + .flag("--vc-token", Some("./token.json")) + .flag("--keystore-file", Some("./keystore.json")) + .flag("--password", Some("abcd")) + .assert_success(|config| { + let expected = ImportConfig { + validators_file_path: None, + keystore_file_path: Some(PathBuf::from("./keystore.json")), + vc_url: SensitiveUrl::parse("http://localhost:5062").unwrap(), + vc_token_path: PathBuf::from("./token.json"), + ignore_duplicates: false, + password: Some(Zeroizing::new("abcd".into())), + fee_recipient: None, + builder_boost_factor: None, + gas_limit: None, + builder_proposals: None, + enabled: None, + prefer_builder_proposals: None, + }; + assert_eq!(expected, config); + println!("{:?}", expected); + }); +} + #[test] pub fn validator_import_missing_both_file_flags() { CommandLineTest::validators_import() @@ -287,6 +322,36 @@ pub fn validator_import_missing_both_file_flags() { .assert_failed(); } +#[test] +pub fn validator_import_fee_recipient_override() { + CommandLineTest::validators_import() + .flag("--validators-file", Some("./vals.json")) + .flag("--vc-token", Some("./token.json")) + .flag("--suggested-fee-recipient", Some(EXAMPLE_ETH1_ADDRESS)) + .flag("--gas-limit", Some("1337")) + .flag("--builder-proposals", Some("true")) + .flag("--builder-boost-factor", Some("150")) + .flag("--prefer-builder-proposals", Some("true")) + .flag("--enabled", Some("false")) + .assert_success(|config| { + let expected = ImportConfig { + validators_file_path: Some(PathBuf::from("./vals.json")), + keystore_file_path: None, + vc_url: SensitiveUrl::parse("http://localhost:5062").unwrap(), + vc_token_path: PathBuf::from("./token.json"), + ignore_duplicates: false, + password: None, + fee_recipient: Some(Address::from_str(EXAMPLE_ETH1_ADDRESS).unwrap()), + builder_boost_factor: Some(150), + gas_limit: Some(1337), + builder_proposals: Some(true), + enabled: Some(false), + prefer_builder_proposals: Some(true), + }; + assert_eq!(expected, config); + }); +} + #[test] pub fn validator_move_defaults() { CommandLineTest::validators_move() diff --git a/validator_manager/src/import_validators.rs b/validator_manager/src/import_validators.rs index 24917f7d1b..0d6d358edb 100644 --- a/validator_manager/src/import_validators.rs +++ b/validator_manager/src/import_validators.rs @@ -112,8 +112,7 @@ pub fn cli_app() -> Command { .value_name("ETH1_ADDRESS") .help("When provided, the imported validator will use the suggested fee recipient. Omit this flag to use the default value from the VC.") .action(ArgAction::Set) - .display_order(0) - .requires(KEYSTORE_FILE_FLAG), + .display_order(0), ) .arg( Arg::new(GAS_LIMIT) @@ -122,8 +121,7 @@ pub fn cli_app() -> Command { .help("When provided, the imported validator will use this gas limit. It is recommended \ to leave this as the default value by not specifying this flag.",) .action(ArgAction::Set) - .display_order(0) - .requires(KEYSTORE_FILE_FLAG), + .display_order(0), ) .arg( Arg::new(BUILDER_PROPOSALS) @@ -132,8 +130,7 @@ pub fn cli_app() -> Command { blocks via builder rather than the local EL.",) .value_parser(["true","false"]) .action(ArgAction::Set) - .display_order(0) - .requires(KEYSTORE_FILE_FLAG), + .display_order(0), ) .arg( Arg::new(BUILDER_BOOST_FACTOR) @@ -144,8 +141,7 @@ pub fn cli_app() -> Command { when choosing between a builder payload header and payload from \ the local execution node.",) .action(ArgAction::Set) - .display_order(0) - .requires(KEYSTORE_FILE_FLAG), + .display_order(0), ) .arg( Arg::new(PREFER_BUILDER_PROPOSALS) @@ -154,8 +150,16 @@ pub fn cli_app() -> Command { constructed by builders, regardless of payload value.",) .value_parser(["true","false"]) .action(ArgAction::Set) - .display_order(0) - .requires(KEYSTORE_FILE_FLAG), + .display_order(0), + ) + .arg( + Arg::new(ENABLED) + .long(ENABLED) + .help("When provided, the imported validator will be \ + enabled or disabled.",) + .value_parser(["true","false"]) + .action(ArgAction::Set) + .display_order(0), ) } @@ -225,48 +229,113 @@ async fn run(config: ImportConfig) -> Result<(), String> { enabled, } = config; - let validators: Vec<ValidatorSpecification> = - if let Some(validators_format_path) = &validators_file_path { - if !validators_format_path.exists() { - return Err(format!( - "Unable to find file at {:?}", - validators_format_path - )); - } + let validators: Vec<ValidatorSpecification> = if let Some(validators_format_path) = + &validators_file_path + { + if !validators_format_path.exists() { + return Err(format!( + "Unable to find file at {:?}", + validators_format_path + )); + } - let validators_file = fs::OpenOptions::new() - .read(true) - .create(false) - .open(validators_format_path) - .map_err(|e| format!("Unable to open {:?}: {:?}", validators_format_path, e))?; + let validators_file = fs::OpenOptions::new() + .read(true) + .create(false) + .open(validators_format_path) + .map_err(|e| format!("Unable to open {:?}: {:?}", validators_format_path, e))?; - serde_json::from_reader(&validators_file).map_err(|e| { + // Define validators as mutable so that if a relevant flag is supplied, the fields can be overridden. + let mut validators: Vec<ValidatorSpecification> = serde_json::from_reader(&validators_file) + .map_err(|e| { format!( "Unable to parse JSON in {:?}: {:?}", validators_format_path, e ) - })? - } else if let Some(keystore_format_path) = &keystore_file_path { - vec![ValidatorSpecification { - voting_keystore: KeystoreJsonStr( - Keystore::from_json_file(keystore_format_path).map_err(|e| format!("{e:?}"))?, - ), - voting_keystore_password: password.ok_or_else(|| { - "The --password flag is required to supply the keystore password".to_string() - })?, - slashing_protection: None, - fee_recipient, - gas_limit, - builder_proposals, - builder_boost_factor, - prefer_builder_proposals, - enabled, - }] - } else { - return Err(format!( - "One of the flag --{VALIDATORS_FILE_FLAG} or --{KEYSTORE_FILE_FLAG} is required." - )); - }; + })?; + + // Log the overridden note when one or more flags is supplied + if let Some(override_fee_recipient) = fee_recipient { + eprintln!( + "Please note! --suggested-fee-recipient is provided. This will override existing fee recipient defined in validators.json with: {:?}", + override_fee_recipient + ); + } + if let Some(override_gas_limit) = gas_limit { + eprintln!( + "Please note! --gas-limit is provided. This will override existing gas limit defined in validators.json with: {}", + override_gas_limit + ); + } + if let Some(override_builder_proposals) = builder_proposals { + eprintln!( + "Please note! --builder-proposals is provided. This will override existing builder proposal setting defined in validators.json with: {}", + override_builder_proposals + ); + } + if let Some(override_builder_boost_factor) = builder_boost_factor { + eprintln!( + "Please note! --builder-boost-factor is provided. This will override existing builder boost factor defined in validators.json with: {}", + override_builder_boost_factor + ); + } + if let Some(override_prefer_builder_proposals) = prefer_builder_proposals { + eprintln!( + "Please note! --prefer-builder-proposals is provided. This will override existing prefer builder proposal setting defined in validators.json with: {}", + override_prefer_builder_proposals + ); + } + if let Some(override_enabled) = enabled { + eprintln!( + "Please note! --enabled flag is provided. This will override existing setting defined in validators.json with: {}", + override_enabled + ); + } + + // Override the fields in validators.json file if the flag is supplied + for validator in &mut validators { + if let Some(override_fee_recipient) = fee_recipient { + validator.fee_recipient = Some(override_fee_recipient); + } + if let Some(override_gas_limit) = gas_limit { + validator.gas_limit = Some(override_gas_limit); + } + if let Some(override_builder_proposals) = builder_proposals { + validator.builder_proposals = Some(override_builder_proposals); + } + if let Some(override_builder_boost_factor) = builder_boost_factor { + validator.builder_boost_factor = Some(override_builder_boost_factor); + } + if let Some(override_prefer_builder_proposals) = prefer_builder_proposals { + validator.prefer_builder_proposals = Some(override_prefer_builder_proposals); + } + if let Some(override_enabled) = enabled { + validator.enabled = Some(override_enabled); + } + } + + validators + } else if let Some(keystore_format_path) = &keystore_file_path { + vec![ValidatorSpecification { + voting_keystore: KeystoreJsonStr( + Keystore::from_json_file(keystore_format_path).map_err(|e| format!("{e:?}"))?, + ), + voting_keystore_password: password.ok_or_else(|| { + "The --password flag is required to supply the keystore password".to_string() + })?, + slashing_protection: None, + fee_recipient, + gas_limit, + builder_proposals, + builder_boost_factor, + prefer_builder_proposals, + enabled, + }] + } else { + return Err(format!( + "One of the flag --{VALIDATORS_FILE_FLAG} or --{KEYSTORE_FILE_FLAG} is required." + )); + }; let count = validators.len(); From 561898fc1c74c11a1a765f252ae504f35263f6ed Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay <pawandhananjay@gmail.com> Date: Thu, 19 Feb 2026 06:08:56 +0530 Subject: [PATCH 38/81] Process head_chains in descending order of number of peers (#8859) N/A Another find by @gitToki. Sort the preferred_ids in descending order as originally intended from the comment in the function. Co-Authored-By: Pawan Dhananjay <pawandhananjay@gmail.com> --- beacon_node/network/src/sync/range_sync/chain_collection.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/beacon_node/network/src/sync/range_sync/chain_collection.rs b/beacon_node/network/src/sync/range_sync/chain_collection.rs index 1d57ee6c3d..bd4dd6c181 100644 --- a/beacon_node/network/src/sync/range_sync/chain_collection.rs +++ b/beacon_node/network/src/sync/range_sync/chain_collection.rs @@ -351,7 +351,8 @@ impl<T: BeaconChainTypes> ChainCollection<T> { .iter() .map(|(id, chain)| (chain.available_peers(), !chain.is_syncing(), *id)) .collect::<Vec<_>>(); - preferred_ids.sort_unstable(); + // Sort in descending order + preferred_ids.sort_unstable_by(|a, b| b.cmp(a)); let mut syncing_chains = SmallVec::<[Id; PARALLEL_HEAD_CHAINS]>::new(); for (_, _, id) in preferred_ids { From 4588971085840dc56cedc85ba0f12bcaa99be8ed Mon Sep 17 00:00:00 2001 From: Jimmy Chen <jchen.tc@gmail.com> Date: Thu, 19 Feb 2026 12:57:53 +1100 Subject: [PATCH 39/81] Add sync batch state metrics (#8847) Co-Authored-By: Jimmy Chen <jchen.tc@gmail.com> --- beacon_node/network/src/metrics.rs | 7 +++++ .../network/src/sync/backfill_sync/mod.rs | 20 +++++++++++++- beacon_node/network/src/sync/batch.rs | 26 ++++++++++++++++++- .../src/sync/custody_backfill_sync/mod.rs | 21 +++++++++++++-- beacon_node/network/src/sync/manager.rs | 3 +++ .../network/src/sync/range_sync/chain.rs | 11 +++++++- .../src/sync/range_sync/chain_collection.rs | 21 +++++++++++++++ .../network/src/sync/range_sync/range.rs | 4 +++ 8 files changed, 108 insertions(+), 5 deletions(-) diff --git a/beacon_node/network/src/metrics.rs b/beacon_node/network/src/metrics.rs index cea06a28c8..0fa95b4758 100644 --- a/beacon_node/network/src/metrics.rs +++ b/beacon_node/network/src/metrics.rs @@ -462,6 +462,13 @@ pub static SYNCING_CHAIN_BATCH_AWAITING_PROCESSING: LazyLock<Result<Histogram>> ]), ) }); +pub static SYNCING_CHAIN_BATCHES: LazyLock<Result<IntGaugeVec>> = LazyLock::new(|| { + try_create_int_gauge_vec( + "sync_batches", + "Number of batches in sync chains by sync type and state", + &["sync_type", "state"], + ) +}); pub static SYNC_SINGLE_BLOCK_LOOKUPS: LazyLock<Result<IntGauge>> = LazyLock::new(|| { try_create_int_gauge( "sync_single_block_lookups", diff --git a/beacon_node/network/src/sync/backfill_sync/mod.rs b/beacon_node/network/src/sync/backfill_sync/mod.rs index 9802ec56a1..f18d31863b 100644 --- a/beacon_node/network/src/sync/backfill_sync/mod.rs +++ b/beacon_node/network/src/sync/backfill_sync/mod.rs @@ -8,9 +8,11 @@ //! If a batch fails, the backfill sync cannot progress. In this scenario, we mark the backfill //! sync as failed, log an error and attempt to retry once a new peer joins the node. +use crate::metrics; use crate::network_beacon_processor::ChainSegmentProcessId; use crate::sync::batch::{ - BatchConfig, BatchId, BatchInfo, BatchOperationOutcome, BatchProcessingResult, BatchState, + BatchConfig, BatchId, BatchInfo, BatchMetricsState, BatchOperationOutcome, + BatchProcessingResult, BatchState, }; use crate::sync::block_sidecar_coupling::CouplingError; use crate::sync::manager::BatchProcessResult; @@ -31,6 +33,7 @@ use std::collections::{ use std::hash::{Hash, Hasher}; use std::marker::PhantomData; use std::sync::Arc; +use strum::IntoEnumIterator; use tracing::{debug, error, info, warn}; use types::{ColumnIndex, Epoch, EthSpec}; @@ -1181,6 +1184,21 @@ impl<T: BeaconChainTypes> BackFillSync<T> { .epoch(T::EthSpec::slots_per_epoch()) } + pub fn register_metrics(&self) { + for state in BatchMetricsState::iter() { + let count = self + .batches + .values() + .filter(|b| b.state().metrics_state() == state) + .count(); + metrics::set_gauge_vec( + &metrics::SYNCING_CHAIN_BATCHES, + &["backfill", state.into()], + count as i64, + ); + } + } + /// Updates the global network state indicating the current state of a backfill sync. fn set_state(&self, state: BackFillState) { *self.network_globals.backfill_state.write() = state; diff --git a/beacon_node/network/src/sync/batch.rs b/beacon_node/network/src/sync/batch.rs index 8de386f5be..8f8d39ca4b 100644 --- a/beacon_node/network/src/sync/batch.rs +++ b/beacon_node/network/src/sync/batch.rs @@ -10,10 +10,22 @@ use std::marker::PhantomData; use std::ops::Sub; use std::time::Duration; use std::time::Instant; -use strum::Display; +use strum::{Display, EnumIter, IntoStaticStr}; use types::Slot; use types::{DataColumnSidecarList, Epoch, EthSpec}; +/// Batch states used as metrics labels. +#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter, IntoStaticStr)] +#[strum(serialize_all = "snake_case")] +pub enum BatchMetricsState { + AwaitingDownload, + Downloading, + AwaitingProcessing, + Processing, + AwaitingValidation, + Failed, +} + pub type BatchId = Epoch; /// Type of expected batch. @@ -142,6 +154,18 @@ impl<D: Hash> BatchState<D> { pub fn poison(&mut self) -> BatchState<D> { std::mem::replace(self, BatchState::Poisoned) } + + /// Returns the metrics state for this batch. + pub fn metrics_state(&self) -> BatchMetricsState { + match self { + BatchState::AwaitingDownload => BatchMetricsState::AwaitingDownload, + BatchState::Downloading(_) => BatchMetricsState::Downloading, + BatchState::AwaitingProcessing(..) => BatchMetricsState::AwaitingProcessing, + BatchState::Processing(_) => BatchMetricsState::Processing, + BatchState::AwaitingValidation(_) => BatchMetricsState::AwaitingValidation, + BatchState::Poisoned | BatchState::Failed => BatchMetricsState::Failed, + } + } } impl<E: EthSpec, B: BatchConfig, D: Hash> BatchInfo<E, B, D> { diff --git a/beacon_node/network/src/sync/custody_backfill_sync/mod.rs b/beacon_node/network/src/sync/custody_backfill_sync/mod.rs index fa8b70c8b4..893aa849d3 100644 --- a/beacon_node/network/src/sync/custody_backfill_sync/mod.rs +++ b/beacon_node/network/src/sync/custody_backfill_sync/mod.rs @@ -12,14 +12,16 @@ use lighthouse_network::{ }; use logging::crit; use std::hash::{DefaultHasher, Hash, Hasher}; +use strum::IntoEnumIterator; use tracing::{debug, error, info, info_span, warn}; use types::{DataColumnSidecarList, Epoch, EthSpec}; +use crate::metrics; use crate::sync::{ backfill_sync::{BACKFILL_EPOCHS_PER_BATCH, ProcessResult, SyncStart}, batch::{ - BatchConfig, BatchId, BatchInfo, BatchOperationOutcome, BatchProcessingResult, BatchState, - ByRangeRequestType, + BatchConfig, BatchId, BatchInfo, BatchMetricsState, BatchOperationOutcome, + BatchProcessingResult, BatchState, ByRangeRequestType, }, block_sidecar_coupling::CouplingError, manager::CustodyBatchProcessResult, @@ -1114,6 +1116,21 @@ impl<T: BeaconChainTypes> CustodyBackFillSync<T> { *self.network_globals.custody_sync_state.write() = state; } + pub fn register_metrics(&self) { + for state in BatchMetricsState::iter() { + let count = self + .batches + .values() + .filter(|b| b.state().metrics_state() == state) + .count(); + metrics::set_gauge_vec( + &metrics::SYNCING_CHAIN_BATCHES, + &["custody_backfill", state.into()], + count as i64, + ); + } + } + /// A fully synced peer has joined us. /// If we are in a failed state, update a local variable to indicate we are able to restart /// the failed sync on the next attempt. diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 096ed9c328..c2faff5b62 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -784,6 +784,9 @@ impl<T: BeaconChainTypes> SyncManager<T> { } _ = register_metrics_interval.tick() => { self.network.register_metrics(); + self.range_sync.register_metrics(); + self.backfill_sync.register_metrics(); + self.custody_backfill_sync.register_metrics(); } _ = epoch_interval.tick() => { self.update_sync_state(); diff --git a/beacon_node/network/src/sync/range_sync/chain.rs b/beacon_node/network/src/sync/range_sync/chain.rs index d67d6468a9..61161ae6f4 100644 --- a/beacon_node/network/src/sync/range_sync/chain.rs +++ b/beacon_node/network/src/sync/range_sync/chain.rs @@ -3,7 +3,8 @@ use crate::metrics; use crate::network_beacon_processor::ChainSegmentProcessId; use crate::sync::batch::BatchId; use crate::sync::batch::{ - BatchConfig, BatchInfo, BatchOperationOutcome, BatchProcessingResult, BatchState, + BatchConfig, BatchInfo, BatchMetricsState, BatchOperationOutcome, BatchProcessingResult, + BatchState, }; use crate::sync::block_sidecar_coupling::CouplingError; use crate::sync::network_context::{RangeRequestId, RpcRequestSendError, RpcResponseError}; @@ -234,6 +235,14 @@ impl<T: BeaconChainTypes> SyncingChain<T> { .sum() } + /// Returns the number of batches in the given metrics state. + pub fn count_batches_in_state(&self, state: BatchMetricsState) -> usize { + self.batches + .values() + .filter(|b| b.state().metrics_state() == state) + .count() + } + /// Removes a peer from the chain. /// If the peer has active batches, those are considered failed and re-requested. pub fn remove_peer(&mut self, peer_id: &PeerId) -> ProcessingResult { diff --git a/beacon_node/network/src/sync/range_sync/chain_collection.rs b/beacon_node/network/src/sync/range_sync/chain_collection.rs index bd4dd6c181..b430b7c572 100644 --- a/beacon_node/network/src/sync/range_sync/chain_collection.rs +++ b/beacon_node/network/src/sync/range_sync/chain_collection.rs @@ -6,6 +6,7 @@ use super::chain::{ChainId, ProcessingResult, RemoveChain, SyncingChain}; use super::sync_type::RangeSyncType; use crate::metrics; +use crate::sync::batch::BatchMetricsState; use crate::sync::network_context::SyncNetworkContext; use beacon_chain::{BeaconChain, BeaconChainTypes}; use fnv::FnvHashMap; @@ -17,6 +18,7 @@ use smallvec::SmallVec; use std::collections::HashMap; use std::collections::hash_map::Entry; use std::sync::Arc; +use strum::IntoEnumIterator; use tracing::{debug, error}; use types::EthSpec; use types::{Epoch, Hash256, Slot}; @@ -516,6 +518,25 @@ impl<T: BeaconChainTypes> ChainCollection<T> { } } + pub fn register_metrics(&self) { + for (sync_type, chains) in [ + ("range_finalized", &self.finalized_chains), + ("range_head", &self.head_chains), + ] { + for state in BatchMetricsState::iter() { + let count: usize = chains + .values() + .map(|chain| chain.count_batches_in_state(state)) + .sum(); + metrics::set_gauge_vec( + &metrics::SYNCING_CHAIN_BATCHES, + &[sync_type, state.into()], + count as i64, + ); + } + } + } + fn update_metrics(&self) { metrics::set_gauge_vec( &metrics::SYNCING_CHAINS_COUNT, diff --git a/beacon_node/network/src/sync/range_sync/range.rs b/beacon_node/network/src/sync/range_sync/range.rs index c9656ad1d0..4c2123451a 100644 --- a/beacon_node/network/src/sync/range_sync/range.rs +++ b/beacon_node/network/src/sync/range_sync/range.rs @@ -371,6 +371,10 @@ where .update(network, &local, &mut self.awaiting_head_peers); } + pub fn register_metrics(&self) { + self.chains.register_metrics(); + } + /// Kickstarts sync. pub fn resume(&mut self, network: &mut SyncNetworkContext<T>) { for (removed_chain, sync_type, remove_reason) in From 2d91009ab4b6452b50371aa759e40a3a7dc9be4a Mon Sep 17 00:00:00 2001 From: Mac L <mjladson@pm.me> Date: Thu, 19 Feb 2026 23:32:42 +0400 Subject: [PATCH 40/81] Bump sqlite deps to remove `hashlink 0.8` (#8866) #8547 Bump the following crates to remove `hashlink 0.8`: - `rusqlite` - `r2d2-sqlite` - `yaml-rust2` Co-Authored-By: Mac L <mjladson@pm.me> --- Cargo.lock | 70 +++++++++++++------ Cargo.toml | 2 +- consensus/int_to_bytes/Cargo.toml | 2 +- .../slashing_protection/Cargo.toml | 2 +- 4 files changed, 50 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5a8e76a8a8..419ba679db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3427,9 +3427,9 @@ dependencies = [ [[package]] name = "fallible-iterator" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" [[package]] name = "fallible-streaming-iterator" @@ -3933,7 +3933,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", - "allocator-api2", ] [[package]] @@ -3958,15 +3957,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "hashlink" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" -dependencies = [ - "hashbrown 0.14.5", -] - [[package]] name = "hashlink" version = "0.9.1" @@ -3985,6 +3975,15 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "hashlink" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" +dependencies = [ + "hashbrown 0.16.1", +] + [[package]] name = "hdrhistogram" version = "7.5.4" @@ -5323,9 +5322,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.25.2" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29f835d03d717946d28b1d1ed632eb6f0e24a299388ee623d0c23118d3e8a7fa" +checksum = "95b4103cffefa72eb8428cb6b47d6627161e51c2739fc5e3b734584157bc642a" dependencies = [ "cc", "pkg-config", @@ -7163,12 +7162,13 @@ dependencies = [ [[package]] name = "r2d2_sqlite" -version = "0.21.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4f5d0337e99cd5cacd91ffc326c6cc9d8078def459df560c4f9bf9ba4a51034" +checksum = "a2ebd03c29250cdf191da93a35118b4567c2ef0eacab54f65e058d6f4c9965f6" dependencies = [ "r2d2", "rusqlite", + "uuid 1.19.0", ] [[package]] @@ -7503,6 +7503,16 @@ dependencies = [ "archery", ] +[[package]] +name = "rsqlite-vfs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d" +dependencies = [ + "hashbrown 0.16.1", + "thiserror 2.0.17", +] + [[package]] name = "rtnetlink" version = "0.13.1" @@ -7558,16 +7568,17 @@ checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" [[package]] name = "rusqlite" -version = "0.28.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01e213bc3ecb39ac32e81e51ebe31fd888a940515173e3a18a35f8c6e896422a" +checksum = "f1c93dd1c9683b438c392c492109cb702b8090b2bfc8fed6f6e4eb4523f17af3" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.10.0", "fallible-iterator", "fallible-streaming-iterator", - "hashlink 0.8.4", + "hashlink 0.11.0", "libsqlite3-sys", "smallvec", + "sqlite-wasm-rs", ] [[package]] @@ -8374,6 +8385,18 @@ dependencies = [ "der", ] +[[package]] +name = "sqlite-wasm-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4206ed3a67690b9c29b77d728f6acc3ce78f16bf846d83c94f76400320181b" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + [[package]] name = "ssz_types" version = "0.14.0" @@ -9514,6 +9537,7 @@ checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "getrandom 0.3.4", "js-sys", + "rand 0.9.2", "wasm-bindgen", ] @@ -10479,13 +10503,13 @@ dependencies = [ [[package]] name = "yaml-rust2" -version = "0.8.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8" +checksum = "631a50d867fafb7093e709d75aaee9e0e0d5deb934021fcea25ac2fe09edc51e" dependencies = [ "arraydeque", "encoding_rs", - "hashlink 0.8.4", + "hashlink 0.11.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 98e8c057b5..44f3a60b2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -227,7 +227,7 @@ reqwest = { version = "0.12", default-features = false, features = [ ] } ring = "0.17" rpds = "0.11" -rusqlite = { version = "0.28", features = ["bundled"] } +rusqlite = { version = "0.38", features = ["bundled"] } rust_eth_kzg = "0.9" safe_arith = "0.1" sensitive_url = { version = "0.1", features = ["serde"] } diff --git a/consensus/int_to_bytes/Cargo.toml b/consensus/int_to_bytes/Cargo.toml index c639dfce8d..75196d7437 100644 --- a/consensus/int_to_bytes/Cargo.toml +++ b/consensus/int_to_bytes/Cargo.toml @@ -9,4 +9,4 @@ bytes = { workspace = true } [dev-dependencies] hex = { workspace = true } -yaml-rust2 = "0.8" +yaml-rust2 = "0.11" diff --git a/validator_client/slashing_protection/Cargo.toml b/validator_client/slashing_protection/Cargo.toml index 86df6d01fe..45244c2e62 100644 --- a/validator_client/slashing_protection/Cargo.toml +++ b/validator_client/slashing_protection/Cargo.toml @@ -17,7 +17,7 @@ ethereum_serde_utils = { workspace = true } filesystem = { workspace = true } fixed_bytes = { workspace = true } r2d2 = { workspace = true } -r2d2_sqlite = "0.21.0" +r2d2_sqlite = "0.32" rusqlite = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } From 9cb72100d4c6f008ee5f2ac7274bd7f12128b4eb Mon Sep 17 00:00:00 2001 From: Mac L <mjladson@pm.me> Date: Thu, 19 Feb 2026 23:32:46 +0400 Subject: [PATCH 41/81] Feature-gate all uses of `arbitrary` (#8867) Feature gate all uses of `arbitrary` so it is not compiled during release builds. Co-Authored-By: Mac L <mjladson@pm.me> --- Cargo.toml | 2 +- consensus/state_processing/Cargo.toml | 4 +++- consensus/state_processing/src/verify_operation.rs | 12 +++++++++--- consensus/types/Cargo.toml | 1 + crypto/bls/Cargo.toml | 4 ++-- crypto/kzg/Cargo.toml | 3 ++- crypto/kzg/src/kzg_commitment.rs | 1 + crypto/kzg/src/kzg_proof.rs | 1 + validator_client/slashing_protection/Cargo.toml | 4 ++-- 9 files changed, 22 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 44f3a60b2f..3b5a7dd6ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -240,7 +240,7 @@ signing_method = { path = "validator_client/signing_method" } slasher = { path = "slasher", default-features = false } slashing_protection = { path = "validator_client/slashing_protection" } slot_clock = { path = "common/slot_clock" } -smallvec = { version = "1.11.2", features = ["arbitrary"] } +smallvec = "1" snap = "1" ssz_types = { version = "0.14.0", features = ["context_deserialize", "runtime_types"] } state_processing = { path = "consensus/state_processing" } diff --git a/consensus/state_processing/Cargo.toml b/consensus/state_processing/Cargo.toml index a83e443e80..7426995439 100644 --- a/consensus/state_processing/Cargo.toml +++ b/consensus/state_processing/Cargo.toml @@ -8,6 +8,8 @@ edition = { workspace = true } default = [] fake_crypto = ["bls/fake_crypto"] arbitrary-fuzz = [ + "dep:arbitrary", + "smallvec/arbitrary", "types/arbitrary-fuzz", "merkle_proof/arbitrary", "ethereum_ssz/arbitrary", @@ -17,7 +19,7 @@ arbitrary-fuzz = [ portable = ["bls/supranational-portable"] [dependencies] -arbitrary = { workspace = true } +arbitrary = { workspace = true, optional = true } bls = { workspace = true } educe = { workspace = true } ethereum_hashing = { workspace = true } diff --git a/consensus/state_processing/src/verify_operation.rs b/consensus/state_processing/src/verify_operation.rs index 1f76f19586..a13786f9f6 100644 --- a/consensus/state_processing/src/verify_operation.rs +++ b/consensus/state_processing/src/verify_operation.rs @@ -7,6 +7,7 @@ use crate::per_block_processing::{ verify_attester_slashing, verify_bls_to_execution_change, verify_exit, verify_proposer_slashing, }; +#[cfg(feature = "arbitrary-fuzz")] use arbitrary::Arbitrary; use educe::Educe; use smallvec::{SmallVec, smallvec}; @@ -39,13 +40,17 @@ pub trait TransformPersist { /// /// The inner `op` field is private, meaning instances of this type can only be constructed /// by calling `validate`. -#[derive(Educe, Debug, Clone, Arbitrary)] +#[derive(Educe, Debug, Clone)] +#[cfg_attr(feature = "arbitrary-fuzz", derive(Arbitrary))] #[educe( PartialEq, Eq, Hash(bound(T: TransformPersist + std::hash::Hash, E: EthSpec)) )] -#[arbitrary(bound = "T: TransformPersist + Arbitrary<'arbitrary>, E: EthSpec")] +#[cfg_attr( + feature = "arbitrary-fuzz", + arbitrary(bound = "T: TransformPersist + Arbitrary<'arbitrary>, E: EthSpec") +)] pub struct SigVerifiedOp<T: TransformPersist, E: EthSpec> { op: T, verified_against: VerifiedAgainst, @@ -133,7 +138,8 @@ struct SigVerifiedOpDecode<P: Decode> { /// /// We need to store multiple `ForkVersion`s because attester slashings contain two indexed /// attestations which may be signed using different versions. -#[derive(Debug, PartialEq, Eq, Clone, Hash, Encode, Decode, TestRandom, Arbitrary)] +#[derive(Debug, PartialEq, Eq, Clone, Hash, Encode, Decode, TestRandom)] +#[cfg_attr(feature = "arbitrary-fuzz", derive(Arbitrary))] pub struct VerifiedAgainst { fork_versions: SmallVec<[ForkVersion; MAX_FORKS_VERIFIED_AGAINST]>, } diff --git a/consensus/types/Cargo.toml b/consensus/types/Cargo.toml index a4b879ddb2..e7e382714b 100644 --- a/consensus/types/Cargo.toml +++ b/consensus/types/Cargo.toml @@ -16,6 +16,7 @@ sqlite = ["dep:rusqlite"] arbitrary = [ "dep:arbitrary", "bls/arbitrary", + "kzg/arbitrary", "ethereum_ssz/arbitrary", "milhouse/arbitrary", "ssz_types/arbitrary", diff --git a/crypto/bls/Cargo.toml b/crypto/bls/Cargo.toml index 4661288679..ac04e1fecf 100644 --- a/crypto/bls/Cargo.toml +++ b/crypto/bls/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Paul Hauner <paul@paulhauner.com>"] edition = { workspace = true } [features] -arbitrary = [] +arbitrary = ["dep:arbitrary"] default = ["supranational"] fake_crypto = [] supranational = ["blst"] @@ -14,7 +14,7 @@ supranational-force-adx = ["supranational", "blst/force-adx"] [dependencies] alloy-primitives = { workspace = true } -arbitrary = { workspace = true } +arbitrary = { workspace = true, optional = true } blst = { version = "0.3.3", optional = true } ethereum_hashing = { workspace = true } ethereum_serde_utils = { workspace = true } diff --git a/crypto/kzg/Cargo.toml b/crypto/kzg/Cargo.toml index d2558663d5..840f8cfc9c 100644 --- a/crypto/kzg/Cargo.toml +++ b/crypto/kzg/Cargo.toml @@ -7,10 +7,11 @@ edition = "2021" [features] default = [] +arbitrary = ["dep:arbitrary"] fake_crypto = [] [dependencies] -arbitrary = { workspace = true } +arbitrary = { workspace = true, optional = true } c-kzg = { workspace = true } educe = { workspace = true } ethereum_hashing = { workspace = true } diff --git a/crypto/kzg/src/kzg_commitment.rs b/crypto/kzg/src/kzg_commitment.rs index 5a5e689429..bc5fc5f5aa 100644 --- a/crypto/kzg/src/kzg_commitment.rs +++ b/crypto/kzg/src/kzg_commitment.rs @@ -114,6 +114,7 @@ impl Debug for KzgCommitment { } } +#[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for KzgCommitment { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> { let mut bytes = [0u8; BYTES_PER_COMMITMENT]; diff --git a/crypto/kzg/src/kzg_proof.rs b/crypto/kzg/src/kzg_proof.rs index 5a83466d0c..aa9ed185a0 100644 --- a/crypto/kzg/src/kzg_proof.rs +++ b/crypto/kzg/src/kzg_proof.rs @@ -110,6 +110,7 @@ impl Debug for KzgProof { } } +#[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for KzgProof { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> { let mut bytes = [0u8; BYTES_PER_PROOF]; diff --git a/validator_client/slashing_protection/Cargo.toml b/validator_client/slashing_protection/Cargo.toml index 45244c2e62..695a693385 100644 --- a/validator_client/slashing_protection/Cargo.toml +++ b/validator_client/slashing_protection/Cargo.toml @@ -6,11 +6,11 @@ edition = { workspace = true } autotests = false [features] -arbitrary-fuzz = ["types/arbitrary-fuzz", "eip_3076/arbitrary-fuzz"] +arbitrary-fuzz = ["dep:arbitrary", "types/arbitrary-fuzz", "eip_3076/arbitrary-fuzz"] portable = ["types/portable"] [dependencies] -arbitrary = { workspace = true, features = ["derive"] } +arbitrary = { workspace = true, features = ["derive"], optional = true } bls = { workspace = true } eip_3076 = { workspace = true, features = ["json"] } ethereum_serde_utils = { workspace = true } From 8d4af658bd5f33be3ac1c3d41443938c3808ddef Mon Sep 17 00:00:00 2001 From: Jimmy Chen <jchen.tc@gmail.com> Date: Fri, 20 Feb 2026 15:27:33 +1100 Subject: [PATCH 42/81] Remove unreachable void pattern for ConnectionLimits (#8871) Co-Authored-By: Jimmy Chen <jchen.tc@gmail.com> --- beacon_node/lighthouse_network/src/service/mod.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index 3d709ed9b5..94e0ad0710 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -1861,8 +1861,6 @@ impl<E: EthSpec> Network<E> { self.inject_upnp_event(e); None } - #[allow(unreachable_patterns)] - BehaviourEvent::ConnectionLimits(le) => libp2p::core::util::unreachable(le), }, SwarmEvent::ConnectionEstablished { .. } => None, SwarmEvent::ConnectionClosed { .. } => None, From 48071b7ae722ac915c678fe518110aee988e6d74 Mon Sep 17 00:00:00 2001 From: Jimmy Chen <jchen.tc@gmail.com> Date: Sat, 21 Feb 2026 01:22:13 +1100 Subject: [PATCH 43/81] Add --jwt-secret-path to lcli mock-el (#8864) Co-Authored-By: Jimmy Chen <jchen.tc@gmail.com> --- lcli/src/main.rs | 12 +++++++++++- lcli/src/mock_el.rs | 24 ++++++++++++++++++++---- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/lcli/src/main.rs b/lcli/src/main.rs index a21dfd4386..63dd0f2c5b 100644 --- a/lcli/src/main.rs +++ b/lcli/src/main.rs @@ -492,10 +492,20 @@ fn main() { .long("jwt-output-path") .value_name("PATH") .action(ArgAction::Set) - .required(true) + .required_unless_present("jwt-secret-path") + .conflicts_with("jwt-secret-path") .help("Path to write the JWT secret.") .display_order(0) ) + .arg( + Arg::new("jwt-secret-path") + .long("jwt-secret-path") + .value_name("PATH") + .action(ArgAction::Set) + .help("Path to an existing hex-encoded JWT secret file. \ + When provided, this secret is used instead of the default.") + .display_order(0) + ) .arg( Arg::new("listen-address") .long("listen-address") diff --git a/lcli/src/mock_el.rs b/lcli/src/mock_el.rs index d6bdfb0d71..544010b6a2 100644 --- a/lcli/src/mock_el.rs +++ b/lcli/src/mock_el.rs @@ -2,7 +2,7 @@ use clap::ArgMatches; use clap_utils::{parse_optional, parse_required}; use environment::Environment; use execution_layer::{ - auth::JwtKey, + auth::{JwtKey, strip_prefix}, test_utils::{ Config, DEFAULT_JWT_SECRET, DEFAULT_TERMINAL_BLOCK, MockExecutionConfig, MockServer, }, @@ -13,7 +13,8 @@ use std::sync::Arc; use types::*; pub fn run<E: EthSpec>(mut env: Environment<E>, matches: &ArgMatches) -> Result<(), String> { - let jwt_path: PathBuf = parse_required(matches, "jwt-output-path")?; + let jwt_output_path: Option<PathBuf> = parse_optional(matches, "jwt-output-path")?; + let jwt_secret_path: Option<PathBuf> = parse_optional(matches, "jwt-secret-path")?; let listen_addr: Ipv4Addr = parse_required(matches, "listen-address")?; let listen_port: u16 = parse_required(matches, "listen-port")?; let all_payloads_valid: bool = parse_required(matches, "all-payloads-valid")?; @@ -25,8 +26,23 @@ pub fn run<E: EthSpec>(mut env: Environment<E>, matches: &ArgMatches) -> Result< let handle = env.core_context().executor.handle().unwrap(); let spec = Arc::new(E::default_spec()); - let jwt_key = JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap(); - std::fs::write(jwt_path, hex::encode(DEFAULT_JWT_SECRET)).unwrap(); + + let jwt_key = if let Some(secret_path) = jwt_secret_path { + let hex_str = std::fs::read_to_string(&secret_path) + .map_err(|e| format!("Failed to read JWT secret file: {}", e))?; + let secret_bytes = hex::decode(strip_prefix(hex_str.trim())) + .map_err(|e| format!("Invalid hex in JWT secret file: {}", e))?; + JwtKey::from_slice(&secret_bytes) + .map_err(|e| format!("Invalid JWT secret length (expected 32 bytes): {}", e))? + } else if let Some(jwt_path) = jwt_output_path { + let jwt_key = JwtKey::from_slice(&DEFAULT_JWT_SECRET) + .map_err(|e| format!("Default JWT secret invalid: {}", e))?; + std::fs::write(jwt_path, hex::encode(jwt_key.as_bytes())) + .map_err(|e| format!("Failed to write JWT secret to output path: {}", e))?; + jwt_key + } else { + return Err("either --jwt-secret-path or --jwt-output-path must be provided".to_string()); + }; let config = MockExecutionConfig { server_config: Config { From 9452d5186729aab2d24460d4a293606f875409f6 Mon Sep 17 00:00:00 2001 From: Mac L <mjladson@pm.me> Date: Sun, 22 Feb 2026 02:03:59 +0400 Subject: [PATCH 44/81] Bump `uuid` to remove duplicate (#8874) #8547 Bump the version of `uuid` in our Cargo.toml to version `1` which removes `uuid 0.8` and unifies it across the workspace to version `1.19.0`. Co-Authored-By: Mac L <mjladson@pm.me> --- Cargo.lock | 19 +++++-------------- Cargo.toml | 2 +- deny.toml | 1 + 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 419ba679db..eccdc8b29c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3199,7 +3199,7 @@ dependencies = [ "sha2", "tempfile", "unicode-normalization", - "uuid 0.8.2", + "uuid", "zeroize", ] @@ -3239,7 +3239,7 @@ dependencies = [ "serde_repr", "tempfile", "tiny-bip39", - "uuid 0.8.2", + "uuid", ] [[package]] @@ -5901,7 +5901,7 @@ dependencies = [ "rustc_version 0.4.1", "smallvec", "tagptr", - "uuid 1.19.0", + "uuid", ] [[package]] @@ -7168,7 +7168,7 @@ checksum = "a2ebd03c29250cdf191da93a35118b4567c2ef0eacab54f65e058d6f4c9965f6" dependencies = [ "r2d2", "rusqlite", - "uuid 1.19.0", + "uuid", ] [[package]] @@ -9519,16 +9519,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "uuid" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" -dependencies = [ - "getrandom 0.2.16", - "serde", -] - [[package]] name = "uuid" version = "1.19.0" @@ -9538,6 +9528,7 @@ dependencies = [ "getrandom 0.3.4", "js-sys", "rand 0.9.2", + "serde_core", "wasm-bindgen", ] diff --git a/Cargo.toml b/Cargo.toml index 3b5a7dd6ba..f735b97540 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -273,7 +273,7 @@ tree_hash_derive = "0.12.0" typenum = "1" types = { path = "consensus/types", features = ["saturating-arith"] } url = "2" -uuid = { version = "0.8", features = ["serde", "v4"] } +uuid = { version = "1", features = ["serde", "v4"] } validator_client = { path = "validator_client" } validator_dir = { path = "common/validator_dir" } validator_http_api = { path = "validator_client/http_api" } diff --git a/deny.toml b/deny.toml index e6c30f6a48..3b230155f7 100644 --- a/deny.toml +++ b/deny.toml @@ -18,6 +18,7 @@ deny = [ { crate = "pbkdf2", deny-multiple-versions = true, reason = "takes a long time to compile" }, { crate = "scrypt", deny-multiple-versions = true, reason = "takes a long time to compile" }, { crate = "syn", deny-multiple-versions = true, reason = "takes a long time to compile" }, + { crate = "uuid", deny-multiple-versions = true, reason = "dependency hygiene" }, ] [sources] From 2b214175d5001b3022321cb0bfcacb13a4ab0d0d Mon Sep 17 00:00:00 2001 From: 0xMushow <105550256+0xMushow@users.noreply.github.com> Date: Mon, 23 Feb 2026 06:02:56 +0400 Subject: [PATCH 45/81] Enforce stricter checks on certain constants (#8500) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Which issue # does this PR address? None All of these are performing a check, and adding a batch, or creating a new lookup, or a new query, etc.. Hence all of these limits would be off by one. Example: ```rust // BACKFILL_BATCH_BUFFER_SIZE = 5 if self.batches.iter().filter(...).count() >= BACKFILL_BATCH_BUFFER_SIZE { return None; // ← REJECT } // ... later adds batch via Entry::Vacant(entry).insert(...) ``` Without the `>` being changed to a `>=` , we would allow 6. The same idea applies to all changes proposed. Co-Authored-By: Antoine James <antoine@ethereum.org> Co-Authored-By: Jimmy Chen <jimmy@sigmaprime.io> Co-Authored-By: Jimmy Chen <jchen.tc@gmail.com> --- beacon_node/lighthouse_network/src/discovery/mod.rs | 2 +- beacon_node/network/src/sync/backfill_sync/mod.rs | 2 +- beacon_node/network/src/sync/block_lookups/mod.rs | 2 +- beacon_node/network/src/sync/custody_backfill_sync/mod.rs | 2 +- beacon_node/network/src/sync/network_context/custody.rs | 2 +- beacon_node/network/src/sync/range_sync/chain.rs | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/beacon_node/lighthouse_network/src/discovery/mod.rs b/beacon_node/lighthouse_network/src/discovery/mod.rs index 38a6a84b44..21b1146aff 100644 --- a/beacon_node/lighthouse_network/src/discovery/mod.rs +++ b/beacon_node/lighthouse_network/src/discovery/mod.rs @@ -674,7 +674,7 @@ impl<E: EthSpec> Discovery<E> { /// updates the min_ttl field. fn add_subnet_query(&mut self, subnet: Subnet, min_ttl: Option<Instant>, retries: usize) { // remove the entry and complete the query if greater than the maximum search count - if retries > MAX_DISCOVERY_RETRY { + if retries >= MAX_DISCOVERY_RETRY { debug!("Subnet peer discovery did not find sufficient peers. Reached max retry limit"); return; } diff --git a/beacon_node/network/src/sync/backfill_sync/mod.rs b/beacon_node/network/src/sync/backfill_sync/mod.rs index 9802ec56a1..7ef72c7f3a 100644 --- a/beacon_node/network/src/sync/backfill_sync/mod.rs +++ b/beacon_node/network/src/sync/backfill_sync/mod.rs @@ -1071,7 +1071,7 @@ impl<T: BeaconChainTypes> BackFillSync<T> { .iter() .filter(|&(_epoch, batch)| in_buffer(batch)) .count() - > BACKFILL_BATCH_BUFFER_SIZE as usize + >= BACKFILL_BATCH_BUFFER_SIZE as usize { return None; } diff --git a/beacon_node/network/src/sync/block_lookups/mod.rs b/beacon_node/network/src/sync/block_lookups/mod.rs index cbf65505ef..394f2fc37d 100644 --- a/beacon_node/network/src/sync/block_lookups/mod.rs +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -398,7 +398,7 @@ impl<T: BeaconChainTypes> BlockLookups<T> { // Lookups contain untrusted data, bound the total count of lookups hold in memory to reduce // the risk of OOM in case of bugs of malicious activity. - if self.single_block_lookups.len() > MAX_LOOKUPS { + if self.single_block_lookups.len() >= MAX_LOOKUPS { warn!(?block_root, "Dropping lookup reached max"); return false; } diff --git a/beacon_node/network/src/sync/custody_backfill_sync/mod.rs b/beacon_node/network/src/sync/custody_backfill_sync/mod.rs index fa8b70c8b4..a964ad9a3c 100644 --- a/beacon_node/network/src/sync/custody_backfill_sync/mod.rs +++ b/beacon_node/network/src/sync/custody_backfill_sync/mod.rs @@ -422,7 +422,7 @@ impl<T: BeaconChainTypes> CustodyBackFillSync<T> { .iter() .filter(|&(_epoch, batch)| in_buffer(batch)) .count() - > BACKFILL_BATCH_BUFFER_SIZE as usize + >= BACKFILL_BATCH_BUFFER_SIZE as usize { return None; } diff --git a/beacon_node/network/src/sync/network_context/custody.rs b/beacon_node/network/src/sync/network_context/custody.rs index de5d9b6e0b..ae0eee9964 100644 --- a/beacon_node/network/src/sync/network_context/custody.rs +++ b/beacon_node/network/src/sync/network_context/custody.rs @@ -239,7 +239,7 @@ impl<T: BeaconChainTypes> ActiveCustodyRequest<T> { if let Some(wait_duration) = request.is_awaiting_download() { // Note: an empty response is considered a successful response, so we may end up // retrying many more times than `MAX_CUSTODY_COLUMN_DOWNLOAD_ATTEMPTS`. - if request.download_failures > MAX_CUSTODY_COLUMN_DOWNLOAD_ATTEMPTS { + if request.download_failures >= MAX_CUSTODY_COLUMN_DOWNLOAD_ATTEMPTS { return Err(Error::TooManyFailures); } diff --git a/beacon_node/network/src/sync/range_sync/chain.rs b/beacon_node/network/src/sync/range_sync/chain.rs index d67d6468a9..25ea1af76a 100644 --- a/beacon_node/network/src/sync/range_sync/chain.rs +++ b/beacon_node/network/src/sync/range_sync/chain.rs @@ -1277,7 +1277,7 @@ impl<T: BeaconChainTypes> SyncingChain<T> { .iter() .filter(|&(_epoch, batch)| in_buffer(batch)) .count() - > BATCH_BUFFER_SIZE as usize + >= BATCH_BUFFER_SIZE as usize { return None; } From dcc43e3d20f44146963aa880fd46cda9e53bda04 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi <eserilev@gmail.com> Date: Sun, 22 Feb 2026 22:17:24 -0800 Subject: [PATCH 46/81] Implement gloas block gossip verification changes (#8878) Co-Authored-By: Eitan Seri-Levi <eserilev@ucsc.edu> Co-Authored-By: Eitan Seri- Levi <eserilev@gmail.com> Co-Authored-By: Michael Sproul <michaelsproul@users.noreply.github.com> --- beacon_node/beacon_chain/src/beacon_chain.rs | 18 ++-- .../beacon_chain/src/block_verification.rs | 84 +++++++++++++++---- .../beacon_chain/src/execution_payload.rs | 11 ++- .../gossip_methods.rs | 33 ++++---- consensus/types/src/block/beacon_block.rs | 20 +++++ 5 files changed, 132 insertions(+), 34 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 9f62bf11f5..26ad2e714b 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -3378,11 +3378,19 @@ impl<T: BeaconChainTypes> BeaconChain<T> { ); } - self.data_availability_checker.put_pre_execution_block( - block_root, - unverified_block.block_cloned(), - block_source, - )?; + // Gloas blocks dont need to be inserted into the DA cache + // they are always available. + if !unverified_block + .block() + .fork_name_unchecked() + .gloas_enabled() + { + self.data_availability_checker.put_pre_execution_block( + block_root, + unverified_block.block_cloned(), + block_source, + )?; + } // Start the Prometheus timer. let _full_timer = metrics::start_timer(&metrics::BLOCK_PROCESSING_TIMES); diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index e0943d5d93..292560d6a7 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -51,7 +51,9 @@ use crate::beacon_snapshot::PreProcessingSnapshot; use crate::blob_verification::GossipBlobError; use crate::block_verification_types::{AsBlock, BlockImportData, RpcBlock}; -use crate::data_availability_checker::{AvailabilityCheckError, MaybeAvailableBlock}; +use crate::data_availability_checker::{ + AvailabilityCheckError, AvailableBlock, AvailableBlockData, MaybeAvailableBlock, +}; use crate::data_column_verification::GossipDataColumnError; use crate::execution_payload::{ AllowOptimisticImport, NotifyExecutionLayer, PayloadNotifier, @@ -334,6 +336,15 @@ pub enum BlockError { max_blobs_at_epoch: usize, block: usize, }, + /// The bid's parent_block_root does not match the block's parent_root. + /// + /// ## Peer scoring + /// + /// The block is invalid and the peer should be penalized. + BidParentRootMismatch { + bid_parent_root: Hash256, + block_parent_root: Hash256, + }, } /// Which specific signature(s) are invalid in a SignedBeaconBlock @@ -887,15 +898,15 @@ impl<T: BeaconChainTypes> GossipVerifiedBlock<T> { // Do not gossip blocks that claim to contain more blobs than the max allowed // at the given block epoch. - if let Ok(commitments) = block.message().body().blob_kzg_commitments() { + if let Some(blob_kzg_commitments_len) = block.message().blob_kzg_commitments_len() { let max_blobs_at_epoch = chain .spec .max_blobs_per_block(block.slot().epoch(T::EthSpec::slots_per_epoch())) as usize; - if commitments.len() > max_blobs_at_epoch { + if blob_kzg_commitments_len > max_blobs_at_epoch { return Err(BlockError::InvalidBlobCount { max_blobs_at_epoch, - block: commitments.len(), + block: blob_kzg_commitments_len, }); } } @@ -932,6 +943,24 @@ impl<T: BeaconChainTypes> GossipVerifiedBlock<T> { let block_epoch = block.slot().epoch(T::EthSpec::slots_per_epoch()); let (parent_block, block) = verify_parent_block_is_known::<T>(&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() + && bid.message.parent_block_root != block.message().parent_root() + { + return Err(BlockError::BidParentRootMismatch { + bid_parent_root: bid.message.parent_block_root, + block_parent_root: block.message().parent_root(), + }); + } + + // 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. @@ -1038,8 +1067,15 @@ impl<T: BeaconChainTypes> GossipVerifiedBlock<T> { }); } - // Validate the block's execution_payload (if any). - validate_execution_payload_for_gossip(&parent_block, block.message(), chain)?; + // [New in Gloas]: Skip payload validation checks. The payload now arrives separately + // via `ExecutionPayloadEnvelope`. + if !chain + .spec + .fork_name_at_slot::<T::EthSpec>(block.slot()) + .gloas_enabled() + { + validate_execution_payload_for_gossip(&parent_block, block.message(), chain)?; + } // Beacon API block_gossip events if let Some(event_handler) = chain.event_handler.as_ref() @@ -1211,15 +1247,35 @@ impl<T: BeaconChainTypes> SignatureVerifiedBlock<T> { let result = info_span!("signature_verify").in_scope(|| signature_verifier.verify()); match result { - Ok(_) => Ok(Self { - block: MaybeAvailableBlock::AvailabilityPending { + Ok(_) => { + // gloas blocks are always available. + let maybe_available = if chain + .spec + .fork_name_at_slot::<T::EthSpec>(block.slot()) + .gloas_enabled() + { + MaybeAvailableBlock::Available( + AvailableBlock::new( + block, + AvailableBlockData::NoData, + &chain.data_availability_checker, + chain.spec.clone(), + ) + .map_err(BlockError::AvailabilityCheck)?, + ) + } else { + MaybeAvailableBlock::AvailabilityPending { + block_root: from.block_root, + block, + } + }; + Ok(Self { + block: maybe_available, block_root: from.block_root, - block, - }, - block_root: from.block_root, - parent: Some(parent), - consensus_context, - }), + parent: Some(parent), + consensus_context, + }) + } Err(_) => Err(BlockError::InvalidSignature( InvalidSignature::BlockBodySignatures, )), diff --git a/beacon_node/beacon_chain/src/execution_payload.rs b/beacon_node/beacon_chain/src/execution_payload.rs index bdf3ab9594..f32a3ba2a3 100644 --- a/beacon_node/beacon_chain/src/execution_payload.rs +++ b/beacon_node/beacon_chain/src/execution_payload.rs @@ -62,7 +62,10 @@ impl<T: BeaconChainTypes> PayloadNotifier<T> { state: &BeaconState<T::EthSpec>, notify_execution_layer: NotifyExecutionLayer, ) -> Result<Self, BlockError> { - let payload_verification_status = if is_execution_enabled(state, block.message().body()) { + let payload_verification_status = if block.fork_name_unchecked().gloas_enabled() { + // Gloas blocks don't contain an execution payload. + Some(PayloadVerificationStatus::Irrelevant) + } else if is_execution_enabled(state, block.message().body()) { // Perform the initial stages of payload verification. // // We will duplicate these checks again during `per_block_processing`, however these @@ -294,6 +297,12 @@ pub fn validate_execution_payload_for_gossip<T: BeaconChainTypes>( block: BeaconBlockRef<'_, T::EthSpec>, chain: &BeaconChain<T>, ) -> Result<(), BlockError> { + // Gloas blocks don't have an execution payload in the block body. + // Bid-related validations are handled in gossip block verification. + if block.fork_name_unchecked().gloas_enabled() { + return Ok(()); + } + // Only apply this validation if this is a Bellatrix beacon block. if let Ok(execution_payload) = block.body().execution_payload() { // This logic should match `is_execution_enabled`. We use only the execution block hash of 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 a9198f1943..e90018c851 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -1356,7 +1356,8 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> { | Err(e @ BlockError::ParentExecutionPayloadInvalid { .. }) | Err(e @ BlockError::KnownInvalidExecutionPayload(_)) | Err(e @ BlockError::GenesisBlock) - | Err(e @ BlockError::InvalidBlobCount { .. }) => { + | Err(e @ BlockError::InvalidBlobCount { .. }) + | Err(e @ BlockError::BidParentRootMismatch { .. }) => { warn!(error = %e, "Could not verify block for gossip. Rejecting the block"); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject); self.gossip_penalize_peer( @@ -1490,19 +1491,23 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> { // Block is gossip valid. Attempt to fetch blobs from the EL using versioned hashes derived // from kzg commitments, without having to wait for all blobs to be sent from the peers. - let publish_blobs = true; - let self_clone = self.clone(); - let block_clone = block.clone(); - let current_span = Span::current(); - self.executor.spawn( - async move { - self_clone - .fetch_engine_blobs_and_publish(block_clone, block_root, publish_blobs) - .await - } - .instrument(current_span), - "fetch_blobs_gossip", - ); + // TODO(gloas) we'll want to use this same optimization, but we need to refactor the + // `fetch_and_process_engine_blobs` flow to support gloas. + if !block.fork_name_unchecked().gloas_enabled() { + let publish_blobs = true; + let self_clone = self.clone(); + let block_clone = block.clone(); + let current_span = Span::current(); + self.executor.spawn( + async move { + self_clone + .fetch_engine_blobs_and_publish(block_clone, block_root, publish_blobs) + .await + } + .instrument(current_span), + "fetch_blobs_gossip", + ); + } let result = self .chain diff --git a/consensus/types/src/block/beacon_block.rs b/consensus/types/src/block/beacon_block.rs index bee3cdb274..5634d842b6 100644 --- a/consensus/types/src/block/beacon_block.rs +++ b/consensus/types/src/block/beacon_block.rs @@ -309,6 +309,26 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload<E>> BeaconBlockRef<'a, E, Payl pub fn execution_payload(&self) -> Result<Payload::Ref<'a>, BeaconStateError> { self.body().execution_payload() } + + pub fn blob_kzg_commitments_len(&self) -> Option<usize> { + match self { + BeaconBlockRef::Base(_) => None, + BeaconBlockRef::Altair(_) => None, + BeaconBlockRef::Bellatrix(_) => None, + BeaconBlockRef::Capella(_) => None, + BeaconBlockRef::Deneb(block) => Some(block.body.blob_kzg_commitments.len()), + BeaconBlockRef::Electra(block) => Some(block.body.blob_kzg_commitments.len()), + BeaconBlockRef::Fulu(block) => Some(block.body.blob_kzg_commitments.len()), + BeaconBlockRef::Gloas(block) => Some( + block + .body + .signed_execution_payload_bid + .message + .blob_kzg_commitments + .len(), + ), + } + } } impl<'a, E: EthSpec, Payload: AbstractExecPayload<E>> BeaconBlockRefMut<'a, E, Payload> { From 341682e7196a598d2e767e655d37ce370d27a350 Mon Sep 17 00:00:00 2001 From: Jimmy Chen <jchen.tc@gmail.com> Date: Tue, 24 Feb 2026 11:15:39 +1100 Subject: [PATCH 47/81] Add unit tests for BatchInfo and fix doc comments (#8873) Co-Authored-By: Jimmy Chen <jchen.tc@gmail.com> --- beacon_node/network/src/sync/batch.rs | 203 +++++++++++++++++- .../network/src/sync/range_sync/mod.rs | 2 + 2 files changed, 202 insertions(+), 3 deletions(-) diff --git a/beacon_node/network/src/sync/batch.rs b/beacon_node/network/src/sync/batch.rs index 8de386f5be..f9a1fcce39 100644 --- a/beacon_node/network/src/sync/batch.rs +++ b/beacon_node/network/src/sync/batch.rs @@ -213,6 +213,9 @@ impl<E: EthSpec, B: BatchConfig, D: Hash> BatchInfo<E, B, D> { /// After different operations over a batch, this could be in a state that allows it to /// continue, or in failed state. When the batch has failed, we check if it did mainly due to /// processing failures. In this case the batch is considered failed and faulty. + /// + /// When failure counts are equal, `blacklist` is `false` — we assume network issues over + /// peer fault when the evidence is ambiguous. pub fn outcome(&self) -> BatchOperationOutcome { match self.state { BatchState::Poisoned => unreachable!("Poisoned batch"), @@ -255,8 +258,10 @@ impl<E: EthSpec, B: BatchConfig, D: Hash> BatchInfo<E, B, D> { /// Mark the batch as failed and return whether we can attempt a re-download. /// /// This can happen if a peer disconnects or some error occurred that was not the peers fault. - /// The `peer` parameter, when set to None, does not increment the failed attempts of - /// this batch and register the peer, rather attempts a re-download. + /// The `peer` parameter, when set to `None`, still counts toward + /// `max_batch_download_attempts` (to prevent infinite retries on persistent failures) + /// but does not register a peer in `failed_peers()`. Use + /// [`Self::downloading_to_awaiting_download`] to retry without counting a failed attempt. #[must_use = "Batch may have failed"] pub fn download_failed( &mut self, @@ -272,7 +277,6 @@ impl<E: EthSpec, B: BatchConfig, D: Hash> BatchInfo<E, B, D> { { BatchState::Failed } else { - // drop the blocks BatchState::AwaitingDownload }; Ok(self.outcome()) @@ -524,3 +528,196 @@ impl<D: Hash> BatchState<D> { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::sync::range_sync::RangeSyncBatchConfig; + use types::MinimalEthSpec; + + type Cfg = RangeSyncBatchConfig<MinimalEthSpec>; + type TestBatch = BatchInfo<MinimalEthSpec, Cfg, Vec<u64>>; + + fn max_dl() -> u8 { + Cfg::max_batch_download_attempts() + } + + fn max_proc() -> u8 { + Cfg::max_batch_processing_attempts() + } + + fn new_batch() -> TestBatch { + BatchInfo::new(&Epoch::new(0), 1, ByRangeRequestType::Blocks) + } + + fn peer() -> PeerId { + PeerId::random() + } + + fn advance_to_processing(batch: &mut TestBatch, req_id: Id, peer_id: PeerId) { + batch.start_downloading(req_id).unwrap(); + batch.download_completed(vec![1, 2, 3], peer_id).unwrap(); + batch.start_processing().unwrap(); + } + + fn advance_to_awaiting_validation(batch: &mut TestBatch, req_id: Id, peer_id: PeerId) { + advance_to_processing(batch, req_id, peer_id); + batch + .processing_completed(BatchProcessingResult::Success) + .unwrap(); + } + + #[test] + fn happy_path_lifecycle() { + let mut batch = new_batch(); + let p = peer(); + + assert!(matches!(batch.state(), BatchState::AwaitingDownload)); + + batch.start_downloading(1).unwrap(); + assert!(matches!(batch.state(), BatchState::Downloading(1))); + + batch.download_completed(vec![10, 20], p).unwrap(); + assert!(matches!(batch.state(), BatchState::AwaitingProcessing(..))); + + let (data, _duration) = batch.start_processing().unwrap(); + assert_eq!(data, vec![10, 20]); + assert!(matches!(batch.state(), BatchState::Processing(..))); + + let outcome = batch + .processing_completed(BatchProcessingResult::Success) + .unwrap(); + assert!(matches!(outcome, BatchOperationOutcome::Continue)); + assert!(matches!(batch.state(), BatchState::AwaitingValidation(..))); + } + + #[test] + fn download_failures_count_toward_limit() { + let mut batch = new_batch(); + + for i in 1..max_dl() as Id { + batch.start_downloading(i).unwrap(); + let outcome = batch.download_failed(Some(peer())).unwrap(); + assert!(matches!(outcome, BatchOperationOutcome::Continue)); + } + + // Next failure hits the limit + batch.start_downloading(max_dl() as Id).unwrap(); + let outcome = batch.download_failed(Some(peer())).unwrap(); + assert!(matches!( + outcome, + BatchOperationOutcome::Failed { blacklist: false } + )); + } + + #[test] + fn download_failed_none_counts_but_does_not_blame_peer() { + let mut batch = new_batch(); + + // None still counts toward the limit (prevents infinite retry on persistent + // network failures), but doesn't register a peer in failed_peers(). + for i in 0..max_dl() as Id { + batch.start_downloading(i).unwrap(); + batch.download_failed(None).unwrap(); + } + assert!(matches!(batch.state(), BatchState::Failed)); + assert!(batch.failed_peers().is_empty()); + } + + #[test] + fn faulty_processing_failures_count_toward_limit() { + let mut batch = new_batch(); + + for i in 1..max_proc() as Id { + advance_to_processing(&mut batch, i, peer()); + let outcome = batch + .processing_completed(BatchProcessingResult::FaultyFailure) + .unwrap(); + assert!(matches!(outcome, BatchOperationOutcome::Continue)); + } + + // Next faulty failure: limit reached + advance_to_processing(&mut batch, max_proc() as Id, peer()); + let outcome = batch + .processing_completed(BatchProcessingResult::FaultyFailure) + .unwrap(); + assert!(matches!( + outcome, + BatchOperationOutcome::Failed { blacklist: true } + )); + } + + #[test] + fn non_faulty_processing_failures_never_exhaust_batch() { + let mut batch = new_batch(); + + // Well past both limits — non-faulty failures should never cause failure + let iterations = (max_dl() + max_proc()) as Id * 2; + for i in 0..iterations { + advance_to_processing(&mut batch, i, peer()); + let outcome = batch + .processing_completed(BatchProcessingResult::NonFaultyFailure) + .unwrap(); + assert!(matches!(outcome, BatchOperationOutcome::Continue)); + } + // Non-faulty failures also don't register peers as failed + assert!(batch.failed_peers().is_empty()); + } + + #[test] + fn validation_failures_count_toward_processing_limit() { + let mut batch = new_batch(); + + for i in 1..max_proc() as Id { + advance_to_awaiting_validation(&mut batch, i, peer()); + let outcome = batch.validation_failed().unwrap(); + assert!(matches!(outcome, BatchOperationOutcome::Continue)); + } + + advance_to_awaiting_validation(&mut batch, max_proc() as Id, peer()); + let outcome = batch.validation_failed().unwrap(); + assert!(matches!( + outcome, + BatchOperationOutcome::Failed { blacklist: true } + )); + } + + #[test] + fn mixed_failure_types_interact_correctly() { + let mut batch = new_batch(); + let mut req_id: Id = 0; + let mut next_id = || { + req_id += 1; + req_id + }; + + // One download failure + batch.start_downloading(next_id()).unwrap(); + batch.download_failed(Some(peer())).unwrap(); + + // One faulty processing failure (requires a successful download first) + advance_to_processing(&mut batch, next_id(), peer()); + batch + .processing_completed(BatchProcessingResult::FaultyFailure) + .unwrap(); + + // One non-faulty processing failure + advance_to_processing(&mut batch, next_id(), peer()); + batch + .processing_completed(BatchProcessingResult::NonFaultyFailure) + .unwrap(); + assert!(matches!(batch.state(), BatchState::AwaitingDownload)); + + // Fill remaining download failures to hit the limit + for _ in 1..max_dl() { + batch.start_downloading(next_id()).unwrap(); + batch.download_failed(Some(peer())).unwrap(); + } + + // Download failures > processing failures → blacklist: false + assert!(matches!( + batch.outcome(), + BatchOperationOutcome::Failed { blacklist: false } + )); + } +} diff --git a/beacon_node/network/src/sync/range_sync/mod.rs b/beacon_node/network/src/sync/range_sync/mod.rs index dd9f17bfd1..3b65e1c84a 100644 --- a/beacon_node/network/src/sync/range_sync/mod.rs +++ b/beacon_node/network/src/sync/range_sync/mod.rs @@ -5,6 +5,8 @@ mod chain_collection; mod range; mod sync_type; +#[cfg(test)] +pub use chain::RangeSyncBatchConfig; pub use chain::{ChainId, EPOCHS_PER_BATCH}; #[cfg(test)] pub use chain_collection::SyncChainStatus; From 886d31fe7e1b6aff7ae81c8b2c35d17061b0b1fd Mon Sep 17 00:00:00 2001 From: Michael Sproul <michaelsproul@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:27:16 +1100 Subject: [PATCH 48/81] Delete dysfunctional fork_revert feature (#8891) I found myself having to update this code for Gloas, and figured we may as well delete it seeing as it doesn't work. See: - https://github.com/sigp/lighthouse/issues/4198 Delete all `fork_revert` logic and the accompanying test. Co-Authored-By: Michael Sproul <michael@sigmaprime.io> --- beacon_node/beacon_chain/src/builder.rs | 43 +--- beacon_node/beacon_chain/src/fork_revert.rs | 204 ------------------ beacon_node/beacon_chain/src/lib.rs | 1 - beacon_node/beacon_chain/tests/store_tests.rs | 182 ---------------- beacon_node/store/src/hot_cold_store.rs | 8 - beacon_node/store/src/iter.rs | 22 +- consensus/types/src/state/beacon_state.rs | 7 +- 7 files changed, 13 insertions(+), 454 deletions(-) delete mode 100644 beacon_node/beacon_chain/src/fork_revert.rs diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 4c82c93ba3..2c1dae9215 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -7,7 +7,6 @@ use crate::beacon_proposer_cache::BeaconProposerCache; use crate::custody_context::NodeCustodyType; use crate::data_availability_checker::DataAvailabilityChecker; use crate::fork_choice_signal::ForkChoiceSignalTx; -use crate::fork_revert::{reset_fork_choice_to_finalization, revert_to_fork_boundary}; use crate::graffiti_calculator::{GraffitiCalculator, GraffitiOrigin}; use crate::kzg_utils::{build_data_column_sidecars_fulu, build_data_column_sidecars_gloas}; use crate::light_client_server_cache::LightClientServerCache; @@ -778,49 +777,17 @@ where .get_head(current_slot, &self.spec) .map_err(|e| format!("Unable to get fork choice head: {:?}", e))?; - // Try to decode the head block according to the current fork, if that fails, try - // to backtrack to before the most recent fork. - let (head_block_root, head_block, head_reverted) = - match store.get_full_block(&initial_head_block_root) { - Ok(Some(block)) => (initial_head_block_root, block, false), - Ok(None) => return Err("Head block not found in store".into()), - Err(StoreError::SszDecodeError(_)) => { - error!( - message = "This node has likely missed a hard fork. \ - It will try to revert the invalid blocks and keep running, \ - but any stray blocks and states will not be deleted. \ - Long-term you should consider re-syncing this node.", - "Error decoding head block" - ); - let (block_root, block) = revert_to_fork_boundary( - current_slot, - initial_head_block_root, - store.clone(), - &self.spec, - )?; - - (block_root, block, true) - } - Err(e) => return Err(descriptive_db_error("head block", &e)), - }; + let head_block_root = initial_head_block_root; + let head_block = store + .get_full_block(&initial_head_block_root) + .map_err(|e| descriptive_db_error("head block", &e))? + .ok_or("Head block not found in store")?; let (_head_state_root, head_state) = store .get_advanced_hot_state(head_block_root, current_slot, head_block.state_root()) .map_err(|e| descriptive_db_error("head state", &e))? .ok_or("Head state not found in store")?; - // If the head reverted then we need to reset fork choice using the new head's finalized - // checkpoint. - if head_reverted { - fork_choice = reset_fork_choice_to_finalization( - head_block_root, - &head_state, - store.clone(), - Some(current_slot), - &self.spec, - )?; - } - let head_shuffling_ids = BlockShufflingIds::try_from_head(head_block_root, &head_state)?; let mut head_snapshot = BeaconSnapshot { diff --git a/beacon_node/beacon_chain/src/fork_revert.rs b/beacon_node/beacon_chain/src/fork_revert.rs deleted file mode 100644 index 4db79790d3..0000000000 --- a/beacon_node/beacon_chain/src/fork_revert.rs +++ /dev/null @@ -1,204 +0,0 @@ -use crate::{BeaconForkChoiceStore, BeaconSnapshot}; -use fork_choice::{ForkChoice, PayloadVerificationStatus}; -use itertools::process_results; -use state_processing::state_advance::complete_state_advance; -use state_processing::{ - ConsensusContext, VerifyBlockRoot, per_block_processing, - per_block_processing::BlockSignatureStrategy, -}; -use std::sync::Arc; -use std::time::Duration; -use store::{HotColdDB, ItemStore, iter::ParentRootBlockIterator}; -use tracing::{info, warn}; -use types::{BeaconState, ChainSpec, EthSpec, ForkName, Hash256, SignedBeaconBlock, Slot}; - -const CORRUPT_DB_MESSAGE: &str = "The database could be corrupt. Check its file permissions or \ - consider deleting it by running with the --purge-db flag."; - -/// Revert the head to the last block before the most recent hard fork. -/// -/// This function is destructive and should only be used if there is no viable alternative. It will -/// cause the reverted blocks and states to be completely forgotten, lying dormant in the database -/// forever. -/// -/// Return the `(head_block_root, head_block)` that should be used post-reversion. -pub fn revert_to_fork_boundary<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>>( - current_slot: Slot, - head_block_root: Hash256, - store: Arc<HotColdDB<E, Hot, Cold>>, - spec: &ChainSpec, -) -> Result<(Hash256, SignedBeaconBlock<E>), String> { - let current_fork = spec.fork_name_at_slot::<E>(current_slot); - let fork_epoch = spec - .fork_epoch(current_fork) - .ok_or_else(|| format!("Current fork '{}' never activates", current_fork))?; - - if current_fork == ForkName::Base { - return Err(format!( - "Cannot revert to before phase0 hard fork. {}", - CORRUPT_DB_MESSAGE - )); - } - - warn!( - target_fork = %current_fork, - %fork_epoch, - "Reverting invalid head block" - ); - let block_iter = ParentRootBlockIterator::fork_tolerant(&store, head_block_root); - - let (block_root, blinded_block) = process_results(block_iter, |mut iter| { - iter.find_map(|(block_root, block)| { - if block.slot() < fork_epoch.start_slot(E::slots_per_epoch()) { - Some((block_root, block)) - } else { - info!( - ?block_root, - slot = %block.slot(), - "Reverting block" - ); - None - } - }) - }) - .map_err(|e| { - format!( - "Error fetching blocks to revert: {:?}. {}", - e, CORRUPT_DB_MESSAGE - ) - })? - .ok_or_else(|| format!("No pre-fork blocks found. {}", CORRUPT_DB_MESSAGE))?; - - let block = store - .make_full_block(&block_root, blinded_block) - .map_err(|e| format!("Unable to add payload to new head block: {:?}", e))?; - - Ok((block_root, block)) -} - -/// Reset fork choice to the finalized checkpoint of the supplied head state. -/// -/// The supplied `head_block_root` should correspond to the most recently applied block on -/// `head_state`. -/// -/// This function avoids quirks of fork choice initialization by replaying all of the blocks from -/// the checkpoint to the head. -/// -/// See this issue for details: https://github.com/ethereum/consensus-specs/issues/2566 -/// -/// It will fail if the finalized state or any of the blocks to replay are unavailable. -/// -/// WARNING: this function is destructive and causes fork choice to permanently forget all -/// chains other than the chain leading to `head_block_root`. It should only be used in extreme -/// circumstances when there is no better alternative. -pub fn reset_fork_choice_to_finalization<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>>( - head_block_root: Hash256, - head_state: &BeaconState<E>, - store: Arc<HotColdDB<E, Hot, Cold>>, - current_slot: Option<Slot>, - spec: &ChainSpec, -) -> Result<ForkChoice<BeaconForkChoiceStore<E, Hot, Cold>, E>, String> { - // Fetch finalized block. - let finalized_checkpoint = head_state.finalized_checkpoint(); - let finalized_block_root = finalized_checkpoint.root; - let finalized_block = store - .get_full_block(&finalized_block_root) - .map_err(|e| format!("Error loading finalized block: {:?}", e))? - .ok_or_else(|| { - format!( - "Finalized block missing for revert: {:?}", - finalized_block_root - ) - })?; - - // Advance finalized state to finalized epoch (to handle skipped slots). - let finalized_state_root = finalized_block.state_root(); - // The enshrined finalized state should be in the state cache. - let mut finalized_state = store - .get_state(&finalized_state_root, Some(finalized_block.slot()), true) - .map_err(|e| format!("Error loading finalized state: {:?}", e))? - .ok_or_else(|| { - format!( - "Finalized block state missing from database: {:?}", - finalized_state_root - ) - })?; - let finalized_slot = finalized_checkpoint.epoch.start_slot(E::slots_per_epoch()); - complete_state_advance( - &mut finalized_state, - Some(finalized_state_root), - finalized_slot, - spec, - ) - .map_err(|e| { - format!( - "Error advancing finalized state to finalized epoch: {:?}", - e - ) - })?; - let finalized_snapshot = BeaconSnapshot { - beacon_block_root: finalized_block_root, - beacon_block: Arc::new(finalized_block), - beacon_state: finalized_state, - }; - - let fc_store = - BeaconForkChoiceStore::get_forkchoice_store(store.clone(), finalized_snapshot.clone()) - .map_err(|e| format!("Unable to reset fork choice store for revert: {e:?}"))?; - - let mut fork_choice = ForkChoice::from_anchor( - fc_store, - finalized_block_root, - &finalized_snapshot.beacon_block, - &finalized_snapshot.beacon_state, - current_slot, - spec, - ) - .map_err(|e| format!("Unable to reset fork choice for revert: {:?}", e))?; - - // Replay blocks from finalized checkpoint back to head. - // We do not replay attestations presently, relying on the absence of other blocks - // to guarantee `head_block_root` as the head. - let blocks = store - .load_blocks_to_replay(finalized_slot + 1, head_state.slot(), head_block_root) - .map_err(|e| format!("Error loading blocks to replay for fork choice: {:?}", e))?; - - let mut state = finalized_snapshot.beacon_state; - for block in blocks { - complete_state_advance(&mut state, None, block.slot(), spec) - .map_err(|e| format!("State advance failed: {:?}", e))?; - - let mut ctxt = ConsensusContext::new(block.slot()) - .set_proposer_index(block.message().proposer_index()); - per_block_processing( - &mut state, - &block, - BlockSignatureStrategy::NoVerification, - VerifyBlockRoot::True, - &mut ctxt, - spec, - ) - .map_err(|e| format!("Error replaying block: {:?}", e))?; - - // Setting this to unverified is the safest solution, since we don't have a way to - // retro-actively determine if they were valid or not. - // - // This scenario is so rare that it seems OK to double-verify some blocks. - let payload_verification_status = PayloadVerificationStatus::Optimistic; - - fork_choice - .on_block( - block.slot(), - block.message(), - block.canonical_root(), - // Reward proposer boost. We are reinforcing the canonical chain. - Duration::from_secs(0), - &state, - payload_verification_status, - spec, - ) - .map_err(|e| format!("Error applying replayed block to fork choice: {:?}", e))?; - } - - Ok(fork_choice) -} diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 3b03395a66..e1a190ffb3 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -26,7 +26,6 @@ pub mod events; pub mod execution_payload; pub mod fetch_blobs; pub mod fork_choice_signal; -pub mod fork_revert; pub mod graffiti_calculator; pub mod historical_blocks; pub mod historical_data_columns; diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 6bea5f6013..ff20e999bb 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -3924,188 +3924,6 @@ async fn finalizes_after_resuming_from_db() { ); } -#[allow(clippy::large_stack_frames)] -#[tokio::test] -async fn revert_minority_fork_on_resume() { - let validator_count = 16; - let slots_per_epoch = MinimalEthSpec::slots_per_epoch(); - - let fork_epoch = Epoch::new(4); - let fork_slot = fork_epoch.start_slot(slots_per_epoch); - let initial_blocks = slots_per_epoch * fork_epoch.as_u64() - 1; - let post_fork_blocks = slots_per_epoch * 3; - - let mut spec1 = MinimalEthSpec::default_spec(); - spec1.altair_fork_epoch = None; - let mut spec2 = MinimalEthSpec::default_spec(); - spec2.altair_fork_epoch = Some(fork_epoch); - - let all_validators = (0..validator_count).collect::<Vec<usize>>(); - - // Chain with no fork epoch configured. - let db_path1 = tempdir().unwrap(); - let store1 = get_store_generic(&db_path1, StoreConfig::default(), spec1.clone()); - let harness1 = BeaconChainHarness::builder(MinimalEthSpec) - .spec(spec1.clone().into()) - .keypairs(KEYPAIRS[0..validator_count].to_vec()) - .fresh_disk_store(store1) - .mock_execution_layer() - .build(); - - // Chain with fork epoch configured. - let db_path2 = tempdir().unwrap(); - let store2 = get_store_generic(&db_path2, StoreConfig::default(), spec2.clone()); - let harness2 = BeaconChainHarness::builder(MinimalEthSpec) - .spec(spec2.clone().into()) - .keypairs(KEYPAIRS[0..validator_count].to_vec()) - .fresh_disk_store(store2) - .mock_execution_layer() - .build(); - - // Apply the same blocks to both chains initially. - let mut state = harness1.get_current_state(); - let mut block_root = harness1.chain.genesis_block_root; - for slot in (1..=initial_blocks).map(Slot::new) { - let state_root = state.update_tree_hash_cache().unwrap(); - - let attestations = harness1.make_attestations( - &all_validators, - &state, - state_root, - block_root.into(), - slot, - ); - harness1.set_current_slot(slot); - harness2.set_current_slot(slot); - harness1.process_attestations(attestations.clone(), &state); - harness2.process_attestations(attestations, &state); - - let ((block, blobs), new_state) = harness1.make_block(state, slot).await; - - harness1 - .process_block(slot, block.canonical_root(), (block.clone(), blobs.clone())) - .await - .unwrap(); - harness2 - .process_block(slot, block.canonical_root(), (block.clone(), blobs.clone())) - .await - .unwrap(); - - state = new_state; - block_root = block.canonical_root(); - } - - assert_eq!(harness1.head_slot(), fork_slot - 1); - assert_eq!(harness2.head_slot(), fork_slot - 1); - - // Fork the two chains. - let mut state1 = state.clone(); - let mut state2 = state.clone(); - - let mut majority_blocks = vec![]; - - for i in 0..post_fork_blocks { - let slot = fork_slot + i; - - // Attestations on majority chain. - let state_root = state.update_tree_hash_cache().unwrap(); - - let attestations = harness2.make_attestations( - &all_validators, - &state2, - state_root, - block_root.into(), - slot, - ); - harness2.set_current_slot(slot); - harness2.process_attestations(attestations, &state2); - - // Minority chain block (no attesters). - let ((block1, blobs1), new_state1) = harness1.make_block(state1, slot).await; - harness1 - .process_block(slot, block1.canonical_root(), (block1, blobs1)) - .await - .unwrap(); - state1 = new_state1; - - // Majority chain block (all attesters). - let ((block2, blobs2), new_state2) = harness2.make_block(state2, slot).await; - harness2 - .process_block(slot, block2.canonical_root(), (block2.clone(), blobs2)) - .await - .unwrap(); - - state2 = new_state2; - block_root = block2.canonical_root(); - - majority_blocks.push(block2); - } - - let end_slot = fork_slot + post_fork_blocks - 1; - assert_eq!(harness1.head_slot(), end_slot); - assert_eq!(harness2.head_slot(), end_slot); - - // Resume from disk with the hard-fork activated: this should revert the post-fork blocks. - // We have to do some hackery with the `slot_clock` so that the correct slot is set when - // the beacon chain builder loads the head block. - drop(harness1); - let resume_store = get_store_generic(&db_path1, StoreConfig::default(), spec2.clone()); - - let resumed_harness = TestHarness::builder(MinimalEthSpec) - .spec(spec2.clone().into()) - .keypairs(KEYPAIRS[0..validator_count].to_vec()) - .resumed_disk_store(resume_store) - .override_store_mutator(Box::new(move |mut builder| { - builder = builder - .resume_from_db() - .unwrap() - .testing_slot_clock(spec2.get_slot_duration()) - .unwrap(); - builder - .get_slot_clock() - .unwrap() - .set_slot(end_slot.as_u64()); - builder - })) - .mock_execution_layer() - .build(); - - // Head should now be just before the fork. - resumed_harness.chain.recompute_head_at_current_slot().await; - assert_eq!(resumed_harness.head_slot(), fork_slot - 1); - - // Fork choice should only know the canonical head. When we reverted the head we also should - // have called `reset_fork_choice_to_finalization` which rebuilds fork choice from scratch - // without the reverted block. - assert_eq!( - resumed_harness.chain.heads(), - vec![(resumed_harness.head_block_root(), fork_slot - 1)] - ); - - // Apply blocks from the majority chain and trigger finalization. - let initial_split_slot = resumed_harness.chain.store.get_split_slot(); - for block in &majority_blocks { - resumed_harness - .process_block_result((block.clone(), None)) - .await - .unwrap(); - - // The canonical head should be the block from the majority chain. - resumed_harness.chain.recompute_head_at_current_slot().await; - assert_eq!(resumed_harness.head_slot(), block.slot()); - assert_eq!(resumed_harness.head_block_root(), block.canonical_root()); - } - let advanced_split_slot = resumed_harness.chain.store.get_split_slot(); - - // Check that the migration ran successfully. - assert!(advanced_split_slot > initial_split_slot); - - // Check that there is only a single head now matching harness2 (the minority chain is gone). - let heads = resumed_harness.chain.heads(); - assert_eq!(heads, harness2.chain.heads()); - assert_eq!(heads.len(), 1); -} - // This test checks whether the schema downgrade from the latest version to some minimum supported // version is correct. This is the easiest schema test to write without historic versions of // Lighthouse on-hand, but has the disadvantage that the min version needs to be adjusted manually diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index 6e165702a2..4d00ed1c4a 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -721,14 +721,6 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold> }) } - /// Fetch a block from the store, ignoring which fork variant it *should* be for. - pub fn get_block_any_variant<Payload: AbstractExecPayload<E>>( - &self, - block_root: &Hash256, - ) -> Result<Option<SignedBeaconBlock<E, Payload>>, Error> { - self.get_block_with(block_root, SignedBeaconBlock::any_from_ssz_bytes) - } - /// Fetch a block from the store using a custom decode function. /// /// This is useful for e.g. ignoring the slot-indicated fork to forcefully load a block as if it diff --git a/beacon_node/store/src/iter.rs b/beacon_node/store/src/iter.rs index e2b666e597..0cb803d1ed 100644 --- a/beacon_node/store/src/iter.rs +++ b/beacon_node/store/src/iter.rs @@ -249,7 +249,6 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> Iterator pub struct ParentRootBlockIterator<'a, E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> { store: &'a HotColdDB<E, Hot, Cold>, next_block_root: Hash256, - decode_any_variant: bool, _phantom: PhantomData<E>, } @@ -260,17 +259,6 @@ impl<'a, E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> Self { store, next_block_root: start_block_root, - decode_any_variant: false, - _phantom: PhantomData, - } - } - - /// Block iterator that is tolerant of blocks that have the wrong fork for their slot. - pub fn fork_tolerant(store: &'a HotColdDB<E, Hot, Cold>, start_block_root: Hash256) -> Self { - Self { - store, - next_block_root: start_block_root, - decode_any_variant: true, _phantom: PhantomData, } } @@ -285,12 +273,10 @@ impl<'a, E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> Ok(None) } else { let block_root = self.next_block_root; - let block = if self.decode_any_variant { - self.store.get_block_any_variant(&block_root) - } else { - self.store.get_blinded_block(&block_root) - }? - .ok_or(Error::BlockNotFound(block_root))?; + let block = self + .store + .get_blinded_block(&block_root)? + .ok_or(Error::BlockNotFound(block_root))?; self.next_block_root = block.message().parent_root(); Ok(Some((block_root, block))) } diff --git a/consensus/types/src/state/beacon_state.rs b/consensus/types/src/state/beacon_state.rs index 6228e40ef8..bd67f469d2 100644 --- a/consensus/types/src/state/beacon_state.rs +++ b/consensus/types/src/state/beacon_state.rs @@ -56,9 +56,10 @@ use crate::{ pub const CACHED_EPOCHS: usize = 3; -// Pre-electra WS calculations are not supported. On mainnet, pre-electra epochs are outside the weak subjectivity -// period. The default pre-electra WS value is set to 256 to allow for `basic-sim``, `fallback-sim`` test case `revert_minority_fork_on_resume` -// to pass. 256 is a small enough number to trigger the WS safety check pre-electra on mainnet. +// Pre-electra WS calculations are not supported. On mainnet, pre-electra epochs are outside the +// weak subjectivity period. The default pre-electra WS value is set to 256 to allow for `basic-sim` +// and `fallback-sim` tests to pass. 256 is a small enough number to trigger the WS safety check +// pre-electra on mainnet. pub const DEFAULT_PRE_ELECTRA_WS_PERIOD: u64 = 256; const MAX_RANDOM_BYTE: u64 = (1 << 8) - 1; From e59f1f03effdef50b4b2fcdbe8918ad1d2e34f87 Mon Sep 17 00:00:00 2001 From: Jimmy Chen <jchen.tc@gmail.com> Date: Wed, 25 Feb 2026 07:53:33 +1100 Subject: [PATCH 49/81] Add debug spans to DB write paths (#8895) Co-Authored-By: Jimmy Chen <jchen.tc@gmail.com> --- .../beacon_chain/src/historical_blocks.rs | 17 ++++++++++---- beacon_node/store/src/hot_cold_store.rs | 22 ++++++++++++++----- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/beacon_node/beacon_chain/src/historical_blocks.rs b/beacon_node/beacon_chain/src/historical_blocks.rs index 3a3c3739c7..1dae2258f6 100644 --- a/beacon_node/beacon_chain/src/historical_blocks.rs +++ b/beacon_node/beacon_chain/src/historical_blocks.rs @@ -12,7 +12,7 @@ use std::time::Duration; use store::metadata::DataColumnInfo; use store::{AnchorInfo, BlobInfo, DBColumn, Error as StoreError, KeyValueStore, KeyValueStoreOp}; use strum::IntoStaticStr; -use tracing::{debug, instrument}; +use tracing::{debug, debug_span, instrument}; use types::{Hash256, Slot}; /// Use a longer timeout on the pubkey cache. @@ -256,9 +256,18 @@ impl<T: BeaconChainTypes> BeaconChain<T> { // Write the I/O batches to disk, writing the blocks themselves first, as it's better // for the hot DB to contain extra blocks than for the cold DB to point to blocks that // do not exist. - self.store.blobs_db.do_atomically(blob_batch)?; - self.store.hot_db.do_atomically(hot_batch)?; - self.store.cold_db.do_atomically(cold_batch)?; + { + let _span = debug_span!("backfill_write_blobs_db").entered(); + self.store.blobs_db.do_atomically(blob_batch)?; + } + { + let _span = debug_span!("backfill_write_hot_db").entered(); + self.store.hot_db.do_atomically(hot_batch)?; + } + { + let _span = debug_span!("backfill_write_cold_db").entered(); + self.store.cold_db.do_atomically(cold_batch)?; + } let mut anchor_and_blob_batch = Vec::with_capacity(3); diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index 4d00ed1c4a..fe3477dbfe 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -38,7 +38,7 @@ use std::num::NonZeroUsize; use std::path::Path; use std::sync::Arc; use std::time::Duration; -use tracing::{debug, error, info, instrument, warn}; +use tracing::{debug, debug_span, error, info, instrument, warn}; use typenum::Unsigned; use types::data::{ColumnIndex, DataColumnSidecar, DataColumnSidecarList}; use types::*; @@ -1510,14 +1510,24 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold> let blob_cache_ops = blobs_ops.clone(); // Try to execute blobs store ops. - self.blobs_db - .do_atomically(self.convert_to_kv_batch(blobs_ops)?)?; + let kv_blob_ops = self.convert_to_kv_batch(blobs_ops)?; + { + let _span = debug_span!("write_blobs_db").entered(); + self.blobs_db.do_atomically(kv_blob_ops)?; + } let hot_db_cache_ops = hot_db_ops.clone(); // Try to execute hot db store ops. - let tx_res = match self.convert_to_kv_batch(hot_db_ops) { - Ok(kv_store_ops) => self.hot_db.do_atomically(kv_store_ops), - Err(e) => Err(e), + let tx_res = { + let _convert_span = debug_span!("convert_hot_db_ops").entered(); + match self.convert_to_kv_batch(hot_db_ops) { + Ok(kv_store_ops) => { + drop(_convert_span); + let _span = debug_span!("write_hot_db").entered(); + self.hot_db.do_atomically(kv_store_ops) + } + Err(e) => Err(e), + } }; // Rollback on failure if let Err(e) = tx_res { From d6bf53834f2646c640bda9838b3033850cd484cc Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Tue, 24 Feb 2026 20:20:28 -0700 Subject: [PATCH 50/81] Remove merge transition code (#8761) Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com> --- .../beacon_chain/src/beacon_block_streamer.rs | 29 +- beacon_node/beacon_chain/src/beacon_chain.rs | 84 +--- .../beacon_chain/src/bellatrix_readiness.rs | 171 +------- .../beacon_chain/src/block_verification.rs | 74 +--- .../overflow_lru_cache.rs | 28 +- .../beacon_chain/src/execution_payload.rs | 143 +------ .../src/otb_verification_service.rs | 369 ------------------ beacon_node/beacon_chain/src/test_utils.rs | 120 +++++- .../tests/attestation_verification.rs | 10 +- beacon_node/beacon_chain/tests/bellatrix.rs | 212 ---------- .../beacon_chain/tests/block_verification.rs | 9 +- beacon_node/beacon_chain/tests/capella.rs | 156 -------- beacon_node/beacon_chain/tests/events.rs | 4 +- beacon_node/beacon_chain/tests/main.rs | 2 - .../tests/payload_invalidation.rs | 117 +----- beacon_node/beacon_chain/tests/store_tests.rs | 220 +++++++++++ beacon_node/client/src/builder.rs | 2 +- beacon_node/client/src/notifier.rs | 90 +---- beacon_node/execution_layer/src/lib.rs | 353 +---------------- beacon_node/execution_layer/src/metrics.rs | 2 - .../test_utils/execution_block_generator.rs | 123 +----- .../src/test_utils/mock_execution_layer.rs | 62 +-- .../execution_layer/src/test_utils/mod.rs | 23 -- beacon_node/http_api/src/lib.rs | 22 +- .../tests/broadcast_validation_tests.rs | 64 ++- beacon_node/http_api/tests/fork_tests.rs | 2 +- .../http_api/tests/interactive_tests.rs | 6 - beacon_node/http_api/tests/status_tests.rs | 7 - beacon_node/http_api/tests/tests.rs | 9 - beacon_node/operation_pool/src/lib.rs | 225 ++++------- .../src/per_block_processing/tests.rs | 226 +++-------- .../src/per_epoch_processing/tests.rs | 4 +- consensus/types/tests/committee_cache.rs | 1 + consensus/types/tests/state.rs | 1 + lcli/src/mock_el.rs | 9 +- .../src/test_rig.rs | 20 +- testing/state_transition_vectors/src/exit.rs | 5 +- testing/state_transition_vectors/src/main.rs | 1 + validator_manager/src/exit_validators.rs | 9 +- 39 files changed, 581 insertions(+), 2433 deletions(-) delete mode 100644 beacon_node/beacon_chain/src/otb_verification_service.rs delete mode 100644 beacon_node/beacon_chain/tests/bellatrix.rs delete mode 100644 beacon_node/beacon_chain/tests/capella.rs diff --git a/beacon_node/beacon_chain/src/beacon_block_streamer.rs b/beacon_node/beacon_chain/src/beacon_block_streamer.rs index edbdd6d4d9..9ddc50a9f7 100644 --- a/beacon_node/beacon_chain/src/beacon_block_streamer.rs +++ b/beacon_node/beacon_chain/src/beacon_block_streamer.rs @@ -686,7 +686,6 @@ mod tests { use crate::beacon_block_streamer::{BeaconBlockStreamer, CheckCaches}; use crate::test_utils::{BeaconChainHarness, EphemeralHarnessType, test_spec}; use bls::Keypair; - use execution_layer::test_utils::Block; use fixed_bytes::FixedBytesExtended; use std::sync::Arc; use std::sync::LazyLock; @@ -720,7 +719,7 @@ mod tests { async fn check_all_blocks_from_altair_to_fulu() { let slots_per_epoch = MinimalEthSpec::slots_per_epoch() as usize; let num_epochs = 12; - let bellatrix_fork_epoch = 2usize; + let bellatrix_fork_epoch = 0usize; let capella_fork_epoch = 4usize; let deneb_fork_epoch = 6usize; let electra_fork_epoch = 8usize; @@ -737,32 +736,8 @@ mod tests { let spec = Arc::new(spec); let harness = get_harness(VALIDATOR_COUNT, spec.clone()); - // go to bellatrix fork - harness - .extend_slots(bellatrix_fork_epoch * slots_per_epoch) - .await; - // extend half an epoch - harness.extend_slots(slots_per_epoch / 2).await; - // trigger merge - harness - .execution_block_generator() - .move_to_terminal_block() - .expect("should move to terminal block"); - let timestamp = - harness.get_timestamp_at_slot() + harness.spec.get_slot_duration().as_secs(); - harness - .execution_block_generator() - .modify_last_block(|block| { - if let Block::PoW(terminal_block) = block { - terminal_block.timestamp = timestamp; - } - }); - // finish out merge epoch - harness.extend_slots(slots_per_epoch / 2).await; // finish rest of epochs - harness - .extend_slots((num_epochs - 1 - bellatrix_fork_epoch) * slots_per_epoch) - .await; + harness.extend_slots(num_epochs * slots_per_epoch).await; let head = harness.chain.head_snapshot(); let state = &head.beacon_state; diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 26ad2e714b..9d204ac7f2 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -9,7 +9,6 @@ use crate::beacon_proposer_cache::{ }; use crate::blob_verification::{GossipBlobError, GossipVerifiedBlob}; use crate::block_times_cache::BlockTimesCache; -use crate::block_verification::POS_PANDA_BANNER; use crate::block_verification::{ BlockError, ExecutionPendingBlock, GossipVerifiedBlock, IntoExecutionPendingBlock, check_block_is_finalized_checkpoint_or_descendant, check_block_relevancy, @@ -3513,28 +3512,6 @@ impl<T: BeaconChainTypes> BeaconChain<T> { .map_err(BeaconChainError::TokioJoin)? .ok_or(BeaconChainError::RuntimeShutdown)??; - // Log the PoS pandas if a merge transition just occurred. - if payload_verification_outcome.is_valid_merge_transition_block { - info!("{}", POS_PANDA_BANNER); - info!(slot = %block.slot(), "Proof of Stake Activated"); - info!( - terminal_pow_block_hash = ?block - .message() - .execution_payload()? - .parent_hash() - .into_root(), - ); - info!( - merge_transition_block_root = ?block.message().tree_hash_root(), - ); - info!( - merge_transition_execution_hash = ?block - .message() - .execution_payload()? - .block_hash() - .into_root(), - ); - } Ok(ExecutedBlock::new( block, import_data, @@ -6078,21 +6055,6 @@ impl<T: BeaconChainTypes> BeaconChain<T> { input_params: ForkchoiceUpdateParameters, override_forkchoice_update: OverrideForkchoiceUpdate, ) -> Result<(), Error> { - let next_slot = current_slot + 1; - - // There is no need to issue a `forkchoiceUpdated` (fcU) message unless the Bellatrix fork - // has: - // - // 1. Already happened. - // 2. Will happen in the next slot. - // - // The reason for a fcU message in the slot prior to the Bellatrix fork is in case the - // terminal difficulty has already been reached and a payload preparation message needs to - // be issued. - if self.slot_is_prior_to_bellatrix(next_slot) { - return Ok(()); - } - let execution_layer = self .execution_layer .as_ref() @@ -6140,50 +6102,8 @@ impl<T: BeaconChainTypes> BeaconChain<T> { .unwrap_or_else(ExecutionBlockHash::zero), ) } else { - // The head block does not have an execution block hash. We must check to see if we - // happen to be the proposer of the transition block, in which case we still need to - // send forkchoice_updated. - if self - .spec - .fork_name_at_slot::<T::EthSpec>(next_slot) - .bellatrix_enabled() - { - // We are post-bellatrix - if let Some(payload_attributes) = execution_layer - .payload_attributes(next_slot, params.head_root) - .await - { - // We are a proposer, check for terminal_pow_block_hash - if let Some(terminal_pow_block_hash) = execution_layer - .get_terminal_pow_block_hash(&self.spec, payload_attributes.timestamp()) - .await - .map_err(Error::ForkchoiceUpdate)? - { - info!( - slot = %next_slot, - "Prepared POS transition block proposer" - ); - ( - params.head_root, - terminal_pow_block_hash, - params - .justified_hash - .unwrap_or_else(ExecutionBlockHash::zero), - params - .finalized_hash - .unwrap_or_else(ExecutionBlockHash::zero), - ) - } else { - // TTD hasn't been reached yet, no need to update the EL. - return Ok(()); - } - } else { - // We are not a proposer, no need to update the EL. - return Ok(()); - } - } else { - return Ok(()); - } + // Proposing the block for the merge is no longer supported. + return Ok(()); }; let forkchoice_updated_response = execution_layer diff --git a/beacon_node/beacon_chain/src/bellatrix_readiness.rs b/beacon_node/beacon_chain/src/bellatrix_readiness.rs index 88ccc21b85..34d9795b84 100644 --- a/beacon_node/beacon_chain/src/bellatrix_readiness.rs +++ b/beacon_node/beacon_chain/src/bellatrix_readiness.rs @@ -1,126 +1,9 @@ -//! Provides tools for checking if a node is ready for the Bellatrix upgrade and following merge -//! transition. +//! Provides tools for checking genesis execution payload consistency. use crate::{BeaconChain, BeaconChainError as Error, BeaconChainTypes}; use execution_layer::BlockByNumberQuery; -use serde::{Deserialize, Serialize, Serializer}; -use std::fmt; -use std::fmt::Write; use types::*; -/// The time before the Bellatrix fork when we will start issuing warnings about preparation. -pub const SECONDS_IN_A_WEEK: u64 = 604800; -pub const BELLATRIX_READINESS_PREPARATION_SECONDS: u64 = SECONDS_IN_A_WEEK * 2; - -#[derive(Default, Debug, Serialize, Deserialize)] -pub struct MergeConfig { - #[serde(serialize_with = "serialize_uint256")] - pub terminal_total_difficulty: Option<Uint256>, - #[serde(skip_serializing_if = "Option::is_none")] - pub terminal_block_hash: Option<ExecutionBlockHash>, - #[serde(skip_serializing_if = "Option::is_none")] - pub terminal_block_hash_epoch: Option<Epoch>, -} - -impl fmt::Display for MergeConfig { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if self.terminal_block_hash.is_none() - && self.terminal_block_hash_epoch.is_none() - && self.terminal_total_difficulty.is_none() - { - return write!( - f, - "Merge terminal difficulty parameters not configured, check your config" - ); - } - let mut display_string = String::new(); - if let Some(terminal_total_difficulty) = self.terminal_total_difficulty { - write!( - display_string, - "terminal_total_difficulty: {},", - terminal_total_difficulty - )?; - } - if let Some(terminal_block_hash) = self.terminal_block_hash { - write!( - display_string, - "terminal_block_hash: {},", - terminal_block_hash - )?; - } - if let Some(terminal_block_hash_epoch) = self.terminal_block_hash_epoch { - write!( - display_string, - "terminal_block_hash_epoch: {},", - terminal_block_hash_epoch - )?; - } - write!(f, "{}", display_string.trim_end_matches(','))?; - Ok(()) - } -} -impl MergeConfig { - /// Instantiate `self` from the values in a `ChainSpec`. - pub fn from_chainspec(spec: &ChainSpec) -> Self { - let mut params = MergeConfig::default(); - if spec.terminal_total_difficulty != Uint256::MAX { - params.terminal_total_difficulty = Some(spec.terminal_total_difficulty); - } - if spec.terminal_block_hash != ExecutionBlockHash::zero() { - params.terminal_block_hash = Some(spec.terminal_block_hash); - } - if spec.terminal_block_hash_activation_epoch != Epoch::max_value() { - params.terminal_block_hash_epoch = Some(spec.terminal_block_hash_activation_epoch); - } - params - } -} - -/// Indicates if a node is ready for the Bellatrix upgrade and subsequent merge transition. -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -#[serde(tag = "type")] -pub enum BellatrixReadiness { - /// The node is ready, as far as we can tell. - Ready { - config: MergeConfig, - #[serde(serialize_with = "serialize_uint256")] - current_difficulty: Option<Uint256>, - }, - /// The EL can be reached and has the correct configuration, however it's not yet synced. - NotSynced, - /// The user has not configured this node to use an execution endpoint. - NoExecutionEndpoint, -} - -impl fmt::Display for BellatrixReadiness { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - BellatrixReadiness::Ready { - config: params, - current_difficulty, - } => { - write!( - f, - "This node appears ready for Bellatrix \ - Params: {}, current_difficulty: {:?}", - params, current_difficulty - ) - } - BellatrixReadiness::NotSynced => write!( - f, - "The execution endpoint is connected and configured, \ - however it is not yet synced" - ), - BellatrixReadiness::NoExecutionEndpoint => write!( - f, - "The --execution-endpoint flag is not specified, this is a \ - requirement for Bellatrix" - ), - } - } -} - pub enum GenesisExecutionPayloadStatus { Correct(ExecutionBlockHash), BlockHashMismatch { @@ -141,47 +24,6 @@ pub enum GenesisExecutionPayloadStatus { } impl<T: BeaconChainTypes> BeaconChain<T> { - /// Returns `true` if user has an EL configured, or if the Bellatrix fork has occurred or will - /// occur within `BELLATRIX_READINESS_PREPARATION_SECONDS`. - pub fn is_time_to_prepare_for_bellatrix(&self, current_slot: Slot) -> bool { - if let Some(bellatrix_epoch) = self.spec.bellatrix_fork_epoch { - let bellatrix_slot = bellatrix_epoch.start_slot(T::EthSpec::slots_per_epoch()); - let bellatrix_readiness_preparation_slots = - BELLATRIX_READINESS_PREPARATION_SECONDS / self.spec.get_slot_duration().as_secs(); - - if self.execution_layer.is_some() { - // The user has already configured an execution layer, start checking for readiness - // right away. - true - } else { - // Return `true` if Bellatrix has happened or is within the preparation time. - current_slot + bellatrix_readiness_preparation_slots > bellatrix_slot - } - } else { - // The Bellatrix fork epoch has not been defined yet, no need to prepare. - false - } - } - - /// Attempts to connect to the EL and confirm that it is ready for Bellatrix. - pub async fn check_bellatrix_readiness(&self, current_slot: Slot) -> BellatrixReadiness { - if let Some(el) = self.execution_layer.as_ref() { - if !el.is_synced_for_notifier(current_slot).await { - // The EL is not synced. - return BellatrixReadiness::NotSynced; - } - let params = MergeConfig::from_chainspec(&self.spec); - let current_difficulty = el.get_current_difficulty().await.ok().flatten(); - BellatrixReadiness::Ready { - config: params, - current_difficulty, - } - } else { - // There is no EL configured. - BellatrixReadiness::NoExecutionEndpoint - } - } - /// Check that the execution payload embedded in the genesis state matches the EL's genesis /// block. pub async fn check_genesis_execution_payload_is_correct( @@ -223,14 +65,3 @@ impl<T: BeaconChainTypes> BeaconChain<T> { Ok(GenesisExecutionPayloadStatus::Correct(exec_block_hash)) } } - -/// Utility function to serialize a Uint256 as a decimal string. -fn serialize_uint256<S>(val: &Option<Uint256>, s: S) -> Result<S::Ok, S::Error> -where - S: Serializer, -{ - match val { - Some(v) => v.to_string().serialize(s), - None => s.serialize_none(), - } -} diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 292560d6a7..d126c3af00 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -56,8 +56,7 @@ use crate::data_availability_checker::{ }; use crate::data_column_verification::GossipDataColumnError; use crate::execution_payload::{ - AllowOptimisticImport, NotifyExecutionLayer, PayloadNotifier, - validate_execution_payload_for_gossip, validate_merge_block, + NotifyExecutionLayer, PayloadNotifier, validate_execution_payload_for_gossip, }; use crate::kzg_utils::blobs_to_data_column_sidecars; use crate::observed_block_producers::SeenBlock; @@ -80,7 +79,7 @@ use safe_arith::ArithError; use slot_clock::SlotClock; use ssz::Encode; use ssz_derive::{Decode, Encode}; -use state_processing::per_block_processing::{errors::IntoWithIndex, is_merge_transition_block}; +use state_processing::per_block_processing::errors::IntoWithIndex; use state_processing::{ AllCaches, BlockProcessingError, BlockSignatureStrategy, ConsensusContext, SlotProcessingError, VerifyBlockRoot, @@ -99,34 +98,10 @@ use task_executor::JoinHandle; use tracing::{Instrument, Span, debug, debug_span, error, info_span, instrument}; use types::{ BeaconBlockRef, BeaconState, BeaconStateError, BlobsList, ChainSpec, DataColumnSidecarList, - Epoch, EthSpec, ExecutionBlockHash, FullPayload, Hash256, InconsistentFork, KzgProofs, - RelativeEpoch, SignedBeaconBlock, SignedBeaconBlockHeader, Slot, data::DataColumnSidecarError, + Epoch, EthSpec, FullPayload, Hash256, InconsistentFork, KzgProofs, RelativeEpoch, + SignedBeaconBlock, SignedBeaconBlockHeader, Slot, data::DataColumnSidecarError, }; -pub const POS_PANDA_BANNER: &str = r#" - ,,, ,,, ,,, ,,, - ;" ^; ;' ", ;" ^; ;' ", - ; s$$$$$$$s ; ; s$$$$$$$s ; - , ss$$$$$$$$$$s ,' ooooooooo. .oooooo. .oooooo..o , ss$$$$$$$$$$s ,' - ;s$$$$$$$$$$$$$$$ `888 `Y88. d8P' `Y8b d8P' `Y8 ;s$$$$$$$$$$$$$$$ - $$$$$$$$$$$$$$$$$$ 888 .d88'888 888Y88bo. $$$$$$$$$$$$$$$$$$ - $$$$P""Y$$$Y""W$$$$$ 888ooo88P' 888 888 `"Y8888o. $$$$P""Y$$$Y""W$$$$$ - $$$$ p"LFG"q $$$$$ 888 888 888 `"Y88b $$$$ p"LFG"q $$$$$ - $$$$ .$$$$$. $$$$ 888 `88b d88'oo .d8P $$$$ .$$$$$. $$$$ - $$DcaU$$$$$$$$$$ o888o `Y8bood8P' 8""88888P' $$DcaU$$$$$$$$$$ - "Y$$$"*"$$$Y" "Y$$$"*"$$$Y" - "$b.$$" "$b.$$" - - .o. . o8o . .o8 - .888. .o8 `"' .o8 "888 - .8"888. .ooooo. .o888oooooo oooo ooo .oooo. .o888oo .ooooo. .oooo888 - .8' `888. d88' `"Y8 888 `888 `88. .8' `P )88b 888 d88' `88bd88' `888 - .88ooo8888. 888 888 888 `88..8' .oP"888 888 888ooo888888 888 - .8' `888. 888 .o8 888 . 888 `888' d8( 888 888 .888 .o888 888 - o88o o8888o`Y8bod8P' "888"o888o `8' `Y888""8o "888"`Y8bod8P'`Y8bod88P" - -"#; - /// Maximum block slot number. Block with slots bigger than this constant will NOT be processed. const MAXIMUM_BLOCK_SLOT_NUMBER: u64 = 4_294_967_296; // 2^32 @@ -392,13 +367,6 @@ pub enum ExecutionPayloadError { /// /// The block is invalid and the peer is faulty InvalidPayloadTimestamp { expected: u64, found: u64 }, - /// The execution payload references an execution block that cannot trigger the merge. - /// - /// ## Peer scoring - /// - /// The block is invalid and the peer sent us a block that passes gossip propagation conditions, - /// but is invalid upon further verification. - InvalidTerminalPoWBlock { parent_hash: ExecutionBlockHash }, /// The `TERMINAL_BLOCK_HASH` is set, but the block has not reached the /// `TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH`. /// @@ -410,16 +378,6 @@ pub enum ExecutionPayloadError { activation_epoch: Epoch, epoch: Epoch, }, - /// The `TERMINAL_BLOCK_HASH` is set, but does not match the value specified by the block. - /// - /// ## Peer scoring - /// - /// The block is invalid and the peer sent us a block that passes gossip propagation conditions, - /// but is invalid upon further verification. - InvalidTerminalBlockHash { - terminal_block_hash: ExecutionBlockHash, - payload_parent_hash: ExecutionBlockHash, - }, /// The execution node is syncing but we fail the conditions for optimistic sync /// /// ## Peer scoring @@ -444,16 +402,11 @@ impl ExecutionPayloadError { // This is a trivial gossip validation condition, there is no reason for an honest peer // to propagate a block with an invalid payload time stamp. ExecutionPayloadError::InvalidPayloadTimestamp { .. } => true, - // An honest optimistic node may propagate blocks with an invalid terminal PoW block, we - // should not penalized them. - ExecutionPayloadError::InvalidTerminalPoWBlock { .. } => false, // This condition is checked *after* gossip propagation, therefore penalizing gossip // peers for this block would be unfair. There may be an argument to penalize RPC // blocks, since even an optimistic node shouldn't verify this block. We will remove the // penalties for all block imports to keep things simple. ExecutionPayloadError::InvalidActivationEpoch { .. } => false, - // As per `Self::InvalidActivationEpoch`. - ExecutionPayloadError::InvalidTerminalBlockHash { .. } => false, // Do not penalize the peer since it's not their fault that *we're* optimistic. ExecutionPayloadError::UnverifiedNonOptimisticCandidate => false, } @@ -537,7 +490,6 @@ impl From<ArithError> for BlockError { #[derive(Debug, PartialEq, Clone, Encode, Decode)] pub struct PayloadVerificationOutcome { pub payload_verification_status: PayloadVerificationStatus, - pub is_valid_merge_transition_block: bool, } /// Information about invalid blocks which might still be slashable despite being invalid. @@ -1469,27 +1421,10 @@ impl<T: BeaconChainTypes> ExecutionPendingBlock<T> { &parent.pre_state, notify_execution_layer, )?; - let is_valid_merge_transition_block = - is_merge_transition_block(&parent.pre_state, block.message().body()); - let payload_verification_future = async move { let chain = payload_notifier.chain.clone(); let block = payload_notifier.block.clone(); - // If this block triggers the merge, check to ensure that it references valid execution - // blocks. - // - // The specification defines this check inside `on_block` in the fork-choice specification, - // however we perform the check here for two reasons: - // - // - There's no point in importing a block that will fail fork choice, so it's best to fail - // early. - // - Doing the check here means we can keep our fork-choice implementation "pure". I.e., no - // calls to remote servers. - if is_valid_merge_transition_block { - validate_merge_block(&chain, block.message(), AllowOptimisticImport::Yes).await?; - }; - // The specification declares that this should be run *inside* `per_block_processing`, // however we run it here to keep `per_block_processing` pure (i.e., no calls to external // servers). @@ -1504,7 +1439,6 @@ impl<T: BeaconChainTypes> ExecutionPendingBlock<T> { Ok(PayloadVerificationOutcome { payload_verification_status, - is_valid_merge_transition_block, }) }; // Spawn the payload verification future as a new task, but don't wait for it to complete. 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 7260a4aca0..c0403595ee 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 @@ -791,8 +791,8 @@ mod test { use store::{HotColdDB, ItemStore, StoreConfig, database::interface::BeaconNodeBackend}; use tempfile::{TempDir, tempdir}; use tracing::info; + use types::MinimalEthSpec; use types::new_non_zero_usize; - use types::{ExecPayload, MinimalEthSpec}; const LOW_VALIDATOR_COUNT: usize = 32; @@ -820,9 +820,8 @@ mod test { async fn get_deneb_chain<E: EthSpec>( db_path: &TempDir, ) -> BeaconChainHarness<DiskHarnessType<E>> { - let altair_fork_epoch = Epoch::new(1); - let bellatrix_fork_epoch = Epoch::new(2); - let bellatrix_fork_slot = bellatrix_fork_epoch.start_slot(E::slots_per_epoch()); + let altair_fork_epoch = Epoch::new(0); + let bellatrix_fork_epoch = Epoch::new(0); let capella_fork_epoch = Epoch::new(3); let deneb_fork_epoch = Epoch::new(4); let deneb_fork_slot = deneb_fork_epoch.start_slot(E::slots_per_epoch()); @@ -844,25 +843,6 @@ mod test { .mock_execution_layer() .build(); - // go to bellatrix slot - harness.extend_to_slot(bellatrix_fork_slot).await; - let bellatrix_head = &harness.chain.head_snapshot().beacon_block; - assert!(bellatrix_head.as_bellatrix().is_ok()); - assert_eq!(bellatrix_head.slot(), bellatrix_fork_slot); - assert!( - bellatrix_head - .message() - .body() - .execution_payload() - .unwrap() - .is_default_with_empty_roots(), - "Bellatrix head is default payload" - ); - // Trigger the terminal PoW block. - harness - .execution_block_generator() - .move_to_terminal_block() - .unwrap(); // go right before deneb slot harness.extend_to_slot(deneb_fork_slot - 1).await; @@ -942,7 +922,6 @@ mod test { let payload_verification_outcome = PayloadVerificationOutcome { payload_verification_status: PayloadVerificationStatus::Verified, - is_valid_merge_transition_block: false, }; let availability_pending_block = AvailabilityPendingExecutedBlock { @@ -1183,7 +1162,6 @@ mod pending_components_tests { }, payload_verification_outcome: PayloadVerificationOutcome { payload_verification_status: PayloadVerificationStatus::Verified, - is_valid_merge_transition_block: false, }, }; (block, blobs, invalid_blobs) diff --git a/beacon_node/beacon_chain/src/execution_payload.rs b/beacon_node/beacon_chain/src/execution_payload.rs index f32a3ba2a3..a2ebed32ee 100644 --- a/beacon_node/beacon_chain/src/execution_payload.rs +++ b/beacon_node/beacon_chain/src/execution_payload.rs @@ -12,19 +12,19 @@ use crate::{ ExecutionPayloadError, }; use execution_layer::{ - BlockProposalContents, BlockProposalContentsType, BuilderParams, NewPayloadRequest, - PayloadAttributes, PayloadParameters, PayloadStatus, + BlockProposalContentsType, BuilderParams, NewPayloadRequest, PayloadAttributes, + PayloadParameters, PayloadStatus, }; use fork_choice::{InvalidationOperation, PayloadVerificationStatus}; use proto_array::{Block as ProtoBlock, ExecutionStatus}; use slot_clock::SlotClock; use state_processing::per_block_processing::{ compute_timestamp_at_slot, get_expected_withdrawals, is_execution_enabled, - is_merge_transition_complete, partially_verify_execution_payload, + partially_verify_execution_payload, }; use std::sync::Arc; use tokio::task::JoinHandle; -use tracing::{Instrument, debug, debug_span, warn}; +use tracing::{Instrument, debug_span, warn}; use tree_hash::TreeHash; use types::execution::BlockProductionVersion; use types::*; @@ -32,12 +32,6 @@ use types::*; pub type PreparePayloadResult<E> = Result<BlockProposalContentsType<E>, BlockProductionError>; pub type PreparePayloadHandle<E> = JoinHandle<Option<PreparePayloadResult<E>>>; -#[derive(PartialEq)] -pub enum AllowOptimisticImport { - Yes, - No, -} - /// Signal whether the execution payloads of new blocks should be /// immediately verified with the EL or imported optimistically without /// any EL communication. @@ -218,78 +212,6 @@ async fn notify_new_payload<T: BeaconChainTypes>( } } -/// Verify that the block which triggers the merge is valid to be imported to fork choice. -/// -/// ## Errors -/// -/// Will return an error when using a pre-merge fork `state`. Ensure to only run this function -/// after the merge fork. -/// -/// ## Specification -/// -/// Equivalent to the `validate_merge_block` function in the merge Fork Choice Changes: -/// -/// https://github.com/ethereum/consensus-specs/blob/v1.1.5/specs/merge/fork-choice.md#validate_merge_block -pub async fn validate_merge_block<T: BeaconChainTypes>( - chain: &Arc<BeaconChain<T>>, - block: BeaconBlockRef<'_, T::EthSpec>, - allow_optimistic_import: AllowOptimisticImport, -) -> Result<(), BlockError> { - let spec = &chain.spec; - let block_epoch = block.slot().epoch(T::EthSpec::slots_per_epoch()); - let execution_payload = block.execution_payload()?; - - if spec.terminal_block_hash != ExecutionBlockHash::zero() { - if block_epoch < spec.terminal_block_hash_activation_epoch { - return Err(ExecutionPayloadError::InvalidActivationEpoch { - activation_epoch: spec.terminal_block_hash_activation_epoch, - epoch: block_epoch, - } - .into()); - } - - if execution_payload.parent_hash() != spec.terminal_block_hash { - return Err(ExecutionPayloadError::InvalidTerminalBlockHash { - terminal_block_hash: spec.terminal_block_hash, - payload_parent_hash: execution_payload.parent_hash(), - } - .into()); - } - - return Ok(()); - } - - let execution_layer = chain - .execution_layer - .as_ref() - .ok_or(ExecutionPayloadError::NoExecutionConnection)?; - - let is_valid_terminal_pow_block = execution_layer - .is_valid_terminal_pow_block_hash(execution_payload.parent_hash(), spec) - .await - .map_err(ExecutionPayloadError::from)?; - - match is_valid_terminal_pow_block { - Some(true) => Ok(()), - Some(false) => Err(ExecutionPayloadError::InvalidTerminalPoWBlock { - parent_hash: execution_payload.parent_hash(), - } - .into()), - None => { - if allow_optimistic_import == AllowOptimisticImport::Yes { - debug!( - block_hash = ?execution_payload.parent_hash(), - msg = "the terminal block/parent was unavailable", - "Optimistically importing merge transition block" - ); - Ok(()) - } else { - Err(ExecutionPayloadError::UnverifiedNonOptimisticCandidate.into()) - } - } - } -} - /// Validate the gossip block's execution_payload according to the checks described here: /// https://github.com/ethereum/consensus-specs/blob/dev/specs/merge/p2p-interface.md#beacon_block pub fn validate_execution_payload_for_gossip<T: BeaconChainTypes>( @@ -305,14 +227,14 @@ pub fn validate_execution_payload_for_gossip<T: BeaconChainTypes>( // Only apply this validation if this is a Bellatrix beacon block. if let Ok(execution_payload) = block.body().execution_payload() { - // This logic should match `is_execution_enabled`. We use only the execution block hash of - // the parent here in order to avoid loading the parent state during gossip verification. + // Check parent execution status to determine if we should validate the payload. + // We use only the execution status of the parent here to avoid loading the parent state + // during gossip verification. - let is_merge_transition_complete = match parent_block.execution_status { - // Optimistically declare that an "unknown" status block has completed the merge. + let parent_has_execution = match parent_block.execution_status { + // Parent has valid or optimistic execution status. ExecutionStatus::Valid(_) | ExecutionStatus::Optimistic(_) => true, - // It's impossible for an irrelevant block to have completed the merge. It is pre-merge - // by definition. + // Pre-merge blocks have irrelevant execution status. ExecutionStatus::Irrelevant(_) => false, // If the parent has an invalid payload then it's impossible to build a valid block upon // it. Reject the block. @@ -323,7 +245,7 @@ pub fn validate_execution_payload_for_gossip<T: BeaconChainTypes>( } }; - if is_merge_transition_complete || !execution_payload.is_default_with_empty_roots() { + if parent_has_execution || !execution_payload.is_default_with_empty_roots() { let expected_timestamp = chain .slot_clock .start_of(block.slot()) @@ -372,7 +294,6 @@ pub fn get_execution_payload<T: BeaconChainTypes>( // task. let spec = &chain.spec; let current_epoch = state.current_epoch(); - let is_merge_transition_complete = is_merge_transition_complete(state); let timestamp = compute_timestamp_at_slot(state, state.slot(), spec).map_err(BeaconStateError::from)?; let random = *state.get_randao_mix(current_epoch)?; @@ -399,7 +320,6 @@ pub fn get_execution_payload<T: BeaconChainTypes>( async move { prepare_execution_payload::<T>( &chain, - is_merge_transition_complete, timestamp, random, proposer_index, @@ -423,8 +343,6 @@ pub fn get_execution_payload<T: BeaconChainTypes>( /// Prepares an execution payload for inclusion in a block. /// -/// Will return `Ok(None)` if the Bellatrix fork has occurred, but a terminal block has not been found. -/// /// ## Errors /// /// Will return an error when using a pre-Bellatrix fork `state`. Ensure to only run this function @@ -438,7 +356,6 @@ pub fn get_execution_payload<T: BeaconChainTypes>( #[allow(clippy::too_many_arguments)] pub async fn prepare_execution_payload<T>( chain: &Arc<BeaconChain<T>>, - is_merge_transition_complete: bool, timestamp: u64, random: Hash256, proposer_index: u64, @@ -453,7 +370,6 @@ pub async fn prepare_execution_payload<T>( where T: BeaconChainTypes, { - let current_epoch = builder_params.slot.epoch(T::EthSpec::slots_per_epoch()); let spec = &chain.spec; let fork = spec.fork_name_at_slot::<T::EthSpec>(builder_params.slot); let execution_layer = chain @@ -461,42 +377,7 @@ where .as_ref() .ok_or(BlockProductionError::ExecutionLayerMissing)?; - let parent_hash = if !is_merge_transition_complete { - let is_terminal_block_hash_set = spec.terminal_block_hash != ExecutionBlockHash::zero(); - let is_activation_epoch_reached = - current_epoch >= spec.terminal_block_hash_activation_epoch; - - if is_terminal_block_hash_set && !is_activation_epoch_reached { - // Use the "empty" payload if there's a terminal block hash, but we haven't reached the - // terminal block epoch yet. - return Ok(BlockProposalContentsType::Full( - BlockProposalContents::Payload { - payload: FullPayload::default_at_fork(fork)?, - block_value: Uint256::ZERO, - }, - )); - } - - let terminal_pow_block_hash = execution_layer - .get_terminal_pow_block_hash(spec, timestamp) - .await - .map_err(BlockProductionError::TerminalPoWBlockLookupFailed)?; - - if let Some(terminal_pow_block_hash) = terminal_pow_block_hash { - terminal_pow_block_hash - } else { - // If the merge transition hasn't occurred yet and the EL hasn't found the terminal - // block, return an "empty" payload. - return Ok(BlockProposalContentsType::Full( - BlockProposalContents::Payload { - payload: FullPayload::default_at_fork(fork)?, - block_value: Uint256::ZERO, - }, - )); - } - } else { - latest_execution_payload_header_block_hash - }; + let parent_hash = latest_execution_payload_header_block_hash; // Try to obtain the fork choice update parameters from the cached head. // diff --git a/beacon_node/beacon_chain/src/otb_verification_service.rs b/beacon_node/beacon_chain/src/otb_verification_service.rs deleted file mode 100644 index e02705f5da..0000000000 --- a/beacon_node/beacon_chain/src/otb_verification_service.rs +++ /dev/null @@ -1,369 +0,0 @@ -use crate::execution_payload::{validate_merge_block, AllowOptimisticImport}; -use crate::{ - BeaconChain, BeaconChainError, BeaconChainTypes, BlockError, ExecutionPayloadError, - INVALID_FINALIZED_MERGE_TRANSITION_BLOCK_SHUTDOWN_REASON, -}; -use itertools::process_results; -use logging::crit; -use proto_array::InvalidationOperation; -use slot_clock::SlotClock; -use ssz::{Decode, Encode}; -use ssz_derive::{Decode, Encode}; -use state_processing::per_block_processing::is_merge_transition_complete; -use std::sync::Arc; -use store::{DBColumn, Error as StoreError, HotColdDB, KeyValueStore, StoreItem}; -use task_executor::{ShutdownReason, TaskExecutor}; -use tokio::time::sleep; -use tracing::{debug, error, info, warn}; -use tree_hash::TreeHash; -use types::{BeaconBlockRef, EthSpec, Hash256, Slot}; -use DBColumn::OptimisticTransitionBlock as OTBColumn; - -#[derive(Clone, Debug, Decode, Encode, PartialEq)] -pub struct OptimisticTransitionBlock { - root: Hash256, - slot: Slot, -} - -impl OptimisticTransitionBlock { - // types::BeaconBlockRef<'_, <T as BeaconChainTypes>::EthSpec> - pub fn from_block<E: EthSpec>(block: BeaconBlockRef<E>) -> Self { - Self { - root: block.tree_hash_root(), - slot: block.slot(), - } - } - - pub fn root(&self) -> &Hash256 { - &self.root - } - - pub fn slot(&self) -> &Slot { - &self.slot - } - - pub fn persist_in_store<T, A>(&self, store: A) -> Result<(), StoreError> - where - T: BeaconChainTypes, - A: AsRef<HotColdDB<T::EthSpec, T::HotStore, T::ColdStore>>, - { - if store - .as_ref() - .item_exists::<OptimisticTransitionBlock>(&self.root)? - { - Ok(()) - } else { - store.as_ref().put_item(&self.root, self) - } - } - - pub fn remove_from_store<T, A>(&self, store: A) -> Result<(), StoreError> - where - T: BeaconChainTypes, - A: AsRef<HotColdDB<T::EthSpec, T::HotStore, T::ColdStore>>, - { - store - .as_ref() - .hot_db - .key_delete(OTBColumn.into(), self.root.as_slice()) - } - - fn is_canonical<T: BeaconChainTypes>( - &self, - chain: &BeaconChain<T>, - ) -> Result<bool, BeaconChainError> { - Ok(chain - .forwards_iter_block_roots_until(self.slot, self.slot)? - .next() - .transpose()? - .map(|(root, _)| root) - == Some(self.root)) - } -} - -impl StoreItem for OptimisticTransitionBlock { - fn db_column() -> DBColumn { - OTBColumn - } - - fn as_store_bytes(&self) -> Vec<u8> { - self.as_ssz_bytes() - } - - fn from_store_bytes(bytes: &[u8]) -> Result<Self, StoreError> { - Ok(Self::from_ssz_bytes(bytes)?) - } -} - -/// The routine is expected to run once per epoch, 1/4th through the epoch. -pub const EPOCH_DELAY_FACTOR: u32 = 4; - -/// Spawns a routine which checks the validity of any optimistically imported transition blocks -/// -/// This routine will run once per epoch, at `epoch_duration / EPOCH_DELAY_FACTOR` after -/// the start of each epoch. -/// -/// The service will not be started if there is no `execution_layer` on the `chain`. -pub fn start_otb_verification_service<T: BeaconChainTypes>( - executor: TaskExecutor, - chain: Arc<BeaconChain<T>>, -) { - // Avoid spawning the service if there's no EL, it'll just error anyway. - if chain.execution_layer.is_some() { - executor.spawn( - async move { otb_verification_service(chain).await }, - "otb_verification_service", - ); - } -} - -pub fn load_optimistic_transition_blocks<T: BeaconChainTypes>( - chain: &BeaconChain<T>, -) -> Result<Vec<OptimisticTransitionBlock>, StoreError> { - process_results( - chain.store.hot_db.iter_column::<Hash256>(OTBColumn), - |iter| { - iter.map(|(_, bytes)| OptimisticTransitionBlock::from_store_bytes(&bytes)) - .collect() - }, - )? -} - -#[derive(Debug)] -pub enum Error { - ForkChoice(String), - BeaconChain(BeaconChainError), - StoreError(StoreError), - NoBlockFound(OptimisticTransitionBlock), -} - -pub async fn validate_optimistic_transition_blocks<T: BeaconChainTypes>( - chain: &Arc<BeaconChain<T>>, - otbs: Vec<OptimisticTransitionBlock>, -) -> Result<(), Error> { - let finalized_slot = chain - .canonical_head - .fork_choice_read_lock() - .get_finalized_block() - .map_err(|e| Error::ForkChoice(format!("{:?}", e)))? - .slot; - - // separate otbs into - // non-canonical - // finalized canonical - // unfinalized canonical - let mut non_canonical_otbs = vec![]; - let (finalized_canonical_otbs, unfinalized_canonical_otbs) = process_results( - otbs.into_iter().map(|otb| { - otb.is_canonical(chain) - .map(|is_canonical| (otb, is_canonical)) - }), - |pair_iter| { - pair_iter - .filter_map(|(otb, is_canonical)| { - if is_canonical { - Some(otb) - } else { - non_canonical_otbs.push(otb); - None - } - }) - .partition::<Vec<_>, _>(|otb| *otb.slot() <= finalized_slot) - }, - ) - .map_err(Error::BeaconChain)?; - - // remove non-canonical blocks that conflict with finalized checkpoint from the database - for otb in non_canonical_otbs { - if *otb.slot() <= finalized_slot { - otb.remove_from_store::<T, _>(&chain.store) - .map_err(Error::StoreError)?; - } - } - - // ensure finalized canonical otb are valid, otherwise kill client - for otb in finalized_canonical_otbs { - match chain.get_block(otb.root()).await { - Ok(Some(block)) => { - match validate_merge_block(chain, block.message(), AllowOptimisticImport::No).await - { - Ok(()) => { - // merge transition block is valid, remove it from OTB - otb.remove_from_store::<T, _>(&chain.store) - .map_err(Error::StoreError)?; - info!( - block_root = %otb.root(), - "type" = "finalized", - "Validated merge transition block" - ); - } - // The block was not able to be verified by the EL. Leave the OTB in the - // database since the EL is likely still syncing and may verify the block - // later. - Err(BlockError::ExecutionPayloadError( - ExecutionPayloadError::UnverifiedNonOptimisticCandidate, - )) => (), - Err(BlockError::ExecutionPayloadError( - ExecutionPayloadError::InvalidTerminalPoWBlock { .. }, - )) => { - // Finalized Merge Transition Block is Invalid! Kill the Client! - crit!( - msg = "You must use the `--purge-db` flag to clear the database and restart sync. \ - You may be on a hostile network.", - block_hash = ?block.canonical_root(), - "Finalized merge transition block is invalid!" - ); - let mut shutdown_sender = chain.shutdown_sender(); - if let Err(e) = shutdown_sender.try_send(ShutdownReason::Failure( - INVALID_FINALIZED_MERGE_TRANSITION_BLOCK_SHUTDOWN_REASON, - )) { - crit!( - error = ?e, - shutdown_reason = INVALID_FINALIZED_MERGE_TRANSITION_BLOCK_SHUTDOWN_REASON, - "Failed to shut down client" - ); - } - } - _ => {} - } - } - Ok(None) => return Err(Error::NoBlockFound(otb)), - // Our database has pruned the payload and the payload was unavailable on the EL since - // the EL is still syncing or the payload is non-canonical. - Err(BeaconChainError::BlockHashMissingFromExecutionLayer(_)) => (), - Err(e) => return Err(Error::BeaconChain(e)), - } - } - - // attempt to validate any non-finalized canonical otb blocks - for otb in unfinalized_canonical_otbs { - match chain.get_block(otb.root()).await { - Ok(Some(block)) => { - match validate_merge_block(chain, block.message(), AllowOptimisticImport::No).await - { - Ok(()) => { - // merge transition block is valid, remove it from OTB - otb.remove_from_store::<T, _>(&chain.store) - .map_err(Error::StoreError)?; - info!( - block_root = ?otb.root(), - "type" = "not finalized", - "Validated merge transition block" - ); - } - // The block was not able to be verified by the EL. Leave the OTB in the - // database since the EL is likely still syncing and may verify the block - // later. - Err(BlockError::ExecutionPayloadError( - ExecutionPayloadError::UnverifiedNonOptimisticCandidate, - )) => (), - Err(BlockError::ExecutionPayloadError( - ExecutionPayloadError::InvalidTerminalPoWBlock { .. }, - )) => { - // Unfinalized Merge Transition Block is Invalid -> Run process_invalid_execution_payload - warn!( - block_root = ?otb.root(), - "Merge transition block invalid" - ); - chain - .process_invalid_execution_payload( - &InvalidationOperation::InvalidateOne { - block_root: *otb.root(), - }, - ) - .await - .map_err(|e| { - warn!( - error = ?e, - location = "process_invalid_execution_payload", - "Error checking merge transition block" - ); - Error::BeaconChain(e) - })?; - } - _ => {} - } - } - Ok(None) => return Err(Error::NoBlockFound(otb)), - // Our database has pruned the payload and the payload was unavailable on the EL since - // the EL is still syncing or the payload is non-canonical. - Err(BeaconChainError::BlockHashMissingFromExecutionLayer(_)) => (), - Err(e) => return Err(Error::BeaconChain(e)), - } - } - - Ok(()) -} - -/// Loop until any optimistically imported merge transition blocks have been verified and -/// the merge has been finalized. -async fn otb_verification_service<T: BeaconChainTypes>(chain: Arc<BeaconChain<T>>) { - let epoch_duration = chain.slot_clock.slot_duration() * T::EthSpec::slots_per_epoch() as u32; - loop { - match chain - .slot_clock - .duration_to_next_epoch(T::EthSpec::slots_per_epoch()) - { - Some(duration) => { - let additional_delay = epoch_duration / EPOCH_DELAY_FACTOR; - sleep(duration + additional_delay).await; - - debug!("OTB verification service firing"); - - if !is_merge_transition_complete( - &chain.canonical_head.cached_head().snapshot.beacon_state, - ) { - // We are pre-merge. Nothing to do yet. - continue; - } - - // load all optimistically imported transition blocks from the database - match load_optimistic_transition_blocks(chain.as_ref()) { - Ok(otbs) => { - if otbs.is_empty() { - if chain - .canonical_head - .fork_choice_read_lock() - .get_finalized_block() - .map_or(false, |block| { - block.execution_status.is_execution_enabled() - }) - { - // there are no optimistic blocks in the database, we can exit - // the service since the merge transition is finalized and we'll - // never see another transition block - break; - } else { - debug!( - info = "waiting for the merge transition to finalize", - "No optimistic transition blocks" - ) - } - } - if let Err(e) = validate_optimistic_transition_blocks(&chain, otbs).await { - warn!( - error = ?e, - "Error while validating optimistic transition blocks" - ); - } - } - Err(e) => { - error!( - error = ?e, - "Error loading optimistic transition blocks" - ); - } - }; - } - None => { - error!("Failed to read slot clock"); - // If we can't read the slot clock, just wait another slot. - sleep(chain.slot_clock.slot_duration()).await; - } - }; - } - debug!( - msg = "shutting down OTB verification service", - "No optimistic transition blocks in database" - ); -} diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 096a0516fc..eefb5d48b7 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -29,10 +29,7 @@ use execution_layer::test_utils::generate_genesis_header; use execution_layer::{ ExecutionLayer, auth::JwtKey, - test_utils::{ - DEFAULT_JWT_SECRET, DEFAULT_TERMINAL_BLOCK, ExecutionBlockGenerator, MockBuilder, - MockExecutionLayer, - }, + test_utils::{DEFAULT_JWT_SECRET, ExecutionBlockGenerator, MockBuilder, MockExecutionLayer}, }; use fixed_bytes::FixedBytesExtended; use futures::channel::mpsc::Receiver; @@ -52,7 +49,11 @@ use rayon::prelude::*; use sensitive_url::SensitiveUrl; use slot_clock::{SlotClock, TestingSlotClock}; use ssz_types::{RuntimeVariableList, VariableList}; +use state_processing::ConsensusContext; use state_processing::per_block_processing::compute_timestamp_at_slot; +use state_processing::per_block_processing::{ + BlockSignatureStrategy, VerifyBlockRoot, per_block_processing, +}; use state_processing::state_advance::complete_state_advance; use std::borrow::Cow; use std::collections::{HashMap, HashSet}; @@ -202,11 +203,12 @@ pub fn fork_name_from_env() -> Option<ForkName> { /// Return a `ChainSpec` suitable for test usage. /// /// If the `fork_from_env` feature is enabled, read the fork to use from the FORK_NAME environment -/// variable. Otherwise use the default spec. +/// variable. Otherwise we default to Bellatrix as the minimum fork (we no longer support +/// starting test networks prior to Bellatrix). pub fn test_spec<E: EthSpec>() -> ChainSpec { let mut spec = fork_name_from_env() .map(|fork| fork.make_genesis_spec(E::default_spec())) - .unwrap_or_else(|| E::default_spec()); + .unwrap_or_else(|| ForkName::Bellatrix.make_genesis_spec(E::default_spec())); // Set target aggregators to a high value by default. spec.target_aggregators_per_committee = DEFAULT_TARGET_AGGREGATORS; @@ -277,16 +279,25 @@ impl<E: EthSpec> Builder<EphemeralHarnessType<E>> { }); let mutator = move |builder: BeaconChainBuilder<_>| { - let header = generate_genesis_header::<E>(builder.get_spec(), false); + let spec = builder.get_spec(); + let header = generate_genesis_header::<E>(spec); let genesis_state = genesis_state_builder - .set_opt_execution_payload_header(header) + .set_opt_execution_payload_header(header.clone()) .build_genesis_state( &validator_keypairs, HARNESS_GENESIS_TIME, Hash256::from_slice(DEFAULT_ETH1_BLOCK_HASH), - builder.get_spec(), + spec, ) .expect("should generate interop state"); + // For post-Bellatrix forks, verify the merge is complete at genesis + if header.is_some() { + assert!( + state_processing::per_block_processing::is_merge_transition_complete( + &genesis_state + ) + ); + } builder .genesis_state(genesis_state) .expect("should build state using recent genesis") @@ -344,7 +355,7 @@ impl<E: EthSpec> Builder<DiskHarnessType<E>> { }); let mutator = move |builder: BeaconChainBuilder<_>| { - let header = generate_genesis_header::<E>(builder.get_spec(), false); + let header = generate_genesis_header::<E>(builder.get_spec()); let genesis_state = genesis_state_builder .set_opt_execution_payload_header(header) .build_genesis_state( @@ -688,7 +699,6 @@ pub fn mock_execution_layer_from_parts<E: EthSpec>( MockExecutionLayer::new( task_executor, - DEFAULT_TERMINAL_BLOCK, shanghai_time, cancun_time, prague_time, @@ -1178,6 +1188,94 @@ where ) } + /// Build a Bellatrix block with the given execution payload, compute the + /// correct state root, sign it, and import it into the chain. + /// + /// This bypasses the normal block production pipeline, which always requests + /// a payload from the execution layer. That makes it possible to construct + /// blocks with **default (zeroed) payloads** — something the EL-backed flow + /// cannot do — which is needed to simulate the pre-merge portion of a chain + /// that starts at Bellatrix genesis with `is_merge_transition_complete = false`. + /// + /// `state` is expected to be the head state *before* `slot`. It will be + /// advanced to `slot` in-place via `complete_state_advance`, then used to + /// derive the proposer, RANDAO reveal, and parent root. After processing, + /// the caller should typically replace `state` with the chain's new head + /// state (`self.get_current_state()`). + pub async fn build_and_import_block_with_payload( + &self, + state: &mut BeaconState<E>, + slot: Slot, + execution_payload: ExecutionPayloadBellatrix<E>, + ) { + complete_state_advance(state, None, slot, &self.spec).expect("should advance state"); + state.build_caches(&self.spec).expect("should build caches"); + + let proposer_index = state.get_beacon_proposer_index(slot, &self.spec).unwrap(); + let randao_reveal = self.sign_randao_reveal(state, proposer_index, slot); + let parent_root = state.latest_block_header().canonical_root(); + + let mut block = BeaconBlock::Bellatrix(BeaconBlockBellatrix { + slot, + proposer_index: proposer_index as u64, + parent_root, + state_root: Hash256::zero(), + body: BeaconBlockBodyBellatrix { + randao_reveal, + eth1_data: state.eth1_data().clone(), + graffiti: Graffiti::default(), + proposer_slashings: VariableList::empty(), + attester_slashings: VariableList::empty(), + attestations: VariableList::empty(), + deposits: VariableList::empty(), + voluntary_exits: VariableList::empty(), + sync_aggregate: SyncAggregate::new(), + execution_payload: FullPayloadBellatrix { execution_payload }, + }, + }); + + // Run per_block_processing on a clone to compute the post-state root. + let signed_tmp = block.clone().sign( + &self.validator_keypairs[proposer_index].sk, + &state.fork(), + state.genesis_validators_root(), + &self.spec, + ); + let mut ctxt = ConsensusContext::new(slot).set_proposer_index(proposer_index as u64); + let mut post_state = state.clone(); + per_block_processing( + &mut post_state, + &signed_tmp, + BlockSignatureStrategy::NoVerification, + VerifyBlockRoot::False, + &mut ctxt, + &self.spec, + ) + .unwrap_or_else(|e| panic!("per_block_processing failed at slot {}: {e:?}", slot)); + + let state_root = post_state.update_tree_hash_cache().unwrap(); + *block.state_root_mut() = state_root; + + let signed_block = self.sign_beacon_block(block, state); + let block_root = signed_block.canonical_root(); + let rpc_block = RpcBlock::BlockOnly { + block_root, + block: Arc::new(signed_block), + }; + self.chain.slot_clock.set_slot(slot.as_u64()); + self.chain + .process_block( + block_root, + rpc_block, + NotifyExecutionLayer::No, + BlockImportSource::Lookup, + || Ok(()), + ) + .await + .unwrap_or_else(|e| panic!("import failed at slot {}: {e:?}", slot)); + self.chain.recompute_head_at_current_slot().await; + } + #[allow(clippy::too_many_arguments)] pub fn produce_single_attestation_for_block( &self, diff --git a/beacon_node/beacon_chain/tests/attestation_verification.rs b/beacon_node/beacon_chain/tests/attestation_verification.rs index 96071be89f..e8ee628f28 100644 --- a/beacon_node/beacon_chain/tests/attestation_verification.rs +++ b/beacon_node/beacon_chain/tests/attestation_verification.rs @@ -14,6 +14,7 @@ use beacon_chain::{ }, }; use bls::{AggregateSignature, Keypair, SecretKey}; +use execution_layer::test_utils::generate_genesis_header; use fixed_bytes::FixedBytesExtended; use genesis::{DEFAULT_ETH1_BLOCK_HASH, interop_genesis_state}; use int_to_bytes::int_to_bytes32; @@ -79,11 +80,13 @@ fn get_harness_capella_spec( let spec = Arc::new(spec); let validator_keypairs = KEYPAIRS[0..validator_count].to_vec(); + // Use the proper genesis execution payload header that matches the mock execution layer + let execution_payload_header = generate_genesis_header(&spec); let genesis_state = interop_genesis_state( &validator_keypairs, HARNESS_GENESIS_TIME, Hash256::from_slice(DEFAULT_ETH1_BLOCK_HASH), - None, + execution_payload_header, &spec, ) .unwrap(); @@ -106,11 +109,6 @@ fn get_harness_capella_spec( .mock_execution_layer() .build(); - harness - .execution_block_generator() - .move_to_terminal_block() - .unwrap(); - harness.advance_slot(); (harness, spec) diff --git a/beacon_node/beacon_chain/tests/bellatrix.rs b/beacon_node/beacon_chain/tests/bellatrix.rs deleted file mode 100644 index fc0f96ef88..0000000000 --- a/beacon_node/beacon_chain/tests/bellatrix.rs +++ /dev/null @@ -1,212 +0,0 @@ -#![cfg(not(debug_assertions))] // Tests run too slow in debug. - -use beacon_chain::test_utils::BeaconChainHarness; -use execution_layer::test_utils::{Block, DEFAULT_TERMINAL_BLOCK, generate_pow_block}; -use types::*; - -const VALIDATOR_COUNT: usize = 32; - -type E = MainnetEthSpec; - -fn verify_execution_payload_chain<E: EthSpec>(chain: &[FullPayload<E>]) { - let mut prev_ep: Option<FullPayload<E>> = None; - - for ep in chain { - assert!(!ep.is_default_with_empty_roots()); - assert!(ep.block_hash() != ExecutionBlockHash::zero()); - - // Check against previous `ExecutionPayload`. - if let Some(prev_ep) = prev_ep { - assert_eq!(prev_ep.block_hash(), ep.parent_hash()); - assert_eq!(prev_ep.block_number() + 1, ep.block_number()); - assert!(ep.timestamp() > prev_ep.timestamp()); - } - prev_ep = Some(ep.clone()); - } -} - -#[tokio::test] -// TODO(merge): This isn't working cause the non-zero values in `initialize_beacon_state_from_eth1` -// are causing failed lookups to the execution node. I need to come back to this. -#[should_panic] -async fn merge_with_terminal_block_hash_override() { - let altair_fork_epoch = Epoch::new(0); - let bellatrix_fork_epoch = Epoch::new(0); - - let mut spec = E::default_spec(); - spec.altair_fork_epoch = Some(altair_fork_epoch); - spec.bellatrix_fork_epoch = Some(bellatrix_fork_epoch); - - let genesis_pow_block_hash = generate_pow_block( - spec.terminal_total_difficulty, - DEFAULT_TERMINAL_BLOCK, - 0, - ExecutionBlockHash::zero(), - ) - .unwrap() - .block_hash; - - spec.terminal_block_hash = genesis_pow_block_hash; - - let harness = BeaconChainHarness::builder(E::default()) - .spec(spec.into()) - .deterministic_keypairs(VALIDATOR_COUNT) - .fresh_ephemeral_store() - .mock_execution_layer() - .build(); - - assert_eq!( - harness - .execution_block_generator() - .latest_block() - .unwrap() - .block_hash(), - genesis_pow_block_hash, - "pre-condition" - ); - - assert!( - harness - .chain - .head_snapshot() - .beacon_block - .as_bellatrix() - .is_ok(), - "genesis block should be a bellatrix block" - ); - - let mut execution_payloads = vec![]; - for i in 0..E::slots_per_epoch() * 3 { - harness.extend_slots(1).await; - - let block = &harness.chain.head_snapshot().beacon_block; - - let execution_payload = block.message().body().execution_payload().unwrap(); - if i == 0 { - assert_eq!(execution_payload.block_hash(), genesis_pow_block_hash); - } - execution_payloads.push(execution_payload.into()); - } - - verify_execution_payload_chain(execution_payloads.as_slice()); -} - -#[tokio::test] -async fn base_altair_bellatrix_with_terminal_block_after_fork() { - let altair_fork_epoch = Epoch::new(4); - let altair_fork_slot = altair_fork_epoch.start_slot(E::slots_per_epoch()); - let bellatrix_fork_epoch = Epoch::new(8); - let bellatrix_fork_slot = bellatrix_fork_epoch.start_slot(E::slots_per_epoch()); - - let mut spec = E::default_spec(); - spec.altair_fork_epoch = Some(altair_fork_epoch); - spec.bellatrix_fork_epoch = Some(bellatrix_fork_epoch); - - let mut execution_payloads = vec![]; - - let harness = BeaconChainHarness::builder(E::default()) - .spec(spec.into()) - .deterministic_keypairs(VALIDATOR_COUNT) - .fresh_ephemeral_store() - .mock_execution_layer() - .build(); - - /* - * Start with the base fork. - */ - - assert!(harness.chain.head_snapshot().beacon_block.as_base().is_ok()); - - /* - * Do the Altair fork. - */ - - harness.extend_to_slot(altair_fork_slot).await; - - let altair_head = &harness.chain.head_snapshot().beacon_block; - assert!(altair_head.as_altair().is_ok()); - assert_eq!(altair_head.slot(), altair_fork_slot); - - /* - * Do the Bellatrix fork, without a terminal PoW block. - */ - - Box::pin(harness.extend_to_slot(bellatrix_fork_slot)).await; - - let bellatrix_head = &harness.chain.head_snapshot().beacon_block; - assert!(bellatrix_head.as_bellatrix().is_ok()); - assert_eq!(bellatrix_head.slot(), bellatrix_fork_slot); - assert!( - bellatrix_head - .message() - .body() - .execution_payload() - .unwrap() - .is_default_with_empty_roots(), - "Bellatrix head is default payload" - ); - - /* - * Next Bellatrix block shouldn't include an exec payload. - */ - - harness.extend_slots(1).await; - - let one_after_bellatrix_head = &harness.chain.head_snapshot().beacon_block; - assert!( - one_after_bellatrix_head - .message() - .body() - .execution_payload() - .unwrap() - .is_default_with_empty_roots(), - "One after bellatrix head is default payload" - ); - assert_eq!(one_after_bellatrix_head.slot(), bellatrix_fork_slot + 1); - - /* - * Trigger the terminal PoW block. - */ - - harness - .execution_block_generator() - .move_to_terminal_block() - .unwrap(); - - // Add a slot duration to get to the next slot - let timestamp = harness.get_timestamp_at_slot() + harness.spec.get_slot_duration().as_secs(); - - harness - .execution_block_generator() - .modify_last_block(|block| { - if let Block::PoW(terminal_block) = block { - terminal_block.timestamp = timestamp; - } - }); - - harness.extend_slots(1).await; - - let two_after_bellatrix_head = &harness.chain.head_snapshot().beacon_block; - assert!( - two_after_bellatrix_head - .message() - .body() - .execution_payload() - .unwrap() - .is_default_with_empty_roots(), - "Two after bellatrix head is default payload" - ); - assert_eq!(two_after_bellatrix_head.slot(), bellatrix_fork_slot + 2); - - /* - * Next Bellatrix block should include an exec payload. - */ - for _ in 0..4 { - harness.extend_slots(1).await; - - let block = &harness.chain.head_snapshot().beacon_block; - execution_payloads.push(block.message().body().execution_payload().unwrap().into()); - } - - verify_execution_payload_chain(execution_payloads.as_slice()); -} diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index d214ea6b15..e94e64e91d 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -20,10 +20,9 @@ use fixed_bytes::FixedBytesExtended; use logging::create_test_tracing_subscriber; use slasher::{Config as SlasherConfig, Slasher}; use state_processing::{ - BlockProcessingError, ConsensusContext, VerifyBlockRoot, + BlockProcessingError, BlockSignatureStrategy, ConsensusContext, VerifyBlockRoot, common::{attesting_indices_base, attesting_indices_electra}, - per_block_processing::{BlockSignatureStrategy, per_block_processing}, - per_slot_processing, + per_block_processing, per_slot_processing, }; use std::marker::PhantomData; use std::sync::{Arc, LazyLock}; @@ -1849,10 +1848,8 @@ async fn add_altair_block_to_base_chain() { // https://github.com/sigp/lighthouse/issues/4332#issuecomment-1565092279 #[tokio::test] async fn import_duplicate_block_unrealized_justification() { - let spec = MainnetEthSpec::default_spec(); - let harness = BeaconChainHarness::builder(MainnetEthSpec) - .spec(spec.into()) + .default_spec() .keypairs(KEYPAIRS[..].to_vec()) .fresh_ephemeral_store() .mock_execution_layer() diff --git a/beacon_node/beacon_chain/tests/capella.rs b/beacon_node/beacon_chain/tests/capella.rs deleted file mode 100644 index e8ab795366..0000000000 --- a/beacon_node/beacon_chain/tests/capella.rs +++ /dev/null @@ -1,156 +0,0 @@ -#![cfg(not(debug_assertions))] // Tests run too slow in debug. - -use beacon_chain::test_utils::BeaconChainHarness; -use execution_layer::test_utils::Block; -use types::*; - -const VALIDATOR_COUNT: usize = 32; -type E = MainnetEthSpec; - -fn verify_execution_payload_chain<E: EthSpec>(chain: &[FullPayload<E>]) { - let mut prev_ep: Option<FullPayload<E>> = None; - - for ep in chain { - assert!(!ep.is_default_with_empty_roots()); - assert!(ep.block_hash() != ExecutionBlockHash::zero()); - - // Check against previous `ExecutionPayload`. - if let Some(prev_ep) = prev_ep { - assert_eq!(prev_ep.block_hash(), ep.parent_hash()); - assert_eq!(prev_ep.block_number() + 1, ep.block_number()); - assert!(ep.timestamp() > prev_ep.timestamp()); - } - prev_ep = Some(ep.clone()); - } -} - -#[tokio::test] -async fn base_altair_bellatrix_capella() { - let altair_fork_epoch = Epoch::new(4); - let altair_fork_slot = altair_fork_epoch.start_slot(E::slots_per_epoch()); - let bellatrix_fork_epoch = Epoch::new(8); - let bellatrix_fork_slot = bellatrix_fork_epoch.start_slot(E::slots_per_epoch()); - let capella_fork_epoch = Epoch::new(12); - let capella_fork_slot = capella_fork_epoch.start_slot(E::slots_per_epoch()); - - let mut spec = E::default_spec(); - spec.altair_fork_epoch = Some(altair_fork_epoch); - spec.bellatrix_fork_epoch = Some(bellatrix_fork_epoch); - spec.capella_fork_epoch = Some(capella_fork_epoch); - - let harness = BeaconChainHarness::builder(E::default()) - .spec(spec.into()) - .deterministic_keypairs(VALIDATOR_COUNT) - .fresh_ephemeral_store() - .mock_execution_layer() - .build(); - - /* - * Start with the base fork. - */ - assert!(harness.chain.head_snapshot().beacon_block.as_base().is_ok()); - - /* - * Do the Altair fork. - */ - Box::pin(harness.extend_to_slot(altair_fork_slot)).await; - - let altair_head = &harness.chain.head_snapshot().beacon_block; - assert!(altair_head.as_altair().is_ok()); - assert_eq!(altair_head.slot(), altair_fork_slot); - - /* - * Do the Bellatrix fork, without a terminal PoW block. - */ - Box::pin(harness.extend_to_slot(bellatrix_fork_slot)).await; - - let bellatrix_head = &harness.chain.head_snapshot().beacon_block; - assert!(bellatrix_head.as_bellatrix().is_ok()); - assert_eq!(bellatrix_head.slot(), bellatrix_fork_slot); - assert!( - bellatrix_head - .message() - .body() - .execution_payload() - .unwrap() - .is_default_with_empty_roots(), - "Bellatrix head is default payload" - ); - - /* - * Next Bellatrix block shouldn't include an exec payload. - */ - Box::pin(harness.extend_slots(1)).await; - - let one_after_bellatrix_head = &harness.chain.head_snapshot().beacon_block; - assert!( - one_after_bellatrix_head - .message() - .body() - .execution_payload() - .unwrap() - .is_default_with_empty_roots(), - "One after bellatrix head is default payload" - ); - assert_eq!(one_after_bellatrix_head.slot(), bellatrix_fork_slot + 1); - - /* - * Trigger the terminal PoW block. - */ - harness - .execution_block_generator() - .move_to_terminal_block() - .unwrap(); - - // Add a slot duration to get to the next slot - let timestamp = harness.get_timestamp_at_slot() + harness.spec.get_slot_duration().as_secs(); - harness - .execution_block_generator() - .modify_last_block(|block| { - if let Block::PoW(terminal_block) = block { - terminal_block.timestamp = timestamp; - } - }); - Box::pin(harness.extend_slots(1)).await; - - let two_after_bellatrix_head = &harness.chain.head_snapshot().beacon_block; - assert!( - two_after_bellatrix_head - .message() - .body() - .execution_payload() - .unwrap() - .is_default_with_empty_roots(), - "Two after bellatrix head is default payload" - ); - assert_eq!(two_after_bellatrix_head.slot(), bellatrix_fork_slot + 2); - - /* - * Next Bellatrix block should include an exec payload. - */ - let mut execution_payloads = vec![]; - for _ in (bellatrix_fork_slot.as_u64() + 3)..capella_fork_slot.as_u64() { - harness.extend_slots(1).await; - let block = &harness.chain.head_snapshot().beacon_block; - let full_payload: FullPayload<E> = - block.message().body().execution_payload().unwrap().into(); - // pre-capella shouldn't have withdrawals - assert!(full_payload.withdrawals_root().is_err()); - execution_payloads.push(full_payload); - } - - /* - * Should enter capella fork now. - */ - for _ in 0..16 { - harness.extend_slots(1).await; - let block = &harness.chain.head_snapshot().beacon_block; - let full_payload: FullPayload<E> = - block.message().body().execution_payload().unwrap().into(); - // post-capella should have withdrawals - assert!(full_payload.withdrawals_root().is_ok()); - execution_payloads.push(full_payload); - } - - verify_execution_payload_chain(execution_payloads.as_slice()); -} diff --git a/beacon_node/beacon_chain/tests/events.rs b/beacon_node/beacon_chain/tests/events.rs index 92727ffd76..121f8c255d 100644 --- a/beacon_node/beacon_chain/tests/events.rs +++ b/beacon_node/beacon_chain/tests/events.rs @@ -115,7 +115,7 @@ async fn data_column_sidecar_event_on_process_gossip_data_column() { /// Verifies that a blob event is emitted when blobs are received via RPC. #[tokio::test] async fn blob_sidecar_event_on_process_rpc_blobs() { - if fork_name_from_env().is_some_and(|f| !f.deneb_enabled() || f.fulu_enabled()) { + if fork_name_from_env().is_none_or(|f| !f.deneb_enabled() || f.fulu_enabled()) { return; }; @@ -170,7 +170,7 @@ async fn blob_sidecar_event_on_process_rpc_blobs() { #[tokio::test] async fn data_column_sidecar_event_on_process_rpc_columns() { - if fork_name_from_env().is_some_and(|f| !f.fulu_enabled()) { + if fork_name_from_env().is_none_or(|f| !f.fulu_enabled()) { return; }; diff --git a/beacon_node/beacon_chain/tests/main.rs b/beacon_node/beacon_chain/tests/main.rs index aec4416419..e02c488ac6 100644 --- a/beacon_node/beacon_chain/tests/main.rs +++ b/beacon_node/beacon_chain/tests/main.rs @@ -1,9 +1,7 @@ mod attestation_production; mod attestation_verification; -mod bellatrix; mod blob_verification; mod block_verification; -mod capella; mod column_verification; mod events; mod op_verification; diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index 7fd70f0e77..b282adecd5 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -3,8 +3,8 @@ use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::{ BeaconChainError, BlockError, ChainConfig, ExecutionPayloadError, - INVALID_JUSTIFIED_PAYLOAD_SHUTDOWN_REASON, NotifyExecutionLayer, OverrideForkchoiceUpdate, - StateSkipConfig, WhenSlotSkipped, + INVALID_JUSTIFIED_PAYLOAD_SHUTDOWN_REASON, NotifyExecutionLayer, StateSkipConfig, + WhenSlotSkipped, canonical_head::{CachedHead, CanonicalHead}, test_utils::{BeaconChainHarness, EphemeralHarnessType, fork_name_from_env, test_spec}, }; @@ -138,25 +138,6 @@ impl InvalidPayloadRig { payload_attributes } - fn move_to_terminal_block(&self) { - let mock_execution_layer = self.harness.mock_execution_layer.as_ref().unwrap(); - mock_execution_layer - .server - .execution_block_generator() - .move_to_terminal_block() - .unwrap(); - } - - fn latest_execution_block_hash(&self) -> ExecutionBlockHash { - let mock_execution_layer = self.harness.mock_execution_layer.as_ref().unwrap(); - mock_execution_layer - .server - .execution_block_generator() - .latest_execution_block() - .unwrap() - .block_hash - } - async fn build_blocks(&mut self, num_blocks: u64, is_valid: Payload) -> Vec<Hash256> { let mut roots = Vec::with_capacity(num_blocks as usize); for _ in 0..num_blocks { @@ -393,7 +374,6 @@ async fn valid_invalid_syncing() { return; } let mut rig = InvalidPayloadRig::new(); - rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; rig.import_block(Payload::Invalid { @@ -411,7 +391,6 @@ async fn invalid_payload_invalidates_parent() { return; } let mut rig = InvalidPayloadRig::new().enable_attestations(); - rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; // Import a valid transition block. rig.move_to_first_justification(Payload::Syncing).await; @@ -443,7 +422,6 @@ async fn immediate_forkchoice_update_invalid_test( invalid_payload: impl FnOnce(Option<ExecutionBlockHash>) -> Payload, ) { let mut rig = InvalidPayloadRig::new().enable_attestations(); - rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; // Import a valid transition block. rig.move_to_first_justification(Payload::Syncing).await; @@ -501,7 +479,6 @@ async fn justified_checkpoint_becomes_invalid() { return; } let mut rig = InvalidPayloadRig::new().enable_attestations(); - rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; // Import a valid transition block. rig.move_to_first_justification(Payload::Syncing).await; @@ -549,7 +526,6 @@ async fn pre_finalized_latest_valid_hash() { let finalized_epoch = 2; let mut rig = InvalidPayloadRig::new().enable_attestations(); - rig.move_to_terminal_block(); let mut blocks = vec![]; blocks.push(rig.import_block(Payload::Valid).await); // Import a valid transition block. blocks.extend(rig.build_blocks(num_blocks - 1, Payload::Syncing).await); @@ -598,7 +574,6 @@ async fn latest_valid_hash_will_not_validate() { const LATEST_VALID_SLOT: u64 = 3; let mut rig = InvalidPayloadRig::new().enable_attestations(); - rig.move_to_terminal_block(); let mut blocks = vec![]; blocks.push(rig.import_block(Payload::Valid).await); // Import a valid transition block. @@ -649,7 +624,6 @@ async fn latest_valid_hash_is_junk() { let finalized_epoch = 3; let mut rig = InvalidPayloadRig::new().enable_attestations(); - rig.move_to_terminal_block(); let mut blocks = vec![]; blocks.push(rig.import_block(Payload::Valid).await); // Import a valid transition block. blocks.extend(rig.build_blocks(num_blocks, Payload::Syncing).await); @@ -694,7 +668,6 @@ async fn invalidates_all_descendants() { let finalized_slot = E::slots_per_epoch() * 2; let mut rig = InvalidPayloadRig::new().enable_attestations(); - rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; // Import a valid transition block. let blocks = rig.build_blocks(num_blocks, Payload::Syncing).await; @@ -804,7 +777,6 @@ async fn switches_heads() { let finalized_slot = E::slots_per_epoch() * 2; let mut rig = InvalidPayloadRig::new().enable_attestations(); - rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; // Import a valid transition block. let blocks = rig.build_blocks(num_blocks, Payload::Syncing).await; @@ -906,7 +878,6 @@ async fn invalid_during_processing() { return; } let mut rig = InvalidPayloadRig::new(); - rig.move_to_terminal_block(); let roots = &[ rig.import_block(Payload::Valid).await, @@ -941,7 +912,6 @@ async fn invalid_after_optimistic_sync() { return; } let mut rig = InvalidPayloadRig::new().enable_attestations(); - rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; // Import a valid transition block. let mut roots = vec![ @@ -982,7 +952,6 @@ async fn manually_validate_child() { return; } let mut rig = InvalidPayloadRig::new().enable_attestations(); - rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; // Import a valid transition block. let parent = rig.import_block(Payload::Syncing).await; @@ -1003,7 +972,6 @@ async fn manually_validate_parent() { return; } let mut rig = InvalidPayloadRig::new().enable_attestations(); - rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; // Import a valid transition block. let parent = rig.import_block(Payload::Syncing).await; @@ -1024,7 +992,6 @@ async fn payload_preparation() { return; } let mut rig = InvalidPayloadRig::new(); - rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; let el = rig.execution_layer(); @@ -1088,7 +1055,6 @@ async fn invalid_parent() { return; } let mut rig = InvalidPayloadRig::new(); - rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; // Import a valid transition block. // Import a syncing block atop the transition block (we'll call this the "parent block" since we @@ -1156,89 +1122,12 @@ async fn invalid_parent() { )); } -/// Tests to ensure that we will still send a proposer preparation -#[tokio::test] -async fn payload_preparation_before_transition_block() { - if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { - return; - } - let rig = InvalidPayloadRig::new(); - let el = rig.execution_layer(); - - // Run the watchdog routine so that the status of the execution engine is set. This ensures - // that we don't end up with `eth_syncing` requests later in this function that will impede - // testing. - el.watchdog_task().await; - - let head = rig.harness.chain.head_snapshot(); - assert_eq!( - head.beacon_block - .message() - .body() - .execution_payload() - .unwrap() - .block_hash(), - ExecutionBlockHash::zero(), - "the head block is post-bellatrix but pre-transition" - ); - - let current_slot = rig.harness.chain.slot().unwrap(); - let next_slot = current_slot + 1; - let proposer = head - .beacon_state - .get_beacon_proposer_index(next_slot, &rig.harness.chain.spec) - .unwrap(); - let fee_recipient = Address::repeat_byte(99); - - // Provide preparation data to the EL for `proposer`. - el.update_proposer_preparation( - Epoch::new(0), - [( - &ProposerPreparationData { - validator_index: proposer as u64, - fee_recipient, - }, - &None, - )], - ) - .await; - - rig.move_to_terminal_block(); - - rig.harness - .chain - .prepare_beacon_proposer(current_slot) - .await - .unwrap(); - let forkchoice_update_params = rig - .harness - .chain - .canonical_head - .fork_choice_read_lock() - .get_forkchoice_update_parameters(); - rig.harness - .chain - .update_execution_engine_forkchoice( - current_slot, - forkchoice_update_params, - OverrideForkchoiceUpdate::Yes, - ) - .await - .unwrap(); - - let (fork_choice_state, payload_attributes) = rig.previous_forkchoice_update_params(); - let latest_block_hash = rig.latest_execution_block_hash(); - assert_eq!(payload_attributes.suggested_fee_recipient(), fee_recipient); - assert_eq!(fork_choice_state.head_block_hash, latest_block_hash); -} - #[tokio::test] async fn attesting_to_optimistic_head() { if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { return; } let mut rig = InvalidPayloadRig::new(); - rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; // Import a valid transition block. let root = rig.import_block(Payload::Syncing).await; @@ -1361,7 +1250,6 @@ impl InvalidHeadSetup { async fn new() -> InvalidHeadSetup { let slots_per_epoch = E::slots_per_epoch(); let mut rig = InvalidPayloadRig::new().enable_attestations(); - rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; // Import a valid transition block. // Import blocks until the first time the chain finalizes. This avoids @@ -1546,7 +1434,6 @@ async fn weights_after_resetting_optimistic_status() { return; } let mut rig = InvalidPayloadRig::new().enable_attestations(); - rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; // Import a valid transition block. let mut roots = vec![]; diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index ff20e999bb..cfc53c8ce0 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -5558,6 +5558,226 @@ fn check_iterators_from_slot(harness: &TestHarness, slot: Slot) { ); } +/// Test that blocks with default (pre-merge) execution payloads and non-default (post-merge) +/// execution payloads can be produced, stored, and retrieved correctly through a merge transition. +/// +/// Spec (see .claude/plans/8658.md): +/// - Bellatrix at epoch 0 (genesis), genesis has default execution payload header +/// - Slots 1-9: blocks have default (zeroed) execution payloads +/// - Slot 10: first block with a non-default execution payload (merge transition block) +/// - Slots 11-32+: non-default payloads, each with parent_hash == prev payload block_hash +/// - Chain must finalize past genesis +#[tokio::test] +async fn bellatrix_produce_and_store_payloads() { + use beacon_chain::test_utils::{ + DEFAULT_ETH1_BLOCK_HASH, HARNESS_GENESIS_TIME, InteropGenesisBuilder, + }; + use safe_arith::SafeArith; + use state_processing::per_block_processing::is_merge_transition_complete; + use tree_hash::TreeHash; + + let merge_slot = 10u64; + let total_slots = 48u64; + let spec = ForkName::Bellatrix.make_genesis_spec(E::default_spec()); + + // Build genesis state with a default (zeroed) execution payload header so that + // is_merge_transition_complete = false at genesis. + let keypairs = KEYPAIRS[0..LOW_VALIDATOR_COUNT].to_vec(); + let genesis_state = InteropGenesisBuilder::default() + .set_alternating_eth1_withdrawal_credentials() + .set_opt_execution_payload_header(None) + .build_genesis_state( + &keypairs, + HARNESS_GENESIS_TIME, + Hash256::from_slice(DEFAULT_ETH1_BLOCK_HASH), + &spec, + ) + .unwrap(); + + assert!( + !is_merge_transition_complete(&genesis_state), + "genesis should NOT have merge complete" + ); + + let db_path = tempdir().unwrap(); + let store = get_store_generic( + &db_path, + StoreConfig { + prune_payloads: false, + ..StoreConfig::default() + }, + spec.clone(), + ); + + let chain_config = ChainConfig { + archive: true, + ..ChainConfig::default() + }; + let harness = TestHarness::builder(MinimalEthSpec) + .spec(store.get_chain_spec().clone()) + .keypairs(keypairs.clone()) + .fresh_disk_store(store.clone()) + .override_store_mutator(Box::new(move |builder: BeaconChainBuilder<_>| { + builder + .genesis_state(genesis_state) + .expect("should set genesis state") + })) + .mock_execution_layer() + .chain_config(chain_config) + .build(); + + harness + .mock_execution_layer + .as_ref() + .unwrap() + .server + .all_payloads_valid(); + + harness.advance_slot(); + + // Phase 1: slots 1 to merge_slot-1 — blocks with default execution payloads. + let mut state = harness.get_current_state(); + for slot_num in 1..merge_slot { + let slot = Slot::new(slot_num); + harness.advance_slot(); + harness + .build_and_import_block_with_payload( + &mut state, + slot, + ExecutionPayloadBellatrix::default(), + ) + .await; + state = harness.get_current_state(); + } + + // Phase 2: slot merge_slot — the merge transition block with a real payload. + { + let slot = Slot::new(merge_slot); + harness.advance_slot(); + + // Advance state to compute correct timestamp and randao. + let mut pre_state = state.clone(); + complete_state_advance(&mut pre_state, None, slot, &harness.spec) + .expect("should advance state"); + pre_state + .build_caches(&harness.spec) + .expect("should build caches"); + + let timestamp = pre_state + .genesis_time() + .safe_add( + slot.as_u64() + .safe_mul(harness.spec.seconds_per_slot) + .unwrap(), + ) + .unwrap(); + let prev_randao = *pre_state.get_randao_mix(pre_state.current_epoch()).unwrap(); + + let mut transition_payload = ExecutionPayloadBellatrix { + parent_hash: ExecutionBlockHash::zero(), + fee_recipient: Address::repeat_byte(42), + receipts_root: Hash256::repeat_byte(42), + state_root: Hash256::repeat_byte(43), + logs_bloom: vec![0; 256].try_into().unwrap(), + prev_randao, + block_number: 1, + gas_limit: 30_000_000, + gas_used: 0, + timestamp, + extra_data: VariableList::empty(), + base_fee_per_gas: Uint256::from(1u64), + block_hash: ExecutionBlockHash::zero(), + transactions: VariableList::empty(), + }; + transition_payload.block_hash = + ExecutionBlockHash::from_root(transition_payload.tree_hash_root()); + + // Insert the transition payload into the mock EL so subsequent blocks can chain. + { + let mock_el = harness.mock_execution_layer.as_ref().unwrap(); + let mut block_gen = mock_el.server.execution_block_generator(); + block_gen.insert_block_without_checks(execution_layer::test_utils::Block::PoS( + ExecutionPayload::Bellatrix(transition_payload.clone()), + )); + } + + harness + .build_and_import_block_with_payload(&mut state, slot, transition_payload) + .await; + state = harness.get_current_state(); + + assert!( + is_merge_transition_complete(&state), + "merge should be complete after slot {merge_slot}" + ); + } + + // Phase 3: slots merge_slot+1 to total_slots — use harness with attestations. + let post_merge_slots = (total_slots - merge_slot) as usize; + harness.extend_slots(post_merge_slots).await; + + // ---- Verification: check all blocks in the store against plan invariants ---- + + let mut prev_payload_block_hash: Option<ExecutionBlockHash> = None; + + for slot_num in 1..=total_slots { + let slot = Slot::new(slot_num); + let block_root = harness + .chain + .block_root_at_slot(slot, WhenSlotSkipped::Prev) + .unwrap() + .unwrap_or_else(|| panic!("missing block at slot {slot_num}")); + let block = store + .get_blinded_block(&block_root) + .unwrap() + .unwrap_or_else(|| panic!("block not in store at slot {slot_num}")); + let payload = block + .message() + .body() + .execution_payload() + .expect("bellatrix block should have execution payload"); + + if slot_num < merge_slot { + // Slots 1 to merge_slot-1: payload must be default. + assert!( + payload.is_default_with_empty_roots(), + "slot {slot_num} should have default payload" + ); + } else if slot_num == merge_slot { + // Merge transition block: first non-default payload. + assert!( + !payload.is_default_with_empty_roots(), + "slot {slot_num} (merge) should have non-default payload" + ); + prev_payload_block_hash = Some(payload.block_hash()); + } else { + // Post-merge: non-default payload with valid parent_hash chain. + assert!( + !payload.is_default_with_empty_roots(), + "slot {slot_num} should have non-default payload" + ); + assert_eq!( + payload.parent_hash(), + prev_payload_block_hash.unwrap(), + "slot {slot_num} payload parent_hash should chain from previous payload" + ); + prev_payload_block_hash = Some(payload.block_hash()); + } + } + + // Verify finalization. + let finalized_epoch = harness + .chain + .canonical_head + .cached_head() + .finalized_checkpoint() + .epoch; + assert!( + finalized_epoch > 0, + "chain should have finalized past genesis" + ); +} + fn get_finalized_epoch_boundary_blocks( dump: &[BeaconSnapshot<MinimalEthSpec, BlindedPayload<MinimalEthSpec>>], ) -> HashSet<SignedBeaconBlockHash> { diff --git a/beacon_node/client/src/builder.rs b/beacon_node/client/src/builder.rs index 1b395ac8da..865599b9bd 100644 --- a/beacon_node/client/src/builder.rs +++ b/beacon_node/client/src/builder.rs @@ -281,7 +281,7 @@ where validator_count, genesis_time, } => { - let execution_payload_header = generate_genesis_header(&spec, true); + let execution_payload_header = generate_genesis_header(&spec); let keypairs = generate_deterministic_keypairs(validator_count); let genesis_state = interop_genesis_state( &keypairs, diff --git a/beacon_node/client/src/notifier.rs b/beacon_node/client/src/notifier.rs index 21a5abeb6c..c1d8cae573 100644 --- a/beacon_node/client/src/notifier.rs +++ b/beacon_node/client/src/notifier.rs @@ -1,9 +1,7 @@ use crate::metrics; use beacon_chain::{ BeaconChain, BeaconChainTypes, ExecutionStatus, - bellatrix_readiness::{ - BellatrixReadiness, GenesisExecutionPayloadStatus, MergeConfig, SECONDS_IN_A_WEEK, - }, + bellatrix_readiness::GenesisExecutionPayloadStatus, }; use execution_layer::{ EngineCapabilities, @@ -36,6 +34,7 @@ const SPEEDO_OBSERVATIONS: usize = 4; /// The number of slots between logs that give detail about backfill process. const BACKFILL_LOG_INTERVAL: u64 = 5; +const SECONDS_IN_A_WEEK: u64 = 604800; pub const FORK_READINESS_PREPARATION_SECONDS: u64 = SECONDS_IN_A_WEEK * 2; pub const ENGINE_CAPABILITIES_REFRESH_INTERVAL: u64 = 300; @@ -70,7 +69,6 @@ pub fn spawn_notifier<T: BeaconChainTypes>( wait_time = estimated_time_pretty(Some(next_slot.as_secs() as f64)), "Waiting for genesis" ); - bellatrix_readiness_logging(Slot::new(0), &beacon_chain).await; post_bellatrix_readiness_logging(Slot::new(0), &beacon_chain).await; genesis_execution_payload_logging(&beacon_chain).await; sleep(slot_duration).await; @@ -414,7 +412,6 @@ pub fn spawn_notifier<T: BeaconChainTypes>( ); } - bellatrix_readiness_logging(current_slot, &beacon_chain).await; post_bellatrix_readiness_logging(current_slot, &beacon_chain).await; } }; @@ -425,88 +422,7 @@ pub fn spawn_notifier<T: BeaconChainTypes>( Ok(()) } -/// Provides some helpful logging to users to indicate if their node is ready for the Bellatrix -/// fork and subsequent merge transition. -async fn bellatrix_readiness_logging<T: BeaconChainTypes>( - current_slot: Slot, - beacon_chain: &BeaconChain<T>, -) { - // There is no execution payload in gloas blocks, so this will trigger - // bellatrix readiness logging in gloas if we dont skip the check below - if beacon_chain - .spec - .fork_name_at_slot::<T::EthSpec>(current_slot) - .gloas_enabled() - { - return; - } - - let merge_completed = beacon_chain - .canonical_head - .cached_head() - .snapshot - .beacon_block - .message() - .body() - .execution_payload() - .is_ok_and(|payload| payload.parent_hash() != ExecutionBlockHash::zero()); - - let has_execution_layer = beacon_chain.execution_layer.is_some(); - - if merge_completed && has_execution_layer - || !beacon_chain.is_time_to_prepare_for_bellatrix(current_slot) - { - return; - } - - match beacon_chain.check_bellatrix_readiness(current_slot).await { - BellatrixReadiness::Ready { - config, - current_difficulty, - } => match config { - MergeConfig { - terminal_total_difficulty: Some(ttd), - terminal_block_hash: None, - terminal_block_hash_epoch: None, - } => { - info!( - terminal_total_difficulty = %ttd, - current_difficulty = current_difficulty - .map(|d| d.to_string()) - .unwrap_or_else(|| "??".into()), - "Ready for Bellatrix" - ) - } - MergeConfig { - terminal_total_difficulty: _, - terminal_block_hash: Some(terminal_block_hash), - terminal_block_hash_epoch: Some(terminal_block_hash_epoch), - } => { - info!( - info = "you are using override parameters, please ensure that you \ - understand these parameters and their implications.", - ?terminal_block_hash, - ?terminal_block_hash_epoch, - "Ready for Bellatrix" - ) - } - other => error!( - config = ?other, - "Inconsistent merge configuration" - ), - }, - readiness @ BellatrixReadiness::NotSynced => warn!( - info = %readiness, - "Not ready Bellatrix" - ), - readiness @ BellatrixReadiness::NoExecutionEndpoint => warn!( - info = %readiness, - "Not ready for Bellatrix" - ), - } -} - -/// Provides some helpful logging to users to indicate if their node is ready for Capella +/// Provides some helpful logging to users to indicate if their node is ready for upcoming forks async fn post_bellatrix_readiness_logging<T: BeaconChainTypes>( current_slot: Slot, beacon_chain: &BeaconChain<T>, diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index 157fe152ef..d6796f6a05 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -22,7 +22,6 @@ use eth2::types::{ForkVersionedResponse, builder::SignedBuilderBid}; use fixed_bytes::UintExtended; use fork_choice::ForkchoiceUpdateParameters; use logging::crit; -use lru::LruCache; pub use payload_status::PayloadStatus; use payload_status::process_payload_status; use sensitive_url::SensitiveUrl; @@ -32,7 +31,6 @@ use std::collections::{HashMap, hash_map::Entry}; use std::fmt; use std::future::Future; use std::io::Write; -use std::num::NonZeroUsize; use std::path::PathBuf; use std::sync::Arc; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; @@ -45,6 +43,7 @@ use tokio::{ use tokio_stream::wrappers::WatchStream; use tracing::{Instrument, debug, debug_span, error, info, instrument, warn}; use tree_hash::TreeHash; +use types::ExecutionPayloadGloas; use types::builder::BuilderBid; use types::execution::BlockProductionVersion; use types::kzg_ext::KzgCommitments; @@ -57,7 +56,6 @@ use types::{ ExecutionPayloadCapella, ExecutionPayloadElectra, ExecutionPayloadFulu, FullPayload, ProposerPreparationData, Slot, }; -use types::{ExecutionPayloadGloas, new_non_zero_usize}; mod block_hash; mod engine_api; @@ -75,10 +73,6 @@ pub const DEFAULT_EXECUTION_ENDPOINT: &str = "http://localhost:8551/"; /// Name for the default file used for the jwt secret. pub const DEFAULT_JWT_FILE: &str = "jwt.hex"; -/// Each time the `ExecutionLayer` retrieves a block from an execution node, it stores that block -/// in an LRU cache to avoid redundant lookups. This is the size of that cache. -const EXECUTION_BLOCKS_LRU_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(128); - /// A fee recipient address for use during block production. Only used as a very last resort if /// there is no address provided by the user. /// @@ -452,7 +446,6 @@ struct Inner<E: EthSpec> { execution_engine_forkchoice_lock: Mutex<()>, suggested_fee_recipient: Option<Address>, proposer_preparation_data: Mutex<HashMap<u64, ProposerPreparationDataEntry>>, - execution_blocks: Mutex<LruCache<ExecutionBlockHash, ExecutionBlock>>, proposers: RwLock<HashMap<ProposerKey, Proposer>>, executor: TaskExecutor, payload_cache: PayloadCache<E>, @@ -563,7 +556,6 @@ impl<E: EthSpec> ExecutionLayer<E> { suggested_fee_recipient, proposer_preparation_data: Mutex::new(HashMap::new()), proposers: RwLock::new(HashMap::new()), - execution_blocks: Mutex::new(LruCache::new(EXECUTION_BLOCKS_LRU_CACHE_SIZE)), executor, payload_cache: PayloadCache::default(), last_new_payload_errored: RwLock::new(false), @@ -655,12 +647,6 @@ impl<E: EthSpec> ExecutionLayer<E> { .ok_or(ApiError::ExecutionHeadBlockNotFound)?; Ok(block.total_difficulty) } - /// Note: this function returns a mutex guard, be careful to avoid deadlocks. - async fn execution_blocks( - &self, - ) -> MutexGuard<'_, LruCache<ExecutionBlockHash, ExecutionBlock>> { - self.inner.execution_blocks.lock().await - } /// Gives access to a channel containing if the last engine state is online or not. /// @@ -1641,208 +1627,6 @@ impl<E: EthSpec> ExecutionLayer<E> { Ok(versions) } - /// Used during block production to determine if the merge has been triggered. - /// - /// ## Specification - /// - /// `get_terminal_pow_block_hash` - /// - /// https://github.com/ethereum/consensus-specs/blob/v1.1.5/specs/merge/validator.md - pub async fn get_terminal_pow_block_hash( - &self, - spec: &ChainSpec, - timestamp: u64, - ) -> Result<Option<ExecutionBlockHash>, Error> { - let _timer = metrics::start_timer_vec( - &metrics::EXECUTION_LAYER_REQUEST_TIMES, - &[metrics::GET_TERMINAL_POW_BLOCK_HASH], - ); - - let hash_opt = self - .engine() - .request(|engine| async move { - let terminal_block_hash = spec.terminal_block_hash; - if terminal_block_hash != ExecutionBlockHash::zero() { - if self - .get_pow_block(engine, terminal_block_hash) - .await? - .is_some() - { - return Ok(Some(terminal_block_hash)); - } else { - return Ok(None); - } - } - - let block = self.get_pow_block_at_total_difficulty(engine, spec).await?; - if let Some(pow_block) = block { - // If `terminal_block.timestamp == transition_block.timestamp`, - // we violate the invariant that a block's timestamp must be - // strictly greater than its parent's timestamp. - // The execution layer will reject a fcu call with such payload - // attributes leading to a missed block. - // Hence, we return `None` in such a case. - if pow_block.timestamp >= timestamp { - return Ok(None); - } - } - Ok(block.map(|b| b.block_hash)) - }) - .await - .map_err(Box::new) - .map_err(Error::EngineError)?; - - if let Some(hash) = &hash_opt { - info!( - terminal_block_hash_override = ?spec.terminal_block_hash, - terminal_total_difficulty = ?spec.terminal_total_difficulty, - block_hash = ?hash, - "Found terminal block hash" - ); - } - - Ok(hash_opt) - } - - /// This function should remain internal. External users should use - /// `self.get_terminal_pow_block` instead, since it checks against the terminal block hash - /// override. - /// - /// ## Specification - /// - /// `get_pow_block_at_terminal_total_difficulty` - /// - /// https://github.com/ethereum/consensus-specs/blob/v1.1.5/specs/merge/validator.md - async fn get_pow_block_at_total_difficulty( - &self, - engine: &Engine, - spec: &ChainSpec, - ) -> Result<Option<ExecutionBlock>, ApiError> { - let mut block = engine - .api - .get_block_by_number(BlockByNumberQuery::Tag(LATEST_TAG)) - .await? - .ok_or(ApiError::ExecutionHeadBlockNotFound)?; - - self.execution_blocks().await.put(block.block_hash, block); - - loop { - let block_reached_ttd = - block.terminal_total_difficulty_reached(spec.terminal_total_difficulty); - if block_reached_ttd { - if block.parent_hash == ExecutionBlockHash::zero() { - return Ok(Some(block)); - } - let parent = self - .get_pow_block(engine, block.parent_hash) - .await? - .ok_or(ApiError::ExecutionBlockNotFound(block.parent_hash))?; - let parent_reached_ttd = - parent.terminal_total_difficulty_reached(spec.terminal_total_difficulty); - - if block_reached_ttd && !parent_reached_ttd { - return Ok(Some(block)); - } else { - block = parent; - } - } else { - return Ok(None); - } - } - } - - /// Used during block verification to check that a block correctly triggers the merge. - /// - /// ## Returns - /// - /// - `Some(true)` if the given `block_hash` is the terminal proof-of-work block. - /// - `Some(false)` if the given `block_hash` is certainly *not* the terminal proof-of-work - /// block. - /// - `None` if the `block_hash` or its parent were not present on the execution engine. - /// - `Err(_)` if there was an error connecting to the execution engine. - /// - /// ## Fallback Behaviour - /// - /// The request will be broadcast to all nodes, simultaneously. It will await a response (or - /// failure) from all nodes and then return based on the first of these conditions which - /// returns true: - /// - /// - Terminal, if any node indicates it is terminal. - /// - Not terminal, if any node indicates it is non-terminal. - /// - Block not found, if any node cannot find the block. - /// - An error, if all nodes return an error. - /// - /// ## Specification - /// - /// `is_valid_terminal_pow_block` - /// - /// https://github.com/ethereum/consensus-specs/blob/v1.1.0/specs/merge/fork-choice.md - pub async fn is_valid_terminal_pow_block_hash( - &self, - block_hash: ExecutionBlockHash, - spec: &ChainSpec, - ) -> Result<Option<bool>, Error> { - let _timer = metrics::start_timer_vec( - &metrics::EXECUTION_LAYER_REQUEST_TIMES, - &[metrics::IS_VALID_TERMINAL_POW_BLOCK_HASH], - ); - - self.engine() - .request(|engine| async move { - if let Some(pow_block) = self.get_pow_block(engine, block_hash).await? - && let Some(pow_parent) = - self.get_pow_block(engine, pow_block.parent_hash).await? - { - return Ok(Some( - self.is_valid_terminal_pow_block(pow_block, pow_parent, spec), - )); - } - Ok(None) - }) - .await - .map_err(Box::new) - .map_err(Error::EngineError) - } - - /// This function should remain internal. - /// - /// External users should use `self.is_valid_terminal_pow_block_hash`. - fn is_valid_terminal_pow_block( - &self, - block: ExecutionBlock, - parent: ExecutionBlock, - spec: &ChainSpec, - ) -> bool { - let is_total_difficulty_reached = - block.terminal_total_difficulty_reached(spec.terminal_total_difficulty); - let is_parent_total_difficulty_valid = parent - .total_difficulty - .is_some_and(|td| td < spec.terminal_total_difficulty); - is_total_difficulty_reached && is_parent_total_difficulty_valid - } - - /// Maps to the `eth_getBlockByHash` JSON-RPC call. - async fn get_pow_block( - &self, - engine: &Engine, - hash: ExecutionBlockHash, - ) -> Result<Option<ExecutionBlock>, ApiError> { - if let Some(cached) = self.execution_blocks().await.get(&hash).copied() { - // The block was in the cache, no need to request it from the execution - // engine. - return Ok(Some(cached)); - } - - // The block was *not* in the cache, request it from the execution - // engine and cache it for future reference. - if let Some(block) = engine.api.get_block_by_hash(hash).await? { - self.execution_blocks().await.put(hash, block); - Ok(Some(block)) - } else { - Ok(None) - } - } - pub async fn get_payload_bodies_by_hash( &self, hashes: Vec<ExecutionBlockHash>, @@ -2330,15 +2114,6 @@ async fn timed_future<F: Future<Output = T>, T>(metric: &str, future: F) -> (T, (result, duration) } -#[cfg(test)] -/// Returns the duration since the unix epoch. -fn timestamp_now() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_else(|_| Duration::from_secs(0)) - .as_secs() -} - fn noop<E: EthSpec>( _: &ExecutionLayer<E>, _: PayloadContentsRefTuple<E>, @@ -2359,7 +2134,6 @@ mod test { async fn produce_three_valid_pos_execution_blocks() { let runtime = TestRuntime::default(); MockExecutionLayer::default_params(runtime.task_executor.clone()) - .move_to_terminal_block() .produce_valid_execution_payload_on_head() .await .produce_valid_execution_payload_on_head() @@ -2388,129 +2162,4 @@ mod test { Some(30_029_266) ); } - - #[tokio::test] - async fn test_forked_terminal_block() { - let runtime = TestRuntime::default(); - let (mock, block_hash) = MockExecutionLayer::default_params(runtime.task_executor.clone()) - .move_to_terminal_block() - .produce_forked_pow_block(); - assert!( - mock.el - .is_valid_terminal_pow_block_hash(block_hash, &mock.spec) - .await - .unwrap() - .unwrap() - ); - } - - #[tokio::test] - async fn finds_valid_terminal_block_hash() { - let runtime = TestRuntime::default(); - MockExecutionLayer::default_params(runtime.task_executor.clone()) - .move_to_block_prior_to_terminal_block() - .with_terminal_block(|spec, el, _| async move { - el.engine().upcheck().await; - assert_eq!( - el.get_terminal_pow_block_hash(&spec, timestamp_now()) - .await - .unwrap(), - None - ) - }) - .await - .move_to_terminal_block() - .with_terminal_block(|spec, el, terminal_block| async move { - assert_eq!( - el.get_terminal_pow_block_hash(&spec, timestamp_now()) - .await - .unwrap(), - Some(terminal_block.unwrap().block_hash) - ) - }) - .await; - } - - #[tokio::test] - async fn rejects_terminal_block_with_equal_timestamp() { - let runtime = TestRuntime::default(); - MockExecutionLayer::default_params(runtime.task_executor.clone()) - .move_to_block_prior_to_terminal_block() - .with_terminal_block(|spec, el, _| async move { - el.engine().upcheck().await; - assert_eq!( - el.get_terminal_pow_block_hash(&spec, timestamp_now()) - .await - .unwrap(), - None - ) - }) - .await - .move_to_terminal_block() - .with_terminal_block(|spec, el, terminal_block| async move { - let timestamp = terminal_block.as_ref().map(|b| b.timestamp).unwrap(); - assert_eq!( - el.get_terminal_pow_block_hash(&spec, timestamp) - .await - .unwrap(), - None - ) - }) - .await; - } - - #[tokio::test] - async fn verifies_valid_terminal_block_hash() { - let runtime = TestRuntime::default(); - MockExecutionLayer::default_params(runtime.task_executor.clone()) - .move_to_terminal_block() - .with_terminal_block(|spec, el, terminal_block| async move { - el.engine().upcheck().await; - assert_eq!( - el.is_valid_terminal_pow_block_hash(terminal_block.unwrap().block_hash, &spec) - .await - .unwrap(), - Some(true) - ) - }) - .await; - } - - #[tokio::test] - async fn rejects_invalid_terminal_block_hash() { - let runtime = TestRuntime::default(); - MockExecutionLayer::default_params(runtime.task_executor.clone()) - .move_to_terminal_block() - .with_terminal_block(|spec, el, terminal_block| async move { - el.engine().upcheck().await; - let invalid_terminal_block = terminal_block.unwrap().parent_hash; - - assert_eq!( - el.is_valid_terminal_pow_block_hash(invalid_terminal_block, &spec) - .await - .unwrap(), - Some(false) - ) - }) - .await; - } - - #[tokio::test] - async fn rejects_unknown_terminal_block_hash() { - let runtime = TestRuntime::default(); - MockExecutionLayer::default_params(runtime.task_executor.clone()) - .move_to_terminal_block() - .with_terminal_block(|spec, el, _| async move { - el.engine().upcheck().await; - let missing_terminal_block = ExecutionBlockHash::repeat_byte(42); - - assert_eq!( - el.is_valid_terminal_pow_block_hash(missing_terminal_block, &spec) - .await - .unwrap(), - None - ) - }) - .await; - } } diff --git a/beacon_node/execution_layer/src/metrics.rs b/beacon_node/execution_layer/src/metrics.rs index 859f33bc81..79bdc37aea 100644 --- a/beacon_node/execution_layer/src/metrics.rs +++ b/beacon_node/execution_layer/src/metrics.rs @@ -10,8 +10,6 @@ pub const GET_BLINDED_PAYLOAD_BUILDER: &str = "get_blinded_payload_builder"; pub const POST_BLINDED_PAYLOAD_BUILDER: &str = "post_blinded_payload_builder"; pub const NEW_PAYLOAD: &str = "new_payload"; pub const FORKCHOICE_UPDATED: &str = "forkchoice_updated"; -pub const GET_TERMINAL_POW_BLOCK_HASH: &str = "get_terminal_pow_block_hash"; -pub const IS_VALID_TERMINAL_POW_BLOCK_HASH: &str = "is_valid_terminal_pow_block_hash"; pub const LOCAL: &str = "local"; pub const BUILDER: &str = "builder"; pub const SUCCESS: &str = "success"; diff --git a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs index 8591359f15..1743b340ab 100644 --- a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs +++ b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs @@ -28,8 +28,6 @@ use types::{ Transactions, Uint256, }; -use super::DEFAULT_TERMINAL_BLOCK; - const TEST_BLOB_BUNDLE: &[u8] = include_bytes!("fixtures/mainnet/test_blobs_bundle.ssz"); const TEST_BLOB_BUNDLE_V2: &[u8] = include_bytes!("fixtures/mainnet/test_blobs_bundle_v2.ssz"); @@ -172,9 +170,6 @@ fn make_rng() -> Arc<Mutex<StdRng>> { impl<E: EthSpec> ExecutionBlockGenerator<E> { #[allow(clippy::too_many_arguments)] pub fn new( - terminal_total_difficulty: Uint256, - terminal_block_number: u64, - terminal_block_hash: ExecutionBlockHash, shanghai_time: Option<u64>, cancun_time: Option<u64>, prague_time: Option<u64>, @@ -187,9 +182,9 @@ impl<E: EthSpec> ExecutionBlockGenerator<E> { finalized_block_hash: <_>::default(), blocks: <_>::default(), block_hashes: <_>::default(), - terminal_total_difficulty, - terminal_block_number, - terminal_block_hash, + terminal_total_difficulty: Default::default(), + terminal_block_number: 0, + terminal_block_hash: Default::default(), pending_payloads: <_>::default(), next_payload_id: 0, payload_ids: <_>::default(), @@ -293,25 +288,6 @@ impl<E: EthSpec> ExecutionBlockGenerator<E> { .and_then(|block| block.as_execution_payload()) } - pub fn move_to_block_prior_to_terminal_block(&mut self) -> Result<(), String> { - let target_block = self - .terminal_block_number - .checked_sub(1) - .ok_or("terminal pow block is 0")?; - self.move_to_pow_block(target_block) - } - - pub fn move_to_terminal_block(&mut self) -> Result<(), String> { - self.move_to_pow_block(self.terminal_block_number) - } - - pub fn move_to_pow_block(&mut self, target_block: u64) -> Result<(), String> { - let next_block = self.latest_block().unwrap().block_number() + 1; - assert!(target_block >= next_block); - - self.insert_pow_blocks(next_block..=target_block) - } - pub fn drop_all_blocks(&mut self) { self.blocks = <_>::default(); self.block_hashes = <_>::default(); @@ -879,27 +855,22 @@ fn payload_id_from_u64(n: u64) -> PayloadId { n.to_le_bytes() } -pub fn generate_genesis_header<E: EthSpec>( - spec: &ChainSpec, - post_transition_merge: bool, -) -> Option<ExecutionPayloadHeader<E>> { +pub fn generate_genesis_header<E: EthSpec>(spec: &ChainSpec) -> Option<ExecutionPayloadHeader<E>> { let genesis_fork = spec.fork_name_at_slot::<E>(spec.genesis_slot); - let genesis_block_hash = - generate_genesis_block(spec.terminal_total_difficulty, DEFAULT_TERMINAL_BLOCK) - .ok() - .map(|block| block.block_hash); + let genesis_block_hash = generate_genesis_block(Default::default(), 0) + .ok() + .map(|block| block.block_hash); let empty_transactions_root = Transactions::<E>::empty().tree_hash_root(); match genesis_fork { - ForkName::Base | ForkName::Altair => None, + ForkName::Base | ForkName::Altair => { + // Pre-Bellatrix forks have no execution payload + None + } ForkName::Bellatrix => { - if post_transition_merge { - let mut header = ExecutionPayloadHeader::Bellatrix(<_>::default()); - *header.block_hash_mut() = genesis_block_hash.unwrap_or_default(); - *header.transactions_root_mut() = empty_transactions_root; - Some(header) - } else { - Some(ExecutionPayloadHeader::<E>::Bellatrix(<_>::default())) - } + let mut header = ExecutionPayloadHeader::Bellatrix(<_>::default()); + *header.block_hash_mut() = genesis_block_hash.unwrap_or_default(); + *header.transactions_root_mut() = empty_transactions_root; + Some(header) } ForkName::Capella => { let mut header = ExecutionPayloadHeader::Capella(<_>::default()); @@ -985,70 +956,6 @@ mod test { use kzg::{Bytes48, CellRef, KzgBlobRef, trusted_setup::get_trusted_setup}; use types::{MainnetEthSpec, MinimalEthSpec}; - #[test] - fn pow_chain_only() { - const TERMINAL_DIFFICULTY: u64 = 10; - const TERMINAL_BLOCK: u64 = 10; - const DIFFICULTY_INCREMENT: u64 = 1; - - let mut generator: ExecutionBlockGenerator<MainnetEthSpec> = ExecutionBlockGenerator::new( - Uint256::from(TERMINAL_DIFFICULTY), - TERMINAL_BLOCK, - ExecutionBlockHash::zero(), - None, - None, - None, - None, - None, - None, - ); - - for i in 0..=TERMINAL_BLOCK { - if i > 0 { - generator.insert_pow_block(i).unwrap(); - } - - /* - * Generate a block, inspect it. - */ - - let block = generator.latest_block().unwrap(); - assert_eq!(block.block_number(), i); - - let expected_parent = i - .checked_sub(1) - .map(|i| generator.block_by_number(i).unwrap().block_hash()) - .unwrap_or_else(ExecutionBlockHash::zero); - assert_eq!(block.parent_hash(), expected_parent); - - assert_eq!( - block.total_difficulty().unwrap(), - Uint256::from(i * DIFFICULTY_INCREMENT) - ); - - assert_eq!(generator.block_by_hash(block.block_hash()).unwrap(), block); - assert_eq!(generator.block_by_number(i).unwrap(), block); - - /* - * Check the parent is accessible. - */ - - if let Some(prev_i) = i.checked_sub(1) { - assert_eq!( - generator.block_by_number(prev_i).unwrap(), - generator.block_by_hash(block.parent_hash()).unwrap() - ); - } - - /* - * Check the next block is inaccessible. - */ - - let next_i = i + 1; - assert!(generator.block_by_number(next_i).is_none()); - } - } - #[test] fn valid_test_blobs_bundle_v1() { assert!( diff --git a/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs b/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs index c69edb8f39..91966ff65e 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs @@ -1,9 +1,4 @@ -use crate::{ - test_utils::{ - DEFAULT_JWT_SECRET, DEFAULT_TERMINAL_BLOCK, DEFAULT_TERMINAL_DIFFICULTY, MockServer, - }, - *, -}; +use crate::{test_utils::DEFAULT_JWT_SECRET, test_utils::MockServer, *}; use alloy_primitives::B256 as H256; use fixed_bytes::FixedBytesExtended; use kzg::Kzg; @@ -20,12 +15,10 @@ pub struct MockExecutionLayer<E: EthSpec> { impl<E: EthSpec> MockExecutionLayer<E> { pub fn default_params(executor: TaskExecutor) -> Self { let mut spec = MainnetEthSpec::default_spec(); - spec.terminal_total_difficulty = Uint256::from(DEFAULT_TERMINAL_DIFFICULTY); spec.terminal_block_hash = ExecutionBlockHash::zero(); spec.terminal_block_hash_activation_epoch = Epoch::new(0); Self::new( executor, - DEFAULT_TERMINAL_BLOCK, None, None, None, @@ -40,7 +33,6 @@ impl<E: EthSpec> MockExecutionLayer<E> { #[allow(clippy::too_many_arguments)] pub fn new( executor: TaskExecutor, - terminal_block: u64, shanghai_time: Option<u64>, cancun_time: Option<u64>, prague_time: Option<u64>, @@ -56,9 +48,6 @@ impl<E: EthSpec> MockExecutionLayer<E> { let server = MockServer::new( &handle, jwt_key, - spec.terminal_total_difficulty, - terminal_block, - spec.terminal_block_hash, shanghai_time, cancun_time, prague_time, @@ -293,53 +282,4 @@ impl<E: EthSpec> MockExecutionLayer<E> { assert_eq!(head_execution_block.block_hash(), block_hash); assert_eq!(head_execution_block.parent_hash(), parent_hash); } - - pub fn move_to_block_prior_to_terminal_block(self) -> Self { - self.server - .execution_block_generator() - .move_to_block_prior_to_terminal_block() - .unwrap(); - self - } - - pub fn move_to_terminal_block(self) -> Self { - self.server - .execution_block_generator() - .move_to_terminal_block() - .unwrap(); - self - } - - pub fn produce_forked_pow_block(self) -> (Self, ExecutionBlockHash) { - let head_block = self - .server - .execution_block_generator() - .latest_block() - .unwrap(); - - let block_hash = self - .server - .execution_block_generator() - .insert_pow_block_by_hash(head_block.parent_hash(), 1) - .unwrap(); - (self, block_hash) - } - - pub async fn with_terminal_block<U, V>(self, func: U) -> Self - where - U: Fn(Arc<ChainSpec>, ExecutionLayer<E>, Option<ExecutionBlock>) -> V, - V: Future<Output = ()>, - { - let terminal_block_number = self - .server - .execution_block_generator() - .terminal_block_number; - let terminal_block = self - .server - .execution_block_generator() - .execution_block_by_number(terminal_block_number); - - func(self.spec.clone(), self.el.clone(), terminal_block).await; - self - } } diff --git a/beacon_node/execution_layer/src/test_utils/mod.rs b/beacon_node/execution_layer/src/test_utils/mod.rs index 2465a41d8b..d8e1e70e49 100644 --- a/beacon_node/execution_layer/src/test_utils/mod.rs +++ b/beacon_node/execution_layer/src/test_utils/mod.rs @@ -35,8 +35,6 @@ pub use hook::Hook; pub use mock_builder::{MockBuilder, Operation, mock_builder_extra_data}; pub use mock_execution_layer::MockExecutionLayer; -pub const DEFAULT_TERMINAL_DIFFICULTY: u64 = 6400; -pub const DEFAULT_TERMINAL_BLOCK: u64 = 64; pub const DEFAULT_JWT_SECRET: [u8; 32] = [42; 32]; pub const DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI: u128 = 10_000_000_000_000_000; pub const DEFAULT_BUILDER_PAYLOAD_VALUE_WEI: u128 = 20_000_000_000_000_000; @@ -79,9 +77,6 @@ mod mock_execution_layer; pub struct MockExecutionConfig { pub server_config: Config, pub jwt_key: JwtKey, - pub terminal_difficulty: Uint256, - pub terminal_block: u64, - pub terminal_block_hash: ExecutionBlockHash, pub shanghai_time: Option<u64>, pub cancun_time: Option<u64>, pub prague_time: Option<u64>, @@ -93,9 +88,6 @@ impl Default for MockExecutionConfig { fn default() -> Self { Self { jwt_key: JwtKey::random(), - terminal_difficulty: Uint256::from(DEFAULT_TERMINAL_DIFFICULTY), - terminal_block: DEFAULT_TERMINAL_BLOCK, - terminal_block_hash: ExecutionBlockHash::zero(), server_config: Config::default(), shanghai_time: None, cancun_time: None, @@ -118,9 +110,6 @@ impl<E: EthSpec> MockServer<E> { Self::new( &runtime::Handle::current(), JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap(), - Uint256::from(DEFAULT_TERMINAL_DIFFICULTY), - DEFAULT_TERMINAL_BLOCK, - ExecutionBlockHash::zero(), None, // FIXME(capella): should this be the default? None, // FIXME(deneb): should this be the default? None, // FIXME(electra): should this be the default? @@ -138,9 +127,6 @@ impl<E: EthSpec> MockServer<E> { create_test_tracing_subscriber(); let MockExecutionConfig { jwt_key, - terminal_difficulty, - terminal_block, - terminal_block_hash, server_config, shanghai_time, cancun_time, @@ -151,9 +137,6 @@ impl<E: EthSpec> MockServer<E> { let last_echo_request = Arc::new(RwLock::new(None)); let preloaded_responses = Arc::new(Mutex::new(vec![])); let execution_block_generator = ExecutionBlockGenerator::new( - terminal_difficulty, - terminal_block, - terminal_block_hash, shanghai_time, cancun_time, prague_time, @@ -215,9 +198,6 @@ impl<E: EthSpec> MockServer<E> { pub fn new( handle: &runtime::Handle, jwt_key: JwtKey, - terminal_difficulty: Uint256, - terminal_block: u64, - terminal_block_hash: ExecutionBlockHash, shanghai_time: Option<u64>, cancun_time: Option<u64>, prague_time: Option<u64>, @@ -230,9 +210,6 @@ impl<E: EthSpec> MockServer<E> { MockExecutionConfig { server_config: Config::default(), jwt_key, - terminal_difficulty, - terminal_block, - terminal_block_hash, shanghai_time, cancun_time, prague_time, diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 92a1ad934d..74710c4ed2 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -93,7 +93,7 @@ use tokio_stream::{ use tracing::{debug, info, warn}; use types::{ BeaconStateError, Checkpoint, ConfigAndPreset, Epoch, EthSpec, ForkName, Hash256, - SignedBlindedBeaconBlock, Slot, + SignedBlindedBeaconBlock, }; use validator::execution_payload_envelope::get_validator_execution_payload_envelope; use version::{ @@ -3126,25 +3126,6 @@ pub fn serve<T: BeaconChainTypes>( }, ); - // GET lighthouse/merge_readiness - let get_lighthouse_merge_readiness = warp::path("lighthouse") - .and(warp::path("merge_readiness")) - .and(warp::path::end()) - .and(task_spawner_filter.clone()) - .and(chain_filter.clone()) - .then( - |task_spawner: TaskSpawner<T::EthSpec>, chain: Arc<BeaconChain<T>>| { - task_spawner.spawn_async_with_rejection(Priority::P1, async move { - let current_slot = chain.slot_clock.now_or_genesis().unwrap_or(Slot::new(0)); - let merge_readiness = chain.check_bellatrix_readiness(current_slot).await; - Ok::<_, warp::reject::Rejection>( - warp::reply::json(&api_types::GenericResponse::from(merge_readiness)) - .into_response(), - ) - }) - }, - ); - let get_events = eth_v1 .clone() .and(warp::path("events")) @@ -3388,7 +3369,6 @@ pub fn serve<T: BeaconChainTypes>( .uor(get_beacon_light_client_bootstrap) .uor(get_beacon_light_client_updates) .uor(get_lighthouse_block_packing_efficiency) - .uor(get_lighthouse_merge_readiness) .uor(get_events) .uor(get_expected_withdrawals) .uor(lighthouse_log_events.boxed()) diff --git a/beacon_node/http_api/tests/broadcast_validation_tests.rs b/beacon_node/http_api/tests/broadcast_validation_tests.rs index ef5c508595..a380f62ecf 100644 --- a/beacon_node/http_api/tests/broadcast_validation_tests.rs +++ b/beacon_node/http_api/tests/broadcast_validation_tests.rs @@ -85,14 +85,18 @@ pub async fn gossip_invalid() { /* mandated by Beacon API spec */ assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); + // The error depends on whether blobs exist (which affects validation order): + // - Pre-Deneb (no blobs): block validation runs first -> NotFinalizedDescendant + // - Deneb/Electra (blobs): blob validation runs first -> ParentUnknown + // - Fulu+ (columns): block validation runs first -> NotFinalizedDescendant let pre_finalized_block_root = Hash256::zero(); - let expected_error_msg = if tester.harness.spec.is_fulu_scheduled() { + let expected_error_msg = if tester.harness.spec.deneb_fork_epoch.is_none() + || tester.harness.spec.is_fulu_scheduled() + { format!( "BAD_REQUEST: NotFinalizedDescendant {{ block_parent_root: {pre_finalized_block_root:?} }}" ) } else { - // Since Deneb, the invalidity of the blobs will be detected prior to the invalidity of the - // block. format!("BAD_REQUEST: ParentUnknown {{ parent_root: {pre_finalized_block_root:?} }}") }; @@ -283,13 +287,13 @@ pub async fn consensus_invalid() { assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); let pre_finalized_block_root = Hash256::zero(); - let expected_error_msg = if tester.harness.spec.is_fulu_scheduled() { + let expected_error_msg = if tester.harness.spec.deneb_fork_epoch.is_none() + || tester.harness.spec.is_fulu_scheduled() + { format!( "BAD_REQUEST: NotFinalizedDescendant {{ block_parent_root: {pre_finalized_block_root:?} }}" ) } else { - // Since Deneb, the invalidity of the blobs will be detected prior to the invalidity of the - // block. format!("BAD_REQUEST: ParentUnknown {{ parent_root: {pre_finalized_block_root:?} }}") }; @@ -520,13 +524,13 @@ pub async fn equivocation_invalid() { assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); let pre_finalized_block_root = Hash256::zero(); - let expected_error_msg = if tester.harness.spec.is_fulu_scheduled() { + let expected_error_msg = if tester.harness.spec.deneb_fork_epoch.is_none() + || tester.harness.spec.is_fulu_scheduled() + { format!( "BAD_REQUEST: NotFinalizedDescendant {{ block_parent_root: {pre_finalized_block_root:?} }}" ) } else { - // Since Deneb, the invalidity of the blobs will be detected prior to the invalidity of the - // block. format!("BAD_REQUEST: ParentUnknown {{ parent_root: {pre_finalized_block_root:?} }}") }; @@ -845,16 +849,17 @@ pub async fn blinded_gossip_invalid() { assert!(response.is_err()); let error_response: eth2::Error = response.err().unwrap(); + /* mandated by Beacon API spec */ assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); let pre_finalized_block_root = Hash256::zero(); - let expected_error_msg = if tester.harness.spec.is_fulu_scheduled() { + let expected_error_msg = if tester.harness.spec.deneb_fork_epoch.is_none() + || tester.harness.spec.is_fulu_scheduled() + { format!( "BAD_REQUEST: NotFinalizedDescendant {{ block_parent_root: {pre_finalized_block_root:?} }}" ) } else { - // Since Deneb, the invalidity of the blobs will be detected prior to the invalidity of the - // block. format!("BAD_REQUEST: ParentUnknown {{ parent_root: {pre_finalized_block_root:?} }}") }; @@ -1070,10 +1075,16 @@ pub async fn blinded_consensus_invalid() { ); } else { assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); - assert_server_message_error( - error_response, - format!("BAD_REQUEST: ParentUnknown {{ parent_root: {pre_finalized_block_root:?} }}"), - ); + let expected_error_msg = if tester.harness.spec.deneb_fork_epoch.is_none() + || tester.harness.spec.is_fulu_scheduled() + { + format!( + "BAD_REQUEST: NotFinalizedDescendant {{ block_parent_root: {pre_finalized_block_root:?} }}" + ) + } else { + format!("BAD_REQUEST: ParentUnknown {{ parent_root: {pre_finalized_block_root:?} }}") + }; + assert_server_message_error(error_response, expected_error_msg); } } @@ -1253,10 +1264,16 @@ pub async fn blinded_equivocation_invalid() { ); } else { assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); - assert_server_message_error( - error_response, - format!("BAD_REQUEST: ParentUnknown {{ parent_root: {pre_finalized_block_root:?} }}"), - ); + let expected_error_msg = if tester.harness.spec.deneb_fork_epoch.is_none() + || tester.harness.spec.is_fulu_scheduled() + { + format!( + "BAD_REQUEST: NotFinalizedDescendant {{ block_parent_root: {pre_finalized_block_root:?} }}" + ) + } else { + format!("BAD_REQUEST: ParentUnknown {{ parent_root: {pre_finalized_block_root:?} }}") + }; + assert_server_message_error(error_response, expected_error_msg); } } @@ -1957,6 +1974,13 @@ pub async fn duplicate_block_status_code() { let validator_count = 64; let num_initial: u64 = 31; let duplicate_block_status_code = StatusCode::IM_A_TEAPOT; + + // Check if deneb is enabled, which is required for blobs. + let spec = test_spec::<E>(); + if !spec.fork_name_at_slot::<E>(Slot::new(0)).deneb_enabled() { + return; + } + let tester = InteractiveTester::<E>::new_with_initializer_and_mutator( None, validator_count, diff --git a/beacon_node/http_api/tests/fork_tests.rs b/beacon_node/http_api/tests/fork_tests.rs index b96c8bd112..4ba35c238c 100644 --- a/beacon_node/http_api/tests/fork_tests.rs +++ b/beacon_node/http_api/tests/fork_tests.rs @@ -404,7 +404,7 @@ async fn bls_to_execution_changes_update_all_around_capella_fork() { bls_withdrawal_credentials(&keypair.pk, spec) } - let header = generate_genesis_header(&spec, true); + let header = generate_genesis_header(&spec); let genesis_state = InteropGenesisBuilder::new() .set_opt_execution_payload_header(header) diff --git a/beacon_node/http_api/tests/interactive_tests.rs b/beacon_node/http_api/tests/interactive_tests.rs index 21458057c4..a18dd10464 100644 --- a/beacon_node/http_api/tests/interactive_tests.rs +++ b/beacon_node/http_api/tests/interactive_tests.rs @@ -450,13 +450,7 @@ pub async fn proposer_boost_re_org_test( let execution_ctx = mock_el.server.ctx.clone(); let slot_clock = &harness.chain.slot_clock; - // Move to terminal block. mock_el.server.all_payloads_valid(); - execution_ctx - .execution_block_generator - .write() - .move_to_terminal_block() - .unwrap(); // Send proposer preparation data for all validators. let proposer_preparation_data = all_validators diff --git a/beacon_node/http_api/tests/status_tests.rs b/beacon_node/http_api/tests/status_tests.rs index 6bca9e51f6..791e643ec4 100644 --- a/beacon_node/http_api/tests/status_tests.rs +++ b/beacon_node/http_api/tests/status_tests.rs @@ -21,15 +21,8 @@ async fn post_merge_tester(chain_depth: u64, validator_count: u64) -> Interactiv let tester = InteractiveTester::<E>::new(Some(spec), validator_count as usize).await; let harness = &tester.harness; let mock_el = harness.mock_execution_layer.as_ref().unwrap(); - let execution_ctx = mock_el.server.ctx.clone(); - // Move to terminal block. mock_el.server.all_payloads_valid(); - execution_ctx - .execution_block_generator - .write() - .move_to_terminal_block() - .unwrap(); // Create some chain depth. harness.advance_slot(); diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 7e3eb8b980..6696e109a5 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -147,15 +147,6 @@ impl ApiTester { .node_custody_type(config.node_custody_type) .build(); - harness - .mock_execution_layer - .as_ref() - .unwrap() - .server - .execution_block_generator() - .move_to_terminal_block() - .unwrap(); - harness.advance_slot(); for _ in 0..CHAIN_LENGTH { diff --git a/beacon_node/operation_pool/src/lib.rs b/beacon_node/operation_pool/src/lib.rs index 81423d6abd..b3bd091691 100644 --- a/beacon_node/operation_pool/src/lib.rs +++ b/beacon_node/operation_pool/src/lib.rs @@ -1851,8 +1851,11 @@ mod release_tests { let mut spec = E::default_spec(); // Give some room to sign surround slashings. - spec.altair_fork_epoch = Some(Epoch::new(3)); - spec.bellatrix_fork_epoch = Some(Epoch::new(6)); + spec.altair_fork_epoch = Some(Epoch::new(0)); + spec.bellatrix_fork_epoch = Some(Epoch::new(0)); + spec.capella_fork_epoch = Some(Epoch::new(0)); + spec.deneb_fork_epoch = Some(Epoch::new(2)); + spec.electra_fork_epoch = Some(Epoch::new(4)); // To make exits immediately valid. spec.shard_committee_period = 0; @@ -1860,185 +1863,114 @@ mod release_tests { let num_validators = 32; let harness = get_harness::<E>(num_validators, Some(spec.clone())); + if let Some(mock_el) = harness.mock_execution_layer.as_ref() { + mock_el.server.all_payloads_valid(); + } (harness, spec) } - /// Test several cross-fork voluntary exits: - /// - /// - phase0 exit (not valid after Bellatrix) - /// - phase0 exit signed with Altair fork version (only valid after Bellatrix) - #[tokio::test] - async fn cross_fork_exits() { - let (harness, spec) = cross_fork_harness::<MainnetEthSpec>(); - let altair_fork_epoch = spec.altair_fork_epoch.unwrap(); - let bellatrix_fork_epoch = spec.bellatrix_fork_epoch.unwrap(); - let slots_per_epoch = MainnetEthSpec::slots_per_epoch(); - - let op_pool = OperationPool::<MainnetEthSpec>::new(); - - // Sign an exit in phase0 with a phase0 epoch. - let exit1 = harness.make_voluntary_exit(0, Epoch::new(0)); - - // Advance to Altair. - harness - .extend_to_slot(altair_fork_epoch.start_slot(slots_per_epoch)) - .await; - let altair_head = harness.chain.canonical_head.cached_head().snapshot; - assert_eq!(altair_head.beacon_state.current_epoch(), altair_fork_epoch); - - // Add exit 1 to the op pool during Altair. It's still valid at this point and should be - // returned. - let verified_exit1 = exit1 - .clone() - .validate(&altair_head.beacon_state, &harness.chain.spec) - .unwrap(); - op_pool.insert_voluntary_exit(verified_exit1); - let exits = - op_pool.get_voluntary_exits(&altair_head.beacon_state, |_| true, &harness.chain.spec); - assert!(exits.contains(&exit1)); - assert_eq!(exits.len(), 1); - - // Advance to Bellatrix. - harness - .extend_to_slot(bellatrix_fork_epoch.start_slot(slots_per_epoch)) - .await; - let bellatrix_head = harness.chain.canonical_head.cached_head().snapshot; - assert_eq!( - bellatrix_head.beacon_state.current_epoch(), - bellatrix_fork_epoch - ); - - // Sign an exit with the Altair domain and a phase0 epoch. This is a weird type of exit - // that is valid because after the Bellatrix fork we'll use the Altair fork domain to verify - // all prior epochs. - let unsigned_exit = VoluntaryExit { - epoch: Epoch::new(0), - validator_index: 2, - }; - let exit2 = SignedVoluntaryExit { - message: unsigned_exit.clone(), - signature: harness.validator_keypairs[2] - .sk - .sign(unsigned_exit.signing_root(spec.compute_domain( - Domain::VoluntaryExit, - harness.spec.altair_fork_version, - harness.chain.genesis_validators_root, - ))), - }; - - let verified_exit2 = exit2 - .clone() - .validate(&bellatrix_head.beacon_state, &harness.chain.spec) - .unwrap(); - op_pool.insert_voluntary_exit(verified_exit2); - - // Attempting to fetch exit1 now should fail, despite it still being in the pool. - // exit2 should still be valid, because it was signed with the Altair fork domain. - assert_eq!(op_pool.voluntary_exits.read().len(), 2); - let exits = - op_pool.get_voluntary_exits(&bellatrix_head.beacon_state, |_| true, &harness.spec); - assert_eq!(&exits, &[exit2]); - } + // Voluntary exits signed post-Capella are perpetually valid across forks, so no + // cross-fork test is required here. /// Test several cross-fork proposer slashings: /// - /// - phase0 slashing (not valid after Bellatrix) - /// - Bellatrix signed with Altair fork version (not valid after Bellatrix) - /// - phase0 exit signed with Altair fork version (only valid after Bellatrix) + /// - Capella slashing (not valid after Electra) + /// - Electra signed with Deneb fork version (not valid after Electra) + /// - Capella exit signed with Deneb fork version (only valid after Electra) #[tokio::test] async fn cross_fork_proposer_slashings() { let (harness, spec) = cross_fork_harness::<MainnetEthSpec>(); let slots_per_epoch = MainnetEthSpec::slots_per_epoch(); - let altair_fork_epoch = spec.altair_fork_epoch.unwrap(); - let bellatrix_fork_epoch = spec.bellatrix_fork_epoch.unwrap(); - let bellatrix_fork_slot = bellatrix_fork_epoch.start_slot(slots_per_epoch); + let deneb_fork_epoch = spec.deneb_fork_epoch.unwrap(); + let electra_fork_epoch = spec.electra_fork_epoch.unwrap(); + let electra_fork_slot = electra_fork_epoch.start_slot(slots_per_epoch); let op_pool = OperationPool::<MainnetEthSpec>::new(); - // Sign a proposer slashing in phase0 with a phase0 epoch. + // Sign a proposer slashing in Capella with a Capella slot. let slashing1 = harness.make_proposer_slashing_at_slot(0, Some(Slot::new(1))); - // Advance to Altair. + // Advance to Deneb. harness - .extend_to_slot(altair_fork_epoch.start_slot(slots_per_epoch)) + .extend_to_slot(deneb_fork_epoch.start_slot(slots_per_epoch)) .await; - let altair_head = harness.chain.canonical_head.cached_head().snapshot; - assert_eq!(altair_head.beacon_state.current_epoch(), altair_fork_epoch); + let deneb_head = harness.chain.canonical_head.cached_head().snapshot; + assert_eq!(deneb_head.beacon_state.current_epoch(), deneb_fork_epoch); - // Add slashing1 to the op pool during Altair. It's still valid at this point and should be + // Add slashing1 to the op pool during Deneb. It's still valid at this point and should be // returned. let verified_slashing1 = slashing1 .clone() - .validate(&altair_head.beacon_state, &harness.chain.spec) + .validate(&deneb_head.beacon_state, &harness.chain.spec) .unwrap(); op_pool.insert_proposer_slashing(verified_slashing1); let (proposer_slashings, _, _) = - op_pool.get_slashings_and_exits(&altair_head.beacon_state, &harness.chain.spec); + op_pool.get_slashings_and_exits(&deneb_head.beacon_state, &harness.chain.spec); assert!(proposer_slashings.contains(&slashing1)); assert_eq!(proposer_slashings.len(), 1); - // Sign a proposer slashing with a Bellatrix slot using the Altair fork domain. + // Sign a proposer slashing with a Electra slot using the Deneb fork domain. // - // This slashing is valid only before the Bellatrix fork epoch. - let slashing2 = harness.make_proposer_slashing_at_slot(1, Some(bellatrix_fork_slot)); + // This slashing is valid only before the Electra fork epoch. + let slashing2 = harness.make_proposer_slashing_at_slot(1, Some(electra_fork_slot)); let verified_slashing2 = slashing2 .clone() - .validate(&altair_head.beacon_state, &harness.chain.spec) + .validate(&deneb_head.beacon_state, &harness.chain.spec) .unwrap(); op_pool.insert_proposer_slashing(verified_slashing2); let (proposer_slashings, _, _) = - op_pool.get_slashings_and_exits(&altair_head.beacon_state, &harness.chain.spec); + op_pool.get_slashings_and_exits(&deneb_head.beacon_state, &harness.chain.spec); assert!(proposer_slashings.contains(&slashing1)); assert!(proposer_slashings.contains(&slashing2)); assert_eq!(proposer_slashings.len(), 2); - // Advance to Bellatrix. - harness.extend_to_slot(bellatrix_fork_slot).await; - let bellatrix_head = harness.chain.canonical_head.cached_head().snapshot; + // Advance to Electra. + harness.extend_to_slot(electra_fork_slot).await; + let electra_head = harness.chain.canonical_head.cached_head().snapshot; assert_eq!( - bellatrix_head.beacon_state.current_epoch(), - bellatrix_fork_epoch + electra_head.beacon_state.current_epoch(), + electra_fork_epoch ); - // Sign a proposer slashing with the Altair domain and a phase0 slot. This is a weird type - // of slashing that is only valid after the Bellatrix fork because we'll use the Altair fork + // Sign a proposer slashing with the Deneb domain and a Capella slot. This is a weird type + // of slashing that is only valid after the Electra fork because we'll use the Deneb fork // domain to verify all prior epochs. let slashing3 = harness.make_proposer_slashing_at_slot(2, Some(Slot::new(1))); let verified_slashing3 = slashing3 .clone() - .validate(&bellatrix_head.beacon_state, &harness.chain.spec) + .validate(&electra_head.beacon_state, &harness.chain.spec) .unwrap(); op_pool.insert_proposer_slashing(verified_slashing3); // Attempting to fetch slashing1 now should fail, despite it still being in the pool. // Likewise slashing2 is also invalid now because it should be signed with the - // Bellatrix fork version. - // slashing3 should still be valid, because it was signed with the Altair fork domain. + // Electra fork version. + // slashing3 should still be valid, because it was signed with the Deneb fork domain. assert_eq!(op_pool.proposer_slashings.read().len(), 3); let (proposer_slashings, _, _) = - op_pool.get_slashings_and_exits(&bellatrix_head.beacon_state, &harness.spec); + op_pool.get_slashings_and_exits(&electra_head.beacon_state, &harness.spec); assert!(proposer_slashings.contains(&slashing3)); assert_eq!(proposer_slashings.len(), 1); } /// Test several cross-fork attester slashings: /// - /// - both target epochs in phase0 (not valid after Bellatrix) - /// - both target epochs in Bellatrix but signed with Altair domain (not valid after Bellatrix) - /// - Altair attestation that surrounds a phase0 attestation (not valid after Bellatrix) - /// - both target epochs in phase0 but signed with Altair domain (only valid after Bellatrix) + /// - both target epochs in Capella (not valid after Electra) + /// - both target epochs in Electra but signed with Deneb domain (not valid after Electra) + /// - Deneb attestation that surrounds a Capella attestation (not valid after Electra) + /// - both target epochs in Capella but signed with Deneb domain (only valid after Electra) #[tokio::test] async fn cross_fork_attester_slashings() { let (harness, spec) = cross_fork_harness::<MainnetEthSpec>(); let slots_per_epoch = MainnetEthSpec::slots_per_epoch(); let zero_epoch = Epoch::new(0); - let altair_fork_epoch = spec.altair_fork_epoch.unwrap(); - let bellatrix_fork_epoch = spec.bellatrix_fork_epoch.unwrap(); - let bellatrix_fork_slot = bellatrix_fork_epoch.start_slot(slots_per_epoch); + let deneb_fork_epoch = spec.deneb_fork_epoch.unwrap(); + let electra_fork_epoch = spec.electra_fork_epoch.unwrap(); + let electra_fork_slot = electra_fork_epoch.start_slot(slots_per_epoch); let op_pool = OperationPool::<MainnetEthSpec>::new(); - // Sign an attester slashing with the phase0 fork version, with both target epochs in phase0. + // Sign an attester slashing with the Capella fork version, with both target epochs in Capella. let slashing1 = harness.make_attester_slashing_with_epochs( vec![0], None, @@ -2047,55 +1979,55 @@ mod release_tests { Some(zero_epoch), ); - // Advance to Altair. + // Advance to Deneb. harness - .extend_to_slot(altair_fork_epoch.start_slot(slots_per_epoch)) + .extend_to_slot(deneb_fork_epoch.start_slot(slots_per_epoch)) .await; - let altair_head = harness.chain.canonical_head.cached_head().snapshot; - assert_eq!(altair_head.beacon_state.current_epoch(), altair_fork_epoch); + let deneb_head = harness.chain.canonical_head.cached_head().snapshot; + assert_eq!(deneb_head.beacon_state.current_epoch(), deneb_fork_epoch); - // Add slashing1 to the op pool during Altair. It's still valid at this point and should be + // Add slashing1 to the op pool during Deneb. It's still valid at this point and should be // returned. let verified_slashing1 = slashing1 .clone() - .validate(&altair_head.beacon_state, &harness.chain.spec) + .validate(&deneb_head.beacon_state, &harness.chain.spec) .unwrap(); op_pool.insert_attester_slashing(verified_slashing1); - // Sign an attester slashing with two Bellatrix epochs using the Altair fork domain. + // Sign an attester slashing with two Electra epochs using the Deneb fork domain. // - // This slashing is valid only before the Bellatrix fork epoch. + // This slashing is valid only before the Electra fork epoch. let slashing2 = harness.make_attester_slashing_with_epochs( vec![1], None, - Some(bellatrix_fork_epoch), + Some(electra_fork_epoch), None, - Some(bellatrix_fork_epoch), + Some(electra_fork_epoch), ); let verified_slashing2 = slashing2 .clone() - .validate(&altair_head.beacon_state, &harness.chain.spec) + .validate(&deneb_head.beacon_state, &harness.chain.spec) .unwrap(); op_pool.insert_attester_slashing(verified_slashing2); let (_, attester_slashings, _) = - op_pool.get_slashings_and_exits(&altair_head.beacon_state, &harness.chain.spec); + op_pool.get_slashings_and_exits(&deneb_head.beacon_state, &harness.chain.spec); assert!(attester_slashings.contains(&slashing1)); assert!(attester_slashings.contains(&slashing2)); assert_eq!(attester_slashings.len(), 2); - // Sign an attester slashing where an Altair attestation surrounds a phase0 one. + // Sign an attester slashing where a Deneb attestation surrounds a Capella one. // - // This slashing is valid only before the Bellatrix fork epoch. + // This slashing is valid only before the Electra fork epoch. let slashing3 = harness.make_attester_slashing_with_epochs( vec![2], Some(Epoch::new(0)), - Some(altair_fork_epoch), + Some(deneb_fork_epoch), Some(Epoch::new(1)), - Some(altair_fork_epoch - 1), + Some(deneb_fork_epoch - 1), ); let verified_slashing3 = slashing3 .clone() - .validate(&altair_head.beacon_state, &harness.chain.spec) + .validate(&deneb_head.beacon_state, &harness.chain.spec) .unwrap(); op_pool.insert_attester_slashing(verified_slashing3); @@ -2104,44 +2036,43 @@ mod release_tests { // slashed. let mut to_be_slashed = hashset! {0}; let attester_slashings = - op_pool.get_attester_slashings(&altair_head.beacon_state, &mut to_be_slashed); + op_pool.get_attester_slashings(&deneb_head.beacon_state, &mut to_be_slashed); assert!(attester_slashings.contains(&slashing2)); assert!(attester_slashings.contains(&slashing3)); assert_eq!(attester_slashings.len(), 2); - // Advance to Bellatrix. - harness.extend_to_slot(bellatrix_fork_slot).await; - let bellatrix_head = harness.chain.canonical_head.cached_head().snapshot; + // Advance to Electra + harness.extend_to_slot(electra_fork_slot).await; + let electra_head = harness.chain.canonical_head.cached_head().snapshot; assert_eq!( - bellatrix_head.beacon_state.current_epoch(), - bellatrix_fork_epoch + electra_head.beacon_state.current_epoch(), + electra_fork_epoch ); - // Sign an attester slashing with the Altair domain and phase0 epochs. This is a weird type - // of slashing that is only valid after the Bellatrix fork because we'll use the Altair fork - // domain to verify all prior epochs. + // Sign an attester slashing with the Deneb domain and Capella epochs. This is only valid + // after the Electra fork. let slashing4 = harness.make_attester_slashing_with_epochs( vec![3], Some(Epoch::new(0)), - Some(altair_fork_epoch - 1), + Some(deneb_fork_epoch - 1), Some(Epoch::new(0)), - Some(altair_fork_epoch - 1), + Some(deneb_fork_epoch - 1), ); let verified_slashing4 = slashing4 .clone() - .validate(&bellatrix_head.beacon_state, &harness.chain.spec) + .validate(&electra_head.beacon_state, &harness.chain.spec) .unwrap(); op_pool.insert_attester_slashing(verified_slashing4); // All slashings except slashing4 are now invalid (despite being present in the pool). assert_eq!(op_pool.attester_slashings.read().len(), 4); let (_, attester_slashings, _) = - op_pool.get_slashings_and_exits(&bellatrix_head.beacon_state, &harness.spec); + op_pool.get_slashings_and_exits(&electra_head.beacon_state, &harness.spec); assert!(attester_slashings.contains(&slashing4)); assert_eq!(attester_slashings.len(), 1); // Pruning the attester slashings should remove all but slashing4. - op_pool.prune_attester_slashings(&bellatrix_head.beacon_state); + op_pool.prune_attester_slashings(&electra_head.beacon_state); assert_eq!(op_pool.attester_slashings.read().len(), 1); } } diff --git a/consensus/state_processing/src/per_block_processing/tests.rs b/consensus/state_processing/src/per_block_processing/tests.rs index 739717b33f..96610c2010 100644 --- a/consensus/state_processing/src/per_block_processing/tests.rs +++ b/consensus/state_processing/src/per_block_processing/tests.rs @@ -8,7 +8,7 @@ use crate::per_block_processing::errors::{ use crate::{BlockReplayError, BlockReplayer, per_block_processing}; use crate::{ BlockSignatureStrategy, ConsensusContext, VerifyBlockRoot, VerifySignatures, - per_block_processing::{process_operations, verify_exit::verify_exit}, + per_block_processing::process_operations, }; use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType}; use bls::{AggregateSignature, Keypair, PublicKeyBytes, Signature, SignatureBytes}; @@ -39,10 +39,13 @@ async fn get_harness<E: EthSpec>( // Set the state and block to be in the last slot of the `epoch_offset`th epoch. let last_slot_of_epoch = (MainnetEthSpec::genesis_epoch() + epoch_offset).end_slot(E::slots_per_epoch()); + // Use Electra spec to ensure blocks are created at the same fork as the state + let spec = Arc::new(ForkName::Electra.make_genesis_spec(E::default_spec())); let harness = BeaconChainHarness::<EphemeralHarnessType<E>>::builder(E::default()) - .default_spec() + .spec(spec.clone()) .keypairs(KEYPAIRS[0..num_validators].to_vec()) .fresh_ephemeral_store() + .mock_execution_layer() .build(); let state = harness.get_current_state(); if last_slot_of_epoch > Slot::new(0) { @@ -63,8 +66,8 @@ async fn get_harness<E: EthSpec>( #[tokio::test] async fn valid_block_ok() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let state = harness.get_current_state(); let slot = state.slot(); @@ -87,8 +90,8 @@ async fn valid_block_ok() { #[tokio::test] async fn invalid_block_header_state_slot() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let state = harness.get_current_state(); let slot = state.slot() + Slot::new(1); @@ -107,18 +110,18 @@ async fn invalid_block_header_state_slot() { &spec, ); - assert_eq!( + assert!(matches!( result, Err(BlockProcessingError::HeaderInvalid { - reason: HeaderInvalid::StateSlotMismatch + reason: HeaderInvalid::StateSlotMismatch, }) - ); + )); } #[tokio::test] async fn invalid_parent_block_root() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let state = harness.get_current_state(); let slot = state.slot(); @@ -139,21 +142,18 @@ async fn invalid_parent_block_root() { &spec, ); - assert_eq!( + assert!(matches!( result, Err(BlockProcessingError::HeaderInvalid { - reason: HeaderInvalid::ParentBlockRootMismatch { - state: state.latest_block_header().canonical_root(), - block: Hash256::from([0xAA; 32]) - } + reason: HeaderInvalid::ParentBlockRootMismatch { .. }, }) - ); + )); } #[tokio::test] async fn invalid_block_signature() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let state = harness.get_current_state(); let slot = state.slot(); @@ -172,19 +172,18 @@ async fn invalid_block_signature() { &spec, ); - // should get a BadSignature error - assert_eq!( + assert!(matches!( result, Err(BlockProcessingError::HeaderInvalid { - reason: HeaderInvalid::ProposalSignatureInvalid + reason: HeaderInvalid::ProposalSignatureInvalid, }) - ); + )); } #[tokio::test] async fn invalid_randao_reveal_signature() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let state = harness.get_current_state(); let slot = state.slot(); @@ -211,8 +210,8 @@ async fn invalid_randao_reveal_signature() { #[tokio::test] async fn valid_4_deposits() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut state = harness.get_current_state(); let (deposits, state) = harness.make_deposits(&mut state, 4, None, None); @@ -235,8 +234,8 @@ async fn valid_4_deposits() { #[tokio::test] async fn invalid_deposit_deposit_count_too_big() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut state = harness.get_current_state(); let (deposits, state) = harness.make_deposits(&mut state, 1, None, None); @@ -267,8 +266,8 @@ async fn invalid_deposit_deposit_count_too_big() { #[tokio::test] async fn invalid_deposit_count_too_small() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut state = harness.get_current_state(); let (deposits, state) = harness.make_deposits(&mut state, 1, None, None); @@ -299,8 +298,8 @@ async fn invalid_deposit_count_too_small() { #[tokio::test] async fn invalid_deposit_bad_merkle_proof() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut state = harness.get_current_state(); let (deposits, state) = harness.make_deposits(&mut state, 1, None, None); @@ -333,8 +332,8 @@ async fn invalid_deposit_bad_merkle_proof() { #[tokio::test] async fn invalid_deposit_wrong_sig() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut state = harness.get_current_state(); let (deposits, state) = @@ -357,8 +356,8 @@ async fn invalid_deposit_wrong_sig() { #[tokio::test] async fn invalid_deposit_invalid_pub_key() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut state = harness.get_current_state(); let (deposits, state) = @@ -382,8 +381,8 @@ async fn invalid_deposit_invalid_pub_key() { #[tokio::test] async fn invalid_attestation_no_committee_for_index() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut state = harness.get_current_state(); let mut head_block = harness @@ -422,8 +421,8 @@ async fn invalid_attestation_no_committee_for_index() { #[tokio::test] async fn invalid_attestation_wrong_justified_checkpoint() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut state = harness.get_current_state(); let mut head_block = harness @@ -477,8 +476,8 @@ async fn invalid_attestation_wrong_justified_checkpoint() { #[tokio::test] async fn invalid_attestation_bad_aggregation_bitfield_len() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut state = harness.get_current_state(); let mut head_block = harness @@ -488,13 +487,14 @@ async fn invalid_attestation_bad_aggregation_bitfield_len() { .clone() .deconstruct() .0; + // Use Electra method since harness runs at Electra fork *head_block .to_mut() .body_mut() .attestations_mut() .next() .unwrap() - .aggregation_bits_base_mut() + .aggregation_bits_electra_mut() .unwrap() = Bitfield::with_capacity(spec.target_committee_size).unwrap(); let mut ctxt = ConsensusContext::new(state.slot()); @@ -506,19 +506,20 @@ async fn invalid_attestation_bad_aggregation_bitfield_len() { &spec, ); - // Expecting InvalidBitfield because the size of the aggregation_bitfield is bigger than the committee size. + // In Electra, setting wrong aggregation_bits capacity causes EmptyCommittee error + // (validation order changed - committee check happens before bitfield check) assert_eq!( result, Err(BlockProcessingError::BeaconStateError( - BeaconStateError::InvalidBitfield + BeaconStateError::EmptyCommittee )) ); } #[tokio::test] async fn invalid_attestation_bad_signature() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, 97).await; // minimal number of required validators for this test + let spec = harness.spec.clone(); let mut state = harness.get_current_state(); let mut head_block = harness @@ -558,8 +559,8 @@ async fn invalid_attestation_bad_signature() { #[tokio::test] async fn invalid_attestation_included_too_early() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut state = harness.get_current_state(); let mut head_block = harness @@ -603,56 +604,15 @@ async fn invalid_attestation_included_too_early() { ); } -#[tokio::test] -async fn invalid_attestation_included_too_late() { - let spec = MainnetEthSpec::default_spec(); - // note to maintainer: might need to increase validator count if we get NoCommittee - let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; - - let mut state = harness.get_current_state(); - let mut head_block = harness - .chain - .head_beacon_block() - .as_ref() - .clone() - .deconstruct() - .0; - let new_attesation_slot = head_block.body().attestations().next().unwrap().data().slot - - Slot::new(MainnetEthSpec::slots_per_epoch()); - head_block - .to_mut() - .body_mut() - .attestations_mut() - .next() - .unwrap() - .data_mut() - .slot = new_attesation_slot; - - let mut ctxt = ConsensusContext::new(state.slot()); - let result = process_operations::process_attestations( - &mut state, - head_block.body(), - VerifySignatures::True, - &mut ctxt, - &spec, - ); - assert_eq!( - result, - Err(BlockProcessingError::AttestationInvalid { - index: 0, - reason: AttestationInvalid::IncludedTooLate { - state: state.slot(), - attestation: new_attesation_slot, - } - }) - ); -} +// Note: `invalid_attestation_included_too_late` test removed. +// The `IncludedTooLate` check was removed in Deneb (EIP7045), so this test is no longer +// applicable when running with Electra spec (which the harness uses by default). #[tokio::test] async fn invalid_attestation_target_epoch_slot_mismatch() { - let spec = MainnetEthSpec::default_spec(); // note to maintainer: might need to increase validator count if we get NoCommittee let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut state = harness.get_current_state(); let mut head_block = harness @@ -694,8 +654,8 @@ async fn invalid_attestation_target_epoch_slot_mismatch() { #[tokio::test] async fn valid_insert_attester_slashing() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let attester_slashing = harness.make_attester_slashing(vec![1, 2]); @@ -715,8 +675,8 @@ async fn valid_insert_attester_slashing() { #[tokio::test] async fn invalid_attester_slashing_not_slashable() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut attester_slashing = harness.make_attester_slashing(vec![1, 2]); match &mut attester_slashing { @@ -750,8 +710,8 @@ async fn invalid_attester_slashing_not_slashable() { #[tokio::test] async fn invalid_attester_slashing_1_invalid() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut attester_slashing = harness.make_attester_slashing(vec![1, 2]); match &mut attester_slashing { @@ -790,8 +750,8 @@ async fn invalid_attester_slashing_1_invalid() { #[tokio::test] async fn invalid_attester_slashing_2_invalid() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut attester_slashing = harness.make_attester_slashing(vec![1, 2]); match &mut attester_slashing { @@ -830,8 +790,8 @@ async fn invalid_attester_slashing_2_invalid() { #[tokio::test] async fn valid_insert_proposer_slashing() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let proposer_slashing = harness.make_proposer_slashing(1); let mut state = harness.get_current_state(); let mut ctxt = ConsensusContext::new(state.slot()); @@ -848,8 +808,8 @@ async fn valid_insert_proposer_slashing() { #[tokio::test] async fn invalid_proposer_slashing_proposals_identical() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut proposer_slashing = harness.make_proposer_slashing(1); proposer_slashing.signed_header_1.message = proposer_slashing.signed_header_2.message.clone(); @@ -876,8 +836,8 @@ async fn invalid_proposer_slashing_proposals_identical() { #[tokio::test] async fn invalid_proposer_slashing_proposer_unknown() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut proposer_slashing = harness.make_proposer_slashing(1); proposer_slashing.signed_header_1.message.proposer_index = 3_141_592; @@ -905,8 +865,8 @@ async fn invalid_proposer_slashing_proposer_unknown() { #[tokio::test] async fn invalid_proposer_slashing_duplicate_slashing() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let proposer_slashing = harness.make_proposer_slashing(1); let mut state = harness.get_current_state(); @@ -939,8 +899,8 @@ async fn invalid_proposer_slashing_duplicate_slashing() { #[tokio::test] async fn invalid_bad_proposal_1_signature() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut proposer_slashing = harness.make_proposer_slashing(1); proposer_slashing.signed_header_1.signature = Signature::empty(); let mut state = harness.get_current_state(); @@ -965,8 +925,8 @@ async fn invalid_bad_proposal_1_signature() { #[tokio::test] async fn invalid_bad_proposal_2_signature() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut proposer_slashing = harness.make_proposer_slashing(1); proposer_slashing.signed_header_2.signature = Signature::empty(); let mut state = harness.get_current_state(); @@ -991,8 +951,8 @@ async fn invalid_bad_proposal_2_signature() { #[tokio::test] async fn invalid_proposer_slashing_proposal_epoch_mismatch() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut proposer_slashing = harness.make_proposer_slashing(1); proposer_slashing.signed_header_1.message.slot = Slot::new(0); proposer_slashing.signed_header_2.message.slot = Slot::new(128); @@ -1019,92 +979,6 @@ async fn invalid_proposer_slashing_proposal_epoch_mismatch() { ); } -#[tokio::test] -async fn fork_spanning_exit() { - let mut spec = MainnetEthSpec::default_spec(); - let slots_per_epoch = MainnetEthSpec::slots_per_epoch(); - - spec.altair_fork_epoch = Some(Epoch::new(2)); - spec.bellatrix_fork_epoch = Some(Epoch::new(4)); - spec.shard_committee_period = 0; - let spec = Arc::new(spec); - - let harness = BeaconChainHarness::builder(MainnetEthSpec) - .spec(spec.clone()) - .deterministic_keypairs(VALIDATOR_COUNT) - .mock_execution_layer() - .fresh_ephemeral_store() - .build(); - - harness.extend_to_slot(slots_per_epoch.into()).await; - - /* - * Produce an exit *before* Altair. - */ - - let signed_exit = harness.make_voluntary_exit(0, Epoch::new(1)); - assert!(signed_exit.message.epoch < spec.altair_fork_epoch.unwrap()); - - /* - * Ensure the exit verifies before Altair. - */ - - let head = harness.chain.canonical_head.cached_head(); - let head_state = &head.snapshot.beacon_state; - assert!(head_state.current_epoch() < spec.altair_fork_epoch.unwrap()); - verify_exit( - head_state, - None, - &signed_exit, - VerifySignatures::True, - &spec, - ) - .expect("phase0 exit verifies against phase0 state"); - - /* - * Ensure the exit verifies after Altair. - */ - - harness - .extend_to_slot(spec.altair_fork_epoch.unwrap().start_slot(slots_per_epoch)) - .await; - let head = harness.chain.canonical_head.cached_head(); - let head_state = &head.snapshot.beacon_state; - assert!(head_state.current_epoch() >= spec.altair_fork_epoch.unwrap()); - assert!(head_state.current_epoch() < spec.bellatrix_fork_epoch.unwrap()); - verify_exit( - head_state, - None, - &signed_exit, - VerifySignatures::True, - &spec, - ) - .expect("phase0 exit verifies against altair state"); - - /* - * Ensure the exit no longer verifies after Bellatrix. - */ - - harness - .extend_to_slot( - spec.bellatrix_fork_epoch - .unwrap() - .start_slot(slots_per_epoch), - ) - .await; - let head = harness.chain.canonical_head.cached_head(); - let head_state = &head.snapshot.beacon_state; - assert!(head_state.current_epoch() >= spec.bellatrix_fork_epoch.unwrap()); - verify_exit( - head_state, - None, - &signed_exit, - VerifySignatures::True, - &spec, - ) - .expect_err("phase0 exit does not verify against bellatrix state"); -} - /// Check that the block replayer does not consume state roots unnecessarily. #[tokio::test] async fn block_replayer_peeking_state_roots() { diff --git a/consensus/state_processing/src/per_epoch_processing/tests.rs b/consensus/state_processing/src/per_epoch_processing/tests.rs index f042e8766c..c04b7f843d 100644 --- a/consensus/state_processing/src/per_epoch_processing/tests.rs +++ b/consensus/state_processing/src/per_epoch_processing/tests.rs @@ -11,10 +11,10 @@ async fn runs_without_error() { .default_spec() .deterministic_keypairs(8) .fresh_ephemeral_store() + .mock_execution_layer() .build(); harness.advance_slot(); - let spec = MinimalEthSpec::default_spec(); let target_slot = (MinimalEthSpec::genesis_epoch() + 4).end_slot(MinimalEthSpec::slots_per_epoch()); @@ -32,7 +32,7 @@ async fn runs_without_error() { .await; let mut new_head_state = harness.get_current_state(); - process_epoch(&mut new_head_state, &spec).unwrap(); + process_epoch(&mut new_head_state, &harness.spec).unwrap(); } #[cfg(not(debug_assertions))] diff --git a/consensus/types/tests/committee_cache.rs b/consensus/types/tests/committee_cache.rs index 0bb8aa1da2..5c1962276f 100644 --- a/consensus/types/tests/committee_cache.rs +++ b/consensus/types/tests/committee_cache.rs @@ -21,6 +21,7 @@ fn get_harness<E: EthSpec>(validator_count: usize) -> BeaconChainHarness<Ephemer .default_spec() .keypairs(KEYPAIRS[0..validator_count].to_vec()) .fresh_ephemeral_store() + .mock_execution_layer() .build(); harness.advance_slot(); harness diff --git a/consensus/types/tests/state.rs b/consensus/types/tests/state.rs index 63ab3b8084..5e223092cf 100644 --- a/consensus/types/tests/state.rs +++ b/consensus/types/tests/state.rs @@ -28,6 +28,7 @@ async fn get_harness<E: EthSpec>( .default_spec() .keypairs(KEYPAIRS[0..validator_count].to_vec()) .fresh_ephemeral_store() + .mock_execution_layer() .build(); let skip_to_slot = slot - SLOT_OFFSET; diff --git a/lcli/src/mock_el.rs b/lcli/src/mock_el.rs index 544010b6a2..6086067a47 100644 --- a/lcli/src/mock_el.rs +++ b/lcli/src/mock_el.rs @@ -3,13 +3,10 @@ use clap_utils::{parse_optional, parse_required}; use environment::Environment; use execution_layer::{ auth::{JwtKey, strip_prefix}, - test_utils::{ - Config, DEFAULT_JWT_SECRET, DEFAULT_TERMINAL_BLOCK, MockExecutionConfig, MockServer, - }, + test_utils::{Config, DEFAULT_JWT_SECRET, MockExecutionConfig, MockServer}, }; use std::net::Ipv4Addr; use std::path::PathBuf; -use std::sync::Arc; use types::*; pub fn run<E: EthSpec>(mut env: Environment<E>, matches: &ArgMatches) -> Result<(), String> { @@ -25,7 +22,6 @@ pub fn run<E: EthSpec>(mut env: Environment<E>, matches: &ArgMatches) -> Result< let amsterdam_time = parse_optional(matches, "amsterdam-time")?; let handle = env.core_context().executor.handle().unwrap(); - let spec = Arc::new(E::default_spec()); let jwt_key = if let Some(secret_path) = jwt_secret_path { let hex_str = std::fs::read_to_string(&secret_path) @@ -50,9 +46,6 @@ pub fn run<E: EthSpec>(mut env: Environment<E>, matches: &ArgMatches) -> Result< listen_port, }, jwt_key, - terminal_difficulty: spec.terminal_total_difficulty, - terminal_block: DEFAULT_TERMINAL_BLOCK, - terminal_block_hash: spec.terminal_block_hash, shanghai_time: Some(shanghai_time), cancun_time, prague_time, diff --git a/testing/execution_engine_integration/src/test_rig.rs b/testing/execution_engine_integration/src/test_rig.rs index 5c3061166e..6bf4a1aa52 100644 --- a/testing/execution_engine_integration/src/test_rig.rs +++ b/testing/execution_engine_integration/src/test_rig.rs @@ -9,8 +9,8 @@ use alloy_signer_local::PrivateKeySigner; use bls::PublicKeyBytes; use execution_layer::test_utils::DEFAULT_GAS_LIMIT; use execution_layer::{ - BlockProposalContentsType, BuilderParams, ChainHealth, ExecutionLayer, PayloadAttributes, - PayloadParameters, PayloadStatus, + BlockByNumberQuery, BlockProposalContentsType, BuilderParams, ChainHealth, ExecutionLayer, + LATEST_TAG, PayloadAttributes, PayloadParameters, PayloadStatus, }; use fixed_bytes::FixedBytesExtended; use fork_choice::ForkchoiceUpdateParameters; @@ -210,25 +210,29 @@ impl<Engine: GenericExecutionEngine> TestRig<Engine> { let account2 = AlloyAddress::from_slice(&hex::decode(ACCOUNT2).unwrap()); /* - * Read the terminal block hash from both pairs, check it's equal. + * Read the genesis block hash from both pairs, check it's equal. + * Since TTD=0, the genesis block is the terminal PoW block. */ - let terminal_pow_block_hash = self + let genesis_block = self .ee_a .execution_layer - .get_terminal_pow_block_hash(&self.spec, timestamp_now()) + .get_block_by_number(BlockByNumberQuery::Tag(LATEST_TAG)) .await .unwrap() - .unwrap(); + .expect("should have genesis block"); + + let terminal_pow_block_hash = genesis_block.block_hash; assert_eq!( terminal_pow_block_hash, self.ee_b .execution_layer - .get_terminal_pow_block_hash(&self.spec, timestamp_now()) + .get_block_by_number(BlockByNumberQuery::Tag(LATEST_TAG)) .await .unwrap() - .unwrap() + .expect("should have genesis block") + .block_hash ); // Submit transactions before getting payload diff --git a/testing/state_transition_vectors/src/exit.rs b/testing/state_transition_vectors/src/exit.rs index f8ece0218f..3b0fe7d8ec 100644 --- a/testing/state_transition_vectors/src/exit.rs +++ b/testing/state_transition_vectors/src/exit.rs @@ -1,4 +1,5 @@ use super::*; +use beacon_chain::test_utils::test_spec; use state_processing::{ BlockProcessingError, BlockSignatureStrategy, ConsensusContext, VerifyBlockRoot, per_block_processing, per_block_processing::errors::ExitInvalid, @@ -70,13 +71,13 @@ impl ExitTest { BlockSignatureStrategy::VerifyIndividual, VerifyBlockRoot::True, &mut ctxt, - &E::default_spec(), + &test_spec::<E>(), ) } #[cfg(all(test, not(debug_assertions)))] async fn run(self) -> BeaconState<E> { - let spec = &E::default_spec(); + let spec = &test_spec::<E>(); let expected = self.expected.clone(); assert_eq!(STATE_EPOCH, spec.shard_committee_period); diff --git a/testing/state_transition_vectors/src/main.rs b/testing/state_transition_vectors/src/main.rs index 80c30489b7..6a212f034d 100644 --- a/testing/state_transition_vectors/src/main.rs +++ b/testing/state_transition_vectors/src/main.rs @@ -57,6 +57,7 @@ async fn get_harness<E: EthSpec>( .default_spec() .keypairs(KEYPAIRS[0..validator_count].to_vec()) .fresh_ephemeral_store() + .mock_execution_layer() .build(); let skip_to_slot = slot - SLOT_OFFSET; if skip_to_slot > Slot::new(0) { diff --git a/validator_manager/src/exit_validators.rs b/validator_manager/src/exit_validators.rs index 8ddcc7e419..00bcb36e80 100644 --- a/validator_manager/src/exit_validators.rs +++ b/validator_manager/src/exit_validators.rs @@ -322,7 +322,7 @@ mod test { let mut spec = ChainSpec::mainnet(); spec.shard_committee_period = 1; spec.altair_fork_epoch = Some(Epoch::new(0)); - spec.bellatrix_fork_epoch = Some(Epoch::new(1)); + spec.bellatrix_fork_epoch = Some(Epoch::new(0)); spec.capella_fork_epoch = Some(Epoch::new(2)); spec.deneb_fork_epoch = Some(Epoch::new(3)); @@ -330,15 +330,8 @@ mod test { let harness = &beacon_node.harness; let mock_el = harness.mock_execution_layer.as_ref().unwrap(); - let execution_ctx = mock_el.server.ctx.clone(); - // Move to terminal block. mock_el.server.all_payloads_valid(); - execution_ctx - .execution_block_generator - .write() - .move_to_terminal_block() - .unwrap(); Self { exit_config: None, From a1ef265c9e612f15060f23f43a4832032e4589dd Mon Sep 17 00:00:00 2001 From: Jimmy Chen <jchen.tc@gmail.com> Date: Wed, 25 Feb 2026 23:17:49 +1100 Subject: [PATCH 51/81] Add getBlobsV1 and getBlobsV2 support to mock EL server (#8870) Co-Authored-By: Jimmy Chen <jchen.tc@gmail.com> --- .../test_utils/execution_block_generator.rs | 38 ++++++++++++++++++- .../src/test_utils/handle_rpc.rs | 29 ++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs index 1743b340ab..62a46246da 100644 --- a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs +++ b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs @@ -1,7 +1,8 @@ use crate::engine_api::{ ExecutionBlock, PayloadAttributes, PayloadId, PayloadStatusV1, PayloadStatusV1Status, json_structures::{ - JsonForkchoiceUpdatedV1Response, JsonPayloadStatusV1, JsonPayloadStatusV1Status, + BlobAndProof, BlobAndProofV1, BlobAndProofV2, JsonForkchoiceUpdatedV1Response, + JsonPayloadStatusV1, JsonPayloadStatusV1Status, }, }; use crate::engines::ForkchoiceState; @@ -15,6 +16,7 @@ use rand::{Rng, SeedableRng, rngs::StdRng}; use serde::{Deserialize, Serialize}; use ssz::Decode; use ssz_types::VariableList; +use state_processing::per_block_processing::deneb::kzg_commitment_to_versioned_hash; use std::cmp::max; use std::collections::HashMap; use std::sync::Arc; @@ -456,6 +458,40 @@ impl<E: EthSpec> ExecutionBlockGenerator<E> { self.blobs_bundles.get(id).cloned() } + /// Look up a blob and proof by versioned hash across all stored bundles. + pub fn get_blob_and_proof(&self, versioned_hash: &Hash256) -> Option<BlobAndProof<E>> { + self.blobs_bundles + .iter() + .find_map(|(payload_id, blobs_bundle)| { + let (blob_idx, _) = + blobs_bundle + .commitments + .iter() + .enumerate() + .find(|(_, commitment)| { + &kzg_commitment_to_versioned_hash(commitment) == versioned_hash + })?; + let is_fulu = self.payload_ids.get(payload_id)?.fork_name().fulu_enabled(); + let blob = blobs_bundle.blobs.get(blob_idx)?.clone(); + if is_fulu { + let start = blob_idx * E::cells_per_ext_blob(); + let end = start + E::cells_per_ext_blob(); + let proofs = blobs_bundle + .proofs + .get(start..end)? + .to_vec() + .try_into() + .ok()?; + Some(BlobAndProof::V2(BlobAndProofV2 { blob, proofs })) + } else { + Some(BlobAndProof::V1(BlobAndProofV1 { + blob, + proof: *blobs_bundle.proofs.get(blob_idx)?, + })) + } + }) + } + pub fn new_payload(&mut self, payload: ExecutionPayload<E>) -> PayloadStatusV1 { let Some(parent) = self.blocks.get(&payload.parent_hash()) else { return PayloadStatusV1 { diff --git a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs index 53eb3b5166..7a81017b3f 100644 --- a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs +++ b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs @@ -468,6 +468,35 @@ pub async fn handle_rpc<E: EthSpec>( _ => unreachable!(), } } + ENGINE_GET_BLOBS_V1 => { + let versioned_hashes = + get_param::<Vec<Hash256>>(params, 0).map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?; + let generator = ctx.execution_block_generator.read(); + // V1: per-element nullable array, positionally matching the request. + let response: Vec<Option<BlobAndProofV1<E>>> = versioned_hashes + .iter() + .map(|hash| match generator.get_blob_and_proof(hash) { + Some(BlobAndProof::V1(v1)) => Some(v1), + _ => None, + }) + .collect(); + Ok(serde_json::to_value(response).unwrap()) + } + ENGINE_GET_BLOBS_V2 => { + let versioned_hashes = + get_param::<Vec<Hash256>>(params, 0).map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?; + let generator = ctx.execution_block_generator.read(); + // V2: all-or-nothing — null if any blob is missing. + let results: Vec<Option<BlobAndProofV2<E>>> = versioned_hashes + .iter() + .map(|hash| match generator.get_blob_and_proof(hash) { + Some(BlobAndProof::V2(v2)) => Some(v2), + _ => None, + }) + .collect(); + let response: Option<Vec<BlobAndProofV2<E>>> = results.into_iter().collect(); + Ok(serde_json::to_value(response).unwrap()) + } ENGINE_FORKCHOICE_UPDATED_V1 | ENGINE_FORKCHOICE_UPDATED_V2 | ENGINE_FORKCHOICE_UPDATED_V3 => { From 2f43d234d88a9c43518f032d5e4b34fe1c889068 Mon Sep 17 00:00:00 2001 From: Michael Sproul <michaelsproul@users.noreply.github.com> Date: Fri, 27 Feb 2026 08:05:57 +1100 Subject: [PATCH 52/81] Add CI job to check for deleted files (#8727) Co-Authored-By: Michael Sproul <michael@sigmaprime.io> Co-Authored-By: Michael Sproul <michaelsproul@users.noreply.github.com> --- .github/CODEOWNERS | 1 + .github/forbidden-files.txt | 7 +++++++ .github/workflows/test-suite.yml | 22 ++++++++++++++++++++++ 3 files changed, 30 insertions(+) create mode 100644 .github/forbidden-files.txt diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cdec442276..e9ec8740a0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,3 +1,4 @@ /beacon_node/network/ @jxs /beacon_node/lighthouse_network/ @jxs /beacon_node/store/ @michaelsproul +/.github/forbidden-files.txt @michaelsproul diff --git a/.github/forbidden-files.txt b/.github/forbidden-files.txt new file mode 100644 index 0000000000..ec89bd2e4b --- /dev/null +++ b/.github/forbidden-files.txt @@ -0,0 +1,7 @@ +# Files that have been intentionally deleted and should not be re-added. +# This prevents accidentally reviving files during botched merges. +# Add one file path per line (relative to repo root). + +beacon_node/beacon_chain/src/otb_verification_service.rs +beacon_node/store/src/partial_beacon_state.rs +beacon_node/store/src/consensus_context.rs diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 72ea9d41ae..d9efbfc148 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -72,6 +72,27 @@ jobs: steps: - name: Check that the pull request is not targeting the stable branch run: test ${{ github.base_ref }} != "stable" + + forbidden-files-check: + name: forbidden-files-check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Check for forbidden files + run: | + if [ -f .github/forbidden-files.txt ]; then + status=0 + while IFS= read -r file || [ -n "$file" ]; do + # Skip comments and empty lines + [[ "$file" =~ ^#.*$ || -z "$file" ]] && continue + if [ -f "$file" ]; then + echo "::error::Forbidden file exists: $file" + status=1 + fi + done < .github/forbidden-files.txt + exit $status + fi + release-tests-ubuntu: name: release-tests-ubuntu needs: [check-labels] @@ -430,6 +451,7 @@ jobs: needs: [ 'check-labels', 'target-branch-check', + 'forbidden-files-check', 'release-tests-ubuntu', 'beacon-chain-tests', 'op-pool-tests', From 8cf6ffac4b1954bfc61939e116afd5e2ab349dbb Mon Sep 17 00:00:00 2001 From: Jimmy Chen <jchen.tc@gmail.com> Date: Wed, 25 Feb 2026 23:10:41 +1100 Subject: [PATCH 53/81] Update yanked keccak 0.1.5 to 0.1.6 (#8900) Co-Authored-By: Jimmy Chen <jchen.tc@gmail.com> --- Cargo.lock | 7 +++---- Cargo.toml | 1 + 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7d75f5c197..dd1637045b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4832,9 +4832,9 @@ dependencies = [ [[package]] name = "keccak" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ "cpufeatures", ] @@ -10607,8 +10607,7 @@ dependencies = [ [[package]] name = "yamux" version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deab71f2e20691b4728b349c6cee8fc7223880fa67b6b4f92225ec32225447e5" +source = "git+https://github.com/sigp/rust-yamux?rev=575b17c0f44f4253079a6bafaa2de74ca1d6dfaa#575b17c0f44f4253079a6bafaa2de74ca1d6dfaa" dependencies = [ "futures", "log", diff --git a/Cargo.toml b/Cargo.toml index aac26e060b..61caacf5df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -303,3 +303,4 @@ debug = true [patch.crates-io] quick-protobuf = { git = "https://github.com/sigp/quick-protobuf.git", rev = "681f413312404ab6e51f0b46f39b0075c6f4ebfd" } +yamux = { git = "https://github.com/sigp/rust-yamux", rev = "575b17c0f44f4253079a6bafaa2de74ca1d6dfaa" } From 95f12d0927831971ca9e1acf7ca7e87fdda56f4a Mon Sep 17 00:00:00 2001 From: Jimmy Chen <jchen.tc@gmail.com> Date: Fri, 27 Feb 2026 16:48:56 +1100 Subject: [PATCH 54/81] Bump version to v8.1.1 (#8853) --- Cargo.lock | 14 +++++++------- Cargo.toml | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dd1637045b..40c550f4c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "account_manager" -version = "8.1.0" +version = "8.1.1" dependencies = [ "account_utils", "bls", @@ -1276,7 +1276,7 @@ dependencies = [ [[package]] name = "beacon_node" -version = "8.1.0" +version = "8.1.1" dependencies = [ "account_utils", "beacon_chain", @@ -1513,7 +1513,7 @@ dependencies = [ [[package]] name = "boot_node" -version = "8.1.0" +version = "8.1.1" dependencies = [ "beacon_node", "bytes", @@ -4897,7 +4897,7 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "lcli" -version = "8.1.0" +version = "8.1.1" dependencies = [ "account_utils", "beacon_chain", @@ -5383,7 +5383,7 @@ dependencies = [ [[package]] name = "lighthouse" -version = "8.1.0" +version = "8.1.1" dependencies = [ "account_manager", "account_utils", @@ -5515,7 +5515,7 @@ dependencies = [ [[package]] name = "lighthouse_version" -version = "8.1.0" +version = "8.1.1" dependencies = [ "regex", ] @@ -9622,7 +9622,7 @@ dependencies = [ [[package]] name = "validator_client" -version = "8.1.0" +version = "8.1.1" dependencies = [ "account_utils", "beacon_node_fallback", diff --git a/Cargo.toml b/Cargo.toml index 61caacf5df..5f6f43d2f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,7 +91,7 @@ resolver = "2" [workspace.package] edition = "2024" -version = "8.1.0" +version = "8.1.1" [workspace.dependencies] account_utils = { path = "common/account_utils" } From 6194dddc5b9ea176fc38796fb5de6c7fac8a8143 Mon Sep 17 00:00:00 2001 From: Michael Sproul <michaelsproul@users.noreply.github.com> Date: Mon, 2 Mar 2026 17:43:51 +1100 Subject: [PATCH 55/81] Persist custody context more readily (#8921) We received a bug report of a node restarting custody backfill unnecessarily after upgrading to Lighthouse v8.1.1. What happened is: - User started LH v8.0.1 many months ago, CGC updated 0 -> N but the CGC was not eagerly persisted. - LH experienced an unclean shutdown (not sure of what type). - Upon restarting (still running v8.0.1), the custody context read from disk contains CGC=0: `DEBUG Loaded persisted custody context custody_context: CustodyContext { validator_custody_count: 0, ...`). - CGC updates again to N, retriggering custody backfill: `DEBUG Validator count at head updated old_count: 0, new_count: N`. - Custody backfill does a bunch of downloading for no gain: `DEBUG Imported historical data columns epoch: Epoch(428433), total_imported: 0` - While custody backfill is running user updated to v8.1.1, and we see logs for the CGC=N being peristed upon clean shutdown, and then correctly read on startup with v8.1.1. - Custody backfill keeps running and downloading due to the CGC change still being considered in progress. - Call `persist_custody_context` inside the `register_validators` handler so that it is written to disk eagerly whenever it changes. The performance impact of this should be minimal as the amount of data is very small and this call can only happen at most ~128 times (once for each change) in the entire life of a beacon node. - Call `persist_custody_context` inside `BeaconChainBuilder::build` so that changes caused by CLI flags are persisted (otherwise starting a node with `--semi-supernode` and no validators, then shutting it down uncleanly would cause use to forget the CGC). These changes greatly reduce the timespan during which an unclean shutdown can create inconsistency. In the worst case, we only lose backfill progress that runs concurrently with the `register_validators` handler (should be extremely minimal, nigh impossible). Co-Authored-By: Michael Sproul <michael@sigmaprime.io> --- beacon_node/beacon_chain/src/beacon_chain.rs | 13 ++++++++++++- beacon_node/beacon_chain/src/builder.rs | 5 +++++ beacon_node/http_api/src/validator/mod.rs | 12 ++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 9d204ac7f2..703ed24420 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -662,7 +662,18 @@ impl<T: BeaconChainTypes> BeaconChain<T> { .custody_context() .as_ref() .into(); - debug!(?custody_context, "Persisting custody context to store"); + + // Pattern match to avoid accidentally missing fields and to ignore deprecated fields. + let CustodyContextSsz { + validator_custody_at_head, + epoch_validator_custody_requirements, + persisted_is_supernode: _, + } = &custody_context; + debug!( + validator_custody_at_head, + ?epoch_validator_custody_requirements, + "Persisting custody context to store" + ); persist_custody_context::<T::EthSpec, T::HotStore, T::ColdStore>( self.store.clone(), diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 2c1dae9215..66a54d46e8 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -1083,6 +1083,11 @@ where let cgc_change_effective_slot = cgc_changed.effective_epoch.start_slot(E::slots_per_epoch()); beacon_chain.update_data_column_custody_info(Some(cgc_change_effective_slot)); + + // Persist change to disk. + beacon_chain + .persist_custody_context() + .map_err(|e| format!("Failed writing updated CGC: {e:?}"))?; } info!( diff --git a/beacon_node/http_api/src/validator/mod.rs b/beacon_node/http_api/src/validator/mod.rs index df237d9f9b..a9082df715 100644 --- a/beacon_node/http_api/src/validator/mod.rs +++ b/beacon_node/http_api/src/validator/mod.rs @@ -727,6 +727,18 @@ pub fn post_validator_prepare_beacon_proposer<T: BeaconChainTypes>( debug!(error = %e, "Could not send message to the network service. \ Likely shutdown") }); + + // Write the updated custody context to disk. This happens at most 128 + // times ever, so the I/O burden should be extremely minimal. Without a + // write here we risk forgetting custody backfill progress upon an + // unclean shutdown. The custody context is otherwise only persisted in + // `BeaconChain::drop`. + if let Err(error) = chain.persist_custody_context() { + error!( + ?error, + "Failed to persist custody context after CGC update" + ); + } } } From f4b5b033a227fcacdb8e8514bbca6cf6702f3a24 Mon Sep 17 00:00:00 2001 From: Mac L <mjladson@pm.me> Date: Mon, 2 Mar 2026 09:19:41 +0200 Subject: [PATCH 56/81] Add `testing` feature to validator_client/http_api (#8909) Create a `testing` feature which we can use to gate off `test_utils.rs` and its associated dependencies from the rest of the crate. Co-Authored-By: Mac L <mjladson@pm.me> --- validator_client/http_api/Cargo.toml | 12 +++++++++--- validator_client/http_api/src/lib.rs | 4 +++- validator_manager/Cargo.toml | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/validator_client/http_api/Cargo.toml b/validator_client/http_api/Cargo.toml index 2bd57867ac..e334ab9db0 100644 --- a/validator_client/http_api/Cargo.toml +++ b/validator_client/http_api/Cargo.toml @@ -8,14 +8,17 @@ authors = ["Sigma Prime <contact@sigmaprime.io>"] name = "validator_http_api" path = "src/lib.rs" +[features] +testing = ["dep:deposit_contract", "dep:doppelganger_service", "dep:tempfile"] + [dependencies] account_utils = { workspace = true } beacon_node_fallback = { workspace = true } bls = { workspace = true } -deposit_contract = { workspace = true } +deposit_contract = { workspace = true, optional = true } directory = { workspace = true } dirs = { workspace = true } -doppelganger_service = { workspace = true } +doppelganger_service = { workspace = true, optional = true } eth2 = { workspace = true, features = ["lighthouse"] } eth2_keystore = { workspace = true } ethereum_serde_utils = { workspace = true } @@ -38,7 +41,7 @@ slot_clock = { workspace = true } sysinfo = { workspace = true } system_health = { workspace = true } task_executor = { workspace = true } -tempfile = { workspace = true } +tempfile = { workspace = true, optional = true } tokio = { workspace = true } tokio-stream = { workspace = true } tracing = { workspace = true } @@ -53,7 +56,10 @@ warp_utils = { workspace = true } zeroize = { workspace = true } [dev-dependencies] +deposit_contract = { workspace = true } +doppelganger_service = { workspace = true } futures = { workspace = true } itertools = { workspace = true } rand = { workspace = true, features = ["small_rng"] } ssz_types = { workspace = true } +tempfile = { workspace = true } diff --git a/validator_client/http_api/src/lib.rs b/validator_client/http_api/src/lib.rs index a35b4ec6c6..8e9c077e57 100644 --- a/validator_client/http_api/src/lib.rs +++ b/validator_client/http_api/src/lib.rs @@ -1,3 +1,6 @@ +#[cfg(feature = "testing")] +pub mod test_utils; + mod api_secret; mod create_signed_voluntary_exit; mod create_validator; @@ -6,7 +9,6 @@ mod keystores; mod remotekeys; mod tests; -pub mod test_utils; pub use api_secret::PK_FILENAME; use graffiti::{delete_graffiti, get_graffiti, set_graffiti}; diff --git a/validator_manager/Cargo.toml b/validator_manager/Cargo.toml index 16ce1e023f..d0155698b4 100644 --- a/validator_manager/Cargo.toml +++ b/validator_manager/Cargo.toml @@ -29,4 +29,4 @@ beacon_chain = { workspace = true } http_api = { workspace = true } regex = { workspace = true } tempfile = { workspace = true } -validator_http_api = { workspace = true } +validator_http_api = { workspace = true, features = ["testing"] } From 9dccfb540f3cd9c5a0f0f9ac18a007f39b3a3b89 Mon Sep 17 00:00:00 2001 From: chonghe <44791194+chong-he@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:48:30 +0800 Subject: [PATCH 57/81] update cargo-sort (#8933) Co-Authored-By: Tan Chee Keong <tanck@sigmaprime.io> --- Cargo.toml | 29 +++-------------------------- common/logging/Cargo.toml | 2 +- common/malloc_utils/Cargo.toml | 5 +---- 3 files changed, 5 insertions(+), 31 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 667ba1f803..222392bcb7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -166,20 +166,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"] } libsecp256k1 = "0.7" lighthouse_network = { path = "beacon_node/lighthouse_network" } lighthouse_validator_store = { path = "validator_client/lighthouse_validator_store" } @@ -219,12 +206,7 @@ r2d2 = "0.8" rand = "0.9.0" rayon = "1.7" regex = "1" -reqwest = { version = "0.12", default-features = false, features = [ - "blocking", - "json", - "stream", - "rustls-tls", -] } +reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "stream", "rustls-tls"] } ring = "0.17" rpds = "0.11" rusqlite = { version = "0.38", features = ["bundled"] } @@ -253,12 +235,7 @@ sysinfo = "0.26" system_health = { path = "common/system_health" } task_executor = { path = "common/task_executor" } tempfile = "3" -tokio = { version = "1", features = [ - "rt-multi-thread", - "sync", - "signal", - "macros", -] } +tokio = { version = "1", features = ["rt-multi-thread", "sync", "signal", "macros"] } tokio-stream = { version = "0.1", features = ["sync"] } tokio-util = { version = "0.7", features = ["codec", "compat", "time"] } tracing = "0.1.40" diff --git a/common/logging/Cargo.toml b/common/logging/Cargo.toml index 41c82dbd61..cbebd1a501 100644 --- a/common/logging/Cargo.toml +++ b/common/logging/Cargo.toml @@ -13,7 +13,7 @@ logroller = { workspace = true } metrics = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -tokio = { workspace = true, features = [ "time" ] } +tokio = { workspace = true, features = ["time"] } tracing = { workspace = true } tracing-appender = { workspace = true } tracing-core = { workspace = true } diff --git a/common/malloc_utils/Cargo.toml b/common/malloc_utils/Cargo.toml index 1052128852..e90490bf09 100644 --- a/common/malloc_utils/Cargo.toml +++ b/common/malloc_utils/Cargo.toml @@ -35,7 +35,4 @@ tikv-jemallocator = { version = "0.6.0", optional = true, features = ["stats"] } # Jemalloc's background_threads feature requires Linux (pthreads). [target.'cfg(target_os = "linux")'.dependencies] -tikv-jemallocator = { version = "0.6.0", optional = true, features = [ - "stats", - "background_threads", -] } +tikv-jemallocator = { version = "0.6.0", optional = true, features = ["stats", "background_threads"] } From 9c4715c251ea19b2cc4c7688916b5cddfa2b1778 Mon Sep 17 00:00:00 2001 From: Mac L <mjladson@pm.me> Date: Fri, 6 Mar 2026 09:54:43 +0200 Subject: [PATCH 58/81] Fix lints for Rust v1.94.0 (#8939) Following the release of Rust v1.94.0 there are new Clippy lints which do not pass and are blocking CI (which pulls in the latest version of Rust) This is pretty much the minimum just to get CI running again. Most of the errors involve error types being too large. For now I've added allows but later it might be worth doing a refactor to `Box` or otherwise remove the problematic error types. Co-Authored-By: Mac L <mjladson@pm.me> --- beacon_node/beacon_chain/tests/attestation_verification.rs | 1 + beacon_node/beacon_chain/tests/payload_invalidation.rs | 1 + beacon_node/beacon_chain/tests/store_tests.rs | 1 + beacon_node/execution_layer/src/lib.rs | 2 +- beacon_node/http_api/src/lib.rs | 1 + slasher/service/src/lib.rs | 1 + 6 files changed, 6 insertions(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/tests/attestation_verification.rs b/beacon_node/beacon_chain/tests/attestation_verification.rs index e8ee628f28..acf326430b 100644 --- a/beacon_node/beacon_chain/tests/attestation_verification.rs +++ b/beacon_node/beacon_chain/tests/attestation_verification.rs @@ -1,4 +1,5 @@ #![cfg(not(debug_assertions))] +#![allow(clippy::result_large_err)] use beacon_chain::attestation_verification::{ Error, batch_verify_aggregated_attestations, batch_verify_unaggregated_attestations, diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index b282adecd5..bcc50990ec 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -1,4 +1,5 @@ #![cfg(not(debug_assertions))] +#![allow(clippy::result_large_err)] use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::{ diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index cfc53c8ce0..b6d729cc61 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -1,4 +1,5 @@ #![cfg(not(debug_assertions))] +#![allow(clippy::result_large_err)] use beacon_chain::attestation_verification::Error as AttnError; use beacon_chain::block_verification_types::RpcBlock; diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index d6796f6a05..90968fa213 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -2048,7 +2048,7 @@ fn verify_builder_bid<E: EthSpec>( .cloned() .map(|withdrawals| { Withdrawals::<E>::try_from(withdrawals) - .map_err(InvalidBuilderPayload::SszTypesError) + .map_err(|e| Box::new(InvalidBuilderPayload::SszTypesError(e))) .map(|w| w.tree_hash_root()) }) .transpose()?; diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 74710c4ed2..e9dfa2876a 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -1,3 +1,4 @@ +#![allow(clippy::result_large_err)] //! This crate contains a HTTP server which serves the endpoints listed here: //! //! https://github.com/ethereum/beacon-APIs diff --git a/slasher/service/src/lib.rs b/slasher/service/src/lib.rs index ac15b49ee9..69ec59aa2c 100644 --- a/slasher/service/src/lib.rs +++ b/slasher/service/src/lib.rs @@ -1,3 +1,4 @@ +#![allow(clippy::result_large_err)] mod service; pub use service::SlasherService; From dbfb6fd9231f5a7c74667e5adbdaddacf4f1b768 Mon Sep 17 00:00:00 2001 From: Mac L <mjladson@pm.me> Date: Sat, 7 Mar 2026 01:09:31 +0200 Subject: [PATCH 59/81] Remove `arbitrary-fuzz` (#8936) We have duplicated features which enable `arbitrary` throughout the codebase. These are `arbitrary` and `arbitrary-fuzz`. I think historically these were supposed to be distinct however in practice these function identically and so we can unify them into a single feature to avoid confusion. Co-Authored-By: Mac L <mjladson@pm.me> --- Makefile | 4 ++-- common/eip_3076/Cargo.toml | 2 +- common/eip_3076/src/lib.rs | 10 +++++----- consensus/state_processing/Cargo.toml | 4 ++-- consensus/state_processing/src/envelope_processing.rs | 2 +- consensus/state_processing/src/per_block_processing.rs | 8 ++++---- .../per_epoch_processing/base/validator_statuses.rs | 10 +++++----- consensus/state_processing/src/verify_operation.rs | 8 ++++---- consensus/types/Cargo.toml | 1 - validator_client/slashing_protection/Cargo.toml | 2 +- .../slashing_protection/src/interchange_test.rs | 8 ++++---- 11 files changed, 29 insertions(+), 30 deletions(-) diff --git a/Makefile b/Makefile index 9786c17cc9..ad1bbbb8e8 100644 --- a/Makefile +++ b/Makefile @@ -321,8 +321,8 @@ make-ef-tests-nightly: # Verifies that crates compile with fuzzing features enabled arbitrary-fuzz: - cargo check -p state_processing --features arbitrary-fuzz,$(TEST_FEATURES) - cargo check -p slashing_protection --features arbitrary-fuzz,$(TEST_FEATURES) + cargo check -p state_processing --features arbitrary,$(TEST_FEATURES) + cargo check -p slashing_protection --features arbitrary,$(TEST_FEATURES) # Runs cargo audit (Audit Cargo.lock files for crates with security vulnerabilities reported to the RustSec Advisory Database) audit: install-audit audit-CI diff --git a/common/eip_3076/Cargo.toml b/common/eip_3076/Cargo.toml index 058e1fd1a0..157fe12cb3 100644 --- a/common/eip_3076/Cargo.toml +++ b/common/eip_3076/Cargo.toml @@ -6,7 +6,7 @@ edition = { workspace = true } [features] default = [] -arbitrary-fuzz = ["dep:arbitrary", "types/arbitrary"] +arbitrary = ["dep:arbitrary", "types/arbitrary"] json = ["dep:serde_json"] [dependencies] diff --git a/common/eip_3076/src/lib.rs b/common/eip_3076/src/lib.rs index cdd05d7b1e..0bf1a94d0e 100644 --- a/common/eip_3076/src/lib.rs +++ b/common/eip_3076/src/lib.rs @@ -13,7 +13,7 @@ pub enum Error { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] #[serde(deny_unknown_fields)] -#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct InterchangeMetadata { #[serde(with = "serde_utils::quoted_u64::require_quotes")] pub interchange_format_version: u64, @@ -22,7 +22,7 @@ pub struct InterchangeMetadata { #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] #[serde(deny_unknown_fields)] -#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct InterchangeData { pub pubkey: PublicKeyBytes, pub signed_blocks: Vec<SignedBlock>, @@ -31,7 +31,7 @@ pub struct InterchangeData { #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] #[serde(deny_unknown_fields)] -#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct SignedBlock { #[serde(with = "serde_utils::quoted_u64::require_quotes")] pub slot: Slot, @@ -41,7 +41,7 @@ pub struct SignedBlock { #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] #[serde(deny_unknown_fields)] -#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct SignedAttestation { #[serde(with = "serde_utils::quoted_u64::require_quotes")] pub source_epoch: Epoch, @@ -52,7 +52,7 @@ pub struct SignedAttestation { } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] -#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct Interchange { pub metadata: InterchangeMetadata, pub data: Vec<InterchangeData>, diff --git a/consensus/state_processing/Cargo.toml b/consensus/state_processing/Cargo.toml index 7426995439..ae0af03231 100644 --- a/consensus/state_processing/Cargo.toml +++ b/consensus/state_processing/Cargo.toml @@ -7,10 +7,10 @@ edition = { workspace = true } [features] default = [] fake_crypto = ["bls/fake_crypto"] -arbitrary-fuzz = [ +arbitrary = [ "dep:arbitrary", "smallvec/arbitrary", - "types/arbitrary-fuzz", + "types/arbitrary", "merkle_proof/arbitrary", "ethereum_ssz/arbitrary", "ssz_types/arbitrary", diff --git a/consensus/state_processing/src/envelope_processing.rs b/consensus/state_processing/src/envelope_processing.rs index c2cfeae5d3..be6b7c1b29 100644 --- a/consensus/state_processing/src/envelope_processing.rs +++ b/consensus/state_processing/src/envelope_processing.rs @@ -21,7 +21,7 @@ macro_rules! envelope_verify { } /// The strategy to be used when validating the payloads state root. -#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive(PartialEq, Clone, Copy)] pub enum VerifyStateRoot { /// Validate state root. diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index 037e1c7cc7..5aa610e98e 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -55,12 +55,12 @@ use crate::common::update_progressive_balances_cache::{ initialize_progressive_balances_cache, update_progressive_balances_metrics, }; use crate::epoch_cache::initialize_epoch_cache; -#[cfg(feature = "arbitrary-fuzz")] +#[cfg(feature = "arbitrary")] use arbitrary::Arbitrary; use tracing::instrument; /// The strategy to be used when validating the block's signatures. -#[cfg_attr(feature = "arbitrary-fuzz", derive(Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[derive(PartialEq, Clone, Copy, Debug)] pub enum BlockSignatureStrategy { /// Do not validate any signature. Use with caution. @@ -74,7 +74,7 @@ pub enum BlockSignatureStrategy { } /// The strategy to be used when validating the block's signatures. -#[cfg_attr(feature = "arbitrary-fuzz", derive(Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[derive(PartialEq, Clone, Copy)] pub enum VerifySignatures { /// Validate all signatures encountered. @@ -90,7 +90,7 @@ impl VerifySignatures { } /// Control verification of the latest block header. -#[cfg_attr(feature = "arbitrary-fuzz", derive(Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[derive(PartialEq, Clone, Copy)] pub enum VerifyBlockRoot { True, diff --git a/consensus/state_processing/src/per_epoch_processing/base/validator_statuses.rs b/consensus/state_processing/src/per_epoch_processing/base/validator_statuses.rs index c5ec80b92a..3e4f7e8189 100644 --- a/consensus/state_processing/src/per_epoch_processing/base/validator_statuses.rs +++ b/consensus/state_processing/src/per_epoch_processing/base/validator_statuses.rs @@ -2,7 +2,7 @@ use crate::common::attesting_indices_base::get_attesting_indices; use safe_arith::SafeArith; use types::{BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, PendingAttestation}; -#[cfg(feature = "arbitrary-fuzz")] +#[cfg(feature = "arbitrary")] use arbitrary::Arbitrary; /// Sets the boolean `var` on `self` to be true if it is true on `other`. Otherwise leaves `self` @@ -16,7 +16,7 @@ macro_rules! set_self_if_other_is_true { } /// The information required to reward a block producer for including an attestation in a block. -#[cfg_attr(feature = "arbitrary-fuzz", derive(Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[derive(Debug, Clone, Copy, PartialEq)] pub struct InclusionInfo { /// The distance between the attestation slot and the slot that attestation was included in a @@ -48,7 +48,7 @@ impl InclusionInfo { } /// Information required to reward some validator during the current and previous epoch. -#[cfg_attr(feature = "arbitrary-fuzz", derive(Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[derive(Debug, Default, Clone, PartialEq)] pub struct ValidatorStatus { /// True if the validator has been slashed, ever. @@ -118,7 +118,7 @@ impl ValidatorStatus { /// epochs. #[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "arbitrary-fuzz", derive(Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] pub struct TotalBalances { /// The effective balance increment from the spec. effective_balance_increment: u64, @@ -175,7 +175,7 @@ impl TotalBalances { /// Summarised information about validator participation in the _previous and _current_ epochs of /// some `BeaconState`. -#[cfg_attr(feature = "arbitrary-fuzz", derive(Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[derive(Debug, Clone)] pub struct ValidatorStatuses { /// Information about each individual validator from the state's validator registry. diff --git a/consensus/state_processing/src/verify_operation.rs b/consensus/state_processing/src/verify_operation.rs index a13786f9f6..1e9c3d5fe3 100644 --- a/consensus/state_processing/src/verify_operation.rs +++ b/consensus/state_processing/src/verify_operation.rs @@ -7,7 +7,7 @@ use crate::per_block_processing::{ verify_attester_slashing, verify_bls_to_execution_change, verify_exit, verify_proposer_slashing, }; -#[cfg(feature = "arbitrary-fuzz")] +#[cfg(feature = "arbitrary")] use arbitrary::Arbitrary; use educe::Educe; use smallvec::{SmallVec, smallvec}; @@ -41,14 +41,14 @@ pub trait TransformPersist { /// The inner `op` field is private, meaning instances of this type can only be constructed /// by calling `validate`. #[derive(Educe, Debug, Clone)] -#[cfg_attr(feature = "arbitrary-fuzz", derive(Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[educe( PartialEq, Eq, Hash(bound(T: TransformPersist + std::hash::Hash, E: EthSpec)) )] #[cfg_attr( - feature = "arbitrary-fuzz", + feature = "arbitrary", arbitrary(bound = "T: TransformPersist + Arbitrary<'arbitrary>, E: EthSpec") )] pub struct SigVerifiedOp<T: TransformPersist, E: EthSpec> { @@ -139,7 +139,7 @@ struct SigVerifiedOpDecode<P: Decode> { /// We need to store multiple `ForkVersion`s because attester slashings contain two indexed /// attestations which may be signed using different versions. #[derive(Debug, PartialEq, Eq, Clone, Hash, Encode, Decode, TestRandom)] -#[cfg_attr(feature = "arbitrary-fuzz", derive(Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] pub struct VerifiedAgainst { fork_versions: SmallVec<[ForkVersion; MAX_FORKS_VERIFIED_AGAINST]>, } diff --git a/consensus/types/Cargo.toml b/consensus/types/Cargo.toml index e7e382714b..c5ced83320 100644 --- a/consensus/types/Cargo.toml +++ b/consensus/types/Cargo.toml @@ -22,7 +22,6 @@ arbitrary = [ "ssz_types/arbitrary", "swap_or_not_shuffle/arbitrary", ] -arbitrary-fuzz = ["arbitrary"] portable = ["bls/supranational-portable"] [dependencies] diff --git a/validator_client/slashing_protection/Cargo.toml b/validator_client/slashing_protection/Cargo.toml index 695a693385..8017941ca6 100644 --- a/validator_client/slashing_protection/Cargo.toml +++ b/validator_client/slashing_protection/Cargo.toml @@ -6,7 +6,7 @@ edition = { workspace = true } autotests = false [features] -arbitrary-fuzz = ["dep:arbitrary", "types/arbitrary-fuzz", "eip_3076/arbitrary-fuzz"] +arbitrary = ["dep:arbitrary", "types/arbitrary", "eip_3076/arbitrary"] portable = ["types/portable"] [dependencies] diff --git a/validator_client/slashing_protection/src/interchange_test.rs b/validator_client/slashing_protection/src/interchange_test.rs index c5c3df7ea4..996116dd1c 100644 --- a/validator_client/slashing_protection/src/interchange_test.rs +++ b/validator_client/slashing_protection/src/interchange_test.rs @@ -11,7 +11,7 @@ use tempfile::tempdir; use types::{Epoch, Hash256, Slot}; #[derive(Debug, Clone, Deserialize, Serialize)] -#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct MultiTestCase { pub name: String, pub genesis_validators_root: Hash256, @@ -19,7 +19,7 @@ pub struct MultiTestCase { } #[derive(Debug, Clone, Deserialize, Serialize)] -#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct TestCase { pub should_succeed: bool, pub contains_slashable_data: bool, @@ -29,7 +29,7 @@ pub struct TestCase { } #[derive(Debug, Clone, Deserialize, Serialize)] -#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct TestBlock { pub pubkey: PublicKeyBytes, pub slot: Slot, @@ -39,7 +39,7 @@ pub struct TestBlock { } #[derive(Debug, Clone, Deserialize, Serialize)] -#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct TestAttestation { pub pubkey: PublicKeyBytes, pub source_epoch: Epoch, From efe43f769967a971fa3006ec764e622109b04e6d Mon Sep 17 00:00:00 2001 From: Akihito Nakano <sora.akatsuki@gmail.com> Date: Sat, 7 Mar 2026 08:09:33 +0900 Subject: [PATCH 60/81] Fix cargo-sort errors (#8945) The `cargo-sort` job in CI is [failing](https://github.com/sigp/lighthouse/actions/runs/22781651620/job/66088700318?pr=8932) since [cargo-sort v2.1.1](https://github.com/DevinR528/cargo-sort/releases/tag/v2.1.1) has been released, which reports new errors for our Cargo.toml files. Ran `cargo-sort` formatter locally with the new version. Co-Authored-By: ackintosh <sora.akatsuki@gmail.com> --- account_manager/Cargo.toml | 5 +---- beacon_node/Cargo.toml | 13 +++++-------- beacon_node/beacon_chain/Cargo.toml | 9 ++++++--- common/logging/Cargo.toml | 3 ++- consensus/types/Cargo.toml | 5 +---- 5 files changed, 15 insertions(+), 20 deletions(-) diff --git a/account_manager/Cargo.toml b/account_manager/Cargo.toml index 8dd50cbc6e..05e6f12554 100644 --- a/account_manager/Cargo.toml +++ b/account_manager/Cargo.toml @@ -1,10 +1,7 @@ [package] name = "account_manager" version = { workspace = true } -authors = [ - "Paul Hauner <paul@paulhauner.com>", - "Luke Anderson <luke@sigmaprime.io>", -] +authors = ["Paul Hauner <paul@paulhauner.com>", "Luke Anderson <luke@sigmaprime.io>"] edition = { workspace = true } [dependencies] diff --git a/beacon_node/Cargo.toml b/beacon_node/Cargo.toml index 5352814dd5..ebefa6a451 100644 --- a/beacon_node/Cargo.toml +++ b/beacon_node/Cargo.toml @@ -1,10 +1,7 @@ [package] name = "beacon_node" version = { workspace = true } -authors = [ - "Paul Hauner <paul@paulhauner.com>", - "Age Manning <Age@AgeManning.com", -] +authors = ["Paul Hauner <paul@paulhauner.com>", "Age Manning <Age@AgeManning.com"] edition = { workspace = true } [lib] @@ -12,10 +9,10 @@ name = "beacon_node" path = "src/lib.rs" [features] -write_ssz_files = [ - "beacon_chain/write_ssz_files", -] # Writes debugging .ssz files to /tmp during block processing. -testing = [] # Enables testing-only CLI flags +# Writes debugging .ssz files to /tmp during block processing. +write_ssz_files = ["beacon_chain/write_ssz_files"] +# Enables testing-only CLI flags. +testing = [] [dependencies] account_utils = { workspace = true } diff --git a/beacon_node/beacon_chain/Cargo.toml b/beacon_node/beacon_chain/Cargo.toml index eec8836ff4..a06db8934b 100644 --- a/beacon_node/beacon_chain/Cargo.toml +++ b/beacon_node/beacon_chain/Cargo.toml @@ -8,9 +8,12 @@ autotests = false # using a single test binary compiles faster [features] default = ["participation_metrics"] -write_ssz_files = [] # Writes debugging .ssz files to /tmp during block processing. -participation_metrics = [] # Exposes validator participation metrics to Prometheus. -fork_from_env = [] # Initialise the harness chain spec from the FORK_NAME env variable +# Writes debugging .ssz files to /tmp during block processing. +write_ssz_files = [] +# Exposes validator participation metrics to Prometheus. +participation_metrics = [] +# Initialise the harness chain spec from the FORK_NAME env variable +fork_from_env = [] portable = ["bls/supranational-portable"] test_backfill = [] diff --git a/common/logging/Cargo.toml b/common/logging/Cargo.toml index cbebd1a501..1606b8ceb4 100644 --- a/common/logging/Cargo.toml +++ b/common/logging/Cargo.toml @@ -5,7 +5,8 @@ authors = ["blacktemplar <blacktemplar@a1.net>"] edition = { workspace = true } [features] -test_logger = [] # Print log output to stderr when running tests instead of dropping it +# Print log output to stderr when running tests instead of dropping it. +test_logger = [] [dependencies] chrono = { version = "0.4", default-features = false, features = ["clock", "std"] } diff --git a/consensus/types/Cargo.toml b/consensus/types/Cargo.toml index c5ced83320..c09e3d6931 100644 --- a/consensus/types/Cargo.toml +++ b/consensus/types/Cargo.toml @@ -1,10 +1,7 @@ [package] name = "types" version = "0.2.1" -authors = [ - "Paul Hauner <paul@paulhauner.com>", - "Age Manning <Age@AgeManning.com>", -] +authors = ["Paul Hauner <paul@paulhauner.com>", "Age Manning <Age@AgeManning.com>"] edition = { workspace = true } [features] From 537c2ba8b3e49dd9e93dd5df5b67003f5bb91f42 Mon Sep 17 00:00:00 2001 From: Michael Sproul <michaelsproul@users.noreply.github.com> Date: Mon, 9 Mar 2026 11:35:52 +1100 Subject: [PATCH 61/81] Remove `/lighthouse/analysis/block_rewards` APIs (#8935) Mark pointed out that these APIs will require updates for Gloas, so I figured we may as well get rid of them. As far as I know, blockprint was the only use case and it is now defunct. The consensus block value is included in getBlock API responses, so there's no reason for VCs to use the `POST` API, and there is now a standard API for the rewards of canonical blocks. The SSE event was non-standard, and likely only used by blockprint as well. Co-Authored-By: Michael Sproul <michael@sigmaprime.io> --- .github/forbidden-files.txt | 3 + beacon_node/beacon_chain/src/block_reward.rs | 140 -------------- .../beacon_chain/src/block_verification.rs | 18 -- beacon_node/beacon_chain/src/events.rs | 15 -- beacon_node/beacon_chain/src/lib.rs | 1 - beacon_node/http_api/src/block_rewards.rs | 178 ------------------ beacon_node/http_api/src/lib.rs | 34 ---- book/src/api_lighthouse.md | 54 ------ common/eth2/src/lighthouse.rs | 25 +-- common/eth2/src/lighthouse/block_rewards.rs | 60 ------ common/eth2/src/types.rs | 17 -- 11 files changed, 4 insertions(+), 541 deletions(-) delete mode 100644 beacon_node/beacon_chain/src/block_reward.rs delete mode 100644 beacon_node/http_api/src/block_rewards.rs delete mode 100644 common/eth2/src/lighthouse/block_rewards.rs diff --git a/.github/forbidden-files.txt b/.github/forbidden-files.txt index ec89bd2e4b..a08a6b4e98 100644 --- a/.github/forbidden-files.txt +++ b/.github/forbidden-files.txt @@ -5,3 +5,6 @@ beacon_node/beacon_chain/src/otb_verification_service.rs beacon_node/store/src/partial_beacon_state.rs beacon_node/store/src/consensus_context.rs +beacon_node/beacon_chain/src/block_reward.rs +beacon_node/http_api/src/block_rewards.rs +common/eth2/src/lighthouse/block_rewards.rs diff --git a/beacon_node/beacon_chain/src/block_reward.rs b/beacon_node/beacon_chain/src/block_reward.rs deleted file mode 100644 index f3924bb473..0000000000 --- a/beacon_node/beacon_chain/src/block_reward.rs +++ /dev/null @@ -1,140 +0,0 @@ -use crate::{BeaconChain, BeaconChainError, BeaconChainTypes}; -use eth2::lighthouse::{AttestationRewards, BlockReward, BlockRewardMeta}; -use operation_pool::{ - AttMaxCover, MaxCover, PROPOSER_REWARD_DENOMINATOR, RewardCache, SplitAttestation, -}; -use state_processing::{ - common::get_attesting_indices_from_state, - per_block_processing::altair::sync_committee::compute_sync_aggregate_rewards, -}; -use types::{AbstractExecPayload, BeaconBlockRef, BeaconState, EthSpec, Hash256}; - -impl<T: BeaconChainTypes> BeaconChain<T> { - pub fn compute_block_reward<Payload: AbstractExecPayload<T::EthSpec>>( - &self, - block: BeaconBlockRef<'_, T::EthSpec, Payload>, - block_root: Hash256, - state: &BeaconState<T::EthSpec>, - reward_cache: &mut RewardCache, - include_attestations: bool, - ) -> Result<BlockReward, BeaconChainError> { - if block.slot() != state.slot() { - return Err(BeaconChainError::BlockRewardSlotError); - } - - reward_cache.update(state)?; - - let total_active_balance = state.get_total_active_balance()?; - - let split_attestations = block - .body() - .attestations() - .map(|att| { - let attesting_indices = get_attesting_indices_from_state(state, att)?; - Ok(SplitAttestation::new( - att.clone_as_attestation(), - attesting_indices, - )) - }) - .collect::<Result<Vec<_>, BeaconChainError>>()?; - - let mut per_attestation_rewards = split_attestations - .iter() - .map(|att| { - AttMaxCover::new( - att.as_ref(), - state, - reward_cache, - total_active_balance, - &self.spec, - ) - .ok_or(BeaconChainError::BlockRewardAttestationError) - }) - .collect::<Result<Vec<_>, _>>()?; - - // Update the attestation rewards for each previous attestation included. - // This is O(n^2) in the number of attestations n. - for i in 0..per_attestation_rewards.len() { - let (updated, to_update) = per_attestation_rewards.split_at_mut(i + 1); - let latest_att = &updated[i]; - - for att in to_update { - att.update_covering_set(latest_att.intermediate(), latest_att.covering_set()); - } - } - - let mut prev_epoch_total = 0; - let mut curr_epoch_total = 0; - - for cover in &per_attestation_rewards { - if cover.att.data.slot.epoch(T::EthSpec::slots_per_epoch()) == state.current_epoch() { - curr_epoch_total += cover.score() as u64; - } else { - prev_epoch_total += cover.score() as u64; - } - } - - let attestation_total = prev_epoch_total + curr_epoch_total; - - // Drop the covers. - let per_attestation_rewards = per_attestation_rewards - .into_iter() - .map(|cover| { - // Divide each reward numerator by the denominator. This can lead to the total being - // less than the sum of the individual rewards due to the fact that integer division - // does not distribute over addition. - let mut rewards = cover.fresh_validators_rewards; - rewards - .values_mut() - .for_each(|reward| *reward /= PROPOSER_REWARD_DENOMINATOR); - rewards - }) - .collect(); - - // Add the attestation data if desired. - let attestations = if include_attestations { - block - .body() - .attestations() - .map(|a| a.data().clone()) - .collect() - } else { - vec![] - }; - - let attestation_rewards = AttestationRewards { - total: attestation_total, - prev_epoch_total, - curr_epoch_total, - per_attestation_rewards, - attestations, - }; - - // Sync committee rewards. - let sync_committee_rewards = if let Ok(sync_aggregate) = block.body().sync_aggregate() { - let (_, proposer_reward_per_bit) = compute_sync_aggregate_rewards(state, &self.spec) - .map_err(|_| BeaconChainError::BlockRewardSyncError)?; - sync_aggregate.sync_committee_bits.num_set_bits() as u64 * proposer_reward_per_bit - } else { - 0 - }; - - // Total, metadata - let total = attestation_total + sync_committee_rewards; - - let meta = BlockRewardMeta { - slot: block.slot(), - parent_slot: state.latest_block_header().slot, - proposer_index: block.proposer_index(), - graffiti: block.body().graffiti().as_utf8_lossy(), - }; - - Ok(BlockReward { - total, - block_root, - meta, - attestation_rewards, - sync_committee_rewards, - }) - } -} diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index d126c3af00..2021b0d952 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -1571,24 +1571,6 @@ impl<T: BeaconChainTypes> ExecutionPendingBlock<T> { metrics::stop_timer(committee_timer); - /* - * If we have block reward listeners, compute the block reward and push it to the - * event handler. - */ - if let Some(ref event_handler) = chain.event_handler - && event_handler.has_block_reward_subscribers() - { - let mut reward_cache = Default::default(); - let block_reward = chain.compute_block_reward( - block.message(), - block_root, - &state, - &mut reward_cache, - true, - )?; - event_handler.register(EventKind::BlockReward(block_reward)); - } - /* * Perform `per_block_processing` on the block and state, returning early if the block is * invalid. diff --git a/beacon_node/beacon_chain/src/events.rs b/beacon_node/beacon_chain/src/events.rs index 63be944eea..276edc3fe6 100644 --- a/beacon_node/beacon_chain/src/events.rs +++ b/beacon_node/beacon_chain/src/events.rs @@ -21,7 +21,6 @@ pub struct ServerSentEventHandler<E: EthSpec> { late_head: Sender<EventKind<E>>, light_client_finality_update_tx: Sender<EventKind<E>>, light_client_optimistic_update_tx: Sender<EventKind<E>>, - block_reward_tx: Sender<EventKind<E>>, proposer_slashing_tx: Sender<EventKind<E>>, attester_slashing_tx: Sender<EventKind<E>>, bls_to_execution_change_tx: Sender<EventKind<E>>, @@ -48,7 +47,6 @@ impl<E: EthSpec> ServerSentEventHandler<E> { let (late_head, _) = broadcast::channel(capacity); let (light_client_finality_update_tx, _) = broadcast::channel(capacity); let (light_client_optimistic_update_tx, _) = broadcast::channel(capacity); - let (block_reward_tx, _) = broadcast::channel(capacity); let (proposer_slashing_tx, _) = broadcast::channel(capacity); let (attester_slashing_tx, _) = broadcast::channel(capacity); let (bls_to_execution_change_tx, _) = broadcast::channel(capacity); @@ -69,7 +67,6 @@ impl<E: EthSpec> ServerSentEventHandler<E> { late_head, light_client_finality_update_tx, light_client_optimistic_update_tx, - block_reward_tx, proposer_slashing_tx, attester_slashing_tx, bls_to_execution_change_tx, @@ -142,10 +139,6 @@ impl<E: EthSpec> ServerSentEventHandler<E> { .light_client_optimistic_update_tx .send(kind) .map(|count| log_count("light client optimistic update", count)), - EventKind::BlockReward(_) => self - .block_reward_tx - .send(kind) - .map(|count| log_count("block reward", count)), EventKind::ProposerSlashing(_) => self .proposer_slashing_tx .send(kind) @@ -224,10 +217,6 @@ impl<E: EthSpec> ServerSentEventHandler<E> { self.light_client_optimistic_update_tx.subscribe() } - pub fn subscribe_block_reward(&self) -> Receiver<EventKind<E>> { - self.block_reward_tx.subscribe() - } - pub fn subscribe_attester_slashing(&self) -> Receiver<EventKind<E>> { self.attester_slashing_tx.subscribe() } @@ -292,10 +281,6 @@ impl<E: EthSpec> ServerSentEventHandler<E> { self.late_head.receiver_count() > 0 } - pub fn has_block_reward_subscribers(&self) -> bool { - self.block_reward_tx.receiver_count() > 0 - } - pub fn has_proposer_slashing_subscribers(&self) -> bool { self.proposer_slashing_tx.receiver_count() > 0 } diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index e1a190ffb3..4d3c3e193e 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -10,7 +10,6 @@ mod beacon_snapshot; pub mod bellatrix_readiness; pub mod blob_verification; mod block_production; -pub mod block_reward; mod block_times_cache; mod block_verification; pub mod block_verification_types; diff --git a/beacon_node/http_api/src/block_rewards.rs b/beacon_node/http_api/src/block_rewards.rs deleted file mode 100644 index 891f024bf9..0000000000 --- a/beacon_node/http_api/src/block_rewards.rs +++ /dev/null @@ -1,178 +0,0 @@ -use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes, WhenSlotSkipped}; -use eth2::lighthouse::{BlockReward, BlockRewardsQuery}; -use lru::LruCache; -use state_processing::BlockReplayer; -use std::num::NonZeroUsize; -use std::sync::Arc; -use tracing::{debug, warn}; -use types::block::BlindedBeaconBlock; -use types::new_non_zero_usize; -use warp_utils::reject::{beacon_state_error, custom_bad_request, unhandled_error}; - -const STATE_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(2); - -/// Fetch block rewards for blocks from the canonical chain. -pub fn get_block_rewards<T: BeaconChainTypes>( - query: BlockRewardsQuery, - chain: Arc<BeaconChain<T>>, -) -> Result<Vec<BlockReward>, warp::Rejection> { - let start_slot = query.start_slot; - let end_slot = query.end_slot; - let prior_slot = start_slot - 1; - - if start_slot > end_slot || start_slot == 0 { - return Err(custom_bad_request(format!( - "invalid start and end: {}, {}", - start_slot, end_slot - ))); - } - - let end_block_root = chain - .block_root_at_slot(end_slot, WhenSlotSkipped::Prev) - .map_err(unhandled_error)? - .ok_or_else(|| custom_bad_request(format!("block at end slot {} unknown", end_slot)))?; - - let blocks = chain - .store - .load_blocks_to_replay(start_slot, end_slot, end_block_root) - .map_err(|e| unhandled_error(BeaconChainError::from(e)))?; - - let state_root = chain - .state_root_at_slot(prior_slot) - .map_err(unhandled_error)? - .ok_or_else(|| custom_bad_request(format!("prior state at slot {} unknown", prior_slot)))?; - - // This branch is reached from the HTTP API. We assume the user wants - // to cache states so that future calls are faster. - let mut state = chain - .get_state(&state_root, Some(prior_slot), true) - .and_then(|maybe_state| maybe_state.ok_or(BeaconChainError::MissingBeaconState(state_root))) - .map_err(unhandled_error)?; - - state - .build_caches(&chain.spec) - .map_err(beacon_state_error)?; - - let mut reward_cache = Default::default(); - let mut block_rewards = Vec::with_capacity(blocks.len()); - - let block_replayer = BlockReplayer::new(state, &chain.spec) - .pre_block_hook(Box::new(|state, block| { - state.build_all_committee_caches(&chain.spec)?; - - // Compute block reward. - let block_reward = chain.compute_block_reward( - block.message(), - block.canonical_root(), - state, - &mut reward_cache, - query.include_attestations, - )?; - block_rewards.push(block_reward); - Ok(()) - })) - .state_root_iter( - chain - .forwards_iter_state_roots_until(prior_slot, end_slot) - .map_err(unhandled_error)?, - ) - .no_signature_verification() - .minimal_block_root_verification() - .apply_blocks(blocks, None) - .map_err(unhandled_error)?; - - if block_replayer.state_root_miss() { - warn!(%start_slot, %end_slot, "Block reward state root miss"); - } - - drop(block_replayer); - - Ok(block_rewards) -} - -/// Compute block rewards for blocks passed in as input. -pub fn compute_block_rewards<T: BeaconChainTypes>( - blocks: Vec<BlindedBeaconBlock<T::EthSpec>>, - chain: Arc<BeaconChain<T>>, -) -> Result<Vec<BlockReward>, warp::Rejection> { - let mut block_rewards = Vec::with_capacity(blocks.len()); - let mut state_cache = LruCache::new(STATE_CACHE_SIZE); - let mut reward_cache = Default::default(); - - for block in blocks { - let parent_root = block.parent_root(); - - // Check LRU cache for a constructed state from a previous iteration. - let state = if let Some(state) = state_cache.get(&(parent_root, block.slot())) { - debug!( - ?parent_root, - slot = %block.slot(), - "Re-using cached state for block rewards" - ); - state - } else { - debug!( - ?parent_root, - slot = %block.slot(), - "Fetching state for block rewards" - ); - let parent_block = chain - .get_blinded_block(&parent_root) - .map_err(unhandled_error)? - .ok_or_else(|| { - custom_bad_request(format!( - "parent block not known or not canonical: {:?}", - parent_root - )) - })?; - - // This branch is reached from the HTTP API. We assume the user wants - // to cache states so that future calls are faster. - let parent_state = chain - .get_state(&parent_block.state_root(), Some(parent_block.slot()), true) - .map_err(unhandled_error)? - .ok_or_else(|| { - custom_bad_request(format!( - "no state known for parent block: {:?}", - parent_root - )) - })?; - - let block_replayer = BlockReplayer::new(parent_state, &chain.spec) - .no_signature_verification() - .state_root_iter([Ok((parent_block.state_root(), parent_block.slot()))].into_iter()) - .minimal_block_root_verification() - .apply_blocks(vec![], Some(block.slot())) - .map_err(unhandled_error::<BeaconChainError>)?; - - if block_replayer.state_root_miss() { - warn!( - parent_slot = %parent_block.slot(), - slot = %block.slot(), - "Block reward state root miss" - ); - } - - let mut state = block_replayer.into_state(); - state - .build_all_committee_caches(&chain.spec) - .map_err(beacon_state_error)?; - - state_cache.get_or_insert((parent_root, block.slot()), || state) - }; - - // Compute block reward. - let block_reward = chain - .compute_block_reward( - block.to_ref(), - block.canonical_root(), - state, - &mut reward_cache, - true, - ) - .map_err(unhandled_error)?; - block_rewards.push(block_reward); - } - - Ok(block_rewards) -} diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index e9dfa2876a..0a0ae683ca 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -12,7 +12,6 @@ mod attester_duties; mod beacon; mod block_id; mod block_packing_efficiency; -mod block_rewards; mod build_block_contents; mod builder_states; mod custody; @@ -3066,34 +3065,6 @@ pub fn serve<T: BeaconChainTypes>( }, ); - // GET lighthouse/analysis/block_rewards - let get_lighthouse_block_rewards = warp::path("lighthouse") - .and(warp::path("analysis")) - .and(warp::path("block_rewards")) - .and(warp::query::<eth2::lighthouse::BlockRewardsQuery>()) - .and(warp::path::end()) - .and(task_spawner_filter.clone()) - .and(chain_filter.clone()) - .then(|query, task_spawner: TaskSpawner<T::EthSpec>, chain| { - task_spawner.blocking_json_task(Priority::P1, move || { - block_rewards::get_block_rewards(query, chain) - }) - }); - - // POST lighthouse/analysis/block_rewards - let post_lighthouse_block_rewards = warp::path("lighthouse") - .and(warp::path("analysis")) - .and(warp::path("block_rewards")) - .and(warp_utils::json::json()) - .and(warp::path::end()) - .and(task_spawner_filter.clone()) - .and(chain_filter.clone()) - .then(|blocks, task_spawner: TaskSpawner<T::EthSpec>, chain| { - task_spawner.blocking_json_task(Priority::P1, move || { - block_rewards::compute_block_rewards(blocks, chain) - }) - }); - // GET lighthouse/analysis/attestation_performance/{index} let get_lighthouse_attestation_performance = warp::path("lighthouse") .and(warp::path("analysis")) @@ -3184,9 +3155,6 @@ pub fn serve<T: BeaconChainTypes>( api_types::EventTopic::LightClientOptimisticUpdate => { event_handler.subscribe_light_client_optimistic_update() } - api_types::EventTopic::BlockReward => { - event_handler.subscribe_block_reward() - } api_types::EventTopic::AttesterSlashing => { event_handler.subscribe_attester_slashing() } @@ -3363,7 +3331,6 @@ pub fn serve<T: BeaconChainTypes>( .uor(get_lighthouse_staking) .uor(get_lighthouse_database_info) .uor(get_lighthouse_custody_info) - .uor(get_lighthouse_block_rewards) .uor(get_lighthouse_attestation_performance) .uor(get_beacon_light_client_optimistic_update) .uor(get_beacon_light_client_finality_update) @@ -3414,7 +3381,6 @@ pub fn serve<T: BeaconChainTypes>( .uor(post_validator_liveness_epoch) .uor(post_lighthouse_liveness) .uor(post_lighthouse_database_reconstruct) - .uor(post_lighthouse_block_rewards) .uor(post_lighthouse_ui_validator_metrics) .uor(post_lighthouse_ui_validator_info) .uor(post_lighthouse_finalize) diff --git a/book/src/api_lighthouse.md b/book/src/api_lighthouse.md index 0442bf4ec0..2fd7290cb2 100644 --- a/book/src/api_lighthouse.md +++ b/book/src/api_lighthouse.md @@ -590,60 +590,6 @@ Caveats: This is because the state *prior* to the `start_epoch` needs to be loaded from the database, and loading a state on a boundary is most efficient. -## `/lighthouse/analysis/block_rewards` - -Fetch information about the block rewards paid to proposers for a range of consecutive blocks. - -Two query parameters are required: - -- `start_slot` (inclusive): the slot of the first block to compute rewards for. -- `end_slot` (inclusive): the slot of the last block to compute rewards for. - -Example: - -```bash -curl -X GET "http://localhost:5052/lighthouse/analysis/block_rewards?start_slot=1&end_slot=1" | jq -``` - -The first few lines of the response would look like: - -```json -[ - { - "total": 637260, - "block_root": "0x4a089c5e390bb98e66b27358f157df825128ea953cee9d191229c0bcf423a4f6", - "meta": { - "slot": "1", - "parent_slot": "0", - "proposer_index": 93, - "graffiti": "EF #vm-eth2-raw-iron-101" - }, - "attestation_rewards": { - "total": 637260, - "prev_epoch_total": 0, - "curr_epoch_total": 637260, - "per_attestation_rewards": [ - { - "50102": 780, - } - ] - } - } -] -``` - -Caveats: - -- Presently only attestation and sync committee rewards are computed. -- The output format is verbose and subject to change. Please see [`BlockReward`][block_reward_src] - in the source. -- For maximum efficiency the `start_slot` should satisfy `start_slot % slots_per_restore_point == 1`. - This is because the state *prior* to the `start_slot` needs to be loaded from the database, and - loading a state on a boundary is most efficient. - -[block_reward_src]: -https://github.com/sigp/lighthouse/tree/unstable/common/eth2/src/lighthouse/block_rewards.rs - ## `/lighthouse/analysis/block_packing` Fetch information about the block packing efficiency of blocks for a range of consecutive diff --git a/common/eth2/src/lighthouse.rs b/common/eth2/src/lighthouse.rs index 993c263cbf..3c039b16b3 100644 --- a/common/eth2/src/lighthouse.rs +++ b/common/eth2/src/lighthouse.rs @@ -2,12 +2,11 @@ mod attestation_performance; mod block_packing_efficiency; -mod block_rewards; mod custody; pub mod sync_state; use crate::{ - BeaconNodeHttpClient, DepositData, Error, Hash256, Slot, + BeaconNodeHttpClient, DepositData, Error, Hash256, lighthouse::sync_state::SyncState, types::{AdminPeer, Epoch, GenericResponse, ValidatorId}, }; @@ -22,7 +21,6 @@ pub use attestation_performance::{ pub use block_packing_efficiency::{ BlockPackingEfficiency, BlockPackingEfficiencyQuery, ProposerInfo, UniqueAttestation, }; -pub use block_rewards::{AttestationRewards, BlockReward, BlockRewardMeta, BlockRewardsQuery}; pub use custody::CustodyInfo; // Define "legacy" implementations of `Option<T>` which use four bytes for encoding the union @@ -317,27 +315,6 @@ impl BeaconNodeHttpClient { Analysis endpoints. */ - /// `GET` lighthouse/analysis/block_rewards?start_slot,end_slot - pub async fn get_lighthouse_analysis_block_rewards( - &self, - start_slot: Slot, - end_slot: Slot, - ) -> Result<Vec<BlockReward>, Error> { - let mut path = self.server.expose_full().clone(); - - path.path_segments_mut() - .map_err(|()| Error::InvalidUrl(self.server.clone()))? - .push("lighthouse") - .push("analysis") - .push("block_rewards"); - - path.query_pairs_mut() - .append_pair("start_slot", &start_slot.to_string()) - .append_pair("end_slot", &end_slot.to_string()); - - self.get(path).await - } - /// `GET` lighthouse/analysis/block_packing?start_epoch,end_epoch pub async fn get_lighthouse_analysis_block_packing( &self, diff --git a/common/eth2/src/lighthouse/block_rewards.rs b/common/eth2/src/lighthouse/block_rewards.rs deleted file mode 100644 index 38070f3539..0000000000 --- a/common/eth2/src/lighthouse/block_rewards.rs +++ /dev/null @@ -1,60 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use types::{AttestationData, Hash256, Slot}; - -/// Details about the rewards paid to a block proposer for proposing a block. -/// -/// All rewards in GWei. -/// -/// Presently this only counts attestation rewards, but in future should be expanded -/// to include information on slashings and sync committee aggregates too. -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] -pub struct BlockReward { - /// Sum of all reward components. - pub total: u64, - /// Block root of the block that these rewards are for. - pub block_root: Hash256, - /// Metadata about the block, particularly reward-relevant metadata. - pub meta: BlockRewardMeta, - /// Rewards due to attestations. - pub attestation_rewards: AttestationRewards, - /// Sum of rewards due to sync committee signatures. - pub sync_committee_rewards: u64, -} - -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] -pub struct BlockRewardMeta { - pub slot: Slot, - pub parent_slot: Slot, - pub proposer_index: u64, - pub graffiti: String, -} - -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] -pub struct AttestationRewards { - /// Total block reward from attestations included. - pub total: u64, - /// Total rewards from previous epoch attestations. - pub prev_epoch_total: u64, - /// Total rewards from current epoch attestations. - pub curr_epoch_total: u64, - /// Vec of attestation rewards for each attestation included. - /// - /// Each element of the vec is a map from validator index to reward. - pub per_attestation_rewards: Vec<HashMap<u64, u64>>, - /// The attestations themselves (optional). - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub attestations: Vec<AttestationData>, -} - -/// Query parameters for the `/lighthouse/block_rewards` endpoint. -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] -pub struct BlockRewardsQuery { - /// Lower slot limit for block rewards returned (inclusive). - pub start_slot: Slot, - /// Upper slot limit for block rewards returned (inclusive). - pub end_slot: Slot, - /// Include the full attestations themselves? - #[serde(default)] - pub include_attestations: bool, -} diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index f8376d430c..2f86170812 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -37,9 +37,6 @@ pub mod beacon_response { pub use crate::beacon_response::*; } -#[cfg(feature = "lighthouse")] -use crate::lighthouse::BlockReward; - // Re-export error types from the unified error module pub use crate::error::{ErrorMessage, Failure, IndexedErrorMessage, ResponseError as Error}; @@ -1199,8 +1196,6 @@ pub enum EventKind<E: EthSpec> { LateHead(SseLateHead), LightClientFinalityUpdate(Box<BeaconResponse<LightClientFinalityUpdate<E>>>), LightClientOptimisticUpdate(Box<BeaconResponse<LightClientOptimisticUpdate<E>>>), - #[cfg(feature = "lighthouse")] - BlockReward(BlockReward), PayloadAttributes(VersionedSsePayloadAttributes), ProposerSlashing(Box<ProposerSlashing>), AttesterSlashing(Box<AttesterSlashing<E>>), @@ -1225,8 +1220,6 @@ impl<E: EthSpec> EventKind<E> { EventKind::LateHead(_) => "late_head", EventKind::LightClientFinalityUpdate(_) => "light_client_finality_update", EventKind::LightClientOptimisticUpdate(_) => "light_client_optimistic_update", - #[cfg(feature = "lighthouse")] - EventKind::BlockReward(_) => "block_reward", EventKind::ProposerSlashing(_) => "proposer_slashing", EventKind::AttesterSlashing(_) => "attester_slashing", EventKind::BlsToExecutionChange(_) => "bls_to_execution_change", @@ -1302,10 +1295,6 @@ impl<E: EthSpec> EventKind<E> { })?), ))) } - #[cfg(feature = "lighthouse")] - "block_reward" => Ok(EventKind::BlockReward(serde_json::from_str(data).map_err( - |e| ServerError::InvalidServerSentEvent(format!("Block Reward: {:?}", e)), - )?)), "attester_slashing" => Ok(EventKind::AttesterSlashing( serde_json::from_str(data).map_err(|e| { ServerError::InvalidServerSentEvent(format!("Attester Slashing: {:?}", e)) @@ -1355,8 +1344,6 @@ pub enum EventTopic { PayloadAttributes, LightClientFinalityUpdate, LightClientOptimisticUpdate, - #[cfg(feature = "lighthouse")] - BlockReward, AttesterSlashing, ProposerSlashing, BlsToExecutionChange, @@ -1382,8 +1369,6 @@ impl FromStr for EventTopic { "late_head" => Ok(EventTopic::LateHead), "light_client_finality_update" => Ok(EventTopic::LightClientFinalityUpdate), "light_client_optimistic_update" => Ok(EventTopic::LightClientOptimisticUpdate), - #[cfg(feature = "lighthouse")] - "block_reward" => Ok(EventTopic::BlockReward), "attester_slashing" => Ok(EventTopic::AttesterSlashing), "proposer_slashing" => Ok(EventTopic::ProposerSlashing), "bls_to_execution_change" => Ok(EventTopic::BlsToExecutionChange), @@ -1410,8 +1395,6 @@ impl fmt::Display for EventTopic { EventTopic::LateHead => write!(f, "late_head"), EventTopic::LightClientFinalityUpdate => write!(f, "light_client_finality_update"), EventTopic::LightClientOptimisticUpdate => write!(f, "light_client_optimistic_update"), - #[cfg(feature = "lighthouse")] - EventTopic::BlockReward => write!(f, "block_reward"), EventTopic::AttesterSlashing => write!(f, "attester_slashing"), EventTopic::ProposerSlashing => write!(f, "proposer_slashing"), EventTopic::BlsToExecutionChange => write!(f, "bls_to_execution_change"), From 7dab32dd16c997f592836f9728b2a2b4bf3ba756 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi <eserilev@gmail.com> Date: Mon, 9 Mar 2026 14:23:34 +0900 Subject: [PATCH 62/81] Gloas payload envelope processing (#8806) Co-Authored-By: Eitan Seri- Levi <eserilev@gmail.com> Co-Authored-By: Eitan Seri-Levi <eserilev@ucsc.edu> Co-Authored-By: Jimmy Chen <jchen.tc@gmail.com> Co-Authored-By: Michael Sproul <michael@sigmaprime.io> Co-Authored-By: Michael Sproul <michaelsproul@users.noreply.github.com> --- beacon_node/beacon_chain/src/beacon_chain.rs | 106 +---- .../beacon_chain/src/beacon_proposer_cache.rs | 79 +++- .../beacon_chain/src/block_verification.rs | 5 +- beacon_node/beacon_chain/src/builder.rs | 1 + .../beacon_chain/src/envelope_times_cache.rs | 197 ++++++++ .../beacon_chain/src/execution_payload.rs | 37 +- .../beacon_chain/src/historical_blocks.rs | 9 +- beacon_node/beacon_chain/src/lib.rs | 2 + beacon_node/beacon_chain/src/metrics.rs | 28 ++ .../execution_pending_envelope.rs | 105 +++++ .../gossip_verified_envelope.rs | 445 ++++++++++++++++++ .../payload_envelope_verification/import.rs | 354 ++++++++++++++ .../src/payload_envelope_verification/mod.rs | 285 +++++++++++ .../payload_notifier.rs | 94 ++++ .../beacon_chain/tests/block_verification.rs | 2 +- beacon_node/network/src/metrics.rs | 10 + .../gossip_methods.rs | 172 ++++++- .../src/network_beacon_processor/mod.rs | 8 +- beacon_node/network/src/router.rs | 1 + 19 files changed, 1813 insertions(+), 127 deletions(-) create mode 100644 beacon_node/beacon_chain/src/envelope_times_cache.rs create mode 100644 beacon_node/beacon_chain/src/payload_envelope_verification/execution_pending_envelope.rs create mode 100644 beacon_node/beacon_chain/src/payload_envelope_verification/gossip_verified_envelope.rs create mode 100644 beacon_node/beacon_chain/src/payload_envelope_verification/import.rs create mode 100644 beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs create mode 100644 beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 703ed24420..07f3bb01fa 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -4,9 +4,7 @@ use crate::attestation_verification::{ batch_verify_unaggregated_attestations, }; use crate::beacon_block_streamer::{BeaconBlockStreamer, CheckCaches}; -use crate::beacon_proposer_cache::{ - BeaconProposerCache, EpochBlockProposers, ensure_state_can_determine_proposers_for_epoch, -}; +use crate::beacon_proposer_cache::{BeaconProposerCache, EpochBlockProposers}; use crate::blob_verification::{GossipBlobError, GossipVerifiedBlob}; use crate::block_times_cache::BlockTimesCache; use crate::block_verification::{ @@ -26,6 +24,7 @@ use crate::data_availability_checker::{ }; use crate::data_column_verification::{GossipDataColumnError, GossipVerifiedDataColumn}; use crate::early_attester_cache::EarlyAttesterCache; +use crate::envelope_times_cache::EnvelopeTimesCache; use crate::errors::{BeaconChainError as Error, BlockProductionError}; use crate::events::ServerSentEventHandler; use crate::execution_payload::{NotifyExecutionLayer, PreparePayloadHandle, get_execution_payload}; @@ -66,7 +65,6 @@ use crate::sync_committee_verification::{ }; use crate::validator_monitor::{ HISTORIC_EPOCHS as VALIDATOR_MONITOR_HISTORIC_EPOCHS, ValidatorMonitor, get_slot_delay_ms, - timestamp_now, }; use crate::validator_pubkey_cache::ValidatorPubkeyCache; use crate::{ @@ -462,6 +460,8 @@ pub struct BeaconChain<T: BeaconChainTypes> { pub early_attester_cache: EarlyAttesterCache<T::EthSpec>, /// A cache used to keep track of various block timings. pub block_times_cache: Arc<RwLock<BlockTimesCache>>, + /// A cache used to keep track of various envelope timings. + pub envelope_times_cache: Arc<RwLock<EnvelopeTimesCache>>, /// A cache used to track pre-finalization block roots for quick rejection. pub pre_finalization_block_cache: PreFinalizationBlockCache, /// A cache used to produce light_client server messages @@ -4042,23 +4042,10 @@ impl<T: BeaconChainTypes> BeaconChain<T> { // See https://github.com/sigp/lighthouse/issues/2028 let (_, signed_block, block_data) = signed_block.deconstruct(); - match self.get_blobs_or_columns_store_op(block_root, signed_block.slot(), block_data) { - Ok(Some(blobs_or_columns_store_op)) => { - ops.push(blobs_or_columns_store_op); - } - Ok(None) => {} - Err(e) => { - error!( - msg = "Restoring fork choice from disk", - error = &e, - ?block_root, - "Failed to store data columns into the database" - ); - return Err(self - .handle_import_block_db_write_error(fork_choice) - .err() - .unwrap_or(BlockError::InternalError(e))); - } + if let Some(blobs_or_columns_store_op) = + self.get_blobs_or_columns_store_op(block_root, signed_block.slot(), block_data) + { + ops.push(blobs_or_columns_store_op); } let block = signed_block.message(); @@ -4088,7 +4075,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> { // We're declaring the block "imported" at this point, since fork choice and the DB know // about it. - let block_time_imported = timestamp_now(); + let block_time_imported = self.slot_clock.now_duration().unwrap_or(Duration::MAX); // compute state proofs for light client updates before inserting the state into the // snapshot cache. @@ -4157,7 +4144,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> { } /// Check block's consistentency with any configured weak subjectivity checkpoint. - fn check_block_against_weak_subjectivity_checkpoint( + pub(crate) fn check_block_against_weak_subjectivity_checkpoint( &self, block: BeaconBlockRef<T::EthSpec>, block_root: Hash256, @@ -6407,6 +6394,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> { // sync anyway). self.naive_aggregation_pool.write().prune(slot); self.block_times_cache.write().prune(slot); + self.envelope_times_cache.write().prune(slot); // Don't run heavy-weight tasks during sync. if self.best_slot() + MAX_PER_SLOT_FORK_CHOICE_DISTANCE < slot { @@ -6466,62 +6454,14 @@ impl<T: BeaconChainTypes> BeaconChain<T> { accessor: impl Fn(&EpochBlockProposers) -> Result<V, BeaconChainError>, state_provider: impl FnOnce() -> Result<(Hash256, BeaconState<T::EthSpec>), E>, ) -> Result<V, E> { - let cache_entry = self - .beacon_proposer_cache - .lock() - .get_or_insert_key(proposal_epoch, shuffling_decision_block); - - // If the cache entry is not initialised, run the code to initialise it inside a OnceCell. - // This prevents duplication of work across multiple threads. - // - // If it is already initialised, then `get_or_try_init` will return immediately without - // executing the initialisation code at all. - let epoch_block_proposers = cache_entry.get_or_try_init(|| { - // Fetch the state on-demand if the required epoch was missing from the cache. - // If the caller wants to not compute the state they must return an error here and then - // catch it at the call site. - let (state_root, mut state) = state_provider()?; - - // Ensure the state can compute proposer duties for `epoch`. - ensure_state_can_determine_proposers_for_epoch( - &mut state, - state_root, - proposal_epoch, - &self.spec, - )?; - - // Sanity check the state. - let latest_block_root = state.get_latest_block_root(state_root); - let state_decision_block_root = state.proposer_shuffling_decision_root_at_epoch( - proposal_epoch, - latest_block_root, - &self.spec, - )?; - if state_decision_block_root != shuffling_decision_block { - return Err(Error::ProposerCacheIncorrectState { - state_decision_block_root, - requested_decision_block_root: shuffling_decision_block, - } - .into()); - } - - let proposers = state.get_beacon_proposer_indices(proposal_epoch, &self.spec)?; - - // Use fork_at_epoch rather than the state's fork, because post-Fulu we may not have - // advanced the state completely into the new epoch. - let fork = self.spec.fork_at_epoch(proposal_epoch); - - debug!( - ?shuffling_decision_block, - epoch = %proposal_epoch, - "Priming proposer shuffling cache" - ); - - Ok::<_, E>(EpochBlockProposers::new(proposal_epoch, fork, proposers)) - })?; - - // Run the accessor function on the computed epoch proposers. - accessor(epoch_block_proposers).map_err(Into::into) + crate::beacon_proposer_cache::with_proposer_cache( + &self.beacon_proposer_cache, + shuffling_decision_block, + proposal_epoch, + accessor, + state_provider, + &self.spec, + ) } /// Runs the `map_fn` with the committee cache for `shuffling_epoch` from the chain with head @@ -7197,16 +7137,16 @@ impl<T: BeaconChainTypes> BeaconChain<T> { block_root: Hash256, block_slot: Slot, block_data: AvailableBlockData<T::EthSpec>, - ) -> Result<Option<StoreOp<'_, T::EthSpec>>, String> { + ) -> Option<StoreOp<'_, T::EthSpec>> { match block_data { - AvailableBlockData::NoData => Ok(None), + AvailableBlockData::NoData => None, AvailableBlockData::Blobs(blobs) => { debug!( %block_root, count = blobs.len(), "Writing blobs to store" ); - Ok(Some(StoreOp::PutBlobs(block_root, blobs))) + Some(StoreOp::PutBlobs(block_root, blobs)) } AvailableBlockData::DataColumns(mut data_columns) => { let columns_to_custody = self.custody_columns_for_epoch(Some( @@ -7222,7 +7162,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> { count = data_columns.len(), "Writing data columns to store" ); - Ok(Some(StoreOp::PutDataColumns(block_root, data_columns))) + Some(StoreOp::PutDataColumns(block_root, data_columns)) } } } diff --git a/beacon_node/beacon_chain/src/beacon_proposer_cache.rs b/beacon_node/beacon_chain/src/beacon_proposer_cache.rs index 912f7f3bad..b258d7471f 100644 --- a/beacon_node/beacon_chain/src/beacon_proposer_cache.rs +++ b/beacon_node/beacon_chain/src/beacon_proposer_cache.rs @@ -12,12 +12,13 @@ use crate::{BeaconChain, BeaconChainError, BeaconChainTypes}; use fork_choice::ExecutionStatus; use lru::LruCache; use once_cell::sync::OnceCell; +use parking_lot::Mutex; use safe_arith::SafeArith; use smallvec::SmallVec; use state_processing::state_advance::partial_state_advance; use std::num::NonZeroUsize; use std::sync::Arc; -use tracing::instrument; +use tracing::{debug, instrument}; use typenum::Unsigned; use types::new_non_zero_usize; use types::{BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, Fork, Hash256, Slot}; @@ -164,6 +165,82 @@ impl BeaconProposerCache { } } +/// Access the proposer cache, computing and caching the proposers if necessary. +/// +/// This is a free function that operates on references to the cache and spec, decoupled from +/// `BeaconChain`. The `accessor` is called with the cached `EpochBlockProposers` for the given +/// `(proposal_epoch, shuffling_decision_block)` key. If the cache entry is missing, the +/// `state_provider` closure is called to produce a state which is then used to compute and +/// cache the proposers. +pub fn with_proposer_cache<Spec, V, Err>( + beacon_proposer_cache: &Mutex<BeaconProposerCache>, + shuffling_decision_block: Hash256, + proposal_epoch: Epoch, + accessor: impl Fn(&EpochBlockProposers) -> Result<V, BeaconChainError>, + state_provider: impl FnOnce() -> Result<(Hash256, BeaconState<Spec>), Err>, + spec: &ChainSpec, +) -> Result<V, Err> +where + Spec: EthSpec, + Err: From<BeaconChainError> + From<BeaconStateError>, +{ + let cache_entry = beacon_proposer_cache + .lock() + .get_or_insert_key(proposal_epoch, shuffling_decision_block); + + // If the cache entry is not initialised, run the code to initialise it inside a OnceCell. + // This prevents duplication of work across multiple threads. + // + // If it is already initialised, then `get_or_try_init` will return immediately without + // executing the initialisation code at all. + let epoch_block_proposers = cache_entry.get_or_try_init(|| { + // Fetch the state on-demand if the required epoch was missing from the cache. + // If the caller wants to not compute the state they must return an error here and then + // catch it at the call site. + let (state_root, mut state) = state_provider()?; + + // Ensure the state can compute proposer duties for `epoch`. + ensure_state_can_determine_proposers_for_epoch( + &mut state, + state_root, + proposal_epoch, + spec, + )?; + + // Sanity check the state. + let latest_block_root = state.get_latest_block_root(state_root); + let state_decision_block_root = state.proposer_shuffling_decision_root_at_epoch( + proposal_epoch, + latest_block_root, + spec, + )?; + if state_decision_block_root != shuffling_decision_block { + return Err(BeaconChainError::ProposerCacheIncorrectState { + state_decision_block_root, + requested_decision_block_root: shuffling_decision_block, + } + .into()); + } + + let proposers = state.get_beacon_proposer_indices(proposal_epoch, spec)?; + + // Use fork_at_epoch rather than the state's fork, because post-Fulu we may not have + // advanced the state completely into the new epoch. + let fork = spec.fork_at_epoch(proposal_epoch); + + debug!( + ?shuffling_decision_block, + epoch = %proposal_epoch, + "Priming proposer shuffling cache" + ); + + Ok::<_, Err>(EpochBlockProposers::new(proposal_epoch, fork, proposers)) + })?; + + // Run the accessor function on the computed epoch proposers. + accessor(epoch_block_proposers).map_err(Into::into) +} + /// Compute the proposer duties using the head state without cache. /// /// Return: diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 2021b0d952..b748bf5c6c 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -681,7 +681,8 @@ pub struct SignatureVerifiedBlock<T: BeaconChainTypes> { } /// Used to await the result of executing payload with an EE. -type PayloadVerificationHandle = JoinHandle<Option<Result<PayloadVerificationOutcome, BlockError>>>; +pub type PayloadVerificationHandle = + JoinHandle<Option<Result<PayloadVerificationOutcome, BlockError>>>; /// A wrapper around a `SignedBeaconBlock` that indicates that this block is fully verified and /// ready to import into the `BeaconChain`. The validation includes: @@ -1357,7 +1358,7 @@ impl<T: BeaconChainTypes> ExecutionPendingBlock<T> { /// verification must be done upstream (e.g., via a `SignatureVerifiedBlock` /// /// Returns an error if the block is invalid, or if the block was unable to be verified. - #[instrument(skip_all, level = "debug")] + #[instrument(skip_all, level = "debug", fields(?block_root))] pub fn from_signature_verified_components( block: MaybeAvailableBlock<T::EthSpec>, block_root: Hash256, diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 66a54d46e8..d5935b492a 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -1023,6 +1023,7 @@ where )), beacon_proposer_cache, block_times_cache: <_>::default(), + envelope_times_cache: <_>::default(), pre_finalization_block_cache: <_>::default(), validator_pubkey_cache: RwLock::new(validator_pubkey_cache), early_attester_cache: <_>::default(), diff --git a/beacon_node/beacon_chain/src/envelope_times_cache.rs b/beacon_node/beacon_chain/src/envelope_times_cache.rs new file mode 100644 index 0000000000..84c936c210 --- /dev/null +++ b/beacon_node/beacon_chain/src/envelope_times_cache.rs @@ -0,0 +1,197 @@ +//! This module provides the `EnvelopeTimesCache` which contains information regarding payload +//! envelope timings. +//! +//! This provides `BeaconChain` and associated functions with access to the timestamps of when a +//! payload envelope was observed, verified, executed, and imported. +//! This allows for better traceability and allows us to determine the root cause for why an +//! envelope was imported late. +//! This allows us to distinguish between the following scenarios: +//! - The envelope was observed late. +//! - Consensus verification was slow. +//! - Execution verification was slow. +//! - The DB write was slow. + +use eth2::types::{Hash256, Slot}; +use std::collections::HashMap; +use std::time::Duration; + +type BlockRoot = Hash256; + +#[derive(Clone, Default)] +pub struct EnvelopeTimestamps { + /// When the envelope was first observed (gossip or RPC). + pub observed: Option<Duration>, + /// When consensus verification (state transition) completed. + pub consensus_verified: Option<Duration>, + /// When execution layer verification started. + pub started_execution: Option<Duration>, + /// When execution layer verification completed. + pub executed: Option<Duration>, + /// When the envelope was imported into the DB. + pub imported: Option<Duration>, +} + +/// Delay data for envelope processing, computed relative to the slot start time. +#[derive(Debug, Default)] +pub struct EnvelopeDelays { + /// Time after start of slot we saw the envelope. + pub observed: Option<Duration>, + /// The time it took to complete consensus verification of the envelope. + pub consensus_verification_time: Option<Duration>, + /// The time it took to complete execution verification of the envelope. + pub execution_time: Option<Duration>, + /// Time after execution until the envelope was imported. + pub imported: Option<Duration>, +} + +impl EnvelopeDelays { + fn new(times: EnvelopeTimestamps, slot_start_time: Duration) -> EnvelopeDelays { + let observed = times + .observed + .and_then(|observed_time| observed_time.checked_sub(slot_start_time)); + let consensus_verification_time = times + .consensus_verified + .and_then(|consensus_verified| consensus_verified.checked_sub(times.observed?)); + let execution_time = times + .executed + .and_then(|executed| executed.checked_sub(times.started_execution?)); + let imported = times + .imported + .and_then(|imported_time| imported_time.checked_sub(times.executed?)); + EnvelopeDelays { + observed, + consensus_verification_time, + execution_time, + imported, + } + } +} + +pub struct EnvelopeTimesCacheValue { + pub slot: Slot, + pub timestamps: EnvelopeTimestamps, + pub peer_id: Option<String>, +} + +impl EnvelopeTimesCacheValue { + fn new(slot: Slot) -> Self { + EnvelopeTimesCacheValue { + slot, + timestamps: Default::default(), + peer_id: None, + } + } +} + +#[derive(Default)] +pub struct EnvelopeTimesCache { + pub cache: HashMap<BlockRoot, EnvelopeTimesCacheValue>, +} + +impl EnvelopeTimesCache { + /// Set the observation time for `block_root` to `timestamp` if `timestamp` is less than + /// any previous timestamp at which this envelope was observed. + pub fn set_time_observed( + &mut self, + block_root: BlockRoot, + slot: Slot, + timestamp: Duration, + peer_id: Option<String>, + ) { + let entry = self + .cache + .entry(block_root) + .or_insert_with(|| EnvelopeTimesCacheValue::new(slot)); + match entry.timestamps.observed { + Some(existing) if existing <= timestamp => { + // Existing timestamp is earlier, do nothing. + } + _ => { + entry.timestamps.observed = Some(timestamp); + entry.peer_id = peer_id; + } + } + } + + /// Set the timestamp for `field` if that timestamp is less than any previously known value. + fn set_time_if_less( + &mut self, + block_root: BlockRoot, + slot: Slot, + field: impl Fn(&mut EnvelopeTimestamps) -> &mut Option<Duration>, + timestamp: Duration, + ) { + let entry = self + .cache + .entry(block_root) + .or_insert_with(|| EnvelopeTimesCacheValue::new(slot)); + let existing_timestamp = field(&mut entry.timestamps); + if existing_timestamp.is_none_or(|prev| timestamp < prev) { + *existing_timestamp = Some(timestamp); + } + } + + pub fn set_time_consensus_verified( + &mut self, + block_root: BlockRoot, + slot: Slot, + timestamp: Duration, + ) { + self.set_time_if_less( + block_root, + slot, + |timestamps| &mut timestamps.consensus_verified, + timestamp, + ) + } + + pub fn set_time_started_execution( + &mut self, + block_root: BlockRoot, + slot: Slot, + timestamp: Duration, + ) { + self.set_time_if_less( + block_root, + slot, + |timestamps| &mut timestamps.started_execution, + timestamp, + ) + } + + pub fn set_time_executed(&mut self, block_root: BlockRoot, slot: Slot, timestamp: Duration) { + self.set_time_if_less( + block_root, + slot, + |timestamps| &mut timestamps.executed, + timestamp, + ) + } + + pub fn set_time_imported(&mut self, block_root: BlockRoot, slot: Slot, timestamp: Duration) { + self.set_time_if_less( + block_root, + slot, + |timestamps| &mut timestamps.imported, + timestamp, + ) + } + + pub fn get_envelope_delays( + &self, + block_root: BlockRoot, + slot_start_time: Duration, + ) -> EnvelopeDelays { + if let Some(entry) = self.cache.get(&block_root) { + EnvelopeDelays::new(entry.timestamps.clone(), slot_start_time) + } else { + EnvelopeDelays::default() + } + } + + /// Prune the cache to only store the most recent 2 epochs. + pub fn prune(&mut self, current_slot: Slot) { + self.cache + .retain(|_, entry| entry.slot > current_slot.saturating_sub(64_u64)); + } +} diff --git a/beacon_node/beacon_chain/src/execution_payload.rs b/beacon_node/beacon_chain/src/execution_payload.rs index a2ebed32ee..2b03a095f1 100644 --- a/beacon_node/beacon_chain/src/execution_payload.rs +++ b/beacon_node/beacon_chain/src/execution_payload.rs @@ -25,7 +25,6 @@ use state_processing::per_block_processing::{ use std::sync::Arc; use tokio::task::JoinHandle; use tracing::{Instrument, debug_span, warn}; -use tree_hash::TreeHash; use types::execution::BlockProductionVersion; use types::*; @@ -109,12 +108,18 @@ impl<T: BeaconChainTypes> PayloadNotifier<T> { if let Some(precomputed_status) = self.payload_verification_status { Ok(precomputed_status) } else { - notify_new_payload(&self.chain, self.block.message()).await + notify_new_payload( + &self.chain, + self.block.message().slot(), + self.block.message().parent_root(), + self.block.message().try_into()?, + ) + .await } } } -/// Verify that `execution_payload` contained by `block` is considered valid by an execution +/// Verify that `execution_payload` is considered valid by an execution /// engine. /// /// ## Specification @@ -123,17 +128,21 @@ impl<T: BeaconChainTypes> PayloadNotifier<T> { /// contains a few extra checks by running `partially_verify_execution_payload` first: /// /// https://github.com/ethereum/consensus-specs/blob/v1.1.9/specs/bellatrix/beacon-chain.md#notify_new_payload -async fn notify_new_payload<T: BeaconChainTypes>( +pub async fn notify_new_payload<T: BeaconChainTypes>( chain: &Arc<BeaconChain<T>>, - block: BeaconBlockRef<'_, T::EthSpec>, + slot: Slot, + parent_beacon_block_root: Hash256, + new_payload_request: NewPayloadRequest<'_, T::EthSpec>, ) -> Result<PayloadVerificationStatus, BlockError> { let execution_layer = chain .execution_layer .as_ref() .ok_or(ExecutionPayloadError::NoExecutionConnection)?; - let execution_block_hash = block.execution_payload()?.block_hash(); - let new_payload_response = execution_layer.notify_new_payload(block.try_into()?).await; + let execution_block_hash = new_payload_request.execution_payload_ref().block_hash(); + let new_payload_response = execution_layer + .notify_new_payload(new_payload_request.clone()) + .await; match new_payload_response { Ok(status) => match status { @@ -149,10 +158,7 @@ async fn notify_new_payload<T: BeaconChainTypes>( ?validation_error, ?latest_valid_hash, ?execution_block_hash, - root = ?block.tree_hash_root(), - graffiti = block.body().graffiti().as_utf8_lossy(), - proposer_index = block.proposer_index(), - slot = %block.slot(), + %slot, method = "new_payload", "Invalid execution payload" ); @@ -175,11 +181,9 @@ async fn notify_new_payload<T: BeaconChainTypes>( { // This block has not yet been applied to fork choice, so the latest block that was // imported to fork choice was the parent. - let latest_root = block.parent_root(); - chain .process_invalid_execution_payload(&InvalidationOperation::InvalidateMany { - head_block_root: latest_root, + head_block_root: parent_beacon_block_root, always_invalidate_head: false, latest_valid_ancestor: latest_valid_hash, }) @@ -194,10 +198,7 @@ async fn notify_new_payload<T: BeaconChainTypes>( warn!( ?validation_error, ?execution_block_hash, - root = ?block.tree_hash_root(), - graffiti = block.body().graffiti().as_utf8_lossy(), - proposer_index = block.proposer_index(), - slot = %block.slot(), + %slot, method = "new_payload", "Invalid execution payload block hash" ); diff --git a/beacon_node/beacon_chain/src/historical_blocks.rs b/beacon_node/beacon_chain/src/historical_blocks.rs index 1dae2258f6..bfda52558e 100644 --- a/beacon_node/beacon_chain/src/historical_blocks.rs +++ b/beacon_node/beacon_chain/src/historical_blocks.rs @@ -165,13 +165,8 @@ impl<T: BeaconChainTypes> BeaconChain<T> { } // Store the blobs or data columns too - if let Some(op) = self - .get_blobs_or_columns_store_op(block_root, block.slot(), block_data) - .map_err(|e| { - HistoricalBlockError::StoreError(StoreError::DBError { - message: format!("get_blobs_or_columns_store_op error {e:?}"), - }) - })? + if let Some(op) = + self.get_blobs_or_columns_store_op(block_root, block.slot(), block_data) { blob_batch.extend(self.store.convert_to_kv_batch(vec![op])?); } diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 4d3c3e193e..4efd90bd22 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -20,6 +20,7 @@ pub mod custody_context; pub mod data_availability_checker; pub mod data_column_verification; mod early_attester_cache; +pub mod envelope_times_cache; mod errors; pub mod events; pub mod execution_payload; @@ -41,6 +42,7 @@ pub mod observed_block_producers; pub mod observed_data_sidecars; pub mod observed_operations; mod observed_slashable; +pub mod payload_envelope_verification; pub mod pending_payload_envelopes; pub mod persisted_beacon_chain; pub mod persisted_custody; diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index 9de67ca93f..786daa09da 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -21,6 +21,34 @@ pub const VALIDATOR_MONITOR_ATTESTATION_SIMULATOR_SOURCE_ATTESTER_HIT_TOTAL: &st pub const VALIDATOR_MONITOR_ATTESTATION_SIMULATOR_SOURCE_ATTESTER_MISS_TOTAL: &str = "validator_monitor_attestation_simulator_source_attester_miss_total"; +/* +* Execution Payload Envelope Processing +*/ + +pub static ENVELOPE_PROCESSING_REQUESTS: LazyLock<Result<IntCounter>> = LazyLock::new(|| { + try_create_int_counter( + "payload_envelope_processing_requests_total", + "Count of payload envelopes submitted for processing", + ) +}); +pub static ENVELOPE_PROCESSING_SUCCESSES: LazyLock<Result<IntCounter>> = LazyLock::new(|| { + try_create_int_counter( + "payload_envelope_processing_successes_total", + "Count of payload envelopes processed without error", + ) +}); +pub static ENVELOPE_PROCESSING_TIMES: LazyLock<Result<Histogram>> = LazyLock::new(|| { + try_create_histogram( + "payload_envelope_processing_seconds", + "Full runtime of payload envelope processing", + ) +}); +pub static ENVELOPE_PROCESSING_DB_WRITE: LazyLock<Result<Histogram>> = LazyLock::new(|| { + try_create_histogram( + "payload_envelope_processing_db_write_seconds", + "Time spent writing a newly processed payload envelope and state to DB", + ) +}); /* * Block Processing */ diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/execution_pending_envelope.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/execution_pending_envelope.rs new file mode 100644 index 0000000000..86f9293c8f --- /dev/null +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/execution_pending_envelope.rs @@ -0,0 +1,105 @@ +use std::sync::Arc; + +use slot_clock::SlotClock; +use state_processing::{ + VerifySignatures, + envelope_processing::{VerifyStateRoot, process_execution_payload_envelope}, +}; +use types::EthSpec; + +use crate::{ + BeaconChain, BeaconChainError, BeaconChainTypes, NotifyExecutionLayer, + PayloadVerificationOutcome, + block_verification::PayloadVerificationHandle, + payload_envelope_verification::{ + EnvelopeError, EnvelopeImportData, MaybeAvailableEnvelope, + gossip_verified_envelope::GossipVerifiedEnvelope, load_snapshot_from_state_root, + payload_notifier::PayloadNotifier, + }, +}; + +pub struct ExecutionPendingEnvelope<E: EthSpec> { + pub signed_envelope: MaybeAvailableEnvelope<E>, + pub import_data: EnvelopeImportData<E>, + pub payload_verification_handle: PayloadVerificationHandle, +} + +impl<T: BeaconChainTypes> GossipVerifiedEnvelope<T> { + pub fn into_execution_pending_envelope( + self, + chain: &Arc<BeaconChain<T>>, + notify_execution_layer: NotifyExecutionLayer, + ) -> Result<ExecutionPendingEnvelope<T::EthSpec>, EnvelopeError> { + let signed_envelope = self.signed_envelope; + let envelope = &signed_envelope.message; + let payload = &envelope.payload; + + // Define a future that will verify the execution payload with an execution engine. + // + // We do this as early as possible so that later parts of this function can run in parallel + // with the payload verification. + let payload_notifier = PayloadNotifier::new( + chain.clone(), + signed_envelope.clone(), + self.block.clone(), + notify_execution_layer, + )?; + let block_root = envelope.beacon_block_root; + let slot = self.block.slot(); + + let payload_verification_future = async move { + let chain = payload_notifier.chain.clone(); + if let Some(started_execution) = chain.slot_clock.now_duration() { + chain + .envelope_times_cache + .write() + .set_time_started_execution(block_root, slot, started_execution); + } + + let payload_verification_status = payload_notifier.notify_new_payload().await?; + Ok(PayloadVerificationOutcome { + payload_verification_status, + }) + }; + // Spawn the payload verification future as a new task, but don't wait for it to complete. + // The `payload_verification_future` will be awaited later to ensure verification completed + // successfully. + let payload_verification_handle = chain + .task_executor + .spawn_handle( + payload_verification_future, + "execution_payload_verification", + ) + .ok_or(BeaconChainError::RuntimeShutdown)?; + + let snapshot = if let Some(snapshot) = self.snapshot { + *snapshot + } else { + load_snapshot_from_state_root::<T>(block_root, self.block.state_root(), &chain.store)? + }; + let mut state = snapshot.pre_state; + + // All the state modifications are done in envelope_processing + process_execution_payload_envelope( + &mut state, + Some(snapshot.state_root), + &signed_envelope, + // verify signature already done for GossipVerifiedEnvelope + VerifySignatures::False, + VerifyStateRoot::True, + &chain.spec, + )?; + + Ok(ExecutionPendingEnvelope { + signed_envelope: MaybeAvailableEnvelope::AvailabilityPending { + block_hash: payload.block_hash, + envelope: signed_envelope, + }, + import_data: EnvelopeImportData { + block_root, + post_state: Box::new(state), + }, + payload_verification_handle, + }) + } +} diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/gossip_verified_envelope.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/gossip_verified_envelope.rs new file mode 100644 index 0000000000..03a3a91ac5 --- /dev/null +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/gossip_verified_envelope.rs @@ -0,0 +1,445 @@ +use std::sync::Arc; + +use educe::Educe; +use parking_lot::{Mutex, RwLock}; +use store::DatabaseBlock; +use tracing::{Span, debug}; +use types::{ + ChainSpec, EthSpec, ExecutionPayloadBid, ExecutionPayloadEnvelope, Hash256, SignedBeaconBlock, + SignedExecutionPayloadEnvelope, Slot, consts::gloas::BUILDER_INDEX_SELF_BUILD, +}; + +use crate::{ + BeaconChain, BeaconChainError, BeaconChainTypes, BeaconStore, + beacon_proposer_cache::{self, BeaconProposerCache}, + canonical_head::CanonicalHead, + payload_envelope_verification::{ + EnvelopeError, EnvelopeProcessingSnapshot, load_snapshot_from_state_root, + }, + validator_pubkey_cache::ValidatorPubkeyCache, +}; + +/// Bundles only the dependencies needed for gossip verification of execution payload envelopes, +/// decoupling `GossipVerifiedEnvelope::new` from the full `BeaconChain`. +pub struct GossipVerificationContext<'a, T: BeaconChainTypes> { + pub canonical_head: &'a CanonicalHead<T>, + pub store: &'a BeaconStore<T>, + pub spec: &'a ChainSpec, + pub beacon_proposer_cache: &'a Mutex<BeaconProposerCache>, + pub validator_pubkey_cache: &'a RwLock<ValidatorPubkeyCache<T>>, + pub genesis_validators_root: Hash256, +} + +/// Verify that an execution payload envelope is consistent with its beacon block +/// and execution bid. +pub(crate) fn verify_envelope_consistency<E: EthSpec>( + envelope: &ExecutionPayloadEnvelope<E>, + block: &SignedBeaconBlock<E>, + execution_bid: &ExecutionPayloadBid<E>, + latest_finalized_slot: Slot, +) -> Result<(), EnvelopeError> { + // Check that the envelope's slot isn't from a slot prior + // to the latest finalized slot. + if envelope.slot < latest_finalized_slot { + return Err(EnvelopeError::PriorToFinalization { + payload_slot: envelope.slot, + latest_finalized_slot, + }); + } + + // Check that the slot of the envelope matches the slot of the block. + if envelope.slot != block.slot() { + return Err(EnvelopeError::SlotMismatch { + block: block.slot(), + envelope: envelope.slot, + }); + } + + // Builder index matches committed bid. + if envelope.builder_index != execution_bid.builder_index { + return Err(EnvelopeError::BuilderIndexMismatch { + committed_bid: execution_bid.builder_index, + envelope: envelope.builder_index, + }); + } + + // The block hash should match the block hash of the execution bid. + if envelope.payload.block_hash != execution_bid.block_hash { + return Err(EnvelopeError::BlockHashMismatch { + committed_bid: execution_bid.block_hash, + envelope: envelope.payload.block_hash, + }); + } + + Ok(()) +} + +/// A wrapper around a `SignedExecutionPayloadEnvelope` that indicates it has been approved for re-gossiping on +/// the p2p network. +#[derive(Educe)] +#[educe(Debug(bound = "T: BeaconChainTypes"))] +pub struct GossipVerifiedEnvelope<T: BeaconChainTypes> { + pub signed_envelope: Arc<SignedExecutionPayloadEnvelope<T::EthSpec>>, + pub block: Arc<SignedBeaconBlock<T::EthSpec>>, + pub snapshot: Option<Box<EnvelopeProcessingSnapshot<T::EthSpec>>>, +} + +impl<T: BeaconChainTypes> GossipVerifiedEnvelope<T> { + pub fn new( + signed_envelope: Arc<SignedExecutionPayloadEnvelope<T::EthSpec>>, + ctx: &GossipVerificationContext<'_, T>, + ) -> Result<Self, EnvelopeError> { + let envelope = &signed_envelope.message; + let beacon_block_root = envelope.beacon_block_root; + + // Check that we've seen the beacon block for this envelope and that it passes validation. + // TODO(EIP-7732): We might need some type of status table in order to differentiate between: + // If we have a block_processing_table, we could have a Processed(Bid, bool) state that is only + // entered post adding to fork choice. That way, we could potentially need only a single call to make + // sure the block is valid and to do all consequent checks with the bid + // + // 1. Blocks we haven't seen (IGNORE), and + // 2. Blocks we've seen that are invalid (REJECT). + // + // Presently these two cases are conflated. + let fork_choice_read_lock = ctx.canonical_head.fork_choice_read_lock(); + let Some(proto_block) = fork_choice_read_lock.get_block(&beacon_block_root) else { + return Err(EnvelopeError::BlockRootUnknown { + block_root: beacon_block_root, + }); + }; + + drop(fork_choice_read_lock); + + let latest_finalized_slot = ctx + .canonical_head + .cached_head() + .finalized_checkpoint() + .epoch + .start_slot(T::EthSpec::slots_per_epoch()); + + // TODO(EIP-7732): check that we haven't seen another valid `SignedExecutionPayloadEnvelope` + // for this block root from this builder - envelope status table check + let block = match ctx.store.try_get_full_block(&beacon_block_root)? { + Some(DatabaseBlock::Full(block)) => Arc::new(block), + Some(DatabaseBlock::Blinded(_)) | None => { + return Err(EnvelopeError::from(BeaconChainError::MissingBeaconBlock( + beacon_block_root, + ))); + } + }; + let execution_bid = &block + .message() + .body() + .signed_execution_payload_bid()? + .message; + + verify_envelope_consistency(envelope, &block, execution_bid, latest_finalized_slot)?; + + // Verify the envelope signature. + // + // For self-built envelopes, we can use the proposer cache for the fork and the + // validator pubkey cache for the proposer's pubkey, avoiding a state load from disk. + // For external builder envelopes, we must load the state to access the builder registry. + let builder_index = envelope.builder_index; + let block_slot = envelope.slot; + let envelope_epoch = block_slot.epoch(T::EthSpec::slots_per_epoch()); + // Since the payload's block is already guaranteed to be imported, the associated `proto_block.current_epoch_shuffling_id` + // already carries the correct `shuffling_decision_block`. + let proposer_shuffling_decision_block = proto_block + .current_epoch_shuffling_id + .shuffling_decision_block; + + let (signature_is_valid, opt_snapshot) = if builder_index == BUILDER_INDEX_SELF_BUILD { + // Fast path: self-built envelopes can be verified without loading the state. + let mut opt_snapshot = None; + let proposer = beacon_proposer_cache::with_proposer_cache( + ctx.beacon_proposer_cache, + proposer_shuffling_decision_block, + envelope_epoch, + |proposers| proposers.get_slot::<T::EthSpec>(block_slot), + || { + debug!( + %beacon_block_root, + "Proposer shuffling cache miss for envelope verification" + ); + let snapshot = load_snapshot_from_state_root::<T>( + beacon_block_root, + proto_block.state_root, + ctx.store, + )?; + opt_snapshot = Some(Box::new(snapshot.clone())); + Ok::<_, EnvelopeError>((snapshot.state_root, snapshot.pre_state)) + }, + ctx.spec, + )?; + let expected_proposer = proposer.index; + let fork = proposer.fork; + + if block.message().proposer_index() != expected_proposer as u64 { + return Err(EnvelopeError::IncorrectBlockProposer { + proposer_index: block.message().proposer_index(), + local_shuffling: expected_proposer as u64, + }); + } + + let pubkey_cache = ctx.validator_pubkey_cache.read(); + let pubkey = pubkey_cache + .get(block.message().proposer_index() as usize) + .ok_or_else(|| EnvelopeError::UnknownValidator { + proposer_index: block.message().proposer_index(), + })?; + let is_valid = signed_envelope.verify_signature( + pubkey, + &fork, + ctx.genesis_validators_root, + ctx.spec, + ); + (is_valid, opt_snapshot) + } else { + // TODO(gloas) if we implement a builder pubkey cache, we'll need to use it here. + // External builder: must load the state to get the builder pubkey. + let snapshot = load_snapshot_from_state_root::<T>( + beacon_block_root, + proto_block.state_root, + ctx.store, + )?; + let is_valid = + signed_envelope.verify_signature_with_state(&snapshot.pre_state, ctx.spec)?; + (is_valid, Some(Box::new(snapshot))) + }; + + if !signature_is_valid { + return Err(EnvelopeError::BadSignature); + } + + Ok(Self { + signed_envelope, + block, + snapshot: opt_snapshot, + }) + } + + pub fn envelope_cloned(&self) -> Arc<SignedExecutionPayloadEnvelope<T::EthSpec>> { + self.signed_envelope.clone() + } +} + +impl<T: BeaconChainTypes> BeaconChain<T> { + /// Build a `GossipVerificationContext` from this `BeaconChain`. + pub fn gossip_verification_context(&self) -> GossipVerificationContext<'_, T> { + GossipVerificationContext { + canonical_head: &self.canonical_head, + store: &self.store, + spec: &self.spec, + beacon_proposer_cache: &self.beacon_proposer_cache, + validator_pubkey_cache: &self.validator_pubkey_cache, + genesis_validators_root: self.genesis_validators_root, + } + } + + /// Returns `Ok(GossipVerifiedEnvelope)` if the supplied `envelope` should be forwarded onto the + /// gossip network. The envelope is not imported into the chain, it is just partially verified. + /// + /// The returned `GossipVerifiedEnvelope` should be provided to `Self::process_execution_payload_envelope` immediately + /// after it is returned, unless some other circumstance decides it should not be imported at + /// all. + /// + /// ## Errors + /// + /// Returns an `Err` if the given envelope was invalid, or an error was encountered during verification. + pub async fn verify_envelope_for_gossip( + self: &Arc<Self>, + envelope: Arc<SignedExecutionPayloadEnvelope<T::EthSpec>>, + ) -> Result<GossipVerifiedEnvelope<T>, EnvelopeError> { + let chain = self.clone(); + let span = Span::current(); + self.task_executor + .clone() + .spawn_blocking_handle( + move || { + let _guard = span.enter(); + let slot = envelope.slot(); + let beacon_block_root = envelope.message.beacon_block_root; + + let ctx = chain.gossip_verification_context(); + match GossipVerifiedEnvelope::new(envelope, &ctx) { + Ok(verified) => { + debug!( + %slot, + ?beacon_block_root, + "Successfully verified gossip envelope" + ); + + Ok(verified) + } + Err(e) => { + debug!( + error = e.to_string(), + ?beacon_block_root, + %slot, + "Rejected gossip envelope" + ); + + Err(e) + } + } + }, + "gossip_envelope_verification_handle", + ) + .ok_or(BeaconChainError::RuntimeShutdown)? + .await + .map_err(BeaconChainError::TokioJoin)? + } +} + +#[cfg(test)] +mod tests { + use std::marker::PhantomData; + + use bls::Signature; + use ssz_types::VariableList; + use types::{ + BeaconBlock, BeaconBlockBodyGloas, BeaconBlockGloas, Eth1Data, ExecutionBlockHash, + ExecutionPayloadBid, ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequests, + Graffiti, Hash256, MinimalEthSpec, SignedBeaconBlock, SignedExecutionPayloadBid, Slot, + SyncAggregate, + }; + + use super::verify_envelope_consistency; + use crate::payload_envelope_verification::EnvelopeError; + + type E = MinimalEthSpec; + + fn make_envelope( + slot: Slot, + builder_index: u64, + block_hash: ExecutionBlockHash, + ) -> ExecutionPayloadEnvelope<E> { + ExecutionPayloadEnvelope { + payload: ExecutionPayloadGloas { + block_hash, + ..ExecutionPayloadGloas::default() + }, + execution_requests: ExecutionRequests::default(), + builder_index, + beacon_block_root: Hash256::ZERO, + slot, + state_root: Hash256::ZERO, + } + } + + fn make_block(slot: Slot) -> SignedBeaconBlock<E> { + let block = BeaconBlock::Gloas(BeaconBlockGloas { + slot, + proposer_index: 0, + parent_root: Hash256::ZERO, + state_root: Hash256::ZERO, + body: BeaconBlockBodyGloas { + randao_reveal: Signature::empty(), + eth1_data: Eth1Data { + deposit_root: Hash256::ZERO, + block_hash: Hash256::ZERO, + deposit_count: 0, + }, + graffiti: Graffiti::default(), + proposer_slashings: VariableList::empty(), + attester_slashings: VariableList::empty(), + attestations: VariableList::empty(), + deposits: VariableList::empty(), + voluntary_exits: VariableList::empty(), + sync_aggregate: SyncAggregate::empty(), + bls_to_execution_changes: VariableList::empty(), + signed_execution_payload_bid: SignedExecutionPayloadBid::empty(), + payload_attestations: VariableList::empty(), + _phantom: PhantomData, + }, + }); + SignedBeaconBlock::from_block(block, Signature::empty()) + } + + fn make_bid(builder_index: u64, block_hash: ExecutionBlockHash) -> ExecutionPayloadBid<E> { + ExecutionPayloadBid { + builder_index, + block_hash, + ..ExecutionPayloadBid::default() + } + } + + #[test] + fn test_valid_envelope() { + let slot = Slot::new(10); + let builder_index = 5; + let block_hash = ExecutionBlockHash::repeat_byte(0xaa); + + let envelope = make_envelope(slot, builder_index, block_hash); + let block = make_block(slot); + let bid = make_bid(builder_index, block_hash); + + assert!(verify_envelope_consistency::<E>(&envelope, &block, &bid, Slot::new(0)).is_ok()); + } + + #[test] + fn test_prior_to_finalization() { + let slot = Slot::new(5); + let builder_index = 1; + let block_hash = ExecutionBlockHash::repeat_byte(0xbb); + + let envelope = make_envelope(slot, builder_index, block_hash); + let block = make_block(slot); + let bid = make_bid(builder_index, block_hash); + let latest_finalized_slot = Slot::new(10); + + let result = + verify_envelope_consistency::<E>(&envelope, &block, &bid, latest_finalized_slot); + assert!(matches!( + result, + Err(EnvelopeError::PriorToFinalization { .. }) + )); + } + + #[test] + fn test_slot_mismatch() { + let builder_index = 1; + let block_hash = ExecutionBlockHash::repeat_byte(0xcc); + + let envelope = make_envelope(Slot::new(10), builder_index, block_hash); + let block = make_block(Slot::new(20)); + let bid = make_bid(builder_index, block_hash); + + let result = verify_envelope_consistency::<E>(&envelope, &block, &bid, Slot::new(0)); + assert!(matches!(result, Err(EnvelopeError::SlotMismatch { .. }))); + } + + #[test] + fn test_builder_index_mismatch() { + let slot = Slot::new(10); + let block_hash = ExecutionBlockHash::repeat_byte(0xdd); + + let envelope = make_envelope(slot, 1, block_hash); + let block = make_block(slot); + let bid = make_bid(2, block_hash); + + let result = verify_envelope_consistency::<E>(&envelope, &block, &bid, Slot::new(0)); + assert!(matches!( + result, + Err(EnvelopeError::BuilderIndexMismatch { .. }) + )); + } + + #[test] + fn test_block_hash_mismatch() { + let slot = Slot::new(10); + let builder_index = 1; + + let envelope = make_envelope(slot, builder_index, ExecutionBlockHash::repeat_byte(0xee)); + let block = make_block(slot); + let bid = make_bid(builder_index, ExecutionBlockHash::repeat_byte(0xff)); + + let result = verify_envelope_consistency::<E>(&envelope, &block, &bid, Slot::new(0)); + assert!(matches!( + result, + Err(EnvelopeError::BlockHashMismatch { .. }) + )); + } +} diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs new file mode 100644 index 0000000000..2ee315e559 --- /dev/null +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -0,0 +1,354 @@ +use std::sync::Arc; +use std::time::Duration; + +use fork_choice::PayloadVerificationStatus; +use slot_clock::SlotClock; +use store::StoreOp; +use tracing::{debug, error, info, info_span, instrument, warn}; +use types::{BeaconState, BlockImportSource, Hash256, Slot}; + +use super::{ + AvailableEnvelope, AvailableExecutedEnvelope, EnvelopeError, EnvelopeImportData, + ExecutedEnvelope, gossip_verified_envelope::GossipVerifiedEnvelope, +}; +use crate::{ + AvailabilityProcessingStatus, BeaconChain, BeaconChainError, BeaconChainTypes, + NotifyExecutionLayer, block_verification_types::AvailableBlockData, metrics, + payload_envelope_verification::ExecutionPendingEnvelope, validator_monitor::get_slot_delay_ms, +}; + +const ENVELOPE_METRICS_CACHE_SLOT_LIMIT: u32 = 64; + +impl<T: BeaconChainTypes> BeaconChain<T> { + /// Returns `Ok(status)` if the given `unverified_envelope` was successfully verified and + /// imported into the chain. + /// + /// ## Errors + /// + /// Returns an `Err` if the given payload envelope was invalid, or an error was encountered during + /// verification. + #[instrument(skip_all, fields(block_root = ?block_root, block_source = %block_source))] + pub async fn process_execution_payload_envelope( + self: &Arc<Self>, + block_root: Hash256, + unverified_envelope: GossipVerifiedEnvelope<T>, + notify_execution_layer: NotifyExecutionLayer, + block_source: BlockImportSource, + publish_fn: impl FnOnce() -> Result<(), EnvelopeError>, + ) -> Result<AvailabilityProcessingStatus, EnvelopeError> { + let block_slot = unverified_envelope.signed_envelope.slot(); + + // Set observed time if not already set. Usually this should be set by gossip or RPC, + // but just in case we set it again here (useful for tests). + if let Some(seen_timestamp) = self.slot_clock.now_duration() { + self.envelope_times_cache.write().set_time_observed( + block_root, + block_slot, + seen_timestamp, + None, + ); + } + + // TODO(gloas) insert the pre-executed envelope into some type of cache. + + let _full_timer = metrics::start_timer(&metrics::ENVELOPE_PROCESSING_TIMES); + + metrics::inc_counter(&metrics::ENVELOPE_PROCESSING_REQUESTS); + + // A small closure to group the verification and import errors. + let chain = self.clone(); + let import_envelope = async move { + let execution_pending = unverified_envelope + .into_execution_pending_envelope(&chain, notify_execution_layer)?; + publish_fn()?; + + // Record the time it took to complete consensus verification. + if let Some(timestamp) = chain.slot_clock.now_duration() { + chain + .envelope_times_cache + .write() + .set_time_consensus_verified(block_root, block_slot, timestamp); + } + + let envelope_times_cache = chain.envelope_times_cache.clone(); + let slot_clock = chain.slot_clock.clone(); + + // TODO(gloas): rename/refactor these `into_` names to be less similar and more clear + // about what the function actually does. + let executed_envelope = chain + .into_executed_payload_envelope(execution_pending) + .await + .inspect_err(|_| { + // TODO(gloas) If the envelope fails execution for whatever reason (e.g. engine offline), + // and we keep it in the cache, then the node will NOT perform lookup and + // reprocess this block until the block is evicted from DA checker, causing the + // chain to get stuck temporarily if the block is canonical. Therefore we remove + // it from the cache if execution fails. + })?; + + // Record the time it took to wait for execution layer verification. + if let Some(timestamp) = slot_clock.now_duration() { + envelope_times_cache + .write() + .set_time_executed(block_root, block_slot, timestamp); + } + + match executed_envelope { + ExecutedEnvelope::Available(envelope) => { + self.import_available_execution_payload_envelope(Box::new(envelope)) + .await + } + ExecutedEnvelope::AvailabilityPending() => Err(EnvelopeError::InternalError( + "Pending payload envelope not yet implemented".to_owned(), + )), + } + }; + + // Verify and import the payload envelope. + match import_envelope.await { + // The payload envelope was successfully verified and imported. + Ok(status @ AvailabilityProcessingStatus::Imported(block_root)) => { + info!( + ?block_root, + %block_slot, + source = %block_source, + "Execution payload envelope imported" + ); + + // TODO(gloas) do we need to send a `PayloadImported` event to the reprocess queue? + // TODO(gloas) do we need to recompute head? + // should canonical_head return the block and the payload now? + self.recompute_head_at_current_slot().await; + + metrics::inc_counter(&metrics::ENVELOPE_PROCESSING_SUCCESSES); + + Ok(status) + } + Ok(status @ AvailabilityProcessingStatus::MissingComponents(slot, block_root)) => { + debug!(?block_root, %slot, "Payload envelope awaiting blobs"); + + 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(other) => { + warn!( + reason = other.to_string(), + "Execution payload envelope rejected" + ); + Err(other) + } + } + } + + /// Accepts a fully-verified payload envelope and awaits on its payload verification handle to + /// get a fully `ExecutedEnvelope`. + /// + /// An error is returned if the verification handle couldn't be awaited. + #[instrument(skip_all, level = "debug")] + async fn into_executed_payload_envelope( + self: Arc<Self>, + pending_envelope: ExecutionPendingEnvelope<T::EthSpec>, + ) -> Result<ExecutedEnvelope<T::EthSpec>, EnvelopeError> { + let ExecutionPendingEnvelope { + signed_envelope, + import_data, + payload_verification_handle, + } = pending_envelope; + + let payload_verification_outcome = payload_verification_handle + .await + .map_err(BeaconChainError::TokioJoin)? + .ok_or(BeaconChainError::RuntimeShutdown)??; + + Ok(ExecutedEnvelope::new( + signed_envelope, + import_data, + payload_verification_outcome, + )) + } + + #[instrument(skip_all)] + pub async fn import_available_execution_payload_envelope( + self: &Arc<Self>, + envelope: Box<AvailableExecutedEnvelope<T::EthSpec>>, + ) -> Result<AvailabilityProcessingStatus, EnvelopeError> { + let AvailableExecutedEnvelope { + envelope, + import_data, + payload_verification_outcome, + } = *envelope; + + let EnvelopeImportData { + block_root, + post_state, + } = import_data; + + let block_root = { + // Capture the current span before moving into the blocking task + let current_span = tracing::Span::current(); + let chain = self.clone(); + self.spawn_blocking_handle( + move || { + // Enter the captured span in the blocking thread + let _guard = current_span.enter(); + chain.import_execution_payload_envelope( + envelope, + block_root, + *post_state, + payload_verification_outcome.payload_verification_status, + ) + }, + "payload_verification_handle", + ) + .await?? + }; + + Ok(AvailabilityProcessingStatus::Imported(block_root)) + } + + /// Accepts a fully-verified and available envelope and imports it into the chain without performing any + /// additional verification. + /// + /// An error is returned if the envelope was unable to be imported. It may be partially imported + /// (i.e., this function is not atomic). + #[allow(clippy::too_many_arguments)] + #[instrument(skip_all)] + fn import_execution_payload_envelope( + &self, + signed_envelope: AvailableEnvelope<T::EthSpec>, + block_root: Hash256, + state: BeaconState<T::EthSpec>, + _payload_verification_status: PayloadVerificationStatus, + ) -> Result<Hash256, EnvelopeError> { + // 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(EnvelopeError::BlockRootUnknown { block_root }); + } + + // TODO(gloas) add defensive check to see if payload envelope is already in fork choice + // Note that a duplicate cache/payload status table should prevent this from happening + // but it doesnt hurt to be defensive. + + // TODO(gloas) when the code below is implemented we can delete this drop + drop(fork_choice_reader); + + // TODO(gloas) no fork choice logic yet + // Take an exclusive write-lock on fork choice. It's very important to prevent deadlocks by + // avoiding taking other locks whilst holding this lock. + // let fork_choice = parking_lot::RwLockUpgradableReadGuard::upgrade(fork_choice_reader); + + // TODO(gloas) Do we need this check? Do not import a block that doesn't descend from the finalized root. + // let signed_block = check_block_is_finalized_checkpoint_or_descendant(self, &fork_choice, signed_block)?; + + // TODO(gloas) emit SSE event if the payload became the new head payload + + // It is important NOT to return errors here before the database commit, because the envelope + // has already been added to fork choice and the database would be left in an inconsistent + // state if we returned early without committing. In other words, an error here would + // corrupt the node's database permanently. + + // Store the envelope, its post-state, and any data columns. + // If the write fails, revert fork choice to the version from disk, else we can + // end up with envelopes in fork choice that are missing from disk. + // See https://github.com/sigp/lighthouse/issues/2028 + let (signed_envelope, columns) = signed_envelope.deconstruct(); + + let mut ops = vec![]; + + if let Some(blobs_or_columns_store_op) = self.get_blobs_or_columns_store_op( + block_root, + signed_envelope.slot(), + AvailableBlockData::DataColumns(columns), + ) { + ops.push(blobs_or_columns_store_op); + } + + let db_write_timer = metrics::start_timer(&metrics::ENVELOPE_PROCESSING_DB_WRITE); + + ops.push(StoreOp::PutPayloadEnvelope( + block_root, + signed_envelope.clone(), + )); + ops.push(StoreOp::PutState( + signed_envelope.message.state_root, + &state, + )); + + let db_span = info_span!("persist_payloads_and_blobs").entered(); + + if let Err(e) = self.store.do_atomically_with_block_and_blobs_cache(ops) { + error!( + msg = "Restoring fork choice from disk", + error = ?e, + "Database write failed!" + ); + return Err(e.into()); + // TODO(gloas) handle db write failure + // return Err(self + // .handle_import_block_db_write_error(fork_choice) + // .err() + // .unwrap_or(e.into())); + } + + drop(db_span); + + // TODO(gloas) drop fork choice lock + // The fork choice write-lock is dropped *after* the on-disk database has been updated. + // This prevents inconsistency between the two at the expense of concurrency. + // drop(fork_choice); + + // We're declaring the envelope "imported" at this point, since fork choice and the DB know + // about it. + let envelope_time_imported = self.slot_clock.now_duration().unwrap_or(Duration::MAX); + + // TODO(gloas) depending on what happens with light clients + // we might need to do some light client related computations here + + metrics::stop_timer(db_write_timer); + + self.import_envelope_update_metrics_and_events( + block_root, + signed_envelope.slot(), + envelope_time_imported, + ); + + Ok(block_root) + } + + fn import_envelope_update_metrics_and_events( + &self, + block_root: Hash256, + envelope_slot: Slot, + envelope_time_imported: Duration, + ) { + let envelope_delay_total = + get_slot_delay_ms(envelope_time_imported, envelope_slot, &self.slot_clock); + + // Do not write to the cache for envelopes older than 2 epochs, this helps reduce writes + // to the cache during sync. + if envelope_delay_total + < self + .slot_clock + .slot_duration() + .saturating_mul(ENVELOPE_METRICS_CACHE_SLOT_LIMIT) + { + self.envelope_times_cache.write().set_time_imported( + block_root, + envelope_slot, + envelope_time_imported, + ); + } + + // TODO(gloas) emit SSE event for envelope import (similar to SseBlock for blocks). + } +} diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs new file mode 100644 index 0000000000..c707d62dc7 --- /dev/null +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs @@ -0,0 +1,285 @@ +//! The incremental processing steps (e.g., signatures verified but not the state transition) is +//! represented as a sequence of wrapper-types around the envelope. There is a linear progression of +//! types, starting at a `SignedExecutionPayloadEnvelope` and finishing with an `AvailableExecutedEnvelope` (see +//! diagram below). +//! +//! ```ignore +//! SignedExecutionPayloadEnvelope +//! | +//! ▼ +//! GossipVerifiedEnvelope +//! | +//! ▼ +//! ExecutionPendingEnvelope +//! | +//! await +//! ▼ +//! ExecutedEnvelope +//! +//! ``` + +use std::sync::Arc; + +use store::Error as DBError; + +use state_processing::{BlockProcessingError, envelope_processing::EnvelopeProcessingError}; +use tracing::instrument; +use types::{ + BeaconState, BeaconStateError, ChainSpec, DataColumnSidecarList, EthSpec, ExecutionBlockHash, + ExecutionPayloadEnvelope, Hash256, SignedExecutionPayloadEnvelope, Slot, +}; + +use crate::{ + BeaconChainError, BeaconChainTypes, BeaconStore, BlockError, ExecutionPayloadError, + PayloadVerificationOutcome, +}; + +pub mod execution_pending_envelope; +pub mod gossip_verified_envelope; +pub mod import; +mod payload_notifier; + +pub use execution_pending_envelope::ExecutionPendingEnvelope; + +#[derive(PartialEq)] +pub struct EnvelopeImportData<E: EthSpec> { + pub block_root: Hash256, + pub post_state: Box<BeaconState<E>>, +} + +#[derive(Debug)] +#[allow(dead_code)] +pub struct AvailableEnvelope<E: EthSpec> { + execution_block_hash: ExecutionBlockHash, + envelope: Arc<SignedExecutionPayloadEnvelope<E>>, + columns: DataColumnSidecarList<E>, + /// Timestamp at which this envelope first became available (UNIX timestamp, time since 1970). + columns_available_timestamp: Option<std::time::Duration>, + pub spec: Arc<ChainSpec>, +} + +impl<E: EthSpec> AvailableEnvelope<E> { + pub fn message(&self) -> &ExecutionPayloadEnvelope<E> { + &self.envelope.message + } + + #[allow(clippy::type_complexity)] + pub fn deconstruct( + self, + ) -> ( + Arc<SignedExecutionPayloadEnvelope<E>>, + DataColumnSidecarList<E>, + ) { + let AvailableEnvelope { + envelope, columns, .. + } = self; + (envelope, columns) + } +} + +pub enum MaybeAvailableEnvelope<E: EthSpec> { + Available(AvailableEnvelope<E>), + AvailabilityPending { + block_hash: ExecutionBlockHash, + envelope: Arc<SignedExecutionPayloadEnvelope<E>>, + }, +} + +/// This snapshot is to be used for verifying a payload envelope. +#[derive(Debug, Clone)] +pub struct EnvelopeProcessingSnapshot<E: EthSpec> { + /// This state is equivalent to the `self.beacon_block.state_root()` before applying the envelope. + pub pre_state: BeaconState<E>, + pub state_root: Hash256, + pub beacon_block_root: Hash256, +} + +/// A payload envelope that has gone through processing checks and execution by an EL client. +/// This envelope hasn't necessarily completed data availability checks. +/// +/// +/// It contains 2 variants: +/// 1. `Available`: This envelope has been executed and also contains all data to consider it +/// fully available. +/// 2. `AvailabilityPending`: This envelope hasn't received all required blobs to consider it +/// fully available. +pub enum ExecutedEnvelope<E: EthSpec> { + Available(AvailableExecutedEnvelope<E>), + // TODO(gloas) implement availability pending + AvailabilityPending(), +} + +impl<E: EthSpec> ExecutedEnvelope<E> { + pub fn new( + envelope: MaybeAvailableEnvelope<E>, + import_data: EnvelopeImportData<E>, + payload_verification_outcome: PayloadVerificationOutcome, + ) -> Self { + match envelope { + MaybeAvailableEnvelope::Available(available_envelope) => { + Self::Available(AvailableExecutedEnvelope::new( + available_envelope, + import_data, + payload_verification_outcome, + )) + } + // TODO(gloas) implement availability pending + MaybeAvailableEnvelope::AvailabilityPending { + block_hash: _, + envelope: _, + } => Self::AvailabilityPending(), + } + } +} + +/// A payload envelope that has completed all payload processing checks including verification +/// by an EL client **and** has all requisite blob data to be imported into fork choice. +pub struct AvailableExecutedEnvelope<E: EthSpec> { + pub envelope: AvailableEnvelope<E>, + pub import_data: EnvelopeImportData<E>, + pub payload_verification_outcome: PayloadVerificationOutcome, +} + +impl<E: EthSpec> AvailableExecutedEnvelope<E> { + pub fn new( + envelope: AvailableEnvelope<E>, + import_data: EnvelopeImportData<E>, + payload_verification_outcome: PayloadVerificationOutcome, + ) -> Self { + Self { + envelope, + import_data, + payload_verification_outcome, + } + } +} + +#[derive(Debug)] +pub enum EnvelopeError { + /// The envelope's block root is unknown. + BlockRootUnknown { block_root: Hash256 }, + /// The signature is invalid. + BadSignature, + /// The builder index doesn't match the committed bid + BuilderIndexMismatch { committed_bid: u64, envelope: u64 }, + /// The envelope slot doesn't match the block + SlotMismatch { block: Slot, envelope: Slot }, + /// The validator index is unknown + UnknownValidator { proposer_index: u64 }, + /// The block hash doesn't match the committed bid + BlockHashMismatch { + committed_bid: ExecutionBlockHash, + envelope: ExecutionBlockHash, + }, + /// The block's proposer_index does not match the locally computed proposer + IncorrectBlockProposer { + proposer_index: u64, + local_shuffling: u64, + }, + /// The slot belongs to a block that is from a slot prior than + /// to most recently finalized slot + PriorToFinalization { + payload_slot: Slot, + latest_finalized_slot: Slot, + }, + /// Some Beacon Chain Error + BeaconChainError(Arc<BeaconChainError>), + /// Some Beacon State error + BeaconStateError(BeaconStateError), + /// Some BlockProcessingError (for electra operations) + BlockProcessingError(BlockProcessingError), + /// Some EnvelopeProcessingError + EnvelopeProcessingError(EnvelopeProcessingError), + /// Error verifying the execution payload + ExecutionPayloadError(ExecutionPayloadError), + /// An error from block-level checks reused during envelope import + BlockError(BlockError), + /// Internal error + InternalError(String), +} + +impl std::fmt::Display for EnvelopeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl From<BeaconChainError> for EnvelopeError { + fn from(e: BeaconChainError) -> Self { + EnvelopeError::BeaconChainError(Arc::new(e)) + } +} + +impl From<ExecutionPayloadError> for EnvelopeError { + fn from(e: ExecutionPayloadError) -> Self { + EnvelopeError::ExecutionPayloadError(e) + } +} + +impl From<BeaconStateError> for EnvelopeError { + fn from(e: BeaconStateError) -> Self { + EnvelopeError::BeaconStateError(e) + } +} + +impl From<DBError> for EnvelopeError { + fn from(e: DBError) -> Self { + EnvelopeError::BeaconChainError(Arc::new(BeaconChainError::DBError(e))) + } +} + +impl From<BlockError> for EnvelopeError { + fn from(e: BlockError) -> Self { + EnvelopeError::BlockError(e) + } +} + +/// Pull errors up from EnvelopeProcessingError to EnvelopeError +impl From<EnvelopeProcessingError> for EnvelopeError { + fn from(e: EnvelopeProcessingError) -> Self { + match e { + EnvelopeProcessingError::BadSignature => EnvelopeError::BadSignature, + EnvelopeProcessingError::BeaconStateError(e) => EnvelopeError::BeaconStateError(e), + EnvelopeProcessingError::BlockHashMismatch { + committed_bid, + envelope, + } => EnvelopeError::BlockHashMismatch { + committed_bid, + envelope, + }, + EnvelopeProcessingError::BlockProcessingError(e) => { + EnvelopeError::BlockProcessingError(e) + } + e => EnvelopeError::EnvelopeProcessingError(e), + } + } +} + +#[instrument(skip_all, level = "debug", fields(beacon_block_root = %beacon_block_root))] +/// Load state from store given a known state root and block root. +/// Use this when the proto block has already been looked up from fork choice. +pub(crate) fn load_snapshot_from_state_root<T: BeaconChainTypes>( + beacon_block_root: Hash256, + block_state_root: Hash256, + store: &BeaconStore<T>, +) -> Result<EnvelopeProcessingSnapshot<T::EthSpec>, EnvelopeError> { + // TODO(EIP-7732): add metrics here + + // We can use `get_hot_state` here rather than `get_advanced_hot_state` because the envelope + // must be from the same slot as its block (so no advance is required). + let cache_state = true; + let state = store + .get_hot_state(&block_state_root, cache_state) + .map_err(EnvelopeError::from)? + .ok_or_else(|| { + BeaconChainError::DBInconsistent(format!( + "Missing state for envelope block {block_state_root:?}", + )) + })?; + + Ok(EnvelopeProcessingSnapshot { + pre_state: state, + state_root: block_state_root, + beacon_block_root, + }) +} 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 new file mode 100644 index 0000000000..df21d33493 --- /dev/null +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs @@ -0,0 +1,94 @@ +use std::sync::Arc; + +use execution_layer::{NewPayloadRequest, NewPayloadRequestGloas}; +use fork_choice::PayloadVerificationStatus; +use state_processing::per_block_processing::deneb::kzg_commitment_to_versioned_hash; +use tracing::warn; +use types::{SignedBeaconBlock, SignedExecutionPayloadEnvelope}; + +use crate::{ + BeaconChain, BeaconChainTypes, BlockError, NotifyExecutionLayer, + execution_payload::notify_new_payload, payload_envelope_verification::EnvelopeError, +}; + +/// Used to await the result of executing payload with a remote EE. +pub struct PayloadNotifier<T: BeaconChainTypes> { + pub chain: Arc<BeaconChain<T>>, + envelope: Arc<SignedExecutionPayloadEnvelope<T::EthSpec>>, + block: Arc<SignedBeaconBlock<T::EthSpec>>, + payload_verification_status: Option<PayloadVerificationStatus>, +} + +impl<T: BeaconChainTypes> PayloadNotifier<T> { + pub fn new( + chain: Arc<BeaconChain<T>>, + envelope: Arc<SignedExecutionPayloadEnvelope<T::EthSpec>>, + block: Arc<SignedBeaconBlock<T::EthSpec>>, + notify_execution_layer: NotifyExecutionLayer, + ) -> Result<Self, EnvelopeError> { + let payload_verification_status = { + let payload_message = &envelope.message; + + match notify_execution_layer { + NotifyExecutionLayer::No if chain.config.optimistic_finalized_sync => { + 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!( + block_number = ?payload_message.payload.block_number, + info = "you can silence this warning with --disable-optimistic-finalized-sync", + error = ?e, + "Falling back to slow block hash verification" + ); + None + } else { + Some(PayloadVerificationStatus::Optimistic) + } + } + _ => None, + } + }; + + Ok(Self { + chain, + envelope, + block, + payload_verification_status, + }) + } + + pub async fn notify_new_payload(self) -> Result<PayloadVerificationStatus, BlockError> { + if let Some(precomputed_status) = self.payload_verification_status { + Ok(precomputed_status) + } else { + let parent_root = self.block.message().parent_root(); + let request = Self::build_new_payload_request(&self.envelope, &self.block)?; + notify_new_payload(&self.chain, self.envelope.slot(), parent_root, request).await + } + } + + fn build_new_payload_request<'a>( + envelope: &'a SignedExecutionPayloadEnvelope<T::EthSpec>, + block: &'a SignedBeaconBlock<T::EthSpec>, + ) -> Result<NewPayloadRequest<'a, T::EthSpec>, BlockError> { + let bid = &block + .message() + .body() + .signed_execution_payload_bid() + .map_err(|e| BlockError::BeaconChainError(Box::new(e.into())))? + .message; + + let versioned_hashes = bid + .blob_kzg_commitments + .iter() + .map(kzg_commitment_to_versioned_hash) + .collect(); + + Ok(NewPayloadRequest::Gloas(NewPayloadRequestGloas { + execution_payload: &envelope.message.payload, + versioned_hashes, + parent_beacon_block_root: block.message().parent_root(), + execution_requests: &envelope.message.execution_requests, + })) + } +} diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index e94e64e91d..e385e0dc48 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -1,5 +1,5 @@ #![cfg(not(debug_assertions))] - +// TODO(gloas) we probably need similar test for payload envelope verification use beacon_chain::block_verification_types::{AsBlock, ExecutedBlock, RpcBlock}; use beacon_chain::data_availability_checker::{AvailabilityCheckError, AvailableBlockData}; use beacon_chain::data_column_verification::CustodyDataColumn; diff --git a/beacon_node/network/src/metrics.rs b/beacon_node/network/src/metrics.rs index 240fd70e01..2119acf946 100644 --- a/beacon_node/network/src/metrics.rs +++ b/beacon_node/network/src/metrics.rs @@ -539,6 +539,16 @@ pub static SYNC_RPC_REQUEST_TIME: LazyLock<Result<HistogramVec>> = LazyLock::new ) }); +/* + * Execution Payload Envelope Delay Metrics + */ +pub static ENVELOPE_DELAY_GOSSIP: LazyLock<Result<IntGauge>> = LazyLock::new(|| { + try_create_int_gauge( + "payload_envelope_delay_gossip", + "The first time we see this payload envelope from gossip as a delay from the start of the slot", + ) +}); + /* * Block Delay Metrics */ diff --git a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs index e90018c851..3335315157 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -4,7 +4,6 @@ use crate::{ service::NetworkMessage, sync::SyncMessage, }; -use beacon_chain::blob_verification::{GossipBlobError, GossipVerifiedBlob}; use beacon_chain::block_verification_types::AsBlock; use beacon_chain::data_column_verification::{GossipDataColumnError, GossipVerifiedDataColumn}; use beacon_chain::store::Error; @@ -19,6 +18,10 @@ use beacon_chain::{ sync_committee_verification::{self, Error as SyncCommitteeError}, validator_monitor::{get_block_delay_ms, get_slot_delay_ms}, }; +use beacon_chain::{ + blob_verification::{GossipBlobError, GossipVerifiedBlob}, + payload_envelope_verification::gossip_verified_envelope::GossipVerifiedEnvelope, +}; use beacon_processor::{Work, WorkEvent}; use lighthouse_network::{Client, MessageAcceptance, MessageId, PeerAction, PeerId, ReportSource}; use logging::crit; @@ -3248,25 +3251,166 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> { } } - pub async fn process_gossip_execution_payload( + #[allow(clippy::too_many_arguments)] + #[instrument( + name = "lh_process_execution_payload_envelope", + parent = None, + level = "debug", + skip_all, + fields(beacon_block_root = tracing::field::Empty), + )] + pub async fn process_gossip_execution_payload_envelope( + self: Arc<Self>, + message_id: MessageId, + peer_id: PeerId, + envelope: Arc<SignedExecutionPayloadEnvelope<T::EthSpec>>, + seen_timestamp: Duration, + ) { + if let Some(gossip_verified_envelope) = self + .process_gossip_unverified_execution_payload_envelope( + message_id, + peer_id, + envelope.clone(), + seen_timestamp, + ) + .await + { + let beacon_block_root = gossip_verified_envelope.signed_envelope.beacon_block_root(); + + Span::current().record("beacon_block_root", beacon_block_root.to_string()); + + // TODO(gloas) in process_gossip_block here we check_and_insert on the duplicate cache + // before calling gossip_verified_block. We need this to ensure we dont try to execute the + // payload multiple times. + + self.process_gossip_verified_execution_payload_envelope( + peer_id, + gossip_verified_envelope, + ) + .await; + } + } + + async fn process_gossip_unverified_execution_payload_envelope( self: &Arc<Self>, message_id: MessageId, peer_id: PeerId, - execution_payload: SignedExecutionPayloadEnvelope<T::EthSpec>, + envelope: Arc<SignedExecutionPayloadEnvelope<T::EthSpec>>, + seen_duration: Duration, + ) -> Option<GossipVerifiedEnvelope<T>> { + let envelope_delay = + get_slot_delay_ms(seen_duration, envelope.slot(), &self.chain.slot_clock); + + let verification_result = self + .chain + .clone() + .verify_envelope_for_gossip(envelope.clone()) + .await; + + let verified_envelope = match verification_result { + Ok(verified_envelope) => { + metrics::set_gauge( + &metrics::ENVELOPE_DELAY_GOSSIP, + envelope_delay.as_millis() as i64, + ); + + // Write the time the envelope was observed into the delay cache. + self.chain.envelope_times_cache.write().set_time_observed( + verified_envelope.signed_envelope.beacon_block_root(), + envelope.slot(), + seen_duration, + Some(peer_id.to_string()), + ); + + info!( + slot = %verified_envelope.signed_envelope.slot(), + root = ?verified_envelope.signed_envelope.beacon_block_root(), + "New envelope received" + ); + + self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Accept); + + verified_envelope + } + // TODO(gloas) penalize peers accordingly + Err(_) => return None, + }; + + let envelope_slot = verified_envelope.signed_envelope.slot(); + let beacon_block_root = verified_envelope.signed_envelope.beacon_block_root(); + match self.chain.slot() { + // We only need to do a simple check about the envelope slot vs the current slot because + // `verify_envelope_for_gossip` already ensures that the envelope slot is within tolerance + // for envelope imports. + Ok(current_slot) if envelope_slot > current_slot => { + warn!( + ?envelope_slot, + ?beacon_block_root, + msg = "if this happens consistently, check system clock", + "envelope arrived early" + ); + + // TODO(gloas) update metrics to note how early the envelope arrived + + let inner_self = self.clone(); + let _process_fn = Box::pin(async move { + inner_self + .process_gossip_verified_execution_payload_envelope( + peer_id, + verified_envelope, + ) + .await; + }); + + // TODO(gloas) send to reprocess queue + None + } + Ok(_) => Some(verified_envelope), + Err(e) => { + error!( + error = ?e, + %envelope_slot, + ?beacon_block_root, + location = "envelope gossip", + "Failed to defer envelope import" + ); + None + } + } + } + + async fn process_gossip_verified_execution_payload_envelope( + self: Arc<Self>, + _peer_id: PeerId, + verified_envelope: GossipVerifiedEnvelope<T>, ) { - // TODO(EIP-7732): Implement proper execution payload envelope gossip processing. - // This should integrate with the envelope_verification.rs module once it's implemented. + let _processing_start_time = Instant::now(); + let beacon_block_root = verified_envelope.signed_envelope.beacon_block_root(); - trace!( - %peer_id, - builder_index = execution_payload.message.builder_index, - slot = %execution_payload.message.slot, - beacon_block_root = %execution_payload.message.beacon_block_root, - "Processing execution payload envelope" - ); + #[allow(clippy::result_large_err)] + let result = self + .chain + .process_execution_payload_envelope( + beacon_block_root, + verified_envelope, + NotifyExecutionLayer::Yes, + BlockImportSource::Gossip, + || Ok(()), + ) + .await; - // For now, ignore all envelopes since verification is not implemented - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); + // TODO(gloas) metrics + // register_process_result_metrics(&result, metrics::BlockSource::Gossip, "envelope"); + + match &result { + Ok(AvailabilityProcessingStatus::Imported(_)) + | Ok(AvailabilityProcessingStatus::MissingComponents(_, _)) => { + // Nothing to do + } + Err(_) => { + // TODO(gloas) implement peer penalties + } + } } pub fn process_gossip_execution_payload_bid( diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index e1adf860de..357d6c08fd 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -429,11 +429,17 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> { message_id: MessageId, peer_id: PeerId, execution_payload: Box<SignedExecutionPayloadEnvelope<T::EthSpec>>, + seen_timestamp: Duration, ) -> Result<(), Error<T::EthSpec>> { let processor = self.clone(); let process_fn = async move { processor - .process_gossip_execution_payload(message_id, peer_id, *execution_payload) + .process_gossip_execution_payload_envelope( + message_id, + peer_id, + Arc::new(*execution_payload), + seen_timestamp, + ) .await }; diff --git a/beacon_node/network/src/router.rs b/beacon_node/network/src/router.rs index 8373dec322..77d64c92e6 100644 --- a/beacon_node/network/src/router.rs +++ b/beacon_node/network/src/router.rs @@ -493,6 +493,7 @@ impl<T: BeaconChainTypes> Router<T> { message_id, peer_id, signed_execution_payload_envelope, + timestamp_now(), ), ) } From 71f6eab51f5c5e58afc9c3f4fbfbca4dfc605025 Mon Sep 17 00:00:00 2001 From: Michael Sproul <michael@sigmaprime.io> Date: Wed, 4 Mar 2026 15:50:42 +1100 Subject: [PATCH 63/81] Bump deps --- Cargo.lock | 79 ++++++++++++++++++++++++++---------------------------- Cargo.toml | 9 +++++-- 2 files changed, 45 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 40c550f4c6..1795de0bc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1909,7 +1909,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] @@ -2442,7 +2442,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" dependencies = [ "data-encoding", - "syn 2.0.111", + "syn 1.0.109", ] [[package]] @@ -3985,11 +3985,11 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" dependencies = [ - "hashbrown 0.15.5", + "hashbrown 0.16.1", ] [[package]] @@ -4996,7 +4996,7 @@ dependencies = [ [[package]] name = "libp2p" version = "0.56.1" -source = "git+https://github.com/libp2p/rust-libp2p.git#5e3519fb66b92c7f7c0dc744ab360fd8b669fe54" +source = "git+https://github.com/sigp/rust-libp2p.git?rev=f88e43de9eba00b416d0374b1a1fb2de47b65864#f88e43de9eba00b416d0374b1a1fb2de47b65864" dependencies = [ "bytes", "either", @@ -5027,7 +5027,7 @@ dependencies = [ [[package]] name = "libp2p-allow-block-list" version = "0.6.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#5e3519fb66b92c7f7c0dc744ab360fd8b669fe54" +source = "git+https://github.com/sigp/rust-libp2p.git?rev=f88e43de9eba00b416d0374b1a1fb2de47b65864#f88e43de9eba00b416d0374b1a1fb2de47b65864" dependencies = [ "libp2p-core", "libp2p-identity", @@ -5037,7 +5037,7 @@ dependencies = [ [[package]] name = "libp2p-connection-limits" version = "0.6.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#5e3519fb66b92c7f7c0dc744ab360fd8b669fe54" +source = "git+https://github.com/sigp/rust-libp2p.git?rev=f88e43de9eba00b416d0374b1a1fb2de47b65864#f88e43de9eba00b416d0374b1a1fb2de47b65864" dependencies = [ "libp2p-core", "libp2p-identity", @@ -5047,7 +5047,7 @@ dependencies = [ [[package]] name = "libp2p-core" version = "0.43.2" -source = "git+https://github.com/libp2p/rust-libp2p.git#5e3519fb66b92c7f7c0dc744ab360fd8b669fe54" +source = "git+https://github.com/sigp/rust-libp2p.git?rev=f88e43de9eba00b416d0374b1a1fb2de47b65864#f88e43de9eba00b416d0374b1a1fb2de47b65864" dependencies = [ "either", "fnv", @@ -5071,7 +5071,7 @@ dependencies = [ [[package]] name = "libp2p-dns" version = "0.44.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#5e3519fb66b92c7f7c0dc744ab360fd8b669fe54" +source = "git+https://github.com/sigp/rust-libp2p.git?rev=f88e43de9eba00b416d0374b1a1fb2de47b65864#f88e43de9eba00b416d0374b1a1fb2de47b65864" dependencies = [ "async-trait", "futures", @@ -5086,7 +5086,7 @@ dependencies = [ [[package]] name = "libp2p-gossipsub" version = "0.50.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#5e3519fb66b92c7f7c0dc744ab360fd8b669fe54" +source = "git+https://github.com/sigp/rust-libp2p.git?rev=f88e43de9eba00b416d0374b1a1fb2de47b65864#f88e43de9eba00b416d0374b1a1fb2de47b65864" dependencies = [ "async-channel 2.5.0", "asynchronous-codec", @@ -5098,7 +5098,7 @@ dependencies = [ "futures", "futures-timer", "getrandom 0.2.16", - "hashlink 0.10.0", + "hashlink 0.11.0", "hex_fmt", "libp2p-core", "libp2p-identity", @@ -5116,7 +5116,7 @@ dependencies = [ [[package]] name = "libp2p-identify" version = "0.47.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#5e3519fb66b92c7f7c0dc744ab360fd8b669fe54" +source = "git+https://github.com/sigp/rust-libp2p.git?rev=f88e43de9eba00b416d0374b1a1fb2de47b65864#f88e43de9eba00b416d0374b1a1fb2de47b65864" dependencies = [ "asynchronous-codec", "either", @@ -5156,7 +5156,7 @@ dependencies = [ [[package]] name = "libp2p-mdns" version = "0.48.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#5e3519fb66b92c7f7c0dc744ab360fd8b669fe54" +source = "git+https://github.com/sigp/rust-libp2p.git?rev=f88e43de9eba00b416d0374b1a1fb2de47b65864#f88e43de9eba00b416d0374b1a1fb2de47b65864" dependencies = [ "futures", "hickory-proto", @@ -5174,7 +5174,7 @@ dependencies = [ [[package]] name = "libp2p-metrics" version = "0.17.1" -source = "git+https://github.com/libp2p/rust-libp2p.git#5e3519fb66b92c7f7c0dc744ab360fd8b669fe54" +source = "git+https://github.com/sigp/rust-libp2p.git?rev=f88e43de9eba00b416d0374b1a1fb2de47b65864#f88e43de9eba00b416d0374b1a1fb2de47b65864" dependencies = [ "futures", "libp2p-core", @@ -5190,7 +5190,7 @@ dependencies = [ [[package]] name = "libp2p-mplex" version = "0.43.1" -source = "git+https://github.com/libp2p/rust-libp2p.git#5e3519fb66b92c7f7c0dc744ab360fd8b669fe54" +source = "git+https://github.com/sigp/rust-libp2p.git?rev=f88e43de9eba00b416d0374b1a1fb2de47b65864#f88e43de9eba00b416d0374b1a1fb2de47b65864" dependencies = [ "asynchronous-codec", "bytes", @@ -5208,7 +5208,7 @@ dependencies = [ [[package]] name = "libp2p-noise" version = "0.46.1" -source = "git+https://github.com/libp2p/rust-libp2p.git#5e3519fb66b92c7f7c0dc744ab360fd8b669fe54" +source = "git+https://github.com/sigp/rust-libp2p.git?rev=f88e43de9eba00b416d0374b1a1fb2de47b65864#f88e43de9eba00b416d0374b1a1fb2de47b65864" dependencies = [ "asynchronous-codec", "bytes", @@ -5230,7 +5230,7 @@ dependencies = [ [[package]] name = "libp2p-quic" version = "0.13.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#5e3519fb66b92c7f7c0dc744ab360fd8b669fe54" +source = "git+https://github.com/sigp/rust-libp2p.git?rev=f88e43de9eba00b416d0374b1a1fb2de47b65864#f88e43de9eba00b416d0374b1a1fb2de47b65864" dependencies = [ "futures", "futures-timer", @@ -5250,14 +5250,14 @@ dependencies = [ [[package]] name = "libp2p-swarm" -version = "0.47.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#5e3519fb66b92c7f7c0dc744ab360fd8b669fe54" +version = "0.47.1" +source = "git+https://github.com/sigp/rust-libp2p.git?rev=f88e43de9eba00b416d0374b1a1fb2de47b65864#f88e43de9eba00b416d0374b1a1fb2de47b65864" dependencies = [ "either", "fnv", "futures", "futures-timer", - "hashlink 0.10.0", + "hashlink 0.11.0", "libp2p-core", "libp2p-identity", "libp2p-swarm-derive", @@ -5272,7 +5272,7 @@ dependencies = [ [[package]] name = "libp2p-swarm-derive" version = "0.35.1" -source = "git+https://github.com/libp2p/rust-libp2p.git#5e3519fb66b92c7f7c0dc744ab360fd8b669fe54" +source = "git+https://github.com/sigp/rust-libp2p.git?rev=f88e43de9eba00b416d0374b1a1fb2de47b65864#f88e43de9eba00b416d0374b1a1fb2de47b65864" dependencies = [ "heck", "quote", @@ -5282,7 +5282,7 @@ dependencies = [ [[package]] name = "libp2p-tcp" version = "0.44.1" -source = "git+https://github.com/libp2p/rust-libp2p.git#5e3519fb66b92c7f7c0dc744ab360fd8b669fe54" +source = "git+https://github.com/sigp/rust-libp2p.git?rev=f88e43de9eba00b416d0374b1a1fb2de47b65864#f88e43de9eba00b416d0374b1a1fb2de47b65864" dependencies = [ "futures", "futures-timer", @@ -5297,7 +5297,7 @@ dependencies = [ [[package]] name = "libp2p-tls" version = "0.6.2" -source = "git+https://github.com/libp2p/rust-libp2p.git#5e3519fb66b92c7f7c0dc744ab360fd8b669fe54" +source = "git+https://github.com/sigp/rust-libp2p.git?rev=f88e43de9eba00b416d0374b1a1fb2de47b65864#f88e43de9eba00b416d0374b1a1fb2de47b65864" dependencies = [ "futures", "futures-rustls", @@ -5315,7 +5315,7 @@ dependencies = [ [[package]] name = "libp2p-upnp" version = "0.6.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#5e3519fb66b92c7f7c0dc744ab360fd8b669fe54" +source = "git+https://github.com/sigp/rust-libp2p.git?rev=f88e43de9eba00b416d0374b1a1fb2de47b65864#f88e43de9eba00b416d0374b1a1fb2de47b65864" dependencies = [ "futures", "futures-timer", @@ -5329,7 +5329,7 @@ dependencies = [ [[package]] name = "libp2p-yamux" version = "0.47.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#5e3519fb66b92c7f7c0dc744ab360fd8b669fe54" +source = "git+https://github.com/sigp/rust-libp2p.git?rev=f88e43de9eba00b416d0374b1a1fb2de47b65864#f88e43de9eba00b416d0374b1a1fb2de47b65864" dependencies = [ "either", "futures", @@ -6021,7 +6021,7 @@ dependencies = [ [[package]] name = "multistream-select" version = "0.13.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#5e3519fb66b92c7f7c0dc744ab360fd8b669fe54" +source = "git+https://github.com/sigp/rust-libp2p.git?rev=f88e43de9eba00b416d0374b1a1fb2de47b65864#f88e43de9eba00b416d0374b1a1fb2de47b65864" dependencies = [ "bytes", "futures", @@ -7129,7 +7129,7 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quick-protobuf" version = "0.8.1" -source = "git+https://github.com/sigp/quick-protobuf.git?rev=681f413312404ab6e51f0b46f39b0075c6f4ebfd#681f413312404ab6e51f0b46f39b0075c6f4ebfd" +source = "git+https://github.com/sigp/quick-protobuf.git?rev=87c4ccb9bb2af494de375f5f6c62850badd26304#87c4ccb9bb2af494de375f5f6c62850badd26304" dependencies = [ "byteorder", ] @@ -7137,7 +7137,7 @@ dependencies = [ [[package]] name = "quick-protobuf-codec" version = "0.3.1" -source = "git+https://github.com/libp2p/rust-libp2p.git#5e3519fb66b92c7f7c0dc744ab360fd8b669fe54" +source = "git+https://github.com/sigp/rust-libp2p.git?rev=f88e43de9eba00b416d0374b1a1fb2de47b65864#f88e43de9eba00b416d0374b1a1fb2de47b65864" dependencies = [ "asynchronous-codec", "bytes", @@ -7149,8 +7149,7 @@ dependencies = [ [[package]] name = "quinn" version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +source = "git+https://github.com/sigp/quinn?rev=59af87979c8411864c1cb68613222f54ed2930a7#59af87979c8411864c1cb68613222f54ed2930a7" dependencies = [ "bytes", "cfg_aliases", @@ -7160,7 +7159,7 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.1", "rustls 0.23.35", - "socket2 0.6.1", + "socket2 0.5.10", "thiserror 2.0.17", "tokio", "tracing", @@ -7170,8 +7169,7 @@ dependencies = [ [[package]] name = "quinn-proto" version = "0.11.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +source = "git+https://github.com/sigp/quinn?rev=59af87979c8411864c1cb68613222f54ed2930a7#59af87979c8411864c1cb68613222f54ed2930a7" dependencies = [ "bytes", "getrandom 0.3.4", @@ -7191,15 +7189,14 @@ dependencies = [ [[package]] name = "quinn-udp" version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +source = "git+https://github.com/sigp/quinn?rev=59af87979c8411864c1cb68613222f54ed2930a7#59af87979c8411864c1cb68613222f54ed2930a7" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2 0.5.10", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -7822,7 +7819,7 @@ dependencies = [ [[package]] name = "rw-stream-sink" version = "0.4.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#5e3519fb66b92c7f7c0dc744ab360fd8b669fe54" +source = "git+https://github.com/sigp/rust-libp2p.git?rev=f88e43de9eba00b416d0374b1a1fb2de47b65864#f88e43de9eba00b416d0374b1a1fb2de47b65864" dependencies = [ "futures", "pin-project", @@ -10132,7 +10129,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] @@ -10607,7 +10604,7 @@ dependencies = [ [[package]] name = "yamux" version = "0.13.8" -source = "git+https://github.com/sigp/rust-yamux?rev=575b17c0f44f4253079a6bafaa2de74ca1d6dfaa#575b17c0f44f4253079a6bafaa2de74ca1d6dfaa" +source = "git+https://github.com/sigp/rust-yamux?rev=29efa6aebd4bdfcb16bfb21969ec0c785e570b74#29efa6aebd4bdfcb16bfb21969ec0c785e570b74" dependencies = [ "futures", "log", diff --git a/Cargo.toml b/Cargo.toml index 5f6f43d2f2..ab33cb6310 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -302,5 +302,10 @@ inherits = "release" debug = true [patch.crates-io] -quick-protobuf = { git = "https://github.com/sigp/quick-protobuf.git", rev = "681f413312404ab6e51f0b46f39b0075c6f4ebfd" } -yamux = { git = "https://github.com/sigp/rust-yamux", rev = "575b17c0f44f4253079a6bafaa2de74ca1d6dfaa" } +quick-protobuf = { git = "https://github.com/sigp/quick-protobuf.git", rev = "87c4ccb9bb2af494de375f5f6c62850badd26304" } +yamux = { git = "https://github.com/sigp/rust-yamux", rev = "29efa6aebd4bdfcb16bfb21969ec0c785e570b74" } +quinn = { git = "https://github.com/sigp/quinn", rev = "59af87979c8411864c1cb68613222f54ed2930a7" } + +[patch."https://github.com/libp2p/rust-libp2p.git"] +libp2p = { git = "https://github.com/sigp/rust-libp2p.git", rev = "f88e43de9eba00b416d0374b1a1fb2de47b65864" } +libp2p-mplex = { git = "https://github.com/sigp/rust-libp2p.git", rev = "f88e43de9eba00b416d0374b1a1fb2de47b65864" } From ac1db1d2e23f849f7937bbc38cb9c85445d837dc Mon Sep 17 00:00:00 2001 From: chonghe <44791194+chong-he@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:48:30 +0800 Subject: [PATCH 64/81] update cargo-sort (#8933) Co-Authored-By: Tan Chee Keong <tanck@sigmaprime.io> --- Cargo.toml | 30 +++--------------------------- common/logging/Cargo.toml | 2 +- common/malloc_utils/Cargo.toml | 5 +---- 3 files changed, 5 insertions(+), 32 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ab33cb6310..82db6dbfc4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -166,20 +166,7 @@ initialized_validators = { path = "validator_client/initialized_validators" } int_to_bytes = { path = "consensus/int_to_bytes" } itertools = "0.10" 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"] } libsecp256k1 = "0.7" lighthouse_network = { path = "beacon_node/lighthouse_network" } lighthouse_validator_store = { path = "validator_client/lighthouse_validator_store" } @@ -219,13 +206,7 @@ r2d2 = "0.8" rand = "0.9.0" rayon = "1.7" regex = "1" -reqwest = { version = "0.12", default-features = false, features = [ - "blocking", - "json", - "stream", - "rustls-tls", - "native-tls-vendored", -] } +reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "stream", "rustls-tls", "native-tls-vendored"] } ring = "0.17" rpds = "0.11" rusqlite = { version = "0.28", features = ["bundled"] } @@ -254,12 +235,7 @@ sysinfo = "0.26" system_health = { path = "common/system_health" } task_executor = { path = "common/task_executor" } tempfile = "3" -tokio = { version = "1", features = [ - "rt-multi-thread", - "sync", - "signal", - "macros", -] } +tokio = { version = "1", features = ["rt-multi-thread", "sync", "signal", "macros"] } tokio-stream = { version = "0.1", features = ["sync"] } tokio-util = { version = "0.7", features = ["codec", "compat", "time"] } tracing = "0.1.40" diff --git a/common/logging/Cargo.toml b/common/logging/Cargo.toml index 41c82dbd61..cbebd1a501 100644 --- a/common/logging/Cargo.toml +++ b/common/logging/Cargo.toml @@ -13,7 +13,7 @@ logroller = { workspace = true } metrics = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -tokio = { workspace = true, features = [ "time" ] } +tokio = { workspace = true, features = ["time"] } tracing = { workspace = true } tracing-appender = { workspace = true } tracing-core = { workspace = true } diff --git a/common/malloc_utils/Cargo.toml b/common/malloc_utils/Cargo.toml index 1052128852..e90490bf09 100644 --- a/common/malloc_utils/Cargo.toml +++ b/common/malloc_utils/Cargo.toml @@ -35,7 +35,4 @@ tikv-jemallocator = { version = "0.6.0", optional = true, features = ["stats"] } # Jemalloc's background_threads feature requires Linux (pthreads). [target.'cfg(target_os = "linux")'.dependencies] -tikv-jemallocator = { version = "0.6.0", optional = true, features = [ - "stats", - "background_threads", -] } +tikv-jemallocator = { version = "0.6.0", optional = true, features = ["stats", "background_threads"] } From 5a174f2a00b33d4905bcc241749b96965541132a Mon Sep 17 00:00:00 2001 From: Mac L <mjladson@pm.me> Date: Fri, 6 Mar 2026 09:54:43 +0200 Subject: [PATCH 65/81] Fix lints for Rust v1.94.0 (#8939) Following the release of Rust v1.94.0 there are new Clippy lints which do not pass and are blocking CI (which pulls in the latest version of Rust) This is pretty much the minimum just to get CI running again. Most of the errors involve error types being too large. For now I've added allows but later it might be worth doing a refactor to `Box` or otherwise remove the problematic error types. Co-Authored-By: Mac L <mjladson@pm.me> --- beacon_node/beacon_chain/tests/attestation_verification.rs | 1 + beacon_node/beacon_chain/tests/payload_invalidation.rs | 1 + beacon_node/beacon_chain/tests/store_tests.rs | 1 + beacon_node/execution_layer/src/lib.rs | 2 +- beacon_node/http_api/src/lib.rs | 1 + slasher/service/src/lib.rs | 1 + 6 files changed, 6 insertions(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/tests/attestation_verification.rs b/beacon_node/beacon_chain/tests/attestation_verification.rs index 208798dfdf..9553965ae6 100644 --- a/beacon_node/beacon_chain/tests/attestation_verification.rs +++ b/beacon_node/beacon_chain/tests/attestation_verification.rs @@ -1,4 +1,5 @@ #![cfg(not(debug_assertions))] +#![allow(clippy::result_large_err)] use beacon_chain::attestation_verification::{ Error, batch_verify_aggregated_attestations, batch_verify_unaggregated_attestations, diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index 1204412d65..11c916e850 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -1,4 +1,5 @@ #![cfg(not(debug_assertions))] +#![allow(clippy::result_large_err)] use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::{ diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index ea5f735bde..e618873bdd 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -1,4 +1,5 @@ #![cfg(not(debug_assertions))] +#![allow(clippy::result_large_err)] use beacon_chain::attestation_verification::Error as AttnError; use beacon_chain::block_verification_types::RpcBlock; diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index 33b83aab09..024c6805b9 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -2205,7 +2205,7 @@ fn verify_builder_bid<E: EthSpec>( .cloned() .map(|withdrawals| { Withdrawals::<E>::try_from(withdrawals) - .map_err(InvalidBuilderPayload::SszTypesError) + .map_err(|e| Box::new(InvalidBuilderPayload::SszTypesError(e))) .map(|w| w.tree_hash_root()) }) .transpose()?; diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 095c52fb29..69aa7cd91f 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -1,3 +1,4 @@ +#![allow(clippy::result_large_err)] //! This crate contains a HTTP server which serves the endpoints listed here: //! //! https://github.com/ethereum/beacon-APIs diff --git a/slasher/service/src/lib.rs b/slasher/service/src/lib.rs index ac15b49ee9..69ec59aa2c 100644 --- a/slasher/service/src/lib.rs +++ b/slasher/service/src/lib.rs @@ -1,3 +1,4 @@ +#![allow(clippy::result_large_err)] mod service; pub use service::SlasherService; From 6a92761f441e5a3a9169454df11025cb1a32d751 Mon Sep 17 00:00:00 2001 From: Akihito Nakano <sora.akatsuki@gmail.com> Date: Sat, 7 Mar 2026 08:09:33 +0900 Subject: [PATCH 66/81] Fix cargo-sort errors (#8945) The `cargo-sort` job in CI is [failing](https://github.com/sigp/lighthouse/actions/runs/22781651620/job/66088700318?pr=8932) since [cargo-sort v2.1.1](https://github.com/DevinR528/cargo-sort/releases/tag/v2.1.1) has been released, which reports new errors for our Cargo.toml files. Ran `cargo-sort` formatter locally with the new version. Co-Authored-By: ackintosh <sora.akatsuki@gmail.com> --- account_manager/Cargo.toml | 5 +---- beacon_node/Cargo.toml | 13 +++++-------- beacon_node/beacon_chain/Cargo.toml | 9 ++++++--- common/logging/Cargo.toml | 3 ++- consensus/types/Cargo.toml | 5 +---- 5 files changed, 15 insertions(+), 20 deletions(-) diff --git a/account_manager/Cargo.toml b/account_manager/Cargo.toml index 8dd50cbc6e..05e6f12554 100644 --- a/account_manager/Cargo.toml +++ b/account_manager/Cargo.toml @@ -1,10 +1,7 @@ [package] name = "account_manager" version = { workspace = true } -authors = [ - "Paul Hauner <paul@paulhauner.com>", - "Luke Anderson <luke@sigmaprime.io>", -] +authors = ["Paul Hauner <paul@paulhauner.com>", "Luke Anderson <luke@sigmaprime.io>"] edition = { workspace = true } [dependencies] diff --git a/beacon_node/Cargo.toml b/beacon_node/Cargo.toml index 5352814dd5..ebefa6a451 100644 --- a/beacon_node/Cargo.toml +++ b/beacon_node/Cargo.toml @@ -1,10 +1,7 @@ [package] name = "beacon_node" version = { workspace = true } -authors = [ - "Paul Hauner <paul@paulhauner.com>", - "Age Manning <Age@AgeManning.com", -] +authors = ["Paul Hauner <paul@paulhauner.com>", "Age Manning <Age@AgeManning.com"] edition = { workspace = true } [lib] @@ -12,10 +9,10 @@ name = "beacon_node" path = "src/lib.rs" [features] -write_ssz_files = [ - "beacon_chain/write_ssz_files", -] # Writes debugging .ssz files to /tmp during block processing. -testing = [] # Enables testing-only CLI flags +# Writes debugging .ssz files to /tmp during block processing. +write_ssz_files = ["beacon_chain/write_ssz_files"] +# Enables testing-only CLI flags. +testing = [] [dependencies] account_utils = { workspace = true } diff --git a/beacon_node/beacon_chain/Cargo.toml b/beacon_node/beacon_chain/Cargo.toml index 5e1c41b830..2d796bf19d 100644 --- a/beacon_node/beacon_chain/Cargo.toml +++ b/beacon_node/beacon_chain/Cargo.toml @@ -8,9 +8,12 @@ autotests = false # using a single test binary compiles faster [features] default = ["participation_metrics"] -write_ssz_files = [] # Writes debugging .ssz files to /tmp during block processing. -participation_metrics = [] # Exposes validator participation metrics to Prometheus. -fork_from_env = [] # Initialise the harness chain spec from the FORK_NAME env variable +# Writes debugging .ssz files to /tmp during block processing. +write_ssz_files = [] +# Exposes validator participation metrics to Prometheus. +participation_metrics = [] +# Initialise the harness chain spec from the FORK_NAME env variable +fork_from_env = [] portable = ["bls/supranational-portable"] test_backfill = [] diff --git a/common/logging/Cargo.toml b/common/logging/Cargo.toml index cbebd1a501..1606b8ceb4 100644 --- a/common/logging/Cargo.toml +++ b/common/logging/Cargo.toml @@ -5,7 +5,8 @@ authors = ["blacktemplar <blacktemplar@a1.net>"] edition = { workspace = true } [features] -test_logger = [] # Print log output to stderr when running tests instead of dropping it +# Print log output to stderr when running tests instead of dropping it. +test_logger = [] [dependencies] chrono = { version = "0.4", default-features = false, features = ["clock", "std"] } diff --git a/consensus/types/Cargo.toml b/consensus/types/Cargo.toml index feea855c84..e5c5662d71 100644 --- a/consensus/types/Cargo.toml +++ b/consensus/types/Cargo.toml @@ -1,10 +1,7 @@ [package] name = "types" version = "0.2.1" -authors = [ - "Paul Hauner <paul@paulhauner.com>", - "Age Manning <Age@AgeManning.com>", -] +authors = ["Paul Hauner <paul@paulhauner.com>", "Age Manning <Age@AgeManning.com>"] edition = { workspace = true } [features] From 3deab9b0410233c1d57bddfaa9903cc6fbdaa958 Mon Sep 17 00:00:00 2001 From: Michael Sproul <michael@sigmaprime.io> Date: Mon, 9 Mar 2026 11:27:08 +1100 Subject: [PATCH 67/81] Release v8.1.2 --- Cargo.lock | 14 +++++++------- Cargo.toml | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1795de0bc1..cba93f2fd5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "account_manager" -version = "8.1.1" +version = "8.1.2" dependencies = [ "account_utils", "bls", @@ -1276,7 +1276,7 @@ dependencies = [ [[package]] name = "beacon_node" -version = "8.1.1" +version = "8.1.2" dependencies = [ "account_utils", "beacon_chain", @@ -1513,7 +1513,7 @@ dependencies = [ [[package]] name = "boot_node" -version = "8.1.1" +version = "8.1.2" dependencies = [ "beacon_node", "bytes", @@ -4897,7 +4897,7 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "lcli" -version = "8.1.1" +version = "8.1.2" dependencies = [ "account_utils", "beacon_chain", @@ -5383,7 +5383,7 @@ dependencies = [ [[package]] name = "lighthouse" -version = "8.1.1" +version = "8.1.2" dependencies = [ "account_manager", "account_utils", @@ -5515,7 +5515,7 @@ dependencies = [ [[package]] name = "lighthouse_version" -version = "8.1.1" +version = "8.1.2" dependencies = [ "regex", ] @@ -9619,7 +9619,7 @@ dependencies = [ [[package]] name = "validator_client" -version = "8.1.1" +version = "8.1.2" dependencies = [ "account_utils", "beacon_node_fallback", diff --git a/Cargo.toml b/Cargo.toml index 82db6dbfc4..f483e998c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,7 +91,7 @@ resolver = "2" [workspace.package] edition = "2024" -version = "8.1.1" +version = "8.1.2" [workspace.dependencies] account_utils = { path = "common/account_utils" } From 9f3873f2bf242440d1ee8e06d078ea189b73e53b Mon Sep 17 00:00:00 2001 From: Michael Sproul <michael@sigmaprime.io> Date: Tue, 10 Mar 2026 16:49:35 +1100 Subject: [PATCH 68/81] Fix syn in Cargo.lock --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index d96021aaea..704039a175 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2461,7 +2461,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn 2.0.111", ] [[package]] From 081229b7488e37d472f768a3908a0c37fb320d7c Mon Sep 17 00:00:00 2001 From: Michael Sproul <michaelsproul@users.noreply.github.com> Date: Tue, 10 Mar 2026 18:57:51 +1100 Subject: [PATCH 69/81] Implement proposer duties v2 endpoint (#8918) Fix the issue with the `proposer_duties` endpoint using the wrong dependent root post-Fulu by implementing the new v2 endpoint: - https://github.com/ethereum/beacon-APIs/pull/563 We need to add this in time for Gloas, and then we can we can deprecate and remove v1. - Add a new API handler for the v2 endpoint - Add client code in the `eth2` crate - Update existing tests and add some new ones to confirm the different behaviour of v1 and v2 There's a bit of test duplication with v1, but this will be resolved once v1 and its tests are deleted. Co-Authored-By: Michael Sproul <michael@sigmaprime.io> Co-Authored-By: Michael Sproul <michaelsproul@users.noreply.github.com> Co-Authored-By: chonghe <44791194+chong-he@users.noreply.github.com> --- beacon_node/http_api/src/lib.rs | 3 +- beacon_node/http_api/src/proposer_duties.rs | 99 ++++++-- beacon_node/http_api/src/validator/mod.rs | 17 +- .../http_api/tests/interactive_tests.rs | 234 ++++++++++++++++++ beacon_node/http_api/tests/tests.rs | 99 ++++++++ common/eth2/src/lib.rs | 18 ++ 6 files changed, 438 insertions(+), 32 deletions(-) diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 0a0ae683ca..b5b74a3840 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -263,6 +263,7 @@ pub fn prometheus_metrics() -> warp::filters::log::Log<impl Fn(warp::filters::lo .or_else(|| starts_with("v1/validator/contribution_and_proofs")) .or_else(|| starts_with("v1/validator/duties/attester")) .or_else(|| starts_with("v1/validator/duties/proposer")) + .or_else(|| starts_with("v2/validator/duties/proposer")) .or_else(|| starts_with("v1/validator/duties/sync")) .or_else(|| starts_with("v1/validator/liveness")) .or_else(|| starts_with("v1/validator/prepare_beacon_proposer")) @@ -2464,7 +2465,7 @@ pub fn serve<T: BeaconChainTypes>( // GET validator/duties/proposer/{epoch} let get_validator_duties_proposer = get_validator_duties_proposer( - eth_v1.clone(), + any_version.clone(), chain_filter.clone(), not_while_syncing_filter.clone(), task_spawner_filter.clone(), diff --git a/beacon_node/http_api/src/proposer_duties.rs b/beacon_node/http_api/src/proposer_duties.rs index 1ebb174785..0b0926f955 100644 --- a/beacon_node/http_api/src/proposer_duties.rs +++ b/beacon_node/http_api/src/proposer_duties.rs @@ -13,13 +13,45 @@ use slot_clock::SlotClock; use tracing::debug; use types::{Epoch, EthSpec, Hash256, Slot}; +/// Selects which dependent root to return in the API response. +/// +/// - `Legacy`: the block root at the last slot of epoch N-1 (v1 behaviour, for backwards compat). +/// - `True`: the fork-aware proposer shuffling decision root (v2 behaviour). Pre-Fulu this equals +/// the legacy root; post-Fulu it uses epoch N-2. +#[derive(Clone, Copy, PartialEq, Eq)] +enum DependentRootSelection { + Legacy, + True, +} + /// The struct that is returned to the requesting HTTP client. type ApiDuties = api_types::DutiesResponse<Vec<api_types::ProposerData>>; -/// Handles a request from the HTTP API for proposer duties. +/// Handles a request from the HTTP API for v1 proposer duties. +/// +/// Returns the legacy dependent root (block root at end of epoch N-1) for backwards compatibility. pub fn proposer_duties<T: BeaconChainTypes>( request_epoch: Epoch, chain: &BeaconChain<T>, +) -> Result<ApiDuties, warp::reject::Rejection> { + proposer_duties_internal(request_epoch, chain, DependentRootSelection::Legacy) +} + +/// Handles a request from the HTTP API for v2 proposer duties. +/// +/// Returns the true fork-aware dependent root. Pre-Fulu this equals the legacy root; post-Fulu it +/// uses epoch N-2 due to deterministic proposer lookahead with `min_seed_lookahead`. +pub fn proposer_duties_v2<T: BeaconChainTypes>( + request_epoch: Epoch, + chain: &BeaconChain<T>, +) -> Result<ApiDuties, warp::reject::Rejection> { + proposer_duties_internal(request_epoch, chain, DependentRootSelection::True) +} + +fn proposer_duties_internal<T: BeaconChainTypes>( + request_epoch: Epoch, + chain: &BeaconChain<T>, + root_selection: DependentRootSelection, ) -> Result<ApiDuties, warp::reject::Rejection> { let current_epoch = chain .slot_clock @@ -49,24 +81,29 @@ pub fn proposer_duties<T: BeaconChainTypes>( if request_epoch == current_epoch || request_epoch == tolerant_current_epoch { // If we could consider ourselves in the `request_epoch` when allowing for clock disparity // tolerance then serve this request from the cache. - if let Some(duties) = try_proposer_duties_from_cache(request_epoch, chain)? { + if let Some(duties) = try_proposer_duties_from_cache(request_epoch, chain, root_selection)? + { Ok(duties) } else { debug!(%request_epoch, "Proposer cache miss"); - compute_and_cache_proposer_duties(request_epoch, chain) + compute_and_cache_proposer_duties(request_epoch, chain, root_selection) } } else if request_epoch == current_epoch .safe_add(1) .map_err(warp_utils::reject::arith_error)? { - let (proposers, _dependent_root, legacy_dependent_root, execution_status, _fork) = + let (proposers, dependent_root, legacy_dependent_root, execution_status, _fork) = compute_proposer_duties_from_head(request_epoch, chain) .map_err(warp_utils::reject::unhandled_error)?; + let selected_root = match root_selection { + DependentRootSelection::Legacy => legacy_dependent_root, + DependentRootSelection::True => dependent_root, + }; convert_to_api_response( chain, request_epoch, - legacy_dependent_root, + selected_root, execution_status.is_optimistic_or_invalid(), proposers, ) @@ -84,7 +121,7 @@ pub fn proposer_duties<T: BeaconChainTypes>( // request_epoch < current_epoch // // Queries about the past are handled with a slow path. - compute_historic_proposer_duties(request_epoch, chain) + compute_historic_proposer_duties(request_epoch, chain, root_selection) } } @@ -98,6 +135,7 @@ pub fn proposer_duties<T: BeaconChainTypes>( fn try_proposer_duties_from_cache<T: BeaconChainTypes>( request_epoch: Epoch, chain: &BeaconChain<T>, + root_selection: DependentRootSelection, ) -> Result<Option<ApiDuties>, warp::reject::Rejection> { let head = chain.canonical_head.cached_head(); let head_block = &head.snapshot.beacon_block; @@ -116,11 +154,14 @@ fn try_proposer_duties_from_cache<T: BeaconChainTypes>( .beacon_state .proposer_shuffling_decision_root_at_epoch(request_epoch, head_block_root, &chain.spec) .map_err(warp_utils::reject::beacon_state_error)?; - let legacy_dependent_root = head - .snapshot - .beacon_state - .legacy_proposer_shuffling_decision_root_at_epoch(request_epoch, head_block_root) - .map_err(warp_utils::reject::beacon_state_error)?; + let selected_root = match root_selection { + DependentRootSelection::Legacy => head + .snapshot + .beacon_state + .legacy_proposer_shuffling_decision_root_at_epoch(request_epoch, head_block_root) + .map_err(warp_utils::reject::beacon_state_error)?, + DependentRootSelection::True => head_decision_root, + }; let execution_optimistic = chain .is_optimistic_or_invalid_head_block(head_block) .map_err(warp_utils::reject::unhandled_error)?; @@ -134,7 +175,7 @@ fn try_proposer_duties_from_cache<T: BeaconChainTypes>( convert_to_api_response( chain, request_epoch, - legacy_dependent_root, + selected_root, execution_optimistic, indices.to_vec(), ) @@ -155,6 +196,7 @@ fn try_proposer_duties_from_cache<T: BeaconChainTypes>( fn compute_and_cache_proposer_duties<T: BeaconChainTypes>( current_epoch: Epoch, chain: &BeaconChain<T>, + root_selection: DependentRootSelection, ) -> Result<ApiDuties, warp::reject::Rejection> { let (indices, dependent_root, legacy_dependent_root, execution_status, fork) = compute_proposer_duties_from_head(current_epoch, chain) @@ -168,10 +210,14 @@ fn compute_and_cache_proposer_duties<T: BeaconChainTypes>( .map_err(BeaconChainError::from) .map_err(warp_utils::reject::unhandled_error)?; + let selected_root = match root_selection { + DependentRootSelection::Legacy => legacy_dependent_root, + DependentRootSelection::True => dependent_root, + }; convert_to_api_response( chain, current_epoch, - legacy_dependent_root, + selected_root, execution_status.is_optimistic_or_invalid(), indices, ) @@ -182,6 +228,7 @@ fn compute_and_cache_proposer_duties<T: BeaconChainTypes>( fn compute_historic_proposer_duties<T: BeaconChainTypes>( epoch: Epoch, chain: &BeaconChain<T>, + root_selection: DependentRootSelection, ) -> Result<ApiDuties, warp::reject::Rejection> { // If the head is quite old then it might still be relevant for a historical request. // @@ -219,9 +266,9 @@ fn compute_historic_proposer_duties<T: BeaconChainTypes>( }; // Ensure the state lookup was correct. - if state.current_epoch() != epoch { + if state.current_epoch() != epoch && state.current_epoch() + 1 != epoch { return Err(warp_utils::reject::custom_server_error(format!( - "state epoch {} not equal to request epoch {}", + "state from epoch {} cannot serve request epoch {}", state.current_epoch(), epoch ))); @@ -234,18 +281,18 @@ fn compute_historic_proposer_duties<T: BeaconChainTypes>( // We can supply the genesis block root as the block root since we know that the only block that // decides its own root is the genesis block. - let legacy_dependent_root = state - .legacy_proposer_shuffling_decision_root_at_epoch(epoch, chain.genesis_block_root) - .map_err(BeaconChainError::from) - .map_err(warp_utils::reject::unhandled_error)?; + let selected_root = match root_selection { + DependentRootSelection::Legacy => state + .legacy_proposer_shuffling_decision_root_at_epoch(epoch, chain.genesis_block_root) + .map_err(BeaconChainError::from) + .map_err(warp_utils::reject::unhandled_error)?, + DependentRootSelection::True => state + .proposer_shuffling_decision_root_at_epoch(epoch, chain.genesis_block_root, &chain.spec) + .map_err(BeaconChainError::from) + .map_err(warp_utils::reject::unhandled_error)?, + }; - convert_to_api_response( - chain, - epoch, - legacy_dependent_root, - execution_optimistic, - indices, - ) + convert_to_api_response(chain, epoch, selected_root, execution_optimistic, indices) } /// Converts the internal representation of proposer duties into one that is compatible with the diff --git a/beacon_node/http_api/src/validator/mod.rs b/beacon_node/http_api/src/validator/mod.rs index a9082df715..3d96b85870 100644 --- a/beacon_node/http_api/src/validator/mod.rs +++ b/beacon_node/http_api/src/validator/mod.rs @@ -6,7 +6,7 @@ use crate::utils::{ AnyVersionFilter, ChainFilter, EthV1Filter, NetworkTxFilter, NotWhileSyncingFilter, ResponseFilter, TaskSpawnerFilter, ValidatorSubscriptionTxFilter, publish_network_message, }; -use crate::version::V3; +use crate::version::{V1, V2, V3, unsupported_version_rejection}; use crate::{StateId, attester_duties, proposer_duties, sync_committees}; use beacon_chain::attestation_verification::VerifiedAttestation; use beacon_chain::validator_monitor::timestamp_now; @@ -971,12 +971,12 @@ pub fn post_validator_aggregate_and_proofs<T: BeaconChainTypes>( // GET validator/duties/proposer/{epoch} pub fn get_validator_duties_proposer<T: BeaconChainTypes>( - eth_v1: EthV1Filter, + any_version: AnyVersionFilter, chain_filter: ChainFilter<T>, not_while_syncing_filter: NotWhileSyncingFilter, task_spawner_filter: TaskSpawnerFilter<T>, ) -> ResponseFilter { - eth_v1 + any_version .and(warp::path("validator")) .and(warp::path("duties")) .and(warp::path("proposer")) @@ -990,13 +990,20 @@ pub fn get_validator_duties_proposer<T: BeaconChainTypes>( .and(task_spawner_filter) .and(chain_filter) .then( - |epoch: Epoch, + |endpoint_version: EndpointVersion, + epoch: Epoch, not_synced_filter: Result<(), Rejection>, task_spawner: TaskSpawner<T::EthSpec>, chain: Arc<BeaconChain<T>>| { task_spawner.blocking_json_task(Priority::P0, move || { not_synced_filter?; - proposer_duties::proposer_duties(epoch, &chain) + if endpoint_version == V1 { + proposer_duties::proposer_duties(epoch, &chain) + } else if endpoint_version == V2 { + proposer_duties::proposer_duties_v2(epoch, &chain) + } else { + Err(unsupported_version_rejection(endpoint_version)) + } }) }, ) diff --git a/beacon_node/http_api/tests/interactive_tests.rs b/beacon_node/http_api/tests/interactive_tests.rs index a18dd10464..e0e4029875 100644 --- a/beacon_node/http_api/tests/interactive_tests.rs +++ b/beacon_node/http_api/tests/interactive_tests.rs @@ -1053,6 +1053,240 @@ async fn proposer_duties_with_gossip_tolerance() { ); } +// Test that a request for next epoch v2 proposer duties succeeds when the current slot clock is +// within gossip clock disparity (500ms) of the new epoch. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn proposer_duties_v2_with_gossip_tolerance() { + let validator_count = 24; + + let tester = InteractiveTester::<E>::new(None, validator_count).await; + let harness = &tester.harness; + let spec = &harness.spec; + let client = &tester.client; + + let num_initial = 4 * E::slots_per_epoch() - 1; + let next_epoch_start_slot = Slot::new(num_initial + 1); + + harness.advance_slot(); + harness + .extend_chain_with_sync( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + SyncCommitteeStrategy::NoValidators, + LightClientStrategy::Disabled, + ) + .await; + + assert_eq!(harness.chain.slot().unwrap(), num_initial); + + // Set the clock to just before the next epoch. + harness.chain.slot_clock.advance_time( + Duration::from_secs(spec.seconds_per_slot) - spec.maximum_gossip_clock_disparity(), + ); + assert_eq!( + harness + .chain + .slot_clock + .now_with_future_tolerance(spec.maximum_gossip_clock_disparity()) + .unwrap(), + next_epoch_start_slot + ); + + let head_state = harness.get_current_state(); + let head_block_root = harness.head_block_root(); + let tolerant_current_epoch = next_epoch_start_slot.epoch(E::slots_per_epoch()); + + // Prime the proposer shuffling cache with an incorrect entry (regression test). + let wrong_decision_root = head_state + .proposer_shuffling_decision_root(head_block_root, spec) + .unwrap(); + let wrong_proposer_indices = vec![0; E::slots_per_epoch() as usize]; + harness + .chain + .beacon_proposer_cache + .lock() + .insert( + tolerant_current_epoch, + wrong_decision_root, + wrong_proposer_indices.clone(), + head_state.fork(), + ) + .unwrap(); + + // Request the v2 proposer duties. + let proposer_duties_tolerant_current_epoch = client + .get_validator_duties_proposer_v2(tolerant_current_epoch) + .await + .unwrap(); + + assert_eq!( + proposer_duties_tolerant_current_epoch.dependent_root, + head_state + .proposer_shuffling_decision_root_at_epoch( + tolerant_current_epoch, + head_block_root, + spec, + ) + .unwrap() + ); + assert_ne!( + proposer_duties_tolerant_current_epoch + .data + .iter() + .map(|data| data.validator_index as usize) + .collect::<Vec<_>>(), + wrong_proposer_indices, + ); + + // We should get the exact same result after properly advancing into the epoch. + harness + .chain + .slot_clock + .advance_time(spec.maximum_gossip_clock_disparity()); + assert_eq!(harness.chain.slot().unwrap(), next_epoch_start_slot); + let proposer_duties_current_epoch = client + .get_validator_duties_proposer_v2(tolerant_current_epoch) + .await + .unwrap(); + + assert_eq!( + proposer_duties_tolerant_current_epoch, + proposer_duties_current_epoch + ); +} + +// Test that post-Fulu, v1 and v2 proposer duties return different dependent roots. +// Post-Fulu, the true dependent root shifts to the block root at the end of epoch N-2 (due to +// `min_seed_lookahead`), while the legacy v1 root remains at the end of epoch N-1. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn proposer_duties_v2_post_fulu_dependent_root() { + type E = MinimalEthSpec; + let spec = test_spec::<E>(); + + if !spec.is_fulu_scheduled() { + return; + } + + let validator_count = 24; + let slots_per_epoch = E::slots_per_epoch(); + + let tester = InteractiveTester::<E>::new(Some(spec.clone()), validator_count).await; + let harness = &tester.harness; + let client = &tester.client; + let mock_el = harness.mock_execution_layer.as_ref().unwrap(); + mock_el.server.all_payloads_valid(); + + // Build 3 full epochs of chain so we're in epoch 3. + let num_slots = 3 * slots_per_epoch; + harness.advance_slot(); + harness + .extend_chain_with_sync( + num_slots as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + SyncCommitteeStrategy::AllValidators, + LightClientStrategy::Disabled, + ) + .await; + + let current_epoch = harness.chain.epoch().unwrap(); + assert_eq!(current_epoch, Epoch::new(3)); + + // For epoch 3 with min_seed_lookahead=1: + // Post-Fulu decision slot: end of epoch N-2 = end of epoch 1 = slot 15 + // Legacy decision slot: end of epoch N-1 = end of epoch 2 = slot 23 + let true_decision_slot = Epoch::new(1).end_slot(slots_per_epoch); + let legacy_decision_slot = Epoch::new(2).end_slot(slots_per_epoch); + assert_eq!(true_decision_slot, Slot::new(15)); + assert_eq!(legacy_decision_slot, Slot::new(23)); + + // Fetch the block roots at these slots to compute expected dependent roots. + let expected_v2_root = harness + .chain + .block_root_at_slot(true_decision_slot, beacon_chain::WhenSlotSkipped::Prev) + .unwrap() + .unwrap(); + let expected_v1_root = harness + .chain + .block_root_at_slot(legacy_decision_slot, beacon_chain::WhenSlotSkipped::Prev) + .unwrap() + .unwrap(); + + // Sanity check: the two roots should be different since they refer to different blocks. + assert_ne!( + expected_v1_root, expected_v2_root, + "legacy and true decision roots should differ post-Fulu" + ); + + // Query v1 and v2 proposer duties for the current epoch. + let v1_result = client + .get_validator_duties_proposer(current_epoch) + .await + .unwrap(); + let v2_result = client + .get_validator_duties_proposer_v2(current_epoch) + .await + .unwrap(); + + // The proposer assignments (data) must be identical. + assert_eq!(v1_result.data, v2_result.data); + + // The dependent roots must differ. + assert_ne!( + v1_result.dependent_root, v2_result.dependent_root, + "v1 and v2 dependent roots should differ post-Fulu" + ); + + // Verify each root matches the expected value. + assert_eq!( + v1_result.dependent_root, expected_v1_root, + "v1 dependent root should be block root at end of epoch N-1" + ); + assert_eq!( + v2_result.dependent_root, expected_v2_root, + "v2 dependent root should be block root at end of epoch N-2" + ); + + // Also verify the next-epoch path (epoch 4). + let next_epoch = current_epoch + 1; + let v1_next = client + .get_validator_duties_proposer(next_epoch) + .await + .unwrap(); + let v2_next = client + .get_validator_duties_proposer_v2(next_epoch) + .await + .unwrap(); + + assert_eq!(v1_next.data, v2_next.data); + assert_ne!( + v1_next.dependent_root, v2_next.dependent_root, + "v1 and v2 next-epoch dependent roots should differ post-Fulu" + ); + + // For epoch 4: true decision is end of epoch 2 (slot 23), legacy is end of epoch 3 (slot 31). + let expected_v2_next_root = harness + .chain + .block_root_at_slot( + Epoch::new(2).end_slot(slots_per_epoch), + beacon_chain::WhenSlotSkipped::Prev, + ) + .unwrap() + .unwrap(); + let expected_v1_next_root = harness + .chain + .block_root_at_slot( + Epoch::new(3).end_slot(slots_per_epoch), + beacon_chain::WhenSlotSkipped::Prev, + ) + .unwrap() + .unwrap_or(harness.head_block_root()); + assert_eq!(v1_next.dependent_root, expected_v1_next_root); + assert_eq!(v2_next.dependent_root, expected_v2_next_root); + assert_ne!(expected_v2_next_root, harness.head_block_root()); +} + // Test that a request to `lighthouse/custody/backfill` succeeds by verifying that `CustodyContext` and `DataColumnCustodyInfo` // have been updated with the correct values. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 6696e109a5..739c39aa32 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -3392,6 +3392,80 @@ impl ApiTester { self } + pub async fn test_get_validator_duties_proposer_v2(self) -> Self { + let current_epoch = self.chain.epoch().unwrap(); + + for epoch in 0..=current_epoch.as_u64() + 1 { + let epoch = Epoch::from(epoch); + + // Compute the true dependent root using the spec's decision slot. + let decision_slot = self.chain.spec.proposer_shuffling_decision_slot::<E>(epoch); + let dependent_root = self + .chain + .block_root_at_slot(decision_slot, WhenSlotSkipped::Prev) + .unwrap() + .unwrap_or(self.chain.head_beacon_block_root()); + + let result = self + .client + .get_validator_duties_proposer_v2(epoch) + .await + .unwrap(); + + let mut state = self + .chain + .state_at_slot( + epoch.start_slot(E::slots_per_epoch()), + StateSkipConfig::WithStateRoots, + ) + .unwrap(); + + state + .build_committee_cache(RelativeEpoch::Current, &self.chain.spec) + .unwrap(); + + let expected_duties = epoch + .slot_iter(E::slots_per_epoch()) + .map(|slot| { + let index = state + .get_beacon_proposer_index(slot, &self.chain.spec) + .unwrap(); + let pubkey = state.validators().get(index).unwrap().pubkey; + + ProposerData { + pubkey, + validator_index: index as u64, + slot, + } + }) + .collect::<Vec<_>>(); + + let expected = DutiesResponse { + data: expected_duties, + execution_optimistic: Some(false), + dependent_root, + }; + + assert_eq!(result, expected); + + // v1 and v2 should return the same data. + let v1_result = self + .client + .get_validator_duties_proposer(epoch) + .await + .unwrap(); + assert_eq!(result.data, v1_result.data); + } + + // Requests to the epochs after the next epoch should fail. + self.client + .get_validator_duties_proposer_v2(current_epoch + 2) + .await + .unwrap_err(); + + self + } + pub async fn test_get_validator_duties_early(self) -> Self { let current_epoch = self.chain.epoch().unwrap(); let next_epoch = current_epoch + 1; @@ -7617,6 +7691,31 @@ async fn get_validator_duties_proposer_with_skip_slots() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn get_validator_duties_proposer_v2() { + ApiTester::new_from_config(ApiTesterConfig { + spec: test_spec::<E>(), + retain_historic_states: true, + ..ApiTesterConfig::default() + }) + .await + .test_get_validator_duties_proposer_v2() + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn get_validator_duties_proposer_v2_with_skip_slots() { + ApiTester::new_from_config(ApiTesterConfig { + spec: test_spec::<E>(), + retain_historic_states: true, + ..ApiTesterConfig::default() + }) + .await + .skip_slots(E::slots_per_epoch() * 2) + .test_get_validator_duties_proposer_v2() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn block_production() { ApiTester::new().await.test_block_production().await; diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index ac96da6173..628c12981a 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -2144,6 +2144,24 @@ impl BeaconNodeHttpClient { .await } + /// `GET v2/validator/duties/proposer/{epoch}` + pub async fn get_validator_duties_proposer_v2( + &self, + epoch: Epoch, + ) -> Result<DutiesResponse<Vec<ProposerData>>, Error> { + let mut path = self.eth_path(V2)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("duties") + .push("proposer") + .push(&epoch.to_string()); + + self.get_with_timeout(path, self.timeouts.proposer_duties) + .await + } + /// `GET v2/validator/blocks/{slot}` pub async fn get_validator_blocks<E: EthSpec>( &self, From 906400ed3435fffebaf5a56b60b778dbe3883ec1 Mon Sep 17 00:00:00 2001 From: Romeo <romeobourne211@gmail.com> Date: Tue, 10 Mar 2026 14:36:58 +0100 Subject: [PATCH 70/81] Implement proposer lookahead endpoint (#8815) closes #8809 Implement GET /eth/v1/beacon/states/{state_id}/proposer_lookahead ([beacon-APIs#565](https://github.com/ethereum/beacon-APIs/pull/565)). Returns the proposer lookahead from Fulu+ states; 400 for pre-Fulu. Includes integration test. Co-Authored-By: romeoscript <romeobourne211@gmail.com> Co-Authored-By: Tan Chee Keong <tanck@sigmaprime.io> --- beacon_node/http_api/src/beacon/states.rs | 68 +++++++++++++++++- beacon_node/http_api/src/lib.rs | 5 ++ beacon_node/http_api/tests/tests.rs | 85 ++++++++++++++++++++++- common/eth2/src/lib.rs | 41 +++++++++++ 4 files changed, 196 insertions(+), 3 deletions(-) diff --git a/beacon_node/http_api/src/beacon/states.rs b/beacon_node/http_api/src/beacon/states.rs index 50be7211d8..02ac3f4da7 100644 --- a/beacon_node/http_api/src/beacon/states.rs +++ b/beacon_node/http_api/src/beacon/states.rs @@ -3,17 +3,20 @@ use crate::task_spawner::{Priority, TaskSpawner}; use crate::utils::ResponseFilter; use crate::validator::pubkey_to_validator_index; use crate::version::{ - ResponseIncludesVersion, add_consensus_version_header, + ResponseIncludesVersion, add_consensus_version_header, add_ssz_content_type_header, execution_optimistic_finalized_beacon_response, }; use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes, WhenSlotSkipped}; use eth2::types::{ - ValidatorBalancesRequestBody, ValidatorId, ValidatorIdentitiesRequestBody, + self as api_types, ValidatorBalancesRequestBody, ValidatorId, ValidatorIdentitiesRequestBody, ValidatorsRequestBody, }; +use ssz::Encode; use std::sync::Arc; use types::{AttestationShufflingId, BeaconStateError, CommitteeCache, EthSpec, RelativeEpoch}; use warp::filters::BoxedFilter; +use warp::http::Response; +use warp::hyper::Body; use warp::{Filter, Reply}; use warp_utils::query::multi_key_query; @@ -160,6 +163,67 @@ pub fn get_beacon_state_pending_deposits<T: BeaconChainTypes>( .boxed() } +// GET beacon/states/{state_id}/proposer_lookahead +pub fn get_beacon_state_proposer_lookahead<T: BeaconChainTypes>( + beacon_states_path: BeaconStatesPath<T>, +) -> ResponseFilter { + beacon_states_path + .clone() + .and(warp::path("proposer_lookahead")) + .and(warp::path::end()) + .and(warp::header::optional::<api_types::Accept>("accept")) + .then( + |state_id: StateId, + task_spawner: TaskSpawner<T::EthSpec>, + chain: Arc<BeaconChain<T>>, + accept_header: Option<api_types::Accept>| { + task_spawner.blocking_response_task(Priority::P1, move || { + let (data, execution_optimistic, finalized, fork_name) = state_id + .map_state_and_execution_optimistic_and_finalized( + &chain, + |state, execution_optimistic, finalized| { + let Ok(lookahead) = state.proposer_lookahead() else { + return Err(warp_utils::reject::custom_bad_request( + "Proposer lookahead is not available for pre-Fulu states" + .to_string(), + )); + }; + + Ok(( + lookahead.to_vec(), + execution_optimistic, + finalized, + state.fork_name_unchecked(), + )) + }, + )?; + + match accept_header { + Some(api_types::Accept::Ssz) => Response::builder() + .status(200) + .body(data.as_ssz_bytes().into()) + .map(|res: Response<Body>| add_ssz_content_type_header(res)) + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "failed to create response: {}", + e + )) + }), + _ => execution_optimistic_finalized_beacon_response( + ResponseIncludesVersion::Yes(fork_name), + execution_optimistic, + finalized, + data, + ) + .map(|res| warp::reply::json(&res).into_response()), + } + .map(|resp| add_consensus_version_header(resp, fork_name)) + }) + }, + ) + .boxed() +} + // GET beacon/states/{state_id}/randao?epoch pub fn get_beacon_state_randao<T: BeaconChainTypes>( beacon_states_path: BeaconStatesPath<T>, diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index b5b74a3840..0ef8654d8d 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -650,6 +650,10 @@ pub fn serve<T: BeaconChainTypes>( let get_beacon_state_pending_consolidations = states::get_beacon_state_pending_consolidations(beacon_states_path.clone()); + // GET beacon/states/{state_id}/proposer_lookahead + let get_beacon_state_proposer_lookahead = + states::get_beacon_state_proposer_lookahead(beacon_states_path.clone()); + // GET beacon/headers // // Note: this endpoint only returns information about blocks in the canonical chain. Given that @@ -3284,6 +3288,7 @@ pub fn serve<T: BeaconChainTypes>( .uor(get_beacon_state_pending_deposits) .uor(get_beacon_state_pending_partial_withdrawals) .uor(get_beacon_state_pending_consolidations) + .uor(get_beacon_state_proposer_lookahead) .uor(get_beacon_headers) .uor(get_beacon_headers_block_id) .uor(get_beacon_block) diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 739c39aa32..a97ce01ac1 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -37,7 +37,7 @@ use proto_array::ExecutionStatus; use reqwest::{RequestBuilder, Response, StatusCode}; use sensitive_url::SensitiveUrl; use slot_clock::SlotClock; -use ssz::BitList; +use ssz::{BitList, Decode}; use state_processing::per_block_processing::get_expected_withdrawals; use state_processing::per_slot_processing; use state_processing::state_advance::partial_state_advance; @@ -1409,6 +1409,72 @@ impl ApiTester { self } + pub async fn test_beacon_states_proposer_lookahead(self) -> Self { + for state_id in self.interesting_state_ids() { + let mut state_opt = state_id + .state(&self.chain) + .ok() + .map(|(state, _execution_optimistic, _finalized)| state); + + let result = match self + .client + .get_beacon_states_proposer_lookahead(state_id.0) + .await + { + Ok(response) => response, + Err(e) => panic!("query failed incorrectly: {e:?}"), + }; + + if result.is_none() && state_opt.is_none() { + continue; + } + + let state = state_opt.as_mut().expect("result should be none"); + let expected = state.proposer_lookahead().unwrap(); + + let response = result.unwrap(); + assert_eq!(response.data(), &expected.to_vec()); + + // Check that the version header is returned in the response + let fork_name = state.fork_name(&self.chain.spec).unwrap(); + assert_eq!(response.version(), Some(fork_name),); + } + + self + } + + pub async fn test_beacon_states_proposer_lookahead_ssz(self) -> Self { + for state_id in self.interesting_state_ids() { + let mut state_opt = state_id + .state(&self.chain) + .ok() + .map(|(state, _execution_optimistic, _finalized)| state); + + let result = match self + .client + .get_beacon_states_proposer_lookahead_ssz(state_id.0) + .await + { + Ok(response) => response, + Err(e) => panic!("query failed incorrectly: {e:?}"), + }; + + if result.is_none() && state_opt.is_none() { + continue; + } + + let state = state_opt.as_mut().expect("result should be none"); + let expected = state.proposer_lookahead().unwrap(); + + let ssz_bytes = result.unwrap(); + let decoded = Vec::<u64>::from_ssz_bytes(&ssz_bytes) + .expect("should decode SSZ proposer lookahead"); + assert_eq!(decoded, expected.to_vec()); + } + + self + } + pub async fn test_beacon_headers_all_slots(self) -> Self { for slot in 0..CHAIN_LENGTH { let slot = Slot::from(slot); @@ -7360,6 +7426,23 @@ async fn beacon_get_state_info_electra() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn beacon_get_state_info_fulu() { + let mut config = ApiTesterConfig::default(); + config.spec.altair_fork_epoch = Some(Epoch::new(0)); + config.spec.bellatrix_fork_epoch = Some(Epoch::new(0)); + config.spec.capella_fork_epoch = Some(Epoch::new(0)); + config.spec.deneb_fork_epoch = Some(Epoch::new(0)); + config.spec.electra_fork_epoch = Some(Epoch::new(0)); + config.spec.fulu_fork_epoch = Some(Epoch::new(0)); + ApiTester::new_from_config(config) + .await + .test_beacon_states_proposer_lookahead() + .await + .test_beacon_states_proposer_lookahead_ssz() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn beacon_get_blocks() { ApiTester::new() diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 628c12981a..5547ced491 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -898,6 +898,47 @@ impl BeaconNodeHttpClient { .map(|opt| opt.map(BeaconResponse::ForkVersioned)) } + /// `GET beacon/states/{state_id}/proposer_lookahead` + /// + /// Returns `Ok(None)` on a 404 error. + pub async fn get_beacon_states_proposer_lookahead( + &self, + state_id: StateId, + ) -> Result<Option<ExecutionOptimisticFinalizedBeaconResponse<Vec<u64>>>, Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("states") + .push(&state_id.to_string()) + .push("proposer_lookahead"); + + self.get_fork_contextual(path, |fork| fork) + .await + .map(|opt| opt.map(BeaconResponse::ForkVersioned)) + } + + /// `GET beacon/states/{state_id}/proposer_lookahead` + /// + /// Returns `Ok(None)` on a 404 error. + pub async fn get_beacon_states_proposer_lookahead_ssz( + &self, + state_id: StateId, + ) -> Result<Option<Vec<u8>>, Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("states") + .push(&state_id.to_string()) + .push("proposer_lookahead"); + + self.get_bytes_opt_accept_header(path, Accept::Ssz, self.timeouts.default) + .await + } + /// `GET beacon/light_client/updates` /// /// Returns `Ok(None)` on a 404 error. From 2bb79f43aadbf6e71ab2ea67efdda4585e9184ea Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi <eserilev@gmail.com> Date: Wed, 11 Mar 2026 02:43:59 +0900 Subject: [PATCH 71/81] Update contribution guidlines regarding LLM usage (#8879) Co-Authored-By: Eitan Seri- Levi <eserilev@gmail.com> --- CONTRIBUTING.md | 9 +++++++++ wordlist.txt | 1 + 2 files changed, 10 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4cad219c89..f81f75cd8b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,6 +37,15 @@ Requests](https://github.com/sigp/lighthouse/pulls) is where code gets reviewed. We use [discord](https://discord.gg/cyAszAh) to chat informally. +### A Note on LLM usage + +We are happy to support contributors who are genuinely engaging with the code base. Our general policy regarding LLM usage: + +- Please refrain from submissions that you haven't thoroughly understood, reviewed, and tested. +- Please disclose if a significant portion of your contribution was AI-generated. +- Descriptions and comments should be made by you. +- We reserve the right to reject any contributions we feel are violating the spirit of open source contribution. + ### General Work-Flow We recommend the following work-flow for contributors: diff --git a/wordlist.txt b/wordlist.txt index e0e1fe7d73..822e336146 100644 --- a/wordlist.txt +++ b/wordlist.txt @@ -58,6 +58,7 @@ JSON KeyManager Kurtosis LMDB +LLM LLVM LRU LTO From 815040dc3c056560c5c67a7a71a87d2bbc658fd2 Mon Sep 17 00:00:00 2001 From: Mac L <mjladson@pm.me> Date: Wed, 11 Mar 2026 07:43:26 +0200 Subject: [PATCH 72/81] Remove `c-kzg` (#8930) #7330 Removes `c-kzg` from our `kzg` crate and rely fully on the `rust_eth_kzg` crate. This removes the old `Blob` type entirely and instead handles `rust_eth_kzg::KzgBlobRef`s directly which allows us to avoid some extra stack allocations . Similarly, we make `Bytes32` and `Bytes48` type aliases rather than structs as this fits better with the new `rust_eth_kzg` API. Co-Authored-By: Mac L <mjladson@pm.me> --- Cargo.lock | 1 - Cargo.toml | 3 - beacon_node/beacon_chain/src/kzg_utils.rs | 50 +++-- .../test_utils/execution_block_generator.rs | 15 +- consensus/types/src/data/blob_sidecar.rs | 11 +- consensus/types/src/kzg_ext/mod.rs | 2 +- crypto/kzg/Cargo.toml | 2 - crypto/kzg/benches/benchmark.rs | 18 +- crypto/kzg/src/kzg_commitment.rs | 10 +- crypto/kzg/src/kzg_proof.rs | 8 +- crypto/kzg/src/lib.rs | 172 ++++++++---------- crypto/kzg/src/trusted_setup.rs | 18 +- deny.toml | 1 + .../cases/kzg_verify_cell_kzg_proof_batch.rs | 6 +- 14 files changed, 129 insertions(+), 188 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 704039a175..0ca12dce46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4825,7 +4825,6 @@ name = "kzg" version = "0.1.0" dependencies = [ "arbitrary", - "c-kzg", "criterion", "educe", "ethereum_hashing", diff --git a/Cargo.toml b/Cargo.toml index efedfe3b37..7572cc324d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -117,9 +117,6 @@ bitvec = "1" bls = { path = "crypto/bls" } byteorder = "1" bytes = "1.11.1" -# Turn off c-kzg's default features which include `blst/portable`. We can turn on blst's portable -# feature ourselves when desired. -c-kzg = { version = "2.1", default-features = false } cargo_metadata = "0.19" clap = { version = "4.5.4", features = ["derive", "cargo", "wrap_help"] } clap_utils = { path = "common/clap_utils" } diff --git a/beacon_node/beacon_chain/src/kzg_utils.rs b/beacon_node/beacon_chain/src/kzg_utils.rs index 33b3260361..10cb208729 100644 --- a/beacon_node/beacon_chain/src/kzg_utils.rs +++ b/beacon_node/beacon_chain/src/kzg_utils.rs @@ -1,6 +1,5 @@ use kzg::{ - Blob as KzgBlob, Bytes48, Cell as KzgCell, CellRef as KzgCellRef, CellsAndKzgProofs, - Error as KzgError, Kzg, KzgBlobRef, + Cell as KzgCell, CellRef as KzgCellRef, CellsAndKzgProofs, Error as KzgError, Kzg, KzgBlobRef, }; use rayon::prelude::*; use ssz_types::{FixedVector, VariableList}; @@ -15,18 +14,18 @@ use types::{ SignedBeaconBlock, SignedBeaconBlockHeader, SignedBlindedBeaconBlock, Slot, }; -/// Converts a blob ssz List object to an array to be used with the kzg -/// crypto library. -fn ssz_blob_to_crypto_blob<E: EthSpec>(blob: &Blob<E>) -> Result<KzgBlob, KzgError> { - KzgBlob::from_bytes(blob.as_ref()).map_err(Into::into) +/// Converts a blob ssz FixedVector to a reference to a fixed-size array +/// to be used with `rust_eth_kzg`. +fn ssz_blob_to_kzg_blob_ref<E: EthSpec>(blob: &Blob<E>) -> Result<KzgBlobRef<'_>, KzgError> { + blob.as_ref().try_into().map_err(|e| { + KzgError::InconsistentArrayLength(format!( + "blob should have a guaranteed size due to FixedVector: {e:?}" + )) + }) } -fn ssz_blob_to_crypto_blob_boxed<E: EthSpec>(blob: &Blob<E>) -> Result<Box<KzgBlob>, KzgError> { - ssz_blob_to_crypto_blob::<E>(blob).map(Box::new) -} - -/// Converts a cell ssz List object to an array to be used with the kzg -/// crypto library. +/// Converts a cell ssz FixedVector to a reference to a fixed-size array +/// to be used with `rust_eth_kzg`. fn ssz_cell_to_crypto_cell<E: EthSpec>(cell: &Cell<E>) -> Result<KzgCellRef<'_>, KzgError> { let cell_bytes: &[u8] = cell.as_ref(); cell_bytes @@ -42,8 +41,8 @@ pub fn validate_blob<E: EthSpec>( kzg_proof: KzgProof, ) -> Result<(), KzgError> { let _timer = crate::metrics::start_timer(&crate::metrics::KZG_VERIFICATION_SINGLE_TIMES); - let kzg_blob = ssz_blob_to_crypto_blob_boxed::<E>(blob)?; - kzg.verify_blob_kzg_proof(&kzg_blob, kzg_commitment, kzg_proof) + let kzg_blob = ssz_blob_to_kzg_blob_ref::<E>(blob)?; + kzg.verify_blob_kzg_proof(kzg_blob, kzg_commitment, kzg_proof) } /// Validate a batch of `DataColumnSidecar`. @@ -72,7 +71,7 @@ where } for &proof in data_column.kzg_proofs() { - proofs.push(Bytes48::from(proof)); + proofs.push(proof.0); } // In Gloas, commitments come from the block's ExecutionPayloadBid, not the sidecar. @@ -90,7 +89,7 @@ where }; for &commitment in kzg_commitments.iter() { - commitments.push(Bytes48::from(commitment)); + commitments.push(commitment.0); } let expected_len = column_indices.len(); @@ -120,7 +119,7 @@ pub fn validate_blobs<E: EthSpec>( let _timer = crate::metrics::start_timer(&crate::metrics::KZG_VERIFICATION_BATCH_TIMES); let blobs = blobs .into_iter() - .map(|blob| ssz_blob_to_crypto_blob::<E>(blob)) + .map(|blob| ssz_blob_to_kzg_blob_ref::<E>(blob)) .collect::<Result<Vec<_>, KzgError>>()?; kzg.verify_blob_kzg_proof_batch(&blobs, expected_kzg_commitments, kzg_proofs) @@ -132,8 +131,8 @@ pub fn compute_blob_kzg_proof<E: EthSpec>( blob: &Blob<E>, kzg_commitment: KzgCommitment, ) -> Result<KzgProof, KzgError> { - let kzg_blob = ssz_blob_to_crypto_blob_boxed::<E>(blob)?; - kzg.compute_blob_kzg_proof(&kzg_blob, kzg_commitment) + let kzg_blob = ssz_blob_to_kzg_blob_ref::<E>(blob)?; + kzg.compute_blob_kzg_proof(kzg_blob, kzg_commitment) } /// Compute the kzg commitment for a given blob. @@ -141,8 +140,8 @@ pub fn blob_to_kzg_commitment<E: EthSpec>( kzg: &Kzg, blob: &Blob<E>, ) -> Result<KzgCommitment, KzgError> { - let kzg_blob = ssz_blob_to_crypto_blob_boxed::<E>(blob)?; - kzg.blob_to_kzg_commitment(&kzg_blob) + let kzg_blob = ssz_blob_to_kzg_blob_ref::<E>(blob)?; + kzg.blob_to_kzg_commitment(kzg_blob) } /// Compute the kzg proof for a given blob and an evaluation point z. @@ -151,10 +150,9 @@ pub fn compute_kzg_proof<E: EthSpec>( blob: &Blob<E>, z: Hash256, ) -> Result<(KzgProof, Hash256), KzgError> { - let z = z.0.into(); - let kzg_blob = ssz_blob_to_crypto_blob_boxed::<E>(blob)?; - kzg.compute_kzg_proof(&kzg_blob, &z) - .map(|(proof, z)| (proof, Hash256::from_slice(&z.to_vec()))) + let kzg_blob = ssz_blob_to_kzg_blob_ref::<E>(blob)?; + kzg.compute_kzg_proof(kzg_blob, &z.0) + .map(|(proof, z)| (proof, Hash256::from_slice(&z))) } /// Verify a `kzg_proof` for a `kzg_commitment` that evaluating a polynomial at `z` results in `y` @@ -165,7 +163,7 @@ pub fn verify_kzg_proof<E: EthSpec>( z: Hash256, y: Hash256, ) -> Result<bool, KzgError> { - kzg.verify_kzg_proof(kzg_commitment, &z.0.into(), &y.0.into(), kzg_proof) + kzg.verify_kzg_proof(kzg_commitment, &z.0, &y.0, kzg_proof) } /// Build data column sidecars from a signed beacon block and its blobs. diff --git a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs index 62a46246da..e94924d8b2 100644 --- a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs +++ b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs @@ -989,7 +989,7 @@ pub fn generate_pow_block( #[cfg(test)] mod test { use super::*; - use kzg::{Bytes48, CellRef, KzgBlobRef, trusted_setup::get_trusted_setup}; + use kzg::{CellRef, KzgBlobRef, trusted_setup::get_trusted_setup}; use types::{MainnetEthSpec, MinimalEthSpec}; #[test] @@ -1015,10 +1015,11 @@ mod test { fn validate_blob_bundle_v1<E: EthSpec>() -> Result<(), String> { let kzg = load_kzg()?; let (kzg_commitment, kzg_proof, blob) = load_test_blobs_bundle_v1::<E>()?; - let kzg_blob = kzg::Blob::from_bytes(blob.as_ref()) - .map(Box::new) - .map_err(|e| format!("Error converting blob to kzg blob: {e:?}"))?; - kzg.verify_blob_kzg_proof(&kzg_blob, kzg_commitment, kzg_proof) + let kzg_blob: KzgBlobRef = blob + .as_ref() + .try_into() + .map_err(|e| format!("Error converting blob to kzg blob ref: {e:?}"))?; + kzg.verify_blob_kzg_proof(kzg_blob, kzg_commitment, kzg_proof) .map_err(|e| format!("Invalid blobs bundle: {e:?}")) } @@ -1028,8 +1029,8 @@ mod test { load_test_blobs_bundle_v2::<E>().map(|(commitment, proofs, blob)| { let kzg_blob: KzgBlobRef = blob.as_ref().try_into().unwrap(); ( - vec![Bytes48::from(commitment); proofs.len()], - proofs.into_iter().map(|p| p.into()).collect::<Vec<_>>(), + vec![commitment.0; proofs.len()], + proofs.into_iter().map(|p| p.0).collect::<Vec<_>>(), kzg.compute_cells(kzg_blob).unwrap(), ) })?; diff --git a/consensus/types/src/data/blob_sidecar.rs b/consensus/types/src/data/blob_sidecar.rs index 638491d6d7..2774176190 100644 --- a/consensus/types/src/data/blob_sidecar.rs +++ b/consensus/types/src/data/blob_sidecar.rs @@ -3,7 +3,7 @@ use std::{fmt::Debug, hash::Hash, sync::Arc}; use bls::Signature; use context_deserialize::context_deserialize; use educe::Educe; -use kzg::{BYTES_PER_BLOB, BYTES_PER_FIELD_ELEMENT, Blob as KzgBlob, Kzg, KzgCommitment, KzgProof}; +use kzg::{BYTES_PER_BLOB, BYTES_PER_FIELD_ELEMENT, Kzg, KzgCommitment, KzgProof}; use merkle_proof::{MerkleTreeError, merkle_root_from_branch, verify_merkle_proof}; use rand::Rng; use safe_arith::ArithError; @@ -253,14 +253,17 @@ impl<E: EthSpec> BlobSidecar<E> { let blob = Blob::<E>::new(blob_bytes) .map_err(|e| format!("error constructing random blob: {:?}", e))?; - let kzg_blob = KzgBlob::from_bytes(&blob).unwrap(); + let kzg_blob: &[u8; BYTES_PER_BLOB] = blob + .as_ref() + .try_into() + .map_err(|e| format!("error converting blob to kzg blob ref: {:?}", e))?; let commitment = kzg - .blob_to_kzg_commitment(&kzg_blob) + .blob_to_kzg_commitment(kzg_blob) .map_err(|e| format!("error computing kzg commitment: {:?}", e))?; let proof = kzg - .compute_blob_kzg_proof(&kzg_blob, commitment) + .compute_blob_kzg_proof(kzg_blob, commitment) .map_err(|e| format!("error computing kzg proof: {:?}", e))?; Ok(Self { diff --git a/consensus/types/src/kzg_ext/mod.rs b/consensus/types/src/kzg_ext/mod.rs index 63533ec71f..e0ec9dd956 100644 --- a/consensus/types/src/kzg_ext/mod.rs +++ b/consensus/types/src/kzg_ext/mod.rs @@ -1,6 +1,6 @@ pub mod consts; -pub use kzg::{Blob as KzgBlob, Error as KzgError, Kzg, KzgCommitment, KzgProof}; +pub use kzg::{Error as KzgError, Kzg, KzgCommitment, KzgProof}; use ssz_types::VariableList; diff --git a/crypto/kzg/Cargo.toml b/crypto/kzg/Cargo.toml index 840f8cfc9c..19f39a182b 100644 --- a/crypto/kzg/Cargo.toml +++ b/crypto/kzg/Cargo.toml @@ -12,7 +12,6 @@ fake_crypto = [] [dependencies] arbitrary = { workspace = true, optional = true } -c-kzg = { workspace = true } educe = { workspace = true } ethereum_hashing = { workspace = true } ethereum_serde_utils = { workspace = true } @@ -28,7 +27,6 @@ tree_hash = { workspace = true } [dev-dependencies] criterion = { workspace = true } -serde_json = { workspace = true } [[bench]] name = "benchmark" diff --git a/crypto/kzg/benches/benchmark.rs b/crypto/kzg/benches/benchmark.rs index 432d84654a..d5d5596211 100644 --- a/crypto/kzg/benches/benchmark.rs +++ b/crypto/kzg/benches/benchmark.rs @@ -1,6 +1,5 @@ -use c_kzg::KzgSettings; use criterion::{criterion_group, criterion_main, Criterion}; -use kzg::{trusted_setup::get_trusted_setup, TrustedSetup, NO_PRECOMPUTE}; +use kzg::trusted_setup::get_trusted_setup; use rust_eth_kzg::{DASContext, TrustedSetup as PeerDASTrustedSetup}; pub fn bench_init_context(c: &mut Criterion) { @@ -20,21 +19,6 @@ pub fn bench_init_context(c: &mut Criterion) { ) }) }); - c.bench_function("Initialize context c-kzg (4844)", |b| { - b.iter(|| { - let trusted_setup: TrustedSetup = - serde_json::from_reader(trusted_setup_bytes.as_slice()) - .map_err(|e| format!("Unable to read trusted setup file: {}", e)) - .expect("should have trusted setup"); - KzgSettings::load_trusted_setup( - &trusted_setup.g1_monomial(), - &trusted_setup.g1_lagrange(), - &trusted_setup.g2_monomial(), - NO_PRECOMPUTE, - ) - .unwrap() - }) - }); } criterion_group!(benches, bench_init_context); diff --git a/crypto/kzg/src/kzg_commitment.rs b/crypto/kzg/src/kzg_commitment.rs index bc5fc5f5aa..d8ef4b36cf 100644 --- a/crypto/kzg/src/kzg_commitment.rs +++ b/crypto/kzg/src/kzg_commitment.rs @@ -1,4 +1,4 @@ -use c_kzg::BYTES_PER_COMMITMENT; +use crate::{Bytes48, BYTES_PER_COMMITMENT}; use educe::Educe; use ethereum_hashing::hash_fixed; use serde::de::{Deserialize, Deserializer}; @@ -14,7 +14,7 @@ pub const VERSIONED_HASH_VERSION_KZG: u8 = 0x01; #[derive(Educe, Clone, Copy, Encode, Decode)] #[educe(PartialEq, Eq, Hash)] #[ssz(struct_behaviour = "transparent")] -pub struct KzgCommitment(pub [u8; c_kzg::BYTES_PER_COMMITMENT]); +pub struct KzgCommitment(pub [u8; BYTES_PER_COMMITMENT]); impl KzgCommitment { pub fn calculate_versioned_hash(&self) -> Hash256 { @@ -24,13 +24,13 @@ impl KzgCommitment { } pub fn empty_for_testing() -> Self { - KzgCommitment([0; c_kzg::BYTES_PER_COMMITMENT]) + KzgCommitment([0; BYTES_PER_COMMITMENT]) } } -impl From<KzgCommitment> for c_kzg::Bytes48 { +impl From<KzgCommitment> for Bytes48 { fn from(value: KzgCommitment) -> Self { - value.0.into() + value.0 } } diff --git a/crypto/kzg/src/kzg_proof.rs b/crypto/kzg/src/kzg_proof.rs index aa9ed185a0..e0867520eb 100644 --- a/crypto/kzg/src/kzg_proof.rs +++ b/crypto/kzg/src/kzg_proof.rs @@ -1,4 +1,4 @@ -use c_kzg::BYTES_PER_PROOF; +use crate::BYTES_PER_PROOF; use serde::de::{Deserialize, Deserializer}; use serde::ser::{Serialize, Serializer}; use ssz_derive::{Decode, Encode}; @@ -11,12 +11,6 @@ use tree_hash::{PackedEncoding, TreeHash}; #[ssz(struct_behaviour = "transparent")] pub struct KzgProof(pub [u8; BYTES_PER_PROOF]); -impl From<KzgProof> for c_kzg::Bytes48 { - fn from(value: KzgProof) -> Self { - value.0.into() - } -} - impl KzgProof { /// Creates a valid proof using `G1_POINT_AT_INFINITY`. pub fn empty() -> Self { diff --git a/crypto/kzg/src/lib.rs b/crypto/kzg/src/lib.rs index 66499dad8e..6ee352b0db 100644 --- a/crypto/kzg/src/lib.rs +++ b/crypto/kzg/src/lib.rs @@ -12,11 +12,12 @@ pub use crate::{ trusted_setup::TrustedSetup, }; -pub use c_kzg::{ - Blob, Bytes32, Bytes48, KzgSettings, BYTES_PER_BLOB, BYTES_PER_COMMITMENT, - BYTES_PER_FIELD_ELEMENT, BYTES_PER_PROOF, FIELD_ELEMENTS_PER_BLOB, +pub use rust_eth_kzg::constants::{ + BYTES_PER_BLOB, BYTES_PER_COMMITMENT, BYTES_PER_FIELD_ELEMENT, FIELD_ELEMENTS_PER_BLOB, }; +pub const BYTES_PER_PROOF: usize = 48; + use crate::trusted_setup::load_trusted_setup; use rayon::prelude::*; pub use rust_eth_kzg::{ @@ -25,13 +26,6 @@ pub use rust_eth_kzg::{ }; use tracing::{instrument, Span}; -/// Disables the fixed-base multi-scalar multiplication optimization for computing -/// cell KZG proofs, because `rust-eth-kzg` already handles the precomputation. -/// -/// Details about `precompute` parameter can be found here: -/// <https://github.com/ethereum/c-kzg-4844/pull/545/files> -pub const NO_PRECOMPUTE: u64 = 0; - // Note: Both `NUMBER_OF_COLUMNS` and `CELLS_PER_EXT_BLOB` are preset values - however this // is a constant in the KZG library - be aware that overriding `NUMBER_OF_COLUMNS` will break KZG // operations. @@ -39,14 +33,15 @@ pub type CellsAndKzgProofs = ([Cell; CELLS_PER_EXT_BLOB], [KzgProof; CELLS_PER_E pub type KzgBlobRef<'a> = &'a [u8; BYTES_PER_BLOB]; +type Bytes32 = [u8; 32]; +type Bytes48 = [u8; 48]; + #[derive(Debug)] pub enum Error { /// An error from initialising the trusted setup. TrustedSetupError(String), - /// An error from the underlying kzg library. - Kzg(c_kzg::Error), - /// A prover/verifier error from the rust-eth-kzg library. - PeerDASKZG(rust_eth_kzg::Error), + /// An error from the rust-eth-kzg library. + Kzg(rust_eth_kzg::Error), /// The kzg verification failed KzgVerificationFailed, /// Misc indexing error @@ -57,38 +52,29 @@ pub enum Error { DASContextUninitialized, } -impl From<c_kzg::Error> for Error { - fn from(value: c_kzg::Error) -> Self { +impl From<rust_eth_kzg::Error> for Error { + fn from(value: rust_eth_kzg::Error) -> Self { Error::Kzg(value) } } -/// A wrapper over a kzg library that holds the trusted setup parameters. +/// A wrapper over the rust-eth-kzg library that holds the trusted setup parameters. #[derive(Debug)] pub struct Kzg { - trusted_setup: KzgSettings, context: DASContext, } impl Kzg { pub fn new_from_trusted_setup_no_precomp(trusted_setup: &[u8]) -> Result<Self, Error> { - let (ckzg_trusted_setup, rkzg_trusted_setup) = load_trusted_setup(trusted_setup)?; + let rkzg_trusted_setup = load_trusted_setup(trusted_setup)?; let context = DASContext::new(&rkzg_trusted_setup, rust_eth_kzg::UsePrecomp::No); - Ok(Self { - trusted_setup: KzgSettings::load_trusted_setup( - &ckzg_trusted_setup.g1_monomial(), - &ckzg_trusted_setup.g1_lagrange(), - &ckzg_trusted_setup.g2_monomial(), - NO_PRECOMPUTE, - )?, - context, - }) + Ok(Self { context }) } /// Load the kzg trusted setup parameters from a vec of G1 and G2 points. pub fn new_from_trusted_setup(trusted_setup: &[u8]) -> Result<Self, Error> { - let (ckzg_trusted_setup, rkzg_trusted_setup) = load_trusted_setup(trusted_setup)?; + let rkzg_trusted_setup = load_trusted_setup(trusted_setup)?; // It's not recommended to change the config parameter for precomputation as storage // grows exponentially, but the speedup is exponential - after a while the speedup @@ -100,15 +86,7 @@ impl Kzg { }, ); - Ok(Self { - trusted_setup: KzgSettings::load_trusted_setup( - &ckzg_trusted_setup.g1_monomial(), - &ckzg_trusted_setup.g1_lagrange(), - &ckzg_trusted_setup.g2_monomial(), - NO_PRECOMPUTE, - )?, - context, - }) + Ok(Self { context }) } fn context(&self) -> &DASContext { @@ -118,34 +96,35 @@ impl Kzg { /// Compute the kzg proof given a blob and its kzg commitment. pub fn compute_blob_kzg_proof( &self, - blob: &Blob, + blob: KzgBlobRef<'_>, kzg_commitment: KzgCommitment, ) -> Result<KzgProof, Error> { - self.trusted_setup - .compute_blob_kzg_proof(blob, &kzg_commitment.into()) - .map(|proof| KzgProof(proof.to_bytes().into_inner())) - .map_err(Into::into) + let proof = self + .context() + .compute_blob_kzg_proof(blob, &kzg_commitment.0) + .map_err(Error::Kzg)?; + Ok(KzgProof(proof)) } /// Verify a kzg proof given the blob, kzg commitment and kzg proof. pub fn verify_blob_kzg_proof( &self, - blob: &Blob, + blob: KzgBlobRef<'_>, kzg_commitment: KzgCommitment, kzg_proof: KzgProof, ) -> Result<(), Error> { if cfg!(feature = "fake_crypto") { return Ok(()); } - if !self.trusted_setup.verify_blob_kzg_proof( - blob, - &kzg_commitment.into(), - &kzg_proof.into(), - )? { - Err(Error::KzgVerificationFailed) - } else { - Ok(()) - } + self.context() + .verify_blob_kzg_proof(blob, &kzg_commitment.0, &kzg_proof.0) + .map_err(|e| { + if e.is_proof_invalid() { + Error::KzgVerificationFailed + } else { + Error::Kzg(e) + } + }) } /// Verify a batch of blob commitment proof triplets. @@ -154,52 +133,48 @@ impl Kzg { /// TODO(pawan): test performance against a parallelized rayon impl. pub fn verify_blob_kzg_proof_batch( &self, - blobs: &[Blob], + blobs: &[KzgBlobRef<'_>], kzg_commitments: &[KzgCommitment], kzg_proofs: &[KzgProof], ) -> Result<(), Error> { if cfg!(feature = "fake_crypto") { return Ok(()); } - let commitments_bytes = kzg_commitments - .iter() - .map(|comm| Bytes48::from(*comm)) - .collect::<Vec<_>>(); + let blob_refs: Vec<&[u8; BYTES_PER_BLOB]> = blobs.to_vec(); + let commitment_refs: Vec<&[u8; 48]> = kzg_commitments.iter().map(|c| &c.0).collect(); + let proof_refs: Vec<&[u8; 48]> = kzg_proofs.iter().map(|p| &p.0).collect(); - let proofs_bytes = kzg_proofs - .iter() - .map(|proof| Bytes48::from(*proof)) - .collect::<Vec<_>>(); - - if !self.trusted_setup.verify_blob_kzg_proof_batch( - blobs, - &commitments_bytes, - &proofs_bytes, - )? { - Err(Error::KzgVerificationFailed) - } else { - Ok(()) - } + self.context() + .verify_blob_kzg_proof_batch(blob_refs, commitment_refs, proof_refs) + .map_err(|e| { + if e.is_proof_invalid() { + Error::KzgVerificationFailed + } else { + Error::Kzg(e) + } + }) } /// Converts a blob to a kzg commitment. - pub fn blob_to_kzg_commitment(&self, blob: &Blob) -> Result<KzgCommitment, Error> { - self.trusted_setup + pub fn blob_to_kzg_commitment(&self, blob: KzgBlobRef<'_>) -> Result<KzgCommitment, Error> { + let commitment = self + .context() .blob_to_kzg_commitment(blob) - .map(|commitment| KzgCommitment(commitment.to_bytes().into_inner())) - .map_err(Into::into) + .map_err(Error::Kzg)?; + Ok(KzgCommitment(commitment)) } /// Computes the kzg proof for a given `blob` and an evaluation point `z` pub fn compute_kzg_proof( &self, - blob: &Blob, + blob: KzgBlobRef<'_>, z: &Bytes32, ) -> Result<(KzgProof, Bytes32), Error> { - self.trusted_setup - .compute_kzg_proof(blob, z) - .map(|(proof, y)| (KzgProof(proof.to_bytes().into_inner()), y)) - .map_err(Into::into) + let (proof, y) = self + .context() + .compute_kzg_proof(blob, *z) + .map_err(Error::Kzg)?; + Ok((KzgProof(proof), y)) } /// Verifies a `kzg_proof` for a `kzg_commitment` that evaluating a polynomial at `z` results in `y` @@ -213,9 +188,14 @@ impl Kzg { if cfg!(feature = "fake_crypto") { return Ok(true); } - self.trusted_setup - .verify_kzg_proof(&kzg_commitment.into(), z, y, &kzg_proof.into()) - .map_err(Into::into) + match self + .context() + .verify_kzg_proof(&kzg_commitment.0, *z, *y, &kzg_proof.0) + { + Ok(()) => Ok(true), + Err(e) if e.is_proof_invalid() => Ok(false), + Err(e) => Err(Error::Kzg(e)), + } } /// Computes the cells and associated proofs for a given `blob`. @@ -226,18 +206,15 @@ impl Kzg { let (cells, proofs) = self .context() .compute_cells_and_kzg_proofs(blob) - .map_err(Error::PeerDASKZG)?; + .map_err(Error::Kzg)?; - // Convert the proof type to a c-kzg proof type - let c_kzg_proof = proofs.map(KzgProof); - Ok((cells, c_kzg_proof)) + let kzg_proofs = proofs.map(KzgProof); + Ok((cells, kzg_proofs)) } /// Computes the cells for a given `blob`. pub fn compute_cells(&self, blob: KzgBlobRef<'_>) -> Result<[Cell; CELLS_PER_EXT_BLOB], Error> { - self.context() - .compute_cells(blob) - .map_err(Error::PeerDASKZG) + self.context().compute_cells(blob).map_err(Error::Kzg) } /// Verifies a batch of cell-proof-commitment triplets. @@ -291,8 +268,8 @@ impl Kzg { for (cell, proof, commitment) in &column_data { cells.push(*cell); - proofs.push(proof.as_ref()); - commitments.push(commitment.as_ref()); + proofs.push(proof); + commitments.push(commitment); } // Create per-chunk tracing span for visualizing parallel processing. @@ -319,7 +296,7 @@ impl Kzg { Err(e) if e.is_proof_invalid() => { Err((Some(column_index), Error::KzgVerificationFailed)) } - Err(e) => Err((Some(column_index), Error::PeerDASKZG(e))), + Err(e) => Err((Some(column_index), Error::Kzg(e))), } }) .collect::<Result<Vec<()>, (Option<u64>, Error)>>()?; @@ -335,10 +312,9 @@ impl Kzg { let (cells, proofs) = self .context() .recover_cells_and_kzg_proofs(cell_ids.to_vec(), cells.to_vec()) - .map_err(Error::PeerDASKZG)?; + .map_err(Error::Kzg)?; - // Convert the proof type to a c-kzg proof type - let c_kzg_proof = proofs.map(KzgProof); - Ok((cells, c_kzg_proof)) + let kzg_proofs = proofs.map(KzgProof); + Ok((cells, kzg_proofs)) } } diff --git a/crypto/kzg/src/trusted_setup.rs b/crypto/kzg/src/trusted_setup.rs index 75884b8199..5c285b50f2 100644 --- a/crypto/kzg/src/trusted_setup.rs +++ b/crypto/kzg/src/trusted_setup.rs @@ -24,7 +24,7 @@ struct G1Point([u8; BYTES_PER_G1_POINT]); struct G2Point([u8; BYTES_PER_G2_POINT]); /// Contains the trusted setup parameters that are required to instantiate a -/// `c_kzg::KzgSettings` object. +/// `rust_eth_kzg::TrustedSetup` object. /// /// The serialize/deserialize implementations are written according to /// the format specified in the ethereum consensus specs trusted setup files. @@ -155,19 +155,9 @@ fn strip_prefix(s: &str) -> &str { } } -/// Loads the trusted setup from JSON. -/// -/// ## Note: -/// Currently we load both c-kzg and rust-eth-kzg trusted setup structs, because c-kzg is still being -/// used for 4844. Longer term we're planning to switch all KZG operations to the rust-eth-kzg -/// crate, and we'll be able to maintain a single trusted setup struct. -pub(crate) fn load_trusted_setup( - trusted_setup: &[u8], -) -> Result<(TrustedSetup, PeerDASTrustedSetup), Error> { - let ckzg_trusted_setup: TrustedSetup = serde_json::from_slice(trusted_setup) - .map_err(|e| Error::TrustedSetupError(format!("{e:?}")))?; +/// Loads the trusted setup from JSON bytes into a `rust_eth_kzg::TrustedSetup`. +pub(crate) fn load_trusted_setup(trusted_setup: &[u8]) -> Result<PeerDASTrustedSetup, Error> { let trusted_setup_json = std::str::from_utf8(trusted_setup) .map_err(|e| Error::TrustedSetupError(format!("{e:?}")))?; - let rkzg_trusted_setup = PeerDASTrustedSetup::from_json(trusted_setup_json); - Ok((ckzg_trusted_setup, rkzg_trusted_setup)) + Ok(PeerDASTrustedSetup::from_json(trusted_setup_json)) } diff --git a/deny.toml b/deny.toml index 3b230155f7..cf0cd7d3cd 100644 --- a/deny.toml +++ b/deny.toml @@ -11,6 +11,7 @@ deny = [ { crate = "derivative", reason = "use educe or derive_more instead" }, { crate = "ark-ff", reason = "present in Cargo.lock but not needed by Lighthouse" }, { crate = "openssl", reason = "non-Rust dependency, use rustls instead" }, + { crate = "c-kzg", reason = "non-Rust dependency, use rust_eth_kzg instead" }, { crate = "strum", deny-multiple-versions = true, reason = "takes a long time to compile" }, { crate = "reqwest", deny-multiple-versions = true, reason = "takes a long time to compile" }, { crate = "aes", deny-multiple-versions = true, reason = "takes a long time to compile" }, diff --git a/testing/ef_tests/src/cases/kzg_verify_cell_kzg_proof_batch.rs b/testing/ef_tests/src/cases/kzg_verify_cell_kzg_proof_batch.rs index 7973af861f..200f439c28 100644 --- a/testing/ef_tests/src/cases/kzg_verify_cell_kzg_proof_batch.rs +++ b/testing/ef_tests/src/cases/kzg_verify_cell_kzg_proof_batch.rs @@ -1,6 +1,6 @@ use super::*; use crate::case_result::compare_result; -use kzg::{Bytes48, Error as KzgError}; +use kzg::Error as KzgError; use serde::Deserialize; use std::marker::PhantomData; @@ -47,8 +47,8 @@ impl<E: EthSpec> Case for KZGVerifyCellKZGProofBatch<E> { let result = parse_input(&self.input).and_then(|(cells, proofs, cell_indices, commitments)| { - let proofs: Vec<Bytes48> = proofs.iter().map(|&proof| proof.into()).collect(); - let commitments: Vec<Bytes48> = commitments.iter().map(|&c| c.into()).collect(); + let proofs = proofs.iter().map(|&proof| proof.0).collect::<Vec<_>>(); + let commitments = commitments.iter().map(|&c| c.0).collect::<Vec<_>>(); let cells = cells.iter().map(|c| c.as_ref()).collect::<Vec<_>>(); let kzg = get_kzg(); match kzg.verify_cell_proof_batch(&cells, &proofs, cell_indices, &commitments) { From 6350a270319267615034475613c7ff3366b941d3 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Wed, 11 Mar 2026 01:20:02 -0500 Subject: [PATCH 73/81] Optionally check DB invariants at runtime (#8952) Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com> --- beacon_node/beacon_chain/src/invariants.rs | 56 ++ beacon_node/beacon_chain/src/lib.rs | 1 + beacon_node/beacon_chain/tests/store_tests.rs | 43 + beacon_node/http_api/src/database.rs | 9 + beacon_node/http_api/src/lib.rs | 14 + beacon_node/store/src/invariants.rs | 781 ++++++++++++++++++ beacon_node/store/src/lib.rs | 1 + beacon_node/store/src/state_cache.rs | 13 + 8 files changed, 918 insertions(+) create mode 100644 beacon_node/beacon_chain/src/invariants.rs create mode 100644 beacon_node/store/src/invariants.rs diff --git a/beacon_node/beacon_chain/src/invariants.rs b/beacon_node/beacon_chain/src/invariants.rs new file mode 100644 index 0000000000..7bcec7b0b4 --- /dev/null +++ b/beacon_node/beacon_chain/src/invariants.rs @@ -0,0 +1,56 @@ +//! Beacon chain database invariant checks. +//! +//! Builds the `InvariantContext` from beacon chain state and delegates all checks +//! to `HotColdDB::check_invariants`. + +use crate::BeaconChain; +use crate::beacon_chain::BeaconChainTypes; +use store::invariants::{InvariantCheckResult, InvariantContext}; + +impl<T: BeaconChainTypes> BeaconChain<T> { + /// Run all database invariant checks. + /// + /// Collects context from fork choice, state cache, custody columns, and pubkey cache, + /// then delegates to the store-level `check_invariants` method. + pub fn check_database_invariants(&self) -> Result<InvariantCheckResult, store::Error> { + let fork_choice_blocks = { + let fc = self.canonical_head.fork_choice_read_lock(); + let proto_array = fc.proto_array().core_proto_array(); + proto_array + .nodes + .iter() + .filter(|node| { + // Only check blocks that are descendants of the finalized checkpoint. + // Pruned non-canonical fork blocks may linger in the proto-array but + // are legitimately absent from the database. + fc.is_finalized_checkpoint_or_descendant(node.root) + }) + .map(|node| (node.root, node.slot)) + .collect() + }; + + let custody_context = self.data_availability_checker.custody_context(); + + let ctx = InvariantContext { + fork_choice_blocks, + state_cache_roots: self.store.state_cache.lock().state_roots(), + custody_columns: custody_context + .custody_columns_for_epoch(None, &self.spec) + .to_vec(), + pubkey_cache_pubkeys: { + let cache = self.validator_pubkey_cache.read(); + (0..cache.len()) + .filter_map(|i| { + cache.get(i).map(|pk| { + use store::StoreItem; + crate::validator_pubkey_cache::DatabasePubkey::from_pubkey(pk) + .as_store_bytes() + }) + }) + .collect() + }, + }; + + self.store.check_invariants(&ctx) + } +} diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 4efd90bd22..29081fd767 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -29,6 +29,7 @@ pub mod fork_choice_signal; pub mod graffiti_calculator; pub mod historical_blocks; pub mod historical_data_columns; +pub mod invariants; pub mod kzg_utils; pub mod light_client_finality_update_verification; pub mod light_client_optimistic_update_verification; diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index b6d729cc61..86f4af3efc 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -148,6 +148,22 @@ fn get_harness_generic( harness } +/// Check that all database invariants hold. +/// +/// Panics with a descriptive message if any invariant is violated. +fn check_db_invariants(harness: &TestHarness) { + let result = harness + .chain + .check_database_invariants() + .expect("invariant check should not error"); + + assert!( + result.is_ok(), + "database invariant violations found:\n{:#?}", + result.violations, + ); +} + fn get_states_descendant_of_block( store: &HotColdDB<E, BeaconNodeBackend<E>, BeaconNodeBackend<E>>, block_root: Hash256, @@ -308,6 +324,7 @@ async fn full_participation_no_skips() { check_split_slot(&harness, store); check_chain_dump(&harness, num_blocks_produced + 1); check_iterators(&harness); + check_db_invariants(&harness); } #[tokio::test] @@ -352,6 +369,7 @@ async fn randomised_skips() { check_split_slot(&harness, store.clone()); check_chain_dump(&harness, num_blocks_produced + 1); check_iterators(&harness); + check_db_invariants(&harness); } #[tokio::test] @@ -400,6 +418,7 @@ async fn long_skip() { check_split_slot(&harness, store); check_chain_dump(&harness, initial_blocks + final_blocks + 1); check_iterators(&harness); + check_db_invariants(&harness); } /// Go forward to the point where the genesis randao value is no longer part of the vector. @@ -1769,6 +1788,8 @@ async fn prunes_abandoned_fork_between_two_finalized_checkpoints() { } assert!(!rig.knows_head(&stray_head)); + + check_db_invariants(&rig); } #[tokio::test] @@ -1897,6 +1918,8 @@ async fn pruning_does_not_touch_abandoned_block_shared_with_canonical_chain() { assert!(!rig.knows_head(&stray_head)); let chain_dump = rig.chain.chain_dump().unwrap(); assert!(get_blocks(&chain_dump).contains(&shared_head)); + + check_db_invariants(&rig); } #[tokio::test] @@ -1988,6 +2011,8 @@ async fn pruning_does_not_touch_blocks_prior_to_finalization() { } rig.assert_knows_head(stray_head.into()); + + check_db_invariants(&rig); } #[tokio::test] @@ -2127,6 +2152,8 @@ async fn prunes_fork_growing_past_youngest_finalized_checkpoint() { } assert!(!rig.knows_head(&stray_head)); + + check_db_invariants(&rig); } // This is to check if state outside of normal block processing are pruned correctly. @@ -2377,6 +2404,8 @@ async fn finalizes_non_epoch_start_slot() { state_hash ); } + + check_db_invariants(&rig); } fn check_all_blocks_exist<'a>( @@ -2643,6 +2672,8 @@ async fn pruning_test( check_all_states_exist(&harness, all_canonical_states.iter()); check_no_states_exist(&harness, stray_states.difference(&all_canonical_states)); check_no_blocks_exist(&harness, stray_blocks.values()); + + check_db_invariants(&harness); } #[tokio::test] @@ -2707,6 +2738,8 @@ async fn garbage_collect_temp_states_from_failed_block_on_finalization() { vec![(genesis_state_root, Slot::new(0))], "get_states_descendant_of_block({bad_block_parent_root:?})" ); + + check_db_invariants(&harness); } #[tokio::test] @@ -3361,6 +3394,16 @@ async fn weak_subjectivity_sync_test( store.clone().reconstruct_historic_states(None).unwrap(); assert_eq!(store.get_anchor_info().anchor_slot, wss_aligned_slot); assert_eq!(store.get_anchor_info().state_upper_limit, Slot::new(0)); + + // Check database invariants after full checkpoint sync + backfill + reconstruction. + let result = beacon_chain + .check_database_invariants() + .expect("invariant check should not error"); + assert!( + result.is_ok(), + "database invariant violations:\n{:#?}", + result.violations, + ); } // This test prunes data columns from epoch 0 and then tries to re-import them via diff --git a/beacon_node/http_api/src/database.rs b/beacon_node/http_api/src/database.rs index 8a50ec45b0..4737d92079 100644 --- a/beacon_node/http_api/src/database.rs +++ b/beacon_node/http_api/src/database.rs @@ -2,6 +2,7 @@ use beacon_chain::store::metadata::CURRENT_SCHEMA_VERSION; use beacon_chain::{BeaconChain, BeaconChainTypes}; use serde::Serialize; use std::sync::Arc; +use store::invariants::InvariantCheckResult; use store::{AnchorInfo, BlobInfo, Split, StoreConfig}; #[derive(Debug, Serialize)] @@ -30,3 +31,11 @@ pub fn info<T: BeaconChainTypes>( blob_info, }) } + +pub fn check_invariants<T: BeaconChainTypes>( + chain: Arc<BeaconChain<T>>, +) -> Result<InvariantCheckResult, warp::Rejection> { + chain.check_database_invariants().map_err(|e| { + warp_utils::reject::custom_bad_request(format!("error checking database invariants: {e:?}")) + }) +} diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 0ef8654d8d..26bad809df 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -3007,6 +3007,19 @@ pub fn serve<T: BeaconChainTypes>( }, ); + // GET lighthouse/database/invariants + let get_lighthouse_database_invariants = database_path + .and(warp::path("invariants")) + .and(warp::path::end()) + .and(task_spawner_filter.clone()) + .and(chain_filter.clone()) + .then( + |task_spawner: TaskSpawner<T::EthSpec>, chain: Arc<BeaconChain<T>>| { + task_spawner + .blocking_json_task(Priority::P1, move || database::check_invariants(chain)) + }, + ); + // POST lighthouse/database/reconstruct let post_lighthouse_database_reconstruct = database_path .and(warp::path("reconstruct")) @@ -3336,6 +3349,7 @@ pub fn serve<T: BeaconChainTypes>( .uor(get_lighthouse_validator_inclusion) .uor(get_lighthouse_staking) .uor(get_lighthouse_database_info) + .uor(get_lighthouse_database_invariants) .uor(get_lighthouse_custody_info) .uor(get_lighthouse_attestation_performance) .uor(get_beacon_light_client_optimistic_update) diff --git a/beacon_node/store/src/invariants.rs b/beacon_node/store/src/invariants.rs new file mode 100644 index 0000000000..eb5232d344 --- /dev/null +++ b/beacon_node/store/src/invariants.rs @@ -0,0 +1,781 @@ +//! Database invariant checks for the hot and cold databases. +//! +//! These checks verify the consistency of data stored in the database. They are designed to be +//! called from the HTTP API and from tests to detect data corruption or bugs in the store logic. +//! +//! See the `check_invariants` and `check_database_invariants` methods for the full list. + +use crate::hdiff::StorageStrategy; +use crate::hot_cold_store::{ColdStateSummary, HotStateSummary}; +use crate::{DBColumn, Error, ItemStore}; +use crate::{HotColdDB, Split}; +use serde::Serialize; +use ssz::Decode; +use std::cmp; +use std::collections::HashSet; +use types::*; + +/// Result of running invariant checks on the database. +#[derive(Debug, Clone, Serialize)] +pub struct InvariantCheckResult { + /// List of invariant violations found. + pub violations: Vec<InvariantViolation>, +} + +impl InvariantCheckResult { + pub fn new() -> Self { + Self { + violations: Vec::new(), + } + } + + pub fn is_ok(&self) -> bool { + self.violations.is_empty() + } + + pub fn add_violation(&mut self, violation: InvariantViolation) { + self.violations.push(violation); + } + + pub fn merge(&mut self, other: InvariantCheckResult) { + self.violations.extend(other.violations); + } +} + +impl Default for InvariantCheckResult { + fn default() -> Self { + Self::new() + } +} + +/// Context data from the beacon chain needed for invariant checks. +/// +/// This allows all invariant checks to live in the store crate while still checking +/// invariants that depend on fork choice, state cache, and custody context. +pub struct InvariantContext { + /// Block roots tracked by fork choice (invariant 1). + pub fork_choice_blocks: Vec<(Hash256, Slot)>, + /// State roots held in the in-memory state cache (invariant 8). + pub state_cache_roots: Vec<Hash256>, + /// Custody columns for the current epoch (invariant 7). + pub custody_columns: Vec<ColumnIndex>, + /// Compressed pubkey bytes from the in-memory validator pubkey cache, indexed by validator index + /// (invariant 9). + pub pubkey_cache_pubkeys: Vec<Vec<u8>>, +} + +/// A single invariant violation. +#[derive(Debug, Clone, Serialize)] +pub enum InvariantViolation { + /// Invariant 1: fork choice block consistency. + /// + /// ```text + /// block in fork_choice && descends_from_finalized -> block in hot_db + /// ``` + ForkChoiceBlockMissing { block_root: Hash256, slot: Slot }, + /// Invariant 2: block and state consistency. + /// + /// ```text + /// block in hot_db && block.slot >= split.slot + /// -> state_summary for block.state_root() in hot_db + /// ``` + HotBlockMissingStateSummary { + block_root: Hash256, + slot: Slot, + state_root: Hash256, + }, + /// Invariant 3: state summary diff consistency. + /// + /// ```text + /// state_summary in hot_db + /// -> state diff/snapshot/nothing in hot_db according to hierarchy rules + /// ``` + HotStateMissingSnapshot { state_root: Hash256, slot: Slot }, + /// Invariant 3: state summary diff consistency (missing diff). + /// + /// ```text + /// state_summary in hot_db + /// -> state diff/snapshot/nothing in hot_db according to hierarchy rules + /// ``` + HotStateMissingDiff { state_root: Hash256, slot: Slot }, + /// Invariant 3: DiffFrom/ReplayFrom base slot must reference an existing summary. + /// + /// ```text + /// state_summary in hot_db + /// -> state diff/snapshot/nothing in hot_db according to hierarchy rules + /// ``` + HotStateBaseSummaryMissing { + slot: Slot, + base_state_root: Hash256, + }, + /// Invariant 4: state summary chain consistency. + /// + /// ```text + /// state_summary in hot_db && state_summary.slot > split.slot + /// -> state_summary for previous_state_root in hot_db + /// ``` + HotStateMissingPreviousSummary { + slot: Slot, + previous_state_root: Hash256, + }, + /// Invariant 5: block and execution payload consistency. + /// + /// ```text + /// block in hot_db && !prune_payloads -> payload for block.root in hot_db + /// ``` + ExecutionPayloadMissing { block_root: Hash256, slot: Slot }, + /// Invariant 6: block and blobs consistency. + /// + /// ```text + /// block in hot_db && num_blob_commitments > 0 + /// -> blob_list for block.root in hot_db + /// ``` + BlobSidecarMissing { block_root: Hash256, slot: Slot }, + /// Invariant 7: block and data columns consistency. + /// + /// ```text + /// block in hot_db && num_blob_commitments > 0 + /// && block.slot >= earliest_available_slot + /// && data_column_idx in custody_columns + /// -> (block_root, data_column_idx) in hot_db + /// ``` + DataColumnMissing { + block_root: Hash256, + slot: Slot, + column_index: ColumnIndex, + }, + /// Invariant 8: state cache and disk consistency. + /// + /// ```text + /// state in state_cache -> state_summary in hot_db + /// ``` + StateCacheMissingSummary { state_root: Hash256 }, + /// Invariant 9: pubkey cache consistency. + /// + /// ```text + /// state_summary in hot_db + /// -> all validator pubkeys from state.validators are in the hot_db + /// ``` + PubkeyCacheMissing { validator_index: usize }, + /// Invariant 9b: pubkey cache value mismatch. + /// + /// ```text + /// pubkey_cache[i] == hot_db(PubkeyCache)[i] + /// ``` + PubkeyCacheMismatch { validator_index: usize }, + /// Invariant 10: block root indices mapping. + /// + /// ```text + /// oldest_block_slot <= i < split.slot + /// -> block_root for slot i in cold_db + /// && block for block_root in hot_db + /// ``` + ColdBlockRootMissing { + slot: Slot, + oldest_block_slot: Slot, + split_slot: Slot, + }, + /// Invariant 10: block root index references a block that must exist. + /// + /// ```text + /// oldest_block_slot <= i < split.slot + /// -> block_root for slot i in cold_db + /// && block for block_root in hot_db + /// ``` + ColdBlockRootOrphan { slot: Slot, block_root: Hash256 }, + /// Invariant 11: state root indices mapping. + /// + /// ```text + /// (i <= state_lower_limit || i >= min(split.slot, state_upper_limit)) && i < split.slot + /// -> i |-> state_root in cold_db(BeaconStateRoots) + /// && state_root |-> cold_state_summary in cold_db(BeaconColdStateSummary) + /// && cold_state_summary.slot == i + /// ``` + ColdStateRootMissing { + slot: Slot, + state_lower_limit: Slot, + state_upper_limit: Slot, + split_slot: Slot, + }, + /// Invariant 11: state root index must have a cold state summary. + /// + /// ```text + /// (i <= state_lower_limit || i >= min(split.slot, state_upper_limit)) && i < split.slot + /// -> i |-> state_root in cold_db(BeaconStateRoots) + /// && state_root |-> cold_state_summary in cold_db(BeaconColdStateSummary) + /// && cold_state_summary.slot == i + /// ``` + ColdStateRootMissingSummary { slot: Slot, state_root: Hash256 }, + /// Invariant 11: cold state summary slot must match index slot. + /// + /// ```text + /// (i <= state_lower_limit || i >= min(split.slot, state_upper_limit)) && i < split.slot + /// -> i |-> state_root in cold_db(BeaconStateRoots) + /// && state_root |-> cold_state_summary in cold_db(BeaconColdStateSummary) + /// && cold_state_summary.slot == i + /// ``` + ColdStateRootSlotMismatch { + slot: Slot, + state_root: Hash256, + summary_slot: Slot, + }, + /// Invariant 12: cold state diff consistency. + /// + /// ```text + /// cold_state_summary in cold_db + /// -> slot |-> state diff/snapshot/nothing in cold_db according to diff hierarchy + /// ``` + ColdStateMissingSnapshot { state_root: Hash256, slot: Slot }, + /// Invariant 12: cold state diff consistency (missing diff). + /// + /// ```text + /// cold_state_summary in cold_db + /// -> slot |-> state diff/snapshot/nothing in cold_db according to diff hierarchy + /// ``` + ColdStateMissingDiff { state_root: Hash256, slot: Slot }, + /// Invariant 12: DiffFrom/ReplayFrom base slot must reference an existing summary. + /// + /// ```text + /// cold_state_summary in cold_db + /// -> slot |-> state diff/snapshot/nothing in cold_db according to diff hierarchy + /// ``` + ColdStateBaseSummaryMissing { slot: Slot, base_slot: Slot }, +} + +impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold> { + /// Run all database invariant checks. + /// + /// The `ctx` parameter provides data from the beacon chain layer (fork choice, state cache, + /// custody columns, pubkey cache) so that all invariant checks can live in this single file. + pub fn check_invariants(&self, ctx: &InvariantContext) -> Result<InvariantCheckResult, Error> { + let mut result = InvariantCheckResult::new(); + let split = self.get_split_info(); + + result.merge(self.check_fork_choice_block_consistency(ctx)?); + result.merge(self.check_hot_block_invariants(&split, ctx)?); + result.merge(self.check_hot_state_summary_diff_consistency()?); + result.merge(self.check_hot_state_summary_chain_consistency(&split)?); + result.merge(self.check_state_cache_consistency(ctx)?); + result.merge(self.check_cold_block_root_indices(&split)?); + result.merge(self.check_cold_state_root_indices(&split)?); + result.merge(self.check_cold_state_diff_consistency()?); + result.merge(self.check_pubkey_cache_consistency(ctx)?); + + Ok(result) + } + + /// Invariant 1 (Hot DB): Fork choice block consistency. + /// + /// ```text + /// block in fork_choice && descends_from_finalized -> block in hot_db + /// ``` + /// + /// Every canonical fork choice block (descending from finalized) must exist in the hot + /// database. Pruned non-canonical fork blocks may linger in the proto-array and are + /// excluded from this check. + fn check_fork_choice_block_consistency( + &self, + ctx: &InvariantContext, + ) -> Result<InvariantCheckResult, Error> { + let mut result = InvariantCheckResult::new(); + + for &(block_root, slot) in &ctx.fork_choice_blocks { + let exists = self + .hot_db + .key_exists(DBColumn::BeaconBlock, block_root.as_slice())?; + if !exists { + result + .add_violation(InvariantViolation::ForkChoiceBlockMissing { block_root, slot }); + } + } + + Ok(result) + } + + /// Invariants 2, 5, 6, 7 (Hot DB): Block-related consistency checks. + /// + /// Iterates hot DB blocks once and checks: + /// - Invariant 2: block-state summary consistency + /// - Invariant 5: execution payload consistency (when prune_payloads=false) + /// - Invariant 6: blob sidecar consistency (Deneb to Fulu) + /// - Invariant 7: data column consistency (post-Fulu, when custody_columns provided) + fn check_hot_block_invariants( + &self, + split: &Split, + ctx: &InvariantContext, + ) -> Result<InvariantCheckResult, Error> { + let mut result = InvariantCheckResult::new(); + + let check_payloads = !self.get_config().prune_payloads; + let bellatrix_fork_slot = self + .spec + .bellatrix_fork_epoch + .map(|epoch| epoch.start_slot(E::slots_per_epoch())); + let deneb_fork_slot = self + .spec + .deneb_fork_epoch + .map(|epoch| epoch.start_slot(E::slots_per_epoch())); + let fulu_fork_slot = self + .spec + .fulu_fork_epoch + .map(|epoch| epoch.start_slot(E::slots_per_epoch())); + let oldest_blob_slot = self.get_blob_info().oldest_blob_slot; + let oldest_data_column_slot = self.get_data_column_info().oldest_data_column_slot; + + for res in self.hot_db.iter_column::<Hash256>(DBColumn::BeaconBlock) { + let (block_root, block_bytes) = res?; + let block = SignedBlindedBeaconBlock::<E>::from_ssz_bytes(&block_bytes, &self.spec)?; + let slot = block.slot(); + + // Invariant 2: block-state consistency. + if slot >= split.slot { + let state_root = block.state_root(); + let has_summary = self + .hot_db + .key_exists(DBColumn::BeaconStateHotSummary, state_root.as_slice())?; + if !has_summary { + result.add_violation(InvariantViolation::HotBlockMissingStateSummary { + block_root, + slot, + state_root, + }); + } + } + + // Invariant 5: execution payload consistency. + // TODO(gloas): reconsider this invariant + if check_payloads + && let Some(bellatrix_slot) = bellatrix_fork_slot + && slot >= bellatrix_slot + && !self.execution_payload_exists(&block_root)? + && !self.payload_envelope_exists(&block_root)? + { + result.add_violation(InvariantViolation::ExecutionPayloadMissing { + block_root, + slot, + }); + } + + // Invariant 6: blob sidecar consistency. + // Only check blocks that actually have blob KZG commitments — blocks with 0 + // commitments legitimately have no blob sidecars stored. + if let Some(deneb_slot) = deneb_fork_slot + && let Some(oldest_blob) = oldest_blob_slot + && slot >= deneb_slot + && slot >= oldest_blob + && fulu_fork_slot.is_none_or(|fulu_slot| slot < fulu_slot) + && block.num_expected_blobs() > 0 + { + let has_blob = self + .blobs_db + .key_exists(DBColumn::BeaconBlob, block_root.as_slice())?; + if !has_blob { + result + .add_violation(InvariantViolation::BlobSidecarMissing { block_root, slot }); + } + } + + // Invariant 7: data column consistency. + // Only check blocks that actually have blob KZG commitments. + // TODO(gloas): reconsider this invariant — non-canonical payloads won't have + // their data column sidecars stored. + if !ctx.custody_columns.is_empty() + && let Some(fulu_slot) = fulu_fork_slot + && let Some(oldest_dc) = oldest_data_column_slot + && slot >= fulu_slot + && slot >= oldest_dc + && block.num_expected_blobs() > 0 + { + let stored_columns = self.get_data_column_keys(block_root)?; + for col_idx in &ctx.custody_columns { + if !stored_columns.contains(col_idx) { + result.add_violation(InvariantViolation::DataColumnMissing { + block_root, + slot, + column_index: *col_idx, + }); + } + } + } + } + + Ok(result) + } + + /// Invariant 3 (Hot DB): State summary diff/snapshot consistency. + /// + /// ```text + /// state_summary in hot_db + /// -> state diff/snapshot/nothing in hot_db per HDiff hierarchy rules + /// ``` + /// + /// Each hot state summary should have the correct storage artifact (snapshot, diff, or + /// nothing) according to the HDiff hierarchy configuration. The hierarchy uses the + /// anchor_slot as its start point for the hot DB. + fn check_hot_state_summary_diff_consistency(&self) -> Result<InvariantCheckResult, Error> { + let mut result = InvariantCheckResult::new(); + + let anchor_slot = self.get_anchor_info().anchor_slot; + + // Collect all summary slots and their strategies in a first pass. + let mut known_state_roots = HashSet::new(); + let mut base_state_refs: Vec<(Slot, Hash256)> = Vec::new(); + + for res in self + .hot_db + .iter_column::<Hash256>(DBColumn::BeaconStateHotSummary) + { + let (state_root, value) = res?; + let summary = HotStateSummary::from_ssz_bytes(&value)?; + + known_state_roots.insert(state_root); + + match self.hierarchy.storage_strategy(summary.slot, anchor_slot)? { + StorageStrategy::Snapshot => { + let has_snapshot = self + .hot_db + .key_exists(DBColumn::BeaconStateHotSnapshot, state_root.as_slice())?; + if !has_snapshot { + result.add_violation(InvariantViolation::HotStateMissingSnapshot { + state_root, + slot: summary.slot, + }); + } + } + StorageStrategy::DiffFrom(base_slot) => { + let has_diff = self + .hot_db + .key_exists(DBColumn::BeaconStateHotDiff, state_root.as_slice())?; + if !has_diff { + result.add_violation(InvariantViolation::HotStateMissingDiff { + state_root, + slot: summary.slot, + }); + } + if let Ok(base_root) = summary.diff_base_state.get_root(base_slot) { + base_state_refs.push((summary.slot, base_root)); + } + } + StorageStrategy::ReplayFrom(base_slot) => { + if let Ok(base_root) = summary.diff_base_state.get_root(base_slot) { + base_state_refs.push((summary.slot, base_root)); + } + } + } + } + + // Verify that all diff base state roots reference existing summaries. + for (slot, base_state_root) in base_state_refs { + if !known_state_roots.contains(&base_state_root) { + result.add_violation(InvariantViolation::HotStateBaseSummaryMissing { + slot, + base_state_root, + }); + } + } + + Ok(result) + } + + /// Invariant 4 (Hot DB): State summary chain consistency. + /// + /// ```text + /// state_summary in hot_db && state_summary.slot > split.slot + /// -> state_summary for previous_state_root in hot_db + /// ``` + /// + /// The chain of `previous_state_root` links must be continuous back to the split state. + /// The split state itself is the boundary and does not need a predecessor in the hot DB. + fn check_hot_state_summary_chain_consistency( + &self, + split: &Split, + ) -> Result<InvariantCheckResult, Error> { + let mut result = InvariantCheckResult::new(); + + for res in self + .hot_db + .iter_column::<Hash256>(DBColumn::BeaconStateHotSummary) + { + let (_state_root, value) = res?; + let summary = HotStateSummary::from_ssz_bytes(&value)?; + + if summary.slot > split.slot { + let prev_root = summary.previous_state_root; + let has_prev = self + .hot_db + .key_exists(DBColumn::BeaconStateHotSummary, prev_root.as_slice())?; + if !has_prev { + result.add_violation(InvariantViolation::HotStateMissingPreviousSummary { + slot: summary.slot, + previous_state_root: prev_root, + }); + } + } + } + + Ok(result) + } + + /// Invariant 8 (Hot DB): State cache and disk consistency. + /// + /// ```text + /// state in state_cache -> state_summary in hot_db + /// ``` + /// + /// Every state held in the in-memory state cache (including the finalized state) should + /// have a corresponding hot state summary on disk. + fn check_state_cache_consistency( + &self, + ctx: &InvariantContext, + ) -> Result<InvariantCheckResult, Error> { + let mut result = InvariantCheckResult::new(); + + for &state_root in &ctx.state_cache_roots { + let has_summary = self + .hot_db + .key_exists(DBColumn::BeaconStateHotSummary, state_root.as_slice())?; + if !has_summary { + result.add_violation(InvariantViolation::StateCacheMissingSummary { state_root }); + } + } + + Ok(result) + } + + /// Invariant 10 (Cold DB): Block root indices. + /// + /// ```text + /// oldest_block_slot <= i < split.slot + /// -> block_root for slot i in cold_db + /// && block for block_root in hot_db + /// ``` + /// + /// Every slot in the cold range (from `oldest_block_slot` to `split.slot`) should have a + /// block root index entry, and the referenced block should exist in the hot DB. Note that + /// skip slots store the most recent non-skipped block's root, so `block.slot()` may differ + /// from the index slot. + fn check_cold_block_root_indices(&self, split: &Split) -> Result<InvariantCheckResult, Error> { + let mut result = InvariantCheckResult::new(); + + let anchor_info = self.get_anchor_info(); + + if anchor_info.oldest_block_slot >= split.slot { + return Ok(result); + } + + for slot_val in anchor_info.oldest_block_slot.as_u64()..split.slot.as_u64() { + let slot = Slot::new(slot_val); + + let slot_bytes = slot_val.to_be_bytes(); + let block_root_bytes = self + .cold_db + .get_bytes(DBColumn::BeaconBlockRoots, &slot_bytes)?; + + let Some(root_bytes) = block_root_bytes else { + result.add_violation(InvariantViolation::ColdBlockRootMissing { + slot, + oldest_block_slot: anchor_info.oldest_block_slot, + split_slot: split.slot, + }); + continue; + }; + + if root_bytes.len() != 32 { + return Err(Error::InvalidKey(format!( + "cold block root at slot {slot} has invalid length {}", + root_bytes.len() + ))); + } + + let block_root = Hash256::from_slice(&root_bytes); + let block_exists = self + .hot_db + .key_exists(DBColumn::BeaconBlock, block_root.as_slice())?; + if !block_exists { + result.add_violation(InvariantViolation::ColdBlockRootOrphan { slot, block_root }); + } + } + + Ok(result) + } + + /// Invariant 11 (Cold DB): State root indices. + /// + /// ```text + /// (i <= state_lower_limit || i >= min(split.slot, state_upper_limit)) && i < split.slot + /// -> i |-> state_root in cold_db(BeaconStateRoots) + /// && state_root |-> cold_state_summary in cold_db(BeaconColdStateSummary) + /// && cold_state_summary.slot == i + /// ``` + fn check_cold_state_root_indices(&self, split: &Split) -> Result<InvariantCheckResult, Error> { + let mut result = InvariantCheckResult::new(); + + let anchor_info = self.get_anchor_info(); + + // Expected slots are: (i <= state_lower_limit || i >= effective_upper) && i < split.slot + // where effective_upper = min(split.slot, state_upper_limit). + for slot_val in 0..split.slot.as_u64() { + let slot = Slot::new(slot_val); + + if slot <= anchor_info.state_lower_limit + || slot >= cmp::min(split.slot, anchor_info.state_upper_limit) + { + let slot_bytes = slot_val.to_be_bytes(); + let Some(root_bytes) = self + .cold_db + .get_bytes(DBColumn::BeaconStateRoots, &slot_bytes)? + else { + result.add_violation(InvariantViolation::ColdStateRootMissing { + slot, + state_lower_limit: anchor_info.state_lower_limit, + state_upper_limit: anchor_info.state_upper_limit, + split_slot: split.slot, + }); + continue; + }; + + if root_bytes.len() != 32 { + return Err(Error::InvalidKey(format!( + "cold state root at slot {slot} has invalid length {}", + root_bytes.len() + ))); + } + + let state_root = Hash256::from_slice(&root_bytes); + + match self + .cold_db + .get_bytes(DBColumn::BeaconColdStateSummary, state_root.as_slice())? + { + None => { + result.add_violation(InvariantViolation::ColdStateRootMissingSummary { + slot, + state_root, + }); + } + Some(summary_bytes) => { + let summary = ColdStateSummary::from_ssz_bytes(&summary_bytes)?; + if summary.slot != slot { + result.add_violation(InvariantViolation::ColdStateRootSlotMismatch { + slot, + state_root, + summary_slot: summary.slot, + }); + } + } + } + } + } + + Ok(result) + } + + /// Invariant 12 (Cold DB): Cold state diff/snapshot consistency. + /// + /// ```text + /// cold_state_summary in cold_db + /// -> state diff/snapshot/nothing in cold_db per HDiff hierarchy rules + /// ``` + /// + /// Each cold state summary should have the correct storage artifact according to the + /// HDiff hierarchy. Cold states always use genesis (slot 0) as the hierarchy start since + /// they are finalized and have no anchor_slot dependency. + fn check_cold_state_diff_consistency(&self) -> Result<InvariantCheckResult, Error> { + let mut result = InvariantCheckResult::new(); + + let mut summary_slots = HashSet::new(); + let mut base_slot_refs = Vec::new(); + + for res in self + .cold_db + .iter_column::<Hash256>(DBColumn::BeaconColdStateSummary) + { + let (state_root, value) = res?; + let summary = ColdStateSummary::from_ssz_bytes(&value)?; + + summary_slots.insert(summary.slot); + + let slot_bytes = summary.slot.as_u64().to_be_bytes(); + + match self + .hierarchy + .storage_strategy(summary.slot, Slot::new(0))? + { + StorageStrategy::Snapshot => { + let has_snapshot = self + .cold_db + .key_exists(DBColumn::BeaconStateSnapshot, &slot_bytes)?; + if !has_snapshot { + result.add_violation(InvariantViolation::ColdStateMissingSnapshot { + state_root, + slot: summary.slot, + }); + } + } + StorageStrategy::DiffFrom(base_slot) => { + let has_diff = self + .cold_db + .key_exists(DBColumn::BeaconStateDiff, &slot_bytes)?; + if !has_diff { + result.add_violation(InvariantViolation::ColdStateMissingDiff { + state_root, + slot: summary.slot, + }); + } + base_slot_refs.push((summary.slot, base_slot)); + } + StorageStrategy::ReplayFrom(base_slot) => { + base_slot_refs.push((summary.slot, base_slot)); + } + } + } + + // Verify that all DiffFrom/ReplayFrom base slots reference existing summaries. + for (slot, base_slot) in base_slot_refs { + if !summary_slots.contains(&base_slot) { + result.add_violation(InvariantViolation::ColdStateBaseSummaryMissing { + slot, + base_slot, + }); + } + } + + Ok(result) + } + + /// Invariant 9 (Hot DB): Pubkey cache consistency. + /// + /// ```text + /// all validator pubkeys from states are in hot_db(PubkeyCache) + /// ``` + /// + /// Checks that the in-memory pubkey cache and the on-disk PubkeyCache column have the same + /// number of entries AND that each pubkey matches at every validator index. + fn check_pubkey_cache_consistency( + &self, + ctx: &InvariantContext, + ) -> Result<InvariantCheckResult, Error> { + let mut result = InvariantCheckResult::new(); + + // Read on-disk pubkeys by sequential validator index (matching how they are stored + // with Hash256::from_low_u64_be(index) as key). + // Iterate in-memory pubkeys and verify each matches on disk. + for (validator_index, in_memory_bytes) in ctx.pubkey_cache_pubkeys.iter().enumerate() { + let mut key = [0u8; 32]; + key[24..].copy_from_slice(&(validator_index as u64).to_be_bytes()); + match self.hot_db.get_bytes(DBColumn::PubkeyCache, &key)? { + Some(on_disk_bytes) if in_memory_bytes != &on_disk_bytes => { + result + .add_violation(InvariantViolation::PubkeyCacheMismatch { validator_index }); + } + None => { + result + .add_violation(InvariantViolation::PubkeyCacheMissing { validator_index }); + } + _ => {} + } + } + + Ok(result) + } +} diff --git a/beacon_node/store/src/lib.rs b/beacon_node/store/src/lib.rs index 3363eb800c..bfa1200602 100644 --- a/beacon_node/store/src/lib.rs +++ b/beacon_node/store/src/lib.rs @@ -15,6 +15,7 @@ pub mod hdiff; pub mod historic_state_cache; pub mod hot_cold_store; mod impls; +pub mod invariants; mod memory_store; pub mod metadata; pub mod metrics; diff --git a/beacon_node/store/src/state_cache.rs b/beacon_node/store/src/state_cache.rs index 4b0d1ee016..6d159c9361 100644 --- a/beacon_node/store/src/state_cache.rs +++ b/beacon_node/store/src/state_cache.rs @@ -111,6 +111,19 @@ impl<E: EthSpec> StateCache<E> { self.hdiff_buffers.mem_usage() } + /// Return all state roots currently held in the cache, including the finalized state. + pub fn state_roots(&self) -> Vec<Hash256> { + let mut roots: Vec<Hash256> = self + .states + .iter() + .map(|(&state_root, _)| state_root) + .collect(); + if let Some(ref finalized) = self.finalized_state { + roots.push(finalized.state_root); + } + roots + } + pub fn update_finalized_state( &mut self, state_root: Hash256, From bff72a920da50a2abefa44b75c98b9597200ee8a Mon Sep 17 00:00:00 2001 From: Michael Sproul <michaelsproul@users.noreply.github.com> Date: Thu, 12 Mar 2026 10:06:25 +1100 Subject: [PATCH 74/81] Update database and block replayer to handle payload envelopes (#8886) Closes: - https://github.com/sigp/lighthouse/issues/8869 - Update `BlockReplayer` to support replay of execution payload envelopes. - Update `HotColdDB` to load payload envelopes and feed them to the `BlockReplayer` for both hot + cold states. However the cold DB code is not fully working yet (see: https://github.com/sigp/lighthouse/issues/8958). - Add `StatePayloadStatus` to allow callers to specify whether they want a state with a payload applied, or not. - Fix the state cache to key by `StatePayloadStatus`. - Lots of fixes to block production and block processing regarding state management. - Initial test harness support for producing+processing Gloas blocks+envelopes - A few new tests to cover Gloas DB operations Co-Authored-By: Eitan Seri- Levi <eserilev@gmail.com> Co-Authored-By: Eitan Seri-Levi <eserilev@ucsc.edu> Co-Authored-By: Michael Sproul <michael@sigmaprime.io> Co-Authored-By: Michael Sproul <michaelsproul@users.noreply.github.com> Co-Authored-By: Jimmy Chen <jchen.tc@gmail.com> --- beacon_node/beacon_chain/src/beacon_chain.rs | 27 +- .../beacon_chain/src/blob_verification.rs | 10 +- .../src/block_production/gloas.rs | 15 +- .../beacon_chain/src/block_production/mod.rs | 41 +- .../beacon_chain/src/block_verification.rs | 37 +- beacon_node/beacon_chain/src/builder.rs | 12 +- .../beacon_chain/src/canonical_head.rs | 13 +- .../src/data_column_verification.rs | 11 +- .../src/schema_change/migration_schema_v24.rs | 2 + .../beacon_chain/src/state_advance_timer.rs | 14 +- beacon_node/beacon_chain/src/test_utils.rs | 164 ++++++- beacon_node/beacon_chain/tests/rewards.rs | 3 +- beacon_node/beacon_chain/tests/store_tests.rs | 443 +++++++++++++++++- .../test_utils/execution_block_generator.rs | 10 +- .../http_api/src/attestation_performance.rs | 3 +- .../http_api/src/block_packing_efficiency.rs | 3 +- beacon_node/http_api/src/produce_block.rs | 2 +- .../http_api/src/sync_committee_rewards.rs | 3 +- beacon_node/store/src/hdiff.rs | 6 + beacon_node/store/src/hot_cold_store.rs | 252 ++++++++-- beacon_node/store/src/reconstruct.rs | 1 + beacon_node/store/src/state_cache.rs | 43 +- .../state_processing/src/block_replayer.rs | 136 +++++- .../src/envelope_processing.rs | 2 - .../src/per_block_processing/tests.rs | 2 +- .../state_processing/src/state_advance.rs | 5 + .../types/src/block/signed_beacon_block.rs | 27 ++ consensus/types/src/execution/mod.rs | 2 + .../src/execution/state_payload_status.rs | 18 + consensus/types/src/state/beacon_state.rs | 20 +- 30 files changed, 1243 insertions(+), 84 deletions(-) create mode 100644 consensus/types/src/execution/state_payload_status.rs diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 07f3bb01fa..ab2097e001 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -2031,9 +2031,16 @@ impl<T: BeaconChainTypes> BeaconChain<T> { // required information. (justified_checkpoint, committee_len) } else { + // We assume that the `Pending` state has the same shufflings as a `Full` state + // for the same block. Analysis: https://hackmd.io/@dapplion/gloas_dependant_root let (advanced_state_root, mut state) = self .store - .get_advanced_hot_state(beacon_block_root, request_slot, beacon_state_root)? + .get_advanced_hot_state( + beacon_block_root, + StatePayloadStatus::Pending, + request_slot, + beacon_state_root, + )? .ok_or(Error::MissingBeaconState(beacon_state_root))?; if state.current_epoch() < request_epoch { partial_state_advance( @@ -4662,12 +4669,19 @@ impl<T: BeaconChainTypes> BeaconChain<T> { if cached_head.head_block_root() == parent_block_root { (Cow::Borrowed(head_state), cached_head.head_state_root()) } else { + // TODO(gloas): this function needs updating to be envelope-aware + // See: https://github.com/sigp/lighthouse/issues/8957 let block = self .get_blinded_block(&parent_block_root)? .ok_or(Error::MissingBeaconBlock(parent_block_root))?; let (state_root, state) = self .store - .get_advanced_hot_state(parent_block_root, proposal_slot, block.state_root())? + .get_advanced_hot_state( + parent_block_root, + StatePayloadStatus::Pending, + proposal_slot, + block.state_root(), + )? .ok_or(Error::MissingBeaconState(block.state_root()))?; (Cow::Owned(state), state_root) }; @@ -6599,9 +6613,16 @@ impl<T: BeaconChainTypes> BeaconChain<T> { let (mut state, state_root) = if let Some((state, state_root)) = head_state_opt { (state, state_root) } else { + // We assume that the `Pending` state has the same shufflings as a `Full` state + // for the same block. Analysis: https://hackmd.io/@dapplion/gloas_dependant_root let (state_root, state) = self .store - .get_advanced_hot_state(head_block_root, target_slot, head_block.state_root)? + .get_advanced_hot_state( + head_block_root, + StatePayloadStatus::Pending, + target_slot, + head_block.state_root, + )? .ok_or(Error::MissingBeaconState(head_block.state_root))?; (state, state_root) }; diff --git a/beacon_node/beacon_chain/src/blob_verification.rs b/beacon_node/beacon_chain/src/blob_verification.rs index fe111628db..86b385d818 100644 --- a/beacon_node/beacon_chain/src/blob_verification.rs +++ b/beacon_node/beacon_chain/src/blob_verification.rs @@ -20,6 +20,7 @@ use tree_hash::TreeHash; use types::data::BlobIdentifier; use types::{ BeaconStateError, BlobSidecar, Epoch, EthSpec, Hash256, SignedBeaconBlockHeader, Slot, + StatePayloadStatus, }; /// An error occurred while validating a gossip blob. @@ -508,9 +509,16 @@ pub fn validate_blob_sidecar_for_gossip<T: BeaconChainTypes, O: ObservationStrat index = %blob_index, "Proposer shuffling cache miss for blob verification" ); + // Blob verification is only relevant pre-Fulu and pre-Gloas, so `Pending` payload + // status is sufficient. chain .store - .get_advanced_hot_state(block_parent_root, blob_slot, parent_block.state_root) + .get_advanced_hot_state( + block_parent_root, + StatePayloadStatus::Pending, + blob_slot, + parent_block.state_root, + ) .map_err(|e| GossipBlobError::BeaconChainError(Box::new(e.into())))? .ok_or_else(|| { GossipBlobError::BeaconChainError(Box::new(BeaconChainError::DBInconsistent( diff --git a/beacon_node/beacon_chain/src/block_production/gloas.rs b/beacon_node/beacon_chain/src/block_production/gloas.rs index 607090c59d..5d7d99b5bd 100644 --- a/beacon_node/beacon_chain/src/block_production/gloas.rs +++ b/beacon_node/beacon_chain/src/block_production/gloas.rs @@ -41,7 +41,7 @@ pub const BID_VALUE_SELF_BUILD: u64 = 0; pub const EXECUTION_PAYMENT_TRUSTLESS_BUILD: u64 = 0; type ConsensusBlockValue = u64; -type BlockProductionResult<E> = (BeaconBlock<E, FullPayload<E>>, ConsensusBlockValue); +type BlockProductionResult<E> = (BeaconBlock<E>, BeaconState<E>, ConsensusBlockValue); pub type PreparePayloadResult<E> = Result<BlockProposalContentsGloas<E>, BlockProductionError>; pub type PreparePayloadHandle<E> = JoinHandle<Option<PreparePayloadResult<E>>>; @@ -425,6 +425,12 @@ impl<T: BeaconChainTypes> BeaconChain<T> { )) } + /// Complete a block by computing its state root, and + /// + /// Return `(block, pending_state, block_value)` where: + /// + /// - `pending_state` is the state post block application (prior to payload application) + /// - `block_value` is the consensus-layer rewards for `block` #[allow(clippy::type_complexity)] fn complete_partial_beacon_block_gloas( &self, @@ -433,7 +439,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> { payload_data: Option<ExecutionPayloadData<T::EthSpec>>, mut state: BeaconState<T::EthSpec>, verification: ProduceBlockVerification, - ) -> Result<(BeaconBlock<T::EthSpec, FullPayload<T::EthSpec>>, u64), BlockProductionError> { + ) -> Result<BlockProductionResult<T::EthSpec>, BlockProductionError> { let PartialBeaconBlock { slot, proposer_index, @@ -545,6 +551,9 @@ impl<T: BeaconChainTypes> BeaconChain<T> { drop(state_root_timer); + // Clone the Pending state (post-block, pre-envelope) for callers that need it. + let pending_state = state.clone(); + let (mut block, _) = signed_beacon_block.deconstruct(); *block.state_root_mut() = state_root; @@ -605,7 +614,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> { "Produced beacon block" ); - Ok((block, consensus_block_value)) + Ok((block, pending_state, consensus_block_value)) } // TODO(gloas) introduce `ProposerPreferences` so we can build out trustless diff --git a/beacon_node/beacon_chain/src/block_production/mod.rs b/beacon_node/beacon_chain/src/block_production/mod.rs index 76c8b77e93..b33323f527 100644 --- a/beacon_node/beacon_chain/src/block_production/mod.rs +++ b/beacon_node/beacon_chain/src/block_production/mod.rs @@ -3,7 +3,7 @@ use std::{sync::Arc, time::Duration}; use proto_array::ProposerHeadError; use slot_clock::SlotClock; use tracing::{debug, error, info, instrument, warn}; -use types::{BeaconState, Hash256, Slot}; +use types::{BeaconState, Hash256, Slot, StatePayloadStatus}; use crate::{ BeaconChain, BeaconChainTypes, BlockProductionError, StateSkipConfig, @@ -37,8 +37,14 @@ impl<T: BeaconChainTypes> BeaconChain<T> { }; let (state, state_root_opt) = if head_slot < slot { // Attempt an aggressive re-org if configured and the conditions are right. - if let Some((re_org_state, re_org_state_root)) = - self.get_state_for_re_org(slot, head_slot, head_block_root) + // TODO(gloas): re-enable reorgs + let gloas_enabled = self + .spec + .fork_name_at_slot::<T::EthSpec>(slot) + .gloas_enabled(); + if !gloas_enabled + && let Some((re_org_state, re_org_state_root)) = + self.get_state_for_re_org(slot, head_slot, head_block_root) { info!( %slot, @@ -49,9 +55,30 @@ impl<T: BeaconChainTypes> BeaconChain<T> { } else { // Fetch the head state advanced through to `slot`, which should be present in the // state cache thanks to the state advance timer. + // TODO(gloas): need to fix this once fork choice understands payloads + // for now we just use the existence of the head's payload envelope to determine + // whether we should build atop it + let (payload_status, parent_state_root) = if gloas_enabled + && let Ok(Some(envelope)) = self.store.get_payload_envelope(&head_block_root) + { + debug!( + %slot, + parent_state_root = ?envelope.message.state_root, + parent_block_root = ?head_block_root, + "Building Gloas block on full state" + ); + (StatePayloadStatus::Full, envelope.message.state_root) + } else { + (StatePayloadStatus::Pending, head_state_root) + }; let (state_root, state) = self .store - .get_advanced_hot_state(head_block_root, slot, head_state_root) + .get_advanced_hot_state( + head_block_root, + payload_status, + slot, + parent_state_root, + ) .map_err(BlockProductionError::FailedToLoadState)? .ok_or(BlockProductionError::UnableToProduceAtSlot(slot))?; (state, Some(state_root)) @@ -204,7 +231,11 @@ impl<T: BeaconChainTypes> BeaconChain<T> { let (state_root, state) = self .store - .get_advanced_hot_state_from_cache(re_org_parent_block, slot) + .get_advanced_hot_state_from_cache( + re_org_parent_block, + StatePayloadStatus::Pending, + slot, + ) .or_else(|| { warn!(reason = "no state in cache", "Not attempting re-org"); None diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index b748bf5c6c..1be9bd4181 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -99,7 +99,8 @@ use tracing::{Instrument, Span, debug, debug_span, error, info_span, instrument} use types::{ BeaconBlockRef, BeaconState, BeaconStateError, BlobsList, ChainSpec, DataColumnSidecarList, Epoch, EthSpec, FullPayload, Hash256, InconsistentFork, KzgProofs, RelativeEpoch, - SignedBeaconBlock, SignedBeaconBlockHeader, Slot, data::DataColumnSidecarError, + SignedBeaconBlock, SignedBeaconBlockHeader, Slot, StatePayloadStatus, + data::DataColumnSidecarError, }; /// Maximum block slot number. Block with slots bigger than this constant will NOT be processed. @@ -1491,7 +1492,11 @@ impl<T: BeaconChainTypes> ExecutionPendingBlock<T> { let distance = block.slot().as_u64().saturating_sub(state.slot().as_u64()); for _ in 0..distance { - let state_root = if parent.beacon_block.slot() == state.slot() { + // TODO(gloas): could do a similar optimisation here for Full blocks if we have access + // to the parent envelope and its `state_root`. + let state_root = if parent.beacon_block.slot() == state.slot() + && state.payload_status() == StatePayloadStatus::Pending + { // If it happens that `pre_state` has *not* already been advanced forward a single // slot, then there is no need to compute the state root for this // `per_slot_processing` call since that state root is already stored in the parent @@ -1908,9 +1913,31 @@ fn load_parent<T: BeaconChainTypes, B: AsBlock<T::EthSpec>>( // Retrieve any state that is advanced through to at most `block.slot()`: this is // particularly important if `block` descends from the finalized/split block, but at a slot // prior to the finalized slot (which is invalid and inaccessible in our DB schema). + // + // Post-Gloas we must also fetch a state with the correct payload status. If the current + // block builds upon the payload of its parent block, then we know the parent block is FULL + // and we need to load the full state. + let (payload_status, parent_state_root) = + if block.as_block().fork_name_unchecked().gloas_enabled() + && let Ok(parent_bid_block_hash) = parent_block.payload_bid_block_hash() + { + if block.as_block().is_parent_block_full(parent_bid_block_hash) { + // TODO(gloas): loading the envelope here is not very efficient + let envelope = chain.store.get_payload_envelope(&root)?.ok_or_else(|| { + BeaconChainError::DBInconsistent(format!( + "Missing envelope for parent block {root:?}", + )) + })?; + (StatePayloadStatus::Full, envelope.message.state_root) + } else { + (StatePayloadStatus::Pending, parent_block.state_root()) + } + } else { + (StatePayloadStatus::Pending, parent_block.state_root()) + }; let (parent_state_root, state) = chain .store - .get_advanced_hot_state(root, block.slot(), parent_block.state_root())? + .get_advanced_hot_state(root, payload_status, block.slot(), parent_state_root)? .ok_or_else(|| { BeaconChainError::DBInconsistent( format!("Missing state for parent block {root:?}",), @@ -1933,7 +1960,9 @@ fn load_parent<T: BeaconChainTypes, B: AsBlock<T::EthSpec>>( ); } - let beacon_state_root = if state.slot() == parent_block.slot() { + let beacon_state_root = if state.slot() == parent_block.slot() + && let StatePayloadStatus::Pending = payload_status + { // Sanity check. if parent_state_root != parent_block.state_root() { return Err(BeaconChainError::DBInconsistent(format!( diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index d5935b492a..59fa5ec9ec 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -45,7 +45,7 @@ use tree_hash::TreeHash; use types::data::CustodyIndex; use types::{ BeaconBlock, BeaconState, BlobSidecarList, ChainSpec, ColumnIndex, DataColumnSidecarList, - Epoch, EthSpec, Hash256, SignedBeaconBlock, Slot, + Epoch, EthSpec, Hash256, SignedBeaconBlock, Slot, StatePayloadStatus, }; /// An empty struct used to "witness" all the `BeaconChainTypes` traits. It has no user-facing @@ -783,8 +783,16 @@ where .map_err(|e| descriptive_db_error("head block", &e))? .ok_or("Head block not found in store")?; + // TODO(gloas): update head loading to load Full block once fork choice works + let payload_status = StatePayloadStatus::Pending; + let (_head_state_root, head_state) = store - .get_advanced_hot_state(head_block_root, current_slot, head_block.state_root()) + .get_advanced_hot_state( + head_block_root, + payload_status, + current_slot, + head_block.state_root(), + ) .map_err(|e| descriptive_db_error("head state", &e))? .ok_or("Head state not found in store")?; diff --git a/beacon_node/beacon_chain/src/canonical_head.rs b/beacon_node/beacon_chain/src/canonical_head.rs index 1a08ac3f88..fd060e2b59 100644 --- a/beacon_node/beacon_chain/src/canonical_head.rs +++ b/beacon_node/beacon_chain/src/canonical_head.rs @@ -305,8 +305,16 @@ impl<T: BeaconChainTypes> CanonicalHead<T> { .get_full_block(&beacon_block_root)? .ok_or(Error::MissingBeaconBlock(beacon_block_root))?; let current_slot = fork_choice.fc_store().get_current_slot(); + + // TODO(gloas): pass a better payload status once fork choice is implemented + let payload_status = StatePayloadStatus::Pending; let (_, beacon_state) = store - .get_advanced_hot_state(beacon_block_root, current_slot, beacon_block.state_root())? + .get_advanced_hot_state( + beacon_block_root, + payload_status, + current_slot, + beacon_block.state_root(), + )? .ok_or(Error::MissingBeaconState(beacon_block.state_root()))?; let snapshot = BeaconSnapshot { @@ -673,10 +681,13 @@ impl<T: BeaconChainTypes> BeaconChain<T> { .get_full_block(&new_view.head_block_root)? .ok_or(Error::MissingBeaconBlock(new_view.head_block_root))?; + // TODO(gloas): update once we have fork choice + let payload_status = StatePayloadStatus::Pending; let (_, beacon_state) = self .store .get_advanced_hot_state( new_view.head_block_root, + payload_status, current_slot, beacon_block.state_root(), )? diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index 08acfdffa4..dde9fad342 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -20,7 +20,7 @@ use tracing::{debug, instrument}; use types::data::ColumnIndex; use types::{ BeaconStateError, ChainSpec, DataColumnSidecar, DataColumnSidecarFulu, DataColumnSubnetId, - EthSpec, Hash256, Slot, + EthSpec, Hash256, Slot, StatePayloadStatus, }; /// An error occurred while validating a gossip data column. @@ -706,9 +706,16 @@ fn verify_proposer_and_signature<T: BeaconChainTypes>( index = %column_index, "Proposer shuffling cache miss for column verification" ); + // We assume that the `Pending` state has the same shufflings as a `Full` state + // for the same block. Analysis: https://hackmd.io/@dapplion/gloas_dependant_root chain .store - .get_advanced_hot_state(block_parent_root, column_slot, parent_block.state_root) + .get_advanced_hot_state( + block_parent_root, + StatePayloadStatus::Pending, + column_slot, + parent_block.state_root, + ) .map_err(|e| GossipDataColumnError::BeaconChainError(Box::new(e.into())))? .ok_or_else(|| { GossipDataColumnError::BeaconChainError(Box::new( diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v24.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v24.rs index 1e1823a836..c8dfe1ac9b 100644 --- a/beacon_node/beacon_chain/src/schema_change/migration_schema_v24.rs +++ b/beacon_node/beacon_chain/src/schema_change/migration_schema_v24.rs @@ -16,6 +16,7 @@ use store::{ use tracing::{debug, info, warn}; use types::{ BeaconState, CACHED_EPOCHS, ChainSpec, Checkpoint, CommitteeCache, EthSpec, Hash256, Slot, + execution::StatePayloadStatus, }; /// We stopped using the pruning checkpoint in schema v23 but never explicitly deleted it. @@ -58,6 +59,7 @@ pub fn get_state_v22<T: BeaconChainTypes>( base_state, summary.slot, summary.latest_block_root, + StatePayloadStatus::Pending, update_cache, ) .map(Some) diff --git a/beacon_node/beacon_chain/src/state_advance_timer.rs b/beacon_node/beacon_chain/src/state_advance_timer.rs index cb916cb514..4c070e7ecc 100644 --- a/beacon_node/beacon_chain/src/state_advance_timer.rs +++ b/beacon_node/beacon_chain/src/state_advance_timer.rs @@ -26,7 +26,10 @@ use std::sync::{ use task_executor::TaskExecutor; use tokio::time::{Instant, sleep, sleep_until}; use tracing::{Instrument, debug, debug_span, error, instrument, warn}; -use types::{AttestationShufflingId, BeaconStateError, EthSpec, Hash256, RelativeEpoch, Slot}; +use types::{ + AttestationShufflingId, BeaconStateError, EthSpec, Hash256, RelativeEpoch, Slot, + StatePayloadStatus, +}; /// If the head slot is more than `MAX_ADVANCE_DISTANCE` from the current slot, then don't perform /// the state advancement. @@ -277,9 +280,16 @@ fn advance_head<T: BeaconChainTypes>(beacon_chain: &Arc<BeaconChain<T>>) -> Resu (snapshot.beacon_block_root, snapshot.beacon_state_root()) }; + // TODO(gloas): do better once we have fork choice + let payload_status = StatePayloadStatus::Pending; let (head_state_root, mut state) = beacon_chain .store - .get_advanced_hot_state(head_block_root, current_slot, head_block_state_root)? + .get_advanced_hot_state( + head_block_root, + payload_status, + current_slot, + head_block_state_root, + )? .ok_or(Error::HeadMissingFromSnapshotCache(head_block_root))?; let initial_slot = state.slot(); diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index eefb5d48b7..4bc5bb21d3 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -27,7 +27,7 @@ use bls::{ use eth2::types::{GraffitiPolicy, SignedBlockContentsTuple}; use execution_layer::test_utils::generate_genesis_header; use execution_layer::{ - ExecutionLayer, + ExecutionLayer, NewPayloadRequest, NewPayloadRequestGloas, auth::JwtKey, test_utils::{DEFAULT_JWT_SECRET, ExecutionBlockGenerator, MockBuilder, MockExecutionLayer}, }; @@ -52,7 +52,8 @@ use ssz_types::{RuntimeVariableList, VariableList}; use state_processing::ConsensusContext; use state_processing::per_block_processing::compute_timestamp_at_slot; use state_processing::per_block_processing::{ - BlockSignatureStrategy, VerifyBlockRoot, per_block_processing, + BlockSignatureStrategy, VerifyBlockRoot, deneb::kzg_commitment_to_versioned_hash, + per_block_processing, }; use state_processing::state_advance::complete_state_advance; use std::borrow::Cow; @@ -66,6 +67,7 @@ use store::database::interface::BeaconNodeBackend; use store::{HotColdDB, ItemStore, MemoryStore, config::StoreConfig}; use task_executor::TaskExecutor; use task_executor::{ShutdownReason, test_utils::TestRuntime}; +use tracing::debug; use tree_hash::TreeHash; use typenum::U4294967296; use types::attestation::IndexedAttestationBase; @@ -1092,6 +1094,86 @@ where (block_contents, block_response.state) } + /// Returns a newly created block, signed by the proposer for the given slot, + /// along with the execution payload envelope (for Gloas) and the pending state. + /// + /// For pre-Gloas forks, the envelope is `None` and this behaves like `make_block`. + pub async fn make_block_with_envelope( + &self, + mut state: BeaconState<E>, + slot: Slot, + ) -> ( + SignedBlockContentsTuple<E>, + Option<SignedExecutionPayloadEnvelope<E>>, + BeaconState<E>, + ) { + assert_ne!(slot, 0, "can't produce a block at slot 0"); + assert!(slot >= state.slot()); + + if state.fork_name_unchecked().gloas_enabled() + || self.spec.fork_name_at_slot::<E>(slot).gloas_enabled() + { + complete_state_advance(&mut state, None, slot, &self.spec) + .expect("should be able to advance state to slot"); + state.build_caches(&self.spec).expect("should build caches"); + + let proposer_index = state.get_beacon_proposer_index(slot, &self.spec).unwrap(); + + let graffiti = Graffiti::from(self.rng.lock().random::<[u8; 32]>()); + let graffiti_settings = + GraffitiSettings::new(Some(graffiti), Some(GraffitiPolicy::PreserveUserGraffiti)); + let randao_reveal = self.sign_randao_reveal(&state, proposer_index, slot); + + let (block, pending_state, _consensus_block_value) = self + .chain + .produce_block_on_state_gloas( + state, + None, + slot, + randao_reveal, + graffiti_settings, + ProduceBlockVerification::VerifyRandao, + ) + .await + .unwrap(); + + let signed_block = Arc::new(block.sign( + &self.validator_keypairs[proposer_index].sk, + &pending_state.fork(), + pending_state.genesis_validators_root(), + &self.spec, + )); + + // Retrieve the cached envelope produced during block production and sign it. + let signed_envelope = self + .chain + .pending_payload_envelopes + .write() + .remove(slot) + .map(|envelope| { + let epoch = slot.epoch(E::slots_per_epoch()); + let domain = self.spec.get_domain( + epoch, + Domain::BeaconBuilder, + &pending_state.fork(), + pending_state.genesis_validators_root(), + ); + let message = envelope.signing_root(domain); + let signature = self.validator_keypairs[proposer_index].sk.sign(message); + SignedExecutionPayloadEnvelope { + message: envelope, + signature, + } + }); + + let block_contents: SignedBlockContentsTuple<E> = (signed_block, None); + (block_contents, signed_envelope, pending_state) + } else { + let (block_contents, state) = self.make_block(state, slot).await; + (block_contents, None, state) + } + } + /// Useful for the `per_block_processing` tests. Creates a block, and returns the state after /// caches are built but before the generated block is processed. pub async fn make_block_return_pre_state( @@ -2575,6 +2657,84 @@ where Ok(block_hash) } + /// Process an execution payload envelope for a Gloas block. + pub async fn process_envelope( + &self, + block_root: Hash256, + signed_envelope: SignedExecutionPayloadEnvelope<E>, + pending_state: &mut BeaconState<E>, + ) -> Hash256 { + let state_root = signed_envelope.message.state_root; + debug!( + slot = %signed_envelope.message.slot, + ?state_root, + "Processing execution payload envelope" + ); + let block_state_root = pending_state + .update_tree_hash_cache() + .expect("should compute pending state root"); + + state_processing::envelope_processing::process_execution_payload_envelope( + pending_state, + Some(block_state_root), + &signed_envelope, + state_processing::VerifySignatures::True, + state_processing::envelope_processing::VerifyStateRoot::True, + &self.spec, + ) + .expect("should process envelope"); + + // Notify the EL of the new payload so forkchoiceUpdated can reference it. + let block = self + .chain + .store + .get_blinded_block(&block_root) + .expect("should read block from store") + .expect("block should exist in store"); + + let bid = &block + .message() + .body() + .signed_execution_payload_bid() + .expect("Gloas block should have a payload bid") + .message; + + let versioned_hashes = bid + .blob_kzg_commitments + .iter() + .map(kzg_commitment_to_versioned_hash) + .collect(); + + let request = NewPayloadRequest::Gloas(NewPayloadRequestGloas { + execution_payload: &signed_envelope.message.payload, + versioned_hashes, + parent_beacon_block_root: block.message().parent_root(), + execution_requests: &signed_envelope.message.execution_requests, + }); + + self.chain + .execution_layer + .as_ref() + .expect("harness should have execution layer") + .notify_new_payload(request) + .await + .expect("newPayload should succeed"); + + // Store the envelope. + self.chain + .store + .put_payload_envelope(&block_root, signed_envelope) + .expect("should store envelope"); + + // Store the Full state. + self.chain + .store + .put_state(&state_root, pending_state) + .expect("should store full state"); + + state_root + } + /// Builds an `Rpc` block from a `SignedBeaconBlock` and blobs or data columns retrieved from /// the database. pub fn build_rpc_block_from_store_blobs( diff --git a/beacon_node/beacon_chain/tests/rewards.rs b/beacon_node/beacon_chain/tests/rewards.rs index bc7c98041f..1889c1f625 100644 --- a/beacon_node/beacon_chain/tests/rewards.rs +++ b/beacon_node/beacon_chain/tests/rewards.rs @@ -845,13 +845,14 @@ async fn check_all_base_rewards_for_subset( .state_at_slot(Slot::new(slot - 1), StateSkipConfig::WithoutStateRoots) .unwrap(); + // TODO(gloas): handle payloads? let mut pre_state = BlockReplayer::<E, BlockReplayError, IntoIter<_, 0>>::new( parent_state, &harness.spec, ) .no_signature_verification() .minimal_block_root_verification() - .apply_blocks(vec![], Some(block.slot())) + .apply_blocks(vec![], vec![], Some(block.slot())) .unwrap() .into_state(); diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 86f4af3efc..a70ad89ca9 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -708,8 +708,13 @@ async fn block_replayer_hooks() { .add_attested_blocks_at_slots(state.clone(), state_root, &block_slots, &all_validators) .await; - let blocks = store - .load_blocks_to_replay(Slot::new(0), max_slot, end_block_root.into()) + let (blocks, envelopes) = store + .load_blocks_to_replay( + Slot::new(0), + max_slot, + end_block_root.into(), + StatePayloadStatus::Pending, + ) .unwrap(); let mut pre_slots = vec![]; @@ -744,7 +749,7 @@ async fn block_replayer_hooks() { post_block_slots.push(block.slot()); Ok(()) })) - .apply_blocks(blocks, None) + .apply_blocks(blocks, envelopes, None) .unwrap() .into_state(); @@ -3842,7 +3847,12 @@ async fn process_blocks_and_attestations_for_unaligned_checkpoint() { let (split_state_root, mut advanced_split_state) = harness .chain .store - .get_advanced_hot_state(split.block_root, split.slot, split.state_root) + .get_advanced_hot_state( + split.block_root, + StatePayloadStatus::Pending, + split.slot, + split.state_root, + ) .unwrap() .unwrap(); complete_state_advance( @@ -5470,6 +5480,427 @@ fn check_finalization(harness: &TestHarness, expected_slot: u64) { ); } +// ===================== Gloas Store Tests ===================== + +/// Test basic Gloas block + envelope storage and retrieval. +#[tokio::test] +async fn test_gloas_block_and_envelope_storage_no_skips() { + test_gloas_block_and_envelope_storage_generic(32, vec![], false).await +} + +#[tokio::test] +async fn test_gloas_block_and_envelope_storage_some_skips() { + test_gloas_block_and_envelope_storage_generic(32, vec![2, 4, 5, 16, 23, 24, 25], false).await +} + +#[tokio::test] +async fn test_gloas_block_and_envelope_storage_no_skips_w_cache() { + test_gloas_block_and_envelope_storage_generic(32, vec![], true).await +} + +#[tokio::test] +async fn test_gloas_block_and_envelope_storage_some_skips_w_cache() { + test_gloas_block_and_envelope_storage_generic(32, vec![2, 4, 5, 16, 23, 24, 25], true).await +} + +async fn test_gloas_block_and_envelope_storage_generic( + num_slots: u64, + skipped_slots: Vec<u64>, + use_state_cache: bool, +) { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } + + let db_path = tempdir().unwrap(); + let store_config = if !use_state_cache { + StoreConfig { + state_cache_size: new_non_zero_usize(1), + ..StoreConfig::default() + } + } else { + StoreConfig::default() + }; + let spec = test_spec::<E>(); + let store = get_store_generic(&db_path, store_config, spec); + let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT); + let spec = &harness.chain.spec; + + let (genesis_state, genesis_state_root) = harness.get_current_state_and_root(); + let mut state = genesis_state; + + let mut block_roots = vec![]; + let mut stored_states = vec![(Slot::new(0), StatePayloadStatus::Full, genesis_state_root)]; + + for i in 1..=num_slots { + let slot = Slot::new(i); + harness.advance_slot(); + + if skipped_slots.contains(&i) { + complete_state_advance(&mut state, None, slot, spec) + .expect("should be able to advance state to slot"); + + let state_root = state.canonical_root().unwrap(); + store.put_state(&state_root, &state).unwrap(); + stored_states.push((slot, state.payload_status(), state_root)); + } + + let (block_contents, envelope, mut pending_state) = + harness.make_block_with_envelope(state, slot).await; + let block_root = block_contents.0.canonical_root(); + + // Process the block. + harness + .process_block(slot, block_root, block_contents) + .await + .unwrap(); + + let pending_state_root = pending_state.update_tree_hash_cache().unwrap(); + stored_states.push((slot, StatePayloadStatus::Pending, pending_state_root)); + + // Process the envelope. + let envelope = envelope.expect("Gloas block should have envelope"); + let mut full_state = pending_state.clone(); + let envelope_state_root = envelope.message.state_root; + let full_state_root = harness + .process_envelope(block_root, envelope, &mut full_state) + .await; + assert_eq!(full_state_root, envelope_state_root); + stored_states.push((slot, StatePayloadStatus::Full, full_state_root)); + + block_roots.push(block_root); + state = full_state; + } + + // Verify block storage. + for (i, block_root) in block_roots.iter().enumerate() { + // Block can be loaded. + assert!( + store.get_blinded_block(block_root).unwrap().is_some(), + "block at slot {} should be in DB", + i + 1 + ); + + // Envelope can be loaded. + let loaded_envelope = store.get_payload_envelope(block_root).unwrap(); + assert!( + loaded_envelope.is_some(), + "envelope at slot {} should be in DB", + i + 1 + ); + } + + // Verify state storage. + // Iterate in reverse order to frustrate the cache. + for (slot, payload_status, state_root) in stored_states.into_iter().rev() { + println!("{slot}: {state_root:?}"); + let Some(mut loaded_state) = store + .get_state(&state_root, Some(slot), CACHE_STATE_IN_TESTS) + .unwrap() + else { + panic!("missing {payload_status:?} state at slot {slot} with root {state_root:?}"); + }; + assert_eq!(loaded_state.slot(), slot); + assert_eq!( + loaded_state.payload_status(), + payload_status, + "slot = {slot}" + ); + assert_eq!( + loaded_state.canonical_root().unwrap(), + state_root, + "slot = {slot}" + ); + } +} + +/// Test that Pending and Full states have the correct payload status through round-trip +/// storage and retrieval. +#[tokio::test] +async fn test_gloas_state_payload_status() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } + + let db_path = tempdir().unwrap(); + let store = get_store(&db_path); + let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT); + + let num_blocks = 6u64; + let (genesis_state, _genesis_state_root) = harness.get_current_state_and_root(); + let mut state = genesis_state; + + for i in 1..=num_blocks { + let slot = Slot::new(i); + harness.advance_slot(); + + let (block_contents, envelope, pending_state) = + harness.make_block_with_envelope(state, slot).await; + let block_root = block_contents.0.canonical_root(); + + harness + .process_block(slot, block_root, block_contents) + .await + .unwrap(); + + // Verify the pending state has correct payload status. + assert_eq!( + pending_state.payload_status(), + StatePayloadStatus::Pending, + "pending state at slot {} should be Pending", + i + ); + + // Process the envelope and verify the full state has correct payload status. + let envelope = envelope.expect("Gloas block should have envelope"); + let mut full_state = pending_state; + let full_state_root = harness + .process_envelope(block_root, envelope, &mut full_state) + .await; + + assert_eq!( + full_state.payload_status(), + StatePayloadStatus::Full, + "full state at slot {} should be Full", + i + ); + + // Round-trip: load the full state from DB and check status. + let loaded_full = store + .get_state(&full_state_root, None, CACHE_STATE_IN_TESTS) + .unwrap() + .expect("full state should exist in DB"); + assert_eq!( + loaded_full.payload_status(), + StatePayloadStatus::Full, + "loaded full state at slot {} should be Full after round-trip", + i + ); + + state = full_state; + } +} + +/// Test block replay with and without envelopes. +#[tokio::test] +async fn test_gloas_block_replay_with_envelopes() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } + + let db_path = tempdir().unwrap(); + let store = get_store(&db_path); + let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT); + + let num_blocks = 16u64; + let (genesis_state, _genesis_state_root) = harness.get_current_state_and_root(); + let mut state = genesis_state.clone(); + + let mut last_block_root = Hash256::zero(); + let mut pending_states = HashMap::new(); + let mut full_states = HashMap::new(); + + for i in 1..=num_blocks { + let slot = Slot::new(i); + harness.advance_slot(); + + let (block_contents, envelope, pending_state) = + harness.make_block_with_envelope(state, slot).await; + let block_root = block_contents.0.canonical_root(); + + harness + .process_block(slot, block_root, block_contents) + .await + .unwrap(); + + let pending_state_root = pending_state.clone().update_tree_hash_cache().unwrap(); + pending_states.insert(slot, (pending_state_root, pending_state.clone())); + + let envelope = envelope.expect("Gloas block should have envelope"); + let mut full_state = pending_state; + let full_state_root = harness + .process_envelope(block_root, envelope, &mut full_state) + .await; + full_states.insert(slot, (full_state_root, full_state.clone())); + + last_block_root = block_root; + state = full_state; + } + + let end_slot = Slot::new(num_blocks); + + // Load blocks for Pending replay (no envelopes for the last block). + let (blocks_pending, envelopes_pending) = store + .load_blocks_to_replay( + Slot::new(0), + end_slot, + last_block_root, + StatePayloadStatus::Pending, + ) + .unwrap(); + assert!( + !blocks_pending.is_empty(), + "should have blocks for pending replay" + ); + // For Pending, no envelope for the first block (slot 0) or last block; envelopes for + // intermediate blocks whose payloads are canonical. + let expected_pending_envelopes = blocks_pending.len().saturating_sub(2); + assert_eq!( + envelopes_pending.len(), + expected_pending_envelopes, + "pending replay should have envelopes for all blocks except the last" + ); + assert!( + blocks_pending + .iter() + .skip(1) + .take(envelopes_pending.len()) + .map(|block| block.slot()) + .eq(envelopes_pending + .iter() + .map(|envelope| envelope.message.slot)), + "block and envelope slots should match" + ); + + // Load blocks for Full replay (envelopes for all blocks including the last). + let (blocks_full, envelopes_full) = store + .load_blocks_to_replay( + Slot::new(0), + end_slot, + last_block_root, + StatePayloadStatus::Full, + ) + .unwrap(); + assert_eq!( + envelopes_full.len(), + expected_pending_envelopes + 1, + "full replay should have one more envelope than pending replay" + ); + + // Replay to Pending state and verify. + let mut replayed_pending = + BlockReplayer::<MinimalEthSpec>::new(genesis_state.clone(), store.get_chain_spec()) + .no_signature_verification() + .minimal_block_root_verification() + .desired_state_payload_status(StatePayloadStatus::Pending) + .apply_blocks(blocks_pending, envelopes_pending, None) + .expect("should replay blocks to pending state") + .into_state(); + replayed_pending.apply_pending_mutations().unwrap(); + + let (_, mut expected_pending) = pending_states.get(&end_slot).unwrap().clone(); + expected_pending.apply_pending_mutations().unwrap(); + + replayed_pending.drop_all_caches().unwrap(); + expected_pending.drop_all_caches().unwrap(); + assert_eq!( + replayed_pending, expected_pending, + "replayed pending state should match stored pending state" + ); + + // Replay to Full state and verify. + let mut replayed_full = + BlockReplayer::<MinimalEthSpec>::new(genesis_state, store.get_chain_spec()) + .no_signature_verification() + .minimal_block_root_verification() + .desired_state_payload_status(StatePayloadStatus::Full) + .apply_blocks(blocks_full, envelopes_full, None) + .expect("should replay blocks to full state") + .into_state(); + replayed_full.apply_pending_mutations().unwrap(); + + let (_, mut expected_full) = full_states.get(&end_slot).unwrap().clone(); + expected_full.apply_pending_mutations().unwrap(); + + replayed_full.drop_all_caches().unwrap(); + expected_full.drop_all_caches().unwrap(); + assert_eq!( + replayed_full, expected_full, + "replayed full state should match stored full state" + ); +} + +/// Test the hot state hierarchy with Full states stored as ReplayFrom. +#[tokio::test] +async fn test_gloas_hot_state_hierarchy() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } + + let db_path = tempdir().unwrap(); + let store = get_store(&db_path); + let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT); + + // Build enough blocks to span multiple epochs. With MinimalEthSpec (8 slots/epoch), + // 40 slots covers 5 epochs. + let num_blocks = E::slots_per_epoch() * 5; + // TODO(gloas): enable finalisation by increasing this threshold + let some_validators = (0..LOW_VALIDATOR_COUNT / 2).collect::<Vec<_>>(); + + let (genesis_state, _genesis_state_root) = harness.get_current_state_and_root(); + + // Use manual block building with envelopes for the first few blocks, + // then use the standard attested-blocks path once we've verified envelope handling. + let mut state = genesis_state; + let mut last_block_root = Hash256::zero(); + + for i in 1..=num_blocks { + let slot = Slot::new(i); + harness.advance_slot(); + + let (block_contents, envelope, pending_state) = + harness.make_block_with_envelope(state.clone(), slot).await; + let block_root = block_contents.0.canonical_root(); + + // Attest to previous block before processing next. + if i > 1 { + let state_root = state.update_tree_hash_cache().unwrap(); + harness.attest_block( + &state, + state_root, + last_block_root.into(), + &block_contents.0, + &some_validators, + ); + } + + harness + .process_block(slot, block_root, block_contents) + .await + .unwrap(); + + let envelope = envelope.expect("Gloas block should have envelope"); + let mut full_state = pending_state; + harness + .process_envelope(block_root, envelope, &mut full_state) + .await; + + last_block_root = block_root; + state = full_state; + } + + // Verify states can be loaded and have correct payload status. + let _head_state = harness.get_current_state(); + let _head_slot = harness.head_slot(); + + // States at all slots on the canonical chain should be retrievable. + for slot_num in 1..=num_blocks { + let slot = Slot::new(slot_num); + // Get the state root from the block at this slot via the state root iterator. + let state_root = harness.chain.state_root_at_slot(slot).unwrap().unwrap(); + + let mut loaded_state = store + .get_state(&state_root, Some(slot), CACHE_STATE_IN_TESTS) + .unwrap() + .unwrap(); + assert_eq!(loaded_state.canonical_root().unwrap(), state_root); + } + + // Verify chain dump and iterators work with Gloas states. + check_chain_dump(&harness, num_blocks + 1); + check_iterators(&harness); +} + /// Check that the HotColdDB's split_slot is equal to the start slot of the last finalized epoch. fn check_split_slot( harness: &TestHarness, @@ -5521,7 +5952,9 @@ fn check_chain_dump_from_slot(harness: &TestHarness, from_slot: Slot, expected_l ); // Check presence of execution payload on disk. - if harness.chain.spec.bellatrix_fork_epoch.is_some() { + if harness.chain.spec.bellatrix_fork_epoch.is_some() + && !harness.chain.spec.is_gloas_scheduled() + { assert!( harness .chain diff --git a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs index e94924d8b2..a66f7a9b55 100644 --- a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs +++ b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs @@ -932,8 +932,14 @@ pub fn generate_genesis_header<E: EthSpec>(spec: &ChainSpec) -> Option<Execution *header.transactions_root_mut() = empty_transactions_root; Some(header) } - // TODO(EIP-7732): need to look into this - ForkName::Gloas => None, + ForkName::Gloas => { + // TODO(gloas): we are using a Fulu header for now, but this gets fixed up by the + // genesis builder anyway which translates it to bid/latest_block_hash. + let mut header = ExecutionPayloadHeader::Fulu(<_>::default()); + *header.block_hash_mut() = genesis_block_hash.unwrap_or_default(); + *header.transactions_root_mut() = empty_transactions_root; + Some(header) + } } } diff --git a/beacon_node/http_api/src/attestation_performance.rs b/beacon_node/http_api/src/attestation_performance.rs index 6e285829d2..05ed36e68b 100644 --- a/beacon_node/http_api/src/attestation_performance.rs +++ b/beacon_node/http_api/src/attestation_performance.rs @@ -205,8 +205,9 @@ pub fn get_attestation_performance<T: BeaconChainTypes>( }) .collect::<Result<Vec<_>, _>>()?; + // TODO(gloas): add payloads replayer = replayer - .apply_blocks(blocks, None) + .apply_blocks(blocks, vec![], None) .map_err(|e| custom_server_error(format!("{:?}", e)))?; } diff --git a/beacon_node/http_api/src/block_packing_efficiency.rs b/beacon_node/http_api/src/block_packing_efficiency.rs index 3772470b28..725a0648a5 100644 --- a/beacon_node/http_api/src/block_packing_efficiency.rs +++ b/beacon_node/http_api/src/block_packing_efficiency.rs @@ -398,8 +398,9 @@ pub fn get_block_packing_efficiency<T: BeaconChainTypes>( }) .collect::<Result<Vec<_>, _>>()?; + // TODO(gloas): add payloads replayer = replayer - .apply_blocks(blocks, None) + .apply_blocks(blocks, vec![], None) .map_err(|e: PackingEfficiencyError| custom_server_error(format!("{:?}", e)))?; } diff --git a/beacon_node/http_api/src/produce_block.rs b/beacon_node/http_api/src/produce_block.rs index 607221686f..70475de130 100644 --- a/beacon_node/http_api/src/produce_block.rs +++ b/beacon_node/http_api/src/produce_block.rs @@ -70,7 +70,7 @@ pub async fn produce_block_v4<T: BeaconChainTypes>( let graffiti_settings = GraffitiSettings::new(query.graffiti, query.graffiti_policy); - let (block, consensus_block_value) = chain + let (block, _pending_state, consensus_block_value) = chain .produce_block_with_verification_gloas( randao_reveal, slot, diff --git a/beacon_node/http_api/src/sync_committee_rewards.rs b/beacon_node/http_api/src/sync_committee_rewards.rs index 9bc1f6ead4..8715fc2b1e 100644 --- a/beacon_node/http_api/src/sync_committee_rewards.rs +++ b/beacon_node/http_api/src/sync_committee_rewards.rs @@ -66,11 +66,12 @@ pub fn get_state_before_applying_block<T: BeaconChainTypes>( }) .map_err(|e| custom_not_found(format!("Parent state is not available! {:?}", e)))?; + // TODO(gloas): handle payloads? let replayer = BlockReplayer::new(parent_state, &chain.spec) .no_signature_verification() .state_root_iter([Ok((parent_block.state_root(), parent_block.slot()))].into_iter()) .minimal_block_root_verification() - .apply_blocks(vec![], Some(block.slot())) + .apply_blocks(vec![], vec![], Some(block.slot())) .map_err(unhandled_error::<BeaconChainError>)?; Ok(replayer.into_state()) diff --git a/beacon_node/store/src/hdiff.rs b/beacon_node/store/src/hdiff.rs index 3777c83b60..85ac56454c 100644 --- a/beacon_node/store/src/hdiff.rs +++ b/beacon_node/store/src/hdiff.rs @@ -654,6 +654,12 @@ impl HierarchyModuli { /// layer 2 diff will point to the start snapshot instead of the layer 1 diff at /// 2998272. pub fn storage_strategy(&self, slot: Slot, start_slot: Slot) -> Result<StorageStrategy, Error> { + // Initially had the idea of using different storage strategies for full and pending states, + // but it was very complex. However without this concept we end up storing two diffs/two + // snapshots at full slots. The complexity of managing skipped slots was the main impetus + // for reverting the payload-status sensitive design: a Full skipped slot has no same-slot + // Pending state to replay from, so has to be handled differently from Full non-skipped + // slots. match slot.cmp(&start_slot) { Ordering::Less => return Err(Error::LessThanStart(slot, start_slot)), Ordering::Equal => return Ok(StorageStrategy::Snapshot), diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index fe3477dbfe..428086c464 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -186,6 +186,7 @@ pub enum HotColdDBError { MissingHotHDiff(Hash256), MissingHDiff(Slot), MissingExecutionPayload(Hash256), + MissingExecutionPayloadEnvelope(Hash256), MissingFullBlockExecutionPayloadPruned(Hash256, Slot), MissingAnchorInfo, MissingFrozenBlockSlot(Hash256), @@ -1132,10 +1133,13 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold> pub fn get_advanced_hot_state( &self, block_root: Hash256, + payload_status: StatePayloadStatus, max_slot: Slot, state_root: Hash256, ) -> Result<Option<(Hash256, BeaconState<E>)>, Error> { - if let Some(cached) = self.get_advanced_hot_state_from_cache(block_root, max_slot) { + if let Some(cached) = + self.get_advanced_hot_state_from_cache(block_root, payload_status, max_slot) + { return Ok(Some(cached)); } @@ -1157,7 +1161,11 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold> .into()); } - let state_root = if block_root == split.block_root && split.slot <= max_slot { + // Split state should always be `Pending`. + let state_root = if block_root == split.block_root + && let StatePayloadStatus::Pending = payload_status + && split.slot <= max_slot + { split.state_root } else { state_root @@ -1204,11 +1212,12 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold> pub fn get_advanced_hot_state_from_cache( &self, block_root: Hash256, + payload_status: StatePayloadStatus, max_slot: Slot, ) -> Option<(Hash256, BeaconState<E>)> { self.state_cache .lock() - .get_by_block_root(block_root, max_slot) + .get_by_block_root(block_root, payload_status, max_slot) } /// Delete a state, ensuring it is removed from the LRU cache, as well as from on-disk. @@ -1379,6 +1388,8 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold> // NOTE: `hot_storage_strategy` can error if there are states in the database // prior to the `anchor_slot`. This can happen if checkpoint sync has been // botched and left some states in the database prior to completing. + // Use `Pending` status here because snapshots and diffs are only stored for + // `Pending` states. if let Some(slot) = slot && let Ok(strategy) = self.hot_storage_strategy(slot) { @@ -1846,6 +1857,55 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold> } } + /// Compute the `StatePayloadStatus` for a stored state based on its summary. + /// + /// In future this might become a field of the summary, but this would require a whole DB + /// migration. For now we use an extra read from the DB to determine it. + fn get_hot_state_summary_payload_status( + &self, + summary: &HotStateSummary, + ) -> Result<StatePayloadStatus, Error> { + // Treat pre-Gloas states as `Pending`. + if !self + .spec + .fork_name_at_slot::<E>(summary.slot) + .gloas_enabled() + { + return Ok(StatePayloadStatus::Pending); + } + + // Treat genesis state as `Pending` (`BeaconBlock` state). + let previous_state_root = summary.previous_state_root; + if previous_state_root.is_zero() { + return Ok(StatePayloadStatus::Pending); + } + + // Load the hot state summary for the previous state. + // + // If it has the same slot as this summary then we know this summary is for a `Full` state + // (payload state), because they are always diffed against their same-slot `Pending` state. + // + // If the previous summary has a different slot AND the latest block is from `summary.slot`, + // then this state *must* be `Pending` (it is the summary for latest block itself). + // + // Otherwise, we are at a skipped slot and must traverse the graph of state summaries + // backwards until we reach a summary for the latest block. This recursion could be quite + // far in the case of a long skip. We could optimise this in future using the + // `diff_base_state` (like in `get_ancestor_state_root`), or by doing a proper DB + // migration. + let previous_state_summary = self + .load_hot_state_summary(&previous_state_root)? + .ok_or(Error::MissingHotStateSummary(previous_state_root))?; + + if previous_state_summary.slot == summary.slot { + Ok(StatePayloadStatus::Full) + } else if summary.slot == summary.latest_block_slot { + Ok(StatePayloadStatus::Pending) + } else { + self.get_hot_state_summary_payload_status(&previous_state_summary) + } + } + fn load_hot_hdiff_buffer(&self, state_root: Hash256) -> Result<HDiffBuffer, Error> { if let Some(buffer) = self .state_cache @@ -1941,13 +2001,22 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold> ) -> Result<Option<(BeaconState<E>, Hash256)>, Error> { metrics::inc_counter(&metrics::BEACON_STATE_HOT_GET_COUNT); - if let Some(HotStateSummary { - slot, - latest_block_root, - diff_base_state, - .. - }) = self.load_hot_state_summary(state_root)? + if let Some( + summary @ HotStateSummary { + slot, + latest_block_root, + diff_base_state, + .. + }, + ) = self.load_hot_state_summary(state_root)? { + let payload_status = self.get_hot_state_summary_payload_status(&summary)?; + debug!( + %slot, + ?state_root, + ?payload_status, + "Loading hot state" + ); let mut state = match self.hot_storage_strategy(slot)? { strat @ StorageStrategy::Snapshot | strat @ StorageStrategy::DiffFrom(_) => { let buffer_timer = metrics::start_timer_vec( @@ -1999,6 +2068,7 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold> base_state, slot, latest_block_root, + payload_status, update_cache, )? } @@ -2016,19 +2086,26 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold> base_state: BeaconState<E>, slot: Slot, latest_block_root: Hash256, + desired_payload_status: StatePayloadStatus, update_cache: bool, ) -> Result<BeaconState<E>, Error> { - if base_state.slot() == slot { + if base_state.slot() == slot && base_state.payload_status() == desired_payload_status { return Ok(base_state); } - let blocks = self.load_blocks_to_replay(base_state.slot(), slot, latest_block_root)?; + let (blocks, envelopes) = self.load_blocks_to_replay( + base_state.slot(), + slot, + latest_block_root, + desired_payload_status, + )?; let _t = metrics::start_timer(&metrics::STORE_BEACON_REPLAY_HOT_BLOCKS_TIME); // If replaying blocks, and `update_cache` is true, also cache the epoch boundary // state that this state is based on. It may be useful as the basis of more states // in the same epoch. let state_cache_hook = |state_root, state: &mut BeaconState<E>| { + // TODO(gloas): prevent caching of the payload_status=Full state? if !update_cache || state.slot() % E::slots_per_epoch() != 0 { return Ok(()); } @@ -2052,9 +2129,19 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold> Ok(()) }; + debug!( + %slot, + blocks = ?blocks.iter().map(|block| block.slot()).collect::<Vec<_>>(), + envelopes = ?envelopes.iter().map(|e| e.message.slot).collect::<Vec<_>>(), + payload_status = ?desired_payload_status, + "Replaying blocks and envelopes" + ); + self.replay_blocks( base_state, blocks, + envelopes, + desired_payload_status, slot, no_state_root_iter(), Some(Box::new(state_cache_hook)), @@ -2358,7 +2445,7 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold> return Ok(base_state); } - let blocks = self.load_cold_blocks(base_state.slot() + 1, slot)?; + let (blocks, envelopes) = self.load_cold_blocks(base_state.slot() + 1, slot)?; // Include state root for base state as it is required by block processing to not // have to hash the state. @@ -2367,7 +2454,17 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold> self.forwards_state_roots_iterator_until(base_state.slot(), slot, || { Err(Error::StateShouldNotBeRequired(slot)) })?; - let state = self.replay_blocks(base_state, blocks, slot, Some(state_root_iter), None)?; + // TODO(gloas): calculate correct payload status for cold states + let payload_status = StatePayloadStatus::Pending; + let state = self.replay_blocks( + base_state, + blocks, + envelopes, + payload_status, + slot, + Some(state_root_iter), + None, + )?; debug!( target_slot = %slot, replay_time_ms = metrics::stop_timer_with_duration(replay_timer).as_millis(), @@ -2460,40 +2557,77 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold> } } - /// Load cold blocks between `start_slot` and `end_slot` inclusive. + /// Load cold blocks and payload envelopes between `start_slot` and `end_slot` inclusive. + #[allow(clippy::type_complexity)] pub fn load_cold_blocks( &self, start_slot: Slot, end_slot: Slot, - ) -> Result<Vec<SignedBlindedBeaconBlock<E>>, Error> { + ) -> Result< + ( + Vec<SignedBlindedBeaconBlock<E>>, + Vec<SignedExecutionPayloadEnvelope<E>>, + ), + Error, + > { let _t = metrics::start_timer(&metrics::STORE_BEACON_LOAD_COLD_BLOCKS_TIME); let block_root_iter = self.forwards_block_roots_iterator_until(start_slot, end_slot, || { Err(Error::StateShouldNotBeRequired(end_slot)) })?; - process_results(block_root_iter, |iter| { + let blocks = process_results(block_root_iter, |iter| { iter.map(|(block_root, _slot)| block_root) .dedup() .map(|block_root| { self.get_blinded_block(&block_root)? .ok_or(Error::MissingBlock(block_root)) }) - .collect() - })? + .collect::<Result<Vec<_>, Error>>() + })??; + + // If Gloas is not enabled for any slots in the range, just return `blocks`. + if !self.spec.fork_name_at_slot::<E>(start_slot).gloas_enabled() + && !self.spec.fork_name_at_slot::<E>(end_slot).gloas_enabled() + { + return Ok((blocks, vec![])); + } + // TODO(gloas): wire this up + let end_block_root = Hash256::ZERO; + let desired_payload_status = StatePayloadStatus::Pending; + let envelopes = self.load_payload_envelopes_for_blocks( + &blocks, + end_block_root, + desired_payload_status, + )?; + + Ok((blocks, envelopes)) } - /// Load the blocks between `start_slot` and `end_slot` by backtracking from `end_block_hash`. + /// Load the blocks & envelopes between `start_slot` and `end_slot` by backtracking from + /// `end_block_root`. /// /// Blocks are returned in slot-ascending order, suitable for replaying on a state with slot /// equal to `start_slot`, to reach a state with slot equal to `end_slot`. + /// + /// Payloads are also returned in slot-ascending order, but only payloads forming part of + /// the chain are loaded (payloads for EMPTY slots are omitted). Prior to Gloas, an empty + /// vec of payloads will be returned. + #[allow(clippy::type_complexity)] pub fn load_blocks_to_replay( &self, start_slot: Slot, end_slot: Slot, - end_block_hash: Hash256, - ) -> Result<Vec<SignedBeaconBlock<E, BlindedPayload<E>>>, Error> { + end_block_root: Hash256, + desired_payload_status: StatePayloadStatus, + ) -> Result< + ( + Vec<SignedBlindedBeaconBlock<E>>, + Vec<SignedExecutionPayloadEnvelope<E>>, + ), + Error, + > { let _t = metrics::start_timer(&metrics::STORE_BEACON_LOAD_HOT_BLOCKS_TIME); - let mut blocks = ParentRootBlockIterator::new(self, end_block_hash) + let mut blocks = ParentRootBlockIterator::new(self, end_block_root) .map(|result| result.map(|(_, block)| block)) // Include the block at the end slot (if any), it needs to be // replayed in order to construct the canonical state at `end_slot`. @@ -2520,17 +2654,70 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold> }) .collect::<Result<Vec<_>, _>>()?; blocks.reverse(); - Ok(blocks) + + // If Gloas is not enabled for any slots in the range, just return `blocks`. + if !self.spec.fork_name_at_slot::<E>(start_slot).gloas_enabled() + && !self.spec.fork_name_at_slot::<E>(end_slot).gloas_enabled() + { + return Ok((blocks, vec![])); + } + + let envelopes = self.load_payload_envelopes_for_blocks( + &blocks, + end_block_root, + desired_payload_status, + )?; + + Ok((blocks, envelopes)) + } + + pub fn load_payload_envelopes_for_blocks( + &self, + blocks: &[SignedBlindedBeaconBlock<E>], + end_block_root: Hash256, + desired_payload_status: StatePayloadStatus, + ) -> Result<Vec<SignedExecutionPayloadEnvelope<E>>, Error> { + let mut envelopes = vec![]; + + for (block, next_block) in blocks.iter().tuple_windows() { + if block.fork_name_unchecked().gloas_enabled() { + // Check next block to see if this block's payload is canonical on this chain. + let block_hash = block.payload_bid_block_hash()?; + if !next_block.is_parent_block_full(block_hash) { + // No payload at this slot (empty), nothing to load. + continue; + } + // Using `parent_root` avoids computation. + let block_root = next_block.parent_root(); + let envelope = self + .get_payload_envelope(&block_root)? + .ok_or(HotColdDBError::MissingExecutionPayloadEnvelope(block_root))?; + envelopes.push(envelope); + } + } + + // Load the payload for the last block if desired. + if let StatePayloadStatus::Full = desired_payload_status { + let envelope = self.get_payload_envelope(&end_block_root)?.ok_or( + HotColdDBError::MissingExecutionPayloadEnvelope(end_block_root), + )?; + envelopes.push(envelope); + } + + Ok(envelopes) } /// Replay `blocks` on top of `state` until `target_slot` is reached. /// /// Will skip slots as necessary. The returned state is not guaranteed /// to have any caches built, beyond those immediately required by block processing. + #[allow(clippy::too_many_arguments)] pub fn replay_blocks( &self, state: BeaconState<E>, - blocks: Vec<SignedBeaconBlock<E, BlindedPayload<E>>>, + blocks: Vec<SignedBlindedBeaconBlock<E>>, + envelopes: Vec<SignedExecutionPayloadEnvelope<E>>, + desired_payload_status: StatePayloadStatus, target_slot: Slot, state_root_iter: Option<impl Iterator<Item = Result<(Hash256, Slot), Error>>>, pre_slot_hook: Option<PreSlotHook<E, Error>>, @@ -2539,7 +2726,8 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold> let mut block_replayer = BlockReplayer::new(state, &self.spec) .no_signature_verification() - .minimal_block_root_verification(); + .minimal_block_root_verification() + .desired_state_payload_status(desired_payload_status); let have_state_root_iterator = state_root_iter.is_some(); if let Some(state_root_iter) = state_root_iter { @@ -2551,7 +2739,7 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold> } block_replayer - .apply_blocks(blocks, Some(target_slot)) + .apply_blocks(blocks, envelopes, Some(target_slot)) .map(|block_replayer| { if have_state_root_iterator && block_replayer.state_root_miss() { warn!( @@ -4006,11 +4194,15 @@ impl HotStateSummary { // slots where there isn't a skip). let latest_block_root = state.get_latest_block_root(state_root); + // Payload status of the state determines a lot about how it is stored. + let payload_status = state.payload_status(); + let get_state_root = |slot| { if slot == state.slot() { + // TODO(gloas): I think we can remove this case Ok::<_, Error>(state_root) } else { - Ok(get_ancestor_state_root(store, state, slot).map_err(|e| { + Ok::<_, Error>(get_ancestor_state_root(store, state, slot).map_err(|e| { Error::StateSummaryIteratorError { error: e, from_state_root: state_root, @@ -4030,6 +4222,12 @@ impl HotStateSummary { let previous_state_root = if state.slot() == 0 { // Set to 0x0 for genesis state to prevent any sort of circular reference. Hash256::zero() + } else if let StatePayloadStatus::Full = payload_status + && state.slot() == state.latest_block_header().slot + { + // A Full state at a non-skipped slot builds off the Pending state of the same slot, + // i.e. the state with the same `state_root` as its `BeaconBlock` + state.latest_block_header().state_root } else { get_state_root(state.slot().safe_sub(1_u64)?)? }; diff --git a/beacon_node/store/src/reconstruct.rs b/beacon_node/store/src/reconstruct.rs index 7aca692ef9..e51543c3a2 100644 --- a/beacon_node/store/src/reconstruct.rs +++ b/beacon_node/store/src/reconstruct.rs @@ -67,6 +67,7 @@ where state.build_caches(&self.spec)?; + // TODO(gloas): handle payload envelope replay process_results(block_root_iter, |iter| -> Result<(), Error> { let mut io_batch = vec![]; diff --git a/beacon_node/store/src/state_cache.rs b/beacon_node/store/src/state_cache.rs index 6d159c9361..d016922ade 100644 --- a/beacon_node/store/src/state_cache.rs +++ b/beacon_node/store/src/state_cache.rs @@ -7,7 +7,7 @@ use lru::LruCache; use std::collections::{BTreeMap, HashMap, HashSet}; use std::num::NonZeroUsize; use tracing::instrument; -use types::{BeaconState, ChainSpec, Epoch, EthSpec, Hash256, Slot}; +use types::{BeaconState, ChainSpec, Epoch, EthSpec, Hash256, Slot, execution::StatePayloadStatus}; /// Fraction of the LRU cache to leave intact during culling. const CULL_EXEMPT_NUMERATOR: usize = 1; @@ -23,10 +23,10 @@ pub struct FinalizedState<E: EthSpec> { state: BeaconState<E>, } -/// Map from block_root -> slot -> state_root. +/// Map from (block_root, payload_status) -> slot -> state_root. #[derive(Debug, Default)] pub struct BlockMap { - blocks: HashMap<Hash256, SlotMap>, + blocks: HashMap<(Hash256, StatePayloadStatus), SlotMap>, } /// Map from slot -> state_root. @@ -143,8 +143,11 @@ impl<E: EthSpec> StateCache<E> { return Err(Error::FinalizedStateDecreasingSlot); } + let payload_status = state.payload_status(); + // Add to block map. - self.block_map.insert(block_root, state.slot(), state_root); + self.block_map + .insert(block_root, payload_status, state.slot(), state_root); // Prune block map. let state_roots_to_prune = self.block_map.prune(state.slot()); @@ -267,7 +270,9 @@ impl<E: EthSpec> StateCache<E> { // Record the connection from block root and slot to this state. let slot = state.slot(); - self.block_map.insert(block_root, slot, state_root); + let payload_status = state.payload_status(); + self.block_map + .insert(block_root, payload_status, slot, state_root); Ok(PutStateOutcome::New(deleted_states)) } @@ -316,9 +321,10 @@ impl<E: EthSpec> StateCache<E> { pub fn get_by_block_root( &mut self, block_root: Hash256, + payload_status: StatePayloadStatus, slot: Slot, ) -> Option<(Hash256, BeaconState<E>)> { - let slot_map = self.block_map.blocks.get(&block_root)?; + let slot_map = self.block_map.blocks.get(&(block_root, payload_status))?; // Find the state at `slot`, or failing that the most recent ancestor. let state_root = slot_map @@ -339,7 +345,12 @@ impl<E: EthSpec> StateCache<E> { } pub fn delete_block_states(&mut self, block_root: &Hash256) { - if let Some(slot_map) = self.block_map.delete_block_states(block_root) { + let (pending_state_roots, full_state_roots) = + self.block_map.delete_block_states(block_root); + for slot_map in [pending_state_roots, full_state_roots] + .into_iter() + .flatten() + { for state_root in slot_map.slots.values() { self.states.pop(state_root); } @@ -412,8 +423,14 @@ impl<E: EthSpec> StateCache<E> { } impl BlockMap { - fn insert(&mut self, block_root: Hash256, slot: Slot, state_root: Hash256) { - let slot_map = self.blocks.entry(block_root).or_default(); + fn insert( + &mut self, + block_root: Hash256, + payload_status: StatePayloadStatus, + slot: Slot, + state_root: Hash256, + ) { + let slot_map = self.blocks.entry((block_root, payload_status)).or_default(); slot_map.slots.insert(slot, state_root); } @@ -444,8 +461,12 @@ impl BlockMap { }); } - fn delete_block_states(&mut self, block_root: &Hash256) -> Option<SlotMap> { - self.blocks.remove(block_root) + fn delete_block_states(&mut self, block_root: &Hash256) -> (Option<SlotMap>, Option<SlotMap>) { + let pending_state_roots = self + .blocks + .remove(&(*block_root, StatePayloadStatus::Pending)); + let full_state_roots = self.blocks.remove(&(*block_root, StatePayloadStatus::Full)); + (pending_state_roots, full_state_roots) } } diff --git a/consensus/state_processing/src/block_replayer.rs b/consensus/state_processing/src/block_replayer.rs index 56e667cdd3..a10d6179fe 100644 --- a/consensus/state_processing/src/block_replayer.rs +++ b/consensus/state_processing/src/block_replayer.rs @@ -1,6 +1,11 @@ use crate::{ BlockProcessingError, BlockSignatureStrategy, ConsensusContext, SlotProcessingError, - VerifyBlockRoot, per_block_processing, per_epoch_processing::EpochProcessingSummary, + VerifyBlockRoot, VerifySignatures, + envelope_processing::{ + EnvelopeProcessingError, VerifyStateRoot, process_execution_payload_envelope, + }, + per_block_processing, + per_epoch_processing::EpochProcessingSummary, per_slot_processing, }; use itertools::Itertools; @@ -8,7 +13,7 @@ use std::iter::Peekable; use std::marker::PhantomData; use types::{ BeaconState, BeaconStateError, BlindedPayload, ChainSpec, EthSpec, Hash256, SignedBeaconBlock, - Slot, + SignedExecutionPayloadEnvelope, Slot, execution::StatePayloadStatus, }; pub type PreBlockHook<'a, E, Error> = Box< @@ -24,7 +29,7 @@ pub type PostSlotHook<'a, E, Error> = Box< >; pub type StateRootIterDefault<Error> = std::iter::Empty<Result<(Hash256, Slot), Error>>; -/// Efficiently apply blocks to a state while configuring various parameters. +/// Efficiently apply blocks and payloads to a state while configuring various parameters. /// /// Usage follows a builder pattern. pub struct BlockReplayer< @@ -41,8 +46,21 @@ pub struct BlockReplayer< post_block_hook: Option<PostBlockHook<'a, Spec, Error>>, pre_slot_hook: Option<PreSlotHook<'a, Spec, Error>>, post_slot_hook: Option<PostSlotHook<'a, Spec, Error>>, + /// Iterator over state roots for all *block* states. + /// + /// Pre-Gloas, this is all states. Post-Gloas, this is *just* the states corresponding to beacon + /// blocks. For states corresponding to payloads, we read the state root from the payload + /// envelope. + // TODO(gloas): this concept might need adjusting when we implement the cold DB. pub(crate) state_root_iter: Option<Peekable<StateRootIter>>, state_root_miss: bool, + /// The payload status of the state desired as the end result of block replay. + /// + /// This dictates whether a payload should be applied after applying the last block. + /// + /// Prior to Gloas, this should always be set to `StatePayloadStatus::Pending` to indicate + /// that no envelope needs to be applied. + desired_state_payload_status: StatePayloadStatus, _phantom: PhantomData<Error>, } @@ -50,7 +68,12 @@ pub struct BlockReplayer< pub enum BlockReplayError { SlotProcessing(SlotProcessingError), BlockProcessing(BlockProcessingError), + EnvelopeProcessing(EnvelopeProcessingError), BeaconState(BeaconStateError), + /// A payload envelope for this `slot` was required but not provided. + MissingPayloadEnvelope { + slot: Slot, + }, } impl From<SlotProcessingError> for BlockReplayError { @@ -65,6 +88,12 @@ impl From<BlockProcessingError> for BlockReplayError { } } +impl From<EnvelopeProcessingError> for BlockReplayError { + fn from(e: EnvelopeProcessingError) -> Self { + Self::EnvelopeProcessing(e) + } +} + impl From<BeaconStateError> for BlockReplayError { fn from(e: BeaconStateError) -> Self { Self::BeaconState(e) @@ -96,6 +125,7 @@ where post_slot_hook: None, state_root_iter: None, state_root_miss: false, + desired_state_payload_status: StatePayloadStatus::Pending, _phantom: PhantomData, } } @@ -161,6 +191,14 @@ where self } + /// Set the desired payload status of the state reached by replay. + /// + /// This determines whether to apply a payload after applying the last block. + pub fn desired_state_payload_status(mut self, payload_status: StatePayloadStatus) -> Self { + self.desired_state_payload_status = payload_status; + self + } + /// Compute the state root for `self.state` as efficiently as possible. /// /// This function MUST only be called when `self.state` is a post-state, i.e. it MUST not be @@ -208,6 +246,38 @@ where Ok(state_root) } + /// Apply an execution payload envelope to `self.state`. + /// + /// The `block_state_root` MUST be the `state_root` of the most recently applied block. + /// + /// Returns the `state_root` of `self.state` after payload application. + fn apply_payload_envelope( + &mut self, + envelope: &SignedExecutionPayloadEnvelope<E>, + block_state_root: Hash256, + ) -> Result<Hash256, Error> { + // TODO(gloas): bulk signature verification could be relevant here? + let verify_payload_signatures = + if let BlockSignatureStrategy::NoVerification = self.block_sig_strategy { + VerifySignatures::False + } else { + VerifySignatures::True + }; + // TODO(gloas): state root verif enabled during initial prototyping + let verify_state_root = VerifyStateRoot::True; + process_execution_payload_envelope( + &mut self.state, + Some(block_state_root), + envelope, + verify_payload_signatures, + verify_state_root, + self.spec, + ) + .map_err(BlockReplayError::from)?; + + Ok(envelope.message.state_root) + } + /// Apply `blocks` atop `self.state`, taking care of slot processing. /// /// If `target_slot` is provided then the state will be advanced through to `target_slot` @@ -215,8 +285,21 @@ where pub fn apply_blocks( mut self, blocks: Vec<SignedBeaconBlock<E, BlindedPayload<E>>>, + payload_envelopes: Vec<SignedExecutionPayloadEnvelope<E>>, target_slot: Option<Slot>, ) -> Result<Self, Error> { + let mut envelopes_iter = payload_envelopes.into_iter(); + + let mut next_envelope_at_slot = |slot| { + if let Some(envelope) = envelopes_iter.next() + && envelope.message.slot == slot + { + Ok(envelope) + } else { + Err(BlockReplayError::MissingPayloadEnvelope { slot }) + } + }; + for (i, block) in blocks.iter().enumerate() { // Allow one additional block at the start which is only used for its state root. if i == 0 && block.slot() <= self.state.slot() { @@ -224,7 +307,35 @@ where } while self.state.slot() < block.slot() { - let state_root = self.get_state_root(&blocks, i)?; + let mut state_root = self.get_state_root(&blocks, i)?; + + // Apply the payload for the *previous* block if the bid in the current block + // indicates that the parent is full (and it hasn't already been applied). + state_root = if block.fork_name_unchecked().gloas_enabled() + && self.state.slot() == self.state.latest_block_header().slot + { + let latest_bid_block_hash = self + .state + .latest_execution_payload_bid() + .map_err(BlockReplayError::from)? + .block_hash; + + // Similar to `is_parent_block_full`, but reading the block hash from the + // not-yet-applied `block`. The slot 0 case covers genesis (no block replay reqd). + if self.state.slot() != 0 && block.is_parent_block_full(latest_bid_block_hash) { + let envelope = next_envelope_at_slot(self.state.slot())?; + // State root for the next slot processing is now the envelope's state root. + self.apply_payload_envelope(&envelope, state_root)? + } else { + // Empty payload at this slot, the state root is unchanged from when the + // beacon block was applied. + state_root + } + } else { + // Pre-Gloas or at skipped slots post-Gloas, the state root of the parent state + // is always the output from `self.get_state_root`. + state_root + }; if let Some(ref mut pre_slot_hook) = self.pre_slot_hook { pre_slot_hook(state_root, &mut self.state)?; @@ -268,9 +379,24 @@ where } } + // Apply the last payload if desired. + let mut opt_state_root = if let StatePayloadStatus::Full = self.desired_state_payload_status + && let Some(last_block) = blocks.last() + { + let envelope = next_envelope_at_slot(self.state.slot())?; + Some(self.apply_payload_envelope(&envelope, last_block.state_root())?) + } else { + None + }; + if let Some(target_slot) = target_slot { while self.state.slot() < target_slot { - let state_root = self.get_state_root(&blocks, blocks.len())?; + // Read state root from `opt_state_root` if a payload was just applied. + let state_root = if let Some(root) = opt_state_root.take() { + root + } else { + self.get_state_root(&blocks, blocks.len())? + }; if let Some(ref mut pre_slot_hook) = self.pre_slot_hook { pre_slot_hook(state_root, &mut self.state)?; diff --git a/consensus/state_processing/src/envelope_processing.rs b/consensus/state_processing/src/envelope_processing.rs index be6b7c1b29..97953b835f 100644 --- a/consensus/state_processing/src/envelope_processing.rs +++ b/consensus/state_processing/src/envelope_processing.rs @@ -241,8 +241,6 @@ pub fn process_execution_payload_envelope<E: EthSpec>( // TODO(gloas): newPayload happens here in the spec, ensure we wire that up correctly process_deposit_requests_post_gloas(state, &execution_requests.deposits, spec)?; - - // TODO(gloas): gotta update these process_withdrawal_requests(state, &execution_requests.withdrawals, spec)?; process_consolidation_requests(state, &execution_requests.consolidations, spec)?; diff --git a/consensus/state_processing/src/per_block_processing/tests.rs b/consensus/state_processing/src/per_block_processing/tests.rs index 96610c2010..0203b33e61 100644 --- a/consensus/state_processing/src/per_block_processing/tests.rs +++ b/consensus/state_processing/src/per_block_processing/tests.rs @@ -1014,7 +1014,7 @@ async fn block_replayer_peeking_state_roots() { let block_replayer = BlockReplayer::new(parent_state, &harness.chain.spec) .state_root_iter(state_root_iter.into_iter()) .no_signature_verification() - .apply_blocks(vec![target_block], None) + .apply_blocks(vec![target_block], vec![], None) .unwrap(); assert_eq!( diff --git a/consensus/state_processing/src/state_advance.rs b/consensus/state_processing/src/state_advance.rs index 11a956bc2a..1114562155 100644 --- a/consensus/state_processing/src/state_advance.rs +++ b/consensus/state_processing/src/state_advance.rs @@ -77,6 +77,11 @@ pub fn partial_state_advance<E: EthSpec>( // (all-zeros) state root. let mut initial_state_root = Some(if state.slot() > state.latest_block_header().slot { state_root_opt.unwrap_or_else(Hash256::zero) + } else if state.slot() == state.latest_block_header().slot + && !state.latest_block_header().state_root.is_zero() + { + // Post-Gloas Full state case. + state.latest_block_header().state_root } else { state_root_opt.ok_or(Error::StateRootNotProvided)? }); diff --git a/consensus/types/src/block/signed_beacon_block.rs b/consensus/types/src/block/signed_beacon_block.rs index aeb3c18d95..b6218ba64d 100644 --- a/consensus/types/src/block/signed_beacon_block.rs +++ b/consensus/types/src/block/signed_beacon_block.rs @@ -14,6 +14,7 @@ use tree_hash::TreeHash; use tree_hash_derive::TreeHash; use crate::{ + ExecutionBlockHash, block::{ BLOB_KZG_COMMITMENTS_INDEX, BeaconBlock, BeaconBlockAltair, BeaconBlockBase, BeaconBlockBellatrix, BeaconBlockBodyBellatrix, BeaconBlockBodyCapella, @@ -365,6 +366,32 @@ impl<E: EthSpec, Payload: AbstractExecPayload<E>> SignedBeaconBlock<E, Payload> format_kzg_commitments(commitments.as_ref()) } + + /// Convenience accessor for the block's bid's `block_hash`. + /// + /// This method returns an error prior to Gloas. + pub fn payload_bid_block_hash(&self) -> Result<ExecutionBlockHash, BeaconStateError> { + self.message() + .body() + .signed_execution_payload_bid() + .map(|bid| bid.message.block_hash) + } + + /// Check if the `parent_hash` in this block's `signed_payload_bid` matches `parent_block_hash`. + /// + /// This function is useful post-Gloas for determining if the parent block is full, *without* + /// necessarily needing access to a beacon state. The passed in `parent_block_hash` MUST be the + /// `block_hash` from the parent beacon block's bid. If the parent beacon state is available + /// this can alternatively be fetched from `state.latest_payload_bid`. + /// + /// This function returns `false` for all blocks prior to Gloas. + pub fn is_parent_block_full(&self, parent_block_hash: ExecutionBlockHash) -> bool { + let Ok(signed_payload_bid) = self.message().body().signed_execution_payload_bid() else { + // Prior to Gloas. + return false; + }; + signed_payload_bid.message.parent_block_hash == parent_block_hash + } } // We can convert pre-Bellatrix blocks without payloads into blocks with payloads. diff --git a/consensus/types/src/execution/mod.rs b/consensus/types/src/execution/mod.rs index a3d4ed8730..591be32b24 100644 --- a/consensus/types/src/execution/mod.rs +++ b/consensus/types/src/execution/mod.rs @@ -12,6 +12,7 @@ mod payload; mod signed_bls_to_execution_change; mod signed_execution_payload_bid; mod signed_execution_payload_envelope; +mod state_payload_status; pub use bls_to_execution_change::BlsToExecutionChange; pub use eth1_data::Eth1Data; @@ -41,3 +42,4 @@ pub use payload::{ pub use signed_bls_to_execution_change::SignedBlsToExecutionChange; pub use signed_execution_payload_bid::SignedExecutionPayloadBid; pub use signed_execution_payload_envelope::SignedExecutionPayloadEnvelope; +pub use state_payload_status::StatePayloadStatus; diff --git a/consensus/types/src/execution/state_payload_status.rs b/consensus/types/src/execution/state_payload_status.rs new file mode 100644 index 0000000000..1661be6060 --- /dev/null +++ b/consensus/types/src/execution/state_payload_status.rs @@ -0,0 +1,18 @@ +use serde::{Deserialize, Serialize}; + +/// Payload status as it applies to a `BeaconState` post-Gloas. +/// +/// A state can either be a post-state for a block (in which case we call it `Pending`) or a +/// payload envelope (`Full`). When handling states it is often necessary to know which of these +/// two variants is required. +/// +/// Note that states at skipped slots could be either `Pending` or `Full`, depending on whether +/// the payload for the most-recently applied block was also applied. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum StatePayloadStatus { + /// For states produced by `process_block` executed on a `BeaconBlock`. + Pending, + /// For states produced by `process_execution_payload` on a `ExecutionPayloadEnvelope`. + Full, +} diff --git a/consensus/types/src/state/beacon_state.rs b/consensus/types/src/state/beacon_state.rs index bd67f469d2..34cfd0ca1c 100644 --- a/consensus/types/src/state/beacon_state.rs +++ b/consensus/types/src/state/beacon_state.rs @@ -36,7 +36,7 @@ use crate::{ execution::{ Eth1Data, ExecutionPayloadHeaderBellatrix, ExecutionPayloadHeaderCapella, ExecutionPayloadHeaderDeneb, ExecutionPayloadHeaderElectra, ExecutionPayloadHeaderFulu, - ExecutionPayloadHeaderRef, ExecutionPayloadHeaderRefMut, + ExecutionPayloadHeaderRef, ExecutionPayloadHeaderRefMut, StatePayloadStatus, }, fork::{Fork, ForkName, ForkVersionDecode, InconsistentFork, map_fork_name}, light_client::consts::{ @@ -1266,6 +1266,24 @@ impl<E: EthSpec> BeaconState<E> { } } + /// Determine the payload status of this state. + /// + /// Prior to Gloas this is always `Pending`. + /// + /// Post-Gloas, the definition of the `StatePayloadStatus` is: + /// + /// - `Full` if this state is the result of envelope processing. + /// - `Pending` if this state is the result of block processing. + pub fn payload_status(&self) -> StatePayloadStatus { + if !self.fork_name_unchecked().gloas_enabled() { + StatePayloadStatus::Pending + } else if self.is_parent_block_full() { + StatePayloadStatus::Full + } else { + StatePayloadStatus::Pending + } + } + /// Return `true` if the validator who produced `slot_signature` is eligible to aggregate. /// /// Spec v0.12.1 From a36b7f3ddbf4bd8456403ef1f945403c7a23affa Mon Sep 17 00:00:00 2001 From: lystopad <oleksandr.lystopad@erigon.tech> Date: Thu, 12 Mar 2026 00:03:05 +0000 Subject: [PATCH 75/81] Schedule Fulu fork for Chiado testnet (#8954) Co-Authored-By: Oleksandr Lystopad <oleksandr.lystopad@erigon.tech> --- .../built_in_network_configs/chiado/config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/eth2_network_config/built_in_network_configs/chiado/config.yaml b/common/eth2_network_config/built_in_network_configs/chiado/config.yaml index f0c04d891a..e1eb022cc9 100644 --- a/common/eth2_network_config/built_in_network_configs/chiado/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/chiado/config.yaml @@ -49,7 +49,7 @@ ELECTRA_FORK_VERSION: 0x0500006f ELECTRA_FORK_EPOCH: 948224 # Thu Mar 6 2025 09:43:40 GMT+0000 # Fulu FULU_FORK_VERSION: 0x0600006f -FULU_FORK_EPOCH: 18446744073709551615 +FULU_FORK_EPOCH: 1353216 # Mon Mar 16 2026 09:33:00 UTC # Gloas GLOAS_FORK_VERSION: 0x0700006f GLOAS_FORK_EPOCH: 18446744073709551615 From e1e97e6df069a67bb687fd02829ac53b6950d378 Mon Sep 17 00:00:00 2001 From: chonghe <44791194+chong-he@users.noreply.github.com> Date: Thu, 12 Mar 2026 09:11:37 +0800 Subject: [PATCH 76/81] Fix proposer lookahead endpoint JSON return type (#8970) Co-Authored-By: Tan Chee Keong <tanck@sigmaprime.io> --- beacon_node/http_api/src/beacon/states.rs | 4 ++-- beacon_node/http_api/tests/tests.rs | 5 +++-- common/eth2/src/lib.rs | 2 +- common/eth2/src/types.rs | 9 +++++++++ 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/beacon_node/http_api/src/beacon/states.rs b/beacon_node/http_api/src/beacon/states.rs index 02ac3f4da7..84ef3c1f26 100644 --- a/beacon_node/http_api/src/beacon/states.rs +++ b/beacon_node/http_api/src/beacon/states.rs @@ -9,7 +9,7 @@ use crate::version::{ use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes, WhenSlotSkipped}; use eth2::types::{ self as api_types, ValidatorBalancesRequestBody, ValidatorId, ValidatorIdentitiesRequestBody, - ValidatorsRequestBody, + ValidatorIndexData, ValidatorsRequestBody, }; use ssz::Encode; use std::sync::Arc; @@ -213,7 +213,7 @@ pub fn get_beacon_state_proposer_lookahead<T: BeaconChainTypes>( ResponseIncludesVersion::Yes(fork_name), execution_optimistic, finalized, - data, + ValidatorIndexData(data), ) .map(|res| warp::reply::json(&res).into_response()), } diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index a97ce01ac1..aed7a6b200 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -1430,10 +1430,11 @@ impl ApiTester { } let state = state_opt.as_mut().expect("result should be none"); - let expected = state.proposer_lookahead().unwrap(); + let expected = state.proposer_lookahead().unwrap().to_vec(); let response = result.unwrap(); - assert_eq!(response.data(), &expected.to_vec()); + // Compare Vec<u64> directly, not Vec<String> + assert_eq!(response.data().0, expected); // Check that the version header is returned in the response let fork_name = state.fork_name(&self.chain.spec).unwrap(); diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 5547ced491..af87af14ba 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -904,7 +904,7 @@ impl BeaconNodeHttpClient { pub async fn get_beacon_states_proposer_lookahead( &self, state_id: StateId, - ) -> Result<Option<ExecutionOptimisticFinalizedBeaconResponse<Vec<u64>>>, Error> { + ) -> Result<Option<ExecutionOptimisticFinalizedBeaconResponse<ValidatorIndexData>>, Error> { let mut path = self.eth_path(V1)?; path.path_segments_mut() diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index 2f86170812..94dff95bc6 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -708,6 +708,15 @@ pub struct DataColumnIndicesQuery { #[serde(transparent)] pub struct ValidatorIndexData(#[serde(with = "serde_utils::quoted_u64_vec")] pub Vec<u64>); +impl<'de, T> ContextDeserialize<'de, T> for ValidatorIndexData { + fn context_deserialize<D>(deserializer: D, _context: T) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + Self::deserialize(deserializer) + } +} + /// Borrowed variant of `ValidatorIndexData`, for serializing/sending. #[derive(Clone, Copy, Serialize)] #[serde(transparent)] From 4b3a9d3d10a6181a1a1588880de133457eb90816 Mon Sep 17 00:00:00 2001 From: Shane K Moore <41407272+shane-moore@users.noreply.github.com> Date: Thu, 12 Mar 2026 02:53:32 -0700 Subject: [PATCH 77/81] Refactor/stream vc vote publishing (#8880) Changes four `ValidatorStore` batch signing methods to return `impl Stream` instead of `Future`. Services consume the stream and publish each batch as it arrives. No behavioral change for lh since `LighthouseValidatorStore` wraps everything in `stream::once` Also replaces anonymous tuples in method signatures with named structs Co-Authored-By: shane-moore <skm1790@gmail.com> Co-Authored-By: Michael Sproul <michaelsproul@users.noreply.github.com> Co-Authored-By: Mac L <mjladson@pm.me> --- Cargo.lock | 1 + testing/web3signer_tests/src/lib.rs | 66 ++- .../http_api/src/tests/keystores.rs | 43 +- .../lighthouse_validator_store/src/lib.rs | 496 ++++++++++++------ .../src/attestation_service.rs | 348 ++++++------ .../src/sync_committee_service.rs | 239 ++++----- validator_client/validator_store/Cargo.toml | 1 + validator_client/validator_store/src/lib.rs | 89 ++-- 8 files changed, 740 insertions(+), 543 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0ca12dce46..1d187d1c68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9710,6 +9710,7 @@ version = "0.1.0" dependencies = [ "bls", "eth2", + "futures", "slashing_protection", "types", ] diff --git a/testing/web3signer_tests/src/lib.rs b/testing/web3signer_tests/src/lib.rs index 4b9432b67b..1f36f8d4ce 100644 --- a/testing/web3signer_tests/src/lib.rs +++ b/testing/web3signer_tests/src/lib.rs @@ -25,6 +25,7 @@ mod tests { use eth2_keystore::KeystoreBuilder; use eth2_network_config::Eth2NetworkConfig; use fixed_bytes::FixedBytesExtended; + use futures::StreamExt; use initialized_validators::{ InitializedValidators, load_pem_certificate, load_pkcs12_identity, }; @@ -50,7 +51,7 @@ mod tests { use types::{attestation::AttestationBase, *}; use url::Url; use validator_store::{ - Error as ValidatorStoreError, SignedBlock, UnsignedBlock, ValidatorStore, + AttestationToSign, Error as ValidatorStoreError, SignedBlock, UnsignedBlock, ValidatorStore, }; /// If the we are unable to reach the Web3Signer HTTP API within this time out then we will @@ -654,13 +655,14 @@ mod tests { .await .assert_signatures_match("attestation", |pubkey, validator_store| async move { let attestation = get_attestation(); - validator_store - .sign_attestations(vec![(0, pubkey, 0, attestation)]) - .await - .unwrap() - .pop() - .unwrap() - .1 + let stream = validator_store.sign_attestations(vec![AttestationToSign { + validator_index: 0, + pubkey, + validator_committee_index: 0, + attestation, + }]); + tokio::pin!(stream); + stream.next().await.unwrap().unwrap().pop().unwrap().1 }) .await .assert_signatures_match("signed_aggregate", |pubkey, validator_store| async move { @@ -879,22 +881,28 @@ mod tests { .await .assert_signatures_match("first_attestation", |pubkey, validator_store| async move { let attestation = first_attestation(); - validator_store - .sign_attestations(vec![(0, pubkey, 0, attestation)]) - .await - .unwrap() - .pop() - .unwrap() - .1 + let stream = validator_store.sign_attestations(vec![AttestationToSign { + validator_index: 0, + pubkey, + validator_committee_index: 0, + attestation, + }]); + tokio::pin!(stream); + stream.next().await.unwrap().unwrap().pop().unwrap().1 }) .await .assert_slashable_attestation_should_sign( "double_vote_attestation", move |pubkey, validator_store| async move { let attestation = double_vote_attestation(); - validator_store - .sign_attestations(vec![(0, pubkey, 0, attestation)]) - .await + let stream = validator_store.sign_attestations(vec![AttestationToSign { + validator_index: 0, + pubkey, + validator_committee_index: 0, + attestation, + }]); + tokio::pin!(stream); + stream.next().await.unwrap() }, slashable_message_should_sign, ) @@ -903,9 +911,14 @@ mod tests { "surrounding_attestation", move |pubkey, validator_store| async move { let attestation = surrounding_attestation(); - validator_store - .sign_attestations(vec![(0, pubkey, 0, attestation)]) - .await + let stream = validator_store.sign_attestations(vec![AttestationToSign { + validator_index: 0, + pubkey, + validator_committee_index: 0, + attestation, + }]); + tokio::pin!(stream); + stream.next().await.unwrap() }, slashable_message_should_sign, ) @@ -914,9 +927,14 @@ mod tests { "surrounded_attestation", move |pubkey, validator_store| async move { let attestation = surrounded_attestation(); - validator_store - .sign_attestations(vec![(0, pubkey, 0, attestation)]) - .await + let stream = validator_store.sign_attestations(vec![AttestationToSign { + validator_index: 0, + pubkey, + validator_committee_index: 0, + attestation, + }]); + tokio::pin!(stream); + stream.next().await.unwrap() }, slashable_message_should_sign, ) diff --git a/validator_client/http_api/src/tests/keystores.rs b/validator_client/http_api/src/tests/keystores.rs index 601b2f1666..eb35075526 100644 --- a/validator_client/http_api/src/tests/keystores.rs +++ b/validator_client/http_api/src/tests/keystores.rs @@ -9,6 +9,7 @@ use eth2::lighthouse_vc::{ types::Web3SignerValidatorRequest, }; use fixed_bytes::FixedBytesExtended; +use futures::StreamExt; use itertools::Itertools; use lighthouse_validator_store::DEFAULT_GAS_LIMIT; use rand::rngs::StdRng; @@ -19,6 +20,7 @@ use std::{collections::HashMap, path::Path}; use tokio::runtime::Handle; use typenum::Unsigned; use types::{Address, attestation::AttestationBase}; +use validator_store::AttestationToSign; use validator_store::ValidatorStore; use zeroize::Zeroizing; @@ -1101,11 +1103,16 @@ async fn generic_migration_test( // Sign attestations on VC1. for (validator_index, attestation) in first_vc_attestations { let public_key = keystore_pubkey(&keystores[validator_index]); - let safe_attestations = tester1 + let stream = tester1 .validator_store - .sign_attestations(vec![(0, public_key, 0, attestation.clone())]) - .await - .unwrap(); + .sign_attestations(vec![AttestationToSign { + validator_index: 0, + pubkey: public_key, + validator_committee_index: 0, + attestation: attestation.clone(), + }]); + tokio::pin!(stream); + let safe_attestations = stream.next().await.unwrap().unwrap(); assert_eq!(safe_attestations.len(), 1); // Compare data only, ignoring signatures which are added during signing. assert_eq!(safe_attestations[0].1.data(), attestation.data()); @@ -1184,10 +1191,16 @@ async fn generic_migration_test( // Sign attestations on the second VC. for (validator_index, attestation, should_succeed) in second_vc_attestations { let public_key = keystore_pubkey(&keystores[validator_index]); - let result = tester2 + let stream = tester2 .validator_store - .sign_attestations(vec![(0, public_key, 0, attestation.clone())]) - .await; + .sign_attestations(vec![AttestationToSign { + validator_index: 0, + pubkey: public_key, + validator_committee_index: 0, + attestation: attestation.clone(), + }]); + tokio::pin!(stream); + let result = stream.next().await.unwrap(); match result { Ok(safe_attestations) => { if should_succeed { @@ -1331,14 +1344,14 @@ async fn delete_concurrent_with_signing() { for j in 0..num_attestations { let att = make_attestation(j, j + 1); for (validator_index, public_key) in thread_pubkeys.iter().enumerate() { - let _ = validator_store - .sign_attestations(vec![( - validator_index as u64, - *public_key, - 0, - att.clone(), - )]) - .await; + let stream = validator_store.sign_attestations(vec![AttestationToSign { + validator_index: validator_index as u64, + pubkey: *public_key, + validator_committee_index: 0, + attestation: att.clone(), + }]); + tokio::pin!(stream); + let _ = stream.next().await; } } }); diff --git a/validator_client/lighthouse_validator_store/src/lib.rs b/validator_client/lighthouse_validator_store/src/lib.rs index 7806482ffb..e8c1cfbc43 100644 --- a/validator_client/lighthouse_validator_store/src/lib.rs +++ b/validator_client/lighthouse_validator_store/src/lib.rs @@ -2,7 +2,7 @@ use account_utils::validator_definitions::{PasswordStorage, ValidatorDefinition} use bls::{PublicKeyBytes, Signature}; use doppelganger_service::DoppelgangerService; use eth2::types::PublishBlockRequest; -use futures::future::join_all; +use futures::{Stream, future::join_all, stream}; use initialized_validators::InitializedValidators; use logging::crit; use parking_lot::{Mutex, RwLock}; @@ -17,7 +17,7 @@ use std::marker::PhantomData; use std::path::Path; use std::sync::Arc; use task_executor::TaskExecutor; -use tracing::{error, info, instrument, warn}; +use tracing::{Instrument, debug, error, info, info_span, instrument, warn}; use types::{ AbstractExecPayload, Address, AggregateAndProof, Attestation, BeaconBlock, BlindedPayload, ChainSpec, ContributionAndProof, Domain, Epoch, EthSpec, ExecutionPayloadEnvelope, Fork, @@ -28,7 +28,8 @@ use types::{ ValidatorRegistrationData, VoluntaryExit, graffiti::GraffitiString, }; use validator_store::{ - DoppelgangerStatus, Error as ValidatorStoreError, ProposalData, SignedBlock, UnsignedBlock, + AggregateToSign, AttestationToSign, ContributionToSign, DoppelgangerStatus, + Error as ValidatorStoreError, ProposalData, SignedBlock, SyncMessageToSign, UnsignedBlock, ValidatorStore, }; @@ -691,6 +692,119 @@ impl<T: SlotClock + 'static, E: EthSpec> LighthouseValidatorStore<T, E> { Ok(safe_attestations) } + + /// Signs an `AggregateAndProof` for a given validator. + /// + /// The resulting `SignedAggregateAndProof` is sent on the aggregation channel and cannot be + /// modified by actors other than the signing validator. + pub async fn produce_signed_aggregate_and_proof( + &self, + validator_pubkey: PublicKeyBytes, + aggregator_index: u64, + aggregate: Attestation<E>, + selection_proof: SelectionProof, + ) -> Result<SignedAggregateAndProof<E>, Error> { + let signing_epoch = aggregate.data().target.epoch; + let signing_context = self.signing_context(Domain::AggregateAndProof, signing_epoch); + + let message = + AggregateAndProof::from_attestation(aggregator_index, aggregate, selection_proof); + + let signing_method = self.doppelganger_checked_signing_method(validator_pubkey)?; + let signature = signing_method + .get_signature::<E, BlindedPayload<E>>( + SignableMessage::SignedAggregateAndProof(message.to_ref()), + signing_context, + &self.spec, + &self.task_executor, + ) + .await?; + + validator_metrics::inc_counter_vec( + &validator_metrics::SIGNED_AGGREGATES_TOTAL, + &[validator_metrics::SUCCESS], + ); + + Ok(SignedAggregateAndProof::from_aggregate_and_proof( + message, signature, + )) + } + + pub async fn produce_sync_committee_signature( + &self, + slot: Slot, + beacon_block_root: Hash256, + validator_index: u64, + validator_pubkey: &PublicKeyBytes, + ) -> Result<SyncCommitteeMessage, Error> { + let signing_epoch = slot.epoch(E::slots_per_epoch()); + let signing_context = self.signing_context(Domain::SyncCommittee, signing_epoch); + + // Bypass `with_validator_signing_method`: sync committee messages are not slashable. + let signing_method = self.doppelganger_bypassed_signing_method(*validator_pubkey)?; + + let signature = signing_method + .get_signature::<E, BlindedPayload<E>>( + SignableMessage::SyncCommitteeSignature { + beacon_block_root, + slot, + }, + signing_context, + &self.spec, + &self.task_executor, + ) + .await + .map_err(Error::SpecificError)?; + + validator_metrics::inc_counter_vec( + &validator_metrics::SIGNED_SYNC_COMMITTEE_MESSAGES_TOTAL, + &[validator_metrics::SUCCESS], + ); + + Ok(SyncCommitteeMessage { + slot, + beacon_block_root, + validator_index, + signature, + }) + } + + pub async fn produce_signed_contribution_and_proof( + &self, + aggregator_index: u64, + aggregator_pubkey: PublicKeyBytes, + contribution: SyncCommitteeContribution<E>, + selection_proof: SyncSelectionProof, + ) -> Result<SignedContributionAndProof<E>, Error> { + let signing_epoch = contribution.slot.epoch(E::slots_per_epoch()); + let signing_context = self.signing_context(Domain::ContributionAndProof, signing_epoch); + + // Bypass `with_validator_signing_method`: sync committee messages are not slashable. + let signing_method = self.doppelganger_bypassed_signing_method(aggregator_pubkey)?; + + let message = ContributionAndProof { + aggregator_index, + contribution, + selection_proof: selection_proof.into(), + }; + + let signature = signing_method + .get_signature::<E, BlindedPayload<E>>( + SignableMessage::SignedContributionAndProof(&message), + signing_context, + &self.spec, + &self.task_executor, + ) + .await + .map_err(Error::SpecificError)?; + + validator_metrics::inc_counter_vec( + &validator_metrics::SIGNED_SYNC_COMMITTEE_CONTRIBUTIONS_TOTAL, + &[validator_metrics::SUCCESS], + ); + + Ok(SignedContributionAndProof { message, signature }) + } } impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore for LighthouseValidatorStore<T, E> { @@ -882,72 +996,83 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore for LighthouseValidatorS } } - async fn sign_attestations( + fn sign_attestations( self: &Arc<Self>, - mut attestations: Vec<(u64, PublicKeyBytes, usize, Attestation<Self::E>)>, - ) -> Result<Vec<(u64, Attestation<E>)>, Error> { - // Sign all attestations concurrently. - let signing_futures = - attestations - .iter_mut() - .map(|(_, pubkey, validator_committee_index, attestation)| { + mut attestations: Vec<AttestationToSign<E>>, + ) -> impl Stream<Item = Result<Vec<(u64, Attestation<E>)>, Error>> + Send { + let store = self.clone(); + stream::once(async move { + // Sign all attestations concurrently. + let signing_futures = attestations.iter_mut().map( + |AttestationToSign { + pubkey, + validator_committee_index, + attestation, + .. + }| { let pubkey = *pubkey; let validator_committee_index = *validator_committee_index; + let store = store.clone(); async move { - self.sign_attestation_no_slashing_protection( - pubkey, - validator_committee_index, - attestation, - ) - .await + store + .sign_attestation_no_slashing_protection( + pubkey, + validator_committee_index, + attestation, + ) + .await } - }); + }, + ); - // Execute all signing in parallel. - let results: Vec<_> = join_all(signing_futures).await; + // Execute all signing in parallel. + let results: Vec<_> = join_all(signing_futures).await; - // Collect successfully signed attestations and log errors. - let mut signed_attestations = Vec::with_capacity(attestations.len()); - for (result, (validator_index, pubkey, _, attestation)) in - results.into_iter().zip(attestations.into_iter()) - { - match result { - Ok(()) => { - signed_attestations.push((validator_index, attestation, pubkey)); - } - Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { - warn!( - info = "a validator may have recently been removed from this VC", - ?pubkey, - "Missing pubkey for attestation" - ); - } - Err(e) => { - crit!( - error = ?e, - "Failed to sign attestation" - ); + // Collect successfully signed attestations and log errors. + let mut signed_attestations = Vec::with_capacity(attestations.len()); + for (result, att) in results.into_iter().zip(attestations.into_iter()) { + match result { + Ok(()) => { + signed_attestations.push(( + att.validator_index, + att.attestation, + att.pubkey, + )); + } + Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { + warn!( + info = "a validator may have recently been removed from this VC", + ?pubkey, + "Missing pubkey for attestation" + ); + } + Err(e) => { + crit!( + error = ?e, + "Failed to sign attestation" + ); + } } } - } - if signed_attestations.is_empty() { - return Ok(vec![]); - } + if signed_attestations.is_empty() { + return Ok(vec![]); + } - // Check slashing protection and insert into database. Use a dedicated blocking thread - // to avoid clogging the async executor with blocking database I/O. - let validator_store = self.clone(); - let safe_attestations = self - .task_executor - .spawn_blocking_handle( - move || validator_store.slashing_protect_attestations(signed_attestations), - "slashing_protect_attestations", - ) - .ok_or(Error::ExecutorError)? - .await - .map_err(|_| Error::ExecutorError)??; - Ok(safe_attestations) + // Check slashing protection and insert into database. Use a dedicated blocking + // thread to avoid clogging the async executor with blocking database I/O. + let validator_store = store.clone(); + let safe_attestations = store + .task_executor + .spawn_blocking_handle( + move || validator_store.slashing_protect_attestations(signed_attestations), + "slashing_protect_attestations", + ) + .ok_or(Error::ExecutorError)? + .await + .map_err(|_| Error::ExecutorError)??; + Ok(safe_attestations) + }) } async fn sign_validator_registration_data( @@ -979,43 +1104,6 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore for LighthouseValidatorS }) } - /// Signs an `AggregateAndProof` for a given validator. - /// - /// The resulting `SignedAggregateAndProof` is sent on the aggregation channel and cannot be - /// modified by actors other than the signing validator. - async fn produce_signed_aggregate_and_proof( - &self, - validator_pubkey: PublicKeyBytes, - aggregator_index: u64, - aggregate: Attestation<E>, - selection_proof: SelectionProof, - ) -> Result<SignedAggregateAndProof<E>, Error> { - let signing_epoch = aggregate.data().target.epoch; - let signing_context = self.signing_context(Domain::AggregateAndProof, signing_epoch); - - let message = - AggregateAndProof::from_attestation(aggregator_index, aggregate, selection_proof); - - let signing_method = self.doppelganger_checked_signing_method(validator_pubkey)?; - let signature = signing_method - .get_signature::<E, BlindedPayload<E>>( - SignableMessage::SignedAggregateAndProof(message.to_ref()), - signing_context, - &self.spec, - &self.task_executor, - ) - .await?; - - validator_metrics::inc_counter_vec( - &validator_metrics::SIGNED_AGGREGATES_TOTAL, - &[validator_metrics::SUCCESS], - ); - - Ok(SignedAggregateAndProof::from_aggregate_and_proof( - message, signature, - )) - } - /// Produces a `SelectionProof` for the `slot`, signed by with corresponding secret key to /// `validator_pubkey`. async fn produce_selection_proof( @@ -1090,80 +1178,172 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore for LighthouseValidatorS Ok(signature.into()) } - async fn produce_sync_committee_signature( - &self, - slot: Slot, - beacon_block_root: Hash256, - validator_index: u64, - validator_pubkey: &PublicKeyBytes, - ) -> Result<SyncCommitteeMessage, Error> { - let signing_epoch = slot.epoch(E::slots_per_epoch()); - let signing_context = self.signing_context(Domain::SyncCommittee, signing_epoch); - - // Bypass `with_validator_signing_method`: sync committee messages are not slashable. - let signing_method = self.doppelganger_bypassed_signing_method(*validator_pubkey)?; - - let signature = signing_method - .get_signature::<E, BlindedPayload<E>>( - SignableMessage::SyncCommitteeSignature { - beacon_block_root, - slot, + fn sign_aggregate_and_proofs( + self: &Arc<Self>, + aggregates: Vec<AggregateToSign<E>>, + ) -> impl Stream<Item = Result<Vec<SignedAggregateAndProof<E>>, Error>> + Send { + let store = self.clone(); + let count = aggregates.len(); + stream::once(async move { + let signing_futures = aggregates.into_iter().map( + |AggregateToSign { + pubkey, + aggregator_index, + aggregate, + selection_proof, + }| { + let store = store.clone(); + async move { + let result = store + .produce_signed_aggregate_and_proof( + pubkey, + aggregator_index, + aggregate, + selection_proof, + ) + .await; + (pubkey, result) + } }, - signing_context, - &self.spec, - &self.task_executor, - ) - .await - .map_err(Error::SpecificError)?; + ); - validator_metrics::inc_counter_vec( - &validator_metrics::SIGNED_SYNC_COMMITTEE_MESSAGES_TOTAL, - &[validator_metrics::SUCCESS], - ); + let results = join_all(signing_futures) + .instrument(info_span!("sign_aggregates", count)) + .await; - Ok(SyncCommitteeMessage { - slot, - beacon_block_root, - validator_index, - signature, + let mut signed = Vec::with_capacity(results.len()); + for (pubkey, result) in results { + match result { + Ok(agg) => signed.push(agg), + Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { + // A pubkey can be missing when a validator was recently + // removed via the API. + debug!(?pubkey, "Missing pubkey for aggregate"); + } + Err(e) => { + crit!(error = ?e, pubkey = ?pubkey, "Failed to sign aggregate"); + } + } + } + Ok(signed) }) } - async fn produce_signed_contribution_and_proof( - &self, - aggregator_index: u64, - aggregator_pubkey: PublicKeyBytes, - contribution: SyncCommitteeContribution<E>, - selection_proof: SyncSelectionProof, - ) -> Result<SignedContributionAndProof<E>, Error> { - let signing_epoch = contribution.slot.epoch(E::slots_per_epoch()); - let signing_context = self.signing_context(Domain::ContributionAndProof, signing_epoch); + fn sign_sync_committee_signatures( + self: &Arc<Self>, + messages: Vec<SyncMessageToSign>, + ) -> impl Stream<Item = Result<Vec<SyncCommitteeMessage>, Error>> + Send { + let store = self.clone(); + let count = messages.len(); + stream::once(async move { + let signing_futures = messages.into_iter().map( + |SyncMessageToSign { + slot, + beacon_block_root, + validator_index, + pubkey, + }| { + let store = store.clone(); + async move { + let result = store + .produce_sync_committee_signature( + slot, + beacon_block_root, + validator_index, + &pubkey, + ) + .await; + (pubkey, validator_index, slot, result) + } + }, + ); - // Bypass `with_validator_signing_method`: sync committee messages are not slashable. - let signing_method = self.doppelganger_bypassed_signing_method(aggregator_pubkey)?; + let results = join_all(signing_futures) + .instrument(info_span!("sign_sync_signatures", count)) + .await; - let message = ContributionAndProof { - aggregator_index, - contribution, - selection_proof: selection_proof.into(), - }; + let mut signed = Vec::with_capacity(results.len()); + for (_pubkey, validator_index, slot, result) in results { + match result { + Ok(sig) => signed.push(sig), + Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { + // A pubkey can be missing when a validator was recently + // removed via the API. + debug!( + ?pubkey, + validator_index, + %slot, + "Missing pubkey for sync committee signature" + ); + } + Err(e) => { + crit!( + validator_index, + %slot, + error = ?e, + "Failed to sign sync committee signature" + ); + } + } + } + Ok(signed) + }) + } - let signature = signing_method - .get_signature::<E, BlindedPayload<E>>( - SignableMessage::SignedContributionAndProof(&message), - signing_context, - &self.spec, - &self.task_executor, - ) - .await - .map_err(Error::SpecificError)?; + fn sign_sync_committee_contributions( + self: &Arc<Self>, + contributions: Vec<ContributionToSign<E>>, + ) -> impl Stream<Item = Result<Vec<SignedContributionAndProof<E>>, Error>> + Send { + let store = self.clone(); + let count = contributions.len(); + stream::once(async move { + let signing_futures = contributions.into_iter().map( + |ContributionToSign { + aggregator_index, + aggregator_pubkey, + contribution, + selection_proof, + }| { + let store = store.clone(); + let slot = contribution.slot; + async move { + let result = store + .produce_signed_contribution_and_proof( + aggregator_index, + aggregator_pubkey, + contribution, + selection_proof, + ) + .await; + (slot, result) + } + }, + ); - validator_metrics::inc_counter_vec( - &validator_metrics::SIGNED_SYNC_COMMITTEE_CONTRIBUTIONS_TOTAL, - &[validator_metrics::SUCCESS], - ); + let results = join_all(signing_futures) + .instrument(info_span!("sign_sync_contributions", count)) + .await; - Ok(SignedContributionAndProof { message, signature }) + let mut signed = Vec::with_capacity(results.len()); + for (slot, result) in results { + match result { + Ok(contribution) => signed.push(contribution), + Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { + // A pubkey can be missing when a validator was recently + // removed via the API. + debug!(?pubkey, %slot, "Missing pubkey for sync contribution"); + } + Err(e) => { + crit!( + %slot, + error = ?e, + "Unable to sign sync committee contribution" + ); + } + } + } + Ok(signed) + }) } /// Prune the slashing protection database so that it remains performant. diff --git a/validator_client/validator_services/src/attestation_service.rs b/validator_client/validator_services/src/attestation_service.rs index a9d5283312..fe808efd88 100644 --- a/validator_client/validator_services/src/attestation_service.rs +++ b/validator_client/validator_services/src/attestation_service.rs @@ -1,6 +1,6 @@ use crate::duties_service::{DutiesService, DutyAndProof}; use beacon_node_fallback::{ApiTopic, BeaconNodeFallback, beacon_head_monitor::HeadEvent}; -use futures::future::join_all; +use futures::StreamExt; use logging::crit; use slot_clock::SlotClock; use std::collections::HashMap; @@ -13,7 +13,7 @@ use tokio::time::{Duration, Instant, sleep, sleep_until}; use tracing::{Instrument, debug, error, info, info_span, instrument, warn}; use tree_hash::TreeHash; use types::{Attestation, AttestationData, ChainSpec, CommitteeIndex, EthSpec, Hash256, Slot}; -use validator_store::{Error as ValidatorStoreError, ValidatorStore}; +use validator_store::{AggregateToSign, AttestationToSign, ValidatorStore}; /// Builds an `AttestationService`. #[derive(Default)] @@ -560,12 +560,12 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> AttestationService<S, } }; - attestations_to_sign.push(( - duty.validator_index, - duty.pubkey, - duty.validator_committee_index as usize, + attestations_to_sign.push(AttestationToSign { + validator_index: duty.validator_index, + pubkey: duty.pubkey, + validator_committee_index: duty.validator_committee_index as usize, attestation, - )); + }); } if attestations_to_sign.is_empty() { @@ -573,83 +573,95 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> AttestationService<S, return Ok(()); } - // Sign and check all attestations (includes slashing protection). - let safe_attestations = self - .validator_store - .sign_attestations(attestations_to_sign) - .await - .map_err(|e| format!("Failed to sign attestations: {e:?}"))?; + let attestation_stream = self.validator_store.sign_attestations(attestations_to_sign); + tokio::pin!(attestation_stream); - if safe_attestations.is_empty() { - warn!("No attestations were published"); - return Ok(()); - } let fork_name = self .chain_spec .fork_name_at_slot::<S::E>(attestation_data.slot); - let single_attestations = safe_attestations - .iter() - .filter_map(|(i, a)| { - match a.to_single_attestation_with_attester_index(*i) { - Ok(a) => Some(a), - Err(e) => { - // This shouldn't happen unless BN and VC are out of sync with - // respect to the Electra fork. - error!( - error = ?e, + // Publish each batch as it arrives from the stream. + let mut received_non_empty_batch = false; + while let Some(result) = attestation_stream.next().await { + match result { + Ok(batch) if !batch.is_empty() => { + received_non_empty_batch = true; + + let single_attestations = batch + .iter() + .filter_map(|(attester_index, attestation)| { + match attestation + .to_single_attestation_with_attester_index(*attester_index) + { + Ok(single_attestation) => Some(single_attestation), + Err(e) => { + // This shouldn't happen unless BN and VC are out of sync with + // respect to the Electra fork. + error!( + error = ?e, + committee_index = attestation_data.index, + slot = slot.as_u64(), + "type" = "unaggregated", + "Unable to convert to SingleAttestation" + ); + None + } + } + }) + .collect::<Vec<_>>(); + let single_attestations = &single_attestations; + let validator_indices = single_attestations + .iter() + .map(|att| att.attester_index) + .collect::<Vec<_>>(); + let published_count = single_attestations.len(); + + // Post the attestations to the BN. + match self + .beacon_nodes + .request(ApiTopic::Attestations, |beacon_node| async move { + let _timer = validator_metrics::start_timer_vec( + &validator_metrics::ATTESTATION_SERVICE_TIMES, + &[validator_metrics::ATTESTATIONS_HTTP_POST], + ); + + beacon_node + .post_beacon_pool_attestations_v2::<S::E>( + single_attestations.clone(), + fork_name, + ) + .await + }) + .instrument(info_span!("publish_attestations", count = published_count)) + .await + { + Ok(()) => info!( + count = published_count, + validator_indices = ?validator_indices, + head_block = ?attestation_data.beacon_block_root, + committee_index = attestation_data.index, + slot = attestation_data.slot.as_u64(), + "type" = "unaggregated", + "Successfully published attestations" + ), + Err(e) => error!( + error = %e, committee_index = attestation_data.index, slot = slot.as_u64(), "type" = "unaggregated", - "Unable to convert to SingleAttestation" - ); - None + "Unable to publish attestations" + ), } } - }) - .collect::<Vec<_>>(); - let single_attestations = &single_attestations; - let validator_indices = single_attestations - .iter() - .map(|att| att.attester_index) - .collect::<Vec<_>>(); - let published_count = single_attestations.len(); + Err(e) => { + crit!(error = ?e, "Failed to sign attestations"); + } + _ => {} + } + } - // Post the attestations to the BN. - match self - .beacon_nodes - .request(ApiTopic::Attestations, |beacon_node| async move { - let _timer = validator_metrics::start_timer_vec( - &validator_metrics::ATTESTATION_SERVICE_TIMES, - &[validator_metrics::ATTESTATIONS_HTTP_POST], - ); - - beacon_node - .post_beacon_pool_attestations_v2::<S::E>( - single_attestations.clone(), - fork_name, - ) - .await - }) - .instrument(info_span!("publish_attestations", count = published_count)) - .await - { - Ok(()) => info!( - count = published_count, - validator_indices = ?validator_indices, - head_block = ?attestation_data.beacon_block_root, - committee_index = attestation_data.index, - slot = attestation_data.slot.as_u64(), - "type" = "unaggregated", - "Successfully published attestations" - ), - Err(e) => error!( - error = %e, - committee_index = attestation_data.index, - slot = slot.as_u64(), - "type" = "unaggregated", - "Unable to publish attestations" - ), + if !received_non_empty_batch { + warn!("No attestations were published"); } Ok(()) @@ -725,113 +737,103 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> AttestationService<S, .await .map_err(|e| e.to_string())?; - // Create futures to produce the signed aggregated attestations. - let signing_futures = validator_duties.iter().map(|duty_and_proof| async move { - let duty = &duty_and_proof.duty; - let selection_proof = duty_and_proof.selection_proof.as_ref()?; - - if !duty.match_attestation_data::<S::E>(attestation_data, &self.chain_spec) { - crit!("Inconsistent validator duties during signing"); - return None; - } - - match self - .validator_store - .produce_signed_aggregate_and_proof( - duty.pubkey, - duty.validator_index, - aggregated_attestation.clone(), - selection_proof.clone(), - ) - .await - { - Ok(aggregate) => Some(aggregate), - Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { - // A pubkey can be missing when a validator was recently - // removed via the API. - debug!(?pubkey, "Missing pubkey for aggregate"); - None - } - Err(e) => { - crit!( - error = ?e, - pubkey = ?duty.pubkey, - "Failed to sign aggregate" - ); - None - } - } - }); - - // Execute all the futures in parallel, collecting any successful results. - let aggregator_count = validator_duties + // Build the batch of aggregates to sign. + let aggregates_to_sign: Vec<_> = validator_duties .iter() - .filter(|d| d.selection_proof.is_some()) - .count(); - let signed_aggregate_and_proofs = join_all(signing_futures) - .instrument(info_span!("sign_aggregates", count = aggregator_count)) - .await - .into_iter() - .flatten() - .collect::<Vec<_>>(); + .filter_map(|duty_and_proof| { + let duty = &duty_and_proof.duty; + let selection_proof = duty_and_proof.selection_proof.as_ref()?; - if !signed_aggregate_and_proofs.is_empty() { - let signed_aggregate_and_proofs_slice = signed_aggregate_and_proofs.as_slice(); - match self - .beacon_nodes - .first_success(|beacon_node| async move { - let _timer = validator_metrics::start_timer_vec( - &validator_metrics::ATTESTATION_SERVICE_TIMES, - &[validator_metrics::AGGREGATES_HTTP_POST], - ); - if fork_name.electra_enabled() { - beacon_node - .post_validator_aggregate_and_proof_v2( - signed_aggregate_and_proofs_slice, - fork_name, - ) - .await - } else { - beacon_node - .post_validator_aggregate_and_proof_v1( - signed_aggregate_and_proofs_slice, - ) - .await - } + if !duty.match_attestation_data::<S::E>(attestation_data, &self.chain_spec) { + crit!("Inconsistent validator duties during signing"); + return None; + } + + Some(AggregateToSign { + pubkey: duty.pubkey, + aggregator_index: duty.validator_index, + aggregate: aggregated_attestation.clone(), + selection_proof: selection_proof.clone(), }) - .instrument(info_span!( - "publish_aggregates", - count = signed_aggregate_and_proofs.len() - )) - .await - { - Ok(()) => { - for signed_aggregate_and_proof in signed_aggregate_and_proofs { - let attestation = signed_aggregate_and_proof.message().aggregate(); - info!( - aggregator = signed_aggregate_and_proof.message().aggregator_index(), - signatures = attestation.num_set_aggregation_bits(), - head_block = format!("{:?}", attestation.data().beacon_block_root), - committee_index = attestation.committee_index(), - slot = attestation.data().slot.as_u64(), - "type" = "aggregated", - "Successfully published attestation" - ); + }) + .collect(); + + // Sign aggregates. Returns a stream of batches. + let aggregate_stream = self + .validator_store + .sign_aggregate_and_proofs(aggregates_to_sign); + tokio::pin!(aggregate_stream); + + // Publish each batch as it arrives from the stream. + while let Some(result) = aggregate_stream.next().await { + match result { + Ok(batch) if !batch.is_empty() => { + let signed_aggregate_and_proofs = batch.as_slice(); + match self + .beacon_nodes + .first_success(|beacon_node| async move { + let _timer = validator_metrics::start_timer_vec( + &validator_metrics::ATTESTATION_SERVICE_TIMES, + &[validator_metrics::AGGREGATES_HTTP_POST], + ); + if fork_name.electra_enabled() { + beacon_node + .post_validator_aggregate_and_proof_v2( + signed_aggregate_and_proofs, + fork_name, + ) + .await + } else { + beacon_node + .post_validator_aggregate_and_proof_v1( + signed_aggregate_and_proofs, + ) + .await + } + }) + .instrument(info_span!( + "publish_aggregates", + count = signed_aggregate_and_proofs.len() + )) + .await + { + Ok(()) => { + for signed_aggregate_and_proof in signed_aggregate_and_proofs { + let attestation = signed_aggregate_and_proof.message().aggregate(); + info!( + aggregator = + signed_aggregate_and_proof.message().aggregator_index(), + signatures = attestation.num_set_aggregation_bits(), + head_block = + format!("{:?}", attestation.data().beacon_block_root), + committee_index = attestation.committee_index(), + slot = attestation.data().slot.as_u64(), + "type" = "aggregated", + "Successfully published attestation" + ); + } + } + Err(e) => { + for signed_aggregate_and_proof in signed_aggregate_and_proofs { + let attestation = &signed_aggregate_and_proof.message().aggregate(); + crit!( + error = %e, + aggregator = signed_aggregate_and_proof + .message() + .aggregator_index(), + committee_index = attestation.committee_index(), + slot = attestation.data().slot.as_u64(), + "type" = "aggregated", + "Failed to publish attestation" + ); + } + } } } Err(e) => { - for signed_aggregate_and_proof in signed_aggregate_and_proofs { - let attestation = &signed_aggregate_and_proof.message().aggregate(); - crit!( - error = %e, - aggregator = signed_aggregate_and_proof.message().aggregator_index(), - committee_index = attestation.committee_index(), - slot = attestation.data().slot.as_u64(), - "type" = "aggregated", - "Failed to publish attestation" - ); - } + crit!(error = ?e, "Failed to sign aggregates"); } + _ => {} } } diff --git a/validator_client/validator_services/src/sync_committee_service.rs b/validator_client/validator_services/src/sync_committee_service.rs index 59e8524a1a..26ce052ea0 100644 --- a/validator_client/validator_services/src/sync_committee_service.rs +++ b/validator_client/validator_services/src/sync_committee_service.rs @@ -2,8 +2,8 @@ use crate::duties_service::DutiesService; use beacon_node_fallback::{ApiTopic, BeaconNodeFallback}; use bls::PublicKeyBytes; use eth2::types::BlockId; +use futures::StreamExt; use futures::future::FutureExt; -use futures::future::join_all; use logging::crit; use slot_clock::SlotClock; use std::collections::HashMap; @@ -17,7 +17,7 @@ use types::{ ChainSpec, EthSpec, Hash256, Slot, SyncCommitteeSubscription, SyncContributionData, SyncDuty, SyncSelectionProof, SyncSubnetId, }; -use validator_store::{Error as ValidatorStoreError, ValidatorStore}; +use validator_store::{ContributionToSign, SyncMessageToSign, ValidatorStore}; pub const SUBSCRIPTION_LOOKAHEAD_EPOCHS: u64 = 4; @@ -247,78 +247,57 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> SyncCommitteeService<S beacon_block_root: Hash256, validator_duties: Vec<SyncDuty>, ) -> Result<(), ()> { - // Create futures to produce sync committee signatures. - let signature_futures = validator_duties.iter().map(|duty| async move { - match self - .validator_store - .produce_sync_committee_signature( - slot, - beacon_block_root, - duty.validator_index, - &duty.pubkey, - ) - .await - { - Ok(signature) => Some(signature), - Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { - // A pubkey can be missing when a validator was recently - // removed via the API. - debug!( - ?pubkey, - validator_index = duty.validator_index, - %slot, - "Missing pubkey for sync committee signature" - ); - None + let messages_to_sign: Vec<_> = validator_duties + .iter() + .map(|duty| SyncMessageToSign { + slot, + beacon_block_root, + validator_index: duty.validator_index, + pubkey: duty.pubkey, + }) + .collect(); + + let signature_stream = self + .validator_store + .sign_sync_committee_signatures(messages_to_sign); + tokio::pin!(signature_stream); + + while let Some(result) = signature_stream.next().await { + match result { + Ok(committee_signatures) if !committee_signatures.is_empty() => { + let committee_signatures = &committee_signatures; + match self + .beacon_nodes + .request(ApiTopic::SyncCommittee, |beacon_node| async move { + beacon_node + .post_beacon_pool_sync_committee_signatures(committee_signatures) + .await + }) + .instrument(info_span!( + "publish_sync_signatures", + count = committee_signatures.len() + )) + .await + { + Ok(()) => info!( + count = committee_signatures.len(), + head_block = ?beacon_block_root, + %slot, + "Successfully published sync committee messages" + ), + Err(e) => error!( + %slot, + error = %e, + "Unable to publish sync committee messages" + ), + } } Err(e) => { - crit!( - validator_index = duty.validator_index, - %slot, - error = ?e, - "Failed to sign sync committee signature" - ); - None + crit!(%slot, error = ?e, "Failed to sign sync committee signatures"); } + _ => {} } - }); - - // Execute all the futures in parallel, collecting any successful results. - let committee_signatures = &join_all(signature_futures) - .instrument(info_span!( - "sign_sync_signatures", - count = validator_duties.len() - )) - .await - .into_iter() - .flatten() - .collect::<Vec<_>>(); - - self.beacon_nodes - .request(ApiTopic::SyncCommittee, |beacon_node| async move { - beacon_node - .post_beacon_pool_sync_committee_signatures(committee_signatures) - .await - }) - .instrument(info_span!( - "publish_sync_signatures", - count = committee_signatures.len() - )) - .await - .map_err(|e| { - error!( - %slot, - error = %e, - "Unable to publish sync committee messages" - ); - })?; - - info!( - count = committee_signatures.len(), - head_block = ?beacon_block_root, - %slot, - "Successfully published sync committee messages" - ); + } Ok(()) } @@ -389,77 +368,61 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> SyncCommitteeService<S })? .data; - // Create futures to produce signed contributions. - let aggregator_count = subnet_aggregators.len(); - let signature_futures = subnet_aggregators.into_iter().map( - |(aggregator_index, aggregator_pk, selection_proof)| async move { - match self - .validator_store - .produce_signed_contribution_and_proof( - aggregator_index, - aggregator_pk, - contribution.clone(), - selection_proof, - ) - .await - { - Ok(signed_contribution) => Some(signed_contribution), - Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { - // A pubkey can be missing when a validator was recently - // removed via the API. - debug!(?pubkey, %slot, "Missing pubkey for sync contribution"); - None - } - Err(e) => { - crit!( + let contributions_to_sign: Vec<_> = subnet_aggregators + .into_iter() + .map( + |(aggregator_index, aggregator_pk, selection_proof)| ContributionToSign { + aggregator_index, + aggregator_pubkey: aggregator_pk, + contribution: contribution.clone(), + selection_proof, + }, + ) + .collect(); + + let contribution_stream = self + .validator_store + .sign_sync_committee_contributions(contributions_to_sign); + tokio::pin!(contribution_stream); + + while let Some(result) = contribution_stream.next().await { + match result { + Ok(signed_contributions) if !signed_contributions.is_empty() => { + let signed_contributions = &signed_contributions; + // Publish to the beacon node. + match self + .beacon_nodes + .first_success(|beacon_node| async move { + beacon_node + .post_validator_contribution_and_proofs(signed_contributions) + .await + }) + .instrument(info_span!( + "publish_sync_contributions", + count = signed_contributions.len() + )) + .await + { + Ok(()) => info!( + subnet = %subnet_id, + beacon_block_root = %beacon_block_root, + num_signers = contribution.aggregation_bits.num_set_bits(), %slot, - error = ?e, - "Unable to sign sync committee contribution" - ); - None + "Successfully published sync contributions" + ), + Err(e) => error!( + %slot, + error = %e, + "Unable to publish signed contributions and proofs" + ), } } - }, - ); - - // Execute all the futures in parallel, collecting any successful results. - let signed_contributions = &join_all(signature_futures) - .instrument(info_span!( - "sign_sync_contributions", - count = aggregator_count - )) - .await - .into_iter() - .flatten() - .collect::<Vec<_>>(); - - // Publish to the beacon node. - self.beacon_nodes - .first_success(|beacon_node| async move { - beacon_node - .post_validator_contribution_and_proofs(signed_contributions) - .await - }) - .instrument(info_span!( - "publish_sync_contributions", - count = signed_contributions.len() - )) - .await - .map_err(|e| { - error!( - %slot, - error = %e, - "Unable to publish signed contributions and proofs" - ); - })?; - - info!( - subnet = %subnet_id, - beacon_block_root = %beacon_block_root, - num_signers = contribution.aggregation_bits.num_set_bits(), - %slot, - "Successfully published sync contributions" - ); + Err(e) => { + crit!(%slot, error = ?e, "Failed to sign sync committee contributions"); + } + _ => {} + } + } Ok(()) } diff --git a/validator_client/validator_store/Cargo.toml b/validator_client/validator_store/Cargo.toml index 8b1879c837..2c6a68d494 100644 --- a/validator_client/validator_store/Cargo.toml +++ b/validator_client/validator_store/Cargo.toml @@ -7,5 +7,6 @@ authors = ["Sigma Prime <contact@sigmaprime.io>"] [dependencies] bls = { workspace = true } eth2 = { workspace = true } +futures = { workspace = true } slashing_protection = { workspace = true } types = { workspace = true } diff --git a/validator_client/validator_store/src/lib.rs b/validator_client/validator_store/src/lib.rs index 87ab669e8d..da0b33de18 100644 --- a/validator_client/validator_store/src/lib.rs +++ b/validator_client/validator_store/src/lib.rs @@ -1,5 +1,6 @@ use bls::{PublicKeyBytes, Signature}; use eth2::types::{FullBlockContents, PublishBlockRequest}; +use futures::Stream; use slashing_protection::NotSafe; use std::fmt::Debug; use std::future::Future; @@ -32,6 +33,38 @@ impl<T> From<T> for Error<T> { } } +/// Input for batch attestation signing +pub struct AttestationToSign<E: EthSpec> { + pub validator_index: u64, + pub pubkey: PublicKeyBytes, + pub validator_committee_index: usize, + pub attestation: Attestation<E>, +} + +/// Input for batch aggregate signing +pub struct AggregateToSign<E: EthSpec> { + pub pubkey: PublicKeyBytes, + pub aggregator_index: u64, + pub aggregate: Attestation<E>, + pub selection_proof: SelectionProof, +} + +/// Input for batch sync committee message signing +pub struct SyncMessageToSign { + pub slot: Slot, + pub beacon_block_root: Hash256, + pub validator_index: u64, + pub pubkey: PublicKeyBytes, +} + +/// Input for batch sync committee contribution signing +pub struct ContributionToSign<E: EthSpec> { + pub aggregator_index: u64, + pub aggregator_pubkey: PublicKeyBytes, + pub contribution: SyncCommitteeContribution<E>, + pub selection_proof: SyncSelectionProof, +} + /// A helper struct, used for passing data from the validator store to services. pub struct ProposalData { pub validator_index: Option<u64>, @@ -106,13 +139,9 @@ pub trait ValidatorStore: Send + Sync { /// Sign a batch of `attestations` and apply slashing protection to them. /// - /// Only successfully signed attestations that pass slashing protection are returned, along with - /// the validator index of the signer. Eventually this will be replaced by `SingleAttestation` - /// use. - /// - /// Input: - /// - /// * Vec of (validator_index, pubkey, validator_committee_index, attestation). + /// Returns a stream of batches of successfully signed attestations. Each batch contains + /// attestations that passed slashing protection, along with the validator index of the signer. + /// Eventually this will be replaced by `SingleAttestation` use. /// /// Output: /// @@ -120,26 +149,14 @@ pub trait ValidatorStore: Send + Sync { #[allow(clippy::type_complexity)] fn sign_attestations( self: &Arc<Self>, - attestations: Vec<(u64, PublicKeyBytes, usize, Attestation<Self::E>)>, - ) -> impl Future<Output = Result<Vec<(u64, Attestation<Self::E>)>, Error<Self::Error>>> + Send; + attestations: Vec<AttestationToSign<Self::E>>, + ) -> impl Stream<Item = Result<Vec<(u64, Attestation<Self::E>)>, Error<Self::Error>>> + Send; fn sign_validator_registration_data( &self, validator_registration_data: ValidatorRegistrationData, ) -> impl Future<Output = Result<SignedValidatorRegistrationData, Error<Self::Error>>> + Send; - /// Signs an `AggregateAndProof` for a given validator. - /// - /// The resulting `SignedAggregateAndProof` is sent on the aggregation channel and cannot be - /// modified by actors other than the signing validator. - fn produce_signed_aggregate_and_proof( - &self, - validator_pubkey: PublicKeyBytes, - aggregator_index: u64, - aggregate: Attestation<Self::E>, - selection_proof: SelectionProof, - ) -> impl Future<Output = Result<SignedAggregateAndProof<Self::E>, Error<Self::Error>>> + Send; - /// Produces a `SelectionProof` for the `slot`, signed by with corresponding secret key to /// `validator_pubkey`. fn produce_selection_proof( @@ -156,21 +173,23 @@ pub trait ValidatorStore: Send + Sync { subnet_id: SyncSubnetId, ) -> impl Future<Output = Result<SyncSelectionProof, Error<Self::Error>>> + Send; - fn produce_sync_committee_signature( - &self, - slot: Slot, - beacon_block_root: Hash256, - validator_index: u64, - validator_pubkey: &PublicKeyBytes, - ) -> impl Future<Output = Result<SyncCommitteeMessage, Error<Self::Error>>> + Send; + /// Sign a batch of aggregate and proofs and return results as a stream of batches. + fn sign_aggregate_and_proofs( + self: &Arc<Self>, + aggregates: Vec<AggregateToSign<Self::E>>, + ) -> impl Stream<Item = Result<Vec<SignedAggregateAndProof<Self::E>>, Error<Self::Error>>> + Send; - fn produce_signed_contribution_and_proof( - &self, - aggregator_index: u64, - aggregator_pubkey: PublicKeyBytes, - contribution: SyncCommitteeContribution<Self::E>, - selection_proof: SyncSelectionProof, - ) -> impl Future<Output = Result<SignedContributionAndProof<Self::E>, Error<Self::Error>>> + Send; + /// Sign a batch of sync committee messages and return results as a stream of batches. + fn sign_sync_committee_signatures( + self: &Arc<Self>, + messages: Vec<SyncMessageToSign>, + ) -> impl Stream<Item = Result<Vec<SyncCommitteeMessage>, Error<Self::Error>>> + Send; + + /// Sign a batch of sync committee contributions and return results as a stream of batches. + fn sign_sync_committee_contributions( + self: &Arc<Self>, + contributions: Vec<ContributionToSign<Self::E>>, + ) -> impl Stream<Item = Result<Vec<SignedContributionAndProof<Self::E>>, Error<Self::Error>>> + Send; /// Prune the slashing protection database so that it remains performant. /// From 53a711956eb5c5ffeef277b2a13850bd4911946b Mon Sep 17 00:00:00 2001 From: Akihito Nakano <sora.akatsuki@gmail.com> Date: Sat, 14 Mar 2026 03:27:15 +0900 Subject: [PATCH 78/81] Fix flaky `test_same_subnet_unsubscription` (#8932) Co-Authored-By: figtracer <1gusredo@gmail.com> Co-Authored-By: ackintosh <sora.akatsuki@gmail.com> --- beacon_node/network/src/subnet_service/mod.rs | 7 ---- .../network/src/subnet_service/tests/mod.rs | 34 +++++++++---------- 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/beacon_node/network/src/subnet_service/mod.rs b/beacon_node/network/src/subnet_service/mod.rs index be491e56d3..008e7ab9ac 100644 --- a/beacon_node/network/src/subnet_service/mod.rs +++ b/beacon_node/network/src/subnet_service/mod.rs @@ -198,13 +198,6 @@ impl<T: BeaconChainTypes> SubnetService<T> { self.permanent_attestation_subscriptions.iter() } - /// Returns whether we are subscribed to a subnet for testing purposes. - #[cfg(test)] - pub(crate) fn is_subscribed(&self, subnet: &Subnet) -> bool { - self.subscriptions.contains_key(subnet) - || self.permanent_attestation_subscriptions.contains(subnet) - } - /// Returns whether we are subscribed to a permanent subnet for testing purposes. #[cfg(test)] pub(crate) fn is_subscribed_permanent(&self, subnet: &Subnet) -> bool { diff --git a/beacon_node/network/src/subnet_service/tests/mod.rs b/beacon_node/network/src/subnet_service/tests/mod.rs index bee6569b7b..619154d738 100644 --- a/beacon_node/network/src/subnet_service/tests/mod.rs +++ b/beacon_node/network/src/subnet_service/tests/mod.rs @@ -335,28 +335,26 @@ mod test { // submit the subscriptions subnet_service.validator_subscriptions(vec![sub1, sub2].into_iter()); - // Unsubscription event should happen at slot 2 (since subnet id's are the same, unsubscription event should be at higher slot + 1) - let expected = SubnetServiceMessage::Subscribe(Subnet::Attestation(subnet_id1)); + let subnet = Subnet::Attestation(subnet_id1); - if subnet_service.is_subscribed(&Subnet::Attestation(subnet_id1)) { - // If we are permanently subscribed to this subnet, we won't see a subscribe message - let _ = get_events_until_num_slots(&mut subnet_service, None, 1).await; + if subnet_service.is_subscribed_permanent(&subnet) { + // If permanently subscribed, no Subscribe/Unsubscribe events will be generated + let events = get_events_until_num_slots(&mut subnet_service, None, 3).await; + assert!(events.is_empty()); } else { - let subscription = get_events_until_num_slots(&mut subnet_service, None, 1).await; - assert_eq!(subscription, [expected]); + // Wait 1 slot: expect a single Subscribe event (no duplicate for the same subnet). + let events = get_events_until_num_slots(&mut subnet_service, None, 1).await; + assert_eq!(events, [SubnetServiceMessage::Subscribe(subnet)]); + + // Wait for the Unsubscribe event after subscription_slot2 expires. + // Use a longer timeout because the test doesn't start exactly at a slot + // boundary, so the previous 1-slot wait may end partway through slot 1, + // leaving insufficient time to catch the Unsubscribe within another 1 slot. + let events = get_events_until_num_slots(&mut subnet_service, Some(1), 3).await; + assert_eq!(events, [SubnetServiceMessage::Unsubscribe(subnet)]); } - // Get event for 1 more slot duration, we should get the unsubscribe event now. - let unsubscribe_event = get_events_until_num_slots(&mut subnet_service, None, 1).await; - - // If the long lived and short lived subnets are different, we should get an unsubscription - // event. - let expected = SubnetServiceMessage::Unsubscribe(Subnet::Attestation(subnet_id1)); - if !subnet_service.is_subscribed(&Subnet::Attestation(subnet_id1)) { - assert_eq!([expected], unsubscribe_event[..]); - } - - // Should no longer be subscribed to any short lived subnets after unsubscription. + // Should no longer be subscribed to any short lived subnets after unsubscription. assert_eq!(subnet_service.subscriptions().count(), 0); } From 02137492f30276619dbb764f4fada34c9d72cd21 Mon Sep 17 00:00:00 2001 From: ethDreamer <37123614+ethDreamer@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:22:25 -0500 Subject: [PATCH 79/81] Fix intermittent simulator test failures (#8983) Fixes intermittent simulator test failures with error: `Head not synced for node 2. Found 127; Should be 128` Modify the delayed node in `basic_sim` to join earlier, giving it sufficient time to discover peers and form a proper gossip mesh before the sync verification check. **Change:** Delayed node now joins at `END_EPOCH - 3` (epoch 13) instead of `END_EPOCH - 1` (epoch 15). Co-Authored-By: Mark Mackey <mark@sigmaprime.io> Co-Authored-By: ethDreamer <37123614+ethDreamer@users.noreply.github.com> --- testing/simulator/src/basic_sim.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/simulator/src/basic_sim.rs b/testing/simulator/src/basic_sim.rs index a9d0a0756b..79581ee529 100644 --- a/testing/simulator/src/basic_sim.rs +++ b/testing/simulator/src/basic_sim.rs @@ -363,7 +363,7 @@ pub fn run_basic_sim(matches: &ArgMatches) -> Result<(), String> { network_1.add_beacon_node_with_delay( beacon_config.clone(), mock_execution_config.clone(), - END_EPOCH - 1, + END_EPOCH - 3, slot_duration, slots_per_epoch ), From 6ca610d918e8a12946c7c9baaeb4bcbfbc3429d5 Mon Sep 17 00:00:00 2001 From: ethDreamer <37123614+ethDreamer@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:22:29 -0500 Subject: [PATCH 80/81] Breakup RPCBlock into LookupBlock & RangeSyncBlock (#8860) Co-Authored-By: Mark Mackey <mark@sigmaprime.io> --- beacon_node/beacon_chain/src/beacon_chain.rs | 8 +- .../beacon_chain/src/block_verification.rs | 97 +++++---- .../src/block_verification_types.rs | 201 +++++++++--------- .../src/data_availability_checker.rs | 13 +- beacon_node/beacon_chain/src/test_utils.rs | 180 ++++++++-------- .../tests/attestation_production.rs | 37 +--- .../beacon_chain/tests/blob_verification.rs | 7 +- .../beacon_chain/tests/block_verification.rs | 169 +++++---------- .../beacon_chain/tests/column_verification.rs | 16 +- .../tests/payload_invalidation.rs | 48 ++--- beacon_node/beacon_chain/tests/store_tests.rs | 53 ++--- beacon_node/http_api/src/publish_blocks.rs | 16 +- .../src/network_beacon_processor/mod.rs | 11 +- .../network_beacon_processor/sync_methods.rs | 51 ++--- .../src/network_beacon_processor/tests.rs | 32 +-- .../network/src/sync/backfill_sync/mod.rs | 6 +- beacon_node/network/src/sync/batch.rs | 4 +- .../src/sync/block_sidecar_coupling.rs | 28 +-- .../network/src/sync/network_context.rs | 19 +- .../network/src/sync/range_sync/chain.rs | 6 +- .../network/src/sync/range_sync/range.rs | 4 +- beacon_node/network/src/sync/tests/lookups.rs | 96 ++++----- beacon_node/network/src/sync/tests/mod.rs | 6 +- beacon_node/network/src/sync/tests/range.rs | 20 +- testing/ef_tests/src/cases/fork_choice.rs | 46 ++-- 25 files changed, 505 insertions(+), 669 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index ab2097e001..20af7b4630 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -13,7 +13,7 @@ use crate::block_verification::{ signature_verify_chain_segment, verify_header_signature, }; use crate::block_verification_types::{ - AsBlock, AvailableExecutedBlock, BlockImportData, ExecutedBlock, RpcBlock, + AsBlock, AvailableExecutedBlock, BlockImportData, ExecutedBlock, RangeSyncBlock, }; pub use crate::canonical_head::CanonicalHead; use crate::chain_config::ChainConfig; @@ -137,7 +137,7 @@ use types::*; pub type ForkChoiceError = fork_choice::Error<crate::ForkChoiceStoreError>; /// Alias to appease clippy. -type HashBlockTuple<E> = (Hash256, RpcBlock<E>); +type HashBlockTuple<E> = (Hash256, RangeSyncBlock<E>); // These keys are all zero because they get stored in different columns, see `DBColumn` type. pub const BEACON_CHAIN_DB_KEY: Hash256 = Hash256::ZERO; @@ -2746,7 +2746,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> { /// This method is potentially long-running and should not run on the core executor. pub fn filter_chain_segment( self: &Arc<Self>, - chain_segment: Vec<RpcBlock<T::EthSpec>>, + chain_segment: Vec<RangeSyncBlock<T::EthSpec>>, ) -> Result<Vec<HashBlockTuple<T::EthSpec>>, Box<ChainSegmentResult>> { // This function will never import any blocks. let imported_blocks = vec![]; @@ -2855,7 +2855,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> { /// `Self::process_block`. pub async fn process_chain_segment( self: &Arc<Self>, - chain_segment: Vec<RpcBlock<T::EthSpec>>, + chain_segment: Vec<RangeSyncBlock<T::EthSpec>>, notify_execution_layer: NotifyExecutionLayer, ) -> ChainSegmentResult { for block in chain_segment.iter() { diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 1be9bd4181..06ec26185f 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -50,7 +50,7 @@ use crate::beacon_snapshot::PreProcessingSnapshot; use crate::blob_verification::GossipBlobError; -use crate::block_verification_types::{AsBlock, BlockImportData, RpcBlock}; +use crate::block_verification_types::{AsBlock, BlockImportData, LookupBlock, RangeSyncBlock}; use crate::data_availability_checker::{ AvailabilityCheckError, AvailableBlock, AvailableBlockData, MaybeAvailableBlock, }; @@ -585,7 +585,7 @@ pub(crate) fn process_block_slash_info<T: BeaconChainTypes, TErr: BlockBlobError /// will be returned. #[instrument(skip_all)] pub fn signature_verify_chain_segment<T: BeaconChainTypes>( - mut chain_segment: Vec<(Hash256, RpcBlock<T::EthSpec>)>, + mut chain_segment: Vec<(Hash256, RangeSyncBlock<T::EthSpec>)>, chain: &BeaconChain<T>, ) -> Result<Vec<SignatureVerifiedBlock<T>>, BlockError> { if chain_segment.is_empty() { @@ -616,24 +616,14 @@ pub fn signature_verify_chain_segment<T: BeaconChainTypes>( let consensus_context = ConsensusContext::new(block.slot()).set_current_block_root(block_root); - match block { - RpcBlock::FullyAvailable(available_block) => { - available_blocks.push(available_block.clone()); - signature_verified_blocks.push(SignatureVerifiedBlock { - block: MaybeAvailableBlock::Available(available_block), - block_root, - parent: None, - consensus_context, - }); - } - RpcBlock::BlockOnly { .. } => { - // RangeSync and BackfillSync already ensure that the chain segment is fully available - // so this shouldn't be possible in practice. - return Err(BlockError::InternalError( - "Chain segment is not fully available".to_string(), - )); - } - } + let available_block = block.into_available_block(); + available_blocks.push(available_block.clone()); + signature_verified_blocks.push(SignatureVerifiedBlock { + block: MaybeAvailableBlock::Available(available_block), + block_root, + parent: None, + consensus_context, + }); } chain @@ -1300,11 +1290,11 @@ impl<T: BeaconChainTypes> IntoExecutionPendingBlock<T> for SignatureVerifiedBloc } } -impl<T: BeaconChainTypes> IntoExecutionPendingBlock<T> for RpcBlock<T::EthSpec> { +impl<T: BeaconChainTypes> IntoExecutionPendingBlock<T> for RangeSyncBlock<T::EthSpec> { /// Verifies the `SignedBeaconBlock` by first transforming it into a `SignatureVerifiedBlock` /// and then using that implementation of `IntoExecutionPendingBlock` to complete verification. #[instrument( - name = "rpc_block_into_execution_pending_block_slashable", + name = "range_sync_block_into_execution_pending_block_slashable", level = "debug" skip_all, )] @@ -1318,24 +1308,51 @@ impl<T: BeaconChainTypes> IntoExecutionPendingBlock<T> for RpcBlock<T::EthSpec> let block_root = check_block_relevancy(self.as_block(), block_root, chain) .map_err(|e| BlockSlashInfo::SignatureNotChecked(self.signed_block_header(), e))?; - let maybe_available_block = match &self { - RpcBlock::FullyAvailable(available_block) => { - chain - .data_availability_checker - .verify_kzg_for_available_block(available_block) - .map_err(|e| { - BlockSlashInfo::SignatureNotChecked( - self.signed_block_header(), - BlockError::AvailabilityCheck(e), - ) - })?; - MaybeAvailableBlock::Available(available_block.clone()) - } - // No need to perform KZG verification unless we have a fully available block - RpcBlock::BlockOnly { block, block_root } => MaybeAvailableBlock::AvailabilityPending { - block_root: *block_root, - block: block.clone(), - }, + let available_block = self.into_available_block(); + chain + .data_availability_checker + .verify_kzg_for_available_block(&available_block) + .map_err(|e| { + BlockSlashInfo::SignatureNotChecked( + available_block.as_block().signed_block_header(), + BlockError::AvailabilityCheck(e), + ) + })?; + let maybe_available_block = MaybeAvailableBlock::Available(available_block); + SignatureVerifiedBlock::check_slashable(maybe_available_block, block_root, chain)? + .into_execution_pending_block_slashable(block_root, chain, notify_execution_layer) + } + + fn block(&self) -> &SignedBeaconBlock<T::EthSpec> { + self.as_block() + } + + fn block_cloned(&self) -> Arc<SignedBeaconBlock<T::EthSpec>> { + self.block_cloned() + } +} + +impl<T: BeaconChainTypes> IntoExecutionPendingBlock<T> for LookupBlock<T::EthSpec> { + /// Verifies the `SignedBeaconBlock` by first transforming it into a `SignatureVerifiedBlock` + /// and then using that implementation of `IntoExecutionPendingBlock` to complete verification. + #[instrument( + name = "lookup_block_into_execution_pending_block_slashable", + level = "debug" + skip_all, + )] + fn into_execution_pending_block_slashable( + self, + block_root: Hash256, + chain: &Arc<BeaconChain<T>>, + notify_execution_layer: NotifyExecutionLayer, + ) -> Result<ExecutionPendingBlock<T>, BlockSlashInfo<BlockError>> { + // Perform an early check to prevent wasting time on irrelevant blocks. + let block_root = check_block_relevancy(self.as_block(), block_root, chain) + .map_err(|e| BlockSlashInfo::SignatureNotChecked(self.signed_block_header(), e))?; + + let maybe_available_block = MaybeAvailableBlock::AvailabilityPending { + block_root, + block: self.block_cloned(), }; SignatureVerifiedBlock::check_slashable(maybe_available_block, block_root, chain)? diff --git a/beacon_node/beacon_chain/src/block_verification_types.rs b/beacon_node/beacon_chain/src/block_verification_types.rs index f98cd40d08..be73ef15d7 100644 --- a/beacon_node/beacon_chain/src/block_verification_types.rs +++ b/beacon_node/beacon_chain/src/block_verification_types.rs @@ -13,76 +13,70 @@ use types::{ SignedBeaconBlock, SignedBeaconBlockHeader, Slot, }; -/// A block that has been received over RPC. It has 2 internal variants: -/// -/// 1. `FullyAvailable`: A fully available block. This can either be a pre-deneb block, a -/// post-Deneb block with blobs, a post-Fulu block with the columns the node is required to custody, -/// or a post-Deneb block that doesn't require blobs/columns. Hence, it is fully self contained w.r.t -/// verification. i.e. this block has all the required data to get verified and imported into fork choice. -/// -/// 2. `BlockOnly`: This is a post-deneb block that requires blobs to be considered fully available. -#[derive(Clone, Educe)] -#[educe(Hash(bound(E: EthSpec)))] -pub enum RpcBlock<E: EthSpec> { - FullyAvailable(AvailableBlock<E>), - BlockOnly { - block: Arc<SignedBeaconBlock<E>>, - block_root: Hash256, - }, +/// A wrapper around a `SignedBeaconBlock`. This varaint is constructed +/// when lookup sync only fetches a single block. It does not contain +/// any blobs or data columns. +pub struct LookupBlock<E: EthSpec> { + block: Arc<SignedBeaconBlock<E>>, + block_root: Hash256, } -impl<E: EthSpec> Debug for RpcBlock<E> { +impl<E: EthSpec> LookupBlock<E> { + pub fn new(block: Arc<SignedBeaconBlock<E>>) -> Self { + let block_root = block.canonical_root(); + Self { block, block_root } + } + + pub fn block(&self) -> &SignedBeaconBlock<E> { + &self.block + } + + pub fn block_root(&self) -> Hash256 { + self.block_root + } + + pub fn block_cloned(&self) -> Arc<SignedBeaconBlock<E>> { + self.block.clone() + } +} + +/// 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<E: EthSpec> { + block: AvailableBlock<E>, +} + +impl<E: EthSpec> Debug for RangeSyncBlock<E> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "RpcBlock({:?})", self.block_root()) } } -impl<E: EthSpec> RpcBlock<E> { +impl<E: EthSpec> RangeSyncBlock<E> { pub fn block_root(&self) -> Hash256 { - match self { - RpcBlock::FullyAvailable(available_block) => available_block.block_root(), - RpcBlock::BlockOnly { block_root, .. } => *block_root, - } + self.block.block_root() } pub fn as_block(&self) -> &SignedBeaconBlock<E> { - match self { - RpcBlock::FullyAvailable(available_block) => available_block.block(), - RpcBlock::BlockOnly { block, .. } => block, - } + self.block.block() } pub fn block_cloned(&self) -> Arc<SignedBeaconBlock<E>> { - match self { - RpcBlock::FullyAvailable(available_block) => available_block.block_cloned(), - RpcBlock::BlockOnly { block, .. } => block.clone(), - } + self.block.block_cloned() } - pub fn block_data(&self) -> Option<&AvailableBlockData<E>> { - match self { - RpcBlock::FullyAvailable(available_block) => Some(available_block.data()), - RpcBlock::BlockOnly { .. } => None, - } + pub fn block_data(&self) -> &AvailableBlockData<E> { + self.block.data() } } -impl<E: EthSpec> RpcBlock<E> { - /// Constructs an `RpcBlock` from a block and optional availability data. - /// - /// This function creates an RpcBlock which can be in one of two states: - /// - `FullyAvailable`: When `block_data` is provided, the block contains all required - /// data for verification. - /// - `BlockOnly`: When `block_data` is `None`, the block may still need additional - /// data to be considered fully available (used during block lookups or when blobs - /// will arrive separately). - /// - /// # Validation - /// - /// When `block_data` is provided, this function validates that: - /// - Block data is not provided when not required. - /// - Required blobs are present and match the expected count. - /// - Required custody columns are included based on the nodes custody requirements. +impl<E: EthSpec> RangeSyncBlock<E> { + /// Constructs an `RangeSyncBlock` from a block and availability data. /// /// # Errors /// @@ -92,62 +86,41 @@ impl<E: EthSpec> RpcBlock<E> { /// - `MissingCustodyColumns`: Block requires custody columns but they are incomplete. pub fn new<T>( block: Arc<SignedBeaconBlock<E>>, - block_data: Option<AvailableBlockData<E>>, + block_data: AvailableBlockData<E>, da_checker: &DataAvailabilityChecker<T>, spec: Arc<ChainSpec>, ) -> Result<Self, AvailabilityCheckError> where T: BeaconChainTypes<EthSpec = E>, { - match block_data { - Some(block_data) => Ok(RpcBlock::FullyAvailable(AvailableBlock::new( - block, block_data, da_checker, spec, - )?)), - None => Ok(RpcBlock::BlockOnly { - block_root: block.canonical_root(), - block, - }), - } + let available_block = AvailableBlock::new(block, block_data, da_checker, spec)?; + Ok(Self { + block: available_block, + }) } #[allow(clippy::type_complexity)] - pub fn deconstruct( - self, - ) -> ( - Hash256, - Arc<SignedBeaconBlock<E>>, - Option<AvailableBlockData<E>>, - ) { - match self { - RpcBlock::FullyAvailable(available_block) => { - let (block_root, block, block_data) = available_block.deconstruct(); - (block_root, block, Some(block_data)) - } - RpcBlock::BlockOnly { block, block_root } => (block_root, block, None), - } + pub fn deconstruct(self) -> (Hash256, Arc<SignedBeaconBlock<E>>, AvailableBlockData<E>) { + self.block.deconstruct() } pub fn n_blobs(&self) -> usize { - if let Some(block_data) = self.block_data() { - match block_data { - AvailableBlockData::NoData | AvailableBlockData::DataColumns(_) => 0, - AvailableBlockData::Blobs(blobs) => blobs.len(), - } - } else { - 0 + match self.block_data() { + AvailableBlockData::NoData | AvailableBlockData::DataColumns(_) => 0, + AvailableBlockData::Blobs(blobs) => blobs.len(), } } pub fn n_data_columns(&self) -> usize { - if let Some(block_data) = self.block_data() { - match block_data { - AvailableBlockData::NoData | AvailableBlockData::Blobs(_) => 0, - AvailableBlockData::DataColumns(columns) => columns.len(), - } - } else { - 0 + match self.block_data() { + AvailableBlockData::NoData | AvailableBlockData::Blobs(_) => 0, + AvailableBlockData::DataColumns(columns) => columns.len(), } } + + pub fn into_available_block(self) -> AvailableBlock<E> { + self.block + } } /// A block that has gone through all pre-deneb block processing checks including block processing @@ -412,7 +385,7 @@ impl<E: EthSpec> AsBlock<E> for AvailableBlock<E> { } } -impl<E: EthSpec> AsBlock<E> for RpcBlock<E> { +impl<E: EthSpec> AsBlock<E> for RangeSyncBlock<E> { fn slot(&self) -> Slot { self.as_block().slot() } @@ -432,24 +405,42 @@ impl<E: EthSpec> AsBlock<E> for RpcBlock<E> { self.as_block().message() } fn as_block(&self) -> &SignedBeaconBlock<E> { - match self { - Self::BlockOnly { - block, - block_root: _, - } => block, - Self::FullyAvailable(available_block) => available_block.block(), - } + self.block.as_block() } fn block_cloned(&self) -> Arc<SignedBeaconBlock<E>> { - match self { - RpcBlock::FullyAvailable(available_block) => available_block.block_cloned(), - RpcBlock::BlockOnly { - block, - block_root: _, - } => block.clone(), - } + self.block.block_cloned() } fn canonical_root(&self) -> Hash256 { - self.as_block().canonical_root() + self.block.block_root() + } +} + +impl<E: EthSpec> AsBlock<E> for LookupBlock<E> { + fn slot(&self) -> Slot { + self.block().slot() + } + fn epoch(&self) -> Epoch { + self.block().epoch() + } + fn parent_root(&self) -> Hash256 { + self.block().parent_root() + } + fn state_root(&self) -> Hash256 { + self.block().state_root() + } + fn signed_block_header(&self) -> SignedBeaconBlockHeader { + self.block().signed_block_header() + } + fn message(&self) -> BeaconBlockRef<'_, E> { + self.block().message() + } + fn as_block(&self) -> &SignedBeaconBlock<E> { + self.block() + } + fn block_cloned(&self) -> Arc<SignedBeaconBlock<E>> { + self.block_cloned() + } + fn canonical_root(&self) -> Hash256 { + 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 e266e02f7f..4372efa809 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -891,7 +891,7 @@ impl<E: EthSpec> MaybeAvailableBlock<E> { mod test { use super::*; use crate::CustodyContext; - use crate::block_verification_types::RpcBlock; + use crate::block_verification_types::RangeSyncBlock; use crate::custody_context::NodeCustodyType; use crate::data_column_verification::CustodyDataColumn; use crate::test_utils::{ @@ -1085,7 +1085,7 @@ mod test { /// Regression test for KZG verification truncation bug (https://github.com/sigp/lighthouse/pull/7927) #[test] - fn verify_kzg_for_rpc_blocks_should_not_truncate_data_columns_fulu() { + fn verify_kzg_for_range_sync_blocks_should_not_truncate_data_columns_fulu() { let spec = Arc::new(ForkName::Fulu.make_genesis_spec(E::default_spec())); let mut rng = StdRng::seed_from_u64(0xDEADBEEF0BAD5EEDu64); let da_checker = new_da_checker(spec.clone()); @@ -1128,17 +1128,14 @@ mod test { let block_data = AvailableBlockData::new_with_data_columns(custody_columns); let da_checker = Arc::new(new_da_checker(spec.clone())); - RpcBlock::new(Arc::new(block), Some(block_data), &da_checker, spec.clone()) + RangeSyncBlock::new(Arc::new(block), block_data, &da_checker, spec.clone()) .expect("should create RPC block with custody columns") }) .collect::<Vec<_>>(); let available_blocks = blocks_with_columns - .iter() - .filter_map(|block| match block { - RpcBlock::FullyAvailable(available_block) => Some(available_block.clone()), - RpcBlock::BlockOnly { .. } => None, - }) + .into_iter() + .map(|block| block.into_available_block()) .collect::<Vec<_>>(); // WHEN verifying all blocks together (totalling 256 data columns) diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 4bc5bb21d3..c53c29438e 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -1,5 +1,5 @@ use crate::blob_verification::GossipVerifiedBlob; -use crate::block_verification_types::{AsBlock, AvailableBlockData, RpcBlock}; +use crate::block_verification_types::{AsBlock, AvailableBlockData, LookupBlock, RangeSyncBlock}; use crate::custody_context::NodeCustodyType; use crate::data_availability_checker::DataAvailabilityChecker; use crate::graffiti_calculator::GraffitiSettings; @@ -823,20 +823,20 @@ where mock_builder_server } - pub fn get_head_block(&self) -> RpcBlock<E> { + pub fn get_head_block(&self) -> RangeSyncBlock<E> { let block = self.chain.head_beacon_block(); let block_root = block.canonical_root(); - self.build_rpc_block_from_store_blobs(Some(block_root), block) + self.build_range_sync_block_from_store_blobs(Some(block_root), block) } - pub fn get_full_block(&self, block_root: &Hash256) -> RpcBlock<E> { + pub fn get_full_block(&self, block_root: &Hash256) -> RangeSyncBlock<E> { let block = self .chain .get_blinded_block(block_root) .unwrap() .unwrap_or_else(|| panic!("block root does not exist in harness {block_root:?}")); let full_block = self.chain.store.make_full_block(block_root, block).unwrap(); - self.build_rpc_block_from_store_blobs(Some(*block_root), Arc::new(full_block)) + self.build_range_sync_block_from_store_blobs(Some(*block_root), Arc::new(full_block)) } pub fn get_all_validators(&self) -> Vec<usize> { @@ -1340,15 +1340,12 @@ where let signed_block = self.sign_beacon_block(block, state); let block_root = signed_block.canonical_root(); - let rpc_block = RpcBlock::BlockOnly { - block_root, - block: Arc::new(signed_block), - }; + let lookup_block = LookupBlock::new(Arc::new(signed_block)); self.chain.slot_clock.set_slot(slot.as_u64()); self.chain .process_block( block_root, - rpc_block, + lookup_block, NotifyExecutionLayer::No, BlockImportSource::Lookup, || Ok(()), @@ -2607,20 +2604,33 @@ where .blob_kzg_commitments() .is_ok_and(|c| !c.is_empty()); let is_available = !has_blob_commitments || blob_items.is_some(); + let block_hash: SignedBeaconBlockHash = if !is_available { + self.chain + .process_block( + block_root, + LookupBlock::new(block), + NotifyExecutionLayer::Yes, + BlockImportSource::Lookup, + || Ok(()), + ) + .await? + .try_into() + .expect("block blobs are available") + } else { + let range_sync_block = self.build_range_sync_block_from_blobs(block, blob_items)?; + self.chain + .process_block( + block_root, + range_sync_block, + NotifyExecutionLayer::Yes, + BlockImportSource::RangeSync, + || Ok(()), + ) + .await? + .try_into() + .expect("block blobs are available") + }; - let rpc_block = self.build_rpc_block_from_blobs(block, blob_items, is_available)?; - let block_hash: SignedBeaconBlockHash = self - .chain - .process_block( - block_root, - rpc_block, - NotifyExecutionLayer::Yes, - BlockImportSource::RangeSync, - || Ok(()), - ) - .await? - .try_into() - .expect("block blobs are available"); self.chain.recompute_head_at_current_slot().await; Ok(block_hash) } @@ -2640,19 +2650,33 @@ where .blob_kzg_commitments() .is_ok_and(|c| !c.is_empty()); let is_available = !has_blob_commitments || blob_items.is_some(); - let rpc_block = self.build_rpc_block_from_blobs(block, blob_items, is_available)?; - let block_hash: SignedBeaconBlockHash = self - .chain - .process_block( - block_root, - rpc_block, - NotifyExecutionLayer::Yes, - BlockImportSource::RangeSync, - || Ok(()), - ) - .await? - .try_into() - .expect("block blobs are available"); + let block_hash: SignedBeaconBlockHash = if is_available { + let range_sync_block = self.build_range_sync_block_from_blobs(block, blob_items)?; + self.chain + .process_block( + block_root, + range_sync_block, + NotifyExecutionLayer::Yes, + BlockImportSource::RangeSync, + || Ok(()), + ) + .await? + .try_into() + .expect("block blobs are available") + } else { + self.chain + .process_block( + block_root, + LookupBlock::new(block), + NotifyExecutionLayer::Yes, + BlockImportSource::Lookup, + || Ok(()), + ) + .await? + .try_into() + .expect("block blobs are available") + }; + self.chain.recompute_head_at_current_slot().await; Ok(block_hash) } @@ -2735,13 +2759,13 @@ where state_root } - /// Builds an `Rpc` block from a `SignedBeaconBlock` and blobs or data columns retrieved from + /// Builds a `RangeSyncBlock` from a `SignedBeaconBlock` and blobs or data columns retrieved from /// the database. - pub fn build_rpc_block_from_store_blobs( + pub fn build_range_sync_block_from_store_blobs( &self, block_root: Option<Hash256>, block: Arc<SignedBeaconBlock<E>>, - ) -> RpcBlock<E> { + ) -> RangeSyncBlock<E> { let block_root = block_root.unwrap_or_else(|| get_block_root(&block)); let has_blobs = block .message() @@ -2749,9 +2773,9 @@ where .blob_kzg_commitments() .is_ok_and(|c| !c.is_empty()); if !has_blobs { - return RpcBlock::new( + return RangeSyncBlock::new( block, - Some(AvailableBlockData::NoData), + AvailableBlockData::NoData, &self.chain.data_availability_checker, self.chain.spec.clone(), ) @@ -2768,9 +2792,9 @@ where .unwrap(); let custody_columns = columns.into_iter().collect::<Vec<_>>(); let block_data = AvailableBlockData::new_with_data_columns(custody_columns); - RpcBlock::new( + RangeSyncBlock::new( block, - Some(block_data), + block_data, &self.chain.data_availability_checker, self.chain.spec.clone(), ) @@ -2783,9 +2807,9 @@ where AvailableBlockData::NoData }; - RpcBlock::new( + RangeSyncBlock::new( block, - Some(block_data), + block_data, &self.chain.data_availability_checker, self.chain.spec.clone(), ) @@ -2793,18 +2817,17 @@ where } } - /// Builds an `RpcBlock` from a `SignedBeaconBlock` and `BlobsList`. - pub fn build_rpc_block_from_blobs( + /// Builds a `RangeSyncBlock` from a `SignedBeaconBlock` and `BlobsList`. + pub fn build_range_sync_block_from_blobs( &self, block: Arc<SignedBeaconBlock<E, FullPayload<E>>>, blob_items: Option<(KzgProofs<E>, BlobsList<E>)>, - is_available: bool, - ) -> Result<RpcBlock<E>, BlockError> { + ) -> Result<RangeSyncBlock<E>, BlockError> { 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); - if blob_items.is_some_and(|(_, blobs)| !blobs.is_empty()) { + if blob_items.is_some_and(|(kzg_proofs, _)| !kzg_proofs.is_empty()) { // Note: this method ignores the actual custody columns and just take the first // `sampling_column_count` for testing purpose only, because the chain does not // currently have any knowledge of the columns being custodied. @@ -2812,33 +2835,17 @@ where .into_iter() .filter(|d| sampling_columns.contains(d.index())) .collect::<Vec<_>>(); - if is_available { - let block_data = AvailableBlockData::new_with_data_columns(columns); - RpcBlock::new( - block, - Some(block_data), - &self.chain.data_availability_checker, - self.chain.spec.clone(), - )? - } else { - RpcBlock::new( - block, - None, - &self.chain.data_availability_checker, - self.chain.spec.clone(), - )? - } - } else if is_available { - RpcBlock::new( + let block_data = AvailableBlockData::new_with_data_columns(columns); + RangeSyncBlock::new( block, - Some(AvailableBlockData::NoData), + block_data, &self.chain.data_availability_checker, self.chain.spec.clone(), )? } else { - RpcBlock::new( + RangeSyncBlock::new( block, - None, + AvailableBlockData::NoData, &self.chain.data_availability_checker, self.chain.spec.clone(), )? @@ -2850,27 +2857,18 @@ where }) .transpose() .unwrap(); - if is_available { - let block_data = if let Some(blobs) = blobs { - AvailableBlockData::new_with_blobs(blobs) - } else { - AvailableBlockData::NoData - }; - - RpcBlock::new( - block, - Some(block_data), - &self.chain.data_availability_checker, - self.chain.spec.clone(), - )? + let block_data = if let Some(blobs) = blobs { + AvailableBlockData::new_with_blobs(blobs) } else { - RpcBlock::new( - block, - None, - &self.chain.data_availability_checker, - self.chain.spec.clone(), - )? - } + AvailableBlockData::NoData + }; + + RangeSyncBlock::new( + block, + block_data, + &self.chain.data_availability_checker, + self.chain.spec.clone(), + )? }) } diff --git a/beacon_node/beacon_chain/tests/attestation_production.rs b/beacon_node/beacon_chain/tests/attestation_production.rs index a1922f32a4..bca60d27cd 100644 --- a/beacon_node/beacon_chain/tests/attestation_production.rs +++ b/beacon_node/beacon_chain/tests/attestation_production.rs @@ -1,7 +1,6 @@ #![cfg(not(debug_assertions))] use beacon_chain::attestation_simulator::produce_unaggregated_attestation; -use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::custody_context::NodeCustodyType; use beacon_chain::test_utils::{AttestationStrategy, BeaconChainHarness, BlockStrategy}; use beacon_chain::validator_monitor::UNAGGREGATED_ATTESTATION_LAG_SLOTS; @@ -223,19 +222,9 @@ async fn produces_attestations() { assert_eq!(data.target.epoch, state.current_epoch(), "bad target epoch"); assert_eq!(data.target.root, target_root, "bad target root"); - let rpc_block = - harness.build_rpc_block_from_store_blobs(Some(block_root), Arc::new(block.clone())); - - let available_block = match rpc_block { - RpcBlock::FullyAvailable(available_block) => { - chain - .data_availability_checker - .verify_kzg_for_available_block(&available_block) - .unwrap(); - available_block - } - RpcBlock::BlockOnly { .. } => panic!("block should be available"), - }; + 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 early_attestation = { let proto_block = chain @@ -292,20 +281,12 @@ async fn early_attester_cache_old_request() { .get_block(&head.beacon_block_root) .unwrap(); - let rpc_block = harness - .build_rpc_block_from_store_blobs(Some(head.beacon_block_root), head.beacon_block.clone()); - - let available_block = match rpc_block { - RpcBlock::FullyAvailable(available_block) => { - harness - .chain - .data_availability_checker - .verify_kzg_for_available_block(&available_block) - .unwrap(); - available_block - } - RpcBlock::BlockOnly { .. } => panic!("block should be available"), - }; + let available_block = harness + .build_range_sync_block_from_store_blobs( + Some(head.beacon_block_root), + head.beacon_block.clone(), + ) + .into_available_block(); harness .chain diff --git a/beacon_node/beacon_chain/tests/blob_verification.rs b/beacon_node/beacon_chain/tests/blob_verification.rs index ee61177b2a..0ee9a7dba6 100644 --- a/beacon_node/beacon_chain/tests/blob_verification.rs +++ b/beacon_node/beacon_chain/tests/blob_verification.rs @@ -5,7 +5,7 @@ use beacon_chain::test_utils::{ }; use beacon_chain::{ AvailabilityProcessingStatus, BlockError, ChainConfig, InvalidSignature, NotifyExecutionLayer, - block_verification_types::AsBlock, + block_verification_types::{AsBlock, LookupBlock}, }; use bls::{Keypair, Signature}; use logging::create_test_tracing_subscriber; @@ -76,14 +76,11 @@ async fn rpc_blobs_with_invalid_header_signature() { // Process the block without blobs so that it doesn't become available. harness.advance_slot(); - let rpc_block = harness - .build_rpc_block_from_blobs(signed_block.clone(), None, false) - .unwrap(); let availability = harness .chain .process_block( block_root, - rpc_block, + LookupBlock::new(signed_block.clone()), NotifyExecutionLayer::Yes, BlockImportSource::Lookup, || Ok(()), diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index e385e0dc48..8981b20a55 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -1,6 +1,6 @@ #![cfg(not(debug_assertions))] // TODO(gloas) we probably need similar test for payload envelope verification -use beacon_chain::block_verification_types::{AsBlock, ExecutedBlock, RpcBlock}; +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::{ @@ -13,7 +13,7 @@ use beacon_chain::{ }; use beacon_chain::{ BeaconSnapshot, BlockError, ChainConfig, ChainSegmentResult, IntoExecutionPendingBlock, - InvalidSignature, NotifyExecutionLayer, signature_verify_chain_segment, + InvalidSignature, NotifyExecutionLayer, }; use bls::{AggregateSignature, Keypair, Signature}; use fixed_bytes::FixedBytesExtended; @@ -136,7 +136,7 @@ fn chain_segment_blocks<T>( chain_segment: &[BeaconSnapshot<E>], chain_segment_sidecars: &[Option<DataSidecars<E>>], chain: Arc<BeaconChain<T>>, -) -> Vec<RpcBlock<E>> +) -> Vec<RangeSyncBlock<E>> where T: BeaconChainTypes<EthSpec = E>, { @@ -145,25 +145,25 @@ where .zip(chain_segment_sidecars.iter()) .map(|(snapshot, data_sidecars)| { let block = snapshot.beacon_block.clone(); - build_rpc_block(block, data_sidecars, chain.clone()) + build_range_sync_block(block, data_sidecars, chain.clone()) }) .collect() } -fn build_rpc_block<T>( +fn build_range_sync_block<T>( block: Arc<SignedBeaconBlock<E>>, data_sidecars: &Option<DataSidecars<E>>, chain: Arc<BeaconChain<T>>, -) -> RpcBlock<E> +) -> RangeSyncBlock<E> where T: BeaconChainTypes<EthSpec = E>, { match data_sidecars { Some(DataSidecars::Blobs(blobs)) => { let block_data = AvailableBlockData::new_with_blobs(blobs.clone()); - RpcBlock::new( + RangeSyncBlock::new( block, - Some(block_data), + block_data, &chain.data_availability_checker, chain.spec.clone(), ) @@ -176,17 +176,17 @@ where .map(|c| c.as_data_column().clone()) .collect::<Vec<_>>(), ); - RpcBlock::new( + RangeSyncBlock::new( block, - Some(block_data), + block_data, &chain.data_availability_checker, chain.spec.clone(), ) .unwrap() } - None => RpcBlock::new( + None => RangeSyncBlock::new( block, - Some(AvailableBlockData::NoData), + AvailableBlockData::NoData, &chain.data_availability_checker, chain.spec.clone(), ) @@ -301,7 +301,7 @@ fn update_data_column_signed_header<E: EthSpec>( async fn chain_segment_full_segment() { let harness = get_harness(VALIDATOR_COUNT, NodeCustodyType::Fullnode); let (chain_segment, chain_segment_blobs) = get_chain_segment().await; - let blocks: Vec<RpcBlock<E>> = + let blocks: Vec<RangeSyncBlock<E>> = chain_segment_blocks(&chain_segment, &chain_segment_blobs, harness.chain.clone()) .into_iter() .collect(); @@ -339,7 +339,7 @@ async fn chain_segment_full_segment() { 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<RpcBlock<E>> = + let blocks: Vec<RangeSyncBlock<E>> = chain_segment_blocks(&chain_segment, &chain_segment_blobs, harness.chain.clone()) .into_iter() .collect(); @@ -384,7 +384,7 @@ async fn chain_segment_non_linear_parent_roots() { /* * Test with a block removed. */ - let mut blocks: Vec<RpcBlock<E>> = + let mut blocks: Vec<RangeSyncBlock<E>> = chain_segment_blocks(&chain_segment, &chain_segment_blobs, harness.chain.clone()) .into_iter() .collect(); @@ -405,7 +405,7 @@ async fn chain_segment_non_linear_parent_roots() { /* * Test with a modified parent root. */ - let mut blocks: Vec<RpcBlock<E>> = + let mut blocks: Vec<RangeSyncBlock<E>> = chain_segment_blocks(&chain_segment, &chain_segment_blobs, harness.chain.clone()) .into_iter() .collect(); @@ -413,9 +413,9 @@ 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] = RpcBlock::new( + blocks[3] = RangeSyncBlock::new( Arc::new(SignedBeaconBlock::from_block(block, signature)), - blocks[3].block_data().cloned(), + blocks[3].block_data().clone(), &harness.chain.data_availability_checker, harness.spec.clone(), ) @@ -447,15 +447,15 @@ async fn chain_segment_non_linear_slots() { * Test where a child is lower than the parent. */ - let mut blocks: Vec<RpcBlock<E>> = + let mut blocks: Vec<RangeSyncBlock<E>> = chain_segment_blocks(&chain_segment, &chain_segment_blobs, harness.chain.clone()) .into_iter() .collect(); let (mut block, signature) = blocks[3].as_block().clone().deconstruct(); *block.slot_mut() = Slot::new(0); - blocks[3] = RpcBlock::new( + blocks[3] = RangeSyncBlock::new( Arc::new(SignedBeaconBlock::from_block(block, signature)), - blocks[3].block_data().cloned(), + blocks[3].block_data().clone(), &harness.chain.data_availability_checker, harness.spec.clone(), ) @@ -477,15 +477,15 @@ async fn chain_segment_non_linear_slots() { * Test where a child is equal to the parent. */ - let mut blocks: Vec<RpcBlock<E>> = + let mut blocks: Vec<RangeSyncBlock<E>> = chain_segment_blocks(&chain_segment, &chain_segment_blobs, harness.chain.clone()) .into_iter() .collect(); let (mut block, signature) = blocks[3].as_block().clone().deconstruct(); *block.slot_mut() = blocks[2].slot(); - blocks[3] = RpcBlock::new( + blocks[3] = RangeSyncBlock::new( Arc::new(SignedBeaconBlock::from_block(block, signature)), - blocks[3].block_data().cloned(), + blocks[3].block_data().clone(), &harness.chain.data_availability_checker, harness.chain.spec.clone(), ) @@ -512,11 +512,11 @@ async fn assert_invalid_signature( snapshots: &[BeaconSnapshot<E>], item: &str, ) { - let blocks: Vec<RpcBlock<E>> = snapshots + let blocks: Vec<RangeSyncBlock<E>> = snapshots .iter() .zip(chain_segment_blobs.iter()) .map(|(snapshot, blobs)| { - build_rpc_block(snapshot.beacon_block.clone(), blobs, harness.chain.clone()) + build_range_sync_block(snapshot.beacon_block.clone(), blobs, harness.chain.clone()) }) .collect(); @@ -543,7 +543,7 @@ async fn assert_invalid_signature( .take(block_index) .zip(chain_segment_blobs.iter()) .map(|(snapshot, blobs)| { - build_rpc_block(snapshot.beacon_block.clone(), blobs, harness.chain.clone()) + build_range_sync_block(snapshot.beacon_block.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 @@ -558,7 +558,7 @@ async fn assert_invalid_signature( .chain .process_block( snapshots[block_index].beacon_block.canonical_root(), - build_rpc_block( + build_range_sync_block( snapshots[block_index].beacon_block.clone(), &chain_segment_blobs[block_index], harness.chain.clone(), @@ -620,7 +620,7 @@ async fn invalid_signature_gossip_block() { .take(block_index) .zip(chain_segment_blobs.iter()) .map(|(snapshot, blobs)| { - build_rpc_block(snapshot.beacon_block.clone(), blobs, harness.chain.clone()) + build_range_sync_block(snapshot.beacon_block.clone(), blobs, harness.chain.clone()) }) .collect(); harness @@ -630,18 +630,12 @@ async fn invalid_signature_gossip_block() { .into_block_error() .expect("should import all blocks prior to the one being tested"); let signed_block = SignedBeaconBlock::from_block(block, junk_signature()); - let rpc_block = RpcBlock::new( - Arc::new(signed_block), - None, - &harness.chain.data_availability_checker, - harness.spec.clone(), - ) - .unwrap(); + let lookup_block = LookupBlock::new(Arc::new(signed_block)); let process_res = harness .chain .process_block( - rpc_block.block_root(), - rpc_block, + lookup_block.block_root(), + lookup_block, NotifyExecutionLayer::Yes, BlockImportSource::Lookup, || Ok(()), @@ -675,11 +669,11 @@ async fn invalid_signature_block_proposal() { block.clone(), junk_signature(), )); - let blocks: Vec<RpcBlock<E>> = snapshots + let blocks: Vec<RangeSyncBlock<E>> = snapshots .iter() .zip(chain_segment_blobs.iter()) .map(|(snapshot, blobs)| { - build_rpc_block(snapshot.beacon_block.clone(), blobs, harness.chain.clone()) + build_range_sync_block(snapshot.beacon_block.clone(), blobs, harness.chain.clone()) }) .collect::<Vec<_>>(); // Ensure the block will be rejected if imported in a chain segment. @@ -994,11 +988,11 @@ async fn invalid_signature_deposit() { Arc::new(SignedBeaconBlock::from_block(block, signature)); update_parent_roots(&mut snapshots, &mut chain_segment_blobs); update_proposal_signatures(&mut snapshots, &harness); - let blocks: Vec<RpcBlock<E>> = snapshots + let blocks: Vec<RangeSyncBlock<E>> = snapshots .iter() .zip(chain_segment_blobs.iter()) .map(|(snapshot, blobs)| { - build_rpc_block(snapshot.beacon_block.clone(), blobs, harness.chain.clone()) + build_range_sync_block(snapshot.beacon_block.clone(), blobs, harness.chain.clone()) }) .collect(); assert!( @@ -1641,9 +1635,9 @@ async fn add_base_block_to_altair_chain() { )); // Ensure that it would be impossible to import via `BeaconChain::process_block`. - let base_rpc_block = RpcBlock::new( + let base_range_sync_block = RangeSyncBlock::new( Arc::new(base_block.clone()), - None, + AvailableBlockData::NoData, &harness.chain.data_availability_checker, harness.spec.clone(), ) @@ -1652,8 +1646,8 @@ async fn add_base_block_to_altair_chain() { harness .chain .process_block( - base_rpc_block.block_root(), - base_rpc_block, + base_range_sync_block.block_root(), + base_range_sync_block, NotifyExecutionLayer::Yes, BlockImportSource::Lookup, || Ok(()), @@ -1672,9 +1666,9 @@ async fn add_base_block_to_altair_chain() { .chain .process_chain_segment( vec![ - RpcBlock::new( + RangeSyncBlock::new( Arc::new(base_block), - None, + AvailableBlockData::NoData, &harness.chain.data_availability_checker, harness.spec.clone() ) @@ -1792,19 +1786,13 @@ async fn add_altair_block_to_base_chain() { )); // Ensure that it would be impossible to import via `BeaconChain::process_block`. - let altair_rpc_block = RpcBlock::new( - Arc::new(altair_block.clone()), - None, - &harness.chain.data_availability_checker, - harness.spec.clone(), - ) - .unwrap(); + let altair_lookup_block = LookupBlock::new(Arc::new(altair_block.clone())); assert!(matches!( harness .chain .process_block( - altair_rpc_block.block_root(), - altair_rpc_block, + altair_lookup_block.block_root(), + altair_lookup_block, NotifyExecutionLayer::Yes, BlockImportSource::Lookup, || Ok(()), @@ -1823,9 +1811,9 @@ async fn add_altair_block_to_base_chain() { .chain .process_chain_segment( vec![ - RpcBlock::new( + RangeSyncBlock::new( Arc::new(altair_block), - None, + AvailableBlockData::NoData, &harness.chain.data_availability_checker, harness.spec.clone() ) @@ -1891,18 +1879,18 @@ 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 rpc_block = RpcBlock::new( + let range_sync_block = RangeSyncBlock::new( block.clone(), - Some(AvailableBlockData::NoData), + AvailableBlockData::NoData, &harness.chain.data_availability_checker, harness.spec.clone(), ) .unwrap(); - let verified_block1 = rpc_block + let verified_block1 = range_sync_block .clone() .into_execution_pending_block(block_root, chain, notify_execution_layer) .unwrap(); - let verified_block2 = rpc_block + let verified_block2 = range_sync_block .into_execution_pending_block(block_root, chain, notify_execution_layer) .unwrap(); @@ -1972,48 +1960,9 @@ async fn import_execution_pending_block<T: BeaconChainTypes>( } } -// Test that `signature_verify_chain_segment` errors with a chain segment of mixed `FullyAvailable` -// and `BlockOnly` RpcBlocks. This situation should never happen in production. -#[tokio::test] -async fn signature_verify_mixed_rpc_block_variants() { - let (snapshots, data_sidecars) = get_chain_segment().await; - let snapshots: Vec<_> = snapshots.into_iter().take(10).collect(); - let data_sidecars: Vec<_> = data_sidecars.into_iter().take(10).collect(); - - let harness = get_harness(VALIDATOR_COUNT, NodeCustodyType::Fullnode); - - let mut chain_segment = Vec::new(); - - for (i, (snapshot, blobs)) in snapshots.iter().zip(data_sidecars.iter()).enumerate() { - let block = snapshot.beacon_block.clone(); - let block_root = snapshot.beacon_block_root; - - // Alternate between FullyAvailable and BlockOnly - let rpc_block = if i % 2 == 0 { - // FullyAvailable - with blobs/columns if needed - build_rpc_block(block, blobs, harness.chain.clone()) - } else { - // BlockOnly - no data - RpcBlock::new( - block, - None, - &harness.chain.data_availability_checker, - harness.chain.spec.clone(), - ) - .unwrap() - }; - - chain_segment.push((block_root, rpc_block)); - } - - // This should error because `signature_verify_chain_segment` expects a list - // of `RpcBlock::FullyAvailable`. - assert!(signature_verify_chain_segment(chain_segment.clone(), &harness.chain).is_err()); -} - // Test that RpcBlock::new() rejects blocks when blob count doesn't match expected. #[tokio::test] -async fn rpc_block_construction_fails_with_wrong_blob_count() { +async fn range_sync_block_construction_fails_with_wrong_blob_count() { let spec = test_spec::<E>(); if !spec.fork_name_at_slot::<E>(Slot::new(0)).deneb_enabled() @@ -2064,9 +2013,9 @@ async fn rpc_block_construction_fails_with_wrong_blob_count() { let block_data = AvailableBlockData::new_with_blobs(wrong_blobs); // Try to create RpcBlock with wrong blob count - let result = RpcBlock::new( + let result = RangeSyncBlock::new( Arc::new(block), - Some(block_data), + block_data, &harness.chain.data_availability_checker, harness.chain.spec.clone(), ); @@ -2086,7 +2035,7 @@ async fn rpc_block_construction_fails_with_wrong_blob_count() { // Test that RpcBlock::new() rejects blocks when custody columns are incomplete. #[tokio::test] -async fn rpc_block_rejects_missing_custody_columns() { +async fn range_sync_block_rejects_missing_custody_columns() { let spec = test_spec::<E>(); if !spec.fork_name_at_slot::<E>(Slot::new(0)).fulu_enabled() { @@ -2139,9 +2088,9 @@ async fn rpc_block_rejects_missing_custody_columns() { let block_data = AvailableBlockData::new_with_data_columns(incomplete_columns); // Try to create RpcBlock with incomplete custody columns - let result = RpcBlock::new( + let result = RangeSyncBlock::new( Arc::new(block), - Some(block_data), + block_data, &harness.chain.data_availability_checker, harness.chain.spec.clone(), ); @@ -2227,9 +2176,9 @@ async fn rpc_block_allows_construction_past_da_boundary() { // Try to create RpcBlock with NoData for a block past DA boundary // This should succeed since columns are not expected for blocks past DA boundary - let result = RpcBlock::new( + let result = RangeSyncBlock::new( Arc::new(block), - Some(AvailableBlockData::NoData), + AvailableBlockData::NoData, &harness.chain.data_availability_checker, harness.chain.spec.clone(), ); diff --git a/beacon_node/beacon_chain/tests/column_verification.rs b/beacon_node/beacon_chain/tests/column_verification.rs index 9941c957e2..6114bd7f45 100644 --- a/beacon_node/beacon_chain/tests/column_verification.rs +++ b/beacon_node/beacon_chain/tests/column_verification.rs @@ -7,7 +7,7 @@ use beacon_chain::test_utils::{ }; use beacon_chain::{ AvailabilityProcessingStatus, BlockError, ChainConfig, InvalidSignature, NotifyExecutionLayer, - block_verification_types::AsBlock, + block_verification_types::{AsBlock, LookupBlock}, }; use bls::{Keypair, Signature}; use logging::create_test_tracing_subscriber; @@ -80,16 +80,13 @@ async fn rpc_columns_with_invalid_header_signature() { // Process the block without blobs so that it doesn't become available. harness.advance_slot(); - let rpc_block = harness - .build_rpc_block_from_blobs(signed_block.clone(), None, false) - .unwrap(); let availability = harness .chain .process_block( block_root, - rpc_block, + LookupBlock::new(signed_block.clone()), NotifyExecutionLayer::Yes, - BlockImportSource::RangeSync, + BlockImportSource::Lookup, || Ok(()), ) .await @@ -169,16 +166,13 @@ async fn verify_header_signature_fork_block_bug() { // The block will be accepted but won't become the head because it's not fully available. // This keeps the head at the pre-fork state (Electra). harness.advance_slot(); - let rpc_block = harness - .build_rpc_block_from_blobs(signed_block.clone(), None, false) - .expect("Should build RPC block"); let availability = harness .chain .process_block( block_root, - rpc_block, + LookupBlock::new(signed_block.clone()), NotifyExecutionLayer::Yes, - BlockImportSource::RangeSync, + BlockImportSource::Lookup, || Ok(()), ) .await diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index bcc50990ec..3ed8f59838 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -1,7 +1,7 @@ #![cfg(not(debug_assertions))] #![allow(clippy::result_large_err)] -use beacon_chain::block_verification_types::RpcBlock; +use beacon_chain::block_verification_types::LookupBlock; use beacon_chain::{ BeaconChainError, BlockError, ChainConfig, ExecutionPayloadError, INVALID_JUSTIFIED_PAYLOAD_SHUTDOWN_REASON, NotifyExecutionLayer, StateSkipConfig, @@ -686,19 +686,13 @@ async fn invalidates_all_descendants() { assert_eq!(fork_parent_state.slot(), fork_parent_slot); let ((fork_block, _), _fork_post_state) = rig.harness.make_block(fork_parent_state, fork_slot).await; - let fork_rpc_block = RpcBlock::new( - fork_block.clone(), - None, - &rig.harness.chain.data_availability_checker, - rig.harness.chain.spec.clone(), - ) - .unwrap(); + let fork_lookup_block = LookupBlock::new(fork_block.clone()); let fork_block_root = rig .harness .chain .process_block( - fork_rpc_block.block_root(), - fork_rpc_block, + fork_lookup_block.block_root(), + fork_lookup_block, NotifyExecutionLayer::Yes, BlockImportSource::Lookup, || Ok(()), @@ -796,19 +790,13 @@ async fn switches_heads() { let ((fork_block, _), _fork_post_state) = rig.harness.make_block(fork_parent_state, fork_slot).await; let fork_parent_root = fork_block.parent_root(); - let fork_rpc_block = RpcBlock::new( - fork_block.clone(), - None, - &rig.harness.chain.data_availability_checker, - rig.harness.chain.spec.clone(), - ) - .unwrap(); + let fork_lookup_block = LookupBlock::new(fork_block.clone()); let fork_block_root = rig .harness .chain .process_block( - fork_rpc_block.block_root(), - fork_rpc_block, + fork_lookup_block.block_root(), + fork_lookup_block, NotifyExecutionLayer::Yes, BlockImportSource::Lookup, || Ok(()), @@ -1086,15 +1074,9 @@ async fn invalid_parent() { )); // Ensure the block built atop an invalid payload is invalid for import. - let rpc_block = RpcBlock::new( - block.clone(), - None, - &rig.harness.chain.data_availability_checker, - rig.harness.chain.spec.clone(), - ) - .unwrap(); + let lookup_block = LookupBlock::new(block.clone()); assert!(matches!( - rig.harness.chain.process_block(rpc_block.block_root(), rpc_block, NotifyExecutionLayer::Yes, BlockImportSource::Lookup, + rig.harness.chain.process_block(lookup_block.block_root(), lookup_block, NotifyExecutionLayer::Yes, BlockImportSource::Lookup, || Ok(()), ).await, Err(BlockError::ParentExecutionPayloadInvalid { parent_root: invalid_root }) @@ -1348,18 +1330,12 @@ async fn recover_from_invalid_head_by_importing_blocks() { } = InvalidHeadSetup::new().await; // Import the fork block, it should become the head. - let fork_rpc_block = RpcBlock::new( - fork_block.clone(), - None, - &rig.harness.chain.data_availability_checker, - rig.harness.chain.spec.clone(), - ) - .unwrap(); + let fork_lookup_block = LookupBlock::new(fork_block.clone()); rig.harness .chain .process_block( - fork_rpc_block.block_root(), - fork_rpc_block, + fork_lookup_block.block_root(), + fork_lookup_block, NotifyExecutionLayer::Yes, BlockImportSource::Lookup, || Ok(()), diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index a70ad89ca9..89c28cca37 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -2,7 +2,7 @@ #![allow(clippy::result_large_err)] use beacon_chain::attestation_verification::Error as AttnError; -use beacon_chain::block_verification_types::RpcBlock; +use beacon_chain::block_verification_types::LookupBlock; use beacon_chain::builder::BeaconChainBuilder; use beacon_chain::custody_context::CUSTODY_CHANGE_DA_EFFECTIVE_DELAY_SECONDS; use beacon_chain::data_availability_checker::AvailableBlock; @@ -3144,7 +3144,10 @@ async fn weak_subjectivity_sync_test( beacon_chain .process_block( full_block_root, - harness.build_rpc_block_from_store_blobs(Some(block_root), Arc::new(full_block)), + harness.build_range_sync_block_from_store_blobs( + Some(block_root), + Arc::new(full_block), + ), NotifyExecutionLayer::Yes, BlockImportSource::Lookup, || Ok(()), @@ -3214,20 +3217,16 @@ async fn weak_subjectivity_sync_test( .expect("should get block") .expect("should get block"); - let rpc_block = - harness.build_rpc_block_from_store_blobs(Some(block_root), Arc::new(full_block)); + let range_sync_block = harness + .build_range_sync_block_from_store_blobs(Some(block_root), Arc::new(full_block)); - match rpc_block { - RpcBlock::FullyAvailable(available_block) => { - harness - .chain - .data_availability_checker - .verify_kzg_for_available_block(&available_block) - .expect("should verify kzg"); - available_blocks.push(available_block); - } - RpcBlock::BlockOnly { .. } => panic!("Should be an available block"), - } + let fully_available_block = range_sync_block.into_available_block(); + harness + .chain + .data_availability_checker + .verify_kzg_for_available_block(&fully_available_block) + .expect("should verify kzg"); + available_blocks.push(fully_available_block); } // Corrupt the signature on the 1st block to ensure that the backfill processor is checking @@ -3798,19 +3797,13 @@ async fn process_blocks_and_attestations_for_unaligned_checkpoint() { assert_eq!(split.block_root, valid_fork_block.parent_root()); assert_ne!(split.state_root, unadvanced_split_state_root); - let invalid_fork_rpc_block = RpcBlock::new( - invalid_fork_block.clone(), - None, - &harness.chain.data_availability_checker, - harness.spec.clone(), - ) - .unwrap(); + let invalid_fork_lookup_block = LookupBlock::new(invalid_fork_block.clone()); // Applying the invalid block should fail. let err = harness .chain .process_block( - invalid_fork_rpc_block.block_root(), - invalid_fork_rpc_block, + invalid_fork_lookup_block.block_root(), + invalid_fork_lookup_block, NotifyExecutionLayer::Yes, BlockImportSource::Lookup, || Ok(()), @@ -3820,18 +3813,12 @@ async fn process_blocks_and_attestations_for_unaligned_checkpoint() { assert!(matches!(err, BlockError::WouldRevertFinalizedSlot { .. })); // Applying the valid block should succeed, but it should not become head. - let valid_fork_rpc_block = RpcBlock::new( - valid_fork_block.clone(), - None, - &harness.chain.data_availability_checker, - harness.spec.clone(), - ) - .unwrap(); + let valid_fork_lookup_block = LookupBlock::new(valid_fork_block.clone()); harness .chain .process_block( - valid_fork_rpc_block.block_root(), - valid_fork_rpc_block, + valid_fork_lookup_block.block_root(), + valid_fork_lookup_block, NotifyExecutionLayer::Yes, BlockImportSource::Lookup, || Ok(()), diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index bbf92a4dda..43dfbeb836 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -2,7 +2,7 @@ use crate::metrics; use std::future::Future; use beacon_chain::blob_verification::{GossipBlobError, GossipVerifiedBlob}; -use beacon_chain::block_verification_types::{AsBlock, RpcBlock}; +use beacon_chain::block_verification_types::{AsBlock, LookupBlock}; use beacon_chain::data_column_verification::GossipVerifiedDataColumn; use beacon_chain::validator_monitor::{get_block_delay_ms, timestamp_now}; use beacon_chain::{ @@ -311,19 +311,11 @@ pub async fn publish_block<T: BeaconChainTypes, B: IntoGossipVerifiedBlock<T>>( slot = %block.slot(), "Block previously seen" ); - let Ok(rpc_block) = RpcBlock::new( - block.clone(), - None, - &chain.data_availability_checker, - chain.spec.clone(), - ) else { - return Err(warp_utils::reject::custom_bad_request( - "Unable to construct rpc block".to_string(), - )); - }; + // try to reprocess as a lookup (single) block and let sync take care of missing components + let lookup_block = LookupBlock::new(block.clone()); let import_result = Box::pin(chain.process_block( block_root, - rpc_block, + lookup_block, NotifyExecutionLayer::Yes, BlockImportSource::HttpApi, publish_fn, diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index 357d6c08fd..e40eacce08 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -1,7 +1,8 @@ use crate::sync::manager::BlockProcessType; use crate::{service::NetworkMessage, sync::manager::SyncMessage}; use beacon_chain::blob_verification::{GossipBlobError, observe_gossip_blob}; -use beacon_chain::block_verification_types::RpcBlock; +use beacon_chain::block_verification_types::LookupBlock; +use beacon_chain::block_verification_types::RangeSyncBlock; use beacon_chain::data_column_verification::{GossipDataColumnError, observe_gossip_data_column}; use beacon_chain::fetch_blobs::{ EngineGetBlobsOutput, FetchEngineBlobError, fetch_and_process_engine_blobs, @@ -517,14 +518,14 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> { /// Create a new `Work` event for some block, where the result from computation (if any) is /// sent to the other side of `result_tx`. - pub fn send_rpc_beacon_block( + pub fn send_lookup_beacon_block( self: &Arc<Self>, block_root: Hash256, - block: RpcBlock<T::EthSpec>, + block: LookupBlock<T::EthSpec>, seen_timestamp: Duration, process_type: BlockProcessType, ) -> Result<(), Error<T::EthSpec>> { - let process_fn = self.clone().generate_rpc_beacon_block_process_fn( + let process_fn = self.clone().generate_lookup_beacon_block_process_fn( block_root, block, seen_timestamp, @@ -610,7 +611,7 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> { pub fn send_chain_segment( self: &Arc<Self>, process_id: ChainSegmentProcessId, - blocks: Vec<RpcBlock<T::EthSpec>>, + blocks: Vec<RangeSyncBlock<T::EthSpec>>, ) -> Result<(), Error<T::EthSpec>> { debug!(blocks = blocks.len(), id = ?process_id, "Batch sending for process"); let processor = self.clone(); 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 629a42c688..f7fbce8e56 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -6,7 +6,8 @@ use crate::sync::{ ChainId, manager::{BlockProcessType, SyncMessage}, }; -use beacon_chain::block_verification_types::{AsBlock, RpcBlock}; +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::historical_data_columns::HistoricalDataColumnError; use beacon_chain::{ @@ -51,16 +52,16 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> { /// /// This separate function was required to prevent a cycle during compiler /// type checking. - pub fn generate_rpc_beacon_block_process_fn( + pub fn generate_lookup_beacon_block_process_fn( self: Arc<Self>, block_root: Hash256, - block: RpcBlock<T::EthSpec>, + block: LookupBlock<T::EthSpec>, seen_timestamp: Duration, process_type: BlockProcessType, ) -> AsyncFn { let process_fn = async move { let duplicate_cache = self.duplicate_cache.clone(); - self.process_rpc_block( + self.process_lookup_block( block_root, block, seen_timestamp, @@ -73,15 +74,15 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> { } /// Returns the `process_fn` and `ignore_fn` required when requeuing an RPC block. - pub fn generate_rpc_beacon_block_fns( + pub fn generate_lookup_beacon_block_fns( self: Arc<Self>, block_root: Hash256, - block: RpcBlock<T::EthSpec>, + block: LookupBlock<T::EthSpec>, seen_timestamp: Duration, process_type: BlockProcessType, ) -> (AsyncFn, BlockingFn) { // An async closure which will import the block. - let process_fn = self.clone().generate_rpc_beacon_block_process_fn( + let process_fn = self.clone().generate_lookup_beacon_block_process_fn( block_root, block, seen_timestamp, @@ -107,10 +108,10 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> { skip_all, fields(?block_root), )] - pub async fn process_rpc_block( + pub async fn process_lookup_block( self: Arc<NetworkBeaconProcessor<T>>, block_root: Hash256, - block: RpcBlock<T::EthSpec>, + block: LookupBlock<T::EthSpec>, seen_timestamp: Duration, process_type: BlockProcessType, duplicate_cache: DuplicateCache, @@ -118,14 +119,14 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> { // Check if the block is already being imported through another source let Some(handle) = duplicate_cache.check_and_insert(block_root) else { debug!( - action = "sending rpc block to reprocessing queue", + action = "sending lookup block to reprocessing queue", %block_root, ?process_type, "Gossip block is being processed" ); // Send message to work reprocess queue to retry the block - let (process_fn, ignore_fn) = self.clone().generate_rpc_beacon_block_fns( + let (process_fn, ignore_fn) = self.clone().generate_lookup_beacon_block_fns( block_root, block, seen_timestamp, @@ -160,7 +161,7 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> { slot = %block.slot(), commitments_formatted, ?process_type, - "Processing RPC block" + "Processing Lookup block" ); let signed_beacon_block = block.block_cloned(); @@ -530,7 +531,7 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> { pub async fn process_chain_segment( &self, process_id: ChainSegmentProcessId, - downloaded_blocks: Vec<RpcBlock<T::EthSpec>>, + downloaded_blocks: Vec<RangeSyncBlock<T::EthSpec>>, ) { let ChainSegmentProcessId::RangeBatchId(chain_id, epoch) = process_id else { // This is a request from range sync, this should _never_ happen @@ -611,7 +612,7 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> { pub fn process_chain_segment_backfill( &self, process_id: ChainSegmentProcessId, - downloaded_blocks: Vec<RpcBlock<T::EthSpec>>, + downloaded_blocks: Vec<RangeSyncBlock<T::EthSpec>>, ) { let ChainSegmentProcessId::BackSyncBatchId(epoch) = process_id else { // this a request from RangeSync, this should _never_ happen @@ -682,7 +683,7 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> { #[instrument(skip_all)] async fn process_blocks<'a>( &self, - downloaded_blocks: impl Iterator<Item = &'a RpcBlock<T::EthSpec>>, + downloaded_blocks: impl Iterator<Item = &'a RangeSyncBlock<T::EthSpec>>, notify_execution_layer: NotifyExecutionLayer, ) -> (usize, Result<(), ChainSegmentFailed>) { let blocks: Vec<_> = downloaded_blocks.cloned().collect(); @@ -716,23 +717,13 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> { #[instrument(skip_all)] fn process_backfill_blocks( &self, - downloaded_blocks: Vec<RpcBlock<T::EthSpec>>, + downloaded_blocks: Vec<RangeSyncBlock<T::EthSpec>>, ) -> (usize, Result<(), ChainSegmentFailed>) { let total_blocks = downloaded_blocks.len(); - let mut available_blocks = vec![]; - - for downloaded_block in downloaded_blocks { - match downloaded_block { - RpcBlock::FullyAvailable(available_block) => available_blocks.push(available_block), - RpcBlock::BlockOnly { .. } => return ( - 0, - Err(ChainSegmentFailed { - peer_action: None, - message: "Invalid downloaded_blocks segment. All downloaded blocks must be fully available".to_string() - }) - ), - } - } + let available_blocks = downloaded_blocks + .into_iter() + .map(|block| block.into_available_block()) + .collect::<Vec<_>>(); match self .chain diff --git a/beacon_node/network/src/network_beacon_processor/tests.rs b/beacon_node/network/src/network_beacon_processor/tests.rs index 4b0ca0d46c..5fa8c729cb 100644 --- a/beacon_node/network/src/network_beacon_processor/tests.rs +++ b/beacon_node/network/src/network_beacon_processor/tests.rs @@ -8,7 +8,7 @@ use crate::{ service::NetworkMessage, sync::{SyncMessage, manager::BlockProcessType}, }; -use beacon_chain::block_verification_types::RpcBlock; +use beacon_chain::block_verification_types::LookupBlock; use beacon_chain::custody_context::NodeCustodyType; use beacon_chain::data_column_verification::validate_data_column_sidecar_for_gossip_fulu; use beacon_chain::kzg_utils::blobs_to_data_column_sidecars; @@ -437,36 +437,24 @@ impl TestRig { } } - pub fn enqueue_rpc_block(&self) { + pub fn enqueue_lookup_block(&self) { let block_root = self.next_block.canonical_root(); self.network_beacon_processor - .send_rpc_beacon_block( + .send_lookup_beacon_block( block_root, - RpcBlock::new( - self.next_block.clone(), - None, - &self._harness.chain.data_availability_checker, - self._harness.spec.clone(), - ) - .unwrap(), + LookupBlock::new(self.next_block.clone()), std::time::Duration::default(), BlockProcessType::SingleBlock { id: 0 }, ) .unwrap(); } - pub fn enqueue_single_lookup_rpc_block(&self) { + pub fn enqueue_single_lookup_block(&self) { let block_root = self.next_block.canonical_root(); self.network_beacon_processor - .send_rpc_beacon_block( + .send_lookup_beacon_block( block_root, - RpcBlock::new( - self.next_block.clone(), - None, - &self._harness.chain.data_availability_checker, - self._harness.spec.clone(), - ) - .unwrap(), + LookupBlock::new(self.next_block.clone()), std::time::Duration::default(), BlockProcessType::SingleBlock { id: 1 }, ) @@ -1305,7 +1293,7 @@ async fn attestation_to_unknown_block_processed(import_method: BlockImportMethod } } BlockImportMethod::Rpc => { - rig.enqueue_rpc_block(); + rig.enqueue_lookup_block(); events.push(WorkType::RpcBlock); if num_blobs > 0 { rig.enqueue_single_lookup_rpc_blobs(); @@ -1391,7 +1379,7 @@ async fn aggregate_attestation_to_unknown_block(import_method: BlockImportMethod } } BlockImportMethod::Rpc => { - rig.enqueue_rpc_block(); + rig.enqueue_lookup_block(); events.push(WorkType::RpcBlock); if num_blobs > 0 { rig.enqueue_single_lookup_rpc_blobs(); @@ -1585,7 +1573,7 @@ async fn test_rpc_block_reprocessing() { 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_rpc_block(); + rig.enqueue_single_lookup_block(); rig.assert_event_journal_completes(&[WorkType::RpcBlock]) .await; diff --git a/beacon_node/network/src/sync/backfill_sync/mod.rs b/beacon_node/network/src/sync/backfill_sync/mod.rs index 801c9eca4d..0f80138d24 100644 --- a/beacon_node/network/src/sync/backfill_sync/mod.rs +++ b/beacon_node/network/src/sync/backfill_sync/mod.rs @@ -19,7 +19,7 @@ use crate::sync::manager::BatchProcessResult; use crate::sync::network_context::{ RangeRequestId, RpcRequestSendError, RpcResponseError, SyncNetworkContext, }; -use beacon_chain::block_verification_types::RpcBlock; +use beacon_chain::block_verification_types::RangeSyncBlock; use beacon_chain::{BeaconChain, BeaconChainTypes}; use lighthouse_network::service::api_types::Id; use lighthouse_network::types::{BackFillState, NetworkGlobals}; @@ -55,7 +55,7 @@ const MAX_BATCH_DOWNLOAD_ATTEMPTS: u8 = 10; /// after `MAX_BATCH_PROCESSING_ATTEMPTS` times, it is considered faulty. const MAX_BATCH_PROCESSING_ATTEMPTS: u8 = 10; -type RpcBlocks<E> = Vec<RpcBlock<E>>; +type RpcBlocks<E> = Vec<RangeSyncBlock<E>>; type BackFillBatchInfo<E> = BatchInfo<E, BackFillBatchConfig<E>, RpcBlocks<E>>; @@ -390,7 +390,7 @@ impl<T: BeaconChainTypes> BackFillSync<T> { batch_id: BatchId, peer_id: &PeerId, request_id: Id, - blocks: Vec<RpcBlock<T::EthSpec>>, + blocks: Vec<RangeSyncBlock<T::EthSpec>>, ) -> Result<ProcessResult, BackFillError> { // check if we have this batch let Some(batch) = self.batches.get_mut(&batch_id) else { diff --git a/beacon_node/network/src/sync/batch.rs b/beacon_node/network/src/sync/batch.rs index e87ffd119e..10af1bf503 100644 --- a/beacon_node/network/src/sync/batch.rs +++ b/beacon_node/network/src/sync/batch.rs @@ -1,4 +1,4 @@ -use beacon_chain::block_verification_types::RpcBlock; +use beacon_chain::block_verification_types::RangeSyncBlock; use educe::Educe; use lighthouse_network::PeerId; use lighthouse_network::rpc::methods::BlocksByRangeRequest; @@ -449,7 +449,7 @@ impl<E: EthSpec, B: BatchConfig, D: Hash> BatchInfo<E, B, D> { } // BatchInfo implementations for RangeSync -impl<E: EthSpec, B: BatchConfig> BatchInfo<E, B, Vec<RpcBlock<E>>> { +impl<E: EthSpec, B: BatchConfig> BatchInfo<E, B, Vec<RangeSyncBlock<E>>> { /// Returns a BlocksByRange request associated with the batch. pub fn to_blocks_by_range_request(&self) -> (BlocksByRangeRequest, ByRangeRequestType) { ( diff --git a/beacon_node/network/src/sync/block_sidecar_coupling.rs b/beacon_node/network/src/sync/block_sidecar_coupling.rs index a287771854..98cf3e0a1f 100644 --- a/beacon_node/network/src/sync/block_sidecar_coupling.rs +++ b/beacon_node/network/src/sync/block_sidecar_coupling.rs @@ -1,6 +1,6 @@ use beacon_chain::{ BeaconChainTypes, - block_verification_types::{AvailableBlockData, RpcBlock}, + block_verification_types::{AvailableBlockData, RangeSyncBlock}, data_availability_checker::DataAvailabilityChecker, data_column_verification::CustodyDataColumn, get_block_root, @@ -200,7 +200,7 @@ impl<E: EthSpec> RangeBlockComponentsRequest<E> { &mut self, da_checker: Arc<DataAvailabilityChecker<T>>, spec: Arc<ChainSpec>, - ) -> Option<Result<Vec<RpcBlock<E>>, CouplingError>> + ) -> Option<Result<Vec<RangeSyncBlock<E>>, CouplingError>> where T: BeaconChainTypes<EthSpec = E>, { @@ -288,7 +288,7 @@ impl<E: EthSpec> RangeBlockComponentsRequest<E> { blobs: Vec<Arc<BlobSidecar<E>>>, da_checker: Arc<DataAvailabilityChecker<T>>, spec: Arc<ChainSpec>, - ) -> Result<Vec<RpcBlock<E>>, CouplingError> + ) -> Result<Vec<RangeSyncBlock<E>>, CouplingError> where T: BeaconChainTypes<EthSpec = E>, { @@ -335,7 +335,7 @@ impl<E: EthSpec> RangeBlockComponentsRequest<E> { })?; let block_data = AvailableBlockData::new_with_blobs(blobs); responses.push( - RpcBlock::new(block, Some(block_data), &da_checker, spec.clone()) + RangeSyncBlock::new(block, block_data, &da_checker, spec.clone()) .map_err(|e| CouplingError::BlobPeerFailure(format!("{e:?}")))?, ) } @@ -360,7 +360,7 @@ impl<E: EthSpec> RangeBlockComponentsRequest<E> { attempt: usize, da_checker: Arc<DataAvailabilityChecker<T>>, spec: Arc<ChainSpec>, - ) -> Result<Vec<RpcBlock<E>>, CouplingError> + ) -> Result<Vec<RangeSyncBlock<E>>, CouplingError> where T: BeaconChainTypes<EthSpec = E>, { @@ -388,12 +388,12 @@ impl<E: EthSpec> RangeBlockComponentsRequest<E> { // Now iterate all blocks ensuring that the block roots of each block and data column match, // plus we have columns for our custody requirements - let mut rpc_blocks = Vec::with_capacity(blocks.len()); + let mut range_sync_blocks = Vec::with_capacity(blocks.len()); let exceeded_retries = attempt >= MAX_COLUMN_RETRIES; for block in blocks { let block_root = get_block_root(&block); - rpc_blocks.push(if block.num_expected_blobs() > 0 { + range_sync_blocks.push(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(); @@ -441,11 +441,11 @@ impl<E: EthSpec> RangeBlockComponentsRequest<E> { let block_data = AvailableBlockData::new_with_data_columns(custody_columns.iter().map(|c| c.as_data_column().clone()).collect::<Vec<_>>()); - RpcBlock::new(block, Some(block_data), &da_checker, spec.clone()) + RangeSyncBlock::new(block, block_data, &da_checker, spec.clone()) .map_err(|e| CouplingError::InternalError(format!("{:?}", e)))? } else { // Block has no data, expects zero columns - RpcBlock::new(block, Some(AvailableBlockData::NoData), &da_checker, spec.clone()) + RangeSyncBlock::new(block, AvailableBlockData::NoData, &da_checker, spec.clone()) .map_err(|e| CouplingError::InternalError(format!("{:?}", e)))? }); } @@ -458,7 +458,7 @@ impl<E: EthSpec> RangeBlockComponentsRequest<E> { debug!(?remaining_roots, "Not all columns consumed for block"); } - Ok(rpc_blocks) + Ok(range_sync_blocks) } } @@ -947,7 +947,7 @@ mod tests { } let result: Result< - Vec<beacon_chain::block_verification_types::RpcBlock<E>>, + Vec<beacon_chain::block_verification_types::RangeSyncBlock<E>>, crate::sync::block_sidecar_coupling::CouplingError, > = info.responses(da_checker.clone(), spec.clone()).unwrap(); assert!(result.is_err()); @@ -981,10 +981,10 @@ mod tests { // WHEN: Attempting to get responses again let result = info.responses(da_checker, spec).unwrap(); - // THEN: Should succeed with complete RPC blocks + // THEN: Should succeed with complete RangeSync blocks assert!(result.is_ok()); - let rpc_blocks = result.unwrap(); - assert_eq!(rpc_blocks.len(), 2); + let range_sync_blocks = result.unwrap(); + assert_eq!(range_sync_blocks.len(), 2); } #[test] diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index 7e2c0d9a94..ff630bb470 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -17,7 +17,8 @@ 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::{AsBlock, RpcBlock}; +use beacon_chain::block_verification_types::LookupBlock; +use beacon_chain::block_verification_types::{AsBlock, RangeSyncBlock}; use beacon_chain::{BeaconChain, BeaconChainTypes, BlockProcessStatus, EngineState}; use custody::CustodyRequestResult; use fnv::FnvHashMap; @@ -735,7 +736,7 @@ impl<T: BeaconChainTypes> SyncNetworkContext<T> { &mut self, id: ComponentsByRangeRequestId, range_block_component: RangeBlockComponent<T::EthSpec>, - ) -> Option<Result<Vec<RpcBlock<T::EthSpec>>, RpcResponseError>> { + ) -> Option<Result<Vec<RangeSyncBlock<T::EthSpec>>, RpcResponseError>> { let Entry::Occupied(mut entry) = self.components_by_range_requests.entry(id) else { metrics::inc_counter_vec(&metrics::SYNC_UNKNOWN_NETWORK_REQUESTS, &["range_blocks"]); return None; @@ -1588,21 +1589,15 @@ impl<T: BeaconChainTypes> SyncNetworkContext<T> { .beacon_processor_if_enabled() .ok_or(SendErrorProcessor::ProcessorNotAvailable)?; - let block = RpcBlock::new( - block, - None, - &self.chain.data_availability_checker, - self.chain.spec.clone(), - ) - .map_err(|_| SendErrorProcessor::SendError)?; + let lookup_block = LookupBlock::new(block); - debug!(block = ?block_root, block_slot = %block.slot(), id, "Sending block for processing"); + debug!(block = ?block_root, block_slot = %lookup_block.slot(), id, "Sending block for processing"); // Lookup sync event safety: If `beacon_processor.send_rpc_beacon_block` returns Ok() sync // must receive a single `SyncMessage::BlockComponentProcessed` with this process type beacon_processor - .send_rpc_beacon_block( + .send_lookup_beacon_block( block_root, - block, + lookup_block, seen_timestamp, BlockProcessType::SingleBlock { id }, ) diff --git a/beacon_node/network/src/sync/range_sync/chain.rs b/beacon_node/network/src/sync/range_sync/chain.rs index e3ff638121..d533d8ed0d 100644 --- a/beacon_node/network/src/sync/range_sync/chain.rs +++ b/beacon_node/network/src/sync/range_sync/chain.rs @@ -10,7 +10,7 @@ use crate::sync::block_sidecar_coupling::CouplingError; use crate::sync::network_context::{RangeRequestId, RpcRequestSendError, RpcResponseError}; use crate::sync::{BatchProcessResult, network_context::SyncNetworkContext}; use beacon_chain::BeaconChainTypes; -use beacon_chain::block_verification_types::RpcBlock; +use beacon_chain::block_verification_types::RangeSyncBlock; use lighthouse_network::service::api_types::Id; use lighthouse_network::{PeerAction, PeerId}; use logging::crit; @@ -40,7 +40,7 @@ const BATCH_BUFFER_SIZE: u8 = 5; /// and continued is now in an inconsistent state. pub type ProcessingResult = Result<KeepChain, RemoveChain>; -type RpcBlocks<E> = Vec<RpcBlock<E>>; +type RpcBlocks<E> = Vec<RangeSyncBlock<E>>; type RangeSyncBatchInfo<E> = BatchInfo<E, RangeSyncBatchConfig<E>, RpcBlocks<E>>; type RangeSyncBatches<E> = BTreeMap<BatchId, RangeSyncBatchInfo<E>>; @@ -273,7 +273,7 @@ impl<T: BeaconChainTypes> SyncingChain<T> { batch_id: BatchId, peer_id: &PeerId, request_id: Id, - blocks: Vec<RpcBlock<T::EthSpec>>, + blocks: Vec<RangeSyncBlock<T::EthSpec>>, ) -> ProcessingResult { let _guard = self.span.clone().entered(); // check if we have this batch diff --git a/beacon_node/network/src/sync/range_sync/range.rs b/beacon_node/network/src/sync/range_sync/range.rs index 9fd72ac98a..6509ac3cb3 100644 --- a/beacon_node/network/src/sync/range_sync/range.rs +++ b/beacon_node/network/src/sync/range_sync/range.rs @@ -47,7 +47,7 @@ use crate::status::ToStatusMessage; use crate::sync::BatchProcessResult; use crate::sync::batch::BatchId; use crate::sync::network_context::{RpcResponseError, SyncNetworkContext}; -use beacon_chain::block_verification_types::RpcBlock; +use beacon_chain::block_verification_types::RangeSyncBlock; use beacon_chain::{BeaconChain, BeaconChainTypes}; use lighthouse_network::rpc::GoodbyeReason; use lighthouse_network::service::api_types::Id; @@ -213,7 +213,7 @@ where chain_id: ChainId, batch_id: BatchId, request_id: Id, - blocks: Vec<RpcBlock<T::EthSpec>>, + blocks: Vec<RangeSyncBlock<T::EthSpec>>, ) { // check if this chunk removes the chain match self.chains.call_by_id(chain_id, |chain| { diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index 769a11d976..cd872df887 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -7,6 +7,7 @@ use crate::sync::{ manager::{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, NotifyExecutionLayer, @@ -464,7 +465,7 @@ impl TestRig { panic!("Test consumer requested unknown block: {id:?}") }) .block_data() - .and_then(|d| d.blobs()) + .blobs() .unwrap_or_else(|| panic!("Block {id:?} has no blobs")) .iter() .find(|blob| blob.index == id.index) @@ -528,7 +529,7 @@ impl TestRig { panic!("Test consumer requested unknown block: {id:?}") }) .block_data() - .and_then(|d| d.data_columns()) + .data_columns() .unwrap_or_else(|| panic!("Block id {id:?} has no columns")); id.columns .iter() @@ -594,7 +595,7 @@ impl TestRig { // - Some blocks may not have blobs as the blob count is random let blobs = (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().and_then(|d| d.blobs())) + .filter_map(|block| block.block_data().blobs()) .flat_map(|blobs| blobs.into_iter()) .collect::<Vec<_>>(); self.send_rpc_blobs_response(req_id, peer_id, &blobs); @@ -610,7 +611,7 @@ impl TestRig { // - Some blocks may not have columns as the blob count is random 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().and_then(|d| d.data_columns())) + .filter_map(|block| block.block_data().data_columns()) .flat_map(|columns| { columns .into_iter() @@ -786,10 +787,10 @@ impl TestRig { } fn corrupt_last_block_signature(&mut self) { - let rpc_block = self.get_last_block().clone(); - let mut block = (*rpc_block.block_cloned()).clone(); - let blobs = rpc_block.block_data().and_then(|d| d.blobs()); - let columns = rpc_block.block_data().and_then(|d| d.data_columns()); + 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(); *block.signature_mut() = self.valid_signature(); self.re_insert_block(Arc::new(block), blobs, columns); } @@ -801,15 +802,15 @@ impl TestRig { } fn corrupt_last_blob_proposer_signature(&mut self) { - let rpc_block = self.get_last_block().clone(); - let block = rpc_block.block_cloned(); - let mut blobs = rpc_block + let range_sync_block = self.get_last_block().clone(); + let block = range_sync_block.block_cloned(); + let mut blobs = range_sync_block .block_data() - .and_then(|d| d.blobs()) + .blobs() .expect("no blobs") .into_iter() .collect::<Vec<_>>(); - let columns = rpc_block.block_data().and_then(|d| d.data_columns()); + 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 = @@ -822,15 +823,15 @@ impl TestRig { } fn corrupt_last_blob_kzg_proof(&mut self) { - let rpc_block = self.get_last_block().clone(); - let block = rpc_block.block_cloned(); - let mut blobs = rpc_block + let range_sync_block = self.get_last_block().clone(); + let block = range_sync_block.block_cloned(); + let mut blobs = range_sync_block .block_data() - .and_then(|d| d.blobs()) + .blobs() .expect("no blobs") .into_iter() .collect::<Vec<_>>(); - let columns = rpc_block.block_data().and_then(|d| d.data_columns()); + 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 = @@ -843,12 +844,12 @@ impl TestRig { } fn corrupt_last_column_proposer_signature(&mut self) { - let rpc_block = self.get_last_block().clone(); - let block = rpc_block.block_cloned(); - let blobs = rpc_block.block_data().and_then(|d| d.blobs()); - let mut columns = rpc_block + 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() - .and_then(|d| d.data_columns()) + .data_columns() .expect("no columns"); let first = columns.first_mut().expect("empty columns"); Arc::make_mut(first) @@ -859,12 +860,12 @@ impl TestRig { } fn corrupt_last_column_kzg_proof(&mut self) { - let rpc_block = self.get_last_block().clone(); - let block = rpc_block.block_cloned(); - let blobs = rpc_block.block_data().and_then(|d| d.blobs()); - let mut columns = rpc_block + 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() - .and_then(|d| d.data_columns()) + .data_columns() .expect("no columns"); let first = columns.first_mut().expect("empty columns"); let column = Arc::make_mut(first); @@ -873,7 +874,7 @@ impl TestRig { self.re_insert_block(block, blobs, Some(columns)); } - fn get_last_block(&self) -> &RpcBlock<E> { + fn get_last_block(&self) -> &RangeSyncBlock<E> { let (_, last_block) = self .network_blocks_by_root .iter() @@ -893,13 +894,13 @@ impl TestRig { let block_root = block.canonical_root(); let block_slot = block.slot(); let block_data = if let Some(columns) = columns { - Some(AvailableBlockData::new_with_data_columns(columns)) + AvailableBlockData::new_with_data_columns(columns) } else if let Some(blobs) = blobs { - Some(AvailableBlockData::new_with_blobs(blobs)) + AvailableBlockData::new_with_blobs(blobs) } else { - Some(AvailableBlockData::NoData) + AvailableBlockData::NoData }; - let rpc_block = RpcBlock::new( + let range_sync_block = RangeSyncBlock::new( block, block_data, &self.harness.chain.data_availability_checker, @@ -907,8 +908,9 @@ impl TestRig { ) .unwrap(); self.network_blocks_by_slot - .insert(block_slot, rpc_block.clone()); - self.network_blocks_by_root.insert(block_root, rpc_block); + .insert(block_slot, range_sync_block.clone()); + self.network_blocks_by_root + .insert(block_root, range_sync_block); } /// Trigger a lookup with the last created block @@ -947,7 +949,7 @@ impl TestRig { /// Import a block directly into the chain without going through lookup sync async fn import_block_by_root(&mut self, block_root: Hash256) { - let rpc_block = self + let range_sync_block = self .network_blocks_by_root .get(&block_root) .unwrap_or_else(|| panic!("No block for root {block_root}")) @@ -957,9 +959,9 @@ impl TestRig { .chain .process_block( block_root, - rpc_block, + range_sync_block, NotifyExecutionLayer::Yes, - BlockImportSource::Gossip, + BlockImportSource::RangeSync, || Ok(()), ) .await @@ -979,7 +981,7 @@ impl TestRig { let blobs = self .get_last_block() .block_data() - .and_then(|d| d.blobs()) + .blobs() .expect("no blobs"); let blob = blobs.first().expect("empty blobs"); self.trigger_unknown_parent_blob(peer_id, blob.clone()); @@ -990,7 +992,7 @@ impl TestRig { let columns = self .get_last_block() .block_data() - .and_then(|d| d.data_columns()) + .data_columns() .expect("No data columns"); let column = columns.first().expect("empty columns"); self.trigger_unknown_parent_column(peer_id, column.clone()); @@ -1475,15 +1477,14 @@ impl TestRig { ) -> AvailabilityProcessingStatus { // Simulate importing block from another source. Don't use GossipVerified as it checks with // the clock, which does not match the timestamp in the payload. - let block_root = block.canonical_root(); - let rpc_block = RpcBlock::BlockOnly { block_root, block }; + let lookup_block = LookupBlock::new(block); self.harness .chain .process_block( - block_root, - rpc_block, + lookup_block.block_root(), + lookup_block, NotifyExecutionLayer::Yes, - BlockImportSource::Gossip, + BlockImportSource::Lookup, || Ok(()), ) .await @@ -2196,10 +2197,7 @@ async fn blobs_in_da_checker_skip_download() { }; r.build_chain(1).await; let block = r.get_last_block().clone(); - let blobs = block - .block_data() - .and_then(|d| d.blobs()) - .expect("block with no blobs"); + let blobs = block.block_data().blobs().expect("block with no blobs"); for blob in &blobs { r.insert_blob_to_da_checker(blob.clone()); } diff --git a/beacon_node/network/src/sync/tests/mod.rs b/beacon_node/network/src/sync/tests/mod.rs index f00cf5841d..6e948e4726 100644 --- a/beacon_node/network/src/sync/tests/mod.rs +++ b/beacon_node/network/src/sync/tests/mod.rs @@ -3,7 +3,7 @@ use crate::sync::SyncMessage; use crate::sync::block_lookups::BlockLookupsMetrics; use crate::sync::manager::SyncManager; use crate::sync::tests::lookups::SimulateConfig; -use beacon_chain::block_verification_types::RpcBlock; +use beacon_chain::block_verification_types::RangeSyncBlock; use beacon_chain::builder::Witness; use beacon_chain::custody_context::NodeCustodyType; use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType}; @@ -77,8 +77,8 @@ struct TestRig { rng: ChaCha20Rng, fork_name: ForkName, /// Blocks that will be used in the test but may not be known to `harness` yet. - network_blocks_by_root: HashMap<Hash256, RpcBlock<E>>, - network_blocks_by_slot: HashMap<Slot, RpcBlock<E>>, + network_blocks_by_root: HashMap<Hash256, RangeSyncBlock<E>>, + network_blocks_by_slot: HashMap<Slot, RangeSyncBlock<E>>, penalties: Vec<ReportedPenalty>, /// All seen lookups through the test run seen_lookups: HashMap<Id, SeenLookup>, diff --git a/beacon_node/network/src/sync/tests/range.rs b/beacon_node/network/src/sync/tests/range.rs index 67395ccd25..c19ee8eb6d 100644 --- a/beacon_node/network/src/sync/tests/range.rs +++ b/beacon_node/network/src/sync/tests/range.rs @@ -10,7 +10,7 @@ use beacon_chain::block_verification_types::AvailableBlockData; use beacon_chain::custody_context::NodeCustodyType; use beacon_chain::data_column_verification::CustodyDataColumn; use beacon_chain::test_utils::{AttestationStrategy, BlockStrategy}; -use beacon_chain::{EngineState, NotifyExecutionLayer, block_verification_types::RpcBlock}; +use beacon_chain::{EngineState, NotifyExecutionLayer, block_verification_types::RangeSyncBlock}; use beacon_processor::WorkType; use lighthouse_network::rpc::RequestType; use lighthouse_network::rpc::methods::{ @@ -430,7 +430,7 @@ impl TestRig { .chain .process_block( block_root, - build_rpc_block(block.into(), &data_sidecars, self.harness.chain.clone()), + build_range_sync_block(block.into(), &data_sidecars, self.harness.chain.clone()), NotifyExecutionLayer::Yes, BlockImportSource::RangeSync, || Ok(()), @@ -443,17 +443,17 @@ impl TestRig { } } -fn build_rpc_block( +fn build_range_sync_block( block: Arc<SignedBeaconBlock<E>>, data_sidecars: &Option<DataSidecars<E>>, chain: Arc<BeaconChain<T>>, -) -> RpcBlock<E> { +) -> RangeSyncBlock<E> { match data_sidecars { Some(DataSidecars::Blobs(blobs)) => { let block_data = AvailableBlockData::new_with_blobs(blobs.clone()); - RpcBlock::new( + RangeSyncBlock::new( block, - Some(block_data), + block_data, &chain.data_availability_checker, chain.spec.clone(), ) @@ -466,18 +466,18 @@ fn build_rpc_block( .map(|c| c.as_data_column().clone()) .collect::<Vec<_>>(), ); - RpcBlock::new( + RangeSyncBlock::new( block, - Some(block_data), + block_data, &chain.data_availability_checker, chain.spec.clone(), ) .unwrap() } // Block has no data, expects zero columns - None => RpcBlock::new( + None => RangeSyncBlock::new( block, - Some(AvailableBlockData::NoData), + AvailableBlockData::NoData, &chain.data_availability_checker, chain.spec.clone(), ) diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index ca77dc8d79..07a7d4c6b6 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -3,7 +3,7 @@ use crate::decode::{ssz_decode_file, ssz_decode_file_with, ssz_decode_state, yam use ::fork_choice::{PayloadVerificationStatus, ProposerHeadError}; use beacon_chain::beacon_proposer_cache::compute_proposer_duties_from_head; use beacon_chain::blob_verification::GossipBlobError; -use beacon_chain::block_verification_types::RpcBlock; +use beacon_chain::block_verification_types::LookupBlock; use beacon_chain::chain_config::{ DEFAULT_RE_ORG_HEAD_THRESHOLD, DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION, DEFAULT_RE_ORG_PARENT_THRESHOLD, DisallowedReOrgOffsets, @@ -561,21 +561,13 @@ impl<E: EthSpec> Tester<E> { let block = Arc::new(block); let result: Result<Result<Hash256, ()>, _> = self - .block_on_dangerous( - self.harness.chain.process_block( - block_root, - RpcBlock::new( - block.clone(), - None, - &self.harness.chain.data_availability_checker, - self.harness.chain.spec.clone(), - ) - .map_err(|e| Error::InternalError(format!("{:?}", e)))?, - NotifyExecutionLayer::Yes, - BlockImportSource::Lookup, - || Ok(()), - ), - )? + .block_on_dangerous(self.harness.chain.process_block( + block_root, + LookupBlock::new(block.clone()), + NotifyExecutionLayer::Yes, + BlockImportSource::Lookup, + || Ok(()), + ))? .map(|avail: AvailabilityProcessingStatus| avail.try_into()); let success = data_column_success && result.as_ref().is_ok_and(|inner| inner.is_ok()); if success != valid { @@ -659,21 +651,13 @@ impl<E: EthSpec> Tester<E> { let block = Arc::new(block); let result: Result<Result<Hash256, ()>, _> = self - .block_on_dangerous( - self.harness.chain.process_block( - block_root, - RpcBlock::new( - block.clone(), - None, - &self.harness.chain.data_availability_checker, - self.harness.chain.spec.clone(), - ) - .map_err(|e| Error::InternalError(format!("{:?}", e)))?, - NotifyExecutionLayer::Yes, - BlockImportSource::Lookup, - || Ok(()), - ), - )? + .block_on_dangerous(self.harness.chain.process_block( + block_root, + LookupBlock::new(block.clone()), + NotifyExecutionLayer::Yes, + BlockImportSource::Lookup, + || Ok(()), + ))? .map(|avail: AvailabilityProcessingStatus| avail.try_into()); let success = blob_success && result.as_ref().is_ok_and(|inner| inner.is_ok()); if success != valid { From 4eecca6da737e922c973516edb502772eba2b204 Mon Sep 17 00:00:00 2001 From: Mac L <mjladson@pm.me> Date: Mon, 16 Mar 2026 07:53:22 +0300 Subject: [PATCH 81/81] Update `/rewards` endpoints to match spec (#8967) I believe one of our rewards endpoints is slightly out of spec. We do not return the `finalized` status for `post_beacon_rewards_attestations`. Additionally, the `eth2` client doesn't expect the correct wrapper types for some other endpoints. - Update `post_beacon_rewards_attestations` server implementation to match spec. - Update all three client functions in `eth2` to the correct wrapper type. - Add missing tests for `http_api` to detect any regressions. Co-Authored-By: Mac L <mjladson@pm.me> --- beacon_node/http_api/src/lib.rs | 12 ++- beacon_node/http_api/tests/tests.rs | 118 ++++++++++++++++++++++++++-- common/eth2/src/lib.rs | 6 +- 3 files changed, 124 insertions(+), 12 deletions(-) diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 26bad809df..fc92128c91 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -1801,8 +1801,16 @@ pub fn serve<T: BeaconChainTypes>( let execution_optimistic = chain.is_optimistic_or_invalid_head().unwrap_or_default(); - Ok(api_types::GenericResponse::from(attestation_rewards)) - .map(|resp| resp.add_execution_optimistic(execution_optimistic)) + let finalized = epoch + 2 + <= chain + .canonical_head + .cached_head() + .finalized_checkpoint() + .epoch; + + Ok(api_types::GenericResponse::from(attestation_rewards)).map(|resp| { + resp.add_execution_optimistic_finalized(execution_optimistic, finalized) + }) }) }, ); diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index aed7a6b200..c9086dd876 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -7195,15 +7195,16 @@ impl ApiTester { assert_eq!(result.execution_optimistic, Some(true)); } - async fn test_get_beacon_rewards_blocks_at_head(&self) -> StandardBlockReward { + async fn test_get_beacon_rewards_blocks_at_head( + &self, + ) -> ExecutionOptimisticFinalizedResponse<StandardBlockReward> { self.client .get_beacon_rewards_blocks(CoreBlockId::Head) .await .unwrap() - .data } - async fn test_beacon_block_rewards_electra(self) -> Self { + async fn test_beacon_block_rewards_fulu(self) -> Self { for _ in 0..E::slots_per_epoch() { let state = self.harness.get_current_state(); let slot = state.slot() + Slot::new(1); @@ -7217,8 +7218,80 @@ impl ApiTester { .compute_beacon_block_reward(signed_block.message(), &mut state) .unwrap(); self.harness.extend_slots(1).await; - let api_beacon_block_reward = self.test_get_beacon_rewards_blocks_at_head().await; - assert_eq!(beacon_block_reward, api_beacon_block_reward); + let response = self.test_get_beacon_rewards_blocks_at_head().await; + assert_eq!(response.execution_optimistic, Some(false)); + assert_eq!(response.finalized, Some(false)); + assert_eq!(beacon_block_reward, response.data); + } + self + } + + async fn test_get_beacon_rewards_sync_committee_at_head( + &self, + ) -> ExecutionOptimisticFinalizedResponse<Vec<SyncCommitteeReward>> { + self.client + .post_beacon_rewards_sync_committee(CoreBlockId::Head, &[]) + .await + .unwrap() + } + + async fn test_beacon_sync_committee_rewards_fulu(self) -> Self { + for _ in 0..E::slots_per_epoch() { + let state = self.harness.get_current_state(); + let slot = state.slot() + Slot::new(1); + + let ((signed_block, _maybe_blob_sidecars), mut state) = + self.harness.make_block_return_pre_state(state, slot).await; + + let mut expected_rewards = self + .harness + .chain + .compute_sync_committee_rewards(signed_block.message(), &mut state) + .unwrap(); + expected_rewards.sort_by_key(|r| r.validator_index); + + self.harness.extend_slots(1).await; + + let response = self.test_get_beacon_rewards_sync_committee_at_head().await; + assert_eq!(response.execution_optimistic, Some(false)); + assert_eq!(response.finalized, Some(false)); + let mut api_rewards = response.data; + api_rewards.sort_by_key(|r| r.validator_index); + assert_eq!(expected_rewards, api_rewards); + } + self + } + + async fn test_get_beacon_rewards_attestations( + &self, + epoch: Epoch, + ) -> ExecutionOptimisticFinalizedResponse<StandardAttestationRewards> { + self.client + .post_beacon_rewards_attestations(epoch, &[]) + .await + .unwrap() + } + + async fn test_beacon_attestation_rewards_fulu(self) -> Self { + // Check 3 epochs. + let num_epochs = 3; + for _ in 0..num_epochs { + self.harness + .extend_slots(E::slots_per_epoch() as usize) + .await; + + let epoch = self.chain.epoch().unwrap() - 1; + + let expected_rewards = self + .harness + .chain + .compute_attestation_rewards(epoch, vec![]) + .unwrap(); + + let response = self.test_get_beacon_rewards_attestations(epoch).await; + assert_eq!(response.execution_optimistic, Some(false)); + assert_eq!(response.finalized, Some(false)); + assert_eq!(expected_rewards, response.data); } self } @@ -8534,16 +8607,47 @@ async fn expected_withdrawals_valid_capella() { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn get_beacon_rewards_blocks_electra() { +async fn get_beacon_rewards_blocks_fulu() { let mut config = ApiTesterConfig::default(); config.spec.altair_fork_epoch = Some(Epoch::new(0)); config.spec.bellatrix_fork_epoch = Some(Epoch::new(0)); config.spec.capella_fork_epoch = Some(Epoch::new(0)); config.spec.deneb_fork_epoch = Some(Epoch::new(0)); config.spec.electra_fork_epoch = Some(Epoch::new(0)); + config.spec.fulu_fork_epoch = Some(Epoch::new(0)); ApiTester::new_from_config(config) .await - .test_beacon_block_rewards_electra() + .test_beacon_block_rewards_fulu() + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn get_beacon_rewards_sync_committee_fulu() { + let mut config = ApiTesterConfig::default(); + config.spec.altair_fork_epoch = Some(Epoch::new(0)); + config.spec.bellatrix_fork_epoch = Some(Epoch::new(0)); + config.spec.capella_fork_epoch = Some(Epoch::new(0)); + config.spec.deneb_fork_epoch = Some(Epoch::new(0)); + config.spec.electra_fork_epoch = Some(Epoch::new(0)); + config.spec.fulu_fork_epoch = Some(Epoch::new(0)); + ApiTester::new_from_config(config) + .await + .test_beacon_sync_committee_rewards_fulu() + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn get_beacon_rewards_attestations_fulu() { + let mut config = ApiTesterConfig::default(); + config.spec.altair_fork_epoch = Some(Epoch::new(0)); + config.spec.bellatrix_fork_epoch = Some(Epoch::new(0)); + config.spec.capella_fork_epoch = Some(Epoch::new(0)); + config.spec.deneb_fork_epoch = Some(Epoch::new(0)); + config.spec.electra_fork_epoch = Some(Epoch::new(0)); + config.spec.fulu_fork_epoch = Some(Epoch::new(0)); + ApiTester::new_from_config(config) + .await + .test_beacon_attestation_rewards_fulu() .await; } diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index af87af14ba..40c5ef58a6 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -1802,7 +1802,7 @@ impl BeaconNodeHttpClient { &self, block_id: BlockId, validators: &[ValidatorId], - ) -> Result<GenericResponse<Vec<SyncCommitteeReward>>, Error> { + ) -> Result<ExecutionOptimisticFinalizedResponse<Vec<SyncCommitteeReward>>, Error> { let mut path = self.eth_path(V1)?; path.path_segments_mut() @@ -1819,7 +1819,7 @@ impl BeaconNodeHttpClient { pub async fn get_beacon_rewards_blocks( &self, block_id: BlockId, - ) -> Result<GenericResponse<StandardBlockReward>, Error> { + ) -> Result<ExecutionOptimisticFinalizedResponse<StandardBlockReward>, Error> { let mut path = self.eth_path(V1)?; path.path_segments_mut() @@ -1837,7 +1837,7 @@ impl BeaconNodeHttpClient { &self, epoch: Epoch, validators: &[ValidatorId], - ) -> Result<StandardAttestationRewards, Error> { + ) -> Result<ExecutionOptimisticFinalizedResponse<StandardAttestationRewards>, Error> { let mut path = self.eth_path(V1)?; path.path_segments_mut()