mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-05-25 01:09:28 +00:00
erge branch 'master' of https://github.com/0xjvn/go-ethereum into fix/rewind
This commit is contained in:
commit
8d623b9464
86 changed files with 5590 additions and 1440 deletions
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
408
cmd/geth/bintrie_convert.go
Normal 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
|
||||
}
|
||||
229
cmd/geth/bintrie_convert_test.go
Normal file
229
cmd/geth/bintrie_convert_test.go
Normal 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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()},
|
||||
|
|
|
|||
|
|
@ -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()...)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
435
core/state/database_iterator.go
Normal file
435
core/state/database_iterator.go
Normal 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
|
||||
}
|
||||
262
core/state/database_iterator_test.go
Normal file
262
core/state/database_iterator_test.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
36
core/vm/gascosts.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ type StateDB interface {
|
|||
Snapshot() int
|
||||
|
||||
AddLog(*types.Log)
|
||||
EmitLogsForBurnAccounts()
|
||||
LogsForBurnAccounts() []*types.Log
|
||||
AddPreimage(common.Hash, []byte)
|
||||
|
||||
Witness() *stateless.Witness
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
314
eth/protocols/snap/handler_test.go
Normal file
314
eth/protocols/snap/handler_test.go
Normal 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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
600
eth/protocols/snap/handlers.go
Normal file
600
eth/protocols/snap/handlers.go
Normal 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
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: ¶ms.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),
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
108
triedb/generate.go
Normal 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
178
triedb/generate_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
363
triedb/internal/conversion.go
Normal file
363
triedb/internal/conversion.go
Normal 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()
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
385
triedb/pathdb/history_index_pruner.go
Normal file
385
triedb/pathdb/history_index_pruner.go
Normal 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
|
||||
}
|
||||
355
triedb/pathdb/history_index_pruner_test.go
Normal file
355
triedb/pathdb/history_index_pruner_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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 1–3.
|
||||
_, 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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue