diff --git a/core/tx_list.go b/core/tx_list.go index 3e746ff3518e..fd2985af7a95 100644 --- a/core/tx_list.go +++ b/core/tx_list.go @@ -23,6 +23,7 @@ import ( "sort" "sync" "sync/atomic" + "time" "github.com/XinFinOrg/XDPoSChain/common" "github.com/XinFinOrg/XDPoSChain/core/types" @@ -284,15 +285,23 @@ func (l *txList) Add(tx *types.Transaction, priceBump uint64) (bool, *types.Tran return false, nil } 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 } } @@ -417,33 +426,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() + } +} + +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 } - // If the prices match, stabilize via nonces (high nonce is worse) - return h[i].Nonce() > h[j].Nonce() + // 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] - *h = old[0 : n-1] + old[n-1] = nil + h.list = old[0 : n-1] return x } @@ -451,18 +481,30 @@ 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 int64 // Number of stale price points to (re-heap trigger) - reheapMu sync.Mutex // Mutex asserts that only one routine is reheaping the list + all *txLookup // Pointer to the map of all transactions + urgent, floating priceHeap // Heaps of prices of all the stored **remote** transactions + stales int64 // Number of stale price points to (re-heap trigger) + reheapMu sync.Mutex // Mutex asserts that only one routine is reheaping the list } +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, } } @@ -471,7 +513,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 @@ -480,58 +523,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%) stales := atomic.AddInt64(&l.stales, int64(count)) - if int(stales) <= len(*l.remotes)/4 { + if int(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 atomic.AddInt64(&l.stales, -1) - 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 @@ -540,21 +568,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 - atomic.AddInt64(&l.stales, -1) - 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 + atomic.AddInt64(&l.stales, -1) + 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 + atomic.AddInt64(&l.stales, -1) + 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 } @@ -565,13 +608,32 @@ func (l *txPricedList) Discard(slots int, force bool) (types.Transactions, bool) func (l *txPricedList) Reheap() { l.reheapMu.Lock() defer l.reheapMu.Unlock() - reheap := make(priceHeap, 0, l.all.RemoteCount()) - + start := time.Now() atomic.StoreInt64(&l.stales, 0) - l.remotes = &reheap + 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 813c9a1a86f6..795a84d6e8d1 100644 --- a/core/tx_pool.go +++ b/core/tx_pool.go @@ -106,6 +106,10 @@ var ( ErrDuplicateSpecialTransaction = errors.New("duplicate a special transaction") ErrMinDeploySMC = errors.New("smart contract creation cost is under allowance") + + // 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 ( @@ -138,6 +142,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. @@ -199,7 +205,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, @@ -262,6 +268,9 @@ type TxPool struct { signer types.Signer mu sync.RWMutex + 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 currentMaxGas uint64 // Current gas limit for transaction caps @@ -285,7 +294,6 @@ type TxPool struct { wg sync.WaitGroup // tracks loop, scheduleReorgLoop initDoneCh chan struct{} // is closed once the pool is initialized (for tests) - eip2718 bool // Fork indicator whether we are using EIP-2718 type transactions. IsSigner func(address common.Address) bool trc21FeeCapacity map[common.Address]*big.Int } @@ -466,10 +474,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) } @@ -575,6 +591,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 @@ -596,13 +616,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 { if !tx.IsSpecialTransaction() || (pool.IsSigner != nil && !pool.IsSigner(from)) { return ErrUnderpriced } @@ -712,10 +736,9 @@ func (pool *TxPool) add(tx *types.Transaction, local bool) (replaced bool, err e } // If the transaction pool is full, discard underpriced transactions if uint64(pool.all.Slots()+numSlots(tx)) > pool.config.GlobalSlots+pool.config.GlobalQueue { - log.Debug("Add transaction to pool full", "hash", hash, "nonce", tx.Nonce()) // 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 } @@ -732,7 +755,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) } @@ -1243,6 +1266,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() @@ -1360,6 +1386,7 @@ func (pool *TxPool) reset(oldHead, newHead *types.Header) { // Update all fork indicator by next pending block number. next := new(big.Int).Add(newHead.Number, big.NewInt(1)) pool.eip2718 = pool.chainconfig.IsEIP1559(next) + pool.eip1559 = pool.chainconfig.IsEIP1559(next) } // promoteExecutables moves transactions that have become processable from the @@ -1572,6 +1599,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 { @@ -1877,6 +1908,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 62e1d70a61da..f73d6f92bc8c 100644 --- a/core/tx_pool_test.go +++ b/core/tx_pool_test.go @@ -36,13 +36,23 @@ import ( "github.com/XinFinOrg/XDPoSChain/params" ) -// 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.Eip1559Block = common.Big0 } type testBlockChain struct { @@ -102,13 +112,32 @@ 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) { diskdb := rawdb.NewMemoryDatabase() statedb, _ := state.New(common.Hash{}, state.NewDatabase(diskdb)) blockchain := &testBlockChain{statedb, 10000000, new(event.Feed)} key, _ := crypto.GenerateKey() - pool := NewTxPool(testTxPoolConfig, params.TestChainConfig, blockchain) + pool := NewTxPool(testTxPoolConfig, config, blockchain) // wait for the pool to initialize <-pool.initDoneCh @@ -126,7 +155,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) } @@ -253,6 +282,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() @@ -262,19 +303,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); 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); 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); err != ErrNonceTooLow { t.Error("expected", ErrNonceTooLow) @@ -298,7 +339,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) @@ -309,7 +350,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)) @@ -332,7 +373,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) @@ -356,12 +397,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() @@ -451,7 +505,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) @@ -475,8 +529,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) @@ -484,7 +538,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) @@ -501,7 +555,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 ( @@ -549,7 +603,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 { @@ -616,7 +670,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{} @@ -659,7 +713,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) @@ -720,7 +774,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) @@ -774,7 +828,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)) testTxPoolConfig.AccountQueue = 10 // Keep queuing up transactions and make sure all above a limit are dropped for i := uint64(1); i <= testTxPoolConfig.AccountQueue; i++ { @@ -831,7 +885,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] @@ -923,8 +977,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 { @@ -1057,7 +1111,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)) testTxPoolConfig.AccountQueue = 10 // Keep track of transaction events to ensure all executables get announced events := make(chan NewTxsEvent, testTxPoolConfig.AccountQueue) @@ -1108,7 +1162,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) @@ -1147,7 +1201,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). // @@ -1212,7 +1266,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++ { @@ -1246,7 +1300,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) @@ -1297,7 +1351,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{} @@ -1397,8 +1451,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() @@ -1407,30 +1588,42 @@ func TestTransactionPoolRepricingKeepsLocals(t *testing.T) { statedb, _ := state.New(common.Hash{}, state.NewDatabase(db)) 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 + // common.LimitThresholdNonceInQueue = 10 for i := uint64(0); i < 5; i++ { - // Add pending - p_tx := pricedTransaction(i, 100000, big.NewInt(int64(i+1)), keys[2]) - if err := pool.AddLocal(p_tx); err != nil { + // Add pending transaction. + pendingTx := pricedTransaction(i, 100000, big.NewInt(int64(i+1)), keys[2]) + if err := pool.AddLocal(pendingTx); err != nil { t.Fatal(err) } - // Add queued - q_tx := pricedTransaction(i+6, 100000, big.NewInt(int64(i+1)), keys[2]) - if err := pool.AddLocal(q_tx); err != nil { + // Add queued transaction. + queuedTx := pricedTransaction(i+6, 100000, big.NewInt(int64(i+1)), keys[2]) + 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+6, 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 := 5, 5 + expPending, expQueued := 10, 10 validate := func() { pending, queued = pool.Stats() if pending != expPending { @@ -1486,7 +1679,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{} @@ -1594,7 +1787,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{} @@ -1635,6 +1828,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(1), 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(1), 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, 20) + } + } + + 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() @@ -1648,7 +2008,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, common.LimitThresholdNonceInQueue) @@ -1720,7 +2080,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) @@ -1781,6 +2141,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) } @@ -1817,8 +2287,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 { @@ -1912,7 +2382,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{} @@ -1982,7 +2452,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) @@ -2007,7 +2477,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) @@ -2035,7 +2505,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++ { @@ -2076,13 +2546,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 f188bb5dff63..a944cc4c858f 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -321,33 +321,67 @@ func (tx *Transaction) From() *common.Address { return &from } +// Cost returns gas * gasPrice + value. +func (tx *Transaction) Cost() *big.Int { + total := new(big.Int).Mul(tx.GasPrice(), new(big.Int).SetUint64(tx.Gas())) + total.Add(total, tx.Value()) + return total +} + +// 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() +} + +// FeeCapCmp compares the fee cap of two transactions. +func (tx *Transaction) FeeCapCmp(other *Transaction) int { + return tx.inner.feeCap().Cmp(other.inner.feeCap()) +} + +// 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. -// Returns error in case of a negative effective miner tip. +// 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 { - return nil, ErrFeeCapTooLow + err = 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() + return math.BigMin(tx.Tip(), feeCap.Sub(feeCap, baseFee)), err } -// GasPriceCmp compares the gas prices of two transactions. -func (tx *Transaction) GasPriceCmp(other *Transaction) int { - return tx.inner.gasPrice().Cmp(other.inner.gasPrice()) +// 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 } -// 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) +// 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. @@ -427,13 +461,6 @@ func (tx *Transaction) WithSignature(signer Signer, sig []byte) (*Transaction, e return &Transaction{inner: cpy, time: tx.time}, nil } -// Cost returns gas * gasPrice + value. -func (tx *Transaction) Cost() *big.Int { - total := new(big.Int).Mul(tx.GasPrice(), new(big.Int).SetUint64(tx.Gas())) - total.Add(total, tx.Value()) - return total -} - // TxCost returns gas * gasPrice + value. func (tx *Transaction) TxCost(number *big.Int) *big.Int { total := new(big.Int).Mul(common.GetGasPrice(number), new(big.Int).SetUint64(tx.Gas())) diff --git a/eth/gasprice/gasprice.go b/eth/gasprice/gasprice.go index fe3a4b511126..e860e9a69f91 100644 --- a/eth/gasprice/gasprice.go +++ b/eth/gasprice/gasprice.go @@ -194,7 +194,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