Skip to content
This repository was archived by the owner on Jan 16, 2026. It is now read-only.
Merged
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
1 change: 1 addition & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions bin/node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ anyhow.workspace = true
futures.workspace = true
metrics.workspace = true
tracing.workspace = true
thiserror.workspace = true
tokio-stream.workspace = true
tokio-util.workspace = true
serde_json = { workspace = true, features = ["std"] }
Expand Down
108 changes: 101 additions & 7 deletions bin/node/src/commands/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,51 @@ use strum::IntoEnumIterator;
use tracing::{debug, error, info};
use url::Url;

/// The Node subcommand.
/// A JWT token validation error.
#[derive(Debug, thiserror::Error)]
pub(super) enum JwtValidationError {
#[error("JWT signature is invalid")]
InvalidSignature,
#[error("Failed to exchange capabilities with engine: {0}")]
CapabilityExchange(String),
}

/// Command-line interface for running a Kona rollup node.
///
/// The `NodeCommand` struct defines all the configuration options needed to start and run
/// a rollup node in the Kona ecosystem. It supports multiple node modes including validator
/// and sequencer modes, and provides comprehensive networking and RPC configuration options.
///
/// # Node Modes
///
/// The node can operate in different modes:
/// - **Validator**: Validates L2 blocks and participates in consensus
/// - **Sequencer**: Sequences transactions and produces L2 blocks
///
/// # Configuration Sources
///
/// Configuration can be provided through:
/// - Command-line arguments
/// - Environment variables (prefixed with `KONA_NODE_`)
/// - Configuration files (for rollup config)
///
/// For compatibility with the [op-node], relevant flags retain an alias that matches that
/// of the [op-node] CLI.
/// # Examples
///
/// [op-node]: https://github.com/ethereum-optimism/optimism/blob/develop/op-node/flags/flags.go
/// ```bash
/// # Run as validator with default settings
/// kona node --l1-eth-rpc http://localhost:8545 \
/// --l1-beacon http://localhost:5052 \
/// --l2-engine-rpc http://localhost:8551 \
/// --l2-provider-rpc http://localhost:8545
///
/// # Run as sequencer with custom JWT secret
/// kona node --mode sequencer \
/// --l1-eth-rpc http://localhost:8545 \
/// --l1-beacon http://localhost:5052 \
/// --l2-engine-rpc http://localhost:8551 \
/// --l2-provider-rpc http://localhost:8545 \
/// --l2-jwt-secret /path/to/jwt.hex
/// ```
#[derive(Parser, PartialEq, Debug, Clone)]
#[command(about = "Runs the consensus node")]
pub struct NodeCommand {
Expand Down Expand Up @@ -141,6 +180,28 @@ impl NodeCommand {
Ok(())
}

/// Check if the error is related to JWT signature validation
fn is_jwt_signature_error(error: &(dyn std::error::Error)) -> bool {
let mut source = Some(error);
while let Some(err) = source {
let err_str = err.to_string().to_lowercase();
if err_str.contains("signature invalid") ||
(err_str.contains("jwt") && err_str.contains("invalid")) ||
err_str.contains("unauthorized") ||
err_str.contains("authentication failed")
{
return true;
}
source = err.source();
}
false
}

/// Helper to check JWT signature error from anyhow::Error (for retry condition)
fn is_jwt_signature_error_from_anyhow(error: &anyhow::Error) -> bool {
Self::is_jwt_signature_error(error.as_ref() as &dyn std::error::Error)
}

/// Validate the jwt secret if specified by exchanging capabilities with the engine.
/// Since the engine client will fail if the jwt token is invalid, this allows to ensure
/// that the jwt token passed as a cli arg is correct.
Expand All @@ -161,22 +222,23 @@ impl NodeCommand {
Ok(jwt_secret)
}
Err(e) => {
if e.to_string().contains("signature invalid") {
if Self::is_jwt_signature_error(&e) {
error!(
"Engine API JWT secret differs from the one specified by --l2.jwt-secret"
);
error!(
"Ensure that the JWT secret file specified is correct (by default it is `jwt.hex` in the current directory)"
);
return Err(JwtValidationError::InvalidSignature.into())
}
bail!("Failed to exchange capabilities with engine: {}", e);
Err(JwtValidationError::CapabilityExchange(e.to_string()).into())
}
}
};

exchange
.retry(ExponentialBuilder::default())
.when(|e| !e.to_string().contains("signature invalid"))
.when(|e| !Self::is_jwt_signature_error_from_anyhow(e))
.notify(|_, duration| {
debug!("Retrying engine capability handshake after {duration:?}");
})
Expand Down Expand Up @@ -283,6 +345,20 @@ impl NodeCommand {
#[cfg(test)]
mod tests {
use super::*;
use anyhow::anyhow;

#[derive(Debug)]
struct MockError {
message: String,
}

impl std::fmt::Display for MockError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}

impl std::error::Error for MockError {}

const fn default_flags() -> &'static [&'static str] {
&[
Expand Down Expand Up @@ -343,4 +419,22 @@ mod tests {
.unwrap_err();
assert!(err.to_string().contains("--l2-provider-rpc"));
}

#[test]
fn test_is_jwt_signature_error() {
let jwt_error = MockError { message: "signature invalid".to_string() };
assert!(NodeCommand::is_jwt_signature_error(&jwt_error));

let other_error = MockError { message: "network timeout".to_string() };
assert!(!NodeCommand::is_jwt_signature_error(&other_error));
}

#[test]
fn test_is_jwt_signature_error_from_anyhow() {
let jwt_anyhow_error = anyhow!("signature invalid");
assert!(NodeCommand::is_jwt_signature_error_from_anyhow(&jwt_anyhow_error));

let other_anyhow_error = anyhow!("network timeout");
assert!(!NodeCommand::is_jwt_signature_error_from_anyhow(&other_anyhow_error));
}
}