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:
2026-05-09 13:03:18 -07:00
parent 0a3ee38673
commit 4fe6fa1c2a
2 changed files with 104 additions and 1 deletions
+24 -1
View File
@@ -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)
+80
View File
@@ -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())
}
}