Skip to main content

zebra_state/
config.rs

1//! Cached state configuration for Zebra.
2
3use std::{
4    fmt,
5    fs::{self, canonicalize, remove_dir_all, DirEntry, ReadDir},
6    io::ErrorKind,
7    path::{Path, PathBuf},
8    time::Duration,
9};
10
11use semver::Version;
12use serde::{Deserialize, Serialize};
13use tokio::task::{spawn_blocking, JoinHandle};
14use tracing::Span;
15
16use zebra_chain::{common::default_cache_dir, parameters::Network};
17
18use crate::{
19    constants::{DATABASE_FORMAT_VERSION_FILE_NAME, STATE_DATABASE_KIND},
20    service::finalized_state::restorable_db_versions,
21    state_database_format_version_in_code, BoxError,
22};
23
24/// A wrapper for [`String`] which hides its contents from `Debug` output, so that secrets
25/// are not written to logs by types that derive `Debug`.
26///
27/// Serialization is unaffected, so the value still round-trips through the config file. Use
28/// [`RedactedString::as_str()`] to reach the contents deliberately.
29#[derive(Clone, Default, Eq, PartialEq, Deserialize, Serialize)]
30#[serde(transparent)]
31pub struct RedactedString(String);
32
33impl fmt::Debug for RedactedString {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        f.pad("[REDACTED]")
36    }
37}
38
39impl RedactedString {
40    /// Return the wrapped string, which exposes its sensitive contents.
41    pub fn as_str(&self) -> &str {
42        &self.0
43    }
44}
45
46impl<S> From<S> for RedactedString
47where
48    S: Into<String>,
49{
50    fn from(value: S) -> Self {
51        Self(value.into())
52    }
53}
54
55/// Configuration for the state service.
56#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
57#[serde(deny_unknown_fields, default)]
58pub struct Config {
59    /// The root directory for storing cached block data.
60    ///
61    /// If you change this directory, you might also want to change `network.cache_dir`.
62    ///
63    /// This cache stores permanent blockchain state that can be replicated from
64    /// the network, including the best chain, blocks, the UTXO set, and other indexes.
65    /// Any state that can be rolled back is only stored in memory.
66    ///
67    /// The `zebra-state` cache does *not* include any private data, such as wallet data.
68    ///
69    /// You can delete the entire cached state directory, but it will impact your node's
70    /// readiness and network usage. If you do, Zebra will re-sync from genesis the next
71    /// time it is launched.
72    ///
73    /// The default directory is platform dependent, based on
74    /// [`dirs::cache_dir()`](https://docs.rs/dirs/3.0.1/dirs/fn.cache_dir.html):
75    ///
76    /// |Platform | Value                                           | Example                              |
77    /// | ------- | ----------------------------------------------- | ------------------------------------ |
78    /// | Linux   | `$XDG_CACHE_HOME/zebra` or `$HOME/.cache/zebra` | `/home/alice/.cache/zebra`           |
79    /// | macOS   | `$HOME/Library/Caches/zebra`                    | `/Users/Alice/Library/Caches/zebra`  |
80    /// | Windows | `{FOLDERID_LocalAppData}\zebra`                 | `C:\Users\Alice\AppData\Local\zebra` |
81    /// | Other   | `std::env::current_dir()/cache/zebra`           | `/cache/zebra`                       |
82    ///
83    /// # Security
84    ///
85    /// If you are running Zebra with elevated permissions ("root"), create the
86    /// directory for this file before running Zebra, and make sure the Zebra user
87    /// account has exclusive access to that directory, and other users can't modify
88    /// its parent directories.
89    ///
90    /// # Implementation Details
91    ///
92    /// Each state format version and network has a separate state.
93    /// These states are stored in `state/vN/mainnet` and `state/vN/testnet` subdirectories,
94    /// underneath the `cache_dir` path, where `N` is the state format version.
95    ///
96    /// When Zebra's state format changes, it creates a new state subdirectory for that version,
97    /// and re-syncs from genesis.
98    ///
99    /// Old state versions are automatically deleted at startup. You can also manually delete old
100    /// state versions.
101    pub cache_dir: PathBuf,
102
103    /// Whether to use an ephemeral database.
104    ///
105    /// Ephemeral databases are stored in a temporary directory created using [`tempfile::tempdir()`].
106    /// They are deleted when Zebra exits successfully.
107    /// (If Zebra panics or crashes, the ephemeral database won't be deleted.)
108    ///
109    /// Set to `false` by default. If this is set to `true`, [`cache_dir`] is ignored.
110    ///
111    /// Ephemeral directories are created in the [`std::env::temp_dir()`].
112    /// Zebra names each directory after the state version and network, for example: `zebra-state-v21-mainnet-XnyGnE`.
113    ///
114    /// [`cache_dir`]: struct.Config.html#structfield.cache_dir
115    pub ephemeral: bool,
116
117    /// Whether to cache non-finalized blocks on disk to be restored when Zebra restarts.
118    ///
119    /// Set to `true` by default. If this is set to `false`, Zebra will irrecoverably drop
120    /// non-finalized blocks when the process exits and will have to re-download them from
121    /// the network when it restarts, if those blocks are still available in the network.
122    ///
123    /// Note: The non-finalized state will be written to a backup cache once per 5 seconds at most.
124    ///       If blocks are added to the non-finalized state more frequently, the backup may not reflect
125    ///       Zebra's last non-finalized state before it shut down.
126    pub should_backup_non_finalized_state: bool,
127
128    /// Whether to delete the old database directories when present.
129    ///
130    /// Set to `true` by default. If this is set to `false`,
131    /// no check for old database versions will be made and nothing will be
132    /// deleted.
133    pub delete_old_database: bool,
134
135    // Debug configs
136    //
137    /// Commit blocks to the finalized state up to this height, then exit Zebra.
138    ///
139    /// Set to `None` by default: Zebra continues syncing indefinitely.
140    pub debug_stop_at_height: Option<u32>,
141
142    /// While Zebra is running, check state validity this often.
143    ///
144    /// Set to `None` by default: Zebra only checks state format validity on startup and shutdown.
145    #[serde(with = "humantime_serde")]
146    pub debug_validity_check_interval: Option<Duration>,
147
148    /// If true, skip spawning the non-finalized state backup task and instead write
149    /// the non-finalized state to the backup directory synchronously before each update
150    /// to the latest chain tip or non-finalized state channels.
151    ///
152    /// Set to `false` by default. When `true`, the non-finalized state is still restored
153    /// from the backup directory on startup, but updates are written synchronously on every
154    /// block commit rather than asynchronously every 5 seconds.
155    ///
156    /// This is intended for testing scenarios where blocks are committed rapidly and the
157    /// async backup task may not flush all blocks before shutdown.
158    pub debug_skip_non_finalized_state_backup_task: bool,
159
160    // Elasticsearch configs
161    //
162    #[cfg(feature = "elasticsearch")]
163    /// The elasticsearch database url.
164    pub elasticsearch_url: String,
165
166    #[cfg(feature = "elasticsearch")]
167    /// The elasticsearch database username.
168    pub elasticsearch_username: String,
169
170    #[cfg(feature = "elasticsearch")]
171    /// The elasticsearch database password.
172    pub elasticsearch_password: RedactedString,
173}
174
175fn gen_temp_path(prefix: &str) -> PathBuf {
176    tempfile::Builder::new()
177        .prefix(prefix)
178        .tempdir()
179        .expect("temporary directory is created successfully")
180        .keep()
181}
182
183impl Config {
184    /// Returns the path for the database, based on the kind, major version and network.
185    /// Each incompatible database format or network gets its own unique path.
186    pub fn db_path(
187        &self,
188        db_kind: impl AsRef<str>,
189        major_version: u64,
190        network: &Network,
191    ) -> PathBuf {
192        let db_kind = db_kind.as_ref();
193        let major_version = format!("v{major_version}");
194        let net_dir = network.lowercase_name();
195
196        if self.ephemeral {
197            gen_temp_path(&format!("zebra-{db_kind}-{major_version}-{net_dir}-"))
198        } else {
199            self.cache_dir
200                .join(db_kind)
201                .join(major_version)
202                .join(net_dir)
203        }
204    }
205
206    /// Returns the path for the non-finalized state backup directory, based on the network.
207    /// Non-finalized state backup files are encoded in the network protocol format and remain
208    /// valid across db format upgrades.
209    pub fn non_finalized_state_backup_dir(&self, network: &Network) -> Option<PathBuf> {
210        if self.ephemeral || !self.should_backup_non_finalized_state {
211            // Ephemeral databases are intended to be irrecoverable across restarts and don't
212            // require a backup for the non-finalized state.
213            return None;
214        }
215
216        let net_dir = network.lowercase_name();
217        Some(self.cache_dir.join("non_finalized_state").join(net_dir))
218    }
219
220    /// Returns the path for the database format minor/patch version file,
221    /// based on the kind, major version and network.
222    pub fn version_file_path(
223        &self,
224        db_kind: impl AsRef<str>,
225        major_version: u64,
226        network: &Network,
227    ) -> PathBuf {
228        let mut version_path = self.db_path(db_kind, major_version, network);
229
230        version_path.push(DATABASE_FORMAT_VERSION_FILE_NAME);
231
232        version_path
233    }
234
235    /// Returns a config for a temporary database that is deleted when it is dropped.
236    pub fn ephemeral() -> Config {
237        Config {
238            ephemeral: true,
239            ..Config::default()
240        }
241    }
242}
243
244impl Default for Config {
245    fn default() -> Self {
246        Self {
247            cache_dir: default_cache_dir(),
248            ephemeral: false,
249            should_backup_non_finalized_state: true,
250            delete_old_database: true,
251            debug_stop_at_height: None,
252            debug_validity_check_interval: None,
253            debug_skip_non_finalized_state_backup_task: false,
254            #[cfg(feature = "elasticsearch")]
255            elasticsearch_url: "https://localhost:9200".to_string(),
256            #[cfg(feature = "elasticsearch")]
257            elasticsearch_username: "elastic".to_string(),
258            #[cfg(feature = "elasticsearch")]
259            elasticsearch_password: RedactedString::default(),
260        }
261    }
262}
263
264// Cleaning up old database versions
265// TODO: put this in a different module?
266
267/// Spawns a task that checks if there are old state database folders,
268/// and deletes them from the filesystem.
269///
270/// See `check_and_delete_old_databases()` for details.
271pub fn check_and_delete_old_state_databases(config: &Config, network: &Network) -> JoinHandle<()> {
272    check_and_delete_old_databases(
273        config,
274        STATE_DATABASE_KIND,
275        state_database_format_version_in_code().major,
276        network,
277    )
278}
279
280/// Spawns a task that checks if there are old database folders,
281/// and deletes them from the filesystem.
282///
283/// Iterate over the files and directories in the databases folder and delete if:
284/// - The `db_kind` directory exists.
285/// - The entry in `db_kind` is a directory.
286/// - The directory name has a prefix `v`.
287/// - The directory name without the prefix can be parsed as an unsigned number.
288/// - The parsed number is lower than the `major_version`.
289///
290/// The network is used to generate the path, then ignored.
291/// If `config` is an ephemeral database, no databases are deleted.
292///
293/// # Panics
294///
295/// If the path doesn't match the expected `db_kind/major_version/network` format.
296pub fn check_and_delete_old_databases(
297    config: &Config,
298    db_kind: impl AsRef<str>,
299    major_version: u64,
300    network: &Network,
301) -> JoinHandle<()> {
302    let current_span = Span::current();
303    let config = config.clone();
304    let db_kind = db_kind.as_ref().to_string();
305    let network = network.clone();
306
307    spawn_blocking(move || {
308        current_span.in_scope(|| {
309            delete_old_databases(config, db_kind, major_version, &network);
310            info!("finished old database version cleanup task");
311        })
312    })
313}
314
315/// Check if there are old database folders and delete them from the filesystem.
316///
317/// See [`check_and_delete_old_databases`] for details.
318fn delete_old_databases(config: Config, db_kind: String, major_version: u64, network: &Network) {
319    if config.ephemeral || !config.delete_old_database {
320        return;
321    }
322
323    info!(db_kind, "checking for old database versions");
324
325    let restorable_db_versions = restorable_db_versions();
326
327    let mut db_path = config.db_path(&db_kind, major_version, network);
328    // Check and remove the network path.
329    assert_eq!(
330        db_path.file_name(),
331        Some(network.lowercase_name().as_ref()),
332        "unexpected database network path structure"
333    );
334    assert!(db_path.pop());
335
336    // Check and remove the major version path, we'll iterate over them all below.
337    assert_eq!(
338        db_path.file_name(),
339        Some(format!("v{major_version}").as_ref()),
340        "unexpected database version path structure"
341    );
342    assert!(db_path.pop());
343
344    // Check for the correct database kind to iterate within.
345    assert_eq!(
346        db_path.file_name(),
347        Some(db_kind.as_ref()),
348        "unexpected database kind path structure"
349    );
350
351    if let Some(db_kind_dir) = read_dir(&db_path) {
352        for entry in db_kind_dir.flatten() {
353            let deleted_db =
354                check_and_delete_database(&config, major_version, &restorable_db_versions, &entry);
355
356            if let Some(deleted_db) = deleted_db {
357                info!(?deleted_db, "deleted outdated {db_kind} database directory");
358            }
359        }
360    }
361}
362
363/// Return a `ReadDir` for `dir`, after checking that `dir` exists and can be read.
364///
365/// Returns `None` if any operation fails.
366fn read_dir(dir: &Path) -> Option<ReadDir> {
367    if dir.exists() {
368        if let Ok(read_dir) = dir.read_dir() {
369            return Some(read_dir);
370        }
371    }
372    None
373}
374
375/// Check if `entry` is an old database directory, and delete it from the filesystem.
376/// See [`check_and_delete_old_databases`] for details.
377///
378/// If the directory was deleted, returns its path.
379fn check_and_delete_database(
380    config: &Config,
381    major_version: u64,
382    restorable_db_versions: &[u64],
383    entry: &DirEntry,
384) -> Option<PathBuf> {
385    let dir_name = parse_dir_name(entry)?;
386    let dir_major_version = parse_major_version(&dir_name)?;
387
388    if dir_major_version >= major_version {
389        return None;
390    }
391
392    // Don't delete databases that can be reused.
393    if restorable_db_versions
394        .iter()
395        .map(|v| v - 1)
396        .any(|v| v == dir_major_version)
397    {
398        return None;
399    }
400
401    let outdated_path = entry.path();
402
403    // # Correctness
404    //
405    // Check that the path we're about to delete is inside the cache directory.
406    // If the user has symlinked the outdated state directory to a non-cache directory,
407    // we don't want to delete it, because it might contain other files.
408    //
409    // We don't attempt to guard against malicious symlinks created by attackers
410    // (TOCTOU attacks). Zebra should not be run with elevated privileges.
411    let cache_path = canonicalize(&config.cache_dir).ok()?;
412    let outdated_path = canonicalize(outdated_path).ok()?;
413
414    if !outdated_path.starts_with(&cache_path) {
415        info!(
416            skipped_path = ?outdated_path,
417            ?cache_path,
418            "skipped cleanup of outdated state directory: state is outside cache directory",
419        );
420
421        return None;
422    }
423
424    remove_dir_all(&outdated_path).ok().map(|()| outdated_path)
425}
426
427/// Check if `entry` is a directory with a valid UTF-8 name.
428/// (State directory names are guaranteed to be UTF-8.)
429///
430/// Returns `None` if any operation fails.
431fn parse_dir_name(entry: &DirEntry) -> Option<String> {
432    if let Ok(file_type) = entry.file_type() {
433        if file_type.is_dir() {
434            if let Ok(dir_name) = entry.file_name().into_string() {
435                return Some(dir_name);
436            }
437        }
438    }
439    None
440}
441
442/// Parse the database major version number from `dir_name`.
443///
444/// Returns `None` if parsing fails, or the directory name is not in the expected format.
445fn parse_major_version(dir_name: &str) -> Option<u64> {
446    dir_name
447        .strip_prefix('v')
448        .and_then(|version| version.parse().ok())
449}
450
451// TODO: move these to the format upgrade module
452
453/// Returns the full semantic version of the on-disk state database, based on its config and network.
454pub fn state_database_format_version_on_disk(
455    config: &Config,
456    network: &Network,
457) -> Result<Option<Version>, BoxError> {
458    database_format_version_on_disk(
459        config,
460        STATE_DATABASE_KIND,
461        state_database_format_version_in_code().major,
462        network,
463    )
464}
465
466/// Returns the full semantic version of the on-disk database, based on its config, kind, major version,
467/// and network.
468///
469/// Typically, the version is read from a version text file.
470///
471/// If there is an existing on-disk database, but no version file,
472/// returns `Ok(Some(major_version.0.0))`.
473/// (This happens even if the database directory was just newly created.)
474///
475/// If there is no existing on-disk database, returns `Ok(None)`.
476///
477/// This is the format of the data on disk, the version
478/// implemented by the running Zebra code can be different.
479pub fn database_format_version_on_disk(
480    config: &Config,
481    db_kind: impl AsRef<str>,
482    major_version: u64,
483    network: &Network,
484) -> Result<Option<Version>, BoxError> {
485    let version_path = config.version_file_path(&db_kind, major_version, network);
486    let db_path = config.db_path(db_kind, major_version, network);
487
488    database_format_version_at_path(&version_path, &db_path, major_version)
489}
490
491/// Returns the full semantic version of the on-disk database at `version_path`.
492///
493/// See [`database_format_version_on_disk()`] for details.
494pub(crate) fn database_format_version_at_path(
495    version_path: &Path,
496    db_path: &Path,
497    major_version: u64,
498) -> Result<Option<Version>, BoxError> {
499    let disk_version_file = match fs::read_to_string(version_path) {
500        Ok(version) => Some(version),
501        Err(e) if e.kind() == ErrorKind::NotFound => {
502            // If the version file doesn't exist, don't guess the version yet.
503            None
504        }
505        Err(e) => Err(e)?,
506    };
507
508    // The database has a version file on disk
509    if let Some(version) = disk_version_file {
510        return Ok(Some(
511            version
512                .parse()
513                // Try to parse the previous format of the disk version file if it cannot be parsed as a `Version` directly.
514                .or_else(|err| {
515                    format!("{major_version}.{version}")
516                        .parse()
517                        .map_err(|err2| format!("failed to parse format version: {err}, {err2}"))
518                })?,
519        ));
520    }
521
522    // There's no version file on disk, so we need to guess the version
523    // based on the database content
524    match fs::metadata(db_path) {
525        // But there is a database on disk, so it has the current major version with no upgrades.
526        // If the database directory was just newly created, we also return this version.
527        Ok(_metadata) => Ok(Some(Version::new(major_version, 0, 0))),
528
529        // There's no version file and no database on disk, so it's a new database.
530        // It will be created with the current version,
531        // but temporarily return the default version above until the version file is written.
532        Err(e) if e.kind() == ErrorKind::NotFound => Ok(None),
533
534        Err(e) => Err(e)?,
535    }
536}
537
538// Hide this destructive method from the public API, except in tests.
539#[allow(unused_imports)]
540pub(crate) use hidden::{
541    write_database_format_version_to_disk, write_state_database_format_version_to_disk,
542};
543
544pub(crate) mod hidden {
545    #![allow(dead_code)]
546
547    use zebra_chain::common::atomic_write;
548
549    use super::*;
550
551    /// Writes `changed_version` to the on-disk state database after the format is changed.
552    /// (Or a new database is created.)
553    ///
554    /// See `write_database_format_version_to_disk()` for details.
555    pub fn write_state_database_format_version_to_disk(
556        config: &Config,
557        changed_version: &Version,
558        network: &Network,
559    ) -> Result<(), BoxError> {
560        write_database_format_version_to_disk(
561            config,
562            STATE_DATABASE_KIND,
563            state_database_format_version_in_code().major,
564            changed_version,
565            network,
566        )
567    }
568
569    /// Writes `changed_version` to the on-disk database after the format is changed.
570    /// (Or a new database is created.)
571    ///
572    /// The database path is based on its kind, `major_version_in_code`, and network.
573    ///
574    /// # Correctness
575    ///
576    /// This should only be called:
577    /// - after each format upgrade is complete,
578    /// - when creating a new database, or
579    /// - when an older Zebra version opens a newer database.
580    ///
581    /// # Concurrency
582    ///
583    /// This must only be called while RocksDB has an open database for `config`.
584    /// Otherwise, multiple Zebra processes could write the version at the same time,
585    /// corrupting the file.
586    pub fn write_database_format_version_to_disk(
587        config: &Config,
588        db_kind: impl AsRef<str>,
589        major_version_in_code: u64,
590        changed_version: &Version,
591        network: &Network,
592    ) -> Result<(), BoxError> {
593        // Write the version file atomically so the cache is not corrupted if Zebra shuts down or
594        // crashes.
595        atomic_write(
596            config.version_file_path(db_kind, major_version_in_code, network),
597            changed_version.to_string().as_bytes(),
598        )??;
599
600        Ok(())
601    }
602}
603
604#[cfg(test)]
605mod tests {
606    use super::*;
607
608    #[test]
609    fn redacted_string_hides_contents_from_debug() {
610        let secret = RedactedString::from("hunter2");
611
612        assert_eq!(format!("{secret:?}"), "[REDACTED]");
613        assert_eq!(secret.as_str(), "hunter2");
614    }
615
616    #[cfg(feature = "elasticsearch")]
617    #[test]
618    fn config_debug_does_not_contain_elasticsearch_password() {
619        let config = Config {
620            elasticsearch_password: RedactedString::from("hunter2"),
621            ..Config::default()
622        };
623
624        assert!(!format!("{config:?}").contains("hunter2"));
625    }
626}