Skip to content

Commit 950d564

Browse files
authored
core/txpool: make transaction validation reusable across packages (pools) (ethereum#27429)
* core/txpool: abstraction prep work for secondary pools (blob pool) * core/txpool: leave subpool concepts to a followup pr * les: fix tests using hard coded errors * core/txpool: use bitmaps instead of maps for tx type filtering
1 parent 4cf708d commit 950d564

File tree

13 files changed

+336
-180
lines changed

13 files changed

+336
-180
lines changed

cmd/geth/main.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ func startNode(ctx *cli.Context, stack *node.Node, backend ethapi.Backend, isCon
419419
}
420420
// Set the gas price to the limits from the CLI and start mining
421421
gasprice := flags.GlobalBig(ctx, utils.MinerGasPriceFlag.Name)
422-
ethBackend.TxPool().SetGasPrice(gasprice)
422+
ethBackend.TxPool().SetGasTip(gasprice)
423423
if err := ethBackend.StartMining(); err != nil {
424424
utils.Fatalf("Failed to start mining: %v", err)
425425
}

core/txpool/txpool.go

+55-125
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package txpool
1818

1919
import (
2020
"errors"
21-
"fmt"
2221
"math"
2322
"math/big"
2423
"sort"
@@ -91,10 +90,6 @@ var (
9190
// ErrFutureReplacePending is returned if a future transaction replaces a pending
9291
// transaction. Future transactions should only be able to replace other future transactions.
9392
ErrFutureReplacePending = errors.New("future transaction tries to replace pending")
94-
95-
// ErrOverdraft is returned if a transaction would cause the senders balance to go negative
96-
// thus invalidating a potential large number of transactions.
97-
ErrOverdraft = errors.New("transaction would cause overdraft")
9893
)
9994

10095
var (
@@ -178,8 +173,7 @@ type Config struct {
178173
Lifetime time.Duration // Maximum amount of time non-executable transaction are queued
179174
}
180175

181-
// DefaultConfig contains the default configurations for the transaction
182-
// pool.
176+
// DefaultConfig contains the default configurations for the transaction pool.
183177
var DefaultConfig = Config{
184178
Journal: "transactions.rlp",
185179
Rejournal: time.Hour,
@@ -245,20 +239,15 @@ type TxPool struct {
245239
config Config
246240
chainconfig *params.ChainConfig
247241
chain blockChain
248-
gasPrice *big.Int
242+
gasTip atomic.Pointer[big.Int]
249243
txFeed event.Feed
250244
scope event.SubscriptionScope
251245
signer types.Signer
252246
mu sync.RWMutex
253247

254-
istanbul atomic.Bool // Fork indicator whether we are in the istanbul stage.
255-
eip2718 atomic.Bool // Fork indicator whether we are using EIP-2718 type transactions.
256-
eip1559 atomic.Bool // Fork indicator whether we are using EIP-1559 type transactions.
257-
shanghai atomic.Bool // Fork indicator whether we are in the Shanghai stage.
258-
259-
currentState *state.StateDB // Current state in the blockchain head
260-
pendingNonces *noncer // Pending state tracking virtual nonces
261-
currentMaxGas atomic.Uint64 // Current gas limit for transaction caps
248+
currentHead atomic.Pointer[types.Header] // Current head of the blockchain
249+
currentState *state.StateDB // Current state in the blockchain head
250+
pendingNonces *noncer // Pending state tracking virtual nonces
262251

263252
locals *accountSet // Set of local transaction to exempt from eviction rules
264253
journal *journal // Journal of local transaction to back up to disk
@@ -286,9 +275,9 @@ type txpoolResetRequest struct {
286275
oldHead, newHead *types.Header
287276
}
288277

289-
// NewTxPool creates a new transaction pool to gather, sort and filter inbound
278+
// New creates a new transaction pool to gather, sort and filter inbound
290279
// transactions from the network.
291-
func NewTxPool(config Config, chainconfig *params.ChainConfig, chain blockChain) *TxPool {
280+
func New(config Config, chainconfig *params.ChainConfig, chain blockChain) *TxPool {
292281
// Sanitize the input to ensure no vulnerable gas prices are set
293282
config = (&config).sanitize()
294283

@@ -309,8 +298,8 @@ func NewTxPool(config Config, chainconfig *params.ChainConfig, chain blockChain)
309298
reorgDoneCh: make(chan chan struct{}),
310299
reorgShutdownCh: make(chan struct{}),
311300
initDoneCh: make(chan struct{}),
312-
gasPrice: new(big.Int).SetUint64(config.PriceLimit),
313301
}
302+
pool.gasTip.Store(new(big.Int).SetUint64(config.PriceLimit))
314303
pool.locals = newAccountSet(pool.signer)
315304
for _, addr := range config.Locals {
316305
log.Info("Setting new local account", "address", addr)
@@ -443,33 +432,25 @@ func (pool *TxPool) SubscribeNewTxsEvent(ch chan<- core.NewTxsEvent) event.Subsc
443432
return pool.scope.Track(pool.txFeed.Subscribe(ch))
444433
}
445434

446-
// GasPrice returns the current gas price enforced by the transaction pool.
447-
func (pool *TxPool) GasPrice() *big.Int {
448-
pool.mu.RLock()
449-
defer pool.mu.RUnlock()
450-
451-
return new(big.Int).Set(pool.gasPrice)
452-
}
453-
454-
// SetGasPrice updates the minimum price required by the transaction pool for a
435+
// SetGasTip updates the minimum gas tip required by the transaction pool for a
455436
// new transaction, and drops all transactions below this threshold.
456-
func (pool *TxPool) SetGasPrice(price *big.Int) {
437+
func (pool *TxPool) SetGasTip(tip *big.Int) {
457438
pool.mu.Lock()
458439
defer pool.mu.Unlock()
459440

460-
old := pool.gasPrice
461-
pool.gasPrice = price
462-
// if the min miner fee increased, remove transactions below the new threshold
463-
if price.Cmp(old) > 0 {
441+
old := pool.gasTip.Load()
442+
pool.gasTip.Store(new(big.Int).Set(tip))
443+
444+
// If the min miner fee increased, remove transactions below the new threshold
445+
if tip.Cmp(old) > 0 {
464446
// pool.priced is sorted by GasFeeCap, so we have to iterate through pool.all instead
465-
drop := pool.all.RemotesBelowTip(price)
447+
drop := pool.all.RemotesBelowTip(tip)
466448
for _, tx := range drop {
467449
pool.removeTx(tx.Hash(), false)
468450
}
469451
pool.priced.Removed(len(drop))
470452
}
471-
472-
log.Info("Transaction pool price threshold updated", "price", price)
453+
log.Info("Transaction pool tip threshold updated", "tip", tip)
473454
}
474455

475456
// Nonce returns the next nonce of an account, with all transactions executable
@@ -556,7 +537,7 @@ func (pool *TxPool) Pending(enforceTips bool) map[common.Address]types.Transacti
556537
// If the miner requests tip enforcement, cap the lists now
557538
if enforceTips && !pool.locals.contains(addr) {
558539
for i, tx := range txs {
559-
if tx.EffectiveGasTipIntCmp(pool.gasPrice, pool.priced.urgent.baseFee) < 0 {
540+
if tx.EffectiveGasTipIntCmp(pool.gasTip.Load(), pool.priced.urgent.baseFee) < 0 {
560541
txs = txs[:i]
561542
break
562543
}
@@ -598,93 +579,48 @@ func (pool *TxPool) local() map[common.Address]types.Transactions {
598579
// This check is meant as an early check which only needs to be performed once,
599580
// and does not require the pool mutex to be held.
600581
func (pool *TxPool) validateTxBasics(tx *types.Transaction, local bool) error {
601-
// Accept only legacy transactions until EIP-2718/2930 activates.
602-
if !pool.eip2718.Load() && tx.Type() != types.LegacyTxType {
603-
return core.ErrTxTypeNotSupported
604-
}
605-
// Reject dynamic fee transactions until EIP-1559 activates.
606-
if !pool.eip1559.Load() && tx.Type() == types.DynamicFeeTxType {
607-
return core.ErrTxTypeNotSupported
608-
}
609-
// Reject blob transactions forever, those will have their own pool.
610-
if tx.Type() == types.BlobTxType {
611-
return core.ErrTxTypeNotSupported
612-
}
613-
// Reject transactions over defined size to prevent DOS attacks
614-
if tx.Size() > txMaxSize {
615-
return ErrOversizedData
616-
}
617-
// Check whether the init code size has been exceeded.
618-
if pool.shanghai.Load() && tx.To() == nil && len(tx.Data()) > params.MaxInitCodeSize {
619-
return fmt.Errorf("%w: code size %v limit %v", core.ErrMaxInitCodeSizeExceeded, len(tx.Data()), params.MaxInitCodeSize)
620-
}
621-
// Transactions can't be negative. This may never happen using RLP decoded
622-
// transactions but may occur if you create a transaction using the RPC.
623-
if tx.Value().Sign() < 0 {
624-
return ErrNegativeValue
625-
}
626-
// Ensure the transaction doesn't exceed the current block limit gas.
627-
if pool.currentMaxGas.Load() < tx.Gas() {
628-
return ErrGasLimit
629-
}
630-
// Sanity check for extremely large numbers
631-
if tx.GasFeeCap().BitLen() > 256 {
632-
return core.ErrFeeCapVeryHigh
633-
}
634-
if tx.GasTipCap().BitLen() > 256 {
635-
return core.ErrTipVeryHigh
582+
opts := &ValidationOptions{
583+
Config: pool.chainconfig,
584+
Accept: 0 |
585+
1<<types.LegacyTxType |
586+
1<<types.AccessListTxType |
587+
1<<types.DynamicFeeTxType,
588+
MaxSize: txMaxSize,
589+
MinTip: pool.gasTip.Load(),
636590
}
637-
// Ensure gasFeeCap is greater than or equal to gasTipCap.
638-
if tx.GasFeeCapIntCmp(tx.GasTipCap()) < 0 {
639-
return core.ErrTipAboveFeeCap
640-
}
641-
// Make sure the transaction is signed properly.
642-
if _, err := types.Sender(pool.signer, tx); err != nil {
643-
return ErrInvalidSender
644-
}
645-
// Drop non-local transactions under our own minimal accepted gas price or tip
646-
if !local && tx.GasTipCapIntCmp(pool.gasPrice) < 0 {
647-
return ErrUnderpriced
591+
if local {
592+
opts.MinTip = new(big.Int)
648593
}
649-
// Ensure the transaction has more gas than the basic tx fee.
650-
intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.To() == nil, true, pool.istanbul.Load(), pool.shanghai.Load())
651-
if err != nil {
594+
if err := ValidateTransaction(tx, nil, nil, nil, pool.currentHead.Load(), pool.signer, opts); err != nil {
652595
return err
653596
}
654-
if tx.Gas() < intrGas {
655-
return core.ErrIntrinsicGas
656-
}
657597
return nil
658598
}
659599

660600
// validateTx checks whether a transaction is valid according to the consensus
661601
// rules and adheres to some heuristic limits of the local node (price and size).
662602
func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error {
663-
// Signature has been checked already, this cannot error.
664-
from, _ := types.Sender(pool.signer, tx)
665-
// Ensure the transaction adheres to nonce ordering
666-
if pool.currentState.GetNonce(from) > tx.Nonce() {
667-
return core.ErrNonceTooLow
668-
}
669-
// Transactor should have enough funds to cover the costs
670-
// cost == V + GP * GL
671-
balance := pool.currentState.GetBalance(from)
672-
if balance.Cmp(tx.Cost()) < 0 {
673-
return core.ErrInsufficientFunds
674-
}
675-
676-
// Verify that replacing transactions will not result in overdraft
677-
list := pool.pending[from]
678-
if list != nil { // Sender already has pending txs
679-
sum := new(big.Int).Add(tx.Cost(), list.totalcost)
680-
if repl := list.txs.Get(tx.Nonce()); repl != nil {
681-
// Deduct the cost of a transaction replaced by this
682-
sum.Sub(sum, repl.Cost())
683-
}
684-
if balance.Cmp(sum) < 0 {
685-
log.Trace("Replacing transactions would overdraft", "sender", from, "balance", pool.currentState.GetBalance(from), "required", sum)
686-
return ErrOverdraft
687-
}
603+
opts := &ValidationOptionsWithState{
604+
State: pool.currentState,
605+
606+
FirstNonceGap: nil, // Pool allows arbitrary arrival order, don't invalidate nonce gaps
607+
ExistingExpenditure: func(addr common.Address) *big.Int {
608+
if list := pool.pending[addr]; list != nil {
609+
return list.totalcost
610+
}
611+
return new(big.Int)
612+
},
613+
ExistingCost: func(addr common.Address, nonce uint64) *big.Int {
614+
if list := pool.pending[addr]; list != nil {
615+
if tx := list.txs.Get(nonce); tx != nil {
616+
return tx.Cost()
617+
}
618+
}
619+
return nil
620+
},
621+
}
622+
if err := ValidateTransactionWithState(tx, pool.signer, opts); err != nil {
623+
return err
688624
}
689625
return nil
690626
}
@@ -995,7 +931,6 @@ func (pool *TxPool) addTxs(txs []*types.Transaction, local, sync bool) []error {
995931
// Exclude transactions with basic errors, e.g invalid signatures and
996932
// insufficient intrinsic gas as soon as possible and cache senders
997933
// in transactions before obtaining lock
998-
999934
if err := pool.validateTxBasics(tx, local); err != nil {
1000935
errs[i] = err
1001936
invalidTxMeter.Mark(1)
@@ -1385,21 +1320,14 @@ func (pool *TxPool) reset(oldHead, newHead *types.Header) {
13851320
log.Error("Failed to reset txpool state", "err", err)
13861321
return
13871322
}
1323+
pool.currentHead.Store(newHead)
13881324
pool.currentState = statedb
13891325
pool.pendingNonces = newNoncer(statedb)
1390-
pool.currentMaxGas.Store(newHead.GasLimit)
13911326

13921327
// Inject any transactions discarded due to reorgs
13931328
log.Debug("Reinjecting stale transactions", "count", len(reinject))
13941329
core.SenderCacher.Recover(pool.signer, reinject)
13951330
pool.addTxsLocked(reinject, false)
1396-
1397-
// Update all fork indicator by next pending block number.
1398-
next := new(big.Int).Add(newHead.Number, big.NewInt(1))
1399-
pool.istanbul.Store(pool.chainconfig.IsIstanbul(next))
1400-
pool.eip2718.Store(pool.chainconfig.IsBerlin(next))
1401-
pool.eip1559.Store(pool.chainconfig.IsLondon(next))
1402-
pool.shanghai.Store(pool.chainconfig.IsShanghai(next, uint64(time.Now().Unix())))
14031331
}
14041332

14051333
// promoteExecutables moves transactions that have become processable from the
@@ -1410,6 +1338,7 @@ func (pool *TxPool) promoteExecutables(accounts []common.Address) []*types.Trans
14101338
var promoted []*types.Transaction
14111339

14121340
// Iterate over all accounts and promote any executable transactions
1341+
gasLimit := pool.currentHead.Load().GasLimit
14131342
for _, addr := range accounts {
14141343
list := pool.queue[addr]
14151344
if list == nil {
@@ -1423,7 +1352,7 @@ func (pool *TxPool) promoteExecutables(accounts []common.Address) []*types.Trans
14231352
}
14241353
log.Trace("Removed old queued transactions", "count", len(forwards))
14251354
// Drop all transactions that are too costly (low balance or out of gas)
1426-
drops, _ := list.Filter(pool.currentState.GetBalance(addr), pool.currentMaxGas.Load())
1355+
drops, _ := list.Filter(pool.currentState.GetBalance(addr), gasLimit)
14271356
for _, tx := range drops {
14281357
hash := tx.Hash()
14291358
pool.all.Remove(hash)
@@ -1609,6 +1538,7 @@ func (pool *TxPool) truncateQueue() {
16091538
// to trigger a re-heap is this function
16101539
func (pool *TxPool) demoteUnexecutables() {
16111540
// Iterate over all accounts and demote any non-executable transactions
1541+
gasLimit := pool.currentHead.Load().GasLimit
16121542
for addr, list := range pool.pending {
16131543
nonce := pool.currentState.GetNonce(addr)
16141544

@@ -1620,7 +1550,7 @@ func (pool *TxPool) demoteUnexecutables() {
16201550
log.Trace("Removed old pending transaction", "hash", hash)
16211551
}
16221552
// Drop all transactions that are too costly (low balance or out of gas), and queue any invalids back for later
1623-
drops, invalids := list.Filter(pool.currentState.GetBalance(addr), pool.currentMaxGas.Load())
1553+
drops, invalids := list.Filter(pool.currentState.GetBalance(addr), gasLimit)
16241554
for _, tx := range drops {
16251555
hash := tx.Hash()
16261556
log.Trace("Removed unpayable pending transaction", "hash", hash)

core/txpool/txpool2_test.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func TestTransactionFutureAttack(t *testing.T) {
8383
config := testTxPoolConfig
8484
config.GlobalQueue = 100
8585
config.GlobalSlots = 100
86-
pool := NewTxPool(config, eip1559Config, blockchain)
86+
pool := New(config, eip1559Config, blockchain)
8787
defer pool.Stop()
8888
fillPool(t, pool)
8989
pending, _ := pool.Stats()
@@ -116,7 +116,7 @@ func TestTransactionFuture1559(t *testing.T) {
116116
// Create the pool to test the pricing enforcement with
117117
statedb, _ := state.New(types.EmptyRootHash, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
118118
blockchain := newTestBlockChain(1000000, statedb, new(event.Feed))
119-
pool := NewTxPool(testTxPoolConfig, eip1559Config, blockchain)
119+
pool := New(testTxPoolConfig, eip1559Config, blockchain)
120120
defer pool.Stop()
121121

122122
// Create a number of test accounts, fund them and make transactions
@@ -148,7 +148,7 @@ func TestTransactionZAttack(t *testing.T) {
148148
// Create the pool to test the pricing enforcement with
149149
statedb, _ := state.New(types.EmptyRootHash, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
150150
blockchain := newTestBlockChain(1000000, statedb, new(event.Feed))
151-
pool := NewTxPool(testTxPoolConfig, eip1559Config, blockchain)
151+
pool := New(testTxPoolConfig, eip1559Config, blockchain)
152152
defer pool.Stop()
153153
// Create a number of test accounts, fund them and make transactions
154154
fillPool(t, pool)
@@ -218,7 +218,7 @@ func BenchmarkFutureAttack(b *testing.B) {
218218
config := testTxPoolConfig
219219
config.GlobalQueue = 100
220220
config.GlobalSlots = 100
221-
pool := NewTxPool(config, eip1559Config, blockchain)
221+
pool := New(config, eip1559Config, blockchain)
222222
defer pool.Stop()
223223
fillPool(b, pool)
224224

0 commit comments

Comments
 (0)