Skip to content
Merged
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions crates/manager/src/manager/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,8 @@ pub enum RollupManagerCommand<N: FullNetwork<Primitives = ScrollNetworkPrimitive
NetworkHandle(oneshot::Sender<ScrollNetworkHandle<N>>),
/// Update the head of the fcs in the engine driver.
UpdateFcsHead(BlockInfo),
/// Enable automatic sequencing.
EnableAutomaticSequencing(oneshot::Sender<bool>),
/// Disable automatic sequencing.
DisableAutomaticSequencing(oneshot::Sender<bool>),
}
14 changes: 14 additions & 0 deletions crates/manager/src/manager/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,18 @@ impl<N: FullNetwork<Primitives = ScrollNetworkPrimitives>> RollupManagerHandle<N
pub async fn update_fcs_head(&self, head: BlockInfo) {
self.send_command(RollupManagerCommand::UpdateFcsHead(head)).await;
}

/// Sends a command to the rollup manager to enable automatic sequencing.
pub async fn enable_automatic_sequencing(&self) -> Result<bool, oneshot::error::RecvError> {
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<bool, oneshot::error::RecvError> {
let (tx, rx) = oneshot::channel();
self.send_command(RollupManagerCommand::DisableAutomaticSequencing(tx)).await;
rx.await
}
}
27 changes: 26 additions & 1 deletion crates/manager/src/manager/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -112,6 +112,8 @@ pub struct RollupNodeManager<
signer: Option<SignerHandle>,
/// The trigger for the block building process.
block_building_trigger: Option<Interval>,
/// The original block time configuration for restoring automatic sequencing.
block_time_config: Option<u64>,
}

/// The current status of the rollup manager.
Expand Down Expand Up @@ -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()
}
}
Expand Down Expand Up @@ -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))
}
Expand Down Expand Up @@ -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");
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions crates/node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions crates/node/src/add_ons/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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::<N::Network>::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,
Expand Down
120 changes: 120 additions & 0 deletions crates/node/src/add_ons/rpc.rs
Original file line number Diff line number Diff line change
@@ -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<N>
where
N: FullNetwork<Primitives = ScrollNetworkPrimitives>,
{
/// Cached rollup manager handle, initialized lazily via `OnceCell`
handle: tokio::sync::OnceCell<RollupManagerHandle<N>>,
/// Oneshot channel receiver for obtaining the rollup manager handle during initialization
rx: Mutex<Option<oneshot::Receiver<RollupManagerHandle<N>>>>,
}

impl<N> RollupNodeRpcExt<N>
where
N: FullNetwork<Primitives = ScrollNetworkPrimitives>,
{
/// Creates a new RPC extension with a receiver for the rollup manager handle.
pub fn new(rx: oneshot::Receiver<RollupManagerHandle<N>>) -> 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<N>> {
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<bool>;

/// Disables automatic sequencing in the rollup node.
#[method(name = "disableAutomaticSequencing")]
async fn disable_automatic_sequencing(&self) -> RpcResult<bool>;
}

#[async_trait]
impl<N> RollupNodeExtApiServer for RollupNodeRpcExt<N>
where
N: FullNetwork<Primitives = ScrollNetworkPrimitives>,
{
async fn enable_automatic_sequencing(&self) -> RpcResult<bool> {
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<bool> {
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::<()>,
)
})
}
}
1 change: 1 addition & 0 deletions crates/node/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
67 changes: 66 additions & 1 deletion crates/node/tests/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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.
///
Expand Down
Loading