diff --git a/Cargo.lock b/Cargo.lock index a924fae7..f5bc475c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10375,6 +10375,7 @@ dependencies = [ "alloy-signer-aws", "alloy-signer-local", "alloy-transport", + "async-trait", "auto_impl", "aws-config", "aws-sdk-kms", @@ -10382,6 +10383,7 @@ dependencies = [ "color-eyre", "eyre", "futures", + "jsonrpsee", "reqwest", "reth-chainspec", "reth-cli-util", diff --git a/crates/manager/src/manager/command.rs b/crates/manager/src/manager/command.rs index dea7d012..1d7d8d1b 100644 --- a/crates/manager/src/manager/command.rs +++ b/crates/manager/src/manager/command.rs @@ -20,4 +20,8 @@ pub enum RollupManagerCommand>), /// Update the head of the fcs in the engine driver. UpdateFcsHead(BlockInfo), + /// Enable automatic sequencing. + EnableAutomaticSequencing(oneshot::Sender), + /// Disable automatic sequencing. + DisableAutomaticSequencing(oneshot::Sender), } diff --git a/crates/manager/src/manager/handle.rs b/crates/manager/src/manager/handle.rs index 01063170..651ce436 100644 --- a/crates/manager/src/manager/handle.rs +++ b/crates/manager/src/manager/handle.rs @@ -52,4 +52,18 @@ impl> RollupManagerHandle Result { + let (tx, rx) = oneshot::channel(); + self.send_command(RollupManagerCommand::EnableAutomaticSequencing(tx)).await; + rx.await + } + + /// Sends a command to the rollup manager to disable automatic sequencing. + pub async fn disable_automatic_sequencing(&self) -> Result { + let (tx, rx) = oneshot::channel(); + self.send_command(RollupManagerCommand::DisableAutomaticSequencing(tx)).await; + rx.await + } } diff --git a/crates/manager/src/manager/mod.rs b/crates/manager/src/manager/mod.rs index eabe7a4f..cc405a70 100644 --- a/crates/manager/src/manager/mod.rs +++ b/crates/manager/src/manager/mod.rs @@ -38,7 +38,7 @@ use tokio::{ time::Interval, }; use tokio_stream::wrappers::ReceiverStream; -use tracing::{error, trace, warn}; +use tracing::{error, info, trace, warn}; use rollup_node_providers::{L1MessageProvider, L1Provider}; use scroll_db::{Database, DatabaseError}; @@ -112,6 +112,8 @@ pub struct RollupNodeManager< signer: Option, /// The trigger for the block building process. block_building_trigger: Option, + /// The original block time configuration for restoring automatic sequencing. + block_time_config: Option, } /// The current status of the rollup manager. @@ -145,6 +147,7 @@ impl< .field("event_sender", &self.event_sender) .field("sequencer", &self.sequencer) .field("block_building_trigger", &self.block_building_trigger) + .field("block_time_config", &self.block_time_config) .finish() } } @@ -189,6 +192,7 @@ where sequencer, signer, block_building_trigger: block_time.map(delayed_interval), + block_time_config: block_time, }; (rnm, RollupManagerHandle::new(handle_tx)) } @@ -512,6 +516,27 @@ where tx.send(network_handle.clone()) .expect("Failed to send network handle to handle"); } + RollupManagerCommand::EnableAutomaticSequencing(tx) => { + let success = if let Some(block_time) = this.block_time_config { + if this.block_building_trigger.is_none() { + this.block_building_trigger = Some(delayed_interval(block_time)); + info!(target: "scroll::node::manager", "Enabled automatic sequencing with interval {}ms", block_time); + } else { + info!(target: "scroll::node::manager", "Automatic sequencing already enabled"); + } + true + } else { + warn!(target: "scroll::node::manager", "Cannot enable automatic sequencing: sequencer and block time not configured"); + false + }; + tx.send(success).expect("Failed to send enable automatic sequencing response"); + } + RollupManagerCommand::DisableAutomaticSequencing(tx) => { + let was_enabled = this.block_building_trigger.is_some(); + this.block_building_trigger = None; + info!(target: "scroll::node::manager", "Disabled automatic sequencing (was enabled: {})", was_enabled); + tx.send(true).expect("Failed to send disable automatic sequencing response"); + } } } diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml index 9cdf45c0..217d0cc5 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -14,6 +14,9 @@ path = "src/main.rs" workspace = true [dependencies] +# async trait support +async-trait.workspace = true + # alloy alloy-chains.workspace = true alloy-primitives.workspace = true @@ -88,6 +91,7 @@ scroll-network.workspace = true auto_impl.workspace = true clap = { workspace = true, features = ["derive", "env"] } eyre.workspace = true +jsonrpsee = { version = "0.25.1", features = ["server", "client", "macros"] } reqwest.workspace = true tokio.workspace = true tracing.workspace = true diff --git a/crates/node/src/add_ons/mod.rs b/crates/node/src/add_ons/mod.rs index ab77e4c3..faef8b86 100644 --- a/crates/node/src/add_ons/mod.rs +++ b/crates/node/src/add_ons/mod.rs @@ -31,6 +31,9 @@ use scroll_wire::ScrollWireEvent; mod handle; pub use handle::ScrollAddOnsHandle; +mod rpc; +pub use rpc::{RollupNodeExtApiClient, RollupNodeExtApiServer, RollupNodeRpcExt}; + mod rollup; pub use rollup::IsDevChain; use rollup::RollupManagerAddOn; @@ -123,9 +126,21 @@ where rpc_add_ons.eth_api_builder.with_propagate_local_transactions( !ctx.config.txpool.no_local_transactions_propagation, ); + + let (tx, rx) = tokio::sync::oneshot::channel(); + let rollup_node_rpc_ext = RollupNodeRpcExt::::new(rx); + rpc_add_ons = rpc_add_ons.extend_rpc_modules(move |ctx| { + ctx.modules.merge_configured(rollup_node_rpc_ext.into_rpc())?; + Ok(()) + }); + let rpc_handle = rpc_add_ons.launch_add_ons_with(ctx.clone(), |_| Ok(())).await?; let (rollup_manager_handle, l1_watcher_tx) = rollup_node_manager_addon.launch(ctx.clone(), rpc_handle.clone()).await?; + + tx.send(rollup_manager_handle.clone()) + .map_err(|_| eyre::eyre!("failed to send rollup manager handle"))?; + Ok(ScrollAddOnsHandle { rollup_manager_handle, rpc_handle, diff --git a/crates/node/src/add_ons/rpc.rs b/crates/node/src/add_ons/rpc.rs new file mode 100644 index 00000000..b253c7fc --- /dev/null +++ b/crates/node/src/add_ons/rpc.rs @@ -0,0 +1,120 @@ +use async_trait::async_trait; +use jsonrpsee::{ + core::RpcResult, + proc_macros::rpc, + types::{error, ErrorObjectOwned}, +}; +use reth_network_api::FullNetwork; +use reth_scroll_node::ScrollNetworkPrimitives; +use rollup_node_manager::RollupManagerHandle; +use tokio::sync::{oneshot, Mutex, OnceCell}; + +/// RPC extension for rollup node management operations. +/// +/// This struct provides a custom JSON-RPC namespace (`rollupNode`) that exposes +/// rollup management functionality to RPC clients. It manages a connection to the +/// rollup manager through a handle that is initialized lazily via a oneshot channel. +#[derive(Debug)] +pub struct RollupNodeRpcExt +where + N: FullNetwork, +{ + /// Cached rollup manager handle, initialized lazily via `OnceCell` + handle: tokio::sync::OnceCell>, + /// Oneshot channel receiver for obtaining the rollup manager handle during initialization + rx: Mutex>>>, +} + +impl RollupNodeRpcExt +where + N: FullNetwork, +{ + /// Creates a new RPC extension with a receiver for the rollup manager handle. + pub fn new(rx: oneshot::Receiver>) -> Self { + Self { rx: Mutex::new(Some(rx)), handle: OnceCell::new() } + } + + /// Gets or initializes the rollup manager handle. + /// + /// This method lazily initializes the rollup manager handle by consuming the oneshot + /// receiver. Subsequent calls will return the cached handle. + async fn rollup_manager_handle(&self) -> eyre::Result<&RollupManagerHandle> { + self.handle + .get_or_try_init(|| async { + let rx = { + let mut g = self.rx.lock().await; + g.take().ok_or_else(|| eyre::eyre!("receiver already consumed"))? + }; + rx.await.map_err(|e| eyre::eyre!("failed to receive handle: {e}")) + }) + .await + } +} + +/// Defines the `rollupNode` JSON-RPC namespace for rollup management operations. +/// +/// This trait provides a custom RPC namespace that exposes rollup node management +/// functionality to external clients. The namespace is exposed as `rollupNode` and +/// provides methods for controlling automatic sequencing behavior. +/// +/// # Usage +/// These methods can be called via JSON-RPC using the `rollupNode` namespace: +/// ```json +/// {"jsonrpc": "2.0", "method": "rollupNode_enableAutomaticSequencing", "params": [], "id": 1} +/// ``` +/// or using cast: +/// ```bash +/// cast rpc rollupNode_enableAutomaticSequencing +/// ``` +#[rpc(server, client, namespace = "rollupNode")] +pub trait RollupNodeExtApi { + /// Enables automatic sequencing in the rollup node. + #[method(name = "enableAutomaticSequencing")] + async fn enable_automatic_sequencing(&self) -> RpcResult; + + /// Disables automatic sequencing in the rollup node. + #[method(name = "disableAutomaticSequencing")] + async fn disable_automatic_sequencing(&self) -> RpcResult; +} + +#[async_trait] +impl RollupNodeExtApiServer for RollupNodeRpcExt +where + N: FullNetwork, +{ + async fn enable_automatic_sequencing(&self) -> RpcResult { + let handle = self.rollup_manager_handle().await.map_err(|e| { + ErrorObjectOwned::owned( + error::INTERNAL_ERROR_CODE, + format!("Failed to get rollup manager handle: {}", e), + None::<()>, + ) + })?; + + handle.enable_automatic_sequencing().await.map_err(|e| { + ErrorObjectOwned::owned( + error::INTERNAL_ERROR_CODE, + format!("Failed to enable automatic sequencing: {}", e), + None::<()>, + ) + }) + } + + async fn disable_automatic_sequencing(&self) -> RpcResult { + let handle = self.rollup_manager_handle().await.map_err(|e| { + ErrorObjectOwned::owned( + error::INTERNAL_ERROR_CODE, + format!("Failed to get rollup manager handle: {}", e), + None::<()>, + ) + })?; + + handle.disable_automatic_sequencing().await.map_err(|e| { + ErrorObjectOwned::owned( + error::INTERNAL_ERROR_CODE, + format!("Failed to disable automatic sequencing: {}", e), + None::<()>, + ) + }) + } +} diff --git a/crates/node/src/lib.rs b/crates/node/src/lib.rs index 0a5981ca..b4abf7bc 100644 --- a/crates/node/src/lib.rs +++ b/crates/node/src/lib.rs @@ -8,6 +8,7 @@ mod node; #[cfg(feature = "test-utils")] pub mod test_utils; +pub use add_ons::*; pub use args::*; pub use context::RollupNodeContext; pub use node::ScrollRollupNode; diff --git a/crates/node/tests/e2e.rs b/crates/node/tests/e2e.rs index 974d5d03..a4b70515 100644 --- a/crates/node/tests/e2e.rs +++ b/crates/node/tests/e2e.rs @@ -26,7 +26,8 @@ use rollup_node::{ }, BeaconProviderArgs, ChainOrchestratorArgs, ConsensusAlgorithm, ConsensusArgs, DatabaseArgs, EngineDriverArgs, GasPriceOracleArgs, L1ProviderArgs, NetworkArgs as ScrollNetworkArgs, - RollupNodeContext, ScrollRollupNode, ScrollRollupNodeConfig, SequencerArgs, + RollupNodeContext, RollupNodeExtApiClient, ScrollRollupNode, ScrollRollupNodeConfig, + SequencerArgs, }; use rollup_node_chain_orchestrator::ChainOrchestratorEvent; use rollup_node_manager::{RollupManagerCommand, RollupManagerEvent}; @@ -1274,6 +1275,70 @@ async fn can_handle_l1_message_reorg() -> eyre::Result<()> { Ok(()) } +#[tokio::test] +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 + + 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 node1_rnm_handle = node1.inner.add_ons_handle.rollup_manager_handle.clone(); + let mut node1_rnm_events = node1_rnm_handle.get_event_listener().await?; + + // Create RPC client + let client0 = node0.rpc_client().expect("RPC client should be available"); + + // 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"); + + // Disable automatic sequencing via RPC + let result = RollupNodeExtApiClient::disable_automatic_sequencing(&client0).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; + tokio::time::sleep(Duration::from_millis(300)).await; + let block_num_after_wait = latest_block(&node0).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); + + // Verify manual block building still works + node0_rnm_handle.build_block().await; + wait_for_block_sequenced_5s(&mut node0_rnm_events, block_num_after_wait + 1).await?; + + // Wait for the follower to import the block + wait_for_block_imported_5s(&mut node1_rnm_events, block_num_after_wait + 1).await?; + + // Enable sequencing again + let result = RollupNodeExtApiClient::enable_automatic_sequencing(&client0).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?; + + Ok(()) +} + /// Tests that a follower node correctly rejects L2 blocks containing L1 messages it hasn't received /// yet. ///