erge branch 'master' of https://github.com/0xjvn/go-ethereum into fix/rewind

This commit is contained in:
0xjvn 2026-04-14 23:53:19 +05:30
commit 8d623b9464
86 changed files with 5590 additions and 1440 deletions

View file

@ -438,14 +438,11 @@ func (s *serverWithLimits) fail(desc string) {
// failLocked calculates the dynamic failure delay and applies it.
func (s *serverWithLimits) failLocked(desc string) {
log.Debug("Server error", "description", desc)
s.failureDelay *= 2
now := s.clock.Now()
if now > s.failureDelayEnd {
s.failureDelay *= math.Pow(2, -float64(now-s.failureDelayEnd)/float64(maxFailureDelay))
}
if s.failureDelay < float64(minFailureDelay) {
s.failureDelay = float64(minFailureDelay)
}
s.failureDelay = max(min(s.failureDelay*2, float64(maxFailureDelay)), float64(minFailureDelay))
s.failureDelayEnd = now + mclock.AbsTime(s.failureDelay)
s.delay(time.Duration(s.failureDelay))
}

View file

@ -62,7 +62,6 @@ const (
ssNeedParent // cp header slot %32 != 0, need parent to check epoch boundary
ssParentRequested // cp parent header requested
ssPrintStatus // has all necessary info, print log message if init still not successful
ssDone // log message printed, no more action required
)
type serverState struct {
@ -180,7 +179,8 @@ func (s *CheckpointInit) Process(requester request.Requester, events []request.E
default:
log.Error("blsync: checkpoint not available, but reported as finalized; specified checkpoint hash might be too old", "server", server.Name())
}
s.serverState[server] = serverState{state: ssDone}
s.serverState[server] = serverState{state: ssDefault}
requester.Fail(server, "checkpoint init failed")
}
}

408
cmd/geth/bintrie_convert.go Normal file
View file

@ -0,0 +1,408 @@
// Copyright 2026 The go-ethereum Authors
// This file is part of go-ethereum.
//
// go-ethereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// go-ethereum 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"errors"
"fmt"
"runtime"
"runtime/debug"
"slices"
"time"
"github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie"
"github.com/ethereum/go-ethereum/trie/bintrie"
"github.com/ethereum/go-ethereum/trie/trienode"
"github.com/ethereum/go-ethereum/triedb"
"github.com/ethereum/go-ethereum/triedb/pathdb"
"github.com/urfave/cli/v2"
)
var (
deleteSourceFlag = &cli.BoolFlag{
Name: "delete-source",
Usage: "Delete MPT trie nodes after conversion",
}
memoryLimitFlag = &cli.Uint64Flag{
Name: "memory-limit",
Usage: "Max heap allocation in MB before forcing a commit cycle",
Value: 16384,
}
bintrieCommand = &cli.Command{
Name: "bintrie",
Usage: "A set of commands for binary trie operations",
Description: "",
Subcommands: []*cli.Command{
{
Name: "convert",
Usage: "Convert MPT state to binary trie",
ArgsUsage: "[state-root]",
Action: convertToBinaryTrie,
Flags: slices.Concat([]cli.Flag{
deleteSourceFlag,
memoryLimitFlag,
}, utils.NetworkFlags, utils.DatabaseFlags),
Description: `
geth bintrie convert [--delete-source] [--memory-limit MB] [state-root]
Reads all state from the Merkle Patricia Trie and writes it into a Binary Trie,
operating offline. Memory-safe via periodic commit-and-reload cycles.
The optional state-root argument specifies which state root to convert.
If omitted, the head block's state root is used.
Flags:
--delete-source Delete MPT trie nodes after successful conversion
--memory-limit Max heap allocation in MB before forcing a commit (default: 16384)
`,
},
},
}
)
type conversionStats struct {
accounts uint64
slots uint64
codes uint64
commits uint64
start time.Time
lastReport time.Time
lastMemChk time.Time
}
func (s *conversionStats) report(force bool) {
if !force && time.Since(s.lastReport) < 8*time.Second {
return
}
elapsed := time.Since(s.start).Seconds()
acctRate := float64(0)
if elapsed > 0 {
acctRate = float64(s.accounts) / elapsed
}
log.Info("Conversion progress",
"accounts", s.accounts,
"slots", s.slots,
"codes", s.codes,
"commits", s.commits,
"accounts/sec", fmt.Sprintf("%.0f", acctRate),
"elapsed", common.PrettyDuration(time.Since(s.start)),
)
s.lastReport = time.Now()
}
func convertToBinaryTrie(ctx *cli.Context) error {
if ctx.NArg() > 1 {
return errors.New("too many arguments")
}
stack, _ := makeConfigNode(ctx)
defer stack.Close()
chaindb := utils.MakeChainDatabase(ctx, stack, false)
defer chaindb.Close()
headBlock := rawdb.ReadHeadBlock(chaindb)
if headBlock == nil {
return errors.New("no head block found")
}
var (
root common.Hash
err error
)
if ctx.NArg() == 1 {
root, err = parseRoot(ctx.Args().First())
if err != nil {
return fmt.Errorf("invalid state root: %w", err)
}
} else {
root = headBlock.Root()
}
log.Info("Starting MPT to binary trie conversion", "root", root, "block", headBlock.NumberU64())
srcTriedb := utils.MakeTrieDatabase(ctx, stack, chaindb, true, true, false)
defer srcTriedb.Close()
destTriedb := triedb.NewDatabase(chaindb, &triedb.Config{
IsVerkle: true,
PathDB: &pathdb.Config{
JournalDirectory: stack.ResolvePath("triedb-bintrie"),
},
})
defer destTriedb.Close()
binTrie, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, destTriedb)
if err != nil {
return fmt.Errorf("failed to create binary trie: %w", err)
}
memLimit := ctx.Uint64(memoryLimitFlag.Name) * 1024 * 1024
currentRoot, err := runConversionLoop(chaindb, srcTriedb, destTriedb, binTrie, root, memLimit)
if err != nil {
return err
}
log.Info("Conversion complete", "binaryRoot", currentRoot)
if ctx.Bool(deleteSourceFlag.Name) {
log.Info("Deleting source MPT data")
if err := deleteMPTData(chaindb, srcTriedb, root); err != nil {
return fmt.Errorf("MPT deletion failed: %w", err)
}
log.Info("Source MPT data deleted")
}
return nil
}
func runConversionLoop(chaindb ethdb.Database, srcTriedb *triedb.Database, destTriedb *triedb.Database, binTrie *bintrie.BinaryTrie, root common.Hash, memLimit uint64) (common.Hash, error) {
currentRoot := types.EmptyBinaryHash
stats := &conversionStats{
start: time.Now(),
lastReport: time.Now(),
lastMemChk: time.Now(),
}
srcTrie, err := trie.NewStateTrie(trie.StateTrieID(root), srcTriedb)
if err != nil {
return common.Hash{}, fmt.Errorf("failed to open source trie: %w", err)
}
acctIt, err := srcTrie.NodeIterator(nil)
if err != nil {
return common.Hash{}, fmt.Errorf("failed to create account iterator: %w", err)
}
accIter := trie.NewIterator(acctIt)
for accIter.Next() {
var acc types.StateAccount
if err := rlp.DecodeBytes(accIter.Value, &acc); err != nil {
return common.Hash{}, fmt.Errorf("invalid account RLP: %w", err)
}
addrBytes := srcTrie.GetKey(accIter.Key)
if addrBytes == nil {
return common.Hash{}, fmt.Errorf("missing preimage for account hash %x (run with --cache.preimages)", accIter.Key)
}
addr := common.BytesToAddress(addrBytes)
var code []byte
codeHash := common.BytesToHash(acc.CodeHash)
if codeHash != types.EmptyCodeHash {
code = rawdb.ReadCode(chaindb, codeHash)
if code == nil {
return common.Hash{}, fmt.Errorf("missing code for hash %x (account %x)", codeHash, addr)
}
stats.codes++
}
if err := binTrie.UpdateAccount(addr, &acc, len(code)); err != nil {
return common.Hash{}, fmt.Errorf("failed to update account %x: %w", addr, err)
}
if len(code) > 0 {
if err := binTrie.UpdateContractCode(addr, codeHash, code); err != nil {
return common.Hash{}, fmt.Errorf("failed to update code for %x: %w", addr, err)
}
}
if acc.Root != types.EmptyRootHash {
addrHash := common.BytesToHash(accIter.Key)
storageTrie, err := trie.NewStateTrie(trie.StorageTrieID(root, addrHash, acc.Root), srcTriedb)
if err != nil {
return common.Hash{}, fmt.Errorf("failed to open storage trie for %x: %w", addr, err)
}
storageNodeIt, err := storageTrie.NodeIterator(nil)
if err != nil {
return common.Hash{}, fmt.Errorf("failed to create storage iterator for %x: %w", addr, err)
}
storageIter := trie.NewIterator(storageNodeIt)
slotCount := uint64(0)
for storageIter.Next() {
slotKey := storageTrie.GetKey(storageIter.Key)
if slotKey == nil {
return common.Hash{}, fmt.Errorf("missing preimage for storage key %x (account %x)", storageIter.Key, addr)
}
_, content, _, err := rlp.Split(storageIter.Value)
if err != nil {
return common.Hash{}, fmt.Errorf("invalid storage RLP for key %x (account %x): %w", slotKey, addr, err)
}
if err := binTrie.UpdateStorage(addr, slotKey, content); err != nil {
return common.Hash{}, fmt.Errorf("failed to update storage %x/%x: %w", addr, slotKey, err)
}
stats.slots++
slotCount++
if slotCount%10000 == 0 {
binTrie, currentRoot, err = maybeCommit(binTrie, currentRoot, destTriedb, memLimit, stats)
if err != nil {
return common.Hash{}, err
}
}
}
if storageIter.Err != nil {
return common.Hash{}, fmt.Errorf("storage iteration error for %x: %w", addr, storageIter.Err)
}
}
stats.accounts++
stats.report(false)
if stats.accounts%1000 == 0 {
binTrie, currentRoot, err = maybeCommit(binTrie, currentRoot, destTriedb, memLimit, stats)
if err != nil {
return common.Hash{}, err
}
}
}
if accIter.Err != nil {
return common.Hash{}, fmt.Errorf("account iteration error: %w", accIter.Err)
}
_, currentRoot, err = commitBinaryTrie(binTrie, currentRoot, destTriedb)
if err != nil {
return common.Hash{}, fmt.Errorf("final commit failed: %w", err)
}
stats.commits++
stats.report(true)
return currentRoot, nil
}
func maybeCommit(bt *bintrie.BinaryTrie, currentRoot common.Hash, destDB *triedb.Database, memLimit uint64, stats *conversionStats) (*bintrie.BinaryTrie, common.Hash, error) {
if time.Since(stats.lastMemChk) < 5*time.Second {
return bt, currentRoot, nil
}
stats.lastMemChk = time.Now()
var m runtime.MemStats
runtime.ReadMemStats(&m)
if m.Alloc < memLimit {
return bt, currentRoot, nil
}
log.Info("Memory limit reached, committing", "alloc", common.StorageSize(m.Alloc), "limit", common.StorageSize(memLimit))
bt, currentRoot, err := commitBinaryTrie(bt, currentRoot, destDB)
if err != nil {
return nil, common.Hash{}, err
}
stats.commits++
stats.report(true)
return bt, currentRoot, nil
}
func commitBinaryTrie(bt *bintrie.BinaryTrie, currentRoot common.Hash, destDB *triedb.Database) (*bintrie.BinaryTrie, common.Hash, error) {
newRoot, nodeSet := bt.Commit(false)
if nodeSet != nil {
merged := trienode.NewWithNodeSet(nodeSet)
if err := destDB.Update(newRoot, currentRoot, 0, merged, triedb.NewStateSet()); err != nil {
return nil, common.Hash{}, fmt.Errorf("triedb update failed: %w", err)
}
if err := destDB.Commit(newRoot, false); err != nil {
return nil, common.Hash{}, fmt.Errorf("triedb commit failed: %w", err)
}
}
runtime.GC()
debug.FreeOSMemory()
bt, err := bintrie.NewBinaryTrie(newRoot, destDB)
if err != nil {
return nil, common.Hash{}, fmt.Errorf("failed to reload binary trie: %w", err)
}
return bt, newRoot, nil
}
func deleteMPTData(chaindb ethdb.Database, srcTriedb *triedb.Database, root common.Hash) error {
isPathDB := srcTriedb.Scheme() == rawdb.PathScheme
srcTrie, err := trie.NewStateTrie(trie.StateTrieID(root), srcTriedb)
if err != nil {
return fmt.Errorf("failed to open source trie for deletion: %w", err)
}
acctIt, err := srcTrie.NodeIterator(nil)
if err != nil {
return fmt.Errorf("failed to create account iterator for deletion: %w", err)
}
batch := chaindb.NewBatch()
deleted := 0
for acctIt.Next(true) {
if isPathDB {
rawdb.DeleteAccountTrieNode(batch, acctIt.Path())
} else {
node := acctIt.Hash()
if node != (common.Hash{}) {
rawdb.DeleteLegacyTrieNode(batch, node)
}
}
deleted++
if acctIt.Leaf() {
var acc types.StateAccount
if err := rlp.DecodeBytes(acctIt.LeafBlob(), &acc); err != nil {
return fmt.Errorf("invalid account during deletion: %w", err)
}
if acc.Root != types.EmptyRootHash {
addrHash := common.BytesToHash(acctIt.LeafKey())
storageTrie, err := trie.NewStateTrie(trie.StorageTrieID(root, addrHash, acc.Root), srcTriedb)
if err != nil {
return fmt.Errorf("failed to open storage trie for deletion: %w", err)
}
storageIt, err := storageTrie.NodeIterator(nil)
if err != nil {
return fmt.Errorf("failed to create storage iterator for deletion: %w", err)
}
for storageIt.Next(true) {
if isPathDB {
rawdb.DeleteStorageTrieNode(batch, addrHash, storageIt.Path())
} else {
node := storageIt.Hash()
if node != (common.Hash{}) {
rawdb.DeleteLegacyTrieNode(batch, node)
}
}
deleted++
if batch.ValueSize() >= ethdb.IdealBatchSize {
if err := batch.Write(); err != nil {
return fmt.Errorf("batch write failed: %w", err)
}
batch.Reset()
}
}
if storageIt.Error() != nil {
return fmt.Errorf("storage deletion iterator error: %w", storageIt.Error())
}
}
}
if batch.ValueSize() >= ethdb.IdealBatchSize {
if err := batch.Write(); err != nil {
return fmt.Errorf("batch write failed: %w", err)
}
batch.Reset()
}
}
if acctIt.Error() != nil {
return fmt.Errorf("account deletion iterator error: %w", acctIt.Error())
}
if batch.ValueSize() > 0 {
if err := batch.Write(); err != nil {
return fmt.Errorf("final batch write failed: %w", err)
}
}
log.Info("MPT deletion complete", "nodesDeleted", deleted)
return nil
}

View file

@ -0,0 +1,229 @@
// Copyright 2026 The go-ethereum Authors
// This file is part of go-ethereum.
//
// go-ethereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// go-ethereum 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"math"
"math/big"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/trie/bintrie"
"github.com/ethereum/go-ethereum/triedb"
"github.com/ethereum/go-ethereum/triedb/pathdb"
"github.com/holiman/uint256"
)
func TestBintrieConvert(t *testing.T) {
var (
addr1 = common.HexToAddress("0x1111111111111111111111111111111111111111")
addr2 = common.HexToAddress("0x2222222222222222222222222222222222222222")
slotKey1 = common.HexToHash("0x01")
slotKey2 = common.HexToHash("0x02")
slotVal1 = common.HexToHash("0xdeadbeef")
slotVal2 = common.HexToHash("0xcafebabe")
code = []byte{0x60, 0x42, 0x60, 0x00, 0x52, 0x60, 0x20, 0x60, 0x00, 0xf3}
)
chaindb := rawdb.NewMemoryDatabase()
srcTriedb := triedb.NewDatabase(chaindb, &triedb.Config{
Preimages: true,
PathDB: pathdb.Defaults,
})
gspec := &core.Genesis{
Config: params.TestChainConfig,
BaseFee: big.NewInt(params.InitialBaseFee),
Alloc: types.GenesisAlloc{
addr1: {
Balance: big.NewInt(1000000),
Nonce: 5,
},
addr2: {
Balance: big.NewInt(2000000),
Nonce: 10,
Code: code,
Storage: map[common.Hash]common.Hash{
slotKey1: slotVal1,
slotKey2: slotVal2,
},
},
},
}
genesisBlock := gspec.MustCommit(chaindb, srcTriedb)
root := genesisBlock.Root()
t.Logf("Genesis root: %x", root)
srcTriedb.Close()
srcTriedb2 := triedb.NewDatabase(chaindb, &triedb.Config{
Preimages: true,
PathDB: &pathdb.Config{ReadOnly: true},
})
defer srcTriedb2.Close()
destTriedb := triedb.NewDatabase(chaindb, &triedb.Config{
IsVerkle: true,
PathDB: pathdb.Defaults,
})
defer destTriedb.Close()
bt, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, destTriedb)
if err != nil {
t.Fatalf("failed to create binary trie: %v", err)
}
currentRoot, err := runConversionLoop(chaindb, srcTriedb2, destTriedb, bt, root, math.MaxUint64)
if err != nil {
t.Fatalf("conversion failed: %v", err)
}
t.Logf("Binary trie root: %x", currentRoot)
bt2, err := bintrie.NewBinaryTrie(currentRoot, destTriedb)
if err != nil {
t.Fatalf("failed to reload binary trie: %v", err)
}
acc1, err := bt2.GetAccount(addr1)
if err != nil {
t.Fatalf("failed to get account1: %v", err)
}
if acc1 == nil {
t.Fatal("account1 not found in binary trie")
}
if acc1.Nonce != 5 {
t.Errorf("account1 nonce: got %d, want 5", acc1.Nonce)
}
wantBal1 := uint256.NewInt(1000000)
if acc1.Balance.Cmp(wantBal1) != 0 {
t.Errorf("account1 balance: got %s, want %s", acc1.Balance, wantBal1)
}
acc2, err := bt2.GetAccount(addr2)
if err != nil {
t.Fatalf("failed to get account2: %v", err)
}
if acc2 == nil {
t.Fatal("account2 not found in binary trie")
}
if acc2.Nonce != 10 {
t.Errorf("account2 nonce: got %d, want 10", acc2.Nonce)
}
wantBal2 := uint256.NewInt(2000000)
if acc2.Balance.Cmp(wantBal2) != 0 {
t.Errorf("account2 balance: got %s, want %s", acc2.Balance, wantBal2)
}
treeKey1 := bintrie.GetBinaryTreeKeyStorageSlot(addr2, slotKey1[:])
val1, err := bt2.GetWithHashedKey(treeKey1)
if err != nil {
t.Fatalf("failed to get storage slot1: %v", err)
}
if len(val1) == 0 {
t.Fatal("storage slot1 not found")
}
got1 := common.BytesToHash(val1)
if got1 != slotVal1 {
t.Errorf("storage slot1: got %x, want %x", got1, slotVal1)
}
treeKey2 := bintrie.GetBinaryTreeKeyStorageSlot(addr2, slotKey2[:])
val2, err := bt2.GetWithHashedKey(treeKey2)
if err != nil {
t.Fatalf("failed to get storage slot2: %v", err)
}
if len(val2) == 0 {
t.Fatal("storage slot2 not found")
}
got2 := common.BytesToHash(val2)
if got2 != slotVal2 {
t.Errorf("storage slot2: got %x, want %x", got2, slotVal2)
}
}
func TestBintrieConvertDeleteSource(t *testing.T) {
addr1 := common.HexToAddress("0x3333333333333333333333333333333333333333")
chaindb := rawdb.NewMemoryDatabase()
srcTriedb := triedb.NewDatabase(chaindb, &triedb.Config{
Preimages: true,
PathDB: pathdb.Defaults,
})
gspec := &core.Genesis{
Config: params.TestChainConfig,
BaseFee: big.NewInt(params.InitialBaseFee),
Alloc: types.GenesisAlloc{
addr1: {
Balance: big.NewInt(1000000),
},
},
}
genesisBlock := gspec.MustCommit(chaindb, srcTriedb)
root := genesisBlock.Root()
srcTriedb.Close()
srcTriedb2 := triedb.NewDatabase(chaindb, &triedb.Config{
Preimages: true,
PathDB: &pathdb.Config{ReadOnly: true},
})
destTriedb := triedb.NewDatabase(chaindb, &triedb.Config{
IsVerkle: true,
PathDB: pathdb.Defaults,
})
bt, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, destTriedb)
if err != nil {
t.Fatalf("failed to create binary trie: %v", err)
}
newRoot, err := runConversionLoop(chaindb, srcTriedb2, destTriedb, bt, root, math.MaxUint64)
if err != nil {
t.Fatalf("conversion failed: %v", err)
}
if err := deleteMPTData(chaindb, srcTriedb2, root); err != nil {
t.Fatalf("deletion failed: %v", err)
}
srcTriedb2.Close()
bt2, err := bintrie.NewBinaryTrie(newRoot, destTriedb)
if err != nil {
t.Fatalf("failed to reload binary trie after deletion: %v", err)
}
acc, err := bt2.GetAccount(addr1)
if err != nil {
t.Fatalf("failed to get account after deletion: %v", err)
}
if acc == nil {
t.Fatal("account not found after MPT deletion")
}
wantBal := uint256.NewInt(1000000)
if acc.Balance.Cmp(wantBal) != 0 {
t.Errorf("balance after deletion: got %s, want %s", acc.Balance, wantBal)
}
destTriedb.Close()
}

View file

@ -260,6 +260,8 @@ func init() {
utils.ShowDeprecated,
// See snapshot.go
snapshotCommand,
// See bintrie_convert.go
bintrieCommand,
}
if logTestCommand != nil {
app.Commands = append(app.Commands, logTestCommand)

View file

@ -275,12 +275,22 @@ func (beacon *Beacon) verifyHeader(chain consensus.ChainHeaderReader, header, pa
}
}
// Verify the existence / non-existence of Amsterdam-specific header fields
amsterdam := chain.Config().IsAmsterdam(header.Number, header.Time)
if amsterdam && header.SlotNumber == nil {
return errors.New("header is missing slotNumber")
}
if !amsterdam && header.SlotNumber != nil {
return fmt.Errorf("invalid slotNumber: have %d, expected nil", *header.SlotNumber)
if amsterdam {
if header.BlockAccessListHash == nil {
return errors.New("header is missing block access list hash")
}
if header.SlotNumber == nil {
return errors.New("header is missing slotNumber")
}
} else {
if header.BlockAccessListHash != nil {
return fmt.Errorf("invalid block access list hash: have %x, expected nil", *header.BlockAccessListHash)
}
if header.SlotNumber != nil {
return fmt.Errorf("invalid slotNumber: have %d, expected nil", *header.SlotNumber)
}
}
return nil
}

View file

@ -296,6 +296,14 @@ func (bc *BlockChain) GetReceiptsRLP(hash common.Hash) rlp.RawValue {
return rawdb.ReadReceiptsRLP(bc.db, hash, number)
}
func (bc *BlockChain) GetAccessListRLP(hash common.Hash) rlp.RawValue {
number, ok := rawdb.ReadHeaderNumber(bc.db, hash)
if !ok {
return nil
}
return rawdb.ReadAccessListRLP(bc.db, hash, number)
}
// GetUnclesInChain retrieves all the uncles from a given block backwards until
// a specific distance is reached.
func (bc *BlockChain) GetUnclesInChain(block *types.Block, length int) []*types.Header {
@ -468,7 +476,7 @@ func (bc *BlockChain) TxIndexProgress() (TxIndexProgress, error) {
}
// StateIndexProgress returns the historical state indexing progress.
func (bc *BlockChain) StateIndexProgress() (uint64, error) {
func (bc *BlockChain) StateIndexProgress() (uint64, uint64, error) {
return bc.triedb.IndexProgress()
}

View file

@ -41,9 +41,7 @@ func TestSingleMatch(t *testing.T) {
t.Fatalf("Invalid length of matches (got %d, expected 1)", len(matches))
}
if matches[0] != lvIndex {
if len(matches) != 1 {
t.Fatalf("Incorrect match returned (got %d, expected %d)", matches[0], lvIndex)
}
t.Fatalf("Incorrect match returned (got %d, expected %d)", matches[0], lvIndex)
}
}
}

View file

@ -26,6 +26,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus/misc/eip4844"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
@ -605,6 +606,55 @@ func DeleteReceipts(db ethdb.KeyValueWriter, hash common.Hash, number uint64) {
}
}
// HasAccessList verifies the existence of a block access list for a block.
func HasAccessList(db ethdb.Reader, hash common.Hash, number uint64) bool {
has, _ := db.Has(accessListKey(number, hash))
return has
}
// ReadAccessListRLP retrieves the RLP-encoded block access list for a block from KV.
func ReadAccessListRLP(db ethdb.Reader, hash common.Hash, number uint64) rlp.RawValue {
data, _ := db.Get(accessListKey(number, hash))
return data
}
// ReadAccessList retrieves and decodes the block access list for a block.
func ReadAccessList(db ethdb.Reader, hash common.Hash, number uint64) *bal.BlockAccessList {
data := ReadAccessListRLP(db, hash, number)
if len(data) == 0 {
return nil
}
b := new(bal.BlockAccessList)
if err := rlp.DecodeBytes(data, b); err != nil {
log.Error("Invalid BAL RLP", "hash", hash, "err", err)
return nil
}
return b
}
// WriteAccessList RLP-encodes and stores a block access list in the active KV store.
func WriteAccessList(db ethdb.KeyValueWriter, hash common.Hash, number uint64, b *bal.BlockAccessList) {
bytes, err := rlp.EncodeToBytes(b)
if err != nil {
log.Crit("Failed to encode BAL", "err", err)
}
WriteAccessListRLP(db, hash, number, bytes)
}
// WriteAccessListRLP stores a pre-encoded block access list in the active KV store.
func WriteAccessListRLP(db ethdb.KeyValueWriter, hash common.Hash, number uint64, encoded rlp.RawValue) {
if err := db.Put(accessListKey(number, hash), encoded); err != nil {
log.Crit("Failed to store BAL", "err", err)
}
}
// DeleteAccessList removes a block access list from the active KV store.
func DeleteAccessList(db ethdb.KeyValueWriter, hash common.Hash, number uint64) {
if err := db.Delete(accessListKey(number, hash)); err != nil {
log.Crit("Failed to delete BAL", "err", err)
}
}
// ReceiptLogs is a barebone version of ReceiptForStorage which only keeps
// the list of logs. When decoding a stored receipt into this object we
// avoid creating the bloom filter.
@ -659,13 +709,25 @@ func ReadBlock(db ethdb.Reader, hash common.Hash, number uint64) *types.Block {
if body == nil {
return nil
}
return types.NewBlockWithHeader(header).WithBody(*body)
block := types.NewBlockWithHeader(header).WithBody(*body)
// Best-effort assembly of the block access list from the database.
if header.BlockAccessListHash != nil {
al := ReadAccessList(db, hash, number)
block = block.WithAccessListUnsafe(al)
}
return block
}
// WriteBlock serializes a block into the database, header and body separately.
func WriteBlock(db ethdb.KeyValueWriter, block *types.Block) {
WriteBody(db, block.Hash(), block.NumberU64(), block.Body())
hash, number := block.Hash(), block.NumberU64()
WriteBody(db, hash, number, block.Body())
WriteHeader(db, block.Header())
if accessList := block.AccessList(); accessList != nil {
WriteAccessList(db, hash, number, accessList)
}
}
// WriteAncientBlocks writes entire block data into ancient store and returns the total written size.

View file

@ -27,10 +27,12 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/keccak"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp"
"github.com/holiman/uint256"
)
// Tests block header storage and retrieval operations.
@ -899,3 +901,78 @@ func TestHeadersRLPStorage(t *testing.T) {
checkSequence(1, 1) // Only block 1
checkSequence(1, 2) // Genesis + block 1
}
func makeTestBAL(t *testing.T) (rlp.RawValue, *bal.BlockAccessList) {
t.Helper()
cb := bal.NewConstructionBlockAccessList()
addr := common.HexToAddress("0x1111111111111111111111111111111111111111")
cb.AccountRead(addr)
cb.StorageRead(addr, common.BytesToHash([]byte{0x01}))
cb.StorageWrite(0, addr, common.BytesToHash([]byte{0x02}), common.BytesToHash([]byte{0xaa}))
cb.BalanceChange(0, addr, uint256.NewInt(100))
cb.NonceChange(addr, 0, 1)
var buf bytes.Buffer
if err := cb.EncodeRLP(&buf); err != nil {
t.Fatalf("failed to encode BAL: %v", err)
}
encoded := buf.Bytes()
var decoded bal.BlockAccessList
if err := rlp.DecodeBytes(encoded, &decoded); err != nil {
t.Fatalf("failed to decode BAL: %v", err)
}
return encoded, &decoded
}
// TestBALStorage tests write/read/delete of BALs in the KV store.
func TestBALStorage(t *testing.T) {
db := NewMemoryDatabase()
hash := common.BytesToHash([]byte{0x03, 0x14})
number := uint64(42)
// Check that no BAL exists in a new database.
if HasAccessList(db, hash, number) {
t.Fatal("BAL found in new database")
}
if b := ReadAccessList(db, hash, number); b != nil {
t.Fatalf("non existent BAL returned: %v", b)
}
// Write a BAL and verify it can be read back.
encoded, testBAL := makeTestBAL(t)
WriteAccessList(db, hash, number, testBAL)
if !HasAccessList(db, hash, number) {
t.Fatal("HasAccessList returned false after write")
}
if blob := ReadAccessListRLP(db, hash, number); len(blob) == 0 {
t.Fatal("ReadAccessListRLP returned empty after write")
}
if b := ReadAccessList(db, hash, number); b == nil {
t.Fatal("ReadAccessList returned nil after write")
} else if b.Hash() != testBAL.Hash() {
t.Fatalf("BAL hash mismatch: got %x, want %x", b.Hash(), testBAL.Hash())
}
// Also test WriteAccessListRLP with pre-encoded data.
hash2 := common.BytesToHash([]byte{0x03, 0x15})
WriteAccessListRLP(db, hash2, number, encoded)
if b := ReadAccessList(db, hash2, number); b == nil {
t.Fatal("ReadAccessList returned nil after WriteAccessListRLP")
} else if b.Hash() != testBAL.Hash() {
t.Fatalf("BAL hash mismatch after WriteAccessListRLP: got %x, want %x", b.Hash(), testBAL.Hash())
}
// Delete the BAL and verify it's gone.
DeleteAccessList(db, hash, number)
if HasAccessList(db, hash, number) {
t.Fatal("HasAccessList returned true after delete")
}
if b := ReadAccessList(db, hash, number); b != nil {
t.Fatalf("deleted BAL returned: %v", b)
}
}

View file

