From 75a64ee3413ef324641b9306efdb5d57e8a01cf8 Mon Sep 17 00:00:00 2001 From: Bosul Mun Date: Thu, 30 Apr 2026 17:55:26 +0200 Subject: [PATCH 01/53] eth/downloader: drop peers sending invalid bodies or receipts (#34745) - Fixes an error shadowing issue in the deliver() function, where a stale result from GetDeliverySlot caused the original failure to be overwritten by errStaleDelivery. - Adds errInvalidBody and errInvalidReceipt to the downloader error checks to properly drop peers who sent invalid responses. --------- Co-authored-by: Felix Lange --- eth/downloader/downloader_test.go | 35 ++++++++++++++++++- eth/downloader/fetchers_concurrent.go | 49 +++++++++++++++++---------- eth/downloader/queue.go | 29 ++++++++-------- 3 files changed, 80 insertions(+), 33 deletions(-) diff --git a/eth/downloader/downloader_test.go b/eth/downloader/downloader_test.go index 9280d455fb..6d5d159631 100644 --- a/eth/downloader/downloader_test.go +++ b/eth/downloader/downloader_test.go @@ -96,6 +96,7 @@ func (dl *downloadTester) newPeer(id string, version uint, blocks []*types.Block id: id, chain: newTestBlockchain(blocks), withholdBodies: make(map[common.Hash]struct{}), + dropped: make(chan error, 1), } dl.peers[id] = peer @@ -121,8 +122,11 @@ func (dl *downloadTester) dropPeer(id string) { type downloadTesterPeer struct { dl *downloadTester withholdBodies map[common.Hash]struct{} + corruptBodies bool // if set, the peer serves incorrect blocks id string chain *core.BlockChain + + dropped chan error // signaled when res.Done receives an error } func unmarshalRlpHeaders(rlpdata []rlp.RawValue) []*types.Header { @@ -236,6 +240,11 @@ func (dlp *downloadTesterPeer) RequestBodies(hashes []common.Hash, sink chan *et txsHashes[i] = hash uncleHashes[i] = types.CalcUncleHash(body.Uncles) } + if dlp.corruptBodies { + for i := range txsHashes { + txsHashes[i] = common.Hash{0xff} + } + } req := ð.Request{ Peer: dlp.id, } @@ -248,10 +257,16 @@ func (dlp *downloadTesterPeer) RequestBodies(hashes []common.Hash, sink chan *et WithdrawalRoots: withdrawalHashes, }, Time: 1, - Done: make(chan error, 1), // Ignore the returned status + Done: make(chan error), } go func() { sink <- res + if err := <-res.Done; err != nil { + select { + case dlp.dropped <- err: + default: + } + } }() return req, nil } @@ -704,3 +719,21 @@ func testSyncProgress(t *testing.T, protocol uint, mode SyncMode) { t.Fatalf("Failed to sync chain in three seconds") } } + +func TestInvalidBodyPeerDrop(t *testing.T) { + tester := newTester(t, FullSync) + defer tester.terminate() + + chain := testChainBase.shorten(blockCacheMaxItems - 15) + peer := tester.newPeer("corrupt", eth.ETH69, chain.blocks[1:]) + peer.corruptBodies = true + + if err := tester.downloader.BeaconSync(chain.blocks[len(chain.blocks)-1].Header(), nil); err != nil { + t.Fatalf("failed to beacon-sync chain: %v", err) + } + select { + case <-peer.dropped: + case <-time.After(1 * time.Minute): + t.Fatal("peer was not dropped") + } +} diff --git a/eth/downloader/fetchers_concurrent.go b/eth/downloader/fetchers_concurrent.go index 9d8cd114c1..51bf3404bd 100644 --- a/eth/downloader/fetchers_concurrent.go +++ b/eth/downloader/fetchers_concurrent.go @@ -323,25 +323,32 @@ func (d *Downloader) concurrentFetch(queue typedQueue) error { delete(pending, res.Req.Peer) delete(stales, res.Req.Peer) - // Signal the dispatcher that the round trip is done. We'll drop the - // peer if the data turns out to be junk. - res.Done <- nil - res.Req.Close() - // If the peer was previously banned and failed to deliver its pack // in a reasonable time frame, ignore its message. - if peer := d.peers.Peer(res.Req.Peer); peer != nil { - // Deliver the received chunk of data and check chain validity - accepted, err := queue.deliver(peer, res) - if errors.Is(err, errInvalidChain) { - return err - } - // Unless a peer delivered something completely else than requested (usually - // caused by a timed out request which came through in the end), set it to - // idle. If the delivery's stale, the peer should have already been idled. - if !errors.Is(err, errStaleDelivery) { - queue.updateCapacity(peer, accepted, res.Time) - } + peer := d.peers.Peer(res.Req.Peer) + if peer == nil { + res.Done <- nil + res.Req.Close() + continue + } + // Deliver the received chunk of data and check chain validity + accepted, err := queue.deliver(peer, res) + // Unless a peer delivered something completely else than requested (usually + // caused by a timed out request which came through in the end), set it to + // idle. If the delivery's stale, the peer should have already been idled. + if !errors.Is(err, errStaleDelivery) { + queue.updateCapacity(peer, accepted, res.Time) + } + res.Done <- validityErrorOfRequest(err) + res.Req.Close() + + if errors.Is(err, errInvalidChain) { + // errInvalidChain is the signal that processing of items failed internally, + // even though the items were validly encoded. + // + // This can be due to invalid blocks, or a database error. + // The sync cycle should be aborted for such errors, so we return it here. + return err } case cont := <-queue.waker(): @@ -352,3 +359,11 @@ func (d *Downloader) concurrentFetch(queue typedQueue) error { } } } + +// validityErrorOfRequest returns err if it is related to block validity, and nil otherwise. +func validityErrorOfRequest(err error) error { + if errors.Is(err, errInvalidBody) || errors.Is(err, errInvalidReceipt) { + return err + } + return nil +} diff --git a/eth/downloader/queue.go b/eth/downloader/queue.go index c0cb9b174a..dd17b7f1ed 100644 --- a/eth/downloader/queue.go +++ b/eth/downloader/queue.go @@ -671,10 +671,10 @@ func (q *queue) deliver(id string, taskPool map[common.Hash]*types.Header, } // Assemble each of the results with their headers and retrieved data parts var ( - accepted int - failure error - i int - hashes []common.Hash + accepted int + failure error + i int + foundStale bool ) for _, header := range request.Headers { // Short circuit assembly if no more fetch results are found @@ -686,42 +686,41 @@ func (q *queue) deliver(id string, taskPool map[common.Hash]*types.Header, failure = err break } - hashes = append(hashes, header.Hash()) i++ } for _, header := range request.Headers[:i] { if res, stale, err := q.resultCache.GetDeliverySlot(header.Number.Uint64()); err == nil && !stale { reconstruct(accepted, res) + accepted++ } else { - // else: between here and above, some other peer filled this result, + // Between here and above, some other peer filled this result, // or it was indeed a no-op. This should not happen, but if it does it's // not something to panic about log.Error("Delivery stale", "stale", stale, "number", header.Number.Uint64(), "err", err) - failure = errStaleDelivery + foundStale = true } // Clean up a successful fetch - delete(taskPool, hashes[accepted]) - accepted++ + delete(taskPool, header.Hash()) } resDropMeter.Mark(int64(results - accepted)) // Return all failed or missing fetches to the queue - for _, header := range request.Headers[accepted:] { + for _, header := range request.Headers[i:] { taskQueue.Push(header, -int64(header.Number.Uint64())) } // Wake up Results if accepted > 0 { q.active.Signal() } - if failure == nil { - return accepted, nil + if failure != nil { + return accepted, failure } // If none of the data was good, it's a stale delivery - if accepted > 0 { - return accepted, fmt.Errorf("partial failure: %v", failure) + if foundStale { + return accepted, errStaleDelivery } - return accepted, fmt.Errorf("%w: %v", failure, errStaleDelivery) + return accepted, nil } // Prepare configures the result cache to allow accepting and caching inbound From 19dc690af86278b4e84fd3d7eee9fd810bcbac25 Mon Sep 17 00:00:00 2001 From: cui Date: Fri, 1 May 2026 00:24:22 +0800 Subject: [PATCH 02/53] triedb/pathdb: fix layer 5 key range in account iterator traversal test (#34639) The layer-5 diff condition used `i > 50 || i < 85`, which is true for almost all keys in the 0..255 loop. Use `i > 50 && i < 85` so layer 5 only covers the intended band (51..84), consistent with the snapshot iterator test fix. --- 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 adb534f47d..2197e85272 100644 --- a/triedb/pathdb/iterator_test.go +++ b/triedb/pathdb/iterator_test.go @@ -369,7 +369,7 @@ func TestAccountIteratorTraversalValues(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 68646229a0275acb18fceef83bf1733ced88ef00 Mon Sep 17 00:00:00 2001 From: cui Date: Fri, 1 May 2026 20:10:41 +0800 Subject: [PATCH 03/53] internal/era/onedb: return false if err (#34816) Next() function in RawIterator returned true on decompression errors.Now it returns false on those cases. Redundant error check on cmd/era/main.go is also removed. --------- Co-authored-by: Bosul Mun --- cmd/era/main.go | 3 --- internal/era/onedb/iterator.go | 8 ++++---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/cmd/era/main.go b/cmd/era/main.go index 1c26f44ad4..43279e7001 100644 --- a/cmd/era/main.go +++ b/cmd/era/main.go @@ -337,9 +337,6 @@ func checkAccumulator(e era.Era) error { // accumulation across the entire set and are verified at the end. for it.Next() { // 1) next() walks the block index, so we're able to implicitly verify it. - if it.Error() != nil { - return fmt.Errorf("error reading block %d: %w", it.Number(), it.Error()) - } block, receipts, err := it.BlockAndReceipts() if err != nil { return fmt.Errorf("error reading block %d: %w", it.Number(), err) diff --git a/internal/era/onedb/iterator.go b/internal/era/onedb/iterator.go index b80fbabbc5..21dc5acbe0 100644 --- a/internal/era/onedb/iterator.go +++ b/internal/era/onedb/iterator.go @@ -156,22 +156,22 @@ func (it *RawIterator) Next() bool { var n int64 if it.Header, n, it.err = newSnappyReader(it.e.s, era.TypeCompressedHeader, off); it.err != nil { it.clear() - return true + return false } off += n if it.Body, n, it.err = newSnappyReader(it.e.s, era.TypeCompressedBody, off); it.err != nil { it.clear() - return true + return false } off += n if it.Receipts, n, it.err = newSnappyReader(it.e.s, era.TypeCompressedReceipts, off); it.err != nil { it.clear() - return true + return false } off += n if it.TotalDifficulty, _, it.err = it.e.s.ReaderAt(era.TypeTotalDifficulty, off); it.err != nil { it.clear() - return true + return false } it.next += 1 return true 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 04/53] 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 05/53] 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 06/53] 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 07/53] 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 08/53] 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 09/53] 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 10/53] 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 11/53] 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 12/53] 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 13/53] 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 14/53] 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 15/53] 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 16/53] 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 17/53] 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 18/53] 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 19/53] 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 20/53] 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 21/53] 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 22/53] 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 23/53] 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 24/53] 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 25/53] 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 26/53] 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 27/53] 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 28/53] 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 29/53] .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 30/53] 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 31/53] 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 32/53] 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 33/53] 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 34/53] 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 35/53] 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 36/53] 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 37/53] 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 38/53] 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 39/53] 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 40/53] 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 41/53] 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 42/53] 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 43/53] 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 44/53] 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 45/53] 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 46/53] 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 47/53] 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 48/53] 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 49/53] 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 50/53] 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 51/53] 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 52/53] 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 53/53] 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)