#![allow(dead_code)]
pub(crate) mod capability;
pub(crate) mod db;
pub(crate) mod entry;
pub(crate) mod migrations;
pub(crate) mod reader;
pub(crate) mod router;
use capability::*;
use db::{DbBackend, VERSION_DIRS};
use migrations::MigrationManager;
use reader::*;
use router::Router;
use tracing::{info, instrument};
use zebra_chain::parameters::NetworkKind;
use crate::{
chain_index::{
finalised_state::db::v1::DB_VERSION_V1, source::BlockchainSourceError,
types::GENESIS_HEIGHT,
},
config::BlockCacheConfig,
error::FinalisedStateError,
BlockHash, BlockMetadata, BlockWithMetadata, ChainWork, Height, IndexedBlock, StatusType,
};
use std::{
sync::{
atomic::{AtomicU64, Ordering},
Arc,
},
time::Duration,
};
use tokio::{
sync::watch,
time::{interval, MissedTickBehavior},
};
use super::source::BlockchainSource;
#[derive(Debug)]
pub(crate) struct ZainoDB {
db: Arc<Router>,
cfg: BlockCacheConfig,
}
impl ZainoDB {
#[instrument(name = "ZainoDB::spawn", skip(cfg, source), fields(db_version = cfg.db_version))]
pub(crate) async fn spawn<T>(
cfg: BlockCacheConfig,
source: T,
) -> Result<Self, FinalisedStateError>
where
T: BlockchainSource,
{
let version_opt = Self::try_find_current_db_version(&cfg).await;
let target_version = match cfg.db_version {
0 => DbVersion {
major: 0,
minor: 0,
patch: 0,
},
1 => DB_VERSION_V1,
x => {
return Err(FinalisedStateError::Custom(format!(
"unsupported database version: DbV{x}"
)));
}
};
let backend = match version_opt {
Some(version) => {
info!(version, "Opening ZainoDB from file");
match version {
0 => DbBackend::spawn_v0(&cfg).await?,
1 => DbBackend::spawn_v1(&cfg).await?,
_ => {
return Err(FinalisedStateError::Custom(format!(
"unsupported database version: DbV{version}"
)));
}
}
}
None => {
info!(version = %target_version, "Creating new ZainoDB");
match target_version.major() {
0 => DbBackend::spawn_v0(&cfg).await?,
1 => DbBackend::spawn_v1(&cfg).await?,
_ => {
return Err(FinalisedStateError::Custom(format!(
"unsupported database version: DbV{target_version}"
)));
}
}
}
};
let current_version = backend.get_metadata().await?.version();
let router = Arc::new(Router::new(Arc::new(backend)));
if version_opt.is_some() && current_version < target_version {
info!(
from_version = %current_version,
to_version = %target_version,
"Starting ZainoDB migration"
);
let mut migration_manager = MigrationManager {
router: Arc::clone(&router),
cfg: cfg.clone(),
current_version,
target_version,
source,
};
migration_manager.migrate().await?;
}
Ok(Self { db: router, cfg })
}
pub(crate) async fn shutdown(&self) -> Result<(), FinalisedStateError> {
self.db.shutdown().await
}
pub(crate) fn status(&self) -> StatusType {
self.db.status()
}
pub(crate) async fn wait_until_ready(&self) {
let mut ticker = interval(Duration::from_millis(100));
ticker.set_missed_tick_behavior(MissedTickBehavior::Delay);
loop {
ticker.tick().await;
if self.db.status() == StatusType::Ready {
break;
}
}
}
pub(crate) fn to_reader(self: &Arc<Self>) -> DbReader {
DbReader {
inner: Arc::clone(self),
}
}
async fn try_find_current_db_version(cfg: &BlockCacheConfig) -> Option<u32> {
let legacy_dir = match cfg.network.to_zebra_network().kind() {
NetworkKind::Mainnet => "live",
NetworkKind::Testnet => "test",
NetworkKind::Regtest => "local",
};
let legacy_path = cfg.storage.database.path.join(legacy_dir);
if legacy_path.join("data.mdb").exists() && legacy_path.join("lock.mdb").exists() {
return Some(0);
}
let net_dir = match cfg.network.to_zebra_network().kind() {
NetworkKind::Mainnet => "mainnet",
NetworkKind::Testnet => "testnet",
NetworkKind::Regtest => "regtest",
};
let net_path = cfg.storage.database.path.join(net_dir);
if net_path.exists() && net_path.is_dir() {
for (i, version_dir) in VERSION_DIRS.iter().enumerate() {
let db_path = net_path.join(version_dir);
let data_file = db_path.join("data.mdb");
let lock_file = db_path.join("lock.mdb");
if data_file.exists() && lock_file.exists() {
let version = (i + 1) as u32;
return Some(version);
}
}
}
None
}
#[inline]
pub(crate) fn backend_for_cap(
&self,
cap: CapabilityRequest,
) -> Result<Arc<DbBackend>, FinalisedStateError> {
self.db.backend(cap)
}
pub(crate) async fn sync_to_height<T>(
&self,
height: Height,
source: &T,
) -> Result<(), FinalisedStateError>
where
T: BlockchainSource,
{
let network = self.cfg.network;
let db_height_opt = self.db_height().await?;
let mut db_height = db_height_opt.unwrap_or(GENESIS_HEIGHT);
let zebra_network = network.to_zebra_network();
let sapling_activation_height = zebra_chain::parameters::NetworkUpgrade::Sapling
.activation_height(&zebra_network)
.expect("Sapling activation height must be set");
let nu5_activation_height =
zebra_chain::parameters::NetworkUpgrade::Nu5.activation_height(&zebra_network);
let mut parent_chainwork = if db_height_opt.is_none() {
ChainWork::from_u256(0.into())
} else {
db_height.0 += 1;
match self
.db
.backend(CapabilityRequest::BlockCoreExt)?
.get_block_header(height)
.await
{
Ok(header) => header.context.chainwork,
Err(_) => ChainWork::from_u256(0.into()),
}
};
let current_height = Arc::new(AtomicU64::new(db_height.0 as u64));
let target_height = height.0 as u64;
let (shutdown_tx, shutdown_rx) = watch::channel(());
let reporter_current = Arc::clone(¤t_height);
let reporter_network = network;
let mut reporter_shutdown = shutdown_rx.clone();
let reporter_handle = tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(10));
loop {
tokio::select! {
_ = interval.tick() => {
let cur = reporter_current.load(Ordering::Relaxed);
tracing::info!(
"sync_to_height: syncing height {current} / {target} on network = {:?}",
reporter_network,
current = cur,
target = target_height
);
}
_ = reporter_shutdown.changed() => {
break;
}
}
}
});
let result: Result<(), FinalisedStateError> = (async {
for height_int in (db_height.0)..=height.0 {
current_height.store(height_int as u64, Ordering::Relaxed);
let block = match source
.get_block(zebra_state::HashOrHeight::Height(
zebra_chain::block::Height(height_int),
))
.await?
{
Some(block) => block,
None => {
return Err(FinalisedStateError::BlockchainSourceError(
BlockchainSourceError::Unrecoverable(format!(
"error fetching block at height {} from validator",
height.0
)),
));
}
};
let block_hash = BlockHash::from(block.hash().0);
let (sapling_opt, orchard_opt) =
source.get_commitment_tree_roots(block_hash).await?;
let is_sapling_active = height_int >= sapling_activation_height.0;
let is_orchard_active = nu5_activation_height
.is_some_and(|nu5_activation_height| height_int >= nu5_activation_height.0);
let (sapling_root, sapling_size) = if is_sapling_active {
sapling_opt.ok_or_else(|| {
FinalisedStateError::BlockchainSourceError(
BlockchainSourceError::Unrecoverable(format!(
"missing Sapling commitment tree root for block {block_hash}"
)),
)
})?
} else {
(zebra_chain::sapling::tree::Root::default(), 0)
};
let (orchard_root, orchard_size) = if is_orchard_active {
orchard_opt.ok_or_else(|| {
FinalisedStateError::BlockchainSourceError(
BlockchainSourceError::Unrecoverable(format!(
"missing Orchard commitment tree root for block {block_hash}"
)),
)
})?
} else {
(zebra_chain::orchard::tree::Root::default(), 0)
};
let metadata = BlockMetadata::new(
sapling_root,
sapling_size as u32,
orchard_root,
orchard_size as u32,
parent_chainwork,
network.to_zebra_network(),
);
let block_with_metadata = BlockWithMetadata::new(block.as_ref(), metadata);
let chain_block = match IndexedBlock::try_from(block_with_metadata) {
Ok(block) => block,
Err(_) => {
return Err(FinalisedStateError::BlockchainSourceError(
BlockchainSourceError::Unrecoverable(format!(
"error building block data at height {}",
height.0
)),
));
}
};
parent_chainwork = chain_block.context.chainwork;
self.write_block(chain_block).await?;
}
Ok(())
})
.await;
let _ = shutdown_tx.send(());
let _ = reporter_handle.await;
result
}
pub(crate) async fn write_block(&self, b: IndexedBlock) -> Result<(), FinalisedStateError> {
self.db.write_block(b).await
}
pub(crate) async fn delete_block_at_height(
&self,
h: Height,
) -> Result<(), FinalisedStateError> {
self.db.delete_block_at_height(h).await
}
pub(crate) async fn delete_block(&self, b: &IndexedBlock) -> Result<(), FinalisedStateError> {
self.db.delete_block(b).await
}
pub(crate) async fn db_height(&self) -> Result<Option<Height>, FinalisedStateError> {
self.db.db_height().await
}
pub(crate) async fn get_block_height(
&self,
hash: BlockHash,
) -> Result<Option<Height>, FinalisedStateError> {
self.db.get_block_height(hash).await
}
pub(crate) async fn get_block_hash(
&self,
height: Height,
) -> Result<Option<BlockHash>, FinalisedStateError> {
self.db.get_block_hash(height).await
}
pub(crate) async fn get_metadata(&self) -> Result<DbMetadata, FinalisedStateError> {
self.db.get_metadata().await
}
#[cfg(test)]
pub(crate) fn router(&self) -> &Router {
&self.db
}
}