Files
evm/core/blockchain_snapshot_test.go
zeekay 5fa8db6037 test(evm): fix snapshot-recovery prepare for non-empty (genesis-active) genesis
GenerateChain forcibly uses a hash-scheme trie db to read parent state, but the
snapshot suite runs the chain under both hash and path schemes. With an empty
genesis (pre genesis-active) parent.Root() is the empty root -- readable in any
scheme -- so block generation worked. With the genesis-active 0x9999 marker the
genesis state is non-empty, and a path-scheme chain commits its genesis nodes in
path format, invisible to GenerateChain's hash-scheme reader. state.New then
errored and GenerateChain returned 0 blocks (the error was discarded), so prepare
built an empty chain and restart recovered to genesis (head 0) instead of C4.

Generate blocks on a dedicated hash-scheme genDb via GenerateChainWithGenesis
(the pattern used by the rest of core's tests); the scheme-agnostic block
structures are re-executed into the chain's own db by InsertChain. The gapped and
wipe-crash variants source their extra blocks from genDb too.

The recovery engine is unchanged and verified correct for a non-empty genesis
under both hash and path schemes: on restart driven by lastAcceptedHash=C4 the
head, header and snapshot disk layer all land at C4 and block[4] is present.
2026-06-26 20:03:13 -07:00

608 lines
20 KiB
Go

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//
// This file is a derived work, based on the go-ethereum library whose original
// notices appear below.
//
// It is distributed under a license compatible with the licensing terms of the
// original code from which it is derived.
//
// Much love to the original authors for their work.
// **********
// Copyright 2020 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 that abnormal program termination (i.e.crash) and restart can recovery
// the snapshot properly if the snapshot is enabled.
package core
import (
"bytes"
"fmt"
"math/big"
"os"
"path"
"strings"
"testing"
"github.com/luxfi/evm/consensus"
"github.com/luxfi/evm/consensus/dummy"
"github.com/luxfi/evm/params"
"github.com/luxfi/evm/plugin/evm/upgrade/legacy"
"github.com/luxfi/geth/common"
"github.com/luxfi/geth/core/rawdb"
"github.com/luxfi/geth/core/types"
"github.com/luxfi/geth/core/vm"
"github.com/luxfi/geth/ethdb"
)
// snapshotTestBasic wraps the common testing fields in the snapshot tests.
type snapshotTestBasic struct {
scheme string // Disk scheme used for storing trie nodes
chainBlocks int // Number of blocks to generate for the canonical chain
snapshotBlock uint64 // Block number of the relevant snapshot disk layer
expCanonicalBlocks int // Number of canonical blocks expected to remain in the database (excl. genesis)
expHeadBlock uint64 // Block number of the expected head full block
expSnapshotBottom uint64 // The block height corresponding to the snapshot disk layer
// share fields, set in runtime
datadir string
ancient string
db ethdb.Database
genDb ethdb.Database
engine consensus.Engine
gspec *Genesis
lastAcceptedHash common.Hash
}
func (basic *snapshotTestBasic) prepare(t *testing.T) (*BlockChain, []*types.Block) {
// Create a temporary persistent database
datadir := t.TempDir()
ancient := path.Join(datadir, "ancient")
// For testing, use a memory database
db := rawdb.NewMemoryDatabase()
// Initialize a fresh chain
var (
gspec = &Genesis{
BaseFee: big.NewInt(legacy.BaseFee),
Config: params.TestChainConfig,
Alloc: types.GenesisAlloc{},
}
engine = dummy.NewFullFaker()
)
// Create blockchain first
chain, err := NewBlockChain(db, DefaultCacheConfigWithScheme(basic.scheme), gspec, engine, vm.Config{}, common.Hash{}, false, nil)
if err != nil {
t.Fatalf("Failed to create chain: %v", err)
}
// Generate blocks on a dedicated hash-scheme database (genDb) so that block
// production can always read the genesis state, even when it is non-empty
// (e.g. a genesis-active precompile marker). GenerateChain forcibly uses a
// hash-scheme trie database to retain all nodes; if it shared the chain's
// path-scheme db, the non-empty genesis nodes would be unreadable and no
// blocks would be produced. The block structures are scheme-agnostic and are
// re-executed into the chain's own (basic.scheme) database via InsertChain
// below, so this matches the GenerateChainWithGenesis pattern used by the
// rest of the core test suite.
genDb, blocks, _, err := GenerateChainWithGenesis(gspec, engine, basic.chainBlocks, 10, func(i int, b *BlockGen) {})
if err != nil {
t.Fatalf("Failed to generate chain: %v", err)
}
// Store the databases for later use
basic.genDb = genDb
basic.db = db
// genesis as last accepted
basic.lastAcceptedHash = chain.GetBlockByNumber(0).Hash()
// Insert blocks one by one
for i, block := range blocks {
if _, err := chain.InsertChain(types.Blocks{block}); err != nil {
t.Fatalf("Failed to import block %d: %v", i, err)
}
// Check if we've reached a snapshot point
if basic.snapshotBlock > 0 && uint64(i+1) == basic.snapshotBlock {
// Flushing from 0 to snapshotBlock into the disk
for j := 0; j <= i; j++ {
if err := chain.Accept(blocks[j]); err != nil {
t.Fatalf("Failed to accept block %v: %v", j, err)
}
basic.lastAcceptedHash = blocks[j].Hash()
}
chain.DrainAcceptorQueue()
// Wait for snapshot generation to complete
if chain.snaps != nil {
diskRoot, blockRoot := chain.snaps.DiskRoot(), blocks[i].Root()
if !bytes.Equal(diskRoot.Bytes(), blockRoot.Bytes()) {
t.Fatalf("Failed to flush disk layer change, want %x, got %x", blockRoot, diskRoot)
}
}
}
}
// Set runtime fields
basic.datadir = datadir
basic.ancient = ancient
basic.engine = engine
basic.gspec = gspec
return chain, blocks
}
func (basic *snapshotTestBasic) verify(t *testing.T, chain *BlockChain, blocks []*types.Block) {
// Iterate over all the remaining blocks and ensure there are no gaps
verifyNoGaps(t, chain, true, blocks)
verifyCutoff(t, chain, true, blocks, basic.expCanonicalBlocks)
if head := chain.CurrentHeader(); head.Number.Uint64() != basic.expHeadBlock {
t.Errorf("Head header mismatch: have %d, want %d", head.Number, basic.expHeadBlock)
}
if head := chain.CurrentBlock(); head.Number.Uint64() != basic.expHeadBlock {
t.Errorf("Head block mismatch: have %d, want %d", head.Number, basic.expHeadBlock)
}
// Check the disk layer, ensure they are matched
block := chain.GetBlockByNumber(basic.expSnapshotBottom)
if block == nil {
t.Errorf("The corresponding block[%d] of snapshot disk layer is missing", basic.expSnapshotBottom)
} else if !bytes.Equal(chain.snaps.DiskRoot().Bytes(), block.Root().Bytes()) {
t.Errorf("The snapshot disk layer root is incorrect, want %x, get %x", block.Root(), chain.snaps.DiskRoot())
} else if len(chain.snaps.Snapshots(block.Hash(), -1, false)) != 1 {
t.Errorf("The corresponding block[%d] of snapshot disk layer is missing", basic.expSnapshotBottom)
}
// Check the snapshot, ensure it's integrated
if err := chain.snaps.Verify(block.Root()); err != nil {
t.Errorf("The disk layer is not integrated %v", err)
}
}
//nolint:unused
func (basic *snapshotTestBasic) dump() string {
buffer := new(strings.Builder)
fmt.Fprint(buffer, "Chain:\n G")
for i := 0; i < basic.chainBlocks; i++ {
fmt.Fprintf(buffer, "->C%d", i+1)
}
fmt.Fprint(buffer, " (HEAD)\n\n")
fmt.Fprintf(buffer, "Snapshot: G")
if basic.snapshotBlock > 0 {
fmt.Fprintf(buffer, ", C%d", basic.snapshotBlock)
}
fmt.Fprint(buffer, "\n")
//if crash {
// fmt.Fprintf(buffer, "\nCRASH\n\n")
//} else {
// fmt.Fprintf(buffer, "\nSetHead(%d)\n\n", basic.setHead)
//}
fmt.Fprintf(buffer, "------------------------------\n\n")
fmt.Fprint(buffer, "Expected in leveldb:\n G")
for i := 0; i < basic.expCanonicalBlocks; i++ {
fmt.Fprintf(buffer, "->C%d", i+1)
}
fmt.Fprintf(buffer, "\n\n")
fmt.Fprintf(buffer, "Expected head header : C%d\n", basic.expHeadBlock)
if basic.expHeadBlock == 0 {
fmt.Fprintf(buffer, "Expected head block : G\n")
} else {
fmt.Fprintf(buffer, "Expected head block : C%d\n", basic.expHeadBlock)
}
if basic.expSnapshotBottom == 0 {
fmt.Fprintf(buffer, "Expected snapshot disk : G\n")
} else {
fmt.Fprintf(buffer, "Expected snapshot disk : C%d\n", basic.expSnapshotBottom)
}
return buffer.String()
}
func (basic *snapshotTestBasic) teardown() {
_ = basic.db.Close()
_ = basic.genDb.Close()
os.RemoveAll(basic.datadir)
os.RemoveAll(basic.ancient)
}
// snapshotTest is a test case type for normal snapshot recovery.
// It can be used for testing that restart Geth normally.
type snapshotTest struct {
snapshotTestBasic
}
func (snaptest *snapshotTest) test(t *testing.T) {
// It's hard to follow the test case, visualize the input
// log.Root().SetHandler(log.LevelFilterHandler(log.LevelTrace, log.StreamHandler(os.Stderr, log.TerminalFormat(true))))
// fmt.Println(tt.dump())
chain, blocks := snaptest.prepare(t)
// Restart the chain normally
chain.Stop()
newchain, err := NewBlockChain(snaptest.db, DefaultCacheConfigWithScheme(snaptest.scheme), snaptest.gspec, snaptest.engine, vm.Config{}, snaptest.lastAcceptedHash, false, nil)
if err != nil {
t.Fatalf("Failed to recreate chain: %v", err)
}
defer newchain.Stop()
snaptest.verify(t, newchain, blocks)
}
// crashSnapshotTest is a test case type for irregular snapshot recovery.
// It can be used for testing that restart Geth after the crash.
type crashSnapshotTest struct {
snapshotTestBasic
}
func (snaptest *crashSnapshotTest) test(t *testing.T) {
// It's hard to follow the test case, visualize the input
// log.Root().SetHandler(log.LevelFilterHandler(log.LevelTrace, log.StreamHandler(os.Stderr, log.TerminalFormat(true))))
// fmt.Println(tt.dump())
chain, blocks := snaptest.prepare(t)
// Pull the plug on the database, simulating a hard crash
db := chain.db
chain.stopWithoutSaving()
_ = chain.triedb.Close()
// The interesting thing is: instead of starting the blockchain after
// the crash, we do restart twice here: one after the crash and one
// after the normal stop. It's used to ensure the broken snapshot
// can be detected all the time.
newchain, err := NewBlockChain(db, DefaultCacheConfigWithScheme(snaptest.scheme), snaptest.gspec, snaptest.engine, vm.Config{}, snaptest.lastAcceptedHash, false, nil)
if err != nil {
t.Fatalf("Failed to recreate chain: %v", err)
}
newchain.Stop()
newchain, err = NewBlockChain(db, DefaultCacheConfigWithScheme(snaptest.scheme), snaptest.gspec, snaptest.engine, vm.Config{}, snaptest.lastAcceptedHash, false, nil)
if err != nil {
t.Fatalf("Failed to recreate chain: %v", err)
}
defer newchain.Stop()
snaptest.verify(t, newchain, blocks)
}
// gappedSnapshotTest is a test type used to test this scenario:
// - have a complete snapshot
// - restart without enabling the snapshot
// - insert a few blocks
// - restart with enabling the snapshot again
type gappedSnapshotTest struct {
snapshotTestBasic
gapped int // Number of blocks to insert without enabling snapshot
}
func (snaptest *gappedSnapshotTest) test(t *testing.T) {
// It's hard to follow the test case, visualize the input
// log.Root().SetHandler(log.LevelFilterHandler(log.LevelTrace, log.StreamHandler(os.Stderr, log.TerminalFormat(true))))
// fmt.Println(tt.dump())
chain, blocks := snaptest.prepare(t)
// Insert blocks without enabling snapshot if gapping is required.
chain.Stop()
gappedBlocks, _, _ := GenerateChain(snaptest.gspec.Config, blocks[len(blocks)-1], snaptest.engine, snaptest.genDb, snaptest.gapped, 10, func(i int, b *BlockGen) {})
// Insert a few more blocks without enabling snapshot
var cacheConfig = &CacheConfig{
TrieCleanLimit: 256,
TrieDirtyLimit: 256,
SnapshotLimit: 0,
Pruning: true,
CommitInterval: 4096,
StateHistory: 32,
StateScheme: snaptest.scheme,
}
newchain, err := NewBlockChain(snaptest.db, cacheConfig, snaptest.gspec, snaptest.engine, vm.Config{}, snaptest.lastAcceptedHash, false, nil)
if err != nil {
t.Fatalf("Failed to recreate chain: %v", err)
}
newchain.InsertChain(gappedBlocks)
newchain.Stop()
// Restart the chain with enabling the snapshot
newchain, err = NewBlockChain(snaptest.db, DefaultCacheConfigWithScheme(snaptest.scheme), snaptest.gspec, snaptest.engine, vm.Config{}, snaptest.lastAcceptedHash, false, nil)
if err != nil {
t.Fatalf("Failed to recreate chain: %v", err)
}
defer newchain.Stop()
snaptest.verify(t, newchain, blocks)
}
// wipeCrashSnapshotTest is the test type used to test this scenario:
// - have a complete snapshot
// - restart, insert more blocks without enabling the snapshot
// - restart again with enabling the snapshot
// - crash
type wipeCrashSnapshotTest struct {
snapshotTestBasic
newBlocks int
}
func (snaptest *wipeCrashSnapshotTest) test(t *testing.T) {
// It's hard to follow the test case, visualize the input
// log.Root().SetHandler(log.LevelFilterHandler(log.LevelTrace, log.StreamHandler(os.Stderr, log.TerminalFormat(true))))
// fmt.Println(tt.dump())
chain, blocks := snaptest.prepare(t)
// Firstly, stop the chain properly, with all snapshot journal
// and state committed.
chain.Stop()
config := &CacheConfig{
TrieCleanLimit: 256,
TrieDirtyLimit: 256,
SnapshotLimit: 0,
Pruning: true,
CommitInterval: 4096,
StateHistory: 32,
StateScheme: snaptest.scheme,
}
newchain, err := NewBlockChain(snaptest.db, config, snaptest.gspec, snaptest.engine, vm.Config{}, snaptest.lastAcceptedHash, false, nil)
if err != nil {
t.Fatalf("Failed to recreate chain: %v", err)
}
newBlocks, _, _ := GenerateChain(snaptest.gspec.Config, blocks[len(blocks)-1], snaptest.engine, snaptest.genDb, snaptest.newBlocks, 10, func(i int, b *BlockGen) {})
newchain.InsertChain(newBlocks)
newchain.Stop()
// Restart the chain, the wiper should start working
config = &CacheConfig{
TrieCleanLimit: 256,
TrieDirtyLimit: 256,
SnapshotLimit: 256,
Pruning: true,
CommitInterval: 4096,
StateHistory: 32,
SnapshotWait: false, // Don't wait rebuild
StateScheme: snaptest.scheme,
}
tmp, err := NewBlockChain(snaptest.db, config, snaptest.gspec, snaptest.engine, vm.Config{}, snaptest.lastAcceptedHash, false, nil)
if err != nil {
t.Fatalf("Failed to recreate chain: %v", err)
}
// Simulate the blockchain crash.
_ = tmp.triedb.Close()
tmp.stopWithoutSaving()
newchain, err = NewBlockChain(snaptest.db, DefaultCacheConfigWithScheme(snaptest.scheme), snaptest.gspec, snaptest.engine, vm.Config{}, snaptest.lastAcceptedHash, false, nil)
if err != nil {
t.Fatalf("Failed to recreate chain: %v", err)
}
snaptest.verify(t, newchain, blocks)
newchain.Stop()
}
// Tests a Geth restart with valid snapshot. Before the shutdown, all snapshot
// journal will be persisted correctly. In this case no snapshot recovery is
// required.
func TestRestartWithNewSnapshot(t *testing.T) {
// Chain:
// G->C1->C2->C3->C4->C5->C6->C7->C8 (HEAD)
//
// Snapshot: G
//
// ------------------------------
//
// Expected in leveldb:
// G->C1->C2->C3->C4->C5->C6->C7->C8
//
// Expected head header : C8
// Expected head block : C4
// Expected snapshot disk : C4
for _, scheme := range []string{rawdb.HashScheme, rawdb.PathScheme} {
test := &snapshotTest{
snapshotTestBasic{
scheme: scheme,
chainBlocks: 8,
snapshotBlock: 4,
expCanonicalBlocks: 8,
expHeadBlock: 4,
expSnapshotBottom: 4, // Initial disk layer built from genesis
},
}
test.test(t)
test.teardown()
}
}
// Tests a Geth was crashed and restarts with a broken snapshot. In this case the
// chain head should be rewound to the point with available state. And also the
// new head should must be lower than disk layer. But there is no committed point
// so the chain should be rewound to genesis and the disk layer should be left
// for recovery.
func TestNoCommitCrashWithNewSnapshot(t *testing.T) {
// Chain:
// G->C1->C2->C3->C4->C5->C6->C7->C8 (HEAD)
//
// Snapshot: G, C4
//
// CRASH
//
// ------------------------------
//
// Expected in leveldb:
// G->C1->C2->C3->C4->C5->C6->C7->C8
//
// Expected head block : C4
// Expected snapshot disk : C4
for _, scheme := range []string{rawdb.HashScheme, rawdb.PathScheme} {
test := &crashSnapshotTest{
snapshotTestBasic{
scheme: scheme,
chainBlocks: 8,
snapshotBlock: 4,
expCanonicalBlocks: 8,
expHeadBlock: 4,
expSnapshotBottom: 4, // Last committed disk layer, wait recovery
},
}
test.test(t)
test.teardown()
}
}
// Tests a Geth was crashed and restarts with a broken snapshot. In this case the
// chain head should be rewound to the point with available state. And also the
// new head should must be lower than disk layer. But there is only a low committed
// point so the chain should be rewound to committed point and the disk layer
// should be left for recovery.
func TestLowCommitCrashWithNewSnapshot(t *testing.T) {
// Chain:
// G->C1->C2->C3->C4->C5->C6->C7->C8 (HEAD)
//
// Snapshot: G, C4
//
// CRASH
//
// ------------------------------
//
// Expected in leveldb:
// G->C1->C2->C3->C4->C5->C6->C7->C8
//
// Expected head block : C4
// Expected snapshot disk : C4
for _, scheme := range []string{rawdb.HashScheme, rawdb.PathScheme} {
test := &crashSnapshotTest{
snapshotTestBasic{
scheme: scheme,
chainBlocks: 8,
snapshotBlock: 4,
expCanonicalBlocks: 8,
expHeadBlock: 4,
expSnapshotBottom: 4, // Last committed disk layer, wait recovery
},
}
test.test(t)
test.teardown()
}
}
// Tests a Geth was crashed and restarts with a broken snapshot. In this case
// the chain head should be rewound to the point with available state. And also
// the new head should must be lower than disk layer. But there is only a high
// committed point so the chain should be rewound to genesis and the disk layer
// should be left for recovery.
func TestHighCommitCrashWithNewSnapshot(t *testing.T) {
// Chain:
// G->C1->C2->C3->C4->C5->C6->C7->C8 (HEAD)
//
// Snapshot: G, C4
//
// CRASH
//
// ------------------------------
//
// Expected in leveldb:
// G->C1->C2->C3->C4->C5->C6->C7->C8
//
// Expected head block : C4
// Expected snapshot disk : C4
for _, scheme := range []string{rawdb.HashScheme, rawdb.PathScheme} {
test := &crashSnapshotTest{
snapshotTestBasic{
scheme: scheme,
chainBlocks: 8,
snapshotBlock: 4,
expCanonicalBlocks: 8,
expHeadBlock: 4,
expSnapshotBottom: 4, // Last committed disk layer, wait recovery
},
}
test.test(t)
test.teardown()
}
}
// Tests a Geth was running with snapshot enabled. Then restarts without
// enabling snapshot and after that re-enable the snapshot again. In this
// case the snapshot should be rebuilt with latest chain head.
func TestGappedNewSnapshot(t *testing.T) {
// Chain:
// G->C1->C2->C3->C4->C5->C6->C7->C8 (HEAD)
//
// Snapshot: G
//
// ------------------------------
//
// Expected in leveldb:
// G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10
//
// Expected head block : G
// Expected snapshot disk : G
for _, scheme := range []string{rawdb.HashScheme, rawdb.PathScheme} {
test := &gappedSnapshotTest{
snapshotTestBasic: snapshotTestBasic{
scheme: scheme,
chainBlocks: 8,
snapshotBlock: 0,
expCanonicalBlocks: 10,
expHeadBlock: 0,
expSnapshotBottom: 0, // Rebuilt snapshot from the latest HEAD
},
gapped: 2,
}
test.test(t)
test.teardown()
}
}
// Tests the Geth was running with a complete snapshot and then imports a few
// more new blocks on top without enabling the snapshot. After the restart,
// crash happens. Check everything is ok after the restart.
func TestRecoverSnapshotFromWipingCrash(t *testing.T) {
// Chain:
// G->C1->C2->C3->C4->C5->C6->C7->C8 (HEAD)
//
// Snapshot: G
//
// ------------------------------
//
// Expected in leveldb:
// G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10
//
// Expected head block : C4
// Expected snapshot disk : C4
for _, scheme := range []string{rawdb.HashScheme, rawdb.PathScheme} {
test := &wipeCrashSnapshotTest{
snapshotTestBasic: snapshotTestBasic{
scheme: scheme,
chainBlocks: 8,
snapshotBlock: 4,
expCanonicalBlocks: 10,
expHeadBlock: 4,
expSnapshotBottom: 4,
},
newBlocks: 2,
}
test.test(t)
test.teardown()
}
}