Files
nanoreth/crates/transaction-pool/tests/it/evict.rs

337 lines
14 KiB
Rust

//! Transaction pool eviction tests.
use alloy_eips::eip1559::{ETHEREUM_BLOCK_GAS_LIMIT, MIN_PROTOCOL_BASE_FEE};
use alloy_primitives::{Address, B256};
use rand::distributions::Uniform;
use reth_transaction_pool::{
error::PoolErrorKind,
test_utils::{
MockFeeRange, MockTransactionDistribution, MockTransactionRatio, TestPool, TestPoolBuilder,
},
BlockInfo, PoolConfig, SubPoolLimit, TransactionOrigin, TransactionPool, TransactionPoolExt,
};
#[tokio::test(flavor = "multi_thread")]
async fn only_blobs_eviction() {
// This test checks that blob transactions can be inserted into the pool, and at each step the
// blob pool can be truncated to the correct size
// set the pool limits to something small
let pool_config = PoolConfig {
pending_limit: SubPoolLimit { max_txs: 10, max_size: 1000 },
queued_limit: SubPoolLimit { max_txs: 10, max_size: 1000 },
basefee_limit: SubPoolLimit { max_txs: 10, max_size: 1000 },
blob_limit: SubPoolLimit { max_txs: 10, max_size: 1000 },
..Default::default()
};
let pool: TestPool = TestPoolBuilder::default().with_config(pool_config.clone()).into();
let block_info = BlockInfo {
block_gas_limit: ETHEREUM_BLOCK_GAS_LIMIT,
last_seen_block_hash: B256::ZERO,
last_seen_block_number: 0,
pending_basefee: 10,
pending_blob_fee: Some(10),
};
pool.set_block_info(block_info);
// this is how many times the test will regenerate transactions and insert them into the pool
let total_txs = 1000;
// If we have a wide size range we can cover cases both where we have a lot of small txs and a
// lot of large txs
let size_range = 10..1100;
// create mock tx distribution, 100% blobs
let tx_ratio = MockTransactionRatio {
legacy_pct: 0,
dynamic_fee_pct: 0,
blob_pct: 100,
access_list_pct: 0,
};
// Vary the amount of senders
let senders = [1, 10, 100, total_txs];
for sender_amt in &senders {
let gas_limit_range = 100_000..1_000_000;
// split the total txs into the amount of senders
let txs_per_sender = total_txs / sender_amt;
let nonce_range = 0..txs_per_sender;
let pending_blob_fee = block_info.pending_blob_fee.unwrap();
// start the fees at zero, some transactions will be underpriced
let fee_range = MockFeeRange {
gas_price: Uniform::from(0u128..(block_info.pending_basefee as u128 + 1000)),
priority_fee: Uniform::from(0u128..(block_info.pending_basefee as u128 + 1000)),
// we need to set the max fee to at least the min protocol base fee, or transactions
// generated could be rejected
max_fee: Uniform::from(
MIN_PROTOCOL_BASE_FEE as u128..(block_info.pending_basefee as u128 + 2000),
),
max_fee_blob: Uniform::from(pending_blob_fee..(pending_blob_fee + 1000)),
};
let distribution = MockTransactionDistribution::new(
tx_ratio.clone(),
fee_range,
gas_limit_range,
size_range.clone(),
);
for _ in 0..*sender_amt {
// use a random sender, create the tx set
let sender = Address::random();
let set = distribution.tx_set(sender, nonce_range.clone(), &mut rand::thread_rng());
let set = set.into_vec();
// ensure that the first nonce is 0
assert_eq!(set[0].get_nonce(), 0);
// and finally insert it into the pool
let results = pool.add_transactions(TransactionOrigin::External, set).await;
for (i, result) in results.iter().enumerate() {
match result {
Ok(hash) => {
println!("✅ Inserted tx into pool with hash: {hash}");
}
Err(e) => {
match e.kind {
PoolErrorKind::DiscardedOnInsert => {
println!("✅ Discarded tx on insert, like we should have");
}
PoolErrorKind::SpammerExceededCapacity(addr) => {
// ensure the address is the same as the sender
assert_eq!(addr, sender);
// ensure that this is only returned when the sender is over the
// pool limit per account
assert!(i + 1 >= pool_config.max_account_slots, "Spammer exceeded capacity, but it shouldn't have. Max accounts slots: {}, current txs by sender: {}", pool_config.max_account_slots, i + 1);
// at this point we know that the sender has been limited, so we
// keep going
}
_ => {
panic!("Failed to insert tx into pool with unexpected error: {e}");
}
}
}
}
}
// after every insert, ensure that it's under the pool limits
assert!(!pool.is_exceeded());
}
}
}
#[tokio::test(flavor = "multi_thread")]
async fn mixed_eviction() {
// This test checks that many transaction types can be inserted into the pool. The fees need
// to be set so that the transactions will actually pass validation. Transactions here do not
// have nonce gaps.
let pool_config = PoolConfig {
pending_limit: SubPoolLimit { max_txs: 20, max_size: 2000 },
queued_limit: SubPoolLimit { max_txs: 20, max_size: 2000 },
basefee_limit: SubPoolLimit { max_txs: 20, max_size: 2000 },
blob_limit: SubPoolLimit { max_txs: 20, max_size: 2000 },
..Default::default()
};
let pool: TestPool = TestPoolBuilder::default().with_config(pool_config.clone()).into();
let block_info = BlockInfo {
block_gas_limit: ETHEREUM_BLOCK_GAS_LIMIT,
last_seen_block_hash: B256::ZERO,
last_seen_block_number: 0,
pending_basefee: 10,
pending_blob_fee: Some(20),
};
pool.set_block_info(block_info);
let total_txs = 100;
let size_range = 10..1100;
// Adjust the ratios to include a mix of transaction types
let tx_ratio = MockTransactionRatio {
legacy_pct: 25,
dynamic_fee_pct: 25,
blob_pct: 25,
access_list_pct: 25,
};
let senders = [1, 5, 10];
for sender_amt in &senders {
let gas_limit_range = 100_000..1_000_000;
let txs_per_sender = total_txs / sender_amt;
let nonce_range = 0..txs_per_sender;
let pending_blob_fee = block_info.pending_blob_fee.unwrap();
// Make sure transactions are not immediately rejected
let min_gas_price = block_info.pending_basefee as u128 + 1;
let min_priority_fee = 1u128;
let min_max_fee = block_info.pending_basefee as u128 + 10;
let fee_range = MockFeeRange {
gas_price: Uniform::from(min_gas_price..(min_gas_price + 1000)),
priority_fee: Uniform::from(min_priority_fee..(min_priority_fee + 1000)),
max_fee: Uniform::from(min_max_fee..(min_max_fee + 2000)),
max_fee_blob: Uniform::from(pending_blob_fee..(pending_blob_fee + 1000)),
};
let distribution = MockTransactionDistribution::new(
tx_ratio.clone(),
fee_range,
gas_limit_range,
size_range.clone(),
);
for _ in 0..*sender_amt {
let sender = Address::random();
let set = distribution.tx_set_non_conflicting_types(
sender,
nonce_range.clone(),
&mut rand::thread_rng(),
);
let set = set.into_inner().into_vec();
assert_eq!(set[0].get_nonce(), 0);
let results = pool.add_transactions(TransactionOrigin::External, set).await;
for (i, result) in results.iter().enumerate() {
match result {
Ok(_) => {
// Transaction inserted successfully
}
Err(e) => {
match e.kind {
PoolErrorKind::DiscardedOnInsert => {
// Transaction discarded on insert
println!("✅ Discarded tx on insert, like we should have");
}
PoolErrorKind::SpammerExceededCapacity(addr) => {
// ensure the address is the same as the sender
assert_eq!(addr, sender);
// ensure that this is only returned when the sender is over the
// pool limit per account
assert!(i + 1 >= pool_config.max_account_slots, "Spammer exceeded capacity, but it shouldn't have. Max accounts slots: {}, current txs by sender: {}", pool_config.max_account_slots, i + 1);
}
_ => panic!("Failed to insert tx into pool with unexpected error: {e}"),
}
}
}
}
assert!(!pool.is_exceeded());
}
}
}
#[tokio::test(flavor = "multi_thread")]
async fn nonce_gaps_eviction() {
// This test checks that many transaction types can be inserted into the pool.
//
// This test also inserts nonce gaps into the non-blob transactions.
let pool_config = PoolConfig {
pending_limit: SubPoolLimit { max_txs: 20, max_size: 2000 },
queued_limit: SubPoolLimit { max_txs: 20, max_size: 2000 },
basefee_limit: SubPoolLimit { max_txs: 20, max_size: 2000 },
blob_limit: SubPoolLimit { max_txs: 20, max_size: 2000 },
..Default::default()
};
let pool: TestPool = TestPoolBuilder::default().with_config(pool_config.clone()).into();
let block_info = BlockInfo {
block_gas_limit: ETHEREUM_BLOCK_GAS_LIMIT,
last_seen_block_hash: B256::ZERO,
last_seen_block_number: 0,
pending_basefee: 10,
pending_blob_fee: Some(20),
};
pool.set_block_info(block_info);
let total_txs = 100;
let size_range = 10..1100;
// Adjust the ratios to include a mix of transaction types
let tx_ratio = MockTransactionRatio {
legacy_pct: 25,
dynamic_fee_pct: 25,
blob_pct: 25,
access_list_pct: 25,
};
let senders = [1, 5, 10];
for sender_amt in &senders {
let gas_limit_range = 100_000..1_000_000;
let txs_per_sender = total_txs / sender_amt;
let nonce_range = 0..txs_per_sender;
let pending_blob_fee = block_info.pending_blob_fee.unwrap();
// Make sure transactions are not immediately rejected
let min_gas_price = block_info.pending_basefee as u128 + 1;
let min_priority_fee = 1u128;
let min_max_fee = block_info.pending_basefee as u128 + 10;
let fee_range = MockFeeRange {
gas_price: Uniform::from(min_gas_price..(min_gas_price + 1000)),
priority_fee: Uniform::from(min_priority_fee..(min_priority_fee + 1000)),
max_fee: Uniform::from(min_max_fee..(min_max_fee + 2000)),
max_fee_blob: Uniform::from(pending_blob_fee..(pending_blob_fee + 1000)),
};
let distribution = MockTransactionDistribution::new(
tx_ratio.clone(),
fee_range,
gas_limit_range,
size_range.clone(),
);
// set up gap percentages and sizes, 30% chance for transactions to be followed by a gap,
// and the gap size is between 1 and 5
let gap_pct = 30;
let gap_range = 1u64..6;
for _ in 0..*sender_amt {
let sender = Address::random();
let mut set = distribution.tx_set_non_conflicting_types(
sender,
nonce_range.clone(),
&mut rand::thread_rng(),
);
set.with_nonce_gaps(gap_pct, gap_range.clone(), &mut rand::thread_rng());
let set = set.into_inner().into_vec();
let results = pool.add_transactions(TransactionOrigin::External, set).await;
for (i, result) in results.iter().enumerate() {
match result {
Ok(_) => {
// Transaction inserted successfully
}
Err(e) => {
match e.kind {
PoolErrorKind::DiscardedOnInsert => {
// Transaction discarded on insert
println!("✅ Discarded tx on insert, like we should have");
}
PoolErrorKind::SpammerExceededCapacity(addr) => {
// ensure the address is the same as the sender
assert_eq!(addr, sender);
// ensure that this is only returned when the sender is over the
// pool limit per account
assert!(i + 1 >= pool_config.max_account_slots, "Spammer exceeded capacity, but it shouldn't have. Max accounts slots: {}, current txs by sender: {}", pool_config.max_account_slots, i + 1);
}
_ => panic!("Failed to insert tx into pool with unexpected error: {e}"),
}
}
}
}
assert!(!pool.is_exceeded());
}
}
}