fix: merge conflicts

This commit is contained in:
0xjvn 2026-03-30 09:30:08 +05:30
commit b524bd3eb8
49 changed files with 1448 additions and 440 deletions

View file

@ -218,11 +218,15 @@ func (tc *conn) read(c net.PacketConn) v5wire.Packet {
if err := c.SetReadDeadline(time.Now().Add(waitTime)); err != nil { if err := c.SetReadDeadline(time.Now().Add(waitTime)); err != nil {
return &readError{err} return &readError{err}
} }
n, fromAddr, err := c.ReadFrom(buf) n, _, err := c.ReadFrom(buf)
if err != nil { if err != nil {
return &readError{err} return &readError{err}
} }
_, _, p, err := tc.codec.Decode(buf[:n], fromAddr.String()) // Always use tc.remoteAddr for session lookup. The actual source address of
// the packet may differ from tc.remoteAddr when the remote node is reachable
// via multiple networks (e.g. Docker bridge vs. overlay), but the codec's
// session cache is keyed by the address used during Encode.
_, _, p, err := tc.codec.Decode(buf[:n], tc.remoteAddr.String())
if err != nil { if err != nil {
return &readError{err} return &readError{err}
} }

View file

@ -731,13 +731,16 @@ func pruneHistory(ctx *cli.Context) error {
// Determine the prune point based on the history mode. // Determine the prune point based on the history mode.
genesisHash := chain.Genesis().Hash() genesisHash := chain.Genesis().Hash()
prunePoint := history.GetPrunePoint(genesisHash, mode) policy, err := history.NewPolicy(mode, genesisHash)
if prunePoint == nil { if err != nil {
return err
}
if policy.Target == nil {
return fmt.Errorf("prune point for %q not found for this network", mode.String()) return fmt.Errorf("prune point for %q not found for this network", mode.String())
} }
var ( var (
targetBlock = prunePoint.BlockNumber targetBlock = policy.Target.BlockNumber
targetBlockHash = prunePoint.BlockHash targetBlockHash = policy.Target.BlockHash
) )
// Check the current freezer tail to see if pruning is needed/possible. // Check the current freezer tail to see if pruning is needed/possible.

View file

@ -36,6 +36,7 @@ import (
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/trie"
"github.com/ethereum/go-ethereum/triedb"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -105,7 +106,9 @@ information about the specified address.
Usage: "Traverse the state with given root hash and perform quick verification", Usage: "Traverse the state with given root hash and perform quick verification",
ArgsUsage: "<root>", ArgsUsage: "<root>",
Action: traverseState, Action: traverseState,
Flags: slices.Concat(utils.NetworkFlags, utils.DatabaseFlags), Flags: slices.Concat([]cli.Flag{
utils.AccountFlag,
}, utils.NetworkFlags, utils.DatabaseFlags),
Description: ` Description: `
geth snapshot traverse-state <state-root> geth snapshot traverse-state <state-root>
will traverse the whole state from the given state root and will abort if any will traverse the whole state from the given state root and will abort if any
@ -113,6 +116,8 @@ referenced trie node or contract code is missing. This command can be used for
state integrity verification. The default checking target is the HEAD state. state integrity verification. The default checking target is the HEAD state.
It's also usable without snapshot enabled. It's also usable without snapshot enabled.
If --account is specified, only the storage trie of that account is traversed.
`, `,
}, },
{ {
@ -120,7 +125,9 @@ It's also usable without snapshot enabled.
Usage: "Traverse the state with given root hash and perform detailed verification", Usage: "Traverse the state with given root hash and perform detailed verification",
ArgsUsage: "<root>", ArgsUsage: "<root>",
Action: traverseRawState, Action: traverseRawState,
Flags: slices.Concat(utils.NetworkFlags, utils.DatabaseFlags), Flags: slices.Concat([]cli.Flag{
utils.AccountFlag,
}, utils.NetworkFlags, utils.DatabaseFlags),
Description: ` Description: `
geth snapshot traverse-rawstate <state-root> geth snapshot traverse-rawstate <state-root>
will traverse the whole state from the given root and will abort if any referenced will traverse the whole state from the given root and will abort if any referenced
@ -129,6 +136,8 @@ verification. The default checking target is the HEAD state. It's basically iden
to traverse-state, but the check granularity is smaller. to traverse-state, but the check granularity is smaller.
It's also usable without snapshot enabled. It's also usable without snapshot enabled.
If --account is specified, only the storage trie of that account is traversed.
`, `,
}, },
{ {
@ -272,6 +281,120 @@ func checkDanglingStorage(ctx *cli.Context) error {
return snapshot.CheckDanglingStorage(db) return snapshot.CheckDanglingStorage(db)
} }
// parseAccount parses the account flag value as either an address (20 bytes)
// or an account hash (32 bytes) and returns the hashed account key.
func parseAccount(input string) (common.Hash, error) {
switch len(input) {
case 40, 42: // address
return crypto.Keccak256Hash(common.HexToAddress(input).Bytes()), nil
case 64, 66: // hash
return common.HexToHash(input), nil
default:
return common.Hash{}, errors.New("malformed account address or hash")
}
}
// lookupAccount resolves the account from the state trie using the given
// account hash.
func lookupAccount(accountHash common.Hash, tr *trie.Trie) (*types.StateAccount, error) {
accData, err := tr.Get(accountHash.Bytes())
if err != nil {
return nil, fmt.Errorf("failed to get account %s: %w", accountHash, err)
}
if accData == nil {
return nil, fmt.Errorf("account not found: %s", accountHash)
}
var acc types.StateAccount
if err := rlp.DecodeBytes(accData, &acc); err != nil {
return nil, fmt.Errorf("invalid account data %s: %w", accountHash, err)
}
return &acc, nil
}
func traverseStorage(id *trie.ID, db *triedb.Database, report bool, detail bool) error {
tr, err := trie.NewStateTrie(id, db)
if err != nil {
log.Error("Failed to open storage trie", "account", id.Owner, "root", id.Root, "err", err)
return err
}
var (
slots int
nodes int
lastReport time.Time
start = time.Now()
)
it, err := tr.NodeIterator(nil)
if err != nil {
log.Error("Failed to open storage iterator", "account", id.Owner, "root", id.Root, "err", err)
return err
}
logger := log.Debug
if report {
logger = log.Info
}
logger("Start traversing storage trie", "account", id.Owner, "storageRoot", id.Root)
if !detail {
iter := trie.NewIterator(it)
for iter.Next() {
slots += 1
if time.Since(lastReport) > time.Second*8 {
logger("Traversing storage", "account", id.Owner, "slots", slots, "elapsed", common.PrettyDuration(time.Since(start)))
lastReport = time.Now()
}
}
if iter.Err != nil {
log.Error("Failed to traverse storage trie", "root", id.Root, "err", iter.Err)
return iter.Err
}
logger("Storage is complete", "account", id.Owner, "slots", slots, "elapsed", common.PrettyDuration(time.Since(start)))
} else {
reader, err := db.NodeReader(id.StateRoot)
if err != nil {
log.Error("Failed to open state reader", "err", err)
return err
}
var (
buffer = make([]byte, 32)
hasher = crypto.NewKeccakState()
)
for it.Next(true) {
nodes += 1
node := it.Hash()
// Check the presence for non-empty hash node(embedded node doesn't
// have their own hash).
if node != (common.Hash{}) {
blob, _ := reader.Node(id.Owner, it.Path(), node)
if len(blob) == 0 {
log.Error("Missing trie node(storage)", "hash", node)
return errors.New("missing storage")
}
hasher.Reset()
hasher.Write(blob)
hasher.Read(buffer)
if !bytes.Equal(buffer, node.Bytes()) {
log.Error("Invalid trie node(storage)", "hash", node.Hex(), "value", blob)
return errors.New("invalid storage node")
}
}
if it.Leaf() {
slots += 1
}
if time.Since(lastReport) > time.Second*8 {
logger("Traversing storage", "account", id.Owner, "nodes", nodes, "slots", slots, "elapsed", common.PrettyDuration(time.Since(start)))
lastReport = time.Now()
}
}
if err := it.Error(); err != nil {
log.Error("Failed to traverse storage trie", "root", id.Root, "err", err)
return err
}
logger("Storage is complete", "account", id.Owner, "nodes", nodes, "slots", slots, "elapsed", common.PrettyDuration(time.Since(start)))
}
return nil
}
// traverseState is a helper function used for pruning verification. // traverseState is a helper function used for pruning verification.
// Basically it just iterates the trie, ensure all nodes and associated // Basically it just iterates the trie, ensure all nodes and associated
// contract codes are present. // contract codes are present.
@ -309,6 +432,30 @@ func traverseState(ctx *cli.Context) error {
root = headBlock.Root() root = headBlock.Root()
log.Info("Start traversing the state", "root", root, "number", headBlock.NumberU64()) log.Info("Start traversing the state", "root", root, "number", headBlock.NumberU64())
} }
// If --account is specified, only traverse the storage trie of that account.
if accountStr := ctx.String(utils.AccountFlag.Name); accountStr != "" {
accountHash, err := parseAccount(accountStr)
if err != nil {
log.Error("Failed to parse account", "err", err)
return err
}
// Use raw trie since the account key is already hashed.
t, err := trie.New(trie.StateTrieID(root), triedb)
if err != nil {
log.Error("Failed to open state trie", "root", root, "err", err)
return err
}
acc, err := lookupAccount(accountHash, t)
if err != nil {
log.Error("Failed to look up account", "hash", accountHash, "err", err)
return err
}
if acc.Root == types.EmptyRootHash {
log.Info("Account has no storage", "hash", accountHash)
return nil
}
return traverseStorage(trie.StorageTrieID(root, accountHash, acc.Root), triedb, true, false)
}
t, err := trie.NewStateTrie(trie.StateTrieID(root), triedb) t, err := trie.NewStateTrie(trie.StateTrieID(root), triedb)
if err != nil { if err != nil {
log.Error("Failed to open trie", "root", root, "err", err) log.Error("Failed to open trie", "root", root, "err", err)
@ -335,30 +482,10 @@ func traverseState(ctx *cli.Context) error {
return err return err
} }
if acc.Root != types.EmptyRootHash { if acc.Root != types.EmptyRootHash {
id := trie.StorageTrieID(root, common.BytesToHash(accIter.Key), acc.Root) err := traverseStorage(trie.StorageTrieID(root, common.BytesToHash(accIter.Key), acc.Root), triedb, false, false)
storageTrie, err := trie.NewStateTrie(id, triedb)
if err != nil { if err != nil {
log.Error("Failed to open storage trie", "root", acc.Root, "err", err)
return err return err
} }
storageIt, err := storageTrie.NodeIterator(nil)
if err != nil {
log.Error("Failed to open storage iterator", "root", acc.Root, "err", err)
return err
}
storageIter := trie.NewIterator(storageIt)
for storageIter.Next() {
slots += 1
if time.Since(lastReport) > time.Second*8 {
log.Info("Traversing state", "accounts", accounts, "slots", slots, "codes", codes, "elapsed", common.PrettyDuration(time.Since(start)))
lastReport = time.Now()
}
}
if storageIter.Err != nil {
log.Error("Failed to traverse storage trie", "root", acc.Root, "err", storageIter.Err)
return storageIter.Err
}
} }
if !bytes.Equal(acc.CodeHash, types.EmptyCodeHash.Bytes()) { if !bytes.Equal(acc.CodeHash, types.EmptyCodeHash.Bytes()) {
if !rawdb.HasCode(chaindb, common.BytesToHash(acc.CodeHash)) { if !rawdb.HasCode(chaindb, common.BytesToHash(acc.CodeHash)) {
@ -418,6 +545,30 @@ func traverseRawState(ctx *cli.Context) error {
root = headBlock.Root() root = headBlock.Root()
log.Info("Start traversing the state", "root", root, "number", headBlock.NumberU64()) log.Info("Start traversing the state", "root", root, "number", headBlock.NumberU64())
} }
// If --account is specified, only traverse the storage trie of that account.
if accountStr := ctx.String(utils.AccountFlag.Name); accountStr != "" {
accountHash, err := parseAccount(accountStr)
if err != nil {
log.Error("Failed to parse account", "err", err)
return err
}
// Use raw trie since the account key is already hashed.
t, err := trie.New(trie.StateTrieID(root), triedb)
if err != nil {
log.Error("Failed to open state trie", "root", root, "err", err)
return err
}
acc, err := lookupAccount(accountHash, t)
if err != nil {
log.Error("Failed to look up account", "hash", accountHash, "err", err)
return err
}
if acc.Root == types.EmptyRootHash {
log.Info("Account has no storage", "hash", accountHash)
return nil
}
return traverseStorage(trie.StorageTrieID(root, accountHash, acc.Root), triedb, true, true)
}
t, err := trie.NewStateTrie(trie.StateTrieID(root), triedb) t, err := trie.NewStateTrie(trie.StateTrieID(root), triedb)
if err != nil { if err != nil {
log.Error("Failed to open trie", "root", root, "err", err) log.Error("Failed to open trie", "root", root, "err", err)
@ -473,50 +624,10 @@ func traverseRawState(ctx *cli.Context) error {
return errors.New("invalid account") return errors.New("invalid account")
} }
if acc.Root != types.EmptyRootHash { if acc.Root != types.EmptyRootHash {
id := trie.StorageTrieID(root, common.BytesToHash(accIter.LeafKey()), acc.Root) err := traverseStorage(trie.StorageTrieID(root, common.BytesToHash(accIter.LeafKey()), acc.Root), triedb, false, true)
storageTrie, err := trie.NewStateTrie(id, triedb)
if err != nil { if err != nil {
log.Error("Failed to open storage trie", "root", acc.Root, "err", err)
return errors.New("missing storage trie")
}
storageIter, err := storageTrie.NodeIterator(nil)
if err != nil {
log.Error("Failed to open storage iterator", "root", acc.Root, "err", err)
return err return err
} }
for storageIter.Next(true) {
nodes += 1
node := storageIter.Hash()
// Check the presence for non-empty hash node(embedded node doesn't
// have their own hash).
if node != (common.Hash{}) {
blob, _ := reader.Node(common.BytesToHash(accIter.LeafKey()), storageIter.Path(), node)
if len(blob) == 0 {
log.Error("Missing trie node(storage)", "hash", node)
return errors.New("missing storage")
}
hasher.Reset()
hasher.Write(blob)
hasher.Read(got)
if !bytes.Equal(got, node.Bytes()) {
log.Error("Invalid trie node(storage)", "hash", node.Hex(), "value", blob)
return errors.New("invalid storage node")
}
}
// Bump the counter if it's leaf node.
if storageIter.Leaf() {
slots += 1
}
if time.Since(lastReport) > time.Second*8 {
log.Info("Traversing state", "nodes", nodes, "accounts", accounts, "slots", slots, "codes", codes, "elapsed", common.PrettyDuration(time.Since(start)))
lastReport = time.Now()
}
}
if storageIter.Error() != nil {
log.Error("Failed to traverse storage trie", "root", acc.Root, "err", storageIter.Error())
return storageIter.Error()
}
} }
if !bytes.Equal(acc.CodeHash, types.EmptyCodeHash.Bytes()) { if !bytes.Equal(acc.CodeHash, types.EmptyCodeHash.Bytes()) {
if !rawdb.HasCode(chaindb, common.BytesToHash(acc.CodeHash)) { if !rawdb.HasCode(chaindb, common.BytesToHash(acc.CodeHash)) {

View file

@ -266,7 +266,7 @@ func ImportHistory(chain *core.BlockChain, db ethdb.Database, dir string, networ
len(checksums), len(entries)) len(checksums), len(entries))
} }
// Determine resume point from last successfully imported block // Determine resume point from last successfully imported block.
var resumeBlock uint64 var resumeBlock uint64
if tail := rawdb.ReadEraImportTail(db); tail != nil { if tail := rawdb.ReadEraImportTail(db); tail != nil {
resumeBlock = *tail resumeBlock = *tail
@ -278,9 +278,8 @@ func ImportHistory(chain *core.BlockChain, db ethdb.Database, dir string, networ
reported = time.Now() reported = time.Now()
imported = 0 imported = 0
h = sha256.New() h = sha256.New()
scratch = bytes.NewBuffer(nil) buf = bytes.NewBuffer(nil)
) )
for i, file := range entries { for i, file := range entries {
err := func() error { err := func() error {
path := filepath.Join(dir, file) path := filepath.Join(dir, file)
@ -291,33 +290,36 @@ func ImportHistory(chain *core.BlockChain, db ethdb.Database, dir string, networ
} }
defer f.Close() defer f.Close()
// Peek at era block range to see if we can skip entirely // Peek at era block range to see if we can skip entirely.
e, err := from(f) e, err := from(f)
if err != nil { if err != nil {
return fmt.Errorf("error opening era: %w", err) return fmt.Errorf("error opening era: %w", err)
} }
eraStart := e.Start() eraStart := e.Start()
eraEnd := eraStart + e.Count() - 1 eraEnd := eraStart + e.Count() - 1
e.Close()
// Skip era files fully behind resume point // Skip era files fully behind resume point.
if resumeBlock > 0 && eraEnd <= resumeBlock { if resumeBlock > 0 && eraEnd <= resumeBlock {
log.Debug("Skipping already imported Era file", "file", file, "eraEnd", eraEnd, "resumeBlock", resumeBlock) log.Debug("Skipping already imported Era file", "file", file, "eraEnd", eraEnd, "resumeBlock", resumeBlock)
return nil return nil
} }
// Validate against checksum file in directory.
if _, err := f.Seek(0, io.SeekStart); err != nil { if _, err := f.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("seek %s: %w", path, err) return fmt.Errorf("seek %s: %w", path, err)
} }
if _, err := io.Copy(h, f); err != nil { if _, err := io.Copy(h, f); err != nil {
return fmt.Errorf("checksum %s: %w", path, err) return fmt.Errorf("checksum %s: %w", path, err)
} }
got := common.BytesToHash(h.Sum(scratch.Bytes()[:])).Hex() got := common.BytesToHash(h.Sum(buf.Bytes()[:])).Hex()
want := checksums[i]
h.Reset() h.Reset()
scratch.Reset() buf.Reset()
if got != want { if got != checksums[i] {
return fmt.Errorf("%s checksum mismatch: have %s want %s", file, got, want) return fmt.Errorf("%s checksum mismatch: have %s want %s", file, got, checksums[i])
} }
// Import all block data from Era1.
if _, err := f.Seek(0, io.SeekStart); err != nil { if _, err := f.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("seek %s: %w", path, err) return fmt.Errorf("seek %s: %w", path, err)
} }
@ -325,11 +327,42 @@ func ImportHistory(chain *core.BlockChain, db ethdb.Database, dir string, networ
if err != nil { if err != nil {
return fmt.Errorf("error opening era: %w", err) return fmt.Errorf("error opening era: %w", err)
} }
defer e.Close()
it, err := e.Iterator() it, err := e.Iterator()
if err != nil { if err != nil {
return fmt.Errorf("error creating iterator: %w", err) return fmt.Errorf("error creating iterator: %w", err)
} }
var (
blocks = make([]*types.Block, 0, importBatchSize)
receiptsList = make([]types.Receipts, 0, importBatchSize)
flush = func() error {
if len(blocks) == 0 {
return nil
}
enc := types.EncodeBlockReceiptLists(receiptsList)
if _, err := chain.InsertReceiptChain(blocks, enc, math.MaxUint64); err != nil {
return fmt.Errorf("error inserting blocks %d-%d: %w",
blocks[0].NumberU64(), blocks[len(blocks)-1].NumberU64(), err)
}
// Track the last successfully imported block for resume support.
lastBlock := blocks[len(blocks)-1].NumberU64()
rawdb.WriteEraImportTail(db, lastBlock)
resumeBlock = lastBlock
imported += len(blocks)
if time.Since(reported) >= 8*time.Second {
head := blocks[len(blocks)-1].NumberU64()
log.Info("Importing Era files", "head", head, "imported", imported,
"elapsed", common.PrettyDuration(time.Since(start)))
imported = 0
reported = time.Now()
}
blocks = blocks[:0]
receiptsList = receiptsList[:0]
return nil
}
)
for it.Next() { for it.Next() {
block, err := it.Block() block, err := it.Block()
if err != nil { if err != nil {
@ -338,7 +371,7 @@ func ImportHistory(chain *core.BlockChain, db ethdb.Database, dir string, networ
if block.Number().BitLen() == 0 { if block.Number().BitLen() == 0 {
continue // skip genesis continue // skip genesis
} }
// Skip blocks already imported (mid-epoch resume) // Skip blocks already imported (mid-era resume).
if resumeBlock > 0 && block.Number().Uint64() <= resumeBlock { if resumeBlock > 0 && block.Number().Uint64() <= resumeBlock {
continue continue
} }
@ -346,25 +379,18 @@ func ImportHistory(chain *core.BlockChain, db ethdb.Database, dir string, networ
if err != nil { if err != nil {
return fmt.Errorf("error reading receipts %d: %w", it.Number(), err) return fmt.Errorf("error reading receipts %d: %w", it.Number(), err)
} }
enc := types.EncodeBlockReceiptLists([]types.Receipts{receipts}) blocks = append(blocks, block)
if _, err := chain.InsertReceiptChain([]*types.Block{block}, enc, math.MaxUint64); err != nil { receiptsList = append(receiptsList, receipts)
return fmt.Errorf("error inserting body %d: %w", it.Number(), err) if len(blocks) == importBatchSize {
} if err := flush(); err != nil {
rawdb.WriteEraImportTail(db, block.Number().Uint64()) return err
resumeBlock = block.Number().Uint64() }
imported++
if time.Since(reported) >= 8*time.Second {
log.Info("Importing Era files", "head", it.Number(), "imported", imported,
"elapsed", common.PrettyDuration(time.Since(start)))
imported = 0
reported = time.Now()
} }
} }
if err := it.Error(); err != nil { if err := it.Error(); err != nil {
return err return err
} }
return nil return flush()
}() }()
if err != nil { if err != nil {
return err return err

View file

@ -218,6 +218,10 @@ var (
Usage: "Max number of elements (0 = no limit)", Usage: "Max number of elements (0 = no limit)",
Value: 0, Value: 0,
} }
AccountFlag = &cli.StringFlag{
Name: "account",
Usage: "Specifies the account address or hash to traverse a single storage trie",
}
OutputFileFlag = &cli.StringFlag{ OutputFileFlag = &cli.StringFlag{
Name: "output", Name: "output",
Usage: "Writes the result in json to the output", Usage: "Writes the result in json to the output",
@ -1580,7 +1584,9 @@ func setOpenTelemetry(ctx *cli.Context, cfg *node.Config) {
if ctx.IsSet(RPCTelemetryTagsFlag.Name) { if ctx.IsSet(RPCTelemetryTagsFlag.Name) {
tcfg.Tags = ctx.String(RPCTelemetryTagsFlag.Name) tcfg.Tags = ctx.String(RPCTelemetryTagsFlag.Name)
} }
tcfg.SampleRatio = ctx.Float64(RPCTelemetrySampleRatioFlag.Name) if ctx.IsSet(RPCTelemetrySampleRatioFlag.Name) {
tcfg.SampleRatio = ctx.Float64(RPCTelemetrySampleRatioFlag.Name)
}
if tcfg.Endpoint != "" && !tcfg.Enabled { if tcfg.Endpoint != "" && !tcfg.Enabled {
log.Warn(fmt.Sprintf("OpenTelemetry endpoint configured but telemetry is not enabled, use --%s to enable.", RPCTelemetryFlag.Name)) log.Warn(fmt.Sprintf("OpenTelemetry endpoint configured but telemetry is not enabled, use --%s to enable.", RPCTelemetryFlag.Name))

View file

@ -155,7 +155,9 @@ func testConfigFromCLI(ctx *cli.Context) (cfg testConfig) {
} }
cfg.historyPruneBlock = new(uint64) cfg.historyPruneBlock = new(uint64)
*cfg.historyPruneBlock = history.PrunePoints[params.MainnetGenesisHash].BlockNumber if p, err := history.NewPolicy(history.KeepPostMerge, params.MainnetGenesisHash); err == nil {
*cfg.historyPruneBlock = p.Target.BlockNumber
}
case ctx.Bool(testSepoliaFlag.Name): case ctx.Bool(testSepoliaFlag.Name):
cfg.fsys = builtinTestFiles cfg.fsys = builtinTestFiles
if ctx.IsSet(filterQueryFileFlag.Name) { if ctx.IsSet(filterQueryFileFlag.Name) {
@ -180,7 +182,9 @@ func testConfigFromCLI(ctx *cli.Context) (cfg testConfig) {
} }
cfg.historyPruneBlock = new(uint64) cfg.historyPruneBlock = new(uint64)
*cfg.historyPruneBlock = history.PrunePoints[params.SepoliaGenesisHash].BlockNumber if p, err := history.NewPolicy(history.KeepPostMerge, params.SepoliaGenesisHash); err == nil {
*cfg.historyPruneBlock = p.Target.BlockNumber
}
default: default:
cfg.fsys = os.DirFS(".") cfg.fsys = os.DirFS(".")
cfg.filterQueryFile = ctx.String(filterQueryFileFlag.Name) cfg.filterQueryFile = ctx.String(filterQueryFileFlag.Name)

View file

@ -194,9 +194,8 @@ type BlockChainConfig struct {
SnapshotNoBuild bool // Whether the background generation is allowed SnapshotNoBuild bool // Whether the background generation is allowed
SnapshotWait bool // Wait for snapshot construction on startup. TODO(karalabe): This is a dirty hack for testing, nuke it SnapshotWait bool // Wait for snapshot construction on startup. TODO(karalabe): This is a dirty hack for testing, nuke it
// This defines the cutoff block for history expiry. // HistoryPolicy defines the chain history pruning intent.
// Blocks before this number may be unavailable in the chain database. HistoryPolicy history.HistoryPolicy
ChainHistoryMode history.HistoryMode
// Misc options // Misc options
NoPrefetch bool // Whether to disable heuristic state prefetching when processing blocks NoPrefetch bool // Whether to disable heuristic state prefetching when processing blocks
@ -227,13 +226,13 @@ type BlockChainConfig struct {
// Note the returned object is safe to modify! // Note the returned object is safe to modify!
func DefaultConfig() *BlockChainConfig { func DefaultConfig() *BlockChainConfig {
return &BlockChainConfig{ return &BlockChainConfig{
TrieCleanLimit: 256, TrieCleanLimit: 256,
TrieDirtyLimit: 256, TrieDirtyLimit: 256,
TrieTimeLimit: 5 * time.Minute, TrieTimeLimit: 5 * time.Minute,
StateScheme: rawdb.HashScheme, StateScheme: rawdb.HashScheme,
SnapshotLimit: 256, SnapshotLimit: 256,
SnapshotWait: true, SnapshotWait: true,
ChainHistoryMode: history.KeepAll, HistoryPolicy: history.HistoryPolicy{Mode: history.KeepAll},
// Transaction indexing is disabled by default. // Transaction indexing is disabled by default.
// This is appropriate for most unit tests. // This is appropriate for most unit tests.
TxLookupLimit: -1, TxLookupLimit: -1,
@ -715,82 +714,44 @@ func (bc *BlockChain) loadLastState() error {
// initializeHistoryPruning sets bc.historyPrunePoint. // initializeHistoryPruning sets bc.historyPrunePoint.
func (bc *BlockChain) initializeHistoryPruning(latest uint64) error { func (bc *BlockChain) initializeHistoryPruning(latest uint64) error {
var ( freezerTail, _ := bc.db.Tail()
freezerTail, _ = bc.db.Tail() policy := bc.cfg.HistoryPolicy
genesisHash = bc.genesisBlock.Hash()
mergePoint = history.MergePrunePoints[genesisHash]
praguePoint = history.PraguePrunePoints[genesisHash]
)
switch bc.cfg.ChainHistoryMode {
case history.KeepAll:
if freezerTail == 0 {
return nil
}
// The database was pruned somehow, so we need to figure out if it's a known
// configuration or an error.
if mergePoint != nil && freezerTail == mergePoint.BlockNumber {
bc.historyPrunePoint.Store(mergePoint)
return nil
}
if praguePoint != nil && freezerTail == praguePoint.BlockNumber {
bc.historyPrunePoint.Store(praguePoint)
return nil
}
log.Error("Chain history database is pruned with unknown configuration", "tail", freezerTail)
return errors.New("unexpected database tail")
case history.KeepPostMerge: switch policy.Mode {
if mergePoint == nil { case history.KeepAll:
return errors.New("history pruning requested for unknown network") if freezerTail > 0 {
// Database was pruned externally. Record the actual state.
log.Warn("Chain history database is pruned", "tail", freezerTail, "mode", policy.Mode)
bc.historyPrunePoint.Store(&history.PrunePoint{
BlockNumber: freezerTail,
BlockHash: bc.GetCanonicalHash(freezerTail),
})
} }
if freezerTail == 0 && latest != 0 {
log.Error(fmt.Sprintf("Chain history mode is configured as %q, but database is not pruned.", bc.cfg.ChainHistoryMode.String()))
log.Error("Run 'geth prune-history --history.chain postmerge' to prune pre-merge history.")
return errors.New("history pruning requested via configuration")
}
// Check if DB is pruned further than requested (to Prague).
if praguePoint != nil && freezerTail == praguePoint.BlockNumber {
log.Error("Chain history database is pruned to Prague block, but postmerge mode was requested.")
log.Error("History cannot be unpruned. To restore history, use 'geth import-history'.")
log.Error("If you intended to keep post-Prague history, use '--history.chain postprague' instead.")
return errors.New("database pruned beyond requested history mode")
}
if freezerTail > 0 && freezerTail != mergePoint.BlockNumber {
return errors.New("chain history database pruned to unknown block")
}
bc.historyPrunePoint.Store(mergePoint)
return nil return nil
case history.KeepPostPrague: case history.KeepPostMerge, history.KeepPostPrague:
if praguePoint == nil { target := policy.Target
return errors.New("history pruning requested for unknown network") // Already at the target.
} if freezerTail == target.BlockNumber {
// Check if already at the prague prune point. bc.historyPrunePoint.Store(target)
if freezerTail == praguePoint.BlockNumber {
bc.historyPrunePoint.Store(praguePoint)
return nil return nil
} }
// Check if database needs pruning. // Database is pruned beyond the target.
if latest != 0 { if freezerTail > target.BlockNumber {
if freezerTail == 0 { return fmt.Errorf("database pruned beyond requested history (tail=%d, target=%d)", freezerTail, target.BlockNumber)
log.Error(fmt.Sprintf("Chain history mode is configured as %q, but database is not pruned.", bc.cfg.ChainHistoryMode.String()))
log.Error("Run 'geth prune-history --history.chain postprague' to prune pre-Prague history.")
return errors.New("history pruning requested via configuration")
}
if mergePoint != nil && freezerTail == mergePoint.BlockNumber {
log.Error(fmt.Sprintf("Chain history mode is configured as %q, but database is only pruned to merge block.", bc.cfg.ChainHistoryMode.String()))
log.Error("Run 'geth prune-history --history.chain postprague' to prune pre-Prague history.")
return errors.New("history pruning requested via configuration")
}
log.Error("Chain history database is pruned to unknown block", "tail", freezerTail)
return errors.New("unexpected database tail")
} }
// Fresh database (latest == 0), will sync from prague point. // Database needs pruning (freezerTail < target).
bc.historyPrunePoint.Store(praguePoint) if latest != 0 {
log.Error(fmt.Sprintf("Chain history mode is configured as %q, but database is not pruned to the target block.", policy.Mode.String()))
log.Error(fmt.Sprintf("Run 'geth prune-history --history.chain %s' to prune history.", policy.Mode.String()))
return errors.New("history pruning required")
}
// Fresh database (latest == 0), will sync from target point.
bc.historyPrunePoint.Store(target)
return nil return nil
default: default:
return fmt.Errorf("invalid history mode: %d", bc.cfg.ChainHistoryMode) return fmt.Errorf("invalid history mode: %d", policy.Mode)
} }
} }
@ -2209,24 +2170,18 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash,
// If we are past Byzantium, enable prefetching to pull in trie node paths // If we are past Byzantium, enable prefetching to pull in trie node paths
// while processing transactions. Before Byzantium the prefetcher is mostly // while processing transactions. Before Byzantium the prefetcher is mostly
// useless due to the intermediate root hashing after each transaction. // useless due to the intermediate root hashing after each transaction.
var ( var witness *stateless.Witness
witness *stateless.Witness
witnessStats *stateless.WitnessStats
)
if bc.chainConfig.IsByzantium(block.Number()) { if bc.chainConfig.IsByzantium(block.Number()) {
// Generate witnesses either if we're self-testing, or if it's the // Generate witnesses either if we're self-testing, or if it's the
// only block being inserted. A bit crude, but witnesses are huge, // only block being inserted. A bit crude, but witnesses are huge,
// so we refuse to make an entire chain of them. // so we refuse to make an entire chain of them.
if config.StatelessSelfValidation || config.MakeWitness { if config.StatelessSelfValidation || config.MakeWitness {
witness, err = stateless.NewWitness(block.Header(), bc) witness, err = stateless.NewWitness(block.Header(), bc, config.EnableWitnessStats)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if config.EnableWitnessStats {
witnessStats = stateless.NewWitnessStats()
}
} }
statedb.StartPrefetcher("chain", witness, witnessStats) statedb.StartPrefetcher("chain", witness)
defer statedb.StopPrefetcher() defer statedb.StopPrefetcher()
} }
@ -2345,8 +2300,8 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash,
stats.BlockWrite = time.Since(wstart) - max(statedb.AccountCommits, statedb.StorageCommits) /* concurrent */ - statedb.DatabaseCommits stats.BlockWrite = time.Since(wstart) - max(statedb.AccountCommits, statedb.StorageCommits) /* concurrent */ - statedb.DatabaseCommits
} }
// Report the collected witness statistics // Report the collected witness statistics
if witnessStats != nil { if witness != nil {
witnessStats.ReportMetrics(block.NumberU64()) witness.ReportMetrics(block.NumberU64())
} }
elapsed := time.Since(startTime) + 1 // prevent zero division elapsed := time.Since(startTime) + 1 // prevent zero division
stats.TotalTime = elapsed stats.TotalTime = elapsed

View file

@ -36,7 +36,6 @@ import (
"github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/consensus/beacon" "github.com/ethereum/go-ethereum/consensus/beacon"
"github.com/ethereum/go-ethereum/consensus/ethash" "github.com/ethereum/go-ethereum/consensus/ethash"
"github.com/ethereum/go-ethereum/core/history"
"github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
@ -4337,26 +4336,13 @@ func TestInsertChainWithCutoff(t *testing.T) {
func testInsertChainWithCutoff(t *testing.T, cutoff uint64, ancientLimit uint64, genesis *Genesis, blocks []*types.Block, receipts []types.Receipts) { func testInsertChainWithCutoff(t *testing.T, cutoff uint64, ancientLimit uint64, genesis *Genesis, blocks []*types.Block, receipts []types.Receipts) {
// log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelDebug, true))) // log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelDebug, true)))
// Add a known pruning point for the duration of the test.
ghash := genesis.ToBlock().Hash() ghash := genesis.ToBlock().Hash()
cutoffBlock := blocks[cutoff-1] cutoffBlock := blocks[cutoff-1]
history.PrunePoints[ghash] = &history.PrunePoint{
BlockNumber: cutoffBlock.NumberU64(),
BlockHash: cutoffBlock.Hash(),
}
defer func() {
delete(history.PrunePoints, ghash)
}()
// Enable pruning in cache config.
config := DefaultConfig().WithStateScheme(rawdb.PathScheme)
config.ChainHistoryMode = history.KeepPostMerge
db, _ := rawdb.Open(rawdb.NewMemoryDatabase(), rawdb.OpenOptions{}) db, _ := rawdb.Open(rawdb.NewMemoryDatabase(), rawdb.OpenOptions{})
defer db.Close() defer db.Close()
options := DefaultConfig().WithStateScheme(rawdb.PathScheme) chain, _ := NewBlockChain(db, genesis, beacon.New(ethash.NewFaker()), DefaultConfig().WithStateScheme(rawdb.PathScheme))
chain, _ := NewBlockChain(db, genesis, beacon.New(ethash.NewFaker()), options)
defer chain.Stop() defer chain.Stop()
var ( var (

View file

@ -66,10 +66,6 @@ var (
// have enough funds for transfer(topmost call only). // have enough funds for transfer(topmost call only).
ErrInsufficientFundsForTransfer = errors.New("insufficient funds for transfer") ErrInsufficientFundsForTransfer = errors.New("insufficient funds for transfer")
// ErrMaxInitCodeSizeExceeded is returned if creation transaction provides the init code bigger
// than init code size limit.
ErrMaxInitCodeSizeExceeded = errors.New("max initcode size exceeded")
// ErrInsufficientBalanceWitness is returned if the transaction sender has enough // ErrInsufficientBalanceWitness is returned if the transaction sender has enough
// funds to cover the transfer, but not enough to pay for witness access/modification // funds to cover the transfer, but not enough to pay for witness access/modification
// costs for the transaction // costs for the transaction

View file

@ -0,0 +1,169 @@
// 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 core
import (
"encoding/binary"
"math/big"
"reflect"
"testing"
"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/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/params"
)
var ethTransferTestCode = common.FromHex("6080604052600436106100345760003560e01c8063574ffc311461003957806366e41cb714610090578063f8a8fd6d1461009a575b600080fd5b34801561004557600080fd5b5061004e6100a4565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100986100ac565b005b6100a26100f5565b005b63deadbeef81565b7f38e80b5c85ba49b7280ccc8f22548faa62ae30d5a008a1b168fba5f47f5d1ee560405160405180910390a1631234567873ffffffffffffffffffffffffffffffffffffffff16ff5b7f24ec1d3ff24c2f6ff210738839dbc339cd45a5294d85c79361016243157aae7b60405160405180910390a163deadbeef73ffffffffffffffffffffffffffffffffffffffff166002348161014657fe5b046040516024016040516020818303038152906040527f66e41cb7000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff83818316178352505050506040518082805190602001908083835b602083106101fd57805182526020820191506020810190506020830392506101da565b6001836020036101000a03801982511681845116808217855250505050505090500191505060006040518083038185875af1925050503d806000811461025f576040519150601f19603f3d011682016040523d82523d6000602084013e610264565b606091505b50505056fea265627a7a723158202cce817a434785d8560c200762f972d453ccd30694481be7545f9035a512826364736f6c63430005100032")
/*
pragma solidity >=0.4.22 <0.6.0;
contract TestLogs {
address public constant target_contract = 0x00000000000000000000000000000000DeaDBeef;
address payable constant selfdestruct_addr = 0x0000000000000000000000000000000012345678;
event Response(bool success, bytes data);
event TestEvent();
event TestEvent2();
function test() public payable {
emit TestEvent();
target_contract.call.value(msg.value/2)(abi.encodeWithSignature("test2()"));
}
function test2() public payable {
emit TestEvent2();
selfdestruct(selfdestruct_addr);
}
}
*/
// TestEthTransferLogs tests EIP-7708 ETH transfer log output by simulating a
// scenario including transaction, CALL and SELFDESTRUCT value transfers, and
// also "ordinary" logs emitted. The same scenario is also tested with no value
// transferred.
func TestEthTransferLogs(t *testing.T) {
testEthTransferLogs(t, 1_000_000_000)
testEthTransferLogs(t, 0)
}
func testEthTransferLogs(t *testing.T, value uint64) {
var (
key1, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
addr1 = crypto.PubkeyToAddress(key1.PublicKey)
addr2 = common.HexToAddress("cafebabe") // caller
addr3 = common.HexToAddress("deadbeef") // callee
addr4 = common.HexToAddress("12345678") // selfdestruct target
testEvent = crypto.Keccak256Hash([]byte("TestEvent()"))
testEvent2 = crypto.Keccak256Hash([]byte("TestEvent2()"))
config = *params.MergedTestChainConfig
signer = types.LatestSigner(&config)
engine = beacon.New(ethash.NewFaker())
)
//TODO remove this hacky config initialization when final Amsterdam config is available
config.AmsterdamTime = new(uint64)
blobConfig := *config.BlobScheduleConfig
blobConfig.Amsterdam = blobConfig.Osaka
config.BlobScheduleConfig = &blobConfig
gspec := &Genesis{
Config: &config,
Alloc: types.GenesisAlloc{
addr1: {Balance: newGwei(1000000000)},
addr2: {Code: ethTransferTestCode},
addr3: {Code: ethTransferTestCode},
},
}
_, blocks, receipts := GenerateChainWithGenesis(gspec, engine, 1, func(i int, b *BlockGen) {
tx := types.MustSignNewTx(key1, signer, &types.DynamicFeeTx{
ChainID: gspec.Config.ChainID,
Nonce: 0,
To: &addr2,
Gas: 500_000,
GasFeeCap: newGwei(5),
GasTipCap: newGwei(5),
Value: big.NewInt(int64(value)),
Data: common.FromHex("f8a8fd6d"),
})
b.AddTx(tx)
})
blockHash := blocks[0].Hash()
txHash := blocks[0].Transactions()[0].Hash()
addr2hash := func(addr common.Address) (hash common.Hash) {
copy(hash[12:], addr[:])
return
}
u256 := func(amount uint64) []byte {
data := make([]byte, 32)
binary.BigEndian.PutUint64(data[24:], amount)
return data
}
var expLogs = []*types.Log{
{
Address: params.SystemAddress,
Topics: []common.Hash{params.EthTransferLogEvent, addr2hash(addr1), addr2hash(addr2)},
Data: u256(value),
},
{
Address: addr2,
Topics: []common.Hash{testEvent},
Data: nil,
},
{
Address: params.SystemAddress,
Topics: []common.Hash{params.EthTransferLogEvent, addr2hash(addr2), addr2hash(addr3)},
Data: u256(value / 2),
},
{
Address: addr3,
Topics: []common.Hash{testEvent2},
Data: nil,
},
{
Address: params.SystemAddress,
Topics: []common.Hash{params.EthTransferLogEvent, addr2hash(addr3), addr2hash(addr4)},
Data: u256(value / 2),
},
}
if value == 0 {
// no ETH transfer logs expected with zero value
expLogs = []*types.Log{expLogs[1], expLogs[3]}
}
for i, log := range expLogs {
log.BlockNumber = 1
log.BlockHash = blockHash
log.BlockTimestamp = 10
log.TxIndex = 0
log.TxHash = txHash
log.Index = uint(i)
}
if len(expLogs) != len(receipts[0][0].Logs) {
t.Fatalf("Incorrect number of logs (expected: %d, got: %d)", len(expLogs), len(receipts[0][0].Logs))
}
for i, log := range receipts[0][0].Logs {
if !reflect.DeepEqual(expLogs[i], log) {
t.Fatalf("Incorrect log at index %d (expected: %v, got: %v)", i, expLogs[i], log)
}
}
}

View file

@ -25,6 +25,7 @@ import (
"github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256" "github.com/holiman/uint256"
) )
@ -138,7 +139,10 @@ func CanTransfer(db vm.StateDB, addr common.Address, amount *uint256.Int) bool {
} }
// Transfer subtracts amount from sender and adds amount to recipient using the given Db // Transfer subtracts amount from sender and adds amount to recipient using the given Db
func Transfer(db vm.StateDB, sender, recipient common.Address, amount *uint256.Int) { func Transfer(db vm.StateDB, sender, recipient common.Address, amount *uint256.Int, rules *params.Rules) {
db.SubBalance(sender, amount, tracing.BalanceChangeTransfer) db.SubBalance(sender, amount, tracing.BalanceChangeTransfer)
db.AddBalance(recipient, amount, tracing.BalanceChangeTransfer) db.AddBalance(recipient, amount, tracing.BalanceChangeTransfer)
if rules.IsAmsterdam && !amount.IsZero() && sender != recipient {
db.AddLog(types.EthTransferLog(sender, recipient, amount))
}
} }

View file

@ -77,57 +77,62 @@ func (m *HistoryMode) UnmarshalText(text []byte) error {
return nil return nil
} }
// PrunePoint identifies a specific block for history pruning.
type PrunePoint struct { type PrunePoint struct {
BlockNumber uint64 BlockNumber uint64
BlockHash common.Hash BlockHash common.Hash
} }
// MergePrunePoints contains the pre-defined history pruning cutoff blocks for known networks. // staticPrunePoints contains the pre-defined history pruning cutoff blocks for
// They point to the first post-merge block. Any pruning should truncate *up to* but excluding // known networks, keyed by history mode and genesis hash. They point to the first
// the given block. // block after the respective fork. Any pruning should truncate *up to* but
var MergePrunePoints = map[common.Hash]*PrunePoint{ // excluding the given block.
// mainnet var staticPrunePoints = map[HistoryMode]map[common.Hash]*PrunePoint{
params.MainnetGenesisHash: { KeepPostMerge: {
BlockNumber: 15537393, params.MainnetGenesisHash: {
BlockHash: common.HexToHash("0x55b11b918355b1ef9c5db810302ebad0bf2544255b530cdce90674d5887bb286"), BlockNumber: 15537393,
BlockHash: common.HexToHash("0x55b11b918355b1ef9c5db810302ebad0bf2544255b530cdce90674d5887bb286"),
},
params.SepoliaGenesisHash: {
BlockNumber: 1450409,
BlockHash: common.HexToHash("0x229f6b18ca1552f1d5146deceb5387333f40dc6275aebee3f2c5c4ece07d02db"),
},
}, },
// sepolia KeepPostPrague: {
params.SepoliaGenesisHash: { params.MainnetGenesisHash: {
BlockNumber: 1450409, BlockNumber: 22431084,
BlockHash: common.HexToHash("0x229f6b18ca1552f1d5146deceb5387333f40dc6275aebee3f2c5c4ece07d02db"), BlockHash: common.HexToHash("0x50c8cab760b2948349c590461b166773c45d8f4858cccf5a43025ab2960152e8"),
},
params.SepoliaGenesisHash: {
BlockNumber: 7836331,
BlockHash: common.HexToHash("0xe6571beb68bf24dbd8a6ba354518996920c55a3f8d8fdca423e391b8ad071f22"),
},
}, },
} }
// PraguePrunePoints contains the pre-defined history pruning cutoff blocks for the Prague // HistoryPolicy describes the configured history pruning strategy. It captures
// (Pectra) upgrade. They point to the first post-Prague block. Any pruning should truncate // user intent as opposed to the actual DB state.
// *up to* but excluding the given block. type HistoryPolicy struct {
var PraguePrunePoints = map[common.Hash]*PrunePoint{ Mode HistoryMode
// mainnet - first Prague block (May 7, 2025) // Static prune point for PostMerge/PostPrague, nil otherwise.
params.MainnetGenesisHash: { Target *PrunePoint
BlockNumber: 22431084,
BlockHash: common.HexToHash("0x50c8cab760b2948349c590461b166773c45d8f4858cccf5a43025ab2960152e8"),
},
// sepolia - first Prague block (March 5, 2025)
params.SepoliaGenesisHash: {
BlockNumber: 7836331,
BlockHash: common.HexToHash("0xe6571beb68bf24dbd8a6ba354518996920c55a3f8d8fdca423e391b8ad071f22"),
},
} }
// PrunePoints is an alias for MergePrunePoints for backward compatibility. // NewPolicy constructs a HistoryPolicy from the given mode and genesis hash.
// Deprecated: Use GetPrunePoint or MergePrunePoints directly. func NewPolicy(mode HistoryMode, genesisHash common.Hash) (HistoryPolicy, error) {
var PrunePoints = MergePrunePoints
// GetPrunePoint returns the prune point for the given genesis hash and history mode.
// Returns nil if no prune point is defined for the given combination.
func GetPrunePoint(genesisHash common.Hash, mode HistoryMode) *PrunePoint {
switch mode { switch mode {
case KeepPostMerge: case KeepAll:
return MergePrunePoints[genesisHash] return HistoryPolicy{Mode: KeepAll}, nil
case KeepPostPrague:
return PraguePrunePoints[genesisHash] case KeepPostMerge, KeepPostPrague:
point := staticPrunePoints[mode][genesisHash]
if point == nil {
return HistoryPolicy{}, fmt.Errorf("%s history pruning not available for network %s", mode, genesisHash.Hex())
}
return HistoryPolicy{Mode: mode, Target: point}, nil
default: default:
return nil return HistoryPolicy{}, fmt.Errorf("invalid history mode: %d", mode)
} }
} }

View file

@ -0,0 +1,58 @@
// 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 history
import (
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/params"
)
func TestNewPolicy(t *testing.T) {
// KeepAll: no target.
p, err := NewPolicy(KeepAll, params.MainnetGenesisHash)
if err != nil {
t.Fatalf("KeepAll: %v", err)
}
if p.Mode != KeepAll || p.Target != nil {
t.Errorf("KeepAll: unexpected policy %+v", p)
}
// PostMerge: resolves known mainnet prune point.
p, err = NewPolicy(KeepPostMerge, params.MainnetGenesisHash)
if err != nil {
t.Fatalf("PostMerge: %v", err)
}
if p.Target == nil || p.Target.BlockNumber != 15537393 {
t.Errorf("PostMerge: unexpected target %+v", p.Target)
}
// PostPrague: resolves known mainnet prune point.
p, err = NewPolicy(KeepPostPrague, params.MainnetGenesisHash)
if err != nil {
t.Fatalf("PostPrague: %v", err)
}
if p.Target == nil || p.Target.BlockNumber != 22431084 {
t.Errorf("PostPrague: unexpected target %+v", p.Target)
}
// PostMerge on unknown network: error.
if _, err = NewPolicy(KeepPostMerge, common.HexToHash("0xdeadbeef")); err == nil {
t.Fatal("PostMerge unknown network: expected error")
}
}

View file

@ -480,7 +480,7 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error {
receipts.add(size) receipts.add(size)
case bytes.HasPrefix(key, headerPrefix) && bytes.HasSuffix(key, headerTDSuffix) && len(key) == (len(headerPrefix)+8+common.HashLength+len(headerTDSuffix)): case bytes.HasPrefix(key, headerPrefix) && bytes.HasSuffix(key, headerTDSuffix) && len(key) == (len(headerPrefix)+8+common.HashLength+len(headerTDSuffix)):
tds.add(size) tds.add(size)
case bytes.HasPrefix(key, headerPrefix) && bytes.HasSuffix(key, headerHashSuffix) && len(key) == (len(headerPrefix)+8+common.HashLength+len(headerHashSuffix)): case bytes.HasPrefix(key, headerPrefix) && bytes.HasSuffix(key, headerHashSuffix) && len(key) == (len(headerPrefix)+8+len(headerHashSuffix)):
numHashPairings.add(size) numHashPairings.add(size)
case bytes.HasPrefix(key, headerNumberPrefix) && len(key) == (len(headerNumberPrefix)+common.HashLength): case bytes.HasPrefix(key, headerNumberPrefix) && len(key) == (len(headerNumberPrefix)+common.HashLength):
hashNumPairings.add(size) hashNumPairings.add(size)

View file

@ -135,8 +135,7 @@ type StateDB struct {
journal *journal journal *journal
// State witness if cross validation is needed // State witness if cross validation is needed
witness *stateless.Witness witness *stateless.Witness
witnessStats *stateless.WitnessStats
// Measurements gathered during execution for debugging purposes // Measurements gathered during execution for debugging purposes
AccountReads time.Duration AccountReads time.Duration
@ -201,13 +200,12 @@ func NewWithReader(root common.Hash, db Database, reader Reader) (*StateDB, erro
// StartPrefetcher initializes a new trie prefetcher to pull in nodes from the // StartPrefetcher initializes a new trie prefetcher to pull in nodes from the
// state trie concurrently while the state is mutated so that when we reach the // state trie concurrently while the state is mutated so that when we reach the
// commit phase, most of the needed data is already hot. // commit phase, most of the needed data is already hot.
func (s *StateDB) StartPrefetcher(namespace string, witness *stateless.Witness, witnessStats *stateless.WitnessStats) { func (s *StateDB) StartPrefetcher(namespace string, witness *stateless.Witness) {
// Terminate any previously running prefetcher // Terminate any previously running prefetcher
s.StopPrefetcher() s.StopPrefetcher()
// Enable witness collection if requested // Enable witness collection if requested
s.witness = witness s.witness = witness
s.witnessStats = witnessStats
// With the switch to the Proof-of-Stake consensus algorithm, block production // With the switch to the Proof-of-Stake consensus algorithm, block production
// rewards are now handled at the consensus layer. Consequently, a block may // rewards are now handled at the consensus layer. Consequently, a block may
@ -743,6 +741,41 @@ func (s *StateDB) GetRefund() uint64 {
return s.refund return s.refund
} }
type removedAccountWithBalance struct {
address common.Address
balance *uint256.Int
}
// EmitLogsForBurnAccounts emits 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
// removal. In this case the burning of these remaining balances still need to
// be logged.
// Specification EIP-7708: https://eips.ethereum.org/EIPS/eip-7708
//
// This function should only be invoked at the transaction boundary, specifically
// before the Finalise.
func (s *StateDB) EmitLogsForBurnAccounts() {
var list []removedAccountWithBalance
for addr := range s.journal.dirties {
if obj, exist := s.stateObjects[addr]; exist && obj.selfDestructed && !obj.Balance().IsZero() {
list = append(list, removedAccountWithBalance{
address: obj.address,
balance: obj.Balance(),
})
}
}
if list != nil {
sort.Slice(list, func(i, j int) bool {
return list[i].address.Cmp(list[j].address) < 0
})
}
for _, acct := range list {
s.AddLog(types.EthBurnLog(acct.address, acct.balance))
}
}
// Finalise finalises the state by removing the destructed objects and clears // Finalise finalises the state by removing the destructed objects and clears
// the journal as well as the refunds. Finalise, however, will not push any updates // the journal as well as the refunds. Finalise, however, will not push any updates
// into the tries just yet. Only IntermediateRoot or Commit will do that. // into the tries just yet. Only IntermediateRoot or Commit will do that.
@ -824,32 +857,65 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
workers errgroup.Group workers errgroup.Group
) )
if s.db.TrieDB().IsVerkle() { if s.db.TrieDB().IsVerkle() {
// Whilst MPT storage tries are independent, Verkle has one single trie // Bypass per-account updateTrie() for binary trie. In binary trie mode
// for all the accounts and all the storage slots merged together. The // there is only one unified trie (OpenStorageTrie returns self), so the
// former can thus be simply parallelized, but updating the latter will // per-account trie setup in updateTrie() (getPrefetchedTrie, getTrie,
// need concurrency support within the trie itself. That's a TODO for a // prefetcher.used) is redundant overhead. Apply all storage updates
// later time. // directly in a single pass.
workers.SetLimit(1) for addr, op := range s.mutations {
} if op.applied || op.isDelete() {
for addr, op := range s.mutations { continue
if op.applied || op.isDelete() { }
continue obj := s.stateObjects[addr]
if len(obj.uncommittedStorage) == 0 {
continue
}
for key, origin := range obj.uncommittedStorage {
value, exist := obj.pendingStorage[key]
if value == origin || !exist {
continue
}
if (value != common.Hash{}) {
if err := s.trie.UpdateStorage(addr, key[:], common.TrimLeftZeroes(value[:])); err != nil {
s.setError(err)
}
} else {
if err := s.trie.DeleteStorage(addr, key[:]); err != nil {
s.setError(err)
}
}
}
} }
obj := s.stateObjects[addr] // closure for the task runner below // Clear uncommittedStorage and assign trie on each touched object.
workers.Go(func() error { // obj.trie must be set because this path bypasses updateTrie(), which
if s.db.TrieDB().IsVerkle() { // is where obj.trie normally gets lazily loaded via getTrie().
obj.updateTrie() for addr, op := range s.mutations {
} else { if op.applied || op.isDelete() {
continue
}
obj := s.stateObjects[addr]
if len(obj.uncommittedStorage) > 0 {
obj.uncommittedStorage = make(Storage)
}
obj.trie = s.trie
}
} else {
for addr, op := range s.mutations {
if op.applied || op.isDelete() {
continue
}
obj := s.stateObjects[addr] // closure for the task runner below
workers.Go(func() error {
obj.updateRoot() obj.updateRoot()
// If witness building is enabled and the state object has a trie, // If witness building is enabled and the state object has a trie,
// gather the witnesses for its specific storage trie // gather the witnesses for its specific storage trie
if s.witness != nil && obj.trie != nil { if s.witness != nil && obj.trie != nil {
s.witness.AddState(obj.trie.Witness()) s.witness.AddState(obj.trie.Witness(), obj.addrHash())
} }
} return nil
return nil })
}) }
} }
// If witness building is enabled, gather all the read-only accesses. // If witness building is enabled, gather all the read-only accesses.
// Skip witness collection in Verkle mode, they will be gathered // Skip witness collection in Verkle mode, they will be gathered
@ -862,17 +928,9 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
continue continue
} }
if trie := obj.getPrefetchedTrie(); trie != nil { if trie := obj.getPrefetchedTrie(); trie != nil {
witness := trie.Witness() s.witness.AddState(trie.Witness(), obj.addrHash())
s.witness.AddState(witness)
if s.witnessStats != nil {
s.witnessStats.Add(witness, obj.addrHash())
}
} else if obj.trie != nil { } else if obj.trie != nil {
witness := obj.trie.Witness() s.witness.AddState(obj.trie.Witness(), obj.addrHash())
s.witness.AddState(witness)
if s.witnessStats != nil {
s.witnessStats.Add(witness, obj.addrHash())
}
} }
} }
// Pull in only-read and non-destructed trie witnesses // Pull in only-read and non-destructed trie witnesses
@ -886,17 +944,9 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
continue continue
} }
if trie := obj.getPrefetchedTrie(); trie != nil { if trie := obj.getPrefetchedTrie(); trie != nil {
witness := trie.Witness() s.witness.AddState(trie.Witness(), obj.addrHash())
s.witness.AddState(witness)
if s.witnessStats != nil {
s.witnessStats.Add(witness, obj.addrHash())
}
} else if obj.trie != nil { } else if obj.trie != nil {
witness := obj.trie.Witness() s.witness.AddState(obj.trie.Witness(), obj.addrHash())
s.witness.AddState(witness)
if s.witnessStats != nil {
s.witnessStats.Add(witness, obj.addrHash())
}
} }
} }
} }
@ -911,7 +961,7 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
// only a single trie is used for state hashing. Replacing a non-nil verkle tree // only a single trie is used for state hashing. Replacing a non-nil verkle tree
// here could result in losing uncommitted changes from storage. // here could result in losing uncommitted changes from storage.
start = time.Now() start = time.Now()
if s.prefetcher != nil { if s.prefetcher != nil && !s.db.TrieDB().IsVerkle() {
if trie := s.prefetcher.trie(common.Hash{}, s.originalRoot); trie == nil { if trie := s.prefetcher.trie(common.Hash{}, s.originalRoot); trie == nil {
log.Error("Failed to retrieve account pre-fetcher trie") log.Error("Failed to retrieve account pre-fetcher trie")
} else { } else {
@ -969,11 +1019,7 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
// If witness building is enabled, gather the account trie witness // If witness building is enabled, gather the account trie witness
if s.witness != nil { if s.witness != nil {
witness := s.trie.Witness() s.witness.AddState(s.trie.Witness(), common.Hash{})
s.witness.AddState(witness)
if s.witnessStats != nil {
s.witnessStats.Add(witness, common.Hash{})
}
} }
return hash return hash
} }

View file

@ -229,6 +229,10 @@ func (s *hookedStateDB) AddLog(log *types.Log) {
} }
} }
func (s *hookedStateDB) EmitLogsForBurnAccounts() {
s.inner.EmitLogsForBurnAccounts()
}
func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) { func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) {
if s.hooks.OnBalanceChange == nil && s.hooks.OnNonceChangeV2 == nil && s.hooks.OnNonceChange == nil && s.hooks.OnCodeChangeV2 == nil && s.hooks.OnCodeChange == nil { if s.hooks.OnBalanceChange == nil && s.hooks.OnNonceChangeV2 == nil && s.hooks.OnNonceChange == nil && s.hooks.OnCodeChangeV2 == nil && s.hooks.OnCodeChange == nil {
// Short circuit if no relevant hooks are set. // Short circuit if no relevant hooks are set.

View file

@ -583,6 +583,9 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
st.evm.AccessEvents.AddAccount(st.evm.Context.Coinbase, true, math.MaxUint64) st.evm.AccessEvents.AddAccount(st.evm.Context.Coinbase, true, math.MaxUint64)
} }
} }
if rules.IsAmsterdam {
st.evm.StateDB.EmitLogsForBurnAccounts()
}
return &ExecutionResult{ return &ExecutionResult{
UsedGas: st.gasUsed(), UsedGas: st.gasUsed(),
MaxUsedGas: peakGasUsed, MaxUsedGas: peakGasUsed,

View file

@ -54,6 +54,13 @@ func NewWitnessStats() *WitnessStats {
} }
} }
func (s *WitnessStats) copy() *WitnessStats {
return &WitnessStats{
accountTrie: s.accountTrie.Copy(),
storageTrie: s.storageTrie.Copy(),
}
}
func (s *WitnessStats) init() { func (s *WitnessStats) init() {
if s.accountTrie == nil { if s.accountTrie == nil {
s.accountTrie = trie.NewLevelStats() s.accountTrie = trie.NewLevelStats()

View file

@ -42,12 +42,13 @@ type Witness struct {
Codes map[string]struct{} // Set of bytecodes ran or accessed Codes map[string]struct{} // Set of bytecodes ran or accessed
State map[string]struct{} // Set of MPT state trie nodes (account and storage together) State map[string]struct{} // Set of MPT state trie nodes (account and storage together)
chain HeaderReader // Chain reader to convert block hash ops to header proofs chain HeaderReader // Chain reader to convert block hash ops to header proofs
lock sync.Mutex // Lock to allow concurrent state insertions stats *WitnessStats // Optional statistics collector
lock sync.Mutex // Lock to allow concurrent state insertions
} }
// NewWitness creates an empty witness ready for population. // NewWitness creates an empty witness ready for population.
func NewWitness(context *types.Header, chain HeaderReader) (*Witness, error) { func NewWitness(context *types.Header, chain HeaderReader, enableStats bool) (*Witness, error) {
// When building witnesses, retrieve the parent header, which will *always* // When building witnesses, retrieve the parent header, which will *always*
// be included to act as a trustless pre-root hash container // be included to act as a trustless pre-root hash container
var headers []*types.Header var headers []*types.Header
@ -59,13 +60,17 @@ func NewWitness(context *types.Header, chain HeaderReader) (*Witness, error) {
headers = append(headers, parent) headers = append(headers, parent)
} }
// Create the witness with a reconstructed gutted out block // Create the witness with a reconstructed gutted out block
return &Witness{ w := &Witness{
context: context, context: context,
Headers: headers, Headers: headers,
Codes: make(map[string]struct{}), Codes: make(map[string]struct{}),
State: make(map[string]struct{}), State: make(map[string]struct{}),
chain: chain, chain: chain,
}, nil }
if enableStats {
w.stats = NewWitnessStats()
}
return w, nil
} }
// AddBlockHash adds a "blockhash" to the witness with the designated offset from // AddBlockHash adds a "blockhash" to the witness with the designated offset from
@ -87,8 +92,11 @@ func (w *Witness) AddCode(code []byte) {
w.Codes[string(code)] = struct{}{} w.Codes[string(code)] = struct{}{}
} }
// AddState inserts a batch of MPT trie nodes into the witness. // AddState inserts a batch of MPT trie nodes into the witness. The owner
func (w *Witness) AddState(nodes map[string][]byte) { // identifies which trie the nodes belong to: the zero hash for the account
// trie, or the hashed address for a storage trie. This is used for optional
// statistics collection.
func (w *Witness) AddState(nodes map[string][]byte, owner common.Hash) {
if len(nodes) == 0 { if len(nodes) == 0 {
return return
} }
@ -98,6 +106,17 @@ func (w *Witness) AddState(nodes map[string][]byte) {
for _, value := range nodes { for _, value := range nodes {
w.State[string(value)] = struct{}{} w.State[string(value)] = struct{}{}
} }
if w.stats != nil {
w.stats.Add(nodes, owner)
}
}
// ReportMetrics reports the collected statistics to the global metrics registry.
func (w *Witness) ReportMetrics(blockNumber uint64) {
if w.stats == nil {
return
}
w.stats.ReportMetrics(blockNumber)
} }
func (w *Witness) AddKey() { func (w *Witness) AddKey() {
@ -113,6 +132,9 @@ func (w *Witness) Copy() *Witness {
State: maps.Clone(w.State), State: maps.Clone(w.State),
chain: w.chain, chain: w.chain,
} }
if w.stats != nil {
cpy.stats = w.stats.copy()
}
if w.context != nil { if w.context != nil {
cpy.context = types.CopyHeader(w.context) cpy.context = types.CopyHeader(w.context)
} }

View file

@ -426,7 +426,7 @@ const (
// NonceChangeNewContract is the nonce change of a newly created contract. // NonceChangeNewContract is the nonce change of a newly created contract.
NonceChangeNewContract NonceChangeReason = 4 NonceChangeNewContract NonceChangeReason = 4
// NonceChangeTransaction is the nonce change due to a EIP-7702 authorization. // NonceChangeAuthorization is the nonce change due to a EIP-7702 authorization.
NonceChangeAuthorization NonceChangeReason = 5 NonceChangeAuthorization NonceChangeReason = 5
// NonceChangeRevert is emitted when the nonce is reverted back to a previous value due to call failure. // NonceChangeRevert is emitted when the nonce is reverted back to a previous value due to call failure.

View file

@ -998,11 +998,7 @@ func (pool *LegacyPool) Status(hash common.Hash) txpool.TxStatus {
// Get returns a transaction if it is contained in the pool and nil otherwise. // Get returns a transaction if it is contained in the pool and nil otherwise.
func (pool *LegacyPool) Get(hash common.Hash) *types.Transaction { func (pool *LegacyPool) Get(hash common.Hash) *types.Transaction {
tx := pool.get(hash) return pool.get(hash)
if tx == nil {
return nil
}
return tx
} }
// get returns a transaction if it is contained in the pool and nil otherwise. // get returns a transaction if it is contained in the pool and nil otherwise.

View file

@ -19,6 +19,8 @@ package types
import ( import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256"
) )
//go:generate go run ../../rlp/rlpgen -type Log -out gen_log_rlp.go //go:generate go run ../../rlp/rlpgen -type Log -out gen_log_rlp.go
@ -62,3 +64,32 @@ type logMarshaling struct {
BlockTimestamp hexutil.Uint64 BlockTimestamp hexutil.Uint64
Index hexutil.Uint Index hexutil.Uint
} }
// EthTransferLog creates and ETH transfer log according to EIP-7708.
// Specification: https://eips.ethereum.org/EIPS/eip-7708
func EthTransferLog(from, to common.Address, amount *uint256.Int) *Log {
amount32 := amount.Bytes32()
return &Log{
Address: params.SystemAddress,
Topics: []common.Hash{
params.EthTransferLogEvent,
common.BytesToHash(from.Bytes()),
common.BytesToHash(to.Bytes()),
},
Data: amount32[:],
}
}
// EthBurnLog creates an ETH burn log according to EIP-7708.
// Specification: https://eips.ethereum.org/EIPS/eip-7708
func EthBurnLog(from common.Address, amount *uint256.Int) *Log {
amount32 := amount.Bytes32()
return &Log{
Address: params.SystemAddress,
Topics: []common.Hash{
params.EthBurnLogEvent,
common.BytesToHash(from.Bytes()),
},
Data: amount32[:],
}
}

View file

@ -35,7 +35,7 @@ type (
// CanTransferFunc is the signature of a transfer guard function // CanTransferFunc is the signature of a transfer guard function
CanTransferFunc func(StateDB, common.Address, *uint256.Int) bool CanTransferFunc func(StateDB, common.Address, *uint256.Int) bool
// TransferFunc is the signature of a transfer function // TransferFunc is the signature of a transfer function
TransferFunc func(StateDB, common.Address, common.Address, *uint256.Int) TransferFunc func(StateDB, common.Address, common.Address, *uint256.Int, *params.Rules)
// GetHashFunc returns the n'th block hash in the blockchain // GetHashFunc returns the n'th block hash in the blockchain
// and is used by the BLOCKHASH EVM op code. // and is used by the BLOCKHASH EVM op code.
GetHashFunc func(uint64) common.Hash GetHashFunc func(uint64) common.Hash
@ -283,8 +283,9 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
// Calling this is required even for zero-value transfers, // Calling this is required even for zero-value transfers,
// to ensure the state clearing mechanism is applied. // to ensure the state clearing mechanism is applied.
if !syscall { if !syscall {
evm.Context.Transfer(evm.StateDB, caller, addr, value) evm.Context.Transfer(evm.StateDB, caller, addr, value, &evm.chainRules)
} }
if isPrecompile { if isPrecompile {
var stateDB StateDB var stateDB StateDB
if evm.chainRules.IsAmsterdam { if evm.chainRules.IsAmsterdam {
@ -567,7 +568,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas uint64, value *ui
} }
gas = gas - consumed gas = gas - consumed
} }
evm.Context.Transfer(evm.StateDB, caller, address, value) evm.Context.Transfer(evm.StateDB, caller, address, value, &evm.chainRules)
// Initialise a new contract and set the code that is to be used by the EVM. // Initialise a new contract and set the code that is to be used by the EVM.
// The contract is a scoped environment for this execution context only. // The contract is a scoped environment for this execution context only.

View file

@ -49,6 +49,5 @@ func callGas(isEip150 bool, availableGas, base uint64, callCost *uint256.Int) (u
if !callCost.IsUint64() { if !callCost.IsUint64() {
return 0, ErrGasUintOverflow return 0, ErrGasUintOverflow
} }
return callCost.Uint64(), nil return callCost.Uint64(), nil
} }

View file

@ -373,7 +373,32 @@ func gasExpEIP158(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memor
return gas, nil return gas, nil
} }
func gasCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { var (
gasCall = makeCallVariantGasCost(gasCallIntrinsic)
gasCallCode = makeCallVariantGasCost(gasCallCodeIntrinsic)
gasDelegateCall = makeCallVariantGasCost(gasDelegateCallIntrinsic)
gasStaticCall = makeCallVariantGasCost(gasStaticCallIntrinsic)
)
func makeCallVariantGasCost(intrinsicFunc gasFunc) gasFunc {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
intrinsic, err := intrinsicFunc(evm, contract, stack, mem, memorySize)
if err != nil {
return 0, err
}
evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas, intrinsic, stack.Back(0))
if err != nil {
return 0, err
}
gas, overflow := math.SafeAdd(intrinsic, evm.callGasTemp)
if overflow {
return 0, ErrGasUintOverflow
}
return gas, nil
}
}
func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
var ( var (
gas uint64 gas uint64
transfersValue = !stack.Back(2).IsZero() transfersValue = !stack.Back(2).IsZero()
@ -382,38 +407,40 @@ func gasCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize
if evm.readOnly && transfersValue { if evm.readOnly && transfersValue {
return 0, ErrWriteProtection return 0, ErrWriteProtection
} }
// Stateless check
if evm.chainRules.IsEIP158 {
if transfersValue && evm.StateDB.Empty(address) {
gas += params.CallNewAccountGas
}
} else if !evm.StateDB.Exist(address) {
gas += params.CallNewAccountGas
}
if transfersValue && !evm.chainRules.IsEIP4762 {
gas += params.CallValueTransferGas
}
memoryGas, err := memoryGasCost(mem, memorySize) memoryGas, err := memoryGasCost(mem, memorySize)
if err != nil { if err != nil {
return 0, err return 0, err
} }
var transferGas uint64
if transfersValue && !evm.chainRules.IsEIP4762 {
transferGas = params.CallValueTransferGas
}
var overflow bool var overflow bool
if gas, overflow = math.SafeAdd(gas, memoryGas); overflow { if gas, overflow = math.SafeAdd(memoryGas, transferGas); overflow {
return 0, ErrGasUintOverflow return 0, ErrGasUintOverflow
} }
// Terminate the gas measurement if the leftover gas is not sufficient,
evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas, gas, stack.Back(0)) // it can effectively prevent accessing the states in the following steps.
if err != nil { if contract.Gas < gas {
return 0, err return 0, ErrOutOfGas
} }
if gas, overflow = math.SafeAdd(gas, evm.callGasTemp); overflow { // Stateful check
var stateGas uint64
if evm.chainRules.IsEIP158 {
if transfersValue && evm.StateDB.Empty(address) {
stateGas += params.CallNewAccountGas
}
} else if !evm.StateDB.Exist(address) {
stateGas += params.CallNewAccountGas
}
if gas, overflow = math.SafeAdd(gas, stateGas); overflow {
return 0, ErrGasUintOverflow return 0, ErrGasUintOverflow
} }
return gas, nil return gas, nil
} }
func gasCallCode(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { func gasCallCodeIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
memoryGas, err := memoryGasCost(mem, memorySize) memoryGas, err := memoryGasCost(mem, memorySize)
if err != nil { if err != nil {
return 0, err return 0, err
@ -428,46 +455,15 @@ func gasCallCode(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memory
if gas, overflow = math.SafeAdd(gas, memoryGas); overflow { if gas, overflow = math.SafeAdd(gas, memoryGas); overflow {
return 0, ErrGasUintOverflow return 0, ErrGasUintOverflow
} }
evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas, gas, stack.Back(0))
if err != nil {
return 0, err
}
if gas, overflow = math.SafeAdd(gas, evm.callGasTemp); overflow {
return 0, ErrGasUintOverflow
}
return gas, nil return gas, nil
} }
func gasDelegateCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { func gasDelegateCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
gas, err := memoryGasCost(mem, memorySize) return memoryGasCost(mem, memorySize)
if err != nil {
return 0, err
}
evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas, gas, stack.Back(0))
if err != nil {
return 0, err
}
var overflow bool
if gas, overflow = math.SafeAdd(gas, evm.callGasTemp); overflow {
return 0, ErrGasUintOverflow
}
return gas, nil
} }
func gasStaticCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { func gasStaticCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
gas, err := memoryGasCost(mem, memorySize) return memoryGasCost(mem, memorySize)
if err != nil {
return 0, err
}
evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas, gas, stack.Back(0))
if err != nil {
return 0, err
}
var overflow bool
if gas, overflow = math.SafeAdd(gas, evm.callGasTemp); overflow {
return 0, ErrGasUintOverflow
}
return 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) (uint64, error) {

View file

@ -94,7 +94,7 @@ func TestEIP2200(t *testing.T) {
vmctx := BlockContext{ vmctx := BlockContext{
CanTransfer: func(StateDB, common.Address, *uint256.Int) bool { return true }, CanTransfer: func(StateDB, common.Address, *uint256.Int) bool { return true },
Transfer: func(StateDB, common.Address, common.Address, *uint256.Int) {}, Transfer: func(StateDB, common.Address, common.Address, *uint256.Int, *params.Rules) {},
} }
evm := NewEVM(vmctx, statedb, params.AllEthashProtocolChanges, Config{ExtraEips: []int{2200}}) evm := NewEVM(vmctx, statedb, params.AllEthashProtocolChanges, Config{ExtraEips: []int{2200}})
@ -144,7 +144,7 @@ func TestCreateGas(t *testing.T) {
statedb.Finalise(true) statedb.Finalise(true)
vmctx := BlockContext{ vmctx := BlockContext{
CanTransfer: func(StateDB, common.Address, *uint256.Int) bool { return true }, CanTransfer: func(StateDB, common.Address, *uint256.Int) bool { return true },
Transfer: func(StateDB, common.Address, common.Address, *uint256.Int) {}, Transfer: func(StateDB, common.Address, common.Address, *uint256.Int, *params.Rules) {},
BlockNumber: big.NewInt(0), BlockNumber: big.NewInt(0),
} }
config := Config{} config := Config{}

View file

@ -934,6 +934,13 @@ func opSelfdestruct6780(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, erro
evm.StateDB.SubBalance(this, balance, tracing.BalanceDecreaseSelfdestruct) evm.StateDB.SubBalance(this, balance, tracing.BalanceDecreaseSelfdestruct)
evm.StateDB.AddBalance(beneficiary, balance, tracing.BalanceIncreaseSelfdestruct) evm.StateDB.AddBalance(beneficiary, balance, tracing.BalanceIncreaseSelfdestruct)
} }
if evm.chainRules.IsAmsterdam && !balance.IsZero() {
if this != beneficiary {
evm.StateDB.AddLog(types.EthTransferLog(this, beneficiary, balance))
} else if newContract {
evm.StateDB.AddLog(types.EthBurnLog(this, balance))
}
}
if tracer := evm.Config.Tracer; tracer != nil { if tracer := evm.Config.Tracer; tracer != nil {
if tracer.OnEnter != nil { if tracer.OnEnter != nil {
@ -1086,9 +1093,6 @@ func makeLog(size int) executionFunc {
Address: scope.Contract.Address(), Address: scope.Contract.Address(),
Topics: topics, Topics: topics,
Data: d, Data: d,
// This is a non-consensus field, but assigned here because
// core/state doesn't know the current block number.
BlockNumber: evm.Context.BlockNumber.Uint64(),
}) })
return nil, nil return nil, nil

View file

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

View file

@ -40,7 +40,7 @@ var loopInterruptTests = []string{
func TestLoopInterrupt(t *testing.T) { func TestLoopInterrupt(t *testing.T) {
address := common.BytesToAddress([]byte("contract")) address := common.BytesToAddress([]byte("contract"))
vmctx := BlockContext{ vmctx := BlockContext{
Transfer: func(StateDB, common.Address, common.Address, *uint256.Int) {}, Transfer: func(StateDB, common.Address, common.Address, *uint256.Int, *params.Rules) {},
} }
for i, tt := range loopInterruptTests { for i, tt := range loopInterruptTests {

View file

@ -256,10 +256,10 @@ func makeSelfdestructGasFn(refundsEnabled bool) gasFunc {
} }
var ( var (
innerGasCallEIP7702 = makeCallVariantGasCallEIP7702(gasCall) innerGasCallEIP7702 = makeCallVariantGasCallEIP7702(gasCallIntrinsic)
gasDelegateCallEIP7702 = makeCallVariantGasCallEIP7702(gasDelegateCall) gasDelegateCallEIP7702 = makeCallVariantGasCallEIP7702(gasDelegateCallIntrinsic)
gasStaticCallEIP7702 = makeCallVariantGasCallEIP7702(gasStaticCall) gasStaticCallEIP7702 = makeCallVariantGasCallEIP7702(gasStaticCallIntrinsic)
gasCallCodeEIP7702 = makeCallVariantGasCallEIP7702(gasCallCode) 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) (uint64, error) {
@ -274,62 +274,85 @@ func gasCallEIP7702(evm *EVM, contract *Contract, stack *Stack, mem *Memory, mem
return innerGasCallEIP7702(evm, contract, stack, mem, memorySize) return innerGasCallEIP7702(evm, contract, stack, mem, memorySize)
} }
func makeCallVariantGasCallEIP7702(oldCalculator gasFunc) gasFunc { 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) (uint64, error) {
var ( var (
total uint64 // total dynamic gas used eip2929Cost uint64
addr = common.Address(stack.Back(1).Bytes20()) eip7702Cost uint64
addr = common.Address(stack.Back(1).Bytes20())
) )
// Perform EIP-2929 checks (stateless), checking address presence
// Check slot presence in the access list // in the accessList and charge the cold access accordingly.
if !evm.StateDB.AddressInAccessList(addr) { if !evm.StateDB.AddressInAccessList(addr) {
evm.StateDB.AddAddressToAccessList(addr) evm.StateDB.AddAddressToAccessList(addr)
// The WarmStorageReadCostEIP2929 (100) is already deducted in the form of a constant cost, so
// the cost to charge for cold access, if any, is Cold - Warm // The WarmStorageReadCostEIP2929 (100) is already deducted in the form
coldCost := params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 // of a constant cost, so the cost to charge for cold access, if any,
// Charge the remaining difference here already, to correctly calculate available // is Cold - Warm
// gas for call eip2929Cost = params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929
if !contract.UseGas(coldCost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
// 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 0, ErrOutOfGas
} }
total += coldCost }
// Perform the intrinsic cost calculation including:
//
// - transfer value
// - memory expansion
// - create new account
intrinsicCost, err := intrinsicFunc(evm, contract, stack, mem, memorySize)
if err != nil {
return 0, 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
} }
// Check if code is a delegation and if so, charge for resolution. // Check if code is a delegation and if so, charge for resolution.
if target, ok := types.ParseDelegation(evm.StateDB.GetCode(addr)); ok { if target, ok := types.ParseDelegation(evm.StateDB.GetCode(addr)); ok {
var cost uint64
if evm.StateDB.AddressInAccessList(target) { if evm.StateDB.AddressInAccessList(target) {
cost = params.WarmStorageReadCostEIP2929 eip7702Cost = params.WarmStorageReadCostEIP2929
} else { } else {
evm.StateDB.AddAddressToAccessList(target) evm.StateDB.AddAddressToAccessList(target)
cost = params.ColdAccountAccessCostEIP2929 eip7702Cost = params.ColdAccountAccessCostEIP2929
} }
if !contract.UseGas(cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { if !contract.UseGas(eip7702Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
return 0, ErrOutOfGas return 0, ErrOutOfGas
} }
total += cost
} }
// Calculate the gas budget for the nested call. The costs defined by
// Now call the old calculator, which takes into account // EIP-2929 and EIP-7702 have already been applied.
// - create new account evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas, intrinsicCost, stack.Back(0))
// - transfer value
// - memory expansion
// - 63/64ths rule
old, err := oldCalculator(evm, contract, stack, mem, memorySize)
if err != nil { if err != nil {
return old, err return 0, err
} }
// Temporarily add the gas charge back to the contract and return value. By // 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 // 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 // part of the dynamic gas. This will ensure it is correctly reported to
// tracers. // tracers.
contract.Gas += total contract.Gas += eip2929Cost + eip7702Cost
var overflow bool // Aggregate the gas costs from all components, including EIP-2929, EIP-7702,
if total, overflow = math.SafeAdd(old, total); overflow { // the CALL opcode itself, and the cost incurred by nested calls.
var (
overflow bool
totalCost uint64
)
if totalCost, overflow = math.SafeAdd(eip2929Cost, eip7702Cost); overflow {
return 0, ErrGasUintOverflow return 0, ErrGasUintOverflow
} }
return total, nil if totalCost, overflow = math.SafeAdd(totalCost, intrinsicCost); overflow {
return 0, ErrGasUintOverflow
}
if totalCost, overflow = math.SafeAdd(totalCost, evm.callGasTemp); overflow {
return 0, ErrGasUintOverflow
}
return totalCost, nil
} }
} }

View file

@ -33,6 +33,7 @@ import (
"github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/filtermaps" "github.com/ethereum/go-ethereum/core/filtermaps"
"github.com/ethereum/go-ethereum/core/history"
"github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state/pruner" "github.com/ethereum/go-ethereum/core/state/pruner"
"github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/txpool"
@ -175,7 +176,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
// Here we determine genesis hash and active ChainConfig. // Here we determine genesis hash and active ChainConfig.
// We need these to figure out the consensus parameters and to set up history pruning. // We need these to figure out the consensus parameters and to set up history pruning.
chainConfig, _, err := core.LoadChainConfig(chainDb, config.Genesis) chainConfig, genesisHash, err := core.LoadChainConfig(chainDb, config.Genesis)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -220,6 +221,10 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
rawdb.WriteDatabaseVersion(chainDb, core.BlockChainVersion) rawdb.WriteDatabaseVersion(chainDb, core.BlockChainVersion)
} }
} }
histPolicy, err := history.NewPolicy(config.HistoryMode, genesisHash)
if err != nil {
return nil, err
}
var ( var (
options = &core.BlockChainConfig{ options = &core.BlockChainConfig{
TrieCleanLimit: config.TrieCleanCache, TrieCleanLimit: config.TrieCleanCache,
@ -233,7 +238,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
TrienodeHistory: config.TrienodeHistory, TrienodeHistory: config.TrienodeHistory,
NodeFullValueCheckpoint: config.NodeFullValueCheckpoint, NodeFullValueCheckpoint: config.NodeFullValueCheckpoint,
StateScheme: scheme, StateScheme: scheme,
ChainHistoryMode: config.HistoryMode, HistoryPolicy: histPolicy,
TxLookupLimit: int64(min(config.TransactionHistory, math.MaxInt64)), TxLookupLimit: int64(min(config.TransactionHistory, math.MaxInt64)),
VmConfig: vm.Config{ VmConfig: vm.Config{
EnablePreimageRecording: config.EnablePreimageRecording, EnablePreimageRecording: config.EnablePreimageRecording,

View file

@ -390,7 +390,7 @@ func (f *Filter) rangeLogs(ctx context.Context, firstBlock, lastBlock uint64) ([
} }
if firstBlock > lastBlock { if firstBlock > lastBlock {
return nil, nil return nil, errInvalidBlockRange
} }
mb := f.sys.backend.NewMatcherBackend() mb := f.sys.backend.NewMatcherBackend()
defer mb.Close() defer mb.Close()

View file

@ -357,7 +357,8 @@ func testFilters(t *testing.T, history uint64, noHistory bool) {
want: `[{"address":"0xff00000000000000000000000000000000000000","topics":["0x0000000000000000000000000000000000000000000000000000746f70696333"],"data":"0x","blockNumber":"0x3e7","transactionHash":"0x53e3675800c6908424b61b35a44e51ca4c73ca603e58a65b32c67968b4f42200","transactionIndex":"0x0","blockHash":"0x2e4620a2b426b0612ec6cad9603f466723edaed87f98c9137405dd4f7a2409ff","blockTimestamp":"0x2706","logIndex":"0x0","removed":false}]`, want: `[{"address":"0xff00000000000000000000000000000000000000","topics":["0x0000000000000000000000000000000000000000000000000000746f70696333"],"data":"0x","blockNumber":"0x3e7","transactionHash":"0x53e3675800c6908424b61b35a44e51ca4c73ca603e58a65b32c67968b4f42200","transactionIndex":"0x0","blockHash":"0x2e4620a2b426b0612ec6cad9603f466723edaed87f98c9137405dd4f7a2409ff","blockTimestamp":"0x2706","logIndex":"0x0","removed":false}]`,
}, },
{ {
f: sys.NewRangeFilter(int64(rpc.LatestBlockNumber), int64(rpc.FinalizedBlockNumber), nil, nil, 0), f: sys.NewRangeFilter(int64(rpc.LatestBlockNumber), int64(rpc.FinalizedBlockNumber), nil, nil, 0),
err: errInvalidBlockRange.Error(),
}, },
{ {
f: sys.NewRangeFilter(int64(rpc.SafeBlockNumber), int64(rpc.LatestBlockNumber), nil, nil, 0), f: sys.NewRangeFilter(int64(rpc.SafeBlockNumber), int64(rpc.LatestBlockNumber), nil, nil, 0),

View file

@ -141,7 +141,7 @@ func txValidationError(err error) *invalidTxError {
return &invalidTxError{Message: err.Error(), Code: errCodeIntrinsicGas} return &invalidTxError{Message: err.Error(), Code: errCodeIntrinsicGas}
case errors.Is(err, core.ErrInsufficientFundsForTransfer): case errors.Is(err, core.ErrInsufficientFundsForTransfer):
return &invalidTxError{Message: err.Error(), Code: errCodeInsufficientFunds} return &invalidTxError{Message: err.Error(), Code: errCodeInsufficientFunds}
case errors.Is(err, core.ErrMaxInitCodeSizeExceeded): case errors.Is(err, vm.ErrMaxInitCodeSizeExceeded):
return &invalidTxError{Message: err.Error(), Code: errCodeMaxInitCodeSizeExceeded} return &invalidTxError{Message: err.Error(), Code: errCodeMaxInitCodeSizeExceeded}
} }
return &invalidTxError{ return &invalidTxError{

View file

@ -350,6 +350,7 @@ func (miner *Miner) BuildTestingPayload(args *BuildPayloadArgs, transactions []*
random: args.Random, random: args.Random,
withdrawals: args.Withdrawals, withdrawals: args.Withdrawals,
beaconRoot: args.BeaconRoot, beaconRoot: args.BeaconRoot,
slotNum: args.SlotNum,
noTxs: empty, noTxs: empty,
forceOverrides: true, forceOverrides: true,
overrideExtraData: extraData, overrideExtraData: extraData,

View file

@ -75,7 +75,7 @@ type environment struct {
witness *stateless.Witness witness *stateless.Witness
} }
// txFits reports whether the transaction fits into the block size limit. // txFitsSize reports whether the transaction fits into the block size limit.
func (env *environment) txFitsSize(tx *types.Transaction) bool { func (env *environment) txFitsSize(tx *types.Transaction) bool {
return env.size+tx.Size() < params.MaxBlockSize-maxBlockSizeBufferZone return env.size+tx.Size() < params.MaxBlockSize-maxBlockSizeBufferZone
} }
@ -330,12 +330,12 @@ func (miner *Miner) makeEnv(parent *types.Header, header *types.Header, coinbase
} }
var bundle *stateless.Witness var bundle *stateless.Witness
if witness { if witness {
bundle, err = stateless.NewWitness(header, miner.chain) bundle, err = stateless.NewWitness(header, miner.chain, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
state.StartPrefetcher("miner", bundle, nil) state.StartPrefetcher("miner", bundle)
// Note the passed coinbase may be different with header.Coinbase. // Note the passed coinbase may be different with header.Coinbase.
return &environment{ return &environment{
signer: types.MakeSigner(miner.chainConfig, header.Number, header.Time), signer: types.MakeSigner(miner.chainConfig, header.Number, header.Time),

View file

@ -35,7 +35,7 @@ function coverbuild {
sed -i -e 's/TestFuzzCorpus/Test'$function'Corpus/' ./"${function,,}"_test.go sed -i -e 's/TestFuzzCorpus/Test'$function'Corpus/' ./"${function,,}"_test.go
cat << DOG > $OUT/$fuzzer cat << DOG > $OUT/$fuzzer
#/bin/sh #!/bin/sh
cd $OUT/$path cd $OUT/$path
go test -run Test${function}Corpus -v $tags -coverprofile \$1 -coverpkg $coverpkg go test -run Test${function}Corpus -v $tags -coverprofile \$1 -coverpkg $coverpkg

View file

@ -76,6 +76,7 @@ var (
errSelf = errors.New("is self") errSelf = errors.New("is self")
errAlreadyDialing = errors.New("already dialing") errAlreadyDialing = errors.New("already dialing")
errAlreadyConnected = errors.New("already connected") errAlreadyConnected = errors.New("already connected")
errPendingInbound = errors.New("peer has pending inbound connection")
errRecentlyDialed = errors.New("recently dialed") errRecentlyDialed = errors.New("recently dialed")
errNetRestrict = errors.New("not contained in netrestrict list") errNetRestrict = errors.New("not contained in netrestrict list")
errNoPort = errors.New("node does not provide TCP port") errNoPort = errors.New("node does not provide TCP port")
@ -104,12 +105,15 @@ type dialScheduler struct {
remStaticCh chan *enode.Node remStaticCh chan *enode.Node
addPeerCh chan *conn addPeerCh chan *conn
remPeerCh chan *conn remPeerCh chan *conn
addPendingCh chan enode.ID
remPendingCh chan enode.ID
// Everything below here belongs to loop and // Everything below here belongs to loop and
// should only be accessed by code on the loop goroutine. // should only be accessed by code on the loop goroutine.
dialing map[enode.ID]*dialTask // active tasks dialing map[enode.ID]*dialTask // active tasks
peers map[enode.ID]struct{} // all connected peers peers map[enode.ID]struct{} // all connected peers
dialPeers int // current number of dialed peers pendingInbound map[enode.ID]struct{} // in-progress inbound connections
dialPeers int // current number of dialed peers
// The static map tracks all static dial tasks. The subset of usable static dial tasks // The static map tracks all static dial tasks. The subset of usable static dial tasks
// (i.e. those passing checkDial) is kept in staticPool. The scheduler prefers // (i.e. those passing checkDial) is kept in staticPool. The scheduler prefers
@ -163,19 +167,22 @@ func (cfg dialConfig) withDefaults() dialConfig {
func newDialScheduler(config dialConfig, it enode.Iterator, setupFunc dialSetupFunc) *dialScheduler { func newDialScheduler(config dialConfig, it enode.Iterator, setupFunc dialSetupFunc) *dialScheduler {
cfg := config.withDefaults() cfg := config.withDefaults()
d := &dialScheduler{ d := &dialScheduler{
dialConfig: cfg, dialConfig: cfg,
historyTimer: mclock.NewAlarm(cfg.clock), historyTimer: mclock.NewAlarm(cfg.clock),
setupFunc: setupFunc, setupFunc: setupFunc,
dnsLookupFunc: net.DefaultResolver.LookupNetIP, dnsLookupFunc: net.DefaultResolver.LookupNetIP,
dialing: make(map[enode.ID]*dialTask), dialing: make(map[enode.ID]*dialTask),
static: make(map[enode.ID]*dialTask), static: make(map[enode.ID]*dialTask),
peers: make(map[enode.ID]struct{}), peers: make(map[enode.ID]struct{}),
doneCh: make(chan *dialTask), pendingInbound: make(map[enode.ID]struct{}),
nodesIn: make(chan *enode.Node), doneCh: make(chan *dialTask),
addStaticCh: make(chan *enode.Node), nodesIn: make(chan *enode.Node),
remStaticCh: make(chan *enode.Node), addStaticCh: make(chan *enode.Node),
addPeerCh: make(chan *conn), remStaticCh: make(chan *enode.Node),
remPeerCh: make(chan *conn), addPeerCh: make(chan *conn),
remPeerCh: make(chan *conn),
addPendingCh: make(chan enode.ID),
remPendingCh: make(chan enode.ID),
} }
d.lastStatsLog = d.clock.Now() d.lastStatsLog = d.clock.Now()
d.ctx, d.cancel = context.WithCancel(context.Background()) d.ctx, d.cancel = context.WithCancel(context.Background())
@ -223,6 +230,22 @@ func (d *dialScheduler) peerRemoved(c *conn) {
} }
} }
// inboundPending notifies the scheduler about a pending inbound connection.
func (d *dialScheduler) inboundPending(id enode.ID) {
select {
case d.addPendingCh <- id:
case <-d.ctx.Done():
}
}
// inboundCompleted notifies the scheduler that an inbound connection completed or failed.
func (d *dialScheduler) inboundCompleted(id enode.ID) {
select {
case d.remPendingCh <- id:
case <-d.ctx.Done():
}
}
// loop is the main loop of the dialer. // loop is the main loop of the dialer.
func (d *dialScheduler) loop(it enode.Iterator) { func (d *dialScheduler) loop(it enode.Iterator) {
var ( var (
@ -276,6 +299,15 @@ loop:
delete(d.peers, c.node.ID()) delete(d.peers, c.node.ID())
d.updateStaticPool(c.node.ID()) d.updateStaticPool(c.node.ID())
case id := <-d.addPendingCh:
d.pendingInbound[id] = struct{}{}
d.log.Trace("Marked node as pending inbound", "id", id)
case id := <-d.remPendingCh:
delete(d.pendingInbound, id)
d.updateStaticPool(id)
d.log.Trace("Unmarked node as pending inbound", "id", id)
case node := <-d.addStaticCh: case node := <-d.addStaticCh:
id := node.ID() id := node.ID()
_, exists := d.static[id] _, exists := d.static[id]
@ -390,6 +422,9 @@ func (d *dialScheduler) checkDial(n *enode.Node) error {
if _, ok := d.peers[n.ID()]; ok { if _, ok := d.peers[n.ID()]; ok {
return errAlreadyConnected return errAlreadyConnected
} }
if _, ok := d.pendingInbound[n.ID()]; ok {
return errPendingInbound
}
if d.netRestrict != nil && !d.netRestrict.ContainsAddr(n.IPAddr()) { if d.netRestrict != nil && !d.netRestrict.ContainsAddr(n.IPAddr()) {
return errNetRestrict return errNetRestrict
} }

View file

@ -423,6 +423,82 @@ func TestDialSchedDNSHostname(t *testing.T) {
}) })
} }
// This test checks that nodes with pending inbound connections are not dialed.
func TestDialSchedPendingInbound(t *testing.T) {
t.Parallel()
config := dialConfig{
maxActiveDials: 5,
maxDialPeers: 4,
}
runDialTest(t, config, []dialTestRound{
// 2 peers are connected, leaving 2 dial slots.
// Node 0x03 has a pending inbound connection.
// Discovered nodes 0x03, 0x04, 0x05 but only 0x04 and 0x05 should be dialed.
{
peersAdded: []*conn{
{flags: dynDialedConn, node: newNode(uintID(0x01), "127.0.0.1:30303")},
{flags: dynDialedConn, node: newNode(uintID(0x02), "127.0.0.2:30303")},
},
update: func(d *dialScheduler) {
d.inboundPending(uintID(0x03))
},
discovered: []*enode.Node{
newNode(uintID(0x03), "127.0.0.3:30303"), // not dialed because pending inbound
newNode(uintID(0x04), "127.0.0.4:30303"),
newNode(uintID(0x05), "127.0.0.5:30303"),
},
wantNewDials: []*enode.Node{
newNode(uintID(0x04), "127.0.0.4:30303"),
newNode(uintID(0x05), "127.0.0.5:30303"),
},
},
// Pending inbound connection for 0x03 completes successfully.
// Node 0x03 becomes a connected peer.
// One dial slot remains, node 0x06 is dialed.
{
update: func(d *dialScheduler) {
// Pending inbound completes
d.inboundCompleted(uintID(0x03))
},
peersAdded: []*conn{
{flags: inboundConn, node: newNode(uintID(0x03), "127.0.0.3:30303")},
},
succeeded: []enode.ID{
uintID(0x04),
},
failed: []enode.ID{
uintID(0x05),
},
discovered: []*enode.Node{
newNode(uintID(0x03), "127.0.0.3:30303"), // not dialed, now connected
newNode(uintID(0x06), "127.0.0.6:30303"),
},
wantNewDials: []*enode.Node{
newNode(uintID(0x06), "127.0.0.6:30303"),
},
},
// Inbound peer 0x03 disconnects.
// Another pending inbound starts for 0x07.
// Only 0x03 should be dialed, not 0x07.
{
peersRemoved: []enode.ID{
uintID(0x03),
},
update: func(d *dialScheduler) {
d.inboundPending(uintID(0x07))
},
discovered: []*enode.Node{
newNode(uintID(0x03), "127.0.0.3:30303"),
newNode(uintID(0x07), "127.0.0.7:30303"), // not dialed because pending inbound
},
wantNewDials: []*enode.Node{
newNode(uintID(0x03), "127.0.0.3:30303"),
},
},
})
}
// ------- // -------
// Code below here is the framework for the tests above. // Code below here is the framework for the tests above.

View file

@ -686,8 +686,11 @@ running:
// Ensure that the trusted flag is set before checking against MaxPeers. // Ensure that the trusted flag is set before checking against MaxPeers.
c.flags |= trustedConn c.flags |= trustedConn
} }
// TODO: track in-progress inbound node IDs (pre-Peer) to avoid dialing them. err := srv.postHandshakeChecks(peers, inboundCount, c)
c.cont <- srv.postHandshakeChecks(peers, inboundCount, c) if err == nil && c.flags&inboundConn != 0 {
srv.dialsched.inboundPending(c.node.ID())
}
c.cont <- err
case c := <-srv.checkpointAddPeer: case c := <-srv.checkpointAddPeer:
// At this point the connection is past the protocol handshake. // At this point the connection is past the protocol handshake.
@ -870,6 +873,11 @@ func (srv *Server) checkInboundConn(remoteIP netip.Addr) error {
// or the handshakes have failed. // or the handshakes have failed.
func (srv *Server) SetupConn(fd net.Conn, flags connFlag, dialDest *enode.Node) error { func (srv *Server) SetupConn(fd net.Conn, flags connFlag, dialDest *enode.Node) error {
c := &conn{fd: fd, flags: flags, cont: make(chan error)} c := &conn{fd: fd, flags: flags, cont: make(chan error)}
defer func() {
if c.is(inboundConn) && c.node != nil {
srv.dialsched.inboundCompleted(c.node.ID())
}
}()
if dialDest == nil { if dialDest == nil {
c.transport = srv.newTransport(fd, nil) c.transport = srv.newTransport(fd, nil)
} else { } else {

View file

@ -222,3 +222,11 @@ var (
ConsolidationQueueAddress = common.HexToAddress("0x0000BBdDc7CE488642fb579F8B00f3a590007251") ConsolidationQueueAddress = common.HexToAddress("0x0000BBdDc7CE488642fb579F8B00f3a590007251")
ConsolidationQueueCode = common.FromHex("3373fffffffffffffffffffffffffffffffffffffffe1460d35760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1461019a57600182026001905f5b5f82111560685781019083028483029004916001019190604d565b9093900492505050366060146088573661019a573461019a575f5260205ff35b341061019a57600154600101600155600354806004026004013381556001015f358155600101602035815560010160403590553360601b5f5260605f60143760745fa0600101600355005b6003546002548082038060021160e7575060025b5f5b8181146101295782810160040260040181607402815460601b815260140181600101548152602001816002015481526020019060030154905260010160e9565b910180921461013b5790600255610146565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff141561017357505f5b6001546001828201116101885750505f61018e565b01600190035b5f555f6001556074025ff35b5f5ffd") ConsolidationQueueCode = common.FromHex("3373fffffffffffffffffffffffffffffffffffffffe1460d35760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1461019a57600182026001905f5b5f82111560685781019083028483029004916001019190604d565b9093900492505050366060146088573661019a573461019a575f5260205ff35b341061019a57600154600101600155600354806004026004013381556001015f358155600101602035815560010160403590553360601b5f5260605f60143760745fa0600101600355005b6003546002548082038060021160e7575060025b5f5b8181146101295782810160040260040181607402815460601b815260140181600101548152602001816002015481526020019060030154905260010160e9565b910180921461013b5790600255610146565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff141561017357505f5b6001546001828201116101885750505f61018e565b01600190035b5f555f6001556074025ff35b5f5ffd")
) )
// System log events.
var (
// EIP-7708 - System logs emitted for ETH transfer and burn
EthTransferLogEvent = common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef") // keccak256('Transfer(address,address,uint256)')
EthBurnLogEvent = common.HexToHash("0xcc16f5dbb4873280815c1ee09dbd06736cffcc184412cf7a71a0fdb75d397ca5") // keccak256('Burn(address,uint256)')
)

View file

@ -366,6 +366,16 @@ func (w *EncoderBuffer) AppendToBytes(dst []byte) []byte {
return out return out
} }
// Size returns the total size of the content that was encoded up to this point.
// Note this does not count the size of any lists which are still 'open' (i.e. for
// which ListEnd has not been called yet).
func (w EncoderBuffer) Size() int {
if w.buf == nil {
return 0
}
return w.buf.size()
}
// Write appends b directly to the encoder output. // Write appends b directly to the encoder output.
func (w EncoderBuffer) Write(b []byte) (int, error) { func (w EncoderBuffer) Write(b []byte) (int, error) {
return w.buf.Write(b) return w.buf.Write(b)

View file

@ -507,6 +507,39 @@ func TestEncodeToReaderReturnToPool(t *testing.T) {
wg.Wait() wg.Wait()
} }
func TestEncoderBufferSize(t *testing.T) {
var output bytes.Buffer
eb := NewEncoderBuffer(&output)
assertSize := func(state string, expectedSize int) {
t.Helper()
if s := eb.Size(); s != expectedSize {
t.Fatalf("wrong size %s: %d", state, s)
}
}
assertSize("empty buffer", 0)
outerList := eb.List()
assertSize("after outer List()", 0)
eb.WriteString("abc")
assertSize("after string write", 4)
innerList := eb.List()
assertSize("after inner List()", 4)
eb.WriteUint64(1)
eb.WriteUint64(2)
assertSize("after inner list writes", 6)
eb.ListEnd(innerList)
assertSize("after end of inner list", 7)
eb.ListEnd(outerList)
assertSize("after end of outer list", 8)
eb.Flush()
assertSize("after Flush()", 0)
if output.Len() != 8 {
t.Fatalf("wrong final output size %d", output.Len())
}
}
var sink interface{} var sink interface{}
func BenchmarkIntsize(b *testing.B) { func BenchmarkIntsize(b *testing.B) {

View file

@ -168,6 +168,18 @@ func (r *RawList[T]) AppendRaw(b []byte) error {
return nil return nil
} }
// AppendList appends all items from another RawList to this list.
func (r *RawList[T]) AppendList(other *RawList[T]) {
if other.enc == nil || other.length == 0 {
return
}
if r.enc == nil {
r.enc = make([]byte, 9)
}
r.enc = append(r.enc, other.Content()...)
r.length += other.length
}
// StringSize returns the encoded size of a string. // StringSize returns the encoded size of a string.
func StringSize(s string) uint64 { func StringSize(s string) uint64 {
switch n := len(s); n { switch n := len(s); n {

View file

@ -246,6 +246,54 @@ func TestRawListAppendRaw(t *testing.T) {
t.Fatalf("wrong Len %d after invalid appends, want 2", rl.Len()) t.Fatalf("wrong Len %d after invalid appends, want 2", rl.Len())
} }
} }
func TestRawListAppendList(t *testing.T) {
var rl1 RawList[uint64]
if err := rl1.Append(uint64(1)); err != nil {
t.Fatal("append 1 failed:", err)
}
if err := rl1.Append(uint64(2)); err != nil {
t.Fatal("append 2 failed:", err)
}
var rl2 RawList[uint64]
if err := rl2.Append(uint64(3)); err != nil {
t.Fatal("append 3 failed:", err)
}
if err := rl2.Append(uint64(4)); err != nil {
t.Fatal("append 4 failed:", err)
}
rl1.AppendList(&rl2)
if rl1.Len() != 4 {
t.Fatalf("wrong Len %d, want 4", rl1.Len())
}
if rl1.Size() != 5 {
t.Fatalf("wrong Size %d, want 5", rl1.Size())
}
items, err := rl1.Items()
if err != nil {
t.Fatal("Items failed:", err)
}
if !reflect.DeepEqual(items, []uint64{1, 2, 3, 4}) {
t.Fatalf("wrong items: %v", items)
}
var empty RawList[uint64]
prevLen := rl1.Len()
rl1.AppendList(&empty)
if rl1.Len() != prevLen {
t.Fatalf("appending empty list changed Len: got %d, want %d", rl1.Len(), prevLen)
}
empty.AppendList(&rl1)
if empty.Len() != 4 {
t.Fatalf("wrong Len %d, want 4", empty.Len())
}
}
func TestRawListDecodeInvalid(t *testing.T) { func TestRawListDecodeInvalid(t *testing.T) {
tests := []struct { tests := []struct {

View file

@ -17,12 +17,33 @@
package bintrie package bintrie
import ( import (
"crypto/sha256"
"errors" "errors"
"fmt" "fmt"
"math/bits"
"runtime"
"sync"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
) )
// parallelDepth returns the tree depth below which Hash() spawns goroutines.
func parallelDepth() int {
return min(bits.Len(uint(runtime.NumCPU())), 8)
}
// isDirty reports whether a BinaryNode child needs rehashing.
func isDirty(n BinaryNode) bool {
switch v := n.(type) {
case *InternalNode:
return v.mustRecompute
case *StemNode:
return v.mustRecompute
default:
return false
}
}
func keyToPath(depth int, key []byte) ([]byte, error) { func keyToPath(depth int, key []byte) ([]byte, error) {
if depth > 31*8 { if depth > 31*8 {
return nil, errors.New("node too deep") return nil, errors.New("node too deep")
@ -124,6 +145,29 @@ func (bt *InternalNode) Hash() common.Hash {
return bt.hash return bt.hash
} }
// At shallow depths, parallelize when both children need rehashing:
// hash left subtree in a goroutine, right subtree inline, then combine.
// Skip goroutine overhead when only one child is dirty (common case
// for narrow state updates that touch a single path through the trie).
if bt.depth < parallelDepth() && isDirty(bt.left) && isDirty(bt.right) {
var input [64]byte
var lh common.Hash
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
lh = bt.left.Hash()
}()
rh := bt.right.Hash()
copy(input[32:], rh[:])
wg.Wait()
copy(input[:32], lh[:])
bt.hash = sha256.Sum256(input[:])
bt.mustRecompute = false
return bt.hash
}
// Deeper nodes: sequential using pooled hasher (goroutine overhead > hash cost)
h := newSha256() h := newSha256()
defer returnSha256(h) defer returnSha256(h)
if bt.left != nil { if bt.left != nil {

View file

@ -119,10 +119,17 @@ func (it *binaryNodeIterator) Next(descend bool) bool {
return it.Next(descend) return it.Next(descend)
case HashedNode: case HashedNode:
// resolve the node // resolve the node
data, err := it.trie.nodeResolver(it.Path(), common.Hash(node)) resolverPath := it.Path()
data, err := it.trie.nodeResolver(resolverPath, common.Hash(node))
if err != nil { if err != nil {
panic(err) panic(err)
} }
if data == nil {
// Empty/nil node — treat as Empty, backtrack
it.current = Empty{}
it.stack[len(it.stack)-1].Node = it.current
return it.Next(descend)
}
it.current, err = DeserializeNodeWithHash(data, len(it.stack)-1, common.Hash(node)) it.current, err = DeserializeNodeWithHash(data, len(it.stack)-1, common.Hash(node))
if err != nil { if err != nil {
panic(err) panic(err)
@ -130,16 +137,25 @@ func (it *binaryNodeIterator) Next(descend bool) bool {
// update the stack and parent with the resolved node // update the stack and parent with the resolved node
it.stack[len(it.stack)-1].Node = it.current it.stack[len(it.stack)-1].Node = it.current
parent := &it.stack[len(it.stack)-2] if len(it.stack) >= 2 {
if parent.Index == 0 { parent := &it.stack[len(it.stack)-2]
parent.Node.(*InternalNode).left = it.current if parent.Index == 0 {
} else { parent.Node.(*InternalNode).left = it.current
parent.Node.(*InternalNode).right = it.current } else {
parent.Node.(*InternalNode).right = it.current
}
} }
return it.Next(descend) return it.Next(descend)
case Empty: case Empty:
// do nothing // Empty node - go back to parent and continue
return false if len(it.stack) <= 1 {
it.lastErr = errIteratorEnd
return false
}
it.stack = it.stack[:len(it.stack)-1]
it.current = it.stack[len(it.stack)-1].Node
it.stack[len(it.stack)-1].Index++
return it.Next(descend)
default: default:
panic("invalid node type") panic("invalid node type")
} }

View file

@ -0,0 +1,239 @@
// Copyright 2026 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 bintrie
import (
"bytes"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/trie"
)
// makeTrie creates a BinaryTrie populated with the given key-value pairs.
func makeTrie(t *testing.T, entries [][2]common.Hash) *BinaryTrie {
t.Helper()
tr := &BinaryTrie{
root: NewBinaryNode(),
tracer: trie.NewPrevalueTracer(),
}
for _, kv := range entries {
var err error
tr.root, err = tr.root.Insert(kv[0][:], kv[1][:], nil, 0)
if err != nil {
t.Fatal(err)
}
}
return tr
}
// countLeaves iterates the trie and returns the number of leaves visited.
func countLeaves(t *testing.T, tr *BinaryTrie) int {
t.Helper()
it, err := newBinaryNodeIterator(tr, nil)
if err != nil {
t.Fatal(err)
}
leaves := 0
for it.Next(true) {
if it.Leaf() {
leaves++
}
}
if it.Error() != nil {
t.Fatalf("iterator error: %v", it.Error())
}
return leaves
}
// TestIteratorEmptyTrie verifies that iterating over an empty trie returns
// no nodes and reports no error.
func TestIteratorEmptyTrie(t *testing.T) {
tr := &BinaryTrie{
root: Empty{},
tracer: trie.NewPrevalueTracer(),
}
it, err := newBinaryNodeIterator(tr, nil)
if err != nil {
t.Fatal(err)
}
if it.Next(true) {
t.Fatal("expected no iteration over empty trie")
}
if it.Error() != nil {
t.Fatalf("unexpected error: %v", it.Error())
}
}
// TestIteratorSingleStem verifies iteration over a trie with a single stem
// node containing multiple values.
func TestIteratorSingleStem(t *testing.T) {
tr := makeTrie(t, [][2]common.Hash{
{common.HexToHash("0000000000000000000000000000000000000000000000000000000000000003"), oneKey},
{common.HexToHash("0000000000000000000000000000000000000000000000000000000000000007"), oneKey},
{common.HexToHash("00000000000000000000000000000000000000000000000000000000000000FF"), oneKey},
})
if leaves := countLeaves(t, tr); leaves != 3 {
t.Fatalf("expected 3 leaves, got %d", leaves)
}
}
// TestIteratorTwoStems verifies iteration over a trie with two stems
// separated by internal nodes, ensuring all leaves from both stems are visited.
func TestIteratorTwoStems(t *testing.T) {
tr := makeTrie(t, [][2]common.Hash{
{common.HexToHash("0000000000000000000000000000000000000000000000000000000000000001"), oneKey},
{common.HexToHash("0000000000000000000000000000000000000000000000000000000000000002"), oneKey},
{common.HexToHash("8000000000000000000000000000000000000000000000000000000000000001"), oneKey},
{common.HexToHash("8000000000000000000000000000000000000000000000000000000000000002"), oneKey},
})
if leaves := countLeaves(t, tr); leaves != 4 {
t.Fatalf("expected 4 leaves, got %d", leaves)
}
}
// TestIteratorLeafKeyAndBlob verifies that the iterator returns correct
// leaf keys and values.
func TestIteratorLeafKeyAndBlob(t *testing.T) {
key := common.HexToHash("0000000000000000000000000000000000000000000000000000000000000005")
val := common.HexToHash("00000000000000000000000000000000000000000000000000000000deadbeef")
tr := makeTrie(t, [][2]common.Hash{{key, val}})
it, err := newBinaryNodeIterator(tr, nil)
if err != nil {
t.Fatal(err)
}
found := false
for it.Next(true) {
if it.Leaf() {
found = true
if !bytes.Equal(it.LeafKey(), key[:]) {
t.Fatalf("leaf key mismatch: got %x, want %x", it.LeafKey(), key)
}
if !bytes.Equal(it.LeafBlob(), val[:]) {
t.Fatalf("leaf blob mismatch: got %x, want %x", it.LeafBlob(), val)
}
}
}
if !found {
t.Fatal("expected to find a leaf")
}
}
// TestIteratorEmptyNodeBacktrack is a regression test for the Empty node
// backtracking bug. Before the fix, encountering an Empty child during
// iteration would terminate the walk prematurely instead of backtracking
// to the parent and continuing with the next sibling.
func TestIteratorEmptyNodeBacktrack(t *testing.T) {
tr := makeTrie(t, [][2]common.Hash{
{common.HexToHash("0000000000000000000000000000000000000000000000000000000000000001"), oneKey},
{common.HexToHash("8000000000000000000000000000000000000000000000000000000000000001"), oneKey},
})
if _, ok := tr.root.(*InternalNode); !ok {
t.Fatalf("expected InternalNode root, got %T", tr.root)
}
if leaves := countLeaves(t, tr); leaves != 2 {
t.Fatalf("expected 2 leaves, got %d (Empty backtrack bug?)", leaves)
}
}
// TestIteratorHashedNodeNilData is a regression test for the nil-data guard.
// When nodeResolver encounters a zero-hash HashedNode, it returns (nil, nil).
// The iterator should treat this as Empty and continue rather than panicking.
func TestIteratorHashedNodeNilData(t *testing.T) {
tr := makeTrie(t, [][2]common.Hash{
{common.HexToHash("0000000000000000000000000000000000000000000000000000000000000001"), oneKey},
{common.HexToHash("8000000000000000000000000000000000000000000000000000000000000001"), oneKey},
})
root, ok := tr.root.(*InternalNode)
if !ok {
t.Fatalf("expected InternalNode root, got %T", tr.root)
}
// Replace right child with a zero-hash HashedNode. nodeResolver
// short-circuits on common.Hash{} and returns (nil, nil), which
// triggers the nil-data guard in the iterator.
root.right = HashedNode(common.Hash{})
// Should not panic; the zero-hash right child should be treated as Empty.
if leaves := countLeaves(t, tr); leaves != 1 {
t.Fatalf("expected 1 leaf (zero-hash right node skipped), got %d", leaves)
}
}
// TestIteratorManyStems verifies iteration correctness with many stems,
// producing a deep tree structure.
func TestIteratorManyStems(t *testing.T) {
entries := make([][2]common.Hash, 16)
for i := range entries {
var key common.Hash
key[0] = byte(i << 4)
key[31] = 1
entries[i] = [2]common.Hash{key, oneKey}
}
tr := makeTrie(t, entries)
if leaves := countLeaves(t, tr); leaves != 16 {
t.Fatalf("expected 16 leaves, got %d", leaves)
}
}
// TestIteratorDeepTree verifies iteration over a trie with stems that share
// a long common prefix, producing many intermediate InternalNodes.
func TestIteratorDeepTree(t *testing.T) {
tr := makeTrie(t, [][2]common.Hash{
{common.HexToHash("0000000000C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0"), oneKey},
{common.HexToHash("0000000000E00000000000000000000000000000000000000000000000000000"), twoKey},
})
if leaves := countLeaves(t, tr); leaves != 2 {
t.Fatalf("expected 2 leaves in deep tree, got %d", leaves)
}
}
// TestIteratorNodeCount verifies the total number of Next(true) calls
// for a known tree structure.
func TestIteratorNodeCount(t *testing.T) {
tr := makeTrie(t, [][2]common.Hash{
{common.HexToHash("0000000000000000000000000000000000000000000000000000000000000001"), oneKey},
{common.HexToHash("8000000000000000000000000000000000000000000000000000000000000001"), oneKey},
})
it, err := newBinaryNodeIterator(tr, nil)
if err != nil {
t.Fatal(err)
}
total := 0
leaves := 0
for it.Next(true) {
total++
if it.Leaf() {
leaves++
}
}
if leaves != 2 {
t.Fatalf("expected 2 leaves, got %d", leaves)
}
// Root(InternalNode) + leaf1 (from left StemNode) + leaf2 (from right StemNode) = 3
// StemNodes are not returned as separate steps; the iterator advances
// directly to the first non-nil value within the stem.
if total != 3 {
t.Fatalf("expected 3 total nodes, got %d", total)
}
}

View file

@ -36,6 +36,18 @@ func NewLevelStats() *LevelStats {
return &LevelStats{} return &LevelStats{}
} }
// Copy returns a deep copy of the statistics.
func (s *LevelStats) Copy() *LevelStats {
cpy := NewLevelStats()
for i := range s.level {
cpy.level[i].short.Store(s.level[i].short.Load())
cpy.level[i].full.Store(s.level[i].full.Load())
cpy.level[i].value.Store(s.level[i].value.Load())
cpy.level[i].size.Store(s.level[i].size.Load())
}
return cpy
}
// MaxDepth iterates each level and finds the deepest level with at least one // MaxDepth iterates each level and finds the deepest level with at least one
// trie node. // trie node.
func (s *LevelStats) MaxDepth() int { func (s *LevelStats) MaxDepth() int {