From a15778c52f3f5e5922d6ecd29bf62df500733c6b Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Fri, 1 May 2026 15:28:19 +0200 Subject: [PATCH 01/63] trie: group 2^N binary trie nodes in serialization (#34794) This PR addresses one of the biggest performance issue with binary tries: storing each internal node individually bloats the index, the disk, and triggers a lot of write amplifications. To fix this issue, this PR serializes groups of nodes together. Because we are still looking for the ideal group size, the "depth" of the group tree is made a parameter, but that will be removed in the future, once the perfect size is known. This is a rebase of #33658 --------- Co-authored-by: Copilot --- cmd/evm/internal/t8ntool/transition.go | 8 +- cmd/geth/bintrie_convert.go | 4 +- cmd/geth/bintrie_convert_test.go | 8 +- cmd/geth/main.go | 1 + cmd/utils/flags.go | 10 + core/bintrie_witness_test.go | 2 + core/blockchain.go | 12 +- core/genesis.go | 5 +- core/genesis_test.go | 29 +-- core/state/database_ubt.go | 2 +- core/state/reader.go | 2 +- eth/backend.go | 1 + eth/ethconfig/config.go | 7 + eth/ethconfig/gen_config.go | 6 + trie/bintrie/binary_node.go | 11 + trie/bintrie/binary_node_test.go | 48 +++-- trie/bintrie/hashed_node_test.go | 2 +- trie/bintrie/internal_node_test.go | 7 +- trie/bintrie/iterator.go | 2 +- trie/bintrie/stem_node_test.go | 5 +- trie/bintrie/store_commit.go | 285 ++++++++++++++++++++----- trie/bintrie/trie.go | 38 ++-- trie/bintrie/trie_test.go | 5 +- triedb/database.go | 24 ++- triedb/pathdb/journal.go | 14 +- 25 files changed, 402 insertions(+), 136 deletions(-) diff --git a/cmd/evm/internal/t8ntool/transition.go b/cmd/evm/internal/t8ntool/transition.go index e0bb3a449d..89b703d3b8 100644 --- a/cmd/evm/internal/t8ntool/transition.go +++ b/cmd/evm/internal/t8ntool/transition.go @@ -546,7 +546,7 @@ func BinKeys(ctx *cli.Context) error { db := triedb.NewDatabase(rawdb.NewMemoryDatabase(), triedb.UBTDefaults) defer db.Close() - bt, err := genBinTrieFromAlloc(alloc, db) + bt, err := genBinTrieFromAlloc(alloc, db, triedb.UBTDefaults.BinTrieGroupDepth) if err != nil { return fmt.Errorf("error generating bt: %w", err) } @@ -590,7 +590,7 @@ func BinTrieRoot(ctx *cli.Context) error { db := triedb.NewDatabase(rawdb.NewMemoryDatabase(), triedb.UBTDefaults) defer db.Close() - bt, err := genBinTrieFromAlloc(alloc, db) + bt, err := genBinTrieFromAlloc(alloc, db, triedb.UBTDefaults.BinTrieGroupDepth) if err != nil { return fmt.Errorf("error generating bt: %w", err) } @@ -600,8 +600,8 @@ func BinTrieRoot(ctx *cli.Context) error { } // TODO(@CPerezz): Should this go to `bintrie` module? -func genBinTrieFromAlloc(alloc core.GenesisAlloc, db database.NodeDatabase) (*bintrie.BinaryTrie, error) { - bt, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, db) +func genBinTrieFromAlloc(alloc core.GenesisAlloc, db database.NodeDatabase, groupDepth int) (*bintrie.BinaryTrie, error) { + bt, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, db, groupDepth) if err != nil { return nil, err } diff --git a/cmd/geth/bintrie_convert.go b/cmd/geth/bintrie_convert.go index 43d2e629ac..46cb3aa7e4 100644 --- a/cmd/geth/bintrie_convert.go +++ b/cmd/geth/bintrie_convert.go @@ -151,7 +151,7 @@ func convertToBinaryTrie(ctx *cli.Context) error { }) defer destTriedb.Close() - binTrie, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, destTriedb) + binTrie, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, destTriedb, ctx.Int(utils.BinTrieGroupDepthFlag.Name)) if err != nil { return fmt.Errorf("failed to create binary trie: %w", err) } @@ -319,7 +319,7 @@ func commitBinaryTrie(bt *bintrie.BinaryTrie, currentRoot common.Hash, destDB *t runtime.GC() debug.FreeOSMemory() - bt, err := bintrie.NewBinaryTrie(newRoot, destDB) + bt, err := bintrie.NewBinaryTrie(newRoot, destDB, bt.GroupDepth()) if err != nil { return nil, common.Hash{}, fmt.Errorf("failed to reload binary trie: %w", err) } diff --git a/cmd/geth/bintrie_convert_test.go b/cmd/geth/bintrie_convert_test.go index 50ae752358..32e8c7e55b 100644 --- a/cmd/geth/bintrie_convert_test.go +++ b/cmd/geth/bintrie_convert_test.go @@ -87,7 +87,7 @@ func TestBintrieConvert(t *testing.T) { }) defer destTriedb.Close() - bt, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, destTriedb) + bt, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, destTriedb, 8) if err != nil { t.Fatalf("failed to create binary trie: %v", err) } @@ -98,7 +98,7 @@ func TestBintrieConvert(t *testing.T) { } t.Logf("Binary trie root: %x", currentRoot) - bt2, err := bintrie.NewBinaryTrie(currentRoot, destTriedb) + bt2, err := bintrie.NewBinaryTrie(currentRoot, destTriedb, 8) if err != nil { t.Fatalf("failed to reload binary trie: %v", err) } @@ -194,7 +194,7 @@ func TestBintrieConvertDeleteSource(t *testing.T) { PathDB: pathdb.Defaults, }) - bt, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, destTriedb) + bt, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, destTriedb, 8) if err != nil { t.Fatalf("failed to create binary trie: %v", err) } @@ -209,7 +209,7 @@ func TestBintrieConvertDeleteSource(t *testing.T) { } srcTriedb2.Close() - bt2, err := bintrie.NewBinaryTrie(newRoot, destTriedb) + bt2, err := bintrie.NewBinaryTrie(newRoot, destTriedb, 8) if err != nil { t.Fatalf("failed to reload binary trie after deletion: %v", err) } diff --git a/cmd/geth/main.go b/cmd/geth/main.go index ae869ec970..c8d7abc65b 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -95,6 +95,7 @@ var ( utils.StateHistoryFlag, utils.TrienodeHistoryFlag, utils.TrienodeHistoryFullValueCheckpointFlag, + utils.BinTrieGroupDepthFlag, utils.LightKDFFlag, utils.EthRequiredBlocksFlag, utils.LegacyWhitelistFlag, // deprecated diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 9d996f15cb..cc4c3bff5c 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -297,6 +297,12 @@ var ( Value: ethconfig.Defaults.EnableStateSizeTracking, Category: flags.StateCategory, } + BinTrieGroupDepthFlag = &cli.IntFlag{ + Name: "bintrie.groupdepth", + Usage: "Number of levels per serialized group in binary trie (1-8, default 5). Lower values create smaller groups with more nodes.", + Value: 5, + Category: flags.StateCategory, + } StateHistoryFlag = &cli.Uint64Flag{ Name: "history.state", Usage: "Number of recent blocks to retain state history for, only relevant in state.scheme=path (default = 90,000 blocks, 0 = entire chain)", @@ -1817,6 +1823,9 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { if ctx.IsSet(TrienodeHistoryFullValueCheckpointFlag.Name) { cfg.NodeFullValueCheckpoint = uint32(ctx.Uint(TrienodeHistoryFullValueCheckpointFlag.Name)) } + if ctx.IsSet(BinTrieGroupDepthFlag.Name) { + cfg.BinTrieGroupDepth = ctx.Int(BinTrieGroupDepthFlag.Name) + } if ctx.IsSet(StateSchemeFlag.Name) { cfg.StateScheme = ctx.String(StateSchemeFlag.Name) } @@ -2433,6 +2442,7 @@ func MakeChain(ctx *cli.Context, stack *node.Node, readonly bool) (*core.BlockCh StateHistory: ctx.Uint64(StateHistoryFlag.Name), TrienodeHistory: ctx.Int64(TrienodeHistoryFlag.Name), NodeFullValueCheckpoint: uint32(ctx.Uint(TrienodeHistoryFullValueCheckpointFlag.Name)), + BinTrieGroupDepth: ctx.Int(BinTrieGroupDepthFlag.Name), // Disable transaction indexing/unindexing. TxLookupLimit: -1, diff --git a/core/bintrie_witness_test.go b/core/bintrie_witness_test.go index 1b033151d3..66feef0675 100644 --- a/core/bintrie_witness_test.go +++ b/core/bintrie_witness_test.go @@ -92,6 +92,7 @@ func TestProcessUBT(t *testing.T) { // genesis := gspec.MustCommit(bcdb, triedb) options := DefaultConfig().WithStateScheme(rawdb.PathScheme) options.SnapshotLimit = 0 + options.BinTrieGroupDepth = triedb.DefaultBinTrieGroupDepth blockchain, _ := NewBlockChain(bcdb, gspec, beacon.New(ethash.NewFaker()), options) defer blockchain.Stop() @@ -218,6 +219,7 @@ func TestProcessParentBlockHash(t *testing.T) { t.Run("UBT", func(t *testing.T) { db := rawdb.NewMemoryDatabase() cacheConfig := DefaultConfig().WithStateScheme(rawdb.PathScheme) + cacheConfig.BinTrieGroupDepth = triedb.DefaultBinTrieGroupDepth cacheConfig.SnapshotLimit = 0 triedb := triedb.NewDatabase(db, cacheConfig.triedbConfig(true)) statedb, _ := state.New(types.EmptyBinaryHash, state.NewDatabase(triedb, nil)) diff --git a/core/blockchain.go b/core/blockchain.go index 296ef6bc16..f21a1462ea 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -170,9 +170,10 @@ type BlockChainConfig struct { TrieNoAsyncFlush bool // Whether the asynchronous buffer flushing is disallowed TrieJournalDirectory string // Directory path to the journal used for persisting trie data across node restarts - Preimages bool // Whether to store preimage of trie key to the disk - StateScheme string // Scheme used to store ethereum states and merkle tree nodes on top - ArchiveMode bool // Whether to enable the archive mode + Preimages bool // Whether to store preimage of trie key to the disk + StateScheme string // Scheme used to store ethereum states and merkle tree nodes on top + ArchiveMode bool // Whether to enable the archive mode + BinTrieGroupDepth int // Number of levels per serialized group in binary trie (1-8) // Number of blocks from the chain head for which state histories are retained. // If set to 0, all state histories across the entire chain will be retained; @@ -260,8 +261,9 @@ func (cfg BlockChainConfig) WithNoAsyncFlush(on bool) *BlockChainConfig { // triedbConfig derives the configures for trie database. func (cfg *BlockChainConfig) triedbConfig(isUBT bool) *triedb.Config { config := &triedb.Config{ - Preimages: cfg.Preimages, - IsUBT: isUBT, + Preimages: cfg.Preimages, + IsUBT: isUBT, + BinTrieGroupDepth: cfg.BinTrieGroupDepth, } if cfg.StateScheme == rawdb.HashScheme { config.HashDB = &hashdb.Config{ diff --git a/core/genesis.go b/core/genesis.go index d77ea10d8c..6a0affa52e 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -136,8 +136,9 @@ func hashAlloc(ga *types.GenesisAlloc, isUBT bool) (common.Hash, error) { var config *triedb.Config if isUBT { config = &triedb.Config{ - PathDB: pathdb.Defaults, - IsUBT: true, + PathDB: pathdb.Defaults, + IsUBT: true, + BinTrieGroupDepth: triedb.UBTDefaults.BinTrieGroupDepth, } } // Create an ephemeral in-memory database for computing hash, diff --git a/core/genesis_test.go b/core/genesis_test.go index e15ad00222..94f1b3a4fd 100644 --- a/core/genesis_test.go +++ b/core/genesis_test.go @@ -261,9 +261,9 @@ func newDbConfig(scheme string) *triedb.Config { return &triedb.Config{PathDB: &config} } -func TestVerkleGenesisCommit(t *testing.T) { - var verkleTime uint64 = 0 - verkleConfig := ¶ms.ChainConfig{ +func TestBinaryGenesisCommit(t *testing.T) { + var ubtTime uint64 = 0 + ubtConfig := ¶ms.ChainConfig{ ChainID: big.NewInt(1), HomesteadBlock: big.NewInt(0), DAOForkBlock: nil, @@ -281,11 +281,11 @@ func TestVerkleGenesisCommit(t *testing.T) { ArrowGlacierBlock: big.NewInt(0), GrayGlacierBlock: big.NewInt(0), MergeNetsplitBlock: nil, - ShanghaiTime: &verkleTime, - CancunTime: &verkleTime, - PragueTime: &verkleTime, - OsakaTime: &verkleTime, - UBTTime: &verkleTime, + ShanghaiTime: &ubtTime, + CancunTime: &ubtTime, + PragueTime: &ubtTime, + OsakaTime: &ubtTime, + UBTTime: &ubtTime, TerminalTotalDifficulty: big.NewInt(0), EnableUBTAtGenesis: true, Ethash: nil, @@ -300,8 +300,8 @@ func TestVerkleGenesisCommit(t *testing.T) { genesis := &Genesis{ BaseFee: big.NewInt(params.InitialBaseFee), - Config: verkleConfig, - Timestamp: verkleTime, + Config: ubtConfig, + Timestamp: ubtTime, Difficulty: big.NewInt(0), Alloc: types.GenesisAlloc{ {1}: {Balance: big.NewInt(1), Storage: map[common.Hash]common.Hash{{1}: {1}}}, @@ -320,17 +320,18 @@ func TestVerkleGenesisCommit(t *testing.T) { config.NoAsyncFlush = true triedb := triedb.NewDatabase(db, &triedb.Config{ - IsUBT: true, - PathDB: &config, + IsUBT: true, + PathDB: &config, + BinTrieGroupDepth: triedb.DefaultBinTrieGroupDepth, }) block := genesis.MustCommit(db, triedb) if !bytes.Equal(block.Root().Bytes(), expected) { t.Fatalf("invalid genesis state root, expected %x, got %x", expected, block.Root()) } - // Test that the trie is verkle + // Test that the trie is a unified binary trie if !triedb.IsUBT() { - t.Fatalf("expected trie to be verkle") + t.Fatalf("expected trie to be a unified binary trie") } vdb := rawdb.NewTable(db, string(rawdb.VerklePrefix)) if !rawdb.HasAccountTrieNode(vdb, nil) { diff --git a/core/state/database_ubt.go b/core/state/database_ubt.go index 718d93df87..16579f6d6a 100644 --- a/core/state/database_ubt.go +++ b/core/state/database_ubt.go @@ -96,7 +96,7 @@ func (db *UBTDatabase) ReadersWithCacheStats(stateRoot common.Hash) (Reader, Rea // OpenTrie opens the main account trie at a specific root hash. func (db *UBTDatabase) OpenTrie(root common.Hash) (Trie, error) { - return bintrie.NewBinaryTrie(root, db.triedb) + return bintrie.NewBinaryTrie(root, db.triedb, db.triedb.BinTrieGroupDepth()) } // OpenStorageTrie opens the storage trie of an account. In binary trie mode, diff --git a/core/state/reader.go b/core/state/reader.go index 5df0acbb9b..be07cec0f9 100644 --- a/core/state/reader.go +++ b/core/state/reader.go @@ -255,7 +255,7 @@ type ubtTrieReader struct { // newUBTTrieReader constructs a Unified-binary-trie reader of the specific state. // An error will be returned if the associated trie specified by root is not existent. func newUBTTrieReader(root common.Hash, db *triedb.Database) (*ubtTrieReader, error) { - binTrie, binErr := bintrie.NewBinaryTrie(root, db) + binTrie, binErr := bintrie.NewBinaryTrie(root, db, db.BinTrieGroupDepth()) if binErr != nil { return nil, binErr } diff --git a/eth/backend.go b/eth/backend.go index 08a3c70c9d..6cfd1f6fa0 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -237,6 +237,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { StateHistory: config.StateHistory, TrienodeHistory: config.TrienodeHistory, NodeFullValueCheckpoint: config.NodeFullValueCheckpoint, + BinTrieGroupDepth: config.BinTrieGroupDepth, StateScheme: scheme, HistoryPolicy: histPolicy, TxLookupLimit: int64(min(config.TransactionHistory, math.MaxInt64)), diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index dd7436bf52..b51b78e199 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -35,6 +35,7 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/miner" "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/triedb" "github.com/ethereum/go-ethereum/triedb/pathdb" ) @@ -59,6 +60,7 @@ var Defaults = Config{ StateHistory: pathdb.Defaults.StateHistory, TrienodeHistory: pathdb.Defaults.TrienodeHistory, NodeFullValueCheckpoint: pathdb.Defaults.FullValueCheckpoint, + BinTrieGroupDepth: triedb.DefaultBinTrieGroupDepth, DatabaseCache: 2048, TrieCleanCache: 614, TrieDirtyCache: 1024, @@ -125,6 +127,11 @@ type Config struct { // consistent with persistent state. StateScheme string `toml:",omitempty"` + // BinTrieGroupDepth is the number of levels per serialized group in binary trie. + // Valid values are 1-8, with 8 being the default (byte-aligned groups). + // Lower values create smaller groups with more nodes. + BinTrieGroupDepth int `toml:",omitempty"` + // RequiredBlocks is a set of block number -> hash mappings which must be in the // canonical chain of all remote peers. Setting the option makes geth verify the // presence of these blocks for every new peer connection. diff --git a/eth/ethconfig/gen_config.go b/eth/ethconfig/gen_config.go index ed85562f44..c5e45348be 100644 --- a/eth/ethconfig/gen_config.go +++ b/eth/ethconfig/gen_config.go @@ -34,6 +34,7 @@ func (c Config) MarshalTOML() (interface{}, error) { TrienodeHistory int64 `toml:",omitempty"` NodeFullValueCheckpoint uint32 `toml:",omitempty"` StateScheme string `toml:",omitempty"` + BinTrieGroupDepth int `toml:",omitempty"` RequiredBlocks map[uint64]common.Hash `toml:"-"` SlowBlockThreshold time.Duration `toml:",omitempty"` SkipBcVersionCheck bool `toml:"-"` @@ -87,6 +88,7 @@ func (c Config) MarshalTOML() (interface{}, error) { enc.TrienodeHistory = c.TrienodeHistory enc.NodeFullValueCheckpoint = c.NodeFullValueCheckpoint enc.StateScheme = c.StateScheme + enc.BinTrieGroupDepth = c.BinTrieGroupDepth enc.RequiredBlocks = c.RequiredBlocks enc.SlowBlockThreshold = c.SlowBlockThreshold enc.SkipBcVersionCheck = c.SkipBcVersionCheck @@ -144,6 +146,7 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { TrienodeHistory *int64 `toml:",omitempty"` NodeFullValueCheckpoint *uint32 `toml:",omitempty"` StateScheme *string `toml:",omitempty"` + BinTrieGroupDepth *int `toml:",omitempty"` RequiredBlocks map[uint64]common.Hash `toml:"-"` SlowBlockThreshold *time.Duration `toml:",omitempty"` SkipBcVersionCheck *bool `toml:"-"` @@ -234,6 +237,9 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { if dec.StateScheme != nil { c.StateScheme = *dec.StateScheme } + if dec.BinTrieGroupDepth != nil { + c.BinTrieGroupDepth = *dec.BinTrieGroupDepth + } if dec.RequiredBlocks != nil { c.RequiredBlocks = dec.RequiredBlocks } diff --git a/trie/bintrie/binary_node.go b/trie/bintrie/binary_node.go index e7f57d45a2..3516bf6bd5 100644 --- a/trie/bintrie/binary_node.go +++ b/trie/bintrie/binary_node.go @@ -27,8 +27,19 @@ const ( NodeTypeBytes = 1 // Size of node type prefix in serialization HashSize = 32 // Size of a hash in bytes StemBitmapSize = 32 // Size of the bitmap in a stem node (256 values = 32 bytes) + + MaxGroupDepth = 8 ) +// bitmapSizeForDepth returns the bitmap size in bytes for a given group depth. +// For depths 1-3, returns 1 byte. For depths 4-8, returns 2^(depth-3) bytes. +func bitmapSizeForDepth(groupDepth int) int { + if groupDepth <= 3 { + return 1 + } + return 1 << (groupDepth - 3) +} + const ( nodeTypeStem = iota + 1 nodeTypeInternal diff --git a/trie/bintrie/binary_node_test.go b/trie/bintrie/binary_node_test.go index 12ac199903..857060a0c0 100644 --- a/trie/bintrie/binary_node_test.go +++ b/trie/bintrie/binary_node_test.go @@ -23,8 +23,8 @@ import ( "github.com/ethereum/go-ethereum/common" ) -// TestSerializeDeserializeInternalNode tests flat 65-byte serialization and -// deserialization of InternalNode through nodeStore. +// TestSerializeDeserializeInternalNode tests grouped serialization and +// deserialization of InternalNode through nodeStore at groupDepth=1. func TestSerializeDeserializeInternalNode(t *testing.T) { leftHash := common.HexToHash("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef") rightHash := common.HexToHash("0xfedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321") @@ -39,24 +39,32 @@ func TestSerializeDeserializeInternalNode(t *testing.T) { rootNode.right = rightRef s.root = rootRef - // Serialize the node — flat 65-byte format - serialized := s.serializeNode(rootRef) + // Serialize the node — grouped format at groupDepth=1: + // [type(1)][groupDepth(1)][bitmap(1)][leftHash(32)][rightHash(32)] = 67 bytes + serialized := s.serializeNode(rootRef, 1) - // Check the serialized format: [type(1)][leftHash(32)][rightHash(32)] if serialized[0] != nodeTypeInternal { t.Errorf("Expected type byte to be %d, got %d", nodeTypeInternal, serialized[0]) } + if serialized[1] != 1 { + t.Errorf("Expected groupDepth byte to be 1, got %d", serialized[1]) + } - expectedLen := NodeTypeBytes + 2*HashSize // 1 + 64 = 65 + expectedLen := NodeTypeBytes + 1 + 1 + 2*HashSize // type + groupDepth + bitmap + 2 hashes = 67 if len(serialized) != expectedLen { t.Errorf("Expected serialized length to be %d, got %d", expectedLen, len(serialized)) } - // Check that left and right hashes are embedded directly - if !bytes.Equal(serialized[NodeTypeBytes:NodeTypeBytes+HashSize], leftHash[:]) { + // Both children present at a 1-level group → bitmap byte = 0b11000000. + if serialized[2] != 0xc0 { + t.Errorf("Expected bitmap byte 0xc0, got 0x%02x", serialized[2]) + } + + hashesStart := NodeTypeBytes + 1 + 1 + if !bytes.Equal(serialized[hashesStart:hashesStart+HashSize], leftHash[:]) { t.Error("Left hash not found at expected position") } - if !bytes.Equal(serialized[NodeTypeBytes+HashSize:], rightHash[:]) { + if !bytes.Equal(serialized[hashesStart+HashSize:], rightHash[:]) { t.Error("Right hash not found at expected position") } @@ -116,7 +124,7 @@ func TestSerializeDeserializeStemNode(t *testing.T) { } // Serialize the node - serialized := s.serializeNode(ref) + serialized := s.serializeNode(ref, 8) // Check the serialized format if serialized[0] != nodeTypeStem { @@ -195,8 +203,9 @@ func TestDeserializeInvalidType(t *testing.T) { // TestDeserializeInvalidLength tests deserialization with invalid data length. func TestDeserializeInvalidLength(t *testing.T) { s := newNodeStore() - // InternalNode with valid type byte but wrong length (needs exactly 65 bytes) - invalidData := []byte{nodeTypeInternal, 0, 0, 0} + // InternalNode group header with groupDepth=1 (valid) and a 1-byte bitmap + // announcing two present hashes, but the hash payload is missing. + invalidData := []byte{nodeTypeInternal, 1, 0xc0} _, err := s.deserializeNode(invalidData, 0) if err == nil { @@ -208,6 +217,21 @@ func TestDeserializeInvalidLength(t *testing.T) { } } +// TestDeserializeInvalidGroupDepth tests deserialization when the group depth +// byte is out of the supported 1..MaxGroupDepth range. +func TestDeserializeInvalidGroupDepth(t *testing.T) { + s := newNodeStore() + invalidData := []byte{nodeTypeInternal, 0, 0, 0} + + _, err := s.deserializeNode(invalidData, 0) + if err == nil { + t.Fatal("Expected error for invalid group depth, got nil") + } + if err.Error() != "invalid group depth" { + t.Errorf("Expected 'invalid group depth' error, got: %v", err) + } +} + // TestKeyToPath tests the keyToPath function. func TestKeyToPath(t *testing.T) { tests := []struct { diff --git a/trie/bintrie/hashed_node_test.go b/trie/bintrie/hashed_node_test.go index ae77b7c570..2e12bfba5e 100644 --- a/trie/bintrie/hashed_node_test.go +++ b/trie/bintrie/hashed_node_test.go @@ -95,7 +95,7 @@ func TestHashedNodeInsertValuesAtStem(t *testing.T) { sn.setValue(byte(i), v) } } - serialized := rs.serializeNode(ref) + serialized := rs.serializeNode(ref, 8) validResolver := func(path []byte, hash common.Hash) ([]byte, error) { return serialized, nil diff --git a/trie/bintrie/internal_node_test.go b/trie/bintrie/internal_node_test.go index 8d5a75de8c..4d8da8af37 100644 --- a/trie/bintrie/internal_node_test.go +++ b/trie/bintrie/internal_node_test.go @@ -90,7 +90,7 @@ func TestInternalNodeGetWithResolver(t *testing.T) { ref := rs.newStemRef(stem, 1) sn := rs.getStem(ref.Index()) sn.setValue(5, common.HexToHash("0xabcd").Bytes()) - return rs.serializeNode(ref), nil + return rs.serializeNode(ref, 8), nil } return nil, errors.New("node not found") } @@ -290,10 +290,7 @@ func TestInternalNodeCollectNodes(t *testing.T) { collectedPaths = append(collectedPaths, pathCopy) } - err := s.collectNodes(s.root, []byte{1}, flushFn) - if err != nil { - t.Fatalf("Failed to collect nodes: %v", err) - } + s.collectNodes(s.root, []byte{1}, flushFn, 8) // Should have collected 3 nodes: left stem, right stem, and the internal node itself if len(collectedPaths) != 3 { diff --git a/trie/bintrie/iterator.go b/trie/bintrie/iterator.go index 31645430c3..a920f91378 100644 --- a/trie/bintrie/iterator.go +++ b/trie/bintrie/iterator.go @@ -205,7 +205,7 @@ func (it *binaryNodeIterator) Path() []byte { } func (it *binaryNodeIterator) NodeBlob() []byte { - return it.store.serializeNode(it.current) + return it.store.serializeNode(it.current, it.trie.groupDepth) } // Leaf reports whether the iterator is currently positioned at a leaf value. diff --git a/trie/bintrie/stem_node_test.go b/trie/bintrie/stem_node_test.go index 5faf903fba..ae6b57ab34 100644 --- a/trie/bintrie/stem_node_test.go +++ b/trie/bintrie/stem_node_test.go @@ -320,10 +320,7 @@ func TestStemNodeCollectNodes(t *testing.T) { collectedPaths = append(collectedPaths, pathCopy) } - err := s.collectNodes(s.root, []byte{0, 1, 0}, flushFn) - if err != nil { - t.Fatalf("Failed to collect nodes: %v", err) - } + s.collectNodes(s.root, []byte{0, 1, 0}, flushFn, 8) // Should have collected one node (itself) if len(collectedPaths) != 1 { diff --git a/trie/bintrie/store_commit.go b/trie/bintrie/store_commit.go index 7101087b51..b14bffbc6c 100644 --- a/trie/bintrie/store_commit.go +++ b/trie/bintrie/store_commit.go @@ -107,18 +107,83 @@ func (s *nodeStore) hashInternal(idx uint32) common.Hash { return node.hash } -// SerializeNode serializes a node into the flat on-disk format. -func (s *nodeStore) serializeNode(ref nodeRef) []byte { +// serializeSubtree recursively collects child hashes from a subtree of InternalNodes. +// It traverses up to `remainingDepth` levels, storing hashes of bottom-layer children. +// position tracks the current index (0 to 2^groupDepth - 1) for bitmap placement. +// hashes collects the hashes of present children, bitmap tracks which positions are present. +func (s *nodeStore) serializeSubtree(ref nodeRef, remainingDepth int, position int, absoluteDepth int, bitmap []byte, hashes *[]common.Hash) { + if remainingDepth == 0 { + // Bottom layer: store hash if not empty + switch ref.Kind() { + case kindEmpty: + // Leave bitmap bit unset, don't add hash + return + default: + // StemNode, HashedNode, or InternalNode at boundary: store hash + bitmap[position/8] |= 1 << (7 - (position % 8)) + *hashes = append(*hashes, s.computeHash(ref)) + } + return + } + switch ref.Kind() { case kindInternal: + leftPos := position * 2 + rightPos := position*2 + 1 + s.serializeSubtree(s.getInternal(ref.Index()).left, remainingDepth-1, leftPos, absoluteDepth+1, bitmap, hashes) + s.serializeSubtree(s.getInternal(ref.Index()).right, remainingDepth-1, rightPos, absoluteDepth+1, bitmap, hashes) + case kindEmpty: + return + default: + // StemNode or HashedNode encountered before reaching the group's bottom + // layer. Compute the leaf bitmap position where this node's hash will + // be stored. + leafPos := position + switch ref.Kind() { + case kindStem: + sn := s.getStem(ref.Index()) + // Extend position using the stem's key bits so that + // GetValuesAtStem traversal (which follows key bits) finds the hash. + for d := 0; d < remainingDepth; d++ { + bit := sn.Stem[(absoluteDepth+d)/8] >> (7 - ((absoluteDepth + d) % 8)) & 1 + leafPos = leafPos*2 + int(bit) + } + default: + // HashedNode or unknown: extend all-left (no key bits available). + // This matches the all-zero path that resolveNode would follow. + leafPos = position << remainingDepth + } + bitmap[leafPos/8] |= 1 << (7 - (leafPos % 8)) + *hashes = append(*hashes, s.computeHash(ref)) + } +} + +// SerializeNode serializes a node into the flat on-disk format. +func (s *nodeStore) serializeNode(ref nodeRef, groupDepth int) []byte { + switch ref.Kind() { + case kindInternal: + // InternalNode group: 1 byte type + 1 byte group depth + variable bitmap + N×32 byte hashes + bitmapSize := bitmapSizeForDepth(groupDepth) + bitmap := make([]byte, bitmapSize) + var hashes []common.Hash + node := s.getInternal(ref.Index()) - var serialized [NodeTypeBytes + HashSize + HashSize]byte + s.serializeSubtree(ref, groupDepth, 0, int(node.depth), bitmap, &hashes) + + // Build serialized output + serializedLen := NodeTypeBytes + 1 + bitmapSize + len(hashes)*HashSize + serialized := make([]byte, serializedLen) serialized[0] = nodeTypeInternal - lh := s.computeHash(node.left) - rh := s.computeHash(node.right) - copy(serialized[NodeTypeBytes:NodeTypeBytes+HashSize], lh[:]) - copy(serialized[NodeTypeBytes+HashSize:], rh[:]) - return serialized[:] + serialized[1] = byte(groupDepth) // group depth => bitmap size for a sparse group + copy(serialized[2:2+bitmapSize], bitmap) + + offset := NodeTypeBytes + 1 + bitmapSize + for _, h := range hashes { + copy(serialized[offset:offset+HashSize], h.Bytes()) + offset += HashSize + } + + return serialized case kindStem: sn := s.getStem(ref.Index()) @@ -163,6 +228,59 @@ func (s *nodeStore) deserializeNodeWithHash(serialized []byte, depth int, hn com return s.decodeNode(serialized, depth, hn, false, false) } +// deserializeSubtree reconstructs an InternalNode subtree from grouped serialization. +// remainingDepth is how many more levels to build, position is current index in the bitmap, +// nodeDepth is the actual trie depth for the node being created. +// hashIdx tracks the current position in the hash data (incremented as hashes are consumed). +func (s *nodeStore) deserializeSubtree(hn common.Hash, remainingDepth int, position int, nodeDepth int, bitmap []byte, hashData []byte, hashIdx *int, mustRecompute bool, dirty bool) (nodeRef, error) { + if remainingDepth == 0 { + // Bottom layer: check bitmap and return HashedNode or Empty + if bitmap[position/8]>>(7-(position%8))&1 == 1 { + if len(hashData) < (*hashIdx+1)*HashSize { + return emptyRef, errInvalidSerializedLength + } + hash := common.BytesToHash(hashData[*hashIdx*HashSize : (*hashIdx+1)*HashSize]) + *hashIdx++ + return s.newHashedRef(hash), nil + } + return emptyRef, nil + } + + // Check if this entire subtree is empty by examining all relevant bitmap bits + leftPos := position * 2 + rightPos := position*2 + 1 + + // note that the parent might not need root computations, but the children + // do, because their hash isn't saved. Hence `mustRecompute` is set to `true`. + left, err := s.deserializeSubtree(common.Hash{}, remainingDepth-1, leftPos, nodeDepth+1, bitmap, hashData, hashIdx, true, dirty) + if err != nil { + return emptyRef, err + } + right, err := s.deserializeSubtree(common.Hash{}, remainingDepth-1, rightPos, nodeDepth+1, bitmap, hashData, hashIdx, true, dirty) + if err != nil { + return emptyRef, err + } + + // If both children are empty, return Empty + if left.IsEmpty() && right.IsEmpty() { + return emptyRef, nil + } + + ref := s.newInternalRef(nodeDepth) + node := s.getInternal(ref.Index()) + node.left = left + node.right = right + node.mustRecompute = mustRecompute + if !mustRecompute { + // mustRecompute will only be false for the root of the subtree, + // for which we already know the hash. + node.hash = hn + node.mustRecompute = false + } + node.dirty = dirty + return ref, nil +} + func (s *nodeStore) decodeNode(serialized []byte, depth int, hn common.Hash, mustRecompute, dirty bool) (nodeRef, error) { if len(serialized) == 0 { return emptyRef, nil @@ -170,31 +288,23 @@ func (s *nodeStore) decodeNode(serialized []byte, depth int, hn common.Hash, mus switch serialized[0] { case nodeTypeInternal: - if len(serialized) != NodeTypeBytes+2*HashSize { + // Grouped format: 1 byte type + 1 byte group depth + variable bitmap + N×32 byte hashes + if len(serialized) < NodeTypeBytes+1 { return emptyRef, errInvalidSerializedLength } - var leftHash, rightHash common.Hash - copy(leftHash[:], serialized[NodeTypeBytes:NodeTypeBytes+HashSize]) - copy(rightHash[:], serialized[NodeTypeBytes+HashSize:]) + groupDepth := int(serialized[1]) + if groupDepth < 1 || groupDepth > MaxGroupDepth { + return 0, errors.New("invalid group depth") + } + bitmapSize := bitmapSizeForDepth(groupDepth) + if len(serialized) < NodeTypeBytes+1+bitmapSize { + return 0, errInvalidSerializedLength + } + bitmap := serialized[2 : 2+bitmapSize] + hashData := serialized[2+bitmapSize:] - var leftRef, rightRef nodeRef - if leftHash != (common.Hash{}) { - leftRef = s.newHashedRef(leftHash) - } - if rightHash != (common.Hash{}) { - rightRef = s.newHashedRef(rightHash) - } - - ref := s.newInternalRef(depth) - node := s.getInternal(ref.Index()) - node.left = leftRef - node.right = rightRef - if !mustRecompute { - node.hash = hn - node.mustRecompute = false - } - node.dirty = dirty - return ref, nil + hashIdx := 0 + return s.deserializeSubtree(hn, groupDepth, 0, depth, bitmap, hashData, &hashIdx, mustRecompute, dirty) case nodeTypeStem: if len(serialized) < NodeTypeBytes+StemSize+StemBitmapSize { @@ -230,45 +340,112 @@ func (s *nodeStore) decodeNode(serialized []byte, depth int, hn common.Hash, mus // CollectNodes flushes every node that needs flushing via flushfn in post-order. // Invariant: any ancestor of a node that needs flushing is itself marked, so a // clean root means the whole subtree is clean. -func (s *nodeStore) collectNodes(ref nodeRef, path []byte, flushfn nodeFlushFn) error { +func (s *nodeStore) collectNodes(ref nodeRef, path []byte, flushfn nodeFlushFn, groupDepth int) { switch ref.Kind() { - case kindEmpty: - return nil case kindInternal: node := s.getInternal(ref.Index()) if !node.dirty { - return nil + return } - // Reuse path buffer across children: flushfn consumers - // (NodeSet.AddNode, tracer.Get) clone via string(path), so in-place - // mutation is safe. - path = append(path, 0) - if err := s.collectNodes(node.left, path, flushfn); err != nil { - return err + // Only flush at group boundaries (depth % groupDepth == 0) + if int(node.depth)%groupDepth == 0 { + // We're at a group boundary - first collect any nodes in deeper groups, + // then flush this group + s.collectChildGroups(node, path, flushfn, groupDepth, groupDepth-1) + flushfn(path, s.computeHash(ref), s.serializeNode(ref, groupDepth)) + node.dirty = false + return } - path[len(path)-1] = 1 - if err := s.collectNodes(node.right, path, flushfn); err != nil { - return err - } - path = path[:len(path)-1] - flushfn(path, s.computeHash(ref), s.serializeNode(ref)) - node.dirty = false - return nil + // Not at a group boundary - this shouldn't happen if we're called correctly from root + // but handle it by continuing to traverse + s.collectChildGroups(node, path, flushfn, groupDepth, groupDepth-(int(node.depth)%groupDepth)-1) case kindStem: sn := s.getStem(ref.Index()) if !sn.dirty { - return nil + return } - flushfn(path, s.computeHash(ref), s.serializeNode(ref)) + flushfn(path, s.computeHash(ref), s.serializeNode(ref, groupDepth)) sn.dirty = false - return nil - case kindHashed: - return nil // Already committed + case kindHashed, kindEmpty: default: - return fmt.Errorf("CollectNodes: unexpected kind %d", ref.Kind()) + panic(fmt.Sprintf("CollectNodes: unexpected kind %d", ref.Kind())) } } +// collectChildGroups traverses within a group to find and collect nodes in the next group. +// remainingLevels is how many more levels below the current node until we reach the group boundary. +// When remainingLevels=0, the current node's children are at the next group boundary. +func (s *nodeStore) collectChildGroups(node *InternalNode, path []byte, flushfn nodeFlushFn, groupDepth int, remainingLevels int) error { + if remainingLevels == 0 { + // Current node is at depth (groupBoundary - 1), its children are at the next group boundary + if !node.left.IsEmpty() { + s.collectNodes(node.left, appendBit(path, 0), flushfn, groupDepth) + } + if !node.right.IsEmpty() { + s.collectNodes(node.right, appendBit(path, 1), flushfn, groupDepth) + } + return nil + } + + if !node.left.IsEmpty() { + switch node.left.Kind() { + case kindInternal: + n := s.getInternal(node.left.Index()) + if err := s.collectChildGroups(n, appendBit(path, 0), flushfn, groupDepth, remainingLevels-1); err != nil { + return err + } + default: + extPath := s.extendPathToGroupLeaf(appendBit(path, 0), node.left, remainingLevels) + s.collectNodes(node.left, extPath, flushfn, groupDepth) + } + } + if !node.right.IsEmpty() { + switch node.right.Kind() { + case kindInternal: + n := s.getInternal(node.right.Index()) + if err := s.collectChildGroups(n, appendBit(path, 1), flushfn, groupDepth, remainingLevels-1); err != nil { + return err + } + default: + extPath := s.extendPathToGroupLeaf(appendBit(path, 1), node.right, remainingLevels) + s.collectNodes(node.right, extPath, flushfn, groupDepth) + } + } + return nil +} + +// extendPathToGroupLeaf extends a storage path to the group's leaf boundary, +// matching the projection done by serializeSubtree. For StemNodes, the path +// is extended using the stem's key bits (same as serializeSubtree). For other +// node types, the path is extended with all-zero (left) bits. +func (s *nodeStore) extendPathToGroupLeaf(path []byte, node nodeRef, remainingLevels int) []byte { + if remainingLevels <= 0 { + return path + } + if node.Kind() == kindStem { + sn := s.getStem(node.Index()) + for _ = range remainingLevels { + bit := sn.Stem[len(path)/8] >> (7 - (len(path) % 8)) & 1 + path = appendBit(path, bit) + } + } else { + // HashedNode or other: all-left extension (matches serializeSubtree's + // position << remainingDepth behavior). + for _ = range remainingLevels { + path = appendBit(path, 0) + } + } + return path +} + +// appendBit appends a bit to a path, returning a new slice +func appendBit(path []byte, bit byte) []byte { + var p [256]byte + copy(p[:], path) + result := p[:len(path)] + return append(result, bit) +} + func (s *nodeStore) toDot(ref nodeRef, parent, path string) string { switch ref.Kind() { case kindInternal: diff --git a/trie/bintrie/trie.go b/trie/bintrie/trie.go index 8c69e0aa00..e3436e3df1 100644 --- a/trie/bintrie/trie.go +++ b/trie/bintrie/trie.go @@ -107,9 +107,14 @@ func ChunkifyCode(code []byte) ChunkedCode { // BinaryTrie is the implementation of https://eips.ethereum.org/EIPS/eip-7864. type BinaryTrie struct { - store *nodeStore - reader *trie.Reader - tracer *trie.PrevalueTracer + store *nodeStore + reader *trie.Reader + tracer *trie.PrevalueTracer + groupDepth int // Number of levels per serialized group (1-8, default 8) +} + +func (t *BinaryTrie) GroupDepth() int { + return t.groupDepth } // ToDot converts the binary trie to a DOT language representation. Useful for debugging. @@ -119,15 +124,20 @@ func (t *BinaryTrie) ToDot() string { } // NewBinaryTrie creates a new binary trie. -func NewBinaryTrie(root common.Hash, db database.NodeDatabase) (*BinaryTrie, error) { +// groupDepth specifies the number of levels per serialized group (1-8). +func NewBinaryTrie(root common.Hash, db database.NodeDatabase, groupDepth int) (*BinaryTrie, error) { + if groupDepth < 1 || groupDepth > MaxGroupDepth { + panic("invalid group depth size") + } reader, err := trie.NewReader(root, common.Hash{}, db) if err != nil { return nil, err } t := &BinaryTrie{ - store: newNodeStore(), - reader: reader, - tracer: trie.NewPrevalueTracer(), + store: newNodeStore(), + reader: reader, + tracer: trie.NewPrevalueTracer(), + groupDepth: groupDepth, } // Parse the root node if it's not empty if root != types.EmptyBinaryHash && root != types.EmptyRootHash { @@ -312,12 +322,9 @@ func (t *BinaryTrie) Commit(_ bool) (common.Hash, *trienode.NodeSet) { // Pre-size the path buffer: collectNodes reuses it in-place via // append/truncate; 32 covers typical binary-trie depth without regrowth. pathBuf := make([]byte, 0, 32) - err := t.store.collectNodes(t.store.root, pathBuf, func(path []byte, hash common.Hash, serialized []byte) { + t.store.collectNodes(t.store.root, pathBuf, func(path []byte, hash common.Hash, serialized []byte) { nodeset.AddNode(path, trienode.NewNodeWithPrev(hash, serialized, t.tracer.Get(path))) - }) - if err != nil { - panic(fmt.Errorf("CollectNodes failed: %v", err)) - } + }, t.groupDepth) return t.Hash(), nodeset } @@ -341,9 +348,10 @@ func (t *BinaryTrie) Prove(key []byte, proofDb ethdb.KeyValueWriter) error { // Copy creates a deep copy of the trie. func (t *BinaryTrie) Copy() *BinaryTrie { return &BinaryTrie{ - store: t.store.Copy(), - reader: t.reader, - tracer: t.tracer.Copy(), + store: t.store.Copy(), + reader: t.reader, + tracer: t.tracer.Copy(), + groupDepth: t.groupDepth, } } diff --git a/trie/bintrie/trie_test.go b/trie/bintrie/trie_test.go index 73aacb76c4..8b7d9e46d6 100644 --- a/trie/bintrie/trie_test.go +++ b/trie/bintrie/trie_test.go @@ -768,8 +768,9 @@ func TestGetStorageNonMembershipInternalRoot(t *testing.T) { // flushes only the root-to-leaf path. func TestCommitSkipCleanSubtrees(t *testing.T) { tr := &BinaryTrie{ - store: newNodeStore(), - tracer: trie.NewPrevalueTracer(), + store: newNodeStore(), + tracer: trie.NewPrevalueTracer(), + groupDepth: 1, } const n = 200 key := func(i int) [HashSize]byte { diff --git a/triedb/database.go b/triedb/database.go index 533097c9e3..0fd3e1aa91 100644 --- a/triedb/database.go +++ b/triedb/database.go @@ -31,12 +31,15 @@ import ( // Config defines all necessary options for database. type Config struct { - Preimages bool // Flag whether the preimage of node key is recorded - IsUBT bool // Flag whether the db is holding a verkle tree - HashDB *hashdb.Config // Configs for hash-based scheme - PathDB *pathdb.Config // Configs for experimental path-based scheme + Preimages bool // Flag whether the preimage of node key is recorded + IsUBT bool // Flag whether the db is holding a unified binary tree + BinTrieGroupDepth int // Number of levels per serialized group in binary trie (1-8, default 8) + HashDB *hashdb.Config // Configs for hash-based scheme + PathDB *pathdb.Config // Configs for experimental path-based scheme } +const DefaultBinTrieGroupDepth = 5 + // HashDefaults represents a config for using hash-based scheme with // default settings. var HashDefaults = &Config{ @@ -45,12 +48,13 @@ var HashDefaults = &Config{ HashDB: hashdb.Defaults, } -// UBTDefaults represents a config for holding verkle trie data +// UBTDefaults represents a config for holding unified binary trie data // using path-based scheme with default settings. var UBTDefaults = &Config{ - Preimages: false, - IsUBT: true, - PathDB: pathdb.Defaults, + Preimages: false, + IsUBT: true, + BinTrieGroupDepth: DefaultBinTrieGroupDepth, + PathDB: pathdb.Defaults, } // backend defines the methods needed to access/update trie nodes in different @@ -393,3 +397,7 @@ func (db *Database) SnapshotCompleted() bool { } return pdb.SnapshotCompleted() } + +func (db *Database) BinTrieGroupDepth() int { + return db.config.BinTrieGroupDepth +} diff --git a/triedb/pathdb/journal.go b/triedb/pathdb/journal.go index efcc3f2549..657fbbff27 100644 --- a/triedb/pathdb/journal.go +++ b/triedb/pathdb/journal.go @@ -161,7 +161,19 @@ func loadGenerator(db ethdb.KeyValueReader, hash nodeHasher) (*journalGenerator, // loadLayers loads a pre-existing state layer backed by a key-value store. func (db *Database) loadLayers() layer { // Retrieve the root node of persistent state. - root, err := db.hasher(rawdb.ReadAccountTrieNode(db.diskdb, nil)) + var ( + root common.Hash + err error + ) + if db.isUBT { + root = rawdb.ReadSnapshotRoot(db.diskdb) + if root == (common.Hash{}) { + root = types.EmptyBinaryHash + } + } else { + blob := rawdb.ReadAccountTrieNode(db.diskdb, nil) + root, err = db.hasher(blob) + } if err != nil { log.Crit("Failed to compute node hash", "err", err) } From b9c5fe6d26342d625c9a393bef9ccd5209c6d888 Mon Sep 17 00:00:00 2001 From: felipe Date: Fri, 1 May 2026 16:38:33 +0200 Subject: [PATCH 02/63] eth/tracers: fix evm trace for t8n (#34862) The mux tracer fanned out every standard hook to its children but never forwarded OnSystemCall{Start,End}. Tracers that rely on these - like `logger.jsonLogger`, which uses the start hook to silence its opcode hook for the duration of a system call - never got the signal when wrapped behind a mux. In evm t8n, combining `--trace` with `--opcode-count` (default for geth with exec specs) produces exactly that wrapping. The first system call (e.g. `ProcessBeaconBlockRoot`) then fires `OnOpcode` on the json logger before any `OnTxStart` has run, dereferencing a nil env and crashing t8n. Forward both hooks through the mux. The V2 fan-out falls back to V1 for children that only implement the legacy hook, mirroring the precedence already used in `core/state_processor.go`. --- eth/tracers/native/mux.go | 44 ++++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/eth/tracers/native/mux.go b/eth/tracers/native/mux.go index b7d6f29a6a..362fb24297 100644 --- a/eth/tracers/native/mux.go +++ b/eth/tracers/native/mux.go @@ -67,18 +67,20 @@ func NewMuxTracer(names []string, objects []*tracers.Tracer) (*tracers.Tracer, e t := &muxTracer{names: names, tracers: objects} return &tracers.Tracer{ Hooks: &tracing.Hooks{ - OnTxStart: t.OnTxStart, - OnTxEnd: t.OnTxEnd, - OnEnter: t.OnEnter, - OnExit: t.OnExit, - OnOpcode: t.OnOpcode, - OnFault: t.OnFault, - OnGasChange: t.OnGasChange, - OnBalanceChange: t.OnBalanceChange, - OnNonceChange: t.OnNonceChange, - OnCodeChange: t.OnCodeChange, - OnStorageChange: t.OnStorageChange, - OnLog: t.OnLog, + OnTxStart: t.OnTxStart, + OnTxEnd: t.OnTxEnd, + OnEnter: t.OnEnter, + OnExit: t.OnExit, + OnOpcode: t.OnOpcode, + OnFault: t.OnFault, + OnGasChange: t.OnGasChange, + OnBalanceChange: t.OnBalanceChange, + OnNonceChange: t.OnNonceChange, + OnCodeChange: t.OnCodeChange, + OnStorageChange: t.OnStorageChange, + OnLog: t.OnLog, + OnSystemCallStartV2: t.OnSystemCallStart, + OnSystemCallEnd: t.OnSystemCallEnd, }, GetResult: t.GetResult, Stop: t.Stop, @@ -189,6 +191,24 @@ func (t *muxTracer) OnLog(log *types.Log) { } } +func (t *muxTracer) OnSystemCallStart(vm *tracing.VMContext) { + for _, t := range t.tracers { + if t.OnSystemCallStartV2 != nil { + t.OnSystemCallStartV2(vm) + } else if t.OnSystemCallStart != nil { + t.OnSystemCallStart() + } + } +} + +func (t *muxTracer) OnSystemCallEnd() { + for _, t := range t.tracers { + if t.OnSystemCallEnd != nil { + t.OnSystemCallEnd() + } + } +} + // GetResult returns an empty json object. func (t *muxTracer) GetResult() (json.RawMessage, error) { resObject := make(map[string]json.RawMessage) From 41b856d472153296c14b145514f202ad3e67c683 Mon Sep 17 00:00:00 2001 From: Richard Creighton Date: Fri, 1 May 2026 19:18:14 +0100 Subject: [PATCH 03/63] ethclient: add maxUsedGas to simulate call results (#34820) This updates the typed `ethclient` model for `eth_simulateV1` call results to include `maxUsedGas`, matching the field already returned by the server-side RPC response. Follow-up to #32789. --- ethclient/ethclient.go | 2 ++ ethclient/ethclient_test.go | 6 ++++++ ethclient/gen_simulate_call_result.go | 6 ++++++ 3 files changed, 14 insertions(+) diff --git a/ethclient/ethclient.go b/ethclient/ethclient.go index 412f8955ba..1d8573f982 100644 --- a/ethclient/ethclient.go +++ b/ethclient/ethclient.go @@ -914,6 +914,7 @@ type SimulateCallResult struct { ReturnValue []byte `json:"returnData"` Logs []*types.Log `json:"logs"` GasUsed uint64 `json:"gasUsed"` + MaxUsedGas uint64 `json:"maxUsedGas"` Status uint64 `json:"status"` Error *CallError `json:"error,omitempty"` } @@ -921,6 +922,7 @@ type SimulateCallResult struct { type simulateCallResultMarshaling struct { ReturnValue hexutil.Bytes GasUsed hexutil.Uint64 + MaxUsedGas hexutil.Uint64 Status hexutil.Uint64 } diff --git a/ethclient/ethclient_test.go b/ethclient/ethclient_test.go index f9e761e412..fb04d77669 100644 --- a/ethclient/ethclient_test.go +++ b/ethclient/ethclient_test.go @@ -861,6 +861,12 @@ func TestSimulateV1(t *testing.T) { if results[0].Calls[0].Error != nil { t.Errorf("expected no error, got %v", results[0].Calls[0].Error) } + if results[0].Calls[0].MaxUsedGas == 0 { + t.Error("expected maxUsedGas to be set") + } + if results[0].Calls[0].MaxUsedGas < results[0].Calls[0].GasUsed { + t.Errorf("expected maxUsedGas >= gasUsed, got %d < %d", results[0].Calls[0].MaxUsedGas, results[0].Calls[0].GasUsed) + } } func TestSimulateV1WithBlockOverrides(t *testing.T) { diff --git a/ethclient/gen_simulate_call_result.go b/ethclient/gen_simulate_call_result.go index 55e14cd697..18373bbb88 100644 --- a/ethclient/gen_simulate_call_result.go +++ b/ethclient/gen_simulate_call_result.go @@ -17,6 +17,7 @@ func (s SimulateCallResult) MarshalJSON() ([]byte, error) { ReturnValue hexutil.Bytes `json:"returnData"` Logs []*types.Log `json:"logs"` GasUsed hexutil.Uint64 `json:"gasUsed"` + MaxUsedGas hexutil.Uint64 `json:"maxUsedGas"` Status hexutil.Uint64 `json:"status"` Error *CallError `json:"error,omitempty"` } @@ -24,6 +25,7 @@ func (s SimulateCallResult) MarshalJSON() ([]byte, error) { enc.ReturnValue = s.ReturnValue enc.Logs = s.Logs enc.GasUsed = hexutil.Uint64(s.GasUsed) + enc.MaxUsedGas = hexutil.Uint64(s.MaxUsedGas) enc.Status = hexutil.Uint64(s.Status) enc.Error = s.Error return json.Marshal(&enc) @@ -35,6 +37,7 @@ func (s *SimulateCallResult) UnmarshalJSON(input []byte) error { ReturnValue *hexutil.Bytes `json:"returnData"` Logs []*types.Log `json:"logs"` GasUsed *hexutil.Uint64 `json:"gasUsed"` + MaxUsedGas *hexutil.Uint64 `json:"maxUsedGas"` Status *hexutil.Uint64 `json:"status"` Error *CallError `json:"error,omitempty"` } @@ -51,6 +54,9 @@ func (s *SimulateCallResult) UnmarshalJSON(input []byte) error { if dec.GasUsed != nil { s.GasUsed = uint64(*dec.GasUsed) } + if dec.MaxUsedGas != nil { + s.MaxUsedGas = uint64(*dec.MaxUsedGas) + } if dec.Status != nil { s.Status = uint64(*dec.Status) } From d270e211d1bfdee7e1a4f9f03b0898327636f85a Mon Sep 17 00:00:00 2001 From: cui Date: Sat, 2 May 2026 02:22:48 +0800 Subject: [PATCH 04/63] core/state/snapshot: fix condition in iterator traversal test (#34638) Fixes a condition in a snapshot-related test. --- core/state/snapshot/iterator_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/state/snapshot/iterator_test.go b/core/state/snapshot/iterator_test.go index a95bd66dde..8e473aa312 100644 --- a/core/state/snapshot/iterator_test.go +++ b/core/state/snapshot/iterator_test.go @@ -441,7 +441,7 @@ func TestStorageIteratorTraversalValues(t *testing.T) { if i%8 == 0 { e[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 4, i) } - if i > 50 || i < 85 { + if i > 50 && i < 85 { f[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 5, i) } if i%64 == 0 { From 8656efcf5b2bf1e377fe162489d52dd09afac2e4 Mon Sep 17 00:00:00 2001 From: hero5512 Date: Fri, 1 May 2026 14:27:57 -0400 Subject: [PATCH 05/63] ethclient/simulated: disable log indexing by default (#32594) Disables the recently added log indexer from a simulated backend. In most cases the log indexer is not required and unindexed search should be fast enough. Fixes https://github.com/ethereum/go-ethereum/issues/32552. --- ethclient/simulated/backend.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ethclient/simulated/backend.go b/ethclient/simulated/backend.go index d573c7e750..160ad924bf 100644 --- a/ethclient/simulated/backend.go +++ b/ethclient/simulated/backend.go @@ -86,6 +86,8 @@ func NewBackend(alloc types.GenesisAlloc, options ...func(nodeConf *node.Config, } ethConf.SyncMode = ethconfig.FullSync ethConf.TxPool.NoLocals = true + // Disable log indexing to force unindexed log search + ethConf.LogNoHistory = true for _, option := range options { option(&nodeConf, ðConf) From 7155c65abbeec23981ea10ecdbeb1a661426e934 Mon Sep 17 00:00:00 2001 From: rayoo Date: Sat, 2 May 2026 02:32:49 +0800 Subject: [PATCH 06/63] internal/ethapi: apply block overrides to header in eth_call (#34842) Apply block overrides to header in eth_call so EIP-1559 fee fields use the correct overridden basefee. --- internal/ethapi/api.go | 4 ++++ internal/ethapi/api_test.go | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index e8669b86c6..6fc43a370b 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -734,6 +734,10 @@ func doCall(ctx context.Context, b Backend, args TransactionArgs, state *state.S if err := blockOverrides.Apply(&blockCtx); err != nil { return nil, err } + // Override the header so callers that compute gas price from 1559 fee + // fields see the overridden basefee. Otherwise GASPRICE/effectiveTip + // would be derived from the pre-override basefee. + header = blockOverrides.MakeHeader(header) } rules := b.ChainConfig().Rules(blockCtx.BlockNumber, blockCtx.Random != nil, blockCtx.Time) precompiles := vm.ActivePrecompiledContracts(rules) diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index 6cf52d636a..161d97b4eb 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -1315,6 +1315,27 @@ func TestCall(t *testing.T) { }, expectErr: errors.New(`block override "withdrawals" is not supported for this RPC method`), }, + // Verify that an overridden basefee is honored when computing gasPrice + // from the 1559 fee fields. Returning GASPRICE opcode; expected value + // is min(MaxFeePerGas, MaxPriorityFeePerGas + overridden BaseFee). + // + // BaseFee override = 0xa (10); MaxFeePerGas = 0x64 (100); + // MaxPriorityFeePerGas = 0x2 (2); expected GASPRICE = 12. + { + name: "basefee-override-used-in-gasprice", + blockNumber: rpc.LatestBlockNumber, + call: TransactionArgs{ + From: &accounts[0].addr, + // Contract: GASPRICE; PUSH1 0; MSTORE; PUSH1 32; PUSH1 0; RETURN + Input: hex2Bytes("3a60005260206000f3"), + MaxFeePerGas: (*hexutil.Big)(big.NewInt(100)), + MaxPriorityFeePerGas: (*hexutil.Big)(big.NewInt(2)), + }, + blockOverrides: override.BlockOverrides{ + BaseFeePerGas: (*hexutil.Big)(big.NewInt(10)), + }, + want: "0x000000000000000000000000000000000000000000000000000000000000000c", + }, } for _, tc := range testSuite { result, err := api.Call(context.Background(), tc.call, &rpc.BlockNumberOrHash{BlockNumber: &tc.blockNumber}, &tc.overrides, &tc.blockOverrides) From f0b21fa11061d3f46de621e38cbbc82c1dc2f819 Mon Sep 17 00:00:00 2001 From: Rahman Date: Sat, 2 May 2026 05:29:21 -0600 Subject: [PATCH 07/63] core/txpool/blobpool: fix gapped queue size cap (#34831) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit the gapped queue cap was effectively per-sender rather than total — a sender pool spread across enough distinct addresses could grow `p.gapped` well past `maxGapped`, defeating the resource bound. `maxGapped` was being compared against `len(p.gapped)`, which is a `map[address][]tx` and counts unique senders, not queued txs. Switched the check to `len(p.gappedSource)` (keyed by tx hash, so its length is the real total). Also wired up a `blobpool/gapped/count` gauge plus `promoted`, `evicted`, and `gappedfull` meters so queue size and churn are actually observable in prod. --- core/txpool/blobpool/blobpool.go | 12 +++++++++--- core/txpool/blobpool/metrics.go | 6 ++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/core/txpool/blobpool/blobpool.go b/core/txpool/blobpool/blobpool.go index 4030a0c339..efa41a0649 100644 --- a/core/txpool/blobpool/blobpool.go +++ b/core/txpool/blobpool/blobpool.go @@ -1596,9 +1596,10 @@ func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error // Store the tx in memory, and revalidate later from, _ := types.Sender(p.signer, tx) allowance := p.gappedAllowance(from) - if allowance >= 1 && len(p.gapped) < maxGapped { + if allowance >= 1 && len(p.gappedSource) < maxGapped { p.gapped[from] = append(p.gapped[from], tx) p.gappedSource[tx.Hash()] = from + gappedGauge.Update(int64(len(p.gappedSource))) log.Trace("added tx to gapped blob queue", "allowance", allowance, "hash", tx.Hash(), "from", from, "nonce", tx.Nonce(), "qlen", len(p.gapped[from])) return nil } else { @@ -1606,6 +1607,7 @@ func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error // transactions by keeping the old and dropping this one. // Thus replacing a gapped transaction with another gapped transaction // is discouraged. + addGappedFullMeter.Mark(1) log.Trace("no gapped blob queue allowance", "allowance", allowance, "hash", tx.Hash(), "from", from, "nonce", tx.Nonce(), "qlen", len(p.gapped[from])) } case errors.Is(err, core.ErrInsufficientFunds): @@ -1791,6 +1793,7 @@ func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error // We do not recurse here, but continue to loop instead. // We are under lock, so we can add the transaction directly. if err := p.addLocked(tx, false); err == nil { + gappedPromotedMeter.Mark(1) log.Trace("Gapped blob transaction added to pool", "hash", tx.Hash(), "from", from, "nonce", tx.Nonce(), "qlen", len(p.gapped[from])) } else { log.Trace("Gapped blob transaction not accepted", "hash", tx.Hash(), "from", from, "nonce", tx.Nonce(), "err", err) @@ -1802,6 +1805,7 @@ func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error } else { p.gapped[from] = gtxs } + gappedGauge.Update(int64(len(p.gappedSource))) } return nil } @@ -2069,8 +2073,9 @@ func (p *BlobPool) evictGapped() { keep = append(keep, gtx) } } - if len(keep) < len(txs) { - log.Trace("Evicting old gapped blob transactions", "count", len(txs)-len(keep), "from", from) + if evicted := len(txs) - len(keep); evicted > 0 { + gappedEvictedMeter.Mark(int64(evicted)) + log.Trace("Evicting old gapped blob transactions", "count", evicted, "from", from) } if len(keep) == 0 { delete(p.gapped, from) @@ -2078,6 +2083,7 @@ func (p *BlobPool) evictGapped() { p.gapped[from] = keep } } + gappedGauge.Update(int64(len(p.gappedSource))) } // isAnnouncable checks whether a transaction is announcable based on its diff --git a/core/txpool/blobpool/metrics.go b/core/txpool/blobpool/metrics.go index 52419ade09..44e2098b22 100644 --- a/core/txpool/blobpool/metrics.go +++ b/core/txpool/blobpool/metrics.go @@ -97,9 +97,15 @@ var ( addUnderpricedMeter = metrics.NewRegisteredMeter("blobpool/add/underpriced", nil) // Gas tip too low, neutral addStaleMeter = metrics.NewRegisteredMeter("blobpool/add/stale", nil) // Nonce already filled, reject, bad-ish addGappedMeter = metrics.NewRegisteredMeter("blobpool/add/gapped", nil) // Nonce gapped, reject, bad-ish + addGappedFullMeter = metrics.NewRegisteredMeter("blobpool/add/gappedfull", nil) // Gapped queue full, reject, neutral addOverdraftedMeter = metrics.NewRegisteredMeter("blobpool/add/overdrafted", nil) // Balance exceeded, reject, neutral addOvercappedMeter = metrics.NewRegisteredMeter("blobpool/add/overcapped", nil) // Per-account cap exceeded, reject, neutral addNoreplaceMeter = metrics.NewRegisteredMeter("blobpool/add/noreplace", nil) // Replacement fees or tips too low, neutral addNonExclusiveMeter = metrics.NewRegisteredMeter("blobpool/add/nonexclusive", nil) // Plain transaction from same account exists, reject, neutral addValidMeter = metrics.NewRegisteredMeter("blobpool/add/valid", nil) // Valid transaction, add, neutral + + // Gapped queue metrics for observability + gappedGauge = metrics.NewRegisteredGauge("blobpool/gapped/count", nil) // Current gapped queue size + gappedPromotedMeter = metrics.NewRegisteredMeter("blobpool/gapped/promoted", nil) // Gapped txs successfully promoted to pool + gappedEvictedMeter = metrics.NewRegisteredMeter("blobpool/gapped/evicted", nil) // Gapped txs evicted due to timeout/stale ) From efd6cdcff111c37bf49aefba0a7376997780a8c8 Mon Sep 17 00:00:00 2001 From: rayoo Date: Tue, 5 May 2026 03:36:26 +0800 Subject: [PATCH 08/63] eth/tracers: forward V2 state hooks through mux tracer (#34869) Fixes the muxTracer to correctly forward events to v2 state hooks, i.e. `OnCodeChangeV2` and `OnNonceChangeV2`. --- eth/tracers/native/mux.go | 26 +++++----- eth/tracers/native/mux_test.go | 87 ++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 12 deletions(-) create mode 100644 eth/tracers/native/mux_test.go diff --git a/eth/tracers/native/mux.go b/eth/tracers/native/mux.go index 362fb24297..bb2101deec 100644 --- a/eth/tracers/native/mux.go +++ b/eth/tracers/native/mux.go @@ -63,6 +63,12 @@ func newMuxTracerFromConfig(ctx *tracers.Context, cfg json.RawMessage, chainConf // // The names parameter associates a label with each tracer, used as keys in // the aggregated JSON result returned by GetResult. +// +// For hooks that have both a V1 and V2 form (OnCodeChange / OnCodeChangeV2, +// OnNonceChange / OnNonceChangeV2, OnSystemCallStart / OnSystemCallStartV2), +// the mux exposes only the V2 variant upward. The fanout then prefers each +// child's V2 hook and falls back to V1 if only V1 is set, mirroring the +// precedence already used in core/state_processor.go. func NewMuxTracer(names []string, objects []*tracers.Tracer) (*tracers.Tracer, error) { t := &muxTracer{names: names, tracers: objects} return &tracers.Tracer{ @@ -75,8 +81,8 @@ func NewMuxTracer(names []string, objects []*tracers.Tracer) (*tracers.Tracer, e OnFault: t.OnFault, OnGasChange: t.OnGasChange, OnBalanceChange: t.OnBalanceChange, - OnNonceChange: t.OnNonceChange, - OnCodeChange: t.OnCodeChange, + OnNonceChangeV2: t.OnNonceChangeV2, + OnCodeChangeV2: t.OnCodeChangeV2, OnStorageChange: t.OnStorageChange, OnLog: t.OnLog, OnSystemCallStartV2: t.OnSystemCallStart, @@ -151,26 +157,22 @@ func (t *muxTracer) OnBalanceChange(a common.Address, prev, new *big.Int, reason } } -func (t *muxTracer) OnNonceChange(a common.Address, prev, new uint64) { +func (t *muxTracer) OnNonceChangeV2(a common.Address, prev, new uint64, reason tracing.NonceChangeReason) { for _, t := range t.tracers { - if t.OnNonceChange != nil { + if t.OnNonceChangeV2 != nil { + t.OnNonceChangeV2(a, prev, new, reason) + } else if t.OnNonceChange != nil { t.OnNonceChange(a, prev, new) } } } -func (t *muxTracer) OnCodeChange(a common.Address, prevCodeHash common.Hash, prev []byte, codeHash common.Hash, code []byte) { - for _, t := range t.tracers { - if t.OnCodeChange != nil { - t.OnCodeChange(a, prevCodeHash, prev, codeHash, code) - } - } -} - func (t *muxTracer) OnCodeChangeV2(a common.Address, prevCodeHash common.Hash, prev []byte, codeHash common.Hash, code []byte, reason tracing.CodeChangeReason) { for _, t := range t.tracers { if t.OnCodeChangeV2 != nil { t.OnCodeChangeV2(a, prevCodeHash, prev, codeHash, code, reason) + } else if t.OnCodeChange != nil { + t.OnCodeChange(a, prevCodeHash, prev, codeHash, code) } } } diff --git a/eth/tracers/native/mux_test.go b/eth/tracers/native/mux_test.go new file mode 100644 index 0000000000..902b7a026a --- /dev/null +++ b/eth/tracers/native/mux_test.go @@ -0,0 +1,87 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package native + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/eth/tracers" +) + +// TestMuxForwardsV2StateHooks verifies that the mux tracer fans out the V2 +// variants of state-change hooks to child tracers. A child tracer that only +// implements OnCodeChangeV2 / OnNonceChangeV2 must still receive events when +// wrapped behind the mux. The mux must also fall back to the V1 hook when a +// child only implements V1, mirroring the precedence used in +// core/state_processor.go. +func TestMuxForwardsV2StateHooks(t *testing.T) { + var ( + codeV2Calls int + nonceV2Calls int + codeV1Calls int + nonceV1Calls int + ) + v2Child := &tracers.Tracer{ + Hooks: &tracing.Hooks{ + OnCodeChangeV2: func(addr common.Address, prevCodeHash common.Hash, prevCode []byte, codeHash common.Hash, code []byte, reason tracing.CodeChangeReason) { + codeV2Calls++ + }, + OnNonceChangeV2: func(addr common.Address, prev, new uint64, reason tracing.NonceChangeReason) { + nonceV2Calls++ + }, + }, + } + v1Child := &tracers.Tracer{ + Hooks: &tracing.Hooks{ + OnCodeChange: func(addr common.Address, prevCodeHash common.Hash, prevCode []byte, codeHash common.Hash, code []byte) { + codeV1Calls++ + }, + OnNonceChange: func(addr common.Address, prev, new uint64) { + nonceV1Calls++ + }, + }, + } + mux, err := NewMuxTracer([]string{"v2", "v1"}, []*tracers.Tracer{v2Child, v1Child}) + if err != nil { + t.Fatalf("NewMuxTracer: %v", err) + } + + if mux.Hooks.OnCodeChangeV2 == nil { + t.Fatal("mux does not expose OnCodeChangeV2; V2-only child tracers will miss code changes") + } + if mux.Hooks.OnNonceChangeV2 == nil { + t.Fatal("mux does not expose OnNonceChangeV2; V2-only child tracers will miss nonce changes") + } + + mux.Hooks.OnCodeChangeV2(common.Address{}, common.Hash{}, nil, common.Hash{}, nil, tracing.CodeChangeContractCreation) + mux.Hooks.OnNonceChangeV2(common.Address{}, 0, 1, tracing.NonceChangeEoACall) + + if codeV2Calls != 1 { + t.Fatalf("V2 child OnCodeChangeV2 got %d calls, want 1", codeV2Calls) + } + if nonceV2Calls != 1 { + t.Fatalf("V2 child OnNonceChangeV2 got %d calls, want 1", nonceV2Calls) + } + if codeV1Calls != 1 { + t.Fatalf("V1 child OnCodeChange got %d calls, want 1 (mux should fall back from V2 to V1)", codeV1Calls) + } + if nonceV1Calls != 1 { + t.Fatalf("V1 child OnNonceChange got %d calls, want 1 (mux should fall back from V2 to V1)", nonceV1Calls) + } +} From d5edb8043824333459a7bd02bcd05868b0d0d997 Mon Sep 17 00:00:00 2001 From: TenderDeve Date: Tue, 5 May 2026 14:59:26 +0530 Subject: [PATCH 09/63] accounts/abi/bind: re-export event signature errors (#34868) Re-exports errors in bind package. --------- Co-authored-by: Marius van der Wijden --- accounts/abi/bind/old.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/accounts/abi/bind/old.go b/accounts/abi/bind/old.go index b09f5f3c7a..1fe1b1cca5 100644 --- a/accounts/abi/bind/old.go +++ b/accounts/abi/bind/old.go @@ -176,6 +176,13 @@ var ( // ErrNoCodeAfterDeploy is returned by WaitDeployed if contract creation leaves // an empty contract behind. ErrNoCodeAfterDeploy = bind2.ErrNoCodeAfterDeploy + + // ErrNoEventSignature is returned when a log entry has no topics. + ErrNoEventSignature = bind2.ErrNoEventSignature + + // ErrEventSignatureMismatch is returned when a log's topic[0] does not match + // the expected event signature. + ErrEventSignatureMismatch = bind2.ErrEventSignatureMismatch ) // ContractCaller defines the methods needed to allow operating with a contract on a read From 5b837e578689e7db5679b25d0a159956563f6a93 Mon Sep 17 00:00:00 2001 From: cui Date: Tue, 5 May 2026 18:41:22 +0800 Subject: [PATCH 10/63] eth/downloader: use batch index in deliver reconstruct (#34870) The reconstruct callback indexes parallel response slices (bodies, receipts). Passing the accept counter used the wrong element when an earlier header in the same batch hit a stale slot. --- eth/downloader/queue.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eth/downloader/queue.go b/eth/downloader/queue.go index dd17b7f1ed..585906b8bd 100644 --- a/eth/downloader/queue.go +++ b/eth/downloader/queue.go @@ -689,9 +689,9 @@ func (q *queue) deliver(id string, taskPool map[common.Hash]*types.Header, i++ } - for _, header := range request.Headers[:i] { + for k, header := range request.Headers[:i] { if res, stale, err := q.resultCache.GetDeliverySlot(header.Number.Uint64()); err == nil && !stale { - reconstruct(accepted, res) + reconstruct(k, res) accepted++ } else { // Between here and above, some other peer filled this result, From 60db25b070d5060a379231e6e0f69c047d0ffcbb Mon Sep 17 00:00:00 2001 From: rayoo Date: Tue, 5 May 2026 21:28:28 +0800 Subject: [PATCH 11/63] p2p/discover: restore nextTimeout update in UDPv4 resetTimeout loop (#34878) The refactor from `for el := plist.Front(); ...; el = el.Next()` to the new `iterList` iterator in #34743 silently dropped two things needed by resetTimeout: 1. `nextTimeout = el.Value.(*replyMatcher)` at the top of the loop. This assignment is what gives `nextTimeout` its documented meaning ("head of plist when timeout was last reset"), and what makes the early-return optimization at the top of resetTimeout work. Without it, nextTimeout is only ever written to nil, so `nextTimeout == plist.Front().Value` is always false and the optimization is dead. 2. `nextTimeout.errc <- errClockWarp` in the clock-warp branch now reads a stale or nil pointer. Prior to the refactor, the inner assignment kept nextTimeout pointing at the current matcher so its errc was the right channel to receive the errClockWarp signal. After the refactor, on first entry into the clock-warp branch nextTimeout is nil, which panics the UDPv4 loop goroutine with a nil pointer deref and takes discv4 down. Re-assign `nextTimeout = p` at the head of the loop (restoring the documented invariant) and send the clock-warp error on `p.errc` rather than the now-stale `nextTimeout.errc`. The clock-warp branch triggers only when the system clock jumps backward after a deadline is assigned (deadline - time.Now() >= 2*respTimeout, i.e. at least ~500ms backward jump), which is why this regression slipped past CI - it is not exercised by any existing unit test, and writing one would require plumbing a clock through the loop. --- p2p/discover/v4_udp.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/p2p/discover/v4_udp.go b/p2p/discover/v4_udp.go index 9e824dae18..b06db4bdb2 100644 --- a/p2p/discover/v4_udp.go +++ b/p2p/discover/v4_udp.go @@ -447,6 +447,7 @@ func (t *UDPv4) loop() { // Start the timer so it fires when the next pending reply has expired. now := time.Now() for p, el := range iterList[*replyMatcher](plist) { + nextTimeout = p if dist := p.deadline.Sub(now); dist < 2*respTimeout { timeout.Reset(dist) return @@ -454,7 +455,7 @@ func (t *UDPv4) loop() { // Remove pending replies whose deadline is too far in the // future. These can occur if the system clock jumped // backwards after the deadline was assigned. - nextTimeout.errc <- errClockWarp + p.errc <- errClockWarp plist.Remove(el) } nextTimeout = nil From 4ff33ba1b6ecfff7087136136ed13dd8c3a3ac38 Mon Sep 17 00:00:00 2001 From: Andrii Furmanets Date: Tue, 5 May 2026 23:17:09 +0300 Subject: [PATCH 12/63] internal/download: only report stale existing downloads (#34849) ## Summary Fixes #31917. `geth era-download` now only prints `is stale` when an existing downloaded file fails checksum verification. Missing files are still downloaded normally, but no longer get mislabeled as stale. ## Why `DownloadFile` used `verifyHash` for both missing files and checksum mismatches, then printed `is stale` for any error. This made first-time downloads look like corrupt or outdated files. ## Validation - `make all` - `go run ./build/ci.go test` - `go run ./build/ci.go lint` - `go run ./build/ci.go check_generate` - `go run ./build/ci.go check_baddeps` --------- Co-authored-by: Felix Lange --- internal/download/download.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/download/download.go b/internal/download/download.go index c59c8a90c3..79cf27a56b 100644 --- a/internal/download/download.go +++ b/internal/download/download.go @@ -22,6 +22,7 @@ import ( "bytes" "crypto/sha256" "encoding/hex" + "errors" "fmt" "io" "iter" @@ -180,12 +181,13 @@ func (db *ChecksumDB) DownloadFile(url, dstPath string) error { return fmt.Errorf("no known hash for file %q", basename) } // Shortcut if already downloaded. - if verifyHash(dstPath, hash) == nil { + if err := verifyHash(dstPath, hash); err == nil { fmt.Printf("%s is up-to-date\n", dstPath) return nil + } else if !errors.Is(err, os.ErrNotExist) { + fmt.Printf("%s is stale\n", dstPath) } - fmt.Printf("%s is stale\n", dstPath) fmt.Printf("downloading from %s\n", url) resp, err := http.Get(url) if err != nil { From 84949107ce4592f901ef77ab082121432bf4b5f6 Mon Sep 17 00:00:00 2001 From: cui Date: Wed, 6 May 2026 15:18:12 +0800 Subject: [PATCH 13/63] beacon/engine: fix wrong presize bound (#34860) --- beacon/engine/types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon/engine/types.go b/beacon/engine/types.go index a312fee88a..9b0b186df7 100644 --- a/beacon/engine/types.go +++ b/beacon/engine/types.go @@ -276,7 +276,7 @@ func ExecutableDataToBlockNoHash(data ExecutableData, versionedHashes []common.H if data.BaseFeePerGas != nil && (data.BaseFeePerGas.Sign() == -1 || data.BaseFeePerGas.BitLen() > 256) { return nil, fmt.Errorf("invalid baseFeePerGas: %v", data.BaseFeePerGas) } - var blobHashes = make([]common.Hash, 0, len(txs)) + var blobHashes = make([]common.Hash, 0, len(versionedHashes)) for _, tx := range txs { blobHashes = append(blobHashes, tx.BlobHashes()...) } From 4d2af275e1fe34eeafc50bbb61ea5bb96f11b10c Mon Sep 17 00:00:00 2001 From: Marius van der Wijden Date: Wed, 6 May 2026 09:59:51 +0200 Subject: [PATCH 14/63] eth/catalyst: allow reorging the head block to a parent (#34767) Implements https://github.com/ethereum/execution-apis/pull/786/changes as discussed on standup today --------- Co-authored-by: Gary Rong --- beacon/engine/errors.go | 1 + core/blockchain.go | 9 +++++++-- eth/catalyst/api.go | 25 +++++++++++++++++++++---- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/beacon/engine/errors.go b/beacon/engine/errors.go index 62773a0ea9..80e13b11b9 100644 --- a/beacon/engine/errors.go +++ b/beacon/engine/errors.go @@ -81,6 +81,7 @@ var ( TooLargeRequest = &EngineAPIError{code: -38004, msg: "Too large request"} InvalidParams = &EngineAPIError{code: -32602, msg: "Invalid parameters"} UnsupportedFork = &EngineAPIError{code: -38005, msg: "Unsupported fork"} + TooDeepReorg = &EngineAPIError{code: -38006, msg: "Too deep reorg"} STATUS_INVALID = ForkChoiceResponse{PayloadStatus: PayloadStatusV1{Status: INVALID}, PayloadID: nil} STATUS_SYNCING = ForkChoiceResponse{PayloadStatus: PayloadStatusV1{Status: SYNCING}, PayloadID: nil} diff --git a/core/blockchain.go b/core/blockchain.go index f21a1462ea..2b49111121 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -2596,8 +2596,13 @@ func (bc *BlockChain) reorg(oldHead *types.Header, newHead *types.Header) error blockReorgAddMeter.Mark(int64(len(newChain))) } else { // len(newChain) == 0 && len(oldChain) > 0 - // rewind the canonical chain to a lower point. - log.Error("Impossible reorg, please file an issue", "oldnum", oldHead.Number, "oldhash", oldHead.Hash(), "oldblocks", len(oldChain), "newnum", newHead.Number, "newhash", newHead.Hash(), "newblocks", len(newChain)) + // Rewind the canonical chain to a lower point. In EPBs we can reorg to + // a parent of the head within 32 blocks. + if len(oldChain) > 32 { + log.Error("Impossible reorg, please file an issue", "oldnum", oldHead.Number, "oldhash", oldHead.Hash(), "oldblocks", len(oldChain)) + } else { + log.Info("Shorten chain", "del", len(oldChain), "number", oldHead.Number, "hash", oldHead.Hash()) + } } // Acquire the tx-lookup lock before mutation. This step is essential // as the txlookups should be changed atomically, and all subsequent diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go index 8a4aced04b..b81ed57a2c 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -82,6 +82,9 @@ const ( // beaconUpdateWarnFrequency is the frequency at which to warn the user that // the beacon client is offline. beaconUpdateWarnFrequency = 5 * time.Minute + + // maxReorgDepth is the maximum reorg depth accepted via forkchoiceUpdated. + maxReorgDepth = 32 ) type ConsensusAPI struct { @@ -237,6 +240,7 @@ func (api *ConsensusAPI) ForkchoiceUpdatedV4(ctx context.Context, update engine. func (api *ConsensusAPI) forkchoiceUpdated(ctx context.Context, update engine.ForkchoiceStateV1, payloadAttributes *engine.PayloadAttributes, payloadVersion engine.PayloadVersion, payloadWitness bool) (result engine.ForkChoiceResponse, err error) { ctx, _, spanEnd := telemetry.StartSpan(ctx, "engine.forkchoiceUpdated") defer spanEnd(&err) + api.forkchoiceLock.Lock() defer api.forkchoiceLock.Unlock() @@ -321,10 +325,23 @@ func (api *ConsensusAPI) forkchoiceUpdated(ctx context.Context, update engine.Fo // generating the payload. It's a special corner case that a few slots are // missing and we are requested to generate the payload in slot. } else { - // If the head block is already in our canonical chain, the beacon client is - // probably resyncing. Ignore the update. - log.Info("Ignoring beacon update to old head", "number", block.NumberU64(), "hash", update.HeadBlockHash, "age", common.PrettyAge(time.Unix(int64(block.Time()), 0)), "have", api.eth.BlockChain().CurrentBlock().Number) - return valid(nil), nil + if finalized := api.eth.BlockChain().CurrentFinalBlock(); finalized != nil && block.NumberU64() <= finalized.Number.Uint64() { + log.Info("Skipping beacon update to finalized ancestor", "number", block.NumberU64(), "hash", update.HeadBlockHash) + return valid(nil), nil + } + depth := api.eth.BlockChain().CurrentBlock().Number.Uint64() - block.NumberU64() + if depth >= maxReorgDepth { + log.Warn("Refusing too deep reorg", "depth", depth, "head", update.HeadBlockHash) + return engine.STATUS_INVALID, engine.TooDeepReorg.With(fmt.Errorf("reorg depth %d exceeds limit %d", depth, maxReorgDepth)) + } + if !api.eth.Synced() { + log.Info("Ignoring beacon update to old head while syncing", "number", block.NumberU64(), "hash", update.HeadBlockHash) + return valid(nil), nil + } + if latestValid, err := api.eth.BlockChain().SetCanonical(block); err != nil { + log.Error("Error setting canonical", "number", block.NumberU64(), "hash", update.HeadBlockHash, "error", err) + return engine.ForkChoiceResponse{PayloadStatus: engine.PayloadStatusV1{Status: engine.INVALID, LatestValidHash: &latestValid}}, err + } } api.eth.SetSynced() From aaa2b662856dcd8527dfb452ceee605467967fb1 Mon Sep 17 00:00:00 2001 From: Marius van der Wijden Date: Wed, 6 May 2026 12:03:11 +0200 Subject: [PATCH 15/63] core: implement eip-7981: Increase Access List Cost (#34755) based on: https://github.com/ethereum/go-ethereum/pull/34748 spec: https://eips.ethereum.org/EIPS/eip-7981 --- cmd/evm/internal/t8ntool/transaction.go | 4 +- core/bench_test.go | 2 +- core/bintrie_witness_test.go | 4 +- core/state_transition.go | 66 +++++- core/state_transition_test.go | 287 ++++++++++++++++++++++++ core/txpool/validation.go | 4 +- tests/transaction_test_util.go | 4 +- 7 files changed, 354 insertions(+), 17 deletions(-) create mode 100644 core/state_transition_test.go diff --git a/cmd/evm/internal/t8ntool/transaction.go b/cmd/evm/internal/t8ntool/transaction.go index 7e068c06af..ca19ae3386 100644 --- a/cmd/evm/internal/t8ntool/transaction.go +++ b/cmd/evm/internal/t8ntool/transaction.go @@ -133,7 +133,7 @@ func Transaction(ctx *cli.Context) error { } // Check intrinsic gas rules := chainConfig.Rules(common.Big0, true, 0) - cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai) + cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai, rules.IsAmsterdam) if err != nil { r.Error = err results = append(results, r) @@ -147,7 +147,7 @@ func Transaction(ctx *cli.Context) error { } // For Prague txs, validate the floor data gas. if rules.IsPrague { - floorDataGas, err := core.FloorDataGas(rules, tx.Data()) + floorDataGas, err := core.FloorDataGas(rules, tx.Data(), tx.AccessList()) if err != nil { r.Error = err results = append(results, r) diff --git a/core/bench_test.go b/core/bench_test.go index 20d1a7794b..65179c54d4 100644 --- a/core/bench_test.go +++ b/core/bench_test.go @@ -89,7 +89,7 @@ func genValueTx(nbytes int) func(int, *BlockGen) { data := make([]byte, nbytes) return func(i int, gen *BlockGen) { toaddr := common.Address{} - cost, _ := IntrinsicGas(data, nil, nil, false, false, false, false) + cost, _ := IntrinsicGas(data, nil, nil, false, false, false, false, false) signer := gen.Signer() gasPrice := big.NewInt(0) if gen.header.BaseFee != nil { diff --git a/core/bintrie_witness_test.go b/core/bintrie_witness_test.go index 66feef0675..5f6239e4fa 100644 --- a/core/bintrie_witness_test.go +++ b/core/bintrie_witness_test.go @@ -63,12 +63,12 @@ var ( func TestProcessUBT(t *testing.T) { var ( code = common.FromHex(`6060604052600a8060106000396000f360606040526008565b00`) - intrinsicContractCreationGas, _ = IntrinsicGas(code, nil, nil, true, true, true, true) + intrinsicContractCreationGas, _ = IntrinsicGas(code, nil, nil, true, true, true, true, false) // A contract creation that calls EXTCODECOPY in the constructor. Used to ensure that the witness // will not contain that copied data. // Source: https://gist.github.com/gballet/a23db1e1cb4ed105616b5920feb75985 codeWithExtCodeCopy = common.FromHex(`0x60806040526040516100109061017b565b604051809103906000f08015801561002c573d6000803e3d6000fd5b506000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555034801561007857600080fd5b5060008067ffffffffffffffff8111156100955761009461024a565b5b6040519080825280601f01601f1916602001820160405280156100c75781602001600182028036833780820191505090505b50905060008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1690506020600083833c81610101906101e3565b60405161010d90610187565b61011791906101a3565b604051809103906000f080158015610133573d6000803e3d6000fd5b50600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550505061029b565b60d58061046783390190565b6102068061053c83390190565b61019d816101d9565b82525050565b60006020820190506101b86000830184610194565b92915050565b6000819050602082019050919050565b600081519050919050565b6000819050919050565b60006101ee826101ce565b826101f8846101be565b905061020381610279565b925060208210156102435761023e7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8360200360080261028e565b831692505b5050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b600061028582516101d9565b80915050919050565b600082821b905092915050565b6101bd806102aa6000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063f566852414610030575b600080fd5b61003861004e565b6040516100459190610146565b60405180910390f35b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166381ca91d36040518163ffffffff1660e01b815260040160206040518083038186803b1580156100b857600080fd5b505afa1580156100cc573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100f0919061010a565b905090565b60008151905061010481610170565b92915050565b6000602082840312156101205761011f61016b565b5b600061012e848285016100f5565b91505092915050565b61014081610161565b82525050565b600060208201905061015b6000830184610137565b92915050565b6000819050919050565b600080fd5b61017981610161565b811461018457600080fd5b5056fea2646970667358221220a6a0e11af79f176f9c421b7b12f441356b25f6489b83d38cc828a701720b41f164736f6c63430008070033608060405234801561001057600080fd5b5060b68061001f6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c8063ab5ed15014602d575b600080fd5b60336047565b604051603e9190605d565b60405180910390f35b60006001905090565b6057816076565b82525050565b6000602082019050607060008301846050565b92915050565b600081905091905056fea26469706673582212203a14eb0d5cd07c277d3e24912f110ddda3e553245a99afc4eeefb2fbae5327aa64736f6c63430008070033608060405234801561001057600080fd5b5060405161020638038061020683398181016040528101906100329190610063565b60018160001c6100429190610090565b60008190555050610145565b60008151905061005d8161012e565b92915050565b60006020828403121561007957610078610129565b5b60006100878482850161004e565b91505092915050565b600061009b826100f0565b91506100a6836100f0565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff038211156100db576100da6100fa565b5b828201905092915050565b6000819050919050565b6000819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600080fd5b610137816100e6565b811461014257600080fd5b50565b60b3806101536000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c806381ca91d314602d575b600080fd5b60336047565b604051603e9190605a565b60405180910390f35b60005481565b6054816073565b82525050565b6000602082019050606d6000830184604d565b92915050565b600081905091905056fea26469706673582212209bff7098a2f526de1ad499866f27d6d0d6f17b74a413036d6063ca6a0998ca4264736f6c63430008070033`) - intrinsicCodeWithExtCodeCopyGas, _ = IntrinsicGas(codeWithExtCodeCopy, nil, nil, true, true, true, true) + intrinsicCodeWithExtCodeCopyGas, _ = IntrinsicGas(codeWithExtCodeCopy, nil, nil, true, true, true, true, false) signer = types.LatestSigner(testUBTChainConfig) testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") bcdb = rawdb.NewMemoryDatabase() // Database for the blockchain diff --git a/core/state_transition.go b/core/state_transition.go index c7b0593857..b5b8b22155 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -68,7 +68,7 @@ func (result *ExecutionResult) Revert() []byte { } // IntrinsicGas computes the 'intrinsic gas' for a message with the given data. -func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.SetCodeAuthorization, isContractCreation, isHomestead, isEIP2028, isEIP3860 bool) (vm.GasCosts, error) { +func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.SetCodeAuthorization, isContractCreation, isHomestead, isEIP2028, isEIP3860, isAmsterdam bool) (vm.GasCosts, error) { // Set the starting gas for the raw transaction var gas uint64 if isContractCreation && isHomestead { @@ -107,8 +107,32 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set } } if accessList != nil { - gas += uint64(len(accessList)) * params.TxAccessListAddressGas - gas += uint64(accessList.StorageKeys()) * params.TxAccessListStorageKeyGas + addresses := uint64(len(accessList)) + storageKeys := uint64(accessList.StorageKeys()) + if (math.MaxUint64-gas)/params.TxAccessListAddressGas < addresses { + return vm.GasCosts{}, ErrGasUintOverflow + } + gas += addresses * params.TxAccessListAddressGas + if (math.MaxUint64-gas)/params.TxAccessListStorageKeyGas < storageKeys { + return vm.GasCosts{}, ErrGasUintOverflow + } + gas += storageKeys * params.TxAccessListStorageKeyGas + + // EIP-7981: access list data is charged in addition to the base charge. + if isAmsterdam { + const ( + addressCost = common.AddressLength * params.TxCostFloorPerToken7976 * params.TxTokenPerNonZeroByte + storageKeyCost = common.HashLength * params.TxCostFloorPerToken7976 * params.TxTokenPerNonZeroByte + ) + if (math.MaxUint64-gas)/addressCost < addresses { + return vm.GasCosts{}, ErrGasUintOverflow + } + gas += addresses * addressCost + if (math.MaxUint64-gas)/storageKeyCost < storageKeys { + return vm.GasCosts{}, ErrGasUintOverflow + } + gas += storageKeys * storageKeyCost + } } if authList != nil { gas += uint64(len(authList)) * params.CallNewAccountGas @@ -117,7 +141,7 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set } // FloorDataGas computes the minimum gas required for a transaction based on its data tokens (EIP-7623). -func FloorDataGas(rules params.Rules, data []byte) (uint64, error) { +func FloorDataGas(rules params.Rules, data []byte, accessList types.AccessList) (uint64, error) { var ( tokens uint64 tokenCost uint64 @@ -125,15 +149,41 @@ func FloorDataGas(rules params.Rules, data []byte) (uint64, error) { if rules.IsAmsterdam { // EIP-7976 changes how calldata is priced. // From 10/40 to 64/64 for zero/non-zero bytes. - tokens = uint64(len(data)) * params.TxTokenPerNonZeroByte tokenCost = params.TxCostFloorPerToken7976 + dataLen := uint64(len(data)) + if math.MaxUint64/params.TxTokenPerNonZeroByte < dataLen { + return 0, ErrGasUintOverflow + } + tokens = dataLen * params.TxTokenPerNonZeroByte + + // EIP-7981 adds additional tokens for every entry in the accesslist + const addressTokenCost = uint64(common.AddressLength) * params.TxTokenPerNonZeroByte + addresses := uint64(len(accessList)) + if (math.MaxUint64-tokens)/addressTokenCost < addresses { + return 0, ErrGasUintOverflow + } + tokens += addresses * addressTokenCost + + const storageKeyTokenCost = uint64(common.HashLength) * params.TxTokenPerNonZeroByte + storageKeys := uint64(accessList.StorageKeys()) + if (math.MaxUint64-tokens)/storageKeyTokenCost < storageKeys { + return 0, ErrGasUintOverflow + } + tokens += storageKeys * storageKeyTokenCost } else { var ( z = uint64(bytes.Count(data, []byte{0})) nz = uint64(len(data)) - z ) // Pre-Amsterdam - tokens = nz*params.TxTokenPerNonZeroByte + z + if math.MaxUint64/params.TxTokenPerNonZeroByte < nz { + return 0, ErrGasUintOverflow + } + tokens = nz * params.TxTokenPerNonZeroByte + if math.MaxUint64-tokens < z { + return 0, ErrGasUintOverflow + } + tokens += z tokenCost = params.TxCostFloorPerToken } @@ -462,7 +512,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { floorDataGas uint64 ) // Check clauses 4-5, subtract intrinsic gas if everything is correct - cost, err := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, contractCreation, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai) + cost, err := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, contractCreation, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai, rules.IsAmsterdam) if err != nil { return nil, err } @@ -475,7 +525,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { } // Gas limit suffices for the floor data cost (EIP-7623) if rules.IsPrague { - floorDataGas, err = FloorDataGas(rules, msg.Data) + floorDataGas, err = FloorDataGas(rules, msg.Data, msg.AccessList) if err != nil { return nil, err } diff --git a/core/state_transition_test.go b/core/state_transition_test.go new file mode 100644 index 0000000000..8aab016123 --- /dev/null +++ b/core/state_transition_test.go @@ -0,0 +1,287 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package core + +import ( + "bytes" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" +) + +func TestFloorDataGas(t *testing.T) { + addr1 := common.HexToAddress("0x1111111111111111111111111111111111111111") + addr2 := common.HexToAddress("0x2222222222222222222222222222222222222222") + key1 := common.HexToHash("0xaa") + key2 := common.HexToHash("0xbb") + + tests := []struct { + name string + amsterdam bool + data []byte + accessList types.AccessList + want uint64 + }{ + { + name: "pre-amsterdam/empty", + want: params.TxGas, + }, + { + name: "pre-amsterdam/zero-bytes-only", + data: bytes.Repeat([]byte{0x00}, 100), + // 100 zero tokens * 10 cost = 1000 + want: params.TxGas + 100*params.TxCostFloorPerToken, + }, + { + name: "pre-amsterdam/non-zero-bytes-only", + data: bytes.Repeat([]byte{0xff}, 100), + // 100 nz * 4 tokens * 10 cost = 4000 + want: params.TxGas + 100*params.TxTokenPerNonZeroByte*params.TxCostFloorPerToken, + }, + { + name: "pre-amsterdam/mixed", + data: append(bytes.Repeat([]byte{0x00}, 50), bytes.Repeat([]byte{0xff}, 50)...), + // 50 zero + 50*4 nz = 250 tokens * 10 = 2500 + want: params.TxGas + (50+50*params.TxTokenPerNonZeroByte)*params.TxCostFloorPerToken, + }, + { + name: "pre-amsterdam/access-list-ignored", + data: bytes.Repeat([]byte{0xff}, 10), + accessList: types.AccessList{ + {Address: addr1, StorageKeys: []common.Hash{key1, key2}}, + }, + // pre-amsterdam: floor calculation does not include access list + want: params.TxGas + 10*params.TxTokenPerNonZeroByte*params.TxCostFloorPerToken, + }, + { + name: "amsterdam/empty", + amsterdam: true, + want: params.TxGas, + }, + { + name: "amsterdam/data-only", + amsterdam: true, + data: bytes.Repeat([]byte{0x00}, 1024), + // post-amsterdam: every byte = 4 tokens regardless of value + want: params.TxGas + 1024*params.TxTokenPerNonZeroByte*params.TxCostFloorPerToken7976, + }, + { + name: "amsterdam/data-non-zero", + amsterdam: true, + data: bytes.Repeat([]byte{0xff}, 1024), + // same as zero data post-amsterdam + want: params.TxGas + 1024*params.TxTokenPerNonZeroByte*params.TxCostFloorPerToken7976, + }, + { + name: "amsterdam/access-list-addresses-only", + amsterdam: true, + accessList: types.AccessList{ + {Address: addr1}, + {Address: addr2}, + }, + // 2 * 20 bytes * 4 tokens/byte * 16 cost/token + want: params.TxGas + 2*common.AddressLength*params.TxTokenPerNonZeroByte*params.TxCostFloorPerToken7976, + }, + { + name: "amsterdam/access-list-with-storage-keys", + amsterdam: true, + accessList: types.AccessList{ + {Address: addr1, StorageKeys: []common.Hash{key1, key2}}, + }, + // 1 addr * 20 * 4 + 2 keys * 32 * 4 = 80 + 256 = 336 tokens * 16 + want: params.TxGas + (1*common.AddressLength+2*common.HashLength)*params.TxTokenPerNonZeroByte*params.TxCostFloorPerToken7976, + }, + { + name: "amsterdam/mixed", + amsterdam: true, + data: bytes.Repeat([]byte{0xff}, 100), + accessList: types.AccessList{ + {Address: addr1, StorageKeys: []common.Hash{key1}}, + {Address: addr2, StorageKeys: []common.Hash{key1, key2}}, + }, + // data: 100*4 = 400; addrs: 2*20*4 = 160; keys: 3*32*4 = 384; total = 944 * 16 + want: params.TxGas + (100*params.TxTokenPerNonZeroByte+2*common.AddressLength*params.TxTokenPerNonZeroByte+3*common.HashLength*params.TxTokenPerNonZeroByte)*params.TxCostFloorPerToken7976, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rules := params.Rules{IsAmsterdam: tt.amsterdam} + got, err := FloorDataGas(rules, tt.data, tt.accessList) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != tt.want { + t.Fatalf("gas mismatch: got %d, want %d", got, tt.want) + } + }) + } +} + +func TestIntrinsicGas(t *testing.T) { + addr1 := common.HexToAddress("0x1111111111111111111111111111111111111111") + addr2 := common.HexToAddress("0x2222222222222222222222222222222222222222") + key1 := common.HexToHash("0xaa") + key2 := common.HexToHash("0xbb") + + const ( + amsterdamAddressCost = uint64(common.AddressLength) * params.TxCostFloorPerToken7976 * params.TxTokenPerNonZeroByte // 1280 + amsterdamStorageKeyCost = uint64(common.HashLength) * params.TxCostFloorPerToken7976 * params.TxTokenPerNonZeroByte // 2048 + ) + + tests := []struct { + name string + data []byte + accessList types.AccessList + authList []types.SetCodeAuthorization + creation bool + isHomestead bool + isEIP2028 bool + isEIP3860 bool + isAmsterdam bool + want uint64 + }{ + { + name: "frontier/empty-call", + want: params.TxGas, + }, + { + name: "frontier/contract-creation-pre-homestead", + creation: true, + isHomestead: false, + // pre-homestead, contract creation still uses TxGas + want: params.TxGas, + }, + { + name: "homestead/contract-creation", + creation: true, + isHomestead: true, + want: params.TxGasContractCreation, + }, + { + name: "frontier/non-zero-data", + data: bytes.Repeat([]byte{0xff}, 100), + // 100 nz bytes * 68 (frontier) + want: params.TxGas + 100*params.TxDataNonZeroGasFrontier, + }, + { + name: "istanbul/non-zero-data", + data: bytes.Repeat([]byte{0xff}, 100), + isEIP2028: true, + // 100 nz bytes * 16 (post-EIP2028) + want: params.TxGas + 100*params.TxDataNonZeroGasEIP2028, + }, + { + name: "istanbul/zero-data", + data: bytes.Repeat([]byte{0x00}, 100), + isEIP2028: true, + // 100 zero bytes * 4 + want: params.TxGas + 100*params.TxDataZeroGas, + }, + { + name: "istanbul/mixed-data", + data: append(bytes.Repeat([]byte{0x00}, 50), bytes.Repeat([]byte{0xff}, 50)...), + isEIP2028: true, + want: params.TxGas + 50*params.TxDataZeroGas + 50*params.TxDataNonZeroGasEIP2028, + }, + { + name: "shanghai/init-code-word-gas", + data: bytes.Repeat([]byte{0x00}, 64), // 2 words + creation: true, + isHomestead: true, + isEIP2028: true, + isEIP3860: true, + // TxGasContractCreation + 64 zero bytes * 4 + 2 words * 2 + want: params.TxGasContractCreation + 64*params.TxDataZeroGas + 2*params.InitCodeWordGas, + }, + { + name: "shanghai/init-code-non-multiple-of-32", + data: bytes.Repeat([]byte{0x00}, 33), // 2 words (rounded up) + creation: true, + isHomestead: true, + isEIP2028: true, + isEIP3860: true, + want: params.TxGasContractCreation + 33*params.TxDataZeroGas + 2*params.InitCodeWordGas, + }, + { + name: "berlin/access-list", + accessList: types.AccessList{ + {Address: addr1, StorageKeys: []common.Hash{key1, key2}}, + {Address: addr2, StorageKeys: []common.Hash{key1}}, + }, + isEIP2028: true, + // 2 addrs * 2400 + 3 keys * 1900 + want: params.TxGas + 2*params.TxAccessListAddressGas + 3*params.TxAccessListStorageKeyGas, + }, + { + name: "amsterdam/access-list-extra-cost", + accessList: types.AccessList{ + {Address: addr1, StorageKeys: []common.Hash{key1, key2}}, + {Address: addr2, StorageKeys: []common.Hash{key1}}, + }, + isEIP2028: true, + isAmsterdam: true, + // base access-list charge + EIP-7981 extra + want: params.TxGas + + 2*params.TxAccessListAddressGas + 3*params.TxAccessListStorageKeyGas + + 2*amsterdamAddressCost + 3*amsterdamStorageKeyCost, + }, + { + name: "prague/auth-list", + authList: []types.SetCodeAuthorization{ + {Address: addr1}, + {Address: addr2}, + {Address: addr1}, + }, + isEIP2028: true, + // 3 auths * 25000 + want: params.TxGas + 3*params.CallNewAccountGas, + }, + { + name: "amsterdam/combined", + data: bytes.Repeat([]byte{0xff}, 100), + accessList: types.AccessList{ + {Address: addr1, StorageKeys: []common.Hash{key1}}, + }, + authList: []types.SetCodeAuthorization{ + {Address: addr2}, + }, + isEIP2028: true, + isAmsterdam: true, + want: params.TxGas + + 100*params.TxDataNonZeroGasEIP2028 + + 1*params.TxAccessListAddressGas + 1*params.TxAccessListStorageKeyGas + + 1*amsterdamAddressCost + 1*amsterdamStorageKeyCost + + 1*params.CallNewAccountGas, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := IntrinsicGas(tt.data, tt.accessList, tt.authList, + tt.creation, tt.isHomestead, tt.isEIP2028, tt.isEIP3860, tt.isAmsterdam) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + want := vm.GasCosts{RegularGas: tt.want} + if got != want { + t.Fatalf("gas mismatch: got %+v, want %+v", got, want) + } + }) + } +} diff --git a/core/txpool/validation.go b/core/txpool/validation.go index 6891dc94d2..c87bba31ac 100644 --- a/core/txpool/validation.go +++ b/core/txpool/validation.go @@ -125,7 +125,7 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types } // Ensure the transaction has more gas than the bare minimum needed to cover // the transaction metadata - intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, true, rules.IsIstanbul, rules.IsShanghai) + intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, true, rules.IsIstanbul, rules.IsShanghai, rules.IsAmsterdam) if err != nil { return err } @@ -134,7 +134,7 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types } // Ensure the transaction can cover floor data gas. if rules.IsPrague { - floorDataGas, err := core.FloorDataGas(rules, tx.Data()) + floorDataGas, err := core.FloorDataGas(rules, tx.Data(), tx.AccessList()) if err != nil { return err } diff --git a/tests/transaction_test_util.go b/tests/transaction_test_util.go index 8b8d0357bf..572c109f1e 100644 --- a/tests/transaction_test_util.go +++ b/tests/transaction_test_util.go @@ -81,7 +81,7 @@ func (tt *TransactionTest) Run() error { return } // Intrinsic gas - cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai) + cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai, rules.IsAmsterdam) if err != nil { return } @@ -92,7 +92,7 @@ func (tt *TransactionTest) Run() error { if rules.IsPrague { var floorDataGas uint64 - floorDataGas, err = core.FloorDataGas(rules, tx.Data()) + floorDataGas, err = core.FloorDataGas(rules, tx.Data(), tx.AccessList()) if err != nil { return } From b92c86deb79dff0338545b0a530442b2c7abf36c Mon Sep 17 00:00:00 2001 From: Rahman Date: Wed, 6 May 2026 07:02:03 -0600 Subject: [PATCH 16/63] internal/download: don't discard `dst.Close` error (#34866) When `io.Copy` succeeds but the buffered `Close` fails (e.g. disk full on `Flush`), the error was swallowed and verification reported a misleading hash mismatch instead of the real I/O failure. Keep the `Close` error when `io.Copy` didn't already produce one. --------- Co-authored-by: Jared Wasinger --- internal/download/download.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/download/download.go b/internal/download/download.go index 79cf27a56b..27d3732731 100644 --- a/internal/download/download.go +++ b/internal/download/download.go @@ -211,9 +211,11 @@ func (db *ChecksumDB) DownloadFile(url, dstPath string) error { if resp.ContentLength > 0 { dst = newDownloadWriter(fd, resp.ContentLength) } - _, err = io.Copy(dst, resp.Body) - dst.Close() - if err != nil { + if _, err = io.Copy(dst, resp.Body); err != nil { + os.Remove(tmpfile) + return err + } + if err = dst.Close(); err != nil { os.Remove(tmpfile) return err } From 06c30cc7e113a639499c2c5d669901bbfc8c189d Mon Sep 17 00:00:00 2001 From: Jonny Rhea <5555162+jrhea@users.noreply.github.com> Date: Wed, 6 May 2026 08:08:15 -0500 Subject: [PATCH 17/63] triedb/pathdb: add AdoptSyncedState for snap/2 completion path (#34874) This PR adds `AdoptSyncedState()` alongside `Enable()`. It does the same pathdb bookkeeping (now factored into a shared `resetForReactivation()` helper), but skips the regeneration. The wiring/calling code lands in #34626 --------- Co-authored-by: Gary Rong --- triedb/database.go | 10 +++++ triedb/pathdb/database.go | 81 ++++++++++++++++++++++++++-------- triedb/pathdb/database_test.go | 78 ++++++++++++++++++++++++++++++++ 3 files changed, 150 insertions(+), 19 deletions(-) diff --git a/triedb/database.go b/triedb/database.go index 0fd3e1aa91..ef95169df1 100644 --- a/triedb/database.go +++ b/triedb/database.go @@ -327,6 +327,16 @@ func (db *Database) Enable(root common.Hash) error { return pdb.Enable(root) } +// AdoptSyncedState activates the database after a snap/2 sync and adopts the +// flat state populated during sync as-is, skipping regeneration. +func (db *Database) AdoptSyncedState(root common.Hash) error { + pdb, ok := db.backend.(*pathdb.Database) + if !ok { + return errors.New("not supported") + } + return pdb.AdoptSyncedState(root) +} + // Journal commits an entire diff hierarchy to disk into a single journal entry. // This is meant to be used during shutdown to persist the snapshot without // flattening everything down (bad for reorgs). It's only supported by path-based diff --git a/triedb/pathdb/database.go b/triedb/pathdb/database.go index 98975d7fa5..e52949c93e 100644 --- a/triedb/pathdb/database.go +++ b/triedb/pathdb/database.go @@ -365,16 +365,9 @@ func (db *Database) Disable() error { return nil } -// Enable activates database and resets the state tree with the provided persistent -// state root once the state sync is finished. -func (db *Database) Enable(root common.Hash) error { - db.lock.Lock() - defer db.lock.Unlock() - - // Short circuit if the database is in read only mode. - if db.readOnly { - return errDatabaseReadOnly - } +// resetForReactivation performs the pathdb-side bookkeeping shared by both +// Enable and AdoptSyncedState. +func (db *Database) resetForReactivation(root common.Hash) error { // Ensure the provided state root matches the stored one. stored, err := db.hasher(rawdb.ReadAccountTrieNode(db.diskdb, nil)) if err != nil { @@ -383,27 +376,40 @@ func (db *Database) Enable(root common.Hash) error { if stored != root { return fmt.Errorf("state root mismatch: stored %x, synced %x", stored, root) } - // Drop the stale state journal in persistent database and - // reset the persistent state id back to zero. + // Drop the stale state journal marker and reset the persistent state id + // back to zero. batch := db.diskdb.NewBatch() rawdb.DeleteSnapshotRoot(batch) rawdb.WritePersistentStateID(batch, 0) if err := batch.Write(); err != nil { return err } - // Clean up all state histories in freezer. Theoretically - // all root->id mappings should be removed as well. Since - // mappings can be huge and might take a while to clear - // them, just leave them in disk and wait for overwriting. + // Clean up all state histories in the freezer. Theoretically all root->id + // mappings should be removed as well; since those can be huge, leave them + // on disk and let them be overwritten. purgeHistory(db.stateFreezer, db.diskdb, typeStateHistory) purgeHistory(db.trienodeFreezer, db.diskdb, typeTrienodeHistory) - // Re-enable the database as the final step. + // Re-enable the database as the final bookkeeping step. db.waitSync = false rawdb.WriteSnapSyncStatusFlag(db.diskdb, rawdb.StateSyncFinished) + return nil +} - // Re-construct a new disk layer backed by persistent state - // and schedule the state snapshot generation if it's permitted. +// Enable activates the database after a snap/1 sync and schedules background +// regeneration of the snapshot from the trie. +func (db *Database) Enable(root common.Hash) error { + db.lock.Lock() + defer db.lock.Unlock() + + if db.readOnly { + return errDatabaseReadOnly + } + if err := db.resetForReactivation(root); err != nil { + return err + } + // Re-construct a new disk layer backed by persistent state and schedule + // the state snapshot generation if it's permitted. db.tree.init(generateSnapshot(db, root, db.isUBT || db.config.SnapshotNoBuild)) // After snap sync, the state of the database may have changed completely. @@ -416,6 +422,43 @@ func (db *Database) Enable(root common.Hash) error { return nil } +// AdoptSyncedState reactivates the database after a snap/2 sync. The syncer +// already wrote a consistent flat state, so we take it as-is instead of +// rebuilding it from the trie. The new disk layer has no generator attached, +// and a "done" marker is written so future boots know the snapshot is +// already complete. +func (db *Database) AdoptSyncedState(root common.Hash) error { + db.lock.Lock() + defer db.lock.Unlock() + + if db.readOnly { + return errDatabaseReadOnly + } + if err := db.resetForReactivation(root); err != nil { + return err + } + + // Tell the snapshot subsystem the flat state is good by writing the new root + // and a "done" marker (nil journal) so the next boot doesn't try to rebuild it. + batch := db.diskdb.NewBatch() + rawdb.WriteSnapshotRoot(batch, root) + journalProgress(batch, nil, nil) + if err := batch.Write(); err != nil { + return err + } + + // New disk layer, no generator attached. Nothing to rebuild, and reads + // can serve the flat state right away without waiting on a generator to + // scan past every key. + dl := newDiskLayer(root, 0, db, nil, nil, newBuffer(db.config.WriteBufferSize, nil, nil, 0), nil) + db.tree.init(dl) + + db.setHistoryIndexer() + + log.Info("Adopted synced state", "root", root) + return nil +} + // Recover rollbacks the database to a specified historical point. // The state is supported as the rollback destination only if it's // canonical state and the corresponding trie histories are existent. diff --git a/triedb/pathdb/database_test.go b/triedb/pathdb/database_test.go index 8ceb22eaba..41212dc9d0 100644 --- a/triedb/pathdb/database_test.go +++ b/triedb/pathdb/database_test.go @@ -748,6 +748,84 @@ func TestDisable(t *testing.T) { } } +// TestAdoptSyncedState verifies that AdoptSyncedState rejects a wrong root, +// writes the on-disk markers that say the snapshot is already complete, +// leaves a single fresh disk layer with no generator attached, and clears +// out stale state histories. +func TestAdoptSyncedState(t *testing.T) { + maxDiffLayers = 4 + defer func() { + maxDiffLayers = 128 + }() + + tester := newTester(t, &testerConfig{layers: 12}) + defer tester.release() + + // Push everything down to disk so the trie root is the persistent root. + if err := tester.db.Commit(tester.lastHash(), false); err != nil { + t.Fatalf("Failed to commit, err: %v", err) + } + stored := crypto.Keccak256Hash(rawdb.ReadAccountTrieNode(tester.db.diskdb, nil)) + + // Mimic the snap-syncing state. + if err := tester.db.Disable(); err != nil { + t.Fatalf("Failed to disable database: %v", err) + } + // Mismatched root must be rejected. + if err := tester.db.AdoptSyncedState(types.EmptyRootHash); err == nil { + t.Fatal("Mismatched root should be rejected") + } + if err := tester.db.AdoptSyncedState(stored); err != nil { + t.Fatalf("AdoptSyncedState failed: %v", err) + } + + // On-disk markers reflect a completed snapshot. + if got := rawdb.ReadSnapshotRoot(tester.db.diskdb); got != stored { + t.Fatalf("SnapshotRoot mismatch: got %x want %x", got, stored) + } + if blob := rawdb.ReadSnapshotGenerator(tester.db.diskdb); len(blob) == 0 { + t.Fatal("Generator journal not written") + } else { + var entry journalGenerator + if err := rlp.DecodeBytes(blob, &entry); err != nil { + t.Fatalf("Failed to decode generator journal: %v", err) + } + if !entry.Done { + t.Fatal("Generator journal should be marked Done") + } + // RLP turns a nil slice into an empty one on decode, so check length. + if len(entry.Marker) != 0 { + t.Fatalf("Generator marker should be empty, got %x", entry.Marker) + } + } + if rawdb.ReadSnapSyncStatusFlag(tester.db.diskdb) != rawdb.StateSyncFinished { + t.Fatal("Sync-status flag should be StateSyncFinished") + } + if tester.db.waitSync { + t.Fatal("waitSync should be false after adopt") + } + + // State histories are purged. + if n, err := tester.db.stateFreezer.Ancients(); err != nil || n != 0 { + t.Fatalf("State histories not purged: count=%d err=%v", n, err) + } + + // Layer tree has a single disk layer with no generator attached. + if got := tester.db.tree.len(); got != 1 { + t.Fatalf("Expected single layer, got %d", got) + } + dl := tester.db.tree.bottom() + if dl.rootHash() != stored { + t.Fatalf("Disk layer root mismatch: got %x want %x", dl.rootHash(), stored) + } + if dl.generator != nil { + t.Fatal("Disk layer should have no generator after adopt") + } + if dl.genMarker() != nil { + t.Fatal("genMarker should be nil after adopt") + } +} + func TestCommit(t *testing.T) { // Redefine the diff layer depth allowance for faster testing. maxDiffLayers = 4 From ea1cf7bf5ee07562284f9d050a6def9704d258e7 Mon Sep 17 00:00:00 2001 From: Bosul Mun Date: Wed, 6 May 2026 15:36:54 +0200 Subject: [PATCH 18/63] eth/protocols/eth: stop serving on unavailable responses (#34787) This is an alternative PR for https://github.com/ethereum/go-ethereum/pull/34746. This PR implements the second approach among the two possible solutions mentioned in the above PR. Requests for unavailable items are possible when the peer is following a different fork from us. However this is not expected to happen frequently. Considering the amount of complexity added to the codebase, the simpler approach (this PR) can be preferred. --- eth/protocols/eth/handler_test.go | 16 ++++++++++------ eth/protocols/eth/handlers.go | 21 ++++++++++++--------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/eth/protocols/eth/handler_test.go b/eth/protocols/eth/handler_test.go index a45abc90eb..d056d121d9 100644 --- a/eth/protocols/eth/handler_test.go +++ b/eth/protocols/eth/handler_test.go @@ -424,16 +424,20 @@ func testGetBlockBodies(t *testing.T, protocol uint) { {0, []common.Hash{backend.chain.CurrentBlock().Hash()}, []bool{true}, 1}, // The chains head block should be retrievable {0, []common.Hash{{}}, []bool{false}, 0}, // A non existent block should not be returned - // Existing and non-existing blocks interleaved should not cause problems + // Existing blocks followed by a non-existing one should stop at the gap + {0, []common.Hash{ + backend.chain.GetBlockByNumber(1).Hash(), + backend.chain.GetBlockByNumber(10).Hash(), + backend.chain.GetBlockByNumber(100).Hash(), + {}, + }, []bool{true, true, true, false}, 3}, + + // A non-existing block at the start should return nothing {0, []common.Hash{ {}, backend.chain.GetBlockByNumber(1).Hash(), - {}, backend.chain.GetBlockByNumber(10).Hash(), - {}, - backend.chain.GetBlockByNumber(100).Hash(), - {}, - }, []bool{false, true, false, true, false, true, false}, 3}, + }, []bool{false, true, true}, 0}, } // Run each of the tests and verify the results against the chain for i, tt := range tests { diff --git a/eth/protocols/eth/handlers.go b/eth/protocols/eth/handlers.go index 7556df9af2..3254a0abc2 100644 --- a/eth/protocols/eth/handlers.go +++ b/eth/protocols/eth/handlers.go @@ -238,10 +238,12 @@ func ServiceGetBlockBodiesQuery(chain *core.BlockChain, query GetBlockBodiesRequ lookups >= 2*maxBodiesServe { break } - if data := chain.GetBodyRLP(hash); len(data) != 0 { - bodies = append(bodies, data) - bytes += len(data) + data := chain.GetBodyRLP(hash) + if len(data) == 0 { + break // If we don't have this block's body, stop serving. } + bodies = append(bodies, data) + bytes += len(data) } return bodies } @@ -281,16 +283,16 @@ func ServiceGetReceiptsQuery69(chain *core.BlockChain, query GetReceiptsRequest) // Retrieve the requested block's receipts results := chain.GetReceiptsRLP(hash) if results == nil { - continue // Can't retrieve the receipts, so we just skip this block. + break // Don't have this block's receipts, stop serving. } body := chain.GetBodyRLP(hash) if body == nil { - continue // The block body is missing, we also have to skip. + break // The block body is missing, stop serving. } results, _, err := blockReceiptsToNetwork(results, body, receiptQueryParams{}) if err != nil { log.Error("Error in block receipts conversion", "hash", hash, "err", err) - continue + break } receipts.AppendRaw(results) bytes += len(results) @@ -312,12 +314,13 @@ func serviceGetReceiptsQuery70(chain *core.BlockChain, query GetReceiptsRequest, break } results := chain.GetReceiptsRLP(hash) + // If we don't have this block's receipts or body, stop serving. if results == nil { - continue // Can't retrieve the receipts, so we just skip this block. + break } body := chain.GetBodyRLP(hash) if body == nil { - continue // The block body is missing, we also have to skip. + break } q := receiptQueryParams{sizeLimit: uint64(maxPacketSize - bytes)} if i == 0 { @@ -326,7 +329,7 @@ func serviceGetReceiptsQuery70(chain *core.BlockChain, query GetReceiptsRequest, results, incomplete, err := blockReceiptsToNetwork(results, body, q) if err != nil { log.Error("Error in block receipts conversion", "hash", hash, "err", err) - continue + break } if results == nil { // This case triggers when the first receipt of the block receipts list doesn't From c5598fe9588be9d19ab210ebf53e760c7e195d95 Mon Sep 17 00:00:00 2001 From: Roshan <48975233+pythonberg1997@users.noreply.github.com> Date: Thu, 7 May 2026 16:44:26 +0800 Subject: [PATCH 19/63] core/txpool: change lock in Pending method of legacy pool to read lock (#32924) This PR makes a small update to the `Pending()` method in the legacy pool. By changing the lock from exclusive to read-only, it aims to improve concurrency performance. --------- Co-authored-by: rjl493456442 --- core/txpool/legacypool/legacypool.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/txpool/legacypool/legacypool.go b/core/txpool/legacypool/legacypool.go index 00630de04c..3d66803fd7 100644 --- a/core/txpool/legacypool/legacypool.go +++ b/core/txpool/legacypool/legacypool.go @@ -467,8 +467,8 @@ func (pool *LegacyPool) stats() (int, int) { // Content retrieves the data content of the transaction pool, returning all the // pending as well as queued transactions, grouped by account and sorted by nonce. func (pool *LegacyPool) Content() (map[common.Address][]*types.Transaction, map[common.Address][]*types.Transaction) { - pool.mu.Lock() - defer pool.mu.Unlock() + pool.mu.RLock() + defer pool.mu.RUnlock() pending := make(map[common.Address][]*types.Transaction, len(pool.pending)) for addr, list := range pool.pending { @@ -503,8 +503,8 @@ func (pool *LegacyPool) Pending(filter txpool.PendingFilter) (map[common.Address if filter.BlobTxs { return nil, 0 } - pool.mu.Lock() - defer pool.mu.Unlock() + pool.mu.RLock() + defer pool.mu.RUnlock() var count int pending := make(map[common.Address][]*txpool.LazyTransaction, len(pool.pending)) From f7b7d4c7e5cd9cd3b596c7a7430942cea8f5a755 Mon Sep 17 00:00:00 2001 From: cui Date: Thu, 7 May 2026 20:03:32 +0800 Subject: [PATCH 20/63] eth/tracers/logger: fix exclude address list (#34887) Fixes the exclusion list of the accessListTracer. --------- Co-authored-by: Sina M <1591639+s1na@users.noreply.github.com> --- eth/tracers/logger/access_list_tracer.go | 5 ++- eth/tracers/logger/access_list_tracer_test.go | 39 +++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 eth/tracers/logger/access_list_tracer_test.go diff --git a/eth/tracers/logger/access_list_tracer.go b/eth/tracers/logger/access_list_tracer.go index 749aade61b..31c3ebde93 100644 --- a/eth/tracers/logger/access_list_tracer.go +++ b/eth/tracers/logger/access_list_tracer.go @@ -112,9 +112,10 @@ type AccessListTracer struct { func NewAccessListTracer(acl types.AccessList, addressesToExclude map[common.Address]struct{}) *AccessListTracer { list := newAccessList() for _, al := range acl { - if _, ok := addressesToExclude[al.Address]; !ok { - list.addAddress(al.Address) + if _, ok := addressesToExclude[al.Address]; ok { + continue } + list.addAddress(al.Address) for _, slot := range al.StorageKeys { list.addSlot(al.Address, slot) } diff --git a/eth/tracers/logger/access_list_tracer_test.go b/eth/tracers/logger/access_list_tracer_test.go new file mode 100644 index 0000000000..04b2b4b31b --- /dev/null +++ b/eth/tracers/logger/access_list_tracer_test.go @@ -0,0 +1,39 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package logger + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +func TestNewAccessListTracerExcludedAddress(t *testing.T) { + excluded := common.HexToAddress("0x2222222222222222222222222222222222222222") + slot := common.HexToHash("0x01") + prelude := types.AccessList{{ + Address: excluded, + StorageKeys: []common.Hash{slot}, + }} + excl := map[common.Address]struct{}{excluded: {}} + tracer := NewAccessListTracer(prelude, excl) + got := tracer.AccessList() + if len(got) != 0 { + t.Fatalf("excluded prelude address must not contribute tuples, got %+v", got) + } +} From e1e3eaa38140c2ba1ed1eefafc126ae08d352ce9 Mon Sep 17 00:00:00 2001 From: cui Date: Thu, 7 May 2026 21:18:04 +0800 Subject: [PATCH 21/63] p2p/discover: copy buffer before sending read errors to unhandled (#34888) This fixes an issue where packets send to the `Unhandled` channel configured on discv4 could be corrupted when the packet buffer gets reused. --------- Co-authored-by: Felix Lange --- p2p/discover/v4_udp.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/p2p/discover/v4_udp.go b/p2p/discover/v4_udp.go index b06db4bdb2..ae8cbec3e2 100644 --- a/p2p/discover/v4_udp.go +++ b/p2p/discover/v4_udp.go @@ -555,8 +555,9 @@ func (t *UDPv4) readLoop(unhandled chan<- ReadPacket) { if err := t.handlePacket(from, buf[:nbytes]); err != nil && unhandled == nil { t.log.Debug("Bad discv4 packet", "addr", from, "err", err) } else if err != nil && unhandled != nil { + p := ReadPacket{bytes.Clone(buf[:nbytes]), from} select { - case unhandled <- ReadPacket{buf[:nbytes], from}: + case unhandled <- p: default: } } From e71098ba4e0814f2ced7165d518f63b281ad0697 Mon Sep 17 00:00:00 2001 From: cui Date: Fri, 8 May 2026 06:40:26 +0800 Subject: [PATCH 22/63] core/state: fix StateDB Reader Error Discard After Commit (#34899) --- core/state/statedb.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/state/statedb.go b/core/state/statedb.go index 1858f4758d..e6d8b5bffc 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -1355,7 +1355,7 @@ func (s *StateDB) commitAndFlush(block uint64, deleteEmptyObjects bool, noStorag // The reader update must be performed as the final step, otherwise, // the new state would not be visible before db.commit. - s.reader, _ = s.db.Reader(s.originalRoot) + s.reader, err = s.db.Reader(s.originalRoot) return ret, err } From 1abbae239d9a9f5797a5967350023c0f6b6aabb9 Mon Sep 17 00:00:00 2001 From: Delweng Date: Fri, 8 May 2026 10:12:46 +0800 Subject: [PATCH 23/63] eth,node: replace the deprecated TypeMux with Feed (#32585) replace the not used event.Typemux to event.Feed --------- Co-authored-by: Felix Lange --- cmd/geth/config.go | 10 ++- cmd/geth/main.go | 27 -------- cmd/utils/flags.go | 8 +-- eth/backend.go | 7 +- eth/downloader/api.go | 22 +++--- eth/downloader/downloader.go | 28 +++++--- eth/downloader/downloader_test.go | 3 +- eth/downloader/events.go | 24 +++++-- eth/handler.go | 35 ++++------ eth/syncer/syncer.go | 108 ++++++++++++++++++------------ node/node.go | 9 --- 11 files changed, 140 insertions(+), 141 deletions(-) diff --git a/cmd/geth/config.go b/cmd/geth/config.go index 8e2db32d76..40458186f4 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -38,6 +38,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/eth/catalyst" "github.com/ethereum/go-ethereum/eth/ethconfig" + "github.com/ethereum/go-ethereum/eth/syncer" "github.com/ethereum/go-ethereum/internal/flags" "github.com/ethereum/go-ethereum/internal/telemetry/tracesetup" "github.com/ethereum/go-ethereum/internal/version" @@ -276,16 +277,19 @@ func makeFullNode(ctx *cli.Context) *node.Node { if cfg.Ethstats.URL != "" { utils.RegisterEthStatsService(stack, backend, cfg.Ethstats.URL) } + // Configure synchronization override service - var synctarget common.Hash + syncConfig := syncer.Config{ + ExitWhenSynced: ctx.Bool(utils.ExitWhenSyncedFlag.Name), + } if ctx.IsSet(utils.SyncTargetFlag.Name) { target := ctx.String(utils.SyncTargetFlag.Name) if !common.IsHexHash(target) { utils.Fatalf("sync target hash is not a valid hex hash: %s", target) } - synctarget = common.HexToHash(target) + syncConfig.TargetBlock = common.HexToHash(target) } - utils.RegisterSyncOverrideService(stack, eth, synctarget, ctx.Bool(utils.ExitWhenSyncedFlag.Name)) + utils.RegisterSyncOverrideService(stack, eth, syncConfig) if ctx.IsSet(utils.DeveloperFlag.Name) { // Start dev mode. diff --git a/cmd/geth/main.go b/cmd/geth/main.go index c8d7abc65b..850e26d161 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -22,13 +22,10 @@ import ( "os" "slices" "sort" - "time" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/cmd/utils" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/console/prompt" - "github.com/ethereum/go-ethereum/eth/downloader" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/internal/debug" "github.com/ethereum/go-ethereum/internal/flags" @@ -387,28 +384,4 @@ func startNode(ctx *cli.Context, stack *node.Node, isConsole bool) { } } }() - - // Spawn a standalone goroutine for status synchronization monitoring, - // close the node when synchronization is complete if user required. - if ctx.Bool(utils.ExitWhenSyncedFlag.Name) { - go func() { - sub := stack.EventMux().Subscribe(downloader.DoneEvent{}) - defer sub.Unsubscribe() - for { - event := <-sub.Chan() - if event == nil { - continue - } - done, ok := event.Data.(downloader.DoneEvent) - if !ok { - continue - } - if timestamp := time.Unix(int64(done.Latest.Time), 0); time.Since(timestamp) < 10*time.Minute { - log.Info("Synchronisation completed", "latestnum", done.Latest.Number, "latesthash", done.Latest.Hash(), - "age", common.PrettyAge(timestamp)) - stack.Close() - } - } - }() - } } diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index cc4c3bff5c..ea0f6f5ee4 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -2237,13 +2237,13 @@ func RegisterFilterAPI(stack *node.Node, backend ethapi.Backend, ethcfg *ethconf } // RegisterSyncOverrideService adds the synchronization override service into node. -func RegisterSyncOverrideService(stack *node.Node, eth *eth.Ethereum, target common.Hash, exitWhenSynced bool) { - if target != (common.Hash{}) { - log.Info("Registered sync override service", "hash", target, "exitWhenSynced", exitWhenSynced) +func RegisterSyncOverrideService(stack *node.Node, eth *eth.Ethereum, config syncer.Config) { + if config.TargetBlock != (common.Hash{}) { + log.Info("Registered sync override service", "hash", config.TargetBlock, "exitWhenSynced", config.ExitWhenSynced) } else { log.Info("Registered sync override service") } - syncer.Register(stack, eth, target, exitWhenSynced) + syncer.Register(stack, eth, config) } // SetupMetrics configures the metrics system. diff --git a/eth/backend.go b/eth/backend.go index 6cfd1f6fa0..af8b04bda6 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -49,7 +49,6 @@ import ( "github.com/ethereum/go-ethereum/eth/protocols/snap" "github.com/ethereum/go-ethereum/eth/tracers" "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/internal/ethapi" "github.com/ethereum/go-ethereum/internal/shutdowncheck" "github.com/ethereum/go-ethereum/internal/version" @@ -105,7 +104,6 @@ type Ethereum struct { // DB interfaces chainDb ethdb.Database // Block chain database - eventMux *event.TypeMux engine consensus.Engine accountManager *accounts.Manager @@ -194,7 +192,6 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { eth := &Ethereum{ config: config, chainDb: chainDb, - eventMux: stack.EventMux(), accountManager: stack.AccountManager(), engine: engine, networkID: networkID, @@ -344,7 +341,6 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { Network: networkID, Sync: config.SyncMode, BloomCache: uint64(cacheLimit), - EventMux: eth.eventMux, RequiredBlocks: config.RequiredBlocks, }); err != nil { return nil, err @@ -405,7 +401,7 @@ func (s *Ethereum) APIs() []rpc.API { Service: NewMinerAPI(s), }, { Namespace: "eth", - Service: downloader.NewDownloaderAPI(s.handler.downloader, s.blockchain, s.eventMux), + Service: downloader.NewDownloaderAPI(s.handler.downloader, s.blockchain), }, { Namespace: "admin", Service: NewAdminAPI(s), @@ -600,7 +596,6 @@ func (s *Ethereum) Stop() error { s.shutdownTracker.Stop() s.chainDb.Close() - s.eventMux.Stop() return nil } diff --git a/eth/downloader/api.go b/eth/downloader/api.go index 1fea35775e..6033e44474 100644 --- a/eth/downloader/api.go +++ b/eth/downloader/api.go @@ -23,7 +23,6 @@ import ( "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/rpc" ) @@ -33,20 +32,18 @@ import ( type DownloaderAPI struct { d *Downloader chain *core.BlockChain - mux *event.TypeMux installSyncSubscription chan chan interface{} uninstallSyncSubscription chan *uninstallSyncSubscriptionRequest } // NewDownloaderAPI creates a new DownloaderAPI. The API has an internal event loop that -// listens for events from the downloader through the global event mux. In case it receives one of +// listens for events from the downloader through the event feed. In case it receives one of // these events it broadcasts it to all syncing subscriptions that are installed through the // installSyncSubscription channel. -func NewDownloaderAPI(d *Downloader, chain *core.BlockChain, m *event.TypeMux) *DownloaderAPI { +func NewDownloaderAPI(d *Downloader, chain *core.BlockChain) *DownloaderAPI { api := &DownloaderAPI{ d: d, chain: chain, - mux: m, installSyncSubscription: make(chan chan interface{}), uninstallSyncSubscription: make(chan *uninstallSyncSubscriptionRequest), } @@ -66,7 +63,8 @@ func NewDownloaderAPI(d *Downloader, chain *core.BlockChain, m *event.TypeMux) * // receive is {false}. func (api *DownloaderAPI) eventLoop() { var ( - sub = api.mux.Subscribe(StartEvent{}) + events = make(chan SyncEvent, 16) + sub = api.d.SubscribeSyncEvents(events) syncSubscriptions = make(map[chan interface{}]struct{}) checkInterval = time.Second * 60 checkTimer = time.NewTimer(checkInterval) @@ -90,6 +88,7 @@ func (api *DownloaderAPI) eventLoop() { } ) defer checkTimer.Stop() + defer sub.Unsubscribe() for { select { @@ -101,14 +100,13 @@ func (api *DownloaderAPI) eventLoop() { case u := <-api.uninstallSyncSubscription: delete(syncSubscriptions, u.c) close(u.uninstalled) - case event := <-sub.Chan(): - if event == nil { - return - } - switch event.Data.(type) { - case StartEvent: + case ev := <-events: + if ev.Type == SyncStarted { started = true } + case <-sub.Err(): + // The downloader is terminated or other internal error occurs + return case <-checkTimer.C: if !started { checkTimer.Reset(checkInterval) diff --git a/eth/downloader/downloader.go b/eth/downloader/downloader.go index 1de0933842..4a575d6856 100644 --- a/eth/downloader/downloader.go +++ b/eth/downloader/downloader.go @@ -97,9 +97,12 @@ type headerTask struct { } type Downloader struct { - mode atomic.Uint32 // Synchronisation mode defining the strategy used (per sync cycle), use d.getMode() to get the SyncMode - moder *syncModer // Sync mode management, deliver the appropriate sync mode choice for each cycle - mux *event.TypeMux // Event multiplexer to announce sync operation events + mode atomic.Uint32 // Synchronisation mode defining the strategy used (per sync cycle), use d.getMode() to get the SyncMode + moder *syncModer // Sync mode management, deliver the appropriate sync mode choice for each cycle + + // Event feed for downloader events + feed event.FeedOf[SyncEvent] + scope event.SubscriptionScope queue *queue // Scheduler for selecting the hashes to download peers *peerSet // Set of active peers from which download can proceed @@ -229,12 +232,11 @@ type BlockChain interface { } // New creates a new downloader to fetch hashes and blocks from remote peers. -func New(stateDb ethdb.Database, mode ethconfig.SyncMode, mux *event.TypeMux, chain BlockChain, dropPeer peerDropFn, success func()) *Downloader { +func New(stateDb ethdb.Database, mode ethconfig.SyncMode, chain BlockChain, dropPeer peerDropFn, success func()) *Downloader { cutoffNumber, cutoffHash := chain.HistoryPruningCutoff() dl := &Downloader{ stateDB: stateDb, moder: newSyncModer(mode, chain, stateDb), - mux: mux, queue: newQueue(blockCacheMaxItems, blockCacheInitialItems), peers: newPeerSet(), blockchain: chain, @@ -427,20 +429,25 @@ func (d *Downloader) ConfigSyncMode() SyncMode { return d.moder.get(false) } +// SubscribeSyncEvents creates a subscription for downloader sync events +func (d *Downloader) SubscribeSyncEvents(ch chan<- SyncEvent) event.Subscription { + return d.scope.Track(d.feed.Subscribe(ch)) +} + // syncToHead starts a block synchronization based on the hash chain from // the specified head hash. func (d *Downloader) syncToHead() (err error) { - d.mux.Post(StartEvent{}) + mode := d.getMode() + d.feed.Send(SyncEvent{Type: SyncStarted, Mode: mode}) defer func() { // reset on error if err != nil { - d.mux.Post(FailedEvent{err}) + d.feed.Send(SyncEvent{Type: SyncFailed, Mode: mode, Err: err}) } else { latest := d.blockchain.CurrentHeader() - d.mux.Post(DoneEvent{latest}) + d.feed.Send(SyncEvent{Type: SyncCompleted, Mode: mode, Latest: latest}) } }() - mode := d.getMode() log.Debug("Backfilling with the network", "mode", mode) defer func(start time.Time) { @@ -662,6 +669,9 @@ func (d *Downloader) Cancel() { // Terminate interrupts the downloader, canceling all pending operations. // The downloader cannot be reused after calling Terminate. func (d *Downloader) Terminate() { + // Unsubscribe all subscriptions registered from downloader + d.scope.Close() + // Close the termination channel (make sure double close is allowed) d.quitLock.Lock() select { diff --git a/eth/downloader/downloader_test.go b/eth/downloader/downloader_test.go index 6d5d159631..e6c477cd33 100644 --- a/eth/downloader/downloader_test.go +++ b/eth/downloader/downloader_test.go @@ -32,7 +32,6 @@ import ( "github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/eth/protocols/eth" "github.com/ethereum/go-ethereum/eth/protocols/snap" - "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" @@ -75,7 +74,7 @@ func newTesterWithNotification(t *testing.T, mode ethconfig.SyncMode, success fu chain: chain, peers: make(map[string]*downloadTesterPeer), } - tester.downloader = New(db, mode, new(event.TypeMux), tester.chain, tester.dropPeer, success) + tester.downloader = New(db, mode, tester.chain, tester.dropPeer, success) return tester } diff --git a/eth/downloader/events.go b/eth/downloader/events.go index 25255a3a72..0fb380a857 100644 --- a/eth/downloader/events.go +++ b/eth/downloader/events.go @@ -16,10 +16,24 @@ package downloader -import "github.com/ethereum/go-ethereum/core/types" +import ( + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth/ethconfig" +) -type DoneEvent struct { - Latest *types.Header +// SyncEventType represents the type of sync event +type SyncEventType int + +const ( + SyncStarted SyncEventType = iota + SyncFailed + SyncCompleted +) + +// SyncEvent represents a downloader synchronization event +type SyncEvent struct { + Type SyncEventType + Mode ethconfig.SyncMode + Err error // Set when Type is SyncFailed + Latest *types.Header // Set when Type is SyncCompleted } -type StartEvent struct{} -type FailedEvent struct{ Err error } diff --git a/eth/handler.go b/eth/handler.go index 27b5e60697..76df635fb0 100644 --- a/eth/handler.go +++ b/eth/handler.go @@ -107,7 +107,6 @@ type handlerConfig struct { Network uint64 // Network identifier to advertise Sync ethconfig.SyncMode // Whether to snap or full sync BloomCache uint64 // Megabytes to alloc for snap sync bloom - EventMux *event.TypeMux // Legacy event mux, deprecate for `feed` RequiredBlocks map[uint64]common.Hash // Hard coded map of required block hashes for sync challenges } @@ -126,7 +125,6 @@ type handler struct { peers *peerSet txBroadcastKey [16]byte - eventMux *event.TypeMux txsCh chan core.NewTxsEvent txsSub event.Subscription blockRange *blockRangeState @@ -144,14 +142,9 @@ type handler struct { // newHandler returns a handler for all Ethereum chain management protocol. func newHandler(config *handlerConfig) (*handler, error) { - // Create the protocol manager with the base fields - if config.EventMux == nil { - config.EventMux = new(event.TypeMux) // Nicety initialization for tests - } h := &handler{ nodeID: config.NodeID, networkID: config.Network, - eventMux: config.EventMux, database: config.Database, txpool: config.TxPool, chain: config.Chain, @@ -163,7 +156,7 @@ func newHandler(config *handlerConfig) (*handler, error) { handlerStartCh: make(chan struct{}), } // Construct the downloader (long sync) - h.downloader = downloader.New(config.Database, config.Sync, h.eventMux, h.chain, h.removePeer, h.enableSyncedFeatures) + h.downloader = downloader.New(config.Database, config.Sync, h.chain, h.removePeer, h.enableSyncedFeatures) // If snap sync is requested but snapshots are disabled, fail loudly if h.downloader.ConfigSyncMode() == ethconfig.SnapSync && (config.Chain.Snapshots() == nil && config.Chain.TrieDB().Scheme() == rawdb.HashScheme) { @@ -420,7 +413,7 @@ func (h *handler) Start(maxPeers int) { // broadcast block range h.wg.Add(1) - h.blockRange = newBlockRangeState(h.chain, h.eventMux) + h.blockRange = newBlockRangeState(h.chain, h.downloader) go h.blockRangeLoop(h.blockRange) // start sync handlers @@ -536,16 +529,19 @@ type blockRangeState struct { next atomic.Pointer[eth.BlockRangeUpdatePacket] headCh chan core.ChainHeadEvent headSub event.Subscription - syncSub *event.TypeMuxSubscription + syncCh chan downloader.SyncEvent + syncSub event.Subscription } -func newBlockRangeState(chain *core.BlockChain, typeMux *event.TypeMux) *blockRangeState { +func newBlockRangeState(chain *core.BlockChain, dl *downloader.Downloader) *blockRangeState { headCh := make(chan core.ChainHeadEvent, chainHeadChanSize) headSub := chain.SubscribeChainHeadEvent(headCh) - syncSub := typeMux.Subscribe(downloader.StartEvent{}, downloader.DoneEvent{}, downloader.FailedEvent{}) + syncCh := make(chan downloader.SyncEvent, 16) + syncSub := dl.SubscribeSyncEvents(syncCh) st := &blockRangeState{ headCh: headCh, headSub: headSub, + syncCh: syncCh, syncSub: syncSub, } st.update(chain, chain.CurrentBlock()) @@ -561,11 +557,8 @@ func (h *handler) blockRangeLoop(st *blockRangeState) { for { select { - case ev := <-st.syncSub.Chan(): - if ev == nil { - continue - } - if _, ok := ev.Data.(downloader.StartEvent); ok && h.downloader.ConfigSyncMode() == ethconfig.SnapSync { + case ev := <-st.syncCh: + if ev.Type == downloader.SyncStarted && ev.Mode == ethconfig.SnapSync { h.blockRangeWhileSnapSyncing(st) } case <-st.headCh: @@ -593,12 +586,8 @@ func (h *handler) blockRangeWhileSnapSyncing(st *blockRangeState) { h.broadcastBlockRange(st) } // back to processing head block updates when sync is done - case ev := <-st.syncSub.Chan(): - if ev == nil { - continue - } - switch ev.Data.(type) { - case downloader.FailedEvent, downloader.DoneEvent: + case ev := <-st.syncCh: + if ev.Type == downloader.SyncFailed || ev.Type == downloader.SyncCompleted { return } // ignore head updates, but exit when the subscription ends diff --git a/eth/syncer/syncer.go b/eth/syncer/syncer.go index c0d54b953b..b04d8f22e8 100644 --- a/eth/syncer/syncer.go +++ b/eth/syncer/syncer.go @@ -26,6 +26,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/eth" + "github.com/ethereum/go-ethereum/eth/downloader" "github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/node" @@ -37,32 +38,40 @@ type syncReq struct { errc chan error } +type Config struct { + TargetBlock common.Hash // if set, sync is triggered at startup + ExitWhenSynced bool // if true, the node shuts down after sync has finished +} + // Syncer is an auxiliary service that allows Geth to perform full sync // alone without consensus-layer attached. Users must specify a valid block hash // as the sync target. // +// Additionally, the syncer can be used to monitor state synchronization. +// It will exit once the specified target has been reached or when the +// most recent chain head is caught up. +// // This tool can be applied to different networks, no matter it's pre-merge or // post-merge, but only for full-sync. type Syncer struct { - stack *node.Node - backend *eth.Ethereum - target common.Hash - request chan *syncReq - closed chan struct{} - wg sync.WaitGroup - exitWhenSynced bool + stack *node.Node + backend *eth.Ethereum + request chan *syncReq + closed chan struct{} + wg sync.WaitGroup + + config Config } // Register registers the synchronization override service into the node // stack for launching and stopping the service controlled by node. -func Register(stack *node.Node, backend *eth.Ethereum, target common.Hash, exitWhenSynced bool) (*Syncer, error) { +func Register(stack *node.Node, backend *eth.Ethereum, cfg Config) (*Syncer, error) { s := &Syncer{ - stack: stack, - backend: backend, - target: target, - request: make(chan *syncReq), - closed: make(chan struct{}), - exitWhenSynced: exitWhenSynced, + stack: stack, + backend: backend, + request: make(chan *syncReq), + closed: make(chan struct{}), + config: cfg, } stack.RegisterAPIs(s.APIs()) stack.RegisterLifecycle(s) @@ -88,9 +97,11 @@ func (s *Syncer) run() { var ( target *types.Header - ticker = time.NewTicker(time.Second * 5) + syncCh = make(chan downloader.SyncEvent, 10) ) - defer ticker.Stop() + sub := s.backend.Downloader().SubscribeSyncEvents(syncCh) + defer sub.Unsubscribe() + for { select { case req := <-s.request: @@ -137,35 +148,50 @@ func (s *Syncer) run() { } } - case <-ticker.C: - if target == nil { + case ev := <-syncCh: + if ev.Type == downloader.SyncStarted { + log.Debug("Synchronization started") continue } + if ev.Type == downloader.SyncFailed { + log.Debug("Synchronization failed", "err", ev.Err) + continue + } + + head := s.backend.BlockChain().CurrentHeader() + if head != nil { + // Set the finalized and safe markers relative to the current head. + // The finalized marker is set two epochs behind the target, + // and the safe marker is set one epoch behind the target. + if header := s.backend.BlockChain().GetHeaderByNumber(head.Number.Uint64() - params.EpochLength*2); header != nil { + if final := s.backend.BlockChain().CurrentFinalBlock(); final == nil || final.Number.Cmp(header.Number) < 0 { + s.backend.BlockChain().SetFinalized(header) + } + } + if header := s.backend.BlockChain().GetHeaderByNumber(head.Number.Uint64() - params.EpochLength); header != nil { + if safe := s.backend.BlockChain().CurrentSafeBlock(); safe == nil || safe.Number.Cmp(header.Number) < 0 { + s.backend.BlockChain().SetSafe(header) + } + } + } // Terminate the node if the target has been reached - if s.exitWhenSynced { - if block := s.backend.BlockChain().GetBlockByHash(target.Hash()); block != nil { - log.Info("Sync target reached", "number", block.NumberU64(), "hash", block.Hash()) - go s.stack.Close() // async since we need to close ourselves - return + if s.config.ExitWhenSynced { + var synced bool + var block *types.Header + if target != nil { + tb := s.backend.BlockChain().GetBlockByHash(target.Hash()) + synced = tb != nil + block = tb.Header() + } else { + timestamp := time.Unix(int64(ev.Latest.Time), 0) + synced = time.Since(timestamp) < 10*time.Minute + block = ev.Latest } - } - // Set the finalized and safe markers relative to the current head. - // The finalized marker is set two epochs behind the target, - // and the safe marker is set one epoch behind the target. - head := s.backend.BlockChain().CurrentHeader() - if head == nil { - continue - } - if header := s.backend.BlockChain().GetHeaderByNumber(head.Number.Uint64() - params.EpochLength*2); header != nil { - if final := s.backend.BlockChain().CurrentFinalBlock(); final == nil || final.Number.Cmp(header.Number) < 0 { - s.backend.BlockChain().SetFinalized(header) - } - } - if header := s.backend.BlockChain().GetHeaderByNumber(head.Number.Uint64() - params.EpochLength); header != nil { - if safe := s.backend.BlockChain().CurrentSafeBlock(); safe == nil || safe.Number.Cmp(header.Number) < 0 { - s.backend.BlockChain().SetSafe(header) + if synced { + log.Info("Sync target reached", "number", block.Number.Uint64(), "hash", block.Hash()) + go s.stack.Close() // async since we need to close ourselves } } @@ -179,10 +205,10 @@ func (s *Syncer) run() { func (s *Syncer) Start() error { s.wg.Add(1) go s.run() - if s.target == (common.Hash{}) { + if s.config.TargetBlock == (common.Hash{}) { return nil } - return s.Sync(s.target) + return s.Sync(s.config.TargetBlock) } // Stop terminates the synchronization service and stop all background activities. diff --git a/node/node.go b/node/node.go index 01318881d4..56ecd7d522 100644 --- a/node/node.go +++ b/node/node.go @@ -35,7 +35,6 @@ import ( "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb/memorydb" - "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/rpc" @@ -44,7 +43,6 @@ import ( // Node is a container on which services can be registered. type Node struct { - eventmux *event.TypeMux config *Config accman *accounts.Manager log log.Logger @@ -108,7 +106,6 @@ func New(conf *Config) (*Node, error) { node := &Node{ config: conf, inprocHandler: server, - eventmux: new(event.TypeMux), log: conf.Logger, stop: make(chan struct{}), server: &p2p.Server{Config: conf.P2P}, @@ -692,12 +689,6 @@ func (n *Node) WSAuthEndpoint() string { return "ws://" + n.wsAuth.listenAddr() + n.wsAuth.wsConfig.prefix } -// EventMux retrieves the event multiplexer used by all the network services in -// the current protocol stack. -func (n *Node) EventMux() *event.TypeMux { - return n.eventmux -} - // OpenDatabaseWithOptions opens an existing database with the given name (or creates one if no // previous can be found) from within the node's instance directory. If the node has no // data directory, an in-memory database is returned. From 281dc4c2091400ea1388b030300e5adc7672ffa0 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Fri, 8 May 2026 11:33:19 +0200 Subject: [PATCH 24/63] p2p/discover: decouple nodeFeed from Table mutex in waitForNodes (#34898) Fixes #34881 This fixes a hang in `Table.waitForNodes`. It is a replacement for PRs #34890, #33665 which tried to fix the same issue in a different way. - #34890 doesn't really fix the issue, just makes it less likely - #33665 tries to fix it by moving the feed send outside of the lock I created this PR because I want to keep the synchronous node feed sending in `Table.nodeAdded`. --------- Co-authored-by: Csaba Kiraly --- p2p/discover/table.go | 55 +++++++++++++++++++++++++++++--------- p2p/discover/table_test.go | 40 +++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 12 deletions(-) diff --git a/p2p/discover/table.go b/p2p/discover/table.go index 721cd7b589..016a2d1af3 100644 --- a/p2p/discover/table.go +++ b/p2p/discover/table.go @@ -753,6 +753,41 @@ func (tab *Table) deleteNode(n *enode.Node) { // waitForNodes blocks until the table contains at least n nodes. func (tab *Table) waitForNodes(ctx context.Context, n int) error { + // Wrap ctx so the forwarder goroutine exits when waitForNodes returns, + // regardless of whether the caller's ctx is canceled. + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + // Set up a notification channel that gets unblocked when there was any activity on + // the table. Ultimately this reads from the table's nodeFeed, but can't use the feed + // directly on the same goroutine that takes Table.mutex, it would deadlock. + var notify chan struct{} + var notifyErr error + initsub := func() event.Subscription { + notify = make(chan struct{}, 1) + newnode := make(chan *enode.Node, 1) + sub := tab.nodeFeed.Subscribe(newnode) + go func() { + defer close(notify) + for { + select { + case <-newnode: + select { + case notify <- struct{}{}: + default: + } + case <-ctx.Done(): + notifyErr = ctx.Err() + return + case <-tab.closeReq: + notifyErr = errClosed + return + } + } + }() + return sub + } + getlength := func() (count int) { for _, b := range &tab.buckets { count += len(b.entries) @@ -760,28 +795,24 @@ func (tab *Table) waitForNodes(ctx context.Context, n int) error { return count } - var ch chan *enode.Node for { tab.mutex.Lock() if getlength() >= n { tab.mutex.Unlock() return nil } - if ch == nil { - // Init subscription. - ch = make(chan *enode.Node) - sub := tab.nodeFeed.Subscribe(ch) + if notify == nil { + // Lazily init the subscription. Do this while holding the + // lock so we don't miss any events that change the node count. + sub := initsub() defer sub.Unsubscribe() } tab.mutex.Unlock() - // Wait for a node add event. - select { - case <-ch: - case <-ctx.Done(): - return ctx.Err() - case <-tab.closeReq: - return errClosed + // Wait for table event. + if _, ok := <-notify; !ok { + break } } + return notifyErr } diff --git a/p2p/discover/table_test.go b/p2p/discover/table_test.go index c3b71ea5a6..a16b4d9cab 100644 --- a/p2p/discover/table_test.go +++ b/p2p/discover/table_test.go @@ -17,6 +17,7 @@ package discover import ( + "context" "crypto/ecdsa" "fmt" "math/rand" @@ -550,6 +551,45 @@ func TestSetFallbackNodes_DNSHostname(t *testing.T) { t.Logf("resolved localhost to %v", resolved.IPAddr()) } +// This test checks that waitForNodes does not block addFoundNode. +// See https://github.com/ethereum/go-ethereum/issues/34881. +func TestTable_waitForNodesLocking(t *testing.T) { + transport := newPingRecorder() + tab, db := newTestTable(transport, Config{}) + defer db.Close() + defer tab.close() + <-tab.initDone + + // waitForNodes will never reach this count, so it stays subscribed + // to nodeFeed and looping for the duration of the test. + waitCtx, cancelWait := context.WithCancel(context.Background()) + defer cancelWait() + waitDone := make(chan struct{}) + go func() { + defer close(waitDone) + tab.waitForNodes(waitCtx, 1<<20) + }() + + // Call addFoundNode in loop to send to the feed. + addDone := make(chan struct{}) + go func() { + defer close(addDone) + for i := range 10000 { + d := 240 + (i % 17) + n := nodeAtDistance(tab.self().ID(), d, intIP(i)) + tab.addFoundNode(n, true) + } + }() + + select { + case <-addDone: + cancelWait() + <-waitDone + case <-time.After(10 * time.Second): + t.Fatal("deadlock detected: add loop did not finish within 10s") + } +} + func newkey() *ecdsa.PrivateKey { key, err := crypto.GenerateKey() if err != nil { From 0ad890e3af967f1d2d3bec3b972f76fc4c208929 Mon Sep 17 00:00:00 2001 From: Miki Noir Date: Fri, 8 May 2026 11:43:31 +0100 Subject: [PATCH 25/63] core/txpool/blobpool: continue on cell proof error in `GetBlobs` (#34891) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `GetBlobs` returned early when `CellProofsAt` reported corrupted/out-of-bounds proofs, dropping every blob already collected and aborting the remaining hashes — a single bad sidecar killed the whole Engine API batch for consensus clients. Replaced the `return nil, nil, nil, err` with `log.Error + continue` so the slot stays `nil` per the sparse-array contract, matching the store/RLP/nil-sidecar branches a few lines above. --- core/txpool/blobpool/blobpool.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/txpool/blobpool/blobpool.go b/core/txpool/blobpool/blobpool.go index efa41a0649..f2e0d5f9d2 100644 --- a/core/txpool/blobpool/blobpool.go +++ b/core/txpool/blobpool/blobpool.go @@ -1517,7 +1517,8 @@ func (p *BlobPool) GetBlobs(vhashes []common.Hash, version byte) ([]*kzg4844.Blo case types.BlobSidecarVersion1: cellProofs, err := sidecar.CellProofsAt(i) if err != nil { - return nil, nil, nil, err + log.Error("Failed to get cell proofs", "id", txID, "err", err) + continue } pf = cellProofs } From 592209c0ee99bd9c3046d27ecea5b970c64a8730 Mon Sep 17 00:00:00 2001 From: Sina M <1591639+s1na@users.noreply.github.com> Date: Fri, 8 May 2026 15:18:24 +0200 Subject: [PATCH 26/63] .gitea, build: cross-compile windows binaries (#34889) --- .gitea/workflows/release.yml | 35 ++++------ build/ci.go | 125 ++++++++++++++++++++++------------- internal/build/gotool.go | 13 +++- 3 files changed, 100 insertions(+), 73 deletions(-) diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index efe76cf170..a3fa4a2ea7 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -145,7 +145,7 @@ jobs: windows: name: Windows Build - runs-on: "win-11" + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -155,57 +155,46 @@ jobs: go-version: 1.24 cache: false - # Note: gcc.exe only works properly if the corresponding bin/ directory is - # contained in PATH. + - name: Install cross toolchain + run: | + apt-get update + apt-get -yq --no-install-suggests --no-install-recommends install \ + gcc-mingw-w64-x86-64 gcc-mingw-w64-i686 nsis - name: "Build (amd64)" - shell: cmd run: | - set PATH=%GETH_MINGW%\bin;%PATH% - go run build/ci.go install -dlgo -arch amd64 -cc %GETH_MINGW%\bin\gcc.exe - env: - GETH_MINGW: 'C:\msys64\mingw64' + go run build/ci.go install -dlgo -os windows -arch amd64 -cc x86_64-w64-mingw32-gcc - name: "Create/upload archive (amd64)" - shell: cmd run: | - go run build/ci.go archive -arch amd64 -type zip -signer WINDOWS_SIGNING_KEY -upload gethstore/builds + go run build/ci.go archive -os windows -arch amd64 -type zip -signer WINDOWS_SIGNING_KEY -upload gethstore/builds env: WINDOWS_SIGNING_KEY: ${{ secrets.WINDOWS_SIGNING_KEY }} AZURE_BLOBSTORE_TOKEN: ${{ secrets.AZURE_BLOBSTORE_TOKEN }} - name: "Create/upload NSIS installer (amd64)" - shell: cmd run: | - set "PATH=C:\Program Files (x86)\NSIS;%PATH%" go run build/ci.go nsis -arch amd64 -signer WINDOWS_SIGNING_KEY -upload gethstore/builds - del /Q build\bin\* + rm -f build/bin/* env: WINDOWS_SIGNING_KEY: ${{ secrets.WINDOWS_SIGNING_KEY }} AZURE_BLOBSTORE_TOKEN: ${{ secrets.AZURE_BLOBSTORE_TOKEN }} - name: "Build (386)" - shell: cmd run: | - set PATH=%GETH_MINGW%\bin;%PATH% - go run build/ci.go install -dlgo -arch 386 -cc %GETH_MINGW%\bin\gcc.exe - env: - GETH_MINGW: 'C:\msys64\mingw32' + go run build/ci.go install -dlgo -os windows -arch 386 -cc i686-w64-mingw32-gcc - name: "Create/upload archive (386)" - shell: cmd run: | - go run build/ci.go archive -arch 386 -type zip -signer WINDOWS_SIGNING_KEY -upload gethstore/builds + go run build/ci.go archive -os windows -arch 386 -type zip -signer WINDOWS_SIGNING_KEY -upload gethstore/builds env: WINDOWS_SIGNING_KEY: ${{ secrets.WINDOWS_SIGNING_KEY }} AZURE_BLOBSTORE_TOKEN: ${{ secrets.AZURE_BLOBSTORE_TOKEN }} - name: "Create/upload NSIS installer (386)" - shell: cmd run: | - set "PATH=C:\Program Files (x86)\NSIS;%PATH%" go run build/ci.go nsis -arch 386 -signer WINDOWS_SIGNING_KEY -upload gethstore/builds - del /Q build\bin\* + rm -f build/bin/* env: WINDOWS_SIGNING_KEY: ${{ secrets.WINDOWS_SIGNING_KEY }} AZURE_BLOBSTORE_TOKEN: ${{ secrets.AZURE_BLOBSTORE_TOKEN }} diff --git a/build/ci.go b/build/ci.go index 173288bcdc..173a3280ce 100644 --- a/build/ci.go +++ b/build/ci.go @@ -73,21 +73,9 @@ var ( "./cmd/keeper", } - // Files that end up in the geth*.zip archive. - gethArchiveFiles = []string{ - "COPYING", - executablePath("geth"), - } - - // Files that end up in the geth-alltools*.zip archive. - allToolsArchiveFiles = []string{ - "COPYING", - executablePath("abigen"), - executablePath("evm"), - executablePath("geth"), - executablePath("rlpdump"), - executablePath("clef"), - } + // Files that end up in the geth-alltools*.zip archive (and the NSIS installer + // dev-tools section). Order matches the historical layout produced by ci.go. + allToolsBinaries = []string{"abigen", "evm", "geth", "rlpdump", "clef"} // Keeper build targets with their configurations keeperTargets = []struct { @@ -180,13 +168,35 @@ var ( var GOBIN, _ = filepath.Abs(filepath.Join("build", "bin")) -func executablePath(name string) string { - if runtime.GOOS == "windows" { +// executablePath returns the path to a built binary in GOBIN, applying the +// platform-specific extension for the given target OS. +func executablePath(name, targetOS string) string { + if targetOS == "windows" { name += ".exe" } return filepath.Join(GOBIN, name) } +// gethArchiveFiles returns the file list for the geth-{platform}-{ver}.zip +// archive, with binary paths resolved for the target OS. +func gethArchiveFiles(targetOS string) []string { + return []string{ + "COPYING", + executablePath("geth", targetOS), + } +} + +// allToolsArchiveFiles returns the file list for the +// geth-alltools-{platform}-{ver}.zip archive, with binary paths resolved for +// the target OS. +func allToolsArchiveFiles(targetOS string) []string { + files := []string{"COPYING"} + for _, name := range allToolsBinaries { + files = append(files, executablePath(name, targetOS)) + } + return files +} + func main() { log.SetFlags(log.Lshortfile) @@ -233,6 +243,7 @@ func main() { func doInstall(cmdline []string) { var ( dlgo = flag.Bool("dlgo", false, "Download Go and build with it") + targetOS = flag.String("os", runtime.GOOS, "Target OS to cross build for") arch = flag.String("arch", "", "Architecture to cross build for") cc = flag.String("cc", "", "C compiler to cross build with") staticlink = flag.Bool("static", false, "Create statically-linked executable") @@ -241,7 +252,7 @@ func doInstall(cmdline []string) { env := build.Env() // Configure the toolchain. - tc := build.GoToolchain{GOARCH: *arch, CC: *cc} + tc := build.GoToolchain{GOOS: *targetOS, GOARCH: *arch, CC: *cc} if *dlgo { csdb := download.MustLoadChecksums("build/checksums.txt") tc.Root = build.DownloadGo(csdb) @@ -255,7 +266,7 @@ func doInstall(cmdline []string) { } // Configure the build. - gobuild := tc.Go("build", buildFlags(env, *staticlink, buildTags)...) + gobuild := tc.Go("build", buildFlags(env, *staticlink, buildTags, *targetOS)...) // Show packages during build. gobuild.Args = append(gobuild.Args, "-v") @@ -270,7 +281,7 @@ func doInstall(cmdline []string) { // Do the build! for _, pkg := range packages { args := slices.Clone(gobuild.Args) - args = append(args, "-o", executablePath(path.Base(pkg))) + args = append(args, "-o", executablePath(path.Base(pkg), *targetOS)) args = append(args, pkg) build.MustRun(&exec.Cmd{Path: gobuild.Path, Args: args, Env: gobuild.Env}) } @@ -297,7 +308,13 @@ func doInstallKeeper(cmdline []string) { tc.GOARCH = target.GOARCH tc.GOOS = target.GOOS tc.CC = target.CC - gobuild := tc.Go("build", buildFlags(env, true, []string{target.Tags})...) + // An empty GOOS means "build for the host OS"; thread that through to + // buildFlags so platform-specific linker flags are picked correctly. + targetOS := target.GOOS + if targetOS == "" { + targetOS = runtime.GOOS + } + gobuild := tc.Go("build", buildFlags(env, true, []string{target.Tags}, targetOS)...) gobuild.Dir = "./cmd/keeper" gobuild.Args = append(gobuild.Args, "-v") @@ -307,14 +324,15 @@ func doInstallKeeper(cmdline []string) { outputName := fmt.Sprintf("keeper-%s", target.Name) args := slices.Clone(gobuild.Args) - args = append(args, "-o", executablePath(outputName)) + args = append(args, "-o", executablePath(outputName, targetOS)) args = append(args, ".") build.MustRun(&exec.Cmd{Path: gobuild.Path, Args: args, Env: gobuild.Env, Dir: gobuild.Dir}) } } -// buildFlags returns the go tool flags for building. -func buildFlags(env build.Environment, staticLinking bool, buildTags []string) (flags []string) { +// buildFlags returns the go tool flags for building. targetOS is the OS we +// are producing binaries for. +func buildFlags(env build.Environment, staticLinking bool, buildTags []string, targetOS string) (flags []string) { var ld []string // See https://github.com/golang/go/issues/33772#issuecomment-528176001 // We need to set --buildid to the linker here, and also pass --build-id to the @@ -326,10 +344,10 @@ func buildFlags(env build.Environment, staticLinking bool, buildTags []string) ( } // Strip DWARF on darwin. This used to be required for certain things, // and there is no downside to this, so we just keep doing it. - if runtime.GOOS == "darwin" { + if targetOS == "darwin" { ld = append(ld, "-s") } - if runtime.GOOS == "linux" { + if targetOS == "linux" { // Enforce the stacksize to 8M, which is the case on most platforms apart from // alpine Linux. // See https://sourceware.org/binutils/docs-2.23.1/ld/Options.html#Options @@ -682,12 +700,13 @@ func downloadProtoc(cachedir string) string { // Release Packaging func doArchive(cmdline []string) { var ( - arch = flag.String("arch", runtime.GOARCH, "Architecture cross packaging") - atype = flag.String("type", "zip", "Type of archive to write (zip|tar)") - signer = flag.String("signer", "", `Environment variable holding the signing key (e.g. LINUX_SIGNING_KEY)`) - signify = flag.String("signify", "", `Environment variable holding the signify key (e.g. LINUX_SIGNIFY_KEY)`) - upload = flag.String("upload", "", `Destination to upload the archives (usually "gethstore/builds")`) - ext string + targetOS = flag.String("os", runtime.GOOS, "Target OS the binaries were built for") + arch = flag.String("arch", runtime.GOARCH, "Architecture cross packaging") + atype = flag.String("type", "zip", "Type of archive to write (zip|tar)") + signer = flag.String("signer", "", `Environment variable holding the signing key (e.g. LINUX_SIGNING_KEY)`) + signify = flag.String("signify", "", `Environment variable holding the signify key (e.g. LINUX_SIGNIFY_KEY)`) + upload = flag.String("upload", "", `Destination to upload the archives (usually "gethstore/builds")`) + ext string ) flag.CommandLine.Parse(cmdline) switch *atype { @@ -701,15 +720,15 @@ func doArchive(cmdline []string) { var ( env = build.Env() - basegeth = archiveBasename(*arch, version.Archive(env.Commit)) + basegeth = archiveBasename(*targetOS, *arch, version.Archive(env.Commit)) geth = "geth-" + basegeth + ext alltools = "geth-alltools-" + basegeth + ext ) maybeSkipArchive(env) - if err := build.WriteArchive(geth, gethArchiveFiles); err != nil { + if err := build.WriteArchive(geth, gethArchiveFiles(*targetOS)); err != nil { log.Fatal(err) } - if err := build.WriteArchive(alltools, allToolsArchiveFiles); err != nil { + if err := build.WriteArchive(alltools, allToolsArchiveFiles(*targetOS)); err != nil { log.Fatal(err) } for _, archive := range []string{geth, alltools} { @@ -735,7 +754,11 @@ func doKeeperArchive(cmdline []string) { maybeSkipArchive(env) files := []string{"COPYING"} for _, target := range keeperTargets { - files = append(files, executablePath(fmt.Sprintf("keeper-%s", target.Name))) + targetOS := target.GOOS + if targetOS == "" { + targetOS = runtime.GOOS + } + files = append(files, executablePath(fmt.Sprintf("keeper-%s", target.Name), targetOS)) } if err := build.WriteArchive(keeper, files); err != nil { log.Fatal(err) @@ -745,8 +768,8 @@ func doKeeperArchive(cmdline []string) { } } -func archiveBasename(arch string, archiveVersion string) string { - platform := runtime.GOOS + "-" + arch +func archiveBasename(targetOS, arch, archiveVersion string) string { + platform := targetOS + "-" + arch if arch == "arm" { platform += os.Getenv("GOARM") } @@ -1209,13 +1232,13 @@ func doWindowsInstaller(cmdline []string) { env := build.Env() maybeSkipArchive(env) - // Aggregate binaries that are included in the installer + // Aggregate binaries that are included in the installer. var ( devTools []string allTools []string gethTool string ) - for _, file := range allToolsArchiveFiles { + for _, file := range allToolsArchiveFiles("windows") { if file == "COPYING" { // license, copied later continue } @@ -1252,16 +1275,24 @@ func doWindowsInstaller(cmdline []string) { if env.Commit != "" { ver[2] += "-" + env.Commit[:8] } - installer, err := filepath.Abs("geth-" + archiveBasename(*arch, version.Archive(env.Commit)) + ".exe") + installer, err := filepath.Abs("geth-" + archiveBasename("windows", *arch, version.Archive(env.Commit)) + ".exe") if err != nil { log.Fatalf("Failed to convert installer file path: %v", err) } - build.MustRunCommand("makensis.exe", - "/DOUTPUTFILE="+installer, - "/DMAJORVERSION="+ver[0], - "/DMINORVERSION="+ver[1], - "/DBUILDVERSION="+ver[2], - "/DARCH="+*arch, + // makensis on Windows is "makensis.exe" with /D-style defines; on Linux + // (and other Unixes) the binary is "makensis" and accepts -D. + makensisCmd := "makensis" + defineFlag := "-D" + if runtime.GOOS == "windows" { + makensisCmd = "makensis.exe" + defineFlag = "/D" + } + build.MustRunCommand(makensisCmd, + defineFlag+"OUTPUTFILE="+installer, + defineFlag+"MAJORVERSION="+ver[0], + defineFlag+"MINORVERSION="+ver[1], + defineFlag+"BUILDVERSION="+ver[2], + defineFlag+"ARCH="+*arch, filepath.Join(*workdir, "geth.nsi"), ) // Sign and publish installer. diff --git a/internal/build/gotool.go b/internal/build/gotool.go index 172fa13464..00aa9d6f02 100644 --- a/internal/build/gotool.go +++ b/internal/build/gotool.go @@ -41,12 +41,19 @@ type GoToolchain struct { func (g *GoToolchain) Go(command string, args ...string) *exec.Cmd { tool := g.goTool(command, args...) - // Configure environment for cross build. - if g.GOARCH != "" && g.GOARCH != runtime.GOARCH { + // Configure environment for cross build. Force CGO_ENABLED=1 whenever + // either GOOS or GOARCH differs from the host: Go's default is + // CGO_ENABLED=0 for any cross-compile, but geth's release builds rely + // on cgo (c-kzg-4844, secp256k1) regardless of which axis is crossing. + crossArch := g.GOARCH != "" && g.GOARCH != runtime.GOARCH + crossOS := g.GOOS != "" && g.GOOS != runtime.GOOS + if crossArch || crossOS { tool.Env = append(tool.Env, "CGO_ENABLED=1") + } + if crossArch { tool.Env = append(tool.Env, "GOARCH="+g.GOARCH) } - if g.GOOS != "" && g.GOOS != runtime.GOOS { + if crossOS { tool.Env = append(tool.Env, "GOOS="+g.GOOS) } // Configure C compiler. From 1f3989dc708fcba783eae5e10932b1102de608d8 Mon Sep 17 00:00:00 2001 From: cui Date: Sat, 9 May 2026 08:51:12 +0800 Subject: [PATCH 27/63] signer/core: avoid mutating the input (#34908) --- signer/core/signed_data.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/signer/core/signed_data.go b/signer/core/signed_data.go index c62b513145..d8b6ef0674 100644 --- a/signer/core/signed_data.go +++ b/signer/core/signed_data.go @@ -17,6 +17,7 @@ package core import ( + "bytes" "context" "encoding/json" "errors" @@ -309,7 +310,8 @@ func (api *SignerAPI) EcRecover(ctx context.Context, data hexutil.Bytes, sig hex if sig[64] != 27 && sig[64] != 28 { return common.Address{}, errors.New("invalid Ethereum signature (V is not 27 or 28)") } - sig[64] -= 27 // Transform yellow paper V from 27/28 to 0/1 + sig = bytes.Clone(sig) // Avoid mutating the input + sig[64] -= 27 // Transform yellow paper V from 27/28 to 0/1 hash := accounts.TextHash(data) rpk, err := crypto.SigToPub(hash, sig) if err != nil { From b927ff8b53c070e8b369324fd0f63ffa28aea83a Mon Sep 17 00:00:00 2001 From: cui Date: Sat, 9 May 2026 21:59:03 +0800 Subject: [PATCH 28/63] cmd/devp2p: fix typo in ENR IP printing code (#34909) --- cmd/devp2p/enrcmd.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/devp2p/enrcmd.go b/cmd/devp2p/enrcmd.go index c9b692612f..af2cf90a81 100644 --- a/cmd/devp2p/enrcmd.go +++ b/cmd/devp2p/enrcmd.go @@ -194,7 +194,7 @@ func formatAttrString(v rlp.RawValue) (string, bool) { func formatAttrIP(v rlp.RawValue) (string, bool) { content, _, err := rlp.SplitString(v) - if err != nil || len(content) != 4 && len(content) != 6 { + if err != nil || len(content) != 4 && len(content) != 16 { return "", false } return net.IP(content).String(), true From 2ca3a6447d5f0bbea325cd99acad6537d0de8e9b Mon Sep 17 00:00:00 2001 From: Richard Creighton Date: Sat, 9 May 2026 15:07:12 +0100 Subject: [PATCH 29/63] cmd/geth: respect --graphql=false (#34914) Passing `--graphql=false` currently still registers the GraphQL handler because the startup path checks whether the flag was set, not its boolean value. This switches the registration condition to use `ctx.Bool`, so explicit false disables GraphQL while the default behavior remains unchanged. --- cmd/geth/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/geth/config.go b/cmd/geth/config.go index 40458186f4..31f19e7a32 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -270,7 +270,7 @@ func makeFullNode(ctx *cli.Context) *node.Node { filterSystem := utils.RegisterFilterAPI(stack, backend, &cfg.Eth) // Configure GraphQL if requested. - if ctx.IsSet(utils.GraphQLEnabledFlag.Name) { + if ctx.Bool(utils.GraphQLEnabledFlag.Name) { utils.RegisterGraphQLService(stack, backend, filterSystem, &cfg.Node) } // Add the Ethereum Stats daemon if requested. From 2ba9be9c0efcb640860cfedc4a43b6f6b2d68c7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20Kj=C3=A6rstad?= Date: Sun, 10 May 2026 11:45:23 +0200 Subject: [PATCH 30/63] build: upgrade -dlgo version to Go 1.25.10 (#34911) New security fix: https://groups.google.com/g/golang-announce/c/qcCIEXso47M --- build/checksums.txt | 84 ++++++++++++++++++++++----------------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/build/checksums.txt b/build/checksums.txt index 1832ce41dd..454efa93c4 100644 --- a/build/checksums.txt +++ b/build/checksums.txt @@ -5,49 +5,49 @@ # https://github.com/ethereum/execution-spec-tests/releases/download/v5.1.0 a3192784375acec7eaec492799d5c5d0c47a2909a3cc40178898e4ecd20cc416 fixtures_develop.tar.gz -# version:golang 1.25.9 +# version:golang 1.25.10 # https://go.dev/dl/ -0ec9ef8ebcea097aac37decae9f09a7218b451cd96be7d6ed513d8e4bcf909cf go1.25.9.src.tar.gz -b9ede6378a8f8d3d22bf52e68beb69ef7abdb65929ab2456020383002da15846 go1.25.9.aix-ppc64.tar.gz -92cb78fba4796e218c1accb0ea0a214ef2094c382049a244ad6505505d015fbe go1.25.9.darwin-amd64.tar.gz -9528be7329b9770631a6bd09ca2f3a73ed7332bec01d87435e75e92d8f130363 go1.25.9.darwin-arm64.tar.gz -918e44a471c5524caa52f74185064240d5eb343aa8023d604776511fc7adffa6 go1.25.9.dragonfly-amd64.tar.gz -2d67dbdfd09c6fcaa0e64485367ef43b8837ea200c663d6417183237bcddf83d go1.25.9.freebsd-386.tar.gz -9152d0c0badbfeb0c0e148e47c12bec28099d8cf2db60958810c879e0b679d07 go1.25.9.freebsd-amd64.tar.gz -437dca59604ad4a806a6a88e3d7ec1cd98ac9b402a3671629f4e553dd8b9888f go1.25.9.freebsd-arm.tar.gz -4c0fe53977412036fc8081e8d0992bbaabe4d3e1926137271ba11c2f5753300f go1.25.9.freebsd-arm64.tar.gz -d6087cdd1c084bd186132f29e0d032852a745f3c7619003d0fd5612c1fa58c8a go1.25.9.freebsd-riscv64.tar.gz -f82e49037e195cb62beae6a6ad83497157b2af5a01bad2f1dcb65df41080aabb go1.25.9.illumos-amd64.tar.gz -1e14a73bc2b19e370e0d4c57ba87aabfe8aef1e435e14d246742d48a13254f36 go1.25.9.linux-386.tar.gz -00859d7bd6defe8bf84d9db9e57b9a4467b2887c18cd93ae7460e713db774bc1 go1.25.9.linux-amd64.tar.gz -ec342e7389b7f489564ed5463c63b16cf8040023dabc7861256677165a8c0e2b go1.25.9.linux-arm64.tar.gz -7d4f0d266d871301e08ef4ac31c56e66048688893b2848392e5c600276351ee8 go1.25.9.linux-armv6l.tar.gz -f3460d901a14496bc609636e4accf9110ee1869d41c64af7e29cd567cffcf49b go1.25.9.linux-loong64.tar.gz -1da96ea449382ff96c09c55cee74815324e01d687d5ac6d2ade58244b8574306 go1.25.9.linux-mips.tar.gz -311a7f5f01f9a4bd51288b575eb619dc8e28e1fbc0cd78256a428b3ca668ff01 go1.25.9.linux-mips64.tar.gz -0b4edaf9e2ba3f0a079547effda70ec6a4b51a6ca3271a1147652c87ebcf3735 go1.25.9.linux-mips64le.tar.gz -42667340df264896f20b12261429d954e736e9772ab83ba289e68c30cf6f9628 go1.25.9.linux-mipsle.tar.gz -b9cbb3a4894b5aca6966c23452608435e8535278ef019b18d8898fbbfab67e74 go1.25.9.linux-ppc64.tar.gz -b0c41c7da1fc8d39020d65296a0dc54167afd9f76d67064e22c31ce3d839a739 go1.25.9.linux-ppc64le.tar.gz -2a630be8f854177c13e5fa75f7812c721369ecb9bd6e4c0fb1bd1c708d08b37c go1.25.9.linux-riscv64.tar.gz -0cf55136ac7eaccfc36d849054f849510ea289c2d959ffbed7b3866b4f484d17 go1.25.9.linux-s390x.tar.gz -eaf8167ff10a6a3e5dd304ef5f2e020b3a7379e76fa1011dc49c895800bf367c go1.25.9.netbsd-386.tar.gz -3cc6a861e62e23feae660984e0f2f14a2efb5d1f655900afee1d51af98919ae4 go1.25.9.netbsd-amd64.tar.gz -c2c44dca10e882c30553f4aa2ab8f6722b670fb12882378c8f461a9105d40188 go1.25.9.netbsd-arm.tar.gz -f301b71a8ec448053a5d2597df2e178120204bc9a33266c81600dd5d020a61b4 go1.25.9.netbsd-arm64.tar.gz -c4543b7fdef9707b4896810c69b4160a43ecec210af45c300f3abd78aa0c9e72 go1.25.9.openbsd-386.tar.gz -37275325e314f5ab7cf8ae65c4efc7cbfdaf20b41c6849549739b57a3ac97544 go1.25.9.openbsd-amd64.tar.gz -f9c05b6b315e979ecdd47354dd287c01708d6a88dc6ae7af74c84df8fa00df94 go1.25.9.openbsd-arm.tar.gz -4e999f42cf959ff95ca84af1ea1db3771000f5e57e157904bc2ffc72c75e29a2 go1.25.9.openbsd-arm64.tar.gz -0c7fa6c7c2b1cc13ad32fa94fc31273b4adf39c1e0f0e5dcedac158ff526af3f go1.25.9.openbsd-ppc64.tar.gz -347b33953a4b6e8df17719296f360f60878fe48a2d482ceb3637a3dfd4950065 go1.25.9.openbsd-riscv64.tar.gz -889f77d567c06832e0d332fe2458653dc66d43cded7ddbca6f72ce0ca60029cc go1.25.9.plan9-386.tar.gz -978b1f931fadec2f2516237d2649ee845d93c8eaf47dd196cfd8d26c7b2706a1 go1.25.9.plan9-amd64.tar.gz -30b9565e5ad0a212fe00990ead700c751b416eb2ef8d7c91a204945a7ff83a48 go1.25.9.plan9-arm.tar.gz -9e9125ff84ab3c3522ec758cab9540a17e9cba12bfcc34b6bf556cb89b522591 go1.25.9.solaris-amd64.tar.gz -bf40515f5f4d834fa9ead31ff75581e61a38ac27bf49840b95c5c998d321c0f6 go1.25.9.windows-386.zip -a7a710e225467b34e9e09fb432b829c86c9b2da5821ee5418f7eb2e8ae1a22cc go1.25.9.windows-amd64.zip -33cd73cf1b3ceee655ef71bc96e94006c02ae3c617fdd67ac9be3dfae3957449 go1.25.9.windows-arm64.zip +20cf04a92e5af99748e341bc8996fa28090c9ac98765fa115ec5ddf41d7af41d go1.25.10.src.tar.gz +a194e767c2ab4216a60acc068b9dbe6bf4fae05c14bb52d6bbdcb5b3ea521308 go1.25.10.aix-ppc64.tar.gz +52321165a3146cd91865ef98371506a846ed4dc4f9f1c9323e5ad90d2a411e06 go1.25.10.darwin-amd64.tar.gz +795691a425de7e7cdba3544f354dcd2cebcf52e87dc6898193878f34eb6d634f go1.25.10.darwin-arm64.tar.gz +e37b4544ba9e9e9a7ab2ed3116b3fc4d39a88da854baa5a566d9d6d3a9de7d4c go1.25.10.dragonfly-amd64.tar.gz +2a70d1fdabab637aa442ca94599a56e381238efa20cb995d5433b8579bfe482c go1.25.10.freebsd-386.tar.gz +9cdf522d87d47d82fec4a313cc4f8c3c94a7770426e8d443e4150a1f330cba71 go1.25.10.freebsd-amd64.tar.gz +6da6183633e9e59ffd9edefab68b5059c89b605596d94aaba650b1681fccd35f go1.25.10.freebsd-arm.tar.gz +7adcefeebdd05331f4d45f1ad2dddb5c53537cff6552e82f6595b3b833b95371 go1.25.10.freebsd-arm64.tar.gz +285f80a1ace21a7d94035cd753196eeada8cacd48e6396fd116ad5eb67aea957 go1.25.10.freebsd-riscv64.tar.gz +de7461bf0e5068a4f6e7f8713026d70516be6dbd5de5d21f9ced1c182f2f326e go1.25.10.illumos-amd64.tar.gz +2f574f2e2e19ead5b280fec0e7af5c81b76632685f03b6ac42dfa34c4b773c52 go1.25.10.linux-386.tar.gz +42d4f7a32316aa66591eca7e89867256057a4264451aca10570a715b3637ba70 go1.25.10.linux-amd64.tar.gz +654da1f9b50a5d1c2a85ccf8ed405aa89c06e94d18384628bf186f7712677b08 go1.25.10.linux-arm64.tar.gz +39f168f158e693887d3ad006168af1b1a3007b19c5993cae4d9d57f82f52aaf8 go1.25.10.linux-armv6l.tar.gz +05401fe5ea50ad2bafb9c797ef9bf21574b0661f19ef4d0dd66af8a0fb7323f3 go1.25.10.linux-loong64.tar.gz +d5bc2d6155d394a3aae41f21eb7c60da5595a6147aa0f30ed6b27da25e06c3f7 go1.25.10.linux-mips.tar.gz +8c64e7493e5953c3ba3153487d2fddd7f8ed142392c77f138e6792a6c1930db4 go1.25.10.linux-mips64.tar.gz +bd53aa2d558b7c1eadfc6bf01132e1859203a92f458ed7ba75b7f3230f14b095 go1.25.10.linux-mips64le.tar.gz +120b254e2e2980bb06687175db5c4064a85696c53001dc9f59934ad18f74a6bc go1.25.10.linux-mipsle.tar.gz +8a6acb21295b0ec974a44608361920ea8dbff5666631a6f556bd7d5f1d56535f go1.25.10.linux-ppc64.tar.gz +778925fdcdf9a272f823d147fad51545c3334b7ccd8652b2ccaaf2b01800280a go1.25.10.linux-ppc64le.tar.gz +b4f04ad0db48bcfea946db5323919cd21034e0bd2821a557dacd29c1b1013a4b go1.25.10.linux-riscv64.tar.gz +936b953e43921a64c12da871f76871ebbeb6d2092a7b8bdc307f5246f3c662cc go1.25.10.linux-s390x.tar.gz +061470e0bc7132146a5925a3cc28d5bc498eb1b1ff09dedcfaae10f781ff2274 go1.25.10.netbsd-386.tar.gz +63b2d50d7f8f269a9c82d42a4060e90cffb7f9102299818bb071b067aac8da8f go1.25.10.netbsd-amd64.tar.gz +c35129f68796526aa4dc4b6f481e2d995ef312aedadc88b659b945cc00e1f8f0 go1.25.10.netbsd-arm.tar.gz +2f541da4e2b298154d992d1f11bbb38c89d0821d91cc50a46776d42bb5e63bca go1.25.10.netbsd-arm64.tar.gz +2d42e569b07f1b99fdbfd008e7c22f967d165e2ce02464f46818fbed2aec43f5 go1.25.10.openbsd-386.tar.gz +0ad05960e8c9f867328151308c87f938433bec8f22f6a9437a896e22169fc840 go1.25.10.openbsd-amd64.tar.gz +099cc11473f99461c77161912740945308f08f6834980afb262c72bdc915f2d7 go1.25.10.openbsd-arm.tar.gz +bdf3335d5008c1ddc81fa94892283e4f1fee22566f5351d4e726d9f55a67c838 go1.25.10.openbsd-arm64.tar.gz +0933d418da0a61e0f29de717a77498f16b9b5b50dbe2205e20b2ed7fd4067f75 go1.25.10.openbsd-ppc64.tar.gz +191e6f3e75712f8c13d189d53b668e2cac6449f26474c1d86fbd04f6e9846f9c go1.25.10.openbsd-riscv64.tar.gz +68c053c8acd76c50fc430e92f4a86110ec3d97dd03d27b9339b4eaf793caff5f go1.25.10.plan9-386.tar.gz +42e2c46638ae22d93402e79efb40faee5c42cf7c56a01bb3ab47c6bb2512b745 go1.25.10.plan9-amd64.tar.gz +3ef1d5838b1648da16724a07b72e839ccbd7cb8899c3e0426afd6b79d494b91c go1.25.10.plan9-arm.tar.gz +631e3716017fbec06500a628d97e1155daec3593f0a7812c2ebfe8fc8c96b2ab go1.25.10.solaris-amd64.tar.gz +ddc693d2d9d7cc671ebb72d1d50aa05670f95b059b7d90440611af57976871d5 go1.25.10.windows-386.zip +ca37af2dadd8544464f1a9ca7c3886499d1cdfcb263855d0a1d71f194b2bd222 go1.25.10.windows-amd64.zip +38be57e0398bd93673d65bcae6dc7ee3cf151d7038d0dba5c60a5153022872da go1.25.10.windows-arm64.zip # version:golangci 2.10.1 # https://github.com/golangci/golangci-lint/releases/ From 8581125a21ee4592631abe127749d6ca3f1020fa Mon Sep 17 00:00:00 2001 From: vickkkkkyy Date: Sun, 10 May 2026 17:49:17 +0800 Subject: [PATCH 31/63] crypto: add hash length check in nocgo VerifySignature (#33839) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I was tracing a signature verification issue in a nocgo build and found that `VerifySignature` doesn't validate hash length. #33104 added the check to `Sign` and `sigToPub` but missed this one. The cgo path in `secp256k1/secp256.go` already rejects non-32-byte hashes, so the nocgo path should do the same — otherwise a wrong-length hash gets passed to decred's `Verify` and silently gives a bogus result. --- crypto/signature_nocgo.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto/signature_nocgo.go b/crypto/signature_nocgo.go index 0aab7180d3..bf273612e9 100644 --- a/crypto/signature_nocgo.go +++ b/crypto/signature_nocgo.go @@ -103,7 +103,7 @@ func Sign(hash []byte, prv *ecdsa.PrivateKey) ([]byte, error) { // The public key should be in compressed (33 bytes) or uncompressed (65 bytes) format. // The signature should have the 64 byte [R || S] format. func VerifySignature(pubkey, hash, signature []byte) bool { - if len(signature) != 64 { + if len(signature) != 64 || len(hash) != DigestLength { return false } var r, s secp256k1.ModNScalar From bcb68d23b3e78855be7d825ede37815d83b3142b Mon Sep 17 00:00:00 2001 From: cui Date: Sun, 10 May 2026 19:02:46 +0800 Subject: [PATCH 32/63] p2p: handle return false from TCPEndpoint (#34916) --- p2p/dial.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/p2p/dial.go b/p2p/dial.go index f9463d6d89..0ffcd10497 100644 --- a/p2p/dial.go +++ b/p2p/dial.go @@ -67,7 +67,10 @@ type tcpDialer struct { } func (t tcpDialer) Dial(ctx context.Context, dest *enode.Node) (net.Conn, error) { - addr, _ := dest.TCPEndpoint() + addr, ok := dest.TCPEndpoint() + if !ok { + return nil, errNoPort + } return t.d.DialContext(ctx, "tcp", addr.String()) } From 7facf9c1292d3783cb079a16ba797def4738e1c8 Mon Sep 17 00:00:00 2001 From: cui Date: Sun, 10 May 2026 19:03:57 +0800 Subject: [PATCH 33/63] core/txpool: use cmp.Compare instead of subtraction (#34918) This fixes a theoretical overflow condition if an account has an impossibly high nonce. --- core/txpool/locals/tx_tracker.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/txpool/locals/tx_tracker.go b/core/txpool/locals/tx_tracker.go index bb178f175e..66f3248105 100644 --- a/core/txpool/locals/tx_tracker.go +++ b/core/txpool/locals/tx_tracker.go @@ -18,6 +18,7 @@ package locals import ( + "cmp" "slices" "sync" "time" @@ -151,7 +152,7 @@ func (tracker *TxTracker) recheck(journalCheck bool) []*types.Transaction { for _, list := range rejournal { // cmp(a, b) should return a negative number when a < b, slices.SortFunc(list, func(a, b *types.Transaction) int { - return int(a.Nonce() - b.Nonce()) + return cmp.Compare(a.Nonce(), b.Nonce()) }) } // Rejournal the tracker while holding the lock. No new transactions will From f63c26509298ecfdf7e50d7b08f96809b2235ddd Mon Sep 17 00:00:00 2001 From: rayoo Date: Sun, 10 May 2026 19:43:40 +0800 Subject: [PATCH 34/63] internal/download: close dst on io.Copy error (#34910) --- internal/download/download.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/download/download.go b/internal/download/download.go index 27d3732731..94517166f5 100644 --- a/internal/download/download.go +++ b/internal/download/download.go @@ -212,6 +212,7 @@ func (db *ChecksumDB) DownloadFile(url, dstPath string) error { dst = newDownloadWriter(fd, resp.ContentLength) } if _, err = io.Copy(dst, resp.Body); err != nil { + dst.Close() os.Remove(tmpfile) return err } From 18becee8cb01f6d35224a6bac3e45d49b2074857 Mon Sep 17 00:00:00 2001 From: Marius van der Wijden Date: Sun, 10 May 2026 22:54:57 +0200 Subject: [PATCH 35/63] appveyor.yml: remove appveyor configuration (#34720) Removes the appveyor.yml since we moved to github runners. --------- Co-authored-by: Sina Mahmoodi Co-authored-by: Felix Lange --- appveyor.yml | 39 --------------------------------------- 1 file changed, 39 deletions(-) delete mode 100644 appveyor.yml diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index aeafcfc838..0000000000 --- a/appveyor.yml +++ /dev/null @@ -1,39 +0,0 @@ -clone_depth: 5 -version: "{branch}.{build}" - -image: - - Visual Studio 2019 - -environment: - matrix: - - GETH_ARCH: amd64 - GETH_MINGW: 'C:\msys64\mingw64' - - GETH_ARCH: 386 - GETH_MINGW: 'C:\msys64\mingw32' - -install: - - git submodule update --init --depth 1 --recursive - - go version - -for: - # Windows builds for amd64 + 386. - - matrix: - only: - - image: Visual Studio 2019 - environment: - # We use gcc from MSYS2 because it is the most recent compiler version available on - # AppVeyor. Note: gcc.exe only works properly if the corresponding bin/ directory is - # contained in PATH. - GETH_CC: '%GETH_MINGW%\bin\gcc.exe' - PATH: '%GETH_MINGW%\bin;C:\Program Files (x86)\NSIS\;%PATH%' - build_script: - - 'echo %GETH_ARCH%' - - 'echo %GETH_CC%' - - '%GETH_CC% --version' - - go run build/ci.go install -dlgo -arch %GETH_ARCH% -cc %GETH_CC% - after_build: - # Upload builds. Note that ci.go makes this a no-op PR builds. - - go run build/ci.go archive -arch %GETH_ARCH% -type zip -signer WINDOWS_SIGNING_KEY -upload gethstore/builds - - go run build/ci.go nsis -arch %GETH_ARCH% -signer WINDOWS_SIGNING_KEY -upload gethstore/builds - test_script: - - go run build/ci.go test -dlgo -arch %GETH_ARCH% -cc %GETH_CC% -short From 934a0091fa66b474832b3664854e979fe8bf76b2 Mon Sep 17 00:00:00 2001 From: rayoo Date: Mon, 11 May 2026 10:23:58 +0800 Subject: [PATCH 36/63] triedb/pathdb: fix layer 5 key range in storage iterator traversal test (#34883) --- triedb/pathdb/iterator_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/triedb/pathdb/iterator_test.go b/triedb/pathdb/iterator_test.go index 2197e85272..191c2fadf5 100644 --- a/triedb/pathdb/iterator_test.go +++ b/triedb/pathdb/iterator_test.go @@ -489,7 +489,7 @@ func TestStorageIteratorTraversalValues(t *testing.T) { if i%8 == 0 { e[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 4, i) } - if i > 50 || i < 85 { + if i > 50 && i < 85 { f[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 5, i) } if i%64 == 0 { From 2f11dccca00bc66b68f348193e98dee4b6e2206d Mon Sep 17 00:00:00 2001 From: Richard Creighton Date: Mon, 11 May 2026 15:08:55 +0100 Subject: [PATCH 37/63] cmd/geth: respect --dev=false (#34920) Passing `--dev=false` currently still enters the dev-mode startup path because a couple of branches check whether the flag was set, not its boolean value. This switches those branches to use `ctx.Bool`, so explicit false does not start dev mode or emit a dev genesis, while `--dev` keeps its existing behavior. --- cmd/geth/chaincmd.go | 2 +- cmd/geth/config.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go index 0aacb0878a..98ed348d8c 100644 --- a/cmd/geth/chaincmd.go +++ b/cmd/geth/chaincmd.go @@ -325,7 +325,7 @@ func dumpGenesis(ctx *cli.Context) error { var genesis *core.Genesis if utils.IsNetworkPreset(ctx) { genesis = utils.MakeGenesis(ctx) - } else if ctx.IsSet(utils.DeveloperFlag.Name) && !ctx.IsSet(utils.DataDirFlag.Name) { + } else if ctx.Bool(utils.DeveloperFlag.Name) && !ctx.IsSet(utils.DataDirFlag.Name) { genesis = core.DeveloperGenesisBlock(11_500_000, nil) } diff --git a/cmd/geth/config.go b/cmd/geth/config.go index 31f19e7a32..c02e307bdc 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -291,7 +291,7 @@ func makeFullNode(ctx *cli.Context) *node.Node { } utils.RegisterSyncOverrideService(stack, eth, syncConfig) - if ctx.IsSet(utils.DeveloperFlag.Name) { + if ctx.Bool(utils.DeveloperFlag.Name) { // Start dev mode. simBeacon, err := catalyst.NewSimulatedBeacon(ctx.Uint64(utils.DeveloperPeriodFlag.Name), cfg.Eth.Miner.PendingFeeRecipient, eth) if err != nil { From e1047b9c8489ed2e26845498b58e3e30dad66f1c Mon Sep 17 00:00:00 2001 From: Marius van der Wijden Date: Mon, 11 May 2026 16:25:57 +0200 Subject: [PATCH 38/63] core: use uint256 in core.Message (#34934) Changes core.Message to use Uint256 which is faster --------- Co-authored-by: Gary Rong --- common/hexutil/json.go | 4 + core/evm.go | 2 +- core/state_processor.go | 19 ++-- core/state_transition.go | 151 ++++++++++++++++++---------- eth/gasestimator/gasestimator.go | 16 +-- internal/ethapi/transaction_args.go | 24 +++-- tests/state_test.go | 3 +- tests/state_test_util.go | 10 +- 8 files changed, 142 insertions(+), 87 deletions(-) diff --git a/common/hexutil/json.go b/common/hexutil/json.go index 6b9f412078..c00cd879c8 100644 --- a/common/hexutil/json.go +++ b/common/hexutil/json.go @@ -204,6 +204,10 @@ func (b *Big) ToInt() *big.Int { return (*big.Int)(b) } +func (b *Big) ToUint256() (*uint256.Int, bool) { + return uint256.FromBig((*big.Int)(b)) +} + // String returns the hex encoding of b. func (b *Big) String() string { return EncodeBig(b.ToInt()) diff --git a/core/evm.go b/core/evm.go index 818b23bee5..73e4c01a99 100644 --- a/core/evm.go +++ b/core/evm.go @@ -87,7 +87,7 @@ func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common func NewEVMTxContext(msg *Message) vm.TxContext { ctx := vm.TxContext{ Origin: msg.From, - GasPrice: uint256.MustFromBig(msg.GasPrice), + GasPrice: msg.GasPrice, BlobHashes: msg.BlobHashes, } return ctx diff --git a/core/state_processor.go b/core/state_processor.go index 54ebbd047b..9dcb4cf07c 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -32,6 +32,7 @@ import ( "github.com/ethereum/go-ethereum/internal/telemetry" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/trie" + "github.com/holiman/uint256" ) // StateProcessor is a basic Processor, which takes care of transitioning @@ -254,9 +255,9 @@ func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM) { msg := &Message{ From: params.SystemAddress, GasLimit: 30_000_000, - GasPrice: common.Big0, - GasFeeCap: common.Big0, - GasTipCap: common.Big0, + GasPrice: uint256.NewInt(0), + GasFeeCap: uint256.NewInt(0), + GasTipCap: uint256.NewInt(0), To: ¶ms.BeaconRootsAddress, Data: beaconRoot[:], } @@ -281,9 +282,9 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM) { msg := &Message{ From: params.SystemAddress, GasLimit: 30_000_000, - GasPrice: common.Big0, - GasFeeCap: common.Big0, - GasTipCap: common.Big0, + GasPrice: uint256.NewInt(0), + GasFeeCap: uint256.NewInt(0), + GasTipCap: uint256.NewInt(0), To: ¶ms.HistoryStorageAddress, Data: prevHash.Bytes(), } @@ -321,9 +322,9 @@ func processRequestsSystemCall(requests *[][]byte, evm *vm.EVM, requestType byte msg := &Message{ From: params.SystemAddress, GasLimit: 30_000_000, - GasPrice: common.Big0, - GasFeeCap: common.Big0, - GasTipCap: common.Big0, + GasPrice: uint256.NewInt(0), + GasFeeCap: uint256.NewInt(0), + GasTipCap: uint256.NewInt(0), To: &addr, } evm.SetTxContext(NewEVMTxContext(msg)) diff --git a/core/state_transition.go b/core/state_transition.go index b5b8b22155..fcd483eeb7 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -210,14 +210,14 @@ type Message struct { To *common.Address From common.Address Nonce uint64 - Value *big.Int + Value *uint256.Int GasLimit uint64 - GasPrice *big.Int - GasFeeCap *big.Int - GasTipCap *big.Int + GasPrice *uint256.Int + GasFeeCap *uint256.Int + GasTipCap *uint256.Int Data []byte AccessList types.AccessList - BlobGasFeeCap *big.Int + BlobGasFeeCap *uint256.Int BlobHashes []common.Hash SetCodeAuthorizations []types.SetCodeAuthorization @@ -238,32 +238,64 @@ type Message struct { // TransactionToMessage converts a transaction into a Message. func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee *big.Int) (*Message, error) { + from, err := types.Sender(s, tx) + if err != nil { + return nil, err + } + gasPrice, overflow := uint256.FromBig(tx.GasPrice()) + if overflow { + return nil, fmt.Errorf("%w: address %v, maxFeePerGas bit length: %d", ErrFeeCapVeryHigh, + from.Hex(), tx.GasPrice().BitLen()) + } + txGasFeeCap := tx.GasFeeCap() + gasFeeCap, overflow := uint256.FromBig(txGasFeeCap) + if overflow { + return nil, fmt.Errorf("%w: address %v, maxFeePerGas bit length: %d", ErrFeeCapVeryHigh, + from.Hex(), tx.GasFeeCap().BitLen()) + } + txGasTipCap := tx.GasTipCap() + gasTipCap, overflow := uint256.FromBig(txGasTipCap) + if overflow { + return nil, fmt.Errorf("%w: address %v, maxPriorityFeePerGas bit length: %d", ErrTipVeryHigh, + from.Hex(), tx.GasTipCap().BitLen()) + } + value, overflow := uint256.FromBig(tx.Value()) + if overflow { + return nil, fmt.Errorf("value exceeds 256 bits: address %v", from.Hex()) + } + blobGasFeeCap, overflow := uint256.FromBig(tx.BlobGasFeeCap()) + if overflow { + return nil, fmt.Errorf("blobGasFeeCap exceeds 256 bits: address %v", from.Hex()) + } + msg := &Message{ + From: from, Nonce: tx.Nonce(), GasLimit: tx.Gas(), - GasPrice: tx.GasPrice(), - GasFeeCap: tx.GasFeeCap(), - GasTipCap: tx.GasTipCap(), + GasPrice: gasPrice, + GasFeeCap: gasFeeCap, + GasTipCap: gasTipCap, To: tx.To(), - Value: tx.Value(), + Value: value, Data: tx.Data(), AccessList: tx.AccessList(), SetCodeAuthorizations: tx.SetCodeAuthorizations(), SkipNonceChecks: false, SkipTransactionChecks: false, BlobHashes: tx.BlobHashes(), - BlobGasFeeCap: tx.BlobGasFeeCap(), + BlobGasFeeCap: blobGasFeeCap, } // If baseFee provided, set gasPrice to effectiveGasPrice. if baseFee != nil { - msg.GasPrice = msg.GasPrice.Add(msg.GasTipCap, baseFee) - if msg.GasPrice.Cmp(msg.GasFeeCap) > 0 { - msg.GasPrice = msg.GasFeeCap + effectiveGasPrice := new(big.Int).Add(baseFee, txGasTipCap) + if effectiveGasPrice.Cmp(txGasFeeCap) > 0 { + effectiveGasPrice = txGasFeeCap } + // EffectiveGasPrice is already capped by txGasFeeCap, therefore + // the overflow check is not required. + msg.GasPrice = uint256.MustFromBig(effectiveGasPrice) } - var err error - msg.From, err = types.Sender(s, tx) - return msg, err + return msg, nil } // ApplyMessage computes the new state by applying the given message @@ -333,32 +365,55 @@ func (st *stateTransition) to() common.Address { } func (st *stateTransition) buyGas() error { - mgval := new(big.Int).SetUint64(st.msg.GasLimit) - mgval.Mul(mgval, st.msg.GasPrice) - balanceCheck := new(big.Int).Set(mgval) + mgval := new(uint256.Int).SetUint64(st.msg.GasLimit) + _, overflow := mgval.MulOverflow(mgval, st.msg.GasPrice) + if overflow { + return fmt.Errorf("%w: address %v required balance exceeds 256 bits", ErrInsufficientFunds, st.msg.From.Hex()) + } + balanceCheck := new(uint256.Int).Set(mgval) if st.msg.GasFeeCap != nil { balanceCheck.SetUint64(st.msg.GasLimit) - balanceCheck = balanceCheck.Mul(balanceCheck, st.msg.GasFeeCap) + if _, overflow := balanceCheck.MulOverflow(balanceCheck, st.msg.GasFeeCap); overflow { + return fmt.Errorf("%w: address %v required balance exceeds 256 bits", ErrInsufficientFunds, st.msg.From.Hex()) + } + } + if st.msg.Value != nil { + if _, overflow := balanceCheck.AddOverflow(balanceCheck, st.msg.Value); overflow { + return fmt.Errorf("%w: address %v required balance exceeds 256 bits", ErrInsufficientFunds, st.msg.From.Hex()) + } } - balanceCheck.Add(balanceCheck, st.msg.Value) if st.evm.ChainConfig().IsCancun(st.evm.Context.BlockNumber, st.evm.Context.Time) { if blobGas := st.blobGasUsed(); blobGas > 0 { // Check that the user has enough funds to cover blobGasUsed * tx.BlobGasFeeCap - blobBalanceCheck := new(big.Int).SetUint64(blobGas) - blobBalanceCheck.Mul(blobBalanceCheck, st.msg.BlobGasFeeCap) - balanceCheck.Add(balanceCheck, blobBalanceCheck) + blobBalanceCheck := new(uint256.Int).SetUint64(blobGas) + if _, overflow := blobBalanceCheck.MulOverflow(blobBalanceCheck, st.msg.BlobGasFeeCap); overflow { + return fmt.Errorf("%w: address %v required balance exceeds 256 bits", ErrInsufficientFunds, st.msg.From.Hex()) + } + if _, overflow := balanceCheck.AddOverflow(balanceCheck, blobBalanceCheck); overflow { + return fmt.Errorf("%w: address %v required balance exceeds 256 bits", ErrInsufficientFunds, st.msg.From.Hex()) + } // Pay for blobGasUsed * actual blob fee - blobFee := new(big.Int).SetUint64(blobGas) - blobFee.Mul(blobFee, st.evm.Context.BlobBaseFee) - mgval.Add(mgval, blobFee) + blobBaseFee, overflow := uint256.FromBig(st.evm.Context.BlobBaseFee) + if overflow { + return fmt.Errorf("invalid blobBaseFee: %v", st.evm.Context.BlobBaseFee) + } + blobFee := new(uint256.Int).SetUint64(blobGas) + + // In practice, overflow checking is unnecessary, as blobBaseFee cannot exceed + // BlobGasFeeCap. However, in eth_call it is still possible for users to specify + // an excessively large blob base fee and bypass the blob base fee validation. + _, overflow = blobFee.MulOverflow(blobFee, blobBaseFee) + if overflow { + return fmt.Errorf("%w: address %v required balance exceeds 256 bits", ErrInsufficientFunds, st.msg.From.Hex()) + } + _, overflow = mgval.AddOverflow(mgval, blobFee) + if overflow { + return fmt.Errorf("%w: address %v required balance exceeds 256 bits", ErrInsufficientFunds, st.msg.From.Hex()) + } } } - balanceCheckU256, overflow := uint256.FromBig(balanceCheck) - if overflow { - return fmt.Errorf("%w: address %v required balance exceeds 256 bits", ErrInsufficientFunds, st.msg.From.Hex()) - } - if have, want := st.state.GetBalance(st.msg.From), balanceCheckU256; have.Cmp(want) < 0 { + if have, want := st.state.GetBalance(st.msg.From), balanceCheck; have.Cmp(want) < 0 { return fmt.Errorf("%w: address %v have %v want %v", ErrInsufficientFunds, st.msg.From.Hex(), have, want) } if err := st.gp.SubGas(st.msg.GasLimit); err != nil { @@ -371,8 +426,7 @@ func (st *stateTransition) buyGas() error { st.gasRemaining = vm.NewGasBudget(st.msg.GasLimit) st.initialBudget = st.gasRemaining.Copy() - mgvalU256, _ := uint256.FromBig(mgval) - st.state.SubBalance(st.msg.From, mgvalU256, tracing.BalanceDecreaseGasBuy) + st.state.SubBalance(st.msg.From, mgval, tracing.BalanceDecreaseGasBuy) return nil } @@ -412,21 +466,13 @@ func (st *stateTransition) preCheck() error { // Skip the checks if gas fields are zero and baseFee was explicitly disabled (eth_call) skipCheck := st.evm.Config.NoBaseFee && msg.GasFeeCap.BitLen() == 0 && msg.GasTipCap.BitLen() == 0 if !skipCheck { - if l := msg.GasFeeCap.BitLen(); l > 256 { - return fmt.Errorf("%w: address %v, maxFeePerGas bit length: %d", ErrFeeCapVeryHigh, - msg.From.Hex(), l) - } - if l := msg.GasTipCap.BitLen(); l > 256 { - return fmt.Errorf("%w: address %v, maxPriorityFeePerGas bit length: %d", ErrTipVeryHigh, - msg.From.Hex(), l) - } if msg.GasFeeCap.Cmp(msg.GasTipCap) < 0 { return fmt.Errorf("%w: address %v, maxPriorityFeePerGas: %s, maxFeePerGas: %s", ErrTipAboveFeeCap, msg.From.Hex(), msg.GasTipCap, msg.GasFeeCap) } // This will panic if baseFee is nil, but basefee presence is verified // as part of header validation. - if msg.GasFeeCap.Cmp(st.evm.Context.BaseFee) < 0 { + if msg.GasFeeCap.CmpBig(st.evm.Context.BaseFee) < 0 { return fmt.Errorf("%w: address %v, maxFeePerGas: %s, baseFee: %s", ErrFeeCapTooLow, msg.From.Hex(), msg.GasFeeCap, st.evm.Context.BaseFee) } @@ -460,7 +506,7 @@ func (st *stateTransition) preCheck() error { if !skipCheck { // This will panic if blobBaseFee is nil, but blobBaseFee presence // is verified as part of header validation. - if msg.BlobGasFeeCap.Cmp(st.evm.Context.BlobBaseFee) < 0 { + if msg.BlobGasFeeCap.CmpBig(st.evm.Context.BlobBaseFee) < 0 { return fmt.Errorf("%w: address %v blobGasFeeCap: %v, blobBaseFee: %v", ErrBlobFeeCapTooLow, msg.From.Hex(), msg.BlobGasFeeCap, st.evm.Context.BlobBaseFee) } @@ -543,9 +589,9 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { } // Check clause 6 - value, overflow := uint256.FromBig(msg.Value) - if overflow { - return nil, fmt.Errorf("%w: address %v", ErrInsufficientFundsForTransfer, msg.From.Hex()) + value := msg.Value + if value == nil { + value = new(uint256.Int) } if !value.IsZero() && !st.evm.Context.CanTransfer(st.state, msg.From, value) { return nil, fmt.Errorf("%w: address %v", ErrInsufficientFundsForTransfer, msg.From.Hex()) @@ -629,9 +675,12 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { } effectiveTip := msg.GasPrice if rules.IsLondon { - effectiveTip = new(big.Int).Sub(msg.GasPrice, st.evm.Context.BaseFee) + baseFee, overflow := uint256.FromBig(st.evm.Context.BaseFee) + if overflow { + return nil, fmt.Errorf("invalid baseFee: %v", st.evm.Context.BaseFee) + } + effectiveTip = new(uint256.Int).Sub(msg.GasPrice, baseFee) } - effectiveTipU256, _ := uint256.FromBig(effectiveTip) if st.evm.Config.NoBaseFee && msg.GasFeeCap.Sign() == 0 && msg.GasTipCap.Sign() == 0 { // Skip fee payment when NoBaseFee is set and the fee fields @@ -639,7 +688,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { // the coinbase when simulating calls. } else { fee := new(uint256.Int).SetUint64(st.gasUsed()) - fee.Mul(fee, effectiveTipU256) + fee.Mul(fee, effectiveTip) st.state.AddBalance(st.evm.Context.Coinbase, fee, tracing.BalanceIncreaseRewardTransactionFee) // add the coinbase to the witness iff the fee is greater than 0 @@ -741,7 +790,7 @@ func (st *stateTransition) calcRefund() vm.GasBudget { // exchanged at the original rate. func (st *stateTransition) returnGas() { remaining := uint256.NewInt(st.gasRemaining.RegularGas) - remaining.Mul(remaining, uint256.MustFromBig(st.msg.GasPrice)) + remaining.Mul(remaining, st.msg.GasPrice) st.state.AddBalance(st.msg.From, remaining, tracing.BalanceIncreaseGasReturn) if st.evm.Config.Tracer != nil && st.evm.Config.Tracer.OnGasChange != nil && st.gasRemaining.RegularGas > 0 { diff --git a/eth/gasestimator/gasestimator.go b/eth/gasestimator/gasestimator.go index ace0752037..f45fc0d8c9 100644 --- a/eth/gasestimator/gasestimator.go +++ b/eth/gasestimator/gasestimator.go @@ -22,13 +22,13 @@ import ( "fmt" "math/big" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" ) // Options are the contextual parameters to execute the requested call. @@ -70,17 +70,17 @@ func Estimate(ctx context.Context, call *core.Message, opts *Options, gasCap uin } // Normalize the max fee per gas the call is willing to spend. - var feeCap *big.Int + var feeCap *uint256.Int if call.GasFeeCap != nil { feeCap = call.GasFeeCap } else if call.GasPrice != nil { feeCap = call.GasPrice } else { - feeCap = common.Big0 + feeCap = uint256.NewInt(0) } // Recap the highest gas limit with account's available balance. if feeCap.BitLen() != 0 { - balance := opts.State.GetBalance(call.From).ToBig() + balance := opts.State.GetBalance(call.From).Clone() available := balance if call.Value != nil { @@ -90,8 +90,8 @@ func Estimate(ctx context.Context, call *core.Message, opts *Options, gasCap uin available.Sub(available, call.Value) } if opts.Config.IsCancun(opts.Header.Number, opts.Header.Time) && len(call.BlobHashes) > 0 { - blobGasPerBlob := new(big.Int).SetInt64(params.BlobTxBlobGasPerBlob) - blobBalanceUsage := new(big.Int).SetInt64(int64(len(call.BlobHashes))) + blobGasPerBlob := uint256.NewInt(params.BlobTxBlobGasPerBlob) + blobBalanceUsage := uint256.NewInt(uint64(len(call.BlobHashes))) blobBalanceUsage.Mul(blobBalanceUsage, blobGasPerBlob) blobBalanceUsage.Mul(blobBalanceUsage, call.BlobGasFeeCap) if blobBalanceUsage.Cmp(available) >= 0 { @@ -99,13 +99,13 @@ func Estimate(ctx context.Context, call *core.Message, opts *Options, gasCap uin } available.Sub(available, blobBalanceUsage) } - allowance := new(big.Int).Div(available, feeCap) + allowance := new(uint256.Int).Div(available, feeCap) // If the allowance is larger than maximum uint64, skip checking if allowance.IsUint64() && hi > allowance.Uint64() { transfer := call.Value if transfer == nil { - transfer = new(big.Int) + transfer = new(uint256.Int) } log.Debug("Gas estimation capped by limited funds", "original", hi, "balance", balance, "sent", transfer, "maxFeePerGas", feeCap, "fundable", allowance) diff --git a/internal/ethapi/transaction_args.go b/internal/ethapi/transaction_args.go index 4fb30e6289..1032d067f1 100644 --- a/internal/ethapi/transaction_args.go +++ b/internal/ethapi/transaction_args.go @@ -446,27 +446,27 @@ func (args *TransactionArgs) CallDefaults(globalGasCap uint64, baseFee *big.Int, // Assumes that fields are not nil, i.e. setDefaults or CallDefaults has been called. func (args *TransactionArgs) ToMessage(baseFee *big.Int, skipNonceCheck bool) *core.Message { var ( - gasPrice *big.Int - gasFeeCap *big.Int - gasTipCap *big.Int + gasPrice *uint256.Int + gasFeeCap *uint256.Int + gasTipCap *uint256.Int ) if baseFee == nil { - gasPrice = args.GasPrice.ToInt() + gasPrice, _ = args.GasPrice.ToUint256() gasFeeCap, gasTipCap = gasPrice, gasPrice } else { // A basefee is provided, necessitating 1559-type execution if args.GasPrice != nil { // User specified the legacy gas field, convert to 1559 gas typing - gasPrice = args.GasPrice.ToInt() + gasPrice, _ = args.GasPrice.ToUint256() gasFeeCap, gasTipCap = gasPrice, gasPrice } else { // User specified 1559 gas fields (or none), use those - gasFeeCap = args.MaxFeePerGas.ToInt() - gasTipCap = args.MaxPriorityFeePerGas.ToInt() + gasFeeCap, _ = args.MaxFeePerGas.ToUint256() + gasTipCap, _ = args.MaxPriorityFeePerGas.ToUint256() // Backfill the legacy gasPrice for EVM execution, unless we're all zeroes - gasPrice = new(big.Int) + gasPrice = uint256.NewInt(0) if gasFeeCap.BitLen() > 0 || gasTipCap.BitLen() > 0 { - gasPrice = gasPrice.Add(gasTipCap, baseFee) + gasPrice = gasPrice.Add(gasTipCap, uint256.MustFromBig(baseFee)) if gasPrice.Cmp(gasFeeCap) > 0 { gasPrice = gasFeeCap } @@ -477,10 +477,12 @@ func (args *TransactionArgs) ToMessage(baseFee *big.Int, skipNonceCheck bool) *c if args.AccessList != nil { accessList = *args.AccessList } + value, _ := args.Value.ToUint256() + blobFeeCap, _ := args.BlobFeeCap.ToUint256() return &core.Message{ From: args.from(), To: args.To, - Value: (*big.Int)(args.Value), + Value: value, Nonce: uint64(*args.Nonce), GasLimit: uint64(*args.Gas), GasPrice: gasPrice, @@ -488,7 +490,7 @@ func (args *TransactionArgs) ToMessage(baseFee *big.Int, skipNonceCheck bool) *c GasTipCap: gasTipCap, Data: args.data(), AccessList: accessList, - BlobGasFeeCap: (*big.Int)(args.BlobFeeCap), + BlobGasFeeCap: blobFeeCap, BlobHashes: args.BlobHashes, SetCodeAuthorizations: args.AuthorizationList, SkipNonceChecks: skipNonceCheck, diff --git a/tests/state_test.go b/tests/state_test.go index 8444d211cf..cf1d4bce4c 100644 --- a/tests/state_test.go +++ b/tests/state_test.go @@ -35,7 +35,6 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/eth/tracers/logger" - "github.com/holiman/uint256" ) func initMatcher(st *testMatcher) { @@ -329,7 +328,7 @@ func runBenchmark(b *testing.B, t *StateTest) { initialGas := vm.NewGasBudget(msg.GasLimit) // Execute the message. - _, leftOverGas, err := evm.Call(sender.Address(), *msg.To, msg.Data, initialGas.Copy(), uint256.MustFromBig(msg.Value)) + _, leftOverGas, err := evm.Call(sender.Address(), *msg.To, msg.Data, initialGas.Copy(), msg.Value) if err != nil { b.Error(err) return diff --git a/tests/state_test_util.go b/tests/state_test_util.go index f7cf1c0697..e33e15fc8c 100644 --- a/tests/state_test_util.go +++ b/tests/state_test_util.go @@ -479,15 +479,15 @@ func (tx *stTransaction) toMessage(ps stPostState, baseFee *big.Int) (*core.Mess From: from, To: to, Nonce: tx.Nonce, - Value: value, + Value: uint256.MustFromBig(value), GasLimit: gasLimit, - GasPrice: gasPrice, - GasFeeCap: tx.MaxFeePerGas, - GasTipCap: tx.MaxPriorityFeePerGas, + GasPrice: uint256.MustFromBig(gasPrice), + GasFeeCap: uint256.MustFromBig(tx.MaxFeePerGas), + GasTipCap: uint256.MustFromBig(tx.MaxPriorityFeePerGas), Data: data, AccessList: accessList, BlobHashes: tx.BlobVersionedHashes, - BlobGasFeeCap: tx.BlobGasFeeCap, + BlobGasFeeCap: uint256.MustFromBig(tx.BlobGasFeeCap), SetCodeAuthorizations: authList, } return msg, nil From 117e067f0f0bae1a17082321f224dedb6765b10f Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Mon, 11 May 2026 23:19:24 +0800 Subject: [PATCH 39/63] version: release go-ethereum v1.17.3 stable (#34937) --- version/version.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version/version.go b/version/version.go index ea1f5fc632..4b64b58bb7 100644 --- a/version/version.go +++ b/version/version.go @@ -17,8 +17,8 @@ package version const ( - Major = 1 // Major version component of the current release - Minor = 17 // Minor version component of the current release - Patch = 3 // Patch version component of the current release - Meta = "unstable" // Version metadata to append to the version string + Major = 1 // Major version component of the current release + Minor = 17 // Minor version component of the current release + Patch = 3 // Patch version component of the current release + Meta = "stable" // Version metadata to append to the version string ) From 8b39453122d9df81744fe44a7c1f3d6c4de88ab4 Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Mon, 11 May 2026 23:57:06 +0800 Subject: [PATCH 40/63] version: start release 1.17.4 cycle (#34938) --- version/version.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version/version.go b/version/version.go index 4b64b58bb7..5d402f3009 100644 --- a/version/version.go +++ b/version/version.go @@ -17,8 +17,8 @@ package version const ( - Major = 1 // Major version component of the current release - Minor = 17 // Minor version component of the current release - Patch = 3 // Patch version component of the current release - Meta = "stable" // Version metadata to append to the version string + Major = 1 // Major version component of the current release + Minor = 17 // Minor version component of the current release + Patch = 4 // Patch version component of the current release + Meta = "unstable" // Version metadata to append to the version string ) From 22919cec1b257b3f2d3a2c348f432c08efae7114 Mon Sep 17 00:00:00 2001 From: rayoo Date: Tue, 12 May 2026 03:21:31 +0800 Subject: [PATCH 41/63] eth/tracers: fix data race on interruption reason across tracers (#34827) Every tracer that implements Stop/GetResult held a `reason error` field that is written by Stop (called from the trace-timeout watchdog goroutine in api.go) and read by GetResult (called by the RPC handler main goroutine). These accesses were unsynchronized. --- eth/tracers/logger/logger.go | 12 ++--- eth/tracers/native/4byte.go | 13 +++-- eth/tracers/native/call.go | 11 +++-- eth/tracers/native/call_flat.go | 5 +- eth/tracers/native/erc7562.go | 16 +++++-- eth/tracers/native/prestate.go | 11 +++-- eth/tracers/native/tracer_test.go | 80 +++++++++++++++++++++++++++++++ 7 files changed, 123 insertions(+), 25 deletions(-) create mode 100644 eth/tracers/native/tracer_test.go diff --git a/eth/tracers/logger/logger.go b/eth/tracers/logger/logger.go index 7f2b2aecf2..8e445818ef 100644 --- a/eth/tracers/logger/logger.go +++ b/eth/tracers/logger/logger.go @@ -229,9 +229,9 @@ type StructLogger struct { logs []json.RawMessage // buffer of json-encoded logs resultSize int - interrupt atomic.Bool // Atomic flag to signal execution interruption - reason error // Textual reason for the interruption - skip bool // skip processing hooks. + interrupt atomic.Bool // Atomic flag to signal execution interruption + reason atomic.Pointer[error] // Reason for the interruption, populated by Stop + skip bool // skip processing hooks. } // NewStreamingStructLogger returns a new streaming logger. @@ -357,8 +357,8 @@ func (l *StructLogger) OnExit(depth int, output []byte, gasUsed uint64, err erro func (l *StructLogger) GetResult() (json.RawMessage, error) { // Tracing aborted - if l.reason != nil { - return nil, l.reason + if p := l.reason.Load(); p != nil { + return nil, *p } failed := l.err != nil returnData := common.CopyBytes(l.output) @@ -376,7 +376,7 @@ func (l *StructLogger) GetResult() (json.RawMessage, error) { // Stop terminates execution of the tracer at the first opportune moment. func (l *StructLogger) Stop(err error) { - l.reason = err + l.reason.Store(&err) l.interrupt.Store(true) } diff --git a/eth/tracers/native/4byte.go b/eth/tracers/native/4byte.go index cec45a1e7a..a542eeffa2 100644 --- a/eth/tracers/native/4byte.go +++ b/eth/tracers/native/4byte.go @@ -49,9 +49,9 @@ func init() { // 0xc281d19e-0: 1 // } type fourByteTracer struct { - ids map[string]int // ids aggregates the 4byte ids found - interrupt atomic.Bool // Atomic flag to signal execution interruption - reason error // Textual reason for the interruption + ids map[string]int // ids aggregates the 4byte ids found + interrupt atomic.Bool // Atomic flag to signal execution interruption + reason atomic.Pointer[error] // Reason for the interruption, populated by Stop chainConfig *params.ChainConfig activePrecompiles []common.Address // Updated on tx start based on given rules } @@ -124,12 +124,15 @@ func (t *fourByteTracer) GetResult() (json.RawMessage, error) { if err != nil { return nil, err } - return res, t.reason + if p := t.reason.Load(); p != nil { + return res, *p + } + return res, nil } // Stop terminates execution of the tracer at the first opportune moment. func (t *fourByteTracer) Stop(err error) { - t.reason = err + t.reason.Store(&err) t.interrupt.Store(true) } diff --git a/eth/tracers/native/call.go b/eth/tracers/native/call.go index 06220da84d..dfa804827b 100644 --- a/eth/tracers/native/call.go +++ b/eth/tracers/native/call.go @@ -116,8 +116,8 @@ type callTracer struct { config callTracerConfig gasLimit uint64 depth int - interrupt atomic.Bool // Atomic flag to signal execution interruption - reason error // Textual reason for the interruption + interrupt atomic.Bool // Atomic flag to signal execution interruption + reason atomic.Pointer[error] // Reason for the interruption, populated by Stop } type callTracerConfig struct { @@ -268,12 +268,15 @@ func (t *callTracer) GetResult() (json.RawMessage, error) { if err != nil { return nil, err } - return res, t.reason + if p := t.reason.Load(); p != nil { + return res, *p + } + return res, nil } // Stop terminates execution of the tracer at the first opportune moment. func (t *callTracer) Stop(err error) { - t.reason = err + t.reason.Store(&err) t.interrupt.Store(true) } diff --git a/eth/tracers/native/call_flat.go b/eth/tracers/native/call_flat.go index 4e7fc31a9c..484f2d4e3b 100644 --- a/eth/tracers/native/call_flat.go +++ b/eth/tracers/native/call_flat.go @@ -233,7 +233,10 @@ func (t *flatCallTracer) GetResult() (json.RawMessage, error) { if err != nil { return nil, err } - return res, t.tracer.reason + if p := t.tracer.reason.Load(); p != nil { + return res, *p + } + return res, nil } // Stop terminates execution of the tracer at the first opportune moment. diff --git a/eth/tracers/native/erc7562.go b/eth/tracers/native/erc7562.go index 34e202f667..0bf80d77b5 100644 --- a/eth/tracers/native/erc7562.go +++ b/eth/tracers/native/erc7562.go @@ -135,8 +135,8 @@ type opcodeWithPartialStack struct { type erc7562Tracer struct { config erc7562TracerConfig gasLimit uint64 - interrupt atomic.Bool // Atomic flag to signal execution interruption - reason error // Textual reason for the interruption + interrupt atomic.Bool // Atomic flag to signal execution interruption + reason atomic.Pointer[error] // Reason for the interruption, populated by Stop env *tracing.VMContext ignoredOpcodes map[vm.OpCode]struct{} @@ -317,7 +317,10 @@ func (t *erc7562Tracer) OnLog(log1 *types.Log) { // error arising from the encoding or forceful termination (via `Stop`). func (t *erc7562Tracer) GetResult() (json.RawMessage, error) { if t.interrupt.Load() { - return nil, t.reason + if p := t.reason.Load(); p != nil { + return nil, *p + } + return nil, nil } if len(t.callstackWithOpcodes) != 1 { return nil, errors.New("incorrect number of top-level calls") @@ -337,12 +340,15 @@ func (t *erc7562Tracer) GetResult() (json.RawMessage, error) { return nil, err } - return enc, t.reason + if p := t.reason.Load(); p != nil { + return enc, *p + } + return enc, nil } // Stop terminates execution of the tracer at the first opportune moment. func (t *erc7562Tracer) Stop(err error) { - t.reason = err + t.reason.Store(&err) t.interrupt.Store(true) } diff --git a/eth/tracers/native/prestate.go b/eth/tracers/native/prestate.go index 36cb16e44b..7026cca7f3 100644 --- a/eth/tracers/native/prestate.go +++ b/eth/tracers/native/prestate.go @@ -71,8 +71,8 @@ type prestateTracer struct { to common.Address config PrestateTracerConfig chainConfig *params.ChainConfig - interrupt atomic.Bool // Atomic flag to signal execution interruption - reason error // Textual reason for the interruption + interrupt atomic.Bool // Atomic flag to signal execution interruption + reason atomic.Pointer[error] // Reason for the interruption, populated by Stop created map[common.Address]bool deleted map[common.Address]bool } @@ -240,12 +240,15 @@ func (t *prestateTracer) GetResult() (json.RawMessage, error) { if err != nil { return nil, err } - return json.RawMessage(res), t.reason + if p := t.reason.Load(); p != nil { + return json.RawMessage(res), *p + } + return json.RawMessage(res), nil } // Stop terminates execution of the tracer at the first opportune moment. func (t *prestateTracer) Stop(err error) { - t.reason = err + t.reason.Store(&err) t.interrupt.Store(true) } diff --git a/eth/tracers/native/tracer_test.go b/eth/tracers/native/tracer_test.go new file mode 100644 index 0000000000..70e6283d34 --- /dev/null +++ b/eth/tracers/native/tracer_test.go @@ -0,0 +1,80 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package native_test + +import ( + "errors" + "math/big" + "sync" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/eth/tracers" + "github.com/ethereum/go-ethereum/params" + "github.com/stretchr/testify/require" +) + +// TestTracerStopRace exercises the concurrent Stop / GetResult path that the +// trace RPC handler uses: a timeout watchdog goroutine calls Stop while the +// main goroutine is still running the trace and will eventually call +// GetResult. Under -race, writes to the interruption reason field must not +// race with reads, for every tracer that implements it. +// +// callTracer, flatCallTracer and erc7562Tracer's GetResult short-circuits on +// an empty callstack ("incorrect number of top-level calls") before loading +// the reason. For those tracers the test pushes a single top-level call frame +// via OnEnter so GetResult reaches the reason.Load() path where the race can +// be observed under -race. +func TestTracerStopRace(t *testing.T) { + type setup struct { + name string + needsFrame bool // whether GetResult requires a top-level call frame + } + cases := []setup{ + {"callTracer", true}, + {"flatCallTracer", true}, + {"4byteTracer", false}, + {"prestateTracer", false}, + {"erc7562Tracer", true}, + } + for _, s := range cases { + t.Run(s.name, func(t *testing.T) { + tr, err := tracers.DefaultDirectory.New(s.name, &tracers.Context{}, nil, params.MainnetChainConfig) + require.NoError(t, err) + + if s.needsFrame && tr.OnEnter != nil { + // Push a single top-level call frame so GetResult doesn't + // short-circuit before reading the interruption reason. + tr.OnEnter(0, byte(vm.CALL), common.Address{}, common.Address{}, nil, 0, big.NewInt(0)) + } + + stopErr := errors.New("execution timeout") + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + tr.Stop(stopErr) + }() + go func() { + defer wg.Done() + _, _ = tr.GetResult() + }() + wg.Wait() + }) + } +} From 298c83502bae65bf4178159114b5a6f689f1e8b3 Mon Sep 17 00:00:00 2001 From: Daniel Liu <139250065@qq.com> Date: Tue, 12 May 2026 03:32:19 +0800 Subject: [PATCH 42/63] cmd/evm: fix gasUsed in evm run cmd (#34732) In the --create path, execFunc returns gasLeft as the second return value, but the rest of the code treats this value as "gas used" (printed as such, and compared in timedExec). This makes gas reporting incorrect and can cause benchmark consistency checks to fail. --- cmd/evm/runner.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/evm/runner.go b/cmd/evm/runner.go index 82e7bdff3d..6d80056d04 100644 --- a/cmd/evm/runner.go +++ b/cmd/evm/runner.go @@ -321,7 +321,7 @@ func runCmd(ctx *cli.Context) error { // don't mutate the state! runtimeConfig.State = prestate.Copy() output, _, gasLeft, err := runtime.Create(input, &runtimeConfig) - return output, gasLeft, err + return output, initialGas - gasLeft, err } } else { if len(code) > 0 { From 56d391b60157234474bcdfba0545c2cbd4898c4b Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Tue, 12 May 2026 04:17:48 +0800 Subject: [PATCH 43/63] cmd, core, internal, miner: wrap pre/post-execution (#34812) This is a refactoring PR to wrap all pre/post-execution system calls as the exported functions, eliminating the duplicated system calls across the codebase. There are a few things unchanged but worths highlight: - ChainMaker is left as unchanged, a significant rewrite is required - BeaconRoot in header should be non-nil if Cancun is enabled --------- Co-authored-by: jwasinger --- cmd/evm/internal/t8ntool/execution.go | 28 +++++---------- core/chain_makers.go | 32 ++++++----------- core/state_processor.go | 52 +++++++++++++++------------ eth/state_accessor.go | 11 +++--- eth/tracers/api.go | 48 ++++++++++--------------- internal/ethapi/simulate.go | 29 ++++----------- miner/worker.go | 26 +++----------- 7 files changed, 82 insertions(+), 144 deletions(-) diff --git a/cmd/evm/internal/t8ntool/execution.go b/cmd/evm/internal/t8ntool/execution.go index 253ebe1111..15973e934d 100644 --- a/cmd/evm/internal/t8ntool/execution.go +++ b/cmd/evm/internal/t8ntool/execution.go @@ -17,6 +17,7 @@ package t8ntool import ( + "context" "encoding/json" "fmt" stdmath "math" @@ -331,27 +332,14 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, } // Gather the execution-layer triggered requests. - var requests [][]byte - if chainConfig.IsPrague(vmContext.BlockNumber, vmContext.Time) { - requests = [][]byte{} - // EIP-6110 - var allLogs []*types.Log - for _, receipt := range receipts { - allLogs = append(allLogs, receipt.Logs...) - } - if err := core.ParseDepositLogs(&requests, allLogs, chainConfig); err != nil { - return nil, nil, nil, NewError(ErrorEVM, fmt.Errorf("could not parse requests logs: %v", err)) - } - // EIP-7002 - if err := core.ProcessWithdrawalQueue(&requests, evm); err != nil { - return nil, nil, nil, NewError(ErrorEVM, fmt.Errorf("could not process withdrawal requests: %v", err)) - } - // EIP-7251 - if err := core.ProcessConsolidationQueue(&requests, evm); err != nil { - return nil, nil, nil, NewError(ErrorEVM, fmt.Errorf("could not process consolidation requests: %v", err)) - } + var allLogs []*types.Log + for _, receipt := range receipts { + allLogs = append(allLogs, receipt.Logs...) + } + requests, err := core.PostExecution(context.Background(), chainConfig, vmContext.BlockNumber, vmContext.Time, allLogs, evm) + if err != nil { + return nil, nil, nil, NewError(ErrorEVM, fmt.Errorf("failed to process post-execution: %v", err)) } - // Commit block root, err := statedb.Commit(vmContext.BlockNumber.Uint64(), chainConfig.IsEIP158(vmContext.BlockNumber), chainConfig.IsCancun(vmContext.BlockNumber, vmContext.Time)) if err != nil { diff --git a/core/chain_makers.go b/core/chain_makers.go index 46cd98de61..7474d892b1 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -17,6 +17,7 @@ package core import ( + "context" "fmt" "math/big" @@ -314,28 +315,17 @@ func (b *BlockGen) collectRequests(readonly bool) (requests [][]byte) { // off the statedb before executing the system calls. statedb = statedb.Copy() } + var blockLogs []*types.Log + for _, r := range b.receipts { + blockLogs = append(blockLogs, r.Logs...) + } + // TODO use the shared EVM throughout the entire generation cycle + blockContext := NewEVMBlockContext(b.header, b.cm, &b.header.Coinbase) + evm := vm.NewEVM(blockContext, statedb, b.cm.config, vm.Config{}) - if b.cm.config.IsPrague(b.header.Number, b.header.Time) { - requests = [][]byte{} - // EIP-6110 deposits - var blockLogs []*types.Log - for _, r := range b.receipts { - blockLogs = append(blockLogs, r.Logs...) - } - if err := ParseDepositLogs(&requests, blockLogs, b.cm.config); err != nil { - panic(fmt.Sprintf("failed to parse deposit log: %v", err)) - } - // create EVM for system calls - blockContext := NewEVMBlockContext(b.header, b.cm, &b.header.Coinbase) - evm := vm.NewEVM(blockContext, statedb, b.cm.config, vm.Config{}) - // EIP-7002 - if err := ProcessWithdrawalQueue(&requests, evm); err != nil { - panic(fmt.Sprintf("could not process withdrawal requests: %v", err)) - } - // EIP-7251 - if err := ProcessConsolidationQueue(&requests, evm); err != nil { - panic(fmt.Sprintf("could not process consolidation requests: %v", err)) - } + requests, err := PostExecution(context.Background(), b.cm.config, b.header.Number, b.header.Time, blockLogs, evm) + if err != nil { + panic(fmt.Sprintf("failed to run post-execution: %v", err)) } return requests } diff --git a/core/state_processor.go b/core/state_processor.go index 9dcb4cf07c..4bffece7ac 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -76,28 +76,18 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated if hooks := cfg.Tracer; hooks != nil { tracingStateDB = state.NewHookedState(statedb, hooks) } - // Mutate the block and state according to any hard-fork specs if config.DAOForkSupport && config.DAOForkBlock != nil && config.DAOForkBlock.Cmp(block.Number()) == 0 { misc.ApplyDAOHardFork(tracingStateDB) } var ( - context vm.BlockContext + context = NewEVMBlockContext(header, p.chain, nil) signer = types.MakeSigner(config, header.Number, header.Time) + evm = vm.NewEVM(context, tracingStateDB, config, cfg) ) - - // Apply pre-execution system calls. - context = NewEVMBlockContext(header, p.chain, nil) - evm := vm.NewEVM(context, tracingStateDB, config, cfg) defer evm.Release() - - if beaconRoot := block.BeaconRoot(); beaconRoot != nil { - ProcessBeaconBlockRoot(*beaconRoot, evm) - } - if config.IsPrague(block.Number(), block.Time()) || config.IsUBT(block.Number(), block.Time()) { - ProcessParentBlockHash(block.ParentHash(), evm) - } - + // Run the pre-execution system calls + PreExecution(ctx, block.BeaconRoot(), block.ParentHash(), config, evm, block.Number(), block.Time()) // Iterate over and process the individual transactions for i, tx := range block.Transactions() { msg, err := TransactionToMessage(tx, signer, header.BaseFee) @@ -119,11 +109,11 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated allLogs = append(allLogs, receipt.Logs...) spanEnd(nil) } - requests, err := postExecution(ctx, config, block, allLogs, evm) + // Run the post-execution system calls + requests, err := PostExecution(ctx, config, block.Number(), block.Time(), allLogs, evm) if err != nil { return nil, err } - // Finalize the block, applying any consensus engine specific extras (e.g. block rewards) p.chain.Engine().Finalize(p.chain, header, tracingStateDB, block.Body()) @@ -135,28 +125,44 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated }, nil } -// postExecution processes the post-execution system calls if Prague is enabled. -func postExecution(ctx context.Context, config *params.ChainConfig, block *types.Block, allLogs []*types.Log, evm *vm.EVM) (requests [][]byte, err error) { +// PreExecution processes pre-execution system calls. +func PreExecution(ctx context.Context, beaconRoot *common.Hash, parent common.Hash, config *params.ChainConfig, evm *vm.EVM, number *big.Int, time uint64) { + _, _, spanEnd := telemetry.StartSpan(ctx, "core.preExecution") + defer spanEnd(nil) + + // EIP-4788 + if beaconRoot != nil { + ProcessBeaconBlockRoot(*beaconRoot, evm) + } + // EIP-2935 + if config.IsPrague(number, time) || config.IsUBT(number, time) { + ProcessParentBlockHash(parent, evm) + } +} + +// PostExecution processes post-execution system calls when Prague is enabled. +// If Prague is not activated, it returns null requests to differentiate from +// empty requests. +func PostExecution(ctx context.Context, config *params.ChainConfig, number *big.Int, time uint64, allLogs []*types.Log, evm *vm.EVM) (requests [][]byte, err error) { _, _, spanEnd := telemetry.StartSpan(ctx, "core.postExecution") defer spanEnd(&err) // Read requests if Prague is enabled. - if config.IsPrague(block.Number(), block.Time()) { + if config.IsPrague(number, time) { requests = [][]byte{} // EIP-6110 if err := ParseDepositLogs(&requests, allLogs, config); err != nil { - return requests, fmt.Errorf("failed to parse deposit logs: %w", err) + return nil, fmt.Errorf("failed to parse deposit logs: %w", err) } // EIP-7002 if err := ProcessWithdrawalQueue(&requests, evm); err != nil { - return requests, fmt.Errorf("failed to process withdrawal queue: %w", err) + return nil, fmt.Errorf("failed to process withdrawal queue: %w", err) } // EIP-7251 if err := ProcessConsolidationQueue(&requests, evm); err != nil { - return requests, fmt.Errorf("failed to process consolidation queue: %w", err) + return nil, fmt.Errorf("failed to process consolidation queue: %w", err) } } - return requests, nil } diff --git a/eth/state_accessor.go b/eth/state_accessor.go index a806a4fc56..53dfb7d458 100644 --- a/eth/state_accessor.go +++ b/eth/state_accessor.go @@ -248,13 +248,10 @@ func (eth *Ethereum) stateAtTransaction(ctx context.Context, block *types.Block, context := core.NewEVMBlockContext(block.Header(), eth.blockchain, nil) evm := vm.NewEVM(context, statedb, eth.blockchain.Config(), vm.Config{}) defer evm.Release() - if beaconRoot := block.BeaconRoot(); beaconRoot != nil { - core.ProcessBeaconBlockRoot(*beaconRoot, evm) - } - // If prague hardfork, insert parent block hash in the state as per EIP-2935. - if eth.blockchain.Config().IsPrague(block.Number(), block.Time()) { - core.ProcessParentBlockHash(block.ParentHash(), evm) - } + + // Run pre-execution system calls + core.PreExecution(ctx, block.BeaconRoot(), block.ParentHash(), eth.blockchain.Config(), evm, block.Number(), block.Time()) + if txIndex == 0 && len(block.Transactions()) == 0 { return nil, context, statedb, release, nil } diff --git a/eth/tracers/api.go b/eth/tracers/api.go index dae11b81de..d9e40f7ec1 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -372,13 +372,8 @@ func (api *API) traceChain(start, end *types.Block, config *TraceConfig, closed // as per EIP-4788. context := core.NewEVMBlockContext(next.Header(), api.chainContext(ctx), nil) evm := vm.NewEVM(context, statedb, api.backend.ChainConfig(), vm.Config{}) - if beaconRoot := next.BeaconRoot(); beaconRoot != nil { - core.ProcessBeaconBlockRoot(*beaconRoot, evm) - } - // Insert parent hash in history contract. - if api.backend.ChainConfig().IsPrague(next.Number(), next.Time()) { - core.ProcessParentBlockHash(next.ParentHash(), evm) - } + + core.PreExecution(ctx, next.BeaconRoot(), next.ParentHash(), api.backend.ChainConfig(), evm, next.Number(), next.Time()) evm.Release() // Clean out any pending release functions of trace state. Note this // step must be done after constructing tracing state, because the @@ -494,8 +489,8 @@ func (api *API) StandardTraceBlockToFile(ctx context.Context, hash common.Hash, return api.standardTraceBlockToFile(ctx, block, config) } -// IntermediateRoots executes a block (bad- or canon- or side-), and returns a list -// of intermediate roots: the stateroot after each transaction. +// IntermediateRoots executes a block, and returns a list of intermediate roots: +// the stateroot after each transaction. func (api *API) IntermediateRoots(ctx context.Context, hash common.Hash, config *TraceConfig) ([]common.Hash, error) { block, _ := api.blockByHash(ctx, hash) if block == nil { @@ -517,21 +512,19 @@ func (api *API) IntermediateRoots(ctx context.Context, hash common.Hash, config return nil, err } defer release() + var ( roots []common.Hash signer = types.MakeSigner(api.backend.ChainConfig(), block.Number(), block.Time()) chainConfig = api.backend.ChainConfig() vmctx = core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil) deleteEmptyObjects = chainConfig.IsEIP158(block.Number()) + evm = vm.NewEVM(vmctx, statedb, chainConfig, vm.Config{}) ) - evm := vm.NewEVM(vmctx, statedb, chainConfig, vm.Config{}) defer evm.Release() - if beaconRoot := block.BeaconRoot(); beaconRoot != nil { - core.ProcessBeaconBlockRoot(*beaconRoot, evm) - } - if chainConfig.IsPrague(block.Number(), block.Time()) { - core.ProcessParentBlockHash(block.ParentHash(), evm) - } + // Run pre-execution system calls + core.PreExecution(ctx, block.BeaconRoot(), block.ParentHash(), chainConfig, evm, block.Number(), block.Time()) + for i, tx := range block.Transactions() { if err := ctx.Err(); err != nil { return nil, err @@ -548,7 +541,7 @@ func (api *API) IntermediateRoots(ctx context.Context, hash common.Hash, config // N.B: This should never happen while tracing canon blocks, only when tracing bad blocks. return roots, nil } - // calling IntermediateRoot will internally call Finalize on the state + // Calling IntermediateRoot will internally call Finalize on the state // so any modifications are written to the trie roots = append(roots, statedb.IntermediateRoot(deleteEmptyObjects)) } @@ -587,12 +580,9 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac blockCtx := core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil) evm := vm.NewEVM(blockCtx, statedb, api.backend.ChainConfig(), vm.Config{}) defer evm.Release() - if beaconRoot := block.BeaconRoot(); beaconRoot != nil { - core.ProcessBeaconBlockRoot(*beaconRoot, evm) - } - if api.backend.ChainConfig().IsPrague(block.Number(), block.Time()) { - core.ProcessParentBlockHash(block.ParentHash(), evm) - } + + // Run pre-execution system calls + core.PreExecution(ctx, block.BeaconRoot(), block.ParentHash(), api.backend.ChainConfig(), evm, block.Number(), block.Time()) // JS tracers have high overhead. In this case run a parallel // process that generates states in one thread and traces txes @@ -760,15 +750,12 @@ func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block // Note: This copies the config, to not screw up the main config chainConfig, canon = overrideConfig(chainConfig, config.Overrides) } - evm := vm.NewEVM(vmctx, statedb, chainConfig, vm.Config{}) defer evm.Release() - if beaconRoot := block.BeaconRoot(); beaconRoot != nil { - core.ProcessBeaconBlockRoot(*beaconRoot, evm) - } - if chainConfig.IsPrague(block.Number(), block.Time()) { - core.ProcessParentBlockHash(block.ParentHash(), evm) - } + + // Run pre-execution system calls + core.PreExecution(ctx, block.BeaconRoot(), block.ParentHash(), chainConfig, evm, block.Number(), block.Time()) + for i, tx := range block.Transactions() { // Prepare the transaction for un-traced execution msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee()) @@ -795,6 +782,7 @@ func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block return nil, err } dumps = append(dumps, dump.Name()) + // Set up the tracer and EVM for the transaction. var ( writer = bufio.NewWriter(dump) diff --git a/internal/ethapi/simulate.go b/internal/ethapi/simulate.go index e3a14bf5d6..170104fbdf 100644 --- a/internal/ethapi/simulate.go +++ b/internal/ethapi/simulate.go @@ -318,12 +318,9 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, if precompiles != nil { evm.SetPrecompiles(precompiles) } - if sim.chainConfig.IsPrague(header.Number, header.Time) || sim.chainConfig.IsUBT(header.Number, header.Time) { - core.ProcessParentBlockHash(header.ParentHash, evm) - } - if header.ParentBeaconRoot != nil { - core.ProcessBeaconBlockRoot(*header.ParentBeaconRoot, evm) - } + // Run pre-execution system calls + core.PreExecution(ctx, header.ParentBeaconRoot, header.ParentHash, sim.chainConfig, evm, header.Number, header.Time) + var allLogs []*types.Log for i, call := range block.Calls { // Terminate if the context is cancelled @@ -393,22 +390,10 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, header.BlobGasUsed = &blobGasUsed } - // Process EIP-7685 requests - var requests [][]byte - if sim.chainConfig.IsPrague(header.Number, header.Time) { - requests = [][]byte{} - // EIP-6110 - if err := core.ParseDepositLogs(&requests, allLogs, sim.chainConfig); err != nil { - return nil, nil, nil, err - } - // EIP-7002 - if err := core.ProcessWithdrawalQueue(&requests, evm); err != nil { - return nil, nil, nil, err - } - // EIP-7251 - if err := core.ProcessConsolidationQueue(&requests, evm); err != nil { - return nil, nil, nil, err - } + // Run post-execution system calls + requests, err := core.PostExecution(ctx, sim.chainConfig, header.Number, header.Time, allLogs, evm) + if err != nil { + return nil, nil, nil, err } if requests != nil { reqHash := types.CalcRequestsHash(requests) diff --git a/miner/worker.go b/miner/worker.go index 42e3695025..ccafa20b29 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -208,21 +208,9 @@ func (miner *Miner) generateWork(ctx context.Context, genParam *generateParams, } // Collect consensus-layer requests if Prague is enabled. - var requests [][]byte - if miner.chainConfig.IsPrague(work.header.Number, work.header.Time) { - requests = [][]byte{} - // EIP-6110 deposits - if err := core.ParseDepositLogs(&requests, allLogs, miner.chainConfig); err != nil { - return &newPayloadResult{err: err} - } - // EIP-7002 - if err := core.ProcessWithdrawalQueue(&requests, work.evm); err != nil { - return &newPayloadResult{err: err} - } - // EIP-7251 consolidations - if err := core.ProcessConsolidationQueue(&requests, work.evm); err != nil { - return &newPayloadResult{err: err} - } + requests, err := core.PostExecution(ctx, miner.chainConfig, work.header.Number, work.header.Time, allLogs, work.evm) + if err != nil { + return &newPayloadResult{err: err} } if requests != nil { reqHash := types.CalcRequestsHash(requests) @@ -329,12 +317,8 @@ func (miner *Miner) prepareWork(ctx context.Context, genParams *generateParams, log.Error("Failed to create sealing context", "err", err) return nil, err } - if header.ParentBeaconRoot != nil { - core.ProcessBeaconBlockRoot(*header.ParentBeaconRoot, env.evm) - } - if miner.chainConfig.IsPrague(header.Number, header.Time) { - core.ProcessParentBlockHash(header.ParentHash, env.evm) - } + // Run pre-execution system calls + core.PreExecution(ctx, header.ParentBeaconRoot, header.ParentHash, miner.chainConfig, env.evm, header.Number, header.Time) return env, nil } From c16684c1eec23bdc3f806827713aad380a6a90f3 Mon Sep 17 00:00:00 2001 From: Sina M <1591639+s1na@users.noreply.github.com> Date: Tue, 12 May 2026 02:33:43 +0200 Subject: [PATCH 44/63] internal/ethapi: fix withdrawal regression in eth_simulateV1 (#34939) Fixes the regression caught by https://hive.ethpandaops.io/#/test/generic/1778481210-e59b7465e1d04f7ed1b0200838584b16?testnumber=137. engine.AssembleBlock explicitly expects withdrawals to be non-nil for pre-Shanghai blocks as opposed to FinaliseAndAssemble which stripped off the withdrawal. --- internal/ethapi/api_test.go | 61 +++++++++++++++++++++++++++++++++++++ internal/ethapi/simulate.go | 7 ++++- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index 161d97b4eb..63e75bd3e3 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -2680,6 +2680,67 @@ func TestSimulateV1TxSender(t *testing.T) { require.Equal(t, sender2, summary[1].Transactions[0].From, "sender address mismatch") } +// TestSimulateV1WithdrawalsByFork verifies that withdrawals and withdrawalsRoot +// are only emitted in the simulated block result when the simulated block is +// post-Shanghai. Pre-Shanghai blocks must omit both fields, otherwise the +// header hash and size would not match a valid pre-Shanghai block. +func TestSimulateV1WithdrawalsByFork(t *testing.T) { + t.Parallel() + + run := func(t *testing.T, cfg *params.ChainConfig, blockTime *uint64, wantWithdrawals bool) { + t.Helper() + gspec := &core.Genesis{Config: cfg, Alloc: types.GenesisAlloc{}} + backend := newTestBackend(t, 1, gspec, beacon.New(ethash.NewFaker()), func(i int, b *core.BlockGen) {}) + + ctx := context.Background() + stateDB, baseHeader, err := backend.StateAndHeaderByNumberOrHash(ctx, rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)) + if err != nil { + t.Fatalf("failed to get state and header: %v", err) + } + sim := &simulator{ + b: backend, + state: stateDB, + base: baseHeader, + chainConfig: backend.ChainConfig(), + budget: newGasBudget(0), + } + + block := simBlock{} + if blockTime != nil { + t := hexutil.Uint64(*blockTime) + block.BlockOverrides = &override.BlockOverrides{Time: &t} + } + results, err := sim.execute(ctx, []simBlock{block}) + if err != nil { + t.Fatalf("simulation execution failed: %v", err) + } + require.Len(t, results, 1) + + enc, err := json.Marshal(results[0]) + if err != nil { + t.Fatalf("failed to marshal result: %v", err) + } + var raw map[string]json.RawMessage + if err := json.Unmarshal(enc, &raw); err != nil { + t.Fatalf("failed to unmarshal result: %v", err) + } + _, hasWithdrawals := raw["withdrawals"] + _, hasWithdrawalsRoot := raw["withdrawalsRoot"] + if hasWithdrawals != wantWithdrawals || hasWithdrawalsRoot != wantWithdrawals { + t.Fatalf("unexpected withdrawals fields: withdrawals=%v withdrawalsRoot=%v want=%v\n%s", hasWithdrawals, hasWithdrawalsRoot, wantWithdrawals, enc) + } + } + + t.Run("pre-shanghai", func(t *testing.T) { + // TestChainConfig has ShanghaiTime=nil, so all simulated blocks are pre-Shanghai. + run(t, params.TestChainConfig, nil, false) + }) + t.Run("post-shanghai", func(t *testing.T) { + // MergedTestChainConfig has every fork active from genesis. + run(t, params.MergedTestChainConfig, nil, true) + }) +} + func TestSignTransaction(t *testing.T) { t.Parallel() // Initialize test accounts diff --git a/internal/ethapi/simulate.go b/internal/ethapi/simulate.go index 170104fbdf..c34970578c 100644 --- a/internal/ethapi/simulate.go +++ b/internal/ethapi/simulate.go @@ -402,7 +402,12 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, blockBody := &types.Body{ Transactions: txes, - Withdrawals: *block.BlockOverrides.Withdrawals, // Withdrawal is also sanitized as non-nil + } + // Withdrawals are a post-Shanghai field. Attaching a non-nil withdrawals + // slice would cause types.NewBlock to populate WithdrawalsHash on the + // header and emit withdrawals fields for pre-Shanghai blocks. + if sim.chainConfig.IsShanghai(header.Number, header.Time) { + blockBody.Withdrawals = *block.BlockOverrides.Withdrawals } chainHeadReader := &simChainHeadReader{ctx, sim.b} From d446676fc448ff6ea3e14f9bcfe725d70e1c49ee Mon Sep 17 00:00:00 2001 From: cui Date: Tue, 12 May 2026 10:05:39 +0800 Subject: [PATCH 45/63] core: write head hash to db after snap sync is complete (#34912) --- core/blockchain.go | 1 + 1 file changed, 1 insertion(+) diff --git a/core/blockchain.go b/core/blockchain.go index 2b49111121..7b5a910b7a 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -1186,6 +1186,7 @@ func (bc *BlockChain) SnapSyncComplete(hash common.Hash) error { } // If all checks out, manually set the head block. + rawdb.WriteHeadBlockHash(bc.db, hash) bc.currentBlock.Store(block.Header()) headBlockGauge.Update(int64(block.NumberU64())) From ab28bda83ecb1e73cd2549b08edfb8f3c683ea13 Mon Sep 17 00:00:00 2001 From: Lessa <230214854+adblesss@users.noreply.github.com> Date: Tue, 12 May 2026 06:16:44 -0400 Subject: [PATCH 46/63] eth/catalyst: fix getBlobsV3 partial/complete metrics (#34666) In b2843a11d, metrics check len(res) == len(hashes) but res is pre-allocated with make(), so length is always equal. Partial hit metric never fires. Count non-nil elements instead. --------- Co-authored-by: Bosul Mun --- eth/catalyst/api.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go index b81ed57a2c..1def169ae0 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -646,6 +646,7 @@ func (api *ConsensusAPI) getBlobs(hashes []common.Hash, v2 bool) ([]*engine.Blob return nil, engine.InvalidParams.With(err) } // Validate the blobs from the pool and assemble the response + filled := 0 res := make([]*engine.BlobAndProofV2, len(hashes)) for i := range blobs { // The blob has been evicted since the last AvailableBlobs call. @@ -666,10 +667,11 @@ func (api *ConsensusAPI) getBlobs(hashes []common.Hash, v2 bool) ([]*engine.Blob Blob: blobs[i][:], CellProofs: cellProofs, } + filled++ } - if len(res) == len(hashes) { + if filled == len(hashes) { getBlobsRequestCompleteHit.Inc(1) - } else if len(res) > 0 { + } else if filled > 0 { getBlobsRequestPartialHit.Inc(1) } else { getBlobsRequestMiss.Inc(1) From 726d657a4a03015171b649daca70abf6a54a9d6b Mon Sep 17 00:00:00 2001 From: Bosul Mun Date: Tue, 12 May 2026 13:59:33 +0200 Subject: [PATCH 47/63] core/txpool/blobpool: add blobTxForPool type (#34882) This PR introduces a separate transaction pool type for sparse blobpool. In sparse blobpool, PooledTransactions message delivers transactions without blobs, partial or full cells are downloaded by Cells message. Blobpool no longer stores transactions with complete sidecars, and it stores transactions without blobs, along with the corresponding cells. Because of this, a dedicated type distinct from types.Transaction is required. This PR introduces a type called `BlobTxForPool` and stores each sidecar field independently, in order to bypass the assumption that a sidecar always exists as a complete unit. Reintroducing the conversion queue was considered, but was ultimately omitted because type conversion should be sufficiently fast. With sparse blobpool, blob -> cell computation would take about ~13ms per blob. Not sure whether this is fast enough, but otherwise we can add the conversion queue later on the sparse blobpool branch. --- core/txpool/blobpool/blobpool.go | 311 +++++++++++++++++++------- core/txpool/blobpool/blobpool_test.go | 163 +++++++++++--- core/txpool/blobpool/limbo.go | 27 ++- 3 files changed, 386 insertions(+), 115 deletions(-) diff --git a/core/txpool/blobpool/blobpool.go b/core/txpool/blobpool/blobpool.go index f2e0d5f9d2..f8021e00c4 100644 --- a/core/txpool/blobpool/blobpool.go +++ b/core/txpool/blobpool/blobpool.go @@ -116,6 +116,8 @@ const ( announceThreshold = -1 ) +var errLegacyTx = errors.New("legacy transaction format") + // blobTxMeta is the minimal subset of types.BlobTx necessary to validate and // schedule the blob transactions into the following blocks. Only ever add the // bare minimum needed fields to keep the size down (and thus number of entries @@ -147,28 +149,137 @@ type blobTxMeta struct { evictionBlobFeeJumps float64 // Worse blob fee (converted to fee jumps) across all previous nonces } -// newBlobTxMeta retrieves the indexed metadata fields from a blob transaction -// and assembles a helper struct to track in memory. -// Requires the transaction to have a sidecar (or that we introduce a special version tag for no-sidecar). -func newBlobTxMeta(id uint64, size uint64, storageSize uint32, tx *types.Transaction) *blobTxMeta { - if tx.BlobTxSidecar() == nil { - // This should never happen, as the pool only admits blob transactions with a sidecar +// blobTxForPool is the storage representation of a blob transaction in the +// blobpool. +type blobTxForPool struct { + Tx *types.Transaction // tx without sidecar + Version byte + Commitments []kzg4844.Commitment + Proofs []kzg4844.Proof + Blobs []kzg4844.Blob +} + +// Sidecar returns BlobTxSidecar of ptx. +func (ptx *blobTxForPool) Sidecar() *types.BlobTxSidecar { + return types.NewBlobTxSidecar(ptx.Version, ptx.Blobs, ptx.Commitments, ptx.Proofs) +} + +// ApplySidecar copies the sidecar's fields into the flat fields. +func (ptx *blobTxForPool) ApplySidecar(sc *types.BlobTxSidecar) { + ptx.Version = sc.Version + ptx.Commitments = sc.Commitments + ptx.Proofs = sc.Proofs + ptx.Blobs = sc.Blobs +} + +// TxSize returns the transaction size on the network without +// reconstructing the transaction. +func (ptx *blobTxForPool) TxSize() uint64 { + var blobs, commitments, proofs uint64 + for i := range ptx.Blobs { + blobs += rlp.BytesSize(ptx.Blobs[i][:]) + } + for i := range ptx.Commitments { + commitments += rlp.BytesSize(ptx.Commitments[i][:]) + } + for i := range ptx.Proofs { + proofs += rlp.BytesSize(ptx.Proofs[i][:]) + } + return ptx.Tx.Size() + rlp.ListSize(rlp.ListSize(blobs)+rlp.ListSize(commitments)+rlp.ListSize(proofs)) +} + +// ToTx reconstructs a full Transaction with the sidecar attached. +func (ptx *blobTxForPool) ToTx() *types.Transaction { + return ptx.Tx.WithBlobTxSidecar(ptx.Sidecar()) +} + +// newBlobTxForPool decomposes a blob transaction into blobTxForPool type. +func newBlobTxForPool(tx *types.Transaction) *blobTxForPool { + sc := tx.BlobTxSidecar() + if sc == nil { panic("missing blob tx sidecar") } + return &blobTxForPool{ + Tx: tx.WithoutBlobTxSidecar(), + Version: sc.Version, + Commitments: sc.Commitments, + Proofs: sc.Proofs, + Blobs: sc.Blobs, + } +} + +// encodeForNetwork transforms stored blobTxForPool RLP into the standard +// network transaction encoding. This is used for getRLP. +// +// Stored RLP: [type_byte || tx_fields, version, [comms], [proofs], [blobs]] +// V0: type_byte || rlp([tx_fields, [blobs], [comms], [proofs]]) +// V1: type_byte || rlp([tx_fields, version, [blobs], [comms], [proofs]]) +func encodeForNetwork(storedRLP []byte) ([]byte, error) { + elems, err := rlp.SplitListValues(storedRLP) + if err != nil { + return nil, fmt.Errorf("invalid blobTxForPool RLP: %w", err) + } + if len(elems) < 5 { + return nil, fmt.Errorf("blobTxForPool has %d elements, need at least 5", len(elems)) + } + + // 1. Extract tx byte and other tx fields + txBytes, _, err := rlp.SplitString(elems[0]) + if err != nil { + return nil, fmt.Errorf("invalid tx bytes: %w", err) + } + if len(txBytes) < 2 { + return nil, errors.New("tx bytes too short") + } + typeByte := txBytes[0] + txRLP := txBytes[1:] + + // 2. Find the version of sidecar. + version, _, err := rlp.SplitUint64(elems[1]) + if err != nil || version > 255 { + return nil, fmt.Errorf("invalid version: %w", err) + } + versionByte := byte(version) + // 3. Extract sidecar elements. + commitmentsRLP := elems[2] + proofsRLP := elems[3] + blobsRLP := elems[4] + + // 4. Reconstruct into the network format. + var outer [][]byte + if versionByte == types.BlobSidecarVersion0 { + outer = [][]byte{txRLP, blobsRLP, commitmentsRLP, proofsRLP} + } else { + outer = [][]byte{txRLP, elems[1], blobsRLP, commitmentsRLP, proofsRLP} + } + body, err := rlp.MergeListValues(outer) + if err != nil { + return nil, err + } + // Prepend type byte and wrap as an RLP string. + inner := make([]byte, 1+len(body)) + inner[0] = typeByte + copy(inner[1:], body) + return rlp.EncodeToBytes(inner) +} + +// newBlobTxMeta retrieves the indexed metadata fields from a pooled blob +// transaction and assembles a helper struct to track in memory. +func newBlobTxMeta(id uint64, size uint64, storageSize uint32, ptx *blobTxForPool) *blobTxMeta { meta := &blobTxMeta{ - hash: tx.Hash(), - vhashes: tx.BlobHashes(), - version: tx.BlobTxSidecar().Version, + hash: ptx.Tx.Hash(), + vhashes: ptx.Tx.BlobHashes(), + version: ptx.Version, id: id, storageSize: storageSize, size: size, - nonce: tx.Nonce(), - costCap: uint256.MustFromBig(tx.Cost()), - execTipCap: uint256.MustFromBig(tx.GasTipCap()), - execFeeCap: uint256.MustFromBig(tx.GasFeeCap()), - blobFeeCap: uint256.MustFromBig(tx.BlobGasFeeCap()), - execGas: tx.Gas(), - blobGas: tx.BlobGas(), + nonce: ptx.Tx.Nonce(), + costCap: uint256.MustFromBig(ptx.Tx.Cost()), + execTipCap: uint256.MustFromBig(ptx.Tx.GasTipCap()), + execFeeCap: uint256.MustFromBig(ptx.Tx.GasFeeCap()), + blobFeeCap: uint256.MustFromBig(ptx.Tx.BlobGasFeeCap()), + execGas: ptx.Tx.Gas(), + blobGas: ptx.Tx.BlobGas(), } meta.basefeeJumps = dynamicFeeJumps(meta.execFeeCap) meta.blobfeeJumps = dynamicBlobFeeJumps(meta.blobFeeCap) @@ -460,10 +571,17 @@ func (p *BlobPool) Init(gasTip uint64, head *types.Header, reserver txpool.Reser return err } // Index all transactions on disk and delete anything unprocessable - var fails []uint64 + var ( + toDelete []uint64 + convertTxs []uint64 + ) index := func(id uint64, size uint32, blob []byte) { - if p.parseTransaction(id, size, blob) != nil { - fails = append(fails, id) + err := p.parseTransaction(id, size, blob) + if err != nil { + toDelete = append(toDelete, id) + } + if errors.Is(err, errLegacyTx) { + convertTxs = append(convertTxs, id) } } store, err := billy.Open(billy.Options{Path: queuedir, Repair: true}, slotter, index) @@ -472,17 +590,58 @@ func (p *BlobPool) Init(gasTip uint64, head *types.Header, reserver txpool.Reser } p.store = store - if len(fails) > 0 { - log.Warn("Dropping invalidated blob transactions", "ids", fails) - dropInvalidMeter.Mark(int64(len(fails))) + // Migrate legacy transactions (types.Transaction) to pooledBlobTx format. + if len(convertTxs) > 0 { + for _, id := range convertTxs { + var tx types.Transaction + data, err := p.store.Get(id) + if err != nil { + continue + } + err = rlp.DecodeBytes(data, &tx) + if err != nil { + continue + } + if tx.BlobTxSidecar() == nil { + continue + } + ptx := newBlobTxForPool(&tx) + blob, err := rlp.EncodeToBytes(ptx) + if err != nil { + continue + } + id, err := p.store.Put(blob) + if err != nil { + continue + } + meta := newBlobTxMeta(id, ptx.TxSize(), p.store.Size(id), ptx) - for _, id := range fails { + // If the newly inserted transaction fails to be tracked, + // it should also be removed with those in `toDelete` + sender, err := types.Sender(p.signer, ptx.Tx) + if err != nil { + toDelete = append(toDelete, id) + continue + } + if err := p.trackTransaction(meta, sender); err != nil { + toDelete = append(toDelete, id) + continue + } + } + } + + if len(toDelete) > 0 { + log.Warn("Dropping invalidated blob transactions", "ids", toDelete) + dropInvalidMeter.Mark(int64(len(toDelete))) + + for _, id := range toDelete { if err := p.store.Delete(id); err != nil { p.Close() return err } } } + // Sort the indexed transactions by nonce and delete anything gapped, create // the eviction heap of anyone still standing for addr := range p.index { @@ -558,36 +717,33 @@ func (p *BlobPool) Close() error { // parseTransaction is a callback method on pool creation that gets called for // each transaction on disk to create the in-memory metadata index. -// Announced state is not initialized here, it needs to be iniitalized seprately. +// Return value `bool` is set to true when the entry has old Transaction type. func (p *BlobPool) parseTransaction(id uint64, size uint32, blob []byte) error { - tx := new(types.Transaction) - if err := rlp.DecodeBytes(blob, tx); err != nil { - // This path is impossible unless the disk data representation changes - // across restarts. For that ever improbable case, recover gracefully - // by ignoring this data entry. - log.Error("Failed to decode blob pool entry", "id", id, "err", err) + var ptx blobTxForPool + if err := rlp.DecodeBytes(blob, &ptx); err != nil { + kind, content, _, splitErr := rlp.Split(blob) + // check whether it is legacy tx type + if splitErr == nil && kind == rlp.String && len(content) > 1 && content[0] == 3 { + return errLegacyTx + } return err } - if tx.BlobTxSidecar() == nil { - log.Error("Missing sidecar in blob pool entry", "id", id, "hash", tx.Hash()) - return errors.New("missing blob sidecar") + meta := newBlobTxMeta(id, ptx.TxSize(), size, &ptx) + sender, err := types.Sender(p.signer, ptx.Tx) + if err != nil { + return err } + return p.trackTransaction(meta, sender) +} - meta := newBlobTxMeta(id, tx.Size(), size, tx) +// trackTransaction registers a transaction's metadata in the pool's indices. +func (p *BlobPool) trackTransaction(meta *blobTxMeta, sender common.Address) error { if p.lookup.exists(meta.hash) { // This path is only possible after a crash, where deleted items are not // removed via the normal shutdown-startup procedure and thus may get // partially resurrected. - log.Error("Rejecting duplicate blob pool entry", "id", id, "hash", tx.Hash()) - return errors.New("duplicate blob entry") - } - sender, err := types.Sender(p.signer, tx) - if err != nil { - // This path is impossible unless the signature validity changes across - // restarts. For that ever improbable case, recover gracefully by ignoring - // this data entry. - log.Error("Failed to recover blob tx sender", "id", id, "hash", tx.Hash(), "err", err) - return err + log.Error("Rejecting duplicate blob pool entry", "id", meta.id, "hash", meta.hash) + return fmt.Errorf("duplicate blob entry %d, %s", meta.id, meta.hash) } if _, ok := p.index[sender]; !ok { if err := p.reserver.Hold(sender); err != nil { @@ -863,17 +1019,17 @@ func (p *BlobPool) offload(addr common.Address, nonce uint64, id uint64, inclusi log.Error("Blobs missing for included transaction", "from", addr, "nonce", nonce, "id", id, "err", err) return } - var tx types.Transaction - if err = rlp.DecodeBytes(data, &tx); err != nil { + var ptx blobTxForPool + if err := rlp.DecodeBytes(data, &ptx); err != nil { log.Error("Blobs corrupted for included transaction", "from", addr, "nonce", nonce, "id", id, "err", err) return } - block, ok := inclusions[tx.Hash()] + block, ok := inclusions[ptx.Tx.Hash()] if !ok { log.Warn("Blob transaction swapped out by signer", "from", addr, "nonce", nonce, "id", id) return } - if err := p.limbo.push(&tx, block); err != nil { + if err := p.limbo.push(&ptx, block); err != nil { log.Warn("Failed to offload blob tx into limbo", "err", err) return } @@ -1108,7 +1264,7 @@ func (p *BlobPool) reorg(oldHead, newHead *types.Header) (map[common.Address][]* func (p *BlobPool) reinject(addr common.Address, txhash common.Hash) error { // Retrieve the associated blob from the limbo. Without the blobs, we cannot // add the transaction back into the pool as it is not mineable. - tx, err := p.limbo.pull(txhash) + ptx, err := p.limbo.pull(txhash) if err != nil { log.Error("Blobs unavailable, dropping reorged tx", "err", err) return err @@ -1124,30 +1280,29 @@ func (p *BlobPool) reinject(addr common.Address, txhash common.Hash) error { // could theoretically halt a Geth node for ~1.2s by reorging per block. However, // this attack is financially inefficient to execute. head := p.head.Load() - if p.chain.Config().IsOsaka(head.Number, head.Time) && tx.BlobTxSidecar().Version == types.BlobSidecarVersion0 { - if err := tx.BlobTxSidecar().ToV1(); err != nil { + if p.chain.Config().IsOsaka(head.Number, head.Time) && ptx.Version == types.BlobSidecarVersion0 { + sc := ptx.Sidecar() + if err := sc.ToV1(); err != nil { log.Error("Failed to convert the legacy sidecar", "err", err) return err } - log.Info("Legacy blob transaction is reorged", "hash", tx.Hash()) + ptx.ApplySidecar(sc) + log.Info("Legacy blob transaction is reorged", "hash", ptx.Tx.Hash()) } - // Serialize the transaction back into the primary datastore. - blob, err := rlp.EncodeToBytes(tx) + blob, err := rlp.EncodeToBytes(ptx) if err != nil { - log.Error("Failed to encode transaction for storage", "hash", tx.Hash(), "err", err) + log.Error("Failed to encode transaction for storage", "hash", ptx.Tx.Hash(), "err", err) return err } id, err := p.store.Put(blob) if err != nil { - log.Error("Failed to write transaction into storage", "hash", tx.Hash(), "err", err) + log.Error("Failed to write transaction into storage", "hash", ptx.Tx.Hash(), "err", err) return err } - - // Update the indices and metrics - meta := newBlobTxMeta(id, tx.Size(), p.store.Size(id), tx) + meta := newBlobTxMeta(id, ptx.TxSize(), p.store.Size(id), ptx) if _, ok := p.index[addr]; !ok { if err := p.reserver.Hold(addr); err != nil { - log.Warn("Failed to reserve account for blob pool", "tx", tx.Hash(), "from", addr, "err", err) + log.Warn("Failed to reserve account for blob pool", "tx", ptx.Tx.Hash(), "from", addr, "err", err) return err } p.index[addr] = []*blobTxMeta{meta} @@ -1404,20 +1559,29 @@ func (p *BlobPool) Get(hash common.Hash) *types.Transaction { if len(data) == 0 { return nil } - item := new(types.Transaction) - if err := rlp.DecodeBytes(data, item); err != nil { + var ptx blobTxForPool + if err := rlp.DecodeBytes(data, &ptx); err != nil { id, _ := p.lookup.storeidOfTx(hash) log.Error("Blobs corrupted for traced transaction", "hash", hash, "id", id, "err", err) return nil } - return item + return ptx.ToTx() } -// GetRLP returns a RLP-encoded transaction if it is contained in the pool. +// GetRLP returns a RLP-encoded transaction for network if it is contained in the pool. +// It converts the pool's internal type to the RLP format used by the eth protocol: +// e.g. type_byte || [..., version, [blobs], [comms], [proofs]] func (p *BlobPool) GetRLP(hash common.Hash) []byte { - return p.getRLP(hash) + data := p.getRLP(hash) + rlp, err := encodeForNetwork(data) + if err != nil { + log.Error("Failed to encode pooled tx into the network type", "hash", hash, "err", err) + return nil + } + + return rlp } // GetMetadata returns the transaction type and transaction size with the @@ -1486,18 +1650,14 @@ func (p *BlobPool) GetBlobs(vhashes []common.Hash, version byte) ([]*kzg4844.Blo } // Decode the blob transaction - tx := new(types.Transaction) - if err := rlp.DecodeBytes(data, tx); err != nil { + var ptx blobTxForPool + if err := rlp.DecodeBytes(data, &ptx); err != nil { log.Error("Blobs corrupted for traced transaction", "id", txID, "err", err) continue } - sidecar := tx.BlobTxSidecar() - if sidecar == nil { - log.Error("Blob tx without sidecar", "hash", tx.Hash(), "id", txID) - continue - } + sidecar := ptx.Sidecar() // Traverse the blobs in the transaction - for i, hash := range tx.BlobHashes() { + for i, hash := range ptx.Tx.BlobHashes() { list, ok := indices[hash] if !ok { continue // non-interesting blob @@ -1644,7 +1804,8 @@ func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error } // Transaction permitted into the pool from a nonce and cost perspective, // insert it into the database and update the indices - blob, err := rlp.EncodeToBytes(tx) + ptx := newBlobTxForPool(tx) + blob, err := rlp.EncodeToBytes(ptx) if err != nil { log.Error("Failed to encode transaction for storage", "hash", tx.Hash(), "err", err) return err @@ -1653,7 +1814,7 @@ func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error if err != nil { return err } - meta := newBlobTxMeta(id, tx.Size(), p.store.Size(id), tx) + meta := newBlobTxMeta(id, tx.Size(), p.store.Size(id), ptx) var ( next = p.state.GetNonce(from) diff --git a/core/txpool/blobpool/blobpool_test.go b/core/txpool/blobpool/blobpool_test.go index 7c57755401..8032e21e8a 100644 --- a/core/txpool/blobpool/blobpool_test.go +++ b/core/txpool/blobpool/blobpool_test.go @@ -235,6 +235,12 @@ func makeTx(nonce uint64, gasTipCap uint64, gasFeeCap uint64, blobFeeCap uint64, return types.MustSignNewTx(key, types.LatestSigner(params.MainnetChainConfig), blobtx) } +// encodeForPool encodes a blob transaction in the blobTxForPool storage format. +func encodeForPool(tx *types.Transaction) []byte { + blob, _ := rlp.EncodeToBytes(newBlobTxForPool(tx)) + return blob +} + // makeMultiBlobTx is a utility method to construct a ramdom blob tx with // certain number of blobs in its sidecar. func makeMultiBlobTx(nonce uint64, gasTipCap uint64, gasFeeCap uint64, blobFeeCap uint64, blobCount int, blobOffset int, key *ecdsa.PrivateKey, version byte) *types.Transaction { @@ -530,7 +536,7 @@ func TestOpenDrops(t *testing.T) { ) for _, nonce := range []uint64{0, 1, 3, 4, 6, 7} { // first gap at #2, another at #5 tx := makeTx(nonce, 1, 1, 1, gapper) - blob, _ := rlp.EncodeToBytes(tx) + blob := encodeForPool(tx) id, _ := store.Put(blob) if nonce < 2 { @@ -547,7 +553,7 @@ func TestOpenDrops(t *testing.T) { ) for _, nonce := range []uint64{1, 2, 3} { // first gap at #0, all set dangling tx := makeTx(nonce, 1, 1, 1, dangler) - blob, _ := rlp.EncodeToBytes(tx) + blob := encodeForPool(tx) id, _ := store.Put(blob) dangling[id] = struct{}{} @@ -560,7 +566,7 @@ func TestOpenDrops(t *testing.T) { ) for _, nonce := range []uint64{0, 1, 2} { // account nonce at 3, all set filled tx := makeTx(nonce, 1, 1, 1, filler) - blob, _ := rlp.EncodeToBytes(tx) + blob := encodeForPool(tx) id, _ := store.Put(blob) filled[id] = struct{}{} @@ -573,7 +579,7 @@ func TestOpenDrops(t *testing.T) { ) for _, nonce := range []uint64{0, 1, 2, 3} { // account nonce at 2, half filled tx := makeTx(nonce, 1, 1, 1, overlapper) - blob, _ := rlp.EncodeToBytes(tx) + blob := encodeForPool(tx) id, _ := store.Put(blob) if nonce >= 2 { @@ -595,7 +601,7 @@ func TestOpenDrops(t *testing.T) { } else { tx = makeTx(uint64(i), 1, 1, 1, underpayer) } - blob, _ := rlp.EncodeToBytes(tx) + blob := encodeForPool(tx) id, _ := store.Put(blob) underpaid[id] = struct{}{} @@ -614,7 +620,7 @@ func TestOpenDrops(t *testing.T) { } else { tx = makeTx(uint64(i), 1, 1, 1, outpricer) } - blob, _ := rlp.EncodeToBytes(tx) + blob := encodeForPool(tx) id, _ := store.Put(blob) if i < 2 { @@ -636,7 +642,7 @@ func TestOpenDrops(t *testing.T) { } else { tx = makeTx(nonce, 1, 1, 1, exceeder) } - blob, _ := rlp.EncodeToBytes(tx) + blob := encodeForPool(tx) id, _ := store.Put(blob) exceeded[id] = struct{}{} @@ -654,7 +660,7 @@ func TestOpenDrops(t *testing.T) { } else { tx = makeTx(nonce, 1, 1, 1, overdrafter) } - blob, _ := rlp.EncodeToBytes(tx) + blob := encodeForPool(tx) id, _ := store.Put(blob) if nonce < 1 { @@ -670,7 +676,7 @@ func TestOpenDrops(t *testing.T) { overcapped = make(map[uint64]struct{}) ) for nonce := uint64(0); nonce < maxTxsPerAccount+3; nonce++ { - blob, _ := rlp.EncodeToBytes(makeTx(nonce, 1, 1, 1, overcapper)) + blob := encodeForPool(makeTx(nonce, 1, 1, 1, overcapper)) id, _ := store.Put(blob) if nonce < maxTxsPerAccount { @@ -686,7 +692,7 @@ func TestOpenDrops(t *testing.T) { duplicated = make(map[uint64]struct{}) ) for _, nonce := range []uint64{0, 1, 2} { - blob, _ := rlp.EncodeToBytes(makeTx(nonce, 1, 1, 1, duplicater)) + blob := encodeForPool(makeTx(nonce, 1, 1, 1, duplicater)) for i := 0; i < int(nonce)+1; i++ { id, _ := store.Put(blob) @@ -705,7 +711,7 @@ func TestOpenDrops(t *testing.T) { ) for _, nonce := range []uint64{0, 1, 2} { for i := 0; i < int(nonce)+1; i++ { - blob, _ := rlp.EncodeToBytes(makeTx(nonce, 1, uint64(i)+1 /* unique hashes */, 1, repeater)) + blob := encodeForPool(makeTx(nonce, 1, uint64(i)+1 /* unique hashes */, 1, repeater)) id, _ := store.Put(blob) if i == 0 { @@ -842,7 +848,7 @@ func TestOpenIndex(t *testing.T) { ) for _, i := range []int{5, 3, 4, 2, 0, 1} { // Randomize the tx insertion order to force sorting on load tx := makeTx(uint64(i), txExecTipCaps[i], txExecFeeCaps[i], txBlobFeeCaps[i], key) - blob, _ := rlp.EncodeToBytes(tx) + blob := encodeForPool(tx) store.Put(blob) } store.Close() @@ -934,9 +940,9 @@ func TestOpenHeap(t *testing.T) { tx2 = makeTx(0, 1, 800, 70, key2) tx3 = makeTx(0, 1, 1500, 110, key3) - blob1, _ = rlp.EncodeToBytes(tx1) - blob2, _ = rlp.EncodeToBytes(tx2) - blob3, _ = rlp.EncodeToBytes(tx3) + blob1 = encodeForPool(tx1) + blob2 = encodeForPool(tx2) + blob3 = encodeForPool(tx3) heapOrder = []common.Address{addr2, addr1, addr3} heapIndex = map[common.Address]int{addr2: 0, addr1: 1, addr3: 2} @@ -1009,9 +1015,9 @@ func TestOpenCap(t *testing.T) { tx2 = makeTx(0, 1, 800, 70, key2) tx3 = makeTx(0, 1, 1500, 110, key3) - blob1, _ = rlp.EncodeToBytes(tx1) - blob2, _ = rlp.EncodeToBytes(tx2) - blob3, _ = rlp.EncodeToBytes(tx3) + blob1 = encodeForPool(tx1) + blob2 = encodeForPool(tx2) + blob3 = encodeForPool(tx3) keep = []common.Address{addr1, addr3} drop = []common.Address{addr2} @@ -1098,8 +1104,8 @@ func TestChangingSlotterSize(t *testing.T) { tx2 = makeMultiBlobTx(0, 1, 800, 70, 6, 0, key2, types.BlobSidecarVersion0) tx3 = makeMultiBlobTx(0, 1, 800, 110, 24, 0, key3, types.BlobSidecarVersion0) - blob1, _ = rlp.EncodeToBytes(tx1) - blob2, _ = rlp.EncodeToBytes(tx2) + blob1 = encodeForPool(tx1) + blob2 = encodeForPool(tx2) ) // Write the two safely sized txs to store. note: although the store is @@ -1201,8 +1207,8 @@ func TestBillyMigration(t *testing.T) { tx2 = makeMultiBlobTx(0, 1, 800, 70, 6, 0, key2, types.BlobSidecarVersion0) tx3 = makeMultiBlobTx(0, 1, 800, 110, 24, 0, key3, types.BlobSidecarVersion0) - blob1, _ = rlp.EncodeToBytes(tx1) - blob2, _ = rlp.EncodeToBytes(tx2) + blob1 = encodeForPool(tx1) + blob2 = encodeForPool(tx2) ) // Write the two safely sized txs to store. note: although the store is @@ -1281,6 +1287,85 @@ func TestBillyMigration(t *testing.T) { } } +// TestLegacyTxConversion verifies that on Init, transactions stored in the +// legacy *types.Transaction RLP format are detected and migrated into the new +// blobTxForPool storage format, and that they remain retrievable via the pool +// API after the conversion. +func TestLegacyTxConversion(t *testing.T) { + storage := t.TempDir() + os.MkdirAll(filepath.Join(storage, pendingTransactionStore), 0700) + os.MkdirAll(filepath.Join(storage, limboedTransactionStore), 0700) + + // Initialize the pending store with two blob transactions encoded in the + // legacy format. + queuedir := filepath.Join(storage, pendingTransactionStore) + store, err := billy.Open(billy.Options{Path: queuedir}, newSlotter(testMaxBlobsPerBlock), nil) + if err != nil { + t.Fatalf("failed to open billy: %v", err) + } + + key1, _ := crypto.GenerateKey() + key2, _ := crypto.GenerateKey() + addr1 := crypto.PubkeyToAddress(key1.PublicKey) + addr2 := crypto.PubkeyToAddress(key2.PublicKey) + + tx1 := makeMultiBlobTx(0, 1, 1000, 100, 2, 0, key1, types.BlobSidecarVersion0) + tx2 := makeMultiBlobTx(0, 1, 1000, 100, 2, 2, key2, types.BlobSidecarVersion0) + + for _, tx := range []*types.Transaction{tx1, tx2} { + legacy, err := rlp.EncodeToBytes(tx) + if err != nil { + t.Fatalf("failed to legacy-encode tx: %v", err) + } + if _, err := store.Put(legacy); err != nil { + t.Fatalf("failed to put legacy blob: %v", err) + } + } + store.Close() + + // Init should migrate the legacy entries into the new storage format. + statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting()) + statedb.AddBalance(addr1, uint256.NewInt(1_000_000_000), tracing.BalanceChangeUnspecified) + statedb.AddBalance(addr2, uint256.NewInt(1_000_000_000), tracing.BalanceChangeUnspecified) + statedb.Commit(0, true, false) + + chain := &testBlockChain{ + config: params.MainnetChainConfig, + basefee: uint256.NewInt(params.InitialBaseFee), + blobfee: uint256.NewInt(params.BlobTxMinBlobGasprice), + statedb: statedb, + } + pool := New(Config{Datadir: storage}, chain, nil) + if err := pool.Init(1, chain.CurrentBlock(), newReserver()); err != nil { + t.Fatalf("failed to create blob pool: %v", err) + } + defer pool.Close() + + // Both transactions should be retrievable. + for _, want := range []*types.Transaction{tx1, tx2} { + got := pool.Get(want.Hash()) + if got == nil { + t.Fatalf("migrated tx %s not found in pool", want.Hash()) + } + if got.BlobTxSidecar() == nil { + t.Fatalf("migrated tx %s lost its sidecar", want.Hash()) + } + if got.Hash() != want.Hash() { + t.Fatalf("migrated tx hash mismatch: have %s, want %s", got.Hash(), want.Hash()) + } + } + + // Legacy formats should not exist on pool.store + pool.store.Iterate(func(id uint64, size uint32, blob []byte) { + var ptx blobTxForPool + if err := rlp.DecodeBytes(blob, &ptx); err != nil { + t.Errorf("entry %d not in new blobTxForPool format: %v", id, err) + } + }) + + verifyPoolInternals(t, pool) +} + // TestBlobCountLimit tests the blobpool enforced limits on the max blob count. func TestBlobCountLimit(t *testing.T) { var ( @@ -1746,7 +1831,7 @@ func TestAdd(t *testing.T) { // Sign the seed transactions and store them in the data store for _, tx := range seed.txs { signed := types.MustSignNewTx(keys[acc], types.LatestSigner(params.MainnetChainConfig), tx) - blob, _ := rlp.EncodeToBytes(signed) + blob := encodeForPool(signed) store.Put(blob) } } @@ -1853,9 +1938,9 @@ func TestGetBlobs(t *testing.T) { tx2 = makeMultiBlobTx(0, 1, 800, 70, 6, 6, key2, types.BlobSidecarVersion1) // [6, 12) tx3 = makeMultiBlobTx(0, 1, 800, 110, 6, 12, key3, types.BlobSidecarVersion0) // [12, 18) - blob1, _ = rlp.EncodeToBytes(tx1) - blob2, _ = rlp.EncodeToBytes(tx2) - blob3, _ = rlp.EncodeToBytes(tx3) + blob1 = encodeForPool(tx1) + blob2 = encodeForPool(tx2) + blob3 = encodeForPool(tx3) ) // Write the two safely sized txs to store. note: although the store is @@ -2055,6 +2140,32 @@ func TestGetBlobs(t *testing.T) { pool.Close() } +// TestEncodeForNetwork verifies that encodeForNetwork produces output identical +// to rlp.EncodeToBytes on the original transaction, for both V0 and V1 sidecars. +func TestEncodeForNetwork(t *testing.T) { + t.Run("v0", func(t *testing.T) { testEncodeForNetwork(t, types.BlobSidecarVersion0) }) + t.Run("v1", func(t *testing.T) { testEncodeForNetwork(t, types.BlobSidecarVersion1) }) +} + +func testEncodeForNetwork(t *testing.T, version byte) { + key, _ := crypto.GenerateKey() + tx := makeMultiBlobTx(0, 1, 1, 1, 1, 0, key, version) + + wantRLP, err := rlp.EncodeToBytes(tx) + if err != nil { + t.Fatalf("failed to encode tx: %v", err) + } + storedRLP := encodeForPool(tx) + + gotRLP, err := encodeForNetwork(storedRLP) + if err != nil { + t.Fatalf("encodeForNetwork failed: %v", err) + } + if !bytes.Equal(gotRLP, wantRLP) { + t.Fatalf("network encoding mismatch (version %d): got %d bytes, want %d bytes", version, len(gotRLP), len(wantRLP)) + } +} + // fakeBilly is a billy.Database implementation which just drops data on the floor. type fakeBilly struct { billy.Database diff --git a/core/txpool/blobpool/limbo.go b/core/txpool/blobpool/limbo.go index 36284d6a03..b8bee2f22a 100644 --- a/core/txpool/blobpool/limbo.go +++ b/core/txpool/blobpool/limbo.go @@ -33,7 +33,7 @@ import ( type limboBlob struct { TxHash common.Hash // Owner transaction's hash to support resurrecting reorged txs Block uint64 // Block in which the blob transaction was included - Tx *types.Transaction + Ptx *blobTxForPool } // limbo is a light, indexed database to temporarily store recently included @@ -146,15 +146,14 @@ func (l *limbo) finalize(final *types.Header) { // push stores a new blob transaction into the limbo, waiting until finality for // it to be automatically evicted. -func (l *limbo) push(tx *types.Transaction, block uint64) error { - // If the blobs are already tracked by the limbo, consider it a programming - // error. There's not much to do against it, but be loud. - if _, ok := l.index[tx.Hash()]; ok { - log.Error("Limbo cannot push already tracked blobs", "tx", tx.Hash()) +func (l *limbo) push(ptx *blobTxForPool, block uint64) error { + hash := ptx.Tx.Hash() + if _, ok := l.index[hash]; ok { + log.Error("Limbo cannot push already tracked blobs", "tx", hash) return errors.New("already tracked blob transaction") } - if err := l.setAndIndex(tx, block); err != nil { - log.Error("Failed to set and index limboed blobs", "tx", tx.Hash(), "err", err) + if err := l.setAndIndex(ptx, block); err != nil { + log.Error("Failed to set and index limboed blobs", "tx", hash, "err", err) return err } return nil @@ -163,7 +162,7 @@ func (l *limbo) push(tx *types.Transaction, block uint64) error { // pull retrieves a previously pushed set of blobs back from the limbo, removing // it at the same time. This method should be used when a previously included blob // transaction gets reorged out. -func (l *limbo) pull(tx common.Hash) (*types.Transaction, error) { +func (l *limbo) pull(tx common.Hash) (*blobTxForPool, error) { // If the blobs are not tracked by the limbo, there's not much to do. This // can happen for example if a blob transaction is mined without pushing it // into the network first. @@ -177,7 +176,7 @@ func (l *limbo) pull(tx common.Hash) (*types.Transaction, error) { log.Error("Failed to get and drop limboed blobs", "tx", tx, "id", id, "err", err) return nil, err } - return item.Tx, nil + return item.Ptx, nil } // update changes the block number under which a blob transaction is tracked. This @@ -209,7 +208,7 @@ func (l *limbo) update(txhash common.Hash, block uint64) { log.Error("Failed to get and drop limboed blobs", "tx", txhash, "id", id, "err", err) return } - if err := l.setAndIndex(item.Tx, block); err != nil { + if err := l.setAndIndex(item.Ptx, block); err != nil { log.Error("Failed to set and index limboed blobs", "tx", txhash, "err", err) return } @@ -240,12 +239,12 @@ func (l *limbo) getAndDrop(id uint64) (*limboBlob, error) { // setAndIndex assembles a limbo blob database entry and stores it, also updating // the in-memory indices. -func (l *limbo) setAndIndex(tx *types.Transaction, block uint64) error { - txhash := tx.Hash() +func (l *limbo) setAndIndex(ptx *blobTxForPool, block uint64) error { + txhash := ptx.Tx.Hash() item := &limboBlob{ TxHash: txhash, Block: block, - Tx: tx, + Ptx: ptx, } data, err := rlp.EncodeToBytes(item) if err != nil { From 91f8e7cd9ef38e9e87385557840179d8335ef38b Mon Sep 17 00:00:00 2001 From: cui Date: Tue, 12 May 2026 20:49:33 +0800 Subject: [PATCH 48/63] internal/ethapi: add balHash to block results (#34652) --- internal/ethapi/api.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 6fc43a370b..6d38c6c7c8 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -996,6 +996,9 @@ func RPCMarshalHeader(head *types.Header) map[string]interface{} { if head.RequestsHash != nil { result["requestsHash"] = head.RequestsHash } + if head.BlockAccessListHash != nil { + result["balHash"] = head.BlockAccessListHash + } if head.SlotNumber != nil { result["slotNumber"] = hexutil.Uint64(*head.SlotNumber) } From 6af374e6aa35d9a6be18184ffa831815dbc42155 Mon Sep 17 00:00:00 2001 From: cui Date: Tue, 12 May 2026 20:50:04 +0800 Subject: [PATCH 49/63] accounts/abi: fix unittest code (#34740) 1. should use !reflect.DeepEqual. 2. big.NewInt(0).SetBits([]big.Word{}) work around for DeepEqual when big.Int is zero, unpack return a []big.Word{}. --- accounts/abi/unpack_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/accounts/abi/unpack_test.go b/accounts/abi/unpack_test.go index 90713c03ca..90cfa68655 100644 --- a/accounts/abi/unpack_test.go +++ b/accounts/abi/unpack_test.go @@ -910,7 +910,7 @@ func TestUnpackTuple(t *testing.T) { }, }, FieldT: T{ - big.NewInt(0), big.NewInt(1), + big.NewInt(0).SetBits([]big.Word{}), big.NewInt(1), }, A: big.NewInt(1), } @@ -919,7 +919,7 @@ func TestUnpackTuple(t *testing.T) { if err != nil { t.Error(err) } - if reflect.DeepEqual(ret, expected) { + if !reflect.DeepEqual(ret, expected) { t.Error("unexpected unpack value") } } From 21c5a287f91640f383bc09ffe97d922999e171c7 Mon Sep 17 00:00:00 2001 From: Richard Creighton Date: Tue, 12 May 2026 14:02:40 +0100 Subject: [PATCH 50/63] cmd/abigen: respect --v2=false (#34943) Passing `--v2=false` currently still selects the v2 binding generator because the command checks whether the flag was set. This switches generation to use the boolean flag value, so explicit false continues to generate legacy bindings while `--v2` keeps selecting v2. --- cmd/abigen/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/abigen/main.go b/cmd/abigen/main.go index c82358be49..d9d1fa02ac 100644 --- a/cmd/abigen/main.go +++ b/cmd/abigen/main.go @@ -215,7 +215,7 @@ func generate(c *cli.Context) error { code string err error ) - if c.IsSet(v2Flag.Name) { + if c.Bool(v2Flag.Name) { code, err = abigen.BindV2(types, abis, bins, c.String(pkgFlag.Name), libs, aliases) } else { code, err = abigen.Bind(types, abis, bins, sigs, c.String(pkgFlag.Name), libs, aliases) From 0494cdce23dcbad953f488a62b15eb024cdc7e16 Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Wed, 13 May 2026 16:53:47 +0800 Subject: [PATCH 51/63] core: introduce GasChangeHook v2 (#34946) This PR introduces OnGasChangeV2 tracing hook, as the pre-requisite for landing EIP-8037. --------- Co-authored-by: Sina M <1591639+s1na@users.noreply.github.com> --- core/state_transition.go | 27 +++++++----- core/tracing/hooks.go | 87 ++++++++++++++++++++++++++++++++++---- core/vm/contract.go | 8 ++-- core/vm/contracts.go | 4 +- core/vm/evm.go | 38 +++++++++-------- core/vm/gascosts.go | 29 ++++++++----- core/vm/interpreter.go | 8 +++- eth/tracers/live/noop.go | 4 ++ eth/tracers/native/mux.go | 19 +++++---- eth/tracers/native/noop.go | 3 ++ 10 files changed, 164 insertions(+), 63 deletions(-) diff --git a/core/state_transition.go b/core/state_transition.go index fcd483eeb7..0a6994505d 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -420,8 +420,10 @@ func (st *stateTransition) buyGas() error { return err } - if st.evm.Config.Tracer != nil && st.evm.Config.Tracer.OnGasChange != nil { - st.evm.Config.Tracer.OnGasChange(0, st.msg.GasLimit, tracing.GasChangeTxInitialBalance) + if st.evm.Config.Tracer.HasGasHook() { + empty := vm.GasBudget{} + initial := vm.NewGasBudget(st.msg.GasLimit) + st.evm.Config.Tracer.EmitGasChange(empty.AsTracing(), initial.AsTracing(), tracing.GasChangeTxInitialBalance) } st.gasRemaining = vm.NewGasBudget(st.msg.GasLimit) st.initialBudget = st.gasRemaining.Copy() @@ -566,8 +568,8 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { if !sufficient { return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, st.gasRemaining.RegularGas, cost.RegularGas) } - if t := st.evm.Config.Tracer; t != nil && t.OnGasChange != nil { - t.OnGasChange(prior, st.gasRemaining.RegularGas, tracing.GasChangeTxIntrinsicGas) + if st.evm.Config.Tracer.HasGasHook() { + st.evm.Config.Tracer.EmitGasChange(prior.AsTracing(), st.gasRemaining.AsTracing(), tracing.GasChangeTxIntrinsicGas) } // Gas limit suffices for the floor data cost (EIP-7623) if rules.IsPrague { @@ -651,8 +653,8 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { // After EIP-7623: Data-heavy transactions pay the floor gas. if used := st.gasUsed(); used < floorDataGas { prior, _ := st.gasRemaining.Charge(vm.GasCosts{RegularGas: floorDataGas - used}) - if t := st.evm.Config.Tracer; t != nil && t.OnGasChange != nil { - t.OnGasChange(prior, st.gasRemaining.RegularGas, tracing.GasChangeTxDataFloor) + if st.evm.Config.Tracer.HasGasHook() { + st.evm.Config.Tracer.EmitGasChange(prior.AsTracing(), st.gasRemaining.AsTracing(), tracing.GasChangeTxDataFloor) } } if peakGasUsed < floorDataGas { @@ -780,8 +782,11 @@ func (st *stateTransition) calcRefund() vm.GasBudget { if refund > st.state.GetRefund() { refund = st.state.GetRefund() } - if st.evm.Config.Tracer != nil && st.evm.Config.Tracer.OnGasChange != nil && refund > 0 { - st.evm.Config.Tracer.OnGasChange(st.gasRemaining.RegularGas, st.gasRemaining.RegularGas+refund, tracing.GasChangeTxRefunds) + if refund > 0 && st.evm.Config.Tracer.HasGasHook() { + after := st.gasRemaining + after.RegularGas += refund + + st.evm.Config.Tracer.EmitGasChange(st.gasRemaining.AsTracing(), after.AsTracing(), tracing.GasChangeTxRefunds) } return vm.NewGasBudget(refund) } @@ -793,8 +798,10 @@ func (st *stateTransition) returnGas() { remaining.Mul(remaining, st.msg.GasPrice) st.state.AddBalance(st.msg.From, remaining, tracing.BalanceIncreaseGasReturn) - if st.evm.Config.Tracer != nil && st.evm.Config.Tracer.OnGasChange != nil && st.gasRemaining.RegularGas > 0 { - st.evm.Config.Tracer.OnGasChange(st.gasRemaining.RegularGas, 0, tracing.GasChangeTxLeftOverReturned) + if st.gasRemaining.RegularGas > 0 && st.evm.Config.Tracer.HasGasHook() { + after := st.gasRemaining + after.RegularGas = 0 + st.evm.Config.Tracer.EmitGasChange(st.gasRemaining.AsTracing(), after.AsTracing(), tracing.GasChangeTxLeftOverReturned) } } diff --git a/core/tracing/hooks.go b/core/tracing/hooks.go index 62c62ac3b3..6ea3f7ebbf 100644 --- a/core/tracing/hooks.go +++ b/core/tracing/hooks.go @@ -164,10 +164,36 @@ type ( // FaultHook is invoked when an error occurs during the execution of an opcode. FaultHook = func(pc uint64, op byte, gas, cost uint64, scope OpContext, depth int, err error) - // GasChangeHook is invoked when the gas changes. + // GasChangeHook reports changes to the regular execution gas. Tracers + // that don't need visibility into the state-access gas dimension + // introduced by EIP-8037 (Amsterdam) can implement only this hook; it + // will continue to fire across the Amsterdam fork unchanged. + // + // If both this hook and GasChangeHookV2 are implemented on the same + // tracer, only V2 will be invoked. Implement exactly one to avoid + // double-counting. GasChangeHook = func(old, new uint64, reason GasChangeReason) - // TODO(sina, rjl), please add GasChangeV2Hook by landing the multi-dimensional gas + // GasChangeHookV2 is invoked when any gas dimension changes. It is the + // multi-dimensional successor to GasChangeHook, exposing the state-access + // gas dimension introduced by EIP-8037 (Amsterdam) alongside the regular + // dimension. + // + // Compatibility: + // - Post-Amsterdam: fires for changes to either the regular or the + // state-access dimension. The non-changing dimension is passed through + // unchanged in both `old` and `new` so consumers always observe the + // complete gas vector. + // - Pre-Amsterdam: no state-access gas events occur, so the State field + // of both `old` and `new` is always zero. Tracers that register only + // V2 still receive every regular-gas change as Gas{State: 0} and + // behave identically to a V1 tracer; there is no pre-Amsterdam event + // a V2-only tracer misses. + // + // V1 and V2 coexist: when both are registered on a tracer, only V2 is + // invoked. Tracers SHOULD register at most one of the two to avoid + // double-counting. + GasChangeHookV2 = func(old, new Gas, reason GasChangeReason) /* - Chain events - @@ -250,13 +276,14 @@ type ( type Hooks struct { // VM events - OnTxStart TxStartHook - OnTxEnd TxEndHook - OnEnter EnterHook - OnExit ExitHook - OnOpcode OpcodeHook - OnFault FaultHook - OnGasChange GasChangeHook + OnTxStart TxStartHook + OnTxEnd TxEndHook + OnEnter EnterHook + OnExit ExitHook + OnOpcode OpcodeHook + OnFault FaultHook + OnGasChange GasChangeHook + OnGasChangeV2 GasChangeHookV2 // Chain events OnBlockchainInit BlockchainInitHook OnClose CloseHook @@ -280,6 +307,35 @@ type Hooks struct { OnBlockHashRead BlockHashReadHook } +// HasGasHook reports whether any gas-change hook is registered. Call sites +// should use this to short-circuit before constructing the Gas / GasBudget +// arguments to EmitGasChange when tracing is off — the dispatch is otherwise +// always paid the cost of evaluating those args. +func (h *Hooks) HasGasHook() bool { + return h != nil && (h.OnGasChangeV2 != nil || h.OnGasChange != nil) +} + +// EmitGasChange dispatches a gas change event to the registered hooks. If the +// multi-dimensional OnGasChangeV2 hook is set it is invoked with the full Gas +// vectors; otherwise the single-dimensional OnGasChange hook is invoked with +// the regular-gas dimension only. The call is a no-op when the receiver is +// nil, when neither hook is registered, or when the reason is GasChangeIgnored. +// +// Call sites SHOULD use this helper instead of invoking the hooks directly so +// that both variants stay consistent across the Amsterdam fork boundary. +func (h *Hooks) EmitGasChange(old, new Gas, reason GasChangeReason) { + if h == nil || reason == GasChangeIgnored { + return + } + if h.OnGasChangeV2 != nil { + h.OnGasChangeV2(old, new, reason) + return + } + if h.OnGasChange != nil { + h.OnGasChange(old.Regular, new.Regular, reason) + } +} + // BalanceChangeReason is used to indicate the reason for a balance change, useful // for tracing and reporting. type BalanceChangeReason byte @@ -335,6 +391,19 @@ const ( BalanceChangeRevert BalanceChangeReason = 15 ) +// Gas represents a multi-dimensional gas budget introduced by EIP-8037. +// It carries the regular execution gas and the state-access gas, which are +// metered independently from the Amsterdam fork onwards. +// +// Before Amsterdam, gas metering is single-dimensional and only the Regular +// field is meaningful; State is always zero. The struct is shaped so that +// pre-Amsterdam call sites can populate it as Gas{Regular: g} without loss +// of fidelity relative to the legacy single-uint64 hook. +type Gas struct { + Regular uint64 // Regular is the budget for ordinary execution gas. + State uint64 // State is the budget dedicated to state-access gas (zero pre-Amsterdam). +} + // GasChangeReason is used to indicate the reason for a gas change, useful // for tracing and reporting. // diff --git a/core/vm/contract.go b/core/vm/contract.go index a55a5dde8b..45c879c80f 100644 --- a/core/vm/contract.go +++ b/core/vm/contract.go @@ -131,8 +131,8 @@ func (c *Contract) UseGas(cost GasCosts, logger *tracing.Hooks, reason tracing.G if !ok { return false } - if logger != nil && logger.OnGasChange != nil && reason != tracing.GasChangeIgnored { - logger.OnGasChange(prior, c.Gas.RegularGas, reason) + if logger.HasGasHook() && reason != tracing.GasChangeIgnored { + logger.EmitGasChange(prior.AsTracing(), c.Gas.AsTracing(), reason) } return true } @@ -143,8 +143,8 @@ func (c *Contract) RefundGas(refund GasBudget, logger *tracing.Hooks, reason tra if !changed { return } - if logger != nil && logger.OnGasChange != nil && reason != tracing.GasChangeIgnored { - logger.OnGasChange(prior, c.Gas.RegularGas, reason) + if logger.HasGasHook() && reason != tracing.GasChangeIgnored { + logger.EmitGasChange(prior.AsTracing(), c.Gas.AsTracing(), reason) } } diff --git a/core/vm/contracts.go b/core/vm/contracts.go index 6dadb64873..71cfdbc527 100644 --- a/core/vm/contracts.go +++ b/core/vm/contracts.go @@ -269,8 +269,8 @@ func RunPrecompiledContract(stateDB StateDB, p PrecompiledContract, address comm gas.Exhaust() return nil, gas, ErrOutOfGas } - if logger != nil && logger.OnGasChange != nil { - logger.OnGasChange(prior, gas.RegularGas, tracing.GasChangeCallPrecompiledContract) + if logger.HasGasHook() { + logger.EmitGasChange(prior.AsTracing(), gas.AsTracing(), tracing.GasChangeCallPrecompiledContract) } // Touch the precompile for block-level accessList recording once Amsterdam // fork is activated. diff --git a/core/vm/evm.go b/core/vm/evm.go index 26b2f73a00..9fe6faa3a2 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -317,8 +317,8 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g if err != nil { evm.StateDB.RevertToSnapshot(snapshot) if err != ErrExecutionReverted { - if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { - evm.Config.Tracer.OnGasChange(gas.RegularGas, 0, tracing.GasChangeCallFailedExecution) + if evm.Config.Tracer.HasGasHook() { + evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution) } gas.Exhaust() } @@ -371,8 +371,8 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt if err != nil { evm.StateDB.RevertToSnapshot(snapshot) if err != ErrExecutionReverted { - if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { - evm.Config.Tracer.OnGasChange(gas.RegularGas, 0, tracing.GasChangeCallFailedExecution) + if evm.Config.Tracer.HasGasHook() { + evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution) } gas.Exhaust() } @@ -415,8 +415,8 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, if err != nil { evm.StateDB.RevertToSnapshot(snapshot) if err != ErrExecutionReverted { - if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { - evm.Config.Tracer.OnGasChange(gas.RegularGas, 0, tracing.GasChangeCallFailedExecution) + if evm.Config.Tracer.HasGasHook() { + evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution) } gas.Exhaust() } @@ -470,8 +470,8 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b if err != nil { evm.StateDB.RevertToSnapshot(snapshot) if err != ErrExecutionReverted { - if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { - evm.Config.Tracer.OnGasChange(gas.RegularGas, 0, tracing.GasChangeCallFailedExecution) + if evm.Config.Tracer.HasGasHook() { + evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution) } gas.Exhaust() } @@ -509,8 +509,8 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value gas.Exhaust() return nil, common.Address{}, gas, ErrOutOfGas } - if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { - evm.Config.Tracer.OnGasChange(prior, gas.RegularGas, tracing.GasChangeWitnessContractCollisionCheck) + if evm.Config.Tracer.HasGasHook() { + evm.Config.Tracer.EmitGasChange(prior.AsTracing(), gas.AsTracing(), tracing.GasChangeWitnessContractCollisionCheck) } } @@ -528,8 +528,8 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value if evm.StateDB.GetNonce(address) != 0 || (contractHash != (common.Hash{}) && contractHash != types.EmptyCodeHash) || // non-empty code isEIP7610RejectedAccount(evm.ChainConfig().ChainID, address, evm.chainRules.IsEIP158) { - if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { - evm.Config.Tracer.OnGasChange(gas.RegularGas, 0, tracing.GasChangeCallFailedExecution) + if evm.Config.Tracer.HasGasHook() { + evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution) } gas.Exhaust() return nil, common.Address{}, gas, ErrContractAddressCollision @@ -558,8 +558,8 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value return nil, common.Address{}, gas, ErrOutOfGas } prior, _ := gas.Charge(GasCosts{RegularGas: consumed}) - if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { - evm.Config.Tracer.OnGasChange(prior, gas.RegularGas, tracing.GasChangeWitnessContractInit) + if evm.Config.Tracer.HasGasHook() { + evm.Config.Tracer.EmitGasChange(prior.AsTracing(), gas.AsTracing(), tracing.GasChangeWitnessContractInit) } } evm.Context.Transfer(evm.StateDB, caller, address, value, &evm.chainRules) @@ -673,15 +673,17 @@ func (evm *EVM) captureBegin(depth int, typ OpCode, from common.Address, to comm if tracer.OnEnter != nil { tracer.OnEnter(depth, byte(typ), from, to, input, startGas, value) } - if tracer.OnGasChange != nil { - tracer.OnGasChange(0, startGas, tracing.GasChangeCallInitialBalance) + if tracer.HasGasHook() { + initial := NewGasBudget(startGas) + tracer.EmitGasChange(tracing.Gas{}, initial.AsTracing(), tracing.GasChangeCallInitialBalance) } } func (evm *EVM) captureEnd(depth int, startGas uint64, leftOverGas uint64, ret []byte, err error) { tracer := evm.Config.Tracer - if leftOverGas != 0 && tracer.OnGasChange != nil { - tracer.OnGasChange(leftOverGas, 0, tracing.GasChangeCallLeftOverReturned) + if leftOverGas != 0 && tracer.HasGasHook() { + leftover := NewGasBudget(leftOverGas) + tracer.EmitGasChange(leftover.AsTracing(), tracing.Gas{}, tracing.GasChangeCallLeftOverReturned) } var reverted bool if err != nil { diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go index cc90c54798..ed938ae41f 100644 --- a/core/vm/gascosts.go +++ b/core/vm/gascosts.go @@ -16,7 +16,11 @@ package vm -import "fmt" +import ( + "fmt" + + "github.com/ethereum/go-ethereum/core/tracing" +) // GasCosts denotes a vector of gas costs in the // multidimensional metering paradigm. It represents the cost @@ -77,21 +81,26 @@ func (g GasBudget) CanAfford(cost GasCosts) bool { } // Charge deducts the given gas cost from the budget. It returns the -// pre-charge gas value and false if the budget does not have sufficient +// pre-charge budget and false if the budget does not have sufficient // gas to cover the cost. -func (g *GasBudget) Charge(cost GasCosts) (uint64, bool) { - prior := g.RegularGas - if prior < cost.RegularGas { +func (g *GasBudget) Charge(cost GasCosts) (GasBudget, bool) { + prior := *g + if g.RegularGas < cost.RegularGas { return prior, false } g.RegularGas -= cost.RegularGas return prior, true } -// Refund adds the given gas budget back. It returns the pre-refund gas -// value and whether the budget was actually changed. -func (g *GasBudget) Refund(other GasBudget) (uint64, bool) { - prior := g.RegularGas +// Refund adds the given gas budget back. It returns the pre-refund budget +// and whether the budget was actually changed. +func (g *GasBudget) Refund(other GasBudget) (GasBudget, bool) { + prior := *g g.RegularGas += other.RegularGas - return prior, g.RegularGas != prior + return prior, g.RegularGas != prior.RegularGas +} + +// AsTracing converts the GasBudget into the tracing-facing Gas vector. +func (g GasBudget) AsTracing() tracing.Gas { + return tracing.Gas{Regular: g.RegularGas, State: g.StateGas} } diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index 4c278fc857..3994327247 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -234,8 +234,12 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte // Do tracing before potential memory expansion if debug { - if evm.Config.Tracer.OnGasChange != nil { - evm.Config.Tracer.OnGasChange(gasCopy, gasCopy-cost, tracing.GasChangeCallOpCode) + if evm.Config.Tracer.HasGasHook() { + evm.Config.Tracer.EmitGasChange( + tracing.Gas{Regular: gasCopy, State: contract.Gas.StateGas}, + tracing.Gas{Regular: gasCopy - cost, State: contract.Gas.StateGas}, + tracing.GasChangeCallOpCode, + ) } if evm.Config.Tracer.OnOpcode != nil { evm.Config.Tracer.OnOpcode(pc, byte(op), gasCopy, cost, callContext, evm.returnData, evm.depth, VMErrorFromErr(err)) diff --git a/eth/tracers/live/noop.go b/eth/tracers/live/noop.go index f3def85606..b1784dbd91 100644 --- a/eth/tracers/live/noop.go +++ b/eth/tracers/live/noop.go @@ -47,6 +47,7 @@ func newNoopTracer(_ json.RawMessage) (*tracing.Hooks, error) { OnOpcode: t.OnOpcode, OnFault: t.OnFault, OnGasChange: t.OnGasChange, + OnGasChangeV2: t.OnGasChangeV2, OnBlockchainInit: t.OnBlockchainInit, OnBlockStart: t.OnBlockStart, OnBlockEnd: t.OnBlockEnd, @@ -113,3 +114,6 @@ func (t *noop) OnBlockHashRead(number uint64, hash common.Hash) {} func (t *noop) OnGasChange(old, new uint64, reason tracing.GasChangeReason) { } + +func (t *noop) OnGasChangeV2(old, new tracing.Gas, reason tracing.GasChangeReason) { +} diff --git a/eth/tracers/native/mux.go b/eth/tracers/native/mux.go index bb2101deec..73f8585a6b 100644 --- a/eth/tracers/native/mux.go +++ b/eth/tracers/native/mux.go @@ -65,10 +65,11 @@ func newMuxTracerFromConfig(ctx *tracers.Context, cfg json.RawMessage, chainConf // the aggregated JSON result returned by GetResult. // // For hooks that have both a V1 and V2 form (OnCodeChange / OnCodeChangeV2, -// OnNonceChange / OnNonceChangeV2, OnSystemCallStart / OnSystemCallStartV2), -// the mux exposes only the V2 variant upward. The fanout then prefers each -// child's V2 hook and falls back to V1 if only V1 is set, mirroring the -// precedence already used in core/state_processor.go. +// OnNonceChange / OnNonceChangeV2, OnGasChange / OnGasChangeV2, +// OnSystemCallStart / OnSystemCallStartV2), the mux exposes only the V2 +// variant upward. The fanout then prefers each child's V2 hook and falls +// back to V1 if only V1 is set, mirroring the precedence already used in +// core/state_processor.go. func NewMuxTracer(names []string, objects []*tracers.Tracer) (*tracers.Tracer, error) { t := &muxTracer{names: names, tracers: objects} return &tracers.Tracer{ @@ -79,7 +80,7 @@ func NewMuxTracer(names []string, objects []*tracers.Tracer) (*tracers.Tracer, e OnExit: t.OnExit, OnOpcode: t.OnOpcode, OnFault: t.OnFault, - OnGasChange: t.OnGasChange, + OnGasChangeV2: t.OnGasChangeV2, OnBalanceChange: t.OnBalanceChange, OnNonceChangeV2: t.OnNonceChangeV2, OnCodeChangeV2: t.OnCodeChangeV2, @@ -109,10 +110,12 @@ func (t *muxTracer) OnFault(pc uint64, op byte, gas, cost uint64, scope tracing. } } -func (t *muxTracer) OnGasChange(old, new uint64, reason tracing.GasChangeReason) { +func (t *muxTracer) OnGasChangeV2(old, new tracing.Gas, reason tracing.GasChangeReason) { for _, t := range t.tracers { - if t.OnGasChange != nil { - t.OnGasChange(old, new, reason) + if t.OnGasChangeV2 != nil { + t.OnGasChangeV2(old, new, reason) + } else if t.OnGasChange != nil { + t.OnGasChange(old.Regular, new.Regular, reason) } } } diff --git a/eth/tracers/native/noop.go b/eth/tracers/native/noop.go index ac174cc25e..323bf4338f 100644 --- a/eth/tracers/native/noop.go +++ b/eth/tracers/native/noop.go @@ -47,6 +47,7 @@ func newNoopTracer(ctx *tracers.Context, cfg json.RawMessage, chainConfig *param OnOpcode: t.OnOpcode, OnFault: t.OnFault, OnGasChange: t.OnGasChange, + OnGasChangeV2: t.OnGasChangeV2, OnBalanceChange: t.OnBalanceChange, OnNonceChange: t.OnNonceChange, OnCodeChange: t.OnCodeChange, @@ -66,6 +67,8 @@ func (t *noopTracer) OnFault(pc uint64, op byte, gas, cost uint64, _ tracing.OpC func (t *noopTracer) OnGasChange(old, new uint64, reason tracing.GasChangeReason) {} +func (t *noopTracer) OnGasChangeV2(old, new tracing.Gas, reason tracing.GasChangeReason) {} + func (t *noopTracer) OnEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { } From b2aa6987ded983b98a56ad14aff0708e9b003567 Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Wed, 13 May 2026 20:38:47 +0800 Subject: [PATCH 52/63] core/state: track the block-level accessList (#34803) This PR extends the journal to track the pre-transaction values of mutated balances, nonces, and code. At the end of the transaction, these values are used to filter out no-op changes, such as balance transitions from a-> b->a. These changes are excluded from the block-level access list. Additionally, there is a dedicated `bal.ConstructionBlockAccessList` objects for gathering the state reads and writes within the current transaction. These state writes will be keyed by the block accessList index. --------- Co-authored-by: jwasinger --- cmd/evm/internal/t8ntool/execution.go | 4 +- core/chain_makers.go | 4 +- core/state/journal.go | 274 +++++++++++++++++++++----- core/state/journal_test.go | 219 ++++++++++++++++++++ core/state/state_object.go | 12 +- core/state/statedb.go | 84 ++++++-- core/state/statedb_hooked.go | 8 +- core/state/statedb_hooked_test.go | 2 +- core/state/statedb_test.go | 86 ++++++-- core/state_prefetcher.go | 2 +- core/state_processor.go | 24 +-- core/types/bal/access_list.go | 94 --------- core/types/bal/bal.go | 6 +- core/vm/interface.go | 3 +- eth/state_accessor.go | 2 +- eth/tracers/api.go | 8 +- internal/ethapi/simulate.go | 6 +- miner/worker.go | 6 +- 18 files changed, 624 insertions(+), 220 deletions(-) create mode 100644 core/state/journal_test.go delete mode 100644 core/types/bal/access_list.go diff --git a/cmd/evm/internal/t8ntool/execution.go b/cmd/evm/internal/t8ntool/execution.go index 15973e934d..a2de58ad46 100644 --- a/cmd/evm/internal/t8ntool/execution.go +++ b/cmd/evm/internal/t8ntool/execution.go @@ -270,7 +270,7 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, continue } } - statedb.SetTxContext(tx.Hash(), len(receipts)) + statedb.SetTxContext(tx.Hash(), len(receipts), uint32(len(receipts)+1)) var ( snapshot = statedb.Snapshot() gp = gaspool.Snapshot() @@ -336,7 +336,7 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, for _, receipt := range receipts { allLogs = append(allLogs, receipt.Logs...) } - requests, err := core.PostExecution(context.Background(), chainConfig, vmContext.BlockNumber, vmContext.Time, allLogs, evm) + requests, err := core.PostExecution(context.Background(), chainConfig, vmContext.BlockNumber, vmContext.Time, allLogs, evm, uint32(len(receipts)+1)) if err != nil { return nil, nil, nil, NewError(ErrorEVM, fmt.Errorf("failed to process post-execution: %v", err)) } diff --git a/core/chain_makers.go b/core/chain_makers.go index 7474d892b1..cfd6302794 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -117,7 +117,7 @@ func (b *BlockGen) addTx(bc *BlockChain, vmConfig vm.Config, tx *types.Transacti blockContext = NewEVMBlockContext(b.header, bc, &b.header.Coinbase) evm = vm.NewEVM(blockContext, b.statedb, b.cm.config, vmConfig) ) - b.statedb.SetTxContext(tx.Hash(), len(b.txs)) + b.statedb.SetTxContext(tx.Hash(), len(b.txs), uint32(len(b.txs)+1)) receipt, err := ApplyTransaction(evm, b.gasPool, b.statedb, b.header, tx) if err != nil { panic(err) @@ -323,7 +323,7 @@ func (b *BlockGen) collectRequests(readonly bool) (requests [][]byte) { blockContext := NewEVMBlockContext(b.header, b.cm, &b.header.Coinbase) evm := vm.NewEVM(blockContext, statedb, b.cm.config, vm.Config{}) - requests, err := PostExecution(context.Background(), b.cm.config, b.header.Number, b.header.Time, blockLogs, evm) + requests, err := PostExecution(context.Background(), b.cm.config, b.header.Number, b.header.Time, blockLogs, evm, uint32(len(b.txs)+1)) if err != nil { panic(fmt.Sprintf("failed to run post-execution: %v", err)) } diff --git a/core/state/journal.go b/core/state/journal.go index a79bd7331a..353144a1c7 100644 --- a/core/state/journal.go +++ b/core/state/journal.go @@ -18,7 +18,6 @@ package state import ( "fmt" - "maps" "slices" "sort" @@ -32,26 +31,163 @@ type revision struct { journalIndex int } +// journalMutationKind indicates the type of account mutation. +type journalMutationKind uint8 + +const ( + // journalMutationKindNone is the zero value returned by mutation() for + // entries that don't carry a tracked account mutation. The accompanying + // bool is false in that case; callers must gate on it before using the + // kind. + journalMutationKindNone journalMutationKind = iota + journalMutationKindTouch + journalMutationKindCreate + journalMutationKindSelfDestruct + journalMutationKindBalance + journalMutationKindNonce + journalMutationKindCode + journalMutationKindStorage + journalMutationKindCount // sentinel, must stay last +) + +type journalMutationCounts [journalMutationKindCount]int + +// journalMutationState tracks, per account, both the per-kind count of mutation +// entries currently present in the journal and the pre-tx value of each +// metadata field captured on its first touch (balance/nonce/code). +// The *Set flags indicate whether the corresponding field has been mutated +// at least once in the current tx window; they are cleared when all entries +// of that kind are reverted. Storage slots are tracked elsewhere. +type journalMutationState struct { + counts journalMutationCounts + + balance *uint256.Int + balanceSet bool + nonce uint64 + nonceSet bool + code []byte + codeSet bool +} + +func (s *journalMutationState) add(kind journalMutationKind) { + s.counts.add(kind) +} + +// remove drops one occurrence of the given mutation kind. It returns a flag +// indicating whether no entries of any kind remain. +func (s *journalMutationState) remove(kind journalMutationKind) bool { + if s.counts.remove(kind) { + // No entries of this kind remain for this account; drop the + // corresponding stashed original so the state mirrors the + // live mutation set. + s.clearKind(kind) + } + return s.counts == (journalMutationCounts{}) +} + +// clearKind drops the stashed original for the given mutation kind. It is +// invoked during revert once no journal entries of that kind remain for the +// account. Kinds that don't correspond to a tracked metadata field are no-ops. +func (s *journalMutationState) clearKind(kind journalMutationKind) { + switch kind { + case journalMutationKindBalance: + s.balance = nil + s.balanceSet = false + case journalMutationKindNonce: + s.nonce = 0 + s.nonceSet = false + case journalMutationKindCode: + s.code = nil + s.codeSet = false + } +} + +func (s *journalMutationState) copy() *journalMutationState { + cpy := *s + if s.balance != nil { + cpy.balance = new(uint256.Int).Set(s.balance) + } + if s.code != nil { + cpy.code = slices.Clone(s.code) + } + return &cpy +} + +func (c *journalMutationCounts) add(kind journalMutationKind) { + c[kind]++ +} + +func (c *journalMutationCounts) remove(kind journalMutationKind) bool { + c[kind]-- + return c[kind] == 0 +} + // journalEntry is a modification entry in the state change journal that can be // reverted on demand. type journalEntry interface { // revert undoes the changes introduced by this journal entry. revert(*StateDB) - // dirtied returns the Ethereum address modified by this journal entry. - // indicates false if no address was changed. - dirtied() (common.Address, bool) + // mutation returns the account mutation introduced by this entry. + // It indicates false if no tracked account mutation was made. + mutation() (common.Address, journalMutationKind, bool) // copy returns a deep-copied journal entry. copy() journalEntry } +// stashBalance records prev as the pre-tx balance of addr, iff this is the +// first balance touch seen in the current tx. Subsequent balance writes are +// ignored so the stored value remains the true pre-tx original. +func (j *journal) stashBalance(addr common.Address, prev *uint256.Int) { + s := j.mutationStateFor(addr) + if s.balanceSet { + return + } + // The balance is already deep-copied and safe to hold the object here. + s.balance = prev + s.balanceSet = true +} + +// stashNonce records prev as the pre-tx nonce of addr on first touch. +func (j *journal) stashNonce(addr common.Address, prev uint64) { + s := j.mutationStateFor(addr) + if s.nonceSet { + return + } + s.nonce = prev + s.nonceSet = true +} + +// stashCode records prev as the pre-tx code of addr on first touch. +func (j *journal) stashCode(addr common.Address, prev []byte) { + s := j.mutationStateFor(addr) + if s.codeSet { + return + } + // The code is already deep-copied in the StateDB, safe to + // hold the reference here. + s.code = prev + s.codeSet = true +} + +// mutationStateFor returns the mutation state for addr, creating an empty one +// if absent. +func (j *journal) mutationStateFor(addr common.Address) *journalMutationState { + s := j.mutations[addr] + if s == nil { + s = new(journalMutationState) + j.mutations[addr] = s + } + return s +} + // journal contains the list of state modifications applied since the last state // commit. These are tracked to be able to be reverted in the case of an execution // exception or request for reversal. type journal struct { - entries []journalEntry // Current changes tracked by the journal - dirties map[common.Address]int // Dirty accounts and the number of changes + entries []journalEntry // Current changes tracked by the journal + mutations map[common.Address]*journalMutationState // Per-account mutation kinds and pre-tx originals validRevisions []revision nextRevisionId int @@ -60,7 +196,7 @@ type journal struct { // newJournal creates a new initialized journal. func newJournal() *journal { return &journal{ - dirties: make(map[common.Address]int), + mutations: make(map[common.Address]*journalMutationState), } } @@ -70,7 +206,7 @@ func newJournal() *journal { func (j *journal) reset() { j.entries = j.entries[:0] j.validRevisions = j.validRevisions[:0] - clear(j.dirties) + clear(j.mutations) j.nextRevisionId = 0 } @@ -101,33 +237,52 @@ func (j *journal) revertToSnapshot(revid int, s *StateDB) { // append inserts a new modification entry to the end of the change journal. func (j *journal) append(entry journalEntry) { j.entries = append(j.entries, entry) - if addr, dirty := entry.dirtied(); dirty { - j.dirties[addr]++ + if addr, kind, dirty := entry.mutation(); dirty { + state := j.mutations[addr] + if state == nil { + state = new(journalMutationState) + j.mutations[addr] = state + } + state.add(kind) } } // revert undoes a batch of journalled modifications along with any reverted -// dirty handling too. +// mutation tracking too. func (j *journal) revert(statedb *StateDB, snapshot int) { for i := len(j.entries) - 1; i >= snapshot; i-- { // Undo the changes made by the operation j.entries[i].revert(statedb) - // Drop any dirty tracking induced by the change - if addr, dirty := j.entries[i].dirtied(); dirty { - if j.dirties[addr]--; j.dirties[addr] == 0 { - delete(j.dirties, addr) + // Drop any mutation tracking induced by the change. + if addr, kind, dirty := j.entries[i].mutation(); dirty { + state := j.mutations[addr] + if state == nil { + panic(fmt.Errorf("journal mutation tracking missing for %x", addr[:])) + } + if state.remove(kind) { + delete(j.mutations, addr) } } } j.entries = j.entries[:snapshot] } -// dirty explicitly sets an address to dirty, even if the change entries would -// otherwise suggest it as clean. This method is an ugly hack to handle the RIPEMD -// precompile consensus exception. -func (j *journal) dirty(addr common.Address) { - j.dirties[addr]++ +// ripemdMagic explicitly keeps RIPEMD160 in the mutation set with a touch change. +// +// Ethereum Mainnet contains an old empty-account touch/revert quirk for address +// 0x03. If we only relied on the journal entry above, the revert path would +// remove the account from the mutation set together with the touch. +// +// Keep an explicit touch marker so tx finalisation still sees RIPEMD160 +// on the mutation pass when replaying that historical case. +func (j *journal) ripemdMagic() { + state := j.mutations[ripemd] + if state == nil { + state = new(journalMutationState) + j.mutations[ripemd] = state + } + state.add(journalMutationKindTouch) } // length returns the current number of entries in the journal. @@ -141,9 +296,13 @@ func (j *journal) copy() *journal { for i := 0; i < j.length(); i++ { entries = append(entries, j.entries[i].copy()) } + mutations := make(map[common.Address]*journalMutationState, len(j.mutations)) + for addr, state := range j.mutations { + mutations[addr] = state.copy() + } return &journal{ entries: entries, - dirties: maps.Clone(j.dirties), + mutations: mutations, validRevisions: slices.Clone(j.validRevisions), nextRevisionId: j.nextRevisionId, } @@ -187,13 +346,16 @@ func (j *journal) refundChange(previous uint64) { } func (j *journal) balanceChange(addr common.Address, previous *uint256.Int) { + prev := previous.Clone() + j.stashBalance(addr, prev) j.append(balanceChange{ account: addr, - prev: previous.Clone(), + prev: prev, }) } func (j *journal) setCode(address common.Address, prevCode []byte) { + j.stashCode(address, prevCode) j.append(codeChange{ account: address, prevCode: prevCode, @@ -201,6 +363,7 @@ func (j *journal) setCode(address common.Address, prevCode []byte) { } func (j *journal) nonceChange(address common.Address, prev uint64) { + j.stashNonce(address, prev) j.append(nonceChange{ account: address, prev: prev, @@ -212,9 +375,18 @@ func (j *journal) touchChange(address common.Address) { account: address, }) if address == ripemd { - // Explicitly put it in the dirty-cache, which is otherwise generated from - // flattened journals. - j.dirty(address) + // Preserve the historical RIPEMD160 precompile consensus exception. + // + // Mainnet contains an old empty-account touch/revert quirk for address + // 0x03. If we only relied on the journal entry above, the revert path + // would remove the account from the dirty set together with the touch. + // Keep an explicit dirty marker so tx finalisation still sees the + // account on the dirty pass when replaying that historical case. + // + // This does not force deletion by itself: Finalise will still delete the + // account only if the state object is present at tx end and qualifies for + // deletion there. + j.ripemdMagic() } } @@ -295,8 +467,8 @@ func (ch createObjectChange) revert(s *StateDB) { delete(s.stateObjects, ch.account) } -func (ch createObjectChange) dirtied() (common.Address, bool) { - return ch.account, true +func (ch createObjectChange) mutation() (common.Address, journalMutationKind, bool) { + return ch.account, journalMutationKindCreate, true } func (ch createObjectChange) copy() journalEntry { @@ -309,8 +481,8 @@ func (ch createContractChange) revert(s *StateDB) { s.getStateObject(ch.account).newContract = false } -func (ch createContractChange) dirtied() (common.Address, bool) { - return common.Address{}, false +func (ch createContractChange) mutation() (common.Address, journalMutationKind, bool) { + return common.Address{}, journalMutationKindNone, false } func (ch createContractChange) copy() journalEntry { @@ -326,8 +498,8 @@ func (ch selfDestructChange) revert(s *StateDB) { } } -func (ch selfDestructChange) dirtied() (common.Address, bool) { - return ch.account, true +func (ch selfDestructChange) mutation() (common.Address, journalMutationKind, bool) { + return ch.account, journalMutationKindSelfDestruct, true } func (ch selfDestructChange) copy() journalEntry { @@ -341,8 +513,8 @@ var ripemd = common.HexToAddress("0000000000000000000000000000000000000003") func (ch touchChange) revert(s *StateDB) { } -func (ch touchChange) dirtied() (common.Address, bool) { - return ch.account, true +func (ch touchChange) mutation() (common.Address, journalMutationKind, bool) { + return ch.account, journalMutationKindTouch, true } func (ch touchChange) copy() journalEntry { @@ -355,8 +527,8 @@ func (ch balanceChange) revert(s *StateDB) { s.getStateObject(ch.account).setBalance(ch.prev) } -func (ch balanceChange) dirtied() (common.Address, bool) { - return ch.account, true +func (ch balanceChange) mutation() (common.Address, journalMutationKind, bool) { + return ch.account, journalMutationKindBalance, true } func (ch balanceChange) copy() journalEntry { @@ -370,8 +542,8 @@ func (ch nonceChange) revert(s *StateDB) { s.getStateObject(ch.account).setNonce(ch.prev) } -func (ch nonceChange) dirtied() (common.Address, bool) { - return ch.account, true +func (ch nonceChange) mutation() (common.Address, journalMutationKind, bool) { + return ch.account, journalMutationKindNonce, true } func (ch nonceChange) copy() journalEntry { @@ -385,8 +557,8 @@ func (ch codeChange) revert(s *StateDB) { s.getStateObject(ch.account).setCode(crypto.Keccak256Hash(ch.prevCode), ch.prevCode) } -func (ch codeChange) dirtied() (common.Address, bool) { - return ch.account, true +func (ch codeChange) mutation() (common.Address, journalMutationKind, bool) { + return ch.account, journalMutationKindCode, true } func (ch codeChange) copy() journalEntry { @@ -400,8 +572,8 @@ func (ch storageChange) revert(s *StateDB) { s.getStateObject(ch.account).setState(ch.key, ch.prevvalue, ch.origvalue) } -func (ch storageChange) dirtied() (common.Address, bool) { - return ch.account, true +func (ch storageChange) mutation() (common.Address, journalMutationKind, bool) { + return ch.account, journalMutationKindStorage, true } func (ch storageChange) copy() journalEntry { @@ -417,8 +589,8 @@ func (ch transientStorageChange) revert(s *StateDB) { s.setTransientState(ch.account, ch.key, ch.prevalue) } -func (ch transientStorageChange) dirtied() (common.Address, bool) { - return common.Address{}, false +func (ch transientStorageChange) mutation() (common.Address, journalMutationKind, bool) { + return common.Address{}, journalMutationKindNone, false } func (ch transientStorageChange) copy() journalEntry { @@ -433,8 +605,8 @@ func (ch refundChange) revert(s *StateDB) { s.refund = ch.prev } -func (ch refundChange) dirtied() (common.Address, bool) { - return common.Address{}, false +func (ch refundChange) mutation() (common.Address, journalMutationKind, bool) { + return common.Address{}, journalMutationKindNone, false } func (ch refundChange) copy() journalEntry { @@ -453,8 +625,8 @@ func (ch addLogChange) revert(s *StateDB) { s.logSize-- } -func (ch addLogChange) dirtied() (common.Address, bool) { - return common.Address{}, false +func (ch addLogChange) mutation() (common.Address, journalMutationKind, bool) { + return common.Address{}, journalMutationKindNone, false } func (ch addLogChange) copy() journalEntry { @@ -476,8 +648,8 @@ func (ch accessListAddAccountChange) revert(s *StateDB) { s.accessList.DeleteAddress(ch.address) } -func (ch accessListAddAccountChange) dirtied() (common.Address, bool) { - return common.Address{}, false +func (ch accessListAddAccountChange) mutation() (common.Address, journalMutationKind, bool) { + return common.Address{}, journalMutationKindNone, false } func (ch accessListAddAccountChange) copy() journalEntry { @@ -490,8 +662,8 @@ func (ch accessListAddSlotChange) revert(s *StateDB) { s.accessList.DeleteSlot(ch.address, ch.slot) } -func (ch accessListAddSlotChange) dirtied() (common.Address, bool) { - return common.Address{}, false +func (ch accessListAddSlotChange) mutation() (common.Address, journalMutationKind, bool) { + return common.Address{}, journalMutationKindNone, false } func (ch accessListAddSlotChange) copy() journalEntry { diff --git a/core/state/journal_test.go b/core/state/journal_test.go new file mode 100644 index 0000000000..262cee77fe --- /dev/null +++ b/core/state/journal_test.go @@ -0,0 +1,219 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package state + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/types" + "github.com/holiman/uint256" +) + +// fuzzJournalAddrs is a small fixed pool used by the fuzz harness to force +// repeated collisions on the same account, which exercises the multi-entry +// path in the journal's mutation tracking and originals cleanup on revert. +// It deliberately excludes the RIPEMD-160 precompile (0x03), which has a +// consensus-level touch/revert exception that would complicate invariants. +var fuzzJournalAddrs = []common.Address{ + common.BytesToAddress([]byte{0x11}), + common.BytesToAddress([]byte{0x22}), + common.BytesToAddress([]byte{0x44}), +} + +// checkJournalInvariants validates that: +// - journal.mutations exactly reflects the dirty entries currently in +// journal.entries (per-kind counts and mask match what you'd get by +// walking the entries from scratch). +// - journal.originals mirrors that set for the three tracked metadata kinds +// (balance/nonce/code): a *Set flag is true iff the account currently has +// at least one corresponding entry in the journal. +// - An address is present in originals only if it also has at least one +// tracked-kind mutation in the journal. +func checkJournalInvariants(t *testing.T, j *journal) { + t.Helper() + + // Reconstruct the expected per-address counts from the live entries. + expected := make(map[common.Address]*journalMutationCounts) + for _, e := range j.entries { + addr, kind, dirty := e.mutation() + if !dirty { + continue + } + c := expected[addr] + if c == nil { + c = &journalMutationCounts{} + expected[addr] = c + } + c.add(kind) + } + + if len(j.mutations) != len(expected) { + t.Fatalf("mutations size %d, want %d", len(j.mutations), len(expected)) + } + for addr, state := range j.mutations { + want, ok := expected[addr] + if !ok { + t.Fatalf("mutations has extra address %x", addr) + } + if state.counts != *want { + t.Fatalf("addr %x: counts=%+v want=%+v", addr, state.counts, *want) + } + // First-touch *Set flags must mirror the live per-kind counts. + if state.balanceSet != (want[journalMutationKindBalance] > 0) { + t.Fatalf("addr %x: balanceSet=%v want=%v (balance count=%d)", + addr, state.balanceSet, want[journalMutationKindBalance] > 0, want[journalMutationKindBalance]) + } + if state.nonceSet != (want[journalMutationKindNonce] > 0) { + t.Fatalf("addr %x: nonceSet=%v want=%v (nonce count=%d)", + addr, state.nonceSet, want[journalMutationKindNonce] > 0, want[journalMutationKindNonce]) + } + if state.codeSet != (want[journalMutationKindCode] > 0) { + t.Fatalf("addr %x: codeSet=%v want=%v (code count=%d)", + addr, state.codeSet, want[journalMutationKindCode] > 0, want[journalMutationKindCode]) + } + } +} + +// FuzzJournal drives a randomised sequence of state mutations, snapshots and +// reverts against a fresh StateDB and validates the journal's internal +// bookkeeping invariants after every step. It also asserts that reverting +// back to the root snapshot empties mutations, originals and entries +// completely. The seed corpus ensures the test also runs as a regular unit +// test via `go test -run FuzzJournal`. +func FuzzJournal(f *testing.F) { + seeds := [][]byte{ + // balance then full revert (simplest a→b→a case). + {0x00, 0x00, 0x05, 0x05, 0x00}, + // balance+nonce+code mixed, then revert to root. + {0x00, 0x00, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x00, 0x03, 0x05, 0x00}, + // snapshot, mutate, revert, mutate again. + {0x04, 0x00, 0x00, 0x07, 0x05, 0x00, 0x00, 0x01, 0x05}, + // storage interleaved with metadata. + {0x03, 0x00, 0x01, 0x00, 0x01, 0x05, 0x03, 0x02, 0x02, 0x04, 0x03, 0x01, 0x07}, + // many ops, no explicit revert — exercises steady-state invariants. + {0x00, 0x01, 0x02, 0x00, 0x01, 0x02, 0x03, 0x00, 0x01, 0x02, + 0x03, 0x04, 0x00, 0x01, 0x02, 0x00, 0x06, 0x08, 0x0a, 0x0c}, + } + for _, s := range seeds { + f.Add(s) + } + + f.Fuzz(func(t *testing.T, data []byte) { + sdb, err := New(types.EmptyRootHash, NewDatabaseForTesting()) + if err != nil { + t.Fatal(err) + } + root := sdb.Snapshot() + + // Stack of snapshot IDs taken during the fuzz loop. + var pending []int + + // readByte returns the next byte and advances the cursor. Returns + // (0, false) if exhausted. + i := 0 + readByte := func() (byte, bool) { + if i >= len(data) { + return 0, false + } + b := data[i] + i++ + return b, true + } + + for { + op, ok := readByte() + if !ok { + break + } + switch op % 6 { + case 0: // SetBalance + a, ok1 := readByte() + v, ok2 := readByte() + if !ok1 || !ok2 { + break + } + addr := fuzzJournalAddrs[int(a)%len(fuzzJournalAddrs)] + sdb.SetBalance(addr, uint256.NewInt(uint64(v)), tracing.BalanceChangeUnspecified) + case 1: // SetNonce + a, ok1 := readByte() + n, ok2 := readByte() + if !ok1 || !ok2 { + break + } + addr := fuzzJournalAddrs[int(a)%len(fuzzJournalAddrs)] + sdb.SetNonce(addr, uint64(n), tracing.NonceChangeUnspecified) + case 2: // SetCode + a, ok1 := readByte() + l, ok2 := readByte() + if !ok1 || !ok2 { + break + } + addr := fuzzJournalAddrs[int(a)%len(fuzzJournalAddrs)] + code := make([]byte, int(l)%8) + for k := range code { + b, ok := readByte() + if !ok { + break + } + code[k] = b + } + sdb.SetCode(addr, code, tracing.CodeChangeUnspecified) + case 3: // SetState (storage; tracked as mutation kind, no original) + a, ok1 := readByte() + k, ok2 := readByte() + v, ok3 := readByte() + if !ok1 || !ok2 || !ok3 { + break + } + addr := fuzzJournalAddrs[int(a)%len(fuzzJournalAddrs)] + sdb.SetState(addr, + common.BytesToHash([]byte{k}), + common.BytesToHash([]byte{v})) + case 4: // Snapshot + pending = append(pending, sdb.Snapshot()) + case 5: // RevertToSnapshot + if len(pending) == 0 { + break + } + sel, ok := readByte() + if !ok { + break + } + idx := int(sel) % len(pending) + sdb.RevertToSnapshot(pending[idx]) + pending = pending[:idx] + } + checkJournalInvariants(t, sdb.journal) + } + + // After reverting to the root snapshot, the journal must be fully + // drained: no entries, no mutations, no originals. This is the core + // guarantee the user cares about — "all mutations against a single + // account reverted" taken to its limit across every account. + sdb.RevertToSnapshot(root) + checkJournalInvariants(t, sdb.journal) + + if n := len(sdb.journal.entries); n != 0 { + t.Fatalf("entries not drained after revert-to-root: %d remain", n) + } + if n := len(sdb.journal.mutations); n != 0 { + t.Fatalf("mutations not drained after revert-to-root: %d remain", n) + } + }) +} diff --git a/core/state/state_object.go b/core/state/state_object.go index 8e72486825..ce456e7668 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -184,8 +184,9 @@ func (s *stateObject) getState(key common.Hash) (common.Hash, common.Hash) { // without any mutations caused in the current execution. func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { // Record slot access regardless of whether the storage slot exists. - s.db.stateReadList.AddState(s.address, key) - + if s.db.stateAccessList != nil { + s.db.stateAccessList.StorageRead(s.address, key) + } // If we have a pending write or clean cached, return that if value, pending := s.pendingStorage[key]; pending { return value @@ -274,6 +275,13 @@ func (s *stateObject) finalise() { // map as the dirty slot might have been committed already (before the // byzantium fork) and entry is necessary to modify the value back. s.pendingStorage[key] = value + + // Aggregate storage writes into the block-level access list. + // All slots in the dirtyStorage set must have post-transaction + // values that differ from their pre-transaction values. + if s.db.stateAccessList != nil { + s.db.stateAccessList.StorageWrite(s.db.blockAccessIndex, s.address, key, value) + } } if s.db.prefetcher != nil && len(slotsToPrefetch) > 0 && s.data.Root != types.EmptyRootHash { if err := s.db.prefetcher.prefetch(s.addrHash(), s.data.Root, s.address, nil, slotsToPrefetch, false); err != nil { diff --git a/core/state/statedb.go b/core/state/statedb.go index e6d8b5bffc..1c49d46020 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -18,6 +18,7 @@ package state import ( + "bytes" "errors" "fmt" "maps" @@ -128,7 +129,10 @@ type StateDB struct { accessEvents *AccessEvents // Per-transaction state access footprint for EIP-7928 - stateReadList *bal.StateAccessList + stateAccessList *bal.ConstructionBlockAccessList + + // Block access index (0 for pre-execution, 1..n for transactions, n+1 for post-execution) + blockAccessIndex uint32 // Transient storage transientStorage transientStorage @@ -589,8 +593,9 @@ func (s *StateDB) deleteStateObject(addr common.Address) { // the object is not found or was deleted in this execution context. func (s *StateDB) getStateObject(addr common.Address) *stateObject { // Record state access regardless of whether the account exists. - s.stateReadList.AddAccount(addr) - + if s.stateAccessList != nil { + s.stateAccessList.AccountRead(addr) + } // Prefer live objects if any is available if obj := s.stateObjects[addr]; obj != nil { return obj @@ -693,6 +698,7 @@ func (s *StateDB) Copy() *StateDB { refund: s.refund, thash: s.thash, txIndex: s.txIndex, + blockAccessIndex: s.blockAccessIndex, logs: make(map[common.Hash][]*types.Log, len(s.logs)), logSize: s.logSize, preimages: maps.Clone(s.preimages), @@ -716,9 +722,6 @@ func (s *StateDB) Copy() *StateDB { if s.accessEvents != nil { state.accessEvents = s.accessEvents.Copy() } - if s.stateReadList != nil { - state.stateReadList = s.stateReadList.Copy() - } // Deep copy cached state objects. for addr, obj := range s.stateObjects { state.stateObjects[addr] = obj.deepCopy(state) @@ -740,6 +743,9 @@ func (s *StateDB) Copy() *StateDB { } state.logs[hash] = cpy } + if s.stateAccessList != nil { + state.stateAccessList = s.stateAccessList.Copy() + } return state } @@ -775,7 +781,7 @@ type removedAccountWithBalance struct { // before the Finalise. func (s *StateDB) LogsForBurnAccounts() []*types.Log { var list []removedAccountWithBalance - for addr := range s.journal.dirties { + for addr := range s.journal.mutations { if obj, exist := s.stateObjects[addr]; exist && obj.selfDestructed && !obj.Balance().IsZero() { list = append(list, removedAccountWithBalance{ address: obj.address, @@ -799,17 +805,20 @@ func (s *StateDB) LogsForBurnAccounts() []*types.Log { // 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. -func (s *StateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList { - addressesToPrefetch := make([]common.Address, 0, len(s.journal.dirties)) - for addr := range s.journal.dirties { +func (s *StateDB) Finalise(deleteEmptyObjects bool) *bal.ConstructionBlockAccessList { + addressesToPrefetch := make([]common.Address, 0, len(s.journal.mutations)) + for addr, state := range s.journal.mutations { obj, exist := s.stateObjects[addr] if !exist { - // ripeMD is 'touched' at block 1714175, in tx 0x1237f737031e40bcde4a8b7e717b2d15e3ecadfe49bb1bbc71ee9deb09c6fcf2 - // That tx goes out of gas, and although the notion of 'touched' does not exist there, the - // touch-event will still be recorded in the journal. Since ripeMD is a special snowflake, - // it will persist in the journal even though the journal is reverted. In this special circumstance, - // it may exist in `s.journal.dirties` but not in `s.stateObjects`. - // Thus, we can safely ignore it here + // RIPEMD160 (0x03) gets an extra dirty marker for a historical + // mainnet consensus exception (at block 1714175, in tx + // 0x1237f737031e40bcde4a8b7e717b2d15e3ecadfe49bb1bbc71ee9deb09c6fcf2) + // around empty-account touch/revert handling. + // + // That marker survives journal revert, so the account may remain in + // s.journal.mutations even though its state object was rolled + // back and no longer exists. In that case there is nothing to + // finalise or delete, so ignore it here. continue } if obj.selfDestructed || (deleteEmptyObjects && obj.empty()) { @@ -822,7 +831,43 @@ func (s *StateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList { if _, ok := s.stateObjectsDestruct[obj.address]; !ok { s.stateObjectsDestruct[obj.address] = obj } + // Aggregate the account mutation into the block-level accessList + // if Amsterdam has been activated. + if s.stateAccessList != nil { + // Notably, if the account is deleted during the transaction, + // its pre-transaction nonce, code, and storage must be empty. + // + // EIP-6780 restricts self-destruct to contracts deployed within + // the same transaction, while EIP-7610 rejects deployments to + // destinations with non-empty storage, non-zero nonce and non-empty + // code. + // + // Therefore, when an account is deleted, its pre-transaction nonce + // code and storage is guaranteed to be empty, leaving nothing to + // clean up here. + balance := uint256.NewInt(0) + if state.balanceSet && balance.Cmp(state.balance) != 0 { + s.stateAccessList.BalanceChange(s.blockAccessIndex, addr, balance) + } + } } else { + // Aggregate the account mutation into the block-level accessList + // if Amsterdam has been activated. + if s.stateAccessList != nil { + balance := obj.Balance() + if state.balanceSet && balance.Cmp(state.balance) != 0 { + s.stateAccessList.BalanceChange(s.blockAccessIndex, addr, balance) + } + nonce := obj.Nonce() + if state.nonceSet && nonce != state.nonce { + s.stateAccessList.NonceChange(addr, s.blockAccessIndex, nonce) + } + if state.codeSet { + if code := obj.Code(); !bytes.Equal(code, state.code) { + s.stateAccessList.CodeChange(addr, s.blockAccessIndex, code) + } + } + } obj.finalise() s.markUpdate(addr) } @@ -839,7 +884,7 @@ func (s *StateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList { // Invalidate journal because reverting across transactions is not allowed. s.clearJournalAndRefund() - return s.stateReadList + return s.stateAccessList } // IntermediateRoot computes the current root hash of the state trie. @@ -1052,9 +1097,10 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { // SetTxContext sets the current transaction hash and index which are // used when the EVM emits new state logs. It should be invoked before // transaction execution. -func (s *StateDB) SetTxContext(thash common.Hash, ti int) { +func (s *StateDB) SetTxContext(thash common.Hash, ti int, blockAccessIndex uint32) { s.thash = thash s.txIndex = ti + s.blockAccessIndex = blockAccessIndex } func (s *StateDB) clearJournalAndRefund() { @@ -1435,7 +1481,7 @@ func (s *StateDB) Prepare(rules params.Rules, sender, coinbase common.Address, d s.transientStorage = newTransientStorage() if rules.IsAmsterdam { - s.stateReadList = bal.NewStateAccessList() + s.stateAccessList = bal.NewConstructionBlockAccessList() } } diff --git a/core/state/statedb_hooked.go b/core/state/statedb_hooked.go index c5faa7c98e..98d01343a4 100644 --- a/core/state/statedb_hooked.go +++ b/core/state/statedb_hooked.go @@ -234,7 +234,7 @@ func (s *hookedStateDB) LogsForBurnAccounts() []*types.Log { return s.inner.LogsForBurnAccounts() } -func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList { +func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) *bal.ConstructionBlockAccessList { 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. return s.inner.Finalise(deleteEmptyObjects) @@ -244,7 +244,7 @@ func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList { // that state change hooks will be invoked in deterministic // order when the accounts are deleted below var selfDestructedAddrs []common.Address - for addr := range s.inner.journal.dirties { + for addr := range s.inner.journal.mutations { obj := s.inner.stateObjects[addr] if obj == nil || !obj.selfDestructed { // Not self-destructed, keep searching. @@ -288,3 +288,7 @@ func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) *bal.StateAccessList { } return s.inner.Finalise(deleteEmptyObjects) } + +func (s *hookedStateDB) SetTxContext(thash common.Hash, ti int, blockAccessIndex uint32) { + s.inner.SetTxContext(thash, ti, blockAccessIndex) +} diff --git a/core/state/statedb_hooked_test.go b/core/state/statedb_hooked_test.go index 6fe17ec1b4..fad234f848 100644 --- a/core/state/statedb_hooked_test.go +++ b/core/state/statedb_hooked_test.go @@ -82,7 +82,7 @@ func TestBurn(t *testing.T) { // TestHooks is a basic sanity-check of all hooks func TestHooks(t *testing.T) { inner, _ := New(types.EmptyRootHash, NewDatabaseForTesting()) - inner.SetTxContext(common.Hash{0x11}, 100) // For the log + inner.SetTxContext(common.Hash{0x11}, 100, 101) // For the log var result []string var wants = []string{ "0xaa00000000000000000000000000000000000000.balance: 0->100 (Unspecified)", diff --git a/core/state/statedb_test.go b/core/state/statedb_test.go index b5ef42b3e0..0bf9b50e7b 100644 --- a/core/state/statedb_test.go +++ b/core/state/statedb_test.go @@ -662,26 +662,30 @@ func (test *snapshotTest) checkEqual(state, checkstate *StateDB) error { return fmt.Errorf("got GetLogs(common.Hash{}) == %v, want GetLogs(common.Hash{}) == %v", state.GetLogs(common.Hash{}, 0, common.Hash{}, 0), checkstate.GetLogs(common.Hash{}, 0, common.Hash{}, 0)) } - if !maps.Equal(state.journal.dirties, checkstate.journal.dirties) { - getKeys := func(dirty map[common.Address]int) string { - var keys []common.Address - out := new(strings.Builder) - for key := range dirty { - keys = append(keys, key) - } - slices.SortFunc(keys, common.Address.Cmp) - for i, key := range keys { - fmt.Fprintf(out, " %d. %v\n", i, key) - } - return out.String() - } - have := getKeys(state.journal.dirties) - want := getKeys(checkstate.journal.dirties) - return fmt.Errorf("dirty-journal set mismatch.\nhave:\n%v\nwant:\n%v\n", have, want) + if !equalMutationSets(state.journal.mutations, checkstate.journal.mutations) { + return fmt.Errorf("journal mutation set mismatch.\nhave:\n%v\nwant:\n%v\n", state.journal.mutations, checkstate.journal.mutations) } return nil } +// equalMutationSets checks that two journal mutation maps have the same set of +// addresses and, for each address, the same per-kind counts. The stashed +// original values are ignored because comparing them across two independent +// state databases (with distinct pointer identities) isn't the point of this +// check — we only care that the two journals agree on what was touched. +func equalMutationSets(a, b map[common.Address]*journalMutationState) bool { + if len(a) != len(b) { + return false + } + for addr, sa := range a { + sb, ok := b[addr] + if !ok || sa.counts != sb.counts { + return false + } + } + return true +} + func TestTouchDelete(t *testing.T) { s := newStateEnv() s.state.getOrNewStateObject(common.Address{}) @@ -691,12 +695,54 @@ func TestTouchDelete(t *testing.T) { snapshot := s.state.Snapshot() s.state.AddBalance(common.Address{}, new(uint256.Int), tracing.BalanceChangeUnspecified) - if len(s.state.journal.dirties) != 1 { - t.Fatal("expected one dirty state object") + if len(s.state.journal.mutations) != 1 { + t.Fatal("expected one mutated state object") } s.state.RevertToSnapshot(snapshot) - if len(s.state.journal.dirties) != 0 { - t.Fatal("expected no dirty state object") + if len(s.state.journal.mutations) != 0 { + t.Fatal("expected no journal mutations") + } +} + +func TestJournalMutationTracking(t *testing.T) { + state, _ := New(types.EmptyRootHash, NewDatabaseForTesting()) + addr := common.HexToAddress("0x01") + key := common.HexToHash("0x02") + + if _, ok := state.journal.mutations[addr]; ok { + t.Fatal("unexpected initial mutation entry") + } + snapshot := state.Snapshot() + + state.SetBalance(addr, uint256.NewInt(1), tracing.BalanceChangeUnspecified) + state.SetNonce(addr, 2, tracing.NonceChangeUnspecified) + state.SetCode(addr, []byte{0x1}, tracing.CodeChangeUnspecified) + state.SetState(addr, key, common.Hash{0x3}) + + want := journalMutationCounts{ + journalMutationKindCreate: 1, + journalMutationKindBalance: 1, + journalMutationKindNonce: 1, + journalMutationKindCode: 1, + journalMutationKindStorage: 1, + } + checkCounts := func(got *journalMutationState, label string) { + t.Helper() + if got == nil { + t.Fatalf("%s: missing mutation entry for %x", label, addr) + } + if got.counts != want { + t.Fatalf("%s: counts=%+v, want=%+v", label, got.counts, want) + } + } + checkCounts(state.journal.mutations[addr], "state") + + copy := state.Copy() + checkCounts(copy.journal.mutations[addr], "copy") + + state.RevertToSnapshot(snapshot) + if _, ok := state.journal.mutations[addr]; ok { + t.Fatalf("unexpected mutation entry after revert") } } diff --git a/core/state_prefetcher.go b/core/state_prefetcher.go index ed292d0beb..d99611ff2c 100644 --- a/core/state_prefetcher.go +++ b/core/state_prefetcher.go @@ -104,7 +104,7 @@ func (p *statePrefetcher) Prefetch(block *types.Block, statedb *state.StateDB, c // Disable the nonce check msg.SkipNonceChecks = true - stateCpy.SetTxContext(tx.Hash(), i) + stateCpy.SetTxContext(tx.Hash(), i, uint32(i+1)) // We attempt to apply a transaction. The goal is not to execute // the transaction successfully, rather to warm up touched data slots. diff --git a/core/state_processor.go b/core/state_processor.go index 4bffece7ac..13466b7815 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -94,7 +94,7 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated if err != nil { return nil, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) } - statedb.SetTxContext(tx.Hash(), i) + statedb.SetTxContext(tx.Hash(), i, uint32(i+1)) _, _, spanEnd := telemetry.StartSpan(ctx, "core.ApplyTransactionWithEVM", telemetry.StringAttribute("tx.hash", tx.Hash().Hex()), telemetry.Int64Attribute("tx.index", int64(i)), @@ -109,8 +109,7 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated allLogs = append(allLogs, receipt.Logs...) spanEnd(nil) } - // Run the post-execution system calls - requests, err := PostExecution(ctx, config, block.Number(), block.Time(), allLogs, evm) + requests, err := PostExecution(ctx, config, block.Number(), block.Time(), allLogs, evm, uint32(len(block.Transactions())+1)) if err != nil { return nil, err } @@ -143,7 +142,7 @@ func PreExecution(ctx context.Context, beaconRoot *common.Hash, parent common.Ha // PostExecution processes post-execution system calls when Prague is enabled. // If Prague is not activated, it returns null requests to differentiate from // empty requests. -func PostExecution(ctx context.Context, config *params.ChainConfig, number *big.Int, time uint64, allLogs []*types.Log, evm *vm.EVM) (requests [][]byte, err error) { +func PostExecution(ctx context.Context, config *params.ChainConfig, number *big.Int, time uint64, allLogs []*types.Log, evm *vm.EVM, blockAccessIndex uint32) (requests [][]byte, err error) { _, _, spanEnd := telemetry.StartSpan(ctx, "core.postExecution") defer spanEnd(&err) @@ -155,11 +154,11 @@ func PostExecution(ctx context.Context, config *params.ChainConfig, number *big. return nil, fmt.Errorf("failed to parse deposit logs: %w", err) } // EIP-7002 - if err := ProcessWithdrawalQueue(&requests, evm); err != nil { + if err := ProcessWithdrawalQueue(&requests, evm, blockAccessIndex); err != nil { return nil, fmt.Errorf("failed to process withdrawal queue: %w", err) } // EIP-7251 - if err := ProcessConsolidationQueue(&requests, evm); err != nil { + if err := ProcessConsolidationQueue(&requests, evm, blockAccessIndex); err != nil { return nil, fmt.Errorf("failed to process consolidation queue: %w", err) } } @@ -268,6 +267,7 @@ func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM) { Data: beaconRoot[:], } evm.SetTxContext(NewEVMTxContext(msg)) + evm.StateDB.SetTxContext(common.Hash{}, 0, 0) evm.StateDB.AddAddressToAccessList(params.BeaconRootsAddress) _, _, _ = evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560) if evm.StateDB.AccessEvents() != nil { @@ -295,6 +295,7 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM) { Data: prevHash.Bytes(), } evm.SetTxContext(NewEVMTxContext(msg)) + evm.StateDB.SetTxContext(common.Hash{}, 0, 0) evm.StateDB.AddAddressToAccessList(params.HistoryStorageAddress) _, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560) if err != nil { @@ -308,17 +309,17 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM) { // ProcessWithdrawalQueue calls the EIP-7002 withdrawal queue contract. // It returns the opaque request data returned by the contract. -func ProcessWithdrawalQueue(requests *[][]byte, evm *vm.EVM) error { - return processRequestsSystemCall(requests, evm, 0x01, params.WithdrawalQueueAddress) +func ProcessWithdrawalQueue(requests *[][]byte, evm *vm.EVM, blockAccessIndex uint32) error { + return processRequestsSystemCall(requests, evm, 0x01, params.WithdrawalQueueAddress, blockAccessIndex) } // ProcessConsolidationQueue calls the EIP-7251 consolidation queue contract. // It returns the opaque request data returned by the contract. -func ProcessConsolidationQueue(requests *[][]byte, evm *vm.EVM) error { - return processRequestsSystemCall(requests, evm, 0x02, params.ConsolidationQueueAddress) +func ProcessConsolidationQueue(requests *[][]byte, evm *vm.EVM, blockAccessIndex uint32) error { + return processRequestsSystemCall(requests, evm, 0x02, params.ConsolidationQueueAddress, blockAccessIndex) } -func processRequestsSystemCall(requests *[][]byte, evm *vm.EVM, requestType byte, addr common.Address) error { +func processRequestsSystemCall(requests *[][]byte, evm *vm.EVM, requestType byte, addr common.Address, blockAccessIndex uint32) error { if tracer := evm.Config.Tracer; tracer != nil { onSystemCallStart(tracer, evm.GetVMContext()) if tracer.OnSystemCallEnd != nil { @@ -334,6 +335,7 @@ func processRequestsSystemCall(requests *[][]byte, evm *vm.EVM, requestType byte To: &addr, } evm.SetTxContext(NewEVMTxContext(msg)) + evm.StateDB.SetTxContext(common.Hash{}, 0, blockAccessIndex) evm.StateDB.AddAddressToAccessList(addr) ret, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560) if evm.StateDB.AccessEvents() != nil { diff --git a/core/types/bal/access_list.go b/core/types/bal/access_list.go deleted file mode 100644 index e563fa22e2..0000000000 --- a/core/types/bal/access_list.go +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright 2026 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see - -package bal - -import ( - "maps" - - "github.com/ethereum/go-ethereum/common" -) - -// StorageAccessList represents a set of storage slots accessed within an account. -type StorageAccessList map[common.Hash]struct{} - -// StateAccessList records the set of accounts and storage slots that have been -// accessed. An entry with an empty StorageAccessList denotes an account access -// without any storage slot access. -type StateAccessList struct { - list map[common.Address]StorageAccessList -} - -// NewStateAccessList returns an empty StateAccessList ready for use. -func NewStateAccessList() *StateAccessList { - return &StateAccessList{ - list: make(map[common.Address]StorageAccessList), - } -} - -// AddAccount records an access to the given account. It is a no-op if the -// account is already present. -func (s *StateAccessList) AddAccount(addr common.Address) { - if s == nil { - return - } - if _, exists := s.list[addr]; !exists { - s.list[addr] = make(StorageAccessList) - } -} - -// AddState records an access to the given storage slot. The owning account is -// implicitly recorded as well. -func (s *StateAccessList) AddState(addr common.Address, slot common.Hash) { - if s == nil { - return - } - slots, exists := s.list[addr] - if !exists { - slots = make(StorageAccessList) - s.list[addr] = slots - } - slots[slot] = struct{}{} -} - -// Merge merges the entries from other into the receiver. -func (s *StateAccessList) Merge(other *StateAccessList) { - if s == nil || other == nil { - return - } - for addr, otherSlots := range other.list { - slots, exists := s.list[addr] - if !exists { - s.list[addr] = otherSlots - continue - } - maps.Copy(slots, otherSlots) - } -} - -// Copy returns a deep copy of the StateAccessList. -func (s *StateAccessList) Copy() *StateAccessList { - if s == nil { - return nil - } - cpy := &StateAccessList{ - list: make(map[common.Address]StorageAccessList, len(s.list)), - } - for addr, slots := range s.list { - cpy.list[addr] = maps.Clone(slots) - } - return cpy -} diff --git a/core/types/bal/bal.go b/core/types/bal/bal.go index 99ead8d6f0..9cbc1faeb9 100644 --- a/core/types/bal/bal.go +++ b/core/types/bal/bal.go @@ -71,8 +71,8 @@ type ConstructionBlockAccessList struct { } // NewConstructionBlockAccessList instantiates an empty access list. -func NewConstructionBlockAccessList() ConstructionBlockAccessList { - return ConstructionBlockAccessList{ +func NewConstructionBlockAccessList() *ConstructionBlockAccessList { + return &ConstructionBlockAccessList{ Accounts: make(map[common.Address]*ConstructionAccountAccess), } } @@ -169,5 +169,5 @@ func (b *ConstructionBlockAccessList) Copy() *ConstructionBlockAccessList { aaCopy.CodeChange = codes res.Accounts[addr] = &aaCopy } - return &res + return res } diff --git a/core/vm/interface.go b/core/vm/interface.go index 487d8002f9..a9938c2a28 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -98,5 +98,6 @@ type StateDB interface { AccessEvents() *state.AccessEvents // Finalise must be invoked at the end of a transaction - Finalise(bool) *bal.StateAccessList + Finalise(bool) *bal.ConstructionBlockAccessList + SetTxContext(thash common.Hash, ti int, blockAccessIndex uint32) } diff --git a/eth/state_accessor.go b/eth/state_accessor.go index 53dfb7d458..284ddf4305 100644 --- a/eth/state_accessor.go +++ b/eth/state_accessor.go @@ -265,7 +265,7 @@ func (eth *Ethereum) stateAtTransaction(ctx context.Context, block *types.Block, msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee()) // Not yet the searched for transaction, execute on top of the current state - statedb.SetTxContext(tx.Hash(), idx) + statedb.SetTxContext(tx.Hash(), idx, uint32(idx+1)) if _, err := core.ApplyMessage(evm, msg, nil); err != nil { return nil, vm.BlockContext{}, nil, nil, fmt.Errorf("transaction %#x failed: %v", tx.Hash(), err) } diff --git a/eth/tracers/api.go b/eth/tracers/api.go index d9e40f7ec1..0df02388b3 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -530,7 +530,7 @@ func (api *API) IntermediateRoots(ctx context.Context, hash common.Hash, config return nil, err } msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee()) - statedb.SetTxContext(tx.Hash(), i) + statedb.SetTxContext(tx.Hash(), i, uint32(i+1)) if _, err := core.ApplyMessage(evm, msg, nil); err != nil { log.Warn("Tracing intermediate roots did not complete", "txindex", i, "txhash", tx.Hash(), "err", err) // We intentionally don't return the error here: if we do, then the RPC server will not @@ -681,7 +681,7 @@ txloop: // Generate the next state snapshot fast without tracing msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee()) - statedb.SetTxContext(tx.Hash(), i) + statedb.SetTxContext(tx.Hash(), i, uint32(i+1)) if _, err := core.ApplyMessage(evm, msg, nil); err != nil { failed = err break txloop @@ -793,7 +793,7 @@ func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block }) ) // Execute the transaction and flush any traces to disk - statedb.SetTxContext(tx.Hash(), i) + statedb.SetTxContext(tx.Hash(), i, uint32(i+1)) if tracer.OnTxStart != nil { tracer.OnTxStart(evm.GetVMContext(), tx, msg.From) } @@ -1016,7 +1016,7 @@ func (api *API) traceTx(ctx context.Context, tx *types.Transaction, message *cor defer cancel() // Call Prepare to clear out the statedb access list - statedb.SetTxContext(txctx.TxHash, txctx.TxIndex) + statedb.SetTxContext(txctx.TxHash, txctx.TxIndex, uint32(txctx.TxIndex+1)) _, err = core.ApplyTransactionWithEVM(message, core.NewGasPool(message.GasLimit), statedb, vmctx.BlockNumber, txctx.BlockHash, vmctx.Time, tx, evm) if err != nil { diff --git a/internal/ethapi/simulate.go b/internal/ethapi/simulate.go index c34970578c..fa2ff2c32b 100644 --- a/internal/ethapi/simulate.go +++ b/internal/ethapi/simulate.go @@ -340,7 +340,7 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, tracer.reset(txHash, uint(i)) // EoA check is always skipped, even in validation mode. - sim.state.SetTxContext(txHash, i) + sim.state.SetTxContext(txHash, i, uint32(i+1)) msg := call.ToMessage(header.BaseFee, !sim.validate) result, err := applyMessageWithEVM(ctx, evm, msg, timeout, gp) if err != nil { @@ -390,8 +390,8 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, header.BlobGasUsed = &blobGasUsed } - // Run post-execution system calls - requests, err := core.PostExecution(ctx, sim.chainConfig, header.Number, header.Time, allLogs, evm) + // Process EIP-7685 requests + requests, err := core.PostExecution(ctx, sim.chainConfig, header.Number, header.Time, allLogs, evm, uint32(len(block.Calls)+1)) if err != nil { return nil, nil, nil, err } diff --git a/miner/worker.go b/miner/worker.go index ccafa20b29..026bafc4e5 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -167,7 +167,7 @@ func (miner *Miner) generateWork(ctx context.Context, genParam *generateParams, // otherwise, fill the block with the current transactions from the txpool if genParam.forceOverrides && len(genParam.overrideTxs) > 0 { for _, tx := range genParam.overrideTxs { - work.state.SetTxContext(tx.Hash(), work.tcount) + work.state.SetTxContext(tx.Hash(), work.tcount, uint32(work.tcount+1)) if err := miner.commitTransaction(ctx, work, tx); err != nil { // all passed transactions HAVE to be valid at this point return &newPayloadResult{err: err} @@ -208,7 +208,7 @@ func (miner *Miner) generateWork(ctx context.Context, genParam *generateParams, } // Collect consensus-layer requests if Prague is enabled. - requests, err := core.PostExecution(ctx, miner.chainConfig, work.header.Number, work.header.Time, allLogs, work.evm) + requests, err := core.PostExecution(ctx, miner.chainConfig, work.header.Number, work.header.Time, allLogs, work.evm, uint32(work.tcount+1)) if err != nil { return &newPayloadResult{err: err} } @@ -502,7 +502,7 @@ func (miner *Miner) commitTransactions(ctx context.Context, env *environment, pl continue } // Start executing the transaction - env.state.SetTxContext(tx.Hash(), env.tcount) + env.state.SetTxContext(tx.Hash(), env.tcount, uint32(env.tcount+1)) err := miner.commitTransaction(ctx, env, tx) switch { From da34eb59fdee4b0d12e3cf0b8a5e5b3546cb0632 Mon Sep 17 00:00:00 2001 From: Barnabas Busa Date: Wed, 13 May 2026 21:08:21 +0200 Subject: [PATCH 53/63] node: default OpenTelemetry SampleRatio to 1.0 (#34948) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary The `--rpc.telemetry.sample-ratio` flag declares `Value: 1.0` and `geth --help` advertises `(default: 1)`. In practice, however, omitting the flag produces a sample ratio of `0`, causing `sdktrace.TraceIDRatioBased(0)` to drop 100% of spans. Users who enable `--rpc.telemetry` see the `OpenTelemetry trace export enabled` log line and a clean startup, but no traces ever leave the process. The root cause is the interaction between two pieces of code: 1. `cmd/utils/flags.go:setOpenTelemetry` (added in #34062) only copies the flag value when `ctx.IsSet(...)` returns true: ```go if ctx.IsSet(RPCTelemetrySampleRatioFlag.Name) { tcfg.SampleRatio = ctx.Float64(RPCTelemetrySampleRatioFlag.Name) } ``` That is the right pattern for "don't clobber a config-file value with the CLI default," but it implies that something else must initialise the field when neither source sets it. 2. `node/defaults.go:DefaultConfig` never initialises `OpenTelemetry.SampleRatio`, leaving it at the float64 zero value. The result for the common CLI-only user (no TOML config) is `SampleRatio = 0` → every span is silently dropped, despite the documented default of 1. ## Change Seed `OpenTelemetry: OpenTelemetryConfig{SampleRatio: 1.0}` in `node.DefaultConfig` so the documented default matches runtime behavior and the `ctx.IsSet` guard in `setOpenTelemetry` continues to do what it was designed to do. --- cmd/utils/flags.go | 2 +- node/defaults.go | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index ea0f6f5ee4..c41cf4ee40 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -1104,7 +1104,7 @@ Please note that --` + MetricsHTTPFlag.Name + ` must be set to start the server. RPCTelemetrySampleRatioFlag = &cli.Float64Flag{ Name: "rpc.telemetry.sample-ratio", Usage: "Defines the sampling ratio for RPC telemetry (0.0 to 1.0)", - Value: 1.0, + Value: node.DefaultConfig.OpenTelemetry.SampleRatio, Category: flags.APICategory, } // Era flags are a group of flags related to the era archive format. diff --git a/node/defaults.go b/node/defaults.go index 403a7f88a3..3410fa2ae5 100644 --- a/node/defaults.go +++ b/node/defaults.go @@ -76,6 +76,9 @@ var DefaultConfig = Config{ DiscoveryV5: true, }, DBEngine: "", // Use whatever exists, will default to Pebble if non-existent and supported + OpenTelemetry: OpenTelemetryConfig{ + SampleRatio: 1.0, + }, } // DefaultDataDir is the default data directory to use for the databases and other From 31bb680997b0d24554ddd3eab8da8e179eed9ee7 Mon Sep 17 00:00:00 2001 From: cui Date: Thu, 14 May 2026 19:45:49 +0800 Subject: [PATCH 54/63] miner: re-use basefee and big.Int in loop (#34783) --- miner/worker.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/miner/worker.go b/miner/worker.go index 026bafc4e5..1ecee96688 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -601,10 +601,14 @@ func (miner *Miner) fillTransactions(ctx context.Context, interrupt *atomic.Int3 // totalFees computes total consumed miner fees in Wei. Block transactions and receipts have to have the same order. func totalFees(block *types.Block, receipts []*types.Receipt) *big.Int { + baseFee := block.BaseFee() feesWei := new(big.Int) + var gasUsed, product big.Int for i, tx := range block.Transactions() { - minerFee, _ := tx.EffectiveGasTip(block.BaseFee()) - feesWei.Add(feesWei, new(big.Int).Mul(new(big.Int).SetUint64(receipts[i].GasUsed), minerFee)) + minerFee, _ := tx.EffectiveGasTip(baseFee) + gasUsed.SetUint64(receipts[i].GasUsed) + product.Mul(&gasUsed, minerFee) + feesWei.Add(feesWei, &product) } return feesWei } From 6f6d006f74ffc650b9a598e8fcb1c757b8aaa15a Mon Sep 17 00:00:00 2001 From: Sina M <1591639+s1na@users.noreply.github.com> Date: Fri, 15 May 2026 12:04:37 +0200 Subject: [PATCH 55/63] core/txpool/blobpool: silence GetRLP miss-log spam (#34965) Avoids every legacy tx hash query hitting the blob pool on the path of BlobPool.GetRLP. --- core/txpool/blobpool/blobpool.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/txpool/blobpool/blobpool.go b/core/txpool/blobpool/blobpool.go index f8021e00c4..3b2bc03422 100644 --- a/core/txpool/blobpool/blobpool.go +++ b/core/txpool/blobpool/blobpool.go @@ -1575,12 +1575,15 @@ func (p *BlobPool) Get(hash common.Hash) *types.Transaction { // e.g. type_byte || [..., version, [blobs], [comms], [proofs]] func (p *BlobPool) GetRLP(hash common.Hash) []byte { data := p.getRLP(hash) + if len(data) == 0 { + // Not in this pool, do not log. + return nil + } rlp, err := encodeForNetwork(data) if err != nil { log.Error("Failed to encode pooled tx into the network type", "hash", hash, "err", err) return nil } - return rlp } From 8a0223e8da596a409df02c11027320df97327e83 Mon Sep 17 00:00:00 2001 From: cui Date: Fri, 15 May 2026 21:51:46 +0800 Subject: [PATCH 56/63] core/txpool: use blobTxForPool inside of `Reset` function (#34960) This PR fixes a bug in the current blobpool `Reset` function where it used the Transaction type instead of blobTxForPool. Decoding transactions fetched from the pool as Transaction type caused an error because the blobpool stores blobTxForPool types. --- core/txpool/blobpool/blobpool.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/txpool/blobpool/blobpool.go b/core/txpool/blobpool/blobpool.go index 3b2bc03422..d33629365f 100644 --- a/core/txpool/blobpool/blobpool.go +++ b/core/txpool/blobpool/blobpool.go @@ -1107,13 +1107,13 @@ func (p *BlobPool) Reset(oldHead, newHead *types.Header) { log.Error("Blobs missing for announcable transaction", "from", addr, "nonce", meta.nonce, "id", meta.id, "err", err) continue } - var tx types.Transaction - if err = rlp.DecodeBytes(data, &tx); err != nil { + var ptx blobTxForPool + if err = rlp.DecodeBytes(data, &ptx); err != nil { log.Error("Blobs corrupted for announcable transaction", "from", addr, "nonce", meta.nonce, "id", meta.id, "err", err) continue } - announcable = append(announcable, tx.WithoutBlobTxSidecar()) - log.Trace("Blob transaction now announcable", "from", addr, "nonce", meta.nonce, "id", meta.id, "hash", tx.Hash()) + announcable = append(announcable, ptx.Tx) + log.Trace("Blob transaction now announcable", "from", addr, "nonce", meta.nonce, "id", meta.id, "hash", ptx.Tx.Hash()) } } } From d4027f3d4654b374b0962946b03d6f12e3b269ee Mon Sep 17 00:00:00 2001 From: Andrii Furmanets Date: Mon, 18 May 2026 05:37:12 +0300 Subject: [PATCH 57/63] node: normalize HTTP vhost host matching (#34693) --- node/rpcstack.go | 1 + node/rpcstack_test.go | 3 +++ 2 files changed, 4 insertions(+) diff --git a/node/rpcstack.go b/node/rpcstack.go index 20d488b734..1db2ed3f44 100644 --- a/node/rpcstack.go +++ b/node/rpcstack.go @@ -463,6 +463,7 @@ func (h *virtualHostHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Either invalid (too many colons) or no port specified host = r.Host } + host = strings.ToLower(host) if ipAddr := net.ParseIP(host); ipAddr != nil { // It's an IP address, we can serve that h.next.ServeHTTP(w, r) diff --git a/node/rpcstack_test.go b/node/rpcstack_test.go index bd75dac4eb..f5668abb08 100644 --- a/node/rpcstack_test.go +++ b/node/rpcstack_test.go @@ -60,6 +60,9 @@ func TestVhosts(t *testing.T) { resp := rpcRequest(t, url, testMethod, "host", "test") assert.Equal(t, resp.StatusCode, http.StatusOK) + respUpper := rpcRequest(t, url, testMethod, "host", "TeSt:1234") + assert.Equal(t, respUpper.StatusCode, http.StatusOK) + resp2 := rpcRequest(t, url, testMethod, "host", "bad") assert.Equal(t, resp2.StatusCode, http.StatusForbidden) } From 3d1e6aa6c395540126b93dd936f39e16e72fc47f Mon Sep 17 00:00:00 2001 From: cui Date: Mon, 18 May 2026 22:30:41 +0800 Subject: [PATCH 58/63] signer/core: fix unconditional http request metadata scheme overwrite (#34653) --- signer/core/api.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/signer/core/api.go b/signer/core/api.go index 12acf925f0..3b7b53a312 100644 --- a/signer/core/api.go +++ b/signer/core/api.go @@ -196,8 +196,9 @@ func MetadataFromContext(ctx context.Context) Metadata { if info.Transport != "" { if info.Transport == "http" { m.Scheme = info.HTTP.Version + } else { + m.Scheme = info.Transport } - m.Scheme = info.Transport } if info.RemoteAddr != "" { m.Remote = info.RemoteAddr From 1149f76dca22c05976f8ac33b167cc69aaff2de8 Mon Sep 17 00:00:00 2001 From: William Morriss Date: Tue, 19 May 2026 01:05:00 -0500 Subject: [PATCH 59/63] internal/ethapi: add eth_baseFee RPC method (#34904) This method is similar to `eth_blobBaseFee` but returns the next base fee. --- eth/api_backend.go | 8 ++++++++ internal/ethapi/api.go | 5 +++++ internal/ethapi/api_test.go | 1 + internal/ethapi/backend.go | 1 + internal/ethapi/transaction_args_test.go | 1 + 5 files changed, 16 insertions(+) diff --git a/eth/api_backend.go b/eth/api_backend.go index 33fe4fe5d9..8bf91ba680 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -26,6 +26,7 @@ import ( "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/consensus/misc/eip1559" "github.com/ethereum/go-ethereum/consensus/misc/eip4844" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/filtermaps" @@ -430,6 +431,13 @@ func (b *EthAPIBackend) FeeHistory(ctx context.Context, blockCount uint64, lastB return b.gpo.FeeHistory(ctx, blockCount, lastBlock, rewardPercentiles) } +func (b *EthAPIBackend) BaseFee(ctx context.Context) *big.Int { + if b.ChainConfig().IsLondon(b.CurrentHeader().Number) { + return eip1559.CalcBaseFee(b.ChainConfig(), b.CurrentHeader()) + } + return nil +} + func (b *EthAPIBackend) BlobBaseFee(ctx context.Context) *big.Int { if excess := b.CurrentHeader().ExcessBlobGas; excess != nil { return eip4844.CalcBlobFee(b.ChainConfig(), b.CurrentHeader()) diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 6d38c6c7c8..68f56920ab 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -146,6 +146,11 @@ func (api *EthereumAPI) BlobBaseFee(ctx context.Context) *hexutil.Big { return (*hexutil.Big)(api.b.BlobBaseFee(ctx)) } +// BaseFee returns the base fee for the next block. +func (api *EthereumAPI) BaseFee(ctx context.Context) *hexutil.Big { + return (*hexutil.Big)(api.b.BaseFee(ctx)) +} + // Syncing returns false in case the node is currently not syncing with the network. It can be up-to-date or has not // yet received the latest block headers from its peers. In case it is synchronizing: // - startingBlock: block number this node started to synchronize from diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index 63e75bd3e3..f191643ce2 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -500,6 +500,7 @@ func (b testBackend) FeeHistory(ctx context.Context, blockCount uint64, lastBloc return nil, nil, nil, nil, nil, nil, nil } func (b testBackend) BlobBaseFee(ctx context.Context) *big.Int { return new(big.Int) } +func (b testBackend) BaseFee(ctx context.Context) *big.Int { return new(big.Int) } func (b testBackend) ChainDb() ethdb.Database { return b.db } func (b testBackend) AccountManager() *accounts.Manager { return b.accman } func (b testBackend) ExtRPCEnabled() bool { return false } diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go index af3d592b82..65112a5294 100644 --- a/internal/ethapi/backend.go +++ b/internal/ethapi/backend.go @@ -46,6 +46,7 @@ type Backend interface { SuggestGasTipCap(ctx context.Context) (*big.Int, error) FeeHistory(ctx context.Context, blockCount uint64, lastBlock rpc.BlockNumber, rewardPercentiles []float64) (*big.Int, [][]*big.Int, []*big.Int, []float64, []*big.Int, []float64, error) BlobBaseFee(ctx context.Context) *big.Int + BaseFee(ctx context.Context) *big.Int ChainDb() ethdb.Database AccountManager() *accounts.Manager ExtRPCEnabled() bool diff --git a/internal/ethapi/transaction_args_test.go b/internal/ethapi/transaction_args_test.go index 30791f32b5..4b7774c9b7 100644 --- a/internal/ethapi/transaction_args_test.go +++ b/internal/ethapi/transaction_args_test.go @@ -318,6 +318,7 @@ func (b *backendMock) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { return big.NewInt(42), nil } func (b *backendMock) BlobBaseFee(ctx context.Context) *big.Int { return big.NewInt(42) } +func (b *backendMock) BaseFee(ctx context.Context) *big.Int { return big.NewInt(42) } func (b *backendMock) CurrentHeader() *types.Header { return b.current } func (b *backendMock) ChainConfig() *params.ChainConfig { return b.config } From 4f4bfdbea7c2f987306e5585e6a259ce20af5043 Mon Sep 17 00:00:00 2001 From: cui Date: Tue, 19 May 2026 18:21:43 +0800 Subject: [PATCH 60/63] beacon/light/sync: check error (#34818) --- beacon/light/sync/update_sync.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/beacon/light/sync/update_sync.go b/beacon/light/sync/update_sync.go index d84a3d64da..b15b967433 100644 --- a/beacon/light/sync/update_sync.go +++ b/beacon/light/sync/update_sync.go @@ -98,7 +98,10 @@ func (s *CheckpointInit) Process(requester request.Requester, events []request.E case ssDefault: if resp != nil { if checkpoint := resp.(*types.BootstrapData); checkpoint.Header.Hash() == common.Hash(req.(ReqCheckpointData)) { - s.chain.CheckpointInit(*checkpoint) + err := s.chain.CheckpointInit(*checkpoint) + if err != nil { + return + } s.initialized = true return } From 970e3cd6f01331fab4888d4f185f7081c88e029d Mon Sep 17 00:00:00 2001 From: cui Date: Tue, 19 May 2026 18:33:09 +0800 Subject: [PATCH 61/63] beacon/light: fix lock after lock deadlock (#34800) --- beacon/light/committee_chain.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/beacon/light/committee_chain.go b/beacon/light/committee_chain.go index 4fa87785c0..7fc735d893 100644 --- a/beacon/light/committee_chain.go +++ b/beacon/light/committee_chain.go @@ -182,6 +182,12 @@ func (s *CommitteeChain) Reset() { s.chainmu.Lock() defer s.chainmu.Unlock() + s.resetLocked() +} + +// ResetLocked resets the committee chain without locking. The caller should hold +// the chainmu lock. +func (s *CommitteeChain) resetLocked() { if err := s.rollback(0); err != nil { log.Error("Error writing batch into chain database", "error", err) } @@ -201,22 +207,22 @@ func (s *CommitteeChain) CheckpointInit(bootstrap types.BootstrapData) error { } period := bootstrap.Header.SyncPeriod() if err := s.deleteFixedCommitteeRootsFrom(period + 2); err != nil { - s.Reset() + s.resetLocked() return err } if s.addFixedCommitteeRoot(period, bootstrap.CommitteeRoot) != nil { - s.Reset() + s.resetLocked() if err := s.addFixedCommitteeRoot(period, bootstrap.CommitteeRoot); err != nil { - s.Reset() + s.resetLocked() return err } } if err := s.addFixedCommitteeRoot(period+1, common.Hash(bootstrap.CommitteeBranch[0])); err != nil { - s.Reset() + s.resetLocked() return err } if err := s.addCommittee(period, bootstrap.Committee); err != nil { - s.Reset() + s.resetLocked() return err } s.changeCounter++ From e3ce773b8c3f934869bbded6e57c0f24a9983a24 Mon Sep 17 00:00:00 2001 From: cui Date: Tue, 19 May 2026 21:22:03 +0800 Subject: [PATCH 62/63] internal/ethapi: propagate SetHead errors to API (#35001) Return blockchain rewind failures from debug_setHead instead of ignoring them. --- eth/api_backend.go | 4 ++-- internal/ethapi/api.go | 3 +-- internal/ethapi/api_test.go | 2 +- internal/ethapi/backend.go | 2 +- internal/ethapi/transaction_args_test.go | 2 +- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/eth/api_backend.go b/eth/api_backend.go index 8bf91ba680..5e3558d8eb 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -62,9 +62,9 @@ func (b *EthAPIBackend) CurrentBlock() *types.Header { return b.eth.blockchain.CurrentBlock() } -func (b *EthAPIBackend) SetHead(number uint64) { +func (b *EthAPIBackend) SetHead(number uint64) error { b.eth.handler.downloader.Cancel() - b.eth.blockchain.SetHead(number) + return b.eth.blockchain.SetHead(number) } func (b *EthAPIBackend) HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) { diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 68f56920ab..109169e0b0 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -2131,8 +2131,7 @@ func (api *DebugAPI) SetHead(number hexutil.Uint64) error { if header.Number.Uint64() <= uint64(number) { return errors.New("not allowed to rewind to a future block") } - api.b.SetHead(uint64(number)) - return nil + return api.b.SetHead(uint64(number)) } // NetAPI offers network related RPC methods diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index f191643ce2..561ce2c2d2 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -508,7 +508,7 @@ func (b testBackend) RPCGasCap() uint64 { return 10000000 func (b testBackend) RPCEVMTimeout() time.Duration { return time.Second } func (b testBackend) RPCTxFeeCap() float64 { return 0 } func (b testBackend) UnprotectedAllowed() bool { return false } -func (b testBackend) SetHead(number uint64) {} +func (b testBackend) SetHead(number uint64) error { return nil } func (b testBackend) HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) { if number == rpc.LatestBlockNumber { return b.chain.CurrentBlock(), nil diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go index 65112a5294..f23be85782 100644 --- a/internal/ethapi/backend.go +++ b/internal/ethapi/backend.go @@ -58,7 +58,7 @@ type Backend interface { RPCTxSyncMaxTimeout() time.Duration // Blockchain API - SetHead(number uint64) + SetHead(number uint64) error HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) HeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*types.Header, error) diff --git a/internal/ethapi/transaction_args_test.go b/internal/ethapi/transaction_args_test.go index 4b7774c9b7..ccb46a810d 100644 --- a/internal/ethapi/transaction_args_test.go +++ b/internal/ethapi/transaction_args_test.go @@ -337,7 +337,7 @@ func (b *backendMock) RPCGasCap() uint64 { return 0 } func (b *backendMock) RPCEVMTimeout() time.Duration { return time.Second } func (b *backendMock) RPCTxFeeCap() float64 { return 0 } func (b *backendMock) UnprotectedAllowed() bool { return false } -func (b *backendMock) SetHead(number uint64) {} +func (b *backendMock) SetHead(number uint64) error { return nil } func (b *backendMock) HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) { return nil, nil } From 1bdc4a60d958ab3f5ef26f8bd428486f5efd18aa Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Tue, 19 May 2026 21:51:53 +0800 Subject: [PATCH 63/63] core, consensus, internal, eth, miner: construct block accessList (#34957) This PR finally lands EIP-7928, collecting the block accessList during the block execution and verifying against the block header. --------- Co-authored-by: jwasinger Co-authored-by: Marius van der Wijden --- beacon/engine/gen_ed.go | 79 +- beacon/engine/types.go | 134 +- cmd/evm/internal/t8ntool/execution.go | 16 +- consensus/beacon/consensus.go | 20 +- consensus/clique/clique.go | 3 +- consensus/consensus.go | 7 +- consensus/ethash/consensus.go | 3 +- core/bal_test.go | 1319 +++++++++++++++++ core/bintrie_witness_test.go | 3 +- core/block_validator.go | 39 + core/chain_makers.go | 27 +- core/genesis.go | 1 + core/state_processor.go | 109 +- core/state_transition.go | 3 +- core/types.go | 5 + core/types/bal/bal.go | 54 +- core/types/bal/bal_encoding.go | 47 +- core/types/bal/bal_test.go | 136 +- core/types/block.go | 5 +- core/types/hashes.go | 3 + core/vm/evm.go | 5 + eth/tracers/api.go | 2 +- .../tracetest/selfdestruct_state_test.go | 2 +- internal/ethapi/simulate.go | 21 +- miner/worker.go | 29 +- params/protocol_params.go | 10 + 26 files changed, 1883 insertions(+), 199 deletions(-) create mode 100644 core/bal_test.go diff --git a/beacon/engine/gen_ed.go b/beacon/engine/gen_ed.go index c733b3f350..02a1fd3805 100644 --- a/beacon/engine/gen_ed.go +++ b/beacon/engine/gen_ed.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" ) var _ = (*executableDataMarshaling)(nil) @@ -17,24 +18,25 @@ var _ = (*executableDataMarshaling)(nil) // MarshalJSON marshals as JSON. func (e ExecutableData) MarshalJSON() ([]byte, error) { type ExecutableData struct { - ParentHash common.Hash `json:"parentHash" gencodec:"required"` - FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"` - StateRoot common.Hash `json:"stateRoot" gencodec:"required"` - ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"` - LogsBloom hexutil.Bytes `json:"logsBloom" gencodec:"required"` - Random common.Hash `json:"prevRandao" gencodec:"required"` - Number hexutil.Uint64 `json:"blockNumber" gencodec:"required"` - GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"` - GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"` - Timestamp hexutil.Uint64 `json:"timestamp" gencodec:"required"` - ExtraData hexutil.Bytes `json:"extraData" gencodec:"required"` - BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"` - BlockHash common.Hash `json:"blockHash" gencodec:"required"` - Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` - Withdrawals []*types.Withdrawal `json:"withdrawals"` - BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` - ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` - SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"` + ParentHash common.Hash `json:"parentHash" gencodec:"required"` + FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"` + StateRoot common.Hash `json:"stateRoot" gencodec:"required"` + ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"` + LogsBloom hexutil.Bytes `json:"logsBloom" gencodec:"required"` + Random common.Hash `json:"prevRandao" gencodec:"required"` + Number hexutil.Uint64 `json:"blockNumber" gencodec:"required"` + GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"` + GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"` + Timestamp hexutil.Uint64 `json:"timestamp" gencodec:"required"` + ExtraData hexutil.Bytes `json:"extraData" gencodec:"required"` + BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"` + BlockHash common.Hash `json:"blockHash" gencodec:"required"` + Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` + Withdrawals []*types.Withdrawal `json:"withdrawals"` + BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` + ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` + SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"` + BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"` } var enc ExecutableData enc.ParentHash = e.ParentHash @@ -60,30 +62,32 @@ func (e ExecutableData) MarshalJSON() ([]byte, error) { enc.BlobGasUsed = (*hexutil.Uint64)(e.BlobGasUsed) enc.ExcessBlobGas = (*hexutil.Uint64)(e.ExcessBlobGas) enc.SlotNumber = (*hexutil.Uint64)(e.SlotNumber) + enc.BlockAccessList = e.BlockAccessList return json.Marshal(&enc) } // UnmarshalJSON unmarshals from JSON. func (e *ExecutableData) UnmarshalJSON(input []byte) error { type ExecutableData struct { - ParentHash *common.Hash `json:"parentHash" gencodec:"required"` - FeeRecipient *common.Address `json:"feeRecipient" gencodec:"required"` - StateRoot *common.Hash `json:"stateRoot" gencodec:"required"` - ReceiptsRoot *common.Hash `json:"receiptsRoot" gencodec:"required"` - LogsBloom *hexutil.Bytes `json:"logsBloom" gencodec:"required"` - Random *common.Hash `json:"prevRandao" gencodec:"required"` - Number *hexutil.Uint64 `json:"blockNumber" gencodec:"required"` - GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"` - GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"` - Timestamp *hexutil.Uint64 `json:"timestamp" gencodec:"required"` - ExtraData *hexutil.Bytes `json:"extraData" gencodec:"required"` - BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"` - BlockHash *common.Hash `json:"blockHash" gencodec:"required"` - Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` - Withdrawals []*types.Withdrawal `json:"withdrawals"` - BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` - ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` - SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"` + ParentHash *common.Hash `json:"parentHash" gencodec:"required"` + FeeRecipient *common.Address `json:"feeRecipient" gencodec:"required"` + StateRoot *common.Hash `json:"stateRoot" gencodec:"required"` + ReceiptsRoot *common.Hash `json:"receiptsRoot" gencodec:"required"` + LogsBloom *hexutil.Bytes `json:"logsBloom" gencodec:"required"` + Random *common.Hash `json:"prevRandao" gencodec:"required"` + Number *hexutil.Uint64 `json:"blockNumber" gencodec:"required"` + GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"` + GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"` + Timestamp *hexutil.Uint64 `json:"timestamp" gencodec:"required"` + ExtraData *hexutil.Bytes `json:"extraData" gencodec:"required"` + BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"` + BlockHash *common.Hash `json:"blockHash" gencodec:"required"` + Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` + Withdrawals []*types.Withdrawal `json:"withdrawals"` + BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` + ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` + SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"` + BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"` } var dec ExecutableData if err := json.Unmarshal(input, &dec); err != nil { @@ -160,5 +164,8 @@ func (e *ExecutableData) UnmarshalJSON(input []byte) error { if dec.SlotNumber != nil { e.SlotNumber = (*uint64)(dec.SlotNumber) } + if dec.BlockAccessList != nil { + e.BlockAccessList = dec.BlockAccessList + } return nil } diff --git a/beacon/engine/types.go b/beacon/engine/types.go index 9b0b186df7..5c31ee4e98 100644 --- a/beacon/engine/types.go +++ b/beacon/engine/types.go @@ -24,6 +24,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/trie" ) @@ -82,24 +83,25 @@ type payloadAttributesMarshaling struct { // ExecutableData is the data necessary to execute an EL payload. type ExecutableData struct { - ParentHash common.Hash `json:"parentHash" gencodec:"required"` - FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"` - StateRoot common.Hash `json:"stateRoot" gencodec:"required"` - ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"` - LogsBloom []byte `json:"logsBloom" gencodec:"required"` - Random common.Hash `json:"prevRandao" gencodec:"required"` - Number uint64 `json:"blockNumber" gencodec:"required"` - GasLimit uint64 `json:"gasLimit" gencodec:"required"` - GasUsed uint64 `json:"gasUsed" gencodec:"required"` - Timestamp uint64 `json:"timestamp" gencodec:"required"` - ExtraData []byte `json:"extraData" gencodec:"required"` - BaseFeePerGas *big.Int `json:"baseFeePerGas" gencodec:"required"` - BlockHash common.Hash `json:"blockHash" gencodec:"required"` - Transactions [][]byte `json:"transactions" gencodec:"required"` - Withdrawals []*types.Withdrawal `json:"withdrawals"` - BlobGasUsed *uint64 `json:"blobGasUsed"` - ExcessBlobGas *uint64 `json:"excessBlobGas"` - SlotNumber *uint64 `json:"slotNumber,omitempty"` + ParentHash common.Hash `json:"parentHash" gencodec:"required"` + FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"` + StateRoot common.Hash `json:"stateRoot" gencodec:"required"` + ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"` + LogsBloom []byte `json:"logsBloom" gencodec:"required"` + Random common.Hash `json:"prevRandao" gencodec:"required"` + Number uint64 `json:"blockNumber" gencodec:"required"` + GasLimit uint64 `json:"gasLimit" gencodec:"required"` + GasUsed uint64 `json:"gasUsed" gencodec:"required"` + Timestamp uint64 `json:"timestamp" gencodec:"required"` + ExtraData []byte `json:"extraData" gencodec:"required"` + BaseFeePerGas *big.Int `json:"baseFeePerGas" gencodec:"required"` + BlockHash common.Hash `json:"blockHash" gencodec:"required"` + Transactions [][]byte `json:"transactions" gencodec:"required"` + Withdrawals []*types.Withdrawal `json:"withdrawals"` + BlobGasUsed *uint64 `json:"blobGasUsed"` + ExcessBlobGas *uint64 `json:"excessBlobGas"` + SlotNumber *uint64 `json:"slotNumber,omitempty"` + BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"` } // JSON type overrides for executableData. @@ -303,56 +305,66 @@ func ExecutableDataToBlockNoHash(data ExecutableData, versionedHashes []common.H requestsHash = &h } - header := &types.Header{ - ParentHash: data.ParentHash, - UncleHash: types.EmptyUncleHash, - Coinbase: data.FeeRecipient, - Root: data.StateRoot, - TxHash: types.DeriveSha(types.Transactions(txs), trie.NewStackTrie(nil)), - ReceiptHash: data.ReceiptsRoot, - Bloom: types.BytesToBloom(data.LogsBloom), - Difficulty: common.Big0, - Number: new(big.Int).SetUint64(data.Number), - GasLimit: data.GasLimit, - GasUsed: data.GasUsed, - Time: data.Timestamp, - BaseFee: data.BaseFeePerGas, - Extra: data.ExtraData, - MixDigest: data.Random, - WithdrawalsHash: withdrawalsRoot, - ExcessBlobGas: data.ExcessBlobGas, - BlobGasUsed: data.BlobGasUsed, - ParentBeaconRoot: beaconRoot, - RequestsHash: requestsHash, - SlotNumber: data.SlotNumber, + // If Amsterdam is enabled, data.BlockAccessList is always non-nil, + // even for empty blocks with no state transitions. + // + // If Amsterdam is not enabled yet, blockAccessListHash is expected + // to be nil. + var blockAccessListHash *common.Hash + if data.BlockAccessList != nil { + hash := data.BlockAccessList.Hash() + blockAccessListHash = &hash } - return types.NewBlockWithHeader(header). - WithBody(types.Body{Transactions: txs, Uncles: nil, Withdrawals: data.Withdrawals}), - nil + header := &types.Header{ + ParentHash: data.ParentHash, + UncleHash: types.EmptyUncleHash, + Coinbase: data.FeeRecipient, + Root: data.StateRoot, + TxHash: types.DeriveSha(types.Transactions(txs), trie.NewStackTrie(nil)), + ReceiptHash: data.ReceiptsRoot, + Bloom: types.BytesToBloom(data.LogsBloom), + Difficulty: common.Big0, + Number: new(big.Int).SetUint64(data.Number), + GasLimit: data.GasLimit, + GasUsed: data.GasUsed, + Time: data.Timestamp, + BaseFee: data.BaseFeePerGas, + Extra: data.ExtraData, + MixDigest: data.Random, + WithdrawalsHash: withdrawalsRoot, + ExcessBlobGas: data.ExcessBlobGas, + BlobGasUsed: data.BlobGasUsed, + ParentBeaconRoot: beaconRoot, + RequestsHash: requestsHash, + SlotNumber: data.SlotNumber, + BlockAccessListHash: blockAccessListHash, + } + return types.NewBlockWithHeader(header).WithBody(types.Body{Transactions: txs, Uncles: nil, Withdrawals: data.Withdrawals}), nil } // BlockToExecutableData constructs the ExecutableData structure by filling the // fields from the given block. It assumes the given block is post-merge block. func BlockToExecutableData(block *types.Block, fees *big.Int, sidecars []*types.BlobTxSidecar, requests [][]byte) *ExecutionPayloadEnvelope { data := &ExecutableData{ - BlockHash: block.Hash(), - ParentHash: block.ParentHash(), - FeeRecipient: block.Coinbase(), - StateRoot: block.Root(), - Number: block.NumberU64(), - GasLimit: block.GasLimit(), - GasUsed: block.GasUsed(), - BaseFeePerGas: block.BaseFee(), - Timestamp: block.Time(), - ReceiptsRoot: block.ReceiptHash(), - LogsBloom: block.Bloom().Bytes(), - Transactions: encodeTransactions(block.Transactions()), - Random: block.MixDigest(), - ExtraData: block.Extra(), - Withdrawals: block.Withdrawals(), - BlobGasUsed: block.BlobGasUsed(), - ExcessBlobGas: block.ExcessBlobGas(), - SlotNumber: block.SlotNumber(), + BlockHash: block.Hash(), + ParentHash: block.ParentHash(), + FeeRecipient: block.Coinbase(), + StateRoot: block.Root(), + Number: block.NumberU64(), + GasLimit: block.GasLimit(), + GasUsed: block.GasUsed(), + BaseFeePerGas: block.BaseFee(), + Timestamp: block.Time(), + ReceiptsRoot: block.ReceiptHash(), + LogsBloom: block.Bloom().Bytes(), + Transactions: encodeTransactions(block.Transactions()), + Random: block.MixDigest(), + ExtraData: block.Extra(), + Withdrawals: block.Withdrawals(), + BlobGasUsed: block.BlobGasUsed(), + ExcessBlobGas: block.ExcessBlobGas(), + SlotNumber: block.SlotNumber(), + BlockAccessList: block.AccessList(), } // Add blobs. diff --git a/cmd/evm/internal/t8ntool/execution.go b/cmd/evm/internal/t8ntool/execution.go index a2de58ad46..043e675494 100644 --- a/cmd/evm/internal/t8ntool/execution.go +++ b/cmd/evm/internal/t8ntool/execution.go @@ -35,6 +35,7 @@ import ( "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto/keccak" "github.com/ethereum/go-ethereum/ethdb" @@ -172,6 +173,9 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, includedTxs types.Transactions blobGasUsed = uint64(0) receipts = make(types.Receipts, 0) + + // TODO return blockAccessList as a part of result + blockAccessList = bal.NewConstructionBlockAccessList() ) vmContext := vm.BlockContext{ CanTransfer: core.CanTransfer, @@ -231,14 +235,14 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, } evm := vm.NewEVM(vmContext, statedb, chainConfig, vmConfig) if beaconRoot := pre.Env.ParentBeaconBlockRoot; beaconRoot != nil { - core.ProcessBeaconBlockRoot(*beaconRoot, evm) + core.ProcessBeaconBlockRoot(*beaconRoot, evm, blockAccessList) } if pre.Env.BlockHashes != nil && chainConfig.IsPrague(new(big.Int).SetUint64(pre.Env.Number), pre.Env.Timestamp) { var ( prevNumber = pre.Env.Number - 1 prevHash = pre.Env.BlockHashes[math.HexOrDecimal64(prevNumber)] ) - core.ProcessParentBlockHash(prevHash, evm) + core.ProcessParentBlockHash(prevHash, evm, blockAccessList) } for i := 0; txIt.Next(); i++ { tx, err := txIt.Tx() @@ -271,11 +275,12 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, } } statedb.SetTxContext(tx.Hash(), len(receipts), uint32(len(receipts)+1)) + var ( snapshot = statedb.Snapshot() gp = gaspool.Snapshot() ) - receipt, err := core.ApplyTransactionWithEVM(msg, gaspool, statedb, vmContext.BlockNumber, blockHash, pre.Env.Timestamp, tx, evm) + receipt, bal, err := core.ApplyTransactionWithEVM(msg, gaspool, statedb, vmContext.BlockNumber, blockHash, pre.Env.Timestamp, tx, evm) if err != nil { statedb.RevertToSnapshot(snapshot) log.Info("rejected tx", "index", i, "hash", tx.Hash(), "from", msg.From, "error", err) @@ -292,6 +297,7 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, } blobGasUsed += txBlobGas receipts = append(receipts, receipt) + blockAccessList.Merge(bal) } statedb.IntermediateRoot(chainConfig.IsEIP158(vmContext.BlockNumber)) @@ -336,10 +342,12 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, for _, receipt := range receipts { allLogs = append(allLogs, receipt.Logs...) } - requests, err := core.PostExecution(context.Background(), chainConfig, vmContext.BlockNumber, vmContext.Time, allLogs, evm, uint32(len(receipts)+1)) + requests, bal, err := core.PostExecution(context.Background(), chainConfig, vmContext.BlockNumber, vmContext.Time, allLogs, evm, uint32(len(receipts)+1)) if err != nil { return nil, nil, nil, NewError(ErrorEVM, fmt.Errorf("failed to process post-execution: %v", err)) } + blockAccessList.Merge(bal) + // Commit block root, err := statedb.Commit(vmContext.BlockNumber.Uint64(), chainConfig.IsEIP158(vmContext.BlockNumber), chainConfig.IsCancun(vmContext.BlockNumber, vmContext.Time)) if err != nil { diff --git a/consensus/beacon/consensus.go b/consensus/beacon/consensus.go index 72ac75c036..4237418e73 100644 --- a/consensus/beacon/consensus.go +++ b/consensus/beacon/consensus.go @@ -27,6 +27,7 @@ import ( "github.com/ethereum/go-ethereum/consensus/misc/eip4844" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" @@ -342,9 +343,9 @@ func (beacon *Beacon) Prepare(chain consensus.ChainHeaderReader, header *types.H } // Finalize implements consensus.Engine and processes withdrawals on top. -func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body) { +func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body, blockAccessIndex uint32, bal *bal.ConstructionBlockAccessList) { if !beacon.IsPoSHeader(header) { - beacon.ethone.Finalize(chain, header, state, body) + beacon.ethone.Finalize(chain, header, state, body, blockAccessIndex, bal) return } // Withdrawals processing. @@ -352,7 +353,20 @@ func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types. // Convert amount from gwei to wei. amount := new(uint256.Int).SetUint64(w.Amount) amount = amount.Mul(amount, uint256.NewInt(params.GWei)) - state.AddBalance(w.Address, amount, tracing.BalanceIncreaseWithdrawal) + prev := state.AddBalance(w.Address, amount, tracing.BalanceIncreaseWithdrawal) + + // Populate the block-level accessList if Amsterdam is enabled + if chain.Config().IsAmsterdam(header.Number, header.Time) { + if w.Amount == 0 { + // Zero amount withdrawal, account is accessed potential + // without state changes. + bal.AccountRead(w.Address) + } else { + // Non-zero amount withdrawal, account is accessed with + // a balance change. + bal.BalanceChange(blockAccessIndex, w.Address, new(uint256.Int).Add(&prev, amount)) + } + } } // No block reward which is issued by consensus layer instead. } diff --git a/consensus/clique/clique.go b/consensus/clique/clique.go index ceaec44656..f44afde241 100644 --- a/consensus/clique/clique.go +++ b/consensus/clique/clique.go @@ -34,6 +34,7 @@ import ( "github.com/ethereum/go-ethereum/consensus/misc" "github.com/ethereum/go-ethereum/consensus/misc/eip1559" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto/keccak" @@ -573,7 +574,7 @@ func (c *Clique) Prepare(chain consensus.ChainHeaderReader, header *types.Header // Finalize implements consensus.Engine. There is no post-transaction // consensus rules in clique, do nothing here. -func (c *Clique) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body) { +func (c *Clique) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body, blockAccessIndex uint32, bal *bal.ConstructionBlockAccessList) { // No block rewards in PoA, so the state remains as is } diff --git a/consensus/consensus.go b/consensus/consensus.go index 4ba389292f..e4f7b7a6a1 100644 --- a/consensus/consensus.go +++ b/consensus/consensus.go @@ -22,6 +22,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/params" ) @@ -79,12 +80,12 @@ type Engine interface { // rules of a particular engine. The changes are executed inline. Prepare(chain ChainHeaderReader, header *types.Header) error - // Finalize runs any post-transaction state modifications (e.g. block rewards - // or process withdrawals) but does not assemble the block. + // Finalize runs any post-transaction consensus-specific state modifications + // (e.g. block rewards or process withdrawals) but does not assemble the block. // // Note: The state database might be updated to reflect any consensus rules // that happen at finalization (e.g. block rewards). - Finalize(chain ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body) + Finalize(chain ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body, blockAccessIndex uint32, bal *bal.ConstructionBlockAccessList) // Seal generates a new sealing request for the given input block and pushes // the result into the given channel. diff --git a/consensus/ethash/consensus.go b/consensus/ethash/consensus.go index ee9d9d97d6..21adc9d279 100644 --- a/consensus/ethash/consensus.go +++ b/consensus/ethash/consensus.go @@ -29,6 +29,7 @@ import ( "github.com/ethereum/go-ethereum/consensus/misc/eip1559" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto/keccak" "github.com/ethereum/go-ethereum/params" @@ -504,7 +505,7 @@ func (ethash *Ethash) Prepare(chain consensus.ChainHeaderReader, header *types.H } // Finalize implements consensus.Engine, accumulating the block and uncle rewards. -func (ethash *Ethash) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body) { +func (ethash *Ethash) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body, blockAccessIndex uint32, bal *bal.ConstructionBlockAccessList) { // Accumulate any block and uncle rewards accumulateRewards(chain.Config(), state, header, body.Uncles) } diff --git a/core/bal_test.go b/core/bal_test.go new file mode 100644 index 0000000000..f0b9dc6443 --- /dev/null +++ b/core/bal_test.go @@ -0,0 +1,1319 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package core + +import ( + "bytes" + "crypto/ecdsa" + "maps" + "math/big" + "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/core/types/bal" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" +) + +// EIP-7928 BAL inclusion tests. +// +// Each test exercises a single rule from the spec and asserts both presence +// and absence in the resulting block access list. + +// balChainConfig returns a MergedTestChainConfig clone with Amsterdam active from genesis. +func balChainConfig() *params.ChainConfig { + cfg := *params.MergedTestChainConfig + cfg.AmsterdamTime = new(uint64) + blob := *cfg.BlobScheduleConfig + blob.Amsterdam = blob.Osaka + cfg.BlobScheduleConfig = &blob + return &cfg +} + +// balTestEnv bundles common identities used across the tests. +type balTestEnv struct { + cfg *params.ChainConfig + signer types.Signer + key *ecdsa.PrivateKey + from common.Address + gspec *Genesis +} + +// newBALTestEnv builds an Amsterdam chain config, funds a sender and pre-deploys +// the EIP-7928 system contracts. Extra accounts can be merged into Alloc. +func newBALTestEnv(extra types.GenesisAlloc) *balTestEnv { + cfg := balChainConfig() + key, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + from := crypto.PubkeyToAddress(key.PublicKey) + + alloc := types.GenesisAlloc{ + from: {Balance: newGwei(1_000_000_000)}, + params.BeaconRootsAddress: {Nonce: 1, Code: params.BeaconRootsCode, Balance: common.Big0}, + params.HistoryStorageAddress: {Nonce: 1, Code: params.HistoryStorageCode, Balance: common.Big0}, + params.WithdrawalQueueAddress: {Nonce: 1, Code: params.WithdrawalQueueCode, Balance: common.Big0}, + params.ConsolidationQueueAddress: {Nonce: 1, Code: params.ConsolidationQueueCode, Balance: common.Big0}, + } + maps.Copy(alloc, extra) + return &balTestEnv{ + cfg: cfg, + signer: types.LatestSigner(cfg), + key: key, + from: from, + gspec: &Genesis{Config: cfg, Alloc: alloc}, + } +} + +// run generates exactly one Amsterdam block and returns its BAL. +func (e *balTestEnv) run(t *testing.T, gen func(*BlockGen)) (*bal.BlockAccessList, types.Receipts) { + t.Helper() + engine := beacon.New(ethash.NewFaker()) + _, blocks, receipts := GenerateChainWithGenesis(e.gspec, engine, 1, func(_ int, b *BlockGen) { + gen(b) + }) + if blocks[0].AccessList() == nil { + t.Fatal("expected non-nil block access list") + } + return blocks[0].AccessList(), receipts[0] +} + +// --- assertion helpers --- + +func findAccount(b *bal.BlockAccessList, addr common.Address) *bal.AccountAccess { + for i := range *b { + if (*b)[i].Address == addr { + return &(*b)[i] + } + } + return nil +} + +func hasSlotIn(slots []*uint256.Int, key common.Hash) bool { + want := new(uint256.Int).SetBytes(key[:]) + for _, s := range slots { + if s.Cmp(want) == 0 { + return true + } + } + return false +} + +func hasStorageWrite(b *bal.BlockAccessList, addr common.Address, key common.Hash) bool { + aa := findAccount(b, addr) + if aa == nil { + return false + } + want := new(uint256.Int).SetBytes(key[:]) + for _, w := range aa.StorageWrites { + if w.Slot.Cmp(want) == 0 { + return true + } + } + return false +} + +func assertPresent(t *testing.T, b *bal.BlockAccessList, addr common.Address) *bal.AccountAccess { + t.Helper() + aa := findAccount(b, addr) + if aa == nil { + t.Fatalf("address %x missing from BAL\n%s", addr, b.PrettyPrint()) + } + return aa +} + +func assertAbsent(t *testing.T, b *bal.BlockAccessList, addr common.Address) { + t.Helper() + if findAccount(b, addr) != nil { + t.Fatalf("address %x must NOT be in BAL\n%s", addr, b.PrettyPrint()) + } +} + +func assertEmpty(t *testing.T, aa *bal.AccountAccess) { + t.Helper() + if len(aa.StorageWrites) != 0 || len(aa.StorageReads) != 0 || + len(aa.BalanceChanges) != 0 || len(aa.NonceChanges) != 0 || len(aa.CodeChanges) != 0 { + t.Fatalf("expected empty change set for %x, got %+v", aa.Address, aa) + } +} + +// --- tx builders --- + +func (e *balTestEnv) tx(nonce uint64, to *common.Address, value *big.Int, gas uint64, tipGwei int64, data []byte) *types.Transaction { + return types.MustSignNewTx(e.key, e.signer, &types.DynamicFeeTx{ + ChainID: e.cfg.ChainID, + Nonce: nonce, + To: to, + Value: value, + Gas: gas, + GasFeeCap: newGwei(10), + GasTipCap: newGwei(tipGwei), + Data: data, + }) +} + +// ============================== Account inclusion ============================== + +// TestBALTxSenderAndRecipient: a value transfer records balance+nonce for sender +// and a balance entry for the recipient. +func TestBALTxSenderAndRecipient(t *testing.T) { + to := common.HexToAddress("0xc0ffee") + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &to, big.NewInt(1000), params.TxGas, 0, nil)) + }) + + sender := assertPresent(t, b, env.from) + if len(sender.NonceChanges) == 0 || sender.NonceChanges[0].Nonce != 1 { + t.Fatalf("sender nonce not bumped: %+v", sender.NonceChanges) + } + if len(sender.BalanceChanges) == 0 { + t.Fatalf("sender missing balance change") + } + recipient := assertPresent(t, b, to) + if len(recipient.BalanceChanges) != 1 || recipient.BalanceChanges[0].Balance.Uint64() != 1000 { + t.Fatalf("recipient balance: %+v", recipient.BalanceChanges) + } +} + +// TestBALZeroValueRecipient: a tx with value 0 still lists the recipient, +// but without a balance entry. +func TestBALZeroValueRecipient(t *testing.T) { + to := common.HexToAddress("0x0123456789abcdef") + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &to, big.NewInt(0), params.TxGas, 0, nil)) + }) + + r := assertPresent(t, b, to) + if len(r.BalanceChanges) != 0 { + t.Fatalf("zero-value recipient should have no balance entry: %+v", r.BalanceChanges) + } +} + +// TestBALEmptyBlockExcludesCoinbase: an empty block (no txs, no withdrawals) +// never touches the coinbase, so it must NOT appear in the BAL — the zero +// block reward alone does not trigger inclusion. +func TestBALEmptyBlockExcludesCoinbase(t *testing.T) { + coinbase := common.Address{0xc0} + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + // SetCoinbase initialises b.bal but does not record any access. + g.SetCoinbase(coinbase) + }) + assertAbsent(t, b, coinbase) +} + +// TestBALCoinbaseTipCapturesBalance: positive priority fee credits coinbase +// and the balance change appears in the BAL. +func TestBALCoinbaseTipCapturesBalance(t *testing.T) { + coinbase := common.Address{0xc0} + to := common.HexToAddress("0xabba") + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + g.SetCoinbase(coinbase) + g.AddTx(env.tx(0, &to, big.NewInt(0), params.TxGas, 2 /* gwei tip */, nil)) + }) + + cb := assertPresent(t, b, coinbase) + if len(cb.BalanceChanges) == 0 || cb.BalanceChanges[0].Balance.Sign() == 0 { + t.Fatalf("coinbase missing positive balance change: %+v", cb.BalanceChanges) + } +} + +// TestBALSystemAddressExcluded: SYSTEM_ADDRESS (0xff…fe) is not in the BAL +// for a regular block. +func TestBALSystemAddressExcluded(t *testing.T) { + to := common.HexToAddress("0xabba") + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &to, big.NewInt(0), params.TxGas, 0, nil)) + }) + assertAbsent(t, b, params.SystemAddress) +} + +// TestBALSystemAddressIncludedWhenTouched: SYSTEM_ADDRESS becomes a regular +// account in the BAL once it experiences state access (here: receives value). +func TestBALSystemAddressIncludedWhenTouched(t *testing.T) { + sys := params.SystemAddress + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &sys, big.NewInt(1000), params.TxGas, 0, nil)) + }) + + aa := assertPresent(t, b, sys) + if len(aa.BalanceChanges) != 1 || aa.BalanceChanges[0].Balance.Uint64() != 1000 { + t.Fatalf("system-address balance change missing: %+v", aa.BalanceChanges) + } +} + +// TestBALPrecompileInvokedFromContractIncluded: a precompile that is invoked +// indirectly — via STATICCALL from a regular contract — must still appear in +// the BAL with no balance entry. +func TestBALPrecompileInvokedFromContractIncluded(t *testing.T) { + identity := common.BytesToAddress([]byte{0x04}) + caller := common.HexToAddress("0xca11") + // PUSH1 0 (retSize) PUSH1 0 (retOff) PUSH1 0 (argsSize) PUSH1 0 (argsOff) + // PUSH20 0x04 GAS STATICCALL POP STOP + code := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x73} + code = append(code, identity.Bytes()...) + code = append(code, 0x5a, 0xfa, 0x50, 0x00) + + env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}}) + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil)) + }) + + aa := assertPresent(t, b, identity) + if len(aa.BalanceChanges) != 0 { + t.Fatalf("precompile invoked via STATICCALL must not record balance: %+v", aa.BalanceChanges) + } +} + +// TestBALPrecompileCalledNoValueIncluded: a tx targeting the identity precompile +// with zero value lists the precompile but records no balance entry. +func TestBALPrecompileCalledNoValueIncluded(t *testing.T) { + identity := common.BytesToAddress([]byte{0x04}) + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &identity, big.NewInt(0), 50_000, 0, []byte{0xde, 0xad})) + }) + + aa := assertPresent(t, b, identity) + if len(aa.BalanceChanges) != 0 { + t.Fatalf("precompile must not record balance change: %+v", aa.BalanceChanges) + } +} + +// TestBALPrecompileValueTransferRecordsBalance: a precompile receives ETH only +// in the form of a value transfer — the balance entry is then recorded. +func TestBALPrecompileValueTransferRecordsBalance(t *testing.T) { + identity := common.BytesToAddress([]byte{0x04}) + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &identity, big.NewInt(5), 50_000, 0, nil)) + }) + + aa := assertPresent(t, b, identity) + if len(aa.BalanceChanges) != 1 || aa.BalanceChanges[0].Balance.Uint64() != 5 { + t.Fatalf("precompile balance change wrong: %+v", aa.BalanceChanges) + } +} + +// TestBALBalanceProbeOnNonExistent: BALANCE against a never-allocated address +// still adds it to the BAL with an empty change set. +func TestBALBalanceProbeOnNonExistent(t *testing.T) { + probe := common.HexToAddress("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef") + caller := common.HexToAddress("0xc1") + code := append([]byte{0x73}, probe.Bytes()...) // PUSH20 probe + code = append(code, 0x31, 0x50, 0x00) // BALANCE, POP, STOP + + env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}}) + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil)) + }) + + assertEmpty(t, assertPresent(t, b, probe)) +} + +// TestBALExtCodeSizeProbeOnNonExistent: EXTCODESIZE against a never-allocated +// address adds it to the BAL with an empty change set. +func TestBALExtCodeSizeProbeOnNonExistent(t *testing.T) { + probe := common.HexToAddress("0xcafecafecafecafecafecafecafecafecafecafe") + caller := common.HexToAddress("0xc1") + code := append([]byte{0x73}, probe.Bytes()...) // PUSH20 probe + code = append(code, 0x3b, 0x50, 0x00) // EXTCODESIZE, POP, STOP + + env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}}) + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil)) + }) + + assertEmpty(t, assertPresent(t, b, probe)) +} + +// TestBALExtCodeHashProbeOnNonExistent: EXTCODEHASH against a never-allocated +// address adds it to the BAL with an empty change set. +func TestBALExtCodeHashProbeOnNonExistent(t *testing.T) { + probe := common.HexToAddress("0xfacefacefacefacefacefacefacefacefacefacE") + caller := common.HexToAddress("0xc1") + code := append([]byte{0x73}, probe.Bytes()...) // PUSH20 probe + code = append(code, 0x3f, 0x50, 0x00) // EXTCODEHASH, POP, STOP + + env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}}) + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil)) + }) + + assertEmpty(t, assertPresent(t, b, probe)) +} + +// TestBALExtCodeCopyProbeOnNonExistent: EXTCODECOPY against a never-allocated +// address adds it to the BAL with an empty change set. +func TestBALExtCodeCopyProbeOnNonExistent(t *testing.T) { + probe := common.HexToAddress("0xfeedfeedfeedfeedfeedfeedfeedfeedfeedfeed") + caller := common.HexToAddress("0xc1") + // PUSH1 0 (length) PUSH1 0 (codeOffset) PUSH1 0 (destOffset) + // PUSH20 probe EXTCODECOPY STOP + code := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x73} + code = append(code, probe.Bytes()...) + code = append(code, 0x3c, 0x00) // EXTCODECOPY, STOP + + env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}}) + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil)) + }) + + assertEmpty(t, assertPresent(t, b, probe)) +} + +// TestBALAccessListNotAutoPromoted: an EIP-2930 access-list entry that is +// never actually touched must NOT appear in the BAL. +func TestBALAccessListNotAutoPromoted(t *testing.T) { + to := common.HexToAddress("0xabba") + dormant := common.HexToAddress("0xd0d0") + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + tx := types.MustSignNewTx(env.key, env.signer, &types.DynamicFeeTx{ + ChainID: env.cfg.ChainID, + Nonce: 0, + To: &to, + Value: big.NewInt(0), + Gas: params.TxGas + 4000, + GasFeeCap: newGwei(10), + GasTipCap: newGwei(0), + AccessList: types.AccessList{{Address: dormant, StorageKeys: nil}}, + }) + g.AddTx(tx) + }) + + assertAbsent(t, b, dormant) +} + +// ============================== CALL family ============================== + +// makeStubCaller emits a single CALL-family op against `target` then STOPs, +// with zero call data and discarded return data. +// +// op = 0xf1 (CALL) / 0xf2 (CALLCODE): +// stack = retSize, retOff, argsSize, argsOff, value, addr, gas +// op = 0xf4 (DELEGATECALL) / 0xfa (STATICCALL): +// stack = retSize, retOff, argsSize, argsOff, addr, gas +func makeStubCaller(op byte, target common.Address) []byte { + // retSize, retOff, argsSize, argsOff = 0 + prelude := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00} + if op == 0xf1 || op == 0xf2 { // CALL/CALLCODE need an extra value=0 + prelude = append(prelude, 0x60, 0x00) + } + prelude = append(prelude, 0x73) // PUSH20 + prelude = append(prelude, target.Bytes()...) + prelude = append(prelude, 0x5a) // GAS + prelude = append(prelude, op) + prelude = append(prelude, 0x50, 0x00) // POP, STOP + return prelude +} + +// TestBALCallTargetWithEmptyChangeSet: a zero-value CALL to an existing +// contract that has no state changes lists the target with empty entries. +func TestBALCallTargetWithEmptyChangeSet(t *testing.T) { + target := common.HexToAddress("0xbabe") + env := newBALTestEnv(types.GenesisAlloc{ + target: {Code: []byte{0x00}, Balance: common.Big0}, // STOP + }) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &target, big.NewInt(0), 100_000, 0, nil)) + }) + + assertEmpty(t, assertPresent(t, b, target)) +} + +// TestBALCallCodeTargetIncluded: CALLCODE puts the target in the BAL with an +// empty change set (CALLCODE executes target's code in the caller's storage +// context, so the target itself records no state changes). +func TestBALCallCodeTargetIncluded(t *testing.T) { + target := common.HexToAddress("0xdeed") + caller := common.HexToAddress("0xca11") + env := newBALTestEnv(types.GenesisAlloc{ + caller: {Code: makeStubCaller(0xf2 /* CALLCODE */, target), Balance: common.Big0}, + target: {Code: []byte{0x00}, Balance: common.Big0}, + }) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &caller, big.NewInt(0), 200_000, 0, nil)) + }) + + assertPresent(t, b, caller) + assertEmpty(t, assertPresent(t, b, target)) +} + +// TestBALDelegateCallTargetIncluded: DELEGATECALL puts both caller and target +// in the BAL even when neither produces state changes. +func TestBALDelegateCallTargetIncluded(t *testing.T) { + target := common.HexToAddress("0xdeed") + caller := common.HexToAddress("0xca11") + env := newBALTestEnv(types.GenesisAlloc{ + caller: {Code: makeStubCaller(0xf4 /* DELEGATECALL */, target), Balance: common.Big0}, + target: {Code: []byte{0x00}, Balance: common.Big0}, + }) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &caller, big.NewInt(0), 200_000, 0, nil)) + }) + + assertPresent(t, b, caller) + assertEmpty(t, assertPresent(t, b, target)) +} + +// TestBALStaticCallTargetIncluded: STATICCALL puts the target in the BAL with +// no balance entry recorded. +func TestBALStaticCallTargetIncluded(t *testing.T) { + target := common.HexToAddress("0xdeed") + caller := common.HexToAddress("0xca11") + env := newBALTestEnv(types.GenesisAlloc{ + caller: {Code: makeStubCaller(0xfa /* STATICCALL */, target), Balance: common.Big0}, + target: {Code: []byte{0x00}, Balance: common.Big0}, + }) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &caller, big.NewInt(0), 200_000, 0, nil)) + }) + + assertPresent(t, b, caller) + assertEmpty(t, assertPresent(t, b, target)) +} + +// ============================== Revert behaviour ============================== + +// TestBALRevertedTxStillIncluded: a tx whose top-level call REVERTs still +// records the touched contract in the BAL with an empty change set. +func TestBALRevertedTxStillIncluded(t *testing.T) { + reverter := common.HexToAddress("0xbeef") + // PUSH1 0 PUSH1 0 REVERT + revertCode := []byte{0x60, 0x00, 0x60, 0x00, 0xfd} + env := newBALTestEnv(types.GenesisAlloc{reverter: {Code: revertCode, Balance: common.Big0}}) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &reverter, big.NewInt(0), 100_000, 0, nil)) + }) + + assertEmpty(t, assertPresent(t, b, reverter)) +} + +// TestBALSenderRecordedOnRevert: even when the top-level call reverts, the +// sender's final nonce and balance MUST be recorded. +func TestBALSenderRecordedOnRevert(t *testing.T) { + reverter := common.HexToAddress("0xbeef") + revertCode := []byte{0x60, 0x00, 0x60, 0x00, 0xfd} + env := newBALTestEnv(types.GenesisAlloc{reverter: {Code: revertCode, Balance: common.Big0}}) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &reverter, big.NewInt(0), 100_000, 0, nil)) + }) + + sender := assertPresent(t, b, env.from) + if len(sender.NonceChanges) == 0 || sender.NonceChanges[0].Nonce != 1 { + t.Fatalf("sender nonce must be bumped even on revert: %+v", sender.NonceChanges) + } + if len(sender.BalanceChanges) == 0 { + t.Fatalf("sender balance change (gas paid) must be present on revert") + } +} + +// ============================== Storage inclusion ============================== + +// TestBALStorageWriteRecorded: SSTORE places the slot in storage_changes and +// keeps it out of storage_reads. +func TestBALStorageWriteRecorded(t *testing.T) { + contract := common.HexToAddress("0xc1") + slot := common.BigToHash(big.NewInt(0x01)) + // PUSH1 0x42 PUSH1 0x01 SSTORE STOP + code := []byte{0x60, 0x42, 0x60, 0x01, 0x55, 0x00} + env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}}) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil)) + }) + + aa := assertPresent(t, b, contract) + if !hasStorageWrite(b, contract, slot) { + t.Fatalf("expected slot 0x01 in storage_changes\n%s", b.PrettyPrint()) + } + if hasSlotIn(aa.StorageReads, slot) { + t.Fatalf("slot 0x01 must NOT appear in storage_reads") + } +} + +// TestBALStorageSloadOnly: SLOAD without a write puts the slot in storage_reads. +func TestBALStorageSloadOnly(t *testing.T) { + contract := common.HexToAddress("0xc1") + slot := common.BigToHash(big.NewInt(0x07)) + // PUSH1 0x07 SLOAD POP STOP + code := []byte{0x60, 0x07, 0x54, 0x50, 0x00} + env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}}) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil)) + }) + + aa := assertPresent(t, b, contract) + if !hasSlotIn(aa.StorageReads, slot) { + t.Fatalf("expected slot in storage_reads\n%s", b.PrettyPrint()) + } + if hasStorageWrite(b, contract, slot) { + t.Fatalf("slot must NOT appear in storage_changes") + } +} + +// TestBALStorageReadThenWriteOnlyInWrites: SLOAD followed by SSTORE on the +// same slot drops the slot from storage_reads (write-wins invariant). +func TestBALStorageReadThenWriteOnlyInWrites(t *testing.T) { + contract := common.HexToAddress("0xc1") + slot := common.BigToHash(big.NewInt(0x05)) + // PUSH1 5 SLOAD POP PUSH1 0x42 PUSH1 5 SSTORE STOP + code := []byte{ + 0x60, 0x05, 0x54, 0x50, + 0x60, 0x42, 0x60, 0x05, 0x55, + 0x00, + } + env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}}) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil)) + }) + + aa := assertPresent(t, b, contract) + if !hasStorageWrite(b, contract, slot) { + t.Fatalf("slot must be in storage_changes\n%s", b.PrettyPrint()) + } + if hasSlotIn(aa.StorageReads, slot) { + t.Fatalf("slot must NOT appear in storage_reads (write-wins)\n%s", b.PrettyPrint()) + } +} + +// TestBALNoOpSSTOREDemotesToRead: an SSTORE whose value equals the committed +// value lands the slot in storage_reads only. +func TestBALNoOpSSTOREDemotesToRead(t *testing.T) { + contract := common.HexToAddress("0xc1") + slot := common.BigToHash(big.NewInt(0x09)) + // SSTORE(0x09, 0x42) — slot pre-state is 0x42, so the write is a no-op. + code := []byte{0x60, 0x42, 0x60, 0x09, 0x55, 0x00} + env := newBALTestEnv(types.GenesisAlloc{ + contract: { + Code: code, + Balance: common.Big0, + Storage: map[common.Hash]common.Hash{slot: common.BigToHash(big.NewInt(0x42))}, + }, + }) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil)) + }) + + aa := assertPresent(t, b, contract) + if !hasSlotIn(aa.StorageReads, slot) { + t.Fatalf("no-op SSTORE should leave slot in storage_reads\n%s", b.PrettyPrint()) + } + if hasStorageWrite(b, contract, slot) { + t.Fatalf("no-op SSTORE must NOT register a write") + } +} + +// TestBALStorageWriteZeroIsAWrite: writing 0 to a non-zero slot is still a +// state change and lands in storage_changes. +func TestBALStorageWriteZeroIsAWrite(t *testing.T) { + contract := common.HexToAddress("0xc1") + slot := common.BigToHash(big.NewInt(0x03)) + // PUSH1 0 PUSH1 3 SSTORE STOP + code := []byte{0x60, 0x00, 0x60, 0x03, 0x55, 0x00} + env := newBALTestEnv(types.GenesisAlloc{ + contract: { + Code: code, + Balance: common.Big0, + Storage: map[common.Hash]common.Hash{slot: common.BigToHash(big.NewInt(0x42))}, + }, + }) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil)) + }) + + aa := assertPresent(t, b, contract) + if !hasStorageWrite(b, contract, slot) { + t.Fatalf("SSTORE to zero must record a write\n%s", b.PrettyPrint()) + } + for _, w := range aa.StorageWrites { + if w.Slot.Uint64() == 0x03 { + if len(w.Accesses) != 1 || !w.Accesses[0].ValueAfter.IsZero() { + t.Fatalf("expected post-value 0 for slot 0x03, got %+v", w.Accesses) + } + } + } +} + +// ============================== CREATE / contract deployment ============================== + +// TestBALCreateDeploysCode: a successful contract-creation tx records the new +// address with nonce 0→1, a balance entry (value transferred), and a code entry. +func TestBALCreateDeploysCode(t *testing.T) { + env := newBALTestEnv(nil) + // Init: deploy runtime [0x00] (single STOP byte). + // PUSH1 0 PUSH1 0 MSTORE8 PUSH1 1 PUSH1 0 RETURN + init := []byte{0x60, 0x00, 0x60, 0x00, 0x53, 0x60, 0x01, 0x60, 0x00, 0xf3} + + b, receipts := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, nil, big.NewInt(7), 200_000, 0, init)) + }) + + created := receipts[0].ContractAddress + aa := assertPresent(t, b, created) + if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 1 { + t.Fatalf("expected nonce 0→1, got %+v", aa.NonceChanges) + } + if len(aa.CodeChanges) != 1 || !bytes.Equal(aa.CodeChanges[0].Code, []byte{0x00}) { + t.Fatalf("expected code [0x00], got %+v", aa.CodeChanges) + } + if len(aa.BalanceChanges) != 1 || aa.BalanceChanges[0].Balance.Uint64() != 7 { + t.Fatalf("expected balance 7, got %+v", aa.BalanceChanges) + } +} + +// TestBALCreateEmptyRuntimeNoCodeEntry: when init code returns 0 bytes the +// new address is still listed with nonce 0→1 but no code entry. +func TestBALCreateEmptyRuntimeNoCodeEntry(t *testing.T) { + env := newBALTestEnv(nil) + // Init: PUSH1 0 PUSH1 0 RETURN → returns 0 bytes + init := []byte{0x60, 0x00, 0x60, 0x00, 0xf3} + + b, receipts := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, nil, big.NewInt(0), 200_000, 0, init)) + }) + + created := receipts[0].ContractAddress + aa := assertPresent(t, b, created) + if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 1 { + t.Fatalf("expected nonce 0→1, got %+v", aa.NonceChanges) + } + if len(aa.CodeChanges) != 0 { + t.Fatalf("empty runtime must NOT record a code entry, got %+v", aa.CodeChanges) + } +} + +// TestBALCreateInitRevertEmptyChangeSet: when init code reverts, the would-be +// contract address is in the BAL with an empty change set. +func TestBALCreateInitRevertEmptyChangeSet(t *testing.T) { + env := newBALTestEnv(nil) + // PUSH1 0 PUSH1 0 REVERT + init := []byte{0x60, 0x00, 0x60, 0x00, 0xfd} + + b, receipts := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, nil, big.NewInt(0), 200_000, 0, init)) + }) + + created := receipts[0].ContractAddress + assertEmpty(t, assertPresent(t, b, created)) +} + +// TestBALCreateInitOOGEmptyChangeSet: init code that runs out of gas leaves +// the deployed address in the BAL with an empty change set. +func TestBALCreateInitOOGEmptyChangeSet(t *testing.T) { + env := newBALTestEnv(nil) + // Infinite loop: JUMPDEST PUSH1 0 JUMP — burns gas until OOG. + init := []byte{0x5b, 0x60, 0x00, 0x56} + + b, receipts := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, nil, big.NewInt(0), 60_000, 0, init)) + }) + + created := receipts[0].ContractAddress + assertEmpty(t, assertPresent(t, b, created)) +} + +// TestBALCreateAddressCollisionStillIncluded: when CREATE targets an address +// that already holds a contract, the deployment fails but the address was +// probed during execution and MUST appear in the BAL with an empty change set. +func TestBALCreateAddressCollisionStillIncluded(t *testing.T) { + env := newBALTestEnv(nil) + // For a top-level CREATE tx the deployed address is CreateAddress(sender, 0). + // Pre-allocate a contract at that address to provoke ErrContractAddressCollision. + collide := crypto.CreateAddress(env.from, 0) + env.gspec.Alloc[collide] = types.Account{ + Nonce: 1, + Code: []byte{0x00}, + Balance: common.Big0, + } + + // Init code doesn't matter — execution never starts. + init := []byte{0x60, 0x00, 0x60, 0x00, 0xf3} + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, nil, big.NewInt(0), 200_000, 0, init)) + }) + + aa := assertPresent(t, b, collide) + // The address must be present but the pre-existing nonce/code MUST NOT + // be overwritten by the failed creation. + if len(aa.NonceChanges) != 0 { + t.Fatalf("collision must not bump nonce: %+v", aa.NonceChanges) + } + if len(aa.CodeChanges) != 0 { + t.Fatalf("collision must not write code: %+v", aa.CodeChanges) + } +} + +// TestBALInEVMCreatePreAccessAbortDestinationExcluded: if a CREATE frame +// aborts BEFORE the destination is read from state (here: the caller has 0 +// balance and CREATE requests value > 0, tripping evm.create's CanTransfer +// check before GetCodeHash), the would-be address MUST NOT appear in the +// BAL — only "if target account is accessed" qualifies for inclusion. +func TestBALInEVMCreatePreAccessAbortDestinationExcluded(t *testing.T) { + factory := common.HexToAddress("0xfac4") + // PUSH1 0 (length) PUSH1 0 (offset) PUSH1 1 (value) CREATE POP STOP + code := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x01, 0xf0, 0x50, 0x00} + env := newBALTestEnv(types.GenesisAlloc{ + factory: {Code: code, Balance: common.Big0, Nonce: 1}, // factory has no balance + }) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &factory, big.NewInt(0), 200_000, 0, nil)) + }) + + // The address that WOULD have been deployed had the create succeeded. + wouldBeDest := crypto.CreateAddress(factory, 1) + assertAbsent(t, b, wouldBeDest) + + // The factory itself is in BAL (it ran), but its nonce MUST NOT have been + // bumped because evm.create returned before the SetNonce call. + aa := assertPresent(t, b, factory) + if len(aa.NonceChanges) != 0 { + t.Fatalf("factory nonce must not be bumped on pre-access abort: %+v", aa.NonceChanges) + } +} + +// TestBALInEVMCreateDeploysContract: a CREATE issued by an existing contract +// (not a top-level CREATE tx) records the deployed address in the BAL. +func TestBALInEVMCreateDeploysContract(t *testing.T) { + factory := common.HexToAddress("0xfac4") + // Factory code: + // Write 5-byte init code (0x60 0x00 0x60 0x00 0xf3) into memory starting at offset 0. + // Then CREATE(value=0, offset=0, length=5). + // + // Layout: store the init code as a single 32-byte word at offset 0 via MSTORE + // with leftmost 27 bytes garbage, then call CREATE with offset = 27, length = 5. + initBlob := []byte{0x60, 0x00, 0x60, 0x00, 0xf3} + var word [32]byte + copy(word[32-len(initBlob):], initBlob) + code := []byte{0x7f} // PUSH32 + code = append(code, word[:]...) + code = append(code, 0x60, 0x00, 0x52) // PUSH1 0, MSTORE + // CREATE expects [value, offset, length] with value on bottom of stack. + code = append(code, + 0x60, 0x05, // PUSH1 5 (length) + 0x60, 0x1b, // PUSH1 27 (offset) + 0x60, 0x00, // PUSH1 0 (value) + 0xf0, // CREATE + 0x00, // STOP (discard result) + ) + + env := newBALTestEnv(types.GenesisAlloc{factory: {Code: code, Balance: common.Big0, Nonce: 1}}) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &factory, big.NewInt(0), 300_000, 0, nil)) + }) + + // Deployed address depends on the factory's nonce at the moment of CREATE, + // which is the factory's genesis nonce (1). + deployed := crypto.CreateAddress(factory, 1) + aa := assertPresent(t, b, deployed) + if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 1 { + t.Fatalf("deployed contract nonce: %+v", aa.NonceChanges) + } +} + +// ============================== SELFDESTRUCT ============================== + +// TestBALSelfDestructBeneficiaryWithZeroBalance: SELFDESTRUCT to a fresh +// beneficiary when the destructing account has 0 balance — both addresses are +// listed with empty change sets (no balance entry). +func TestBALSelfDestructBeneficiaryWithZeroBalance(t *testing.T) { + beneficiary := common.HexToAddress("0xbeefbeef") + env := newBALTestEnv(nil) + // Init code performs SELFDESTRUCT to beneficiary inside the constructor, + // so EIP-6780's same-tx requirement is satisfied. The destructing account + // starts with balance 0 because the creation tx sends 0 value. + // PUSH20 SELFDESTRUCT + init := append([]byte{0x73}, beneficiary.Bytes()...) + init = append(init, 0xff) + + b, receipts := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, nil, big.NewInt(0), 200_000, 0, init)) + }) + + created := receipts[0].ContractAddress + ben := assertPresent(t, b, beneficiary) + if len(ben.BalanceChanges) != 0 { + t.Fatalf("zero-value SELFDESTRUCT must not credit beneficiary: %+v", ben.BalanceChanges) + } + cc := assertPresent(t, b, created) + if len(cc.BalanceChanges) != 0 { + t.Fatalf("destructing contract must not record a balance entry: %+v", cc.BalanceChanges) + } +} + +// TestBALSelfDestructBeneficiaryWithValueTransfer: SELFDESTRUCT from a freshly +// created contract that received positive value — beneficiary records the +// credit; destructing account's balance entry is omitted because its +// pre-transaction balance was 0. +func TestBALSelfDestructBeneficiaryWithValueTransfer(t *testing.T) { + beneficiary := common.HexToAddress("0xbeefbeef") + env := newBALTestEnv(nil) + // Init code: PUSH20 SELFDESTRUCT + init := append([]byte{0x73}, beneficiary.Bytes()...) + init = append(init, 0xff) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, nil, big.NewInt(100), 200_000, 0, init)) + }) + + ben := assertPresent(t, b, beneficiary) + if len(ben.BalanceChanges) != 1 || ben.BalanceChanges[0].Balance.Uint64() != 100 { + t.Fatalf("beneficiary balance must be credited with 100: %+v", ben.BalanceChanges) + } +} + +// TestBALSelfDestructPreExistingContract: SELFDESTRUCT on a pre-existing +// contract with positive balance records balance→0 for the contract and the +// corresponding credit on the beneficiary. EIP-6780 means the contract is +// only credited and not deleted, but its balance moves regardless. +func TestBALSelfDestructPreExistingContract(t *testing.T) { + suicidal := common.HexToAddress("0x5e1f") + beneficiary := common.HexToAddress("0xbeefbeef") + // PUSH20 SELFDESTRUCT + code := append([]byte{0x73}, beneficiary.Bytes()...) + code = append(code, 0xff) + + env := newBALTestEnv(types.GenesisAlloc{ + suicidal: {Code: code, Balance: big.NewInt(50)}, + }) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &suicidal, big.NewInt(0), 200_000, 0, nil)) + }) + + aa := assertPresent(t, b, suicidal) + if len(aa.BalanceChanges) != 1 || !aa.BalanceChanges[0].Balance.IsZero() { + t.Fatalf("suicidal contract balance should drop to 0: %+v", aa.BalanceChanges) + } + ben := assertPresent(t, b, beneficiary) + if len(ben.BalanceChanges) != 1 || ben.BalanceChanges[0].Balance.Uint64() != 50 { + t.Fatalf("beneficiary should receive 50: %+v", ben.BalanceChanges) + } +} + +// ============================== Mid-tx balance round-trip ============================== + +// TestBALMidTxBalanceRoundTrip: when an address's balance changes during a +// transaction but returns to its pre-transaction value, the address is still +// listed in the BAL but MUST NOT have a balance entry. +func TestBALMidTxBalanceRoundTrip(t *testing.T) { + bouncer := common.HexToAddress("0xb0unce") + // On receiving value, the bouncer immediately CALLs CALLER with CALLVALUE + // and zero data. Net effect: bouncer.balance returns to its pre-tx value. + // + // PUSH1 0 (retSize) + // PUSH1 0 (retOff) + // PUSH1 0 (argsSize) + // PUSH1 0 (argsOff) + // CALLVALUE + // CALLER + // GAS + // CALL + // POP + // STOP + code := []byte{ + 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, + 0x34, // CALLVALUE + 0x33, // CALLER + 0x5a, // GAS + 0xf1, // CALL + 0x50, // POP + 0x00, // STOP + } + env := newBALTestEnv(types.GenesisAlloc{bouncer: {Code: code, Balance: common.Big0}}) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &bouncer, big.NewInt(1234), 200_000, 0, nil)) + }) + + aa := assertPresent(t, b, bouncer) + if len(aa.BalanceChanges) != 0 { + t.Fatalf("mid-tx round-trip must not record a balance entry: %+v", aa.BalanceChanges) + } +} + +// ============================== System contracts (pre/post-execution) ============================== + +// TestBALSystemContractsPresent: per EIP-7928, "System contract addresses +// accessed during pre/post-execution" MUST be included in the BAL. That +// means all four of the post-merge system contracts touched by every +// Amsterdam block: +// +// - EIP-4788 beacon roots (pre-execution, when ParentBeaconRoot is set) +// - EIP-2935 history storage (pre-execution) +// - EIP-7002 withdrawal queue (post-execution) +// - EIP-7251 consolidation queue (post-execution) +func TestBALSystemContractsPresent(t *testing.T) { + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + // SetCoinbase initialises b.bal; SetParentBeaconRoot triggers EIP-4788. + g.SetCoinbase(common.Address{0xc0}) + g.SetParentBeaconRoot(common.Hash{0xbe, 0xac}) + }) + + for _, sys := range []struct { + name string + addr common.Address + }{ + {"BeaconRoots (4788)", params.BeaconRootsAddress}, + {"HistoryStorage (2935)", params.HistoryStorageAddress}, + {"WithdrawalQueue (7002)", params.WithdrawalQueueAddress}, + {"ConsolidationQueue (7251)", params.ConsolidationQueueAddress}, + } { + if findAccount(b, sys.addr) == nil { + t.Errorf("%s (%x) MUST appear in BAL but is missing\n%s", sys.name, sys.addr, b.PrettyPrint()) + } + } +} + +// ============================== Withdrawals ============================== + +// TestBALWithdrawalZeroAmountIncluded: a withdrawal with amount 0 still puts +// the recipient in the BAL (with no balance entry). +func TestBALWithdrawalZeroAmountIncluded(t *testing.T) { + recipient := common.HexToAddress("0xdada") + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + g.SetCoinbase(common.Address{0xc0}) + g.AddWithdrawal(&types.Withdrawal{Validator: 1, Address: recipient, Amount: 0}) + }) + + r := assertPresent(t, b, recipient) + if len(r.BalanceChanges) != 0 { + t.Fatalf("zero-amount withdrawal must not record balance: %+v", r.BalanceChanges) + } +} + +// TestBALWithdrawalNonZeroAmountRecordsBalance: a positive-amount withdrawal +// records a balance change for the recipient. +func TestBALWithdrawalNonZeroAmountRecordsBalance(t *testing.T) { + recipient := common.HexToAddress("0xdada") + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + g.SetCoinbase(common.Address{0xc0}) + g.AddWithdrawal(&types.Withdrawal{Validator: 1, Address: recipient, Amount: 7}) + }) + + r := assertPresent(t, b, recipient) + if len(r.BalanceChanges) != 1 || r.BalanceChanges[0].Balance.Sign() == 0 { + t.Fatalf("withdrawal balance change missing: %+v", r.BalanceChanges) + } +} + +// ============================== EIP-7702 authority ============================== + +// TestBALAuthorityIncludedOnSetCodeTx: the authority of an EIP-7702 set-code +// transaction is added to the BAL once its delegation is loaded, recording +// both the nonce bump and the delegation-pointer code entry. +func TestBALAuthorityIncludedOnSetCodeTx(t *testing.T) { + env := newBALTestEnv(nil) + authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020") + authority := crypto.PubkeyToAddress(authKey.PublicKey) + delegate := common.HexToAddress("0xdeadbeef") + + auth, err := types.SignSetCode(authKey, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(env.cfg.ChainID), + Address: delegate, + Nonce: 0, + }) + if err != nil { + t.Fatalf("sign auth: %v", err) + } + + b, _ := env.run(t, func(g *BlockGen) { + tx := types.MustSignNewTx(env.key, env.signer, &types.SetCodeTx{ + ChainID: uint256.MustFromBig(env.cfg.ChainID), + Nonce: 0, + To: env.from, + Value: new(uint256.Int), + Gas: 200_000, + GasFeeCap: uint256.NewInt(uint64(newGwei(10).Int64())), + GasTipCap: new(uint256.Int), + AuthList: []types.SetCodeAuthorization{auth}, + }) + g.AddTx(tx) + }) + + aa := assertPresent(t, b, authority) + if len(aa.NonceChanges) == 0 { + t.Fatalf("authority nonce should be bumped by delegation: %+v", aa.NonceChanges) + } + if len(aa.CodeChanges) == 0 { + t.Fatalf("authority code (delegation pointer) should be recorded: %+v", aa.CodeChanges) + } +} + +// TestBALDelegationTargetNotIncludedOnAuthOnly: the EIP-7702 delegation target +// MUST NOT appear in the BAL when only the authorization is installed and the +// target is never loaded as an execution target. +func TestBALDelegationTargetNotIncludedOnAuthOnly(t *testing.T) { + env := newBALTestEnv(nil) + authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020") + delegate := common.HexToAddress("0xdeadbeef") // never accessed + + auth, err := types.SignSetCode(authKey, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(env.cfg.ChainID), + Address: delegate, + Nonce: 0, + }) + if err != nil { + t.Fatalf("sign auth: %v", err) + } + + b, _ := env.run(t, func(g *BlockGen) { + tx := types.MustSignNewTx(env.key, env.signer, &types.SetCodeTx{ + ChainID: uint256.MustFromBig(env.cfg.ChainID), + Nonce: 0, + To: env.from, // tx.to is an EOA with no code: delegate is never called + Value: new(uint256.Int), + Gas: 200_000, + GasFeeCap: uint256.NewInt(uint64(newGwei(10).Int64())), + GasTipCap: new(uint256.Int), + AuthList: []types.SetCodeAuthorization{auth}, + }) + g.AddTx(tx) + }) + + assertAbsent(t, b, delegate) +} + +// newSetCodeTx is a small constructor used by the multi-auth tests below. +func (e *balTestEnv) newSetCodeTx(t *testing.T, nonce uint64, to common.Address, auths []types.SetCodeAuthorization) *types.Transaction { + t.Helper() + tx, err := types.SignTx(types.NewTx(&types.SetCodeTx{ + ChainID: uint256.MustFromBig(e.cfg.ChainID), + Nonce: nonce, + To: to, + Value: new(uint256.Int), + Gas: 400_000, + GasFeeCap: uint256.NewInt(uint64(newGwei(10).Int64())), + GasTipCap: new(uint256.Int), + AuthList: auths, + }), e.signer, e.key) + if err != nil { + t.Fatalf("sign SetCodeTx: %v", err) + } + return tx +} + +// TestBALAuthFailedBeforeLoadExcluded: an EIP-7702 auth whose ChainID check +// fails returns before the authority is loaded, so the authority address +// MUST NOT appear in the BAL. +func TestBALAuthFailedBeforeLoadExcluded(t *testing.T) { + env := newBALTestEnv(nil) + authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020") + authority := crypto.PubkeyToAddress(authKey.PublicKey) + + auth, err := types.SignSetCode(authKey, types.SetCodeAuthorization{ + ChainID: *uint256.NewInt(999), // wrong chain → fails ChainID check (pre-load) + Address: common.HexToAddress("0xdeadbeef"), + Nonce: 0, + }) + if err != nil { + t.Fatalf("sign auth: %v", err) + } + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.newSetCodeTx(t, 0, env.from, []types.SetCodeAuthorization{auth})) + }) + + assertAbsent(t, b, authority) +} + +// TestBALAuthFailedAfterLoadEmptyChangeSet: an EIP-7702 auth that fails the +// nonce check happens AFTER the authority's code is loaded (and the address +// added to accessed_addresses), so the authority MUST appear in the BAL — +// but with no nonce or code change. +func TestBALAuthFailedAfterLoadEmptyChangeSet(t *testing.T) { + env := newBALTestEnv(nil) + authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020") + authority := crypto.PubkeyToAddress(authKey.PublicKey) + + // The authority's actual nonce is 0; supplying auth.Nonce=99 makes + // validation fail only after the code has been loaded. + auth, err := types.SignSetCode(authKey, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(env.cfg.ChainID), + Address: common.HexToAddress("0xdeadbeef"), + Nonce: 99, + }) + if err != nil { + t.Fatalf("sign auth: %v", err) + } + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.newSetCodeTx(t, 0, env.from, []types.SetCodeAuthorization{auth})) + }) + + aa := assertPresent(t, b, authority) + if len(aa.NonceChanges) != 0 { + t.Fatalf("failed auth must not bump nonce: %+v", aa.NonceChanges) + } + if len(aa.CodeChanges) != 0 { + t.Fatalf("failed auth must not record a code change: %+v", aa.CodeChanges) + } +} + +// TestBALMultipleAuthsOnlyLoadedIncluded: a SetCode tx with a mix of valid and +// pre-load-failed auths lists only the loaded authorities in the BAL. +func TestBALMultipleAuthsOnlyLoadedIncluded(t *testing.T) { + env := newBALTestEnv(nil) + goodKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020") + badKey, _ := crypto.HexToECDSA("0303030303030303030303030303030303030303030303030303003030303030") + good := crypto.PubkeyToAddress(goodKey.PublicKey) + bad := crypto.PubkeyToAddress(badKey.PublicKey) + delegate := common.HexToAddress("0xdeadbeef") + + goodAuth, err := types.SignSetCode(goodKey, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(env.cfg.ChainID), + Address: delegate, + Nonce: 0, + }) + if err != nil { + t.Fatalf("sign good auth: %v", err) + } + badAuth, err := types.SignSetCode(badKey, types.SetCodeAuthorization{ + ChainID: *uint256.NewInt(999), // fails before load + Address: delegate, + Nonce: 0, + }) + if err != nil { + t.Fatalf("sign bad auth: %v", err) + } + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.newSetCodeTx(t, 0, env.from, []types.SetCodeAuthorization{goodAuth, badAuth})) + }) + + assertPresent(t, b, good) // loaded → in BAL + assertAbsent(t, b, bad) // never loaded → not in BAL +} + +// TestBALAuthCodeRoundTripNoCodeEntry: two auths on the same authority that +// (1) install a delegation and (2) clear it again. Final code equals pre-tx +// code (empty), so the BAL records only the cumulative nonce bump and NO +// code change. +func TestBALAuthCodeRoundTripNoCodeEntry(t *testing.T) { + env := newBALTestEnv(nil) + authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020") + authority := crypto.PubkeyToAddress(authKey.PublicKey) + delegateA := common.HexToAddress("0xa11ce") + + auth1, err := types.SignSetCode(authKey, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(env.cfg.ChainID), + Address: delegateA, // empty → A + Nonce: 0, + }) + if err != nil { + t.Fatalf("sign auth1: %v", err) + } + auth2, err := types.SignSetCode(authKey, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(env.cfg.ChainID), + Address: common.Address{}, // delegation to zero clears the code (A → empty) + Nonce: 1, + }) + if err != nil { + t.Fatalf("sign auth2: %v", err) + } + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.newSetCodeTx(t, 0, env.from, []types.SetCodeAuthorization{auth1, auth2})) + }) + + aa := assertPresent(t, b, authority) + if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 2 { + t.Fatalf("expected final nonce 2, got %+v", aa.NonceChanges) + } + if len(aa.CodeChanges) != 0 { + t.Fatalf("code round-trip (empty→A→empty) must NOT record a code change: %+v", aa.CodeChanges) + } +} + +// TestBALAuthCodeOverwrittenFinalRecorded: two auths on the same authority +// switching delegation A → B record exactly one code change carrying the +// final delegation pointer (B), not the intermediate value. +func TestBALAuthCodeOverwrittenFinalRecorded(t *testing.T) { + env := newBALTestEnv(nil) + authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020") + authority := crypto.PubkeyToAddress(authKey.PublicKey) + delegateA := common.HexToAddress("0xa11ce") + delegateB := common.HexToAddress("0xb0b0b0") + + auth1, err := types.SignSetCode(authKey, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(env.cfg.ChainID), + Address: delegateA, + Nonce: 0, + }) + if err != nil { + t.Fatalf("sign auth1: %v", err) + } + auth2, err := types.SignSetCode(authKey, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(env.cfg.ChainID), + Address: delegateB, + Nonce: 1, + }) + if err != nil { + t.Fatalf("sign auth2: %v", err) + } + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.newSetCodeTx(t, 0, env.from, []types.SetCodeAuthorization{auth1, auth2})) + }) + + aa := assertPresent(t, b, authority) + if len(aa.CodeChanges) != 1 { + t.Fatalf("expected exactly 1 code change (final), got %+v", aa.CodeChanges) + } + want := types.AddressToDelegation(delegateB) + if !bytes.Equal(aa.CodeChanges[0].Code, want) { + t.Fatalf("final code mismatch: want %x, got %x", want, aa.CodeChanges[0].Code) + } + if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 2 { + t.Fatalf("expected final nonce 2, got %+v", aa.NonceChanges) + } +} diff --git a/core/bintrie_witness_test.go b/core/bintrie_witness_test.go index 5f6239e4fa..b49ac83bb5 100644 --- a/core/bintrie_witness_test.go +++ b/core/bintrie_witness_test.go @@ -29,6 +29,7 @@ import ( "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" @@ -202,7 +203,7 @@ func TestProcessParentBlockHash(t *testing.T) { } vmContext := NewEVMBlockContext(header, nil, new(common.Address)) evm := vm.NewEVM(vmContext, statedb, chainConfig, vm.Config{}) - ProcessParentBlockHash(header.ParentHash, evm) + ProcessParentBlockHash(header.ParentHash, evm, bal.NewConstructionBlockAccessList()) } // Read block hashes for block 0 .. num-1 for i := 0; i < num; i++ { diff --git a/core/block_validator.go b/core/block_validator.go index 008444fbbc..4086a2ead7 100644 --- a/core/block_validator.go +++ b/core/block_validator.go @@ -111,6 +111,28 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error { } } + // Block access list hash must be present in header after the + // Amsterdam hard fork. + if v.config.IsAmsterdam(block.Number(), block.Time()) { + if block.Header().BlockAccessListHash == nil { + return errors.New("block access list hash not set in header") + } + // If the block does not include an access list, compute it locally during + // execution and validate it against the access list hash in the header. + // + // If the block includes an attached access list, validate it directly here. + if block.AccessList() != nil { + computed := block.AccessList().Hash() + if *block.Header().BlockAccessListHash != computed { + return fmt.Errorf("access list hash mismatch, computed: %x, remote: %x", computed, *block.Header().BlockAccessListHash) + } else if err := block.AccessList().Validate(block.GasLimit()); err != nil { + return fmt.Errorf("invalid block access list: %v", err) + } + } + } else if block.Header().BlockAccessListHash != nil || block.AccessList() != nil { + return errors.New("block had access list before Amsterdam") + } + // Ancestor block must be known. if !v.bc.HasBlockAndState(block.ParentHash(), block.NumberU64()-1) { if !v.bc.HasBlock(block.ParentHash(), block.NumberU64()-1) { @@ -160,6 +182,23 @@ func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateD } else if res.Requests != nil { return errors.New("block has requests before prague fork") } + // Verify Block-level accessList once Amsterdam is enabled + if v.config.IsAmsterdam(block.Number(), block.Time()) { + if res.Bal == nil { + return errors.New("block access list is not available in amsterdam") + } + if block.Header().BlockAccessListHash == nil { + return errors.New("block access list hash not set in header") + } + enc := res.Bal.ToEncodingObj() + local, remote := enc.Hash(), *block.Header().BlockAccessListHash + if local != remote { + return fmt.Errorf("access list hash mismatch, local: %x, remote: %x", local, remote) + } + if err := enc.Validate(block.GasLimit()); err != nil { + return fmt.Errorf("invalid block access list: %v", err) + } + } // Validate the state root against the received state root and throw // an error if they don't match. if root := statedb.IntermediateRoot(v.config.IsEIP158(header.Number)); header.Root != root { diff --git a/core/chain_makers.go b/core/chain_makers.go index cfd6302794..2e856b5161 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -29,6 +29,7 @@ import ( "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/params" @@ -50,6 +51,7 @@ type BlockGen struct { receipts []*types.Receipt uncles []*types.Header withdrawals []*types.Withdrawal + bal *bal.ConstructionBlockAccessList engine consensus.Engine } @@ -99,7 +101,7 @@ func (b *BlockGen) Difficulty() *big.Int { func (b *BlockGen) SetParentBeaconRoot(root common.Hash) { b.header.ParentBeaconRoot = &root blockContext := NewEVMBlockContext(b.header, b.cm, &b.header.Coinbase) - ProcessBeaconBlockRoot(root, vm.NewEVM(blockContext, b.statedb, b.cm.config, vm.Config{})) + ProcessBeaconBlockRoot(root, vm.NewEVM(blockContext, b.statedb, b.cm.config, vm.Config{}), b.bal) } // addTx adds a transaction to the generated block. If no coinbase has @@ -118,7 +120,7 @@ func (b *BlockGen) addTx(bc *BlockChain, vmConfig vm.Config, tx *types.Transacti evm = vm.NewEVM(blockContext, b.statedb, b.cm.config, vmConfig) ) b.statedb.SetTxContext(tx.Hash(), len(b.txs), uint32(len(b.txs)+1)) - receipt, err := ApplyTransaction(evm, b.gasPool, b.statedb, b.header, tx) + receipt, bal, err := ApplyTransaction(evm, b.gasPool, b.statedb, b.header, tx) if err != nil { panic(err) } @@ -134,6 +136,7 @@ func (b *BlockGen) addTx(bc *BlockChain, vmConfig vm.Config, tx *types.Transacti if b.header.BlobGasUsed != nil { *b.header.BlobGasUsed += receipt.BlobGasUsed } + b.bal.Merge(bal) } // AddTx adds a transaction to the generated block. If no coinbase has @@ -304,10 +307,11 @@ func (b *BlockGen) OffsetTime(seconds int64) { // ConsensusLayerRequests returns the EIP-7685 requests which have accumulated so far. func (b *BlockGen) ConsensusLayerRequests() [][]byte { - return b.collectRequests(true) + requests, _ := b.collectRequests(true) + return requests } -func (b *BlockGen) collectRequests(readonly bool) (requests [][]byte) { +func (b *BlockGen) collectRequests(readonly bool) (requests [][]byte, bal *bal.ConstructionBlockAccessList) { statedb := b.statedb if readonly { // The system contracts clear themselves on a system-initiated read. @@ -323,11 +327,11 @@ func (b *BlockGen) collectRequests(readonly bool) (requests [][]byte) { blockContext := NewEVMBlockContext(b.header, b.cm, &b.header.Coinbase) evm := vm.NewEVM(blockContext, statedb, b.cm.config, vm.Config{}) - requests, err := PostExecution(context.Background(), b.cm.config, b.header.Number, b.header.Time, blockLogs, evm, uint32(len(b.txs)+1)) + requests, bal, err := PostExecution(context.Background(), b.cm.config, b.header.Number, b.header.Time, blockLogs, evm, uint32(len(b.txs)+1)) if err != nil { panic(fmt.Sprintf("failed to run post-execution: %v", err)) } - return requests + return requests, bal } // GenerateChain creates a chain of n blocks. The first block's @@ -354,6 +358,7 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse genblock := func(i int, parent *types.Block, triedb *triedb.Database, statedb *state.StateDB) (*types.Block, types.Receipts) { b := &BlockGen{i: i, cm: cm, parent: parent, statedb: statedb, engine: engine} b.header = cm.makeHeader(parent, statedb, b.engine) + b.bal = bal.NewConstructionBlockAccessList() // Set the difficulty for clique block. The chain maker doesn't have access // to a chain, so the difficulty will be left unset (nil). Set it here to the @@ -386,7 +391,7 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse blockContext := NewEVMBlockContext(b.header, cm, &b.header.Coinbase) blockContext.Random = &common.Hash{} // enable post-merge instruction set evm := vm.NewEVM(blockContext, statedb, cm.config, vm.Config{}) - ProcessParentBlockHash(b.header.ParentHash, evm) + ProcessParentBlockHash(b.header.ParentHash, evm, b.bal) } // Execute any user modifications to the block @@ -394,11 +399,12 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse gen(i, b) } - requests := b.collectRequests(false) + requests, bal := b.collectRequests(false) if requests != nil { reqHash := types.CalcRequestsHash(requests) b.header.RequestsHash = &reqHash } + b.bal.Merge(bal) body := types.Body{ Transactions: b.txs, @@ -414,8 +420,11 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse body.Withdrawals = make([]*types.Withdrawal, 0) } } + // Apply the consensus-specific post-transaction changes + b.engine.Finalize(cm, b.header, statedb, &body, uint32(len(b.txs)+1), b.bal) + // Assemble the block for delivery. - block := AssembleBlock(b.engine, cm, b.header, statedb, &body, b.receipts) + block := AssembleBlock(cm, b.header, statedb, &body, b.receipts, b.bal) // Write state changes to db root, err := statedb.Commit(b.header.Number.Uint64(), config.IsEIP158(b.header.Number), config.IsCancun(b.header.Number, b.header.Time)) diff --git a/core/genesis.go b/core/genesis.go index 6a0affa52e..e1c67e57c2 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -555,6 +555,7 @@ func (g *Genesis) toBlockWithRoot(root common.Hash) *types.Block { if head.SlotNumber == nil { head.SlotNumber = new(uint64) } + head.BlockAccessListHash = &types.EmptyBlockAccessListHash } } return types.NewBlock(head, &types.Body{Withdrawals: withdrawals}, nil, trie.NewStackTrie(nil)) diff --git a/core/state_processor.go b/core/state_processor.go index 13466b7815..5690a152e7 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -27,6 +27,7 @@ import ( "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/internal/telemetry" @@ -81,13 +82,16 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated misc.ApplyDAOHardFork(tracingStateDB) } var ( - context = NewEVMBlockContext(header, p.chain, nil) - signer = types.MakeSigner(config, header.Number, header.Time) - evm = vm.NewEVM(context, tracingStateDB, config, cfg) + context = NewEVMBlockContext(header, p.chain, nil) + signer = types.MakeSigner(config, header.Number, header.Time) + evm = vm.NewEVM(context, tracingStateDB, config, cfg) + blockAccessList = bal.NewConstructionBlockAccessList() ) defer evm.Release() + // Run the pre-execution system calls - PreExecution(ctx, block.BeaconRoot(), block.ParentHash(), config, evm, block.Number(), block.Time()) + blockAccessList.Merge(PreExecution(ctx, block.BeaconRoot(), block.ParentHash(), config, evm, block.Number(), block.Time())) + // Iterate over and process the individual transactions for i, tx := range block.Transactions() { msg, err := TransactionToMessage(tx, signer, header.BaseFee) @@ -99,76 +103,92 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated telemetry.StringAttribute("tx.hash", tx.Hash().Hex()), telemetry.Int64Attribute("tx.index", int64(i)), ) - - receipt, err := ApplyTransactionWithEVM(msg, gp, statedb, blockNumber, blockHash, context.Time, tx, evm) + receipt, bal, err := ApplyTransactionWithEVM(msg, gp, statedb, blockNumber, blockHash, context.Time, tx, evm) if err != nil { spanEnd(&err) return nil, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) } receipts = append(receipts, receipt) allLogs = append(allLogs, receipt.Logs...) + blockAccessList.Merge(bal) spanEnd(nil) } - requests, err := PostExecution(ctx, config, block.Number(), block.Time(), allLogs, evm, uint32(len(block.Transactions())+1)) + requests, bal, err := PostExecution(ctx, config, block.Number(), block.Time(), allLogs, evm, uint32(len(block.Transactions())+1)) if err != nil { return nil, err } - // Finalize the block, applying any consensus engine specific extras (e.g. block rewards) - p.chain.Engine().Finalize(p.chain, header, tracingStateDB, block.Body()) + blockAccessList.Merge(bal) + + // Finalize the block, applying any consensus engine specific extras + // (e.g. block rewards). + // + // TODO(rjl493456442) integrate it into the PostExecution. + p.chain.Engine().Finalize(p.chain, header, tracingStateDB, block.Body(), uint32(len(block.Transactions())+1), blockAccessList) return &ProcessResult{ Receipts: receipts, Requests: requests, Logs: allLogs, GasUsed: gp.Used(), + Bal: blockAccessList, }, nil } // PreExecution processes pre-execution system calls. -func PreExecution(ctx context.Context, beaconRoot *common.Hash, parent common.Hash, config *params.ChainConfig, evm *vm.EVM, number *big.Int, time uint64) { +func PreExecution(ctx context.Context, beaconRoot *common.Hash, parent common.Hash, config *params.ChainConfig, evm *vm.EVM, number *big.Int, time uint64) *bal.ConstructionBlockAccessList { _, _, spanEnd := telemetry.StartSpan(ctx, "core.preExecution") defer spanEnd(nil) + var blockAccessList *bal.ConstructionBlockAccessList + if config.IsAmsterdam(number, time) { + blockAccessList = bal.NewConstructionBlockAccessList() + } // EIP-4788 if beaconRoot != nil { - ProcessBeaconBlockRoot(*beaconRoot, evm) + ProcessBeaconBlockRoot(*beaconRoot, evm, blockAccessList) } // EIP-2935 if config.IsPrague(number, time) || config.IsUBT(number, time) { - ProcessParentBlockHash(parent, evm) + ProcessParentBlockHash(parent, evm, blockAccessList) } + return blockAccessList } // PostExecution processes post-execution system calls when Prague is enabled. // If Prague is not activated, it returns null requests to differentiate from // empty requests. -func PostExecution(ctx context.Context, config *params.ChainConfig, number *big.Int, time uint64, allLogs []*types.Log, evm *vm.EVM, blockAccessIndex uint32) (requests [][]byte, err error) { +func PostExecution(ctx context.Context, config *params.ChainConfig, number *big.Int, time uint64, allLogs []*types.Log, evm *vm.EVM, blockAccessIndex uint32) (requests [][]byte, blockAccessList *bal.ConstructionBlockAccessList, err error) { _, _, spanEnd := telemetry.StartSpan(ctx, "core.postExecution") defer spanEnd(&err) + if config.IsAmsterdam(number, time) { + blockAccessList = bal.NewConstructionBlockAccessList() + } // Read requests if Prague is enabled. if config.IsPrague(number, time) { + rules := config.Rules(number, true, time) // IsMerge is always true + requests = [][]byte{} // EIP-6110 if err := ParseDepositLogs(&requests, allLogs, config); err != nil { - return nil, fmt.Errorf("failed to parse deposit logs: %w", err) + return nil, nil, fmt.Errorf("failed to parse deposit logs: %w", err) } // EIP-7002 - if err := ProcessWithdrawalQueue(&requests, evm, blockAccessIndex); err != nil { - return nil, fmt.Errorf("failed to process withdrawal queue: %w", err) + if err := ProcessWithdrawalQueue(&requests, rules, evm, blockAccessIndex, blockAccessList); err != nil { + return nil, nil, fmt.Errorf("failed to process withdrawal queue: %w", err) } // EIP-7251 - if err := ProcessConsolidationQueue(&requests, evm, blockAccessIndex); err != nil { - return nil, fmt.Errorf("failed to process consolidation queue: %w", err) + if err := ProcessConsolidationQueue(&requests, rules, evm, blockAccessIndex, blockAccessList); err != nil { + return nil, nil, fmt.Errorf("failed to process consolidation queue: %w", err) } } - return requests, nil + return requests, blockAccessList, nil } // ApplyTransactionWithEVM attempts to apply a transaction to the given state database // and uses the input parameters for its environment similar to ApplyTransaction. However, // this method takes an already created EVM instance as input. -func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, blockTime uint64, tx *types.Transaction, evm *vm.EVM) (receipt *types.Receipt, err error) { +func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, blockTime uint64, tx *types.Transaction, evm *vm.EVM) (receipt *types.Receipt, bal *bal.ConstructionBlockAccessList, err error) { if hooks := evm.Config.Tracer; hooks != nil { if hooks.OnTxStart != nil { hooks.OnTxStart(evm.GetVMContext(), tx, msg.From) @@ -180,12 +200,12 @@ func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB, // Apply the transaction to the current state (included in the env). result, err := ApplyMessage(evm, msg, gp) if err != nil { - return nil, err + return nil, nil, err } // Update the state with pending changes. var root []byte if evm.ChainConfig().IsByzantium(blockNumber) { - evm.StateDB.Finalise(true) + bal = evm.StateDB.Finalise(true) } else { root = statedb.IntermediateRoot(evm.ChainConfig().IsEIP158(blockNumber)).Bytes() } @@ -194,7 +214,7 @@ func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB, if statedb.Database().Type().Is(state.TypeUBT) { statedb.AccessEvents().Merge(evm.AccessEvents) } - return MakeReceipt(evm, result, statedb, blockNumber, blockHash, blockTime, tx, gp.CumulativeUsed(), root), nil + return MakeReceipt(evm, result, statedb, blockNumber, blockHash, blockTime, tx, gp.CumulativeUsed(), root), bal, nil } // MakeReceipt generates the receipt object for a transaction given its execution result. @@ -239,10 +259,10 @@ func MakeReceipt(evm *vm.EVM, result *ExecutionResult, statedb *state.StateDB, b // and uses the input parameters for its environment. It returns the receipt // for the transaction and an error if the transaction failed, // indicating the block was invalid. -func ApplyTransaction(evm *vm.EVM, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction) (*types.Receipt, error) { +func ApplyTransaction(evm *vm.EVM, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction) (*types.Receipt, *bal.ConstructionBlockAccessList, error) { msg, err := TransactionToMessage(tx, types.MakeSigner(evm.ChainConfig(), header.Number, header.Time), header.BaseFee) if err != nil { - return nil, err + return nil, nil, err } // Create a new context to be used in the EVM environment return ApplyTransactionWithEVM(msg, gp, statedb, header.Number, header.Hash(), header.Time, tx, evm) @@ -250,7 +270,7 @@ func ApplyTransaction(evm *vm.EVM, gp *GasPool, statedb *state.StateDB, header * // ProcessBeaconBlockRoot applies the EIP-4788 system call to the beacon block root // contract. This method is exported to be used in tests. -func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM) { +func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM, blockAccessList *bal.ConstructionBlockAccessList) { if tracer := evm.Config.Tracer; tracer != nil { onSystemCallStart(tracer, evm.GetVMContext()) if tracer.OnSystemCallEnd != nil { @@ -267,18 +287,19 @@ func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM) { Data: beaconRoot[:], } evm.SetTxContext(NewEVMTxContext(msg)) + evm.StateDB.Prepare(evm.GetRules(), common.Address{}, common.Address{}, nil, nil, nil) evm.StateDB.SetTxContext(common.Hash{}, 0, 0) evm.StateDB.AddAddressToAccessList(params.BeaconRootsAddress) _, _, _ = evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560) if evm.StateDB.AccessEvents() != nil { evm.StateDB.AccessEvents().Merge(evm.AccessEvents) } - evm.StateDB.Finalise(true) + blockAccessList.Merge(evm.StateDB.Finalise(true)) } // ProcessParentBlockHash stores the parent block hash in the history storage contract // as per EIP-2935/7709. -func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM) { +func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM, blockAccessList *bal.ConstructionBlockAccessList) { if tracer := evm.Config.Tracer; tracer != nil { onSystemCallStart(tracer, evm.GetVMContext()) if tracer.OnSystemCallEnd != nil { @@ -295,6 +316,7 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM) { Data: prevHash.Bytes(), } evm.SetTxContext(NewEVMTxContext(msg)) + evm.StateDB.Prepare(evm.GetRules(), common.Address{}, common.Address{}, nil, nil, nil) evm.StateDB.SetTxContext(common.Hash{}, 0, 0) evm.StateDB.AddAddressToAccessList(params.HistoryStorageAddress) _, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560) @@ -304,22 +326,22 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM) { if evm.StateDB.AccessEvents() != nil { evm.StateDB.AccessEvents().Merge(evm.AccessEvents) } - evm.StateDB.Finalise(true) + blockAccessList.Merge(evm.StateDB.Finalise(true)) } // ProcessWithdrawalQueue calls the EIP-7002 withdrawal queue contract. // It returns the opaque request data returned by the contract. -func ProcessWithdrawalQueue(requests *[][]byte, evm *vm.EVM, blockAccessIndex uint32) error { - return processRequestsSystemCall(requests, evm, 0x01, params.WithdrawalQueueAddress, blockAccessIndex) +func ProcessWithdrawalQueue(requests *[][]byte, rules params.Rules, evm *vm.EVM, blockAccessIndex uint32, blockAccessList *bal.ConstructionBlockAccessList) error { + return processRequestsSystemCall(requests, rules, evm, 0x01, params.WithdrawalQueueAddress, blockAccessIndex, blockAccessList) } // ProcessConsolidationQueue calls the EIP-7251 consolidation queue contract. // It returns the opaque request data returned by the contract. -func ProcessConsolidationQueue(requests *[][]byte, evm *vm.EVM, blockAccessIndex uint32) error { - return processRequestsSystemCall(requests, evm, 0x02, params.ConsolidationQueueAddress, blockAccessIndex) +func ProcessConsolidationQueue(requests *[][]byte, rules params.Rules, evm *vm.EVM, blockAccessIndex uint32, blockAccessList *bal.ConstructionBlockAccessList) error { + return processRequestsSystemCall(requests, rules, evm, 0x02, params.ConsolidationQueueAddress, blockAccessIndex, blockAccessList) } -func processRequestsSystemCall(requests *[][]byte, evm *vm.EVM, requestType byte, addr common.Address, blockAccessIndex uint32) error { +func processRequestsSystemCall(requests *[][]byte, rules params.Rules, evm *vm.EVM, requestType byte, addr common.Address, blockAccessIndex uint32, blockAccessList *bal.ConstructionBlockAccessList) error { if tracer := evm.Config.Tracer; tracer != nil { onSystemCallStart(tracer, evm.GetVMContext()) if tracer.OnSystemCallEnd != nil { @@ -335,16 +357,19 @@ func processRequestsSystemCall(requests *[][]byte, evm *vm.EVM, requestType byte To: &addr, } evm.SetTxContext(NewEVMTxContext(msg)) + evm.StateDB.Prepare(rules, common.Address{}, common.Address{}, nil, nil, nil) evm.StateDB.SetTxContext(common.Hash{}, 0, blockAccessIndex) evm.StateDB.AddAddressToAccessList(addr) ret, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560) if evm.StateDB.AccessEvents() != nil { evm.StateDB.AccessEvents().Merge(evm.AccessEvents) } - evm.StateDB.Finalise(true) + bal := evm.StateDB.Finalise(true) if err != nil { return fmt.Errorf("system call failed to execute: %v", err) } + blockAccessList.Merge(bal) + if len(ret) == 0 { return nil // skip empty output } @@ -387,8 +412,16 @@ func onSystemCallStart(tracer *tracing.Hooks, ctx *tracing.VMContext) { // AssembleBlock finalizes the state and assembles the block with provided // body and receipts. -func AssembleBlock(engine consensus.Engine, chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body, receipts []*types.Receipt) *types.Block { - engine.Finalize(chain, header, state, body) +func AssembleBlock(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body, receipts []*types.Receipt, blockAccessList *bal.ConstructionBlockAccessList) *types.Block { + // Assign the post-transition state root header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number)) - return types.NewBlock(header, body, receipts, trie.NewStackTrie(nil)) + + if !chain.Config().IsAmsterdam(header.Number, header.Time) { + return types.NewBlock(header, body, receipts, trie.NewStackTrie(nil)) + } + // Assign the BlockAccessListHash if Amsterdam has been enabled + bal := blockAccessList.ToEncodingObj() + balHash := bal.Hash() + header.BlockAccessListHash = &balHash + return types.NewBlock(header, body, receipts, trie.NewStackTrie(nil)).WithAccessListUnsafe(bal) } diff --git a/core/state_transition.go b/core/state_transition.go index 0a6994505d..51c5836892 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -608,7 +608,8 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { // Execute the preparatory steps for state transition which includes: // - prepare accessList(post-berlin) - // - reset transient storage(eip 1153) + // - reset transient storage(EIP-1153) + // - enable block-level accessList construction (EIP-7928) st.state.Prepare(rules, msg.From, st.evm.Context.Coinbase, msg.To, vm.ActivePrecompiles(rules), msg.AccessList) var ( diff --git a/core/types.go b/core/types.go index 87bbfcff58..edbfc43db3 100644 --- a/core/types.go +++ b/core/types.go @@ -22,6 +22,7 @@ import ( "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/vm" ) @@ -58,4 +59,8 @@ type ProcessResult struct { Requests [][]byte Logs []*types.Log GasUsed uint64 + + // BAL is only meaningful for post-Amsterdam blocks. Please ensure + // fork validation is performed before accessing it. + Bal *bal.ConstructionBlockAccessList } diff --git a/core/types/bal/bal.go b/core/types/bal/bal.go index 9cbc1faeb9..2eb5fe93cd 100644 --- a/core/types/bal/bal.go +++ b/core/types/bal/bal.go @@ -138,10 +138,62 @@ func (b *ConstructionBlockAccessList) BalanceChange(txIdx uint32, address common // PrettyPrint returns a human-readable representation of the access list func (b *ConstructionBlockAccessList) PrettyPrint() string { - enc := b.toEncodingObj() + enc := b.ToEncodingObj() return enc.PrettyPrint() } +// Merge applies other on top of the local block access list. For colliding +// entries (a (slot, txIdx) write or a txIdx-keyed balance/nonce/code change), +// the value from other wins, matching the semantics of applying the local +// effects first and then other's. Storage reads are unioned; any slot +// written by either side is dropped from StorageReads. +// +// Typically each list covers its own tx index, so txIdx-level collisions are +// not expected; the exception is pre/post-transition system calls, which +// share a single tx index. In that case callers must pass block-accessList +// in order strictly. +// +// other is referenced (not deep copied), after the call both lists share +// inner maps and other must not be mutated. +func (b *ConstructionBlockAccessList) Merge(other *ConstructionBlockAccessList) { + if other == nil { + return + } + for addr, otherAcc := range other.Accounts { + acc, ok := b.Accounts[addr] + if !ok { + b.Accounts[addr] = otherAcc + continue + } + for key, writes := range otherAcc.StorageWrites { + existing, ok := acc.StorageWrites[key] + if !ok { + acc.StorageWrites[key] = writes + } else { + for txIdx, value := range writes { + existing[txIdx] = value + } + } + delete(acc.StorageReads, key) + } + for key := range otherAcc.StorageReads { + if _, ok := acc.StorageWrites[key]; ok { + continue + } + acc.StorageReads[key] = struct{}{} + } + for txIdx, balance := range otherAcc.BalanceChanges { + acc.BalanceChanges[txIdx] = balance + } + for txIdx, nonce := range otherAcc.NonceChanges { + acc.NonceChanges[txIdx] = nonce + } + for txIdx, code := range otherAcc.CodeChange { + acc.CodeChange[txIdx] = code + } + } +} + // Copy returns a deep copy of the access list. func (b *ConstructionBlockAccessList) Copy() *ConstructionBlockAccessList { res := NewConstructionBlockAccessList() diff --git a/core/types/bal/bal_encoding.go b/core/types/bal/bal_encoding.go index 03f97f3809..399f9db7c0 100644 --- a/core/types/bal/bal_encoding.go +++ b/core/types/bal/bal_encoding.go @@ -78,17 +78,43 @@ func (e *BlockAccessList) DecodeRLP(s *rlp.Stream) error { // Validate returns an error if the contents of the access list are not ordered // according to the spec or any code changes are contained which exceed protocol // max code size. -func (e *BlockAccessList) Validate(rules params.Rules) error { +func (e *BlockAccessList) Validate(blockGasLimit uint64) error { if !slices.IsSortedFunc(*e, func(a, b AccountAccess) int { return bytes.Compare(a.Address[:], b.Address[:]) }) { return errors.New("block access list accounts not in lexicographic order") } for _, entry := range *e { - if err := entry.validate(rules); err != nil { + if err := entry.validate(); err != nil { return err } } + return e.ValidateSize(blockGasLimit) +} + +// itemCount returns the number of items in the BAL for EIP-7928 size-constraint +// purposes: the count of distinct addresses plus every storage key (writes + +// reads) carried by those accounts. A storage slot is counted once regardless +// of how many transactions wrote to it. +func (e *BlockAccessList) itemCount() uint64 { + count := uint64(len(*e)) // distinct addresses + for i := range *e { + count += uint64(len((*e)[i].StorageWrites)) + uint64(len((*e)[i].StorageReads)) + } + return count +} + +// ValidateSize returns an error if the BAL violates the EIP-7928 size +// constraint for the given block gas limit: +// +// itemCount() <= blockGasLimit / params.BALItemCost +func (e *BlockAccessList) ValidateSize(blockGasLimit uint64) error { + items := e.itemCount() + limit := blockGasLimit / params.BALItemCost + if items > limit { + return fmt.Errorf("block access list exceeds size constraint: items=%d, limit=%d (block gas limit %d / %d)", + items, limit, blockGasLimit, params.BALItemCost) + } return nil } @@ -159,7 +185,7 @@ type AccountAccess struct { // validate converts the account accesses out of encoding format. // If any of the keys in the encoding object are not ordered according to the // spec, an error is returned. -func (e *AccountAccess) validate(rules params.Rules) error { +func (e *AccountAccess) validate() error { // Check the storage write slots are sorted in order if !slices.IsSortedFunc(e.StorageWrites, func(a, b encodingSlotWrites) int { return a.Slot.Cmp(b.Slot) @@ -200,14 +226,7 @@ func (e *AccountAccess) validate(rules params.Rules) error { return errors.New("code changes not in ascending order by tx index") } for _, change := range e.CodeChanges { - var sizeLimit int - switch { - case rules.IsAmsterdam: - sizeLimit = params.MaxCodeSizeAmsterdam - default: - sizeLimit = params.MaxCodeSize - } - if len(change.Code) > sizeLimit { + if len(change.Code) > params.MaxCodeSizeAmsterdam { return errors.New("code change contained oversized code") } } @@ -257,7 +276,7 @@ func (e *AccountAccess) Copy() AccountAccess { // EncodeRLP returns the RLP-encoded access list func (b *ConstructionBlockAccessList) EncodeRLP(wr io.Writer) error { - return b.toEncodingObj().EncodeRLP(wr) + return b.ToEncodingObj().EncodeRLP(wr) } var _ rlp.Encoder = &ConstructionBlockAccessList{} @@ -340,9 +359,9 @@ func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAc return res } -// toEncodingObj returns an instance of the access list expressed as the type +// ToEncodingObj returns an instance of the access list expressed as the type // which is used as input for the encoding/decoding. -func (b *ConstructionBlockAccessList) toEncodingObj() *BlockAccessList { +func (b *ConstructionBlockAccessList) ToEncodingObj() *BlockAccessList { var addresses []common.Address for addr := range b.Accounts { addresses = append(addresses, addr) diff --git a/core/types/bal/bal_test.go b/core/types/bal/bal_test.go index 32a0292f2e..2b6a3c194e 100644 --- a/core/types/bal/bal_test.go +++ b/core/types/bal/bal_test.go @@ -19,6 +19,7 @@ package bal import ( "bytes" "cmp" + "math" "reflect" "slices" "testing" @@ -98,14 +99,65 @@ func TestBALEncoding(t *testing.T) { if err := dec.DecodeRLP(rlp.NewStream(bytes.NewReader(buf.Bytes()), 0)); err != nil { t.Fatalf("decoding failed: %v\n", err) } - if dec.Hash() != bal.toEncodingObj().Hash() { + if dec.Hash() != bal.ToEncodingObj().Hash() { t.Fatalf("encoded block hash doesn't match decoded") } - if !reflect.DeepEqual(bal.toEncodingObj(), &dec) { + if !reflect.DeepEqual(bal.ToEncodingObj(), &dec) { t.Fatal("decoded BAL doesn't match") } } +func TestConstructionBALMerge(t *testing.T) { + var ( + addrA = common.BytesToAddress([]byte{0xAA}) + addrB = common.BytesToAddress([]byte{0xBB}) + slot1 = common.BytesToHash([]byte{0x01}) + slot2 = common.BytesToHash([]byte{0x02}) + slot3 = common.BytesToHash([]byte{0x03}) + ) + a := NewConstructionBlockAccessList() + a.StorageWrite(1, addrA, slot1, common.BytesToHash([]byte{0x11})) + a.StorageRead(addrA, slot2) // demoted by other's write below + a.BalanceChange(1, addrA, uint256.NewInt(100)) + a.NonceChange(addrA, 1, 7) + + b := NewConstructionBlockAccessList() + b.StorageWrite(2, addrA, slot1, common.BytesToHash([]byte{0x22})) // same slot, disjoint txIdx + b.StorageWrite(2, addrA, slot2, common.BytesToHash([]byte{0x33})) + b.StorageRead(addrA, slot3) + b.BalanceChange(2, addrA, uint256.NewInt(200)) + b.NonceChange(addrA, 2, 8) + b.CodeChange(addrB, 2, []byte{0xde, 0xad}) // account only in other + + a.Merge(b) + + accA := a.Accounts[addrA] + wantWrites := map[common.Hash]map[uint32]common.Hash{ + slot1: {1: common.BytesToHash([]byte{0x11}), 2: common.BytesToHash([]byte{0x22})}, + slot2: {2: common.BytesToHash([]byte{0x33})}, + } + if !reflect.DeepEqual(accA.StorageWrites, wantWrites) { + t.Fatalf("storage writes mismatch: got %v, want %v", accA.StorageWrites, wantWrites) + } + wantReads := map[common.Hash]struct{}{slot3: {}} + if !reflect.DeepEqual(accA.StorageReads, wantReads) { + t.Fatalf("storage reads mismatch: got %v, want %v", accA.StorageReads, wantReads) + } + if accA.BalanceChanges[1].Uint64() != 100 || accA.BalanceChanges[2].Uint64() != 200 { + t.Fatalf("balance changes mismatch: %v", accA.BalanceChanges) + } + if accA.NonceChanges[1] != 7 || accA.NonceChanges[2] != 8 { + t.Fatalf("nonce changes mismatch: %v", accA.NonceChanges) + } + accB, ok := a.Accounts[addrB] + if !ok { + t.Fatal("account only present in other was not adopted") + } + if !bytes.Equal(accB.CodeChange[2], []byte{0xde, 0xad}) { + t.Fatalf("code change for adopted account missing: %x", accB.CodeChange[2]) + } +} + func makeTestAccountAccess(sort bool) AccountAccess { var ( storageWrites []encodingSlotWrites @@ -231,10 +283,82 @@ func TestBlockAccessListCopy(t *testing.T) { } } +func TestBlockAccessListItemCount(t *testing.T) { + empty := &BlockAccessList{} + if got := empty.itemCount(); got != 0 { + t.Fatalf("empty BAL item count: got %d, want 0", got) + } + + addr1 := [20]byte(testrand.Bytes(20)) + addr2 := [20]byte(testrand.Bytes(20)) + one := func() *uint256.Int { return new(uint256.Int).SetBytes(testrand.Bytes(32)) } + bal := &BlockAccessList{ + AccountAccess{ + Address: addr1, + StorageWrites: []encodingSlotWrites{ + {Slot: one(), Accesses: []encodingStorageWrite{{TxIdx: 0, ValueAfter: one()}, {TxIdx: 1, ValueAfter: one()}}}, + {Slot: one()}, + }, + StorageReads: []*uint256.Int{one()}, + }, + AccountAccess{Address: addr2}, // address-only, no slots + } + // 2 addresses + 2 write-slots + 1 read-slot = 5 items. + // (Multiple TxIdx writes to the same slot count as ONE item.) + if got := bal.itemCount(); got != 5 { + t.Fatalf("item count: got %d, want 5", got) + } +} + +func TestBlockAccessListValidateSize(t *testing.T) { + // Build a BAL with exactly 30 items: 3 addresses, each with 9 storage + // slots (some writes, some reads). 3 + 9*3 = 30. + one := func() *uint256.Int { return new(uint256.Int).SetBytes(testrand.Bytes(32)) } + bal := make(BlockAccessList, 3) + for i := range bal { + bal[i].Address = [20]byte(testrand.Bytes(20)) + for j := 0; j < 5; j++ { + bal[i].StorageWrites = append(bal[i].StorageWrites, encodingSlotWrites{ + Slot: one(), Accesses: []encodingStorageWrite{{TxIdx: 0, ValueAfter: one()}}, + }) + } + for j := 0; j < 4; j++ { + bal[i].StorageReads = append(bal[i].StorageReads, one()) + } + } + if got := bal.itemCount(); got != 30 { + t.Fatalf("setup: item count = %d, want 30", got) + } + + // limit = blockGasLimit / BALItemCost. + // 30 items requires limit >= 30, i.e. gasLimit >= 30 * 2000 = 60_000. + tests := []struct { + name string + gasLimit uint64 + expectError bool + }{ + {"exactly at limit", 30 * params.BALItemCost, false}, + {"well above limit", 60_000_000, false}, + {"one below limit", 30*params.BALItemCost - 1, true}, + {"zero gas limit", 0, true}, + } + for _, tc := range tests { + err := bal.ValidateSize(tc.gasLimit) + if (err != nil) != tc.expectError { + t.Errorf("%s: got err=%v, expectError=%v", tc.name, err, tc.expectError) + } + } + + // Empty BAL is always valid (even with 0 gas limit). + if err := (&BlockAccessList{}).ValidateSize(0); err != nil { + t.Fatalf("empty BAL must pass any limit: %v", err) + } +} + func TestBlockAccessListValidation(t *testing.T) { // Validate the block access list after RLP decoding enc := makeTestBAL(true) - if err := enc.Validate(params.Rules{}); err != nil { + if err := enc.Validate(math.MaxUint64); err != nil { t.Fatalf("Unexpected validation error: %v", err) } var buf bytes.Buffer @@ -246,14 +370,14 @@ func TestBlockAccessListValidation(t *testing.T) { if err := dec.DecodeRLP(rlp.NewStream(bytes.NewReader(buf.Bytes()), 0)); err != nil { t.Fatalf("Unexpected RLP-decode error: %v", err) } - if err := dec.Validate(params.Rules{}); err != nil { + if err := dec.Validate(math.MaxUint64); err != nil { t.Fatalf("Unexpected validation error: %v", err) } // Validate the derived block access list cBAL := makeTestConstructionBAL() - listB := cBAL.toEncodingObj() - if err := listB.Validate(params.Rules{}); err != nil { + listB := cBAL.ToEncodingObj() + if err := listB.Validate(math.MaxUint64); err != nil { t.Fatalf("Unexpected validation error: %v", err) } } diff --git a/core/types/block.go b/core/types/block.go index ea576ed232..0856845a4e 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -413,8 +413,9 @@ func (b *Block) BaseFee() *big.Int { return new(big.Int).Set(b.header.BaseFee) } -func (b *Block) BeaconRoot() *common.Hash { return b.header.ParentBeaconRoot } -func (b *Block) RequestsHash() *common.Hash { return b.header.RequestsHash } +func (b *Block) BeaconRoot() *common.Hash { return b.header.ParentBeaconRoot } +func (b *Block) RequestsHash() *common.Hash { return b.header.RequestsHash } +func (b *Block) BlockAccessListHash() *common.Hash { return b.header.BlockAccessListHash } func (b *Block) ExcessBlobGas() *uint64 { var excessBlobGas *uint64 diff --git a/core/types/hashes.go b/core/types/hashes.go index db8912a66f..541681e4db 100644 --- a/core/types/hashes.go +++ b/core/types/hashes.go @@ -43,6 +43,9 @@ var ( // EmptyRequestsHash is the known hash of an empty request set, sha256(""). EmptyRequestsHash = common.HexToHash("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") + // EmptyBlockAccessListHash is the known hash of an empty block accessList, keccak256(rlp.encode([])). + EmptyBlockAccessListHash = common.HexToHash("0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347") + // EmptyBinaryHash is the known hash of an empty binary trie. EmptyBinaryHash = common.Hash{} ) diff --git a/core/vm/evm.go b/core/vm/evm.go index 9fe6faa3a2..832306b9a0 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -709,3 +709,8 @@ func (evm *EVM) GetVMContext() *tracing.VMContext { StateDB: evm.StateDB, } } + +// GetRules returns the chain rules used throughout the EVM execution. +func (evm *EVM) GetRules() params.Rules { + return evm.chainRules +} diff --git a/eth/tracers/api.go b/eth/tracers/api.go index 0df02388b3..88132b4b63 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -1018,7 +1018,7 @@ func (api *API) traceTx(ctx context.Context, tx *types.Transaction, message *cor // Call Prepare to clear out the statedb access list statedb.SetTxContext(txctx.TxHash, txctx.TxIndex, uint32(txctx.TxIndex+1)) - _, err = core.ApplyTransactionWithEVM(message, core.NewGasPool(message.GasLimit), statedb, vmctx.BlockNumber, txctx.BlockHash, vmctx.Time, tx, evm) + _, _, err = core.ApplyTransactionWithEVM(message, core.NewGasPool(message.GasLimit), statedb, vmctx.BlockNumber, txctx.BlockHash, vmctx.Time, tx, evm) if err != nil { return nil, fmt.Errorf("tracing failed: %w", err) } diff --git a/eth/tracers/internal/tracetest/selfdestruct_state_test.go b/eth/tracers/internal/tracetest/selfdestruct_state_test.go index 692c5eb775..39067e8efc 100644 --- a/eth/tracers/internal/tracetest/selfdestruct_state_test.go +++ b/eth/tracers/internal/tracetest/selfdestruct_state_test.go @@ -620,7 +620,7 @@ func TestSelfdestructStateTracer(t *testing.T) { } context := core.NewEVMBlockContext(block.Header(), blockchain, nil) evm := vm.NewEVM(context, hookedState, tt.genesis.Config, vm.Config{Tracer: tracer.Hooks()}) - _, err = core.ApplyTransactionWithEVM(msg, core.NewGasPool(msg.GasLimit), statedb, block.Number(), block.Hash(), block.Time(), tx, evm) + _, _, err = core.ApplyTransactionWithEVM(msg, core.NewGasPool(msg.GasLimit), statedb, block.Number(), block.Hash(), block.Time(), tx, evm) if err != nil { t.Fatalf("failed to execute transaction: %v", err) } diff --git a/internal/ethapi/simulate.go b/internal/ethapi/simulate.go index fa2ff2c32b..8462194b1d 100644 --- a/internal/ethapi/simulate.go +++ b/internal/ethapi/simulate.go @@ -33,6 +33,7 @@ import ( "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/internal/ethapi/override" "github.com/ethereum/go-ethereum/params" @@ -292,9 +293,10 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, gp = core.NewGasPool(blockContext.GasLimit) blobGasUsed uint64 - txes = make([]*types.Transaction, len(block.Calls)) - callResults = make([]simCallResult, len(block.Calls)) - receipts = make([]*types.Receipt, len(block.Calls)) + txes = make([]*types.Transaction, len(block.Calls)) + callResults = make([]simCallResult, len(block.Calls)) + receipts = make([]*types.Receipt, len(block.Calls)) + blockAccessList = bal.NewConstructionBlockAccessList() // Block hash will be repaired after execution. tracer = newTracer(sim.traceTransfers, blockContext.BlockNumber.Uint64(), blockContext.Time, common.Hash{}, common.Hash{}, 0) @@ -313,13 +315,14 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, } evm := vm.NewEVM(blockContext, tracingStateDB, sim.chainConfig, *vmConfig) defer evm.Release() + // It is possible to override precompiles with EVM bytecode, or // move them to another address. if precompiles != nil { evm.SetPrecompiles(precompiles) } // Run pre-execution system calls - core.PreExecution(ctx, header.ParentBeaconRoot, header.ParentHash, sim.chainConfig, evm, header.Number, header.Time) + blockAccessList.Merge(core.PreExecution(ctx, header.ParentBeaconRoot, header.ParentHash, sim.chainConfig, evm, header.Number, header.Time)) var allLogs []*types.Log for i, call := range block.Calls { @@ -350,7 +353,7 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, // Update the state with pending changes. var root []byte if sim.chainConfig.IsByzantium(blockContext.BlockNumber) { - tracingStateDB.Finalise(true) + blockAccessList.Merge(tracingStateDB.Finalise(true)) } else { root = sim.state.IntermediateRoot(sim.chainConfig.IsEIP158(blockContext.BlockNumber)).Bytes() } @@ -391,7 +394,7 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, } // Process EIP-7685 requests - requests, err := core.PostExecution(ctx, sim.chainConfig, header.Number, header.Time, allLogs, evm, uint32(len(block.Calls)+1)) + requests, bal, err := core.PostExecution(ctx, sim.chainConfig, header.Number, header.Time, allLogs, evm, uint32(len(block.Calls)+1)) if err != nil { return nil, nil, nil, err } @@ -399,6 +402,7 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, reqHash := types.CalcRequestsHash(requests) header.RequestsHash = &reqHash } + blockAccessList.Merge(bal) blockBody := &types.Body{ Transactions: txes, @@ -411,8 +415,11 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, } chainHeadReader := &simChainHeadReader{ctx, sim.b} + // Apply the consensus-specific post-transaction changes + sim.b.Engine().Finalize(chainHeadReader, header, sim.state, blockBody, uint32(len(block.Calls)+1), blockAccessList) + // Assemble the block - b := core.AssembleBlock(sim.b.Engine(), chainHeadReader, header, sim.state, blockBody, receipts) + b := core.AssembleBlock(chainHeadReader, header, sim.state, blockBody, receipts, blockAccessList) repairLogs(callResults, b.Hash()) return b, callResults, senders, nil diff --git a/miner/worker.go b/miner/worker.go index 1ecee96688..21bc95cf92 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -32,6 +32,7 @@ import ( "github.com/ethereum/go-ethereum/core/stateless" "github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/internal/telemetry" "github.com/ethereum/go-ethereum/log" @@ -71,6 +72,7 @@ type environment struct { receipts []*types.Receipt sidecars []*types.BlobTxSidecar blobs int + bal *bal.ConstructionBlockAccessList witness *stateless.Witness } @@ -208,7 +210,7 @@ func (miner *Miner) generateWork(ctx context.Context, genParam *generateParams, } // Collect consensus-layer requests if Prague is enabled. - requests, err := core.PostExecution(ctx, miner.chainConfig, work.header.Number, work.header.Time, allLogs, work.evm, uint32(work.tcount+1)) + requests, bal, err := core.PostExecution(ctx, miner.chainConfig, work.header.Number, work.header.Time, allLogs, work.evm, uint32(work.tcount+1)) if err != nil { return &newPayloadResult{err: err} } @@ -216,9 +218,14 @@ func (miner *Miner) generateWork(ctx context.Context, genParam *generateParams, reqHash := types.CalcRequestsHash(requests) work.header.RequestsHash = &reqHash } + work.bal.Merge(bal) + + // Apply the consensus-specific post-transaction changes + miner.engine.Finalize(miner.chain, work.header, work.state, &body, uint32(work.tcount+1), work.bal) + // Assemble the block for delivery. _, _, assembleSpanEnd := telemetry.StartSpan(ctx, "miner.AssembleBlock") - block := core.AssembleBlock(miner.engine, miner.chain, work.header, work.state, &body, work.receipts) + block := core.AssembleBlock(miner.chain, work.header, work.state, &body, work.receipts, work.bal) assembleSpanEnd(nil) return &newPayloadResult{ @@ -318,7 +325,7 @@ func (miner *Miner) prepareWork(ctx context.Context, genParams *generateParams, return nil, err } // Run pre-execution system calls - core.PreExecution(ctx, header.ParentBeaconRoot, header.ParentHash, miner.chainConfig, env.evm, header.Number, header.Time) + env.bal.Merge(core.PreExecution(ctx, header.ParentBeaconRoot, header.ParentHash, miner.chainConfig, env.evm, header.Number, header.Time)) return env, nil } @@ -337,6 +344,7 @@ func (miner *Miner) makeEnv(parent *types.Header, header *types.Header, coinbase } } 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), @@ -345,6 +353,7 @@ func (miner *Miner) makeEnv(parent *types.Header, header *types.Header, coinbase coinbase: coinbase, gasPool: core.NewGasPool(header.GasLimit), header: header, + bal: bal.NewConstructionBlockAccessList(), witness: state.Witness(), evm: vm.NewEVM(core.NewEVMBlockContext(header, miner.chain, &coinbase), state, miner.chainConfig, vm.Config{}), }, nil @@ -356,7 +365,7 @@ func (miner *Miner) commitTransaction(ctx context.Context, env *environment, tx if tx.Type() == types.BlobTxType { return miner.commitBlobTransaction(env, tx) } - receipt, err := miner.applyTransaction(env, tx) + receipt, bal, err := miner.applyTransaction(env, tx) if err != nil { return err } @@ -364,6 +373,7 @@ func (miner *Miner) commitTransaction(ctx context.Context, env *environment, tx env.receipts = append(env.receipts, receipt) env.size += tx.Size() env.tcount++ + env.bal.Merge(bal) return nil } @@ -380,7 +390,7 @@ func (miner *Miner) commitBlobTransaction(env *environment, tx *types.Transactio if env.blobs+len(sc.Blobs) > maxBlobs { return errors.New("max data blobs reached") } - receipt, err := miner.applyTransaction(env, tx) + receipt, bal, err := miner.applyTransaction(env, tx) if err != nil { return err } @@ -392,23 +402,24 @@ func (miner *Miner) commitBlobTransaction(env *environment, tx *types.Transactio env.size += txNoBlob.Size() *env.header.BlobGasUsed += receipt.BlobGasUsed env.tcount++ + env.bal.Merge(bal) return nil } // applyTransaction runs the transaction. If execution fails, state and gas pool are reverted. -func (miner *Miner) applyTransaction(env *environment, tx *types.Transaction) (*types.Receipt, error) { +func (miner *Miner) applyTransaction(env *environment, tx *types.Transaction) (*types.Receipt, *bal.ConstructionBlockAccessList, error) { var ( snap = env.state.Snapshot() gp = env.gasPool.Snapshot() ) - receipt, err := core.ApplyTransaction(env.evm, env.gasPool, env.state, env.header, tx) + receipt, bal, err := core.ApplyTransaction(env.evm, env.gasPool, env.state, env.header, tx) if err != nil { env.state.RevertToSnapshot(snap) env.gasPool.Set(gp) - return nil, err + return nil, nil, err } env.header.GasUsed = env.gasPool.Used() - return receipt, nil + return receipt, bal, nil } func (miner *Miner) commitTransactions(ctx context.Context, env *environment, plainTxs, blobTxs *transactionsByPriceAndNonce, interrupt *atomic.Int32) error { diff --git a/params/protocol_params.go b/params/protocol_params.go index 9da275c486..3e36b83547 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -186,6 +186,16 @@ const ( HistoryServeWindow = 8191 // Number of blocks to serve historical block hashes for, EIP-2935. MaxBlockSize = 8_388_608 // maximum size of an RLP-encoded block + + // BALItemCost is the gas-cost divisor for the EIP-7928 block access list + // size constraint: bal_items <= block_gas_limit / BALItemCost, where + // bal_items counts every distinct address in the BAL plus every storage + // key (writes + reads) carried by those accounts. + // + // The value (2000) is set deliberately below COLD_SLOAD_COST (2100) so + // the bound has a small safety margin for system-contract accesses that + // don't consume block gas. + BALItemCost uint64 = 2000 ) // Bls12381G1MultiExpDiscountTable is the gas discount table for BLS12-381 G1 multi exponentiation operation