#![allow(dead_code)]
pub(crate) mod capability;
pub(crate) mod entry;
pub(crate) mod finalised_source;
pub(crate) mod migrations;
pub(crate) mod reader;
pub(crate) mod router;
use capability::*;
use finalised_source::{FinalisedSource, 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::{finalised_source::v1::DB_VERSION_V1, router::EphemeralMode},
source::BlockchainSourceError,
types::GENESIS_HEIGHT,
},
config::ChainIndexConfig,
error::FinalisedStateError,
BlockHash, BlockMetadata, BlockWithMetadata, ChainWork, Height, IndexedBlock, StatusType,
};
use std::{sync::Arc, time::Duration};
use tokio::time::{interval, MissedTickBehavior};
pub(crate) async fn build_indexed_block_from_source<S: BlockchainSource>(
source: &S,
network: zaino_common::Network,
sapling_activation_height: zebra_chain::block::Height,
nu5_activation_height: Option<zebra_chain::block::Height>,
height_int: u32,
parent_chainwork: Option<ChainWork>,
) -> Result<IndexedBlock, FinalisedStateError> {
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 {height_int} from validator"
)),
));
}
};
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);
IndexedBlock::try_from(block_with_metadata).map_err(|_| {
FinalisedStateError::BlockchainSourceError(BlockchainSourceError::Unrecoverable(format!(
"error building block data at height {height_int}"
)))
})
}
use super::source::BlockchainSource;
const LONG_RUNNING_SYNC_THRESHOLD: u32 = 10;
const MAX_BACKGROUND_SYNC_RETRIES: u32 = 5;
const BACKGROUND_SYNC_RETRY_BACKOFF: Duration = Duration::from_secs(5);
#[derive(Debug)]
pub(crate) struct FinalisedState<T: BlockchainSource> {
db: Arc<Router<T>>,
cfg: ChainIndexConfig,
}
impl<T: BlockchainSource> FinalisedState<T> {
#[instrument(name = "FinalisedState::spawn", skip(cfg, source), fields(db_version = cfg.db_version))]
pub(crate) async fn spawn(
cfg: ChainIndexConfig,
source: T,
) -> Result<Self, FinalisedStateError> {
if cfg.ephemeral {
return Ok(Self {
db: Arc::new(Router::new(Arc::new(FinalisedSource::ephemeral(
source,
cfg.network.into(),
None,
)))),
cfg,
});
} else {
let version_opt = Self::try_find_current_db_version(&cfg).await;
let target_version = match cfg.db_version {
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 FinalisedState from file");
match version {
0 => {
return Err(FinalisedStateError::Custom(format!(
"legacy v0 database detected at {}; v0 is no longer supported. \
Remove the directory and restart to resync a v1 database from genesis.",
cfg.storage.database.path.display()
)));
}
1 => FinalisedSource::spawn_v1(&cfg).await?,
_ => {
return Err(FinalisedStateError::Custom(format!(
"unsupported database version: DbV{version}"
)));
}
}
}
None => {
info!(version = %target_version, "Creating new FinalisedState");
match target_version.major() {
1 => FinalisedSource::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 FinalisedState migration in background"
);
let migration_router = Arc::clone(&router);
let migration_cfg = cfg.clone();
let migration_source = source.clone();
let op_guard = router.begin_background_op();
tokio::spawn(async move {
let _op_guard = op_guard;
let mut migration_manager = MigrationManager {
router: migration_router.clone(),
cfg: migration_cfg,
current_version,
target_version,
source: migration_source,
};
if let Err(error) = migration_manager.migrate().await {
tracing::error!("FinalisedState migration failed: {error}");
migration_router.store_primary_status(StatusType::CriticalError);
}
});
}
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) async fn wait_until_synced(&self) {
let mut ticker = interval(Duration::from_millis(100));
ticker.set_missed_tick_behavior(MissedTickBehavior::Delay);
loop {
ticker.tick().await;
if !self.db.has_background_ops()
&& matches!(
self.db.status(),
StatusType::Ready | StatusType::CriticalError
)
{
break;
}
}
}
pub(crate) fn to_reader(self: &Arc<Self>) -> DbReader<T> {
DbReader {
inner: Arc::clone(self),
}
}
async fn try_find_current_db_version(cfg: &ChainIndexConfig) -> 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<FinalisedSource<T>>, FinalisedStateError> {
self.db.backend(cap)
}
pub(crate) async fn sync_to_height(
&self,
height: Height,
source: &T,
) -> Result<(), FinalisedStateError>
where
T: Send + Sync + 'static,
{
if self.db.primary_is_ephemeral() {
return Ok(());
}
if self.db.has_full_ephemeral_reference() {
return Ok(());
}
if self.db.has_background_ops() {
return Ok(());
}
let primary = self.db.primary_backend();
let db_height_opt = primary.db_height().await?;
if let Some(existing) = db_height_opt {
if height <= existing {
return Ok(());
}
}
let db_height = db_height_opt.unwrap_or(GENESIS_HEIGHT);
let sync_is_long_running =
height.0.saturating_sub(db_height.0) > LONG_RUNNING_SYNC_THRESHOLD;
let router = Arc::clone(&self.db);
let cfg = self.cfg.clone();
let source = source.clone();
if sync_is_long_running {
let op_guard = router.begin_background_op();
let ephemeral_reference = router
.init_or_take_ephemeral(
source.clone(),
cfg.network.to_zebra_network(),
EphemeralMode::ReadOnly,
db_height_opt,
)
.await?;
tokio::spawn(async move {
let _op_guard = op_guard;
let _ephemeral_reference = ephemeral_reference;
let mut attempt: u32 = 0;
loop {
if router.has_full_ephemeral_reference() {
return;
}
match Self::sync_to_height_background(
router.clone(),
cfg.clone(),
height,
source.clone(),
)
.await
{
Ok(()) => return,
Err(error) => {
attempt += 1;
if attempt >= MAX_BACKGROUND_SYNC_RETRIES {
tracing::error!(
"FinalisedState background sync_to_height failed after {attempt} \
attempts, giving up: {error}"
);
router.store_primary_status(StatusType::CriticalError);
return;
}
tracing::warn!(
"FinalisedState background sync_to_height failed (attempt \
{attempt}/{MAX_BACKGROUND_SYNC_RETRIES}), retrying: {error}"
);
router.store_primary_status(StatusType::RecoverableError);
tokio::time::sleep(BACKGROUND_SYNC_RETRY_BACKOFF).await;
}
}
}
});
Ok(())
} else {
Self::sync_to_height_background(router, cfg, height, source).await
}
}
async fn sync_to_height_background(
router: Arc<Router<T>>,
_cfg: ChainIndexConfig,
height: Height,
source: T,
) -> Result<(), FinalisedStateError>
where
T: Send + Sync + 'static,
{
if router.primary_is_ephemeral() {
return Ok(());
}
if router.has_full_ephemeral_reference() {
return Ok(());
}
let result = router.write_blocks_to_height(height, &source).await;
if result.is_ok() {
router.update_ephemeral_db_height(Some(height))?;
let env = router.backend(CapabilityRequest::WriteCore)?.env()?;
tokio::task::block_in_place(|| env.sync(true))
.map_err(FinalisedStateError::LmdbError)?;
}
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)]
impl<T: BlockchainSource> FinalisedState<T> {
pub(crate) fn router(&self) -> &Router<T> {
&self.db
}
pub(crate) async fn spawn_with_target_version(
cfg: ChainIndexConfig,
source: T,
target_version: DbVersion,
) -> Result<Self, FinalisedStateError> {
if target_version.major() > DB_VERSION_V1.major() {
return Err(FinalisedStateError::Custom(format!(
"unsupported database version: {target_version}"
)));
}
if target_version.major() == DB_VERSION_V1.major() && target_version > DB_VERSION_V1 {
return Err(FinalisedStateError::Custom(format!(
"unsupported database version: {target_version}"
)));
}
let version_opt = Self::try_find_current_db_version(&cfg).await;
let backend = match version_opt {
Some(version) => {
info!(version, "Opening FinalisedState from file");
match version {
1 => FinalisedSource::spawn_v1(&cfg).await?,
_ => {
return Err(FinalisedStateError::Custom(format!(
"unsupported database version: DbV{version}"
)));
}
}
}
None => {
return Err(FinalisedStateError::Custom(
"expected existing v1.0.0 migration-test database, found no database"
.to_string(),
));
}
};
let current_version = backend.get_metadata().await?.version();
let router = Arc::new(Router::new(Arc::new(backend)));
if current_version < target_version {
info!(
from_version = %current_version,
to_version = %target_version,
"Starting FinalisedState migration"
);
let mut migration_manager = MigrationManager {
router: Arc::clone(&router),
cfg: cfg.clone(),
current_version,
target_version,
source,
};
migration_manager.migrate().await?;
}
let metadata = router.get_metadata().await?;
if metadata.version() != target_version {
return Err(FinalisedStateError::Custom(format!(
"database version mismatch after test spawn: expected {}, found {}",
target_version,
metadata.version()
)));
}
Ok(Self { db: router, cfg })
}
pub(crate) async fn build_clean_v1_0_0(
cfg: &ChainIndexConfig,
source: T,
) -> Result<FinalisedSource<T>, FinalisedStateError> {
let db = FinalisedSource::spawn_v1_0_0(cfg).await?;
db.wait_until_ready().await;
let tip = source.get_best_block_height().await?.ok_or_else(|| {
FinalisedStateError::BlockchainSourceError(BlockchainSourceError::Unrecoverable(
"source has no best block height".to_string(),
))
})?;
let tip = Height::from(tip);
let mut parent_chainwork: Option<ChainWork> = None;
for height in crate::chain_index::types::GENESIS_HEIGHT.0..=tip.0 {
let block = source
.get_block(zebra_state::HashOrHeight::Height(
zebra_chain::block::Height(height),
))
.await?
.ok_or_else(|| {
FinalisedStateError::BlockchainSourceError(
BlockchainSourceError::Unrecoverable(format!(
"source missing block at height {height}"
)),
)
})?;
let block_hash = BlockHash::from(block.hash().0);
let (sapling_opt, orchard_opt) = source.get_commitment_tree_roots(block_hash).await?;
let (sapling_root, sapling_size) = sapling_opt.ok_or_else(|| {
FinalisedStateError::BlockchainSourceError(BlockchainSourceError::Unrecoverable(
format!("missing Sapling commitment tree root for block {block_hash}"),
))
})?;
let (orchard_root, orchard_size) = orchard_opt.ok_or_else(|| {
FinalisedStateError::BlockchainSourceError(BlockchainSourceError::Unrecoverable(
format!("missing Orchard commitment tree root for block {block_hash}"),
))
})?;
let metadata = BlockMetadata::new(
sapling_root,
sapling_size as u32,
orchard_root,
orchard_size as u32,
parent_chainwork,
cfg.network.to_zebra_network(),
);
let block_with_metadata = BlockWithMetadata::new(block.as_ref(), metadata);
let chain_block = IndexedBlock::try_from(block_with_metadata).map_err(|_| {
FinalisedStateError::BlockchainSourceError(BlockchainSourceError::Unrecoverable(
format!("error building block data at height {height}"),
))
})?;
parent_chainwork = Some(chain_block.context.chainwork);
db.write_block_v1_0_0(chain_block).await?;
}
Ok(db)
}
pub(crate) async fn build_db_to_version(
cfg: ChainIndexConfig,
source: T,
target_version: DbVersion,
) -> Result<Self, FinalisedStateError> {
let v1_0_0 = DbVersion::new(1, 0, 0);
if target_version < v1_0_0 {
return Err(FinalisedStateError::Custom(format!(
"target version {} is older than v1.0.0",
target_version
)));
}
let db = Self::build_clean_v1_0_0(&cfg, source.clone()).await?;
db.shutdown().await?;
drop(db);
Self::spawn_with_target_version(cfg, source, target_version).await
}
}