From 60abd4b5b985f5ef47baa799c43c085521e3e596 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Thu, 21 May 2026 23:21:20 -0700 Subject: [PATCH] Gloas alpha spec 8 (#9315) https://github.com/ethereum/consensus-specs/releases/tag/v1.7.0-alpha.8 Co-Authored-By: Eitan Seri-Levi Co-Authored-By: Michael Sproul --- beacon_node/beacon_chain/src/beacon_chain.rs | 30 ++- .../src/block_production/gloas.rs | 64 +++-- .../beacon_chain/src/execution_payload.rs | 20 +- .../gossip_verified_bid.rs | 98 +++++++- .../src/payload_bid_verification/mod.rs | 2 +- .../src/payload_bid_verification/tests.rs | 15 +- .../gossip_verified_proposer_preferences.rs | 47 +++- .../proposer_preferences_verification/mod.rs | 2 +- .../proposer_preference_cache.rs | 2 +- .../tests.rs | 6 +- .../tests/payload_invalidation.rs | 1 + beacon_node/execution_layer/src/engine_api.rs | 43 ++-- .../src/engine_api/json_structures.rs | 5 + beacon_node/execution_layer/src/lib.rs | 9 +- .../src/test_utils/mock_builder.rs | 7 +- .../src/test_utils/mock_execution_layer.rs | 7 +- beacon_node/http_api/tests/tests.rs | 2 +- .../gossip_methods.rs | 2 +- .../mainnet/config.yaml | 4 +- consensus/fork_choice/src/fork_choice.rs | 96 +++++--- consensus/proto_array/src/error.rs | 5 + .../src/fork_choice_test_definition.rs | 6 +- consensus/proto_array/src/proto_array.rs | 99 ++++++-- .../src/proto_array_fork_choice.rs | 30 +++ .../process_operations.rs | 44 ++-- .../state_processing/src/upgrade/gloas.rs | 94 ++++--- consensus/types/configs/mainnet.yaml | 4 +- .../types/src/builder/proposer_preferences.rs | 2 +- consensus/types/src/core/chain_spec.rs | 92 ++++++- consensus/types/src/state/beacon_state.rs | 4 +- testing/ef_tests/Makefile | 2 +- testing/ef_tests/src/cases/fork_choice.rs | 233 +++++++++++++++++- testing/ef_tests/src/handler.rs | 13 +- testing/ef_tests/tests/tests.rs | 6 + .../src/test_rig.rs | 8 +- .../src/proposer_preferences_service.rs | 2 +- 36 files changed, 863 insertions(+), 243 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 2259e1d809..db8f55a18a 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -96,8 +96,8 @@ use eth2::types::{ SseExtendedPayloadAttributes, SseHead, }; use execution_layer::{ - BlockProposalContents, BlockProposalContentsType, BuilderParams, ChainHealth, ExecutionLayer, - FailedCondition, PayloadAttributes, PayloadStatus, + BlockProposalContents, BlockProposalContentsType, BuilderParams, ChainHealth, + DEFAULT_GAS_LIMIT, ExecutionLayer, FailedCondition, PayloadAttributes, PayloadStatus, }; use fixed_bytes::FixedBytesExtended; use fork_choice::{ @@ -2185,12 +2185,20 @@ impl BeaconChain { // TODO(gloas) do we want to use a dedicated envelope cache instead? // Maybe the new gloas DA cache? (Or should the gloas DA cache use - // the envelopes_times_cache internally?) + // the envelopes_times_cache internally? + // The payload is considered present only if it was observed before + // the payload due deadline (PAYLOAD_DUE_BPS into the slot). + let payload_due = self.spec.get_payload_due(); let payload_present = self .envelope_times_cache .read() .cache - .contains_key(&beacon_block_root); + .get(&beacon_block_root) + .and_then(|entry| entry.timestamps.observed) + .is_some_and(|observed| { + let slot_start = self.slot_clock.start_of(request_slot); + slot_start.is_some_and(|start| observed.saturating_sub(start) < payload_due) + }); // TODO(EIP-7732): Check blob data availability. For now, default to true. let blob_data_available = true; @@ -6476,6 +6484,19 @@ impl BeaconChain { None }; + let target_gas_limit = if prepare_slot_fork.gloas_enabled() { + let proposer_gas_limit = execution_layer.get_proposer_gas_limit(proposer).await; + if proposer_gas_limit.is_none() { + warn!( + %proposer, + "No proposer gas limit configured, falling back to parent gas limit" + ); + } + proposer_gas_limit.or(Some(DEFAULT_GAS_LIMIT)) + } else { + None + }; + let payload_attributes = PayloadAttributes::new( self.slot_clock .start_of(prepare_slot) @@ -6486,6 +6507,7 @@ impl BeaconChain { withdrawals.map(Into::into), parent_beacon_block_root, slot_number, + target_gas_limit, ); execution_layer diff --git a/beacon_node/beacon_chain/src/block_production/gloas.rs b/beacon_node/beacon_chain/src/block_production/gloas.rs index 6510c20ba7..82dad6f6ad 100644 --- a/beacon_node/beacon_chain/src/block_production/gloas.rs +++ b/beacon_node/beacon_chain/src/block_production/gloas.rs @@ -2,11 +2,13 @@ use std::collections::{HashMap, HashSet}; use std::marker::PhantomData; use std::sync::Arc; +use proto_array::PayloadStatus; + use bls::{PublicKeyBytes, Signature}; use execution_layer::{ - BlockProposalContentsGloas, BuilderParams, PayloadAttributes, PayloadParameters, + BlockProposalContentsGloas, BuilderParams, DEFAULT_GAS_LIMIT, PayloadAttributes, + PayloadParameters, }; -use fork_choice::PayloadStatus; use operation_pool::CompactAttestationRef; use ssz::Encode; use state_processing::common::{get_attesting_indices_from_state, get_indexed_payload_attestation}; @@ -150,8 +152,24 @@ impl BeaconChain { verification: ProduceBlockVerification, builder_boost_factor: Option, ) -> Result, BlockProductionError> { - // Extract the parent's execution requests from the envelope (if parent was full). - let parent_execution_requests = if parent_payload_status == PayloadStatus::Full { + let parent_root = if state.slot() > 0 { + *state + .get_block_root(state.slot() - 1) + .map_err(|_| BlockProductionError::UnableToGetBlockRootFromState)? + } else { + state.latest_block_header().canonical_root() + }; + + let should_build_on_full = self + .canonical_head + .fork_choice_read_lock() + .should_build_on_full(&parent_root, parent_payload_status) + .map_err(|e| { + BlockProductionError::BeaconChain(Box::new(BeaconChainError::ForkChoiceError(e))) + })?; + + // Extract the parent's execution requests from the envelope (if building on full). + let parent_execution_requests = if should_build_on_full { parent_envelope .as_ref() .map(|env| env.message.execution_requests.clone()) @@ -197,7 +215,7 @@ impl BeaconChain { .clone() .produce_execution_payload_bid( state, - parent_payload_status, + should_build_on_full, parent_envelope, produce_at_slot, BID_VALUE_SELF_BUILD, @@ -700,12 +718,12 @@ impl BeaconChain { /// data needed to construct the `ExecutionPayloadEnvelope` after the beacon block is /// created, plus the EL block value and `should_override_builder` flag used by the /// caller to compare against any cached p2p builder bid. - #[allow(clippy::type_complexity)] + #[allow(clippy::type_complexity, clippy::too_many_arguments)] #[instrument(level = "debug", skip_all)] pub async fn produce_execution_payload_bid( self: Arc, state: BeaconState, - parent_payload_status: PayloadStatus, + should_build_on_full: bool, parent_envelope: Option>>, produce_at_slot: Slot, bid_value: u64, @@ -751,20 +769,18 @@ impl BeaconChain { let parent_bid = state.latest_execution_payload_bid()?; - // TODO(gloas): need should_extend_payload check here as well let parent_block_slot = state.latest_block_header().slot; let parent_is_pre_gloas = !self .spec .fork_name_at_slot::(parent_block_slot) .gloas_enabled(); - let parent_block_hash = - if parent_payload_status == PayloadStatus::Full || parent_is_pre_gloas { - // Build on parent bid's payload. - parent_bid.block_hash - } else { - // Skip parent bid's payload. For genesis this is the EL genesis hash. - parent_bid.parent_block_hash - }; + let parent_block_hash = if should_build_on_full || parent_is_pre_gloas { + // Build on parent bid's payload. + parent_bid.block_hash + } else { + // Skip parent bid's payload. For genesis this is the EL genesis hash. + parent_bid.parent_block_hash + }; // TODO(gloas) this should be BlockProductionVersion::V4 // V3 is okay for now as long as we're not connected to a builder @@ -953,10 +969,7 @@ fn get_execution_payload_gloas( compute_timestamp_at_slot(state, state.slot(), spec).map_err(BeaconStateError::from)?; let random = *state.get_randao_mix(current_epoch)?; - // TODO(gloas): this gas limit calc is not necessarily right let parent_bid = state.latest_execution_payload_bid()?; - let latest_gas_limit = parent_bid.gas_limit; - let is_parent_block_full = parent_block_hash == parent_bid.block_hash; let withdrawals = if is_parent_block_full { @@ -992,7 +1005,6 @@ fn get_execution_payload_gloas( random, proposer_index, parent_block_hash, - latest_gas_limit, builder_params, withdrawals, parent_beacon_block_root, @@ -1020,7 +1032,6 @@ async fn prepare_execution_payload( random: Hash256, proposer_index: u64, parent_block_hash: ExecutionBlockHash, - parent_gas_limit: u64, builder_params: BuilderParams, withdrawals: Vec, parent_beacon_block_root: Hash256, @@ -1058,6 +1069,10 @@ where .get_suggested_fee_recipient(proposer_index) .await; let slot_number = Some(builder_params.slot.as_u64()); + let target_gas_limit = execution_layer + .get_proposer_gas_limit(proposer_index) + .await + .unwrap_or(DEFAULT_GAS_LIMIT); let payload_attributes = PayloadAttributes::new( timestamp, @@ -1066,13 +1081,12 @@ where Some(withdrawals), Some(parent_beacon_block_root), slot_number, + Some(target_gas_limit), ); - - let target_gas_limit = execution_layer.get_proposer_gas_limit(proposer_index).await; let payload_parameters = PayloadParameters { parent_hash: parent_block_hash, - parent_gas_limit, - proposer_gas_limit: target_gas_limit, + parent_gas_limit: None, + proposer_gas_limit: Some(target_gas_limit), payload_attributes: &payload_attributes, forkchoice_update_params: &forkchoice_update_params, current_fork: fork, diff --git a/beacon_node/beacon_chain/src/execution_payload.rs b/beacon_node/beacon_chain/src/execution_payload.rs index 16542eea2d..c8976fc6a8 100644 --- a/beacon_node/beacon_chain/src/execution_payload.rs +++ b/beacon_node/beacon_chain/src/execution_payload.rs @@ -342,7 +342,7 @@ pub fn get_execution_payload( Ok(join_handle) } -/// Prepares an execution payload for inclusion in a block. +/// Prepares an execution payload (pre-gloas) for inclusion in a block. /// /// ## Errors /// @@ -373,6 +373,13 @@ where { let spec = &chain.spec; let fork = spec.fork_name_at_slot::(builder_params.slot); + + if fork.gloas_enabled() { + return Err(BlockProductionError::InvalidBlockVariant( + "Called pre-gloas prepare_execution_payload on a gloas block".to_string(), + )); + } + let execution_layer = chain .execution_layer .as_ref() @@ -403,25 +410,20 @@ where .get_suggested_fee_recipient(proposer_index) .await; - let slot_number = if fork.gloas_enabled() { - Some(builder_params.slot.as_u64()) - } else { - None - }; - let payload_attributes = PayloadAttributes::new( timestamp, random, suggested_fee_recipient, withdrawals, parent_beacon_block_root, - slot_number, + None, + None, ); let target_gas_limit = execution_layer.get_proposer_gas_limit(proposer_index).await; let payload_parameters = PayloadParameters { parent_hash, - parent_gas_limit: latest_execution_payload_header_gas_limit, + parent_gas_limit: Some(latest_execution_payload_header_gas_limit), proposer_gas_limit: target_gas_limit, payload_attributes: &payload_attributes, forkchoice_update_params: &forkchoice_update_params, diff --git a/beacon_node/beacon_chain/src/payload_bid_verification/gossip_verified_bid.rs b/beacon_node/beacon_chain/src/payload_bid_verification/gossip_verified_bid.rs index 1f3f074598..354705b92c 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/gossip_verified_bid.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/gossip_verified_bid.rs @@ -43,9 +43,6 @@ pub(crate) fn verify_bid_consistency( if bid.fee_recipient != proposer_preferences.message.fee_recipient { return Err(PayloadBidError::InvalidFeeRecipient); } - if bid.gas_limit != proposer_preferences.message.gas_limit { - return Err(PayloadBidError::InvalidGasLimit); - } let max_blobs_per_block = spec.max_blobs_per_block(bid_slot.epoch(E::slots_per_epoch())) as usize; @@ -161,7 +158,23 @@ impl GossipVerifiedPayloadBid { }); } - // TODO(gloas) [IGNORE] bid.parent_block_hash is the block hash of a known execution payload in fork choice. + // TODO(gloas): [IGNORE] bid.parent_block_hash is the block hash of a known execution + // payload in fork choice. + + // TODO(gloas): This uses head state's bid gas_limit as parent_gas_limit, which is only + // correct when the bid's parent is the head. If the parent is an ancestor further back + // this check may be inaccurate. Fixing this requires storing + // gas_limit in fork choice or looking it up from the store by parent_block_hash. Taking the above + // TODO into consideration maybe should persist parent block hash and gas limit in fork choice? + if let Ok(parent_bid) = head_state.latest_execution_payload_bid() + && !is_gas_limit_target_compatible( + parent_bid.gas_limit, + signed_bid.message.gas_limit, + proposer_preferences.message.target_gas_limit, + )? + { + return Err(PayloadBidError::InvalidGasLimit); + } drop(fork_choice); @@ -263,8 +276,36 @@ impl BeaconChain { } } +/// Check if `gas_limit` is compatible with `target_gas_limit` under the +/// EIP-1559 transition rule from `parent_gas_limit`. +pub fn is_gas_limit_target_compatible( + parent_gas_limit: u64, + gas_limit: u64, + target_gas_limit: u64, +) -> Result { + let max_gas_limit_difference = (parent_gas_limit / 1024) + .max(1) + .checked_sub(1) + .ok_or(PayloadBidError::InvalidGasLimit)?; + let min_gas_limit = parent_gas_limit + .checked_sub(max_gas_limit_difference) + .ok_or(PayloadBidError::InvalidGasLimit)?; + let max_gas_limit = parent_gas_limit + .checked_add(max_gas_limit_difference) + .ok_or(PayloadBidError::InvalidGasLimit)?; + + if target_gas_limit >= min_gas_limit && target_gas_limit <= max_gas_limit { + Ok(gas_limit == target_gas_limit) + } else if target_gas_limit > max_gas_limit { + Ok(gas_limit == max_gas_limit) + } else { + Ok(gas_limit == min_gas_limit) + } +} + #[cfg(test)] mod tests { + use super::is_gas_limit_target_compatible; use bls::Signature; use kzg::KzgCommitment; use ssz_types::VariableList; @@ -288,11 +329,14 @@ mod tests { } } - fn make_preferences(fee_recipient: Address, gas_limit: u64) -> SignedProposerPreferences { + fn make_preferences( + fee_recipient: Address, + target_gas_limit: u64, + ) -> SignedProposerPreferences { SignedProposerPreferences { message: ProposerPreferences { fee_recipient, - gas_limit, + target_gas_limit, ..ProposerPreferences::default() }, signature: Signature::empty(), @@ -382,13 +426,41 @@ mod tests { } #[test] - fn test_gas_limit_mismatch() { - let (state, spec) = state_and_spec(); - let current_slot = Slot::new(10); - let bid = make_bid(current_slot, Address::ZERO, 30_000_000); - let prefs = make_preferences(Address::ZERO, 50_000_000); + fn test_is_gas_limit_target_compatible_increase_within_limit() { + assert!(is_gas_limit_target_compatible(60_000_000, 60_000_100, 60_000_100).unwrap()); + } - let result = verify_bid_consistency::(&bid, current_slot, &prefs, &state, &spec); - assert!(matches!(result, Err(PayloadBidError::InvalidGasLimit))); + #[test] + fn test_is_gas_limit_target_compatible_increase_exceeding_limit() { + // max_diff = 60_000_000 / 1024 - 1 = 58_592 + // max_gas_limit = 60_000_000 + 58_592 = 60_058_592 + assert!(is_gas_limit_target_compatible(60_000_000, 60_058_592, 100_000_000).unwrap()); + } + + #[test] + fn test_is_gas_limit_target_compatible_increase_exceeding_off_by_one() { + assert!(!is_gas_limit_target_compatible(60_000_000, 60_058_593, 100_000_000).unwrap()); + } + + #[test] + fn test_is_gas_limit_target_compatible_decrease_within_limit() { + assert!(is_gas_limit_target_compatible(60_000_000, 59_999_990, 59_999_990).unwrap()); + } + + #[test] + fn test_is_gas_limit_target_compatible_decrease_exceeding_limit() { + // min_gas_limit = 60_000_000 - 58_592 = 59_941_408 + assert!(is_gas_limit_target_compatible(60_000_000, 59_941_408, 30_000_000).unwrap()); + } + + #[test] + fn test_is_gas_limit_target_compatible_target_equals_parent() { + assert!(is_gas_limit_target_compatible(60_000_000, 60_000_000, 60_000_000).unwrap()); + } + + #[test] + fn test_is_gas_limit_target_compatible_parent_underflows() { + // parent=1023: max(1023/1024, 1) - 1 = max(0, 1) - 1 = 0, no change allowed + assert!(is_gas_limit_target_compatible(1023, 1023, 60_000_000).unwrap()); } } diff --git a/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs b/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs index 514695f5c0..a40fd14872 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs @@ -48,7 +48,7 @@ pub enum PayloadBidError { }, /// The bids fee recipient doesn't match the proposer preferences fee recipient. InvalidFeeRecipient, - /// The bids gas limit doesn't match the proposer preferences gas limit. + /// The bid's gas limit is not compatible with the proposer's target gas limit. InvalidGasLimit, /// The bids execution payment is non-zero ExecutionPaymentNonZero { execution_payment: u64 }, diff --git a/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs b/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs index c68e6d9d32..ccdf64d41d 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs @@ -101,6 +101,17 @@ impl TestContext { root: Hash256::ZERO, }; + // Set a non-zero gas_limit on latest_execution_payload_bid so the gas limit + // compatibility check doesn't reject all bids at genesis. + if let Ok(bid) = state.latest_execution_payload_bid_mut() { + bid.gas_limit = 30_000_000; + } + // Update body_root to reflect the modified bid (genesis block embeds it). + let genesis_body_root = genesis_block(&state, &spec) + .expect("should build genesis block") + .body_root(); + state.latest_block_header_mut().body_root = genesis_body_root; + let inactive_keypair = &keypairs[NUM_BUILDERS]; let inactive_creds = builder_withdrawal_credentials(&inactive_keypair.pk, &spec); let inactive_builder_index = state @@ -248,7 +259,7 @@ fn make_signed_preferences( proposal_slot: Slot, validator_index: u64, fee_recipient: Address, - gas_limit: u64, + target_gas_limit: u64, ) -> Arc { Arc::new(SignedProposerPreferences { message: ProposerPreferences { @@ -256,7 +267,7 @@ fn make_signed_preferences( proposal_slot, validator_index, fee_recipient, - gas_limit, + target_gas_limit, }, signature: Signature::empty(), }) diff --git a/beacon_node/beacon_chain/src/proposer_preferences_verification/gossip_verified_proposer_preferences.rs b/beacon_node/beacon_chain/src/proposer_preferences_verification/gossip_verified_proposer_preferences.rs index 4ba33fde72..586721d8c1 100644 --- a/beacon_node/beacon_chain/src/proposer_preferences_verification/gossip_verified_proposer_preferences.rs +++ b/beacon_node/beacon_chain/src/proposer_preferences_verification/gossip_verified_proposer_preferences.rs @@ -18,13 +18,16 @@ pub(crate) fn verify_preferences_consistency( preferences: &ProposerPreferences, current_slot: Slot, head_state: &BeaconState, + spec: &ChainSpec, ) -> Result<(), ProposerPreferencesError> { let proposal_slot = preferences.proposal_slot; let validator_index = preferences.validator_index; let current_epoch = current_slot.epoch(E::slots_per_epoch()); let proposal_epoch = proposal_slot.epoch(E::slots_per_epoch()); - if proposal_epoch < current_epoch || proposal_epoch > current_epoch.saturating_add(1u64) { + if proposal_epoch < current_epoch + || proposal_epoch > current_epoch.saturating_add(spec.min_seed_lookahead) + { return Err(ProposerPreferencesError::InvalidProposalEpoch { proposal_epoch }); } @@ -35,7 +38,7 @@ pub(crate) fn verify_preferences_consistency( }); } - if !head_state.is_valid_proposal_slot(preferences)? { + if !head_state.is_valid_proposal_slot(preferences, spec)? { return Err(ProposerPreferencesError::InvalidProposalSlot { validator_index, proposal_slot, @@ -83,7 +86,12 @@ impl GossipVerifiedProposerPreferences { }); } - verify_preferences_consistency(&signed_preferences.message, current_slot, head_state)?; + verify_preferences_consistency( + &signed_preferences.message, + current_slot, + head_state, + ctx.spec, + )?; // Verify signature proposer_preferences_signature_set( @@ -155,11 +163,13 @@ impl BeaconChain { #[cfg(test)] mod tests { use types::{ - Address, BeaconState, EthSpec, Hash256, MinimalEthSpec, ProposerPreferences, Slot, + Address, BeaconState, ChainSpec, EthSpec, Hash256, MinimalEthSpec, ProposerPreferences, + Slot, }; use super::verify_preferences_consistency; use crate::proposer_preferences_verification::ProposerPreferencesError; + use crate::test_utils::{fork_name_from_env, test_spec}; type E = MinimalEthSpec; @@ -169,20 +179,28 @@ mod tests { proposal_slot, validator_index, fee_recipient: Address::ZERO, - gas_limit: 30_000_000, + target_gas_limit: 30_000_000, } } fn state() -> BeaconState { - BeaconState::new(0, <_>::default(), &E::default_spec()) + let spec = spec(); + BeaconState::new(0, <_>::default(), &spec) + } + + fn spec() -> ChainSpec { + test_spec::() } #[test] fn test_invalid_epoch_too_old() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } let current_slot = Slot::new(2 * E::slots_per_epoch()); let prefs = make_preferences(Slot::new(3), 0); - let result = verify_preferences_consistency::(&prefs, current_slot, &state()); + let result = verify_preferences_consistency::(&prefs, current_slot, &state(), &spec()); assert!(matches!( result, Err(ProposerPreferencesError::InvalidProposalEpoch { .. }) @@ -191,10 +209,13 @@ mod tests { #[test] fn test_invalid_epoch_too_far_ahead() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } let current_slot = Slot::new(E::slots_per_epoch()); let prefs = make_preferences(Slot::new(3 * E::slots_per_epoch() + 1), 0); - let result = verify_preferences_consistency::(&prefs, current_slot, &state()); + let result = verify_preferences_consistency::(&prefs, current_slot, &state(), &spec()); assert!(matches!( result, Err(ProposerPreferencesError::InvalidProposalEpoch { .. }) @@ -203,10 +224,13 @@ mod tests { #[test] fn test_proposal_slot_already_passed() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } let current_slot = Slot::new(10); let prefs = make_preferences(Slot::new(9), 0); - let result = verify_preferences_consistency::(&prefs, current_slot, &state()); + let result = verify_preferences_consistency::(&prefs, current_slot, &state(), &spec()); assert!(matches!( result, Err(ProposerPreferencesError::ProposalSlotAlreadyPassed { .. }) @@ -215,10 +239,13 @@ mod tests { #[test] fn test_proposal_slot_equal_to_current() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } let current_slot = Slot::new(10); let prefs = make_preferences(Slot::new(10), 0); - let result = verify_preferences_consistency::(&prefs, current_slot, &state()); + let result = verify_preferences_consistency::(&prefs, current_slot, &state(), &spec()); assert!(matches!( result, Err(ProposerPreferencesError::ProposalSlotAlreadyPassed { .. }) diff --git a/beacon_node/beacon_chain/src/proposer_preferences_verification/mod.rs b/beacon_node/beacon_chain/src/proposer_preferences_verification/mod.rs index a2e96dfce1..6c79e56733 100644 --- a/beacon_node/beacon_chain/src/proposer_preferences_verification/mod.rs +++ b/beacon_node/beacon_chain/src/proposer_preferences_verification/mod.rs @@ -24,7 +24,7 @@ mod tests; #[derive(Debug)] pub enum ProposerPreferencesError { - /// The proposal slot is not in the current or next epoch. + /// The proposal slot is not within the proposer lookahead. InvalidProposalEpoch { proposal_epoch: Epoch }, /// The proposal slot has already passed. ProposalSlotAlreadyPassed { diff --git a/beacon_node/beacon_chain/src/proposer_preferences_verification/proposer_preference_cache.rs b/beacon_node/beacon_chain/src/proposer_preferences_verification/proposer_preference_cache.rs index 7bbdf34888..c423418fbc 100644 --- a/beacon_node/beacon_chain/src/proposer_preferences_verification/proposer_preference_cache.rs +++ b/beacon_node/beacon_chain/src/proposer_preferences_verification/proposer_preference_cache.rs @@ -87,7 +87,7 @@ mod tests { proposal_slot: slot, validator_index, fee_recipient: Address::ZERO, - gas_limit: 30_000_000, + target_gas_limit: 30_000_000, }, signature: Signature::empty(), }), diff --git a/beacon_node/beacon_chain/src/proposer_preferences_verification/tests.rs b/beacon_node/beacon_chain/src/proposer_preferences_verification/tests.rs index 468e08ff3b..53c1c4ded3 100644 --- a/beacon_node/beacon_chain/src/proposer_preferences_verification/tests.rs +++ b/beacon_node/beacon_chain/src/proposer_preferences_verification/tests.rs @@ -112,7 +112,7 @@ impl TestContext { let slot_in_epoch = slot.as_usize() % E::slots_per_epoch() as usize; let epoch = slot.epoch(E::slots_per_epoch()); let current_epoch = state.slot().epoch(E::slots_per_epoch()); - let index = if epoch == current_epoch.saturating_add(1u64) { + let index = if epoch == current_epoch.saturating_add(self.spec.min_seed_lookahead) { E::slots_per_epoch() as usize + slot_in_epoch } else { slot_in_epoch @@ -131,7 +131,7 @@ fn make_signed_preferences( proposal_slot, validator_index, fee_recipient: Address::ZERO, - gas_limit: 30_000_000, + target_gas_limit: 30_000_000, }, signature: Signature::empty(), }) @@ -271,7 +271,7 @@ fn same_validator_different_dependent_root_not_deduplicated() { validator_index: 42, dependent_root: Hash256::repeat_byte(0xaa), fee_recipient: Address::ZERO, - gas_limit: 30_000_000, + target_gas_limit: 30_000_000, }, signature: Signature::empty(), }), diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index be85fc2245..abf1fe48a6 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -1035,6 +1035,7 @@ async fn payload_preparation() { None, None, None, + None, ); assert_eq!(rig.previous_payload_attributes(), payload_attributes); } diff --git a/beacon_node/execution_layer/src/engine_api.rs b/beacon_node/execution_layer/src/engine_api.rs index acf5f2778b..7337a29c8f 100644 --- a/beacon_node/execution_layer/src/engine_api.rs +++ b/beacon_node/execution_layer/src/engine_api.rs @@ -178,6 +178,8 @@ pub struct PayloadAttributes { pub parent_beacon_block_root: Hash256, #[superstruct(only(V4), partial_getter(copy))] pub slot_number: u64, + #[superstruct(only(V4), partial_getter(copy))] + pub target_gas_limit: u64, } impl PayloadAttributes { @@ -188,19 +190,29 @@ impl PayloadAttributes { withdrawals: Option>, parent_beacon_block_root: Option, slot_number: Option, + target_gas_limit: Option, ) -> Self { - match (withdrawals, parent_beacon_block_root, slot_number) { - (Some(withdrawals), Some(parent_beacon_block_root), Some(slot_number)) => { - PayloadAttributes::V4(PayloadAttributesV4 { - timestamp, - prev_randao, - suggested_fee_recipient, - withdrawals, - parent_beacon_block_root, - slot_number, - }) - } - (Some(withdrawals), Some(parent_beacon_block_root), None) => { + match ( + withdrawals, + parent_beacon_block_root, + slot_number, + target_gas_limit, + ) { + ( + Some(withdrawals), + Some(parent_beacon_block_root), + Some(slot_number), + Some(target_gas_limit), + ) => PayloadAttributes::V4(PayloadAttributesV4 { + timestamp, + prev_randao, + suggested_fee_recipient, + withdrawals, + parent_beacon_block_root, + slot_number, + target_gas_limit, + }), + (Some(withdrawals), Some(parent_beacon_block_root), _, _) => { PayloadAttributes::V3(PayloadAttributesV3 { timestamp, prev_randao, @@ -209,13 +221,13 @@ impl PayloadAttributes { parent_beacon_block_root, }) } - (Some(withdrawals), None, _) => PayloadAttributes::V2(PayloadAttributesV2 { + (Some(withdrawals), None, _, _) => PayloadAttributes::V2(PayloadAttributesV2 { timestamp, prev_randao, suggested_fee_recipient, withdrawals, }), - (None, _, _) => PayloadAttributes::V1(PayloadAttributesV1 { + (None, _, _, _) => PayloadAttributes::V1(PayloadAttributesV1 { timestamp, prev_randao, suggested_fee_recipient, @@ -260,7 +272,7 @@ impl From for SsePayloadAttributes { withdrawals, parent_beacon_block_root, }), - // V4 maps to V3 for SSE (slot_number is not part of the SSE spec) + // V4 maps to V3 for SSE (slot_number/target_gas_limit are not part of the SSE spec) PayloadAttributes::V4(PayloadAttributesV4 { timestamp, prev_randao, @@ -268,6 +280,7 @@ impl From for SsePayloadAttributes { withdrawals, parent_beacon_block_root, slot_number: _, + target_gas_limit: _, }) => Self::V3(SsePayloadAttributesV3 { timestamp, prev_randao, diff --git a/beacon_node/execution_layer/src/engine_api/json_structures.rs b/beacon_node/execution_layer/src/engine_api/json_structures.rs index 9d9391a1e1..fb516e3e16 100644 --- a/beacon_node/execution_layer/src/engine_api/json_structures.rs +++ b/beacon_node/execution_layer/src/engine_api/json_structures.rs @@ -777,6 +777,9 @@ pub struct JsonPayloadAttributes { #[superstruct(only(V4))] #[serde(with = "serde_utils::u64_hex_be")] pub slot_number: u64, + #[superstruct(only(V4))] + #[serde(with = "serde_utils::u64_hex_be")] + pub target_gas_limit: u64, } impl From for JsonPayloadAttributes { @@ -807,6 +810,7 @@ impl From for JsonPayloadAttributes { withdrawals: pa.withdrawals.into_iter().map(Into::into).collect(), parent_beacon_block_root: pa.parent_beacon_block_root, slot_number: pa.slot_number, + target_gas_limit: pa.target_gas_limit, }), } } @@ -840,6 +844,7 @@ impl From for PayloadAttributes { withdrawals: jpa.withdrawals.into_iter().map(Into::into).collect(), parent_beacon_block_root: jpa.parent_beacon_block_root, slot_number: jpa.slot_number, + target_gas_limit: jpa.target_gas_limit, }), } } diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index b2dabb7c01..b1b8b0deaa 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -73,6 +73,8 @@ pub const DEFAULT_EXECUTION_ENDPOINT: &str = "http://localhost:8551/"; /// Name for the default file used for the jwt secret. pub const DEFAULT_JWT_FILE: &str = "jwt.hex"; +pub const DEFAULT_GAS_LIMIT: u64 = 60_000_000; + /// A fee recipient address for use during block production. Only used as a very last resort if /// there is no address provided by the user. /// @@ -358,7 +360,10 @@ impl> BlockProposalContents { pub parent_hash: ExecutionBlockHash, - pub parent_gas_limit: u64, + // NOTE: The `parent_gas_limit` is a bit scuffed. We made it optional for Gloas because it + // isn't currently required, but it should possibly be made non-optional again if needed. + // Or we should superstruct this type. + pub parent_gas_limit: Option, pub proposer_gas_limit: Option, pub payload_attributes: &'a PayloadAttributes, pub forkchoice_update_params: &'a ForkchoiceUpdateParameters, @@ -2082,7 +2087,7 @@ fn verify_builder_bid( let payload_withdrawals_root = header.withdrawals_root().ok(); let expected_gas_limit = proposer_gas_limit - .and_then(|target_gas_limit| expected_gas_limit(parent_gas_limit, target_gas_limit, spec)); + .and_then(|target_gas_limit| expected_gas_limit(parent_gas_limit?, target_gas_limit, spec)); if header.parent_hash() != parent_hash { Err(Box::new(InvalidBuilderPayload::ParentHash { diff --git a/beacon_node/execution_layer/src/test_utils/mock_builder.rs b/beacon_node/execution_layer/src/test_utils/mock_builder.rs index d6243a7c4d..d456c9adc1 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_builder.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_builder.rs @@ -282,7 +282,7 @@ impl BidStuff for BuilderBid { #[derive(Clone)] pub struct PayloadParametersCloned { pub parent_hash: ExecutionBlockHash, - pub parent_gas_limit: u64, + pub parent_gas_limit: Option, pub proposer_gas_limit: Option, pub payload_attributes: PayloadAttributes, pub forkchoice_update_params: ForkchoiceUpdateParameters, @@ -903,6 +903,7 @@ impl MockBuilder { expected_withdrawals, None, None, + None, ), ForkName::Deneb | ForkName::Electra | ForkName::Fulu => PayloadAttributes::new( timestamp, @@ -911,6 +912,7 @@ impl MockBuilder { expected_withdrawals, Some(head_block_root), None, + None, ), ForkName::Gloas => PayloadAttributes::new( timestamp, @@ -919,6 +921,7 @@ impl MockBuilder { expected_withdrawals, Some(head_block_root), Some(slot.as_u64()), + None, // TODO(gloas): pass target_gas_limit ), ForkName::Base | ForkName::Altair => { return Err("invalid fork".to_string()); @@ -969,7 +972,7 @@ impl MockBuilder { let payload_parameters = PayloadParametersCloned { parent_hash: head_execution_hash, - parent_gas_limit: head_gas_limit, + parent_gas_limit: Some(head_gas_limit), proposer_gas_limit: Some(proposer_gas_limit), payload_attributes, forkchoice_update_params, diff --git a/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs b/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs index 5b721bcab2..583808281f 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs @@ -105,6 +105,7 @@ impl MockExecutionLayer { None, None, None, + None, ); // Insert a proposer to ensure the fork choice updated command works. @@ -146,11 +147,12 @@ impl MockExecutionLayer { None, None, None, + None, ); let payload_parameters = PayloadParameters { parent_hash, - parent_gas_limit, + parent_gas_limit: Some(parent_gas_limit), proposer_gas_limit: None, payload_attributes: &payload_attributes, forkchoice_update_params: &forkchoice_update_params, @@ -199,11 +201,12 @@ impl MockExecutionLayer { None, None, None, + None, ); let payload_parameters = PayloadParameters { parent_hash, - parent_gas_limit, + parent_gas_limit: Some(parent_gas_limit), proposer_gas_limit: None, payload_attributes: &payload_attributes, forkchoice_update_params: &forkchoice_update_params, diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index a7fe34593a..3da0841a4e 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -2929,7 +2929,7 @@ impl ApiTester { proposal_slot, validator_index: validator_index as u64, fee_recipient: Address::repeat_byte(0xaa), - gas_limit: 30_000_000, + target_gas_limit: 30_000_000, }; let epoch = proposal_slot.epoch(E::slots_per_epoch()); diff --git a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs index 7a902649cb..3e8845f017 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -4058,7 +4058,6 @@ impl NetworkBeaconProcessor { PayloadBidError::BadSignature | PayloadBidError::InvalidBuilder { .. } | PayloadBidError::InvalidFeeRecipient - | PayloadBidError::InvalidGasLimit | PayloadBidError::ExecutionPaymentNonZero { .. } | PayloadBidError::InvalidBlobKzgCommitments { .. }, ) => { @@ -4076,6 +4075,7 @@ impl NetworkBeaconProcessor { | PayloadBidError::ParentBlockRootUnknown { .. } | PayloadBidError::ParentBlockRootNotCanonical { .. } | PayloadBidError::BuilderCantCoverBid { .. } + | PayloadBidError::InvalidGasLimit | PayloadBidError::BeaconStateError(_) | PayloadBidError::InternalError(_) | PayloadBidError::InvalidBidSlot { .. } diff --git a/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml b/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml index 02bf37cb55..ced9679142 100644 --- a/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml @@ -93,8 +93,8 @@ SYNC_MESSAGE_DUE_BPS: 3333 CONTRIBUTION_DUE_BPS: 6667 # Gloas -# 2**6 (= 64) epochs -MIN_BUILDER_WITHDRAWABILITY_DELAY: 64 +# 2**13 (= 8192) epochs +MIN_BUILDER_WITHDRAWABILITY_DELAY: 8192 # 2500 basis points, 25% of SLOT_DURATION_MS ATTESTATION_DUE_BPS_GLOAS: 2500 # 5000 basis points, 50% of SLOT_DURATION_MS diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index a60859585c..2de8ce7d81 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -1207,7 +1207,6 @@ where fn validate_on_payload_attestation( &self, indexed_payload_attestation: &IndexedPayloadAttestation, - is_from_block: AttestationFromBlock, ) -> Result<(), InvalidPayloadAttestation> { // This check is from `is_valid_indexed_payload_attestation`, but we do it immediately to // avoid wasting time on junk attestations. @@ -1233,25 +1232,6 @@ where }); } - // PTC votes can only change the vote for their assigned beacon block, return early otherwise - if block.slot != indexed_payload_attestation.data.slot { - return Ok(()); - } - - // Gossip payload attestations must be for the current slot. - // NOTE: signature is assumed to have been verified by caller. - // https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/fork-choice.md - if matches!(is_from_block, AttestationFromBlock::False) - && indexed_payload_attestation.data.slot != self.fc_store.get_current_slot() - { - return Err( - InvalidPayloadAttestation::PayloadAttestationNotCurrentSlot { - attestation_slot: indexed_payload_attestation.data.slot, - current_slot: self.fc_store.get_current_slot(), - }, - ); - } - Ok(()) } @@ -1339,34 +1319,69 @@ where pub fn on_payload_attestation( &mut self, system_time_current_slot: Slot, - attestation: &IndexedPayloadAttestation, + payload_attestation: &IndexedPayloadAttestation, is_from_block: AttestationFromBlock, ptc: &[usize], ) -> Result<(), Error> { self.update_time(system_time_current_slot)?; - if attestation.data.beacon_block_root.is_zero() { + if payload_attestation.data.beacon_block_root.is_zero() { return Ok(()); } // TODO(gloas): Should ignore wrong-slot payload attestations at the caller, they could // have been processed at the correct slot when received on gossip, but then have the // wrong-slot by the time they make it to here (TOCTOU). - self.validate_on_payload_attestation(attestation, is_from_block)?; + // TODO(gloas): Consider inlining validate_on_payload_attestation here to look more like the spec. + self.validate_on_payload_attestation(payload_attestation)?; - // Resolve validator indices to PTC committee positions. - let ptc_indices: Vec = attestation - .attesting_indices - .iter() - .filter_map(|validator_index| ptc.iter().position(|&p| p == *validator_index as usize)) - .collect(); + // PTC votes can only change the vote for their assigned beacon block, return early otherwise. + let block = self + .proto_array + .get_block(&payload_attestation.data.beacon_block_root) + .ok_or(InvalidPayloadAttestation::UnknownHeadBlock { + beacon_block_root: payload_attestation.data.beacon_block_root, + })?; + if block.slot != payload_attestation.data.slot { + return Ok(()); + } + + // Gossip payload attestations must be for the current slot. + if matches!(is_from_block, AttestationFromBlock::False) + && payload_attestation.data.slot != self.fc_store.get_current_slot() + { + return Err( + InvalidPayloadAttestation::PayloadAttestationNotCurrentSlot { + attestation_slot: payload_attestation.data.slot, + current_slot: self.fc_store.get_current_slot(), + } + .into(), + ); + } + + // Resolve validator indices to all PTC committee positions. A validator may + // appear multiple times in the PTC committee. + let mut ptc_indices = vec![]; + let mut validators_found = 0; + for validator_index in payload_attestation.attesting_indices.iter() { + let mut found = false; + for (ptc_index, &ptc_validator_index) in ptc.iter().enumerate() { + if ptc_validator_index == *validator_index as usize { + ptc_indices.push(ptc_index); + found = true; + } + } + if found { + validators_found += 1; + } + } // Check that all the attesters are in the PTC - if ptc_indices.len() != attestation.attesting_indices.len() { + if validators_found != payload_attestation.attesting_indices.len() { return Err( InvalidPayloadAttestation::PayloadAttestationAttestersNotInPtc { - attesting_indices_len: attestation.attesting_indices.len(), - attesting_indices_in_ptc: ptc_indices.len(), + attesting_indices_len: payload_attestation.attesting_indices.len(), + attesting_indices_in_ptc: validators_found, } .into(), ); @@ -1374,10 +1389,10 @@ where for &ptc_index in &ptc_indices { self.proto_array.process_payload_attestation( - attestation.data.beacon_block_root, + payload_attestation.data.beacon_block_root, ptc_index, - attestation.data.payload_present, - attestation.data.blob_data_available, + payload_attestation.data.payload_present, + payload_attestation.data.blob_data_available, )?; } @@ -1522,6 +1537,17 @@ where && self.is_finalized_checkpoint_or_descendant(*block_root) } + /// Called by the proposer to decide whether to build on the full or empty parent. + pub fn should_build_on_full( + &self, + block_root: &Hash256, + parent_payload_status: PayloadStatus, + ) -> Result> { + self.proto_array + .should_build_on_full::(block_root, parent_payload_status) + .map_err(Error::ProtoArrayStringError) + } + /// Returns whether the proposer should extend the execution payload chain of the given block. pub fn should_extend_payload(&self, block_root: &Hash256) -> Result> { let proposer_boost_root = self.fc_store.proposer_boost_root(); diff --git a/consensus/proto_array/src/error.rs b/consensus/proto_array/src/error.rs index bb47af97d9..d185ed371c 100644 --- a/consensus/proto_array/src/error.rs +++ b/consensus/proto_array/src/error.rs @@ -1,3 +1,4 @@ +use crate::PayloadStatus; use safe_arith::ArithError; use types::{Checkpoint, Epoch, ExecutionBlockHash, Hash256, Slot}; @@ -62,6 +63,10 @@ pub enum Error { }, NoViableChildren, OnBlockRequiresProposerIndex, + InvalidPayloadStatus { + block_root: Hash256, + payload_status: PayloadStatus, + }, } impl From for Error { diff --git a/consensus/proto_array/src/fork_choice_test_definition.rs b/consensus/proto_array/src/fork_choice_test_definition.rs index d537f16bb2..43b76ec7cb 100644 --- a/consensus/proto_array/src/fork_choice_test_definition.rs +++ b/consensus/proto_array/src/fork_choice_test_definition.rs @@ -556,7 +556,11 @@ impl ForkChoiceTestDefinition { node_v29.payload_data_availability_votes = BitVector::from_bytes(smallvec::smallvec![fill; 64]) .expect("valid 512-bit bitvector"); - // Per spec, is_payload_timely/is_payload_data_available require + // Mark all PTC members as having participated. + node_v29.ptc_participation = + BitVector::from_bytes(smallvec::smallvec![0xFF; 64]) + .expect("valid 512-bit bitvector"); + // Per spec, payload_timeliness/payload_data_availability require // the payload to be in payload_states (payload_received). node_v29.payload_received = is_timely || is_data_available; } diff --git a/consensus/proto_array/src/proto_array.rs b/consensus/proto_array/src/proto_array.rs index 78f5026689..8ac8354f06 100644 --- a/consensus/proto_array/src/proto_array.rs +++ b/consensus/proto_array/src/proto_array.rs @@ -155,6 +155,10 @@ pub struct ProtoNode { /// Tiebreak derived as: `num_set_bits() > ptc_size / 2`. #[superstruct(only(V29))] pub payload_data_availability_votes: BitVector, + /// Tracks which PTC members have cast a vote. + /// Bit i set means PTC member i has submitted a payload attestation. + #[superstruct(only(V29))] + pub ptc_participation: BitVector, /// Whether the execution payload for this block has been received and validated locally. /// Maps to `root in store.payload_states` in the spec. #[superstruct(only(V29), partial_getter(copy))] @@ -193,31 +197,60 @@ impl ProtoNode { } } - pub fn is_payload_timely(&self) -> bool { + /// Checks if `timely` matches our view of payload timeliness. + /// Returns whether the execution payload for the node is considered `timely` + /// (or not `timely` when `timely` is `false`), taking into consideration local + /// availability and PTC votes. + pub fn payload_timeliness(&self, timely: bool) -> Result { let Ok(node) = self.as_v29() else { - return false; + return Err(Error::InvalidNodeVariant { + block_root: self.root(), + }); }; - // Equivalent to `if root not in store.payload_states` in the spec. + // Equivalent to `if not is_payload_verified(store, root)` in the spec. if !node.payload_received { - return false; + return Ok(!timely); } - node.payload_timeliness_votes.num_set_bits() > E::payload_timely_threshold() + let matching_votes = if timely { + node.payload_timeliness_votes.num_set_bits() + } else { + // We take into consideration only participating ptc votes. An unset bit + // in `payload_timeliness_votes` could be an absent vote or a no vote. + node.ptc_participation + .num_set_bits() + .saturating_sub(node.payload_timeliness_votes.num_set_bits()) + }; + Ok(matching_votes > E::payload_timely_threshold()) } - pub fn is_payload_data_available(&self) -> bool { + /// Checks if `available` matches our view of payload data availability. + /// Return whether the blob data for the node is considered `available` + /// (or not, when `available` is `False`), taking into consideration local + /// availability and PTC votes. + pub fn payload_data_availability(&self, available: bool) -> Result { let Ok(node) = self.as_v29() else { - return false; + return Err(Error::InvalidNodeVariant { + block_root: self.root(), + }); }; - // Equivalent to `if root not in store.payload_states` in the spec. + // Equivalent to `if not is_payload_verified(store, root)` in the spec. if !node.payload_received { - return false; + return Ok(!available); } - node.payload_data_availability_votes.num_set_bits() - > E::data_availability_timely_threshold() + let matching_votes = if available { + node.payload_data_availability_votes.num_set_bits() + } else { + // We take into consideration only participating ptc votes. An unset bit + // in `payload_data_availability_votes` could be an absent vote or a no vote. + node.ptc_participation + .num_set_bits() + .saturating_sub(node.payload_data_availability_votes.num_set_bits()) + }; + Ok(matching_votes > E::data_availability_timely_threshold()) } } @@ -605,6 +638,7 @@ impl ProtoArray { execution_payload_parent_hash, payload_timeliness_votes: BitVector::default(), payload_data_availability_votes: BitVector::default(), + ptc_participation: BitVector::default(), payload_received: false, proposer_index, // Spec: `record_block_timeliness` + `get_forkchoice_store`. @@ -1501,12 +1535,46 @@ impl ProtoArray { } } + /// Called by the proposer to decide whether to build on the full or empty + /// parent pending node. Returns false if the PTC has voted the data as unavailable. + pub fn should_build_on_full( + &self, + fc_node: &IndexedForkChoiceNode, + proto_node: &ProtoNode, + ) -> Result { + if fc_node.payload_status == PayloadStatus::Pending { + return Err(Error::InvalidPayloadStatus { + block_root: proto_node.root(), + payload_status: fc_node.payload_status, + }); + } + + if fc_node.payload_status == PayloadStatus::Empty { + return Ok(false); + } + // Check that false votes have not achieved an absolute majority. This allows the payload to be + // considered available when either a majority have voted true or not enough votes have + // been cast either way. + Ok(!proto_node.payload_data_availability::(false)?) + } + pub fn should_extend_payload( &self, fc_node: &IndexedForkChoiceNode, proto_node: &ProtoNode, proposer_boost_root: Hash256, ) -> Result { + let Ok(node) = proto_node.as_v29() else { + return Err(Error::InvalidNodeVariant { + block_root: fc_node.root, + }); + }; + + // Spec equivalent to `if not is_payload_verified(store, root): return False` + if !node.payload_received { + return Ok(false); + } + // Per spec: `proposer_root == Root()` is one of the `or` conditions that // makes `should_extend_payload` return True. if proposer_boost_root.is_zero() { @@ -1531,11 +1599,10 @@ impl ProtoArray { .ok_or(Error::InvalidNodeIndex(parent_index))? .root(); - Ok( - (proto_node.is_payload_timely::() && proto_node.is_payload_data_available::()) - || proposer_boost_parent_root != fc_node.root - || proposer_boost_node.is_parent_node_full(), - ) + Ok((proto_node.payload_timeliness::(true)? + && proto_node.payload_data_availability::(true)?) + || proposer_boost_parent_root != fc_node.root + || proposer_boost_node.is_parent_node_full()) } /// Update the tree with new finalization information. The tree is only actually pruned if both diff --git a/consensus/proto_array/src/proto_array_fork_choice.rs b/consensus/proto_array/src/proto_array_fork_choice.rs index 7abba8a1f6..96d2302266 100644 --- a/consensus/proto_array/src/proto_array_fork_choice.rs +++ b/consensus/proto_array/src/proto_array_fork_choice.rs @@ -640,6 +640,9 @@ impl ProtoArrayForkChoice { .map_err(|e| { format!("process_payload_attestation: data availability set failed: {e:?}") })?; + v29.ptc_participation + .set(ptc_index, true) + .map_err(|e| format!("process_payload_attestation: participation set failed: {e:?}"))?; Ok(()) } @@ -1006,6 +1009,33 @@ impl ProtoArrayForkChoice { }) } + /// Called by the proposer to decide whether to build on the full or empty + /// parent. Returns false if the PTC has voted the data as unavailable. + pub fn should_build_on_full( + &self, + block_root: &Hash256, + parent_payload_status: PayloadStatus, + ) -> Result { + let block_index = self + .proto_array + .indices + .get(block_root) + .ok_or_else(|| format!("Unknown block root: {block_root:?}"))?; + let proto_node = self + .proto_array + .nodes + .get(*block_index) + .ok_or_else(|| format!("Missing node at index: {block_index}"))?; + let fc_node = IndexedForkChoiceNode { + root: proto_node.root(), + proto_node_index: *block_index, + payload_status: parent_payload_status, + }; + self.proto_array + .should_build_on_full::(&fc_node, proto_node) + .map_err(|e| format!("{e:?}")) + } + /// Returns whether the proposer should extend the parent's execution payload chain. /// /// This checks timeliness, data availability, and proposer boost conditions per the spec. diff --git a/consensus/state_processing/src/per_block_processing/process_operations.rs b/consensus/state_processing/src/per_block_processing/process_operations.rs index 422e0afe06..f88a325d4e 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -916,25 +916,24 @@ pub fn process_deposit_requests_post_gloas( /// Check if there is a pending deposit for a new validator with the given pubkey. // TODO(gloas): cache the deposit signature validation or remove this loop entirely if possible, // it is `O(n * m)` where `n` is max 8192 and `m` is max 128M. -fn is_pending_validator( - state: &BeaconState, +pub fn is_pending_validator<'a>( + pending_deposits: impl IntoIterator, pubkey: &PublicKeyBytes, spec: &ChainSpec, -) -> Result { - for deposit in state.pending_deposits()?.iter() { - if deposit.pubkey == *pubkey { - let deposit_data = DepositData { - pubkey: deposit.pubkey, - withdrawal_credentials: deposit.withdrawal_credentials, - amount: deposit.amount, - signature: deposit.signature.clone(), - }; - if is_valid_deposit_signature(&deposit_data, spec).is_ok() { - return Ok(true); - } - } - } - Ok(false) +) -> bool { + pending_deposits.into_iter().any(|deposit| { + deposit.pubkey == *pubkey + && is_valid_deposit_signature( + &DepositData { + pubkey: deposit.pubkey, + withdrawal_credentials: deposit.withdrawal_credentials, + amount: deposit.amount, + signature: deposit.signature.clone(), + }, + spec, + ) + .is_ok() + }) } pub fn process_deposit_request_post_gloas( @@ -964,7 +963,7 @@ pub fn process_deposit_request_post_gloas( if is_builder || (has_builder_prefix && !is_validator - && !is_pending_validator(state, &deposit_request.pubkey, spec)?) + && !is_pending_validator(state.pending_deposits()?, &deposit_request.pubkey, spec)) { // Apply builder deposits immediately apply_deposit_for_builder( @@ -1003,7 +1002,7 @@ pub fn apply_deposit_for_builder( signature: SignatureBytes, slot: Slot, spec: &ChainSpec, -) -> Result<(), BeaconStateError> { +) -> Result, BeaconStateError> { match builder_index_opt { None => { // Verify the deposit signature (proof of possession) which is not checked by the deposit contract @@ -1014,13 +1013,16 @@ pub fn apply_deposit_for_builder( signature, }; if is_valid_deposit_signature(&deposit_data, spec).is_ok() { - state.add_builder_to_registry( + let builder_index = state.add_builder_to_registry( pubkey, withdrawal_credentials, amount, slot, spec, )?; + Ok(Some(builder_index)) + } else { + Ok(None) } } Some(builder_index) => { @@ -1030,9 +1032,9 @@ pub fn apply_deposit_for_builder( .ok_or(BeaconStateError::UnknownBuilder(builder_index))? .balance .safe_add_assign(amount)?; + Ok(Some(builder_index)) } } - Ok(()) } // Make sure to build the pubkey cache before calling this function diff --git a/consensus/state_processing/src/upgrade/gloas.rs b/consensus/state_processing/src/upgrade/gloas.rs index 84cdbf22c2..c26547e304 100644 --- a/consensus/state_processing/src/upgrade/gloas.rs +++ b/consensus/state_processing/src/upgrade/gloas.rs @@ -1,18 +1,16 @@ -use crate::per_block_processing::{ - is_valid_deposit_signature, process_operations::apply_deposit_for_builder, -}; +use crate::per_block_processing::process_operations::apply_deposit_for_builder; +use crate::per_block_processing::process_operations::is_pending_validator; use milhouse::{List, Vector}; use safe_arith::SafeArith; use ssz_types::BitVector; use ssz_types::FixedVector; -use std::collections::HashSet; +use std::collections::HashMap; use std::mem; use tree_hash::TreeHash; use typenum::Unsigned; use types::{ BeaconState, BeaconStateError as Error, BeaconStateGloas, BuilderPendingPayment, ChainSpec, - DepositData, EthSpec, ExecutionPayloadBid, ExecutionRequests, Fork, - is_builder_withdrawal_credential, + EthSpec, ExecutionPayloadBid, ExecutionRequests, Fork, is_builder_withdrawal_credential, }; /// Transform a `Fulu` state into a `Gloas` state. @@ -80,6 +78,7 @@ pub fn upgrade_state_to_gloas( // Execution Bid latest_execution_payload_bid: ExecutionPayloadBid { block_hash: pre.latest_execution_payload_header.block_hash, + gas_limit: pre.latest_execution_payload_header.gas_limit, execution_requests_root: ExecutionRequests::::default().tree_hash_root(), ..Default::default() }, @@ -167,66 +166,57 @@ fn onboard_builders_from_pending_deposits( state: &mut BeaconState, spec: &ChainSpec, ) -> Result<(), Error> { - // Rather than tracking all `validator_pubkeys` in one place as the spec does, we keep a - // hashset for *just* the new validator pubkeys, and use the state's efficient - // `get_validator_index` function instead of an O(n) iteration over the full validator list. - let mut new_validator_pubkeys = HashSet::new(); - // Clone pending deposits to avoid borrow conflicts when mutating state. let current_pending_deposits = state.pending_deposits()?.clone(); let mut pending_deposits = List::empty(); + // TODO(gloas): introduce a global builder pubkey cache, see: + // https://github.com/sigp/lighthouse/issues/8783 + let mut builder_pubkey_to_index = state + .builders()? + .iter() + .enumerate() + .map(|(i, b)| (b.pubkey, i as u64)) + .collect::>(); + for deposit in ¤t_pending_deposits { // Deposits for existing validators stay in the pending queue. - if new_validator_pubkeys.contains(&deposit.pubkey) - || state.get_validator_index(&deposit.pubkey)?.is_some() - { + if state.get_validator_index(&deposit.pubkey)?.is_some() { pending_deposits.push(deposit.clone())?; continue; } - // Re-scan builder list each iteration because `apply_deposit_for_builder` may add - // new builders to the registry. - // TODO(gloas): this linear scan could be optimized, see: - // https://github.com/sigp/lighthouse/issues/8783 - let builder_index = state - .builders()? - .iter() - .position(|b| b.pubkey == deposit.pubkey); + if !builder_pubkey_to_index.contains_key(&deposit.pubkey) { + // Deposits without builder withdrawal credentials are for new validators. + if !is_builder_withdrawal_credential(deposit.withdrawal_credentials, spec) { + pending_deposits.push(deposit.clone())?; + continue; + } - let has_builder_credentials = - is_builder_withdrawal_credential(deposit.withdrawal_credentials, spec); - - if builder_index.is_some() || has_builder_credentials { - let builder_index_opt = builder_index.map(|i| i as u64); - apply_deposit_for_builder( - state, - builder_index_opt, - deposit.pubkey, - deposit.withdrawal_credentials, - deposit.amount, - deposit.signature.clone(), - deposit.slot, - spec, - )?; - continue; + // If there is a valid pending deposit for a new validator with this pubkey, + // keep this deposit in the pending queue to be applied to that validator later. + if is_pending_validator(&pending_deposits, &deposit.pubkey, spec) { + pending_deposits.push(deposit.clone())?; + continue; + } } - // If there is a pending deposit for a new validator that has a valid signature, - // track the pubkey so that subsequent builder deposits for the same pubkey stay - // in pending (applied to the validator later) rather than creating a builder. - // Deposits with invalid signatures are dropped since they would fail in - // apply_pending_deposit anyway. - let deposit_data = DepositData { - pubkey: deposit.pubkey, - withdrawal_credentials: deposit.withdrawal_credentials, - amount: deposit.amount, - signature: deposit.signature.clone(), - }; - if is_valid_deposit_signature(&deposit_data, spec).is_ok() { - new_validator_pubkeys.insert(deposit.pubkey); - pending_deposits.push(deposit.clone())?; + let builder_index = builder_pubkey_to_index.get(&deposit.pubkey).copied(); + + if let Some(new_builder_index) = apply_deposit_for_builder( + state, + builder_index, + deposit.pubkey, + deposit.withdrawal_credentials, + deposit.amount, + deposit.signature.clone(), + deposit.slot, + spec, + )? { + builder_pubkey_to_index + .entry(deposit.pubkey) + .or_insert(new_builder_index); } } diff --git a/consensus/types/configs/mainnet.yaml b/consensus/types/configs/mainnet.yaml index 25bf872a7a..743384bcc9 100644 --- a/consensus/types/configs/mainnet.yaml +++ b/consensus/types/configs/mainnet.yaml @@ -91,8 +91,8 @@ SYNC_MESSAGE_DUE_BPS: 3333 CONTRIBUTION_DUE_BPS: 6667 # Gloas -# 2**6 (= 64) epochs -MIN_BUILDER_WITHDRAWABILITY_DELAY: 64 +# 2**13 (= 8192) epochs +MIN_BUILDER_WITHDRAWABILITY_DELAY: 8192 # 2500 basis points, 25% of SLOT_DURATION_MS ATTESTATION_DUE_BPS_GLOAS: 2500 # 5000 basis points, 50% of SLOT_DURATION_MS diff --git a/consensus/types/src/builder/proposer_preferences.rs b/consensus/types/src/builder/proposer_preferences.rs index e3773e333d..4f27020105 100644 --- a/consensus/types/src/builder/proposer_preferences.rs +++ b/consensus/types/src/builder/proposer_preferences.rs @@ -16,7 +16,7 @@ pub struct ProposerPreferences { pub proposal_slot: Slot, pub validator_index: u64, pub fee_recipient: Address, - pub gas_limit: u64, + pub target_gas_limit: u64, } impl SignedRoot for ProposerPreferences {} diff --git a/consensus/types/src/core/chain_spec.rs b/consensus/types/src/core/chain_spec.rs index c54d032891..c42bb4b5b9 100644 --- a/consensus/types/src/core/chain_spec.rs +++ b/consensus/types/src/core/chain_spec.rs @@ -108,6 +108,7 @@ pub struct ChainSpec { pub proposer_reorg_cutoff_bps: u64, pub attestation_due_bps: u64, pub attestation_due_bps_gloas: u64, + pub payload_due_bps: u64, pub payload_attestation_due_bps: u64, pub aggregate_due_bps: u64, pub sync_message_due_bps: u64, @@ -118,6 +119,7 @@ pub struct ChainSpec { */ pub unaggregated_attestation_due: Duration, pub unaggregated_attestation_due_gloas: Duration, + pub payload_due: Duration, pub payload_attestation_due: Duration, pub aggregate_attestation_due: Duration, pub sync_message_due: Duration, @@ -894,6 +896,11 @@ impl ChainSpec { } } + /// Spec: `get_payload_due_ms`. + pub fn get_payload_due(&self) -> Duration { + self.payload_due + } + /// Spec: `get_payload_attestation_due_ms`. pub fn get_payload_attestation_due(&self) -> Duration { self.payload_attestation_due @@ -974,6 +981,9 @@ impl ChainSpec { self.unaggregated_attestation_due_gloas = self .compute_slot_component_duration(self.attestation_due_bps_gloas) .expect("invalid chain spec: cannot compute unaggregated_attestation_due_gloas"); + self.payload_due = self + .compute_slot_component_duration(self.payload_due_bps) + .expect("invalid chain spec: cannot compute payload_due"); self.payload_attestation_due = self .compute_slot_component_duration(self.payload_attestation_due_bps) .expect("invalid chain spec: cannot compute payload_attestation_due"); @@ -1108,6 +1118,7 @@ impl ChainSpec { proposer_reorg_cutoff_bps: 1667, attestation_due_bps: 3333, attestation_due_bps_gloas: 2500, + payload_due_bps: 7500, payload_attestation_due_bps: 7500, aggregate_due_bps: 6667, sync_message_due_bps: 3333, @@ -1118,6 +1129,7 @@ impl ChainSpec { */ unaggregated_attestation_due: Duration::from_millis(3999), unaggregated_attestation_due_gloas: Duration::from_millis(3000), + payload_due: Duration::from_millis(9000), payload_attestation_due: Duration::from_millis(9000), aggregate_attestation_due: Duration::from_millis(8000), sync_message_due: Duration::from_millis(3999), @@ -1270,7 +1282,7 @@ impl ChainSpec { gloas_fork_epoch: None, builder_payment_threshold_numerator: 6, builder_payment_threshold_denominator: 10, - min_builder_withdrawability_delay: Epoch::new(64), + min_builder_withdrawability_delay: Epoch::new(8192), churn_limit_quotient_gloas: option_wrapper(|| u64::checked_pow(2, 15)) .expect("calculation does not overflow"), consolidation_churn_limit_quotient: option_wrapper(|| u64::checked_pow(2, 16)) @@ -1440,6 +1452,7 @@ impl ChainSpec { */ unaggregated_attestation_due: Duration::from_millis(1999), unaggregated_attestation_due_gloas: Duration::from_millis(1500), + payload_due: Duration::from_millis(4500), payload_attestation_due: Duration::from_millis(4500), aggregate_attestation_due: Duration::from_millis(4000), sync_message_due: Duration::from_millis(1999), @@ -1531,6 +1544,7 @@ impl ChainSpec { proposer_reorg_cutoff_bps: 1667, attestation_due_bps: 3333, attestation_due_bps_gloas: 2500, + payload_due_bps: 7500, payload_attestation_due_bps: 7500, aggregate_due_bps: 6667, @@ -1540,6 +1554,7 @@ impl ChainSpec { */ unaggregated_attestation_due: Duration::from_millis(1666), unaggregated_attestation_due_gloas: Duration::from_millis(1250), + payload_due: Duration::from_millis(3750), payload_attestation_due: Duration::from_millis(3750), aggregate_attestation_due: Duration::from_millis(3333), sync_message_due: Duration::from_millis(1666), @@ -1693,7 +1708,7 @@ impl ChainSpec { gloas_fork_epoch: None, builder_payment_threshold_numerator: 6, builder_payment_threshold_denominator: 10, - min_builder_withdrawability_delay: Epoch::new(64), + min_builder_withdrawability_delay: Epoch::new(8192), churn_limit_quotient_gloas: option_wrapper(|| u64::checked_pow(2, 15)) .expect("calculation does not overflow"), consolidation_churn_limit_quotient: option_wrapper(|| u64::checked_pow(2, 16)) @@ -2136,6 +2151,9 @@ pub struct Config { #[serde(default = "default_attestation_due_bps_gloas")] #[serde(with = "serde_utils::quoted_u64")] attestation_due_bps_gloas: u64, + #[serde(default = "default_payload_due_bps")] + #[serde(with = "serde_utils::quoted_u64")] + payload_due_bps: u64, #[serde(default = "default_payload_attestation_due_bps")] #[serde(with = "serde_utils::quoted_u64")] payload_attestation_due_bps: u64, @@ -2379,6 +2397,10 @@ const fn default_attestation_due_bps_gloas() -> u64 { 2500 } +const fn default_payload_due_bps() -> u64 { + 7500 +} + const fn default_payload_attestation_due_bps() -> u64 { 7500 } @@ -2396,7 +2418,7 @@ const fn default_contribution_due_bps() -> u64 { } const fn default_min_builder_withdrawability_delay() -> u64 { - 64 + 8192 } const fn default_churn_limit_quotient_gloas() -> u64 { @@ -2656,6 +2678,7 @@ impl Config { proposer_reorg_cutoff_bps: spec.proposer_reorg_cutoff_bps, attestation_due_bps: spec.attestation_due_bps, attestation_due_bps_gloas: spec.attestation_due_bps_gloas, + payload_due_bps: spec.payload_due_bps, payload_attestation_due_bps: spec.payload_attestation_due_bps, aggregate_due_bps: spec.aggregate_due_bps, sync_message_due_bps: spec.sync_message_due_bps, @@ -2759,6 +2782,7 @@ impl Config { proposer_reorg_cutoff_bps, attestation_due_bps, attestation_due_bps_gloas, + payload_due_bps, payload_attestation_due_bps, aggregate_due_bps, sync_message_due_bps, @@ -2867,6 +2891,7 @@ impl Config { proposer_reorg_cutoff_bps, attestation_due_bps, attestation_due_bps_gloas, + payload_due_bps, payload_attestation_due_bps, aggregate_due_bps, sync_message_due_bps, @@ -3694,6 +3719,30 @@ mod yaml_tests { let custom_spec = custom_spec.compute_derived_values::(); let tiny_due = custom_spec.get_unaggregated_attestation_due(); assert_eq!(tiny_due, Duration::from_millis(1)); // 12000 * 1 / 10000 = 1.2 -> 1 + + // Test payload due (7500 bps = 75% of 12s = 9s) + let spec = ChainSpec::mainnet().compute_derived_values::(); + let payload_due = spec.get_payload_due(); + assert_eq!(payload_due, Duration::from_millis(9000)); // 12000 * 7500 / 10000 + + // Test payload attestation due (7500 bps = 75% of 12s = 9s) + let payload_att_due = spec.get_payload_attestation_due(); + assert_eq!(payload_att_due, Duration::from_millis(9000)); // 12000 * 7500 / 10000 + + // Test gloas attestation due (2500 bps = 25% of 12s = 3s) + assert_eq!( + spec.unaggregated_attestation_due_gloas, + Duration::from_millis(3000) + ); // 12000 * 2500 / 10000 + + // Test gloas with custom bps + let mut custom_spec = spec; + custom_spec.attestation_due_bps_gloas = 5000; + let custom_spec = custom_spec.compute_derived_values::(); + assert_eq!( + custom_spec.unaggregated_attestation_due_gloas, + Duration::from_millis(6000) + ); // 12000 * 5000 / 10000 } #[test] @@ -3715,6 +3764,19 @@ mod yaml_tests { Duration::from_millis(8000) ); + // Mainnet payload due: 12000ms slots, 7500 bps = 9000ms + assert_eq!(mainnet.get_payload_due(), Duration::from_millis(9000)); + assert_eq!( + mainnet.get_payload_attestation_due(), + Duration::from_millis(9000) + ); + + // Mainnet gloas: 12000ms slots, 2500 bps = 3000ms + assert_eq!( + mainnet.unaggregated_attestation_due_gloas, + Duration::from_millis(3000) + ); + // Minimal spec: 6000ms slots, 3333 bps = 1999ms, 6667 bps = 4000ms let minimal = ChainSpec::minimal(); assert_eq!( @@ -3730,6 +3792,18 @@ mod yaml_tests { minimal.get_contribution_message_due(), Duration::from_millis(4000) ); + // Minimal payload due: 6000ms slots, 7500 bps = 4500ms + assert_eq!(minimal.get_payload_due(), Duration::from_millis(4500)); + assert_eq!( + minimal.get_payload_attestation_due(), + Duration::from_millis(4500) + ); + + // Minimal gloas: 6000ms slots, 2500 bps = 1500ms + assert_eq!( + minimal.unaggregated_attestation_due_gloas, + Duration::from_millis(1500) + ); // Gnosis spec: 5000ms slots, 3333 bps = 1666ms, 6667 bps = 3333ms let gnosis = ChainSpec::gnosis(); @@ -3746,6 +3820,18 @@ mod yaml_tests { gnosis.get_contribution_message_due(), Duration::from_millis(3333) ); + // Gnosis payload due: 5000ms slots, 7500 bps = 3750ms + assert_eq!(gnosis.get_payload_due(), Duration::from_millis(3750)); + assert_eq!( + gnosis.get_payload_attestation_due(), + Duration::from_millis(3750) + ); + + // Gnosis gloas: 5000ms slots, 2500 bps = 1250ms + assert_eq!( + gnosis.unaggregated_attestation_due_gloas, + Duration::from_millis(1250) + ); } #[test] diff --git a/consensus/types/src/state/beacon_state.rs b/consensus/types/src/state/beacon_state.rs index 4d2c7533ca..027acfab7f 100644 --- a/consensus/types/src/state/beacon_state.rs +++ b/consensus/types/src/state/beacon_state.rs @@ -1333,6 +1333,7 @@ impl BeaconState { pub fn is_valid_proposal_slot( &self, preferences: &ProposerPreferences, + spec: &ChainSpec, ) -> Result { let current_epoch = self.current_epoch(); let proposal_epoch = preferences.proposal_slot.epoch(E::slots_per_epoch()); @@ -1341,8 +1342,7 @@ impl BeaconState { return Ok(false); } - let next_epoch = current_epoch.saturating_add(1u64); - if proposal_epoch > next_epoch { + if proposal_epoch > current_epoch.saturating_add(spec.min_seed_lookahead) { return Ok(false); } diff --git a/testing/ef_tests/Makefile b/testing/ef_tests/Makefile index 36f6684685..f566a89ded 100644 --- a/testing/ef_tests/Makefile +++ b/testing/ef_tests/Makefile @@ -1,6 +1,6 @@ # To download/extract nightly tests, run: # CONSENSUS_SPECS_TEST_VERSION=nightly make -CONSENSUS_SPECS_TEST_VERSION ?= v1.7.0-alpha.7 +CONSENSUS_SPECS_TEST_VERSION ?= v1.7.0-alpha.8 REPO_NAME := consensus-spec-tests OUTPUT_DIR := ./$(REPO_NAME) diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index 8b0b74d256..69fce09505 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -1,6 +1,6 @@ use super::*; use crate::decode::{ssz_decode_file, ssz_decode_file_with, ssz_decode_state, yaml_decode_file}; -use ::fork_choice::{PayloadVerificationStatus, ProposerHeadError}; +use ::fork_choice::{AttestationFromBlock, PayloadVerificationStatus, ProposerHeadError}; use beacon_chain::beacon_proposer_cache::compute_proposer_duties_from_head; use beacon_chain::blob_verification::GossipBlobError; use beacon_chain::block_verification_types::LookupBlock; @@ -19,13 +19,16 @@ use beacon_chain::{ custody_context::NodeCustodyType, test_utils::{BeaconChainHarness, EphemeralHarnessType}, }; +use bls::AggregateSignature; use execution_layer::{ PayloadStatusV1, PayloadStatusV1Status, json_structures::JsonPayloadStatusV1Status, }; use serde::Deserialize; use ssz_derive::Decode; +use ssz_types::VariableList; use state_processing::VerifySignatures; use state_processing::envelope_processing::verify_execution_payload_envelope; +use state_processing::per_block_processing::is_valid_indexed_payload_attestation; use state_processing::state_advance::complete_state_advance; use std::future::Future; use std::sync::Arc; @@ -34,8 +37,8 @@ use types::{ Attestation, AttestationRef, AttesterSlashing, AttesterSlashingRef, BeaconBlock, BeaconState, BlobSidecar, BlobsList, BlockImportSource, Checkpoint, DataColumnSidecar, DataColumnSidecarList, DataColumnSubnetId, ExecutionBlockHash, Hash256, IndexedAttestation, - KzgProof, ProposerPreparationData, SignedBeaconBlock, SignedExecutionPayloadEnvelope, Slot, - Uint256, + IndexedPayloadAttestation, KzgProof, PayloadAttestationMessage, ProposerPreparationData, + SignedBeaconBlock, SignedExecutionPayloadEnvelope, Slot, Uint256, }; // When set to true, cache any states fetched from the db. @@ -63,6 +66,13 @@ pub struct ShouldOverrideFcu { result: bool, } +#[derive(Debug, Clone, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct PayloadVoteCheck { + block_root: Hash256, + votes: Vec>, +} + #[derive(Debug, Clone, Deserialize)] #[serde(deny_unknown_fields)] pub struct Checks { @@ -78,6 +88,8 @@ pub struct Checks { get_proposer_head: Option, should_override_forkchoice_update: Option, head_payload_status: Option, + payload_timeliness_vote: Option, + payload_data_availability_vote: Option, } #[derive(Debug, Clone, Deserialize)] @@ -108,6 +120,7 @@ pub enum Step< TAttesterSlashing, TPowBlock, TExecutionPayload = String, + TPayloadAttestationMessage = String, > { Tick { tick: u64, @@ -146,6 +159,15 @@ pub enum Step< execution_payload: TExecutionPayload, valid: bool, }, + PayloadAttestationMessage { + payload_attestation_message: TPayloadAttestationMessage, + #[serde(default = "default_true")] + valid: bool, + }, +} + +fn default_true() -> bool { + true } #[derive(Debug, Clone, Deserialize)] @@ -170,6 +192,7 @@ pub struct ForkChoiceTest { AttesterSlashing, PowBlock, SignedExecutionPayloadEnvelope, + PayloadAttestationMessage, >, >, } @@ -184,8 +207,12 @@ impl LoadCase for ForkChoiceTest { .expect("path must be valid OsStr") .to_string(); let spec = &testing_spec::(fork_name); - let steps: Vec, String, String, String>> = - yaml_decode_file(&path.join("steps.yaml"))?; + + #[allow(clippy::type_complexity)] + let steps: Vec< + Step, String, String, String, String, String>, + > = yaml_decode_file(&path.join("steps.yaml"))?; + // Resolve the object names in `steps.yaml` into actual decoded block/attestation objects. let steps = steps .into_iter() @@ -301,6 +328,18 @@ impl LoadCase for ForkChoiceTest { valid, }) } + Step::PayloadAttestationMessage { + payload_attestation_message, + valid, + } => { + let msg: PayloadAttestationMessage = ssz_decode_file( + &path.join(format!("{payload_attestation_message}.ssz_snappy")), + )?; + Ok(Step::PayloadAttestationMessage { + payload_attestation_message: msg, + valid, + }) + } }) .collect::>()?; let anchor_state = ssz_decode_state(&path.join("anchor_state.ssz_snappy"), spec)?; @@ -381,6 +420,8 @@ impl Case for ForkChoiceTest { get_proposer_head, should_override_forkchoice_update: should_override_fcu, head_payload_status, + payload_timeliness_vote, + payload_data_availability_vote, } = checks.as_ref(); if let Some(expected_head) = head { @@ -431,6 +472,14 @@ impl Case for ForkChoiceTest { if let Some(expected_status) = head_payload_status { tester.check_head_payload_status(*expected_status)?; } + + if let Some(expected) = payload_timeliness_vote { + tester.check_payload_timeliness_vote(expected)?; + } + + if let Some(expected) = payload_data_availability_vote { + tester.check_payload_data_availability_vote(expected)?; + } } Step::MaybeValidBlockAndColumns { @@ -446,6 +495,13 @@ impl Case for ForkChoiceTest { } => { tester.process_execution_payload(execution_payload, *valid)?; } + Step::PayloadAttestationMessage { + payload_attestation_message, + valid, + } => { + tester + .process_payload_attestation_message(payload_attestation_message, *valid)?; + } } } @@ -1149,6 +1205,173 @@ impl Tester { expected_should_override_fcu.result, ) } + + pub fn process_payload_attestation_message( + &self, + msg: &PayloadAttestationMessage, + valid: bool, + ) -> Result<(), Error> { + let slot = msg.data.slot; + let block_root = msg.data.beacon_block_root; + + // Get the state at the block to compute the PTC and verify signature. + let store = &self.harness.chain.store; + let block = store + .get_blinded_block(&block_root) + .map_err(|e| Error::InternalError(format!("Failed to load block: {e:?}")))?; + + let state_opt = block.and_then(|block| { + store + .get_hot_state(&block.state_root(), CACHE_STATE_IN_TESTS) + .ok()? + }); + + // Build IndexedPayloadAttestation from the message. + let indexed = IndexedPayloadAttestation:: { + attesting_indices: VariableList::new(vec![msg.validator_index]).unwrap(), + data: msg.data.clone(), + signature: AggregateSignature::from(&msg.signature), + }; + + let result = if let Some(ref state) = state_opt { + is_valid_indexed_payload_attestation( + state, + &indexed, + VerifySignatures::True, + &self.spec, + ) + .map_err(|e| { + Error::InternalError(format!( + "payload attestation signature verification failed for validator {}: {:?}", + msg.validator_index, e + )) + }) + .and_then(|_| { + let ptc = state.get_ptc(slot, &self.spec).map_err(|e| { + Error::InternalError(format!( + "Could not compute PTC for block root {block_root:?} at slot {slot:?}: {e:?}" + )) + })?; + + self.harness + .chain + .canonical_head + .fork_choice_write_lock() + .on_payload_attestation( + self.harness.chain.slot().unwrap(), + &indexed, + AttestationFromBlock::False, + &ptc.0, + ) + .map_err(|e| { + Error::InternalError(format!( + "on_payload_attestation for validator {} failed: {:?}", + msg.validator_index, e + )) + }) + }) + } else { + Err(Error::InternalError(format!( + "Could not get state for block root {block_root:?} at slot {slot:?}" + ))) + }; + + if valid { + result?; + } else if result.is_ok() { + return Err(Error::DidntFail(format!( + "payload_attestation_message for validator {} should have failed", + msg.validator_index + ))); + } + + Ok(()) + } + + pub fn check_payload_timeliness_vote(&self, expected: &PayloadVoteCheck) -> Result<(), Error> { + let fc = self.harness.chain.canonical_head.fork_choice_read_lock(); + let proto_array = fc.proto_array().core_proto_array(); + + let node_index = proto_array + .indices + .get(&expected.block_root) + .ok_or_else(|| { + Error::InternalError(format!( + "Block root {:?} not found in proto array", + expected.block_root + )) + })?; + let node = proto_array + .nodes + .get(*node_index) + .ok_or_else(|| Error::InternalError(format!("Node index {} not found", node_index)))?; + let v29 = node + .as_v29() + .map_err(|_| Error::InternalError("Node is not V29".to_string()))?; + + let timeliness_votes = &v29.payload_timeliness_votes; + let participation = &v29.ptc_participation; + + for (i, expected_vote) in expected.votes.iter().enumerate() { + let actual = if !participation.get(i).unwrap() { + None // not yet voted + } else { + Some(timeliness_votes.get(i).unwrap()) + }; + if actual != *expected_vote { + return Err(Error::NotEqual(format!( + "payload_timeliness_vote[{}]: Got {:?} | Expected {:?}", + i, actual, expected_vote + ))); + } + } + + Ok(()) + } + + pub fn check_payload_data_availability_vote( + &self, + expected: &PayloadVoteCheck, + ) -> Result<(), Error> { + let fc = self.harness.chain.canonical_head.fork_choice_read_lock(); + let proto_array = fc.proto_array().core_proto_array(); + + let node_index = proto_array + .indices + .get(&expected.block_root) + .ok_or_else(|| { + Error::InternalError(format!( + "Block root {:?} not found in proto array", + expected.block_root + )) + })?; + let node = proto_array + .nodes + .get(*node_index) + .ok_or_else(|| Error::InternalError(format!("Node index {} not found", node_index)))?; + let v29 = node + .as_v29() + .map_err(|_| Error::InternalError("Node is not V29".to_string()))?; + + let availability_votes = &v29.payload_data_availability_votes; + let participation = &v29.ptc_participation; + + for (i, expected_vote) in expected.votes.iter().enumerate() { + let actual = if !participation.get(i).unwrap() { + None // not yet voted + } else { + Some(availability_votes.get(i).unwrap()) + }; + if actual != *expected_vote { + return Err(Error::NotEqual(format!( + "payload_data_availability_vote[{}]: Got {:?} | Expected {:?}", + i, actual, expected_vote + ))); + } + } + + Ok(()) + } } /// Checks that the `head` checkpoint from the beacon chain head matches the `fc` checkpoint gleaned diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index e380f51c0a..52cc5d57ae 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -715,10 +715,8 @@ impl Handler for ForkChoiceHandler { return false; } - // Deposit tests exist only for Electra and Fulu (not Gloas). - if self.handler_name == "deposit_with_reorg" - && (!fork_name.electra_enabled() || fork_name.gloas_enabled()) - { + // Deposit tests exist only for Electra and later. + if self.handler_name == "deposit_with_reorg" && !fork_name.electra_enabled() { return false; } @@ -727,10 +725,11 @@ impl Handler for ForkChoiceHandler { return false; } - // on_execution_payload_envelope and get_parent_payload_status tests exist only for - // Gloas and later. + // on_execution_payload_envelope, get_parent_payload_status, and + // on_payload_attestation_message tests exist only for Gloas and later. if (self.handler_name == "on_execution_payload_envelope" - || self.handler_name == "get_parent_payload_status") + || self.handler_name == "get_parent_payload_status" + || self.handler_name == "on_payload_attestation_message") && !fork_name.gloas_enabled() { return false; diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index ca383efdb0..0ff854bd21 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -1079,6 +1079,12 @@ fn fork_choice_get_parent_payload_status() { ForkChoiceHandler::::new("get_parent_payload_status").run(); } +#[test] +fn fork_choice_on_payload_attestation_message() { + ForkChoiceHandler::::new("on_payload_attestation_message").run(); + ForkChoiceHandler::::new("on_payload_attestation_message").run(); +} + #[test] fn optimistic_sync() { OptimisticSyncHandler::::default().run(); diff --git a/testing/execution_engine_integration/src/test_rig.rs b/testing/execution_engine_integration/src/test_rig.rs index ed6b5787b5..61f25e63a1 100644 --- a/testing/execution_engine_integration/src/test_rig.rs +++ b/testing/execution_engine_integration/src/test_rig.rs @@ -320,6 +320,7 @@ impl TestRig { Some(vec![]), None, None, + None, ), ) .await; @@ -366,11 +367,12 @@ impl TestRig { Some(vec![]), None, None, + None, ); let payload_parameters = PayloadParameters { parent_hash, - parent_gas_limit, + parent_gas_limit: Some(parent_gas_limit), proposer_gas_limit: None, payload_attributes: &payload_attributes, forkchoice_update_params: &forkchoice_update_params, @@ -527,11 +529,12 @@ impl TestRig { Some(vec![]), None, None, + None, ); let payload_parameters = PayloadParameters { parent_hash, - parent_gas_limit, + parent_gas_limit: Some(parent_gas_limit), proposer_gas_limit: None, payload_attributes: &payload_attributes, forkchoice_update_params: &forkchoice_update_params, @@ -588,6 +591,7 @@ impl TestRig { Some(vec![]), None, None, + None, ); let slot = Slot::new(42); let head_block_root = Hash256::repeat_byte(100); diff --git a/validator_client/validator_services/src/proposer_preferences_service.rs b/validator_client/validator_services/src/proposer_preferences_service.rs index fbefdf5d96..fc17a1bce6 100644 --- a/validator_client/validator_services/src/proposer_preferences_service.rs +++ b/validator_client/validator_services/src/proposer_preferences_service.rs @@ -136,7 +136,7 @@ impl ProposerPreferencesSer proposal_slot: duty.slot, validator_index: duty.validator_index, fee_recipient, - gas_limit: proposal_data.gas_limit, + target_gas_limit: proposal_data.gas_limit, }, )); }