fix(plugin/evm): force HashScheme default to stop fresh L2s wedging on pathdb

On lux-mainnet 2026-06-02 every newly-created L2 EVM chain (hanzo, zoo,
spc, pars) panicked at chain create with:

  panic in eth.New: unable to commit genesis block to statedb:
    triedb parent [0x56e81f17171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421] layer missing

Root cause: the operator does not specify state-scheme in chain config, so
vm.ethConfig.StateScheme = "" enters eth.New. backend.go then calls
customrawdb.ParseStateSchemeExt("", chainDb) which delegates to
geth/rawdb.ParseStateScheme — upstream returns PathScheme as the implicit
default for any database with no stored scheme (every fresh L2). That
silently re-enables path mode even though vm.go's preflight at the top
of Initialize hard-refuses it. Genesis commit then runs through pathdb's
Update with parentRoot=EmptyRootHash and dies the moment the layer tree
is consulted.

Fix: extract the scheme normalisation into resolveStateScheme() and lock
the empty-string default to HashScheme here, before backend.go can inherit
geth's path-by-default. Path explicit is still refused as before, so this
is purely an operator-default fix.

Adds:
- plugin/evm/vm_statescheme_test.go: TestResolveStateScheme covering all
  four input cases (empty→hash, hash passthrough, path refused, custom
  passthrough).
- core/genesis_pathdb_test.go: TestGenesisCommit_PathDB_{FreshDB,
  SetupGenesisBlock_FreshDB} document the upstream pathdb genesis-commit
  invariant on a fresh in-memory DB so future drift here is caught
  immediately.

Also bumps compatibility.json to register v0.18.4-v0.18.18 with the
existing RPC chain VM protocol (42); restores TestCompatibility (which
was failing on main since v0.18.4 because the table was never updated).

