diff --git a/Cargo.lock b/Cargo.lock index 260423d7..a4a6d170 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1049,9 +1049,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -1070,9 +1070,9 @@ checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -2432,6 +2432,7 @@ dependencies = [ "reqwest", "reth-chainspec", "signet-block-processor", + "signet-bundle", "signet-constants", "signet-genesis", "signet-sim", @@ -2647,9 +2648,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.60" +version = "4.5.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "52fa72306bb30daf11bc97773431628e5b4916e97aaa74b7d3f625d4d495da02" dependencies = [ "clap_builder", "clap_derive", @@ -2657,9 +2658,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.5.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "2071365c5c56eae7d77414029dde2f4f4ba151cf68d5a3261c9a40de428ace93" dependencies = [ "anstream", "anstyle", @@ -2669,9 +2670,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.5.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "dec5be1eea072311774b7b84ded287adbd9f293f9d23456817605c6042f4f5e0" dependencies = [ "heck", "proc-macro2", @@ -2681,9 +2682,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "0e78417baa3b3114dc0e95e7357389a249c4da97c3c2b540700079db6171bfd7" [[package]] name = "cmake" @@ -6191,9 +6192,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" dependencies = [ "critical-section", "portable-atomic", @@ -6328,9 +6329,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.75" +version = "0.10.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" dependencies = [ "bitflags 2.11.0", "cfg-if", @@ -6369,9 +6370,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.111" +version = "0.9.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" dependencies = [ "cc", "libc", diff --git a/Cargo.toml b/Cargo.toml index 9d20bfbe..f66517f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,6 +63,7 @@ url = "2.5.4" [dev-dependencies] alloy-hardforks = "0.4.0" alloy-chains = "0.2" +signet-bundle = "0.16.0-rc.11" # comment / uncomment for local dev # [patch.crates-io] diff --git a/src/tasks/cache/task.rs b/src/tasks/cache/task.rs index 74a1cd6f..9d4f0b04 100644 --- a/src/tasks/cache/task.rs +++ b/src/tasks/cache/task.rs @@ -59,7 +59,8 @@ impl CacheTask { basefee = sim_env.basefee; info!( basefee, - block_env_number = sim_env.number.to::(), block_env_timestamp = sim_env.timestamp.to::(), + block_env_number = sim_env.number.to::(), + block_env_timestamp = sim_env.timestamp.to::(), "rollup block env changed, clearing cache" ); cache.clean( diff --git a/src/test_utils/block.rs b/src/test_utils/block.rs index c39a1f4a..1708d53b 100644 --- a/src/test_utils/block.rs +++ b/src/test_utils/block.rs @@ -111,15 +111,13 @@ impl TestBlockBuildBuilder { /// This creates a `BlockBuild` ready for simulation. /// Call `.build().await` on the result to execute the simulation and get a `BuiltBlock`. pub fn build(self) -> TestBlockBuild { - let sim_env_builder = self.sim_env_builder.unwrap_or_default(); - - let (rollup_env, host_env, ru_source, host_source) = match (self.rollup_env, self.host_env) - { - (Some(rollup), Some(host)) => { - let (ru_source, host_source) = sim_env_builder.build_state_sources(); - (rollup, host, ru_source, host_source) - } - _ => sim_env_builder.build_with_sources(), + let builder = self.sim_env_builder.unwrap_or_default(); + let ru_state_source = TestStateSource::new(builder.rollup_db()); + let host_state_source = TestStateSource::new(builder.host_db()); + + let (rollup_env, host_env) = match (self.rollup_env, self.host_env) { + (Some(rollup), Some(host)) => (rollup, host), + _ => builder.build(), }; let finish_by = Instant::now() + self.deadline_duration; @@ -132,8 +130,8 @@ impl TestBlockBuildBuilder { self.sim_cache, self.max_gas, self.max_host_gas, - ru_source, - host_source, + ru_state_source, + host_state_source, ) } } diff --git a/src/test_utils/db.rs b/src/test_utils/db.rs index df5764b4..98f3332e 100644 --- a/src/test_utils/db.rs +++ b/src/test_utils/db.rs @@ -3,11 +3,10 @@ //! for testing block simulation without requiring network access. use alloy::primitives::{Address, B256, U256}; -use signet_sim::AcctInfo; +use signet_sim::{AcctInfo, StateSource}; use trevm::revm::{ database::{CacheDB, EmptyDB}, database_interface::DatabaseRef, - primitives::KECCAK_EMPTY, state::AccountInfo, }; @@ -17,32 +16,33 @@ use trevm::revm::{ /// with `RollupEnv` and `HostEnv` for offline simulation testing. pub type TestDb = CacheDB; -/// A [`StateSource`] backed by a [`TestDb`] for offline testing. -/// -/// This wraps an in-memory database and implements [`signet_sim::StateSource`] -/// so it can be used as the async state source parameter in [`BlockBuild::new`]. -/// -/// [`StateSource`]: signet_sim::StateSource -/// [`BlockBuild::new`]: signet_sim::BlockBuild::new +/// A [`StateSource`] for testing backed by an in-memory [`TestDb`]. +/// Returns actual account info (nonce, balance) from the database, +/// which is required for preflight validity checks during simulation. #[derive(Debug, Clone)] pub struct TestStateSource { db: TestDb, } impl TestStateSource { - /// Create a new [`TestStateSource`] from a [`TestDb`]. + /// Create a new `TestStateSource` backed by the given database. pub const fn new(db: TestDb) -> Self { Self { db } } } -impl signet_sim::StateSource for TestStateSource { - type Error = ::Error; +impl StateSource for TestStateSource { + type Error = std::convert::Infallible; async fn account_details(&self, address: &Address) -> Result { - let info = self.db.basic_ref(*address)?.unwrap_or_default(); - let has_code = info.code_hash() != KECCAK_EMPTY; - Ok(AcctInfo { nonce: info.nonce, balance: info.balance, has_code }) + match self.db.basic_ref(*address) { + Ok(Some(info)) => Ok(AcctInfo { + nonce: info.nonce, + balance: info.balance, + has_code: info.code_hash != trevm::revm::primitives::KECCAK_EMPTY, + }), + _ => Ok(AcctInfo { nonce: 0, balance: U256::ZERO, has_code: false }), + } } } diff --git a/src/test_utils/env.rs b/src/test_utils/env.rs index 3a860ed7..72a16987 100644 --- a/src/test_utils/env.rs +++ b/src/test_utils/env.rs @@ -2,7 +2,7 @@ //! This module provides builders for creating `RollupEnv` and `HostEnv` //! instances with in-memory databases for offline testing. -use super::db::{TestDb, TestDbBuilder, TestStateSource}; +use super::db::{TestDb, TestDbBuilder}; use crate::tasks::block::cfg::SignetCfgEnv; use alloy::primitives::{Address, B256, U256}; use signet_constants::SignetSystemConstants; @@ -117,9 +117,14 @@ impl TestSimEnvBuilder { HostEnv::new(self.host_db.clone(), self.constants.clone(), &cfg, &self.host_block_env) } - /// Build [`TestStateSource`] instances from the current databases. - pub fn build_state_sources(&self) -> (TestStateSource, TestStateSource) { - (TestStateSource::new(self.rollup_db.clone()), TestStateSource::new(self.host_db.clone())) + /// Get a clone of the rollup database. + pub fn rollup_db(&self) -> TestDb { + self.rollup_db.clone() + } + + /// Get a clone of the host database. + pub fn host_db(&self) -> TestDb { + self.host_db.clone() } /// Build both environments as a tuple. @@ -128,16 +133,6 @@ impl TestSimEnvBuilder { let host = self.build_host_env(); (rollup, host) } - - /// Build environments and state sources together. - pub fn build_with_sources( - &self, - ) -> (TestRollupEnv, TestHostEnv, TestStateSource, TestStateSource) { - let rollup = self.build_rollup_env(); - let host = self.build_host_env(); - let (ru_source, host_source) = self.build_state_sources(); - (rollup, host, ru_source, host_source) - } } #[cfg(test)] diff --git a/tests/bundle_load_test.rs b/tests/bundle_load_test.rs new file mode 100644 index 00000000..9a64417e --- /dev/null +++ b/tests/bundle_load_test.rs @@ -0,0 +1,308 @@ +//! Load tests for bundle simulation. +//! +//! These tests exercise the block building loop with high volumes of bundles +//! and transactions to verify correctness and deadline compliance under stress. + +use alloy::{ + primitives::{Address, U256}, + serde::OtherFields, + signers::local::PrivateKeySigner, +}; +use builder::test_utils::{ + DEFAULT_BALANCE, DEFAULT_BASEFEE, TestBlockBuildBuilder, TestDbBuilder, TestSimEnvBuilder, + create_transfer_tx, scenarios_test_block_env, +}; +use signet_bundle::RecoveredBundle; +use signet_sim::{BuiltBlock, SimCache}; +use std::collections::HashSet; +use std::time::Duration; + +/// Block number used for all test environments and bundles. +const BLOCK_NUMBER: u64 = 100; + +/// Block timestamp used for all test environments and bundles. +const BLOCK_TIMESTAMP: u64 = 1_700_000_000; + +/// Parmigiana rollup chain ID. +const RU_CHAIN_ID: u64 = 88888; + +/// Default max priority fee used for transfer bundles in tests. +const DEFAULT_PRIORITY_FEE: u128 = 10_000_000_000; + +/// Generate N random funded signers and a database builder with all of them funded. +fn generate_funded_accounts(n: usize) -> (Vec, TestDbBuilder) { + let signers: Vec = (0..n).map(|_| PrivateKeySigner::random()).collect(); + let balance = U256::from(DEFAULT_BALANCE); + + let mut db_builder = TestDbBuilder::new(); + for signer in &signers { + db_builder = db_builder.with_account(signer.address(), balance, 0); + } + + (signers, db_builder) +} + +/// Create a `RecoveredBundle` with one transfer transaction. +fn make_bundle( + signer: &PrivateKeySigner, + to: Address, + uuid: String, + max_priority_fee: u128, +) -> RecoveredBundle { + let tx = create_transfer_tx(signer, to, U256::from(1_000u64), 0, RU_CHAIN_ID, max_priority_fee) + .unwrap(); + + RecoveredBundle::new_unchecked( + vec![tx], + vec![], + BLOCK_NUMBER, + Some(BLOCK_TIMESTAMP - 100), + Some(BLOCK_TIMESTAMP + 100), + vec![], + Some(uuid), + vec![], + None, + None, + vec![], + OtherFields::default(), + ) +} + +/// Build a `TestBlockBuildBuilder` from a pre-funded db builder. +fn build_env(db_builder: TestDbBuilder) -> TestBlockBuildBuilder { + let db = db_builder.build(); + let block_env = + scenarios_test_block_env(BLOCK_NUMBER, DEFAULT_BASEFEE, BLOCK_TIMESTAMP, 3_000_000_000); + let sim_env = TestSimEnvBuilder::new() + .with_rollup_db(db.clone()) + .with_host_db(db) + .with_block_env(block_env); + TestBlockBuildBuilder::new().with_sim_env_builder(sim_env) +} + +/// 50 bundles each containing 1 transfer tx. Verify block builds and includes txs. +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn test_load_many_bundles() { + let count = 50; + let (signers, db_builder) = generate_funded_accounts(count); + let recipient = Address::repeat_byte(0xAA); + + let cache = SimCache::with_capacity(count); + let bundles: Vec = signers + .iter() + .enumerate() + .map(|(i, signer)| { + make_bundle(signer, recipient, format!("bundle-{i}"), DEFAULT_PRIORITY_FEE) + }) + .collect(); + + cache.add_bundles(bundles, DEFAULT_BASEFEE); + assert_eq!(cache.len(), count); + + let builder = build_env(db_builder).with_cache(cache).with_deadline(Duration::from_secs(5)); + let built: BuiltBlock = builder.build().build().await; + + assert!(built.tx_count() > 0, "expected transactions in built block, got 0"); + assert_eq!( + built.tx_count(), + count, + "expected all {count} bundle txs to be included, got {}", + built.tx_count() + ); +} + +/// 50k bundles each containing 1 transfer tx. Verify block builds with non-zero count of included txs. +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn test_load_50k_bundles() { + let count = 50_000; + let (signers, db_builder) = generate_funded_accounts(count); + let recipient = Address::repeat_byte(0xAA); + + let cache = SimCache::with_capacity(count); + let bundles: Vec = signers + .iter() + .enumerate() + .map(|(i, signer)| { + // Keep ranks distinct to avoid pathological cache insertion cost at high volume. + make_bundle(signer, recipient, format!("bundle-{i}"), DEFAULT_PRIORITY_FEE + i as u128) + }) + .collect(); + + cache.add_bundles(bundles, DEFAULT_BASEFEE); + assert_eq!(cache.len(), count); + + let builder = build_env(db_builder).with_cache(cache).with_deadline(Duration::from_secs(12)); + let built: BuiltBlock = builder.build().build().await; + + assert!(built.tx_count() > 0, "expected transactions in built block, got 0"); +} + +/// 30 bundles + 30 standalone txs. Verify both types land in the built block. +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn test_load_bundles_and_txs_mixed() { + let bundle_count = 30; + let tx_count = 30; + let total = bundle_count + tx_count; + + let (signers, db_builder) = generate_funded_accounts(total); + let recipient = Address::repeat_byte(0xBB); + + let cache = SimCache::with_capacity(total); + + let bundles: Vec = signers[..bundle_count] + .iter() + .enumerate() + .map(|(i, signer)| { + make_bundle(signer, recipient, format!("mix-bundle-{i}"), DEFAULT_PRIORITY_FEE) + }) + .collect(); + cache.add_bundles(bundles, DEFAULT_BASEFEE); + + for signer in &signers[bundle_count..] { + let tx = create_transfer_tx( + signer, + recipient, + U256::from(1_000u64), + 0, + RU_CHAIN_ID, + 10_000_000_000, + ) + .unwrap(); + cache.add_tx(tx, DEFAULT_BASEFEE); + } + + assert_eq!(cache.len(), total); + + let builder = build_env(db_builder).with_cache(cache).with_deadline(Duration::from_secs(5)); + let built: BuiltBlock = builder.build().build().await; + + assert!(built.tx_count() > 0, "expected transactions in built block"); + assert_eq!( + built.tx_count(), + total, + "expected all {total} items included, got {}", + built.tx_count() + ); +} + +/// Many bundles with a constrained gas limit. Verify gas cap is respected. +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn test_load_saturate_gas_limit() { + let count = 50; + let (signers, db_builder) = generate_funded_accounts(count); + let recipient = Address::repeat_byte(0xCC); + + let cache = SimCache::with_capacity(count); + let bundles: Vec = signers + .iter() + .enumerate() + .map(|(i, signer)| { + make_bundle(signer, recipient, format!("gas-bundle-{i}"), DEFAULT_PRIORITY_FEE) + }) + .collect(); + cache.add_bundles(bundles, DEFAULT_BASEFEE); + + // Each transfer costs 21,000 gas. Allow room for ~10 transfers. + let max_gas: u64 = 21_000 * 10; + + let builder = build_env(db_builder) + .with_cache(cache) + .with_deadline(Duration::from_secs(5)) + .with_max_gas(max_gas); + let built: BuiltBlock = builder.build().build().await; + + assert!( + built.tx_count() <= 10, + "expected at most 10 txs within gas limit, got {}", + built.tx_count() + ); + assert!(built.tx_count() > 0, "expected at least some txs to be included"); +} + +/// Many bundles with a tight deadline. Verify block completes within time. +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn test_load_deadline_pressure() { + let count = 100; + let (signers, db_builder) = generate_funded_accounts(count); + let recipient = Address::repeat_byte(0xDD); + + let cache = SimCache::with_capacity(count); + let bundles: Vec = signers + .iter() + .enumerate() + .map(|(i, signer)| { + make_bundle(signer, recipient, format!("deadline-bundle-{i}"), DEFAULT_PRIORITY_FEE) + }) + .collect(); + cache.add_bundles(bundles, DEFAULT_BASEFEE); + + let deadline = Duration::from_millis(500); + let start = std::time::Instant::now(); + + let builder = build_env(db_builder).with_cache(cache).with_deadline(deadline); + let built: BuiltBlock = builder.build().build().await; + + let elapsed = start.elapsed(); + + assert!(built.tx_count() > 0, "expected at least some txs under deadline pressure"); + + // Should complete within a reasonable margin of the deadline. + assert!(elapsed < deadline * 3, "block build took {elapsed:?}, expected within ~{deadline:?}"); +} + +/// Gas-constrained block: verify the builder selects the highest-fee bundles first. +/// +/// 10 low-fee bundles + 10 high-fee bundles, with a gas cap that can only fit 10. +/// Every included transaction must originate from a high-fee sender. +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn test_load_fee_priority_ordering() { + let low_count = 10usize; + let high_count = 10usize; + let total = low_count + high_count; + + let (signers, db_builder) = generate_funded_accounts(total); + let recipient = Address::repeat_byte(0xEE); + + let cache = SimCache::with_capacity(total); + + let low_fee = DEFAULT_PRIORITY_FEE; + let high_fee = 90_000_000_000u128; // 90 Gwei — valid (<= max_fee_per_gas=100 Gwei), 9× above low_fee + + let low_fee_senders: HashSet
= + signers[..low_count].iter().map(|s| s.address()).collect(); + + let bundles: Vec = signers + .iter() + .enumerate() + .map(|(i, signer)| { + let fee = if i < low_count { low_fee } else { high_fee }; + make_bundle(signer, recipient, format!("priority-bundle-{i}"), fee) + }) + .collect(); + + cache.add_bundles(bundles, DEFAULT_BASEFEE); + + // Gas limit exactly fits the 10 high-fee bundles (21,000 gas each). + let max_gas: u64 = 21_000 * high_count as u64; + + let builder = build_env(db_builder) + .with_cache(cache) + .with_deadline(Duration::from_secs(5)) + .with_max_gas(max_gas); + let built: BuiltBlock = builder.build().build().await; + + assert_eq!( + built.tx_count(), + high_count, + "expected exactly {high_count} txs, got {}", + built.tx_count() + ); + + for tx in built.transactions() { + assert!( + !low_fee_senders.contains(&tx.signer()), + "low-fee sender {} was included instead of a high-fee sender", + tx.signer() + ); + } +}