use std::{path::Path, sync::Arc};
use crossbeam_channel::bounded;
use semver::Version;
use zakura_chain::{block::Height, diagnostic::task::WaitForPanics, parameters::Network};
use crate::{
config::database_format_version_on_disk,
service::finalized_state::{
disk_db::DiskDb,
disk_format::{
block::MAX_ON_DISK_HEIGHT,
transparent::AddressLocation,
upgrade::{DbFormatChange, DbFormatChangeThreadHandle},
},
},
write_database_format_version_to_disk, BoxError, Config, StateInitError,
};
use super::disk_format::upgrade::repair_vct_sprout_history;
use super::disk_format::upgrade::restorable_db_versions;
pub mod block;
pub mod chain;
pub mod metrics;
pub(crate) const PARALLEL_BLOCK_TX_THRESHOLD: usize = 16;
pub(crate) const PARALLEL_BLOCK_READ_THRESHOLD: usize = 16;
pub mod prune;
pub mod rollback;
pub mod shielded;
pub mod transparent;
#[cfg(any(test, feature = "proptest-impl"))]
pub mod arbitrary;
#[derive(Clone, Debug)]
pub struct ZakuraDb {
config: Arc<Config>,
debug_skip_format_upgrades: bool,
format_change_handle: Option<DbFormatChangeThreadHandle>,
db: DiskDb,
}
#[derive(Clone, Copy)]
enum DbOpenMode {
Writable,
ReadOnly,
VctSproutValidation,
}
impl DbOpenMode {
fn is_read_only(self) -> bool {
!matches!(self, Self::Writable)
}
fn enforces_vct_repair_guard(self) -> bool {
!matches!(self, Self::VctSproutValidation)
}
}
impl ZakuraDb {
#[allow(clippy::unwrap_in_result)]
pub fn new(
config: &Config,
db_kind: impl AsRef<str>,
format_version_in_code: &Version,
network: &Network,
debug_skip_format_upgrades: bool,
column_families_in_code: impl IntoIterator<Item = String>,
read_only: bool,
) -> Result<ZakuraDb, StateInitError> {
let open_mode = if read_only {
DbOpenMode::ReadOnly
} else {
DbOpenMode::Writable
};
Self::new_with_vct_repair_guard(
config,
db_kind,
format_version_in_code,
network,
debug_skip_format_upgrades,
column_families_in_code,
open_mode,
)
}
pub(crate) fn new_for_vct_sprout_history_validation(
config: &Config,
db_kind: impl AsRef<str>,
format_version_in_code: &Version,
network: &Network,
column_families_in_code: impl IntoIterator<Item = String>,
) -> Result<ZakuraDb, StateInitError> {
Self::new_with_vct_repair_guard(
config,
db_kind,
format_version_in_code,
network,
false,
column_families_in_code,
DbOpenMode::VctSproutValidation,
)
}
#[allow(clippy::unwrap_in_result)]
fn new_with_vct_repair_guard(
config: &Config,
db_kind: impl AsRef<str>,
format_version_in_code: &Version,
network: &Network,
debug_skip_format_upgrades: bool,
column_families_in_code: impl IntoIterator<Item = String>,
open_mode: DbOpenMode,
) -> Result<ZakuraDb, StateInitError> {
let read_only = open_mode.is_read_only();
if read_only && config.ephemeral {
return Err(StateInitError::ReadOnlyEphemeralConflict);
}
let disk_version = if read_only {
DiskDb::check_cache_dir_readable(&config.cache_dir)?;
database_format_version_on_disk(config, &db_kind, format_version_in_code.major, network)
.expect("unable to read database format version file")
} else {
DiskDb::try_reusing_previous_db_after_major_upgrade(
&restorable_db_versions(),
format_version_in_code,
config,
&db_kind,
network,
)
.or_else(|| {
database_format_version_on_disk(
config,
&db_kind,
format_version_in_code.major,
network,
)
.expect("unable to read database format version file")
})
};
let disk_version_before_open = disk_version.clone();
let format_change = DbFormatChange::open_database(format_version_in_code, disk_version);
if read_only && format_change.is_newly_created() {
let db_path = config.db_path(&db_kind, format_version_in_code.major, network);
return Err(StateInitError::ReadOnlyDatabaseNotFound { path: db_path });
}
let upgrades_explicitly_disabled = debug_skip_format_upgrades;
let debug_skip_format_upgrades = read_only || debug_skip_format_upgrades;
let disk_db = DiskDb::new(
config,
db_kind,
format_version_in_code,
network,
column_families_in_code,
read_only,
)?;
let mut db = ZakuraDb {
config: Arc::new(config.clone()),
debug_skip_format_upgrades,
format_change_handle: None,
db: disk_db,
};
let prepared_vct_repair = if open_mode.enforces_vct_repair_guard()
&& repair_vct_sprout_history::is_repair_eligible(&db, disk_version_before_open.as_ref())
{
if read_only || upgrades_explicitly_disabled {
let reason = if read_only {
"read-only databases cannot be repaired"
} else {
"database format upgrades are disabled"
};
return Err(StateInitError::VctSproutHistoryRepairRequired {
mode: if read_only { "read-only" } else { "writable" },
reason,
});
}
Some(
repair_vct_sprout_history::prepare_startup_repair(&db).map_err(|error| {
StateInitError::VctSproutHistoryRepairInvalid {
reason: error.to_string(),
}
})?,
)
} else {
None
};
let zero_location_utxos =
db.address_utxo_locations(AddressLocation::from_usize(Height(0), 0, 0));
if !zero_location_utxos.is_empty() {
warn!(
"You have been impacted by the Zebra 2.4.0 address indexer corruption bug. \
If you rely on the data from the RPC interface, you will need to recover your database. \
Follow the instructions in the 2.4.1 release notes: https://github.com/ZcashFoundation/zebra/releases/tag/v2.4.1 \
If you just run the node for consensus and don't use data from the RPC interface, you can ignore this warning."
)
}
if !read_only && config.repair_zakura_header_store_on_startup {
db.audit_and_repair_zakura_header_store()
.expect("startup header-store repair write failed: RocksDB is unavailable");
}
db.run_startup_format_change(format_change, prepared_vct_repair);
Ok(db)
}
pub(crate) fn run_startup_format_change(
&mut self,
format_change: DbFormatChange,
prepared_vct_repair: Option<Arc<repair_vct_sprout_history::RepairInput>>,
) {
if self.debug_skip_format_upgrades {
return;
}
let initial_tip_height = self.finalized_tip_height();
let (_never_cancel_handle, never_cancel_receiver) = bounded(1);
format_change
.run_format_change_or_check(
self,
initial_tip_height,
&never_cancel_receiver,
prepared_vct_repair,
)
.expect("startup format change cannot be cancelled");
let format_change_handle =
DbFormatChange::spawn_periodic_format_checks(self.clone(), initial_tip_height);
self.format_change_handle = Some(format_change_handle);
}
pub fn mark_finished_format_upgrades(&self) {
self.db.mark_finished_format_upgrades();
}
pub fn finished_format_upgrades(&self) -> bool {
self.db.finished_format_upgrades()
}
pub fn config(&self) -> &Config {
&self.config
}
pub fn db_kind(&self) -> String {
self.db.db_kind()
}
pub fn format_version_in_code(&self) -> Version {
self.db.format_version_in_code()
}
pub fn major_version(&self) -> u64 {
self.db.major_version()
}
pub fn format_version_on_disk(&self) -> Result<Option<Version>, BoxError> {
database_format_version_on_disk(
self.config(),
self.db_kind(),
self.major_version(),
&self.network(),
)
}
pub(crate) fn update_format_version_on_disk(
&self,
new_version: &Version,
) -> Result<(), BoxError> {
write_database_format_version_to_disk(
self.config(),
self.db_kind(),
self.major_version(),
new_version,
&self.network(),
)
}
pub fn network(&self) -> Network {
self.db.network()
}
pub fn path(&self) -> &Path {
self.db.path()
}
pub fn check_for_panics(&mut self) {
if let Some(format_change_handle) = self.format_change_handle.as_mut() {
format_change_handle.check_for_panics();
}
}
pub fn try_catch_up_with_primary(&self) -> Result<(), rocksdb::Error> {
self.db.try_catch_up_with_primary()
}
pub async fn spawn_try_catch_up_with_primary(&self) -> Result<(), rocksdb::Error> {
let db = self.clone();
tokio::task::spawn_blocking(move || {
let result = db.try_catch_up_with_primary();
if let Err(catch_up_error) = &result {
tracing::warn!(?catch_up_error, "failed to catch up to primary");
}
result
})
.wait_for_panics()
.await
}
pub fn shutdown(&mut self, force: bool) {
let is_shutdown = force || self.db.shared_database_owners() <= 1;
if !self.debug_skip_format_upgrades && is_shutdown {
if let Some(format_change_handle) = self.format_change_handle.as_mut() {
format_change_handle.force_cancel();
}
let disk_version = database_format_version_on_disk(
&self.config,
self.db_kind(),
self.major_version(),
&self.network(),
)
.expect("unexpected invalid or unreadable database version file");
if let Some(disk_version) = disk_version {
let (_never_cancel_handle, never_cancel_receiver) = bounded(1);
if disk_version >= self.db.format_version_in_code() {
DbFormatChange::check_new_blocks(self)
.run_format_change_or_check(
self,
None,
&never_cancel_receiver,
None,
)
.expect("cancel handle is never used");
}
}
}
self.check_for_panics();
self.db.shutdown(force);
}
pub(crate) fn check_max_on_disk_tip_height(&self) -> Result<(), String> {
if let Some((tip_height, tip_hash)) = self.tip() {
if tip_height.0 > MAX_ON_DISK_HEIGHT.0 / 2 {
let err = Err(format!(
"unexpectedly large tip height, database format upgrade required: \
tip height: {tip_height:?}, tip hash: {tip_hash:?}, \
max height: {MAX_ON_DISK_HEIGHT:?}"
));
error!(?err);
return err;
}
}
Ok(())
}
pub fn print_db_metrics(&self) {
self.db.print_db_metrics();
}
pub(crate) fn export_metrics(&self) {
self.db.export_metrics();
}
pub fn size(&self) -> u64 {
self.db.size()
}
}
impl Drop for ZakuraDb {
fn drop(&mut self) {
self.shutdown(false);
}
}