From 6138a11c39aa162dd723518d6edba57cd538a867 Mon Sep 17 00:00:00 2001
From: CPerezz <37264926+CPerezz@users.noreply.github.com>
Date: Wed, 18 Mar 2026 13:54:23 +0100
Subject: [PATCH 01/22] trie/bintrie: parallelize InternalNode.Hash at shallow
tree depths (#34032)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
At tree depths below `log2(NumCPU)` (clamped to [2, 8]), hash the left
subtree in a goroutine while hashing the right subtree inline. This
exploits available CPU cores for the top levels of the tree where
subtree hashing is most expensive. On single-core machines, the parallel
path is disabled entirely.
Deeper nodes use sequential hashing with the existing `sync.Pool` hasher
where goroutine overhead would exceed the hash computation cost. The
parallel path uses `sha256.Sum256` with a stack-allocated buffer to
avoid pool contention across goroutines.
**Safety:**
- Left/right subtrees are disjoint — no shared mutable state
- `sync.WaitGroup` provides happens-before guarantee for the result
- `defer wg.Done()` + `recover()` prevents goroutine panics from
crashing the process
- `!bt.mustRecompute` early return means clean nodes never enter the
parallel path
- Hash results are deterministic regardless of computation order — no
consensus risk
## Benchmark (AMD EPYC 48-core, 500K entries, `--benchtime=10s
--count=3`, post-H01 baseline)
| Metric | Baseline | Parallel | Delta |
|--------|----------|----------|-------|
| Approve (Mgas/s) | 224.5 ± 7.1 | **259.6 ± 2.4** | **+15.6%** |
| BalanceOf (Mgas/s) | 982.9 ± 5.1 | 954.3 ± 10.8 | -2.9% (noise, clean
nodes skip parallel path) |
| Allocs/op (approve) | ~810K | ~700K | -13.6% |
---
trie/bintrie/internal_node.go | 44 +++++++++++++++++++++++++++++++++++
1 file changed, 44 insertions(+)
diff --git a/trie/bintrie/internal_node.go b/trie/bintrie/internal_node.go
index 7ad76aa9db..946203bcfb 100644
--- a/trie/bintrie/internal_node.go
+++ b/trie/bintrie/internal_node.go
@@ -17,12 +17,33 @@
package bintrie
import (
+ "crypto/sha256"
"errors"
"fmt"
+ "math/bits"
+ "runtime"
+ "sync"
"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) {
if depth > 31*8 {
return nil, errors.New("node too deep")
@@ -124,6 +145,29 @@ func (bt *InternalNode) Hash() common.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()
defer returnSha256(h)
if bt.left != nil {
From 6ae3f9fa562f28e805e6a5c20f0e42c1efc7d729 Mon Sep 17 00:00:00 2001
From: Sina M <1591639+s1na@users.noreply.github.com>
Date: Wed, 18 Mar 2026 13:54:29 +0100
Subject: [PATCH 02/22] core/history: refactor pruning configuration (#34036)
This PR introduces a new type HistoryPolicy which captures user intent
as opposed to pruning point stored in the blockchain which persists the
actual tail of data in the database.
It is in preparation for the rolling history expiry feature.
It comes with a semantic change: if database was pruned and geth is
running without a history mode flag (or explicit keep all flag) geth
will emit a warning but continue running as opposed to stopping the
world.
---
cmd/geth/chaincmd.go | 11 +--
cmd/workload/testsuite.go | 8 ++-
core/blockchain.go | 113 ++++++++++---------------------
core/blockchain_test.go | 16 +----
core/history/historymode.go | 81 +++++++++++-----------
core/history/historymode_test.go | 58 ++++++++++++++++
eth/backend.go | 9 ++-
7 files changed, 159 insertions(+), 137 deletions(-)
create mode 100644 core/history/historymode_test.go
diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go
index 7e14ec1c60..1084100f39 100644
--- a/cmd/geth/chaincmd.go
+++ b/cmd/geth/chaincmd.go
@@ -731,13 +731,16 @@ func pruneHistory(ctx *cli.Context) error {
// Determine the prune point based on the history mode.
genesisHash := chain.Genesis().Hash()
- prunePoint := history.GetPrunePoint(genesisHash, mode)
- if prunePoint == nil {
+ policy, err := history.NewPolicy(mode, genesisHash)
+ if err != nil {
+ return err
+ }
+ if policy.Target == nil {
return fmt.Errorf("prune point for %q not found for this network", mode.String())
}
var (
- targetBlock = prunePoint.BlockNumber
- targetBlockHash = prunePoint.BlockHash
+ targetBlock = policy.Target.BlockNumber
+ targetBlockHash = policy.Target.BlockHash
)
// Check the current freezer tail to see if pruning is needed/possible.
diff --git a/cmd/workload/testsuite.go b/cmd/workload/testsuite.go
index 80cbd15352..4e33522f1b 100644
--- a/cmd/workload/testsuite.go
+++ b/cmd/workload/testsuite.go
@@ -155,7 +155,9 @@ func testConfigFromCLI(ctx *cli.Context) (cfg testConfig) {
}
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):
cfg.fsys = builtinTestFiles
if ctx.IsSet(filterQueryFileFlag.Name) {
@@ -180,7 +182,9 @@ func testConfigFromCLI(ctx *cli.Context) (cfg testConfig) {
}
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:
cfg.fsys = os.DirFS(".")
cfg.filterQueryFile = ctx.String(filterQueryFileFlag.Name)
diff --git a/core/blockchain.go b/core/blockchain.go
index 42a8405ec9..1b45a5ac39 100644
--- a/core/blockchain.go
+++ b/core/blockchain.go
@@ -194,9 +194,8 @@ type BlockChainConfig struct {
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
- // This defines the cutoff block for history expiry.
- // Blocks before this number may be unavailable in the chain database.
- ChainHistoryMode history.HistoryMode
+ // HistoryPolicy defines the chain history pruning intent.
+ HistoryPolicy history.HistoryPolicy
// Misc options
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!
func DefaultConfig() *BlockChainConfig {
return &BlockChainConfig{
- TrieCleanLimit: 256,
- TrieDirtyLimit: 256,
- TrieTimeLimit: 5 * time.Minute,
- StateScheme: rawdb.HashScheme,
- SnapshotLimit: 256,
- SnapshotWait: true,
- ChainHistoryMode: history.KeepAll,
+ TrieCleanLimit: 256,
+ TrieDirtyLimit: 256,
+ TrieTimeLimit: 5 * time.Minute,
+ StateScheme: rawdb.HashScheme,
+ SnapshotLimit: 256,
+ SnapshotWait: true,
+ HistoryPolicy: history.HistoryPolicy{Mode: history.KeepAll},
// Transaction indexing is disabled by default.
// This is appropriate for most unit tests.
TxLookupLimit: -1,
@@ -715,82 +714,44 @@ func (bc *BlockChain) loadLastState() error {
// initializeHistoryPruning sets bc.historyPrunePoint.
func (bc *BlockChain) initializeHistoryPruning(latest uint64) error {
- var (
- freezerTail, _ = bc.db.Tail()
- 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")
+ freezerTail, _ := bc.db.Tail()
+ policy := bc.cfg.HistoryPolicy
- case history.KeepPostMerge:
- if mergePoint == nil {
- return errors.New("history pruning requested for unknown network")
+ switch policy.Mode {
+ case history.KeepAll:
+ 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
- case history.KeepPostPrague:
- if praguePoint == nil {
- return errors.New("history pruning requested for unknown network")
- }
- // Check if already at the prague prune point.
- if freezerTail == praguePoint.BlockNumber {
- bc.historyPrunePoint.Store(praguePoint)
+ case history.KeepPostMerge, history.KeepPostPrague:
+ target := policy.Target
+ // Already at the target.
+ if freezerTail == target.BlockNumber {
+ bc.historyPrunePoint.Store(target)
return nil
}
- // Check if database needs pruning.
- if latest != 0 {
- if freezerTail == 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 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")
+ // Database is pruned beyond the target.
+ if freezerTail > target.BlockNumber {
+ return fmt.Errorf("database pruned beyond requested history (tail=%d, target=%d)", freezerTail, target.BlockNumber)
}
- // Fresh database (latest == 0), will sync from prague point.
- bc.historyPrunePoint.Store(praguePoint)
+ // Database needs pruning (freezerTail < target).
+ 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
default:
- return fmt.Errorf("invalid history mode: %d", bc.cfg.ChainHistoryMode)
+ return fmt.Errorf("invalid history mode: %d", policy.Mode)
}
}
diff --git a/core/blockchain_test.go b/core/blockchain_test.go
index ce592f0267..d3ca21b2b3 100644
--- a/core/blockchain_test.go
+++ b/core/blockchain_test.go
@@ -36,7 +36,6 @@ import (
"github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/consensus/beacon"
"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/state"
"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) {
// 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()
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{})
defer db.Close()
- options := DefaultConfig().WithStateScheme(rawdb.PathScheme)
- chain, _ := NewBlockChain(db, genesis, beacon.New(ethash.NewFaker()), options)
+ chain, _ := NewBlockChain(db, genesis, beacon.New(ethash.NewFaker()), DefaultConfig().WithStateScheme(rawdb.PathScheme))
defer chain.Stop()
var (
diff --git a/core/history/historymode.go b/core/history/historymode.go
index bdaf07826d..1adfe014b2 100644
--- a/core/history/historymode.go
+++ b/core/history/historymode.go
@@ -77,57 +77,62 @@ func (m *HistoryMode) UnmarshalText(text []byte) error {
return nil
}
+// PrunePoint identifies a specific block for history pruning.
type PrunePoint struct {
BlockNumber uint64
BlockHash common.Hash
}
-// MergePrunePoints contains the pre-defined history pruning cutoff blocks for known networks.
-// They point to the first post-merge block. Any pruning should truncate *up to* but excluding
-// the given block.
-var MergePrunePoints = map[common.Hash]*PrunePoint{
- // mainnet
- params.MainnetGenesisHash: {
- BlockNumber: 15537393,
- BlockHash: common.HexToHash("0x55b11b918355b1ef9c5db810302ebad0bf2544255b530cdce90674d5887bb286"),
+// staticPrunePoints contains the pre-defined history pruning cutoff blocks for
+// known networks, keyed by history mode and genesis hash. They point to the first
+// block after the respective fork. Any pruning should truncate *up to* but
+// excluding the given block.
+var staticPrunePoints = map[HistoryMode]map[common.Hash]*PrunePoint{
+ KeepPostMerge: {
+ params.MainnetGenesisHash: {
+ BlockNumber: 15537393,
+ BlockHash: common.HexToHash("0x55b11b918355b1ef9c5db810302ebad0bf2544255b530cdce90674d5887bb286"),
+ },
+ params.SepoliaGenesisHash: {
+ BlockNumber: 1450409,
+ BlockHash: common.HexToHash("0x229f6b18ca1552f1d5146deceb5387333f40dc6275aebee3f2c5c4ece07d02db"),
+ },
},
- // sepolia
- params.SepoliaGenesisHash: {
- BlockNumber: 1450409,
- BlockHash: common.HexToHash("0x229f6b18ca1552f1d5146deceb5387333f40dc6275aebee3f2c5c4ece07d02db"),
+ KeepPostPrague: {
+ params.MainnetGenesisHash: {
+ BlockNumber: 22431084,
+ BlockHash: common.HexToHash("0x50c8cab760b2948349c590461b166773c45d8f4858cccf5a43025ab2960152e8"),
+ },
+ params.SepoliaGenesisHash: {
+ BlockNumber: 7836331,
+ BlockHash: common.HexToHash("0xe6571beb68bf24dbd8a6ba354518996920c55a3f8d8fdca423e391b8ad071f22"),
+ },
},
}
-// PraguePrunePoints contains the pre-defined history pruning cutoff blocks for the Prague
-// (Pectra) upgrade. They point to the first post-Prague block. Any pruning should truncate
-// *up to* but excluding the given block.
-var PraguePrunePoints = map[common.Hash]*PrunePoint{
- // mainnet - first Prague block (May 7, 2025)
- params.MainnetGenesisHash: {
- BlockNumber: 22431084,
- BlockHash: common.HexToHash("0x50c8cab760b2948349c590461b166773c45d8f4858cccf5a43025ab2960152e8"),
- },
- // sepolia - first Prague block (March 5, 2025)
- params.SepoliaGenesisHash: {
- BlockNumber: 7836331,
- BlockHash: common.HexToHash("0xe6571beb68bf24dbd8a6ba354518996920c55a3f8d8fdca423e391b8ad071f22"),
- },
+// HistoryPolicy describes the configured history pruning strategy. It captures
+// user intent as opposed to the actual DB state.
+type HistoryPolicy struct {
+ Mode HistoryMode
+ // Static prune point for PostMerge/PostPrague, nil otherwise.
+ Target *PrunePoint
}
-// PrunePoints is an alias for MergePrunePoints for backward compatibility.
-// Deprecated: Use GetPrunePoint or MergePrunePoints directly.
-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 {
+// NewPolicy constructs a HistoryPolicy from the given mode and genesis hash.
+func NewPolicy(mode HistoryMode, genesisHash common.Hash) (HistoryPolicy, error) {
switch mode {
- case KeepPostMerge:
- return MergePrunePoints[genesisHash]
- case KeepPostPrague:
- return PraguePrunePoints[genesisHash]
+ case KeepAll:
+ return HistoryPolicy{Mode: KeepAll}, nil
+
+ 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:
- return nil
+ return HistoryPolicy{}, fmt.Errorf("invalid history mode: %d", mode)
}
}
diff --git a/core/history/historymode_test.go b/core/history/historymode_test.go
new file mode 100644
index 0000000000..87eae188dd
--- /dev/null
+++ b/core/history/historymode_test.go
@@ -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 .
+
+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")
+ }
+}
diff --git a/eth/backend.go b/eth/backend.go
index 72228614f0..e9bea59734 100644
--- a/eth/backend.go
+++ b/eth/backend.go
@@ -33,6 +33,7 @@ import (
"github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/core"
"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/state/pruner"
"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.
// 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 {
return nil, err
}
@@ -220,6 +221,10 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
rawdb.WriteDatabaseVersion(chainDb, core.BlockChainVersion)
}
}
+ histPolicy, err := history.NewPolicy(config.HistoryMode, genesisHash)
+ if err != nil {
+ return nil, err
+ }
var (
options = &core.BlockChainConfig{
TrieCleanLimit: config.TrieCleanCache,
@@ -233,7 +238,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
TrienodeHistory: config.TrienodeHistory,
NodeFullValueCheckpoint: config.NodeFullValueCheckpoint,
StateScheme: scheme,
- ChainHistoryMode: config.HistoryMode,
+ HistoryPolicy: histPolicy,
TxLookupLimit: int64(min(config.TransactionHistory, math.MaxInt64)),
VmConfig: vm.Config{
EnablePreimageRecording: config.EnablePreimageRecording,
From b35645bdf7dfb2f0a22f14e8d278b9ec3cb1d48b Mon Sep 17 00:00:00 2001
From: haoyu-haoyu <85037553+haoyu-haoyu@users.noreply.github.com>
Date: Wed, 18 Mar 2026 12:56:26 +0000
Subject: [PATCH 03/22] build: fix missing '!' in shebang of generated oss-fuzz
scripts (#34044)
\`oss-fuzz.sh\` line 38 writes \`#/bin/sh\` instead of \`#!/bin/sh\` as
the shebang of generated fuzz test runner scripts.
\`\`\`diff
-#/bin/sh
+#!/bin/sh
\`\`\`
Without the \`!\`, the kernel does not recognize the interpreter
directive.
Co-authored-by: Claude Opus 4.6
---
oss-fuzz.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/oss-fuzz.sh b/oss-fuzz.sh
index bd87665125..73209fd8c8 100644
--- a/oss-fuzz.sh
+++ b/oss-fuzz.sh
@@ -35,7 +35,7 @@ function coverbuild {
sed -i -e 's/TestFuzzCorpus/Test'$function'Corpus/' ./"${function,,}"_test.go
cat << DOG > $OUT/$fuzzer
-#/bin/sh
+#!/bin/sh
cd $OUT/$path
go test -run Test${function}Corpus -v $tags -coverprofile \$1 -coverpkg $coverpkg
From 3341d8ace0dd85cb5aa90548269e307439ef8b35 Mon Sep 17 00:00:00 2001
From: vickkkkkyy
Date: Thu, 19 Mar 2026 06:31:40 +0800
Subject: [PATCH 04/22] eth/filters: rangeLogs should error on invalid block
range (#33763)
Fixes log filter to reject out of order block ranges.
---
eth/filters/filter.go | 2 +-
eth/filters/filter_test.go | 3 ++-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/eth/filters/filter.go b/eth/filters/filter.go
index 9915f28128..04e11f0475 100644
--- a/eth/filters/filter.go
+++ b/eth/filters/filter.go
@@ -390,7 +390,7 @@ func (f *Filter) rangeLogs(ctx context.Context, firstBlock, lastBlock uint64) ([
}
if firstBlock > lastBlock {
- return nil, nil
+ return nil, errInvalidBlockRange
}
mb := f.sys.backend.NewMatcherBackend()
defer mb.Close()
diff --git a/eth/filters/filter_test.go b/eth/filters/filter_test.go
index 63727200f7..e7b1b08046 100644
--- a/eth/filters/filter_test.go
+++ b/eth/filters/filter_test.go
@@ -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}]`,
},
{
- 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),
From 4faadf17fbc29d7890089acc660d553be454067a Mon Sep 17 00:00:00 2001
From: Bosul Mun
Date: Thu, 19 Mar 2026 17:51:03 +0900
Subject: [PATCH 05/22] rlp: add AppendList method to RawList (#34048)
This the AppendList method to merge two RawList instances by
appending the raw content.
---
rlp/raw.go | 12 ++++++++++++
rlp/raw_test.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 60 insertions(+)
diff --git a/rlp/raw.go b/rlp/raw.go
index 08ec667158..5f41cad5c4 100644
--- a/rlp/raw.go
+++ b/rlp/raw.go
@@ -168,6 +168,18 @@ func (r *RawList[T]) AppendRaw(b []byte) error {
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.
func StringSize(s string) uint64 {
switch n := len(s); n {
diff --git a/rlp/raw_test.go b/rlp/raw_test.go
index 112c5d7897..ed7d3524c2 100644
--- a/rlp/raw_test.go
+++ b/rlp/raw_test.go
@@ -246,6 +246,54 @@ func TestRawListAppendRaw(t *testing.T) {
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) {
tests := []struct {
From a3083ff5d0fdc8dec370a421ca4a7ad876e4fe08 Mon Sep 17 00:00:00 2001
From: rjl493456442
Date: Thu, 19 Mar 2026 16:52:10 +0800
Subject: [PATCH 06/22] cmd: add support for enumerating a single storage trie
(#34051)
---
cmd/geth/snapshot.go | 239 +++++++++++++++++++++++++++++++------------
cmd/utils/flags.go | 4 +
2 files changed, 179 insertions(+), 64 deletions(-)
diff --git a/cmd/geth/snapshot.go b/cmd/geth/snapshot.go
index fc0658a59c..c177fb5ea2 100644
--- a/cmd/geth/snapshot.go
+++ b/cmd/geth/snapshot.go
@@ -36,6 +36,7 @@ import (
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie"
+ "github.com/ethereum/go-ethereum/triedb"
"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",
ArgsUsage: "",
Action: traverseState,
- Flags: slices.Concat(utils.NetworkFlags, utils.DatabaseFlags),
+ Flags: slices.Concat([]cli.Flag{
+ utils.AccountFlag,
+ }, utils.NetworkFlags, utils.DatabaseFlags),
Description: `
geth snapshot traverse-state
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.
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",
ArgsUsage: "",
Action: traverseRawState,
- Flags: slices.Concat(utils.NetworkFlags, utils.DatabaseFlags),
+ Flags: slices.Concat([]cli.Flag{
+ utils.AccountFlag,
+ }, utils.NetworkFlags, utils.DatabaseFlags),
Description: `
geth snapshot traverse-rawstate
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.
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)
}
+// 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.
// Basically it just iterates the trie, ensure all nodes and associated
// contract codes are present.
@@ -309,6 +432,30 @@ func traverseState(ctx *cli.Context) error {
root = headBlock.Root()
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)
if err != nil {
log.Error("Failed to open trie", "root", root, "err", err)
@@ -335,30 +482,10 @@ func traverseState(ctx *cli.Context) error {
return err
}
if acc.Root != types.EmptyRootHash {
- id := trie.StorageTrieID(root, common.BytesToHash(accIter.Key), acc.Root)
- storageTrie, err := trie.NewStateTrie(id, triedb)
+ err := traverseStorage(trie.StorageTrieID(root, common.BytesToHash(accIter.Key), acc.Root), triedb, false, false)
if err != nil {
- log.Error("Failed to open storage trie", "root", acc.Root, "err", 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 !rawdb.HasCode(chaindb, common.BytesToHash(acc.CodeHash)) {
@@ -418,6 +545,30 @@ func traverseRawState(ctx *cli.Context) error {
root = headBlock.Root()
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)
if err != nil {
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")
}
if acc.Root != types.EmptyRootHash {
- id := trie.StorageTrieID(root, common.BytesToHash(accIter.LeafKey()), acc.Root)
- storageTrie, err := trie.NewStateTrie(id, triedb)
+ err := traverseStorage(trie.StorageTrieID(root, common.BytesToHash(accIter.LeafKey()), acc.Root), triedb, false, true)
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
}
- 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 !rawdb.HasCode(chaindb, common.BytesToHash(acc.CodeHash)) {
diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go
index 792e0e55ab..3a0bcc6b05 100644
--- a/cmd/utils/flags.go
+++ b/cmd/utils/flags.go
@@ -218,6 +218,10 @@ var (
Usage: "Max number of elements (0 = no limit)",
Value: 0,
}
+ AccountFlag = &cli.StringFlag{
+ Name: "account",
+ Usage: "Specifies the account address or hash to traverse a single storage trie",
+ }
OutputFileFlag = &cli.StringFlag{
Name: "output",
Usage: "Writes the result in json to the output",
From fd859638bd76d15b15468c9b2dea601035779769 Mon Sep 17 00:00:00 2001
From: jwasinger
Date: Thu, 19 Mar 2026 12:02:49 -0400
Subject: [PATCH 07/22] core/vm: rework gas measurement for call variants
(#33648)
EIP-7928 brings state reads into consensus by recording accounts and storage accessed during execution in the block access list. As part of the spec, we need to check that there is enough gas available to cover the cost component which doesn't depend on looking up state. If this component can't be covered by the available gas, we exit immediately.
The portion of the call dynamic cost which doesn't depend on state look ups:
- EIP2929 call costs
- value transfer cost
- memory expansion cost
This PR:
- breaks up the "inner" gas calculation for each call variant into a pair of stateless/stateful cost methods
- modifies the gas calculation logic of calls to check stateless cost component first, and go out of gas immediately if it is not covered.
---------
Co-authored-by: Gary Rong
---
core/vm/gas.go | 1 -
core/vm/gas_table.go | 106 ++++++++++++++++++--------------------
core/vm/operations_acl.go | 91 ++++++++++++++++++++------------
3 files changed, 108 insertions(+), 90 deletions(-)
diff --git a/core/vm/gas.go b/core/vm/gas.go
index 5fe589bce6..dcb20893c5 100644
--- a/core/vm/gas.go
+++ b/core/vm/gas.go
@@ -49,6 +49,5 @@ func callGas(isEip150 bool, availableGas, base uint64, callCost *uint256.Int) (u
if !callCost.IsUint64() {
return 0, ErrGasUintOverflow
}
-
return callCost.Uint64(), nil
}
diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go
index aa1ad918bb..f075a99468 100644
--- a/core/vm/gas_table.go
+++ b/core/vm/gas_table.go
@@ -373,7 +373,32 @@ func gasExpEIP158(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memor
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 (
gas uint64
transfersValue = !stack.Back(2).IsZero()
@@ -382,38 +407,40 @@ func gasCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize
if evm.readOnly && transfersValue {
return 0, ErrWriteProtection
}
-
- 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
- }
+ // Stateless check
memoryGas, err := memoryGasCost(mem, memorySize)
if err != nil {
return 0, err
}
+ var transferGas uint64
+ if transfersValue && !evm.chainRules.IsEIP4762 {
+ transferGas = params.CallValueTransferGas
+ }
var overflow bool
- if gas, overflow = math.SafeAdd(gas, memoryGas); overflow {
+ if gas, overflow = math.SafeAdd(memoryGas, transferGas); overflow {
return 0, ErrGasUintOverflow
}
-
- evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas, gas, stack.Back(0))
- 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.
+ if contract.Gas < gas {
+ 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 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)
if err != nil {
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 {
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
}
-func gasDelegateCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
- gas, err := 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 gasDelegateCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
+ return memoryGasCost(mem, memorySize)
}
-func gasStaticCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
- gas, err := 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 gasStaticCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
+ return memoryGasCost(mem, memorySize)
}
func gasSelfdestruct(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go
index ce394d9384..addd2b162f 100644
--- a/core/vm/operations_acl.go
+++ b/core/vm/operations_acl.go
@@ -256,10 +256,10 @@ func makeSelfdestructGasFn(refundsEnabled bool) gasFunc {
}
var (
- innerGasCallEIP7702 = makeCallVariantGasCallEIP7702(gasCall)
- gasDelegateCallEIP7702 = makeCallVariantGasCallEIP7702(gasDelegateCall)
- gasStaticCallEIP7702 = makeCallVariantGasCallEIP7702(gasStaticCall)
- gasCallCodeEIP7702 = makeCallVariantGasCallEIP7702(gasCallCode)
+ innerGasCallEIP7702 = makeCallVariantGasCallEIP7702(gasCallIntrinsic)
+ gasDelegateCallEIP7702 = makeCallVariantGasCallEIP7702(gasDelegateCallIntrinsic)
+ gasStaticCallEIP7702 = makeCallVariantGasCallEIP7702(gasStaticCallIntrinsic)
+ gasCallCodeEIP7702 = makeCallVariantGasCallEIP7702(gasCallCodeIntrinsic)
)
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)
}
-func makeCallVariantGasCallEIP7702(oldCalculator gasFunc) gasFunc {
+func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
var (
- total uint64 // total dynamic gas used
- addr = common.Address(stack.Back(1).Bytes20())
+ eip2929Cost uint64
+ eip7702Cost uint64
+ addr = common.Address(stack.Back(1).Bytes20())
)
-
- // Check slot presence in the access list
+ // Perform EIP-2929 checks (stateless), checking address presence
+ // in the accessList and charge the cold access accordingly.
if !evm.StateDB.AddressInAccessList(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
- coldCost := params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929
- // Charge the remaining difference here already, to correctly calculate available
- // gas for call
- if !contract.UseGas(coldCost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
+
+ // 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
+ eip2929Cost = params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929
+
+ // 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
}
- 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.
if target, ok := types.ParseDelegation(evm.StateDB.GetCode(addr)); ok {
- var cost uint64
if evm.StateDB.AddressInAccessList(target) {
- cost = params.WarmStorageReadCostEIP2929
+ eip7702Cost = params.WarmStorageReadCostEIP2929
} else {
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
}
- total += cost
}
-
- // Now call the old calculator, which takes into account
- // - create new account
- // - transfer value
- // - memory expansion
- // - 63/64ths rule
- old, err := oldCalculator(evm, contract, stack, mem, memorySize)
+ // Calculate the gas budget for the nested call. The costs defined by
+ // EIP-2929 and EIP-7702 have already been applied.
+ evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas, intrinsicCost, stack.Back(0))
if err != nil {
- return old, err
+ return 0, err
}
-
// Temporarily add the gas charge back to the contract and return value. By
// adding it to the return, it will be charged outside of this function, as
// part of the dynamic gas. This will ensure it is correctly reported to
// tracers.
- contract.Gas += total
+ contract.Gas += eip2929Cost + eip7702Cost
- var overflow bool
- if total, overflow = math.SafeAdd(old, total); overflow {
+ // Aggregate the gas costs from all components, including EIP-2929, EIP-7702,
+ // 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 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
}
}
From 35b91092c5b75399b15354da23f9433c574ce3bc Mon Sep 17 00:00:00 2001
From: Felix Lange
Date: Thu, 19 Mar 2026 18:26:00 +0100
Subject: [PATCH 08/22] rlp: add Size method to EncoderBuffer (#34052)
The new method returns the size of the written data, excluding any
unfinished list structure.
---
rlp/encbuffer.go | 10 ++++++++++
rlp/encode_test.go | 33 +++++++++++++++++++++++++++++++++
2 files changed, 43 insertions(+)
diff --git a/rlp/encbuffer.go b/rlp/encbuffer.go
index 61d8bd059c..ca0aa290fe 100644
--- a/rlp/encbuffer.go
+++ b/rlp/encbuffer.go
@@ -366,6 +366,16 @@ func (w *EncoderBuffer) AppendToBytes(dst []byte) []byte {
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.
func (w EncoderBuffer) Write(b []byte) (int, error) {
return w.buf.Write(b)
diff --git a/rlp/encode_test.go b/rlp/encode_test.go
index e63ea319b4..8dc9fdaf1f 100644
--- a/rlp/encode_test.go
+++ b/rlp/encode_test.go
@@ -507,6 +507,39 @@ func TestEncodeToReaderReturnToPool(t *testing.T) {
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{}
func BenchmarkIntsize(b *testing.B) {
From 59ce2cb6a14db04432857b2ae76b1889841bc4e6 Mon Sep 17 00:00:00 2001
From: jvn
Date: Fri, 20 Mar 2026 10:22:15 +0530
Subject: [PATCH 09/22] p2p: track in-progress inbound node IDs (#33198)
Avoid dialing a node while we have an inbound
connection request from them in progress.
Closes #33197
---
p2p/dial.go | 67 ++++++++++++++++++++++++++++++++----------
p2p/dial_test.go | 76 ++++++++++++++++++++++++++++++++++++++++++++++++
p2p/server.go | 12 ++++++--
3 files changed, 137 insertions(+), 18 deletions(-)
diff --git a/p2p/dial.go b/p2p/dial.go
index 225709427c..f9463d6d89 100644
--- a/p2p/dial.go
+++ b/p2p/dial.go
@@ -76,6 +76,7 @@ var (
errSelf = errors.New("is self")
errAlreadyDialing = errors.New("already dialing")
errAlreadyConnected = errors.New("already connected")
+ errPendingInbound = errors.New("peer has pending inbound connection")
errRecentlyDialed = errors.New("recently dialed")
errNetRestrict = errors.New("not contained in netrestrict list")
errNoPort = errors.New("node does not provide TCP port")
@@ -104,12 +105,15 @@ type dialScheduler struct {
remStaticCh chan *enode.Node
addPeerCh chan *conn
remPeerCh chan *conn
+ addPendingCh chan enode.ID
+ remPendingCh chan enode.ID
// Everything below here belongs to loop and
// should only be accessed by code on the loop goroutine.
- dialing map[enode.ID]*dialTask // active tasks
- peers map[enode.ID]struct{} // all connected peers
- dialPeers int // current number of dialed peers
+ dialing map[enode.ID]*dialTask // active tasks
+ peers map[enode.ID]struct{} // all connected 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
// (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 {
cfg := config.withDefaults()
d := &dialScheduler{
- dialConfig: cfg,
- historyTimer: mclock.NewAlarm(cfg.clock),
- setupFunc: setupFunc,
- dnsLookupFunc: net.DefaultResolver.LookupNetIP,
- dialing: make(map[enode.ID]*dialTask),
- static: make(map[enode.ID]*dialTask),
- peers: make(map[enode.ID]struct{}),
- doneCh: make(chan *dialTask),
- nodesIn: make(chan *enode.Node),
- addStaticCh: make(chan *enode.Node),
- remStaticCh: make(chan *enode.Node),
- addPeerCh: make(chan *conn),
- remPeerCh: make(chan *conn),
+ dialConfig: cfg,
+ historyTimer: mclock.NewAlarm(cfg.clock),
+ setupFunc: setupFunc,
+ dnsLookupFunc: net.DefaultResolver.LookupNetIP,
+ dialing: make(map[enode.ID]*dialTask),
+ static: make(map[enode.ID]*dialTask),
+ peers: make(map[enode.ID]struct{}),
+ pendingInbound: make(map[enode.ID]struct{}),
+ doneCh: make(chan *dialTask),
+ nodesIn: make(chan *enode.Node),
+ addStaticCh: make(chan *enode.Node),
+ remStaticCh: make(chan *enode.Node),
+ addPeerCh: make(chan *conn),
+ remPeerCh: make(chan *conn),
+ addPendingCh: make(chan enode.ID),
+ remPendingCh: make(chan enode.ID),
}
d.lastStatsLog = d.clock.Now()
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.
func (d *dialScheduler) loop(it enode.Iterator) {
var (
@@ -276,6 +299,15 @@ loop:
delete(d.peers, 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:
id := node.ID()
_, exists := d.static[id]
@@ -390,6 +422,9 @@ func (d *dialScheduler) checkDial(n *enode.Node) error {
if _, ok := d.peers[n.ID()]; ok {
return errAlreadyConnected
}
+ if _, ok := d.pendingInbound[n.ID()]; ok {
+ return errPendingInbound
+ }
if d.netRestrict != nil && !d.netRestrict.ContainsAddr(n.IPAddr()) {
return errNetRestrict
}
diff --git a/p2p/dial_test.go b/p2p/dial_test.go
index f18dacce2a..9684aa6e91 100644
--- a/p2p/dial_test.go
+++ b/p2p/dial_test.go
@@ -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.
diff --git a/p2p/server.go b/p2p/server.go
index 10c855f1c4..6d2323f9ce 100644
--- a/p2p/server.go
+++ b/p2p/server.go
@@ -686,8 +686,11 @@ running:
// Ensure that the trusted flag is set before checking against MaxPeers.
c.flags |= trustedConn
}
- // TODO: track in-progress inbound node IDs (pre-Peer) to avoid dialing them.
- c.cont <- srv.postHandshakeChecks(peers, inboundCount, c)
+ err := 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:
// 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.
func (srv *Server) SetupConn(fd net.Conn, flags connFlag, dialDest *enode.Node) 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 {
c.transport = srv.newTransport(fd, nil)
} else {
From 77779d1098c86d478768d8f2d0b6982ff2364d44 Mon Sep 17 00:00:00 2001
From: CPerezz <37264926+CPerezz@users.noreply.github.com>
Date: Fri, 20 Mar 2026 15:40:04 +0100
Subject: [PATCH 10/22] core/state: bypass per-account updateTrie in
IntermediateRoot for binary trie (#34022)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
In binary trie mode, `IntermediateRoot` calls `updateTrie()` once per
dirty account. But with the binary trie there is only one unified trie
(`OpenStorageTrie` returns `self`), so each call redundantly does
per-account trie setup: `getPrefetchedTrie`, `getTrie`, slice
allocations for deletions/used, and `prefetcher.used` — all for the same
trie pointer.
This PR replaces the per-account `updateTrie()` calls with a single flat
loop that applies all storage updates directly to `s.trie`. The MPT path
is unchanged. The prefetcher trie replacement is guarded to avoid
overwriting the binary trie that received updates.
This is the phase-1 counterpart to #34021 (H01). H01 fixes the commit
phase (`trie.Commit()` called N+1 times). This PR fixes the update phase
(`updateTrie()` called N times with redundant setup). Same root cause —
unified binary trie operated on per-account — different phases.
## Benchmark (Apple M4 Pro, 500K entries, `--benchtime=10s --count=3`,
on top of #34021)
| Metric | H01 baseline | H01 + this PR | Delta |
|--------|:------------:|:-------------:|:-----:|
| Approve (Mgas/s) | 368 | **414** | **+12.5%** |
| BalanceOf (Mgas/s) | 870 | 875 | +0.6% |
Should be rebased after #34021 is merged.
---
core/state/statedb.go | 71 +++++++++++++++++++++++++++++++------------
1 file changed, 52 insertions(+), 19 deletions(-)
diff --git a/core/state/statedb.go b/core/state/statedb.go
index 2477242eb5..02622f0bd6 100644
--- a/core/state/statedb.go
+++ b/core/state/statedb.go
@@ -824,22 +824,55 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
workers errgroup.Group
)
if s.db.TrieDB().IsVerkle() {
- // Whilst MPT storage tries are independent, Verkle has one single trie
- // for all the accounts and all the storage slots merged together. The
- // former can thus be simply parallelized, but updating the latter will
- // need concurrency support within the trie itself. That's a TODO for a
- // later time.
- workers.SetLimit(1)
- }
- for addr, op := range s.mutations {
- if op.applied || op.isDelete() {
- continue
+ // Bypass per-account updateTrie() for binary trie. In binary trie mode
+ // there is only one unified trie (OpenStorageTrie returns self), so the
+ // per-account trie setup in updateTrie() (getPrefetchedTrie, getTrie,
+ // prefetcher.used) is redundant overhead. Apply all storage updates
+ // directly in a single pass.
+ for addr, op := range s.mutations {
+ 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
- workers.Go(func() error {
- if s.db.TrieDB().IsVerkle() {
- obj.updateTrie()
- } else {
+ // Clear uncommittedStorage and assign trie on each touched object.
+ // obj.trie must be set because this path bypasses updateTrie(), which
+ // is where obj.trie normally gets lazily loaded via getTrie().
+ for addr, op := range s.mutations {
+ 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()
// If witness building is enabled and the state object has a trie,
@@ -847,9 +880,9 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
if s.witness != nil && obj.trie != nil {
s.witness.AddState(obj.trie.Witness())
}
- }
- return nil
- })
+ return nil
+ })
+ }
}
// If witness building is enabled, gather all the read-only accesses.
// Skip witness collection in Verkle mode, they will be gathered
@@ -911,7 +944,7 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
// only a single trie is used for state hashing. Replacing a non-nil verkle tree
// here could result in losing uncommitted changes from storage.
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 {
log.Error("Failed to retrieve account pre-fetcher trie")
} else {
From 305cd7b9eb1c2b2f9e555309bce8a389633ca923 Mon Sep 17 00:00:00 2001
From: Guillaume Ballet <3272758+gballet@users.noreply.github.com>
Date: Fri, 20 Mar 2026 18:53:14 +0100
Subject: [PATCH 11/22] trie/bintrie: fix NodeIterator Empty node handling and
expose tree accessors (#34056)
Fix three issues in the binary trie NodeIterator:
1. Empty nodes now properly backtrack to parent and continue iteration
instead of terminating the entire walk early.
2. `HashedNode` resolver handles `nil` data (all-zeros hash) gracefully
by treating it as Empty rather than panicking.
3. Parent update after node resolution guards against stack underflow
when resolving the root node itself.
---------
Co-authored-by: tellabg <249254436+tellabg@users.noreply.github.com>
---
trie/bintrie/iterator.go | 32 +++--
trie/bintrie/iterator_test.go | 239 ++++++++++++++++++++++++++++++++++
2 files changed, 263 insertions(+), 8 deletions(-)
create mode 100644 trie/bintrie/iterator_test.go
diff --git a/trie/bintrie/iterator.go b/trie/bintrie/iterator.go
index 917f82efc9..048d37f766 100644
--- a/trie/bintrie/iterator.go
+++ b/trie/bintrie/iterator.go
@@ -119,10 +119,17 @@ func (it *binaryNodeIterator) Next(descend bool) bool {
return it.Next(descend)
case HashedNode:
// 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 {
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))
if err != nil {
panic(err)
@@ -130,16 +137,25 @@ func (it *binaryNodeIterator) Next(descend bool) bool {
// update the stack and parent with the resolved node
it.stack[len(it.stack)-1].Node = it.current
- parent := &it.stack[len(it.stack)-2]
- if parent.Index == 0 {
- parent.Node.(*InternalNode).left = it.current
- } else {
- parent.Node.(*InternalNode).right = it.current
+ if len(it.stack) >= 2 {
+ parent := &it.stack[len(it.stack)-2]
+ if parent.Index == 0 {
+ parent.Node.(*InternalNode).left = it.current
+ } else {
+ parent.Node.(*InternalNode).right = it.current
+ }
}
return it.Next(descend)
case Empty:
- // do nothing
- return false
+ // Empty node - go back to parent and continue
+ 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:
panic("invalid node type")
}
diff --git a/trie/bintrie/iterator_test.go b/trie/bintrie/iterator_test.go
new file mode 100644
index 0000000000..3e717c07ba
--- /dev/null
+++ b/trie/bintrie/iterator_test.go
@@ -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 .
+
+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)
+ }
+}
From e23b0cbc2254b0af4a5d38e6295621de52689bc0 Mon Sep 17 00:00:00 2001
From: Lessa <230214854+adblesss@users.noreply.github.com>
Date: Mon, 23 Mar 2026 09:54:30 -0400
Subject: [PATCH 12/22] core/rawdb: fix key length check for num -- hash in db
inspect (#34074)
Fix incorrect key length calculation for `numHashPairings` in
`InspectDatabase`, introduced in #34000.
The `headerHashKey` format is `headerPrefix + num + headerHashSuffix`
(10 bytes), but the check incorrectly included `common.HashLength`,
expecting 42 bytes.
This caused all number -- hash entries to be misclassified as
unaccounted data.
---
core/rawdb/database.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/core/rawdb/database.go b/core/rawdb/database.go
index a13afb9c0e..945fd9097d 100644
--- a/core/rawdb/database.go
+++ b/core/rawdb/database.go
@@ -480,7 +480,7 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error {
receipts.add(size)
case bytes.HasPrefix(key, headerPrefix) && bytes.HasSuffix(key, headerTDSuffix) && len(key) == (len(headerPrefix)+8+common.HashLength+len(headerTDSuffix)):
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)
case bytes.HasPrefix(key, headerNumberPrefix) && len(key) == (len(headerNumberPrefix)+common.HashLength):
hashNumPairings.add(size)
From a61e5ccb1e343d9c28b85ba6851e7829517b98b1 Mon Sep 17 00:00:00 2001
From: Daniel Liu <139250065@qq.com>
Date: Mon, 23 Mar 2026 22:10:32 +0800
Subject: [PATCH 13/22] core, internal/ethapi: fix incorrect max-initcode RPC
error mapping (#34067)
Problem:
The max-initcode sentinel moved from core to vm, but RPC pre-check
mapping still depended on core.ErrMaxInitCodeSizeExceeded. This mismatch
could surface inconsistent error mapping when oversized initcode is
submitted through JSON-RPC.
Solution:
- Remove core.ErrMaxInitCodeSizeExceeded from the core pre-check error
set.
- Map max-initcode validation errors in RPC from
vm.ErrMaxInitCodeSizeExceeded.
- Keep the RPC error code mapping unchanged (-38025).
Impact:
- Restores consistent max-initcode error mapping after the sentinel
move.
- Preserves existing JSON-RPC client expectations for error code -38025.
- No consensus, state, or protocol behavior changes.
---
core/error.go | 4 ----
internal/ethapi/errors.go | 2 +-
2 files changed, 1 insertion(+), 5 deletions(-)
diff --git a/core/error.go b/core/error.go
index 4610842cee..7dd5b8a432 100644
--- a/core/error.go
+++ b/core/error.go
@@ -66,10 +66,6 @@ var (
// have enough funds for transfer(topmost call only).
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
// funds to cover the transfer, but not enough to pay for witness access/modification
// costs for the transaction
diff --git a/internal/ethapi/errors.go b/internal/ethapi/errors.go
index e406c36d6c..cc79af6f3c 100644
--- a/internal/ethapi/errors.go
+++ b/internal/ethapi/errors.go
@@ -141,7 +141,7 @@ func txValidationError(err error) *invalidTxError {
return &invalidTxError{Message: err.Error(), Code: errCodeIntrinsicGas}
case errors.Is(err, core.ErrInsufficientFundsForTransfer):
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{
From b87340a856573ab5f15577acc1dc563702e4fe14 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Felf=C3=B6ldi=20Zsolt?=
Date: Mon, 23 Mar 2026 15:29:53 +0100
Subject: [PATCH 14/22] core, core/vm: implement EIP-7708 (#33645)
This PR implements EIP-7708 according to the latest "rough consensus":
https://github.com/ethereum/EIPs/pull/9003
https://github.com/etan-status/EIPs/blob/fl-ethlogs/EIPS/eip-7708.md
---------
Co-authored-by: Jared Wasinger
Co-authored-by: raxhvl
Co-authored-by: Gary Rong
---
core/eth_transfer_logs_test.go | 169 +++++++++++++++++++++++++++++++++
core/evm.go | 6 +-
core/state/statedb.go | 35 +++++++
core/state/statedb_hooked.go | 4 +
core/state_transition.go | 3 +
core/types/log.go | 31 ++++++
core/vm/evm.go | 7 +-
core/vm/gas_table_test.go | 4 +-
core/vm/instructions.go | 10 +-
core/vm/interface.go | 1 +
core/vm/interpreter_test.go | 2 +-
params/protocol_params.go | 8 ++
12 files changed, 270 insertions(+), 10 deletions(-)
create mode 100644 core/eth_transfer_logs_test.go
diff --git a/core/eth_transfer_logs_test.go b/core/eth_transfer_logs_test.go
new file mode 100644
index 0000000000..815b56b588
--- /dev/null
+++ b/core/eth_transfer_logs_test.go
@@ -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 .
+
+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)
+ }
+ }
+}
diff --git a/core/evm.go b/core/evm.go
index 7430c0e21f..818b23bee5 100644
--- a/core/evm.go
+++ b/core/evm.go
@@ -25,6 +25,7 @@ import (
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
+ "github.com/ethereum/go-ethereum/params"
"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
-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.AddBalance(recipient, amount, tracing.BalanceChangeTransfer)
+ if rules.IsAmsterdam && !amount.IsZero() && sender != recipient {
+ db.AddLog(types.EthTransferLog(sender, recipient, amount))
+ }
}
diff --git a/core/state/statedb.go b/core/state/statedb.go
index 02622f0bd6..59ad3cdfef 100644
--- a/core/state/statedb.go
+++ b/core/state/statedb.go
@@ -743,6 +743,41 @@ func (s *StateDB) GetRefund() uint64 {
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
// 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.
diff --git a/core/state/statedb_hooked.go b/core/state/statedb_hooked.go
index 48794a3f41..8c217fba48 100644
--- a/core/state/statedb_hooked.go
+++ b/core/state/statedb_hooked.go
@@ -229,6 +229,10 @@ func (s *hookedStateDB) AddLog(log *types.Log) {
}
}
+func (s *hookedStateDB) EmitLogsForBurnAccounts() {
+ s.inner.EmitLogsForBurnAccounts()
+}
+
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 {
// Short circuit if no relevant hooks are set.
diff --git a/core/state_transition.go b/core/state_transition.go
index 6a40b4f7ab..52375bedaa 100644
--- a/core/state_transition.go
+++ b/core/state_transition.go
@@ -583,6 +583,9 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
st.evm.AccessEvents.AddAccount(st.evm.Context.Coinbase, true, math.MaxUint64)
}
}
+ if rules.IsAmsterdam {
+ st.evm.StateDB.EmitLogsForBurnAccounts()
+ }
return &ExecutionResult{
UsedGas: st.gasUsed(),
MaxUsedGas: peakGasUsed,
diff --git a/core/types/log.go b/core/types/log.go
index f0e6a3a745..487ca57b5a 100644
--- a/core/types/log.go
+++ b/core/types/log.go
@@ -19,6 +19,8 @@ package types
import (
"github.com/ethereum/go-ethereum/common"
"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
@@ -62,3 +64,32 @@ type logMarshaling struct {
BlockTimestamp hexutil.Uint64
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[:],
+ }
+}
diff --git a/core/vm/evm.go b/core/vm/evm.go
index 5897dbd265..36494de2a8 100644
--- a/core/vm/evm.go
+++ b/core/vm/evm.go
@@ -35,7 +35,7 @@ type (
// CanTransferFunc is the signature of a transfer guard function
CanTransferFunc func(StateDB, common.Address, *uint256.Int) bool
// 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
// and is used by the BLOCKHASH EVM op code.
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,
// to ensure the state clearing mechanism is applied.
if !syscall {
- evm.Context.Transfer(evm.StateDB, caller, addr, value)
+ evm.Context.Transfer(evm.StateDB, caller, addr, value, &evm.chainRules)
}
+
if isPrecompile {
var stateDB StateDB
if evm.chainRules.IsAmsterdam {
@@ -567,7 +568,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas uint64, value *ui
}
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.
// The contract is a scoped environment for this execution context only.
diff --git a/core/vm/gas_table_test.go b/core/vm/gas_table_test.go
index 436cc47f2e..e9e56038dd 100644
--- a/core/vm/gas_table_test.go
+++ b/core/vm/gas_table_test.go
@@ -94,7 +94,7 @@ func TestEIP2200(t *testing.T) {
vmctx := BlockContext{
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}})
@@ -144,7 +144,7 @@ func TestCreateGas(t *testing.T) {
statedb.Finalise(true)
vmctx := BlockContext{
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),
}
config := Config{}
diff --git a/core/vm/instructions.go b/core/vm/instructions.go
index 4e4a33acda..a5fa11e307 100644
--- a/core/vm/instructions.go
+++ b/core/vm/instructions.go
@@ -934,6 +934,13 @@ func opSelfdestruct6780(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, erro
evm.StateDB.SubBalance(this, balance, tracing.BalanceDecreaseSelfdestruct)
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.OnEnter != nil {
@@ -1086,9 +1093,6 @@ func makeLog(size int) executionFunc {
Address: scope.Contract.Address(),
Topics: topics,
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
diff --git a/core/vm/interface.go b/core/vm/interface.go
index e285b18b0f..6a93846ac5 100644
--- a/core/vm/interface.go
+++ b/core/vm/interface.go
@@ -87,6 +87,7 @@ type StateDB interface {
Snapshot() int
AddLog(*types.Log)
+ EmitLogsForBurnAccounts()
AddPreimage(common.Hash, []byte)
Witness() *stateless.Witness
diff --git a/core/vm/interpreter_test.go b/core/vm/interpreter_test.go
index 79531f78d2..28df8546b5 100644
--- a/core/vm/interpreter_test.go
+++ b/core/vm/interpreter_test.go
@@ -40,7 +40,7 @@ var loopInterruptTests = []string{
func TestLoopInterrupt(t *testing.T) {
address := common.BytesToAddress([]byte("contract"))
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 {
diff --git a/params/protocol_params.go b/params/protocol_params.go
index cebf5008c8..652574287c 100644
--- a/params/protocol_params.go
+++ b/params/protocol_params.go
@@ -222,3 +222,11 @@ var (
ConsolidationQueueAddress = common.HexToAddress("0x0000BBdDc7CE488642fb579F8B00f3a590007251")
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)')
+
+)
From 745b0a8c09ad9d0866da67403ffa99d11ba70ec3 Mon Sep 17 00:00:00 2001
From: vickkkkkyy
Date: Tue, 24 Mar 2026 01:14:28 +0800
Subject: [PATCH 15/22] cmd/utils: guard SampleRatio flag with IsSet check
(#34062)
In `setOpenTelemetry`, all other fields (Enabled, Endpoint, AuthUser,
AuthPassword, InstanceID, Tags) are guarded by `ctx.IsSet()` checks, so
they only override the config file when explicitly set via CLI flags.
`SampleRatio` was the only field missing this guard, causing the flag
default (`1.0`) to always overwrite whatever was loaded from the config
file.
- Fix OpenTelemetry `SampleRatio` being unconditionally overwritten by
the CLI flag default value (`1.0`), even when the user did not pass
`--rpc.telemetry.sample-ratio`
- This caused config file values for `SampleRatio` to be silently
ignored
---
cmd/utils/flags.go | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go
index 3a0bcc6b05..c1284044eb 100644
--- a/cmd/utils/flags.go
+++ b/cmd/utils/flags.go
@@ -1584,7 +1584,9 @@ func setOpenTelemetry(ctx *cli.Context, cfg *node.Config) {
if ctx.IsSet(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 {
log.Warn(fmt.Sprintf("OpenTelemetry endpoint configured but telemetry is not enabled, use --%s to enable.", RPCTelemetryFlag.Name))
From e951bcbff729b0eae1e1743c2bbb27064bbaf165 Mon Sep 17 00:00:00 2001
From: Csaba Kiraly
Date: Tue, 24 Mar 2026 14:57:11 +0100
Subject: [PATCH 16/22] cmd/devp2p: fix discv5 PingMultiIP test session key
mismatch (#34031)
conn.read() used the actual UDP packet source address for
codec.Decode(), but conn.write() always used tc.remoteAddr. When the
remote node is reachable via multiple Docker networks, the packet source
IP differs from tc.remoteAddr, causing a session key lookup failure in
the codec.
Use tc.remoteAddr.String() consistently in conn.read() so the session
cache key matches what was used during Encode.
---
cmd/devp2p/internal/v5test/framework.go | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/cmd/devp2p/internal/v5test/framework.go b/cmd/devp2p/internal/v5test/framework.go
index 92a5048150..acab1eef79 100644
--- a/cmd/devp2p/internal/v5test/framework.go
+++ b/cmd/devp2p/internal/v5test/framework.go
@@ -218,11 +218,15 @@ func (tc *conn) read(c net.PacketConn) v5wire.Packet {
if err := c.SetReadDeadline(time.Now().Add(waitTime)); err != nil {
return &readError{err}
}
- n, fromAddr, err := c.ReadFrom(buf)
+ n, _, err := c.ReadFrom(buf)
if err != nil {
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 {
return &readError{err}
}
From 8f9061f937e8fe0cbd45f7416073fb03bd321667 Mon Sep 17 00:00:00 2001
From: Andrew Davis <1709934+Savid@users.noreply.github.com>
Date: Wed, 25 Mar 2026 06:47:18 +1000
Subject: [PATCH 17/22] cmd/utils: optimize history import with batched
insertion (#33894)
Improve speed of import-history command by two orders of magnitude.
Rework ImportHistory to collect up to 2500 blocks per flush instead of
flushing after each block, reducing database commit overhead.
---------
Co-authored-by: Sina Mahmoodi
---
cmd/utils/cmd.go | 61 ++++++++++++++++++++++++++++++++----------------
1 file changed, 41 insertions(+), 20 deletions(-)
diff --git a/cmd/utils/cmd.go b/cmd/utils/cmd.go
index 995724e6fc..e490f613b3 100644
--- a/cmd/utils/cmd.go
+++ b/cmd/utils/cmd.go
@@ -274,40 +274,66 @@ func ImportHistory(chain *core.BlockChain, dir string, network string, from func
reported = time.Now()
imported = 0
h = sha256.New()
- scratch = bytes.NewBuffer(nil)
+ buf = bytes.NewBuffer(nil)
)
for i, file := range entries {
err := func() error {
path := filepath.Join(dir, file)
- // validate against checksum file in directory
+ // Validate against checksum file in directory.
f, err := os.Open(path)
if err != nil {
return fmt.Errorf("open %s: %w", path, err)
}
defer f.Close()
+
if _, err := io.Copy(h, f); err != nil {
return fmt.Errorf("checksum %s: %w", path, err)
}
- got := common.BytesToHash(h.Sum(scratch.Bytes()[:])).Hex()
- want := checksums[i]
+ got := common.BytesToHash(h.Sum(buf.Bytes()[:])).Hex()
h.Reset()
- scratch.Reset()
-
- if got != want {
- return fmt.Errorf("%s checksum mismatch: have %s want %s", file, got, want)
+ buf.Reset()
+ if got != checksums[i] {
+ return fmt.Errorf("%s checksum mismatch: have %s want %s", file, got, checksums[i])
}
// Import all block data from Era1.
e, err := from(f)
if err != nil {
return fmt.Errorf("error opening era: %w", err)
}
+ defer e.Close()
+
it, err := e.Iterator()
if err != nil {
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)
+ }
+ 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() {
block, err := it.Block()
if err != nil {
@@ -320,23 +346,18 @@ func ImportHistory(chain *core.BlockChain, dir string, network string, from func
if err != nil {
return fmt.Errorf("error reading receipts %d: %w", it.Number(), err)
}
- enc := types.EncodeBlockReceiptLists([]types.Receipts{receipts})
- if _, err := chain.InsertReceiptChain([]*types.Block{block}, enc, math.MaxUint64); err != nil {
- return fmt.Errorf("error inserting body %d: %w", it.Number(), err)
- }
- 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()
+ blocks = append(blocks, block)
+ receiptsList = append(receiptsList, receipts)
+ if len(blocks) == importBatchSize {
+ if err := flush(); err != nil {
+ return err
+ }
}
}
if err := it.Error(); err != nil {
return err
}
- return nil
+ return flush()
}()
if err != nil {
return err
From 5d0e18f7757d811db2cae9dac7ac41d02bc59ef8 Mon Sep 17 00:00:00 2001
From: bigbear <155267841+aso20455@users.noreply.github.com>
Date: Wed, 25 Mar 2026 09:16:09 +0100
Subject: [PATCH 18/22] core/tracing: fix NonceChangeAuthorization comment
(#34085)
Comment referenced NonceChangeTransaction which doesn't exist, should be
NonceChangeAuthorization.
---
core/tracing/hooks.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/core/tracing/hooks.go b/core/tracing/hooks.go
index 6d0131ce70..de63689bc5 100644
--- a/core/tracing/hooks.go
+++ b/core/tracing/hooks.go
@@ -426,7 +426,7 @@ const (
// NonceChangeNewContract is the nonce change of a newly created contract.
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
// NonceChangeRevert is emitted when the nonce is reverted back to a previous value due to call failure.
From 8a3a309fa97bff7252da3e7e8cac47d024d2e281 Mon Sep 17 00:00:00 2001
From: Lessa <230214854+adblesss@users.noreply.github.com>
Date: Thu, 26 Mar 2026 09:02:31 -0400
Subject: [PATCH 19/22] core/txpool/legacypool: remove redundant nil check in
Get (#34092)
Leftover from d40a255 when return type changed from *txpool.Transaction
to *types.Transaction.
---
core/txpool/legacypool/legacypool.go | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/core/txpool/legacypool/legacypool.go b/core/txpool/legacypool/legacypool.go
index 25c4b13166..d29b71ebc2 100644
--- a/core/txpool/legacypool/legacypool.go
+++ b/core/txpool/legacypool/legacypool.go
@@ -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.
func (pool *LegacyPool) Get(hash common.Hash) *types.Transaction {
- tx := pool.get(hash)
- if tx == nil {
- return nil
- }
- return tx
+ return pool.get(hash)
}
// get returns a transaction if it is contained in the pool and nil otherwise.
From 1b3b028d1da4aedcb480693e58910712abe20009 Mon Sep 17 00:00:00 2001
From: Daniel Liu <139250065@qq.com>
Date: Fri, 27 Mar 2026 09:41:56 +0800
Subject: [PATCH 20/22] miner: fix txFitsSize comment (#34100)
Rename the comment so it matches the helper name.
---
miner/worker.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/miner/worker.go b/miner/worker.go
index e82f5f6e55..1260662fe7 100644
--- a/miner/worker.go
+++ b/miner/worker.go
@@ -75,7 +75,7 @@ type environment struct {
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 {
return env.size+tx.Size() < params.MaxBlockSize-maxBlockSizeBufferZone
}
From acdd13971705c767330bdfbd4511b4d1e53580fd Mon Sep 17 00:00:00 2001
From: jwasinger
Date: Thu, 26 Mar 2026 21:45:49 -0400
Subject: [PATCH 21/22] miner: set slot number when building test payload
(#34094)
---
miner/payload_building.go | 1 +
1 file changed, 1 insertion(+)
diff --git a/miner/payload_building.go b/miner/payload_building.go
index 97b4d0c509..ccaabec373 100644
--- a/miner/payload_building.go
+++ b/miner/payload_building.go
@@ -350,6 +350,7 @@ func (miner *Miner) BuildTestingPayload(args *BuildPayloadArgs, transactions []*
random: args.Random,
withdrawals: args.Withdrawals,
beaconRoot: args.BeaconRoot,
+ slotNum: args.SlotNum,
noTxs: empty,
forceOverrides: true,
overrideExtraData: extraData,
From c3467dd8b5e2cf151744b04b62888649ee21f52d Mon Sep 17 00:00:00 2001
From: rjl493456442
Date: Sat, 28 Mar 2026 00:06:46 +0800
Subject: [PATCH 22/22] core, miner, trie: relocate witness stats (#34106)
This PR relocates the witness statistics into the witness itself, making
it more self-contained.
---
core/blockchain.go | 16 +++++-----------
core/state/statedb.go | 38 ++++++++------------------------------
core/stateless/stats.go | 7 +++++++
core/stateless/witness.go | 36 +++++++++++++++++++++++++++++-------
miner/worker.go | 4 ++--
trie/levelstats.go | 12 ++++++++++++
6 files changed, 63 insertions(+), 50 deletions(-)
diff --git a/core/blockchain.go b/core/blockchain.go
index 1b45a5ac39..35b2d35dc7 100644
--- a/core/blockchain.go
+++ b/core/blockchain.go
@@ -2170,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
// while processing transactions. Before Byzantium the prefetcher is mostly
// useless due to the intermediate root hashing after each transaction.
- var (
- witness *stateless.Witness
- witnessStats *stateless.WitnessStats
- )
+ var witness *stateless.Witness
if bc.chainConfig.IsByzantium(block.Number()) {
// Generate witnesses either if we're self-testing, or if it's the
// only block being inserted. A bit crude, but witnesses are huge,
// so we refuse to make an entire chain of them.
if config.StatelessSelfValidation || config.MakeWitness {
- witness, err = stateless.NewWitness(block.Header(), bc)
+ witness, err = stateless.NewWitness(block.Header(), bc, config.EnableWitnessStats)
if err != nil {
return nil, err
}
- if config.EnableWitnessStats {
- witnessStats = stateless.NewWitnessStats()
- }
}
- statedb.StartPrefetcher("chain", witness, witnessStats)
+ statedb.StartPrefetcher("chain", witness)
defer statedb.StopPrefetcher()
}
@@ -2306,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
}
// Report the collected witness statistics
- if witnessStats != nil {
- witnessStats.ReportMetrics(block.NumberU64())
+ if witness != nil {
+ witness.ReportMetrics(block.NumberU64())
}
elapsed := time.Since(startTime) + 1 // prevent zero division
stats.TotalTime = elapsed
diff --git a/core/state/statedb.go b/core/state/statedb.go
index 59ad3cdfef..93dd7d6488 100644
--- a/core/state/statedb.go
+++ b/core/state/statedb.go
@@ -135,8 +135,7 @@ type StateDB struct {
journal *journal
// State witness if cross validation is needed
- witness *stateless.Witness
- witnessStats *stateless.WitnessStats
+ witness *stateless.Witness
// Measurements gathered during execution for debugging purposes
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
// state trie concurrently while the state is mutated so that when we reach the
// 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
s.StopPrefetcher()
// Enable witness collection if requested
s.witness = witness
- s.witnessStats = witnessStats
// With the switch to the Proof-of-Stake consensus algorithm, block production
// rewards are now handled at the consensus layer. Consequently, a block may
@@ -913,7 +911,7 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
// If witness building is enabled and the state object has a trie,
// gather the witnesses for its specific storage trie
if s.witness != nil && obj.trie != nil {
- s.witness.AddState(obj.trie.Witness())
+ s.witness.AddState(obj.trie.Witness(), obj.addrHash())
}
return nil
})
@@ -930,17 +928,9 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
continue
}
if trie := obj.getPrefetchedTrie(); trie != nil {
- witness := trie.Witness()
- s.witness.AddState(witness)
- if s.witnessStats != nil {
- s.witnessStats.Add(witness, obj.addrHash())
- }
+ s.witness.AddState(trie.Witness(), obj.addrHash())
} else if obj.trie != nil {
- witness := obj.trie.Witness()
- s.witness.AddState(witness)
- if s.witnessStats != nil {
- s.witnessStats.Add(witness, obj.addrHash())
- }
+ s.witness.AddState(obj.trie.Witness(), obj.addrHash())
}
}
// Pull in only-read and non-destructed trie witnesses
@@ -954,17 +944,9 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
continue
}
if trie := obj.getPrefetchedTrie(); trie != nil {
- witness := trie.Witness()
- s.witness.AddState(witness)
- if s.witnessStats != nil {
- s.witnessStats.Add(witness, obj.addrHash())
- }
+ s.witness.AddState(trie.Witness(), obj.addrHash())
} else if obj.trie != nil {
- witness := obj.trie.Witness()
- s.witness.AddState(witness)
- if s.witnessStats != nil {
- s.witnessStats.Add(witness, obj.addrHash())
- }
+ s.witness.AddState(obj.trie.Witness(), obj.addrHash())
}
}
}
@@ -1037,11 +1019,7 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
// If witness building is enabled, gather the account trie witness
if s.witness != nil {
- witness := s.trie.Witness()
- s.witness.AddState(witness)
- if s.witnessStats != nil {
- s.witnessStats.Add(witness, common.Hash{})
- }
+ s.witness.AddState(s.trie.Witness(), common.Hash{})
}
return hash
}
diff --git a/core/stateless/stats.go b/core/stateless/stats.go
index 7f4473a67c..8c05b23d37 100644
--- a/core/stateless/stats.go
+++ b/core/stateless/stats.go
@@ -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() {
if s.accountTrie == nil {
s.accountTrie = trie.NewLevelStats()
diff --git a/core/stateless/witness.go b/core/stateless/witness.go
index 588c895a2f..f1321699c1 100644
--- a/core/stateless/witness.go
+++ b/core/stateless/witness.go
@@ -42,12 +42,13 @@ type Witness struct {
Codes map[string]struct{} // Set of bytecodes ran or accessed
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
- lock sync.Mutex // Lock to allow concurrent state insertions
+ chain HeaderReader // Chain reader to convert block hash ops to header proofs
+ stats *WitnessStats // Optional statistics collector
+ lock sync.Mutex // Lock to allow concurrent state insertions
}
// 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*
// be included to act as a trustless pre-root hash container
var headers []*types.Header
@@ -59,13 +60,17 @@ func NewWitness(context *types.Header, chain HeaderReader) (*Witness, error) {
headers = append(headers, parent)
}
// Create the witness with a reconstructed gutted out block
- return &Witness{
+ w := &Witness{
context: context,
Headers: headers,
Codes: make(map[string]struct{}),
State: make(map[string]struct{}),
chain: chain,
- }, nil
+ }
+ if enableStats {
+ w.stats = NewWitnessStats()
+ }
+ return w, nil
}
// 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{}{}
}
-// AddState inserts a batch of MPT trie nodes into the witness.
-func (w *Witness) AddState(nodes map[string][]byte) {
+// AddState inserts a batch of MPT trie nodes into the witness. The owner
+// 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 {
return
}
@@ -98,6 +106,17 @@ func (w *Witness) AddState(nodes map[string][]byte) {
for _, value := range nodes {
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() {
@@ -113,6 +132,9 @@ func (w *Witness) Copy() *Witness {
State: maps.Clone(w.State),
chain: w.chain,
}
+ if w.stats != nil {
+ cpy.stats = w.stats.copy()
+ }
if w.context != nil {
cpy.context = types.CopyHeader(w.context)
}
diff --git a/miner/worker.go b/miner/worker.go
index 1260662fe7..39a61de318 100644
--- a/miner/worker.go
+++ b/miner/worker.go
@@ -330,12 +330,12 @@ func (miner *Miner) makeEnv(parent *types.Header, header *types.Header, coinbase
}
var bundle *stateless.Witness
if witness {
- bundle, err = stateless.NewWitness(header, miner.chain)
+ bundle, err = stateless.NewWitness(header, miner.chain, false)
if err != nil {
return nil, err
}
}
- state.StartPrefetcher("miner", bundle, nil)
+ state.StartPrefetcher("miner", bundle)
// Note the passed coinbase may be different with header.Coinbase.
return &environment{
signer: types.MakeSigner(miner.chainConfig, header.Number, header.Time),
diff --git a/trie/levelstats.go b/trie/levelstats.go
index 9168e3fbaf..c73d652146 100644
--- a/trie/levelstats.go
+++ b/trie/levelstats.go
@@ -36,6 +36,18 @@ func NewLevelStats() *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
// trie node.
func (s *LevelStats) MaxDepth() int {