use std::{fmt::Debug, ops::Bound::*, sync::Arc};
use zakura_chain::block::{self, Height};
use super::{
AdvertisedBodySize, ZAKURA_HEADER_BODY_SIZE_BY_HEIGHT, ZAKURA_HEADER_BY_HEIGHT,
ZAKURA_HEADER_HASH_BY_HEIGHT, ZAKURA_HEADER_HEIGHT_BY_HASH,
};
use crate::service::finalized_state::{
disk_db::{DiskWriteBatch, ReadDisk, WriteDisk},
disk_format::{shielded::CommitmentRootsByHeight, FromDisk, IntoDisk},
zakura_db::ZakuraDb,
COMMITMENT_ROOTS_BY_HEIGHT,
};
const LOGGED_VIOLATIONS: usize = 8;
const AUDIT_BATCH_ROWS: usize = 100_000;
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub(crate) enum ZakuraStoreViolation {
StaleRowAtCommittedHeight {
cf: &'static str,
height: Height,
},
MissingHeaderRow {
height: Height,
},
HeaderHashMismatch {
height: Height,
indexed: block::Hash,
computed: block::Hash,
},
BrokenLinkage {
height: Height,
expected_parent: block::Hash,
actual_below: block::Hash,
},
WrongHeightByHash {
height: Height,
hash: block::Hash,
indexed: Option<Height>,
},
RowAboveLastCoherent {
cf: &'static str,
height: Height,
},
OrphanHeightByHash {
hash: block::Hash,
points_at: Height,
},
}
#[allow(dead_code)]
#[derive(Debug)]
pub(crate) struct ZakuraStoreRepair {
pub last_coherent: Option<(Height, block::Hash)>,
pub deleted_rows: usize,
pub violations: Vec<ZakuraStoreViolation>,
}
impl ZakuraDb {
pub(crate) fn audit_and_repair_zakura_header_store(
&self,
) -> Result<Option<ZakuraStoreRepair>, rocksdb::Error> {
let (
Some(header_cf),
Some(hash_cf),
Some(height_by_hash_cf),
Some(body_size_cf),
Some(roots_cf),
) = (
self.db.cf_handle(ZAKURA_HEADER_BY_HEIGHT),
self.db.cf_handle(ZAKURA_HEADER_HASH_BY_HEIGHT),
self.db.cf_handle(ZAKURA_HEADER_HEIGHT_BY_HASH),
self.db.cf_handle(ZAKURA_HEADER_BODY_SIZE_BY_HEIGHT),
self.db.cf_handle(COMMITMENT_ROOTS_BY_HEIGHT),
)
else {
return Ok(None);
};
let finalized_tip = self.tip();
let provisional_roots_start =
finalized_tip.and_then(|(tip_height, _)| tip_height.next().ok());
let provisional_roots_empty = match (finalized_tip, provisional_roots_start) {
(Some(_), None) => true,
(Some(_), Some(start)) => self
.db
.zs_forward_range_iter::<_, Height, CommitmentRootsByHeight, _>(&roots_cf, start..)
.next()
.is_none(),
(None, _) => true,
};
if self.db.zs_is_empty(&header_cf)
&& self.db.zs_is_empty(&hash_cf)
&& self.db.zs_is_empty(&height_by_hash_cf)
&& self.db.zs_is_empty(&body_size_cf)
&& provisional_roots_empty
{
tracing::info!(
?finalized_tip,
frontier_rows = 0,
"zakura header store passed its startup coherence audit (empty frontier)"
);
return Ok(None);
}
let mut violations = Vec::new();
let last_coherent = finalized_tip.map(|(anchor_height, anchor_hash)| {
let (mut last_height, mut last_hash) = (anchor_height, anchor_hash);
while let Ok(height) = last_height.next() {
let Some(hash) = self.db.zs_get(&hash_cf, &height) else {
break;
};
let Some(header): Option<Arc<block::Header>> = self.db.zs_get(&header_cf, &height)
else {
violations.push(ZakuraStoreViolation::MissingHeaderRow { height });
break;
};
let computed = block::Hash::from(&*header);
if computed != hash {
violations.push(ZakuraStoreViolation::HeaderHashMismatch {
height,
indexed: hash,
computed,
});
break;
}
if header.previous_block_hash != last_hash {
violations.push(ZakuraStoreViolation::BrokenLinkage {
height,
expected_parent: header.previous_block_hash,
actual_below: last_hash,
});
break;
}
let indexed = self.db.zs_get(&height_by_hash_cf, &hash);
if indexed != Some(height) {
violations.push(ZakuraStoreViolation::WrongHeightByHash {
height,
hash,
indexed,
});
break;
}
(last_height, last_hash) = (height, hash);
}
(last_height, last_hash)
});
let committed =
|height: Height| finalized_tip.is_some_and(|(tip_height, _)| height <= tip_height);
let in_window = |height: Height| {
!committed(height)
&& last_coherent.is_some_and(|(last_height, _)| height <= last_height)
};
let mut batch = DiskWriteBatch::new();
let mut deleted_rows = 0;
let mut pending_deletes = 0;
let mut start_after_hash = None;
loop {
let heights_by_hash =
hash_keyed_batch::<Height>(&self.db, &height_by_hash_cf, start_after_hash);
let Some(&(last_hash, _)) = heights_by_hash.last() else {
break;
};
start_after_hash = Some(last_hash);
for &(hash, points_at) in &heights_by_hash {
let target_matches =
self.db.zs_get::<_, _, block::Hash>(&hash_cf, &points_at) == Some(hash);
if in_window(points_at) && target_matches {
continue;
}
if target_matches {
violations.push(if committed(points_at) {
ZakuraStoreViolation::StaleRowAtCommittedHeight {
cf: ZAKURA_HEADER_HEIGHT_BY_HASH,
height: points_at,
}
} else {
ZakuraStoreViolation::RowAboveLastCoherent {
cf: ZAKURA_HEADER_HEIGHT_BY_HASH,
height: points_at,
}
});
} else {
violations.push(ZakuraStoreViolation::OrphanHeightByHash { hash, points_at });
}
queue_repair_delete(
&self.db,
&mut batch,
&mut pending_deletes,
&height_by_hash_cf,
hash,
)?;
deleted_rows += 1;
}
}
audit_height_keyed_rows::<Arc<block::Header>>(
&self.db,
&header_cf,
ZAKURA_HEADER_BY_HEIGHT,
&committed,
&in_window,
&mut violations,
&mut batch,
&mut pending_deletes,
&mut deleted_rows,
None,
)?;
let frontier_rows = audit_height_keyed_rows::<block::Hash>(
&self.db,
&hash_cf,
ZAKURA_HEADER_HASH_BY_HEIGHT,
&committed,
&in_window,
&mut violations,
&mut batch,
&mut pending_deletes,
&mut deleted_rows,
None,
)?;
audit_height_keyed_rows::<AdvertisedBodySize>(
&self.db,
&body_size_cf,
ZAKURA_HEADER_BODY_SIZE_BY_HEIGHT,
&committed,
&in_window,
&mut violations,
&mut batch,
&mut pending_deletes,
&mut deleted_rows,
None,
)?;
if let Some(start) = match (finalized_tip, provisional_roots_start) {
(Some(_), None) => None,
(Some(_), Some(start)) => Some(start),
(None, _) => None,
} {
audit_height_keyed_rows::<CommitmentRootsByHeight>(
&self.db,
&roots_cf,
COMMITMENT_ROOTS_BY_HEIGHT,
&committed,
&in_window,
&mut violations,
&mut batch,
&mut pending_deletes,
&mut deleted_rows,
Some(start),
)?;
}
if violations.is_empty() {
tracing::info!(
?finalized_tip,
?last_coherent,
frontier_rows,
"zakura header store passed its startup coherence audit"
);
return Ok(None);
}
metrics::counter!("state.zakura.header_store.incoherent").increment(1);
tracing::warn!(
?finalized_tip,
?last_coherent,
deleted_rows,
violation_count = violations.len(),
first_violations = ?&violations[..violations.len().min(LOGGED_VIOLATIONS)],
"zakura header store failed its startup coherence audit; \
truncating to the last coherent height so header sync re-downloads the rest"
);
flush_repair_batch(&self.db, &mut batch, &mut pending_deletes)?;
Ok(Some(ZakuraStoreRepair {
last_coherent,
deleted_rows,
violations,
}))
}
}
fn height_keyed_batch<V>(
db: &crate::service::finalized_state::disk_db::DiskDb,
cf: &rocksdb::ColumnFamilyRef<'_>,
start: Height,
) -> Vec<Height>
where
V: FromDisk,
{
db.zs_forward_range_iter::<_, Height, V, _>(cf, start..)
.map(|(height, _)| height)
.take(AUDIT_BATCH_ROWS)
.collect()
}
fn hash_keyed_batch<V>(
db: &crate::service::finalized_state::disk_db::DiskDb,
cf: &rocksdb::ColumnFamilyRef<'_>,
start_after: Option<block::Hash>,
) -> Vec<(block::Hash, V)>
where
V: FromDisk,
{
match start_after {
Some(start_after) => db
.zs_forward_range_iter::<_, block::Hash, V, _>(cf, (Excluded(start_after), Unbounded))
.take(AUDIT_BATCH_ROWS)
.collect(),
None => db
.zs_forward_range_iter::<_, block::Hash, V, _>(cf, ..)
.take(AUDIT_BATCH_ROWS)
.collect(),
}
}
#[allow(clippy::too_many_arguments)]
fn audit_height_keyed_rows<V>(
db: &crate::service::finalized_state::disk_db::DiskDb,
cf: &rocksdb::ColumnFamilyRef<'_>,
cf_name: &'static str,
committed: &impl Fn(Height) -> bool,
in_window: &impl Fn(Height) -> bool,
violations: &mut Vec<ZakuraStoreViolation>,
batch: &mut DiskWriteBatch,
pending_deletes: &mut usize,
deleted_rows: &mut usize,
start: Option<Height>,
) -> Result<usize, rocksdb::Error>
where
V: FromDisk,
{
let mut next_start = start.unwrap_or(Height(0));
let mut scanned_rows = 0;
loop {
let heights = height_keyed_batch::<V>(db, cf, next_start);
let Some(&last_height) = heights.last() else {
break;
};
scanned_rows += heights.len();
next_start = match last_height.next() {
Ok(next_height) => next_height,
Err(_) => break,
};
for height in heights {
if in_window(height) {
continue;
}
violations.push(if committed(height) {
ZakuraStoreViolation::StaleRowAtCommittedHeight {
cf: cf_name,
height,
}
} else {
ZakuraStoreViolation::RowAboveLastCoherent {
cf: cf_name,
height,
}
});
queue_repair_delete(db, batch, pending_deletes, cf, height)?;
*deleted_rows += 1;
}
}
Ok(scanned_rows)
}
fn queue_repair_delete<K>(
db: &crate::service::finalized_state::disk_db::DiskDb,
batch: &mut DiskWriteBatch,
pending_deletes: &mut usize,
cf: &rocksdb::ColumnFamilyRef<'_>,
key: K,
) -> Result<(), rocksdb::Error>
where
K: IntoDisk + Debug,
{
batch.zs_delete(cf, key);
*pending_deletes += 1;
if *pending_deletes >= AUDIT_BATCH_ROWS {
flush_repair_batch(db, batch, pending_deletes)?;
}
Ok(())
}
fn flush_repair_batch(
db: &crate::service::finalized_state::disk_db::DiskDb,
batch: &mut DiskWriteBatch,
pending_deletes: &mut usize,
) -> Result<(), rocksdb::Error> {
if *pending_deletes == 0 {
return Ok(());
}
db.write(std::mem::take(batch))?;
*pending_deletes = 0;
Ok(())
}