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:
parent
564db9a95f
commit
6cf2e921a7
@ -254,17 +254,19 @@ type list struct {
|
|||||||
strict bool // Whether nonces are strictly continuous or not
|
strict bool // Whether nonces are strictly continuous or not
|
||||||
txs *sortedMap // Heap indexed sorted hash map of the transactions
|
txs *sortedMap // Heap indexed sorted hash map of the transactions
|
||||||
|
|
||||||
costcap *big.Int // Price of the highest costing transaction (reset only if exceeds balance)
|
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)
|
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,
|
// newList create a new transaction list for maintaining nonce-indexable fast,
|
||||||
// gapped, sortable transaction lists.
|
// gapped, sortable transaction lists.
|
||||||
func newList(strict bool) *list {
|
func newList(strict bool) *list {
|
||||||
return &list{
|
return &list{
|
||||||
strict: strict,
|
strict: strict,
|
||||||
txs: newSortedMap(),
|
txs: newSortedMap(),
|
||||||
costcap: new(big.Int),
|
costcap: new(big.Int),
|
||||||
|
totalcost: new(big.Int),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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 {
|
if tx.GasFeeCapIntCmp(thresholdFeeCap) < 0 || tx.GasTipCapIntCmp(thresholdTip) < 0 {
|
||||||
return false, nil
|
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
|
// Otherwise overwrite the old transaction with the current one
|
||||||
l.txs.Put(tx)
|
l.txs.Put(tx)
|
||||||
if cost := tx.Cost(); l.costcap.Cmp(cost) < 0 {
|
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
|
|||||||
// provided threshold. Every removed transaction is returned for any post-removal
|
// provided threshold. Every removed transaction is returned for any post-removal
|
||||||
// maintenance.
|
// maintenance.
|
||||||
func (l *list) Forward(threshold uint64) types.Transactions {
|
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
|
// Filter removes all transactions from the list with a cost or gas limit higher
|
||||||
@ -357,6 +365,9 @@ func (l *list) Filter(costLimit *big.Int, gasLimit uint64) (types.Transactions,
|
|||||||
}
|
}
|
||||||
invalids = l.txs.filter(func(tx *types.Transaction) bool { return tx.Nonce() > lowest })
|
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()
|
l.txs.reheap()
|
||||||
return removed, invalids
|
return removed, invalids
|
||||||
}
|
}
|
||||||
@ -364,7 +375,9 @@ func (l *list) Filter(costLimit *big.Int, gasLimit uint64) (types.Transactions,
|
|||||||
// Cap places a hard limit on the number of items, returning all transactions
|
// Cap places a hard limit on the number of items, returning all transactions
|
||||||
// exceeding that limit.
|
// exceeding that limit.
|
||||||
func (l *list) Cap(threshold int) types.Transactions {
|
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
|
// 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) {
|
|||||||
if removed := l.txs.Remove(nonce); !removed {
|
if removed := l.txs.Remove(nonce); !removed {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
l.subTotalCost([]*types.Transaction{tx})
|
||||||
// In strict mode, filter out non-executable transactions
|
// In strict mode, filter out non-executable transactions
|
||||||
if l.strict {
|
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
|
return true, nil
|
||||||
}
|
}
|
||||||
@ -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
|
// prevent getting into and invalid state. This is not something that should ever
|
||||||
// happen but better to be self correcting than failing!
|
// happen but better to be self correcting than failing!
|
||||||
func (l *list) Ready(start uint64) types.Transactions {
|
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.
|
// Len returns the length of the transaction list.
|
||||||
@ -417,6 +435,14 @@ func (l *list) LastElement() *types.Transaction {
|
|||||||
return l.txs.LastElement()
|
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
|
// priceHeap is a heap.Interface implementation over transactions for retrieving
|
||||||
// price-sorted transactions to discard when the pool fills up. If baseFee is set
|
// 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.
|
// 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 {
|
|||||||
|
|
||||||
// Discard finds a number of most underpriced transactions, removes them from the
|
// Discard finds a number of most underpriced transactions, removes them from the
|
||||||
// priced list and returns them for further removal from the entire pool.
|
// 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.
|
// Note local transaction won't be considered for eviction.
|
||||||
func (l *pricedList) Discard(slots int, force bool) (types.Transactions, bool) {
|
func (l *pricedList) Discard(slots int, force bool) (types.Transactions, bool) {
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
package txpool
|
package txpool
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"container/heap"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
@ -87,6 +88,14 @@ var (
|
|||||||
// than some meaningful limit a user might use. This is not a consensus error
|
// than some meaningful limit a user might use. This is not a consensus error
|
||||||
// making the transaction invalid, rather a DOS protection.
|
// making the transaction invalid, rather a DOS protection.
|
||||||
ErrOversizedData = errors.New("oversized data")
|
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 (
|
var (
|
||||||
@ -639,9 +648,25 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error {
|
|||||||
}
|
}
|
||||||
// Transactor should have enough funds to cover the costs
|
// Transactor should have enough funds to cover the costs
|
||||||
// cost == V + GP * GL
|
// 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
|
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.
|
// 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)
|
intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.To() == nil, true, pool.istanbul, pool.shanghai)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -678,6 +703,10 @@ func (pool *TxPool) add(tx *types.Transaction, local bool) (replaced bool, err e
|
|||||||
invalidTxMeter.Mark(1)
|
invalidTxMeter.Mark(1)
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// already validated by this point
|
||||||
|
from, _ := types.Sender(pool.signer, tx)
|
||||||
|
|
||||||
// If the transaction pool is full, discard underpriced transactions
|
// If the transaction pool is full, discard underpriced transactions
|
||||||
if uint64(pool.all.Slots()+numSlots(tx)) > pool.config.GlobalSlots+pool.config.GlobalQueue {
|
if uint64(pool.all.Slots()+numSlots(tx)) > pool.config.GlobalSlots+pool.config.GlobalQueue {
|
||||||
// If the new transaction is underpriced, don't accept it
|
// 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)
|
underpricedTxMeter.Mark(1)
|
||||||
return false, ErrUnderpriced
|
return false, ErrUnderpriced
|
||||||
}
|
}
|
||||||
|
|
||||||
// We're about to replace a transaction. The reorg does a more thorough
|
// 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
|
// 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
|
// 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)
|
overflowedTxMeter.Mark(1)
|
||||||
return false, ErrTxPoolOverflow
|
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.
|
// Kick out the underpriced remote transactions.
|
||||||
for _, tx := range drop {
|
for _, tx := range drop {
|
||||||
log.Trace("Discarding freshly underpriced transaction", "hash", tx.Hash(), "gasTipCap", tx.GasTipCap(), "gasFeeCap", tx.GasFeeCap())
|
log.Trace("Discarding freshly underpriced transaction", "hash", tx.Hash(), "gasTipCap", tx.GasTipCap(), "gasFeeCap", tx.GasFeeCap())
|
||||||
underpricedTxMeter.Mark(1)
|
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
|
// 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) {
|
if list := pool.pending[from]; list != nil && list.Overlaps(tx) {
|
||||||
// Nonce already pending, check if required price bump is met
|
// Nonce already pending, check if required price bump is met
|
||||||
inserted, old := list.Add(tx, pool.config.PriceBump)
|
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
|
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.
|
// enqueueTx inserts a new transaction into the non-executable transaction queue.
|
||||||
//
|
//
|
||||||
// Note, this method assumes the pool lock is held!
|
// 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
|
// removeTx removes a single transaction from the queue, moving all subsequent
|
||||||
// transactions back to the future queue.
|
// 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
|
// Fetch the transaction we wish to delete
|
||||||
tx := pool.all.Get(hash)
|
tx := pool.all.Get(hash)
|
||||||
if tx == nil {
|
if tx == nil {
|
||||||
return
|
return 0
|
||||||
}
|
}
|
||||||
addr, _ := types.Sender(pool.signer, tx) // already validated during insertion
|
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())
|
pool.pendingNonces.setIfLower(addr, tx.Nonce())
|
||||||
// Reduce the pending counter
|
// Reduce the pending counter
|
||||||
pendingGauge.Dec(int64(1 + len(invalids)))
|
pendingGauge.Dec(int64(1 + len(invalids)))
|
||||||
return
|
return 1 + len(invalids)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Transaction is in the future queue
|
// Transaction is in the future queue
|
||||||
@ -1042,6 +1107,7 @@ func (pool *TxPool) removeTx(hash common.Hash, outofbound bool) {
|
|||||||
delete(pool.beats, addr)
|
delete(pool.beats, addr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// requestReset requests a pool reset to the new head block.
|
// requestReset requests a pool reset to the new head block.
|
||||||
|
212
core/txpool/txpool2_test.go
Normal file
212
core/txpool/txpool2_test.go
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
// Copyright 2023 The go-ethereum Authors
|
||||||
|
// This file is part of the go-ethereum library.
|
||||||
|
//
|
||||||
|
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Lesser General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Lesser General Public License
|
||||||
|
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
package txpool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"math/big"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||||
|
"github.com/ethereum/go-ethereum/core/state"
|
||||||
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/ethereum/go-ethereum/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
func pricedValuedTransaction(nonce uint64, value int64, gaslimit uint64, gasprice *big.Int, key *ecdsa.PrivateKey) *types.Transaction {
|
||||||
|
tx, _ := types.SignTx(types.NewTransaction(nonce, common.Address{}, big.NewInt(value), gaslimit, gasprice, nil), types.HomesteadSigner{}, key)
|
||||||
|
return tx
|
||||||
|
}
|
||||||
|
|
||||||
|
func count(t *testing.T, pool *TxPool) (pending int, queued int) {
|
||||||
|
t.Helper()
|
||||||
|
pending, queued = pool.stats()
|
||||||
|
if err := validatePoolInternals(pool); err != nil {
|
||||||
|
t.Fatalf("pool internal state corrupted: %v", err)
|
||||||
|
}
|
||||||
|
return pending, queued
|
||||||
|
}
|
||||||
|
|
||||||
|
func fillPool(t *testing.T, pool *TxPool) {
|
||||||
|
t.Helper()
|
||||||
|
// Create a number of test accounts, fund them and make transactions
|
||||||
|
executableTxs := types.Transactions{}
|
||||||
|
nonExecutableTxs := types.Transactions{}
|
||||||
|
for i := 0; i < 384; i++ {
|
||||||
|
key, _ := crypto.GenerateKey()
|
||||||
|
pool.currentState.AddBalance(crypto.PubkeyToAddress(key.PublicKey), big.NewInt(10000000000))
|
||||||
|
// Add executable ones
|
||||||
|
for j := 0; j < int(pool.config.AccountSlots); j++ {
|
||||||
|
executableTxs = append(executableTxs, pricedTransaction(uint64(j), 100000, big.NewInt(300), key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Import the batch and verify that limits have been enforced
|
||||||
|
pool.AddRemotesSync(executableTxs)
|
||||||
|
pool.AddRemotesSync(nonExecutableTxs)
|
||||||
|
pending, queued := pool.Stats()
|
||||||
|
slots := pool.all.Slots()
|
||||||
|
// sanity-check that the test prerequisites are ok (pending full)
|
||||||
|
if have, want := pending, slots; have != want {
|
||||||
|
t.Fatalf("have %d, want %d", have, want)
|
||||||
|
}
|
||||||
|
if have, want := queued, 0; have != want {
|
||||||
|
t.Fatalf("have %d, want %d", have, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("pool.config: GlobalSlots=%d, GlobalQueue=%d\n", pool.config.GlobalSlots, pool.config.GlobalQueue)
|
||||||
|
t.Logf("pending: %d queued: %d, all: %d\n", pending, queued, slots)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests that if a batch high-priced of non-executables arrive, they do not kick out
|
||||||
|
// executable transactions
|
||||||
|
func TestTransactionFutureAttack(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Create the pool to test the limit enforcement with
|
||||||
|
statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
|
||||||
|
blockchain := &testBlockChain{1000000, statedb, new(event.Feed)}
|
||||||
|
config := testTxPoolConfig
|
||||||
|
config.GlobalQueue = 100
|
||||||
|
config.GlobalSlots = 100
|
||||||
|
pool := NewTxPool(config, eip1559Config, blockchain)
|
||||||
|
defer pool.Stop()
|
||||||
|
fillPool(t, pool)
|
||||||
|
pending, _ := pool.Stats()
|
||||||
|
// Now, future transaction attack starts, let's add a bunch of expensive non-executables, and see if the pending-count drops
|
||||||
|
{
|
||||||
|
key, _ := crypto.GenerateKey()
|
||||||
|
pool.currentState.AddBalance(crypto.PubkeyToAddress(key.PublicKey), big.NewInt(100000000000))
|
||||||
|
futureTxs := types.Transactions{}
|
||||||
|
for j := 0; j < int(pool.config.GlobalSlots+pool.config.GlobalQueue); j++ {
|
||||||
|
futureTxs = append(futureTxs, pricedTransaction(1000+uint64(j), 100000, big.NewInt(500), key))
|
||||||
|
}
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
pool.AddRemotesSync(futureTxs)
|
||||||
|
newPending, newQueued := count(t, pool)
|
||||||
|
t.Logf("pending: %d queued: %d, all: %d\n", newPending, newQueued, pool.all.Slots())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newPending, _ := pool.Stats()
|
||||||
|
// Pending should not have been touched
|
||||||
|
if have, want := newPending, pending; have < want {
|
||||||
|
t.Errorf("wrong pending-count, have %d, want %d (GlobalSlots: %d)",
|
||||||
|
have, want, pool.config.GlobalSlots)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests that if a batch high-priced of non-executables arrive, they do not kick out
|
||||||
|
// executable transactions
|
||||||
|
func TestTransactionFuture1559(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
// Create the pool to test the pricing enforcement with
|
||||||
|
statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
|
||||||
|
blockchain := &testBlockChain{1000000, statedb, new(event.Feed)}
|
||||||
|
pool := NewTxPool(testTxPoolConfig, eip1559Config, blockchain)
|
||||||
|
defer pool.Stop()
|
||||||
|
|
||||||
|
// Create a number of test accounts, fund them and make transactions
|
||||||
|
fillPool(t, pool)
|
||||||
|
pending, _ := pool.Stats()
|
||||||
|
|
||||||
|
// Now, future transaction attack starts, let's add a bunch of expensive non-executables, and see if the pending-count drops
|
||||||
|
{
|
||||||
|
key, _ := crypto.GenerateKey()
|
||||||
|
pool.currentState.AddBalance(crypto.PubkeyToAddress(key.PublicKey), big.NewInt(100000000000))
|
||||||
|
futureTxs := types.Transactions{}
|
||||||
|
for j := 0; j < int(pool.config.GlobalSlots+pool.config.GlobalQueue); j++ {
|
||||||
|
futureTxs = append(futureTxs, dynamicFeeTx(1000+uint64(j), 100000, big.NewInt(200), big.NewInt(101), key))
|
||||||
|
}
|
||||||
|
pool.AddRemotesSync(futureTxs)
|
||||||
|
}
|
||||||
|
newPending, _ := pool.Stats()
|
||||||
|
// Pending should not have been touched
|
||||||
|
if have, want := newPending, pending; have != want {
|
||||||
|
t.Errorf("Wrong pending-count, have %d, want %d (GlobalSlots: %d)",
|
||||||
|
have, want, pool.config.GlobalSlots)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests that if a batch of balance-overdraft txs arrive, they do not kick out
|
||||||
|
// executable transactions
|
||||||
|
func TestTransactionZAttack(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
// Create the pool to test the pricing enforcement with
|
||||||
|
statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
|
||||||
|
blockchain := &testBlockChain{1000000, statedb, new(event.Feed)}
|
||||||
|
pool := NewTxPool(testTxPoolConfig, eip1559Config, blockchain)
|
||||||
|
defer pool.Stop()
|
||||||
|
// Create a number of test accounts, fund them and make transactions
|
||||||
|
fillPool(t, pool)
|
||||||
|
|
||||||
|
countInvalidPending := func() int {
|
||||||
|
t.Helper()
|
||||||
|
var ivpendingNum int
|
||||||
|
pendingtxs, _ := pool.Content()
|
||||||
|
for account, txs := range pendingtxs {
|
||||||
|
cur_balance := new(big.Int).Set(pool.currentState.GetBalance(account))
|
||||||
|
for _, tx := range txs {
|
||||||
|
if cur_balance.Cmp(tx.Value()) <= 0 {
|
||||||
|
ivpendingNum++
|
||||||
|
} else {
|
||||||
|
cur_balance.Sub(cur_balance, tx.Value())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := validatePoolInternals(pool); err != nil {
|
||||||
|
t.Fatalf("pool internal state corrupted: %v", err)
|
||||||
|
}
|
||||||
|
return ivpendingNum
|
||||||
|
}
|
||||||
|
ivPending := countInvalidPending()
|
||||||
|
t.Logf("invalid pending: %d\n", ivPending)
|
||||||
|
|
||||||
|
// Now, DETER-Z attack starts, let's add a bunch of expensive non-executables (from N accounts) along with balance-overdraft txs (from one account), and see if the pending-count drops
|
||||||
|
for j := 0; j < int(pool.config.GlobalQueue); j++ {
|
||||||
|
futureTxs := types.Transactions{}
|
||||||
|
key, _ := crypto.GenerateKey()
|
||||||
|
pool.currentState.AddBalance(crypto.PubkeyToAddress(key.PublicKey), big.NewInt(100000000000))
|
||||||
|
futureTxs = append(futureTxs, pricedTransaction(1000+uint64(j), 21000, big.NewInt(500), key))
|
||||||
|
pool.AddRemotesSync(futureTxs)
|
||||||
|
}
|
||||||
|
|
||||||
|
overDraftTxs := types.Transactions{}
|
||||||
|
{
|
||||||
|
key, _ := crypto.GenerateKey()
|
||||||
|
pool.currentState.AddBalance(crypto.PubkeyToAddress(key.PublicKey), big.NewInt(100000000000))
|
||||||
|
for j := 0; j < int(pool.config.GlobalSlots); j++ {
|
||||||
|
overDraftTxs = append(overDraftTxs, pricedValuedTransaction(uint64(j), 60000000000, 21000, big.NewInt(500), key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pool.AddRemotesSync(overDraftTxs)
|
||||||
|
pool.AddRemotesSync(overDraftTxs)
|
||||||
|
pool.AddRemotesSync(overDraftTxs)
|
||||||
|
pool.AddRemotesSync(overDraftTxs)
|
||||||
|
pool.AddRemotesSync(overDraftTxs)
|
||||||
|
|
||||||
|
newPending, newQueued := count(t, pool)
|
||||||
|
newIvPending := countInvalidPending()
|
||||||
|
t.Logf("pool.all.Slots(): %d\n", pool.all.Slots())
|
||||||
|
t.Logf("pending: %d queued: %d, all: %d\n", newPending, newQueued, pool.all.Slots())
|
||||||
|
t.Logf("invalid pending: %d\n", newIvPending)
|
||||||
|
|
||||||
|
// Pending should not have been touched
|
||||||
|
if newIvPending != ivPending {
|
||||||
|
t.Errorf("Wrong invalid pending-count, have %d, want %d (GlobalSlots: %d, queued: %d)",
|
||||||
|
newIvPending, ivPending, pool.config.GlobalSlots, newQueued)
|
||||||
|
}
|
||||||
|
}
|
@ -158,6 +158,9 @@ func validatePoolInternals(pool *TxPool) error {
|
|||||||
if nonce := pool.pendingNonces.get(addr); nonce != last+1 {
|
if nonce := pool.pendingNonces.get(addr); nonce != last+1 {
|
||||||
return fmt.Errorf("pending nonce mismatch: have %v, want %v", nonce, last+1)
|
return fmt.Errorf("pending nonce mismatch: have %v, want %v", nonce, last+1)
|
||||||
}
|
}
|
||||||
|
if txs.totalcost.Cmp(common.Big0) < 0 {
|
||||||
|
return fmt.Errorf("totalcost went negative: %v", txs.totalcost)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -1105,7 +1108,7 @@ func TestPendingLimiting(t *testing.T) {
|
|||||||
defer pool.Stop()
|
defer pool.Stop()
|
||||||
|
|
||||||
account := crypto.PubkeyToAddress(key.PublicKey)
|
account := crypto.PubkeyToAddress(key.PublicKey)
|
||||||
testAddBalance(pool, account, big.NewInt(1000000))
|
testAddBalance(pool, account, big.NewInt(1000000000000))
|
||||||
|
|
||||||
// Keep track of transaction events to ensure all executables get announced
|
// Keep track of transaction events to ensure all executables get announced
|
||||||
events := make(chan core.NewTxsEvent, testTxPoolConfig.AccountQueue+5)
|
events := make(chan core.NewTxsEvent, testTxPoolConfig.AccountQueue+5)
|
||||||
@ -1584,7 +1587,7 @@ func TestRepricingKeepsLocals(t *testing.T) {
|
|||||||
keys := make([]*ecdsa.PrivateKey, 3)
|
keys := make([]*ecdsa.PrivateKey, 3)
|
||||||
for i := 0; i < len(keys); i++ {
|
for i := 0; i < len(keys); i++ {
|
||||||
keys[i], _ = crypto.GenerateKey()
|
keys[i], _ = crypto.GenerateKey()
|
||||||
testAddBalance(pool, crypto.PubkeyToAddress(keys[i].PublicKey), big.NewInt(1000*1000000))
|
testAddBalance(pool, crypto.PubkeyToAddress(keys[i].PublicKey), big.NewInt(100000*1000000))
|
||||||
}
|
}
|
||||||
// Create transaction (both pending and queued) with a linearly growing gasprice
|
// Create transaction (both pending and queued) with a linearly growing gasprice
|
||||||
for i := uint64(0); i < 500; i++ {
|
for i := uint64(0); i < 500; i++ {
|
||||||
@ -1663,7 +1666,7 @@ func TestUnderpricing(t *testing.T) {
|
|||||||
defer sub.Unsubscribe()
|
defer sub.Unsubscribe()
|
||||||
|
|
||||||
// Create a number of test accounts and fund them
|
// Create a number of test accounts and fund them
|
||||||
keys := make([]*ecdsa.PrivateKey, 4)
|
keys := make([]*ecdsa.PrivateKey, 5)
|
||||||
for i := 0; i < len(keys); i++ {
|
for i := 0; i < len(keys); i++ {
|
||||||
keys[i], _ = crypto.GenerateKey()
|
keys[i], _ = crypto.GenerateKey()
|
||||||
testAddBalance(pool, crypto.PubkeyToAddress(keys[i].PublicKey), big.NewInt(1000000))
|
testAddBalance(pool, crypto.PubkeyToAddress(keys[i].PublicKey), big.NewInt(1000000))
|
||||||
@ -1699,6 +1702,10 @@ func TestUnderpricing(t *testing.T) {
|
|||||||
if err := pool.AddRemote(pricedTransaction(0, 100000, big.NewInt(1), keys[1])); err != ErrUnderpriced {
|
if err := pool.AddRemote(pricedTransaction(0, 100000, big.NewInt(1), keys[1])); err != ErrUnderpriced {
|
||||||
t.Fatalf("adding underpriced pending transaction error mismatch: have %v, want %v", err, ErrUnderpriced)
|
t.Fatalf("adding underpriced pending transaction error mismatch: have %v, want %v", err, ErrUnderpriced)
|
||||||
}
|
}
|
||||||
|
// Replace a future transaction with a future transaction
|
||||||
|
if err := pool.AddRemote(pricedTransaction(1, 100000, big.NewInt(2), keys[1])); err != nil { // +K1:1 => -K1:1 => Pend K0:0, K0:1, K2:0; Que K1:1
|
||||||
|
t.Fatalf("failed to add well priced transaction: %v", err)
|
||||||
|
}
|
||||||
// Ensure that adding high priced transactions drops cheap ones, but not own
|
// Ensure that adding high priced transactions drops cheap ones, but not own
|
||||||
if err := pool.AddRemote(pricedTransaction(0, 100000, big.NewInt(3), keys[1])); err != nil { // +K1:0 => -K1:1 => Pend K0:0, K0:1, K1:0, K2:0; Que -
|
if err := pool.AddRemote(pricedTransaction(0, 100000, big.NewInt(3), keys[1])); err != nil { // +K1:0 => -K1:1 => Pend K0:0, K0:1, K1:0, K2:0; Que -
|
||||||
t.Fatalf("failed to add well priced transaction: %v", err)
|
t.Fatalf("failed to add well priced transaction: %v", err)
|
||||||
@ -1709,6 +1716,10 @@ func TestUnderpricing(t *testing.T) {
|
|||||||
if err := pool.AddRemote(pricedTransaction(3, 100000, big.NewInt(5), keys[1])); err != nil { // +K1:3 => -K0:1 => Pend K1:0, K2:0; Que K1:2 K1:3
|
if err := pool.AddRemote(pricedTransaction(3, 100000, big.NewInt(5), keys[1])); err != nil { // +K1:3 => -K0:1 => Pend K1:0, K2:0; Que K1:2 K1:3
|
||||||
t.Fatalf("failed to add well priced transaction: %v", err)
|
t.Fatalf("failed to add well priced transaction: %v", err)
|
||||||
}
|
}
|
||||||
|
// Ensure that replacing a pending transaction with a future transaction fails
|
||||||
|
if err := pool.AddRemote(pricedTransaction(5, 100000, big.NewInt(6), keys[1])); err != ErrFutureReplacePending {
|
||||||
|
t.Fatalf("adding future replace transaction error mismatch: have %v, want %v", err, ErrFutureReplacePending)
|
||||||
|
}
|
||||||
pending, queued = pool.Stats()
|
pending, queued = pool.Stats()
|
||||||
if pending != 2 {
|
if pending != 2 {
|
||||||
t.Fatalf("pending transactions mismatched: have %d, want %d", pending, 2)
|
t.Fatalf("pending transactions mismatched: have %d, want %d", pending, 2)
|
||||||
@ -1716,7 +1727,7 @@ func TestUnderpricing(t *testing.T) {
|
|||||||
if queued != 2 {
|
if queued != 2 {
|
||||||
t.Fatalf("queued transactions mismatched: have %d, want %d", queued, 2)
|
t.Fatalf("queued transactions mismatched: have %d, want %d", queued, 2)
|
||||||
}
|
}
|
||||||
if err := validateEvents(events, 1); err != nil {
|
if err := validateEvents(events, 2); err != nil {
|
||||||
t.Fatalf("additional event firing failed: %v", err)
|
t.Fatalf("additional event firing failed: %v", err)
|
||||||
}
|
}
|
||||||
if err := validatePoolInternals(pool); err != nil {
|
if err := validatePoolInternals(pool); err != nil {
|
||||||
@ -1878,11 +1889,11 @@ func TestUnderpricingDynamicFee(t *testing.T) {
|
|||||||
t.Fatalf("failed to add well priced transaction: %v", err)
|
t.Fatalf("failed to add well priced transaction: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tx = pricedTransaction(2, 100000, big.NewInt(3), keys[1])
|
tx = pricedTransaction(1, 100000, big.NewInt(3), keys[1])
|
||||||
if err := pool.AddRemote(tx); err != nil { // +K1:2, -K0:1 => Pend K0:0 K1:0, K2:0; Que K1:2
|
if err := pool.AddRemote(tx); err != nil { // +K1:2, -K0:1 => Pend K0:0 K1:0, K2:0; Que K1:2
|
||||||
t.Fatalf("failed to add well priced transaction: %v", err)
|
t.Fatalf("failed to add well priced transaction: %v", err)
|
||||||
}
|
}
|
||||||
tx = dynamicFeeTx(3, 100000, big.NewInt(4), big.NewInt(1), keys[1])
|
tx = dynamicFeeTx(2, 100000, big.NewInt(4), big.NewInt(1), keys[1])
|
||||||
if err := pool.AddRemote(tx); err != nil { // +K1:3, -K1:0 => Pend K0:0 K2:0; Que K1:2 K1:3
|
if err := pool.AddRemote(tx); err != nil { // +K1:3, -K1:0 => Pend K0:0 K2:0; Que K1:2 K1:3
|
||||||
t.Fatalf("failed to add well priced transaction: %v", err)
|
t.Fatalf("failed to add well priced transaction: %v", err)
|
||||||
}
|
}
|
||||||
@ -1893,7 +1904,7 @@ func TestUnderpricingDynamicFee(t *testing.T) {
|
|||||||
if queued != 2 {
|
if queued != 2 {
|
||||||
t.Fatalf("queued transactions mismatched: have %d, want %d", queued, 2)
|
t.Fatalf("queued transactions mismatched: have %d, want %d", queued, 2)
|
||||||
}
|
}
|
||||||
if err := validateEvents(events, 1); err != nil {
|
if err := validateEvents(events, 2); err != nil {
|
||||||
t.Fatalf("additional event firing failed: %v", err)
|
t.Fatalf("additional event firing failed: %v", err)
|
||||||
}
|
}
|
||||||
if err := validatePoolInternals(pool); err != nil {
|
if err := validatePoolInternals(pool); err != nil {
|
||||||
@ -2487,7 +2498,7 @@ func benchmarkBatchInsert(b *testing.B, size int, local bool) {
|
|||||||
defer pool.Stop()
|
defer pool.Stop()
|
||||||
|
|
||||||
account := crypto.PubkeyToAddress(key.PublicKey)
|
account := crypto.PubkeyToAddress(key.PublicKey)
|
||||||
testAddBalance(pool, account, big.NewInt(1000000))
|
testAddBalance(pool, account, big.NewInt(1000000000000000000))
|
||||||
|
|
||||||
batches := make([]types.Transactions, b.N)
|
batches := make([]types.Transactions, b.N)
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
|
Loading…
Reference in New Issue
Block a user