From 99470315051d47afee6cdc0451ab4a08ca1e6236 Mon Sep 17 00:00:00 2001 From: conache Date: Mon, 13 Jul 2026 14:22:58 +0300 Subject: [PATCH 01/44] Add InclusionList and SignedInclusionList types --- .../types/src/execution/inclusion_list.rs | 35 +++++++++++++++++++ consensus/types/src/execution/mod.rs | 4 +++ .../src/execution/signed_inclusion_list.rs | 30 ++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 consensus/types/src/execution/inclusion_list.rs create mode 100644 consensus/types/src/execution/signed_inclusion_list.rs diff --git a/consensus/types/src/execution/inclusion_list.rs b/consensus/types/src/execution/inclusion_list.rs new file mode 100644 index 00000000000..ead408c6ba0 --- /dev/null +++ b/consensus/types/src/execution/inclusion_list.rs @@ -0,0 +1,35 @@ +use crate::{EthSpec, ForkName, Hash256, SignedRoot, Slot, Transactions}; +use context_deserialize::context_deserialize; +use educe::Educe; +use serde::{Deserialize, Serialize}; +use ssz_derive::{Decode, Encode}; +use tree_hash_derive::TreeHash; + +#[derive(Default, Debug, Clone, Serialize, Encode, Decode, Deserialize, TreeHash, Educe)] +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") +)] +#[educe(PartialEq, Hash)] +#[serde(bound = "E: EthSpec")] +#[context_deserialize(ForkName)] +pub struct InclusionList { + pub slot: Slot, + #[serde(with = "serde_utils::quoted_u64")] + pub validator_index: u64, + pub inclusion_list_committee_root: Hash256, + // TODO(heze): switch to using `ProgressiveList` once the support for EIP-7916 is added. + #[serde(with = "ssz_types::serde_utils::list_of_hex_var_list")] + pub transactions: Transactions, +} + +impl SignedRoot for InclusionList {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::MainnetEthSpec; + + ssz_and_tree_hash_tests!(InclusionList); +} diff --git a/consensus/types/src/execution/mod.rs b/consensus/types/src/execution/mod.rs index 380fd378a94..dc45bd94f97 100644 --- a/consensus/types/src/execution/mod.rs +++ b/consensus/types/src/execution/mod.rs @@ -8,10 +8,12 @@ mod execution_payload_bid; mod execution_payload_envelope; mod execution_payload_header; mod execution_requests; +mod inclusion_list; mod payload; mod signed_bls_to_execution_change; mod signed_execution_payload_bid; mod signed_execution_payload_envelope; +mod signed_inclusion_list; pub use bls_to_execution_change::BlsToExecutionChange; pub use eth1_data::Eth1Data; @@ -33,6 +35,7 @@ pub use execution_requests::{ ExecutionRequests, ExecutionRequestsElectra, ExecutionRequestsGloas, ExecutionRequestsRef, RequestType, WithdrawalRequests, }; +pub use inclusion_list::InclusionList; pub use payload::{ AbstractExecPayload, BlindedPayload, BlindedPayloadBellatrix, BlindedPayloadCapella, BlindedPayloadDeneb, BlindedPayloadElectra, BlindedPayloadFulu, BlindedPayloadRef, @@ -43,3 +46,4 @@ pub use payload::{ pub use signed_bls_to_execution_change::SignedBlsToExecutionChange; pub use signed_execution_payload_bid::SignedExecutionPayloadBid; pub use signed_execution_payload_envelope::SignedExecutionPayloadEnvelope; +pub use signed_inclusion_list::SignedInclusionList; diff --git a/consensus/types/src/execution/signed_inclusion_list.rs b/consensus/types/src/execution/signed_inclusion_list.rs new file mode 100644 index 00000000000..14786ceb0a0 --- /dev/null +++ b/consensus/types/src/execution/signed_inclusion_list.rs @@ -0,0 +1,30 @@ +use crate::execution::InclusionList; +use crate::{EthSpec, ForkName}; +use bls::Signature; +use context_deserialize::context_deserialize; +use educe::Educe; +use serde::{Deserialize, Serialize}; +use ssz_derive::{Decode, Encode}; +use tree_hash_derive::TreeHash; + +#[derive(TreeHash, Debug, Clone, Encode, Decode, Serialize, Deserialize, Educe)] +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") +)] +#[educe(PartialEq, Hash)] +#[serde(bound = "E: EthSpec")] +#[context_deserialize(ForkName)] +pub struct SignedInclusionList { + pub message: InclusionList, + pub signature: Signature, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::MainnetEthSpec; + + ssz_and_tree_hash_tests!(SignedInclusionList); +} From 3bc8458909df189ba144b82ec766f2248d71a1cf Mon Sep 17 00:00:00 2001 From: conache Date: Mon, 13 Jul 2026 14:38:22 +0300 Subject: [PATCH 02/44] Add IL committee size to the EthSpec trait --- consensus/types/src/core/eth_spec.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/consensus/types/src/core/eth_spec.rs b/consensus/types/src/core/eth_spec.rs index 9d794706476..447f03d52ab 100644 --- a/consensus/types/src/core/eth_spec.rs +++ b/consensus/types/src/core/eth_spec.rs @@ -184,6 +184,11 @@ pub trait EthSpec: 'static + Default + Sync + Send + Clone + Debug + PartialEq + type MaxBuilderDepositRequestsPerPayload: Unsigned + Clone + Sync + Send + Debug + PartialEq; type MaxBuilderExitRequestsPerPayload: Unsigned + Clone + Sync + Send + Debug + PartialEq; + /* + * New in Heze + */ + type InclusionListCommitteeSize: Unsigned + Clone + Sync + Send + Debug + PartialEq; + fn default_spec() -> ChainSpec; fn spec_name() -> EthSpecId; @@ -465,6 +470,11 @@ pub trait EthSpec: 'static + Default + Sync + Send + Clone + Debug + PartialEq + fn data_availability_timely_threshold() -> usize { Self::PTCSize::to_usize() / 2 } + + /// Returns the `INCLUSION_LIST_COMMITTEE_SIZE` constant for this specification. + fn inclusion_list_committee_size() -> usize { + Self::InclusionListCommitteeSize::to_usize() + } } /// Macro to inherit some type values from another EthSpec. @@ -543,6 +553,7 @@ impl EthSpec for MainnetEthSpec { type MaxBuildersPerWithdrawalsSweep = U16384; type MaxBuilderDepositRequestsPerPayload = U256; type MaxBuilderExitRequestsPerPayload = U16; + type InclusionListCommitteeSize = U16; fn default_spec() -> ChainSpec { ChainSpec::mainnet() @@ -622,6 +633,7 @@ impl EthSpec for MinimalEthSpec { MaxPayloadAttestations, MaxBuilderDepositRequestsPerPayload, MaxBuilderExitRequestsPerPayload, + InclusionListCommitteeSize, BuilderRegistryLimit }); @@ -702,6 +714,7 @@ impl EthSpec for GnosisEthSpec { type MaxBuildersPerWithdrawalsSweep = U16384; type MaxBuilderDepositRequestsPerPayload = U256; type MaxBuilderExitRequestsPerPayload = U16; + type InclusionListCommitteeSize = U16; fn default_spec() -> ChainSpec { ChainSpec::gnosis() From 4d5751f8ca435d422d411f060485bc601ef7ab72 Mon Sep 17 00:00:00 2001 From: conache Date: Mon, 13 Jul 2026 15:09:46 +0300 Subject: [PATCH 03/44] Add Heze presets --- consensus/types/presets/gnosis/heze.yaml | 5 +++++ consensus/types/presets/mainnet/heze.yaml | 5 +++++ consensus/types/presets/minimal/heze.yaml | 5 +++++ consensus/types/src/core/preset.rs | 9 +++++++-- 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/consensus/types/presets/gnosis/heze.yaml b/consensus/types/presets/gnosis/heze.yaml index 89acccc4112..8aa06a2a519 100644 --- a/consensus/types/presets/gnosis/heze.yaml +++ b/consensus/types/presets/gnosis/heze.yaml @@ -1 +1,6 @@ # Gnosis preset - Heze + +# Misc +# --------------------------------------------------------------- +# 2**4 (= 16) validators +INCLUSION_LIST_COMMITTEE_SIZE: 16 diff --git a/consensus/types/presets/mainnet/heze.yaml b/consensus/types/presets/mainnet/heze.yaml index 7e9d65ecda0..be038fbcc20 100644 --- a/consensus/types/presets/mainnet/heze.yaml +++ b/consensus/types/presets/mainnet/heze.yaml @@ -1 +1,6 @@ # Mainnet preset - Heze + +# Misc +# --------------------------------------------------------------- +# 2**4 (= 16) validators +INCLUSION_LIST_COMMITTEE_SIZE: 16 diff --git a/consensus/types/presets/minimal/heze.yaml b/consensus/types/presets/minimal/heze.yaml index 01dbf0e1a27..a36f5d7d35d 100644 --- a/consensus/types/presets/minimal/heze.yaml +++ b/consensus/types/presets/minimal/heze.yaml @@ -1 +1,6 @@ # Minimal preset - Heze + +# Misc +# --------------------------------------------------------------- +# 2**4 (= 16) validators +INCLUSION_LIST_COMMITTEE_SIZE: 16 diff --git a/consensus/types/src/core/preset.rs b/consensus/types/src/core/preset.rs index 1364b612dcf..611c31d0a1f 100644 --- a/consensus/types/src/core/preset.rs +++ b/consensus/types/src/core/preset.rs @@ -366,11 +366,16 @@ impl GloasPreset { #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] #[serde(rename_all = "UPPERCASE")] -pub struct HezePreset {} +pub struct HezePreset { + #[serde(with = "serde_utils::quoted_u64")] + pub inclusion_list_committee_size: u64, +} impl HezePreset { pub fn from_chain_spec(_spec: &ChainSpec) -> Self { - Self {} + Self { + inclusion_list_committee_size: E::inclusion_list_committee_size() as u64, + } } } From 1a985074d737f987c375f3ca49932f00de29c2f3 Mon Sep 17 00:00:00 2001 From: conache Date: Mon, 13 Jul 2026 15:15:54 +0300 Subject: [PATCH 04/44] Add IL committee domain constant --- consensus/types/src/core/chain_spec.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/consensus/types/src/core/chain_spec.rs b/consensus/types/src/core/chain_spec.rs index 67ac13ff2f7..8e968068ed1 100644 --- a/consensus/types/src/core/chain_spec.rs +++ b/consensus/types/src/core/chain_spec.rs @@ -38,6 +38,7 @@ pub enum Domain { PTCAttester, ProposerPreferences, BuilderDeposit, + InclusionListCommittee, ApplicationMask(ApplicationDomain), } @@ -149,6 +150,7 @@ pub struct ChainSpec { pub(crate) domain_ptc_attester: u32, pub(crate) domain_proposer_preferences: u32, pub(crate) domain_builder_deposit: u32, + pub(crate) domain_inclusion_list_committee: u32, /* * Fork choice @@ -550,6 +552,7 @@ impl ChainSpec { Domain::PTCAttester => self.domain_ptc_attester, Domain::ProposerPreferences => self.domain_proposer_preferences, Domain::BuilderDeposit => self.domain_builder_deposit, + Domain::InclusionListCommittee => self.domain_inclusion_list_committee, Domain::SyncCommittee => self.domain_sync_committee, Domain::ContributionAndProof => self.domain_contribution_and_proof, Domain::SyncCommitteeSelectionProof => self.domain_sync_committee_selection_proof, @@ -1184,6 +1187,7 @@ impl ChainSpec { domain_ptc_attester: 0x0C, domain_proposer_preferences: 0x0D, domain_builder_deposit: 0x0E, + domain_inclusion_list_committee: 0x10, /* * Fork choice @@ -1624,6 +1628,7 @@ impl ChainSpec { domain_ptc_attester: 0x0C, domain_proposer_preferences: 0x0D, domain_builder_deposit: 0x0E, + domain_inclusion_list_committee: 0x10, /* * Fork choice @@ -3067,6 +3072,11 @@ 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::InclusionListCommittee, + spec.domain_inclusion_list_committee, + &spec, + ); test_domain( Domain::ProposerPreferences, spec.domain_proposer_preferences, From 175191d3568b07ffd069cbc0bf9c383d423aaedf Mon Sep 17 00:00:00 2001 From: conache Date: Mon, 13 Jul 2026 16:25:10 +0300 Subject: [PATCH 05/44] Add il due bps and IL p2p limits constants --- consensus/types/src/core/chain_spec.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/consensus/types/src/core/chain_spec.rs b/consensus/types/src/core/chain_spec.rs index 8e968068ed1..c3f9aad1174 100644 --- a/consensus/types/src/core/chain_spec.rs +++ b/consensus/types/src/core/chain_spec.rs @@ -115,6 +115,7 @@ pub struct ChainSpec { pub aggregate_due_bps: u64, pub sync_message_due_bps: u64, pub contribution_due_bps: u64, + pub inclusion_list_due_bps: u64, /* * Derived time values (computed at startup via `compute_derived_values()`) @@ -323,6 +324,12 @@ pub struct ChainSpec { */ pub max_request_payloads: u64, + /* + * Networking Heze + */ + pub max_bytes_per_inclusion_list: u64, + pub max_request_inclusion_list: u64, + /* * Networking Derived * @@ -1151,6 +1158,7 @@ impl ChainSpec { aggregate_due_bps: 6667, sync_message_due_bps: 3333, contribution_due_bps: 6667, + inclusion_list_due_bps: 6667, /* * Derived time values (set by `compute_derived_values()`) @@ -1333,6 +1341,8 @@ impl ChainSpec { */ heze_fork_version: [0x08, 0x00, 0x00, 0x00], heze_fork_epoch: None, + max_bytes_per_inclusion_list: 8192, + max_request_inclusion_list: 16, /* * Network specific @@ -1681,6 +1691,7 @@ impl ChainSpec { altair_fork_epoch: Some(Epoch::new(512)), sync_message_due_bps: 3333, contribution_due_bps: 6667, + inclusion_list_due_bps: 6667, /* * Bellatrix hard fork params @@ -1775,6 +1786,8 @@ impl ChainSpec { */ heze_fork_version: [0x08, 0x00, 0x00, 0x64], heze_fork_epoch: None, + max_bytes_per_inclusion_list: 8192, + max_request_inclusion_list: 16, /* * Network specific @@ -2237,6 +2250,9 @@ pub struct Config { #[serde(default = "default_contribution_due_bps")] #[serde(with = "serde_utils::quoted_u64")] contribution_due_bps: u64, + #[serde(default = "default_inclusion_list_due_bps")] + #[serde(with = "serde_utils::quoted_u64")] + inclusion_list_due_bps: u64, #[serde(default = "default_min_builder_withdrawability_delay")] #[serde(with = "serde_utils::quoted_u64")] @@ -2489,6 +2505,10 @@ const fn default_aggregate_due_bps() -> u64 { 6667 } +const fn default_inclusion_list_due_bps() -> u64 { + 6667 +} + const fn default_sync_message_due_bps() -> u64 { 3333 } @@ -2778,6 +2798,7 @@ impl Config { aggregate_due_bps: spec.aggregate_due_bps, sync_message_due_bps: spec.sync_message_due_bps, contribution_due_bps: spec.contribution_due_bps, + inclusion_list_due_bps: spec.inclusion_list_due_bps, min_builder_withdrawability_delay: spec.min_builder_withdrawability_delay.as_u64(), @@ -2885,6 +2906,7 @@ impl Config { sync_message_due_bps, contribution_due_bps, confirmation_byzantine_threshold, + inclusion_list_due_bps, min_builder_withdrawability_delay, churn_limit_quotient_gloas, consolidation_churn_limit_quotient, @@ -2998,6 +3020,7 @@ impl Config { aggregate_due_bps, sync_message_due_bps, contribution_due_bps, + inclusion_list_due_bps, min_builder_withdrawability_delay: Epoch::new(min_builder_withdrawability_delay), @@ -3977,7 +4000,6 @@ mod yaml_tests { "CONTRIBUTION_DUE_BPS_GLOAS", "MAX_REQUEST_PAYLOADS", // Heze networking - "INCLUSION_LIST_DUE_BPS", "MAX_REQUEST_INCLUSION_LIST", "MAX_BYTES_PER_INCLUSION_LIST", ]; From 7e1f9ffb94ace5a9ebf1cbd756610719652510e9 Mon Sep 17 00:00:00 2001 From: conache Date: Mon, 13 Jul 2026 19:54:29 +0300 Subject: [PATCH 06/44] Add il deadline bps validation --- consensus/types/src/core/chain_spec.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/consensus/types/src/core/chain_spec.rs b/consensus/types/src/core/chain_spec.rs index c3f9aad1174..d74fdd4fae7 100644 --- a/consensus/types/src/core/chain_spec.rs +++ b/consensus/types/src/core/chain_spec.rs @@ -1009,6 +1009,11 @@ impl ChainSpec { "invalid chain spec: contribution_due_bps ({}) exceeds slot duration", self.contribution_due_bps ); + assert!( + self.inclusion_list_due_bps <= BASIS_POINTS, + "invalid chain spec: inclusion_list_due_bps ({}) exceeds slot duration", + self.inclusion_list_due_bps + ); self.unaggregated_attestation_due = self .compute_slot_component_duration(self.attestation_due_bps) From b7da74e57a1b81a24049cacbb1248ff80b810d85 Mon Sep 17 00:00:00 2001 From: conache Date: Tue, 28 Jul 2026 10:29:25 +0300 Subject: [PATCH 07/44] Add min_slots_for_inclusion_lists_requests constant --- consensus/types/configs/mainnet.yaml | 1 + consensus/types/configs/minimal.yaml | 1 + consensus/types/src/core/chain_spec.rs | 4 ++++ 3 files changed, 6 insertions(+) diff --git a/consensus/types/configs/mainnet.yaml b/consensus/types/configs/mainnet.yaml index 743384bcc90..372bf65b081 100644 --- a/consensus/types/configs/mainnet.yaml +++ b/consensus/types/configs/mainnet.yaml @@ -217,6 +217,7 @@ MAX_REQUEST_PAYLOADS: 128 # Heze # 2**4 (= 16) inclusion lists MAX_REQUEST_INCLUSION_LIST: 16 +MIN_SLOTS_FOR_INCLUSION_LISTS_REQUESTS: 1 # 2**13 (= 8,192) bytes MAX_BYTES_PER_INCLUSION_LIST: 8192 diff --git a/consensus/types/configs/minimal.yaml b/consensus/types/configs/minimal.yaml index 7251efc7626..fa7633fcceb 100644 --- a/consensus/types/configs/minimal.yaml +++ b/consensus/types/configs/minimal.yaml @@ -214,6 +214,7 @@ MAX_REQUEST_PAYLOADS: 128 # Heze # 2**4 (= 16) inclusion lists MAX_REQUEST_INCLUSION_LIST: 16 +MIN_SLOTS_FOR_INCLUSION_LISTS_REQUESTS: 1 # 2**13 (= 8,192) bytes MAX_BYTES_PER_INCLUSION_LIST: 8192 diff --git a/consensus/types/src/core/chain_spec.rs b/consensus/types/src/core/chain_spec.rs index d74fdd4fae7..f0922db0d0d 100644 --- a/consensus/types/src/core/chain_spec.rs +++ b/consensus/types/src/core/chain_spec.rs @@ -329,6 +329,7 @@ pub struct ChainSpec { */ pub max_bytes_per_inclusion_list: u64, pub max_request_inclusion_list: u64, + pub min_slots_for_inclusion_lists_requests: u64, /* * Networking Derived @@ -1348,6 +1349,7 @@ impl ChainSpec { heze_fork_epoch: None, max_bytes_per_inclusion_list: 8192, max_request_inclusion_list: 16, + min_slots_for_inclusion_lists_requests: 1, /* * Network specific @@ -1793,6 +1795,7 @@ impl ChainSpec { heze_fork_epoch: None, max_bytes_per_inclusion_list: 8192, max_request_inclusion_list: 16, + min_slots_for_inclusion_lists_requests: 1, /* * Network specific @@ -4007,6 +4010,7 @@ mod yaml_tests { // Heze networking "MAX_REQUEST_INCLUSION_LIST", "MAX_BYTES_PER_INCLUSION_LIST", + "MIN_SLOTS_FOR_INCLUSION_LISTS_REQUESTS", ]; /// Compare a `ChainSpec` against an upstream consensus-specs config YAML file. From 035a2814e9de20333fed68a460d0e3bb5cc72bb3 Mon Sep 17 00:00:00 2001 From: conache Date: Tue, 14 Jul 2026 16:52:01 +0300 Subject: [PATCH 08/44] Convert ExecutionPayloadBid to a fork-aware superstruct and propagate changes --- beacon_node/beacon_chain/src/beacon_chain.rs | 2 +- .../src/block_production/gloas.rs | 73 +++++---- .../beacon_chain/src/block_verification.rs | 4 +- .../src/block_verification_types.rs | 4 +- .../beacon_chain/src/canonical_head.rs | 2 +- .../src/data_availability_checker.rs | 2 +- .../src/data_column_verification.rs | 14 +- .../gossip_verified_bid.rs | 104 +++++++----- .../payload_bid_cache.rs | 40 ++--- .../src/payload_bid_verification/tests.rs | 120 +++++++------- .../gossip_verified_envelope.rs | 54 ++++--- .../payload_notifier.rs | 6 +- .../src/pending_payload_cache/mod.rs | 14 +- .../pending_components.rs | 4 +- beacon_node/beacon_chain/src/test_utils.rs | 21 +-- .../beacon_chain/tests/block_verification.rs | 28 ++-- .../beacon_chain/tests/column_verification.rs | 2 +- beacon_node/beacon_chain/tests/events.rs | 13 +- .../src/beacon/execution_payload_bids.rs | 19 ++- .../http_api/tests/gloas_reorg_tests.rs | 8 +- beacon_node/http_api/tests/tests.rs | 15 +- .../lighthouse_network/src/types/pubsub.rs | 29 +++- .../gossip_methods.rs | 2 +- .../sync/block_lookups/single_block_lookup.rs | 2 +- .../src/sync/block_sidecar_coupling.rs | 15 +- beacon_node/network/src/sync/tests/lookups.rs | 2 +- consensus/fork_choice/src/fork_choice.rs | 10 +- .../src/envelope_processing.rs | 20 +-- consensus/state_processing/src/genesis.rs | 19 ++- .../src/per_block_processing.rs | 79 +++++---- .../per_block_processing/signature_sets.rs | 14 +- .../src/per_block_processing/withdrawals.rs | 2 +- .../state_processing/src/upgrade/gloas.rs | 4 +- .../state_processing/src/upgrade/heze.rs | 27 +++- consensus/types/src/block/beacon_block.rs | 6 +- .../types/src/block/beacon_block_body.rs | 34 +++- .../types/src/block/signed_beacon_block.rs | 8 +- .../src/execution/execution_payload_bid.rs | 123 ++++++++++++-- consensus/types/src/execution/mod.rs | 10 +- .../execution/signed_execution_payload_bid.rs | 150 +++++++++++++++--- consensus/types/src/state/beacon_state.rs | 46 +++++- testing/ef_tests/src/cases/operations.rs | 14 +- testing/ef_tests/src/type_name.rs | 4 + testing/ef_tests/tests/tests.rs | 14 +- 44 files changed, 817 insertions(+), 366 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index ea85859cc62..d321ff1cbc5 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -3836,7 +3836,7 @@ impl BeaconChain { .message() .body() .signed_execution_payload_bid()? - .clone(), + .clone_as_signed_execution_payload_bid(), ); chain.pending_payload_cache.insert_bid(block_root, bid); } diff --git a/beacon_node/beacon_chain/src/block_production/gloas.rs b/beacon_node/beacon_chain/src/block_production/gloas.rs index ed21c37df3c..c89c50666f6 100644 --- a/beacon_node/beacon_chain/src/block_production/gloas.rs +++ b/beacon_node/beacon_chain/src/block_production/gloas.rs @@ -30,11 +30,12 @@ use types::consts::gloas::BUILDER_INDEX_SELF_BUILD; use types::{ Address, Attestation, AttestationElectra, AttesterSlashing, AttesterSlashingElectra, BeaconBlock, BeaconBlockBodyGloas, BeaconBlockGloas, BeaconState, BeaconStateError, - BuilderIndex, ChainSpec, Deposit, Eth1Data, EthSpec, ExecutionBlockHash, ExecutionPayloadBid, - ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequestsGloas, FullPayload, Graffiti, - Hash256, PayloadAttestation, ProposerSlashing, RelativeEpoch, SignedBeaconBlock, - SignedBlsToExecutionChange, SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, - SignedVoluntaryExit, Slot, SyncAggregate, Withdrawal, Withdrawals, + BuilderIndex, ChainSpec, Deposit, Eth1Data, EthSpec, ExecutionBlockHash, + ExecutionPayloadBidGloas, ExecutionPayloadEnvelope, ExecutionPayloadGloas, + ExecutionRequestsGloas, FullPayload, Graffiti, Hash256, PayloadAttestation, ProposerSlashing, + RelativeEpoch, SignedBeaconBlock, SignedBlsToExecutionChange, SignedExecutionPayloadBid, + SignedExecutionPayloadBidGloas, SignedExecutionPayloadEnvelope, SignedVoluntaryExit, Slot, + SyncAggregate, Withdrawal, Withdrawals, }; use crate::pending_payload_envelopes::PendingEnvelopeData; @@ -552,6 +553,14 @@ impl BeaconChain { bls_to_execution_changes, } = partial_beacon_block; + let SignedExecutionPayloadBid::Gloas(signed_execution_payload_bid) = + signed_execution_payload_bid + else { + return Err(BlockProductionError::InvalidBlockVariant( + "Cannot construct a Gloas block with a non-Gloas execution payload bid".to_string(), + )); + }; + let beacon_block = match &state { BeaconState::Base(_) | BeaconState::Altair(_) @@ -783,10 +792,10 @@ impl BeaconChain { .gloas_enabled(); let parent_block_hash = if should_build_on_full || parent_is_pre_gloas { // Build on parent bid's payload. - parent_bid.block_hash + parent_bid.block_hash() } else { // Skip parent bid's payload. For genesis this is the EL genesis hash. - parent_bid.parent_block_hash + parent_bid.parent_block_hash() }; // TODO(gloas) this should be BlockProductionVersion::V4 @@ -818,7 +827,7 @@ impl BeaconChain { // TODO(gloas) since we are defaulting to local building, execution payment is 0 // execution payment should only be set to > 0 for trusted building. - let bid = ExecutionPayloadBid:: { + let bid = ExecutionPayloadBidGloas:: { parent_block_hash, parent_block_root: parent_root, block_hash: payload.block_hash, @@ -843,10 +852,10 @@ impl BeaconChain { }; Ok(( - SignedExecutionPayloadBid { + SignedExecutionPayloadBid::Gloas(SignedExecutionPayloadBidGloas { message: bid, signature: Signature::infinity().map_err(BlockProductionError::BlsError)?, - }, + }), state, LocalBuildResult { payload_data, @@ -868,9 +877,9 @@ impl BeaconChain { Option>, ) { let cached_bid = self.gossip_verified_payload_bid_cache.get_highest_bid( - local_signed_bid.message.slot, - local_signed_bid.message.parent_block_hash, - local_signed_bid.message.parent_block_root, + local_signed_bid.message().slot(), + local_signed_bid.message().parent_block_hash(), + local_signed_bid.message().parent_block_root(), ); select_payload_bid_pure( local_signed_bid, @@ -910,19 +919,19 @@ pub(crate) fn select_payload_bid_pure( return (local_signed_bid, Some(payload_data)); }; - let slot = local_signed_bid.message.slot; + let slot = local_signed_bid.message().slot(); if should_override_builder { debug!( %slot, - cached_bid_value = cached_bid.message.value, + cached_bid_value = cached_bid.message().value(), "Using local payload because EL signaled shouldOverrideBuilder" ); return (local_signed_bid, Some(payload_data)); } // Convert bid value (gwei) to wei for comparison with `payload_value` (wei). - let bid_value_wei = types::Uint256::from(cached_bid.message.value) + let bid_value_wei = types::Uint256::from(cached_bid.message().value()) .saturating_mul(types::Uint256::from(1_000_000_000u64)); let boosted_bid_wei = match builder_boost_factor { Some(factor) => { @@ -935,7 +944,7 @@ pub(crate) fn select_payload_bid_pure( debug!( %slot, %payload_value, - cached_bid_value_gwei = cached_bid.message.value, + cached_bid_value_gwei = cached_bid.message().value(), ?builder_boost_factor, "Local payload is more profitable than cached builder bid" ); @@ -944,8 +953,8 @@ pub(crate) fn select_payload_bid_pure( debug!( %slot, %payload_value, - cached_bid_value_gwei = cached_bid.message.value, - cached_bid_builder_index = cached_bid.message.builder_index, + cached_bid_value_gwei = cached_bid.message().value(), + cached_bid_builder_index = cached_bid.message().builder_index(), ?builder_boost_factor, "Including cached builder bid" ); @@ -977,7 +986,7 @@ fn get_execution_payload_gloas( let random = *state.get_randao_mix(current_epoch)?; let parent_bid = state.latest_execution_payload_bid()?; - let is_parent_block_full = parent_block_hash == parent_bid.block_hash; + let is_parent_block_full = parent_block_hash == parent_bid.block_hash(); let withdrawals = if is_parent_block_full { if let Some(envelope) = parent_envelope { @@ -1292,24 +1301,26 @@ mod tests { } fn local_bid() -> SignedExecutionPayloadBid { - SignedExecutionPayloadBid { - message: ExecutionPayloadBid { + SignedExecutionPayloadBid::Gloas(SignedExecutionPayloadBidGloas { + message: ExecutionPayloadBidGloas { builder_index: BUILDER_INDEX_SELF_BUILD, ..Default::default() }, signature: Signature::empty(), - } + }) } fn cached_bid(value_gwei: u64) -> Arc> { - Arc::new(SignedExecutionPayloadBid { - message: ExecutionPayloadBid { - builder_index: REMOTE_BUILDER, - value: value_gwei, - ..Default::default() + Arc::new(SignedExecutionPayloadBid::Gloas( + SignedExecutionPayloadBidGloas { + message: ExecutionPayloadBidGloas { + builder_index: REMOTE_BUILDER, + value: value_gwei, + ..Default::default() + }, + signature: Signature::empty(), }, - signature: Signature::empty(), - }) + )) } fn local_build(payload_gwei: u64, should_override_builder: bool) -> LocalBuildResult { @@ -1345,7 +1356,7 @@ mod tests { let build = local_build(local_payload_gwei, should_override); let cache = cached_gwei.map(cached_bid); let (out, data) = select_payload_bid_pure::(local_bid(), build, cache, boost); - (out.message.builder_index, data.is_some()) + (out.message().builder_index(), data.is_some()) } #[test] diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index abbcba94b95..d3a76510eba 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -931,10 +931,10 @@ impl GossipVerifiedBlock { // [New in Gloas]: Verify bid.parent_block_root matches block.parent_root. if let Ok(bid) = block.message().body().signed_execution_payload_bid() - && bid.message.parent_block_root != block.message().parent_root() + && bid.message().parent_block_root() != block.message().parent_root() { return Err(BlockError::BidParentRootMismatch { - bid_parent_root: bid.message.parent_block_root, + bid_parent_root: bid.message().parent_block_root(), block_parent_root: block.message().parent_root(), }); } diff --git a/beacon_node/beacon_chain/src/block_verification_types.rs b/beacon_node/beacon_chain/src/block_verification_types.rs index 51feb12a695..68ddc6ab578 100644 --- a/beacon_node/beacon_chain/src/block_verification_types.rs +++ b/beacon_node/beacon_chain/src/block_verification_types.rs @@ -144,12 +144,12 @@ impl RangeSyncBlock { envelope: Option>, ) -> Result { if let Some(envelope) = envelope.as_ref() { - let execution_bid = &block + let execution_bid = block .message() .body() .signed_execution_payload_bid() .map_err(|e| format!("missing signed_execution_payload_bid: {e:?}"))? - .message; + .message(); // Skip the finalized-slot check; range sync imports historical (finalized) blocks. let latest_finalized_slot = Slot::new(0); verify_envelope_consistency( diff --git a/beacon_node/beacon_chain/src/canonical_head.rs b/beacon_node/beacon_chain/src/canonical_head.rs index b831519a6ef..0043c1e4a4d 100644 --- a/beacon_node/beacon_chain/src/canonical_head.rs +++ b/beacon_node/beacon_chain/src/canonical_head.rs @@ -357,7 +357,7 @@ impl CachedHead { self.snapshot .beacon_state .latest_execution_payload_bid() - .map(|bid| bid.prev_randao) + .map(|bid| bid.prev_randao()) } else { block.body().execution_payload().map(|p| p.prev_randao()) } diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 909f75e815d..bad373514d4 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -633,7 +633,7 @@ pub fn verify_columns_against_block( .message() .body() .signed_execution_payload_bid() - .map(|bid| bid.message.blob_kzg_commitments.clone()) + .map(|bid| bid.message().blob_kzg_commitments().clone()) .map_err(|_| { AvailabilityCheckError::Unexpected( "Gloas block missing signed_execution_payload_bid".to_string(), diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index 480a1e6871a..73df57961e4 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -389,7 +389,7 @@ impl GossipVerifiedDataColumn )?; verify_data_column_sidecar_with_commitments_len( &column_sidecar, - bid.message.blob_kzg_commitments.len(), + bid.message().blob_kzg_commitments().len(), &chain.spec, )?; } @@ -1117,13 +1117,13 @@ pub fn validate_data_column_sidecar_for_gossip_gloas< slot: column_slot, }, )?; - if bid.message.slot != column_slot { + if bid.message().slot() != column_slot { return Err(GossipDataColumnError::BlockSlotMismatch { - block_slot: bid.message.slot, + block_slot: bid.message().slot(), data_column_slot: column_slot, }); } - let kzg_commitments = &bid.message.blob_kzg_commitments; + let kzg_commitments = bid.message().blob_kzg_commitments(); verify_data_column_sidecar_with_commitments_len( &data_column, kzg_commitments.len(), @@ -1359,7 +1359,7 @@ pub(crate) fn load_gloas_payload_bid( .body() .signed_execution_payload_bid() .map_err(BeaconChainError::BeaconStateError)? - .clone(), + .clone_as_signed_execution_payload_bid(), ) } else { match chain @@ -1373,7 +1373,7 @@ pub(crate) fn load_gloas_payload_bid( .body() .signed_execution_payload_bid() .map_err(BeaconChainError::BeaconStateError)? - .clone(), + .clone_as_signed_execution_payload_bid(), ), Some(DatabaseBlock::Blinded(block)) => Arc::new( block @@ -1381,7 +1381,7 @@ pub(crate) fn load_gloas_payload_bid( .body() .signed_execution_payload_bid() .map_err(BeaconChainError::BeaconStateError)? - .clone(), + .clone_as_signed_execution_payload_bid(), ), None => { return Ok(None); 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 8c7619f2b70..73f28dffc3e 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 @@ -13,20 +13,20 @@ use state_processing::signature_sets::{ }; use tracing::debug; use types::{ - BeaconState, ChainSpec, EthSpec, ExecutionPayloadBid, SignedExecutionPayloadBid, + BeaconState, ChainSpec, EthSpec, ExecutionPayloadBidRef, SignedExecutionPayloadBid, SignedProposerPreferences, Slot, consts::gloas::PAYLOAD_BUILDER_VERSION, }; /// Verify that an execution payload bid is consistent with the current chain state /// and proposer preferences. pub(crate) fn verify_bid_consistency( - bid: &ExecutionPayloadBid, + bid: ExecutionPayloadBidRef, current_slot: Slot, proposer_preferences: &SignedProposerPreferences, head_state: &BeaconState, spec: &ChainSpec, ) -> Result<(), PayloadBidError> { - let bid_slot = bid.slot; + let bid_slot = bid.slot(); if bid_slot != current_slot && bid_slot != current_slot.saturating_add(1u64) { return Err(PayloadBidError::InvalidBidSlot { bid_slot }); @@ -34,27 +34,27 @@ pub(crate) fn verify_bid_consistency( // Execution payments are used by off protocol builders. In protocol bids // should always have this value set to zero. - if bid.execution_payment != 0 { + if bid.execution_payment() != 0 { return Err(PayloadBidError::ExecutionPaymentNonZero { - execution_payment: bid.execution_payment, + execution_payment: bid.execution_payment(), }); } - if bid.fee_recipient != proposer_preferences.message.fee_recipient { + if bid.fee_recipient() != proposer_preferences.message.fee_recipient { return Err(PayloadBidError::InvalidFeeRecipient); } let max_blobs_per_block = spec.max_blobs_per_block(bid_slot.epoch(E::slots_per_epoch())) as usize; - if bid.blob_kzg_commitments.len() > max_blobs_per_block { + if bid.blob_kzg_commitments().len() > max_blobs_per_block { return Err(PayloadBidError::InvalidBlobKzgCommitments { max_blobs_per_block, - blob_kzg_commitments_len: bid.blob_kzg_commitments.len(), + blob_kzg_commitments_len: bid.blob_kzg_commitments().len(), }); } - let builder_index = bid.builder_index; + let builder_index = bid.builder_index(); let is_active_builder = head_state .is_active_builder(builder_index, spec) @@ -72,10 +72,10 @@ pub(crate) fn verify_bid_consistency( }); } - if !head_state.can_builder_cover_bid(builder_index, bid.value, spec)? { + if !head_state.can_builder_cover_bid(builder_index, bid.value(), spec)? { return Err(PayloadBidError::BuilderCantCoverBid { builder_index, - builder_bid: bid.value, + builder_bid: bid.value(), }); } @@ -106,17 +106,17 @@ impl GossipVerifiedPayloadBid { where T: BeaconChainTypes, { - let bid_slot = signed_bid.message.slot; - let bid_parent_block_hash = signed_bid.message.parent_block_hash; - let bid_parent_block_root = signed_bid.message.parent_block_root; - let bid_value = signed_bid.message.value; + let bid_slot = signed_bid.message().slot(); + let bid_parent_block_hash = signed_bid.message().parent_block_hash(); + let bid_parent_block_root = signed_bid.message().parent_block_root(); + let bid_value = signed_bid.message().value(); if ctx .gossip_verified_payload_bid_cache - .seen_builder_index(&bid_slot, signed_bid.message.builder_index) + .seen_builder_index(&bid_slot, signed_bid.message().builder_index()) { return Err(PayloadBidError::BuilderAlreadySeen { - builder_index: signed_bid.message.builder_index, + builder_index: signed_bid.message().builder_index(), slot: bid_slot, }); } @@ -127,10 +127,10 @@ impl GossipVerifiedPayloadBid { bid_slot, bid_parent_block_hash, bid_parent_block_root, - ) && bid_value <= cached_bid.message.value + ) && bid_value <= cached_bid.message().value() { return Err(PayloadBidError::BidValueBelowCached { - cached_value: cached_bid.message.value, + cached_value: cached_bid.message().value(), incoming_value: bid_value, }); } @@ -177,7 +177,7 @@ 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 + if signed_bid.message().prev_randao() != *head_state.get_randao_mix(current_slot.epoch(E::slots_per_epoch()))? { return Err(PayloadBidError::InvalidPrevRandao { slot: bid_slot }); @@ -201,8 +201,8 @@ impl GossipVerifiedPayloadBid { // TODO into consideration maybe should persist parent block hash and gas limit in fork choice? if let Ok(parent_bid) = head_state.latest_execution_payload_bid() && !is_gas_limit_target_compatible( - parent_bid.gas_limit, - signed_bid.message.gas_limit, + parent_bid.gas_limit(), + signed_bid.message().gas_limit(), proposer_preferences.message.target_gas_limit, )? { @@ -212,7 +212,7 @@ impl GossipVerifiedPayloadBid { drop(fork_choice); verify_bid_consistency( - &signed_bid.message, + signed_bid.message(), current_slot, &proposer_preferences, head_state, @@ -223,7 +223,7 @@ impl GossipVerifiedPayloadBid { execution_payload_bid_signature_set( head_state, |i| get_builder_pubkey_from_state(head_state, i), - &signed_bid, + signed_bid.to_ref(), ctx.spec, ) .map_err(|_| PayloadBidError::BadSignature)? @@ -267,9 +267,9 @@ impl BeaconChain { &self, bid: Arc>, ) -> Result, PayloadBidError> { - let slot = bid.message.slot; - let parent_block_root = bid.message.parent_block_root; - let parent_block_hash = bid.message.parent_block_hash; + let slot = bid.message().slot(); + let parent_block_root = bid.message().parent_block_root(); + let parent_block_hash = bid.message().parent_block_hash(); let ctx = self.payload_bid_gossip_verification_context(); match GossipVerifiedPayloadBid::new(bid, &ctx) { @@ -343,8 +343,8 @@ mod tests { use kzg::KzgCommitment; use ssz_types::VariableList; use types::{ - Address, BeaconState, ChainSpec, EthSpec, ExecutionPayloadBid, MinimalEthSpec, - ProposerPreferences, SignedProposerPreferences, Slot, + Address, BeaconState, ChainSpec, EthSpec, ExecutionPayloadBidGloas, ExecutionPayloadBidRef, + MinimalEthSpec, ProposerPreferences, SignedProposerPreferences, Slot, }; use super::verify_bid_consistency; @@ -352,13 +352,13 @@ mod tests { type E = MinimalEthSpec; - fn make_bid(slot: Slot, fee_recipient: Address, gas_limit: u64) -> ExecutionPayloadBid { - ExecutionPayloadBid { + fn make_bid(slot: Slot, fee_recipient: Address, gas_limit: u64) -> ExecutionPayloadBidGloas { + ExecutionPayloadBidGloas { slot, fee_recipient, gas_limit, value: 100, - ..ExecutionPayloadBid::default() + ..ExecutionPayloadBidGloas::default() } } @@ -389,7 +389,13 @@ mod tests { let bid = make_bid(Slot::new(5), Address::ZERO, 30_000_000); let prefs = make_preferences(Address::ZERO, 30_000_000); - let result = verify_bid_consistency::(&bid, current_slot, &prefs, &state, &spec); + let result = verify_bid_consistency::( + ExecutionPayloadBidRef::Gloas(&bid), + current_slot, + &prefs, + &state, + &spec, + ); assert!(matches!( result, Err(PayloadBidError::InvalidBidSlot { .. }) @@ -403,7 +409,13 @@ mod tests { let bid = make_bid(Slot::new(12), Address::ZERO, 30_000_000); let prefs = make_preferences(Address::ZERO, 30_000_000); - let result = verify_bid_consistency::(&bid, current_slot, &prefs, &state, &spec); + let result = verify_bid_consistency::( + ExecutionPayloadBidRef::Gloas(&bid), + current_slot, + &prefs, + &state, + &spec, + ); assert!(matches!( result, Err(PayloadBidError::InvalidBidSlot { .. }) @@ -418,7 +430,13 @@ mod tests { bid.execution_payment = 42; let prefs = make_preferences(Address::ZERO, 30_000_000); - let result = verify_bid_consistency::(&bid, current_slot, &prefs, &state, &spec); + let result = verify_bid_consistency::( + ExecutionPayloadBidRef::Gloas(&bid), + current_slot, + &prefs, + &state, + &spec, + ); assert!(matches!( result, Err(PayloadBidError::ExecutionPaymentNonZero { @@ -434,7 +452,13 @@ mod tests { let bid = make_bid(current_slot, Address::ZERO, 30_000_000); let prefs = make_preferences(Address::repeat_byte(0xaa), 30_000_000); - let result = verify_bid_consistency::(&bid, current_slot, &prefs, &state, &spec); + let result = verify_bid_consistency::( + ExecutionPayloadBidRef::Gloas(&bid), + current_slot, + &prefs, + &state, + &spec, + ); assert!(matches!(result, Err(PayloadBidError::InvalidFeeRecipient))); } @@ -451,7 +475,13 @@ mod tests { .collect(); bid.blob_kzg_commitments = VariableList::new(commitments).unwrap(); - let result = verify_bid_consistency::(&bid, current_slot, &prefs, &state, &spec); + let result = verify_bid_consistency::( + ExecutionPayloadBidRef::Gloas(&bid), + current_slot, + &prefs, + &state, + &spec, + ); assert!(matches!( result, Err(PayloadBidError::InvalidBlobKzgCommitments { .. }) diff --git a/beacon_node/beacon_chain/src/payload_bid_verification/payload_bid_cache.rs b/beacon_node/beacon_chain/src/payload_bid_verification/payload_bid_cache.rs index 22e21bd57e9..62e2ebffd6a 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/payload_bid_cache.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/payload_bid_cache.rs @@ -41,14 +41,16 @@ impl GossipVerifiedPayloadBidCache { /// its value is higher than the currently cached bid for that tuple. pub fn insert_highest_bid(&self, bid: GossipVerifiedPayloadBid) { let key = ( - bid.signed_bid.message.parent_block_hash, - bid.signed_bid.message.parent_block_root, + bid.signed_bid.message().parent_block_hash(), + bid.signed_bid.message().parent_block_root(), ); let mut highest_bid = self.highest_bid.write(); - let slot_map = highest_bid.entry(bid.signed_bid.message.slot).or_default(); + let slot_map = highest_bid + .entry(bid.signed_bid.message().slot()) + .or_default(); if let Some(existing) = slot_map.get(&key) - && existing.signed_bid.message.value >= bid.signed_bid.message.value + && existing.signed_bid.message().value() >= bid.signed_bid.message().value() { return; } @@ -67,9 +69,9 @@ impl GossipVerifiedPayloadBidCache { pub fn insert_seen_builder(&self, bid: &GossipVerifiedPayloadBid) { let mut seen_builder = self.seen_builder.write(); seen_builder - .entry(bid.signed_bid.message.slot) + .entry(bid.signed_bid.message().slot()) .or_default() - .insert(bid.signed_bid.message.builder_index); + .insert(bid.signed_bid.message().builder_index()); } /// Prune anything before `current_slot` @@ -90,8 +92,8 @@ mod tests { use bls::Signature; use types::{ - ExecutionBlockHash, ExecutionPayloadBid, Hash256, MinimalEthSpec, - SignedExecutionPayloadBid, Slot, + ExecutionBlockHash, ExecutionPayloadBidGloas, Hash256, MinimalEthSpec, + SignedExecutionPayloadBid, SignedExecutionPayloadBidGloas, Slot, }; use super::GossipVerifiedPayloadBidCache; @@ -107,17 +109,19 @@ mod tests { value: u64, ) -> GossipVerifiedPayloadBid { GossipVerifiedPayloadBid { - signed_bid: Arc::new(SignedExecutionPayloadBid { - message: ExecutionPayloadBid { - slot, - builder_index, - parent_block_hash, - parent_block_root, - value, - ..ExecutionPayloadBid::default() + signed_bid: Arc::new(SignedExecutionPayloadBid::Gloas( + SignedExecutionPayloadBidGloas { + message: ExecutionPayloadBidGloas { + slot, + builder_index, + parent_block_hash, + parent_block_root, + value, + ..ExecutionPayloadBidGloas::default() + }, + signature: Signature::empty(), }, - signature: Signature::empty(), - }), + )), } } 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 e414c058916..8ac7858b6f3 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs @@ -1,4 +1,3 @@ -use std::assert_matches; use std::sync::Arc; use std::time::Duration; @@ -14,9 +13,9 @@ use state_processing::genesis::genesis_block; use store::{HotColdDB, StoreConfig}; use types::{ Address, ChainSpec, Checkpoint, Domain, Epoch, EthSpec, ExecutionBlockHash, - ExecutionPayloadBid, Hash256, MinimalEthSpec, ProposerPreferences, SignedBeaconBlock, - SignedExecutionPayloadBid, SignedProposerPreferences, SignedRoot, Slot, - consts::gloas::PAYLOAD_BUILDER_VERSION, + ExecutionPayloadBidGloas, ExecutionPayloadBidRef, Hash256, MinimalEthSpec, ProposerPreferences, + SignedBeaconBlock, SignedExecutionPayloadBid, SignedExecutionPayloadBidGloas, + SignedProposerPreferences, SignedRoot, Slot, consts::gloas::PAYLOAD_BUILDER_VERSION, }; use proto_array::{Block as ProtoBlock, ExecutionStatus, PayloadStatus}; @@ -106,7 +105,7 @@ impl TestContext { // Set a non-zero gas_limit on latest_execution_payload_bid so the gas limit // compatibility check doesn't reject all bids at genesis. - if let Ok(bid) = state.latest_execution_payload_bid_mut() { + if let Ok(bid) = state.latest_execution_payload_bid_gloas_mut() { bid.gas_limit = 30_000_000; } // Update body_root to reflect the modified bid (genesis block embeds it). @@ -176,7 +175,7 @@ impl TestContext { } } - fn sign_bid(&self, bid: ExecutionPayloadBid) -> Arc> { + fn sign_bid(&self, bid: ExecutionPayloadBidGloas) -> Arc> { let head = self.canonical_head.cached_head(); let state = &head.snapshot.beacon_state; let domain = self.spec.get_domain( @@ -185,12 +184,14 @@ impl TestContext { &state.fork(), state.genesis_validators_root(), ); - let message = bid.signing_root(domain); + let message = ExecutionPayloadBidRef::Gloas(&bid).signing_root(domain); let signature = self.keypairs[bid.builder_index as usize].sk.sign(message); - Arc::new(SignedExecutionPayloadBid { - message: bid, - signature, - }) + Arc::new(SignedExecutionPayloadBid::Gloas( + SignedExecutionPayloadBidGloas { + message: bid, + signature, + }, + )) } fn gossip_ctx(&self) -> GossipVerificationContext<'_, T> { @@ -222,19 +223,21 @@ impl TestContext { 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() + Arc::new(SignedExecutionPayloadBid::Gloas( + SignedExecutionPayloadBidGloas { + message: ExecutionPayloadBidGloas { + slot, + builder_index, + fee_recipient, + gas_limit, + value, + parent_block_root, + prev_randao: self.expected_prev_randao(), + ..ExecutionPayloadBidGloas::default() + }, + signature: Signature::empty(), }, - signature: Signature::empty(), - }) + )) } fn insert_non_canonical_block(&self) -> Hash256 { @@ -413,7 +416,10 @@ 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] @@ -470,17 +476,19 @@ fn execution_payment_nonzero() { let slot = Slot::new(1); seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); - let bid = Arc::new(SignedExecutionPayloadBid { - message: ExecutionPayloadBid { - slot, - gas_limit: 30_000_000, - execution_payment: 42, - parent_block_root: ctx.genesis_block_root, - prev_randao: ctx.expected_prev_randao(), - ..ExecutionPayloadBid::default() + let bid = Arc::new(SignedExecutionPayloadBid::Gloas( + SignedExecutionPayloadBidGloas { + message: ExecutionPayloadBidGloas { + slot, + gas_limit: 30_000_000, + execution_payment: 42, + parent_block_root: ctx.genesis_block_root, + prev_randao: ctx.expected_prev_randao(), + ..ExecutionPayloadBidGloas::default() + }, + signature: Signature::empty(), }, - signature: Signature::empty(), - }); + )); let result = GossipVerifiedPayloadBid::new(bid, &gossip); assert!(matches!( result, @@ -659,20 +667,22 @@ fn invalid_blob_kzg_commitments() { .map(|_| KzgCommitment::empty_for_testing()) .collect(); - let bid = Arc::new(SignedExecutionPayloadBid { - message: ExecutionPayloadBid { - slot, - builder_index: 0, - fee_recipient: Address::ZERO, - 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() + let bid = Arc::new(SignedExecutionPayloadBid::Gloas( + SignedExecutionPayloadBidGloas { + message: ExecutionPayloadBidGloas { + slot, + builder_index: 0, + fee_recipient: Address::ZERO, + 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(), + ..ExecutionPayloadBidGloas::default() + }, + signature: Signature::empty(), }, - signature: Signature::empty(), - }); + )); let result = GossipVerifiedPayloadBid::new(bid, &gossip); assert!(matches!( result, @@ -719,7 +729,7 @@ fn valid_bid() { let slot = Slot::new(1); seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); - let bid = ctx.sign_bid(ExecutionPayloadBid { + let bid = ctx.sign_bid(ExecutionPayloadBidGloas { slot, builder_index: 0, fee_recipient: Address::ZERO, @@ -727,7 +737,7 @@ fn valid_bid() { value: 0, parent_block_root: ctx.genesis_block_root, prev_randao: ctx.expected_prev_randao(), - ..ExecutionPayloadBid::default() + ..ExecutionPayloadBidGloas::default() }); let result = GossipVerifiedPayloadBid::new(bid, &gossip); assert!( @@ -747,7 +757,7 @@ fn two_builders_coexist_in_cache() { let slot = Slot::new(1); seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); - let bid_0 = ctx.sign_bid(ExecutionPayloadBid { + let bid_0 = ctx.sign_bid(ExecutionPayloadBidGloas { slot, builder_index: 0, fee_recipient: Address::ZERO, @@ -755,7 +765,7 @@ fn two_builders_coexist_in_cache() { value: 0, parent_block_root: ctx.genesis_block_root, prev_randao: ctx.expected_prev_randao(), - ..ExecutionPayloadBid::default() + ..ExecutionPayloadBidGloas::default() }); let result_0 = GossipVerifiedPayloadBid::new(bid_0, &gossip); assert!( @@ -765,7 +775,7 @@ fn two_builders_coexist_in_cache() { ); // Builder 1 must bid strictly higher than builder 0's cached value. - let bid_1 = ctx.sign_bid(ExecutionPayloadBid { + let bid_1 = ctx.sign_bid(ExecutionPayloadBidGloas { slot, builder_index: 1, fee_recipient: Address::ZERO, @@ -773,7 +783,7 @@ fn two_builders_coexist_in_cache() { value: 1, parent_block_root: ctx.genesis_block_root, prev_randao: ctx.expected_prev_randao(), - ..ExecutionPayloadBid::default() + ..ExecutionPayloadBidGloas::default() }); let result_1 = GossipVerifiedPayloadBid::new(bid_1, &gossip); assert!( @@ -790,8 +800,8 @@ fn two_builders_coexist_in_cache() { .bid_cache .get_highest_bid(slot, ExecutionBlockHash::zero(), ctx.genesis_block_root) .expect("should have highest bid"); - assert_eq!(highest.message.value, 1); - assert_eq!(highest.message.builder_index, 1); + assert_eq!(highest.message().value(), 1); + assert_eq!(highest.message().builder_index(), 1); } #[test] 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 341c7c00f02..224acfa997a 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 @@ -6,8 +6,9 @@ use parking_lot::{Mutex, RwLock}; use store::DatabaseBlock; use tracing::debug; use types::{ - ChainSpec, EthSpec, ExecutionPayloadBid, ExecutionPayloadEnvelope, Hash256, SignedBeaconBlock, - SignedExecutionPayloadEnvelope, Slot, consts::gloas::BUILDER_INDEX_SELF_BUILD, + ChainSpec, EthSpec, ExecutionPayloadBidRef, ExecutionPayloadEnvelope, Hash256, + SignedBeaconBlock, SignedExecutionPayloadEnvelope, Slot, + consts::gloas::BUILDER_INDEX_SELF_BUILD, }; use crate::{ @@ -37,7 +38,7 @@ pub struct GossipVerificationContext<'a, T: BeaconChainTypes> { pub(crate) fn verify_envelope_consistency( envelope: &ExecutionPayloadEnvelope, block: &SignedBeaconBlock, - execution_bid: &ExecutionPayloadBid, + execution_bid: ExecutionPayloadBidRef, latest_finalized_slot: Slot, ) -> Result<(), EnvelopeError> { // Check that the envelope's slot isn't from a slot prior @@ -58,17 +59,17 @@ pub(crate) fn verify_envelope_consistency( } // Builder index matches committed bid. - if envelope.builder_index != execution_bid.builder_index { + if envelope.builder_index != execution_bid.builder_index() { return Err(EnvelopeError::BuilderIndexMismatch { - committed_bid: execution_bid.builder_index, + committed_bid: execution_bid.builder_index(), envelope: envelope.builder_index, }); } // The block hash should match the block hash of the execution bid. - if envelope.payload.block_hash != execution_bid.block_hash { + if envelope.payload.block_hash != execution_bid.block_hash() { return Err(EnvelopeError::BlockHashMismatch { - committed_bid: execution_bid.block_hash, + committed_bid: execution_bid.block_hash(), envelope: envelope.payload.block_hash, }); } @@ -130,11 +131,11 @@ impl GossipVerifiedEnvelope { ))); } }; - let execution_bid = &block + let execution_bid = block .message() .body() .signed_execution_payload_bid()? - .message; + .message(); verify_envelope_consistency(envelope, &block, execution_bid, latest_finalized_slot)?; @@ -315,9 +316,9 @@ mod tests { use ssz_types::VariableList; use types::{ BeaconBlock, BeaconBlockBodyGloas, BeaconBlockGloas, Eth1Data, ExecutionBlockHash, - ExecutionPayloadBid, ExecutionPayloadEnvelope, ExecutionPayloadGloas, - ExecutionRequestsGloas, Graffiti, Hash256, MinimalEthSpec, SignedBeaconBlock, - SignedExecutionPayloadBid, Slot, SyncAggregate, + ExecutionPayloadBid, ExecutionPayloadBidGloas, ExecutionPayloadEnvelope, + ExecutionPayloadGloas, ExecutionRequestsGloas, Graffiti, Hash256, MinimalEthSpec, + SignedBeaconBlock, SignedExecutionPayloadBidGloas, Slot, SyncAggregate, }; use super::verify_envelope_consistency; @@ -365,7 +366,7 @@ mod tests { sync_aggregate: SyncAggregate::empty(), bls_to_execution_changes: VariableList::empty(), parent_execution_requests: ExecutionRequestsGloas::default(), - signed_execution_payload_bid: SignedExecutionPayloadBid::empty(), + signed_execution_payload_bid: SignedExecutionPayloadBidGloas::empty(), payload_attestations: VariableList::empty(), _phantom: PhantomData, }, @@ -374,11 +375,11 @@ mod tests { } fn make_bid(builder_index: u64, block_hash: ExecutionBlockHash) -> ExecutionPayloadBid { - ExecutionPayloadBid { + ExecutionPayloadBid::Gloas(ExecutionPayloadBidGloas { builder_index, block_hash, - ..ExecutionPayloadBid::default() - } + ..ExecutionPayloadBidGloas::default() + }) } #[test] @@ -391,7 +392,9 @@ mod tests { let block = make_block(slot); let bid = make_bid(builder_index, block_hash); - assert!(verify_envelope_consistency::(&envelope, &block, &bid, Slot::new(0)).is_ok()); + assert!( + verify_envelope_consistency::(&envelope, &block, bid.to_ref(), Slot::new(0)).is_ok() + ); } #[test] @@ -405,8 +408,12 @@ mod tests { let bid = make_bid(builder_index, block_hash); let latest_finalized_slot = Slot::new(10); - let result = - verify_envelope_consistency::(&envelope, &block, &bid, latest_finalized_slot); + let result = verify_envelope_consistency::( + &envelope, + &block, + bid.to_ref(), + latest_finalized_slot, + ); assert!(matches!( result, Err(EnvelopeError::PriorToFinalization { .. }) @@ -422,7 +429,8 @@ mod tests { let block = make_block(Slot::new(20)); let bid = make_bid(builder_index, block_hash); - let result = verify_envelope_consistency::(&envelope, &block, &bid, Slot::new(0)); + let result = + verify_envelope_consistency::(&envelope, &block, bid.to_ref(), Slot::new(0)); assert!(matches!(result, Err(EnvelopeError::SlotMismatch { .. }))); } @@ -435,7 +443,8 @@ mod tests { let block = make_block(slot); let bid = make_bid(2, block_hash); - let result = verify_envelope_consistency::(&envelope, &block, &bid, Slot::new(0)); + let result = + verify_envelope_consistency::(&envelope, &block, bid.to_ref(), Slot::new(0)); assert!(matches!( result, Err(EnvelopeError::BuilderIndexMismatch { .. }) @@ -451,7 +460,8 @@ mod tests { let block = make_block(slot); let bid = make_bid(builder_index, ExecutionBlockHash::repeat_byte(0xff)); - let result = verify_envelope_consistency::(&envelope, &block, &bid, Slot::new(0)); + let result = + verify_envelope_consistency::(&envelope, &block, bid.to_ref(), Slot::new(0)); assert!(matches!( result, Err(EnvelopeError::BlockHashMismatch { .. }) diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs index 8a47e4689a5..13c3c74f25d 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs @@ -73,15 +73,15 @@ impl PayloadNotifier { envelope: &'a SignedExecutionPayloadEnvelope, block: &'a SignedBeaconBlock, ) -> Result, PayloadVerificationError> { - let bid = &block + let bid = block .message() .body() .signed_execution_payload_bid() .map_err(|e| PayloadVerificationError::BeaconChainError(Box::new(e.into())))? - .message; + .message(); let versioned_hashes = bid - .blob_kzg_commitments + .blob_kzg_commitments() .iter() .map(kzg_commitment_to_versioned_hash) .collect(); 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 18d9a7ca52c..381f671e780 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs @@ -233,12 +233,12 @@ impl PendingPayloadCache { .ok_or(AvailabilityCheckError::MissingBid(block_root))?; let kzg_verified_columns = KzgVerifiedDataColumn::from_batch_with_scoring_and_commitments( custody_columns, - bid.message.blob_kzg_commitments.as_ref(), + bid.message().blob_kzg_commitments().as_ref(), &self.kzg, ) .map_err(AvailabilityCheckError::InvalidColumn)?; - let epoch = bid.message.slot.epoch(T::EthSpec::slots_per_epoch()); + let epoch = bid.message().slot().epoch(T::EthSpec::slots_per_epoch()); let sampling_columns = self.custody_context.sampling_columns_for_epoch(epoch); let verified_custody_columns = kzg_verified_columns .into_iter() @@ -260,7 +260,7 @@ impl PendingPayloadCache { let bid = self .get_bid(&block_root) .ok_or(AvailabilityCheckError::MissingBid(block_root))?; - let epoch = bid.message.slot.epoch(T::EthSpec::slots_per_epoch()); + let epoch = bid.message().slot().epoch(T::EthSpec::slots_per_epoch()); let sampling_columns = self.custody_context.sampling_columns_for_epoch(epoch); let custody_columns = data_columns .into_iter() @@ -324,7 +324,7 @@ impl PendingPayloadCache { let all_data_columns = KzgVerifiedCustodyDataColumn::reconstruct_columns( &self.kzg, verified_data_columns, - bid.message.blob_kzg_commitments.as_ref(), + bid.message().blob_kzg_commitments().as_ref(), &self.spec, ) .map_err(|e| { @@ -338,7 +338,7 @@ impl PendingPayloadCache { AvailabilityCheckError::ReconstructColumnsError(e) })?; - let slot = bid.message.slot; + let slot = bid.message().slot(); let columns_to_sample = self .custody_context() .sampling_columns_for_epoch(slot.epoch(T::EthSpec::slots_per_epoch())); @@ -571,11 +571,11 @@ mod data_availability_checker_tests { .body() .signed_execution_payload_bid() .expect("Gloas block has bid") - .clone(), + .clone_as_signed_execution_payload_bid(), ); cache.insert_bid(block_root, bid.clone()); - let epoch = bid.message.slot.epoch(E::slots_per_epoch()); + let epoch = bid.message().slot().epoch(E::slots_per_epoch()); let sampling = cache.custody_context().sampling_columns_for_epoch(epoch); let custody = columns .into_iter() diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs index dac0180be8d..7a138ec16ce 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs @@ -42,7 +42,7 @@ impl PendingComponents { /// Returns columns that have all cells present. pub fn get_cached_data_columns(&self) -> Vec>> { - let slot = self.bid.message.slot; + let slot = self.bid.message().slot(); let block_root = self.block_root; self.verified_data_columns .iter() @@ -52,7 +52,7 @@ impl PendingComponents { /// Returns the indices of columns that have all cells present. pub fn get_cached_data_columns_indices(&self) -> Vec { - let slot = self.bid.message.slot; + let slot = self.bid.message().slot(); let block_root = self.block_root; self.verified_data_columns .iter() diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index efe165ec50c..fdb924a2441 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -3107,15 +3107,15 @@ where .expect("should read block from store") .expect("block should exist in store"); - let bid = &block + let bid = block .message() .body() .signed_execution_payload_bid() .expect("Gloas block should have a payload bid") - .message; + .message(); let versioned_hashes = bid - .blob_kzg_commitments + .blob_kzg_commitments() .iter() .map(kzg_commitment_to_versioned_hash) .collect(); @@ -3197,13 +3197,14 @@ where let has_blobs = block.num_expected_blobs() > 0; if !has_blobs { return if let Ok(bid) = block.message().body().signed_execution_payload_bid() { + let bid = bid.clone_as_signed_execution_payload_bid(); let envelope = self .chain .get_payload_envelope(&block_root) .unwrap() .map(Arc::new) .map(|envelope| { - AvailableEnvelope::new(envelope, vec![], bid, &self.chain.custody_context) + AvailableEnvelope::new(envelope, vec![], &bid, &self.chain.custody_context) }) .transpose() .unwrap(); @@ -3228,6 +3229,7 @@ where .unwrap(); let custody_columns = columns.into_iter().collect::>(); if let Ok(bid) = block.message().body().signed_execution_payload_bid() { + let bid = bid.clone_as_signed_execution_payload_bid(); let envelope = self .chain .get_payload_envelope(&block_root) @@ -3237,7 +3239,7 @@ where AvailableEnvelope::new( envelope, custody_columns, - bid, + &bid, &self.chain.custody_context, ) }) @@ -3267,6 +3269,7 @@ where blob_items: Option<(KzgProofs, BlobsList)>, ) -> Result, BlockError> { if let Ok(bid) = block.message().body().signed_execution_payload_bid() { + let bid = bid.clone_as_signed_execution_payload_bid(); let columns = blob_items .map(|_| generate_data_column_sidecars_from_block(&block, &self.spec)) .unwrap_or_default(); @@ -3276,7 +3279,7 @@ where .map_err(|e| BlockError::BeaconChainError(Box::new(e)))? .map(Arc::new) .map(|envelope| { - AvailableEnvelope::new(envelope, columns, bid, &self.chain.custody_context) + AvailableEnvelope::new(envelope, columns, &bid, &self.chain.custody_context) }) .transpose() .unwrap(); @@ -4216,13 +4219,13 @@ pub fn generate_data_column_sidecars_from_block( // Load the precomputed column sidecar to avoid computing them for every block in the tests. // Then repeat the cells and proofs for every blob if block.fork_name_unchecked().gloas_enabled() { - let kzg_commitments = &block + let kzg_commitments = block .message() .body() .signed_execution_payload_bid() .expect("Gloas block should have a payload bid") - .message - .blob_kzg_commitments; + .message() + .blob_kzg_commitments(); if kzg_commitments.is_empty() { return vec![]; } diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index bc3aa485d14..9e6f0dca323 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -207,6 +207,7 @@ where T: BeaconChainTypes, { if let Ok(bid) = block.message().body().signed_execution_payload_bid() { + let bid = bid.clone_as_signed_execution_payload_bid(); let columns = match data_sidecars { Some(DataSidecars::DataColumns(columns)) => columns .iter() @@ -215,7 +216,7 @@ where Some(DataSidecars::Blobs(_)) | None => vec![], }; let envelope = execution_envelope - .map(|envelope| AvailableEnvelope::new(envelope, columns, bid, &chain.custody_context)) + .map(|envelope| AvailableEnvelope::new(envelope, columns, &bid, &chain.custody_context)) .transpose() .unwrap(); return RangeSyncBlock::new_gloas(block, envelope).unwrap(); @@ -2298,9 +2299,10 @@ async fn range_sync_block_new_gloas_accepts_matching_envelope() { .message() .body() .signed_execution_payload_bid() - .unwrap(); + .unwrap() + .clone_as_signed_execution_payload_bid(); let available_envelope = - AvailableEnvelope::new(Arc::new(envelope), columns, bid, &custody_context).unwrap(); + AvailableEnvelope::new(Arc::new(envelope), columns, &bid, &custody_context).unwrap(); let result = RangeSyncBlock::new_gloas(block, Some(available_envelope)); assert!( @@ -2338,9 +2340,10 @@ async fn range_sync_block_new_gloas_rejects_slot_mismatch() { .message() .body() .signed_execution_payload_bid() - .unwrap(); + .unwrap() + .clone_as_signed_execution_payload_bid(); let available_envelope = - AvailableEnvelope::new(Arc::new(envelope), columns, bid, &custody_context).unwrap(); + AvailableEnvelope::new(Arc::new(envelope), columns, &bid, &custody_context).unwrap(); let result = RangeSyncBlock::new_gloas(block, Some(available_envelope)); assert!( @@ -2363,9 +2366,10 @@ async fn range_sync_block_new_gloas_rejects_builder_index_mismatch() { .message() .body() .signed_execution_payload_bid() - .unwrap(); + .unwrap() + .clone_as_signed_execution_payload_bid(); let available_envelope = - AvailableEnvelope::new(Arc::new(envelope), columns, bid, &custody_context).unwrap(); + AvailableEnvelope::new(Arc::new(envelope), columns, &bid, &custody_context).unwrap(); let result = RangeSyncBlock::new_gloas(block, Some(available_envelope)); assert!( @@ -2388,9 +2392,10 @@ async fn range_sync_block_new_gloas_rejects_block_hash_mismatch() { .message() .body() .signed_execution_payload_bid() - .unwrap(); + .unwrap() + .clone_as_signed_execution_payload_bid(); let available_envelope = - AvailableEnvelope::new(Arc::new(envelope), columns, bid, &custody_context).unwrap(); + AvailableEnvelope::new(Arc::new(envelope), columns, &bid, &custody_context).unwrap(); let result = RangeSyncBlock::new_gloas(block, Some(available_envelope)); assert!( @@ -2640,12 +2645,13 @@ async fn filter_chain_segment_keeps_checkpoint_gloas_block_by_split_root() { .message() .body() .signed_execution_payload_bid() - .unwrap(); + .unwrap() + .clone_as_signed_execution_payload_bid(); let columns = generate_data_column_sidecars_from_block(&checkpoint_block, &harness.chain.spec); let available_envelope = AvailableEnvelope::new( Arc::new(envelope), columns, - bid, + &bid, &harness.chain.custody_context, ) .unwrap(); diff --git a/beacon_node/beacon_chain/tests/column_verification.rs b/beacon_node/beacon_chain/tests/column_verification.rs index 180e187e900..aebd3c20842 100644 --- a/beacon_node/beacon_chain/tests/column_verification.rs +++ b/beacon_node/beacon_chain/tests/column_verification.rs @@ -158,7 +158,7 @@ async fn gloas_envelope_blobs_produce_valid_columns() { .signed_execution_payload_bid() .expect("Gloas block should have a payload bid"); assert!( - !bid.message.blob_kzg_commitments.is_empty(), + !bid.message().blob_kzg_commitments().is_empty(), "Block should have blob KZG commitments" ); diff --git a/beacon_node/beacon_chain/tests/events.rs b/beacon_node/beacon_chain/tests/events.rs index 6e61f19e7f6..80e4fd33ad8 100644 --- a/beacon_node/beacon_chain/tests/events.rs +++ b/beacon_node/beacon_chain/tests/events.rs @@ -9,7 +9,8 @@ use types::data::FixedBlobSidecarList; use types::{ Address, BlobSidecar, DataColumnSidecar, DataColumnSidecarFulu, DataColumnSidecarGloas, Domain, EthSpec, MinimalEthSpec, PayloadAttestationData, PayloadAttestationMessage, - ProposerPreferences, SignedExecutionPayloadBid, SignedProposerPreferences, SignedRoot, Slot, + ProposerPreferences, SignedExecutionPayloadBid, SignedExecutionPayloadBidGloas, + SignedProposerPreferences, SignedRoot, Slot, }; type E = MinimalEthSpec; @@ -49,12 +50,12 @@ async fn data_column_sidecar_event_on_process_gossip_data_column() { .sampling_columns_for_epoch(epoch)[0]; // For gloas, the bid must be known, e.g. in the pending payload cache - let mut bid = SignedExecutionPayloadBid::::empty(); + let mut bid = SignedExecutionPayloadBidGloas::::empty(); bid.message.slot = Slot::new(10); - harness - .chain - .pending_payload_cache - .insert_bid(random_sidecar.beacon_block_root, Arc::new(bid)); + harness.chain.pending_payload_cache.insert_bid( + random_sidecar.beacon_block_root, + Arc::new(SignedExecutionPayloadBid::Gloas(bid)), + ); DataColumnSidecar::Gloas(random_sidecar) } else { diff --git a/beacon_node/http_api/src/beacon/execution_payload_bids.rs b/beacon_node/http_api/src/beacon/execution_payload_bids.rs index de469453a29..721ffbf10b7 100644 --- a/beacon_node/http_api/src/beacon/execution_payload_bids.rs +++ b/beacon_node/http_api/src/beacon/execution_payload_bids.rs @@ -5,13 +5,13 @@ use crate::utils::{ }; use beacon_chain::{BeaconChain, BeaconChainTypes}; use bytes::Bytes; +use eth2::CONSENSUS_VERSION_HEADER; use lighthouse_network::PubsubMessage; use network::NetworkMessage; -use ssz::Decode; use std::sync::Arc; use tokio::sync::mpsc::UnboundedSender; use tracing::{debug, warn}; -use types::SignedExecutionPayloadBid; +use types::{ForkName, ForkVersionDecode, SignedExecutionPayloadBid}; use warp::{ Filter, Rejection, reply::{Reply, Response}, @@ -29,19 +29,24 @@ pub(crate) fn post_beacon_execution_payload_bids_ssz( .and(warp::path("execution_payload_bids")) .and(warp::path::end()) .and(warp::body::bytes()) + .and(warp::header::header::(CONSENSUS_VERSION_HEADER)) .and(task_spawner_filter) .and(chain_filter) .and(network_tx_filter) .then( |body_bytes: Bytes, + consensus_version: ForkName, task_spawner: TaskSpawner, chain: Arc>, network_tx: UnboundedSender>| { task_spawner.blocking_response_task(Priority::P0, move || { - let bid = SignedExecutionPayloadBid::::from_ssz_bytes(&body_bytes) - .map_err(|e| { - warp_utils::reject::custom_bad_request(format!("invalid SSZ: {e:?}")) - })?; + let bid = SignedExecutionPayloadBid::::from_ssz_bytes_by_fork( + &body_bytes, + consensus_version, + ) + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!("invalid SSZ: {e:?}")) + })?; publish_execution_payload_bid(bid, &chain, &network_tx) }) }, @@ -83,7 +88,7 @@ pub fn publish_execution_payload_bid( network_tx: &UnboundedSender>, ) -> Result { let slot = bid.slot(); - let builder_index = bid.message.builder_index; + let builder_index = bid.message().builder_index(); if !chain.spec.is_gloas_scheduled() { return Err(warp_utils::reject::custom_bad_request( diff --git a/beacon_node/http_api/tests/gloas_reorg_tests.rs b/beacon_node/http_api/tests/gloas_reorg_tests.rs index bed7a35b024..0553c3cdda4 100644 --- a/beacon_node/http_api/tests/gloas_reorg_tests.rs +++ b/beacon_node/http_api/tests/gloas_reorg_tests.rs @@ -743,16 +743,16 @@ pub async fn proposer_boost_re_org_test( .body() .signed_execution_payload_bid() .unwrap() - .message - .block_hash + .message() + .block_hash() }; let exec_parent_hash = |block: BeaconBlockRef| -> ExecutionBlockHash { block .body() .signed_execution_payload_bid() .unwrap() - .message - .parent_block_hash + .message() + .parent_block_hash() }; let block_a_exec_hash = exec_block_hash(block_a.0.message()); diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index bb20123665b..0ee3aa238d8 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -49,10 +49,11 @@ use tokio::time::Duration; use tree_hash::TreeHash; use types::ApplicationDomain; use types::{ - Address, Domain, EthSpec, ExecutionBlockHash, ExecutionPayloadBid, Hash256, MainnetEthSpec, - ProposerPreferences, RelativeEpoch, SelectionProof, SignedExecutionPayloadBid, - SignedExecutionPayloadEnvelope, SignedProposerPreferences, SignedRoot, SingleAttestation, Slot, - attestation::AttestationBase, consts::gloas::BUILDER_INDEX_SELF_BUILD, + Address, Domain, EthSpec, ExecutionBlockHash, ExecutionPayloadBidGloas, Hash256, + MainnetEthSpec, ProposerPreferences, RelativeEpoch, SelectionProof, SignedExecutionPayloadBid, + SignedExecutionPayloadBidGloas, SignedExecutionPayloadEnvelope, SignedProposerPreferences, + SignedRoot, SingleAttestation, Slot, attestation::AttestationBase, + consts::gloas::BUILDER_INDEX_SELF_BUILD, }; type E = MainnetEthSpec; @@ -3065,7 +3066,7 @@ impl ApiTester { let slot = self.chain.slot().unwrap(); let fork_name = self.chain.spec.fork_name_at_slot::(slot); - let bid = ExecutionPayloadBid { + let bid = ExecutionPayloadBidGloas { parent_block_hash: ExecutionBlockHash::zero(), parent_block_root: head.head_block_root(), block_hash: ExecutionBlockHash::zero(), @@ -3080,10 +3081,10 @@ impl ApiTester { execution_requests_root: Hash256::zero(), }; - let signed = SignedExecutionPayloadBid { + let signed = SignedExecutionPayloadBid::Gloas(SignedExecutionPayloadBidGloas { message: bid, signature: bls::Signature::empty(), - }; + }); (signed, fork_name) } diff --git a/beacon_node/lighthouse_network/src/types/pubsub.rs b/beacon_node/lighthouse_network/src/types/pubsub.rs index 5f00517c616..b6b77e84ea0 100644 --- a/beacon_node/lighthouse_network/src/types/pubsub.rs +++ b/beacon_node/lighthouse_network/src/types/pubsub.rs @@ -8,11 +8,11 @@ use std::io::{Error, ErrorKind}; use std::sync::Arc; use types::{ AttesterSlashing, AttesterSlashingBase, AttesterSlashingElectra, CellBitmap, DataColumnSidecar, - DataColumnSubnetId, EthSpec, ForkContext, ForkName, Hash256, LightClientFinalityUpdate, - LightClientOptimisticUpdate, PartialDataColumn, PartialDataColumnHeader, - PartialDataColumnSidecar, PayloadAttestationMessage, ProposerSlashing, SignedAggregateAndProof, - SignedAggregateAndProofBase, SignedAggregateAndProofElectra, SignedBeaconBlock, - SignedBeaconBlockAltair, SignedBeaconBlockBase, SignedBeaconBlockBellatrix, + DataColumnSubnetId, EthSpec, ForkContext, ForkName, ForkVersionDecode, Hash256, + LightClientFinalityUpdate, LightClientOptimisticUpdate, PartialDataColumn, + PartialDataColumnHeader, PartialDataColumnSidecar, PayloadAttestationMessage, ProposerSlashing, + SignedAggregateAndProof, SignedAggregateAndProofBase, SignedAggregateAndProofElectra, + SignedBeaconBlock, SignedBeaconBlockAltair, SignedBeaconBlockBase, SignedBeaconBlockBellatrix, SignedBeaconBlockCapella, SignedBeaconBlockDeneb, SignedBeaconBlockElectra, SignedBeaconBlockFulu, SignedBeaconBlockGloas, SignedBeaconBlockHeze, SignedBlsToExecutionChange, SignedContributionAndProof, SignedExecutionPayloadBid, @@ -366,8 +366,20 @@ impl PubsubMessage { ))) } GossipKind::ExecutionPayloadBid => { - let execution_payload_bid = SignedExecutionPayloadBid::from_ssz_bytes(data) - .map_err(|e| format!("{:?}", e))?; + let execution_payload_bid = match fork_context + .get_fork_from_context_bytes(gossip_topic.fork_digest) + { + Some(&fork_name) => { + SignedExecutionPayloadBid::from_ssz_bytes_by_fork(data, fork_name) + .map_err(|e| format!("{:?}", e))? + } + None => { + return Err(format!( + "Unknown gossipsub fork digest: {:?}", + gossip_topic.fork_digest + )); + } + }; Ok(PubsubMessage::ExecutionPayloadBid(Box::new( execution_payload_bid, ))) @@ -551,7 +563,8 @@ impl std::fmt::Display for PubsubMessage { write!( f, "Execution payload bid: slot: {:?} value: {:?}", - data.message.slot, data.message.value + data.message().slot(), + data.message().value() ) } PubsubMessage::ProposerPreferences(data) => { 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 416dcf47bb2..9a19b44da04 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -4059,7 +4059,7 @@ impl NetworkBeaconProcessor { parent = None, level = "debug", skip_all, - fields(parent_block_hash = ?bid.message.parent_block_hash, parent_block_root = ?bid.message.parent_block_root), + fields(parent_block_hash = ?bid.message().parent_block_hash(), parent_block_root = ?bid.message().parent_block_root()), )] pub fn process_gossip_execution_payload_bid( self: &Arc, diff --git a/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs b/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs index 6962e4bc1e9..05d02500640 100644 --- a/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs +++ b/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs @@ -262,7 +262,7 @@ impl SingleBlockLookup { .body() .signed_execution_payload_bid() .ok() - .map(|bid| bid.message.block_hash) + .map(|bid| bid.message().block_hash()) }) } diff --git a/beacon_node/network/src/sync/block_sidecar_coupling.rs b/beacon_node/network/src/sync/block_sidecar_coupling.rs index a45a6cf2b13..2e1eaa2b6c1 100644 --- a/beacon_node/network/src/sync/block_sidecar_coupling.rs +++ b/beacon_node/network/src/sync/block_sidecar_coupling.rs @@ -445,9 +445,10 @@ impl RangeBlockComponentsRequest { .body() .signed_execution_payload_bid() // this really should never fail - .map_err(|_| AvailabilityCheckError::MissingBid(block_root))?; + .map_err(|_| AvailabilityCheckError::MissingBid(block_root))? + .clone_as_signed_execution_payload_bid(); let available_envelope = envelope - .map(|env| AvailableEnvelope::new(env, custody_columns, bid, custody_context)) + .map(|env| AvailableEnvelope::new(env, custody_columns, &bid, custody_context)) .transpose()?; RangeSyncBlock::new_gloas(block, available_envelope) @@ -586,22 +587,22 @@ mod tests { } fn matching_envelope(block: &SignedBeaconBlock) -> Arc> { - let bid = &block + let bid = block .message() .body() .signed_execution_payload_bid() .expect("Gloas block should have payload bid") - .message; + .message(); let mut envelope = SignedExecutionPayloadEnvelope { message: ExecutionPayloadEnvelope::empty(), signature: Signature::empty(), }; envelope.message.beacon_block_root = block.canonical_root(); envelope.message.parent_beacon_block_root = block.parent_root(); - envelope.message.builder_index = bid.builder_index; + envelope.message.builder_index = bid.builder_index(); envelope.message.payload.slot_number = block.slot(); - envelope.message.payload.parent_hash = bid.parent_block_hash; - envelope.message.payload.block_hash = bid.block_hash; + envelope.message.payload.parent_hash = bid.parent_block_hash(); + envelope.message.payload.block_hash = bid.block_hash(); Arc::new(envelope) } diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index f9afb910d9c..0a1ff831c9f 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -1293,7 +1293,7 @@ impl TestRig { AvailableEnvelope::new( envelope, columns.unwrap_or_default(), - bid, + &bid.clone_as_signed_execution_payload_bid(), &self.harness.chain.custody_context, ) }) diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index d9d0ede24ee..3d8a1181154 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -431,8 +431,8 @@ where // is decoupled from beacon blocks. ( ExecutionStatus::irrelevant(), - Some(signed_bid.message.parent_block_hash), - Some(signed_bid.message.block_hash), + Some(signed_bid.message().parent_block_hash()), + Some(signed_bid.message().block_hash()), ) } else if let Ok(execution_payload) = anchor_block.message().execution_payload() { // Pre-Gloas forks: do not set payload hashes, they are only used post-Gloas. @@ -822,7 +822,7 @@ where let builds_on_full = block .body() .signed_execution_payload_bid() - .is_ok_and(|bid| bid.message.parent_block_hash == parent_block_hash); + .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(), @@ -1047,8 +1047,8 @@ where let (execution_payload_parent_hash, execution_payload_block_hash) = if let Ok(signed_bid) = block.body().signed_execution_payload_bid() { ( - Some(signed_bid.message.parent_block_hash), - Some(signed_bid.message.block_hash), + Some(signed_bid.message().parent_block_hash()), + Some(signed_bid.message().block_hash()), ) } else { (None, None) diff --git a/consensus/state_processing/src/envelope_processing.rs b/consensus/state_processing/src/envelope_processing.rs index 3da4d1e9d60..223d3800ab5 100644 --- a/consensus/state_processing/src/envelope_processing.rs +++ b/consensus/state_processing/src/envelope_processing.rs @@ -150,16 +150,16 @@ pub fn verify_execution_payload_envelope( // Verify consistency with the committed bid let committed_bid = state.latest_execution_payload_bid()?; envelope_verify!( - envelope.builder_index == committed_bid.builder_index, + envelope.builder_index == committed_bid.builder_index(), EnvelopeProcessingError::BuilderIndexMismatch { - committed_bid: committed_bid.builder_index, + committed_bid: committed_bid.builder_index(), envelope: envelope.builder_index, } ); envelope_verify!( - committed_bid.prev_randao == payload.prev_randao, + committed_bid.prev_randao() == payload.prev_randao, EnvelopeProcessingError::PrevRandaoMismatch { - committed_bid: committed_bid.prev_randao, + committed_bid: committed_bid.prev_randao(), envelope: payload.prev_randao, } ); @@ -183,18 +183,18 @@ pub fn verify_execution_payload_envelope( // Verify the gas limit envelope_verify!( - committed_bid.gas_limit == payload.gas_limit, + committed_bid.gas_limit() == payload.gas_limit, EnvelopeProcessingError::GasLimitMismatch { - committed_bid: committed_bid.gas_limit, + committed_bid: committed_bid.gas_limit(), envelope: payload.gas_limit, } ); // Verify the block hash envelope_verify!( - committed_bid.block_hash == payload.block_hash, + committed_bid.block_hash() == payload.block_hash, EnvelopeProcessingError::BlockHashMismatch { - committed_bid: committed_bid.block_hash, + committed_bid: committed_bid.block_hash(), envelope: payload.block_hash, } ); @@ -221,9 +221,9 @@ pub fn verify_execution_payload_envelope( // Verify execution requests root matches committed bid let execution_requests_root = envelope.execution_requests.tree_hash_root(); envelope_verify!( - execution_requests_root == committed_bid.execution_requests_root, + execution_requests_root == committed_bid.execution_requests_root(), EnvelopeProcessingError::ExecutionRequestsRootMismatch { - committed_bid: committed_bid.execution_requests_root, + committed_bid: committed_bid.execution_requests_root(), envelope: execution_requests_root, } ); diff --git a/consensus/state_processing/src/genesis.rs b/consensus/state_processing/src/genesis.rs index d0b2bcd34e0..e9febbf9e43 100644 --- a/consensus/state_processing/src/genesis.rs +++ b/consensus/state_processing/src/genesis.rs @@ -169,9 +169,10 @@ pub fn initialize_beacon_state_from_eth1( // The genesis block's bid must have block_hash = 0x00 per spec (empty payload). // Retain the EL genesis hash in latest_block_hash and parent_block_hash so the - // first post-genesis proposer can build on the correct EL head. - let el_genesis_hash = state.latest_execution_payload_bid()?.block_hash; - let bid = state.latest_execution_payload_bid_mut()?; + // first post-genesis proposer can build on the correct EL head. This runs on the + // Gloas genesis state; a later Heze upgrade carries these fields forward. + let bid = state.latest_execution_payload_bid_gloas_mut()?; + let el_genesis_hash = bid.block_hash; bid.parent_block_hash = el_genesis_hash; bid.block_hash = ExecutionBlockHash::default(); @@ -216,8 +217,16 @@ pub fn genesis_block( spec: &ChainSpec, ) -> Result, BeaconStateError> { let mut block = BeaconBlock::empty(spec); - if let Ok(signed_bid) = block.body_mut().signed_execution_payload_bid_mut() { - signed_bid.message = state.latest_execution_payload_bid()?.clone(); + match &mut block { + BeaconBlock::Gloas(gloas_block) => { + gloas_block.body.signed_execution_payload_bid.message = + state.latest_execution_payload_bid_gloas()?.clone(); + } + BeaconBlock::Heze(heze_block) => { + heze_block.body.signed_execution_payload_bid.message = + state.latest_execution_payload_bid_heze()?.clone(); + } + _ => {} } Ok(block) } diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index fc215942fae..c31d98b8f57 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -557,12 +557,12 @@ pub fn process_parent_execution_payload( requests: &ExecutionRequestsGloas, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { - let parent_bid = state.latest_execution_payload_bid()?.clone(); - let parent_slot = parent_bid.slot; + let parent_bid = state.latest_execution_payload_bid()?; + let parent_slot = parent_bid.slot(); + let parent_value = parent_bid.value(); + let parent_fee_recipient = parent_bid.fee_recipient(); + let parent_builder_index = parent_bid.builder_index(); + let parent_block_hash = parent_bid.block_hash(); let parent_epoch = parent_slot.epoch(E::slots_per_epoch()); // Process execution requests from the parent's payload @@ -615,16 +619,16 @@ pub fn apply_parent_execution_payload( } else if parent_epoch == state.previous_epoch() { let payment_index = parent_slot.as_u64().safe_rem(E::slots_per_epoch())? as usize; settle_builder_payment(state, payment_index)?; - } else if parent_bid.value > 0 { + } else if parent_value > 0 { // Parent is older than previous epoch -- payment entry has already been // settled or evicted by process_builder_pending_payments at epoch boundaries. // Append the withdrawal directly from the bid. state .builder_pending_withdrawals_mut()? .push(BuilderPendingWithdrawal { - fee_recipient: parent_bid.fee_recipient, - amount: parent_bid.value, - builder_index: parent_bid.builder_index, + fee_recipient: parent_fee_recipient, + amount: parent_value, + builder_index: parent_builder_index, }) .map_err(|e| BlockProcessingError::BeaconStateError(e.into()))?; } @@ -639,7 +643,7 @@ pub fn apply_parent_execution_payload( .map_err(BlockProcessingError::BitfieldError)?; // Update latest_block_hash to the parent bid's block_hash - *state.latest_block_hash_mut()? = parent_bid.block_hash; + *state.latest_block_hash_mut()? = parent_block_hash; Ok(()) } @@ -674,14 +678,14 @@ pub fn settle_builder_payment( pub fn process_execution_payload_bid( state: &mut BeaconState, - signed_bid: &SignedExecutionPayloadBid, + signed_bid: SignedExecutionPayloadBidRef, verify_signatures: VerifySignatures, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { // Verify the bid signature - let bid = &signed_bid.message; - let amount = bid.value; - let builder_index = bid.builder_index; + let bid = signed_bid.message(); + let amount = bid.value(); + let builder_index = bid.builder_index(); // For self-builds, amount must be zero regardless of withdrawal credential prefix if builder_index == BUILDER_INDEX_SELF_BUILD { @@ -690,7 +694,7 @@ pub fn process_execution_payload_bid( ExecutionPayloadBidInvalid::SelfBuildNonZeroAmount.into() ); block_verify!( - signed_bid.signature.is_infinity(), + signed_bid.signature().is_infinity(), ExecutionPayloadBidInvalid::BadSignature.into() ); } else { @@ -743,19 +747,19 @@ pub fn process_execution_payload_bid( // Verify commitments are under limit let max_blobs_per_block = spec.max_blobs_per_block(state.current_epoch()) as usize; block_verify!( - bid.blob_kzg_commitments.len() <= max_blobs_per_block, + bid.blob_kzg_commitments().len() <= max_blobs_per_block, ExecutionPayloadBidInvalid::ExcessBlobCommitments { max: max_blobs_per_block, - bid: bid.blob_kzg_commitments.len(), + bid: bid.blob_kzg_commitments().len(), } .into() ); // Verify that the bid is for the current slot block_verify!( - bid.slot == state.slot(), + bid.slot() == state.slot(), ExecutionPayloadBidInvalid::SlotMismatch { - bid_slot: bid.slot, + bid_slot: bid.slot(), state_slot: state.slot(), } .into() @@ -764,30 +768,30 @@ pub fn process_execution_payload_bid( // Verify that the bid is for the right parent block let latest_block_hash = state.latest_block_hash()?; block_verify!( - bid.parent_block_hash == *latest_block_hash, + bid.parent_block_hash() == *latest_block_hash, ExecutionPayloadBidInvalid::ParentBlockHashMismatch { state_block_hash: *latest_block_hash, - bid_parent_hash: bid.parent_block_hash, + bid_parent_hash: bid.parent_block_hash(), } .into() ); let expected_parent_root = *state.get_block_root(state.slot().safe_sub(1)?)?; block_verify!( - bid.parent_block_root == expected_parent_root, + bid.parent_block_root() == expected_parent_root, ExecutionPayloadBidInvalid::ParentBlockRootMismatch { block_parent_root: expected_parent_root, - bid_parent_root: bid.parent_block_root, + bid_parent_root: bid.parent_block_root(), } .into() ); let expected_randao = *state.get_randao_mix(state.current_epoch())?; block_verify!( - bid.prev_randao == expected_randao, + bid.prev_randao() == expected_randao, ExecutionPayloadBidInvalid::PrevRandaoMismatch { expected: expected_randao, - bid: bid.prev_randao, + bid: bid.prev_randao(), } .into() ); @@ -798,15 +802,18 @@ pub fn process_execution_payload_bid( let pending_payment = BuilderPendingPayment { weight: 0, withdrawal: BuilderPendingWithdrawal { - fee_recipient: bid.fee_recipient, + fee_recipient: bid.fee_recipient(), amount, builder_index, }, proposer_index, }; - let payment_index = E::SlotsPerEpoch::to_usize() - .safe_add(bid.slot.as_usize().safe_rem(E::SlotsPerEpoch::to_usize())?)?; + let payment_index = E::SlotsPerEpoch::to_usize().safe_add( + bid.slot() + .as_usize() + .safe_rem(E::SlotsPerEpoch::to_usize())?, + )?; *state .builder_pending_payments_mut()? @@ -816,8 +823,16 @@ pub fn process_execution_payload_bid( ))? = pending_payment; } - // Cache the execution bid - *state.latest_execution_payload_bid_mut()? = bid.clone(); + // Cache the execution bid (the bid's fork must match the state's fork) + match (state.latest_execution_payload_bid_mut()?, bid) { + (ExecutionPayloadBidRefMut::Gloas(bid_mut), ExecutionPayloadBidRef::Gloas(cached)) => { + *bid_mut = cached.clone(); + } + (ExecutionPayloadBidRefMut::Heze(bid_mut), ExecutionPayloadBidRef::Heze(cached)) => { + *bid_mut = cached.clone(); + } + _ => return Err(BlockProcessingError::IncorrectStateType), + } Ok(()) } 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 6f9305f08ba..88d43745ccd 100644 --- a/consensus/state_processing/src/per_block_processing/signature_sets.rs +++ b/consensus/state_processing/src/per_block_processing/signature_sets.rs @@ -12,7 +12,7 @@ use types::{ BuilderIndex, ChainSpec, DepositData, Domain, Epoch, EthSpec, Fork, Hash256, InconsistentFork, IndexedAttestation, IndexedAttestationRef, IndexedPayloadAttestation, ProposerSlashing, SignedAggregateAndProof, SignedBeaconBlock, SignedBeaconBlockHeader, - SignedBlsToExecutionChange, SignedContributionAndProof, SignedExecutionPayloadBid, + SignedBlsToExecutionChange, SignedContributionAndProof, SignedExecutionPayloadBidRef, SignedProposerPreferences, SignedRoot, SignedVoluntaryExit, SigningData, Slot, SyncAggregate, SyncAggregatorSelectionData, consts::gloas::BUILDER_INDEX_SELF_BUILD, }; @@ -439,15 +439,15 @@ where pub fn execution_payload_bid_signature_set<'a, E, F>( state: &'a BeaconState, get_builder_pubkey: F, - signed_execution_payload_bid: &'a SignedExecutionPayloadBid, + signed_execution_payload_bid: SignedExecutionPayloadBidRef<'a, E>, spec: &'a ChainSpec, ) -> Result>> where E: EthSpec, F: Fn(BuilderIndex) -> Option>, { - let execution_payload_bid = &signed_execution_payload_bid.message; - let builder_index = execution_payload_bid.builder_index; + let execution_payload_bid = signed_execution_payload_bid.message(); + let builder_index = execution_payload_bid.builder_index(); if builder_index == BUILDER_INDEX_SELF_BUILD { // No signatures to verify in case of a self-build, but consensus code MUST check that // the signature is the point at infinity. @@ -456,8 +456,8 @@ where } let bid_epoch = signed_execution_payload_bid - .message - .slot + .message() + .slot() .epoch(E::slots_per_epoch()); let bid_fork = spec.fork_at_epoch(bid_epoch); let domain = spec.get_domain( @@ -471,7 +471,7 @@ where let message = execution_payload_bid.signing_root(domain); Ok(Some(SignatureSet::single_pubkey( - &signed_execution_payload_bid.signature, + signed_execution_payload_bid.signature(), pubkey, message, ))) diff --git a/consensus/state_processing/src/per_block_processing/withdrawals.rs b/consensus/state_processing/src/per_block_processing/withdrawals.rs index 8a09e35cdfc..d698da44e46 100644 --- a/consensus/state_processing/src/per_block_processing/withdrawals.rs +++ b/consensus/state_processing/src/per_block_processing/withdrawals.rs @@ -495,7 +495,7 @@ pub mod gloas { spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { // Return early if the parent block is empty. - if *state.latest_block_hash()? != state.latest_execution_payload_bid()?.block_hash { + if *state.latest_block_hash()? != state.latest_execution_payload_bid()?.block_hash() { return Ok(()); } diff --git a/consensus/state_processing/src/upgrade/gloas.rs b/consensus/state_processing/src/upgrade/gloas.rs index 6e32bd00bc3..0015474a96b 100644 --- a/consensus/state_processing/src/upgrade/gloas.rs +++ b/consensus/state_processing/src/upgrade/gloas.rs @@ -8,7 +8,7 @@ use tree_hash::TreeHash; use typenum::Unsigned; use types::{ BeaconState, BeaconStateError as Error, BeaconStateGloas, BuilderPendingPayment, ChainSpec, - DepositData, EthSpec, ExecutionPayloadBid, ExecutionRequestsGloas, Fork, + DepositData, EthSpec, ExecutionPayloadBidGloas, ExecutionRequestsGloas, Fork, consts::gloas::PAYLOAD_BUILDER_VERSION, is_builder_withdrawal_credential, }; @@ -75,7 +75,7 @@ pub fn upgrade_state_to_gloas( current_sync_committee: pre.current_sync_committee.clone(), next_sync_committee: pre.next_sync_committee.clone(), // Execution Bid - latest_execution_payload_bid: ExecutionPayloadBid { + latest_execution_payload_bid: ExecutionPayloadBidGloas { block_hash: pre.latest_execution_payload_header.block_hash, gas_limit: pre.latest_execution_payload_header.gas_limit, execution_requests_root: ExecutionRequestsGloas::::default().tree_hash_root(), diff --git a/consensus/state_processing/src/upgrade/heze.rs b/consensus/state_processing/src/upgrade/heze.rs index e9156418c1e..dd03dcd02fc 100644 --- a/consensus/state_processing/src/upgrade/heze.rs +++ b/consensus/state_processing/src/upgrade/heze.rs @@ -1,5 +1,9 @@ +use ssz_types::BitVector; use std::mem; -use types::{BeaconState, BeaconStateError as Error, BeaconStateHeze, ChainSpec, EthSpec, Fork}; +use types::{ + BeaconState, BeaconStateError as Error, BeaconStateHeze, ChainSpec, EthSpec, + ExecutionPayloadBidHeze, Fork, +}; /// Transform a `Gloas` state into a `Heze` state. pub fn upgrade_to_heze( @@ -64,7 +68,26 @@ pub fn upgrade_state_to_heze( current_sync_committee: pre.current_sync_committee.clone(), next_sync_committee: pre.next_sync_committee.clone(), // Execution Bid - latest_execution_payload_bid: pre.latest_execution_payload_bid.clone(), + // [New in Heze:EIP7805] the bid gains `inclusion_list_bits`; carry the shared Gloas + // fields forward and default the new field to empty (no bits computed at upgrade). + latest_execution_payload_bid: { + let pre_bid = &pre.latest_execution_payload_bid; + ExecutionPayloadBidHeze { + parent_block_hash: pre_bid.parent_block_hash, + parent_block_root: pre_bid.parent_block_root, + block_hash: pre_bid.block_hash, + prev_randao: pre_bid.prev_randao, + fee_recipient: pre_bid.fee_recipient, + gas_limit: pre_bid.gas_limit, + builder_index: pre_bid.builder_index, + slot: pre_bid.slot, + value: pre_bid.value, + execution_payment: pre_bid.execution_payment, + blob_kzg_commitments: pre_bid.blob_kzg_commitments.clone(), + execution_requests_root: pre_bid.execution_requests_root, + inclusion_list_bits: BitVector::new(), + } + }, // Capella next_withdrawal_index: pre.next_withdrawal_index, next_withdrawal_validator_index: pre.next_withdrawal_validator_index, diff --git a/consensus/types/src/block/beacon_block.rs b/consensus/types/src/block/beacon_block.rs index 469a6ca1a75..fee79ee819b 100644 --- a/consensus/types/src/block/beacon_block.rs +++ b/consensus/types/src/block/beacon_block.rs @@ -14,7 +14,7 @@ use tree_hash_derive::TreeHash; use typenum::Unsigned; use crate::{ - SignedExecutionPayloadBid, + SignedExecutionPayloadBidGloas, SignedExecutionPayloadBidHeze, attestation::{AttestationBase, AttestationData, IndexedAttestationBase}, block::{ BeaconBlockBodyAltair, BeaconBlockBodyBase, BeaconBlockBodyBellatrix, @@ -727,7 +727,7 @@ impl> EmptyBlock for BeaconBlockGloa sync_aggregate: SyncAggregate::empty(), bls_to_execution_changes: VariableList::empty(), parent_execution_requests: ExecutionRequestsGloas::default(), - signed_execution_payload_bid: SignedExecutionPayloadBid::empty(), + signed_execution_payload_bid: SignedExecutionPayloadBidGloas::empty(), payload_attestations: VariableList::empty(), _phantom: PhantomData, }, @@ -759,7 +759,7 @@ impl> EmptyBlock for BeaconBlockHeze sync_aggregate: SyncAggregate::empty(), bls_to_execution_changes: VariableList::empty(), parent_execution_requests: ExecutionRequestsGloas::default(), - signed_execution_payload_bid: SignedExecutionPayloadBid::empty(), + signed_execution_payload_bid: SignedExecutionPayloadBidHeze::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 3e6e66b3a12..8e7e0726404 100644 --- a/consensus/types/src/block/beacon_block_body.rs +++ b/consensus/types/src/block/beacon_block_body.rs @@ -13,7 +13,7 @@ use tree_hash::TreeHash; use tree_hash_derive::TreeHash; use crate::{ - SignedExecutionPayloadBid, + SignedExecutionPayloadBidGloas, SignedExecutionPayloadBidHeze, SignedExecutionPayloadBidRef, attestation::{ AttestationBase, AttestationElectra, AttestationRef, AttestationRefMut, PayloadAttestation, }, @@ -166,8 +166,16 @@ pub struct BeaconBlockBody = FullPay pub blob_kzg_commitments: KzgCommitments, #[superstruct(only(Electra, Fulu))] pub execution_requests: ExecutionRequestsElectra, - #[superstruct(only(Gloas, Heze))] - pub signed_execution_payload_bid: SignedExecutionPayloadBid, + #[superstruct( + only(Gloas), + partial_getter(rename = "signed_execution_payload_bid_gloas") + )] + pub signed_execution_payload_bid: SignedExecutionPayloadBidGloas, + #[superstruct( + only(Heze), + partial_getter(rename = "signed_execution_payload_bid_heze") + )] + pub signed_execution_payload_bid: SignedExecutionPayloadBidHeze, #[superstruct(only(Gloas, Heze))] pub payload_attestations: VariableList, E::MaxPayloadAttestations>, #[superstruct(only(Gloas, Heze))] @@ -190,6 +198,12 @@ impl> BeaconBlockBody { pub fn fork_name(&self) -> ForkName { self.to_ref().fork_name() } + + pub fn signed_execution_payload_bid( + &self, + ) -> Result, BeaconStateError> { + self.to_ref().signed_execution_payload_bid() + } } impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, Payload> { @@ -206,6 +220,20 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, } } + pub fn signed_execution_payload_bid( + &self, + ) -> Result, BeaconStateError> { + match self { + Self::Gloas(body) => Ok(SignedExecutionPayloadBidRef::Gloas( + &body.signed_execution_payload_bid, + )), + Self::Heze(body) => Ok(SignedExecutionPayloadBidRef::Heze( + &body.signed_execution_payload_bid, + )), + _ => Err(BeaconStateError::IncorrectStateVariant), + } + } + pub fn body_merkle_leaves(&self) -> Vec { let mut leaves = vec![]; match self { diff --git a/consensus/types/src/block/signed_beacon_block.rs b/consensus/types/src/block/signed_beacon_block.rs index d30a2d14da5..eecd3396264 100644 --- a/consensus/types/src/block/signed_beacon_block.rs +++ b/consensus/types/src/block/signed_beacon_block.rs @@ -360,7 +360,7 @@ impl> SignedBeaconBlock self.message() .body() .signed_execution_payload_bid() - .map(|bid| &bid.message.blob_kzg_commitments) + .map(|bid| bid.message().blob_kzg_commitments()) }) .map(|c| c.len()) .unwrap_or(0) @@ -382,7 +382,7 @@ impl> SignedBeaconBlock self.message() .body() .signed_execution_payload_bid() - .map(|bid| bid.message.block_hash) + .map(|bid| bid.message().block_hash()) } /// Convenience accessor for the block's bid's `parent_block_hash`. @@ -392,7 +392,7 @@ impl> SignedBeaconBlock self.message() .body() .signed_execution_payload_bid() - .map(|bid| bid.message.parent_block_hash) + .map(|bid| bid.message().parent_block_hash()) } /// Check if the `parent_hash` in this block's `signed_payload_bid` matches `parent_block_hash`. @@ -408,7 +408,7 @@ impl> SignedBeaconBlock // Prior to Gloas. return false; }; - signed_payload_bid.message.parent_block_hash == parent_block_hash + signed_payload_bid.message().parent_block_hash() == parent_block_hash } } diff --git a/consensus/types/src/execution/execution_payload_bid.rs b/consensus/types/src/execution/execution_payload_bid.rs index 87097bbd3bd..2c4dfc435e4 100644 --- a/consensus/types/src/execution/execution_payload_bid.rs +++ b/consensus/types/src/execution/execution_payload_bid.rs @@ -1,47 +1,150 @@ use crate::kzg_ext::KzgCommitments; -use crate::{Address, EthSpec, ExecutionBlockHash, ForkName, Hash256, SignedRoot, Slot}; -use context_deserialize::context_deserialize; +use crate::{ + Address, BeaconStateError, EthSpec, ExecutionBlockHash, ForkName, ForkVersionDecode, Hash256, + SignedRoot, Slot, +}; +use context_deserialize::{ContextDeserialize, context_deserialize}; use educe::Educe; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; +use ssz::Decode; use ssz_derive::{Decode, Encode}; +use ssz_types::BitVector; +use superstruct::superstruct; use tree_hash_derive::TreeHash; -#[derive(Default, Debug, Clone, Serialize, Encode, Decode, Deserialize, TreeHash, Educe)] +#[superstruct( + variants(Gloas, Heze), + variant_attributes( + derive( + Default, + Debug, + Clone, + Serialize, + Deserialize, + Encode, + Decode, + TreeHash, + Educe, + ), + context_deserialize(ForkName), + educe(PartialEq, Hash(bound(E: EthSpec))), + serde(bound = "E: EthSpec", deny_unknown_fields), + cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec"), + ), + ), + ref_attributes(derive(Debug, PartialEq, TreeHash), tree_hash(enum_behaviour = "transparent")), + 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") )] -#[educe(PartialEq, Hash)] -#[serde(bound = "E: EthSpec")] -#[context_deserialize(ForkName)] -// https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#executionpayloadbid +#[derive(Debug, Clone, Serialize, Deserialize, Encode, TreeHash, Educe)] +#[educe(PartialEq, Hash(bound(E: EthSpec)))] +#[serde(bound = "E: EthSpec", untagged)] +#[ssz(enum_behaviour = "transparent")] +#[tree_hash(enum_behaviour = "transparent")] +// https://github.com/ethereum/consensus-specs/blob/master/specs/heze/beacon-chain.md#executionpayloadbid pub struct ExecutionPayloadBid { + #[superstruct(getter(copy))] pub parent_block_hash: ExecutionBlockHash, + #[superstruct(getter(copy))] pub parent_block_root: Hash256, + #[superstruct(getter(copy))] pub block_hash: ExecutionBlockHash, + #[superstruct(getter(copy))] pub prev_randao: Hash256, + #[superstruct(getter(copy))] #[serde(with = "serde_utils::address_hex")] pub fee_recipient: Address, + #[superstruct(getter(copy))] #[serde(with = "serde_utils::quoted_u64")] pub gas_limit: u64, + #[superstruct(getter(copy))] #[serde(with = "serde_utils::quoted_u64")] pub builder_index: u64, + #[superstruct(getter(copy))] pub slot: Slot, + #[superstruct(getter(copy))] #[serde(with = "serde_utils::quoted_u64")] pub value: u64, + #[superstruct(getter(copy))] #[serde(with = "serde_utils::quoted_u64")] pub execution_payment: u64, pub blob_kzg_commitments: KzgCommitments, + #[superstruct(getter(copy))] pub execution_requests_root: Hash256, + // [New in Heze:EIP7805] + #[superstruct(only(Heze))] + pub inclusion_list_bits: BitVector, } impl SignedRoot for ExecutionPayloadBid {} +impl<'a, E: EthSpec> SignedRoot for ExecutionPayloadBidRef<'a, E> {} + +impl ForkVersionDecode for ExecutionPayloadBid { + fn from_ssz_bytes_by_fork(bytes: &[u8], fork_name: ForkName) -> Result { + match fork_name { + ForkName::Gloas => ExecutionPayloadBidGloas::from_ssz_bytes(bytes).map(Self::Gloas), + ForkName::Heze => ExecutionPayloadBidHeze::from_ssz_bytes(bytes).map(Self::Heze), + _ => Err(ssz::DecodeError::BytesInvalid(format!( + "unsupported fork for ExecutionPayloadBid: {fork_name}" + ))), + } + } +} + +impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for ExecutionPayloadBid { + fn context_deserialize(deserializer: D, context: ForkName) -> Result + where + D: Deserializer<'de>, + { + let convert_err = |e| { + serde::de::Error::custom(format!( + "ExecutionPayloadBid failed to deserialize: {:?}", + e + )) + }; + Ok(match context { + ForkName::Gloas => { + Self::Gloas(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Heze => { + Self::Heze(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + _ => { + return Err(serde::de::Error::custom(format!( + "ExecutionPayloadBid failed to deserialize: unsupported fork '{}'", + context + ))); + } + }) + } +} + +#[cfg(test)] +mod gloas_tests { + use super::*; + use crate::MainnetEthSpec; + + ssz_and_tree_hash_tests!(ExecutionPayloadBidGloas); +} #[cfg(test)] -mod tests { +mod heze_tests { use super::*; use crate::MainnetEthSpec; - ssz_and_tree_hash_tests!(ExecutionPayloadBid); + ssz_and_tree_hash_tests!(ExecutionPayloadBidHeze); } diff --git a/consensus/types/src/execution/mod.rs b/consensus/types/src/execution/mod.rs index dc45bd94f97..cb76d0e1111 100644 --- a/consensus/types/src/execution/mod.rs +++ b/consensus/types/src/execution/mod.rs @@ -23,7 +23,10 @@ pub use execution_payload::{ ExecutionPayloadElectra, ExecutionPayloadFulu, ExecutionPayloadGloas, ExecutionPayloadHeze, ExecutionPayloadRef, Transaction, Transactions, }; -pub use execution_payload_bid::ExecutionPayloadBid; +pub use execution_payload_bid::{ + ExecutionPayloadBid, ExecutionPayloadBidGloas, ExecutionPayloadBidHeze, ExecutionPayloadBidRef, + ExecutionPayloadBidRefMut, +}; pub use execution_payload_envelope::ExecutionPayloadEnvelope; pub use execution_payload_header::{ ExecutionPayloadHeader, ExecutionPayloadHeaderBellatrix, ExecutionPayloadHeaderCapella, @@ -44,6 +47,9 @@ pub use payload::{ OwnedExecPayload, }; pub use signed_bls_to_execution_change::SignedBlsToExecutionChange; -pub use signed_execution_payload_bid::SignedExecutionPayloadBid; +pub use signed_execution_payload_bid::{ + SignedExecutionPayloadBid, SignedExecutionPayloadBidGloas, SignedExecutionPayloadBidHeze, + SignedExecutionPayloadBidRef, +}; pub use signed_execution_payload_envelope::SignedExecutionPayloadEnvelope; pub use signed_inclusion_list::SignedInclusionList; diff --git a/consensus/types/src/execution/signed_execution_payload_bid.rs b/consensus/types/src/execution/signed_execution_payload_bid.rs index 7dd6a88a1a9..3f152c2ce66 100644 --- a/consensus/types/src/execution/signed_execution_payload_bid.rs +++ b/consensus/types/src/execution/signed_execution_payload_bid.rs @@ -1,52 +1,166 @@ -use crate::execution::ExecutionPayloadBid; -use crate::{EthSpec, ForkName}; +use crate::execution::{ExecutionPayloadBidGloas, ExecutionPayloadBidHeze, ExecutionPayloadBidRef}; +use crate::{EthSpec, ForkName, ForkVersionDecode}; use bls::Signature; -use context_deserialize::context_deserialize; +use context_deserialize::{ContextDeserialize, context_deserialize}; use educe::Educe; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; +use ssz::Decode; use ssz_derive::{Decode, Encode}; +use superstruct::superstruct; use tree_hash_derive::TreeHash; -#[derive(TreeHash, Debug, Clone, Encode, Decode, Serialize, Deserialize, Educe)] +/// An `ExecutionPayloadBid` and a signature from its builder. +#[superstruct( + variants(Gloas, Heze), + variant_attributes( + derive(Debug, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, Educe), + educe(PartialEq, Hash(bound(E: EthSpec))), + serde(bound = "E: EthSpec"), + cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec"), + ), + context_deserialize(ForkName), + ) +)] #[cfg_attr( feature = "arbitrary", derive(arbitrary::Arbitrary), arbitrary(bound = "E: EthSpec") )] -#[educe(PartialEq, Hash)] -#[serde(bound = "E: EthSpec")] -#[context_deserialize(ForkName)] -// https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#signedexecutionpayloadbid +#[derive(Debug, Clone, Serialize, Deserialize, Encode, TreeHash, Educe)] +#[educe(PartialEq, Hash(bound(E: EthSpec)))] +#[serde(bound = "E: EthSpec", untagged)] +#[ssz(enum_behaviour = "transparent")] +#[tree_hash(enum_behaviour = "transparent")] +// https://github.com/ethereum/consensus-specs/blob/master/specs/heze/beacon-chain.md#signedexecutionpayloadbid pub struct SignedExecutionPayloadBid { - pub message: ExecutionPayloadBid, + #[superstruct(only(Gloas), partial_getter(rename = "message_gloas"))] + pub message: ExecutionPayloadBidGloas, + #[superstruct(only(Heze), partial_getter(rename = "message_heze"))] + pub message: ExecutionPayloadBidHeze, pub signature: Signature, } impl SignedExecutionPayloadBid { - pub fn epoch(&self) -> crate::Epoch { - self.message.slot.epoch(E::slots_per_epoch()) + /// The bid message as a fork-agnostic reference. + pub fn message(&self) -> ExecutionPayloadBidRef<'_, E> { + match self { + Self::Gloas(inner) => ExecutionPayloadBidRef::Gloas(&inner.message), + Self::Heze(inner) => ExecutionPayloadBidRef::Heze(&inner.message), + } } pub fn slot(&self) -> crate::Slot { - self.message.slot + self.message().slot() + } + + pub fn epoch(&self) -> crate::Epoch { + self.slot().epoch(E::slots_per_epoch()) + } + + pub fn num_blobs_expected(&self) -> usize { + self.message().blob_kzg_commitments().len() } +} +impl SignedExecutionPayloadBidGloas { pub fn empty() -> Self { Self { - message: ExecutionPayloadBid::default(), + message: ExecutionPayloadBidGloas::default(), signature: Signature::empty(), } } +} - pub fn num_blobs_expected(&self) -> usize { - self.message.blob_kzg_commitments.len() +impl SignedExecutionPayloadBidHeze { + pub fn empty() -> Self { + Self { + message: ExecutionPayloadBidHeze::default(), + signature: Signature::empty(), + } + } +} + +impl<'a, E: EthSpec> SignedExecutionPayloadBidRef<'a, E> { + /// The bid message as a fork-agnostic reference. + pub fn message(&self) -> ExecutionPayloadBidRef<'a, E> { + match self { + SignedExecutionPayloadBidRef::Gloas(inner) => { + ExecutionPayloadBidRef::Gloas(&inner.message) + } + SignedExecutionPayloadBidRef::Heze(inner) => { + ExecutionPayloadBidRef::Heze(&inner.message) + } + } + } + + /// Clone this reference into an owned `SignedExecutionPayloadBid`. + pub fn clone_as_signed_execution_payload_bid(self) -> SignedExecutionPayloadBid { + match self { + SignedExecutionPayloadBidRef::Gloas(bid) => { + SignedExecutionPayloadBid::Gloas(bid.clone()) + } + SignedExecutionPayloadBidRef::Heze(bid) => SignedExecutionPayloadBid::Heze(bid.clone()), + } + } +} + +impl ForkVersionDecode for SignedExecutionPayloadBid { + fn from_ssz_bytes_by_fork(bytes: &[u8], fork_name: ForkName) -> Result { + match fork_name { + ForkName::Gloas => { + SignedExecutionPayloadBidGloas::from_ssz_bytes(bytes).map(Self::Gloas) + } + ForkName::Heze => SignedExecutionPayloadBidHeze::from_ssz_bytes(bytes).map(Self::Heze), + _ => Err(ssz::DecodeError::BytesInvalid(format!( + "unsupported fork for SignedExecutionPayloadBid: {fork_name}" + ))), + } } } +impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for SignedExecutionPayloadBid { + fn context_deserialize(deserializer: D, context: ForkName) -> Result + where + D: Deserializer<'de>, + { + let convert_err = |e| { + serde::de::Error::custom(format!( + "SignedExecutionPayloadBid failed to deserialize: {:?}", + e + )) + }; + Ok(match context { + ForkName::Gloas => { + Self::Gloas(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Heze => { + Self::Heze(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + _ => { + return Err(serde::de::Error::custom(format!( + "SignedExecutionPayloadBid failed to deserialize: unsupported fork '{}'", + context + ))); + } + }) + } +} + +#[cfg(test)] +mod gloas_tests { + use super::*; + use crate::MainnetEthSpec; + + ssz_and_tree_hash_tests!(SignedExecutionPayloadBidGloas); +} + #[cfg(test)] -mod tests { +mod heze_tests { use super::*; use crate::MainnetEthSpec; - ssz_and_tree_hash_tests!(SignedExecutionPayloadBid); + ssz_and_tree_hash_tests!(SignedExecutionPayloadBidHeze); } diff --git a/consensus/types/src/state/beacon_state.rs b/consensus/types/src/state/beacon_state.rs index 2c5628a5efd..3d22dc34737 100644 --- a/consensus/types/src/state/beacon_state.rs +++ b/consensus/types/src/state/beacon_state.rs @@ -23,7 +23,8 @@ use tree_hash_derive::TreeHash; use typenum::Unsigned; use crate::{ - Address, ExecutionBlockHash, ExecutionPayloadBid, Withdrawal, + Address, ExecutionBlockHash, ExecutionPayloadBidGloas, ExecutionPayloadBidHeze, + ExecutionPayloadBidRef, ExecutionPayloadBidRefMut, Withdrawal, attestation::{ AttestationData, AttestationDuty, BeaconCommittee, Checkpoint, CommitteeIndex, PTC, ParticipationFlags, PendingAttestation, @@ -670,9 +671,19 @@ where List, #[cfg_attr(feature = "arbitrary", arbitrary(default))] - #[superstruct(only(Gloas, Heze))] + #[superstruct( + only(Gloas), + partial_getter(rename = "latest_execution_payload_bid_gloas") + )] #[metastruct(exclude_from(tree_lists))] - pub latest_execution_payload_bid: ExecutionPayloadBid, + pub latest_execution_payload_bid: ExecutionPayloadBidGloas, + #[cfg_attr(feature = "arbitrary", arbitrary(default))] + #[superstruct( + only(Heze), + partial_getter(rename = "latest_execution_payload_bid_heze") + )] + #[metastruct(exclude_from(tree_lists))] + pub latest_execution_payload_bid: ExecutionPayloadBidHeze, #[compare_fields(as_iter)] #[cfg_attr(feature = "arbitrary", arbitrary(default))] @@ -1300,6 +1311,35 @@ impl BeaconState { } } + /// Convenience accessor for `latest_execution_payload_bid` as an `ExecutionPayloadBidRef`. + pub fn latest_execution_payload_bid( + &self, + ) -> Result, BeaconStateError> { + match self { + BeaconState::Gloas(state) => Ok(ExecutionPayloadBidRef::Gloas( + &state.latest_execution_payload_bid, + )), + BeaconState::Heze(state) => Ok(ExecutionPayloadBidRef::Heze( + &state.latest_execution_payload_bid, + )), + _ => Err(BeaconStateError::IncorrectStateVariant), + } + } + + pub fn latest_execution_payload_bid_mut( + &mut self, + ) -> Result, BeaconStateError> { + match self { + BeaconState::Gloas(state) => Ok(ExecutionPayloadBidRefMut::Gloas( + &mut state.latest_execution_payload_bid, + )), + BeaconState::Heze(state) => Ok(ExecutionPayloadBidRefMut::Heze( + &mut state.latest_execution_payload_bid, + )), + _ => Err(BeaconStateError::IncorrectStateVariant), + } + } + /// Return `true` if the validator who produced `slot_signature` is eligible to aggregate. /// /// Spec v0.12.1 diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index 81c8b7bc128..75279c4d19d 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -545,8 +545,11 @@ impl Operation for ExecutionPayloadBidBlock { fork_name.gloas_enabled() } - fn decode(path: &Path, _fork_name: ForkName, _spec: &ChainSpec) -> Result { - ssz_decode_file(path).map(|signed_bid| ExecutionPayloadBidBlock { signed_bid }) + fn decode(path: &Path, fork_name: ForkName, _spec: &ChainSpec) -> Result { + ssz_decode_file_with(path, |bytes| { + SignedExecutionPayloadBid::from_ssz_bytes_by_fork(bytes, fork_name) + }) + .map(|signed_bid| ExecutionPayloadBidBlock { signed_bid }) } fn apply_to( @@ -555,7 +558,12 @@ impl Operation for ExecutionPayloadBidBlock { spec: &ChainSpec, _: &Operations, ) -> Result<(), BlockProcessingError> { - process_execution_payload_bid(state, &self.signed_bid, VerifySignatures::True, spec)?; + process_execution_payload_bid( + state, + self.signed_bid.to_ref(), + VerifySignatures::True, + spec, + )?; Ok(()) } } diff --git a/testing/ef_tests/src/type_name.rs b/testing/ef_tests/src/type_name.rs index 836510c68fa..4bbf2d6c409 100644 --- a/testing/ef_tests/src/type_name.rs +++ b/testing/ef_tests/src/type_name.rs @@ -95,7 +95,11 @@ type_name_generic!(ExecutionPayloadHeaderDeneb, "ExecutionPayloadHeader"); type_name_generic!(ExecutionPayloadHeaderElectra, "ExecutionPayloadHeader"); type_name_generic!(ExecutionPayloadHeaderFulu, "ExecutionPayloadHeader"); type_name_generic!(ExecutionPayloadBid); +type_name_generic!(ExecutionPayloadBidGloas, "ExecutionPayloadBid"); +type_name_generic!(ExecutionPayloadBidHeze, "ExecutionPayloadBid"); type_name_generic!(SignedExecutionPayloadBid); +type_name_generic!(SignedExecutionPayloadBidGloas, "SignedExecutionPayloadBid"); +type_name_generic!(SignedExecutionPayloadBidHeze, "SignedExecutionPayloadBid"); type_name_generic!(ExecutionRequestsElectra, "ExecutionRequests"); type_name_generic!(ExecutionRequestsGloas, "ExecutionRequests"); type_name_generic!(ExecutionPayloadEnvelope); diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index 151a99a3216..19515392660 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -693,16 +693,22 @@ mod ssz_static { #[test] fn execution_payload_bid() { - SszStaticHandler::, MinimalEthSpec>::gloas_and_later() + SszStaticHandler::, MinimalEthSpec>::gloas_only() .run(); - SszStaticHandler::, MainnetEthSpec>::gloas_and_later() + SszStaticHandler::, MainnetEthSpec>::gloas_only() + .run(); + SszStaticHandler::, MinimalEthSpec>::heze_only() + .run(); + SszStaticHandler::, MainnetEthSpec>::heze_only() .run(); } #[test] fn signed_execution_payload_bid() { - SszStaticHandler::, MinimalEthSpec>::gloas_and_later().run(); - SszStaticHandler::, MainnetEthSpec>::gloas_and_later().run(); + SszStaticHandler::, MinimalEthSpec>::gloas_only().run(); + SszStaticHandler::, MainnetEthSpec>::gloas_only().run(); + SszStaticHandler::, MinimalEthSpec>::heze_only().run(); + SszStaticHandler::, MainnetEthSpec>::heze_only().run(); } #[test] From aafc704511730016ce0919bc6d4733267f12844a Mon Sep 17 00:00:00 2001 From: conache Date: Tue, 14 Jul 2026 19:42:50 +0300 Subject: [PATCH 09/44] Remove unused imports --- testing/ef_tests/tests/tests.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index 19515392660..42473ea0df1 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -285,11 +285,10 @@ mod ssz_static { use types::state::HistoricalSummary; use types::{ AttesterSlashingBase, AttesterSlashingElectra, Builder, BuilderPendingPayment, - BuilderPendingWithdrawal, ConsolidationRequest, DepositRequest, ExecutionPayloadBid, - ExecutionPayloadEnvelope, IndexedPayloadAttestation, LightClientBootstrapAltair, - PayloadAttestation, PayloadAttestationData, PayloadAttestationMessage, PendingDeposit, - PendingPartialWithdrawal, SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, - WithdrawalRequest, *, + BuilderPendingWithdrawal, ConsolidationRequest, DepositRequest, ExecutionPayloadEnvelope, + IndexedPayloadAttestation, LightClientBootstrapAltair, PayloadAttestation, + PayloadAttestationData, PayloadAttestationMessage, PendingDeposit, + PendingPartialWithdrawal, SignedExecutionPayloadEnvelope, WithdrawalRequest, *, }; ssz_static_test!(attestation_data, AttestationData); From 4e52244946bc80bb150ef94902231d60c328170a Mon Sep 17 00:00:00 2001 From: conache Date: Wed, 5 Aug 2026 12:01:04 +0300 Subject: [PATCH 10/44] Fix tests after merge --- .../beacon_chain/src/block_production/gloas.rs | 13 ++++++------- .../gossip_verified_envelope.rs | 4 +++- .../src/payload_envelope_verification/mod.rs | 4 ++-- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/beacon_node/beacon_chain/src/block_production/gloas.rs b/beacon_node/beacon_chain/src/block_production/gloas.rs index b16d3c92287..be4f72981ae 100644 --- a/beacon_node/beacon_chain/src/block_production/gloas.rs +++ b/beacon_node/beacon_chain/src/block_production/gloas.rs @@ -31,13 +31,12 @@ use types::consts::gloas::BUILDER_INDEX_SELF_BUILD; use types::{ Address, Attestation, AttestationGloas, AttesterSlashing, AttesterSlashingGloas, BeaconBlock, BeaconBlockBodyGloas, BeaconBlockGloas, BeaconState, BeaconStateError, BlobsList, BuilderIndex, - ChainSpec, Deposit, Eth1Data, EthSpec, ExecutionBlockHash, - ExecutionPayloadBidGloas, ExecutionPayloadEnvelope, ExecutionPayloadGloas, - ExecutionRequestsGloas, FullPayload, Graffiti, Hash256, IndexedAttestation, KzgProofs, - PayloadAttestation, ProposerSlashing, RelativeEpoch, SignedBeaconBlock, - SignedBlsToExecutionChange, SignedExecutionPayloadBid, SignedExecutionPayloadBidGloas, - SignedExecutionPayloadEnvelope, SignedVoluntaryExit, Slot, SyncAggregate, Uint256, Withdrawal, - Withdrawals, + ChainSpec, Deposit, Eth1Data, EthSpec, ExecutionBlockHash, ExecutionPayloadBidGloas, + ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequestsGloas, FullPayload, Graffiti, + Hash256, IndexedAttestation, KzgProofs, PayloadAttestation, ProposerSlashing, RelativeEpoch, + SignedBeaconBlock, SignedBlsToExecutionChange, SignedExecutionPayloadBid, + SignedExecutionPayloadBidGloas, SignedExecutionPayloadEnvelope, SignedVoluntaryExit, Slot, + SyncAggregate, Uint256, Withdrawal, Withdrawals, }; use crate::pending_payload_envelopes::PendingEnvelopeData; 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 9a3cd772702..c0e7ce87d69 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 @@ -480,7 +480,9 @@ mod tests { // Not a gossip condition; the sync-only rejection is inlined in // `RangeSyncBlock` construction. - assert!(verify_envelope_consistency::(&envelope, &block, &bid, Slot::new(0)).is_ok()); + assert!( + verify_envelope_consistency::(&envelope, &block, bid.to_ref(), Slot::new(0)).is_ok() + ); } #[test] diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs index 82493e01bd1..0b9299d7698 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs @@ -432,7 +432,7 @@ mod payload_hash_tests { use types::{ BeaconBlock, BeaconBlockBodyGloas, BeaconBlockGloas, Eth1Data, ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionPayloadRef, ExecutionRequestsGloas, ExecutionRequestsRef, - Graffiti, Hash256, MinimalEthSpec, SignedBeaconBlock, SignedExecutionPayloadBid, + Graffiti, Hash256, MinimalEthSpec, SignedBeaconBlock, SignedExecutionPayloadBidGloas, SignedExecutionPayloadEnvelope, Slot, SyncAggregate, }; @@ -460,7 +460,7 @@ mod payload_hash_tests { sync_aggregate: SyncAggregate::empty(), bls_to_execution_changes: ProgressiveVariableList::empty(), parent_execution_requests: ExecutionRequestsGloas::default(), - signed_execution_payload_bid: SignedExecutionPayloadBid::empty(), + signed_execution_payload_bid: SignedExecutionPayloadBidGloas::empty(), payload_attestations: ProgressiveVariableList::empty(), _phantom: PhantomData, }, From 04b38411e8c56fafba02b0e7cfb0bb97a6af3db7 Mon Sep 17 00:00:00 2001 From: conache Date: Thu, 6 Aug 2026 13:25:09 +0300 Subject: [PATCH 11/44] Fix tests after merge --- .../beacon_chain/tests/block_verification.rs | 35 +++++++++++-------- .../src/sync/block_sidecar_coupling.rs | 2 +- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index 4f962511909..126c30fe9c9 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -3032,12 +3032,13 @@ async fn process_chain_segment_rejects_envelope_with_invalid_signature() { .message() .body() .signed_execution_payload_bid() - .unwrap(); + .unwrap() + .clone_as_signed_execution_payload_bid(); let available_envelope = AvailableEnvelope::new( Arc::new(envelope), columns, - bid, + &bid, &harness.chain.custody_context, ) .unwrap(); @@ -3097,11 +3098,12 @@ async fn process_chain_segment_partial_import_with_invalid_envelope() { .message() .body() .signed_execution_payload_bid() - .unwrap(); + .unwrap() + .clone_as_signed_execution_payload_bid(); let available_envelope1 = AvailableEnvelope::new( envelope1, columns1, - bid1, + &bid1, &import_harness.chain.custody_context, ) .unwrap(); @@ -3125,11 +3127,12 @@ async fn process_chain_segment_partial_import_with_invalid_envelope() { .message() .body() .signed_execution_payload_bid() - .unwrap(); + .unwrap() + .clone_as_signed_execution_payload_bid(); let available_envelope2 = AvailableEnvelope::new( Arc::new(envelope2), columns2, - bid2, + &bid2, &import_harness.chain.custody_context, ) .unwrap(); @@ -3221,11 +3224,12 @@ async fn process_chain_segment_import_fails_without_parent_envelope() { .message() .body() .signed_execution_payload_bid() - .unwrap(); + .unwrap() + .clone_as_signed_execution_payload_bid(); let available_envelope2 = AvailableEnvelope::new( envelope2, columns2, - bid2, + &bid2, &import_harness.chain.custody_context, ) .unwrap(); @@ -3316,11 +3320,12 @@ async fn process_chain_segment_full_and_empty_transitions() { .message() .body() .signed_execution_payload_bid() - .unwrap(); + .unwrap() + .clone_as_signed_execution_payload_bid(); let available_envelope1 = AvailableEnvelope::new( envelope1, columns1, - bid1, + &bid1, &import_harness.chain.custody_context, ) .unwrap(); @@ -3344,11 +3349,12 @@ async fn process_chain_segment_full_and_empty_transitions() { .message() .body() .signed_execution_payload_bid() - .unwrap(); + .unwrap() + .clone_as_signed_execution_payload_bid(); let available_envelope3 = AvailableEnvelope::new( Arc::new(envelope3), columns3, - bid3, + &bid3, &import_harness.chain.custody_context, ) .unwrap(); @@ -3433,11 +3439,12 @@ async fn process_chain_segment_rejects_envelope_with_invalid_columns() { .message() .body() .signed_execution_payload_bid() - .unwrap(); + .unwrap() + .clone_as_signed_execution_payload_bid(); let available_envelope = AvailableEnvelope::new( envelope, columns, - bid, + &bid, &import_harness.chain.custody_context, ) .unwrap(); diff --git a/beacon_node/network/src/sync/block_sidecar_coupling.rs b/beacon_node/network/src/sync/block_sidecar_coupling.rs index 24a08011ce4..41732d60e93 100644 --- a/beacon_node/network/src/sync/block_sidecar_coupling.rs +++ b/beacon_node/network/src/sync/block_sidecar_coupling.rs @@ -652,7 +652,7 @@ mod tests { if let Ok(bid) = block .message_mut() .body_mut() - .signed_execution_payload_bid_mut() + .signed_execution_payload_bid_gloas_mut() { bid.message.execution_requests_root = ExecutionRequestsGloas::::default().tree_hash_root(); From 106e9471b43391d21bb9d031f1401a3b3c52e4c1 Mon Sep 17 00:00:00 2001 From: conache Date: Thu, 6 Aug 2026 14:38:37 +0300 Subject: [PATCH 12/44] Add test ensuring il bits are covered in Heze bid signing_root --- .../src/execution/execution_payload_bid.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/consensus/types/src/execution/execution_payload_bid.rs b/consensus/types/src/execution/execution_payload_bid.rs index 2fdb803c493..1aaf5dd5a77 100644 --- a/consensus/types/src/execution/execution_payload_bid.rs +++ b/consensus/types/src/execution/execution_payload_bid.rs @@ -164,4 +164,21 @@ mod heze_tests { use crate::MainnetEthSpec; ssz_and_tree_hash_tests!(ExecutionPayloadBidHeze); + + #[test] + fn inclusion_list_bits_committed_to_signing_root() { + let bid = ExecutionPayloadBidHeze::::default(); + let mut bid_with_bits = bid.clone(); + bid_with_bits + .inclusion_list_bits + .set(0, true) + .expect("bit index should be within the committee size"); + + let domain = Hash256::ZERO; + assert_ne!( + ExecutionPayloadBidRef::Heze(&bid).signing_root(domain), + ExecutionPayloadBidRef::Heze(&bid_with_bits).signing_root(domain), + "inclusion_list_bits must be covered by the Heze bid signing root", + ); + } } From 81251c23be658c0104f9abc79dd40d9ff1f42a8d Mon Sep 17 00:00:00 2001 From: conache Date: Mon, 24 Aug 2026 16:58:15 +0300 Subject: [PATCH 13/44] Fix types in test files --- beacon_node/http_api/tests/tests.rs | 8 ++++---- .../src/per_block_processing/tests.rs | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 705e7ff8cc1..d6e9478c5da 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -50,10 +50,10 @@ use tokio::time::Duration; use tree_hash::TreeHash; use types::ApplicationDomain; use types::{ - Address, Builder, Domain, EthSpec, ExecutionBlockHash, ExecutionPayloadBid, - ExecutionPayloadBidGloas, Hash256, MainnetEthSpec, ProposerPreferences, RelativeEpoch, - SelectionProof, SignedExecutionPayloadBid, SignedExecutionPayloadBidGloas, - SignedExecutionPayloadEnvelope, SignedProposerPreferences, SignedRoot, SingleAttestation, Slot, + Address, Builder, Domain, EthSpec, ExecutionBlockHash, ExecutionPayloadBidGloas, Hash256, + MainnetEthSpec, ProposerPreferences, RelativeEpoch, SelectionProof, SignedExecutionPayloadBid, + SignedExecutionPayloadBidGloas, SignedExecutionPayloadEnvelope, SignedProposerPreferences, + SignedRoot, SingleAttestation, Slot, attestation::AttestationBase, consts::gloas::{BUILDER_INDEX_SELF_BUILD, PAYLOAD_BUILDER_VERSION}, }; diff --git a/consensus/state_processing/src/per_block_processing/tests.rs b/consensus/state_processing/src/per_block_processing/tests.rs index 4b21c108963..ae322193363 100644 --- a/consensus/state_processing/src/per_block_processing/tests.rs +++ b/consensus/state_processing/src/per_block_processing/tests.rs @@ -444,7 +444,7 @@ async fn invalid_attestation_no_committee_for_index() { let parent_slot = state .latest_execution_payload_bid() .ok() - .map(|bid| bid.slot); + .map(|bid| bid.slot()); let result = process_operations::process_attestations( &mut state, head_block.body(), @@ -499,7 +499,7 @@ async fn invalid_attestation_wrong_justified_checkpoint() { let parent_slot = state .latest_execution_payload_bid() .ok() - .map(|bid| bid.slot); + .map(|bid| bid.slot()); let result = process_operations::process_attestations( &mut state, head_block.body(), @@ -558,7 +558,7 @@ async fn invalid_attestation_bad_aggregation_bitfield_len() { let parent_slot = state .latest_execution_payload_bid() .ok() - .map(|bid| bid.slot); + .map(|bid| bid.slot()); let result = process_operations::process_attestations( &mut state, head_block.body(), @@ -603,7 +603,7 @@ async fn invalid_attestation_bad_signature() { let parent_slot = state .latest_execution_payload_bid() .ok() - .map(|bid| bid.slot); + .map(|bid| bid.slot()); let result = process_operations::process_attestations( &mut state, head_block.body(), @@ -652,7 +652,7 @@ async fn invalid_attestation_included_too_early() { let parent_slot = state .latest_execution_payload_bid() .ok() - .map(|bid| bid.slot); + .map(|bid| bid.slot()); let result = process_operations::process_attestations( &mut state, head_block.body(), @@ -708,7 +708,7 @@ async fn invalid_attestation_target_epoch_slot_mismatch() { let parent_slot = state .latest_execution_payload_bid() .ok() - .map(|bid| bid.slot); + .map(|bid| bid.slot()); let result = process_operations::process_attestations( &mut state, head_block.body(), From 0a5f7f035b895a257b61ab1906a340b5f2473b8c Mon Sep 17 00:00:00 2001 From: conache Date: Fri, 4 Sep 2026 13:14:10 +0300 Subject: [PATCH 14/44] Fix beacon state initialization from genesis data for Heze --- consensus/state_processing/src/genesis.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/consensus/state_processing/src/genesis.rs b/consensus/state_processing/src/genesis.rs index 93377c2c16d..cc3fd43ca52 100644 --- a/consensus/state_processing/src/genesis.rs +++ b/consensus/state_processing/src/genesis.rs @@ -176,12 +176,6 @@ pub fn initialize_beacon_state_from_eth1( let el_genesis_hash = bid.block_hash; bid.parent_block_hash = el_genesis_hash; bid.block_hash = ExecutionBlockHash::default(); - - // Update the `latest_block_header.body_root` so that it matches the body of the - // Gloas genesis block, which embeds `state.latest_execution_payload_bid` in its - // `signed_execution_payload_bid` field (see `genesis_block`). - let genesis_body_root = genesis_block(&state, spec)?.body_root(); - state.latest_block_header_mut().body_root = genesis_body_root; } // Upgrade to heze if configured from genesis. @@ -195,6 +189,14 @@ pub fn initialize_beacon_state_from_eth1( state.fork_mut().previous_version = spec.heze_fork_version; } + // From Gloas onwards the genesis block body embeds `state.latest_execution_payload_bid` + // (see `genesis_block`), so `latest_block_header.body_root` must be computed against the + // final genesis fork, after every upgrade has run + if state.fork_name_unchecked().gloas_enabled() { + let genesis_body_root = genesis_block(&state, spec)?.body_root(); + state.latest_block_header_mut().body_root = genesis_body_root; + } + // Now that we have our validators, initialize the caches (including the committees) state.build_caches(spec)?; From 7b03c923cea0ba969ca28dae4c0555f64eab3d6c Mon Sep 17 00:00:00 2001 From: conache Date: Tue, 15 Sep 2026 17:56:33 +0300 Subject: [PATCH 15/44] Explicitly handle each fork variant when generating unsigned genesis beacon block --- consensus/state_processing/src/genesis.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/consensus/state_processing/src/genesis.rs b/consensus/state_processing/src/genesis.rs index cc3fd43ca52..725942b4817 100644 --- a/consensus/state_processing/src/genesis.rs +++ b/consensus/state_processing/src/genesis.rs @@ -221,6 +221,13 @@ pub fn genesis_block( ) -> Result, BeaconStateError> { let mut block = BeaconBlock::empty(spec); match &mut block { + BeaconBlock::Base(_) + | BeaconBlock::Altair(_) + | BeaconBlock::Bellatrix(_) + | BeaconBlock::Capella(_) + | BeaconBlock::Deneb(_) + | BeaconBlock::Electra(_) + | BeaconBlock::Fulu(_) => {} BeaconBlock::Gloas(gloas_block) => { gloas_block.body.signed_execution_payload_bid.message = state.latest_execution_payload_bid_gloas()?.clone(); @@ -229,7 +236,6 @@ pub fn genesis_block( heze_block.body.signed_execution_payload_bid.message = state.latest_execution_payload_bid_heze()?.clone(); } - _ => {} } Ok(block) } From 078bad11083df6fb54b55626d8ac147a22082075 Mon Sep 17 00:00:00 2001 From: conache Date: Tue, 15 Sep 2026 18:50:18 +0300 Subject: [PATCH 16/44] Decode execution payload bids based on the consensus version header --- .../src/beacon/execution_payload_bids.rs | 21 ++++++++++++++----- beacon_node/http_api/src/lib.rs | 4 +++- beacon_node/http_api/src/utils.rs | 1 + 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/beacon_node/http_api/src/beacon/execution_payload_bids.rs b/beacon_node/http_api/src/beacon/execution_payload_bids.rs index 721ffbf10b7..739588802b2 100644 --- a/beacon_node/http_api/src/beacon/execution_payload_bids.rs +++ b/beacon_node/http_api/src/beacon/execution_payload_bids.rs @@ -1,11 +1,11 @@ use crate::task_spawner::{Priority, TaskSpawner}; use crate::utils::{ - ChainFilter, EthV1Filter, NetworkTxFilter, ResponseFilter, TaskSpawnerFilter, - publish_pubsub_message, + ChainFilter, ConsensusVersionHeaderFilter, EthV1Filter, NetworkTxFilter, ResponseFilter, + TaskSpawnerFilter, publish_pubsub_message, }; use beacon_chain::{BeaconChain, BeaconChainTypes}; use bytes::Bytes; -use eth2::CONSENSUS_VERSION_HEADER; +use context_deserialize::ContextDeserialize; use lighthouse_network::PubsubMessage; use network::NetworkMessage; use std::sync::Arc; @@ -20,6 +20,7 @@ use warp::{ // POST /eth/v1/beacon/execution_payload_bids (SSZ) pub(crate) fn post_beacon_execution_payload_bids_ssz( eth_v1: EthV1Filter, + consensus_version_header_filter: ConsensusVersionHeaderFilter, task_spawner_filter: TaskSpawnerFilter, chain_filter: ChainFilter, network_tx_filter: NetworkTxFilter, @@ -29,7 +30,7 @@ pub(crate) fn post_beacon_execution_payload_bids_ssz( .and(warp::path("execution_payload_bids")) .and(warp::path::end()) .and(warp::body::bytes()) - .and(warp::header::header::(CONSENSUS_VERSION_HEADER)) + .and(consensus_version_header_filter) .and(task_spawner_filter) .and(chain_filter) .and(network_tx_filter) @@ -57,6 +58,7 @@ pub(crate) fn post_beacon_execution_payload_bids_ssz( // POST /eth/v1/beacon/execution_payload_bids pub(crate) fn post_beacon_execution_payload_bids( eth_v1: EthV1Filter, + consensus_version_header_filter: ConsensusVersionHeaderFilter, task_spawner_filter: TaskSpawnerFilter, chain_filter: ChainFilter, network_tx_filter: NetworkTxFilter, @@ -66,15 +68,24 @@ pub(crate) fn post_beacon_execution_payload_bids( .and(warp::path("execution_payload_bids")) .and(warp::path::end()) .and(warp::body::json()) + .and(consensus_version_header_filter) .and(task_spawner_filter) .and(chain_filter) .and(network_tx_filter) .then( - |bid: SignedExecutionPayloadBid, + |value: serde_json::Value, + consensus_version: ForkName, task_spawner: TaskSpawner, chain: Arc>, network_tx: UnboundedSender>| { task_spawner.blocking_response_task(Priority::P0, move || { + let bid = SignedExecutionPayloadBid::::context_deserialize( + &value, + consensus_version, + ) + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!("invalid SSZ: {e:?}")) + })?; publish_execution_payload_bid(bid, &chain, &network_tx) }) }, diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index e73cf06c00a..297b3f83a21 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -1068,7 +1068,7 @@ pub async fn serve( .and(warp::query::()) .and(warp::path::end()) .and(warp_utils::json::json()) - .and(consensus_version_header_filter) + .and(consensus_version_header_filter.clone()) .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .and(network_tx_filter.clone()) @@ -1573,6 +1573,7 @@ pub async fn serve( // POST beacon/execution_payload_bids let post_beacon_execution_payload_bids = post_beacon_execution_payload_bids( eth_v1.clone(), + consensus_version_header_filter.clone(), task_spawner_filter.clone(), chain_filter.clone(), network_tx_filter.clone(), @@ -1581,6 +1582,7 @@ pub async fn serve( // POST beacon/execution_payload_bids (SSZ) let post_beacon_execution_payload_bids_ssz = post_beacon_execution_payload_bids_ssz( eth_v1.clone(), + consensus_version_header_filter.clone(), task_spawner_filter.clone(), chain_filter.clone(), network_tx_filter.clone(), diff --git a/beacon_node/http_api/src/utils.rs b/beacon_node/http_api/src/utils.rs index f2b859ebe59..3189fe2c506 100644 --- a/beacon_node/http_api/src/utils.rs +++ b/beacon_node/http_api/src/utils.rs @@ -21,6 +21,7 @@ pub type ValidatorSubscriptionTxFilter = BoxedFilter<(Sender = BoxedFilter<(UnboundedSender::EthSpec>>,)>; pub type OptionalConsensusVersionHeaderFilter = BoxedFilter<(Option,)>; +pub type ConsensusVersionHeaderFilter = BoxedFilter<(ForkName,)>; pub fn from_meta_data( meta_data: &RwLock>, From b4026216e629a8469cd6c8d5d2a02002df960caa Mon Sep 17 00:00:00 2001 From: conache Date: Tue, 15 Sep 2026 20:06:44 +0300 Subject: [PATCH 17/44] Make the execution payload bid size check fork-aware --- .../lighthouse_network/src/types/pubsub.rs | 118 +++++++++++++----- consensus/types/presets/gnosis/heze.yaml | 5 + consensus/types/presets/mainnet/heze.yaml | 5 + consensus/types/presets/minimal/heze.yaml | 5 + consensus/types/src/core/chain_spec.rs | 14 +++ consensus/types/src/core/eth_spec.rs | 15 --- consensus/types/src/core/preset.rs | 11 +- 7 files changed, 124 insertions(+), 49 deletions(-) diff --git a/beacon_node/lighthouse_network/src/types/pubsub.rs b/beacon_node/lighthouse_network/src/types/pubsub.rs index 6459c922899..5b8dbb5431c 100644 --- a/beacon_node/lighthouse_network/src/types/pubsub.rs +++ b/beacon_node/lighthouse_network/src/types/pubsub.rs @@ -415,27 +415,27 @@ impl PubsubMessage { ))) } GossipKind::ExecutionPayloadBid => { - if data.len() > E::max_signed_execution_payload_bid_size() { + let fork_name = *fork_context + .get_fork_from_context_bytes(gossip_topic.fork_digest) + .ok_or_else(|| { + format!( + "Unknown gossipsub fork digest: {:?}", + gossip_topic.fork_digest + ) + })?; + let max_size = fork_context + .spec + .max_signed_execution_payload_bid_size_for_fork(fork_name); + if data.len() > max_size { return Err(format!( "SignedExecutionPayloadBid size {} exceeds MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE {}", data.len(), - E::max_signed_execution_payload_bid_size() + max_size )); } - let execution_payload_bid = match fork_context - .get_fork_from_context_bytes(gossip_topic.fork_digest) - { - Some(&fork_name) => { - SignedExecutionPayloadBid::from_ssz_bytes_by_fork(data, fork_name) - .map_err(|e| format!("{:?}", e))? - } - None => { - return Err(format!( - "Unknown gossipsub fork digest: {:?}", - gossip_topic.fork_digest - )); - } - }; + let execution_payload_bid = + SignedExecutionPayloadBid::from_ssz_bytes_by_fork(data, fork_name) + .map_err(|e| format!("{:?}", e))?; Ok(PubsubMessage::ExecutionPayloadBid(Box::new( execution_payload_bid, ))) @@ -699,11 +699,12 @@ mod tests { use crate::types::OutgoingPartialColumnGloas; use libp2p::gossipsub::partial_messages::Partial; use types::data::{CellBitmap, PartialDataColumnSidecarGloas}; - use types::{Epoch, EthSpec, MainnetEthSpec, Slot, data::DataColumnSubnetId}; + use types::{ChainSpec, Epoch, EthSpec, MainnetEthSpec, Slot, data::DataColumnSubnetId}; type E = MainnetEthSpec; - fn gloas_fork_context() -> ForkContext { + /// A spec with every fork up to Fulu scheduled at genesis. + fn pre_gloas_spec() -> ChainSpec { let mut spec = E::default_spec(); spec.altair_fork_epoch = Some(Epoch::new(0)); spec.bellatrix_fork_epoch = Some(Epoch::new(0)); @@ -711,10 +712,22 @@ mod tests { spec.deneb_fork_epoch = Some(Epoch::new(0)); spec.electra_fork_epoch = Some(Epoch::new(0)); spec.fulu_fork_epoch = Some(Epoch::new(0)); + spec + } + + fn gloas_fork_context() -> ForkContext { + let mut spec = pre_gloas_spec(); spec.gloas_fork_epoch = Some(Epoch::new(0)); ForkContext::new::(Slot::new(0), Hash256::ZERO, &spec) } + fn heze_fork_context() -> ForkContext { + let mut spec = pre_gloas_spec(); + spec.gloas_fork_epoch = Some(Epoch::new(0)); + spec.heze_fork_epoch = Some(Epoch::new(0)); + ForkContext::new::(Slot::new(0), Hash256::ZERO, &spec) + } + #[test] fn gloas_group_id_round_trips_through_decode_partial() { let fork_context = gloas_fork_context(); @@ -754,8 +767,11 @@ mod tests { assert_eq!(decoded.index, column.index); } - fn decode_oversized(kind: GossipKind, size: usize) -> Result, String> { - let fork_context = gloas_fork_context(); + fn decode_oversized( + fork_context: &ForkContext, + kind: GossipKind, + size: usize, + ) -> Result, String> { let topic = GossipTopic::new( kind, GossipEncoding::default(), @@ -763,15 +779,25 @@ mod tests { ); let topic_hash = TopicHash::from_raw(String::from(topic)); let data = vec![0u8; size]; - PubsubMessage::decode(&topic_hash, &data, &fork_context) + PubsubMessage::decode(&topic_hash, &data, fork_context) } #[test] fn gloas_aggregate_and_proof_size_bound() { let max = E::max_signed_aggregate_and_proof_size(); - let err = decode_oversized(GossipKind::BeaconAggregateAndProof, max + 1).unwrap_err(); + let err = decode_oversized( + &gloas_fork_context(), + GossipKind::BeaconAggregateAndProof, + max + 1, + ) + .unwrap_err(); assert!(err.contains("MAX_SIGNED_AGGREGATE_AND_PROOF_SIZE"), "{err}"); - let err = decode_oversized(GossipKind::BeaconAggregateAndProof, max).unwrap_err(); + let err = decode_oversized( + &gloas_fork_context(), + GossipKind::BeaconAggregateAndProof, + max, + ) + .unwrap_err(); assert!( !err.contains("MAX_SIGNED_AGGREGATE_AND_PROOF_SIZE"), "{err}" @@ -781,9 +807,11 @@ mod tests { #[test] fn gloas_attester_slashing_size_bound() { let max = E::max_attester_slashing_size(); - let err = decode_oversized(GossipKind::AttesterSlashing, max + 1).unwrap_err(); + let err = decode_oversized(&gloas_fork_context(), GossipKind::AttesterSlashing, max + 1) + .unwrap_err(); assert!(err.contains("MAX_ATTESTER_SLASHING_SIZE"), "{err}"); - let err = decode_oversized(GossipKind::AttesterSlashing, max).unwrap_err(); + let err = + decode_oversized(&gloas_fork_context(), GossipKind::AttesterSlashing, max).unwrap_err(); assert!(!err.contains("MAX_ATTESTER_SLASHING_SIZE"), "{err}"); } @@ -791,9 +819,9 @@ mod tests { fn gloas_data_column_sidecar_size_bound() { let max = E::max_data_column_sidecar_size(); let kind = GossipKind::DataColumnSidecar(DataColumnSubnetId::new(0)); - let err = decode_oversized(kind.clone(), max + 1).unwrap_err(); + let err = decode_oversized(&gloas_fork_context(), kind.clone(), max + 1).unwrap_err(); assert!(err.contains("MAX_DATA_COLUMN_SIDECAR_SIZE"), "{err}"); - let err = decode_oversized(kind, max).unwrap_err(); + let err = decode_oversized(&gloas_fork_context(), kind, max).unwrap_err(); assert!(!err.contains("MAX_DATA_COLUMN_SIDECAR_SIZE"), "{err}"); } @@ -829,13 +857,43 @@ mod tests { #[test] fn gloas_execution_payload_bid_size_bound() { - let max = E::max_signed_execution_payload_bid_size(); - let err = decode_oversized(GossipKind::ExecutionPayloadBid, max + 1).unwrap_err(); + let max = gloas_fork_context() + .spec + .max_signed_execution_payload_bid_size_for_fork(ForkName::Gloas); + let err = decode_oversized( + &gloas_fork_context(), + GossipKind::ExecutionPayloadBid, + max + 1, + ) + .unwrap_err(); + assert!( + err.contains("MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE"), + "{err}" + ); + let err = decode_oversized(&gloas_fork_context(), GossipKind::ExecutionPayloadBid, max) + .unwrap_err(); + assert!( + !err.contains("MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE"), + "{err}" + ); + } + + #[test] + fn heze_execution_payload_bid_size_bound() { + let fork_context = heze_fork_context(); + let spec = &fork_context.spec; + let gloas_max = spec.max_signed_execution_payload_bid_size_for_fork(ForkName::Gloas); + let heze_max = spec.max_signed_execution_payload_bid_size_for_fork(ForkName::Heze); + assert!(heze_max > gloas_max); + + let err = decode_oversized(&fork_context, GossipKind::ExecutionPayloadBid, heze_max + 1) + .unwrap_err(); assert!( err.contains("MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE"), "{err}" ); - let err = decode_oversized(GossipKind::ExecutionPayloadBid, max).unwrap_err(); + let err = + decode_oversized(&fork_context, GossipKind::ExecutionPayloadBid, heze_max).unwrap_err(); assert!( !err.contains("MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE"), "{err}" diff --git a/consensus/types/presets/gnosis/heze.yaml b/consensus/types/presets/gnosis/heze.yaml index 8aa06a2a519..0b1a0fdfd25 100644 --- a/consensus/types/presets/gnosis/heze.yaml +++ b/consensus/types/presets/gnosis/heze.yaml @@ -4,3 +4,8 @@ # --------------------------------------------------------------- # 2**4 (= 16) validators INCLUSION_LIST_COMMITTEE_SIZE: 16 + +# Networking +# --------------------------------------------------------------- +# 196,934 bytes, ~192 KiB +MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE_HEZE: 196934 diff --git a/consensus/types/presets/mainnet/heze.yaml b/consensus/types/presets/mainnet/heze.yaml index be038fbcc20..8a957108058 100644 --- a/consensus/types/presets/mainnet/heze.yaml +++ b/consensus/types/presets/mainnet/heze.yaml @@ -4,3 +4,8 @@ # --------------------------------------------------------------- # 2**4 (= 16) validators INCLUSION_LIST_COMMITTEE_SIZE: 16 + +# Networking +# --------------------------------------------------------------- +# 196,934 bytes, ~192 KiB +MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE_HEZE: 196934 diff --git a/consensus/types/presets/minimal/heze.yaml b/consensus/types/presets/minimal/heze.yaml index a36f5d7d35d..667d38b51e9 100644 --- a/consensus/types/presets/minimal/heze.yaml +++ b/consensus/types/presets/minimal/heze.yaml @@ -4,3 +4,8 @@ # --------------------------------------------------------------- # 2**4 (= 16) validators INCLUSION_LIST_COMMITTEE_SIZE: 16 + +# Networking +# --------------------------------------------------------------- +# 196,934 bytes, ~192 KiB +MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE_HEZE: 196934 diff --git a/consensus/types/src/core/chain_spec.rs b/consensus/types/src/core/chain_spec.rs index 6aed6a9d502..8b930343f3a 100644 --- a/consensus/types/src/core/chain_spec.rs +++ b/consensus/types/src/core/chain_spec.rs @@ -331,6 +331,7 @@ pub struct ChainSpec { * Networking Gloas */ pub max_request_payloads: u64, + pub max_signed_execution_payload_bid_size: u64, /* * Networking Heze @@ -338,6 +339,7 @@ pub struct ChainSpec { pub max_transactions_bytes_per_inclusion_list: u64, pub max_request_inclusion_list: u64, pub min_slots_for_inclusion_lists_requests: u64, + pub max_signed_execution_payload_bid_size_heze: u64, /* * Networking Derived @@ -479,6 +481,14 @@ impl ChainSpec { } } + pub fn max_signed_execution_payload_bid_size_for_fork(&self, fork_name: ForkName) -> usize { + if fork_name.heze_enabled() { + self.max_signed_execution_payload_bid_size_heze as usize + } else { + self.max_signed_execution_payload_bid_size as usize + } + } + pub fn max_effective_balance_for_fork(&self, fork_name: ForkName) -> u64 { if fork_name.electra_enabled() { self.max_effective_balance_electra @@ -1412,6 +1422,7 @@ impl ChainSpec { }) .expect("calculation does not overflow"), max_request_payloads: 128, + max_signed_execution_payload_bid_size: 196932, /* * Heze hard fork params @@ -1420,6 +1431,7 @@ impl ChainSpec { heze_fork_epoch: None, max_transactions_bytes_per_inclusion_list: 8192, max_request_inclusion_list: 16, + max_signed_execution_payload_bid_size_heze: 196934, min_slots_for_inclusion_lists_requests: 1, /* @@ -1868,6 +1880,7 @@ impl ChainSpec { }) .expect("calculation does not overflow"), max_request_payloads: 128, + max_signed_execution_payload_bid_size: 196932, /* * Heze hard fork params @@ -1876,6 +1889,7 @@ impl ChainSpec { heze_fork_epoch: None, max_transactions_bytes_per_inclusion_list: 8192, max_request_inclusion_list: 16, + max_signed_execution_payload_bid_size_heze: 196934, min_slots_for_inclusion_lists_requests: 1, /* diff --git a/consensus/types/src/core/eth_spec.rs b/consensus/types/src/core/eth_spec.rs index fd352df7c84..b393104dab8 100644 --- a/consensus/types/src/core/eth_spec.rs +++ b/consensus/types/src/core/eth_spec.rs @@ -463,9 +463,6 @@ pub trait EthSpec: 'static + Default + Sync + Send + Clone + Debug + PartialEq + /// Returns the `MAX_PARTIAL_DATA_COLUMN_SIDECAR_SIZE` constant for this specification. fn max_partial_data_column_sidecar_size() -> usize; - /// Returns the `MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE` constant for this specification. - fn max_signed_execution_payload_bid_size() -> usize; - /// Returns the `PAYLOAD_TIMELY_THRESHOLD` constant (PTC_SIZE / 2). fn payload_timely_threshold() -> usize { Self::PTCSize::to_usize() / 2 @@ -581,10 +578,6 @@ impl EthSpec for MainnetEthSpec { fn max_partial_data_column_sidecar_size() -> usize { 8585741 } - - fn max_signed_execution_payload_bid_size() -> usize { - 196932 - } } /// Ethereum Foundation minimal spec, as defined in the eth2.0-specs repo. @@ -681,10 +674,6 @@ impl EthSpec for MinimalEthSpec { fn max_partial_data_column_sidecar_size() -> usize { 8585741 } - - fn max_signed_execution_payload_bid_size() -> usize { - 196932 - } } /// Gnosis Beacon Chain specifications. @@ -778,10 +767,6 @@ impl EthSpec for GnosisEthSpec { fn max_partial_data_column_sidecar_size() -> usize { 8585741 } - - fn max_signed_execution_payload_bid_size() -> usize { - 196932 - } } #[cfg(test)] diff --git a/consensus/types/src/core/preset.rs b/consensus/types/src/core/preset.rs index 43fa650e13c..40a23fd7e84 100644 --- a/consensus/types/src/core/preset.rs +++ b/consensus/types/src/core/preset.rs @@ -355,7 +355,7 @@ pub struct GloasPreset { } impl GloasPreset { - pub fn from_chain_spec(_spec: &ChainSpec) -> Self { + pub fn from_chain_spec(spec: &ChainSpec) -> Self { Self { ptc_size: E::ptc_size() as u64, max_payload_attestations: E::max_payload_attestations() as u64, @@ -368,8 +368,7 @@ impl GloasPreset { max_attester_slashing_size: E::max_attester_slashing_size() as u64, max_data_column_sidecar_size: E::max_data_column_sidecar_size() as u64, max_partial_data_column_sidecar_size: E::max_partial_data_column_sidecar_size() as u64, - max_signed_execution_payload_bid_size: E::max_signed_execution_payload_bid_size() - as u64, + max_signed_execution_payload_bid_size: spec.max_signed_execution_payload_bid_size, } } } @@ -379,12 +378,16 @@ impl GloasPreset { pub struct HezePreset { #[serde(with = "serde_utils::quoted_u64")] pub inclusion_list_committee_size: u64, + #[serde(with = "serde_utils::quoted_u64")] + pub max_signed_execution_payload_bid_size_heze: u64, } impl HezePreset { - pub fn from_chain_spec(_spec: &ChainSpec) -> Self { + pub fn from_chain_spec(spec: &ChainSpec) -> Self { Self { inclusion_list_committee_size: E::inclusion_list_committee_size() as u64, + max_signed_execution_payload_bid_size_heze: spec + .max_signed_execution_payload_bid_size_heze, } } } From 16992ef8e891cddbb9f4c1c3ff47b04a4032e512 Mon Sep 17 00:00:00 2001 From: conache Date: Wed, 16 Sep 2026 11:46:59 +0300 Subject: [PATCH 18/44] Avoid cloning the bid when constructing an AvailableEnvelope --- .../beacon_chain/src/custody_context.rs | 4 +- .../src/payload_envelope_verification/mod.rs | 4 +- .../pending_components.rs | 10 ++- beacon_node/beacon_chain/src/test_utils.rs | 9 +-- .../beacon_chain/tests/block_verification.rs | 62 +++++++------------ .../src/sync/block_sidecar_coupling.rs | 6 +- beacon_node/network/src/sync/tests/lookups.rs | 2 +- .../execution/signed_execution_payload_bid.rs | 14 ++++- 8 files changed, 52 insertions(+), 59 deletions(-) diff --git a/beacon_node/beacon_chain/src/custody_context.rs b/beacon_node/beacon_chain/src/custody_context.rs index 4a1dfdbe711..7664c4b6b3e 100644 --- a/beacon_node/beacon_chain/src/custody_context.rs +++ b/beacon_node/beacon_chain/src/custody_context.rs @@ -11,7 +11,7 @@ use std::{ }; use tracing::{debug, warn}; use types::{ - ChainSpec, ColumnIndex, Epoch, EthSpec, SignedBeaconBlock, SignedExecutionPayloadBid, Slot, + ChainSpec, ColumnIndex, Epoch, EthSpec, SignedBeaconBlock, SignedExecutionPayloadBidRef, Slot, }; /// A delay before making the CGC change effective to the data availability checker. @@ -590,7 +590,7 @@ impl CustodyContext { pub fn data_columns_required_for_bid( &self, - bid: &SignedExecutionPayloadBid, + bid: SignedExecutionPayloadBidRef, ) -> bool { bid.num_blobs_expected() > 0 && self.data_columns_required_for_epoch(bid.epoch()) } diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs index 727ecd97d90..ff5068c21ac 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs @@ -30,7 +30,7 @@ use strum::AsRefStr; use tracing::{instrument, warn}; use types::{ BeaconState, BeaconStateError, BuilderIndex, DataColumnSidecarList, EthSpec, - ExecutionBlockHash, ExecutionPayloadEnvelope, Hash256, SignedExecutionPayloadBid, + ExecutionBlockHash, ExecutionPayloadEnvelope, Hash256, SignedExecutionPayloadBidRef, SignedExecutionPayloadEnvelope, Slot, }; @@ -70,7 +70,7 @@ impl AvailableEnvelope { pub fn new( envelope: Arc>, columns: DataColumnSidecarList, - bid: &SignedExecutionPayloadBid, + bid: SignedExecutionPayloadBidRef, custody_context: &CustodyContext, ) -> Result where diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs index 18415d9b874..8629dfb4824 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs @@ -44,7 +44,7 @@ impl PendingComponents { where T: BeaconChainTypes, { - if custody_context.data_columns_required_for_bid(&self.bid) { + if custody_context.data_columns_required_for_bid(self.bid.to_ref()) { custody_context.num_of_data_columns_to_sample(self.bid.epoch()) } else { 0 @@ -291,8 +291,12 @@ impl PendingComponents { } }; - let available_envelope = - AvailableEnvelope::new(envelope.clone(), columns, &self.bid, custody_context)?; + let available_envelope = AvailableEnvelope::new( + envelope.clone(), + columns, + self.bid.to_ref(), + custody_context, + )?; Ok(Some(AvailableExecutedEnvelope { envelope: available_envelope, diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 80490f8dc4f..5f04e4eaf51 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -3197,14 +3197,13 @@ where let has_blobs = block.num_expected_blobs() > 0; if !has_blobs { return if let Ok(bid) = block.message().body().signed_execution_payload_bid() { - let bid = bid.clone_as_signed_execution_payload_bid(); let envelope = self .chain .get_payload_envelope(&block_root) .unwrap() .map(Arc::new) .map(|envelope| { - AvailableEnvelope::new(envelope, vec![], &bid, &self.chain.custody_context) + AvailableEnvelope::new(envelope, vec![], bid, &self.chain.custody_context) }) .transpose() .unwrap(); @@ -3229,7 +3228,6 @@ where .unwrap(); let custody_columns = columns.into_iter().collect::>(); if let Ok(bid) = block.message().body().signed_execution_payload_bid() { - let bid = bid.clone_as_signed_execution_payload_bid(); let envelope = self .chain .get_payload_envelope(&block_root) @@ -3239,7 +3237,7 @@ where AvailableEnvelope::new( envelope, custody_columns, - &bid, + bid, &self.chain.custody_context, ) }) @@ -3269,7 +3267,6 @@ where blob_items: Option<(KzgProofs, BlobsList)>, ) -> Result, BlockError> { if let Ok(bid) = block.message().body().signed_execution_payload_bid() { - let bid = bid.clone_as_signed_execution_payload_bid(); let columns = blob_items .map(|_| generate_data_column_sidecars_from_block(&block, &self.spec)) .unwrap_or_default(); @@ -3279,7 +3276,7 @@ where .map_err(|e| BlockError::BeaconChainError(Box::new(e)))? .map(Arc::new) .map(|envelope| { - AvailableEnvelope::new(envelope, columns, &bid, &self.chain.custody_context) + AvailableEnvelope::new(envelope, columns, bid, &self.chain.custody_context) }) .transpose() .unwrap(); diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index d0e3ee797eb..05462b1c384 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -208,7 +208,6 @@ where T: BeaconChainTypes, { if let Ok(bid) = block.message().body().signed_execution_payload_bid() { - let bid = bid.clone_as_signed_execution_payload_bid(); let columns = match data_sidecars { Some(DataSidecars::DataColumns(columns)) => columns .iter() @@ -217,7 +216,7 @@ where Some(DataSidecars::Blobs(_)) | None => vec![], }; let envelope = execution_envelope - .map(|envelope| AvailableEnvelope::new(envelope, columns, &bid, &chain.custody_context)) + .map(|envelope| AvailableEnvelope::new(envelope, columns, bid, &chain.custody_context)) .transpose() .unwrap(); return RangeSyncBlock::new_gloas(block, envelope).unwrap(); @@ -2406,10 +2405,9 @@ async fn range_sync_block_new_gloas_accepts_matching_envelope() { .message() .body() .signed_execution_payload_bid() - .unwrap() - .clone_as_signed_execution_payload_bid(); + .unwrap(); let available_envelope = - AvailableEnvelope::new(Arc::new(envelope), columns, &bid, &custody_context).unwrap(); + AvailableEnvelope::new(Arc::new(envelope), columns, bid, &custody_context).unwrap(); let result = RangeSyncBlock::new_gloas(block, Some(available_envelope)); assert!( @@ -2447,10 +2445,9 @@ async fn range_sync_block_new_gloas_rejects_slot_mismatch() { .message() .body() .signed_execution_payload_bid() - .unwrap() - .clone_as_signed_execution_payload_bid(); + .unwrap(); let available_envelope = - AvailableEnvelope::new(Arc::new(envelope), columns, &bid, &custody_context).unwrap(); + AvailableEnvelope::new(Arc::new(envelope), columns, bid, &custody_context).unwrap(); let result = RangeSyncBlock::new_gloas(block, Some(available_envelope)); assert!( @@ -2473,10 +2470,9 @@ async fn range_sync_block_new_gloas_rejects_builder_index_mismatch() { .message() .body() .signed_execution_payload_bid() - .unwrap() - .clone_as_signed_execution_payload_bid(); + .unwrap(); let available_envelope = - AvailableEnvelope::new(Arc::new(envelope), columns, &bid, &custody_context).unwrap(); + AvailableEnvelope::new(Arc::new(envelope), columns, bid, &custody_context).unwrap(); let result = RangeSyncBlock::new_gloas(block, Some(available_envelope)); assert!( @@ -2499,10 +2495,9 @@ async fn range_sync_block_new_gloas_rejects_block_hash_mismatch() { .message() .body() .signed_execution_payload_bid() - .unwrap() - .clone_as_signed_execution_payload_bid(); + .unwrap(); let available_envelope = - AvailableEnvelope::new(Arc::new(envelope), columns, &bid, &custody_context).unwrap(); + AvailableEnvelope::new(Arc::new(envelope), columns, bid, &custody_context).unwrap(); let result = RangeSyncBlock::new_gloas(block, Some(available_envelope)); assert!( @@ -2752,16 +2747,9 @@ async fn filter_chain_segment_keeps_checkpoint_gloas_block_by_split_root() { .message() .body() .signed_execution_payload_bid() - .unwrap() - .clone_as_signed_execution_payload_bid(); + .unwrap(); let columns = generate_data_column_sidecars_from_block(&checkpoint_block, &harness.chain.spec); - let available_envelope = AvailableEnvelope::new( - Arc::new(envelope), - columns, - &bid, - &harness.chain.custody_context, - ) - .unwrap(); + let available_envelope = AvailableEnvelope::new(Arc::new(envelope), columns, bid).unwrap(); let range_sync_block = RangeSyncBlock::Gloas { block: checkpoint_block, envelope: Some(available_envelope), @@ -3051,13 +3039,11 @@ async fn process_chain_segment_rejects_envelope_with_invalid_signature() { .message() .body() .signed_execution_payload_bid() - .unwrap() - .clone_as_signed_execution_payload_bid(); - + .unwrap(); let available_envelope = AvailableEnvelope::new( Arc::new(envelope), columns, - &bid, + bid, &harness.chain.custody_context, ) .unwrap(); @@ -3117,12 +3103,11 @@ async fn process_chain_segment_partial_import_with_invalid_envelope() { .message() .body() .signed_execution_payload_bid() - .unwrap() - .clone_as_signed_execution_payload_bid(); + .unwrap(); let available_envelope1 = AvailableEnvelope::new( envelope1, columns1, - &bid1, + bid1, &import_harness.chain.custody_context, ) .unwrap(); @@ -3146,12 +3131,11 @@ async fn process_chain_segment_partial_import_with_invalid_envelope() { .message() .body() .signed_execution_payload_bid() - .unwrap() - .clone_as_signed_execution_payload_bid(); + .unwrap(); let available_envelope2 = AvailableEnvelope::new( Arc::new(envelope2), columns2, - &bid2, + bid2, &import_harness.chain.custody_context, ) .unwrap(); @@ -3243,8 +3227,7 @@ async fn process_chain_segment_import_fails_without_parent_envelope() { .message() .body() .signed_execution_payload_bid() - .unwrap() - .clone_as_signed_execution_payload_bid(); + .unwrap(); let available_envelope2 = AvailableEnvelope::new( envelope2, columns2, @@ -3339,8 +3322,7 @@ async fn process_chain_segment_full_and_empty_transitions() { .message() .body() .signed_execution_payload_bid() - .unwrap() - .clone_as_signed_execution_payload_bid(); + .unwrap(); let available_envelope1 = AvailableEnvelope::new( envelope1, columns1, @@ -3368,8 +3350,7 @@ async fn process_chain_segment_full_and_empty_transitions() { .message() .body() .signed_execution_payload_bid() - .unwrap() - .clone_as_signed_execution_payload_bid(); + .unwrap(); let available_envelope3 = AvailableEnvelope::new( Arc::new(envelope3), columns3, @@ -3458,8 +3439,7 @@ async fn process_chain_segment_rejects_envelope_with_invalid_columns() { .message() .body() .signed_execution_payload_bid() - .unwrap() - .clone_as_signed_execution_payload_bid(); + .unwrap(); let available_envelope = AvailableEnvelope::new( envelope, columns, diff --git a/beacon_node/network/src/sync/block_sidecar_coupling.rs b/beacon_node/network/src/sync/block_sidecar_coupling.rs index 41732d60e93..3a840a8d632 100644 --- a/beacon_node/network/src/sync/block_sidecar_coupling.rs +++ b/beacon_node/network/src/sync/block_sidecar_coupling.rs @@ -466,10 +466,10 @@ impl RangeBlockComponentsRequest { .body() .signed_execution_payload_bid() // this really should never fail - .map_err(|_| AvailabilityCheckError::MissingBid(block_root))? - .clone_as_signed_execution_payload_bid(); + .map_err(|_| AvailabilityCheckError::MissingBid(block_root))?; + let available_envelope = envelope - .map(|env| AvailableEnvelope::new(env, custody_columns, &bid, custody_context)) + .map(|env| AvailableEnvelope::new(env, custody_columns, bid, custody_context)) .transpose()?; RangeSyncBlock::new_gloas(block, available_envelope) diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index 9316725de63..d800faca67a 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -1300,7 +1300,7 @@ impl TestRig { AvailableEnvelope::new( envelope, columns.unwrap_or_default(), - &bid.clone_as_signed_execution_payload_bid(), + bid, &self.harness.chain.custody_context, ) }) diff --git a/consensus/types/src/execution/signed_execution_payload_bid.rs b/consensus/types/src/execution/signed_execution_payload_bid.rs index 3f152c2ce66..98873ddc479 100644 --- a/consensus/types/src/execution/signed_execution_payload_bid.rs +++ b/consensus/types/src/execution/signed_execution_payload_bid.rs @@ -1,5 +1,5 @@ use crate::execution::{ExecutionPayloadBidGloas, ExecutionPayloadBidHeze, ExecutionPayloadBidRef}; -use crate::{EthSpec, ForkName, ForkVersionDecode}; +use crate::{Epoch, EthSpec, ForkName, ForkVersionDecode, Slot}; use bls::Signature; use context_deserialize::{ContextDeserialize, context_deserialize}; use educe::Educe; @@ -84,6 +84,18 @@ impl SignedExecutionPayloadBidHeze { } impl<'a, E: EthSpec> SignedExecutionPayloadBidRef<'a, E> { + pub fn slot(&self) -> Slot { + self.message().slot() + } + + pub fn epoch(&self) -> Epoch { + self.slot().epoch(E::slots_per_epoch()) + } + + pub fn num_blobs_expected(&self) -> usize { + self.message().blob_kzg_commitments().len() + } + /// The bid message as a fork-agnostic reference. pub fn message(&self) -> ExecutionPayloadBidRef<'a, E> { match self { From 853a0a4283ad2cf2481da0f985fff94f92ef466d Mon Sep 17 00:00:00 2001 From: conache Date: Wed, 16 Sep 2026 13:45:18 +0300 Subject: [PATCH 19/44] Fix: produce Heze execution payload bids --- .../src/block_production/gloas.rs | 93 ++++++++++++++----- 1 file changed, 68 insertions(+), 25 deletions(-) diff --git a/beacon_node/beacon_chain/src/block_production/gloas.rs b/beacon_node/beacon_chain/src/block_production/gloas.rs index 2b1ae701865..ede52cede07 100644 --- a/beacon_node/beacon_chain/src/block_production/gloas.rs +++ b/beacon_node/beacon_chain/src/block_production/gloas.rs @@ -10,7 +10,7 @@ use execution_layer::{ PayloadParameters, }; use operation_pool::CompactAttestationRef; -use ssz::{Encode, ProgressiveBitList}; +use ssz::{BitVector, Encode, ProgressiveBitList}; use ssz_types::ProgressiveVariableList; use state_processing::common::{get_attesting_indices_from_state, get_indexed_payload_attestation}; use state_processing::envelope_processing::verify_execution_payload_envelope; @@ -32,10 +32,11 @@ use types::{ Address, Attestation, AttestationGloas, AttesterSlashing, AttesterSlashingGloas, BeaconBlock, BeaconBlockBodyGloas, BeaconBlockBodyHeze, BeaconBlockGloas, BeaconBlockHeze, BeaconState, BeaconStateError, BlobsList, BuilderIndex, Deposit, Eth1Data, EthSpec, ExecutionBlockHash, - ExecutionPayloadBidGloas, ExecutionPayloadEnvelope, ExecutionRequestsGloas, FullPayload, - Graffiti, Hash256, IndexedAttestation, KzgProofs, PayloadAttestation, ProposerSlashing, - RelativeEpoch, SignedBeaconBlock, SignedBlsToExecutionChange, SignedExecutionPayloadBid, - SignedExecutionPayloadBidGloas, SignedExecutionPayloadEnvelope, SignedProposerPreferences, + ExecutionPayloadBidGloas, ExecutionPayloadBidHeze, ExecutionPayloadEnvelope, + ExecutionRequestsGloas, FullPayload, Graffiti, Hash256, IndexedAttestation, KzgProofs, + PayloadAttestation, ProposerSlashing, RelativeEpoch, SignedBeaconBlock, + SignedBlsToExecutionChange, SignedExecutionPayloadBid, SignedExecutionPayloadBidGloas, + SignedExecutionPayloadBidHeze, SignedExecutionPayloadEnvelope, SignedProposerPreferences, SignedVoluntaryExit, Slot, SyncAggregate, Uint256, Withdrawal, Withdrawals, }; @@ -995,22 +996,67 @@ impl BeaconChain { should_override_builder, } = block_proposal_contents; - // TODO(gloas) since we are defaulting to local building, execution payment is 0 - // execution payment should only be set to > 0 for trusted building. - let bid = ExecutionPayloadBidGloas:: { - parent_block_hash: executed_ancestor_hash, - parent_block_root: parent_root, - block_hash: payload.block_hash, - prev_randao: payload.prev_randao, - fee_recipient: Address::ZERO, - gas_limit: payload.gas_limit, - builder_index, - slot: produce_at_slot, - value: bid_value, - execution_payment: EXECUTION_PAYMENT_TRUSTLESS_BUILD, - blob_kzg_commitments, - execution_requests_root: execution_requests.tree_hash_root(), - _phantom: PhantomData, + let signature = Signature::infinity().map_err(BlockProductionError::BlsError)?; + let signed_execution_payload_bid = match state { + BeaconState::Base(_) + | BeaconState::Altair(_) + | BeaconState::Bellatrix(_) + | BeaconState::Capella(_) + | BeaconState::Deneb(_) + | BeaconState::Electra(_) + | BeaconState::Fulu(_) => { + return Err(BlockProductionError::InvalidBlockVariant( + "Cannot produce an execution payload bid pre-Gloas".to_owned(), + )); + } + BeaconState::Gloas(_) => { + // TODO(gloas) since we are defaulting to local building, execution payment is 0 + // execution payment should only be set to > 0 for trusted building. + let bid = ExecutionPayloadBidGloas:: { + parent_block_hash: executed_ancestor_hash, + parent_block_root: parent_root, + block_hash: payload.block_hash, + prev_randao: payload.prev_randao, + fee_recipient: Address::ZERO, + gas_limit: payload.gas_limit, + builder_index, + slot: produce_at_slot, + value: bid_value, + execution_payment: EXECUTION_PAYMENT_TRUSTLESS_BUILD, + blob_kzg_commitments, + execution_requests_root: execution_requests.tree_hash_root(), + _phantom: PhantomData, + }; + + SignedExecutionPayloadBid::Gloas(SignedExecutionPayloadBidGloas { + message: bid, + signature, + }) + } + BeaconState::Heze(_) => { + let bid = ExecutionPayloadBidHeze:: { + parent_block_hash: executed_ancestor_hash, + parent_block_root: parent_root, + block_hash: payload.block_hash, + prev_randao: payload.prev_randao, + fee_recipient: Address::ZERO, + gas_limit: payload.gas_limit, + builder_index, + slot: produce_at_slot, + value: bid_value, + execution_payment: EXECUTION_PAYMENT_TRUSTLESS_BUILD, + blob_kzg_commitments, + execution_requests_root: execution_requests.tree_hash_root(), + // TODO(heze): set the bits based on the IL committee members whose + // inclusion lists were considered when building the payload + inclusion_list_bits: BitVector::new(), + _phantom: PhantomData, + }; + SignedExecutionPayloadBid::Heze(SignedExecutionPayloadBidHeze { + message: bid, + signature, + }) + } }; // Store payload data for envelope construction after block is created @@ -1023,10 +1069,7 @@ impl BeaconChain { }; Ok(( - SignedExecutionPayloadBid::Gloas(SignedExecutionPayloadBidGloas { - message: bid, - signature: Signature::infinity().map_err(BlockProductionError::BlsError)?, - }), + signed_execution_payload_bid, LocalBuildResult { payload_data, payload_value, From 0f76601ed5f3de33bb0db9aad22015dab4d65b86 Mon Sep 17 00:00:00 2001 From: conache Date: Wed, 16 Sep 2026 14:20:25 +0300 Subject: [PATCH 20/44] Ensure BN execution payload bids endpoints are tested against Heze variants --- .../src/beacon/execution_payload_bids.rs | 2 +- beacon_node/http_api/tests/tests.rs | 66 ++++++++++++------- .../src/sync/block_sidecar_coupling.rs | 1 - 3 files changed, 45 insertions(+), 24 deletions(-) diff --git a/beacon_node/http_api/src/beacon/execution_payload_bids.rs b/beacon_node/http_api/src/beacon/execution_payload_bids.rs index 739588802b2..732f66488b1 100644 --- a/beacon_node/http_api/src/beacon/execution_payload_bids.rs +++ b/beacon_node/http_api/src/beacon/execution_payload_bids.rs @@ -84,7 +84,7 @@ pub(crate) fn post_beacon_execution_payload_bids( consensus_version, ) .map_err(|e| { - warp_utils::reject::custom_bad_request(format!("invalid SSZ: {e:?}")) + warp_utils::reject::custom_bad_request(format!("invalid JSON: {e:?}")) })?; publish_execution_payload_bid(bid, &chain, &network_tx) }) diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 57bc96ccd09..f2f9bd987a1 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -51,9 +51,10 @@ use tokio::time::Duration; use tree_hash::TreeHash; use types::ApplicationDomain; use types::{ - Address, Builder, Domain, EthSpec, ExecutionBlockHash, ExecutionPayloadBidGloas, Hash256, - MainnetEthSpec, ProposerPreferences, RelativeEpoch, SelectionProof, SignedExecutionPayloadBid, - SignedExecutionPayloadBidGloas, SignedExecutionPayloadEnvelope, SignedProposerPreferences, + Address, Builder, Domain, EthSpec, ExecutionBlockHash, ExecutionPayloadBidGloas, + ExecutionPayloadBidHeze, Hash256, MainnetEthSpec, ProposerPreferences, RelativeEpoch, + SelectionProof, SignedExecutionPayloadBid, SignedExecutionPayloadBidGloas, + SignedExecutionPayloadBidHeze, SignedExecutionPayloadEnvelope, SignedProposerPreferences, SignedRoot, SingleAttestation, Slot, attestation::AttestationBase, consts::gloas::{BUILDER_INDEX_SELF_BUILD, PAYLOAD_BUILDER_VERSION}, @@ -3429,27 +3430,48 @@ impl ApiTester { let slot = self.chain.slot().unwrap(); let fork_name = self.chain.spec.fork_name_at_slot::(slot); - let bid = ExecutionPayloadBidGloas { - parent_block_hash: ExecutionBlockHash::zero(), - parent_block_root: head.head_block_root(), - block_hash: ExecutionBlockHash::zero(), - prev_randao: Hash256::zero(), - fee_recipient: Address::zero(), - gas_limit: 30_000_000, - builder_index: 0, - slot, - value: 100, - execution_payment: 0, - blob_kzg_commitments: Default::default(), - execution_requests_root: Hash256::zero(), - _phantom: std::marker::PhantomData, + let signature = bls::Signature::empty(); + let signed = if fork_name.heze_enabled() { + SignedExecutionPayloadBid::Heze(SignedExecutionPayloadBidHeze { + message: ExecutionPayloadBidHeze { + parent_block_hash: ExecutionBlockHash::zero(), + parent_block_root: head.head_block_root(), + block_hash: ExecutionBlockHash::zero(), + prev_randao: Hash256::zero(), + fee_recipient: Address::zero(), + gas_limit: 30_000_000, + builder_index: 0, + slot, + value: 100, + execution_payment: 0, + blob_kzg_commitments: Default::default(), + execution_requests_root: Hash256::zero(), + inclusion_list_bits: Default::default(), + _phantom: std::marker::PhantomData, + }, + signature, + }) + } else { + SignedExecutionPayloadBid::Gloas(SignedExecutionPayloadBidGloas { + message: ExecutionPayloadBidGloas { + parent_block_hash: ExecutionBlockHash::zero(), + parent_block_root: head.head_block_root(), + block_hash: ExecutionBlockHash::zero(), + prev_randao: Hash256::zero(), + fee_recipient: Address::zero(), + gas_limit: 30_000_000, + builder_index: 0, + slot, + value: 100, + execution_payment: 0, + blob_kzg_commitments: Default::default(), + execution_requests_root: Hash256::zero(), + _phantom: std::marker::PhantomData, + }, + signature, + }) }; - let signed = SignedExecutionPayloadBid::Gloas(SignedExecutionPayloadBidGloas { - message: bid, - signature: bls::Signature::empty(), - }); - (signed, fork_name) } diff --git a/beacon_node/network/src/sync/block_sidecar_coupling.rs b/beacon_node/network/src/sync/block_sidecar_coupling.rs index 3a840a8d632..fd81b450d1f 100644 --- a/beacon_node/network/src/sync/block_sidecar_coupling.rs +++ b/beacon_node/network/src/sync/block_sidecar_coupling.rs @@ -467,7 +467,6 @@ impl RangeBlockComponentsRequest { .signed_execution_payload_bid() // this really should never fail .map_err(|_| AvailabilityCheckError::MissingBid(block_root))?; - let available_envelope = envelope .map(|env| AvailableEnvelope::new(env, custody_columns, bid, custody_context)) .transpose()?; From 9ca662c5a5a3342738529eb91614820c6b20c11d Mon Sep 17 00:00:00 2001 From: conache Date: Fri, 18 Sep 2026 16:34:49 +0300 Subject: [PATCH 21/44] Fix compilation error --- validator_client/validator_services/src/block_service.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/validator_client/validator_services/src/block_service.rs b/validator_client/validator_services/src/block_service.rs index 7c8e029c4f6..27db89a8ae9 100644 --- a/validator_client/validator_services/src/block_service.rs +++ b/validator_client/validator_services/src/block_service.rs @@ -726,7 +726,7 @@ impl BlockService { .block() .body() .signed_execution_payload_bid() - .map(|bid| bid.message.builder_index) + .map(|bid| bid.message().builder_index()) .ok(), UnsignedBlock::Blinded(_) => None, }; From b6f849a4d780b585b1657bdcf9750090d45ba626 Mon Sep 17 00:00:00 2001 From: conache Date: Fri, 18 Sep 2026 20:00:58 +0300 Subject: [PATCH 22/44] Add gossip il bits inclusivity check for execution payload bids --- .../gossip_verified_bid.rs | 46 +++++++++++- .../src/payload_bid_verification/mod.rs | 3 + .../src/payload_bid_verification/tests.rs | 70 +++++++++++++++++-- .../gossip_methods.rs | 3 +- 4 files changed, 113 insertions(+), 9 deletions(-) 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 b778c7d466f..ba5751ec382 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 @@ -1,5 +1,6 @@ use std::sync::Arc; +use crate::inclusion_list_store::InclusionListStore; use crate::{ BeaconChain, BeaconChainTypes, BeaconStore, CachedHead, CanonicalHead, canonical_head::ForkChoiceReadGuard, @@ -12,6 +13,7 @@ use crate::{ }; use educe::Educe; use eth2::types::{EventKind, ForkVersionedResponse}; +use parking_lot::RwLock; use proto_array::Block as ProtoBlock; use slot_clock::SlotClock; use state_processing::signature_sets::{ @@ -20,7 +22,7 @@ use state_processing::signature_sets::{ use tracing::debug; use types::{ BeaconState, Builder, ChainSpec, EthSpec, ExecutionPayloadBidRef, ExecutionRequestsGloas, - SignedExecutionPayloadBid, SignedProposerPreferences, Slot, + RelativeEpoch, SignedExecutionPayloadBid, SignedProposerPreferences, Slot, consts::gloas::PAYLOAD_BUILDER_VERSION, }; @@ -270,6 +272,7 @@ pub struct GossipVerificationContext<'a, T: BeaconChainTypes> { pub observed_execution_payloads: &'a ObservedExecutionPayloads, pub gossip_verified_payload_bid_cache: &'a GossipVerifiedPayloadBidCache, pub gossip_verified_proposer_preferences_cache: &'a GossipVerifiedProposerPreferenceCache, + pub inclusion_list_store: &'a RwLock>, pub slot_clock: &'a T::SlotClock, pub spec: &'a ChainSpec, pub store: &'a BeaconStore, @@ -455,6 +458,46 @@ impl GossipVerifiedPayloadBid { }); } + // [IGNORE] `bid.inclusion_list_bits` is inclusive of the node's view of the inclusion + // lists for the slot preceding the bid's slot. + if ctx + .spec + .fork_name_at_slot::(bid_slot) + .heze_enabled() + { + let inclusion_list_bits = signed_bid.message().inclusion_list_bits()?; + let inclusion_list_slot = bid_slot.saturating_sub(1u64); + let inclusion_list_epoch = inclusion_list_slot.epoch(T::EthSpec::slots_per_epoch()); + let relative_epoch = + RelativeEpoch::from_epoch(head_state.current_epoch(), inclusion_list_epoch) + .map_err(|e| { + PayloadBidError::InternalError(format!( + "inclusion list epoch {inclusion_list_epoch} out of range: {e:?}" + )) + })?; + let inclusion_list_dependent_root = head_state + .attester_shuffling_decision_root(cached_head.head_block_root(), relative_epoch)?; + let inclusion_list_committee = + head_state.get_inclusion_list_committee(inclusion_list_slot)?; + + let inclusive = ctx + .inclusion_list_store + .read() + .is_inclusion_list_bits_inclusive( + inclusion_list_slot, + inclusion_list_dependent_root, + &inclusion_list_committee, + inclusion_list_bits, + true, + ) + .map_err(|e| { + PayloadBidError::InternalError(format!("inclusion list store: {e:?}")) + })?; + if !inclusive { + return Err(PayloadBidError::InclusionListBitsNotInclusive { slot: bid_slot }); + } + } + execution_payload_bid_signature_set( head_state, |i| get_builder_pubkey_from_state(head_state, i), @@ -485,6 +528,7 @@ impl BeaconChain { gossip_verified_payload_bid_cache: &self.gossip_verified_payload_bid_cache, gossip_verified_proposer_preferences_cache: &self .gossip_verified_proposer_preferences_cache, + inclusion_list_store: &self.inclusion_list_store, slot_clock: &self.slot_clock, spec: &self.spec, store: &self.store, 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 98ad661adf8..adab8fa7b61 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs @@ -92,6 +92,9 @@ pub enum PayloadBidError { }, /// The bids prev randao value is invalid InvalidPrevRandao { slot: Slot }, + /// The bid's `inclusion_list_bits` are not inclusive of the node's view of the inclusion + /// lists for the slot preceding the bid's slot + InclusionListBitsNotInclusive { 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 8e94ea02462..e9c19605832 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs @@ -14,15 +14,14 @@ use store::{HotColdDB, StoreConfig}; use types::{ Address, BuilderExitRequest, ChainSpec, Checkpoint, Domain, Epoch, EthSpec, ExecutionBlockHash, ExecutionPayloadBidGloas, ExecutionPayloadBidRef, ExecutionPayloadEnvelope, - ExecutionPayloadHeader, ExecutionPayloadHeaderFulu, Hash256, MinimalEthSpec, - ProposerPreferences, SignedBeaconBlock, SignedExecutionPayloadBid, - SignedExecutionPayloadBidGloas, SignedExecutionPayloadEnvelope, SignedProposerPreferences, - SignedRoot, Slot, consts::gloas::PAYLOAD_BUILDER_VERSION, + ExecutionPayloadHeader, ExecutionPayloadHeaderFulu, Hash256, InclusionList, MinimalEthSpec, + ProgressiveTransactions, ProposerPreferences, RelativeEpoch, SignedBeaconBlock, + SignedExecutionPayloadBid, SignedExecutionPayloadBidGloas, SignedExecutionPayloadEnvelope, + SignedInclusionList, SignedProposerPreferences, SignedRoot, Slot, + consts::gloas::PAYLOAD_BUILDER_VERSION, }; -use proto_array::{Block as ProtoBlock, ExecutionStatus}; -use types::AttestationShufflingId; - +use crate::inclusion_list_store::{InclusionListStore, InsertOutcome}; use crate::{ beacon_fork_choice_store::BeaconForkChoiceStore, beacon_snapshot::BeaconSnapshot, @@ -43,6 +42,9 @@ use crate::{ }, test_utils::{EphemeralHarnessType, fork_name_from_env, test_spec}, }; +use parking_lot::RwLock; +use proto_array::{Block as ProtoBlock, ExecutionStatus}; +use types::AttestationShufflingId; type E = MinimalEthSpec; type T = EphemeralHarnessType; @@ -65,6 +67,7 @@ struct TestContext { genesis_block_root: Hash256, inactive_builder_index: u64, store: crate::BeaconStore, + inclusion_list_store: RwLock>, } fn builder_withdrawal_credentials(pubkey: &bls::PublicKey, spec: &ChainSpec) -> Hash256 { @@ -187,6 +190,8 @@ impl TestContext { spec.get_slot_duration(), ); + let inclusion_list_store = RwLock::new(InclusionListStore::new(&spec)); + Self { canonical_head, observed_execution_payloads, @@ -198,6 +203,7 @@ impl TestContext { genesis_block_root: block_root, inactive_builder_index, store, + inclusion_list_store, } } @@ -229,6 +235,7 @@ impl TestContext { slot_clock: &self.slot_clock, spec: &self.spec, store: &self.store, + inclusion_list_store: &self.inclusion_list_store, } } @@ -366,6 +373,35 @@ fn make_signed_preferences( }) } +fn seed_inclusion_list( + ctx: &TestContext, + slot: Slot, + validator_index: u64, + is_timely: bool, +) -> InsertOutcome { + let cached_head = ctx.canonical_head.cached_head(); + let head_state = &cached_head.snapshot.beacon_state; + let relative_epoch = + RelativeEpoch::from_epoch(head_state.current_epoch(), slot.epoch(E::slots_per_epoch())) + .expect("inclusion list slot should be within one epoch of the head"); + let dependent_root = head_state + .attester_shuffling_decision_root(cached_head.head_block_root(), relative_epoch) + .expect("should compute attester shuffling decision root"); + + let signed_inclusion_list = SignedInclusionList { + message: InclusionList { + slot, + validator_index, + dependent_root, + transactions: ProgressiveTransactions::default(), + }, + signature: Signature::empty(), + }; + ctx.inclusion_list_store + .write() + .process_inclusion_list(signed_inclusion_list, is_timely) +} + fn seed_preferences(ctx: &TestContext, slot: Slot, fee_recipient: Address, gas_limit: u64) { // Key the preferences by the same dependent root that gossip verification will compute from // the head state, otherwise the lookup misses and verification returns `NoProposerPreferences`. @@ -1252,4 +1288,24 @@ fn bid_equal_to_cached_value_rejected() { incoming_value: 100, }) )); + + #[test] + fn il_bits_inclusivity_checks_are_not_applied_for_gloas_bids() { + todo!() + } + + #[test] + fn bid_il_bits_not_inclusive() { + todo!() + } + + #[test] + fn bid_il_bits_are_inclusive() { + todo!() + } + + #[test] + fn il_bits_inclusivity_check_applied_to_slot_prior_to_bid_slot() { + todo!() + } } 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 2b1f2fba613..53c224f8998 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -4285,7 +4285,8 @@ impl NetworkBeaconProcessor { | PayloadBidError::BeaconStateError(_) | PayloadBidError::InternalError(_) | PayloadBidError::InvalidBidSlot { .. } - | PayloadBidError::UnableToReadSlot, + | PayloadBidError::UnableToReadSlot + | PayloadBidError::InclusionListBitsNotInclusive { .. }, ) => { self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); } From acb18a08f4f9048b045d4a364a2496277041b6d1 Mon Sep 17 00:00:00 2001 From: conache Date: Sat, 19 Sep 2026 21:46:10 +0300 Subject: [PATCH 23/44] Add bid il bits inclusivity checks tests --- .../src/payload_bid_verification/tests.rs | 562 ++++++++++++++---- 1 file changed, 451 insertions(+), 111 deletions(-) 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 e9c19605832..5dcafc91cf5 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs @@ -7,21 +7,22 @@ use fork_choice::ForkChoice; use genesis::{generate_deterministic_keypairs, interop_genesis_state}; use kzg::KzgCommitment; use slot_clock::{SlotClock, TestingSlotClock}; -use ssz::Encode; +use ssz::{BitVector, Encode}; use ssz_types::ProgressiveVariableList; use state_processing::genesis::genesis_block; use store::{HotColdDB, StoreConfig}; use types::{ Address, BuilderExitRequest, ChainSpec, Checkpoint, Domain, Epoch, EthSpec, ExecutionBlockHash, - ExecutionPayloadBidGloas, ExecutionPayloadBidRef, ExecutionPayloadEnvelope, - ExecutionPayloadHeader, ExecutionPayloadHeaderFulu, Hash256, InclusionList, MinimalEthSpec, - ProgressiveTransactions, ProposerPreferences, RelativeEpoch, SignedBeaconBlock, - SignedExecutionPayloadBid, SignedExecutionPayloadBidGloas, SignedExecutionPayloadEnvelope, + ExecutionPayloadBid, ExecutionPayloadBidGloas, ExecutionPayloadBidHeze, ExecutionPayloadBidRef, + ExecutionPayloadEnvelope, ExecutionPayloadHeader, ExecutionPayloadHeaderFulu, Hash256, + InclusionList, InclusionListCommittee, MinimalEthSpec, ProgressiveTransactions, + ProposerPreferences, RelativeEpoch, SignedBeaconBlock, SignedExecutionPayloadBid, + SignedExecutionPayloadBidGloas, SignedExecutionPayloadBidHeze, SignedExecutionPayloadEnvelope, SignedInclusionList, SignedProposerPreferences, SignedRoot, Slot, consts::gloas::PAYLOAD_BUILDER_VERSION, }; -use crate::inclusion_list_store::{InclusionListStore, InsertOutcome}; +use crate::inclusion_list_store::InclusionListStore; use crate::{ beacon_fork_choice_store::BeaconForkChoiceStore, beacon_snapshot::BeaconSnapshot, @@ -44,6 +45,7 @@ use crate::{ }; use parking_lot::RwLock; use proto_array::{Block as ProtoBlock, ExecutionStatus}; +use state_processing::AllCaches; use types::AttestationShufflingId; type E = MinimalEthSpec; @@ -146,6 +148,10 @@ impl TestContext { let signed_block = SignedBeaconBlock::from_block(block, Signature::empty()); let block_root = signed_block.canonical_root(); + state + .build_all_caches(&spec) + .expect("should build state caches"); + let snapshot = BeaconSnapshot::new( Arc::new(signed_block.clone()), None, @@ -207,23 +213,33 @@ impl TestContext { } } - fn sign_bid(&self, bid: ExecutionPayloadBidGloas) -> Arc> { + fn sign_bid(&self, bid: ExecutionPayloadBid) -> Arc> { let head = self.canonical_head.cached_head(); let state = &head.snapshot.beacon_state; let domain = self.spec.get_domain( - bid.slot.epoch(E::slots_per_epoch()), + bid.slot().epoch(E::slots_per_epoch()), Domain::BeaconBuilder, &state.fork(), state.genesis_validators_root(), ); - let message = ExecutionPayloadBidRef::Gloas(&bid).signing_root(domain); - let signature = self.keypairs[bid.builder_index as usize].sk.sign(message); - Arc::new(SignedExecutionPayloadBid::Gloas( - SignedExecutionPayloadBidGloas { - message: bid, - signature, - }, - )) + + let message = bid.signing_root(domain); + let signature = self.keypairs[bid.builder_index() as usize].sk.sign(message); + + match bid { + ExecutionPayloadBid::Gloas(bid) => Arc::new(SignedExecutionPayloadBid::Gloas( + SignedExecutionPayloadBidGloas { + message: bid, + signature, + }, + )), + ExecutionPayloadBid::Heze(bid) => Arc::new(SignedExecutionPayloadBid::Heze( + SignedExecutionPayloadBidHeze { + message: bid, + signature, + }, + )), + } } fn gossip_ctx(&self) -> GossipVerificationContext<'_, T> { @@ -249,6 +265,14 @@ impl TestContext { .expect("should read current epoch randao mix") } + fn inclusion_list_committee(&self, slot: Slot) -> InclusionListCommittee { + let head = self.canonical_head.cached_head(); + head.snapshot + .beacon_state + .get_inclusion_list_committee(slot) + .expect("should read the inclusion list committee for the slot") + } + fn execution_parent_hash(&self) -> ExecutionBlockHash { let head = self.canonical_head.cached_head(); *head @@ -258,6 +282,44 @@ impl TestContext { .expect("should have a Gloas execution block hash") } + fn make_bid( + &self, + slot: Slot, + builder_index: u64, + fee_recipient: Address, + gas_limit: u64, + value: u64, + parent_block_root: Hash256, + ) -> ExecutionPayloadBid { + let parent_block_hash = self.execution_parent_hash(); + let prev_randao = self.expected_prev_randao(); + if self.spec.fork_name_at_slot::(slot).heze_enabled() { + ExecutionPayloadBid::Heze(ExecutionPayloadBidHeze { + slot, + builder_index, + fee_recipient, + gas_limit, + value, + parent_block_root, + parent_block_hash, + prev_randao, + ..ExecutionPayloadBidHeze::default() + }) + } else { + ExecutionPayloadBid::Gloas(ExecutionPayloadBidGloas { + slot, + builder_index, + fee_recipient, + gas_limit, + value, + parent_block_root, + parent_block_hash, + prev_randao, + ..ExecutionPayloadBidGloas::default() + }) + } + } + fn make_signed_bid( &self, slot: Slot, @@ -267,22 +329,30 @@ impl TestContext { value: u64, parent_block_root: Hash256, ) -> Arc> { - Arc::new(SignedExecutionPayloadBid::Gloas( - SignedExecutionPayloadBidGloas { - message: ExecutionPayloadBidGloas { - slot, - builder_index, - fee_recipient, - gas_limit, - value, - parent_block_root, - parent_block_hash: self.execution_parent_hash(), - prev_randao: self.expected_prev_randao(), - ..ExecutionPayloadBidGloas::default() - }, - signature: Signature::empty(), + let signature = Signature::empty(); + Arc::new( + match self.make_bid( + slot, + builder_index, + fee_recipient, + gas_limit, + value, + parent_block_root, + ) { + ExecutionPayloadBid::Gloas(message) => { + SignedExecutionPayloadBid::Gloas(SignedExecutionPayloadBidGloas { + message, + signature, + }) + } + ExecutionPayloadBid::Heze(message) => { + SignedExecutionPayloadBid::Heze(SignedExecutionPayloadBidHeze { + message, + signature, + }) + } }, - )) + ) } fn slot_1_proto_block( @@ -373,12 +443,26 @@ fn make_signed_preferences( }) } -fn seed_inclusion_list( - ctx: &TestContext, - slot: Slot, - validator_index: u64, - is_timely: bool, -) -> InsertOutcome { +/// The inclusion list bits claiming `validator_indices`: every committee position held by one of +/// them is set, the way the store derives its own view. +fn inclusion_list_bits_for_validators( + inclusion_list_committee: &InclusionListCommittee, + validator_indices: &[u64], +) -> BitVector<::InclusionListCommitteeSize> { + let mut inclusion_list_bits = BitVector::<::InclusionListCommitteeSize>::new(); + + for (position, validator_index) in inclusion_list_committee.iter().enumerate() { + if validator_indices.contains(validator_index) { + inclusion_list_bits + .set(position, true) + .expect("position within committee size"); + } + } + + inclusion_list_bits +} + +fn seed_inclusion_list(ctx: &TestContext, slot: Slot, validator_indices: &[u64], is_timely: bool) { let cached_head = ctx.canonical_head.cached_head(); let head_state = &cached_head.snapshot.beacon_state; let relative_epoch = @@ -388,18 +472,22 @@ fn seed_inclusion_list( .attester_shuffling_decision_root(cached_head.head_block_root(), relative_epoch) .expect("should compute attester shuffling decision root"); - let signed_inclusion_list = SignedInclusionList { - message: InclusionList { - slot, - validator_index, - dependent_root, - transactions: ProgressiveTransactions::default(), - }, - signature: Signature::empty(), - }; - ctx.inclusion_list_store - .write() - .process_inclusion_list(signed_inclusion_list, is_timely) + for index in validator_indices { + let signed_inclusion_list = SignedInclusionList { + message: InclusionList { + slot, + validator_index: *index, + dependent_root, + // we're seeding empty inclusion lists since the execution payload bid verifications + // focus only on the inclusion list bits + transactions: ProgressiveTransactions::default(), + }, + signature: Signature::empty(), + }; + ctx.inclusion_list_store + .write() + .process_inclusion_list(signed_inclusion_list, is_timely); + } } fn seed_preferences(ctx: &TestContext, slot: Slot, fee_recipient: Address, gas_limit: u64) { @@ -485,17 +573,14 @@ fn same_builder_new_parent_tuple_not_blocked() { let slot = Slot::new(1); seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); - let bid = ctx.sign_bid(ExecutionPayloadBidGloas { + let bid = ctx.sign_bid(ctx.make_bid( slot, - builder_index: 0, - fee_recipient: Address::ZERO, - gas_limit: 30_000_000, - value: 0, - parent_block_root: ctx.genesis_block_root, - parent_block_hash: ctx.execution_parent_hash(), - prev_randao: ctx.expected_prev_randao(), - ..ExecutionPayloadBidGloas::default() - }); + 0, + Address::ZERO, + 30_000_000, + 0, + ctx.genesis_block_root, + )); let result = GossipVerifiedPayloadBid::new(bid, &gossip); assert!( result.is_ok(), @@ -596,17 +681,14 @@ fn gas_limit_mismatch() { let slot = Slot::new(1); seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); - let bid = ctx.sign_bid(ExecutionPayloadBidGloas { + let bid = ctx.sign_bid(ctx.make_bid( slot, - builder_index: 0, - fee_recipient: Address::ZERO, - gas_limit: 50_000_000, - value: 100, - parent_block_root: ctx.genesis_block_root, - parent_block_hash: ctx.execution_parent_hash(), - prev_randao: ctx.expected_prev_randao(), - ..ExecutionPayloadBidGloas::default() - }); + 0, + Address::ZERO, + 50_000_000, + 100, + ctx.genesis_block_root, + )); let result = GossipVerifiedPayloadBid::new(bid, &gossip); assert!(matches!(result, Err(PayloadBidError::InvalidGasLimit))); assert_eq!( @@ -1160,17 +1242,14 @@ fn valid_bid_after_empty_genesis_uses_parent_payload_gas_limit() { let slot = Slot::new(1); seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); - let bid = ctx.sign_bid(ExecutionPayloadBidGloas { + let bid = ctx.sign_bid(ctx.make_bid( slot, - builder_index: 0, - fee_recipient: Address::ZERO, - gas_limit: 30_000_000, - value: 0, - parent_block_root: ctx.genesis_block_root, - parent_block_hash: ctx.execution_parent_hash(), - prev_randao: ctx.expected_prev_randao(), - ..ExecutionPayloadBidGloas::default() - }); + 0, + Address::ZERO, + 30_000_000, + 0, + ctx.genesis_block_root, + )); let result = GossipVerifiedPayloadBid::new(bid, &gossip); assert!( result.is_ok(), @@ -1189,17 +1268,14 @@ fn two_builders_coexist_in_cache() { let slot = Slot::new(1); seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); - let bid_0 = ctx.sign_bid(ExecutionPayloadBidGloas { + let bid_0 = ctx.sign_bid(ctx.make_bid( slot, - builder_index: 0, - fee_recipient: Address::ZERO, - gas_limit: 30_000_000, - value: 0, - parent_block_root: ctx.genesis_block_root, - parent_block_hash: ctx.execution_parent_hash(), - prev_randao: ctx.expected_prev_randao(), - ..ExecutionPayloadBidGloas::default() - }); + 0, + Address::ZERO, + 30_000_000, + 0, + ctx.genesis_block_root, + )); let result_0 = GossipVerifiedPayloadBid::new(bid_0, &gossip); assert!( result_0.is_ok(), @@ -1208,17 +1284,14 @@ fn two_builders_coexist_in_cache() { ); // Builder 1 must bid strictly higher than builder 0's cached value. - let bid_1 = ctx.sign_bid(ExecutionPayloadBidGloas { + let bid_1 = ctx.sign_bid(ctx.make_bid( slot, - builder_index: 1, - fee_recipient: Address::ZERO, - gas_limit: 30_000_000, - value: 1, - parent_block_root: ctx.genesis_block_root, - parent_block_hash: ctx.execution_parent_hash(), - prev_randao: ctx.expected_prev_randao(), - ..ExecutionPayloadBidGloas::default() - }); + 1, + Address::ZERO, + 30_000_000, + 1, + ctx.genesis_block_root, + )); let result_1 = GossipVerifiedPayloadBid::new(bid_1, &gossip); assert!( result_1.is_ok(), @@ -1288,24 +1361,291 @@ fn bid_equal_to_cached_value_rejected() { incoming_value: 100, }) )); +} - #[test] - fn il_bits_inclusivity_checks_are_not_applied_for_gloas_bids() { - todo!() +#[test] +fn il_bits_inclusivity_checks_are_not_applied_for_gloas_bids() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled() && !f.heze_enabled()) { + return; } + let ctx = TestContext::new(); + let gossip = ctx.gossip_ctx(); + let slot = Slot::new(1); + seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); + seed_inclusion_list(&ctx, slot - 1, &[0], true); + + // the bid doesn't contain any inclusion list bits, so it's by definition not inclusive + // of the inclusion list bits stored in the inclusion list store + let bid = ctx.sign_bid(ctx.make_bid( + slot, + 0, + Address::ZERO, + 30_000_000, + 0, + ctx.genesis_block_root, + )); + let result = GossipVerifiedPayloadBid::new(bid, &gossip); + assert!( + result.is_ok(), + "expected Ok, got: {:?}", + result.unwrap_err() + ); +} - #[test] - fn bid_il_bits_not_inclusive() { - todo!() +#[test] +fn bid_il_bits_not_inclusive() { + if !fork_name_from_env().is_some_and(|f| f.heze_enabled()) { + return; } + let ctx = TestContext::new(); + let gossip = ctx.gossip_ctx(); - #[test] - fn bid_il_bits_are_inclusive() { - todo!() + let bid_slot = Slot::new(1); + let inclusion_list_slot = bid_slot - 1; + let inclusion_list_committee = ctx.inclusion_list_committee(inclusion_list_slot); + + seed_inclusion_list( + &ctx, + inclusion_list_slot, + &inclusion_list_committee[0..3], + true, + ); + seed_preferences(&ctx, bid_slot, Address::ZERO, 30_000_000); + + let bid = ctx.sign_bid(ExecutionPayloadBid::Heze(ExecutionPayloadBidHeze { + slot: bid_slot, + builder_index: 0, + fee_recipient: Address::ZERO, + gas_limit: 30_000_000, + value: 0, + parent_block_root: ctx.genesis_block_root, + parent_block_hash: ctx.execution_parent_hash(), + prev_randao: ctx.expected_prev_randao(), + // inclusion list bits vector only claims the IL committee members with indices 0 and 1 + inclusion_list_bits: inclusion_list_bits_for_validators( + &inclusion_list_committee, + &inclusion_list_committee[0..2], + ), + ..ExecutionPayloadBidHeze::default() + })); + + let result = GossipVerifiedPayloadBid::new(bid, &gossip); + assert!( + matches!( + result, + Err(PayloadBidError::InclusionListBitsNotInclusive{ slot }) if slot == bid_slot + ), + "unexpected result: {result:?}" + ); +} + +#[test] +fn bid_il_bits_are_inclusive() { + if !fork_name_from_env().is_some_and(|f| f.heze_enabled()) { + return; + } + let ctx = TestContext::new(); + let gossip = ctx.gossip_ctx(); + + let bid_slot = Slot::new(1); + let inclusion_list_slot = bid_slot - 1; + let inclusion_list_committee = ctx.inclusion_list_committee(inclusion_list_slot); + + seed_inclusion_list( + &ctx, + inclusion_list_slot, + &inclusion_list_committee[0..2], + true, + ); + seed_preferences(&ctx, bid_slot, Address::ZERO, 30_000_000); + + let bid = ctx.sign_bid(ExecutionPayloadBid::Heze(ExecutionPayloadBidHeze { + slot: bid_slot, + builder_index: 0, + fee_recipient: Address::ZERO, + gas_limit: 30_000_000, + value: 0, + parent_block_root: ctx.genesis_block_root, + parent_block_hash: ctx.execution_parent_hash(), + prev_randao: ctx.expected_prev_randao(), + inclusion_list_bits: inclusion_list_bits_for_validators( + &inclusion_list_committee, + &inclusion_list_committee[0..2], + ), + ..ExecutionPayloadBidHeze::default() + })); + + let result = GossipVerifiedPayloadBid::new(bid, &gossip); + assert!( + result.is_ok(), + "expected Ok, got: {:?}", + result.unwrap_err() + ); +} + +#[test] +fn bid_il_bits_checked_against_slot_before_bid() { + if !fork_name_from_env().is_some_and(|f| f.heze_enabled()) { + return; + } + let ctx = TestContext::new(); + let gossip = ctx.gossip_ctx(); + + let bid_slot = Slot::new(1); + seed_preferences(&ctx, bid_slot, Address::ZERO, 30_000_000); + + // lists stored for the bid's own slot must not be consulted + // the check looks at the slot preceding the bid's slot, which holds nothing in our case + let inclusion_list_committee = ctx.inclusion_list_committee(bid_slot); + seed_inclusion_list(&ctx, bid_slot, &inclusion_list_committee[..3], true); + + // the bid claims nothing, so it would fail against the seeded slot if that were consulted + let bid = ctx.sign_bid(ExecutionPayloadBid::Heze(ExecutionPayloadBidHeze { + slot: bid_slot, + builder_index: 0, + fee_recipient: Address::ZERO, + gas_limit: 30_000_000, + value: 0, + parent_block_root: ctx.genesis_block_root, + parent_block_hash: ctx.execution_parent_hash(), + prev_randao: ctx.expected_prev_randao(), + ..ExecutionPayloadBidHeze::default() + })); + + let result = GossipVerifiedPayloadBid::new(bid, &gossip); + assert!( + result.is_ok(), + "bid should pass, only the prior slot's lists are checked: {:?}", + result.unwrap_err() + ); +} + +#[test] +fn bid_il_bits_checks_consider_only_timely_inclusion_lists() { + if !fork_name_from_env().is_some_and(|f| f.heze_enabled()) { + return; } + let ctx = TestContext::new(); + let gossip = ctx.gossip_ctx(); + + let bid_slot = Slot::new(1); + let inclusion_list_slot = bid_slot - 1; + let inclusion_list_committee = ctx.inclusion_list_committee(inclusion_list_slot); + + seed_inclusion_list( + &ctx, + inclusion_list_slot, + &inclusion_list_committee[0..2], + true, + ); + seed_inclusion_list( + &ctx, + inclusion_list_slot, + &inclusion_list_committee[2..4], + false, + ); + seed_preferences(&ctx, bid_slot, Address::ZERO, 30_000_000); + + let bid = ctx.sign_bid(ExecutionPayloadBid::Heze(ExecutionPayloadBidHeze { + slot: bid_slot, + builder_index: 0, + fee_recipient: Address::ZERO, + gas_limit: 30_000_000, + value: 0, + parent_block_root: ctx.genesis_block_root, + parent_block_hash: ctx.execution_parent_hash(), + prev_randao: ctx.expected_prev_randao(), + // the bid only claims the bits matching the inclusion list committee indices + // of the validators that sent timely inclusion lists + inclusion_list_bits: inclusion_list_bits_for_validators( + &inclusion_list_committee, + &inclusion_list_committee[0..2], + ), + ..ExecutionPayloadBidHeze::default() + })); - #[test] - fn il_bits_inclusivity_check_applied_to_slot_prior_to_bid_slot() { - todo!() + let result = GossipVerifiedPayloadBid::new(bid, &gossip); + assert!( + result.is_ok(), + "expected Ok, got: {:?}", + result.unwrap_err() + ); +} + +#[test] +fn bid_il_bits_empty_claim_passes_against_empty_store() { + if !fork_name_from_env().is_some_and(|f| f.heze_enabled()) { + return; } + let ctx = TestContext::new(); + let gossip = ctx.gossip_ctx(); + + let bid_slot = Slot::new(1); + seed_preferences(&ctx, bid_slot, Address::ZERO, 30_000_000); + + // nothing stored for the slot preceding the bid's slot and nothing claimed by the bid; + // this is the situation of the first Heze bid at the fork boundary + let bid = ctx.sign_bid(ExecutionPayloadBid::Heze(ExecutionPayloadBidHeze { + slot: bid_slot, + builder_index: 0, + fee_recipient: Address::ZERO, + gas_limit: 30_000_000, + value: 0, + parent_block_root: ctx.genesis_block_root, + parent_block_hash: ctx.execution_parent_hash(), + prev_randao: ctx.expected_prev_randao(), + ..ExecutionPayloadBidHeze::default() + })); + + let result = GossipVerifiedPayloadBid::new(bid, &gossip); + assert!( + result.is_ok(), + "expected Ok, got: {:?}", + result.unwrap_err() + ); +} + +#[test] +fn bid_il_bits_claiming_more_than_stored_are_inclusive() { + if !fork_name_from_env().is_some_and(|f| f.heze_enabled()) { + return; + } + let ctx = TestContext::new(); + let gossip = ctx.gossip_ctx(); + + let bid_slot = Slot::new(1); + let inclusion_list_slot = bid_slot - 1; + let inclusion_list_committee = ctx.inclusion_list_committee(inclusion_list_slot); + + seed_inclusion_list( + &ctx, + inclusion_list_slot, + &inclusion_list_committee[0..2], + true, + ); + seed_preferences(&ctx, bid_slot, Address::ZERO, 30_000_000); + + // inclusivity is one-directional: the bid may claim members the node holds no list from + let bid = ctx.sign_bid(ExecutionPayloadBid::Heze(ExecutionPayloadBidHeze { + slot: bid_slot, + builder_index: 0, + fee_recipient: Address::ZERO, + gas_limit: 30_000_000, + value: 0, + parent_block_root: ctx.genesis_block_root, + parent_block_hash: ctx.execution_parent_hash(), + prev_randao: ctx.expected_prev_randao(), + inclusion_list_bits: inclusion_list_bits_for_validators( + &inclusion_list_committee, + &inclusion_list_committee, + ), + ..ExecutionPayloadBidHeze::default() + })); + + let result = GossipVerifiedPayloadBid::new(bid, &gossip); + assert!( + result.is_ok(), + "expected Ok, got: {:?}", + result.unwrap_err() + ); } From 6067991c2401cf1d92ea9c85ae55478ae8d12cc4 Mon Sep 17 00:00:00 2001 From: conache Date: Sun, 20 Sep 2026 13:02:27 +0300 Subject: [PATCH 24/44] Ensure IL dependent root is determined based on the bid's parent block root attester shuffling --- .../src/payload_bid_verification/gossip_verified_bid.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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 ba5751ec382..cc60d151841 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 @@ -475,8 +475,13 @@ impl GossipVerifiedPayloadBid { "inclusion list epoch {inclusion_list_epoch} out of range: {e:?}" )) })?; - let inclusion_list_dependent_root = head_state - .attester_shuffling_decision_root(cached_head.head_block_root(), relative_epoch)?; + + // determine inclusion list dependent root and committee necessary for determining + // the inclusion list bits for the inclusivity check + let inclusion_list_dependent_root = head_state.attester_shuffling_decision_root( + signed_bid.message().parent_block_root(), + relative_epoch, + )?; let inclusion_list_committee = head_state.get_inclusion_list_committee(inclusion_list_slot)?; From 68fd607596edfdcbb044ae5b4ebb9a059ea2f5dc Mon Sep 17 00:00:00 2001 From: conache Date: Sun, 20 Sep 2026 13:11:32 +0300 Subject: [PATCH 25/44] Use clearer comment for InclusionListBitsNotInclusive error --- beacon_node/beacon_chain/src/payload_bid_verification/mod.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 adab8fa7b61..a501bd5234a 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs @@ -92,8 +92,7 @@ pub enum PayloadBidError { }, /// The bids prev randao value is invalid InvalidPrevRandao { slot: Slot }, - /// The bid's `inclusion_list_bits` are not inclusive of the node's view of the inclusion - /// lists for the slot preceding the bid's slot + /// The bid's `inclusion_list_bits` do not cover every inclusion list the node holds for `slot - 1` InclusionListBitsNotInclusive { slot: Slot }, /// Some Beacon State error BeaconStateError(BeaconStateError), From 2e6c4fac9ee9ec2dc0c6666cab33ba9e54ed3ce0 Mon Sep 17 00:00:00 2001 From: conache Date: Sun, 20 Sep 2026 14:03:00 +0300 Subject: [PATCH 26/44] Umprove gloas bid check --- .../beacon_chain/src/payload_bid_verification/tests.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 5dcafc91cf5..9cd80003f49 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs @@ -1372,10 +1372,12 @@ fn il_bits_inclusivity_checks_are_not_applied_for_gloas_bids() { let gossip = ctx.gossip_ctx(); let slot = Slot::new(1); seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); - seed_inclusion_list(&ctx, slot - 1, &[0], true); - // the bid doesn't contain any inclusion list bits, so it's by definition not inclusive - // of the inclusion list bits stored in the inclusion list store + // a gloas bid carries no inclusion list bits, so it could never claim the stored list + // the check would reject it at Heze, so `Ok` proves the check was not applied + let inclusion_list_committee = ctx.inclusion_list_committee(slot - 1); + seed_inclusion_list(&ctx, slot - 1, &[inclusion_list_committee[0]], true); + let bid = ctx.sign_bid(ctx.make_bid( slot, 0, From 6505b5f8bcfda4adaa596a9db59c0872c1247ec0 Mon Sep 17 00:00:00 2001 From: conache Date: Thu, 24 Sep 2026 19:15:55 +0300 Subject: [PATCH 27/44] Add inclusion list bits check helper --- .../beacon_chain/src/inclusion_list_store.rs | 25 ++++++++--- .../gossip_verified_bid.rs | 43 +++++++++++++++---- .../types/src/execution/inclusion_list.rs | 5 ++- consensus/types/src/execution/mod.rs | 2 +- 4 files changed, 57 insertions(+), 18 deletions(-) diff --git a/beacon_node/beacon_chain/src/inclusion_list_store.rs b/beacon_node/beacon_chain/src/inclusion_list_store.rs index 97d29912a87..2068e80ddc0 100644 --- a/beacon_node/beacon_chain/src/inclusion_list_store.rs +++ b/beacon_node/beacon_chain/src/inclusion_list_store.rs @@ -10,7 +10,10 @@ use ssz_types::{BitVector, ProgressiveVariableList}; use std::collections::{HashMap, HashSet}; use std::marker::PhantomData; use tree_hash::TreeHash; -use types::{ChainSpec, EthSpec, Hash256, InclusionListCommittee, SignedInclusionList, Slot}; +use types::{ + ChainSpec, EthSpec, Hash256, InclusionListBits, InclusionListCommittee, SignedInclusionList, + Slot, +}; /// The shuffling `dependent_root` an inclusion list was produced against. pub type DependentRoot = Hash256; @@ -63,6 +66,19 @@ pub struct InclusionListStore { _phantom: PhantomData, } +/// Returns whether `bits` is inclusive of `local`: every bit set in `local` is also set in `bits`. +pub fn inclusion_list_bits_are_inclusive( + local: &InclusionListBits, + bits: &InclusionListBits, +) -> Result { + for i in 0..local.len() { + if local.get(i)? && !bits.get(i)? { + return Ok(false); + } + } + Ok(true) +} + impl InclusionListStore { pub fn new(spec: &ChainSpec) -> Self { // `heze_fork_epoch` holds the far future sentinel when Heze is unscheduled. @@ -235,12 +251,7 @@ impl InclusionListStore { ) -> Result { let local = self.get_inclusion_list_bits(slot, dependent_root, il_committee, only_timely)?; - for i in 0..local.len() { - if local.get(i)? && !bits.get(i)? { - return Ok(false); - } - } - Ok(true) + inclusion_list_bits_are_inclusive::(&local, bits) } /// The stored signed inclusion lists for the given `validators`, used to serve 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 cc60d151841..f3679289ea9 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 @@ -1,6 +1,6 @@ use std::sync::Arc; -use crate::inclusion_list_store::InclusionListStore; +use crate::inclusion_list_store::{InclusionListStore, inclusion_list_bits_are_inclusive}; use crate::{ BeaconChain, BeaconChainTypes, BeaconStore, CachedHead, CanonicalHead, canonical_head::ForkChoiceReadGuard, @@ -22,7 +22,7 @@ use state_processing::signature_sets::{ use tracing::debug; use types::{ BeaconState, Builder, ChainSpec, EthSpec, ExecutionPayloadBidRef, ExecutionRequestsGloas, - RelativeEpoch, SignedExecutionPayloadBid, SignedProposerPreferences, Slot, + InclusionListBits, RelativeEpoch, SignedExecutionPayloadBid, SignedProposerPreferences, Slot, consts::gloas::PAYLOAD_BUILDER_VERSION, }; @@ -85,6 +85,34 @@ fn verify_bid_blobs( Ok(()) } +/// Reject a bid whose `inclusion_list_bits` do not cover `local_inclusion_list_bits`, the node's +/// own view of the inclusion lists for the slot before the bid's. Pre-Heze bids carry no bits and pass. +/// +/// Shared by both intakes, each resolves the local bits under its own timeliness rule: +/// timely inclusion lists only on gossip, and all (timely and untimely) inclusion lists on the +/// proposer's block production path. +pub(crate) fn verify_bid_inclusion_list_bits( + bid: ExecutionPayloadBidRef, + local_inclusion_list_bits: &InclusionListBits, +) -> Result<(), PayloadBidError> { + let ExecutionPayloadBidRef::Heze(bid) = bid else { + return Ok(()); + }; + + let inclusion_list_inclusive = + inclusion_list_bits_are_inclusive::(local_inclusion_list_bits, &bid.inclusion_list_bits) + .map_err(|e| { + PayloadBidError::InternalError(format!( + "inclusion list bits inclusivity check error: {e:?}" + )) + })?; + + if !inclusion_list_inclusive { + return Err(PayloadBidError::InclusionListBitsNotInclusive { slot: bid.slot }); + } + Ok(()) +} + /// Verify that a direct (builder-API) bid is consistent with the current chain state /// and proposer preferences: the direct path's bundle of the shared bid checks. /// @@ -465,7 +493,6 @@ impl GossipVerifiedPayloadBid { .fork_name_at_slot::(bid_slot) .heze_enabled() { - let inclusion_list_bits = signed_bid.message().inclusion_list_bits()?; let inclusion_list_slot = bid_slot.saturating_sub(1u64); let inclusion_list_epoch = inclusion_list_slot.epoch(T::EthSpec::slots_per_epoch()); let relative_epoch = @@ -485,22 +512,20 @@ impl GossipVerifiedPayloadBid { let inclusion_list_committee = head_state.get_inclusion_list_committee(inclusion_list_slot)?; - let inclusive = ctx + // Only the timely inclusion lists count on gossip + let local_inclusion_list_bits = ctx .inclusion_list_store .read() - .is_inclusion_list_bits_inclusive( + .get_inclusion_list_bits( inclusion_list_slot, inclusion_list_dependent_root, &inclusion_list_committee, - inclusion_list_bits, true, ) .map_err(|e| { PayloadBidError::InternalError(format!("inclusion list store: {e:?}")) })?; - if !inclusive { - return Err(PayloadBidError::InclusionListBitsNotInclusive { slot: bid_slot }); - } + verify_bid_inclusion_list_bits(signed_bid.message(), &local_inclusion_list_bits)?; } execution_payload_bid_signature_set( diff --git a/consensus/types/src/execution/inclusion_list.rs b/consensus/types/src/execution/inclusion_list.rs index ed6739a2460..fd585d1d42a 100644 --- a/consensus/types/src/execution/inclusion_list.rs +++ b/consensus/types/src/execution/inclusion_list.rs @@ -3,12 +3,15 @@ use context_deserialize::context_deserialize; use educe::Educe; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; -use ssz_types::FixedVector; +use ssz_types::{BitVector, FixedVector}; use tree_hash_derive::TreeHash; /// The inclusion list committee, ordered by committee position. pub type InclusionListCommittee = FixedVector::InclusionListCommitteeSize>; +/// One bit per inclusion list committee member, in committee position order. +pub type InclusionListBits = BitVector<::InclusionListCommitteeSize>; + #[derive(Default, Debug, Clone, Serialize, Encode, Decode, Deserialize, TreeHash, Educe)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[educe(PartialEq, Hash)] diff --git a/consensus/types/src/execution/mod.rs b/consensus/types/src/execution/mod.rs index 4b08020cda0..4297871e43e 100644 --- a/consensus/types/src/execution/mod.rs +++ b/consensus/types/src/execution/mod.rs @@ -44,7 +44,7 @@ pub use execution_requests::{ ExecutionRequests, ExecutionRequestsElectra, ExecutionRequestsGloas, ExecutionRequestsRef, RequestType, WithdrawalRequests, }; -pub use inclusion_list::{InclusionList, InclusionListCommittee}; +pub use inclusion_list::{InclusionList, InclusionListBits, InclusionListCommittee}; pub use payload::{ AbstractExecPayload, BlindedPayload, BlindedPayloadBellatrix, BlindedPayloadCapella, BlindedPayloadDeneb, BlindedPayloadElectra, BlindedPayloadFulu, BlindedPayloadRef, From aea07596d46d0370146d740250179f33f6e9fbaf Mon Sep 17 00:00:00 2001 From: conache Date: Wed, 30 Sep 2026 13:40:25 +0300 Subject: [PATCH 28/44] Use Box for the process_block future in the weak_subjectivity_sync_test to fix the lint error --- beacon_node/beacon_chain/tests/store_tests.rs | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index cde2372d649..231ce79a759 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -4376,19 +4376,15 @@ async fn weak_subjectivity_sync_test( info!(block_root = ?full_block_root, ?state_root, %slot, "Importing block from chain dump"); beacon_chain.slot_clock.set_slot(slot.as_u64()); - beacon_chain - .process_block( - full_block_root, - harness.build_range_sync_block_from_store_blobs( - Some(block_root), - Arc::new(full_block), - ), - NotifyExecutionLayer::Yes, - BlockImportSource::Lookup, - || Ok(()), - ) - .await - .unwrap(); + Box::pin(beacon_chain.process_block( + full_block_root, + harness.build_range_sync_block_from_store_blobs(Some(block_root), Arc::new(full_block)), + NotifyExecutionLayer::Yes, + BlockImportSource::Lookup, + || Ok(()), + )) + .await + .unwrap(); // Store the envelope, its columns, and apply to fork choice. if let Some(envelope) = &snapshot.execution_envelope { From e9463c49c3e1987cee27f28d6b949a82a09006c4 Mon Sep 17 00:00:00 2001 From: conache Date: Tue, 6 Oct 2026 15:58:01 +0300 Subject: [PATCH 29/44] Remove redundant TODO --- beacon_node/beacon_chain/src/block_production/gloas.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/beacon_node/beacon_chain/src/block_production/gloas.rs b/beacon_node/beacon_chain/src/block_production/gloas.rs index 95caf399c6e..d4f3c22344c 100644 --- a/beacon_node/beacon_chain/src/block_production/gloas.rs +++ b/beacon_node/beacon_chain/src/block_production/gloas.rs @@ -1041,8 +1041,6 @@ impl BeaconChain { )); } BeaconState::Gloas(_) => { - // TODO(gloas) since we are defaulting to local building, execution payment is 0 - // execution payment should only be set to > 0 for trusted building. let bid = ExecutionPayloadBidGloas:: { parent_block_hash: executed_ancestor_hash, parent_block_root: parent_root, From 3a64770678a9d978dbc1792fdaf005494c485c45 Mon Sep 17 00:00:00 2001 From: conache Date: Tue, 6 Oct 2026 17:40:52 +0300 Subject: [PATCH 30/44] Check the bid variant matches the fork at its slot --- .../gossip_verified_bid.rs | 5 +++ .../src/payload_bid_verification/mod.rs | 4 ++- .../src/payload_bid_verification/tests.rs | 32 +++++++++++++++--- .../gossip_methods.rs | 3 +- .../src/execution/execution_payload_bid.rs | 33 +++++++++++++++++-- .../execution/signed_execution_payload_bid.rs | 6 +++- 6 files changed, 73 insertions(+), 10 deletions(-) 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 e67be98ab08..74cfb08c79c 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 @@ -286,6 +286,11 @@ impl GossipVerifiedPayloadBid { where T: BeaconChainTypes, { + // Ensure the bid is the correct structure for the fork at `bid.slot()`. + signed_bid + .fork_name(ctx.spec) + .map_err(PayloadBidError::InconsistentFork)?; + let bid_slot = signed_bid.message().slot(); let bid_parent = BidParent::from_bid(signed_bid.message()); let bid_parent_block_root = signed_bid.message().parent_block_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 98ad661adf8..935db42a15b 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs @@ -10,7 +10,7 @@ //! GossipVerifiedPayloadBid -------> Insert into GossipVerifiedPayloadBidCache //! ``` -use types::{BeaconStateError, ExecutionBlockHash, Hash256, Slot}; +use types::{BeaconStateError, ExecutionBlockHash, Hash256, InconsistentFork, Slot}; pub mod direct_verified_bid; pub mod gossip_verified_bid; @@ -94,6 +94,8 @@ pub enum PayloadBidError { InvalidPrevRandao { slot: Slot }, /// Some Beacon State error BeaconStateError(BeaconStateError), + /// The bid's variant does not match the fork at its slot. + InconsistentFork(InconsistentFork), /// Internal error InternalError(String), } 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 8f5d86fc7a5..19bbbd21712 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs @@ -13,11 +13,11 @@ use state_processing::genesis::genesis_block; use store::{HotColdDB, StoreConfig, StoreOp}; use types::{ Address, BuilderExitRequest, ChainSpec, Checkpoint, Domain, Epoch, EthSpec, ExecutionBlockHash, - ExecutionPayloadBidGloas, ExecutionPayloadBidRef, ExecutionPayloadEnvelope, - ExecutionPayloadHeader, ExecutionPayloadHeaderFulu, Hash256, MinimalEthSpec, - ProposerPreferences, SignedBeaconBlock, SignedExecutionPayloadBid, - SignedExecutionPayloadBidGloas, SignedExecutionPayloadEnvelope, SignedProposerPreferences, - SignedRoot, Slot, consts::gloas::PAYLOAD_BUILDER_VERSION, + ExecutionPayloadBidGloas, ExecutionPayloadBidHeze, ExecutionPayloadBidRef, + ExecutionPayloadEnvelope, ExecutionPayloadHeader, ExecutionPayloadHeaderFulu, ForkName, + Hash256, MinimalEthSpec, ProposerPreferences, SignedBeaconBlock, SignedExecutionPayloadBid, + SignedExecutionPayloadBidGloas, SignedExecutionPayloadBidHeze, SignedExecutionPayloadEnvelope, + SignedProposerPreferences, SignedRoot, Slot, consts::gloas::PAYLOAD_BUILDER_VERSION, }; use proto_array::{Block as ProtoBlock, ExecutionStatus}; @@ -532,6 +532,28 @@ fn invalid_bid_slot() { )); } +#[test] +fn inconsistent_fork() { + // A Heze bid is only inconsistent with the slot's fork before Heze. + if fork_name_from_env() != Some(ForkName::Gloas) { + return; + } + let ctx = TestContext::new(); + let gossip = ctx.gossip_ctx(); + + let bid = Arc::new(SignedExecutionPayloadBid::Heze( + SignedExecutionPayloadBidHeze { + message: ExecutionPayloadBidHeze { + slot: Slot::new(1), + ..ExecutionPayloadBidHeze::default() + }, + signature: Signature::empty(), + }, + )); + let result = GossipVerifiedPayloadBid::new(bid, &gossip); + assert!(matches!(result, Err(PayloadBidError::InconsistentFork(_)))); +} + #[test] fn fee_recipient_mismatch() { if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { 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 e75632f861b..2ce26304cde 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -4261,7 +4261,8 @@ impl NetworkBeaconProcessor { | PayloadBidError::BlockHashEqualsParentBlockHash { .. } | PayloadBidError::InvalidBlobKzgCommitments { .. } | PayloadBidError::BidNotDescendantOfParent { .. } - | PayloadBidError::InvalidPrevRandao { .. }, + | PayloadBidError::InvalidPrevRandao { .. } + | PayloadBidError::InconsistentFork(_), ) => { self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject); self.gossip_penalize_peer( diff --git a/consensus/types/src/execution/execution_payload_bid.rs b/consensus/types/src/execution/execution_payload_bid.rs index a8914dcd9b0..ea323e9ac3a 100644 --- a/consensus/types/src/execution/execution_payload_bid.rs +++ b/consensus/types/src/execution/execution_payload_bid.rs @@ -1,7 +1,7 @@ use crate::kzg_ext::ProgressiveKzgCommitments; use crate::{ - Address, BeaconStateError, EthSpec, ExecutionBlockHash, ForkName, ForkVersionDecode, Hash256, - SignedRoot, Slot, + Address, BeaconStateError, ChainSpec, EthSpec, ExecutionBlockHash, ForkName, ForkVersionDecode, + Hash256, InconsistentFork, SignedRoot, Slot, }; use context_deserialize::{ContextDeserialize, context_deserialize}; use educe::Educe; @@ -144,6 +144,35 @@ impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for ExecutionPayloadBid< } } +impl<'a, E: EthSpec> ExecutionPayloadBidRef<'a, E> { + /// Returns the name of the fork pertaining to `self`. + /// + /// Will return an `Err` if `self` has been instantiated to a variant conflicting with the fork + /// dictated by `self.slot()`. + pub fn fork_name(&self, spec: &ChainSpec) -> Result { + let fork_at_slot = spec.fork_name_at_slot::(self.slot()); + let object_fork = self.fork_name_unchecked(); + + if fork_at_slot == object_fork { + Ok(object_fork) + } else { + Err(InconsistentFork { + fork_at_slot, + object_fork, + }) + } + } + + /// Returns the name of the fork pertaining to `self`. + /// Does not check that the fork is consistent with the slot. + pub fn fork_name_unchecked(&self) -> ForkName { + match self { + ExecutionPayloadBidRef::Gloas(_) => ForkName::Gloas, + ExecutionPayloadBidRef::Heze(_) => ForkName::Heze, + } + } +} + #[cfg(test)] mod gloas_tests { use super::*; diff --git a/consensus/types/src/execution/signed_execution_payload_bid.rs b/consensus/types/src/execution/signed_execution_payload_bid.rs index 98873ddc479..92d22351082 100644 --- a/consensus/types/src/execution/signed_execution_payload_bid.rs +++ b/consensus/types/src/execution/signed_execution_payload_bid.rs @@ -1,5 +1,5 @@ use crate::execution::{ExecutionPayloadBidGloas, ExecutionPayloadBidHeze, ExecutionPayloadBidRef}; -use crate::{Epoch, EthSpec, ForkName, ForkVersionDecode, Slot}; +use crate::{ChainSpec, Epoch, EthSpec, ForkName, ForkVersionDecode, InconsistentFork, Slot}; use bls::Signature; use context_deserialize::{ContextDeserialize, context_deserialize}; use educe::Educe; @@ -52,6 +52,10 @@ impl SignedExecutionPayloadBid { } } + pub fn fork_name(&self, spec: &ChainSpec) -> Result { + self.message().fork_name(spec) + } + pub fn slot(&self) -> crate::Slot { self.message().slot() } From 38f3e1b78f03c48bfb83c2f74b9fd3310e8dd304 Mon Sep 17 00:00:00 2001 From: conache Date: Tue, 6 Oct 2026 19:31:41 +0300 Subject: [PATCH 31/44] Ensure bids are pushed to the correct topic at the fork boundary --- .../lighthouse_network/src/service/mod.rs | 7 +- .../lighthouse_network/src/types/pubsub.rs | 70 +++++++++++++++++-- 2 files changed, 71 insertions(+), 6 deletions(-) diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index 389f07d7ef1..b6b91f15f09 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -858,7 +858,12 @@ impl Network { /// Publishes a list of messages on the pubsub (gossipsub) behaviour, choosing the encoding. pub fn publish(&mut self, messages: Vec>) { for message in messages { - for topic in message.topics(GossipEncoding::default(), self.enr_fork_id.fork_digest) { + let fork_digest = message + .context_epoch() + .map_or(self.enr_fork_id.fork_digest, |epoch| { + self.fork_context.context_bytes(epoch) + }); + for topic in message.topics(GossipEncoding::default(), fork_digest) { let message_data = message.encode(GossipEncoding::default()); if let Err(e) = self .gossipsub_mut() diff --git a/beacon_node/lighthouse_network/src/types/pubsub.rs b/beacon_node/lighthouse_network/src/types/pubsub.rs index 192d9037bfd..80788780c36 100644 --- a/beacon_node/lighthouse_network/src/types/pubsub.rs +++ b/beacon_node/lighthouse_network/src/types/pubsub.rs @@ -9,9 +9,9 @@ use std::io::{Error, ErrorKind}; use std::sync::Arc; use types::{ AttesterSlashing, AttesterSlashingBase, AttesterSlashingElectra, AttesterSlashingGloas, - CellBitmap, DataColumnSidecar, DataColumnSubnetId, EthSpec, ForkContext, ForkVersionDecode, - Hash256, LightClientFinalityUpdate, LightClientOptimisticUpdate, PartialDataColumn, - PartialDataColumnFulu, PartialDataColumnGloas, PartialDataColumnGroupId, + CellBitmap, DataColumnSidecar, DataColumnSubnetId, Epoch, EthSpec, ForkContext, + ForkVersionDecode, Hash256, LightClientFinalityUpdate, LightClientOptimisticUpdate, + PartialDataColumn, PartialDataColumnFulu, PartialDataColumnGloas, PartialDataColumnGroupId, PartialDataColumnHeader, PartialDataColumnSidecarFulu, PartialDataColumnSidecarGloas, PayloadAttestationMessage, ProposerSlashing, SignedAggregateAndProof, SignedAggregateAndProofBase, SignedAggregateAndProofElectra, SignedAggregateAndProofGloas, @@ -158,6 +158,15 @@ impl PubsubMessage { vec![GossipTopic::new(self.kind(), encoding, fork_version)] } + /// The epoch whose fork digest to publish under, or `None` to use the current one. + pub fn context_epoch(&self) -> Option { + match self { + // Bids can be for the next slot, so they could cross the fork boundary + PubsubMessage::ExecutionPayloadBid(bid) => Some(bid.epoch()), + _ => None, + } + } + /// Returns the kind of gossipsub topic associated with the message. pub fn kind(&self) -> GossipKind { match self { @@ -699,8 +708,8 @@ mod tests { use libp2p::gossipsub::partial_messages::Partial; use types::data::{CellBitmap, PartialDataColumnSidecarGloas}; use types::{ - BeaconBlock, ChainSpec, Epoch, EthSpec, ForkName, MainnetEthSpec, Slot, - data::DataColumnSubnetId, + BeaconBlock, ChainSpec, Epoch, EthSpec, ExecutionPayloadBidHeze, ForkName, MainnetEthSpec, + SignedExecutionPayloadBidHeze, Slot, data::DataColumnSubnetId, }; type E = MainnetEthSpec; @@ -806,6 +815,17 @@ mod tests { PubsubMessage::decode(&topic_hash, &data, fork_context) } + fn decode_from_topic( + fork_context: &ForkContext, + kind: GossipKind, + fork_digest: [u8; 4], + data: &[u8], + ) -> Result, String> { + let topic = GossipTopic::new(kind, GossipEncoding::default(), fork_digest); + let topic_hash = TopicHash::from_raw(String::from(topic)); + PubsubMessage::decode(&topic_hash, data, fork_context) + } + #[test] fn gloas_aggregate_and_proof_size_bound() { let max = E::max_signed_aggregate_and_proof_size(); @@ -939,4 +959,44 @@ mod tests { decode_oversized(&gloas_fork_context(), GossipKind::InclusionList, max).unwrap_err(); assert!(!err.contains("MAX_SIGNED_INCLUSION_LIST_SIZE"), "{err}"); } + + #[test] + fn heze_bid_published_before_the_fork_uses_the_heze_topic() { + let mut spec = pre_gloas_spec(); + spec.gloas_fork_epoch = Some(Epoch::new(0)); + spec.heze_fork_epoch = Some(Epoch::new(1)); + let first_heze_slot = Epoch::new(1).start_slot(E::slots_per_epoch()); + // The node is still in the last Gloas slot + let fork_context = ForkContext::new::(first_heze_slot - 1, Hash256::ZERO, &spec); + + let bid = SignedExecutionPayloadBid::::Heze(SignedExecutionPayloadBidHeze { + message: ExecutionPayloadBidHeze { + slot: first_heze_slot, + ..ExecutionPayloadBidHeze::default() + }, + signature: Signature::empty(), + }); + let message = PubsubMessage::ExecutionPayloadBid(Box::new(bid.clone())); + let data = bid.as_ssz_bytes(); + + let fork_digest = fork_context.context_bytes(message.context_epoch().unwrap()); + assert_eq!( + decode_from_topic( + &fork_context, + GossipKind::ExecutionPayloadBid, + fork_digest, + &data + ), + Ok(message) + ); + assert!( + decode_from_topic( + &fork_context, + GossipKind::ExecutionPayloadBid, + fork_context.current_fork_digest(), + &data + ) + .is_err() + ); + } } From f265250e1704903b8c73fb0f9307b73056dfead3 Mon Sep 17 00:00:00 2001 From: conache Date: Tue, 6 Oct 2026 19:58:28 +0300 Subject: [PATCH 32/44] Publish bids through publish_fork_digest --- .../lighthouse_network/src/service/mod.rs | 28 +++++++- .../lighthouse_network/src/types/pubsub.rs | 70 ++----------------- 2 files changed, 32 insertions(+), 66 deletions(-) diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index cb198494f13..e640e7d7c70 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -2208,7 +2208,8 @@ mod tests { use super::*; use bls::Signature; use types::{ - Epoch, Hash256, MinimalEthSpec, ProposerPreferences, SignedProposerPreferences, + Epoch, ExecutionPayloadBidHeze, Hash256, MinimalEthSpec, ProposerPreferences, + SignedExecutionPayloadBid, SignedExecutionPayloadBidHeze, SignedProposerPreferences, SignedVoluntaryExit, VoluntaryExit, }; @@ -2251,4 +2252,29 @@ mod tests { current_digest ); } + + #[test] + fn publish_fork_digest_uses_bid_epoch_for_bids() { + let heze_epoch = Epoch::new(2); + let mut spec = ForkName::Gloas.make_genesis_spec(E::default_spec()); + spec.heze_fork_epoch = Some(heze_epoch); + let fork_context = ForkContext::new::(Slot::new(0), Hash256::ZERO, &spec); + let current_digest = fork_context.current_fork_digest(); + let heze_digest = fork_context.context_bytes(heze_epoch); + + let bid = PubsubMessage::::ExecutionPayloadBid(Box::new( + SignedExecutionPayloadBid::Heze(SignedExecutionPayloadBidHeze { + message: ExecutionPayloadBidHeze { + slot: heze_epoch.start_slot(E::slots_per_epoch()), + ..ExecutionPayloadBidHeze::default() + }, + signature: Signature::empty(), + }), + )); + + assert_eq!( + publish_fork_digest(&bid, &fork_context, current_digest), + heze_digest + ); + } } diff --git a/beacon_node/lighthouse_network/src/types/pubsub.rs b/beacon_node/lighthouse_network/src/types/pubsub.rs index 80788780c36..192d9037bfd 100644 --- a/beacon_node/lighthouse_network/src/types/pubsub.rs +++ b/beacon_node/lighthouse_network/src/types/pubsub.rs @@ -9,9 +9,9 @@ use std::io::{Error, ErrorKind}; use std::sync::Arc; use types::{ AttesterSlashing, AttesterSlashingBase, AttesterSlashingElectra, AttesterSlashingGloas, - CellBitmap, DataColumnSidecar, DataColumnSubnetId, Epoch, EthSpec, ForkContext, - ForkVersionDecode, Hash256, LightClientFinalityUpdate, LightClientOptimisticUpdate, - PartialDataColumn, PartialDataColumnFulu, PartialDataColumnGloas, PartialDataColumnGroupId, + CellBitmap, DataColumnSidecar, DataColumnSubnetId, EthSpec, ForkContext, ForkVersionDecode, + Hash256, LightClientFinalityUpdate, LightClientOptimisticUpdate, PartialDataColumn, + PartialDataColumnFulu, PartialDataColumnGloas, PartialDataColumnGroupId, PartialDataColumnHeader, PartialDataColumnSidecarFulu, PartialDataColumnSidecarGloas, PayloadAttestationMessage, ProposerSlashing, SignedAggregateAndProof, SignedAggregateAndProofBase, SignedAggregateAndProofElectra, SignedAggregateAndProofGloas, @@ -158,15 +158,6 @@ impl PubsubMessage { vec![GossipTopic::new(self.kind(), encoding, fork_version)] } - /// The epoch whose fork digest to publish under, or `None` to use the current one. - pub fn context_epoch(&self) -> Option { - match self { - // Bids can be for the next slot, so they could cross the fork boundary - PubsubMessage::ExecutionPayloadBid(bid) => Some(bid.epoch()), - _ => None, - } - } - /// Returns the kind of gossipsub topic associated with the message. pub fn kind(&self) -> GossipKind { match self { @@ -708,8 +699,8 @@ mod tests { use libp2p::gossipsub::partial_messages::Partial; use types::data::{CellBitmap, PartialDataColumnSidecarGloas}; use types::{ - BeaconBlock, ChainSpec, Epoch, EthSpec, ExecutionPayloadBidHeze, ForkName, MainnetEthSpec, - SignedExecutionPayloadBidHeze, Slot, data::DataColumnSubnetId, + BeaconBlock, ChainSpec, Epoch, EthSpec, ForkName, MainnetEthSpec, Slot, + data::DataColumnSubnetId, }; type E = MainnetEthSpec; @@ -815,17 +806,6 @@ mod tests { PubsubMessage::decode(&topic_hash, &data, fork_context) } - fn decode_from_topic( - fork_context: &ForkContext, - kind: GossipKind, - fork_digest: [u8; 4], - data: &[u8], - ) -> Result, String> { - let topic = GossipTopic::new(kind, GossipEncoding::default(), fork_digest); - let topic_hash = TopicHash::from_raw(String::from(topic)); - PubsubMessage::decode(&topic_hash, data, fork_context) - } - #[test] fn gloas_aggregate_and_proof_size_bound() { let max = E::max_signed_aggregate_and_proof_size(); @@ -959,44 +939,4 @@ mod tests { decode_oversized(&gloas_fork_context(), GossipKind::InclusionList, max).unwrap_err(); assert!(!err.contains("MAX_SIGNED_INCLUSION_LIST_SIZE"), "{err}"); } - - #[test] - fn heze_bid_published_before_the_fork_uses_the_heze_topic() { - let mut spec = pre_gloas_spec(); - spec.gloas_fork_epoch = Some(Epoch::new(0)); - spec.heze_fork_epoch = Some(Epoch::new(1)); - let first_heze_slot = Epoch::new(1).start_slot(E::slots_per_epoch()); - // The node is still in the last Gloas slot - let fork_context = ForkContext::new::(first_heze_slot - 1, Hash256::ZERO, &spec); - - let bid = SignedExecutionPayloadBid::::Heze(SignedExecutionPayloadBidHeze { - message: ExecutionPayloadBidHeze { - slot: first_heze_slot, - ..ExecutionPayloadBidHeze::default() - }, - signature: Signature::empty(), - }); - let message = PubsubMessage::ExecutionPayloadBid(Box::new(bid.clone())); - let data = bid.as_ssz_bytes(); - - let fork_digest = fork_context.context_bytes(message.context_epoch().unwrap()); - assert_eq!( - decode_from_topic( - &fork_context, - GossipKind::ExecutionPayloadBid, - fork_digest, - &data - ), - Ok(message) - ); - assert!( - decode_from_topic( - &fork_context, - GossipKind::ExecutionPayloadBid, - fork_context.current_fork_digest(), - &data - ) - .is_err() - ); - } } From e5e704b2baae0f0ddfd59b363ab848cae3c86311 Mon Sep 17 00:00:00 2001 From: conache Date: Wed, 7 Oct 2026 12:15:51 +0300 Subject: [PATCH 33/44] List fork variants explicitly and require ContextDeserialize for bids --- .../src/per_block_processing.rs | 5 ++- .../types/src/block/beacon_block_body.rs | 8 ++++- .../src/execution/execution_payload_bid.rs | 34 +++++++++++++------ .../execution/signed_execution_payload_bid.rs | 32 +++++++++++------ consensus/types/src/state/beacon_state.rs | 16 +++++++-- testing/ef_tests/src/cases/operations.rs | 2 +- 6 files changed, 71 insertions(+), 26 deletions(-) diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index 610d3052491..acc4d2791d9 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -888,7 +888,10 @@ pub fn process_execution_payload_bid( (ExecutionPayloadBidRefMut::Heze(bid_mut), ExecutionPayloadBidRef::Heze(cached)) => { *bid_mut = cached.clone(); } - _ => return Err(BlockProcessingError::IncorrectStateType), + (ExecutionPayloadBidRefMut::Gloas(_), ExecutionPayloadBidRef::Heze(_)) + | (ExecutionPayloadBidRefMut::Heze(_), ExecutionPayloadBidRef::Gloas(_)) => { + return Err(BlockProcessingError::IncorrectStateType); + } } Ok(()) diff --git a/consensus/types/src/block/beacon_block_body.rs b/consensus/types/src/block/beacon_block_body.rs index cc80afb4ae0..e6f79ad2e40 100644 --- a/consensus/types/src/block/beacon_block_body.rs +++ b/consensus/types/src/block/beacon_block_body.rs @@ -296,13 +296,19 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, &self, ) -> Result, BeaconStateError> { match self { + Self::Base(_) + | Self::Altair(_) + | Self::Bellatrix(_) + | Self::Capella(_) + | Self::Deneb(_) + | Self::Electra(_) + | Self::Fulu(_) => Err(BeaconStateError::IncorrectStateVariant), Self::Gloas(body) => Ok(SignedExecutionPayloadBidRef::Gloas( &body.signed_execution_payload_bid, )), Self::Heze(body) => Ok(SignedExecutionPayloadBidRef::Heze( &body.signed_execution_payload_bid, )), - _ => Err(BeaconStateError::IncorrectStateVariant), } } diff --git a/consensus/types/src/execution/execution_payload_bid.rs b/consensus/types/src/execution/execution_payload_bid.rs index 372ac55c794..863a7d43c24 100644 --- a/consensus/types/src/execution/execution_payload_bid.rs +++ b/consensus/types/src/execution/execution_payload_bid.rs @@ -28,7 +28,7 @@ use tree_hash_derive::TreeHash; ), context_deserialize(ForkName), educe(PartialEq, Hash(bound(E: EthSpec))), - serde(bound = "E: EthSpec", deny_unknown_fields), + serde(bound = "E: EthSpec"), cfg_attr( feature = "arbitrary", derive(arbitrary::Arbitrary), @@ -60,7 +60,7 @@ use tree_hash_derive::TreeHash; derive(arbitrary::Arbitrary), arbitrary(bound = "E: EthSpec") )] -#[derive(Debug, Clone, Serialize, Deserialize, Encode, TreeHash, Educe)] +#[derive(Debug, Clone, Serialize, Encode, TreeHash, Educe)] #[educe(PartialEq, Hash(bound(E: EthSpec)))] #[serde(bound = "E: EthSpec", untagged)] #[ssz(enum_behaviour = "transparent")] @@ -107,11 +107,17 @@ impl<'a, E: EthSpec> SignedRoot for ExecutionPayloadBidRef<'a, E> {} impl ForkVersionDecode for ExecutionPayloadBid { fn from_ssz_bytes_by_fork(bytes: &[u8], fork_name: ForkName) -> Result { match fork_name { - ForkName::Gloas => ExecutionPayloadBidGloas::from_ssz_bytes(bytes).map(Self::Gloas), - ForkName::Heze => ExecutionPayloadBidHeze::from_ssz_bytes(bytes).map(Self::Heze), - _ => Err(ssz::DecodeError::BytesInvalid(format!( + ForkName::Base + | ForkName::Altair + | ForkName::Bellatrix + | ForkName::Capella + | ForkName::Deneb + | ForkName::Electra + | ForkName::Fulu => Err(ssz::DecodeError::BytesInvalid(format!( "unsupported fork for ExecutionPayloadBid: {fork_name}" ))), + ForkName::Gloas => ExecutionPayloadBidGloas::from_ssz_bytes(bytes).map(Self::Gloas), + ForkName::Heze => ExecutionPayloadBidHeze::from_ssz_bytes(bytes).map(Self::Heze), } } } @@ -128,18 +134,24 @@ impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for ExecutionPayloadBid< )) }; Ok(match context { + ForkName::Base + | ForkName::Altair + | ForkName::Bellatrix + | ForkName::Capella + | ForkName::Deneb + | ForkName::Electra + | ForkName::Fulu => { + return Err(serde::de::Error::custom(format!( + "ExecutionPayloadBid failed to deserialize: unsupported fork '{}'", + context + ))); + } ForkName::Gloas => { Self::Gloas(Deserialize::deserialize(deserializer).map_err(convert_err)?) } ForkName::Heze => { Self::Heze(Deserialize::deserialize(deserializer).map_err(convert_err)?) } - _ => { - return Err(serde::de::Error::custom(format!( - "ExecutionPayloadBid failed to deserialize: unsupported fork '{}'", - context - ))); - } }) } } diff --git a/consensus/types/src/execution/signed_execution_payload_bid.rs b/consensus/types/src/execution/signed_execution_payload_bid.rs index 0e535c70522..9d095a9737e 100644 --- a/consensus/types/src/execution/signed_execution_payload_bid.rs +++ b/consensus/types/src/execution/signed_execution_payload_bid.rs @@ -29,7 +29,7 @@ use tree_hash_derive::TreeHash; derive(arbitrary::Arbitrary), arbitrary(bound = "E: EthSpec") )] -#[derive(Debug, Clone, Serialize, Deserialize, Encode, TreeHash, Educe)] +#[derive(Debug, Clone, Serialize, Encode, TreeHash, Educe)] #[educe(PartialEq, Hash(bound(E: EthSpec)))] #[serde(bound = "E: EthSpec", untagged)] #[ssz(enum_behaviour = "transparent")] @@ -126,13 +126,19 @@ impl<'a, E: EthSpec> SignedExecutionPayloadBidRef<'a, E> { impl ForkVersionDecode for SignedExecutionPayloadBid { 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 + | ForkName::Electra + | ForkName::Fulu => Err(ssz::DecodeError::BytesInvalid(format!( + "unsupported fork for SignedExecutionPayloadBid: {fork_name}" + ))), ForkName::Gloas => { SignedExecutionPayloadBidGloas::from_ssz_bytes(bytes).map(Self::Gloas) } ForkName::Heze => SignedExecutionPayloadBidHeze::from_ssz_bytes(bytes).map(Self::Heze), - _ => Err(ssz::DecodeError::BytesInvalid(format!( - "unsupported fork for SignedExecutionPayloadBid: {fork_name}" - ))), } } } @@ -149,18 +155,24 @@ impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for SignedExecutionPaylo )) }; Ok(match context { + ForkName::Base + | ForkName::Altair + | ForkName::Bellatrix + | ForkName::Capella + | ForkName::Deneb + | ForkName::Electra + | ForkName::Fulu => { + return Err(serde::de::Error::custom(format!( + "SignedExecutionPayloadBid failed to deserialize: unsupported fork '{}'", + context + ))); + } ForkName::Gloas => { Self::Gloas(Deserialize::deserialize(deserializer).map_err(convert_err)?) } ForkName::Heze => { Self::Heze(Deserialize::deserialize(deserializer).map_err(convert_err)?) } - _ => { - return Err(serde::de::Error::custom(format!( - "SignedExecutionPayloadBid failed to deserialize: unsupported fork '{}'", - context - ))); - } }) } } diff --git a/consensus/types/src/state/beacon_state.rs b/consensus/types/src/state/beacon_state.rs index b76e10f1b34..81b32545ed3 100644 --- a/consensus/types/src/state/beacon_state.rs +++ b/consensus/types/src/state/beacon_state.rs @@ -1572,13 +1572,19 @@ impl BeaconState { &self, ) -> Result, BeaconStateError> { match self { + BeaconState::Base(_) + | BeaconState::Altair(_) + | BeaconState::Bellatrix(_) + | BeaconState::Capella(_) + | BeaconState::Deneb(_) + | BeaconState::Electra(_) + | BeaconState::Fulu(_) => Err(BeaconStateError::IncorrectStateVariant), BeaconState::Gloas(state) => Ok(ExecutionPayloadBidRef::Gloas( &state.latest_execution_payload_bid, )), BeaconState::Heze(state) => Ok(ExecutionPayloadBidRef::Heze( &state.latest_execution_payload_bid, )), - _ => Err(BeaconStateError::IncorrectStateVariant), } } @@ -1586,13 +1592,19 @@ impl BeaconState { &mut self, ) -> Result, BeaconStateError> { match self { + BeaconState::Base(_) + | BeaconState::Altair(_) + | BeaconState::Bellatrix(_) + | BeaconState::Capella(_) + | BeaconState::Deneb(_) + | BeaconState::Electra(_) + | BeaconState::Fulu(_) => Err(BeaconStateError::IncorrectStateVariant), BeaconState::Gloas(state) => Ok(ExecutionPayloadBidRefMut::Gloas( &mut state.latest_execution_payload_bid, )), BeaconState::Heze(state) => Ok(ExecutionPayloadBidRefMut::Heze( &mut state.latest_execution_payload_bid, )), - _ => Err(BeaconStateError::IncorrectStateVariant), } } diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index f20edd0c43a..27133b04989 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -64,7 +64,7 @@ pub struct VoluntaryExitChurn { } /// Newtype for testing execution payload bids. -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone)] pub struct ExecutionPayloadBidBlock { signed_bid: SignedExecutionPayloadBid, } From dc3547b09e9a4a3c5372b1d6882ff4c5d679560c Mon Sep 17 00:00:00 2001 From: conache Date: Wed, 7 Oct 2026 12:39:56 +0300 Subject: [PATCH 34/44] Add bid variant check for the direct path bids --- .../src/payload_bid_verification/direct_verified_bid.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/beacon_node/beacon_chain/src/payload_bid_verification/direct_verified_bid.rs b/beacon_node/beacon_chain/src/payload_bid_verification/direct_verified_bid.rs index 4459be30feb..691edb71506 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/direct_verified_bid.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/direct_verified_bid.rs @@ -47,6 +47,11 @@ pub fn verify_direct_bid( ) -> Result<(), PayloadBidError> { let bid = signed_bid.message(); + // Ensure the bid is the correct structure for the fork at `bid.slot()`. + signed_bid + .fork_name(spec) + .map_err(PayloadBidError::InconsistentFork)?; + // The bid must be for exactly the slot being produced. if bid.slot() != proposal_slot { return Err(PayloadBidError::InvalidBidSlot { @@ -124,7 +129,7 @@ mod tests { use super::*; use bls::Signature; use types::{ - Address, ExecutionPayloadBidGloas, MinimalEthSpec, ProposerPreferences, + Address, ExecutionPayloadBidGloas, ForkName, MinimalEthSpec, ProposerPreferences, SignedExecutionPayloadBidGloas, }; @@ -134,7 +139,7 @@ mod tests { const EXECUTED_ANCESTOR_GAS_LIMIT: u64 = 30_000_000; fn state_and_spec() -> (BeaconState, ChainSpec) { - let spec = E::default_spec(); + let spec = ForkName::Gloas.make_genesis_spec(E::default_spec()); let state = BeaconState::new(0, <_>::default(), &spec); (state, spec) } From 59db449f4c7e2f6f84ff51715225b6069e3a0db4 Mon Sep 17 00:00:00 2001 From: conache Date: Wed, 7 Oct 2026 13:06:17 +0300 Subject: [PATCH 35/44] Skip bids whose variant doesn't match the fork at their slot on block production --- .../beacon_chain/src/block_production/gloas.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/beacon_node/beacon_chain/src/block_production/gloas.rs b/beacon_node/beacon_chain/src/block_production/gloas.rs index f0911335188..1a74e735893 100644 --- a/beacon_node/beacon_chain/src/block_production/gloas.rs +++ b/beacon_node/beacon_chain/src/block_production/gloas.rs @@ -1179,6 +1179,19 @@ impl BeaconChain { } } + // Filter out any bid whose variant doesn't match the fork at its slot, making sure + // that we don't fail block production + externals.retain(|candidate| { + if let Err(error) = candidate.signed_bid.fork_name(&self.spec) { + warn!( + ?error, + "Skipping bid with a variant inconsistent with its fork" + ); + return false; + } + true + }); + // The parent's exit requests apply to the state before this block's bid is processed, so a // bid from a builder the parent payload exits fails `process_execution_payload_bid`. externals.retain(|candidate| { From abb66304d5d436c0892bda031005130287c21b56 Mon Sep 17 00:00:00 2001 From: conache Date: Wed, 7 Oct 2026 13:19:27 +0300 Subject: [PATCH 36/44] Check the builder bid variant matches the requested fork --- .../builder_client/src/builder_http_client.rs | 31 +++++++++++++++++++ beacon_node/builder_client/src/error.rs | 10 +++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/beacon_node/builder_client/src/builder_http_client.rs b/beacon_node/builder_client/src/builder_http_client.rs index 82008ccd5e7..66322954333 100644 --- a/beacon_node/builder_client/src/builder_http_client.rs +++ b/beacon_node/builder_client/src/builder_http_client.rs @@ -192,6 +192,13 @@ impl BuilderHttpClient { ContentType::Json => { let versioned: ForkVersionedResponse> = serde_json::from_slice(&response_bytes).map_err(Error::InvalidJson)?; + let received = versioned.data.message().fork_name_unchecked(); + if received != fork_name { + return Err(Error::InvalidFork { + expected: fork_name, + received, + }); + } Ok(Some(versioned.data)) } } @@ -523,4 +530,28 @@ mod tests { .create(); assert!(request_bid(&server).await.is_none()); } + + #[tokio::test] + async fn get_execution_payload_bid_rejects_invalid_fork() { + let mut server = Server::new_async().await; + mock_bid(&mut server, ContentType::Json); + let result = client_for() + .get_execution_payload_bid::( + &builder_url(&server), + Slot::new(1), + ExecutionBlockHash::repeat_byte(1), + Hash256::repeat_byte(2), + &PublicKeyBytes::empty(), + &signed_request_auth(), + ForkName::Heze, + ) + .await; + assert!(matches!( + result, + Err(Error::InvalidFork { + expected: ForkName::Heze, + received: ForkName::Gloas, + }) + )); + } } diff --git a/beacon_node/builder_client/src/error.rs b/beacon_node/builder_client/src/error.rs index 1fe4af0fa43..00e89836c62 100644 --- a/beacon_node/builder_client/src/error.rs +++ b/beacon_node/builder_client/src/error.rs @@ -7,7 +7,7 @@ //! 406/415 negotiation) into an opaque status code. The pre-Gloas builder client still uses //! `eth2::Error`. -use eth2::types::{BuilderUrlError, ErrorMessage}; +use eth2::types::{BuilderUrlError, ErrorMessage, ForkName}; use pretty_reqwest_error::PrettyReqwestError; use reqwest::{Response, StatusCode}; use sensitive_url::SensitiveUrl; @@ -32,6 +32,11 @@ pub enum Error { InvalidSsz(ssz::DecodeError), /// Request headers could not be constructed. InvalidHeaders(String), + /// The builder returned a bid for a different fork than the one requested. + InvalidFork { + expected: ForkName, + received: ForkName, + }, } impl From for Error { @@ -59,6 +64,9 @@ impl fmt::Display for Error { Error::InvalidJson(e) => write!(f, "invalid JSON response: {e}"), Error::InvalidSsz(e) => write!(f, "invalid SSZ response: {e:?}"), Error::InvalidHeaders(e) => write!(f, "invalid response headers: {e}"), + Error::InvalidFork { expected, received } => { + write!(f, "expected a {expected} bid, received a {received} bid") + } } } } From 7f3ea1fa0cf641b703643f0b9ec01bba2cebc98f Mon Sep 17 00:00:00 2001 From: conache Date: Wed, 7 Oct 2026 13:36:35 +0300 Subject: [PATCH 37/44] Move gossip bounds in eth spec and add heze variant --- .../lighthouse_network/src/types/pubsub.rs | 17 +++++------ consensus/types/src/core/chain_spec.rs | 14 --------- consensus/types/src/core/eth_spec.rs | 30 +++++++++++++++++++ consensus/types/src/core/preset.rs | 11 +++---- 4 files changed, 44 insertions(+), 28 deletions(-) diff --git a/beacon_node/lighthouse_network/src/types/pubsub.rs b/beacon_node/lighthouse_network/src/types/pubsub.rs index 192d9037bfd..7293b0c6c67 100644 --- a/beacon_node/lighthouse_network/src/types/pubsub.rs +++ b/beacon_node/lighthouse_network/src/types/pubsub.rs @@ -396,9 +396,11 @@ impl PubsubMessage { gossip_topic.fork_digest ) })?; - let max_size = fork_context - .spec - .max_signed_execution_payload_bid_size_for_fork(fork_name); + let max_size = if fork_name.heze_enabled() { + E::max_signed_execution_payload_bid_size_heze() + } else { + E::max_signed_execution_payload_bid_size() + }; if data.len() > max_size { return Err(format!( "SignedExecutionPayloadBid size {} exceeds MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE {}", @@ -886,9 +888,7 @@ mod tests { #[test] fn gloas_execution_payload_bid_size_bound() { - let max = gloas_fork_context() - .spec - .max_signed_execution_payload_bid_size_for_fork(ForkName::Gloas); + let max = E::max_signed_execution_payload_bid_size(); let err = decode_oversized( &gloas_fork_context(), GossipKind::ExecutionPayloadBid, @@ -910,9 +910,8 @@ mod tests { #[test] fn heze_execution_payload_bid_size_bound() { let fork_context = heze_fork_context(); - let spec = &fork_context.spec; - let gloas_max = spec.max_signed_execution_payload_bid_size_for_fork(ForkName::Gloas); - let heze_max = spec.max_signed_execution_payload_bid_size_for_fork(ForkName::Heze); + let gloas_max = E::max_signed_execution_payload_bid_size(); + let heze_max = E::max_signed_execution_payload_bid_size_heze(); assert!(heze_max > gloas_max); let err = decode_oversized(&fork_context, GossipKind::ExecutionPayloadBid, heze_max + 1) diff --git a/consensus/types/src/core/chain_spec.rs b/consensus/types/src/core/chain_spec.rs index 9576f663af4..ce071989d80 100644 --- a/consensus/types/src/core/chain_spec.rs +++ b/consensus/types/src/core/chain_spec.rs @@ -333,7 +333,6 @@ pub struct ChainSpec { * Networking Gloas */ pub max_request_payloads: u64, - pub max_signed_execution_payload_bid_size: u64, /* * Networking Heze @@ -341,7 +340,6 @@ pub struct ChainSpec { pub max_transactions_bytes_per_inclusion_list: u64, pub max_request_inclusion_list: u64, pub min_slots_for_inclusion_lists_requests: u64, - pub max_signed_execution_payload_bid_size_heze: u64, /* * Networking Derived @@ -487,14 +485,6 @@ impl ChainSpec { } } - pub fn max_signed_execution_payload_bid_size_for_fork(&self, fork_name: ForkName) -> usize { - if fork_name.heze_enabled() { - self.max_signed_execution_payload_bid_size_heze as usize - } else { - self.max_signed_execution_payload_bid_size as usize - } - } - pub fn max_effective_balance_for_fork(&self, fork_name: ForkName) -> u64 { if fork_name.electra_enabled() { self.max_effective_balance_electra @@ -1463,7 +1453,6 @@ impl ChainSpec { .expect("calculation does not overflow"), gas_limit_schedule: GasLimitSchedule::default(), max_request_payloads: 128, - max_signed_execution_payload_bid_size: 196932, /* * Heze hard fork params @@ -1472,7 +1461,6 @@ impl ChainSpec { heze_fork_epoch: None, max_transactions_bytes_per_inclusion_list: 8192, max_request_inclusion_list: 16, - max_signed_execution_payload_bid_size_heze: 196934, min_slots_for_inclusion_lists_requests: 1, /* @@ -1923,7 +1911,6 @@ impl ChainSpec { .expect("calculation does not overflow"), gas_limit_schedule: GasLimitSchedule::default(), max_request_payloads: 128, - max_signed_execution_payload_bid_size: 196932, /* * Heze hard fork params @@ -1932,7 +1919,6 @@ impl ChainSpec { heze_fork_epoch: None, max_transactions_bytes_per_inclusion_list: 8192, max_request_inclusion_list: 16, - max_signed_execution_payload_bid_size_heze: 196934, min_slots_for_inclusion_lists_requests: 1, /* diff --git a/consensus/types/src/core/eth_spec.rs b/consensus/types/src/core/eth_spec.rs index f9f4807c477..9e0dc91ddab 100644 --- a/consensus/types/src/core/eth_spec.rs +++ b/consensus/types/src/core/eth_spec.rs @@ -457,6 +457,12 @@ pub trait EthSpec: 'static + Default + Sync + Send + Clone + Debug + PartialEq + /// Returns the `MAX_ATTESTER_SLASHING_SIZE` constant for this specification. fn max_attester_slashing_size() -> usize; + /// Returns the `MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE` constant for this specification. + fn max_signed_execution_payload_bid_size() -> usize; + + /// Returns the `MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE_HEZE` constant for this specification. + fn max_signed_execution_payload_bid_size_heze() -> usize; + /// Returns the `MAX_SIGNED_INCLUSION_LIST_SIZE` constant for this specification. fn max_signed_inclusion_list_size() -> usize; @@ -568,6 +574,14 @@ impl EthSpec for MainnetEthSpec { 2097616 } + fn max_signed_execution_payload_bid_size() -> usize { + 196932 + } + + fn max_signed_execution_payload_bid_size_heze() -> usize { + 196934 + } + fn max_signed_inclusion_list_size() -> usize { 41112 } @@ -660,6 +674,14 @@ impl EthSpec for MinimalEthSpec { 131536 } + fn max_signed_execution_payload_bid_size() -> usize { + 196932 + } + + fn max_signed_execution_payload_bid_size_heze() -> usize { + 196934 + } + fn max_signed_inclusion_list_size() -> usize { 41112 } @@ -749,6 +771,14 @@ impl EthSpec for GnosisEthSpec { 2097616 } + fn max_signed_execution_payload_bid_size() -> usize { + 196932 + } + + fn max_signed_execution_payload_bid_size_heze() -> usize { + 196934 + } + fn max_signed_inclusion_list_size() -> usize { 41112 } diff --git a/consensus/types/src/core/preset.rs b/consensus/types/src/core/preset.rs index 13e2e44a122..0afb1664c5b 100644 --- a/consensus/types/src/core/preset.rs +++ b/consensus/types/src/core/preset.rs @@ -351,7 +351,7 @@ pub struct GloasPreset { } impl GloasPreset { - pub fn from_chain_spec(spec: &ChainSpec) -> Self { + pub fn from_chain_spec(_spec: &ChainSpec) -> Self { Self { ptc_size: E::ptc_size() as u64, max_payload_attestations: E::max_payload_attestations() as u64, @@ -362,7 +362,8 @@ impl GloasPreset { as u64, max_signed_aggregate_and_proof_size: E::max_signed_aggregate_and_proof_size() as u64, max_attester_slashing_size: E::max_attester_slashing_size() as u64, - max_signed_execution_payload_bid_size: spec.max_signed_execution_payload_bid_size, + max_signed_execution_payload_bid_size: E::max_signed_execution_payload_bid_size() + as u64, } } } @@ -379,11 +380,11 @@ pub struct HezePreset { } impl HezePreset { - pub fn from_chain_spec(spec: &ChainSpec) -> Self { + pub fn from_chain_spec(_spec: &ChainSpec) -> Self { Self { inclusion_list_committee_size: E::inclusion_list_committee_size() as u64, - max_signed_execution_payload_bid_size_heze: spec - .max_signed_execution_payload_bid_size_heze, + max_signed_execution_payload_bid_size_heze: + E::max_signed_execution_payload_bid_size_heze() as u64, max_signed_inclusion_list_size: E::max_signed_inclusion_list_size() as u64, } } From b8da456b9d6cdfe644bdf1b73c9ca3bc5fc38d64 Mon Sep 17 00:00:00 2001 From: conache Date: Wed, 7 Oct 2026 13:52:52 +0300 Subject: [PATCH 38/44] Test bid variant mismatches on the direct path --- .../direct_verified_bid.rs | 55 ++++++++++++++++++- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/beacon_node/beacon_chain/src/payload_bid_verification/direct_verified_bid.rs b/beacon_node/beacon_chain/src/payload_bid_verification/direct_verified_bid.rs index 691edb71506..a063b9c3b0e 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/direct_verified_bid.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/direct_verified_bid.rs @@ -129,8 +129,9 @@ mod tests { use super::*; use bls::Signature; use types::{ - Address, ExecutionPayloadBidGloas, ForkName, MinimalEthSpec, ProposerPreferences, - SignedExecutionPayloadBidGloas, + Address, Epoch, ExecutionPayloadBidGloas, ExecutionPayloadBidHeze, ForkName, + MinimalEthSpec, ProposerPreferences, SignedExecutionPayloadBidGloas, + SignedExecutionPayloadBidHeze, }; type E = MinimalEthSpec; @@ -388,4 +389,54 @@ mod tests { "expected to fail after the gas check, got {result:?}" ); } + #[test] + fn rejects_gloas_bid_at_heze_slot() { + let (state, mut spec) = state_and_spec(); + let heze_fork_epoch = Epoch::new(1); + spec.heze_fork_epoch = Some(heze_fork_epoch); + let heze_slot = heze_fork_epoch.start_slot(E::slots_per_epoch()); + let bid = signed_bid( + heze_slot, + ExecutionBlockHash::zero(), + Hash256::ZERO, + Hash256::ZERO, + ); + let result = verify_direct_bid( + &bid, + heze_slot, + ExecutionBlockHash::zero(), + Hash256::ZERO, + EXECUTED_ANCESTOR_GAS_LIMIT, + &BuilderPubkeys::default(), + &preferences(), + &state, + &spec, + ); + assert!(matches!(result, Err(PayloadBidError::InconsistentFork(_)))); + } + + #[test] + fn rejects_heze_bid_at_gloas_slot() { + let (state, mut spec) = state_and_spec(); + spec.heze_fork_epoch = Some(Epoch::new(1)); + let bid = SignedExecutionPayloadBid::Heze(SignedExecutionPayloadBidHeze { + message: ExecutionPayloadBidHeze { + slot: Slot::new(1), + ..ExecutionPayloadBidHeze::default() + }, + signature: Signature::empty(), + }); + let result = verify_direct_bid( + &bid, + Slot::new(1), + ExecutionBlockHash::zero(), + Hash256::ZERO, + EXECUTED_ANCESTOR_GAS_LIMIT, + &BuilderPubkeys::default(), + &preferences(), + &state, + &spec, + ); + assert!(matches!(result, Err(PayloadBidError::InconsistentFork(_)))); + } } From 805f75486248b2a194a68ea3873bc8995e1f4a7d Mon Sep 17 00:00:00 2001 From: conache Date: Wed, 7 Oct 2026 14:22:32 +0300 Subject: [PATCH 39/44] Add test for Gloas bid at the Heze fork boundary on the gossip path --- .../src/payload_bid_verification/tests.rs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) 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 19bbbd21712..a11ba912b15 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs @@ -554,6 +554,30 @@ fn inconsistent_fork() { assert!(matches!(result, Err(PayloadBidError::InconsistentFork(_)))); } +#[test] +fn inconsistent_fork_at_heze_boundary() { + // A Gloas bid for the first Heze slot, + // possible from a peer still on the Gloas topic. + if fork_name_from_env() != Some(ForkName::Gloas) { + return; + } + let mut ctx = TestContext::new(); + let heze_fork_epoch = Epoch::new(1); + ctx.spec.heze_fork_epoch = Some(heze_fork_epoch); + let gossip = ctx.gossip_ctx(); + + let bid = ctx.make_signed_bid( + heze_fork_epoch.start_slot(E::slots_per_epoch()), + 0, + Address::ZERO, + 30_000_000, + 100, + ctx.genesis_block_root, + ); + let result = GossipVerifiedPayloadBid::new(bid, &gossip); + assert!(matches!(result, Err(PayloadBidError::InconsistentFork(_)))); +} + #[test] fn fee_recipient_mismatch() { if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { From 4969cc1300655bb0ab199d4f1c5b59f49e4c8fa8 Mon Sep 17 00:00:00 2001 From: conache Date: Wed, 7 Oct 2026 14:44:17 +0300 Subject: [PATCH 40/44] Re-add deny_unknown_fields on the bid variants --- consensus/types/src/execution/execution_payload_bid.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/consensus/types/src/execution/execution_payload_bid.rs b/consensus/types/src/execution/execution_payload_bid.rs index 863a7d43c24..8072ce565aa 100644 --- a/consensus/types/src/execution/execution_payload_bid.rs +++ b/consensus/types/src/execution/execution_payload_bid.rs @@ -28,7 +28,7 @@ use tree_hash_derive::TreeHash; ), context_deserialize(ForkName), educe(PartialEq, Hash(bound(E: EthSpec))), - serde(bound = "E: EthSpec"), + serde(bound = "E: EthSpec", deny_unknown_fields), cfg_attr( feature = "arbitrary", derive(arbitrary::Arbitrary), From 5769b3e14efd10ae8196c473af2f0591c19c12b4 Mon Sep 17 00:00:00 2001 From: conache Date: Wed, 7 Oct 2026 15:44:36 +0300 Subject: [PATCH 41/44] Box block production and import futures in the test harness --- beacon_node/beacon_chain/src/test_utils.rs | 86 +++++++++++----------- 1 file changed, 41 insertions(+), 45 deletions(-) diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 5db0a3f6f31..33651b85fcc 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -2997,30 +2997,28 @@ where .is_ok_and(|c| !c.is_empty()); let is_available = !has_blob_commitments || blob_items.is_some(); let block_hash: SignedBeaconBlockHash = if !is_available { - self.chain - .process_block( - block_root, - LookupBlock::new(block), - NotifyExecutionLayer::Yes, - BlockImportSource::Lookup, - || Ok(()), - ) - .await? - .try_into() - .expect("block blobs are available") + Box::pin(self.chain.process_block( + block_root, + LookupBlock::new(block), + NotifyExecutionLayer::Yes, + BlockImportSource::Lookup, + || Ok(()), + )) + .await? + .try_into() + .expect("block blobs are available") } else { let range_sync_block = self.build_range_sync_block_from_blobs(block, blob_items)?; - self.chain - .process_block( - block_root, - range_sync_block, - NotifyExecutionLayer::Yes, - BlockImportSource::RangeSync, - || Ok(()), - ) - .await? - .try_into() - .expect("block blobs are available") + Box::pin(self.chain.process_block( + block_root, + range_sync_block, + NotifyExecutionLayer::Yes, + BlockImportSource::RangeSync, + || Ok(()), + )) + .await? + .try_into() + .expect("block blobs are available") }; self.chain.recompute_head_at_current_slot().await; @@ -3044,29 +3042,27 @@ where let is_available = !has_blob_commitments || blob_items.is_some(); let block_hash: SignedBeaconBlockHash = if is_available { let range_sync_block = self.build_range_sync_block_from_blobs(block, blob_items)?; - self.chain - .process_block( - block_root, - range_sync_block, - NotifyExecutionLayer::Yes, - BlockImportSource::RangeSync, - || Ok(()), - ) - .await? - .try_into() - .expect("block blobs are available") + Box::pin(self.chain.process_block( + block_root, + range_sync_block, + NotifyExecutionLayer::Yes, + BlockImportSource::RangeSync, + || Ok(()), + )) + .await? + .try_into() + .expect("block blobs are available") } else { - self.chain - .process_block( - block_root, - LookupBlock::new(block), - NotifyExecutionLayer::Yes, - BlockImportSource::Lookup, - || Ok(()), - ) - .await? - .try_into() - .expect("block blobs are available") + Box::pin(self.chain.process_block( + block_root, + LookupBlock::new(block), + NotifyExecutionLayer::Yes, + BlockImportSource::Lookup, + || Ok(()), + )) + .await? + .try_into() + .expect("block blobs are available") }; self.chain.recompute_head_at_current_slot().await; @@ -3421,7 +3417,7 @@ where > { self.set_current_slot(slot); let (block_contents, opt_envelope, new_state) = - self.make_block_with_envelope(state, slot).await; + Box::pin(self.make_block_with_envelope(state, slot)).await; let block_hash = self .process_block( From 3022a0f7dca7441e56ac97645d89165a4741cec3 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Wed, 7 Oct 2026 08:45:00 -0700 Subject: [PATCH 42/44] Fix bid commitments access after merging unstable --- .../gossip_verified_execution_proof.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beacon_node/beacon_chain/src/execution_proof_verification/gossip_verified_execution_proof.rs b/beacon_node/beacon_chain/src/execution_proof_verification/gossip_verified_execution_proof.rs index 158ec24f6a9..256c979ee3b 100644 --- a/beacon_node/beacon_chain/src/execution_proof_verification/gossip_verified_execution_proof.rs +++ b/beacon_node/beacon_chain/src/execution_proof_verification/gossip_verified_execution_proof.rs @@ -241,8 +241,8 @@ fn get_execution_proof( })?; let versioned_hashes = VersionedHashes::::new( - bid.message - .blob_kzg_commitments + bid.message() + .blob_kzg_commitments() .iter() .map(kzg_commitment_to_versioned_hash) .collect(), From 2816df44fb12e89beaeca58f167ce6886a0f8a6c Mon Sep 17 00:00:00 2001 From: conache Date: Thu, 8 Oct 2026 12:53:07 +0300 Subject: [PATCH 43/44] Fix tests --- .../src/payload_bid_verification/tests.rs | 97 ++++++++----------- 1 file changed, 39 insertions(+), 58 deletions(-) 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 6c08f88cde4..5520939afdc 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs @@ -447,8 +447,6 @@ fn make_signed_preferences( }) } -/// The inclusion list bits claiming `validator_indices`: every committee position held by one of -/// them is set, the way the store derives its own view. fn inclusion_list_bits_for_validators( inclusion_list_committee: &InclusionListCommittee, validator_indices: &[u64], @@ -687,14 +685,15 @@ fn inconsistent_fork_at_heze_boundary() { ctx.spec.heze_fork_epoch = Some(heze_fork_epoch); let gossip = ctx.gossip_ctx(); - let bid = ctx.make_signed_bid( - heze_fork_epoch.start_slot(E::slots_per_epoch()), - 0, - Address::ZERO, - 30_000_000, - 100, - ctx.genesis_block_root, - ); + let bid = ctx.sign_bid(ExecutionPayloadBid::Gloas(ExecutionPayloadBidGloas { + slot: heze_fork_epoch.start_slot(E::slots_per_epoch()), + gas_limit: 30_000_000, + value: 100, + parent_block_root: ctx.genesis_block_root, + parent_block_hash: ctx.execution_parent_hash(), + prev_randao: ctx.expected_prev_randao(), + ..ExecutionPayloadBidGloas::default() + })); let result = GossipVerifiedPayloadBid::new(bid, &gossip); assert!(matches!(result, Err(PayloadBidError::InconsistentFork(_)))); } @@ -785,22 +784,16 @@ fn block_hash_equals_parent_block_hash() { let slot = Slot::new(1); seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); - let parent_block_hash = ctx.execution_parent_hash(); - let bid = Arc::new(SignedExecutionPayloadBid::Gloas( - SignedExecutionPayloadBidGloas { - message: ExecutionPayloadBidGloas { - slot, - gas_limit: 30_000_000, - parent_block_root: ctx.genesis_block_root, - parent_block_hash, - block_hash: parent_block_hash, - prev_randao: ctx.expected_prev_randao(), - ..ExecutionPayloadBidGloas::default() - }, - signature: Signature::empty(), - }, - )); - let result = GossipVerifiedPayloadBid::new(bid, &gossip); + let mut bid = ctx.make_bid( + slot, + 0, + Address::ZERO, + 30_000_000, + 0, + ctx.genesis_block_root, + ); + *bid.block_hash_mut() = ctx.execution_parent_hash(); + let result = GossipVerifiedPayloadBid::new(ctx.sign_bid(bid), &gossip); assert!(matches!( result, Err(PayloadBidError::BlockHashEqualsParentBlockHash { .. }) @@ -974,21 +967,16 @@ fn execution_payment_nonzero() { let slot = Slot::new(1); seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); - let bid = Arc::new(SignedExecutionPayloadBid::Gloas( - SignedExecutionPayloadBidGloas { - message: ExecutionPayloadBidGloas { - slot, - gas_limit: 30_000_000, - execution_payment: 42, - parent_block_root: ctx.genesis_block_root, - parent_block_hash: ctx.execution_parent_hash(), - prev_randao: ctx.expected_prev_randao(), - ..ExecutionPayloadBidGloas::default() - }, - signature: Signature::empty(), - }, - )); - let result = GossipVerifiedPayloadBid::new(bid, &gossip); + let mut bid = ctx.make_bid( + slot, + 0, + Address::ZERO, + 30_000_000, + 0, + ctx.genesis_block_root, + ); + *bid.execution_payment_mut() = 42; + let result = GossipVerifiedPayloadBid::new(ctx.sign_bid(bid), &gossip); assert!(matches!( result, Err(PayloadBidError::ExecutionPaymentNonZero { .. }) @@ -1263,23 +1251,16 @@ fn invalid_blob_kzg_commitments() { .map(|_| KzgCommitment::empty_for_testing()) .collect(); - let bid = Arc::new(SignedExecutionPayloadBid::Gloas( - SignedExecutionPayloadBidGloas { - message: ExecutionPayloadBidGloas { - slot, - builder_index: 0, - fee_recipient: Address::ZERO, - gas_limit: 30_000_000, - value: 0, - parent_block_root: ctx.genesis_block_root, - parent_block_hash: ctx.execution_parent_hash(), - prev_randao: ctx.expected_prev_randao(), - blob_kzg_commitments: ProgressiveVariableList::new(commitments).unwrap(), - ..ExecutionPayloadBidGloas::default() - }, - signature: Signature::empty(), - }, - )); + let mut bid = ctx.make_bid( + slot, + 0, + Address::ZERO, + 30_000_000, + 0, + ctx.genesis_block_root, + ); + *bid.blob_kzg_commitments_mut() = ProgressiveVariableList::new(commitments).unwrap(); + let bid = ctx.sign_bid(bid); let result = GossipVerifiedPayloadBid::new(bid, &gossip); assert!(matches!( result, From 7e274ae370c2e275848956c886fb716f006c7c4b Mon Sep 17 00:00:00 2001 From: conache Date: Thu, 8 Oct 2026 13:12:05 +0300 Subject: [PATCH 44/44] Add Heze fork boundary test --- .../beacon_chain/src/inclusion_list_store.rs | 8 +-- .../gossip_verified_bid.rs | 6 +-- .../src/payload_bid_verification/mod.rs | 3 +- .../src/payload_bid_verification/tests.rs | 54 +++++++++++++++---- 4 files changed, 52 insertions(+), 19 deletions(-) diff --git a/beacon_node/beacon_chain/src/inclusion_list_store.rs b/beacon_node/beacon_chain/src/inclusion_list_store.rs index e331437eef9..c0fdd3eea24 100644 --- a/beacon_node/beacon_chain/src/inclusion_list_store.rs +++ b/beacon_node/beacon_chain/src/inclusion_list_store.rs @@ -6,7 +6,7 @@ //! keyed by `(slot, dependent_root)`, which pins an inclusion list to the committee it was produced //! against. -use ssz_types::{BitVector, ProgressiveVariableList}; +use ssz_types::ProgressiveVariableList; use std::collections::{HashMap, HashSet}; use std::marker::PhantomData; use tree_hash::TreeHash; @@ -227,10 +227,10 @@ impl InclusionListStore { dependent_root: DependentRoot, il_committee: &InclusionListCommittee, only_timely: bool, - ) -> Result, Error> { + ) -> Result, Error> { let submitted = self.submitted_validators(slot, dependent_root, only_timely); - let mut bits = BitVector::new(); + let mut bits = InclusionListBits::::new(); for (i, validator) in il_committee.iter().enumerate() { if submitted.contains(validator) { bits.set(i, true)?; @@ -246,7 +246,7 @@ impl InclusionListStore { slot: Slot, dependent_root: DependentRoot, il_committee: &InclusionListCommittee, - bits: &BitVector, + bits: &InclusionListBits, only_timely: bool, ) -> Result { let local = 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 6b89c62e29f..14384d18ada 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 @@ -88,9 +88,9 @@ fn verify_bid_blobs( /// Reject a bid whose `inclusion_list_bits` do not cover `local_inclusion_list_bits`, the node's /// own view of the inclusion lists for the slot before the bid's. Pre-Heze bids carry no bits and pass. /// -/// Shared by both intakes, each resolves the local bits under its own timeliness rule: -/// timely inclusion lists only on gossip, and all (timely and untimely) inclusion lists on the -/// proposer's block production path. +/// Callers resolve `local_inclusion_list_bits` under their own timeliness rule. +/// On gossip only timely inclusion lists count, while for the proposer's block production path +/// both timely and untimely inclusion lists count. pub(crate) fn verify_bid_inclusion_list_bits( bid: ExecutionPayloadBidRef, local_inclusion_list_bits: &InclusionListBits, 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 7c9a5e8a63e..26fa924b6a4 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs @@ -92,7 +92,8 @@ pub enum PayloadBidError { }, /// The bids prev randao value is invalid InvalidPrevRandao { slot: Slot }, - /// The bid's `inclusion_list_bits` do not cover every inclusion list the node holds for `slot - 1` + /// The bid's `inclusion_list_bits` do not cover every timely inclusion list + /// the node holds for `slot - 1` InclusionListBitsNotInclusive { slot: Slot }, /// Some Beacon State error BeaconStateError(BeaconStateError), 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 5520939afdc..7972d38ca46 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs @@ -7,7 +7,7 @@ use fork_choice::ForkChoice; use genesis::{generate_deterministic_keypairs, interop_genesis_state}; use kzg::KzgCommitment; use slot_clock::{SlotClock, TestingSlotClock}; -use ssz::{BitVector, Encode}; +use ssz::Encode; use ssz_types::ProgressiveVariableList; use state_processing::genesis::genesis_block; use store::{HotColdDB, StoreConfig, StoreOp}; @@ -15,11 +15,11 @@ use types::{ Address, BuilderExitRequest, ChainSpec, Checkpoint, Domain, Epoch, EthSpec, ExecutionBlockHash, ExecutionPayloadBid, ExecutionPayloadBidGloas, ExecutionPayloadBidHeze, ExecutionPayloadBidRef, ExecutionPayloadEnvelope, ExecutionPayloadHeader, ExecutionPayloadHeaderFulu, ForkName, - Hash256, InclusionList, InclusionListCommittee, MinimalEthSpec, ProgressiveTransactions, - ProposerPreferences, RelativeEpoch, SignedBeaconBlock, SignedExecutionPayloadBid, - SignedExecutionPayloadBidGloas, SignedExecutionPayloadBidHeze, SignedExecutionPayloadEnvelope, - SignedInclusionList, SignedProposerPreferences, SignedRoot, Slot, - consts::gloas::PAYLOAD_BUILDER_VERSION, + Hash256, InclusionList, InclusionListBits, InclusionListCommittee, MinimalEthSpec, + ProgressiveTransactions, ProposerPreferences, RelativeEpoch, SignedBeaconBlock, + SignedExecutionPayloadBid, SignedExecutionPayloadBidGloas, SignedExecutionPayloadBidHeze, + SignedExecutionPayloadEnvelope, SignedInclusionList, SignedProposerPreferences, SignedRoot, + Slot, consts::gloas::PAYLOAD_BUILDER_VERSION, }; use crate::inclusion_list_store::InclusionListStore; @@ -216,10 +216,11 @@ impl TestContext { fn sign_bid(&self, bid: ExecutionPayloadBid) -> Arc> { let head = self.canonical_head.cached_head(); let state = &head.snapshot.beacon_state; + let bid_epoch = bid.slot().epoch(E::slots_per_epoch()); let domain = self.spec.get_domain( - bid.slot().epoch(E::slots_per_epoch()), + bid_epoch, Domain::BeaconBuilder, - &state.fork(), + &self.spec.fork_at_epoch(bid_epoch), state.genesis_validators_root(), ); @@ -450,8 +451,8 @@ fn make_signed_preferences( fn inclusion_list_bits_for_validators( inclusion_list_committee: &InclusionListCommittee, validator_indices: &[u64], -) -> BitVector<::InclusionListCommitteeSize> { - let mut inclusion_list_bits = BitVector::<::InclusionListCommitteeSize>::new(); +) -> InclusionListBits { + let mut inclusion_list_bits = InclusionListBits::::new(); for (position, validator_index) in inclusion_list_committee.iter().enumerate() { if validator_indices.contains(validator_index) { @@ -1551,7 +1552,7 @@ fn bid_il_bits_not_inclusive() { assert!( matches!( result, - Err(PayloadBidError::InclusionListBitsNotInclusive{ slot }) if slot == bid_slot + Err(PayloadBidError::InclusionListBitsNotInclusive { slot }) if slot == bid_slot ), "unexpected result: {result:?}" ); @@ -1767,3 +1768,34 @@ fn bid_il_bits_claiming_more_than_stored_are_inclusive() { result.unwrap_err() ); } + +#[test] +fn bid_il_bits_check_at_heze_boundary() { + // A bid for the first Heze slot, received during the last Gloas slot + // The inclusion list slot is a Gloas slot, so the node holds no lists for it and the check passes + if fork_name_from_env() != Some(ForkName::Gloas) { + return; + } + let mut ctx = TestContext::new(); + let heze_fork_epoch = Epoch::new(1); + ctx.spec.heze_fork_epoch = Some(heze_fork_epoch); + let bid_slot = heze_fork_epoch.start_slot(E::slots_per_epoch()); + ctx.slot_clock.set_slot((bid_slot - 1).as_u64()); + seed_preferences(&ctx, bid_slot, Address::ZERO, 30_000_000); + let gossip = ctx.gossip_ctx(); + + let bid = ctx.sign_bid(ctx.make_bid( + bid_slot, + 0, + Address::ZERO, + 30_000_000, + 0, + ctx.genesis_block_root, + )); + let result = GossipVerifiedPayloadBid::new(bid, &gossip); + assert!( + result.is_ok(), + "expected Ok, got: {:?}", + result.unwrap_err() + ); +}