From 0078b6be898d6e8bcc88e84e299ef247b5b5a11f Mon Sep 17 00:00:00 2001 From: Josh King Date: Mon, 27 Apr 2026 23:26:29 +0200 Subject: [PATCH 1/6] 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 477d1fa3b4e..1cee6cba926 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 c643ad56e34..46541e03263 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`. -/// -/// 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. +/// Create an unsigned genesis `BeaconBlock` matching the genesis state. /// -/// `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. From 5360e7669686936c856eb920539ca580e99a18fa Mon Sep 17 00:00:00 2001 From: hopinheimer Date: Mon, 27 Apr 2026 01:57:22 -0400 Subject: [PATCH 2/6] fix `genesis_block` init in tests --- .../beacon_chain/src/payload_attestation_verification/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs b/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs index 7faad98e550..5fbcc481de2 100644 --- a/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs +++ b/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs @@ -7,8 +7,8 @@ use genesis::{generate_deterministic_keypairs, interop_genesis_state}; use parking_lot::RwLock; use proto_array::PayloadStatus; use slot_clock::{SlotClock, TestingSlotClock}; -use state_processing::AllCaches; use state_processing::genesis::genesis_block; +use state_processing::AllCaches; use store::{HotColdDB, StoreConfig}; use types::{ ChainSpec, Checkpoint, Domain, Epoch, EthSpec, Hash256, MinimalEthSpec, PayloadAttestationData, From 0fe75382da0eaf12f774ba8ed47de6a90cafc1b4 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Thu, 30 Apr 2026 13:05:30 +0200 Subject: [PATCH 3/6] fmt --- .../beacon_chain/src/payload_attestation_verification/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs b/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs index 5fbcc481de2..7faad98e550 100644 --- a/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs +++ b/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs @@ -7,8 +7,8 @@ use genesis::{generate_deterministic_keypairs, interop_genesis_state}; use parking_lot::RwLock; use proto_array::PayloadStatus; use slot_clock::{SlotClock, TestingSlotClock}; -use state_processing::genesis::genesis_block; use state_processing::AllCaches; +use state_processing::genesis::genesis_block; use store::{HotColdDB, StoreConfig}; use types::{ ChainSpec, Checkpoint, Domain, Epoch, EthSpec, Hash256, MinimalEthSpec, PayloadAttestationData, From 989eb4dd80e63237bc56a92eb1b7ad89895a7574 Mon Sep 17 00:00:00 2001 From: Josh King Date: Thu, 30 Apr 2026 14:21:02 +0200 Subject: [PATCH 4/6] fix: restore genesis_block bid population for ef-tests The alpha-7 spec tests expect the Gloas genesis block body to contain the execution payload bid from state. Restores the genesis_block() function and body_root fixup that were removed in PR #9244. The fork choice from_anchor fix (reading latest_block_hash when bid hashes are zero) remains for Kurtosis/external genesis compatibility. --- consensus/state_processing/src/genesis.rs | 28 ++++++++++++++++++----- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/consensus/state_processing/src/genesis.rs b/consensus/state_processing/src/genesis.rs index 46541e03263..c643ad56e34 100644 --- a/consensus/state_processing/src/genesis.rs +++ b/consensus/state_processing/src/genesis.rs @@ -167,12 +167,19 @@ 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) @@ -184,16 +191,25 @@ pub fn initialize_beacon_state_from_eth1( Ok(state) } -/// Create an unsigned genesis `BeaconBlock` matching the genesis state. +/// Create an unsigned genesis `BeaconBlock`. +/// +/// 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. /// -/// 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. +/// `state.latest_block_header.body_root` is set from this same block's body, so the +/// two must stay in sync. pub fn genesis_block( - _genesis_state: &BeaconState, + state: &BeaconState, spec: &ChainSpec, ) -> Result, BeaconStateError> { - Ok(BeaconBlock::empty(spec)) + 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) } /// Determine whether a candidate genesis state is suitable for starting the chain. From 78f22be80c37de26a62862c43b48b9c1ae0ebdcf Mon Sep 17 00:00:00 2001 From: Josh King Date: Thu, 30 Apr 2026 15:26:20 +0200 Subject: [PATCH 5/6] fix: fallback to empty genesis block for external genesis states --- beacon_node/beacon_chain/src/builder.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index d70561db9ba..5fb572d15ca 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -46,8 +46,8 @@ use tracing::{debug, error, info, warn}; use tree_hash::TreeHash; use types::data::CustodyIndex; use types::{ - BeaconState, BlobSidecarList, ChainSpec, ColumnIndex, DataColumnSidecarList, Epoch, EthSpec, - Hash256, SignedBeaconBlock, Slot, + BeaconBlock, BeaconState, BlobSidecarList, ChainSpec, ColumnIndex, DataColumnSidecarList, + Epoch, EthSpec, Hash256, SignedBeaconBlock, Slot, }; /// An empty struct used to "witness" all the `BeaconChainTypes` traits. It has no user-facing @@ -1177,9 +1177,19 @@ fn make_genesis_block( genesis_state: &mut BeaconState, spec: &ChainSpec, ) -> Result, String> { + // For Gloas, genesis_block() populates the bid in the block body. However, if + // the genesis state was produced by an external tool (e.g. ethereum-genesis-generator), + // its latest_block_header.body_root may correspond to an empty block. In that case, + // use an empty block so the stored block root matches what fork choice derives from + // the state's latest_block_header. let mut block = genesis_block(genesis_state, spec) .map_err(|e| format!("Error building genesis block: {:?}", e))?; + let state_body_root = genesis_state.latest_block_header().body_root; + if state_body_root != block.body_root() { + block = BeaconBlock::empty(spec); + } + *block.state_root_mut() = genesis_state .update_tree_hash_cache() .map_err(|e| format!("Error hashing genesis state: {:?}", e))?; From c32b89c902722bad348ae42434d5dd2c84015ff8 Mon Sep 17 00:00:00 2001 From: Josh King Date: Thu, 30 Apr 2026 17:27:37 +0200 Subject: [PATCH 6/6] fix: tighten genesis block fallback to match empty body root only --- beacon_node/beacon_chain/src/builder.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 5fb572d15ca..95b5f32a940 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -1186,7 +1186,9 @@ fn make_genesis_block( .map_err(|e| format!("Error building genesis block: {:?}", e))?; let state_body_root = genesis_state.latest_block_header().body_root; - if state_body_root != block.body_root() { + if state_body_root != block.body_root() + && state_body_root == BeaconBlock::::empty(spec).body_root() + { block = BeaconBlock::empty(spec); }