From 369decc1df03dbbd6f830ccc7cd9b07b9a60d11a Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Tue, 30 Jun 2026 11:12:52 -0700 Subject: [PATCH] Gloas alpha spec 11 (#9511) Alpha spec 11 changes Co-Authored-By: Eitan Seri-Levi Co-Authored-By: Michael Sproul --- beacon_node/beacon_chain/src/beacon_chain.rs | 10 + .../src/block_production/gloas.rs | 22 +- .../gossip_verified_bid.rs | 18 +- .../src/payload_bid_verification/mod.rs | 4 + .../src/payload_bid_verification/tests.rs | 103 +++-- .../gossip_verified_envelope.rs | 10 +- .../src/pending_payload_cache/mod.rs | 7 +- .../src/pending_payload_envelopes.rs | 4 +- .../beacon_chain/tests/prepare_payload.rs | 6 +- beacon_node/beacon_chain/tests/store_tests.rs | 2 +- beacon_node/execution_layer/src/block_hash.rs | 4 +- beacon_node/execution_layer/src/engine_api.rs | 15 +- .../execution_layer/src/engine_api/http.rs | 9 +- .../src/engine_api/json_structures.rs | 425 ++++++++++++++---- .../src/engine_api/new_payload_request.rs | 23 +- beacon_node/execution_layer/src/lib.rs | 8 +- .../src/test_utils/handle_rpc.rs | 12 +- .../src/test_utils/mock_builder.rs | 21 +- .../gossip_methods.rs | 4 +- .../src/network_beacon_processor/tests.rs | 8 +- consensus/fork_choice/src/fork_choice.rs | 17 +- consensus/proto_array/src/error.rs | 10 +- consensus/proto_array/src/proto_array.rs | 16 +- .../src/proto_array_fork_choice.rs | 3 +- .../src/per_block_processing.rs | 43 +- .../src/per_block_processing/errors.rs | 6 +- .../process_operations.rs | 289 +++++------- .../per_block_processing/signature_sets.rs | 18 +- .../state_processing/src/upgrade/gloas.rs | 80 ++-- consensus/types/presets/gnosis/gloas.yaml | 7 + consensus/types/presets/mainnet/gloas.yaml | 9 +- consensus/types/presets/minimal/gloas.yaml | 7 + consensus/types/src/block/beacon_block.rs | 10 +- .../types/src/block/beacon_block_body.rs | 11 +- consensus/types/src/builder/builder_bid.rs | 4 +- .../src/builder/builder_deposit_request.rs | 71 +++ .../types/src/builder/builder_exit_request.rs | 35 ++ .../src/builder/builder_pending_payment.rs | 2 + consensus/types/src/builder/mod.rs | 4 + consensus/types/src/core/chain_spec.rs | 11 + consensus/types/src/core/config_and_preset.rs | 1 + consensus/types/src/core/consts.rs | 3 + consensus/types/src/core/eth_spec.rs | 18 + consensus/types/src/core/preset.rs | 8 + .../execution/execution_payload_envelope.rs | 6 +- .../types/src/execution/execution_requests.rs | 139 +++++- consensus/types/src/execution/mod.rs | 4 +- consensus/types/src/state/beacon_state.rs | 5 +- testing/ef_tests/Makefile | 2 +- testing/ef_tests/src/cases/operations.rs | 79 +++- testing/ef_tests/src/handler.rs | 9 +- testing/ef_tests/src/type_name.rs | 5 +- testing/ef_tests/tests/tests.rs | 51 ++- 53 files changed, 1198 insertions(+), 500 deletions(-) create mode 100644 consensus/types/src/builder/builder_deposit_request.rs create mode 100644 consensus/types/src/builder/builder_exit_request.rs diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index e77701c25f..bd3716ea54 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -6087,6 +6087,11 @@ impl BeaconChain { blob_kzg_commitments: kzg_commitments .ok_or(BlockProductionError::InvalidPayloadFork)?, execution_requests: maybe_requests + .map(|r| ExecutionRequestsElectra { + deposits: r.deposits().clone(), + withdrawals: r.withdrawals().clone(), + consolidations: r.consolidations().clone(), + }) .ok_or(BlockProductionError::MissingExecutionRequests)?, }, }), @@ -6141,6 +6146,11 @@ impl BeaconChain { blob_kzg_commitments: kzg_commitments .ok_or(BlockProductionError::InvalidPayloadFork)?, execution_requests: maybe_requests + .map(|r| ExecutionRequestsElectra { + deposits: r.deposits().clone(), + withdrawals: r.withdrawals().clone(), + consolidations: r.consolidations().clone(), + }) .ok_or(BlockProductionError::MissingExecutionRequests)?, }, }), diff --git a/beacon_node/beacon_chain/src/block_production/gloas.rs b/beacon_node/beacon_chain/src/block_production/gloas.rs index 90fc60524a..f84ad65e12 100644 --- a/beacon_node/beacon_chain/src/block_production/gloas.rs +++ b/beacon_node/beacon_chain/src/block_production/gloas.rs @@ -31,7 +31,7 @@ use types::{ Address, Attestation, AttestationElectra, AttesterSlashing, AttesterSlashingElectra, BeaconBlock, BeaconBlockBodyGloas, BeaconBlockGloas, BeaconState, BeaconStateError, BuilderIndex, ChainSpec, Deposit, Eth1Data, EthSpec, ExecutionBlockHash, ExecutionPayloadBid, - ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequests, FullPayload, Graffiti, + ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequestsGloas, FullPayload, Graffiti, Hash256, PayloadAttestation, ProposerSlashing, RelativeEpoch, SignedBeaconBlock, SignedBlsToExecutionChange, SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, SignedVoluntaryExit, Slot, SyncAggregate, Withdrawal, Withdrawals, @@ -74,7 +74,7 @@ pub struct PartialBeaconBlock { /// The envelope requires the beacon_block_root which can only be computed after the block exists. pub struct ExecutionPayloadData { pub payload: ExecutionPayloadGloas, - pub execution_requests: ExecutionRequests, + pub execution_requests: ExecutionRequestsGloas, pub builder_index: BuilderIndex, pub slot: Slot, pub blobs_and_proofs: (types::BlobsList, types::KzgProofs), @@ -175,7 +175,7 @@ impl BeaconChain { .map(|env| env.message.execution_requests.clone()) .ok_or(BlockProductionError::MissingParentExecutionPayload)? } else { - ExecutionRequests::default() + ExecutionRequestsGloas::default() }; // Part 1/3 (blocking) @@ -259,7 +259,7 @@ impl BeaconChain { produce_at_slot: Slot, randao_reveal: Signature, graffiti: Graffiti, - parent_execution_requests: &ExecutionRequests, + parent_execution_requests: &ExecutionRequestsGloas, ) -> Result<(PartialBeaconBlock, BeaconState), BlockProductionError> { // It is invalid to try to produce a block using a state from a future slot. @@ -530,7 +530,7 @@ impl BeaconChain { &self, partial_beacon_block: PartialBeaconBlock, signed_execution_payload_bid: SignedExecutionPayloadBid, - parent_execution_requests: ExecutionRequests, + parent_execution_requests: ExecutionRequestsGloas, payload_data: Option>, mut state: BeaconState, verification: ProduceBlockVerification, @@ -1109,7 +1109,7 @@ where /// `exit_epoch`. A voluntary exit for the same validator would then fail with `AlreadyExited`. fn filter_voluntary_exits_for_parent_execution_requests( voluntary_exits: &mut Vec, - parent_execution_requests: &ExecutionRequests, + parent_execution_requests: &ExecutionRequestsGloas, pubkey_at_index: impl Fn(u64) -> Option, spec: &ChainSpec, ) { @@ -1161,17 +1161,19 @@ mod tests { fn requests( withdrawals: Vec, consolidations: Vec, - ) -> ExecutionRequests { - ExecutionRequests { + ) -> ExecutionRequestsGloas { + ExecutionRequestsGloas { deposits: VariableList::empty(), withdrawals: VariableList::new(withdrawals).unwrap(), consolidations: VariableList::new(consolidations).unwrap(), + builder_deposits: VariableList::empty(), + builder_exits: VariableList::empty(), } } fn run_filter( exits: &mut Vec, - requests: &ExecutionRequests, + requests: &ExecutionRequestsGloas, validator_pubkeys: &[PublicKeyBytes], spec: &ChainSpec, ) { @@ -1307,7 +1309,7 @@ mod tests { LocalBuildResult { payload_data: ExecutionPayloadData { payload: types::ExecutionPayloadGloas::default(), - execution_requests: ExecutionRequests::default(), + execution_requests: ExecutionRequestsGloas::default(), builder_index: BUILDER_INDEX_SELF_BUILD, slot: Slot::new(0), blobs_and_proofs: (VariableList::empty(), VariableList::empty()), 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 1687760d25..2a0c49aff3 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 @@ -14,7 +14,7 @@ use state_processing::signature_sets::{ use tracing::debug; use types::{ BeaconState, ChainSpec, EthSpec, ExecutionPayloadBid, SignedExecutionPayloadBid, - SignedProposerPreferences, Slot, + SignedProposerPreferences, Slot, consts::gloas::PAYLOAD_BUILDER_VERSION, }; /// Verify that an execution payload bid is consistent with the current chain state @@ -64,6 +64,14 @@ pub(crate) fn verify_bid_consistency( return Err(PayloadBidError::InvalidBuilder { builder_index }); } + let builder_version = head_state.get_builder(builder_index)?.version; + if builder_version != PAYLOAD_BUILDER_VERSION { + return Err(PayloadBidError::InvalidBuilderVersion { + builder_index, + version: builder_version, + }); + } + if !head_state.can_builder_cover_bid(builder_index, bid.value, spec)? { return Err(PayloadBidError::BuilderCantCoverBid { builder_index, @@ -158,6 +166,14 @@ impl GossipVerifiedPayloadBid { }); } + // [REJECT] `bid.prev_randao` is the correct RANDAO mix -- i.e. validate that + // `bid.prev_randao == get_randao_mix(parent_state, get_current_epoch(parent_state))` + if signed_bid.message.prev_randao + != *head_state.get_randao_mix(current_slot.epoch(T::EthSpec::slots_per_epoch()))? + { + return Err(PayloadBidError::InvalidPrevRandao { slot: bid_slot }); + } + // TODO(gloas) reprocess bids whose parent_block_root becomes canonical after a reorg. let head_root = cached_head.head_block_root(); if !fork_choice.is_descendant(bid_parent_block_root, head_root) { 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 e23c537e18..a5453d0c5b 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs @@ -30,6 +30,8 @@ pub enum PayloadBidError { BuilderAlreadySeen { builder_index: u64, slot: Slot }, /// Builder is not valid/active for the given epoch InvalidBuilder { builder_index: u64 }, + /// The builder's version is not `PAYLOAD_BUILDER_VERSION`. + InvalidBuilderVersion { builder_index: u64, version: u8 }, /// The bid value is lower than the currently cached bid. BidValueBelowCached { cached_value: u64, @@ -59,6 +61,8 @@ pub enum PayloadBidError { max_blobs_per_block: usize, blob_kzg_commitments_len: usize, }, + /// The bids prev randao value is invalid + InvalidPrevRandao { slot: Slot }, /// Some Beacon State error BeaconStateError(BeaconStateError), /// Internal error 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 e764f0beb5..cf132c2a97 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs @@ -1,5 +1,5 @@ +use std::assert_matches; use std::sync::Arc; - use std::time::Duration; use bls::{Keypair, PublicKeyBytes, Signature}; @@ -16,6 +16,7 @@ use types::{ Address, ChainSpec, Checkpoint, Domain, Epoch, EthSpec, ExecutionBlockHash, ExecutionPayloadBid, Hash256, MinimalEthSpec, ProposerPreferences, SignedBeaconBlock, SignedExecutionPayloadBid, SignedProposerPreferences, SignedRoot, Slot, + consts::gloas::PAYLOAD_BUILDER_VERSION, }; use proto_array::{Block as ProtoBlock, ExecutionStatus, PayloadStatus}; @@ -86,6 +87,7 @@ impl TestContext { state .add_builder_to_registry( PublicKeyBytes::from(keypair.pk.clone()), + PAYLOAD_BUILDER_VERSION, creds, BUILDER_BALANCE, Slot::new(0), @@ -117,6 +119,7 @@ impl TestContext { let inactive_builder_index = state .add_builder_to_registry( PublicKeyBytes::from(inactive_keypair.pk.clone()), + PAYLOAD_BUILDER_VERSION, inactive_creds, BUILDER_BALANCE, Slot::new(E::slots_per_epoch()), @@ -192,6 +195,40 @@ impl TestContext { } } + fn expected_prev_randao(&self) -> Hash256 { + let current_slot = self.slot_clock.now().expect("should read slot clock"); + let head = self.canonical_head.cached_head(); + *head + .snapshot + .beacon_state + .get_randao_mix(current_slot.epoch(E::slots_per_epoch())) + .expect("should read current epoch randao mix") + } + + fn make_signed_bid( + &self, + slot: Slot, + builder_index: u64, + fee_recipient: Address, + gas_limit: u64, + value: u64, + parent_block_root: Hash256, + ) -> Arc> { + Arc::new(SignedExecutionPayloadBid { + message: ExecutionPayloadBid { + slot, + builder_index, + fee_recipient, + gas_limit, + value, + parent_block_root, + prev_randao: self.expected_prev_randao(), + ..ExecutionPayloadBid::default() + }, + signature: Signature::empty(), + }) + } + fn insert_non_canonical_block(&self) -> Hash256 { let shuffling_id = AttestationShufflingId { shuffling_epoch: Epoch::new(0), @@ -234,28 +271,6 @@ impl TestContext { } } -fn make_signed_bid( - slot: Slot, - builder_index: u64, - fee_recipient: Address, - gas_limit: u64, - value: u64, - parent_block_root: Hash256, -) -> Arc> { - Arc::new(SignedExecutionPayloadBid { - message: ExecutionPayloadBid { - slot, - builder_index, - fee_recipient, - gas_limit, - value, - parent_block_root, - ..ExecutionPayloadBid::default() - }, - signature: Signature::empty(), - }) -} - fn make_signed_preferences( proposal_slot: Slot, validator_index: u64, @@ -288,7 +303,7 @@ fn no_proposer_preferences_for_slot() { } let ctx = TestContext::new(); let gossip = ctx.gossip_ctx(); - let bid = make_signed_bid( + let bid = ctx.make_signed_bid( Slot::new(0), 0, Address::ZERO, @@ -314,7 +329,7 @@ fn builder_already_seen_for_slot() { let slot = Slot::new(1); seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); - let bid = make_signed_bid(slot, 42, Address::ZERO, 30_000_000, 100, Hash256::ZERO); + let bid = ctx.make_signed_bid(slot, 42, Address::ZERO, 30_000_000, 100, Hash256::ZERO); let verified = GossipVerifiedPayloadBid { signed_bid: bid.clone(), }; @@ -341,11 +356,11 @@ fn bid_value_below_cached() { seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); let high_bid = GossipVerifiedPayloadBid { - signed_bid: make_signed_bid(slot, 99, Address::ZERO, 30_000_000, 500, Hash256::ZERO), + signed_bid: ctx.make_signed_bid(slot, 99, Address::ZERO, 30_000_000, 500, Hash256::ZERO), }; ctx.bid_cache.insert_highest_bid(high_bid); - let low_bid = make_signed_bid(slot, 1, Address::ZERO, 30_000_000, 100, Hash256::ZERO); + let low_bid = ctx.make_signed_bid(slot, 1, Address::ZERO, 30_000_000, 100, Hash256::ZERO); let result = GossipVerifiedPayloadBid::new(low_bid, &gossip); assert!(matches!( result, @@ -363,7 +378,7 @@ fn invalid_bid_slot() { let slot = Slot::new(5); seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); - let bid = make_signed_bid( + let bid = ctx.make_signed_bid( slot, 0, Address::ZERO, @@ -372,10 +387,7 @@ fn invalid_bid_slot() { ctx.genesis_block_root, ); let result = GossipVerifiedPayloadBid::new(bid, &gossip); - assert!(matches!( - result, - Err(PayloadBidError::InvalidBidSlot { .. }) - )); + assert_matches!(result, Err(PayloadBidError::InvalidBidSlot { .. })); } #[test] @@ -388,7 +400,7 @@ fn fee_recipient_mismatch() { let slot = Slot::new(1); seed_preferences(&ctx, slot, Address::repeat_byte(0xaa), 30_000_000); - let bid = make_signed_bid( + let bid = ctx.make_signed_bid( slot, 0, Address::ZERO, @@ -410,7 +422,7 @@ fn gas_limit_mismatch() { let slot = Slot::new(1); seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); - let bid = make_signed_bid( + let bid = ctx.make_signed_bid( slot, 0, Address::ZERO, @@ -438,6 +450,7 @@ fn execution_payment_nonzero() { gas_limit: 30_000_000, execution_payment: 42, parent_block_root: ctx.genesis_block_root, + prev_randao: ctx.expected_prev_randao(), ..ExecutionPayloadBid::default() }, signature: Signature::empty(), @@ -460,7 +473,7 @@ fn unknown_builder_index() { seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); // Use a builder_index that doesn't exist in the registry. - let bid = make_signed_bid( + let bid = ctx.make_signed_bid( slot, 9999, Address::ZERO, @@ -487,7 +500,7 @@ fn inactive_builder() { let slot = Slot::new(1); seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); - let bid = make_signed_bid( + let bid = ctx.make_signed_bid( slot, ctx.inactive_builder_index, Address::ZERO, @@ -513,7 +526,7 @@ fn builder_cant_cover_bid() { seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); // Builder index 0 exists but bid value far exceeds their balance. - let bid = make_signed_bid( + let bid = ctx.make_signed_bid( slot, 0, Address::ZERO, @@ -540,7 +553,7 @@ fn parent_block_root_unknown() { // Parent block root not in fork choice. let unknown_root = Hash256::repeat_byte(0xff); - let bid = make_signed_bid(slot, 0, Address::ZERO, 30_000_000, 0, unknown_root); + let bid = ctx.make_signed_bid(slot, 0, Address::ZERO, 30_000_000, 0, unknown_root); let result = GossipVerifiedPayloadBid::new(bid, &gossip); assert!(result.is_err(), "expected error, got Ok"); let err = result.unwrap_err(); @@ -563,7 +576,7 @@ fn parent_block_root_not_canonical() { seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); let fork_root = ctx.insert_non_canonical_block(); - let bid = make_signed_bid(slot, 0, Address::ZERO, 30_000_000, 0, fork_root); + let bid = ctx.make_signed_bid(slot, 0, Address::ZERO, 30_000_000, 0, fork_root); let result = GossipVerifiedPayloadBid::new(bid, &gossip); assert!(result.is_err(), "expected error, got Ok"); let err = result.unwrap_err(); @@ -585,7 +598,7 @@ fn bid_slot_not_after_parent() { let slot = Slot::new(0); seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); - let bid = make_signed_bid( + let bid = ctx.make_signed_bid( slot, 0, Address::ZERO, @@ -628,6 +641,7 @@ fn invalid_blob_kzg_commitments() { gas_limit: 30_000_000, value: 0, parent_block_root: ctx.genesis_block_root, + prev_randao: ctx.expected_prev_randao(), blob_kzg_commitments: VariableList::new(commitments).unwrap(), ..ExecutionPayloadBid::default() }, @@ -651,7 +665,7 @@ fn bad_signature() { seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); // All checks pass but signature is empty/invalid. - let bid = make_signed_bid( + let bid = ctx.make_signed_bid( slot, 0, Address::ZERO, @@ -686,6 +700,7 @@ fn valid_bid() { gas_limit: 30_000_000, value: 0, parent_block_root: ctx.genesis_block_root, + prev_randao: ctx.expected_prev_randao(), ..ExecutionPayloadBid::default() }); let result = GossipVerifiedPayloadBid::new(bid, &gossip); @@ -713,6 +728,7 @@ fn two_builders_coexist_in_cache() { gas_limit: 30_000_000, value: 0, parent_block_root: ctx.genesis_block_root, + prev_randao: ctx.expected_prev_randao(), ..ExecutionPayloadBid::default() }); let result_0 = GossipVerifiedPayloadBid::new(bid_0, &gossip); @@ -730,6 +746,7 @@ fn two_builders_coexist_in_cache() { gas_limit: 30_000_000, value: 1, parent_block_root: ctx.genesis_block_root, + prev_randao: ctx.expected_prev_randao(), ..ExecutionPayloadBid::default() }); let result_1 = GossipVerifiedPayloadBid::new(bid_1, &gossip); @@ -763,7 +780,7 @@ fn bid_equal_to_cached_value_rejected() { // Seed a cached bid with value 100. let high_bid = GossipVerifiedPayloadBid { - signed_bid: make_signed_bid( + signed_bid: ctx.make_signed_bid( slot, 99, Address::ZERO, @@ -775,7 +792,7 @@ fn bid_equal_to_cached_value_rejected() { ctx.bid_cache.insert_highest_bid(high_bid); // Submit a bid with exactly the same value — should be rejected. - let equal_bid = make_signed_bid( + let equal_bid = ctx.make_signed_bid( slot, 1, Address::ZERO, 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 index a20963302b..341c7c00f0 100644 --- 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 @@ -315,9 +315,9 @@ mod tests { use ssz_types::VariableList; use types::{ BeaconBlock, BeaconBlockBodyGloas, BeaconBlockGloas, Eth1Data, ExecutionBlockHash, - ExecutionPayloadBid, ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequests, - Graffiti, Hash256, MinimalEthSpec, SignedBeaconBlock, SignedExecutionPayloadBid, Slot, - SyncAggregate, + ExecutionPayloadBid, ExecutionPayloadEnvelope, ExecutionPayloadGloas, + ExecutionRequestsGloas, Graffiti, Hash256, MinimalEthSpec, SignedBeaconBlock, + SignedExecutionPayloadBid, Slot, SyncAggregate, }; use super::verify_envelope_consistency; @@ -336,7 +336,7 @@ mod tests { slot_number: slot, ..ExecutionPayloadGloas::default() }, - execution_requests: ExecutionRequests::default(), + execution_requests: ExecutionRequestsGloas::default(), builder_index, beacon_block_root: Hash256::ZERO, parent_beacon_block_root: Hash256::ZERO, @@ -364,7 +364,7 @@ mod tests { voluntary_exits: VariableList::empty(), sync_aggregate: SyncAggregate::empty(), bls_to_execution_changes: VariableList::empty(), - parent_execution_requests: ExecutionRequests::default(), + parent_execution_requests: ExecutionRequestsGloas::default(), signed_execution_payload_bid: SignedExecutionPayloadBid::empty(), payload_attestations: VariableList::empty(), _phantom: PhantomData, diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs index 33271d6d0e..bb0a1a124d 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs @@ -502,10 +502,9 @@ mod data_availability_checker_tests { use logging::create_test_tracing_subscriber; use slot_clock::{SlotClock, TestingSlotClock}; use std::time::Duration; - use types::test_utils::test_unstructured; use types::{ - ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequests, ForkName, - MinimalEthSpec, SignedExecutionPayloadEnvelope, Slot, + ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequestsGloas, ForkName, + MinimalEthSpec, SignedExecutionPayloadEnvelope, Slot, test_utils::test_unstructured, }; type E = MinimalEthSpec; @@ -611,7 +610,7 @@ mod data_availability_checker_tests { envelope: Arc::new(SignedExecutionPayloadEnvelope { message: ExecutionPayloadEnvelope { payload: ExecutionPayloadGloas::default(), - execution_requests: ExecutionRequests::default(), + execution_requests: ExecutionRequestsGloas::default(), builder_index: 0, beacon_block_root: block_root, parent_beacon_block_root: Hash256::random(), diff --git a/beacon_node/beacon_chain/src/pending_payload_envelopes.rs b/beacon_node/beacon_chain/src/pending_payload_envelopes.rs index 8f7568d017..5d4e461f26 100644 --- a/beacon_node/beacon_chain/src/pending_payload_envelopes.rs +++ b/beacon_node/beacon_chain/src/pending_payload_envelopes.rs @@ -91,7 +91,7 @@ impl PendingPayloadEnvelopes { #[cfg(test)] mod tests { use super::*; - use types::{ExecutionPayloadGloas, ExecutionRequests, Hash256, MainnetEthSpec}; + use types::{ExecutionPayloadGloas, ExecutionRequestsGloas, Hash256, MainnetEthSpec}; type E = MainnetEthSpec; @@ -102,7 +102,7 @@ mod tests { slot_number: slot, ..ExecutionPayloadGloas::default() }, - execution_requests: ExecutionRequests::default(), + execution_requests: ExecutionRequestsGloas::default(), builder_index: 0, beacon_block_root: Hash256::ZERO, parent_beacon_block_root: Hash256::ZERO, diff --git a/beacon_node/beacon_chain/tests/prepare_payload.rs b/beacon_node/beacon_chain/tests/prepare_payload.rs index de8bfb3865..ad23b77a55 100644 --- a/beacon_node/beacon_chain/tests/prepare_payload.rs +++ b/beacon_node/beacon_chain/tests/prepare_payload.rs @@ -184,11 +184,13 @@ async fn prepare_payload_generic( // created with eth1 withdrawal credentials in the interop genesis builder. let consolidation_request = harness.make_switch_to_compounding_request(1); - let execution_requests = ExecutionRequests:: { + let execution_requests = ExecutionRequests::Gloas(ExecutionRequestsGloas:: { deposits: VariableList::empty(), withdrawals: VariableList::empty(), consolidations: VariableList::new(vec![consolidation_request]).unwrap(), - }; + builder_deposits: VariableList::empty(), + builder_exits: VariableList::empty(), + }); // Inject the execution requests into the mock EL so the next payload includes them. harness diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index f15beb8197..7eb040fc71 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -1485,7 +1485,7 @@ async fn proposer_shuffling_changing_with_lookahead() { target_pubkey: validator_to_topup.pubkey, }; - let execution_requests = ExecutionRequests:: { + let execution_requests = ExecutionRequestsElectra:: { deposits: VariableList::new(vec![deposit_request]).unwrap(), withdrawals: vec![].try_into().unwrap(), consolidations: VariableList::new(vec![consolidation_request]).unwrap(), diff --git a/beacon_node/execution_layer/src/block_hash.rs b/beacon_node/execution_layer/src/block_hash.rs index e45bf477a2..69a6324b23 100644 --- a/beacon_node/execution_layer/src/block_hash.rs +++ b/beacon_node/execution_layer/src/block_hash.rs @@ -7,7 +7,7 @@ use keccak_hash::KECCAK_EMPTY_LIST_RLP; use triehash::ordered_trie_root; use types::{ EncodableExecutionBlockHeader, EthSpec, ExecutionBlockHash, ExecutionBlockHeader, - ExecutionPayloadRef, ExecutionRequests, Hash256, + ExecutionPayloadRef, ExecutionRequestsRef, Hash256, }; /// Calculate the block hash of an execution block. @@ -17,7 +17,7 @@ use types::{ pub fn calculate_execution_block_hash( payload: ExecutionPayloadRef, parent_beacon_block_root: Option, - execution_requests: Option<&ExecutionRequests>, + execution_requests: Option>, ) -> (ExecutionBlockHash, Hash256) { // Calculate the transactions root. // We're currently using a deprecated Parity library for this. We should move to a diff --git a/beacon_node/execution_layer/src/engine_api.rs b/beacon_node/execution_layer/src/engine_api.rs index 079a82ff98..9f32bfdfeb 100644 --- a/beacon_node/execution_layer/src/engine_api.rs +++ b/beacon_node/execution_layer/src/engine_api.rs @@ -341,8 +341,13 @@ pub struct GetPayloadResponse { pub blobs_bundle: BlobsBundle, #[superstruct(only(Deneb, Electra, Fulu, Gloas), partial_getter(copy))] pub should_override_builder: bool, - #[superstruct(only(Electra, Fulu, Gloas))] - pub requests: ExecutionRequests, + #[superstruct( + only(Electra, Fulu), + partial_getter(rename = "execution_requests_electra") + )] + pub requests: types::ExecutionRequestsElectra, + #[superstruct(only(Gloas), partial_getter(rename = "execution_requests_gloas"))] + pub requests: types::ExecutionRequestsGloas, } impl GetPayloadResponse { @@ -407,19 +412,19 @@ impl From> ExecutionPayload::Electra(inner.execution_payload), inner.block_value, Some(inner.blobs_bundle), - Some(inner.requests), + Some(ExecutionRequests::Electra(inner.requests)), ), GetPayloadResponse::Fulu(inner) => ( ExecutionPayload::Fulu(inner.execution_payload), inner.block_value, Some(inner.blobs_bundle), - Some(inner.requests), + Some(ExecutionRequests::Electra(inner.requests)), ), GetPayloadResponse::Gloas(inner) => ( ExecutionPayload::Gloas(inner.execution_payload), inner.block_value, Some(inner.blobs_bundle), - Some(inner.requests), + Some(ExecutionRequests::Gloas(inner.requests)), ), } } diff --git a/beacon_node/execution_layer/src/engine_api/http.rs b/beacon_node/execution_layer/src/engine_api/http.rs index 8df7d2a54b..cb5168b6f8 100644 --- a/beacon_node/execution_layer/src/engine_api/http.rs +++ b/beacon_node/execution_layer/src/engine_api/http.rs @@ -844,8 +844,7 @@ impl HttpJsonRpc { ), new_payload_request_electra.versioned_hashes, new_payload_request_electra.parent_beacon_block_root, - new_payload_request_electra - .execution_requests + types::ExecutionRequestsRef::Electra(new_payload_request_electra.execution_requests) .get_execution_requests_list(), ]); @@ -873,8 +872,7 @@ impl HttpJsonRpc { ), new_payload_request_fulu.versioned_hashes, new_payload_request_fulu.parent_beacon_block_root, - new_payload_request_fulu - .execution_requests + types::ExecutionRequestsRef::Electra(new_payload_request_fulu.execution_requests) .get_execution_requests_list(), ]); @@ -902,8 +900,7 @@ impl HttpJsonRpc { ), new_payload_request_gloas.versioned_hashes, new_payload_request_gloas.parent_beacon_block_root, - new_payload_request_gloas - .execution_requests + types::ExecutionRequestsRef::Gloas(new_payload_request_gloas.execution_requests) .get_execution_requests_list(), ]); diff --git a/beacon_node/execution_layer/src/engine_api/json_structures.rs b/beacon_node/execution_layer/src/engine_api/json_structures.rs index fb516e3e16..dfa1c25282 100644 --- a/beacon_node/execution_layer/src/engine_api/json_structures.rs +++ b/beacon_node/execution_layer/src/engine_api/json_structures.rs @@ -1,12 +1,15 @@ use super::*; use alloy_rlp::RlpEncodable; use serde::{Deserialize, Serialize}; -use ssz::{Decode, Encode, TryFromIter}; +use ssz::{Decode, TryFromIter}; use ssz_types::{FixedVector, VariableList, typenum::Unsigned}; use strum::EnumString; use superstruct::superstruct; use types::data::BlobsList; -use types::execution::{ConsolidationRequests, DepositRequests, RequestType, WithdrawalRequests}; +use types::execution::{ + BuilderDepositRequests, BuilderExitRequests, ConsolidationRequests, DepositRequests, + ExecutionRequestsElectra, ExecutionRequestsGloas, RequestType, WithdrawalRequests, +}; use types::kzg_ext::KzgCommitments; use types::{Blob, KzgProof}; @@ -471,6 +474,7 @@ pub enum RequestsError { InvalidOrdering, InvalidPrefix(u8), DecodeError(String), + VariantMismatch, } /// Format of `ExecutionRequests` received over the engine api. @@ -483,92 +487,150 @@ pub struct JsonExecutionRequests(pub Vec); impl From> for JsonExecutionRequests { fn from(requests: ExecutionRequests) -> Self { - let mut result = Vec::new(); - if !requests.deposits.is_empty() { - result.push(format!( - "0x{:02x}{}", - RequestType::Deposit.to_u8(), - hex::encode(requests.deposits.as_ssz_bytes()) - )); - } - if !requests.withdrawals.is_empty() { - result.push(format!( - "0x{:02x}{}", - RequestType::Withdrawal.to_u8(), - hex::encode(requests.withdrawals.as_ssz_bytes()) - )); - } - if !requests.consolidations.is_empty() { - result.push(format!( - "0x{:02x}{}", - RequestType::Consolidation.to_u8(), - hex::encode(requests.consolidations.as_ssz_bytes()) - )); - } + // Each element is a `RequestType`-prefixed, SSZ-encoded request list. + let result = requests + .get_execution_requests_list() + .into_iter() + .map(|bytes| format!("0x{}", hex::encode(bytes))) + .collect(); JsonExecutionRequests(result) } } -impl TryFrom for ExecutionRequests { - type Error = RequestsError; +/// Parse an EIP-7685 `JsonExecutionRequests` list into its component request lists. +/// +/// Returns the deposit, withdrawal, consolidation, builder deposit and builder exit lists. +/// Builder lists are empty pre-gloas or post-gloas when no builder requests are present. +#[allow(clippy::type_complexity)] +fn parse_execution_requests( + value: JsonExecutionRequests, +) -> Result< + ( + DepositRequests, + WithdrawalRequests, + ConsolidationRequests, + BuilderDepositRequests, + BuilderExitRequests, + ), + RequestsError, +> { + let mut deposits = DepositRequests::::default(); + let mut withdrawals = WithdrawalRequests::::default(); + let mut consolidations = ConsolidationRequests::::default(); + let mut builder_deposits = BuilderDepositRequests::::default(); + let mut builder_exits = BuilderExitRequests::::default(); + let mut prev_prefix: Option = None; + for (i, request) in value.0.into_iter().enumerate() { + // hex string + let decoded_bytes = hex::decode(request.strip_prefix("0x").unwrap_or(&request)) + .map_err(RequestsError::InvalidHex)?; - fn try_from(value: JsonExecutionRequests) -> Result { - let mut requests = ExecutionRequests::default(); - let mut prev_prefix: Option = None; - for (i, request) in value.0.into_iter().enumerate() { - // hex string - let decoded_bytes = hex::decode(request.strip_prefix("0x").unwrap_or(&request)) - .map_err(RequestsError::InvalidHex)?; + // The first byte of each element is the `request_type` and the remaining bytes are the `request_data`. + // Elements with empty `request_data` **MUST** be excluded from the list. + let Some((prefix_byte, request_bytes)) = decoded_bytes.split_first() else { + return Err(RequestsError::EmptyRequest(i)); + }; + if request_bytes.is_empty() { + return Err(RequestsError::EmptyRequest(i)); + } + // Elements of the list **MUST** be ordered by `request_type` in ascending order + let current_prefix = + RequestType::from_u8(*prefix_byte).ok_or(RequestsError::InvalidPrefix(*prefix_byte))?; + if let Some(prev) = prev_prefix + && prev.to_u8() >= current_prefix.to_u8() + { + return Err(RequestsError::InvalidOrdering); + } + prev_prefix = Some(current_prefix); - // The first byte of each element is the `request_type` and the remaining bytes are the `request_data`. - // Elements with empty `request_data` **MUST** be excluded from the list. - let Some((prefix_byte, request_bytes)) = decoded_bytes.split_first() else { - return Err(RequestsError::EmptyRequest(i)); - }; - if request_bytes.is_empty() { - return Err(RequestsError::EmptyRequest(i)); + match current_prefix { + RequestType::Deposit => { + deposits = DepositRequests::::from_ssz_bytes(request_bytes).map_err(|e| { + RequestsError::DecodeError(format!( + "Failed to decode DepositRequest from EL: {:?}", + e + )) + })?; } - // Elements of the list **MUST** be ordered by `request_type` in ascending order - let current_prefix = RequestType::from_u8(*prefix_byte) - .ok_or(RequestsError::InvalidPrefix(*prefix_byte))?; - if let Some(prev) = prev_prefix - && prev.to_u8() >= current_prefix.to_u8() - { - return Err(RequestsError::InvalidOrdering); - } - prev_prefix = Some(current_prefix); - - match current_prefix { - RequestType::Deposit => { - requests.deposits = DepositRequests::::from_ssz_bytes(request_bytes) - .map_err(|e| { - RequestsError::DecodeError(format!( - "Failed to decode DepositRequest from EL: {:?}", - e - )) - })?; - } - RequestType::Withdrawal => { - requests.withdrawals = WithdrawalRequests::::from_ssz_bytes(request_bytes) - .map_err(|e| { + RequestType::Withdrawal => { + withdrawals = + WithdrawalRequests::::from_ssz_bytes(request_bytes).map_err(|e| { RequestsError::DecodeError(format!( "Failed to decode WithdrawalRequest from EL: {:?}", e )) })?; - } - RequestType::Consolidation => { - requests.consolidations = - ConsolidationRequests::::from_ssz_bytes(request_bytes).map_err(|e| { - RequestsError::DecodeError(format!( - "Failed to decode ConsolidationRequest from EL: {:?}", - e - )) - })?; - } + } + RequestType::Consolidation => { + consolidations = ConsolidationRequests::::from_ssz_bytes(request_bytes) + .map_err(|e| { + RequestsError::DecodeError(format!( + "Failed to decode ConsolidationRequest from EL: {:?}", + e + )) + })?; + } + RequestType::BuilderDeposit => { + builder_deposits = BuilderDepositRequests::::from_ssz_bytes(request_bytes) + .map_err(|e| { + RequestsError::DecodeError(format!( + "Failed to decode BuilderDepositRequest from EL: {:?}", + e + )) + })?; + } + RequestType::BuilderExit => { + builder_exits = + BuilderExitRequests::::from_ssz_bytes(request_bytes).map_err(|e| { + RequestsError::DecodeError(format!( + "Failed to decode BuilderExitRequest from EL: {:?}", + e + )) + })?; } } - Ok(requests) + } + + Ok(( + deposits, + withdrawals, + consolidations, + builder_deposits, + builder_exits, + )) +} + +impl TryFrom for ExecutionRequestsElectra { + type Error = RequestsError; + + fn try_from(value: JsonExecutionRequests) -> Result { + let (deposits, withdrawals, consolidations, builder_deposits, builder_exits) = + parse_execution_requests::(value)?; + // Builder requests are not valid pre-Gloas. + if !builder_deposits.is_empty() || !builder_exits.is_empty() { + return Err(RequestsError::VariantMismatch); + } + Ok(ExecutionRequestsElectra { + deposits, + withdrawals, + consolidations, + }) + } +} + +impl TryFrom for ExecutionRequestsGloas { + type Error = RequestsError; + + fn try_from(value: JsonExecutionRequests) -> Result { + let (deposits, withdrawals, consolidations, builder_deposits, builder_exits) = + parse_execution_requests::(value)?; + Ok(ExecutionRequestsGloas { + deposits, + withdrawals, + consolidations, + builder_deposits, + builder_exits, + }) } } @@ -1167,7 +1229,8 @@ mod tests { use bls::{PublicKeyBytes, SignatureBytes}; use ssz::Encode; use types::{ - ConsolidationRequest, DepositRequest, MainnetEthSpec, RequestType, WithdrawalRequest, + BuilderDepositRequest, BuilderExitRequest, ConsolidationRequest, DepositRequest, + MainnetEthSpec, RequestType, WithdrawalRequest, }; use super::*; @@ -1180,6 +1243,10 @@ mod tests { ) } + fn singleton_list(x: &T) -> VariableList { + VariableList::try_from(vec![x.clone()]).unwrap() + } + /// Tests all error conditions except ssz decoding errors /// /// *** @@ -1211,40 +1278,60 @@ mod tests { }; // First check a valid request with all requests - assert!( - ExecutionRequests::::try_from(JsonExecutionRequests(vec![ + assert_eq!( + ExecutionRequestsElectra::::try_from(JsonExecutionRequests(vec![ create_request_string(RequestType::Deposit.to_u8(), &deposit_request), create_request_string(RequestType::Withdrawal.to_u8(), &withdrawal_request), create_request_string(RequestType::Consolidation.to_u8(), &consolidation_request), ])) - .is_ok() + .unwrap(), + ExecutionRequestsElectra { + deposits: singleton_list(&deposit_request), + withdrawals: singleton_list(&withdrawal_request), + consolidations: singleton_list(&consolidation_request), + } ); // Single requests - assert!( - ExecutionRequests::::try_from(JsonExecutionRequests(vec![ + assert_eq!( + ExecutionRequestsElectra::::try_from(JsonExecutionRequests(vec![ create_request_string(RequestType::Deposit.to_u8(), &deposit_request), ])) - .is_ok() + .unwrap(), + ExecutionRequestsElectra { + deposits: singleton_list(&deposit_request), + withdrawals: Default::default(), + consolidations: Default::default(), + } ); - assert!( - ExecutionRequests::::try_from(JsonExecutionRequests(vec![ + assert_eq!( + ExecutionRequestsElectra::::try_from(JsonExecutionRequests(vec![ create_request_string(RequestType::Withdrawal.to_u8(), &withdrawal_request), ])) - .is_ok() + .unwrap(), + ExecutionRequestsElectra { + deposits: Default::default(), + withdrawals: singleton_list(&withdrawal_request), + consolidations: Default::default(), + } ); - assert!( - ExecutionRequests::::try_from(JsonExecutionRequests(vec![ + assert_eq!( + ExecutionRequestsElectra::::try_from(JsonExecutionRequests(vec![ create_request_string(RequestType::Consolidation.to_u8(), &consolidation_request), ])) - .is_ok() + .unwrap(), + ExecutionRequestsElectra { + deposits: Default::default(), + withdrawals: Default::default(), + consolidations: singleton_list(&consolidation_request), + } ); // Out of order assert!(matches!( - ExecutionRequests::::try_from(JsonExecutionRequests(vec![ + ExecutionRequestsElectra::::try_from(JsonExecutionRequests(vec![ create_request_string(RequestType::Withdrawal.to_u8(), &withdrawal_request), create_request_string(RequestType::Deposit.to_u8(), &deposit_request), ])) @@ -1253,7 +1340,7 @@ mod tests { )); assert!(matches!( - ExecutionRequests::::try_from(JsonExecutionRequests(vec![ + ExecutionRequestsElectra::::try_from(JsonExecutionRequests(vec![ create_request_string(RequestType::Consolidation.to_u8(), &consolidation_request), create_request_string(RequestType::Withdrawal.to_u8(), &withdrawal_request), ])) @@ -1262,7 +1349,7 @@ mod tests { )); assert!(matches!( - ExecutionRequests::::try_from(JsonExecutionRequests(vec![ + ExecutionRequestsElectra::::try_from(JsonExecutionRequests(vec![ create_request_string(RequestType::Consolidation.to_u8(), &consolidation_request), create_request_string(RequestType::Deposit.to_u8(), &deposit_request), ])) @@ -1272,7 +1359,7 @@ mod tests { // Multiple requests of same type assert!(matches!( - ExecutionRequests::::try_from(JsonExecutionRequests(vec![ + ExecutionRequestsElectra::::try_from(JsonExecutionRequests(vec![ create_request_string(RequestType::Deposit.to_u8(), &deposit_request), create_request_string(RequestType::Deposit.to_u8(), &deposit_request), ])) @@ -1282,7 +1369,7 @@ mod tests { // Invalid prefix assert!(matches!( - ExecutionRequests::::try_from(JsonExecutionRequests(vec![ + ExecutionRequestsElectra::::try_from(JsonExecutionRequests(vec![ create_request_string(42, &deposit_request), ])) .unwrap_err(), @@ -1291,7 +1378,7 @@ mod tests { // Prefix followed by no data assert!(matches!( - ExecutionRequests::::try_from(JsonExecutionRequests(vec![ + ExecutionRequestsElectra::::try_from(JsonExecutionRequests(vec![ create_request_string(RequestType::Deposit.to_u8(), &deposit_request), create_request_string( RequestType::Consolidation.to_u8(), @@ -1303,12 +1390,162 @@ mod tests { )); // Empty request assert!(matches!( - ExecutionRequests::::try_from(JsonExecutionRequests(vec![ + ExecutionRequestsElectra::::try_from(JsonExecutionRequests(vec![ create_request_string(RequestType::Deposit.to_u8(), &deposit_request), "0x".to_string() ])) .unwrap_err(), RequestsError::EmptyRequest(1) )); + + // Builder requests are not valid pre-gloas. + let builder_deposit_request = BuilderDepositRequest { + pubkey: PublicKeyBytes::empty(), + withdrawal_credentials: Hash256::random(), + amount: 32, + signature: SignatureBytes::empty(), + }; + assert!(matches!( + ExecutionRequestsElectra::::try_from(JsonExecutionRequests(vec![ + create_request_string( + RequestType::BuilderDeposit.to_u8(), + &builder_deposit_request + ), + ])) + .unwrap_err(), + RequestsError::VariantMismatch + )); + } + + #[test] + fn test_gloas_execution_requests() { + let deposit_request = DepositRequest { + pubkey: PublicKeyBytes::empty(), + withdrawal_credentials: Hash256::random(), + amount: 32, + signature: SignatureBytes::empty(), + index: 0, + }; + + let withdrawal_request = WithdrawalRequest { + amount: 32, + source_address: Address::random(), + validator_pubkey: PublicKeyBytes::empty(), + }; + + let consolidation_request = ConsolidationRequest { + source_address: Address::random(), + source_pubkey: PublicKeyBytes::empty(), + target_pubkey: PublicKeyBytes::empty(), + }; + + let builder_deposit_request = BuilderDepositRequest { + pubkey: PublicKeyBytes::empty(), + withdrawal_credentials: Hash256::random(), + amount: 32, + signature: SignatureBytes::empty(), + }; + + let builder_exit_request = BuilderExitRequest { + source_address: Address::random(), + pubkey: PublicKeyBytes::empty(), + }; + + // Valid request with all five request types, in ascending prefix order. + assert_eq!( + ExecutionRequestsGloas::::try_from(JsonExecutionRequests(vec![ + create_request_string(RequestType::Deposit.to_u8(), &deposit_request), + create_request_string(RequestType::Withdrawal.to_u8(), &withdrawal_request), + create_request_string(RequestType::Consolidation.to_u8(), &consolidation_request), + create_request_string( + RequestType::BuilderDeposit.to_u8(), + &builder_deposit_request + ), + create_request_string(RequestType::BuilderExit.to_u8(), &builder_exit_request), + ])) + .unwrap(), + ExecutionRequestsGloas { + deposits: singleton_list(&deposit_request), + withdrawals: singleton_list(&withdrawal_request), + consolidations: singleton_list(&consolidation_request), + builder_deposits: singleton_list(&builder_deposit_request), + builder_exits: singleton_list(&builder_exit_request), + } + ); + + // A builder-less list is a valid Gloas value (builder lists are simply empty). + assert_eq!( + ExecutionRequestsGloas::::try_from(JsonExecutionRequests(vec![ + create_request_string(RequestType::Deposit.to_u8(), &deposit_request), + ])) + .unwrap(), + ExecutionRequestsGloas { + deposits: singleton_list(&deposit_request), + withdrawals: Default::default(), + consolidations: Default::default(), + builder_deposits: Default::default(), + builder_exits: Default::default(), + } + ); + + // Only builder requests. + assert_eq!( + ExecutionRequestsGloas::::try_from(JsonExecutionRequests(vec![ + create_request_string( + RequestType::BuilderDeposit.to_u8(), + &builder_deposit_request + ), + create_request_string(RequestType::BuilderExit.to_u8(), &builder_exit_request), + ])) + .unwrap(), + ExecutionRequestsGloas { + deposits: Default::default(), + withdrawals: Default::default(), + consolidations: Default::default(), + builder_deposits: singleton_list(&builder_deposit_request), + builder_exits: singleton_list(&builder_exit_request), + } + ); + + // Out of order: builder exit must come after a builder deposit. + assert!(matches!( + ExecutionRequestsGloas::::try_from(JsonExecutionRequests(vec![ + create_request_string(RequestType::BuilderExit.to_u8(), &builder_exit_request), + create_request_string( + RequestType::BuilderDeposit.to_u8(), + &builder_deposit_request + ), + ])) + .unwrap_err(), + RequestsError::InvalidOrdering + )); + + // Duplicate builder request type. + assert!(matches!( + ExecutionRequestsGloas::::try_from(JsonExecutionRequests(vec![ + create_request_string( + RequestType::BuilderDeposit.to_u8(), + &builder_deposit_request + ), + create_request_string( + RequestType::BuilderDeposit.to_u8(), + &builder_deposit_request + ), + ])) + .unwrap_err(), + RequestsError::InvalidOrdering + )); + + // Empty builder request data. + assert!(matches!( + ExecutionRequestsGloas::::try_from(JsonExecutionRequests(vec![ + create_request_string( + RequestType::BuilderDeposit.to_u8(), + &Vec::::new() + ), + ])) + .unwrap_err(), + RequestsError::EmptyRequest(0) + )); } } diff --git a/beacon_node/execution_layer/src/engine_api/new_payload_request.rs b/beacon_node/execution_layer/src/engine_api/new_payload_request.rs index ba94296b85..d6b7f018aa 100644 --- a/beacon_node/execution_layer/src/engine_api/new_payload_request.rs +++ b/beacon_node/execution_layer/src/engine_api/new_payload_request.rs @@ -9,7 +9,8 @@ use types::{ }; use types::{ ExecutionPayloadBellatrix, ExecutionPayloadCapella, ExecutionPayloadDeneb, - ExecutionPayloadElectra, ExecutionPayloadFulu, ExecutionPayloadGloas, ExecutionRequests, + ExecutionPayloadElectra, ExecutionPayloadFulu, ExecutionPayloadGloas, ExecutionRequestsElectra, + ExecutionRequestsGloas, ExecutionRequestsRef, }; #[superstruct( @@ -47,8 +48,13 @@ pub struct NewPayloadRequest<'block, E: EthSpec> { pub versioned_hashes: Vec, #[superstruct(only(Deneb, Electra, Fulu, Gloas))] pub parent_beacon_block_root: Hash256, - #[superstruct(only(Electra, Fulu, Gloas))] - pub execution_requests: &'block ExecutionRequests, + #[superstruct( + only(Electra, Fulu), + partial_getter(rename = "execution_requests_electra") + )] + pub execution_requests: &'block ExecutionRequestsElectra, + #[superstruct(only(Gloas), partial_getter(rename = "execution_requests_gloas"))] + pub execution_requests: &'block ExecutionRequestsGloas, } impl<'block, E: EthSpec> NewPayloadRequest<'block, E> { @@ -123,6 +129,15 @@ impl<'block, E: EthSpec> NewPayloadRequest<'block, E> { Ok(()) } + pub fn execution_requests_ref(&self) -> Option> { + match self { + Self::Bellatrix(_) | Self::Capella(_) | Self::Deneb(_) => None, + Self::Electra(r) => Some(ExecutionRequestsRef::Electra(r.execution_requests)), + Self::Fulu(r) => Some(ExecutionRequestsRef::Electra(r.execution_requests)), + Self::Gloas(r) => Some(ExecutionRequestsRef::Gloas(r.execution_requests)), + } + } + /// Verify the block hash is consistent locally within Lighthouse. /// /// ## Specification @@ -143,7 +158,7 @@ impl<'block, E: EthSpec> NewPayloadRequest<'block, E> { let (header_hash, rlp_transactions_root) = calculate_execution_block_hash( payload, parent_beacon_block_root, - self.execution_requests().ok().copied(), + self.execution_requests_ref(), ); if header_hash != self.block_hash() { diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index 78076bee6c..b255e1b75d 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -43,7 +43,6 @@ 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; @@ -56,6 +55,7 @@ use types::{ ExecutionPayloadCapella, ExecutionPayloadElectra, ExecutionPayloadFulu, FullPayload, ProposerPreparationData, Slot, }; +use types::{ExecutionPayloadGloas, ExecutionRequestsGloas}; mod block_hash; mod engine_api; @@ -118,14 +118,14 @@ impl TryFrom> for ProvenancedPayload BlockProposalContents::PayloadAndBlobs { payload: ExecutionPayloadHeader::Fulu(builder_bid.header).into(), block_value: builder_bid.value, kzg_commitments: builder_bid.blob_kzg_commitments, blobs_and_proofs: None, - requests: Some(builder_bid.execution_requests), + requests: Some(builder_bid.execution_requests.into()), }, }; Ok(ProvenancedPayload::Builder( @@ -206,7 +206,7 @@ pub struct BlockProposalContentsGloas { pub payload_value: Uint256, pub blob_kzg_commitments: KzgCommitments, pub blobs_and_proofs: (BlobsList, KzgProofs), - pub execution_requests: ExecutionRequests, + pub execution_requests: ExecutionRequestsGloas, pub should_override_builder: bool, } 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 9924fbe474..10eaefaaba 100644 --- a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs +++ b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs @@ -438,7 +438,9 @@ pub async fn handle_rpc( should_override_builder: false, execution_requests: maybe_execution_requests .clone() - .unwrap_or_default() + .unwrap_or_else(|| { + types::ExecutionRequests::Electra(Default::default()) + }) .into(), }) .unwrap() @@ -461,7 +463,9 @@ pub async fn handle_rpc( should_override_builder: false, execution_requests: maybe_execution_requests .clone() - .unwrap_or_default() + .unwrap_or_else(|| { + types::ExecutionRequests::Electra(Default::default()) + }) .into(), }) .unwrap() @@ -483,7 +487,9 @@ pub async fn handle_rpc( .into(), should_override_builder: false, execution_requests: maybe_execution_requests - .unwrap_or_default() + .unwrap_or_else(|| { + types::ExecutionRequests::Electra(Default::default()) + }) .into(), }) .unwrap() 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 ee4f29a653..e02107e116 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_builder.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_builder.rs @@ -35,8 +35,9 @@ use types::builder::{ }; use types::{ Address, BeaconState, ChainSpec, Epoch, EthSpec, ExecPayload, ExecutionPayload, - ExecutionPayloadHeaderRefMut, ExecutionRequests, ForkName, ForkVersionDecode, Hash256, - SignedBlindedBeaconBlock, SignedRoot, SignedValidatorRegistrationData, Slot, Uint256, + ExecutionPayloadHeaderRefMut, ExecutionRequests, ExecutionRequestsElectra, ForkName, + ForkVersionDecode, Hash256, SignedBlindedBeaconBlock, SignedRoot, + SignedValidatorRegistrationData, Slot, Uint256, }; use warp::{ Filter, Rejection, @@ -606,7 +607,13 @@ impl MockBuilder { .unwrap_or_default(), value: self.get_bid_value(value), pubkey: self.builder_sk.public_key().compress(), - execution_requests: maybe_requests.unwrap_or_default(), + execution_requests: maybe_requests + .map(|r| ExecutionRequestsElectra { + deposits: r.deposits().clone(), + withdrawals: r.withdrawals().clone(), + consolidations: r.consolidations().clone(), + }) + .unwrap_or_default(), }), ForkName::Electra => BuilderBid::Electra(BuilderBidElectra { header: payload @@ -618,7 +625,13 @@ impl MockBuilder { .unwrap_or_default(), value: self.get_bid_value(value), pubkey: self.builder_sk.public_key().compress(), - execution_requests: maybe_requests.unwrap_or_default(), + execution_requests: maybe_requests + .map(|r| ExecutionRequestsElectra { + deposits: r.deposits().clone(), + withdrawals: r.withdrawals().clone(), + consolidations: r.consolidations().clone(), + }) + .unwrap_or_default(), }), ForkName::Deneb => BuilderBid::Deneb(BuilderBidDeneb { header: payload 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 d6873fbe9c..c20673b79b 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -4017,10 +4017,12 @@ impl NetworkBeaconProcessor { Err( PayloadBidError::BadSignature | PayloadBidError::InvalidBuilder { .. } + | PayloadBidError::InvalidBuilderVersion { .. } | PayloadBidError::InvalidFeeRecipient | PayloadBidError::ExecutionPaymentNonZero { .. } | PayloadBidError::InvalidBlobKzgCommitments { .. } - | PayloadBidError::BidNotDescendantOfParent { .. }, + | PayloadBidError::BidNotDescendantOfParent { .. } + | PayloadBidError::InvalidPrevRandao { .. }, ) => { self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject); self.gossip_penalize_peer( diff --git a/beacon_node/network/src/network_beacon_processor/tests.rs b/beacon_node/network/src/network_beacon_processor/tests.rs index 95f7d7649f..f5c557a130 100644 --- a/beacon_node/network/src/network_beacon_processor/tests.rs +++ b/beacon_node/network/src/network_beacon_processor/tests.rs @@ -41,13 +41,13 @@ use std::iter::Iterator; use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc; -use types::data::BlobIdentifier; use types::{ AttesterSlashing, ChainSpec, DataColumnSidecarList, DataColumnSubnetId, Epoch, EthSpec, - ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequests, Hash256, MainnetEthSpec, - ProposerSlashing, SignedAggregateAndProof, SignedBeaconBlock, SignedExecutionPayloadEnvelope, + ExecutionPayloadEnvelope, ExecutionPayloadGloas, Hash256, MainnetEthSpec, ProposerSlashing, + SignedAggregateAndProof, SignedBeaconBlock, SignedExecutionPayloadEnvelope, SignedVoluntaryExit, SingleAttestation, Slot, SubnetId, }; +use types::{ExecutionRequestsGloas, data::BlobIdentifier}; type E = MainnetEthSpec; type T = EphemeralHarnessType; @@ -1968,7 +1968,7 @@ fn make_test_payload_envelope( slot_number: slot, ..ExecutionPayloadGloas::default() }, - execution_requests: ExecutionRequests::default(), + execution_requests: ExecutionRequestsGloas::default(), builder_index: 0, beacon_block_root, parent_beacon_block_root: Hash256::ZERO, diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index 95c8f70a04..bfb1f0b81e 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -134,6 +134,7 @@ impl ResetPayloadStatuses { #[derive(Debug)] pub enum InvalidBlock { UnknownParent(Hash256), + ParentPayloadNotVerified(Hash256), FutureSlot { current_slot: Slot, block_slot: Slot, @@ -816,6 +817,19 @@ where .get_block(&block.parent_root()) .ok_or_else(|| Error::InvalidBlock(InvalidBlock::UnknownParent(block.parent_root())))?; + // If the block builds on a full payload envelope, the envelope must be known. + if let Some(parent_block_hash) = parent_block.execution_payload_block_hash { + let builds_on_full = block + .body() + .signed_execution_payload_bid() + .is_ok_and(|bid| bid.message.parent_block_hash == parent_block_hash); + if builds_on_full && !self.is_payload_received(&block.parent_root()) { + return Err(Error::InvalidBlock(InvalidBlock::ParentPayloadNotVerified( + block.parent_root(), + ))); + } + } + // Blocks cannot be in the future. If they are, their consideration must be delayed until // they are in the past. // @@ -1633,9 +1647,10 @@ where /// 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 current_slot = self.fc_store.get_current_slot(); let proposer_boost_root = self.fc_store.proposer_boost_root(); self.proto_array - .should_extend_payload::(block_root, proposer_boost_root) + .should_extend_payload::(block_root, current_slot, proposer_boost_root) .map_err(Error::ProtoArrayStringError) } diff --git a/consensus/proto_array/src/error.rs b/consensus/proto_array/src/error.rs index 383c1946c6..edf18b1bd1 100644 --- a/consensus/proto_array/src/error.rs +++ b/consensus/proto_array/src/error.rs @@ -1,6 +1,6 @@ use crate::PayloadStatus; use safe_arith::ArithError; -use types::{Epoch, ExecutionBlockHash, Hash256}; +use types::{Epoch, ExecutionBlockHash, Hash256, Slot}; #[derive(Clone, PartialEq, Debug)] pub enum Error { @@ -63,6 +63,14 @@ pub enum Error { block_root: Hash256, payload_status: PayloadStatus, }, + /// `should_extend_payload` was called for a block whose slot is not the previous slot. + /// + /// Spec equivalent: `assert store.blocks[root].slot + 1 == get_current_slot(store)`. + ShouldExtendPayloadInvalidSlot { + block_root: Hash256, + block_slot: Slot, + current_slot: Slot, + }, } impl From for Error { diff --git a/consensus/proto_array/src/proto_array.rs b/consensus/proto_array/src/proto_array.rs index 04113e2c0e..57d3acd55b 100644 --- a/consensus/proto_array/src/proto_array.rs +++ b/consensus/proto_array/src/proto_array.rs @@ -1564,7 +1564,12 @@ impl ProtoArray { Ok(fc_node.payload_status as u8) } else if fc_node.payload_status == PayloadStatus::Empty { Ok(1) - } else if self.should_extend_payload::(fc_node, proto_node, proposer_boost_root)? { + } else if self.should_extend_payload::( + fc_node, + proto_node, + current_slot, + proposer_boost_root, + )? { Ok(2) } else { Ok(0) @@ -1614,8 +1619,17 @@ impl ProtoArray { &self, fc_node: &IndexedForkChoiceNode, proto_node: &ProtoNode, + current_slot: Slot, proposer_boost_root: Hash256, ) -> Result { + if proto_node.slot().saturating_add(1u64) != current_slot { + return Err(Error::ShouldExtendPayloadInvalidSlot { + block_root: fc_node.root, + block_slot: proto_node.slot(), + current_slot, + }); + } + let Ok(node) = proto_node.as_v29() else { return Err(Error::InvalidNodeVariant { block_root: fc_node.root, diff --git a/consensus/proto_array/src/proto_array_fork_choice.rs b/consensus/proto_array/src/proto_array_fork_choice.rs index c6a7829c27..3470ae302f 100644 --- a/consensus/proto_array/src/proto_array_fork_choice.rs +++ b/consensus/proto_array/src/proto_array_fork_choice.rs @@ -999,6 +999,7 @@ impl ProtoArrayForkChoice { pub fn should_extend_payload( &self, block_root: &Hash256, + current_slot: Slot, proposer_boost_root: Hash256, ) -> Result { let block_index = self @@ -1017,7 +1018,7 @@ impl ProtoArrayForkChoice { payload_status: proto_node.get_parent_payload_status(), }; self.proto_array - .should_extend_payload::(&fc_node, proto_node, proposer_boost_root) + .should_extend_payload::(&fc_node, proto_node, current_slot, proposer_boost_root) .map_err(|e| format!("{e:?}")) } diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index f13f2a339b..fc215942fa 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -11,7 +11,10 @@ use signature_sets::{ use std::borrow::Cow; use tree_hash::TreeHash; use typenum::Unsigned; -use types::{consts::gloas::BUILDER_INDEX_SELF_BUILD, *}; +use types::{ + consts::gloas::{BUILDER_INDEX_SELF_BUILD, PAYLOAD_BUILDER_VERSION}, + *, +}; pub use self::verify_attester_slashing::{ get_slashable_indices, get_slashable_indices_modular, verify_attester_slashing, @@ -189,7 +192,8 @@ pub fn per_block_processing>( let body = block.body(); if state.fork_name_unchecked().gloas_enabled() { withdrawals::gloas::process_withdrawals::(state, spec)?; - process_execution_payload_bid(state, block, verify_signatures, spec)?; + let signed_bid = block.body().signed_execution_payload_bid()?; + process_execution_payload_bid(state, signed_bid, verify_signatures, spec)?; } else { if state.fork_name_unchecked().capella_enabled() { withdrawals::capella_electra::process_withdrawals::( @@ -561,7 +565,7 @@ pub fn process_parent_execution_payload( state: &mut BeaconState, - requests: &ExecutionRequests, + requests: &ExecutionRequestsGloas, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { let parent_bid = state.latest_execution_payload_bid()?.clone(); @@ -596,9 +600,11 @@ pub fn apply_parent_execution_payload( let parent_epoch = parent_slot.epoch(E::slots_per_epoch()); // Process execution requests from the parent's payload - process_operations::process_deposit_requests_post_gloas(state, &requests.deposits, spec)?; + process_operations::process_deposit_requests(state, &requests.deposits, spec)?; process_operations::process_withdrawal_requests(state, &requests.withdrawals, spec)?; process_operations::process_consolidation_requests(state, &requests.consolidations, spec)?; + process_operations::process_builder_deposit_requests(state, &requests.builder_deposits, spec)?; + process_operations::process_builder_exit_requests(state, &requests.builder_exits, spec)?; // Queue the builder payment if parent_epoch == state.current_epoch() { @@ -666,15 +672,13 @@ pub fn settle_builder_payment( Ok(()) } -pub fn process_execution_payload_bid>( +pub fn process_execution_payload_bid( state: &mut BeaconState, - block: BeaconBlockRef<'_, E, Payload>, + signed_bid: &SignedExecutionPayloadBid, 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; @@ -698,6 +702,16 @@ pub fn process_execution_payload_bid ExecutionPayloadBidInvalid::BuilderNotActive(builder_index).into() ); + // Verify that the builder is a payload builder + block_verify!( + builder.version == PAYLOAD_BUILDER_VERSION, + ExecutionPayloadBidInvalid::InvalidBuilderVersion { + builder_index, + version: builder.version, + } + .into() + ); + // Verify that the builder has funds to cover the bid block_verify!( state.can_builder_cover_bid(builder_index, amount, spec)?, @@ -739,10 +753,10 @@ pub fn process_execution_payload_bid // Verify that the bid is for the current slot block_verify!( - bid.slot == block.slot(), + bid.slot == state.slot(), ExecutionPayloadBidInvalid::SlotMismatch { bid_slot: bid.slot, - block_slot: block.slot(), + state_slot: state.slot(), } .into() ); @@ -758,10 +772,11 @@ pub fn process_execution_payload_bid .into() ); + let expected_parent_root = *state.get_block_root(state.slot().safe_sub(1)?)?; block_verify!( - bid.parent_block_root == block.parent_root(), + bid.parent_block_root == expected_parent_root, ExecutionPayloadBidInvalid::ParentBlockRootMismatch { - block_parent_root: block.parent_root(), + block_parent_root: expected_parent_root, bid_parent_root: bid.parent_block_root, } .into() @@ -779,6 +794,7 @@ pub fn process_execution_payload_bid // Record the pending payment if there is some payment if amount > 0 { + let proposer_index = state.get_beacon_proposer_index(state.slot(), spec)? as u64; let pending_payment = BuilderPendingPayment { weight: 0, withdrawal: BuilderPendingWithdrawal { @@ -786,6 +802,7 @@ pub fn process_execution_payload_bid amount, builder_index, }, + proposer_index, }; let payment_index = E::SlotsPerEpoch::to_usize() diff --git a/consensus/state_processing/src/per_block_processing/errors.rs b/consensus/state_processing/src/per_block_processing/errors.rs index 93d668c8c9..2679cdf9d5 100644 --- a/consensus/state_processing/src/per_block_processing/errors.rs +++ b/consensus/state_processing/src/per_block_processing/errors.rs @@ -527,14 +527,16 @@ pub enum ExecutionPayloadBidInvalid { BadSignature, /// The builder is not active. BuilderNotActive(u64), + /// The builder's version is not `PAYLOAD_BUILDER_VERSION`. + InvalidBuilderVersion { builder_index: u64, version: u8 }, /// 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 }, + /// Bid slot doesn't match state slot + SlotMismatch { bid_slot: Slot, state_slot: Slot }, /// The bid's parent block hash doesn't match the state's latest block hash ParentBlockHashMismatch { state_block_hash: ExecutionBlockHash, 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 876e66d3af..8d9962c478 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -4,13 +4,9 @@ use crate::common::{ get_attestation_participation_flag_indices, increase_balance, initiate_validator_exit, slash_validator, }; -use crate::per_block_processing::builder::{ - convert_validator_index_to_builder_index, is_builder_index, -}; use crate::per_block_processing::errors::{BlockProcessingError, ExitInvalid, IntoWithIndex}; -use crate::per_block_processing::signature_sets::{exit_signature_set, get_pubkey_from_state}; use crate::per_block_processing::verify_payload_attestation::verify_payload_attestation; -use bls::{PublicKeyBytes, SignatureBytes}; +use bls::PublicKeyBytes; use ssz_types::FixedVector; use typenum::U33; use types::consts::altair::{PARTICIPATION_FLAG_WEIGHTS, PROPOSER_WEIGHT, WEIGHT_DENOMINATOR}; @@ -54,11 +50,7 @@ pub fn process_operations>( )?; } else if state.fork_name_unchecked().electra_enabled() { state.update_pubkey_cache()?; - process_deposit_requests_pre_gloas( - state, - &block_body.execution_requests()?.deposits, - spec, - )?; + process_deposit_requests(state, &block_body.execution_requests()?.deposits, spec)?; process_withdrawal_requests(state, &block_body.execution_requests()?.withdrawals, spec)?; process_consolidation_requests( state, @@ -397,7 +389,9 @@ pub fn process_proposer_slashings( // [New in Gloas:EIP7732] // Remove the BuilderPendingPayment corresponding to this proposal - // if it is still in the 2-epoch window. + // if it is still in the 2-epoch window. Only clear it when the slashed validator is + // the proposer associated with the payment; otherwise an unrelated same-slot + // equivocation could grief an honest proposer's payment. 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()); @@ -416,7 +410,12 @@ pub fn process_proposer_slashings( .builder_pending_payments_mut()? .get_mut(index) .ok_or(BlockProcessingError::BuilderPaymentIndexOutOfBounds(index))?; - *payment = BuilderPendingPayment::default(); + + if payment.proposer_index + == proposer_slashing.signed_header_1.message.proposer_index + { + *payment = BuilderPendingPayment::default(); + } } } @@ -521,15 +520,6 @@ pub fn process_exits( .into_with_index(i)); } - // [New in Gloas:EIP7732] - if state.fork_name_unchecked().gloas_enabled() - && is_builder_index(exit.message.validator_index) - { - process_builder_voluntary_exit(state, exit, verify_signatures, spec) - .map_err(|e| e.into_with_index(i))?; - continue; - } - verify_exit(state, Some(current_epoch), exit, verify_signatures, spec) .map_err(|e| e.into_with_index(i))?; @@ -538,59 +528,6 @@ pub fn process_exits( Ok(()) } -/// Process a builder voluntary exit. [New in Gloas:EIP7732] -fn process_builder_voluntary_exit( - state: &mut BeaconState, - signed_exit: &SignedVoluntaryExit, - verify_signatures: VerifySignatures, - spec: &ChainSpec, -) -> Result<(), BlockOperationError> { - let builder_index = - convert_validator_index_to_builder_index(signed_exit.message.validator_index); - - // Verify builder is known - state - .builders()? - .get(builder_index as usize) - .cloned() - .ok_or(BlockOperationError::invalid(ExitInvalid::ValidatorUnknown( - signed_exit.message.validator_index, - )))?; - - // Verify the builder is active - if !state.is_active_builder(builder_index, spec)? { - return Err(BlockOperationError::invalid(ExitInvalid::NotActive( - signed_exit.message.validator_index, - ))); - } - - // Only exit builder if it has no pending withdrawals in the queue - let pending_balance = state.get_pending_balance_to_withdraw_for_builder(builder_index)?; - if pending_balance != 0 { - return Err(BlockOperationError::invalid( - ExitInvalid::PendingWithdrawalInQueue(signed_exit.message.validator_index), - )); - } - - if verify_signatures.is_true() { - verify!( - exit_signature_set( - state, - |i| get_pubkey_from_state(state, i), - signed_exit, - spec - )? - .verify(), - ExitInvalid::BadSignature - ); - } - - // Initiate builder exit - initiate_builder_exit(state, builder_index, spec)?; - - Ok(()) -} - /// Initiate the exit of a builder. [New in Gloas:EIP7732] fn initiate_builder_exit( state: &mut BeaconState, @@ -874,7 +811,7 @@ pub fn process_withdrawal_requests( Ok(()) } -pub fn process_deposit_requests_pre_gloas( +pub fn process_deposit_requests( state: &mut BeaconState, deposit_requests: &[DepositRequest], spec: &ChainSpec, @@ -904,18 +841,111 @@ pub fn process_deposit_requests_pre_gloas( Ok(()) } -pub fn process_deposit_requests_post_gloas( +pub fn process_builder_deposit_requests( state: &mut BeaconState, - deposit_requests: &[DepositRequest], + builder_deposit_requests: &[BuilderDepositRequest], spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { - for request in deposit_requests { - process_deposit_request_post_gloas(state, request, spec)?; + for builder_deposit_request in builder_deposit_requests { + process_builder_deposit_request(state, builder_deposit_request, spec)?; } Ok(()) } +fn process_builder_deposit_request( + state: &mut BeaconState, + builder_deposit_request: &BuilderDepositRequest, + spec: &ChainSpec, +) -> Result<(), BlockProcessingError> { + let builder_index = state + .builders()? + .iter() + .position(|builder| builder.pubkey == builder_deposit_request.pubkey); + + match builder_index { + None => { + if builder_deposit_request.is_valid_builder_deposit_signature(spec) { + let version = builder_deposit_request + .version() + .ok_or(BeaconStateError::WithdrawalCredentialMissingVersion)?; + let slot = state.slot(); + state.add_builder_to_registry( + builder_deposit_request.pubkey, + version, + builder_deposit_request.withdrawal_credentials, + builder_deposit_request.amount, + slot, + spec, + )?; + } + } + Some(builder_index) => { + let current_epoch = state.current_epoch(); + let builder = state + .builders_mut()? + .get_mut(builder_index) + .ok_or(BeaconStateError::UnknownBuilder(builder_index as u64))?; + + // TODO(gloas): this is already different in `master`, needs an update when we go + // to spec 1.7.0-alpha.12+ + builder + .balance + .safe_add_assign(builder_deposit_request.amount)?; + + if builder.withdrawable_epoch != spec.far_future_epoch { + builder.withdrawable_epoch = + current_epoch.safe_add(spec.min_builder_withdrawability_delay)?; + } + } + } + + Ok(()) +} + +pub fn process_builder_exit_requests( + state: &mut BeaconState, + builder_exit_requests: &[BuilderExitRequest], + spec: &ChainSpec, +) -> Result<(), BlockProcessingError> { + for builder_exit_request in builder_exit_requests { + process_builder_exit_request(state, builder_exit_request, spec)?; + } + + Ok(()) +} + +fn process_builder_exit_request( + state: &mut BeaconState, + builder_exit_request: &BuilderExitRequest, + spec: &ChainSpec, +) -> Result<(), BlockProcessingError> { + let Some(builder_index) = state + .builders()? + .iter() + .position(|builder| builder.pubkey == builder_exit_request.pubkey) + .map(|i| i as u64) + else { + return Ok(()); + }; + + if !state.is_active_builder(builder_index, spec)? { + return Ok(()); + } + + if state.get_builder(builder_index)?.execution_address != builder_exit_request.source_address { + return Ok(()); + } + + if state.get_pending_balance_to_withdraw_for_builder(builder_index)? != 0 { + return Ok(()); + } + + initiate_builder_exit(state, builder_index, spec)?; + + Ok(()) +} + /// 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. @@ -939,107 +969,6 @@ pub fn is_pending_validator<'a>( }) } -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 has_builder_prefix = - is_builder_withdrawal_credential(deposit_request.withdrawal_credentials, spec); - - if is_builder - || (has_builder_prefix - && !is_validator - && !is_pending_validator(state.pending_deposits()?, &deposit_request.pubkey, spec)) - { - // 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, BeaconStateError> { - 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() { - let builder_index = state.add_builder_to_registry( - pubkey, - withdrawal_credentials, - amount, - slot, - spec, - )?; - Ok(Some(builder_index)) - } else { - Ok(None) - } - } - Some(builder_index) => { - state - .builders_mut()? - .get_mut(builder_index as usize) - .ok_or(BeaconStateError::UnknownBuilder(builder_index))? - .balance - .safe_add_assign(amount)?; - Ok(Some(builder_index)) - } - } -} - // 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/signature_sets.rs b/consensus/state_processing/src/per_block_processing/signature_sets.rs index f56cb17554..6f9305f08b 100644 --- a/consensus/state_processing/src/per_block_processing/signature_sets.rs +++ b/consensus/state_processing/src/per_block_processing/signature_sets.rs @@ -2,7 +2,6 @@ //! validated individually, or alongside in others in a potentially cheaper bulk operation. //! //! This module exposes one function to extract each type of `SignatureSet` from a `BeaconBlock`. -use super::builder::{convert_validator_index_to_builder_index, is_builder_index}; use bls::{AggregateSignature, PublicKey, PublicKeyBytes, Signature, SignatureSet}; use ssz::DecodeError; use std::borrow::Cow; @@ -520,7 +519,10 @@ pub fn deposit_pubkey_signature_message( } /// Returns a signature set that is valid if the `SignedVoluntaryExit` was signed by the indicated -/// validator (or builder, in the case of a builder exit). +/// validator. +/// +/// It is invalid for voluntary exits to be signed by builders. Builder exits are made via execution +/// requests per EIP-8282. pub fn exit_signature_set<'a, E, F>( state: &'a BeaconState, get_pubkey: F, @@ -534,16 +536,8 @@ where let exit = &signed_exit.message; let validator_index = exit.validator_index; - let is_builder_exit = - state.fork_name_unchecked().gloas_enabled() && is_builder_index(validator_index); - - let pubkey = if is_builder_exit { - let builder_index = convert_validator_index_to_builder_index(validator_index); - get_builder_pubkey_from_state(state, builder_index) - .ok_or(Error::ValidatorUnknown(validator_index))? - } else { - get_pubkey(validator_index as usize).ok_or(Error::ValidatorUnknown(validator_index))? - }; + let pubkey = + get_pubkey(validator_index as usize).ok_or(Error::ValidatorUnknown(validator_index))?; let domain = if state.fork_name_unchecked().deneb_enabled() { // EIP-7044 diff --git a/consensus/state_processing/src/upgrade/gloas.rs b/consensus/state_processing/src/upgrade/gloas.rs index c26547e304..6e32bd00bc 100644 --- a/consensus/state_processing/src/upgrade/gloas.rs +++ b/consensus/state_processing/src/upgrade/gloas.rs @@ -1,16 +1,15 @@ -use crate::per_block_processing::process_operations::apply_deposit_for_builder; +use crate::per_block_processing::is_valid_deposit_signature; 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::HashMap; -use std::mem; +use ssz_types::{BitVector, FixedVector}; +use std::{collections::HashMap, mem}; use tree_hash::TreeHash; use typenum::Unsigned; use types::{ BeaconState, BeaconStateError as Error, BeaconStateGloas, BuilderPendingPayment, ChainSpec, - EthSpec, ExecutionPayloadBid, ExecutionRequests, Fork, is_builder_withdrawal_credential, + DepositData, EthSpec, ExecutionPayloadBid, ExecutionRequestsGloas, Fork, + consts::gloas::PAYLOAD_BUILDER_VERSION, is_builder_withdrawal_credential, }; /// Transform a `Fulu` state into a `Gloas` state. @@ -79,7 +78,7 @@ pub fn upgrade_state_to_gloas( 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(), + execution_requests_root: ExecutionRequestsGloas::::default().tree_hash_root(), ..Default::default() }, // Capella @@ -187,37 +186,50 @@ fn onboard_builders_from_pending_deposits( continue; } - 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; + match builder_pubkey_to_index.get(&deposit.pubkey).copied() { + None => { + // Deposits without builder withdrawal credentials are for new validators. + if !is_builder_withdrawal_credential(deposit.withdrawal_credentials, spec) { + pending_deposits.push(deposit.clone())?; + 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; + } + + 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_err() { + continue; + } + + let builder_index = state.add_builder_to_registry( + deposit.pubkey, + PAYLOAD_BUILDER_VERSION, + deposit.withdrawal_credentials, + deposit.amount, + deposit.slot, + spec, + )?; + builder_pubkey_to_index.insert(deposit.pubkey, builder_index); } + Some(builder_index) => { + let builder = state + .builders_mut()? + .get_mut(builder_index as usize) + .ok_or(Error::UnknownBuilder(builder_index))?; - // 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; + builder.balance.safe_add_assign(deposit.amount)?; } } - - 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); - } } *state.pending_deposits_mut()? = pending_deposits; diff --git a/consensus/types/presets/gnosis/gloas.yaml b/consensus/types/presets/gnosis/gloas.yaml index d1a48adca1..1f290d7010 100644 --- a/consensus/types/presets/gnosis/gloas.yaml +++ b/consensus/types/presets/gnosis/gloas.yaml @@ -21,3 +21,10 @@ BUILDER_PENDING_WITHDRAWALS_LIMIT: 1048576 # --------------------------------------------------------------- # 2**14 (= 16,384) builders MAX_BUILDERS_PER_WITHDRAWALS_SWEEP: 16384 + +# Execution +# --------------------------------------------------------------- +# 2**8 (= 256) builder deposit requests +MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD: 256 +# 2**4 (= 16) builder exit requests +MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD: 16 diff --git a/consensus/types/presets/mainnet/gloas.yaml b/consensus/types/presets/mainnet/gloas.yaml index 2da198dcae..4bd7888c6c 100644 --- a/consensus/types/presets/mainnet/gloas.yaml +++ b/consensus/types/presets/mainnet/gloas.yaml @@ -20,4 +20,11 @@ BUILDER_PENDING_WITHDRAWALS_LIMIT: 1048576 # Withdrawals processing # --------------------------------------------------------------- # 2**14 (= 16,384) builders -MAX_BUILDERS_PER_WITHDRAWALS_SWEEP: 16384 \ No newline at end of file +MAX_BUILDERS_PER_WITHDRAWALS_SWEEP: 16384 + +# Execution +# --------------------------------------------------------------- +# 2**8 (= 256) builder deposit requests +MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD: 256 +# 2**4 (= 16) builder exit requests +MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD: 16 \ No newline at end of file diff --git a/consensus/types/presets/minimal/gloas.yaml b/consensus/types/presets/minimal/gloas.yaml index 559c2d46df..096afd53f4 100644 --- a/consensus/types/presets/minimal/gloas.yaml +++ b/consensus/types/presets/minimal/gloas.yaml @@ -21,3 +21,10 @@ BUILDER_PENDING_WITHDRAWALS_LIMIT: 1048576 # --------------------------------------------------------------- # [customized] 2**4 (= 16) builders MAX_BUILDERS_PER_WITHDRAWALS_SWEEP: 16 + +# Execution +# --------------------------------------------------------------- +# 2**8 (= 256) builder deposit requests +MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD: 256 +# 2**4 (= 16) builder exit requests +MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD: 16 diff --git a/consensus/types/src/block/beacon_block.rs b/consensus/types/src/block/beacon_block.rs index 639a89d7e6..a307c9bd95 100644 --- a/consensus/types/src/block/beacon_block.rs +++ b/consensus/types/src/block/beacon_block.rs @@ -25,8 +25,8 @@ use crate::{ core::{ChainSpec, Domain, Epoch, EthSpec, Graffiti, Hash256, SignedRoot, Slot}, deposit::{Deposit, DepositData}, execution::{ - AbstractExecPayload, BlindedPayload, Eth1Data, ExecutionPayload, ExecutionRequests, - FullPayload, + AbstractExecPayload, BlindedPayload, Eth1Data, ExecutionPayload, ExecutionRequestsElectra, + ExecutionRequestsGloas, FullPayload, }, exit::{SignedVoluntaryExit, VoluntaryExit}, fork::{Fork, ForkName, InconsistentFork, map_fork_name}, @@ -650,7 +650,7 @@ impl> EmptyBlock for BeaconBlockElec execution_payload: Payload::Electra::default(), bls_to_execution_changes: VariableList::empty(), blob_kzg_commitments: VariableList::empty(), - execution_requests: ExecutionRequests::default(), + execution_requests: ExecutionRequestsElectra::default(), }, } } @@ -684,7 +684,7 @@ impl> EmptyBlock for BeaconBlockFulu execution_payload: Payload::Fulu::default(), bls_to_execution_changes: VariableList::empty(), blob_kzg_commitments: VariableList::empty(), - execution_requests: ExecutionRequests::default(), + execution_requests: ExecutionRequestsElectra::default(), }, } } @@ -713,7 +713,7 @@ impl> EmptyBlock for BeaconBlockGloa voluntary_exits: VariableList::empty(), sync_aggregate: SyncAggregate::empty(), bls_to_execution_changes: VariableList::empty(), - parent_execution_requests: ExecutionRequests::default(), + parent_execution_requests: ExecutionRequestsGloas::default(), signed_execution_payload_bid: SignedExecutionPayloadBid::empty(), payload_attestations: VariableList::empty(), _phantom: PhantomData, diff --git a/consensus/types/src/block/beacon_block_body.rs b/consensus/types/src/block/beacon_block_body.rs index 071c9e76d4..f8e6ad6ae7 100644 --- a/consensus/types/src/block/beacon_block_body.rs +++ b/consensus/types/src/block/beacon_block_body.rs @@ -24,9 +24,10 @@ use crate::{ AbstractExecPayload, BlindedPayload, BlindedPayloadBellatrix, BlindedPayloadCapella, BlindedPayloadDeneb, BlindedPayloadElectra, BlindedPayloadFulu, Eth1Data, ExecutionPayload, ExecutionPayloadBellatrix, ExecutionPayloadCapella, ExecutionPayloadDeneb, - ExecutionPayloadElectra, ExecutionPayloadFulu, ExecutionPayloadGloas, ExecutionRequests, - FullPayload, FullPayloadBellatrix, FullPayloadCapella, FullPayloadDeneb, - FullPayloadElectra, FullPayloadFulu, SignedBlsToExecutionChange, + ExecutionPayloadElectra, ExecutionPayloadFulu, ExecutionPayloadGloas, + ExecutionRequestsElectra, ExecutionRequestsGloas, FullPayload, FullPayloadBellatrix, + FullPayloadCapella, FullPayloadDeneb, FullPayloadElectra, FullPayloadFulu, + SignedBlsToExecutionChange, }, exit::SignedVoluntaryExit, fork::{ForkName, map_fork_name}, @@ -163,13 +164,13 @@ pub struct BeaconBlockBody = FullPay #[superstruct(only(Deneb, Electra, Fulu))] pub blob_kzg_commitments: KzgCommitments, #[superstruct(only(Electra, Fulu))] - pub execution_requests: ExecutionRequests, + pub execution_requests: ExecutionRequestsElectra, #[superstruct(only(Gloas))] pub signed_execution_payload_bid: SignedExecutionPayloadBid, #[superstruct(only(Gloas))] pub payload_attestations: VariableList, E::MaxPayloadAttestations>, #[superstruct(only(Gloas))] - pub parent_execution_requests: ExecutionRequests, + pub parent_execution_requests: ExecutionRequestsGloas, #[superstruct(only(Base, Altair, Gloas))] #[metastruct(exclude_from(fields))] #[ssz(skip_serializing, skip_deserializing)] diff --git a/consensus/types/src/builder/builder_bid.rs b/consensus/types/src/builder/builder_bid.rs index df7893b909..ddd877c46f 100644 --- a/consensus/types/src/builder/builder_bid.rs +++ b/consensus/types/src/builder/builder_bid.rs @@ -12,7 +12,7 @@ use crate::{ execution::{ ExecutionPayloadHeaderBellatrix, ExecutionPayloadHeaderCapella, ExecutionPayloadHeaderDeneb, ExecutionPayloadHeaderElectra, ExecutionPayloadHeaderFulu, - ExecutionPayloadHeaderRef, ExecutionPayloadHeaderRefMut, ExecutionRequests, + ExecutionPayloadHeaderRef, ExecutionPayloadHeaderRefMut, ExecutionRequestsElectra, }, fork::{ForkName, ForkVersionDecode}, kzg_ext::KzgCommitments, @@ -59,7 +59,7 @@ pub struct BuilderBid { #[superstruct(only(Deneb, Electra, Fulu))] pub blob_kzg_commitments: KzgCommitments, #[superstruct(only(Electra, Fulu))] - pub execution_requests: ExecutionRequests, + pub execution_requests: ExecutionRequestsElectra, #[serde(with = "serde_utils::quoted_u256")] pub value: Uint256, pub pubkey: PublicKeyBytes, diff --git a/consensus/types/src/builder/builder_deposit_request.rs b/consensus/types/src/builder/builder_deposit_request.rs new file mode 100644 index 0000000000..f57845750d --- /dev/null +++ b/consensus/types/src/builder/builder_deposit_request.rs @@ -0,0 +1,71 @@ +use bls::{PublicKeyBytes, SignatureBytes}; +use context_deserialize::context_deserialize; +use serde::{Deserialize, Serialize}; +use ssz::Encode; +use ssz_derive::{Decode, Encode}; +use tree_hash_derive::TreeHash; + +use crate::{ChainSpec, DepositMessage, Domain, SignedRoot, core::Hash256, fork::ForkName}; + +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Debug, PartialEq, Hash, Clone, Serialize, Deserialize, Encode, Decode, TreeHash)] +#[context_deserialize(ForkName)] +pub struct BuilderDepositRequest { + pub pubkey: PublicKeyBytes, + pub withdrawal_credentials: Hash256, + #[serde(with = "serde_utils::quoted_u64")] + pub amount: u64, + pub signature: SignatureBytes, +} + +impl BuilderDepositRequest { + fn as_deposit_message(&self) -> DepositMessage { + DepositMessage { + pubkey: self.pubkey, + withdrawal_credentials: self.withdrawal_credentials, + amount: self.amount, + } + } + + pub fn version(&self) -> Option { + self.withdrawal_credentials.as_slice().first().cloned() + } + + pub fn is_valid_builder_deposit_signature(&self, spec: &ChainSpec) -> bool { + let Ok(pubkey) = self.pubkey.decompress() else { + return false; + }; + let Ok(signature) = self.signature.decompress() else { + return false; + }; + + let domain = spec.compute_domain( + Domain::BuilderDeposit, + spec.genesis_fork_version, + Hash256::ZERO, + ); + let signing_root = self.as_deposit_message().signing_root(domain); + + signature.verify(&pubkey, signing_root) + } +} + +impl BuilderDepositRequest { + pub fn max_size() -> usize { + Self { + pubkey: PublicKeyBytes::empty(), + withdrawal_credentials: Hash256::ZERO, + amount: 0, + signature: SignatureBytes::empty(), + } + .as_ssz_bytes() + .len() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + ssz_and_tree_hash_tests!(BuilderDepositRequest); +} diff --git a/consensus/types/src/builder/builder_exit_request.rs b/consensus/types/src/builder/builder_exit_request.rs new file mode 100644 index 0000000000..667ff8a618 --- /dev/null +++ b/consensus/types/src/builder/builder_exit_request.rs @@ -0,0 +1,35 @@ +use bls::PublicKeyBytes; +use context_deserialize::context_deserialize; +use serde::{Deserialize, Serialize}; +use ssz::Encode; +use ssz_derive::{Decode, Encode}; +use tree_hash_derive::TreeHash; + +use crate::{core::Address, fork::ForkName}; + +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Encode, Decode, TreeHash)] +#[context_deserialize(ForkName)] +pub struct BuilderExitRequest { + #[serde(with = "serde_utils::address_hex")] + pub source_address: Address, + pub pubkey: PublicKeyBytes, +} + +impl BuilderExitRequest { + pub fn max_size() -> usize { + Self { + source_address: Address::repeat_byte(0), + pubkey: PublicKeyBytes::empty(), + } + .as_ssz_bytes() + .len() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + ssz_and_tree_hash_tests!(BuilderExitRequest); +} diff --git a/consensus/types/src/builder/builder_pending_payment.rs b/consensus/types/src/builder/builder_pending_payment.rs index 61c76dfc15..bc2cabc6bc 100644 --- a/consensus/types/src/builder/builder_pending_payment.rs +++ b/consensus/types/src/builder/builder_pending_payment.rs @@ -13,6 +13,8 @@ pub struct BuilderPendingPayment { #[serde(with = "serde_utils::quoted_u64")] pub weight: u64, pub withdrawal: BuilderPendingWithdrawal, + #[serde(with = "serde_utils::quoted_u64")] + pub proposer_index: u64, } #[cfg(test)] diff --git a/consensus/types/src/builder/mod.rs b/consensus/types/src/builder/mod.rs index bc6140f9d9..e9325939bb 100644 --- a/consensus/types/src/builder/mod.rs +++ b/consensus/types/src/builder/mod.rs @@ -1,5 +1,7 @@ mod builder; mod builder_bid; +mod builder_deposit_request; +mod builder_exit_request; mod builder_pending_payment; mod builder_pending_withdrawal; mod proposer_preferences; @@ -9,6 +11,8 @@ pub use builder_bid::{ BuilderBid, BuilderBidBellatrix, BuilderBidCapella, BuilderBidDeneb, BuilderBidElectra, BuilderBidFulu, SignedBuilderBid, }; +pub use builder_deposit_request::BuilderDepositRequest; +pub use builder_exit_request::BuilderExitRequest; pub use builder_pending_payment::BuilderPendingPayment; pub use builder_pending_withdrawal::BuilderPendingWithdrawal; pub use proposer_preferences::{ProposerPreferences, SignedProposerPreferences}; diff --git a/consensus/types/src/core/chain_spec.rs b/consensus/types/src/core/chain_spec.rs index 9ccaa86579..73163060e3 100644 --- a/consensus/types/src/core/chain_spec.rs +++ b/consensus/types/src/core/chain_spec.rs @@ -37,6 +37,7 @@ pub enum Domain { BeaconBuilder, PTCAttester, ProposerPreferences, + BuilderDeposit, ApplicationMask(ApplicationDomain), } @@ -147,6 +148,7 @@ pub struct ChainSpec { pub(crate) domain_beacon_builder: u32, pub(crate) domain_ptc_attester: u32, pub(crate) domain_proposer_preferences: u32, + pub(crate) domain_builder_deposit: u32, /* * Fork choice @@ -525,6 +527,7 @@ impl ChainSpec { Domain::BeaconBuilder => self.domain_beacon_builder, Domain::PTCAttester => self.domain_ptc_attester, Domain::ProposerPreferences => self.domain_proposer_preferences, + Domain::BuilderDeposit => self.domain_builder_deposit, Domain::SyncCommittee => self.domain_sync_committee, Domain::ContributionAndProof => self.domain_contribution_and_proof, Domain::SyncCommitteeSelectionProof => self.domain_sync_committee_selection_proof, @@ -1158,6 +1161,7 @@ impl ChainSpec { domain_beacon_builder: 0x0B, domain_ptc_attester: 0x0C, domain_proposer_preferences: 0x0D, + domain_builder_deposit: 0x0E, /* * Fork choice @@ -1583,6 +1587,7 @@ impl ChainSpec { domain_beacon_builder: 0x0B, domain_ptc_attester: 0x0C, domain_proposer_preferences: 0x0D, + domain_builder_deposit: 0x0E, /* * Fork choice @@ -2982,6 +2987,12 @@ mod tests { test_domain(Domain::SyncCommittee, spec.domain_sync_committee, &spec); test_domain(Domain::BeaconBuilder, spec.domain_beacon_builder, &spec); test_domain(Domain::PTCAttester, spec.domain_ptc_attester, &spec); + test_domain( + Domain::ProposerPreferences, + spec.domain_proposer_preferences, + &spec, + ); + test_domain(Domain::BuilderDeposit, spec.domain_builder_deposit, &spec); // The builder domain index is zero let builder_domain_pre_mask = [0; 4]; diff --git a/consensus/types/src/core/config_and_preset.rs b/consensus/types/src/core/config_and_preset.rs index 02f9867fcb..8538e6e609 100644 --- a/consensus/types/src/core/config_and_preset.rs +++ b/consensus/types/src/core/config_and_preset.rs @@ -136,6 +136,7 @@ pub fn get_extra_fields(spec: &ChainSpec) -> HashMap { "domain_beacon_builder".to_uppercase() => u32_hex(spec.domain_beacon_builder), "domain_ptc_attester".to_uppercase() => u32_hex(spec.domain_ptc_attester), "domain_proposer_preferences".to_uppercase() => u32_hex(spec.domain_proposer_preferences), + "domain_builder_deposit".to_uppercase() => u32_hex(spec.domain_builder_deposit), "sync_committee_subnet_count".to_uppercase() => consts::altair::SYNC_COMMITTEE_SUBNET_COUNT.to_string().into(), "target_aggregators_per_sync_subcommittee".to_uppercase() => diff --git a/consensus/types/src/core/consts.rs b/consensus/types/src/core/consts.rs index 049094da76..66de387b01 100644 --- a/consensus/types/src/core/consts.rs +++ b/consensus/types/src/core/consts.rs @@ -29,6 +29,9 @@ pub mod gloas { pub const BUILDER_INDEX_SELF_BUILD: u64 = u64::MAX; pub const BUILDER_INDEX_FLAG: u64 = 1 << 40; + /// Version for an execution payload builder. + pub const PAYLOAD_BUILDER_VERSION: u8 = 0; + // Fork choice constants pub type PayloadStatus = u8; pub const PAYLOAD_STATUS_EMPTY: PayloadStatus = 0; diff --git a/consensus/types/src/core/eth_spec.rs b/consensus/types/src/core/eth_spec.rs index 5f296afb44..9d79470647 100644 --- a/consensus/types/src/core/eth_spec.rs +++ b/consensus/types/src/core/eth_spec.rs @@ -181,6 +181,8 @@ pub trait EthSpec: 'static + Default + Sync + Send + Clone + Debug + PartialEq + type BuilderPendingPaymentsLimit: Unsigned + Clone + Sync + Send + Debug + PartialEq; type BuilderPendingWithdrawalsLimit: Unsigned + Clone + Sync + Send + Debug + PartialEq; type MaxBuildersPerWithdrawalsSweep: Unsigned + Clone + Sync + Send + Debug + PartialEq; + type MaxBuilderDepositRequestsPerPayload: Unsigned + Clone + Sync + Send + Debug + PartialEq; + type MaxBuilderExitRequestsPerPayload: Unsigned + Clone + Sync + Send + Debug + PartialEq; fn default_spec() -> ChainSpec; @@ -444,6 +446,16 @@ pub trait EthSpec: 'static + Default + Sync + Send + Clone + Debug + PartialEq + Self::MaxBuildersPerWithdrawalsSweep::to_usize() } + /// Returns the `MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD` constant for this specification. + fn max_builder_deposit_requests_per_payload() -> usize { + Self::MaxBuilderDepositRequestsPerPayload::to_usize() + } + + /// Returns the `MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD` constant for this specification. + fn max_builder_exit_requests_per_payload() -> usize { + Self::MaxBuilderExitRequestsPerPayload::to_usize() + } + /// Returns the `PAYLOAD_TIMELY_THRESHOLD` constant (PTC_SIZE / 2). fn payload_timely_threshold() -> usize { Self::PTCSize::to_usize() / 2 @@ -529,6 +541,8 @@ impl EthSpec for MainnetEthSpec { type PtcWindowLength = U96; // (2 + MIN_SEED_LOOKAHEAD) * SLOTS_PER_EPOCH type MaxPayloadAttestations = U4; type MaxBuildersPerWithdrawalsSweep = U16384; + type MaxBuilderDepositRequestsPerPayload = U256; + type MaxBuilderExitRequestsPerPayload = U16; fn default_spec() -> ChainSpec { ChainSpec::mainnet() @@ -606,6 +620,8 @@ impl EthSpec for MinimalEthSpec { MaxDepositRequestsPerPayload, MaxWithdrawalRequestsPerPayload, MaxPayloadAttestations, + MaxBuilderDepositRequestsPerPayload, + MaxBuilderExitRequestsPerPayload, BuilderRegistryLimit }); @@ -684,6 +700,8 @@ impl EthSpec for GnosisEthSpec { type PtcWindowLength = U48; // (2 + MIN_SEED_LOOKAHEAD) * SLOTS_PER_EPOCH type MaxPayloadAttestations = U2; type MaxBuildersPerWithdrawalsSweep = U16384; + type MaxBuilderDepositRequestsPerPayload = U256; + type MaxBuilderExitRequestsPerPayload = U16; fn default_spec() -> ChainSpec { ChainSpec::gnosis() diff --git a/consensus/types/src/core/preset.rs b/consensus/types/src/core/preset.rs index 978fc6f4a1..d9043a5d77 100644 --- a/consensus/types/src/core/preset.rs +++ b/consensus/types/src/core/preset.rs @@ -342,6 +342,10 @@ pub struct GloasPreset { pub builder_pending_withdrawals_limit: u64, #[serde(with = "serde_utils::quoted_u64")] pub max_builders_per_withdrawals_sweep: u64, + #[serde(with = "serde_utils::quoted_u64")] + pub max_builder_deposit_requests_per_payload: u64, + #[serde(with = "serde_utils::quoted_u64")] + pub max_builder_exit_requests_per_payload: u64, } impl GloasPreset { @@ -352,6 +356,10 @@ impl GloasPreset { builder_registry_limit: E::BuilderRegistryLimit::to_u64(), builder_pending_withdrawals_limit: E::builder_pending_withdrawals_limit() as u64, max_builders_per_withdrawals_sweep: E::max_builders_per_withdrawals_sweep() as u64, + max_builder_deposit_requests_per_payload: E::max_builder_deposit_requests_per_payload() + as u64, + max_builder_exit_requests_per_payload: E::max_builder_exit_requests_per_payload() + as u64, } } } diff --git a/consensus/types/src/execution/execution_payload_envelope.rs b/consensus/types/src/execution/execution_payload_envelope.rs index 87a0ea7a63..c24665f41a 100644 --- a/consensus/types/src/execution/execution_payload_envelope.rs +++ b/consensus/types/src/execution/execution_payload_envelope.rs @@ -1,4 +1,4 @@ -use crate::execution::{ExecutionPayloadGloas, ExecutionRequests}; +use crate::execution::{ExecutionPayloadGloas, ExecutionRequestsGloas}; use crate::{EthSpec, ForkName, Hash256, SignedRoot, Slot}; use context_deserialize::context_deserialize; use educe::Educe; @@ -19,7 +19,7 @@ use tree_hash_derive::TreeHash; #[serde(bound = "E: EthSpec")] pub struct ExecutionPayloadEnvelope { pub payload: ExecutionPayloadGloas, - pub execution_requests: ExecutionRequests, + pub execution_requests: ExecutionRequestsGloas, #[serde(with = "serde_utils::quoted_u64")] pub builder_index: u64, pub beacon_block_root: Hash256, @@ -31,7 +31,7 @@ impl ExecutionPayloadEnvelope { pub fn empty() -> Self { Self { payload: ExecutionPayloadGloas::default(), - execution_requests: ExecutionRequests::default(), + execution_requests: ExecutionRequestsGloas::default(), builder_index: 0, beacon_block_root: Hash256::zero(), parent_beacon_block_root: Hash256::zero(), diff --git a/consensus/types/src/execution/execution_requests.rs b/consensus/types/src/execution/execution_requests.rs index 218b7edc17..dc14b71cd5 100644 --- a/consensus/types/src/execution/execution_requests.rs +++ b/consensus/types/src/execution/execution_requests.rs @@ -6,15 +6,19 @@ use serde::{Deserialize, Serialize}; use ssz::Encode; use ssz_derive::{Decode, Encode}; use ssz_types::VariableList; +use superstruct::superstruct; use tree_hash_derive::TreeHash; use crate::{ + builder::{BuilderDepositRequest, BuilderExitRequest}, consolidation::ConsolidationRequest, core::{EthSpec, Hash256}, deposit::DepositRequest, - fork::ForkName, + fork::{ForkName, ForkVersionDecode}, + state::BeaconStateError, withdrawal::WithdrawalRequest, }; +use ssz::Decode; pub type DepositRequests = VariableList::MaxDepositRequestsPerPayload>; @@ -22,46 +26,106 @@ pub type WithdrawalRequests = VariableList::MaxWithdrawalRequestsPerPayload>; pub type ConsolidationRequests = VariableList::MaxConsolidationRequestsPerPayload>; +pub type BuilderDepositRequests = + VariableList::MaxBuilderDepositRequestsPerPayload>; +pub type BuilderExitRequests = + VariableList::MaxBuilderExitRequestsPerPayload>; +#[superstruct( + variants(Electra, Gloas), + variant_attributes( + derive( + Default, + Debug, + Clone, + Serialize, + Deserialize, + Encode, + Decode, + TreeHash, + Educe, + ), + context_deserialize(ForkName), + educe(PartialEq, Eq, Hash(bound(E: EthSpec))), + serde(bound = "E: EthSpec"), + cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec"), + ), + ), + cast_error( + ty = "BeaconStateError", + expr = "BeaconStateError::IncorrectStateVariant" + ), + partial_getter_error( + ty = "BeaconStateError", + expr = "BeaconStateError::IncorrectStateVariant" + ) +)] #[cfg_attr( feature = "arbitrary", derive(arbitrary::Arbitrary), arbitrary(bound = "E: EthSpec") )] -#[derive(Debug, Educe, Default, Clone, Serialize, Deserialize, Encode, Decode, TreeHash)] -#[serde(bound = "E: EthSpec")] +#[derive(Debug, Educe, Clone, Serialize, Deserialize, Encode, TreeHash)] +#[serde(bound = "E: EthSpec", untagged)] #[educe(PartialEq, Eq, Hash(bound(E: EthSpec)))] -#[context_deserialize(ForkName)] +#[tree_hash(enum_behaviour = "transparent")] +#[ssz(enum_behaviour = "transparent")] pub struct ExecutionRequests { pub deposits: DepositRequests, pub withdrawals: WithdrawalRequests, pub consolidations: ConsolidationRequests, + #[superstruct(only(Gloas))] + pub builder_deposits: BuilderDepositRequests, + #[superstruct(only(Gloas))] + pub builder_exits: BuilderExitRequests, } -impl ExecutionRequests { +impl<'a, E: EthSpec> ExecutionRequestsRef<'a, E> { /// Returns the encoding according to EIP-7685 to send /// to the execution layer over the engine api. pub fn get_execution_requests_list(&self) -> Vec { let mut requests_list = Vec::new(); - if !self.deposits.is_empty() { + if !self.deposits().is_empty() { requests_list.push(Bytes::from_iter( [RequestType::Deposit.to_u8()] .into_iter() - .chain(self.deposits.as_ssz_bytes()), + .chain(self.deposits().as_ssz_bytes()), )); } - if !self.withdrawals.is_empty() { + if !self.withdrawals().is_empty() { requests_list.push(Bytes::from_iter( [RequestType::Withdrawal.to_u8()] .into_iter() - .chain(self.withdrawals.as_ssz_bytes()), + .chain(self.withdrawals().as_ssz_bytes()), )); } - if !self.consolidations.is_empty() { + if !self.consolidations().is_empty() { requests_list.push(Bytes::from_iter( [RequestType::Consolidation.to_u8()] .into_iter() - .chain(self.consolidations.as_ssz_bytes()), + .chain(self.consolidations().as_ssz_bytes()), + )); + } + // [New in Gloas:EIP8282] The builder request lists are only present on the Gloas variant. + if let Ok(builder_deposits) = self.builder_deposits() + && !builder_deposits.is_empty() + { + requests_list.push(Bytes::from_iter( + [RequestType::BuilderDeposit.to_u8()] + .into_iter() + .chain(builder_deposits.as_ssz_bytes()), + )); + } + if let Ok(builder_exits) = self.builder_exits() + && !builder_exits.is_empty() + { + requests_list.push(Bytes::from_iter( + [RequestType::BuilderExit.to_u8()] + .into_iter() + .chain(builder_exits.as_ssz_bytes()), )); } requests_list @@ -85,12 +149,46 @@ impl ExecutionRequests { } } +impl ExecutionRequests { + /// Returns the encoding according to EIP-7685 to send + /// to the execution layer over the engine api. + pub fn get_execution_requests_list(&self) -> Vec { + self.to_ref().get_execution_requests_list() + } + + /// Generate the execution layer `requests_hash` based on EIP-7685. + pub fn requests_hash(&self) -> Hash256 { + self.to_ref().requests_hash() + } +} + +impl ForkVersionDecode for ExecutionRequests { + /// SSZ decode with explicit fork variant. + fn from_ssz_bytes_by_fork(bytes: &[u8], fork_name: ForkName) -> Result { + match fork_name { + ForkName::Base + | ForkName::Altair + | ForkName::Bellatrix + | ForkName::Capella + | ForkName::Deneb => Err(ssz::DecodeError::BytesInvalid(format!( + "unsupported fork for ExecutionRequests: {fork_name}", + ))), + ForkName::Electra | ForkName::Fulu => { + ExecutionRequestsElectra::from_ssz_bytes(bytes).map(Self::Electra) + } + ForkName::Gloas => ExecutionRequestsGloas::from_ssz_bytes(bytes).map(Self::Gloas), + } + } +} + /// The prefix types for `ExecutionRequest` objects. #[derive(Debug, Copy, Clone)] pub enum RequestType { Deposit, Withdrawal, Consolidation, + BuilderDeposit, + BuilderExit, } impl RequestType { @@ -99,6 +197,8 @@ impl RequestType { 0 => Some(Self::Deposit), 1 => Some(Self::Withdrawal), 2 => Some(Self::Consolidation), + 3 => Some(Self::BuilderDeposit), + 4 => Some(Self::BuilderExit), _ => None, } } @@ -107,15 +207,24 @@ impl RequestType { Self::Deposit => 0, Self::Withdrawal => 1, Self::Consolidation => 2, + Self::BuilderDeposit => 3, + Self::BuilderExit => 4, } } } #[cfg(test)] -mod tests { +mod electra_tests { + use super::*; use crate::MainnetEthSpec; - use super::*; - - ssz_and_tree_hash_tests!(ExecutionRequests); + ssz_and_tree_hash_tests!(ExecutionRequestsElectra); +} + +#[cfg(test)] +mod gloas_tests { + use super::*; + use crate::MainnetEthSpec; + + ssz_and_tree_hash_tests!(ExecutionRequestsGloas); } diff --git a/consensus/types/src/execution/mod.rs b/consensus/types/src/execution/mod.rs index a3d4ed8730..befc27ceab 100644 --- a/consensus/types/src/execution/mod.rs +++ b/consensus/types/src/execution/mod.rs @@ -29,7 +29,9 @@ pub use execution_payload_header::{ ExecutionPayloadHeaderRef, ExecutionPayloadHeaderRefMut, }; pub use execution_requests::{ - ConsolidationRequests, DepositRequests, ExecutionRequests, RequestType, WithdrawalRequests, + BuilderDepositRequests, BuilderExitRequests, ConsolidationRequests, DepositRequests, + ExecutionRequests, ExecutionRequestsElectra, ExecutionRequestsGloas, ExecutionRequestsRef, + RequestType, WithdrawalRequests, }; pub use payload::{ AbstractExecPayload, BlindedPayload, BlindedPayloadBellatrix, BlindedPayloadCapella, diff --git a/consensus/types/src/state/beacon_state.rs b/consensus/types/src/state/beacon_state.rs index 26f28eda45..d25d3fc150 100644 --- a/consensus/types/src/state/beacon_state.rs +++ b/consensus/types/src/state/beacon_state.rs @@ -2061,6 +2061,7 @@ impl BeaconState { pub fn add_builder_to_registry( &mut self, pubkey: PublicKeyBytes, + version: u8, withdrawal_credentials: Hash256, amount: u64, slot: Slot, @@ -2072,10 +2073,6 @@ impl BeaconState { 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..) diff --git a/testing/ef_tests/Makefile b/testing/ef_tests/Makefile index a497fdaeff..1af7ca6433 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.10 +CONSENSUS_SPECS_TEST_VERSION ?= v1.7.0-alpha.11 REPO_NAME := consensus-spec-tests OUTPUT_DIR := ./$(REPO_NAME) diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index d851427a95..81c8b7bc12 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -8,8 +8,8 @@ use state_processing::common::update_progressive_balances_cache::initialize_prog use state_processing::envelope_processing::verify_execution_payload_envelope; use state_processing::epoch_cache::initialize_epoch_cache; use state_processing::per_block_processing::process_operations::{ - process_consolidation_requests, process_deposit_requests_post_gloas, - process_deposit_requests_pre_gloas, process_withdrawal_requests, + process_builder_deposit_requests, process_builder_exit_requests, + process_consolidation_requests, process_deposit_requests, process_withdrawal_requests, }; use state_processing::{ ConsensusContext, @@ -30,10 +30,10 @@ use std::fmt::Debug; use types::{ Attestation, AttesterSlashing, BeaconBlock, BeaconBlockBody, BeaconBlockBodyBellatrix, BeaconBlockBodyCapella, BeaconBlockBodyDeneb, BeaconBlockBodyElectra, BeaconBlockBodyFulu, - BeaconState, BlindedPayload, ConsolidationRequest, Deposit, DepositRequest, ExecutionPayload, - ForkVersionDecode, FullPayload, PayloadAttestation, ProposerSlashing, - SignedBlsToExecutionChange, SignedExecutionPayloadEnvelope, SignedVoluntaryExit, SyncAggregate, - WithdrawalRequest, + BeaconState, BlindedPayload, BuilderDepositRequest, BuilderExitRequest, ConsolidationRequest, + Deposit, DepositRequest, ExecutionPayload, ForkVersionDecode, FullPayload, PayloadAttestation, + ProposerSlashing, SignedBlsToExecutionChange, SignedExecutionPayloadBid, + SignedExecutionPayloadEnvelope, SignedVoluntaryExit, SyncAggregate, WithdrawalRequest, }; #[derive(Debug, Clone, Default, Deserialize)] @@ -65,7 +65,7 @@ pub struct VoluntaryExitChurn { /// Newtype for testing execution payload bids. #[derive(Debug, Clone, Deserialize)] pub struct ExecutionPayloadBidBlock { - block: BeaconBlock, + signed_bid: SignedExecutionPayloadBid, } /// Newtype for testing parent execution payload processing. @@ -538,16 +538,15 @@ impl Operation for ExecutionPayloadBidBlock { } fn filename() -> String { - "block.ssz_snappy".into() + "execution_payload_bid.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 { - ssz_decode_file_with(path, |bytes| BeaconBlock::from_ssz_bytes(bytes, spec)) - .map(|block| ExecutionPayloadBidBlock { block }) + fn decode(path: &Path, _fork_name: ForkName, _spec: &ChainSpec) -> Result { + ssz_decode_file(path).map(|signed_bid| ExecutionPayloadBidBlock { signed_bid }) } fn apply_to( @@ -556,7 +555,7 @@ impl Operation for ExecutionPayloadBidBlock { spec: &ChainSpec, _: &Operations, ) -> Result<(), BlockProcessingError> { - process_execution_payload_bid(state, self.block.to_ref(), VerifySignatures::True, spec)?; + process_execution_payload_bid(state, &self.signed_bid, VerifySignatures::True, spec)?; Ok(()) } } @@ -720,11 +719,7 @@ impl Operation for DepositRequest { spec: &ChainSpec, _extra: &Operations, ) -> Result<(), BlockProcessingError> { - 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) - } + process_deposit_requests(state, std::slice::from_ref(self), spec) } } @@ -754,6 +749,56 @@ impl Operation for ConsolidationRequest { } } +impl Operation for BuilderDepositRequest { + type Error = BlockProcessingError; + + fn handler_name() -> String { + "builder_deposit_request".into() + } + + fn is_enabled_for_fork(fork_name: ForkName) -> bool { + fork_name.gloas_enabled() + } + + fn decode(path: &Path, _fork_name: ForkName, _spec: &ChainSpec) -> Result { + ssz_decode_file(path) + } + + fn apply_to( + &self, + state: &mut BeaconState, + spec: &ChainSpec, + _extra: &Operations, + ) -> Result<(), BlockProcessingError> { + process_builder_deposit_requests(state, std::slice::from_ref(self), spec) + } +} + +impl Operation for BuilderExitRequest { + type Error = BlockProcessingError; + + fn handler_name() -> String { + "builder_exit_request".into() + } + + fn is_enabled_for_fork(fork_name: ForkName) -> bool { + fork_name.gloas_enabled() + } + + fn decode(path: &Path, _fork_name: ForkName, _spec: &ChainSpec) -> Result { + ssz_decode_file(path) + } + + fn apply_to( + &self, + state: &mut BeaconState, + spec: &ChainSpec, + _extra: &Operations, + ) -> Result<(), BlockProcessingError> { + process_builder_exit_requests(state, std::slice::from_ref(self), spec) + } +} + impl Operation for PayloadAttestation { type Error = BlockProcessingError; diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index b45ea3a230..a438fcc91c 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -718,12 +718,15 @@ impl Handler for ForkChoiceHandler { return false; } - // on_attestation, on_execution_payload_envelope, get_parent_payload_status, and - // on_payload_attestation_message tests exist only for Gloas and later. + // on_attestation, on_execution_payload_envelope, get_parent_payload_status, + // on_payload_attestation_message, payload_timeliness, and payload_data_availability + // tests exist only for Gloas and later. if (self.handler_name == "on_attestation" || self.handler_name == "on_execution_payload_envelope" || self.handler_name == "get_parent_payload_status" - || self.handler_name == "on_payload_attestation_message") + || self.handler_name == "on_payload_attestation_message" + || self.handler_name == "payload_timeliness" + || self.handler_name == "payload_data_availability") && !fork_name.gloas_enabled() { return false; diff --git a/testing/ef_tests/src/type_name.rs b/testing/ef_tests/src/type_name.rs index 18666befaa..199de1d5ab 100644 --- a/testing/ef_tests/src/type_name.rs +++ b/testing/ef_tests/src/type_name.rs @@ -73,6 +73,8 @@ type_name!(DepositMessage); type_name!(DepositRequest); type_name!(Eth1Data); type_name!(Builder); +type_name!(BuilderDepositRequest); +type_name!(BuilderExitRequest); type_name!(BuilderPendingPayment); type_name!(BuilderPendingWithdrawal); type_name!(WithdrawalRequest); @@ -92,7 +94,8 @@ type_name_generic!(ExecutionPayloadHeaderElectra, "ExecutionPayloadHeader"); type_name_generic!(ExecutionPayloadHeaderFulu, "ExecutionPayloadHeader"); type_name_generic!(ExecutionPayloadBid); type_name_generic!(SignedExecutionPayloadBid); -type_name_generic!(ExecutionRequests); +type_name_generic!(ExecutionRequestsElectra, "ExecutionRequests"); +type_name_generic!(ExecutionRequestsGloas, "ExecutionRequests"); type_name_generic!(ExecutionPayloadEnvelope); type_name_generic!(SignedExecutionPayloadEnvelope); type_name_generic!(BlindedPayload, "ExecutionPayloadHeader"); diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index 9af88e0201..5222634c90 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -136,6 +136,19 @@ fn operations_consolidations() { OperationsHandler::::default().run(); } +#[test] +#[cfg(not(feature = "fake_crypto"))] +fn operations_builder_deposit_requests() { + OperationsHandler::::default().run(); + OperationsHandler::::default().run(); +} + +#[test] +fn operations_builder_exit_requests() { + OperationsHandler::::default().run(); + OperationsHandler::::default().run(); +} + #[test] fn operations_bls_to_execution_change() { OperationsHandler::::default().run(); @@ -792,9 +805,19 @@ mod ssz_static { #[test] fn execution_requests() { - SszStaticHandler::, MainnetEthSpec>::electra_and_later() + SszStaticHandler::, MainnetEthSpec>::electra_only( + ) + .run(); + SszStaticHandler::, MinimalEthSpec>::electra_only( + ) + .run(); + SszStaticHandler::, MainnetEthSpec>::fulu_only() .run(); - SszStaticHandler::, MinimalEthSpec>::electra_and_later() + SszStaticHandler::, MinimalEthSpec>::fulu_only() + .run(); + SszStaticHandler::, MainnetEthSpec>::gloas_only() + .run(); + SszStaticHandler::, MinimalEthSpec>::gloas_only() .run(); } @@ -805,6 +828,18 @@ mod ssz_static { SszStaticHandler::::gloas_and_later().run(); } + #[test] + fn builder_deposit_request() { + SszStaticHandler::::gloas_and_later().run(); + SszStaticHandler::::gloas_and_later().run(); + } + + #[test] + fn builder_exit_request() { + SszStaticHandler::::gloas_and_later().run(); + SszStaticHandler::::gloas_and_later().run(); + } + #[test] fn builder_pending_payment() { SszStaticHandler::::gloas_and_later().run(); @@ -1089,6 +1124,18 @@ fn fork_choice_on_payload_attestation_message() { ForkChoiceHandler::::new("on_payload_attestation_message").run(); } +#[test] +fn fork_choice_payload_timeliness() { + ForkChoiceHandler::::new("payload_timeliness").run(); + ForkChoiceHandler::::new("payload_timeliness").run(); +} + +#[test] +fn fork_choice_payload_data_availability() { + ForkChoiceHandler::::new("payload_data_availability").run(); + ForkChoiceHandler::::new("payload_data_availability").run(); +} + #[test] fn optimistic_sync() { OptimisticSyncHandler::::default().run();