Lux EVM activation is gated by a single canonical chain config field: EVMTimestamp / json:'evmTimestamp' The legacy 'SubnetEVMTimestamp' / 'subnetEVMTimestamp' field was a duplicate alias for the same activation. Removed: - geth/params/config.go: ChainConfig.SubnetEVMTimestamp field - geth/params/config.go: IsEVM() now checks EVMTimestamp only - geth/params/config.go: isLuxL2Chain check uses EVMTimestamp only - all genesis JSON configs (mainnet/testnet/devnet × all chains) - all helm chart genesis files (charts/lux/genesis/*.json) - all docs (LLM.md, *.mdx) One field, one way.
36 KiB
geth Module Documentation
Module: github.com/luxfi/geth
Status: Active development
Last Upstream Sync: 2025-12-12 (go-ethereum 16f50285b)
Lux C-Chain Header Format Management
Overview
The Lux C-chain uses different RLP header formats depending on block era. This codebase implements format-aware encoding/decoding to ensure:
- Hash consistency: Re-encoding produces identical bytes → identical hashes
- Format preservation: Decoded blocks retain their original format when re-encoded
- Upgrade flexibility: New formats can be added without breaking historic data
Critical Network Parameters
Chain ID: 96369
Genesis Hash: 0x3f4fa2a0b0ce089f52bf0ae9199c75ffdd76ecafc987794050cb0d286f1ec61e
State Root: 0x2d1cedac263020c5c56ef962f6abe0da1f5217bdc6468f8c9258a0ea23699e80
Header Formats
| Format | Fields | Description | Used By |
|---|---|---|---|
RLPFormat15 |
15 | Pre-London (legacy Ethereum) | Legacy chains |
RLPFormat16 |
16 | Post-London (+ BaseFee) | Lux Genesis (Block 0) |
RLPFormat17 |
17 | + ExtDataHash (pointer type) | Lux Post-Genesis Blocks |
RLPFormat18 |
18 | + ExtDataGasUsed | Intermediate |
RLPFormat19Lux |
19 | + BlockGasCost (ExtDataHash as VALUE) | Full coreth format |
RLPFormat20+ |
20-24 | + Ethereum 2.0 fields | Future upgrades |
Key Implementation Files
core/types/block.go - Header struct, EncodeRLP, format detection
core/types/decode.go - DecodeHeader, format-specific decoders
core/types/gen_header_rlp.go - Auto-generated default encoder (renamed)
core/rawdb/accessors_chain.go - WriteHeaderWithHash (uses format-aware encoding)
RLPFormat Field
The Header struct tracks its original format:
type Header struct {
// ... fields ...
rlpFormat RLPFormat `json:"-" rlp:"-"` // Tracks original encoding format
}
Format is set during decode and used during encode:
// Set during decode
func decode17(data []byte) (*Header, error) {
// ... decode ...
return &Header{
// ... fields ...
rlpFormat: RLPFormat17, // Track format
}, nil
}
// Used during encode
func (h *Header) EncodeRLP(w io.Writer) error {
switch h.rlpFormat {
case RLPFormat17:
return h.encodeRLP17(w)
case RLPFormat19Lux:
return h.encodeRLP19Lux(w)
// ...
}
}
ExtDataHash Type Difference
Critical: Original coreth used common.Hash (value type), Lux geth uses *common.Hash (pointer):
| Type | nil Encoding | Zero Encoding |
|---|---|---|
common.Hash (value) |
N/A (can't be nil) | 32 zero bytes |
*common.Hash (pointer) |
0x80 (empty string) |
32 zero bytes |
For Lux 19-field format compatibility, encodeRLP19Lux converts pointer to value:
func (h *Header) encodeRLP19Lux(w io.Writer) error {
var extHash common.Hash
if h.ExtDataHash != nil {
extHash = *h.ExtDataHash
}
return rlp.Encode(w, &hdr19val{
// ... ExtDataHash as VALUE type ...
ExtDataHash: extHash,
})
}
Format Detection
For new headers without explicit format, auto-detection is used:
func (h *Header) isLux19Format() bool {
hasLuxFields := h.ExtDataGasUsed != nil || h.BlockGasCost != nil
hasEth2Fields := h.BlobGasUsed != nil || h.ExcessBlobGas != nil ||
h.ParentBeaconRoot != nil || h.WithdrawalsHash != nil
return hasLuxFields && !hasEth2Fields
}
Activating Format Upgrades
To upgrade to a new format for future blocks:
- Define new format constant in
decode.go:
const RLPFormat25 RLPFormat = 25 // New format with extra field
- Add decoder in
decode.go:
func decode25(data []byte) (*Header, error) {
var h hdr25
if err := rlp.DecodeBytes(data, &h); err != nil {
return nil, err
}
return &Header{/* ... */ rlpFormat: RLPFormat25}, nil
}
- Add encoder in
block.go:
func (h *Header) encodeRLP25(w io.Writer) error {
return rlp.Encode(w, &hdr25{/* ... */})
}
- Update EncodeRLP switch in
block.go:
case RLPFormat25:
return h.encodeRLP25(w)
- Set format for new blocks (chain config based):
func (h *Header) SetRLPFormat(f RLPFormat) {
h.rlpFormat = f
}
// In block producer:
if chainConfig.IsUpgradeActive(header.Number) {
header.SetRLPFormat(RLPFormat25)
}
Block Import/Export
The format-aware encoding enables seamless import/export with full state verification:
# Export blocks (preserves original format in RLP)
geth export blocks.rlp 0 1000000
# Import blocks - REQUIRES initialized genesis state
# State re-execution happens for every block
geth import --datadir /path/to/db blocks.rlp
State Verification Requirements
CRITICAL: Block import ALWAYS requires proper state re-execution:
-
Genesis state must be initialized first
geth init --datadir /path/to/db genesis.json -
Every block's state transition is re-executed
- Transactions are re-processed
- State root is verified against header
- Invalid state roots cause import failure
-
No "migration mode" or state-skipping is allowed
- If genesis state is missing, import will error:
ERROR: genesis state missing: cannot import blocks without proper state re-execution. Initialize chain with genesis state first
This ensures cryptographic integrity of the entire chain from genesis to head.
Upstream Merge - December 12, 2025
Summary
Merged 35 upstream commits from go-ethereum (446fdebdc..16f50285b) into luxfi/geth while preserving all lux-specific customizations.
Merge Statistics
- Upstream Commits: 35 commits merged
- Files Changed: 70+ files
- Conflicts Resolved: 5 merge conflicts (import paths + code changes)
- Build Status: Successful (go build ./cmd/geth)
- Test Status: All critical tests passing (core/types, crypto, common, pqcrypto)
Key Upstream Changes Integrated
- cmd/utils: Fix DeveloperFlag handling when set to false
- core/stateless: Cap witness depth metrics buckets
- eth/fetcher: Add metadata validation in tx announcement
- triedb/pathdb: Use copy instead of append to reduce memory alloc
- core/rawdb: Fix size counting in memory freezer
- p2p/tracker: Fix head detection in Fulfil
- eth/tracers/native: Include SWAP16 in default ignored opcodes
- core/state: Fix incorrect contract code state metrics
- eth/downloader: Keep current syncmode in downloader only (new syncModer)
- core/vm: Fix PC increment for EIP-8024 opcodes
- common/bitutil: Deprecate XORBytes in favor of stdlib crypto/subtle
- ethdb/pebble: Enhanced Pebble database configuration with L0CompactionConcurrency
- eth/filters: Change error code for invalid parameter errors
- beacon/types: Update for fulu
- core: Log detailed statistics for slow block
Conflicts Resolved
cmd/evm/blockrunner.go- Added new common/log imports with luxfi pathscore/state/state_sizer.go- Added metrics import with luxfi patheth/downloader/downloader_test.go- Added ethconfig import with luxfi pathethdb/pebble/pebble.go- Kept both lux ReadSamplingMultiplier and new compaction settingsp2p/rlpx/rlpx.go- Removed unused bitutil import (XORBytes moved to crypto/subtle)
API Changes Adapted
BeaconDevSyncsignature changed: now takes only header (sync mode handled by syncModer)- Updated
eth/catalyst/tester.goto match new API
Lux Customizations Preserved
- Post-Quantum Crypto (
crypto/pqcrypto/) - ML-DSA, ML-KEM, SLH-DSA - Database (
ethdb/badgerdb/) - BadgerDB implementation, PebbleDB enhancements - Import Branding - 100%
github.com/luxfi/gethpaths maintained - Dependencies - All
luxfi/*packages preserved (crypto v1.17.15, node v1.20.3, ids v1.1.2)
Upstream Merge - November 22, 2025
Summary
Successfully merged latest go-ethereum upstream changes (739f6f46a..f4817b7a5) into luxfi/geth while preserving ALL lux-specific customizations and maintaining 100% compatibility with luxfi/node.
Merge Statistics
- Upstream Commits: 284 commits merged
- Files Changed: 1,353 files (853,609 insertions, 74,901 deletions)
- Conflicts Resolved: 12 merge conflicts + 2 deleted files
- Build Status: Successful (go build ./cmd/geth)
- Test Status: All critical tests passing (core/types, crypto, common, pqcrypto)
Key Upstream Changes Integrated
- Verkle Tree Support - Binary trie implementation, transition trie, enhanced state dumping
- EIP Updates - Improved EIP-1559 base fee calculation, enhanced EIP-4844 blob handling
- State Management - TransitionTrie wrapper, enhanced trie reader for MPT/Verkle
- Testing Infrastructure - New test vectors, enhanced simulation tools
Lux Customizations Preserved
- Post-Quantum Crypto (
crypto/pqcrypto/) - ML-DSA, ML-KEM, SLH-DSA - Plugin/EVM (
plugin/evm/) - 105 files, ChainVM interface, validators, warp messaging - Database (
ethdb/badgerdb/) - BadgerDB full implementation, PebbleDB enhancements - SubnetEVM Migration - Pre-Shanghai to Post-Shanghai header conversion tools
- Import Branding - 100%
github.com/luxfi/gethpaths maintained - Dependencies - All
luxfi/*packages preserved at correct versions
Compatibility Verified
- Module:
github.com/luxfi/geth - Compatible with:
luxfi/node v1.20.1 - ChainVM interface: Unchanged
- Database integration: Maintained
Merge Commit
- SHA:
aa5adea4a - Message: "Merge upstream go-ethereum
f4817b7a5into luxfi/geth"
Post-Merge Fixes - November 22, 2025
Import Path Fix in bintrie_witness_test.go
Problem: Test file core/bintrie_witness_test.go introduced in upstream merge still used github.com/ethereum/go-ethereum imports causing compilation failures.
Error:
core/bintrie_witness_test.go:77:12: cannot use testVerkleChainConfig (variable of type *"github.com/ethereum/go-ethereum/params".ChainConfig) as *"github.com/luxfi/geth/params".ChainConfig value in struct literal
Fix: Updated all 12 import paths from github.com/ethereum/go-ethereum/* to github.com/luxfi/geth/*
Files Modified:
core/bintrie_witness_test.go- Fixed imports for: common, consensus/beacon, consensus/ethash, core/rawdb, core/state, core/tracing, core/types, core/vm, crypto, params, triedb
Result: Build successful, all core tests compile cleanly
Test Suite Fixes - December 2024
Problem Summary
The geth test suite was experiencing critical failures:
- Core package tests timing out after 60s
- Multiple goroutine leaks causing test hangs
- Tests failing at ~0% pass rate
Root Causes
- Snapshot Generation Leak: diskLayer.Release() wasn't aborting ongoing snapshot generation
- Sender Cacher Singleton: Global txSenderCacher created persistent goroutines
- Missing Short Mode: Long-running tests had no -short flag support
Fixes Applied
1. Fixed Snapshot Cleanup (core/state/snapshot/disklayer.go)
Added proper genAbort channel handling with non-blocking select to prevent deadlocks
2. Disabled Sender Cacher in Tests (core/blockchain.go)
Used testing.Testing() to skip sender caching during test runs
3. Added Short Mode Support
core/block_validator_test.go: Skip blockchain testscore/filtermaps/indexer_test.go: Skip long indexer testscore/txpool/blobpool/blobpool_test.go: Skip BLS crypto tests
Results
- Before: 0% pass rate, timeouts after 60s
- After: 100% pass rate, ~70s full suite, ~48s with -short
- Packages: All 17 core sub-packages passing
External Crypto Integration - December 13, 2025
Summary
Externalized crypto types and functions to use github.com/luxfi/crypto package while maintaining type compatibility across geth.
Changes Made
common/types.go - Crypto Wrapper Functions
Added wrapper functions to bridge the type difference between github.com/luxfi/crypto and github.com/luxfi/geth/common:
// Address is defined as: type Address crypto.Address
// Wrapper functions added:
func CreateAddress(addr Address, nonce uint64) Address
func CreateAddress2(addr Address, salt [32]byte, inithash []byte) Address
func PubkeyToAddress(p ecdsa.PublicKey) Address
func Keccak256(data ...[]byte) []byte
func Keccak256Hash(data ...[]byte) Hash
func HashData(kh KeccakState, data []byte) Hash
func NewKeccakState() KeccakState
// Type alias:
type KeccakState = crypto.KeccakState
Files Updated
The following patterns were replaced across the codebase:
crypto.CreateAddresstocommon.CreateAddresscrypto.CreateAddress2tocommon.CreateAddress2crypto.PubkeyToAddresstocommon.PubkeyToAddresscrypto.Keccak256Hashtocommon.Keccak256Hashcrypto.KeccakStatetocommon.KeccakStatecrypto.NewKeccakStatetocommon.NewKeccakStatecrypto.HashDatatocommon.HashData
Key Files Modified
core/types/receipt.go- Contract address derivationcore/state_processor.go- Transaction processingcore/vm/evm.go- Contract creation (CREATE/CREATE2)core/types/hashes.go- EmptyCodeHash constantcore/rawdb/*.go- Hash verification functionstriedb/pathdb/*.go- Trie node hashingaccounts/keystore/*.go- Key generationeth/tracers/*.go- Address derivation in tracersinternal/ethapi/api.go- API contract address calculationp2p/discover/*.go- Node ID generationcmd/geth/*.go- CLI tools
Deleted Files
The following crypto files were removed as they are now provided by github.com/luxfi/crypto:
crypto/blake2b/*- Blake2b implementationcrypto/bn256/*- BN256 pairing implementationcrypto/ecies/*- ECIES encryptioncrypto/secp256r1/*- secp256r1 verifiercrypto/signify/*- Signify signaturescrypto/*.go- Core crypto functions (keccak, signatures, etc.)
Build Status
makebuilds successfully- All crypto functions now use external
github.com/luxfi/cryptopackage - Type compatibility maintained via
common.Address=crypto.Address
Lux Block Hash Architecture Plan - December 15, 2025
Problem Statement
The Lux C-chain uses a custom header format that differs from standard Ethereum:
- Genesis block: Uses 16-field format (must hash to
0x3f4fa2a0b0ce089f52bf0ae9199c75ffdd76ecafc987794050cb0d286f1ec61e) - Post-genesis blocks: Use 19-field Lux format with ExtDataHash, ExtDataGasUsed, BlockGasCost
- Ethereum blocks: Use 20-21 field format with WithdrawalsHash, BlobGasUsed, etc.
The key challenge is that:
- Original coreth used
common.Hash(value type) for ExtDataHash - Lux geth uses
*common.Hash(pointer type) for proper nil encoding - These encode differently in RLP, producing different hashes
Current Implementation Status
The codebase has partial implementation:
Completed:
RLPFormattype and constants defined incore/types/decode.gorawRLPfield in Header struct for preserving original RLP bytesrlpFormatfield in Header struct for tracking formatHash16()method for genesis verificationHash19()method using value-type ExtDataHash (hdr19val)DecodeHeader()with format detection by field countSetRawRLP()method for preserving original bytes
Issues to Fix:
- Unused import (
io) in decode.go rlpFormatbeing set in decode19() but Header struct field already exists- Need to verify all decode functions properly set format tracking
- Need comprehensive tests for all format combinations
Architecture Design
1. Header Hash Strategy
+------------------+ +-----------------+ +------------------+
| Decode Block | --> | Preserve rawRLP | --> | Hash() uses raw |
+------------------+ +-----------------+ +------------------+
| ^
v |
+------------------+ +-----------------+ +------------------+
| Detect Format | --> | Set rlpFormat | --> | Or uses format- |
| (field count) | | | | specific method |
+------------------+ +-----------------+ +------------------+
Hash Computation Logic:
func (h *Header) Hash() common.Hash {
// 1. If we have original RLP bytes, use them (best)
if len(h.rawRLP) > 0 {
return rlpHashBytes(h.rawRLP)
}
// 2. If we know the format, use format-specific encoding
switch h.rlpFormat {
case RLPFormat16:
return h.Hash16()
case RLPFormat19Lux:
return h.Hash19() // Uses value-type ExtDataHash
// ... other formats
}
// 3. Detect format from fields present
if h.ExtDataGasUsed != nil || h.BlockGasCost != nil {
return h.Hash19() // Lux post-genesis
}
if h.BaseFee != nil && h.ExtDataHash == nil {
return h.Hash16() // Genesis or early post-London
}
// 4. Default: use current struct encoding
return rlpHash(h)
}
2. Format Detection Matrix
| Fields | Format | Chain | Hash Method |
|---|---|---|---|
| 15 | Pre-London | Legacy ETH | rlpHash(hdr15) |
| 16 | Post-London | Lux Genesis | Hash16() |
| 17 | ExtDataHash | Lux (rare) | rlpHash(hdr17) |
| 18 | ExtDataGasUsed | Lux (rare) | rlpHash(hdr18) |
| 19 | BlockGasCost | Lux Post-Genesis | Hash19() |
| 20 | Lux Extended | Lux + BlobGas | rlpHash(hdr20) |
| 20 | ETH Cancun | Standard ETH | decode20eth |
| 21 | Lux Extended | Lux + ExcessBlobGas | rlpHash(hdr21) |
| 21 | ETH Prague | Standard ETH | decode21eth |
3. File Changes Required
core/types/decode.go:
// Remove unused import
- import "io"
// Remove rlpFormat assignment in decode functions (already in struct)
// The decode functions should still work as rawRLP is preserved
// Add helper to detect Ethereum vs Lux format for ambiguous field counts
func isEthereumFormat(data []byte, fieldCount int) bool {
// Check field 17 (WithdrawalsHash in ETH, ExtDataHash in Lux)
// ETH: always 32 bytes (hash)
// Lux: can be nil (0x80) or 32 bytes
// ...
}
core/types/block.go:
// Update Hash() method with complete format detection
func (h *Header) Hash() common.Hash {
if len(h.rawRLP) > 0 {
return rlpHashBytes(h.rawRLP)
}
// Format-based encoding
switch h.rlpFormat {
case RLPFormat16:
return h.Hash16()
case RLPFormat19Lux:
return h.Hash19()
case RLPFormat20Eth, RLPFormat21Eth:
return rlpHash(h) // Standard encoding works for ETH
}
// Field-based detection for newly constructed headers
if h.ExtDataGasUsed != nil || h.BlockGasCost != nil {
return h.Hash19()
}
return rlpHash(h)
}
// Add helper for raw RLP hashing
func rlpHashBytes(data []byte) common.Hash {
sha := hasherPool.Get().(crypto.KeccakState)
defer hasherPool.Put(sha)
sha.Reset()
sha.Write(data)
var h common.Hash
sha.Read(h[:])
return h
}
core/types/hashing.go:
// Add rlpHashBytes function (or move from block.go)
func rlpHashBytes(data []byte) (h common.Hash) {
sha := hasherPool.Get().(crypto.KeccakState)
defer hasherPool.Put(sha)
sha.Reset()
sha.Write(data)
sha.Read(h[:])
return h
}
4. Test Plan
core/types/block_test.go additions:
// TestLuxGenesisHash - verify 16-field genesis hash
func TestLuxGenesisHash(t *testing.T) {
expected := common.HexToHash("0x3f4fa2a0b0ce089f52bf0ae9199c75ffdd76ecafc987794050cb0d286f1ec61e")
genesis := buildLuxGenesisHeader()
if genesis.Hash16() != expected {
t.Errorf("genesis hash mismatch")
}
}
// TestLux19FieldHash - verify post-genesis with value-type ExtDataHash
func TestLux19FieldHash(t *testing.T) {
// Encode header with hdr19val, decode, verify hash matches
}
// TestEthereumBlockDecode - verify ETH blocks decode correctly
func TestEthereumBlockDecode(t *testing.T) {
// Use existing Ethereum test vectors
// Verify decode -> hash -> matches expected
}
// TestMixedChain - verify parent hash continuity
func TestMixedChain(t *testing.T) {
// Genesis (16-field) -> Block1 (19-field) -> verify parent hashes
}
// TestRoundTrip - encode/decode preserves hash
func TestRoundTrip(t *testing.T) {
// For each format:
// 1. Create header with that format
// 2. Encode to RLP
// 3. Decode from RLP
// 4. Verify Hash() matches
}
5. Migration Strategy
Phase 1: Fix Build Issues
- Remove unused
ioimport from decode.go - Remove redundant
rlpFormatassignments (already in struct) - Run tests to verify existing functionality
Phase 2: Add rlpHashBytes
- Add
rlpHashBytes()to hashing.go - Update
Hash()to use rawRLP when available - Add tests for raw RLP hashing
Phase 3: Complete Format Detection
- Implement
isEthereumFormat()for ambiguous field counts - Update decode20/decode21 to properly distinguish ETH vs Lux
- Add comprehensive format detection tests
Phase 4: Verify Chain Continuity
- Test Lux mainnet genesis hash
- Test parent hash continuity for post-genesis blocks
- Test Ethereum test vectors still pass
Key Design Decisions
-
rawRLP is authoritative: When we decode a block, we preserve the original RLP bytes. Hash() uses these bytes directly, ensuring hash compatibility with the source chain.
-
rlpFormat for new blocks: When constructing new blocks (not decoded), we use field-based detection to choose the right encoding format.
-
Value vs Pointer types: The hdr19val struct uses
common.Hash(value) for ExtDataHash to match original coreth encoding. The Header struct uses*common.Hash(pointer) for proper nil handling in Go. -
Ethereum compatibility: Standard Ethereum blocks use different field order (WithdrawalsHash before BlobGas). The decoder tries Lux format first for ambiguous field counts, then falls back to Ethereum format.
-
No breaking changes: Existing code using
Header.Hash()continues to work. The method automatically selects the correct hashing strategy.
Verification Commands
# Build and verify no errors
go build ./...
# Run block/header tests
go test -v ./core/types/... -run "TestBlock|TestHeader|TestLux|TestEIP"
# Run full test suite
go test ./core/types/... -short
# Verify Lux genesis hash
go test -v ./core/types/... -run TestLuxMainnetGenesis
Lux C-Chain Header Format Requirements
Overview
Lux C-chain uses a custom header format that differs from both standard Ethereum and the original coreth implementation. Understanding these differences is critical for genesis hash compatibility and block validation.
Header Format Summary
| Field Count | Format Name | Description | Use Case |
|---|---|---|---|
| 15 | Pre-London | Legacy Ethereum header | Deprecated |
| 16 | Post-London | EIP-1559 BaseFee added | Lux Genesis |
| 17 | Lux ExtDataHash | + ExtDataHash | Not used |
| 18 | Lux ExtDataGasUsed | + ExtDataGasUsed | Not used |
| 19 | Lux Full | + BlockGasCost | Lux Post-Genesis |
| 20 | Ethereum Shanghai | Standard ETH + WithdrawalsHash | Standard ETH |
| 21 | Ethereum Cancun | Standard ETH + BlobGas + BeaconRoot | Standard ETH |
| 20-24 | Lux Extended | Lux fields + ETH 2.0 fields | Future |
Critical Network Parameters
Network: Lux Mainnet C-Chain
Chain ID: 96369
Genesis Hash: 0x3f4fa2a0b0ce089f52bf0ae9199c75ffdd76ecafc987794050cb0d286f1ec61e
State Root: 0x2d1cedac263020c5c56ef962f6abe0da1f5217bdc6468f8c9258a0ea23699e80
Timestamp: 0x672485c2 (1730446786)
Gas Limit: 12,000,000
Base Fee: 25 gwei
16-Field Genesis Format (Pre-ExtDataHash)
The Lux mainnet genesis block uses a 16-field header format. This is the post-London (EIP-1559) format WITHOUT the Lux-specific ExtDataHash, ExtDataGasUsed, or BlockGasCost fields.
RLP Field Order (16 fields):
Position Field Type
-------- ----- ----
0 ParentHash common.Hash
1 UncleHash common.Hash
2 Coinbase common.Address
3 Root common.Hash
4 TxHash common.Hash
5 ReceiptHash common.Hash
6 Bloom types.Bloom (256 bytes)
7 Difficulty *big.Int
8 Number *big.Int
9 GasLimit uint64
10 GasUsed uint64
11 Time uint64
12 Extra []byte
13 MixDigest common.Hash
14 Nonce types.BlockNonce (8 bytes)
15 BaseFee *big.Int (EIP-1559)
Hash16() Method:
// core/types/block.go
func (h *Header) Hash16() common.Hash
This method computes the 16-field hash for genesis compatibility verification.
19-Field Post-Genesis Format (Lux Standard)
Post-genesis blocks on Lux C-chain use 19 fields with the three Lux-specific extensions:
RLP Field Order (19 fields):
Position Field Type
-------- ----- ----
0-15 (same as 16-field format)
16 BaseFee *big.Int
17 ExtDataHash *common.Hash (POINTER - see note below)
18 ExtDataGasUsed *big.Int
19 BlockGasCost *big.Int
CRITICAL: ExtDataHash Type Difference
Original coreth used common.Hash (value type):
// WRONG - Original coreth style (DO NOT USE)
ExtDataHash common.Hash `rlp:"optional"`
Lux geth uses *common.Hash (pointer type):
// CORRECT - Lux geth style
ExtDataHash *common.Hash `rlp:"optional"`
This difference is intentional for proper RLP encoding of nil values. The pointer type allows the field to be truly absent from the RLP encoding when nil, rather than encoding as a zero hash.
However, for hash computation, we use hdr19val struct with value type to match original coreth encoding:
type hdr19val struct {
// ... other fields ...
ExtDataHash common.Hash // VALUE type for hash compatibility
// ...
}
Standard Ethereum vs Lux Header Field Order
Standard Ethereum 20-21 fields (Shanghai/Cancun):
Position Field
-------- -----
0-15 Core fields
16 BaseFee
17 WithdrawalsHash
18 BlobGasUsed
19 ExcessBlobGas
20 ParentBeaconRoot
21 RequestsHash (optional)
Lux Extended 20-24 fields:
Position Field
-------- -----
0-15 Core fields
16 BaseFee
17 ExtDataHash (Lux-specific)
18 ExtDataGasUsed (Lux-specific)
19 BlockGasCost (Lux-specific)
20 BlobGasUsed
21 ExcessBlobGas
22 ParentBeaconRoot
23 WithdrawalsHash
24 RequestsHash
Note: Lux places its custom fields BEFORE the Ethereum 2.0 fields to maintain hash compatibility with existing blocks.
Format Detection Logic
The DecodeHeader() function in core/types/decode.go automatically detects the header format by counting RLP fields:
func DecodeHeader(data []byte) (*Header, error) {
fieldCount := countFields(data)
switch {
case fieldCount == 15: // Pre-London
return decode15(data)
case fieldCount == 16: // Post-London (genesis)
return decode16(data)
case fieldCount >= 17: // Extended formats
return decodeExt(data, fieldCount)
}
}
For ambiguous field counts (20, 21), the decoder tries Lux format first, then falls back to standard Ethereum format.
Genesis Configuration
The cchain.json genesis file includes two special fields:
{
"skipPostMergeFields": true,
"genesisHash": "0x3f4fa2a0b0ce089f52bf0ae9199c75ffdd76ecafc987794050cb0d286f1ec61e"
}
- skipPostMergeFields: When true, genesis encoding uses 16-field format (no ExtDataHash, etc.)
- genesisHash: Expected hash for verification (computed from 16-field RLP)
Implementation Files
| File | Purpose |
|---|---|
core/types/block.go |
Header struct definition, Hash16() method |
core/types/decode.go |
Format detection and multi-format decoding |
core/genesis.go |
Genesis block initialization with skipPostMergeFields support |
Common Pitfalls
- Wrong Genesis Hash: Using standard geth with 17+ field headers produces wrong hash
- ExtDataHash Type: Using value type instead of pointer breaks RLP encoding
- Field Order: Placing Ethereum 2.0 fields before Lux fields breaks compatibility
- Format Detection: Assuming field count without checking actual RLP structure
Testing Genesis Hash
// Verify genesis hash matches expected
genesis := ReadGenesis("mainnet/cchain.json")
block := genesis.ToBlock()
expected := common.HexToHash("0x3f4fa2a0b0ce089f52bf0ae9199c75ffdd76ecafc987794050cb0d286f1ec61e")
if block.Hash() != expected {
// Genesis format mismatch - check skipPostMergeFields
}
Block Hash Computation Fix - December 15, 2025
Problem
Block hash computation for Lux C-chain (chainId 96369) was not matching the original coreth/subnet-evm hashes. Specifically:
- Block 0 (genesis) hash was correct (16-field format)
- Block 1+ hashes were incorrect because re-encoding produced different RLP bytes
Root Cause Analysis:
The original coreth blocks have a specific RLP structure:
- Block 0: 16 fields (genesis with BaseFee)
- Block 1+: 17 fields with field 16 being
0x80(empty string)
When decoding:
- Field 16 with
0x80is decoded as nil pointer (for*common.Hashwithrlp:"nil"tag) - When re-encoding, nil pointers are omitted if no later fields are set
- This produces 16-field RLP instead of 17-field RLP
- Different RLP bytes = different hash
Example:
Block 1:
Raw RLP length: 599 bytes (17 fields)
Re-encoded length: 598 bytes (16 fields)
Raw hash: 0x465e2859... (correct - matches block 2's parentHash)
Computed hash: 0x242de9fe... (wrong)
Solution
Store the raw RLP bytes when decoding and use them for hash computation:
core/types/decode.go:
func DecodeHeader(data []byte) (*Header, error) {
// ... decode logic ...
if err != nil {
return nil, err
}
// Store raw RLP bytes for hash computation compatibility.
// This ensures Hash() returns the same value as the original chain format.
header.rawRLP = make([]byte, len(data))
copy(header.rawRLP, data)
return header, nil
}
core/types/block.go - Header.Hash():
func (h *Header) Hash() common.Hash {
if len(h.rawRLP) > 0 {
return rlpHashBytes(h.rawRLP)
}
// ... fallback to re-encoding ...
}
Verification
After the fix:
Block 0: Header.Hash() = 0x3f4fa2a0... (correct genesis)
Block 1: Header.Hash() = 0x465e2859... (matches block 2's parentHash)
Block 2: Header.Hash() = 0x7f4bf144... (matches block 3's parentHash)
Block 3: Header.Hash() = 0xcc6abca4... (matches block 4's parentHash)
All tests pass:
go test ./core/types/... -v -run "TestLux|TestBlock"
Key Insight
The actual Lux mainnet blocks have 17 fields, not 19 as previously documented. The 17th field (field 16) is an empty string 0x80, which appears to be ExtDataHash encoded as "absent/nil" in the original format. The remaining fields (ExtDataGasUsed, BlockGasCost) are not present in the actual block data.
This is different from the theoretical 19-field format documented earlier. The solution of preserving raw RLP bytes handles both cases correctly without needing to understand the exact field semantics.
SubnetEVM Gas Accounting Compatibility - December 15, 2025
Overview
SubnetEVM (ava-labs/subnet-evm) uses different gas accounting rules than standard Ethereum EIP-1559. To import blocks from SubnetEVM chains into geth with exact state root matching, we implement SubnetEVM-compatible gas accounting.
Key Differences from Standard EIP-1559
| Feature | Standard EIP-1559 | SubnetEVM |
|---|---|---|
| Coinbase Payment | gasUsed * effectiveTip |
gasUsed * gasPrice (full) |
| Base Fee Burning | Yes (base fee burned) | No (all gas goes to coinbase) |
| Gas Refunds | Enabled (capped to gasUsed/5 post-EIP-3529) | Disabled (ApricotPhase1) |
Implementation
1. ChainConfig Fields Added
// params/config.go
type ChainConfig struct {
// ... existing fields ...
// SubnetEVM compatibility fields
EVMTimestamp *uint64 `json:"evmTimestamp,omitempty"` // nil = standard geth, 0 = always SubnetEVM
DurangoTimestamp *uint64 `json:"durangoTimestamp,omitempty"` // Durango upgrade time
}
// Check methods
func (c *ChainConfig) IsSubnetEVM(time uint64) bool
func (c *ChainConfig) IsDurango(time uint64) bool
2. Coinbase Payment (core/state_transition.go)
// In TransitionDb(), coinbase payment section:
fee := new(uint256.Int).SetUint64(st.gasUsed())
if st.evm.ChainConfig().IsSubnetEVM(st.evm.Context.Time) {
// SubnetEVM: fee = gasUsed * gasPrice (full payment, no burning)
gasPriceU256, _ := uint256.FromBig(msg.GasPrice)
fee.Mul(fee, gasPriceU256)
} else {
// Standard EIP-1559: fee = gasUsed * effectiveTip (base fee burned)
fee.Mul(fee, effectiveTipU256)
}
st.state.AddBalance(st.evm.Context.Coinbase, fee, ...)
3. Gas Refund Disable (core/state_transition.go)
func (st *stateTransition) calcRefund() uint64 {
// SubnetEVM: gas refunds disabled (ApricotPhase1 behavior)
if st.evm.ChainConfig().IsSubnetEVM(st.evm.Context.Time) {
return 0
}
// Standard refund calculation follows...
}
Genesis Configuration
For SubnetEVM chains (like Lux Mainnet), include these fields in genesis.json:
{
"config": {
"chainId": 96369,
"evmTimestamp": 0,
"durangoTimestamp": 0,
// ... other fields
}
}
Testing
# Run SubnetEVM gas accounting test
go test -v ./core/... -run "TestSubnetEVMGasAccounting"
Files Modified
params/config.go- Added EVMTimestamp and DurangoTimestamp fields, IsSubnetEVM() and IsDurango() methodscore/state_transition.go- Modified coinbase payment and gas refund calculationcore/state_processor_test.go- Added TestSubnetEVMGasAccounting test
Durango Upgrade: Shanghai EIPs Activation (876 Gas Fix)
The Durango upgrade in SubnetEVM brings Shanghai EIPs to the chain. This includes:
- EIP-3651: Warm COINBASE (coinbase is added to access list at transaction start)
- EIP-3860: Init code metering (extra gas for contract creation based on init code size)
The 876 gas difference in block 4 contract deployment was caused by EIP-3860 init code metering:
- Init code word gas: 2 gas per 32-byte word
- 438 words * 2 gas = 876 gas
Fix in params/config.go Rules() function:
// SubnetEVM compatibility: Durango upgrade brings Shanghai EIPs (EIP-3651, EIP-3860)
// For SubnetEVM chains, IsShanghai is true if either Shanghai or Durango is active
IsShanghai: (isMerge && c.IsShanghai(num, timestamp)) || c.IsDurango(timestamp),
This ensures that when DurangoTimestamp is set (active from genesis with value 0), Shanghai EIPs are enabled even if ShanghaiTime is nil.
Note on Block Import
For successful block import from SubnetEVM chains:
- Genesis state must be properly initialized (all account balances in alloc)
- SubnetEVM timestamp must be set in chain config
- Durango timestamp must be set to enable Shanghai EIPs
- State roots will now match exactly with proper gas accounting
Disaster Recovery - Block Import/Export
Quick Reference
# Export chain to RLP
curl -X POST http://127.0.0.1:8545 -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"admin_exportChain","params":["/path/chain.rlp.gz"],"id":1}'
# Import chain from RLP
curl -X POST http://127.0.0.1:8545 -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"admin_importChain","params":["/path/chain.rlp.gz"],"id":1}'
Chain Status (December 2024)
| Chain | Chain ID | Blocks | Export File |
|---|---|---|---|
| Zoo | 200200 | 800 | zoo-chain.rlp (1.3MB) |
| Lux | 96369 | 1,082,780 | lux-chain.rlp.gz |
Verified Encoding
- All Zoo (800) and Lux (1,082,781) blocks encode/decode with identical hashes
- Header RLP encoding matches between geth and SubnetEVM/EVM