From 6fc8ac5d316c1463b26f7932bd1560cfbcae7229 Mon Sep 17 00:00:00 2001 From: Barnabas Busa Date: Sun, 24 May 2026 11:45:46 +0200 Subject: [PATCH] Fix Gloas genesis block reconstruction (#16821) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary On a Gloas-genesis devnet (Kurtosis, glamsterdam-devnet-4), Prysm reconstructs a genesis block whose `body_root` does **not** match `state.latest_block_header.body_root`. Every Lodestar/Lighthouse node returns canonical genesis root `0x2ed167c76d353b77…`; both Prysm nodes return `0xfef8248511dc8b91…` (orphaned). All clients agree on the state root (`0xf29bc0bb3aff0efa…`), so the divergence is purely in Prysm's locally-constructed `BeaconBlockBodyGloas`, not in the loaded SSZ state. The validator client surfaces the inconsistency at slot 1 with the BN's own state: ``` ERROR rpc/validator: Finished building block error=… could not process block header: parent root 0xfef8248511… does not match the latest block header signing root in state 0x2ed167c7… ``` Diffing the JSON of the genesis block body returned by Lighthouse vs Prysm reveals exactly two fields differ, both inside `signed_execution_payload_bid.message`: | field | Lighthouse / Lodestar (canonical) | Prysm (orphan) | |---|---|---| | `parent_block_hash` | `0xab172d9e48985f0dc7789…` (== `state.latest_block_hash`) | `0x0000…0000` | | `execution_requests_root` | `0x85e253b40599d0df756be…` (== `hash_tree_root(ExecutionRequests())`) | `0x0000…0000` | Both of those values appear verbatim in `state.latest_execution_payload_bid` (verified against the genesis SSZ from `eth-beacon-genesis`). The genesis distribution tool commits to `body.signed_execution_payload_bid.message == state.latest_execution_payload_bid`; Prysm's `gloasGenesisBlock` was using zero defaults instead. This PR threads `state.LatestExecutionPayloadBid` into `gloasGenesisBlock` so the reconstructed body matches what the state header committed to. Also initializes `parent_execution_requests` to a non-nil empty `ExecutionRequests`, since the proto field was added but never populated here — `BeaconBlockBodyGloas.HashTreeRoot()` dereferences `ParentExecutionRequests` unconditionally (`gloas.ssz.go`). ## Test plan - [x] `go build ./...` - [x] `go test ./beacon-chain/core/blocks/ -run TestGenesis` - [ ] Verify on a Gloas Kurtosis devnet that: - All Prysm nodes return the same genesis `block_root` as Lighthouse/Lodestar/Teku. - Validator proposes successfully at slot 1 (no "parent root … does not match" error). - [ ] Walk a fresh chain from genesis through the Gloas fork epoch (non-Gloas-genesis path) to confirm no regression for `upgrade_to_gloas` flows. --- beacon-chain/core/blocks/genesis.go | 36 +++++++++++++------ .../barnabasbusa_fix-gloas-genesis-block.md | 3 ++ 2 files changed, 29 insertions(+), 10 deletions(-) create mode 100644 changelog/barnabasbusa_fix-gloas-genesis-block.md diff --git a/beacon-chain/core/blocks/genesis.go b/beacon-chain/core/blocks/genesis.go index afdfdfd48c68..435a02fb24e0 100644 --- a/beacon-chain/core/blocks/genesis.go +++ b/beacon-chain/core/blocks/genesis.go @@ -193,8 +193,9 @@ func NewGenesisBlockForState(ctx context.Context, st state.BeaconState) (interfa Signature: params.BeaconConfig().EmptySignature[:], }) case *ethpb.BeaconStateGloas: + gs := ps.(*ethpb.BeaconStateGloas) return blocks.NewSignedBeaconBlock(ðpb.SignedBeaconBlockGloas{ - Block: gloasGenesisBlock(root), + Block: gloasGenesisBlock(root, gs.LatestExecutionPayloadBid), Signature: params.BeaconConfig().EmptySignature[:], }) default: @@ -202,7 +203,24 @@ func NewGenesisBlockForState(ctx context.Context, st state.BeaconState) (interfa } } -func gloasGenesisBlock(root [fieldparams.RootLength]byte) *ethpb.BeaconBlockGloas { +func gloasGenesisBlock(root [fieldparams.RootLength]byte, latestBid *ethpb.ExecutionPayloadBid) *ethpb.BeaconBlockGloas { + // The genesis block body's signed_execution_payload_bid mirrors the state's + // latest_execution_payload_bid so the reconstructed block's body_root matches + // state.latest_block_header.body_root (which the genesis distribution tool + // commits to). Falling back to a zero bid is only useful in tests that + // initialize a Gloas state without populating latest_execution_payload_bid. + bidMessage := latestBid.Copy() + if bidMessage == nil { + bidMessage = ðpb.ExecutionPayloadBid{ + ParentBlockHash: make([]byte, 32), + ParentBlockRoot: make([]byte, 32), + BlockHash: make([]byte, 32), + PrevRandao: make([]byte, 32), + FeeRecipient: make([]byte, 20), + BlobKzgCommitments: make([][]byte, 0), + ExecutionRequestsRoot: make([]byte, 32), + } + } return ðpb.BeaconBlockGloas{ ParentRoot: params.BeaconConfig().ZeroHash[:], StateRoot: root[:], @@ -218,17 +236,15 @@ func gloasGenesisBlock(root [fieldparams.RootLength]byte) *ethpb.BeaconBlockGloa SyncCommitteeSignature: make([]byte, fieldparams.BLSSignatureLength), }, SignedExecutionPayloadBid: ðpb.SignedExecutionPayloadBid{ - Message: ðpb.ExecutionPayloadBid{ - ParentBlockHash: make([]byte, 32), - ParentBlockRoot: make([]byte, 32), - BlockHash: make([]byte, 32), - PrevRandao: make([]byte, 32), - FeeRecipient: make([]byte, 20), - BlobKzgCommitments: make([][]byte, 0), - }, + Message: bidMessage, Signature: make([]byte, fieldparams.BLSSignatureLength), }, PayloadAttestations: make([]*ethpb.PayloadAttestation, 0), + ParentExecutionRequests: &enginev1.ExecutionRequests{ + Withdrawals: make([]*enginev1.WithdrawalRequest, 0), + Deposits: make([]*enginev1.DepositRequest, 0), + Consolidations: make([]*enginev1.ConsolidationRequest, 0), + }, }, } } diff --git a/changelog/barnabasbusa_fix-gloas-genesis-block.md b/changelog/barnabasbusa_fix-gloas-genesis-block.md new file mode 100644 index 000000000000..e0dec53ddc88 --- /dev/null +++ b/changelog/barnabasbusa_fix-gloas-genesis-block.md @@ -0,0 +1,3 @@ +### Fixed + +- Fixed Gloas genesis block reconstruction so the body's `signed_execution_payload_bid.message` mirrors `state.latest_execution_payload_bid`, matching the body root committed to in `state.latest_block_header`. Without this, Prysm built a divergent genesis block (different `block_root` / `body_root` from other clients) and the validator client failed to propose at slot 1 with "parent root … does not match the latest block header signing root in state". Also initializes `parent_execution_requests` so the body's SSZ hash tree root can be computed.