@ -413,6 +413,7 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error {
tds stat
numHashPairings stat
hashNumPairings stat
blockAccessList stat
legacyTries stat
stateLookups stat
accountTries stat
@ -484,6 +485,9 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error {
numHashPairings.add(size)
case bytes.HasPrefix(key, headerNumberPrefix) && len(key) == (len(headerNumberPrefix)+common.HashLength):
hashNumPairings.add(size)
case bytes.HasPrefix(key, accessListPrefix) && len(key) == len(accessListPrefix)+8+common.HashLength:
blockAccessList.add(size)
case IsLegacyTrieNode(key, it.Value()):
legacyTries.add(size)
case bytes.HasPrefix(key, stateIDPrefix) && len(key) == len(stateIDPrefix)+common.HashLength:
@ -625,6 +629,7 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error {
{"Key-Value store", "Difficulties (deprecated)", tds.sizeString(), tds.countString()},
{"Key-Value store", "Block number->hash", numHashPairings.sizeString(), numHashPairings.countString()},
{"Key-Value store", "Block hash->number", hashNumPairings.sizeString(), hashNumPairings.countString()},
{"Key-Value store", "Block accessList", blockAccessList.sizeString(), blockAccessList.countString()},
{"Key-Value store", "Transaction index", txLookups.sizeString(), txLookups.countString()},
{"Key-Value store", "Log index filter-map rows", filterMapRows.sizeString(), filterMapRows.countString()},
{"Key-Value store", "Log index last-block-of-map", filterMapLastBlock.sizeString(), filterMapLastBlock.countString()},

View file

@ -112,6 +112,7 @@ var (
blockBodyPrefix = []byte("b") // blockBodyPrefix + num (uint64 big endian) + hash -> block body
blockReceiptsPrefix = []byte("r") // blockReceiptsPrefix + num (uint64 big endian) + hash -> block receipts
accessListPrefix = []byte("j") // accessListPrefix + num (uint64 big endian) + hash -> block access list
txLookupPrefix = []byte("l") // txLookupPrefix + hash -> transaction/receipt lookup metadata
bloomBitsPrefix = []byte("B") // bloomBitsPrefix + bit (uint16 big endian) + section (uint64 big endian) + hash -> bloom bits
@ -214,6 +215,11 @@ func blockReceiptsKey(number uint64, hash common.Hash) []byte {
return append(append(blockReceiptsPrefix, encodeBlockNumber(number)...), hash.Bytes()...)
}
// accessListKey = accessListPrefix + num (uint64 big endian) + hash
func accessListKey(number uint64, hash common.Hash) []byte {
return append(append(accessListPrefix, encodeBlockNumber(number)...), hash.Bytes()...)
}
// txLookupKey = txLookupPrefix + hash
func txLookupKey(hash common.Hash) []byte {
return append(txLookupPrefix, hash.Bytes()...)

View file

@ -39,6 +39,10 @@ type Database interface {
// Reader returns a state reader associated with the specified state root.
Reader(root common.Hash) (Reader, error)
// Iteratee returns a state iteratee associated with the specified state root,
// through which the account iterator and storage iterator can be created.
Iteratee(root common.Hash) (Iteratee, error)
// OpenTrie opens the main account trie.
OpenTrie(root common.Hash) (Trie, error)
@ -48,9 +52,6 @@ type Database interface {
// TrieDB returns the underlying trie database for managing trie nodes.
TrieDB() *triedb.Database
// Snapshot returns the underlying state snapshot.
Snapshot() *snapshot.Tree
// Commit flushes all pending writes and finalizes the state transition,
// committing the changes to the underlying storage. It returns an error
// if the commit fails.
@ -310,6 +311,12 @@ func (db *CachingDB) Commit(update *stateUpdate) error {
return db.triedb.Update(update.root, update.originRoot, update.blockNumber, update.nodes, update.stateSet())
}
// Iteratee returns a state iteratee associated with the specified state root,
// through which the account iterator and storage iterator can be created.
func (db *CachingDB) Iteratee(root common.Hash) (Iteratee, error) {
return newStateIteratee(!db.triedb.IsVerkle(), root, db.triedb, db.snap)
}
// mustCopyTrie returns a deep-copied trie.
func mustCopyTrie(t Trie) Trie {
switch t := t.(type) {

View file

@ -22,7 +22,6 @@ import (
"sync"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/state/snapshot"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/rlp"
@ -289,14 +288,15 @@ func (db *HistoricDB) TrieDB() *triedb.Database {
return db.triedb
}
// Snapshot returns the underlying state snapshot.
func (db *HistoricDB) Snapshot() *snapshot.Tree {
return nil
}
// Commit flushes all pending writes and finalizes the state transition,
// committing the changes to the underlying storage. It returns an error
// if the commit fails.
func (db *HistoricDB) Commit(update *stateUpdate) error {
return errors.New("not implemented")
}
// Iteratee returns a state iteratee associated with the specified state root,
// through which the account iterator and storage iterator can be created.
func (db *HistoricDB) Iteratee(root common.Hash) (Iteratee, error) {
return nil, errors.New("not implemented")
}

View file

@ -0,0 +1,435 @@
// Copyright 2025 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/>.
package state
import (
"errors"
"fmt"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/state/snapshot"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/trie"
"github.com/ethereum/go-ethereum/triedb"
)
// Iterator is an iterator to step over all the accounts or the specific
// storage in the specific state.
type Iterator interface {
// Next steps the iterator forward one element. It returns false if the iterator
// is exhausted or if an error occurs. Any error encountered is retained and
// can be retrieved via Error().
Next() bool
// Error returns any failure that occurred during iteration, which might have
// caused a premature iteration exit.
Error() error
// Hash returns the hash of the account or storage slot the iterator is
// currently at.
Hash() common.Hash
// Release releases associated resources. Release should always succeed and
// can be called multiple times without causing error.
Release()
}
// AccountIterator is an iterator to step over all the accounts in the
// specific state.
type AccountIterator interface {
Iterator
// Address returns the raw account address the iterator is currently at.
// An error will be returned if the preimage is not available.
Address() (common.Address, error)
// Account returns the RLP encoded account the iterator is currently at.
// An error will be retained if the iterator becomes invalid.
Account() []byte
}
// StorageIterator is an iterator to step over the specific storage in the
// specific state.
type StorageIterator interface {
Iterator
// Key returns the raw storage slot key the iterator is currently at.
// An error will be returned if the preimage is not available.
Key() (common.Hash, error)
// Slot returns the storage slot the iterator is currently at. An error will
// be retained if the iterator becomes invalid.
Slot() []byte
}
// Iteratee wraps the NewIterator methods for traversing the accounts and
// storages of the specific state.
type Iteratee interface {
// NewAccountIterator creates an account iterator for the state specified by
// the given root. It begins at a specified starting position, corresponding
// to a particular initial key (or the next key if the specified one does
// not exist).
//
// The starting position here refers to the hash of the account address.
NewAccountIterator(start common.Hash) (AccountIterator, error)
// NewStorageIterator creates a storage iterator for the state specified by
// the address hash. It begins at a specified starting position, corresponding
// to a particular initial key (or the next key if the specified one does
// not exist).
//
// The starting position here refers to the hash of the slot key.
NewStorageIterator(addressHash common.Hash, start common.Hash) (StorageIterator, error)
}
// PreimageReader wraps the function Preimage for accessing the preimage of
// a given hash.
type PreimageReader interface {
// Preimage returns the preimage of associated hash.
Preimage(hash common.Hash) []byte
}
// flatAccountIterator is a wrapper around the underlying flat state iterator.
// Before returning data from the iterator, it performs an additional conversion
// to bridge the slim encoding with the full encoding format.
type flatAccountIterator struct {
err error
it snapshot.AccountIterator
preimage PreimageReader
}
// newFlatAccountIterator constructs the account iterator with the provided
// flat state iterator.
func newFlatAccountIterator(it snapshot.AccountIterator, preimage PreimageReader) *flatAccountIterator {
return &flatAccountIterator{it: it, preimage: preimage}
}
// Next steps the iterator forward one element. It returns false if the iterator
// is exhausted or if an error occurs. Any error encountered is retained and
// can be retrieved via Error().
func (ai *flatAccountIterator) Next() bool {
if ai.err != nil {
return false
}
return ai.it.Next()
}
// Error returns any failure that occurred during iteration, which might have
// caused a premature iteration exit.
func (ai *flatAccountIterator) Error() error {
if ai.err != nil {
return ai.err
}
return ai.it.Error()
}
// Hash returns the hash of the account or storage slot the iterator is
// currently at.
func (ai *flatAccountIterator) Hash() common.Hash {
return ai.it.Hash()
}
// Release releases associated resources. Release should always succeed and
// can be called multiple times without causing error.
func (ai *flatAccountIterator) Release() {
ai.it.Release()
}
// Address returns the raw account address the iterator is currently at.
// An error will be returned if the preimage is not available.
func (ai *flatAccountIterator) Address() (common.Address, error) {
if ai.preimage == nil {
return common.Address{}, errors.New("account address is not available")
}
preimage := ai.preimage.Preimage(ai.Hash())
if preimage == nil {
return common.Address{}, errors.New("account address is not available")
}
return common.BytesToAddress(preimage), nil
}
// Account returns the account data the iterator is currently at. The account
// data is encoded as slim format from the underlying iterator, the conversion
// is required.
func (ai *flatAccountIterator) Account() []byte {
data, err := types.FullAccountRLP(ai.it.Account())
if err != nil {
ai.err = err
return nil
}
return data
}
// flatStorageIterator is a wrapper around the underlying flat state iterator.
type flatStorageIterator struct {
it snapshot.StorageIterator
preimage PreimageReader
}
// newFlatStorageIterator constructs the storage iterator with the provided
// flat state iterator.
func newFlatStorageIterator(it snapshot.StorageIterator, preimage PreimageReader) *flatStorageIterator {
return &flatStorageIterator{it: it, preimage: preimage}
}
// Next steps the iterator forward one element. It returns false if the iterator
// is exhausted or if an error occurs. Any error encountered is retained and
// can be retrieved via Error().
func (si *flatStorageIterator) Next() bool {
return si.it.Next()
}
// Error returns any failure that occurred during iteration, which might have
// caused a premature iteration exit.
func (si *flatStorageIterator) Error() error {
return si.it.Error()
}
// Hash returns the hash of the account or storage slot the iterator is
// currently at.
func (si *flatStorageIterator) Hash() common.Hash {
return si.it.Hash()
}
// Release releases associated resources. Release should always succeed and
// can be called multiple times without causing error.
func (si *flatStorageIterator) Release() {
si.it.Release()
}
// Key returns the raw storage slot key the iterator is currently at.
// An error will be returned if the preimage is not available.
func (si *flatStorageIterator) Key() (common.Hash, error) {
if si.preimage == nil {
return common.Hash{}, errors.New("slot key is not available")
}
preimage := si.preimage.Preimage(si.Hash())
if preimage == nil {
return common.Hash{}, errors.New("slot key is not available")
}
return common.BytesToHash(preimage), nil
}
// Slot returns the storage slot data the iterator is currently at.
func (si *flatStorageIterator) Slot() []byte {
return si.it.Slot()
}
// merkleIterator implements the Iterator interface, providing functions to traverse
// the accounts or storages with the manner of Merkle-Patricia-Trie.
type merkleIterator struct {
tr Trie
it *trie.Iterator
account bool
}
// newMerkleTrieIterator constructs the iterator with the given trie and starting position.
func newMerkleTrieIterator(tr Trie, start common.Hash, account bool) (*merkleIterator, error) {
it, err := tr.NodeIterator(start.Bytes())
if err != nil {
return nil, err
}
return &merkleIterator{
tr: tr,
it: trie.NewIterator(it),
account: account,
}, nil
}
// Next steps the iterator forward one element. It returns false if the iterator
// is exhausted or if an error occurs. Any error encountered is retained and
// can be retrieved via Error().
func (ti *merkleIterator) Next() bool {
return ti.it.Next()
}
// Error returns any failure that occurred during iteration, which might have
// caused a premature iteration exit.
func (ti *merkleIterator) Error() error {
return ti.it.Err
}
// Hash returns the hash of the account or storage slot the iterator is
// currently at.
func (ti *merkleIterator) Hash() common.Hash {
return common.BytesToHash(ti.it.Key)
}
// Release releases associated resources. Release should always succeed and
// can be called multiple times without causing error.
func (ti *merkleIterator) Release() {}
// Address returns the raw account address the iterator is currently at.
// An error will be returned if the preimage is not available.
func (ti *merkleIterator) Address() (common.Address, error) {
if !ti.account {
return common.Address{}, errors.New("account address is not available")
}
preimage := ti.tr.GetKey(ti.it.Key)
if preimage == nil {
return common.Address{}, errors.New("account address is not available")
}
return common.BytesToAddress(preimage), nil
}
// Account returns the account data the iterator is currently at.
func (ti *merkleIterator) Account() []byte {
if !ti.account {
return nil
}
return ti.it.Value
}
// Key returns the raw storage slot key the iterator is currently at.
// An error will be returned if the preimage is not available.
func (ti *merkleIterator) Key() (common.Hash, error) {
if ti.account {
return common.Hash{}, errors.New("slot key is not available")
}
preimage := ti.tr.GetKey(ti.it.Key)
if preimage == nil {
return common.Hash{}, errors.New("slot key is not available")
}
return common.BytesToHash(preimage), nil
}
// Slot returns the storage slot the iterator is currently at.
func (ti *merkleIterator) Slot() []byte {
if ti.account {
return nil
}
return ti.it.Value
}
// stateIteratee implements Iteratee interface, providing the state traversal
// functionalities of a specific state.
type stateIteratee struct {
merkle bool
root common.Hash
triedb *triedb.Database
snap *snapshot.Tree
}
func newStateIteratee(merkle bool, root common.Hash, triedb *triedb.Database, snap *snapshot.Tree) (*stateIteratee, error) {
return &stateIteratee{
merkle: merkle,
root: root,
triedb: triedb,
snap: snap,
}, nil
}
// NewAccountIterator creates an account iterator for the state specified by
// the given root. It begins at a specified starting position, corresponding
// to a particular initial key (or the next key if the specified one does
// not exist).
//
// The starting position here refers to the hash of the account address.
func (si *stateIteratee) NewAccountIterator(start common.Hash) (AccountIterator, error) {
// If the external snapshot is available (hash scheme), try to initialize
// the account iterator from there first.
if si.snap != nil {
it, err := si.snap.AccountIterator(si.root, start)
if err == nil {
return newFlatAccountIterator(it, si.triedb), nil
}
}
// If the external snapshot is not available, try to initialize the
// account iterator from the trie database (path scheme)
it, err := si.triedb.AccountIterator(si.root, start)
if err == nil {
return newFlatAccountIterator(it, si.triedb), nil
}
if !si.merkle {
return nil, fmt.Errorf("state %x is not available for account traversal", si.root)
}
// The snapshot is not usable so far, construct the account iterator from
// the trie as the fallback. It's not as efficient as the flat state iterator.
tr, err := trie.NewStateTrie(trie.StateTrieID(si.root), si.triedb)
if err != nil {
return nil, err
}
return newMerkleTrieIterator(tr, start, true)
}
// NewStorageIterator creates a storage iterator for the state specified by
// the address hash. It begins at a specified starting position, corresponding
// to a particular initial key (or the next key if the specified one does not exist).
//
// The starting position here refers to the hash of the slot key.
func (si *stateIteratee) NewStorageIterator(addressHash common.Hash, start common.Hash) (StorageIterator, error) {
// If the external snapshot is available (hash scheme), try to initialize
// the storage iterator from there first.
if si.snap != nil {
it, err := si.snap.StorageIterator(si.root, addressHash, start)
if err == nil {
return newFlatStorageIterator(it, si.triedb), nil
}
}
// If the external snapshot is not available, try to initialize the
// storage iterator from the trie database (path scheme)
it, err := si.triedb.StorageIterator(si.root, addressHash, start)
if err == nil {
return newFlatStorageIterator(it, si.triedb), nil
}
if !si.merkle {
return nil, fmt.Errorf("state %x is not available for storage traversal", si.root)
}
// The snapshot is not usable so far, construct the storage iterator from
// the trie as the fallback. It's not as efficient as the flat state iterator.
tr, err := trie.NewStateTrie(trie.StateTrieID(si.root), si.triedb)
if err != nil {
return nil, err
}
acct, err := tr.GetAccountByHash(addressHash)
if err != nil {
return nil, err
}
if acct == nil || acct.Root == types.EmptyRootHash {
return &exhaustedIterator{}, nil
}
storageTr, err := trie.NewStateTrie(trie.StorageTrieID(si.root, addressHash, acct.Root), si.triedb)
if err != nil {
return nil, err
}
return newMerkleTrieIterator(storageTr, start, false)
}
type exhaustedIterator struct{}
func (e exhaustedIterator) Next() bool {
return false
}
func (e exhaustedIterator) Error() error {
return nil
}
func (e exhaustedIterator) Hash() common.Hash {
return common.Hash{}
}
func (e exhaustedIterator) Release() {
}
func (e exhaustedIterator) Key() (common.Hash, error) {
return common.Hash{}, nil
}
func (e exhaustedIterator) Slot() []byte {
return nil
}

View file

@ -0,0 +1,262 @@
// Copyright 2026 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/>.
package state
import (
"bytes"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie"
)
// TestExhaustedIterator verifies the exhaustedIterator sentinel: Next is false,
// Error is nil, Hash/Key are zero, Slot is nil, and double Release is safe.
func TestExhaustedIterator(t *testing.T) {
var it exhaustedIterator
if it.Next() {
t.Fatal("Next() returned true")
}
if err := it.Error(); err != nil {
t.Fatalf("Error() = %v, want nil", err)
}
if hash := it.Hash(); hash != (common.Hash{}) {
t.Fatalf("Hash() = %x, want zero", hash)
}
if key, err := it.Key(); key != (common.Hash{}) || err != nil {
t.Fatalf("Key() = %x, %v; want zero, nil", key, err)
}
if slot := it.Slot(); slot != nil {
t.Fatalf("Slot() = %x, want nil", slot)
}
it.Release()
it.Release()
}
// TestAccountIterator tests the account iterator: correct count, ascending
// hash order, valid full-format RLP, data integrity, address preimage
// resolution, and seek behavior.
func TestAccountIterator(t *testing.T) {
testAccountIterator(t, rawdb.HashScheme)
testAccountIterator(t, rawdb.PathScheme)
}
func testAccountIterator(t *testing.T, scheme string) {
_, sdb, ndb, root, accounts := makeTestState(scheme)
ndb.Commit(root, false)
iteratee, err := sdb.Iteratee(root)
if err != nil {
t.Fatalf("(%s) failed to create iteratee: %v", scheme, err)
}
// Build lookups from address hash.
addrByHash := make(map[common.Hash]*testAccount)
for _, acc := range accounts {
addrByHash[crypto.Keccak256Hash(acc.address.Bytes())] = acc
}
// --- Full iteration: count, ordering, RLP validity, data integrity, address resolution ---
acctIt, err := iteratee.NewAccountIterator(common.Hash{})
if err != nil {
t.Fatalf("(%s) failed to create account iterator: %v", scheme, err)
}
var (
hashes []common.Hash
prevHash common.Hash
)
for acctIt.Next() {
hash := acctIt.Hash()
if hash == (common.Hash{}) {
t.Fatalf("(%s) zero hash at position %d", scheme, len(hashes))
}
if len(hashes) > 0 && bytes.Compare(prevHash.Bytes(), hash.Bytes()) >= 0 {
t.Fatalf("(%s) hashes not ascending: %x >= %x", scheme, prevHash, hash)
}
prevHash = hash
hashes = append(hashes, hash)
// Decode and verify account data.
blob := acctIt.Account()
if blob == nil {
t.Fatalf("(%s) nil account at %x", scheme, hash)
}
var decoded types.StateAccount
if err := rlp.DecodeBytes(blob, &decoded); err != nil {
t.Fatalf("(%s) bad RLP at %x: %v", scheme, hash, err)
}
acc := addrByHash[hash]
if decoded.Nonce != acc.nonce {
t.Fatalf("(%s) nonce %x: got %d, want %d", scheme, hash, decoded.Nonce, acc.nonce)
}
if decoded.Balance.Cmp(acc.balance) != 0 {
t.Fatalf("(%s) balance %x: got %v, want %v", scheme, hash, decoded.Balance, acc.balance)
}
// Verify address preimage resolution.
addr, err := acctIt.Address()
if err != nil {
t.Fatalf("(%s) failed to address: %v", scheme, err)
}
if addr != acc.address {
t.Fatalf("(%s) Address() = %x, want %x", scheme, addr, acc.address)
}
}
acctIt.Release()
if err := acctIt.Error(); err != nil {
t.Fatalf("(%s) iteration error: %v", scheme, err)
}
if len(hashes) != len(accounts) {
t.Fatalf("(%s) iterated %d accounts, want %d", scheme, len(hashes), len(accounts))
}
// --- Seek: starting from midpoint should skip earlier entries ---
mid := hashes[len(hashes)/2]
seekIt, err := iteratee.NewAccountIterator(mid)
if err != nil {
t.Fatalf("(%s) failed to create seeked iterator: %v", scheme, err)
}
seekCount := 0
for seekIt.Next() {
if bytes.Compare(seekIt.Hash().Bytes(), mid.Bytes()) < 0 {
t.Fatalf("(%s) seeked iterator returned hash before start", scheme)
}
seekCount++
}
seekIt.Release()
if seekCount != len(hashes)/2 {
t.Fatalf("(%s) unexpected seeked count, %d != %d", scheme, seekCount, len(hashes)/2)
}
}
// TestStorageIterator tests the storage iterator: correct slot counts against
// the trie, ascending hash order, non-nil slot data, key preimage resolution,
// seek behavior, and empty-storage accounts.
func TestStorageIterator(t *testing.T) {
testStorageIterator(t, rawdb.HashScheme)
testStorageIterator(t, rawdb.PathScheme)
}
func testStorageIterator(t *testing.T, scheme string) {
_, sdb, ndb, root, accounts := makeTestState(scheme)
ndb.Commit(root, false)
iteratee, err := sdb.Iteratee(root)
if err != nil {
t.Fatalf("(%s) failed to create iteratee: %v", scheme, err)
}
// --- Slot count and ordering for every account ---
var withStorage common.Hash // remember an account that has storage for seek test
for _, acc := range accounts {
addrHash := crypto.Keccak256Hash(acc.address.Bytes())
expected := countStorageSlots(t, scheme, sdb, root, addrHash)
storageIt, err := iteratee.NewStorageIterator(addrHash, common.Hash{})
if err != nil {
t.Fatalf("(%s) failed to create storage iterator for %x: %v", scheme, acc.address, err)
}
count := 0
var prevHash common.Hash
for storageIt.Next() {
hash := storageIt.Hash()
if count > 0 && bytes.Compare(prevHash.Bytes(), hash.Bytes()) >= 0 {
t.Fatalf("(%s) storage hashes not ascending for %x", scheme, acc.address)
}
prevHash = hash
if storageIt.Slot() == nil {
t.Fatalf("(%s) nil slot at %x", scheme, hash)
}
// Check key preimage resolution on first slot.
if _, err := storageIt.Key(); err != nil {
t.Fatalf("(%s) Key() failed to resolve", scheme)
}
count++
}
if err := storageIt.Error(); err != nil {
t.Fatalf("(%s) storage iteration error for %x: %v", scheme, acc.address, err)
}
storageIt.Release()
if count != expected {
t.Fatalf("(%s) account %x: %d slots, want %d", scheme, acc.address, count, expected)
}
if count > 0 {
withStorage = addrHash
}
}
// --- Seek: starting from second slot should skip the first ---
if withStorage == (common.Hash{}) {
t.Fatalf("(%s) no account with storage found", scheme)
}
fullIt, err := iteratee.NewStorageIterator(withStorage, common.Hash{})
if err != nil {
t.Fatalf("(%s) failed to create full storage iterator: %v", scheme, err)
}
var slotHashes []common.Hash
for fullIt.Next() {
slotHashes = append(slotHashes, fullIt.Hash())
}
fullIt.Release()
seekIt, err := iteratee.NewStorageIterator(withStorage, slotHashes[1])
if err != nil {
t.Fatalf("(%s) failed to create seeked storage iterator: %v", scheme, err)
}
seekCount := 0
for seekIt.Next() {
if bytes.Compare(seekIt.Hash().Bytes(), slotHashes[1].Bytes()) < 0 {
t.Fatalf("(%s) seeked storage iterator returned hash before start", scheme)
}
seekCount++
}
seekIt.Release()
if seekCount != len(slotHashes)-1 {
t.Fatalf("(%s) unexpected seeked storage count %d != %d", scheme, seekCount, len(slotHashes)-1)
}
}
// countStorageSlots counts storage slots for an account by opening the
// storage trie directly.
func countStorageSlots(t *testing.T, scheme string, sdb Database, root common.Hash, addrHash common.Hash) int {
t.Helper()
accTrie, err := trie.NewStateTrie(trie.StateTrieID(root), sdb.TrieDB())
if err != nil {
t.Fatalf("(%s) failed to open account trie: %v", scheme, err)
}
acct, err := accTrie.GetAccountByHash(addrHash)
if err != nil || acct == nil || acct.Root == types.EmptyRootHash {
return 0
}
storageTrie, err := trie.NewStateTrie(trie.StorageTrieID(root, addrHash, acct.Root), sdb.TrieDB())
if err != nil {
t.Fatalf("(%s) failed to open storage trie for %x: %v", scheme, addrHash, err)
}
it := trie.NewIterator(storageTrie.MustNodeIterator(nil))
count := 0
for it.Next() {
count++
}
return count
}

View file

@ -27,7 +27,6 @@ import (
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie"
"github.com/ethereum/go-ethereum/trie/bintrie"
)
@ -45,6 +44,7 @@ type DumpConfig struct {
type DumpCollector interface {
// OnRoot is called with the state root
OnRoot(common.Hash)
// OnAccount is called once for each account in the trie
OnAccount(*common.Address, DumpAccount)
}
@ -65,9 +65,10 @@ type DumpAccount struct {
type Dump struct {
Root string `json:"root"`
Accounts map[string]DumpAccount `json:"accounts"`
// Next can be set to represent that this dump is only partial, and Next
// is where an iterator should be positioned in order to continue the dump.
Next []byte `json:"next,omitempty"` // nil if no more accounts
Next hexutil.Bytes `json:"next,omitempty"` // nil if no more accounts
}
// OnRoot implements DumpCollector interface
@ -114,9 +115,6 @@ func (d iterativeDump) OnRoot(root common.Hash) {
// DumpToCollector iterates the state according to the given options and inserts
// the items into a collector for aggregation or serialization.
//
// The state iterator is still trie-based and can be converted to snapshot-based
// once the state snapshot is fully integrated into database. TODO(rjl493456442).
func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey []byte) {
// Sanitize the input to allow nil configs
if conf == nil {
@ -131,20 +129,23 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey []
log.Info("Trie dumping started", "root", s.originalRoot)
c.OnRoot(s.originalRoot)
tr, err := s.db.OpenTrie(s.originalRoot)
iteratee, err := s.db.Iteratee(s.originalRoot)
if err != nil {
return nil
}
trieIt, err := tr.NodeIterator(conf.Start)
var startHash common.Hash
if conf.Start != nil {
startHash = common.BytesToHash(conf.Start)
}
acctIt, err := iteratee.NewAccountIterator(startHash)
if err != nil {
log.Error("Trie dumping error", "err", err)
return nil
}
it := trie.NewIterator(trieIt)
defer acctIt.Release()
for it.Next() {
for acctIt.Next() {
var data types.StateAccount
if err := rlp.DecodeBytes(it.Value, &data); err != nil {
if err := rlp.DecodeBytes(acctIt.Account(), &data); err != nil {
panic(err)
}
var (
@ -153,63 +154,55 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey []
Nonce: data.Nonce,
Root: data.Root[:],
CodeHash: data.CodeHash,
AddressHash: it.Key,
AddressHash: acctIt.Hash().Bytes(),
}
address *common.Address
addr common.Address
addrBytes = tr.GetKey(it.Key)
address *common.Address
)
if addrBytes == nil {
addrBytes, err := acctIt.Address()
if err != nil {
missingPreimages++
if conf.OnlyWithAddresses {
continue
}
} else {
addr = common.BytesToAddress(addrBytes)
address = &addr
address = &addrBytes
account.Address = address
}
obj := newObject(s, addr, &data)
obj := newObject(s, addrBytes, &data)
if !conf.SkipCode {
account.Code = obj.Code()
}
if !conf.SkipStorage {
account.Storage = make(map[common.Hash]string)
storageTr, err := s.db.OpenStorageTrie(s.originalRoot, addr, obj.Root(), tr)
storageIt, err := iteratee.NewStorageIterator(acctIt.Hash(), common.Hash{})
if err != nil {
log.Error("Failed to load storage trie", "err", err)
continue
}
trieIt, err := storageTr.NodeIterator(nil)
if err != nil {
log.Error("Failed to create trie iterator", "err", err)
continue
}
storageIt := trie.NewIterator(trieIt)
for storageIt.Next() {
_, content, _, err := rlp.Split(storageIt.Value)
_, content, _, err := rlp.Split(storageIt.Slot())
if err != nil {
log.Error("Failed to decode the value returned by iterator", "error", err)
continue
}
key := storageTr.GetKey(storageIt.Key)
if key == nil {
key, err := storageIt.Key()
if err != nil {
continue
}
account.Storage[common.BytesToHash(key)] = common.Bytes2Hex(content)
account.Storage[key] = common.Bytes2Hex(content)
}
storageIt.Release()
}
c.OnAccount(address, account)
accounts++
if time.Since(logged) > 8*time.Second {
log.Info("Trie dumping in progress", "at", common.Bytes2Hex(it.Key), "accounts", accounts,
"elapsed", common.PrettyDuration(time.Since(start)))
log.Info("Trie dumping in progress", "at", acctIt.Hash().Hex(), "accounts", accounts, "elapsed", common.PrettyDuration(time.Since(start)))
logged = time.Now()
}
if conf.Max > 0 && accounts >= conf.Max {
if it.Next() {
nextKey = it.Key
if acctIt.Next() {
nextKey = acctIt.Hash().Bytes()
}
break
}
@ -217,9 +210,7 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey []
if missingPreimages > 0 {
log.Warn("Dump incomplete due to missing preimages", "missing", missingPreimages)
}
log.Info("Trie dumping complete", "accounts", accounts,
"elapsed", common.PrettyDuration(time.Since(start)))
log.Info("Trie dumping complete", "accounts", accounts, "elapsed", common.PrettyDuration(time.Since(start)))
return nextKey
}

View file

@ -28,7 +28,6 @@ import (
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/state/snapshot"
"github.com/ethereum/go-ethereum/core/stateless"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
@ -746,7 +745,7 @@ type removedAccountWithBalance struct {
balance *uint256.Int
}
// EmitLogsForBurnAccounts emits the eth burn logs for accounts scheduled for
// LogsForBurnAccounts returns the eth burn logs for accounts scheduled for
// removal which still have positive balance. The purpose of this function is
// to handle a corner case of EIP-7708 where a self-destructed account might
// still receive funds between sending/burning its previous balance and actual
@ -756,7 +755,7 @@ type removedAccountWithBalance struct {
//
// This function should only be invoked at the transaction boundary, specifically
// before the Finalise.
func (s *StateDB) EmitLogsForBurnAccounts() {
func (s *StateDB) LogsForBurnAccounts() []*types.Log {
var list []removedAccountWithBalance
for addr := range s.journal.dirties {
if obj, exist := s.stateObjects[addr]; exist && obj.selfDestructed && !obj.Balance().IsZero() {
@ -766,14 +765,17 @@ func (s *StateDB) EmitLogsForBurnAccounts() {
})
}
}
if list != nil {
sort.Slice(list, func(i, j int) bool {
return list[i].address.Cmp(list[j].address) < 0
})
if list == nil {
return nil
}
for _, acct := range list {
s.AddLog(types.EthBurnLog(acct.address, acct.balance))
sort.Slice(list, func(i, j int) bool {
return list[i].address.Cmp(list[j].address) < 0
})
logs := make([]*types.Log, len(list))
for i, acct := range list {
logs[i] = types.EthBurnLog(acct.address, acct.balance)
}
return logs
}
// Finalise finalises the state by removing the destructed objects and clears
@ -879,10 +881,12 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
if err := s.trie.UpdateStorage(addr, key[:], common.TrimLeftZeroes(value[:])); err != nil {
s.setError(err)
}
s.StorageUpdated.Add(1)
} else {
if err := s.trie.DeleteStorage(addr, key[:]); err != nil {
s.setError(err)
}
s.StorageDeleted.Add(1)
}
}
}
@ -1037,31 +1041,32 @@ func (s *StateDB) clearJournalAndRefund() {
s.refund = 0
}
// fastDeleteStorage is the function that efficiently deletes the storage trie
// of a specific account. It leverages the associated state snapshot for fast
// storage iteration and constructs trie node deletion markers by creating
// stack trie with iterated slots.
func (s *StateDB) fastDeleteStorage(snaps *snapshot.Tree, addrHash common.Hash, root common.Hash) (map[common.Hash][]byte, map[common.Hash][]byte, *trienode.NodeSet, error) {
iter, err := snaps.StorageIterator(s.originalRoot, addrHash, common.Hash{})
if err != nil {
return nil, nil, nil, err
}
defer iter.Release()
// deleteStorage is designed to delete the storage trie of a designated account.
func (s *StateDB) deleteStorage(addrHash common.Hash, root common.Hash) (map[common.Hash][]byte, map[common.Hash][]byte, *trienode.NodeSet, error) {
var (
nodes = trienode.NewNodeSet(addrHash) // the set for trie node mutations (value is nil)
storages = make(map[common.Hash][]byte) // the set for storage mutations (value is nil)
storageOrigins = make(map[common.Hash][]byte) // the set for tracking the original value of slot
)
iteratee, err := s.db.Iteratee(s.originalRoot)
if err != nil {
return nil, nil, nil, err
}
it, err := iteratee.NewStorageIterator(addrHash, common.Hash{})
if err != nil {
return nil, nil, nil, err
}
defer it.Release()
stack := trie.NewStackTrie(func(path []byte, hash common.Hash, blob []byte) {
nodes.AddNode(path, trienode.NewDeletedWithPrev(blob))
})
for iter.Next() {
slot := common.CopyBytes(iter.Slot())
if err := iter.Error(); err != nil { // error might occur after Slot function
for it.Next() {
slot := common.CopyBytes(it.Slot())
if err := it.Error(); err != nil { // error might occur after Slot function
return nil, nil, nil, err
}
key := iter.Hash()
key := it.Hash()
storages[key] = nil
storageOrigins[key] = slot
@ -1069,7 +1074,7 @@ func (s *StateDB) fastDeleteStorage(snaps *snapshot.Tree, addrHash common.Hash,
return nil, nil, nil, err
}
}
if err := iter.Error(); err != nil { // error might occur during iteration
if err := it.Error(); err != nil { // error might occur during iteration
return nil, nil, nil, err
}
if stack.Hash() != root {
@ -1078,68 +1083,6 @@ func (s *StateDB) fastDeleteStorage(snaps *snapshot.Tree, addrHash common.Hash,
return storages, storageOrigins, nodes, nil
}
// slowDeleteStorage serves as a less-efficient alternative to "fastDeleteStorage,"
// employed when the associated state snapshot is not available. It iterates the
// storage slots along with all internal trie nodes via trie directly.
func (s *StateDB) slowDeleteStorage(addr common.Address, addrHash common.Hash, root common.Hash) (map[common.Hash][]byte, map[common.Hash][]byte, *trienode.NodeSet, error) {
tr, err := s.db.OpenStorageTrie(s.originalRoot, addr, root, s.trie)
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to open storage trie, err: %w", err)
}
it, err := tr.NodeIterator(nil)
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to open storage iterator, err: %w", err)
}
var (
nodes = trienode.NewNodeSet(addrHash) // the set for trie node mutations (value is nil)
storages = make(map[common.Hash][]byte) // the set for storage mutations (value is nil)
storageOrigins = make(map[common.Hash][]byte) // the set for tracking the original value of slot
)
for it.Next(true) {
if it.Leaf() {
key := common.BytesToHash(it.LeafKey())
storages[key] = nil
storageOrigins[key] = common.CopyBytes(it.LeafBlob())
continue
}
if it.Hash() == (common.Hash{}) {
continue
}
nodes.AddNode(it.Path(), trienode.NewDeletedWithPrev(it.NodeBlob()))
}
if err := it.Error(); err != nil {
return nil, nil, nil, err
}
return storages, storageOrigins, nodes, nil
}
// deleteStorage is designed to delete the storage trie of a designated account.
// The function will make an attempt to utilize an efficient strategy if the
// associated state snapshot is reachable; otherwise, it will resort to a less
// efficient approach.
func (s *StateDB) deleteStorage(addr common.Address, addrHash common.Hash, root common.Hash) (map[common.Hash][]byte, map[common.Hash][]byte, *trienode.NodeSet, error) {
var (
err error
nodes *trienode.NodeSet // the set for trie node mutations (value is nil)
storages map[common.Hash][]byte // the set for storage mutations (value is nil)
storageOrigins map[common.Hash][]byte // the set for tracking the original value of slot
)
// The fast approach can be failed if the snapshot is not fully
// generated, or it's internally corrupted. Fallback to the slow
// one just in case.
snaps := s.db.Snapshot()
if snaps != nil {
storages, storageOrigins, nodes, err = s.fastDeleteStorage(snaps, addrHash, root)
}
if snaps == nil || err != nil {
storages, storageOrigins, nodes, err = s.slowDeleteStorage(addr, addrHash, root)
}
if err != nil {
return nil, nil, nil, err
}
return storages, storageOrigins, nodes, nil
}
// handleDestruction processes all destruction markers and deletes the account
// and associated storage slots if necessary. There are four potential scenarios
// as following:
@ -1190,7 +1133,7 @@ func (s *StateDB) handleDestruction(noStorageWiping bool) (map[common.Hash]*acco
return nil, nil, fmt.Errorf("unexpected storage wiping, %x", addr)
}
// Remove storage slots belonging to the account.
storages, storagesOrigin, set, err := s.deleteStorage(addr, addrHash, prev.Root)
storages, storagesOrigin, set, err := s.deleteStorage(addrHash, prev.Root)
if err != nil {
return nil, nil, fmt.Errorf("failed to delete storage, err: %w", err)
}

View file

@ -229,8 +229,8 @@ func (s *hookedStateDB) AddLog(log *types.Log) {
}
}
func (s *hookedStateDB) EmitLogsForBurnAccounts() {
s.inner.EmitLogsForBurnAccounts()
func (s *hookedStateDB) LogsForBurnAccounts() []*types.Log {
return s.inner.LogsForBurnAccounts()
}
func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) {

View file

@ -1296,12 +1296,12 @@ func TestDeleteStorage(t *testing.T) {
obj := fastState.getOrNewStateObject(addr)
storageRoot := obj.data.Root
_, _, fastNodes, err := fastState.deleteStorage(addr, crypto.Keccak256Hash(addr[:]), storageRoot)
_, _, fastNodes, err := fastState.deleteStorage(crypto.Keccak256Hash(addr[:]), storageRoot)
if err != nil {
t.Fatal(err)
}
_, _, slowNodes, err := slowState.deleteStorage(addr, crypto.Keccak256Hash(addr[:]), storageRoot)
_, _, slowNodes, err := slowState.deleteStorage(crypto.Keccak256Hash(addr[:]), storageRoot)
if err != nil {
t.Fatal(err)
}

View file

@ -260,6 +260,9 @@ func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM) {
evm.SetTxContext(NewEVMTxContext(msg))
evm.StateDB.AddAddressToAccessList(params.BeaconRootsAddress)
_, _, _ = evm.Call(msg.From, *msg.To, msg.Data, 30_000_000, common.U2560)
if evm.StateDB.AccessEvents() != nil {
evm.StateDB.AccessEvents().Merge(evm.AccessEvents)
}
evm.StateDB.Finalise(true)
}
@ -323,6 +326,9 @@ func processRequestsSystemCall(requests *[][]byte, evm *vm.EVM, requestType byte
evm.SetTxContext(NewEVMTxContext(msg))
evm.StateDB.AddAddressToAccessList(addr)
ret, _, err := evm.Call(msg.From, *msg.To, msg.Data, 30_000_000, common.U2560)
if evm.StateDB.AccessEvents() != nil {
evm.StateDB.AccessEvents().Merge(evm.AccessEvents)
}
evm.StateDB.Finalise(true)
if err != nil {
return fmt.Errorf("system call failed to execute: %v", err)

View file

@ -584,7 +584,9 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
}
}
if rules.IsAmsterdam {
st.evm.StateDB.EmitLogsForBurnAccounts()
for _, log := range st.evm.StateDB.LogsForBurnAccounts() {
st.evm.StateDB.AddLog(log)
}
}
return &ExecutionResult{
UsedGas: st.gasUsed(),

View file

@ -17,6 +17,7 @@
package stateless
import (
"errors"
"io"
"github.com/ethereum/go-ethereum/common/hexutil"
@ -42,6 +43,9 @@ func (w *Witness) ToExtWitness() *ExtWitness {
// FromExtWitness converts the consensus witness format into our internal one.
func (w *Witness) FromExtWitness(ext *ExtWitness) error {
if len(ext.Headers) == 0 {
return errors.New("witness must contain at least one header")
}
w.Headers = ext.Headers
w.Codes = make(map[string]struct{}, len(ext.Codes))

View file

@ -350,9 +350,12 @@ func (e *BlockAccessList) PrettyPrint() string {
}
// Copy returns a deep copy of the access list
func (e *BlockAccessList) Copy() (res BlockAccessList) {
for _, accountAccess := range e.Accesses {
res.Accesses = append(res.Accesses, accountAccess.Copy())
func (e *BlockAccessList) Copy() *BlockAccessList {
cpy := &BlockAccessList{
Accesses: make([]AccountAccess, 0, len(e.Accesses)),
}
return
for _, accountAccess := range e.Accesses {
cpy.Accesses = append(cpy.Accesses, accountAccess.Copy())
}
return cpy
}

View file

@ -190,8 +190,8 @@ func makeTestAccountAccess(sort bool) AccountAccess {
}
}
func makeTestBAL(sort bool) BlockAccessList {
list := BlockAccessList{}
func makeTestBAL(sort bool) *BlockAccessList {
list := &BlockAccessList{}
for i := 0; i < 5; i++ {
list.Accesses = append(list.Accesses, makeTestAccountAccess(sort))
}

View file

@ -30,6 +30,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/rlp"
)
@ -99,6 +100,9 @@ type Header struct {
// RequestsHash was added by EIP-7685 and is ignored in legacy headers.
RequestsHash *common.Hash `json:"requestsHash" rlp:"optional"`
// BlockAccessListHash was added by EIP-7928 and is ignored in legacy headers.
BlockAccessListHash *common.Hash `json:"balHash" rlp:"optional"`
// SlotNumber was added by EIP-7843 and is ignored in legacy headers.
SlotNumber *uint64 `json:"slotNumber" rlp:"optional"`
}
@ -204,6 +208,7 @@ type Block struct {
uncles []*Header
transactions Transactions
withdrawals Withdrawals
accessList *bal.BlockAccessList
// caches
hash atomic.Pointer[common.Hash]
@ -320,6 +325,10 @@ func CopyHeader(h *Header) *Header {
cpy.RequestsHash = new(common.Hash)
*cpy.RequestsHash = *h.RequestsHash
}
if h.BlockAccessListHash != nil {
cpy.BlockAccessListHash = new(common.Hash)
*cpy.BlockAccessListHash = *h.BlockAccessListHash
}
if h.SlotNumber != nil {
cpy.SlotNumber = new(uint64)
*cpy.SlotNumber = *h.SlotNumber
@ -358,9 +367,10 @@ func (b *Block) Body() *Body {
// Accessors for body data. These do not return a copy because the content
// of the body slices does not affect the cached hash/size in block.
func (b *Block) Uncles() []*Header { return b.uncles }
func (b *Block) Transactions() Transactions { return b.transactions }
func (b *Block) Withdrawals() Withdrawals { return b.withdrawals }
func (b *Block) Uncles() []*Header { return b.uncles }
func (b *Block) Transactions() Transactions { return b.transactions }
func (b *Block) Withdrawals() Withdrawals { return b.withdrawals }
func (b *Block) AccessList() *bal.BlockAccessList { return b.accessList }
func (b *Block) Transaction(hash common.Hash) *Transaction {
for _, transaction := range b.transactions {
@ -495,6 +505,7 @@ func (b *Block) WithSeal(header *Header) *Block {
transactions: b.transactions,
uncles: b.uncles,
withdrawals: b.withdrawals,
accessList: b.accessList,
}
}
@ -506,6 +517,7 @@ func (b *Block) WithBody(body Body) *Block {
transactions: slices.Clone(body.Transactions),
uncles: make([]*Header, len(body.Uncles)),
withdrawals: slices.Clone(body.Withdrawals),
accessList: b.accessList,
}
for i := range body.Uncles {
block.uncles[i] = CopyHeader(body.Uncles[i])
@ -513,6 +525,24 @@ func (b *Block) WithBody(body Body) *Block {
return block
}
// WithAccessList returns a copy of the block with the given access list embedded.
func (b *Block) WithAccessList(accessList *bal.BlockAccessList) *Block {
return b.WithAccessListUnsafe(accessList.Copy())
}
// WithAccessListUnsafe returns a copy of the block with the given access list
// embedded. Note that the access list is not deep-copied; use WithAccessList
// if the provided list may be modified by other actors.
func (b *Block) WithAccessListUnsafe(accessList *bal.BlockAccessList) *Block {
return &Block{
header: b.header,
transactions: b.transactions,
uncles: b.uncles,
withdrawals: b.withdrawals,
accessList: accessList,
}
}
// Hash returns the keccak256 hash of b's header.
// The hash is computed on the first call and cached thereafter.
func (b *Block) Hash() common.Hash {

View file

@ -16,29 +16,30 @@ var _ = (*headerMarshaling)(nil)
// MarshalJSON marshals as JSON.
func (h Header) MarshalJSON() ([]byte, error) {
type Header struct {
ParentHash common.Hash `json:"parentHash" gencodec:"required"`
UncleHash common.Hash `json:"sha3Uncles" gencodec:"required"`
Coinbase common.Address `json:"miner"`
Root common.Hash `json:"stateRoot" gencodec:"required"`
TxHash common.Hash `json:"transactionsRoot" gencodec:"required"`
ReceiptHash common.Hash `json:"receiptsRoot" gencodec:"required"`
Bloom Bloom `json:"logsBloom" gencodec:"required"`
Difficulty *hexutil.Big `json:"difficulty" gencodec:"required"`
Number *hexutil.Big `json:"number" gencodec:"required"`
GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"`
GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"`
Time hexutil.Uint64 `json:"timestamp" gencodec:"required"`
Extra hexutil.Bytes `json:"extraData" gencodec:"required"`
MixDigest common.Hash `json:"mixHash"`
Nonce BlockNonce `json:"nonce"`
BaseFee *hexutil.Big `json:"baseFeePerGas" rlp:"optional"`
WithdrawalsHash *common.Hash `json:"withdrawalsRoot" rlp:"optional"`
BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed" rlp:"optional"`
ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas" rlp:"optional"`
ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"`
RequestsHash *common.Hash `json:"requestsHash" rlp:"optional"`
SlotNumber *hexutil.Uint64 `json:"slotNumber" rlp:"optional"`
Hash common.Hash `json:"hash"`
ParentHash common.Hash `json:"parentHash" gencodec:"required"`
UncleHash common.Hash `json:"sha3Uncles" gencodec:"required"`
Coinbase common.Address `json:"miner"`
Root common.Hash `json:"stateRoot" gencodec:"required"`
TxHash common.Hash `json:"transactionsRoot" gencodec:"required"`
ReceiptHash common.Hash `json:"receiptsRoot" gencodec:"required"`
Bloom Bloom `json:"logsBloom" gencodec:"required"`
Difficulty *hexutil.Big `json:"difficulty" gencodec:"required"`
Number *hexutil.Big `json:"number" gencodec:"required"`
GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"`
GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"`
Time hexutil.Uint64 `json:"timestamp" gencodec:"required"`
Extra hexutil.Bytes `json:"extraData" gencodec:"required"`
MixDigest common.Hash `json:"mixHash"`
Nonce BlockNonce `json:"nonce"`
BaseFee *hexutil.Big `json:"baseFeePerGas" rlp:"optional"`
WithdrawalsHash *common.Hash `json:"withdrawalsRoot" rlp:"optional"`
BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed" rlp:"optional"`
ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas" rlp:"optional"`
ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"`
RequestsHash *common.Hash `json:"requestsHash" rlp:"optional"`
BlockAccessListHash *common.Hash `json:"balHash" rlp:"optional"`
SlotNumber *hexutil.Uint64 `json:"slotNumber" rlp:"optional"`
Hash common.Hash `json:"hash"`
}
var enc Header
enc.ParentHash = h.ParentHash
@ -62,6 +63,7 @@ func (h Header) MarshalJSON() ([]byte, error) {
enc.ExcessBlobGas = (*hexutil.Uint64)(h.ExcessBlobGas)
enc.ParentBeaconRoot = h.ParentBeaconRoot
enc.RequestsHash = h.RequestsHash
enc.BlockAccessListHash = h.BlockAccessListHash
enc.SlotNumber = (*hexutil.Uint64)(h.SlotNumber)
enc.Hash = h.Hash()
return json.Marshal(&enc)
@ -70,28 +72,29 @@ func (h Header) MarshalJSON() ([]byte, error) {
// UnmarshalJSON unmarshals from JSON.
func (h *Header) UnmarshalJSON(input []byte) error {
type Header struct {
ParentHash *common.Hash `json:"parentHash" gencodec:"required"`
UncleHash *common.Hash `json:"sha3Uncles" gencodec:"required"`
Coinbase *common.Address `json:"miner"`
Root *common.Hash `json:"stateRoot" gencodec:"required"`
TxHash *common.Hash `json:"transactionsRoot" gencodec:"required"`
ReceiptHash *common.Hash `json:"receiptsRoot" gencodec:"required"`
Bloom *Bloom `json:"logsBloom" gencodec:"required"`
Difficulty *hexutil.Big `json:"difficulty" gencodec:"required"`
Number *hexutil.Big `json:"number" gencodec:"required"`
GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"`
GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"`
Time *hexutil.Uint64 `json:"timestamp" gencodec:"required"`
Extra *hexutil.Bytes `json:"extraData" gencodec:"required"`
MixDigest *common.Hash `json:"mixHash"`
Nonce *BlockNonce `json:"nonce"`
BaseFee *hexutil.Big `json:"baseFeePerGas" rlp:"optional"`
WithdrawalsHash *common.Hash `json:"withdrawalsRoot" rlp:"optional"`
BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed" rlp:"optional"`
ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas" rlp:"optional"`
ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"`
RequestsHash *common.Hash `json:"requestsHash" rlp:"optional"`
SlotNumber *hexutil.Uint64 `json:"slotNumber" rlp:"optional"`
ParentHash *common.Hash `json:"parentHash" gencodec:"required"`
UncleHash *common.Hash `json:"sha3Uncles" gencodec:"required"`
Coinbase *common.Address `json:"miner"`
Root *common.Hash `json:"stateRoot" gencodec:"required"`
TxHash *common.Hash `json:"transactionsRoot" gencodec:"required"`
ReceiptHash *common.Hash `json:"receiptsRoot" gencodec:"required"`
Bloom *Bloom `json:"logsBloom" gencodec:"required"`
Difficulty *hexutil.Big `json:"difficulty" gencodec:"required"`
Number *hexutil.Big `json:"number" gencodec:"required"`
GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"`
GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"`
Time *hexutil.Uint64 `json:"timestamp" gencodec:"required"`
Extra *hexutil.Bytes `json:"extraData" gencodec:"required"`
MixDigest *common.Hash `json:"mixHash"`
Nonce *BlockNonce `json:"nonce"`
BaseFee *hexutil.Big `json:"baseFeePerGas" rlp:"optional"`
WithdrawalsHash *common.Hash `json:"withdrawalsRoot" rlp:"optional"`
BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed" rlp:"optional"`
ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas" rlp:"optional"`
ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"`
RequestsHash *common.Hash `json:"requestsHash" rlp:"optional"`
BlockAccessListHash *common.Hash `json:"balHash" rlp:"optional"`
SlotNumber *hexutil.Uint64 `json:"slotNumber" rlp:"optional"`
}
var dec Header
if err := json.Unmarshal(input, &dec); err != nil {
@ -172,6 +175,9 @@ func (h *Header) UnmarshalJSON(input []byte) error {
if dec.RequestsHash != nil {
h.RequestsHash = dec.RequestsHash
}
if dec.BlockAccessListHash != nil {
h.BlockAccessListHash = dec.BlockAccessListHash
}
if dec.SlotNumber != nil {
h.SlotNumber = (*uint64)(dec.SlotNumber)
}

View file

@ -43,8 +43,9 @@ func (obj *Header) EncodeRLP(_w io.Writer) error {
_tmp4 := obj.ExcessBlobGas != nil
_tmp5 := obj.ParentBeaconRoot != nil
_tmp6 := obj.RequestsHash != nil
_tmp7 := obj.SlotNumber != nil
if _tmp1 || _tmp2 || _tmp3 || _tmp4 || _tmp5 || _tmp6 || _tmp7 {
_tmp7 := obj.BlockAccessListHash != nil
_tmp8 := obj.SlotNumber != nil
if _tmp1 || _tmp2 || _tmp3 || _tmp4 || _tmp5 || _tmp6 || _tmp7 || _tmp8 {
if obj.BaseFee == nil {
w.Write(rlp.EmptyString)
} else {
@ -54,42 +55,49 @@ func (obj *Header) EncodeRLP(_w io.Writer) error {
w.WriteBigInt(obj.BaseFee)
}
}
if _tmp2 || _tmp3 || _tmp4 || _tmp5 || _tmp6 || _tmp7 {
if _tmp2 || _tmp3 || _tmp4 || _tmp5 || _tmp6 || _tmp7 || _tmp8 {
if obj.WithdrawalsHash == nil {
w.Write([]byte{0x80})
} else {
w.WriteBytes(obj.WithdrawalsHash[:])
}
}
if _tmp3 || _tmp4 || _tmp5 || _tmp6 || _tmp7 {
if _tmp3 || _tmp4 || _tmp5 || _tmp6 || _tmp7 || _tmp8 {
if obj.BlobGasUsed == nil {
w.Write([]byte{0x80})
} else {
w.WriteUint64((*obj.BlobGasUsed))
}
}
if _tmp4 || _tmp5 || _tmp6 || _tmp7 {
if _tmp4 || _tmp5 || _tmp6 || _tmp7 || _tmp8 {
if obj.ExcessBlobGas == nil {
w.Write([]byte{0x80})
} else {
w.WriteUint64((*obj.ExcessBlobGas))
}
}
if _tmp5 || _tmp6 || _tmp7 {
if _tmp5 || _tmp6 || _tmp7 || _tmp8 {
if obj.ParentBeaconRoot == nil {
w.Write([]byte{0x80})
} else {
w.WriteBytes(obj.ParentBeaconRoot[:])
}
}
if _tmp6 || _tmp7 {
if _tmp6 || _tmp7 || _tmp8 {
if obj.RequestsHash == nil {
w.Write([]byte{0x80})
} else {
w.WriteBytes(obj.RequestsHash[:])
}
}
if _tmp7 {
if _tmp7 || _tmp8 {
if obj.BlockAccessListHash == nil {
w.Write([]byte{0x80})
} else {
w.WriteBytes(obj.BlockAccessListHash[:])
}
}
if _tmp8 {
if obj.SlotNumber == nil {
w.Write([]byte{0x80})
} else {

View file

@ -42,7 +42,7 @@ type Contract struct {
IsDeployment bool
IsSystemCall bool
Gas uint64
Gas GasCosts
value *uint256.Int
}
@ -56,7 +56,7 @@ func NewContract(caller common.Address, address common.Address, value *uint256.I
caller: caller,
address: address,
jumpDests: jumpDests,
Gas: gas,
Gas: GasCosts{RegularGas: gas},
value: value,
}
}
@ -127,13 +127,13 @@ func (c *Contract) Caller() common.Address {
// UseGas attempts the use gas and subtracts it and returns true on success
func (c *Contract) UseGas(gas uint64, logger *tracing.Hooks, reason tracing.GasChangeReason) (ok bool) {
if c.Gas < gas {
if c.Gas.RegularGas < gas {
return false
}
if logger != nil && logger.OnGasChange != nil && reason != tracing.GasChangeIgnored {
logger.OnGasChange(c.Gas, c.Gas-gas, reason)
logger.OnGasChange(c.Gas.RegularGas, c.Gas.RegularGas-gas, reason)
}
c.Gas -= gas
c.Gas.RegularGas -= gas
return true
}
@ -143,9 +143,9 @@ func (c *Contract) RefundGas(gas uint64, logger *tracing.Hooks, reason tracing.G
return
}
if logger != nil && logger.OnGasChange != nil && reason != tracing.GasChangeIgnored {
logger.OnGasChange(c.Gas, c.Gas+gas, reason)
logger.OnGasChange(c.Gas.RegularGas, c.Gas.RegularGas+gas, reason)
}
c.Gas += gas
c.Gas.RegularGas += gas
}
// Address returns the contracts address

View file

@ -381,7 +381,7 @@ func opExtCodeCopyEIP4762(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, er
addr := common.Address(a.Bytes20())
code := evm.StateDB.GetCode(addr)
paddedCodeCopy, copyOffset, nonPaddedCopyLength := getDataAndAdjustedBounds(code, uint64CodeOffset, length.Uint64())
consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(addr, copyOffset, nonPaddedCopyLength, uint64(len(code)), false, scope.Contract.Gas)
consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(addr, copyOffset, nonPaddedCopyLength, uint64(len(code)), false, scope.Contract.Gas.RegularGas)
scope.Contract.UseGas(consumed, evm.Config.Tracer, tracing.GasChangeUnspecified)
if consumed < wanted {
return nil, ErrOutOfGas
@ -407,7 +407,7 @@ func opPush1EIP4762(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
// touch next chunk if PUSH1 is at the boundary. if so, *pc has
// advanced past this boundary.
contractAddr := scope.Contract.Address()
consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(contractAddr, *pc+1, uint64(1), uint64(len(scope.Contract.Code)), false, scope.Contract.Gas)
consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(contractAddr, *pc+1, uint64(1), uint64(len(scope.Contract.Code)), false, scope.Contract.Gas.RegularGas)
scope.Contract.UseGas(wanted, evm.Config.Tracer, tracing.GasChangeUnspecified)
if consumed < wanted {
return nil, ErrOutOfGas
@ -435,7 +435,7 @@ func makePushEIP4762(size uint64, pushByteSize int) executionFunc {
if !scope.Contract.IsDeployment && !scope.Contract.IsSystemCall {
contractAddr := scope.Contract.Address()
consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(contractAddr, uint64(start), uint64(pushByteSize), uint64(len(scope.Contract.Code)), false, scope.Contract.Gas)
consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(contractAddr, uint64(start), uint64(pushByteSize), uint64(len(scope.Contract.Code)), false, scope.Contract.Gas.RegularGas)
scope.Contract.UseGas(consumed, evm.Config.Tracer, tracing.GasChangeUnspecified)
if consumed < wanted {
return nil, ErrOutOfGas

View file

@ -303,7 +303,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
contract.IsSystemCall = isSystemCall(caller)
contract.SetCallCode(evm.resolveCodeHash(addr), code)
ret, err = evm.Run(contract, input, false)
gas = contract.Gas
gas = contract.Gas.RegularGas
}
}
// When an error was returned by the EVM or when setting the creation code
@ -365,7 +365,7 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt
contract := NewContract(caller, caller, value, gas, evm.jumpDests)
contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr))
ret, err = evm.Run(contract, input, false)
gas = contract.Gas
gas = contract.Gas.RegularGas
}
if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
@ -413,7 +413,7 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address,
contract := NewContract(originCaller, caller, value, gas, evm.jumpDests)
contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr))
ret, err = evm.Run(contract, input, false)
gas = contract.Gas
gas = contract.Gas.RegularGas
}
if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
@ -472,7 +472,7 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b
// above we revert to the snapshot and consume any gas remaining. Additionally
// when we're in Homestead this also counts for code storage gas errors.
ret, err = evm.Run(contract, input, true)
gas = contract.Gas
gas = contract.Gas.RegularGas
}
if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
@ -583,10 +583,10 @@ func (evm *EVM) create(caller common.Address, code []byte, gas uint64, value *ui
if err != nil && (evm.chainRules.IsHomestead || err != ErrCodeStoreOutOfGas) {
evm.StateDB.RevertToSnapshot(snapshot)
if err != ErrExecutionReverted {
contract.UseGas(contract.Gas, evm.Config.Tracer, tracing.GasChangeCallFailedExecution)
contract.UseGas(contract.Gas.RegularGas, evm.Config.Tracer, tracing.GasChangeCallFailedExecution)
}
}
return ret, address, contract.Gas, err
return ret, address, contract.Gas.RegularGas, err
}
// initNewContract runs a new contract's creation code, performs checks on the
@ -613,7 +613,7 @@ func (evm *EVM) initNewContract(contract *Contract, address common.Address) ([]b
return ret, ErrCodeStoreOutOfGas
}
} else {
consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(address, 0, uint64(len(ret)), uint64(len(ret)), true, contract.Gas)
consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(address, 0, uint64(len(ret)), uint64(len(ret)), true, contract.Gas.RegularGas)
contract.UseGas(consumed, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk)
if len(ret) > 0 && (consumed < wanted) {
return ret, ErrCodeStoreOutOfGas

View file

@ -64,26 +64,26 @@ func memoryGasCost(mem *Memory, newMemSize uint64) (uint64, error) {
// EXTCODECOPY (stack position 3)
// RETURNDATACOPY (stack position 2)
func memoryCopierGas(stackpos int) gasFunc {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
// Gas for expanding the memory
gas, err := memoryGasCost(mem, memorySize)
if err != nil {
return 0, err
return GasCosts{}, err
}
// And gas for copying data, charged per word at param.CopyGas
words, overflow := stack.Back(stackpos).Uint64WithOverflow()
if overflow {
return 0, ErrGasUintOverflow
return GasCosts{}, ErrGasUintOverflow
}
if words, overflow = math.SafeMul(toWordSize(words), params.CopyGas); overflow {
return 0, ErrGasUintOverflow
return GasCosts{}, ErrGasUintOverflow
}
if gas, overflow = math.SafeAdd(gas, words); overflow {
return 0, ErrGasUintOverflow
return GasCosts{}, ErrGasUintOverflow
}
return gas, nil
return GasCosts{RegularGas: gas}, nil
}
}
@ -95,9 +95,9 @@ var (
gasReturnDataCopy = memoryCopierGas(2)
)
func gasSStore(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
func gasSStore(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
if evm.readOnly {
return 0, ErrWriteProtection
return GasCosts{}, ErrWriteProtection
}
var (
y, x = stack.Back(1), stack.Back(0)
@ -114,12 +114,12 @@ func gasSStore(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySi
// 3. From a non-zero to a non-zero (CHANGE)
switch {
case current == (common.Hash{}) && y.Sign() != 0: // 0 => non 0
return params.SstoreSetGas, nil
return GasCosts{RegularGas: params.SstoreSetGas}, nil
case current != (common.Hash{}) && y.Sign() == 0: // non 0 => 0
evm.StateDB.AddRefund(params.SstoreRefundGas)
return params.SstoreClearGas, nil
return GasCosts{RegularGas: params.SstoreClearGas}, nil
default: // non 0 => non 0 (or 0 => 0)
return params.SstoreResetGas, nil
return GasCosts{RegularGas: params.SstoreResetGas}, nil
}
}
@ -139,16 +139,16 @@ func gasSStore(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySi
// (2.2.2.2.) Otherwise, add 4800 gas to refund counter.
value := common.Hash(y.Bytes32())
if current == value { // noop (1)
return params.NetSstoreNoopGas, nil
return GasCosts{RegularGas: params.NetSstoreNoopGas}, nil
}
if original == current {
if original == (common.Hash{}) { // create slot (2.1.1)
return params.NetSstoreInitGas, nil
return GasCosts{RegularGas: params.NetSstoreInitGas}, nil
}
if value == (common.Hash{}) { // delete slot (2.1.2b)
evm.StateDB.AddRefund(params.NetSstoreClearRefund)
}
return params.NetSstoreCleanGas, nil // write existing slot (2.1.2)
return GasCosts{RegularGas: params.NetSstoreCleanGas}, nil // write existing slot (2.1.2)
}
if original != (common.Hash{}) {
if current == (common.Hash{}) { // recreate slot (2.2.1.1)
@ -164,7 +164,7 @@ func gasSStore(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySi
evm.StateDB.AddRefund(params.NetSstoreResetRefund)
}
}
return params.NetSstoreDirtyGas, nil
return GasCosts{RegularGas: params.NetSstoreDirtyGas}, nil
}
// Here come the EIP2200 rules:
@ -182,13 +182,13 @@ func gasSStore(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySi
// (2.2.2.) If original value equals new value (this storage slot is reset):
// (2.2.2.1.) If original value is 0, add SSTORE_SET_GAS - SLOAD_GAS to refund counter.
// (2.2.2.2.) Otherwise, add SSTORE_RESET_GAS - SLOAD_GAS gas to refund counter.
func gasSStoreEIP2200(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
func gasSStoreEIP2200(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
if evm.readOnly {
return 0, ErrWriteProtection
return GasCosts{}, ErrWriteProtection
}
// If we fail the minimum gas availability invariant, fail (0)
if contract.Gas <= params.SstoreSentryGasEIP2200 {
return 0, errors.New("not enough gas for reentrancy sentry")
if contract.Gas.RegularGas <= params.SstoreSentryGasEIP2200 {
return GasCosts{}, errors.New("not enough gas for reentrancy sentry")
}
// Gas sentry honoured, do the actual gas calculation based on the stored value
var (
@ -198,16 +198,16 @@ func gasSStoreEIP2200(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m
value := common.Hash(y.Bytes32())
if current == value { // noop (1)
return params.SloadGasEIP2200, nil
return GasCosts{RegularGas: params.SloadGasEIP2200}, nil
}
if original == current {
if original == (common.Hash{}) { // create slot (2.1.1)
return params.SstoreSetGasEIP2200, nil
return GasCosts{RegularGas: params.SstoreSetGasEIP2200}, nil
}
if value == (common.Hash{}) { // delete slot (2.1.2b)
evm.StateDB.AddRefund(params.SstoreClearsScheduleRefundEIP2200)
}
return params.SstoreResetGasEIP2200, nil // write existing slot (2.1.2)
return GasCosts{RegularGas: params.SstoreResetGasEIP2200}, nil // write existing slot (2.1.2)
}
if original != (common.Hash{}) {
if current == (common.Hash{}) { // recreate slot (2.2.1.1)
@ -223,62 +223,66 @@ func gasSStoreEIP2200(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m
evm.StateDB.AddRefund(params.SstoreResetGasEIP2200 - params.SloadGasEIP2200)
}
}
return params.SloadGasEIP2200, nil // dirty update (2.2)
return GasCosts{RegularGas: params.SloadGasEIP2200}, nil // dirty update (2.2)
}
func makeGasLog(n uint64) gasFunc {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
requestedSize, overflow := stack.Back(1).Uint64WithOverflow()
if overflow {
return 0, ErrGasUintOverflow
return GasCosts{}, ErrGasUintOverflow
}
gas, err := memoryGasCost(mem, memorySize)
if err != nil {
return 0, err
return GasCosts{}, err
}
if gas, overflow = math.SafeAdd(gas, params.LogGas); overflow {
return 0, ErrGasUintOverflow
return GasCosts{}, ErrGasUintOverflow
}
if gas, overflow = math.SafeAdd(gas, n*params.LogTopicGas); overflow {
return 0, ErrGasUintOverflow
return GasCosts{}, ErrGasUintOverflow
}
var memorySizeGas uint64
if memorySizeGas, overflow = math.SafeMul(requestedSize, params.LogDataGas); overflow {
return 0, ErrGasUintOverflow
return GasCosts{}, ErrGasUintOverflow
}
if gas, overflow = math.SafeAdd(gas, memorySizeGas); overflow {
return 0, ErrGasUintOverflow
return GasCosts{}, ErrGasUintOverflow
}
return gas, nil
return GasCosts{RegularGas: gas}, nil
}
}
func gasKeccak256(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
func gasKeccak256(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
gas, err := memoryGasCost(mem, memorySize)
if err != nil {
return 0, err
return GasCosts{}, err
}
wordGas, overflow := stack.Back(1).Uint64WithOverflow()
if overflow {
return 0, ErrGasUintOverflow
return GasCosts{}, ErrGasUintOverflow
}
if wordGas, overflow = math.SafeMul(toWordSize(wordGas), params.Keccak256WordGas); overflow {
return 0, ErrGasUintOverflow
return GasCosts{}, ErrGasUintOverflow
}
if gas, overflow = math.SafeAdd(gas, wordGas); overflow {
return 0, ErrGasUintOverflow
return GasCosts{}, ErrGasUintOverflow
}
return gas, nil
return GasCosts{RegularGas: gas}, nil
}
// pureMemoryGascost is used by several operations, which aside from their
// static cost have a dynamic cost which is solely based on the memory
// expansion
func pureMemoryGascost(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
return memoryGasCost(mem, memorySize)
func pureMemoryGascost(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
gas, err := memoryGasCost(mem, memorySize)
if err != nil {
return GasCosts{}, err
}
return GasCosts{RegularGas: gas}, nil
}
var (
@ -290,64 +294,64 @@ var (
gasCreate = pureMemoryGascost
)
func gasCreate2(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
func gasCreate2(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
gas, err := memoryGasCost(mem, memorySize)
if err != nil {
return 0, err
return GasCosts{}, err
}
wordGas, overflow := stack.Back(2).Uint64WithOverflow()
if overflow {
return 0, ErrGasUintOverflow
return GasCosts{}, ErrGasUintOverflow
}
if wordGas, overflow = math.SafeMul(toWordSize(wordGas), params.Keccak256WordGas); overflow {
return 0, ErrGasUintOverflow
return GasCosts{}, ErrGasUintOverflow
}
if gas, overflow = math.SafeAdd(gas, wordGas); overflow {
return 0, ErrGasUintOverflow
return GasCosts{}, ErrGasUintOverflow
}
return gas, nil
return GasCosts{RegularGas: gas}, nil
}
func gasCreateEip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
func gasCreateEip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
gas, err := memoryGasCost(mem, memorySize)
if err != nil {
return 0, err
return GasCosts{}, err
}
size, overflow := stack.Back(2).Uint64WithOverflow()
if overflow {
return 0, ErrGasUintOverflow
return GasCosts{}, ErrGasUintOverflow
}
if err := CheckMaxInitCodeSize(&evm.chainRules, size); err != nil {
return 0, err
return GasCosts{}, err
}
// Since size <= the protocol-defined maximum initcode size limit, these multiplication cannot overflow
moreGas := params.InitCodeWordGas * ((size + 31) / 32)
if gas, overflow = math.SafeAdd(gas, moreGas); overflow {
return 0, ErrGasUintOverflow
return GasCosts{}, ErrGasUintOverflow
}
return gas, nil
return GasCosts{RegularGas: gas}, nil
}
func gasCreate2Eip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
func gasCreate2Eip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
gas, err := memoryGasCost(mem, memorySize)
if err != nil {
return 0, err
return GasCosts{}, err
}
size, overflow := stack.Back(2).Uint64WithOverflow()
if overflow {
return 0, ErrGasUintOverflow
return GasCosts{}, ErrGasUintOverflow
}
if err := CheckMaxInitCodeSize(&evm.chainRules, size); err != nil {
return 0, err
return GasCosts{}, err
}
// Since size <= the protocol-defined maximum initcode size limit, these multiplication cannot overflow
moreGas := (params.InitCodeWordGas + params.Keccak256WordGas) * ((size + 31) / 32)
if gas, overflow = math.SafeAdd(gas, moreGas); overflow {
return 0, ErrGasUintOverflow
return GasCosts{}, ErrGasUintOverflow
}
return gas, nil
return GasCosts{RegularGas: gas}, nil
}
func gasExpFrontier(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
func gasExpFrontier(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
expByteLen := uint64((stack.data[stack.len()-2].BitLen() + 7) / 8)
var (
@ -355,12 +359,12 @@ func gasExpFrontier(evm *EVM, contract *Contract, stack *Stack, mem *Memory, mem
overflow bool
)
if gas, overflow = math.SafeAdd(gas, params.ExpGas); overflow {
return 0, ErrGasUintOverflow
return GasCosts{}, ErrGasUintOverflow
}
return gas, nil
return GasCosts{RegularGas: gas}, nil
}
func gasExpEIP158(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
func gasExpEIP158(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
expByteLen := uint64((stack.data[stack.len()-2].BitLen() + 7) / 8)
var (
@ -368,9 +372,9 @@ func gasExpEIP158(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memor
overflow bool
)
if gas, overflow = math.SafeAdd(gas, params.ExpGas); overflow {
return 0, ErrGasUintOverflow
return GasCosts{}, ErrGasUintOverflow
}
return gas, nil
return GasCosts{RegularGas: gas}, nil
}
var (
@ -381,36 +385,36 @@ var (
)
func makeCallVariantGasCost(intrinsicFunc gasFunc) gasFunc {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
intrinsic, err := intrinsicFunc(evm, contract, stack, mem, memorySize)
if err != nil {
return 0, err
return GasCosts{}, err
}
evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas, intrinsic, stack.Back(0))
evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, intrinsic.RegularGas, stack.Back(0))
if err != nil {
return 0, err
return GasCosts{}, err
}
gas, overflow := math.SafeAdd(intrinsic, evm.callGasTemp)
gas, overflow := math.SafeAdd(intrinsic.RegularGas, evm.callGasTemp)
if overflow {
return 0, ErrGasUintOverflow
return GasCosts{}, ErrGasUintOverflow
}
return gas, nil
return GasCosts{RegularGas: gas}, nil
}
}
func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
var (
gas uint64
transfersValue = !stack.Back(2).IsZero()
address = common.Address(stack.Back(1).Bytes20())
)
if evm.readOnly && transfersValue {
return 0, ErrWriteProtection
return GasCosts{}, ErrWriteProtection
}
// Stateless check
memoryGas, err := memoryGasCost(mem, memorySize)
if err != nil {
return 0, err
return GasCosts{}, err
}
var transferGas uint64
if transfersValue && !evm.chainRules.IsEIP4762 {
@ -418,12 +422,12 @@ func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m
}
var overflow bool
if gas, overflow = math.SafeAdd(memoryGas, transferGas); overflow {
return 0, ErrGasUintOverflow
return GasCosts{}, ErrGasUintOverflow
}
// Terminate the gas measurement if the leftover gas is not sufficient,
// it can effectively prevent accessing the states in the following steps.
if contract.Gas < gas {
return 0, ErrOutOfGas
if contract.Gas.RegularGas < gas {
return GasCosts{}, ErrOutOfGas
}
// Stateful check
var stateGas uint64
@ -435,15 +439,15 @@ func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m
stateGas += params.CallNewAccountGas
}
if gas, overflow = math.SafeAdd(gas, stateGas); overflow {
return 0, ErrGasUintOverflow
return GasCosts{}, ErrGasUintOverflow
}
return gas, nil
return GasCosts{RegularGas: gas}, nil
}
func gasCallCodeIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
func gasCallCodeIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
memoryGas, err := memoryGasCost(mem, memorySize)
if err != nil {
return 0, err
return GasCosts{}, err
}
var (
gas uint64
@ -453,22 +457,30 @@ func gasCallCodeIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memor
gas += params.CallValueTransferGas
}
if gas, overflow = math.SafeAdd(gas, memoryGas); overflow {
return 0, ErrGasUintOverflow
return GasCosts{}, ErrGasUintOverflow
}
return gas, nil
return GasCosts{RegularGas: gas}, nil
}
func gasDelegateCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
return memoryGasCost(mem, memorySize)
func gasDelegateCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
gas, err := memoryGasCost(mem, memorySize)
if err != nil {
return GasCosts{}, err
}
return GasCosts{RegularGas: gas}, nil
}
func gasStaticCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
return memoryGasCost(mem, memorySize)
func gasStaticCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
gas, err := memoryGasCost(mem, memorySize)
if err != nil {
return GasCosts{}, err
}
return GasCosts{RegularGas: gas}, nil
}
func gasSelfdestruct(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
func gasSelfdestruct(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
if evm.readOnly {
return 0, ErrWriteProtection
return GasCosts{}, ErrWriteProtection
}
var gas uint64
@ -490,5 +502,5 @@ func gasSelfdestruct(evm *EVM, contract *Contract, stack *Stack, mem *Memory, me
if !evm.StateDB.HasSelfDestructed(contract.Address()) {
evm.StateDB.AddRefund(params.SelfdestructRefundGas)
}
return gas, nil
return GasCosts{RegularGas: gas}, nil
}

36
core/vm/gascosts.go Normal file
View file

@ -0,0 +1,36 @@
// Copyright 2026 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/>.
package vm
import "fmt"
// GasCosts denotes a vector of gas costs in the
// multidimensional metering paradigm.
type GasCosts struct {
RegularGas uint64
StateGas uint64
}
// Sum returns the total gas (regular + state).
func (g GasCosts) Sum() uint64 {
return g.RegularGas + g.StateGas
}
// String returns a visual representation of the gas vector.
func (g GasCosts) String() string {
return fmt.Sprintf("<%v,%v>", g.RegularGas, g.StateGas)
}

View file

@ -566,7 +566,7 @@ func opMsize(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
}
func opGas(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
scope.Stack.push(new(uint256.Int).SetUint64(scope.Contract.Gas))
scope.Stack.push(new(uint256.Int).SetUint64(scope.Contract.Gas.RegularGas))
return nil, nil
}
@ -658,7 +658,7 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
value = scope.Stack.pop()
offset, size = scope.Stack.pop(), scope.Stack.pop()
input = scope.Memory.GetCopy(offset.Uint64(), size.Uint64())
gas = scope.Contract.Gas
gas = scope.Contract.Gas.RegularGas
)
if evm.chainRules.IsEIP150 {
gas -= gas / 64
@ -702,7 +702,7 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
offset, size = scope.Stack.pop(), scope.Stack.pop()
salt = scope.Stack.pop()
input = scope.Memory.GetCopy(offset.Uint64(), size.Uint64())
gas = scope.Contract.Gas
gas = scope.Contract.Gas.RegularGas
)
// Apply EIP150

View file

@ -879,7 +879,7 @@ func TestOpMCopy(t *testing.T) {
if dynamicCost, err := gasMcopy(evm, nil, stack, mem, memorySize); err != nil {
t.Error(err)
} else {
haveGas = GasFastestStep + dynamicCost
haveGas = GasFastestStep + dynamicCost.RegularGas
}
// Expand mem
if memorySize > 0 {

View file

@ -87,7 +87,7 @@ type StateDB interface {
Snapshot() int
AddLog(*types.Log)
EmitLogsForBurnAccounts()
LogsForBurnAccounts() []*types.Log
AddPreimage(common.Hash, []byte)
Witness() *stateless.Witness

View file

@ -166,14 +166,14 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte
for {
if debug {
// Capture pre-execution values for tracing.
logged, pcCopy, gasCopy = false, pc, contract.Gas
logged, pcCopy, gasCopy = false, pc, contract.Gas.RegularGas
}
if isEIP4762 && !contract.IsDeployment && !contract.IsSystemCall {
// if the PC ends up in a new "chunk" of verkleized code, charge the
// associated costs.
contractAddr := contract.Address()
consumed, wanted := evm.TxContext.AccessEvents.CodeChunksRangeGas(contractAddr, pc, 1, uint64(len(contract.Code)), false, contract.Gas)
consumed, wanted := evm.TxContext.AccessEvents.CodeChunksRangeGas(contractAddr, pc, 1, uint64(len(contract.Code)), false, contract.Gas.RegularGas)
contract.UseGas(consumed, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk)
if consumed < wanted {
return nil, ErrOutOfGas
@ -192,10 +192,10 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte
return nil, &ErrStackOverflow{stackLen: sLen, limit: operation.maxStack}
}
// for tracing: this gas consumption event is emitted below in the debug section.
if contract.Gas < cost {
if contract.Gas.RegularGas < cost {
return nil, ErrOutOfGas
} else {
contract.Gas -= cost
contract.Gas.RegularGas -= cost
}
// All ops with a dynamic memory usage also has a dynamic gas cost.
@ -218,17 +218,17 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte
}
// Consume the gas and return an error if not enough gas is available.
// cost is explicitly set so that the capture state defer method can get the proper cost
var dynamicCost uint64
var dynamicCost GasCosts
dynamicCost, err = operation.dynamicGas(evm, contract, stack, mem, memorySize)
cost += dynamicCost // for tracing
cost += dynamicCost.RegularGas // for tracing
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrOutOfGas, err)
}
// for tracing: this gas consumption event is emitted below in the debug section.
if contract.Gas < dynamicCost {
if contract.Gas.RegularGas < dynamicCost.RegularGas {
return nil, ErrOutOfGas
} else {
contract.Gas -= dynamicCost
contract.Gas.RegularGas -= dynamicCost.RegularGas
}
}

View file

@ -24,7 +24,7 @@ import (
type (
executionFunc func(pc *uint64, evm *EVM, callContext *ScopeContext) ([]byte, error)
gasFunc func(*EVM, *Contract, *Stack, *Memory, uint64) (uint64, error) // last parameter is the requested memory size as a uint64
gasFunc func(*EVM, *Contract, *Stack, *Memory, uint64) (GasCosts, error) // last parameter is the requested memory size as a uint64
// memorySizeFunc returns the required size, and whether the operation overflowed a uint64
memorySizeFunc func(*Stack) (size uint64, overflow bool)
)

View file

@ -27,13 +27,13 @@ import (
)
func makeGasSStoreFunc(clearingRefund uint64) gasFunc {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
if evm.readOnly {
return 0, ErrWriteProtection
return GasCosts{}, ErrWriteProtection
}
// If we fail the minimum gas availability invariant, fail (0)
if contract.Gas <= params.SstoreSentryGasEIP2200 {
return 0, errors.New("not enough gas for reentrancy sentry")
if contract.Gas.RegularGas <= params.SstoreSentryGasEIP2200 {
return GasCosts{}, errors.New("not enough gas for reentrancy sentry")
}
// Gas sentry honoured, do the actual gas calculation based on the stored value
var (
@ -53,18 +53,18 @@ func makeGasSStoreFunc(clearingRefund uint64) gasFunc {
if current == value { // noop (1)
// EIP 2200 original clause:
// return params.SloadGasEIP2200, nil
return cost + params.WarmStorageReadCostEIP2929, nil // SLOAD_GAS
return GasCosts{RegularGas: cost + params.WarmStorageReadCostEIP2929}, nil // SLOAD_GAS
}
if original == current {
if original == (common.Hash{}) { // create slot (2.1.1)
return cost + params.SstoreSetGasEIP2200, nil
return GasCosts{RegularGas: cost + params.SstoreSetGasEIP2200}, nil
}
if value == (common.Hash{}) { // delete slot (2.1.2b)
evm.StateDB.AddRefund(clearingRefund)
}
// EIP-2200 original clause:
// return params.SstoreResetGasEIP2200, nil // write existing slot (2.1.2)
return cost + (params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929), nil // write existing slot (2.1.2)
return GasCosts{RegularGas: cost + (params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929)}, nil // write existing slot (2.1.2)
}
if original != (common.Hash{}) {
if current == (common.Hash{}) { // recreate slot (2.2.1.1)
@ -89,7 +89,7 @@ func makeGasSStoreFunc(clearingRefund uint64) gasFunc {
}
// EIP-2200 original clause:
//return params.SloadGasEIP2200, nil // dirty update (2.2)
return cost + params.WarmStorageReadCostEIP2929, nil // dirty update (2.2)
return GasCosts{RegularGas: cost + params.WarmStorageReadCostEIP2929}, nil // dirty update (2.2)
}
}
@ -98,7 +98,7 @@ func makeGasSStoreFunc(clearingRefund uint64) gasFunc {
// whose storage is being read) is not yet in accessed_storage_keys,
// charge 2100 gas and add the pair to accessed_storage_keys.
// If the pair is already in accessed_storage_keys, charge 100 gas.
func gasSLoadEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
func gasSLoadEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
loc := stack.peek()
slot := common.Hash(loc.Bytes32())
// Check slot presence in the access list
@ -106,9 +106,9 @@ func gasSLoadEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memory, me
// If the caller cannot afford the cost, this change will be rolled back
// If he does afford it, we can skip checking the same thing later on, during execution
evm.StateDB.AddSlotToAccessList(contract.Address(), slot)
return params.ColdSloadCostEIP2929, nil
return GasCosts{RegularGas: params.ColdSloadCostEIP2929}, nil
}
return params.WarmStorageReadCostEIP2929, nil
return GasCosts{RegularGas: params.WarmStorageReadCostEIP2929}, nil
}
// gasExtCodeCopyEIP2929 implements extcodecopy according to EIP-2929
@ -116,12 +116,13 @@ func gasSLoadEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memory, me
// > If the target is not in accessed_addresses,
// > charge COLD_ACCOUNT_ACCESS_COST gas, and add the address to accessed_addresses.
// > Otherwise, charge WARM_STORAGE_READ_COST gas.
func gasExtCodeCopyEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
func gasExtCodeCopyEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
// memory expansion first (dynamic part of pre-2929 implementation)
gas, err := gasExtCodeCopy(evm, contract, stack, mem, memorySize)
gasCost, err := gasExtCodeCopy(evm, contract, stack, mem, memorySize)
if err != nil {
return 0, err
return GasCosts{}, err
}
gas := gasCost.RegularGas
addr := common.Address(stack.peek().Bytes20())
// Check slot presence in the access list
if !evm.StateDB.AddressInAccessList(addr) {
@ -129,11 +130,11 @@ func gasExtCodeCopyEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memo
var overflow bool
// We charge (cold-warm), since 'warm' is already charged as constantGas
if gas, overflow = math.SafeAdd(gas, params.ColdAccountAccessCostEIP2929-params.WarmStorageReadCostEIP2929); overflow {
return 0, ErrGasUintOverflow
return GasCosts{}, ErrGasUintOverflow
}
return gas, nil
return GasCosts{RegularGas: gas}, nil
}
return gas, nil
return GasCosts{RegularGas: gas}, nil
}
// gasEip2929AccountCheck checks whether the first stack item (as address) is present in the access list.
@ -143,20 +144,20 @@ func gasExtCodeCopyEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memo
// - extcodehash,
// - extcodesize,
// - (ext) balance
func gasEip2929AccountCheck(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
func gasEip2929AccountCheck(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
addr := common.Address(stack.peek().Bytes20())
// Check slot presence in the access list
if !evm.StateDB.AddressInAccessList(addr) {
// If the caller cannot afford the cost, this change will be rolled back
evm.StateDB.AddAddressToAccessList(addr)
// The warm storage read cost is already charged as constantGas
return params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929, nil
return GasCosts{RegularGas: params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929}, nil
}
return 0, nil
return GasCosts{}, nil
}
func makeCallVariantGasCallEIP2929(oldCalculator gasFunc, addressPosition int) gasFunc {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
addr := common.Address(stack.Back(addressPosition).Bytes20())
// Check slot presence in the access list
warmAccess := evm.StateDB.AddressInAccessList(addr)
@ -168,7 +169,7 @@ func makeCallVariantGasCallEIP2929(oldCalculator gasFunc, addressPosition int) g
// Charge the remaining difference here already, to correctly calculate available
// gas for call
if !contract.UseGas(coldCost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
return 0, ErrOutOfGas
return GasCosts{}, ErrOutOfGas
}
}
// Now call the old calculator, which takes into account
@ -176,21 +177,22 @@ func makeCallVariantGasCallEIP2929(oldCalculator gasFunc, addressPosition int) g
// - transfer value
// - memory expansion
// - 63/64ths rule
gas, err := oldCalculator(evm, contract, stack, mem, memorySize)
gasCost, err := oldCalculator(evm, contract, stack, mem, memorySize)
if warmAccess || err != nil {
return gas, err
return gasCost, err
}
// In case of a cold access, we temporarily add the cold charge back, and also
// add it to the returned gas. By adding it to the return, it will be charged
// outside of this function, as part of the dynamic gas, and that will make it
// also become correctly reported to tracers.
contract.Gas += coldCost
contract.Gas.RegularGas += coldCost
gas := gasCost.RegularGas
var overflow bool
if gas, overflow = math.SafeAdd(gas, coldCost); overflow {
return 0, ErrGasUintOverflow
return GasCosts{}, ErrGasUintOverflow
}
return gas, nil
return GasCosts{RegularGas: gas}, nil
}
}
@ -224,13 +226,13 @@ var (
// makeSelfdestructGasFn can create the selfdestruct dynamic gas function for EIP-2929 and EIP-3529
func makeSelfdestructGasFn(refundsEnabled bool) gasFunc {
gasFunc := func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
gasFunc := func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
var (
gas uint64
address = common.Address(stack.peek().Bytes20())
)
if evm.readOnly {
return 0, ErrWriteProtection
return GasCosts{}, ErrWriteProtection
}
if !evm.StateDB.AddressInAccessList(address) {
// If the caller cannot afford the cost, this change will be rolled back
@ -239,8 +241,8 @@ func makeSelfdestructGasFn(refundsEnabled bool) gasFunc {
// Terminate the gas measurement if the leftover gas is not sufficient,
// it can effectively prevent accessing the states in the following steps
if contract.Gas < gas {
return 0, ErrOutOfGas
if contract.Gas.RegularGas < gas {
return GasCosts{}, ErrOutOfGas
}
}
// if empty and transfers value
@ -250,7 +252,7 @@ func makeSelfdestructGasFn(refundsEnabled bool) gasFunc {
if refundsEnabled && !evm.StateDB.HasSelfDestructed(contract.Address()) {
evm.StateDB.AddRefund(params.SelfdestructRefundGas)
}
return gas, nil
return GasCosts{RegularGas: gas}, nil
}
return gasFunc
}
@ -262,20 +264,20 @@ var (
gasCallCodeEIP7702 = makeCallVariantGasCallEIP7702(gasCallCodeIntrinsic)
)
func gasCallEIP7702(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
func gasCallEIP7702(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
// Return early if this call attempts to transfer value in a static context.
// Although it's checked in `gasCall`, EIP-7702 loads the target's code before
// to determine if it is resolving a delegation. This could incorrectly record
// the target in the block access list (BAL) if the call later fails.
transfersValue := !stack.Back(2).IsZero()
if evm.readOnly && transfersValue {
return 0, ErrWriteProtection
return GasCosts{}, ErrWriteProtection
}
return innerGasCallEIP7702(evm, contract, stack, mem, memorySize)
}
func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
var (
eip2929Cost uint64
eip7702Cost uint64
@ -294,7 +296,7 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc {
// Charge the remaining difference here already, to correctly calculate
// available gas for call
if !contract.UseGas(eip2929Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
return 0, ErrOutOfGas
return GasCosts{}, ErrOutOfGas
}
}
@ -305,13 +307,13 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc {
// - create new account
intrinsicCost, err := intrinsicFunc(evm, contract, stack, mem, memorySize)
if err != nil {
return 0, err
return GasCosts{}, err
}
// Terminate the gas measurement if the leftover gas is not sufficient,
// it can effectively prevent accessing the states in the following steps.
// It's an essential safeguard before any stateful check.
if contract.Gas < intrinsicCost {
return 0, ErrOutOfGas
if contract.Gas.RegularGas < intrinsicCost.RegularGas {
return GasCosts{}, ErrOutOfGas
}
// Check if code is a delegation and if so, charge for resolution.
@ -323,20 +325,20 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc {
eip7702Cost = params.ColdAccountAccessCostEIP2929
}
if !contract.UseGas(eip7702Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
return 0, ErrOutOfGas
return GasCosts{}, ErrOutOfGas
}
}
// Calculate the gas budget for the nested call. The costs defined by
// EIP-2929 and EIP-7702 have already been applied.
evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas, intrinsicCost, stack.Back(0))
evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, intrinsicCost.RegularGas, stack.Back(0))
if err != nil {
return 0, err
return GasCosts{}, err
}
// Temporarily add the gas charge back to the contract and return value. By
// adding it to the return, it will be charged outside of this function, as
// part of the dynamic gas. This will ensure it is correctly reported to
// tracers.
contract.Gas += eip2929Cost + eip7702Cost
contract.Gas.RegularGas += eip2929Cost + eip7702Cost
// Aggregate the gas costs from all components, including EIP-2929, EIP-7702,
// the CALL opcode itself, and the cost incurred by nested calls.
@ -345,14 +347,14 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc {
totalCost uint64
)
if totalCost, overflow = math.SafeAdd(eip2929Cost, eip7702Cost); overflow {
return 0, ErrGasUintOverflow
return GasCosts{}, ErrGasUintOverflow
}
if totalCost, overflow = math.SafeAdd(totalCost, intrinsicCost); overflow {
return 0, ErrGasUintOverflow
if totalCost, overflow = math.SafeAdd(totalCost, intrinsicCost.RegularGas); overflow {
return GasCosts{}, ErrGasUintOverflow
}
if totalCost, overflow = math.SafeAdd(totalCost, evm.callGasTemp); overflow {
return 0, ErrGasUintOverflow
return GasCosts{}, ErrGasUintOverflow
}
return totalCost, nil
return GasCosts{RegularGas: totalCost}, nil
}
}

View file

@ -24,37 +24,37 @@ import (
"github.com/ethereum/go-ethereum/params"
)
func gasSStore4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
return evm.AccessEvents.SlotGas(contract.Address(), stack.peek().Bytes32(), true, contract.Gas, true), nil
func gasSStore4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
return GasCosts{RegularGas: evm.AccessEvents.SlotGas(contract.Address(), stack.peek().Bytes32(), true, contract.Gas.RegularGas, true)}, nil
}
func gasSLoad4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
return evm.AccessEvents.SlotGas(contract.Address(), stack.peek().Bytes32(), false, contract.Gas, true), nil
func gasSLoad4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
return GasCosts{RegularGas: evm.AccessEvents.SlotGas(contract.Address(), stack.peek().Bytes32(), false, contract.Gas.RegularGas, true)}, nil
}
func gasBalance4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
func gasBalance4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
address := stack.peek().Bytes20()
return evm.AccessEvents.BasicDataGas(address, false, contract.Gas, true), nil
return GasCosts{RegularGas: evm.AccessEvents.BasicDataGas(address, false, contract.Gas.RegularGas, true)}, nil
}
func gasExtCodeSize4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
func gasExtCodeSize4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
address := stack.peek().Bytes20()
if _, isPrecompile := evm.precompile(address); isPrecompile {
return 0, nil
return GasCosts{}, nil
}
return evm.AccessEvents.BasicDataGas(address, false, contract.Gas, true), nil
return GasCosts{RegularGas: evm.AccessEvents.BasicDataGas(address, false, contract.Gas.RegularGas, true)}, nil
}
func gasExtCodeHash4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
func gasExtCodeHash4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
address := stack.peek().Bytes20()
if _, isPrecompile := evm.precompile(address); isPrecompile {
return 0, nil
return GasCosts{}, nil
}
return evm.AccessEvents.CodeHashGas(address, false, contract.Gas, true), nil
return GasCosts{RegularGas: evm.AccessEvents.CodeHashGas(address, false, contract.Gas.RegularGas, true)}, nil
}
func makeCallVariantGasEIP4762(oldCalculator gasFunc, withTransferCosts bool) gasFunc {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
var (
target = common.Address(stack.Back(1).Bytes20())
witnessGas uint64
@ -65,9 +65,9 @@ func makeCallVariantGasEIP4762(oldCalculator gasFunc, withTransferCosts bool) ga
// If value is transferred, it is charged before 1/64th
// is subtracted from the available gas pool.
if withTransferCosts && !stack.Back(2).IsZero() {
wantedValueTransferWitnessGas := evm.AccessEvents.ValueTransferGas(contract.Address(), target, contract.Gas)
if wantedValueTransferWitnessGas > contract.Gas {
return wantedValueTransferWitnessGas, nil
wantedValueTransferWitnessGas := evm.AccessEvents.ValueTransferGas(contract.Address(), target, contract.Gas.RegularGas)
if wantedValueTransferWitnessGas > contract.Gas.RegularGas {
return GasCosts{RegularGas: wantedValueTransferWitnessGas}, nil
}
witnessGas = wantedValueTransferWitnessGas
} else if isPrecompile || isSystemContract {
@ -78,25 +78,26 @@ func makeCallVariantGasEIP4762(oldCalculator gasFunc, withTransferCosts bool) ga
// (so before we get to this point)
// But the message call is part of the subcall, for which only 63/64th
// of the gas should be available.
wantedMessageCallWitnessGas := evm.AccessEvents.MessageCallGas(target, contract.Gas-witnessGas)
wantedMessageCallWitnessGas := evm.AccessEvents.MessageCallGas(target, contract.Gas.RegularGas-witnessGas)
var overflow bool
if witnessGas, overflow = math.SafeAdd(witnessGas, wantedMessageCallWitnessGas); overflow {
return 0, ErrGasUintOverflow
return GasCosts{}, ErrGasUintOverflow
}
if witnessGas > contract.Gas {
return witnessGas, nil
if witnessGas > contract.Gas.RegularGas {
return GasCosts{RegularGas: witnessGas}, nil
}
}
contract.Gas -= witnessGas
contract.Gas.RegularGas -= witnessGas
// if the operation fails, adds witness gas to the gas before returning the error
gas, err := oldCalculator(evm, contract, stack, mem, memorySize)
contract.Gas += witnessGas // restore witness gas so that it can be charged at the callsite
gasCost, err := oldCalculator(evm, contract, stack, mem, memorySize)
contract.Gas.RegularGas += witnessGas // restore witness gas so that it can be charged at the callsite
gas := gasCost.RegularGas
var overflow bool
if gas, overflow = math.SafeAdd(gas, witnessGas); overflow {
return 0, ErrGasUintOverflow
return GasCosts{}, ErrGasUintOverflow
}
return gas, err
return GasCosts{RegularGas: gas}, err
}
}
@ -107,18 +108,18 @@ var (
gasDelegateCallEIP4762 = makeCallVariantGasEIP4762(gasDelegateCall, false)
)
func gasSelfdestructEIP4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
func gasSelfdestructEIP4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
beneficiaryAddr := common.Address(stack.peek().Bytes20())
if _, isPrecompile := evm.precompile(beneficiaryAddr); isPrecompile {
return 0, nil
return GasCosts{}, nil
}
if contract.IsSystemCall {
return 0, nil
return GasCosts{}, nil
}
contractAddr := contract.Address()
wanted := evm.AccessEvents.BasicDataGas(contractAddr, false, contract.Gas, false)
if wanted > contract.Gas {
return wanted, nil
wanted := evm.AccessEvents.BasicDataGas(contractAddr, false, contract.Gas.RegularGas, false)
if wanted > contract.Gas.RegularGas {
return GasCosts{RegularGas: wanted}, nil
}
statelessGas := wanted
balanceIsZero := evm.StateDB.GetBalance(contractAddr).Sign() == 0
@ -126,44 +127,45 @@ func gasSelfdestructEIP4762(evm *EVM, contract *Contract, stack *Stack, mem *Mem
isSystemContract := beneficiaryAddr == params.HistoryStorageAddress
if (isPrecompile || isSystemContract) && balanceIsZero {
return statelessGas, nil
return GasCosts{RegularGas: statelessGas}, nil
}
if contractAddr != beneficiaryAddr {
wanted := evm.AccessEvents.BasicDataGas(beneficiaryAddr, false, contract.Gas-statelessGas, false)
if wanted > contract.Gas-statelessGas {
return statelessGas + wanted, nil
wanted := evm.AccessEvents.BasicDataGas(beneficiaryAddr, false, contract.Gas.RegularGas-statelessGas, false)
if wanted > contract.Gas.RegularGas-statelessGas {
return GasCosts{RegularGas: statelessGas + wanted}, nil
}
statelessGas += wanted
}
// Charge write costs if it transfers value
if !balanceIsZero {
wanted := evm.AccessEvents.BasicDataGas(contractAddr, true, contract.Gas-statelessGas, false)
if wanted > contract.Gas-statelessGas {
return statelessGas + wanted, nil
wanted := evm.AccessEvents.BasicDataGas(contractAddr, true, contract.Gas.RegularGas-statelessGas, false)
if wanted > contract.Gas.RegularGas-statelessGas {
return GasCosts{RegularGas: statelessGas + wanted}, nil
}
statelessGas += wanted
if contractAddr != beneficiaryAddr {
if evm.StateDB.Exist(beneficiaryAddr) {
wanted = evm.AccessEvents.BasicDataGas(beneficiaryAddr, true, contract.Gas-statelessGas, false)
wanted = evm.AccessEvents.BasicDataGas(beneficiaryAddr, true, contract.Gas.RegularGas-statelessGas, false)
} else {
wanted = evm.AccessEvents.AddAccount(beneficiaryAddr, true, contract.Gas-statelessGas)
wanted = evm.AccessEvents.AddAccount(beneficiaryAddr, true, contract.Gas.RegularGas-statelessGas)
}
if wanted > contract.Gas-statelessGas {
return statelessGas + wanted, nil
if wanted > contract.Gas.RegularGas-statelessGas {
return GasCosts{RegularGas: statelessGas + wanted}, nil
}
statelessGas += wanted
}
}
return statelessGas, nil
return GasCosts{RegularGas: statelessGas}, nil
}
func gasCodeCopyEip4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
gas, err := gasCodeCopy(evm, contract, stack, mem, memorySize)
func gasCodeCopyEip4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
gasCost, err := gasCodeCopy(evm, contract, stack, mem, memorySize)
if err != nil {
return 0, err
return GasCosts{}, err
}
gas := gasCost.RegularGas
if !contract.IsDeployment && !contract.IsSystemCall {
var (
codeOffset = stack.Back(1)
@ -175,31 +177,32 @@ func gasCodeCopyEip4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory,
}
_, copyOffset, nonPaddedCopyLength := getDataAndAdjustedBounds(contract.Code, uint64CodeOffset, length.Uint64())
_, wanted := evm.AccessEvents.CodeChunksRangeGas(contract.Address(), copyOffset, nonPaddedCopyLength, uint64(len(contract.Code)), false, contract.Gas-gas)
_, wanted := evm.AccessEvents.CodeChunksRangeGas(contract.Address(), copyOffset, nonPaddedCopyLength, uint64(len(contract.Code)), false, contract.Gas.RegularGas-gas)
gas += wanted
}
return gas, nil
return GasCosts{RegularGas: gas}, nil
}
func gasExtCodeCopyEIP4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
func gasExtCodeCopyEIP4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
// memory expansion first (dynamic part of pre-2929 implementation)
gas, err := gasExtCodeCopy(evm, contract, stack, mem, memorySize)
gasCost, err := gasExtCodeCopy(evm, contract, stack, mem, memorySize)
if err != nil {
return 0, err
return GasCosts{}, err
}
gas := gasCost.RegularGas
addr := common.Address(stack.peek().Bytes20())
_, isPrecompile := evm.precompile(addr)
if isPrecompile || addr == params.HistoryStorageAddress {
var overflow bool
if gas, overflow = math.SafeAdd(gas, params.WarmStorageReadCostEIP2929); overflow {
return 0, ErrGasUintOverflow
return GasCosts{}, ErrGasUintOverflow
}
return gas, nil
return GasCosts{RegularGas: gas}, nil
}
wgas := evm.AccessEvents.BasicDataGas(addr, false, contract.Gas-gas, true)
wgas := evm.AccessEvents.BasicDataGas(addr, false, contract.Gas.RegularGas-gas, true)
var overflow bool
if gas, overflow = math.SafeAdd(gas, wgas); overflow {
return 0, ErrGasUintOverflow
return GasCosts{}, ErrGasUintOverflow
}
return gas, nil
return GasCosts{RegularGas: gas}, nil
}

View file

@ -414,9 +414,10 @@ func (b *EthAPIBackend) SyncProgress(ctx context.Context) ethereum.SyncProgress
prog.TxIndexFinishedBlocks = txProg.Indexed
prog.TxIndexRemainingBlocks = txProg.Remaining
}
remain, err := b.eth.blockchain.StateIndexProgress()
stateRemain, trienodeRemain, err := b.eth.blockchain.StateIndexProgress()
if err == nil {
prog.StateIndexRemaining = remain
prog.StateIndexRemaining = stateRemain
prog.TrienodeIndexRemaining = trienodeRemain
}
return prog
}
@ -484,12 +485,12 @@ func (b *EthAPIBackend) CurrentHeader() *types.Header {
return b.eth.blockchain.CurrentHeader()
}
func (b *EthAPIBackend) StateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, readOnly bool, preferDisk bool) (*state.StateDB, tracers.StateReleaseFunc, error) {
return b.eth.stateAtBlock(ctx, block, reexec, base, readOnly, preferDisk)
func (b *EthAPIBackend) StateAtBlock(ctx context.Context, block *types.Block, base *state.StateDB, readOnly bool, preferDisk bool) (*state.StateDB, tracers.StateReleaseFunc, error) {
return b.eth.stateAtBlock(ctx, block, base, readOnly, preferDisk)
}
func (b *EthAPIBackend) StateAtTransaction(ctx context.Context, block *types.Block, txIndex int, reexec uint64) (*types.Transaction, vm.BlockContext, *state.StateDB, tracers.StateReleaseFunc, error) {
return b.eth.stateAtTransaction(ctx, block, txIndex, reexec)
func (b *EthAPIBackend) StateAtTransaction(ctx context.Context, block *types.Block, txIndex int) (*types.Transaction, vm.BlockContext, *state.StateDB, tracers.StateReleaseFunc, error) {
return b.eth.stateAtTransaction(ctx, block, txIndex)
}
func (b *EthAPIBackend) RPCTxSyncDefaultTimeout() time.Duration {

View file

@ -222,7 +222,7 @@ func (api *DebugAPI) StorageRangeAt(ctx context.Context, blockNrOrHash rpc.Block
if block == nil {
return StorageRangeResult{}, fmt.Errorf("block %v not found", blockNrOrHash)
}
_, _, statedb, release, err := api.eth.stateAtTransaction(ctx, block, txIndex, 0)
_, _, statedb, release, err := api.eth.stateAtTransaction(ctx, block, txIndex)
if err != nil {
return StorageRangeResult{}, err
}
@ -236,6 +236,8 @@ func storageRangeAt(statedb *state.StateDB, root common.Hash, address common.Add
if storageRoot == types.EmptyRootHash || storageRoot == (common.Hash{}) {
return StorageRangeResult{}, nil // empty storage
}
// TODO(rjl493456442) it's problematic for traversing the state with in-memory
// state mutations, specifically txIndex != 0.
id := trie.StorageTrieID(root, crypto.Keccak256Hash(address.Bytes()), storageRoot)
tr, err := trie.NewStateTrie(id, statedb.Database().TrieDB())
if err != nil {

View file

@ -81,9 +81,10 @@ func (api *DownloaderAPI) eventLoop() {
prog.TxIndexFinishedBlocks = txProg.Indexed
prog.TxIndexRemainingBlocks = txProg.Remaining
}
remain, err := api.chain.StateIndexProgress()
stateRemain, trienodeRemain, err := api.chain.StateIndexProgress()
if err == nil {
prog.StateIndexRemaining = remain
prog.StateIndexRemaining = stateRemain
prog.TrienodeIndexRemaining = trienodeRemain
}
return prog
}

View file

@ -370,7 +370,7 @@ func (dlp *downloadTesterPeer) RequestTrieNodes(id uint64, root common.Hash, cou
Paths: encPaths,
Bytes: uint64(bytes),
}
nodes, _ := snap.ServiceGetTrieNodesQuery(dlp.chain, req, time.Now())
nodes, _ := snap.ServiceGetTrieNodesQuery(dlp.chain, req)
go dlp.dl.downloader.SnapSyncer.OnTrieNodes(dlp, id, nodes)
return nil
}

View file

@ -19,7 +19,6 @@ package filters
import (
"context"
"errors"
"fmt"
"math"
"math/big"
"slices"
@ -147,7 +146,7 @@ func (f *Filter) Logs(ctx context.Context) ([]*types.Log, error) {
return nil, err
}
if f.rangeLimit != 0 && (end-begin) > f.rangeLimit {
return nil, fmt.Errorf("exceed maximum block range: %d", f.rangeLimit)
return nil, invalidParamsErr("exceed maximum block range %d", f.rangeLimit)
}
return f.rangeLogs(ctx, begin, end)
}

View file

@ -19,6 +19,7 @@ package filters
import (
"context"
"encoding/json"
"errors"
"math/big"
"strings"
"testing"
@ -634,7 +635,19 @@ func TestRangeLimit(t *testing.T) {
// Set rangeLimit to 5, but request a range of 9 (end - begin = 9, from 0 to 9)
filter := sys.NewRangeFilter(0, 9, nil, nil, 5)
_, err = filter.Logs(context.Background())
if err == nil || !strings.Contains(err.Error(), "exceed maximum block range") {
t.Fatalf("expected range limit error, got %v", err)
if err == nil {
t.Fatal("expected range limit error, got nil")
}
var re rpc.Error
if errors.As(err, &re) {
if re.ErrorCode() != -32602 {
t.Fatalf("expected error code -32602, got %d", re.ErrorCode())
}
if re.Error() != "exceed maximum block range 5" {
t.Fatalf("expected error message 'exceed maximum block range 5', got %q", re.Error())
}
} else {
t.Fatalf("expected rpc error, got %v", err)
}
}

View file

@ -27,7 +27,6 @@ import (
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/internal/ethapi/override"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
)
@ -38,11 +37,12 @@ import (
// these together, it would be excessively hard to test. Splitting the parts out
// allows testing without needing a proper live chain.
type Options struct {
Config *params.ChainConfig // Chain configuration for hard fork selection
Chain core.ChainContext // Chain context to access past block hashes
Header *types.Header // Header defining the block context to execute in
State *state.StateDB // Pre-state on top of which to estimate the gas
BlockOverrides *override.BlockOverrides // Block overrides to apply during the estimation
Config *params.ChainConfig // Chain configuration for hard fork selection
Chain core.ChainContext // Chain context to access past block hashes
Header *types.Header // Header defining the block context to execute in
State *state.StateDB // Pre-state on top of which to estimate the gas
BlobBaseFee *big.Int // BlobBaseFee optionally overrides the blob base fee in the execution context.
ErrorRatio float64 // Allowed overestimation ratio for faster estimation termination
}
@ -64,16 +64,7 @@ func Estimate(ctx context.Context, call *core.Message, opts *Options, gasCap uin
// Cap the maximum gas allowance according to EIP-7825 if the estimation targets Osaka
if hi > params.MaxTxGas {
blockNumber, blockTime := opts.Header.Number, opts.Header.Time
if opts.BlockOverrides != nil {
if opts.BlockOverrides.Number != nil {
blockNumber = opts.BlockOverrides.Number.ToInt()
}
if opts.BlockOverrides.Time != nil {
blockTime = uint64(*opts.BlockOverrides.Time)
}
}
if opts.Config.IsOsaka(blockNumber, blockTime) {
if opts.Config.IsOsaka(opts.Header.Number, opts.Header.Time) {
hi = params.MaxTxGas
}
}
@ -241,10 +232,8 @@ func run(ctx context.Context, call *core.Message, opts *Options) (*core.Executio
evmContext = core.NewEVMBlockContext(opts.Header, opts.Chain, nil)
dirtyState = opts.State.Copy()
)
if opts.BlockOverrides != nil {
if err := opts.BlockOverrides.Apply(&evmContext); err != nil {
return nil, err
}
if opts.BlobBaseFee != nil {
evmContext.BlobBaseFee = new(big.Int).Set(opts.BlobBaseFee)
}
// Lower the basefee to 0 to avoid breaking EVM
// invariants (basefee < feecap).

View file

@ -167,7 +167,6 @@ func Handle(backend Backend, peer *Peer) error {
type msgHandler func(backend Backend, msg Decoder, peer *Peer) error
type Decoder interface {
Decode(val interface{}) error
Time() time.Time
}
var eth69 = map[uint64]msgHandler{

View file

@ -17,25 +17,14 @@
package snap
import (
"bytes"
"fmt"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state/snapshot"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/p2p/enr"
"github.com/ethereum/go-ethereum/p2p/tracker"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie"
"github.com/ethereum/go-ethereum/trie/trienode"
"github.com/ethereum/go-ethereum/triedb/database"
)
const (
@ -55,6 +44,10 @@ const (
// number is there to limit the number of disk lookups.
maxTrieNodeLookups = 1024
// maxAccessListLookups is the maximum number of BALs to server. This number
// is there to limit the number of disk lookups.
maxAccessListLookups = 1024
// maxTrieNodeTimeSpent is the maximum time we should spend on looking up trie nodes.
// If we spend too much time, then it's a fairly high chance of timing out
// at the remote side, which means all the work is in vain.
@ -123,6 +116,34 @@ func Handle(backend Backend, peer *Peer) error {
}
}
type msgHandler func(backend Backend, msg Decoder, peer *Peer) error
type Decoder interface {
Decode(val interface{}) error
}
var snap1 = map[uint64]msgHandler{
GetAccountRangeMsg: handleGetAccountRange,
AccountRangeMsg: handleAccountRange,
GetStorageRangesMsg: handleGetStorageRanges,
StorageRangesMsg: handleStorageRanges,
GetByteCodesMsg: handleGetByteCodes,
ByteCodesMsg: handleByteCodes,
GetTrieNodesMsg: handleGetTrienodes,
TrieNodesMsg: handleTrieNodes,
}
// nolint:unused
var snap2 = map[uint64]msgHandler{
GetAccountRangeMsg: handleGetAccountRange,
AccountRangeMsg: handleAccountRange,
GetStorageRangesMsg: handleGetStorageRanges,
StorageRangesMsg: handleStorageRanges,
GetByteCodesMsg: handleGetByteCodes,
ByteCodesMsg: handleByteCodes,
GetAccessListsMsg: handleGetAccessLists,
// AccessListsMsg: TODO
}
// HandleMessage is invoked whenever an inbound message is received from a
// remote peer on the `snap` protocol. The remote connection is torn down upon
// returning any error.
@ -136,8 +157,19 @@ func HandleMessage(backend Backend, peer *Peer) error {
return fmt.Errorf("%w: %v > %v", errMsgTooLarge, msg.Size, maxMessageSize)
}
defer msg.Discard()
start := time.Now()
var handlers map[uint64]msgHandler
switch peer.version {
case SNAP1:
handlers = snap1
//case SNAP2:
// handlers = snap2
default:
return fmt.Errorf("unknown eth protocol version: %v", peer.version)
}
// Track the amount of time it takes to serve the request and run the handler
start := time.Now()
if metrics.Enabled() {
h := fmt.Sprintf("%s/%s/%d/%#02x", p2p.HandleHistName, ProtocolName, peer.Version(), msg.Code)
defer func(start time.Time) {
@ -149,520 +181,11 @@ func HandleMessage(backend Backend, peer *Peer) error {
metrics.GetOrRegisterHistogramLazy(h, nil, sampler).Update(time.Since(start).Microseconds())
}(start)
}
// Handle the message depending on its contents
switch {
case msg.Code == GetAccountRangeMsg:
var req GetAccountRangePacket
if err := msg.Decode(&req); err != nil {
return fmt.Errorf("%w: message %v: %v", errDecode, msg, err)
}
// Service the request, potentially returning nothing in case of errors
accounts, proofs := ServiceGetAccountRangeQuery(backend.Chain(), &req)
// Send back anything accumulated (or empty in case of errors)
return p2p.Send(peer.rw, AccountRangeMsg, &AccountRangePacket{
ID: req.ID,
Accounts: accounts,
Proof: proofs,
})
case msg.Code == AccountRangeMsg:
res := new(accountRangeInput)
if err := msg.Decode(res); err != nil {
return fmt.Errorf("%w: message %v: %v", errDecode, msg, err)
}
// Check response validity.
if len := res.Proof.Len(); len > 128 {
return fmt.Errorf("AccountRange: invalid proof (length %d)", len)
}
tresp := tracker.Response{ID: res.ID, MsgCode: AccountRangeMsg, Size: len(res.Accounts.Content())}
if err := peer.tracker.Fulfil(tresp); err != nil {
return err
}
// Decode.
accounts, err := res.Accounts.Items()
if err != nil {
return fmt.Errorf("AccountRange: invalid accounts list: %v", err)
}
proof, err := res.Proof.Items()
if err != nil {
return fmt.Errorf("AccountRange: invalid proof: %v", err)
}
// Ensure the range is monotonically increasing
for i := 1; i < len(accounts); i++ {
if bytes.Compare(accounts[i-1].Hash[:], accounts[i].Hash[:]) >= 0 {
return fmt.Errorf("accounts not monotonically increasing: #%d [%x] vs #%d [%x]", i-1, accounts[i-1].Hash[:], i, accounts[i].Hash[:])
}
}
return backend.Handle(peer, &AccountRangePacket{res.ID, accounts, proof})
case msg.Code == GetStorageRangesMsg:
var req GetStorageRangesPacket
if err := msg.Decode(&req); err != nil {
return fmt.Errorf("%w: message %v: %v", errDecode, msg, err)
}
// Service the request, potentially returning nothing in case of errors
slots, proofs := ServiceGetStorageRangesQuery(backend.Chain(), &req)
// Send back anything accumulated (or empty in case of errors)
return p2p.Send(peer.rw, StorageRangesMsg, &StorageRangesPacket{
ID: req.ID,
Slots: slots,
Proof: proofs,
})
case msg.Code == StorageRangesMsg:
res := new(storageRangesInput)
if err := msg.Decode(res); err != nil {
return fmt.Errorf("%w: message %v: %v", errDecode, msg, err)
}
// Check response validity.
if len := res.Proof.Len(); len > 128 {
return fmt.Errorf("StorageRangesMsg: invalid proof (length %d)", len)
}
tresp := tracker.Response{ID: res.ID, MsgCode: StorageRangesMsg, Size: len(res.Slots.Content())}
if err := peer.tracker.Fulfil(tresp); err != nil {
return fmt.Errorf("StorageRangesMsg: %w", err)
}
// Decode.
slotLists, err := res.Slots.Items()
if err != nil {
return fmt.Errorf("AccountRange: invalid accounts list: %v", err)
}
proof, err := res.Proof.Items()
if err != nil {
return fmt.Errorf("AccountRange: invalid proof: %v", err)
}
// Ensure the ranges are monotonically increasing
for i, slots := range slotLists {
for j := 1; j < len(slots); j++ {
if bytes.Compare(slots[j-1].Hash[:], slots[j].Hash[:]) >= 0 {
return fmt.Errorf("storage slots not monotonically increasing for account #%d: #%d [%x] vs #%d [%x]", i, j-1, slots[j-1].Hash[:], j, slots[j].Hash[:])
}
}
}
return backend.Handle(peer, &StorageRangesPacket{res.ID, slotLists, proof})
case msg.Code == GetByteCodesMsg:
var req GetByteCodesPacket
if err := msg.Decode(&req); err != nil {
return fmt.Errorf("%w: message %v: %v", errDecode, msg, err)
}
// Service the request, potentially returning nothing in case of errors
codes := ServiceGetByteCodesQuery(backend.Chain(), &req)
// Send back anything accumulated (or empty in case of errors)
return p2p.Send(peer.rw, ByteCodesMsg, &ByteCodesPacket{
ID: req.ID,
Codes: codes,
})
case msg.Code == ByteCodesMsg:
res := new(byteCodesInput)
if err := msg.Decode(res); err != nil {
return fmt.Errorf("%w: message %v: %v", errDecode, msg, err)
}
length := res.Codes.Len()
tresp := tracker.Response{ID: res.ID, MsgCode: ByteCodesMsg, Size: length}
if err := peer.tracker.Fulfil(tresp); err != nil {
return fmt.Errorf("ByteCodes: %w", err)
}
codes, err := res.Codes.Items()
if err != nil {
return fmt.Errorf("ByteCodes: %w", err)
}
return backend.Handle(peer, &ByteCodesPacket{res.ID, codes})
case msg.Code == GetTrieNodesMsg:
var req GetTrieNodesPacket
if err := msg.Decode(&req); err != nil {
return fmt.Errorf("%w: message %v: %v", errDecode, msg, err)
}
// Service the request, potentially returning nothing in case of errors
nodes, err := ServiceGetTrieNodesQuery(backend.Chain(), &req, start)
if err != nil {
return err
}
// Send back anything accumulated (or empty in case of errors)
return p2p.Send(peer.rw, TrieNodesMsg, &TrieNodesPacket{
ID: req.ID,
Nodes: nodes,
})
case msg.Code == TrieNodesMsg:
res := new(trieNodesInput)
if err := msg.Decode(res); err != nil {
return fmt.Errorf("%w: message %v: %v", errDecode, msg, err)
}
tresp := tracker.Response{ID: res.ID, MsgCode: TrieNodesMsg, Size: res.Nodes.Len()}
if err := peer.tracker.Fulfil(tresp); err != nil {
return fmt.Errorf("TrieNodes: %w", err)
}
nodes, err := res.Nodes.Items()
if err != nil {
return fmt.Errorf("TrieNodes: %w", err)
}
return backend.Handle(peer, &TrieNodesPacket{res.ID, nodes})
default:
return fmt.Errorf("%w: %v", errInvalidMsgCode, msg.Code)
if handler := handlers[msg.Code]; handler != nil {
return handler(backend, msg, peer)
}
}
// ServiceGetAccountRangeQuery assembles the response to an account range query.
// It is exposed to allow external packages to test protocol behavior.
func ServiceGetAccountRangeQuery(chain *core.BlockChain, req *GetAccountRangePacket) ([]*AccountData, [][]byte) {
if req.Bytes > softResponseLimit {
req.Bytes = softResponseLimit
}
// Retrieve the requested state and bail out if non existent
tr, err := trie.New(trie.StateTrieID(req.Root), chain.TrieDB())
if err != nil {
return nil, nil
}
// Temporary solution: using the snapshot interface for both cases.
// This can be removed once the hash scheme is deprecated.
var it snapshot.AccountIterator
if chain.TrieDB().Scheme() == rawdb.HashScheme {
// The snapshot is assumed to be available in hash mode if
// the SNAP protocol is enabled.
it, err = chain.Snapshots().AccountIterator(req.Root, req.Origin)
} else {
it, err = chain.TrieDB().AccountIterator(req.Root, req.Origin)
}
if err != nil {
return nil, nil
}
// Iterate over the requested range and pile accounts up
var (
accounts []*AccountData
size uint64
last common.Hash
)
for it.Next() {
hash, account := it.Hash(), common.CopyBytes(it.Account())
// Track the returned interval for the Merkle proofs
last = hash
// Assemble the reply item
size += uint64(common.HashLength + len(account))
accounts = append(accounts, &AccountData{
Hash: hash,
Body: account,
})
// If we've exceeded the request threshold, abort
if bytes.Compare(hash[:], req.Limit[:]) >= 0 {
break
}
if size > req.Bytes {
break
}
}
it.Release()
// Generate the Merkle proofs for the first and last account
proof := trienode.NewProofSet()
if err := tr.Prove(req.Origin[:], proof); err != nil {
log.Warn("Failed to prove account range", "origin", req.Origin, "err", err)
return nil, nil
}
if last != (common.Hash{}) {
if err := tr.Prove(last[:], proof); err != nil {
log.Warn("Failed to prove account range", "last", last, "err", err)
return nil, nil
}
}
return accounts, proof.List()
}
func ServiceGetStorageRangesQuery(chain *core.BlockChain, req *GetStorageRangesPacket) ([][]*StorageData, [][]byte) {
if req.Bytes > softResponseLimit {
req.Bytes = softResponseLimit
}
// TODO(karalabe): Do we want to enforce > 0 accounts and 1 account if origin is set?
// TODO(karalabe): - Logging locally is not ideal as remote faults annoy the local user
// TODO(karalabe): - Dropping the remote peer is less flexible wrt client bugs (slow is better than non-functional)
// Calculate the hard limit at which to abort, even if mid storage trie
hardLimit := uint64(float64(req.Bytes) * (1 + stateLookupSlack))
// Retrieve storage ranges until the packet limit is reached
var (
slots [][]*StorageData
proofs [][]byte
size uint64
)
for _, account := range req.Accounts {
// If we've exceeded the requested data limit, abort without opening
// a new storage range (that we'd need to prove due to exceeded size)
if size >= req.Bytes {
break
}
// The first account might start from a different origin and end sooner
var origin common.Hash
if len(req.Origin) > 0 {
origin, req.Origin = common.BytesToHash(req.Origin), nil
}
var limit = common.MaxHash
if len(req.Limit) > 0 {
limit, req.Limit = common.BytesToHash(req.Limit), nil
}
// Retrieve the requested state and bail out if non existent
var (
err error
it snapshot.StorageIterator
)
// Temporary solution: using the snapshot interface for both cases.
// This can be removed once the hash scheme is deprecated.
if chain.TrieDB().Scheme() == rawdb.HashScheme {
// The snapshot is assumed to be available in hash mode if
// the SNAP protocol is enabled.
it, err = chain.Snapshots().StorageIterator(req.Root, account, origin)
} else {
it, err = chain.TrieDB().StorageIterator(req.Root, account, origin)
}
if err != nil {
return nil, nil
}
// Iterate over the requested range and pile slots up
var (
storage []*StorageData
last common.Hash
abort bool
)
for it.Next() {
if size >= hardLimit {
abort = true
break
}
hash, slot := it.Hash(), common.CopyBytes(it.Slot())
// Track the returned interval for the Merkle proofs
last = hash
// Assemble the reply item
size += uint64(common.HashLength + len(slot))
storage = append(storage, &StorageData{
Hash: hash,
Body: slot,
})
// If we've exceeded the request threshold, abort
if bytes.Compare(hash[:], limit[:]) >= 0 {
break
}
}
if len(storage) > 0 {
slots = append(slots, storage)
}
it.Release()
// Generate the Merkle proofs for the first and last storage slot, but
// only if the response was capped. If the entire storage trie included
// in the response, no need for any proofs.
if origin != (common.Hash{}) || (abort && len(storage) > 0) {
// Request started at a non-zero hash or was capped prematurely, add
// the endpoint Merkle proofs
accTrie, err := trie.NewStateTrie(trie.StateTrieID(req.Root), chain.TrieDB())
if err != nil {
return nil, nil
}
acc, err := accTrie.GetAccountByHash(account)
if err != nil || acc == nil {
return nil, nil
}
id := trie.StorageTrieID(req.Root, account, acc.Root)
stTrie, err := trie.NewStateTrie(id, chain.TrieDB())
if err != nil {
return nil, nil
}
proof := trienode.NewProofSet()
if err := stTrie.Prove(origin[:], proof); err != nil {
log.Warn("Failed to prove storage range", "origin", req.Origin, "err", err)
return nil, nil
}
if last != (common.Hash{}) {
if err := stTrie.Prove(last[:], proof); err != nil {
log.Warn("Failed to prove storage range", "last", last, "err", err)
return nil, nil
}
}
proofs = append(proofs, proof.List()...)
// Proof terminates the reply as proofs are only added if a node
// refuses to serve more data (exception when a contract fetch is
// finishing, but that's that).
break
}
}
return slots, proofs
}
// ServiceGetByteCodesQuery assembles the response to a byte codes query.
// It is exposed to allow external packages to test protocol behavior.
func ServiceGetByteCodesQuery(chain *core.BlockChain, req *GetByteCodesPacket) [][]byte {
if req.Bytes > softResponseLimit {
req.Bytes = softResponseLimit
}
if len(req.Hashes) > maxCodeLookups {
req.Hashes = req.Hashes[:maxCodeLookups]
}
// Retrieve bytecodes until the packet size limit is reached
var (
codes [][]byte
bytes uint64
)
for _, hash := range req.Hashes {
if hash == types.EmptyCodeHash {
// Peers should not request the empty code, but if they do, at
// least sent them back a correct response without db lookups
codes = append(codes, []byte{})
} else if blob := chain.ContractCodeWithPrefix(hash); len(blob) > 0 {
codes = append(codes, blob)
bytes += uint64(len(blob))
}
if bytes > req.Bytes {
break
}
}
return codes
}
// ServiceGetTrieNodesQuery assembles the response to a trie nodes query.
// It is exposed to allow external packages to test protocol behavior.
func ServiceGetTrieNodesQuery(chain *core.BlockChain, req *GetTrieNodesPacket, start time.Time) ([][]byte, error) {
if req.Bytes > softResponseLimit {
req.Bytes = softResponseLimit
}
// Make sure we have the state associated with the request
triedb := chain.TrieDB()
accTrie, err := trie.NewStateTrie(trie.StateTrieID(req.Root), triedb)
if err != nil {
// We don't have the requested state available, bail out
return nil, nil
}
// The 'reader' might be nil, in which case we cannot serve storage slots
// via snapshot.
var reader database.StateReader
if chain.Snapshots() != nil {
reader = chain.Snapshots().Snapshot(req.Root)
}
if reader == nil {
reader, _ = triedb.StateReader(req.Root)
}
// Retrieve trie nodes until the packet size limit is reached
var (
outerIt = req.Paths.ContentIterator()
nodes [][]byte
bytes uint64
loads int // Trie hash expansions to count database reads
)
for outerIt.Next() {
innerIt, err := rlp.NewListIterator(outerIt.Value())
if err != nil {
return nodes, err
}
switch innerIt.Count() {
case 0:
// Ensure we penalize invalid requests
return nil, fmt.Errorf("%w: zero-item pathset requested", errBadRequest)
case 1:
// If we're only retrieving an account trie node, fetch it directly
accKey := nextBytes(&innerIt)
if accKey == nil {
return nodes, fmt.Errorf("%w: invalid account node request", errBadRequest)
}
blob, resolved, err := accTrie.GetNode(accKey)
loads += resolved // always account database reads, even for failures
if err != nil {
break
}
nodes = append(nodes, blob)
bytes += uint64(len(blob))
default:
// Storage slots requested, open the storage trie and retrieve from there
accKey := nextBytes(&innerIt)
if accKey == nil {
return nodes, fmt.Errorf("%w: invalid account storage request", errBadRequest)
}
var stRoot common.Hash
if reader == nil {
// We don't have the requested state snapshotted yet (or it is stale),
// but can look up the account via the trie instead.
account, err := accTrie.GetAccountByHash(common.BytesToHash(accKey))
loads += 8 // We don't know the exact cost of lookup, this is an estimate
if err != nil || account == nil {
break
}
stRoot = account.Root
} else {
account, err := reader.Account(common.BytesToHash(accKey))
loads++ // always account database reads, even for failures
if err != nil || account == nil {
break
}
stRoot = common.BytesToHash(account.Root)
}
id := trie.StorageTrieID(req.Root, common.BytesToHash(accKey), stRoot)
stTrie, err := trie.NewStateTrie(id, triedb)
loads++ // always account database reads, even for failures
if err != nil {
break
}
for innerIt.Next() {
path, _, err := rlp.SplitString(innerIt.Value())
if err != nil {
return nil, fmt.Errorf("%w: invalid storage key: %v", errBadRequest, err)
}
blob, resolved, err := stTrie.GetNode(path)
loads += resolved // always account database reads, even for failures
if err != nil {
break
}
nodes = append(nodes, blob)
bytes += uint64(len(blob))
// Sanity check limits to avoid DoS on the store trie loads
if bytes > req.Bytes || loads > maxTrieNodeLookups || time.Since(start) > maxTrieNodeTimeSpent {
break
}
}
}
// Abort request processing if we've exceeded our limits
if bytes > req.Bytes || loads > maxTrieNodeLookups || time.Since(start) > maxTrieNodeTimeSpent {
break
}
}
return nodes, nil
}
func nextBytes(it *rlp.Iterator) []byte {
if !it.Next() {
return nil
}
content, _, err := rlp.SplitString(it.Value())
if err != nil {
return nil
}
return content
return fmt.Errorf("%w: %v", errInvalidMsgCode, msg.Code)
}
// NodeInfo represents a short summary of the `snap` sub-protocol metadata

View file

@ -60,6 +60,12 @@ func FuzzTrieNodes(f *testing.F) {
})
}
func FuzzAccessLists(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
doFuzz(data, &GetAccessListsPacket{}, GetAccessListsMsg)
})
}
func doFuzz(input []byte, obj interface{}, code int) {
bc := getChain()
defer bc.Stop()

View file

@ -0,0 +1,314 @@
// Copyright 2026 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/>.
package snap
import (
"bytes"
"encoding/binary"
"reflect"
"testing"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus/beacon"
"github.com/ethereum/go-ethereum/consensus/ethash"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp"
)
func makeTestBAL(minSize int) *bal.BlockAccessList {
n := minSize/33 + 1 // 33 bytes per storage read slot in RLP
access := bal.AccountAccess{
Address: common.HexToAddress("0x01"),
StorageReads: make([][32]byte, n),
}
for i := range access.StorageReads {
binary.BigEndian.PutUint64(access.StorageReads[i][24:], uint64(i))
}
return &bal.BlockAccessList{Accesses: []bal.AccountAccess{access}}
}
// getChainWithBALs creates a minimal test chain with BALs stored for each block.
// It returns the chain, block hashes, and the stored BAL data.
func getChainWithBALs(nBlocks int, balSize int) (*core.BlockChain, []common.Hash, []rlp.RawValue) {
gspec := &core.Genesis{
Config: params.MergedTestChainConfig,
}
db := rawdb.NewMemoryDatabase()
engine := beacon.New(ethash.NewFaker())
_, blocks, _ := core.GenerateChainWithGenesis(gspec, engine, nBlocks, func(i int, gen *core.BlockGen) {})
options := &core.BlockChainConfig{
StateScheme: rawdb.PathScheme,
TrieTimeLimit: 5 * time.Minute,
NoPrefetch: true,
}
bc, err := core.NewBlockChain(db, gspec, engine, options)
if err != nil {
panic(err)
}
if _, err := bc.InsertChain(blocks); err != nil {
panic(err)
}
// Store BALs for each block
var (
hashes []common.Hash
bals []rlp.RawValue
)
for _, block := range blocks {
hash := block.Hash()
number := block.NumberU64()
// Fill with data based on block number
bytes, err := rlp.EncodeToBytes(makeTestBAL(balSize))
if err != nil {
panic(err)
}
rawdb.WriteAccessListRLP(db, hash, number, bytes)
hashes = append(hashes, hash)
bals = append(bals, bytes)
}
return bc, hashes, bals
}
// TestServiceGetAccessListsQuery verifies that known block hashes return the
// correct BALs with positional correspondence.
func TestServiceGetAccessListsQuery(t *testing.T) {
t.Parallel()
bc, hashes, bals := getChainWithBALs(5, 100)
defer bc.Stop()
req := &GetAccessListsPacket{
ID: 1,
Hashes: hashes,
Bytes: softResponseLimit,
}
result := ServiceGetAccessListsQuery(bc, req)
// Verify the results
if result.Len() != len(hashes) {
t.Fatalf("expected %d results, got %d", len(hashes), result.Len())
}
var (
index int
it = result.ContentIterator()
)
for it.Next() {
if !bytes.Equal(it.Value(), bals[index]) {
t.Errorf("BAL %d mismatch: got %x, want %x", index, it.Value(), bals[index])
}
index++
}
}
// TestServiceGetAccessListsQueryEmpty verifies that unknown block hashes return
// nil placeholders and that mixed known/unknown hashes preserve alignment.
func TestServiceGetAccessListsQueryEmpty(t *testing.T) {
t.Parallel()
bc, hashes, bals := getChainWithBALs(3, 100)
defer bc.Stop()
unknown := common.HexToHash("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
mixed := []common.Hash{hashes[0], unknown, hashes[1], unknown, hashes[2]}
req := &GetAccessListsPacket{
ID: 2,
Hashes: mixed,
Bytes: softResponseLimit,
}
result := ServiceGetAccessListsQuery(bc, req)
// Verify length
if result.Len() != len(mixed) {
t.Fatalf("expected %d results, got %d", len(mixed), result.Len())
}
// Check positional correspondence
var expectVal = []rlp.RawValue{
bals[0], rlp.EmptyString, bals[1], rlp.EmptyString, bals[2],
}
var (
index int
it = result.ContentIterator()
)
for it.Next() {
if !bytes.Equal(it.Value(), expectVal[index]) {
t.Errorf("BAL %d mismatch: got %x, want %x", index, it.Value(), expectVal[index])
}
index++
}
}
// TestServiceGetAccessListsQueryCap verifies that requests exceeding
// maxAccessListLookups are capped.
func TestServiceGetAccessListsQueryCap(t *testing.T) {
t.Parallel()
bc, _, _ := getChainWithBALs(2, 100)
defer bc.Stop()
// Create a request with more hashes than the cap
hashes := make([]common.Hash, maxAccessListLookups+100)
for i := range hashes {
hashes[i] = common.BytesToHash([]byte{byte(i), byte(i >> 8)})
}
req := &GetAccessListsPacket{
ID: 3,
Hashes: hashes,
Bytes: softResponseLimit,
}
result := ServiceGetAccessListsQuery(bc, req)
// Can't get more than maxAccessListLookups results
if result.Len() > maxAccessListLookups {
t.Fatalf("expected at most %d results, got %d", maxAccessListLookups, result.Len())
}
}
// TestServiceGetAccessListsQueryByteLimit verifies that the response stops
// once the byte limit is exceeded. The handler appends the entry that crosses
// the limit before breaking, so the total size will exceed the limit by at
// most one BAL.
func TestServiceGetAccessListsQueryByteLimit(t *testing.T) {
t.Parallel()
// The handler will return 3/5 entries (3MB total) then break.
balSize := 1024 * 1024
nBlocks := 5
bc, hashes, _ := getChainWithBALs(nBlocks, balSize)
defer bc.Stop()
req := &GetAccessListsPacket{
ID: 0,
Hashes: hashes,
Bytes: softResponseLimit,
}
result := ServiceGetAccessListsQuery(bc, req)
// Should have stopped before returning all blocks
if result.Len() >= nBlocks {
t.Fatalf("expected fewer than %d results due to byte limit, got %d", nBlocks, result.Len())
}
// Should have returned at least one
if result.Len() == 0 {
t.Fatal("expected at least one result")
}
// The total size should exceed the limit (the entry that crosses it is included)
if result.Size() <= softResponseLimit {
t.Errorf("total response size %d should exceed soft limit %d (includes one entry past limit)", result.Size(), softResponseLimit)
}
}
// TestGetAccessListResponseDecoding verifies that an AccessListsPacket
// round-trips through RLP encode/decode, preserving positional
// correspondence and correctly representing absent BALs as empty strings.
func TestGetAccessListResponseDecoding(t *testing.T) {
t.Parallel()
// Build two real BALs of different sizes.
bal1 := makeTestBAL(100)
bal2 := makeTestBAL(200)
bytes1, _ := rlp.EncodeToBytes(bal1)
bytes2, _ := rlp.EncodeToBytes(bal2)
tests := []struct {
name string
items []rlp.RawValue // nil entry = unavailable BAL
counts int // expected decoded length
}{
{
name: "all present",
items: []rlp.RawValue{bytes1, bytes2},
counts: 2,
},
{
name: "all absent",
items: []rlp.RawValue{rlp.EmptyString, rlp.EmptyString, rlp.EmptyString},
counts: 3,
},
{
name: "mixed present and absent",
items: []rlp.RawValue{bytes1, rlp.EmptyString, bytes2, rlp.EmptyString},
counts: 4,
},
{
name: "empty response",
items: []rlp.RawValue{},
counts: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Build the packet using Append.
var orig AccessListsPacket
orig.ID = 42
for _, item := range tt.items {
if err := orig.AccessLists.AppendRaw(item); err != nil {
t.Fatalf("AppendRaw failed: %v", err)
}
}
// Encode -> Decode round-trip.
enc, err := rlp.EncodeToBytes(&orig)
if err != nil {
t.Fatalf("encode failed: %v", err)
}
var dec AccessListsPacket
if err := rlp.DecodeBytes(enc, &dec); err != nil {
t.Fatalf("decode failed: %v", err)
}
// Verify ID preserved.
if dec.ID != orig.ID {
t.Fatalf("ID mismatch: got %d, want %d", dec.ID, orig.ID)
}
// Verify element count.
if dec.AccessLists.Len() != tt.counts {
t.Fatalf("length mismatch: got %d, want %d", dec.AccessLists.Len(), tt.counts)
}
// Verify each element positionally.
it := dec.AccessLists.ContentIterator()
for i, want := range tt.items {
if !it.Next() {
t.Fatalf("iterator exhausted at index %d", i)
}
got := it.Value()
if !bytes.Equal(got, want) {
t.Errorf("element %d: got %x, want %x", i, got, want)
}
if !bytes.Equal(got, rlp.EmptyString) {
obj := new(bal.BlockAccessList)
if err := rlp.DecodeBytes(got, obj); err != nil {
t.Fatalf("decode failed: %v", err)
}
if bytes.Equal(got, bytes1) && !reflect.DeepEqual(obj, bal1) {
t.Fatalf("decode failed: got %x, want %x", obj, bal1)
}
if bytes.Equal(got, bytes2) && !reflect.DeepEqual(obj, bal2) {
t.Fatalf("decode failed: got %x, want %x", obj, bal2)
}
}
}
if it.Next() {
t.Error("iterator has extra elements after expected end")
}
})
}
}

View file

@ -0,0 +1,600 @@
// Copyright 2026 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/>
package snap
import (
"bytes"
"fmt"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state/snapshot"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/p2p/tracker"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie"
"github.com/ethereum/go-ethereum/trie/trienode"
"github.com/ethereum/go-ethereum/triedb/database"
)
func handleGetAccountRange(backend Backend, msg Decoder, peer *Peer) error {
var req GetAccountRangePacket
if err := msg.Decode(&req); err != nil {
return fmt.Errorf("%w: message %v: %v", errDecode, msg, err)
}
// Service the request, potentially returning nothing in case of errors
accounts, proofs := ServiceGetAccountRangeQuery(backend.Chain(), &req)
// Send back anything accumulated (or empty in case of errors)
return p2p.Send(peer.rw, AccountRangeMsg, &AccountRangePacket{
ID: req.ID,
Accounts: accounts,
Proof: proofs,
})
}
// ServiceGetAccountRangeQuery assembles the response to an account range query.
// It is exposed to allow external packages to test protocol behavior.
func ServiceGetAccountRangeQuery(chain *core.BlockChain, req *GetAccountRangePacket) ([]*AccountData, [][]byte) {
if req.Bytes > softResponseLimit {
req.Bytes = softResponseLimit
}
// Retrieve the requested state and bail out if non existent
tr, err := trie.New(trie.StateTrieID(req.Root), chain.TrieDB())
if err != nil {
return nil, nil
}
// Temporary solution: using the snapshot interface for both cases.
// This can be removed once the hash scheme is deprecated.
var it snapshot.AccountIterator
if chain.TrieDB().Scheme() == rawdb.HashScheme {
// The snapshot is assumed to be available in hash mode if
// the SNAP protocol is enabled.
it, err = chain.Snapshots().AccountIterator(req.Root, req.Origin)
} else {
it, err = chain.TrieDB().AccountIterator(req.Root, req.Origin)
}
if err != nil {
return nil, nil
}
// Iterate over the requested range and pile accounts up
var (
accounts []*AccountData
size uint64
last common.Hash
)
for it.Next() {
hash, account := it.Hash(), common.CopyBytes(it.Account())
// Track the returned interval for the Merkle proofs
last = hash
// Assemble the reply item
size += uint64(common.HashLength + len(account))
accounts = append(accounts, &AccountData{
Hash: hash,
Body: account,
})
// If we've exceeded the request threshold, abort
if bytes.Compare(hash[:], req.Limit[:]) >= 0 {
break
}
if size > req.Bytes {
break
}
}
it.Release()
// Generate the Merkle proofs for the first and last account
proof := trienode.NewProofSet()
if err := tr.Prove(req.Origin[:], proof); err != nil {
log.Warn("Failed to prove account range", "origin", req.Origin, "err", err)
return nil, nil
}
if last != (common.Hash{}) {
if err := tr.Prove(last[:], proof); err != nil {
log.Warn("Failed to prove account range", "last", last, "err", err)
return nil, nil
}
}
return accounts, proof.List()
}
func handleAccountRange(backend Backend, msg Decoder, peer *Peer) error {
res := new(accountRangeInput)
if err := msg.Decode(res); err != nil {
return fmt.Errorf("%w: message %v: %v", errDecode, msg, err)
}
// Check response validity.
if len := res.Proof.Len(); len > 128 {
return fmt.Errorf("AccountRange: invalid proof (length %d)", len)
}
tresp := tracker.Response{ID: res.ID, MsgCode: AccountRangeMsg, Size: len(res.Accounts.Content())}
if err := peer.tracker.Fulfil(tresp); err != nil {
return err
}
// Decode.
accounts, err := res.Accounts.Items()
if err != nil {
return fmt.Errorf("AccountRange: invalid accounts list: %v", err)
}
proof, err := res.Proof.Items()
if err != nil {
return fmt.Errorf("AccountRange: invalid proof: %v", err)
}
// Ensure the range is monotonically increasing
for i := 1; i < len(accounts); i++ {
if bytes.Compare(accounts[i-1].Hash[:], accounts[i].Hash[:]) >= 0 {
return fmt.Errorf("accounts not monotonically increasing: #%d [%x] vs #%d [%x]", i-1, accounts[i-1].Hash[:], i, accounts[i].Hash[:])
}
}
return backend.Handle(peer, &AccountRangePacket{res.ID, accounts, proof})
}
func handleGetStorageRanges(backend Backend, msg Decoder, peer *Peer) error {
var req GetStorageRangesPacket
if err := msg.Decode(&req); err != nil {
return fmt.Errorf("%w: message %v: %v", errDecode, msg, err)
}
// Service the request, potentially returning nothing in case of errors
slots, proofs := ServiceGetStorageRangesQuery(backend.Chain(), &req)
// Send back anything accumulated (or empty in case of errors)
return p2p.Send(peer.rw, StorageRangesMsg, &StorageRangesPacket{
ID: req.ID,
Slots: slots,
Proof: proofs,
})
}
func ServiceGetStorageRangesQuery(chain *core.BlockChain, req *GetStorageRangesPacket) ([][]*StorageData, [][]byte) {
if req.Bytes > softResponseLimit {
req.Bytes = softResponseLimit
}
// TODO(karalabe): Do we want to enforce > 0 accounts and 1 account if origin is set?
// TODO(karalabe): - Logging locally is not ideal as remote faults annoy the local user
// TODO(karalabe): - Dropping the remote peer is less flexible wrt client bugs (slow is better than non-functional)
// Calculate the hard limit at which to abort, even if mid storage trie
hardLimit := uint64(float64(req.Bytes) * (1 + stateLookupSlack))
// Retrieve storage ranges until the packet limit is reached
var (
slots [][]*StorageData
proofs [][]byte
size uint64
)
for _, account := range req.Accounts {
// If we've exceeded the requested data limit, abort without opening
// a new storage range (that we'd need to prove due to exceeded size)
if size >= req.Bytes {
break
}
// The first account might start from a different origin and end sooner
var origin common.Hash
if len(req.Origin) > 0 {
origin, req.Origin = common.BytesToHash(req.Origin), nil
}
var limit = common.MaxHash
if len(req.Limit) > 0 {
limit, req.Limit = common.BytesToHash(req.Limit), nil
}
// Retrieve the requested state and bail out if non existent
var (
err error
it snapshot.StorageIterator
)
// Temporary solution: using the snapshot interface for both cases.
// This can be removed once the hash scheme is deprecated.
if chain.TrieDB().Scheme() == rawdb.HashScheme {
// The snapshot is assumed to be available in hash mode if
// the SNAP protocol is enabled.
it, err = chain.Snapshots().StorageIterator(req.Root, account, origin)
} else {
it, err = chain.TrieDB().StorageIterator(req.Root, account, origin)
}
if err != nil {
return nil, nil
}
// Iterate over the requested range and pile slots up
var (
storage []*StorageData
last common.Hash
abort bool
)
for it.Next() {
if size >= hardLimit {
abort = true
break
}
hash, slot := it.Hash(), common.CopyBytes(it.Slot())
// Track the returned interval for the Merkle proofs
last = hash
// Assemble the reply item
size += uint64(common.HashLength + len(slot))
storage = append(storage, &StorageData{
Hash: hash,
Body: slot,
})
// If we've exceeded the request threshold, abort
if bytes.Compare(hash[:], limit[:]) >= 0 {
break
}
}
if len(storage) > 0 {
slots = append(slots, storage)
}
it.Release()
// Generate the Merkle proofs for the first and last storage slot, but
// only if the response was capped. If the entire storage trie included
// in the response, no need for any proofs.
if origin != (common.Hash{}) || (abort && len(storage) > 0) {
// Request started at a non-zero hash or was capped prematurely, add
// the endpoint Merkle proofs
accTrie, err := trie.NewStateTrie(trie.StateTrieID(req.Root), chain.TrieDB())
if err != nil {
return nil, nil
}
acc, err := accTrie.GetAccountByHash(account)
if err != nil || acc == nil {
return nil, nil
}
id := trie.StorageTrieID(req.Root, account, acc.Root)
stTrie, err := trie.NewStateTrie(id, chain.TrieDB())
if err != nil {
return nil, nil
}
proof := trienode.NewProofSet()
if err := stTrie.Prove(origin[:], proof); err != nil {
log.Warn("Failed to prove storage range", "origin", req.Origin, "err", err)
return nil, nil
}
if last != (common.Hash{}) {
if err := stTrie.Prove(last[:], proof); err != nil {
log.Warn("Failed to prove storage range", "last", last, "err", err)
return nil, nil
}
}
proofs = append(proofs, proof.List()...)
// Proof terminates the reply as proofs are only added if a node
// refuses to serve more data (exception when a contract fetch is
// finishing, but that's that).
break
}
}
return slots, proofs
}
func handleStorageRanges(backend Backend, msg Decoder, peer *Peer) error {
res := new(storageRangesInput)
if err := msg.Decode(res); err != nil {
return fmt.Errorf("%w: message %v: %v", errDecode, msg, err)
}
// Check response validity.
if len := res.Proof.Len(); len > 128 {
return fmt.Errorf("StorageRangesMsg: invalid proof (length %d)", len)
}
tresp := tracker.Response{ID: res.ID, MsgCode: StorageRangesMsg, Size: len(res.Slots.Content())}
if err := peer.tracker.Fulfil(tresp); err != nil {
return fmt.Errorf("StorageRangesMsg: %w", err)
}
// Decode.
slotLists, err := res.Slots.Items()
if err != nil {
return fmt.Errorf("AccountRange: invalid accounts list: %v", err)
}
proof, err := res.Proof.Items()
if err != nil {
return fmt.Errorf("AccountRange: invalid proof: %v", err)
}
// Ensure the ranges are monotonically increasing
for i, slots := range slotLists {
for j := 1; j < len(slots); j++ {
if bytes.Compare(slots[j-1].Hash[:], slots[j].Hash[:]) >= 0 {
return fmt.Errorf("storage slots not monotonically increasing for account #%d: #%d [%x] vs #%d [%x]", i, j-1, slots[j-1].Hash[:], j, slots[j].Hash[:])
}
}
}
return backend.Handle(peer, &StorageRangesPacket{res.ID, slotLists, proof})
}
func handleGetByteCodes(backend Backend, msg Decoder, peer *Peer) error {
var req GetByteCodesPacket
if err := msg.Decode(&req); err != nil {
return fmt.Errorf("%w: message %v: %v", errDecode, msg, err)
}
// Service the request, potentially returning nothing in case of errors
codes := ServiceGetByteCodesQuery(backend.Chain(), &req)
// Send back anything accumulated (or empty in case of errors)
return p2p.Send(peer.rw, ByteCodesMsg, &ByteCodesPacket{
ID: req.ID,
Codes: codes,
})
}
// ServiceGetByteCodesQuery assembles the response to a byte codes query.
// It is exposed to allow external packages to test protocol behavior.
func ServiceGetByteCodesQuery(chain *core.BlockChain, req *GetByteCodesPacket) [][]byte {
if req.Bytes > softResponseLimit {
req.Bytes = softResponseLimit
}
if len(req.Hashes) > maxCodeLookups {
req.Hashes = req.Hashes[:maxCodeLookups]
}
// Retrieve bytecodes until the packet size limit is reached
var (
codes [][]byte
bytes uint64
)
for _, hash := range req.Hashes {
if hash == types.EmptyCodeHash {
// Peers should not request the empty code, but if they do, at
// least sent them back a correct response without db lookups
codes = append(codes, []byte{})
} else if blob := chain.ContractCodeWithPrefix(hash); len(blob) > 0 {
codes = append(codes, blob)
bytes += uint64(len(blob))
}
if bytes > req.Bytes {
break
}
}
return codes
}
func handleByteCodes(backend Backend, msg Decoder, peer *Peer) error {
res := new(byteCodesInput)
if err := msg.Decode(res); err != nil {
return fmt.Errorf("%w: message %v: %v", errDecode, msg, err)
}
length := res.Codes.Len()
tresp := tracker.Response{ID: res.ID, MsgCode: ByteCodesMsg, Size: length}
if err := peer.tracker.Fulfil(tresp); err != nil {
return fmt.Errorf("ByteCodes: %w", err)
}
codes, err := res.Codes.Items()
if err != nil {
return fmt.Errorf("ByteCodes: %w", err)
}
return backend.Handle(peer, &ByteCodesPacket{res.ID, codes})
}
func handleGetTrienodes(backend Backend, msg Decoder, peer *Peer) error {
var req GetTrieNodesPacket
if err := msg.Decode(&req); err != nil {
return fmt.Errorf("%w: message %v: %v", errDecode, msg, err)
}
// Service the request, potentially returning nothing in case of errors
nodes, err := ServiceGetTrieNodesQuery(backend.Chain(), &req)
if err != nil {
return err
}
// Send back anything accumulated (or empty in case of errors)
return p2p.Send(peer.rw, TrieNodesMsg, &TrieNodesPacket{
ID: req.ID,
Nodes: nodes,
})
}
func nextBytes(it *rlp.Iterator) []byte {
if !it.Next() {
return nil
}
content, _, err := rlp.SplitString(it.Value())
if err != nil {
return nil
}
return content
}
// ServiceGetTrieNodesQuery assembles the response to a trie nodes query.
// It is exposed to allow external packages to test protocol behavior.
func ServiceGetTrieNodesQuery(chain *core.BlockChain, req *GetTrieNodesPacket) ([][]byte, error) {
start := time.Now()
if req.Bytes > softResponseLimit {
req.Bytes = softResponseLimit
}
// Make sure we have the state associated with the request
triedb := chain.TrieDB()
accTrie, err := trie.NewStateTrie(trie.StateTrieID(req.Root), triedb)
if err != nil {
// We don't have the requested state available, bail out
return nil, nil
}
// The 'reader' might be nil, in which case we cannot serve storage slots
// via snapshot.
var reader database.StateReader
if chain.Snapshots() != nil {
reader = chain.Snapshots().Snapshot(req.Root)
}
if reader == nil {
reader, _ = triedb.StateReader(req.Root)
}
// Retrieve trie nodes until the packet size limit is reached
var (
outerIt = req.Paths.ContentIterator()
nodes [][]byte
bytes uint64
loads int // Trie hash expansions to count database reads
)
for outerIt.Next() {
innerIt, err := rlp.NewListIterator(outerIt.Value())
if err != nil {
return nodes, err
}
switch innerIt.Count() {
case 0:
// Ensure we penalize invalid requests
return nil, fmt.Errorf("%w: zero-item pathset requested", errBadRequest)
case 1:
// If we're only retrieving an account trie node, fetch it directly
accKey := nextBytes(&innerIt)
if accKey == nil {
return nodes, fmt.Errorf("%w: invalid account node request", errBadRequest)
}
blob, resolved, err := accTrie.GetNode(accKey)
loads += resolved // always account database reads, even for failures
if err != nil {
break
}
nodes = append(nodes, blob)
bytes += uint64(len(blob))
default:
// Storage slots requested, open the storage trie and retrieve from there
accKey := nextBytes(&innerIt)
if accKey == nil {
return nodes, fmt.Errorf("%w: invalid account storage request", errBadRequest)
}
var stRoot common.Hash
if reader == nil {
// We don't have the requested state snapshotted yet (or it is stale),
// but can look up the account via the trie instead.
account, err := accTrie.GetAccountByHash(common.BytesToHash(accKey))
loads += 8 // We don't know the exact cost of lookup, this is an estimate
if err != nil || account == nil {
break
}
stRoot = account.Root
} else {
account, err := reader.Account(common.BytesToHash(accKey))
loads++ // always account database reads, even for failures
if err != nil || account == nil {
break
}
stRoot = common.BytesToHash(account.Root)
}
id := trie.StorageTrieID(req.Root, common.BytesToHash(accKey), stRoot)
stTrie, err := trie.NewStateTrie(id, triedb)
loads++ // always account database reads, even for failures
if err != nil {
break
}
for innerIt.Next() {
path, _, err := rlp.SplitString(innerIt.Value())
if err != nil {
return nil, fmt.Errorf("%w: invalid storage key: %v", errBadRequest, err)
}
blob, resolved, err := stTrie.GetNode(path)
loads += resolved // always account database reads, even for failures
if err != nil {
break
}
nodes = append(nodes, blob)
bytes += uint64(len(blob))
// Sanity check limits to avoid DoS on the store trie loads
if bytes > req.Bytes || loads > maxTrieNodeLookups || time.Since(start) > maxTrieNodeTimeSpent {
break
}
}
}
// Abort request processing if we've exceeded our limits
if bytes > req.Bytes || loads > maxTrieNodeLookups || time.Since(start) > maxTrieNodeTimeSpent {
break
}
}
return nodes, nil
}
func handleTrieNodes(backend Backend, msg Decoder, peer *Peer) error {
res := new(trieNodesInput)
if err := msg.Decode(res); err != nil {
return fmt.Errorf("%w: message %v: %v", errDecode, msg, err)
}
tresp := tracker.Response{ID: res.ID, MsgCode: TrieNodesMsg, Size: res.Nodes.Len()}
if err := peer.tracker.Fulfil(tresp); err != nil {
return fmt.Errorf("TrieNodes: %w", err)
}
nodes, err := res.Nodes.Items()
if err != nil {
return fmt.Errorf("TrieNodes: %w", err)
}
return backend.Handle(peer, &TrieNodesPacket{res.ID, nodes})
}
// nolint:unused
func handleGetAccessLists(backend Backend, msg Decoder, peer *Peer) error {
var req GetAccessListsPacket
if err := msg.Decode(&req); err != nil {
return fmt.Errorf("%w: message %v: %v", errDecode, msg, err)
}
return p2p.Send(peer.rw, AccessListsMsg, &AccessListsPacket{
ID: req.ID,
AccessLists: ServiceGetAccessListsQuery(backend.Chain(), &req),
})
}
// ServiceGetAccessListsQuery assembles the response to an access list query.
// It is exposed to allow external packages to test protocol behavior.
func ServiceGetAccessListsQuery(chain *core.BlockChain, req *GetAccessListsPacket) rlp.RawList[rlp.RawValue] {
if req.Bytes > softResponseLimit {
req.Bytes = softResponseLimit
}
// Cap the number of lookups
if len(req.Hashes) > maxAccessListLookups {
req.Hashes = req.Hashes[:maxAccessListLookups]
}
var (
err error
bytes uint64
response = rlp.RawList[rlp.RawValue]{}
)
for _, hash := range req.Hashes {
if bal := chain.GetAccessListRLP(hash); len(bal) > 0 {
err = response.AppendRaw(bal)
bytes += uint64(len(bal))
} else {
// Either the block is unknown or the BAL doesn't exist
err = response.AppendRaw(rlp.EmptyString)
bytes += 1
}
if err != nil {
break
}
if bytes > req.Bytes {
break
}
}
return response
}

View file

@ -28,6 +28,7 @@ import (
// Constants to match up protocol versions and messages
const (
SNAP1 = 1
//SNAP2 = 2
)
// ProtocolName is the official short name of the `snap` protocol used during
@ -40,7 +41,7 @@ var ProtocolVersions = []uint{SNAP1}
// protocolLengths are the number of implemented message corresponding to
// different protocol versions.
var protocolLengths = map[uint]uint64{SNAP1: 8}
var protocolLengths = map[uint]uint64{ /*SNAP2: 10,*/ SNAP1: 8}
// maxMessageSize is the maximum cap on the size of a protocol message.
const maxMessageSize = 10 * 1024 * 1024
@ -54,6 +55,8 @@ const (
ByteCodesMsg = 0x05
GetTrieNodesMsg = 0x06
TrieNodesMsg = 0x07
GetAccessListsMsg = 0x08
AccessListsMsg = 0x09
)
var (
@ -215,6 +218,21 @@ type TrieNodesPacket struct {
Nodes [][]byte // Requested state trie nodes
}
// GetAccessListsPacket requests BALs for a set of block hashes.
type GetAccessListsPacket struct {
ID uint64 // Request ID to match up responses with
Hashes []common.Hash // Block hashes to retrieve BALs for
Bytes uint64 // Soft limit at which to stop returning data
}
// AccessListsPacket is the response to GetAccessListsPacket.
// Each entry corresponds to the requested hash at the same index.
// Empty entries indicate the BAL is unavailable.
type AccessListsPacket struct {
ID uint64 // ID of the request this is a response for
AccessLists rlp.RawList[rlp.RawValue] // Requested BALs
}
func (*GetAccountRangePacket) Name() string { return "GetAccountRange" }
func (*GetAccountRangePacket) Kind() byte { return GetAccountRangeMsg }
@ -238,3 +256,9 @@ func (*GetTrieNodesPacket) Kind() byte { return GetTrieNodesMsg }
func (*TrieNodesPacket) Name() string { return "TrieNodes" }
func (*TrieNodesPacket) Kind() byte { return TrieNodesMsg }
func (*GetAccessListsPacket) Name() string { return "GetAccessLists" }
func (*GetAccessListsPacket) Kind() byte { return GetAccessListsMsg }
func (*AccessListsPacket) Name() string { return "AccessLists" }
func (*AccessListsPacket) Kind() byte { return AccessListsMsg }

View file

@ -38,7 +38,11 @@ import (
// for releasing state.
var noopReleaser = tracers.StateReleaseFunc(func() {})
func (eth *Ethereum) hashState(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, readOnly bool, preferDisk bool) (statedb *state.StateDB, release tracers.StateReleaseFunc, err error) {
// reexecLimit is the maximum number of ancestor blocks to walk back when
// attempting to reconstruct missing historical state for hash-scheme nodes.
const reexecLimit = uint64(128)
func (eth *Ethereum) hashState(ctx context.Context, block *types.Block, base *state.StateDB, readOnly bool, preferDisk bool) (statedb *state.StateDB, release tracers.StateReleaseFunc, err error) {
var (
current *types.Block
database state.Database
@ -99,7 +103,7 @@ func (eth *Ethereum) hashState(ctx context.Context, block *types.Block, reexec u
}
}
// Database does not have the state for the given block, try to regenerate
for i := uint64(0); i < reexec; i++ {
for i := uint64(0); i < reexecLimit; i++ {
if err := ctx.Err(); err != nil {
return nil, nil, err
}
@ -120,7 +124,7 @@ func (eth *Ethereum) hashState(ctx context.Context, block *types.Block, reexec u
if err != nil {
switch err.(type) {
case *trie.MissingNodeError:
return nil, nil, fmt.Errorf("required historical state unavailable (reexec=%d)", reexec)
return nil, nil, fmt.Errorf("required historical state unavailable (reexec=%d)", reexecLimit)
default:
return nil, nil, err
}
@ -190,10 +194,9 @@ func (eth *Ethereum) pathState(block *types.Block) (*state.StateDB, func(), erro
}
// stateAtBlock retrieves the state database associated with a certain block.
// If no state is locally available for the given block, a number of blocks
// are attempted to be reexecuted to generate the desired state. The optional
// base layer statedb can be provided which is regarded as the statedb of the
// parent block.
// If no state is locally available for the given block, up to reexecLimit ancestor
// blocks are reexecuted to generate the desired state. The optional base layer
// statedb can be provided which is regarded as the statedb of the parent block.
//
// An additional release function will be returned if the requested state is
// available. Release is expected to be invoked when the returned state is no
@ -202,7 +205,6 @@ func (eth *Ethereum) pathState(block *types.Block) (*state.StateDB, func(), erro
//
// Parameters:
// - block: The block for which we want the state(state = block.Root)
// - reexec: The maximum number of blocks to reprocess trying to obtain the desired state
// - base: If the caller is tracing multiple blocks, the caller can provide the parent
// state continuously from the callsite.
// - readOnly: If true, then the live 'blockchain' state database is used. No mutation should
@ -211,9 +213,9 @@ func (eth *Ethereum) pathState(block *types.Block) (*state.StateDB, func(), erro
// - preferDisk: This arg can be used by the caller to signal that even though the 'base' is
// provided, it would be preferable to start from a fresh state, if we have it
// on disk.
func (eth *Ethereum) stateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, readOnly bool, preferDisk bool) (statedb *state.StateDB, release tracers.StateReleaseFunc, err error) {
func (eth *Ethereum) stateAtBlock(ctx context.Context, block *types.Block, base *state.StateDB, readOnly bool, preferDisk bool) (statedb *state.StateDB, release tracers.StateReleaseFunc, err error) {
if eth.blockchain.TrieDB().Scheme() == rawdb.HashScheme {
return eth.hashState(ctx, block, reexec, base, readOnly, preferDisk)
return eth.hashState(ctx, block, base, readOnly, preferDisk)
}
return eth.pathState(block)
}
@ -225,7 +227,7 @@ func (eth *Ethereum) stateAtBlock(ctx context.Context, block *types.Block, reexe
// function will return the state of block after the pre-block operations have
// been completed (e.g. updating system contracts), but before post-block
// operations are completed (e.g. processing withdrawals).
func (eth *Ethereum) stateAtTransaction(ctx context.Context, block *types.Block, txIndex int, reexec uint64) (*types.Transaction, vm.BlockContext, *state.StateDB, tracers.StateReleaseFunc, error) {
func (eth *Ethereum) stateAtTransaction(ctx context.Context, block *types.Block, txIndex int) (*types.Transaction, vm.BlockContext, *state.StateDB, tracers.StateReleaseFunc, error) {
// Short circuit if it's genesis block.
if block.NumberU64() == 0 {
return nil, vm.BlockContext{}, nil, nil, errors.New("no transaction in genesis")
@ -237,7 +239,7 @@ func (eth *Ethereum) stateAtTransaction(ctx context.Context, block *types.Block,
}
// Lookup the statedb of parent block from the live database,
// otherwise regenerate it on the flight.
statedb, release, err := eth.stateAtBlock(ctx, parent, reexec, nil, true, false)
statedb, release, err := eth.stateAtBlock(ctx, parent, nil, true, false)
if err != nil {
return nil, vm.BlockContext{}, nil, nil, err
}

View file

@ -51,11 +51,6 @@ const (
// by default before being forcefully aborted.
defaultTraceTimeout = 5 * time.Second
// defaultTraceReexec is the number of blocks the tracer is willing to go back
// and reexecute to produce missing historical state necessary to run a specific
// trace.
defaultTraceReexec = uint64(128)
// defaultTracechainMemLimit is the size of the triedb, at which traceChain
// switches over and tries to use a disk-backed database instead of building
// on top of memory.
@ -89,8 +84,8 @@ type Backend interface {
ChainConfig() *params.ChainConfig
Engine() consensus.Engine
ChainDb() ethdb.Database
StateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, readOnly bool, preferDisk bool) (*state.StateDB, StateReleaseFunc, error)
StateAtTransaction(ctx context.Context, block *types.Block, txIndex int, reexec uint64) (*types.Transaction, vm.BlockContext, *state.StateDB, StateReleaseFunc, error)
StateAtBlock(ctx context.Context, block *types.Block, base *state.StateDB, readOnly bool, preferDisk bool) (*state.StateDB, StateReleaseFunc, error)
StateAtTransaction(ctx context.Context, block *types.Block, txIndex int) (*types.Transaction, vm.BlockContext, *state.StateDB, StateReleaseFunc, error)
}
// API is the collection of tracing APIs exposed over the private debugging endpoint.
@ -156,7 +151,6 @@ type TraceConfig struct {
*logger.Config
Tracer *string
Timeout *string
Reexec *uint64
// Config specific to given tracer. Note struct logger
// config are historically embedded in main object.
TracerConfig json.RawMessage
@ -174,7 +168,6 @@ type TraceCallConfig struct {
// StdTraceConfig holds extra parameters to standard-json trace functions.
type StdTraceConfig struct {
logger.Config
Reexec *uint64
TxHash common.Hash
}
@ -245,10 +238,6 @@ func (api *API) TraceChain(ctx context.Context, start, end rpc.BlockNumber, conf
// transaction, dependent on the requested tracer.
// The tracing procedure should be aborted in case the closed signal is received.
func (api *API) traceChain(start, end *types.Block, config *TraceConfig, closed <-chan error) chan *blockTraceResult {
reexec := defaultTraceReexec
if config != nil && config.Reexec != nil {
reexec = *config.Reexec
}
blocks := int(end.NumberU64() - start.NumberU64())
threads := runtime.NumCPU()
if threads > blocks {
@ -374,7 +363,7 @@ func (api *API) traceChain(start, end *types.Block, config *TraceConfig, closed
s1, s2, s3 := statedb.Database().TrieDB().Size()
preferDisk = s1+s2+s3 > defaultTracechainMemLimit
}
statedb, release, err = api.backend.StateAtBlock(ctx, block, reexec, statedb, false, preferDisk)
statedb, release, err = api.backend.StateAtBlock(ctx, block, statedb, false, preferDisk)
if err != nil {
failed = err
break
@ -522,11 +511,7 @@ func (api *API) IntermediateRoots(ctx context.Context, hash common.Hash, config
if err != nil {
return nil, err
}
reexec := defaultTraceReexec
if config != nil && config.Reexec != nil {
reexec = *config.Reexec
}
statedb, release, err := api.backend.StateAtBlock(ctx, parent, reexec, nil, true, false)
statedb, release, err := api.backend.StateAtBlock(ctx, parent, nil, true, false)
if err != nil {
return nil, err
}
@ -591,11 +576,7 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac
if err != nil {
return nil, err
}
reexec := defaultTraceReexec
if config != nil && config.Reexec != nil {
reexec = *config.Reexec
}
statedb, release, err := api.backend.StateAtBlock(ctx, parent, reexec, nil, true, false)
statedb, release, err := api.backend.StateAtBlock(ctx, parent, nil, true, false)
if err != nil {
return nil, err
}
@ -743,11 +724,7 @@ func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block
if err != nil {
return nil, err
}
reexec := defaultTraceReexec
if config != nil && config.Reexec != nil {
reexec = *config.Reexec
}
statedb, release, err := api.backend.StateAtBlock(ctx, parent, reexec, nil, true, false)
statedb, release, err := api.backend.StateAtBlock(ctx, parent, nil, true, false)
if err != nil {
return nil, err
}
@ -877,15 +854,11 @@ func (api *API) TraceTransaction(ctx context.Context, hash common.Hash, config *
if blockNumber == 0 {
return nil, errors.New("genesis is not traceable")
}
reexec := defaultTraceReexec
if config != nil && config.Reexec != nil {
reexec = *config.Reexec
}
block, err := api.blockByNumberAndHash(ctx, rpc.BlockNumber(blockNumber), blockHash)
if err != nil {
return nil, err
}
tx, vmctx, statedb, release, err := api.backend.StateAtTransaction(ctx, block, int(index), reexec)
tx, vmctx, statedb, release, err := api.backend.StateAtTransaction(ctx, block, int(index))
if err != nil {
return nil, err
}
@ -939,15 +912,10 @@ func (api *API) TraceCall(ctx context.Context, args ethapi.TransactionArgs, bloc
return nil, err
}
// try to recompute the state
reexec := defaultTraceReexec
if config != nil && config.Reexec != nil {
reexec = *config.Reexec
}
if config != nil && config.TxIndex != nil {
_, _, statedb, release, err = api.backend.StateAtTransaction(ctx, block, int(*config.TxIndex), reexec)
_, _, statedb, release, err = api.backend.StateAtTransaction(ctx, block, int(*config.TxIndex))
} else {
statedb, release, err = api.backend.StateAtBlock(ctx, block, reexec, nil, true, false)
statedb, release, err = api.backend.StateAtBlock(ctx, block, nil, true, false)
}
if err != nil {
return nil, err

View file

@ -151,7 +151,7 @@ func (b *testBackend) teardown() {
b.chain.Stop()
}
func (b *testBackend) StateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, readOnly bool, preferDisk bool) (*state.StateDB, StateReleaseFunc, error) {
func (b *testBackend) StateAtBlock(ctx context.Context, block *types.Block, base *state.StateDB, readOnly bool, preferDisk bool) (*state.StateDB, StateReleaseFunc, error) {
statedb, err := b.chain.StateAt(block.Root())
if err != nil {
return nil, nil, errStateNotFound
@ -167,12 +167,12 @@ func (b *testBackend) StateAtBlock(ctx context.Context, block *types.Block, reex
return statedb, release, nil
}
func (b *testBackend) StateAtTransaction(ctx context.Context, block *types.Block, txIndex int, reexec uint64) (*types.Transaction, vm.BlockContext, *state.StateDB, StateReleaseFunc, error) {
func (b *testBackend) StateAtTransaction(ctx context.Context, block *types.Block, txIndex int) (*types.Transaction, vm.BlockContext, *state.StateDB, StateReleaseFunc, error) {
parent := b.chain.GetBlock(block.ParentHash(), block.NumberU64()-1)
if parent == nil {
return nil, vm.BlockContext{}, nil, nil, errBlockNotFound
}
statedb, release, err := b.StateAtBlock(ctx, parent, reexec, nil, true, false)
statedb, release, err := b.StateAtBlock(ctx, parent, nil, true, false)
if err != nil {
return nil, vm.BlockContext{}, nil, nil, errStateNotFound
}
@ -202,6 +202,18 @@ type stateTracer struct {
Storage map[common.Address]map[common.Hash]common.Hash
}
type tracedOpcodeLog struct {
Op string `json:"op"`
Refund *uint64 `json:"refund,omitempty"`
Storage map[string]string `json:"storage,omitempty"`
}
type tracedOpcodeResult struct {
Failed bool `json:"failed"`
ReturnValue string `json:"returnValue"`
StructLogs []tracedOpcodeLog `json:"structLogs"`
}
func newStateTracer(ctx *Context, cfg json.RawMessage, chainCfg *params.ChainConfig) (*Tracer, error) {
t := &stateTracer{
Balance: make(map[common.Address]*hexutil.Big),
@ -1058,6 +1070,176 @@ func TestTracingWithOverrides(t *testing.T) {
}
}
func TestTraceTransactionRefundAndStorageSnapshots(t *testing.T) {
t.Parallel()
accounts := newAccounts(1)
contract := common.HexToAddress("0x00000000000000000000000000000000deadbeef")
slot0 := common.BigToHash(big.NewInt(0))
txSigner := types.HomesteadSigner{}
genesis := &core.Genesis{
Config: params.TestChainConfig,
Alloc: types.GenesisAlloc{
accounts[0].addr: {Balance: big.NewInt(params.Ether)},
contract: {
Nonce: 1,
Code: []byte{
byte(vm.PUSH1), 0x00,
byte(vm.SLOAD),
byte(vm.POP),
byte(vm.PUSH1), 0x00,
byte(vm.PUSH1), 0x00,
byte(vm.SSTORE),
byte(vm.STOP),
},
Storage: map[common.Hash]common.Hash{
slot0: common.BigToHash(big.NewInt(1)),
},
},
},
}
var target common.Hash
backend := newTestBackend(t, 1, genesis, func(i int, b *core.BlockGen) {
tx, _ := types.SignTx(types.NewTx(&types.LegacyTx{
Nonce: 0,
To: &contract,
Value: big.NewInt(0),
Gas: 100000,
GasPrice: b.BaseFee(),
}), txSigner, accounts[0].key)
b.AddTx(tx)
target = tx.Hash()
})
defer backend.teardown()
api := NewAPI(backend)
result, err := api.TraceTransaction(context.Background(), target, nil)
if err != nil {
t.Fatalf("failed to trace refunding transaction: %v", err)
}
var traced tracedOpcodeResult
if err := json.Unmarshal(result.(json.RawMessage), &traced); err != nil {
t.Fatalf("failed to unmarshal trace result: %v", err)
}
if traced.Failed {
t.Fatal("expected refunding transaction to succeed")
}
if traced.ReturnValue != "0x" {
t.Fatalf("unexpected return value: have %s want 0x", traced.ReturnValue)
}
slotHex := slot0.Hex()
oneHex := common.BigToHash(big.NewInt(1)).Hex()
zeroHex := common.Hash{}.Hex()
var (
foundSloadSnapshot bool
foundSstoreSnapshot bool
foundRefund bool
)
for _, log := range traced.StructLogs {
switch log.Op {
case "SLOAD":
if got := log.Storage[slotHex]; got == oneHex {
foundSloadSnapshot = true
}
case "SSTORE":
if got := log.Storage[slotHex]; got == zeroHex {
foundSstoreSnapshot = true
}
}
if log.Refund != nil && *log.Refund > 0 {
foundRefund = true
}
}
if !foundSloadSnapshot {
t.Fatal("expected SLOAD snapshot to include the pre-existing non-zero storage value")
}
if !foundSstoreSnapshot {
t.Fatal("expected SSTORE snapshot to include the post-write zeroed storage value")
}
if !foundRefund {
t.Fatal("expected at least one structLog entry with a non-zero refund field")
}
}
func TestTraceTransactionFailureReturnValues(t *testing.T) {
t.Parallel()
tests := []struct {
name string
code []byte
wantReturnValue string
}{
{
name: "revert preserves return data",
code: []byte{
byte(vm.PUSH1), 0x2a,
byte(vm.PUSH1), 0x00,
byte(vm.MSTORE),
byte(vm.PUSH1), 0x20,
byte(vm.PUSH1), 0x00,
byte(vm.REVERT),
},
wantReturnValue: "0x000000000000000000000000000000000000000000000000000000000000002a",
},
{
name: "hard failure clears return data",
code: []byte{
byte(vm.INVALID),
},
wantReturnValue: "0x",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
accounts := newAccounts(1)
contract := common.HexToAddress("0x00000000000000000000000000000000deadbeef")
txSigner := types.HomesteadSigner{}
genesis := &core.Genesis{
Config: params.TestChainConfig,
Alloc: types.GenesisAlloc{
accounts[0].addr: {Balance: big.NewInt(params.Ether)},
contract: {
Nonce: 1,
Code: tc.code,
},
},
}
var target common.Hash
backend := newTestBackend(t, 1, genesis, func(i int, b *core.BlockGen) {
tx, _ := types.SignTx(types.NewTx(&types.LegacyTx{
Nonce: 0,
To: &contract,
Value: big.NewInt(0),
Gas: 100000,
GasPrice: b.BaseFee(),
}), txSigner, accounts[0].key)
b.AddTx(tx)
target = tx.Hash()
})
defer backend.teardown()
api := NewAPI(backend)
result, err := api.TraceTransaction(context.Background(), target, nil)
if err != nil {
t.Fatalf("failed to trace transaction: %v", err)
}
var traced tracedOpcodeResult
if err := json.Unmarshal(result.(json.RawMessage), &traced); err != nil {
t.Fatalf("failed to unmarshal trace result: %v", err)
}
if !traced.Failed {
t.Fatal("expected traced transaction to fail")
}
if traced.ReturnValue != tc.wantReturnValue {
t.Fatalf("unexpected returnValue: have %s want %s", traced.ReturnValue, tc.wantReturnValue)
}
if len(traced.StructLogs) == 0 {
t.Fatal("expected failing trace to still include structLogs")
}
})
}
}
type Account struct {
key *ecdsa.PrivateKey
addr common.Address

View file

@ -66,9 +66,9 @@ func runTrace(tracer *tracers.Tracer, vmctx *vmContext, chaincfg *params.ChainCo
tracer.OnTxStart(evm.GetVMContext(), types.NewTx(&types.LegacyTx{Gas: gasLimit, GasPrice: vmctx.txCtx.GasPrice.ToBig()}), contract.Caller())
tracer.OnEnter(0, byte(vm.CALL), contract.Caller(), contract.Address(), []byte{}, startGas, value.ToBig())
ret, err := evm.Run(contract, []byte{}, false)
tracer.OnExit(0, ret, startGas-contract.Gas, err, true)
tracer.OnExit(0, ret, startGas-contract.Gas.RegularGas, err, true)
// Rest gas assumes no refund
tracer.OnTxEnd(&types.Receipt{GasUsed: gasLimit - contract.Gas}, nil)
tracer.OnTxEnd(&types.Receipt{GasUsed: gasLimit - contract.Gas.RegularGas}, nil)
if err != nil {
return nil, err
}

View file

@ -148,7 +148,7 @@ type structLogLegacy struct {
Gas uint64 `json:"gas"`
GasCost uint64 `json:"gasCost"`
Depth int `json:"depth"`
Error string `json:"error,omitempty"`
Error string `json:"error,omitempty,omitzero"`
Stack *[]string `json:"stack,omitempty"`
ReturnData string `json:"returnData,omitempty"`
Memory *[]string `json:"memory,omitempty"`
@ -156,6 +156,15 @@ type structLogLegacy struct {
RefundCounter uint64 `json:"refund,omitempty"`
}
func formatMemoryWord(chunk []byte) string {
if len(chunk) == 32 {
return hexutil.Encode(chunk)
}
var word [32]byte
copy(word[:], chunk)
return hexutil.Encode(word[:])
}
// toLegacyJSON converts the structLog to legacy json-encoded legacy form.
func (s *StructLog) toLegacyJSON() json.RawMessage {
msg := structLogLegacy{
@ -175,7 +184,7 @@ func (s *StructLog) toLegacyJSON() json.RawMessage {
msg.Stack = &stack
}
if len(s.ReturnData) > 0 {
msg.ReturnData = hexutil.Bytes(s.ReturnData).String()
msg.ReturnData = hexutil.Encode(s.ReturnData)
}
if len(s.Memory) > 0 {
memory := make([]string, 0, (len(s.Memory)+31)/32)
@ -184,14 +193,14 @@ func (s *StructLog) toLegacyJSON() json.RawMessage {
if end > len(s.Memory) {
end = len(s.Memory)
}
memory = append(memory, fmt.Sprintf("%x", s.Memory[i:end]))
memory = append(memory, formatMemoryWord(s.Memory[i:end]))
}
msg.Memory = &memory
}
if len(s.Storage) > 0 {
storage := make(map[string]string)
for i, storageValue := range s.Storage {
storage[fmt.Sprintf("%x", i)] = fmt.Sprintf("%x", storageValue)
storage[i.Hex()] = storageValue.Hex()
}
msg.Storage = &storage
}

View file

@ -96,3 +96,46 @@ func TestStructLogMarshalingOmitEmpty(t *testing.T) {
})
}
}
func TestStructLogLegacyJSONSpecFormatting(t *testing.T) {
tests := []struct {
name string
log *StructLog
want string
}{
{
name: "omits empty error and pads memory/storage",
log: &StructLog{
Pc: 7,
Op: vm.SSTORE,
Gas: 100,
GasCost: 20,
Memory: []byte{0xaa, 0xbb},
Storage: map[common.Hash]common.Hash{common.BigToHash(big.NewInt(1)): common.BigToHash(big.NewInt(2))},
Depth: 1,
ReturnData: []byte{0x12, 0x34},
},
want: `{"pc":7,"op":"SSTORE","gas":100,"gasCost":20,"depth":1,"returnData":"0x1234","memory":["0xaabb000000000000000000000000000000000000000000000000000000000000"],"storage":{"0x0000000000000000000000000000000000000000000000000000000000000001":"0x0000000000000000000000000000000000000000000000000000000000000002"}}`,
},
{
name: "includes error only when present",
log: &StructLog{
Pc: 1,
Op: vm.STOP,
Gas: 2,
GasCost: 3,
Depth: 1,
Err: errors.New("boom"),
},
want: `{"pc":1,"op":"STOP","gas":2,"gasCost":3,"depth":1,"error":"boom"}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
have := string(tt.log.toLegacyJSON())
if have != tt.want {
t.Fatalf("mismatched results\n\thave: %v\n\twant: %v", have, tt.want)
}
})
}
}

View file

@ -498,7 +498,11 @@ func (ec *Client) SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuer
func toFilterArg(q ethereum.FilterQuery) (interface{}, error) {
arg := map[string]interface{}{}
if q.Addresses != nil {
// Only include "address" when there are actual address filters.
// An empty slice is treated the same as nil (no filter), and omitting
// the field avoids sending "address":[] to nodes that reject empty arrays
// (e.g. Hedera, some non-Geth implementations).
if len(q.Addresses) > 0 {
arg["address"] = q.Addresses
}
if q.Topics != nil {
@ -838,6 +842,7 @@ type rpcProgress struct {
TxIndexFinishedBlocks hexutil.Uint64
TxIndexRemainingBlocks hexutil.Uint64
StateIndexRemaining hexutil.Uint64
TrienodeIndexRemaining hexutil.Uint64
}
func (p *rpcProgress) toSyncProgress() *ethereum.SyncProgress {
@ -865,6 +870,7 @@ func (p *rpcProgress) toSyncProgress() *ethereum.SyncProgress {
TxIndexFinishedBlocks: uint64(p.TxIndexFinishedBlocks),
TxIndexRemainingBlocks: uint64(p.TxIndexRemainingBlocks),
StateIndexRemaining: uint64(p.StateIndexRemaining),
TrienodeIndexRemaining: uint64(p.TrienodeIndexRemaining),
}
}

View file

@ -53,6 +53,22 @@ func TestToFilterArg(t *testing.T) {
},
nil,
},
{
// empty Addresses slice must be treated same as nil:
// the "address" field must be omitted so that non-Geth nodes
// (e.g. Hedera) do not reject the request with an error.
"with empty addresses slice",
ethereum.FilterQuery{
Addresses: []common.Address{},
FromBlock: big.NewInt(1),
ToBlock: big.NewInt(2),
},
map[string]interface{}{
"fromBlock": "0x1",
"toBlock": "0x2",
},
nil,
},
{
"without BlockHash",
ethereum.FilterQuery{

View file

@ -1531,6 +1531,9 @@ func (s *SyncState) TxIndexRemainingBlocks() hexutil.Uint64 {
func (s *SyncState) StateIndexRemaining() hexutil.Uint64 {
return hexutil.Uint64(s.progress.StateIndexRemaining)
}
func (s *SyncState) TrienodeIndexRemaining() hexutil.Uint64 {
return hexutil.Uint64(s.progress.TrienodeIndexRemaining)
}
// Syncing returns false in case the node is currently not syncing with the network. It can be up-to-date or has not
// yet received the latest block headers from its peers. In case it is synchronizing:

View file

@ -139,8 +139,9 @@ type SyncProgress struct {
TxIndexFinishedBlocks uint64 // Number of blocks whose transactions are already indexed
TxIndexRemainingBlocks uint64 // Number of blocks whose transactions are not indexed yet
// "historical state indexing" fields
StateIndexRemaining uint64 // Number of states remain unindexed
// "historical data indexing" fields
StateIndexRemaining uint64 // Number of states remain unindexed
TrienodeIndexRemaining uint64 // Number of trienodes remain unindexed
}
// Done returns the indicator if the initial sync is finished or not.
@ -148,7 +149,7 @@ func (prog SyncProgress) Done() bool {
if prog.CurrentBlock < prog.HighestBlock {
return false
}
return prog.TxIndexRemainingBlocks == 0 && prog.StateIndexRemaining == 0
return prog.TxIndexRemainingBlocks == 0 && prog.StateIndexRemaining == 0 && prog.TrienodeIndexRemaining == 0
}
// ChainSyncReader wraps access to the node's current sync status. If there's no

View file

@ -180,6 +180,7 @@ func (api *EthereumAPI) Syncing(ctx context.Context) (interface{}, error) {
"txIndexFinishedBlocks": hexutil.Uint64(progress.TxIndexFinishedBlocks),
"txIndexRemainingBlocks": hexutil.Uint64(progress.TxIndexRemainingBlocks),
"stateIndexRemaining": hexutil.Uint64(progress.StateIndexRemaining),
"trienodeIndexRemaining": hexutil.Uint64(progress.TrienodeIndexRemaining),
}, nil
}
@ -897,6 +898,7 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr
if err := blockOverrides.Apply(&blockCtx); err != nil {
return 0, err
}
header = blockOverrides.MakeHeader(header)
}
rules := b.ChainConfig().Rules(blockCtx.BlockNumber, blockCtx.Random != nil, blockCtx.Time)
precompiles := vm.ActivePrecompiledContracts(rules)
@ -904,13 +906,17 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr
return 0, err
}
// Construct the gas estimator option from the user input
var blobBaseFee *big.Int
if blockOverrides != nil && blockOverrides.BlobBaseFee != nil {
blobBaseFee = blockOverrides.BlobBaseFee.ToInt()
}
opts := &gasestimator.Options{
Config: b.ChainConfig(),
Chain: NewChainContext(ctx, b),
Header: header,
BlockOverrides: blockOverrides,
State: state,
ErrorRatio: estimateGasErrorRatio,
Config: b.ChainConfig(),
Chain: NewChainContext(ctx, b),
Header: header,
State: state,
BlobBaseFee: blobBaseFee,
ErrorRatio: estimateGasErrorRatio,
}
// Set any required transaction default, but make sure the gas cap itself is not messed with
// if it was not specified in the original argument list.

View file

@ -780,6 +780,17 @@ func TestEstimateGas(t *testing.T) {
expectErr: core.ErrInsufficientFunds,
want: 21000,
},
// block override gas limit should bound estimation search space.
{
blockNumber: rpc.LatestBlockNumber,
call: TransactionArgs{
From: &accounts[0].addr,
Input: hex2Bytes("6080604052348015600f57600080fd5b50483a1015601c57600080fd5b60003a111560315760004811603057600080fd5b5b603f80603e6000396000f3fe6080604052600080fdfea264697066735822122060729c2cee02b10748fae5200f1c9da4661963354973d9154c13a8e9ce9dee1564736f6c63430008130033"),
Gas: func() *hexutil.Uint64 { v := hexutil.Uint64(0); return &v }(),
},
blockOverrides: override.BlockOverrides{GasLimit: func() *hexutil.Uint64 { v := hexutil.Uint64(50000); return &v }()},
expectErr: errors.New("gas required exceeds allowance (50000)"),
},
// empty create
{
blockNumber: rpc.LatestBlockNumber,
@ -861,6 +872,19 @@ func TestEstimateGas(t *testing.T) {
},
want: 21000,
},
// blob base fee block override should be applied during estimation.
{
blockNumber: rpc.LatestBlockNumber,
call: TransactionArgs{
From: &accounts[0].addr,
To: &accounts[1].addr,
Value: (*hexutil.Big)(big.NewInt(1)),
BlobHashes: []common.Hash{{0x01, 0x22}},
BlobFeeCap: (*hexutil.Big)(big.NewInt(1)),
},
blockOverrides: override.BlockOverrides{BlobBaseFee: (*hexutil.Big)(big.NewInt(2))},
expectErr: core.ErrBlobFeeCapTooLow,
},
// // SPDX-License-Identifier: GPL-3.0
//pragma solidity >=0.8.2 <0.9.0;
//
@ -1014,7 +1038,7 @@ func TestCall(t *testing.T) {
Balance: big.NewInt(params.Ether),
Nonce: 1,
Storage: map[common.Hash]common.Hash{
common.Hash{}: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000001"),
{}: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000001"),
},
},
},
@ -3795,7 +3819,7 @@ func TestCreateAccessListWithStateOverrides(t *testing.T) {
Balance: (*hexutil.Big)(big.NewInt(1000000000000000000)),
Nonce: &nonce,
State: map[common.Hash]common.Hash{
common.Hash{}: common.HexToHash("0x000000000000000000000000000000000000000000000000000000000000002a"),
{}: common.HexToHash("0x000000000000000000000000000000000000000000000000000000000000002a"),
},
},
}

View file

@ -720,6 +720,43 @@ var Forks = map[string]*params.ChainConfig{
BPO4: params.DefaultBPO4BlobConfig,
},
},
"Amsterdam": {
ChainID: big.NewInt(1),
HomesteadBlock: big.NewInt(0),
EIP150Block: big.NewInt(0),
EIP155Block: big.NewInt(0),
EIP158Block: big.NewInt(0),
ByzantiumBlock: big.NewInt(0),
ConstantinopleBlock: big.NewInt(0),
PetersburgBlock: big.NewInt(0),
IstanbulBlock: big.NewInt(0),
MuirGlacierBlock: big.NewInt(0),
BerlinBlock: big.NewInt(0),
LondonBlock: big.NewInt(0),
ArrowGlacierBlock: big.NewInt(0),
MergeNetsplitBlock: big.NewInt(0),
TerminalTotalDifficulty: big.NewInt(0),
ShanghaiTime: u64(0),
CancunTime: u64(0),
PragueTime: u64(0),
OsakaTime: u64(0),
BPO1Time: u64(0),
BPO2Time: u64(0),
BPO3Time: u64(0),
BPO4Time: u64(0),
AmsterdamTime: u64(0),
DepositContractAddress: params.MainnetChainConfig.DepositContractAddress,
BlobScheduleConfig: &params.BlobScheduleConfig{
Cancun: params.DefaultCancunBlobConfig,
Prague: params.DefaultPragueBlobConfig,
Osaka: params.DefaultOsakaBlobConfig,
BPO1: bpo1BlobConfig,
BPO2: bpo2BlobConfig,
BPO3: params.DefaultBPO3BlobConfig,
BPO4: params.DefaultBPO4BlobConfig,
Amsterdam: params.DefaultBPO4BlobConfig, // TODO update when defined
},
},
"Verkle": {
ChainID: big.NewInt(1),
HomesteadBlock: big.NewInt(0),

View file

@ -37,7 +37,10 @@ type StemNode struct {
// Get retrieves the value for the given key.
func (bt *StemNode) Get(key []byte, _ NodeResolverFn) ([]byte, error) {
panic("this should not be called directly")
if !bytes.Equal(bt.Stem, key[:StemSize]) {
return nil, nil
}
return bt.Values[key[StemSize]], nil
}
// Insert inserts a new key-value pair into the node.

View file

@ -23,6 +23,50 @@ import (
"github.com/ethereum/go-ethereum/common"
)
// TestStemNodeGet tests the Get method for matching stem, non-matching stem,
// and nil-value suffix scenarios.
func TestStemNodeGet(t *testing.T) {
stem := make([]byte, StemSize)
stem[0] = 0xAB
var values [StemNodeWidth][]byte
values[5] = common.HexToHash("0xdeadbeef").Bytes()
node := &StemNode{Stem: stem, Values: values[:], depth: 0}
// Matching stem, populated suffix → returns value.
key := make([]byte, HashSize)
copy(key[:StemSize], stem)
key[StemSize] = 5
got, err := node.Get(key, nil)
if err != nil {
t.Fatalf("Get error: %v", err)
}
if !bytes.Equal(got, values[5]) {
t.Fatalf("Get = %x, want %x", got, values[5])
}
// Matching stem, empty suffix → returns nil (slot not set).
key[StemSize] = 99
got, err = node.Get(key, nil)
if err != nil {
t.Fatalf("Get error: %v", err)
}
if got != nil {
t.Fatalf("Get(empty suffix) = %x, want nil", got)
}
// Non-matching stem → returns nil, nil.
otherKey := make([]byte, HashSize)
otherKey[0] = 0xFF
got, err = node.Get(otherKey, nil)
if err != nil {
t.Fatalf("Get error: %v", err)
}
if got != nil {
t.Fatalf("Get(wrong stem) = %x, want nil", got)
}
}
// TestStemNodeInsertSameStem tests inserting values with the same stem
func TestStemNodeInsertSameStem(t *testing.T) {
stem := make([]byte, 31)

View file

@ -191,7 +191,7 @@ func (t *BinaryTrie) GetAccount(addr common.Address) (*types.StateAccount, error
case *InternalNode:
values, err = r.GetValuesAtStem(key[:StemSize], t.nodeResolver)
case *StemNode:
values = r.Values
values, err = r.GetValuesAtStem(key[:StemSize], t.nodeResolver)
case Empty:
return nil, nil
default:
@ -216,10 +216,12 @@ func (t *BinaryTrie) GetAccount(addr common.Address) (*types.StateAccount, error
return nil, nil
}
// If the account has been deleted, then values[10] will be 0 and not nil. If it has
// been recreated after that, then its code keccak will NOT be 0. So return `nil` if
// the nonce, and values[10], and code keccak is 0.
if bytes.Equal(values[BasicDataLeafKey], zero[:]) && len(values) > 10 && len(values[10]) > 0 && bytes.Equal(values[CodeHashLeafKey], zero[:]) {
// If the account has been deleted, BasicData and CodeHash will both be
// 32-byte zero blobs (not nil). If the account is recreated afterwards,
// UpdateAccount overwrites BasicData and CodeHash with non-zero values,
// so this branch won't activate..
if bytes.Equal(values[BasicDataLeafKey], zero[:]) &&
bytes.Equal(values[CodeHashLeafKey], zero[:]) {
return nil, nil
}
@ -294,8 +296,22 @@ func (t *BinaryTrie) UpdateStorage(address common.Address, key, value []byte) er
return nil
}
// DeleteAccount is a no-op as it is disabled in stateless.
// DeleteAccount erases an account by overwriting the account
// descriptors with 0s.
func (t *BinaryTrie) DeleteAccount(addr common.Address) error {
var (
values = make([][]byte, StemNodeWidth)
stem = GetBinaryTreeKey(addr, zero[:])
)
// Clear BasicData (nonce, balance, code size) and CodeHash.
values[BasicDataLeafKey] = zero[:]
values[CodeHashLeafKey] = zero[:]
root, err := t.root.InsertValuesAtStem(stem, values, t.nodeResolver, 0)
if err != nil {
return fmt.Errorf("DeleteAccount (%x) error: %v", addr, err)
}
t.root = root
return nil
}

View file

@ -267,6 +267,334 @@ func TestStorageRoundTrip(t *testing.T) {
}
}
// newEmptyTestTrie creates a fresh BinaryTrie with an empty root and a
// default prevalue tracer. Use this for tests that populate the trie
// incrementally via Update*; for tests that want a pre-populated trie with
// a fixed entry set, use makeTrie (in iterator_test.go) instead.
func newEmptyTestTrie(t *testing.T) *BinaryTrie {
t.Helper()
return &BinaryTrie{
root: NewBinaryNode(),
tracer: trie.NewPrevalueTracer(),
}
}
// makeAccount constructs a StateAccount with the given fields. The Root is
// zeroed out because the bintrie has no per-account storage root.
func makeAccount(nonce uint64, balance uint64, codeHash common.Hash) *types.StateAccount {
return &types.StateAccount{
Nonce: nonce,
Balance: uint256.NewInt(balance),
CodeHash: codeHash.Bytes(),
}
}
// TestDeleteAccountRoundTrip verifies the basic delete path: create an
// account, read it back, delete it, confirm subsequent reads return nil.
// Regression test for the no-op DeleteAccount bug where the deletion was
// silently ignored and the old values remained in the trie.
func TestDeleteAccountRoundTrip(t *testing.T) {
tr := newEmptyTestTrie(t)
addr := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678")
codeHash := common.HexToHash("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470")
// Create: write account, verify round-trip.
acc := makeAccount(42, 1000, codeHash)
if err := tr.UpdateAccount(addr, acc, 0); err != nil {
t.Fatalf("UpdateAccount: %v", err)
}
got, err := tr.GetAccount(addr)
if err != nil {
t.Fatalf("GetAccount: %v", err)
}
if got == nil {
t.Fatal("GetAccount returned nil after UpdateAccount")
}
if got.Nonce != 42 {
t.Fatalf("Nonce: got %d, want 42", got.Nonce)
}
if got.Balance.Uint64() != 1000 {
t.Fatalf("Balance: got %s, want 1000", got.Balance)
}
if !bytes.Equal(got.CodeHash, codeHash[:]) {
t.Fatalf("CodeHash: got %x, want %x", got.CodeHash, codeHash)
}
// Delete: verify GetAccount returns nil afterwards.
if err := tr.DeleteAccount(addr); err != nil {
t.Fatalf("DeleteAccount: %v", err)
}
got, err = tr.GetAccount(addr)
if err != nil {
t.Fatalf("GetAccount after delete: %v", err)
}
if got != nil {
t.Fatalf("GetAccount after delete: got %+v, want nil", got)
}
}
// TestDeleteAccountOnMissingAccount verifies that deleting an account that
// was never created does not error and subsequent reads still return nil.
func TestDeleteAccountOnMissingAccount(t *testing.T) {
tr := newEmptyTestTrie(t)
addr := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678")
// Delete without any prior create. Should not panic or error on an
// empty root, and GetAccount should still return nil.
if err := tr.DeleteAccount(addr); err != nil {
t.Fatalf("DeleteAccount on empty trie: %v", err)
}
got, err := tr.GetAccount(addr)
if err != nil {
t.Fatalf("GetAccount after delete on empty trie: %v", err)
}
if got != nil {
t.Fatalf("GetAccount on deleted missing account: got %+v, want nil", got)
}
}
// TestDeleteAccountPreservesOtherAccounts verifies that deleting one account
// does not affect accounts at different stems.
func TestDeleteAccountPreservesOtherAccounts(t *testing.T) {
tr := newEmptyTestTrie(t)
addrA := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678")
addrB := common.HexToAddress("0xabcdef1234567890abcdef1234567890abcdef12")
codeHashA := common.HexToHash("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470")
codeHashB := common.HexToHash("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff0102030405060708090a0b0c0d0e0f10")
// Create two distinct accounts.
if err := tr.UpdateAccount(addrA, makeAccount(1, 100, codeHashA), 0); err != nil {
t.Fatalf("UpdateAccount(A): %v", err)
}
if err := tr.UpdateAccount(addrB, makeAccount(2, 200, codeHashB), 0); err != nil {
t.Fatalf("UpdateAccount(B): %v", err)
}
// Delete A.
if err := tr.DeleteAccount(addrA); err != nil {
t.Fatalf("DeleteAccount(A): %v", err)
}
// A should be gone.
if got, err := tr.GetAccount(addrA); err != nil {
t.Fatalf("GetAccount(A): %v", err)
} else if got != nil {
t.Fatalf("GetAccount(A) after delete: got %+v, want nil", got)
}
// B should still be readable with its original values.
got, err := tr.GetAccount(addrB)
if err != nil {
t.Fatalf("GetAccount(B): %v", err)
}
if got == nil {
t.Fatal("GetAccount(B) returned nil after unrelated delete")
}
if got.Nonce != 2 {
t.Fatalf("Account B Nonce: got %d, want 2", got.Nonce)
}
if got.Balance.Uint64() != 200 {
t.Fatalf("Account B Balance: got %s, want 200", got.Balance)
}
if !bytes.Equal(got.CodeHash, codeHashB[:]) {
t.Fatalf("Account B CodeHash: got %x, want %x", got.CodeHash, codeHashB)
}
}
// TestDeleteAccountThenRecreate verifies that an account can be deleted and
// then recreated with different values; the second read must return the new
// values, not the stale ones from before deletion.
func TestDeleteAccountThenRecreate(t *testing.T) {
tr := newEmptyTestTrie(t)
addr := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678")
codeHash1 := common.HexToHash("1111111111111111111111111111111111111111111111111111111111111111")
codeHash2 := common.HexToHash("2222222222222222222222222222222222222222222222222222222222222222")
// Create.
if err := tr.UpdateAccount(addr, makeAccount(1, 100, codeHash1), 0); err != nil {
t.Fatalf("UpdateAccount #1: %v", err)
}
// Delete.
if err := tr.DeleteAccount(addr); err != nil {
t.Fatalf("DeleteAccount: %v", err)
}
// Recreate with new values.
if err := tr.UpdateAccount(addr, makeAccount(7, 9999, codeHash2), 0); err != nil {
t.Fatalf("UpdateAccount #2: %v", err)
}
// Read: must observe the new values, not the originals.
got, err := tr.GetAccount(addr)
if err != nil {
t.Fatalf("GetAccount: %v", err)
}
if got == nil {
t.Fatal("GetAccount returned nil after recreate")
}
if got.Nonce != 7 {
t.Fatalf("Nonce: got %d, want 7", got.Nonce)
}
if got.Balance.Uint64() != 9999 {
t.Fatalf("Balance: got %s, want 9999", got.Balance)
}
if !bytes.Equal(got.CodeHash, codeHash2[:]) {
t.Fatalf("CodeHash: got %x, want %x", got.CodeHash, codeHash2)
}
}
// TestDeleteAccountDoesNotAffectMainStorage verifies that DeleteAccount only
// clears the account's BasicData and CodeHash, leaving main storage slots
// untouched. Main storage slots live at different stems entirely (their
// keys route through the non-header branch in GetBinaryTreeKeyStorageSlot),
// so this test exercises the inter-stem isolation. Header-range storage
// slots share the same stem and are covered separately by
// TestDeleteAccountPreservesHeaderStorage.
//
// Wiping storage on self-destruct is a separate concern handled at the
// StateDB level.
func TestDeleteAccountDoesNotAffectMainStorage(t *testing.T) {
tr := newEmptyTestTrie(t)
addr := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678")
codeHash := common.HexToHash("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470")
// Create account.
if err := tr.UpdateAccount(addr, makeAccount(1, 100, codeHash), 0); err != nil {
t.Fatalf("UpdateAccount: %v", err)
}
// Write a main storage slot — i.e. key[31] >= 64 or key[:31] != 0 — so
// it lives at a different stem from the account header.
slot := common.HexToHash("0000000000000000000000000000000000000000000000000000000000000080")
value := common.TrimLeftZeroes(common.HexToHash("00000000000000000000000000000000000000000000000000000000deadbeef").Bytes())
if err := tr.UpdateStorage(addr, slot[:], value); err != nil {
t.Fatalf("UpdateStorage: %v", err)
}
// Delete the account.
if err := tr.DeleteAccount(addr); err != nil {
t.Fatalf("DeleteAccount: %v", err)
}
// Account should be absent.
got, err := tr.GetAccount(addr)
if err != nil {
t.Fatalf("GetAccount after delete: %v", err)
}
if got != nil {
t.Fatalf("GetAccount after delete: got %+v, want nil", got)
}
// Main storage slot should still be readable — DeleteAccount must not
// have touched it.
stored, err := tr.GetStorage(addr, slot[:])
if err != nil {
t.Fatalf("GetStorage after DeleteAccount: %v", err)
}
if len(stored) == 0 {
t.Fatal("main storage slot was wiped by DeleteAccount, expected it to survive")
}
var expected [HashSize]byte
copy(expected[HashSize-len(value):], value)
if !bytes.Equal(stored, expected[:]) {
t.Fatalf("main storage slot: got %x, want %x", stored, expected)
}
}
// TestDeleteAccountPreservesHeaderStorage verifies that DeleteAccount does
// not clobber header-range storage slots (key[31] < 64), which live at the
// SAME stem as BasicData/CodeHash but at offsets 64-127. The safety here
// relies on StemNode.InsertValuesAtStem treating nil entries in the values
// slice as "do not overwrite"; this test pins that invariant so a future
// change cannot silently corrupt slots 0-63 of any contract.
func TestDeleteAccountPreservesHeaderStorage(t *testing.T) {
tr := newEmptyTestTrie(t)
addr := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678")
codeHash := common.HexToHash("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470")
// Create account.
if err := tr.UpdateAccount(addr, makeAccount(1, 100, codeHash), 0); err != nil {
t.Fatalf("UpdateAccount: %v", err)
}
// Create a second, unrelated account so the root promotes from StemNode
// to InternalNode. BinaryTrie.GetStorage walks via root.Get, which is
// only implemented on InternalNode/Empty — calling it with a StemNode
// root panics. The existing main-storage test gets away with this because
// the main-storage slot lands on a separate stem and forces the same
// promotion implicitly; here we want a same-stem header slot, so the
// promotion has to come from a second account.
other := common.HexToAddress("0xabcdef1234567890abcdef1234567890abcdef12")
if err := tr.UpdateAccount(other, makeAccount(0, 0, common.Hash{}), 0); err != nil {
t.Fatalf("UpdateAccount(other): %v", err)
}
// Write a header-range storage slot — key[:31] == 0 and key[31] < 64
// — which routes through the header branch in GetBinaryTreeKeyStorageSlot
// and lands on the same stem as BasicData/CodeHash.
var slot [HashSize]byte
slot[31] = 5
value := []byte{0xde, 0xad, 0xbe, 0xef}
if err := tr.UpdateStorage(addr, slot[:], value); err != nil {
t.Fatalf("UpdateStorage: %v", err)
}
// Delete the account.
if err := tr.DeleteAccount(addr); err != nil {
t.Fatalf("DeleteAccount: %v", err)
}
// Account metadata should be gone.
got, err := tr.GetAccount(addr)
if err != nil {
t.Fatalf("GetAccount after delete: %v", err)
}
if got != nil {
t.Fatalf("GetAccount after delete: got %+v, want nil", got)
}
// Header storage slot must survive — DeleteAccount only writes offsets
// BasicDataLeafKey, CodeHashLeafKey, and accountDeletedMarkerKey, leaving
// the header-storage offsets (64-127) untouched.
stored, err := tr.GetStorage(addr, slot[:])
if err != nil {
t.Fatalf("GetStorage after DeleteAccount: %v", err)
}
if len(stored) == 0 {
t.Fatal("header storage slot was wiped by DeleteAccount, expected it to survive")
}
var expected [HashSize]byte
copy(expected[HashSize-len(value):], value)
if !bytes.Equal(stored, expected[:]) {
t.Fatalf("header storage slot: got %x, want %x", stored, expected)
}
}
func TestDeleteAccountHashIsDeterministic(t *testing.T) {
addr := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678")
codeHash := common.HexToHash("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470")
acc := makeAccount(42, 1000, codeHash)
run := func() common.Hash {
tr := newEmptyTestTrie(t)
if err := tr.UpdateAccount(addr, acc, 0); err != nil {
t.Fatalf("UpdateAccount: %v", err)
}
if err := tr.DeleteAccount(addr); err != nil {
t.Fatalf("DeleteAccount: %v", err)
}
return tr.Hash()
}
first := run()
second := run()
if first != second {
t.Fatalf("non-deterministic root after Update+Delete: first=%x second=%x", first, second)
}
empty := newEmptyTestTrie(t).Hash()
if first == empty {
t.Fatalf("post-delete root unexpectedly equals empty-trie root %x", empty)
}
}
func TestBinaryTrieWitness(t *testing.T) {
tracer := trie.NewPrevalueTracer()
@ -292,3 +620,162 @@ func TestBinaryTrieWitness(t *testing.T) {
t.Fatal("unexpected witness value for path2")
}
}
// testAccount is a helper that creates a BinaryTrie with a tracer and
// inserts a single account, returning the trie.
func testAccount(t *testing.T, addr common.Address, nonce uint64, balance uint64) *BinaryTrie {
t.Helper()
tr := &BinaryTrie{
root: NewBinaryNode(),
tracer: trie.NewPrevalueTracer(),
}
acc := &types.StateAccount{
Nonce: nonce,
Balance: uint256.NewInt(balance),
CodeHash: types.EmptyCodeHash[:],
}
if err := tr.UpdateAccount(addr, acc, 0); err != nil {
t.Fatalf("UpdateAccount error: %v", err)
}
return tr
}
// TestGetAccountNonMembershipStemRoot verifies that querying a non-existent
// address returns nil when the trie root is a StemNode (single-account trie).
// This is a regression test: previously the StemNode branch in GetAccount
// returned the root's values without verifying the stem.
func TestGetAccountNonMembershipStemRoot(t *testing.T) {
addr := common.HexToAddress("0x1111111111111111111111111111111111111111")
tr := testAccount(t, addr, 42, 100)
// Verify root is a StemNode (single stem inserted).
if _, ok := tr.root.(*StemNode); !ok {
t.Fatalf("expected StemNode root, got %T", tr.root)
}
// Query a completely different address — must return nil.
other := common.HexToAddress("0x2222222222222222222222222222222222222222")
got, err := tr.GetAccount(other)
if err != nil {
t.Fatalf("GetAccount error: %v", err)
}
if got != nil {
t.Fatalf("expected nil for non-existent account, got nonce=%d balance=%s", got.Nonce, got.Balance)
}
// Original account must still be retrievable.
got, err = tr.GetAccount(addr)
if err != nil {
t.Fatalf("GetAccount(original) error: %v", err)
}
if got == nil {
t.Fatal("expected original account, got nil")
}
if got.Nonce != 42 {
t.Fatalf("expected nonce=42, got %d", got.Nonce)
}
}
// TestGetAccountNonMembershipInternalRoot verifies that querying a non-existent
// address returns nil when the trie root is an InternalNode (multi-account trie).
func TestGetAccountNonMembershipInternalRoot(t *testing.T) {
tr := &BinaryTrie{
root: NewBinaryNode(),
tracer: trie.NewPrevalueTracer(),
}
// Insert two accounts whose binary tree keys have different first bits
// so the root splits into an InternalNode.
addr1 := common.HexToAddress("0x1111111111111111111111111111111111111111")
addr2 := common.HexToAddress("0x9999999999999999999999999999999999999999")
for _, addr := range []common.Address{addr1, addr2} {
acc := &types.StateAccount{
Nonce: 1,
Balance: uint256.NewInt(1),
CodeHash: types.EmptyCodeHash[:],
}
if err := tr.UpdateAccount(addr, acc, 0); err != nil {
t.Fatalf("UpdateAccount error: %v", err)
}
}
// Verify root is an InternalNode.
if _, ok := tr.root.(*InternalNode); !ok {
t.Fatalf("expected InternalNode root, got %T", tr.root)
}
// Query a non-existent address — must return nil.
other := common.HexToAddress("0x5555555555555555555555555555555555555555")
got, err := tr.GetAccount(other)
if err != nil {
t.Fatalf("GetAccount error: %v", err)
}
if got != nil {
t.Fatalf("expected nil for non-existent account, got nonce=%d", got.Nonce)
}
}
// TestGetStorageNonMembershipStemRoot verifies that querying storage for a
// non-existent address returns nil when the root is a StemNode. This is a
// regression test: previously StemNode.Get panicked unconditionally.
func TestGetStorageNonMembershipStemRoot(t *testing.T) {
addr := common.HexToAddress("0x1111111111111111111111111111111111111111")
tr := testAccount(t, addr, 1, 100)
// Verify root is a StemNode.
if _, ok := tr.root.(*StemNode); !ok {
t.Fatalf("expected StemNode root, got %T", tr.root)
}
// Query storage for a different address — must return nil, not panic.
other := common.HexToAddress("0x2222222222222222222222222222222222222222")
slot := common.HexToHash("0x01")
got, err := tr.GetStorage(other, slot[:])
if err != nil {
t.Fatalf("GetStorage error: %v", err)
}
if len(got) > 0 && !bytes.Equal(got, zero[:]) {
t.Fatalf("expected nil/zero for non-existent storage, got %x", got)
}
}
// TestGetStorageNonMembershipInternalRoot verifies that querying storage for a
// non-existent address returns nil when the root is an InternalNode.
func TestGetStorageNonMembershipInternalRoot(t *testing.T) {
tr := &BinaryTrie{
root: NewBinaryNode(),
tracer: trie.NewPrevalueTracer(),
}
addr := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678")
acc := &types.StateAccount{
Nonce: 1,
Balance: uint256.NewInt(1000),
CodeHash: types.EmptyCodeHash[:],
}
if err := tr.UpdateAccount(addr, acc, 0); err != nil {
t.Fatalf("UpdateAccount error: %v", err)
}
// Add a storage slot so the root becomes an InternalNode (storage
// slots use a different stem than account data).
slot := common.HexToHash("0xFF")
val := common.TrimLeftZeroes(common.HexToHash("0xdeadbeef").Bytes())
if err := tr.UpdateStorage(addr, slot[:], val); err != nil {
t.Fatalf("UpdateStorage error: %v", err)
}
if _, ok := tr.root.(*InternalNode); !ok {
t.Fatalf("expected InternalNode root, got %T", tr.root)
}
// Query storage for a non-existent address — must return nil.
other := common.HexToAddress("0x9999999999999999999999999999999999999999")
got, err := tr.GetStorage(other, slot[:])
if err != nil {
t.Fatalf("GetStorage error: %v", err)
}
if len(got) > 0 && !bytes.Equal(got, zero[:]) {
t.Fatalf("expected nil/zero for non-existent storage, got %x", got)
}
}

View file

@ -367,10 +367,10 @@ func (db *Database) StorageIterator(root common.Hash, account common.Hash, seek
// IndexProgress returns the indexing progress made so far. It provides the
// number of states that remain unindexed.
func (db *Database) IndexProgress() (uint64, error) {
func (db *Database) IndexProgress() (uint64, uint64, error) {
pdb, ok := db.backend.(*pathdb.Database)
if !ok {
return 0, errors.New("not supported")
return 0, 0, errors.New("not supported")
}
return pdb.IndexProgress()
}

108
triedb/generate.go Normal file
View file

@ -0,0 +1,108 @@
// Copyright 2026 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/>.
package triedb
import (
"fmt"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/triedb/internal"
)
// kvAccountIterator wraps an ethdb.Iterator to iterate over account snapshot
// entries in the database, implementing internal.AccountIterator.
type kvAccountIterator struct {
it ethdb.Iterator
hash common.Hash
}
func newKVAccountIterator(db ethdb.Iteratee) *kvAccountIterator {
it := rawdb.NewKeyLengthIterator(
db.NewIterator(rawdb.SnapshotAccountPrefix, nil),
len(rawdb.SnapshotAccountPrefix)+common.HashLength,
)
return &kvAccountIterator{it: it}
}
func (it *kvAccountIterator) Next() bool {
if !it.it.Next() {
return false
}
key := it.it.Key()
copy(it.hash[:], key[len(rawdb.SnapshotAccountPrefix):])
return true
}
func (it *kvAccountIterator) Hash() common.Hash { return it.hash }
func (it *kvAccountIterator) Account() []byte { return it.it.Value() }
func (it *kvAccountIterator) Error() error { return it.it.Error() }
func (it *kvAccountIterator) Release() { it.it.Release() }
// kvStorageIterator wraps an ethdb.Iterator to iterate over storage snapshot
// entries for a specific account, implementing internal.StorageIterator.
type kvStorageIterator struct {
it ethdb.Iterator
hash common.Hash
}
func newKVStorageIterator(db ethdb.Iteratee, accountHash common.Hash) *kvStorageIterator {
it := rawdb.IterateStorageSnapshots(db, accountHash)
return &kvStorageIterator{it: it}
}
func (it *kvStorageIterator) Next() bool {
if !it.it.Next() {
return false
}
key := it.it.Key()
copy(it.hash[:], key[len(rawdb.SnapshotStoragePrefix)+common.HashLength:])
return true
}
func (it *kvStorageIterator) Hash() common.Hash { return it.hash }
func (it *kvStorageIterator) Slot() []byte { return it.it.Value() }
func (it *kvStorageIterator) Error() error { return it.it.Error() }
func (it *kvStorageIterator) Release() { it.it.Release() }
// GenerateTrie rebuilds all tries (storage + account) from flat snapshot data
// in the database. It reads account and storage snapshots from the KV store,
// builds tries using StackTrie with streaming node writes, and verifies the
// computed state root matches the expected root.
func GenerateTrie(db ethdb.Database, scheme string, root common.Hash) error {
acctIt := newKVAccountIterator(db)
defer acctIt.Release()
got, err := internal.GenerateTrieRoot(db, scheme, acctIt, common.Hash{}, internal.StackTrieGenerate, func(dst ethdb.KeyValueWriter, accountHash, codeHash common.Hash, stat *internal.GenerateStats) (common.Hash, error) {
storageIt := newKVStorageIterator(db, accountHash)
defer storageIt.Release()
hash, err := internal.GenerateTrieRoot(dst, scheme, storageIt, accountHash, internal.StackTrieGenerate, nil, stat, false)
if err != nil {
return common.Hash{}, err
}
return hash, nil
}, internal.NewGenerateStats(), true)
if err != nil {
return err
}
if got != root {
return fmt.Errorf("state root mismatch: got %x, want %x", got, root)
}
return nil
}

178
triedb/generate_test.go Normal file
View file

@ -0,0 +1,178 @@
// Copyright 2026 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/>.
package triedb
import (
"bytes"
"sort"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie"
"github.com/holiman/uint256"
)
// testAccount is a helper for building test state with deterministic ordering.
type testAccount struct {
hash common.Hash
account types.StateAccount
storage []testSlot // must be sorted by hash
}
type testSlot struct {
hash common.Hash
value []byte
}
// buildExpectedRoot computes the state root from sorted test accounts using
// StackTrie (which requires sorted key insertion).
func buildExpectedRoot(t *testing.T, accounts []testAccount) common.Hash {
t.Helper()
// Sort accounts by hash
sort.Slice(accounts, func(i, j int) bool {
return bytes.Compare(accounts[i].hash[:], accounts[j].hash[:]) < 0
})
acctTrie := trie.NewStackTrie(nil)
for i := range accounts {
data, err := rlp.EncodeToBytes(&accounts[i].account)
if err != nil {
t.Fatal(err)
}
acctTrie.Update(accounts[i].hash[:], data)
}
return acctTrie.Hash()
}
// computeStorageRoot computes the storage trie root from sorted slots.
func computeStorageRoot(slots []testSlot) common.Hash {
sort.Slice(slots, func(i, j int) bool {
return bytes.Compare(slots[i].hash[:], slots[j].hash[:]) < 0
})
st := trie.NewStackTrie(nil)
for _, s := range slots {
st.Update(s.hash[:], s.value)
}
return st.Hash()
}
func TestGenerateTrieEmpty(t *testing.T) {
db := rawdb.NewMemoryDatabase()
if err := GenerateTrie(db, rawdb.HashScheme, types.EmptyRootHash); err != nil {
t.Fatalf("GenerateTrie on empty state failed: %v", err)
}
}
func TestGenerateTrieAccountsOnly(t *testing.T) {
db := rawdb.NewMemoryDatabase()
accounts := []testAccount{
{
hash: common.HexToHash("0x01"),
account: types.StateAccount{
Nonce: 1,
Balance: uint256.NewInt(100),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash.Bytes(),
},
},
{
hash: common.HexToHash("0x02"),
account: types.StateAccount{
Nonce: 2,
Balance: uint256.NewInt(200),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash.Bytes(),
},
},
}
for _, a := range accounts {
rawdb.WriteAccountSnapshot(db, a.hash, types.SlimAccountRLP(a.account))
}
root := buildExpectedRoot(t, accounts)
if err := GenerateTrie(db, rawdb.HashScheme, root); err != nil {
t.Fatalf("GenerateTrie failed: %v", err)
}
}
func TestGenerateTrieWithStorage(t *testing.T) {
db := rawdb.NewMemoryDatabase()
slots := []testSlot{
{hash: common.HexToHash("0xaa"), value: []byte{0x01, 0x02, 0x03}},
{hash: common.HexToHash("0xbb"), value: []byte{0x04, 0x05, 0x06}},
}
storageRoot := computeStorageRoot(slots)
accounts := []testAccount{
{
hash: common.HexToHash("0x01"),
account: types.StateAccount{
Nonce: 1,
Balance: uint256.NewInt(100),
Root: storageRoot,
CodeHash: types.EmptyCodeHash.Bytes(),
},
storage: slots,
},
{
hash: common.HexToHash("0x02"),
account: types.StateAccount{
Nonce: 0,
Balance: uint256.NewInt(50),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash.Bytes(),
},
},
}
// Write account snapshots
for _, a := range accounts {
rawdb.WriteAccountSnapshot(db, a.hash, types.SlimAccountRLP(a.account))
}
// Write storage snapshots
for _, a := range accounts {
for _, s := range a.storage {
rawdb.WriteStorageSnapshot(db, a.hash, s.hash, s.value)
}
}
root := buildExpectedRoot(t, accounts)
if err := GenerateTrie(db, rawdb.HashScheme, root); err != nil {
t.Fatalf("GenerateTrie failed: %v", err)
}
}
func TestGenerateTrieRootMismatch(t *testing.T) {
db := rawdb.NewMemoryDatabase()
acct := types.StateAccount{
Nonce: 1,
Balance: uint256.NewInt(100),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash.Bytes(),
}
rawdb.WriteAccountSnapshot(db, common.HexToHash("0x01"), types.SlimAccountRLP(acct))
wrongRoot := common.HexToHash("0xdeadbeef")
err := GenerateTrie(db, rawdb.HashScheme, wrongRoot)
if err == nil {
t.Fatal("expected error for root mismatch, got nil")
}
}

View file

@ -0,0 +1,363 @@
// Copyright 2026 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/>.
// Package internal contains shared trie generation utilities used by both
// triedb and triedb/pathdb. All code is ported from
// core/state/snapshot/conversion.go (with exported names) unless noted.
package internal
import (
"encoding/binary"
"fmt"
"math"
"runtime"
"sync"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie"
)
// Iterator is an iterator to step over all the accounts or the specific
// storage in a snapshot which may or may not be composed of multiple layers.
type Iterator interface {
// Next steps the iterator forward one element, returning false if exhausted,
// or an error if iteration failed for some reason (e.g. root being iterated
// becomes stale and garbage collected).
Next() bool
// Error returns any failure that occurred during iteration, which might have
// caused a premature iteration exit (e.g. snapshot stack becoming stale).
Error() error
// Hash returns the hash of the account or storage slot the iterator is
// currently at.
Hash() common.Hash
// Release releases associated resources. Release should always succeed and
// can be called multiple times without causing error.
Release()
}
// AccountIterator is an iterator to step over all the accounts in a snapshot,
// which may or may not be composed of multiple layers.
type AccountIterator interface {
Iterator
// Account returns the RLP encoded slim account the iterator is currently at.
// An error will be returned if the iterator becomes invalid
Account() []byte
}
// StorageIterator is an iterator to step over the specific storage in a snapshot,
// which may or may not be composed of multiple layers.
type StorageIterator interface {
Iterator
// Slot returns the storage slot the iterator is currently at. An error will
// be returned if the iterator becomes invalid
Slot() []byte
}
// TrieKV represents a trie key-value pair.
type TrieKV struct {
Key common.Hash
Value []byte
}
type (
// TrieGeneratorFn is the interface of trie generation which can
// be implemented by different trie algorithm.
TrieGeneratorFn func(db ethdb.KeyValueWriter, scheme string, owner common.Hash, in chan (TrieKV), out chan (common.Hash))
// LeafCallbackFn is the callback invoked at the leaves of the trie,
// returns the subtrie root with the specified subtrie identifier.
LeafCallbackFn func(db ethdb.KeyValueWriter, accountHash, codeHash common.Hash, stat *GenerateStats) (common.Hash, error)
)
// GenerateStats is a collection of statistics gathered by the trie generator
// for logging purposes.
type GenerateStats struct {
head common.Hash
start time.Time
accounts uint64 // Number of accounts done (including those being crawled)
slots uint64 // Number of storage slots done (including those being crawled)
slotsStart map[common.Hash]time.Time // Start time for account slot crawling
slotsHead map[common.Hash]common.Hash // Slot head for accounts being crawled
lock sync.RWMutex
}
// NewGenerateStats creates a new generator stats.
func NewGenerateStats() *GenerateStats {
return &GenerateStats{
slotsStart: make(map[common.Hash]time.Time),
slotsHead: make(map[common.Hash]common.Hash),
start: time.Now(),
}
}
// ProgressAccounts updates the generator stats for the account range.
func (stat *GenerateStats) ProgressAccounts(account common.Hash, done uint64) {
stat.lock.Lock()
defer stat.lock.Unlock()
stat.accounts += done
stat.head = account
}
// FinishAccounts updates the generator stats for the finished account range.
func (stat *GenerateStats) FinishAccounts(done uint64) {
stat.lock.Lock()
defer stat.lock.Unlock()
stat.accounts += done
}
// ProgressContract updates the generator stats for a specific in-progress contract.
func (stat *GenerateStats) ProgressContract(account common.Hash, slot common.Hash, done uint64) {
stat.lock.Lock()
defer stat.lock.Unlock()
stat.slots += done
stat.slotsHead[account] = slot
if _, ok := stat.slotsStart[account]; !ok {
stat.slotsStart[account] = time.Now()
}
}
// FinishContract updates the generator stats for a specific just-finished contract.
func (stat *GenerateStats) FinishContract(account common.Hash, done uint64) {
stat.lock.Lock()
defer stat.lock.Unlock()
stat.slots += done
delete(stat.slotsHead, account)
delete(stat.slotsStart, account)
}
// Report prints the cumulative progress statistic smartly.
func (stat *GenerateStats) Report() {
stat.lock.RLock()
defer stat.lock.RUnlock()
ctx := []interface{}{
"accounts", stat.accounts,
"slots", stat.slots,
"elapsed", common.PrettyDuration(time.Since(stat.start)),
}
if stat.accounts > 0 {
if done := binary.BigEndian.Uint64(stat.head[:8]) / stat.accounts; done > 0 {
var (
left = (math.MaxUint64 - binary.BigEndian.Uint64(stat.head[:8])) / stat.accounts
eta = common.CalculateETA(done, left, time.Since(stat.start))
)
// If there are large contract crawls in progress, estimate their finish time
for acc, head := range stat.slotsHead {
start := stat.slotsStart[acc]
if done := binary.BigEndian.Uint64(head[:8]); done > 0 {
left := math.MaxUint64 - binary.BigEndian.Uint64(head[:8])
// Override the ETA if larger than the largest until now
if slotETA := common.CalculateETA(done, left, time.Since(start)); eta < slotETA {
eta = slotETA
}
}
}
ctx = append(ctx, []interface{}{
"eta", common.PrettyDuration(eta),
}...)
}
}
log.Info("Iterating state snapshot", ctx...)
}
// ReportDone prints the last log when the whole generation is finished.
func (stat *GenerateStats) ReportDone() {
stat.lock.RLock()
defer stat.lock.RUnlock()
var ctx []interface{}
ctx = append(ctx, []interface{}{"accounts", stat.accounts}...)
if stat.slots != 0 {
ctx = append(ctx, []interface{}{"slots", stat.slots}...)
}
ctx = append(ctx, []interface{}{"elapsed", common.PrettyDuration(time.Since(stat.start))}...)
log.Info("Iterated snapshot", ctx...)
}
// RunReport periodically prints the progress information.
func RunReport(stats *GenerateStats, stop chan bool) {
timer := time.NewTimer(0)
defer timer.Stop()
for {
select {
case <-timer.C:
stats.Report()
timer.Reset(time.Second * 8)
case success := <-stop:
if success {
stats.ReportDone()
}
return
}
}
}
// GenerateTrieRoot generates the trie hash based on the snapshot iterator.
// It can be used for generating account trie, storage trie or even the
// whole state which connects the accounts and the corresponding storages.
func GenerateTrieRoot(db ethdb.KeyValueWriter, scheme string, it Iterator, account common.Hash, generatorFn TrieGeneratorFn, leafCallback LeafCallbackFn, stats *GenerateStats, report bool) (common.Hash, error) {
var (
in = make(chan TrieKV) // chan to pass leaves
out = make(chan common.Hash, 1) // chan to collect result
stoplog = make(chan bool, 1) // 1-size buffer, works when logging is not enabled
wg sync.WaitGroup
)
// Spin up a go-routine for trie hash re-generation
wg.Add(1)
go func() {
defer wg.Done()
generatorFn(db, scheme, account, in, out)
}()
// Spin up a go-routine for progress logging
if report && stats != nil {
wg.Add(1)
go func() {
defer wg.Done()
RunReport(stats, stoplog)
}()
}
// Create a semaphore to assign tasks and collect results through. We'll pre-
// fill it with nils, thus using the same channel for both limiting concurrent
// processing and gathering results.
threads := runtime.NumCPU()
results := make(chan error, threads)
for i := 0; i < threads; i++ {
results <- nil // fill the semaphore
}
// stop is a helper function to shutdown the background threads
// and return the re-generated trie hash.
stop := func(fail error) (common.Hash, error) {
close(in)
result := <-out
for i := 0; i < threads; i++ {
if err := <-results; err != nil && fail == nil {
fail = err
}
}
stoplog <- fail == nil
wg.Wait()
return result, fail
}
var (
logged = time.Now()
processed = uint64(0)
leaf TrieKV
)
// Start to feed leaves
for it.Next() {
if account == (common.Hash{}) {
var (
err error
fullData []byte
)
if leafCallback == nil {
fullData, err = types.FullAccountRLP(it.(AccountIterator).Account())
if err != nil {
return stop(err)
}
} else {
// Wait until the semaphore allows us to continue, aborting if
// a sub-task failed
if err := <-results; err != nil {
results <- nil // stop will drain the results, add a noop back for this error we just consumed
return stop(err)
}
// Fetch the next account and process it concurrently
account, err := types.FullAccount(it.(AccountIterator).Account())
if err != nil {
return stop(err)
}
go func(hash common.Hash) {
subroot, err := leafCallback(db, hash, common.BytesToHash(account.CodeHash), stats)
if err != nil {
results <- err
return
}
if account.Root != subroot {
results <- fmt.Errorf("invalid subroot(path %x), want %x, have %x", hash, account.Root, subroot)
return
}
results <- nil
}(it.Hash())
fullData, err = rlp.EncodeToBytes(account)
if err != nil {
return stop(err)
}
}
leaf = TrieKV{it.Hash(), fullData}
} else {
leaf = TrieKV{it.Hash(), common.CopyBytes(it.(StorageIterator).Slot())}
}
in <- leaf
// Accumulate the generation statistic if it's required.
processed++
if time.Since(logged) > 3*time.Second && stats != nil {
if account == (common.Hash{}) {
stats.ProgressAccounts(it.Hash(), processed)
} else {
stats.ProgressContract(account, it.Hash(), processed)
}
logged, processed = time.Now(), 0
}
}
// Commit the last part statistic.
if processed > 0 && stats != nil {
if account == (common.Hash{}) {
stats.FinishAccounts(processed)
} else {
stats.FinishContract(account, processed)
}
}
return stop(nil)
}
// StackTrieGenerate is the trie generation function that creates a StackTrie
// and persists nodes via rawdb.WriteTrieNode.
func StackTrieGenerate(db ethdb.KeyValueWriter, scheme string, owner common.Hash, in chan TrieKV, out chan common.Hash) {
var onTrieNode trie.OnTrieNode
if db != nil {
onTrieNode = func(path []byte, hash common.Hash, blob []byte) {
rawdb.WriteTrieNode(db, owner, path, hash, blob, scheme)
}
}
t := trie.NewStackTrie(onTrieNode)
for leaf := range in {
t.Update(leaf.Key[:], leaf.Value)
}
out <- t.Hash()
}

View file

@ -626,11 +626,26 @@ func (db *Database) HistoryRange() (uint64, uint64, error) {
// IndexProgress returns the indexing progress made so far. It provides the
// number of states that remain unindexed.
func (db *Database) IndexProgress() (uint64, error) {
if db.stateIndexer == nil {
return 0, nil
func (db *Database) IndexProgress() (uint64, uint64, error) {
var (
stateProgress uint64
trieProgress uint64
)
if db.stateIndexer != nil {
prog, err := db.stateIndexer.progress()
if err != nil {
return 0, 0, err
}
stateProgress = prog
}
return db.stateIndexer.progress()
if db.trienodeIndexer != nil {
prog, err := db.trienodeIndexer.progress()
if err != nil {
return 0, 0, err
}
trieProgress = prog
}
return stateProgress, trieProgress, nil
}
// AccountIterator creates a new account iterator for the specified root hash and

View file

@ -987,7 +987,7 @@ func TestDatabaseIndexRecovery(t *testing.T) {
t.Fatalf("Unexpected state history found, %d", i)
}
}
remain, err := env.db.IndexProgress()
remain, _, err := env.db.IndexProgress()
if err != nil {
t.Fatalf("Failed to obtain the progress, %v", err)
}
@ -1001,7 +1001,7 @@ func TestDatabaseIndexRecovery(t *testing.T) {
panic(fmt.Errorf("failed to update state changes, err: %w", err))
}
}
remain, err = env.db.IndexProgress()
remain, _, err = env.db.IndexProgress()
if err != nil {
t.Fatalf("Failed to obtain the progress, %v", err)
}

View file

@ -408,6 +408,11 @@ func (dl *diskLayer) writeHistory(typ historyType, diff *diffLayer) (bool, error
if err != nil {
return false, err
}
// Notify the index pruner about the new tail so that stale index
// blocks referencing the pruned histories can be cleaned up.
if indexer != nil && pruned > 0 {
indexer.prune(newFirst)
}
log.Debug("Pruned history", "type", typ, "items", pruned, "tailid", newFirst)
return false, nil
}

View file

@ -0,0 +1,385 @@
// Copyright 2025 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/>.
package pathdb
import (
"encoding/binary"
"sync"
"sync/atomic"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
)
const (
// indexPruningThreshold defines the number of pruned histories that must
// accumulate before triggering index pruning. This helps avoid scheduling
// index pruning too frequently.
indexPruningThreshold = 90000
// iteratorReopenInterval is how long the iterator is kept open before
// being released and re-opened. Long-lived iterators hold a read snapshot
// that blocks LSM compaction; periodically re-opening avoids stalling the
// compactor during a large scan.
iteratorReopenInterval = 30 * time.Second
)
// indexPruner is responsible for pruning stale index data from the tail side
// when old history objects are removed. It runs as a background goroutine and
// processes pruning signals whenever the history tail advances.
//
// The pruning operates at the block level: for each state element's index
// metadata, leading index blocks whose maximum history ID falls below the
// new tail are removed entirely. This avoids the need to decode individual
// block contents and is efficient because index blocks store monotonically
// increasing history IDs.
type indexPruner struct {
disk ethdb.KeyValueStore
typ historyType
tail atomic.Uint64 // Tail below which index entries can be pruned
lastRun uint64 // The tail in the last pruning run
trigger chan struct{} // Non-blocking signal that tail has advanced
closed chan struct{}
wg sync.WaitGroup
log log.Logger
pauseReq chan chan struct{} // Pause request; caller sends ack channel, pruner closes it when paused
resumeCh chan struct{} // Resume signal sent by caller after indexSingle/unindexSingle completes
}
// newIndexPruner creates and starts a new index pruner for the given history type.
func newIndexPruner(disk ethdb.KeyValueStore, typ historyType) *indexPruner {
p := &indexPruner{
disk: disk,
typ: typ,
trigger: make(chan struct{}, 1),
closed: make(chan struct{}),
log: log.New("type", typ.String()),
pauseReq: make(chan chan struct{}),
resumeCh: make(chan struct{}),
}
p.wg.Add(1)
go p.run()
return p
}
// prune signals the pruner that the history tail has advanced to the given ID.
// All index entries referencing history IDs below newTail can be removed.
func (p *indexPruner) prune(newTail uint64) {
// Only update if the tail is actually advancing
for {
old := p.tail.Load()
if newTail <= old {
return
}
if p.tail.CompareAndSwap(old, newTail) {
break
}
}
// Non-blocking signal
select {
case p.trigger <- struct{}{}:
default:
}
}
// pause requests the pruner to flush all pending writes and pause. It blocks
// until the pruner has acknowledged the pause. This must be paired with a
// subsequent call to resume.
func (p *indexPruner) pause() {
ack := make(chan struct{})
select {
case p.pauseReq <- ack:
<-ack // wait for the pruner to flush and acknowledge
case <-p.closed:
}
}
// resume unblocks a previously paused pruner, allowing it to continue
// processing.
func (p *indexPruner) resume() {
select {
case p.resumeCh <- struct{}{}:
case <-p.closed:
}
}
// close shuts down the pruner and waits for it to finish.
func (p *indexPruner) close() {
select {
case <-p.closed:
return
default:
close(p.closed)
p.wg.Wait()
}
}
// run is the main loop of the pruner. It waits for trigger signals and
// processes a small batch of entries on each trigger, advancing the cursor.
func (p *indexPruner) run() {
defer p.wg.Done()
for {
select {
case <-p.trigger:
tail := p.tail.Load()
if tail < p.lastRun || tail-p.lastRun < indexPruningThreshold {
continue
}
if err := p.process(tail); err != nil {
p.log.Error("Failed to prune index", "tail", tail, "err", err)
} else {
p.lastRun = tail
}
case ack := <-p.pauseReq:
// Pruner is idle, acknowledge immediately and wait for resume.
close(ack)
select {
case <-p.resumeCh:
case <-p.closed:
return
}
case <-p.closed:
return
}
}
}
// process iterates all index metadata entries for the history type and prunes
// leading blocks whose max history ID is below the given tail.
func (p *indexPruner) process(tail uint64) error {
var (
err error
pruned int
start = time.Now()
)
switch p.typ {
case typeStateHistory:
n, err := p.prunePrefix(rawdb.StateHistoryAccountMetadataPrefix, typeAccount, tail)
if err != nil {
return err
}
pruned += n
n, err = p.prunePrefix(rawdb.StateHistoryStorageMetadataPrefix, typeStorage, tail)
if err != nil {
return err
}
pruned += n
statePruneHistoryIndexTimer.UpdateSince(start)
case typeTrienodeHistory:
pruned, err = p.prunePrefix(rawdb.TrienodeHistoryMetadataPrefix, typeTrienode, tail)
if err != nil {
return err
}
trienodePruneHistoryIndexTimer.UpdateSince(start)
default:
panic("unknown history type")
}
if pruned > 0 {
p.log.Info("Pruned stale index blocks", "pruned", pruned, "tail", tail, "elapsed", common.PrettyDuration(time.Since(start)))
}
return nil
}
// prunePrefix scans all metadata entries under the given prefix and prunes
// leading index blocks below the tail. The iterator is periodically released
// and re-opened to avoid holding a read snapshot that blocks LSM compaction.
func (p *indexPruner) prunePrefix(prefix []byte, elemType elementType, tail uint64) (int, error) {
var (
pruned int
opened = time.Now()
it = p.disk.NewIterator(prefix, nil)
batch = p.disk.NewBatchWithSize(ethdb.IdealBatchSize)
)
for {
// Terminate if iterator is exhausted
if !it.Next() {
it.Release()
break
}
// Check termination or pause request
select {
case <-p.closed:
// Terminate the process if indexer is closed
it.Release()
if batch.ValueSize() > 0 {
return pruned, batch.Write()
}
return pruned, nil
case ack := <-p.pauseReq:
// Save the current position so that after resume the
// iterator can be re-opened from where it left off.
start := common.CopyBytes(it.Key()[len(prefix):])
it.Release()
// Flush all pending writes before acknowledging the pause.
var flushErr error
if batch.ValueSize() > 0 {
if err := batch.Write(); err != nil {
flushErr = err
}
batch.Reset()
}
close(ack)
// Block until resumed or closed. Always wait here even if
// the flush failed — returning early would cause resume()
// to deadlock since nobody would receive on resumeCh.
select {
case <-p.resumeCh:
if flushErr != nil {
return 0, flushErr
}
// Re-open the iterator from the saved position so the
// pruner sees the current database state (including any
// writes made by indexer during the pause).
it = p.disk.NewIterator(prefix, start)
opened = time.Now()
continue
case <-p.closed:
return pruned, flushErr
}
default:
// Keep processing
}
// Prune the index data block
key, value := it.Key(), it.Value()
ident, bsize := p.identFromKey(key, prefix, elemType)
n, err := p.pruneEntry(batch, ident, value, bsize, tail)
if err != nil {
p.log.Warn("Failed to prune index entry", "ident", ident, "err", err)
continue
}
pruned += n
// Flush the batch if there are too many accumulated
if batch.ValueSize() >= ethdb.IdealBatchSize {
if err := batch.Write(); err != nil {
it.Release()
return 0, err
}
batch.Reset()
}
// Periodically release the iterator so the LSM compactor
// is not blocked by the read snapshot we hold.
if time.Since(opened) >= iteratorReopenInterval {
opened = time.Now()
start := common.CopyBytes(it.Key()[len(prefix):])
it.Release()
it = p.disk.NewIterator(prefix, start)
}
}
if batch.ValueSize() > 0 {
if err := batch.Write(); err != nil {
return 0, err
}
}
return pruned, nil
}
// identFromKey reconstructs the stateIdent and bitmapSize from a metadata key.
func (p *indexPruner) identFromKey(key []byte, prefix []byte, elemType elementType) (stateIdent, int) {
rest := key[len(prefix):]
switch elemType {
case typeAccount:
// key = prefix + addressHash(32)
var addrHash common.Hash
copy(addrHash[:], rest[:32])
return newAccountIdent(addrHash), 0
case typeStorage:
// key = prefix + addressHash(32) + storageHash(32)
var addrHash, storHash common.Hash
copy(addrHash[:], rest[:32])
copy(storHash[:], rest[32:64])
return newStorageIdent(addrHash, storHash), 0
case typeTrienode:
// key = prefix + addressHash(32) + path(variable)
var addrHash common.Hash
copy(addrHash[:], rest[:32])
path := string(rest[32:])
ident := newTrienodeIdent(addrHash, path)
return ident, ident.bloomSize()
default:
panic("unknown element type")
}
}
// pruneEntry checks a single metadata entry and removes leading index blocks
// whose max < tail. Returns the number of blocks pruned.
func (p *indexPruner) pruneEntry(batch ethdb.Batch, ident stateIdent, blob []byte, bsize int, tail uint64) (int, error) {
// Fast path: the first 8 bytes of the metadata encode the max history ID
// of the first index block (big-endian uint64). If it is >= tail, no
// blocks can be pruned and we skip the full parse entirely.
if len(blob) >= 8 && binary.BigEndian.Uint64(blob[:8]) >= tail {
return 0, nil
}
descList, err := parseIndex(blob, bsize)
if err != nil {
return 0, err
}
// Find the number of leading blocks that can be entirely pruned.
// A block can be pruned if its max history ID is strictly below
// the tail.
var count int
for _, desc := range descList {
if desc.max < tail {
count++
} else {
break // blocks are ordered, no more to prune
}
}
if count == 0 {
return 0, nil
}
// Delete the pruned index blocks
for i := 0; i < count; i++ {
deleteStateIndexBlock(ident, batch, descList[i].id)
}
// Update or delete the metadata
remaining := descList[count:]
if len(remaining) == 0 {
// All blocks pruned, remove the metadata entry entirely
deleteStateIndex(ident, batch)
} else {
// Rewrite the metadata with the remaining blocks
size := indexBlockDescSize + bsize
buf := make([]byte, 0, size*len(remaining))
for _, desc := range remaining {
buf = append(buf, desc.encode()...)
}
writeStateIndex(ident, batch, buf)
}
return count, nil
}

View file

@ -0,0 +1,355 @@
// Copyright 2025 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/>.
package pathdb
import (
"math"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
)
func writeMultiBlockIndex(t *testing.T, db ethdb.Database, ident stateIdent, bitmapSize int, startID uint64) []*indexBlockDesc {
t.Helper()
if startID == 0 {
startID = 1
}
iw, _ := newIndexWriter(db, ident, 0, bitmapSize)
for i := 0; i < 10000; i++ {
if err := iw.append(startID+uint64(i), randomExt(bitmapSize, 5)); err != nil {
t.Fatalf("Failed to append element %d: %v", i, err)
}
}
batch := db.NewBatch()
iw.finish(batch)
if err := batch.Write(); err != nil {
t.Fatalf("Failed to write batch: %v", err)
}
blob := readStateIndex(ident, db)
descList, err := parseIndex(blob, bitmapSize)
if err != nil {
t.Fatalf("Failed to parse index: %v", err)
}
return descList
}
// TestPruneEntryBasic verifies that pruneEntry correctly removes leading index
// blocks whose max is below the given tail.
func TestPruneEntryBasic(t *testing.T) {
db := rawdb.NewMemoryDatabase()
ident := newAccountIdent(common.Hash{0xa})
descList := writeMultiBlockIndex(t, db, ident, 0, 1)
// Prune with a tail that is above the first block's max but below the second
firstBlockMax := descList[0].max
pruner := newIndexPruner(db, typeStateHistory)
defer pruner.close()
if err := pruner.process(firstBlockMax + 1); err != nil {
t.Fatalf("Failed to process pruning: %v", err)
}
// Verify the first block was removed
blob := readStateIndex(ident, db)
if len(blob) == 0 {
t.Fatal("Index metadata should not be empty after partial prune")
}
remaining, err := parseIndex(blob, 0)
if err != nil {
t.Fatalf("Failed to parse index after prune: %v", err)
}
if len(remaining) != len(descList)-1 {
t.Fatalf("Expected %d blocks remaining, got %d", len(descList)-1, len(remaining))
}
// The first remaining block should be what was previously the second block
if remaining[0].id != descList[1].id {
t.Fatalf("Expected first remaining block id %d, got %d", descList[1].id, remaining[0].id)
}
// Verify the pruned block data is actually deleted
blockData := readStateIndexBlock(ident, db, descList[0].id)
if len(blockData) != 0 {
t.Fatal("Pruned block data should have been deleted")
}
// Remaining blocks should still have their data
for _, desc := range remaining {
blockData = readStateIndexBlock(ident, db, desc.id)
if len(blockData) == 0 {
t.Fatalf("Block %d data should still exist", desc.id)
}
}
}
// TestPruneEntryBasicTrienode is the same as TestPruneEntryBasic but for
// trienode index entries with a non-zero bitmapSize.
func TestPruneEntryBasicTrienode(t *testing.T) {
db := rawdb.NewMemoryDatabase()
addrHash := common.Hash{0xa}
path := string([]byte{0x0, 0x0, 0x0})
ident := newTrienodeIdent(addrHash, path)
descList := writeMultiBlockIndex(t, db, ident, ident.bloomSize(), 1)
firstBlockMax := descList[0].max
pruner := newIndexPruner(db, typeTrienodeHistory)
defer pruner.close()
if err := pruner.process(firstBlockMax + 1); err != nil {
t.Fatalf("Failed to process pruning: %v", err)
}
blob := readStateIndex(ident, db)
remaining, err := parseIndex(blob, ident.bloomSize())
if err != nil {
t.Fatalf("Failed to parse index after prune: %v", err)
}
if len(remaining) != len(descList)-1 {
t.Fatalf("Expected %d blocks remaining, got %d", len(descList)-1, len(remaining))
}
if remaining[0].id != descList[1].id {
t.Fatalf("Expected first remaining block id %d, got %d", descList[1].id, remaining[0].id)
}
blockData := readStateIndexBlock(ident, db, descList[0].id)
if len(blockData) != 0 {
t.Fatal("Pruned block data should have been deleted")
}
}
// TestPruneEntryComplete verifies that when all blocks are pruned, the metadata
// entry is also deleted.
func TestPruneEntryComplete(t *testing.T) {
db := rawdb.NewMemoryDatabase()
ident := newAccountIdent(common.Hash{0xb})
iw, _ := newIndexWriter(db, ident, 0, 0)
for i := 1; i <= 10; i++ {
if err := iw.append(uint64(i), nil); err != nil {
t.Fatalf("Failed to append: %v", err)
}
}
batch := db.NewBatch()
iw.finish(batch)
if err := batch.Write(); err != nil {
t.Fatalf("Failed to write: %v", err)
}
pruner := newIndexPruner(db, typeStateHistory)
defer pruner.close()
// Prune with tail above all elements
if err := pruner.process(11); err != nil {
t.Fatalf("Failed to process: %v", err)
}
// Metadata entry should be deleted
blob := readStateIndex(ident, db)
if len(blob) != 0 {
t.Fatal("Index metadata should be empty after full prune")
}
}
// TestPruneNoop verifies that pruning does nothing when the tail is below all
// block maximums.
func TestPruneNoop(t *testing.T) {
db := rawdb.NewMemoryDatabase()
ident := newAccountIdent(common.Hash{0xc})
iw, _ := newIndexWriter(db, ident, 0, 0)
for i := 100; i <= 200; i++ {
if err := iw.append(uint64(i), nil); err != nil {
t.Fatalf("Failed to append: %v", err)
}
}
batch := db.NewBatch()
iw.finish(batch)
if err := batch.Write(); err != nil {
t.Fatalf("Failed to write: %v", err)
}
blob := readStateIndex(ident, db)
origLen := len(blob)
pruner := newIndexPruner(db, typeStateHistory)
defer pruner.close()
if err := pruner.process(50); err != nil {
t.Fatalf("Failed to process: %v", err)
}
// Nothing should have changed
blob = readStateIndex(ident, db)
if len(blob) != origLen {
t.Fatalf("Expected no change, original len %d, got %d", origLen, len(blob))
}
}
// TestPrunePreservesReadability verifies that after pruning, the remaining
// index data is still readable and returns correct results.
func TestPrunePreservesReadability(t *testing.T) {
db := rawdb.NewMemoryDatabase()
ident := newAccountIdent(common.Hash{0xe})
descList := writeMultiBlockIndex(t, db, ident, 0, 1)
firstBlockMax := descList[0].max
pruner := newIndexPruner(db, typeStateHistory)
defer pruner.close()
if err := pruner.process(firstBlockMax + 1); err != nil {
t.Fatalf("Failed to process: %v", err)
}
// Read the remaining index and verify lookups still work
ir, err := newIndexReader(db, ident, 0)
if err != nil {
t.Fatalf("Failed to create reader: %v", err)
}
// Looking for something greater than firstBlockMax should still work
result, err := ir.readGreaterThan(firstBlockMax)
if err != nil {
t.Fatalf("Failed to read: %v", err)
}
if result != firstBlockMax+1 {
t.Fatalf("Expected %d, got %d", firstBlockMax+1, result)
}
// Looking for the last element should return MaxUint64
result, err = ir.readGreaterThan(20000)
if err != nil {
t.Fatalf("Failed to read: %v", err)
}
if result != math.MaxUint64 {
t.Fatalf("Expected MaxUint64, got %d", result)
}
}
// TestPrunePauseResume verifies the pause/resume mechanism:
// - The pruner pauses mid-iteration and flushes its batch
// - Data written while the pruner is paused (simulating indexSingle) is
// visible after resume via a fresh iterator
// - Pruning still completes correctly after resume
func TestPrunePauseResume(t *testing.T) {
db := rawdb.NewMemoryDatabase()
// Create many accounts with multi-block indexes so the pruner is still
// iterating when the pause request arrives.
var firstBlockMax uint64
for i := 0; i < 200; i++ {
hash := common.Hash{byte(i)}
ident := newAccountIdent(hash)
descList := writeMultiBlockIndex(t, db, ident, 0, 1)
if i == 0 {
firstBlockMax = descList[0].max
}
}
// Target account at the end of the key space — the pruner should not
// have visited it yet when the pause is acknowledged.
targetIdent := newAccountIdent(common.Hash{0xff})
targetDescList := writeMultiBlockIndex(t, db, targetIdent, 0, 1)
tail := firstBlockMax + 1
// Construct the pruner without starting run(). We call process()
// directly to exercise the mid-iteration pause path deterministically.
pruner := &indexPruner{
disk: db,
typ: typeStateHistory,
log: log.New("type", "account"),
closed: make(chan struct{}),
pauseReq: make(chan chan struct{}, 1), // buffered so we can pre-deposit
resumeCh: make(chan struct{}),
}
// Pre-deposit a pause request before process() starts. Because
// pauseReq is buffered, this succeeds immediately. When prunePrefix's
// select checks the channel on an early iteration, it will find the
// pending request and pause — no scheduling race is possible.
ack := make(chan struct{})
pruner.pauseReq <- ack
// Run process() in the background.
errCh := make(chan error, 1)
go func() {
errCh <- pruner.process(tail)
}()
// Block until the pruner has flushed pending writes and acknowledged.
<-ack
// While paused, append a new element to the target account's index,
// simulating what indexSingle would do during the pause window.
lastMax := targetDescList[len(targetDescList)-1].max
newID := lastMax + 10000
iw, err := newIndexWriter(db, targetIdent, lastMax, 0)
if err != nil {
t.Fatalf("Failed to create index writer: %v", err)
}
if err := iw.append(newID, nil); err != nil {
t.Fatalf("Failed to append: %v", err)
}
batch := db.NewBatch()
iw.finish(batch)
if err := batch.Write(); err != nil {
t.Fatalf("Failed to write batch: %v", err)
}
// Resume the pruner.
pruner.resume()
// Wait for process() to complete.
if err := <-errCh; err != nil {
t.Fatalf("process() failed: %v", err)
}
// Verify: the entry written during the pause must still be accessible.
// If the pruner used a stale iterator snapshot, it would overwrite the
// target's metadata and lose the new entry.
ir, err := newIndexReader(db, targetIdent, 0)
if err != nil {
t.Fatalf("Failed to create index reader: %v", err)
}
result, err := ir.readGreaterThan(newID - 1)
if err != nil {
t.Fatalf("Failed to read: %v", err)
}
if result != newID {
t.Fatalf("Entry written during pause was lost: want %d, got %d", newID, result)
}
// Verify: pruning actually occurred on an early account.
earlyIdent := newAccountIdent(common.Hash{0x00})
earlyBlob := readStateIndex(earlyIdent, db)
if len(earlyBlob) == 0 {
t.Fatal("Early account index should not be completely empty")
}
earlyRemaining, err := parseIndex(earlyBlob, 0)
if err != nil {
t.Fatalf("Failed to parse early account index: %v", err)
}
// The first block (id=0) should have been pruned.
if earlyRemaining[0].id == 0 {
t.Fatal("First block of early account should have been pruned")
}
}

View file

@ -719,6 +719,7 @@ func (i *indexIniter) recover() bool {
// state history.
type historyIndexer struct {
initer *indexIniter
pruner *indexPruner
typ historyType
disk ethdb.KeyValueStore
freezer ethdb.AncientStore
@ -774,6 +775,7 @@ func newHistoryIndexer(disk ethdb.Database, freezer ethdb.AncientStore, lastHist
checkVersion(disk, typ)
return &historyIndexer{
initer: newIndexIniter(disk, freezer, typ, lastHistoryID, noWait),
pruner: newIndexPruner(disk, typ),
typ: typ,
disk: disk,
freezer: freezer,
@ -782,6 +784,7 @@ func newHistoryIndexer(disk ethdb.Database, freezer ethdb.AncientStore, lastHist
func (i *historyIndexer) close() {
i.initer.close()
i.pruner.close()
}
// inited returns a flag indicating whether the existing state histories
@ -802,6 +805,8 @@ func (i *historyIndexer) extend(historyID uint64) error {
case <-i.initer.closed:
return errors.New("indexer is closed")
case <-i.initer.done:
i.pruner.pause()
defer i.pruner.resume()
return indexSingle(historyID, i.disk, i.freezer, i.typ)
case i.initer.interrupt <- signal:
return <-signal.result
@ -819,12 +824,27 @@ func (i *historyIndexer) shorten(historyID uint64) error {
case <-i.initer.closed:
return errors.New("indexer is closed")
case <-i.initer.done:
i.pruner.pause()
defer i.pruner.resume()
return unindexSingle(historyID, i.disk, i.freezer, i.typ)
case i.initer.interrupt <- signal:
return <-signal.result
}
}
// prune signals the pruner that the history tail has advanced to the given ID,
// so that stale index blocks referencing pruned histories can be removed.
func (i *historyIndexer) prune(newTail uint64) {
select {
case <-i.initer.closed:
log.Debug("Ignored the pruning signal", "reason", "closed")
case <-i.initer.done:
i.pruner.prune(newTail)
default:
log.Debug("Ignored the pruning signal", "reason", "busy")
}
}
// progress returns the indexing progress made so far. It provides the number
// of states that remain unindexed.
func (i *historyIndexer) progress() (uint64, error) {

View file

@ -24,48 +24,15 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/triedb/internal"
)
// Iterator is an iterator to step over all the accounts or the specific
// storage in a snapshot which may or may not be composed of multiple layers.
type Iterator interface {
// Next steps the iterator forward one element, returning false if exhausted,
// or an error if iteration failed for some reason (e.g. root being iterated
// becomes stale and garbage collected).
Next() bool
// Error returns any failure that occurred during iteration, which might have
// caused a premature iteration exit (e.g. layer stack becoming stale).
Error() error
// Hash returns the hash of the account or storage slot the iterator is
// currently at.
Hash() common.Hash
// Release releases associated resources. Release should always succeed and
// can be called multiple times without causing error.
Release()
}
// AccountIterator is an iterator to step over all the accounts in a snapshot,
// which may or may not be composed of multiple layers.
type AccountIterator interface {
Iterator
// Account returns the RLP encoded slim account the iterator is currently at.
// An error will be returned if the iterator becomes invalid
Account() []byte
}
// StorageIterator is an iterator to step over the specific storage in a snapshot,
// which may or may not be composed of multiple layers.
type StorageIterator interface {
Iterator
// Slot returns the storage slot the iterator is currently at. An error will
// be returned if the iterator becomes invalid
Slot() []byte
}
// Type aliases for the iterator interfaces defined in triedb/internal.
type (
Iterator = internal.Iterator
AccountIterator = internal.AccountIterator
StorageIterator = internal.StorageIterator
)
type (
// loadAccount is the function to retrieve the account from the associated

View file

@ -151,6 +151,15 @@ func (tree *layerTree) add(root common.Hash, parentRoot common.Hash, block uint6
if root == parentRoot {
return errors.New("layer cycle")
}
// If a layer with this root already exists, skip the insertion. Fork blocks
// can produce the same state root as the canonical block (same parent, same
// coinbase, zero txs); overwriting tree.layers[root] would corrupt the parent
// chain for any child layers already built on top of the existing one, and
// appending a duplicate root to the lookup indices causes accountTip/storageTip
// to resolve the wrong layer.
if tree.get(root) != nil {
return nil
}
parent := tree.get(parentRoot)
if parent == nil {
return fmt.Errorf("triedb parent [%#x] layer missing", parentRoot)
@ -310,8 +319,8 @@ func (tree *layerTree) lookupAccount(accountHash common.Hash, state common.Hash)
tree.lock.RLock()
defer tree.lock.RUnlock()
tip := tree.lookup.accountTip(accountHash, state, tree.base.root)
if tip == (common.Hash{}) {
tip, ok := tree.lookup.accountTip(accountHash, state, tree.base.root)
if !ok {
return nil, fmt.Errorf("[%#x] %w", state, errSnapshotStale)
}
l := tree.layers[tip]
@ -328,8 +337,8 @@ func (tree *layerTree) lookupStorage(accountHash common.Hash, slotHash common.Ha
tree.lock.RLock()
defer tree.lock.RUnlock()
tip := tree.lookup.storageTip(accountHash, slotHash, state, tree.base.root)
if tip == (common.Hash{}) {
tip, ok := tree.lookup.storageTip(accountHash, slotHash, state, tree.base.root)
if !ok {
return nil, fmt.Errorf("[%#x] %w", state, errSnapshotStale)
}
l := tree.layers[tip]

View file

@ -575,6 +575,40 @@ func TestDescendant(t *testing.T) {
}
}
func TestDuplicateRootLookup(t *testing.T) {
// Chain:
// C1->C2->C3 (HEAD)
tr := newTestLayerTree() // base = 0x1
tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, NewNodeSetWithOrigin(nil, nil),
NewStateSetWithOrigin(randomAccountSet("0xa"), randomStorageSet([]string{"0xa"}, [][]string{{"0x1"}}, nil), nil, nil, false))
tr.add(common.Hash{0x3}, common.Hash{0x2}, 2, NewNodeSetWithOrigin(nil, nil),
NewStateSetWithOrigin(randomAccountSet("0xa"), randomStorageSet([]string{"0xa"}, [][]string{{"0x1"}}, nil), nil, nil, false))
// A fork block with the same state root as C2; inserting it must not
// pollute the lookup history for the canonical descendant C3.
tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, NewNodeSetWithOrigin(nil, nil),
NewStateSetWithOrigin(randomAccountSet("0xa"), randomStorageSet([]string{"0xa"}, [][]string{{"0x1"}}, nil), nil, nil, false))
if n := tr.len(); n != 3 {
t.Fatalf("duplicate root insert changed layer count, got %d, want 3", n)
}
l, err := tr.lookupAccount(common.HexToHash("0xa"), common.Hash{0x3})
if err != nil {
t.Fatalf("account lookup failed: %v", err)
}
if l.rootHash() != (common.Hash{0x3}) {
t.Errorf("unexpected account tip, want %x, got %x", common.Hash{0x3}, l.rootHash())
}
l, err = tr.lookupStorage(common.HexToHash("0xa"), common.HexToHash("0x1"), common.Hash{0x3})
if err != nil {
t.Fatalf("storage lookup failed: %v", err)
}
if l.rootHash() != (common.Hash{0x3}) {
t.Errorf("unexpected storage tip, want %x, got %x", common.Hash{0x3}, l.rootHash())
}
}
func TestAccountLookup(t *testing.T) {
// Chain:
// C1->C2->C3->C4 (HEAD)
@ -882,3 +916,118 @@ func TestStorageLookup(t *testing.T) {
}
}
}
// TestLookupZeroBaseRootFallback is a regression test for a sentinel
// collision in accountTip/storageTip: before the fix they returned
// common.Hash{} as both the "stale" marker and the disk-layer fallback
// when the disk root itself happened to be zero. lookupAccount/Storage
// then misreported a legitimate fallback as errSnapshotStale.
//
// On the merkle path the collision was invisible because the empty
// merkle trie hashes to types.EmptyRootHash (a concrete non-zero
// keccak), so the disk layer's root was never the zero hash in
// practice. The bug only surfaces once the disk layer root can
// legitimately be zero (for example a fresh verkle/bintrie database
// where the empty binary trie hashes to EmptyVerkleHash ==
// common.Hash{}).
//
// The test constructs a layer tree whose base layer's root IS the zero
// hash, stacks diff layers on top, and exercises four cases:
//
// 1. Look up an account NEVER written → should fall through to the
// disk layer and return (diskLayer, nil). Before the fix this
// returned errSnapshotStale because the fallback hash collided
// with the sentinel.
// 2. Symmetric case for lookupStorage.
// 3. Look up an account written in a diff layer → should return that
// diff layer (the normal happy path is unaffected by the fix).
// 4. Look up any key at a state root that isn't part of the tree
// (neither the disk root nor a descendant of it) → MUST still
// return errSnapshotStale. This pins the "other half" of the
// contract so a future refactor that always returns ok=true would
// fail here.
func TestLookupZeroBaseRootFallback(t *testing.T) {
// Build a layer tree whose disk-layer root is common.Hash{} —
// mirrors the bintrie/verkle configuration where the empty trie
// hashes to EmptyVerkleHash. newTestLayerTree can't be reused
// because it hard-codes common.Hash{0x1}.
db := New(rawdb.NewMemoryDatabase(), nil, false)
base := newDiskLayer(common.Hash{}, 0, db, nil, nil, newBuffer(0, nil, nil, 0), nil)
tr := newLayerTree(base)
// Stack two diff layers on the zero-rooted disk layer, each
// touching a known account and slot so we have something for the
// happy-path lookups to find later.
if err := tr.add(
common.Hash{0x2}, common.Hash{},
1,
NewNodeSetWithOrigin(nil, nil),
NewStateSetWithOrigin(
randomAccountSet("0xa"),
randomStorageSet([]string{"0xa"}, [][]string{{"0x1"}}, nil),
nil, nil, false),
); err != nil {
t.Fatalf("add first diff layer: %v", err)
}
if err := tr.add(
common.Hash{0x3}, common.Hash{0x2},
2,
NewNodeSetWithOrigin(nil, nil),
NewStateSetWithOrigin(
randomAccountSet("0xb"),
nil, nil, nil, false),
); err != nil {
t.Fatalf("add second diff layer: %v", err)
}
// Case 1: unknown account queried at the head. The lookup must
// fall through the diff layers, hit the disk-layer fallback at
// base=common.Hash{}, and return the disk layer with no error —
// NOT errSnapshotStale.
l, err := tr.lookupAccount(common.HexToHash("0xdead"), common.Hash{0x3})
if err != nil {
t.Fatalf("lookupAccount on zero-base disk layer: unexpected error %v", err)
}
if l.rootHash() != (common.Hash{}) {
t.Errorf("expected fall-through to disk layer (root=0), got %x", l.rootHash())
}
// Case 2: symmetric check for storage. Slot 0x99 was never written,
// so the lookup must fall through to the disk layer just like
// Case 1.
l, err = tr.lookupStorage(
common.HexToHash("0xdead"), common.HexToHash("0x99"), common.Hash{0x3})
if err != nil {
t.Fatalf("lookupStorage on zero-base disk layer: unexpected error %v", err)
}
if l.rootHash() != (common.Hash{}) {
t.Errorf("expected fall-through to disk layer (root=0), got %x", l.rootHash())
}
// Case 3: happy path. Account 0xa was written at diff layer 0x2.
// The lookup must return that layer, proving the fix didn't break
// the normal resolution path.
l, err = tr.lookupAccount(common.HexToHash("0xa"), common.Hash{0x3})
if err != nil {
t.Fatalf("lookupAccount(known): %v", err)
}
if l.rootHash() != (common.Hash{0x2}) {
t.Errorf("known account tip: want %x, got %x",
common.Hash{0x2}, l.rootHash())
}
// Case 4: truly stale state root. This pins the other half of the
// contract — the boolean must actually signal not-found for an
// unknown state, otherwise a refactor that always returned
// ok=true would still pass cases 13.
_, err = tr.lookupAccount(common.HexToHash("0xa"), common.HexToHash("0xdeadbeef"))
if !errors.Is(err, errSnapshotStale) {
t.Errorf("lookupAccount(stale state): want errSnapshotStale, got %v", err)
}
_, err = tr.lookupStorage(
common.HexToHash("0xa"), common.HexToHash("0x1"),
common.HexToHash("0xdeadbeef"))
if !errors.Is(err, errSnapshotStale) {
t.Errorf("lookupStorage(stale state): want errSnapshotStale, got %v", err)
}
}

View file

@ -92,12 +92,16 @@ func newLookup(head layer, descendant func(state common.Hash, ancestor common.Ha
// stateID or is a descendant of it.
//
// If found, the account data corresponding to the supplied stateID resides
// in that layer. Otherwise, two scenarios are possible:
// in the layer identified by the returned hash (ok=true). Otherwise,
// (common.Hash{}, false) is returned to signal that the supplied stateID is
// stale.
//
// (a) the account remains unmodified from the current disk layer up to the state
// layer specified by the stateID: fallback to the disk layer for data retrieval,
// (b) or the layer specified by the stateID is stale: reject the data retrieval.
func (l *lookup) accountTip(accountHash common.Hash, stateID common.Hash, base common.Hash) common.Hash {
// Note the returned hash may itself be common.Hash{} when the disk layer's
// root is zero — as is the case for a fresh verkle/bintrie database whose
// empty trie hashes to EmptyVerkleHash. Callers must therefore consult the
// boolean rather than comparing the returned hash against common.Hash{}
// directly.
func (l *lookup) accountTip(accountHash common.Hash, stateID common.Hash, base common.Hash) (common.Hash, bool) {
// Traverse the mutation history from latest to oldest one. Several
// scenarios are possible:
//
@ -123,31 +127,26 @@ func (l *lookup) accountTip(accountHash common.Hash, stateID common.Hash, base c
// containing the modified data. Otherwise, the current state may be ahead
// of the requested one or belong to a different branch.
if list[i] == stateID || l.descendant(stateID, list[i]) {
return list[i]
return list[i], true
}
}
// No layer matching the stateID or its descendants was found. Use the
// current disk layer as a fallback.
if base == stateID || l.descendant(stateID, base) {
return base
return base, true
}
// The layer associated with 'stateID' is not the descendant of the current
// disk layer, it's already stale, return nothing.
return common.Hash{}
return common.Hash{}, false
}
// storageTip traverses the layer list associated with the given account and
// slot hash in reverse order to locate the first entry that either matches
// the specified stateID or is a descendant of it.
//
// If found, the storage data corresponding to the supplied stateID resides
// in that layer. Otherwise, two scenarios are possible:
//
// (a) the storage slot remains unmodified from the current disk layer up to
// the state layer specified by the stateID: fallback to the disk layer for
// data retrieval, (b) or the layer specified by the stateID is stale: reject
// the data retrieval.
func (l *lookup) storageTip(accountHash common.Hash, slotHash common.Hash, stateID common.Hash, base common.Hash) common.Hash {
// See accountTip for the returned-hash / ok convention — the same
// bintrie-zero-root caveat applies here.
func (l *lookup) storageTip(accountHash common.Hash, slotHash common.Hash, stateID common.Hash, base common.Hash) (common.Hash, bool) {
list := l.storages[storageKey(accountHash, slotHash)]
for i := len(list) - 1; i >= 0; i-- {
// If the current state matches the stateID, or the requested state is a
@ -155,17 +154,17 @@ func (l *lookup) storageTip(accountHash common.Hash, slotHash common.Hash, state
// containing the modified data. Otherwise, the current state may be ahead
// of the requested one or belong to a different branch.
if list[i] == stateID || l.descendant(stateID, list[i]) {
return list[i]
return list[i], true
}
}
// No layer matching the stateID or its descendants was found. Use the
// current disk layer as a fallback.
if base == stateID || l.descendant(stateID, base) {
return base
return base, true
}
// The layer associated with 'stateID' is not the descendant of the current
// disk layer, it's already stale, return nothing.
return common.Hash{}
return common.Hash{}, false
}
// addLayer traverses the state data retained in the specified diff layer and

View file

@ -77,10 +77,12 @@ var (
trienodeHistoryDataBytesMeter = metrics.NewRegisteredMeter("pathdb/history/trienode/bytes/data", nil)
trienodeHistoryIndexBytesMeter = metrics.NewRegisteredMeter("pathdb/history/trienode/bytes/index", nil)
stateIndexHistoryTimer = metrics.NewRegisteredResettingTimer("pathdb/history/state/index/time", nil)
stateUnindexHistoryTimer = metrics.NewRegisteredResettingTimer("pathdb/history/state/unindex/time", nil)
trienodeIndexHistoryTimer = metrics.NewRegisteredResettingTimer("pathdb/history/trienode/index/time", nil)
trienodeUnindexHistoryTimer = metrics.NewRegisteredResettingTimer("pathdb/history/trienode/unindex/time", nil)
stateIndexHistoryTimer = metrics.NewRegisteredResettingTimer("pathdb/history/state/index/time", nil)
stateUnindexHistoryTimer = metrics.NewRegisteredResettingTimer("pathdb/history/state/unindex/time", nil)
statePruneHistoryIndexTimer = metrics.NewRegisteredResettingTimer("pathdb/history/state/prune/time", nil)
trienodeIndexHistoryTimer = metrics.NewRegisteredResettingTimer("pathdb/history/trienode/index/time", nil)
trienodeUnindexHistoryTimer = metrics.NewRegisteredResettingTimer("pathdb/history/trienode/unindex/time", nil)
trienodePruneHistoryIndexTimer = metrics.NewRegisteredResettingTimer("pathdb/history/trienode/prune/time", nil)
lookupAddLayerTimer = metrics.NewRegisteredResettingTimer("pathdb/lookup/add/time", nil)
lookupRemoveLayerTimer = metrics.NewRegisteredResettingTimer("pathdb/lookup/remove/time", nil)

View file

@ -17,36 +17,15 @@
package pathdb
import (
"encoding/binary"
"errors"
"fmt"
"math"
"runtime"
"sync"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/trie"
)
// trieKV represents a trie key-value pair
type trieKV struct {
key common.Hash
value []byte
}
type (
// trieHasherFn is the interface of trie hasher which can be implemented
// by different trie algorithm.
trieHasherFn func(in chan trieKV, out chan common.Hash)
// leafCallbackFn is the callback invoked at the leaves of the trie,
// returns the subtrie root with the specified subtrie identifier.
leafCallbackFn func(accountHash, codeHash common.Hash, stat *generateStats) (common.Hash, error)
"github.com/ethereum/go-ethereum/triedb/internal"
)
// VerifyState traverses the flat states specified by the given state root and
@ -58,7 +37,7 @@ func (db *Database) VerifyState(root common.Hash) error {
}
defer acctIt.Release()
got, err := generateTrieRoot(acctIt, common.Hash{}, stackTrieHasher, func(accountHash, codeHash common.Hash, stat *generateStats) (common.Hash, error) {
got, err := internal.GenerateTrieRoot(nil, "", acctIt, common.Hash{}, stackTrieHasher, func(_ ethdb.KeyValueWriter, accountHash, codeHash common.Hash, stat *internal.GenerateStats) (common.Hash, error) {
// Migrate the code first, commit the contract code into the tmp db.
if codeHash != types.EmptyCodeHash {
code := rawdb.ReadCode(db.diskdb, codeHash)
@ -73,12 +52,12 @@ func (db *Database) VerifyState(root common.Hash) error {
}
defer storageIt.Release()
hash, err := generateTrieRoot(storageIt, accountHash, stackTrieHasher, nil, stat, false)
hash, err := internal.GenerateTrieRoot(nil, "", storageIt, accountHash, stackTrieHasher, nil, stat, false)
if err != nil {
return common.Hash{}, err
}
return hash, nil
}, newGenerateStats(), true)
}, internal.NewGenerateStats(), true)
if err != nil {
return err
@ -89,264 +68,10 @@ func (db *Database) VerifyState(root common.Hash) error {
return nil
}
// generateStats is a collection of statistics gathered by the trie generator
// for logging purposes.
type generateStats struct {
head common.Hash
start time.Time
accounts uint64 // Number of accounts done (including those being crawled)
slots uint64 // Number of storage slots done (including those being crawled)
slotsStart map[common.Hash]time.Time // Start time for account slot crawling
slotsHead map[common.Hash]common.Hash // Slot head for accounts being crawled
lock sync.RWMutex
}
// newGenerateStats creates a new generator stats.
func newGenerateStats() *generateStats {
return &generateStats{
slotsStart: make(map[common.Hash]time.Time),
slotsHead: make(map[common.Hash]common.Hash),
start: time.Now(),
}
}
// progressAccounts updates the generator stats for the account range.
func (stat *generateStats) progressAccounts(account common.Hash, done uint64) {
stat.lock.Lock()
defer stat.lock.Unlock()
stat.accounts += done
stat.head = account
}
// finishAccounts updates the generator stats for the finished account range.
func (stat *generateStats) finishAccounts(done uint64) {
stat.lock.Lock()
defer stat.lock.Unlock()
stat.accounts += done
}
// progressContract updates the generator stats for a specific in-progress contract.
func (stat *generateStats) progressContract(account common.Hash, slot common.Hash, done uint64) {
stat.lock.Lock()
defer stat.lock.Unlock()
stat.slots += done
stat.slotsHead[account] = slot
if _, ok := stat.slotsStart[account]; !ok {
stat.slotsStart[account] = time.Now()
}
}
// finishContract updates the generator stats for a specific just-finished contract.
func (stat *generateStats) finishContract(account common.Hash, done uint64) {
stat.lock.Lock()
defer stat.lock.Unlock()
stat.slots += done
delete(stat.slotsHead, account)
delete(stat.slotsStart, account)
}
// report prints the cumulative progress statistic smartly.
func (stat *generateStats) report() {
stat.lock.RLock()
defer stat.lock.RUnlock()
ctx := []interface{}{
"accounts", stat.accounts,
"slots", stat.slots,
"elapsed", common.PrettyDuration(time.Since(stat.start)),
}
if stat.accounts > 0 {
// If there's progress on the account trie, estimate the time to finish crawling it
if done := binary.BigEndian.Uint64(stat.head[:8]) / stat.accounts; done > 0 {
var (
left = (math.MaxUint64 - binary.BigEndian.Uint64(stat.head[:8])) / stat.accounts
eta = common.CalculateETA(done, left, time.Since(stat.start))
)
// If there are large contract crawls in progress, estimate their finish time
for acc, head := range stat.slotsHead {
start := stat.slotsStart[acc]
if done := binary.BigEndian.Uint64(head[:8]); done > 0 {
left := math.MaxUint64 - binary.BigEndian.Uint64(head[:8])
// Override the ETA if larger than the largest until now
if slotETA := common.CalculateETA(done, left, time.Since(start)); eta < slotETA {
eta = slotETA
}
}
}
ctx = append(ctx, []interface{}{
"eta", common.PrettyDuration(eta),
}...)
}
}
log.Info("Iterating state snapshot", ctx...)
}
// reportDone prints the last log when the whole generation is finished.
func (stat *generateStats) reportDone() {
stat.lock.RLock()
defer stat.lock.RUnlock()
var ctx []interface{}
ctx = append(ctx, []interface{}{"accounts", stat.accounts}...)
if stat.slots != 0 {
ctx = append(ctx, []interface{}{"slots", stat.slots}...)
}
ctx = append(ctx, []interface{}{"elapsed", common.PrettyDuration(time.Since(stat.start))}...)
log.Info("Iterated snapshot", ctx...)
}
// runReport periodically prints the progress information.
func runReport(stats *generateStats, stop chan bool) {
timer := time.NewTimer(0)
defer timer.Stop()
for {
select {
case <-timer.C:
stats.report()
timer.Reset(time.Second * 8)
case success := <-stop:
if success {
stats.reportDone()
}
return
}
}
}
// generateTrieRoot generates the trie hash based on the snapshot iterator.
// It can be used for generating account trie, storage trie or even the
// whole state which connects the accounts and the corresponding storages.
func generateTrieRoot(it Iterator, account common.Hash, generatorFn trieHasherFn, leafCallback leafCallbackFn, stats *generateStats, report bool) (common.Hash, error) {
var (
in = make(chan trieKV) // chan to pass leaves
out = make(chan common.Hash, 1) // chan to collect result
stoplog = make(chan bool, 1) // 1-size buffer, works when logging is not enabled
wg sync.WaitGroup
)
// Spin up a go-routine for trie hash re-generation
wg.Add(1)
go func() {
defer wg.Done()
generatorFn(in, out)
}()
// Spin up a go-routine for progress logging
if report && stats != nil {
wg.Add(1)
go func() {
defer wg.Done()
runReport(stats, stoplog)
}()
}
// Create a semaphore to assign tasks and collect results through. We'll pre-
// fill it with nils, thus using the same channel for both limiting concurrent
// processing and gathering results.
threads := runtime.NumCPU()
results := make(chan error, threads)
for i := 0; i < threads; i++ {
results <- nil // fill the semaphore
}
// stop is a helper function to shutdown the background threads
// and return the re-generated trie hash.
stop := func(fail error) (common.Hash, error) {
close(in)
result := <-out
for i := 0; i < threads; i++ {
if err := <-results; err != nil && fail == nil {
fail = err
}
}
stoplog <- fail == nil
wg.Wait()
return result, fail
}
var (
logged = time.Now()
processed = uint64(0)
leaf trieKV
)
// Start to feed leaves
for it.Next() {
if account == (common.Hash{}) {
var (
err error
fullData []byte
)
if leafCallback == nil {
fullData, err = types.FullAccountRLP(it.(AccountIterator).Account())
if err != nil {
return stop(err)
}
} else {
// Wait until the semaphore allows us to continue, aborting if
// a sub-task failed
if err := <-results; err != nil {
results <- nil // stop will drain the results, add a noop back for this error we just consumed
return stop(err)
}
// Fetch the next account and process it concurrently
account, err := types.FullAccount(it.(AccountIterator).Account())
if err != nil {
return stop(err)
}
go func(hash common.Hash) {
subroot, err := leafCallback(hash, common.BytesToHash(account.CodeHash), stats)
if err != nil {
results <- err
return
}
if account.Root != subroot {
results <- fmt.Errorf("invalid subroot(path %x), want %x, have %x", hash, account.Root, subroot)
return
}
results <- nil
}(it.Hash())
fullData, err = rlp.EncodeToBytes(account)
if err != nil {
return stop(err)
}
}
leaf = trieKV{it.Hash(), fullData}
} else {
leaf = trieKV{it.Hash(), common.CopyBytes(it.(StorageIterator).Slot())}
}
in <- leaf
// Accumulate the generation statistic if it's required.
processed++
if time.Since(logged) > 3*time.Second && stats != nil {
if account == (common.Hash{}) {
stats.progressAccounts(it.Hash(), processed)
} else {
stats.progressContract(account, it.Hash(), processed)
}
logged, processed = time.Now(), 0
}
}
// Commit the last part statistic.
if processed > 0 && stats != nil {
if account == (common.Hash{}) {
stats.finishAccounts(processed)
} else {
stats.finishContract(account, processed)
}
}
return stop(nil)
}
func stackTrieHasher(in chan trieKV, out chan common.Hash) {
func stackTrieHasher(_ ethdb.KeyValueWriter, _ string, _ common.Hash, in chan internal.TrieKV, out chan common.Hash) {
t := trie.NewStackTrie(nil)
for leaf := range in {
t.Update(leaf.key[:], leaf.value)
t.Update(leaf.Key[:], leaf.Value)
}
out <- t.Hash()
}