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.
This commit is contained in:
rayoo 2026-04-30 18:11:14 +08:00
parent 01036bed83
commit 0ab02b4383
2 changed files with 80 additions and 2 deletions

View file

@ -1223,9 +1223,11 @@ func (pool *LegacyPool) runReorg(done chan struct{}, reset *txpoolResetRequest,
if reset != nil { if reset != nil {
if reset.newHead != nil && reset.oldHead != nil { if reset.newHead != nil && reset.oldHead != nil {
// Discard the transactions with the gas limit higher than the cap at the // 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) && 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 var hashes []common.Hash
pool.all.Range(func(hash common.Hash, tx *types.Transaction) bool { pool.all.Range(func(hash common.Hash, tx *types.Transaction) bool {
if tx.Gas() > params.MaxTxGas { if tx.Gas() > params.MaxTxGas {

View file

@ -2627,6 +2627,82 @@ func BenchmarkPendingDemotion100(b *testing.B) { benchmarkPendingDemotion(b, 1
func BenchmarkPendingDemotion1000(b *testing.B) { benchmarkPendingDemotion(b, 1000) } func BenchmarkPendingDemotion1000(b *testing.B) { benchmarkPendingDemotion(b, 1000) }
func BenchmarkPendingDemotion10000(b *testing.B) { benchmarkPendingDemotion(b, 10000) } 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) { func benchmarkPendingDemotion(b *testing.B, size int) {
// Add a batch of transactions to a pool one by one // Add a batch of transactions to a pool one by one
pool, key := setupPool() pool, key := setupPool()