Version: v0.18.17 -> v0.18.18.
This commit is contained in:
2026-06-02 11:53:21 -07:00
parent 6e25b35864
commit 3d0966555f
5 changed files with 235 additions and 24 deletions
+15
View File
@@ -1,5 +1,20 @@
{
"rpcChainVMProtocolVersion": {
"v0.18.18": 42,
"v0.18.17": 42,
"v0.18.16": 42,
"v0.18.15": 42,
"v0.18.14": 42,
"v0.18.13": 42,
"v0.18.12": 42,
"v0.18.11": 42,
"v0.18.10": 42,
"v0.18.9": 42,
"v0.18.8": 42,
"v0.18.7": 42,
"v0.18.6": 42,
"v0.18.5": 42,
"v0.18.4": 42,
"v0.8.13": 42,
"v0.8.12": 42,
"v0.8.11": 42,
+122
View File
@@ -0,0 +1,122 @@
// Copyright (C) 2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package core
import (
"math/big"
"testing"
"github.com/luxfi/evm/params"
"github.com/luxfi/geth/common"
"github.com/luxfi/geth/core/rawdb"
"github.com/luxfi/geth/core/types"
"github.com/luxfi/geth/triedb"
gethpathdb "github.com/luxfi/geth/triedb/pathdb"
)
// TestGenesisCommit_PathDB_FreshDB documents the contract that pathdb must
// satisfy for genesis-commit to work on a brand-new database.
//
// In production on lux-mainnet 2026-06-02, every newly-created L2 EVM chain
// (hanzo, zoo, spc, pars) panicked at chain create with:
//
// panic in eth.New: unable to commit genesis block to statedb:
// triedb parent [0x56e81f17…] layer missing
//
// 0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421 is
// types.EmptyRootHash — the parentRoot that core.Genesis.toBlock + statedb.Commit
// always supply when committing the very first state on a fresh DB.
//
// Contract: a fresh pathdb-backed triedb.Database must have EmptyRootHash
// registered as a disk layer in its layer tree, so that statedb.Commit(0, …)
// can call db.Update(newRoot, parentRoot=EmptyRootHash, …) without
// layertree.add returning "triedb parent … layer missing".
//
// This test asserts the in-memory pathdb path works. The production wedge
// was caused by a separate problem: the EVM plugin lets eth/backend.go
// pick pathdb as the implicit default for empty DBs (because the upstream
// geth rawdb.ParseStateScheme returns "path" when neither scheme nor stored
// state is present) while simultaneously refusing path mode at vm.go's
// preflight check. The combined effect is that path-mode commit runs
// without ever installing the EmptyRootHash disk layer the way that
// production BadgerDB triggers. See plugin/evm/vm.go's StateScheme
// defaulting block for the fix.
//
// If this test ever regresses, every fresh L1/L2 EVM chain fails at boot.
func TestGenesisCommit_PathDB_FreshDB(t *testing.T) {
db := rawdb.NewMemoryDatabase()
tdb := triedb.NewDatabase(db, &triedb.Config{
PathDB: gethpathdb.Defaults,
})
defer tdb.Close()
g := &Genesis{
Config: params.TestChainConfig,
Alloc: types.GenesisAlloc{
common.HexToAddress("0x1000000000000000000000000000000000000001"): {
Balance: big.NewInt(1),
},
},
GasLimit: params.GetExtra(params.TestChainConfig).FeeConfig.GasLimit.Uint64(),
Difficulty: big.NewInt(1),
}
defer func() {
if r := recover(); r != nil {
t.Fatalf("genesis.Commit panicked on fresh pathdb DB (regression): %v\n"+
"This is the lux-mainnet 2026-06-02 chain-creation failure mode.\n"+
"Verify geth/triedb/pathdb.merkleNodeHasher returns types.EmptyRootHash\n"+
"when ReadAccountTrieNode is empty, AND that newLayerTree(loadLayers())\n"+
"installs the disk layer at EmptyRootHash before SetupGenesisBlock runs.",
r)
}
}()
block, err := g.Commit(db, tdb)
if err != nil {
t.Fatalf("genesis.Commit returned error on fresh pathdb DB: %v", err)
}
if block == nil {
t.Fatal("genesis.Commit returned nil block")
}
if block.NumberU64() != 0 {
t.Fatalf("genesis block number = %d, want 0", block.NumberU64())
}
}
// TestGenesisCommit_PathDB_SetupGenesisBlock_FreshDB exercises the full
// SetupGenesisBlock path that NewBlockChain uses, with pathdb. This is the
// exact call sequence executed by eth.New for a newly-created L2 EVM chain.
func TestGenesisCommit_PathDB_SetupGenesisBlock_FreshDB(t *testing.T) {
db := rawdb.NewMemoryDatabase()
tdb := triedb.NewDatabase(db, &triedb.Config{
PathDB: gethpathdb.Defaults,
})
defer tdb.Close()
g := &Genesis{
Config: params.TestChainConfig,
Alloc: types.GenesisAlloc{
common.HexToAddress("0x1000000000000000000000000000000000000001"): {
Balance: big.NewInt(1),
},
},
GasLimit: params.GetExtra(params.TestChainConfig).FeeConfig.GasLimit.Uint64(),
Difficulty: big.NewInt(1),
}
defer func() {
if r := recover(); r != nil {
t.Fatalf("SetupGenesisBlock panicked on fresh pathdb DB (regression): %v", r)
}
}()
_, hash, err := SetupGenesisBlock(db, tdb, g, common.Hash{}, false)
if err != nil {
t.Fatalf("SetupGenesisBlock returned error on fresh pathdb DB: %v", err)
}
if (hash == common.Hash{}) {
t.Fatal("SetupGenesisBlock returned empty hash")
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ var (
// GitCommit is set by the build script
GitCommit string
// Version is the version of Lux EVM (set by build script via ldflags)
Version string = "v0.18.17"
Version string = "v0.18.18"
)
func init() {
+34 -23
View File
@@ -166,6 +166,36 @@ func debugLog(msg string, args ...interface{}) {
log.Debug(fmt.Sprintf(msg, args...))
}
// resolveStateScheme normalises the operator-supplied state scheme into the
// concrete scheme this VM will run with, refusing schemes it does not support.
//
// History (lux-mainnet 2026-06-02): when the operator does not pick a scheme,
// geth's rawdb.ParseStateScheme (called via customrawdb.ParseStateSchemeExt
// from eth/backend.go) silently returns "path" for any database with no
// previously-stored scheme — which is the case for every freshly-created L2
// EVM chain. That collided with this VM's hard refusal of path mode, but only
// AFTER chain creation had already plumbed pathdb into the genesis-commit
// path. The chain then panicked with:
//
// panic in eth.New: unable to commit genesis block to statedb:
// triedb parent [0x56e81f17…] layer missing
//
// taking down hanzo/zoo/spc/pars L2s. The fix is to lock the default to
// HashScheme HERE — before backend.go can inherit the upstream "path"
// default — so the VM-level refusal is enforced and chain creation succeeds.
func resolveStateScheme(provided string) (string, error) {
switch provided {
case rawdb.PathScheme:
return "", errors.New("Path state scheme is not supported. Please use HashDB state scheme instead")
case "":
// No scheme requested → use HashScheme. Do not rely on geth's empty-DB
// default, which is path-by-default and breaks fresh L2 chain creation.
return rawdb.HashScheme, nil
default:
return provided, nil
}
}
// legacyApiNames maps pre geth v1.10.20 api names to their updated counterparts.
// used in attachEthService for backward configuration compatibility.
var legacyApiNames = map[string]string{
@@ -449,30 +479,11 @@ func (vm *VM) Initialize(ctx context.Context, init block.Init) error {
vm.ethConfig.StateHistory = vm.config.StateHistory
vm.ethConfig.TransactionHistory = vm.config.TransactionHistory
vm.ethConfig.SkipTxIndexing = vm.config.SkipTxIndexing
vm.ethConfig.StateScheme = vm.config.StateScheme
// if vm.ethConfig.StateScheme == customrawdb.FirewoodScheme {
// log.Warn("Firewood state scheme is enabled")
// log.Warn("This is untested in production, use at your own risk")
// // Firewood only supports pruning for now.
// if !vm.config.Pruning {
// return errors.New("Pruning must be enabled for Firewood")
// }
// // Firewood does not support iterators, so the snapshot cannot be constructed
// if vm.config.SnapshotCache > 0 {
// return errors.New("Snapshot cache must be disabled for Firewood")
// }
// if vm.config.OfflinePruning {
// return errors.New("Offline pruning is not supported for Firewood")
// }
// if vm.config.StateSyncEnabled {
// return errors.New("State sync is not yet supported for Firewood")
// }
// }
if vm.ethConfig.StateScheme == rawdb.PathScheme {
log.Error("Path state scheme is not supported. Please use HashDB state scheme instead")
return errors.New("Path state scheme is not supported")
resolvedScheme, err := resolveStateScheme(vm.config.StateScheme)
if err != nil {
return err
}
vm.ethConfig.StateScheme = resolvedScheme
// Create directory for offline pruning
if len(vm.ethConfig.OfflinePruningDataDirectory) != 0 {
+63
View File
@@ -0,0 +1,63 @@
// Copyright (C) 2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package evm
import (
"testing"
"github.com/luxfi/geth/core/rawdb"
)
// TestResolveStateScheme captures the contract that fixed the lux-mainnet
// 2026-06-02 chain-creation wedge.
//
// Without this normalisation, an L2 EVM chain with no operator-supplied
// StateScheme would let eth/backend.go's customrawdb.ParseStateSchemeExt
// inherit geth's "path by default for empty DB" behavior, and the VM would
// panic in eth.New because it does not support path mode.
func TestResolveStateScheme(t *testing.T) {
for _, tc := range []struct {
name string
provided string
want string
wantErr bool
}{
{
name: "empty defaults to hash (the fresh-L2 case)",
provided: "",
want: rawdb.HashScheme,
},
{
name: "hash passes through",
provided: rawdb.HashScheme,
want: rawdb.HashScheme,
},
{
name: "path is refused",
provided: rawdb.PathScheme,
wantErr: true,
},
{
name: "unknown passes through (let downstream decide)",
provided: "firewood",
want: "firewood",
},
} {
t.Run(tc.name, func(t *testing.T) {
got, err := resolveStateScheme(tc.provided)
if tc.wantErr {
if err == nil {
t.Fatalf("resolveStateScheme(%q) = %q, nil; want error", tc.provided, got)
}
return
}
if err != nil {
t.Fatalf("resolveStateScheme(%q) returned unexpected error: %v", tc.provided, err)
}
if got != tc.want {
t.Fatalf("resolveStateScheme(%q) = %q, want %q", tc.provided, got, tc.want)
}
})
}
}