fix: merge conflicts

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

View file

@ -218,11 +218,15 @@ func (tc *conn) read(c net.PacketConn) v5wire.Packet {
if err := c.SetReadDeadline(time.Now().Add(waitTime)); err != nil {
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}
}

View file

@ -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.

View file

@ -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: "<root>",
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 <state-root>
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: "<root>",
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 <state-root>
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)) {

View file

@ -266,7 +266,7 @@ func ImportHistory(chain *core.BlockChain, db ethdb.Database, dir string, networ
len(checksums), len(entries))
}
// Determine resume point from last successfully imported block
// Determine resume point from last successfully imported block.
var resumeBlock uint64
if tail := rawdb.ReadEraImportTail(db); tail != nil {
resumeBlock = *tail
@ -278,9 +278,8 @@ func ImportHistory(chain *core.BlockChain, db ethdb.Database, dir string, networ
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)
@ -291,33 +290,36 @@ func ImportHistory(chain *core.BlockChain, db ethdb.Database, dir string, networ
}
defer f.Close()
// Peek at era block range to see if we can skip entirely
// Peek at era block range to see if we can skip entirely.
e, err := from(f)
if err != nil {
return fmt.Errorf("error opening era: %w", err)
}
eraStart := e.Start()
eraEnd := eraStart + e.Count() - 1
e.Close()
// Skip era files fully behind resume point
// Skip era files fully behind resume point.
if resumeBlock > 0 && eraEnd <= resumeBlock {
log.Debug("Skipping already imported Era file", "file", file, "eraEnd", eraEnd, "resumeBlock", resumeBlock)
return nil
}
// Validate against checksum file in directory.
if _, err := f.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("seek %s: %w", path, err)
}
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.
if _, err := f.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("seek %s: %w", path, err)
}
@ -325,11 +327,42 @@ func ImportHistory(chain *core.BlockChain, db ethdb.Database, dir string, networ
if err != nil {
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)
}
// Track the last successfully imported block for resume support.
lastBlock := blocks[len(blocks)-1].NumberU64()
rawdb.WriteEraImportTail(db, lastBlock)
resumeBlock = lastBlock
imported += len(blocks)
if time.Since(reported) >= 8*time.Second {
head := blocks[len(blocks)-1].NumberU64()
log.Info("Importing Era files", "head", head, "imported", imported,
"elapsed", common.PrettyDuration(time.Since(start)))
imported = 0
reported = time.Now()
}
blocks = blocks[:0]
receiptsList = receiptsList[:0]
return nil
}
)
for it.Next() {
block, err := it.Block()
if err != nil {
@ -338,7 +371,7 @@ func ImportHistory(chain *core.BlockChain, db ethdb.Database, dir string, networ
if block.Number().BitLen() == 0 {
continue // skip genesis
}
// Skip blocks already imported (mid-epoch resume)
// Skip blocks already imported (mid-era resume).
if resumeBlock > 0 && block.Number().Uint64() <= resumeBlock {
continue
}
@ -346,25 +379,18 @@ func ImportHistory(chain *core.BlockChain, db ethdb.Database, dir string, networ
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)
}
rawdb.WriteEraImportTail(db, block.Number().Uint64())
resumeBlock = block.Number().Uint64()
imported++
if time.Since(reported) >= 8*time.Second {
log.Info("Importing Era files", "head", it.Number(), "imported", imported,
"elapsed", common.PrettyDuration(time.Since(start)))
imported = 0
reported = time.Now()
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

View file

@ -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",
@ -1580,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))

View file

@ -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)

View file

@ -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)
}
}
@ -2209,24 +2170,18 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash,
// If we are past Byzantium, enable prefetching to pull in trie node paths
// 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()
}
@ -2345,8 +2300,8 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash,
stats.BlockWrite = time.Since(wstart) - max(statedb.AccountCommits, statedb.StorageCommits) /* concurrent */ - statedb.DatabaseCommits
}
// 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

View file

