use std::{
collections::BTreeMap,
sync::{Arc, Mutex},
};
use thiserror::Error;
use zakura_chain::{
block::Height, ironwood, orchard, parallel::commitment_aux::BlockCommitmentRoots, sapling,
transaction::Transaction,
};
use zakura_chain::subtree::NoteCommitmentSubtreeIndex;
use crate::service::finalized_state::{TransactionLocation, ZakuraDb};
pub const MAX_MEMOIZED_FRONTIERS: usize = 64;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ShieldedPool {
Sapling,
Orchard,
Ironwood,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct CompletedSubtree {
pub index: NoteCommitmentSubtreeIndex,
pub end_height: Height,
pub root: [u8; 32],
}
#[derive(Clone, Debug)]
pub struct DerivedFrontiers {
pub sapling: Arc<sapling::tree::NoteCommitmentTree>,
pub orchard: Arc<orchard::tree::NoteCommitmentTree>,
pub ironwood: Arc<ironwood::tree::NoteCommitmentTree>,
}
impl DerivedFrontiers {
pub fn empty() -> Self {
Self {
sapling: Arc::<sapling::tree::NoteCommitmentTree>::default(),
orchard: Arc::<orchard::tree::NoteCommitmentTree>::default(),
ironwood: Arc::<ironwood::tree::NoteCommitmentTree>::default(),
}
}
fn matches(&self, roots: &BlockCommitmentRoots) -> bool {
self.sapling.root() == roots.sapling_root
&& self.orchard.root() == roots.orchard_root
&& self.ironwood.root() == roots.ironwood_root
}
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
pub enum HistoricalTreeDerivationError {
#[error(
"cannot verify note commitment subtrees in ({from:?}, {to:?}]: the endpoint must be above \
the anchor"
)]
InvalidReplayRange {
from: Height,
to: Height,
},
#[error(
"deriving the note commitment tree at {height:?} needs {blocks} blocks of replay, more \
than the {limit} block limit"
)]
ReplayTooLong {
height: Height,
blocks: u64,
limit: u64,
},
#[error("cannot derive the note commitment tree at {height:?}: block body {missing:?} is not retained")]
MissingBlockBody {
height: Height,
missing: Height,
},
#[error(
"cannot derive the note commitment tree at {height:?}: no usable anchor frontier at {anchor:?}"
)]
MissingAnchor {
height: Height,
anchor: Height,
},
#[error("cannot derive the note commitment tree at {height:?}: no authenticated root is stored for that height")]
MissingAuthenticatedRoot {
height: Height,
},
#[error(
"the note commitment tree derived at {height:?} does not match the authenticated root"
)]
RootMismatch {
height: Height,
},
#[error("cannot derive the note commitment tree at {height:?}: appending block {block:?} failed: {error}")]
Append {
height: Height,
block: Height,
error: String,
},
}
#[derive(Debug, Default)]
pub struct HistoricalTreeCache {
frontiers: BTreeMap<Height, Arc<DerivedFrontiers>>,
}
impl HistoricalTreeCache {
fn anchor_at_or_below(&self, height: Height) -> Option<(Height, Arc<DerivedFrontiers>)> {
self.frontiers
.range(..=height)
.next_back()
.map(|(anchor, frontiers)| (*anchor, frontiers.clone()))
}
fn insert(&mut self, height: Height, frontiers: Arc<DerivedFrontiers>) {
self.frontiers.insert(height, frontiers);
while self.frontiers.len() > MAX_MEMOIZED_FRONTIERS {
self.frontiers.pop_first();
}
}
}
fn lock(cache: &Mutex<HistoricalTreeCache>) -> std::sync::MutexGuard<'_, HistoricalTreeCache> {
cache
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
pub fn derive_historical_frontiers(
db: &ZakuraDb,
cache: &Mutex<HistoricalTreeCache>,
height: Height,
max_replay_blocks: u64,
) -> Result<Arc<DerivedFrontiers>, HistoricalTreeDerivationError> {
derive_historical_frontiers_measured(db, cache, height, max_replay_blocks)
.map(|derivation| derivation.frontiers)
}
#[derive(Clone, Debug)]
pub struct Derivation {
pub frontiers: Arc<DerivedFrontiers>,
pub replayed_blocks: u64,
}
pub fn derive_historical_frontiers_measured(
db: &ZakuraDb,
cache: &Mutex<HistoricalTreeCache>,
height: Height,
max_replay_blocks: u64,
) -> Result<Derivation, HistoricalTreeDerivationError> {
let anchor = anchor_for(db, cache, height)?;
if let Some((anchor_height, frontiers)) = &anchor {
match anchor_height.cmp(&height) {
std::cmp::Ordering::Equal => {
verify_against_index(db, height, frontiers)?;
lock(cache).insert(height, frontiers.clone());
return Ok(Derivation {
frontiers: frontiers.clone(),
replayed_blocks: 0,
});
}
std::cmp::Ordering::Greater => {
return Err(HistoricalTreeDerivationError::MissingAnchor {
height,
anchor: *anchor_height,
});
}
std::cmp::Ordering::Less => {}
}
}
let replay_from = anchor.as_ref().map_or(0, |(anchor, _)| anchor.0 + 1);
let blocks = u64::from(
height
.0
.checked_sub(replay_from)
.expect("anchors above the requested height are rejected"),
) + 1;
if blocks > max_replay_blocks {
return Err(HistoricalTreeDerivationError::ReplayTooLong {
height,
blocks,
limit: max_replay_blocks,
});
}
let frontiers = anchor.map_or_else(DerivedFrontiers::empty, |(_, frontiers)| {
(*frontiers).clone()
});
let frontiers = replay(db, height, replay_from, frontiers)?;
verify_against_index(db, height, &frontiers)?;
let frontiers = Arc::new(frontiers);
lock(cache).insert(height, frontiers.clone());
Ok(Derivation {
frontiers,
replayed_blocks: blocks,
})
}
fn anchor_for(
db: &ZakuraDb,
cache: &Mutex<HistoricalTreeCache>,
height: Height,
) -> Result<Option<(Height, Arc<DerivedFrontiers>)>, HistoricalTreeDerivationError> {
let memoized = lock(cache).anchor_at_or_below(height);
if memoized.is_some() {
return Ok(memoized);
}
let Some(upgrade) = db.vct_upgrade_height().filter(|upgrade| upgrade.0 > 0) else {
return Ok(None);
};
let anchor = Height(upgrade.0 - 1);
if db
.finalized_tip_height()
.is_none_or(|tip_height| anchor > tip_height)
{
return Err(HistoricalTreeDerivationError::MissingAnchor { height, anchor });
}
let (Some(sapling), Some(orchard), Some(ironwood)) = (
db.latest_stored_sapling_tree(&anchor),
db.latest_stored_orchard_tree(&anchor),
db.latest_stored_ironwood_tree(&anchor),
) else {
return Err(HistoricalTreeDerivationError::MissingAnchor { height, anchor });
};
Ok(Some((
anchor,
Arc::new(DerivedFrontiers {
sapling,
orchard,
ironwood,
}),
)))
}
fn replay(
db: &ZakuraDb,
height: Height,
replay_from: u32,
frontiers: DerivedFrontiers,
) -> Result<DerivedFrontiers, HistoricalTreeDerivationError> {
replay_with_subtrees(db, height, replay_from, frontiers, |_, _| {})
}
pub fn replay_with_subtrees(
db: &ZakuraDb,
height: Height,
replay_from: u32,
frontiers: DerivedFrontiers,
mut on_subtree: impl FnMut(ShieldedPool, CompletedSubtree),
) -> Result<DerivedFrontiers, HistoricalTreeDerivationError> {
let DerivedFrontiers {
sapling,
orchard,
ironwood,
} = frontiers;
let mut sapling = (*sapling).clone();
let mut orchard = (*orchard).clone();
let mut ironwood = (*ironwood).clone();
let first_height = Height(replay_from);
let mut transactions = db
.transactions_by_location_range(
TransactionLocation::min_for_height(first_height)
..=TransactionLocation::max_for_height(height),
)
.peekable();
for block_height in replay_from..=height.0 {
let block_height = Height(block_height);
let append_error = |error: &dyn std::fmt::Display| HistoricalTreeDerivationError::Append {
height,
block: block_height,
error: error.to_string(),
};
let Some((first_location, first_transaction)) = transactions.next() else {
return Err(HistoricalTreeDerivationError::MissingBlockBody {
height,
missing: block_height,
});
};
if first_location != TransactionLocation::min_for_height(block_height) {
return Err(HistoricalTreeDerivationError::MissingBlockBody {
height,
missing: block_height,
});
}
let mut sapling_commitments = Vec::new();
let mut orchard_commitments = Vec::new();
let mut ironwood_commitments = Vec::new();
extend_commitments(
&first_transaction,
&mut sapling_commitments,
&mut orchard_commitments,
&mut ironwood_commitments,
);
while transactions
.peek()
.is_some_and(|(location, _)| location.height == block_height)
{
let (_, transaction) = transactions
.next()
.expect("a transaction observed through peek is available");
extend_commitments(
&transaction,
&mut sapling_commitments,
&mut orchard_commitments,
&mut ironwood_commitments,
);
}
if !sapling_commitments.is_empty() {
let completed = sapling
.append_batch(&sapling_commitments)
.map_err(|error| append_error(&error))?;
if let Some((index, root)) = completed {
on_subtree(
ShieldedPool::Sapling,
CompletedSubtree {
index,
end_height: block_height,
root: root.to_bytes(),
},
);
}
}
if !orchard_commitments.is_empty() {
let completed = orchard
.append_batch(&orchard_commitments)
.map_err(|error| append_error(&error))?;
if let Some((index, root)) = completed {
on_subtree(
ShieldedPool::Orchard,
CompletedSubtree {
index,
end_height: block_height,
root: root.to_repr(),
},
);
}
}
if !ironwood_commitments.is_empty() {
let completed = ironwood
.append_batch(&ironwood_commitments)
.map_err(|error| append_error(&error))?;
if let Some((index, root)) = completed {
on_subtree(
ShieldedPool::Ironwood,
CompletedSubtree {
index,
end_height: block_height,
root: root.to_repr(),
},
);
}
}
}
Ok(DerivedFrontiers {
sapling: Arc::new(sapling),
orchard: Arc::new(orchard),
ironwood: Arc::new(ironwood),
})
}
fn extend_commitments(
transaction: &Transaction,
sapling: &mut Vec<sapling::tree::NoteCommitmentUpdate>,
orchard: &mut Vec<orchard::tree::NoteCommitmentUpdate>,
ironwood: &mut Vec<ironwood::tree::NoteCommitmentUpdate>,
) {
sapling.extend(transaction.sapling_note_commitments().cloned());
orchard.extend(transaction.orchard_note_commitments().copied());
ironwood.extend(transaction.ironwood_note_commitments().copied());
}
pub fn verify_against_index(
db: &ZakuraDb,
height: Height,
frontiers: &DerivedFrontiers,
) -> Result<(), HistoricalTreeDerivationError> {
let roots = authenticated_roots(db, height)?;
if frontiers.matches(&roots) {
Ok(())
} else {
Err(HistoricalTreeDerivationError::RootMismatch { height })
}
}
fn authenticated_roots(
db: &ZakuraDb,
height: Height,
) -> Result<BlockCommitmentRoots, HistoricalTreeDerivationError> {
db.commitment_roots_by_height_range(height..=height)
.into_iter()
.next()
.filter(|roots| roots.height == height)
.ok_or(HistoricalTreeDerivationError::MissingAuthenticatedRoot { height })
}