Skip to content
This repository was archived by the owner on Jan 16, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

6 changes: 4 additions & 2 deletions bin/node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ workspace = true

[dependencies]
# workspace
kona-rpc.workspace = true
kona-rpc = { workspace = true, features = ["client"] }
kona-peers.workspace = true
kona-genesis = { workspace = true, features = ["tabled"] }
kona-protocol.workspace = true
Expand All @@ -41,6 +41,7 @@ alloy-transport-http.workspace = true
alloy-primitives.workspace = true
alloy-signer-local.workspace = true
alloy-rpc-types-engine = { workspace = true, features = ["jwt", "serde"] }
alloy-eips.workspace = true

# op-alloy
op-alloy-provider.workspace = true
Expand All @@ -62,10 +63,11 @@ metrics.workspace = true
reqwest.workspace = true
tracing.workspace = true
thiserror.workspace = true
async-trait.workspace = true
tokio-stream.workspace = true
tokio-util.workspace = true
serde_json = { workspace = true, features = ["std"] }
jsonrpsee = { workspace = true, features = ["server"] }
jsonrpsee = { workspace = true, features = ["server", "http-client"] }
clap = { workspace = true, features = ["derive", "env"] }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
backon = { workspace = true, features = ["std", "tokio", "tokio-sleep"] }
Expand Down
34 changes: 31 additions & 3 deletions bin/node/src/commands/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

use crate::{
flags::{
BuilderClientArgs, GlobalArgs, L1ClientArgs, L2ClientArgs, P2PArgs, RollupBoostFlags,
RpcArgs, SequencerArgs,
BuilderClientArgs, FollowClientArgs, GlobalArgs, L1ClientArgs, L2ClientArgs, P2PArgs,
RollupBoostFlags, RpcArgs, SequencerArgs,
},
metrics::{CliMetrics, init_rollup_config_metrics},
};
Expand All @@ -16,7 +16,9 @@ use clap::Parser;
use kona_cli::{LogConfig, MetricsArgs};
use kona_engine::{HyperAuthClient, OpEngineClient};
use kona_genesis::{L1ChainConfig, RollupConfig};
use kona_node_service::{EngineConfig, L1ConfigBuilder, NodeMode, RollupNodeBuilder};
use kona_node_service::{
EngineConfig, FollowClientConfig, L1ConfigBuilder, NodeMode, RollupNodeBuilder,
};
use kona_registry::{L1Config, scr_rollup_config_by_alloy_ident};
use op_alloy_network::Optimism;
use op_alloy_provider::ext::engine::OpEngineApi;
Expand Down Expand Up @@ -98,6 +100,10 @@ pub struct NodeCommand {
#[clap(flatten)]
pub builder_client_args: BuilderClientArgs,

/// Optional follow client for L2 CL sync status.
#[clap(flatten)]
pub follow_client_args: FollowClientArgs,

/// Path to a custom L2 rollup configuration file
/// (overrides the default rollup configuration from the registry)
#[arg(long, visible_alias = "rollup-cfg", env = "KONA_NODE_ROLLUP_CONFIG")]
Expand Down Expand Up @@ -127,6 +133,7 @@ impl Default for NodeCommand {
l1_rpc_args: L1ClientArgs::default(),
l2_client_args: L2ClientArgs::default(),
builder_client_args: BuilderClientArgs::default(),
follow_client_args: FollowClientArgs::default(),
l2_config_file: None,
l1_config_file: None,
node_mode: NodeMode::Validator,
Expand Down Expand Up @@ -311,16 +318,37 @@ impl NodeCommand {
l2_timeout: Duration::from_millis(self.l2_client_args.l2_engine_timeout),
l1_url: self.l1_rpc_args.l1_eth_rpc.clone(),
mode: self.node_mode,
follow_enabled: self.follow_client_args.l2_follow_source.is_some(),
rollup_boost: self.rollup_boost_flags.as_rollup_boost_args(),
};

// Create the follow client config if a follow source URL is provided
let follow_client_config =
self.follow_client_args.l2_follow_source.as_ref().map(|l2_follow_url| {
info!(
target: "rollup_node",
l2_follow_url = %l2_follow_url,
l1_url = %self.l1_rpc_args.l1_eth_rpc,
"Follow client config provided"
);
FollowClientConfig {
l2_url: l2_follow_url.clone(),
l1_url: self.l1_rpc_args.l1_eth_rpc.clone(),
}
});

if follow_client_config.is_none() {
debug!(target: "rollup_node", "No follow source configured");
}

RollupNodeBuilder::new(
cfg,
l1_config,
self.l2_client_args.l2_trust_rpc,
engine_config,
p2p_config,
rpc_config,
follow_client_config,
)
.with_sequencer_config(self.sequencer_flags.config())
.build()
Expand Down
2 changes: 1 addition & 1 deletion bin/node/src/flags/engine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ mod flashblocks;
pub use flashblocks::{FlashblocksFlags, FlashblocksWebsocketFlags};

mod providers;
pub use providers::{BuilderClientArgs, L1ClientArgs, L2ClientArgs};
pub use providers::{BuilderClientArgs, FollowClientArgs, L1ClientArgs, L2ClientArgs};

mod rollup_boost;
pub use rollup_boost::RollupBoostFlags;
9 changes: 9 additions & 0 deletions bin/node/src/flags/engine/providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,12 @@ impl Default for L2ClientArgs {
}
}
}