@ -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 (

View file

@ -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

View file

@ -0,0 +1,169 @@
// Copyright 2026 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package core
import (
"encoding/binary"
"math/big"
"reflect"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus/beacon"
"github.com/ethereum/go-ethereum/consensus/ethash"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/params"
)
var ethTransferTestCode = common.FromHex("6080604052600436106100345760003560e01c8063574ffc311461003957806366e41cb714610090578063f8a8fd6d1461009a575b600080fd5b34801561004557600080fd5b5061004e6100a4565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100986100ac565b005b6100a26100f5565b005b63deadbeef81565b7f38e80b5c85ba49b7280ccc8f22548faa62ae30d5a008a1b168fba5f47f5d1ee560405160405180910390a1631234567873ffffffffffffffffffffffffffffffffffffffff16ff5b7f24ec1d3ff24c2f6ff210738839dbc339cd45a5294d85c79361016243157aae7b60405160405180910390a163deadbeef73ffffffffffffffffffffffffffffffffffffffff166002348161014657fe5b046040516024016040516020818303038152906040527f66e41cb7000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff83818316178352505050506040518082805190602001908083835b602083106101fd57805182526020820191506020810190506020830392506101da565b6001836020036101000a03801982511681845116808217855250505050505090500191505060006040518083038185875af1925050503d806000811461025f576040519150601f19603f3d011682016040523d82523d6000602084013e610264565b606091505b50505056fea265627a7a723158202cce817a434785d8560c200762f972d453ccd30694481be7545f9035a512826364736f6c63430005100032")
/*
pragma solidity >=0.4.22 <0.6.0;
contract TestLogs {
address public constant target_contract = 0x00000000000000000000000000000000DeaDBeef;
address payable constant selfdestruct_addr = 0x0000000000000000000000000000000012345678;
event Response(bool success, bytes data);
event TestEvent();
event TestEvent2();
function test() public payable {
emit TestEvent();
target_contract.call.value(msg.value/2)(abi.encodeWithSignature("test2()"));
}
function test2() public payable {
emit TestEvent2();
selfdestruct(selfdestruct_addr);
}
}
*/
// TestEthTransferLogs tests EIP-7708 ETH transfer log output by simulating a
// scenario including transaction, CALL and SELFDESTRUCT value transfers, and
// also "ordinary" logs emitted. The same scenario is also tested with no value
// transferred.
func TestEthTransferLogs(t *testing.T) {
testEthTransferLogs(t, 1_000_000_000)
testEthTransferLogs(t, 0)
}
func testEthTransferLogs(t *testing.T, value uint64) {
var (
key1, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
addr1 = crypto.PubkeyToAddress(key1.PublicKey)
addr2 = common.HexToAddress("cafebabe") // caller
addr3 = common.HexToAddress("deadbeef") // callee
addr4 = common.HexToAddress("12345678") // selfdestruct target
testEvent = crypto.Keccak256Hash([]byte("TestEvent()"))
testEvent2 = crypto.Keccak256Hash([]byte("TestEvent2()"))
config = *params.MergedTestChainConfig
signer = types.LatestSigner(&config)
engine = beacon.New(ethash.NewFaker())
)
//TODO remove this hacky config initialization when final Amsterdam config is available
config.AmsterdamTime = new(uint64)
blobConfig := *config.BlobScheduleConfig
blobConfig.Amsterdam = blobConfig.Osaka
config.BlobScheduleConfig = &blobConfig
gspec := &Genesis{
Config: &config,
Alloc: types.GenesisAlloc{
addr1: {Balance: newGwei(1000000000)},
addr2: {Code: ethTransferTestCode},
addr3: {Code: ethTransferTestCode},
},
}
_, blocks, receipts := GenerateChainWithGenesis(gspec, engine, 1, func(i int, b *BlockGen) {
tx := types.MustSignNewTx(key1, signer, &types.DynamicFeeTx{
ChainID: gspec.Config.ChainID,
Nonce: 0,
To: &addr2,
Gas: 500_000,
GasFeeCap: newGwei(5),
GasTipCap: newGwei(5),
Value: big.NewInt(int64(value)),
Data: common.FromHex("f8a8fd6d"),
})
b.AddTx(tx)
})
blockHash := blocks[0].Hash()
txHash := blocks[0].Transactions()[0].Hash()
addr2hash := func(addr common.Address) (hash common.Hash) {
copy(hash[12:], addr[:])
return
}
u256 := func(amount uint64) []byte {
data := make([]byte, 32)
binary.BigEndian.PutUint64(data[24:], amount)
return data
}
var expLogs = []*types.Log{
{
Address: params.SystemAddress,
Topics: []common.Hash{params.EthTransferLogEvent, addr2hash(addr1), addr2hash(addr2)},
Data: u256(value),
},
{
Address: addr2,
Topics: []common.Hash{testEvent},
Data: nil,
},
{
Address: params.SystemAddress,
Topics: []common.Hash{params.EthTransferLogEvent, addr2hash(addr2), addr2hash(addr3)},
Data: u256(value / 2),
},
{
Address: addr3,
Topics: []common.Hash{testEvent2},
Data: nil,
},
{
Address: params.SystemAddress,
Topics: []common.Hash{params.EthTransferLogEvent, addr2hash(addr3), addr2hash(addr4)},
Data: u256(value / 2),
},
}
if value == 0 {
// no ETH transfer logs expected with zero value
expLogs = []*types.Log{expLogs[1], expLogs[3]}
}
for i, log := range expLogs {
log.BlockNumber = 1
log.BlockHash = blockHash
log.BlockTimestamp = 10
log.TxIndex = 0
log.TxHash = txHash
log.Index = uint(i)
}
if len(expLogs) != len(receipts[0][0].Logs) {
t.Fatalf("Incorrect number of logs (expected: %d, got: %d)", len(expLogs), len(receipts[0][0].Logs))
}
for i, log := range receipts[0][0].Logs {
if !reflect.DeepEqual(expLogs[i], log) {
t.Fatalf("Incorrect log at index %d (expected: %v, got: %v)", i, expLogs[i], log)
}
}
}

View file

@ -25,6 +25,7 @@ import (
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/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))
}
}

