use std::{
collections::{BTreeMap, BTreeSet},
time::{Duration, Instant},
};
use zakura_chain::{block::Height, subtree::NoteCommitmentSubtreeIndex};
use crate::service::{
finalized_state::{CommitmentRootIndexIssue, ZakuraDb},
read::{
historical_tree::{
derive_historical_frontiers_measured, replay_with_subtrees, verify_against_index,
CompletedSubtree, HistoricalTreeDerivationError, ShieldedPool,
},
DerivedFrontiers, HistoricalTreeCache,
},
};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VctTreestateInventory {
pub finalized_tip: Option<Height>,
pub upgrade_height: Option<Height>,
pub last_checkpoint: Option<Height>,
pub lowest_retained_height: Option<Height>,
pub root_index_scanned: bool,
pub block_bodies_scanned: bool,
pub root_index_gap: Option<Height>,
pub malformed_root_row: Option<Height>,
pub missing_block_body: Option<Height>,
pub missing_anchor: Option<Height>,
pub scan_duration: Duration,
}
impl VctTreestateInventory {
pub fn absent_band(&self) -> Option<(Height, Height)> {
let last_checkpoint = self.last_checkpoint?;
let finalized_tip = self.finalized_tip?;
let upgrade = self.upgrade_height.unwrap_or(Height(0));
let committed_end = Height(last_checkpoint.0.min(finalized_tip.0.saturating_add(1)));
(upgrade < committed_end).then_some((upgrade, committed_end))
}
pub fn can_derive(&self) -> Option<bool> {
let known_problem = self.absent_band().is_none()
|| self.root_index_gap.is_some()
|| self.malformed_root_row.is_some()
|| self.missing_block_body.is_some()
|| self.missing_anchor.is_some();
if known_problem {
Some(false)
} else if self.root_index_scanned && self.block_bodies_scanned {
Some(true)
} else {
None
}
}
}
pub fn inventory(db: &ZakuraDb, scan_band: bool) -> VctTreestateInventory {
inventory_with_scans(db, scan_band, scan_band)
}
pub fn inventory_with_scans(
db: &ZakuraDb,
scan_root_index: bool,
scan_block_bodies: bool,
) -> VctTreestateInventory {
let start = Instant::now();
let upgrade_height = db.vct_upgrade_height();
let last_checkpoint = db.vct_synced_below();
let mut inventory = VctTreestateInventory {
finalized_tip: db.finalized_tip_height(),
upgrade_height,
last_checkpoint,
lowest_retained_height: db.lowest_retained_height(),
root_index_scanned: scan_root_index,
block_bodies_scanned: scan_block_bodies,
root_index_gap: None,
malformed_root_row: None,
missing_block_body: None,
missing_anchor: None,
scan_duration: Duration::ZERO,
};
if let Some((band_start, band_end)) = inventory.absent_band() {
let last = Height(band_end.0 - 1);
if scan_root_index {
match db.first_commitment_root_issue(band_start..=last) {
Some(CommitmentRootIndexIssue::Missing(height)) => {
inventory.root_index_gap = Some(height);
}
Some(CommitmentRootIndexIssue::Malformed(height)) => {
inventory.malformed_root_row = Some(height);
}
None => {}
}
}
if scan_block_bodies {
inventory.missing_block_body = db.first_missing_block_body(band_start, last);
}
}
if let Some((band_start, _)) = inventory.absent_band() {
if band_start.0 > 0 {
let anchor = Height(band_start.0 - 1);
if db.latest_stored_sapling_tree(&anchor).is_none()
|| db.latest_stored_orchard_tree(&anchor).is_none()
|| db.latest_stored_ironwood_tree(&anchor).is_none()
{
inventory.missing_anchor = Some(anchor);
}
}
}
inventory.scan_duration = start.elapsed();
inventory
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DerivationSample {
pub height: Height,
pub replayed_blocks: u64,
pub elapsed: Duration,
}
impl DerivationSample {
pub fn per_block(&self) -> Option<Duration> {
(self.replayed_blocks > 0)
.then(|| self.elapsed / u32::try_from(self.replayed_blocks).unwrap_or(u32::MAX))
}
}
pub fn measure_derivations(
db: &ZakuraDb,
cache: &std::sync::Mutex<HistoricalTreeCache>,
heights: impl IntoIterator<Item = Height>,
max_replay_blocks: u64,
mut on_sample: impl FnMut(&DerivationSample),
) -> Result<(), (Height, HistoricalTreeDerivationError)> {
for height in heights {
let start = Instant::now();
let derivation = derive_historical_frontiers_measured(db, cache, height, max_replay_blocks)
.map_err(|error| (height, error))?;
let elapsed = start.elapsed();
let replayed_blocks = derivation.replayed_blocks;
let sample = DerivationSample {
height,
replayed_blocks,
elapsed,
};
on_sample(&sample);
}
Ok(())
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct SubtreeVerification {
pub matched: usize,
pub mismatched: Vec<(NoteCommitmentSubtreeIndex, &'static str)>,
pub unstored: usize,
pub stored_only: Vec<(NoteCommitmentSubtreeIndex, &'static str)>,
}
pub fn verify_subtrees_against_stored(
db: &ZakuraDb,
from: Height,
to: Height,
) -> Result<SubtreeVerification, HistoricalTreeDerivationError> {
if to <= from {
return Err(HistoricalTreeDerivationError::InvalidReplayRange { from, to });
}
let (Some(sapling), Some(orchard), Some(ironwood)) = (
db.latest_stored_sapling_tree(&from),
db.latest_stored_orchard_tree(&from),
db.latest_stored_ironwood_tree(&from),
) else {
return Err(HistoricalTreeDerivationError::MissingAnchor {
height: to,
anchor: from,
});
};
let anchor = DerivedFrontiers {
sapling,
orchard,
ironwood,
};
let mut completions = Vec::new();
let frontiers = replay_with_subtrees(db, to, from.0 + 1, anchor, |pool, completed| {
completions.push((pool, completed))
})?;
verify_against_index(db, to, &frontiers)?;
let stored = db
.sapling_subtree_list_by_index_range(..)
.into_iter()
.map(|(index, data)| (("sapling", index), (data.root.to_bytes(), data.end_height)))
.chain(
db.orchard_subtree_list_by_index_range(..)
.into_iter()
.map(|(index, data)| (("orchard", index), (data.root.to_repr(), data.end_height))),
)
.chain(
db.ironwood_subtree_list_by_index_range(..)
.into_iter()
.map(|(index, data)| (("ironwood", index), (data.root.to_repr(), data.end_height))),
)
.collect();
Ok(compare_subtrees(completions, stored, from, to))
}
fn compare_subtrees(
completions: impl IntoIterator<Item = (ShieldedPool, CompletedSubtree)>,
stored: BTreeMap<(&'static str, NoteCommitmentSubtreeIndex), ([u8; 32], Height)>,
from: Height,
to: Height,
) -> SubtreeVerification {
let mut outcome = SubtreeVerification::default();
let mut replayed = BTreeSet::new();
for (pool, completed) in completions {
let pool_name = pool_name(pool);
replayed.insert((pool_name, completed.index));
match stored.get(&(pool_name, completed.index)) {
Some((root, end_height)) if stored_subtree_matches(*root, *end_height, &completed) => {
outcome.matched += 1
}
Some(_) => outcome.mismatched.push((completed.index, pool_name)),
None => outcome.unstored += 1,
}
}
outcome.stored_only = stored
.into_iter()
.filter_map(|((pool, index), (_, end_height))| {
(end_height > from && end_height <= to && !replayed.contains(&(pool, index)))
.then_some((index, pool))
})
.collect();
outcome
}
fn pool_name(pool: ShieldedPool) -> &'static str {
match pool {
ShieldedPool::Sapling => "sapling",
ShieldedPool::Orchard => "orchard",
ShieldedPool::Ironwood => "ironwood",
}
}
fn stored_subtree_matches(
stored_root: [u8; 32],
stored_end_height: Height,
completed: &CompletedSubtree,
) -> bool {
stored_root == completed.root && stored_end_height == completed.end_height
}
pub fn derived_roots_in_display_order(
db: &ZakuraDb,
cache: &std::sync::Mutex<HistoricalTreeCache>,
heights: impl IntoIterator<Item = Height>,
max_replay_blocks: u64,
) -> Result<Vec<(Height, String, String, String)>, (Height, HistoricalTreeDerivationError)> {
let mut roots = Vec::new();
for height in heights {
let derivation = derive_historical_frontiers_measured(db, cache, height, max_replay_blocks)
.map_err(|error| (height, error))?;
roots.push((
height,
hex::encode(derivation.frontiers.sapling.root().bytes_in_display_order()),
hex::encode(derivation.frontiers.orchard.root().bytes_in_display_order()),
hex::encode(
derivation
.frontiers
.ironwood
.root()
.bytes_in_display_order(),
),
));
}
Ok(roots)
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct ReplayInputs {
pub blocks: u64,
pub bytes: u64,
pub commitments: u64,
}
pub fn replay_inputs(db: &ZakuraDb, from: Height, to: Height) -> ReplayInputs {
let mut inputs = ReplayInputs::default();
for height in from.0..=to.0 {
let height = Height(height);
inputs.blocks += 1;
inputs.bytes += db
.block_info(height.into())
.map_or(0, |info| u64::from(info.size()));
if let Some(block) = db.block(height.into()) {
inputs.commitments += (block.sapling_note_commitments().count()
+ block.orchard_note_commitments().count()
+ block.ironwood_note_commitments().count())
as u64;
}
}
inputs
}
#[cfg(test)]
mod tests {
use super::*;
fn inventory_with_tip(finalized_tip: Height) -> VctTreestateInventory {
VctTreestateInventory {
finalized_tip: Some(finalized_tip),
upgrade_height: Some(Height(100)),
last_checkpoint: Some(Height(200)),
lowest_retained_height: None,
root_index_scanned: true,
block_bodies_scanned: true,
root_index_gap: None,
malformed_root_row: None,
missing_block_body: None,
missing_anchor: None,
scan_duration: Duration::ZERO,
}
}
#[test]
fn absent_band_is_capped_at_finalized_tip() {
assert_eq!(
inventory_with_tip(Height(149)).absent_band(),
Some((Height(100), Height(150)))
);
assert_eq!(
inventory_with_tip(Height(250)).absent_band(),
Some((Height(100), Height(200)))
);
assert_eq!(inventory_with_tip(Height(99)).absent_band(), None);
}
#[test]
fn missing_anchor_prevents_derivation() {
let mut inventory = inventory_with_tip(Height(149));
assert_eq!(inventory.can_derive(), Some(true));
inventory.missing_anchor = Some(Height(99));
assert_eq!(inventory.can_derive(), Some(false));
}
#[test]
fn malformed_root_row_prevents_derivation() {
let mut inventory = inventory_with_tip(Height(149));
inventory.malformed_root_row = Some(Height(125));
assert_eq!(inventory.can_derive(), Some(false));
}
#[test]
fn deferred_body_scan_keeps_derivation_pending_unless_an_issue_is_known() {
let mut inventory = inventory_with_tip(Height(149));
inventory.block_bodies_scanned = false;
assert_eq!(inventory.can_derive(), None);
inventory.root_index_gap = Some(Height(125));
assert_eq!(inventory.can_derive(), Some(false));
}
#[test]
fn stored_subtree_match_requires_root_and_end_height() {
let completed = CompletedSubtree {
index: NoteCommitmentSubtreeIndex(3),
end_height: Height(100),
root: [7; 32],
};
assert!(stored_subtree_matches(
completed.root,
completed.end_height,
&completed
));
assert!(!stored_subtree_matches(
completed.root,
Height(101),
&completed
));
assert!(!stored_subtree_matches(
[8; 32],
completed.end_height,
&completed
));
}
#[test]
fn subtree_comparison_detects_stored_only_rows_in_range() {
let replayed = CompletedSubtree {
index: NoteCommitmentSubtreeIndex(3),
end_height: Height(100),
root: [7; 32],
};
let stored = [
(
("sapling", replayed.index),
(replayed.root, replayed.end_height),
),
(
("sapling", NoteCommitmentSubtreeIndex(4)),
([8; 32], Height(101)),
),
(
("orchard", NoteCommitmentSubtreeIndex(2)),
([9; 32], Height(99)),
),
]
.into_iter()
.collect();
let outcome = compare_subtrees(
[(ShieldedPool::Sapling, replayed)],
stored,
Height(99),
Height(101),
);
assert_eq!(outcome.matched, 1);
assert_eq!(
outcome.stored_only,
[(NoteCommitmentSubtreeIndex(4), "sapling")]
);
}
}