/// L2 follow client arguments.
#[derive(Clone, Debug, Default, clap::Args)]
pub struct FollowClientArgs {
/// URL of the L2 follow source RPC API.
/// The source must be the L2 CL RPC.
#[arg(long, visible_alias = "l2.follow.source", env = "KONA_NODE_L2_FOLLOW_SOURCE")]
pub l2_follow_source: Option<Url>,
}
Comment on lines +135 to +142
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// L2 follow client arguments.
#[derive(Clone, Debug, Default, clap::Args)]
pub struct FollowClientArgs {
/// URL of the L2 follow source RPC API.
/// The source must be the L2 CL RPC.
#[arg(long, visible_alias = "l2.follow.source", env = "KONA_NODE_L2_FOLLOW_SOURCE")]
pub l2_follow_source: Option<Url>,
}
/// L2 derivation delegate connection arguments.
#[derive(Clone, Debug, Default, clap::Args)]
pub struct DerivationDelegateArgs {
/// URL of the L2 derivation delegate RPC API.
/// The url must be the L2 CL RPC.
#[arg(long, visible_alias = "l2.follow.url", env = "KONA_NODE_L2_DERIVATION_DELEGATE_URL")]
pub l2_derivation_delegate_url: Option<Url>,
}

4 changes: 2 additions & 2 deletions bin/node/src/flags/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ pub use signer::{SignerArgs, SignerArgsParseError};

mod engine;
pub use engine::{
BuilderClientArgs, FlashblocksFlags, FlashblocksWebsocketFlags, L1ClientArgs, L2ClientArgs,
RollupBoostFlags,
BuilderClientArgs, FlashblocksFlags, FlashblocksWebsocketFlags, FollowClientArgs, L1ClientArgs,
L2ClientArgs, RollupBoostFlags,
};
180 changes: 180 additions & 0 deletions bin/node/src/follow.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
//! Follow client for querying sync status from an L2 CL RPC.

use alloy_eips::BlockNumberOrTag;
use alloy_provider::{Provider, RootProvider};
use async_trait::async_trait;
use jsonrpsee::http_client::{HttpClient, HttpClientBuilder};
use kona_protocol::{BlockInfo, L2BlockInfo};
use kona_rpc::RollupNodeApiClient;
use std::time::Duration;
use thiserror::Error;
use url::Url;

/// Default timeout for follow client requests in milliseconds.
const DEFAULT_FOLLOW_TIMEOUT: u64 = 5000;

