diff --git a/Cargo.lock b/Cargo.lock index fedbeaee..7e80a00f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10917,6 +10917,7 @@ dependencies = [ "reth-rpc-api", "reth-rpc-eth-api", "reth-rpc-eth-types", + "reth-rpc-layer", "reth-rpc-server-types", "reth-scroll-chainspec", "reth-scroll-cli", diff --git a/Cargo.toml b/Cargo.toml index 180594f8..2578b31e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -169,6 +169,7 @@ reth-provider = { git = "https://github.com/scroll-tech/reth.git", tag = "scroll reth-rpc-api = { git = "https://github.com/scroll-tech/reth.git", tag = "scroll-v91", default-features = false } reth-rpc-eth-api = { git = "https://github.com/scroll-tech/reth.git", tag = "scroll-v91", default-features = false } reth-rpc-eth-types = { git = "https://github.com/scroll-tech/reth.git", tag = "scroll-v91", default-features = false } +reth-rpc-layer = { git = "https://github.com/scroll-tech/reth.git", tag = "scroll-v91", default-features = false } reth-rpc-server-types = { git = "https://github.com/scroll-tech/reth.git", tag = "scroll-v91", default-features = false } reth-storage-api = { git = "https://github.com/scroll-tech/reth.git", tag = "scroll-v91", default-features = false } reth-tasks = { git = "https://github.com/scroll-tech/reth.git", tag = "scroll-v91", default-features = false } diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml index 6f81447d..ddda6994 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -76,11 +76,16 @@ aws-config = "1.8.0" aws-sdk-kms = "1.76.0" # test-utils +alloy-eips = { workspace = true, optional = true } +alloy-rpc-types-eth = { workspace = true, optional = true } alloy-rpc-types-engine = { workspace = true, optional = true } reth-e2e-test-utils = { workspace = true, optional = true } reth-engine-local = { workspace = true, optional = true } reth-provider = { workspace = true, optional = true } +reth-rpc-layer = { workspace = true, optional = true } reth-rpc-server-types = { workspace = true, optional = true } +reth-storage-api = { workspace = true, optional = true } +reth-tokio-util = { workspace = true, optional = true } scroll-alloy-rpc-types-engine = { workspace = true, optional = true } scroll-alloy-rpc-types.workspace = true @@ -108,6 +113,7 @@ reth-e2e-test-utils.workspace = true reth-node-core.workspace = true reth-provider.workspace = true reth-primitives-traits.workspace = true +reth-rpc-layer.workspace = true reth-rpc-server-types.workspace = true reth-scroll-node = { workspace = true, features = ["test-utils"] } reth-storage-api.workspace = true @@ -140,10 +146,15 @@ test-utils = [ "rollup-node/test-utils", "reth-e2e-test-utils", "reth-rpc-server-types", + "reth-rpc-layer", + "reth-tokio-util", "scroll-alloy-rpc-types-engine", "alloy-rpc-types-engine", "reth-primitives-traits/test-utils", "reth-network-p2p/test-utils", "rollup-node-chain-orchestrator/test-utils", "scroll-network/test-utils", + "alloy-eips", + "reth-storage-api", + "alloy-rpc-types-eth", ] diff --git a/crates/node/src/test_utils/block_builder.rs b/crates/node/src/test_utils/block_builder.rs new file mode 100644 index 00000000..a7400eca --- /dev/null +++ b/crates/node/src/test_utils/block_builder.rs @@ -0,0 +1,183 @@ +//! Block building helpers for test fixtures. + +use super::fixture::TestFixture; +use crate::test_utils::EventAssertions; + +use alloy_primitives::B256; +use reth_primitives_traits::transaction::TxHashRef; +use reth_scroll_primitives::ScrollBlock; +use scroll_alloy_consensus::ScrollTransaction; + +/// Builder for constructing and validating blocks in tests. +#[derive(Debug)] +pub struct BlockBuilder<'a> { + fixture: &'a mut TestFixture, + expected_tx_hashes: Vec, + expected_tx_count: Option, + expected_base_fee: Option, + expected_block_number: Option, + expected_l1_message: Option, +} + +/// The assertion on the L1 messages. +#[derive(Debug)] +pub enum L1MessagesAssertion { + /// Expect at least a single L1 message. + ExpectL1Message, + /// Expect an exact number of L1 messages. + ExpectL1MessageCount(usize), +} + +impl L1MessagesAssertion { + /// Assert the L1 messages count is correct. + pub fn assert(&self, got: usize) -> eyre::Result<()> { + match self { + Self::ExpectL1Message => { + if got == 0 { + return Err(eyre::eyre!("Expected at least one L1 message, but block has none")); + } + } + Self::ExpectL1MessageCount(count) => { + if got != *count { + return Err(eyre::eyre!("Expected at {count} L1 messages, but block has {got}")); + } + } + } + Ok(()) + } +} + +impl<'a> BlockBuilder<'a> { + /// Create a new block builder. + pub(crate) fn new(fixture: &'a mut TestFixture) -> Self { + Self { + fixture, + expected_tx_hashes: Vec::new(), + expected_tx_count: None, + expected_block_number: None, + expected_base_fee: None, + expected_l1_message: None, + } + } + + /// Expect a specific transaction to be included in the block. + pub fn expect_tx(mut self, tx_hash: B256) -> Self { + self.expected_tx_hashes.push(tx_hash); + self + } + + /// Expect a specific number of transactions in the block. + pub const fn expect_tx_count(mut self, count: usize) -> Self { + self.expected_tx_count = Some(count); + self + } + + /// Expect a specific block number. + pub const fn expect_block_number(mut self, number: u64) -> Self { + self.expected_block_number = Some(number); + self + } + + /// Expect at least one L1 message in the block. + pub const fn expect_l1_message(mut self) -> Self { + self.expected_l1_message = Some(L1MessagesAssertion::ExpectL1Message); + self + } + + /// Expect a specific number of L1 messages in the block. + pub const fn expect_l1_message_count(mut self, count: usize) -> Self { + self.expected_l1_message = Some(L1MessagesAssertion::ExpectL1MessageCount(count)); + self + } + + /// Build the block and validate against expectations. + pub async fn build_and_await_block(self) -> eyre::Result { + let sequencer_node = &self.fixture.nodes[0]; + + // Get the sequencer from the rollup manager handle + let handle = &sequencer_node.rollup_manager_handle; + + // Trigger block building + handle.build_block(); + + // If extract the block number. + let expect = self.fixture.expect_event(); + let block = + if let Some(b) = self.expected_block_number { + expect.block_sequenced(b).await? + } else { + expect.extract(|e| { + if let rollup_node_chain_orchestrator::ChainOrchestratorEvent::BlockSequenced( + block, + ) = e + { + Some(block.clone()) + } else { + None + } + }).await?.first().expect("should have block sequenced").clone() + }; + + // Finally validate the block. + self.validate_block(&block) + } + + /// Validate the block against expectations. + fn validate_block(self, block: &ScrollBlock) -> eyre::Result { + // Check transaction count + if let Some(expected_count) = self.expected_tx_count { + if block.body.transactions.len() != expected_count { + return Err(eyre::eyre!( + "Expected {} transactions, but block has {}", + expected_count, + block.body.transactions.len() + )); + } + } + + // Check block number + if let Some(expected_number) = self.expected_block_number { + if block.header.number != expected_number { + return Err(eyre::eyre!( + "Expected {} number, but block has {}", + expected_number, + block.header.number + )); + } + } + + // Check specific transaction hashes + for expected_hash in &self.expected_tx_hashes { + if !block.body.transactions.iter().any(|tx| tx.tx_hash() == expected_hash) { + return Err(eyre::eyre!( + "Expected transaction {:?} not found in block", + expected_hash + )); + } + } + + // Check base fee + if let Some(expected_base_fee) = self.expected_base_fee { + let actual_base_fee = block + .header + .base_fee_per_gas + .ok_or_else(|| eyre::eyre!("Block has no base fee"))?; + if actual_base_fee != expected_base_fee { + return Err(eyre::eyre!( + "Expected base fee {}, but block has {}", + expected_base_fee, + actual_base_fee + )); + } + } + + // Check L1 messages + if let Some(assertion) = self.expected_l1_message { + let l1_message_count = + block.body.transactions.iter().filter(|tx| tx.queue_index().is_some()).count(); + assertion.assert(l1_message_count)?; + } + + Ok(block.clone()) + } +} diff --git a/crates/node/src/test_utils/event_utils.rs b/crates/node/src/test_utils/event_utils.rs new file mode 100644 index 00000000..80d38b7d --- /dev/null +++ b/crates/node/src/test_utils/event_utils.rs @@ -0,0 +1,346 @@ +//! Event handling and assertion utilities for test fixtures. + +use super::fixture::TestFixture; +use std::time::Duration; + +use futures::{FutureExt, StreamExt}; +use reth_scroll_primitives::ScrollBlock; +use rollup_node_chain_orchestrator::ChainOrchestratorEvent; +use rollup_node_primitives::ChainImport; +use tokio::time::timeout; + +/// The default event wait time. +pub const DEFAULT_EVENT_WAIT_TIMEOUT: Duration = Duration::from_secs(30); + +/// Builder for waiting for events on multiple nodes. +#[derive(Debug)] +pub struct EventWaiter<'a> { + fixture: &'a mut TestFixture, + node_indices: Vec, + timeout_duration: Duration, +} + +impl<'a> EventWaiter<'a> { + /// Create a new multi-node event waiter. + pub fn new(fixture: &'a mut TestFixture, node_indices: Vec) -> Self { + Self { fixture, node_indices, timeout_duration: Duration::from_secs(30) } + } + + /// Set a custom timeout for waiting. + pub const fn timeout(mut self, duration: Duration) -> Self { + self.timeout_duration = duration; + self + } + + /// Wait for block sequenced event on all specified nodes. + pub async fn block_sequenced(self, target: u64) -> eyre::Result { + self.wait_for_event_on_all(|e| { + if let ChainOrchestratorEvent::BlockSequenced(block) = e { + (block.header.number == target).then(|| block.clone()) + } else { + None + } + }) + .await + .map(|v| v.first().expect("should have block sequenced").clone()) + } + + /// Wait for chain consolidated event on all specified nodes. + pub async fn chain_consolidated(self) -> eyre::Result> { + self.wait_for_event_on_all(|e| { + if let ChainOrchestratorEvent::ChainConsolidated { from, to } = e { + Some((*from, *to)) + } else { + None + } + }) + .await + } + + /// Wait for chain extended event on all specified nodes. + pub async fn chain_extended(self, target: u64) -> eyre::Result<()> { + self.wait_for_event_on_all(|e| { + matches!(e, ChainOrchestratorEvent::ChainExtended(ChainImport{chain,..}) if chain.last().map(|b| b.header.number) >= Some(target)).then_some(()) + }) + .await?; + Ok(()) + } + + /// Wait for chain reorged event on all specified nodes. + pub async fn chain_reorged(self) -> eyre::Result<()> { + self.wait_for_event_on_all(|e| { + matches!(e, ChainOrchestratorEvent::ChainReorged(_)).then_some(()) + }) + .await?; + Ok(()) + } + + /// Wait for L1 synced event on all specified nodes. + pub async fn l1_synced(self) -> eyre::Result<()> { + self.wait_for_event_on_all(|e| matches!(e, ChainOrchestratorEvent::L1Synced).then_some(())) + .await?; + Ok(()) + } + + /// Wait for optimistic sync event on all specified nodes. + pub async fn optimistic_sync(self) -> eyre::Result<()> { + self.wait_for_event_on_all(|e| { + matches!(e, ChainOrchestratorEvent::OptimisticSync(_)).then_some(()) + }) + .await?; + Ok(()) + } + + /// Wait for new L1 block event on all specified nodes. + pub async fn new_l1_block(self) -> eyre::Result> { + self.wait_for_event_on_all(|e| { + if let ChainOrchestratorEvent::NewL1Block(block_number) = e { + Some(*block_number) + } else { + None + } + }) + .await + } + + /// Wait for L1 message committed event on all specified nodes. + pub async fn l1_message_committed(self) -> eyre::Result<()> { + self.wait_for_event_on_all(|e| { + matches!(e, ChainOrchestratorEvent::L1MessageCommitted(_)).then_some(()) + }) + .await?; + Ok(()) + } + + /// Wait for L1 reorg event to be received by all. + pub async fn l1_reorg(self) -> eyre::Result<()> { + self.wait_for_event_on_all(|e| { + matches!(e, ChainOrchestratorEvent::L1Reorg { .. }).then_some(()) + }) + .await?; + Ok(()) + } + + /// Wait for batch consolidated event on all specified nodes. + pub async fn batch_consolidated(self) -> eyre::Result<()> { + self.wait_for_event_on_all(|e| { + matches!(e, ChainOrchestratorEvent::BatchConsolidated(_)).then_some(()) + }) + .await?; + Ok(()) + } + + /// Wait for block consolidated event on all specified nodes. + pub async fn block_consolidated(self, target_block: u64) -> eyre::Result<()> { + self.wait_for_event_on_all(|e| { + if let ChainOrchestratorEvent::BlockConsolidated(outcome) = e { + (outcome.block_info().block_info.number == target_block).then_some(()) + } else { + None + } + }) + .await?; + Ok(()) + } + + /// Wait for batch reverted event on all specified nodes. + pub async fn batch_reverted(self) -> eyre::Result<()> { + self.wait_for_event_on_all(|e| { + matches!(e, ChainOrchestratorEvent::BatchReverted { .. }).then_some(()) + }) + .await?; + Ok(()) + } + + /// Wait for L1 block finalized event on all specified nodes. + pub async fn l1_block_finalized(self) -> eyre::Result<()> { + self.wait_for_event_on_all(|e| { + matches!(e, ChainOrchestratorEvent::L1BlockFinalized(_, _)).then_some(()) + }) + .await?; + Ok(()) + } + + /// Wait for new block received event on all specified nodes. + pub async fn new_block_received(self) -> eyre::Result { + self.wait_for_event_on_all(|e| { + if let ChainOrchestratorEvent::NewBlockReceived(block_with_peer) = e { + Some(block_with_peer.block.clone()) + } else { + None + } + }) + .await + .map(|v| v.first().expect("should have block received").clone()) + } + + /// Wait for any event where the predicate returns true on all specified nodes. + pub async fn where_event( + self, + predicate: impl Fn(&ChainOrchestratorEvent) -> bool, + ) -> eyre::Result> { + self.wait_for_event_on_all(move |e| predicate(e).then(|| e.clone())).await + } + + /// Wait for N events matching a predicate. + pub async fn where_n_events( + self, + count: usize, + mut predicate: impl FnMut(&ChainOrchestratorEvent) -> bool, + ) -> eyre::Result> { + let mut matched_events = Vec::new(); + for node in self.node_indices { + let events = &mut self.fixture.nodes[node].chain_orchestrator_rx; + let mut node_matched_events = Vec::new(); + + let result = timeout(self.timeout_duration, async { + while let Some(event) = events.next().await { + if predicate(&event) { + node_matched_events.push(event.clone()); + if node_matched_events.len() >= count { + return Ok(matched_events.clone()); + } + } + } + Err(eyre::eyre!("Event stream ended before matching {} events", count)) + }) + .await; + + match result { + Ok(_) => matched_events = node_matched_events, + Err(_) => { + return Err(eyre::eyre!( + "Timeout waiting for {} events (matched {} so far)", + count, + matched_events.len() + )) + } + } + } + + Ok(matched_events) + } + + /// Wait for any event and extract a value from it on all specified nodes. + pub async fn extract( + self, + extractor: impl Fn(&ChainOrchestratorEvent) -> Option, + ) -> eyre::Result> + where + T: Send + Clone + 'static, + { + self.wait_for_event_on_all(extractor).await + } + + /// Internal helper to wait for a specific event on all nodes. + async fn wait_for_event_on_all( + self, + extractor: impl Fn(&ChainOrchestratorEvent) -> Option, + ) -> eyre::Result> + where + T: Send + Clone + 'static, + { + let timeout_duration = self.timeout_duration; + let node_indices = self.node_indices; + let node_count = node_indices.len(); + + // Track which nodes have found their event + let mut results: Vec> = vec![None; node_count]; + let mut completed = 0; + + let result = timeout(timeout_duration, async { + loop { + // Poll each node's event stream + for (idx, &node_index) in node_indices.iter().enumerate() { + // Skip nodes that already found their event + if results[idx].is_some() { + continue; + } + + let events = &mut self.fixture.nodes[node_index].chain_orchestrator_rx; + + // Try to get the next event (non-blocking with try_next) + if let Some(event) = events.next().now_or_never() { + match event { + Some(event) => { + if let Some(value) = extractor(&event) { + results[idx] = Some(value); + completed += 1; + + if completed == node_count { + // All nodes have found their events + return Ok(results + .into_iter() + .map(|r| r.unwrap()) + .collect::>()); + } + } + } + None => { + return Err(eyre::eyre!( + "Event stream ended without matching event on node {}", + node_index + )); + } + } + } + } + + // Small delay to avoid busy waiting + tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; + } + }) + .await; + + result.unwrap_or_else(|_| { + Err(eyre::eyre!( + "Timeout waiting for event on {} nodes (completed {}/{})", + node_count, + completed, + node_count + )) + }) + } +} + +/// Extension trait for `TestFixture` to add event waiting capabilities. +pub trait EventAssertions { + /// Wait for an event on the sequencer node. + fn expect_event(&mut self) -> EventWaiter<'_>; + + /// Wait for an event on a specific node. + fn expect_event_on(&mut self, node_index: usize) -> EventWaiter<'_>; + + /// Wait for an event on multiple nodes. + fn expect_event_on_nodes(&mut self, node_indices: Vec) -> EventWaiter<'_>; + + /// Wait for an event on all nodes. + fn expect_event_on_all_nodes(&mut self) -> EventWaiter<'_>; + + /// Wait for an event on all follower nodes (excluding sequencer at index 0). + fn expect_event_on_followers(&mut self) -> EventWaiter<'_>; +} + +impl EventAssertions for TestFixture { + fn expect_event(&mut self) -> EventWaiter<'_> { + EventWaiter::new(self, vec![0]) + } + + fn expect_event_on(&mut self, node_index: usize) -> EventWaiter<'_> { + EventWaiter::new(self, vec![node_index]) + } + + fn expect_event_on_nodes(&mut self, node_indices: Vec) -> EventWaiter<'_> { + EventWaiter::new(self, node_indices) + } + + fn expect_event_on_all_nodes(&mut self) -> EventWaiter<'_> { + let node_indices = (0..self.nodes.len()).collect(); + EventWaiter::new(self, node_indices) + } + + fn expect_event_on_followers(&mut self) -> EventWaiter<'_> { + let node_indices = (1..self.nodes.len()).collect(); + EventWaiter::new(self, node_indices) + } +} diff --git a/crates/node/src/test_utils/fixture.rs b/crates/node/src/test_utils/fixture.rs new file mode 100644 index 00000000..ab1766ae --- /dev/null +++ b/crates/node/src/test_utils/fixture.rs @@ -0,0 +1,480 @@ +//! Core test fixture for setting up and managing test nodes. + +use super::{ + block_builder::BlockBuilder, l1_helpers::L1Helper, setup_engine, tx_helpers::TxHelper, +}; +use crate::{ + BlobProviderArgs, ChainOrchestratorArgs, ConsensusAlgorithm, ConsensusArgs, EngineDriverArgs, + L1ProviderArgs, RollupNodeDatabaseArgs, RollupNodeGasPriceOracleArgs, RollupNodeNetworkArgs, + RpcArgs, ScrollRollupNode, ScrollRollupNodeConfig, SequencerArgs, SignerArgs, +}; + +use alloy_eips::BlockNumberOrTag; +use alloy_primitives::Address; +use alloy_rpc_types_eth::Block; +use alloy_signer_local::PrivateKeySigner; +use reth_chainspec::EthChainSpec; +use reth_e2e_test_utils::{wallet::Wallet, NodeHelperType, TmpDB}; +use reth_eth_wire_types::BasicNetworkPrimitives; +use reth_network::NetworkHandle; +use reth_node_builder::NodeTypes; +use reth_node_types::NodeTypesWithDBAdapter; +use reth_provider::providers::BlockchainProvider; +use reth_scroll_chainspec::SCROLL_DEV; +use reth_scroll_primitives::ScrollPrimitives; +use reth_tasks::TaskManager; +use reth_tokio_util::EventStream; +use rollup_node_chain_orchestrator::{ChainOrchestratorEvent, ChainOrchestratorHandle}; +use rollup_node_primitives::BlockInfo; +use rollup_node_sequencer::L1MessageInclusionMode; +use rollup_node_watcher::L1Notification; +use scroll_alloy_consensus::ScrollPooledTransaction; +use scroll_alloy_provider::{ScrollAuthApiEngineClient, ScrollEngineApi}; +use scroll_alloy_rpc_types::Transaction; +use scroll_engine::{Engine, ForkchoiceState}; +use std::{ + fmt::{Debug, Formatter}, + path::PathBuf, + sync::Arc, +}; +use tokio::sync::{mpsc, Mutex}; + +/// Main test fixture providing a high-level interface for testing rollup nodes. +#[derive(Debug)] +pub struct TestFixture { + /// The list of nodes in the test setup. + pub nodes: Vec, + /// Shared wallet for generating transactions. + pub wallet: Arc>, + /// Chain spec used by the nodes. + pub chain_spec: Arc<::ChainSpec>, + /// The task manager. Held in order to avoid dropping the node. + _tasks: TaskManager, +} + +/// The network handle to the Scroll network. +pub type ScrollNetworkHandle = + NetworkHandle>; + +/// The blockchain test provider. +pub type TestBlockChainProvider = + BlockchainProvider>; + +/// The node type (sequencer or follower). +#[derive(Debug)] +pub enum NodeType { + /// A sequencer node. + Sequencer, + /// A follower node. + Follower, +} + +/// Handle to a single test node with its components. +pub struct NodeHandle { + /// The underlying node context. + pub node: NodeHelperType, + /// Engine instance for this node. + pub engine: Engine>, + /// L1 watcher notification channel. + pub l1_watcher_tx: Option>>, + /// Chain orchestrator listener. + pub chain_orchestrator_rx: EventStream, + /// Chain orchestrator handle. + pub rollup_manager_handle: ChainOrchestratorHandle, + /// The type of the node. + pub typ: NodeType, +} + +impl NodeHandle { + /// Returns true if this is a handle to the sequencer. + pub const fn is_sequencer(&self) -> bool { + matches!(self.typ, NodeType::Sequencer) + } + + /// Returns true if this is a handle to a follower. + pub const fn is_follower(&self) -> bool { + matches!(self.typ, NodeType::Follower) + } +} + +impl Debug for NodeHandle { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("NodeHandle") + .field("node", &"NodeHelper") + .field("engine", &"Box") + .field("l1_watcher_tx", &self.l1_watcher_tx) + .field("rollup_manager_handle", &self.rollup_manager_handle) + .finish() + } +} + +impl TestFixture { + /// Create a new test fixture builder with custom configuration. + pub fn builder() -> TestFixtureBuilder { + TestFixtureBuilder::new() + } + + /// Get the sequencer node (assumes first node is sequencer). + pub fn sequencer(&mut self) -> &mut NodeHandle { + let handle = &mut self.nodes[0]; + assert!(handle.is_sequencer(), "expected sequencer, got follower"); + handle + } + + /// Get a follower node by index. + pub fn follower(&mut self, index: usize) -> &mut NodeHandle { + if index == 0 && self.nodes[0].is_sequencer() { + return &mut self.nodes[index + 1]; + } + &mut self.nodes[index] + } + + /// Get the wallet. + pub fn wallet(&self) -> Arc> { + self.wallet.clone() + } + + /// Start building a block using the sequencer. + pub fn build_block(&mut self) -> BlockBuilder<'_> { + BlockBuilder::new(self) + } + + /// Get L1 helper for managing L1 interactions. + pub fn l1(&mut self) -> L1Helper<'_> { + L1Helper::new(self) + } + + /// Get transaction helper for creating and injecting transactions. + pub fn tx(&mut self) -> TxHelper<'_> { + TxHelper::new(self) + } + + /// Inject a simple transfer transaction and return its hash. + pub async fn inject_transfer(&mut self) -> eyre::Result { + self.tx().transfer().inject().await + } + + /// Inject a raw transaction into a specific node's pool. + pub async fn inject_tx_on( + &mut self, + node_index: usize, + tx: impl Into, + ) -> eyre::Result<()> { + self.nodes[node_index].node.rpc.inject_tx(tx.into()).await?; + Ok(()) + } + + /// Get the current (latest) block from a specific node. + pub async fn get_block(&self, node_index: usize) -> eyre::Result> { + use reth_rpc_api::EthApiServer; + + self.nodes[node_index] + .node + .rpc + .inner + .eth_api() + .block_by_number(BlockNumberOrTag::Latest, false) + .await? + .ok_or_else(|| eyre::eyre!("Latest block not found")) + } + + /// Get the current (latest) block from the sequencer node. + pub async fn get_sequencer_block(&self) -> eyre::Result> { + self.get_block(0).await + } + + /// Get the status (including forkchoice state) from a specific node. + pub async fn get_status( + &self, + node_index: usize, + ) -> eyre::Result { + self.nodes[node_index] + .rollup_manager_handle + .status() + .await + .map_err(|e| eyre::eyre!("Failed to get status: {}", e)) + } + + /// Get the status (including forkchoice state) from the sequencer node. + pub async fn get_sequencer_status( + &self, + ) -> eyre::Result { + self.get_status(0).await + } +} + +/// Builder for creating test fixtures with a fluent API. +#[derive(Debug)] +pub struct TestFixtureBuilder { + config: ScrollRollupNodeConfig, + num_nodes: usize, + chain_spec: Option::ChainSpec>>, + is_dev: bool, + no_local_transactions_propagation: bool, +} + +impl Default for TestFixtureBuilder { + fn default() -> Self { + Self::new() + } +} + +impl TestFixtureBuilder { + /// Create a new test fixture builder. + pub fn new() -> Self { + Self { + config: Self::default_config(), + num_nodes: 0, + chain_spec: None, + is_dev: false, + no_local_transactions_propagation: false, + } + } + + /// Returns the default rollup node config. + fn default_config() -> ScrollRollupNodeConfig { + ScrollRollupNodeConfig { + test: true, + network_args: RollupNodeNetworkArgs::default(), + database_args: RollupNodeDatabaseArgs::default(), + l1_provider_args: L1ProviderArgs::default(), + engine_driver_args: EngineDriverArgs { sync_at_startup: true }, + chain_orchestrator_args: ChainOrchestratorArgs { + optimistic_sync_trigger: 100, + chain_buffer_size: 100, + }, + sequencer_args: SequencerArgs { + payload_building_duration: 1000, + allow_empty_blocks: true, + ..Default::default() + }, + blob_provider_args: BlobProviderArgs { mock: true, ..Default::default() }, + signer_args: SignerArgs::default(), + gas_price_oracle_args: RollupNodeGasPriceOracleArgs::default(), + consensus_args: ConsensusArgs::noop(), + database: None, + rpc_args: RpcArgs { enabled: true }, + } + } + + /// Adds a sequencer node to the test with default settings. + pub fn sequencer(mut self) -> Self { + self.config.sequencer_args.sequencer_enabled = true; + self.config.sequencer_args.auto_start = false; + self.config.sequencer_args.block_time = 100; + self.config.sequencer_args.payload_building_duration = 40; + self.config.sequencer_args.l1_message_inclusion_mode = + L1MessageInclusionMode::BlockDepth(0); + self.config.sequencer_args.allow_empty_blocks = true; + self.config.database_args.rn_db_path = Some(PathBuf::from("sqlite::memory:")); + + self.num_nodes += 1; + self + } + + /// Adds `count`s follower nodes to the test. + pub const fn followers(mut self, count: usize) -> Self { + self.num_nodes += count; + self + } + + /// Toggle the test field. + pub const fn with_test(mut self, test: bool) -> Self { + self.config.test = test; + self + } + + /// Set the sequencer url for the node. + pub fn with_sequencer_url(mut self, url: String) -> Self { + self.config.network_args.sequencer_url = Some(url); + self + } + + /// Set the sequencer auto start for the node. + pub const fn with_sequencer_auto_start(mut self, auto_start: bool) -> Self { + self.config.sequencer_args.auto_start = auto_start; + self + } + + /// Set a custom chain spec. + pub fn with_chain_spec( + mut self, + spec: Arc<::ChainSpec>, + ) -> Self { + self.chain_spec = Some(spec); + self + } + + /// Enable dev mode. + pub const fn with_dev_mode(mut self, enabled: bool) -> Self { + self.is_dev = enabled; + self + } + + /// Disable local transaction propagation. + pub const fn no_local_tx_propagation(mut self) -> Self { + self.no_local_transactions_propagation = true; + self + } + + /// Set the block time for the sequencer. + pub const fn block_time(mut self, millis: u64) -> Self { + self.config.sequencer_args.block_time = millis; + self + } + + /// Set whether to allow empty blocks. + pub const fn allow_empty_blocks(mut self, allow: bool) -> Self { + self.config.sequencer_args.allow_empty_blocks = allow; + self + } + + /// Set L1 message inclusion mode with block depth. + pub const fn with_l1_message_delay(mut self, depth: u64) -> Self { + self.config.sequencer_args.l1_message_inclusion_mode = + L1MessageInclusionMode::BlockDepth(depth); + self + } + + /// Set L1 message inclusion mode to finalized with optional block depth. + pub const fn with_finalized_l1_messages(mut self, depth: u64) -> Self { + self.config.sequencer_args.l1_message_inclusion_mode = + L1MessageInclusionMode::FinalizedWithBlockDepth(depth); + self + } + + /// Use an in-memory `SQLite` database. + pub fn with_memory_db(mut self) -> Self { + self.config.database_args.rn_db_path = Some(PathBuf::from("sqlite::memory:")); + self + } + + /// Set a custom database path. + pub fn with_db_path(mut self, path: PathBuf) -> Self { + self.config.database_args.rn_db_path = Some(path); + self + } + + /// Use noop consensus (no validation). + pub const fn with_noop_consensus(mut self) -> Self { + self.config.consensus_args = ConsensusArgs::noop(); + self + } + + /// Use `SystemContract` consensus with the given authorized signer address. + pub const fn with_consensus_system_contract(mut self, authorized_signer: Address) -> Self { + self.config.consensus_args.algorithm = ConsensusAlgorithm::SystemContract; + self.config.consensus_args.authorized_signer = Some(authorized_signer); + self + } + + /// Set the private key signer for the node. + pub fn with_signer(mut self, signer: PrivateKeySigner) -> Self { + self.config.signer_args.private_key = Some(signer); + self + } + + /// Set the payload building duration in milliseconds. + pub const fn payload_building_duration(mut self, millis: u64) -> Self { + self.config.sequencer_args.payload_building_duration = millis; + self + } + + /// Set the fee recipient address. + pub const fn fee_recipient(mut self, address: Address) -> Self { + self.config.sequencer_args.fee_recipient = address; + self + } + + /// Enable auto-start for the sequencer. + pub const fn auto_start(mut self, enabled: bool) -> Self { + self.config.sequencer_args.auto_start = enabled; + self + } + + /// Set the maximum number of L1 messages per block. + pub const fn max_l1_messages(mut self, max: u64) -> Self { + self.config.sequencer_args.max_l1_messages = Some(max); + self + } + + /// Enable the Scroll wire protocol. + pub const fn with_scroll_wire(mut self, enabled: bool) -> Self { + self.config.network_args.enable_scroll_wire = enabled; + self + } + + /// Enable the ETH-Scroll wire bridge. + pub const fn with_eth_scroll_bridge(mut self, enabled: bool) -> Self { + self.config.network_args.enable_eth_scroll_wire_bridge = enabled; + self + } + + /// Set the optimistic sync trigger threshold. + pub const fn optimistic_sync_trigger(mut self, blocks: u64) -> Self { + self.config.chain_orchestrator_args.optimistic_sync_trigger = blocks; + self + } + + /// Get a mutable reference to the underlying config for advanced customization. + pub fn config_mut(&mut self) -> &mut ScrollRollupNodeConfig { + &mut self.config + } + + /// Build the test fixture. + pub async fn build(self) -> eyre::Result { + let config = self.config; + let chain_spec = self.chain_spec.unwrap_or_else(|| SCROLL_DEV.clone()); + + let (nodes, _tasks, wallet) = setup_engine( + config.clone(), + self.num_nodes, + chain_spec.clone(), + self.is_dev, + self.no_local_transactions_propagation, + ) + .await?; + + let mut node_handles = Vec::with_capacity(nodes.len()); + for (index, node) in nodes.into_iter().enumerate() { + let genesis_hash = node.inner.chain_spec().genesis_hash(); + + // Create engine for the node + let auth_client = node.inner.engine_http_client(); + let engine_client = Arc::new(ScrollAuthApiEngineClient::new(auth_client)) + as Arc; + let fcs = ForkchoiceState::new( + BlockInfo { hash: genesis_hash, number: 0 }, + Default::default(), + Default::default(), + ); + let engine = Engine::new(Arc::new(engine_client), fcs); + + // Get handles if available + let l1_watcher_tx = node.inner.add_ons_handle.l1_watcher_tx.clone(); + let rollup_manager_handle = node.inner.add_ons_handle.rollup_manager_handle.clone(); + let chain_orchestrator_rx = + node.inner.add_ons_handle.rollup_manager_handle.get_event_listener().await?; + + node_handles.push(NodeHandle { + node, + engine, + chain_orchestrator_rx, + l1_watcher_tx, + rollup_manager_handle, + typ: if config.sequencer_args.sequencer_enabled && index == 0 { + NodeType::Sequencer + } else { + NodeType::Follower + }, + }); + } + + Ok(TestFixture { + nodes: node_handles, + wallet: Arc::new(Mutex::new(wallet)), + chain_spec, + _tasks, + }) + } +} diff --git a/crates/node/src/test_utils/l1_helpers.rs b/crates/node/src/test_utils/l1_helpers.rs new file mode 100644 index 00000000..9a54d5cf --- /dev/null +++ b/crates/node/src/test_utils/l1_helpers.rs @@ -0,0 +1,455 @@ +//! L1 interaction helpers for test fixtures. + +use super::fixture::TestFixture; +use std::{fmt::Debug, str::FromStr, sync::Arc}; + +use alloy_primitives::{Address, Bytes, B256, U256}; +use rollup_node_primitives::{BatchCommitData, BlockInfo, ConsensusUpdate}; +use rollup_node_watcher::L1Notification; +use scroll_alloy_consensus::TxL1Message; + +/// Helper for managing L1 interactions in tests. +#[derive(Debug)] +pub struct L1Helper<'a> { + fixture: &'a mut TestFixture, + target_node_index: Option, +} + +impl<'a> L1Helper<'a> { + /// Create a new L1 helper. + pub(crate) fn new(fixture: &'a mut TestFixture) -> Self { + Self { fixture, target_node_index: None } + } + + /// Target a specific node for L1 notifications (default is all nodes). + pub const fn for_node(mut self, index: usize) -> Self { + self.target_node_index = Some(index); + self + } + + /// Send a notification that L1 is synced. + pub async fn sync(self) -> eyre::Result<()> { + let notification = Arc::new(L1Notification::Synced); + self.send_to_nodes(notification).await + } + + /// Send a new L1 block notification. + pub async fn new_block(self, block_number: u64) -> eyre::Result<()> { + let notification = Arc::new(L1Notification::NewBlock(BlockInfo { + number: block_number, + hash: B256::random(), + })); + self.send_to_nodes(notification).await + } + + /// Send an L1 reorg notification. + pub async fn reorg_to(self, block_number: u64) -> eyre::Result<()> { + let notification = Arc::new(L1Notification::Reorg(block_number)); + self.send_to_nodes(notification).await + } + + /// Send an L1 consensus notification. + pub async fn signer_update(self, new_signer: Address) -> eyre::Result<()> { + let notification = + Arc::new(L1Notification::Consensus(ConsensusUpdate::AuthorizedSigner(new_signer))); + self.send_to_nodes(notification).await + } + + /// Send an L1 reorg notification. + pub async fn batch_commit(self, calldata_path: Option<&str>, index: u64) -> eyre::Result<()> { + let raw_calldata = calldata_path + .map(|path| { + Result::<_, eyre::Report>::Ok(Bytes::from_str(&std::fs::read_to_string(path)?)?) + }) + .transpose()? + .unwrap_or_default(); + let batch_data = BatchCommitData { + hash: B256::random(), + index, + block_number: 0, + block_timestamp: 0, + calldata: Arc::new(raw_calldata), + blob_versioned_hash: None, + finalized_block_number: None, + reverted_block_number: None, + }; + + let notification = Arc::new(L1Notification::BatchCommit { + block_info: BlockInfo { number: 0, hash: B256::random() }, + data: batch_data, + }); + self.send_to_nodes(notification).await + } + + /// Create a batch commit builder for more control. + pub fn commit_batch(self) -> BatchCommitBuilder<'a> { + BatchCommitBuilder::new(self) + } + + /// Create a batch finalization builder. + pub fn finalize_batch(self) -> BatchFinalizationBuilder<'a> { + BatchFinalizationBuilder::new(self) + } + + /// Create a batch revert builder. + pub fn revert_batch(self) -> BatchRevertBuilder<'a> { + BatchRevertBuilder::new(self) + } + + /// Send an L1 finalized block notification. + pub async fn finalize_l1_block(self, block_number: u64) -> eyre::Result<()> { + let notification = Arc::new(L1Notification::Finalized(block_number)); + self.send_to_nodes(notification).await + } + + /// Create a new L1 message builder. + pub fn add_message(self) -> L1MessageBuilder<'a> { + L1MessageBuilder::new(self) + } + + /// Send notification to target nodes. + async fn send_to_nodes(&self, notification: Arc) -> eyre::Result<()> { + let nodes = if let Some(index) = self.target_node_index { + vec![&self.fixture.nodes[index]] + } else { + self.fixture.nodes.iter().collect() + }; + + for node in nodes { + if let Some(tx) = &node.l1_watcher_tx { + tx.send(notification.clone()).await?; + } + } + + Ok(()) + } +} + +/// Builder for creating L1 messages in tests. +#[derive(Debug)] +pub struct L1MessageBuilder<'a> { + l1_helper: L1Helper<'a>, + l1_block_number: u64, + queue_index: u64, + gas_limit: u64, + to: Address, + value: U256, + sender: Option
, + input: Bytes, +} + +impl<'a> L1MessageBuilder<'a> { + /// Create a new L1 message builder. + fn new(l1_helper: L1Helper<'a>) -> Self { + Self { + l1_helper, + l1_block_number: 0, + queue_index: 0, + gas_limit: 21000, + to: Address::random(), + value: U256::from(1), + sender: None, + input: Bytes::default(), + } + } + + /// Set the L1 block number for this message. + pub const fn at_block(mut self, block_number: u64) -> Self { + self.l1_block_number = block_number; + self + } + + /// Set the queue index for this message. + pub const fn queue_index(mut self, index: u64) -> Self { + self.queue_index = index; + self + } + + /// Set the gas limit for this message. + pub const fn gas_limit(mut self, limit: u64) -> Self { + self.gas_limit = limit; + self + } + + /// Set the recipient address. + pub const fn to(mut self, address: Address) -> Self { + self.to = address; + self + } + + /// Set the value to send. + pub fn value(mut self, value: impl TryInto) -> Self { + self.value = value.try_into().expect("should convert to U256"); + self + } + + /// Set the sender address. + pub const fn sender(mut self, address: Address) -> Self { + self.sender = Some(address); + self + } + + /// Set the input data. + pub fn input(mut self, data: Bytes) -> Self { + self.input = data; + self + } + + /// Send the L1 message to the database and notify nodes. + pub async fn send(self) -> eyre::Result { + let sender = self.sender.unwrap_or_else(|| Address::random()); + + let tx_l1_message = TxL1Message { + queue_index: self.queue_index, + gas_limit: self.gas_limit, + to: self.to, + value: self.value, + sender, + input: self.input, + }; + + // Send notification to nodes + let notification = Arc::new(L1Notification::L1Message { + message: tx_l1_message.clone(), + block_timestamp: self.l1_block_number * 10, + block_info: BlockInfo { number: self.l1_block_number, hash: B256::random() }, + }); + + let nodes = if let Some(index) = self.l1_helper.target_node_index { + vec![&self.l1_helper.fixture.nodes[index]] + } else { + self.l1_helper.fixture.nodes.iter().collect() + }; + + for node in nodes { + if let Some(tx) = &node.l1_watcher_tx { + tx.send(notification.clone()).await?; + } + } + + Ok(tx_l1_message) + } +} + +/// Builder for creating batch commit notifications in tests. +#[derive(Debug)] +pub struct BatchCommitBuilder<'a> { + l1_helper: L1Helper<'a>, + block_info: BlockInfo, + hash: B256, + index: u64, + block_number: u64, + block_timestamp: u64, + calldata: Option, + calldata_path: Option, + blob_versioned_hash: Option, +} + +impl<'a> BatchCommitBuilder<'a> { + fn new(l1_helper: L1Helper<'a>) -> Self { + Self { + l1_helper, + block_info: BlockInfo { number: 0, hash: B256::random() }, + hash: B256::random(), + index: 0, + block_number: 0, + block_timestamp: 0, + calldata: None, + calldata_path: None, + blob_versioned_hash: None, + } + } + + /// Set the L1 block info for this batch commit. + pub const fn at_block(mut self, block_info: BlockInfo) -> Self { + self.block_info = block_info; + self + } + + /// Set the L1 block number for this batch commit. + pub const fn at_block_number(mut self, block_number: u64) -> Self { + self.block_info.number = block_number; + self + } + + /// Set the batch hash. + pub const fn hash(mut self, hash: B256) -> Self { + self.hash = hash; + self + } + + /// Set the batch index. + pub const fn index(mut self, index: u64) -> Self { + self.index = index; + self + } + + /// Set the batch block number. + pub const fn block_number(mut self, block_number: u64) -> Self { + self.block_number = block_number; + self + } + + /// Set the batch block timestamp. + pub const fn block_timestamp(mut self, timestamp: u64) -> Self { + self.block_timestamp = timestamp; + self + } + + /// Set the calldata directly. + pub fn calldata(mut self, calldata: Bytes) -> Self { + self.calldata = Some(calldata); + self + } + + /// Set the calldata from a file path. + pub fn calldata_from_file(mut self, path: impl Into) -> Self { + self.calldata_path = Some(path.into()); + self + } + + /// Set the blob versioned hash. + pub const fn blob_versioned_hash(mut self, hash: B256) -> Self { + self.blob_versioned_hash = Some(hash); + self + } + + /// Send the batch commit notification. + pub async fn send(self) -> eyre::Result<(B256, u64)> { + let raw_calldata = if let Some(calldata) = self.calldata { + calldata + } else if let Some(path) = self.calldata_path { + Bytes::from_str(&std::fs::read_to_string(path)?)? + } else { + Bytes::default() + }; + + let batch_data = BatchCommitData { + hash: self.hash, + index: self.index, + block_number: self.block_number, + block_timestamp: self.block_timestamp, + calldata: Arc::new(raw_calldata), + blob_versioned_hash: self.blob_versioned_hash, + finalized_block_number: None, + reverted_block_number: None, + }; + + let notification = + Arc::new(L1Notification::BatchCommit { block_info: self.block_info, data: batch_data }); + + self.l1_helper.send_to_nodes(notification).await?; + Ok((self.hash, self.index)) + } +} + +/// Builder for creating batch finalization notifications in tests. +#[derive(Debug)] +pub struct BatchFinalizationBuilder<'a> { + l1_helper: L1Helper<'a>, + block_info: BlockInfo, + hash: B256, + index: u64, +} + +impl<'a> BatchFinalizationBuilder<'a> { + fn new(l1_helper: L1Helper<'a>) -> Self { + Self { + l1_helper, + block_info: BlockInfo { number: 0, hash: B256::random() }, + hash: B256::random(), + index: 0, + } + } + + /// Set the L1 block info for this batch finalization. + pub const fn at_block(mut self, block_info: BlockInfo) -> Self { + self.block_info = block_info; + self + } + + /// Set the L1 block number for this batch finalization. + pub const fn at_block_number(mut self, block_number: u64) -> Self { + self.block_info.number = block_number; + self + } + + /// Set the batch hash. + pub const fn hash(mut self, hash: B256) -> Self { + self.hash = hash; + self + } + + /// Set the batch index. + pub const fn index(mut self, index: u64) -> Self { + self.index = index; + self + } + + /// Send the batch finalization notification. + pub async fn send(self) -> eyre::Result<()> { + let notification = Arc::new(L1Notification::BatchFinalization { + hash: self.hash, + index: self.index, + block_info: self.block_info, + }); + + self.l1_helper.send_to_nodes(notification).await + } +} + +/// Builder for creating batch revert notifications in tests. +#[derive(Debug)] +pub struct BatchRevertBuilder<'a> { + l1_helper: L1Helper<'a>, + block_info: BlockInfo, + hash: B256, + index: u64, +} + +impl<'a> BatchRevertBuilder<'a> { + fn new(l1_helper: L1Helper<'a>) -> Self { + Self { + l1_helper, + block_info: BlockInfo { number: 0, hash: B256::random() }, + hash: B256::random(), + index: 0, + } + } + + /// Set the L1 block info for this batch revert. + pub const fn at_block(mut self, block_info: BlockInfo) -> Self { + self.block_info = block_info; + self + } + + /// Set the L1 block number for this batch revert. + pub const fn at_block_number(mut self, block_number: u64) -> Self { + self.block_info.number = block_number; + self + } + + /// Set the batch hash. + pub const fn hash(mut self, hash: B256) -> Self { + self.hash = hash; + self + } + + /// Set the batch index. + pub const fn index(mut self, index: u64) -> Self { + self.index = index; + self + } + + /// Send the batch revert notification. + pub async fn send(self) -> eyre::Result<()> { + use rollup_node_primitives::BatchInfo; + + let notification = Arc::new(L1Notification::BatchRevert { + batch_info: BatchInfo { hash: self.hash, index: self.index }, + block_info: self.block_info, + }); + + self.l1_helper.send_to_nodes(notification).await + } +} diff --git a/crates/node/src/test_utils.rs b/crates/node/src/test_utils/mod.rs similarity index 70% rename from crates/node/src/test_utils.rs rename to crates/node/src/test_utils/mod.rs index dd668222..00f075c5 100644 --- a/crates/node/src/test_utils.rs +++ b/crates/node/src/test_utils/mod.rs @@ -1,10 +1,84 @@ -//! This crate contains utilities for running end-to-end tests for the scroll reth node. +//! Test utilities for the Scroll rollup node. +//! +//! This module provides a high-level test framework for creating and managing +//! test nodes, building blocks, managing L1 interactions, and asserting on events. +//! +//! # Quick Start +//! +//! ```rust,ignore +//! use rollup_node::test_utils::TestFixture; +//! +//! #[tokio::test] +//! async fn test_basic_block_production() -> eyre::Result<()> { +//! let mut fixture = TestFixture::sequencer().build().await?; +//! +//! // Inject a transaction +//! let tx_hash = fixture.inject_transfer().await?; +//! +//! // Build a block +//! let block = fixture.build_block() +//! .expect_tx(tx_hash) +//! .await_block() +//! .await?; +//! +//! // Get the current block +//! let current_block = fixture.get_sequencer_block().await?; +//! +//! Ok(()) +//! } +//! ``` +//! +//! # Event Assertions +//! +//! The framework provides powerful event assertion capabilities: +//! +//! ```rust,ignore +//! // Wait for events on a single node +//! fixture.expect_event_on(1).chain_extended().await?; +//! +//! // Wait for the same event on multiple nodes +//! fixture.expect_event_on_followers().new_block_received().await?; +//! +//! // Wait for events on all nodes (including sequencer) +//! fixture.expect_event_on_all_nodes().chain_extended().await?; +//! +//! // Custom event predicates - just check if event matches +//! fixture.expect_event() +//! .where_event(|e| matches!(e, ChainOrchestratorEvent::BlockSequenced(_))) +//! .await?; +//! +//! // Extract values from events +//! let block_numbers = fixture.expect_event_on_nodes(vec![1, 2]) +//! .extract(|e| { +//! if let ChainOrchestratorEvent::NewL1Block(num) = e { +//! Some(*num) +//! } else { +//! None +//! } +//! }) +//! .await?; +//! ``` -use crate::{ConsensusArgs, RollupNodeGasPriceOracleArgs}; +// Module declarations +pub mod block_builder; +pub mod event_utils; +pub mod fixture; +pub mod l1_helpers; +pub mod network_helpers; +pub mod tx_helpers; -use super::{ - BlobProviderArgs, ChainOrchestratorArgs, EngineDriverArgs, L1ProviderArgs, - RollupNodeDatabaseArgs, RpcArgs, ScrollRollupNode, ScrollRollupNodeConfig, SequencerArgs, +// Re-export main types for convenience +pub use event_utils::{EventAssertions, EventWaiter}; +pub use fixture::{NodeHandle, TestFixture, TestFixtureBuilder}; +pub use network_helpers::{ + NetworkHelper, NetworkHelperProvider, ReputationChecker, ReputationChecks, +}; + +// Legacy utilities - keep existing functions for backward compatibility +use crate::{ + BlobProviderArgs, ChainOrchestratorArgs, ConsensusArgs, EngineDriverArgs, L1ProviderArgs, + RollupNodeDatabaseArgs, RollupNodeNetworkArgs, RpcArgs, ScrollRollupNode, + ScrollRollupNodeConfig, SequencerArgs, }; use alloy_primitives::Bytes; use reth_chainspec::EthChainSpec; @@ -14,8 +88,9 @@ use reth_e2e_test_utils::{ }; use reth_engine_local::LocalPayloadAttributesBuilder; use reth_node_builder::{ - rpc::RpcHandleProvider, EngineNodeLauncher, Node, NodeBuilder, NodeConfig, NodeHandle, - NodeTypes, NodeTypesWithDBAdapter, PayloadAttributesBuilder, PayloadTypes, TreeConfig, + rpc::RpcHandleProvider, EngineNodeLauncher, Node, NodeBuilder, NodeConfig, + NodeHandle as RethNodeHandle, NodeTypes, NodeTypesWithDBAdapter, PayloadAttributesBuilder, + PayloadTypes, TreeConfig, }; use reth_node_core::args::{DiscoveryArgs, NetworkArgs, RpcServerArgs, TxPoolArgs}; use reth_provider::providers::BlockchainProvider; @@ -27,6 +102,9 @@ use tokio::sync::Mutex; use tracing::{span, Level}; /// Creates the initial setup with `num_nodes` started and interconnected. +/// +/// This is the legacy setup function that's used by existing tests. +/// For new tests, consider using the `TestFixture` API instead. pub async fn setup_engine( mut scroll_node_config: ScrollRollupNodeConfig, num_nodes: usize, @@ -84,7 +162,7 @@ where let testing_node = NodeBuilder::new(node_config.clone()).testing_node(exec.clone()); let testing_config = testing_node.config().clone(); let node = ScrollRollupNode::new(scroll_node_config.clone(), testing_config).await; - let NodeHandle { node, node_exit_future: _ } = testing_node + let RethNodeHandle { node, node_exit_future: _ } = testing_node .with_types_and_provider::>() .with_components(node.components_builder()) .with_add_ons(node.add_ons()) @@ -143,7 +221,7 @@ pub async fn generate_tx(wallet: Arc>) -> Bytes { pub fn default_test_scroll_rollup_node_config() -> ScrollRollupNodeConfig { ScrollRollupNodeConfig { test: true, - network_args: crate::args::RollupNodeNetworkArgs::default(), + network_args: RollupNodeNetworkArgs::default(), database_args: RollupNodeDatabaseArgs::default(), l1_provider_args: L1ProviderArgs::default(), engine_driver_args: EngineDriverArgs { sync_at_startup: true }, @@ -158,7 +236,7 @@ pub fn default_test_scroll_rollup_node_config() -> ScrollRollupNodeConfig { }, blob_provider_args: BlobProviderArgs { mock: true, ..Default::default() }, signer_args: Default::default(), - gas_price_oracle_args: RollupNodeGasPriceOracleArgs::default(), + gas_price_oracle_args: crate::RollupNodeGasPriceOracleArgs::default(), consensus_args: ConsensusArgs::noop(), database: None, rpc_args: RpcArgs { enabled: true }, @@ -176,7 +254,7 @@ pub fn default_test_scroll_rollup_node_config() -> ScrollRollupNodeConfig { pub fn default_sequencer_test_scroll_rollup_node_config() -> ScrollRollupNodeConfig { ScrollRollupNodeConfig { test: true, - network_args: crate::args::RollupNodeNetworkArgs::default(), + network_args: RollupNodeNetworkArgs::default(), database_args: RollupNodeDatabaseArgs { rn_db_path: Some(PathBuf::from("sqlite::memory:")), }, @@ -198,7 +276,7 @@ pub fn default_sequencer_test_scroll_rollup_node_config() -> ScrollRollupNodeCon }, blob_provider_args: BlobProviderArgs { mock: true, ..Default::default() }, signer_args: Default::default(), - gas_price_oracle_args: RollupNodeGasPriceOracleArgs::default(), + gas_price_oracle_args: crate::RollupNodeGasPriceOracleArgs::default(), consensus_args: ConsensusArgs::noop(), database: None, rpc_args: RpcArgs { enabled: true }, diff --git a/crates/node/src/test_utils/network_helpers.rs b/crates/node/src/test_utils/network_helpers.rs new file mode 100644 index 00000000..5537b75b --- /dev/null +++ b/crates/node/src/test_utils/network_helpers.rs @@ -0,0 +1,285 @@ +//! Network-related test helpers for managing peers, reputation, and block propagation. +//! +//! This module provides utilities for testing network-level behaviors including: +//! - Checking and asserting on peer reputation +//! - Announcing blocks to the network +//! - Getting peer information +//! +//! # Examples +//! +//! ## Checking reputation +//! +//! ```rust,ignore +//! use rollup_node::test_utils::{TestFixture, ReputationChecks}; +//! +//! #[tokio::test] +//! async fn test_reputation() -> eyre::Result<()> { +//! let mut fixture = TestFixture::sequencer().with_nodes(2).build().await?; +//! +//! // Get node 0's peer ID +//! let node0_peer_id = fixture.network_on(0).peer_id().await?; +//! +//! // Check reputation from node 1's perspective +//! fixture.check_reputation_on(1) +//! .of_peer(node0_peer_id) +//! .equals(0) +//! .await?; +//! +//! // ... do something that should decrease reputation ... +//! +//! // Wait for reputation to drop below initial value +//! fixture.check_reputation_on(1) +//! .of_node(0).await? +//! .eventually_less_than(0) +//! .await?; +//! +//! Ok(()) +//! } +//! ``` +//! +//! ## Announcing blocks +//! +//! ```rust,ignore +//! use rollup_node::test_utils::{TestFixture, NetworkHelpers}; +//! use alloy_primitives::{Signature, U256}; +//! use reth_scroll_primitives::ScrollBlock; +//! +//! #[tokio::test] +//! async fn test_block_announce() -> eyre::Result<()> { +//! let mut fixture = TestFixture::sequencer().with_nodes(2).build().await?; +//! +//! // Create a block +//! let block = ScrollBlock::default(); +//! let signature = Signature::new(U256::from(1), U256::from(1), false); +//! +//! // Announce from node 0 +//! fixture.network_on(0).announce_block(block, signature).await?; +//! +//! Ok(()) +//! } +//! ``` + +use super::fixture::{ScrollNetworkHandle, TestFixture}; +use alloy_primitives::Signature; +use reth_network_api::{PeerId, PeerInfo, Peers}; +use reth_scroll_primitives::ScrollBlock; +use std::time::Duration; +use tokio::time; + +/// Helper for network-related test operations. +#[derive(Debug)] +pub struct NetworkHelper<'a> { + fixture: &'a TestFixture, + node_index: usize, +} + +impl<'a> NetworkHelper<'a> { + /// Create a new network helper for a specific node. + pub const fn new(fixture: &'a TestFixture, node_index: usize) -> Self { + Self { fixture, node_index } + } + + /// Get the network handle for this node. + pub async fn network_handle( + &self, + ) -> eyre::Result> { + self.fixture.nodes[self.node_index] + .rollup_manager_handle + .get_network_handle() + .await + .map_err(|e| eyre::eyre!("Failed to get network handle: {}", e)) + } + + /// Get this node's peer ID. + pub async fn peer_id(&self) -> eyre::Result { + let handle = self.network_handle().await?; + Ok(*handle.inner().peer_id()) + } + + /// Get the reputation of a peer from this node's perspective. + pub async fn reputation_of(&self, peer_id: PeerId) -> eyre::Result> { + let handle = self.network_handle().await?; + handle + .inner() + .reputation_by_id(peer_id) + .await + .map_err(|e| eyre::eyre!("Failed to get reputation: {}", e)) + } + + /// Get all connected peers. + pub async fn peers(&self) -> eyre::Result> { + let handle = self.network_handle().await?; + handle.inner().get_all_peers().await.map_err(|e| eyre::eyre!("Failed to get peers: {}", e)) + } + + /// Announce a block from this node to the network. + pub async fn announce_block( + &self, + block: ScrollBlock, + signature: Signature, + ) -> eyre::Result<()> { + let handle = self.network_handle().await?; + handle.announce_block(block, signature); + Ok(()) + } + + /// Get the number of connected peers. + pub async fn peer_count(&self) -> eyre::Result { + let peers = self.peers().await?; + Ok(peers.len()) + } +} + +/// Extension trait for `TestFixture` to add network helper capabilities. +pub trait NetworkHelperProvider { + /// Get a network helper for the sequencer node (node 0). + fn network(&self) -> NetworkHelper<'_>; + + /// Get a network helper for a specific node by index. + fn network_on(&self, node_index: usize) -> NetworkHelper<'_>; +} + +impl NetworkHelperProvider for TestFixture { + fn network(&self) -> NetworkHelper<'_> { + NetworkHelper::new(self, 0) + } + + fn network_on(&self, node_index: usize) -> NetworkHelper<'_> { + NetworkHelper::new(self, node_index) + } +} + +/// Builder for checking reputation with assertions. +#[derive(Debug)] +pub struct ReputationChecker<'a> { + fixture: &'a mut TestFixture, + observer_node: usize, + target_peer: Option, + timeout: Duration, + poll_interval: Duration, +} + +impl<'a> ReputationChecker<'a> { + /// Create a new reputation checker. + pub fn new(fixture: &'a mut TestFixture, observer_node: usize) -> Self { + Self { + fixture, + observer_node, + target_peer: None, + timeout: Duration::from_secs(5), + poll_interval: Duration::from_millis(100), + } + } + + /// Set the target peer by node index. + pub async fn of_node(mut self, node_index: usize) -> eyre::Result { + let peer_id = self.fixture.network_on(node_index).peer_id().await?; + self.target_peer = Some(peer_id); + Ok(self) + } + + /// Set a custom timeout for polling assertions (default: 5s). + pub const fn with_timeout(mut self, timeout: Duration) -> Self { + self.timeout = timeout; + self + } + + /// Set a custom poll interval for polling assertions (default: 100ms). + pub const fn with_poll_interval(mut self, interval: Duration) -> Self { + self.poll_interval = interval; + self + } + + /// Get the current reputation value. + pub async fn get(&self) -> eyre::Result> { + let peer_id = self.target_peer.ok_or_else(|| eyre::eyre!("No target peer set"))?; + self.fixture.network_on(self.observer_node).reputation_of(peer_id).await + } + + /// Assert that the reputation equals a specific value. + pub async fn equals(&mut self, expected: i32) -> eyre::Result<()> { + let reputation = self.get().await?; + let actual = reputation.ok_or_else(|| eyre::eyre!("Peer not found"))?; + if actual != expected { + return Err(eyre::eyre!("Expected reputation {}, got {}", expected, actual)); + } + Ok(()) + } + + /// Wait until the reputation becomes less than a threshold. + /// Polls repeatedly until the condition is met or timeout occurs. + pub async fn eventually_less_than(self, threshold: i32) -> eyre::Result<()> { + let peer_id = self.target_peer.ok_or_else(|| eyre::eyre!("No target peer set"))?; + let mut interval = time::interval(self.poll_interval); + let start = time::Instant::now(); + + loop { + let reputation = + self.fixture.network_on(self.observer_node).reputation_of(peer_id).await?; + if let Some(rep) = reputation { + if rep < threshold { + return Ok(()); + } + } + + if start.elapsed() > self.timeout { + let current = reputation.unwrap_or(0); + return Err(eyre::eyre!( + "Timeout waiting for reputation < {} (current: {})", + threshold, + current + )); + } + + interval.tick().await; + } + } + + /// Wait until the reputation becomes greater than a threshold. + /// Polls repeatedly until the condition is met or timeout occurs. + pub async fn eventually_greater_than(self, threshold: i32) -> eyre::Result<()> { + let peer_id = self.target_peer.ok_or_else(|| eyre::eyre!("No target peer set"))?; + let mut interval = time::interval(self.poll_interval); + let start = time::Instant::now(); + + loop { + let reputation = + self.fixture.network_on(self.observer_node).reputation_of(peer_id).await?; + if let Some(rep) = reputation { + if rep > threshold { + return Ok(()); + } + } + + if start.elapsed() > self.timeout { + let current = reputation.unwrap_or(0); + return Err(eyre::eyre!( + "Timeout waiting for reputation > {} (current: {})", + threshold, + current + )); + } + + interval.tick().await; + } + } +} + +/// Extension trait for checking reputation. +pub trait ReputationChecks { + /// Start checking reputation from the sequencer's perspective. + fn check_reputation(&mut self) -> ReputationChecker<'_>; + + /// Start checking reputation from a specific node's perspective. + fn check_reputation_on(&mut self, observer_node: usize) -> ReputationChecker<'_>; +} + +impl ReputationChecks for TestFixture { + fn check_reputation(&mut self) -> ReputationChecker<'_> { + ReputationChecker::new(self, 0) + } + + fn check_reputation_on(&mut self, observer_node: usize) -> ReputationChecker<'_> { + ReputationChecker::new(self, observer_node) + } +} diff --git a/crates/node/src/test_utils/tx_helpers.rs b/crates/node/src/test_utils/tx_helpers.rs new file mode 100644 index 00000000..ded7123b --- /dev/null +++ b/crates/node/src/test_utils/tx_helpers.rs @@ -0,0 +1,95 @@ +//! Transaction creation and injection helpers for test fixtures. + +use super::fixture::TestFixture; +use alloy_primitives::{Address, Bytes, B256, U256}; +use reth_e2e_test_utils::transaction::TransactionTestContext; + +/// Helper for creating and injecting transactions in tests. +#[derive(Debug)] +pub struct TxHelper<'a> { + fixture: &'a mut TestFixture, + target_node_index: usize, +} + +impl<'a> TxHelper<'a> { + /// Create a new transaction helper. + pub(crate) fn new(fixture: &'a mut TestFixture) -> Self { + Self { fixture, target_node_index: 0 } + } + + /// Target a specific node for transaction injection (default is sequencer). + pub const fn for_node(mut self, index: usize) -> Self { + self.target_node_index = index; + self + } + + /// Create a transfer transaction builder. + pub fn transfer(self) -> TransferTxBuilder<'a> { + TransferTxBuilder::new(self) + } +} + +/// Builder for creating transfer transactions. +#[derive(Debug)] +pub struct TransferTxBuilder<'a> { + tx_helper: TxHelper<'a>, + to: Option
, + value: U256, +} + +impl<'a> TransferTxBuilder<'a> { + /// Create a new transfer transaction builder. + fn new(tx_helper: TxHelper<'a>) -> Self { + Self { tx_helper, to: None, value: U256::from(1) } + } + + /// Set the recipient address. + pub const fn to(mut self, address: Address) -> Self { + self.to = Some(address); + self + } + + /// Set the value to transfer. + pub fn value(mut self, value: impl Into) -> Self { + self.value = value.into(); + self + } + + /// Build and inject the transaction, returning its hash. + pub async fn inject(self) -> eyre::Result { + let mut wallet = self.tx_helper.fixture.wallet.lock().await; + + // Generate the transaction + let raw_tx = TransactionTestContext::transfer_tx_nonce_bytes( + wallet.chain_id, + wallet.inner.clone(), + wallet.inner_nonce, + ) + .await; + + wallet.inner_nonce += 1; + drop(wallet); + + // Inject into the target node + let node = &self.tx_helper.fixture.nodes[self.tx_helper.target_node_index]; + let tx_hash = node.node.rpc.inject_tx(raw_tx).await?; + + Ok(tx_hash) + } + + /// Build the transaction bytes without injecting. + pub async fn build(self) -> eyre::Result { + let mut wallet = self.tx_helper.fixture.wallet.lock().await; + + let raw_tx = TransactionTestContext::transfer_tx_nonce_bytes( + wallet.chain_id, + wallet.inner.clone(), + wallet.inner_nonce, + ) + .await; + + wallet.inner_nonce += 1; + + Ok(raw_tx) + } +} diff --git a/crates/node/tests/e2e.rs b/crates/node/tests/e2e.rs index 48b7e10f..fc9dba89 100644 --- a/crates/node/tests/e2e.rs +++ b/crates/node/tests/e2e.rs @@ -1,18 +1,13 @@ //! End-to-end tests for the rollup node. -use alloy_eips::{eip2718::Encodable2718, BlockNumberOrTag}; -use alloy_primitives::{address, b256, hex::FromHex, Address, Bytes, Signature, B256, U256}; -use alloy_rpc_types_eth::Block; +use alloy_eips::BlockNumberOrTag; +use alloy_primitives::{address, b256, Address, Bytes, Signature, B256, U256}; use alloy_signer::Signer; use alloy_signer_local::PrivateKeySigner; -use eyre::Ok; use futures::{task::noop_waker_ref, FutureExt, StreamExt}; use reth_chainspec::EthChainSpec; -use reth_e2e_test_utils::{NodeHelperType, TmpDB}; -use reth_network::{NetworkConfigBuilder, NetworkEventListenerProvider, Peers, PeersInfo}; +use reth_network::{NetworkConfigBuilder, NetworkEventListenerProvider, PeersInfo}; use reth_network_api::block::EthWireProvider; -use reth_node_api::NodeTypesWithDBAdapter; -use reth_provider::providers::BlockchainProvider; use reth_rpc_api::EthApiServer; use reth_scroll_chainspec::{ScrollChainSpec, SCROLL_DEV, SCROLL_MAINNET, SCROLL_SEPOLIA}; use reth_scroll_node::ScrollNetworkPrimitives; @@ -24,21 +19,14 @@ use rollup_node::{ constants::SCROLL_GAS_LIMIT, test_utils::{ default_sequencer_test_scroll_rollup_node_config, default_test_scroll_rollup_node_config, - generate_tx, setup_engine, + generate_tx, setup_engine, EventAssertions, NetworkHelperProvider, ReputationChecks, + TestFixture, }, - BlobProviderArgs, ChainOrchestratorArgs, ConsensusAlgorithm, ConsensusArgs, EngineDriverArgs, - L1ProviderArgs, RollupNodeContext, RollupNodeDatabaseArgs, RollupNodeExtApiClient, - RollupNodeGasPriceOracleArgs, RollupNodeNetworkArgs as ScrollNetworkArgs, RpcArgs, - ScrollRollupNode, ScrollRollupNodeConfig, SequencerArgs, + RollupNodeContext, RollupNodeExtApiClient, }; use rollup_node_chain_orchestrator::ChainOrchestratorEvent; -use rollup_node_primitives::{ - sig_encode_hash, BatchCommitData, BatchInfo, BlockInfo, ConsensusUpdate, -}; -use rollup_node_sequencer::L1MessageInclusionMode; +use rollup_node_primitives::{sig_encode_hash, BatchCommitData, BlockInfo}; use rollup_node_watcher::L1Notification; -use scroll_alloy_consensus::TxL1Message; -use scroll_alloy_rpc_types::Transaction as ScrollAlloyTransaction; use scroll_db::{test_utils::setup_test_db, L1MessageKey}; use scroll_network::NewBlockWithPeer; use scroll_wire::{ScrollWireConfig, ScrollWireProtocolHandler}; @@ -57,265 +45,117 @@ use tracing::trace; async fn can_bridge_l1_messages() -> eyre::Result<()> { reth_tracing::init_test_tracing(); - // Create the chain spec for scroll mainnet with Feynman activated and a test genesis. - let chain_spec = (*SCROLL_DEV).clone(); - let node_args = ScrollRollupNodeConfig { - test: true, - network_args: ScrollNetworkArgs::default(), - database_args: RollupNodeDatabaseArgs { - rn_db_path: Some(PathBuf::from("sqlite::memory:")), - }, - l1_provider_args: L1ProviderArgs::default(), - engine_driver_args: EngineDriverArgs::default(), - chain_orchestrator_args: ChainOrchestratorArgs::default(), - sequencer_args: SequencerArgs { - sequencer_enabled: true, - auto_start: false, - block_time: 0, - l1_message_inclusion_mode: L1MessageInclusionMode::BlockDepth(0), - allow_empty_blocks: true, - ..SequencerArgs::default() - }, - blob_provider_args: BlobProviderArgs { mock: true, ..Default::default() }, - signer_args: Default::default(), - gas_price_oracle_args: RollupNodeGasPriceOracleArgs::default(), - consensus_args: ConsensusArgs::noop(), - database: None, - rpc_args: RpcArgs::default(), - }; - let (mut nodes, _tasks, _wallet) = setup_engine(node_args, 1, chain_spec, false, false).await?; - let node = nodes.pop().unwrap(); - - let chain_orchestrator = node.inner.add_ons_handle.rollup_manager_handle.clone(); - let mut events = chain_orchestrator.get_event_listener().await?; - let l1_watcher_tx = node.inner.add_ons_handle.l1_watcher_tx.clone().unwrap(); + // Create a sequencer test fixture + let mut fixture = TestFixture::builder() + .sequencer() + .with_l1_message_delay(0) + .allow_empty_blocks(true) + .build() + .await?; // Send a notification to set the L1 to synced - l1_watcher_tx.send(Arc::new(L1Notification::Synced)).await?; - - let block_info = BlockInfo { number: 0, hash: B256::random() }; - let l1_message = TxL1Message { - queue_index: 0, - gas_limit: 21000, - sender: Address::random(), - to: Address::random(), - value: U256::from(1), - input: Default::default(), - }; - l1_watcher_tx - .send(Arc::new(L1Notification::L1Message { - message: l1_message.clone(), - block_info, - block_timestamp: 1000, - })) + fixture.l1().sync().await?; + + // Create and send an L1 message + fixture + .l1() + .add_message() + .queue_index(0) + .gas_limit(21000) + .sender(Address::random()) + .to(Address::random()) + .value(1u32) + .at_block(0) + .send() .await?; - wait_n_events( - &mut events, - |e| { - if let ChainOrchestratorEvent::L1MessageCommitted(index) = e { - assert_eq!(index, 0); - true - } else { - false - } - }, - 1, - ) - .await; - - chain_orchestrator.build_block(); + // Wait for the L1 message to be committed + fixture.expect_event().l1_message_committed().await?; - wait_n_events( - &mut events, - |e| { - if let ChainOrchestratorEvent::BlockSequenced(block) = e { - assert_eq!(block.body.transactions.len(), 1); - assert_eq!( - block.body.transactions[0].as_l1_message().unwrap().inner(), - &l1_message - ); - true - } else { - false - } - }, - 1, - ) - .await; + // Build a block and expect it to contain the L1 message + fixture.build_block().expect_l1_message_count(1).build_and_await_block().await?; Ok(()) } #[tokio::test] -async fn can_sequence_and_gossip_blocks() { +async fn can_sequence_and_gossip_blocks() -> eyre::Result<()> { reth_tracing::init_test_tracing(); - // create 2 nodes - let chain_spec = (*SCROLL_DEV).clone(); - let rollup_manager_args = ScrollRollupNodeConfig { - test: true, - network_args: ScrollNetworkArgs { - enable_eth_scroll_wire_bridge: true, - enable_scroll_wire: true, - sequencer_url: None, - signer_address: None, - }, - database_args: RollupNodeDatabaseArgs { - rn_db_path: Some(PathBuf::from("sqlite::memory:")), - }, - l1_provider_args: L1ProviderArgs::default(), - engine_driver_args: EngineDriverArgs::default(), - chain_orchestrator_args: ChainOrchestratorArgs::default(), - sequencer_args: SequencerArgs { - sequencer_enabled: true, - auto_start: false, - block_time: 0, - l1_message_inclusion_mode: L1MessageInclusionMode::BlockDepth(0), - payload_building_duration: 1000, - allow_empty_blocks: true, - ..SequencerArgs::default() - }, - blob_provider_args: BlobProviderArgs { mock: true, ..Default::default() }, - signer_args: Default::default(), - gas_price_oracle_args: RollupNodeGasPriceOracleArgs::default(), - consensus_args: ConsensusArgs::noop(), - database: None, - rpc_args: RpcArgs::default(), - }; - - let (nodes, _tasks, wallet) = - setup_engine(rollup_manager_args, 2, chain_spec, false, false).await.unwrap(); - let wallet = Arc::new(Mutex::new(wallet)); + // create 2 nodes with the new TestFixture API + let mut fixture = TestFixture::builder() + .sequencer() + .followers(1) + .block_time(0) + .allow_empty_blocks(true) + .with_eth_scroll_bridge(true) + .with_scroll_wire(true) + .payload_building_duration(1000) + .build() + .await?; - // generate rollup node manager event streams for each node - let sequencer_rnm_handle = nodes[0].inner.add_ons_handle.rollup_manager_handle.clone(); - let mut sequencer_events = sequencer_rnm_handle.get_event_listener().await.unwrap(); - let sequencer_l1_watcher_tx = nodes[0].inner.add_ons_handle.l1_watcher_tx.clone().unwrap(); - let mut follower_events = - nodes[1].inner.add_ons_handle.rollup_manager_handle.get_event_listener().await.unwrap(); + // Send L1 synced notification to the sequencer + fixture.l1().for_node(0).sync().await?; - // Send a notification to set the L1 to synced - sequencer_l1_watcher_tx.send(Arc::new(L1Notification::Synced)).await.unwrap(); + // Inject a transaction into the sequencer node + let tx_hash = fixture.inject_transfer().await?; - // inject a transaction into the pool of the first node - let tx = generate_tx(wallet).await; - nodes[0].rpc.inject_tx(tx).await.unwrap(); - sequencer_rnm_handle.build_block(); + // Build a block and wait for it to be sequenced + fixture.build_block().expect_tx(tx_hash).expect_tx_count(1).build_and_await_block().await?; - // wait for the sequencer to build a block - wait_n_events( - &mut sequencer_events, - |e| { - if let ChainOrchestratorEvent::BlockSequenced(block) = e { - assert_eq!(block.body.transactions.len(), 1); - true - } else { - false - } - }, - 1, - ) - .await; + // Assert that the follower node receives the block from the network + let received_block = fixture.expect_event_on(1).new_block_received().await?; + assert_eq!(received_block.body.transactions.len(), 1); - // assert that the follower node has received the block from the peer - wait_n_events( - &mut follower_events, - |e| { - if let ChainOrchestratorEvent::NewBlockReceived(block_with_peer) = e { - assert_eq!(block_with_peer.block.body.transactions.len(), 1); - true - } else { - false - } - }, - 1, - ) - .await; + // Assert that a chain extension is triggered on the follower node + fixture.expect_event_on(1).chain_extended(1).await?; - // assert that a chain extension is triggered on the follower node - wait_n_events( - &mut follower_events, - |e| matches!(e, ChainOrchestratorEvent::ChainExtended(_)), - 1, - ) - .await; + Ok(()) } #[tokio::test] -async fn can_penalize_peer_for_invalid_block() { +async fn can_penalize_peer_for_invalid_block() -> eyre::Result<()> { reth_tracing::init_test_tracing(); - // create 2 nodes - let chain_spec = (*SCROLL_DEV).clone(); - let rollup_manager_args = ScrollRollupNodeConfig { - test: true, - network_args: ScrollNetworkArgs { - enable_eth_scroll_wire_bridge: true, - enable_scroll_wire: true, - sequencer_url: None, - signer_address: None, - }, - database_args: RollupNodeDatabaseArgs { - rn_db_path: Some(PathBuf::from("sqlite::memory:")), - }, - l1_provider_args: L1ProviderArgs::default(), - engine_driver_args: EngineDriverArgs::default(), - sequencer_args: SequencerArgs { - sequencer_enabled: true, - auto_start: false, - block_time: 0, - l1_message_inclusion_mode: L1MessageInclusionMode::BlockDepth(0), - payload_building_duration: 1000, - allow_empty_blocks: true, - ..SequencerArgs::default() - }, - blob_provider_args: BlobProviderArgs { mock: true, ..Default::default() }, - signer_args: Default::default(), - gas_price_oracle_args: RollupNodeGasPriceOracleArgs::default(), - consensus_args: ConsensusArgs::noop(), - chain_orchestrator_args: ChainOrchestratorArgs::default(), - database: None, - rpc_args: RpcArgs::default(), - }; - - let (nodes, _tasks, _) = - setup_engine(rollup_manager_args, 2, chain_spec, false, false).await.unwrap(); - - let node0_rmn_handle = nodes[0].inner.add_ons_handle.rollup_manager_handle.clone(); - let node0_network_handle = node0_rmn_handle.get_network_handle().await.unwrap(); - let node0_id = node0_network_handle.inner().peer_id(); - - let node1_rnm_handle = nodes[1].inner.add_ons_handle.rollup_manager_handle.clone(); - let node1_network_handle = node1_rnm_handle.get_network_handle().await.unwrap(); + // Create 2 nodes with the TestFixture API + let mut fixture = TestFixture::builder() + .sequencer() + .followers(1) + .block_time(0) + .allow_empty_blocks(true) + .with_eth_scroll_bridge(true) + .with_scroll_wire(true) + .payload_building_duration(1000) + .build() + .await?; - // get initial reputation of node0 from pov of node1 - let initial_reputation = - node1_network_handle.inner().reputation_by_id(*node0_id).await.unwrap().unwrap(); - assert_eq!(initial_reputation, 0); + // Check initial reputation of node 0 from node 1's perspective + fixture.check_reputation_on(1).of_node(0).await?.equals(0).await?; - // create invalid block + // Create invalid block let mut block = ScrollBlock::default(); block.header.number = 1; block.header.parent_hash = b256!("0x14844a4fc967096c628e90df3bb0c3e98941bdd31d1982c2f3e70ed17250d98b"); - // send invalid block from node0 to node1. We don't care about the signature here since we use a + // Send invalid block from node0 to node1. We don't care about the signature here since we use a // NoopConsensus in the test. - node0_network_handle.announce_block(block, Signature::new(U256::from(1), U256::from(1), false)); - - eventually( - Duration::from_secs(5), - Duration::from_millis(10), - "Peer0 reputation should be lower after sending invalid block", - || async { - // check that the node0 is penalized on node1 - let slashed_reputation = - node1_network_handle.inner().reputation_by_id(*node0_id).await.unwrap().unwrap(); - slashed_reputation < initial_reputation - }, - ) - .await; + fixture + .network_on(0) + .announce_block(block, Signature::new(U256::from(1), U256::from(1), false)) + .await?; + + // Wait for reputation to decrease + fixture + .check_reputation_on(1) + .of_node(0) + .await? + .with_timeout(Duration::from_secs(5)) + .with_poll_interval(Duration::from_millis(10)) + .eventually_less_than(0) + .await?; + + Ok(()) } /// Tests that peers are penalized for broadcasting blocks with invalid signatures. @@ -343,74 +183,39 @@ async fn can_penalize_peer_for_invalid_signature() -> eyre::Result<()> { let unauthorized_signer = PrivateKeySigner::random().with_chain_id(Some(chain_spec.chain().id())); - let mut test_config = default_sequencer_test_scroll_rollup_node_config(); - test_config.consensus_args.algorithm = ConsensusAlgorithm::SystemContract; - test_config.consensus_args.authorized_signer = Some(authorized_address); - test_config.signer_args.private_key = Some(authorized_signer.clone()); - - // Setup nodes - let (mut nodes, _tasks, _) = - setup_engine(test_config, 2, chain_spec.clone(), false, false).await.unwrap(); - - let node0 = nodes.remove(0); - let node1 = nodes.remove(0); - - // Get handles - let node0_rmn_handle = node0.inner.add_ons_handle.rollup_manager_handle.clone(); - let node0_network_handle = node0_rmn_handle.get_network_handle().await.unwrap(); - let node0_l1_watcher_tx = node0.inner.add_ons_handle.l1_watcher_tx.clone().unwrap(); - let node0_id = node0_network_handle.inner().peer_id(); - - let node1_rnm_handle = node1.inner.add_ons_handle.rollup_manager_handle.clone(); - let node1_network_handle = node1_rnm_handle.get_network_handle().await.unwrap(); - - // Get event streams - let mut node0_events = node0_rmn_handle.get_event_listener().await.unwrap(); - let mut node1_events = node1_rnm_handle.get_event_listener().await.unwrap(); + // Build fixture with SystemContract consensus + let mut fixture = TestFixture::builder() + .sequencer() + .followers(1) + .with_chain_spec(chain_spec) + .block_time(0) + .allow_empty_blocks(true) + .with_consensus_system_contract(authorized_address) + .with_signer(authorized_signer.clone()) + .payload_building_duration(1000) + .build() + .await?; // Set the L1 to synced on the sequencer node - node0_l1_watcher_tx.send(Arc::new(L1Notification::Synced)).await.unwrap(); - node0_events.next().await; - node0_events.next().await; + fixture.l1().for_node(0).sync().await?; + fixture.expect_event_on(0).l1_synced().await?; // === Phase 1: Test valid block with correct signature === // Have the legitimate sequencer build and sign a block - node0_rmn_handle.build_block(); - - // Wait for the sequencer to build the block - let block0 = - if let Some(ChainOrchestratorEvent::BlockSequenced(block)) = node0_events.next().await { - assert_eq!(block.body.transactions.len(), 0, "Block should have no transactions"); - block - } else { - panic!("Failed to receive block from sequencer"); - }; + let block0 = fixture.build_block().expect_tx_count(0).build_and_await_block().await?; - // Node1 should receive and accept the valid block - if let Some(ChainOrchestratorEvent::NewBlockReceived(block_with_peer)) = - node1_events.next().await - { - assert_eq!(block0.hash_slow(), block_with_peer.block.hash_slow()); - - // Verify the signature is from the authorized signer - let hash = sig_encode_hash(&block_with_peer.block); - let recovered = block_with_peer.signature.recover_address_from_prehash(&hash).unwrap(); - assert_eq!(recovered, authorized_address, "Block should be signed by authorized signer"); - } else { - panic!("Failed to receive valid block at follower"); - } + // Wait for node1 to receive and validate the block with correct signature + let received_block = fixture.expect_event_on(1).new_block_received().await?; + assert_eq!(block0.hash_slow(), received_block.hash_slow()); // Wait for successful import - wait_n_events(&mut node1_events, |e| matches!(e, ChainOrchestratorEvent::ChainExtended(_)), 1) - .await; + fixture.expect_event_on(1).chain_extended(block0.header.number).await?; // === Phase 2: Create and send valid block with unauthorized signer signature === - // Get initial reputation of node0 from node1's perspective - let initial_reputation = - node1_network_handle.inner().reputation_by_id(*node0_id).await.unwrap().unwrap(); - assert_eq!(initial_reputation, 0, "Initial reputation should be zero"); + // Check initial reputation + fixture.check_reputation_on(1).of_node(0).await?.equals(0).await?; // Create a new block manually (we'll reuse the valid block structure but with wrong signature) let mut block1 = block0.clone(); @@ -420,73 +225,69 @@ async fn can_penalize_peer_for_invalid_signature() -> eyre::Result<()> { // Sign the block with the unauthorized signer let block_hash = sig_encode_hash(&block1); - let unauthorized_signature = unauthorized_signer.sign_hash(&block_hash).await.unwrap(); + let unauthorized_signature = unauthorized_signer.sign_hash(&block_hash).await?; // Send the block with invalid signature from node0 to node1 - node0_network_handle.announce_block(block1.clone(), unauthorized_signature); + fixture.network_on(0).announce_block(block1.clone(), unauthorized_signature).await?; // Node1 should receive and process the invalid block - wait_for_event_predicate_5s(&mut node1_events, |e| { - if let ChainOrchestratorEvent::NewBlockReceived(block_with_peer) = e { - assert_eq!(block1.hash_slow(), block_with_peer.block.hash_slow()); - - // Verify the signature is from the unauthorized signer - let hash = sig_encode_hash(&block_with_peer.block); - let recovered = block_with_peer.signature.recover_address_from_prehash(&hash).unwrap(); - return recovered == unauthorized_signer.address(); - } - false - }) - .await?; - - eventually( - Duration::from_secs(5), - Duration::from_millis(100), - "Node0 reputation should be lower after sending block with invalid signature", - || async { - let current_reputation = - node1_network_handle.inner().reputation_by_id(*node0_id).await.unwrap().unwrap(); - current_reputation < initial_reputation - }, - ) - .await; + fixture + .expect_event_on(1) + .timeout(Duration::from_secs(5)) + .extract(|e| { + if let ChainOrchestratorEvent::NewBlockReceived(block_with_peer) = e { + if block1.hash_slow() == block_with_peer.block.hash_slow() { + // Verify the signature is from the unauthorized signer + let hash = sig_encode_hash(&block_with_peer.block); + if let Result::Ok(recovered) = + block_with_peer.signature.recover_address_from_prehash(&hash) + { + return Some(recovered == unauthorized_signer.address()); + } + } + } + None + }) + .await?; + + // Wait for reputation to decrease + fixture + .check_reputation_on(1) + .of_node(0) + .await? + .with_timeout(Duration::from_secs(5)) + .with_poll_interval(Duration::from_millis(100)) + .eventually_less_than(0) + .await?; // === Phase 3: Send valid block with invalid signature === - // Get current reputation of node0 from node1's perspective - let current_reputation = - node1_network_handle.inner().reputation_by_id(*node0_id).await.unwrap().unwrap(); + + // Get current reputation before sending malformed signature + let current_reputation = fixture.check_reputation_on(1).of_node(0).await?.get().await?.unwrap(); let invalid_signature = Signature::new(U256::from(1), U256::from(1), false); // Create a new block with the same structure as before but with an invalid signature. // We need to make sure the block is different so that it is not filtered. block1.header.timestamp += 1; - node0_network_handle.announce_block(block1.clone(), invalid_signature); - - eventually( - Duration::from_secs(5), - Duration::from_millis(100), - "Node0 reputation should be lower after sending block with invalid signature", - || async { - let all_peers = node1_network_handle.inner().get_all_peers().await.unwrap(); - if all_peers.is_empty() { - return true; // No peers to check, assume penalization and peer0 is blocked and - // disconnected - } + fixture.network_on(0).announce_block(block1.clone(), invalid_signature).await?; - let penalized_reputation = - node1_network_handle.inner().reputation_by_id(*node0_id).await.unwrap().unwrap(); - penalized_reputation < current_reputation - }, - ) - .await; + // Wait for the node's 0 reputation to eventually fall. + fixture + .check_reputation_on(1) + .of_node(0) + .await? + .with_timeout(Duration::from_secs(5)) + .with_poll_interval(Duration::from_millis(100)) + .eventually_less_than(current_reputation) + .await?; Ok(()) } #[allow(clippy::large_stack_frames)] #[tokio::test] -async fn can_forward_tx_to_sequencer() { +async fn can_forward_tx_to_sequencer() -> eyre::Result<()> { reth_tracing::init_test_tracing(); // create 2 nodes @@ -592,118 +393,52 @@ async fn can_forward_tx_to_sequencer() { 1, ) .await; + + Ok(()) } #[allow(clippy::large_stack_frames)] #[tokio::test] -async fn can_sequence_and_gossip_transactions() { +async fn can_sequence_and_gossip_transactions() -> eyre::Result<()> { reth_tracing::init_test_tracing(); - // create 2 nodes - let mut sequencer_node_config = default_sequencer_test_scroll_rollup_node_config(); - sequencer_node_config.sequencer_args.block_time = 0; - let follower_node_config = default_test_scroll_rollup_node_config(); - - // Create the chain spec for scroll mainnet with Euclid v2 activated and a test genesis. - let chain_spec = (*SCROLL_DEV).clone(); - let (mut sequencer_node, _tasks, _) = - setup_engine(sequencer_node_config, 1, chain_spec.clone(), false, false).await.unwrap(); - - let (mut follower_node, _tasks, wallet) = - setup_engine(follower_node_config, 1, chain_spec, false, false).await.unwrap(); - - let wallet = Arc::new(Mutex::new(wallet)); - - // Connect the nodes together. - sequencer_node[0].network.add_peer(follower_node[0].network.record()).await; - follower_node[0].network.next_session_established().await; - sequencer_node[0].network.next_session_established().await; - - // generate rollup node manager event streams for each node - let sequencer_rnm_handle = sequencer_node[0].inner.add_ons_handle.rollup_manager_handle.clone(); - let mut sequencer_events = sequencer_rnm_handle.get_event_listener().await.unwrap(); - let sequencer_l1_watcher_tx = - sequencer_node[0].inner.add_ons_handle.l1_watcher_tx.clone().unwrap(); - let mut follower_events = follower_node[0] - .inner - .add_ons_handle - .rollup_manager_handle - .get_event_listener() - .await - .unwrap(); - - // Send a notification to set the L1 to synced - sequencer_l1_watcher_tx.send(Arc::new(L1Notification::Synced)).await.unwrap(); - sequencer_events.next().await; - sequencer_events.next().await; + // Create 2 nodes with the TestFixture API + let mut fixture = TestFixture::builder() + .sequencer() + .followers(1) + .block_time(0) + .allow_empty_blocks(true) + .build() + .await?; - // have the sequencer build an empty block and gossip it to follower - sequencer_rnm_handle.build_block(); + // Send L1 synced notification to sequencer + fixture.l1().for_node(0).sync().await?; + fixture.expect_event_on(0).l1_synced().await?; - // wait for the sequencer to build a block with no transactions - if let Some(ChainOrchestratorEvent::BlockSequenced(block)) = sequencer_events.next().await { - assert_eq!(block.body.transactions.len(), 0); - } else { - panic!("Failed to receive block from rollup node"); - } + // Have the sequencer build an empty block + fixture.build_block().expect_tx_count(0).build_and_await_block().await?; - // assert that the follower node has received the block from the peer - wait_n_events( - &mut follower_events, - |e| matches!(e, ChainOrchestratorEvent::ChainExtended(_)), - 1, - ) - .await; + // Assert that the follower node has received the block + fixture.expect_event_on(1).chain_extended(1).await?; - // inject a transaction into the pool of the follower node - let tx = generate_tx(wallet).await; - follower_node[0].rpc.inject_tx(tx).await.unwrap(); + // Inject a transaction into the follower node's pool + let tx = generate_tx(fixture.wallet.clone()).await; + fixture.inject_tx_on(1, tx).await?; - tokio::time::sleep(tokio::time::Duration::from_secs(10)).await; + // Wait for transaction propagation + tokio::time::sleep(Duration::from_secs(10)).await; - // build block - sequencer_rnm_handle.build_block(); + // Build block on sequencer - should include the transaction gossiped from follower + fixture.build_block().expect_tx_count(1).expect_block_number(2).build_and_await_block().await?; - // wait for the sequencer to build a block with transactions - wait_n_events( - &mut sequencer_events, - |e| { - if let ChainOrchestratorEvent::BlockSequenced(block) = e { - assert_eq!(block.header.number, 2); - assert_eq!(block.body.transactions.len(), 1); - return true - } - false - }, - 1, - ) - .await; + // Assert that the follower node has received the block with the transaction + let received_block = fixture.expect_event_on(1).new_block_received().await?; + assert_eq!(received_block.body.transactions.len(), 1); - // assert that the follower node has received the block from the peer - if let Some(ChainOrchestratorEvent::NewBlockReceived(block_with_peer)) = - follower_events.next().await - { - assert_eq!(block_with_peer.block.body.transactions.len(), 1); - } else { - panic!("Failed to receive block from rollup node"); - } + // Assert that the block was successfully imported by the follower node + fixture.expect_event_on(1).chain_extended(2).await?; - // assert that the block was successfully imported by the follower node - wait_n_events( - &mut follower_events, - |e| { - if let ChainOrchestratorEvent::ChainExtended(chain) = e { - assert_eq!(chain.chain.len(), 1); - let block = chain.chain.first().unwrap(); - assert_eq!(block.body.transactions.len(), 1); - true - } else { - false - } - }, - 1, - ) - .await; + Ok(()) } /// We test the bridge from the eth-wire protocol to the scroll-wire protocol. @@ -717,7 +452,7 @@ async fn can_sequence_and_gossip_transactions() { /// The test will send messages from Node 3 to Node 1, which will bridge the messages to Node /// Node 2 will then receive the messages and verify that they are correct. #[tokio::test] -async fn can_bridge_blocks() { +async fn can_bridge_blocks() -> eyre::Result<()> { reth_tracing::init_test_tracing(); // Create the chain spec for scroll dev with Feynman activated and a test genesis. @@ -726,8 +461,7 @@ async fn can_bridge_blocks() { // Setup the bridge node and a standard node. let (mut nodes, tasks, _) = setup_engine(default_test_scroll_rollup_node_config(), 1, chain_spec.clone(), false, false) - .await - .unwrap(); + .await?; let mut bridge_node = nodes.pop().unwrap(); let bridge_peer_id = bridge_node.network.record().id; let bridge_node_l1_watcher_tx = bridge_node.inner.add_ons_handle.l1_watcher_tx.clone().unwrap(); @@ -806,13 +540,14 @@ async fn can_bridge_blocks() { assert_eq!(peer_id, bridge_peer_id); assert_eq!(block.hash_slow(), block_1_hash); assert_eq!( - TryInto::::try_into(extra_data.as_ref().windows(65).last().unwrap()) - .unwrap(), + TryInto::::try_into(extra_data.as_ref().windows(65).last().unwrap())?, signature ) } else { panic!("Failed to receive block from scroll-wire network"); } + + Ok(()) } /// Test that when the rollup node manager is shutdown, it consolidates the most recent batch @@ -1210,115 +945,92 @@ async fn graceful_shutdown_sets_fcs_to_latest_signed_block_in_db_on_start_up() - #[tokio::test] async fn consolidates_committed_batches_after_chain_consolidation() -> eyre::Result<()> { reth_tracing::init_test_tracing(); - let chain_spec = (*SCROLL_MAINNET).clone(); - - // Launch a node - let (mut nodes, _tasks, _) = - setup_engine(default_test_scroll_rollup_node_config(), 1, chain_spec.clone(), false, false) - .await?; - let node = nodes.pop().unwrap(); - let handle = node.inner.add_ons_handle.rollup_manager_handle.clone(); - let l1_watcher_tx = node.inner.add_ons_handle.l1_watcher_tx.clone().unwrap(); - // Request an event stream from the rollup node manager and manually poll rnm to process the - // event stream request from the handle. - let mut rnm_events = handle.get_event_listener().await?; + // Create a follower test fixture using SCROLL_MAINNET chain spec + let mut fixture = TestFixture::builder() + .followers(1) + .with_chain_spec(SCROLL_MAINNET.clone()) + .with_memory_db() + .build() + .await?; // Load test batches let batch_0_block_info = BlockInfo { number: 18318207, hash: B256::random() }; let raw_calldata_0 = read_to_bytes("./tests/testdata/batch_0_calldata.bin")?; - let batch_0_data = BatchCommitData { - hash: b256!("5AAEB6101A47FC16866E80D77FFE090B6A7B3CF7D988BE981646AB6AEDFA2C42"), - index: 1, - block_number: 18318207, - block_timestamp: 1696935971, - calldata: Arc::new(raw_calldata_0), - blob_versioned_hash: None, - finalized_block_number: None, - reverted_block_number: None, - }; - let batch_0_info = BatchInfo { index: batch_0_data.index, hash: batch_0_data.hash }; + let batch_0_hash = b256!("5AAEB6101A47FC16866E80D77FFE090B6A7B3CF7D988BE981646AB6AEDFA2C42"); + let batch_0_finalization_block_info = BlockInfo { number: 18318210, hash: B256::random() }; let batch_1_block_info = BlockInfo { number: 18318215, hash: B256::random() }; let raw_calldata_1 = read_to_bytes("./tests/testdata/batch_1_calldata.bin")?; - let batch_1_data = BatchCommitData { - hash: b256!("AA8181F04F8E305328A6117FA6BC13FA2093A3C4C990C5281DF95A1CB85CA18F"), - index: 2, - block_number: 18318215, - block_timestamp: 1696936000, - calldata: Arc::new(raw_calldata_1), - blob_versioned_hash: None, - finalized_block_number: None, - reverted_block_number: None, - }; - let batch_1_info = BatchInfo { index: batch_1_data.index, hash: batch_1_data.hash }; + let batch_1_hash = b256!("AA8181F04F8E305328A6117FA6BC13FA2093A3C4C990C5281DF95A1CB85CA18F"); + let batch_1_finalization_block_info = BlockInfo { number: 18318220, hash: B256::random() }; - // Send the first batch. - l1_watcher_tx - .send(Arc::new(L1Notification::BatchCommit { - block_info: batch_0_block_info, - data: batch_0_data, - })) + // Send the first batch + let (_, batch_0_index) = fixture + .l1() + .commit_batch() + .at_block(batch_0_block_info) + .hash(batch_0_hash) + .index(1) + .calldata(raw_calldata_0) + .send() .await?; - // Send a batch finalization for the first batch. - l1_watcher_tx - .send(Arc::new(L1Notification::BatchFinalization { - hash: batch_0_info.hash, - index: batch_0_info.index, - block_info: batch_0_finalization_block_info, - })) + // Send a batch finalization for the first batch + fixture + .l1() + .finalize_batch() + .hash(batch_0_hash) + .index(batch_0_index) + .at_block(batch_0_finalization_block_info) + .send() .await?; - // Send the L1 block finalized notification. - l1_watcher_tx - .send(Arc::new(L1Notification::Finalized(batch_0_finalization_block_info.number))) - .await?; - - wait_for_event_predicate_5s(&mut rnm_events, |event| { - matches!(event, ChainOrchestratorEvent::BatchConsolidated(_)) - }) - .await?; - // Send the second batch. - l1_watcher_tx - .send(Arc::new(L1Notification::BatchCommit { - block_info: batch_1_block_info, - data: batch_1_data, - })) + // Send the L1 block finalized notification + fixture.l1().finalize_l1_block(batch_0_finalization_block_info.number).await?; + + // Wait for batch consolidated event + fixture.expect_event().batch_consolidated().await?; + + // Send the second batch + let (_, batch_1_index) = fixture + .l1() + .commit_batch() + .at_block(batch_1_block_info) + .hash(batch_1_hash) + .index(2) + .calldata(raw_calldata_1) + .send() .await?; - // send the Synced notification to the chain orchestrator - l1_watcher_tx.send(Arc::new(L1Notification::Synced)).await?; + // Send the Synced notification to the chain orchestrator + fixture.l1().sync().await?; - wait_for_event_predicate_5s(&mut rnm_events, |event| { - matches!(event, ChainOrchestratorEvent::BatchConsolidated(_)) - }) - .await?; + // Wait for the second batch to be consolidated + fixture.expect_event().batch_consolidated().await?; - let status = handle.status().await?; + let status = fixture.get_sequencer_status().await?; assert_eq!(status.l2.fcs.safe_block_info().number, 57); assert_eq!(status.l2.fcs.finalized_block_info().number, 4); - // Now send the batch finalization event for the second batch and finalize the L1 block. - l1_watcher_tx - .send(Arc::new(L1Notification::BatchFinalization { - hash: batch_1_info.hash, - index: batch_1_info.index, - block_info: batch_1_finalization_block_info, - })) - .await?; - l1_watcher_tx - .send(Arc::new(L1Notification::Finalized(batch_1_finalization_block_info.number))) + // Now send the batch finalization event for the second batch and finalize the L1 block + fixture + .l1() + .finalize_batch() + .hash(batch_1_hash) + .index(batch_1_index) + .at_block(batch_1_finalization_block_info) + .send() .await?; - wait_for_event_predicate_5s(&mut rnm_events, |event| { - matches!(event, ChainOrchestratorEvent::L1BlockFinalized(_, _)) - }) - .await?; + fixture.l1().finalize_l1_block(batch_1_finalization_block_info.number).await?; - let status = handle.status().await?; + // Wait for L1 block finalized event + fixture.expect_event().l1_block_finalized().await?; + + let status = fixture.get_sequencer_status().await?; assert_eq!(status.l2.fcs.safe_block_info().number, 57); assert_eq!(status.l2.fcs.finalized_block_info().number, 57); @@ -1329,146 +1041,93 @@ async fn consolidates_committed_batches_after_chain_consolidation() -> eyre::Res #[tokio::test] async fn can_handle_batch_revert_with_reorg() -> eyre::Result<()> { reth_tracing::init_test_tracing(); - let chain_spec = (*SCROLL_MAINNET).clone(); - // Launch a node - let (mut nodes, _tasks, _) = - setup_engine(default_test_scroll_rollup_node_config(), 1, chain_spec.clone(), false, false) - .await?; - let node = nodes.pop().unwrap(); - let handle = node.inner.add_ons_handle.rollup_manager_handle.clone(); - let l1_watcher_tx = node.inner.add_ons_handle.l1_watcher_tx.clone().unwrap(); - - // Request an event stream from the rollup node manager and manually poll rnm to process the - // event stream request from the handle. - let mut rnm_events = handle.get_event_listener().await?; + // Create a follower test fixture using SCROLL_MAINNET chain spec + let mut fixture = TestFixture::builder() + .followers(1) + .with_chain_spec(SCROLL_MAINNET.clone()) + .with_memory_db() + .build() + .await?; - // send a Synced notification to the chain orchestrator - l1_watcher_tx.send(Arc::new(L1Notification::Synced)).await.unwrap(); + // Send a Synced notification to the chain orchestrator + fixture.l1().sync().await?; // Load test batches let batch_0_block_info = BlockInfo { number: 18318207, hash: B256::random() }; let raw_calldata_0 = read_to_bytes("./tests/testdata/batch_0_calldata.bin")?; - let batch_0_data = BatchCommitData { - hash: b256!("5AAEB6101A47FC16866E80D77FFE090B6A7B3CF7D988BE981646AB6AEDFA2C42"), - index: 1, - block_number: 18318207, - block_timestamp: 1696935971, - calldata: Arc::new(raw_calldata_0), - blob_versioned_hash: None, - finalized_block_number: None, - reverted_block_number: None, - }; - let batch_0_info = BatchInfo { index: batch_0_data.index, hash: batch_0_data.hash }; + let batch_0_hash = b256!("5AAEB6101A47FC16866E80D77FFE090B6A7B3CF7D988BE981646AB6AEDFA2C42"); + let batch_1_block_info = BlockInfo { number: 18318215, hash: B256::random() }; let raw_calldata_1 = read_to_bytes("./tests/testdata/batch_1_calldata.bin")?; - let batch_1_data = BatchCommitData { - hash: b256!("AA8181F04F8E305328A6117FA6BC13FA2093A3C4C990C5281DF95A1CB85CA18F"), - index: 2, - block_number: 18318215, - block_timestamp: 1696936000, - calldata: Arc::new(raw_calldata_1), - blob_versioned_hash: None, - finalized_block_number: None, - reverted_block_number: None, - }; + let batch_1_hash = b256!("AA8181F04F8E305328A6117FA6BC13FA2093A3C4C990C5281DF95A1CB85CA18F"); + let batch_1_revert_block_info = BlockInfo { number: 18318216, hash: B256::random() }; - let batch_1_revert = L1Notification::BatchRevert { - batch_info: BatchInfo { index: batch_1_data.index, hash: batch_1_data.hash }, - block_info: batch_1_revert_block_info, - }; - // Send the first batch. - l1_watcher_tx - .send(Arc::new(L1Notification::BatchCommit { - block_info: batch_0_block_info, - data: batch_0_data, - })) + // Send the first batch + fixture + .l1() + .commit_batch() + .at_block(batch_0_block_info) + .hash(batch_0_hash) + .index(1) + .block_number(18318207) + .block_timestamp(1696935971) + .calldata(raw_calldata_0) + .send() .await?; - // Read the first 4 blocks. - loop { - if let Some(ChainOrchestratorEvent::BlockConsolidated(consolidation_outcome)) = - rnm_events.next().await - { - if consolidation_outcome.block_info().block_info.number == 4 { - break - } - } - } - - // Send the second batch. - l1_watcher_tx - .send(Arc::new(L1Notification::BatchCommit { - block_info: batch_1_block_info, - data: batch_1_data, - })) + // Wait for block 4 to be consolidated + fixture.expect_event().block_consolidated(4).await?; + + // Send the second batch + let (_, batch_1_index) = fixture + .l1() + .commit_batch() + .at_block(batch_1_block_info) + .hash(batch_1_hash) + .index(2) + .block_number(18318215) + .block_timestamp(1696936000) + .calldata(raw_calldata_1) + .send() .await?; - // Read the next 42 blocks. - loop { - if let Some(ChainOrchestratorEvent::BlockConsolidated(consolidation_outcome)) = - rnm_events.next().await - { - if consolidation_outcome.block_info().block_info.number == 46 { - break - } - } - } - - let status = handle.status().await?; + // Wait for block 57 to be consolidated (after processing batch 1) + fixture.expect_event().block_consolidated(57).await?; - // Assert the forkchoice state is above 4 + let status = fixture.get_sequencer_status().await?; assert!(status.l2.fcs.head_block_info().number > 4); assert!(status.l2.fcs.safe_block_info().number > 4); - // Send the revert for the second batch. - l1_watcher_tx.send(Arc::new(batch_1_revert)).await?; - wait_for_event( - &mut rnm_events, - ChainOrchestratorEvent::BatchReverted { - batch_info: batch_0_info, - safe_head: BlockInfo { - number: 4, - hash: B256::from_hex( - "30af93536b9f2899c2f5e77be24a4447a8e49c5683c74c4aab8c880c1508fdc5", - ) - .unwrap(), - }, - }, - Duration::from_secs(5), - ) - .await?; + // Send the revert for the second batch + fixture + .l1() + .revert_batch() + .hash(batch_1_hash) + .index(batch_1_index) + .at_block(batch_1_revert_block_info) + .send() + .await?; - let status = handle.status().await?; + // Wait for batch reverted event + fixture.expect_event().batch_reverted().await?; + + let status = fixture.get_sequencer_status().await?; - // Assert the forkchoice state was reset to 4. + // Assert the forkchoice state was reset to 4 assert_eq!(status.l2.fcs.head_block_info().number, 57); assert_eq!(status.l2.fcs.safe_block_info().number, 4); - // Now lets reorg the L1 such that the batch revert should be reorged out. - l1_watcher_tx.send(Arc::new(L1Notification::Reorg(18318215))).await?; - wait_for_event( - &mut rnm_events, - ChainOrchestratorEvent::L1Reorg { - l1_block_number: 18318215, - queue_index: None, - l2_head_block_info: None, - l2_safe_block_info: Some(BlockInfo { - number: 57, - hash: B256::from_hex( - "88ab32bd52bdbab5dd148bad0de208c634d357570055a62bacc46e7a78b371dd", - ) - .unwrap(), - }), - }, - Duration::from_secs(5), - ) - .await?; + // Now let's reorg the L1 such that the batch revert should be reorged out + fixture.l1().reorg_to(18318215).await?; - let status = handle.status().await?; + // Wait for L1 reorg event + fixture.expect_event().l1_reorg().await?; + + let status = fixture.get_sequencer_status().await?; - // Assert the forkchoice state safe block was reset to 57. + // Assert the forkchoice state safe block was reset to 57 assert_eq!(status.l2.fcs.safe_block_info().number, 57); Ok(()) @@ -1479,145 +1138,105 @@ async fn can_handle_batch_revert_with_reorg() -> eyre::Result<()> { async fn can_handle_l1_message_reorg() -> eyre::Result<()> { reth_tracing::init_test_tracing(); color_eyre::install()?; - let chain_spec = (*SCROLL_DEV).clone(); // Launch 2 nodes: node0=sequencer and node1=follower. - let config = default_sequencer_test_scroll_rollup_node_config(); - let (mut nodes, _tasks, _) = setup_engine(config, 2, chain_spec.clone(), false, false).await?; - let node0 = nodes.remove(0); - let node1 = nodes.remove(0); - - // Get handles - let node0_rnm_handle = node0.inner.add_ons_handle.rollup_manager_handle.clone(); - let mut node0_rnm_events = node0_rnm_handle.get_event_listener().await?; - let node0_l1_watcher_tx = node0.inner.add_ons_handle.l1_watcher_tx.as_ref().unwrap(); - - let node1_rnm_handle = node1.inner.add_ons_handle.rollup_manager_handle.clone(); - let mut node1_rnm_events = node1_rnm_handle.get_event_listener().await?; - let node1_l1_watcher_tx = node1.inner.add_ons_handle.l1_watcher_tx.as_ref().unwrap(); + let mut fixture = TestFixture::builder().sequencer().followers(1).block_time(0).build().await?; // Set L1 synced on both the sequencer and follower nodes. - node0_l1_watcher_tx.send(Arc::new(L1Notification::Synced)).await?; - node1_l1_watcher_tx.send(Arc::new(L1Notification::Synced)).await?; + fixture.l1().sync().await?; + fixture.expect_event_on_all_nodes().l1_synced().await?; // Let the sequencer build 10 blocks before performing the reorg process. let mut reorg_block = None; for i in 1..=10 { - node0_rnm_handle.build_block(); - let b = wait_for_block_sequenced_5s(&mut node0_rnm_events, i).await?; + let b = fixture.build_block().expect_block_number(i).build_and_await_block().await?; tracing::info!(target: "scroll::test", block_number = ?b.header.number, block_hash = ?b.header.hash_slow(), "Sequenced block"); reorg_block = Some(b); } // Assert that the follower node has received all 10 blocks from the sequencer node. - wait_for_block_imported_5s(&mut node1_rnm_events, 10).await?; - - // Send a L1 message and wait for it to be indexed. - let block_10_block_info = BlockInfo { number: 10, hash: B256::random() }; - let l1_message_notification = L1Notification::L1Message { - message: TxL1Message { - queue_index: 0, - gas_limit: 21000, - to: Default::default(), - value: Default::default(), - sender: address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"), - input: Default::default(), - }, - block_info: block_10_block_info, - block_timestamp: 0, - }; + fixture + .expect_event_on(1) + .where_n_events(10, |e| matches!(e, ChainOrchestratorEvent::ChainExtended(_))) + .await?; - // Send the L1 message to the sequencer node. - node0_l1_watcher_tx.send(Arc::new(l1_message_notification.clone())).await?; - node0_l1_watcher_tx.send(Arc::new(L1Notification::NewBlock(block_10_block_info))).await?; - wait_for_event_5s(&mut node0_rnm_events, ChainOrchestratorEvent::L1MessageCommitted(0)).await?; - wait_for_event_5s(&mut node0_rnm_events, ChainOrchestratorEvent::NewL1Block(10)).await?; + // Send the L1 message to the nodes. + fixture + .l1() + .add_message() + .at_block(10) + .sender(address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266")) + .send() + .await?; + fixture.l1().new_block(10).await?; - // Send L1 the L1 message to follower node. - node1_l1_watcher_tx.send(Arc::new(l1_message_notification)).await?; - node1_l1_watcher_tx.send(Arc::new(L1Notification::NewBlock(block_10_block_info))).await?; - wait_for_event_5s(&mut node1_rnm_events, ChainOrchestratorEvent::L1MessageCommitted(0)).await?; - wait_for_event_5s(&mut node1_rnm_events, ChainOrchestratorEvent::NewL1Block(10)).await?; + // Expect the events to reach all nodes. + fixture.expect_event_on_all_nodes().l1_message_committed().await?; + fixture.expect_event_on_all_nodes().new_l1_block().await?; // Build block that contains the L1 message. - let mut block11_before_reorg = None; - node0_rnm_handle.build_block(); - wait_for_event_predicate_5s(&mut node0_rnm_events, |e| { - if let ChainOrchestratorEvent::BlockSequenced(block) = e { - if block.header.number == 11 && - block.body.transactions.len() == 1 && - block.body.transactions.iter().any(|tx| tx.is_l1_message()) - { - block11_before_reorg = Some(block.header.hash_slow()); - return true; - } - } - - false - }) - .await?; + let block11_before_reorg = fixture + .build_block() + .expect_block_number(11) + .expect_l1_message_count(1) + .build_and_await_block() + .await?; for i in 12..=15 { - node0_rnm_handle.build_block(); - wait_for_block_sequenced_5s(&mut node0_rnm_events, i).await?; + fixture.build_block().expect_block_number(i).build_and_await_block().await?; } // Assert that the follower node has received the latest block from the sequencer node. - wait_for_block_imported_5s(&mut node1_rnm_events, 15).await?; + fixture + .expect_event_on(1) + .where_n_events(5, |e| matches!(e, ChainOrchestratorEvent::ChainExtended(_))) + .await?; // Assert both nodes are at block 15. - let node0_latest_block = latest_block(&node0).await?; - assert_eq!(node0_latest_block.header.number, 15); - assert_eq!( - node0_latest_block.header.hash_slow(), - latest_block(&node1).await?.header.hash_slow() - ); + let sequencer_latest_block = fixture.get_sequencer_block().await?; + let follower_latest_block = fixture.get_block(1).await?; + assert_eq!(sequencer_latest_block.header.number, 15); + assert_eq!(sequencer_latest_block.header.hash_slow(), follower_latest_block.header.hash_slow()); // Issue and wait for the reorg. - node0_l1_watcher_tx.send(Arc::new(L1Notification::Reorg(9))).await?; - - let reorg_block = reorg_block.as_ref().map(Into::::into); - wait_for_event_5s( - &mut node0_rnm_events, - ChainOrchestratorEvent::L1Reorg { - l1_block_number: 9, - queue_index: Some(0), - l2_head_block_info: reorg_block, - l2_safe_block_info: None, - }, - ) - .await?; - node1_l1_watcher_tx.send(Arc::new(L1Notification::Reorg(9))).await?; - wait_for_event_5s( - &mut node1_rnm_events, - ChainOrchestratorEvent::L1Reorg { - l1_block_number: 9, - queue_index: Some(0), - l2_head_block_info: reorg_block, - l2_safe_block_info: None, - }, - ) - .await?; + fixture.l1().reorg_to(9).await?; + fixture + .expect_event_on_all_nodes() + .where_event(|e| { + matches!( + e, + ChainOrchestratorEvent::L1Reorg { + l1_block_number: 9, + queue_index: Some(0), + l2_head_block_info: b, + l2_safe_block_info: None, + } + if *b == reorg_block.as_ref().map(|b| b.into()) + ) + }) + .await?; + + // Wait for block to handle the reorg + tokio::time::sleep(Duration::from_secs(1)).await; // Assert both nodes are at block 10. - assert_latest_block_on_rpc_by_number(&node0, 10).await; - assert_latest_block_on_rpc_by_number(&node1, 10).await; + assert_eq!(fixture.get_sequencer_block().await?.header.number, 10); + assert_eq!(fixture.get_block(1).await?.header.number, 10); // Since the L1 reorg reverted the L1 message included in block 11, the sequencer // should produce a new block at height 11. - node0_rnm_handle.build_block(); - wait_for_block_sequenced_5s(&mut node0_rnm_events, 11).await?; + fixture.build_block().expect_block_number(11).build_and_await_block().await?; // Assert that the follower node has received the new block from the sequencer node. - wait_for_block_imported_5s(&mut node1_rnm_events, 11).await?; + fixture.expect_event_on(1).chain_extended(11).await?; // Assert both nodes are at block 11. - assert_latest_block_on_rpc_by_number(&node0, 11).await; - let node0_latest_block = latest_block(&node0).await?; - assert_latest_block_on_rpc_by_hash(&node1, node0_latest_block.header.hash_slow()).await; + let sequencer_block11 = fixture.get_sequencer_block().await?; + assert_eq!(sequencer_block11.header.number, 11); + assert_eq!(fixture.get_block(1).await?.header.number, 11); // Assert that block 11 has a different hash after the reorg. - assert_ne!(block11_before_reorg.unwrap(), node0_latest_block.header.hash_slow()); + assert_ne!(block11_before_reorg.hash_slow(), sequencer_block11.header.hash_slow()); Ok(()) } @@ -1628,80 +1247,39 @@ async fn can_handle_l1_message_reorg() -> eyre::Result<()> { async fn requeues_transactions_after_l1_reorg() -> eyre::Result<()> { reth_tracing::init_test_tracing(); - let chain_spec = (*SCROLL_DEV).clone(); - let mut config = default_sequencer_test_scroll_rollup_node_config(); - config.sequencer_args.auto_start = false; - config.sequencer_args.block_time = 0; + let mut sequencer = + TestFixture::builder().sequencer().auto_start(false).block_time(0).build().await?; - let (mut nodes, _tasks, wallet) = - setup_engine(config, 1, chain_spec.clone(), false, false).await?; - let node = nodes.pop().expect("node exists"); - - let rnm_handle = node.inner.add_ons_handle.rollup_manager_handle.clone(); - let mut events = rnm_handle.get_event_listener().await?; - let l1_watcher_tx = node.inner.add_ons_handle.l1_watcher_tx.clone().unwrap(); - - l1_watcher_tx.send(Arc::new(L1Notification::Synced)).await?; - let _ = events.next().await; - let _ = events.next().await; + // Set the l1 as being synced. + sequencer.l1().sync().await?; // Let the sequencer build 10 blocks. for i in 1..=10 { - rnm_handle.build_block(); - let b = wait_for_block_sequenced_5s(&mut events, i).await?; + let b = sequencer.build_block().expect_block_number(i).build_and_await_block().await?; tracing::info!(target: "scroll::test", block_number = ?b.header.number, block_hash = ?b.header.hash_slow(), "Sequenced block"); } // Send a L1 message and wait for it to be indexed. - let block_2_info = BlockInfo { number: 2, hash: B256::random() }; - let l1_message_notification = L1Notification::L1Message { - message: TxL1Message { - queue_index: 0, - gas_limit: 21000, - to: Default::default(), - value: Default::default(), - sender: Default::default(), - input: Default::default(), - }, - block_info: block_2_info, - block_timestamp: 0, - }; + sequencer.l1().add_message().at_block(2).send().await?; + sequencer.expect_event().l1_message_committed().await?; + + // Send the L1 block which finalizes the L1 message. + sequencer.l1().new_block(2).await?; + sequencer.expect_event().new_l1_block().await?; // Build a L2 block with L1 message, so we can revert it later. - l1_watcher_tx.send(Arc::new(l1_message_notification.clone())).await?; - l1_watcher_tx.send(Arc::new(L1Notification::NewBlock(block_2_info))).await?; - wait_for_event_5s(&mut events, ChainOrchestratorEvent::L1MessageCommitted(0)).await?; - wait_for_event_5s(&mut events, ChainOrchestratorEvent::NewL1Block(2)).await?; - rnm_handle.build_block(); - wait_for_block_sequenced_5s(&mut events, 11).await?; + sequencer.build_block().build_and_await_block().await?; // Inject a user transaction and force the sequencer to include it in the next block - let wallet = Arc::new(Mutex::new(wallet)); - let tx = generate_tx(wallet.clone()).await; - let injected_tx_bytes: Vec = tx.clone().into(); - node.rpc.inject_tx(tx).await?; - - rnm_handle.build_block(); - let block_with_tx = wait_for_block_sequenced_5s(&mut events, 12).await?; - assert!( - block_contains_raw_tx(&block_with_tx, &injected_tx_bytes), - "block 11 should contain the injected transaction before the reorg" - ); + let hash = sequencer.inject_transfer().await?; + sequencer.build_block().expect_tx(hash).expect_tx_count(1).build_and_await_block().await?; // Trigger an L1 reorg that reverts the block containing the transaction - l1_watcher_tx.send(Arc::new(L1Notification::Reorg(1))).await?; - wait_for_event_predicate_5s(&mut events, |event| { - matches!(event, ChainOrchestratorEvent::L1Reorg { l1_block_number: 1, .. }) - }) - .await?; + sequencer.l1().reorg_to(1).await?; + sequencer.expect_event().l1_reorg().await?; // Build the next block – the reverted transaction should have been requeued - rnm_handle.build_block(); - let reseq_block = wait_for_block_sequenced_5s(&mut events, 11).await?; - assert!( - block_contains_raw_tx(&reseq_block, &injected_tx_bytes), - "re-sequenced block should contain the reverted transaction" - ); + sequencer.build_block().expect_tx(hash).expect_tx_count(1).build_and_await_block().await?; Ok(()) } @@ -1713,58 +1291,49 @@ async fn requeues_transactions_after_l1_reorg() -> eyre::Result<()> { async fn requeues_transactions_after_update_fcs_head() -> eyre::Result<()> { reth_tracing::init_test_tracing(); - let chain_spec = (*SCROLL_DEV).clone(); - let mut config = default_sequencer_test_scroll_rollup_node_config(); - config.sequencer_args.auto_start = false; - config.sequencer_args.block_time = 0; - - let (mut nodes, _tasks, wallet) = - setup_engine(config, 1, chain_spec.clone(), false, false).await?; - let node = nodes.pop().expect("node exists"); - - let handle = node.inner.add_ons_handle.rollup_manager_handle.clone(); - let mut events = handle.get_event_listener().await?; + let mut sequencer = + TestFixture::builder().sequencer().auto_start(false).block_time(0).build().await?; - // Set L1 synced to allow sequencing. - let l1_watcher_tx = node.inner.add_ons_handle.l1_watcher_tx.clone().unwrap(); - l1_watcher_tx.send(Arc::new(L1Notification::Synced)).await?; - let _ = events.next().await; - let _ = events.next().await; + // Set the l1 as being synced. + sequencer.l1().sync().await?; // Build a few blocks and remember block #4 as the future reset target. let mut target_head: Option = None; for i in 1..=4 { - handle.build_block(); - let b = wait_for_block_sequenced_5s(&mut events, i).await?; + let b = + sequencer.build_block().expect_block_number(i as u64).build_and_await_block().await?; if i == 4 { target_head = Some(BlockInfo { number: b.header.number, hash: b.header.hash_slow() }); } } // Inject a user transaction and include it in block 5. - let wallet = Arc::new(Mutex::new(wallet)); - let tx = generate_tx(wallet.clone()).await; - let injected_tx_bytes: Vec = tx.clone().into(); - node.rpc.inject_tx(tx).await?; - - handle.build_block(); - let block_with_tx = wait_for_block_sequenced_5s(&mut events, 5).await?; - assert!( - block_contains_raw_tx(&block_with_tx, &injected_tx_bytes), - "block 5 should contain the injected transaction before the FCS reset", - ); + let hash = sequencer.inject_transfer().await?; + sequencer + .build_block() + .expect_block_number(5) + .expect_tx(hash) + .expect_tx_count(1) + .build_and_await_block() + .await?; // Reset FCS head back to block 4; this should collect block 5's txs and requeue them. let head = target_head.expect("target head exists"); - handle.update_fcs_head(head).await.expect("update_fcs_head should succeed"); + sequencer + .sequencer() + .rollup_manager_handle + .update_fcs_head(head) + .await + .expect("update_fcs_head should succeed"); // Build the next block – the reverted transaction should have been requeued and included. - handle.build_block(); - let reseq_block = wait_for_block_sequenced_5s(&mut events, 5).await?; - assert!( - block_contains_raw_tx(&reseq_block, &injected_tx_bytes), - "re-sequenced block should contain the reverted transaction after FCS reset", - ); + sequencer + .build_block() + .expect_block_number(5) + .expect_tx(hash) + .expect_tx_count(1) + .build_and_await_block() + .await?; Ok(()) } @@ -1853,19 +1422,12 @@ async fn test_custom_genesis_block_production_and_propagation() -> eyre::Result< let custom_chain_spec = Arc::new(ScrollChainSpec::from_custom_genesis(custom_genesis)); // Launch 2 nodes: node0=sequencer and node1=follower. - let config = default_sequencer_test_scroll_rollup_node_config(); - let (mut nodes, _tasks, _) = - setup_engine(config, 2, custom_chain_spec.clone(), false, false).await?; - let node0 = nodes.remove(0); - let node1 = nodes.remove(0); - - // Get handles - let node0_rnm_handle = node0.inner.add_ons_handle.rollup_manager_handle.clone(); - let mut node0_rnm_events = node0_rnm_handle.get_event_listener().await?; - let node0_l1_watcher_tx = node0.inner.add_ons_handle.l1_watcher_tx.clone().unwrap(); - - let node1_rnm_handle = node1.inner.add_ons_handle.rollup_manager_handle.clone(); - let mut node1_rnm_events = node1_rnm_handle.get_event_listener().await?; + let mut fixture = TestFixture::builder() + .sequencer() + .followers(1) + .with_chain_spec(custom_chain_spec.clone()) + .build() + .await?; // Verify the genesis hash is different from all predefined networks assert_ne!(custom_chain_spec.genesis_hash(), SCROLL_DEV.genesis_hash()); @@ -1875,33 +1437,40 @@ async fn test_custom_genesis_block_production_and_propagation() -> eyre::Result< // Verify both nodes start with the same genesis hash from the custom chain spec assert_eq!( custom_chain_spec.genesis_hash(), - node0.block_hash(0), + fixture.get_sequencer_block().await?.header.hash_slow(), "Node0 should have the custom genesis hash" ); assert_eq!( custom_chain_spec.genesis_hash(), - node1.block_hash(0), + fixture.get_block(1).await?.header.hash_slow(), "Node1 should have the custom genesis hash" ); // Set L1 synced on sequencer node - node0_l1_watcher_tx.send(Arc::new(L1Notification::Synced)).await?; + fixture.l1().for_node(0).sync().await?; + fixture.expect_event().l1_synced().await?; // Let the sequencer build 10 blocks. - for i in 1..=10 { - node0_rnm_handle.build_block(); - let b = wait_for_block_sequenced_5s(&mut node0_rnm_events, i).await?; + for _ in 1..=10 { + let b = fixture.build_block().build_and_await_block().await?; tracing::info!(target: "scroll::test", block_number = ?b.header.number, block_hash = ?b.header.hash_slow(), "Sequenced block"); } // Assert that the follower node has received all 10 blocks from the sequencer node. - wait_for_block_imported_5s(&mut node1_rnm_events, 10).await?; + fixture + .expect_event_on(1) + .where_n_events(10, |e| matches!(e, ChainOrchestratorEvent::ChainExtended(_))) + .await?; // Assert both nodes have the same latest block hash. - assert_eq!(latest_block(&node0).await?.header.number, 10, "Node0 should be at block 10"); assert_eq!( - latest_block(&node0).await?.header.hash_slow(), - latest_block(&node1).await?.header.hash_slow(), + fixture.get_sequencer_block().await?.header.number, + 10, + "Node0 should be at block 10" + ); + assert_eq!( + fixture.get_sequencer_block().await?.header.hash_slow(), + fixture.get_block(1).await?.header.hash_slow(), "Both nodes should have the same latest block hash" ); @@ -1912,67 +1481,59 @@ async fn test_custom_genesis_block_production_and_propagation() -> eyre::Result< async fn can_rpc_enable_disable_sequencing() -> eyre::Result<()> { reth_tracing::init_test_tracing(); color_eyre::install()?; - let chain_spec = (*SCROLL_DEV).clone(); // Launch sequencer node with automatic sequencing enabled. - let mut config = default_sequencer_test_scroll_rollup_node_config(); - config.sequencer_args.block_time = 40; // Enable automatic block production - config.sequencer_args.auto_start = true; - - let (mut nodes, _tasks, _) = setup_engine(config, 2, chain_spec.clone(), false, false).await?; - let node0 = nodes.remove(0); - let node1 = nodes.remove(0); - - // Get handles - let node0_rnm_handle = node0.inner.add_ons_handle.rollup_manager_handle.clone(); - let mut node0_rnm_events = node0_rnm_handle.get_event_listener().await?; - let node0_l1_watcher_tx = node0.inner.add_ons_handle.l1_watcher_tx.clone().unwrap(); - - let node1_rnm_handle = node1.inner.add_ons_handle.rollup_manager_handle.clone(); - let mut node1_rnm_events = node1_rnm_handle.get_event_listener().await?; + let mut fixture = TestFixture::builder() + .sequencer() + .followers(1) + .block_time(40) + .with_sequencer_auto_start(true) + .build() + .await?; // Set L1 synced - node0_l1_watcher_tx.send(Arc::new(L1Notification::Synced)).await?; - - // Create RPC client - let client0 = node0.rpc_client().expect("RPC client should be available"); + fixture.l1().sync().await?; // Test that sequencing is initially enabled (blocks produced automatically) tokio::time::sleep(Duration::from_millis(100)).await; - assert_ne!(latest_block(&node0).await?.header.number, 0, "Should produce blocks"); + assert_ne!(fixture.get_sequencer_block().await?.header.number, 0, "Should produce blocks"); // Disable automatic sequencing via RPC - let result = RollupNodeExtApiClient::disable_automatic_sequencing(&client0).await?; + let client = fixture.sequencer().node.rpc_client().expect("Should have rpc client"); + let result = RollupNodeExtApiClient::disable_automatic_sequencing(&client).await?; assert!(result, "Disable automatic sequencing should return true"); // Wait a bit and verify no more blocks are produced automatically. // +1 blocks is okay due to still being processed - let block_num_before_wait = latest_block(&node0).await?.header.number; + let block_num_before_wait = fixture.get_sequencer_block().await?.header.number; tokio::time::sleep(Duration::from_millis(300)).await; - let block_num_after_wait = latest_block(&node0).await?.header.number; + let block_num_after_wait = fixture.get_sequencer_block().await?.header.number; assert!( (block_num_before_wait..=block_num_before_wait + 1).contains(&block_num_after_wait), "No blocks should be produced automatically after disabling" ); // Make sure follower is at same block - wait_for_block_imported_5s(&mut node1_rnm_events, block_num_after_wait).await?; - assert_eq!(block_num_after_wait, latest_block(&node1).await?.header.number); + fixture.expect_event_on(1).chain_extended(block_num_after_wait).await?; + assert_eq!(block_num_after_wait, fixture.get_block(1).await?.header.number); // Verify manual block building still works - node0_rnm_handle.build_block(); - wait_for_block_sequenced_5s(&mut node0_rnm_events, block_num_after_wait + 1).await?; + fixture + .build_block() + .expect_block_number(block_num_after_wait + 1) + .build_and_await_block() + .await?; // Wait for the follower to import the block - wait_for_block_imported_5s(&mut node1_rnm_events, block_num_after_wait + 1).await?; + fixture.expect_event_on(1).chain_extended(block_num_after_wait + 1).await?; // Enable sequencing again - let result = RollupNodeExtApiClient::enable_automatic_sequencing(&client0).await?; + let result = RollupNodeExtApiClient::enable_automatic_sequencing(&client).await?; assert!(result, "Enable automatic sequencing should return true"); // Make sure automatic sequencing resumes - wait_for_block_sequenced_5s(&mut node0_rnm_events, block_num_after_wait + 2).await?; - wait_for_block_imported_5s(&mut node1_rnm_events, block_num_after_wait + 2).await?; + fixture.expect_event().block_sequenced(block_num_after_wait + 2).await?; + fixture.expect_event_on(1).chain_extended(block_num_after_wait + 2).await?; Ok(()) } @@ -2004,112 +1565,97 @@ async fn can_rpc_enable_disable_sequencing() -> eyre::Result<()> { async fn can_reject_l2_block_with_unknown_l1_message() -> eyre::Result<()> { reth_tracing::init_test_tracing(); color_eyre::install()?; - let chain_spec = (*SCROLL_DEV).clone(); // Launch 2 nodes: node0=sequencer and node1=follower. - let config = default_sequencer_test_scroll_rollup_node_config(); - let (mut nodes, _tasks, _) = setup_engine(config, 2, chain_spec.clone(), false, false).await?; - let node0 = nodes.remove(0); - let node1 = nodes.remove(0); - - // Get handles - let node0_rnm_handle = node0.inner.add_ons_handle.rollup_manager_handle.clone(); - let mut node0_rnm_events = node0_rnm_handle.get_event_listener().await?; - let node0_l1_watcher_tx = node0.inner.add_ons_handle.l1_watcher_tx.as_ref().unwrap(); - - let node1_rnm_handle = node1.inner.add_ons_handle.rollup_manager_handle.clone(); - let mut node1_rnm_events = node1_rnm_handle.get_event_listener().await?; - let node1_l1_watcher_tx = node1.inner.add_ons_handle.l1_watcher_tx.as_ref().unwrap(); + let mut fixture = TestFixture::builder().sequencer().followers(1).build().await?; // Set L1 synced - node0_l1_watcher_tx.send(Arc::new(L1Notification::Synced)).await?; - node1_l1_watcher_tx.send(Arc::new(L1Notification::Synced)).await?; + fixture.l1().sync().await?; + fixture.expect_event_on_all_nodes().l1_synced().await?; // Let the sequencer build 10 blocks before performing the reorg process. for i in 1..=10 { - node0_rnm_handle.build_block(); - let b = wait_for_block_sequenced_5s(&mut node0_rnm_events, i).await?; + let b = fixture.build_block().expect_block_number(i).build_and_await_block().await?; tracing::info!(target: "scroll::test", block_number = ?b.header.number, block_hash = ?b.header.hash_slow(), "Sequenced block") } // Assert that the follower node has received all 10 blocks from the sequencer node. - wait_for_block_imported_5s(&mut node1_rnm_events, 10).await?; - - // Send a L1 message and wait for it to be indexed. - let block_10_block_info = BlockInfo { number: 10, hash: B256::random() }; - let l1_message_notification = L1Notification::L1Message { - message: TxL1Message { - queue_index: 0, - gas_limit: 21000, - to: Default::default(), - value: Default::default(), - sender: address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"), - input: Default::default(), - }, - block_info: block_10_block_info, - block_timestamp: 0, - }; + fixture.expect_event_on(1).chain_extended(10).await?; // Send the L1 message to the sequencer node but not to follower node. - node0_l1_watcher_tx.send(Arc::new(l1_message_notification.clone())).await?; - node0_l1_watcher_tx.send(Arc::new(L1Notification::NewBlock(block_10_block_info))).await?; - wait_for_event_5s(&mut node0_rnm_events, ChainOrchestratorEvent::L1MessageCommitted(0)).await?; - wait_for_event_5s(&mut node0_rnm_events, ChainOrchestratorEvent::NewL1Block(10)).await?; + fixture + .l1() + .for_node(0) + .add_message() + .at_block(10) + .sender(address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266")) + .value(0) + .to(Address::ZERO) + .send() + .await?; + fixture.expect_event().l1_message_committed().await?; - // Build block that contains the L1 message. - node0_rnm_handle.build_block(); - wait_for_event_predicate_5s(&mut node0_rnm_events, |e| { - if let ChainOrchestratorEvent::BlockSequenced(block) = e { - if block.header.number == 11 && - block.body.transactions.len() == 1 && - block.body.transactions.iter().any(|tx| tx.is_l1_message()) - { - return true; - } - } + fixture.l1().for_node(0).new_block(10).await?; + fixture.expect_event().new_l1_block().await?; - false - }) - .await?; + // Build block that contains the L1 message. + fixture + .build_block() + .expect_block_number(11) + .expect_l1_message() + .build_and_await_block() + .await?; for i in 12..=15 { - node0_rnm_handle.build_block(); - wait_for_block_sequenced_5s(&mut node0_rnm_events, i).await?; + fixture.build_block().expect_block_number(i).build_and_await_block().await?; } - wait_for_event_5s( - &mut node1_rnm_events, - ChainOrchestratorEvent::L1MessageNotFoundInDatabase(L1MessageKey::TransactionHash(b256!( - "0x0a2f8e75392ab51a26a2af835042c614eb141cd934fe1bdd4934c10f2fe17e98" - ))), - ) - .await?; + fixture + .expect_event_on(1) + .where_event(|e| { + matches!( + e, + ChainOrchestratorEvent::L1MessageNotFoundInDatabase(L1MessageKey::TransactionHash( + hash + )) if hash == &b256!("0x0a2f8e75392ab51a26a2af835042c614eb141cd934fe1bdd4934c10f2fe17e98") + ) + }) + .await?; // follower node should not import block 15 // follower node doesn't know about the L1 message so stops processing the chain at block 10 - assert_eq!(latest_block(&node1).await?.header.number, 10); + assert_eq!(fixture.get_block(1).await?.header.number, 10); // Finally send L1 the L1 message to follower node. - node1_l1_watcher_tx.send(Arc::new(l1_message_notification)).await?; - node1_l1_watcher_tx.send(Arc::new(L1Notification::NewBlock(block_10_block_info))).await?; - wait_for_event_5s(&mut node1_rnm_events, ChainOrchestratorEvent::L1MessageCommitted(0)).await?; - wait_for_event_5s(&mut node1_rnm_events, ChainOrchestratorEvent::NewL1Block(10)).await?; + fixture + .l1() + .for_node(1) + .add_message() + .at_block(10) + .sender(address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266")) + .value(0) + .to(Address::ZERO) + .send() + .await?; + fixture.expect_event_on(1).l1_message_committed().await?; + + fixture.l1().for_node(1).new_block(10).await?; + fixture.expect_event_on(1).new_l1_block().await?; // Produce another block and send to follower node. - node0_rnm_handle.build_block(); - wait_for_block_sequenced_5s(&mut node0_rnm_events, 16).await?; + fixture.build_block().expect_block_number(16).build_and_await_block().await?; // Assert that the follower node has received the latest block from the sequencer node and // processed the missing chain before. // This is possible now because it has received the L1 message. - wait_for_block_imported_5s(&mut node1_rnm_events, 16).await?; + fixture.expect_event_on(1).chain_extended(16).await?; // Assert both nodes are at block 16. - let node0_latest_block = latest_block(&node0).await?; + let node0_latest_block = fixture.get_sequencer_block().await?; assert_eq!(node0_latest_block.header.number, 16); assert_eq!( node0_latest_block.header.hash_slow(), - latest_block(&node1).await?.header.hash_slow() + fixture.get_block(1).await?.header.hash_slow() ); Ok(()) @@ -2120,30 +1666,20 @@ async fn can_gossip_over_eth_wire() -> eyre::Result<()> { reth_tracing::init_test_tracing(); // Create the chain spec for scroll dev with Feynman activated and a test genesis. - let chain_spec = (*SCROLL_DEV).clone(); - - let mut config = default_sequencer_test_scroll_rollup_node_config(); - config.sequencer_args.block_time = 40; - config.sequencer_args.auto_start = true; - - // Setup the rollup node manager. - let (mut nodes, _tasks, _) = - setup_engine(config, 2, chain_spec.clone(), false, false).await.unwrap(); - let follower = nodes.pop().unwrap(); - let sequencer = nodes.pop().unwrap(); - - // Set the L1 synced on the sequencer node to start block production. - let mut sequencer_events = - sequencer.inner.add_ons_handle.rollup_manager_handle.get_event_listener().await.unwrap(); - let sequencer_l1_notification_tx = - sequencer.inner.add_ons_handle.l1_watcher_tx.clone().unwrap(); + let mut fixture = TestFixture::builder() + .sequencer() + .followers(1) + .with_sequencer_auto_start(true) + .block_time(40) + .build() + .await?; // Set the L1 synced on the sequencer node to start block production. - sequencer_l1_notification_tx.send(Arc::new(L1Notification::Synced)).await?; - sequencer_events.next().await; - sequencer_events.next().await; + fixture.l1().for_node(0).sync().await?; + fixture.expect_event().l1_synced().await?; - let mut eth_wire_blocks = follower.inner.network.eth_wire_block_listener().await?; + let mut eth_wire_blocks = + fixture.follower(0).node.inner.network.eth_wire_block_listener().await?; if let Some(block) = eth_wire_blocks.next().await { println!("Received block from eth-wire network: {block:?}"); @@ -2168,58 +1704,41 @@ async fn signer_rotation() -> eyre::Result<()> { let signer_2 = PrivateKeySigner::random().with_chain_id(Some(chain_spec.chain().id())); let signer_2_address = signer_2.address(); - let mut sequencer_1_config = default_sequencer_test_scroll_rollup_node_config(); - - sequencer_1_config.test = false; - sequencer_1_config.consensus_args.algorithm = ConsensusAlgorithm::SystemContract; - sequencer_1_config.consensus_args.authorized_signer = Some(signer_1_address); - sequencer_1_config.signer_args.private_key = Some(signer_1); - sequencer_1_config.sequencer_args.block_time = 40; - sequencer_1_config.sequencer_args.auto_start = true; - sequencer_1_config.network_args.enable_eth_scroll_wire_bridge = false; - - let mut sequencer_2_config = default_sequencer_test_scroll_rollup_node_config(); - sequencer_2_config.test = false; - sequencer_2_config.consensus_args.algorithm = ConsensusAlgorithm::SystemContract; - sequencer_2_config.consensus_args.authorized_signer = Some(signer_1_address); - sequencer_2_config.signer_args.private_key = Some(signer_2); - sequencer_2_config.sequencer_args.block_time = 40; - sequencer_2_config.sequencer_args.auto_start = true; - sequencer_2_config.network_args.enable_eth_scroll_wire_bridge = false; - - // Setup two sequencer nodes. - let (mut nodes, _tasks, _) = - setup_engine(sequencer_1_config, 2, chain_spec.clone(), false, false).await.unwrap(); - let follower = nodes.pop().unwrap(); - let mut sequencer_1 = nodes.pop().unwrap(); - let (mut nodes, _tasks, _) = - setup_engine(sequencer_2_config, 1, chain_spec.clone(), false, false).await.unwrap(); - let mut sequencer_2 = nodes.pop().unwrap(); + let mut fixture1 = TestFixture::builder() + .sequencer() + .followers(1) + .with_test(false) + .with_consensus_system_contract(signer_1_address) + .with_signer(signer_1) + .with_sequencer_auto_start(true) + .with_eth_scroll_bridge(false) + .block_time(40) + .build() + .await?; - // Create an L1 - let follower_l1_notification_tx = follower.inner.add_ons_handle.l1_watcher_tx.clone().unwrap(); - let sequencer_1_l1_notification_tx = - sequencer_1.inner.add_ons_handle.l1_watcher_tx.clone().unwrap(); - let sequencer_2_l1_notification_tx = - sequencer_2.inner.add_ons_handle.l1_watcher_tx.clone().unwrap(); + let mut fixture2 = TestFixture::builder() + .sequencer() + .with_test(false) + .with_consensus_system_contract(signer_1_address) + .with_signer(signer_2) + .with_sequencer_auto_start(true) + .with_eth_scroll_bridge(false) + .block_time(40) + .build() + .await?; // Set the L1 synced on both nodes to start block production. - sequencer_1_l1_notification_tx.send(Arc::new(L1Notification::Synced)).await?; - sequencer_2_l1_notification_tx.send(Arc::new(L1Notification::Synced)).await?; - - // Create a follower event stream. - let mut follower_events = - follower.inner.add_ons_handle.rollup_manager_handle.get_event_listener().await.unwrap(); - let mut sequencer_2_events = - sequencer_2.inner.add_ons_handle.rollup_manager_handle.get_event_listener().await.unwrap(); + fixture1.l1().for_node(0).sync().await?; + fixture2.l1().for_node(0).sync().await?; // connect the two sequencers - sequencer_1.connect(&mut sequencer_2).await; + fixture1.sequencer().node.connect(&mut fixture2.sequencer().node).await; - for _ in 0..5 { - wait_n_events( - &mut follower_events, - |event| { + // wait for 5 blocks to be produced. + for i in 1..=5 { + fixture1 + .expect_event_on(1) + .where_event(|event| { if let ChainOrchestratorEvent::NewBlockReceived(block) = event { let signature = block.signature; let hash = sig_encode_hash(&block.block); @@ -2229,45 +1748,22 @@ async fn signer_rotation() -> eyre::Result<()> { } else { false } - }, - 1, - ) - .await; - wait_n_events( - &mut follower_events, - |event| matches!(event, ChainOrchestratorEvent::ChainExtended(_)), - 1, - ) - .await; + }) + .await?; + fixture1.expect_event_on(1).chain_extended(i).await?; } - wait_n_events( - &mut sequencer_2_events, - |e| matches!(e, ChainOrchestratorEvent::ChainExtended(_)), - 5, - ) - .await; + fixture2.expect_event().chain_extended(5).await?; // now update the authorized signer to sequencer 2 - follower_l1_notification_tx - .send(Arc::new(L1Notification::Consensus(ConsensusUpdate::AuthorizedSigner( - signer_2_address, - )))) - .await?; - sequencer_1_l1_notification_tx - .send(Arc::new(L1Notification::Consensus(ConsensusUpdate::AuthorizedSigner( - signer_2_address, - )))) - .await?; - sequencer_2_l1_notification_tx - .send(Arc::new(L1Notification::Consensus(ConsensusUpdate::AuthorizedSigner( - signer_2_address, - )))) - .await?; + fixture1.l1().signer_update(signer_2_address).await?; + fixture2.l1().signer_update(signer_2_address).await?; - wait_n_events( - &mut follower_events, - |event| { + tokio::time::sleep(Duration::from_secs(1)).await; + + fixture1 + .expect_event_on(1) + .where_n_events(5, |event| { if let ChainOrchestratorEvent::NewBlockReceived(block) = event { let signature = block.signature; let hash = sig_encode_hash(&block.block); @@ -2277,10 +1773,8 @@ async fn signer_rotation() -> eyre::Result<()> { } else { false } - }, - 5, - ) - .await; + }) + .await?; Ok(()) } @@ -2291,136 +1785,6 @@ pub fn read_to_bytes>(path: P) -> eyre::Result Ok(Bytes::from_str(&std::fs::read_to_string(path)?)?) } -async fn latest_block( - node: &NodeHelperType< - ScrollRollupNode, - BlockchainProvider>, - >, -) -> eyre::Result> { - node.rpc - .inner - .eth_api() - .block_by_number(BlockNumberOrTag::Latest, false) - .await? - .ok_or_else(|| eyre::eyre!("Latest block not found")) -} - -async fn wait_for_block_sequenced( - events: &mut EventStream, - block_number: u64, - timeout: Duration, -) -> eyre::Result { - let mut block = None; - - wait_for_event_predicate( - events, - |e| { - if let ChainOrchestratorEvent::BlockSequenced(b) = e { - if b.header.number == block_number { - block = Some(b); - return true; - } - } - - false - }, - timeout, - ) - .await?; - - block.ok_or_else(|| eyre::eyre!("Block with number {block_number} was not sequenced")) -} - -async fn wait_for_block_sequenced_5s( - events: &mut EventStream, - block_number: u64, -) -> eyre::Result { - wait_for_block_sequenced(events, block_number, Duration::from_secs(5)).await -} - -async fn wait_for_chain_extended( - events: &mut EventStream, - block_number: u64, - timeout: Duration, -) -> eyre::Result { - let mut block = None; - - wait_for_event_predicate( - events, - |e| { - if let ChainOrchestratorEvent::ChainExtended(b) = e { - let b = &b.chain[0]; - if b.header.number == block_number { - block = Some(b.clone()); - return true; - } - } - - false - }, - timeout, - ) - .await?; - - block.ok_or_else(|| eyre::eyre!("Block with number {block_number} was not imported")) -} - -async fn wait_for_block_imported_5s( - events: &mut EventStream, - block_number: u64, -) -> eyre::Result { - wait_for_chain_extended(events, block_number, Duration::from_secs(5)).await -} - -async fn wait_for_event_predicate( - event_stream: &mut EventStream, - mut predicate: impl FnMut(ChainOrchestratorEvent) -> bool, - timeout: Duration, -) -> eyre::Result<()> { - let sleep = tokio::time::sleep(timeout); - tokio::pin!(sleep); - - loop { - tokio::select! { - maybe_event = event_stream.next() => { - match maybe_event { - Some(e) if predicate(e.clone()) => { - tracing::debug!(target: "scroll::test", event = ?e, "Received event"); - return Ok(()); - } - Some(e) => { - tracing::debug!(target: "scroll::test", event = ?e, "Ignoring event"); - }, // Ignore other events - None => return Err(eyre::eyre!("Event stream ended unexpectedly")), - } - } - _ = &mut sleep => return Err(eyre::eyre!("Timeout while waiting for event")), - } - } -} - -async fn wait_for_event_predicate_5s( - event_stream: &mut EventStream, - predicate: impl FnMut(ChainOrchestratorEvent) -> bool, -) -> eyre::Result<()> { - wait_for_event_predicate(event_stream, predicate, Duration::from_secs(5)).await -} - -async fn wait_for_event( - event_stream: &mut EventStream, - event: ChainOrchestratorEvent, - timeout: Duration, -) -> eyre::Result<()> { - wait_for_event_predicate(event_stream, |e| e == event, timeout).await -} - -async fn wait_for_event_5s( - event_stream: &mut EventStream, - event: ChainOrchestratorEvent, -) -> eyre::Result<()> { - wait_for_event(event_stream, event, Duration::from_secs(5)).await -} - /// Waits for n events to be emitted. async fn wait_n_events( events: &mut EventStream, @@ -2442,7 +1806,7 @@ async fn wait_n_events( pub async fn eventually(timeout: Duration, tick: Duration, message: &str, mut predicate: F) where F: FnMut() -> Fut, - Fut: std::future::Future, + Fut: Future, { let mut interval = time::interval(tick); let start = time::Instant::now(); @@ -2456,46 +1820,3 @@ where interval.tick().await; } } - -async fn assert_latest_block_on_rpc_by_number( - node: &NodeHelperType< - ScrollRollupNode, - BlockchainProvider>, - >, - block_number: u64, -) { - eventually( - Duration::from_secs(5), - Duration::from_millis(100), - "Waiting for latest block by number on node", - || async { - println!( - "Latest block number: {}, hash: {}", - latest_block(node).await.unwrap().header.number, - latest_block(node).await.unwrap().header.hash_slow() - ); - latest_block(node).await.unwrap().header.number == block_number - }, - ) - .await; -} - -async fn assert_latest_block_on_rpc_by_hash( - node: &NodeHelperType< - ScrollRollupNode, - BlockchainProvider>, - >, - block_hash: B256, -) { - eventually( - Duration::from_secs(5), - Duration::from_millis(100), - "Waiting for latest block by hash on node", - || async { latest_block(node).await.unwrap().header.hash_slow() == block_hash }, - ) - .await; -} - -fn block_contains_raw_tx(block: &ScrollBlock, raw_tx: &[u8]) -> bool { - block.body.transactions.iter().any(|tx| tx.encoded_2718().as_slice() == raw_tx) -} diff --git a/crates/node/tests/sync.rs b/crates/node/tests/sync.rs index 9e5bf813..190cb1f1 100644 --- a/crates/node/tests/sync.rs +++ b/crates/node/tests/sync.rs @@ -1,16 +1,16 @@ //! Contains tests related to RN and EN sync. use alloy_primitives::{b256, Address, B256, U256}; -use alloy_provider::{Provider, ProviderBuilder}; use futures::StreamExt; use reqwest::Url; use reth_provider::{BlockIdReader, BlockReader}; +use reth_rpc_eth_api::helpers::EthTransactions; use reth_scroll_chainspec::{SCROLL_DEV, SCROLL_SEPOLIA}; use reth_tokio_util::EventStream; use rollup_node::{ test_utils::{ - default_sequencer_test_scroll_rollup_node_config, default_test_scroll_rollup_node_config, - generate_tx, setup_engine, + default_test_scroll_rollup_node_config, generate_tx, setup_engine, EventAssertions, + TestFixture, }, BlobProviderArgs, ChainOrchestratorArgs, ConsensusArgs, EngineDriverArgs, L1ProviderArgs, RollupNodeDatabaseArgs, RollupNodeGasPriceOracleArgs, RollupNodeNetworkArgs, RpcArgs, @@ -103,58 +103,31 @@ async fn test_should_consolidate_to_block_15k() -> eyre::Result<()> { async fn test_node_produces_block_on_startup() -> eyre::Result<()> { reth_tracing::init_test_tracing(); - let mut sequencer_node_config = default_sequencer_test_scroll_rollup_node_config(); - sequencer_node_config.sequencer_args.auto_start = true; - sequencer_node_config.sequencer_args.allow_empty_blocks = false; + // Start a sequencer and follower node. + let mut fixture = TestFixture::builder() + .sequencer() + .followers(1) + .auto_start(true) + .allow_empty_blocks(false) + .build() + .await?; - let (mut nodes, _tasks, wallet) = - setup_engine(sequencer_node_config, 2, (*SCROLL_DEV).clone(), false, false).await?; - - let follower = nodes.pop().unwrap(); - let mut follower_events = - follower.inner.add_ons_handle.rollup_manager_handle.get_event_listener().await?; - let follower_l1_watcher_tx = follower.inner.add_ons_handle.l1_watcher_tx.clone().unwrap(); - - let sequencer = nodes.pop().unwrap(); - let mut sequencer_events = - sequencer.inner.add_ons_handle.rollup_manager_handle.get_event_listener().await?; - let sequencer_l1_watcher_tx = sequencer.inner.add_ons_handle.l1_watcher_tx.clone().unwrap(); - - // Send a notification to the sequencer and follower nodes that the L1 watcher is synced. - sequencer_l1_watcher_tx.send(Arc::new(L1Notification::Synced)).await.unwrap(); - follower_l1_watcher_tx.send(Arc::new(L1Notification::Synced)).await.unwrap(); + fixture.l1().sync().await?; // wait for both nodes to be synced. - wait_n_events( - &mut sequencer_events, - |e| matches!(e, ChainOrchestratorEvent::ChainConsolidated { from: _, to: _ }), - 1, - ) - .await; - wait_n_events( - &mut follower_events, - |e| matches!(e, ChainOrchestratorEvent::ChainConsolidated { from: _, to: _ }), - 1, - ) - .await; + fixture.expect_event_on_all_nodes().chain_consolidated().await?; // construct a transaction and send it to the follower node. - let wallet = Arc::new(tokio::sync::Mutex::new(wallet)); + let wallet = fixture.wallet(); + let follower_rpc = fixture.follower(0).node.rpc.inner.clone(); let handle = tokio::spawn(async move { loop { let tx = generate_tx(wallet.clone()).await; - follower.rpc.inject_tx(tx).await.unwrap(); + let _ = follower_rpc.eth_api().send_raw_transaction(tx).await; } }); - // Assert that the follower node receives the new block. - wait_n_events( - &mut follower_events, - |e| matches!(e, ChainOrchestratorEvent::ChainExtended(_)), - 1, - ) - .await; - + fixture.expect_event_on_followers().chain_extended(1).await?; drop(handle); Ok(()) @@ -165,72 +138,49 @@ async fn test_node_produces_block_on_startup() -> eyre::Result<()> { #[tokio::test] async fn test_should_trigger_pipeline_sync_for_execution_node() -> eyre::Result<()> { reth_tracing::init_test_tracing(); - let node_config = default_test_scroll_rollup_node_config(); - let mut sequencer_node_config = default_sequencer_test_scroll_rollup_node_config(); - sequencer_node_config.sequencer_args.block_time = 40; - sequencer_node_config.sequencer_args.auto_start = true; - // Create the chain spec for scroll mainnet with Feynman activated and a test genesis. - let chain_spec = (*SCROLL_DEV).clone(); - let (mut nodes, _tasks, _) = - setup_engine(sequencer_node_config.clone(), 1, chain_spec.clone(), false, false) - .await - .unwrap(); - let mut synced = nodes.pop().unwrap(); - let mut synced_events = synced.inner.rollup_manager_handle.get_event_listener().await?; - let synced_l1_watcher_tx = synced.inner.add_ons_handle.l1_watcher_tx.clone().unwrap(); - - let (mut nodes, _tasks, _) = - setup_engine(node_config.clone(), 1, chain_spec, false, false).await.unwrap(); - let mut unsynced = nodes.pop().unwrap(); - let mut unsynced_events = unsynced.inner.rollup_manager_handle.get_event_listener().await?; + const OPTIMISTIC_SYNC_TRIGGER: u64 = 100; + let mut sequencer = TestFixture::builder() + .sequencer() + .block_time(40) + .auto_start(true) + .optimistic_sync_trigger(OPTIMISTIC_SYNC_TRIGGER) + .build() + .await?; + + let mut follower = TestFixture::builder() + .followers(1) + .optimistic_sync_trigger(OPTIMISTIC_SYNC_TRIGGER) + .build() + .await?; // Set the L1 to synced on the synced node to start block production. - synced_l1_watcher_tx.send(Arc::new(L1Notification::Synced)).await.unwrap(); + sequencer.l1().sync().await?; // Wait for the chain to be advanced by the sequencer. - let optimistic_sync_trigger = node_config.chain_orchestrator_args.optimistic_sync_trigger + 1; - wait_n_events( - &mut synced_events, - |e| matches!(e, ChainOrchestratorEvent::BlockSequenced(_)), - optimistic_sync_trigger, - ) - .await; + sequencer.expect_event().block_sequenced(OPTIMISTIC_SYNC_TRIGGER + 1).await?; // Connect the nodes together. - synced.network.add_peer(unsynced.network.record()).await; - unsynced.network.next_session_established().await; - synced.network.next_session_established().await; + sequencer.sequencer().node.connect(&mut follower.follower(0).node).await; // Assert that the unsynced node triggers optimistic sync. - wait_n_events( - &mut unsynced_events, - |e| matches!(e, ChainOrchestratorEvent::OptimisticSync(_)), - 1, - ) - .await; + follower.expect_event().optimistic_sync().await?; // Verify the unsynced node syncs. - let provider = ProviderBuilder::new().connect_http(unsynced.rpc_url()); + let mut num = follower.get_block(0).await?.header.number; let mut retries = 0; - let mut num = provider.get_block_number().await.unwrap(); loop { - if retries > 10 || num > optimistic_sync_trigger { + if retries > 10 || num > OPTIMISTIC_SYNC_TRIGGER { break } - num = provider.get_block_number().await.unwrap(); + num = follower.get_block(0).await?.header.number; tokio::time::sleep(tokio::time::Duration::from_millis(200)).await; retries += 1; } // Assert that the unsynced node triggers a chain extension on the optimistic chain. - wait_n_events( - &mut unsynced_events, - |e| matches!(e, ChainOrchestratorEvent::ChainExtended(_)), - 1, - ) - .await; + follower.expect_event().chain_extended(num).await?; Ok(()) } @@ -239,60 +189,26 @@ async fn test_should_trigger_pipeline_sync_for_execution_node() -> eyre::Result< #[tokio::test] async fn test_should_consolidate_after_optimistic_sync() -> eyre::Result<()> { reth_tracing::init_test_tracing(); - let node_config = default_test_scroll_rollup_node_config(); - let sequencer_node_config = ScrollRollupNodeConfig { - test: true, - network_args: RollupNodeNetworkArgs { - enable_eth_scroll_wire_bridge: true, - enable_scroll_wire: true, - sequencer_url: None, - signer_address: None, - }, - database_args: RollupNodeDatabaseArgs::default(), - l1_provider_args: L1ProviderArgs::default(), - engine_driver_args: EngineDriverArgs::default(), - chain_orchestrator_args: ChainOrchestratorArgs::default(), - sequencer_args: SequencerArgs { - sequencer_enabled: true, - auto_start: true, - block_time: 20, - l1_message_inclusion_mode: L1MessageInclusionMode::BlockDepth(0), - allow_empty_blocks: true, - ..SequencerArgs::default() - }, - blob_provider_args: BlobProviderArgs { mock: true, ..Default::default() }, - signer_args: Default::default(), - gas_price_oracle_args: RollupNodeGasPriceOracleArgs::default(), - consensus_args: ConsensusArgs::noop(), - database: None, - rpc_args: RpcArgs::default(), - }; - // Create the chain spec for scroll dev with Feynman activated and a test genesis. - let chain_spec = (*SCROLL_DEV).clone(); + let mut sequencer = TestFixture::builder() + .sequencer() + .with_eth_scroll_bridge(true) + .with_scroll_wire(true) + .auto_start(true) + .block_time(20) + .with_l1_message_delay(0) + .allow_empty_blocks(true) + .build() + .await?; - // Create a sequencer node and an unsynced node. - let (mut nodes, _tasks, _) = - setup_engine(sequencer_node_config, 1, chain_spec.clone(), false, false).await.unwrap(); - let mut sequencer = nodes.pop().unwrap(); - let sequencer_l1_watcher_tx = sequencer.inner.add_ons_handle.l1_watcher_tx.clone().unwrap(); - let sequencer_handle = sequencer.inner.rollup_manager_handle.clone(); - let mut sequencer_events = sequencer_handle.get_event_listener().await?; - - let (mut nodes, _tasks, _) = - setup_engine(node_config.clone(), 1, chain_spec, false, false).await.unwrap(); - let mut follower = nodes.pop().unwrap(); - let follower_l1_watcher_tx = follower.inner.add_ons_handle.l1_watcher_tx.clone().unwrap(); - let mut follower_events = - follower.inner.add_ons_handle.rollup_manager_handle.get_event_listener().await?; + let mut follower = TestFixture::builder().followers(1).build().await?; // Send a notification to the sequencer node that the L1 watcher is synced. - sequencer_l1_watcher_tx.send(Arc::new(L1Notification::Synced)).await.unwrap(); + sequencer.l1().sync().await?; // Create a sequence of L1 messages to be added to the sequencer node. const L1_MESSAGES_COUNT: usize = 200; let mut l1_messages = Vec::with_capacity(L1_MESSAGES_COUNT); - let mut l1_block_info = Vec::with_capacity(L1_MESSAGES_COUNT); for i in 0..L1_MESSAGES_COUNT as u64 { let l1_message = TxL1Message { queue_index: i, @@ -303,156 +219,98 @@ async fn test_should_consolidate_after_optimistic_sync() -> eyre::Result<()> { input: Default::default(), }; l1_messages.push(l1_message); - let block_info = BlockInfo { number: i, hash: B256::random() }; - l1_block_info.push(block_info) } // Add the L1 messages to the sequencer node. - for (i, (l1_message, block_info)) in l1_messages.iter().zip(l1_block_info.iter()).enumerate() { - sequencer_l1_watcher_tx - .send(Arc::new(L1Notification::L1Message { - message: l1_message.clone(), - block_info: *block_info, - block_timestamp: i as u64 * 10, - })) - .await - .unwrap(); - wait_n_events( - &mut sequencer_events, - |e| { - matches!( - e, - rollup_node_chain_orchestrator::ChainOrchestratorEvent::L1MessageCommitted(_) - ) - }, - 1, - ) - .await; - sequencer_l1_watcher_tx - .send(Arc::new(L1Notification::NewBlock(*block_info))) - .await - .unwrap(); - wait_n_events( - &mut sequencer_events, - |e| matches!(e, ChainOrchestratorEvent::NewL1Block(_)), - 1, - ) - .await; - sequencer_handle.build_block(); - wait_n_events( - &mut sequencer_events, - |e: ChainOrchestratorEvent| matches!(e, ChainOrchestratorEvent::BlockSequenced(_)), - 1, - ) - .await; + for (i, l1_message) in l1_messages.iter().enumerate() { + sequencer + .l1() + .add_message() + .to(l1_message.to) + .queue_index(l1_message.queue_index) + .gas_limit(l1_message.gas_limit) + .sender(l1_message.sender) + .value(l1_message.value) + .input(l1_message.input.clone()) + .at_block(i as u64) + .send() + .await?; + sequencer.expect_event().l1_message_committed().await?; + + sequencer.l1().new_block(i as u64).await?; + sequencer.expect_event().new_l1_block().await?; + + sequencer.build_block().expect_block_number((i + 1) as u64).build_and_await_block().await?; } // Connect the nodes together. - sequencer.network.add_peer(follower.network.record()).await; - follower.network.next_session_established().await; - sequencer.network.next_session_established().await; + sequencer.sequencer().node.connect(&mut follower.follower(0).node).await; // trigger a new block on the sequencer node. - sequencer_handle.build_block(); + sequencer.build_block().build_and_await_block().await?; // Assert that the unsynced node triggers optimistic sync. - wait_n_events( - &mut follower_events, - |e| matches!(e, ChainOrchestratorEvent::OptimisticSync(_)), - 1, - ) - .await; + follower.expect_event().optimistic_sync().await?; // Let the unsynced node process the optimistic sync. tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; // Send all L1 messages to the unsynced node. - for (i, (l1_message, block_info)) in l1_messages.iter().zip(l1_block_info).enumerate() { - follower_l1_watcher_tx - .send(Arc::new(L1Notification::L1Message { - message: l1_message.clone(), - block_info, - block_timestamp: i as u64 * 10, - })) - .await - .unwrap(); - wait_n_events( - &mut follower_events, - |e: ChainOrchestratorEvent| { - matches!( - e, - rollup_node_chain_orchestrator::ChainOrchestratorEvent::L1MessageCommitted(_) - ) - }, - 1, - ) - .await; + for (i, l1_message) in l1_messages.iter().enumerate() { + follower + .l1() + .add_message() + .to(l1_message.to) + .queue_index(l1_message.queue_index) + .gas_limit(l1_message.gas_limit) + .sender(l1_message.sender) + .value(l1_message.value) + .input(l1_message.input.clone()) + .at_block(i as u64) + .send() + .await?; + follower.expect_event().l1_message_committed().await?; } // Send a notification to the unsynced node that the L1 watcher is synced. - follower_l1_watcher_tx.send(Arc::new(L1Notification::Synced)).await.unwrap(); + follower.l1().sync().await?; // Wait for the unsynced node to sync to the L1 watcher. - wait_n_events(&mut follower_events, |e| matches!(e, ChainOrchestratorEvent::L1Synced), 1).await; + follower.expect_event().l1_synced().await?; // Let the unsynced node process the L1 messages. tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; // build a new block on the sequencer node to trigger consolidation on the unsynced node. - sequencer_handle.build_block(); + sequencer.build_block().build_and_await_block().await?; // Assert that the unsynced node consolidates the chain. - wait_n_events( - &mut follower_events, - |e| matches!(e, ChainOrchestratorEvent::ChainExtended(_)), - 1, - ) - .await; + follower.expect_event().chain_extended((L1_MESSAGES_COUNT + 2) as u64).await?; // Now push a L1 message to the sequencer node and build a new block. - let block_info_200 = BlockInfo { number: 200, hash: B256::random() }; - let block_info_201 = BlockInfo { number: 201, hash: B256::random() }; - sequencer_l1_watcher_tx - .send(Arc::new(L1Notification::L1Message { - message: TxL1Message { - queue_index: 200, - gas_limit: 21000, - sender: Address::random(), - to: Address::random(), - value: U256::from(1), - input: Default::default(), - }, - block_info: block_info_200, - block_timestamp: 2010, - })) - .await - .unwrap(); - wait_n_events( - &mut sequencer_events, - |e: ChainOrchestratorEvent| matches!(e, ChainOrchestratorEvent::L1MessageCommitted(_)), - 1, - ) - .await; - sequencer_l1_watcher_tx.send(Arc::new(L1Notification::NewBlock(block_info_201))).await.unwrap(); - wait_n_events(&mut sequencer_events, |e| matches!(e, ChainOrchestratorEvent::NewL1Block(_)), 1) - .await; - sequencer_handle.build_block(); - - wait_n_events( - &mut follower_events, - |e| matches!(e, ChainOrchestratorEvent::NewBlockReceived(_)), - 1, - ) - .await; + sequencer + .l1() + .add_message() + .queue_index(200) + .sender(Address::random()) + .value(1) + .at_block(200) + .send() + .await?; + sequencer.expect_event().l1_message_committed().await?; + + sequencer.l1().new_block(201).await?; + sequencer.expect_event().new_l1_block().await?; + + sequencer.build_block().build_and_await_block().await?; + follower.expect_event().new_block_received().await?; // Assert that the follower node does not accept the new block as it does not have the L1 // message. - wait_n_events( - &mut follower_events, - |e| matches!(e, ChainOrchestratorEvent::L1MessageNotFoundInDatabase(_)), - 1, - ) - .await; + follower + .expect_event() + .where_event(|e| matches!(e, ChainOrchestratorEvent::L1MessageNotFoundInDatabase(_))) + .await?; Ok(()) } @@ -461,157 +319,65 @@ async fn test_should_consolidate_after_optimistic_sync() -> eyre::Result<()> { #[tokio::test] async fn test_consolidation() -> eyre::Result<()> { reth_tracing::init_test_tracing(); - let node_config = default_test_scroll_rollup_node_config(); - let sequencer_node_config = ScrollRollupNodeConfig { - test: true, - network_args: RollupNodeNetworkArgs { - enable_eth_scroll_wire_bridge: true, - enable_scroll_wire: true, - sequencer_url: None, - signer_address: None, - }, - database_args: RollupNodeDatabaseArgs { - rn_db_path: Some(PathBuf::from("sqlite::memory:")), - }, - l1_provider_args: L1ProviderArgs::default(), - engine_driver_args: EngineDriverArgs::default(), - chain_orchestrator_args: ChainOrchestratorArgs::default(), - sequencer_args: SequencerArgs { - sequencer_enabled: true, - auto_start: false, - block_time: 10, - l1_message_inclusion_mode: L1MessageInclusionMode::BlockDepth(0), - allow_empty_blocks: true, - ..SequencerArgs::default() - }, - blob_provider_args: BlobProviderArgs { mock: true, ..Default::default() }, - signer_args: Default::default(), - gas_price_oracle_args: RollupNodeGasPriceOracleArgs::default(), - consensus_args: ConsensusArgs::noop(), - database: None, - rpc_args: RpcArgs::default(), - }; - // Create the chain spec for scroll dev with Feynman activated and a test genesis. - let chain_spec = (*SCROLL_DEV).clone(); + let mut sequencer = TestFixture::builder() + .sequencer() + .with_eth_scroll_bridge(true) + .with_scroll_wire(true) + .auto_start(false) + .block_time(10) + .with_l1_message_delay(0) + .allow_empty_blocks(true) + .build() + .await?; - // Create a sequencer node and an unsynced node. - let (mut nodes, _tasks, _) = - setup_engine(sequencer_node_config, 1, chain_spec.clone(), false, false).await.unwrap(); - let mut sequencer = nodes.pop().unwrap(); - let sequencer_l1_watcher_tx = sequencer.inner.add_ons_handle.l1_watcher_tx.clone().unwrap(); - let sequencer_handle = sequencer.inner.rollup_manager_handle.clone(); - let mut sequencer_events = sequencer_handle.get_event_listener().await?; - - let (mut nodes, _tasks, _) = - setup_engine(node_config.clone(), 1, chain_spec, false, false).await.unwrap(); - let mut follower = nodes.pop().unwrap(); - let follower_l1_watcher_tx = follower.inner.add_ons_handle.l1_watcher_tx.clone().unwrap(); - let mut follower_events = follower.inner.rollup_manager_handle.get_event_listener().await?; + let mut follower = TestFixture::builder().followers(1).build().await?; // Connect the nodes together. - sequencer.network.add_peer(follower.network.record()).await; - follower.network.next_session_established().await; - sequencer.network.next_session_established().await; + sequencer.sequencer().node.connect(&mut follower.follower(0).node).await; // Create a L1 message and send it to both nodes. - let block_info_0 = BlockInfo { number: 0, hash: B256::random() }; - let l1_message = TxL1Message { - queue_index: 0, - gas_limit: 21000, - sender: Address::random(), - to: Address::random(), - value: U256::from(1), - input: Default::default(), - }; - sequencer_l1_watcher_tx - .send(Arc::new(L1Notification::L1Message { - message: l1_message.clone(), - block_info: block_info_0, - block_timestamp: 0, - })) - .await - .unwrap(); - wait_n_events( - &mut sequencer_events, - |e| matches!(e, ChainOrchestratorEvent::L1MessageCommitted(_)), - 1, - ) - .await; - sequencer_l1_watcher_tx - .send(Arc::new(L1Notification::NewBlock(BlockInfo { number: 2, hash: B256::random() }))) - .await - .unwrap(); - - follower_l1_watcher_tx - .send(Arc::new(L1Notification::L1Message { - message: l1_message, - block_info: block_info_0, - block_timestamp: 0, - })) - .await - .unwrap(); - wait_n_events( - &mut follower_events, - |e| matches!(e, ChainOrchestratorEvent::L1MessageCommitted(_)), - 1, - ) - .await; + let sender = Address::random(); + let to = Address::random(); + + sequencer.l1().add_message().sender(sender).to(to).value(1).queue_index(0).send().await?; + sequencer.expect_event().l1_message_committed().await?; + + follower.l1().add_message().sender(sender).to(to).value(1).send().await?; + follower.expect_event().l1_message_committed().await?; // Send a notification to both nodes that the L1 watcher is synced. - sequencer_l1_watcher_tx.send(Arc::new(L1Notification::Synced)).await.unwrap(); - follower_l1_watcher_tx.send(Arc::new(L1Notification::Synced)).await.unwrap(); + sequencer.l1().sync().await?; + follower.l1().sync().await?; // Assert that the unsynced node consolidates the chain. - wait_n_events( - &mut follower_events, - |e| matches!(e, ChainOrchestratorEvent::ChainConsolidated { from: 0, to: 0 }), - 1, - ) - .await; + follower.expect_event().chain_consolidated().await?; // Build a new block on the sequencer node. - sequencer_handle.build_block(); + sequencer.build_block().build_and_await_block().await?; // Now push a L1 message to the sequencer node and build a new block. - let block_info_1 = BlockInfo { number: 1, hash: B256::random() }; - sequencer_l1_watcher_tx - .send(Arc::new(L1Notification::L1Message { - message: TxL1Message { - queue_index: 1, - gas_limit: 21000, - sender: Address::random(), - to: Address::random(), - value: U256::from(1), - input: Default::default(), - }, - block_info: block_info_1, - block_timestamp: 10, - })) - .await - .unwrap(); - wait_n_events( - &mut sequencer_events, - |e| matches!(e, ChainOrchestratorEvent::L1MessageCommitted(_)), - 1, - ) - .await; - - sequencer_l1_watcher_tx - .send(Arc::new(L1Notification::NewBlock(BlockInfo { number: 5, hash: B256::random() }))) - .await - .unwrap(); - wait_n_events(&mut sequencer_events, |e| matches!(e, ChainOrchestratorEvent::NewL1Block(_)), 1) - .await; - sequencer_handle.build_block(); + sequencer + .l1() + .add_message() + .sender(Address::random()) + .to(Address::random()) + .value(1) + .queue_index(1) + .at_block(1) + .send() + .await?; + sequencer.expect_event().l1_message_committed().await?; + + sequencer.l1().new_block(5).await?; + sequencer.expect_event().new_l1_block().await?; + sequencer.build_block().build_and_await_block().await?; // Assert that the follower node rejects the new block as it hasn't received the L1 message. - wait_n_events( - &mut follower_events, - |e| matches!(e, ChainOrchestratorEvent::L1MessageNotFoundInDatabase(_)), - 1, - ) - .await; + follower + .expect_event() + .where_event(|e| matches!(e, ChainOrchestratorEvent::L1MessageNotFoundInDatabase(_))) + .await?; Ok(()) } @@ -681,97 +447,49 @@ async fn test_chain_orchestrator_fork_choice( initial_blocks: usize, reorg_block_number: Option, additional_blocks: usize, - expected_final_event_predicate: impl FnMut(ChainOrchestratorEvent) -> bool, + expected_final_event_predicate: impl Fn(&ChainOrchestratorEvent) -> bool, ) -> eyre::Result<()> { reth_tracing::init_test_tracing(); - let node_config = default_test_scroll_rollup_node_config(); - let sequencer_node_config = ScrollRollupNodeConfig { - test: true, - network_args: RollupNodeNetworkArgs { - enable_eth_scroll_wire_bridge: false, - enable_scroll_wire: true, - ..Default::default() - }, - database_args: RollupNodeDatabaseArgs { - rn_db_path: Some(PathBuf::from("sqlite::memory:")), - }, - l1_provider_args: L1ProviderArgs::default(), - engine_driver_args: EngineDriverArgs::default(), - chain_orchestrator_args: ChainOrchestratorArgs::default(), - sequencer_args: SequencerArgs { - sequencer_enabled: true, - auto_start: false, - block_time: 10, - l1_message_inclusion_mode: L1MessageInclusionMode::BlockDepth(0), - allow_empty_blocks: true, - ..SequencerArgs::default() - }, - blob_provider_args: BlobProviderArgs { mock: true, ..Default::default() }, - signer_args: Default::default(), - gas_price_oracle_args: RollupNodeGasPriceOracleArgs::default(), - consensus_args: ConsensusArgs::noop(), - database: None, - rpc_args: RpcArgs::default(), - }; - // Create the chain spec for scroll dev with Feynman activated and a test genesis. - let chain_spec = (*SCROLL_DEV).clone(); + let mut sequencer = TestFixture::builder() + .sequencer() + .with_scroll_wire(true) + .with_eth_scroll_bridge(false) + .auto_start(false) + .block_time(10) + .with_l1_message_delay(0) + .allow_empty_blocks(true) + .build() + .await?; - // Create a sequencer node and an unsynced node. - let (mut nodes, _tasks, _) = - setup_engine(sequencer_node_config.clone(), 1, chain_spec.clone(), false, false) - .await - .unwrap(); - let mut sequencer = nodes.pop().unwrap(); - let sequencer_handle = sequencer.inner.rollup_manager_handle.clone(); - let mut sequencer_events = sequencer_handle.get_event_listener().await?; - let sequencer_l1_watcher_tx = sequencer.inner.add_ons_handle.l1_watcher_tx.clone().unwrap(); - - let (mut nodes, _tasks, _) = - setup_engine(node_config.clone(), 1, chain_spec.clone(), false, false).await.unwrap(); - let mut follower = nodes.pop().unwrap(); - let mut follower_events = follower.inner.rollup_manager_handle.get_event_listener().await?; - let follower_l1_watcher_tx = follower.inner.add_ons_handle.l1_watcher_tx.clone().unwrap(); + let mut follower = TestFixture::builder().followers(1).build().await?; // Connect the nodes together. - sequencer.connect(&mut follower).await; + sequencer.sequencer().node.connect(&mut follower.follower(0).node).await; // set both the sequencer and follower L1 watchers to synced - sequencer_l1_watcher_tx.send(Arc::new(L1Notification::Synced)).await.unwrap(); - follower_l1_watcher_tx.send(Arc::new(L1Notification::Synced)).await.unwrap(); + sequencer.l1().sync().await?; + follower.l1().sync().await?; // Initially the sequencer should build 100 empty blocks in each and the follower // should follow them let mut reorg_block_info: Option = None; for i in 0..initial_blocks { - sequencer_handle.build_block(); - wait_n_events( - &mut sequencer_events, - |e| { - if let ChainOrchestratorEvent::BlockSequenced(block) = e { - if Some(i) == reorg_block_number { - reorg_block_info = Some((&block).into()); - } - true - } else { - false - } - }, - 1, - ) - .await; - wait_n_events( - &mut follower_events, - |e| matches!(e, ChainOrchestratorEvent::ChainExtended(_)), - 1, - ) - .await; + let num = (i + 1) as u64; + let block = sequencer.build_block().build_and_await_block().await?; + + if Some(i) == reorg_block_number { + reorg_block_info = Some((&block).into()); + } + + follower.expect_event().chain_extended(num).await?; } // Now reorg the sequencer and disable gossip so we can create fork - sequencer_handle.set_gossip(false).await.unwrap(); + let sequencer_handle = &sequencer.sequencer().rollup_manager_handle; + sequencer_handle.set_gossip(false).await?; if let Some(block_info) = reorg_block_info { - sequencer_handle.update_fcs_head(block_info).await.unwrap(); + sequencer_handle.update_fcs_head(block_info).await?; } // wait two seconds to ensure the timestamp of the new blocks is greater than the old ones @@ -779,21 +497,16 @@ async fn test_chain_orchestrator_fork_choice( // Have the sequencer build 20 new blocks, containing new L1 messages. for _ in 0..additional_blocks { - sequencer_handle.build_block(); - wait_n_events( - &mut sequencer_events, - |e| matches!(e, ChainOrchestratorEvent::BlockSequenced(_block)), - 1, - ) - .await; + sequencer.build_block().build_and_await_block().await?; } // now build a final block - sequencer_handle.set_gossip(true).await.unwrap(); - sequencer_handle.build_block(); + let sequencer_handle = &sequencer.sequencer().rollup_manager_handle; + sequencer_handle.set_gossip(true).await?; + sequencer.build_block().build_and_await_block().await?; // Wait for the follower node to accept the new chain - wait_n_events(&mut follower_events, expected_final_event_predicate, 1).await; + follower.expect_event().where_event(expected_final_event_predicate).await?; Ok(()) }