From 966ee3ae6d0e8201eae682d22312a600307a65d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felf=C3=B6ldi=20Zsolt?= Date: Fri, 28 May 2021 10:28:07 +0200 Subject: [PATCH] all: EIP-1559 tx pool support (#22898) This pull request implements EIP-1559 compatible transaction pool with dual heap eviction ordering. It is based on #22791 The eviction ordering scheme and the reasoning behind it is described here: https://gist.github.com/zsfelfoldi/9607ad248707a925b701f49787904fd6 --- core/tx_list.go | 202 +++++++++----- core/tx_pool.go | 58 +++- core/tx_pool_test.go | 565 ++++++++++++++++++++++++++++++++++---- core/types/transaction.go | 65 +++-- eth/gasprice/gasprice.go | 4 +- 5 files changed, 747 insertions(+), 147 deletions(-) diff --git a/core/tx_list.go b/core/tx_list.go index 894640d57..d0dcb7bae 100644 --- a/core/tx_list.go +++ b/core/tx_list.go @@ -21,6 +21,7 @@ import ( "math" "math/big" "sort" + "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" @@ -279,15 +280,23 @@ func (l *txList) Add(tx *types.Transaction, priceBump uint64) (bool, *types.Tran // If there's an older better transaction, abort old := l.txs.Get(tx.Nonce()) if old != nil { - // threshold = oldGP * (100 + priceBump) / 100 + if old.FeeCapCmp(tx) >= 0 || old.TipCmp(tx) >= 0 { + return false, nil + } + // thresholdFeeCap = oldFC * (100 + priceBump) / 100 a := big.NewInt(100 + int64(priceBump)) - a = a.Mul(a, old.GasPrice()) + aFeeCap := new(big.Int).Mul(a, old.FeeCap()) + aTip := a.Mul(a, old.Tip()) + + // thresholdTip = oldTip * (100 + priceBump) / 100 b := big.NewInt(100) - threshold := a.Div(a, b) - // Have to ensure that the new gas price is higher than the old gas - // price as well as checking the percentage threshold to ensure that + thresholdFeeCap := aFeeCap.Div(aFeeCap, b) + thresholdTip := aTip.Div(aTip, b) + + // Have to ensure that either the new fee cap or tip is higher than the + // old ones as well as checking the percentage threshold to ensure that // this is accurate for low (Wei-level) gas price replacements - if old.GasPriceCmp(tx) >= 0 || tx.GasPriceIntCmp(threshold) < 0 { + if tx.FeeCapIntCmp(thresholdFeeCap) < 0 || tx.TipIntCmp(thresholdTip) < 0 { return false, nil } } @@ -406,34 +415,54 @@ func (l *txList) LastElement() *types.Transaction { } // priceHeap is a heap.Interface implementation over transactions for retrieving -// price-sorted transactions to discard when the pool fills up. -type priceHeap []*types.Transaction +// 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. +// If baseFee is nil then the sorting is based on feeCap. +type priceHeap struct { + baseFee *big.Int // heap should always be re-sorted after baseFee is changed + list []*types.Transaction +} -func (h priceHeap) Len() int { return len(h) } -func (h priceHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } +func (h *priceHeap) Len() int { return len(h.list) } +func (h *priceHeap) Swap(i, j int) { h.list[i], h.list[j] = h.list[j], h.list[i] } -func (h priceHeap) Less(i, j int) bool { - // Sort primarily by price, returning the cheaper one - switch h[i].GasPriceCmp(h[j]) { +func (h *priceHeap) Less(i, j int) bool { + switch h.cmp(h.list[i], h.list[j]) { case -1: return true case 1: return false + default: + return h.list[i].Nonce() > h.list[j].Nonce() } - // If the prices match, stabilize via nonces (high nonce is worse) - return h[i].Nonce() > h[j].Nonce() +} + +func (h *priceHeap) cmp(a, b *types.Transaction) int { + if h.baseFee != nil { + // Compare effective tips if baseFee is specified + if c := a.EffectiveTipCmp(b, h.baseFee); c != 0 { + return c + } + } + // Compare fee caps if baseFee is not specified or effective tips are equal + if c := a.FeeCapCmp(b); c != 0 { + return c + } + // Compare tips if effective tips and fee caps are equal + return a.TipCmp(b) } func (h *priceHeap) Push(x interface{}) { - *h = append(*h, x.(*types.Transaction)) + tx := x.(*types.Transaction) + h.list = append(h.list, tx) } func (h *priceHeap) Pop() interface{} { - old := *h + old := h.list n := len(old) x := old[n-1] old[n-1] = nil - *h = old[0 : n-1] + h.list = old[0 : n-1] return x } @@ -441,17 +470,29 @@ func (h *priceHeap) Pop() interface{} { // contents in a price-incrementing way. It's built opon the all transactions // in txpool but only interested in the remote part. It means only remote transactions // will be considered for tracking, sorting, eviction, etc. +// +// Two heaps are used for sorting: the urgent heap (based on effective tip in the next +// block) and the floating heap (based on feeCap). Always the bigger heap is chosen for +// eviction. Transactions evicted from the urgent heap are first demoted into the floating heap. +// In some cases (during a congestion, when blocks are full) the urgent heap can provide +// better candidates for inclusion while in other cases (at the top of the baseFee peak) +// the floating heap is better. When baseFee is decreasing they behave similarly. type txPricedList struct { - all *txLookup // Pointer to the map of all transactions - remotes *priceHeap // Heap of prices of all the stored **remote** transactions - stales int // Number of stale price points to (re-heap trigger) + all *txLookup // Pointer to the map of all transactions + urgent, floating priceHeap // Heaps of prices of all the stored **remote** transactions + stales int // Number of stale price points to (re-heap trigger) } +const ( + // urgentRatio : floatingRatio is the capacity ratio of the two queues + urgentRatio = 4 + floatingRatio = 1 +) + // newTxPricedList creates a new price-sorted transaction heap. func newTxPricedList(all *txLookup) *txPricedList { return &txPricedList{ - all: all, - remotes: new(priceHeap), + all: all, } } @@ -460,7 +501,8 @@ func (l *txPricedList) Put(tx *types.Transaction, local bool) { if local { return } - heap.Push(l.remotes, tx) + // Insert every new transaction to the urgent heap first; Discard will balance the heaps + heap.Push(&l.urgent, tx) } // Removed notifies the prices transaction list that an old transaction dropped @@ -469,58 +511,43 @@ func (l *txPricedList) Put(tx *types.Transaction, local bool) { func (l *txPricedList) Removed(count int) { // Bump the stale counter, but exit if still too low (< 25%) l.stales += count - if l.stales <= len(*l.remotes)/4 { + if l.stales <= (len(l.urgent.list)+len(l.floating.list))/4 { return } // Seems we've reached a critical number of stale transactions, reheap l.Reheap() } -// Cap finds all the transactions below the given price threshold, drops them -// from the priced list and returns them for further removal from the entire pool. -// -// Note: only remote transactions will be considered for eviction. -func (l *txPricedList) Cap(threshold *big.Int) types.Transactions { - drop := make(types.Transactions, 0, 128) // Remote underpriced transactions to drop - for len(*l.remotes) > 0 { - // Discard stale transactions if found during cleanup - cheapest := (*l.remotes)[0] - if l.all.GetRemote(cheapest.Hash()) == nil { // Removed or migrated - heap.Pop(l.remotes) - l.stales-- - continue - } - // Stop the discards if we've reached the threshold - if cheapest.GasPriceIntCmp(threshold) >= 0 { - break - } - heap.Pop(l.remotes) - drop = append(drop, cheapest) - } - return drop -} - // Underpriced checks whether a transaction is cheaper than (or as cheap as) the // lowest priced (remote) transaction currently being tracked. func (l *txPricedList) Underpriced(tx *types.Transaction) bool { + // Note: with two queues, being underpriced is defined as being worse than the worst item + // in all non-empty queues if there is any. If both queues are empty then nothing is underpriced. + return (l.underpricedFor(&l.urgent, tx) || len(l.urgent.list) == 0) && + (l.underpricedFor(&l.floating, tx) || len(l.floating.list) == 0) && + (len(l.urgent.list) != 0 || len(l.floating.list) != 0) +} + +// underpricedFor checks whether a transaction is cheaper than (or as cheap as) the +// lowest priced (remote) transaction in the given heap. +func (l *txPricedList) underpricedFor(h *priceHeap, tx *types.Transaction) bool { // Discard stale price points if found at the heap start - for len(*l.remotes) > 0 { - head := []*types.Transaction(*l.remotes)[0] + for len(h.list) > 0 { + head := h.list[0] if l.all.GetRemote(head.Hash()) == nil { // Removed or migrated l.stales-- - heap.Pop(l.remotes) + heap.Pop(h) continue } break } // Check if the transaction is underpriced or not - if len(*l.remotes) == 0 { + if len(h.list) == 0 { return false // There is no remote transaction at all. } // If the remote transaction is even cheaper than the // cheapest one tracked locally, reject it. - cheapest := []*types.Transaction(*l.remotes)[0] - return cheapest.GasPriceCmp(tx) >= 0 + return h.cmp(h.list[0], tx) >= 0 } // Discard finds a number of most underpriced transactions, removes them from the @@ -529,21 +556,36 @@ func (l *txPricedList) Underpriced(tx *types.Transaction) bool { // Note local transaction won't be considered for eviction. func (l *txPricedList) Discard(slots int, force bool) (types.Transactions, bool) { drop := make(types.Transactions, 0, slots) // Remote underpriced transactions to drop - for len(*l.remotes) > 0 && slots > 0 { - // Discard stale transactions if found during cleanup - tx := heap.Pop(l.remotes).(*types.Transaction) - if l.all.GetRemote(tx.Hash()) == nil { // Removed or migrated - l.stales-- - continue + for slots > 0 { + if len(l.urgent.list)*floatingRatio > len(l.floating.list)*urgentRatio || floatingRatio == 0 { + // Discard stale transactions if found during cleanup + tx := heap.Pop(&l.urgent).(*types.Transaction) + if l.all.GetRemote(tx.Hash()) == nil { // Removed or migrated + l.stales-- + continue + } + // Non stale transaction found, move to floating heap + heap.Push(&l.floating, tx) + } else { + if len(l.floating.list) == 0 { + // Stop if both heaps are empty + break + } + // Discard stale transactions if found during cleanup + tx := heap.Pop(&l.floating).(*types.Transaction) + if l.all.GetRemote(tx.Hash()) == nil { // Removed or migrated + l.stales-- + continue + } + // Non stale transaction found, discard it + drop = append(drop, tx) + slots -= numSlots(tx) } - // Non stale transaction found, discard it - drop = append(drop, tx) - slots -= numSlots(tx) } // If we still can't make enough room for the new transaction if slots > 0 && !force { for _, tx := range drop { - heap.Push(l.remotes, tx) + heap.Push(&l.urgent, tx) } return nil, false } @@ -552,12 +594,32 @@ func (l *txPricedList) Discard(slots int, force bool) (types.Transactions, bool) // Reheap forcibly rebuilds the heap based on the current remote transaction set. func (l *txPricedList) Reheap() { - reheap := make(priceHeap, 0, l.all.RemoteCount()) - - l.stales, l.remotes = 0, &reheap + start := time.Now() + l.stales = 0 + l.urgent.list = make([]*types.Transaction, 0, l.all.RemoteCount()) l.all.Range(func(hash common.Hash, tx *types.Transaction, local bool) bool { - *l.remotes = append(*l.remotes, tx) + l.urgent.list = append(l.urgent.list, tx) return true }, false, true) // Only iterate remotes - heap.Init(l.remotes) + heap.Init(&l.urgent) + + // balance out the two heaps by moving the worse half of transactions into the + // floating heap + // Note: Discard would also do this before the first eviction but Reheap can do + // is more efficiently. Also, Underpriced would work suboptimally the first time + // if the floating queue was empty. + floatingCount := len(l.urgent.list) * floatingRatio / (urgentRatio + floatingRatio) + l.floating.list = make([]*types.Transaction, floatingCount) + for i := 0; i < floatingCount; i++ { + l.floating.list[i] = heap.Pop(&l.urgent).(*types.Transaction) + } + heap.Init(&l.floating) + reheapTimer.Update(time.Since(start)) +} + +// SetBaseFee updates the base fee and triggers a re-heap. Note that Removed is not +// necessary to call right before SetBaseFee when processing a new block. +func (l *txPricedList) SetBaseFee(baseFee *big.Int) { + l.urgent.baseFee = baseFee + l.Reheap() } diff --git a/core/tx_pool.go b/core/tx_pool.go index 0abc092ec..545b7c626 100644 --- a/core/tx_pool.go +++ b/core/tx_pool.go @@ -83,6 +83,10 @@ 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") + + // ErrTipAboveFeeCap is a sanity error to ensure no one is able to specify a + // transaction with a tip higher than the total fee cap. + ErrTipAboveFeeCap = errors.New("tip higher than fee cap") ) var ( @@ -115,6 +119,8 @@ var ( queuedGauge = metrics.NewRegisteredGauge("txpool/queued", nil) localGauge = metrics.NewRegisteredGauge("txpool/local", nil) slotsGauge = metrics.NewRegisteredGauge("txpool/slots", nil) + + reheapTimer = metrics.NewRegisteredTimer("txpool/reheap", nil) ) // TxStatus is the current status of a transaction as seen by the pool. @@ -165,7 +171,7 @@ var DefaultTxPoolConfig = TxPoolConfig{ PriceBump: 10, AccountSlots: 16, - GlobalSlots: 4096, + GlobalSlots: 4096 + 1024, // urgent + floating queue capacity with 4:1 ratio AccountQueue: 64, GlobalQueue: 1024, @@ -230,6 +236,7 @@ type TxPool struct { istanbul bool // Fork indicator whether we are in the istanbul stage. eip2718 bool // Fork indicator whether we are using EIP-2718 type transactions. + eip1559 bool // Fork indicator whether we are using EIP-1559 type transactions. currentState *state.StateDB // Current state in the blockchain head pendingNonces *txNoncer // Pending state tracking virtual nonces @@ -426,10 +433,18 @@ func (pool *TxPool) SetGasPrice(price *big.Int) { pool.mu.Lock() defer pool.mu.Unlock() + old := pool.gasPrice pool.gasPrice = price - for _, tx := range pool.priced.Cap(price) { - pool.removeTx(tx.Hash(), false) + // if the min miner fee increased, remove transactions below the new threshold + if price.Cmp(old) > 0 { + // pool.priced is sorted by FeeCap, so we have to iterate through pool.all instead + drop := pool.all.RemotesBelowTip(price) + for _, tx := range drop { + pool.removeTx(tx.Hash(), false) + } + pool.priced.Removed(len(drop)) } + log.Info("Transaction pool price threshold updated", "price", price) } @@ -527,6 +542,10 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error { if !pool.eip2718 && tx.Type() != types.LegacyTxType { return ErrTxTypeNotSupported } + // Reject dynamic fee transactions until EIP-1559 activates. + if !pool.eip1559 && tx.Type() == types.DynamicFeeTxType { + return ErrTxTypeNotSupported + } // Reject transactions over defined size to prevent DOS attacks if uint64(tx.Size()) > txMaxSize { return ErrOversizedData @@ -540,13 +559,17 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error { if pool.currentMaxGas < tx.Gas() { return ErrGasLimit } + // Ensure feeCap is less than or equal to tip. + if tx.FeeCapIntCmp(tx.Tip()) < 0 { + return ErrTipAboveFeeCap + } // Make sure the transaction is signed properly. from, err := types.Sender(pool.signer, tx) if err != nil { return ErrInvalidSender } - // Drop non-local transactions under our own minimal accepted gas price - if !local && tx.GasPriceIntCmp(pool.gasPrice) < 0 { + // Drop non-local transactions under our own minimal accepted gas price or tip + if !local && tx.TipIntCmp(pool.gasPrice) < 0 { return ErrUnderpriced } // Ensure the transaction adheres to nonce ordering @@ -598,7 +621,7 @@ func (pool *TxPool) add(tx *types.Transaction, local bool) (replaced bool, err e if uint64(pool.all.Slots()+numSlots(tx)) > pool.config.GlobalSlots+pool.config.GlobalQueue { // If the new transaction is underpriced, don't accept it if !isLocal && pool.priced.Underpriced(tx) { - log.Trace("Discarding underpriced transaction", "hash", hash, "price", tx.GasPrice()) + log.Trace("Discarding underpriced transaction", "hash", hash, "tip", tx.Tip(), "feeCap", tx.FeeCap()) underpricedTxMeter.Mark(1) return false, ErrUnderpriced } @@ -615,7 +638,7 @@ func (pool *TxPool) add(tx *types.Transaction, local bool) (replaced bool, err e } // Kick out the underpriced remote transactions. for _, tx := range drop { - log.Trace("Discarding freshly underpriced transaction", "hash", tx.Hash(), "price", tx.GasPrice()) + log.Trace("Discarding freshly underpriced transaction", "hash", tx.Hash(), "tip", tx.Tip(), "feeCap", tx.FeeCap()) underpricedTxMeter.Mark(1) pool.removeTx(tx.Hash(), false) } @@ -1088,6 +1111,9 @@ func (pool *TxPool) runReorg(done chan struct{}, reset *txpoolResetRequest, dirt // because of another transaction (e.g. higher gas price). if reset != nil { pool.demoteUnexecutables() + if reset.newHead != nil { + pool.priced.SetBaseFee(reset.newHead.BaseFee) + } } // Ensure pool.queue and pool.pending sizes stay within the configured limits. pool.truncatePending() @@ -1205,6 +1231,7 @@ func (pool *TxPool) reset(oldHead, newHead *types.Header) { next := new(big.Int).Add(newHead.Number, big.NewInt(1)) pool.istanbul = pool.chainconfig.IsIstanbul(next) pool.eip2718 = pool.chainconfig.IsBerlin(next) + pool.eip1559 = pool.chainconfig.IsLondon(next) } // promoteExecutables moves transactions that have become processable from the @@ -1408,6 +1435,10 @@ func (pool *TxPool) truncateQueue() { // demoteUnexecutables removes invalid and processed transactions from the pools // executable/pending queue and any subsequent transactions that become unexecutable // are moved back into the future queue. +// +// Note: transactions are not marked as removed in the priced list because re-heaping +// is always explicitly triggered by SetBaseFee and it would be unnecessary and wasteful +// to trigger a re-heap is this function func (pool *TxPool) demoteUnexecutables() { // Iterate over all accounts and demote any non-executable transactions for addr, list := range pool.pending { @@ -1427,7 +1458,6 @@ func (pool *TxPool) demoteUnexecutables() { log.Trace("Removed unpayable pending transaction", "hash", hash) pool.all.Remove(hash) } - pool.priced.Removed(len(olds) + len(drops)) pendingNofundsMeter.Mark(int64(len(drops))) for _, tx := range invalids { @@ -1709,6 +1739,18 @@ func (t *txLookup) RemoteToLocals(locals *accountSet) int { return migrated } +// RemotesBelowTip finds all remote transactions below the given tip threshold. +func (t *txLookup) RemotesBelowTip(threshold *big.Int) types.Transactions { + found := make(types.Transactions, 0, 128) + t.Range(func(hash common.Hash, tx *types.Transaction, local bool) bool { + if tx.TipIntCmp(threshold) < 0 { + found = append(found, tx) + } + return true + }, false, true) // Only iterate remotes + return found +} + // numSlots calculates the number of slots needed for a single transaction. func numSlots(tx *types.Transaction) int { return int((tx.Size() + txSlotSize - 1) / txSlotSize) diff --git a/core/tx_pool_test.go b/core/tx_pool_test.go index 5d555f5a9..380ba7167 100644 --- a/core/tx_pool_test.go +++ b/core/tx_pool_test.go @@ -37,13 +37,23 @@ import ( "github.com/ethereum/go-ethereum/trie" ) -// testTxPoolConfig is a transaction pool configuration without stateful disk -// sideeffects used during testing. -var testTxPoolConfig TxPoolConfig +var ( + // testTxPoolConfig is a transaction pool configuration without stateful disk + // sideeffects used during testing. + testTxPoolConfig TxPoolConfig + + // eip1559Config is a chain config with EIP-1559 enabled at block 0. + eip1559Config *params.ChainConfig +) func init() { testTxPoolConfig = DefaultTxPoolConfig testTxPoolConfig.Journal = "" + + cpy := *params.TestChainConfig + eip1559Config = &cpy + eip1559Config.BerlinBlock = common.Big0 + eip1559Config.LondonBlock = common.Big0 } type testBlockChain struct { @@ -87,12 +97,31 @@ func pricedDataTransaction(nonce uint64, gaslimit uint64, gasprice *big.Int, key return tx } +func dynamicFeeTx(nonce uint64, gaslimit uint64, gasFee *big.Int, tip *big.Int, key *ecdsa.PrivateKey) *types.Transaction { + tx, _ := types.SignNewTx(key, types.LatestSignerForChainID(params.TestChainConfig.ChainID), &types.DynamicFeeTx{ + ChainID: params.TestChainConfig.ChainID, + Nonce: nonce, + Tip: tip, + FeeCap: gasFee, + Gas: gaslimit, + To: &common.Address{}, + Value: big.NewInt(100), + Data: nil, + AccessList: nil, + }) + return tx +} + func setupTxPool() (*TxPool, *ecdsa.PrivateKey) { + return setupTxPoolWithConfig(params.TestChainConfig) +} + +func setupTxPoolWithConfig(config *params.ChainConfig) (*TxPool, *ecdsa.PrivateKey) { statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) blockchain := &testBlockChain{statedb, 10000000, new(event.Feed)} key, _ := crypto.GenerateKey() - pool := NewTxPool(testTxPoolConfig, params.TestChainConfig, blockchain) + pool := NewTxPool(testTxPoolConfig, config, blockchain) return pool, key } @@ -108,7 +137,7 @@ func validateTxPoolInternals(pool *TxPool) error { return fmt.Errorf("total transaction count %d != %d pending + %d queued", total, pending, queued) } pool.priced.Reheap() - priced, remote := pool.priced.remotes.Len(), pool.all.RemoteCount() + priced, remote := pool.priced.urgent.Len()+pool.priced.floating.Len(), pool.all.RemoteCount() if priced != remote { return fmt.Errorf("total priced transaction count %d != %d", priced, remote) } @@ -233,6 +262,18 @@ func TestStateChangeDuringTransactionPoolReset(t *testing.T) { } } +func testAddBalance(pool *TxPool, addr common.Address, amount *big.Int) { + pool.mu.Lock() + pool.currentState.AddBalance(addr, amount) + pool.mu.Unlock() +} + +func testSetNonce(pool *TxPool, addr common.Address, nonce uint64) { + pool.mu.Lock() + pool.currentState.SetNonce(addr, nonce) + pool.mu.Unlock() +} + func TestInvalidTransactions(t *testing.T) { t.Parallel() @@ -242,19 +283,19 @@ func TestInvalidTransactions(t *testing.T) { tx := transaction(0, 100, key) from, _ := deriveSender(tx) - pool.currentState.AddBalance(from, big.NewInt(1)) + testAddBalance(pool, from, big.NewInt(1)) if err := pool.AddRemote(tx); !errors.Is(err, ErrInsufficientFunds) { t.Error("expected", ErrInsufficientFunds) } balance := new(big.Int).Add(tx.Value(), new(big.Int).Mul(new(big.Int).SetUint64(tx.Gas()), tx.GasPrice())) - pool.currentState.AddBalance(from, balance) + testAddBalance(pool, from, balance) if err := pool.AddRemote(tx); !errors.Is(err, ErrIntrinsicGas) { t.Error("expected", ErrIntrinsicGas, "got", err) } - pool.currentState.SetNonce(from, 1) - pool.currentState.AddBalance(from, big.NewInt(0xffffffffffffff)) + testSetNonce(pool, from, 1) + testAddBalance(pool, from, big.NewInt(0xffffffffffffff)) tx = transaction(0, 100000, key) if err := pool.AddRemote(tx); !errors.Is(err, ErrNonceTooLow) { t.Error("expected", ErrNonceTooLow) @@ -278,7 +319,7 @@ func TestTransactionQueue(t *testing.T) { tx := transaction(0, 100, key) from, _ := deriveSender(tx) - pool.currentState.AddBalance(from, big.NewInt(1000)) + testAddBalance(pool, from, big.NewInt(1000)) <-pool.requestReset(nil, nil) pool.enqueueTx(tx.Hash(), tx, false, true) @@ -289,7 +330,7 @@ func TestTransactionQueue(t *testing.T) { tx = transaction(1, 100, key) from, _ = deriveSender(tx) - pool.currentState.SetNonce(from, 2) + testSetNonce(pool, from, 2) pool.enqueueTx(tx.Hash(), tx, false, true) <-pool.requestPromoteExecutables(newAccountSet(pool.signer, from)) @@ -311,7 +352,7 @@ func TestTransactionQueue2(t *testing.T) { tx2 := transaction(10, 100, key) tx3 := transaction(11, 100, key) from, _ := deriveSender(tx1) - pool.currentState.AddBalance(from, big.NewInt(1000)) + testAddBalance(pool, from, big.NewInt(1000)) pool.reset(nil, nil) pool.enqueueTx(tx1.Hash(), tx1, false, true) @@ -335,12 +376,25 @@ func TestTransactionNegativeValue(t *testing.T) { tx, _ := types.SignTx(types.NewTransaction(0, common.Address{}, big.NewInt(-1), 100, big.NewInt(1), nil), types.HomesteadSigner{}, key) from, _ := deriveSender(tx) - pool.currentState.AddBalance(from, big.NewInt(1)) + testAddBalance(pool, from, big.NewInt(1)) if err := pool.AddRemote(tx); err != ErrNegativeValue { t.Error("expected", ErrNegativeValue, "got", err) } } +func TestTransactionTipAboveFeeCap(t *testing.T) { + t.Parallel() + + pool, key := setupTxPoolWithConfig(eip1559Config) + defer pool.Stop() + + tx := dynamicFeeTx(0, 100, big.NewInt(1), big.NewInt(2), key) + + if err := pool.AddRemote(tx); err != ErrTipAboveFeeCap { + t.Error("expected", ErrTipAboveFeeCap, "got", err) + } +} + func TestTransactionChainFork(t *testing.T) { t.Parallel() @@ -428,7 +482,7 @@ func TestTransactionMissingNonce(t *testing.T) { defer pool.Stop() addr := crypto.PubkeyToAddress(key.PublicKey) - pool.currentState.AddBalance(addr, big.NewInt(100000000000000)) + testAddBalance(pool, addr, big.NewInt(100000000000000)) tx := transaction(1, 100000, key) if _, err := pool.add(tx, false); err != nil { t.Error("didn't expect error", err) @@ -452,8 +506,8 @@ func TestTransactionNonceRecovery(t *testing.T) { defer pool.Stop() addr := crypto.PubkeyToAddress(key.PublicKey) - pool.currentState.SetNonce(addr, n) - pool.currentState.AddBalance(addr, big.NewInt(100000000000000)) + testSetNonce(pool, addr, n) + testAddBalance(pool, addr, big.NewInt(100000000000000)) <-pool.requestReset(nil, nil) tx := transaction(n, 100000, key) @@ -461,7 +515,7 @@ func TestTransactionNonceRecovery(t *testing.T) { t.Error(err) } // simulate some weird re-order of transactions and missing nonce(s) - pool.currentState.SetNonce(addr, n-1) + testSetNonce(pool, addr, n-1) <-pool.requestReset(nil, nil) if fn := pool.Nonce(addr); fn != n-1 { t.Errorf("expected nonce to be %d, got %d", n-1, fn) @@ -478,7 +532,7 @@ func TestTransactionDropping(t *testing.T) { defer pool.Stop() account := crypto.PubkeyToAddress(key.PublicKey) - pool.currentState.AddBalance(account, big.NewInt(1000)) + testAddBalance(pool, account, big.NewInt(1000)) // Add some pending and some queued transactions var ( @@ -526,7 +580,7 @@ func TestTransactionDropping(t *testing.T) { t.Errorf("total transaction mismatch: have %d, want %d", pool.all.Count(), 6) } // Reduce the balance of the account, and check that invalidated transactions are dropped - pool.currentState.AddBalance(account, big.NewInt(-650)) + testAddBalance(pool, account, big.NewInt(-650)) <-pool.requestReset(nil, nil) if _, ok := pool.pending[account].txs.items[tx0.Nonce()]; !ok { @@ -592,7 +646,7 @@ func TestTransactionPostponing(t *testing.T) { keys[i], _ = crypto.GenerateKey() accs[i] = crypto.PubkeyToAddress(keys[i].PublicKey) - pool.currentState.AddBalance(crypto.PubkeyToAddress(keys[i].PublicKey), big.NewInt(50100)) + testAddBalance(pool, crypto.PubkeyToAddress(keys[i].PublicKey), big.NewInt(50100)) } // Add a batch consecutive pending transactions for validation txs := []*types.Transaction{} @@ -635,7 +689,7 @@ func TestTransactionPostponing(t *testing.T) { } // Reduce the balance of the account, and check that transactions are reorganised for _, addr := range accs { - pool.currentState.AddBalance(addr, big.NewInt(-1)) + testAddBalance(pool, addr, big.NewInt(-1)) } <-pool.requestReset(nil, nil) @@ -696,7 +750,7 @@ func TestTransactionGapFilling(t *testing.T) { defer pool.Stop() account := crypto.PubkeyToAddress(key.PublicKey) - pool.currentState.AddBalance(account, big.NewInt(1000000)) + testAddBalance(pool, account, big.NewInt(1000000)) // Keep track of transaction events to ensure all executables get announced events := make(chan NewTxsEvent, testTxPoolConfig.AccountQueue+5) @@ -750,7 +804,7 @@ func TestTransactionQueueAccountLimiting(t *testing.T) { defer pool.Stop() account := crypto.PubkeyToAddress(key.PublicKey) - pool.currentState.AddBalance(account, big.NewInt(1000000)) + testAddBalance(pool, account, big.NewInt(1000000)) // Keep queuing up transactions and make sure all above a limit are dropped for i := uint64(1); i <= testTxPoolConfig.AccountQueue+5; i++ { @@ -805,7 +859,7 @@ func testTransactionQueueGlobalLimiting(t *testing.T, nolocals bool) { keys := make([]*ecdsa.PrivateKey, 5) for i := 0; i < len(keys); i++ { keys[i], _ = crypto.GenerateKey() - pool.currentState.AddBalance(crypto.PubkeyToAddress(keys[i].PublicKey), big.NewInt(1000000)) + testAddBalance(pool, crypto.PubkeyToAddress(keys[i].PublicKey), big.NewInt(1000000)) } local := keys[len(keys)-1] @@ -897,8 +951,8 @@ func testTransactionQueueTimeLimiting(t *testing.T, nolocals bool) { local, _ := crypto.GenerateKey() remote, _ := crypto.GenerateKey() - pool.currentState.AddBalance(crypto.PubkeyToAddress(local.PublicKey), big.NewInt(1000000000)) - pool.currentState.AddBalance(crypto.PubkeyToAddress(remote.PublicKey), big.NewInt(1000000000)) + testAddBalance(pool, crypto.PubkeyToAddress(local.PublicKey), big.NewInt(1000000000)) + testAddBalance(pool, crypto.PubkeyToAddress(remote.PublicKey), big.NewInt(1000000000)) // Add the two transactions and ensure they both are queued up if err := pool.AddLocal(pricedTransaction(1, 100000, big.NewInt(1), local)); err != nil { @@ -1031,7 +1085,7 @@ func TestTransactionPendingLimiting(t *testing.T) { defer pool.Stop() account := crypto.PubkeyToAddress(key.PublicKey) - pool.currentState.AddBalance(account, big.NewInt(1000000)) + testAddBalance(pool, account, big.NewInt(1000000)) // Keep track of transaction events to ensure all executables get announced events := make(chan NewTxsEvent, testTxPoolConfig.AccountQueue+5) @@ -1081,7 +1135,7 @@ func TestTransactionPendingGlobalLimiting(t *testing.T) { keys := make([]*ecdsa.PrivateKey, 5) for i := 0; i < len(keys); i++ { keys[i], _ = crypto.GenerateKey() - pool.currentState.AddBalance(crypto.PubkeyToAddress(keys[i].PublicKey), big.NewInt(1000000)) + testAddBalance(pool, crypto.PubkeyToAddress(keys[i].PublicKey), big.NewInt(1000000)) } // Generate and queue a batch of transactions nonces := make(map[common.Address]uint64) @@ -1120,7 +1174,7 @@ func TestTransactionAllowedTxSize(t *testing.T) { defer pool.Stop() account := crypto.PubkeyToAddress(key.PublicKey) - pool.currentState.AddBalance(account, big.NewInt(1000000000)) + testAddBalance(pool, account, big.NewInt(1000000000)) // Compute maximal data size for transactions (lower bound). // @@ -1184,7 +1238,7 @@ func TestTransactionCapClearsFromAll(t *testing.T) { // Create a number of test accounts and fund them key, _ := crypto.GenerateKey() addr := crypto.PubkeyToAddress(key.PublicKey) - pool.currentState.AddBalance(addr, big.NewInt(1000000)) + testAddBalance(pool, addr, big.NewInt(1000000)) txs := types.Transactions{} for j := 0; j < int(config.GlobalSlots)*2; j++ { @@ -1217,7 +1271,7 @@ func TestTransactionPendingMinimumAllowance(t *testing.T) { keys := make([]*ecdsa.PrivateKey, 5) for i := 0; i < len(keys); i++ { keys[i], _ = crypto.GenerateKey() - pool.currentState.AddBalance(crypto.PubkeyToAddress(keys[i].PublicKey), big.NewInt(1000000)) + testAddBalance(pool, crypto.PubkeyToAddress(keys[i].PublicKey), big.NewInt(1000000)) } // Generate and queue a batch of transactions nonces := make(map[common.Address]uint64) @@ -1267,7 +1321,7 @@ func TestTransactionPoolRepricing(t *testing.T) { keys := make([]*ecdsa.PrivateKey, 4) for i := 0; i < len(keys); i++ { keys[i], _ = crypto.GenerateKey() - pool.currentState.AddBalance(crypto.PubkeyToAddress(keys[i].PublicKey), big.NewInt(1000000)) + testAddBalance(pool, crypto.PubkeyToAddress(keys[i].PublicKey), big.NewInt(1000000)) } // Generate and queue a batch of transactions, both pending and queued txs := types.Transactions{} @@ -1367,8 +1421,135 @@ func TestTransactionPoolRepricing(t *testing.T) { } } +// Tests that setting the transaction pool gas price to a higher value correctly +// discards everything cheaper (legacy & dynamic fee) than that and moves any +// gapped transactions back from the pending pool to the queue. +// +// Note, local transactions are never allowed to be dropped. +func TestTransactionPoolRepricingDynamicFee(t *testing.T) { + t.Parallel() + + // Create the pool to test the pricing enforcement with + pool, _ := setupTxPoolWithConfig(eip1559Config) + defer pool.Stop() + + // Keep track of transaction events to ensure all executables get announced + events := make(chan NewTxsEvent, 32) + sub := pool.txFeed.Subscribe(events) + defer sub.Unsubscribe() + + // Create a number of test accounts and fund them + keys := make([]*ecdsa.PrivateKey, 4) + for i := 0; i < len(keys); i++ { + keys[i], _ = crypto.GenerateKey() + testAddBalance(pool, crypto.PubkeyToAddress(keys[i].PublicKey), big.NewInt(1000000)) + } + // Generate and queue a batch of transactions, both pending and queued + txs := types.Transactions{} + + txs = append(txs, pricedTransaction(0, 100000, big.NewInt(2), keys[0])) + txs = append(txs, pricedTransaction(1, 100000, big.NewInt(1), keys[0])) + txs = append(txs, pricedTransaction(2, 100000, big.NewInt(2), keys[0])) + + txs = append(txs, dynamicFeeTx(0, 100000, big.NewInt(2), big.NewInt(1), keys[1])) + txs = append(txs, dynamicFeeTx(1, 100000, big.NewInt(3), big.NewInt(2), keys[1])) + txs = append(txs, dynamicFeeTx(2, 100000, big.NewInt(3), big.NewInt(2), keys[1])) + + txs = append(txs, dynamicFeeTx(1, 100000, big.NewInt(2), big.NewInt(2), keys[2])) + txs = append(txs, dynamicFeeTx(2, 100000, big.NewInt(1), big.NewInt(1), keys[2])) + txs = append(txs, dynamicFeeTx(3, 100000, big.NewInt(2), big.NewInt(2), keys[2])) + + ltx := dynamicFeeTx(0, 100000, big.NewInt(2), big.NewInt(1), keys[3]) + + // Import the batch and that both pending and queued transactions match up + pool.AddRemotesSync(txs) + pool.AddLocal(ltx) + + pending, queued := pool.Stats() + if pending != 7 { + t.Fatalf("pending transactions mismatched: have %d, want %d", pending, 7) + } + if queued != 3 { + t.Fatalf("queued transactions mismatched: have %d, want %d", queued, 3) + } + if err := validateEvents(events, 7); err != nil { + t.Fatalf("original event firing failed: %v", err) + } + if err := validateTxPoolInternals(pool); err != nil { + t.Fatalf("pool internal state corrupted: %v", err) + } + // Reprice the pool and check that underpriced transactions get dropped + pool.SetGasPrice(big.NewInt(2)) + + pending, queued = pool.Stats() + if pending != 2 { + t.Fatalf("pending transactions mismatched: have %d, want %d", pending, 2) + } + if queued != 5 { + t.Fatalf("queued transactions mismatched: have %d, want %d", queued, 5) + } + if err := validateEvents(events, 0); err != nil { + t.Fatalf("reprice event firing failed: %v", err) + } + if err := validateTxPoolInternals(pool); err != nil { + t.Fatalf("pool internal state corrupted: %v", err) + } + // Check that we can't add the old transactions back + tx := pricedTransaction(1, 100000, big.NewInt(1), keys[0]) + if err := pool.AddRemote(tx); err != ErrUnderpriced { + t.Fatalf("adding underpriced pending transaction error mismatch: have %v, want %v", err, ErrUnderpriced) + } + tx = dynamicFeeTx(0, 100000, big.NewInt(2), big.NewInt(1), keys[1]) + if err := pool.AddRemote(tx); err != ErrUnderpriced { + t.Fatalf("adding underpriced pending transaction error mismatch: have %v, want %v", err, ErrUnderpriced) + } + tx = dynamicFeeTx(2, 100000, big.NewInt(1), big.NewInt(1), keys[2]) + if err := pool.AddRemote(tx); err != ErrUnderpriced { + t.Fatalf("adding underpriced queued transaction error mismatch: have %v, want %v", err, ErrUnderpriced) + } + if err := validateEvents(events, 0); err != nil { + t.Fatalf("post-reprice event firing failed: %v", err) + } + if err := validateTxPoolInternals(pool); err != nil { + t.Fatalf("pool internal state corrupted: %v", err) + } + // However we can add local underpriced transactions + tx = dynamicFeeTx(1, 100000, big.NewInt(1), big.NewInt(1), keys[3]) + if err := pool.AddLocal(tx); err != nil { + t.Fatalf("failed to add underpriced local transaction: %v", err) + } + if pending, _ = pool.Stats(); pending != 3 { + t.Fatalf("pending transactions mismatched: have %d, want %d", pending, 3) + } + if err := validateEvents(events, 1); err != nil { + t.Fatalf("post-reprice local event firing failed: %v", err) + } + if err := validateTxPoolInternals(pool); err != nil { + t.Fatalf("pool internal state corrupted: %v", err) + } + // And we can fill gaps with properly priced transactions + tx = pricedTransaction(1, 100000, big.NewInt(2), keys[0]) + if err := pool.AddRemote(tx); err != nil { + t.Fatalf("failed to add pending transaction: %v", err) + } + tx = dynamicFeeTx(0, 100000, big.NewInt(3), big.NewInt(2), keys[1]) + if err := pool.AddRemote(tx); err != nil { + t.Fatalf("failed to add pending transaction: %v", err) + } + tx = dynamicFeeTx(2, 100000, big.NewInt(2), big.NewInt(2), keys[2]) + if err := pool.AddRemote(tx); err != nil { + t.Fatalf("failed to add queued transaction: %v", err) + } + if err := validateEvents(events, 5); err != nil { + t.Fatalf("post-reprice event firing failed: %v", err) + } + if err := validateTxPoolInternals(pool); err != nil { + t.Fatalf("pool internal state corrupted: %v", err) + } +} + // Tests that setting the transaction pool gas price to a higher value does not -// remove local transactions. +// remove local transactions (legacy & dynamic fee). func TestTransactionPoolRepricingKeepsLocals(t *testing.T) { t.Parallel() @@ -1376,14 +1557,14 @@ func TestTransactionPoolRepricingKeepsLocals(t *testing.T) { statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) blockchain := &testBlockChain{statedb, 1000000, new(event.Feed)} - pool := NewTxPool(testTxPoolConfig, params.TestChainConfig, blockchain) + pool := NewTxPool(testTxPoolConfig, eip1559Config, blockchain) defer pool.Stop() // Create a number of test accounts and fund them keys := make([]*ecdsa.PrivateKey, 3) for i := 0; i < len(keys); i++ { keys[i], _ = crypto.GenerateKey() - pool.currentState.AddBalance(crypto.PubkeyToAddress(keys[i].PublicKey), big.NewInt(1000*1000000)) + testAddBalance(pool, crypto.PubkeyToAddress(keys[i].PublicKey), big.NewInt(1000*1000000)) } // Create transaction (both pending and queued) with a linearly growing gasprice for i := uint64(0); i < 500; i++ { @@ -1397,9 +1578,20 @@ func TestTransactionPoolRepricingKeepsLocals(t *testing.T) { if err := pool.AddLocal(queuedTx); err != nil { t.Fatal(err) } + + // Add pending dynamic fee transaction. + pendingTx = dynamicFeeTx(i, 100000, big.NewInt(int64(i)+1), big.NewInt(int64(i)), keys[1]) + if err := pool.AddLocal(pendingTx); err != nil { + t.Fatal(err) + } + // Add queued dynamic fee transaction. + queuedTx = dynamicFeeTx(i+501, 100000, big.NewInt(int64(i)+1), big.NewInt(int64(i)), keys[1]) + if err := pool.AddLocal(queuedTx); err != nil { + t.Fatal(err) + } } pending, queued := pool.Stats() - expPending, expQueued := 500, 500 + expPending, expQueued := 1000, 1000 validate := func() { pending, queued = pool.Stats() if pending != expPending { @@ -1454,7 +1646,7 @@ func TestTransactionPoolUnderpricing(t *testing.T) { keys := make([]*ecdsa.PrivateKey, 4) for i := 0; i < len(keys); i++ { keys[i], _ = crypto.GenerateKey() - pool.currentState.AddBalance(crypto.PubkeyToAddress(keys[i].PublicKey), big.NewInt(1000000)) + testAddBalance(pool, crypto.PubkeyToAddress(keys[i].PublicKey), big.NewInt(1000000)) } // Generate and queue a batch of transactions, both pending and queued txs := types.Transactions{} @@ -1560,7 +1752,7 @@ func TestTransactionPoolStableUnderpricing(t *testing.T) { keys := make([]*ecdsa.PrivateKey, 2) for i := 0; i < len(keys); i++ { keys[i], _ = crypto.GenerateKey() - pool.currentState.AddBalance(crypto.PubkeyToAddress(keys[i].PublicKey), big.NewInt(1000000)) + testAddBalance(pool, crypto.PubkeyToAddress(keys[i].PublicKey), big.NewInt(1000000)) } // Fill up the entire queue with the same transaction price points txs := types.Transactions{} @@ -1601,6 +1793,173 @@ func TestTransactionPoolStableUnderpricing(t *testing.T) { } } +// Tests that when the pool reaches its global transaction limit, underpriced +// transactions (legacy & dynamic fee) are gradually shifted out for more +// expensive ones and any gapped pending transactions are moved into the queue. +// +// Note, local transactions are never allowed to be dropped. +func TestTransactionPoolUnderpricingDynamicFee(t *testing.T) { + t.Parallel() + + pool, _ := setupTxPoolWithConfig(eip1559Config) + defer pool.Stop() + + pool.config.GlobalSlots = 2 + pool.config.GlobalQueue = 2 + + // Keep track of transaction events to ensure all executables get announced + events := make(chan NewTxsEvent, 32) + sub := pool.txFeed.Subscribe(events) + defer sub.Unsubscribe() + + // Create a number of test accounts and fund them + keys := make([]*ecdsa.PrivateKey, 4) + for i := 0; i < len(keys); i++ { + keys[i], _ = crypto.GenerateKey() + testAddBalance(pool, crypto.PubkeyToAddress(keys[i].PublicKey), big.NewInt(1000000)) + } + + // Generate and queue a batch of transactions, both pending and queued + txs := types.Transactions{} + + txs = append(txs, dynamicFeeTx(0, 100000, big.NewInt(3), big.NewInt(2), keys[0])) + txs = append(txs, pricedTransaction(1, 100000, big.NewInt(2), keys[0])) + txs = append(txs, dynamicFeeTx(1, 100000, big.NewInt(2), big.NewInt(1), keys[1])) + + ltx := dynamicFeeTx(0, 100000, big.NewInt(2), big.NewInt(1), keys[2]) + + // Import the batch and that both pending and queued transactions match up + pool.AddRemotes(txs) // Pend K0:0, K0:1; Que K1:1 + pool.AddLocal(ltx) // +K2:0 => Pend K0:0, K0:1, K2:0; Que K1:1 + + pending, queued := pool.Stats() + if pending != 3 { + t.Fatalf("pending transactions mismatched: have %d, want %d", pending, 3) + } + if queued != 1 { + t.Fatalf("queued transactions mismatched: have %d, want %d", queued, 1) + } + if err := validateEvents(events, 3); err != nil { + t.Fatalf("original event firing failed: %v", err) + } + if err := validateTxPoolInternals(pool); err != nil { + t.Fatalf("pool internal state corrupted: %v", err) + } + + // Ensure that adding an underpriced transaction fails + tx := dynamicFeeTx(0, 100000, big.NewInt(2), big.NewInt(1), keys[1]) + if err := pool.AddRemote(tx); err != ErrUnderpriced { // Pend K0:0, K0:1, K2:0; Que K1:1 + t.Fatalf("adding underpriced pending transaction error mismatch: have %v, want %v", err, ErrUnderpriced) + } + + // Ensure that adding high priced transactions drops cheap ones, but not own + tx = pricedTransaction(0, 100000, big.NewInt(2), keys[1]) + if err := pool.AddRemote(tx); 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) + } + + tx = pricedTransaction(2, 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 + t.Fatalf("failed to add well priced transaction: %v", err) + } + tx = dynamicFeeTx(3, 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 + t.Fatalf("failed to add well priced transaction: %v", err) + } + pending, queued = pool.Stats() + if pending != 2 { + t.Fatalf("pending transactions mismatched: have %d, want %d", pending, 2) + } + if queued != 2 { + t.Fatalf("queued transactions mismatched: have %d, want %d", queued, 2) + } + if err := validateEvents(events, 1); err != nil { + t.Fatalf("additional event firing failed: %v", err) + } + if err := validateTxPoolInternals(pool); err != nil { + t.Fatalf("pool internal state corrupted: %v", err) + } + // Ensure that adding local transactions can push out even higher priced ones + ltx = dynamicFeeTx(1, 100000, big.NewInt(0), big.NewInt(0), keys[2]) + if err := pool.AddLocal(ltx); err != nil { + t.Fatalf("failed to append underpriced local transaction: %v", err) + } + ltx = dynamicFeeTx(0, 100000, big.NewInt(0), big.NewInt(0), keys[3]) + if err := pool.AddLocal(ltx); err != nil { + t.Fatalf("failed to add new underpriced local transaction: %v", err) + } + pending, queued = pool.Stats() + if pending != 3 { + t.Fatalf("pending transactions mismatched: have %d, want %d", pending, 3) + } + if queued != 1 { + t.Fatalf("queued transactions mismatched: have %d, want %d", queued, 1) + } + if err := validateEvents(events, 2); err != nil { + t.Fatalf("local event firing failed: %v", err) + } + if err := validateTxPoolInternals(pool); err != nil { + t.Fatalf("pool internal state corrupted: %v", err) + } +} + +// Tests whether highest fee cap transaction is retained after a batch of high effective +// tip transactions are added and vice versa +func TestDualHeapEviction(t *testing.T) { + t.Parallel() + + pool, _ := setupTxPoolWithConfig(eip1559Config) + defer pool.Stop() + + pool.config.GlobalSlots = 10 + pool.config.GlobalQueue = 10 + + var ( + highTip, highCap *types.Transaction + baseFee int + ) + + check := func(tx *types.Transaction, name string) { + if pool.all.GetRemote(tx.Hash()) == nil { + t.Fatalf("highest %s transaction evicted from the pool", name) + } + } + + add := func(urgent bool) { + txs := make([]*types.Transaction, 20) + for i := range txs { + // Create a test accounts and fund it + key, _ := crypto.GenerateKey() + testAddBalance(pool, crypto.PubkeyToAddress(key.PublicKey), big.NewInt(1000000000000)) + if urgent { + txs[i] = dynamicFeeTx(0, 100000, big.NewInt(int64(baseFee+1+i)), big.NewInt(int64(1+i)), key) + highTip = txs[i] + } else { + txs[i] = dynamicFeeTx(0, 100000, big.NewInt(int64(baseFee+200+i)), big.NewInt(1), key) + highCap = txs[i] + } + } + pool.AddRemotes(txs) + pending, queued := pool.Stats() + if pending+queued != 20 { + t.Fatalf("transaction count mismatch: have %d, want %d", pending+queued, 10) + } + } + + add(false) + for baseFee = 0; baseFee <= 1000; baseFee += 100 { + pool.priced.SetBaseFee(big.NewInt(int64(baseFee))) + add(true) + check(highCap, "fee cap") + add(false) + check(highTip, "effective tip") + } + + if err := validateTxPoolInternals(pool); err != nil { + t.Fatalf("pool internal state corrupted: %v", err) + } +} + // Tests that the pool rejects duplicate transactions. func TestTransactionDeduplication(t *testing.T) { t.Parallel() @@ -1614,7 +1973,7 @@ func TestTransactionDeduplication(t *testing.T) { // Create a test account to add transactions with key, _ := crypto.GenerateKey() - pool.currentState.AddBalance(crypto.PubkeyToAddress(key.PublicKey), big.NewInt(1000000000)) + testAddBalance(pool, crypto.PubkeyToAddress(key.PublicKey), big.NewInt(1000000000)) // Create a batch of transactions and add a few of them txs := make([]*types.Transaction, 16) @@ -1685,7 +2044,7 @@ func TestTransactionReplacement(t *testing.T) { // Create a test account to add transactions with key, _ := crypto.GenerateKey() - pool.currentState.AddBalance(crypto.PubkeyToAddress(key.PublicKey), big.NewInt(1000000000)) + testAddBalance(pool, crypto.PubkeyToAddress(key.PublicKey), big.NewInt(1000000000)) // Add pending transactions, ensuring the minimum price bump is enforced for replacement (for ultra low prices too) price := int64(100) @@ -1746,6 +2105,116 @@ func TestTransactionReplacement(t *testing.T) { } } +// Tests that the pool rejects replacement dynamic fee transactions that don't +// meet the minimum price bump required. +func TestTransactionReplacementDynamicFee(t *testing.T) { + t.Parallel() + + // Create the pool to test the pricing enforcement with + pool, key := setupTxPoolWithConfig(eip1559Config) + defer pool.Stop() + testAddBalance(pool, crypto.PubkeyToAddress(key.PublicKey), big.NewInt(1000000000)) + + // Keep track of transaction events to ensure all executables get announced + events := make(chan NewTxsEvent, 32) + sub := pool.txFeed.Subscribe(events) + defer sub.Unsubscribe() + + // Add pending transactions, ensuring the minimum price bump is enforced for replacement (for ultra low prices too) + feeCap := int64(100) + feeCapThreshold := (feeCap * (100 + int64(testTxPoolConfig.PriceBump))) / 100 + tip := int64(60) + tipThreshold := (tip * (100 + int64(testTxPoolConfig.PriceBump))) / 100 + + // Run the following identical checks for both the pending and queue pools: + // 1. Send initial tx => accept + // 2. Don't bump tip or fee cap => discard + // 3. Bump both more than min => accept + // 4. Check events match expected (2 new executable txs during pending, 0 during queue) + // 5. Send new tx with larger tip and feeCap => accept + // 6. Bump tip max allowed so it's still underpriced => discard + // 7. Bump fee cap max allowed so it's still underpriced => discard + // 8. Bump tip min for acceptance => discard + // 9. Bump feecap min for acceptance => discard + // 10. Bump feecap and tip min for acceptance => accept + // 11. Check events match expected (2 new executable txs during pending, 0 during queue) + stages := []string{"pending", "queued"} + for _, stage := range stages { + // Since state is empty, 0 nonce txs are "executable" and can go + // into pending immediately. 2 nonce txs are "happed + nonce := uint64(0) + if stage == "queued" { + nonce = 2 + } + + // 1. Send initial tx => accept + tx := dynamicFeeTx(nonce, 100000, big.NewInt(2), big.NewInt(1), key) + if err := pool.addRemoteSync(tx); err != nil { + t.Fatalf("failed to add original cheap %s transaction: %v", stage, err) + } + // 2. Don't bump tip or feecap => discard + tx = dynamicFeeTx(nonce, 100001, big.NewInt(2), big.NewInt(1), key) + if err := pool.AddRemote(tx); err != ErrReplaceUnderpriced { + t.Fatalf("original cheap %s transaction replacement error mismatch: have %v, want %v", stage, err, ErrReplaceUnderpriced) + } + // 3. Bump both more than min => accept + tx = dynamicFeeTx(nonce, 100000, big.NewInt(3), big.NewInt(2), key) + if err := pool.AddRemote(tx); err != nil { + t.Fatalf("failed to replace original cheap %s transaction: %v", stage, err) + } + // 4. Check events match expected (2 new executable txs during pending, 0 during queue) + count := 2 + if stage == "queued" { + count = 0 + } + if err := validateEvents(events, count); err != nil { + t.Fatalf("cheap %s replacement event firing failed: %v", stage, err) + } + // 5. Send new tx with larger tip and feeCap => accept + tx = dynamicFeeTx(nonce, 100000, big.NewInt(feeCap), big.NewInt(tip), key) + if err := pool.addRemoteSync(tx); err != nil { + t.Fatalf("failed to add original proper %s transaction: %v", stage, err) + } + // 6. Bump tip max allowed so it's still underpriced => discard + tx = dynamicFeeTx(nonce, 100000, big.NewInt(feeCap), big.NewInt(tipThreshold-1), key) + if err := pool.AddRemote(tx); err != ErrReplaceUnderpriced { + t.Fatalf("original proper %s transaction replacement error mismatch: have %v, want %v", stage, err, ErrReplaceUnderpriced) + } + // 7. Bump fee cap max allowed so it's still underpriced => discard + tx = dynamicFeeTx(nonce, 100000, big.NewInt(feeCapThreshold-1), big.NewInt(tip), key) + if err := pool.AddRemote(tx); err != ErrReplaceUnderpriced { + t.Fatalf("original proper %s transaction replacement error mismatch: have %v, want %v", stage, err, ErrReplaceUnderpriced) + } + // 8. Bump tip min for acceptance => accept + tx = dynamicFeeTx(nonce, 100000, big.NewInt(feeCap), big.NewInt(tipThreshold), key) + if err := pool.AddRemote(tx); err != ErrReplaceUnderpriced { + t.Fatalf("original proper %s transaction replacement error mismatch: have %v, want %v", stage, err, ErrReplaceUnderpriced) + } + // 9. Bump fee cap min for acceptance => accept + tx = dynamicFeeTx(nonce, 100000, big.NewInt(feeCapThreshold), big.NewInt(tip), key) + if err := pool.AddRemote(tx); err != ErrReplaceUnderpriced { + t.Fatalf("original proper %s transaction replacement error mismatch: have %v, want %v", stage, err, ErrReplaceUnderpriced) + } + // 10. Check events match expected (3 new executable txs during pending, 0 during queue) + tx = dynamicFeeTx(nonce, 100000, big.NewInt(feeCapThreshold), big.NewInt(tipThreshold), key) + if err := pool.AddRemote(tx); err != nil { + t.Fatalf("failed to replace original cheap %s transaction: %v", stage, err) + } + // 11. Check events match expected (3 new executable txs during pending, 0 during queue) + count = 2 + if stage == "queued" { + count = 0 + } + if err := validateEvents(events, count); err != nil { + t.Fatalf("replacement %s event firing failed: %v", stage, err) + } + } + + if err := validateTxPoolInternals(pool); err != nil { + t.Fatalf("pool internal state corrupted: %v", err) + } +} + // Tests that local transactions are journaled to disk, but remote transactions // get discarded between restarts. func TestTransactionJournaling(t *testing.T) { testTransactionJournaling(t, false) } @@ -1781,8 +2250,8 @@ func testTransactionJournaling(t *testing.T, nolocals bool) { local, _ := crypto.GenerateKey() remote, _ := crypto.GenerateKey() - pool.currentState.AddBalance(crypto.PubkeyToAddress(local.PublicKey), big.NewInt(1000000000)) - pool.currentState.AddBalance(crypto.PubkeyToAddress(remote.PublicKey), big.NewInt(1000000000)) + testAddBalance(pool, crypto.PubkeyToAddress(local.PublicKey), big.NewInt(1000000000)) + testAddBalance(pool, crypto.PubkeyToAddress(remote.PublicKey), big.NewInt(1000000000)) // Add three local and a remote transactions and ensure they are queued up if err := pool.AddLocal(pricedTransaction(0, 100000, big.NewInt(1), local)); err != nil { @@ -1875,7 +2344,7 @@ func TestTransactionStatusCheck(t *testing.T) { keys := make([]*ecdsa.PrivateKey, 3) for i := 0; i < len(keys); i++ { keys[i], _ = crypto.GenerateKey() - pool.currentState.AddBalance(crypto.PubkeyToAddress(keys[i].PublicKey), big.NewInt(1000000)) + testAddBalance(pool, crypto.PubkeyToAddress(keys[i].PublicKey), big.NewInt(1000000)) } // Generate and queue a batch of transactions, both pending and queued txs := types.Transactions{} @@ -1945,7 +2414,7 @@ func benchmarkPendingDemotion(b *testing.B, size int) { defer pool.Stop() account := crypto.PubkeyToAddress(key.PublicKey) - pool.currentState.AddBalance(account, big.NewInt(1000000)) + testAddBalance(pool, account, big.NewInt(1000000)) for i := 0; i < size; i++ { tx := transaction(uint64(i), 100000, key) @@ -1970,7 +2439,7 @@ func benchmarkFuturePromotion(b *testing.B, size int) { defer pool.Stop() account := crypto.PubkeyToAddress(key.PublicKey) - pool.currentState.AddBalance(account, big.NewInt(1000000)) + testAddBalance(pool, account, big.NewInt(1000000)) for i := 0; i < size; i++ { tx := transaction(uint64(1+i), 100000, key) @@ -1998,7 +2467,7 @@ func benchmarkPoolBatchInsert(b *testing.B, size int, local bool) { defer pool.Stop() account := crypto.PubkeyToAddress(key.PublicKey) - pool.currentState.AddBalance(account, big.NewInt(1000000)) + testAddBalance(pool, account, big.NewInt(1000000)) batches := make([]types.Transactions, b.N) for i := 0; i < b.N; i++ { @@ -2039,13 +2508,13 @@ func BenchmarkInsertRemoteWithAllLocals(b *testing.B) { for i := 0; i < b.N; i++ { b.StopTimer() pool, _ := setupTxPool() - pool.currentState.AddBalance(account, big.NewInt(100000000)) + testAddBalance(pool, account, big.NewInt(100000000)) for _, local := range locals { pool.AddLocal(local) } b.StartTimer() // Assign a high enough balance for testing - pool.currentState.AddBalance(remoteAddr, big.NewInt(100000000)) + testAddBalance(pool, remoteAddr, big.NewInt(100000000)) for i := 0; i < len(remotes); i++ { pool.AddRemotes([]*types.Transaction{remotes[i]}) } diff --git a/core/types/transaction.go b/core/types/transaction.go index 5347fdab8..37f67f034 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -300,33 +300,60 @@ func (tx *Transaction) Cost() *big.Int { return total } -// EffectiveTip returns the effective miner tip for the given base fee. -// Returns error in case of a negative effective miner tip. -func (tx *Transaction) EffectiveTip(baseFee *big.Int) (*big.Int, error) { - if baseFee == nil { - return tx.Tip(), nil - } - feeCap := tx.FeeCap() - if feeCap.Cmp(baseFee) == -1 { - return nil, ErrFeeCapTooLow - } - return math.BigMin(tx.Tip(), feeCap.Sub(feeCap, baseFee)), nil -} - // RawSignatureValues returns the V, R, S signature values of the transaction. // The return values should not be modified by the caller. func (tx *Transaction) RawSignatureValues() (v, r, s *big.Int) { return tx.inner.rawSignatureValues() } -// GasPriceCmp compares the gas prices of two transactions. -func (tx *Transaction) GasPriceCmp(other *Transaction) int { - return tx.inner.gasPrice().Cmp(other.inner.gasPrice()) +// FeeCapCmp compares the fee cap of two transactions. +func (tx *Transaction) FeeCapCmp(other *Transaction) int { + return tx.inner.feeCap().Cmp(other.inner.feeCap()) } -// GasPriceIntCmp compares the gas price of the transaction against the given price. -func (tx *Transaction) GasPriceIntCmp(other *big.Int) int { - return tx.inner.gasPrice().Cmp(other) +// FeeCapIntCmp compares the fee cap of the transaction against the given fee cap. +func (tx *Transaction) FeeCapIntCmp(other *big.Int) int { + return tx.inner.feeCap().Cmp(other) +} + +// TipCmp compares the tip of two transactions. +func (tx *Transaction) TipCmp(other *Transaction) int { + return tx.inner.tip().Cmp(other.inner.tip()) +} + +// TipIntCmp compares the tip of the transaction against the given tip. +func (tx *Transaction) TipIntCmp(other *big.Int) int { + return tx.inner.tip().Cmp(other) +} + +// EffectiveTip returns the effective miner tip for the given base fee. +// Note: if the effective tip is negative, this method returns both error +// the actual negative value, _and_ ErrFeeCapTooLow +func (tx *Transaction) EffectiveTip(baseFee *big.Int) (*big.Int, error) { + if baseFee == nil { + return tx.Tip(), nil + } + var err error + feeCap := tx.FeeCap() + if feeCap.Cmp(baseFee) == -1 { + err = ErrFeeCapTooLow + } + return math.BigMin(tx.Tip(), feeCap.Sub(feeCap, baseFee)), err +} + +// EffectiveTipValue is identical to EffectiveTip, but does not return an +// error in case the effective tip is negative +func (tx *Transaction) EffectiveTipValue(baseFee *big.Int) *big.Int { + effectiveTip, _ := tx.EffectiveTip(baseFee) + return effectiveTip +} + +// EffectiveTipCmp compares the effective tip of two transactions assuming the given base fee. +func (tx *Transaction) EffectiveTipCmp(other *Transaction, baseFee *big.Int) int { + if baseFee == nil { + return tx.TipCmp(other) + } + return tx.EffectiveTipValue(baseFee).Cmp(other.EffectiveTipValue(baseFee)) } // Hash returns the transaction hash. diff --git a/eth/gasprice/gasprice.go b/eth/gasprice/gasprice.go index 566c954ec..9087e2920 100644 --- a/eth/gasprice/gasprice.go +++ b/eth/gasprice/gasprice.go @@ -188,7 +188,7 @@ type transactionsByGasPrice []*types.Transaction func (t transactionsByGasPrice) Len() int { return len(t) } func (t transactionsByGasPrice) Swap(i, j int) { t[i], t[j] = t[j], t[i] } -func (t transactionsByGasPrice) Less(i, j int) bool { return t[i].GasPriceCmp(t[j]) < 0 } +func (t transactionsByGasPrice) Less(i, j int) bool { return t[i].FeeCapCmp(t[j]) < 0 } // getBlockPrices calculates the lowest transaction gas price in a given block // and sends it to the result channel. If the block is empty or all transactions @@ -210,7 +210,7 @@ func (gpo *Oracle) getBlockPrices(ctx context.Context, signer types.Signer, bloc var prices []*big.Int for _, tx := range txs { - if ignoreUnder != nil && tx.GasPriceIntCmp(ignoreUnder) == -1 { + if ignoreUnder != nil && tx.GasPrice().Cmp(ignoreUnder) == -1 { continue } sender, err := types.Sender(signer, tx)