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 command.config_path() {
247            Some(path) => match self.load_config(&path) {
248                Ok(config) => config,
249                // Ignore errors loading the config for some commands.
250                Err(_e) if command.cmd().should_ignore_load_config_error() => Default::default(),
251                Err(e) => {
252                    status_err!("Zebra could not parse the provided config file. This might mean you are using a deprecated format of the file. You can generate a valid config by running \"zebrad generate\", and diff it against yours to examine any format inconsistencies.");
253                    return Err(e);
254                }
255            },
256            None => ZebradConfig::default(),
257        };
258
259        let config = command.process_config(config)?;
260
261        let theme = if config.tracing.use_color_stdout_and_stderr() {
262            color_eyre::config::Theme::dark()
263        } else {
264            color_eyre::config::Theme::new()
265        };
266
267        // collect the common metadata for the issue URL and panic report,
268        // skipping any env vars that aren't present
269
270        // reads state disk version file, doesn't open RocksDB database
271        let disk_db_version =
272            match state_database_format_version_on_disk(&config.state, &config.network.network) {
273                Ok(Some(version)) => version.to_string(),
274                // This "version" is specially formatted to match a relaxed version regex in CI
275                Ok(None) => "creating.new.database".to_string(),
276                Err(error) => {
277                    let mut error = format!("error: {error:?}");
278                    error.truncate(100);
279                    error
280                }
281            };
282
283        let app_metadata = [
284            // build-time constant: cargo or git tag + short commit
285            ("version", build_version().to_string()),
286            // config
287            ("Zcash network", config.network.network.to_string()),
288            // code constant
289            (
290                "running state version",
291                state_database_format_version_in_code().to_string(),
292            ),
293            // state disk file, doesn't open database
294            ("initial disk state version", disk_db_version),
295            // build-time constant
296            ("features", env!("VERGEN_CARGO_FEATURES").to_string()),
297        ];
298
299        // git env vars can be skipped if there is no `.git` during the
300        // build, so they must all be optional
301        let git_metadata: &[(_, Option<_>)] = &[
302            ("branch", option_env!("VERGEN_GIT_BRANCH")),
303            ("git commit", Self::git_commit()),
304            (
305                "commit timestamp",
306                option_env!("VERGEN_GIT_COMMIT_TIMESTAMP"),
307            ),
308        ];
309        // skip missing metadata
310        let git_metadata: Vec<(_, String)> = git_metadata
311            .iter()
312            .filter_map(|(k, v)| Some((k, (*v)?)))
313            .map(|(k, v)| (*k, v.to_string()))
314            .collect();
315
316        let build_metadata: Vec<_> = [
317            ("target triple", env!("VERGEN_CARGO_TARGET_TRIPLE")),
318            ("rust compiler", env!("VERGEN_RUSTC_SEMVER")),
319            ("rust release date", env!("VERGEN_RUSTC_COMMIT_DATE")),
320            ("optimization level", env!("VERGEN_CARGO_OPT_LEVEL")),
321            ("debug checks", env!("VERGEN_CARGO_DEBUG")),
322        ]
323        .iter()
324        .map(|(k, v)| (*k, v.to_string()))
325        .collect();
326
327        let panic_metadata: Vec<_> = app_metadata
328            .iter()
329            .chain(git_metadata.iter())
330            .chain(build_metadata.iter())
331            .collect();
332
333        let mut builder = color_eyre::config::HookBuilder::default();
334        let mut metadata_section = "Metadata:".to_string();
335        for (k, v) in panic_metadata {
336            builder = builder.add_issue_metadata(k, v.clone());
337            write!(&mut metadata_section, "\n{k}: {}", &v)
338                .expect("unexpected failure writing to string");
339        }
340
341        builder = builder
342            .theme(theme)
343            .panic_section(metadata_section.clone())
344            .issue_url(concat!(env!("CARGO_PKG_REPOSITORY"), "/issues/new"))
345            .issue_filter(|kind| match kind {
346                color_eyre::ErrorKind::NonRecoverable(error) => {
347                    let error_str = match error.downcast_ref::<String>() {
348                        Some(as_string) => as_string,
349                        None => return true,
350                    };
351                    // listener port conflicts
352                    if PORT_IN_USE_ERROR.is_match(error_str) {
353                        return false;
354                    }
355                    // RocksDB lock file conflicts
356                    if LOCK_FILE_ERROR.is_match(error_str) {
357                        return false;
358                    }
359                    // Don't ask users to report old version panics.
360                    if error_str.to_string().contains(EOS_PANIC_MESSAGE_HEADER) {
361                        return false;
362                    }
363                    true
364                }
365                color_eyre::ErrorKind::Recoverable(error) => {
366                    // Type checks should be faster than string conversions.
367                    //
368                    // Don't ask users to create bug reports for timeouts and peer errors.
369                    if error.is::<tower::timeout::error::Elapsed>()
370                        || error.is::<tokio::time::error::Elapsed>()
371                        || error.is::<zebra_network::PeerError>()
372                        || error.is::<zebra_network::SharedPeerError>()
373                        || error.is::<zebra_network::HandshakeError>()
374                    {
375                        return false;
376                    }
377
378                    // Don't ask users to create bug reports for known timeouts, duplicate blocks,
379                    // full disks, or updated binaries.
380                    let error_str = error.to_string();
381                    !error_str.contains("timed out")
382                        && !error_str.contains("duplicate hash")
383                        && !error_str.contains("No space left on device")
384                        // abscissa panics like this when the running zebrad binary has been updated
385                        && !error_str.contains("error canonicalizing application path")
386                }
387            });
388
389        // This MUST happen after `Terminal::new` to ensure our preferred panic
390        // handler is the last one installed
391        let (panic_hook, eyre_hook) = builder.into_hooks();
392        eyre_hook.install().expect("eyre_hook.install() error");
393
394        // The Sentry default config pulls in the DSN from the `SENTRY_DSN`
395        // environment variable.
396        #[cfg(feature = "sentry")]
397        let guard = sentry::init(sentry::ClientOptions {
398            debug: true,
399            release: Some(build_version().to_string().into()),
400            ..Default::default()
401        });
402
403        std::panic::set_hook(Box::new(move |panic_info| {
404            let panic_report = panic_hook.panic_report(panic_info);
405            eprintln!("{panic_report}");
406
407            #[cfg(feature = "sentry")]
408            {
409                let event = crate::sentry::panic_event_from(panic_report);
410                sentry::capture_event(event);
411
412                if !guard.close(None) {
413                    warn!("unable to flush sentry events during panic");
414                }
415            }
416        }));
417
418        // Apply the configured number of threads to the thread pool.
419        //
420        // TODO:
421        // - set rayon panic handler to a function that takes `Box<dyn Any + Send + 'static>`,
422        //   which forwards to sentry. If possible, use eyre's panic report for formatting.
423        // - do we also need to call this code in `zebra_consensus::init()`,
424        //   when that crate is being used by itself?
425        rayon::ThreadPoolBuilder::new()
426            .num_threads(config.sync.parallel_cpu_threads)
427            .thread_name(|thread_index| format!("rayon {thread_index}"))
428            .build_global()
429            .expect("unable to initialize rayon thread pool");
430
431        let cfg_ref = &config;
432        let default_filter = command.cmd().default_tracing_filter(command.verbose);
433        let is_server = command.cmd().is_server();
434
435        // Ignore the configured tracing filter for short-lived utility commands
436        let mut tracing_config = cfg_ref.tracing.clone();
437        let metrics_config = cfg_ref.metrics.clone();
438        if is_server {
439            // Override the default tracing filter based on the command-line verbosity.
440            tracing_config.filter = tracing_config
441                .filter
442                .clone()
443                .or_else(|| Some(default_filter.to_owned()));
444        } else {
445            // Don't apply the configured filter for short-lived commands.
446            tracing_config.filter = Some(default_filter.to_owned());
447            tracing_config.flamegraph = None;
448        }
449        components.push(Box::new(Tracing::new(
450            &config.network.network,
451            tracing_config,
452            command.cmd().uses_intro(),
453        )?));
454
455        // Log git metadata and platform info when zebrad starts up
456        if is_server {
457            tracing::info!("Diagnostic {}", metadata_section);
458            info!(config_path = ?command.config_path(), config = ?cfg_ref, "loaded zebrad config");
459        }
460
461        // Activate the global span, so it's visible when we load the other
462        // components. Space is at a premium here, so we use an empty message,
463        // short commit hash, and the unique part of the network name.
464        let net = config.network.network.to_string();
465        let net = match net.as_str() {
466            default_net_name @ ("Testnet" | "Mainnet") => &default_net_name[..4],
467            other_net_name => other_net_name,
468        };
469        let global_span = if let Some(git_commit) = ZebradApp::git_commit() {
470            error_span!("", zebrad = git_commit, net)
471        } else {
472            error_span!("", net)
473        };
474
475        let global_guard = global_span.enter();
476        // leak the global span, to make sure it stays active
477        std::mem::forget(global_guard);
478
479        tracing::info!(
480            num_threads = rayon::current_num_threads(),
481            "initialized rayon thread pool for CPU-bound tasks",
482        );
483
484        // Launch network and async endpoints only for long-running commands.
485        if is_server {
486            components.push(Box::new(TokioComponent::new()?));
487            components.push(Box::new(TracingEndpoint::new(cfg_ref)?));
488            components.push(Box::new(MetricsEndpoint::new(&metrics_config)?));
489        }
490
491        self.state.components_mut().register(components)?;
492
493        // Fire callback to signal state in the application lifecycle
494        self.after_config(config)
495    }
496
497    /// Load this application's configuration and initialize its components.
498    #[allow(clippy::unwrap_in_result)]
499    fn init(&mut self, command: &Self::Cmd) -> Result<(), FrameworkError> {
500        // Create and register components with the application.
501        // We do this first to calculate a proper dependency ordering before
502        // application configuration is processed
503        self.register_components(command)
504    }
505
506    /// Post-configuration lifecycle callback.
507    ///
508    /// Called regardless of whether config is loaded to indicate this is the
509    /// time in app lifecycle when configuration would be loaded if
510    /// possible.
511    fn after_config(&mut self, config: Self::Cfg) -> Result<(), FrameworkError> {
512        // Configure components
513        self.state.components_mut().after_config(&config)?;
514        self.config.set_once(config);
515
516        Ok(())
517    }
518
519    fn shutdown(&self, shutdown: Shutdown) -> ! {
520        // Some OSes require a flush to send all output to the terminal.
521        // zebrad's logging uses Abscissa, so we flush its streams.
522        //
523        // TODO:
524        // - if this doesn't work, send an empty line as well
525        // - move this code to the tracing component's `before_shutdown()`
526        let _ = stdout().lock().flush();
527        let _ = stderr().lock().flush();
528
529        let shutdown_result = self.state().components().shutdown(self, shutdown);
530
531        self.state()
532            .components_mut()
533            .get_downcast_mut::<Tracing>()
534            .map(Tracing::shutdown);
535
536        if let Err(e) = shutdown_result {
537            let app_name = self.name().to_string();
538            fatal_error(app_name, &e);
539        }
540
541        match shutdown {
542            Shutdown::Graceful => process::exit(0),
543            Shutdown::Forced => process::exit(1),
544            Shutdown::Crash => process::exit(2),
545        }
546    }
547}
548
549/// Boot the given application, parsing subcommand and options from
550/// command-line arguments, and terminating when complete.
551// <https://docs.rs/abscissa_core/0.7.0/src/abscissa_core/application.rs.html#174-178>
552pub fn boot(app_cell: &'static AppCell<ZebradApp>) -> ! {
553    let args =
554        EntryPoint::process_cli_args(env::args_os().collect()).unwrap_or_else(|err| err.exit());
555
556    ZebradApp::run(app_cell, args);
557    process::exit(0);
558}