mirror of
https://github.com/luxfi/evm.git
synced 2026-09-24 00:27:44 +00:00
fix(pathdb): seed disk layer at EmptyRootHash on fresh DB
loadLayers() previously computed Keccak256Hash(nil) as the disk layer root for an unwritten DB, producing 0xc5d2…a470 — the keccak of zero bytes, NOT the canonical empty-trie root (0x56e81f…b421) that core/Genesis.toBlock uses as parentRoot when committing genesis. Consequence: layertree.add(root, parentRoot=EmptyRootHash) couldn't find a parent in the layer tree (only Keccak256Hash(nil) was there) and panicked with "triedb parent layer missing". Every fresh-launched chain on this fork (lqd primary network's C-Chain, every Liquidity local stack since the path-DB switch) failed to commit its genesis block, leaving block 0's stateRoot pointing at an empty trie no matter what `alloc` the genesis specified. Symptom: eth_getBalance "latest" returned the genesis-allocation values from in-memory cache, but every block-build saw "address has 0" and looped on BAD BLOCK. Fix: when ReadAccountTrieNode returns nil (no persistent state yet), short-circuit `root` to types.EmptyRootHash. Matches go-ethereum upstream and the contract that Genesis.toBlock + layertree.add rely on. Adds journal_freshdb_test.go with two regression tests: - the disk layer's root MUST be EmptyRootHash on a fresh DB - Update() onto EmptyRootHash parent succeeds (the genesis path) Without this fix, any future refactor that re-introduces an unconditional Keccak256Hash(nil) silently breaks fresh chains again.
This commit is contained in:
@@ -124,10 +124,33 @@ func (db *Database) loadJournal(diskRoot common.Hash) (layer, error) {
|
||||
}
|
||||
|
||||
// loadLayers loads a pre-existing state layer backed by a key-value store.
|
||||
//
|
||||
// Fresh-DB invariant: when no persistent account-trie node exists yet
|
||||
// (`ReadAccountTrieNode` returns nil), the disk layer's root MUST be
|
||||
// `types.EmptyRootHash` — the canonical RLP-of-empty-trie hash that
|
||||
// `core.Genesis.toBlock` uses as the parent when committing genesis.
|
||||
//
|
||||
// The previous implementation computed `Keccak256Hash(nil)` for an
|
||||
// empty DB, producing `0xc5d2…a470` which is the keccak of zero bytes,
|
||||
// NOT the empty-trie root (`0x56e81f…b421`). Genesis-commit then looked
|
||||
// up `EmptyRootHash` as the parent in the layer tree, found nothing,
|
||||
// and panicked with "triedb parent layer missing". That single hash
|
||||
// mismatch broke every fresh-launched chain on this fork.
|
||||
//
|
||||
// Fix: short-circuit to `EmptyRootHash` when there is no persistent
|
||||
// state, matching go-ethereum upstream and the contract that
|
||||
// `genesis.toBlock` + `layertree.add` rely on.
|
||||
func (db *Database) loadLayers() layer {
|
||||
// Retrieve the root node of persistent state.
|
||||
data := rawdb.ReadAccountTrieNode(db.diskdb, nil)
|
||||
root := types.TrieRootHash(crypto.Keccak256Hash(data))
|
||||
var root common.Hash
|
||||
if len(data) == 0 {
|
||||
// Fresh DB — no persistent state yet. The empty-trie root is the
|
||||
// only value `core.Genesis.toBlock` will accept as a parent.
|
||||
root = types.EmptyRootHash
|
||||
} else {
|
||||
root = types.TrieRootHash(crypto.Keccak256Hash(data))
|
||||
}
|
||||
|
||||
// Load the layers by resolving the journal
|
||||
head, err := db.loadJournal(root)
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright (C) 2026, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package pathdb
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/geth/common"
|
||||
"github.com/luxfi/geth/core/rawdb"
|
||||
"github.com/luxfi/geth/core/types"
|
||||
"github.com/luxfi/geth/trie/trienode"
|
||||
"github.com/luxfi/geth/trie/triestate"
|
||||
)
|
||||
|
||||
// TestLoadLayers_FreshDB_UsesEmptyRootHash asserts the disk layer's root on a
|
||||
// pristine key-value store is types.EmptyRootHash, not Keccak256Hash(nil).
|
||||
//
|
||||
// Why this matters: core.Genesis.toBlock commits genesis state with parentRoot
|
||||
// == EmptyRootHash. layertree.add looks up parentRoot in the layer tree before
|
||||
// inserting the new layer; if the only existing layer is at Keccak256Hash(nil)
|
||||
// (`0xc5d2…a470`), the lookup misses and the commit panics with
|
||||
// "triedb parent layer missing". That single hash mismatch broke every
|
||||
// fresh-launched chain on this fork.
|
||||
//
|
||||
// Regression guard: any future refactor that re-introduces
|
||||
// `crypto.Keccak256Hash(data)` as the unconditional root computation for an
|
||||
// empty disk node MUST update this test in tandem with the layer-tree
|
||||
// initialization, or chains will silently fail to commit genesis again.
|
||||
func TestLoadLayers_FreshDB_UsesEmptyRootHash(t *testing.T) {
|
||||
memdb := rawdb.NewMemoryDatabase()
|
||||
db := New(memdb, nil)
|
||||
defer db.Close()
|
||||
|
||||
head := db.tree.get(types.EmptyRootHash)
|
||||
if head == nil {
|
||||
got := "<no layers in tree>"
|
||||
// Walk the (small) layer set so the failure message tells you what
|
||||
// root the tree DID install — most useful when the regression hits.
|
||||
db.tree.lock.RLock()
|
||||
for k := range db.tree.layers {
|
||||
got = k.Hex()
|
||||
break
|
||||
}
|
||||
db.tree.lock.RUnlock()
|
||||
t.Fatalf("fresh DB disk layer should be at EmptyRootHash (%s); tree has root=%s",
|
||||
types.EmptyRootHash.Hex(), got)
|
||||
}
|
||||
|
||||
if head.rootHash() != types.EmptyRootHash {
|
||||
t.Fatalf("disk layer root mismatch: got %s, want %s",
|
||||
head.rootHash().Hex(), types.EmptyRootHash.Hex())
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoadLayers_FreshDB_AddOnTopOfEmptyRootSucceeds end-to-end check:
|
||||
// simulate the sequence the genesis-commit path takes on a fresh DB —
|
||||
// add a new layer with parentRoot = EmptyRootHash. Before the fix this
|
||||
// returned "triedb parent layer missing"; with the fix it succeeds.
|
||||
func TestLoadLayers_FreshDB_AddOnTopOfEmptyRootSucceeds(t *testing.T) {
|
||||
memdb := rawdb.NewMemoryDatabase()
|
||||
db := New(memdb, nil)
|
||||
defer db.Close()
|
||||
|
||||
// Construct a single trivial layer pretending to be the genesis state.
|
||||
childRoot := common.HexToHash("0x000000000000000000000000000000000000000000000000000000000000beef")
|
||||
|
||||
// Empty payload — genesis with no allocations is similar in shape
|
||||
// (the alloc map only adds account nodes; for this regression test we
|
||||
// only care that parent lookup against EmptyRootHash succeeds).
|
||||
emptyNodes := trienode.NewMergedNodeSet()
|
||||
emptyStates := triestate.New(nil, nil, nil)
|
||||
|
||||
if err := db.Update(childRoot, types.EmptyRootHash, 0, emptyNodes, emptyStates); err != nil {
|
||||
t.Fatalf("Update onto EmptyRootHash parent failed (the bug): %v", err)
|
||||
}
|
||||
if got := db.tree.get(childRoot); got == nil {
|
||||
t.Fatalf("child layer at %s missing from tree after Update", childRoot.Hex())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user