Skip to main content

zebra_state/service/finalized_state/
disk_db.rs

1//! Provides low-level access to RocksDB using some database-specific types.
2//!
3//! This module makes sure that:
4//! - all disk writes happen inside a RocksDB transaction
5//!   ([`rocksdb::WriteBatch`]), and
6//! - format-specific invariants are maintained.
7//!
8//! # Correctness
9//!
10//! [`crate::constants::state_database_format_version_in_code()`] must be incremented
11//! each time the database format (column, serialization, etc) changes.
12
13use std::{
14    collections::{BTreeMap, HashMap},
15    fmt::{Debug, Write},
16    fs,
17    ops::RangeBounds,
18    path::Path,
19    sync::{
20        atomic::{self, AtomicBool},
21        Arc,
22    },
23};
24
25use itertools::Itertools;
26use rlimit::increase_nofile_limit;
27
28use rocksdb::{ColumnFamilyDescriptor, ErrorKind, Options, ReadOptions};
29use semver::Version;
30use zebra_chain::{parameters::Network, primitives::byte_array::increment_big_endian};
31
32use crate::{
33    database_format_version_on_disk,
34    service::finalized_state::disk_format::{FromDisk, IntoDisk},
35    write_database_format_version_to_disk, Config, StateInitError,
36};
37
38use super::zebra_db::transparent::{
39    fetch_add_balance_and_received, BALANCE_BY_TRANSPARENT_ADDR,
40    BALANCE_BY_TRANSPARENT_ADDR_MERGE_OP,
41};
42// Doc-only imports
43#[allow(unused_imports)]
44use super::{TypedColumnFamily, WriteTypedBatch};
45
46#[cfg(any(test, feature = "proptest-impl"))]
47mod tests;
48
49/// The [`rocksdb::ThreadMode`] used by the database.
50pub type DBThreadMode = rocksdb::SingleThreaded;
51
52/// The [`rocksdb`] database type, including thread mode.
53///
54/// Also the [`rocksdb::DBAccess`] used by database iterators.
55pub type DB = rocksdb::DBWithThreadMode<DBThreadMode>;
56
57/// Wrapper struct to ensure low-level database access goes through the correct API.
58///
59/// `rocksdb` allows concurrent writes through a shared reference,
60/// so database instances are cloneable. When the final clone is dropped,
61/// the database is closed.
62///
63/// # Correctness
64///
65/// Reading transactions from the database using RocksDB iterators causes hangs.
66/// But creating iterators and reading the tip height works fine.
67///
68/// So these hangs are probably caused by holding column family locks to read:
69/// - multiple values, or
70/// - large values.
71///
72/// This bug might be fixed by moving database operations to blocking threads (#2188),
73/// so that they don't block the tokio executor.
74/// (Or it might be fixed by future RocksDB upgrades.)
75#[derive(Clone, Debug)]
76pub struct DiskDb {
77    // Configuration
78    //
79    // This configuration cannot be modified after the database is initialized,
80    // because some clones would have different values.
81    //
82    /// The configured database kind for this database.
83    db_kind: String,
84
85    /// The format version of the running Zebra code.
86    format_version_in_code: Version,
87
88    /// The configured network for this database.
89    network: Network,
90
91    /// How this database was opened: a read-write primary (persistent or ephemeral)
92    /// or a read-only secondary. See [`DbMode`].
93    mode: DbMode,
94
95    /// A boolean flag indicating whether the db format change task has finished
96    /// applying any format changes that may have been required.
97    finished_format_upgrades: Arc<AtomicBool>,
98
99    // Owned State
100    //
101    // Everything contained in this state must be shared by all clones, or read-only.
102    //
103    /// The shared inner RocksDB database.
104    ///
105    /// RocksDB allows reads and writes via a shared reference.
106    ///
107    /// In [`SingleThreaded`](rocksdb::SingleThreaded) mode,
108    /// column family changes and [`Drop`] require exclusive access.
109    ///
110    /// In [`MultiThreaded`](rocksdb::MultiThreaded) mode,
111    /// only [`Drop`] requires exclusive access.
112    db: Arc<DB>,
113}
114
115/// Wrapper struct to ensure low-level database writes go through the correct API.
116///
117/// [`rocksdb::WriteBatch`] is a batched set of database updates,
118/// which must be written to the database using `DiskDb::write(batch)`.
119#[must_use = "batches must be written to the database"]
120#[derive(Default)]
121pub struct DiskWriteBatch {
122    /// The inner RocksDB write batch.
123    batch: rocksdb::WriteBatch,
124}
125
126impl Debug for DiskWriteBatch {
127    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128        f.debug_struct("DiskWriteBatch")
129            .field("batch", &format!("{} bytes", self.batch.size_in_bytes()))
130            .finish()
131    }
132}
133
134impl PartialEq for DiskWriteBatch {
135    fn eq(&self, other: &Self) -> bool {
136        self.batch.data() == other.batch.data()
137    }
138}
139
140impl Eq for DiskWriteBatch {}
141
142/// Helper trait for inserting serialized typed (Key, Value) pairs into rocksdb.
143///
144/// # Deprecation
145///
146/// This trait should not be used in new code, use [`WriteTypedBatch`] instead.
147//
148// TODO: replace uses of this trait with WriteTypedBatch,
149//       implement these methods directly on WriteTypedBatch, and delete the trait.
150pub trait WriteDisk {
151    /// Serialize and insert the given key and value into a rocksdb column family,
152    /// overwriting any existing `value` for `key`.
153    fn zs_insert<C, K, V>(&mut self, cf: &C, key: K, value: V)
154    where
155        C: rocksdb::AsColumnFamilyRef,
156        K: IntoDisk + Debug,
157        V: IntoDisk;
158
159    /// Serialize and merge the given key and value into a rocksdb column family,
160    /// merging with any existing `value` for `key`.
161    fn zs_merge<C, K, V>(&mut self, cf: &C, key: K, value: V)
162    where
163        C: rocksdb::AsColumnFamilyRef,
164        K: IntoDisk + Debug,
165        V: IntoDisk;
166
167    /// Remove the given key from a rocksdb column family, if it exists.
168    fn zs_delete<C, K>(&mut self, cf: &C, key: K)
169    where
170        C: rocksdb::AsColumnFamilyRef,
171        K: IntoDisk + Debug;
172
173    /// Delete the given key range from a rocksdb column family, if it exists, including `from`
174    /// and excluding `until_strictly_before`.
175    //
176    // TODO: convert zs_delete_range() to take std::ops::RangeBounds
177    //       see zs_range_iter() for an example of the edge cases
178    fn zs_delete_range<C, K>(&mut self, cf: &C, from: K, until_strictly_before: K)
179    where
180        C: rocksdb::AsColumnFamilyRef,
181        K: IntoDisk + Debug;
182}
183
184/// # Deprecation
185///
186/// These impls should not be used in new code, use [`WriteTypedBatch`] instead.
187//
188// TODO: replace uses of these impls with WriteTypedBatch,
189//       implement these methods directly on WriteTypedBatch, and delete the trait.
190impl WriteDisk for DiskWriteBatch {
191    fn zs_insert<C, K, V>(&mut self, cf: &C, key: K, value: V)
192    where
193        C: rocksdb::AsColumnFamilyRef,
194        K: IntoDisk + Debug,
195        V: IntoDisk,
196    {
197        let key_bytes = key.as_bytes();
198        let value_bytes = value.as_bytes();
199        self.batch.put_cf(cf, key_bytes, value_bytes);
200    }
201
202    fn zs_merge<C, K, V>(&mut self, cf: &C, key: K, value: V)
203    where
204        C: rocksdb::AsColumnFamilyRef,
205        K: IntoDisk + Debug,
206        V: IntoDisk,
207    {
208        let key_bytes = key.as_bytes();
209        let value_bytes = value.as_bytes();
210        self.batch.merge_cf(cf, key_bytes, value_bytes);
211    }
212
213    fn zs_delete<C, K>(&mut self, cf: &C, key: K)
214    where
215        C: rocksdb::AsColumnFamilyRef,
216        K: IntoDisk + Debug,
217    {
218        let key_bytes = key.as_bytes();
219        self.batch.delete_cf(cf, key_bytes);
220    }
221
222    // TODO: convert zs_delete_range() to take std::ops::RangeBounds
223    //       see zs_range_iter() for an example of the edge cases
224    fn zs_delete_range<C, K>(&mut self, cf: &C, from: K, until_strictly_before: K)
225    where
226        C: rocksdb::AsColumnFamilyRef,
227        K: IntoDisk + Debug,
228    {
229        let from_bytes = from.as_bytes();
230        let until_strictly_before_bytes = until_strictly_before.as_bytes();
231        self.batch
232            .delete_range_cf(cf, from_bytes, until_strictly_before_bytes);
233    }
234}
235
236// Allow &mut DiskWriteBatch as well as owned DiskWriteBatch
237impl<T> WriteDisk for &mut T
238where
239    T: WriteDisk,
240{
241    fn zs_insert<C, K, V>(&mut self, cf: &C, key: K, value: V)
242    where
243        C: rocksdb::AsColumnFamilyRef,
244        K: IntoDisk + Debug,
245        V: IntoDisk,
246    {
247        (*self).zs_insert(cf, key, value)
248    }
249
250    fn zs_merge<C, K, V>(&mut self, cf: &C, key: K, value: V)
251    where
252        C: rocksdb::AsColumnFamilyRef,
253        K: IntoDisk + Debug,
254        V: IntoDisk,
255    {
256        (*self).zs_merge(cf, key, value)
257    }
258
259    fn zs_delete<C, K>(&mut self, cf: &C, key: K)
260    where
261        C: rocksdb::AsColumnFamilyRef,
262        K: IntoDisk + Debug,
263    {
264        (*self).zs_delete(cf, key)
265    }
266
267    fn zs_delete_range<C, K>(&mut self, cf: &C, from: K, until_strictly_before: K)
268    where
269        C: rocksdb::AsColumnFamilyRef,
270        K: IntoDisk + Debug,
271    {
272        (*self).zs_delete_range(cf, from, until_strictly_before)
273    }
274}
275
276/// Helper trait for retrieving and deserializing values from rocksdb column families.
277///
278/// # Deprecation
279///
280/// This trait should not be used in new code, use [`TypedColumnFamily`] instead.
281//
282// TODO: replace uses of this trait with TypedColumnFamily,
283//       implement these methods directly on DiskDb, and delete the trait.
284pub trait ReadDisk {
285    /// Returns true if a rocksdb column family `cf` does not contain any entries.
286    fn zs_is_empty<C>(&self, cf: &C) -> bool
287    where
288        C: rocksdb::AsColumnFamilyRef;
289
290    /// Returns the value for `key` in the rocksdb column family `cf`, if present.
291    fn zs_get<C, K, V>(&self, cf: &C, key: &K) -> Option<V>
292    where
293        C: rocksdb::AsColumnFamilyRef,
294        K: IntoDisk,
295        V: FromDisk;
296
297    /// Check if a rocksdb column family `cf` contains the serialized form of `key`.
298    fn zs_contains<C, K>(&self, cf: &C, key: &K) -> bool
299    where
300        C: rocksdb::AsColumnFamilyRef,
301        K: IntoDisk;
302
303    /// Returns the lowest key in `cf`, and the corresponding value.
304    ///
305    /// Returns `None` if the column family is empty.
306    fn zs_first_key_value<C, K, V>(&self, cf: &C) -> Option<(K, V)>
307    where
308        C: rocksdb::AsColumnFamilyRef,
309        K: IntoDisk + FromDisk,
310        V: FromDisk;
311
312    /// Returns the highest key in `cf`, and the corresponding value.
313    ///
314    /// Returns `None` if the column family is empty.
315    fn zs_last_key_value<C, K, V>(&self, cf: &C) -> Option<(K, V)>
316    where
317        C: rocksdb::AsColumnFamilyRef,
318        K: IntoDisk + FromDisk,
319        V: FromDisk;
320
321    /// Returns the first key greater than or equal to `lower_bound` in `cf`,
322    /// and the corresponding value.
323    ///
324    /// Returns `None` if there are no keys greater than or equal to `lower_bound`.
325    fn zs_next_key_value_from<C, K, V>(&self, cf: &C, lower_bound: &K) -> Option<(K, V)>
326    where
327        C: rocksdb::AsColumnFamilyRef,
328        K: IntoDisk + FromDisk,
329        V: FromDisk;
330
331    /// Returns the first key strictly greater than `lower_bound` in `cf`,
332    /// and the corresponding value.
333    ///
334    /// Returns `None` if there are no keys greater than `lower_bound`.
335    fn zs_next_key_value_strictly_after<C, K, V>(&self, cf: &C, lower_bound: &K) -> Option<(K, V)>
336    where
337        C: rocksdb::AsColumnFamilyRef,
338        K: IntoDisk + FromDisk,
339        V: FromDisk;
340
341    /// Returns the first key less than or equal to `upper_bound` in `cf`,
342    /// and the corresponding value.
343    ///
344    /// Returns `None` if there are no keys less than or equal to `upper_bound`.
345    fn zs_prev_key_value_back_from<C, K, V>(&self, cf: &C, upper_bound: &K) -> Option<(K, V)>
346    where
347        C: rocksdb::AsColumnFamilyRef,
348        K: IntoDisk + FromDisk,
349        V: FromDisk;
350
351    /// Returns the first key strictly less than `upper_bound` in `cf`,
352    /// and the corresponding value.
353    ///
354    /// Returns `None` if there are no keys less than `upper_bound`.
355    fn zs_prev_key_value_strictly_before<C, K, V>(&self, cf: &C, upper_bound: &K) -> Option<(K, V)>
356    where
357        C: rocksdb::AsColumnFamilyRef,
358        K: IntoDisk + FromDisk,
359        V: FromDisk;
360
361    /// Returns the keys and values in `cf` in `range`, in an ordered `BTreeMap`.
362    ///
363    /// Holding this iterator open might delay block commit transactions.
364    fn zs_items_in_range_ordered<C, K, V, R>(&self, cf: &C, range: R) -> BTreeMap<K, V>
365    where
366        C: rocksdb::AsColumnFamilyRef,
367        K: IntoDisk + FromDisk + Ord,
368        V: FromDisk,
369        R: RangeBounds<K>;
370
371    /// Returns the keys and values in `cf` in `range`, in an unordered `HashMap`.
372    ///
373    /// Holding this iterator open might delay block commit transactions.
374    fn zs_items_in_range_unordered<C, K, V, R>(&self, cf: &C, range: R) -> HashMap<K, V>
375    where
376        C: rocksdb::AsColumnFamilyRef,
377        K: IntoDisk + FromDisk + Eq + std::hash::Hash,
378        V: FromDisk,
379        R: RangeBounds<K>;
380}
381
382/// How a [`DiskDb`] instance is opened.
383///
384/// These modes are mutually exclusive — an ephemeral database is a read-write primary
385/// this process owns and deletes on drop, a persistent database is a read-write primary
386/// whose files are kept, and a read-only secondary follows another process's primary and
387/// never writes, flushes, or deletes it.
388#[derive(Clone, Copy, Debug, PartialEq, Eq)]
389enum DbMode {
390    /// A read-write primary backed by persistent on-disk files that are kept on drop.
391    Persistent,
392
393    /// A read-write primary backed by temporary files that are deleted on drop.
394    Ephemeral,
395
396    /// A read-only secondary that follows an existing primary's on-disk state. It owns
397    /// no files and never writes, flushes, or deletes them.
398    ReadOnlySecondary,
399}
400
401impl DbMode {
402    /// Returns true if dropping the database should delete its files.
403    fn deletes_files_on_drop(self) -> bool {
404        matches!(self, DbMode::Ephemeral)
405    }
406
407    /// Returns true if this is a read-only secondary instance, which RocksDB does not
408    /// allow writes or flushes on.
409    fn is_read_only(self) -> bool {
410        matches!(self, DbMode::ReadOnlySecondary)
411    }
412}
413
414impl PartialEq for DiskDb {
415    fn eq(&self, other: &Self) -> bool {
416        if self.db.path() == other.db.path() {
417            assert_eq!(
418                self.network, other.network,
419                "database with same path but different network configs",
420            );
421            assert_eq!(
422                self.mode.deletes_files_on_drop(),
423                other.mode.deletes_files_on_drop(),
424                "database with same path but different ephemeral configs",
425            );
426
427            return true;
428        }
429
430        false
431    }
432}
433
434impl Eq for DiskDb {}
435
436/// # Deprecation
437///
438/// These impls should not be used in new code, use [`TypedColumnFamily`] instead.
439//
440// TODO: replace uses of these impls with TypedColumnFamily,
441//       implement these methods directly on DiskDb, and delete the trait.
442impl ReadDisk for DiskDb {
443    fn zs_is_empty<C>(&self, cf: &C) -> bool
444    where
445        C: rocksdb::AsColumnFamilyRef,
446    {
447        // Empty column families return invalid forward iterators.
448        //
449        // Checking iterator validity does not seem to cause database hangs.
450        let iterator = self.db.iterator_cf(cf, rocksdb::IteratorMode::Start);
451        let raw_iterator: rocksdb::DBRawIteratorWithThreadMode<DB> = iterator.into();
452
453        !raw_iterator.valid()
454    }
455
456    #[allow(clippy::unwrap_in_result)]
457    fn zs_get<C, K, V>(&self, cf: &C, key: &K) -> Option<V>
458    where
459        C: rocksdb::AsColumnFamilyRef,
460        K: IntoDisk,
461        V: FromDisk,
462    {
463        let key_bytes = key.as_bytes();
464
465        // We use `get_pinned_cf` to avoid taking ownership of the serialized
466        // value, because we're going to deserialize it anyways, which avoids an
467        // extra copy
468        let value_bytes = self
469            .db
470            .get_pinned_cf(cf, key_bytes)
471            .expect("unexpected database failure");
472
473        value_bytes.map(V::from_bytes)
474    }
475
476    fn zs_contains<C, K>(&self, cf: &C, key: &K) -> bool
477    where
478        C: rocksdb::AsColumnFamilyRef,
479        K: IntoDisk,
480    {
481        let key_bytes = key.as_bytes();
482
483        // We use `get_pinned_cf` to avoid taking ownership of the serialized
484        // value, because we don't use the value at all. This avoids an extra copy.
485        self.db
486            .get_pinned_cf(cf, key_bytes)
487            .expect("unexpected database failure")
488            .is_some()
489    }
490
491    fn zs_first_key_value<C, K, V>(&self, cf: &C) -> Option<(K, V)>
492    where
493        C: rocksdb::AsColumnFamilyRef,
494        K: IntoDisk + FromDisk,
495        V: FromDisk,
496    {
497        // Reading individual values from iterators does not seem to cause database hangs.
498        self.zs_forward_range_iter(cf, ..).next()
499    }
500
501    fn zs_last_key_value<C, K, V>(&self, cf: &C) -> Option<(K, V)>
502    where
503        C: rocksdb::AsColumnFamilyRef,
504        K: IntoDisk + FromDisk,
505        V: FromDisk,
506    {
507        // Reading individual values from iterators does not seem to cause database hangs.
508        self.zs_reverse_range_iter(cf, ..).next()
509    }
510
511    fn zs_next_key_value_from<C, K, V>(&self, cf: &C, lower_bound: &K) -> Option<(K, V)>
512    where
513        C: rocksdb::AsColumnFamilyRef,
514        K: IntoDisk + FromDisk,
515        V: FromDisk,
516    {
517        self.zs_forward_range_iter(cf, lower_bound..).next()
518    }
519
520    fn zs_next_key_value_strictly_after<C, K, V>(&self, cf: &C, lower_bound: &K) -> Option<(K, V)>
521    where
522        C: rocksdb::AsColumnFamilyRef,
523        K: IntoDisk + FromDisk,
524        V: FromDisk,
525    {
526        use std::ops::Bound::*;
527
528        // There is no standard syntax for an excluded start bound.
529        self.zs_forward_range_iter(cf, (Excluded(lower_bound), Unbounded))
530            .next()
531    }
532
533    fn zs_prev_key_value_back_from<C, K, V>(&self, cf: &C, upper_bound: &K) -> Option<(K, V)>
534    where
535        C: rocksdb::AsColumnFamilyRef,
536        K: IntoDisk + FromDisk,
537        V: FromDisk,
538    {
539        self.zs_reverse_range_iter(cf, ..=upper_bound).next()
540    }
541
542    fn zs_prev_key_value_strictly_before<C, K, V>(&self, cf: &C, upper_bound: &K) -> Option<(K, V)>
543    where
544        C: rocksdb::AsColumnFamilyRef,
545        K: IntoDisk + FromDisk,
546        V: FromDisk,
547    {
548        self.zs_reverse_range_iter(cf, ..upper_bound).next()
549    }
550
551    fn zs_items_in_range_ordered<C, K, V, R>(&self, cf: &C, range: R) -> BTreeMap<K, V>
552    where
553        C: rocksdb::AsColumnFamilyRef,
554        K: IntoDisk + FromDisk + Ord,
555        V: FromDisk,
556        R: RangeBounds<K>,
557    {
558        self.zs_forward_range_iter(cf, range).collect()
559    }
560
561    fn zs_items_in_range_unordered<C, K, V, R>(&self, cf: &C, range: R) -> HashMap<K, V>
562    where
563        C: rocksdb::AsColumnFamilyRef,
564        K: IntoDisk + FromDisk + Eq + std::hash::Hash,
565        V: FromDisk,
566        R: RangeBounds<K>,
567    {
568        self.zs_forward_range_iter(cf, range).collect()
569    }
570}
571
572impl DiskWriteBatch {
573    /// Creates and returns a new transactional batch write.
574    ///
575    /// # Correctness
576    ///
577    /// Each block must be written to the state inside a batch, so that:
578    /// - concurrent `ReadStateService` queries don't see half-written blocks, and
579    /// - if Zebra calls `exit`, panics, or crashes, half-written blocks are rolled back.
580    pub fn new() -> Self {
581        DiskWriteBatch {
582            batch: rocksdb::WriteBatch::default(),
583        }
584    }
585}
586
587impl DiskDb {
588    /// Prints rocksdb metrics for each column family along with total database disk size, live data disk size and database memory size.
589    pub fn print_db_metrics(&self) {
590        let mut total_size_on_disk = 0;
591        let mut total_live_size_on_disk = 0;
592        let mut total_size_in_mem = 0;
593        let db: &Arc<DB> = &self.db;
594        let db_options = DiskDb::options();
595        let column_families = DiskDb::construct_column_families(db_options, db.path(), []);
596        let mut column_families_log_string = String::from("");
597
598        write!(column_families_log_string, "Column families and sizes: ").unwrap();
599
600        for cf_descriptor in column_families {
601            let cf_name = &cf_descriptor.name();
602            let cf_handle = db
603                .cf_handle(cf_name)
604                .expect("Column family handle must exist");
605            let live_data_size = db
606                .property_int_value_cf(cf_handle, "rocksdb.estimate-live-data-size")
607                .unwrap_or(Some(0));
608            let total_sst_files_size = db
609                .property_int_value_cf(cf_handle, "rocksdb.total-sst-files-size")
610                .unwrap_or(Some(0));
611            let cf_disk_size = total_sst_files_size.unwrap_or(0);
612            total_size_on_disk += cf_disk_size;
613            total_live_size_on_disk += live_data_size.unwrap_or(0);
614            let mem_table_size = db
615                .property_int_value_cf(cf_handle, "rocksdb.size-all-mem-tables")
616                .unwrap_or(Some(0));
617            total_size_in_mem += mem_table_size.unwrap_or(0);
618
619            write!(
620                column_families_log_string,
621                "{} (Disk: {}, Memory: {})",
622                cf_name,
623                human_bytes::human_bytes(cf_disk_size as f64),
624                human_bytes::human_bytes(mem_table_size.unwrap_or(0) as f64)
625            )
626            .unwrap();
627        }
628
629        debug!("{}", column_families_log_string);
630        info!(
631            "Total Database Disk Size: {}",
632            human_bytes::human_bytes(total_size_on_disk as f64)
633        );
634        info!(
635            "Total Live Data Disk Size: {}",
636            human_bytes::human_bytes(total_live_size_on_disk as f64)
637        );
638        info!(
639            "Total Database Memory Size: {}",
640            human_bytes::human_bytes(total_size_in_mem as f64)
641        );
642    }
643
644    /// Exports RocksDB metrics to Prometheus.
645    ///
646    /// This function collects database statistics and exposes them as Prometheus metrics.
647    /// Call this periodically (e.g., every 30 seconds) from a background task.
648    pub(crate) fn export_metrics(&self) {
649        let db: &Arc<DB> = &self.db;
650        let db_options = DiskDb::options();
651        let column_families = DiskDb::construct_column_families(db_options, db.path(), []);
652
653        let mut total_disk: u64 = 0;
654        let mut total_live: u64 = 0;
655        let mut total_mem: u64 = 0;
656
657        for cf_descriptor in column_families {
658            let cf_name = cf_descriptor.name().to_string();
659            if let Some(cf_handle) = db.cf_handle(&cf_name) {
660                let disk = db
661                    .property_int_value_cf(cf_handle, "rocksdb.total-sst-files-size")
662                    .ok()
663                    .flatten()
664                    .unwrap_or(0);
665                let live = db
666                    .property_int_value_cf(cf_handle, "rocksdb.estimate-live-data-size")
667                    .ok()
668                    .flatten()
669                    .unwrap_or(0);
670                let mem = db
671                    .property_int_value_cf(cf_handle, "rocksdb.size-all-mem-tables")
672                    .ok()
673                    .flatten()
674                    .unwrap_or(0);
675
676                total_disk += disk;
677                total_live += live;
678                total_mem += mem;
679
680                metrics::gauge!("zebra.state.rocksdb.cf_disk_size_bytes", "cf" => cf_name.clone())
681                    .set(disk as f64);
682                metrics::gauge!("zebra.state.rocksdb.cf_memory_size_bytes", "cf" => cf_name)
683                    .set(mem as f64);
684            }
685        }
686
687        metrics::gauge!("zebra.state.rocksdb.total_disk_size_bytes").set(total_disk as f64);
688        metrics::gauge!("zebra.state.rocksdb.live_data_size_bytes").set(total_live as f64);
689        metrics::gauge!("zebra.state.rocksdb.total_memory_size_bytes").set(total_mem as f64);
690
691        // Compaction metrics - these use database-wide properties (not per-column-family)
692        if let Ok(Some(pending)) = db.property_int_value("rocksdb.compaction-pending") {
693            metrics::gauge!("zebra.state.rocksdb.compaction.pending_bytes").set(pending as f64);
694        }
695
696        if let Ok(Some(running)) = db.property_int_value("rocksdb.num-running-compactions") {
697            metrics::gauge!("zebra.state.rocksdb.compaction.running").set(running as f64);
698        }
699
700        if let Ok(Some(cache)) = db.property_int_value("rocksdb.block-cache-usage") {
701            metrics::gauge!("zebra.state.rocksdb.block_cache_usage_bytes").set(cache as f64);
702        }
703
704        // Level-by-level file counts (RocksDB typically has up to 7 levels)
705        for level in 0..7 {
706            let prop = format!("rocksdb.num-files-at-level{level}");
707            if let Ok(Some(count)) = db.property_int_value(&prop) {
708                metrics::gauge!("zebra.state.rocksdb.num_files_at_level", "level" => level.to_string())
709                    .set(count as f64);
710            }
711        }
712    }
713
714    /// Returns the estimated total disk space usage of the database.
715    pub fn size(&self) -> u64 {
716        let db: &Arc<DB> = &self.db;
717        let db_options = DiskDb::options();
718        let mut total_size_on_disk = 0;
719        for cf_descriptor in DiskDb::construct_column_families(db_options, db.path(), []) {
720            let cf_name = &cf_descriptor.name();
721            let cf_handle = db
722                .cf_handle(cf_name)
723                .expect("Column family handle must exist");
724
725            total_size_on_disk += db
726                .property_int_value_cf(cf_handle, "rocksdb.total-sst-files-size")
727                .ok()
728                .flatten()
729                .unwrap_or(0);
730        }
731
732        total_size_on_disk
733    }
734
735    /// Sets `finished_format_upgrades` to true to indicate that Zebra has
736    /// finished applying any required db format upgrades.
737    pub fn mark_finished_format_upgrades(&self) {
738        self.finished_format_upgrades
739            .store(true, atomic::Ordering::SeqCst);
740    }
741
742    /// Returns true if the `finished_format_upgrades` flag has been set to true to
743    /// indicate that Zebra has finished applying any required db format upgrades.
744    pub fn finished_format_upgrades(&self) -> bool {
745        self.finished_format_upgrades.load(atomic::Ordering::SeqCst)
746    }
747
748    /// When called with a secondary DB instance, tries to catch up with the primary DB instance
749    pub fn try_catch_up_with_primary(&self) -> Result<(), rocksdb::Error> {
750        self.db.try_catch_up_with_primary()
751    }
752
753    /// Returns a forward iterator over the items in `cf` in `range`.
754    ///
755    /// Holding this iterator open might delay block commit transactions.
756    pub fn zs_forward_range_iter<C, K, V, R>(
757        &self,
758        cf: &C,
759        range: R,
760    ) -> impl Iterator<Item = (K, V)> + '_
761    where
762        C: rocksdb::AsColumnFamilyRef,
763        K: IntoDisk + FromDisk,
764        V: FromDisk,
765        R: RangeBounds<K>,
766    {
767        self.zs_range_iter_with_direction(cf, range, false)
768    }
769
770    /// Returns a reverse iterator over the items in `cf` in `range`.
771    ///
772    /// Holding this iterator open might delay block commit transactions.
773    pub fn zs_reverse_range_iter<C, K, V, R>(
774        &self,
775        cf: &C,
776        range: R,
777    ) -> impl Iterator<Item = (K, V)> + '_
778    where
779        C: rocksdb::AsColumnFamilyRef,
780        K: IntoDisk + FromDisk,
781        V: FromDisk,
782        R: RangeBounds<K>,
783    {
784        self.zs_range_iter_with_direction(cf, range, true)
785    }
786
787    /// Returns an iterator over the items in `cf` in `range`.
788    ///
789    /// RocksDB iterators are ordered by increasing key bytes by default.
790    /// Otherwise, if `reverse` is `true`, the iterator is ordered by decreasing key bytes.
791    ///
792    /// Holding this iterator open might delay block commit transactions.
793    fn zs_range_iter_with_direction<C, K, V, R>(
794        &self,
795        cf: &C,
796        range: R,
797        reverse: bool,
798    ) -> impl Iterator<Item = (K, V)> + '_
799    where
800        C: rocksdb::AsColumnFamilyRef,
801        K: IntoDisk + FromDisk,
802        V: FromDisk,
803        R: RangeBounds<K>,
804    {
805        use std::ops::Bound::{self, *};
806
807        // Replace with map() when it stabilises:
808        // https://github.com/rust-lang/rust/issues/86026
809        let map_to_vec = |bound: Bound<&K>| -> Bound<Vec<u8>> {
810            match bound {
811                Unbounded => Unbounded,
812                Included(x) => Included(x.as_bytes().as_ref().to_vec()),
813                Excluded(x) => Excluded(x.as_bytes().as_ref().to_vec()),
814            }
815        };
816
817        let start_bound = map_to_vec(range.start_bound());
818        let end_bound = map_to_vec(range.end_bound());
819        let range = (start_bound, end_bound);
820
821        let mode = Self::zs_iter_mode(&range, reverse);
822        let opts = Self::zs_iter_opts(&range);
823
824        // Reading multiple items from iterators has caused database hangs,
825        // in previous RocksDB versions
826        self.db
827            .iterator_cf_opt(cf, opts, mode)
828            .map(|result| result.expect("unexpected database failure"))
829            .map(|(key, value)| (key.to_vec(), value))
830            // Skip excluded "from" bound and empty ranges. The `mode` already skips keys
831            // strictly before the "from" bound.
832            .skip_while({
833                let range = range.clone();
834                move |(key, _value)| !range.contains(key)
835            })
836            // Take until the excluded "to" bound is reached,
837            // or we're after the included "to" bound.
838            .take_while(move |(key, _value)| range.contains(key))
839            .map(|(key, value)| (K::from_bytes(key), V::from_bytes(value)))
840    }
841
842    /// Returns the RocksDB ReadOptions with a lower and upper bound for a range.
843    fn zs_iter_opts<R>(range: &R) -> ReadOptions
844    where
845        R: RangeBounds<Vec<u8>>,
846    {
847        let mut opts = ReadOptions::default();
848        let (lower_bound, upper_bound) = Self::zs_iter_bounds(range);
849
850        if let Some(bound) = lower_bound {
851            opts.set_iterate_lower_bound(bound);
852        };
853
854        if let Some(bound) = upper_bound {
855            opts.set_iterate_upper_bound(bound);
856        };
857
858        opts
859    }
860
861    /// Returns a lower and upper iterate bounds for a range.
862    ///
863    /// Note: Since upper iterate bounds are always exclusive in RocksDB, this method
864    ///       will increment the upper bound by 1 if the end bound of the provided range
865    ///       is inclusive.
866    fn zs_iter_bounds<R>(range: &R) -> (Option<Vec<u8>>, Option<Vec<u8>>)
867    where
868        R: RangeBounds<Vec<u8>>,
869    {
870        use std::ops::Bound::*;
871
872        let lower_bound = match range.start_bound() {
873            Included(bound) | Excluded(bound) => Some(bound.clone()),
874            Unbounded => None,
875        };
876
877        let upper_bound = match range.end_bound().cloned() {
878            Included(mut bound) => {
879                // Increment the last byte in the upper bound that is less than u8::MAX, and
880                // clear any bytes after it to increment the next key in lexicographic order
881                // (next big-endian number). RocksDB uses lexicographic order for keys.
882                let is_wrapped_overflow = increment_big_endian(&mut bound);
883
884                if is_wrapped_overflow {
885                    bound.insert(0, 0x01)
886                }
887
888                Some(bound)
889            }
890            Excluded(bound) => Some(bound),
891            Unbounded => None,
892        };
893
894        (lower_bound, upper_bound)
895    }
896
897    /// Returns the RocksDB iterator "from" mode for `range`.
898    ///
899    /// RocksDB iterators are ordered by increasing key bytes by default.
900    /// Otherwise, if `reverse` is `true`, the iterator is ordered by decreasing key bytes.
901    fn zs_iter_mode<R>(range: &R, reverse: bool) -> rocksdb::IteratorMode<'_>
902    where
903        R: RangeBounds<Vec<u8>>,
904    {
905        use std::ops::Bound::*;
906
907        let from_bound = if reverse {
908            range.end_bound()
909        } else {
910            range.start_bound()
911        };
912
913        match from_bound {
914            Unbounded => {
915                if reverse {
916                    // Reversed unbounded iterators start from the last item
917                    rocksdb::IteratorMode::End
918                } else {
919                    // Unbounded iterators start from the first item
920                    rocksdb::IteratorMode::Start
921                }
922            }
923
924            Included(bound) | Excluded(bound) => {
925                let direction = if reverse {
926                    rocksdb::Direction::Reverse
927                } else {
928                    rocksdb::Direction::Forward
929                };
930
931                rocksdb::IteratorMode::From(bound.as_slice(), direction)
932            }
933        }
934    }
935
936    /// The ideal open file limit for Zebra
937    const IDEAL_OPEN_FILE_LIMIT: u64 = 1024;
938
939    /// The minimum number of open files for Zebra to operate normally. Also used
940    /// as the default open file limit, when the OS doesn't tell us how many
941    /// files we can use.
942    ///
943    /// We want 100+ file descriptors for peers, and 100+ for the database.
944    ///
945    /// On Windows, the default limit is 512 high-level I/O files, and 8192
946    /// low-level I/O files:
947    /// <https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/setmaxstdio?view=msvc-160#remarks>
948    const MIN_OPEN_FILE_LIMIT: u64 = 512;
949
950    /// The number of files used internally by Zebra.
951    ///
952    /// Zebra uses file descriptors for OS libraries (10+), polling APIs (10+),
953    /// stdio (3), and other OS facilities (2+).
954    const RESERVED_FILE_COUNT: u64 = 48;
955
956    /// The size of the database memtable RAM cache in megabytes.
957    ///
958    /// <https://github.com/facebook/rocksdb/wiki/RocksDB-FAQ#configuration-and-tuning>
959    const MEMTABLE_RAM_CACHE_MEGABYTES: usize = 128;
960
961    /// Build a vector of current column families on the disk and optionally any new column families.
962    /// Returns an iterable collection of all column families.
963    fn construct_column_families(
964        db_options: Options,
965        path: &Path,
966        column_families_in_code: impl IntoIterator<Item = String>,
967    ) -> impl Iterator<Item = ColumnFamilyDescriptor> {
968        // When opening the database in read/write mode, all column families must be opened.
969        //
970        // To make Zebra forward-compatible with databases updated by later versions,
971        // we read any existing column families off the disk, then add any new column families
972        // from the current implementation.
973        //
974        // <https://github.com/facebook/rocksdb/wiki/Column-Families#reference>
975        let column_families_on_disk = DB::list_cf(&db_options, path).unwrap_or_default();
976        let column_families_in_code = column_families_in_code.into_iter();
977
978        column_families_on_disk
979            .into_iter()
980            .chain(column_families_in_code)
981            .unique()
982            .map(move |cf_name: String| {
983                let mut cf_options = db_options.clone();
984
985                if cf_name == BALANCE_BY_TRANSPARENT_ADDR {
986                    cf_options.set_merge_operator_associative(
987                        BALANCE_BY_TRANSPARENT_ADDR_MERGE_OP,
988                        fetch_add_balance_and_received,
989                    );
990                }
991
992                rocksdb::ColumnFamilyDescriptor::new(cf_name, cf_options.clone())
993            })
994    }
995
996    /// Opens or creates the database at a path based on the kind, major version and network,
997    /// with the supplied column families, preserving any existing column families,
998    /// and returns a shared low-level database wrapper.
999    ///
1000    /// # Errors
1001    ///
1002    /// - In read-only mode, if the cache directory is missing or unreadable.
1003    ///
1004    /// # Panics
1005    ///
1006    /// - In read-write mode, if the cache directory does not exist and can't be created.
1007    /// - If the database cannot be opened for whatever reason.
1008    pub fn new(
1009        config: &Config,
1010        db_kind: impl AsRef<str>,
1011        format_version_in_code: &Version,
1012        network: &Network,
1013        column_families_in_code: impl IntoIterator<Item = String>,
1014        read_only: bool,
1015    ) -> Result<DiskDb, StateInitError> {
1016        // `ephemeral` and `read_only` describe mutually-exclusive modes: an ephemeral
1017        // database is a read-write primary this process owns and deletes on drop, while
1018        // a read-only secondary follows another process's primary and must never delete
1019        // it. Combining them is a configuration error — it would delete the primary's
1020        // files on drop — so reject it here rather than silently coercing it (a check
1021        // that must hold in release builds, not just debug).
1022        let mode = match (read_only, config.ephemeral) {
1023            (true, true) => return Err(StateInitError::ReadOnlyEphemeralConflict),
1024            (true, false) => DbMode::ReadOnlySecondary,
1025            (false, true) => DbMode::Ephemeral,
1026            (false, false) => DbMode::Persistent,
1027        };
1028
1029        // If the database is ephemeral, we don't need to check the cache directory.
1030        if !config.ephemeral {
1031            if read_only {
1032                // A read-only secondary instance must never create the primary cache
1033                // directory: the primary zebrad owns it. At most, verify it already
1034                // exists and is readable, and fail with a clear error otherwise.
1035                DiskDb::check_cache_dir_readable(&config.cache_dir)?;
1036            } else {
1037                DiskDb::validate_cache_dir(&config.cache_dir);
1038            }
1039        }
1040
1041        let db_kind = db_kind.as_ref();
1042        let path = config.db_path(db_kind, format_version_in_code.major, network);
1043
1044        let db_options = DiskDb::options();
1045
1046        let column_families =
1047            DiskDb::construct_column_families(db_options.clone(), &path, column_families_in_code);
1048
1049        let db_result = if read_only {
1050            // Use a tempfile for the secondary instance cache directory
1051            let secondary_config = Config {
1052                ephemeral: true,
1053                ..config.clone()
1054            };
1055            let secondary_path =
1056                secondary_config.db_path("secondary_state", format_version_in_code.major, network);
1057            let create_dir_result = std::fs::create_dir_all(&secondary_path);
1058
1059            info!(?create_dir_result, "creating secondary db directory");
1060
1061            DB::open_cf_descriptors_as_secondary(
1062                &db_options,
1063                &path,
1064                &secondary_path,
1065                column_families,
1066            )
1067        } else {
1068            DB::open_cf_descriptors(&db_options, &path, column_families)
1069        };
1070
1071        match db_result {
1072            Ok(db) => {
1073                info!("Opened Zebra state cache at {}", path.display());
1074
1075                let db = DiskDb {
1076                    db_kind: db_kind.to_string(),
1077                    format_version_in_code: format_version_in_code.clone(),
1078                    network: network.clone(),
1079                    mode,
1080                    db: Arc::new(db),
1081                    finished_format_upgrades: Arc::new(AtomicBool::new(false)),
1082                };
1083
1084                db.assert_default_cf_is_empty();
1085
1086                Ok(db)
1087            }
1088
1089            Err(e) if matches!(e.kind(), ErrorKind::Busy | ErrorKind::IOError) => panic!(
1090                "Database likely already open {path:?} \
1091                         Hint: Check if another zebrad process is running."
1092            ),
1093
1094            Err(e) => panic!(
1095                "Opening database {path:?} failed. \
1096                        Hint: Try changing the state cache_dir in the Zebra config. \
1097                        Error: {e}",
1098            ),
1099        }
1100    }
1101
1102    // Accessor methods
1103
1104    /// Returns the configured database kind for this database.
1105    pub fn db_kind(&self) -> String {
1106        self.db_kind.clone()
1107    }
1108
1109    /// Returns the format version of the running code that created this `DiskDb` instance in memory.
1110    pub fn format_version_in_code(&self) -> Version {
1111        self.format_version_in_code.clone()
1112    }
1113
1114    /// Returns the fixed major version for this database.
1115    pub fn major_version(&self) -> u64 {
1116        self.format_version_in_code().major
1117    }
1118
1119    /// Returns the configured network for this database.
1120    pub fn network(&self) -> Network {
1121        self.network.clone()
1122    }
1123
1124    /// Returns the `Path` where the files used by this database are located.
1125    pub fn path(&self) -> &Path {
1126        self.db.path()
1127    }
1128
1129    /// Returns the low-level rocksdb inner database.
1130    #[allow(dead_code)]
1131    fn inner(&self) -> &Arc<DB> {
1132        &self.db
1133    }
1134
1135    /// Returns the column family handle for `cf_name`.
1136    pub fn cf_handle(&self, cf_name: &str) -> Option<rocksdb::ColumnFamilyRef<'_>> {
1137        // Note: the lifetime returned by this method is subtly wrong. As of December 2023 it is
1138        // the shorter of &self and &str, but RocksDB clones column family names internally, so it
1139        // should just be &self. To avoid this restriction, clone the string before passing it to
1140        // this method. Currently Zebra uses static strings, so this doesn't matter.
1141        self.db.cf_handle(cf_name)
1142    }
1143
1144    // Read methods are located in the ReadDisk trait
1145
1146    // Write methods
1147    // Low-level write methods are located in the WriteDisk trait
1148
1149    /// Writes `batch` to the database.
1150    pub(crate) fn write(&self, batch: DiskWriteBatch) -> Result<(), rocksdb::Error> {
1151        self.db.write(batch.batch)
1152    }
1153
1154    // Private methods
1155
1156    /// Tries to reuse an existing db after a major upgrade.
1157    ///
1158    /// If the current db version belongs to `restorable_db_versions`, the function moves a previous
1159    /// db to a new path so it can be used again. It does so by merely trying to rename the path
1160    /// corresponding to the db version directly preceding the current version to the path that is
1161    /// used by the current db. If successful, it also deletes the db version file.
1162    ///
1163    /// Returns the old disk version if one existed and the db directory was renamed, or None otherwise.
1164    // TODO: Update this function to rename older major db format version to the current version (#9565).
1165    #[allow(clippy::unwrap_in_result)]
1166    pub(crate) fn try_reusing_previous_db_after_major_upgrade(
1167        restorable_db_versions: &[u64],
1168        format_version_in_code: &Version,
1169        config: &Config,
1170        db_kind: impl AsRef<str>,
1171        network: &Network,
1172    ) -> Option<Version> {
1173        if let Some(&major_db_ver) = restorable_db_versions
1174            .iter()
1175            .find(|v| **v == format_version_in_code.major)
1176        {
1177            let db_kind = db_kind.as_ref();
1178
1179            let old_major_db_ver = major_db_ver - 1;
1180            let old_path = config.db_path(db_kind, old_major_db_ver, network);
1181            // Exit early if the path doesn't exist or there's an error checking it.
1182            if !fs::exists(&old_path).unwrap_or(false) {
1183                return None;
1184            }
1185
1186            let new_path = config.db_path(db_kind, major_db_ver, network);
1187
1188            let old_path = match fs::canonicalize(&old_path) {
1189                Ok(canonicalized_old_path) => canonicalized_old_path,
1190                Err(e) => {
1191                    warn!("could not canonicalize {old_path:?}: {e}");
1192                    return None;
1193                }
1194            };
1195
1196            let cache_path = match fs::canonicalize(&config.cache_dir) {
1197                Ok(canonicalized_cache_path) => canonicalized_cache_path,
1198                Err(e) => {
1199                    warn!("could not canonicalize {:?}: {e}", config.cache_dir);
1200                    return None;
1201                }
1202            };
1203
1204            // # Correctness
1205            //
1206            // Check that the path we're about to move is inside the cache directory.
1207            //
1208            // If the user has symlinked the state directory to a non-cache directory, we don't want
1209            // to move it, because it might contain other files.
1210            //
1211            // We don't attempt to guard against malicious symlinks created by attackers
1212            // (TOCTOU attacks). Zebra should not be run with elevated privileges.
1213            if !old_path.starts_with(&cache_path) {
1214                info!("skipped reusing previous state cache: state is outside cache directory");
1215                return None;
1216            }
1217
1218            let opts = DiskDb::options();
1219            let old_db_exists = DB::list_cf(&opts, &old_path).is_ok_and(|cf| !cf.is_empty());
1220            let new_db_exists = DB::list_cf(&opts, &new_path).is_ok_and(|cf| !cf.is_empty());
1221
1222            if old_db_exists && !new_db_exists {
1223                // Create the parent directory for the new db. This is because we can't directly
1224                // rename e.g. `state/v25/mainnet/` to `state/v26/mainnet/` with `fs::rename()` if
1225                // `state/v26/` does not exist.
1226                match fs::create_dir_all(
1227                    new_path
1228                        .parent()
1229                        .expect("new state cache must have a parent path"),
1230                ) {
1231                    Ok(()) => info!("created new directory for state cache at {new_path:?}"),
1232                    Err(e) => {
1233                        warn!(
1234                            "could not create new directory for state cache at {new_path:?}: {e}"
1235                        );
1236                        return None;
1237                    }
1238                };
1239
1240                match fs::rename(&old_path, &new_path) {
1241                    Ok(()) => {
1242                        info!("moved state cache from {old_path:?} to {new_path:?}");
1243
1244                        let mut disk_version =
1245                            database_format_version_on_disk(config, db_kind, major_db_ver, network)
1246                                .expect("unable to read database format version file")
1247                                .expect("unable to parse database format version");
1248
1249                        disk_version.major = old_major_db_ver;
1250
1251                        write_database_format_version_to_disk(
1252                            config,
1253                            db_kind,
1254                            major_db_ver,
1255                            &disk_version,
1256                            network,
1257                        )
1258                        .expect("unable to write database format version file to disk");
1259
1260                        // Get the parent of the old path, e.g. `state/v25/` and delete it if it is
1261                        // empty.
1262                        let old_path = old_path
1263                            .parent()
1264                            .expect("old state cache must have parent path");
1265
1266                        if fs::read_dir(old_path)
1267                            .expect("cached state dir needs to be readable")
1268                            .next()
1269                            .is_none()
1270                        {
1271                            match fs::remove_dir_all(old_path) {
1272                                Ok(()) => {
1273                                    info!("removed empty old state cache directory at {old_path:?}")
1274                                }
1275                                Err(e) => {
1276                                    warn!(
1277                                        "could not remove empty old state cache directory \
1278                                           at {old_path:?}: {e}"
1279                                    )
1280                                }
1281                            }
1282                        }
1283
1284                        return Some(disk_version);
1285                    }
1286                    Err(e) => {
1287                        warn!("could not move state cache from {old_path:?} to {new_path:?}: {e}");
1288                    }
1289                };
1290            }
1291        };
1292
1293        None
1294    }
1295
1296    /// Returns the database options for the finalized state database.
1297    fn options() -> rocksdb::Options {
1298        let mut opts = rocksdb::Options::default();
1299        let mut block_based_opts = rocksdb::BlockBasedOptions::default();
1300
1301        const ONE_MEGABYTE: usize = 1024 * 1024;
1302
1303        opts.create_if_missing(true);
1304        opts.create_missing_column_families(true);
1305
1306        // Use the recommended Ribbon filter setting for all column families.
1307        //
1308        // Ribbon filters are faster than Bloom filters in Zebra, as of April 2022.
1309        // (They aren't needed for single-valued column families, but they don't hurt either.)
1310        block_based_opts.set_ribbon_filter(9.9);
1311
1312        // Use the recommended LZ4 compression type.
1313        //
1314        // https://github.com/facebook/rocksdb/wiki/Compression#configuration
1315        opts.set_compression_type(rocksdb::DBCompressionType::Lz4);
1316
1317        // Tune level-style database file compaction.
1318        //
1319        // This improves Zebra's initial sync speed slightly, as of April 2022.
1320        opts.optimize_level_style_compaction(Self::MEMTABLE_RAM_CACHE_MEGABYTES * ONE_MEGABYTE);
1321
1322        // Increase the process open file limit if needed,
1323        // then use it to set RocksDB's limit.
1324        let open_file_limit = DiskDb::increase_open_file_limit();
1325        let db_file_limit = DiskDb::get_db_open_file_limit(open_file_limit);
1326
1327        // If the current limit is very large, set the DB limit using the ideal limit
1328        let ideal_limit = DiskDb::get_db_open_file_limit(DiskDb::IDEAL_OPEN_FILE_LIMIT)
1329            .try_into()
1330            .expect("ideal open file limit fits in a c_int");
1331        let db_file_limit = db_file_limit.try_into().unwrap_or(ideal_limit);
1332
1333        opts.set_max_open_files(db_file_limit);
1334
1335        // Set the block-based options
1336        opts.set_block_based_table_factory(&block_based_opts);
1337
1338        opts
1339    }
1340
1341    /// Calculate the database's share of `open_file_limit`
1342    fn get_db_open_file_limit(open_file_limit: u64) -> u64 {
1343        // Give the DB half the files, and reserve half the files for peers
1344        (open_file_limit - DiskDb::RESERVED_FILE_COUNT) / 2
1345    }
1346
1347    /// Increase the open file limit for this process to `IDEAL_OPEN_FILE_LIMIT`.
1348    /// If that fails, try `MIN_OPEN_FILE_LIMIT`.
1349    ///
1350    /// If the current limit is above `IDEAL_OPEN_FILE_LIMIT`, leaves it
1351    /// unchanged.
1352    ///
1353    /// Returns the current limit, after any successful increases.
1354    ///
1355    /// # Panics
1356    ///
1357    /// If the open file limit can not be increased to `MIN_OPEN_FILE_LIMIT`.
1358    fn increase_open_file_limit() -> u64 {
1359        // Zebra mainly uses TCP sockets (`zebra-network`) and low-level files
1360        // (`zebra-state` database).
1361        //
1362        // On Unix-based platforms, `increase_nofile_limit` changes the limit for
1363        // both database files and TCP connections.
1364        //
1365        // But it doesn't do anything on Windows in rlimit 0.7.0.
1366        //
1367        // On Windows, the default limits are:
1368        // - 512 high-level stream I/O files (via the C standard functions),
1369        // - 8192 low-level I/O files (via the Unix C functions), and
1370        // - 1000 TCP Control Block entries (network connections).
1371        //
1372        // https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/setmaxstdio?view=msvc-160#remarks
1373        // http://smallvoid.com/article/winnt-tcpip-max-limit.html
1374        //
1375        // `zebra-state`'s `IDEAL_OPEN_FILE_LIMIT` is much less than
1376        // the Windows low-level I/O file limit.
1377        //
1378        // The [`setmaxstdio` and `getmaxstdio`](https://docs.rs/rlimit/latest/rlimit/#windows)
1379        // functions from the `rlimit` crate only change the high-level I/O file limit.
1380        //
1381        // `zebra-network`'s default connection limit is much less than
1382        // the TCP Control Block limit on Windows.
1383
1384        // We try setting the ideal limit, then the minimum limit.
1385        let current_limit = match increase_nofile_limit(DiskDb::IDEAL_OPEN_FILE_LIMIT) {
1386            Ok(current_limit) => current_limit,
1387            Err(limit_error) => {
1388                // These errors can happen due to sandboxing or unsupported system calls,
1389                // even if the file limit is high enough.
1390                info!(
1391                    ?limit_error,
1392                    min_limit = ?DiskDb::MIN_OPEN_FILE_LIMIT,
1393                    ideal_limit = ?DiskDb::IDEAL_OPEN_FILE_LIMIT,
1394                    "unable to increase the open file limit, \
1395                     assuming Zebra can open a minimum number of files"
1396                );
1397
1398                return DiskDb::MIN_OPEN_FILE_LIMIT;
1399            }
1400        };
1401
1402        if current_limit < DiskDb::MIN_OPEN_FILE_LIMIT {
1403            panic!(
1404                "open file limit too low: \
1405                 unable to set the number of open files to {}, \
1406                 the minimum number of files required by Zebra. \
1407                 Current limit is {:?}. \
1408                 Hint: Increase the open file limit to {} before launching Zebra",
1409                DiskDb::MIN_OPEN_FILE_LIMIT,
1410                current_limit,
1411                DiskDb::IDEAL_OPEN_FILE_LIMIT
1412            );
1413        } else if current_limit < DiskDb::IDEAL_OPEN_FILE_LIMIT {
1414            warn!(
1415                ?current_limit,
1416                min_limit = ?DiskDb::MIN_OPEN_FILE_LIMIT,
1417                ideal_limit = ?DiskDb::IDEAL_OPEN_FILE_LIMIT,
1418                "the maximum number of open files is below Zebra's ideal limit. \
1419                 Hint: Increase the open file limit to {} before launching Zebra",
1420                DiskDb::IDEAL_OPEN_FILE_LIMIT
1421            );
1422        } else if cfg!(windows) {
1423            // This log is verbose during tests.
1424            #[cfg(not(test))]
1425            info!(
1426                min_limit = ?DiskDb::MIN_OPEN_FILE_LIMIT,
1427                ideal_limit = ?DiskDb::IDEAL_OPEN_FILE_LIMIT,
1428                "assuming the open file limit is high enough for Zebra",
1429            );
1430            #[cfg(test)]
1431            debug!(
1432                min_limit = ?DiskDb::MIN_OPEN_FILE_LIMIT,
1433                ideal_limit = ?DiskDb::IDEAL_OPEN_FILE_LIMIT,
1434                "assuming the open file limit is high enough for Zebra",
1435            );
1436        } else {
1437            #[cfg(not(test))]
1438            debug!(
1439                ?current_limit,
1440                min_limit = ?DiskDb::MIN_OPEN_FILE_LIMIT,
1441                ideal_limit = ?DiskDb::IDEAL_OPEN_FILE_LIMIT,
1442                "the open file limit is high enough for Zebra",
1443            );
1444            #[cfg(test)]
1445            debug!(
1446                ?current_limit,
1447                min_limit = ?DiskDb::MIN_OPEN_FILE_LIMIT,
1448                ideal_limit = ?DiskDb::IDEAL_OPEN_FILE_LIMIT,
1449                "the open file limit is high enough for Zebra",
1450            );
1451        }
1452
1453        current_limit
1454    }
1455
1456    // Cleanup methods
1457
1458    /// Returns the number of shared instances of this database.
1459    ///
1460    /// # Concurrency
1461    ///
1462    /// The actual number of owners can be higher or lower than the returned value,
1463    /// because databases can simultaneously be cloned or dropped in other threads.
1464    ///
1465    /// However, if the number of owners is 1, and the caller has exclusive access,
1466    /// the count can't increase unless that caller clones the database.
1467    pub(crate) fn shared_database_owners(&self) -> usize {
1468        Arc::strong_count(&self.db) + Arc::weak_count(&self.db)
1469    }
1470
1471    /// Shut down the database, cleaning up background tasks and ephemeral data.
1472    ///
1473    /// If `force` is true, clean up regardless of any shared references.
1474    /// `force` can cause errors accessing the database from other shared references.
1475    /// It should only be used in debugging or test code, immediately before a manual shutdown.
1476    ///
1477    /// TODO: make private after the stop height check has moved to the syncer (#3442)
1478    ///       move shutting down the database to a blocking thread (#2188)
1479    pub(crate) fn shutdown(&mut self, force: bool) {
1480        // # Correctness
1481        //
1482        // If we're the only owner of the shared database instance,
1483        // then there are no other threads that can increase the strong or weak count.
1484        //
1485        // ## Implementation Requirements
1486        //
1487        // This function and all functions that it calls should avoid cloning the shared database
1488        // instance. If they do, they must drop it before:
1489        // - shutting down database threads, or
1490        // - deleting database files.
1491
1492        if self.shared_database_owners() > 1 {
1493            let path = self.path();
1494
1495            let mut ephemeral_note = "";
1496
1497            if force {
1498                if self.mode.deletes_files_on_drop() {
1499                    ephemeral_note = " and removing ephemeral files";
1500                }
1501
1502                // This log is verbose during tests.
1503                #[cfg(not(test))]
1504                info!(
1505                    ?path,
1506                    "forcing shutdown{} of a state database with multiple active instances",
1507                    ephemeral_note,
1508                );
1509                #[cfg(test)]
1510                debug!(
1511                    ?path,
1512                    "forcing shutdown{} of a state database with multiple active instances",
1513                    ephemeral_note,
1514                );
1515            } else {
1516                if self.mode.deletes_files_on_drop() {
1517                    ephemeral_note = " and files";
1518                }
1519
1520                debug!(
1521                    ?path,
1522                    "dropping DiskDb clone, \
1523                     but keeping shared database instance{} until the last reference is dropped",
1524                    ephemeral_note,
1525                );
1526                return;
1527            }
1528        }
1529
1530        self.assert_default_cf_is_empty();
1531
1532        // Drop isn't guaranteed to run, such as when we panic, or if the tokio shutdown times out.
1533        //
1534        // Zebra's data should be fine if we don't clean up, because:
1535        // - the database flushes regularly anyway
1536        // - Zebra commits each block in a database transaction, any incomplete blocks get rolled back
1537        // - ephemeral files are placed in the os temp dir and should be cleaned up automatically eventually
1538        let path = self.path();
1539        debug!(?path, "flushing database to disk");
1540
1541        // A read-only secondary instance has nothing to flush, and RocksDB rejects
1542        // `flush()`/`flush_wal()` on secondaries with "Not supported operation in
1543        // secondary mode". Only a read-write primary needs flushing on shutdown.
1544        if !self.mode.is_read_only() {
1545            // These flushes can fail during forced shutdown or during Drop after a shutdown,
1546            // particularly in tests. If they fail, there's nothing we can do about it anyway.
1547            if let Err(error) = self.db.flush() {
1548                if matches!(error.kind(), ErrorKind::ShutdownInProgress) {
1549                    debug!(
1550                        ?error,
1551                        ?path,
1552                        "expected shutdown error flushing database SST files to disk"
1553                    );
1554                } else {
1555                    info!(
1556                        ?error,
1557                        ?path,
1558                        "unexpected error flushing database SST files to disk during shutdown"
1559                    );
1560                }
1561            }
1562
1563            if let Err(error) = self.db.flush_wal(true) {
1564                if matches!(error.kind(), ErrorKind::ShutdownInProgress) {
1565                    debug!(
1566                        ?error,
1567                        ?path,
1568                        "expected shutdown error flushing database WAL buffer to disk"
1569                    );
1570                } else {
1571                    info!(
1572                        ?error,
1573                        ?path,
1574                        "unexpected error flushing database WAL buffer to disk during shutdown"
1575                    );
1576                }
1577            }
1578        }
1579
1580        // # Memory Safety
1581        //
1582        // We'd like to call `cancel_all_background_work()` before Zebra exits,
1583        // but when we call it, we get memory, thread, or C++ errors when the process exits.
1584        // (This seems to be a bug in RocksDB: cancel_all_background_work() should wait until
1585        // all the threads have cleaned up.)
1586        //
1587        // # Change History
1588        //
1589        // We've changed this setting multiple times since 2021, in response to new RocksDB
1590        // and Rust compiler behaviour.
1591        //
1592        // We enabled cancel_all_background_work() due to failures on:
1593        // - Rust 1.57 on Linux
1594        //
1595        // We disabled cancel_all_background_work() due to failures on:
1596        // - Rust 1.64 on Linux
1597        //
1598        // We tried enabling cancel_all_background_work() due to failures on:
1599        // - Rust 1.70 on macOS 12.6.5 on x86_64
1600        // but it didn't stop the aborts happening (PR #6820).
1601        //
1602        // There weren't any failures with cancel_all_background_work() disabled on:
1603        // - Rust 1.69 or earlier
1604        // - Linux with Rust 1.70
1605        // And with cancel_all_background_work() enabled or disabled on:
1606        // - macOS 13.2 on aarch64 (M1), native and emulated x86_64, with Rust 1.70
1607        //
1608        // # Detailed Description
1609        //
1610        // We see these kinds of errors:
1611        // ```
1612        // pthread lock: Invalid argument
1613        // pure virtual method called
1614        // terminate called without an active exception
1615        // pthread destroy mutex: Device or resource busy
1616        // Aborted (core dumped)
1617        // signal: 6, SIGABRT: process abort signal
1618        // signal: 11, SIGSEGV: invalid memory reference
1619        // ```
1620        //
1621        // # Reference
1622        //
1623        // The RocksDB wiki says:
1624        // > Q: Is it safe to close RocksDB while another thread is issuing read, write or manual compaction requests?
1625        // >
1626        // > A: No. The users of RocksDB need to make sure all functions have finished before they close RocksDB.
1627        // > You can speed up the waiting by calling CancelAllBackgroundWork().
1628        //
1629        // <https://github.com/facebook/rocksdb/wiki/RocksDB-FAQ>
1630        //
1631        // > rocksdb::DB instances need to be destroyed before your main function exits.
1632        // > RocksDB instances usually depend on some internal static variables.
1633        // > Users need to make sure rocksdb::DB instances are destroyed before those static variables.
1634        //
1635        // <https://github.com/facebook/rocksdb/wiki/Known-Issues>
1636        //
1637        // # TODO
1638        //
1639        // Try re-enabling this code and fixing the underlying concurrency bug.
1640        //
1641        //info!(?path, "stopping background database tasks");
1642        //self.db.cancel_all_background_work(true);
1643
1644        // We'd like to drop the database before deleting its files,
1645        // because that closes the column families and the database correctly.
1646        // But Rust's ownership rules make that difficult,
1647        // so we just flush and delete ephemeral data instead.
1648        //
1649        // This implementation doesn't seem to cause any issues,
1650        // and the RocksDB Drop implementation handles any cleanup.
1651        self.delete_ephemeral();
1652    }
1653
1654    /// If the database is `ephemeral`, delete its files.
1655    fn delete_ephemeral(&mut self) {
1656        // # Correctness
1657        //
1658        // This function and all functions that it calls should avoid cloning the shared database
1659        // instance. See `shutdown()` for details.
1660
1661        if !self.mode.deletes_files_on_drop() {
1662            return;
1663        }
1664
1665        let path = self.path();
1666
1667        // This log is verbose during tests.
1668        #[cfg(not(test))]
1669        info!(?path, "removing temporary database files");
1670        #[cfg(test)]
1671        debug!(?path, "removing temporary database files");
1672
1673        // We'd like to use `rocksdb::Env::mem_env` for ephemeral databases,
1674        // but the Zcash blockchain might not fit in memory. So we just
1675        // delete the database files instead.
1676        //
1677        // We'd also like to call `DB::destroy` here, but calling destroy on a
1678        // live DB is undefined behaviour:
1679        // https://github.com/facebook/rocksdb/wiki/RocksDB-FAQ#basic-readwrite
1680        //
1681        // So we assume that all the database files are under `path`, and
1682        // delete them using standard filesystem APIs. Deleting open files
1683        // might cause errors on non-Unix platforms, so we ignore the result.
1684        // (The OS will delete them eventually anyway, if they are in a temporary directory.)
1685        let result = std::fs::remove_dir_all(path);
1686
1687        if result.is_err() {
1688            // This log is verbose during tests.
1689            #[cfg(not(test))]
1690            info!(
1691                ?result,
1692                ?path,
1693                "removing temporary database files caused an error",
1694            );
1695            #[cfg(test)]
1696            debug!(
1697                ?result,
1698                ?path,
1699                "removing temporary database files caused an error",
1700            );
1701        } else {
1702            debug!(
1703                ?result,
1704                ?path,
1705                "successfully removed temporary database files",
1706            );
1707        }
1708    }
1709
1710    /// Check that the "default" column family is empty.
1711    ///
1712    /// # Panics
1713    ///
1714    /// If Zebra has a bug where it is storing data in the wrong column family.
1715    fn assert_default_cf_is_empty(&self) {
1716        // # Correctness
1717        //
1718        // This function and all functions that it calls should avoid cloning the shared database
1719        // instance. See `shutdown()` for details.
1720
1721        if let Some(default_cf) = self.cf_handle("default") {
1722            assert!(
1723                self.zs_is_empty(&default_cf),
1724                "Zebra should not store data in the 'default' column family"
1725            );
1726        }
1727    }
1728
1729    // Checks that a cache directory already exists and is readable, without creating it.
1730    //
1731    // Used when opening a read-only secondary instance, which must never create the
1732    // primary's cache directory. Returns a [`StateInitError`] if the directory is missing
1733    // or unreadable.
1734    pub(crate) fn check_cache_dir_readable(
1735        cache_dir: &std::path::Path,
1736    ) -> Result<(), StateInitError> {
1737        match fs::read_dir(cache_dir) {
1738            Ok(_) => Ok(()),
1739            Err(source) => Err(StateInitError::ReadOnlyCacheDirUnreadable {
1740                path: cache_dir.to_path_buf(),
1741                source,
1742            }),
1743        }
1744    }
1745
1746    // Validates a cache directory and creates it if it doesn't exist.
1747    // If the directory cannot be created, it panics with a specific error message.
1748    fn validate_cache_dir(cache_dir: &std::path::PathBuf) {
1749        if let Err(e) = fs::create_dir_all(cache_dir) {
1750            match e.kind() {
1751                std::io::ErrorKind::PermissionDenied => panic!(
1752                    "Permission denied creating {cache_dir:?}. \
1753                     Hint: check if cache directory exist and has write permissions."
1754                ),
1755                std::io::ErrorKind::StorageFull => panic!(
1756                    "No space left on device creating {cache_dir:?}. \
1757                     Hint: check if the disk is full."
1758                ),
1759                _ => panic!("Could not create cache dir {cache_dir:?}: {e}"),
1760            }
1761        }
1762    }
1763}
1764
1765impl Drop for DiskDb {
1766    fn drop(&mut self) {
1767        let path = self.path();
1768        debug!(?path, "dropping DiskDb instance");
1769
1770        self.shutdown(false);
1771    }
1772}