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
This commit is contained in:
Felföldi Zsolt
2021-05-28 10:28:07 +02:00
committed by GitHub
parent ee35ddc8fd
commit 966ee3ae6d
5 changed files with 747 additions and 147 deletions

View File

@ -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]})
}