Skip to main content

zebra_state/service/finalized_state/
zebra_db.rs

1//! Provides high-level access to the database using [`zebra_chain`] types.
2//!
3//! This module makes sure that:
4//! - all disk writes happen inside a RocksDB transaction, and
5//! - format-specific invariants are maintained.
6//!
7//! # Correctness
8//!
9//! [`crate::constants::state_database_format_version_in_code()`] must be incremented
10//! each time the database format (column, serialization, etc) changes.
11
12use std::{path::Path, sync::Arc};
13
14use crossbeam_channel::bounded;
15use semver::Version;
16
17use zebra_chain::{block::Height, diagnostic::task::WaitForPanics, parameters::Network};
18
19use crate::{
20    config::database_format_version_on_disk,
21    service::finalized_state::{
22        disk_db::DiskDb,
23        disk_format::{
24            block::MAX_ON_DISK_HEIGHT,
25            transparent::AddressLocation,
26            upgrade::{DbFormatChange, DbFormatChangeThreadHandle},
27        },
28    },
29    write_database_format_version_to_disk, BoxError, Config, StateInitError,
30};
31
32use super::disk_format::upgrade::restorable_db_versions;
33
34pub mod block;
35pub mod chain;
36pub mod metrics;
37pub mod shielded;
38pub mod transparent;
39
40#[cfg(any(test, feature = "proptest-impl"))]
41// TODO: when the database is split out of zebra-state, always expose these methods.
42pub mod arbitrary;
43
44/// Wrapper struct to ensure high-level `zebra-state` database access goes through the correct API.
45///
46/// `rocksdb` allows concurrent writes through a shared reference,
47/// so database instances are cloneable. When the final clone is dropped,
48/// the database is closed.
49#[derive(Clone, Debug)]
50pub struct ZebraDb {
51    // Configuration
52    //
53    // This configuration cannot be modified after the database is initialized,
54    // because some clones would have different values.
55    //
56    /// The configuration for the database.
57    //
58    // TODO: move the config to DiskDb
59    config: Arc<Config>,
60
61    /// Should format upgrades and format checks be skipped for this instance?
62    /// Only used in test code.
63    //
64    // TODO: move this to DiskDb
65    debug_skip_format_upgrades: bool,
66
67    // Owned State
68    //
69    // Everything contained in this state must be shared by all clones, or read-only.
70    //
71    /// A handle to a running format change task, which cancels the task when dropped.
72    ///
73    /// # Concurrency
74    ///
75    /// This field should be dropped before the database field, so the format upgrade task is
76    /// cancelled before the database is dropped. This helps avoid some kinds of deadlocks.
77    //
78    // TODO: move the generic upgrade code and fields to DiskDb
79    format_change_handle: Option<DbFormatChangeThreadHandle>,
80
81    /// The inner low-level database wrapper for the RocksDB database.
82    db: DiskDb,
83}
84
85impl ZebraDb {
86    /// Opens or creates the database at a path based on the kind, major version and network,
87    /// with the supplied column families, preserving any existing column families,
88    /// and returns a shared high-level typed database wrapper.
89    ///
90    /// If `debug_skip_format_upgrades` is true, don't do any format upgrades or format checks.
91    /// This argument is only used when running tests, it is ignored in production code.
92    //
93    // TODO: rename to StateDb and remove the db_kind and column_families_in_code arguments
94    #[allow(clippy::unwrap_in_result)]
95    pub fn new(
96        config: &Config,
97        db_kind: impl AsRef<str>,
98        format_version_in_code: &Version,
99        network: &Network,
100        debug_skip_format_upgrades: bool,
101        column_families_in_code: impl IntoIterator<Item = String>,
102        read_only: bool,
103    ) -> Result<ZebraDb, StateInitError> {
104        // A read-only secondary instance must never modify the primary's cache directory, so it
105        // skips the post-major-upgrade DB reuse (which can create directories and rename the
106        // on-disk database) and reads the on-disk format version directly. The cache directory is
107        // checked for readability first, so a missing or unreadable directory returns a typed
108        // `ReadOnlyCacheDirUnreadable` error here instead of panicking on the version-file read.
109        let disk_version = if read_only {
110            // While this check is also done in `DiskDB::new()` below, we must
111            // repeat it here because the `check_cache_dir_readable()` call just
112            // after this will look into `cache_dir` but that should be ignored
113            // when `ephemeral` is true.
114            if config.ephemeral {
115                return Err(StateInitError::ReadOnlyEphemeralConflict);
116            }
117
118            DiskDb::check_cache_dir_readable(&config.cache_dir)?;
119
120            database_format_version_on_disk(config, &db_kind, format_version_in_code.major, network)
121                .expect("unable to read database format version file")
122        } else {
123            DiskDb::try_reusing_previous_db_after_major_upgrade(
124                &restorable_db_versions(),
125                format_version_in_code,
126                config,
127                &db_kind,
128                network,
129            )
130            .or_else(|| {
131                database_format_version_on_disk(
132                    config,
133                    &db_kind,
134                    format_version_in_code.major,
135                    network,
136                )
137                .expect("unable to read database format version file")
138            })
139        };
140
141        // Log any format changes before opening the database, in case opening fails.
142        let format_change = DbFormatChange::open_database(format_version_in_code, disk_version);
143
144        // A read-only secondary instance cannot create a database. If there's no database on
145        // disk, fail with a clear, actionable error instead of silently "creating" one.
146        //
147        // The read-write path is unaffected: creating a new database is the correct behavior there.
148        if read_only && format_change.is_newly_created() {
149            let db_path = config.db_path(&db_kind, format_version_in_code.major, network);
150            return Err(StateInitError::ReadOnlyDatabaseNotFound { path: db_path });
151        }
152
153        // Format upgrades try to write to the database, so we always skip them
154        // if `read_only` is `true`.
155        //
156        // We also allow skipping them when we are running tests.
157        let debug_skip_format_upgrades = read_only || (cfg!(test) && debug_skip_format_upgrades);
158
159        // Open the low-level database and do initial checks.
160        //
161        // After the database directory is created, a newly created database temporarily
162        // changes to the default database version. Then we set the correct version in the
163        // upgrade thread. We need to do the version change in this order, because the version
164        // file can only be changed while we hold the RocksDB database lock.
165        let disk_db = DiskDb::new(
166            config,
167            db_kind,
168            format_version_in_code,
169            network,
170            column_families_in_code,
171            read_only,
172        )?;
173
174        let mut db = ZebraDb {
175            config: Arc::new(config.clone()),
176            debug_skip_format_upgrades,
177            format_change_handle: None,
178            db: disk_db,
179        };
180
181        let zero_location_utxos =
182            db.address_utxo_locations(AddressLocation::from_usize(Height(0), 0, 0));
183        if !zero_location_utxos.is_empty() {
184            warn!(
185                "You have been impacted by the Zebra 2.4.0 address indexer corruption bug. \
186                If you rely on the data from the RPC interface, you will need to recover your database. \
187                Follow the instructions in the 2.4.1 release notes: https://github.com/ZcashFoundation/zebra/releases/tag/v2.4.1 \
188                If you just run the node for consensus and don't use data from the RPC interface, you can ignore this warning."
189            )
190        }
191
192        db.spawn_format_change(format_change);
193
194        Ok(db)
195    }
196
197    /// Launch any required format changes or format checks, and store their thread handle.
198    pub fn spawn_format_change(&mut self, format_change: DbFormatChange) {
199        if self.debug_skip_format_upgrades {
200            return;
201        }
202
203        // We have to get this height before we spawn the upgrade task, because threads can take
204        // a while to start, and new blocks can be committed as soon as we return from this method.
205        let initial_tip_height = self.finalized_tip_height();
206
207        // `upgrade_db` is a special clone of this database, which can't be used to shut down
208        // the upgrade task. (Because the task hasn't been launched yet,
209        // its `db.format_change_handle` is always None.)
210        let upgrade_db = self.clone();
211
212        // TODO:
213        // - should debug_stop_at_height wait for the upgrade task to finish?
214        let format_change_handle =
215            format_change.spawn_format_change(upgrade_db, initial_tip_height);
216
217        self.format_change_handle = Some(format_change_handle);
218    }
219
220    /// Sets `finished_format_upgrades` to true on the inner [`DiskDb`] to indicate that Zebra has
221    /// finished applying any required db format upgrades.
222    pub fn mark_finished_format_upgrades(&self) {
223        self.db.mark_finished_format_upgrades();
224    }
225
226    /// Returns true if the `finished_format_upgrades` flag has been set to true on the inner [`DiskDb`] to
227    /// indicate that Zebra has finished applying any required db format upgrades.
228    pub fn finished_format_upgrades(&self) -> bool {
229        self.db.finished_format_upgrades()
230    }
231
232    /// Returns config for this database.
233    pub fn config(&self) -> &Config {
234        &self.config
235    }
236
237    /// Returns the configured database kind for this database.
238    pub fn db_kind(&self) -> String {
239        self.db.db_kind()
240    }
241
242    /// Returns the format version of the running code that created this `ZebraDb` instance in memory.
243    pub fn format_version_in_code(&self) -> Version {
244        self.db.format_version_in_code()
245    }
246
247    /// Returns the fixed major version for this database.
248    pub fn major_version(&self) -> u64 {
249        self.db.major_version()
250    }
251
252    /// Returns the format version of this database on disk.
253    ///
254    /// See `database_format_version_on_disk()` for details.
255    pub fn format_version_on_disk(&self) -> Result<Option<Version>, BoxError> {
256        database_format_version_on_disk(
257            self.config(),
258            self.db_kind(),
259            self.major_version(),
260            &self.network(),
261        )
262    }
263
264    /// Updates the format of this database on disk to the suppled version.
265    ///
266    /// See `write_database_format_version_to_disk()` for details.
267    pub(crate) fn update_format_version_on_disk(
268        &self,
269        new_version: &Version,
270    ) -> Result<(), BoxError> {
271        write_database_format_version_to_disk(
272            self.config(),
273            self.db_kind(),
274            self.major_version(),
275            new_version,
276            &self.network(),
277        )
278    }
279
280    /// Returns the configured network for this database.
281    pub fn network(&self) -> Network {
282        self.db.network()
283    }
284
285    /// Returns the `Path` where the files used by this database are located.
286    pub fn path(&self) -> &Path {
287        self.db.path()
288    }
289
290    /// Check for panics in code running in spawned threads.
291    /// If a thread exited with a panic, resume that panic.
292    ///
293    /// This method should be called regularly, so that panics are detected as soon as possible.
294    pub fn check_for_panics(&mut self) {
295        if let Some(format_change_handle) = self.format_change_handle.as_mut() {
296            format_change_handle.check_for_panics();
297        }
298    }
299
300    /// When called with a secondary DB instance, tries to catch up with the primary DB instance
301    pub fn try_catch_up_with_primary(&self) -> Result<(), rocksdb::Error> {
302        self.db.try_catch_up_with_primary()
303    }
304
305    /// Spawns a blocking task to try catching up with the primary DB instance.
306    pub async fn spawn_try_catch_up_with_primary(&self) -> Result<(), rocksdb::Error> {
307        let db = self.clone();
308        tokio::task::spawn_blocking(move || {
309            let result = db.try_catch_up_with_primary();
310            if let Err(catch_up_error) = &result {
311                tracing::warn!(?catch_up_error, "failed to catch up to primary");
312            }
313            result
314        })
315        .wait_for_panics()
316        .await
317    }
318
319    /// Shut down the database, cleaning up background tasks and ephemeral data.
320    ///
321    /// If `force` is true, clean up regardless of any shared references.
322    /// `force` can cause errors accessing the database from other shared references.
323    /// It should only be used in debugging or test code, immediately before a manual shutdown.
324    ///
325    /// See [`DiskDb::shutdown`] for details.
326    pub fn shutdown(&mut self, force: bool) {
327        // Are we shutting down the underlying database instance?
328        let is_shutdown = force || self.db.shared_database_owners() <= 1;
329
330        // # Concurrency
331        //
332        // The format upgrade task should be cancelled before the database is flushed or shut down.
333        // This helps avoid some kinds of deadlocks.
334        //
335        // See also the correctness note in `DiskDb::shutdown()`.
336        if !self.debug_skip_format_upgrades && is_shutdown {
337            if let Some(format_change_handle) = self.format_change_handle.as_mut() {
338                format_change_handle.force_cancel();
339            }
340
341            // # Correctness
342            //
343            // Check that the database format is correct before shutting down.
344            // This lets users know to delete and re-sync their database immediately,
345            // rather than surprising them next time Zebra starts up.
346            //
347            // # Testinng
348            //
349            // In Zebra's CI, panicking here stops us writing invalid cached states,
350            // which would then make unrelated PRs fail when Zebra starts up.
351
352            // If the upgrade has completed, or we've done a downgrade, check the state is valid.
353            let disk_version = database_format_version_on_disk(
354                &self.config,
355                self.db_kind(),
356                self.major_version(),
357                &self.network(),
358            )
359            .expect("unexpected invalid or unreadable database version file");
360
361            if let Some(disk_version) = disk_version {
362                // We need to keep the cancel handle until the format check has finished,
363                // because dropping it cancels the format check.
364                let (_never_cancel_handle, never_cancel_receiver) = bounded(1);
365
366                // We block here because the checks are quick and database validity is
367                // consensus-critical.
368                if disk_version >= self.db.format_version_in_code() {
369                    DbFormatChange::check_new_blocks(self)
370                        .run_format_change_or_check(
371                            self,
372                            // The initial tip height is not used by the new blocks format check.
373                            None,
374                            &never_cancel_receiver,
375                        )
376                        .expect("cancel handle is never used");
377                }
378            }
379        }
380
381        self.check_for_panics();
382
383        self.db.shutdown(force);
384    }
385
386    /// Check that the on-disk height is well below the maximum supported database height.
387    ///
388    /// Zebra only supports on-disk heights up to 3 bytes.
389    ///
390    /// # Logs an Error
391    ///
392    /// If Zebra is storing block heights that are close to [`MAX_ON_DISK_HEIGHT`].
393    pub(crate) fn check_max_on_disk_tip_height(&self) -> Result<(), String> {
394        if let Some((tip_height, tip_hash)) = self.tip() {
395            if tip_height.0 > MAX_ON_DISK_HEIGHT.0 / 2 {
396                let err = Err(format!(
397                    "unexpectedly large tip height, database format upgrade required: \
398                     tip height: {tip_height:?}, tip hash: {tip_hash:?}, \
399                     max height: {MAX_ON_DISK_HEIGHT:?}"
400                ));
401                error!(?err);
402                return err;
403            }
404        }
405
406        Ok(())
407    }
408
409    /// Logs metrics related to the underlying RocksDB instance.
410    ///
411    /// This function prints various metrics and statistics about the RocksDB database,
412    /// such as disk usage, memory usage, and other performance-related metrics.
413    pub fn print_db_metrics(&self) {
414        self.db.print_db_metrics();
415    }
416
417    /// Exports RocksDB metrics to Prometheus.
418    ///
419    /// This function collects database statistics and exposes them as Prometheus metrics.
420    /// Call this periodically (e.g., every 30 seconds) from a background task.
421    pub(crate) fn export_metrics(&self) {
422        self.db.export_metrics();
423    }
424
425    /// Returns the estimated total disk space usage of the database.
426    pub fn size(&self) -> u64 {
427        self.db.size()
428    }
429}
430
431impl Drop for ZebraDb {
432    fn drop(&mut self) {
433        self.shutdown(false);
434    }
435}