mirror of
https://github.com/hl-archive-node/nanoreth.git
synced 2025-12-06 10:59:55 +00:00
338 lines
14 KiB
Rust
338 lines
14 KiB
Rust
//! Transaction pool eviction tests.
|
|
|
|
use alloy_consensus::Transaction;
|
|
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].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].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());
|
|
}
|
|
}
|
|
}
|