From 0078b6be898d6e8bcc88e84e299ef247b5b5a11f Mon Sep 17 00:00:00 2001 From: Josh King Date: Mon, 27 Apr 2026 23:26:29 +0200 Subject: [PATCH] fix: gloas from genesis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix forkchoice update sending zero-hash head to EL at genesis by reading latest_block_hash from state when the genesis bid hashes are all zeros - Simplify genesis block construction — the genesis block body is empty per spec, remove the incorrect bid-copying logic and body root override in initialize_beacon_state_from_eth1 --- consensus/fork_choice/src/fork_choice.rs | 19 ++++++++++++--- consensus/state_processing/src/genesis.rs | 28 +++++------------------ 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index 477d1fa3b4..1cee6cba92 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -416,11 +416,24 @@ where let (execution_status, execution_payload_parent_hash, execution_payload_block_hash) = if let Ok(signed_bid) = anchor_block.message().body().signed_execution_payload_bid() { - // Gloas: execution status is irrelevant post-Gloas; payload validation - // is decoupled from beacon blocks. + // At Gloas genesis the block bid is empty (all zeros) per spec, but the + // state holds the EL genesis hash in `latest_block_hash`. Use it so the + // first forkchoice update sends a valid head to the EL. + let parent_hash = if anchor_block.slot() == spec.genesis_slot + && anchor_state.slot() == spec.genesis_slot + && signed_bid.message.parent_block_hash.into_root().is_zero() + && signed_bid.message.block_hash.into_root().is_zero() + { + *anchor_state + .latest_block_hash() + .map_err(Error::BeaconStateError)? + } else { + signed_bid.message.parent_block_hash + }; + ( ExecutionStatus::irrelevant(), - Some(signed_bid.message.parent_block_hash), + Some(parent_hash), Some(signed_bid.message.block_hash), ) } else if let Ok(execution_payload) = anchor_block.message().execution_payload() { diff --git a/consensus/state_processing/src/genesis.rs b/consensus/state_processing/src/genesis.rs index c643ad56e3..46541e0326 100644 --- a/consensus/state_processing/src/genesis.rs +++ b/consensus/state_processing/src/genesis.rs @@ -167,19 +167,12 @@ pub fn initialize_beacon_state_from_eth1( // Remove intermediate Fulu fork from `state.fork`. state.fork_mut().previous_version = spec.gloas_fork_version; - // 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()?; 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; } // Now that we have our validators, initialize the caches (including the committees) @@ -191,25 +184,16 @@ pub fn initialize_beacon_state_from_eth1( Ok(state) } -/// Create an unsigned genesis `BeaconBlock`. +/// Create an unsigned genesis `BeaconBlock` matching the genesis state. /// -/// Per spec, the genesis block body is empty (all default fields) except for Gloas, -/// where `body.signed_execution_payload_bid.message` is initialised from -/// `state.latest_execution_payload_bid` so that the first post-genesis proposer can -/// build on the correct execution layer head. -/// -/// `state.latest_block_header.body_root` is set from this same block's body, so the -/// two must stay in sync. +/// Per spec, the genesis block body is empty (all default fields). +/// `state.latest_block_header.body_root` is set from `BeaconBlock::empty()`, +/// so this function must return the same empty block to keep roots consistent. pub fn genesis_block( - state: &BeaconState, + _genesis_state: &BeaconState, spec: &ChainSpec, ) -> Result, BeaconStateError> { - let mut block = BeaconBlock::empty(spec); - if let BeaconBlock::Gloas(ref mut gloas_block) = block { - let bid = state.latest_execution_payload_bid()?.clone(); - gloas_block.body.signed_execution_payload_bid.message = bid; - } - Ok(block) + Ok(BeaconBlock::empty(spec)) } /// Determine whether a candidate genesis state is suitable for starting the chain.