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::parameters::Network;
18
19use crate::{
20    config::database_format_version_on_disk,
21    constants::RESTORABLE_DB_VERSIONS,
22    service::finalized_state::{
23        disk_db::DiskDb,
24        disk_format::{
25            block::MAX_ON_DISK_HEIGHT,
26            upgrade::{DbFormatChange, DbFormatChangeThreadHandle},
27        },
28    },
29    write_database_format_version_to_disk, BoxError, Config,
30};
31
32pub mod block;
33pub mod chain;
34pub mod metrics;
35pub mod shielded;
36pub mod transparent;
37
38#[cfg(any(test, feature = "proptest-impl", feature = "shielded-scan"))]
39// TODO: when the database is split out of zebra-state, always expose these methods.
40pub mod arbitrary;
41
42/// Wrapper struct to ensure high-level `zebra-state` database access goes through the correct API.
43///
44/// `rocksdb` allows concurrent writes through a shared reference,
45/// so database instances are cloneable. When the final clone is dropped,
46/// the database is closed.
47#[derive(Clone, Debug)]
48pub struct ZebraDb {
49    // Configuration
50    //
51    // This configuration cannot be modified after the database is initialized,
52    // because some clones would have different values.
53    //
54    /// The configuration for the database.
55    //
56    // TODO: move the config to DiskDb
57    config: Arc<Config>,
58
59    /// Should format upgrades and format checks be skipped for this instance?
60    /// Only used in test code.
61    //
62    // TODO: move this to DiskDb
63    debug_skip_format_upgrades: bool,
64
65    // Owned State
66    //
67    // Everything contained in this state must be shared by all clones, or read-only.
68    //
69    /// A handle to a running format change task, which cancels the task when dropped.
70    ///
71    /// # Concurrency
72    ///
73    /// This field should be dropped before the database field, so the format upgrade task is
74    /// cancelled before the database is dropped. This helps avoid some kinds of deadlocks.
75    //
76    // TODO: move the generic upgrade code and fields to DiskDb
77    format_change_handle: Option<DbFormatChangeThreadHandle>,
78
79    /// The inner low-level database wrapper for the RocksDB database.
80    db: DiskDb,
81}
82
83impl ZebraDb {
84    /// Opens or creates the database at a path based on the kind, major version and network,
85    /// with the supplied column families, preserving any existing column families,
86    /// and returns a shared high-level typed database wrapper.
87    ///
88    /// If `debug_skip_format_upgrades` is true, don't do any format upgrades or format checks.
89    /// This argument is only used when running tests, it is ignored in production code.
90    //
91    // TODO: rename to StateDb and remove the db_kind and column_families_in_code arguments
92    pub fn new(
93        config: &Config,
94        db_kind: impl AsRef<str>,
95        format_version_in_code: &Version,
96        network: &Network,
97        debug_skip_format_upgrades: bool,
98        column_families_in_code: impl IntoIterator<Item = String>,
99        read_only: bool,
100    ) -> ZebraDb {
101        let disk_version = database_format_version_on_disk(
102            config,
103            &db_kind,
104            format_version_in_code.major,
105            network,
106        )
107        .expect("unable to read database format version file");
108
109        DiskDb::try_reusing_previous_db_after_major_upgrade(
110            &RESTORABLE_DB_VERSIONS,
111            format_version_in_code,
112            config,
113            &db_kind,
114            network,
115        );
116
117        // Log any format changes before opening the database, in case opening fails.
118        let format_change = DbFormatChange::open_database(format_version_in_code, disk_version);
119
120        // Format upgrades try to write to the database, so we always skip them if `read_only` is
121        // `true`.
122        //
123        // We allow skipping the upgrades by the scanner because it doesn't support them yet and we
124        // also allow skipping them when we are running tests.
125        //
126        // TODO: Make scanner support format upgrades, then remove `shielded-scan` here.
127        let debug_skip_format_upgrades = read_only
128            || ((cfg!(test) || cfg!(feature = "shielded-scan")) && debug_skip_format_upgrades);
129
130        // Open the database and do initial checks.
131        let mut db = ZebraDb {
132            config: Arc::new(config.clone()),
133            debug_skip_format_upgrades,
134            format_change_handle: None,
135            // After the database directory is created, a newly created database temporarily
136            // changes to the default database version. Then we set the correct version in the
137            // upgrade thread. We need to do the version change in this order, because the version
138            // file can only be changed while we hold the RocksDB database lock.
139            db: DiskDb::new(
140                config,
141                db_kind,
142                format_version_in_code,
143                network,
144                column_families_in_code,
145                read_only,
146            ),
147        };
148
149        db.spawn_format_change(format_change);
150
151        db
152    }
153
154    /// Launch any required format changes or format checks, and store their thread handle.
155    pub fn spawn_format_change(&mut self, format_change: DbFormatChange) {
156        if self.debug_skip_format_upgrades {
157            return;
158        }
159
160        // We have to get this height before we spawn the upgrade task, because threads can take
161        // a while to start, and new blocks can be committed as soon as we return from this method.
162        let initial_tip_height = self.finalized_tip_height();
163
164        // `upgrade_db` is a special clone of this database, which can't be used to shut down
165        // the upgrade task. (Because the task hasn't been launched yet,
166        // its `db.format_change_handle` is always None.)
167        let upgrade_db = self.clone();
168
169        // TODO:
170        // - should debug_stop_at_height wait for the upgrade task to finish?
171        let format_change_handle =
172            format_change.spawn_format_change(upgrade_db, initial_tip_height);
173
174        self.format_change_handle = Some(format_change_handle);
175    }
176
177    /// Returns config for this database.
178    pub fn config(&self) -> &Config {
179        &self.config
180    }
181
182    /// Returns the configured database kind for this database.
183    pub fn db_kind(&self) -> String {
184        self.db.db_kind()
185    }
186
187    /// Returns the format version of the running code that created this `ZebraDb` instance in memory.
188    pub fn format_version_in_code(&self) -> Version {
189        self.db.format_version_in_code()
190    }
191
192    /// Returns the fixed major version for this database.
193    pub fn major_version(&self) -> u64 {
194        self.db.major_version()
195    }
196
197    /// Returns the format version of this database on disk.
198    ///
199    /// See `database_format_version_on_disk()` for details.
200    pub fn format_version_on_disk(&self) -> Result<Option<Version>, BoxError> {
201        database_format_version_on_disk(
202            self.config(),
203            self.db_kind(),
204            self.major_version(),
205            &self.network(),
206        )
207    }
208
209    /// Updates the format of this database on disk to the suppled version.
210    ///
211    /// See `write_database_format_version_to_disk()` for details.
212    pub(crate) fn update_format_version_on_disk(
213        &self,
214        new_version: &Version,
215    ) -> Result<(), BoxError> {
216        write_database_format_version_to_disk(
217            self.config(),
218            self.db_kind(),
219            new_version,
220            &self.network(),
221        )
222    }
223
224    /// Returns the configured network for this database.
225    pub fn network(&self) -> Network {
226        self.db.network()
227    }
228
229    /// Returns the `Path` where the files used by this database are located.
230    pub fn path(&self) -> &Path {
231        self.db.path()
232    }
233
234    /// Check for panics in code running in spawned threads.
235    /// If a thread exited with a panic, resume that panic.
236    ///
237    /// This method should be called regularly, so that panics are detected as soon as possible.
238    pub fn check_for_panics(&mut self) {
239        if let Some(format_change_handle) = self.format_change_handle.as_mut() {
240            format_change_handle.check_for_panics();
241        }
242    }
243
244    /// When called with a secondary DB instance, tries to catch up with the primary DB instance
245    pub fn try_catch_up_with_primary(&self) -> Result<(), rocksdb::Error> {
246        self.db.try_catch_up_with_primary()
247    }
248
249    /// Shut down the database, cleaning up background tasks and ephemeral data.
250    ///
251    /// If `force` is true, clean up regardless of any shared references.
252    /// `force` can cause errors accessing the database from other shared references.
253    /// It should only be used in debugging or test code, immediately before a manual shutdown.
254    ///
255    /// See [`DiskDb::shutdown`] for details.
256    pub fn shutdown(&mut self, force: bool) {
257        // Are we shutting down the underlying database instance?
258        let is_shutdown = force || self.db.shared_database_owners() <= 1;
259
260        // # Concurrency
261        //
262        // The format upgrade task should be cancelled before the database is flushed or shut down.
263        // This helps avoid some kinds of deadlocks.
264        //
265        // See also the correctness note in `DiskDb::shutdown()`.
266        if !self.debug_skip_format_upgrades && is_shutdown {
267            if let Some(format_change_handle) = self.format_change_handle.as_mut() {
268                format_change_handle.force_cancel();
269            }
270
271            // # Correctness
272            //
273            // Check that the database format is correct before shutting down.
274            // This lets users know to delete and re-sync their database immediately,
275            // rather than surprising them next time Zebra starts up.
276            //
277            // # Testinng
278            //
279            // In Zebra's CI, panicking here stops us writing invalid cached states,
280            // which would then make unrelated PRs fail when Zebra starts up.
281
282            // If the upgrade has completed, or we've done a downgrade, check the state is valid.
283            let disk_version = database_format_version_on_disk(
284                &self.config,
285                self.db_kind(),
286                self.major_version(),
287                &self.network(),
288            )
289            .expect("unexpected invalid or unreadable database version file");
290
291            if let Some(disk_version) = disk_version {
292                // We need to keep the cancel handle until the format check has finished,
293                // because dropping it cancels the format check.
294                let (_never_cancel_handle, never_cancel_receiver) = bounded(1);
295
296                // We block here because the checks are quick and database validity is
297                // consensus-critical.
298                if disk_version >= self.db.format_version_in_code() {
299                    DbFormatChange::check_new_blocks(self)
300                        .run_format_change_or_check(
301                            self,
302                            // The initial tip height is not used by the new blocks format check.
303                            None,
304                            &never_cancel_receiver,
305                        )
306                        .expect("cancel handle is never used");
307                }
308            }
309        }
310
311        self.check_for_panics();
312
313        self.db.shutdown(force);
314    }
315
316    /// Check that the on-disk height is well below the maximum supported database height.
317    ///
318    /// Zebra only supports on-disk heights up to 3 bytes.
319    ///
320    /// # Logs an Error
321    ///
322    /// If Zebra is storing block heights that are close to [`MAX_ON_DISK_HEIGHT`].
323    pub(crate) fn check_max_on_disk_tip_height(&self) -> Result<(), String> {
324        if let Some((tip_height, tip_hash)) = self.tip() {
325            if tip_height.0 > MAX_ON_DISK_HEIGHT.0 / 2 {
326                let err = Err(format!(
327                    "unexpectedly large tip height, database format upgrade required: \
328                     tip height: {tip_height:?}, tip hash: {tip_hash:?}, \
329                     max height: {MAX_ON_DISK_HEIGHT:?}"
330                ));
331                error!(?err);
332                return err;
333            }
334        }
335
336        Ok(())
337    }
338
339    /// Logs metrics related to the underlying RocksDB instance.
340    ///
341    /// This function prints various metrics and statistics about the RocksDB database,
342    /// such as disk usage, memory usage, and other performance-related metrics.
343    pub fn print_db_metrics(&self) {
344        self.db.print_db_metrics();
345    }
346
347    /// Returns the estimated total disk space usage of the database.
348    pub fn size(&self) -> u64 {
349        self.db.size()
350    }
351}
352
353impl Drop for ZebraDb {
354    fn drop(&mut self) {
355        self.shutdown(false);
356    }
357}