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::{
serve_block_roots, FrontierArtifact, TransactionLocation, ZakuraDb,
};
pub const MAX_CACHED_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>>,
artifact: Option<Arc<FrontierArtifact>>,
}
impl HistoricalTreeCache {
pub fn with_artifact(artifact: Arc<FrontierArtifact>) -> Self {
Self {
frontiers: BTreeMap::new(),
artifact: Some(artifact),
}
}
pub(crate) fn last_checkpoint(&self) -> Option<Height> {
self.artifact
.as_ref()
.map(|artifact| artifact.last_checkpoint)
}
fn artifact_anchor_at_or_below(&self, height: Height) -> Option<(Height, DerivedFrontiers)> {
let entry = self.artifact.as_ref()?.anchor_at_or_below(height)?;
Some((
entry.height,
DerivedFrontiers {
sapling: entry.sapling.clone(),
orchard: entry.orchard.clone(),
ironwood: entry.ironwood.clone(),
},
))
}
fn cached_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_CACHED_FRONTIERS {
self.frontiers.pop_first();
}
}
}
fn lock(cache: &Mutex<HistoricalTreeCache>) -> std::sync::MutexGuard<'_, HistoricalTreeCache> {
cache
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn published_is_nearer(cached: Option<Height>, published: Option<Height>) -> bool {
published.is_some_and(|published_height| {
cached.is_none_or(|cached_height| published_height > cached_height)
})
}
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_with_subtrees(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 cached = lock(cache).cached_anchor_at_or_below(height);
let cached_height = cached.as_ref().map(|(anchor_height, _)| *anchor_height);
let published_floor = db.vct_upgrade_height().unwrap_or(Height(0));
let mut skip_at_or_above: Option<Height> = None;
loop {
let search_height = match skip_at_or_above {
None => Some(height),
Some(Height(0)) => None,
Some(skipped) => Some(Height(skipped.0 - 1)),
};
let Some(search_height) = search_height else {
break;
};
let published = lock(cache).artifact_anchor_at_or_below(search_height);
let Some((anchor_height, frontiers)) = published else {
break;
};
if anchor_height < published_floor {
break;
}
if !published_is_nearer(cached_height, Some(anchor_height)) {
break;
}
match verify_against_index(db, anchor_height, &frontiers) {
Ok(()) => {
let frontiers = Arc::new(frontiers);
lock(cache).insert(anchor_height, frontiers.clone());
return Ok(Some((anchor_height, frontiers)));
}
Err(error) => {
tracing::warn!(
?anchor_height,
%error,
"ignoring a published frontier entry that does not match the authenticated root",
);
metrics::counter!("state.historical_tree.artifact_entry_rejected").increment(1);
skip_at_or_above = Some(anchor_height);
}
}
}
if cached.is_some() {
return Ok(cached);
}
stored_frontier_before_absent_band(db, height)
.map(|anchor| anchor.map(|(height, frontiers)| (height, Arc::new(frontiers))))
}
pub(crate) fn stored_frontier_before_absent_band(
db: &ZakuraDb,
height: Height,
) -> Result<Option<(Height, DerivedFrontiers)>, HistoricalTreeDerivationError> {
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 frontiers = stored_frontiers_at(db, anchor)
.map_err(|_| HistoricalTreeDerivationError::MissingAnchor { height, anchor })?;
Ok(Some((anchor, frontiers)))
}
pub(crate) fn stored_frontiers_at(
db: &ZakuraDb,
height: Height,
) -> Result<DerivedFrontiers, HistoricalTreeDerivationError> {
let (Some(sapling), Some(orchard), Some(ironwood)) = (
db.latest_stored_sapling_tree(&height),
db.latest_stored_orchard_tree(&height),
db.latest_stored_ironwood_tree(&height),
) else {
return Err(HistoricalTreeDerivationError::MissingAnchor {
height,
anchor: height,
});
};
Ok(DerivedFrontiers {
sapling,
orchard,
ironwood,
})
}
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 })
}
}
pub(crate) fn verify_against_available_roots(
db: &ZakuraDb,
height: Height,
frontiers: &DerivedFrontiers,
) -> Result<(), HistoricalTreeDerivationError> {
let roots = serve_block_roots(db, height..=height)
.into_iter()
.next()
.filter(|roots| roots.height == height)
.ok_or(HistoricalTreeDerivationError::MissingAuthenticatedRoot { 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 })
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex};
use zakura_chain::{
block::{merkle::AuthDataRoot, Height},
orchard,
parameters::Network,
};
use crate::{
config::Config,
constants::{
state_database_format_version_in_code, MAX_HISTORICAL_TREE_REPLAY_BLOCKS,
STATE_DATABASE_KIND,
},
service::finalized_state::{
DiskWriteBatch, FrontierArtifact, FrontierEntry, STATE_COLUMN_FAMILIES_IN_CODE,
},
};
use super::*;
const LOW: Height = Height(1_000);
const MID: Height = Height(2_000_000);
const HIGH: Height = Height(3_000_000);
fn ephemeral_db() -> ZakuraDb {
ZakuraDb::new(
&Config::ephemeral(),
STATE_DATABASE_KIND,
&state_database_format_version_in_code(),
&Network::Mainnet,
true,
STATE_COLUMN_FAMILIES_IN_CODE
.iter()
.map(ToString::to_string),
false,
)
.expect("opening an ephemeral database should succeed")
}
fn mismatched_frontiers() -> DerivedFrontiers {
let mut orchard = orchard::tree::NoteCommitmentTree::default();
orchard
.append(halo2::pasta::pallas::Base::from(1u64))
.expect("test tree is not full");
DerivedFrontiers {
sapling: Arc::new(Default::default()),
orchard: Arc::new(orchard),
ironwood: Arc::new(Default::default()),
}
}
fn seed_roots(db: &ZakuraDb, height: Height, frontiers: &DerivedFrontiers) {
let mut batch = DiskWriteBatch::new();
batch.insert_commitment_roots_by_height(
db,
height,
&frontiers.sapling.root(),
&frontiers.orchard.root(),
&frontiers.ironwood.root(),
0,
0,
0,
&AuthDataRoot::from([0; 32]),
);
db.write_batch(batch)
.expect("seeding authenticated roots succeeds");
}
fn artifact_at(height: Height, frontiers: &DerivedFrontiers) -> Arc<FrontierArtifact> {
artifact_entries(Height(height.0 + 1), &[(height, frontiers)])
}
fn artifact_entries(
last_checkpoint: Height,
entries: &[(Height, &DerivedFrontiers)],
) -> Arc<FrontierArtifact> {
Arc::new(FrontierArtifact {
spacing: 1,
last_checkpoint,
entries: entries
.iter()
.map(|(height, frontiers)| FrontierEntry {
height: *height,
sapling: frontiers.sapling.clone(),
orchard: frontiers.orchard.clone(),
ironwood: frontiers.ironwood.clone(),
})
.collect(),
})
}
fn cache_with(
artifact: Arc<FrontierArtifact>,
cached_height: Height,
cached: DerivedFrontiers,
) -> Mutex<HistoricalTreeCache> {
let mut cache = HistoricalTreeCache::with_artifact(artifact);
cache.insert(cached_height, Arc::new(cached));
Mutex::new(cache)
}
#[test]
fn published_is_nearer_takes_the_higher_height() {
assert!(published_is_nearer(None, Some(HIGH)));
assert!(published_is_nearer(Some(LOW), Some(HIGH)));
assert!(!published_is_nearer(Some(HIGH), Some(LOW)));
assert!(!published_is_nearer(Some(HIGH), Some(HIGH)));
assert!(!published_is_nearer(Some(LOW), None));
assert!(!published_is_nearer(None, None));
}
#[test]
fn genesis_absent_band_has_no_stored_predecessor() {
let _init_guard = zakura_test::init();
let db = ephemeral_db();
assert!(stored_frontier_before_absent_band(&db, Height(0))
.expect("an unset upgrade marker starts replay at genesis")
.is_none());
let mut batch = DiskWriteBatch::new();
batch.update_vct_upgrade_marker(&db, Height(0));
db.write_batch(batch)
.expect("seeding a genesis upgrade marker succeeds");
assert!(stored_frontier_before_absent_band(&db, Height(0))
.expect("a genesis upgrade starts replay at genesis")
.is_none());
}
#[test]
fn nearer_grid_entry_wins_over_a_lower_cache_entry() {
let _init_guard = zakura_test::init();
let db = ephemeral_db();
let frontiers = DerivedFrontiers::empty();
seed_roots(&db, HIGH, &frontiers);
let cache = cache_with(artifact_at(HIGH, &frontiers), LOW, frontiers);
let derivation = derive_historical_frontiers_measured(&db, &cache, HIGH, 0)
.expect("a grid entry at the target is a zero-replay hit");
assert_eq!(derivation.replayed_blocks, 0);
}
#[test]
fn grid_entries_below_the_vct_upgrade_height_are_not_anchors() {
let _init_guard = zakura_test::init();
let db = ephemeral_db();
let cached_height = Height(500);
let published_height = Height(1_500);
let upgrade = Height(2_000);
let frontiers = DerivedFrontiers::empty();
let mut batch = DiskWriteBatch::new();
batch.update_vct_upgrade_marker(&db, upgrade);
db.write_batch(batch)
.expect("seeding the VCT upgrade marker succeeds");
seed_roots(&db, published_height, &frontiers);
let cache = cache_with(
artifact_at(published_height, &frontiers),
cached_height,
frontiers,
);
let (anchor_height, _) = anchor_for(&db, &cache, upgrade)
.expect("the cached anchor is available")
.unwrap();
assert_eq!(
anchor_height, cached_height,
"a published entry below U must not displace an eligible fallback"
);
}
#[test]
fn nearer_cache_entry_wins_over_a_lower_grid_entry() {
let _init_guard = zakura_test::init();
let db = ephemeral_db();
let frontiers = DerivedFrontiers::empty();
seed_roots(&db, LOW, &frontiers);
seed_roots(&db, HIGH, &frontiers);
let cache = cache_with(artifact_at(LOW, &frontiers), HIGH, frontiers);
let derivation = derive_historical_frontiers_measured(&db, &cache, HIGH, 0)
.expect("a cached target is a zero-replay hit");
assert_eq!(derivation.replayed_blocks, 0);
}
#[test]
fn rejected_nearer_grid_entry_falls_back_to_the_cache() {
let _init_guard = zakura_test::init();
let db = ephemeral_db();
let frontiers = DerivedFrontiers::empty();
seed_roots(&db, HIGH, &frontiers);
let cache = cache_with(artifact_at(HIGH, &mismatched_frontiers()), LOW, frontiers);
let error = derive_historical_frontiers_measured(&db, &cache, HIGH, 0)
.expect_err("replay from the low cache entry exceeds a zero-block bound");
assert_eq!(
error,
HistoricalTreeDerivationError::ReplayTooLong {
height: HIGH,
blocks: u64::from(HIGH.0 - LOW.0),
limit: 0,
}
);
}
#[test]
fn rejected_nearer_grid_entry_falls_back_to_the_previous_grid_entry() {
let _init_guard = zakura_test::init();
let db = ephemeral_db();
let frontiers = DerivedFrontiers::empty();
let mismatched = mismatched_frontiers();
seed_roots(&db, MID, &frontiers);
seed_roots(&db, HIGH, &frontiers);
let cache = Mutex::new(HistoricalTreeCache::with_artifact(artifact_entries(
Height(HIGH.0 + 1),
&[(MID, &frontiers), (HIGH, &mismatched)],
)));
let error = derive_historical_frontiers_measured(&db, &cache, HIGH, 0)
.expect_err("replay from the previous grid entry exceeds a zero-block bound");
assert_eq!(
error,
HistoricalTreeDerivationError::ReplayTooLong {
height: HIGH,
blocks: u64::from(HIGH.0 - MID.0),
limit: 0,
}
);
}
#[test]
fn a_wholly_unverifiable_grid_cannot_replay_past_the_serving_bound() {
let _init_guard = zakura_test::init();
let db = ephemeral_db();
let mismatched = mismatched_frontiers();
seed_roots(&db, HIGH, &DerivedFrontiers::empty());
let cache = Mutex::new(HistoricalTreeCache::with_artifact(artifact_entries(
Height(HIGH.0 + 1),
&[(MID, &mismatched), (HIGH, &mismatched)],
)));
let error = derive_historical_frontiers_measured(
&db,
&cache,
HIGH,
MAX_HISTORICAL_TREE_REPLAY_BLOCKS,
)
.expect_err("a genesis fall-through must not be served");
assert_eq!(
error,
HistoricalTreeDerivationError::ReplayTooLong {
height: HIGH,
blocks: u64::from(HIGH.0) + 1,
limit: MAX_HISTORICAL_TREE_REPLAY_BLOCKS,
},
"the serving bound is what stops a fall-through to genesis"
);
}
}