From be705c840937d2667178269d2238682d4982dcc4 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Sun, 21 Jun 2026 17:29:59 +0300 Subject: [PATCH] should_extend_payload and gossip bid verification changes --- .../gossip_verified_bid.rs | 8 ++++++++ .../src/payload_bid_verification/mod.rs | 2 ++ .../network_beacon_processor/gossip_methods.rs | 1 + consensus/fork_choice/src/fork_choice.rs | 3 ++- consensus/proto_array/src/error.rs | 10 +++++++++- consensus/proto_array/src/proto_array.rs | 16 +++++++++++++++- .../proto_array/src/proto_array_fork_choice.rs | 3 ++- .../per_block_processing/process_operations.rs | 4 +++- 8 files changed, 42 insertions(+), 5 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 1687760d25..2e69a21853 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 @@ -158,6 +158,14 @@ impl GossipVerifiedPayloadBid { }); } + // [REJECT] `bid.prev_randao` is the correct RANDAO mix -- i.e. validate that + // `bid.prev_randao == get_randao_mix(parent_state, get_current_epoch(parent_state))` + if signed_bid.message.prev_randao + != *head_state.get_randao_mix(current_slot.epoch(T::EthSpec::slots_per_epoch()))? + { + return Err(PayloadBidError::InvalidPrevRandao { slot: bid_slot }); + } + // TODO(gloas) reprocess bids whose parent_block_root becomes canonical after a reorg. let head_root = cached_head.head_block_root(); if !fork_choice.is_descendant(bid_parent_block_root, head_root) { diff --git a/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs b/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs index e23c537e18..b590d9200f 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs @@ -59,6 +59,8 @@ pub enum PayloadBidError { max_blobs_per_block: usize, blob_kzg_commitments_len: usize, }, + /// The bids prev randao value is invalid + InvalidPrevRandao { slot: Slot }, /// Some Beacon State error BeaconStateError(BeaconStateError), /// Internal error diff --git a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs index 346e621879..5a7d104b4d 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -4025,6 +4025,7 @@ impl NetworkBeaconProcessor { | PayloadBidError::ExecutionPaymentNonZero { .. } | PayloadBidError::InvalidBlobKzgCommitments { .. } | PayloadBidError::BidNotDescendantOfParent { .. }, + | PayloadBidError::InvalidPrevRandao { .. }, ) => { self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject); self.gossip_penalize_peer( diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index aca5ab7851..19a9bc3b64 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -1631,9 +1631,10 @@ where /// Returns whether the proposer should extend the execution payload chain of the given block. pub fn should_extend_payload(&self, block_root: &Hash256) -> Result> { + let current_slot = self.fc_store.get_current_slot(); let proposer_boost_root = self.fc_store.proposer_boost_root(); self.proto_array - .should_extend_payload::(block_root, proposer_boost_root) + .should_extend_payload::(block_root, current_slot, proposer_boost_root) .map_err(Error::ProtoArrayStringError) } diff --git a/consensus/proto_array/src/error.rs b/consensus/proto_array/src/error.rs index 383c1946c6..edf18b1bd1 100644 --- a/consensus/proto_array/src/error.rs +++ b/consensus/proto_array/src/error.rs @@ -1,6 +1,6 @@ use crate::PayloadStatus; use safe_arith::ArithError; -use types::{Epoch, ExecutionBlockHash, Hash256}; +use types::{Epoch, ExecutionBlockHash, Hash256, Slot}; #[derive(Clone, PartialEq, Debug)] pub enum Error { @@ -63,6 +63,14 @@ pub enum Error { block_root: Hash256, payload_status: PayloadStatus, }, + /// `should_extend_payload` was called for a block whose slot is not the previous slot. + /// + /// Spec equivalent: `assert store.blocks[root].slot + 1 == get_current_slot(store)`. + ShouldExtendPayloadInvalidSlot { + block_root: Hash256, + block_slot: Slot, + current_slot: Slot, + }, } impl From for Error { diff --git a/consensus/proto_array/src/proto_array.rs b/consensus/proto_array/src/proto_array.rs index 04113e2c0e..57d3acd55b 100644 --- a/consensus/proto_array/src/proto_array.rs +++ b/consensus/proto_array/src/proto_array.rs @@ -1564,7 +1564,12 @@ impl ProtoArray { Ok(fc_node.payload_status as u8) } else if fc_node.payload_status == PayloadStatus::Empty { Ok(1) - } else if self.should_extend_payload::(fc_node, proto_node, proposer_boost_root)? { + } else if self.should_extend_payload::( + fc_node, + proto_node, + current_slot, + proposer_boost_root, + )? { Ok(2) } else { Ok(0) @@ -1614,8 +1619,17 @@ impl ProtoArray { &self, fc_node: &IndexedForkChoiceNode, proto_node: &ProtoNode, + current_slot: Slot, proposer_boost_root: Hash256, ) -> Result { + if proto_node.slot().saturating_add(1u64) != current_slot { + return Err(Error::ShouldExtendPayloadInvalidSlot { + block_root: fc_node.root, + block_slot: proto_node.slot(), + current_slot, + }); + } + let Ok(node) = proto_node.as_v29() else { return Err(Error::InvalidNodeVariant { block_root: fc_node.root, diff --git a/consensus/proto_array/src/proto_array_fork_choice.rs b/consensus/proto_array/src/proto_array_fork_choice.rs index c6a7829c27..3470ae302f 100644 --- a/consensus/proto_array/src/proto_array_fork_choice.rs +++ b/consensus/proto_array/src/proto_array_fork_choice.rs @@ -999,6 +999,7 @@ impl ProtoArrayForkChoice { pub fn should_extend_payload( &self, block_root: &Hash256, + current_slot: Slot, proposer_boost_root: Hash256, ) -> Result { let block_index = self @@ -1017,7 +1018,7 @@ impl ProtoArrayForkChoice { payload_status: proto_node.get_parent_payload_status(), }; self.proto_array - .should_extend_payload::(&fc_node, proto_node, proposer_boost_root) + .should_extend_payload::(&fc_node, proto_node, current_slot, proposer_boost_root) .map_err(|e| format!("{e:?}")) } diff --git a/consensus/state_processing/src/per_block_processing/process_operations.rs b/consensus/state_processing/src/per_block_processing/process_operations.rs index 876e66d3af..0ab66a3224 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -397,7 +397,9 @@ pub fn process_proposer_slashings( // [New in Gloas:EIP7732] // Remove the BuilderPendingPayment corresponding to this proposal - // if it is still in the 2-epoch window. + // if it is still in the 2-epoch window. Only clear it when the slashed validator is + // the proposer associated with the payment; otherwise an unrelated same-slot + // equivocation could grief an honest proposer's payment. if state.fork_name_unchecked().gloas_enabled() { let slot = proposer_slashing.signed_header_1.message.slot; let proposal_epoch = slot.epoch(E::slots_per_epoch());