Skip to main content

tempo/
defaults.rs

1use base64::{Engine, prelude::BASE64_STANDARD};
2use eyre::Context as _;
3use jiff::SignedDuration;
4use reth_cli_commands::download::DownloadDefaults;
5use reth_ethereum::node::core::args::{
6    DefaultDiscoveryArgs, DefaultEngineValues, DefaultNetworkArgs, DefaultPayloadBuilderValues,
7    DefaultTraceValues, DefaultTxPoolValues,
8};
9use std::{borrow::Cow, str::FromStr, time::Duration};
10use tempo_chainspec::spec::TEMPO_T7_BASE_FEE_FLOOR;
11use url::Url;
12
13pub(crate) const DEFAULT_DOWNLOAD_URL: &str = "https://snapshots.tempoxyz.dev/4217";
14const SNAPSHOT_API_URL: &str = "https://snapshots.tempoxyz.dev/api/snapshots";
15
16/// Default OTLP logs filter level for telemetry.
17const DEFAULT_LOGS_OTLP_FILTER: &str = "debug";
18
19/// CLI arguments for telemetry configuration.
20#[derive(Debug, Clone, clap::Args)]
21pub(crate) struct TelemetryArgs {
22    /// Enables telemetry export (OTLP logs & Prometheus metrics push). Coupled
23    /// to VictoriaMetrics which supports both with different api paths.
24    ///
25    /// The URL must include credentials: `https://user:pass@metrics.example.com`
26    #[arg(
27        long,
28        value_name = "URL",
29        conflicts_with = "logs_otlp",
30        env = "TEMPO_TELEMETRY_URL"
31    )]
32    pub(crate) telemetry_url: Option<UrlWithAuth>,
33
34    /// The interval at which to push Prometheus metrics.
35    #[arg(long, default_value = "10s")]
36    pub(crate) telemetry_metrics_interval: SignedDuration,
37}
38
39impl TelemetryArgs {
40    /// Converts the telemetry args into a [`TelemetryConfig`].
41    ///
42    /// Returns `None` if telemetry is not enabled (no URL provided).
43    pub(crate) fn try_to_config(&self) -> eyre::Result<Option<TelemetryConfig>> {
44        let Some(telemetry_url) = self.telemetry_url.clone().map(Url::from) else {
45            return Ok(None);
46        };
47
48        // Extract credentials - both username and password are required
49        let username = telemetry_url.username();
50        let password = telemetry_url.password().expect("ensured when parsing args");
51
52        // Build auth header for metrics push and OTLP logs
53        let credentials = format!("{username}:{password}");
54        let encoded = BASE64_STANDARD.encode(credentials.as_bytes());
55        let auth_header = format!("Basic {encoded}");
56
57        // Set OTEL_EXPORTER_OTLP_HEADERS for OTLP logs authentication
58        if std::env::var_os("OTEL_EXPORTER_OTLP_HEADERS").is_none() {
59            let header_value = format!("Authorization={auth_header}");
60            // SAFETY: Must be called at startup before any other threads are spawned
61            unsafe {
62                std::env::set_var("OTEL_EXPORTER_OTLP_HEADERS", header_value);
63            }
64        }
65
66        // Build URL without credentials
67        let mut base_url_no_creds = telemetry_url.clone();
68        base_url_no_creds.set_username("").ok();
69        base_url_no_creds.set_password(None).ok();
70
71        // Build logs OTLP URL (Victoria Metrics OTLP path)
72        let logs_otlp_url = base_url_no_creds
73            .join("opentelemetry/v1/logs")
74            .wrap_err("failed to construct logs OTLP URL")?;
75
76        // Build metrics prometheus URL (Victoria Metrics Prometheus import path)
77        let metrics_prometheus_url = base_url_no_creds
78            .join("api/v1/import/prometheus")
79            .wrap_err("failed to construct metrics URL")?;
80
81        Ok(Some(TelemetryConfig {
82            logs_otlp_url,
83            logs_otlp_filter: DEFAULT_LOGS_OTLP_FILTER.to_string(),
84            metrics_prometheus_url,
85            metrics_prometheus_interval: self.telemetry_metrics_interval,
86            metrics_auth_header: Some(auth_header),
87        }))
88    }
89}
90
91/// A `Url` with username and password set.
92///
93/// `Debug` redacts credentials so they don't leak in clap error output or logs.
94#[derive(Clone)]
95pub(crate) struct UrlWithAuth(Url);
96
97impl UrlWithAuth {
98    /// Returns a copy of the URL with the password replaced by `***`.
99    fn redacted(&self) -> Url {
100        let mut url = self.0.clone();
101        url.set_password(Some("***")).ok();
102        url
103    }
104}
105
106impl std::fmt::Debug for UrlWithAuth {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        write!(f, "{}", self.redacted())
109    }
110}
111
112impl std::fmt::Display for UrlWithAuth {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        write!(f, "{}", self.redacted())
115    }
116}
117
118impl FromStr for UrlWithAuth {
119    type Err = Box<dyn std::error::Error + Send + Sync + 'static>;
120
121    fn from_str(s: &str) -> Result<Self, Self::Err> {
122        let url = s.parse::<Url>()?;
123        if url.username().is_empty() || url.password().is_none() {
124            return Err("URL must include username and password".into());
125        }
126        Ok(Self(url))
127    }
128}
129impl From<UrlWithAuth> for Url {
130    fn from(value: UrlWithAuth) -> Self {
131        value.0
132    }
133}
134
135/// Telemetry configuration derived from a unified telemetry URL.
136#[derive(Debug, Clone)]
137pub(crate) struct TelemetryConfig {
138    /// OTLP logs endpoint (without credentials).
139    pub(crate) logs_otlp_url: Url,
140    /// OTLP logs filter level.
141    pub(crate) logs_otlp_filter: String,
142    /// Prometheus metrics push endpoint (without credentials).
143    /// Used for both consensus and execution metrics.
144    pub(crate) metrics_prometheus_url: Url,
145    /// The interval at which to push Prometheus metrics.
146    pub(crate) metrics_prometheus_interval: SignedDuration,
147    /// Authorization header for metrics push
148    pub(crate) metrics_auth_header: Option<String>,
149}
150
151fn init_download_urls() {
152    let download_defaults = DownloadDefaults {
153        available_snapshots: vec![
154            Cow::Owned(format!("{DEFAULT_DOWNLOAD_URL} (mainnet)")),
155            Cow::Borrowed("https://snapshots.tempoxyz.dev/42431 (moderato)"),
156            Cow::Borrowed("https://snapshots.tempoxyz.dev/42429 (andantino)"),
157        ],
158        default_base_url: Cow::Borrowed(DEFAULT_DOWNLOAD_URL),
159        default_chain_aware_base_url: None,
160        snapshot_api_url: Cow::Borrowed(SNAPSHOT_API_URL),
161        long_help: None,
162    };
163
164    download_defaults
165        .try_init()
166        .expect("failed to initialize download URLs");
167}
168
169fn init_payload_builder_defaults() {
170    DefaultPayloadBuilderValues::default()
171        .with_interval(Duration::from_millis(100))
172        .with_max_payload_tasks(1)
173        .with_deadline(4)
174        .try_init()
175        .expect("failed to initialize payload builder defaults");
176}
177
178fn init_txpool_defaults() {
179    DefaultTxPoolValues::default()
180        .with_pending_max_count(50000)
181        .with_basefee_max_count(50000)
182        .with_queued_max_count(50000)
183        .with_pending_max_size(100)
184        .with_basefee_max_size(100)
185        .with_queued_max_size(100)
186        .with_no_locals(true)
187        .with_max_queued_lifetime(Duration::from_secs(120))
188        .with_max_new_pending_txs_notifications(150000)
189        .with_max_account_slots(150000)
190        .with_pending_tx_listener_buffer_size(50000)
191        .with_new_tx_listener_buffer_size(50000)
192        .with_disable_transactions_backup(true)
193        .with_additional_validation_tasks(8)
194        .with_minimal_protocol_basefee(TEMPO_T7_BASE_FEE_FLOOR)
195        .with_minimum_priority_fee(Some(0))
196        .with_max_batch_size(50000)
197        .try_init()
198        .expect("failed to initialize txpool defaults");
199}
200
201fn init_engine_defaults() {
202    DefaultEngineValues::default()
203        // In Commonware consensus, it might happen that a head is notarized (causing it to become a canonical tip for reth),
204        // and immediately nullified (allowing to build a payload on top of its parent). In that case reth might be asked to
205        // build a payload on top an ancestor of canonical tip, which is not possible without this flag.
206        //
207        // Another case when this might happen is if we optimistically canonicalize a valid block that is later getting nullified (reorged).
208        // In that case, if we are asked to propose, we would have to build a payload on top of its parent, which is an ancestor of the canonical tip as well.
209        //
210        // This setting allows reth to process payload attributes even if `headBlockHash` is an ancestor of the canonical tip.
211        .with_always_process_payload_attributes_on_canonical_head(true)
212        // Defer persistence I/O during active payload builds.
213        .with_suppress_persistence_during_build(true)
214        .with_share_sparse_trie_with_payload_builder(true)
215        .with_share_execution_cache_with_payload_builder(true)
216        .try_init()
217        .expect("failed to initialize engine defaults");
218}
219
220fn init_trace_defaults() {
221    DefaultTraceValues::default()
222        .with_service_name("tempo")
223        .with_service_version(env!("CARGO_PKG_VERSION"))
224        .try_init()
225        .expect("failed to initialize trace defaults");
226}
227
228fn init_otlp_defaults() {
229    // Override the default OTLP max queue size (2048) to prevent trace/log dropping under load.
230    // See also reth-bench-compare which uses the same approach via env vars.
231    if std::env::var_os("OTEL_BSP_MAX_QUEUE_SIZE").is_none() {
232        // SAFETY: Must be called at startup before any other threads are spawned
233        unsafe {
234            std::env::set_var("OTEL_BSP_MAX_QUEUE_SIZE", "65536");
235        }
236    }
237    if std::env::var_os("OTEL_BLRP_MAX_QUEUE_SIZE").is_none() {
238        // SAFETY: Must be called at startup before any other threads are spawned
239        unsafe {
240            std::env::set_var("OTEL_BLRP_MAX_QUEUE_SIZE", "65536");
241        }
242    }
243}
244
245fn init_network_defaults() {
246    DefaultNetworkArgs::default()
247        .with_enforce_enr_fork_id(true)
248        .try_init()
249        .expect("failed to initialize network defaults");
250}
251
252fn init_discovery_defaults() {
253    DefaultDiscoveryArgs::default()
254        // Set `None` as default for discv5 ports to make discv5 share the regular discovery port with discv4.
255        .with_discv5_port(None)
256        .with_discv5_port_ipv6(None)
257        .try_init()
258        .expect("failed to initialize discovery defaults");
259}
260
261pub(crate) fn init_defaults() {
262    init_download_urls();
263    init_payload_builder_defaults();
264    init_txpool_defaults();
265    init_engine_defaults();
266    init_trace_defaults();
267    init_otlp_defaults();
268    init_network_defaults();
269    init_discovery_defaults();
270}