From 4354ec880a9d0cf4d98ba280d23bd481a87a5c12 Mon Sep 17 00:00:00 2001 From: dylan Date: Thu, 2 Oct 2025 15:12:11 -0600 Subject: [PATCH 01/15] feat: adds a flashbots provider and submitter - adds a Flashbots provider with the alloy MEV API extensions - adds the logic for building Flashbots bundles in the builder - adds the Flashbots submission task --- Cargo.toml | 2 +- src/tasks/submit/prep.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index c68e9d7..5236669 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ signet-zenith = { version = "0.13.0" } trevm = { version = "0.29", features = ["concurrent-db", "test-utils"] } -alloy = { version = "1.0.35", features = [ +alloy = { version = "1.0.36", features = [ "full", "json-rpc", "signer-aws", diff --git a/src/tasks/submit/prep.rs b/src/tasks/submit/prep.rs index daf807f..d7affda 100644 --- a/src/tasks/submit/prep.rs +++ b/src/tasks/submit/prep.rs @@ -130,6 +130,7 @@ impl<'a> SubmitPrep<'a> { /// Prepares a transaction for submission to the host chain. pub async fn prep_transaction(self, prev_host: &Header) -> eyre::Result { + debug!(prev_host = ?prev_host, "preparing transaction for submission to host chain"); let req = self.new_tx_request().in_current_span().await?; Ok(Bumpable::new(req, prev_host)) } From 14f986494296e2c0b8260ca23fd7f23a1287b7de Mon Sep 17 00:00:00 2001 From: dylan Date: Wed, 8 Oct 2025 12:35:52 -0600 Subject: [PATCH 02/15] bump alloy back up --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 5236669..c21a11c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ signet-zenith = { version = "0.13.0" } trevm = { version = "0.29", features = ["concurrent-db", "test-utils"] } -alloy = { version = "1.0.36", features = [ +alloy = { version = "1.0.37", features = [ "full", "json-rpc", "signer-aws", From ddb81f2bbdaa7b130b52be6b6ce65757c34c0c8b Mon Sep 17 00:00:00 2001 From: dylan Date: Wed, 8 Oct 2025 12:43:58 -0600 Subject: [PATCH 03/15] cleanup --- src/tasks/submit/prep.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/tasks/submit/prep.rs b/src/tasks/submit/prep.rs index d7affda..daf807f 100644 --- a/src/tasks/submit/prep.rs +++ b/src/tasks/submit/prep.rs @@ -130,7 +130,6 @@ impl<'a> SubmitPrep<'a> { /// Prepares a transaction for submission to the host chain. pub async fn prep_transaction(self, prev_host: &Header) -> eyre::Result { - debug!(prev_host = ?prev_host, "preparing transaction for submission to host chain"); let req = self.new_tx_request().in_current_span().await?; Ok(Bumpable::new(req, prev_host)) } From f0ab05b060d30f1324311a62cf2fc94a9c348a61 Mon Sep 17 00:00:00 2001 From: dylan Date: Wed, 8 Oct 2025 17:13:27 -0600 Subject: [PATCH 04/15] removes bin-base flashbots dependency --- src/config.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/config.rs b/src/config.rs index c7d93bb..270809b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -58,6 +58,18 @@ pub type FlashbotsProvider = FillProvider< providers::RootProvider, >; +/// +pub type FlashbotsProviderV2 = FillProvider< + JoinFill< + JoinFill< + Identity, + JoinFill>>, + >, + WalletFiller, + >, + providers::RootProvider, +>; + /// The default concurrency limit for the builder if the system call /// fails and no user-specified value is set. pub const DEFAULT_CONCURRENCY_LIMIT: usize = 8; From 4300dcf127b6ac15c897b63ff9bf5a0abef9a2eb Mon Sep 17 00:00:00 2001 From: dylan Date: Wed, 8 Oct 2025 17:22:03 -0600 Subject: [PATCH 05/15] moar cleanup --- src/config.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/config.rs b/src/config.rs index 270809b..c7d93bb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -58,18 +58,6 @@ pub type FlashbotsProvider = FillProvider< providers::RootProvider, >; -/// -pub type FlashbotsProviderV2 = FillProvider< - JoinFill< - JoinFill< - Identity, - JoinFill>>, - >, - WalletFiller, - >, - providers::RootProvider, ->; - /// The default concurrency limit for the builder if the system call /// fails and no user-specified value is set. pub const DEFAULT_CONCURRENCY_LIMIT: usize = 8; From 540fec603d52618c9e4832103b822a320b87b538 Mon Sep 17 00:00:00 2001 From: dylan Date: Mon, 22 Sep 2025 18:07:24 -0600 Subject: [PATCH 06/15] wip: adds test harness --- tests/block_builder_test.rs | 28 +++++++++ tests/harness.rs | 114 ++++++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 tests/harness.rs diff --git a/tests/block_builder_test.rs b/tests/block_builder_test.rs index 92adfef..3ec108d 100644 --- a/tests/block_builder_test.rs +++ b/tests/block_builder_test.rs @@ -18,6 +18,9 @@ use builder::{ use signet_sim::SimCache; use std::time::{Duration, Instant}; +mod harness; +use harness::TestHarness; + /// Tests the `handle_build` method of the `Simulator`. /// /// This test sets up a simulated environment using Anvil, creates a block builder, @@ -78,3 +81,28 @@ async fn test_handle_build() { assert!(got.is_ok()); assert!(got.unwrap().tx_count() == 2); } + +/// End-to-end simulation flow using the TestHarness. +#[ignore = "integration test"] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_harness_ticks_and_emits() { + setup_logging(); + + // Build harness + let mut h = TestHarness::new().await.unwrap(); + + // Prepare two senders and fund them if needed from anvil default accounts + let keys = h.anvil.keys(); + let test_key_0 = PrivateKeySigner::from_signing_key(keys[0].clone().into()); + + // Add a transaction into the sim cache + h.add_tx(&test_key_0, 0, U256::from(1_u64), 11_000); + + // Start simulator and tick a new SimEnv + h.start(); + h.tick_from_ru_latest().await; + + // Expect a SimResult + let got = h.recv_result(Duration::from_secs(5)).await.expect("sim result"); + assert_eq!(got.block.tx_count(), 1); +} diff --git a/tests/harness.rs b/tests/harness.rs new file mode 100644 index 0000000..79814b1 --- /dev/null +++ b/tests/harness.rs @@ -0,0 +1,114 @@ +//! Test harness for end-to-end simulation flow. +//! +//! This harness wires a Simulator to a manual SimEnv watch channel and an +//! mpsc submit channel so tests can tick BlockEnv values and assert on +//! emitted SimResult blocks without involving submission logic. + +use std::time::Duration; + +use alloy::{ + eips::BlockId, + network::Ethereum, + node_bindings::{Anvil, AnvilInstance}, + primitives::U256, + providers::{Provider, RootProvider}, + signers::local::PrivateKeySigner, +}; +use builder::{ + config::BuilderConfig, + tasks::{ + block::sim::{SimResult, Simulator}, + env::SimEnv, + }, + test_utils::{new_signed_tx, setup_logging, setup_test_config, test_block_env}, +}; +use signet_sim::SimCache; +use signet_types::constants::SignetSystemConstants; +use tokio::sync::{mpsc, watch}; + +pub struct TestHarness { + pub config: BuilderConfig, + pub constants: SignetSystemConstants, + pub anvil: AnvilInstance, + pub ru_provider: RootProvider, + sim_env_tx: watch::Sender>, + sim_env_rx: watch::Receiver>, + submit_tx: mpsc::UnboundedSender, + submit_rx: mpsc::UnboundedReceiver, + sim_cache: SimCache, +} + +impl TestHarness { + /// Create a new harness with a fresh Anvil rollup chain and default test config. + pub async fn new() -> eyre::Result { + setup_logging(); + + let config = setup_test_config()?; + let constants = SignetSystemConstants::pecorino(); + + let anvil = Anvil::new().chain_id(signet_constants::pecorino::RU_CHAIN_ID).spawn(); + let ru_provider = RootProvider::::new_http(anvil.endpoint_url()); + + let (sim_env_tx, sim_env_rx) = watch::channel::>(None); + let (submit_tx, submit_rx) = mpsc::unbounded_channel::(); + + Ok(Self { + config, + constants, + anvil, + ru_provider, + sim_env_tx, + sim_env_rx, + submit_tx, + submit_rx, + sim_cache: SimCache::new(), + }) + } + + /// Add a signed transaction from a provided signer to the sim cache. + pub fn add_tx(&self, signer: &PrivateKeySigner, nonce: u64, value: U256, mpfpg: u128) { + let tx = new_signed_tx(signer, nonce, value, mpfpg).expect("tx signing"); + // group index 0 for simplicity in tests + self.sim_cache.add_tx(tx, 0); + } + + /// Start the simulator task. + pub fn start(&self) { + // Spawn the simulator background task + let cache = self.sim_cache.clone(); + let constants = self.constants.clone(); + let submit_tx = self.submit_tx.clone(); + let simulator = + Simulator::new(&self.config, self.ru_provider.clone(), self.sim_env_rx.clone()); + let _jh = simulator.spawn_simulator_task(constants, cache, submit_tx); + // NB: We intentionally leak the JoinHandle in tests; tokio will cancel on drop. + } + + /// Tick a new SimEnv computed from the current RU latest header. + pub async fn tick_from_ru_latest(&self) { + let header = self + .ru_provider + .get_block(BlockId::latest()) + .await + .expect("ru latest block") + .expect("ru latest exists") + .header + .inner; + + let number = header.number + 1; + let timestamp = header.timestamp + self.config.slot_calculator.slot_duration(); + // A small basefee is fine for local testing + let block_env = test_block_env(self.config.clone(), number, 7, timestamp); + + // Re-use RU header as prev_host for harness; submit path doesn't run here. + let span = tracing::info_span!("TestHarness::tick", number, timestamp); + let sim_env = SimEnv { block_env, prev_header: header.clone(), prev_host: header, span }; + + let _ = self.sim_env_tx.send(Some(sim_env)); + } + + /// Receive the next SimResult with a timeout. + pub async fn recv_result(&mut self, timeout: Duration) -> Option { + tokio::time::timeout(timeout, self.submit_rx.recv()).await.ok().flatten() + } +} From 510e9ae95e74ddbe9825bbe0b3d472db4d05057b Mon Sep 17 00:00:00 2001 From: dylan Date: Fri, 26 Sep 2025 15:21:05 -0600 Subject: [PATCH 07/15] harness tests are working --- tests/block_builder_test.rs | 107 +++++++++++++++++++-- tests/harness.rs | 180 ++++++++++++++++++++++++++++-------- 2 files changed, 242 insertions(+), 45 deletions(-) diff --git a/tests/block_builder_test.rs b/tests/block_builder_test.rs index 3ec108d..87ddd9b 100644 --- a/tests/block_builder_test.rs +++ b/tests/block_builder_test.rs @@ -65,9 +65,11 @@ async fn test_handle_build() { // Setup the block envs let finish_by = Instant::now() + Duration::from_secs(2); let ru_header = ru_provider.get_block(BlockId::latest()).await.unwrap().unwrap().header.inner; - let number = ru_header.number + 1; - let timestamp = ru_header.timestamp + config.slot_calculator.slot_duration(); - let block_env = test_block_env(config, number, 7, timestamp); + let target_block_number = ru_header.number + 1; + let target_timestamp = ru_header.timestamp + config.slot_calculator.slot_duration(); + + assert!(target_timestamp > ru_header.timestamp); + let block_env = test_block_env(config, target_block_number, 7, target_timestamp); // Spawn the block builder task let sim_env = SimEnv { @@ -82,7 +84,7 @@ async fn test_handle_build() { assert!(got.unwrap().tx_count() == 2); } -/// End-to-end simulation flow using the TestHarness. +// TODO: Make this properly tick; something is wrong with the simulation deadline, it's not getting transactions before it exits. #[ignore = "integration test"] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_harness_ticks_and_emits() { @@ -92,17 +94,104 @@ async fn test_harness_ticks_and_emits() { let mut h = TestHarness::new().await.unwrap(); // Prepare two senders and fund them if needed from anvil default accounts - let keys = h.anvil.keys(); + let keys = h.rollup.anvil.anvil().keys(); let test_key_0 = PrivateKeySigner::from_signing_key(keys[0].clone().into()); + // Start simulator and tick a new SimEnv + h.start(); + // Add a transaction into the sim cache h.add_tx(&test_key_0, 0, U256::from(1_u64), 11_000); + // Tick host chain + h.update_host_environment().await; + + // Expect a SimResult. Use the harness slot duration plus a small buffer so + // we wait long enough for the simulator to complete heavy simulations. + let wait = Duration::from_secs(h.config.slot_calculator.slot_duration() + 5); + let got = h.recv_result(wait).await.expect("sim result"); + assert_eq!(got.block.tx_count(), 1); +} + +#[ignore = "integration test"] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_harness_simulates_full_flow() { + setup_logging(); + + // Build harness + let mut h = TestHarness::new().await.unwrap(); + + // Prepare two senders and fund them if needed from anvil default accounts + let keys = h.rollup.anvil.anvil().keys(); + let test_key_0 = PrivateKeySigner::from_signing_key(keys[0].clone().into()); + let test_key_1 = PrivateKeySigner::from_signing_key(keys[1].clone().into()); + + // Add two transactions into the sim cache + h.add_tx(&test_key_0, 0, U256::from(1_u64), 11_000); + h.add_tx(&test_key_1, 0, U256::from(2_u64), 10_000); + // Start simulator and tick a new SimEnv h.start(); - h.tick_from_ru_latest().await; - // Expect a SimResult - let got = h.recv_result(Duration::from_secs(5)).await.expect("sim result"); - assert_eq!(got.block.tx_count(), 1); + let prev_host_header = h + .host + .provider + .get_block(BlockId::latest()) + .await + .expect("latest block") + .expect("block exists") + .header + .inner; + + let prev_ru_header = h + .rollup + .provider + .get_block(BlockId::latest()) + .await + .expect("latest block") + .expect("block exists") + .header + .inner; + + h.tick_sim_env(prev_ru_header, prev_host_header).await; + + // Expect a SimResult. Use the harness slot duration plus a small buffer. + let wait = Duration::from_secs(h.config.slot_calculator.slot_duration() + 5); + let got = h.recv_result(wait).await.expect("sim result"); + assert_eq!(got.block.tx_count(), 2); +} + +/// Ensure the harness can manually advance the Anvil chain. +#[ignore = "integration test"] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_harness_advances_anvil_chain() { + setup_logging(); + + let h = TestHarness::new().await.unwrap(); + + let initial_number = h + .rollup + .provider + .get_block(BlockId::latest()) + .await + .expect("latest block") + .expect("block exists") + .header + .inner + .number; + + h.advance_blocks(2).await.expect("advance blocks"); + + let new_number = h + .rollup + .provider + .get_block(BlockId::latest()) + .await + .expect("latest block") + .expect("block exists") + .header + .inner + .number; + + assert_eq!(new_number, initial_number + 2); } diff --git a/tests/harness.rs b/tests/harness.rs index 79814b1..b132e82 100644 --- a/tests/harness.rs +++ b/tests/harness.rs @@ -4,14 +4,18 @@ //! mpsc submit channel so tests can tick BlockEnv values and assert on //! emitted SimResult blocks without involving submission logic. -use std::time::Duration; +use std::{ + sync::Arc, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; use alloy::{ + consensus::Header, eips::BlockId, network::Ethereum, - node_bindings::{Anvil, AnvilInstance}, + node_bindings::Anvil, primitives::U256, - providers::{Provider, RootProvider}, + providers::{Provider, RootProvider, ext::AnvilApi, layers::AnvilProvider}, signers::local::PrivateKeySigner, }; use builder::{ @@ -24,18 +28,42 @@ use builder::{ }; use signet_sim::SimCache; use signet_types::constants::SignetSystemConstants; -use tokio::sync::{mpsc, watch}; +use tokio::{ + sync::{mpsc, watch}, + task::JoinHandle, +}; + +const DEFAULT_BLOCK_TIME: u64 = 5; // seconds + +pub struct HostChain { + /// The provider for the host chain. + pub provider: RootProvider, + /// The Anvil provider for the host chain, used to control mining in tests. + pub anvil: AnvilProvider>, +} + +pub struct RollupChain { + pub provider: RootProvider, + pub anvil: AnvilProvider, +} + +pub struct SimulatorTask { + sim_env_tx: watch::Sender>, + sim_env_rx: watch::Receiver>, + sim_cache: SimCache, +} pub struct TestHarness { pub config: BuilderConfig, pub constants: SignetSystemConstants, - pub anvil: AnvilInstance, - pub ru_provider: RootProvider, - sim_env_tx: watch::Sender>, - sim_env_rx: watch::Receiver>, + pub rollup: RollupChain, + pub host: HostChain, + pub simulator: SimulatorTask, submit_tx: mpsc::UnboundedSender, submit_rx: mpsc::UnboundedReceiver, - sim_cache: SimCache, + /// Keeps the simulator task alive for the duration of the harness so the + /// background task isn't aborted when `start()` returns. + simulator_handle: Option>, } impl TestHarness { @@ -43,11 +71,27 @@ impl TestHarness { pub async fn new() -> eyre::Result { setup_logging(); - let config = setup_test_config()?; + let mut config = setup_test_config()?; + + // Ensure the slot calculator is aligned with the current time and has + // a sufficiently large slot duration so the simulator's deadline + // calculation yields a non-zero remaining window during tests. + let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + // Give a generous slot duration for tests (60s) + config.slot_calculator = init4_bin_base::utils::calc::SlotCalculator::new(now, 0, 10); let constants = SignetSystemConstants::pecorino(); - let anvil = Anvil::new().chain_id(signet_constants::pecorino::RU_CHAIN_ID).spawn(); - let ru_provider = RootProvider::::new_http(anvil.endpoint_url()); + // Create host anvil + let host_anvil = Anvil::new().chain_id(signet_constants::pecorino::HOST_CHAIN_ID).spawn(); + let host_provider = RootProvider::::new_http(host_anvil.endpoint_url().clone()); + let host_anvil_provider = AnvilProvider::new(host_provider.clone(), Arc::new(host_anvil)); + + // Create rollup anvil + let rollup_anvil = Anvil::new().chain_id(signet_constants::pecorino::RU_CHAIN_ID).spawn(); + let rollup_provider = + RootProvider::::new_http(rollup_anvil.endpoint_url().clone()); + let rollup_anvil_provider = + AnvilProvider::new(rollup_provider.clone(), Arc::new(rollup_anvil)); let (sim_env_tx, sim_env_rx) = watch::channel::>(None); let (submit_tx, submit_rx) = mpsc::unbounded_channel::(); @@ -55,13 +99,15 @@ impl TestHarness { Ok(Self { config, constants, - anvil, - ru_provider, - sim_env_tx, - sim_env_rx, + rollup: RollupChain { + provider: rollup_provider.clone(), + anvil: rollup_anvil_provider.clone(), + }, + host: HostChain { provider: host_provider.clone(), anvil: host_anvil_provider }, + simulator: SimulatorTask { sim_env_tx, sim_env_rx, sim_cache: SimCache::new() }, submit_tx, submit_rx, - sim_cache: SimCache::new(), + simulator_handle: None, }) } @@ -69,46 +115,108 @@ impl TestHarness { pub fn add_tx(&self, signer: &PrivateKeySigner, nonce: u64, value: U256, mpfpg: u128) { let tx = new_signed_tx(signer, nonce, value, mpfpg).expect("tx signing"); // group index 0 for simplicity in tests - self.sim_cache.add_tx(tx, 0); + self.simulator.sim_cache.add_tx(tx, 0); + } + + /// Mine additional blocks on the underlying Anvil instance. + pub async fn advance_blocks(&self, count: u64) -> eyre::Result<()> { + if count == 0 { + return Ok(()); + } + + self.host.anvil.anvil_mine(Some(count), Some(1)).await?; + self.rollup.anvil.anvil_mine(Some(count), Some(1)).await?; + + Ok(()) } /// Start the simulator task. - pub fn start(&self) { + pub fn start(&mut self) { + if self.simulator_handle.is_some() { + tracing::warn!("TestHarness simulator already running"); + return; + } + + tracing::debug!("TestHarness starting simulator task"); // Spawn the simulator background task - let cache = self.sim_cache.clone(); + let cache = self.simulator.sim_cache.clone(); let constants = self.constants.clone(); + + // Wire up the simulator and submit channels let submit_tx = self.submit_tx.clone(); - let simulator = - Simulator::new(&self.config, self.ru_provider.clone(), self.sim_env_rx.clone()); - let _jh = simulator.spawn_simulator_task(constants, cache, submit_tx); - // NB: We intentionally leak the JoinHandle in tests; tokio will cancel on drop. + let simulator = Simulator::new( + &self.config, + self.rollup.provider.clone(), + self.simulator.sim_env_rx.clone(), + ); + + // Keep the JoinHandle on the harness so the task isn't aborted when + // this function returns. + let jh = simulator.spawn_simulator_task(constants, cache, submit_tx); + self.simulator_handle = Some(jh); + tracing::debug!("TestHarness spawned simulator task"); } - /// Tick a new SimEnv computed from the current RU latest header. - pub async fn tick_from_ru_latest(&self) { + /// Tick a new SimEnv computed from the current host latest header. + pub async fn update_host_environment(&self) { let header = self - .ru_provider + .host + .provider .get_block(BlockId::latest()) .await - .expect("ru latest block") - .expect("ru latest exists") + .expect("host latest block") + .expect("host latest exists") .header .inner; - let number = header.number + 1; - let timestamp = header.timestamp + self.config.slot_calculator.slot_duration(); + let target_block_number = header.number + 1; + let deadline = header.timestamp + DEFAULT_BLOCK_TIME; + let block_env = test_block_env(self.config.clone(), target_block_number, 7, deadline); + + assert!(deadline > header.timestamp); + assert!(header.number < target_block_number); + + // Re-use host header as prev_host for harness; submit path doesn't run here. + let span = + tracing::info_span!("TestHarness::tick_from_host", target_block_number, deadline); + let sim_env = SimEnv { block_env, prev_header: header.clone(), prev_host: header, span }; + + let _ = self.simulator.sim_env_tx.send(Some(sim_env)); + } + + /// Tick a new SimEnv computed from the current RU latest header. + pub async fn tick_sim_env(&self, prev_header: Header, prev_host_header: Header) { + let target_block_number = prev_header.number + 1; + // Add a small buffer to the deadline so the test harness holds the + // simulation open long enough for the simulator to run to completion. + // We intentionally don't change simulator timing logic; instead we + // supply a BlockEnv timestamp that gives the simulator a reasonable + // remaining window. + let deadline = + prev_header.timestamp + self.config.slot_calculator.slot_duration() + DEFAULT_BLOCK_TIME; // A small basefee is fine for local testing - let block_env = test_block_env(self.config.clone(), number, 7, timestamp); + let block_env = test_block_env(self.config.clone(), target_block_number, 7, deadline); // Re-use RU header as prev_host for harness; submit path doesn't run here. - let span = tracing::info_span!("TestHarness::tick", number, timestamp); - let sim_env = SimEnv { block_env, prev_header: header.clone(), prev_host: header, span }; + let span = tracing::info_span!("TestHarness::tick", target_block_number, deadline); + let sim_env = SimEnv { block_env, prev_header: prev_header.clone(), prev_host: prev_host_header, span }; - let _ = self.sim_env_tx.send(Some(sim_env)); + let _ = self.simulator.sim_env_tx.send(Some(sim_env)); } /// Receive the next SimResult with a timeout. pub async fn recv_result(&mut self, timeout: Duration) -> Option { - tokio::time::timeout(timeout, self.submit_rx.recv()).await.ok().flatten() + tracing::debug!(?timeout, "TestHarness waiting for sim result"); + let res = tokio::time::timeout(timeout, self.submit_rx.recv()).await.ok().flatten(); + tracing::debug!(received = res.is_some(), "TestHarness recv_result returning"); + res + } +} + +impl Drop for TestHarness { + fn drop(&mut self) { + if let Some(handle) = self.simulator_handle.take() { + handle.abort(); + } } } From 7fa40e5d3ae8cb2e5509dd5bd87544932e49c5f2 Mon Sep 17 00:00:00 2001 From: dylan Date: Wed, 1 Oct 2025 20:25:54 -0600 Subject: [PATCH 08/15] more work on the harness --- tests/block_builder_test.rs | 49 ++++-------------------------- tests/harness.rs | 59 +++++++++++++++++++++++++++---------- 2 files changed, 49 insertions(+), 59 deletions(-) diff --git a/tests/block_builder_test.rs b/tests/block_builder_test.rs index 87ddd9b..41ec7cc 100644 --- a/tests/block_builder_test.rs +++ b/tests/block_builder_test.rs @@ -133,25 +133,7 @@ async fn test_harness_simulates_full_flow() { // Start simulator and tick a new SimEnv h.start(); - let prev_host_header = h - .host - .provider - .get_block(BlockId::latest()) - .await - .expect("latest block") - .expect("block exists") - .header - .inner; - - let prev_ru_header = h - .rollup - .provider - .get_block(BlockId::latest()) - .await - .expect("latest block") - .expect("block exists") - .header - .inner; + let (prev_ru_header, prev_host_header) = h.get_headers().await; h.tick_sim_env(prev_ru_header, prev_host_header).await; @@ -166,32 +148,13 @@ async fn test_harness_simulates_full_flow() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_harness_advances_anvil_chain() { setup_logging(); - let h = TestHarness::new().await.unwrap(); - let initial_number = h - .rollup - .provider - .get_block(BlockId::latest()) - .await - .expect("latest block") - .expect("block exists") - .header - .inner - .number; - + let (rollup, host) = h.get_headers().await; + h.advance_blocks(2).await.expect("advance blocks"); - let new_number = h - .rollup - .provider - .get_block(BlockId::latest()) - .await - .expect("latest block") - .expect("block exists") - .header - .inner - .number; - - assert_eq!(new_number, initial_number + 2); + let (new_rollup, new_host) = h.get_headers().await; + assert_eq!(new_rollup.number, rollup.number + 2); + assert_eq!(new_host.number, host.number + 2); } diff --git a/tests/harness.rs b/tests/harness.rs index b132e82..4cd9554 100644 --- a/tests/harness.rs +++ b/tests/harness.rs @@ -118,7 +118,7 @@ impl TestHarness { self.simulator.sim_cache.add_tx(tx, 0); } - /// Mine additional blocks on the underlying Anvil instance. + /// Mine additional blocks on the underlying Anvil instance and update the sim_env with the latest headers. pub async fn advance_blocks(&self, count: u64) -> eyre::Result<()> { if count == 0 { return Ok(()); @@ -127,10 +127,13 @@ impl TestHarness { self.host.anvil.anvil_mine(Some(count), Some(1)).await?; self.rollup.anvil.anvil_mine(Some(count), Some(1)).await?; + let (ru, host) = self.get_headers().await; + self.tick_sim_env(ru, host).await; + Ok(()) } - /// Start the simulator task. + /// Starts the simulator task. pub fn start(&mut self) { if self.simulator_handle.is_some() { tracing::warn!("TestHarness simulator already running"); @@ -157,6 +160,31 @@ impl TestHarness { tracing::debug!("TestHarness spawned simulator task"); } + /// Returns the latest rollup and host headers. + pub async fn get_headers(&self) -> (Header, Header) { + let ru_header = self + .rollup + .provider + .get_block(BlockId::latest()) + .await + .expect("rollup latest block") + .expect("rollup latest exists") + .header + .inner; + + let host_header = self + .host + .provider + .get_block(BlockId::latest()) + .await + .expect("host latest block") + .expect("host latest exists") + .header + .inner; + + (ru_header, host_header) + } + /// Tick a new SimEnv computed from the current host latest header. pub async fn update_host_environment(&self) { let header = self @@ -184,22 +212,21 @@ impl TestHarness { let _ = self.simulator.sim_env_tx.send(Some(sim_env)); } - /// Tick a new SimEnv computed from the current RU latest header. - pub async fn tick_sim_env(&self, prev_header: Header, prev_host_header: Header) { - let target_block_number = prev_header.number + 1; - // Add a small buffer to the deadline so the test harness holds the - // simulation open long enough for the simulator to run to completion. - // We intentionally don't change simulator timing logic; instead we - // supply a BlockEnv timestamp that gives the simulator a reasonable - // remaining window. + /// Tick a new `SimEnv` computed from the current latest rollup and host headers. + pub async fn tick_sim_env(&self, prev_ru_header: Header, prev_host_header: Header) { + let target_ru_block_number = prev_ru_header.number + 1; + + // Set new simulation deadline from previous header let deadline = - prev_header.timestamp + self.config.slot_calculator.slot_duration() + DEFAULT_BLOCK_TIME; - // A small basefee is fine for local testing - let block_env = test_block_env(self.config.clone(), target_block_number, 7, deadline); + prev_ru_header.timestamp + self.config.slot_calculator.slot_duration(); + + // Make a new block env from the previous rollup header and our new simulation deadline. + let block_env = test_block_env(self.config.clone(), target_ru_block_number, 7, deadline); + + let span = tracing::info_span!("TestHarness::tick", target_ru_block_number, deadline); - // Re-use RU header as prev_host for harness; submit path doesn't run here. - let span = tracing::info_span!("TestHarness::tick", target_block_number, deadline); - let sim_env = SimEnv { block_env, prev_header: prev_header.clone(), prev_host: prev_host_header, span }; + // Make a new SimEnv and send it to the simulator task. + let sim_env = SimEnv { block_env, prev_header: prev_ru_header.clone(), prev_host: prev_host_header, span }; let _ = self.simulator.sim_env_tx.send(Some(sim_env)); } From 7cf132087a327493adb671031d7efef096ee35f8 Mon Sep 17 00:00:00 2001 From: dylan Date: Wed, 1 Oct 2025 20:26:07 -0600 Subject: [PATCH 09/15] fmt --- tests/block_builder_test.rs | 9 ++++----- tests/harness.rs | 31 ++++++++++++++++++++++--------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/tests/block_builder_test.rs b/tests/block_builder_test.rs index 41ec7cc..3f15248 100644 --- a/tests/block_builder_test.rs +++ b/tests/block_builder_test.rs @@ -84,8 +84,7 @@ async fn test_handle_build() { assert!(got.unwrap().tx_count() == 2); } -// TODO: Make this properly tick; something is wrong with the simulation deadline, it's not getting transactions before it exits. -#[ignore = "integration test"] +// #[ignore = "integration test"] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_harness_ticks_and_emits() { setup_logging(); @@ -113,7 +112,7 @@ async fn test_harness_ticks_and_emits() { assert_eq!(got.block.tx_count(), 1); } -#[ignore = "integration test"] +// #[ignore = "integration test"] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_harness_simulates_full_flow() { setup_logging(); @@ -144,14 +143,14 @@ async fn test_harness_simulates_full_flow() { } /// Ensure the harness can manually advance the Anvil chain. -#[ignore = "integration test"] +// #[ignore = "integration test"] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_harness_advances_anvil_chain() { setup_logging(); let h = TestHarness::new().await.unwrap(); let (rollup, host) = h.get_headers().await; - + h.advance_blocks(2).await.expect("advance blocks"); let (new_rollup, new_host) = h.get_headers().await; diff --git a/tests/harness.rs b/tests/harness.rs index 4cd9554..f3e6a21 100644 --- a/tests/harness.rs +++ b/tests/harness.rs @@ -1,8 +1,12 @@ //! Test harness for end-to-end simulation flow. //! -//! This harness wires a Simulator to a manual SimEnv watch channel and an -//! mpsc submit channel so tests can tick BlockEnv values and assert on -//! emitted SimResult blocks without involving submission logic. +//! This harness wires a [`Simulator`] up to a manual [`SimEnv`] watch channel +//! and a submit channel so tests can manually control the simulation environment, +//! ticking along new blocks and asserting on the state of the simulator's results. +//! +//! It also manages two Anvil instances to simulate the rollup and host chains, +//! and provides utility functions for advancing blocks and adding transactions to +//! the simulator's mempool. use std::{ sync::Arc, @@ -71,6 +75,7 @@ impl TestHarness { pub async fn new() -> eyre::Result { setup_logging(); + // Make a new test config let mut config = setup_test_config()?; // Ensure the slot calculator is aligned with the current time and has @@ -93,6 +98,10 @@ impl TestHarness { let rollup_anvil_provider = AnvilProvider::new(rollup_provider.clone(), Arc::new(rollup_anvil)); + // Create a new sim cache. + let sim_cache = SimCache::new(); + + // Plumb the sim environment and submit channels let (sim_env_tx, sim_env_rx) = watch::channel::>(None); let (submit_tx, submit_rx) = mpsc::unbounded_channel::(); @@ -104,7 +113,7 @@ impl TestHarness { anvil: rollup_anvil_provider.clone(), }, host: HostChain { provider: host_provider.clone(), anvil: host_anvil_provider }, - simulator: SimulatorTask { sim_env_tx, sim_env_rx, sim_cache: SimCache::new() }, + simulator: SimulatorTask { sim_env_tx, sim_env_rx, sim_cache }, submit_tx, submit_rx, simulator_handle: None, @@ -183,7 +192,7 @@ impl TestHarness { .inner; (ru_header, host_header) - } + } /// Tick a new SimEnv computed from the current host latest header. pub async fn update_host_environment(&self) { @@ -216,9 +225,8 @@ impl TestHarness { pub async fn tick_sim_env(&self, prev_ru_header: Header, prev_host_header: Header) { let target_ru_block_number = prev_ru_header.number + 1; - // Set new simulation deadline from previous header - let deadline = - prev_ru_header.timestamp + self.config.slot_calculator.slot_duration(); + // Set new simulation deadline from previous header + let deadline = prev_ru_header.timestamp + self.config.slot_calculator.slot_duration(); // Make a new block env from the previous rollup header and our new simulation deadline. let block_env = test_block_env(self.config.clone(), target_ru_block_number, 7, deadline); @@ -226,7 +234,12 @@ impl TestHarness { let span = tracing::info_span!("TestHarness::tick", target_ru_block_number, deadline); // Make a new SimEnv and send it to the simulator task. - let sim_env = SimEnv { block_env, prev_header: prev_ru_header.clone(), prev_host: prev_host_header, span }; + let sim_env = SimEnv { + block_env, + prev_header: prev_ru_header.clone(), + prev_host: prev_host_header, + span, + }; let _ = self.simulator.sim_env_tx.send(Some(sim_env)); } From 6587d53de22bafc2bae0702679fe6d1d786ceaf0 Mon Sep 17 00:00:00 2001 From: dylan Date: Wed, 8 Oct 2025 16:10:51 -0600 Subject: [PATCH 10/15] refactor: simplify harness to only use anvil providers --- src/test_utils.rs | 13 ++++---- tests/block_builder_test.rs | 12 ++----- tests/harness.rs | 65 ++++++++++++++++--------------------- 3 files changed, 36 insertions(+), 54 deletions(-) diff --git a/src/test_utils.rs b/src/test_utils.rs index c239366..e4b6075 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -14,15 +14,14 @@ use init4_bin_base::{ perms::OAuthConfig, utils::{calc::SlotCalculator, provider::ProviderConfig}, }; -use signet_constants::SignetSystemConstants; +use signet_constants::{SignetSystemConstants, pecorino}; use std::env; use std::str::FromStr; use trevm::revm::{context::BlockEnv, context_interface::block::BlobExcessGasAndPrice}; /// Sets up a block builder with test values pub fn setup_test_config() -> Result { - let config = BuilderConfig { - // host_chain_id: signet_constants::pecorino::HOST_CHAIN_ID, + let pecorino_config = BuilderConfig { host_rpc: "ws://host-rpc.pecorino.signet.sh" .parse::() .map(ProviderConfig::new) @@ -32,10 +31,10 @@ pub fn setup_test_config() -> Result { .unwrap() .try_into() .unwrap(), - flashbots_endpoint: Some("https://relay-sepolia.flashbots.net:443".parse().unwrap()), + flashbots_endpoint: Some("https://host-builder-rpc.pecorino.signet.sh".parse().unwrap()), quincey_url: "http://localhost:8080".into(), sequencer_key: None, - builder_key: env::var("SEPOLIA_ETH_PRIV_KEY") + builder_key: env::var("BUILDER_KEY") .unwrap_or_else(|_| B256::repeat_byte(0x42).to_string()), builder_port: 8080, builder_rewards_address: Address::default(), @@ -56,7 +55,7 @@ pub fn setup_test_config() -> Result { max_host_gas_coefficient: Some(80), constants: SignetSystemConstants::pecorino(), }; - Ok(config) + Ok(pecorino_config) } /// Returns a new signed test transaction with the provided nonce, value, and mpfpg. @@ -67,7 +66,7 @@ pub fn new_signed_tx( mpfpg: u128, ) -> Result { let tx = TxEip1559 { - chain_id: 11155111, + chain_id: pecorino::RU_CHAIN_ID, nonce, max_fee_per_gas: 10_000_000, max_priority_fee_per_gas: mpfpg, diff --git a/tests/block_builder_test.rs b/tests/block_builder_test.rs index 3f15248..3f7cd1f 100644 --- a/tests/block_builder_test.rs +++ b/tests/block_builder_test.rs @@ -21,12 +21,6 @@ use std::time::{Duration, Instant}; mod harness; use harness::TestHarness; -/// Tests the `handle_build` method of the `Simulator`. -/// -/// This test sets up a simulated environment using Anvil, creates a block builder, -/// and verifies that the block builder can successfully build a block containing -/// transactions from multiple senders. -#[ignore = "integration test"] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_handle_build() { setup_logging(); @@ -84,7 +78,6 @@ async fn test_handle_build() { assert!(got.unwrap().tx_count() == 2); } -// #[ignore = "integration test"] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_harness_ticks_and_emits() { setup_logging(); @@ -93,7 +86,7 @@ async fn test_harness_ticks_and_emits() { let mut h = TestHarness::new().await.unwrap(); // Prepare two senders and fund them if needed from anvil default accounts - let keys = h.rollup.anvil.anvil().keys(); + let keys = h.rollup.anvil().keys(); let test_key_0 = PrivateKeySigner::from_signing_key(keys[0].clone().into()); // Start simulator and tick a new SimEnv @@ -112,7 +105,6 @@ async fn test_harness_ticks_and_emits() { assert_eq!(got.block.tx_count(), 1); } -// #[ignore = "integration test"] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_harness_simulates_full_flow() { setup_logging(); @@ -121,7 +113,7 @@ async fn test_harness_simulates_full_flow() { let mut h = TestHarness::new().await.unwrap(); // Prepare two senders and fund them if needed from anvil default accounts - let keys = h.rollup.anvil.anvil().keys(); + let keys = h.rollup.anvil().keys(); let test_key_0 = PrivateKeySigner::from_signing_key(keys[0].clone().into()); let test_key_1 = PrivateKeySigner::from_signing_key(keys[1].clone().into()); diff --git a/tests/harness.rs b/tests/harness.rs index f3e6a21..3ede80d 100644 --- a/tests/harness.rs +++ b/tests/harness.rs @@ -19,7 +19,7 @@ use alloy::{ network::Ethereum, node_bindings::Anvil, primitives::U256, - providers::{Provider, RootProvider, ext::AnvilApi, layers::AnvilProvider}, + providers::{ext::AnvilApi, layers::AnvilProvider, Provider, RootProvider}, signers::local::PrivateKeySigner, }; use builder::{ @@ -37,19 +37,8 @@ use tokio::{ task::JoinHandle, }; -const DEFAULT_BLOCK_TIME: u64 = 5; // seconds - -pub struct HostChain { - /// The provider for the host chain. - pub provider: RootProvider, - /// The Anvil provider for the host chain, used to control mining in tests. - pub anvil: AnvilProvider>, -} - -pub struct RollupChain { - pub provider: RootProvider, - pub anvil: AnvilProvider, -} +// 5 seconds of slot time means 3 seconds of simulation time. +const DEFAULT_SLOT_DURATION: u64 = 5; // seconds pub struct SimulatorTask { sim_env_tx: watch::Sender>, @@ -60,8 +49,8 @@ pub struct SimulatorTask { pub struct TestHarness { pub config: BuilderConfig, pub constants: SignetSystemConstants, - pub rollup: RollupChain, - pub host: HostChain, + pub rollup: RollupAnvilProvider, + pub host: HostAnvilProvider, pub simulator: SimulatorTask, submit_tx: mpsc::UnboundedSender, submit_rx: mpsc::UnboundedReceiver, @@ -70,6 +59,9 @@ pub struct TestHarness { simulator_handle: Option>, } +type HostAnvilProvider = AnvilProvider>; +type RollupAnvilProvider = AnvilProvider>; + impl TestHarness { /// Create a new harness with a fresh Anvil rollup chain and default test config. pub async fn new() -> eyre::Result { @@ -78,17 +70,12 @@ impl TestHarness { // Make a new test config let mut config = setup_test_config()?; - // Ensure the slot calculator is aligned with the current time and has - // a sufficiently large slot duration so the simulator's deadline - // calculation yields a non-zero remaining window during tests. - let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); - // Give a generous slot duration for tests (60s) - config.slot_calculator = init4_bin_base::utils::calc::SlotCalculator::new(now, 0, 10); - let constants = SignetSystemConstants::pecorino(); + configure_slot_timing(&mut config)?; // Create host anvil let host_anvil = Anvil::new().chain_id(signet_constants::pecorino::HOST_CHAIN_ID).spawn(); let host_provider = RootProvider::::new_http(host_anvil.endpoint_url().clone()); + // Wrap it in an AnvilProvider to keep it alive let host_anvil_provider = AnvilProvider::new(host_provider.clone(), Arc::new(host_anvil)); // Create rollup anvil @@ -107,12 +94,9 @@ impl TestHarness { Ok(Self { config, - constants, - rollup: RollupChain { - provider: rollup_provider.clone(), - anvil: rollup_anvil_provider.clone(), - }, - host: HostChain { provider: host_provider.clone(), anvil: host_anvil_provider }, + constants: SignetSystemConstants::pecorino(), + rollup: rollup_anvil_provider, + host: host_anvil_provider, simulator: SimulatorTask { sim_env_tx, sim_env_rx, sim_cache }, submit_tx, submit_rx, @@ -133,8 +117,8 @@ impl TestHarness { return Ok(()); } - self.host.anvil.anvil_mine(Some(count), Some(1)).await?; - self.rollup.anvil.anvil_mine(Some(count), Some(1)).await?; + self.host.anvil_mine(Some(count), Some(1)).await?; + self.rollup.anvil_mine(Some(count), Some(1)).await?; let (ru, host) = self.get_headers().await; self.tick_sim_env(ru, host).await; @@ -154,11 +138,14 @@ impl TestHarness { let cache = self.simulator.sim_cache.clone(); let constants = self.constants.clone(); - // Wire up the simulator and submit channels + // Create a rollup provider from the rollup anvil + let ru_provider = RootProvider::::new_http(self.rollup.anvil().endpoint_url()); + + // Wire up the simulator with that provider and submit channels let submit_tx = self.submit_tx.clone(); let simulator = Simulator::new( &self.config, - self.rollup.provider.clone(), + ru_provider, self.simulator.sim_env_rx.clone(), ); @@ -173,7 +160,6 @@ impl TestHarness { pub async fn get_headers(&self) -> (Header, Header) { let ru_header = self .rollup - .provider .get_block(BlockId::latest()) .await .expect("rollup latest block") @@ -183,7 +169,6 @@ impl TestHarness { let host_header = self .host - .provider .get_block(BlockId::latest()) .await .expect("host latest block") @@ -198,7 +183,6 @@ impl TestHarness { pub async fn update_host_environment(&self) { let header = self .host - .provider .get_block(BlockId::latest()) .await .expect("host latest block") @@ -207,7 +191,7 @@ impl TestHarness { .inner; let target_block_number = header.number + 1; - let deadline = header.timestamp + DEFAULT_BLOCK_TIME; + let deadline = header.timestamp + DEFAULT_SLOT_DURATION; let block_env = test_block_env(self.config.clone(), target_block_number, 7, deadline); assert!(deadline > header.timestamp); @@ -253,6 +237,13 @@ impl TestHarness { } } +// This function sets the slot timing to start now with a 10 second slot duration for tests. +fn configure_slot_timing(config: &mut BuilderConfig) -> Result<(), eyre::Error> { + let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + config.slot_calculator = init4_bin_base::utils::calc::SlotCalculator::new(now, 0, DEFAULT_SLOT_DURATION); + Ok(()) +} + impl Drop for TestHarness { fn drop(&mut self) { if let Some(handle) = self.simulator_handle.take() { From 3f62361a43dc27137143348e702d1763fd674dfe Mon Sep 17 00:00:00 2001 From: dylan Date: Wed, 8 Oct 2025 16:54:15 -0600 Subject: [PATCH 11/15] moar refactors --- tests/block_builder_test.rs | 21 +++++--- tests/harness.rs | 101 ++++++++++++++++++------------------ 2 files changed, 65 insertions(+), 57 deletions(-) diff --git a/tests/block_builder_test.rs b/tests/block_builder_test.rs index 3f7cd1f..3fd4eea 100644 --- a/tests/block_builder_test.rs +++ b/tests/block_builder_test.rs @@ -96,7 +96,7 @@ async fn test_harness_ticks_and_emits() { h.add_tx(&test_key_0, 0, U256::from(1_u64), 11_000); // Tick host chain - h.update_host_environment().await; + h.tick_from_host().await; // Expect a SimResult. Use the harness slot duration plus a small buffer so // we wait long enough for the simulator to complete heavy simulations. @@ -124,9 +124,9 @@ async fn test_harness_simulates_full_flow() { // Start simulator and tick a new SimEnv h.start(); - let (prev_ru_header, prev_host_header) = h.get_headers().await; + let (prev_ru_header, prev_host_header) = h.get_headers().await.unwrap(); - h.tick_sim_env(prev_ru_header, prev_host_header).await; + h.tick_from_headers(prev_ru_header, prev_host_header).await; // Expect a SimResult. Use the harness slot duration plus a small buffer. let wait = Duration::from_secs(h.config.slot_calculator.slot_duration() + 5); @@ -141,11 +141,20 @@ async fn test_harness_advances_anvil_chain() { setup_logging(); let h = TestHarness::new().await.unwrap(); - let (rollup, host) = h.get_headers().await; + let (rollup, host) = h.get_headers().await.unwrap(); - h.advance_blocks(2).await.expect("advance blocks"); + h.mine_blocks(2).await.expect("mine blocks"); - let (new_rollup, new_host) = h.get_headers().await; + let (new_rollup, new_host) = h.get_headers().await.unwrap(); assert_eq!(new_rollup.number, rollup.number + 2); assert_eq!(new_host.number, host.number + 2); } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_harness_stops() { + setup_logging(); + let mut h = TestHarness::new().await.unwrap(); + + h.start(); + h.stop().await; +} \ No newline at end of file diff --git a/tests/harness.rs b/tests/harness.rs index 3ede80d..2c703ef 100644 --- a/tests/harness.rs +++ b/tests/harness.rs @@ -1,11 +1,11 @@ //! Test harness for end-to-end simulation flow. //! //! This harness wires a [`Simulator`] up to a manual [`SimEnv`] watch channel -//! and a submit channel so tests can manually control the simulation environment, +//! and a submit channel so tests can manually control the simulation environment, //! ticking along new blocks and asserting on the state of the simulator's results. -//! -//! It also manages two Anvil instances to simulate the rollup and host chains, -//! and provides utility functions for advancing blocks and adding transactions to +//! +//! It also manages two Anvil instances to simulate the rollup and host chains, +//! and provides utility functions for advancing blocks and adding transactions to //! the simulator's mempool. use std::{ @@ -19,7 +19,7 @@ use alloy::{ network::Ethereum, node_bindings::Anvil, primitives::U256, - providers::{ext::AnvilApi, layers::AnvilProvider, Provider, RootProvider}, + providers::{Provider, RootProvider, ext::AnvilApi, layers::AnvilProvider}, signers::local::PrivateKeySigner, }; use builder::{ @@ -30,6 +30,7 @@ use builder::{ }, test_utils::{new_signed_tx, setup_logging, setup_test_config, test_block_env}, }; +use init4_bin_base::utils::calc::SlotCalculator; use signet_sim::SimCache; use signet_types::constants::SignetSystemConstants; use tokio::{ @@ -47,12 +48,19 @@ pub struct SimulatorTask { } pub struct TestHarness { + /// Builder configuration for the Harness pub config: BuilderConfig, + /// System constants for the Harness pub constants: SignetSystemConstants, + /// Anvil provider made from the Rollup Anvil instance pub rollup: RollupAnvilProvider, + /// Anvilk provider made from the Host Anvil instance pub host: HostAnvilProvider, + /// The Simulator task that is assembled each tick. pub simulator: SimulatorTask, + /// Transaction plumbing - Submit submit_tx: mpsc::UnboundedSender, + /// Transaction plumbing - Receive submit_rx: mpsc::UnboundedReceiver, /// Keeps the simulator task alive for the duration of the harness so the /// background task isn't aborted when `start()` returns. @@ -67,23 +75,12 @@ impl TestHarness { pub async fn new() -> eyre::Result { setup_logging(); - // Make a new test config + // Make a new test config and set its slot timing let mut config = setup_test_config()?; - - configure_slot_timing(&mut config)?; - - // Create host anvil - let host_anvil = Anvil::new().chain_id(signet_constants::pecorino::HOST_CHAIN_ID).spawn(); - let host_provider = RootProvider::::new_http(host_anvil.endpoint_url().clone()); - // Wrap it in an AnvilProvider to keep it alive - let host_anvil_provider = AnvilProvider::new(host_provider.clone(), Arc::new(host_anvil)); - - // Create rollup anvil - let rollup_anvil = Anvil::new().chain_id(signet_constants::pecorino::RU_CHAIN_ID).spawn(); - let rollup_provider = - RootProvider::::new_http(rollup_anvil.endpoint_url().clone()); - let rollup_anvil_provider = - AnvilProvider::new(rollup_provider.clone(), Arc::new(rollup_anvil)); + + // Spawn host and rollup anvil chains (providers + keeping anvil alive) + let host_anvil_provider = spawn_chain(signet_constants::pecorino::HOST_CHAIN_ID)?; + let rollup_anvil_provider = spawn_chain(signet_constants::pecorino::RU_CHAIN_ID)?; // Create a new sim cache. let sim_cache = SimCache::new(); @@ -112,7 +109,7 @@ impl TestHarness { } /// Mine additional blocks on the underlying Anvil instance and update the sim_env with the latest headers. - pub async fn advance_blocks(&self, count: u64) -> eyre::Result<()> { + pub async fn mine_blocks(&self, count: u64) -> eyre::Result<()> { if count == 0 { return Ok(()); } @@ -120,8 +117,8 @@ impl TestHarness { self.host.anvil_mine(Some(count), Some(1)).await?; self.rollup.anvil_mine(Some(count), Some(1)).await?; - let (ru, host) = self.get_headers().await; - self.tick_sim_env(ru, host).await; + let (ru, host) = self.get_headers().await?; + self.tick_from_headers(ru, host).await; Ok(()) } @@ -143,11 +140,8 @@ impl TestHarness { // Wire up the simulator with that provider and submit channels let submit_tx = self.submit_tx.clone(); - let simulator = Simulator::new( - &self.config, - ru_provider, - self.simulator.sim_env_rx.clone(), - ); + let simulator = + Simulator::new(&self.config, ru_provider, self.simulator.sim_env_rx.clone()); // Keep the JoinHandle on the harness so the task isn't aborted when // this function returns. @@ -157,30 +151,18 @@ impl TestHarness { } /// Returns the latest rollup and host headers. - pub async fn get_headers(&self) -> (Header, Header) { - let ru_header = self - .rollup - .get_block(BlockId::latest()) - .await - .expect("rollup latest block") - .expect("rollup latest exists") - .header - .inner; + pub async fn get_headers(&self) -> eyre::Result<(Header, Header)> { + let ru_block = self.rollup.get_block(BlockId::latest()).await?; + let ru_header = ru_block.expect("rollup latest exists").header.inner; - let host_header = self - .host - .get_block(BlockId::latest()) - .await - .expect("host latest block") - .expect("host latest exists") - .header - .inner; + let host_block = self.host.get_block(BlockId::latest()).await?; + let host_header = host_block.expect("host latest exists").header.inner; - (ru_header, host_header) + Ok((ru_header, host_header)) } /// Tick a new SimEnv computed from the current host latest header. - pub async fn update_host_environment(&self) { + pub async fn tick_from_host(&self) { let header = self .host .get_block(BlockId::latest()) @@ -197,7 +179,6 @@ impl TestHarness { assert!(deadline > header.timestamp); assert!(header.number < target_block_number); - // Re-use host header as prev_host for harness; submit path doesn't run here. let span = tracing::info_span!("TestHarness::tick_from_host", target_block_number, deadline); let sim_env = SimEnv { block_env, prev_header: header.clone(), prev_host: header, span }; @@ -206,7 +187,7 @@ impl TestHarness { } /// Tick a new `SimEnv` computed from the current latest rollup and host headers. - pub async fn tick_sim_env(&self, prev_ru_header: Header, prev_host_header: Header) { + pub async fn tick_from_headers(&self, prev_ru_header: Header, prev_host_header: Header) { let target_ru_block_number = prev_ru_header.number + 1; // Set new simulation deadline from previous header @@ -235,15 +216,33 @@ impl TestHarness { tracing::debug!(received = res.is_some(), "TestHarness recv_result returning"); res } + + /// Stop the background simulator task if running. + pub async fn stop(&mut self) { + if let Some(handle) = self.simulator_handle.take() { + // Abort and give it a short period to finish cleanup. + handle.abort(); + let _ = tokio::time::timeout(Duration::from_millis(200), handle).await; + } + } } // This function sets the slot timing to start now with a 10 second slot duration for tests. fn configure_slot_timing(config: &mut BuilderConfig) -> Result<(), eyre::Error> { let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); - config.slot_calculator = init4_bin_base::utils::calc::SlotCalculator::new(now, 0, DEFAULT_SLOT_DURATION); + config.slot_calculator = SlotCalculator::new(now, 0, DEFAULT_SLOT_DURATION); Ok(()) } +// Spawn an Anvil instance and return its provider and an AnvilProvider wrapper that +// keeps the Anvil process alive for the lifetime of the provider. +fn spawn_chain(chain_id: u64) -> eyre::Result>> { + let anvil = Anvil::new().chain_id(chain_id).spawn(); + let provider = RootProvider::::new_http(anvil.endpoint_url().clone()); + let anvil_provider = AnvilProvider::new(provider.clone(), Arc::new(anvil)); + Ok(anvil_provider) +} + impl Drop for TestHarness { fn drop(&mut self) { if let Some(handle) = self.simulator_handle.take() { From 078075a3d2d089c1d93af27fa4be75e08e832a13 Mon Sep 17 00:00:00 2001 From: dylan Date: Thu, 30 Oct 2025 19:17:24 -0600 Subject: [PATCH 12/15] improve span usage in test harness --- tests/block_builder_test.rs | 3 +-- tests/harness.rs | 42 ++++++++++++++++++++++++------------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/tests/block_builder_test.rs b/tests/block_builder_test.rs index 3fd4eea..a7cf55a 100644 --- a/tests/block_builder_test.rs +++ b/tests/block_builder_test.rs @@ -125,7 +125,6 @@ async fn test_harness_simulates_full_flow() { h.start(); let (prev_ru_header, prev_host_header) = h.get_headers().await.unwrap(); - h.tick_from_headers(prev_ru_header, prev_host_header).await; // Expect a SimResult. Use the harness slot duration plus a small buffer. @@ -157,4 +156,4 @@ async fn test_harness_stops() { h.start(); h.stop().await; -} \ No newline at end of file +} diff --git a/tests/harness.rs b/tests/harness.rs index 2c703ef..f5a9ddb 100644 --- a/tests/harness.rs +++ b/tests/harness.rs @@ -52,13 +52,13 @@ pub struct TestHarness { pub config: BuilderConfig, /// System constants for the Harness pub constants: SignetSystemConstants, - /// Anvil provider made from the Rollup Anvil instance + /// Anvil provider made from the Rollup Anvil instance pub rollup: RollupAnvilProvider, /// Anvilk provider made from the Host Anvil instance pub host: HostAnvilProvider, /// The Simulator task that is assembled each tick. pub simulator: SimulatorTask, - /// Transaction plumbing - Submit + /// Transaction plumbing - Submit submit_tx: mpsc::UnboundedSender, /// Transaction plumbing - Receive submit_rx: mpsc::UnboundedReceiver, @@ -70,14 +70,17 @@ pub struct TestHarness { type HostAnvilProvider = AnvilProvider>; type RollupAnvilProvider = AnvilProvider>; +type RollupHeader = Header; +type HostHeader = Header; +type Headers = (RollupHeader, HostHeader); + impl TestHarness { /// Create a new harness with a fresh Anvil rollup chain and default test config. pub async fn new() -> eyre::Result { setup_logging(); - - // Make a new test config and set its slot timing let mut config = setup_test_config()?; - + configure_slot_timing(&mut config)?; + // Spawn host and rollup anvil chains (providers + keeping anvil alive) let host_anvil_provider = spawn_chain(signet_constants::pecorino::HOST_CHAIN_ID)?; let rollup_anvil_provider = spawn_chain(signet_constants::pecorino::RU_CHAIN_ID)?; @@ -151,14 +154,15 @@ impl TestHarness { } /// Returns the latest rollup and host headers. - pub async fn get_headers(&self) -> eyre::Result<(Header, Header)> { + pub async fn get_headers(&self) -> eyre::Result { let ru_block = self.rollup.get_block(BlockId::latest()).await?; let ru_header = ru_block.expect("rollup latest exists").header.inner; let host_block = self.host.get_block(BlockId::latest()).await?; let host_header = host_block.expect("host latest exists").header.inner; - Ok((ru_header, host_header)) + let headers = (ru_header, host_header); + Ok(headers) } /// Tick a new SimEnv computed from the current host latest header. @@ -179,8 +183,13 @@ impl TestHarness { assert!(deadline > header.timestamp); assert!(header.number < target_block_number); - let span = - tracing::info_span!("TestHarness::tick_from_host", target_block_number, deadline); + let span = tracing::info_span!( + "TestHarness::tick_from_host", + target_block_number = target_block_number, + deadline = deadline, + prev_host_number = header.number, + prev_host_timestamp = header.timestamp + ); let sim_env = SimEnv { block_env, prev_header: header.clone(), prev_host: header, span }; let _ = self.simulator.sim_env_tx.send(Some(sim_env)); @@ -196,17 +205,20 @@ impl TestHarness { // Make a new block env from the previous rollup header and our new simulation deadline. let block_env = test_block_env(self.config.clone(), target_ru_block_number, 7, deadline); - let span = tracing::info_span!("TestHarness::tick", target_ru_block_number, deadline); + let span = tracing::info_span!( + "TestHarness::tick", + target_ru_block_number = target_ru_block_number, + deadline = deadline, + prev_ru_number = prev_ru_header.number, + prev_host_number = prev_host_header.number + ); - // Make a new SimEnv and send it to the simulator task. - let sim_env = SimEnv { + let _ = self.simulator.sim_env_tx.send(Some(SimEnv { block_env, prev_header: prev_ru_header.clone(), prev_host: prev_host_header, span, - }; - - let _ = self.simulator.sim_env_tx.send(Some(sim_env)); + })); } /// Receive the next SimResult with a timeout. From d3b0e1b2573dc84d7d9a3e40921c8281f51dc3f4 Mon Sep 17 00:00:00 2001 From: dylan Date: Thu, 30 Oct 2025 21:26:31 -0600 Subject: [PATCH 13/15] refactors --- tests/block_builder_test.rs | 20 +++++++++++--------- tests/harness.rs | 32 +------------------------------- 2 files changed, 12 insertions(+), 40 deletions(-) diff --git a/tests/block_builder_test.rs b/tests/block_builder_test.rs index a7cf55a..d46ad4d 100644 --- a/tests/block_builder_test.rs +++ b/tests/block_builder_test.rs @@ -38,22 +38,23 @@ async fn test_handle_build() { // Create a rollup provider let ru_provider = RootProvider::::new_http(anvil_instance.endpoint_url()); - let host_provider = config.connect_host_provider().await.unwrap(); - let block_env = - EnvTask::new(config.clone(), host_provider.clone(), ru_provider.clone()).spawn().0; + // Create a host provider + let host_provider = config.connect_host_provider().await.unwrap(); - let block_builder = - Simulator::new(&config, host_provider.clone(), ru_provider.clone(), block_env); + // Provide a dummy env receiver; this test calls handle_build directly and + // doesn't use the env watch channel. + let (_env_tx, env_rx) = tokio::sync::watch::channel(None); + let block_builder = Simulator::new(&config, host_provider, ru_provider.clone(), env_rx); // Setup a sim cache let sim_items = SimCache::new(); // Add two transactions from two senders to the sim cache - let tx_1 = new_signed_tx(&test_key_0, 0, U256::from(1_f64), 11_000).unwrap(); + let tx_1 = new_signed_tx(&test_key_0, 0, U256::from(1_u64), 11_000).unwrap(); sim_items.add_tx(tx_1, 0); - let tx_2 = new_signed_tx(&test_key_1, 0, U256::from(2_f64), 10_000).unwrap(); + let tx_2 = new_signed_tx(&test_key_1, 0, U256::from(2_u64), 10_000).unwrap(); sim_items.add_tx(tx_2, 0); // Setup the block envs @@ -95,8 +96,9 @@ async fn test_harness_ticks_and_emits() { // Add a transaction into the sim cache h.add_tx(&test_key_0, 0, U256::from(1_u64), 11_000); - // Tick host chain - h.tick_from_host().await; + // Tick using the latest rollup and host headers + let (prev_ru_header, prev_host_header) = h.get_headers().await.unwrap(); + h.tick_from_headers(prev_ru_header, prev_host_header).await; // Expect a SimResult. Use the harness slot duration plus a small buffer so // we wait long enough for the simulator to complete heavy simulations. diff --git a/tests/harness.rs b/tests/harness.rs index f5a9ddb..a18e419 100644 --- a/tests/harness.rs +++ b/tests/harness.rs @@ -38,7 +38,7 @@ use tokio::{ task::JoinHandle, }; -// 5 seconds of slot time means 3 seconds of simulation time. +// Default test slot duration (seconds) const DEFAULT_SLOT_DURATION: u64 = 5; // seconds pub struct SimulatorTask { @@ -165,36 +165,6 @@ impl TestHarness { Ok(headers) } - /// Tick a new SimEnv computed from the current host latest header. - pub async fn tick_from_host(&self) { - let header = self - .host - .get_block(BlockId::latest()) - .await - .expect("host latest block") - .expect("host latest exists") - .header - .inner; - - let target_block_number = header.number + 1; - let deadline = header.timestamp + DEFAULT_SLOT_DURATION; - let block_env = test_block_env(self.config.clone(), target_block_number, 7, deadline); - - assert!(deadline > header.timestamp); - assert!(header.number < target_block_number); - - let span = tracing::info_span!( - "TestHarness::tick_from_host", - target_block_number = target_block_number, - deadline = deadline, - prev_host_number = header.number, - prev_host_timestamp = header.timestamp - ); - let sim_env = SimEnv { block_env, prev_header: header.clone(), prev_host: header, span }; - - let _ = self.simulator.sim_env_tx.send(Some(sim_env)); - } - /// Tick a new `SimEnv` computed from the current latest rollup and host headers. pub async fn tick_from_headers(&self, prev_ru_header: Header, prev_host_header: Header) { let target_ru_block_number = prev_ru_header.number + 1; From 207e19e91c7fc73e8795858aa19175c9531467e9 Mon Sep 17 00:00:00 2001 From: dylan Date: Fri, 31 Oct 2025 15:03:29 -0600 Subject: [PATCH 14/15] refactors --- tests/block_builder_test.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/block_builder_test.rs b/tests/block_builder_test.rs index d46ad4d..324a2e7 100644 --- a/tests/block_builder_test.rs +++ b/tests/block_builder_test.rs @@ -97,8 +97,7 @@ async fn test_harness_ticks_and_emits() { h.add_tx(&test_key_0, 0, U256::from(1_u64), 11_000); // Tick using the latest rollup and host headers - let (prev_ru_header, prev_host_header) = h.get_headers().await.unwrap(); - h.tick_from_headers(prev_ru_header, prev_host_header).await; + h.mine_blocks(1).await.unwrap(); // Expect a SimResult. Use the harness slot duration plus a small buffer so // we wait long enough for the simulator to complete heavy simulations. @@ -126,8 +125,7 @@ async fn test_harness_simulates_full_flow() { // Start simulator and tick a new SimEnv h.start(); - let (prev_ru_header, prev_host_header) = h.get_headers().await.unwrap(); - h.tick_from_headers(prev_ru_header, prev_host_header).await; + h.mine_blocks(1).await.unwrap(); // Expect a SimResult. Use the harness slot duration plus a small buffer. let wait = Duration::from_secs(h.config.slot_calculator.slot_duration() + 5); @@ -144,7 +142,7 @@ async fn test_harness_advances_anvil_chain() { let (rollup, host) = h.get_headers().await.unwrap(); - h.mine_blocks(2).await.expect("mine blocks"); + h.mine_blocks(2).await.unwrap(); let (new_rollup, new_host) = h.get_headers().await.unwrap(); assert_eq!(new_rollup.number, rollup.number + 2); From 18ef5345d57ccba9adacf35ab8e9c525d7c14015 Mon Sep 17 00:00:00 2001 From: dylan Date: Thu, 20 Nov 2025 15:56:40 -0700 Subject: [PATCH 15/15] refactor for host simulation and add some test cases --- src/test_utils.rs | 7 +- tests/block_builder_test.rs | 115 +++++++++++++++++++++++++++-- tests/harness.rs | 143 +++++++++++++++++++++++++++--------- tests/tx_poller_test.rs | 4 +- 4 files changed, 221 insertions(+), 48 deletions(-) diff --git a/src/test_utils.rs b/src/test_utils.rs index e4b6075..8d7e8c7 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -58,17 +58,18 @@ pub fn setup_test_config() -> Result { Ok(pecorino_config) } -/// Returns a new signed test transaction with the provided nonce, value, and mpfpg. -pub fn new_signed_tx( +/// Returns a new signed test transaction with the provided nonce, value, mpfpg, and max fee. +pub fn new_signed_tx_with_max_fee( wallet: &PrivateKeySigner, nonce: u64, value: U256, mpfpg: u128, + max_fee_per_gas: u128, ) -> Result { let tx = TxEip1559 { chain_id: pecorino::RU_CHAIN_ID, nonce, - max_fee_per_gas: 10_000_000, + max_fee_per_gas, max_priority_fee_per_gas: mpfpg, to: TxKind::Call(Address::from_str("0x0000000000000000000000000000000000000000").unwrap()), value, diff --git a/tests/block_builder_test.rs b/tests/block_builder_test.rs index 324a2e7..c9b8cc1 100644 --- a/tests/block_builder_test.rs +++ b/tests/block_builder_test.rs @@ -11,9 +11,9 @@ use alloy::{ use builder::{ tasks::{ block::sim::Simulator, - env::{EnvTask, Environment, SimEnv}, + env::{Environment, SimEnv}, }, - test_utils::{new_signed_tx, setup_logging, setup_test_config, test_block_env}, + test_utils::{new_signed_tx_with_max_fee, setup_logging, setup_test_config, test_block_env}, }; use signet_sim::SimCache; use std::time::{Duration, Instant}; @@ -39,7 +39,7 @@ async fn test_handle_build() { // Create a rollup provider let ru_provider = RootProvider::::new_http(anvil_instance.endpoint_url()); - // Create a host provider + // Create a host provider let host_provider = config.connect_host_provider().await.unwrap(); // Provide a dummy env receiver; this test calls handle_build directly and @@ -51,10 +51,12 @@ async fn test_handle_build() { let sim_items = SimCache::new(); // Add two transactions from two senders to the sim cache - let tx_1 = new_signed_tx(&test_key_0, 0, U256::from(1_u64), 11_000).unwrap(); + let tx_1 = + new_signed_tx_with_max_fee(&test_key_0, 0, U256::from(1_u64), 11_000, 10_000_000).unwrap(); sim_items.add_tx(tx_1, 0); - let tx_2 = new_signed_tx(&test_key_1, 0, U256::from(2_u64), 10_000).unwrap(); + let tx_2 = + new_signed_tx_with_max_fee(&test_key_1, 0, U256::from(2_u64), 10_000, 10_000_000).unwrap(); sim_items.add_tx(tx_2, 0); // Setup the block envs @@ -91,7 +93,7 @@ async fn test_harness_ticks_and_emits() { let test_key_0 = PrivateKeySigner::from_signing_key(keys[0].clone().into()); // Start simulator and tick a new SimEnv - h.start(); + h.start().await; // Add a transaction into the sim cache h.add_tx(&test_key_0, 0, U256::from(1_u64), 11_000); @@ -123,7 +125,7 @@ async fn test_harness_simulates_full_flow() { h.add_tx(&test_key_1, 0, U256::from(2_u64), 10_000); // Start simulator and tick a new SimEnv - h.start(); + h.start().await; h.mine_blocks(1).await.unwrap(); @@ -154,6 +156,103 @@ async fn test_harness_stops() { setup_logging(); let mut h = TestHarness::new().await.unwrap(); - h.start(); + h.start().await; + + h.stop().await; + + assert_eq!(h.simulator_handle.is_none(), true); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_harness_timeout_without_results() { + setup_logging(); + let mut h = TestHarness::new().await.unwrap(); + + h.start().await; + + let wait = Duration::from_millis(250); + let got = h.recv_result(wait).await; + + h.stop().await; + + assert!(got.is_none(), "expected timeout when no blocks are mined"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_harness_start_is_idempotent() { + setup_logging(); + let mut h = TestHarness::new().await.unwrap(); + + h.start().await; + let first_id = h.simulator_handle.as_ref().expect("simulator handle").id(); + + h.start().await; + let second_id = h.simulator_handle.as_ref().expect("simulator handle").id(); + + h.stop().await; + + assert_eq!(first_id, second_id, "second start should reuse existing task"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_harness_stop_without_start() { + setup_logging(); + let mut h = TestHarness::new().await.unwrap(); + + h.stop().await; + + assert!(h.simulator_handle.is_none()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_harness_emits_multiple_results() { + setup_logging(); + let mut h = TestHarness::new().await.unwrap(); + + let keys = h.rollup.anvil().keys(); + let signer = PrivateKeySigner::from_signing_key(keys[0].clone().into()); + + // First tick uses a transaction added before the simulator starts. + h.add_tx(&signer, 0, U256::from(1_u64), 11_000); + h.start().await; + h.mine_blocks(1).await.unwrap(); + + let wait = Duration::from_secs(h.config.slot_calculator.slot_duration() + 5); + let first = h.recv_result(wait).await.expect("first sim result"); + assert_eq!(first.block.tx_count(), 1); + + // Second tick shouldn't need new transactions to emit a block. + h.mine_blocks(1).await.unwrap(); + let second = h.recv_result(wait).await.expect("second sim result"); + assert_eq!(second.block.tx_count(), 0); + assert_eq!(second.block.block_number(), first.block.block_number() + 1); + + h.stop().await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_harness_result_matches_headers() { + setup_logging(); + let mut h = TestHarness::new().await.unwrap(); + + let keys = h.rollup.anvil().keys(); + let signer = PrivateKeySigner::from_signing_key(keys[0].clone().into()); + + // Capture the headers the harness should target. + let (prev_rollup, prev_host) = h.get_headers().await.unwrap(); + + h.add_tx(&signer, 0, U256::from(1_u64), 11_000); + h.start().await; + h.mine_blocks(1).await.unwrap(); + + let wait = Duration::from_secs(h.config.slot_calculator.slot_duration() + 5); + let got = h.recv_result(wait).await.expect("sim result"); + + assert_eq!(got.block.tx_count(), 1); + assert_eq!(got.rollup_block_number(), prev_rollup.number + 2); + assert_eq!(got.host_block_number(), prev_host.number + 2); + assert_eq!(got.prev_rollup().number, prev_rollup.number + 1); + assert_eq!(got.prev_host().number, prev_host.number + 1); + h.stop().await; } diff --git a/tests/harness.rs b/tests/harness.rs index a18e419..8341eaa 100644 --- a/tests/harness.rs +++ b/tests/harness.rs @@ -15,31 +15,37 @@ use std::{ use alloy::{ consensus::Header, - eips::BlockId, - network::Ethereum, + eips::{BlockId, eip1559::BaseFeeParams}, + network::{Ethereum, EthereumWallet}, node_bindings::Anvil, - primitives::U256, - providers::{Provider, RootProvider, ext::AnvilApi, layers::AnvilProvider}, + primitives::{B256, U256}, + providers::{ + Provider, ProviderBuilder, RootProvider, + ext::AnvilApi, + fillers::{BlobGasFiller, SimpleNonceManager}, + layers::AnvilProvider, + }, signers::local::PrivateKeySigner, }; use builder::{ - config::BuilderConfig, + config::{BuilderConfig, HostProvider}, tasks::{ block::sim::{SimResult, Simulator}, - env::SimEnv, + env::{Environment, SimEnv}, }, - test_utils::{new_signed_tx, setup_logging, setup_test_config, test_block_env}, + test_utils::{new_signed_tx_with_max_fee, setup_logging, setup_test_config}, }; use init4_bin_base::utils::calc::SlotCalculator; use signet_sim::SimCache; -use signet_types::constants::SignetSystemConstants; use tokio::{ sync::{mpsc, watch}, task::JoinHandle, }; +use trevm::revm::{context::BlockEnv, context_interface::block::BlobExcessGasAndPrice}; // Default test slot duration (seconds) const DEFAULT_SLOT_DURATION: u64 = 5; // seconds +const TEST_TX_MAX_FEE: u128 = 1_000_000_000_000; pub struct SimulatorTask { sim_env_tx: watch::Sender>, @@ -50,21 +56,19 @@ pub struct SimulatorTask { pub struct TestHarness { /// Builder configuration for the Harness pub config: BuilderConfig, - /// System constants for the Harness - pub constants: SignetSystemConstants, /// Anvil provider made from the Rollup Anvil instance pub rollup: RollupAnvilProvider, - /// Anvilk provider made from the Host Anvil instance + /// Anvil provider made from the Host Anvil instance pub host: HostAnvilProvider, /// The Simulator task that is assembled each tick. pub simulator: SimulatorTask, /// Transaction plumbing - Submit - submit_tx: mpsc::UnboundedSender, + pub submit_tx: mpsc::UnboundedSender, /// Transaction plumbing - Receive - submit_rx: mpsc::UnboundedReceiver, + pub submit_rx: mpsc::UnboundedReceiver, /// Keeps the simulator task alive for the duration of the harness so the /// background task isn't aborted when `start()` returns. - simulator_handle: Option>, + pub simulator_handle: Option>, } type HostAnvilProvider = AnvilProvider>; @@ -94,7 +98,6 @@ impl TestHarness { Ok(Self { config, - constants: SignetSystemConstants::pecorino(), rollup: rollup_anvil_provider, host: host_anvil_provider, simulator: SimulatorTask { sim_env_tx, sim_env_rx, sim_cache }, @@ -106,7 +109,8 @@ impl TestHarness { /// Add a signed transaction from a provided signer to the sim cache. pub fn add_tx(&self, signer: &PrivateKeySigner, nonce: u64, value: U256, mpfpg: u128) { - let tx = new_signed_tx(signer, nonce, value, mpfpg).expect("tx signing"); + let tx = new_signed_tx_with_max_fee(signer, nonce, value, mpfpg, TEST_TX_MAX_FEE) + .expect("tx signing"); // group index 0 for simplicity in tests self.simulator.sim_cache.add_tx(tx, 0); } @@ -127,28 +131,34 @@ impl TestHarness { } /// Starts the simulator task. - pub fn start(&mut self) { + pub async fn start(&mut self) { if self.simulator_handle.is_some() { tracing::warn!("TestHarness simulator already running"); return; } - tracing::debug!("TestHarness starting simulator task"); + // Spawn the simulator background task let cache = self.simulator.sim_cache.clone(); - let constants = self.constants.clone(); - // Create a rollup provider from the rollup anvil + // Rollup provider let ru_provider = RootProvider::::new_http(self.rollup.anvil().endpoint_url()); - // Wire up the simulator with that provider and submit channels + // Host provider + let host_provider = self.host_provider().await; + + // Wire up the simulator with the providers and submit channels let submit_tx = self.submit_tx.clone(); - let simulator = - Simulator::new(&self.config, ru_provider, self.simulator.sim_env_rx.clone()); + let simulator = Simulator::new( + &self.config, + host_provider, + ru_provider, + self.simulator.sim_env_rx.clone(), + ); // Keep the JoinHandle on the harness so the task isn't aborted when // this function returns. - let jh = simulator.spawn_simulator_task(constants, cache, submit_tx); + let jh = simulator.spawn_simulator_task(cache, submit_tx); self.simulator_handle = Some(jh); tracing::debug!("TestHarness spawned simulator task"); } @@ -161,20 +171,21 @@ impl TestHarness { let host_block = self.host.get_block(BlockId::latest()).await?; let host_header = host_block.expect("host latest exists").header.inner; + tracing::debug!( + rollup_header_number = ru_header.number, + rollup_header_gas_limit = ru_header.gas_limit + ); + let headers = (ru_header, host_header); Ok(headers) } /// Tick a new `SimEnv` computed from the current latest rollup and host headers. pub async fn tick_from_headers(&self, prev_ru_header: Header, prev_host_header: Header) { + // Set new simulation deadline and target block number from previous header let target_ru_block_number = prev_ru_header.number + 1; - - // Set new simulation deadline from previous header let deadline = prev_ru_header.timestamp + self.config.slot_calculator.slot_duration(); - // Make a new block env from the previous rollup header and our new simulation deadline. - let block_env = test_block_env(self.config.clone(), target_ru_block_number, 7, deadline); - let span = tracing::info_span!( "TestHarness::tick", target_ru_block_number = target_ru_block_number, @@ -183,12 +194,13 @@ impl TestHarness { prev_host_number = prev_host_header.number ); - let _ = self.simulator.sim_env_tx.send(Some(SimEnv { - block_env, - prev_header: prev_ru_header.clone(), - prev_host: prev_host_header, - span, - })); + let host_env = build_host_environment(&self.config, prev_host_header); + let rollup_env = build_rollup_environment(&self.config, prev_ru_header); + + self.simulator + .sim_env_tx + .send(Some(SimEnv { host: host_env, rollup: rollup_env, span })) + .expect("send sim_env environment"); } /// Receive the next SimResult with a timeout. @@ -207,6 +219,25 @@ impl TestHarness { let _ = tokio::time::timeout(Duration::from_millis(200), handle).await; } } + + /// Returns a host provider configured with the builder's wallet and blob gas params + async fn host_provider(&self) -> HostProvider { + let wallet = EthereumWallet::from( + self.config.connect_builder_signer().await.expect("builder signer"), + ); + + let host_provider_inner = + RootProvider::::new_http(self.host.anvil().endpoint_url()); + + ProviderBuilder::new_with_network() + .disable_recommended_fillers() + .filler(BlobGasFiller) + .with_gas_estimation() + .with_nonce_management(SimpleNonceManager::default()) + .fetch_chain_id() + .wallet(wallet) + .connect_provider(host_provider_inner) + } } // This function sets the slot timing to start now with a 10 second slot duration for tests. @@ -216,6 +247,48 @@ fn configure_slot_timing(config: &mut BuilderConfig) -> Result<(), eyre::Error> Ok(()) } +/// Builds a host environment from the given prev_header for the _next_ block. +fn build_host_environment(config: &BuilderConfig, prev_header: Header) -> Environment { + let block_env = BlockEnv { + number: U256::from(prev_header.number + 1), + beneficiary: config.builder_rewards_address, + timestamp: U256::from(prev_header.timestamp + config.slot_calculator.slot_duration()), + gas_limit: config.max_host_gas(prev_header.gas_limit), + basefee: prev_header + .next_block_base_fee(BaseFeeParams::ethereum()) + .expect("signet has no non-1559 headers"), + difficulty: U256::ZERO, + prevrandao: Some(B256::random()), + blob_excess_gas_and_price: Some(BlobExcessGasAndPrice { + excess_blob_gas: 0, + blob_gasprice: 0, + }), + }; + + Environment::new(block_env, prev_header) +} + +/// Builds a rollup environment from the given prev_header for the _next_ block. +fn build_rollup_environment(config: &BuilderConfig, prev_header: Header) -> Environment { + let block_env = BlockEnv { + number: U256::from(prev_header.number + 1), + beneficiary: config.builder_rewards_address, + timestamp: U256::from(prev_header.timestamp + config.slot_calculator.slot_duration()), + gas_limit: config.rollup_block_gas_limit, + basefee: prev_header + .next_block_base_fee(BaseFeeParams::ethereum()) + .expect("signet has no non-1559 headers"), + difficulty: U256::ZERO, + prevrandao: Some(B256::random()), + blob_excess_gas_and_price: Some(BlobExcessGasAndPrice { + excess_blob_gas: 0, + blob_gasprice: 0, + }), + }; + + Environment::new(block_env, prev_header) +} + // Spawn an Anvil instance and return its provider and an AnvilProvider wrapper that // keeps the Anvil process alive for the lifetime of the provider. fn spawn_chain(chain_id: u64) -> eyre::Result>> { diff --git a/tests/tx_poller_test.rs b/tests/tx_poller_test.rs index e48338f..92be5dc 100644 --- a/tests/tx_poller_test.rs +++ b/tests/tx_poller_test.rs @@ -2,7 +2,7 @@ use alloy::{primitives::U256, signers::local::PrivateKeySigner}; use builder::{ config::BuilderConfig, tasks::cache::TxPoller, - test_utils::{new_signed_tx, setup_logging, setup_test_config}, + test_utils::{new_signed_tx_with_max_fee, setup_logging, setup_test_config}, }; // Import the refactored function use eyre::{Ok, Result}; @@ -34,7 +34,7 @@ async fn post_tx(config: &BuilderConfig) -> Result<()> { let client = reqwest::Client::new(); let wallet = PrivateKeySigner::random(); - let tx_envelope = new_signed_tx(&wallet, 1, U256::from(1), 10_000)?; + let tx_envelope = new_signed_tx_with_max_fee(&wallet, 1, U256::from(1), 10_000, 10_000_000)?; let url = format!("{}/transactions", config.tx_pool_url); let response = client.post(&url).json(&tx_envelope).send().await?;