mirror of
https://github.com/luxfi/evm.git
synced 2026-09-24 00:27:44 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user