use std::{path::Path, sync::Arc};
use crossbeam_channel::bounded;
use semver::Version;
use zakura_chain::{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,
upgrade::{DbFormatChange, DbFormatChangeThreadHandle},
},
},
write_database_format_version_to_disk, BoxError, Config, StateInitError,
};
use super::disk_format::upgrade::restorable_db_versions;
pub mod block;
pub mod chain;
#[allow(dead_code)]
pub(crate) mod commitment_roots_db;
#[allow(dead_code)]
pub mod highest_completed_checkpoint;
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,
}
impl ZakuraDb {
pub(in crate::service) fn header_chain_disk_db(&self) -> DiskDb {
self.db.clone()
}
#[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> {
if read_only && config.ephemeral {
return Err(StateInitError::ReadOnlyEphemeralConflict);
}
let version_path =
config.version_file_path(&db_kind, format_version_in_code.major, network);
let read_disk_version = || {
database_format_version_on_disk(config, &db_kind, format_version_in_code.major, network)
.map_err(|source| StateInitError::DatabaseFormatVersion {
path: version_path.clone(),
source,
})
};
let disk_version = if read_only {
DiskDb::check_cache_dir_readable(&config.cache_dir)?;
read_disk_version()?
} else {
match DiskDb::try_reusing_previous_db_after_major_upgrade(
&restorable_db_versions(),
format_version_in_code,
config,
&db_kind,
network,
) {
Some(version) => Some(version),
None => read_disk_version()?,
}
};
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 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,
};
if is_unrepairable_vct_database(&db, disk_version_before_open.as_ref()) {
return Err(StateInitError::VctSproutHistoryUnrepairable);
}
db.run_startup_format_change(format_change)?;
Ok(db)
}
pub(crate) fn run_startup_format_change(
&mut self,
format_change: DbFormatChange,
) -> Result<(), StateInitError> {
if self.debug_skip_format_upgrades {
return Ok(());
}
let initial_finalized_tip_height = self.finalized_tip_height();
let (_never_cancel_handle, never_cancel_receiver) = bounded(1);
format_change
.run_format_change_or_check(self, initial_finalized_tip_height, &never_cancel_receiver)
.map_err(|source| StateInitError::DatabaseFormatUpgrade {
path: self.path().to_owned(),
source: Box::new(source),
})?;
let format_change_handle = DbFormatChange::spawn_periodic_format_checks(
self.clone(),
initial_finalized_tip_height,
);
self.format_change_handle = Some(format_change_handle);
Ok(())
}
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()
}
#[cfg(test)]
pub(crate) fn secondary_path(&self) -> Option<&Path> {
self.db.secondary_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,
)
.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()
}
}
const VCT_SPROUT_HISTORY_VERSION: Version = Version::new(28, 0, 1);
fn is_unrepairable_vct_database(db: &ZakuraDb, disk_version: Option<&Version>) -> bool {
db.network() == Network::Mainnet
&& db.is_vct_synced()
&& disk_version.is_some_and(|version| version < &VCT_SPROUT_HISTORY_VERSION)
}
impl Drop for ZakuraDb {
fn drop(&mut self) {
self.shutdown(false);
}
}
#[cfg(test)]
mod tests {
use tempfile::TempDir;
use zakura_chain::block::Height;
use crate::{
constants::{state_database_format_version_in_code, STATE_DATABASE_KIND},
service::finalized_state::{DiskWriteBatch, STATE_COLUMN_FAMILIES_IN_CODE},
};
use super::*;
fn persistent_config() -> (TempDir, Config) {
let cache = tempfile::tempdir().expect("temporary cache directory is created");
let config = Config {
cache_dir: cache.path().to_path_buf(),
ephemeral: false,
..Config::default()
};
(cache, config)
}
fn open(
config: &Config,
network: &Network,
read_only: bool,
) -> Result<ZakuraDb, StateInitError> {
ZakuraDb::new(
config,
STATE_DATABASE_KIND,
&state_database_format_version_in_code(),
network,
false,
STATE_COLUMN_FAMILIES_IN_CODE
.iter()
.map(ToString::to_string),
read_only,
)
}
fn seed_db(config: &Config, network: &Network, disk_version: Version, vct_synced: bool) {
let db = ZakuraDb::new(
config,
STATE_DATABASE_KIND,
&state_database_format_version_in_code(),
network,
true,
STATE_COLUMN_FAMILIES_IN_CODE
.iter()
.map(ToString::to_string),
false,
)
.expect("fixture database opens");
if vct_synced {
let mut batch = DiskWriteBatch::new();
batch.update_vct_sync_marker(&db, Height(1));
db.write_batch(batch).expect("VCT marker write succeeds");
}
db.update_format_version_on_disk(&disk_version)
.expect("fixture version write succeeds");
}
#[test]
fn unrepairable_vct_database_is_rejected_in_every_open_mode() {
let network = Network::Mainnet;
let old = Version::new(28, 0, 0);
for read_only in [false, true] {
let (_cache, config) = persistent_config();
seed_db(&config, &network, old.clone(), true);
assert!(
matches!(
open(&config, &network, read_only),
Err(StateInitError::VctSproutHistoryUnrepairable)
),
"read_only = {read_only} startup must refuse an unrepairable VCT database"
);
}
}
#[test]
fn databases_without_missing_sprout_history_open_normally() {
let old = Version::new(28, 0, 0);
let repaired = Version::new(28, 0, 1);
let (_cache, config) = persistent_config();
seed_db(&config, &Network::Mainnet, old.clone(), false);
assert!(open(&config, &Network::Mainnet, false).is_ok());
let (_cache, config) = persistent_config();
seed_db(&config, &Network::Mainnet, repaired, true);
assert!(open(&config, &Network::Mainnet, false).is_ok());
let regtest = Network::new_regtest(Default::default());
let (_cache, config) = persistent_config();
seed_db(&config, ®test, old, true);
assert!(open(&config, ®test, false).is_ok());
}
}