diff --git a/core/tx_list.go b/core/tx_list.go index 08d7d80e81..626d3a3b71 100644 --- a/core/tx_list.go +++ b/core/tx_list.go @@ -220,9 +220,11 @@ func (m *txSortedMap) Flatten() types.Transactions { // the executable/pending queue; and for storing gapped transactions for the non- // executable/future queue, with minor behavioral changes. type txList struct { - strict bool // Whether nonces are strictly continuous or not - txs *txSortedMap // Heap indexed sorted hash map of the transactions - costcap *big.Int // Price of the highest costing transaction (reset only if exceeds balance) + strict bool // Whether nonces are strictly continuous or not + txs *txSortedMap // Heap indexed sorted hash map of the transactions + + costcap *big.Int // Price of the highest costing transaction (reset only if exceeds balance) + gascap *big.Int // Gas limit of the highest spending transaction (reset only if exceeds block limit) } // newTxList create a new transaction list for maintaining nonce-indexable fast, @@ -232,6 +234,7 @@ func newTxList(strict bool) *txList { strict: strict, txs: newTxSortedMap(), costcap: new(big.Int), + gascap: new(big.Int), } } @@ -244,8 +247,8 @@ func (l *txList) Overlaps(tx *types.Transaction) bool { // Add tries to insert a new transaction into the list, returning whether the // transaction was accepted, and if yes, any previous transaction it replaced. // -// If the new transaction is accepted into the list, the lists' cost threshold -// is also potentially updated. +// If the new transaction is accepted into the list, the lists' cost and gas +// thresholds are also potentially updated. func (l *txList) Add(tx *types.Transaction, priceBump uint64) (bool, *types.Transaction) { // If there's an older better transaction, abort old := l.txs.Get(tx.Nonce()) @@ -260,6 +263,9 @@ func (l *txList) Add(tx *types.Transaction, priceBump uint64) (bool, *types.Tran if cost := tx.Cost(); l.costcap.Cmp(cost) < 0 { l.costcap = cost } + if gas := tx.Gas(); l.gascap.Cmp(gas) < 0 { + l.gascap = gas + } return true, old } @@ -270,23 +276,25 @@ func (l *txList) Forward(threshold uint64) types.Transactions { return l.txs.Forward(threshold) } -// Filter removes all transactions from the list with a cost higher than the -// provided threshold. Every removed transaction is returned for any post-removal -// maintenance. Strict-mode invalidated transactions are also returned. +// Filter removes all transactions from the list with a cost or gas limit higher +// than the provided thresholds. Every removed transaction is returned for any +// post-removal maintenance. Strict-mode invalidated transactions are also +// returned. // -// This method uses the cached costcap to quickly decide if there's even a point -// in calculating all the costs or if the balance covers all. If the threshold is -// lower than the costcap, the costcap will be reset to a new high after removing -// expensive the too transactions. -func (l *txList) Filter(threshold *big.Int) (types.Transactions, types.Transactions) { +// This method uses the cached costcap and gascap to quickly decide if there's even +// a point in calculating all the costs or if the balance covers all. If the threshold +// is lower than the costgas cap, the caps will be reset to a new high after removing +// the newly invalidated transactions. +func (l *txList) Filter(costLimit, gasLimit *big.Int) (types.Transactions, types.Transactions) { // If all transactions are below the threshold, short circuit - if l.costcap.Cmp(threshold) <= 0 { + if l.costcap.Cmp(costLimit) <= 0 && l.gascap.Cmp(gasLimit) <= 0 { return nil, nil } - l.costcap = new(big.Int).Set(threshold) // Lower the cap to the threshold + l.costcap = new(big.Int).Set(costLimit) // Lower the caps to the thresholds + l.gascap = new(big.Int).Set(gasLimit) // Filter out all the transactions above the account's funds - removed := l.txs.Filter(func(tx *types.Transaction) bool { return tx.Cost().Cmp(threshold) > 0 }) + removed := l.txs.Filter(func(tx *types.Transaction) bool { return tx.Cost().Cmp(costLimit) > 0 || tx.Gas().Cmp(gasLimit) > 0 }) // If the list was strict, filter anything above the lowest nonce var invalids types.Transactions diff --git a/core/tx_pool.go b/core/tx_pool.go index 7aea02101f..1f5b46d4b6 100644 --- a/core/tx_pool.go +++ b/core/tx_pool.go @@ -663,6 +663,8 @@ func (pool *TxPool) removeTx(hash common.Hash) { // future queue to the set of pending transactions. During this process, all // invalidated transactions (low nonce, low balance) are deleted. func (pool *TxPool) promoteExecutables(state *state.StateDB) { + gaslimit := pool.gasLimit() + // Iterate over all accounts and promote any executable transactions queued := uint64(0) for addr, list := range pool.queue { @@ -673,8 +675,8 @@ func (pool *TxPool) promoteExecutables(state *state.StateDB) { delete(pool.all, hash) pool.priced.Removed() } - // Drop all transactions that are too costly (low balance) - drops, _ := list.Filter(state.GetBalance(addr)) + // Drop all transactions that are too costly (low balance or out of gas) + drops, _ := list.Filter(state.GetBalance(addr), gaslimit) for _, tx := range drops { hash := tx.Hash() log.Trace("Removed unpayable queued transaction", "hash", hash) @@ -798,6 +800,8 @@ func (pool *TxPool) promoteExecutables(state *state.StateDB) { // executable/pending queue and any subsequent transactions that become unexecutable // are moved back into the future queue. func (pool *TxPool) demoteUnexecutables(state *state.StateDB) { + gaslimit := pool.gasLimit() + // Iterate over all accounts and demote any non-executable transactions for addr, list := range pool.pending { nonce := state.GetNonce(addr) @@ -809,8 +813,8 @@ func (pool *TxPool) demoteUnexecutables(state *state.StateDB) { delete(pool.all, hash) pool.priced.Removed() } - // Drop all transactions that are too costly (low balance), and queue any invalids back for later - drops, invalids := list.Filter(state.GetBalance(addr)) + // Drop all transactions that are too costly (low balance or out of gas), and queue any invalids back for later + drops, invalids := list.Filter(state.GetBalance(addr), gaslimit) for _, tx := range drops { hash := tx.Hash() log.Trace("Removed unpayable pending transaction", "hash", hash) diff --git a/core/tx_pool_test.go b/core/tx_pool_test.go index 63e0144bdb..c12bd20a16 100644 --- a/core/tx_pool_test.go +++ b/core/tx_pool_test.go @@ -397,50 +397,79 @@ func TestTransactionDropping(t *testing.T) { var ( tx0 = transaction(0, big.NewInt(100), key) tx1 = transaction(1, big.NewInt(200), key) + tx2 = transaction(2, big.NewInt(300), key) tx10 = transaction(10, big.NewInt(100), key) tx11 = transaction(11, big.NewInt(200), key) + tx12 = transaction(12, big.NewInt(300), key) ) pool.promoteTx(account, tx0.Hash(), tx0) pool.promoteTx(account, tx1.Hash(), tx1) + pool.promoteTx(account, tx1.Hash(), tx2) pool.enqueueTx(tx10.Hash(), tx10) pool.enqueueTx(tx11.Hash(), tx11) + pool.enqueueTx(tx11.Hash(), tx12) // Check that pre and post validations leave the pool as is - if pool.pending[account].Len() != 2 { - t.Errorf("pending transaction mismatch: have %d, want %d", pool.pending[account].Len(), 2) + if pool.pending[account].Len() != 3 { + t.Errorf("pending transaction mismatch: have %d, want %d", pool.pending[account].Len(), 3) } - if pool.queue[account].Len() != 2 { - t.Errorf("queued transaction mismatch: have %d, want %d", pool.queue[account].Len(), 2) + if pool.queue[account].Len() != 3 { + t.Errorf("queued transaction mismatch: have %d, want %d", pool.queue[account].Len(), 3) } if len(pool.all) != 4 { t.Errorf("total transaction mismatch: have %d, want %d", len(pool.all), 4) } pool.resetState() - if pool.pending[account].Len() != 2 { - t.Errorf("pending transaction mismatch: have %d, want %d", pool.pending[account].Len(), 2) + if pool.pending[account].Len() != 3 { + t.Errorf("pending transaction mismatch: have %d, want %d", pool.pending[account].Len(), 3) } - if pool.queue[account].Len() != 2 { - t.Errorf("queued transaction mismatch: have %d, want %d", pool.queue[account].Len(), 2) + if pool.queue[account].Len() != 3 { + t.Errorf("queued transaction mismatch: have %d, want %d", pool.queue[account].Len(), 3) } if len(pool.all) != 4 { t.Errorf("total transaction mismatch: have %d, want %d", len(pool.all), 4) } // Reduce the balance of the account, and check that invalidated transactions are dropped - state.AddBalance(account, big.NewInt(-750)) + state.AddBalance(account, big.NewInt(-650)) pool.resetState() if _, ok := pool.pending[account].txs.items[tx0.Nonce()]; !ok { t.Errorf("funded pending transaction missing: %v", tx0) } - if _, ok := pool.pending[account].txs.items[tx1.Nonce()]; ok { + if _, ok := pool.pending[account].txs.items[tx1.Nonce()]; !ok { + t.Errorf("funded pending transaction missing: %v", tx0) + } + if _, ok := pool.pending[account].txs.items[tx2.Nonce()]; ok { t.Errorf("out-of-fund pending transaction present: %v", tx1) } if _, ok := pool.queue[account].txs.items[tx10.Nonce()]; !ok { t.Errorf("funded queued transaction missing: %v", tx10) } - if _, ok := pool.queue[account].txs.items[tx11.Nonce()]; ok { + if _, ok := pool.queue[account].txs.items[tx11.Nonce()]; !ok { + t.Errorf("funded queued transaction missing: %v", tx10) + } + if _, ok := pool.queue[account].txs.items[tx12.Nonce()]; ok { t.Errorf("out-of-fund queued transaction present: %v", tx11) } + if len(pool.all) != 4 { + t.Errorf("total transaction mismatch: have %d, want %d", len(pool.all), 4) + } + // Reduce the block gas limit, check that invalidated transactions are dropped + pool.gasLimit = func() *big.Int { return big.NewInt(100) } + pool.resetState() + + if _, ok := pool.pending[account].txs.items[tx0.Nonce()]; !ok { + t.Errorf("funded pending transaction missing: %v", tx0) + } + if _, ok := pool.pending[account].txs.items[tx1.Nonce()]; ok { + t.Errorf("over-gased pending transaction present: %v", tx1) + } + if _, ok := pool.queue[account].txs.items[tx10.Nonce()]; !ok { + t.Errorf("funded queued transaction missing: %v", tx10) + } + if _, ok := pool.queue[account].txs.items[tx11.Nonce()]; ok { + t.Errorf("over-gased queued transaction present: %v", tx11) + } if len(pool.all) != 2 { t.Errorf("total transaction mismatch: have %d, want %d", len(pool.all), 2) }