From 355a10fd1f3a9689c779b0741fa2b883b57bdc5e Mon Sep 17 00:00:00 2001 From: Andreas Bigger Date: Wed, 7 Jan 2026 16:36:02 -0500 Subject: [PATCH 1/3] feat(bin): base binary --- Cargo.lock | 38 +++++-- Cargo.toml | 2 +- Justfile | 4 + bin/base/Cargo.toml | 49 +++++++++ bin/base/README.md | 0 bin/base/src/cli.rs | 73 +++++++++++++ bin/base/src/commands/consensus.rs | 21 ++++ bin/base/src/commands/execution.rs | 21 ++++ bin/base/src/commands/mempool.rs | 116 +++++++++++++++++++++ bin/base/src/commands/mod.rs | 27 +++++ bin/base/src/flags/globals.rs | 22 ++++ bin/base/src/flags/mod.rs | 4 + bin/base/src/main.rs | 22 ++++ bin/base/src/version.rs | 7 ++ bin/node/Cargo.toml | 2 +- crates/cli/Cargo.toml | 16 ++- crates/cli/README.md | 8 +- crates/cli/src/backtrace.rs | 16 +++ crates/cli/src/lib.rs | 15 +++ crates/cli/src/logging.rs | 94 +++++++++++++++++ crates/cli/src/logs.rs | 97 ++++++++++++++++++ crates/cli/src/sigsegv.rs | 155 ++++++++++++++++++++++++++++ crates/cli/src/tracing.rs | 158 +++++++++++++++++++++++++++++ 23 files changed, 954 insertions(+), 13 deletions(-) create mode 100644 bin/base/Cargo.toml create mode 100644 bin/base/README.md create mode 100644 bin/base/src/cli.rs create mode 100644 bin/base/src/commands/consensus.rs create mode 100644 bin/base/src/commands/execution.rs create mode 100644 bin/base/src/commands/mempool.rs create mode 100644 bin/base/src/commands/mod.rs create mode 100644 bin/base/src/flags/globals.rs create mode 100644 bin/base/src/flags/mod.rs create mode 100644 bin/base/src/main.rs create mode 100644 bin/base/src/version.rs create mode 100644 crates/cli/src/backtrace.rs create mode 100644 crates/cli/src/logging.rs create mode 100644 crates/cli/src/logs.rs create mode 100644 crates/cli/src/sigsegv.rs create mode 100644 crates/cli/src/tracing.rs diff --git a/Cargo.lock b/Cargo.lock index db1ccefb..2ad235d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1502,6 +1502,22 @@ dependencies = [ "tokio", ] +[[package]] +name = "base-bin" +version = "0.2.1" +dependencies = [ + "alloy-chains", + "alloy-provider", + "alloy-rpc-types-eth", + "anyhow", + "base-cli", + "clap", + "derive_more", + "futures-util", + "tokio", + "tracing", +] + [[package]] name = "base-bundles" version = "0.2.1" @@ -1519,6 +1535,19 @@ dependencies = [ "uuid", ] +[[package]] +name = "base-cli" +version = "0.2.1" +dependencies = [ + "clap", + "libc", + "reth", + "serde", + "tracing", + "tracing-appender", + "tracing-subscriber 0.3.22", +] + [[package]] name = "base-fbal" version = "0.2.1" @@ -1557,13 +1586,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "base-reth-cli" -version = "0.2.1" -dependencies = [ - "reth", -] - [[package]] name = "base-reth-flashblocks" version = "0.2.1" @@ -1620,7 +1642,7 @@ dependencies = [ name = "base-reth-node" version = "0.2.1" dependencies = [ - "base-reth-cli", + "base-cli", "base-reth-runner", "clap", "once_cell", diff --git a/Cargo.toml b/Cargo.toml index cbe76044..809f8c5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,7 @@ codegen-units = 1 # local base-bundles = { path = "crates/bundles" } base-fbal = { path = "crates/fbal" } -base-reth-cli = { path = "crates/cli" } +base-cli = { path = "crates/cli" } base-reth-rpc = { path = "crates/rpc" } base-reth-rpc-types = { path = "crates/reth-rpc-types" } base-tracex = { path = "crates/tracex" } diff --git a/Justfile b/Justfile index b9df1e93..b6eb3f60 100644 --- a/Justfile +++ b/Justfile @@ -81,6 +81,10 @@ build-maxperf: build-node: cargo build --bin base-reth-node +# Runs the base cli binary +base *args: + cargo run -p base-bin -- {{args}} + # Build the contracts used for tests build-contracts: cd crates/test-utils/contracts && forge build diff --git a/bin/base/Cargo.toml b/bin/base/Cargo.toml new file mode 100644 index 00000000..fae6d66f --- /dev/null +++ b/bin/base/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "base-bin" +description = "Base CLI" + +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[[bin]] +name = "base" +path = "src/main.rs" + +[lints] +workspace = true + +[dependencies] +# Local +base-cli.workspace = true + +# CLI +clap = { workspace = true, features = ["derive"] } + +# Async +tokio = { workspace = true, features = ["full", "signal"] } + +# Error handling +anyhow = "1" + +# Chains +alloy-chains = "0.2" + +# Alloy - WebSocket provider for mempool subscriptions +alloy-provider = { workspace = true, features = ["pubsub", "ws"] } +alloy-rpc-types-eth.workspace = true + +# Async utilities +futures-util.workspace = true + +# Derive +derive_more = { workspace = true, features = ["display"] } + +# Logging +tracing.workspace = true + +[features] +default = [] diff --git a/bin/base/README.md b/bin/base/README.md new file mode 100644 index 00000000..e69de29b diff --git a/bin/base/src/cli.rs b/bin/base/src/cli.rs new file mode 100644 index 00000000..293a8774 --- /dev/null +++ b/bin/base/src/cli.rs @@ -0,0 +1,73 @@ +//! Contains the CLI entry point for the Base binary. + +use anyhow::Result; +use clap::{builder::styling::Styles, Parser}; + +use crate::commands::Commands; +use crate::flags::GlobalArgs; +use crate::version; + +/// Returns the CLI styles. +const fn cli_styles() -> Styles { + Styles::plain() +} + +/// The 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 { + /// The subcommand to run. + #[command(subcommand)] + pub subcommand: Commands, + /// Global arguments for the CLI. + #[command(flatten)] + pub global: GlobalArgs, +} + +impl Cli { + /// Runs the CLI. + pub fn run(self) -> Result<()> { + // TODO: Initialize telemetry - allow subcommands to customize the filter. + + // TODO: Initialize unified metrics + + // TODO: Allow subcommands to initialize cli metrics. + + // Run the subcommand. + match self.subcommand { + Commands::Consensus(c) => Self::run_until_ctrl_c(c.run(&self.global)), + Commands::Execution(e) => Self::run_until_ctrl_c(e.run(&self.global)), + Commands::Mempool(m) => Self::run_until_ctrl_c(m.run(&self.global)), + } + } + + /// Run until ctrl-c is pressed. + pub fn run_until_ctrl_c(fut: F) -> Result<()> + where + F: std::future::Future>, + { + let rt = Self::tokio_runtime().map_err(|e| anyhow::anyhow!(e))?; + rt.block_on(async move { + tokio::select! { + res = fut => res, + _ = tokio::signal::ctrl_c() => { + tracing::info!(target: "cli", "Received Ctrl-C, shutting down..."); + Ok(()) + } + } + }) + } + + /// Creates a new default tokio multi-thread [Runtime](tokio::runtime::Runtime) with all + /// features enabled + pub fn tokio_runtime() -> Result { + tokio::runtime::Builder::new_multi_thread().enable_all().build() + } +} diff --git a/bin/base/src/commands/consensus.rs b/bin/base/src/commands/consensus.rs new file mode 100644 index 00000000..d08c1882 --- /dev/null +++ b/bin/base/src/commands/consensus.rs @@ -0,0 +1,21 @@ +//! Consensus command for the Base Stack. + +use anyhow::Result; +use clap::Args; + +use crate::flags::GlobalArgs; + +/// The consensus command. +#[derive(Debug, Clone, Args)] +pub struct ConsensusCommand { + // TODO: Add consensus-specific arguments here. +} + +impl ConsensusCommand { + /// Runs the consensus command. + pub async fn run(&self, _global: &GlobalArgs) -> Result<()> { + tracing::info!(target: "cli", "Running consensus command..."); + // TODO: Implement consensus logic. + Ok(()) + } +} diff --git a/bin/base/src/commands/execution.rs b/bin/base/src/commands/execution.rs new file mode 100644 index 00000000..bb561445 --- /dev/null +++ b/bin/base/src/commands/execution.rs @@ -0,0 +1,21 @@ +//! Execution command for the Base Stack. + +use anyhow::Result; +use clap::Args; + +use crate::flags::GlobalArgs; + +/// The execution command. +#[derive(Debug, Clone, Args)] +pub struct ExecutionCommand { + // TODO: Add execution-specific arguments here. +} + +impl ExecutionCommand { + /// Runs the execution command. + pub async fn run(&self, _global: &GlobalArgs) -> Result<()> { + tracing::info!(target: "cli", "Running execution command..."); + // TODO: Implement execution logic. + Ok(()) + } +} diff --git a/bin/base/src/commands/mempool.rs b/bin/base/src/commands/mempool.rs new file mode 100644 index 00000000..85969677 --- /dev/null +++ b/bin/base/src/commands/mempool.rs @@ -0,0 +1,116 @@ +//! Mempool command - streams pending transactions via WebSocket. + +use alloy_chains::Chain; +use alloy_provider::{Provider, ProviderBuilder, WsConnect}; +use alloy_rpc_types_eth::Transaction; +use anyhow::Result; +use clap::Args; +use futures_util::StreamExt; + +use crate::flags::GlobalArgs; + +// Re-export traits for accessing tx fields +use alloy_provider::network::TransactionResponse; +use alloy_rpc_types_eth::TransactionTrait; + +/// Returns the default public WebSocket RPC URL for the given chain. +/// +/// These are free, public endpoints from PublicNode that support eth_subscribe. +/// See: +fn default_ws_url(chain: &Chain) -> Option<&'static str> { + match chain.id() { + 8453 => Some("wss://base-rpc.publicnode.com"), // Base Mainnet + 84532 => Some("wss://base-sepolia-rpc.publicnode.com"), // Base Sepolia + 1 => Some("wss://ethereum-rpc.publicnode.com"), // Ethereum Mainnet + 11155111 => Some("wss://ethereum-sepolia-rpc.publicnode.com"), // Ethereum Sepolia + _ => None, + } +} + +/// Prints a transaction to stdout in human-readable format. +fn print_transaction(tx: &Transaction) { + println!("tx: {}", tx.tx_hash()); + println!(" from: {}", tx.from()); + if let Some(to) = tx.to() { + println!(" to: {to}"); + } + println!(" value: {}", tx.value()); + println!(" gas: {}", tx.gas_limit()); + println!(); +} + +/// The mempool command - streams pending transactions via WebSocket. +/// +/// Uses free public WebSocket endpoints by default (from PublicNode). +/// You can override with `--rpc-url` for a custom endpoint. +#[derive(Debug, Clone, Args)] +pub struct MempoolCommand { + /// WebSocket RPC URL. Defaults to a free public endpoint for supported networks. + /// + /// Examples: + /// - PublicNode: wss://base-rpc.publicnode.com (default for Base) + /// - Alchemy: wss://base-mainnet.g.alchemy.com/v2/YOUR_API_KEY + /// - QuickNode: wss://YOUR_ENDPOINT.base-mainnet.quiknode.pro/YOUR_API_KEY + #[arg(long = "rpc-url", env = "BASE_RPC_URL")] + pub rpc_url: Option, + + /// Show full transaction details instead of just hashes. + #[arg(long = "full", short = 'f', default_value = "false")] + pub full: bool, +} + +impl MempoolCommand { + /// Runs the mempool command. + pub async fn run(&self, global: &GlobalArgs) -> Result<()> { + // Determine WebSocket URL + let ws_url = match &self.rpc_url { + Some(url) => url.clone(), + None => default_ws_url(&global.network) + .ok_or_else(|| { + anyhow::anyhow!( + "No default WebSocket URL for network {}. Provide --rpc-url.", + global.network + ) + })? + .to_string(), + }; + + println!("Connecting to {}...", ws_url); + + // Connect to WebSocket provider + let ws = WsConnect::new(&ws_url); + let provider = ProviderBuilder::new().connect_ws(ws).await?; + + println!( + "Connected! Subscribing to pending transactions on {}...", + global.network + ); + + // Subscribe to pending transactions + let sub = provider.subscribe_pending_transactions().await?; + let mut stream = sub.into_stream(); + + println!("Subscribed! Streaming transactions (Ctrl-C to stop)..."); + println!(); + + // Stream transactions + while let Some(tx_hash) = stream.next().await { + if self.full { + // Fetch full transaction details + match provider.get_transaction_by_hash(tx_hash).await { + Ok(Some(tx)) => { + print_transaction(&tx); + } + Ok(None) => println!("tx: {tx_hash} (not found)"), + Err(e) => { + eprintln!("Failed to fetch tx {}: {}", tx_hash, e); + } + } + } else { + println!("tx: {tx_hash}"); + } + } + + Ok(()) + } +} diff --git a/bin/base/src/commands/mod.rs b/bin/base/src/commands/mod.rs new file mode 100644 index 00000000..9f964f43 --- /dev/null +++ b/bin/base/src/commands/mod.rs @@ -0,0 +1,27 @@ +//! Contains cli commands. + +use clap::Subcommand; + +pub mod consensus; +pub use consensus::ConsensusCommand; + +pub mod execution; +pub use execution::ExecutionCommand; + +pub mod mempool; +pub use mempool::MempoolCommand; + +/// Subcommands for the CLI. +#[derive(Debug, Clone, Subcommand)] +#[allow(clippy::large_enum_variant)] +pub enum Commands { + /// Runs the consensus layer for the Base Stack. + #[command(alias = "c")] + Consensus(ConsensusCommand), + /// Runs the execution layer for the Base Stack. + #[command(alias = "e")] + Execution(ExecutionCommand), + /// Streams pending transactions from the mempool. + #[command(alias = "m")] + Mempool(MempoolCommand), +} diff --git a/bin/base/src/flags/globals.rs b/bin/base/src/flags/globals.rs new file mode 100644 index 00000000..dbfe96c8 --- /dev/null +++ b/bin/base/src/flags/globals.rs @@ -0,0 +1,22 @@ +//! Contains the global CLI flags. + +use clap::Parser; +use base_cli::LogArgs; + +/// Global arguments for the CLI. +#[derive(Parser, Default, Clone, Debug)] +pub struct GlobalArgs { + /// Logging arguments. + #[command(flatten)] + pub log_args: LogArgs, + /// The network ID (e.g. "8453" or "base-mainnet"). + #[arg( + long = "network", + short = 'n', + global = true, + default_value = "8453", + env = "BASE_NETWORK", + help = "The network ID" + )] + pub network: alloy_chains::Chain, +} diff --git a/bin/base/src/flags/mod.rs b/bin/base/src/flags/mod.rs new file mode 100644 index 00000000..64566bab --- /dev/null +++ b/bin/base/src/flags/mod.rs @@ -0,0 +1,4 @@ +//! Contains CLI flags. + +mod globals; +pub use globals::GlobalArgs; diff --git a/bin/base/src/main.rs b/bin/base/src/main.rs new file mode 100644 index 00000000..0f0e0bb3 --- /dev/null +++ b/bin/base/src/main.rs @@ -0,0 +1,22 @@ +#![doc = include_str!("../README.md")] +#![doc( + issue_tracker_base_url = "https://github.com/base/node-reth/issues/" +)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +pub mod cli; +pub mod commands; +pub mod flags; +pub mod version; + +fn main() { + use clap::Parser; + + base_cli::Backtracing::enable(); + base_cli::SigsegvHandler::install(); + + if let Err(err) = cli::Cli::parse().run() { + eprintln!("Error: {err:?}"); + std::process::exit(1); + } +} diff --git a/bin/base/src/version.rs b/bin/base/src/version.rs new file mode 100644 index 00000000..f66b7397 --- /dev/null +++ b/bin/base/src/version.rs @@ -0,0 +1,7 @@ +//! Version information for the Base binary. + +/// Short version string. +pub const SHORT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +/// Long version string with additional build info. +pub const LONG_VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), "\n", "Base Stack CLI"); diff --git a/bin/node/Cargo.toml b/bin/node/Cargo.toml index 2782995f..3580ff3d 100644 --- a/bin/node/Cargo.toml +++ b/bin/node/Cargo.toml @@ -14,7 +14,7 @@ workspace = true [dependencies] # internal -base-reth-cli.workspace = true +base-cli.workspace = true base-reth-runner.workspace = true # reth diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 3680b617..88c75f6a 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "base-reth-cli" +name = "base-cli" version.workspace = true edition.workspace = true rust-version.workspace = true @@ -13,3 +13,17 @@ workspace = true [dependencies] reth.workspace = true + +# CLI +clap = { workspace = true, features = ["derive"] } + +# Logging +tracing.workspace = true +tracing-subscriber = { workspace = true, features = ["env-filter"] } +tracing-appender = "0.2" + +# Serialization +serde = { workspace = true, features = ["derive"] } + +[target.'cfg(unix)'.dependencies] +libc = "0.2" diff --git a/crates/cli/README.md b/crates/cli/README.md index 1b35a8f1..ff1ca5ea 100644 --- a/crates/cli/README.md +++ b/crates/cli/README.md @@ -1,3 +1,7 @@ -# CLI Utilities for Base Reth +# CLI Utilities for Base + +Contains a set of utilities for the Base. + +### Sigsegv Handling + -Contains a set of utilities for the Base Reth node CLI. diff --git a/crates/cli/src/backtrace.rs b/crates/cli/src/backtrace.rs new file mode 100644 index 00000000..c3f97c3e --- /dev/null +++ b/crates/cli/src/backtrace.rs @@ -0,0 +1,16 @@ +//! Minimal helper utility to set the backtrace environment variable if not set. + +/// The backtracing utility. +#[derive(Debug, Clone, Copy)] +pub struct Backtracing; + +impl Backtracing { + /// Sets the RUST_BACKTRACE environment variable to 1 if it is not already set. + pub fn enable() { + // Enable backtraces unless a RUST_BACKTRACE value has already been explicitly provided. + if std::env::var_os("RUST_BACKTRACE").is_none() { + // We accept the risk that another process may set RUST_BACKTRACE at the same time. + unsafe { std::env::set_var("RUST_BACKTRACE", "1") }; + } + } +} diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 0f1115dd..79a26ac8 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -5,3 +5,18 @@ mod version; pub use version::Version; + +mod sigsegv; +pub use sigsegv::SigsegvHandler; + +mod backtrace; +pub use backtrace::Backtracing; + +mod logs; +pub use logs::{FileLogConfig, LogConfig, LogRotation, StdoutLogConfig}; + +mod logging; +pub use logging::LogArgs; + +mod tracing; +pub use tracing::{LogFormat, TestTracing}; diff --git a/crates/cli/src/logging.rs b/crates/cli/src/logging.rs new file mode 100644 index 00000000..572cbe32 --- /dev/null +++ b/crates/cli/src/logging.rs @@ -0,0 +1,94 @@ +//! 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, logfmt. + /// + /// 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. + /// logfmt: The logs are printed in logfmt key=value 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, logfmt. + /// + /// 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. + /// logfmt: The logs are printed in logfmt key=value 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/cli/src/logs.rs b/crates/cli/src/logs.rs new file mode 100644 index 00000000..729ddd6e --- /dev/null +++ b/crates/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/cli/src/sigsegv.rs b/crates/cli/src/sigsegv.rs new file mode 100644 index 00000000..cf6f9c2d --- /dev/null +++ b/crates/cli/src/sigsegv.rs @@ -0,0 +1,155 @@ +//! Signal handler to extract a backtrace from reth, which is originally from stack overflow. +//! +//! Implementation modified from [reth](https://github.com/paradigmxyz/reth/blob/main/crates/cli/util/src/sigsegv_handler.rs#L120). +//! +//! Implementation modified from [`rustc`](https://github.com/rust-lang/rust/blob/3dee9775a8c94e701a08f7b2df2c444f353d8699/compiler/rustc_driver_impl/src/signal_handler.rs). + +use std::{ + alloc::{Layout, alloc}, + fmt, mem, ptr, +}; + +/// The SIGSEGV handler. +#[derive(Debug, Clone, Copy)] +pub struct SigsegvHandler; + +impl SigsegvHandler { + /// Installs a SIGSEGV handler. + /// + /// When SIGSEGV is delivered to the process, print a stack trace and then exit. + pub fn install() { + unsafe { + let alt_stack_size: usize = min_sigstack_size() + 64 * 1024; + let mut alt_stack: libc::stack_t = mem::zeroed(); + alt_stack.ss_sp = alloc(Layout::from_size_align(alt_stack_size, 1).unwrap()).cast(); + alt_stack.ss_size = alt_stack_size; + libc::sigaltstack(&alt_stack, ptr::null_mut()); + + let mut sa: libc::sigaction = mem::zeroed(); + sa.sa_sigaction = print_stack_trace as libc::sighandler_t; + sa.sa_flags = libc::SA_NODEFER | libc::SA_RESETHAND | libc::SA_ONSTACK; + libc::sigemptyset(&mut sa.sa_mask); + libc::sigaction(libc::SIGSEGV, &sa, ptr::null_mut()); + } + } +} + +unsafe extern "C" { + fn backtrace_symbols_fd(buffer: *const *mut libc::c_void, size: libc::c_int, fd: libc::c_int); +} + +fn backtrace_stderr(buffer: &[*mut libc::c_void]) { + let size = buffer.len().try_into().unwrap_or_default(); + unsafe { backtrace_symbols_fd(buffer.as_ptr(), size, libc::STDERR_FILENO) }; +} + +/// Unbuffered, unsynchronized writer to stderr. +/// +/// Only acceptable because everything will end soon anyways. +struct RawStderr(()); + +impl fmt::Write for RawStderr { + fn write_str(&mut self, s: &str) -> Result<(), fmt::Error> { + let ret = unsafe { libc::write(libc::STDERR_FILENO, s.as_ptr().cast(), s.len()) }; + if ret == -1 { Err(fmt::Error) } else { Ok(()) } + } +} + +/// We don't really care how many bytes we actually get out. SIGSEGV comes for our head. +/// Splash stderr with letters of our own blood to warn our friends about the monster. +macro_rules! raw_errln { + ($tokens:tt) => { + let _ = ::core::fmt::Write::write_fmt(&mut RawStderr(()), format_args!($tokens)); + let _ = ::core::fmt::Write::write_char(&mut RawStderr(()), '\n'); + }; +} + +/// Signal handler installed for SIGSEGV +extern "C" fn print_stack_trace(_: libc::c_int) { + const MAX_FRAMES: usize = 256; + let mut stack_trace: [*mut libc::c_void; MAX_FRAMES] = [ptr::null_mut(); MAX_FRAMES]; + let stack = unsafe { + // Collect return addresses + let depth = libc::backtrace(stack_trace.as_mut_ptr(), MAX_FRAMES as i32); + if depth == 0 { + return; + } + &stack_trace[0..depth as usize] + }; + + // Just a stack trace is cryptic. Explain what we're doing. + raw_errln!("error: reth interrupted by SIGSEGV, printing backtrace\n"); + let mut written = 1; + let mut consumed = 0; + // Begin elaborating return addrs into symbols and writing them directly to stderr + // Most backtraces are stack overflow, most stack overflows are from recursion + // Check for cycles before writing 250 lines of the same ~5 symbols + let cycled = |(runner, walker)| runner == walker; + let mut cyclic = false; + if let Some(period) = stack.iter().skip(1).step_by(2).zip(stack).position(cycled) { + let period = period.saturating_add(1); // avoid "what if wrapped?" branches + let Some(offset) = stack.iter().skip(period).zip(stack).position(cycled) else { + // impossible. + return; + }; + + // Count matching trace slices, else we could miscount "biphasic cycles" + // with the same period + loop entry but a different inner loop + let next_cycle = stack[offset..].chunks_exact(period).skip(1); + let cycles = 1 + next_cycle + .zip(stack[offset..].chunks_exact(period)) + .filter(|(next, prev)| next == prev) + .count(); + backtrace_stderr(&stack[..offset]); + written += offset; + consumed += offset; + if cycles > 1 { + raw_errln!("\n### cycle encountered after {offset} frames with period {period}"); + backtrace_stderr(&stack[consumed..consumed + period]); + raw_errln!("### recursed {cycles} times\n"); + written += period + 4; + consumed += period * cycles; + cyclic = true; + }; + } + let rem = &stack[consumed..]; + backtrace_stderr(rem); + raw_errln!(""); + written += rem.len() + 1; + + let random_depth = || 8 * 16; // chosen by random diceroll (2d20) + if cyclic || stack.len() > random_depth() { + // technically speculation, but assert it with confidence anyway. + // We only arrived in this signal handler because bad things happened + // and this message is for explaining it's not the programmer's fault + raw_errln!("note: reth unexpectedly overflowed its stack! this is a bug"); + written += 1; + } + if stack.len() == MAX_FRAMES { + raw_errln!("note: maximum backtrace depth reached, frames may have been lost"); + written += 1; + } + raw_errln!("note: we would appreciate a report at https://github.com/paradigmxyz/reth"); + written += 1; + if written > 24 { + // We probably just scrolled the earlier "we got SIGSEGV" message off the terminal + raw_errln!("note: backtrace dumped due to SIGSEGV! resuming signal"); + } +} + +/// Modern kernels on modern hardware can have dynamic signal stack sizes. +#[cfg(any(target_os = "linux", target_os = "android"))] +fn min_sigstack_size() -> usize { + const AT_MINSIGSTKSZ: core::ffi::c_ulong = 51; + let dynamic_sigstksz = unsafe { libc::getauxval(AT_MINSIGSTKSZ) }; + // If getauxval couldn't find the entry, it returns 0, + // so take the higher of the "constant" and auxval. + // This transparently supports older kernels which don't provide AT_MINSIGSTKSZ + libc::MINSIGSTKSZ.max(dynamic_sigstksz as _) +} + +/// Not all OS support hardware where this is needed. +#[cfg(not(any(target_os = "linux", target_os = "android")))] +const fn min_sigstack_size() -> usize { + libc::MINSIGSTKSZ +} diff --git a/crates/cli/src/tracing.rs b/crates/cli/src/tracing.rs new file mode 100644 index 00000000..c576abca --- /dev/null +++ b/crates/cli/src/tracing.rs @@ -0,0 +1,158 @@ +//! [tracing_subscriber] utilities. + +use tracing_subscriber::{ + Layer, + fmt::{ + format::{FormatEvent, FormatFields, Writer}, + time::{FormatTime, SystemTime}, + }, + prelude::__tracing_subscriber_SubscriberExt, + registry::LookupSpan, + util::{SubscriberInitExt, TryInitError}, +}; + +use serde::{Deserialize, Serialize}; +use std::fmt; +use tracing_subscriber::EnvFilter; + +use crate::{LogConfig, LogRotation}; + +/// The format of the logs. +#[derive( + Default, Debug, Clone, Copy, PartialEq, Eq, Hash, clap::ValueEnum, Serialize, Deserialize, +)] +#[serde(rename_all = "lowercase")] +#[clap(rename_all = "lowercase")] +pub enum LogFormat { + /// Full format (default). + #[default] + Full, + /// JSON format. + Json, + /// Pretty format. + Pretty, + /// Compact format. + Compact, + /// Logfmt format. + Logfmt, +} + +/// Custom logfmt formatter for tracing events. +struct LogfmtFormatter; + +impl FormatEvent for LogfmtFormatter +where + S: tracing::Subscriber + for<'a> LookupSpan<'a>, + N: for<'a> FormatFields<'a> + 'static, +{ + fn format_event( + &self, + ctx: &tracing_subscriber::fmt::FmtContext<'_, S, N>, + mut writer: Writer<'_>, + event: &tracing::Event<'_>, + ) -> fmt::Result { + let meta = event.metadata(); + + // Write timestamp + let time_format = SystemTime; + write!(writer, "time=\"")?; + time_format.format_time(&mut writer)?; + write!(writer, "\" ")?; + + // Write level + write!(writer, "level={} ", meta.level())?; + + // Write target + write!(writer, "target={} ", meta.target())?; + + // Write the message and fields + ctx.field_format().format_fields(writer.by_ref(), event)?; + + writeln!(writer) + } +} + +impl LogConfig { + /// Initializes the tracing subscriber + /// + /// # Arguments + /// * `verbosity_level` - The verbosity level (0-5). If `0`, no logs are printed. + /// * `env_filter` - Optional environment filter for the subscriber. + /// + /// # Returns + /// * `Result<()>` - Ok if successful, Err otherwise. + pub fn init_tracing_subscriber( + &self, + env_filter: Option, + ) -> Result<(), TryInitError> { + let file_layer = self.file_logs.as_ref().map(|file_logs| { + let directory_path = file_logs.directory_path.clone(); + + let appender = match file_logs.rotation { + LogRotation::Minutely => { + tracing_appender::rolling::minutely(directory_path, "kona.log") + } + LogRotation::Hourly => { + tracing_appender::rolling::hourly(directory_path, "kona.log") + } + LogRotation::Daily => tracing_appender::rolling::daily(directory_path, "kona.log"), + LogRotation::Never => tracing_appender::rolling::never(directory_path, "kona.log"), + }; + + match file_logs.format { + LogFormat::Full => tracing_subscriber::fmt::layer().with_writer(appender).boxed(), + LogFormat::Json => { + tracing_subscriber::fmt::layer().json().with_writer(appender).boxed() + } + LogFormat::Pretty => { + tracing_subscriber::fmt::layer().pretty().with_writer(appender).boxed() + } + LogFormat::Compact => { + tracing_subscriber::fmt::layer().compact().with_writer(appender).boxed() + } + LogFormat::Logfmt => tracing_subscriber::fmt::layer() + .event_format(LogfmtFormatter) + .with_writer(appender) + .boxed(), + } + }); + + let stdout_layer = self.stdout_logs.as_ref().map(|stdout_logs| match stdout_logs.format { + LogFormat::Full => tracing_subscriber::fmt::layer().boxed(), + LogFormat::Json => tracing_subscriber::fmt::layer().json().boxed(), + LogFormat::Pretty => tracing_subscriber::fmt::layer().pretty().boxed(), + LogFormat::Compact => tracing_subscriber::fmt::layer().compact().boxed(), + LogFormat::Logfmt => { + tracing_subscriber::fmt::layer().event_format(LogfmtFormatter).boxed() + } + }); + + let env_filter = env_filter + .unwrap_or(EnvFilter::from_default_env()) + .add_directive(self.global_level.into()); + + tracing_subscriber::registry() + .with(env_filter) + .with(file_layer) + .with(stdout_layer) + .try_init()?; + + Ok(()) + } +} + +/// Test Tracing +#[derive(Debug)] +pub struct TestTracing; + +impl TestTracing { + /// This provides function for init tracing in testing + /// + /// # Functions + /// - `init`: A helper function for initializing tracing in test environments. + /// - `init_tracing_subscriber`: Initializes the tracing subscriber with a specified verbosity level + /// and optional environment filter. + pub fn init() { + let _ = LogConfig::default().init_tracing_subscriber(None::); + } +} From 275c1711fe279d8b01fe7cecf2d09f63855d292f Mon Sep 17 00:00:00 2001 From: Andreas Bigger Date: Wed, 7 Jan 2026 16:52:19 -0500 Subject: [PATCH 2/3] fix: correct crate reference and update tests for Metadata changes Fix base_reth_cli -> base_cli reference in node binary and update EIP-7702 tests to match Metadata struct after receipts/balances removal. --- bin/node/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/node/src/main.rs b/bin/node/src/main.rs index b084503f..6d3a8e00 100644 --- a/bin/node/src/main.rs +++ b/bin/node/src/main.rs @@ -14,7 +14,7 @@ static ALLOC: reth_cli_util::allocator::Allocator = reth_cli_util::allocator::ne fn main() { // Step 1: Initialize versioning so logs / telemetry report the right build info. - base_reth_cli::Version::init(); + base_cli::Version::init(); // Step 2: Parse CLI arguments and hand execution to the Optimism node runner. use clap::Parser; From 12022e3ac2a3c6b72e5e7798f35d34f2f099e104 Mon Sep 17 00:00:00 2001 From: Andreas Bigger Date: Wed, 7 Jan 2026 16:52:46 -0500 Subject: [PATCH 3/3] feat(bin): base binary --- bin/base/src/cli.rs | 6 ++---- bin/base/src/commands/mempool.rs | 15 +++++---------- bin/base/src/flags/globals.rs | 2 +- bin/base/src/main.rs | 4 +--- crates/cli/src/logging.rs | 3 ++- crates/cli/src/tracing.rs | 9 ++++----- 6 files changed, 15 insertions(+), 24 deletions(-) diff --git a/bin/base/src/cli.rs b/bin/base/src/cli.rs index 293a8774..902cb67b 100644 --- a/bin/base/src/cli.rs +++ b/bin/base/src/cli.rs @@ -1,11 +1,9 @@ //! Contains the CLI entry point for the Base binary. use anyhow::Result; -use clap::{builder::styling::Styles, Parser}; +use clap::{Parser, builder::styling::Styles}; -use crate::commands::Commands; -use crate::flags::GlobalArgs; -use crate::version; +use crate::{commands::Commands, flags::GlobalArgs, version}; /// Returns the CLI styles. const fn cli_styles() -> Styles { diff --git a/bin/base/src/commands/mempool.rs b/bin/base/src/commands/mempool.rs index 85969677..a8061598 100644 --- a/bin/base/src/commands/mempool.rs +++ b/bin/base/src/commands/mempool.rs @@ -1,25 +1,23 @@ //! Mempool command - streams pending transactions via WebSocket. use alloy_chains::Chain; +// Re-export traits for accessing tx fields +use alloy_provider::network::TransactionResponse; use alloy_provider::{Provider, ProviderBuilder, WsConnect}; -use alloy_rpc_types_eth::Transaction; +use alloy_rpc_types_eth::{Transaction, TransactionTrait}; use anyhow::Result; use clap::Args; use futures_util::StreamExt; use crate::flags::GlobalArgs; -// Re-export traits for accessing tx fields -use alloy_provider::network::TransactionResponse; -use alloy_rpc_types_eth::TransactionTrait; - /// Returns the default public WebSocket RPC URL for the given chain. /// /// These are free, public endpoints from PublicNode that support eth_subscribe. /// See: fn default_ws_url(chain: &Chain) -> Option<&'static str> { match chain.id() { - 8453 => Some("wss://base-rpc.publicnode.com"), // Base Mainnet + 8453 => Some("wss://base-rpc.publicnode.com"), // Base Mainnet 84532 => Some("wss://base-sepolia-rpc.publicnode.com"), // Base Sepolia 1 => Some("wss://ethereum-rpc.publicnode.com"), // Ethereum Mainnet 11155111 => Some("wss://ethereum-sepolia-rpc.publicnode.com"), // Ethereum Sepolia @@ -81,10 +79,7 @@ impl MempoolCommand { let ws = WsConnect::new(&ws_url); let provider = ProviderBuilder::new().connect_ws(ws).await?; - println!( - "Connected! Subscribing to pending transactions on {}...", - global.network - ); + println!("Connected! Subscribing to pending transactions on {}...", global.network); // Subscribe to pending transactions let sub = provider.subscribe_pending_transactions().await?; diff --git a/bin/base/src/flags/globals.rs b/bin/base/src/flags/globals.rs index dbfe96c8..e2e579b1 100644 --- a/bin/base/src/flags/globals.rs +++ b/bin/base/src/flags/globals.rs @@ -1,7 +1,7 @@ //! Contains the global CLI flags. -use clap::Parser; use base_cli::LogArgs; +use clap::Parser; /// Global arguments for the CLI. #[derive(Parser, Default, Clone, Debug)] diff --git a/bin/base/src/main.rs b/bin/base/src/main.rs index 0f0e0bb3..41b4dd93 100644 --- a/bin/base/src/main.rs +++ b/bin/base/src/main.rs @@ -1,7 +1,5 @@ #![doc = include_str!("../README.md")] -#![doc( - issue_tracker_base_url = "https://github.com/base/node-reth/issues/" -)] +#![doc(issue_tracker_base_url = "https://github.com/base/node-reth/issues/")] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] pub mod cli; diff --git a/crates/cli/src/logging.rs b/crates/cli/src/logging.rs index 572cbe32..12e95f04 100644 --- a/crates/cli/src/logging.rs +++ b/crates/cli/src/logging.rs @@ -61,9 +61,10 @@ pub struct LogArgs { #[cfg(test)] mod tests { - use super::*; use clap::Parser; + use super::*; + // Helper struct to parse GlobalArgs within a test CLI structure #[derive(Parser, Debug)] struct TestCli { diff --git a/crates/cli/src/tracing.rs b/crates/cli/src/tracing.rs index c576abca..c6610306 100644 --- a/crates/cli/src/tracing.rs +++ b/crates/cli/src/tracing.rs @@ -1,7 +1,10 @@ //! [tracing_subscriber] utilities. +use std::fmt; + +use serde::{Deserialize, Serialize}; use tracing_subscriber::{ - Layer, + EnvFilter, Layer, fmt::{ format::{FormatEvent, FormatFields, Writer}, time::{FormatTime, SystemTime}, @@ -11,10 +14,6 @@ use tracing_subscriber::{ util::{SubscriberInitExt, TryInitError}, }; -use serde::{Deserialize, Serialize}; -use std::fmt; -use tracing_subscriber::EnvFilter; - use crate::{LogConfig, LogRotation}; /// The format of the logs.