zebrad/
application.rs

1//! Zebrad Abscissa Application
2//!
3//! This is the code that starts `zebrad`, and launches its tasks and services.
4//! See [the crate docs](crate) and [the start docs](crate::commands::start) for more details.
5
6use std::{env, fmt::Write as _, io::Write as _, process, sync::Arc};
7
8use abscissa_core::{
9    application::{self, AppCell},
10    config::CfgCell,
11    status_err,
12    terminal::{component::Terminal, stderr, stdout, ColorChoice},
13    Application, Component, Configurable, FrameworkError, Shutdown, StandardPaths,
14};
15use semver::{BuildMetadata, Version};
16
17use tokio::sync::watch;
18use zebra_network::constants::PORT_IN_USE_ERROR;
19use zebra_state::{
20    constants::LOCK_FILE_ERROR, state_database_format_version_in_code,
21    state_database_format_version_on_disk,
22};
23
24use crate::{
25    commands::EntryPoint,
26    components::{sync::end_of_support::EOS_PANIC_MESSAGE_HEADER, tracing::Tracing},
27    config::ZebradConfig,
28};
29
30/// See <https://docs.rs/abscissa_core/latest/src/abscissa_core/application/exit.rs.html#7-10>
31/// Print a fatal error message and exit
32fn fatal_error(app_name: String, err: &dyn std::error::Error) -> ! {
33    status_err!("{} fatal error: {}", app_name, err);
34    process::exit(1)
35}
36
37/// Application state
38pub static APPLICATION: AppCell<ZebradApp> = AppCell::new();
39
40lazy_static::lazy_static! {
41    /// The last log event that occurred in the application.
42    pub static ref LAST_WARN_ERROR_LOG_SENDER: watch::Sender<Option<(String, tracing::Level, chrono::DateTime<chrono::Utc>)>> = watch::Sender::new(None);
43}
44
45/// Returns the `zebrad` version for this build, in SemVer 2.0 format.
46///
47/// Includes `git describe` build metatata if available:
48/// - the number of commits since the last version tag, and
49/// - the git commit.
50///
51/// For details, see <https://semver.org/>
52pub fn build_version() -> Version {
53    // CARGO_PKG_VERSION is always a valid SemVer 2.0 version.
54    const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
55
56    // We're using the same library as cargo uses internally, so this is guaranteed.
57    let fallback_version = CARGO_PKG_VERSION.parse().unwrap_or_else(|error| {
58        panic!(
59            "unexpected invalid CARGO_PKG_VERSION: {error:?} in {CARGO_PKG_VERSION:?}, \
60             should have been checked by cargo"
61        )
62    });
63
64    vergen_build_version().unwrap_or(fallback_version)
65}
66
67/// Returns the `zebrad` version from this build, if available from `vergen`.
68fn vergen_build_version() -> Option<Version> {
69    // VERGEN_GIT_DESCRIBE should be in the format:
70    // - v1.0.0-rc.9-6-g319b01bb84
71    // - v1.0.0-6-g319b01bb84
72    // but sometimes it is just a short commit hash. See #6879 for details.
73    //
74    // Currently it is the output of `git describe --tags --dirty --match='v*.*.*'`,
75    // or whatever is specified in zebrad/build.rs.
76    const VERGEN_GIT_DESCRIBE: Option<&str> = option_env!("VERGEN_GIT_DESCRIBE");
77
78    // The SemVer 2.0 format is:
79    // - 1.0.0-rc.9+6.g319b01bb84
80    // - 1.0.0+6.g319b01bb84
81    //
82    // Or as a pattern:
83    // - version: major`.`minor`.`patch
84    // - optional pre-release: `-`tag[`.`tag ...]
85    // - optional build: `+`tag[`.`tag ...]
86    // change the git describe format to the semver 2.0 format
87    let vergen_git_describe = VERGEN_GIT_DESCRIBE?;
88
89    // `git describe` uses "dirty" for uncommitted changes,
90    // but users won't understand what that means.
91    let vergen_git_describe = vergen_git_describe.replace("dirty", "modified");
92
93    // Split using "git describe" separators.
94    let mut vergen_git_describe = vergen_git_describe.split('-').peekable();
95
96    // Check the "version core" part.
97    let mut version = vergen_git_describe.next()?;
98
99    // strip the leading "v", if present.
100    version = version.strip_prefix('v').unwrap_or(version);
101
102    // If the initial version is empty, just a commit hash, or otherwise invalid.
103    if Version::parse(version).is_err() {
104        return None;
105    }
106
107    let mut semver = version.to_string();
108
109    // Check if the next part is a pre-release or build part,
110    // but only consume it if it is a pre-release tag.
111    let Some(part) = vergen_git_describe.peek() else {
112        // No pre-release or build.
113        return semver.parse().ok();
114    };
115
116    if part.starts_with(char::is_alphabetic) {
117        // It's a pre-release tag.
118        semver.push('-');
119        semver.push_str(part);
120
121        // Consume the pre-release tag to move on to the build tags, if any.
122        let _ = vergen_git_describe.next();
123    }
124
125    // Check if the next part is a build part.
126    let Some(build) = vergen_git_describe.peek() else {
127        // No build tags.
128        return semver.parse().ok();
129    };
130
131    if !build.starts_with(char::is_numeric) {
132        // It's not a valid "commit count" build tag from "git describe".
133        return None;
134    }
135
136    // Append the rest of the build parts with the correct `+` and `.` separators.
137    let build_parts: Vec<_> = vergen_git_describe.collect();
138    let build_parts = build_parts.join(".");
139
140    semver.push('+');
141    semver.push_str(&build_parts);
142
143    semver.parse().ok()
144}
145
146/// The Zebra current release version, without any build metadata.
147pub fn release_version() -> Version {
148    let mut release_version = build_version();
149
150    release_version.build = BuildMetadata::EMPTY;
151
152    release_version
153}
154
155/// The User-Agent string provided by the node.
156///
157/// This must be a valid [BIP 14] user agent.
158///
159/// [BIP 14]: https://github.com/bitcoin/bips/blob/master/bip-0014.mediawiki
160pub fn user_agent() -> String {
161    let release_version = release_version();
162    format!("/Zebra:{release_version}/")
163}
164
165/// Zebrad Application
166#[derive(Debug, Default)]
167pub struct ZebradApp {
168    /// Application configuration.
169    config: CfgCell<ZebradConfig>,
170
171    /// Application state.
172    state: application::State<Self>,
173}
174
175impl ZebradApp {
176    /// Returns the git commit for this build, if available.
177    ///
178    ///
179    /// # Accuracy
180    ///
181    /// If the user makes changes, but does not commit them, the git commit will
182    /// not match the compiled source code.
183    pub fn git_commit() -> Option<&'static str> {
184        const GIT_COMMIT_GCLOUD: Option<&str> = option_env!("SHORT_SHA");
185        const GIT_COMMIT_VERGEN: Option<&str> = option_env!("VERGEN_GIT_SHA");
186
187        GIT_COMMIT_GCLOUD.or(GIT_COMMIT_VERGEN)
188    }
189}
190
191impl Application for ZebradApp {
192    /// Entrypoint command for this application.
193    type Cmd = EntryPoint;
194
195    /// Application configuration.
196    type Cfg = ZebradConfig;
197
198    /// Paths to resources within the application.
199    type Paths = StandardPaths;
200
201    /// Accessor for application configuration.
202    fn config(&self) -> Arc<ZebradConfig> {
203        self.config.read()
204    }
205
206    /// Borrow the application state immutably.
207    fn state(&self) -> &application::State<Self> {
208        &self.state
209    }
210
211    /// Returns the framework components used by this application.
212    fn framework_components(
213        &mut self,
214        _command: &Self::Cmd,
215    ) -> Result<Vec<Box<dyn Component<Self>>>, FrameworkError> {
216        // TODO: Open a PR in abscissa to add a TerminalBuilder for opting out
217        //       of the `color_eyre::install` part of `Terminal::new` without
218        //       ColorChoice::Never?
219
220        // The Tracing component uses stdout directly and will apply colors automatically.
221        //
222        // Note: It's important to use `ColorChoice::Never` here to avoid panicking in
223        //       `register_components()` below if `color_eyre::install()` is called
224        //       after `color_spantrace` has been initialized.
225        let terminal = Terminal::new(ColorChoice::Never);
226
227        Ok(vec![Box::new(terminal)])
228    }
229
230    /// Register all components used by this application.
231    ///
232    /// If you would like to add additional components to your application
233    /// beyond the default ones provided by the framework, this is the place
234    /// to do so.
235    #[allow(clippy::print_stderr)]
236    #[allow(clippy::unwrap_in_result)]
237    fn register_components(&mut self, command: &Self::Cmd) -> Result<(), FrameworkError> {
238        use crate::components::{
239            metrics::MetricsEndpoint, tokio::TokioComponent, tracing::TracingEndpoint,
240        };
241
242        let mut components = self.framework_components(command)?;
243
244        // Load config *after* framework components so that we can
245        // report an error to the terminal if it occurs (unless used with a command that doesn't need the config).
246        let config = match ZebradConfig::load(command.config_path()) {
247            Ok(config) => config,
248            // Ignore errors loading the config for some commands.
249            Err(_e) if command.cmd().should_ignore_load_config_error() => Default::default(),
250            Err(e) => {
251                status_err!(
252                    "Zebra could not load the provided configuration file and/or environment variables.\
253                     This might mean you are using a deprecated format of the file, or are attempting to
254                     configure deprecated or unknown fields via environment variables.\
255                     You can generate a valid config by running \"zebrad generate\", \
256                     and diff it against yours to examine any format inconsistencies."
257                );
258                // Convert config::ConfigError to FrameworkError using a generic IO error
259                let io_error = std::io::Error::new(
260                    std::io::ErrorKind::InvalidData,
261                    format!("Configuration error: {}", e),
262                );
263                return Err(FrameworkError::from(io_error));
264            }
265        };
266
267        let config = command.process_config(config)?;
268
269        let theme = if config.tracing.use_color_stdout_and_stderr() {
270            color_eyre::config::Theme::dark()
271        } else {
272            color_eyre::config::Theme::new()
273        };
274
275        // collect the common metadata for the issue URL and panic report,
276        // skipping any env vars that aren't present
277
278        // reads state disk version file, doesn't open RocksDB database
279        let disk_db_version =
280            match state_database_format_version_on_disk(&config.state, &config.network.network) {
281                Ok(Some(version)) => version.to_string(),
282                // This "version" is specially formatted to match a relaxed version regex in CI
283                Ok(None) => "creating.new.database".to_string(),
284                Err(error) => {
285                    let mut error = format!("error: {error:?}");
286                    error.truncate(100);
287                    error
288                }
289            };
290
291        let app_metadata = [
292            // build-time constant: cargo or git tag + short commit
293            ("version", build_version().to_string()),
294            // config
295            ("Zcash network", config.network.network.to_string()),
296            // code constant
297            (
298                "running state version",
299                state_database_format_version_in_code().to_string(),
300            ),
301            // state disk file, doesn't open database
302            ("initial disk state version", disk_db_version),
303            // build-time constant
304            ("features", env!("VERGEN_CARGO_FEATURES").to_string()),
305        ];
306
307        // git env vars can be skipped if there is no `.git` during the
308        // build, so they must all be optional
309        let git_metadata: &[(_, Option<_>)] = &[
310            ("branch", option_env!("VERGEN_GIT_BRANCH")),
311            ("git commit", Self::git_commit()),
312            (
313                "commit timestamp",
314                option_env!("VERGEN_GIT_COMMIT_TIMESTAMP"),
315            ),
316        ];
317        // skip missing metadata
318        let git_metadata: Vec<(_, String)> = git_metadata
319            .iter()
320            .filter_map(|(k, v)| Some((k, (*v)?)))
321            .map(|(k, v)| (*k, v.to_string()))
322            .collect();
323
324        let build_metadata: Vec<_> = [
325            ("target triple", env!("VERGEN_CARGO_TARGET_TRIPLE")),
326            ("rust compiler", env!("VERGEN_RUSTC_SEMVER")),
327            ("rust release date", env!("VERGEN_RUSTC_COMMIT_DATE")),
328            ("optimization level", env!("VERGEN_CARGO_OPT_LEVEL")),
329            ("debug checks", env!("VERGEN_CARGO_DEBUG")),
330        ]
331        .iter()
332        .map(|(k, v)| (*k, v.to_string()))
333        .collect();
334
335        let panic_metadata: Vec<_> = app_metadata
336            .iter()
337            .chain(git_metadata.iter())
338            .chain(build_metadata.iter())
339            .collect();
340
341        let mut builder = color_eyre::config::HookBuilder::default();
342        let mut metadata_section = "Diagnostic metadata:".to_string();
343        for (k, v) in panic_metadata {
344            builder = builder.add_issue_metadata(k, v.clone());
345            write!(&mut metadata_section, "\n{k}: {}", &v)
346                .expect("unexpected failure writing to string");
347        }
348
349        builder = builder
350            .theme(theme)
351            .panic_section(metadata_section.clone())
352            .issue_url(concat!(env!("CARGO_PKG_REPOSITORY"), "/issues/new"))
353            .issue_filter(|kind| match kind {
354                color_eyre::ErrorKind::NonRecoverable(error) => {
355                    let error_str = match error.downcast_ref::<String>() {
356                        Some(as_string) => as_string,
357                        None => return true,
358                    };
359                    // listener port conflicts
360                    if PORT_IN_USE_ERROR.is_match(error_str) {
361                        return false;
362                    }
363                    // RocksDB lock file conflicts
364                    if LOCK_FILE_ERROR.is_match(error_str) {
365                        return false;
366                    }
367                    // Don't ask users to report old version panics.
368                    if error_str.to_string().contains(EOS_PANIC_MESSAGE_HEADER) {
369                        return false;
370                    }
371                    true
372                }
373                color_eyre::ErrorKind::Recoverable(error) => {
374                    // Type checks should be faster than string conversions.
375                    //
376                    // Don't ask users to create bug reports for timeouts and peer errors.
377                    if error.is::<tower::timeout::error::Elapsed>()
378                        || error.is::<tokio::time::error::Elapsed>()
379                        || error.is::<zebra_network::PeerError>()
380                        || error.is::<zebra_network::SharedPeerError>()
381                        || error.is::<zebra_network::HandshakeError>()
382                    {
383                        return false;
384                    }
385
386                    // Don't ask users to create bug reports for known timeouts, duplicate blocks,
387                    // full disks, or updated binaries.
388                    let error_str = error.to_string();
389                    !error_str.contains("timed out")
390                        && !error_str.contains("duplicate hash")
391                        && !error_str.contains("No space left on device")
392                        // abscissa panics like this when the running zebrad binary has been updated
393                        && !error_str.contains("error canonicalizing application path")
394                }
395            });
396
397        // This MUST happen after `Terminal::new` to ensure our preferred panic
398        // handler is the last one installed
399        let (panic_hook, eyre_hook) = builder.into_hooks();
400        eyre_hook.install().expect("eyre_hook.install() error");
401
402        // The Sentry default config pulls in the DSN from the `SENTRY_DSN`
403        // environment variable.
404        #[cfg(feature = "sentry")]
405        let guard = sentry::init(sentry::ClientOptions {
406            debug: true,
407            release: Some(build_version().to_string().into()),
408            ..Default::default()
409        });
410
411        std::panic::set_hook(Box::new(move |panic_info| {
412            let panic_report = panic_hook.panic_report(panic_info);
413            eprintln!("{panic_report}");
414
415            #[cfg(feature = "sentry")]
416            {
417                let event = crate::sentry::panic_event_from(panic_report);
418                sentry::capture_event(event);
419
420                if !guard.close(None) {
421                    warn!("unable to flush sentry events during panic");
422                }
423            }
424        }));
425
426        // Apply the configured number of threads to the thread pool.
427        //
428        // TODO:
429        // - set rayon panic handler to a function that takes `Box<dyn Any + Send + 'static>`,
430        //   which forwards to sentry. If possible, use eyre's panic report for formatting.
431        // - do we also need to call this code in `zebra_consensus::init()`,
432        //   when that crate is being used by itself?
433        rayon::ThreadPoolBuilder::new()
434            .num_threads(config.sync.parallel_cpu_threads)
435            .thread_name(|thread_index| format!("rayon {thread_index}"))
436            .build_global()
437            .expect("unable to initialize rayon thread pool");
438
439        let default_filter = command.cmd().default_tracing_filter(command.verbose);
440        let is_server = command.cmd().is_server();
441
442        // Ignore the configured tracing filter for short-lived utility commands
443        let mut tracing_config = config.tracing.clone();
444        let metrics_config = config.metrics.clone();
445        if is_server {
446            // Override the default tracing filter based on the command-line verbosity.
447            tracing_config.filter = tracing_config
448                .filter
449                .clone()
450                .or_else(|| Some(default_filter.to_owned()));
451        } else {
452            // Don't apply the configured filter for short-lived commands.
453            tracing_config.filter = Some(default_filter.to_owned());
454            tracing_config.flamegraph = None;
455        }
456        components.push(Box::new(Tracing::new(
457            &config.network.network,
458            tracing_config,
459            command.cmd().uses_intro(),
460        )?));
461
462        // Log git metadata and platform info when zebrad starts up
463        if is_server {
464            info!("{metadata_section}");
465
466            if command.config_path().is_some() {
467                info!("Using config file at: {:?}", command.config_path().unwrap());
468            } else {
469                info!("No config file provided, using default configuration");
470            }
471
472            info!("{config:?}");
473
474            // Explicitly log the configured miner address so CI can assert env override
475            if let Some(miner_address) = &config.mining.miner_address {
476                info!(%miner_address, "configured miner address");
477            }
478        }
479
480        // Activate the global span, so it's visible when we load the other
481        // components. Space is at a premium here, so we use an empty message,
482        // short commit hash, and the unique part of the network name.
483        let net = config.network.network.to_string();
484        let net = match net.as_str() {
485            default_net_name @ ("Testnet" | "Mainnet") => &default_net_name[..4],
486            other_net_name => other_net_name,
487        };
488        let global_span = if let Some(git_commit) = ZebradApp::git_commit() {
489            error_span!("", zebrad = git_commit, net)
490        } else {
491            error_span!("", net)
492        };
493
494        let global_guard = global_span.enter();
495        // leak the global span, to make sure it stays active
496        std::mem::forget(global_guard);
497
498        tracing::info!(
499            num_threads = rayon::current_num_threads(),
500            "initialized rayon thread pool for CPU-bound tasks",
501        );
502
503        // Launch network and async endpoints only for long-running commands.
504        if is_server {
505            components.push(Box::new(TokioComponent::new()?));
506            components.push(Box::new(TracingEndpoint::new(&config)?));
507            components.push(Box::new(MetricsEndpoint::new(&metrics_config)?));
508        }
509
510        self.state.components_mut().register(components)?;
511
512        // Fire callback to signal state in the application lifecycle
513        self.after_config(config)
514    }
515
516    /// Load this application's configuration and initialize its components.
517    #[allow(clippy::unwrap_in_result)]
518    fn init(&mut self, command: &Self::Cmd) -> Result<(), FrameworkError> {
519        // Create and register components with the application.
520        // We do this first to calculate a proper dependency ordering before
521        // application configuration is processed
522        self.register_components(command)
523    }
524
525    /// Post-configuration lifecycle callback.
526    ///
527    /// Called regardless of whether config is loaded to indicate this is the
528    /// time in app lifecycle when configuration would be loaded if
529    /// possible.
530    fn after_config(&mut self, config: Self::Cfg) -> Result<(), FrameworkError> {
531        // Configure components
532        self.state.components_mut().after_config(&config)?;
533        self.config.set_once(config);
534
535        Ok(())
536    }
537
538    fn shutdown(&self, shutdown: Shutdown) -> ! {
539        // Some OSes require a flush to send all output to the terminal.
540        // zebrad's logging uses Abscissa, so we flush its streams.
541        //
542        // TODO:
543        // - if this doesn't work, send an empty line as well
544        // - move this code to the tracing component's `before_shutdown()`
545        let _ = stdout().lock().flush();
546        let _ = stderr().lock().flush();
547
548        let shutdown_result = self.state().components().shutdown(self, shutdown);
549
550        self.state()
551            .components_mut()
552            .get_downcast_mut::<Tracing>()
553            .map(Tracing::shutdown);
554
555        if let Err(e) = shutdown_result {
556            let app_name = self.name().to_string();
557            fatal_error(app_name, &e);
558        }
559
560        match shutdown {
561            Shutdown::Graceful => process::exit(0),
562            Shutdown::Forced => process::exit(1),
563            Shutdown::Crash => process::exit(2),
564        }
565    }
566}
567
568/// Boot the given application, parsing subcommand and options from
569/// command-line arguments, and terminating when complete.
570// <https://docs.rs/abscissa_core/0.7.0/src/abscissa_core/application.rs.html#174-178>
571pub fn boot(app_cell: &'static AppCell<ZebradApp>) -> ! {
572    let args =
573        EntryPoint::process_cli_args(env::args_os().collect()).unwrap_or_else(|err| err.exit());
574
575    ZebradApp::run(app_cell, args);
576    process::exit(0);
577}