View file

@ -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)
}
}

View file

@ -0,0 +1,58 @@
// Copyright 2026 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package history
import (
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/params"
)
func TestNewPolicy(t *testing.T) {
// KeepAll: no target.
p, err := NewPolicy(KeepAll, params.MainnetGenesisHash)
if err != nil {
t.Fatalf("KeepAll: %v", err)
}
if p.Mode != KeepAll || p.Target != nil {
t.Errorf("KeepAll: unexpected policy %+v", p)
}
// PostMerge: resolves known mainnet prune point.
p, err = NewPolicy(KeepPostMerge, params.MainnetGenesisHash)
if err != nil {
t.Fatalf("PostMerge: %v", err)
}
if p.Target == nil || p.Target.BlockNumber != 15537393 {
t.Errorf("PostMerge: unexpected target %+v", p.Target)
}
// PostPrague: resolves known mainnet prune point.
p, err = NewPolicy(KeepPostPrague, params.MainnetGenesisHash)
if err != nil {
t.Fatalf("PostPrague: %v", err)
}
if p.Target == nil || p.Target.BlockNumber != 22431084 {
t.Errorf("PostPrague: unexpected target %+v", p.Target)
}
// PostMerge on unknown network: error.
if _, err = NewPolicy(KeepPostMerge, common.HexToHash("0xdeadbeef")); err == nil {
t.Fatal("PostMerge unknown network: expected error")
}
}

View file

@ -480,7 +480,7 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error {
receipts.add(size)
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)

View file

@ -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
@ -743,6 +741,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.
@ -824,32 +857,65 @@ 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,
// 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
})
return nil
})
}
}
// If witness building is enabled, gather all the read-only accesses.
// Skip witness collection in Verkle mode, they will be gathered
@ -862,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
@ -886,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())
}
}
}
@ -911,7 +961,7 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
// only a single trie is used for state hashing. Replacing a non-nil verkle tree
// 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 {
@ -969,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
}

View file

@ -229,6 +229,10 @@ func (s *hookedStateDB) AddLog(log *types.Log) {
}
}
func (s *hookedStateDB) EmitLogsForBurnAccounts() {
s.inner.EmitLogsForBurnAccounts()
}
func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) {
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.

View file

@ -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,

View file

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

View file

@ -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)
}

View file

@ -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.

View file

@ -998,11 +998,7 @@ func (pool *LegacyPool) Status(hash common.Hash) txpool.TxStatus {
// Get returns a transaction if it is contained in the pool and nil otherwise.
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.

View file

@ -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[:],
}
}

View file

@ -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.

View file

@ -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
}

View file

@ -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) {

View file

@ -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{}

View file

@ -934,6 +934,13 @@ func opSelfdestruct6780(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, erro
evm.StateDB.SubBalance(this, balance, tracing.BalanceDecreaseSelfdestruct)
evm.StateDB.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

View file

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

View file

@ -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 {

View file

@ -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
}
}

View file

@ -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,

View file

@ -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()

View file

@ -357,7 +357,8 @@ func testFilters(t *testing.T, history uint64, noHistory bool) {
want: `[{"address":"0xff00000000000000000000000000000000000000","topics":["0x0000000000000000000000000000000000000000000000000000746f70696333"],"data":"0x","blockNumber":"0x3e7","transactionHash":"0x53e3675800c6908424b61b35a44e51ca4c73ca603e58a65b32c67968b4f42200","transactionIndex":"0x0","blockHash":"0x2e4620a2b426b0612ec6cad9603f466723edaed87f98c9137405dd4f7a2409ff","blockTimestamp":"0x2706","logIndex":"0x0","removed":false}]`,
},
{
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),

View file

@ -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{

View file

@ -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,

View file

@ -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
}
@ -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),

