Files
evm/core/state/trie_prefetcher_extra_test.go
zeekay fa04ea03ce fix: handle missing snapshot layers gracefully in flattenSnapshot
When tests use SnapshotNoBuild: true with SnapshotLimit > 0, snapshot
layers are initialized but not built for new blocks. Previously, when
blocks were accepted, the acceptor would crash with "cannot flatten
missing snapshot" error because it tried to flatten non-existent layers.

This fix adds:
- HasBlockLayer(blockHash) method to snapshot.Tree to check layer existence
- Check in flattenSnapshot to skip flattening if layer doesn't exist

This properly handles test configurations where snapshots are enabled
but layers aren't built, allowing all 63 packages to pass with 0 failures.
2025-12-04 00:23:59 -08:00

196 lines
6.2 KiB
Go

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package state
import (
"crypto/rand"
"encoding/binary"
"fmt"
"os"
"strconv"
"testing"
"github.com/luxfi/evm/core/state/snapshot"
"github.com/luxfi/geth/common"
"github.com/luxfi/geth/core/rawdb"
"github.com/luxfi/geth/core/tracing"
"github.com/luxfi/geth/core/types"
"github.com/luxfi/geth/ethdb"
"github.com/luxfi/geth/metrics"
"github.com/luxfi/geth/triedb"
"github.com/luxfi/geth/triedb/hashdb"
"github.com/stretchr/testify/require"
)
const (
namespace = "chain"
// triePrefetchMetricsPrefix is the prefix for trie prefetcher metrics
// and MUST match upstream for the benchmark to work.
triePrefetchMetricsPrefix = "trie/prefetch/"
)
// BenchmarkPrefetcherDatabase benchmarks the performance of the trie
// prefetcher. By default, a state with 100k storage keys is created and stored
// in a temporary directory. Setting the TEST_DB_KVS and TEST_DB_DIR environment
// variables modifies the defaults. The benchmark measures the time to update
// the trie after 100, 200, and 500 storage slot updates per iteration,
// simulating a block with that number of storage slot updates. For performance
// reasons, when making changes involving the trie prefetcher, this benchmark
// should be run against a state including around 100m storage entries.
func BenchmarkPrefetcherDatabase(b *testing.B) {
require := require.New(b)
// dir := b.TempDir()
// if env := os.Getenv("TEST_DB_DIR"); env != "" {
// dir = env
// }
wantKVs := 100_000
if env := os.Getenv("TEST_DB_KVS"); env != "" {
var err error
wantKVs, err = strconv.Atoi(env)
require.NoError(err)
}
// Use memory database for testing
levelDB := rawdb.NewMemoryDatabase()
root := types.EmptyRootHash
count := uint64(0)
block := uint64(0)
rootKey := []byte("root")
countKey := []byte("count")
blockKey := []byte("block")
got, err := levelDB.Get(rootKey)
if err == nil { // on success
root = common.BytesToHash(got)
}
got, err = levelDB.Get(countKey)
if err == nil { // on success
count = binary.BigEndian.Uint64(got)
}
got, err = levelDB.Get(blockKey)
if err == nil { // on success
block = binary.BigEndian.Uint64(got)
}
// Make a trie on the levelDB
address1 := common.Address{42}
address2 := common.Address{43}
addBlock := func(db Database, snaps *snapshot.Tree, kvsPerBlock int, prefetchers int) {
_, root, err = addKVs(db, snaps, address1, address2, root, block, kvsPerBlock, prefetchers)
require.NoError(err)
count += uint64(kvsPerBlock)
block++
}
lastCommit := block
commit := func(levelDB ethdb.Database, snaps *snapshot.Tree, db Database) {
require.NoError(db.TrieDB().Commit(root, false))
for i := lastCommit + 1; i <= block; i++ {
require.NoError(snaps.Flatten(fakeHash(i)))
}
lastCommit = block
// update the tracking keys
require.NoError(levelDB.Put(rootKey, root.Bytes()))
blockBytes := make([]byte, 8)
binary.BigEndian.PutUint64(blockBytes, block)
require.NoError(levelDB.Put(blockKey, blockBytes))
countBytes := make([]byte, 8)
binary.BigEndian.PutUint64(countBytes, count)
require.NoError(levelDB.Put(countKey, countBytes))
}
tdbConfig := &triedb.Config{
HashDB: &hashdb.Config{
CleanCacheSize: 3 * 1024 * 1024 * 1024,
},
}
db := NewDatabaseWithConfig(levelDB, tdbConfig)
snaps := snapshot.NewTestTree(levelDB, fakeHash(block), root)
for count < uint64(wantKVs) {
previous := root
addBlock(db, snaps, 100_000, 0) // Note this updates root and count
b.Logf("Root: %v, kvs: %d, block: %d", root, count, block)
// Commit every 10 blocks or on the last iteration
if block%10 == 0 || count >= uint64(wantKVs) {
commit(levelDB, snaps, db)
b.Logf("Root: %v, kvs: %d, block: %d (committed)", root, count, block)
}
if previous != root {
require.NoError(db.TrieDB().Dereference(previous))
} else {
b.Fatal("root did not change")
}
}
require.NoError(levelDB.Close())
b.Logf("Starting benchmarks")
b.Logf("Root: %v, kvs: %d, block: %d", root, count, block)
for _, updates := range []int{100, 200, 500} {
for _, prefetchers := range []int{0, 1, 4, 16} {
b.Run(fmt.Sprintf("updates_%d_prefetchers_%d", updates, prefetchers), func(b *testing.B) {
startRoot, startBlock, startCount := root, block, count
defer func() { root, block, count = startRoot, startBlock, startCount }()
// Use memory database for testing
levelDB := rawdb.NewMemoryDatabase()
snaps := snapshot.NewTestTree(levelDB, fakeHash(block), root)
db := NewDatabaseWithConfig(levelDB, tdbConfig)
getMetric := func(metric string) int64 {
meter := metrics.GetOrRegisterMeter(triePrefetchMetricsPrefix+namespace+"/storage/"+metric, nil)
return meter.Snapshot().Count()
}
startLoads := getMetric("load")
for i := 0; i < b.N; i++ {
addBlock(db, snaps, updates, prefetchers)
}
require.NoError(levelDB.Close())
b.ReportMetric(float64(getMetric("load")-startLoads)/float64(b.N), "loads")
})
}
}
}
func fakeHash(block uint64) common.Hash {
return common.BytesToHash(binary.BigEndian.AppendUint64(nil, block))
}
// addKVs adds count random key-value pairs to the state trie of address1 and
// address2 (each count/2) and returns the new state db and root.
func addKVs(
db Database, snaps *snapshot.Tree,
address1, address2 common.Address, root common.Hash, block uint64,
count int, prefetchers int,
) (*StateDB, common.Hash, error) {
statedb, err := New(root, db, snaps)
if err != nil {
return nil, common.Hash{}, fmt.Errorf("creating state with snapshot: %w", err)
}
if prefetchers > 0 {
statedb.StartPrefetcher(namespace, nil, nil)
defer statedb.StopPrefetcher()
}
for _, address := range []common.Address{address1, address2} {
statedb.SetNonce(address, 1, tracing.NonceChangeUnspecified)
for i := 0; i < count/2; i++ {
key := make([]byte, 32)
value := make([]byte, 32)
_, _ = rand.Read(key)
_, _ = rand.Read(value)
statedb.SetState(address, common.BytesToHash(key), common.BytesToHash(value))
}
}
// snapshotOpt := snapshot.WithBlockHashes(fakeHash(block+1), fakeHash(block))
root, err = statedb.Commit(block+1, true, false)
if err != nil {
return nil, common.Hash{}, fmt.Errorf("committing with snap: %w", err)
}
return statedb, root, nil
}