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