View file

@ -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

View file

@ -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
}

View file

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

View file

@ -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 {

View file

@ -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)')
)

View file

@ -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)

View file

@ -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) {

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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")
}

View file

@ -0,0 +1,239 @@
// Copyright 2026 go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package bintrie
import (
"bytes"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/trie"
)
// makeTrie creates a BinaryTrie populated with the given key-value pairs.
func makeTrie(t *testing.T, entries [][2]common.Hash) *BinaryTrie {
t.Helper()
tr := &BinaryTrie{
root: NewBinaryNode(),
tracer: trie.NewPrevalueTracer(),
}
for _, kv := range entries {
var err error
tr.root, err = tr.root.Insert(kv[0][:], kv[1][:], nil, 0)
if err != nil {
t.Fatal(err)
}
}
return tr
}
// countLeaves iterates the trie and returns the number of leaves visited.
func countLeaves(t *testing.T, tr *BinaryTrie) int {
t.Helper()
it, err := newBinaryNodeIterator(tr, nil)
if err != nil {
t.Fatal(err)
}
leaves := 0
for it.Next(true) {
if it.Leaf() {
leaves++
}
}
if it.Error() != nil {
t.Fatalf("iterator error: %v", it.Error())
}
return leaves
}
// TestIteratorEmptyTrie verifies that iterating over an empty trie returns
// no nodes and reports no error.
func TestIteratorEmptyTrie(t *testing.T) {
tr := &BinaryTrie{
root: Empty{},
tracer: trie.NewPrevalueTracer(),
}
it, err := newBinaryNodeIterator(tr, nil)
if err != nil {
t.Fatal(err)
}
if it.Next(true) {
t.Fatal("expected no iteration over empty trie")
}
if it.Error() != nil {
t.Fatalf("unexpected error: %v", it.Error())
}
}
// TestIteratorSingleStem verifies iteration over a trie with a single stem
// node containing multiple values.
func TestIteratorSingleStem(t *testing.T) {
tr := makeTrie(t, [][2]common.Hash{
{common.HexToHash("0000000000000000000000000000000000000000000000000000000000000003"), oneKey},
{common.HexToHash("0000000000000000000000000000000000000000000000000000000000000007"), oneKey},
{common.HexToHash("00000000000000000000000000000000000000000000000000000000000000FF"), oneKey},
})
if leaves := countLeaves(t, tr); leaves != 3 {
t.Fatalf("expected 3 leaves, got %d", leaves)
}
}
// TestIteratorTwoStems verifies iteration over a trie with two stems
// separated by internal nodes, ensuring all leaves from both stems are visited.
func TestIteratorTwoStems(t *testing.T) {
tr := makeTrie(t, [][2]common.Hash{
{common.HexToHash("0000000000000000000000000000000000000000000000000000000000000001"), oneKey},
{common.HexToHash("0000000000000000000000000000000000000000000000000000000000000002"), oneKey},
{common.HexToHash("8000000000000000000000000000000000000000000000000000000000000001"), oneKey},
{common.HexToHash("8000000000000000000000000000000000000000000000000000000000000002"), oneKey},
})
if leaves := countLeaves(t, tr); leaves != 4 {
t.Fatalf("expected 4 leaves, got %d", leaves)
}
}
// TestIteratorLeafKeyAndBlob verifies that the iterator returns correct
// leaf keys and values.
func TestIteratorLeafKeyAndBlob(t *testing.T) {
key := common.HexToHash("0000000000000000000000000000000000000000000000000000000000000005")
val := common.HexToHash("00000000000000000000000000000000000000000000000000000000deadbeef")
tr := makeTrie(t, [][2]common.Hash{{key, val}})
it, err := newBinaryNodeIterator(tr, nil)
if err != nil {
t.Fatal(err)
}
found := false
for it.Next(true) {
if it.Leaf() {
found = true
if !bytes.Equal(it.LeafKey(), key[:]) {
t.Fatalf("leaf key mismatch: got %x, want %x", it.LeafKey(), key)
}
if !bytes.Equal(it.LeafBlob(), val[:]) {
t.Fatalf("leaf blob mismatch: got %x, want %x", it.LeafBlob(), val)
}
}
}
if !found {
t.Fatal("expected to find a leaf")
}
}
// TestIteratorEmptyNodeBacktrack is a regression test for the Empty node
// backtracking bug. Before the fix, encountering an Empty child during
// iteration would terminate the walk prematurely instead of backtracking
// to the parent and continuing with the next sibling.
func TestIteratorEmptyNodeBacktrack(t *testing.T) {
tr := makeTrie(t, [][2]common.Hash{
{common.HexToHash("0000000000000000000000000000000000000000000000000000000000000001"), oneKey},
{common.HexToHash("8000000000000000000000000000000000000000000000000000000000000001"), oneKey},
})
if _, ok := tr.root.(*InternalNode); !ok {
t.Fatalf("expected InternalNode root, got %T", tr.root)
}
if leaves := countLeaves(t, tr); leaves != 2 {
t.Fatalf("expected 2 leaves, got %d (Empty backtrack bug?)", leaves)
}
}
// TestIteratorHashedNodeNilData is a regression test for the nil-data guard.
// When nodeResolver encounters a zero-hash HashedNode, it returns (nil, nil).
// The iterator should treat this as Empty and continue rather than panicking.
func TestIteratorHashedNodeNilData(t *testing.T) {
tr := makeTrie(t, [][2]common.Hash{
{common.HexToHash("0000000000000000000000000000000000000000000000000000000000000001"), oneKey},
{common.HexToHash("8000000000000000000000000000000000000000000000000000000000000001"), oneKey},
})
root, ok := tr.root.(*InternalNode)
if !ok {
t.Fatalf("expected InternalNode root, got %T", tr.root)
}
// Replace right child with a zero-hash HashedNode. nodeResolver
// short-circuits on common.Hash{} and returns (nil, nil), which
// triggers the nil-data guard in the iterator.
root.right = HashedNode(common.Hash{})
// Should not panic; the zero-hash right child should be treated as Empty.
if leaves := countLeaves(t, tr); leaves != 1 {
t.Fatalf("expected 1 leaf (zero-hash right node skipped), got %d", leaves)
}
}
// TestIteratorManyStems verifies iteration correctness with many stems,
// producing a deep tree structure.
func TestIteratorManyStems(t *testing.T) {
entries := make([][2]common.Hash, 16)
for i := range entries {
var key common.Hash
key[0] = byte(i << 4)
key[31] = 1
entries[i] = [2]common.Hash{key, oneKey}
}
tr := makeTrie(t, entries)
if leaves := countLeaves(t, tr); leaves != 16 {
t.Fatalf("expected 16 leaves, got %d", leaves)
}
}
// TestIteratorDeepTree verifies iteration over a trie with stems that share
// a long common prefix, producing many intermediate InternalNodes.
func TestIteratorDeepTree(t *testing.T) {
tr := makeTrie(t, [][2]common.Hash{
{common.HexToHash("0000000000C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0"), oneKey},
{common.HexToHash("0000000000E00000000000000000000000000000000000000000000000000000"), twoKey},
})
if leaves := countLeaves(t, tr); leaves != 2 {
t.Fatalf("expected 2 leaves in deep tree, got %d", leaves)
}
}
// TestIteratorNodeCount verifies the total number of Next(true) calls
// for a known tree structure.
func TestIteratorNodeCount(t *testing.T) {
tr := makeTrie(t, [][2]common.Hash{
{common.HexToHash("0000000000000000000000000000000000000000000000000000000000000001"), oneKey},
{common.HexToHash("8000000000000000000000000000000000000000000000000000000000000001"), oneKey},
})
it, err := newBinaryNodeIterator(tr, nil)
if err != nil {
t.Fatal(err)
}
total := 0
leaves := 0
for it.Next(true) {
total++
if it.Leaf() {
leaves++
}
}
if leaves != 2 {
t.Fatalf("expected 2 leaves, got %d", leaves)
}
// Root(InternalNode) + leaf1 (from left StemNode) + leaf2 (from right StemNode) = 3
// StemNodes are not returned as separate steps; the iterator advances
// directly to the first non-nil value within the stem.
if total != 3 {
t.Fatalf("expected 3 total nodes, got %d", total)
}
}

View file

@ -36,6 +36,18 @@ func NewLevelStats() *LevelStats {
return &LevelStats{}
}
// 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 {