/// An error that occurred in the [`FollowClient`].
#[derive(Error, Debug)]
pub enum FollowClientError {
/// An error occurred while building the HTTP client
#[error("Failed to build HTTP client: {0}")]
HttpClientBuild(String),

/// An RPC error occurred
#[error("RPC error: {0}")]
RpcError(#[from] jsonrpsee::core::ClientError),

/// An error occurred while fetching L1 block
#[error("Failed to fetch L1 block: {0}")]
L1BlockFetchError(String),

/// Block not found
#[error("Block not found")]
BlockNotFound,
}

/// Simplified follow status containing only the essential sync information.
///
/// This struct contains a subset of fields from [`kona_protocol::SyncStatus`],
/// focusing only on the fields needed for following another rollup node.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FollowStatus {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub struct FollowStatus {
pub struct DerivationStatus {

/// The current L1 block that the derivation process is last idled at.
pub current_l1: BlockInfo,
/// The safe L2 block ref, derived from the L1 chain.
pub safe_l2: L2BlockInfo,
/// The finalized L2 block ref, derived from finalized L1 information.
pub finalized_l2: L2BlockInfo,
}

/// Follow client trait for querying sync status from an L2 consensus layer RPC.
///
/// This trait defines the interface for communicating with another rollup node's
/// consensus layer to fetch its synchronization status and querying L1 blocks.
/// The main reason this trait exists is for mocking and unit testing.
#[async_trait]
pub trait FollowClient: Send + Sync {
/// Gets the synchronization status from the follow source.
///
/// Calls the `optimism_syncStatus` RPC method on the remote rollup node,
/// extracts the essential fields, and returns a simplified status.
///
/// # Returns
///
/// Returns the [`FollowStatus`] containing only the current L1, safe L2,
/// and finalized L2 blocks from the remote rollup node, or an error if
/// the RPC call fails.
async fn get_follow_status(&self) -> Result<FollowStatus, FollowClientError>;

/// Fetches the L1 [`BlockInfo`] by block number.
///
/// Queries the L1 execution layer for the block at the given number and
/// returns the block information.
///
/// # Arguments
///
/// * `number` - The L1 block number to fetch
///
/// # Returns
///
/// Returns the [`BlockInfo`] for the requested L1 block, or an error if
/// the block cannot be fetched or does not exist.
async fn l1_block_info_by_number(&self, number: u64) -> Result<BlockInfo, FollowClientError>;
Comment on lines +69 to +82
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to bundle this into FollowClient?

It makes it seem like we're fetching the L1 block from the 3rd party that we trust for derivation, when we never should be. It would probably be clearer to make FollowActor generic over Provider and inject a RootProvider into it. Otherwise, it looks like we're asking the follow_client to validate data that it gave us, rather than obviously validating it using our own sources:

async fn validate_l1_block(
&self,
number: u64,
hash: alloy_primitives::B256,
) -> Result<bool, FollowActorError> {
// Fetch the canonical block at this number from L1
let canonical_block = self
.follow_client
.l1_block_info_by_number(number)
.await
.map_err(|e| FollowActorError::L1ValidationError(e.to_string()))?;
// Compare hashes
Ok(canonical_block.hash == hash)
}
}

}
Comment on lines +50 to +83
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're not using this client for sync status (which is ambiguous), but L2 derivation status, right? Would DerivationDelegate + DerivationStatus be more accurate names? If a developer reads FollowClient, they might not have an understanding of what it does and why, whereas DerivationDelegate says exactly what we're doing (delegating derivation to a 3rd party), and it is clear that we should use this trait to get info on the status of L2 derivation.

Just commenting here, but all of the other Follow* cases would need to be updated as well.

