use std::{collections::BTreeMap, sync::Arc};
use zakura_chain::{
block, ironwood, orchard,
parameters::NetworkUpgrade,
sapling,
subtree::{NoteCommitmentSubtreeData, NoteCommitmentSubtreeIndex, TRACKED_SUBTREE_HEIGHT},
};
use crate::{
error::{
HistoricalSubtreeUnavailable, HistoricalSubtreeUnavailableReason, HistoricalTreeUnavailable,
},
service::{
finalized_state::{embedded_last_checkpoint_leaf_counts, ZakuraDb},
non_finalized_state::Chain,
},
HashOrHeight,
};
#[allow(unused_imports)]
use zakura_chain::subtree::NoteCommitmentSubtree;
fn resolve_historical_tree<Tree: Default>(
db: &ZakuraDb,
hash_or_height: HashOrHeight,
pool_activation: NetworkUpgrade,
tree: Option<Tree>,
) -> Result<Option<Tree>, HistoricalTreeUnavailable> {
if tree.is_some() {
return Ok(tree);
}
let Some(height) = (match hash_or_height {
HashOrHeight::Hash(hash) => db.height(hash),
HashOrHeight::Height(height) => db.contains_height(height).then_some(height),
}) else {
return Ok(None);
};
if pool_activation
.activation_height(&db.network())
.is_none_or(|activation| height < activation)
{
return Ok(Some(Default::default()));
}
match db.vct_synced_below() {
Some(last_checkpoint) if db.vct_tree_absent(height) => Err(HistoricalTreeUnavailable {
hash_or_height,
last_checkpoint,
}),
_ => Ok(None),
}
}
pub fn contiguous_subtrees_from<Node>(
mut subtrees: BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<Node>>,
start_index: NoteCommitmentSubtreeIndex,
) -> BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<Node>> {
if !subtrees.contains_key(&start_index) {
return BTreeMap::new();
}
subtrees.retain(|index, _| *index >= start_index);
let mut expected = start_index.0;
let mut contiguous = BTreeMap::new();
for (index, data) in subtrees {
if index.0 != expected {
break;
}
contiguous.insert(index, data);
let Some(next) = expected.checked_add(1) else {
break;
};
expected = next;
}
contiguous
}
pub fn merge_published_subtrees<Node>(
stored: &mut BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<Node>>,
published: impl IntoIterator<Item = (NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<Node>)>,
vct_applied_below: block::Height,
) {
for (index, data) in published
.into_iter()
.filter(|(_, data)| data.end_height <= vct_applied_below)
{
stored.entry(index).or_insert(data);
}
}
pub fn retain_subtrees_completed_at_or_below<Node>(
subtrees: &mut BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<Node>>,
verified_tip: block::Height,
) {
subtrees.retain(|_, data| data.end_height <= verified_tip);
}
pub(crate) fn subtree_completed_by_last_checkpoint(
start_index: NoteCommitmentSubtreeIndex,
last_checkpoint_leaves: u64,
) -> bool {
u64::from(start_index) < (last_checkpoint_leaves >> TRACKED_SUBTREE_HEIGHT)
}
pub(crate) fn is_syncing_below_last_checkpoint(
finalized_tip: Option<block::Height>,
last_checkpoint: block::Height,
) -> bool {
finalized_tip.is_some_and(|tip| tip < last_checkpoint)
}
fn check_historical_subtree_available(
db: &ZakuraDb,
pool: &'static str,
first_missing: Option<NoteCommitmentSubtreeIndex>,
authenticated_last_checkpoint_leaves: impl FnOnce(&ZakuraDb, block::Height) -> Option<u64>,
last_checkpoint_leaves: impl FnOnce(&ZakuraDb, block::Height) -> Option<u64>,
) -> Result<(), HistoricalSubtreeUnavailable> {
let Some(first_missing) = first_missing else {
return Ok(());
};
let Some(last_checkpoint) = db.vct_synced_below() else {
return Ok(());
};
if let Some(leaves) = authenticated_last_checkpoint_leaves(db, last_checkpoint) {
return if subtree_completed_by_last_checkpoint(first_missing, leaves) {
Err(HistoricalSubtreeUnavailable {
pool,
index: first_missing,
last_checkpoint,
reason: HistoricalSubtreeUnavailableReason::NotStored,
})
} else {
Ok(())
};
}
if is_syncing_below_last_checkpoint(db.finalized_tip_height(), last_checkpoint) {
tracing::debug!(
pool,
?last_checkpoint,
"last checkpoint has not been reached; refusing to serve incomplete subtrees",
);
return Err(HistoricalSubtreeUnavailable {
pool,
index: first_missing,
last_checkpoint,
reason: HistoricalSubtreeUnavailableReason::Indeterminate,
});
}
let Some(leaves) = last_checkpoint_leaves(db, last_checkpoint) else {
tracing::error!(
pool,
?last_checkpoint,
"no note commitment tree at the last checkpoint, so completed subtrees cannot be \
distinguished from absent ones; refusing to serve",
);
metrics::counter!("state.historical_tree.last_checkpoint_tree_missing").increment(1);
return Err(HistoricalSubtreeUnavailable {
pool,
index: first_missing,
last_checkpoint,
reason: HistoricalSubtreeUnavailableReason::NotStored,
});
};
if subtree_completed_by_last_checkpoint(first_missing, leaves) {
Err(HistoricalSubtreeUnavailable {
pool,
index: first_missing,
last_checkpoint,
reason: HistoricalSubtreeUnavailableReason::NotStored,
})
} else {
Ok(())
}
}
pub(crate) fn first_missing_subtree_index<Node>(
subtrees: &BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<Node>>,
start_index: NoteCommitmentSubtreeIndex,
end_index: Option<NoteCommitmentSubtreeIndex>,
) -> Option<NoteCommitmentSubtreeIndex> {
let mut first_missing = start_index;
for index in subtrees.keys() {
if *index != first_missing {
break;
}
first_missing = NoteCommitmentSubtreeIndex(first_missing.0.checked_add(1)?);
}
match end_index {
Some(end) if first_missing >= end => None,
_ => Some(first_missing),
}
}
pub(crate) fn check_historical_sapling_subtrees_available(
db: &ZakuraDb,
start_index: NoteCommitmentSubtreeIndex,
end_index: Option<NoteCommitmentSubtreeIndex>,
subtrees: &BTreeMap<
NoteCommitmentSubtreeIndex,
NoteCommitmentSubtreeData<sapling_crypto::Node>,
>,
) -> Result<(), HistoricalSubtreeUnavailable> {
check_historical_subtree_available(
db,
"sapling",
first_missing_subtree_index(subtrees, start_index, end_index),
|db, last_checkpoint| {
embedded_last_checkpoint_leaf_counts(&db.network(), last_checkpoint)
.map(|(sapling, _, _)| sapling)
},
|db, last_checkpoint| {
db.latest_stored_sapling_tree(&last_checkpoint)
.map(|tree| tree.count())
},
)
}
pub(crate) fn check_historical_orchard_subtrees_available(
db: &ZakuraDb,
start_index: NoteCommitmentSubtreeIndex,
end_index: Option<NoteCommitmentSubtreeIndex>,
subtrees: &BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<orchard::tree::Node>>,
) -> Result<(), HistoricalSubtreeUnavailable> {
check_historical_subtree_available(
db,
"orchard",
first_missing_subtree_index(subtrees, start_index, end_index),
|db, last_checkpoint| {
embedded_last_checkpoint_leaf_counts(&db.network(), last_checkpoint)
.map(|(_, orchard, _)| orchard)
},
|db, last_checkpoint| {
db.latest_stored_orchard_tree(&last_checkpoint)
.map(|tree| tree.count())
},
)
}
pub(crate) fn check_historical_ironwood_subtrees_available(
db: &ZakuraDb,
start_index: NoteCommitmentSubtreeIndex,
end_index: Option<NoteCommitmentSubtreeIndex>,
subtrees: &BTreeMap<
NoteCommitmentSubtreeIndex,
NoteCommitmentSubtreeData<ironwood::tree::Node>,
>,
) -> Result<(), HistoricalSubtreeUnavailable> {
check_historical_subtree_available(
db,
"ironwood",
first_missing_subtree_index(subtrees, start_index, end_index),
|db, last_checkpoint| {
embedded_last_checkpoint_leaf_counts(&db.network(), last_checkpoint)
.map(|(_, _, ironwood)| ironwood)
},
|db, last_checkpoint| {
db.latest_stored_ironwood_tree(&last_checkpoint)
.map(|tree| tree.count())
},
)
}
pub fn sapling_tree<C>(
chain: Option<C>,
db: &ZakuraDb,
hash_or_height: HashOrHeight,
) -> Result<Option<Arc<sapling::tree::NoteCommitmentTree>>, HistoricalTreeUnavailable>
where
C: AsRef<Chain>,
{
let tree = chain
.and_then(|chain| chain.as_ref().sapling_tree(hash_or_height))
.or_else(|| db.sapling_tree_by_hash_or_height(hash_or_height));
resolve_historical_tree(db, hash_or_height, NetworkUpgrade::Sapling, tree)
}
pub fn sapling_subtrees<C>(
chain: Option<C>,
db: &ZakuraDb,
range: impl std::ops::RangeBounds<NoteCommitmentSubtreeIndex> + Clone,
) -> Result<
BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<sapling_crypto::Node>>,
HistoricalSubtreeUnavailable,
>
where
C: AsRef<Chain>,
{
let (start_index, end_index) = subtree_range_bounds(&range);
let subtrees = sapling_subtrees_with_gaps(chain, db, range);
let subtrees = subtrees_from_start(subtrees, start_index);
if let Some(start_index) = start_index {
check_historical_sapling_subtrees_available(db, start_index, end_index, &subtrees)?;
}
Ok(subtrees)
}
pub(crate) fn sapling_subtrees_with_gaps<C>(
chain: Option<C>,
db: &ZakuraDb,
range: impl std::ops::RangeBounds<NoteCommitmentSubtreeIndex> + Clone,
) -> BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<sapling_crypto::Node>>
where
C: AsRef<Chain>,
{
subtrees_with_gaps(
chain,
range,
|chain, range| chain.sapling_subtrees_in_range(range),
|range| db.sapling_subtree_list_by_index_range(range),
)
}
pub fn orchard_tree<C>(
chain: Option<C>,
db: &ZakuraDb,
hash_or_height: HashOrHeight,
) -> Result<Option<Arc<orchard::tree::NoteCommitmentTree>>, HistoricalTreeUnavailable>
where
C: AsRef<Chain>,
{
let tree = chain
.and_then(|chain| chain.as_ref().orchard_tree(hash_or_height))
.or_else(|| db.orchard_tree_by_hash_or_height(hash_or_height));
resolve_historical_tree(db, hash_or_height, NetworkUpgrade::Nu5, tree)
}
pub fn orchard_subtrees<C>(
chain: Option<C>,
db: &ZakuraDb,
range: impl std::ops::RangeBounds<NoteCommitmentSubtreeIndex> + Clone,
) -> Result<
BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<orchard::tree::Node>>,
HistoricalSubtreeUnavailable,
>
where
C: AsRef<Chain>,
{
let (start_index, end_index) = subtree_range_bounds(&range);
let subtrees = orchard_subtrees_with_gaps(chain, db, range);
let subtrees = subtrees_from_start(subtrees, start_index);
if let Some(start_index) = start_index {
check_historical_orchard_subtrees_available(db, start_index, end_index, &subtrees)?;
}
Ok(subtrees)
}
pub(crate) fn orchard_subtrees_with_gaps<C>(
chain: Option<C>,
db: &ZakuraDb,
range: impl std::ops::RangeBounds<NoteCommitmentSubtreeIndex> + Clone,
) -> BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<orchard::tree::Node>>
where
C: AsRef<Chain>,
{
subtrees_with_gaps(
chain,
range,
|chain, range| chain.orchard_subtrees_in_range(range),
|range| db.orchard_subtree_list_by_index_range(range),
)
}
pub fn ironwood_tree<C>(
chain: Option<C>,
db: &ZakuraDb,
hash_or_height: HashOrHeight,
) -> Result<Option<Arc<ironwood::tree::NoteCommitmentTree>>, HistoricalTreeUnavailable>
where
C: AsRef<Chain>,
{
let tree = chain
.and_then(|chain| chain.as_ref().ironwood_tree(hash_or_height))
.or_else(|| db.ironwood_tree_by_hash_or_height(hash_or_height));
resolve_historical_tree(db, hash_or_height, NetworkUpgrade::Nu6_3, tree)
}
pub fn ironwood_subtrees<C>(
chain: Option<C>,
db: &ZakuraDb,
range: impl std::ops::RangeBounds<NoteCommitmentSubtreeIndex> + Clone,
) -> Result<
BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<ironwood::tree::Node>>,
HistoricalSubtreeUnavailable,
>
where
C: AsRef<Chain>,
{
let (start_index, end_index) = subtree_range_bounds(&range);
let subtrees = ironwood_subtrees_with_gaps(chain, db, range);
let subtrees = subtrees_from_start(subtrees, start_index);
if let Some(start_index) = start_index {
check_historical_ironwood_subtrees_available(db, start_index, end_index, &subtrees)?;
}
Ok(subtrees)
}
pub(crate) fn ironwood_subtrees_with_gaps<C>(
chain: Option<C>,
db: &ZakuraDb,
range: impl std::ops::RangeBounds<NoteCommitmentSubtreeIndex> + Clone,
) -> BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<ironwood::tree::Node>>
where
C: AsRef<Chain>,
{
subtrees_with_gaps(
chain,
range,
|chain, range| chain.ironwood_subtrees_in_range(range),
|range| db.ironwood_subtree_list_by_index_range(range),
)
}
fn subtree_range_bounds(
range: &impl std::ops::RangeBounds<NoteCommitmentSubtreeIndex>,
) -> (
Option<NoteCommitmentSubtreeIndex>,
Option<NoteCommitmentSubtreeIndex>,
) {
use std::ops::Bound::*;
let start = match range.start_bound().cloned() {
Included(start) => Some(start),
Excluded(start) => start.0.checked_add(1).map(Into::into),
Unbounded => Some(0.into()),
};
let end = match range.end_bound().cloned() {
Included(end) => end.0.checked_add(1).map(Into::into),
Excluded(end) => Some(end),
Unbounded => None,
};
(start, end)
}
fn subtrees_from_start<Node>(
subtrees: BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<Node>>,
start_index: Option<NoteCommitmentSubtreeIndex>,
) -> BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<Node>> {
if start_index.is_some_and(|start_index| subtrees.contains_key(&start_index)) {
subtrees
} else {
BTreeMap::new()
}
}
fn subtrees_with_gaps<C, Range, Node, ChainSubtreeFn, DbSubtreeFn>(
chain: Option<C>,
range: Range,
read_chain: ChainSubtreeFn,
read_disk: DbSubtreeFn,
) -> BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<Node>>
where
C: AsRef<Chain>,
Node: PartialEq,
Range: std::ops::RangeBounds<NoteCommitmentSubtreeIndex> + Clone,
ChainSubtreeFn: FnOnce(
&Chain,
Range,
)
-> BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<Node>>,
DbSubtreeFn:
FnOnce(Range) -> BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<Node>>,
{
use std::ops::Bound::*;
let Some(start_index) = (match range.start_bound().cloned() {
Included(start_index) => Some(start_index),
Excluded(start_index) => start_index.0.checked_add(1).map(Into::into),
Unbounded => Some(0.into()),
}) else {
return BTreeMap::new();
};
match chain.map(|chain| read_chain(chain.as_ref(), range.clone())) {
Some(chain_results) if chain_results.contains_key(&start_index) => chain_results,
Some(chain_results) => {
let mut db_results = read_disk(range);
for (chain_index, chain_subtree) in chain_results {
let Some(db_subtree) = db_results.get(&chain_index) else {
db_results.insert(chain_index, chain_subtree);
continue;
};
if &chain_subtree != db_subtree {
break;
}
}
db_results
}
None => read_disk(range),
}
}
pub fn history_tree<C>(
chain: Option<C>,
db: &ZakuraDb,
hash_or_height: HashOrHeight,
) -> Option<Arc<zakura_chain::history_tree::HistoryTree>>
where
C: AsRef<Chain>,
{
chain
.and_then(|chain| chain.as_ref().history_tree(hash_or_height))
.or_else(|| {
let (tip_height, tip_hash) = db.tip()?;
match hash_or_height {
HashOrHeight::Height(height) if height == tip_height => Some(db.history_tree()),
HashOrHeight::Hash(hash) if hash == tip_hash => Some(db.history_tree()),
_ => None,
}
})
}