From 0ab02b4383ebbfedb2aac4b05752c74c7e5ccd1b Mon Sep 17 00:00:00 2001 From: rayoo Date: Thu, 30 Apr 2026 18:11:14 +0800 Subject: [PATCH] core/txpool/legacypool: skip MaxTxGas purge when Amsterdam is active PR #34841 lifts the EIP-7825 per-transaction gas cap once Amsterdam (EIP-8037) is active, since tx.Gas() then includes the state-gas reservoir in addition to the regular gas dimension. The PR added an !IsAmsterdam guard in five call sites (txpool.ValidateTransaction, state_transition.preCheck, miner.fillTransactions, eth/gasestimator, cmd/evm t8ntool) but missed one: legacypool.runReorg still purges all transactions with tx.Gas() > MaxTxGas at the Osaka fork boundary, regardless of whether Amsterdam is also active on the new head. On a chain that activates Osaka and Amsterdam at the same timestamp (the expected EIP-8037 devnet/testnet layout before mainnet cuts the two forks), the first reorg crossing that boundary deletes otherwise valid Amsterdam transactions whose total gas legitimately exceeds MaxTxGas. Add the missing IsAmsterdam guard so the purge matches the stated semantics of #34841. Add a regression test that constructs a chain config with OsakaTime == AmsterdamTime, injects a tx with gas = MaxTxGas+100k in the pre-fork state, and verifies the tx survives the reorg into the post-fork head. The test fails on master and passes with this change. --- core/txpool/legacypool/legacypool.go | 6 +- core/txpool/legacypool/legacypool_test.go | 76 +++++++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/core/txpool/legacypool/legacypool.go b/core/txpool/legacypool/legacypool.go index 00630de04c..fefecb6c64 100644 --- a/core/txpool/legacypool/legacypool.go +++ b/core/txpool/legacypool/legacypool.go @@ -1223,9 +1223,11 @@ func (pool *LegacyPool) runReorg(done chan struct{}, reset *txpoolResetRequest, if reset != nil { if reset.newHead != nil && reset.oldHead != nil { // Discard the transactions with the gas limit higher than the cap at the - // Osaka fork boundary. + // Osaka fork boundary. Amsterdam/EIP-8037 lifts the per-transaction gas + // cap, so skip this purge when Amsterdam is also active on the new head. if pool.chainconfig.IsOsaka(reset.newHead.Number, reset.newHead.Time) && - !pool.chainconfig.IsOsaka(reset.oldHead.Number, reset.oldHead.Time) { + !pool.chainconfig.IsOsaka(reset.oldHead.Number, reset.oldHead.Time) && + !pool.chainconfig.IsAmsterdam(reset.newHead.Number, reset.newHead.Time) { var hashes []common.Hash pool.all.Range(func(hash common.Hash, tx *types.Transaction) bool { if tx.Gas() > params.MaxTxGas { diff --git a/core/txpool/legacypool/legacypool_test.go b/core/txpool/legacypool/legacypool_test.go index f8592ba001..e41e8d5142 100644 --- a/core/txpool/legacypool/legacypool_test.go +++ b/core/txpool/legacypool/legacypool_test.go @@ -2627,6 +2627,82 @@ func BenchmarkPendingDemotion100(b *testing.B) { benchmarkPendingDemotion(b, 1 func BenchmarkPendingDemotion1000(b *testing.B) { benchmarkPendingDemotion(b, 1000) } func BenchmarkPendingDemotion10000(b *testing.B) { benchmarkPendingDemotion(b, 10000) } +// TestOsakaAmsterdamGasCapPurge verifies that the legacy pool does NOT purge +// transactions with gas > MaxTxGas at the Osaka fork boundary when Amsterdam is +// also active. Under EIP-8037 (Amsterdam), transactions can include state-gas +// in addition to regular gas, so tx.Gas() is allowed to exceed MaxTxGas. +// +// The fix for this (#34841) added the IsAmsterdam guard in 4 of the 5 sites +// (ValidateTransaction, preCheck, miner.fillTransactions, gasestimator, t8ntool), +// but the legacypool.runReorg site was missed: it still purges on +// IsOsaka(newHead) && !IsOsaka(oldHead) without checking IsAmsterdam. +// +// Failing without fix: the pre-injected high-gas transaction is removed after +// the reorg into Osaka/Amsterdam, even though Amsterdam makes it legal. +func TestOsakaAmsterdamGasCapPurge(t *testing.T) { + t.Parallel() + + // Fork config: Osaka and Amsterdam activate at the same timestamp (1000). + // Before 1000 both are inactive; at/after 1000 both are active. + forkTime := uint64(1000) + config := *params.MergedTestChainConfig + config.OsakaTime = &forkTime + config.AmsterdamTime = &forkTime + + statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting()) + // gasLimit must accommodate the high-gas tx. + blockchain := newTestBlockChain(&config, 50_000_000, statedb, new(event.Feed)) + + key, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(key.PublicKey) + // Enough balance for gas*gasPrice + value with a 20M-gas tx. + statedb.AddBalance(addr, uint256.NewInt(1e18), tracing.BalanceChangeUnspecified) + + pool := New(testTxPoolConfig, blockchain) + // Init pool at a pre-fork head (Time=0). At this time IsOsaka=false so + // tx.Gas() > MaxTxGas is NOT rejected by stateless validation. + preForkHead := &types.Header{ + Number: big.NewInt(0), + Difficulty: common.Big0, + GasLimit: 50_000_000, + BaseFee: big.NewInt(1), + Time: 0, + } + if err := pool.Init(testTxPoolConfig.PriceLimit, preForkHead, newReserver()); err != nil { + t.Fatalf("pool init: %v", err) + } + defer pool.Close() + <-pool.initDoneCh + + // Inject a transaction with gas above MaxTxGas. Valid under pre-Osaka rules. + highGasTx := pricedTransaction(0, params.MaxTxGas+100_000, big.NewInt(1), key) + if err := pool.addRemoteSync(highGasTx); err != nil { + t.Fatalf("failed to add high-gas tx: %v", err) + } + if pool.Status(highGasTx.Hash()) == txpool.TxStatusUnknown { + t.Fatalf("tx missing after add (pre-reorg)") + } + + // Reorg: oldHead is still pre-fork (Time=0), newHead crosses into Osaka AND + // Amsterdam (Time=forkTime). Under the current buggy code this triggers the + // purge branch because IsOsaka(newHead) && !IsOsaka(oldHead), ignoring that + // Amsterdam is also active. + newHead := &types.Header{ + Number: big.NewInt(1), + Difficulty: common.Big0, + GasLimit: 50_000_000, + BaseFee: big.NewInt(1), + Time: forkTime, + } + <-pool.requestReset(preForkHead, newHead) + + // With the fix: tx is kept because Amsterdam is active and tx.Gas() > + // MaxTxGas is legal. Without the fix: tx is purged. + if pool.Status(highGasTx.Hash()) == txpool.TxStatusUnknown { + t.Fatalf("high-gas tx was purged despite Amsterdam being active at newHead; runReorg is missing the !IsAmsterdam guard at legacypool.go:1227") + } +} + func benchmarkPendingDemotion(b *testing.B, size int) { // Add a batch of transactions to a pool one by one pool, key := setupPool()