zebra_state/
config.rs

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