Skip to content

Commit 6cf2e92

Browse files
MariusVanDerWijdendwn1998holiman
authored
core/txpool: implement additional DoS defenses (#26648)
This adds two new rules to the transaction pool: - A future transaction can not evict a pending transaction. - A transaction can not overspend available funds of a sender. --- Co-authored-by: dwn1998 <42262393+dwn1998@users.noreply.github.com> Co-authored-by: Martin Holst Swende <martin@swende.se>
1 parent 564db9a commit 6cf2e92

File tree

4 files changed

+341
-25
lines changed

4 files changed

+341
-25
lines changed

core/txpool/list.go

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -254,17 +254,19 @@ type list struct {
254254
strict bool // Whether nonces are strictly continuous or not
255255
txs *sortedMap // Heap indexed sorted hash map of the transactions
256256

257-
costcap *big.Int // Price of the highest costing transaction (reset only if exceeds balance)
258-
gascap uint64 // Gas limit of the highest spending transaction (reset only if exceeds block limit)
257+
costcap *big.Int // Price of the highest costing transaction (reset only if exceeds balance)
258+
gascap uint64 // Gas limit of the highest spending transaction (reset only if exceeds block limit)
259+
totalcost *big.Int // Total cost of all transactions in the list
259260
}
260261

261262
// newList create a new transaction list for maintaining nonce-indexable fast,
262263
// gapped, sortable transaction lists.
263264
func newList(strict bool) *list {
264265
return &list{
265-
strict: strict,
266-
txs: newSortedMap(),
267-
costcap: new(big.Int),
266+
strict: strict,
267+
txs: newSortedMap(),
268+
costcap: new(big.Int),
269+
totalcost: new(big.Int),
268270
}
269271
}
270272

@@ -302,7 +304,11 @@ func (l *list) Add(tx *types.Transaction, priceBump uint64) (bool, *types.Transa
302304
if tx.GasFeeCapIntCmp(thresholdFeeCap) < 0 || tx.GasTipCapIntCmp(thresholdTip) < 0 {
303305
return false, nil
304306
}
307+
// Old is being replaced, subtract old cost
308+
l.subTotalCost([]*types.Transaction{old})
305309
}
310+
// Add new tx cost to totalcost
311+
l.totalcost.Add(l.totalcost, tx.Cost())
306312
// Otherwise overwrite the old transaction with the current one
307313
l.txs.Put(tx)
308314
if cost := tx.Cost(); l.costcap.Cmp(cost) < 0 {
@@ -318,7 +324,9 @@ func (l *list) Add(tx *types.Transaction, priceBump uint64) (bool, *types.Transa
318324
// provided threshold. Every removed transaction is returned for any post-removal
319325
// maintenance.
320326
func (l *list) Forward(threshold uint64) types.Transactions {
321-
return l.txs.Forward(threshold)
327+
txs := l.txs.Forward(threshold)
328+
l.subTotalCost(txs)
329+
return txs
322330
}
323331

324332
// Filter removes all transactions from the list with a cost or gas limit higher
@@ -357,14 +365,19 @@ func (l *list) Filter(costLimit *big.Int, gasLimit uint64) (types.Transactions,
357365
}
358366
invalids = l.txs.filter(func(tx *types.Transaction) bool { return tx.Nonce() > lowest })
359367
}
368+
// Reset total cost
369+
l.subTotalCost(removed)
370+
l.subTotalCost(invalids)
360371
l.txs.reheap()
361372
return removed, invalids
362373
}
363374

364375
// Cap places a hard limit on the number of items, returning all transactions
365376
// exceeding that limit.
366377
func (l *list) Cap(threshold int) types.Transactions {
367-
return l.txs.Cap(threshold)
378+
txs := l.txs.Cap(threshold)
379+
l.subTotalCost(txs)
380+
return txs
368381
}
369382

370383
// Remove deletes a transaction from the maintained list, returning whether the
@@ -376,9 +389,12 @@ func (l *list) Remove(tx *types.Transaction) (bool, types.Transactions) {
376389
if removed := l.txs.Remove(nonce); !removed {
377390
return false, nil
378391
}
392+
l.subTotalCost([]*types.Transaction{tx})
379393
// In strict mode, filter out non-executable transactions
380394
if l.strict {
381-
return true, l.txs.Filter(func(tx *types.Transaction) bool { return tx.Nonce() > nonce })
395+
txs := l.txs.Filter(func(tx *types.Transaction) bool { return tx.Nonce() > nonce })
396+
l.subTotalCost(txs)
397+
return true, txs
382398
}
383399
return true, nil
384400
}
@@ -391,7 +407,9 @@ func (l *list) Remove(tx *types.Transaction) (bool, types.Transactions) {
391407
// prevent getting into and invalid state. This is not something that should ever
392408
// happen but better to be self correcting than failing!
393409
func (l *list) Ready(start uint64) types.Transactions {
394-
return l.txs.Ready(start)
410+
txs := l.txs.Ready(start)
411+
l.subTotalCost(txs)
412+
return txs
395413
}
396414

397415
// Len returns the length of the transaction list.
@@ -417,6 +435,14 @@ func (l *list) LastElement() *types.Transaction {
417435
return l.txs.LastElement()
418436
}
419437

438+
// subTotalCost subtracts the cost of the given transactions from the
439+
// total cost of all transactions.
440+
func (l *list) subTotalCost(txs []*types.Transaction) {
441+
for _, tx := range txs {
442+
l.totalcost.Sub(l.totalcost, tx.Cost())
443+
}
444+
}
445+
420446
// priceHeap is a heap.Interface implementation over transactions for retrieving
421447
// price-sorted transactions to discard when the pool fills up. If baseFee is set
422448
// then the heap is sorted based on the effective tip based on the given base fee.
@@ -561,6 +587,7 @@ func (l *pricedList) underpricedFor(h *priceHeap, tx *types.Transaction) bool {
561587

562588
// Discard finds a number of most underpriced transactions, removes them from the
563589
// priced list and returns them for further removal from the entire pool.
590+
// If noPending is set to true, we will only consider the floating list
564591
//
565592
// Note local transaction won't be considered for eviction.
566593
func (l *pricedList) Discard(slots int, force bool) (types.Transactions, bool) {

core/txpool/txpool.go

Lines changed: 74 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package txpool
1818

1919
import (
20+
"container/heap"
2021
"errors"
2122
"fmt"
2223
"math"
@@ -87,6 +88,14 @@ var (
8788
// than some meaningful limit a user might use. This is not a consensus error
8889
// making the transaction invalid, rather a DOS protection.
8990
ErrOversizedData = errors.New("oversized data")
91+
92+
// ErrFutureReplacePending is returned if a future transaction replaces a pending
93+
// transaction. Future transactions should only be able to replace other future transactions.
94+
ErrFutureReplacePending = errors.New("future transaction tries to replace pending")
95+
96+
// ErrOverdraft is returned if a transaction would cause the senders balance to go negative
97+
// thus invalidating a potential large number of transactions.
98+
ErrOverdraft = errors.New("transaction would cause overdraft")
9099
)
91100

92101
var (
@@ -639,9 +648,25 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error {
639648
}
640649
// Transactor should have enough funds to cover the costs
641650
// cost == V + GP * GL
642-
if pool.currentState.GetBalance(from).Cmp(tx.Cost()) < 0 {
651+
balance := pool.currentState.GetBalance(from)
652+
if balance.Cmp(tx.Cost()) < 0 {
643653
return core.ErrInsufficientFunds
644654
}
655+
656+
// Verify that replacing transactions will not result in overdraft
657+
list := pool.pending[from]
658+
if list != nil { // Sender already has pending txs
659+
sum := new(big.Int).Add(tx.Cost(), list.totalcost)
660+
if repl := list.txs.Get(tx.Nonce()); repl != nil {
661+
// Deduct the cost of a transaction replaced by this
662+
sum.Sub(sum, repl.Cost())
663+
}
664+
if balance.Cmp(sum) < 0 {
665+
log.Trace("Replacing transactions would overdraft", "sender", from, "balance", pool.currentState.GetBalance(from), "required", sum)
666+
return ErrOverdraft
667+
}
668+
}
669+
645670
// Ensure the transaction has more gas than the basic tx fee.
646671
intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.To() == nil, true, pool.istanbul, pool.shanghai)
647672
if err != nil {
@@ -678,6 +703,10 @@ func (pool *TxPool) add(tx *types.Transaction, local bool) (replaced bool, err e
678703
invalidTxMeter.Mark(1)
679704
return false, err
680705
}
706+
707+
// already validated by this point
708+
from, _ := types.Sender(pool.signer, tx)
709+
681710
// If the transaction pool is full, discard underpriced transactions
682711
if uint64(pool.all.Slots()+numSlots(tx)) > pool.config.GlobalSlots+pool.config.GlobalQueue {
683712
// If the new transaction is underpriced, don't accept it
@@ -686,6 +715,7 @@ func (pool *TxPool) add(tx *types.Transaction, local bool) (replaced bool, err e
686715
underpricedTxMeter.Mark(1)
687716
return false, ErrUnderpriced
688717
}
718+
689719
// We're about to replace a transaction. The reorg does a more thorough
690720
// analysis of what to remove and how, but it runs async. We don't want to
691721
// do too many replacements between reorg-runs, so we cap the number of
@@ -706,17 +736,37 @@ func (pool *TxPool) add(tx *types.Transaction, local bool) (replaced bool, err e
706736
overflowedTxMeter.Mark(1)
707737
return false, ErrTxPoolOverflow
708738
}
709-
// Bump the counter of rejections-since-reorg
710-
pool.changesSinceReorg += len(drop)
739+
740+
// If the new transaction is a future transaction it should never churn pending transactions
741+
if pool.isFuture(from, tx) {
742+
var replacesPending bool
743+
for _, dropTx := range drop {
744+
dropSender, _ := types.Sender(pool.signer, dropTx)
745+
if list := pool.pending[dropSender]; list != nil && list.Overlaps(dropTx) {
746+
replacesPending = true
747+
break
748+
}
749+
}
750+
// Add all transactions back to the priced queue
751+
if replacesPending {
752+
for _, dropTx := range drop {
753+
heap.Push(&pool.priced.urgent, dropTx)
754+
}
755+
log.Trace("Discarding future transaction replacing pending tx", "hash", hash)
756+
return false, ErrFutureReplacePending
757+
}
758+
}
759+
711760
// Kick out the underpriced remote transactions.
712761
for _, tx := range drop {
713762
log.Trace("Discarding freshly underpriced transaction", "hash", tx.Hash(), "gasTipCap", tx.GasTipCap(), "gasFeeCap", tx.GasFeeCap())
714763
underpricedTxMeter.Mark(1)
715-
pool.removeTx(tx.Hash(), false)
764+
dropped := pool.removeTx(tx.Hash(), false)
765+
pool.changesSinceReorg += dropped
716766
}
717767
}
768+
718769
// Try to replace an existing transaction in the pending pool
719-
from, _ := types.Sender(pool.signer, tx) // already validated
720770
if list := pool.pending[from]; list != nil && list.Overlaps(tx) {
721771
// Nonce already pending, check if required price bump is met
722772
inserted, old := list.Add(tx, pool.config.PriceBump)
@@ -760,6 +810,20 @@ func (pool *TxPool) add(tx *types.Transaction, local bool) (replaced bool, err e
760810
return replaced, nil
761811
}
762812

813+
// isFuture reports whether the given transaction is immediately executable.
814+
func (pool *TxPool) isFuture(from common.Address, tx *types.Transaction) bool {
815+
list := pool.pending[from]
816+
if list == nil {
817+
return pool.pendingNonces.get(from) != tx.Nonce()
818+
}
819+
// Sender has pending transactions.
820+
if old := list.txs.Get(tx.Nonce()); old != nil {
821+
return false // It replaces a pending transaction.
822+
}
823+
// Not replacing, check if parent nonce exists in pending.
824+
return list.txs.Get(tx.Nonce()-1) == nil
825+
}
826+
763827
// enqueueTx inserts a new transaction into the non-executable transaction queue.
764828
//
765829
// Note, this method assumes the pool lock is held!
@@ -996,11 +1060,12 @@ func (pool *TxPool) Has(hash common.Hash) bool {
9961060

9971061
// removeTx removes a single transaction from the queue, moving all subsequent
9981062
// transactions back to the future queue.
999-
func (pool *TxPool) removeTx(hash common.Hash, outofbound bool) {
1063+
// Returns the number of transactions removed from the pending queue.
1064+
func (pool *TxPool) removeTx(hash common.Hash, outofbound bool) int {
10001065
// Fetch the transaction we wish to delete
10011066
tx := pool.all.Get(hash)
10021067
if tx == nil {
1003-
return
1068+
return 0
10041069
}
10051070
addr, _ := types.Sender(pool.signer, tx) // already validated during insertion
10061071

@@ -1028,7 +1093,7 @@ func (pool *TxPool) removeTx(hash common.Hash, outofbound bool) {
10281093
pool.pendingNonces.setIfLower(addr, tx.Nonce())
10291094
// Reduce the pending counter
10301095
pendingGauge.Dec(int64(1 + len(invalids)))
1031-
return
1096+
return 1 + len(invalids)
10321097
}
10331098
}
10341099
// Transaction is in the future queue
@@ -1042,6 +1107,7 @@ func (pool *TxPool) removeTx(hash common.Hash, outofbound bool) {
10421107
delete(pool.beats, addr)
10431108
}
10441109
}
1110+
return 0
10451111
}
10461112

10471113
// requestReset requests a pool reset to the new head block.

0 commit comments

Comments
 (0)