Skip to content

Commit c6e0572

Browse files
nirantynes
andauthored
txpool: introduce MaxTxGasLimit feature to enforce per-transaction gas limits (#626)
Adds a new flag `--txpool.maxtxgas` which represents the maximum gas limit for individual transactions (0 = no limit) when added to the mempool. Transactions exceeding this limit will be rejected by the transaction pool. Co-authored-by: Mark Tyneway <[email protected]>
1 parent 11218e8 commit c6e0572

File tree

7 files changed

+191
-3
lines changed

7 files changed

+191
-3
lines changed

cmd/geth/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ var (
8686
utils.TxPoolAccountQueueFlag,
8787
utils.TxPoolGlobalQueueFlag,
8888
utils.TxPoolLifetimeFlag,
89+
utils.TxPoolMaxTxGasLimitFlag,
8990
utils.BlobPoolDataDirFlag,
9091
utils.BlobPoolDataCapFlag,
9192
utils.BlobPoolPriceBumpFlag,

cmd/utils/flags.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,12 @@ var (
478478
Value: ethconfig.Defaults.TxPool.Lifetime,
479479
Category: flags.TxPoolCategory,
480480
}
481+
TxPoolMaxTxGasLimitFlag = &cli.Uint64Flag{
482+
Name: "txpool.maxtxgas",
483+
Usage: "Maximum gas limit for individual transactions (0 = no limit). Transactions exceeding this limit will be rejected by the transaction pool",
484+
Value: 0,
485+
Category: flags.TxPoolCategory,
486+
}
481487
// Blob transaction pool settings
482488
BlobPoolDataDirFlag = &cli.StringFlag{
483489
Name: "blobpool.datadir",
@@ -1671,6 +1677,9 @@ func setTxPool(ctx *cli.Context, cfg *legacypool.Config) {
16711677
// it to avoid accepting transactions that can never be included in a block.
16721678
cfg.EffectiveGasCeil = ctx.Uint64(MinerEffectiveGasLimitFlag.Name)
16731679
}
1680+
if ctx.IsSet(TxPoolMaxTxGasLimitFlag.Name) {
1681+
cfg.MaxTxGasLimit = ctx.Uint64(TxPoolMaxTxGasLimitFlag.Name)
1682+
}
16741683
}
16751684

16761685
func setBlobPool(ctx *cli.Context, cfg *blobpool.Config) {

core/txpool/errors.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,8 @@ var (
6767
// ErrInflightTxLimitReached is returned when the maximum number of in-flight
6868
// transactions is reached for specific accounts.
6969
ErrInflightTxLimitReached = errors.New("in-flight transaction limit reached for delegated accounts")
70+
71+
// ErrTxGasLimitExceeded is returned if a transaction's gas limit exceeds the
72+
// configured maximum per-transaction limit.
73+
ErrTxGasLimitExceeded = errors.New("exceeds maximum per-transaction gas limit")
7074
)

core/txpool/legacypool/legacypool.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ type Config struct {
158158
Lifetime time.Duration // Maximum amount of time non-executable transaction are queued
159159

160160
EffectiveGasCeil uint64 // OP-Stack: if non-zero, a gas ceiling to enforce independent of the header's gaslimit value
161+
MaxTxGasLimit uint64 // Maximum gas limit allowed per individual transaction
161162

162163
// FilterInterval defines how often already-added transactions are rechecked
163164
// against ingress filters.
@@ -177,6 +178,8 @@ var DefaultConfig = Config{
177178
AccountQueue: 64,
178179
GlobalQueue: 1024,
179180

181+
MaxTxGasLimit: 0, // 0 means no limit (default behavior)
182+
180183
Lifetime: 3 * time.Hour,
181184
FilterInterval: 12 * time.Second,
182185
}
@@ -652,6 +655,7 @@ func (pool *LegacyPool) ValidateTxBasics(tx *types.Transaction) error {
652655
MaxSize: txMaxSize,
653656
MinTip: pool.gasTip.Load().ToBig(),
654657
EffectiveGasCeil: pool.config.EffectiveGasCeil,
658+
MaxTxGasLimit: pool.config.MaxTxGasLimit,
655659
}
656660
return txpool.ValidateTransaction(tx, pool.currentHead.Load(), pool.signer, opts)
657661
}

core/txpool/legacypool/legacypool_test.go

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -219,12 +219,17 @@ func (f *dummyFilter) FilterTx(ctx context.Context, tx *types.Transaction) bool
219219
}
220220

221221
func setupPoolWithConfig(config *params.ChainConfig) (*LegacyPool, *ecdsa.PrivateKey) {
222+
return setupPoolWithTxPoolConfig(config, testTxPoolConfig)
223+
}
224+
225+
// setupPoolWithTxPoolConfig creates a new pool with custom pool configuration
226+
func setupPoolWithTxPoolConfig(chainConfig *params.ChainConfig, poolConfig Config) (*LegacyPool, *ecdsa.PrivateKey) {
222227
statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
223-
blockchain := newTestBlockChain(config, 10000000, statedb, new(event.Feed))
228+
blockchain := newTestBlockChain(chainConfig, 10000000, statedb, new(event.Feed))
224229

225230
key, _ := crypto.GenerateKey()
226-
pool := New(testTxPoolConfig, blockchain)
227-
if err := pool.Init(testTxPoolConfig.PriceLimit, blockchain.CurrentBlock(), newReserver()); err != nil {
231+
pool := New(poolConfig, blockchain)
232+
if err := pool.Init(poolConfig.PriceLimit, blockchain.CurrentBlock(), newReserver()); err != nil {
228233
panic(err)
229234
}
230235
// wait for the pool to initialize
@@ -2767,3 +2772,51 @@ func BenchmarkMultiAccountBatchInsert(b *testing.B) {
27672772
pool.addRemotesSync([]*types.Transaction{tx})
27682773
}
27692774
}
2775+
2776+
func TestTxPoolMaxTxGasLimit(t *testing.T) {
2777+
t.Parallel()
2778+
2779+
// Create custom config with MaxTxGasLimit set
2780+
config := testTxPoolConfig
2781+
config.MaxTxGasLimit = 50000
2782+
2783+
pool, key := setupPoolWithTxPoolConfig(params.TestChainConfig, config)
2784+
defer pool.Close()
2785+
2786+
// Create transaction that exceeds the limit
2787+
tx := transaction(0, 100000, key) // gas limit > 50000
2788+
from, _ := deriveSender(tx)
2789+
testAddBalance(pool, from, big.NewInt(1000000))
2790+
2791+
// Should be rejected
2792+
if err := pool.addRemoteSync(tx); !errors.Is(err, txpool.ErrTxGasLimitExceeded) {
2793+
t.Errorf("Expected ErrTxGasLimitExceeded, got %v", err)
2794+
}
2795+
2796+
// Create transaction within the limit
2797+
tx2 := transaction(0, 30000, key) // gas limit < 50000
2798+
if err := pool.addRemoteSync(tx2); err != nil {
2799+
t.Errorf("Expected transaction within limit to be accepted, got %v", err)
2800+
}
2801+
}
2802+
2803+
func TestTxPoolMaxTxGasLimitDisabled(t *testing.T) {
2804+
t.Parallel()
2805+
2806+
// Test with default config (MaxTxGasLimit = 0, disabled)
2807+
config := testTxPoolConfig
2808+
config.MaxTxGasLimit = 0
2809+
2810+
pool, key := setupPoolWithTxPoolConfig(params.TestChainConfig, config)
2811+
defer pool.Close()
2812+
2813+
// Create transaction with high gas limit
2814+
tx := transaction(0, 100000, key)
2815+
from, _ := deriveSender(tx)
2816+
testAddBalance(pool, from, big.NewInt(1000000))
2817+
2818+
// Should be accepted since MaxTxGasLimit is disabled (0)
2819+
if err := pool.addRemoteSync(tx); err != nil {
2820+
t.Errorf("Expected transaction to be accepted when MaxTxGasLimit is disabled, got %v", err)
2821+
}
2822+
}

core/txpool/validation.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ type ValidationOptions struct {
6666
MinTip *big.Int // Minimum gas tip needed to allow a transaction into the caller pool
6767

6868
EffectiveGasCeil uint64 // if non-zero, a gas ceiling to enforce independent of the header's gaslimit value
69+
MaxTxGasLimit uint64 // Maximum gas limit allowed per individual transaction
6970
}
7071

7172
// ValidationFunction is an method type which the pools use to perform the tx-validations which do not
@@ -125,6 +126,10 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types
125126
if EffectiveGasLimit(opts.Config, head.GasLimit, opts.EffectiveGasCeil) < tx.Gas() {
126127
return ErrGasLimit
127128
}
129+
// Check individual transaction gas limit if configured
130+
if opts.MaxTxGasLimit != 0 && tx.Gas() > opts.MaxTxGasLimit {
131+
return fmt.Errorf("%w: transaction gas %v, limit %v", ErrTxGasLimitExceeded, tx.Gas(), opts.MaxTxGasLimit)
132+
}
128133
// Sanity check for extremely large numbers (supported by RLP or RPC)
129134
if tx.GasFeeCap().BitLen() > 256 {
130135
return core.ErrFeeCapVeryHigh

core/txpool/validation_test.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Copyright 2025 The go-ethereum Authors
2+
// This file is part of the go-ethereum library.
3+
//
4+
// The go-ethereum library is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// The go-ethereum library is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package txpool
18+
19+
import (
20+
"errors"
21+
"math/big"
22+
"testing"
23+
24+
"github.com/ethereum/go-ethereum/common"
25+
"github.com/ethereum/go-ethereum/core/types"
26+
"github.com/ethereum/go-ethereum/crypto"
27+
"github.com/ethereum/go-ethereum/params"
28+
)
29+
30+
func TestValidateTransactionMaxTxGasLimit(t *testing.T) {
31+
// Create a test private key and signer
32+
key, _ := crypto.GenerateKey()
33+
signer := types.NewEIP155Signer(big.NewInt(1))
34+
35+
tests := []struct {
36+
name string
37+
maxTxGasLimit uint64
38+
txGasLimit uint64
39+
expectError bool
40+
expectedError error
41+
}{
42+
{
43+
name: "No limit set",
44+
maxTxGasLimit: 0,
45+
txGasLimit: 1000000,
46+
expectError: false,
47+
},
48+
{
49+
name: "Under limit",
50+
maxTxGasLimit: 100000,
51+
txGasLimit: 50000,
52+
expectError: false,
53+
},
54+
{
55+
name: "At limit",
56+
maxTxGasLimit: 100000,
57+
txGasLimit: 100000,
58+
expectError: false,
59+
},
60+
{
61+
name: "Over limit",
62+
maxTxGasLimit: 100000,
63+
txGasLimit: 150000,
64+
expectError: true,
65+
expectedError: ErrTxGasLimitExceeded,
66+
},
67+
}
68+
69+
for _, test := range tests {
70+
t.Run(test.name, func(t *testing.T) {
71+
// Create test transaction with specified gas limit
72+
tx := types.NewTransaction(0, common.Address{}, big.NewInt(0), test.txGasLimit, big.NewInt(1000000000), nil)
73+
74+
// Sign the transaction
75+
signedTx, err := types.SignTx(tx, signer, key)
76+
if err != nil {
77+
t.Fatalf("Failed to sign transaction: %v", err)
78+
}
79+
80+
// Create minimal validation options
81+
opts := &ValidationOptions{
82+
Config: params.TestChainConfig,
83+
Accept: 1 << types.LegacyTxType,
84+
MaxSize: 32 * 1024,
85+
MinTip: big.NewInt(0),
86+
MaxTxGasLimit: test.maxTxGasLimit,
87+
}
88+
89+
// Create test header with high gas limit to not interfere
90+
header := &types.Header{
91+
Number: big.NewInt(1),
92+
GasLimit: 10000000,
93+
Time: 0,
94+
Difficulty: big.NewInt(0),
95+
}
96+
97+
err = ValidateTransaction(signedTx, header, signer, opts)
98+
99+
if test.expectError {
100+
if err == nil {
101+
t.Errorf("Expected error but got none")
102+
} else if !errors.Is(err, test.expectedError) {
103+
t.Errorf("Expected error %v, got %v", test.expectedError, err)
104+
}
105+
} else {
106+
if err != nil {
107+
t.Errorf("Unexpected error: %v", err)
108+
}
109+
}
110+
})
111+
}
112+
}

0 commit comments

Comments
 (0)