Suggested change
/// Follow client trait for querying sync status from an L2 consensus layer RPC.
///
/// This trait defines the interface for communicating with another rollup node's
/// consensus layer to fetch its synchronization status and querying L1 blocks.
/// The main reason this trait exists is for mocking and unit testing.
#[async_trait]
pub trait FollowClient: Send + Sync {
/// Gets the synchronization status from the follow source.
///
/// Calls the `optimism_syncStatus` RPC method on the remote rollup node,
/// extracts the essential fields, and returns a simplified status.
///
/// # Returns
///
/// Returns the [`FollowStatus`] containing only the current L1, safe L2,
/// and finalized L2 blocks from the remote rollup node, or an error if
/// the RPC call fails.
async fn get_follow_status(&self) -> Result<FollowStatus, FollowClientError>;
/// Fetches the L1 [`BlockInfo`] by block number.
///
/// Queries the L1 execution layer for the block at the given number and
/// returns the block information.
///
/// # Arguments
///
/// * `number` - The L1 block number to fetch
///
/// # Returns
///
/// Returns the [`BlockInfo`] for the requested L1 block, or an error if
/// the block cannot be fetched or does not exist.
async fn l1_block_info_by_number(&self, number: u64) -> Result<BlockInfo, FollowClientError>;
}
/// Derivation delegate trait for querying derivation status from an L2 consensus layer RPC.
///
/// This trait defines the interface for communicating with another rollup node's
/// consensus layer to fetch its derivation status and querying L1 blocks.
/// The main reason this trait exists is for mocking and unit testing.
#[async_trait]
pub trait DerivationDelegate: Send + Sync {
/// Gets the derivation status from the configured delegate.
///
/// Calls the `optimism_syncStatus` RPC method on the remote rollup node,
/// extracts the essential fields, and returns a simplified status.
///
/// # Returns
///
/// Returns the [`DerivationStatus`] containing only the current L1, safe L2,
/// and finalized L2 blocks from the remote rollup node, or an error if
/// the RPC call fails.
async fn get_derivation_status(&self) -> Result<DerivationStatus, DerivationDelegateError>;
/// Fetches the L1 [`BlockInfo`] by block number.
///
/// Queries the L1 execution layer for the block at the given number and
/// returns the block information.
///
/// # Arguments
///
/// * `number` - The L1 block number to fetch
///
/// # Returns
///
/// Returns the [`BlockInfo`] for the requested L1 block, or an error if
/// the block cannot be fetched or does not exist.
async fn l1_block_info_by_number(&self, number: u64) -> Result<BlockInfo, DerivationDelegateError>;
}


/// Builder for creating a [`HttpFollowClient`].
#[derive(Debug, Clone)]
pub struct FollowClientBuilder {
/// The L2 consensus layer RPC URL.
pub l2_url: Url,
/// The L1 execution layer RPC URL.
pub l1_url: Url,
/// The timeout duration for requests.
Comment on lines +89 to +92
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: The name can indicate all info from the comment above it.

Suggested change
pub l2_url: Url,
/// The L1 execution layer RPC URL.
pub l1_url: Url,
/// The timeout duration for requests.
pub l2_cl_rpc_url: Url,
/// The L1 execution layer RPC URL.
pub l1_el_rpc_url: Url,
/// The timeout duration for requests.

pub timeout: Duration,
}

impl FollowClientBuilder {
/// Creates a new [`FollowClientBuilder`] with the given URLs.
///
/// # Arguments
///
/// * `l2_url` - The L2 consensus layer RPC endpoint URL
/// * `l1_url` - The L1 execution layer RPC endpoint URL
pub const fn new(l2_url: Url, l1_url: Url) -> Self {
Self { l2_url, l1_url, timeout: Duration::from_millis(DEFAULT_FOLLOW_TIMEOUT) }
}

/// Sets the timeout duration for requests.
///
/// # Arguments
///
/// * `timeout` - The timeout duration
pub const fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}

/// Builds the [`HttpFollowClient`].
///
/// # Returns
///
/// Returns a new `HttpFollowClient` instance or an error if the HTTP client
/// cannot be built.
pub fn build(self) -> Result<HttpFollowClient, FollowClientError> {
// Build L2 CL client for sync status queries
let l2_client = HttpClientBuilder::default()
.request_timeout(self.timeout)
.build(self.l2_url)
.map_err(|e| FollowClientError::HttpClientBuild(e.to_string()))?;

// Build L1 provider for block queries
let l1_provider = RootProvider::new_http(self.l1_url);

Ok(HttpFollowClient { l2_client, l1_provider })
}
}

