diff --git a/Cargo.lock b/Cargo.lock index 2d3102f0b9..9d65124fb0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4301,13 +4301,17 @@ dependencies = [ name = "kona-cli" version = "0.3.2" dependencies = [ + "alloy-chains", "alloy-primitives", "anyhow", "clap", + "kona-genesis", + "kona-registry", "libc", "libp2p", "metrics-exporter-prometheus", "metrics-process", + "rstest", "serde", "thiserror 2.0.16", "tracing", @@ -8278,6 +8282,15 @@ dependencies = [ [[package]] name = "rollup" version = "0.0.0" +dependencies = [ + "anyhow", + "clap", + "kona-cli", + "kona-genesis", + "rstest", + "vergen", + "vergen-git2", +] [[package]] name = "route-recognizer" diff --git a/bin/host/src/bin/host.rs b/bin/host/src/bin/host.rs index 8f0171d33e..a7f27ca5cf 100644 --- a/bin/host/src/bin/host.rs +++ b/bin/host/src/bin/host.rs @@ -6,7 +6,7 @@ use anyhow::Result; use clap::{Parser, Subcommand}; -use kona_cli::{LogConfig, cli_styles, log::LogArgs}; +use kona_cli::{LogArgs, LogConfig, cli_styles}; use serde::Serialize; use tracing::info; use tracing_subscriber::EnvFilter; diff --git a/bin/node/src/commands/node.rs b/bin/node/src/commands/node.rs index 316591bf93..db089d7a91 100644 --- a/bin/node/src/commands/node.rs +++ b/bin/node/src/commands/node.rs @@ -8,7 +8,7 @@ use alloy_rpc_types_engine::JwtSecret; use anyhow::{Result, bail}; use backon::{ExponentialBuilder, Retryable}; use clap::Parser; -use kona_cli::{LogConfig, metrics_args::MetricsArgs}; +use kona_cli::{LogConfig, MetricsArgs}; use kona_genesis::RollupConfig; use kona_node_service::{NodeMode, RollupNode, RollupNodeService}; use kona_registry::scr_rollup_config_by_alloy_ident; diff --git a/bin/node/src/flags/globals.rs b/bin/node/src/flags/globals.rs index 6747ab692a..d89837209c 100644 --- a/bin/node/src/flags/globals.rs +++ b/bin/node/src/flags/globals.rs @@ -2,7 +2,7 @@ use alloy_primitives::Address; use clap::Parser; -use kona_cli::{log::LogArgs, metrics_args::MetricsArgs}; +use kona_cli::{LogArgs, MetricsArgs}; use kona_genesis::RollupConfig; use kona_registry::OPCHAINS; diff --git a/bin/node/src/flags/metrics.rs b/bin/node/src/flags/metrics.rs index 33577af3b3..16b3edecfb 100644 --- a/bin/node/src/flags/metrics.rs +++ b/bin/node/src/flags/metrics.rs @@ -3,7 +3,7 @@ //! Specifies the available flags for prometheus metric configuration inside CLI use crate::metrics::VersionInfo; -use kona_cli::metrics_args::MetricsArgs; +use kona_cli::MetricsArgs; /// Initializes metrics for a Kona application, including Prometheus and node-specific metrics. /// Initialize the tracing stack and Prometheus metrics recorder. diff --git a/bin/rollup/Cargo.toml b/bin/rollup/Cargo.toml index 858de14f43..ff54a45d22 100644 --- a/bin/rollup/Cargo.toml +++ b/bin/rollup/Cargo.toml @@ -15,3 +15,18 @@ rust-version.workspace = true workspace = true [dependencies] +# Workspace +kona-genesis.workspace = true + +kona-cli = { workspace = true, features = ["secrets"] } + +# General +anyhow = { workspace = true, default-features = false } +clap = { workspace = true, features = ["derive", "env"] } + +[dev-dependencies] +rstest.workspace = true + +[build-dependencies] +vergen = { workspace = true, features = ["build", "cargo", "emit_and_set"] } +vergen-git2.workspace = true diff --git a/bin/rollup/README.md b/bin/rollup/README.md index e90557ffc9..1d570dff01 100644 --- a/bin/rollup/README.md +++ b/bin/rollup/README.md @@ -1,3 +1,28 @@ -# rollup +# Rollup -Coming soon. +Unified OP Stack rollup binary that integrates Kona services as an Execution Extension (ExEx). + +## Usage + +For example, this shows how to run `rollup` with the `kona-node` as an execution extension. + +```bash +./rollup node --l1.eth http://localhost:8545 --l1.beacon http://localhost:5052 --chain 10 +``` + +## Architecture + +- **Custom CLI**: Extends kona-node arguments with reth compatibility +- **ExEx Integration**: Embeds kona-node as a reth Execution Extension +- **Buffered Provider**: Caches L2 chain state for efficient processing +- **Event Processing**: Handles chain commits, reorgs, and reverts + +## Key Files + +- `src/main.rs` - Entry point and CLI parsing +- `src/cli.rs` - Command-line interface +- `src/exex.rs` - Kona Node ExEx implementation + +## Configuration + +Use `--kona.*` prefixed flags for kona-specific options to avoid conflicts with reth. diff --git a/bin/rollup/build.rs b/bin/rollup/build.rs new file mode 100644 index 0000000000..2d57478916 --- /dev/null +++ b/bin/rollup/build.rs @@ -0,0 +1,92 @@ +//! Derived from [`reth-node-core`][reth-build-script] +//! +//! [reth-build-script]: https://github.com/paradigmxyz/reth/blob/805fb1012cd1601c3b4fe9e8ca2d97c96f61355b/crates/node/core/build.rs + +#![allow(missing_docs)] + +use std::{env, error::Error}; +use vergen::{BuildBuilder, CargoBuilder, Emitter}; +use vergen_git2::Git2Builder; + +fn main() -> Result<(), Box> { + let mut emitter = Emitter::default(); + + let build_builder = BuildBuilder::default().build_timestamp(true).build()?; + + // Add build timestamp information. + emitter.add_instructions(&build_builder)?; + + let cargo_builder = CargoBuilder::default().features(true).target_triple(true).build()?; + + // Add cargo features and target information. + emitter.add_instructions(&cargo_builder)?; + + let git_builder = + Git2Builder::default().describe(false, true, None).dirty(true).sha(false).build()?; + + // Add commit information. + emitter.add_instructions(&git_builder)?; + + emitter.emit_and_set()?; + let sha = env::var("VERGEN_GIT_SHA")?; + let sha_short = &sha[0..7]; + + let is_dirty = env::var("VERGEN_GIT_DIRTY")? == "true"; + // > git describe --always --tags + // if not on a tag: v0.2.0-beta.3-82-g1939939b + // if on a tag: v0.2.0-beta.3 + let not_on_tag = env::var("VERGEN_GIT_DESCRIBE")?.ends_with(&format!("-g{sha_short}")); + let version_suffix = if is_dirty || not_on_tag { "-dev" } else { "" }; + println!("cargo:rustc-env=KONA_NODE_VERSION_SUFFIX={version_suffix}"); + + // Set short SHA + println!("cargo:rustc-env=VERGEN_GIT_SHA_SHORT={}", &sha[..8]); + + // Set the build profile + let out_dir = env::var("OUT_DIR").unwrap(); + let profile = out_dir.rsplit(std::path::MAIN_SEPARATOR).nth(3).unwrap(); + println!("cargo:rustc-env=KONA_NODE_BUILD_PROFILE={profile}"); + + // Set formatted version strings + let pkg_version = env!("CARGO_PKG_VERSION"); + + // The short version information for kona-node. + // - The latest version from Cargo.toml + // - The short SHA of the latest commit. + // Example: 0.1.0 (defa64b2) + println!("cargo:rustc-env=KONA_NODE_SHORT_VERSION={pkg_version}{version_suffix} ({sha_short})"); + + let features = env::var("VERGEN_CARGO_FEATURES")?; + + // LONG_VERSION + // The long version information for kona-node. + // + // - The latest version from Cargo.toml + version suffix (if any) + // - The full SHA of the latest commit + // - The build datetime + // - The build features + // - The build profile + // + // Example: + // + // ```text + // Version: 0.1.0 + // Commit SHA: defa64b2 + // Build Timestamp: 2023-05-19T01:47:19.815651705Z + // Build Features: jemalloc + // Build Profile: maxperf + // ``` + println!("cargo:rustc-env=KONA_NODE_LONG_VERSION_0=Version: {pkg_version}{version_suffix}"); + println!("cargo:rustc-env=KONA_NODE_LONG_VERSION_1=Commit SHA: {sha}"); + println!( + "cargo:rustc-env=KONA_NODE_LONG_VERSION_2=Build Timestamp: {}", + env::var("VERGEN_BUILD_TIMESTAMP")? + ); + println!( + "cargo:rustc-env=KONA_NODE_LONG_VERSION_3=Build Features: {}", + if features.is_empty() { "no features enabled".to_string() } else { features } + ); + println!("cargo:rustc-env=KONA_NODE_LONG_VERSION_4=Build Profile: {profile}"); + + Ok(()) +} diff --git a/bin/rollup/src/cli.rs b/bin/rollup/src/cli.rs new file mode 100644 index 0000000000..50aab17fc7 --- /dev/null +++ b/bin/rollup/src/cli.rs @@ -0,0 +1,29 @@ +//! Contains the rollup CLI. + +use crate::version; +use anyhow::Result; +use clap::Parser; +use kona_cli::{GlobalArgs, cli_styles}; + +/// The rollup CLI. +#[derive(Parser, Clone, Debug)] +#[command( + author, + version = version::SHORT_VERSION, + long_version = version::LONG_VERSION, + about, + styles = cli_styles(), + long_about = None +)] +pub struct Cli { + /// Global arguments for the CLI. + #[command(flatten)] + pub global: GlobalArgs, +} + +impl Cli { + /// Runs the rollup binary. + pub fn run(self) -> Result<()> { + unimplemented!("Rollup CLI is not yet implemented") + } +} diff --git a/bin/rollup/src/main.rs b/bin/rollup/src/main.rs index a0059f552e..082246c6be 100644 --- a/bin/rollup/src/main.rs +++ b/bin/rollup/src/main.rs @@ -1,13 +1,23 @@ -//! Kona's Rollup Binary. - -#![doc(issue_tracker_base_url = "https://github.com/op-rs/kona/issues/")] +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/square.png", + html_favicon_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/favicon.ico", + issue_tracker_base_url = "https://github.com/op-rs/kona/issues/" +)] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] -#![deny(missing_docs)] -#![deny(unused_must_use)] -#![deny(rust_2018_idioms)] +#![deny(missing_docs, unused_must_use, rust_2018_idioms)] + +pub mod cli; +pub mod version; -/// Main entry point for the rollup binary. -#[allow(clippy::missing_const_for_fn)] fn main() { - // TODO: Implementation coming soon + use clap::Parser; + + kona_cli::sigsegv_handler::install(); + kona_cli::backtrace::enable(); + + if let Err(err) = cli::Cli::parse().run() { + eprintln!("Error: {err:?}"); + std::process::exit(1); + } } diff --git a/bin/rollup/src/version.rs b/bin/rollup/src/version.rs new file mode 100644 index 0000000000..b868a9ed86 --- /dev/null +++ b/bin/rollup/src/version.rs @@ -0,0 +1,37 @@ +//! Version information for kona-node. + +#![allow(dead_code)] + +/// The latest version from Cargo.toml. +pub(crate) const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); + +/// The 8 character short SHA of the latest commit. +pub(crate) const VERGEN_GIT_SHA: &str = env!("VERGEN_GIT_SHA_SHORT"); + +/// The build timestamp. +pub(crate) const VERGEN_BUILD_TIMESTAMP: &str = env!("VERGEN_BUILD_TIMESTAMP"); + +/// The target triple. +pub(crate) const VERGEN_CARGO_TARGET_TRIPLE: &str = env!("VERGEN_CARGO_TARGET_TRIPLE"); + +/// The build features. +pub(crate) const VERGEN_CARGO_FEATURES: &str = env!("VERGEN_CARGO_FEATURES"); + +/// The short version information for kona-node. +pub(crate) const SHORT_VERSION: &str = env!("KONA_NODE_SHORT_VERSION"); + +/// The long version information for kona-node. +pub(crate) const LONG_VERSION: &str = concat!( + env!("KONA_NODE_LONG_VERSION_0"), + "\n", + env!("KONA_NODE_LONG_VERSION_1"), + "\n", + env!("KONA_NODE_LONG_VERSION_2"), + "\n", + env!("KONA_NODE_LONG_VERSION_3"), + "\n", + env!("KONA_NODE_LONG_VERSION_4") +); + +/// The build profile name. +pub(crate) const BUILD_PROFILE_NAME: &str = env!("KONA_NODE_BUILD_PROFILE"); diff --git a/bin/supervisor/src/cli.rs b/bin/supervisor/src/cli.rs index a14d8c9c98..326d19acb8 100644 --- a/bin/supervisor/src/cli.rs +++ b/bin/supervisor/src/cli.rs @@ -3,7 +3,7 @@ use crate::{flags::SupervisorArgs, metrics::VersionInfo}; use anyhow::Result; use clap::Parser; -use kona_cli::{LogConfig, cli_styles, log::LogArgs, metrics_args::MetricsArgs}; +use kona_cli::{LogArgs, LogConfig, MetricsArgs, cli_styles}; use kona_supervisor_service::Service; use tracing::{error, info}; diff --git a/crates/utilities/cli/Cargo.toml b/crates/utilities/cli/Cargo.toml index 6b92cc10d9..a2c1eeb9d3 100644 --- a/crates/utilities/cli/Cargo.toml +++ b/crates/utilities/cli/Cargo.toml @@ -12,6 +12,14 @@ homepage.workspace = true workspace = true [dependencies] +# Workspace +kona-genesis.workspace = true +kona-registry.workspace = true + +# Alloy +alloy-chains.workspace = true + +# General tracing.workspace = true serde = { workspace = true, features = ["derive"]} clap = { workspace = true, features = ["derive", "env"] } @@ -26,6 +34,9 @@ libp2p = { workspace = true, features = ["secp256k1"], optional = true } thiserror = { workspace = true, optional = true } alloy-primitives = { workspace = true, optional = true } +[dev-dependencies] +rstest.workspace = true + [target.'cfg(unix)'.dependencies] libc = "0.2" diff --git a/crates/utilities/cli/src/flags/globals.rs b/crates/utilities/cli/src/flags/globals.rs new file mode 100644 index 0000000000..d5d5307b2b --- /dev/null +++ b/crates/utilities/cli/src/flags/globals.rs @@ -0,0 +1,125 @@ +//! Global arguments for the CLI. + +use alloy_chains::Chain; +use alloy_primitives::Address; +use clap::Parser; +use kona_genesis::RollupConfig; +use kona_registry::OPCHAINS; + +use crate::{LogArgs, MetricsArgs, OverrideArgs}; + +/// Global arguments for the CLI. +#[derive(Parser, Default, Clone, Debug)] +pub struct GlobalArgs { + /// Logging arguments. + #[command(flatten)] + pub log_args: LogArgs, + /// The L2 chain ID to use. + #[arg( + long = "chain", + alias = "l2-chain-id", + short = 'c', + global = true, + default_value = "10", + env = "KONA_L2_CHAIN_ID", + help = "The L2 chain ID to use" + )] + pub l2_chain_id: Chain, + /// Embed the override flags globally to provide override values adjacent to the configs. + #[command(flatten)] + pub override_args: OverrideArgs, + /// Prometheus CLI arguments. + #[command(flatten)] + pub metrics: MetricsArgs, +} + +impl GlobalArgs { + /// Applies the specified overrides to the given rollup config. + /// + /// Transforms the rollup config and returns the updated config with the overrides applied. + pub fn apply_overrides(&self, config: RollupConfig) -> RollupConfig { + self.override_args.apply(config) + } + + /// Returns the signer [`Address`] from the rollup config for the given l2 chain id. + pub fn genesis_signer(&self) -> anyhow::Result
{ + let id = self.l2_chain_id; + OPCHAINS + .get(&id.id()) + .ok_or(anyhow::anyhow!("No chain config found for chain ID: {id}"))? + .roles + .as_ref() + .ok_or(anyhow::anyhow!("No roles found for chain ID: {id}"))? + .unsafe_block_signer + .ok_or(anyhow::anyhow!("No unsafe block signer found for chain ID: {id}")) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use clap::Parser; + use rstest::rstest; + + #[test] + fn test_genesis_signer() { + let args = GlobalArgs { l2_chain_id: 10.into(), ..Default::default() }; + assert_eq!( + args.genesis_signer().unwrap(), + alloy_primitives::address!("aaaa45d9549eda09e70937013520214382ffc4a2") + ); + } + + #[rstest] + #[case::numeric_optimism("10", 10)] + #[case::numeric_ethereum("1", 1)] + #[case::numeric_base("8453", 8453)] + #[case::numeric_unknown("999999", 999999)] + #[case::string_optimism("optimism", 10)] + #[case::string_mainnet("mainnet", 1)] + #[case::string_base("base", 8453)] + fn test_l2_chain_id_parse_valid(#[case] value: &str, #[case] expected_id: u64) { + let args = GlobalArgs::try_parse_from(["test", "--l2-chain-id", value]).unwrap(); + assert_eq!(args.l2_chain_id.id(), expected_id); + } + + #[rstest] + #[case::invalid_string("invalid_chain")] + fn test_l2_chain_id_parse_invalid(#[case] invalid_value: &str) { + let result = GlobalArgs::try_parse_from(["test", "--l2-chain-id", invalid_value]); + assert!(result.is_err()); + + // The error should be related to parsing + let err = result.unwrap_err(); + assert!(err.to_string().to_lowercase().contains("invalid")); + } + + #[rstest] + #[case::numeric("10", 10)] + #[case::string("optimism", 10)] + fn test_l2_chain_id_short_flag(#[case] value: &str, #[case] expected_id: u64) { + let args = GlobalArgs::try_parse_from(["test", "-c", value]).unwrap(); + assert_eq!(args.l2_chain_id.id(), expected_id); + } + + #[rstest] + #[case::numeric("10", 10)] + #[case::string("optimism", 10)] + fn test_l2_chain_id_env_var(#[case] env_value: &str, #[case] expected_id: u64) { + unsafe { + std::env::set_var("KONA_NODE_L2_CHAIN_ID", env_value); + } + let args = GlobalArgs::try_parse_from(["test"]).unwrap(); + assert_eq!(args.l2_chain_id.id(), expected_id); + unsafe { + std::env::remove_var("KONA_NODE_L2_CHAIN_ID"); + } + } + + #[test] + fn test_l2_chain_id_default() { + // Test that the default value is chain ID 10 (Optimism) + let args = GlobalArgs::try_parse_from(["test"]).unwrap(); + assert_eq!(args.l2_chain_id.id(), 10); + } +} diff --git a/crates/utilities/cli/src/flags/log.rs b/crates/utilities/cli/src/flags/log.rs new file mode 100644 index 0000000000..05ccd8d399 --- /dev/null +++ b/crates/utilities/cli/src/flags/log.rs @@ -0,0 +1,92 @@ +//! Arguments for logging. + +use std::path::PathBuf; + +use clap::{ArgAction, Args}; +use serde::{Deserialize, Serialize}; + +use crate::{LogFormat, LogRotation}; + +/// Global configuration arguments. +#[derive(Args, Debug, Default, Serialize, Deserialize, Clone)] +pub struct LogArgs { + /// Verbosity level (1-5). + /// By default, the verbosity level is set to 3 (info level). + /// + /// This verbosity level is shared by both stdout and file logging (if enabled). + #[arg( + short = 'v', + global = true, + default_value = "3", + env = "KONA_LOG_LEVEL", + action = ArgAction::Count, + )] + pub level: u8, + /// If set, no logs are printed to stdout. + #[arg( + long = "logs.stdout.quiet", + short = 'q', + global = true, + default_value = "false", + env = "KONA_STDOUT_LOG_QUIET" + )] + pub stdout_quiet: bool, + /// The format of the logs printed to stdout. One of: full, json, pretty, compact. + /// + /// full: The default rust log format. + /// json: The logs are printed in JSON structured format. + /// pretty: The logs are printed in a pretty, human readable format. + /// compact: The logs are printed in a compact format. + #[arg(long = "logs.stdout.format", default_value = "full", env = "KONA_LOG_STDOUT_FORMAT")] + pub stdout_format: LogFormat, + /// The directory to store the log files. + /// If not set, no logs are printed to files. + #[arg(long = "logs.file.directory", env = "KONA_LOG_FILE_DIRECTORY")] + pub file_directory: Option, + /// The format of the logs printed to log files. One of: full, json, pretty, compact. + /// + /// full: The default rust log format. + /// json: The logs are printed in JSON structured format. + /// pretty: The logs are printed in a pretty, human readable format. + /// compact: The logs are printed in a compact format. + #[arg(long = "logs.file.format", default_value = "full", env = "KONA_LOG_FILE_FORMAT")] + pub file_format: LogFormat, + /// The rotation of the log files. One of: hourly, daily, weekly, monthly, never. + /// If set, new log files will be created every interval. + #[arg(long = "logs.file.rotation", default_value = "never", env = "KONA_LOG_FILE_ROTATION")] + pub file_rotation: LogRotation, +} + +#[cfg(test)] +mod tests { + use super::*; + use clap::Parser; + + // Helper struct to parse GlobalArgs within a test CLI structure + #[derive(Parser, Debug)] + struct TestCli { + #[command(flatten)] + global: LogArgs, + } + + #[test] + fn test_default_verbosity_level() { + let cli = TestCli::parse_from(["test_app"]); + assert_eq!( + cli.global.level, 3, + "Default verbosity should be 3 when no -v flag is present." + ); + } + + #[test] + fn test_verbosity_count() { + let cli_v1 = TestCli::parse_from(["test_app", "-v"]); + assert_eq!(cli_v1.global.level, 1, "Verbosity with a single -v should be 1."); + + let cli_v3 = TestCli::parse_from(["test_app", "-vvv"]); + assert_eq!(cli_v3.global.level, 3, "Verbosity with -vvv should be 3."); + + let cli_v5 = TestCli::parse_from(["test_app", "-vvvvv"]); + assert_eq!(cli_v5.global.level, 5, "Verbosity with -vvvvv should be 5."); + } +} diff --git a/crates/utilities/cli/src/metrics_args.rs b/crates/utilities/cli/src/flags/metrics.rs similarity index 100% rename from crates/utilities/cli/src/metrics_args.rs rename to crates/utilities/cli/src/flags/metrics.rs diff --git a/crates/utilities/cli/src/flags/mod.rs b/crates/utilities/cli/src/flags/mod.rs new file mode 100644 index 0000000000..e4d2723781 --- /dev/null +++ b/crates/utilities/cli/src/flags/mod.rs @@ -0,0 +1,15 @@ +//! Common CLI Flags +//! +//! These are cli flags that are shared across binaries to standardize kona's services CLI UX. + +mod globals; +pub use globals::GlobalArgs; + +mod overrides; +pub use overrides::OverrideArgs; + +mod log; +pub use log::LogArgs; + +mod metrics; +pub use metrics::MetricsArgs; diff --git a/crates/utilities/cli/src/flags/overrides.rs b/crates/utilities/cli/src/flags/overrides.rs new file mode 100644 index 0000000000..dd804fa691 --- /dev/null +++ b/crates/utilities/cli/src/flags/overrides.rs @@ -0,0 +1,168 @@ +//! Flags that allow overriding derived values. + +use clap::Parser; +use kona_genesis::RollupConfig; + +/// Override Flags. +#[derive(Parser, Debug, Clone, Copy, PartialEq, Eq)] +pub struct OverrideArgs { + /// Manually specify the timestamp for the Canyon fork, overriding the bundled setting. + #[arg(long, env = "KONA_OVERRIDE_CANYON")] + pub canyon_override: Option, + /// Manually specify the timestamp for the Delta fork, overriding the bundled setting. + #[arg(long, env = "KONA_OVERRIDE_DELTA")] + pub delta_override: Option, + /// Manually specify the timestamp for the Ecotone fork, overriding the bundled setting. + #[arg(long, env = "KONA_OVERRIDE_ECOTONE")] + pub ecotone_override: Option, + /// Manually specify the timestamp for the Fjord fork, overriding the bundled setting. + #[arg(long, env = "KONA_OVERRIDE_FJORD")] + pub fjord_override: Option, + /// Manually specify the timestamp for the Granite fork, overriding the bundled setting. + #[arg(long, env = "KONA_OVERRIDE_GRANITE")] + pub granite_override: Option, + /// Manually specify the timestamp for the Holocene fork, overriding the bundled setting. + #[arg(long, env = "KONA_OVERRIDE_HOLOCENE")] + pub holocene_override: Option, + /// Manually specify the timestamp for the Isthmus fork, overriding the bundled setting. + #[arg(long, env = "KONA_OVERRIDE_ISTHMUS")] + pub isthmus_override: Option, + /// Manually specify the timestamp for the Jovian fork, overriding the bundled setting. + #[arg(long, env = "KONA_OVERRIDE_JOVIAN")] + pub jovian_override: Option, + /// Manually specify the timestamp for the pectra blob schedule, overriding the bundled + /// setting. + #[arg(long, env = "KONA_OVERRIDE_PECTRA_BLOB_SCHEDULE")] + pub pectra_blob_schedule_override: Option, + /// Manually specify the timestamp for the Interop fork, overriding the bundled setting. + #[arg(long, env = "KONA_OVERRIDE_INTEROP")] + pub interop_override: Option, +} + +impl Default for OverrideArgs { + fn default() -> Self { + // Construct default values using the clap parser. + // This works since none of the cli flags are required. + Self::parse_from::<[_; 0], &str>([]) + } +} + +impl OverrideArgs { + /// Applies the override args to the given rollup config. + pub fn apply(&self, config: RollupConfig) -> RollupConfig { + let hardforks = kona_genesis::HardForkConfig { + regolith_time: config.hardforks.regolith_time, + canyon_time: self.canyon_override.map(Some).unwrap_or(config.hardforks.canyon_time), + delta_time: self.delta_override.map(Some).unwrap_or(config.hardforks.delta_time), + ecotone_time: self.ecotone_override.map(Some).unwrap_or(config.hardforks.ecotone_time), + fjord_time: self.fjord_override.map(Some).unwrap_or(config.hardforks.fjord_time), + granite_time: self.granite_override.map(Some).unwrap_or(config.hardforks.granite_time), + holocene_time: self + .holocene_override + .map(Some) + .unwrap_or(config.hardforks.holocene_time), + pectra_blob_schedule_time: self + .pectra_blob_schedule_override + .map(Some) + .unwrap_or(config.hardforks.pectra_blob_schedule_time), + isthmus_time: self.isthmus_override.map(Some).unwrap_or(config.hardforks.isthmus_time), + jovian_time: self.jovian_override.map(Some).unwrap_or(config.hardforks.jovian_time), + interop_time: self.interop_override.map(Some).unwrap_or(config.hardforks.interop_time), + }; + RollupConfig { hardforks, ..config } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A mock command that uses the override args. + #[derive(Parser, Debug, Clone)] + #[command(about = "Mock command")] + struct MockCommand { + /// Override flags. + #[clap(flatten)] + pub override_flags: OverrideArgs, + } + + #[test] + fn test_apply_overrides() { + let args = MockCommand::parse_from([ + "test", + "--canyon-override", + "1699981200", + "--delta-override", + "1703203200", + "--ecotone-override", + "1708534800", + "--fjord-override", + "1716998400", + "--granite-override", + "1723478400", + "--holocene-override", + "1732633200", + "--pectra-blob-schedule-override", + "1745000000", + "--isthmus-override", + "1740000000", + "--jovian-override", + "1745000001", + "--interop-override", + "1750000000", + ]); + let config = RollupConfig::default(); + let updated_config = args.override_flags.apply(config); + assert_eq!( + updated_config.hardforks, + kona_genesis::HardForkConfig { + regolith_time: Default::default(), + canyon_time: Some(1699981200), + delta_time: Some(1703203200), + ecotone_time: Some(1708534800), + fjord_time: Some(1716998400), + granite_time: Some(1723478400), + holocene_time: Some(1732633200), + pectra_blob_schedule_time: Some(1745000000), + isthmus_time: Some(1740000000), + jovian_time: Some(1745000001), + interop_time: Some(1750000000), + } + ); + } + + #[test] + fn test_apply_default_overrides() { + // Use OP Mainnet rollup config. + let config = kona_registry::ROLLUP_CONFIGS + .get(&10) + .expect("No config found for chain ID 10") + .clone(); + let init_forks = config.hardforks; + let args = MockCommand::parse_from(["test"]); + let updated_config = args.override_flags.apply(config); + assert_eq!(updated_config.hardforks, init_forks); + } + + #[test] + fn test_default_override_flags() { + let args = MockCommand::parse_from(["test"]); + assert_eq!( + args.override_flags, + OverrideArgs { + canyon_override: None, + delta_override: None, + ecotone_override: None, + fjord_override: None, + granite_override: None, + holocene_override: None, + pectra_blob_schedule_override: None, + isthmus_override: None, + jovian_override: None, + interop_override: None, + } + ); + // Sanity check that the default impl matches the expected default values. + assert_eq!(args.override_flags, OverrideArgs::default()); + } +} diff --git a/crates/utilities/cli/src/lib.rs b/crates/utilities/cli/src/lib.rs index 6b071c0022..ec20e50abc 100644 --- a/crates/utilities/cli/src/lib.rs +++ b/crates/utilities/cli/src/lib.rs @@ -5,6 +5,12 @@ )] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +mod flags; +pub use flags::{GlobalArgs, LogArgs, MetricsArgs, OverrideArgs}; + +mod logs; +pub use logs::{FileLogConfig, LogConfig, LogRotation, StdoutLogConfig}; + mod clap; pub use clap::cli_styles; @@ -15,9 +21,6 @@ pub use secrets::{KeypairError, ParseKeyError, SecretKeyLoader}; pub mod backtrace; -pub mod log; -pub use log::LogConfig; - mod tracing; pub use tracing::{LogFormat, init_test_tracing}; @@ -25,5 +28,3 @@ mod prometheus; pub use prometheus::init_prometheus_server; pub mod sigsegv_handler; - -pub mod metrics_args; diff --git a/crates/utilities/cli/src/log.rs b/crates/utilities/cli/src/log.rs deleted file mode 100644 index 01ec19f20c..0000000000 --- a/crates/utilities/cli/src/log.rs +++ /dev/null @@ -1,189 +0,0 @@ -//! Arguments for logging. - -use std::path::PathBuf; - -use clap::{ArgAction, Args, ValueEnum}; -use serde::{Deserialize, Serialize}; -use tracing::level_filters::LevelFilter; - -use crate::tracing::LogFormat; - -/// Global configuration arguments. -#[derive(Args, Debug, Default, Serialize, Deserialize, Clone)] -pub struct LogArgs { - /// Verbosity level (1-5). - /// By default, the verbosity level is set to 3 (info level). - /// - /// This verbosity level is shared by both stdout and file logging (if enabled). - #[arg( - short = 'v', - global = true, - default_value = "3", - env = "KONA_NODE_LOG_LEVEL", - action = ArgAction::Count, - )] - pub level: u8, - /// If set, no logs are printed to stdout. - #[arg( - long = "logs.stdout.quiet", - short = 'q', - global = true, - default_value = "false", - env = "KONA_NODE_STDOUT_LOG_QUIET" - )] - pub stdout_quiet: bool, - /// The format of the logs printed to stdout. One of: full, json, pretty, compact. - /// - /// full: The default rust log format. - /// json: The logs are printed in JSON structured format. - /// pretty: The logs are printed in a pretty, human readable format. - /// compact: The logs are printed in a compact format. - #[arg( - long = "logs.stdout.format", - default_value = "full", - env = "KONA_NODE_LOG_STDOUT_FORMAT" - )] - pub stdout_format: LogFormat, - /// The directory to store the log files. - /// If not set, no logs are printed to files. - #[arg(long = "logs.file.directory", env = "KONA_NODE_LOG_FILE_DIRECTORY")] - pub file_directory: Option, - /// The format of the logs printed to log files. One of: full, json, pretty, compact. - /// - /// full: The default rust log format. - /// json: The logs are printed in JSON structured format. - /// pretty: The logs are printed in a pretty, human readable format. - /// compact: The logs are printed in a compact format. - #[arg(long = "logs.file.format", default_value = "full", env = "KONA_NODE_LOG_FILE_FORMAT")] - pub file_format: LogFormat, - /// The rotation of the log files. One of: hourly, daily, weekly, monthly, never. - /// If set, new log files will be created every interval. - #[arg( - long = "logs.file.rotation", - default_value = "never", - env = "KONA_NODE_LOG_FILE_ROTATION" - )] - pub file_rotation: LogRotation, -} - -/// The rotation of the log files. -#[derive(Debug, Clone, Serialize, Deserialize, ValueEnum, Default)] -#[serde(rename_all = "lowercase")] -pub enum LogRotation { - /// Rotate the log files every minute. - Minutely, - /// Rotate the log files hourly. - Hourly, - /// Rotate the log files daily. - Daily, - /// Do not rotate the log files. - #[default] - Never, -} - -/// Configuration for file logging. -#[derive(Debug, Clone)] -pub struct FileLogConfig { - /// The path to the directory where the log files are stored. - pub directory_path: PathBuf, - /// The format of the logs printed to the log file. - pub format: LogFormat, - /// The rotation of the log files. - pub rotation: LogRotation, -} - -/// Configuration for stdout logging. -#[derive(Debug, Clone)] -pub struct StdoutLogConfig { - /// The format of the logs printed to stdout. - pub format: LogFormat, -} - -/// Global configuration for logging. -/// Default is to only print logs to stdout in full format. -#[derive(Debug, Clone)] -pub struct LogConfig { - /// Global verbosity level for logging. - pub global_level: LevelFilter, - /// The configuration for stdout logging. - pub stdout_logs: Option, - /// The configuration for file logging. - pub file_logs: Option, -} - -impl Default for LogConfig { - fn default() -> Self { - Self { - global_level: LevelFilter::INFO, - stdout_logs: Some(StdoutLogConfig { format: LogFormat::Full }), - file_logs: None, - } - } -} - -impl From for LogConfig { - fn from(args: LogArgs) -> Self { - Self::new(args) - } -} - -impl LogConfig { - /// Creates a new `LogConfig` from `LogArgs`. - pub fn new(args: LogArgs) -> Self { - let level = match args.level { - 1 => LevelFilter::ERROR, - 2 => LevelFilter::WARN, - 3 => LevelFilter::INFO, - 4 => LevelFilter::DEBUG, - _ => LevelFilter::TRACE, - }; - - let stdout_logs = if args.stdout_quiet { - None - } else { - Some(StdoutLogConfig { format: args.stdout_format }) - }; - - let file_logs = args.file_directory.as_ref().map(|path| FileLogConfig { - directory_path: path.clone(), - format: args.file_format, - rotation: args.file_rotation, - }); - - Self { global_level: level, stdout_logs, file_logs } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use clap::Parser; - - // Helper struct to parse GlobalArgs within a test CLI structure - #[derive(Parser, Debug)] - struct TestCli { - #[command(flatten)] - global: LogArgs, - } - - #[test] - fn test_default_verbosity_level() { - let cli = TestCli::parse_from(["test_app"]); - assert_eq!( - cli.global.level, 3, - "Default verbosity should be 3 when no -v flag is present." - ); - } - - #[test] - fn test_verbosity_count() { - let cli_v1 = TestCli::parse_from(["test_app", "-v"]); - assert_eq!(cli_v1.global.level, 1, "Verbosity with a single -v should be 1."); - - let cli_v3 = TestCli::parse_from(["test_app", "-vvv"]); - assert_eq!(cli_v3.global.level, 3, "Verbosity with -vvv should be 3."); - - let cli_v5 = TestCli::parse_from(["test_app", "-vvvvv"]); - assert_eq!(cli_v5.global.level, 5, "Verbosity with -vvvvv should be 5."); - } -} diff --git a/crates/utilities/cli/src/logs.rs b/crates/utilities/cli/src/logs.rs new file mode 100644 index 0000000000..729ddd6ef8 --- /dev/null +++ b/crates/utilities/cli/src/logs.rs @@ -0,0 +1,97 @@ +//! Logging Configuration Types + +use std::path::PathBuf; + +use clap::ValueEnum; +use serde::{Deserialize, Serialize}; +use tracing::level_filters::LevelFilter; + +use crate::{LogArgs, LogFormat}; + +/// The rotation of the log files. +#[derive(Debug, Clone, Serialize, Deserialize, ValueEnum, Default)] +#[serde(rename_all = "lowercase")] +pub enum LogRotation { + /// Rotate the log files every minute. + Minutely, + /// Rotate the log files hourly. + Hourly, + /// Rotate the log files daily. + Daily, + /// Do not rotate the log files. + #[default] + Never, +} + +/// Configuration for file logging. +#[derive(Debug, Clone)] +pub struct FileLogConfig { + /// The path to the directory where the log files are stored. + pub directory_path: PathBuf, + /// The format of the logs printed to the log file. + pub format: LogFormat, + /// The rotation of the log files. + pub rotation: LogRotation, +} + +/// Configuration for stdout logging. +#[derive(Debug, Clone)] +pub struct StdoutLogConfig { + /// The format of the logs printed to stdout. + pub format: LogFormat, +} + +/// Global configuration for logging. +/// Default is to only print logs to stdout in full format. +#[derive(Debug, Clone)] +pub struct LogConfig { + /// Global verbosity level for logging. + pub global_level: LevelFilter, + /// The configuration for stdout logging. + pub stdout_logs: Option, + /// The configuration for file logging. + pub file_logs: Option, +} + +impl Default for LogConfig { + fn default() -> Self { + Self { + global_level: LevelFilter::INFO, + stdout_logs: Some(StdoutLogConfig { format: LogFormat::Full }), + file_logs: None, + } + } +} + +impl From for LogConfig { + fn from(args: LogArgs) -> Self { + Self::new(args) + } +} + +impl LogConfig { + /// Creates a new `LogConfig` from `LogArgs`. + pub fn new(args: LogArgs) -> Self { + let level = match args.level { + 1 => LevelFilter::ERROR, + 2 => LevelFilter::WARN, + 3 => LevelFilter::INFO, + 4 => LevelFilter::DEBUG, + _ => LevelFilter::TRACE, + }; + + let stdout_logs = if args.stdout_quiet { + None + } else { + Some(StdoutLogConfig { format: args.stdout_format }) + }; + + let file_logs = args.file_directory.as_ref().map(|path| FileLogConfig { + directory_path: path.clone(), + format: args.file_format, + rotation: args.file_rotation, + }); + + Self { global_level: level, stdout_logs, file_logs } + } +} diff --git a/crates/utilities/cli/src/tracing.rs b/crates/utilities/cli/src/tracing.rs index fa64a9d29f..a50719d7e2 100644 --- a/crates/utilities/cli/src/tracing.rs +++ b/crates/utilities/cli/src/tracing.rs @@ -9,7 +9,7 @@ use tracing_subscriber::{ use serde::{Deserialize, Serialize}; use tracing_subscriber::EnvFilter; -use crate::log::{LogConfig, LogRotation}; +use crate::{LogConfig, LogRotation}; /// The format of the logs. #[derive( diff --git a/examples/discovery/src/main.rs b/examples/discovery/src/main.rs index 5d81834460..aa24f9b261 100644 --- a/examples/discovery/src/main.rs +++ b/examples/discovery/src/main.rs @@ -19,7 +19,7 @@ use clap::Parser; use discv5::enr::CombinedKey; -use kona_cli::{LogConfig, log::LogArgs}; +use kona_cli::{LogArgs, LogConfig}; use kona_disc::{Discv5Builder, LocalNode}; use std::net::{IpAddr, Ipv4Addr}; diff --git a/examples/execution-fixture/src/main.rs b/examples/execution-fixture/src/main.rs index 8f2fdefa5f..07f8831f8b 100644 --- a/examples/execution-fixture/src/main.rs +++ b/examples/execution-fixture/src/main.rs @@ -18,7 +18,7 @@ use anyhow::{Result, anyhow}; use clap::Parser; -use kona_cli::{LogConfig, log::LogArgs}; +use kona_cli::{LogArgs, LogConfig}; use kona_executor::test_utils::ExecutorTestFixtureCreator; use std::path::PathBuf; use tracing::info; diff --git a/examples/gossip/src/main.rs b/examples/gossip/src/main.rs index 2574e5a4c6..3a936efbb7 100644 --- a/examples/gossip/src/main.rs +++ b/examples/gossip/src/main.rs @@ -19,7 +19,7 @@ use clap::Parser; use discv5::enr::CombinedKey; -use kona_cli::{LogConfig, log::LogArgs}; +use kona_cli::{LogArgs, LogConfig}; use kona_disc::LocalNode; use kona_node_service::{NetworkActor, NetworkConfig, NetworkContext, NodeActor}; use kona_registry::ROLLUP_CONFIGS;