core/genesis: honor SkipPostMergeFields flag from JSON

Lux mainnet C-Chain canonical genesis hash is
0x3f4fa2a0b0ce089f52bf0ae9199c75ffdd76ecafc987794050cb0d286f1ec61e,
matching the 1,082,781-block historic RLP export at
lux-mainnet-96369.rlp. The genesis JSON activates Cancun at
genesis time (cancunTime=0) for MCOPY etc., but the genesis BLOCK
itself predates Cancun and must use the 16-field pre-Shanghai
header format to preserve hash continuity with the historic chain.

luxfi/geth's core.Genesis exposes SkipPostMergeFields for exactly
this case. luxfi/evm shipped the JSON unmarshaling but ignored the
flag in toBlock(), so any post-merge field that fork-activates at
genesis time (ParentBeaconRoot, ExcessBlobGas, BlobGasUsed,
WithdrawalsHash) gets baked into the header and shifts the
computed genesis hash, e.g. to 0x1ade42ec…. luxd then panics
during EnsureGenesisState because the resulting state root has no
parent in pathdb's layertree ("triedb parent
[0x56e81f17…] layer missing"), which blocks mainnet C-Chain
bootstrap and admin_importChain.

Mirror the upstream geth pattern: gate the entire Shanghai+
post-merge field assignment block with `if !g.SkipPostMergeFields`,
and round-trip the flag through MarshalJSON/UnmarshalJSON in
gen_genesis.go.