/// HTTP-based follow client for querying sync status from an L2 consensus layer RPC.
///
/// The [`HttpFollowClient`] wraps JSON-RPC clients to communicate with another
/// rollup node's consensus layer (L2) and the L1 execution layer.
#[derive(Clone, Debug)]
pub struct HttpFollowClient {
/// The L2 consensus layer HTTP client for making RPC calls.
l2_client: HttpClient,
/// The L1 execution layer provider for querying blocks.
l1_provider: RootProvider,
}

#[async_trait]
impl FollowClient for HttpFollowClient {
async fn get_follow_status(&self) -> Result<FollowStatus, FollowClientError> {
// Fetch the full sync status from the remote rollup node
let sync_status = self.l2_client.op_sync_status().await?;

// Extract only the fields we need
Ok(FollowStatus {
current_l1: sync_status.current_l1,
safe_l2: sync_status.safe_l2,
finalized_l2: sync_status.finalized_l2,
})
}

async fn l1_block_info_by_number(&self, number: u64) -> Result<BlockInfo, FollowClientError> {
// Fetch the block from L1
let block = self
.l1_provider
.get_block_by_number(BlockNumberOrTag::Number(number))
.await
.map_err(|e| FollowClientError::L1BlockFetchError(e.to_string()))?
.ok_or(FollowClientError::BlockNotFound)?;

// Convert to BlockInfo
Ok(BlockInfo {
hash: block.header.hash,
number: block.header.number,
parent_hash: block.header.parent_hash,
timestamp: block.header.timestamp,
})
}
}
1 change: 1 addition & 0 deletions bin/node/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
pub mod cli;
pub mod commands;
pub mod flags;
pub mod follow;
pub mod metrics;

pub(crate) mod version;
Expand Down
4 changes: 2 additions & 2 deletions crates/node/engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ mod task_queue;
pub use task_queue::{
BuildTask, BuildTaskError, ConsolidateTask, ConsolidateTaskError, Engine, EngineBuildError,
EngineResetError, EngineTask, EngineTaskError, EngineTaskErrorSeverity, EngineTaskErrors,
EngineTaskExt, FinalizeTask, FinalizeTaskError, InsertTask, InsertTaskError, SealTask,
SealTaskError, SynchronizeTask, SynchronizeTaskError,
EngineTaskExt, FinalizeTask, FinalizeTaskError, FollowTask, FollowTaskError, InsertTask,
InsertTaskError, SealTask, SealTaskError, SynchronizeTask, SynchronizeTaskError,
};

mod attributes;
Expand Down
2 changes: 2 additions & 0 deletions crates/node/engine/src/metrics/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ impl Metrics {
pub const BUILD_TASK_LABEL: &str = "build";
/// Seal task label.
pub const SEAL_TASK_LABEL: &str = "seal";
/// Follow task label.
pub const FOLLOW_TASK_LABEL: &str = "follow";
/// Finalize task label.
pub const FINALIZE_TASK_LABEL: &str = "finalize";

Expand Down
21 changes: 21 additions & 0 deletions crates/node/engine/src/task_queue/tasks/follow/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//! Errors for the [`FollowTask`](super::FollowTask).

use crate::{EngineTaskErrorSeverity, SynchronizeTaskError};

/// An error that occurred while executing a [`FollowTask`].
///
/// [`FollowTask`]: super::FollowTask
#[derive(Debug, thiserror::Error)]
pub enum FollowTaskError {
/// An error occurred while synchronizing the engine state.
#[error(transparent)]
Synchronize(#[from] SynchronizeTaskError),
}

impl crate::EngineTaskError for FollowTaskError {
fn severity(&self) -> EngineTaskErrorSeverity {
match self {
Self::Synchronize(e) => e.severity(),
}
}
}
7 changes: 7 additions & 0 deletions crates/node/engine/src/task_queue/tasks/follow/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//! Contains the [`FollowTask`] implementation.

mod error;
pub use error::FollowTaskError;

mod task;
pub use task::FollowTask;
Loading
Loading