Skip to content
15 changes: 15 additions & 0 deletions core/state/pruner/pruner.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,21 @@ func NewPruner(db ethdb.Database, datadir, trieCachePath string, bloomSize uint6
if snapDiskRoot == (common.Hash{}) {
return nil, err // No snapshot at all — nothing we can do.
}
// The persisted snapshot root may be a zkStateRoot (Poseidon hash)
// written when an MPT node first tried — and failed — to generate a
// snapshot for a ZK-era block. The snapshot trie walk calls
// trie.New(root, triedb) directly, bypassing the OpenTrie translation
// layer, so it can only succeed with the actual on-disk mptStateRoot.
// Resolve the mapping here so that the snapshot.New call below (and
// the generator it may start) both operate on the correct root.
if mptRoot, err2 := rawdb.ReadDiskStateRoot(db, snapDiskRoot); err2 == nil {
log.Info("Pruner: resolved snapshot ZK root to MPT root",
"zkRoot", snapDiskRoot, "mptRoot", mptRoot)
snapDiskRoot = mptRoot
// Persist the corrected root so that subsequent snapshot.New
// calls (inside loadSnapshot) can locate the disk layer.
rawdb.WriteSnapshotRoot(db, snapDiskRoot)
}
Comment on lines +109 to +113

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ReadDiskStateRoot implementation =="
fd 'accessors_state.go$' --exec sed -n '95,125p' {}

echo
echo "== NewPruner fallback segment =="
fd 'pruner.go$' --exec sed -n '100,135p' {}

echo
echo "== All ReadDiskStateRoot callsites =="
rg -n -C2 '\bReadDiskStateRoot\s*\(' --type=go

Repository: morph-l2/go-ethereum

Length of output: 15914


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Search for ReadTrieNode implementation =="
rg -n 'func\s+ReadTrieNode' --type=go

echo
echo "== Check snapshot.New signature and early validation =="
fd 'snapshot.go$' -path '*/state/snapshot/*' --exec grep -n "func New" {}

echo
echo "== Check if snapshot.New validates root =="
fd 'snapshot.go$' -path '*/state/snapshot/*' --exec sed -n '1,100p' {} | head -60

Repository: morph-l2/go-ethereum

Length of output: 454


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ReadTrieNode implementation =="
sed -n '79,95p' core/rawdb/accessors_state.go

echo
echo "== ReadDiskStateRoot full implementation =="
sed -n '107,115p' core/rawdb/accessors_state.go

echo
echo "== Snapshot.New signature and validation =="
fd 'snapshot.go' -path '*/state/snapshot/*' | head -1 | xargs sed -n '1,150p' | grep -A 20 "^func New"

echo
echo "== Check if snapshot.New validates root early =="
fd 'snapshot.go' -path '*/state/snapshot/*' | head -1 | xargs sed -n '1,300p' | tail -200

Repository: morph-l2/go-ethereum

Length of output: 1379


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Find snapshot.New function =="
rg -n 'func New\(' core/state/snapshot/ --type=go -A 10

echo
echo "== Check pruner fallback and snapshot loading context =="
sed -n '100,145p' core/state/pruner/pruner.go

Repository: morph-l2/go-ethereum

Length of output: 3162


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Check if pruner code validates mptRoot before WriteSnapshotRoot =="
sed -n '110,130p' core/state/pruner/pruner.go

echo
echo "== Check other ReadDiskStateRoot callsites for validation patterns =="
rg -B2 -A5 'WriteSnapshotRoot|snapshot.New' core/state/snapshot/generate.go

echo
echo "== Check if mptRoot validation exists elsewhere in codebase =="
rg -n 'mptRoot.*Hash\{\}' --type=go

echo
echo "== Check snapshot journal handling =="
sed -n '160,180p' core/state/snapshot/journal.go

Repository: morph-l2/go-ethereum

Length of output: 2194


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Check snapshot.New full implementation for root validation =="
sed -n '186,250p' core/state/snapshot/snapshot.go

echo
echo "== Check if snapshot.New or its callees validate root existence =="
rg -n 'func.*loadDiskLayer|func.*load' core/state/snapshot/snapshot.go | head -5

echo
echo "== Check snapshot/generate.go more context around WriteSnapshotRoot =="
sed -n '155,175p' core/state/snapshot/generate.go

echo
echo "== Look for any tests that trigger this pruner path =="
fd 'pruner.*test' --type=f

Repository: morph-l2/go-ethereum

Length of output: 2019


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Find loadSnapshot implementation =="
rg -n 'func loadSnapshot' core/state/snapshot/ --type=go -A 20 | head -40

echo
echo "== Check if loadSnapshot validates root =="
rg -n 'func.*loadDiskLayer' core/state/snapshot/ --type=go -A 15 | head -40

echo
echo "== Summary: Check all places where mptRoot is used after ReadDiskStateRoot =="
rg -B3 -A8 'ReadDiskStateRoot.*err.*== nil' core/state/pruner/pruner.go

Repository: morph-l2/go-ethereum

Length of output: 1779


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Check WriteSnapshotRoot implementation =="
rg -n 'func WriteSnapshotRoot' core/rawdb/ --type=go -A 5

echo
echo "== Check if snapshot.New/loadSnapshot handles bad roots gracefully =="
sed -n '130,170p' core/state/snapshot/journal.go

echo
echo "== Examine what happens after snapshot.New fails in pruner =="
sed -n '125,145p' core/state/pruner/pruner.go

echo
echo "== Check if there's recovery logic for corrupted roots =="
rg -n 'WriteSnapshotRoot' core/state/snapshot/ --type=go -B 3 -A 3

Repository: morph-l2/go-ethereum

Length of output: 7832


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Full context of snapshot/generate.go root handling =="
sed -n '155,180p' core/state/snapshot/generate.go

echo
echo "== Verify if generate.go also has the same unvalidated persist issue =="
sed -n '162,175p' core/state/snapshot/generate.go

echo
echo "== Check if both pruner and generate paths have the same problem =="
echo "Pruner WriteSnapshotRoot location:"
grep -n 'WriteSnapshotRoot' core/state/pruner/pruner.go

echo
echo "Generate WriteSnapshotRoot location:"
grep -n 'WriteSnapshotRoot' core/state/snapshot/generate.go

Repository: morph-l2/go-ethereum

Length of output: 2068


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Confirm the exact line numbers in pruner.go =="
sed -n '107,125p' core/state/pruner/pruner.go | cat -n

echo
echo "== Confirm the exact pattern in generate.go also needs fixing =="
sed -n '157,180p' core/state/snapshot/generate.go | cat -n

Repository: morph-l2/go-ethereum

Length of output: 2543


Validate translated root before persisting to prevent corrupted snapshot state.

At lines 114–120, ReadDiskStateRoot returns the translated root without validating it. If the mapping is malformed or stale, line 120 persists an invalid snapshot root that causes all subsequent snapshot.New calls to fail on restart. The same vulnerability exists in core/state/snapshot/generate.go:162–173.

Add validation to confirm the translated root points to an actual trie node before persisting:

Suggested fix
if mptRoot, err2 := rawdb.ReadDiskStateRoot(db, snapDiskRoot); err2 == nil {
+	if mptRoot == (common.Hash{}) || len(rawdb.ReadTrieNode(db, mptRoot)) == 0 {
+		log.Warn("Pruner: translated MPT root is not usable, keeping persisted snapshot root",
+			"zkRoot", snapDiskRoot, "mptRoot", mptRoot)
+	} else {
		log.Info("Pruner: resolved snapshot ZK root to MPT root",
			"zkRoot", snapDiskRoot, "mptRoot", mptRoot)
		snapDiskRoot = mptRoot
		rawdb.WriteSnapshotRoot(db, snapDiskRoot)
+	}
}

log.Warn("Snapshot journal missing, falling back to snapshot disk-layer root",
"snapDiskRoot", snapDiskRoot, "chainHead", headBlock.Root())
// If the snapshot was mid-generation when the node was killed, New will
Expand Down
17 changes: 17 additions & 0 deletions core/state/snapshot/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,23 @@ func (gs *generatorStats) Log(msg string, root common.Hash, marker []byte) {
// database and head block asynchronously. The snapshot is returned immediately
// and generation is continued in the background until done.
func generateSnapshot(diskdb ethdb.KeyValueStore, triedb *trie.Database, cache int, root common.Hash) *diskLayer {
// For MPT nodes that sync ZK-era blocks the block header carries a
// zkStateRoot (Poseidon hash) while the on-disk trie is keyed by the
// locally-computed mptStateRoot (Keccak256 hash). The snapshot trie
// walk uses trie.New(root, triedb) directly — it does NOT go through
// cachingDB.OpenTrie, so it never sees the ReadDiskStateRoot redirect.
// If we leave root as the zkStateRoot the trie lookup fails immediately
// with "missing trie node", the generator goroutine blocks on genAbort
// forever, and waitBuild() hangs the caller.
//
// Resolve the zkStateRoot → mptStateRoot mapping before doing anything
// else so that both WriteSnapshotRoot and the generator goroutine use
// the correct on-disk root.
if mptRoot, err := rawdb.ReadDiskStateRoot(diskdb, root); err == nil {
log.Info("Snapshot generation: resolved ZK state root to MPT root",
"zkRoot", root, "mptRoot", mptRoot)
root = mptRoot
}
// Create a new disk layer with an initialized state marker at zero
var (
stats = &generatorStats{start: time.Now()}
Expand Down
28 changes: 19 additions & 9 deletions core/state/snapshot/journal.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,17 +159,27 @@ func loadSnapshot(diskdb ethdb.KeyValueStore, triedb *trie.Database, cache int,
// which is below the snapshot. In this case the snapshot can be recovered
// by re-executing blocks but right now it's unavailable.
if head := snapshot.Root(); head != root {
// If it's legacy snapshot, or it's new-format snapshot but
// it's not in recovery mode, returns the error here for
// rebuilding the entire snapshot forcibly.
if !recovery {
// Special case: MPT nodes syncing ZK-era blocks store the snapshot
// under the locally-computed mptStateRoot while the block header
// (and therefore the `root` argument) carries the zkStateRoot.
// If the on-disk snapshot root equals the MPT translation of the
// requested root, the snapshot is perfectly valid — accept it
// without triggering a costly rebuild.
if translated, err := rawdb.ReadDiskStateRoot(diskdb, root); err == nil && head == translated {
log.Info("Snapshot root is MPT translation of block root — accepting",
"blockRoot", root, "mptRoot", head)
} else if !recovery {
// If it's legacy snapshot, or it's new-format snapshot but
// it's not in recovery mode, returns the error here for
// rebuilding the entire snapshot forcibly.
return nil, false, fmt.Errorf("head doesn't match snapshot: have %#x, want %#x", head, root)
} else {
// It's in snapshot recovery, the assumption is held that
// the disk layer is always higher than chain head. It can
// be eventually recovered when the chain head beyonds the
// disk layer.
log.Warn("Snapshot is not continuous with chain", "snaproot", head, "chainroot", root)
}
// It's in snapshot recovery, the assumption is held that
// the disk layer is always higher than chain head. It can
// be eventually recovered when the chain head beyonds the
// disk layer.
log.Warn("Snapshot is not continuous with chain", "snaproot", head, "chainroot", root)
}
// Everything loaded correctly, resume any suspended operations
if !generator.Done {
Expand Down
8 changes: 5 additions & 3 deletions core/state/snapshot/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -730,9 +730,11 @@ func (t *Tree) Rebuild(root common.Hash) {
// Start generating a new snapshot from scratch on a background thread. The
// generator will run a wiper first if there's not one running right now.
log.Info("Rebuilding state snapshot")
t.layers = map[common.Hash]snapshot{
root: generateSnapshot(t.diskdb, t.triedb, t.cache, root),
}
base := generateSnapshot(t.diskdb, t.triedb, t.cache, root)
// generateSnapshot may have translated root (e.g. zkStateRoot → mptStateRoot
// for MPT nodes syncing ZK-era blocks). Use base.root as the map key so that
// all subsequent Snapshot()/Update() lookups find the layer correctly.
t.layers = map[common.Hash]snapshot{base.root: base}
}

// AccountIterator creates a new account iterator for the specified root hash and
Expand Down
278 changes: 278 additions & 0 deletions core/state/snapshot/zk_mpt_compat_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
// Copyright 2024 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

// Tests for ZK-MPT compatibility fixes:
// - generateSnapshot resolves zkStateRoot → mptStateRoot via ReadDiskStateRoot
// - loadSnapshot accepts a snapshot whose disk root is the MPT translation of
// the requested zkStateRoot
// - Rebuild stores the disk layer under base.root (the possibly-translated root)
// as the map key so that Snapshot() lookups work correctly after translation

package snapshot

import (
"math/big"
"testing"
"time"

"github.com/morph-l2/go-ethereum/common"
"github.com/morph-l2/go-ethereum/core/rawdb"
"github.com/morph-l2/go-ethereum/ethdb"
"github.com/morph-l2/go-ethereum/ethdb/memorydb"
"github.com/morph-l2/go-ethereum/rlp"
"github.com/morph-l2/go-ethereum/trie"
)

// buildMPTTrie creates a small MPT account trie in diskdb/triedb and returns its root.
func buildMPTTrie(t *testing.T, diskdb *memorydb.Database) (common.Hash, *trie.Database) {
t.Helper()
triedb := trie.NewDatabase(diskdb)
accTrie, _ := trie.NewSecure(common.Hash{}, triedb)
for _, key := range []string{"acc-1", "acc-2", "acc-3"} {
acc := &Account{
Balance: big.NewInt(1),
Root: emptyRoot.Bytes(),
KeccakCodeHash: emptyKeccakCode.Bytes(),
PoseidonCodeHash: emptyPoseidonCode.Bytes(),
}
val, _ := rlp.EncodeToBytes(acc)
accTrie.Update([]byte(key), val)
}
mptRoot, _, _ := accTrie.Commit(nil)
triedb.Commit(mptRoot, false, nil)
return mptRoot, triedb
Comment on lines +40 to +56

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify trie/RLP APIs used here actually return errors that should be handled.
rg -nP --type go 'func\s+NewSecure\(' -C2
rg -nP --type go 'func\s+\(.*\)\s+Update\([^)]*\)\s+error' -C2
rg -nP --type go 'func\s+\(.*\)\s+Commit\([^)]*\)\s*\([^)]*error' -C2
rg -nP --type go 'func\s+EncodeToBytes\(' -C2

Repository: morph-l2/go-ethereum

Length of output: 4538


🏁 Script executed:

# Find SecureTrie.Update method definition
rg -nP --type go 'func\s+\(\w+\s+\*SecureTrie\)\s+Update' -C3

Repository: morph-l2/go-ethereum

Length of output: 485


🏁 Script executed:

# Find trie.Database.Commit method definition
rg -nP --type go 'func\s+\(\w+\s+\*Database\)\s+Commit' -C3

Repository: morph-l2/go-ethereum

Length of output: 665


🏁 Script executed:

# Read the specific lines of the test file to confirm context
sed -n '40,70p' core/state/snapshot/zk_mpt_compat_test.go

Repository: morph-l2/go-ethereum

Length of output: 1265


Fix error handling in test fixture builders.

The helpers currently discard returned errors from setup operations, which can hide initialization failures and produce misleading test failures.

The accTrie.Update method does not return an error (it logs internally), so error-checking logic should not be added there. The following error checks should be added:

🔧 Proposed fix
 func buildMPTTrie(t *testing.T, diskdb *memorydb.Database) (common.Hash, *trie.Database) {
 	t.Helper()
 	triedb := trie.NewDatabase(diskdb)
-	accTrie, _ := trie.NewSecure(common.Hash{}, triedb)
+	accTrie, err := trie.NewSecure(common.Hash{}, triedb)
+	if err != nil {
+		t.Fatalf("failed to create secure trie: %v", err)
+	}
 	for _, key := range []string{"acc-1", "acc-2", "acc-3"} {
 		acc := &Account{
 			Balance:          big.NewInt(1),
 			Root:             emptyRoot.Bytes(),
 			KeccakCodeHash:   emptyKeccakCode.Bytes(),
 			PoseidonCodeHash: emptyPoseidonCode.Bytes(),
 		}
-		val, _ := rlp.EncodeToBytes(acc)
+		val, err := rlp.EncodeToBytes(acc)
+		if err != nil {
+			t.Fatalf("failed to encode account: %v", err)
+		}
 		accTrie.Update([]byte(key), val)
 	}
-	mptRoot, _, _ := accTrie.Commit(nil)
-	triedb.Commit(mptRoot, false, nil)
+	mptRoot, _, err := accTrie.Commit(nil)
+	if err != nil {
+		t.Fatalf("failed to commit trie: %v", err)
+	}
+	if err := triedb.Commit(mptRoot, false, nil); err != nil {
+		t.Fatalf("failed to commit trie db: %v", err)
+	}
 	return mptRoot, triedb
 }

-func writeDoneGenerator(db ethdb.KeyValueWriter) {
-	blob, _ := rlp.EncodeToBytes(&journalGenerator{Done: true})
+func writeDoneGenerator(t *testing.T, db ethdb.KeyValueWriter) {
+	t.Helper()
+	blob, err := rlp.EncodeToBytes(&journalGenerator{Done: true})
+	if err != nil {
+		t.Fatalf("failed to encode generator journal: %v", err)
+	}
 	rawdb.WriteSnapshotGenerator(db, blob)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/state/snapshot/zk_mpt_compat_test.go` around lines 40 - 56, The test
helper buildMPTTrie currently ignores errors from functions that do return them;
update it to check and fail the test on errors from trie.NewSecure,
rlp.EncodeToBytes, accTrie.Commit and triedb.Commit (use t.Fatalf or t.Fatal
with the wrapped error) while leaving accTrie.Update unchecked since it does not
return an error. Specifically, after calling trie.NewSecure(common.Hash{},
triedb) verify the returned error before proceeding, after
rlp.EncodeToBytes(acc) check its error and abort on failure, check the error
returned by accTrie.Commit(nil) and handle it, and check/handle any error
returned by triedb.Commit(mptRoot, false, nil).

}

// writeDoneGenerator writes a completed (Done=true) snapshot generator record
// into the given db — required by loadAndParseJournal before loadSnapshot
// proceeds to the head-root comparison.
func writeDoneGenerator(db ethdb.KeyValueWriter) {
blob, _ := rlp.EncodeToBytes(&journalGenerator{Done: true})
rawdb.WriteSnapshotGenerator(db, blob)
}

// ---- generate.go: generateSnapshot root translation -------------------------

// TestGenerateSnapshotTranslatesZkRoot verifies that when a DiskStateRoot
// mapping (zkRoot → mptRoot) exists, generateSnapshot uses mptRoot for both
// WriteSnapshotRoot and the trie walk, allowing generation to complete.
func TestGenerateSnapshotTranslatesZkRoot(t *testing.T) {
diskdb := memorydb.New()
mptRoot, triedb := buildMPTTrie(t, diskdb)

// Simulate ZK-era: block header carries zkRoot, local trie is at mptRoot.
zkRoot := common.HexToHash("0x1111111111111111111111111111111111111111111111111111111111111111")
rawdb.WriteDiskStateRoot(diskdb, zkRoot, mptRoot)

snap := generateSnapshot(diskdb, triedb, 16, zkRoot)

// diskLayer.root must be mptRoot, not zkRoot.
if snap.root != mptRoot {
t.Fatalf("diskLayer.root: got %x, want mptRoot %x", snap.root, mptRoot)
}
// SnapshotRoot persisted to DB must be mptRoot.
if stored := rawdb.ReadSnapshotRoot(diskdb); stored != mptRoot {
t.Fatalf("SnapshotRoot in DB: got %x, want mptRoot %x", stored, mptRoot)
}
// Generation must complete successfully.
select {
case <-snap.genPending:
case <-time.After(3 * time.Second):
t.Fatal("snapshot generation timed out — trie walk likely used zkRoot instead of mptRoot")
}
// The generated snapshot data must reproduce mptRoot exactly.
checkSnapRoot(t, snap, mptRoot)

stop := make(chan *generatorStats)
snap.genAbort <- stop
<-stop
}
Comment on lines +80 to +102

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use t.Cleanup for generator shutdown to prevent leaked goroutines on failing paths.

Manual shutdown at the end of each test is skipped if an earlier t.Fatal fires, leaving background generator goroutines alive and making the suite flaky.

🔧 Proposed fix
+func stopGeneratorOnCleanup(t *testing.T, dl *diskLayer) {
+	t.Helper()
+	t.Cleanup(func() {
+		stop := make(chan *generatorStats)
+		dl.genAbort <- stop
+		<-stop
+	})
+}

 func TestGenerateSnapshotTranslatesZkRoot(t *testing.T) {
 	...
 	snap := generateSnapshot(diskdb, triedb, 16, zkRoot)
+	stopGeneratorOnCleanup(t, snap)
 	...
-	stop := make(chan *generatorStats)
-	snap.genAbort <- stop
-	<-stop
 }

 func TestGenerateSnapshotNoTranslation(t *testing.T) {
 	...
 	snap := generateSnapshot(diskdb, triedb, 16, mptRoot)
+	stopGeneratorOnCleanup(t, snap)
 	...
-	stop := make(chan *generatorStats)
-	snap.genAbort <- stop
-	<-stop
 }

 func TestRebuildZkRootMapKeyConsistency(t *testing.T) {
 	...
 	snaps.Rebuild(zkRoot)
+	stopGeneratorOnCleanup(t, snaps.disklayer())
 	...
-	stop := make(chan *generatorStats)
-	snaps.disklayer().genAbort <- stop
-	<-stop
 }

 func TestRebuildWithoutTranslation(t *testing.T) {
 	...
 	snaps.Rebuild(mptRoot)
+	stopGeneratorOnCleanup(t, snaps.disklayer())
 	...
-	stop := make(chan *generatorStats)
-	snaps.disklayer().genAbort <- stop
-	<-stop
 }

Also applies to: 112-130, 201-244, 260-278

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/state/snapshot/zk_mpt_compat_test.go` around lines 80 - 102, The test
currently manually shuts down the snapshot generator with stop := make(chan
*generatorStats); snap.genAbort <- stop; <-stop at the end, which is skipped on
t.Fatal and leaks goroutines; instead register a t.Cleanup handler immediately
after creating snap (or after generateSnapshot) that sends a stop channel into
snap.genAbort and waits for the reply (i.e. t.Cleanup(func(){ stop := make(chan
*generatorStats); snap.genAbort <- stop; <-stop })), and remove the manual
shutdown code at the end of the test; apply the same change to the other
occurrences referenced (lines around the other tests) so the generator is always
stopped even on failures.


// TestGenerateSnapshotNoTranslation verifies that when no DiskStateRoot mapping
// exists (post-Jade-fork MPT blocks where mptRoot IS the block root), root is
// used unchanged and generation still succeeds.
func TestGenerateSnapshotNoTranslation(t *testing.T) {
diskdb := memorydb.New()
mptRoot, triedb := buildMPTTrie(t, diskdb)
// No WriteDiskStateRoot call — no mapping.

snap := generateSnapshot(diskdb, triedb, 16, mptRoot)

if snap.root != mptRoot {
t.Fatalf("diskLayer.root: got %x, want %x", snap.root, mptRoot)
}
if stored := rawdb.ReadSnapshotRoot(diskdb); stored != mptRoot {
t.Fatalf("SnapshotRoot in DB: got %x, want %x", stored, mptRoot)
}
select {
case <-snap.genPending:
case <-time.After(3 * time.Second):
t.Fatal("snapshot generation timed out")
}
checkSnapRoot(t, snap, mptRoot)

stop := make(chan *generatorStats)
snap.genAbort <- stop
<-stop
}

// ---- journal.go: loadSnapshot ZK/MPT mismatch tolerance --------------------

// TestLoadSnapshotAcceptsZkMptMismatch verifies that loadSnapshot succeeds when
// the snapshot was stored under mptRoot but is requested with the corresponding
// zkRoot (MPT node syncing ZK-era blocks, snapshot already generated with fix).
func TestLoadSnapshotAcceptsZkMptMismatch(t *testing.T) {
db := rawdb.NewMemoryDatabase()
triedb := trie.NewDatabase(db)

mptRoot := common.HexToHash("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
zkRoot := common.HexToHash("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")

// Snapshot is stored at mptRoot (written by generateSnapshot after translation).
rawdb.WriteSnapshotRoot(db, mptRoot)
writeDoneGenerator(db)
// The ZK→MPT mapping tells loadSnapshot they correspond.
rawdb.WriteDiskStateRoot(db, zkRoot, mptRoot)

snap, disabled, err := loadSnapshot(db, triedb, 16, zkRoot, false /* recovery */)
if err != nil {
t.Fatalf("loadSnapshot with ZK/MPT mismatch should succeed, got: %v", err)
}
if disabled {
t.Fatal("snapshot should not be disabled")
}
if snap.Root() != mptRoot {
t.Fatalf("snapshot root: got %x, want mptRoot %x", snap.Root(), mptRoot)
}
}

// TestLoadSnapshotRejectsTrueMismatch verifies that a genuine root mismatch
// (no DiskStateRoot mapping, and not in recovery mode) is still rejected so
// that the ZK/MPT tolerance does not become a security hole.
func TestLoadSnapshotRejectsTrueMismatch(t *testing.T) {
db := rawdb.NewMemoryDatabase()
triedb := trie.NewDatabase(db)

storedRoot := common.HexToHash("0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc")
requestedRoot := common.HexToHash("0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd")

// Snapshot stored at storedRoot, no DiskStateRoot mapping for requestedRoot.
rawdb.WriteSnapshotRoot(db, storedRoot)
writeDoneGenerator(db)
// Intentionally no WriteDiskStateRoot call.

_, _, err := loadSnapshot(db, triedb, 16, requestedRoot, false /* recovery */)
if err == nil {
t.Fatal("loadSnapshot should return error for genuine root mismatch without DiskStateRoot mapping")
}
}

// ---- snapshot.go: Rebuild uses base.root as map key ------------------------

// TestRebuildZkRootMapKeyConsistency verifies that after Rebuild(zkRoot), the
// snapshot tree's layers map is keyed by mptRoot (base.root after translation),
// not zkRoot, so that Snapshot(mptRoot) and DiskRoot() return correct results.
func TestRebuildZkRootMapKeyConsistency(t *testing.T) {
diskdb := memorydb.New()
mptRoot, triedb := buildMPTTrie(t, diskdb)

zkRoot := common.HexToHash("0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee")
rawdb.WriteDiskStateRoot(diskdb, zkRoot, mptRoot)

snaps := &Tree{
diskdb: diskdb,
triedb: triedb,
cache: 16,
layers: make(map[common.Hash]snapshot),
}
snaps.Rebuild(zkRoot)

// Wait for generation to complete.
select {
case <-snaps.disklayer().genPending:
case <-time.After(3 * time.Second):
t.Fatal("snapshot generation timed out")
}

// DiskRoot() must return mptRoot.
if got := snaps.DiskRoot(); got != mptRoot {
t.Fatalf("DiskRoot(): got %x, want mptRoot %x", got, mptRoot)
}
// The layers map must be keyed by mptRoot so Snapshot(mptRoot) finds it.
if snaps.Snapshot(mptRoot) == nil {
t.Fatal("Snapshot(mptRoot) returned nil — map key was not translated from zkRoot")
}
// Snapshot(zkRoot) should NOT return a layer (key is mptRoot, not zkRoot).
if snaps.Snapshot(zkRoot) != nil {
t.Fatal("Snapshot(zkRoot) returned non-nil — map should be keyed by mptRoot only")
}
// Verify layers map has exactly one entry (the disk layer at mptRoot).
snaps.lock.RLock()
count := len(snaps.layers)
snaps.lock.RUnlock()
if count != 1 {
t.Fatalf("layers map has %d entries, want 1", count)
}

// Cap/Update should work correctly using mptRoot as parent.
diffRoot := common.HexToHash("0xff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00")
if err := snaps.Update(diffRoot, mptRoot, nil, map[common.Hash][]byte{
common.HexToHash("0xab"): randomAccount(),
}, nil); err != nil {
t.Fatalf("Update after Rebuild failed: %v", err)
}
if snaps.Snapshot(diffRoot) == nil {
t.Fatal("diffLayer not found after Update — parent lookup using mptRoot failed")
}

stop := make(chan *generatorStats)
snaps.disklayer().genAbort <- stop
<-stop
}

// TestRebuildWithoutTranslation verifies that when no DiskStateRoot mapping
// exists, Rebuild(mptRoot) stores the disk layer under mptRoot as before —
// ensuring no regression for the post-Jade-fork (pure MPT) case.
func TestRebuildWithoutTranslation(t *testing.T) {
diskdb := memorydb.New()
mptRoot, triedb := buildMPTTrie(t, diskdb)
// No DiskStateRoot mapping — pure MPT case.

snaps := &Tree{
diskdb: diskdb,
triedb: triedb,
cache: 16,
layers: make(map[common.Hash]snapshot),
}
snaps.Rebuild(mptRoot)

select {
case <-snaps.disklayer().genPending:
case <-time.After(3 * time.Second):
t.Fatal("snapshot generation timed out")
}

if got := snaps.DiskRoot(); got != mptRoot {
t.Fatalf("DiskRoot(): got %x, want %x", got, mptRoot)
}
if snaps.Snapshot(mptRoot) == nil {
t.Fatal("Snapshot(mptRoot) returned nil after Rebuild(mptRoot)")
}

stop := make(chan *generatorStats)
snaps.disklayer().genAbort <- stop
<-stop
}
Loading