Verification (TestSkipPostMergeFieldsGenesisHash pins both):
- `skipPostMergeFields: true` → 0x3f4fa2a0… (canonical, RLP block 0)
- flag absent / false                → different hash (gate is active)
This commit is contained in:
2026-06-01 19:25:03 -07:00
parent b2eb72e963
commit ee78eef322
3 changed files with 254 additions and 52 deletions
+42 -36
View File
@@ -19,24 +19,25 @@ var _ = (*genesisSpecMarshaling)(nil)
// MarshalJSON marshals as JSON.
func (g Genesis) MarshalJSON() ([]byte, error) {
type Genesis struct {
Config *params.ChainConfig `json:"config"`
Nonce math.HexOrDecimal64 `json:"nonce"`
Timestamp math.HexOrDecimal64 `json:"timestamp"`
ExtraData hexutil.Bytes `json:"extraData"`
GasLimit math.HexOrDecimal64 `json:"gasLimit" gencodec:"required"`
Difficulty *math.HexOrDecimal256 `json:"difficulty" gencodec:"required"`
Mixhash common.Hash `json:"mixHash"`
Coinbase common.Address `json:"coinbase"`
Alloc map[common.UnprefixedAddress]types.Account `json:"alloc" gencodec:"required"`
AirdropHash common.Hash `json:"airdropHash"`
AirdropAmount *math.HexOrDecimal256 `json:"airdropAmount"`
AirdropData []byte `json:"-"`
Number math.HexOrDecimal64 `json:"number"`
GasUsed math.HexOrDecimal64 `json:"gasUsed"`
ParentHash common.Hash `json:"parentHash"`
BaseFee *math.HexOrDecimal256 `json:"baseFeePerGas"`
ExcessBlobGas *math.HexOrDecimal64 `json:"excessBlobGas"`
BlobGasUsed *math.HexOrDecimal64 `json:"blobGasUsed"`
Config *params.ChainConfig `json:"config"`
Nonce math.HexOrDecimal64 `json:"nonce"`
Timestamp math.HexOrDecimal64 `json:"timestamp"`
ExtraData hexutil.Bytes `json:"extraData"`
GasLimit math.HexOrDecimal64 `json:"gasLimit" gencodec:"required"`
Difficulty *math.HexOrDecimal256 `json:"difficulty" gencodec:"required"`
Mixhash common.Hash `json:"mixHash"`
Coinbase common.Address `json:"coinbase"`
Alloc map[common.UnprefixedAddress]types.Account `json:"alloc" gencodec:"required"`
AirdropHash common.Hash `json:"airdropHash"`
AirdropAmount *math.HexOrDecimal256 `json:"airdropAmount"`
AirdropData []byte `json:"-"`
Number math.HexOrDecimal64 `json:"number"`
GasUsed math.HexOrDecimal64 `json:"gasUsed"`
ParentHash common.Hash `json:"parentHash"`
BaseFee *math.HexOrDecimal256 `json:"baseFeePerGas"`
ExcessBlobGas *math.HexOrDecimal64 `json:"excessBlobGas"`
BlobGasUsed *math.HexOrDecimal64 `json:"blobGasUsed"`
SkipPostMergeFields bool `json:"skipPostMergeFields,omitempty"`
}
var enc Genesis
enc.Config = g.Config
@@ -62,30 +63,32 @@ func (g Genesis) MarshalJSON() ([]byte, error) {
enc.BaseFee = (*math.HexOrDecimal256)(g.BaseFee)
enc.ExcessBlobGas = (*math.HexOrDecimal64)(g.ExcessBlobGas)
enc.BlobGasUsed = (*math.HexOrDecimal64)(g.BlobGasUsed)
enc.SkipPostMergeFields = g.SkipPostMergeFields
return json.Marshal(&enc)
}
// UnmarshalJSON unmarshals from JSON.
func (g *Genesis) UnmarshalJSON(input []byte) error {
type Genesis struct {
Config *params.ChainConfig `json:"config"`
Nonce *math.HexOrDecimal64 `json:"nonce"`
Timestamp *math.HexOrDecimal64 `json:"timestamp"`
ExtraData *hexutil.Bytes `json:"extraData"`
GasLimit *math.HexOrDecimal64 `json:"gasLimit" gencodec:"required"`
Difficulty *math.HexOrDecimal256 `json:"difficulty" gencodec:"required"`
Mixhash *common.Hash `json:"mixHash"`
Coinbase *common.Address `json:"coinbase"`
Alloc map[common.UnprefixedAddress]types.Account `json:"alloc" gencodec:"required"`
AirdropHash *common.Hash `json:"airdropHash"`
AirdropAmount *math.HexOrDecimal256 `json:"airdropAmount"`
AirdropData []byte `json:"-"`
Number *math.HexOrDecimal64 `json:"number"`
GasUsed *math.HexOrDecimal64 `json:"gasUsed"`
ParentHash *common.Hash `json:"parentHash"`
BaseFee *math.HexOrDecimal256 `json:"baseFeePerGas"`
ExcessBlobGas *math.HexOrDecimal64 `json:"excessBlobGas"`
BlobGasUsed *math.HexOrDecimal64 `json:"blobGasUsed"`
Config *params.ChainConfig `json:"config"`
Nonce *math.HexOrDecimal64 `json:"nonce"`
Timestamp *math.HexOrDecimal64 `json:"timestamp"`
ExtraData *hexutil.Bytes `json:"extraData"`
GasLimit *math.HexOrDecimal64 `json:"gasLimit" gencodec:"required"`
Difficulty *math.HexOrDecimal256 `json:"difficulty" gencodec:"required"`
Mixhash *common.Hash `json:"mixHash"`
Coinbase *common.Address `json:"coinbase"`
Alloc map[common.UnprefixedAddress]types.Account `json:"alloc" gencodec:"required"`
AirdropHash *common.Hash `json:"airdropHash"`
AirdropAmount *math.HexOrDecimal256 `json:"airdropAmount"`
AirdropData []byte `json:"-"`
Number *math.HexOrDecimal64 `json:"number"`
GasUsed *math.HexOrDecimal64 `json:"gasUsed"`
ParentHash *common.Hash `json:"parentHash"`
BaseFee *math.HexOrDecimal256 `json:"baseFeePerGas"`
ExcessBlobGas *math.HexOrDecimal64 `json:"excessBlobGas"`
BlobGasUsed *math.HexOrDecimal64 `json:"blobGasUsed"`
SkipPostMergeFields *bool `json:"skipPostMergeFields,omitempty"`
}
var dec Genesis
if err := json.Unmarshal(input, &dec); err != nil {
@@ -151,5 +154,8 @@ func (g *Genesis) UnmarshalJSON(input []byte) error {
if dec.BlobGasUsed != nil {
g.BlobGasUsed = (*uint64)(dec.BlobGasUsed)
}
if dec.SkipPostMergeFields != nil {
g.SkipPostMergeFields = *dec.SkipPostMergeFields
}
return nil
}
+39 -16
View File
@@ -94,6 +94,19 @@ type Genesis struct {
BaseFee *big.Int `json:"baseFeePerGas"` // EIP-1559
ExcessBlobGas *uint64 `json:"excessBlobGas"` // EIP-4844
BlobGasUsed *uint64 `json:"blobGasUsed"` // EIP-4844
// SkipPostMergeFields disables adding Shanghai/Cancun/Prague header
// fields (WithdrawalsHash, ParentBeaconRoot, ExcessBlobGas,
// BlobGasUsed, RequestsHash) to the genesis block, regardless of
// whether the corresponding forks are active at genesis time.
//
// Lux mainnet's canonical C-Chain genesis hash (0x3f4fa2a0…) was
// produced with the 16-field pre-Shanghai header format. The chain
// activates Cancun at genesis time (cancunTime=0) for EVM bytecode
// (MCOPY etc.) but the genesis BLOCK itself must stay in the legacy
// header shape to preserve hash continuity with historic RLP
// exports. Mirrors the SkipPostMergeFields flag in luxfi/geth core.
SkipPostMergeFields bool `json:"skipPostMergeFields,omitempty"`
}
// field type overrides for gencodec
@@ -444,23 +457,33 @@ func (g *Genesis) toBlock(db ethdb.Database, triedb *triedb.Database) *types.Blo
// at genesis timestamp, but the genesis BLOCK itself must remain
// in pre-withdrawals format to preserve hash continuity. Only
// post-genesis blocks (Number > 0) get a WithdrawalsHash here.
if num.Sign() != 0 && conf.IsShanghai(num, g.Timestamp) {
head.WithdrawalsHash = &types.EmptyWithdrawalsHash
withdrawals = make([]*types.Withdrawal, 0)
}
if conf.IsCancun(num, g.Timestamp) {
// 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.
head.ParentBeaconRoot = new(common.Hash)
// EIP-4844 fields
head.ExcessBlobGas = g.ExcessBlobGas
head.BlobGasUsed = g.BlobGasUsed
if head.ExcessBlobGas == nil {
head.ExcessBlobGas = new(uint64)
//
// SkipPostMergeFields is the explicit operator opt-out for ALL
// post-merge header fields (mirrors luxfi/geth core.Genesis). When
// set, neither WithdrawalsHash, ParentBeaconRoot, ExcessBlobGas
// nor BlobGasUsed are populated, regardless of fork activation
// time. This is the canonical path for replaying historic RLP
// exports whose block-0 hash was produced before any post-merge
// fork existed (e.g. lux-mainnet-96369.rlp → 0x3f4fa2a0…).
if !g.SkipPostMergeFields {
if num.Sign() != 0 && conf.IsShanghai(num, g.Timestamp) {
head.WithdrawalsHash = &types.EmptyWithdrawalsHash
withdrawals = make([]*types.Withdrawal, 0)
}
if head.BlobGasUsed == nil {
head.BlobGasUsed = new(uint64)
if conf.IsCancun(num, g.Timestamp) {
// 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.
head.ParentBeaconRoot = new(common.Hash)
// EIP-4844 fields
head.ExcessBlobGas = g.ExcessBlobGas
head.BlobGasUsed = g.BlobGasUsed
if head.ExcessBlobGas == nil {
head.ExcessBlobGas = new(uint64)
}
if head.BlobGasUsed == nil {
head.BlobGasUsed = new(uint64)
}
}
}
}
+173
View File
@@ -0,0 +1,173 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package core
import (
"encoding/json"
"testing"
"github.com/luxfi/geth/common"
)
// TestSkipPostMergeFieldsGenesisHash pins the canonical Lux mainnet
// C-Chain genesis hash 0x3f4fa2a0b0ce089f52bf0ae9199c75ffdd76ecafc987794050cb0d286f1ec61e.
//
// The genesis JSON sets cancunTime=0, so without SkipPostMergeFields
// the genesis block would carry ParentBeaconRoot/ExcessBlobGas/BlobGasUsed
// header fields that did not exist when the historic chain was launched.
// Honoring the JSON flag preserves hash continuity with the historic
// 1.08M-block RLP export at lux-mainnet-96369.rlp (block 0 hash above).
func TestSkipPostMergeFieldsGenesisHash(t *testing.T) {
const luxMainnetGenesis = `{
"alloc": {
"0200000000000000000000000000000000000005": {
"balance": "0x0",
"code": "0x01",
"nonce": "0x1"
},
"0x9011E888251AB053B7bD1cdB598Db4f9DEd94714": {
"balance": "0x193e5939a08ce9dbd480000000"
}
},
"baseFeePerGas": "0x5d21dba00",
"coinbase": "0x0000000000000000000000000000000000000000",
"config": {
"arrowGlacierBlock": 0,
"berlinBlock": 0,
"byzantiumBlock": 0,
"cancunTime": 0,
"chainId": 96369,
"constantinopleBlock": 0,
"eip150Block": 0,
"eip155Block": 0,
"eip158Block": 0,
"evmTimestamp": 0,
"feeConfig": {
"baseFeeChangeDenominator": 36,
"blockGasCostStep": 200000,
"gasLimit": 12000000,
"maxBlockGasCost": 1000000,
"minBaseFee": 25000000000,
"minBlockGasCost": 0,
"targetBlockRate": 2,
"targetGas": 500000000
},
"grayGlacierBlock": 0,
"homesteadBlock": 0,
"istanbulBlock": 0,
"londonBlock": 0,
"mergeNetsplitBlock": 0,
"muirGlacierBlock": 0,
"petersburgBlock": 0,
"shanghaiTime": 0,
"terminalTotalDifficulty": 0,
"durangoTimestamp": 0,
"etnaTimestamp": 0,
"fortunaTimestamp": 0,
"graniteTimestamp": 0
},
"difficulty": "0x0",
"extraData": "0x",
"gasLimit": "0xb71b00",
"gasUsed": "0x0",
"mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"nonce": "0x0",
"number": "0x0",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"skipPostMergeFields": true,
"stateRoot": "0x2d1cedac263020c5c56ef962f6abe0da1f5217bdc6468f8c9258a0ea23699e80",
"timestamp": "0x672485c2"
}`
const wantHash = "0x3f4fa2a0b0ce089f52bf0ae9199c75ffdd76ecafc987794050cb0d286f1ec61e"
var g Genesis
if err := json.Unmarshal([]byte(luxMainnetGenesis), &g); err != nil {
t.Fatalf("unmarshal genesis: %v", err)
}
if !g.SkipPostMergeFields {
t.Fatalf("SkipPostMergeFields not honored from JSON")
}
got := g.ToBlock().Hash()
if got != common.HexToHash(wantHash) {
t.Fatalf("genesis hash mismatch:\n got %s\n want %s", got.Hex(), wantHash)
}
}
// TestPostMergeFieldsActiveWhenFlagFalse ensures that without
// SkipPostMergeFields the genesis hash flips to a different value
// (i.e. the flag is the gate, not a no-op). Uses the same chain spec
// minus the flag.
func TestPostMergeFieldsActiveWhenFlagFalse(t *testing.T) {
const baseGenesis = `{
"alloc": {
"0200000000000000000000000000000000000005": {
"balance": "0x0",
"code": "0x01",
"nonce": "0x1"
},
"0x9011E888251AB053B7bD1cdB598Db4f9DEd94714": {
"balance": "0x193e5939a08ce9dbd480000000"
}
},
"baseFeePerGas": "0x5d21dba00",
"coinbase": "0x0000000000000000000000000000000000000000",
"config": {
"arrowGlacierBlock": 0,
"berlinBlock": 0,
"byzantiumBlock": 0,
"cancunTime": 0,
"chainId": 96369,
"constantinopleBlock": 0,
"eip150Block": 0,
"eip155Block": 0,
"eip158Block": 0,
"evmTimestamp": 0,
"feeConfig": {
"baseFeeChangeDenominator": 36,
"blockGasCostStep": 200000,
"gasLimit": 12000000,
"maxBlockGasCost": 1000000,
"minBaseFee": 25000000000,
"minBlockGasCost": 0,
"targetBlockRate": 2,
"targetGas": 500000000
},
"grayGlacierBlock": 0,
"homesteadBlock": 0,
"istanbulBlock": 0,
"londonBlock": 0,
"mergeNetsplitBlock": 0,
"muirGlacierBlock": 0,
"petersburgBlock": 0,
"shanghaiTime": 0,
"terminalTotalDifficulty": 0,
"durangoTimestamp": 0,
"etnaTimestamp": 0,
"fortunaTimestamp": 0,
"graniteTimestamp": 0
},
"difficulty": "0x0",
"extraData": "0x",
"gasLimit": "0xb71b00",
"gasUsed": "0x0",
"mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"nonce": "0x0",
"number": "0x0",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"stateRoot": "0x2d1cedac263020c5c56ef962f6abe0da1f5217bdc6468f8c9258a0ea23699e80",
"timestamp": "0x672485c2"
}`
var g Genesis
if err := json.Unmarshal([]byte(baseGenesis), &g); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if g.SkipPostMergeFields {
t.Fatalf("SkipPostMergeFields should default false")
}
got := g.ToBlock().Hash()
canonical := common.HexToHash("0x3f4fa2a0b0ce089f52bf0ae9199c75ffdd76ecafc987794050cb0d286f1ec61e")
if got == canonical {
t.Fatalf("expected different hash when post-merge fields are active, got canonical %s", canonical.Hex())
}
}