From 1d6ca779afd7b32e9a49f8baa87fd4e6a3326001 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 16 Sep 2026 05:20:39 +0000 Subject: [PATCH 1/2] Fix gossip bid RANDAO validation against the beacon parent --- .../gossip_verified_bid.rs | 10 +- .../src/payload_bid_verification/tests.rs | 125 ++++++++++++++++++ 2 files changed, 132 insertions(+), 3 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 851bf821de7..bc3febbdcd7 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 @@ -432,9 +432,13 @@ 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(E::slots_per_epoch()))? - { + let expected_randao = if bid_parent_block_root == cached_head.head_block_root() { + cached_head.head_random()? + } else { + // Head compatibility also permits a bid on the head's parent. + cached_head.parent_random()? + }; + if signed_bid.message.prev_randao != expected_randao { return Err(PayloadBidError::InvalidPrevRandao { slot: bid_slot }); } 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 4a7d4f2c217..0d3d61a0034 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs @@ -322,6 +322,63 @@ impl TestContext { fork_block_root } + fn advance_head(&mut self) { + let parent = self.canonical_head.cached_head().snapshot; + let fc_store = + BeaconForkChoiceStore::get_forkchoice_store(self.store.clone(), (*parent).clone()) + .unwrap(); + let mut fork_choice = ForkChoice::from_anchor( + fc_store, + parent.beacon_block_root, + &parent.beacon_block, + &parent.beacon_state, + None, + &self.spec, + ) + .unwrap(); + let mut state = parent.beacon_state.clone(); + let parent_randao = *state.get_randao_mix(state.current_epoch()).unwrap(); + *state.slot_mut() = Slot::new(1); + state + .set_block_root(Slot::new(0), parent.beacon_block_root) + .unwrap(); + state + .set_randao_mix(state.current_epoch(), Hash256::repeat_byte(0xab)) + .unwrap(); + state + .latest_execution_payload_bid_mut() + .unwrap() + .prev_randao = parent_randao; + let mut block = genesis_block(&state, &self.spec).unwrap(); + *block.slot_mut() = state.slot(); + *block.parent_root_mut() = parent.beacon_block_root; + *block.state_root_mut() = state.update_tree_hash_cache().unwrap(); + let signed_block = SignedBeaconBlock::from_block(block, Signature::empty()); + let root = signed_block.canonical_root(); + let mut proto_block = self.slot_1_proto_block(root, ExecutionBlockHash::repeat_byte(0xab)); + proto_block.execution_payload_parent_hash = Some(self.execution_parent_hash()); + fork_choice + .proto_array_mut() + .process_block::(proto_block, Slot::new(1), &self.spec, Duration::ZERO) + .unwrap(); + let (_, payload_status) = fork_choice.get_head(Slot::new(1), &self.spec).unwrap(); + self.canonical_head = CanonicalHead::new( + fork_choice, + Arc::new(BeaconSnapshot::new( + Arc::new(signed_block), + None, + root, + state, + )), + payload_status, + FastConfirmationMode::Disabled, + &self.store, + &self.spec, + ) + .unwrap(); + self.slot_clock.set_slot(1); + } + fn put_envelope_with_builder_exit( &self, block_root: Hash256, @@ -1111,6 +1168,74 @@ fn valid_bid_after_empty_genesis_uses_parent_payload_gas_limit() { ); } +#[test] +fn valid_bid_with_parent_in_previous_epoch() { + if !fork_name_from_env().is_some_and(|fork| fork.gloas_enabled()) { + return; + } + let epoch_start = E::slots_per_epoch(); + for (current_slot, bid_slot) in [ + (epoch_start - 1, epoch_start), + (epoch_start, epoch_start), + (epoch_start + 1, epoch_start + 1), + ] { + let ctx = TestContext::new(); + ctx.slot_clock.set_slot(current_slot); + let slot = Slot::new(bid_slot); + seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); + let parent_state = &ctx.canonical_head.cached_head().snapshot.beacon_state; + let bid = ctx.sign_bid(ExecutionPayloadBid { + slot, + builder_index: 0, + fee_recipient: Address::ZERO, + gas_limit: 30_000_000, + parent_block_root: ctx.genesis_block_root, + parent_block_hash: ctx.execution_parent_hash(), + prev_randao: *parent_state + .get_randao_mix(parent_state.current_epoch()) + .unwrap(), + ..ExecutionPayloadBid::default() + }); + let result = GossipVerifiedPayloadBid::new(bid, &ctx.gossip_ctx()); + assert!( + result.is_ok(), + "clock {current_slot}, bid {bid_slot}: {result:?}" + ); + } +} + +#[test] +fn bid_on_head_parent_uses_parent_randao() { + if !fork_name_from_env().is_some_and(|fork| fork.gloas_enabled()) { + return; + } + let mut ctx = TestContext::new(); + let parent_randao = ctx.canonical_head.cached_head().head_random().unwrap(); + ctx.advance_head(); + let head_randao = ctx.canonical_head.cached_head().head_random().unwrap(); + assert_ne!(head_randao, parent_randao); + let slot = Slot::new(2); + seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); + let mut bid = ExecutionPayloadBid { + slot, + builder_index: 0, + fee_recipient: Address::ZERO, + gas_limit: 30_000_000, + parent_block_root: ctx.genesis_block_root, + parent_block_hash: ctx.execution_parent_hash(), + prev_randao: head_randao, + ..ExecutionPayloadBid::default() + }; + let result = GossipVerifiedPayloadBid::new(ctx.sign_bid(bid.clone()), &ctx.gossip_ctx()); + assert!( + matches!(result, Err(PayloadBidError::InvalidPrevRandao { .. })), + "{result:?}" + ); + bid.prev_randao = parent_randao; + let result = GossipVerifiedPayloadBid::new(ctx.sign_bid(bid), &ctx.gossip_ctx()); + assert!(result.is_ok(), "{result:?}"); +} + #[test] fn two_builders_coexist_in_cache() { if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { From f75a10c0197fea530b1e1a1454c9ed127cd23435 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 16 Sep 2026 06:01:37 +0000 Subject: [PATCH 2/2] Defer head-parent RANDAO coverage to gossip spec tests --- .../gossip_verified_bid.rs | 2 + .../src/payload_bid_verification/tests.rs | 89 ------------------- 2 files changed, 2 insertions(+), 89 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 bc3febbdcd7..8ee17fdedca 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 @@ -432,6 +432,8 @@ 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))` + // TODO: Enable head-parent RANDAO gossip spec coverage: + // https://github.com/ethereum/consensus-specs/pull/5645 let expected_randao = if bid_parent_block_root == cached_head.head_block_root() { cached_head.head_random()? } else { 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 0d3d61a0034..329fe024cc1 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs @@ -322,63 +322,6 @@ impl TestContext { fork_block_root } - fn advance_head(&mut self) { - let parent = self.canonical_head.cached_head().snapshot; - let fc_store = - BeaconForkChoiceStore::get_forkchoice_store(self.store.clone(), (*parent).clone()) - .unwrap(); - let mut fork_choice = ForkChoice::from_anchor( - fc_store, - parent.beacon_block_root, - &parent.beacon_block, - &parent.beacon_state, - None, - &self.spec, - ) - .unwrap(); - let mut state = parent.beacon_state.clone(); - let parent_randao = *state.get_randao_mix(state.current_epoch()).unwrap(); - *state.slot_mut() = Slot::new(1); - state - .set_block_root(Slot::new(0), parent.beacon_block_root) - .unwrap(); - state - .set_randao_mix(state.current_epoch(), Hash256::repeat_byte(0xab)) - .unwrap(); - state - .latest_execution_payload_bid_mut() - .unwrap() - .prev_randao = parent_randao; - let mut block = genesis_block(&state, &self.spec).unwrap(); - *block.slot_mut() = state.slot(); - *block.parent_root_mut() = parent.beacon_block_root; - *block.state_root_mut() = state.update_tree_hash_cache().unwrap(); - let signed_block = SignedBeaconBlock::from_block(block, Signature::empty()); - let root = signed_block.canonical_root(); - let mut proto_block = self.slot_1_proto_block(root, ExecutionBlockHash::repeat_byte(0xab)); - proto_block.execution_payload_parent_hash = Some(self.execution_parent_hash()); - fork_choice - .proto_array_mut() - .process_block::(proto_block, Slot::new(1), &self.spec, Duration::ZERO) - .unwrap(); - let (_, payload_status) = fork_choice.get_head(Slot::new(1), &self.spec).unwrap(); - self.canonical_head = CanonicalHead::new( - fork_choice, - Arc::new(BeaconSnapshot::new( - Arc::new(signed_block), - None, - root, - state, - )), - payload_status, - FastConfirmationMode::Disabled, - &self.store, - &self.spec, - ) - .unwrap(); - self.slot_clock.set_slot(1); - } - fn put_envelope_with_builder_exit( &self, block_root: Hash256, @@ -1204,38 +1147,6 @@ fn valid_bid_with_parent_in_previous_epoch() { } } -#[test] -fn bid_on_head_parent_uses_parent_randao() { - if !fork_name_from_env().is_some_and(|fork| fork.gloas_enabled()) { - return; - } - let mut ctx = TestContext::new(); - let parent_randao = ctx.canonical_head.cached_head().head_random().unwrap(); - ctx.advance_head(); - let head_randao = ctx.canonical_head.cached_head().head_random().unwrap(); - assert_ne!(head_randao, parent_randao); - let slot = Slot::new(2); - seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); - let mut bid = ExecutionPayloadBid { - slot, - builder_index: 0, - fee_recipient: Address::ZERO, - gas_limit: 30_000_000, - parent_block_root: ctx.genesis_block_root, - parent_block_hash: ctx.execution_parent_hash(), - prev_randao: head_randao, - ..ExecutionPayloadBid::default() - }; - let result = GossipVerifiedPayloadBid::new(ctx.sign_bid(bid.clone()), &ctx.gossip_ctx()); - assert!( - matches!(result, Err(PayloadBidError::InvalidPrevRandao { .. })), - "{result:?}" - ); - bid.prev_randao = parent_randao; - let result = GossipVerifiedPayloadBid::new(ctx.sign_bid(bid), &ctx.gossip_ctx()); - assert!(result.is_ok(), "{result:?}"); -} - #[test] fn two_builders_coexist_in_cache() { if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) {