diff --git a/Cargo.lock b/Cargo.lock index b635e9e8d0..7bf8e17a21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4656,6 +4656,7 @@ dependencies = [ "serde_json", "strum", "tabled", + "thiserror 2.0.12", "tokio", "tokio-stream", "tokio-util", diff --git a/bin/node/Cargo.toml b/bin/node/Cargo.toml index 2bf7c98574..9f141e3b6e 100644 --- a/bin/node/Cargo.toml +++ b/bin/node/Cargo.toml @@ -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"] } diff --git a/bin/node/src/commands/node.rs b/bin/node/src/commands/node.rs index 4fc385e31a..f3b34e0cd7 100644 --- a/bin/node/src/commands/node.rs +++ b/bin/node/src/commands/node.rs @@ -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 { @@ -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. @@ -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:?}"); }) @@ -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] { &[ @@ -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)); + } }