diff --git a/crates/starknet_transaction_prover/README.md b/crates/starknet_transaction_prover/README.md index c26ef87a749..e67bc136b66 100644 --- a/crates/starknet_transaction_prover/README.md +++ b/crates/starknet_transaction_prover/README.md @@ -256,9 +256,8 @@ docker run -e LOG_FORMAT=json ... and `span` keys. The `text` format may include ANSI colour codes. `json` never does, so use `--log-format json` in containers and in production. A URL can carry credentials in its userinfo component, so the service redacts every logged URL down to `scheme://host[:port]`. That covers -`rpc_node_url` in the startup logs and in its CLI-override message, and `blocking_check_url` in its -CLI-override message. The startup logs say only whether the blocking check is enabled, never its -URL. +`rpc_node_url` and `blocking_check_url` in the startup `config_resolved` log, and each of them +again in its own CLI-override message. ## Compression @@ -371,6 +370,21 @@ there was never a status. The logging layer never reads request bodies. Transaction calldata is private user data and stays out of the logs. +### Startup logs + +Startup emits two `info` logs, plus an `OHTTP envelope encryption enabled` line between them when +OHTTP is enabled. Before binding, an `event="config_resolved"` log records the build identity +(version, git SHA) together with the resolved service and prover settings, which are the merge of +the config file, environment variables, and CLI flags. Read that line to see the values the process +runs with, instead of working the precedence rules out by hand. The `contract_class_manager_config` +and `runner_config` sub-configs are not included. After the listener is bound, a second log records +what the server came up on: local address, scheme (`http` or `https`), `max_concurrent_requests`, +`max_connections`, `ohttp_enabled`, and the CORS mode and allowed origins. + +Both lines are redacted. The service logs the RPC and blocking-check URLs host-only (see +[Logging](#logging)), never logs TLS certificate and key paths, and puts no transaction-scoped data +in either line. + ### Shutdown `SIGTERM` and `SIGINT` start a graceful shutdown. The server stops accepting new requests and lets diff --git a/crates/starknet_transaction_prover/src/main.rs b/crates/starknet_transaction_prover/src/main.rs index 835fe502450..1b8a9366758 100644 --- a/crates/starknet_transaction_prover/src/main.rs +++ b/crates/starknet_transaction_prover/src/main.rs @@ -23,7 +23,6 @@ async fn main() -> anyhow::Result<()> { }; use starknet_transaction_prover::server::cors::{build_cors_layer, cors_mode}; use starknet_transaction_prover::server::health::HealthLayer; - use starknet_transaction_prover::server::log_redact::redact_url_host; use starknet_transaction_prover::server::metrics::{install_exporter, spawn_upkeep}; use starknet_transaction_prover::server::panic::install_panic_hook; use starknet_transaction_prover::server::rpc_api::ProvingRpcServer; @@ -60,6 +59,8 @@ async fn main() -> anyhow::Result<()> { let config = ServiceConfig::from_args(args)?; + config.log_startup_summary(); + // Install the Prometheus exporter and emit `prover_build_info` before binding, so a scrape // during a slow startup still returns the build identity. let prometheus_handle = @@ -68,20 +69,6 @@ async fn main() -> anyhow::Result<()> { let metrics_layer = MetricsLayer::new(prometheus_handle.clone()); spawn_upkeep(prometheus_handle); - // Startup banner — version + chain id + redacted RPC host only. No URLs - // with userinfo, no fee token address, no TLS paths, no tx data. - info!( - version = env!("CARGO_PKG_VERSION"), - git_sha = option_env!("GIT_SHA").unwrap_or("unknown"), - chain_id = %config.prover_config.chain_id, - rpc_node_host = %redact_url_host(&config.prover_config.rpc_node_url), - validate_zero_fee_fields = config.prover_config.validate_zero_fee_fields, - blocking_check_enabled = config.prover_config.blocking_check_url.is_some(), - blocking_check_fail_open = config.prover_config.blocking_check_fail_open, - ohttp_enabled = config.ohttp_enabled, - "Starting Starknet transaction prover." - ); - // Build and start the JSON-RPC server. The request path and the health probe share one // saturation monitor. The request path records rejects and worker-slot progress, and the // probe reads it. diff --git a/crates/starknet_transaction_prover/src/server/config.rs b/crates/starknet_transaction_prover/src/server/config.rs index 17b1581a34f..85eaaa90b6e 100644 --- a/crates/starknet_transaction_prover/src/server/config.rs +++ b/crates/starknet_transaction_prover/src/server/config.rs @@ -502,6 +502,48 @@ impl ServiceConfig { health_max_saturated_ms: config.health_max_saturated_ms, }) } + + /// Logs the resolved service and prover settings (file + env + CLI merge) once at startup. + /// The `contract_class_manager_config` and `runner_config` sub-configs are not included. + /// + /// It logs the RPC and blocking-check URLs host-only, and never logs the TLS key and + /// certificate paths. + pub fn log_startup_summary(&self) { + let version = env!("CARGO_PKG_VERSION"); + let git_sha = option_env!("GIT_SHA").unwrap_or("unknown"); + let transport_label = match self.transport { + TransportMode::Http => "http", + TransportMode::Https { .. } => "https", + }; + let blocking_check_host = + self.prover_config.blocking_check_url.as_deref().map(redact_url_host); + info!( + event = "config_resolved", + version, + git_sha, + ip = %self.ip, + port = self.port, + transport = transport_label, + max_concurrent_requests = self.max_concurrent_requests, + max_queued_requests = self.max_queued_requests, + queue_wait_timeout_millis = self.queue_wait_timeout_millis, + max_connections = self.max_connections, + max_request_body_size = self.max_request_body_size, + cors_allow_origin = ?self.cors_allow_origin, + ohttp_enabled = self.ohttp_enabled, + ohttp_key_cache_max_age_secs = self.ohttp_key_cache_max_age_secs, + health_max_saturated_ms = self.health_max_saturated_ms, + chain_id = %self.prover_config.chain_id, + rpc_node_host = %redact_url_host(&self.prover_config.rpc_node_url), + strk_fee_token_address = ?self.prover_config.strk_fee_token_address, + validate_zero_fee_fields = self.prover_config.validate_zero_fee_fields, + blocking_check_enabled = self.prover_config.blocking_check_url.is_some(), + blocking_check_host = ?blocking_check_host, + blocking_check_timeout_millis = self.prover_config.blocking_check_timeout_millis, + blocking_check_fail_open = self.prover_config.blocking_check_fail_open, + "Starting Starknet transaction prover." + ); + } } /// CLI arguments for the proving service. diff --git a/crates/starknet_transaction_prover/src/server/config_test.rs b/crates/starknet_transaction_prover/src/server/config_test.rs index df77fe80e30..9e34a1b640d 100644 --- a/crates/starknet_transaction_prover/src/server/config_test.rs +++ b/crates/starknet_transaction_prover/src/server/config_test.rs @@ -5,6 +5,7 @@ use std::sync::{Mutex, MutexGuard}; use clap::Parser; use rstest::rstest; use tempfile::NamedTempFile; +use tracing_test::traced_test; use crate::errors::ConfigError; use crate::server::config::{CliArgs, LogFormat, ServiceConfig, TransportMode}; @@ -344,3 +345,27 @@ fn env_var_sets_tls_key_file() { assert_eq!(args.tls_key_file, Some(PathBuf::from("/etc/ssl/key.pem"))); } + +/// The startup summary is the only place that writes the resolved service and +/// prover settings to the log stream, so its redaction is what keeps credentials +/// out of the operator's log aggregator. +#[test] +#[traced_test] +fn startup_summary_logs_hosts_and_redacts_url_credentials() { + let mut args = base_args(); + args.rpc_url = Some("https://user:sekret@rpc.example.com:8545/v2/api-key".to_string()); + args.blocking_check_url = Some("https://ops:hunter2@screen.example.com/check".to_string()); + let config = ServiceConfig::from_args(args).unwrap(); + + config.log_startup_summary(); + + assert!(logs_contain("event=\"config_resolved\""), "must tag the event for log-based checks"); + assert!(logs_contain("rpc.example.com"), "the host is operational context, so it is logged"); + assert!( + logs_contain("screen.example.com"), + "the blocking-check host is operational context, safe to log" + ); + for secret in ["sekret", "hunter2", "api-key"] { + assert!(!logs_contain(secret), "`{secret}` from a URL must never reach the log stream"); + } +}