Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core/txpool: implement additional DoS defenses #26648

Merged
merged 21 commits into from
Mar 10, 2023
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
34a5629
Update txpool.go
dwn1998 Dec 5, 2022
72243e0
core/txpool: add initial txpool redesign
MariusVanDerWijden Dec 6, 2022
7676a28
core/txpool: fix tests
MariusVanDerWijden Dec 6, 2022
050c712
core/txpool: move totalcost into sender list
MariusVanDerWijden Feb 3, 2023
892332e
core/txpool: give sender enough funds for benchmark
MariusVanDerWijden Feb 3, 2023
07a97dc
core/txpool: fix edge case on replacement txs
MariusVanDerWijden Feb 3, 2023
e2259e9
core/txpool: move overdraft check
MariusVanDerWijden Feb 8, 2023
ec012b9
core/txpool: calculate churn more correctly
MariusVanDerWijden Feb 8, 2023
e6328cd
core/txpool: happy lint
MariusVanDerWijden Feb 10, 2023
e5b2889
core/txpool: move checks, make checks less costly
MariusVanDerWijden Mar 10, 2023
7a52cb1
core/txpool: use len of index
MariusVanDerWijden Mar 10, 2023
73b6565
core/txpool: revert change
MariusVanDerWijden Mar 10, 2023
fa66e33
core/txpool: apply changes from code review
MariusVanDerWijden Mar 10, 2023
0dda16c
core/txpool: apply changes from code review
MariusVanDerWijden Mar 10, 2023
e49d6f7
core/txpool: fixup
MariusVanDerWijden Mar 10, 2023
523ae86
Update core/txpool/txpool.go
MariusVanDerWijden Mar 10, 2023
0f7423a
core/txpool: doc
MariusVanDerWijden Mar 10, 2023
3612a8e
core/txpool: don't take stale txs into account
MariusVanDerWijden Mar 10, 2023
3307da8
core/txpool: happy lint
MariusVanDerWijden Mar 10, 2023
d0eecca
core/txpool: license
MariusVanDerWijden Mar 10, 2023
d1de0bf
core/txpool: add break
MariusVanDerWijden Mar 10, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 38 additions & 9 deletions core/txpool/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,17 +254,19 @@ type list struct {
strict bool // Whether nonces are strictly continuous or not
txs *sortedMap // Heap indexed sorted hash map of the transactions

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

// newList create a new transaction list for maintaining nonce-indexable fast,
// gapped, sortable transaction lists.
func newList(strict bool) *list {
return &list{
strict: strict,
txs: newSortedMap(),
costcap: new(big.Int),
strict: strict,
txs: newSortedMap(),
costcap: new(big.Int),
totalcost: new(big.Int),
}
}

Expand Down Expand Up @@ -302,7 +304,11 @@ func (l *list) Add(tx *types.Transaction, priceBump uint64) (bool, *types.Transa
if tx.GasFeeCapIntCmp(thresholdFeeCap) < 0 || tx.GasTipCapIntCmp(thresholdTip) < 0 {
return false, nil
}
// Old is being replaced, subtract old cost
l.subTotalCost([]*types.Transaction{old})
}
// Add new tx cost to totalcost
l.totalcost.Add(l.totalcost, tx.Cost())
// Otherwise overwrite the old transaction with the current one
l.txs.Put(tx)
if cost := tx.Cost(); l.costcap.Cmp(cost) < 0 {
Expand All @@ -318,7 +324,9 @@ func (l *list) Add(tx *types.Transaction, priceBump uint64) (bool, *types.Transa
// provided threshold. Every removed transaction is returned for any post-removal
// maintenance.
func (l *list) Forward(threshold uint64) types.Transactions {
return l.txs.Forward(threshold)
txs := l.txs.Forward(threshold)
l.subTotalCost(txs)
return txs
}

// Filter removes all transactions from the list with a cost or gas limit higher
Expand Down Expand Up @@ -357,14 +365,19 @@ func (l *list) Filter(costLimit *big.Int, gasLimit uint64) (types.Transactions,
}
invalids = l.txs.filter(func(tx *types.Transaction) bool { return tx.Nonce() > lowest })
}
// Reset total cost
l.subTotalCost(removed)
l.subTotalCost(invalids)
l.txs.reheap()
return removed, invalids
}

// Cap places a hard limit on the number of items, returning all transactions
// exceeding that limit.
func (l *list) Cap(threshold int) types.Transactions {
return l.txs.Cap(threshold)
txs := l.txs.Cap(threshold)
l.subTotalCost(txs)
return txs
}

// Remove deletes a transaction from the maintained list, returning whether the
Expand All @@ -376,9 +389,12 @@ func (l *list) Remove(tx *types.Transaction) (bool, types.Transactions) {
if removed := l.txs.Remove(nonce); !removed {
return false, nil
}
l.subTotalCost([]*types.Transaction{tx})
// In strict mode, filter out non-executable transactions
if l.strict {
return true, l.txs.Filter(func(tx *types.Transaction) bool { return tx.Nonce() > nonce })
txs := l.txs.Filter(func(tx *types.Transaction) bool { return tx.Nonce() > nonce })
l.subTotalCost(txs)
return true, txs
}
return true, nil
}
Expand All @@ -391,7 +407,9 @@ func (l *list) Remove(tx *types.Transaction) (bool, types.Transactions) {
// prevent getting into and invalid state. This is not something that should ever
// happen but better to be self correcting than failing!
func (l *list) Ready(start uint64) types.Transactions {
return l.txs.Ready(start)
txs := l.txs.Ready(start)
l.subTotalCost(txs)
return txs
}

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

// subTotalCost subtracts the cost of the given transactions from the
// total cost of all transactions.
func (l *list) subTotalCost(txs []*types.Transaction) {
for _, tx := range txs {
l.totalcost.Sub(l.totalcost, tx.Cost())
}
}

// priceHeap is a heap.Interface implementation over transactions for retrieving
// price-sorted transactions to discard when the pool fills up. If baseFee is set
// then the heap is sorted based on the effective tip based on the given base fee.
Expand Down Expand Up @@ -561,6 +587,7 @@ func (l *pricedList) underpricedFor(h *priceHeap, tx *types.Transaction) bool {

// Discard finds a number of most underpriced transactions, removes them from the
// priced list and returns them for further removal from the entire pool.
// If noPending is set to true, we will only consider the floating list
//
// Note local transaction won't be considered for eviction.
func (l *pricedList) Discard(slots int, force bool) (types.Transactions, bool) {
Expand All @@ -571,6 +598,7 @@ func (l *pricedList) Discard(slots int, force bool) (types.Transactions, bool) {
tx := heap.Pop(&l.urgent).(*types.Transaction)
if l.all.GetRemote(tx.Hash()) == nil { // Removed or migrated
atomic.AddInt64(&l.stales, -1)
slots -= numSlots(tx)
MariusVanDerWijden marked this conversation as resolved.
Show resolved Hide resolved
continue
}
// Non stale transaction found, move to floating heap
Expand All @@ -584,6 +612,7 @@ func (l *pricedList) Discard(slots int, force bool) (types.Transactions, bool) {
tx := heap.Pop(&l.floating).(*types.Transaction)
if l.all.GetRemote(tx.Hash()) == nil { // Removed or migrated
atomic.AddInt64(&l.stales, -1)
slots -= numSlots(tx)
MariusVanDerWijden marked this conversation as resolved.
Show resolved Hide resolved
continue
}
// Non stale transaction found, discard it
Expand Down
81 changes: 73 additions & 8 deletions core/txpool/txpool.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package txpool

import (
"container/heap"
"errors"
"fmt"
"math"
Expand Down Expand Up @@ -87,6 +88,14 @@ var (
// than some meaningful limit a user might use. This is not a consensus error
// making the transaction invalid, rather a DOS protection.
ErrOversizedData = errors.New("oversized data")

// ErrFutureReplacePending is returned if a future transaction replaces a pending
// transaction. Future transactions should only be able to replace other future transactions.
ErrFutureReplacePending = errors.New("future transaction tries to replace pending")

// ErrOverdraft is returned if a transaction would cause the senders balance to go negative
// thus invalidating a potential large number of transactions.
ErrOverdraft = errors.New("transaction would cause overdraft")
)

var (
Expand Down Expand Up @@ -639,9 +648,25 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error {
}
// Transactor should have enough funds to cover the costs
// cost == V + GP * GL
if pool.currentState.GetBalance(from).Cmp(tx.Cost()) < 0 {
balance := pool.currentState.GetBalance(from)
if balance.Cmp(tx.Cost()) < 0 {
return core.ErrInsufficientFunds
}

// Verify that replacing transactions will not result in overdraft
list := pool.pending[from]
if list != nil { // Sender already has pending txs
sum := new(big.Int).Add(tx.Cost(), list.totalcost)
if repl := list.txs.Get(tx.Nonce()); repl != nil {
// Deduct the cost of a transaction replaced by this
sum.Sub(sum, repl.Cost())
}
if balance.Cmp(sum) < 0 {
log.Trace("Replacing transactions would overdraft", "sender", from, "balance", pool.currentState.GetBalance(from), "required", sum)
return ErrOverdraft
}
}

// Ensure the transaction has more gas than the basic tx fee.
intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.To() == nil, true, pool.istanbul, pool.shanghai)
if err != nil {
Expand Down Expand Up @@ -678,6 +703,10 @@ func (pool *TxPool) add(tx *types.Transaction, local bool) (replaced bool, err e
invalidTxMeter.Mark(1)
return false, err
}

// already validated by this point
from, _ := types.Sender(pool.signer, tx)

// If the transaction pool is full, discard underpriced transactions
if uint64(pool.all.Slots()+numSlots(tx)) > pool.config.GlobalSlots+pool.config.GlobalQueue {
// If the new transaction is underpriced, don't accept it
Expand All @@ -686,6 +715,7 @@ func (pool *TxPool) add(tx *types.Transaction, local bool) (replaced bool, err e
underpricedTxMeter.Mark(1)
return false, ErrUnderpriced
}

// We're about to replace a transaction. The reorg does a more thorough
// analysis of what to remove and how, but it runs async. We don't want to
// do too many replacements between reorg-runs, so we cap the number of
Expand All @@ -706,17 +736,36 @@ func (pool *TxPool) add(tx *types.Transaction, local bool) (replaced bool, err e
overflowedTxMeter.Mark(1)
return false, ErrTxPoolOverflow
}
// Bump the counter of rejections-since-reorg
pool.changesSinceReorg += len(drop)

// If the new transaction is a future transaction it should never churn pending transactions
if pool.isFuture(from, tx) {
var replacesPending bool
for _, dropTx := range drop {
dropSender, _ := types.Sender(pool.signer, dropTx)
if list := pool.pending[dropSender]; list != nil && list.Overlaps(dropTx) {
replacesPending = true
}
}
// Add all transactions back to the priced queue
if replacesPending {
for _, dropTx := range drop {
heap.Push(&pool.priced.urgent, dropTx)
}
log.Trace("Discarding future transaction replacing pending tx", "hash", hash)
return false, ErrFutureReplacePending
}
}

// Kick out the underpriced remote transactions.
for _, tx := range drop {
log.Trace("Discarding freshly underpriced transaction", "hash", tx.Hash(), "gasTipCap", tx.GasTipCap(), "gasFeeCap", tx.GasFeeCap())
underpricedTxMeter.Mark(1)
pool.removeTx(tx.Hash(), false)
dropped := pool.removeTx(tx.Hash(), false)
pool.changesSinceReorg += dropped
MariusVanDerWijden marked this conversation as resolved.
Show resolved Hide resolved
}
}

// Try to replace an existing transaction in the pending pool
from, _ := types.Sender(pool.signer, tx) // already validated
if list := pool.pending[from]; list != nil && list.Overlaps(tx) {
// Nonce already pending, check if required price bump is met
inserted, old := list.Add(tx, pool.config.PriceBump)
Expand Down Expand Up @@ -760,6 +809,20 @@ func (pool *TxPool) add(tx *types.Transaction, local bool) (replaced bool, err e
return replaced, nil
}

// isFuture reports whether the given transaction is immediately executable.
func (pool *TxPool) isFuture(from common.Address, tx *types.Transaction) bool {
list := pool.pending[from]
if list == nil {
return pool.pendingNonces.get(from) != tx.Nonce()
}
// Sender has pending transations.
if old := list.txs.Get(tx.Nonce()); old != nil {
return false // It replaces a pending transaction.
}
// Not replacing, check if parent nonce exists in pending.
return list.txs.Get(tx.Nonce()-1) == nil
}

// enqueueTx inserts a new transaction into the non-executable transaction queue.
//
// Note, this method assumes the pool lock is held!
Expand Down Expand Up @@ -996,11 +1059,12 @@ func (pool *TxPool) Has(hash common.Hash) bool {

// removeTx removes a single transaction from the queue, moving all subsequent
// transactions back to the future queue.
func (pool *TxPool) removeTx(hash common.Hash, outofbound bool) {
// Returns the number of transactions removed from the pending queue.
func (pool *TxPool) removeTx(hash common.Hash, outofbound bool) int {
// Fetch the transaction we wish to delete
tx := pool.all.Get(hash)
if tx == nil {
return
return 0
}
addr, _ := types.Sender(pool.signer, tx) // already validated during insertion

Expand Down Expand Up @@ -1028,7 +1092,7 @@ func (pool *TxPool) removeTx(hash common.Hash, outofbound bool) {
pool.pendingNonces.setIfLower(addr, tx.Nonce())
// Reduce the pending counter
pendingGauge.Dec(int64(1 + len(invalids)))
return
return 1 + len(invalids)
}
}
// Transaction is in the future queue
Expand All @@ -1042,6 +1106,7 @@ func (pool *TxPool) removeTx(hash common.Hash, outofbound bool) {
delete(pool.beats, addr)
}
}
return 0
}

// requestReset requests a pool reset to the new head block.
Expand Down
Loading