diff --git a/crates/chain-orchestrator/src/handle/command.rs b/crates/chain-orchestrator/src/handle/command.rs index ce2d8cb9..afdf49a3 100644 --- a/crates/chain-orchestrator/src/handle/command.rs +++ b/crates/chain-orchestrator/src/handle/command.rs @@ -3,9 +3,9 @@ use crate::{ChainOrchestratorEvent, ChainOrchestratorStatus}; use reth_network_api::FullNetwork; use reth_scroll_node::ScrollNetworkPrimitives; use reth_tokio_util::EventStream; -use rollup_node_primitives::{BlockInfo, L1MessageEnvelope}; +use rollup_node_primitives::{BlockInfo, ChainImport, L1MessageEnvelope}; use scroll_db::L1MessageKey; -use scroll_network::ScrollNetworkHandle; +use scroll_network::{NewBlockWithPeer, ScrollNetworkHandle}; use tokio::sync::oneshot; /// The commands that can be sent to the rollup manager. @@ -29,6 +29,13 @@ pub enum ChainOrchestratorCommand)), + /// Import a block from a remote source. + ImportBlock { + /// The block to import with peer info + block_with_peer: NewBlockWithPeer, + /// Response channel + response: oneshot::Sender>, + }, /// Enable gossiping of blocks to peers. #[cfg(feature = "test-utils")] SetGossip((bool, oneshot::Sender<()>)), diff --git a/crates/chain-orchestrator/src/handle/mod.rs b/crates/chain-orchestrator/src/handle/mod.rs index 76737777..ab78ddea 100644 --- a/crates/chain-orchestrator/src/handle/mod.rs +++ b/crates/chain-orchestrator/src/handle/mod.rs @@ -5,9 +5,9 @@ use super::ChainOrchestratorEvent; use reth_network_api::FullNetwork; use reth_scroll_node::ScrollNetworkPrimitives; use reth_tokio_util::EventStream; -use rollup_node_primitives::{BlockInfo, L1MessageEnvelope}; +use rollup_node_primitives::{BlockInfo, ChainImport, L1MessageEnvelope}; use scroll_db::L1MessageKey; -use scroll_network::ScrollNetworkHandle; +use scroll_network::{NewBlockWithPeer, ScrollNetworkHandle}; use tokio::sync::{mpsc, oneshot}; use tracing::error; @@ -132,6 +132,16 @@ impl> ChainOrchestratorHand rx.await } + /// Import a block from a remote source. + pub async fn import_block( + &self, + block_with_peer: NewBlockWithPeer, + ) -> Result, oneshot::error::RecvError> { + let (tx, rx) = oneshot::channel(); + self.send_command(ChainOrchestratorCommand::ImportBlock { block_with_peer, response: tx }); + rx.await + } + /// Sends a command to the rollup manager to enable or disable gossiping of blocks to peers. #[cfg(feature = "test-utils")] pub async fn set_gossip(&self, enabled: bool) -> Result<(), oneshot::error::RecvError> { diff --git a/crates/chain-orchestrator/src/lib.rs b/crates/chain-orchestrator/src/lib.rs index 64d5d657..3b2f5322 100644 --- a/crates/chain-orchestrator/src/lib.rs +++ b/crates/chain-orchestrator/src/lib.rs @@ -425,6 +425,13 @@ impl< self.notify(ChainOrchestratorEvent::UnwoundToL1Block(block_number)); let _ = tx.send(true); } + ChainOrchestratorCommand::ImportBlock { block_with_peer, response } => { + let result = self + .import_chain(vec![block_with_peer.block.clone()], block_with_peer) + .await + .map_err(|e| e.to_string()); + let _ = response.send(result); + } #[cfg(feature = "test-utils")] ChainOrchestratorCommand::SetGossip((enabled, tx)) => { self.network.handle().set_gossip(enabled).await; @@ -1218,6 +1225,7 @@ impl< chain, peer_id: block_with_peer.peer_id, signature: block_with_peer.signature, + result, }) } diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml index b90e609b..73d8af48 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -19,9 +19,11 @@ async-trait.workspace = true # alloy alloy-chains.workspace = true +alloy-eips.workspace = true alloy-primitives.workspace = true alloy-provider.workspace = true alloy-rpc-client.workspace = true +alloy-rpc-types-engine.workspace = true alloy-signer-local.workspace = true alloy-signer-aws = "1.0.30" alloy-signer = "1.0.30" @@ -59,6 +61,7 @@ reth-rpc-api.workspace = true reth-rpc-eth-api.workspace = true reth-rpc-eth-types.workspace = true reth-tasks.workspace = true +reth-tokio-util.workspace = true reth-transaction-pool.workspace = true reth-trie-db.workspace = true @@ -76,16 +79,13 @@ 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 @@ -154,14 +154,11 @@ 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/add_ons/mod.rs b/crates/node/src/add_ons/mod.rs index 509e1536..cda0e7f8 100644 --- a/crates/node/src/add_ons/mod.rs +++ b/crates/node/src/add_ons/mod.rs @@ -32,6 +32,9 @@ use std::sync::Arc; mod handle; pub use handle::ScrollAddOnsHandle; +mod remote_block_source; +pub use remote_block_source::RemoteBlockSourceAddOn; + mod rpc; pub use rpc::{ RollupNodeAdminApiClient, RollupNodeAdminApiServer, RollupNodeApiClient, RollupNodeApiServer, @@ -133,6 +136,8 @@ where let (tx, rx) = tokio::sync::oneshot::channel(); let rpc_config = rollup_node_manager_addon.config().rpc_args.clone(); + let remote_block_source_config = + rollup_node_manager_addon.config().remote_block_source_args.clone(); // Register rollupNode API and rollupNodeAdmin API if enabled let rollup_node_rpc_ext = Arc::new(RollupNodeRpcExt::::new(rx)); @@ -161,6 +166,22 @@ where .map_err(|_| eyre::eyre!("failed to send rollup manager handle"))?; } + // Launch remote block source if enabled + if remote_block_source_config.enabled { + let remote_source = RemoteBlockSourceAddOn::new( + remote_block_source_config, + rollup_manager_handle.clone(), + ) + .await?; + ctx.node + .task_executor() + .spawn_critical_with_shutdown_signal("remote_block_source", |shutdown| async move { + if let Err(e) = remote_source.run_until_shutdown(shutdown).await { + tracing::error!(target: "scroll::remote_source", ?e, "Remote block source failed"); + } + }); + } + Ok(ScrollAddOnsHandle { rollup_manager_handle, rpc_handle }) } } diff --git a/crates/node/src/add_ons/remote_block_source.rs b/crates/node/src/add_ons/remote_block_source.rs new file mode 100644 index 00000000..29802aa7 --- /dev/null +++ b/crates/node/src/add_ons/remote_block_source.rs @@ -0,0 +1,183 @@ +//! Remote block source add-on for importing blocks from a remote L2 node +//! and building new blocks on top. + +use crate::args::RemoteBlockSourceArgs; +use alloy_primitives::Signature; +use alloy_provider::{Provider, ProviderBuilder}; +use alloy_rpc_client::RpcClient; +use alloy_transport::layers::RetryBackoffLayer; +use futures::StreamExt; +use reth_network_api::{FullNetwork, PeerId}; +use reth_scroll_node::ScrollNetworkPrimitives; +use reth_tasks::shutdown::Shutdown; +use reth_tokio_util::EventStream; +use rollup_node_chain_orchestrator::{ChainOrchestratorEvent, ChainOrchestratorHandle}; +use scroll_alloy_network::Scroll; +use scroll_network::NewBlockWithPeer; +use tokio::time::{interval, Duration}; + +/// Remote block source add-on that imports blocks from a trusted remote L2 node +/// and triggers block building on top of each imported block. +#[derive(Debug)] +pub struct RemoteBlockSourceAddOn +where + N: FullNetwork, +{ + /// Configuration for the remote block source. + config: RemoteBlockSourceArgs, + /// Handle to the chain orchestrator for sending commands. + handle: ChainOrchestratorHandle, + /// Tracks the last block number we imported from remote. + /// This is different from local head because we build blocks on top of imports. + last_imported_block: u64, +} + +impl RemoteBlockSourceAddOn +where + N: FullNetwork + Send + Sync + 'static, +{ + /// Creates a new remote block source add-on. + pub async fn new( + config: RemoteBlockSourceArgs, + handle: ChainOrchestratorHandle, + ) -> eyre::Result { + let last_imported_block = handle.status().await?.l2.fcs.head_block_info().number; + Ok(Self { config, handle, last_imported_block }) + } + + /// Runs the remote block source until shutdown. + pub async fn run_until_shutdown(mut self, mut shutdown: Shutdown) -> eyre::Result<()> { + let Some(url) = self.config.url.clone() else { + tracing::error!(target: "scroll::remote_source", "URL required when remote-source is enabled"); + return Err(eyre::eyre!("URL required when remote-source is enabled")); + }; + + // Build remote provider with retry layer + let retry_layer = RetryBackoffLayer::new(10, 100, 330); + let client = RpcClient::builder().layer(retry_layer).http(url); + let remote = ProviderBuilder::<_, _, Scroll>::default().connect_client(client); + + // Get event listener for waiting on block completion + let mut event_stream = match self.handle.get_event_listener().await { + Ok(stream) => stream, + Err(e) => { + tracing::error!(target: "scroll::remote_source", ?e, "Failed to get event listener"); + return Err(eyre::eyre!(e)); + } + }; + + let mut poll_interval = interval(Duration::from_millis(self.config.poll_interval_ms)); + + loop { + tokio::select! { + biased; + _guard = &mut shutdown => break, + _ = poll_interval.tick() => { + if let Err(e) = self.follow_and_build(&remote, &mut event_stream).await { + tracing::error!(target: "scroll::remote_source", ?e, "Sync error"); + } + } + } + } + + Ok(()) + } + + /// Follows the remote node and builds blocks on top of imported blocks. + async fn follow_and_build>( + &mut self, + remote: &P, + event_stream: &mut EventStream, + ) -> eyre::Result<()> { + loop { + // Get remote head + let remote_block = remote + .get_block_by_number(alloy_eips::BlockNumberOrTag::Latest) + .full() + .await? + .ok_or_else(|| eyre::eyre!("Remote block not found"))?; + + let remote_head = remote_block.header.number; + + // Compare against last imported block + if remote_head <= self.last_imported_block { + tracing::trace!(target: "scroll::remote_source", + last_imported = self.last_imported_block, + remote_head, + "Already synced with remote"); + return Ok(()); + } + + let blocks_behind = remote_head - self.last_imported_block; + tracing::info!(target: "scroll::remote_source", + last_imported = self.last_imported_block, + remote_head, + blocks_behind, + "Catching up"); + + // Fetch and import the next block from remote + let next_block_num = self.last_imported_block + 1; + let block = remote + .get_block_by_number(next_block_num.into()) + .full() + .await? + .ok_or_else(|| eyre::eyre!("Block {} not found", next_block_num))? + .into_consensus() + .map_transactions(|tx| tx.inner.into_inner()); + + // Create NewBlockWithPeer with dummy peer_id and signature (trusted source) + let block_with_peer = NewBlockWithPeer { + peer_id: PeerId::default(), + block, + signature: Signature::new(Default::default(), Default::default(), false), + }; + + // Import the block (this will cause a reorg if we had a locally built block at this + // height) + let chain_import = match self.handle.import_block(block_with_peer).await { + Ok(Ok(chain_import)) => { + self.last_imported_block = next_block_num; + chain_import + } + Ok(Err(e)) => { + return Err(eyre::eyre!("Import block failed: {}", e)); + } + Err(e) => { + return Err(eyre::eyre!("chain orchestrator command channel error: {}", e)); + } + }; + + if !chain_import.result.is_valid() { + tracing::info!(target: "scroll::remote_source", + result = ?chain_import.result, + "Imported block is not valid according to forkchoice, skipping build"); + continue; + } + + // Trigger block building on top of the imported block + self.handle.build_block(); + + // Wait for BlockSequenced event + tracing::debug!(target: "scroll::remote_source", "Waiting for block to be built..."); + loop { + match event_stream.next().await { + Some(ChainOrchestratorEvent::BlockSequenced(block)) => { + tracing::info!(target: "scroll::remote_source", + block_number = block.header.number, + block_hash = ?block.hash_slow(), + "Block built successfully, proceeding to next"); + break; + } + Some(_) => { + // Ignore other events, keep waiting + } + None => { + return Err(eyre::eyre!("Event stream ended unexpectedly")); + } + } + } + + // Loop continues to process next block + } + } +} diff --git a/crates/node/src/args.rs b/crates/node/src/args.rs index d8a95d86..9d482225 100644 --- a/crates/node/src/args.rs +++ b/crates/node/src/args.rs @@ -94,6 +94,9 @@ pub struct ScrollRollupNodeConfig { /// The pprof server arguments #[command(flatten)] pub pprof_args: PprofArgs, + /// The remote block source arguments + #[command(flatten)] + pub remote_block_source_args: RemoteBlockSourceArgs, /// The database connection (not parsed via CLI but hydrated after validation). #[arg(skip)] pub database: Option>, @@ -128,6 +131,10 @@ impl ScrollRollupNodeConfig { return Err("System contract consensus requires either an authorized signer or a L1 provider URL".to_string()); } + if self.remote_block_source_args.enabled && self.remote_block_source_args.url.is_none() { + return Err("Remote source URL required when remote source is enabled".to_string()); + } + Ok(()) } @@ -932,6 +939,26 @@ impl Default for PprofArgs { } } +/// The arguments for the remote block source. +#[derive(Debug, Default, Clone, clap::Args)] +pub struct RemoteBlockSourceArgs { + /// Enable the remote block source feature + #[arg(long = "remote-source.enabled", default_value_t = false)] + pub enabled: bool, + + /// URL for the remote L2 source node RPC + #[arg(long = "remote-source.url", id = "remote_source_url", value_name = "URL")] + pub url: Option, + + /// Polling interval in milliseconds (when already synced) + #[arg( + long = "remote-source.poll-interval-ms", + default_value_t = 100, + value_name = "POLL_INTERVAL_MS" + )] + pub poll_interval_ms: u64, +} + /// Returns the total difficulty constant for the given chain. const fn td_constant(chain: Option) -> U128 { match chain { @@ -1019,6 +1046,7 @@ mod tests { database: None, rpc_args: RpcArgs::default(), pprof_args: PprofArgs::default(), + remote_block_source_args: RemoteBlockSourceArgs::default(), }; let result = config.validate(); @@ -1028,6 +1056,37 @@ mod tests { )); } + #[test] + fn test_validate_remote_source_enabled_without_url_fails() { + let config = ScrollRollupNodeConfig { + test: false, + sequencer_args: SequencerArgs::default(), + signer_args: SignerArgs::default(), + database_args: RollupNodeDatabaseArgs::default(), + engine_driver_args: EngineDriverArgs::default(), + chain_orchestrator_args: ChainOrchestratorArgs::default(), + l1_provider_args: L1ProviderArgs::default(), + blob_provider_args: BlobProviderArgs::default(), + network_args: RollupNodeNetworkArgs::default(), + gas_price_oracle_args: RollupNodeGasPriceOracleArgs::default(), + consensus_args: ConsensusArgs::noop(), + database: None, + rpc_args: RpcArgs::default(), + pprof_args: PprofArgs::default(), + remote_block_source_args: RemoteBlockSourceArgs { + enabled: true, + url: None, + poll_interval_ms: 100, + }, + }; + + let result = config.validate(); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .contains("Remote source URL required when remote source is enabled")); + } + #[test] fn test_validate_sequencer_enabled_with_both_signers_fails() { let config = ScrollRollupNodeConfig { @@ -1052,6 +1111,7 @@ mod tests { database: None, rpc_args: RpcArgs::default(), pprof_args: PprofArgs::default(), + remote_block_source_args: RemoteBlockSourceArgs::default(), }; let result = config.validate(); @@ -1080,6 +1140,7 @@ mod tests { database: None, rpc_args: RpcArgs::default(), pprof_args: PprofArgs::default(), + remote_block_source_args: RemoteBlockSourceArgs::default(), }; assert!(config.validate().is_ok()); @@ -1106,6 +1167,7 @@ mod tests { database: None, rpc_args: RpcArgs::default(), pprof_args: PprofArgs::default(), + remote_block_source_args: RemoteBlockSourceArgs::default(), }; assert!(config.validate().is_ok()); @@ -1128,6 +1190,7 @@ mod tests { database: None, rpc_args: RpcArgs::default(), pprof_args: PprofArgs::default(), + remote_block_source_args: RemoteBlockSourceArgs::default(), }; assert!(config.validate().is_ok()); diff --git a/crates/node/src/test_utils/fixture.rs b/crates/node/src/test_utils/fixture.rs index f17618ee..55a72a11 100644 --- a/crates/node/src/test_utils/fixture.rs +++ b/crates/node/src/test_utils/fixture.rs @@ -60,13 +60,15 @@ pub type ScrollNetworkHandle = pub type TestBlockChainProvider = BlockchainProvider>; -/// The node type (sequencer or follower). +/// The node type (sequencer, follower, or remote source). #[derive(Debug)] pub enum NodeType { /// A sequencer node. Sequencer, /// A follower node. Follower, + /// A remote source node that imports blocks from a remote L2 and builds on top. + RemoteSource, } /// Handle to a single test node with its components. @@ -93,6 +95,11 @@ impl NodeHandle { pub const fn is_follower(&self) -> bool { matches!(self.typ, NodeType::Follower) } + + /// Returns true if this is a handle to a remote source node. + pub const fn is_remote_source(&self) -> bool { + matches!(self.typ, NodeType::RemoteSource) + } } impl Debug for NodeHandle { @@ -126,6 +133,14 @@ impl TestFixture { &mut self.nodes[index] } + /// Get the remote source node. + pub fn remote_source(&mut self) -> &mut NodeHandle { + self.nodes + .iter_mut() + .find(|n| matches!(n.typ, NodeType::RemoteSource)) + .expect("remote source node not found") + } + /// Get the wallet. pub fn wallet(&self) -> Arc> { self.wallet.clone() @@ -205,6 +220,7 @@ impl TestFixture { pub struct TestFixtureBuilder { config: ScrollRollupNodeConfig, num_nodes: usize, + has_remote_source_node: bool, chain_spec: Option::ChainSpec>>, is_dev: bool, no_local_transactions_propagation: bool, @@ -222,6 +238,7 @@ impl TestFixtureBuilder { Self { config: Self::default_config(), num_nodes: 0, + has_remote_source_node: false, chain_spec: None, is_dev: false, no_local_transactions_propagation: false, @@ -251,6 +268,7 @@ impl TestFixtureBuilder { consensus_args: ConsensusArgs::noop(), database: None, rpc_args: RpcArgs { basic_enabled: true, admin_enabled: true }, + remote_block_source_args: Default::default(), pprof_args: PprofArgs::default(), } } @@ -276,6 +294,13 @@ impl TestFixtureBuilder { self } + /// Adds a remote source node that follows the sequencer via `RemoteBlockSourceAddOn`. + /// Must be used together with `.sequencer()`. + pub const fn remote_source_node(mut self) -> Self { + self.has_remote_source_node = true; + self + } + /// Toggle the test field. pub const fn with_test(mut self, test: bool) -> Self { self.config.test = test; @@ -424,15 +449,45 @@ impl TestFixtureBuilder { let config = self.config; let chain_spec = self.chain_spec.unwrap_or_else(|| SCROLL_DEV.clone()); - let (nodes, _tasks, wallet) = setup_engine( + let (mut nodes, mut tasks, wallet) = setup_engine( config.clone(), self.num_nodes, chain_spec.clone(), self.is_dev, self.no_local_transactions_propagation, + None, ) .await?; + // Launch remote source node if requested + if self.has_remote_source_node { + // Get sequencer's RPC URL + let sequencer_url: reqwest::Url = + format!("http://localhost:{}", nodes[0].rpc_url().port().unwrap()).parse()?; + + // Configure remote source node + let mut remote_config = config.clone(); + remote_config.sequencer_args.sequencer_enabled = true; // needs to build blocks + remote_config.sequencer_args.auto_start = false; + remote_config.remote_block_source_args.enabled = true; + remote_config.remote_block_source_args.url = Some(sequencer_url); + // Use a fast poll interval for tests + remote_config.remote_block_source_args.poll_interval_ms = 100; + + let (mut remote_nodes, new_tasks, _) = setup_engine( + remote_config, + 1, + chain_spec.clone(), + self.is_dev, + self.no_local_transactions_propagation, + Some(tasks), + ) + .await?; + tasks = new_tasks; + + nodes.push(remote_nodes.pop().unwrap()); + } + 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(); @@ -460,6 +515,8 @@ impl TestFixtureBuilder { rollup_manager_handle, typ: if config.sequencer_args.sequencer_enabled && index == 0 { NodeType::Sequencer + } else if config.remote_block_source_args.enabled && index == node_handles.len() { + NodeType::RemoteSource } else { NodeType::Follower }, @@ -470,7 +527,7 @@ impl TestFixtureBuilder { nodes: node_handles, wallet: Arc::new(Mutex::new(wallet)), chain_spec, - _tasks, + _tasks: tasks, }) } } diff --git a/crates/node/src/test_utils/mod.rs b/crates/node/src/test_utils/mod.rs index 62e1cf04..60bf8e03 100644 --- a/crates/node/src/test_utils/mod.rs +++ b/crates/node/src/test_utils/mod.rs @@ -111,6 +111,7 @@ pub async fn setup_engine( chain_spec: Arc<::ChainSpec>, is_dev: bool, no_local_transactions_propagation: bool, + task_executor: Option, ) -> eyre::Result<( Vec< NodeHelperType< @@ -129,7 +130,7 @@ where TmpNodeAddOnsHandle: RpcHandleProvider, TmpNodeEthApi>, { - let tasks = TaskManager::current(); + let tasks = task_executor.unwrap_or_else(TaskManager::current); let exec = tasks.executor(); let network_config = NetworkArgs { @@ -240,6 +241,7 @@ pub fn default_test_scroll_rollup_node_config() -> ScrollRollupNodeConfig { consensus_args: ConsensusArgs::noop(), database: None, pprof_args: PprofArgs::default(), + remote_block_source_args: Default::default(), rpc_args: RpcArgs { basic_enabled: true, admin_enabled: true }, } } @@ -280,6 +282,7 @@ pub fn default_sequencer_test_scroll_rollup_node_config() -> ScrollRollupNodeCon gas_price_oracle_args: crate::RollupNodeGasPriceOracleArgs::default(), consensus_args: ConsensusArgs::noop(), database: None, + remote_block_source_args: Default::default(), pprof_args: PprofArgs::default(), rpc_args: RpcArgs { basic_enabled: true, admin_enabled: true }, } diff --git a/crates/node/tests/e2e.rs b/crates/node/tests/e2e.rs index 9132598a..70922a85 100644 --- a/crates/node/tests/e2e.rs +++ b/crates/node/tests/e2e.rs @@ -296,12 +296,14 @@ async fn can_forward_tx_to_sequencer() -> eyre::Result<()> { // 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, true).await.unwrap(); + setup_engine(sequencer_node_config, 1, chain_spec.clone(), false, true, None) + .await + .unwrap(); let sequencer_url = format!("http://localhost:{}", sequencer_node[0].rpc_url().port().unwrap()); follower_node_config.network_args.sequencer_url = Some(sequencer_url); let (mut follower_node, _tasks, wallet) = - setup_engine(follower_node_config, 1, chain_spec, false, true).await.unwrap(); + setup_engine(follower_node_config, 1, chain_spec, false, true, None).await.unwrap(); let wallet = Arc::new(Mutex::new(wallet)); @@ -463,9 +465,15 @@ async fn can_bridge_blocks() -> eyre::Result<()> { let chain_spec = (*SCROLL_DEV).clone(); // 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?; + let (mut nodes, tasks, _) = setup_engine( + default_test_scroll_rollup_node_config(), + 1, + chain_spec.clone(), + false, + false, + None, + ) + .await?; let mut bridge_node = nodes.pop().unwrap(); let bridge_peer_id = bridge_node.network.record().id; let bridge_node_l1_watcher_tx = @@ -564,9 +572,15 @@ async fn shutdown_consolidates_most_recent_batch_on_startup() -> eyre::Result<() 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 (mut nodes, _tasks, _) = setup_engine( + default_test_scroll_rollup_node_config(), + 1, + chain_spec.clone(), + false, + false, + None, + ) + .await?; let node = nodes.pop().unwrap(); // Instantiate the rollup node manager. @@ -845,7 +859,7 @@ async fn graceful_shutdown_sets_fcs_to_latest_signed_block_in_db_on_start_up() - // Launch a node let (mut nodes, _tasks, _) = - setup_engine(config.clone(), 1, chain_spec.clone(), false, false).await?; + setup_engine(config.clone(), 1, chain_spec.clone(), false, false, None).await?; let node = nodes.pop().unwrap(); // Instantiate the rollup node manager. diff --git a/crates/node/tests/remote_block_source.rs b/crates/node/tests/remote_block_source.rs new file mode 100644 index 00000000..eb6cf734 --- /dev/null +++ b/crates/node/tests/remote_block_source.rs @@ -0,0 +1,25 @@ +//! Integration tests for the `RemoteBlockSourceAddOn` feature. +//! +//! These tests verify that a node configured with `RemoteBlockSourceAddOn` can: +//! - Import blocks from a remote L2 node (the sequencer) +//! - Build new blocks on top of each imported block + +use rollup_node::test_utils::{EventAssertions, TestFixture}; + +#[allow(clippy::large_stack_frames)] +#[tokio::test] +async fn test_remote_block_source() -> eyre::Result<()> { + reth_tracing::init_test_tracing(); + + let mut fixture = TestFixture::builder().sequencer().remote_source_node().build().await?; + + fixture.l1().sync().await?; + + // Sequencer produces blocks 1-5 + for i in 1..=5 { + fixture.build_block().expect_block_number(i).build_and_await_block().await?; + fixture.expect_event_on(1).block_sequenced(i + 1).await?; + } + + Ok(()) +} diff --git a/crates/node/tests/sync.rs b/crates/node/tests/sync.rs index d95a0526..b2f465ef 100644 --- a/crates/node/tests/sync.rs +++ b/crates/node/tests/sync.rs @@ -77,12 +77,13 @@ async fn test_should_consolidate_to_block_15k() -> eyre::Result<()> { consensus_args: ConsensusArgs::noop(), database: None, rpc_args: RpcArgs::default(), + remote_block_source_args: Default::default(), pprof_args: PprofArgs::default(), }; let chain_spec = (*SCROLL_SEPOLIA).clone(); let (mut nodes, _tasks, _) = - setup_engine(node_config, 1, chain_spec.clone(), false, false).await?; + setup_engine(node_config, 1, chain_spec.clone(), false, false, None).await?; let node = nodes.pop().unwrap(); // We perform consolidation up to block 15k. This allows us to capture a batch revert event at @@ -546,6 +547,7 @@ async fn test_chain_orchestrator_l1_reorg() -> eyre::Result<()> { consensus_args: ConsensusArgs::noop(), database: None, rpc_args: RpcArgs::default(), + remote_block_source_args: Default::default(), pprof_args: PprofArgs::default(), }; @@ -554,7 +556,7 @@ async fn test_chain_orchestrator_l1_reorg() -> eyre::Result<()> { // Create a sequencer node and an unsynced node. let (mut nodes, _tasks, _) = - setup_engine(sequencer_node_config.clone(), 1, chain_spec.clone(), false, false) + setup_engine(sequencer_node_config.clone(), 1, chain_spec.clone(), false, false, None) .await .unwrap(); let mut sequencer = nodes.pop().unwrap(); @@ -564,7 +566,7 @@ async fn test_chain_orchestrator_l1_reorg() -> eyre::Result<()> { sequencer.inner.add_ons_handle.rollup_manager_handle.l1_watcher_mock.clone().unwrap(); let (mut nodes, _tasks, _) = - setup_engine(node_config.clone(), 1, chain_spec.clone(), false, false).await.unwrap(); + setup_engine(node_config.clone(), 1, chain_spec.clone(), false, false, None).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 = diff --git a/crates/primitives/src/chain.rs b/crates/primitives/src/chain.rs index 2568960e..8deae40d 100644 --- a/crates/primitives/src/chain.rs +++ b/crates/primitives/src/chain.rs @@ -1,4 +1,5 @@ use alloy_primitives::Signature; +use alloy_rpc_types_engine::ForkchoiceUpdated; use reth_network_peers::PeerId; use reth_scroll_primitives::ScrollBlock; use std::vec::Vec; @@ -14,11 +15,18 @@ pub struct ChainImport { pub peer_id: PeerId, /// The signature for the import of the chain tip. pub signature: Signature, + /// The result of the chain import operation. + pub result: ForkchoiceUpdated, } impl ChainImport { /// Creates a new `ChainImport` instance with the provided blocks, peer ID, and signature. - pub const fn new(blocks: Vec, peer_id: PeerId, signature: Signature) -> Self { - Self { chain: blocks, peer_id, signature } + pub const fn new( + blocks: Vec, + peer_id: PeerId, + signature: Signature, + result: ForkchoiceUpdated, + ) -> Self { + Self { chain: blocks, peer_id, signature, result } } } diff --git a/crates/sequencer/tests/e2e.rs b/crates/sequencer/tests/e2e.rs index 991d183f..c8f2943c 100644 --- a/crates/sequencer/tests/e2e.rs +++ b/crates/sequencer/tests/e2e.rs @@ -10,8 +10,8 @@ use rollup_node::{ constants::SCROLL_GAS_LIMIT, test_utils::{default_test_scroll_rollup_node_config, setup_engine}, BlobProviderArgs, ChainOrchestratorArgs, ConsensusArgs, EngineDriverArgs, L1ProviderArgs, - PprofArgs, RollupNodeDatabaseArgs, RollupNodeGasPriceOracleArgs, RollupNodeNetworkArgs, - RpcArgs, ScrollRollupNodeConfig, SequencerArgs, SignerArgs, + PprofArgs, RemoteBlockSourceArgs, RollupNodeDatabaseArgs, RollupNodeGasPriceOracleArgs, + RollupNodeNetworkArgs, RpcArgs, ScrollRollupNodeConfig, SequencerArgs, SignerArgs, }; use rollup_node_chain_orchestrator::ChainOrchestratorEvent; use rollup_node_primitives::{sig_encode_hash, BlockInfo, L1MessageEnvelope}; @@ -212,7 +212,7 @@ async fn can_build_blocks_with_delayed_l1_messages() { // setup a test node let (mut nodes, _tasks, wallet) = - setup_engine(default_test_scroll_rollup_node_config(), 1, chain_spec, false, false) + setup_engine(default_test_scroll_rollup_node_config(), 1, chain_spec, false, false, None) .await .unwrap(); @@ -337,7 +337,7 @@ async fn can_build_blocks_with_finalized_l1_messages() { let chain_spec = SCROLL_DEV.clone(); // setup a test node let (mut nodes, _tasks, wallet) = - setup_engine(default_test_scroll_rollup_node_config(), 1, chain_spec, false, false) + setup_engine(default_test_scroll_rollup_node_config(), 1, chain_spec, false, false, None) .await .unwrap(); let node = nodes.pop().unwrap(); @@ -509,10 +509,11 @@ async fn can_sequence_blocks_with_private_key_file() -> eyre::Result<()> { database: None, rpc_args: RpcArgs::default(), pprof_args: PprofArgs::default(), + remote_block_source_args: RemoteBlockSourceArgs::default(), }; let (nodes, _tasks, wallet) = - setup_engine(rollup_manager_args, 1, chain_spec, false, false).await?; + setup_engine(rollup_manager_args, 1, chain_spec, false, false, None).await?; let wallet = Arc::new(Mutex::new(wallet)); let sequencer_rnm_handle = nodes[0].inner.add_ons_handle.rollup_manager_handle.clone(); @@ -611,10 +612,11 @@ async fn can_sequence_blocks_with_hex_key_file_without_prefix() -> eyre::Result< database: None, rpc_args: RpcArgs::default(), pprof_args: PprofArgs::default(), + remote_block_source_args: RemoteBlockSourceArgs::default(), }; let (nodes, _tasks, wallet) = - setup_engine(rollup_manager_args, 1, chain_spec, false, false).await?; + setup_engine(rollup_manager_args, 1, chain_spec, false, false, None).await?; let wallet = Arc::new(Mutex::new(wallet)); let sequencer_rnm_handle = nodes[0].inner.add_ons_handle.rollup_manager_handle.clone(); @@ -684,6 +686,7 @@ async fn can_build_blocks_and_exit_at_gas_limit() { chain_spec, false, false, + None, ) .await .unwrap(); @@ -770,6 +773,7 @@ async fn can_build_blocks_and_exit_at_time_limit() { chain_spec, false, false, + None, ) .await .unwrap(); @@ -850,7 +854,7 @@ async fn should_limit_l1_message_cumulative_gas() { // setup a test node let chain_spec = SCROLL_DEV.clone(); let (mut nodes, _tasks, wallet) = - setup_engine(default_test_scroll_rollup_node_config(), 1, chain_spec, false, false) + setup_engine(default_test_scroll_rollup_node_config(), 1, chain_spec, false, false, None) .await .unwrap(); let node = nodes.pop().unwrap(); @@ -967,7 +971,7 @@ async fn should_not_add_skipped_messages() { // setup a test node let chain_spec = SCROLL_DEV.clone(); let (mut nodes, _tasks, wallet) = - setup_engine(default_test_scroll_rollup_node_config(), 1, chain_spec, false, false) + setup_engine(default_test_scroll_rollup_node_config(), 1, chain_spec, false, false, None) .await .unwrap(); let node = nodes.pop().unwrap(); diff --git a/tests/docker-compose.remote-source.yml b/tests/docker-compose.remote-source.yml new file mode 100644 index 00000000..53e5d908 --- /dev/null +++ b/tests/docker-compose.remote-source.yml @@ -0,0 +1,72 @@ +services: + l1-node: + image: ghcr.io/foundry-rs/foundry:v1.2.3 + entrypoint: ["bash", "/launch_l1.bash"] + ports: + - "8544:8545" + volumes: + - ./launch_l1.bash:/launch_l1.bash:ro + - ./anvil.env:/anvil.env:ro + - ./anvil_state.json:/anvil_state.json:ro + healthcheck: + test: ["CMD", "bash", "-c", "[ \"$(cast storage 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0 0x67 --rpc-url http://localhost:8545)\" = \"0x000000000000000000000000b674ff99cca262c99d3eab5b32796a99188543da\" ]"] + interval: 3s + timeout: 10s + retries: 30 + start_period: 0s + + rollup-node-sequencer: + build: + context: ../ + dockerfile: Dockerfile.test + entrypoint: ["bash", "/launch_rollup_node_sequencer.bash"] + environment: + - RUST_LOG=info + ports: + - "8545:8545" # JSON-RPC + - "6060:6060" # Metrics + volumes: + - ./launch_rollup_node_sequencer.bash:/launch_rollup_node_sequencer.bash:ro + - ./l2reth-genesis-e2e.json:/l2reth/l2reth-genesis-e2e.json:ro + - l2reth-sequencer:/l2reth + depends_on: + l1-node: + condition: service_healthy + + rollup-node-remote-source: + build: + context: ../ + dockerfile: Dockerfile.test + entrypoint: ["bash", "/launch_rollup_node_remote_source.bash"] + environment: + - RUST_LOG=info + ports: + - "8546:8545" # JSON-RPC + - "6061:6060" # Metrics + volumes: + - ./launch_rollup_node_remote_source.bash:/launch_rollup_node_remote_source.bash:ro + - ./l2reth-genesis-e2e.json:/l2reth/l2reth-genesis-e2e.json:ro + - l2reth-remote-source:/l2reth + depends_on: + l1-node: + condition: service_healthy + + l2geth-skipsignercheck: + image: scrolltech/l2geth:scroll-v5.10.2-skip-signer-check + platform: linux/amd64 + entrypoint: ["bash", "/launch_l2geth_skipsignercheck.bash"] + ports: + - "8547:8545" # JSON-RPC + - "6062:6060" # Metrics + volumes: + - ./l2geth-genesis-e2e.json:/l2geth-genesis-e2e.json:ro + - ./launch_l2geth_skipsignercheck.bash:/launch_l2geth_skipsignercheck.bash:ro + - l2geth-skipsignercheck:/l2geth + depends_on: + l1-node: + condition: service_healthy + +volumes: + l2reth-sequencer: + l2reth-remote-source: + l2geth-skipsignercheck: diff --git a/tests/launch_l2geth_skipsignercheck.bash b/tests/launch_l2geth_skipsignercheck.bash new file mode 100755 index 00000000..ea905af1 --- /dev/null +++ b/tests/launch_l2geth_skipsignercheck.bash @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -e + +geth init --datadir=/l2geth /l2geth-genesis-e2e.json + +echo "Starting l2geth with skip signer check..." +exec geth --datadir=/l2geth \ + --port 30303 --syncmode full --networkid 938471 --nodiscover \ + --http --http.addr 0.0.0.0 --http.port 8545 --http.vhosts "*" --http.corsdomain "*" --http.api "admin,eth,scroll,net,web3,debug" \ + --ws --ws.addr 0.0.0.0 --ws.port 8546 --ws.api "admin,eth,scroll,net,web3,debug" \ + --pprof --pprof.addr 0.0.0.0 --pprof.port 6060 --metrics --verbosity 5 --log.debug \ + --l1.endpoint "http://l1-node:8545" --l1.confirmations finalized --l1.sync.startblock 0 --l1.sync.interval 1s \ + --gcmode archive --cache.noprefetch --cache.snapshot=0 --snapshot=false \ + --gossip.enablebroadcasttoall \ + --nat extip:0.0.0.0 \ + --skipsignercheck diff --git a/tests/launch_rollup_node_remote_source.bash b/tests/launch_rollup_node_remote_source.bash new file mode 100755 index 00000000..df2ede66 --- /dev/null +++ b/tests/launch_rollup_node_remote_source.bash @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -e + +# Prepare signer key +echo -n "0xd510c4b7c61a604f800c4f06803b1ee14b9a63de345e53426ae50425f2dbb058" > /l2reth/sequencer-key + +# Prepare node key for the remote source node to have a predictable enode URL +echo -n "53b69b913cb8c64a3ae83bf0ffb1e9e9efa80d7e1924dd076a8d2b1482f1b21b" > /l2reth/nodekey +# -> enode://849431bd98c23f8203cf475cfd8efb980d1e2af46337141cfb7dd960d4ae6f8c489846da293305705c8918fbf991e41805afc8c7231c96f3f938120b6826affc@rollup-node-remote-source:30303 + +export RUST_LOG=sqlx=off,scroll=trace,reth=info,rollup=trace,info + +exec rollup-node node --chain /l2reth/l2reth-genesis-e2e.json --datadir=/l2reth --metrics=0.0.0.0:6060 \ + --disable-discovery \ + --network.valid_signer "0xb674ff99cca262c99d3eab5b32796a99188543da" \ + --http --http.addr=0.0.0.0 --http.port=8545 --http.corsdomain "*" --http.api admin,debug,eth,net,trace,txpool,web3,rpc,reth,ots,flashbots,miner,mev \ + --ws --ws.addr=0.0.0.0 --ws.port=8546 --ws.api admin,debug,eth,net,trace,txpool,web3,rpc,reth,ots,flashbots,miner,mev \ + --rpc.rollup-node-admin \ + --log.stdout.format log-fmt -vvv \ + --sequencer.enabled \ + --sequencer.allow-empty-blocks \ + --signer.key-file /l2reth/sequencer-key \ + --sequencer.block-time 500 \ + --sequencer.payload-building-duration 400 \ + --txpool.pending-max-count=1000 \ + --builder.gaslimit=30000000 \ + --rpc.max-connections=5000 \ + --p2p-secret-key /l2reth/nodekey \ + --engine.sync-at-startup false \ + --remote-source.enabled \ + --remote-source.url http://rollup-node-sequencer:8545 \ + --remote-source.poll-interval-ms 100 \ + --l1.url http://l1-node:8545 \ + --blob.mock diff --git a/tests/src/docker_compose.rs b/tests/src/docker_compose.rs index a1678f27..8df1ec21 100644 --- a/tests/src/docker_compose.rs +++ b/tests/src/docker_compose.rs @@ -43,9 +43,17 @@ const RN_FOLLOWER_RPC_URL: &str = "http://localhost:8546"; /// The l2geth node RPC URL for the Docker Compose environment. const L2GETH_SEQUENCER_RPC_URL: &str = "http://localhost:8547"; +/// The l2geth follower node RPC URL for the Docker Compose environment. const L2GETH_FOLLOWER_RPC_URL: &str = "http://localhost:8548"; -const RN_SEQUENCER_ENODE: &str= "enode://e7f7e271f62bd2b697add14e6987419758c97e83b0478bd948f5f2d271495728e7edef5bd78ad65258ac910f28e86928ead0c42ee51f2a0168d8ca23ba939766@{IP}:30303"; +/// The remote source node RPC URL for the Docker Compose environment. +const RN_REMOTE_SOURCE_RPC_URL: &str = "http://localhost:8546"; + +/// The l2geth skip signer check node RPC URL for the Docker Compose environment. +const L2GETH_SKIPSIGNERCHECK_RPC_URL: &str = "http://localhost:8547"; + +const RN_SEQUENCER_ENODE: &str = "enode://e7f7e271f62bd2b697add14e6987419758c97e83b0478bd948f5f2d271495728e7edef5bd78ad65258ac910f28e86928ead0c42ee51f2a0168d8ca23ba939766@{IP}:30303"; +const RN_REMOTE_SOURCE_ENODE: &str = "enode://849431bd98c23f8203cf475cfd8efb980d1e2af46337141cfb7dd960d4ae6f8c489846da293305705c8918fbf991e41805afc8c7231c96f3f938120b6826affc@{IP}:30303"; const L2GETH_SEQUENCER_ENODE: &str = "enode://8fc4f6dfd0a2ebf56560d0b0ef5e60ad7bcb01e13f929eae53a4c77086d9c1e74eb8b8c8945035d25c6287afdd871f0d41b3fd7e189697decd0f13538d1ac620@{IP}:30303"; pub struct DockerComposeEnv { @@ -83,6 +91,33 @@ impl DockerComposeEnv { Ok(env) } + /// Create a new DockerComposeEnv for remote block source tests and wait for all services to be + /// ready + pub async fn new_remote_source(test_name: &str) -> Result { + let project_name = format!("test-{test_name}"); + let compose_file = "docker-compose.remote-source.yml"; + + tracing::info!("🚀 Starting test environment: {project_name}"); + + // Pre-cleanup existing containers to avoid conflicts + Self::cleanup(compose_file, &project_name, false); + + // Start the environment + let env = Self::start_environment(compose_file, &project_name)?; + + // Start streaming logs in the background + let _ = Self::stream_container_logs(compose_file, &project_name).await; + + // Wait for all services to be ready + tracing::info!("⏳ Waiting for services to be ready..."); + Self::wait_for_l2_node_ready(RN_SEQUENCER_RPC_URL, 30).await?; + Self::wait_for_l2_node_ready(RN_REMOTE_SOURCE_RPC_URL, 30).await?; + Self::wait_for_l2_node_ready(L2GETH_SKIPSIGNERCHECK_RPC_URL, 30).await?; + + tracing::info!("✅ All services are ready!"); + Ok(env) + } + fn start_environment(compose_file: &str, project_name: &str) -> Result { tracing::info!("📦 Starting docker-compose services..."); @@ -188,6 +223,21 @@ impl DockerComposeEnv { )) } + /// Get a configured remote source provider + pub async fn get_rn_remote_source_provider(&self) -> Result { + let provider = ProviderBuilder::<_, _, Scroll>::default() + .with_recommended_fillers() + .connect(RN_REMOTE_SOURCE_RPC_URL) + .await + .map_err(|e| eyre::eyre!("Failed to connect to RN remote source: {}", e))?; + Ok(NamedProvider::new( + Box::new(provider), + "RN Remote Source", + "rollup-node-remote-source", + RN_REMOTE_SOURCE_RPC_URL, + )) + } + /// Get a configured l2geth sequencer provider pub async fn get_l2geth_sequencer_provider(&self) -> Result { let provider = ProviderBuilder::<_, _, Scroll>::default() @@ -218,6 +268,21 @@ impl DockerComposeEnv { )) } + /// Get a configured l2geth skip signer check provider + pub async fn get_l2geth_skipsignercheck_provider(&self) -> Result { + let provider = ProviderBuilder::<_, _, Scroll>::default() + .with_recommended_fillers() + .connect(L2GETH_SKIPSIGNERCHECK_RPC_URL) + .await + .map_err(|e| eyre::eyre!("Failed to connect to l2geth skip signer check: {}", e))?; + Ok(NamedProvider::new( + Box::new(provider), + "L2Geth SkipSignerCheck", + "l2geth-skipsignercheck", + L2GETH_SKIPSIGNERCHECK_RPC_URL, + )) + } + // ===== UTILITIES ===== /// Wait for L2 node to be ready @@ -441,6 +506,13 @@ impl DockerComposeEnv { Ok(RN_SEQUENCER_ENODE.replace("{IP}", &ip)) } + /// Get the rollup node remote source enode URL with resolved IP address + pub fn rn_remote_source_enode(&self) -> Result { + let ip = + self.get_container_ip(&self.get_full_container_name("rollup-node-remote-source"))?; + Ok(RN_REMOTE_SOURCE_ENODE.replace("{IP}", &ip)) + } + /// Get the l2geth sequencer enode URL with resolved IP address pub fn l2geth_sequencer_enode(&self) -> Result { let ip = self.get_container_ip(&self.get_full_container_name("l2geth-sequencer"))?; diff --git a/tests/tests/remote_block_source_docker.rs b/tests/tests/remote_block_source_docker.rs new file mode 100644 index 00000000..a3f64871 --- /dev/null +++ b/tests/tests/remote_block_source_docker.rs @@ -0,0 +1,68 @@ +use eyre::Result; +use tests::*; + +#[tokio::test] +async fn docker_test_remote_block_source_basic() -> Result<()> { + reth_tracing::init_test_tracing(); + + tracing::info!("=== STARTING docker_test_remote_block_source_basic ==="); + let env = DockerComposeEnv::new_remote_source("docker_test_remote_block_source_basic").await?; + + let rn_sequencer = env.get_rn_sequencer_provider().await?; + let rn_remote_source = env.get_rn_remote_source_provider().await?; + let l2geth = env.get_l2geth_skipsignercheck_provider().await?; + + utils::enable_automatic_sequencing(&rn_sequencer).await?; + + let target_block = 10; + utils::wait_for_block(&[&rn_sequencer], target_block).await?; + let sequencer_block = rn_sequencer.get_block_number().await?; + + utils::wait_for_block(&[&rn_remote_source], sequencer_block).await?; + utils::assert_blocks_match(&[&rn_sequencer, &rn_remote_source], sequencer_block).await?; + + utils::admin_add_peer(&l2geth, &env.rn_remote_source_enode()?).await?; + utils::wait_for_block(&[&l2geth], sequencer_block).await?; + utils::assert_blocks_match(&[&rn_remote_source, &l2geth], sequencer_block).await?; + + let remote_block = rn_remote_source.get_block_number().await?; + assert!( + remote_block >= sequencer_block, + "remote source should be at or ahead of sequencer: remote={}, sequencer={}", + remote_block, + sequencer_block + ); + + Ok(()) +} + +#[tokio::test] +async fn docker_test_remote_block_source_recovery() -> Result<()> { + reth_tracing::init_test_tracing(); + + tracing::info!("=== STARTING docker_test_remote_block_source_recovery ==="); + let env = + DockerComposeEnv::new_remote_source("docker_test_remote_block_source_recovery").await?; + + let rn_sequencer = env.get_rn_sequencer_provider().await?; + let rn_remote_source = env.get_rn_remote_source_provider().await?; + + utils::enable_automatic_sequencing(&rn_sequencer).await?; + + let target_block = 10; + utils::wait_for_block(&[&rn_sequencer], target_block).await?; + let sequencer_block = rn_sequencer.get_block_number().await?; + utils::wait_for_block(&[&rn_remote_source], sequencer_block).await?; + + tracing::info!("Restarting rollup-node-sequencer container"); + env.restart_container(&rn_sequencer).await?; + let rn_sequencer = env.get_rn_sequencer_provider().await?; + utils::enable_automatic_sequencing(&rn_sequencer).await?; + + let target_block = sequencer_block + 5; + utils::wait_for_block(&[&rn_sequencer], target_block).await?; + utils::wait_for_block(&[&rn_remote_source], target_block).await?; + utils::assert_blocks_match(&[&rn_sequencer, &rn_remote_source], target_block).await?; + + Ok(()) +}