fix(core/genesis): set WithdrawalsHash on Cancun-active genesis headers

A genesis header with Cancun active (CancunTime=0) populated the Cancun fields
(ParentBeaconRoot/ExcessBlobGas/BlobGasUsed) but left WithdrawalsHash nil,
because the assignment was guarded by num.Sign()!=0 (skipped at block 0). RLP
encodes post-merge fields positionally, so a nil WithdrawalsHash followed by
present Cancun fields yields a header ReadHeader cannot decode:
  rlp: input string too short for common.Hash, decoding into ...WithdrawalsHash
NewHeaderChain then fabricates a fake genesis header, which has the wrong state
root, so block-building on a fresh L1 fails and the chain produces zero blocks.

Cancun implies Shanghai, so a Cancun header MUST carry WithdrawalsHash. Set it
(EmptyWithdrawalsHash) whenever Cancun is active and it's nil. Chains preserving
a pre-withdrawals genesis hash opt out via SkipPostMergeFields (short-circuits
the whole post-merge block), so they're unaffected.

Verified on lqd devnet (chainId 8675312): genesis header round-trips, balance
readable at 0x0, self-transfer mines block 1.
This commit is contained in:
Darkhorse7stars
2026-06-03 09:23:29 -07:00
parent 3d0966555f
commit 5ae4f42d2a
+15 -4
View File
@@ -471,6 +471,21 @@ func (g *Genesis) toBlock(db ethdb.Database, triedb *triedb.Database) *types.Blo
withdrawals = make([]*types.Withdrawal, 0)
}
if conf.IsCancun(num, g.Timestamp) {
// Cancun implies Shanghai: a Cancun header MUST carry a
// WithdrawalsHash because the RLP codec reads it positionally
// before the Cancun fields below. The `num.Sign() != 0` guard
// above omits it at genesis, yielding an undecodable header
// ("rlp: input string too short for common.Hash, decoding into
// ...WithdrawalsHash") — which makes NewHeaderChain fabricate a
// fake genesis header and the chain produce zero blocks. Set it
// here so the genesis header round-trips. Chains that must keep a
// pre-withdrawals genesis hash opt out entirely via SkipPostMergeFields.
if head.WithdrawalsHash == nil {
head.WithdrawalsHash = &types.EmptyWithdrawalsHash
if withdrawals == nil {
withdrawals = make([]*types.Withdrawal, 0)
}
}
// EIP-4788: The parentBeaconBlockRoot of the genesis block is always
// the zero hash. This is because the genesis block does not have a parent
// by definition.
@@ -564,10 +579,6 @@ func (g *Genesis) Commit(db ethdb.Database, triedb *triedb.Database) (*types.Blo
// Write chain config
customrawdb.WriteChainConfig(db, hash, config)
// Verify the block can be read back - remove debug code later
// canonHash := rawdb.ReadCanonicalHash(db, 0)
// log.Info("After write check", "canonical_hash", canonHash, "block_hash", block.Hash(), "match", canonHash == block.Hash())
return block, nil
}