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 DiskDb::check_cache_dir_readable(&config.cache_dir)?;
111
112 database_format_version_on_disk(config, &db_kind, format_version_in_code.major, network)
113 .expect("unable to read database format version file")
114 } else {
115 DiskDb::try_reusing_previous_db_after_major_upgrade(
116 &restorable_db_versions(),
117 format_version_in_code,
118 config,
119 &db_kind,
120 network,
121 )
122 .or_else(|| {
123 database_format_version_on_disk(
124 config,
125 &db_kind,
126 format_version_in_code.major,
127 network,
128 )
129 .expect("unable to read database format version file")
130 })
131 };
132
133 // Log any format changes before opening the database, in case opening fails.
134 let format_change = DbFormatChange::open_database(format_version_in_code, disk_version);
135
136 // A read-only secondary instance cannot create a database. If there's no database on
137 // disk, fail with a clear, actionable error instead of silently "creating" one.
138 //
139 // The read-write path is unaffected: creating a new database is the correct behavior there.
140 if read_only && format_change.is_newly_created() {
141 let db_path = config.db_path(&db_kind, format_version_in_code.major, network);
142 return Err(StateInitError::ReadOnlyDatabaseNotFound { path: db_path });
143 }
144
145 // Format upgrades try to write to the database, so we always skip them
146 // if `read_only` is `true`.
147 //
148 // We also allow skipping them when we are running tests.
149 let debug_skip_format_upgrades = read_only || (cfg!(test) && debug_skip_format_upgrades);
150
151 // Open the low-level database and do initial checks.
152 //
153 // After the database directory is created, a newly created database temporarily
154 // changes to the default database version. Then we set the correct version in the
155 // upgrade thread. We need to do the version change in this order, because the version
156 // file can only be changed while we hold the RocksDB database lock.
157 let disk_db = DiskDb::new(
158 config,
159 db_kind,
160 format_version_in_code,
161 network,
162 column_families_in_code,
163 read_only,
164 )?;
165
166 let mut db = ZebraDb {
167 config: Arc::new(config.clone()),
168 debug_skip_format_upgrades,
169 format_change_handle: None,
170 db: disk_db,
171 };
172
173 let zero_location_utxos =
174 db.address_utxo_locations(AddressLocation::from_usize(Height(0), 0, 0));
175 if !zero_location_utxos.is_empty() {
176 warn!(
177 "You have been impacted by the Zebra 2.4.0 address indexer corruption bug. \
178 If you rely on the data from the RPC interface, you will need to recover your database. \
179 Follow the instructions in the 2.4.1 release notes: https://github.com/ZcashFoundation/zebra/releases/tag/v2.4.1 \
180 If you just run the node for consensus and don't use data from the RPC interface, you can ignore this warning."
181 )
182 }
183
184 db.spawn_format_change(format_change);
185
186 Ok(db)
187 }
188
189 /// Launch any required format changes or format checks, and store their thread handle.
190 pub fn spawn_format_change(&mut self, format_change: DbFormatChange) {
191 if self.debug_skip_format_upgrades {
192 return;
193 }
194
195 // We have to get this height before we spawn the upgrade task, because threads can take
196 // a while to start, and new blocks can be committed as soon as we return from this method.
197 let initial_tip_height = self.finalized_tip_height();
198
199 // `upgrade_db` is a special clone of this database, which can't be used to shut down
200 // the upgrade task. (Because the task hasn't been launched yet,
201 // its `db.format_change_handle` is always None.)
202 let upgrade_db = self.clone();
203
204 // TODO:
205 // - should debug_stop_at_height wait for the upgrade task to finish?
206 let format_change_handle =
207 format_change.spawn_format_change(upgrade_db, initial_tip_height);
208
209 self.format_change_handle = Some(format_change_handle);
210 }
211
212 /// Sets `finished_format_upgrades` to true on the inner [`DiskDb`] to indicate that Zebra has
213 /// finished applying any required db format upgrades.
214 pub fn mark_finished_format_upgrades(&self) {
215 self.db.mark_finished_format_upgrades();
216 }
217
218 /// Returns true if the `finished_format_upgrades` flag has been set to true on the inner [`DiskDb`] to
219 /// indicate that Zebra has finished applying any required db format upgrades.
220 pub fn finished_format_upgrades(&self) -> bool {
221 self.db.finished_format_upgrades()
222 }
223
224 /// Returns config for this database.
225 pub fn config(&self) -> &Config {
226 &self.config
227 }
228
229 /// Returns the configured database kind for this database.
230 pub fn db_kind(&self) -> String {
231 self.db.db_kind()
232 }
233
234 /// Returns the format version of the running code that created this `ZebraDb` instance in memory.
235 pub fn format_version_in_code(&self) -> Version {
236 self.db.format_version_in_code()
237 }
238
239 /// Returns the fixed major version for this database.
240 pub fn major_version(&self) -> u64 {
241 self.db.major_version()
242 }
243
244 /// Returns the format version of this database on disk.
245 ///
246 /// See `database_format_version_on_disk()` for details.
247 pub fn format_version_on_disk(&self) -> Result<Option<Version>, BoxError> {
248 database_format_version_on_disk(
249 self.config(),
250 self.db_kind(),
251 self.major_version(),
252 &self.network(),
253 )
254 }
255
256 /// Updates the format of this database on disk to the suppled version.
257 ///
258 /// See `write_database_format_version_to_disk()` for details.
259 pub(crate) fn update_format_version_on_disk(
260 &self,
261 new_version: &Version,
262 ) -> Result<(), BoxError> {
263 write_database_format_version_to_disk(
264 self.config(),
265 self.db_kind(),
266 self.major_version(),
267 new_version,
268 &self.network(),
269 )
270 }
271
272 /// Returns the configured network for this database.
273 pub fn network(&self) -> Network {
274 self.db.network()
275 }
276
277 /// Returns the `Path` where the files used by this database are located.
278 pub fn path(&self) -> &Path {
279 self.db.path()
280 }
281
282 /// Check for panics in code running in spawned threads.
283 /// If a thread exited with a panic, resume that panic.
284 ///
285 /// This method should be called regularly, so that panics are detected as soon as possible.
286 pub fn check_for_panics(&mut self) {
287 if let Some(format_change_handle) = self.format_change_handle.as_mut() {
288 format_change_handle.check_for_panics();
289 }
290 }
291
292 /// When called with a secondary DB instance, tries to catch up with the primary DB instance
293 pub fn try_catch_up_with_primary(&self) -> Result<(), rocksdb::Error> {
294 self.db.try_catch_up_with_primary()
295 }
296
297 /// Spawns a blocking task to try catching up with the primary DB instance.
298 pub async fn spawn_try_catch_up_with_primary(&self) -> Result<(), rocksdb::Error> {
299 let db = self.clone();
300 tokio::task::spawn_blocking(move || {
301 let result = db.try_catch_up_with_primary();
302 if let Err(catch_up_error) = &result {
303 tracing::warn!(?catch_up_error, "failed to catch up to primary");
304 }
305 result
306 })
307 .wait_for_panics()
308 .await
309 }
310
311 /// Shut down the database, cleaning up background tasks and ephemeral data.
312 ///
313 /// If `force` is true, clean up regardless of any shared references.
314 /// `force` can cause errors accessing the database from other shared references.
315 /// It should only be used in debugging or test code, immediately before a manual shutdown.
316 ///
317 /// See [`DiskDb::shutdown`] for details.
318 pub fn shutdown(&mut self, force: bool) {
319 // Are we shutting down the underlying database instance?
320 let is_shutdown = force || self.db.shared_database_owners() <= 1;
321
322 // # Concurrency
323 //
324 // The format upgrade task should be cancelled before the database is flushed or shut down.
325 // This helps avoid some kinds of deadlocks.
326 //
327 // See also the correctness note in `DiskDb::shutdown()`.
328 if !self.debug_skip_format_upgrades && is_shutdown {
329 if let Some(format_change_handle) = self.format_change_handle.as_mut() {
330 format_change_handle.force_cancel();
331 }
332
333 // # Correctness
334 //
335 // Check that the database format is correct before shutting down.
336 // This lets users know to delete and re-sync their database immediately,
337 // rather than surprising them next time Zebra starts up.
338 //
339 // # Testinng
340 //
341 // In Zebra's CI, panicking here stops us writing invalid cached states,
342 // which would then make unrelated PRs fail when Zebra starts up.
343
344 // If the upgrade has completed, or we've done a downgrade, check the state is valid.
345 let disk_version = database_format_version_on_disk(
346 &self.config,
347 self.db_kind(),
348 self.major_version(),
349 &self.network(),
350 )
351 .expect("unexpected invalid or unreadable database version file");
352
353 if let Some(disk_version) = disk_version {
354 // We need to keep the cancel handle until the format check has finished,
355 // because dropping it cancels the format check.
356 let (_never_cancel_handle, never_cancel_receiver) = bounded(1);
357
358 // We block here because the checks are quick and database validity is
359 // consensus-critical.
360 if disk_version >= self.db.format_version_in_code() {
361 DbFormatChange::check_new_blocks(self)
362 .run_format_change_or_check(
363 self,
364 // The initial tip height is not used by the new blocks format check.
365 None,
366 &never_cancel_receiver,
367 )
368 .expect("cancel handle is never used");
369 }
370 }
371 }
372
373 self.check_for_panics();
374
375 self.db.shutdown(force);
376 }
377
378 /// Check that the on-disk height is well below the maximum supported database height.
379 ///
380 /// Zebra only supports on-disk heights up to 3 bytes.
381 ///
382 /// # Logs an Error
383 ///
384 /// If Zebra is storing block heights that are close to [`MAX_ON_DISK_HEIGHT`].
385 pub(crate) fn check_max_on_disk_tip_height(&self) -> Result<(), String> {
386 if let Some((tip_height, tip_hash)) = self.tip() {
387 if tip_height.0 > MAX_ON_DISK_HEIGHT.0 / 2 {
388 let err = Err(format!(
389 "unexpectedly large tip height, database format upgrade required: \
390 tip height: {tip_height:?}, tip hash: {tip_hash:?}, \
391 max height: {MAX_ON_DISK_HEIGHT:?}"
392 ));
393 error!(?err);
394 return err;
395 }
396 }
397
398 Ok(())
399 }
400
401 /// Logs metrics related to the underlying RocksDB instance.
402 ///
403 /// This function prints various metrics and statistics about the RocksDB database,
404 /// such as disk usage, memory usage, and other performance-related metrics.
405 pub fn print_db_metrics(&self) {
406 self.db.print_db_metrics();
407 }
408
409 /// Exports RocksDB metrics to Prometheus.
410 ///
411 /// This function collects database statistics and exposes them as Prometheus metrics.
412 /// Call this periodically (e.g., every 30 seconds) from a background task.
413 pub(crate) fn export_metrics(&self) {
414 self.db.export_metrics();
415 }
416
417 /// Returns the estimated total disk space usage of the database.
418 pub fn size(&self) -> u64 {
419 self.db.size()
420 }
421}
422
423impl Drop for ZebraDb {
424 fn drop(&mut self) {
425 self.shutdown(false);
426 }
427}