forked from cerc-io/plugeth
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>
This commit is contained in:
co-authored by
dwn1998
Martin Holst Swende
parent
564db9a95f
commit
6cf2e921a7
+74
-8
@@ -17,6 +17,7 @@
|
||||
package txpool
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
@@ -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 (
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -706,17 +736,37 @@ 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
|
||||
break
|
||||
}
|
||||
}
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
@@ -760,6 +810,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 transactions.
|
||||
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!
|
||||
@@ -996,11 +1060,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
|
||||
|
||||
@@ -1028,7 +1093,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
|
||||
@@ -1042,6 +1107,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.
|
||||
|
||||
Reference in New Issue
Block a user