|
| 1 | +//! Load tests for bundle simulation. |
| 2 | +//! |
| 3 | +//! These tests exercise the block building loop with high volumes of bundles |
| 4 | +//! and transactions to verify correctness and deadline compliance under stress. |
| 5 | +
|
| 6 | +use alloy::{ |
| 7 | + primitives::{Address, U256}, |
| 8 | + serde::OtherFields, |
| 9 | + signers::local::PrivateKeySigner, |
| 10 | +}; |
| 11 | +use builder::test_utils::{ |
| 12 | + DEFAULT_BALANCE, DEFAULT_BASEFEE, TestBlockBuildBuilder, TestDbBuilder, TestSimEnvBuilder, |
| 13 | + create_transfer_tx, scenarios_test_block_env, |
| 14 | +}; |
| 15 | +use signet_bundle::RecoveredBundle; |
| 16 | +use signet_sim::{BuiltBlock, SimCache}; |
| 17 | +use std::time::Duration; |
| 18 | + |
| 19 | +/// Block number used for all test environments and bundles. |
| 20 | +const BLOCK_NUMBER: u64 = 100; |
| 21 | + |
| 22 | +/// Block timestamp used for all test environments and bundles. |
| 23 | +const BLOCK_TIMESTAMP: u64 = 1_700_000_000; |
| 24 | + |
| 25 | +/// Parmigiana rollup chain ID. |
| 26 | +const RU_CHAIN_ID: u64 = 88888; |
| 27 | + |
| 28 | +/// Generate N random funded signers and a database builder with all of them funded. |
| 29 | +fn generate_funded_accounts(n: usize) -> (Vec<PrivateKeySigner>, TestDbBuilder) { |
| 30 | + let signers: Vec<PrivateKeySigner> = (0..n).map(|_| PrivateKeySigner::random()).collect(); |
| 31 | + let balance = U256::from(DEFAULT_BALANCE); |
| 32 | + |
| 33 | + let mut db_builder = TestDbBuilder::new(); |
| 34 | + for signer in &signers { |
| 35 | + db_builder = db_builder.with_account(signer.address(), balance, 0); |
| 36 | + } |
| 37 | + |
| 38 | + (signers, db_builder) |
| 39 | +} |
| 40 | + |
| 41 | +/// Create a `RecoveredBundle` with one transfer transaction. |
| 42 | +fn make_bundle(signer: &PrivateKeySigner, to: Address, uuid: String) -> RecoveredBundle { |
| 43 | + let tx = create_transfer_tx( |
| 44 | + signer, |
| 45 | + to, |
| 46 | + U256::from(1_000u64), |
| 47 | + 0, |
| 48 | + RU_CHAIN_ID, |
| 49 | + 10_000_000_000, // 10 gwei priority fee |
| 50 | + ) |
| 51 | + .unwrap(); |
| 52 | + |
| 53 | + RecoveredBundle::new_unchecked( |
| 54 | + vec![tx], |
| 55 | + vec![], |
| 56 | + BLOCK_NUMBER, |
| 57 | + Some(BLOCK_TIMESTAMP - 100), |
| 58 | + Some(BLOCK_TIMESTAMP + 100), |
| 59 | + vec![], |
| 60 | + Some(uuid), |
| 61 | + vec![], |
| 62 | + None, |
| 63 | + None, |
| 64 | + vec![], |
| 65 | + OtherFields::default(), |
| 66 | + ) |
| 67 | +} |
| 68 | + |
| 69 | +/// Build a `TestBlockBuildBuilder` from a pre-funded db builder. |
| 70 | +fn build_env(db_builder: TestDbBuilder) -> TestBlockBuildBuilder { |
| 71 | + let db = db_builder.build(); |
| 72 | + let block_env = |
| 73 | + scenarios_test_block_env(BLOCK_NUMBER, DEFAULT_BASEFEE, BLOCK_TIMESTAMP, 3_000_000_000); |
| 74 | + let sim_env = TestSimEnvBuilder::new() |
| 75 | + .with_rollup_db(db.clone()) |
| 76 | + .with_host_db(db) |
| 77 | + .with_block_env(block_env); |
| 78 | + TestBlockBuildBuilder::new().with_sim_env_builder(sim_env) |
| 79 | +} |
| 80 | + |
| 81 | +/// 50 bundles each containing 1 transfer tx. Verify block builds and includes txs. |
| 82 | +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] |
| 83 | +async fn test_load_many_bundles() { |
| 84 | + let count = 50; |
| 85 | + let (signers, db_builder) = generate_funded_accounts(count); |
| 86 | + let recipient = Address::repeat_byte(0xAA); |
| 87 | + |
| 88 | + let cache = SimCache::with_capacity(count); |
| 89 | + let bundles: Vec<RecoveredBundle> = signers |
| 90 | + .iter() |
| 91 | + .enumerate() |
| 92 | + .map(|(i, signer)| make_bundle(signer, recipient, format!("bundle-{i}"))) |
| 93 | + .collect(); |
| 94 | + |
| 95 | + cache.add_bundles(bundles, DEFAULT_BASEFEE); |
| 96 | + assert_eq!(cache.len(), count); |
| 97 | + |
| 98 | + let builder = build_env(db_builder).with_cache(cache).with_deadline(Duration::from_secs(5)); |
| 99 | + let built: BuiltBlock = builder.build().build().await; |
| 100 | + |
| 101 | + assert!(built.tx_count() > 0, "expected transactions in built block, got 0"); |
| 102 | + assert_eq!( |
| 103 | + built.tx_count(), |
| 104 | + count, |
| 105 | + "expected all {count} bundle txs to be included, got {}", |
| 106 | + built.tx_count() |
| 107 | + ); |
| 108 | +} |
| 109 | + |
| 110 | + |
| 111 | +/// 50 bundles each containing 1 transfer tx. Verify block builds and includes txs. |
| 112 | +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] |
| 113 | +async fn test_load_50k_bundles() { |
| 114 | + let count = 50_000; |
| 115 | + let (signers, db_builder) = generate_funded_accounts(count); |
| 116 | + let recipient = Address::repeat_byte(0xAA); |
| 117 | + |
| 118 | + let cache = SimCache::with_capacity(count); |
| 119 | + let bundles: Vec<RecoveredBundle> = signers |
| 120 | + .iter() |
| 121 | + .enumerate() |
| 122 | + .map(|(i, signer)| make_bundle(signer, recipient, format!("bundle-{i}"))) |
| 123 | + .collect(); |
| 124 | + |
| 125 | + cache.add_bundles(bundles, DEFAULT_BASEFEE); |
| 126 | + assert_eq!(cache.len(), count); |
| 127 | + |
| 128 | + let builder = build_env(db_builder).with_cache(cache).with_deadline(Duration::from_secs(12)); |
| 129 | + let built: BuiltBlock = builder.build().build().await; |
| 130 | + |
| 131 | + assert!(built.tx_count() > 0, "expected transactions in built block, got 0"); |
| 132 | +} |
| 133 | + |
| 134 | +/// 30 bundles + 30 standalone txs. Verify both types land in the built block. |
| 135 | +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] |
| 136 | +async fn test_load_bundles_and_txs_mixed() { |
| 137 | + let bundle_count = 30; |
| 138 | + let tx_count = 30; |
| 139 | + let total = bundle_count + tx_count; |
| 140 | + |
| 141 | + let (signers, db_builder) = generate_funded_accounts(total); |
| 142 | + let recipient = Address::repeat_byte(0xBB); |
| 143 | + |
| 144 | + let cache = SimCache::with_capacity(total); |
| 145 | + |
| 146 | + let bundles: Vec<RecoveredBundle> = signers[..bundle_count] |
| 147 | + .iter() |
| 148 | + .enumerate() |
| 149 | + .map(|(i, signer)| make_bundle(signer, recipient, format!("mix-bundle-{i}"))) |
| 150 | + .collect(); |
| 151 | + cache.add_bundles(bundles, DEFAULT_BASEFEE); |
| 152 | + |
| 153 | + for signer in &signers[bundle_count..] { |
| 154 | + let tx = create_transfer_tx( |
| 155 | + signer, |
| 156 | + recipient, |
| 157 | + U256::from(1_000u64), |
| 158 | + 0, |
| 159 | + RU_CHAIN_ID, |
| 160 | + 10_000_000_000, |
| 161 | + ) |
| 162 | + .unwrap(); |
| 163 | + cache.add_tx(tx, DEFAULT_BASEFEE); |
| 164 | + } |
| 165 | + |
| 166 | + assert_eq!(cache.len(), total); |
| 167 | + |
| 168 | + let builder = build_env(db_builder).with_cache(cache).with_deadline(Duration::from_secs(5)); |
| 169 | + let built: BuiltBlock = builder.build().build().await; |
| 170 | + |
| 171 | + assert!(built.tx_count() > 0, "expected transactions in built block"); |
| 172 | + assert_eq!( |
| 173 | + built.tx_count(), |
| 174 | + total, |
| 175 | + "expected all {total} items included, got {}", |
| 176 | + built.tx_count() |
| 177 | + ); |
| 178 | +} |
| 179 | + |
| 180 | +/// Many bundles with a constrained gas limit. Verify gas cap is respected. |
| 181 | +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] |
| 182 | +async fn test_load_saturate_gas_limit() { |
| 183 | + let count = 50; |
| 184 | + let (signers, db_builder) = generate_funded_accounts(count); |
| 185 | + let recipient = Address::repeat_byte(0xCC); |
| 186 | + |
| 187 | + let cache = SimCache::with_capacity(count); |
| 188 | + let bundles: Vec<RecoveredBundle> = signers |
| 189 | + .iter() |
| 190 | + .enumerate() |
| 191 | + .map(|(i, signer)| make_bundle(signer, recipient, format!("gas-bundle-{i}"))) |
| 192 | + .collect(); |
| 193 | + cache.add_bundles(bundles, DEFAULT_BASEFEE); |
| 194 | + |
| 195 | + // Each transfer costs 21,000 gas. Allow room for ~10 transfers. |
| 196 | + let max_gas: u64 = 21_000 * 10; |
| 197 | + |
| 198 | + let builder = build_env(db_builder) |
| 199 | + .with_cache(cache) |
| 200 | + .with_deadline(Duration::from_secs(5)) |
| 201 | + .with_max_gas(max_gas); |
| 202 | + let built: BuiltBlock = builder.build().build().await; |
| 203 | + |
| 204 | + assert!( |
| 205 | + built.tx_count() <= 10, |
| 206 | + "expected at most 10 txs within gas limit, got {}", |
| 207 | + built.tx_count() |
| 208 | + ); |
| 209 | + assert!(built.tx_count() > 0, "expected at least some txs to be included"); |
| 210 | +} |
| 211 | + |
| 212 | +/// Many bundles with a tight deadline. Verify block completes within time. |
| 213 | +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] |
| 214 | +async fn test_load_deadline_pressure() { |
| 215 | + let count = 100; |
| 216 | + let (signers, db_builder) = generate_funded_accounts(count); |
| 217 | + let recipient = Address::repeat_byte(0xDD); |
| 218 | + |
| 219 | + let cache = SimCache::with_capacity(count); |
| 220 | + let bundles: Vec<RecoveredBundle> = signers |
| 221 | + .iter() |
| 222 | + .enumerate() |
| 223 | + .map(|(i, signer)| make_bundle(signer, recipient, format!("deadline-bundle-{i}"))) |
| 224 | + .collect(); |
| 225 | + cache.add_bundles(bundles, DEFAULT_BASEFEE); |
| 226 | + |
| 227 | + let deadline = Duration::from_millis(500); |
| 228 | + let start = std::time::Instant::now(); |
| 229 | + |
| 230 | + let builder = build_env(db_builder).with_cache(cache).with_deadline(deadline); |
| 231 | + let built: BuiltBlock = builder.build().build().await; |
| 232 | + |
| 233 | + let elapsed = start.elapsed(); |
| 234 | + |
| 235 | + assert!(built.tx_count() > 0, "expected at least some txs under deadline pressure"); |
| 236 | + |
| 237 | + // Should complete within a reasonable margin of the deadline. |
| 238 | + assert!(elapsed < deadline * 3, "block build took {elapsed:?}, expected within ~{deadline:?}"); |
| 239 | +} |
0 commit comments