From 0a9f7842dbbbc99066611491cd11c6a49c84eb05 Mon Sep 17 00:00:00 2001 From: grumbach Date: Fri, 3 Apr 2026 14:47:34 +0900 Subject: [PATCH 01/11] feat: gate all logging behind `logging` feature flag Make tracing, tracing-subscriber, and tracing-appender optional dependencies activated by the new `logging` feature flag. When disabled, all log macros compile to no-ops with zero binary overhead. - `default = ["logging"]` so dev builds get logging automatically - Release without logging: `cargo build --release --no-default-features` - New `src/logging.rs` facade re-exports tracing macros or no-ops - All 25 library files switched from `use tracing::` to `use crate::logging::` - CLI log flags (--log-level, --log-format, --log-dir, --log-max-files) gated behind `#[cfg(feature = "logging")]` - Subscriber init in both binaries gated behind the feature --- Cargo.toml | 15 ++-- src/bin/ant-devnet/main.rs | 31 ++++--- src/bin/ant-node/cli.rs | 12 ++- src/bin/ant-node/main.rs | 141 ++++++++++++++++--------------- src/client/chunk_protocol.rs | 2 +- src/devnet.rs | 2 +- src/lib.rs | 2 + src/logging.rs | 91 ++++++++++++++++++++ src/node.rs | 2 +- src/payment/metrics.rs | 2 +- src/payment/quote.rs | 10 +-- src/payment/single_node.rs | 2 +- src/payment/verifier.rs | 14 +-- src/replication/audit.rs | 2 +- src/replication/bootstrap.rs | 2 +- src/replication/fresh.rs | 2 +- src/replication/mod.rs | 2 +- src/replication/neighbor_sync.rs | 2 +- src/replication/paid_list.rs | 2 +- src/replication/pruning.rs | 2 +- src/replication/quorum.rs | 2 +- src/replication/scheduling.rs | 2 +- src/storage/handler.rs | 2 +- src/storage/lmdb.rs | 4 +- src/upgrade/apply.rs | 2 +- src/upgrade/binary_cache.rs | 2 +- src/upgrade/mod.rs | 2 +- src/upgrade/monitor.rs | 2 +- src/upgrade/release_cache.rs | 2 +- src/upgrade/rollout.rs | 2 +- src/upgrade/signature.rs | 2 +- 31 files changed, 240 insertions(+), 124 deletions(-) create mode 100644 src/logging.rs diff --git a/Cargo.toml b/Cargo.toml index 203d89e9..ad92fb62 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,10 +62,10 @@ directories = "5" reqwest = { version = "0.13", features = ["json", "rustls"], default-features = false } semver = "1" -# Logging -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } -tracing-appender = "0.2" +# Logging (optional — behind `logging` feature flag) +tracing = { version = "0.1", optional = true } +tracing-subscriber = { version = "0.3", features = ["env-filter", "json"], optional = true } +tracing-appender = { version = "0.2", optional = true } # Error handling thiserror = "2" @@ -119,7 +119,12 @@ path = "tests/e2e/mod.rs" required-features = ["test-utils"] [features] -default = [] +default = ["logging"] +# Enable tracing/logging infrastructure. +# Included in `default` so dev builds (`cargo build`, `cargo test`) get logging +# automatically. For zero-overhead release builds, compile with: +# cargo build --release --no-default-features +logging = ["tracing", "tracing-subscriber", "tracing-appender"] # Enable additional diagnostics for development dev = [] # Expose test helpers (cache_insert, payment_verifier accessor) for diff --git a/src/bin/ant-devnet/main.rs b/src/bin/ant-devnet/main.rs index 3103902b..39e2a18c 100644 --- a/src/bin/ant-devnet/main.rs +++ b/src/bin/ant-devnet/main.rs @@ -5,8 +5,6 @@ mod cli; use ant_node::devnet::{Devnet, DevnetConfig, DevnetEvmInfo, DevnetManifest}; use clap::Parser; use cli::Cli; -use tracing::info; -use tracing_subscriber::{fmt, prelude::*, EnvFilter}; #[tokio::main] async fn main() -> color_eyre::Result<()> { @@ -14,15 +12,20 @@ async fn main() -> color_eyre::Result<()> { let cli = Cli::parse(); - let filter = - EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&cli.log_level)); + #[cfg(feature = "logging")] + { + use tracing_subscriber::{fmt, prelude::*, EnvFilter}; - tracing_subscriber::registry() - .with(fmt::layer()) - .with(filter) - .init(); + let filter = + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&cli.log_level)); - info!("ant-devnet v{}", env!("CARGO_PKG_VERSION")); + tracing_subscriber::registry() + .with(fmt::layer()) + .with(filter) + .init(); + } + + ant_node::logging::info!("ant-devnet v{}", env!("CARGO_PKG_VERSION")); let mut config = cli.preset @@ -55,7 +58,7 @@ async fn main() -> color_eyre::Result<()> { // Start Anvil and deploy contracts if EVM is enabled let evm_info = if cli.enable_evm { - info!("Starting local Anvil blockchain for EVM payment enforcement..."); + ant_node::logging::info!("Starting local Anvil blockchain for EVM payment enforcement..."); let testnet = evmlib::testnet::Testnet::new() .await .map_err(|e| color_eyre::eyre::eyre!("Failed to start Anvil testnet: {e}"))?; @@ -79,8 +82,8 @@ async fn main() -> color_eyre::Result<()> { config.evm_network = Some(network); - info!("Anvil blockchain running at {rpc_url}"); - info!("Funded wallet private key: {wallet_key}"); + ant_node::logging::info!("Anvil blockchain running at {rpc_url}"); + ant_node::logging::info!("Funded wallet private key: {wallet_key}"); // Keep testnet alive by leaking it (it will be cleaned up on process exit) // This is necessary because AnvilInstance stops Anvil when dropped @@ -111,12 +114,12 @@ async fn main() -> color_eyre::Result<()> { let json = serde_json::to_string_pretty(&manifest)?; if let Some(path) = cli.manifest { tokio::fs::write(&path, &json).await?; - info!("Wrote manifest to {}", path.display()); + ant_node::logging::info!("Wrote manifest to {}", path.display()); } else { println!("{json}"); } - info!("Devnet running. Press Ctrl+C to stop."); + ant_node::logging::info!("Devnet running. Press Ctrl+C to stop."); tokio::signal::ctrl_c().await?; devnet.shutdown().await?; diff --git a/src/bin/ant-node/cli.rs b/src/bin/ant-node/cli.rs index 0d6509cc..ddbd00b4 100644 --- a/src/bin/ant-node/cli.rs +++ b/src/bin/ant-node/cli.rs @@ -62,21 +62,25 @@ pub struct Cli { pub metrics_port: u16, /// Log level. + #[cfg(feature = "logging")] #[arg(long, value_enum, default_value = "info", env = "RUST_LOG")] pub log_level: CliLogLevel, /// Log output format. + #[cfg(feature = "logging")] #[arg(long, value_enum, default_value = "text", env = "ANT_LOG_FORMAT")] pub log_format: CliLogFormat, /// Directory for log file output. /// When set, logs are written to files in this directory instead of stdout. /// Files rotate daily and are named ant-node.YYYY-MM-DD.log. + #[cfg(feature = "logging")] #[arg(long, env = "ANT_LOG_DIR")] pub log_dir: Option, /// Maximum number of rotated log files to retain (only used with --log-dir). /// Oldest files are deleted when this limit is reached. Rotation is daily. + #[cfg(feature = "logging")] #[arg(long, default_value = "7", env = "ANT_LOG_MAX_FILES")] pub log_max_files: usize, @@ -136,6 +140,7 @@ pub enum CliEvmNetwork { } /// Log level CLI enum. +#[cfg(feature = "logging")] #[derive(Debug, Clone, Copy, ValueEnum, Default)] pub enum CliLogLevel { /// Error messages only. @@ -152,6 +157,7 @@ pub enum CliLogLevel { } /// Log format CLI enum. +#[cfg(feature = "logging")] #[derive(Debug, Clone, Copy, ValueEnum, Default)] pub enum CliLogFormat { /// Plain text output (default). @@ -207,7 +213,10 @@ impl Cli { config.port = self.port; config.ipv4_only = self.ipv4_only; - config.log_level = self.log_level.into(); + #[cfg(feature = "logging")] + { + config.log_level = self.log_level.into(); + } config.network_mode = self.network_mode.into(); // Apply CLI bootstrap peers if provided; otherwise keep config file value. @@ -275,6 +284,7 @@ impl From for EvmNetworkConfig { } } +#[cfg(feature = "logging")] impl From for String { fn from(level: CliLogLevel) -> Self { match level { diff --git a/src/bin/ant-node/main.rs b/src/bin/ant-node/main.rs index c7670496..db1c15bb 100644 --- a/src/bin/ant-node/main.rs +++ b/src/bin/ant-node/main.rs @@ -6,10 +6,10 @@ mod platform; use ant_node::config::BootstrapSource; use ant_node::NodeBuilder; use clap::Parser; -use cli::{Cli, CliLogFormat}; -use tracing::{info, warn}; -use tracing_subscriber::prelude::*; -use tracing_subscriber::{fmt, EnvFilter, Layer}; +use cli::Cli; + +#[cfg(feature = "logging")] +use cli::CliLogFormat; /// Force at least 4 worker threads regardless of CPU count. /// @@ -26,65 +26,70 @@ async fn main() -> color_eyre::Result<()> { // Parse CLI arguments let cli = Cli::parse(); - // Extract logging options before consuming the CLI struct - let log_format = cli.log_format; - let log_dir = cli.log_dir.clone(); - let log_max_files = cli.log_max_files; - - // Initialize tracing - let log_level: String = cli.log_level.into(); - let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&log_level)); - - // _guard must live for the duration of main() to ensure log flushing. - // The guard's Drop impl flushes buffered logs — it is intentionally held, not read. - #[allow(clippy::collection_is_never_read)] - let _guard: Option; - - let layer: Box + Send + Sync> = match (log_format, log_dir) { - (CliLogFormat::Text, None) => { - _guard = None; - Box::new(fmt::layer()) - } - (CliLogFormat::Json, None) => { - _guard = None; - Box::new(fmt::layer().json().flatten_event(true)) - } - (CliLogFormat::Text, Some(dir)) => { - let file_appender = tracing_appender::rolling::Builder::new() - .rotation(tracing_appender::rolling::Rotation::DAILY) - .max_log_files(log_max_files) - .filename_prefix("ant-node") - .filename_suffix("log") - .build(dir)?; - let (non_blocking, guard) = tracing_appender::non_blocking(file_appender); - _guard = Some(guard); - Box::new(fmt::layer().with_writer(non_blocking).with_ansi(false)) - } - (CliLogFormat::Json, Some(dir)) => { - let file_appender = tracing_appender::rolling::Builder::new() - .rotation(tracing_appender::rolling::Rotation::DAILY) - .max_log_files(log_max_files) - .filename_prefix("ant-node") - .filename_suffix("log") - .build(dir)?; - let (non_blocking, guard) = tracing_appender::non_blocking(file_appender); - _guard = Some(guard); - Box::new( - fmt::layer() - .json() - .flatten_event(true) - .with_writer(non_blocking) - .with_ansi(false), - ) - } + // Initialize tracing (only when logging feature is enabled) + #[cfg(feature = "logging")] + let _logging_guard = { + use tracing_subscriber::prelude::*; + use tracing_subscriber::{fmt, EnvFilter, Layer}; + + let log_format = cli.log_format; + let log_dir = cli.log_dir.clone(); + let log_max_files = cli.log_max_files; + let log_level: String = cli.log_level.into(); + let filter = + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&log_level)); + + // _guard must live for the duration of main() to ensure log flushing. + let guard: Option; + + let layer: Box + Send + Sync> = match (log_format, log_dir) { + (CliLogFormat::Text, None) => { + guard = None; + Box::new(fmt::layer()) + } + (CliLogFormat::Json, None) => { + guard = None; + Box::new(fmt::layer().json().flatten_event(true)) + } + (CliLogFormat::Text, Some(dir)) => { + let file_appender = tracing_appender::rolling::Builder::new() + .rotation(tracing_appender::rolling::Rotation::DAILY) + .max_log_files(log_max_files) + .filename_prefix("ant-node") + .filename_suffix("log") + .build(dir)?; + let (non_blocking, g) = tracing_appender::non_blocking(file_appender); + guard = Some(g); + Box::new(fmt::layer().with_writer(non_blocking).with_ansi(false)) + } + (CliLogFormat::Json, Some(dir)) => { + let file_appender = tracing_appender::rolling::Builder::new() + .rotation(tracing_appender::rolling::Rotation::DAILY) + .max_log_files(log_max_files) + .filename_prefix("ant-node") + .filename_suffix("log") + .build(dir)?; + let (non_blocking, g) = tracing_appender::non_blocking(file_appender); + guard = Some(g); + Box::new( + fmt::layer() + .json() + .flatten_event(true) + .with_writer(non_blocking) + .with_ansi(false), + ) + } + }; + + tracing_subscriber::registry() + .with(layer) + .with(filter) + .init(); + + guard }; - tracing_subscriber::registry() - .with(layer) - .with(filter) - .init(); - - info!( + ant_node::logging::info!( version = env!("CARGO_PKG_VERSION"), commit = env!("ANT_GIT_COMMIT"), "ant-node starting" @@ -95,11 +100,11 @@ async fn main() -> color_eyre::Result<()> { #[allow(clippy::collection_is_never_read)] let _activity = match platform::disable_app_nap() { Ok(activity) => { - info!("App Nap prevention enabled"); + ant_node::logging::info!("App Nap prevention enabled"); Some(activity) } Err(e) => { - warn!("Failed to disable App Nap: {e}"); + ant_node::logging::warn!("Failed to disable App Nap: {e}"); None } }; @@ -109,26 +114,26 @@ async fn main() -> color_eyre::Result<()> { match &bootstrap_source { BootstrapSource::Cli => { - info!( + ant_node::logging::info!( count = config.bootstrap.len(), "Bootstrap peers provided via CLI" ); } BootstrapSource::ConfigFile => { - info!( + ant_node::logging::info!( count = config.bootstrap.len(), "Bootstrap peers loaded from config file" ); } BootstrapSource::AutoDiscovered(path) => { - info!( + ant_node::logging::info!( count = config.bootstrap.len(), path = %path.display(), "Bootstrap peers loaded from discovered config" ); } BootstrapSource::None => { - warn!( + ant_node::logging::warn!( "No bootstrap peers configured — node will not be able to join an existing network" ); } @@ -140,6 +145,6 @@ async fn main() -> color_eyre::Result<()> { // Run until shutdown node.run().await?; - info!("Goodbye!"); + ant_node::logging::info!("Goodbye!"); Ok(()) } diff --git a/src/client/chunk_protocol.rs b/src/client/chunk_protocol.rs index 9ce0603d..ea9e50c4 100644 --- a/src/client/chunk_protocol.rs +++ b/src/client/chunk_protocol.rs @@ -4,12 +4,12 @@ //! generic function used by both [`super::QuantumClient`] and E2E test helpers. use crate::ant_protocol::{ChunkMessage, ChunkMessageBody, CHUNK_PROTOCOL_ID}; +use crate::logging::{debug, warn}; use saorsa_core::identity::PeerId; use saorsa_core::{MultiAddr, P2PEvent, P2PNode}; use std::time::Duration; use tokio::sync::broadcast::error::RecvError; use tokio::time::Instant; -use tracing::{debug, warn}; /// Send a chunk-protocol message to `target_peer` and await a matching response. /// diff --git a/src/devnet.rs b/src/devnet.rs index a702ce56..a049be6c 100644 --- a/src/devnet.rs +++ b/src/devnet.rs @@ -5,6 +5,7 @@ use crate::ant_protocol::CHUNK_PROTOCOL_ID; use crate::config::{default_root_dir, NODES_SUBDIR, NODE_IDENTITY_FILENAME}; +use crate::logging::{debug, info, warn}; use crate::payment::{ EvmVerifierConfig, PaymentVerifier, PaymentVerifierConfig, QuoteGenerator, QuotingMetricsTracker, @@ -26,7 +27,6 @@ use tokio::sync::RwLock; use tokio::task::JoinHandle; use tokio::time::Instant; use tokio_util::sync::CancellationToken; -use tracing::{debug, info, warn}; // ============================================================================= // Devnet Constants diff --git a/src/lib.rs b/src/lib.rs index 91234ebc..b7d24c24 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,6 +42,8 @@ pub mod ant_protocol; pub mod client; pub mod config; +#[macro_use] +pub mod logging; pub mod devnet; pub mod error; pub mod event; diff --git a/src/logging.rs b/src/logging.rs new file mode 100644 index 00000000..325827bf --- /dev/null +++ b/src/logging.rs @@ -0,0 +1,91 @@ +//! Logging facade that compiles to no-ops when the `logging` feature is disabled. +//! +//! When the `logging` feature is enabled, this module re-exports the +//! [`tracing`] macros (`info!`, `warn!`, `debug!`, `error!`, `trace!`). +//! +//! When disabled, all macros expand to nothing — zero binary size overhead, +//! zero runtime cost. + +// ---- Feature enabled: re-export tracing ---- +#[cfg(feature = "logging")] +pub use tracing::{debug, enabled, error, info, trace, warn, Level}; + +// ---- Feature disabled: no-op macros ---- +// +// `#[macro_export]` places macros at the crate root. We use prefixed names +// to avoid clashing with built-in attributes (e.g. `warn`), then re-export +// them here under the expected names. + +#[cfg(not(feature = "logging"))] +#[macro_export] +#[doc(hidden)] +macro_rules! __log_noop_info { + ($($arg:tt)*) => {}; +} + +#[cfg(not(feature = "logging"))] +#[macro_export] +#[doc(hidden)] +macro_rules! __log_noop_warn { + ($($arg:tt)*) => {}; +} + +#[cfg(not(feature = "logging"))] +#[macro_export] +#[doc(hidden)] +macro_rules! __log_noop_debug { + ($($arg:tt)*) => {}; +} + +#[cfg(not(feature = "logging"))] +#[macro_export] +#[doc(hidden)] +macro_rules! __log_noop_error { + ($($arg:tt)*) => {}; +} + +#[cfg(not(feature = "logging"))] +#[macro_export] +#[doc(hidden)] +macro_rules! __log_noop_trace { + ($($arg:tt)*) => {}; +} + +#[cfg(not(feature = "logging"))] +#[macro_export] +#[doc(hidden)] +macro_rules! __log_noop_enabled { + ($($arg:tt)*) => { + false + }; +} + +// Re-export under short names so `use crate::logging::info;` works. +#[cfg(not(feature = "logging"))] +pub use __log_noop_debug as debug; +#[cfg(not(feature = "logging"))] +pub use __log_noop_enabled as enabled; +#[cfg(not(feature = "logging"))] +pub use __log_noop_error as error; +#[cfg(not(feature = "logging"))] +pub use __log_noop_info as info; +#[cfg(not(feature = "logging"))] +pub use __log_noop_trace as trace; +#[cfg(not(feature = "logging"))] +pub use __log_noop_warn as warn; + +/// Stub for `tracing::Level` when logging is disabled. +#[cfg(not(feature = "logging"))] +#[allow(non_upper_case_globals, dead_code)] +pub mod Level { + /// Debug level stub. + pub const DEBUG: () = (); + /// Info level stub. + pub const INFO: () = (); + /// Warn level stub. + pub const WARN: () = (); + /// Error level stub. + pub const ERROR: () = (); + /// Trace level stub. + pub const TRACE: () = (); +} diff --git a/src/node.rs b/src/node.rs index 378474a4..4dc2a6d8 100644 --- a/src/node.rs +++ b/src/node.rs @@ -7,6 +7,7 @@ use crate::config::{ }; use crate::error::{Error, Result}; use crate::event::{create_event_channel, NodeEvent, NodeEventsChannel, NodeEventsSender}; +use crate::logging::{debug, error, info, warn}; use crate::payment::metrics::QuotingMetricsTracker; use crate::payment::wallet::parse_rewards_address; use crate::payment::{EvmVerifierConfig, PaymentVerifier, PaymentVerifierConfig, QuoteGenerator}; @@ -31,7 +32,6 @@ use std::sync::Arc; use tokio::sync::Semaphore; use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; -use tracing::{debug, error, info, warn}; #[cfg(unix)] use tokio::signal::unix::{signal, SignalKind}; diff --git a/src/payment/metrics.rs b/src/payment/metrics.rs index d2f65281..9678790a 100644 --- a/src/payment/metrics.rs +++ b/src/payment/metrics.rs @@ -5,12 +5,12 @@ //! - Storage capacity and usage //! - Network liveness information +use crate::logging::{debug, info, warn}; use evmlib::quoting_metrics::QuotingMetrics; use parking_lot::RwLock; use std::path::PathBuf; use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; use std::time::Instant; -use tracing::{debug, info, warn}; /// Number of operations between disk persists (debounce). const PERSIST_INTERVAL: usize = 10; diff --git a/src/payment/quote.rs b/src/payment/quote.rs index 4d6e616f..d3dd9f64 100644 --- a/src/payment/quote.rs +++ b/src/payment/quote.rs @@ -8,6 +8,7 @@ //! and will be fully integrated when the node is initialized. use crate::error::{Error, Result}; +use crate::logging::debug; use crate::payment::metrics::QuotingMetricsTracker; use crate::payment::pricing::calculate_price; use evmlib::merkle_payments::MerklePaymentCandidateNode; @@ -17,7 +18,6 @@ use saorsa_core::MlDsa65; use saorsa_pqc::pqc::types::{MlDsaPublicKey, MlDsaSecretKey, MlDsaSignature}; use saorsa_pqc::pqc::MlDsaOperations; use std::time::SystemTime; -use tracing::debug; /// Content address type (32-byte `XorName`). pub type XorName = [u8; 32]; @@ -155,7 +155,7 @@ impl QuoteGenerator { signature, }; - if tracing::enabled!(tracing::Level::DEBUG) { + if crate::logging::enabled!(crate::logging::Level::DEBUG) { let content_hex = hex::encode(content); debug!("Generated quote for {content_hex} (size: {data_size}, type: {data_type})"); } @@ -235,7 +235,7 @@ impl QuoteGenerator { signature, }; - if tracing::enabled!(tracing::Level::DEBUG) { + if crate::logging::enabled!(crate::logging::Level::DEBUG) { debug!( "Generated ML-DSA-65 merkle candidate quote (size: {data_size}, type: {data_type}, ts: {merkle_payment_timestamp})" ); @@ -259,7 +259,7 @@ impl QuoteGenerator { pub fn verify_quote_content(quote: &PaymentQuote, expected_content: &XorName) -> bool { // Check content matches if quote.content.0 != *expected_content { - if tracing::enabled!(tracing::Level::DEBUG) { + if crate::logging::enabled!(crate::logging::Level::DEBUG) { debug!( "Quote content mismatch: expected {}, got {}", hex::encode(expected_content), @@ -397,7 +397,7 @@ pub fn wire_ml_dsa_signer( generator.set_signer(pub_key_bytes, move |msg| match ml_dsa.sign(&sk, msg) { Ok(sig) => sig.as_bytes().to_vec(), Err(e) => { - tracing::error!("ML-DSA-65 signing failed: {e}"); + crate::logging::error!("ML-DSA-65 signing failed: {e}"); vec![] } }); diff --git a/src/payment/single_node.rs b/src/payment/single_node.rs index 433ecb5f..32a93c80 100644 --- a/src/payment/single_node.rs +++ b/src/payment/single_node.rs @@ -12,12 +12,12 @@ use crate::ant_protocol::CLOSE_GROUP_SIZE; use crate::error::{Error, Result}; +use crate::logging::info; use evmlib::common::{Amount, QuoteHash}; use evmlib::wallet::Wallet; use evmlib::Network as EvmNetwork; use evmlib::PaymentQuote; use evmlib::RewardsAddress; -use tracing::info; /// Index of the median-priced node after sorting, derived from `CLOSE_GROUP_SIZE`. const MEDIAN_INDEX: usize = CLOSE_GROUP_SIZE / 2; diff --git a/src/payment/verifier.rs b/src/payment/verifier.rs index f8c50395..7b33af4e 100644 --- a/src/payment/verifier.rs +++ b/src/payment/verifier.rs @@ -5,6 +5,7 @@ use crate::ant_protocol::CLOSE_GROUP_SIZE; use crate::error::{Error, Result}; +use crate::logging::{debug, info}; use crate::payment::cache::{CacheStats, VerifiedCache, XorName}; use crate::payment::proof::{ deserialize_merkle_proof, deserialize_proof, detect_proof_type, ProofType, @@ -22,7 +23,6 @@ use parking_lot::Mutex; use saorsa_core::identity::node_identity::peer_id_from_public_key_bytes; use std::num::NonZeroUsize; use std::time::SystemTime; -use tracing::{debug, info}; /// Minimum allowed size for a payment proof in bytes. /// @@ -162,14 +162,14 @@ impl PaymentVerifier { pub fn check_payment_required(&self, xorname: &XorName) -> PaymentStatus { // Check LRU cache (fast path) if self.cache.contains(xorname) { - if tracing::enabled!(tracing::Level::DEBUG) { + if crate::logging::enabled!(crate::logging::Level::DEBUG) { debug!("Data {} found in verified cache", hex::encode(xorname)); } return PaymentStatus::CachedAsVerified; } // Not in cache - payment required - if tracing::enabled!(tracing::Level::DEBUG) { + if crate::logging::enabled!(crate::logging::Level::DEBUG) { debug!( "Data {} not in cache - payment required", hex::encode(xorname) @@ -305,7 +305,7 @@ impl PaymentVerifier { /// the cache so `verify_payment` returns `CachedAsVerified` before /// reaching this method. async fn verify_evm_payment(&self, xorname: &XorName, payment: &ProofOfPayment) -> Result<()> { - if tracing::enabled!(tracing::Level::DEBUG) { + if crate::logging::enabled!(crate::logging::Level::DEBUG) { let xorname_hex = hex::encode(xorname); let quote_count = payment.peer_quotes.len(); debug!("Verifying EVM payment for {xorname_hex} with {quote_count} quotes"); @@ -357,7 +357,7 @@ impl PaymentVerifier { )) })?; - if tracing::enabled!(tracing::Level::INFO) { + if crate::logging::enabled!(crate::logging::Level::INFO) { let xorname_hex = hex::encode(xorname); info!("EVM payment verified for {xorname_hex} (median paid {verified_amount} atto)"); } @@ -465,7 +465,7 @@ impl PaymentVerifier { /// 5. Cache the pool hash for subsequent chunk verifications in the same batch #[allow(clippy::too_many_lines)] async fn verify_merkle_payment(&self, xorname: &XorName, proof_bytes: &[u8]) -> Result<()> { - if tracing::enabled!(tracing::Level::DEBUG) { + if crate::logging::enabled!(crate::logging::Level::DEBUG) { debug!("Verifying merkle payment for {}", hex::encode(xorname)); } @@ -648,7 +648,7 @@ impl PaymentVerifier { } } - if tracing::enabled!(tracing::Level::INFO) { + if crate::logging::enabled!(crate::logging::Level::INFO) { info!( "Merkle payment verified for {} (pool: {})", hex::encode(xorname), diff --git a/src/replication/audit.rs b/src/replication/audit.rs index 283deae4..eb09a315 100644 --- a/src/replication/audit.rs +++ b/src/replication/audit.rs @@ -6,9 +6,9 @@ use std::collections::HashMap; use std::sync::Arc; use std::time::Instant; +use crate::logging::{debug, info, warn}; use rand::seq::SliceRandom; use rand::Rng; -use tracing::{debug, info, warn}; use crate::ant_protocol::XorName; use crate::replication::config::{ReplicationConfig, REPLICATION_PROTOCOL_ID}; diff --git a/src/replication/bootstrap.rs b/src/replication/bootstrap.rs index 9ddcfbed..b4d66f9d 100644 --- a/src/replication/bootstrap.rs +++ b/src/replication/bootstrap.rs @@ -7,9 +7,9 @@ use std::collections::HashSet; use std::sync::Arc; use std::time::Duration; +use crate::logging::{debug, info, warn}; use tokio::sync::RwLock; use tokio_util::sync::CancellationToken; -use tracing::{debug, info, warn}; use saorsa_core::DhtNetworkEvent; diff --git a/src/replication/fresh.rs b/src/replication/fresh.rs index 0bd2fb0e..5bc47896 100644 --- a/src/replication/fresh.rs +++ b/src/replication/fresh.rs @@ -7,10 +7,10 @@ use std::sync::Arc; +use crate::logging::{debug, warn}; use rand::Rng; use saorsa_core::identity::PeerId; use saorsa_core::P2PNode; -use tracing::{debug, warn}; use crate::ant_protocol::XorName; use crate::replication::config::{ReplicationConfig, REPLICATION_PROTOCOL_ID}; diff --git a/src/replication/mod.rs b/src/replication/mod.rs index 853362b6..d956e00b 100644 --- a/src/replication/mod.rs +++ b/src/replication/mod.rs @@ -34,13 +34,13 @@ use std::time::{Duration, Instant}; use std::pin::Pin; +use crate::logging::{debug, error, info, warn}; use futures::stream::FuturesUnordered; use futures::{Future, StreamExt}; use rand::Rng; use tokio::sync::{mpsc, Notify, RwLock}; use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; -use tracing::{debug, error, info, warn}; use crate::ant_protocol::XorName; use crate::error::{Error, Result}; diff --git a/src/replication/neighbor_sync.rs b/src/replication/neighbor_sync.rs index da857e27..578c0cad 100644 --- a/src/replication/neighbor_sync.rs +++ b/src/replication/neighbor_sync.rs @@ -6,10 +6,10 @@ use std::sync::Arc; use std::time::{Duration, Instant}; +use crate::logging::{debug, warn}; use rand::Rng; use saorsa_core::identity::PeerId; use saorsa_core::P2PNode; -use tracing::{debug, warn}; use crate::ant_protocol::XorName; use crate::replication::config::{ReplicationConfig, REPLICATION_PROTOCOL_ID}; diff --git a/src/replication/paid_list.rs b/src/replication/paid_list.rs index 427ac33a..dd8673f6 100644 --- a/src/replication/paid_list.rs +++ b/src/replication/paid_list.rs @@ -22,6 +22,7 @@ use crate::ant_protocol::XorName; use crate::error::{Error, Result}; +use crate::logging::{debug, trace, warn}; use heed::types::Bytes; use heed::{Database, Env, EnvOpenOptions}; use parking_lot::RwLock; @@ -29,7 +30,6 @@ use std::collections::HashMap; use std::path::Path; use std::time::Instant; use tokio::task::spawn_blocking; -use tracing::{debug, trace, warn}; use crate::ant_protocol::XORNAME_LEN; diff --git a/src/replication/pruning.rs b/src/replication/pruning.rs index bda4fc39..07db6dd5 100644 --- a/src/replication/pruning.rs +++ b/src/replication/pruning.rs @@ -7,7 +7,7 @@ use std::sync::Arc; use std::time::{Duration, Instant}; -use tracing::{debug, info, warn}; +use crate::logging::{debug, info, warn}; use saorsa_core::identity::PeerId; use saorsa_core::{DHTNode, P2PNode}; diff --git a/src/replication/quorum.rs b/src/replication/quorum.rs index 8dbcf56a..b1348750 100644 --- a/src/replication/quorum.rs +++ b/src/replication/quorum.rs @@ -6,9 +6,9 @@ use std::collections::{HashMap, HashSet}; use std::sync::Arc; +use crate::logging::{debug, warn}; use saorsa_core::identity::PeerId; use saorsa_core::P2PNode; -use tracing::{debug, warn}; use crate::ant_protocol::XorName; use crate::replication::config::{ReplicationConfig, REPLICATION_PROTOCOL_ID}; diff --git a/src/replication/scheduling.rs b/src/replication/scheduling.rs index 1582fd3b..2240b286 100644 --- a/src/replication/scheduling.rs +++ b/src/replication/scheduling.rs @@ -7,7 +7,7 @@ use std::collections::{BinaryHeap, HashMap, HashSet}; use std::time::{Duration, Instant}; -use tracing::debug; +use crate::logging::debug; use crate::ant_protocol::XorName; use crate::replication::types::{FetchCandidate, VerificationEntry}; diff --git a/src/storage/handler.rs b/src/storage/handler.rs index 5eab7094..3fe69877 100644 --- a/src/storage/handler.rs +++ b/src/storage/handler.rs @@ -35,13 +35,13 @@ use crate::ant_protocol::{ }; use crate::client::compute_address; use crate::error::{Error, Result}; +use crate::logging::{debug, info, warn}; use crate::payment::{PaymentVerifier, QuoteGenerator}; use crate::replication::fresh::FreshWriteEvent; use crate::storage::lmdb::LmdbStorage; use bytes::Bytes; use std::sync::Arc; use tokio::sync::mpsc; -use tracing::{debug, info, warn}; /// ANT protocol handler. /// diff --git a/src/storage/lmdb.rs b/src/storage/lmdb.rs index e5d178ad..3b21d66a 100644 --- a/src/storage/lmdb.rs +++ b/src/storage/lmdb.rs @@ -9,13 +9,13 @@ use crate::ant_protocol::XorName; use crate::error::{Error, Result}; +use crate::logging::{debug, info, trace, warn}; use heed::types::Bytes; use heed::{Database, Env, EnvOpenOptions, MdbError}; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Instant; use tokio::task::spawn_blocking; -use tracing::{debug, info, trace, warn}; use crate::ant_protocol::XORNAME_LEN; @@ -541,7 +541,7 @@ impl LmdbStorage { key.copy_from_slice(key_bytes); keys.push(key); } else { - tracing::warn!( + crate::logging::warn!( "LmdbStorage: skipping entry with unexpected key length {} (expected {XORNAME_LEN})", key_bytes.len() ); diff --git a/src/upgrade/apply.rs b/src/upgrade/apply.rs index 27d9ba0c..9d19870a 100644 --- a/src/upgrade/apply.rs +++ b/src/upgrade/apply.rs @@ -8,6 +8,7 @@ //! 5. Restart the node process use crate::error::{Error, Result}; +use crate::logging::{debug, error, info, warn}; use crate::upgrade::binary_cache::BinaryCache; use crate::upgrade::{signature, UpgradeInfo, UpgradeResult}; use flate2::read::GzDecoder; @@ -17,7 +18,6 @@ use std::fs::{self, File}; use std::io::Read; use std::path::{Path, PathBuf}; use tar::Archive; -use tracing::{debug, error, info, warn}; /// Maximum allowed upgrade archive size (200 MiB). const MAX_ARCHIVE_SIZE_BYTES: usize = 200 * 1024 * 1024; diff --git a/src/upgrade/binary_cache.rs b/src/upgrade/binary_cache.rs index 897b0b5e..43aeb2ff 100644 --- a/src/upgrade/binary_cache.rs +++ b/src/upgrade/binary_cache.rs @@ -10,13 +10,13 @@ //! ML-DSA-65 signature verification performed during the initial download. use crate::error::{Error, Result}; +use crate::logging::{debug, warn}; use fs2::FileExt; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::fs::{self, File}; use std::io::{Read, Write}; use std::path::PathBuf; -use tracing::{debug, warn}; /// On-disk cache for downloaded upgrade binaries. #[derive(Clone)] diff --git a/src/upgrade/mod.rs b/src/upgrade/mod.rs index ab332186..5502ac43 100644 --- a/src/upgrade/mod.rs +++ b/src/upgrade/mod.rs @@ -27,10 +27,10 @@ pub use signature::{ }; use crate::error::{Error, Result}; +use crate::logging::{debug, info, warn}; use semver::Version; use std::fs; use std::path::Path; -use tracing::{debug, info, warn}; /// Maximum allowed upgrade binary size (200 MiB). /// diff --git a/src/upgrade/monitor.rs b/src/upgrade/monitor.rs index 1c61fae5..587443e4 100644 --- a/src/upgrade/monitor.rs +++ b/src/upgrade/monitor.rs @@ -9,13 +9,13 @@ use crate::config::UpgradeChannel; use crate::error::{Error, Result}; +use crate::logging::{debug, info, warn}; use crate::upgrade::release_cache::ReleaseCache; use crate::upgrade::rollout::StagedRollout; use crate::upgrade::UpgradeInfo; use semver::Version; use serde::Deserialize; use std::time::{Duration, Instant}; -use tracing::{debug, info, warn}; /// GitHub release API response. #[derive(Debug, Deserialize)] diff --git a/src/upgrade/release_cache.rs b/src/upgrade/release_cache.rs index 959de33c..af1496bc 100644 --- a/src/upgrade/release_cache.rs +++ b/src/upgrade/release_cache.rs @@ -6,6 +6,7 @@ //! first node to hit a stale cache actually contacts GitHub. use crate::error::{Error, Result}; +use crate::logging::debug; use crate::upgrade::monitor::{Asset, GitHubRelease}; use fs2::FileExt; use serde::{Deserialize, Serialize}; @@ -13,7 +14,6 @@ use std::fs::{self, File}; use std::io::Write; use std::path::PathBuf; use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use tracing::debug; /// On-disk cache for GitHub release metadata. #[derive(Clone)] diff --git a/src/upgrade/rollout.rs b/src/upgrade/rollout.rs index ef680fd0..0a391863 100644 --- a/src/upgrade/rollout.rs +++ b/src/upgrade/rollout.rs @@ -23,8 +23,8 @@ //! - Nodes are evenly distributed across the rollout window //! - The same node always upgrades at the same point in the window +use crate::logging::debug; use std::time::Duration; -use tracing::debug; /// Staged rollout configuration and delay calculation. #[derive(Debug, Clone)] diff --git a/src/upgrade/signature.rs b/src/upgrade/signature.rs index af71ef79..009aae14 100644 --- a/src/upgrade/signature.rs +++ b/src/upgrade/signature.rs @@ -18,10 +18,10 @@ //! is accepted as a reasonable trade-off for post-quantum security. use crate::error::{Error, Result}; +use crate::logging::debug; use saorsa_pqc::api::sig::{ml_dsa_65, MlDsaPublicKey, MlDsaSignature, MlDsaVariant}; use std::fs; use std::path::Path; -use tracing::debug; /// Signing context for domain separation (prevents cross-protocol attacks). pub const SIGNING_CONTEXT: &[u8] = b"ant-node-release-v1"; From 7b1fad71954ee35a9aa755fbb0e0ac5a1cfd8119 Mon Sep 17 00:00:00 2001 From: grumbach Date: Fri, 3 Apr 2026 14:49:45 +0900 Subject: [PATCH 02/11] ci: strip logging from release binaries via --no-default-features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update CI/release workflows and deployment scripts to build release binaries with --no-default-features, which excludes the logging feature flag. Tests retain logging (default features) for debugging visibility. Affected: - .github/workflows/ci.yml — CI build job - .github/workflows/release.yml — cross and native release builds - scripts/testnet/build-and-deploy.sh — production deployment - scripts/testnet/testnet-endurance.sh — endurance test binary --- .github/workflows/ci.yml | 4 ++-- .github/workflows/release.yml | 8 ++++---- scripts/testnet/build-and-deploy.sh | 4 ++-- scripts/testnet/testnet-endurance.sh | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9459f04..cef14317 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,8 +75,8 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - - name: Build release - run: cargo build --release + - name: Build release (no logging) + run: cargo build --release --no-default-features security: name: Security Audit diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 25301234..239fa384 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -157,13 +157,13 @@ jobs: if: matrix.cross run: cargo install cross --git https://github.com/cross-rs/cross - - name: Build (cross) + - name: Build (cross, no logging) if: matrix.cross - run: cross build --release --target ${{ matrix.target }} + run: cross build --release --no-default-features --target ${{ matrix.target }} - - name: Build (native) + - name: Build (native, no logging) if: ${{ !matrix.cross }} - run: cargo build --release --target ${{ matrix.target }} + run: cargo build --release --no-default-features --target ${{ matrix.target }} - name: Create archive (Unix) if: matrix.archive == 'tar.gz' diff --git a/scripts/testnet/build-and-deploy.sh b/scripts/testnet/build-and-deploy.sh index bf3ac2b0..62f2d5cf 100755 --- a/scripts/testnet/build-and-deploy.sh +++ b/scripts/testnet/build-and-deploy.sh @@ -54,8 +54,8 @@ ssh -o StrictHostKeyChecking=no "root@${BUILD_HOST}" " cd /root/ant-node fi - # Build release binary - cargo build --release + # Build release binary (no logging for production deployment) + cargo build --release --no-default-features # Verify binary ls -la target/release/ant-node diff --git a/scripts/testnet/testnet-endurance.sh b/scripts/testnet/testnet-endurance.sh index 0e318c63..672d09dd 100755 --- a/scripts/testnet/testnet-endurance.sh +++ b/scripts/testnet/testnet-endurance.sh @@ -30,9 +30,9 @@ echo "Log file: ${LOG_FILE}" | tee -a "$LOG_FILE" echo "Start time: $(date)" | tee -a "$LOG_FILE" echo "" | tee -a "$LOG_FILE" -# Build the test binary if needed -echo "Building test binary..." | tee -a "$LOG_FILE" -cargo build --release 2>&1 | tee -a "$LOG_FILE" +# Build the binary (no logging for deployed nodes) +echo "Building binary..." | tee -a "$LOG_FILE" +cargo build --release --no-default-features 2>&1 | tee -a "$LOG_FILE" # Export test configuration export ANT_TEST_BOOTSTRAP="$BOOTSTRAP" From f31e55f13f5e184553a213b2c65f86a2dab5a17f Mon Sep 17 00:00:00 2001 From: grumbach Date: Fri, 3 Apr 2026 14:52:11 +0900 Subject: [PATCH 03/11] fix: restore default=["logging"] and revert --no-default-features approach Keep logging in default features so dev builds are ergonomic. Release builds explicitly use --no-default-features to strip logging. --- Cargo.toml | 2 +- scripts/testnet/build-and-deploy.sh | 2 +- scripts/testnet/testnet-endurance.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ad92fb62..85b08c54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -122,7 +122,7 @@ required-features = ["test-utils"] default = ["logging"] # Enable tracing/logging infrastructure. # Included in `default` so dev builds (`cargo build`, `cargo test`) get logging -# automatically. For zero-overhead release builds, compile with: +# automatically. Release builds strip it: # cargo build --release --no-default-features logging = ["tracing", "tracing-subscriber", "tracing-appender"] # Enable additional diagnostics for development diff --git a/scripts/testnet/build-and-deploy.sh b/scripts/testnet/build-and-deploy.sh index 62f2d5cf..f1b4be66 100755 --- a/scripts/testnet/build-and-deploy.sh +++ b/scripts/testnet/build-and-deploy.sh @@ -54,7 +54,7 @@ ssh -o StrictHostKeyChecking=no "root@${BUILD_HOST}" " cd /root/ant-node fi - # Build release binary (no logging for production deployment) + # Build release binary (no logging) cargo build --release --no-default-features # Verify binary diff --git a/scripts/testnet/testnet-endurance.sh b/scripts/testnet/testnet-endurance.sh index 672d09dd..88e5bda5 100755 --- a/scripts/testnet/testnet-endurance.sh +++ b/scripts/testnet/testnet-endurance.sh @@ -30,7 +30,7 @@ echo "Log file: ${LOG_FILE}" | tee -a "$LOG_FILE" echo "Start time: $(date)" | tee -a "$LOG_FILE" echo "" | tee -a "$LOG_FILE" -# Build the binary (no logging for deployed nodes) +# Build the binary (no logging) echo "Building binary..." | tee -a "$LOG_FILE" cargo build --release --no-default-features 2>&1 | tee -a "$LOG_FILE" From a3a643bc761532b20d85607edb7490bb464ae368 Mon Sep 17 00:00:00 2001 From: grumbach Date: Fri, 3 Apr 2026 15:10:28 +0900 Subject: [PATCH 04/11] fix: address review findings from multi-agent consensus review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix `pub mod Level` → `pub struct Level` to avoid non_snake_case lint - Remove unnecessary `#[macro_use]` on logging module - Extract `init_logging()` from main() to fix too_many_lines clippy lint - No-op macros now expand to `()` instead of `{}` for expression-position safety - Replace `eprintln!` in `BootstrapPeersConfig::discover()` with `crate::logging::warn!` to prevent stderr leaks in no-logging builds - Fix direct indexing `candidate_prices[len/2]` → safe `.get()` in merkle payment verifier (CDX-002) - Add `#[allow(clippy::manual_unwrap_or_default)]` where match arms intentionally log errors - Override RUSTFLAGS in CI/release no-default-features builds to allow expected unused-variable warnings from compiled-out log macros --- .github/workflows/ci.yml | 4 + .github/workflows/release.yml | 4 + src/bin/ant-node/main.rs | 134 +++++++++++++++++----------------- src/config.rs | 6 +- src/lib.rs | 3 +- src/logging.rs | 38 +++++++--- src/payment/verifier.rs | 4 +- src/storage/handler.rs | 4 + 8 files changed, 112 insertions(+), 85 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cef14317..73fb7bbe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,6 +77,10 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: Build release (no logging) run: cargo build --release --no-default-features + env: + # When logging is compiled out, variables used only in log macros + # become unused. These warnings are expected and harmless. + RUSTFLAGS: "-A unused_variables -A unused_assignments -A dead_code" security: name: Security Audit diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 239fa384..224b652f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -160,10 +160,14 @@ jobs: - name: Build (cross, no logging) if: matrix.cross run: cross build --release --no-default-features --target ${{ matrix.target }} + env: + RUSTFLAGS: "-A unused_variables -A unused_assignments -A dead_code" - name: Build (native, no logging) if: ${{ !matrix.cross }} run: cargo build --release --no-default-features --target ${{ matrix.target }} + env: + RUSTFLAGS: "-A unused_variables -A unused_assignments -A dead_code" - name: Create archive (Unix) if: matrix.archive == 'tar.gz' diff --git a/src/bin/ant-node/main.rs b/src/bin/ant-node/main.rs index db1c15bb..a06227bb 100644 --- a/src/bin/ant-node/main.rs +++ b/src/bin/ant-node/main.rs @@ -8,8 +8,72 @@ use ant_node::NodeBuilder; use clap::Parser; use cli::Cli; +/// Initialize the tracing subscriber when the `logging` feature is active. +/// +/// Returns a guard that must be held for the lifetime of the process to ensure +/// buffered logs are flushed on shutdown. #[cfg(feature = "logging")] -use cli::CliLogFormat; +fn init_logging( + cli: &Cli, +) -> color_eyre::Result> { + use cli::CliLogFormat; + use tracing_subscriber::prelude::*; + use tracing_subscriber::{fmt, EnvFilter, Layer}; + + let log_format = cli.log_format; + let log_dir = cli.log_dir.clone(); + let log_max_files = cli.log_max_files; + let log_level: String = cli.log_level.into(); + let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&log_level)); + + let guard: Option; + + let layer: Box + Send + Sync> = match (log_format, log_dir) { + (CliLogFormat::Text, None) => { + guard = None; + Box::new(fmt::layer()) + } + (CliLogFormat::Json, None) => { + guard = None; + Box::new(fmt::layer().json().flatten_event(true)) + } + (CliLogFormat::Text, Some(dir)) => { + let file_appender = tracing_appender::rolling::Builder::new() + .rotation(tracing_appender::rolling::Rotation::DAILY) + .max_log_files(log_max_files) + .filename_prefix("ant-node") + .filename_suffix("log") + .build(dir)?; + let (non_blocking, g) = tracing_appender::non_blocking(file_appender); + guard = Some(g); + Box::new(fmt::layer().with_writer(non_blocking).with_ansi(false)) + } + (CliLogFormat::Json, Some(dir)) => { + let file_appender = tracing_appender::rolling::Builder::new() + .rotation(tracing_appender::rolling::Rotation::DAILY) + .max_log_files(log_max_files) + .filename_prefix("ant-node") + .filename_suffix("log") + .build(dir)?; + let (non_blocking, g) = tracing_appender::non_blocking(file_appender); + guard = Some(g); + Box::new( + fmt::layer() + .json() + .flatten_event(true) + .with_writer(non_blocking) + .with_ansi(false), + ) + } + }; + + tracing_subscriber::registry() + .with(layer) + .with(filter) + .init(); + + Ok(guard) +} /// Force at least 4 worker threads regardless of CPU count. /// @@ -20,74 +84,13 @@ use cli::CliLogFormat; /// keepalives can't fire, and connections die silently. #[tokio::main(flavor = "multi_thread", worker_threads = 4)] async fn main() -> color_eyre::Result<()> { - // Initialize error handling color_eyre::install()?; - // Parse CLI arguments let cli = Cli::parse(); - // Initialize tracing (only when logging feature is enabled) + // _guard must live for the duration of main() to ensure log flushing. #[cfg(feature = "logging")] - let _logging_guard = { - use tracing_subscriber::prelude::*; - use tracing_subscriber::{fmt, EnvFilter, Layer}; - - let log_format = cli.log_format; - let log_dir = cli.log_dir.clone(); - let log_max_files = cli.log_max_files; - let log_level: String = cli.log_level.into(); - let filter = - EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&log_level)); - - // _guard must live for the duration of main() to ensure log flushing. - let guard: Option; - - let layer: Box + Send + Sync> = match (log_format, log_dir) { - (CliLogFormat::Text, None) => { - guard = None; - Box::new(fmt::layer()) - } - (CliLogFormat::Json, None) => { - guard = None; - Box::new(fmt::layer().json().flatten_event(true)) - } - (CliLogFormat::Text, Some(dir)) => { - let file_appender = tracing_appender::rolling::Builder::new() - .rotation(tracing_appender::rolling::Rotation::DAILY) - .max_log_files(log_max_files) - .filename_prefix("ant-node") - .filename_suffix("log") - .build(dir)?; - let (non_blocking, g) = tracing_appender::non_blocking(file_appender); - guard = Some(g); - Box::new(fmt::layer().with_writer(non_blocking).with_ansi(false)) - } - (CliLogFormat::Json, Some(dir)) => { - let file_appender = tracing_appender::rolling::Builder::new() - .rotation(tracing_appender::rolling::Rotation::DAILY) - .max_log_files(log_max_files) - .filename_prefix("ant-node") - .filename_suffix("log") - .build(dir)?; - let (non_blocking, g) = tracing_appender::non_blocking(file_appender); - guard = Some(g); - Box::new( - fmt::layer() - .json() - .flatten_event(true) - .with_writer(non_blocking) - .with_ansi(false), - ) - } - }; - - tracing_subscriber::registry() - .with(layer) - .with(filter) - .init(); - - guard - }; + let _logging_guard = init_logging(&cli)?; ant_node::logging::info!( version = env!("CARGO_PKG_VERSION"), @@ -139,10 +142,7 @@ async fn main() -> color_eyre::Result<()> { } } - // Build and run the node let mut node = NodeBuilder::new(config).build().await?; - - // Run until shutdown node.run().await?; ant_node::logging::info!("Goodbye!"); diff --git a/src/config.rs b/src/config.rs index b99e3d25..ac8c81ba 100644 --- a/src/config.rs +++ b/src/config.rs @@ -555,9 +555,9 @@ impl BootstrapPeersConfig { match Self::from_file(&path) { Ok(config) if !config.peers.is_empty() => return Some((config, path)), Ok(_) => {} - Err(err) => { - eprintln!( - "Warning: failed to load bootstrap peers from {}: {err}", + Err(_err) => { + crate::logging::warn!( + "Failed to load bootstrap peers from {}", path.display(), ); } diff --git a/src/lib.rs b/src/lib.rs index b7d24c24..f8d50aab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,11 +42,10 @@ pub mod ant_protocol; pub mod client; pub mod config; -#[macro_use] -pub mod logging; pub mod devnet; pub mod error; pub mod event; +pub mod logging; pub mod node; pub mod payment; pub mod replication; diff --git a/src/logging.rs b/src/logging.rs index 325827bf..23920f20 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -20,35 +20,45 @@ pub use tracing::{debug, enabled, error, info, trace, warn, Level}; #[macro_export] #[doc(hidden)] macro_rules! __log_noop_info { - ($($arg:tt)*) => {}; + ($($arg:tt)*) => { + () + }; } #[cfg(not(feature = "logging"))] #[macro_export] #[doc(hidden)] macro_rules! __log_noop_warn { - ($($arg:tt)*) => {}; + ($($arg:tt)*) => { + () + }; } #[cfg(not(feature = "logging"))] #[macro_export] #[doc(hidden)] macro_rules! __log_noop_debug { - ($($arg:tt)*) => {}; + ($($arg:tt)*) => { + () + }; } #[cfg(not(feature = "logging"))] #[macro_export] #[doc(hidden)] macro_rules! __log_noop_error { - ($($arg:tt)*) => {}; + ($($arg:tt)*) => { + () + }; } #[cfg(not(feature = "logging"))] #[macro_export] #[doc(hidden)] macro_rules! __log_noop_trace { - ($($arg:tt)*) => {}; + ($($arg:tt)*) => { + () + }; } #[cfg(not(feature = "logging"))] @@ -76,16 +86,20 @@ pub use __log_noop_warn as warn; /// Stub for `tracing::Level` when logging is disabled. #[cfg(not(feature = "logging"))] -#[allow(non_upper_case_globals, dead_code)] -pub mod Level { +#[allow(dead_code)] +pub struct Level; + +#[cfg(not(feature = "logging"))] +#[allow(dead_code)] +impl Level { /// Debug level stub. - pub const DEBUG: () = (); + pub const DEBUG: Self = Self; /// Info level stub. - pub const INFO: () = (); + pub const INFO: Self = Self; /// Warn level stub. - pub const WARN: () = (); + pub const WARN: Self = Self; /// Error level stub. - pub const ERROR: () = (); + pub const ERROR: Self = Self; /// Trace level stub. - pub const TRACE: () = (); + pub const TRACE: Self = Self; } diff --git a/src/payment/verifier.rs b/src/payment/verifier.rs index 7b33af4e..8d81d2c3 100644 --- a/src/payment/verifier.rs +++ b/src/payment/verifier.rs @@ -599,7 +599,9 @@ impl PaymentVerifier { .collect(); candidate_prices.sort_unstable(); // ascending // Upper median (index 8 of 16) — matches Solidity's median16 (k = 8) - let median_price = candidate_prices[candidate_prices.len() / 2]; + let median_price = *candidate_prices + .get(candidate_prices.len() / 2) + .ok_or_else(|| Error::Payment("empty candidate pool in merkle proof".into()))?; let total_amount = median_price * Amount::from(1u64 << payment_info.depth); total_amount / Amount::from(u64::from(payment_info.depth)) } else { diff --git a/src/storage/handler.rs b/src/storage/handler.rs index 3fe69877..5d6604fe 100644 --- a/src/storage/handler.rs +++ b/src/storage/handler.rs @@ -280,6 +280,10 @@ impl AntProtocol { // Check if the chunk is already stored so we can tell the client // to skip payment (already_stored = true). + // The match intentionally logs the error when the `logging` feature is + // active. Clippy suggests `unwrap_or_default()` when logging is compiled + // out, but keeping the explicit match preserves the diagnostic intent. + #[allow(clippy::manual_unwrap_or_default)] let already_stored = match self.storage.exists(&request.address) { Ok(exists) => exists, Err(e) => { From f50c522a3b9d49c593da5362b76761f194cf1488 Mon Sep 17 00:00:00 2001 From: grumbach Date: Fri, 3 Apr 2026 15:29:19 +0900 Subject: [PATCH 05/11] fix: prevent shift overflow panic in merkle payment verifier Use checked_shl() instead of raw << on attacker-controlled depth value from on-chain data. A depth >= 64 would panic (or abort in release), making this a remotely-triggerable crash via crafted merkle proof. --- src/payment/verifier.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/payment/verifier.rs b/src/payment/verifier.rs index 8d81d2c3..324ff3f0 100644 --- a/src/payment/verifier.rs +++ b/src/payment/verifier.rs @@ -602,7 +602,11 @@ impl PaymentVerifier { let median_price = *candidate_prices .get(candidate_prices.len() / 2) .ok_or_else(|| Error::Payment("empty candidate pool in merkle proof".into()))?; - let total_amount = median_price * Amount::from(1u64 << payment_info.depth); + let shift = u32::from(payment_info.depth); + let multiplier = 1u64 + .checked_shl(shift) + .ok_or_else(|| Error::Payment("merkle proof depth too large".into()))?; + let total_amount = median_price * Amount::from(multiplier); total_amount / Amount::from(u64::from(payment_info.depth)) } else { Amount::ZERO From 041553d0e911e0261b5ded78f1e6fead3686f2bd Mon Sep 17 00:00:00 2001 From: grumbach Date: Fri, 3 Apr 2026 15:51:03 +0900 Subject: [PATCH 06/11] fix: replace global RUSTFLAGS overrides with targeted cfg_attr allows Instead of suppressing warnings via RUSTFLAGS in CI, use crate-level cfg_attr to allow unused_variables and unused_assignments only when the logging feature is disabled. This is self-documenting and scoped: #![cfg_attr(not(feature = "logging"), allow(unused_variables, unused_assignments))] Applied to lib.rs (covers the library) and both binary entry points. Removed all RUSTFLAGS overrides from ci.yml and release.yml. --- .github/workflows/ci.yml | 4 ---- .github/workflows/release.yml | 4 ---- src/bin/ant-devnet/main.rs | 2 ++ src/bin/ant-node/main.rs | 2 ++ src/lib.rs | 3 +++ 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73fb7bbe..cef14317 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,10 +77,6 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: Build release (no logging) run: cargo build --release --no-default-features - env: - # When logging is compiled out, variables used only in log macros - # become unused. These warnings are expected and harmless. - RUSTFLAGS: "-A unused_variables -A unused_assignments -A dead_code" security: name: Security Audit diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 224b652f..239fa384 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -160,14 +160,10 @@ jobs: - name: Build (cross, no logging) if: matrix.cross run: cross build --release --no-default-features --target ${{ matrix.target }} - env: - RUSTFLAGS: "-A unused_variables -A unused_assignments -A dead_code" - name: Build (native, no logging) if: ${{ !matrix.cross }} run: cargo build --release --no-default-features --target ${{ matrix.target }} - env: - RUSTFLAGS: "-A unused_variables -A unused_assignments -A dead_code" - name: Create archive (Unix) if: matrix.archive == 'tar.gz' diff --git a/src/bin/ant-devnet/main.rs b/src/bin/ant-devnet/main.rs index 39e2a18c..d8c9fc87 100644 --- a/src/bin/ant-devnet/main.rs +++ b/src/bin/ant-devnet/main.rs @@ -1,5 +1,7 @@ //! ant-devnet CLI entry point. +#![cfg_attr(not(feature = "logging"), allow(unused_variables))] + mod cli; use ant_node::devnet::{Devnet, DevnetConfig, DevnetEvmInfo, DevnetManifest}; diff --git a/src/bin/ant-node/main.rs b/src/bin/ant-node/main.rs index a06227bb..30560e50 100644 --- a/src/bin/ant-node/main.rs +++ b/src/bin/ant-node/main.rs @@ -1,5 +1,7 @@ //! ant-node CLI entry point. +#![cfg_attr(not(feature = "logging"), allow(unused_variables))] + mod cli; mod platform; diff --git a/src/lib.rs b/src/lib.rs index f8d50aab..bcad4850 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,6 +38,9 @@ #![warn(missing_docs)] #![warn(clippy::all)] #![warn(clippy::pedantic)] +// When the `logging` feature is off, variables used only in log macros become +// unused. These are expected and harmless — the compiler optimises them away. +#![cfg_attr(not(feature = "logging"), allow(unused_variables, unused_assignments))] pub mod ant_protocol; pub mod client; From f2e021d501c8a80a83fc4777902e98817080c863 Mon Sep 17 00:00:00 2001 From: grumbach Date: Fri, 3 Apr 2026 15:55:25 +0900 Subject: [PATCH 07/11] fix: include error details in bootstrap peers warning Restore the error message in the bootstrap peers discovery warning so failures are diagnosable when logging is enabled. --- src/config.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config.rs b/src/config.rs index ac8c81ba..a9f569c3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -555,9 +555,9 @@ impl BootstrapPeersConfig { match Self::from_file(&path) { Ok(config) if !config.peers.is_empty() => return Some((config, path)), Ok(_) => {} - Err(_err) => { + Err(err) => { crate::logging::warn!( - "Failed to load bootstrap peers from {}", + "Failed to load bootstrap peers from {}: {err}", path.display(), ); } From 2539595ef013abc3f9c6a21e17e3596ba4b370fc Mon Sep 17 00:00:00 2001 From: grumbach Date: Fri, 3 Apr 2026 15:59:03 +0900 Subject: [PATCH 08/11] docs: document unused-variable tradeoff in logging facade Add module-level doc explaining that log-only variables become unused when macros are compiled out, and how the crate-level cfg_attr handles it. --- src/logging.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/logging.rs b/src/logging.rs index 23920f20..d3cd9e36 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -3,8 +3,16 @@ //! When the `logging` feature is enabled, this module re-exports the //! [`tracing`] macros (`info!`, `warn!`, `debug!`, `error!`, `trace!`). //! -//! When disabled, all macros expand to nothing — zero binary size overhead, -//! zero runtime cost. +//! When disabled, all macros expand to `()` — zero binary size overhead, +//! zero runtime cost. Argument expressions are **not evaluated**, so +//! `info!("{}", expensive_call())` costs nothing in release builds. +//! +//! ## Unused variables with `--no-default-features` +//! +//! Variables that exist only for logging (e.g. `let addr_hex = hex::encode(...)`) +//! become unused when macros are compiled out. The crate-level attribute +//! `#![cfg_attr(not(feature = "logging"), allow(unused_variables, unused_assignments))]` +//! in `lib.rs` (and the binary entry points) suppresses these expected warnings. // ---- Feature enabled: re-export tracing ---- #[cfg(feature = "logging")] From 8f2c7d63821290c881620daa7dc98a6f669466f1 Mon Sep 17 00:00:00 2001 From: grumbach Date: Fri, 3 Apr 2026 16:00:06 +0900 Subject: [PATCH 09/11] ci: add test job for --no-default-features (no logging) path Run unit tests without the logging feature to catch regressions in the release configuration where all log macros are compiled out. --- .github/workflows/ci.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cef14317..899b6277 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,6 +78,16 @@ jobs: - name: Build release (no logging) run: cargo build --release --no-default-features + test-no-logging: + name: Test (no logging) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Run unit tests without logging + run: cargo test --lib --no-default-features + security: name: Security Audit runs-on: ubuntu-latest From 72938d08b5484ace25ff9ef000e33be96e5e6f84 Mon Sep 17 00:00:00 2001 From: grumbach Date: Fri, 3 Apr 2026 16:12:29 +0900 Subject: [PATCH 10/11] fix: install Foundry in test-no-logging CI job The single_node payment tests spawn Anvil for EVM verification. Without Foundry installed the tests fail with "No such file or directory". --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 899b6277..3c26f278 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,6 +85,10 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly - name: Run unit tests without logging run: cargo test --lib --no-default-features From bc5057477c3434ebc4978c23a6fecaa259760230 Mon Sep 17 00:00:00 2001 From: Chris O'Neil Date: Fri, 3 Apr 2026 13:00:48 +0100 Subject: [PATCH 11/11] ci: use debug builds for RC pre-releases to retain logging RC releases need logging for validation and debugging before a full release is cut. This changes the release workflow to build without --release --no-default-features when the tag has an RC/alpha/beta suffix, producing debug binaries with the logging feature enabled. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/release.yml | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 239fa384..ce8bf4e4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -144,6 +144,20 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Determine build profile + id: profile + shell: bash + run: | + if [[ "${{ needs.validate.outputs.is_prerelease }}" == "true" ]]; then + echo "build_flags=" >> $GITHUB_OUTPUT + echo "profile_dir=debug" >> $GITHUB_OUTPUT + echo "Building DEBUG (RC pre-release: logging enabled)" + else + echo "build_flags=--release --no-default-features" >> $GITHUB_OUTPUT + echo "profile_dir=release" >> $GITHUB_OUTPUT + echo "Building RELEASE (logging stripped)" + fi + - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable with: @@ -157,19 +171,19 @@ jobs: if: matrix.cross run: cargo install cross --git https://github.com/cross-rs/cross - - name: Build (cross, no logging) + - name: Build (cross) if: matrix.cross - run: cross build --release --no-default-features --target ${{ matrix.target }} + run: cross build ${{ steps.profile.outputs.build_flags }} --target ${{ matrix.target }} - - name: Build (native, no logging) + - name: Build (native) if: ${{ !matrix.cross }} - run: cargo build --release --no-default-features --target ${{ matrix.target }} + run: cargo build ${{ steps.profile.outputs.build_flags }} --target ${{ matrix.target }} - name: Create archive (Unix) if: matrix.archive == 'tar.gz' run: | - cp config/bootstrap_peers.toml target/${{ matrix.target }}/release/ - cd target/${{ matrix.target }}/release + cp config/bootstrap_peers.toml target/${{ matrix.target }}/${{ steps.profile.outputs.profile_dir }}/ + cd target/${{ matrix.target }}/${{ steps.profile.outputs.profile_dir }} tar -czvf ../../../ant-node-cli-${{ matrix.friendly_name }}.tar.gz ${{ matrix.binary }} bootstrap_peers.toml cd ../../.. @@ -177,8 +191,8 @@ jobs: if: matrix.archive == 'zip' shell: pwsh run: | - Copy-Item "config/bootstrap_peers.toml" "target/${{ matrix.target }}/release/bootstrap_peers.toml" - Push-Location "target/${{ matrix.target }}/release" + Copy-Item "config/bootstrap_peers.toml" "target/${{ matrix.target }}/${{ steps.profile.outputs.profile_dir }}/bootstrap_peers.toml" + Push-Location "target/${{ matrix.target }}/${{ steps.profile.outputs.profile_dir }}" Compress-Archive -Path "${{ matrix.binary }}", "bootstrap_peers.toml" -DestinationPath "../../../ant-node-cli-${{ matrix.friendly_name }}.zip" Pop-Location