From afc2346d9e3c746b46d8dd89c722c940883a07fc 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 | 31 +++++------------------ 2 files changed, 22 insertions(+), 28 deletions(-) diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index a9e62dbe94..28c1dea2a6 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 9dfbc87b48..46541e0326 100644 --- a/consensus/state_processing/src/genesis.rs +++ b/consensus/state_processing/src/genesis.rs @@ -167,21 +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 latest_block_header to reflect the Gloas genesis block body which contains - // the EL genesis hash in the signed_execution_payload_bid. This is needed because - // BeaconState::new() created the header from BeaconBlock::empty() which has zero bid - // fields, but the spec requires the genesis block's bid to contain the EL block hash - // and the tree hash root of empty ExecutionRequests. - let block = genesis_block(&state, spec)?; - state.latest_block_header_mut().body_root = block.body_root(); } // Now that we have our validators, initialize the caches (including the committees) @@ -193,26 +184,16 @@ pub fn initialize_beacon_state_from_eth1( Ok(state) } -/// Create an unsigned genesis `BeaconBlock` whose body matches the genesis state. +/// Create an unsigned genesis `BeaconBlock` matching the genesis state. /// -/// For Gloas, the block's `signed_execution_payload_bid` is populated from the state's -/// `latest_execution_payload_bid` so that the body root is consistent with -/// `state.latest_block_header.body_root`. -/// -/// The returned block has `state_root == Hash256::ZERO`; callers that need the real -/// state root should set it themselves. +/// 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( - genesis_state: &BeaconState, + _genesis_state: &BeaconState, spec: &ChainSpec, ) -> Result, BeaconStateError> { - let mut block = BeaconBlock::empty(spec); - if let Ok(block) = block.as_gloas_mut() { - let state_bid = genesis_state.latest_execution_payload_bid()?; - let bid = &mut block.body.signed_execution_payload_bid.message; - bid.block_hash = state_bid.block_hash; - bid.execution_requests_root = state_bid.execution_requests_root; - } - Ok(block) + Ok(BeaconBlock::empty(spec)) } /// Determine whether a candidate genesis state is suitable for starting the chain.