use std::{
collections::BTreeMap,
time::{Duration, Instant},
};
use thiserror::Error;
use zakura_chain::{
block::Height,
subtree::{NoteCommitmentSubtreeData, NoteCommitmentSubtreeIndex, TRACKED_SUBTREE_HEIGHT},
};
use crate::service::read::historical_tree::{
replay_with_subtrees, stored_frontier_before_absent_band, stored_frontiers_at,
verify_against_available_roots, DerivedFrontiers, HistoricalTreeDerivationError,
};
use super::{
commitment_aux::{
produce_settled_final_frontiers, FinalFrontiers, FinalFrontiersGenerationError,
},
treestate_artifact::{
embedded_historical_subtrees, FrontierArtifact, FrontierEntry, SubtreeArtifact,
SubtreeRecord, TreestateArtifactError,
},
ZakuraDb,
};
#[derive(Clone, Debug, Error, Eq, PartialEq)]
pub enum ReleaseTreestateArtifactsError {
#[error("the {network} network has no embedded subtree artifact to extend")]
NoEmbeddedSubtreeArtifact {
network: String,
},
#[error(
"release checkpoint {target:?} must be above embedded subtree checkpoint {embedded:?}"
)]
CheckpointNotAdvanced {
target: Height,
embedded: Height,
},
#[error(
"embedded subtree checkpoint {embedded:?} is below VCT database handoff {handoff:?}; install a newer zakura-checkpoints exporter"
)]
EmbeddedArtifactBeforeVctHandoff {
embedded: Height,
handoff: Height,
},
#[error("cannot produce the release frontier: {0}")]
Frontier(#[from] FinalFrontiersGenerationError),
#[error(
"{pool} frontier requires {found} completed subtrees, which the artifact format cannot represent"
)]
UnrepresentableSubtreeCount {
pool: &'static str,
found: u64,
},
#[error(
"{pool} frontier at {target:?} has {target_count} completed subtrees, below the embedded artifact's {embedded_count}"
)]
FrontierBeforeEmbeddedArtifact {
pool: &'static str,
target: Height,
target_count: usize,
embedded_count: usize,
},
#[error("stored {pool} subtree {index} does not match the embedded artifact")]
EmbeddedSubtreeMismatch {
pool: &'static str,
index: u16,
},
#[error(
"{pool} subtree roots are incomplete at {target:?}: expected {expected} records, found {found}"
)]
IncompleteSubtrees {
pool: &'static str,
target: Height,
expected: usize,
found: usize,
},
#[error(
"stored {pool} subtree {index} completes at {end_height:?}, but the frontier at {target:?} only contains {expected} completed subtrees"
)]
SubtreeBeyondFrontier {
pool: &'static str,
index: u16,
end_height: Height,
target: Height,
expected: usize,
},
#[error("combined subtree roots do not match the frontier at {target:?}: {source}")]
SubtreeRootsUnverified {
target: Height,
#[source]
source: TreestateArtifactError,
},
}
#[derive(Clone, Debug)]
pub struct ReleaseTreestateArtifacts {
pub last_checkpoint: Height,
pub previous_last_checkpoint: Height,
pub final_frontiers: Vec<u8>,
pub historical_subtrees: Vec<u8>,
pub added_subtree_roots: usize,
pub verified_subtree_roots: usize,
}
pub fn produce_release_treestate_artifacts(
db: &ZakuraDb,
last_checkpoint: Height,
) -> Result<ReleaseTreestateArtifacts, ReleaseTreestateArtifactsError> {
let network = db.network();
let previous = embedded_historical_subtrees(&network).ok_or_else(|| {
ReleaseTreestateArtifactsError::NoEmbeddedSubtreeArtifact {
network: network.to_string(),
}
})?;
if last_checkpoint <= previous.last_checkpoint {
return Err(ReleaseTreestateArtifactsError::CheckpointNotAdvanced {
target: last_checkpoint,
embedded: previous.last_checkpoint,
});
}
let uncovered_handoff = db
.vct_synced_below()
.filter(|handoff| previous.last_checkpoint < *handoff);
if let Some(handoff) = uncovered_handoff {
return Err(
ReleaseTreestateArtifactsError::EmbeddedArtifactBeforeVctHandoff {
embedded: previous.last_checkpoint,
handoff,
},
);
}
let frontiers = produce_settled_final_frontiers(db, last_checkpoint)?;
let final_frontiers = frontiers.to_bytes();
let previous_last_checkpoint = previous.last_checkpoint;
let previous_root_count = artifact_root_count(&previous);
let subtrees = extend_subtree_artifact(db, previous, &frontiers)?;
let verified_subtree_roots = subtrees
.verify_against_frontiers(&frontiers.sapling, &frontiers.orchard, &frontiers.ironwood)
.map(|counts| counts.total())
.map_err(
|source| ReleaseTreestateArtifactsError::SubtreeRootsUnverified {
target: last_checkpoint,
source,
},
)?;
let added_subtree_roots = verified_subtree_roots
.checked_sub(previous_root_count)
.expect("the extended artifact contains the complete embedded prefix");
let historical_subtrees = subtrees.encode(&network);
Ok(ReleaseTreestateArtifacts {
last_checkpoint,
previous_last_checkpoint,
final_frontiers,
historical_subtrees,
added_subtree_roots,
verified_subtree_roots,
})
}
fn extend_subtree_artifact(
db: &ZakuraDb,
previous: SubtreeArtifact,
frontiers: &FinalFrontiers,
) -> Result<SubtreeArtifact, ReleaseTreestateArtifactsError> {
Ok(SubtreeArtifact {
last_checkpoint: frontiers.height,
sapling: extend_pool(
"sapling",
frontiers.height,
frontiers.sapling.count(),
previous.sapling,
db.sapling_subtree_list_by_index_range(..),
|root| root.to_bytes(),
)?,
orchard: extend_pool(
"orchard",
frontiers.height,
frontiers.orchard.count(),
previous.orchard,
db.orchard_subtree_list_by_index_range(..),
|root| root.to_repr(),
)?,
ironwood: extend_pool(
"ironwood",
frontiers.height,
frontiers.ironwood.count(),
previous.ironwood,
db.ironwood_subtree_list_by_index_range(..),
|root| root.to_repr(),
)?,
})
}
fn extend_pool<Node>(
pool: &'static str,
target: Height,
leaf_count: u64,
mut records: Vec<SubtreeRecord>,
stored: BTreeMap<NoteCommitmentSubtreeIndex, NoteCommitmentSubtreeData<Node>>,
root_bytes: impl Fn(&Node) -> [u8; 32],
) -> Result<Vec<SubtreeRecord>, ReleaseTreestateArtifactsError> {
let completed_subtrees = leaf_count >> TRACKED_SUBTREE_HEIGHT;
if completed_subtrees > u64::from(u16::MAX) + 1 {
return Err(
ReleaseTreestateArtifactsError::UnrepresentableSubtreeCount {
pool,
found: completed_subtrees,
},
);
}
let expected = usize::try_from(completed_subtrees).map_err(|_| {
ReleaseTreestateArtifactsError::UnrepresentableSubtreeCount {
pool,
found: completed_subtrees,
}
})?;
if records.len() > expected {
return Err(
ReleaseTreestateArtifactsError::FrontierBeforeEmbeddedArtifact {
pool,
target,
target_count: expected,
embedded_count: records.len(),
},
);
}
for (index, data) in stored {
if data.end_height > target {
continue;
}
let ordinal = usize::from(index.0);
if ordinal >= expected {
return Err(ReleaseTreestateArtifactsError::SubtreeBeyondFrontier {
pool,
index: index.0,
end_height: data.end_height,
target,
expected,
});
}
let stored_record = SubtreeRecord {
index,
end_height: data.end_height,
root: root_bytes(&data.root),
};
if let Some(embedded_record) = records.get(ordinal) {
if *embedded_record != stored_record {
return Err(ReleaseTreestateArtifactsError::EmbeddedSubtreeMismatch {
pool,
index: index.0,
});
}
} else if ordinal == records.len() {
records.push(stored_record);
} else {
return Err(ReleaseTreestateArtifactsError::IncompleteSubtrees {
pool,
target,
expected,
found: records.len(),
});
}
}
if records.len() != expected {
return Err(ReleaseTreestateArtifactsError::IncompleteSubtrees {
pool,
target,
expected,
found: records.len(),
});
}
Ok(records)
}
fn artifact_root_count(artifact: &SubtreeArtifact) -> usize {
artifact.sapling.len() + artifact.orchard.len() + artifact.ironwood.len()
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use zakura_chain::{
ironwood, orchard, parameters::Network, sapling, sprout, subtree::NoteCommitmentSubtree,
};
use crate::{
config::Config,
service::finalized_state::{
DiskWriteBatch, STATE_COLUMN_FAMILIES_IN_CODE, STATE_DATABASE_KIND,
},
state_database_format_version_in_code,
};
use super::*;
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 sapling_note_commitment(value: u64) -> sapling::tree::NoteCommitmentUpdate {
let mut bytes = [0u8; 32];
bytes[..8].copy_from_slice(&value.to_le_bytes());
Option::<sapling::tree::NoteCommitmentUpdate>::from(
sapling::tree::NoteCommitmentUpdate::from_bytes(&bytes),
)
.expect("small little-endian integers are canonical")
}
fn sapling_tree_with_completed_subtrees(count: u64) -> sapling::tree::NoteCommitmentTree {
let mut tree = sapling::tree::NoteCommitmentTree::default();
let leaves = count << TRACKED_SUBTREE_HEIGHT;
const BATCH: u64 = 4096;
let mut value = 0u64;
while value < leaves {
let end = (value + BATCH).min(leaves);
let batch: Vec<_> = (value..end).map(sapling_note_commitment).collect();
tree.append_batch(&batch).expect("test tree is not full");
value = end;
}
tree
}
#[test]
fn embedded_prefix_and_later_database_rows_form_one_verified_artifact() {
let _init_guard = zakura_test::init();
let db = ephemeral_db();
let first_tree = sapling_tree_with_completed_subtrees(1);
let target_tree = sapling_tree_with_completed_subtrees(2);
let (_, first_root) = first_tree
.completed_subtree_index_and_root()
.expect("the first tree completes one subtree");
let (second_index, second_root) = target_tree
.completed_subtree_index_and_root()
.expect("the target tree completes its second subtree");
let mut batch = DiskWriteBatch::new();
batch.insert_sapling_subtree(
&db,
&NoteCommitmentSubtree::new(second_index, Height(19), second_root),
);
db.write_batch(batch)
.expect("writing the later row succeeds");
let previous = SubtreeArtifact {
last_checkpoint: Height(10),
sapling: vec![SubtreeRecord {
index: NoteCommitmentSubtreeIndex(0),
end_height: Height(7),
root: first_root.to_bytes(),
}],
orchard: Vec::new(),
ironwood: Vec::new(),
};
let frontiers = FinalFrontiers {
height: Height(20),
sapling: Arc::new(target_tree),
orchard: Arc::new(orchard::tree::NoteCommitmentTree::default()),
sprout: Arc::new(sprout::tree::NoteCommitmentTree::default()),
ironwood: Arc::new(ironwood::tree::NoteCommitmentTree::default()),
};
let extended = extend_subtree_artifact(&db, previous, &frontiers)
.expect("the embedded prefix and later row are complete");
let verified = extended
.verify_against_frontiers(&frontiers.sapling, &frontiers.orchard, &frontiers.ironwood)
.expect("the combined roots match the target frontier");
assert_eq!(extended.sapling.len(), 2);
assert_eq!(extended.sapling[0].root, first_root.to_bytes());
assert_eq!(extended.sapling[1].root, second_root.to_bytes());
assert_eq!(verified.sapling, 2);
}
#[test]
fn exporter_rejects_a_vct_database_with_a_newer_handoff() {
let _init_guard = zakura_test::init();
let db = ephemeral_db();
let embedded = embedded_historical_subtrees(&Network::Mainnet)
.expect("Mainnet has an embedded subtree artifact")
.last_checkpoint;
let handoff = Height(embedded.0 + 1);
let target = Height(handoff.0 + 1);
let mut batch = DiskWriteBatch::new();
batch.update_vct_upgrade_marker(&db, Height(0));
batch.update_vct_sync_marker(&db, handoff);
db.write_batch(batch)
.expect("writing the VCT handoff succeeds");
let error = produce_release_treestate_artifacts(&db, target)
.expect_err("an older exporter cannot cover the VCT handoff");
assert_eq!(
error,
ReleaseTreestateArtifactsError::EmbeddedArtifactBeforeVctHandoff { embedded, handoff }
);
}
#[test]
fn matching_overlap_is_allowed_but_rewriting_the_prefix_is_rejected() {
let root = sapling_crypto::Node::from_bytes([7; 32]).unwrap();
let embedded = SubtreeRecord {
index: NoteCommitmentSubtreeIndex(0),
end_height: Height(4),
root: root.to_bytes(),
};
let mut stored = BTreeMap::new();
stored.insert(
embedded.index,
NoteCommitmentSubtreeData::new(embedded.end_height, root),
);
assert_eq!(
extend_pool(
"sapling",
Height(10),
1 << TRACKED_SUBTREE_HEIGHT,
vec![embedded],
stored.clone(),
|root| root.to_bytes(),
),
Ok(vec![embedded])
);
stored.insert(
embedded.index,
NoteCommitmentSubtreeData::new(Height(5), root),
);
assert_eq!(
extend_pool(
"sapling",
Height(10),
1 << TRACKED_SUBTREE_HEIGHT,
vec![embedded],
stored,
|root| root.to_bytes(),
),
Err(ReleaseTreestateArtifactsError::EmbeddedSubtreeMismatch {
pool: "sapling",
index: 0,
})
);
}
#[test]
fn missing_next_database_row_is_rejected() {
let root = sapling_crypto::Node::from_bytes([7; 32]).unwrap();
let embedded = SubtreeRecord {
index: NoteCommitmentSubtreeIndex(0),
end_height: Height(4),
root: root.to_bytes(),
};
assert_eq!(
extend_pool(
"sapling",
Height(10),
2 << TRACKED_SUBTREE_HEIGHT,
vec![embedded],
BTreeMap::<
NoteCommitmentSubtreeIndex,
NoteCommitmentSubtreeData<sapling_crypto::Node>,
>::new(),
|root| root.to_bytes(),
),
Err(ReleaseTreestateArtifactsError::IncompleteSubtrees {
pool: "sapling",
target: Height(10),
expected: 2,
found: 1,
})
);
}
}
const COST_PER_BLOCK_US: u64 = 249;
const COST_PER_COMMITMENT_US: u64 = 30;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GridSpacing {
Uniform {
blocks: u32,
},
Adaptive {
budget_us: u64,
},
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
pub enum FrontierGridExportError {
#[error("this database has no finalized tip to generate a frontier grid from")]
NoFinalizedTip,
#[error("cannot generate a grid for {target:?}: this database is only finalized to {tip:?}")]
TargetAboveTip {
target: Height,
tip: Height,
},
#[error("a grid at {target:?} covers no heights")]
EmptyRange {
target: Height,
},
#[error("grid spacing and cost budget must be at least 1")]
ZeroSpacing,
#[error(
"cannot resume from a grid whose last entry {seed:?} is not below the requested \
checkpoint {target:?}"
)]
ResumeAboveTarget {
seed: Height,
target: Height,
},
#[error(
"cannot place frontier grid entries: block body {missing:?} is not retained; generate \
from an archive database"
)]
UnretainedBlockBody {
missing: Height,
},
#[error("generation failed at height {height:?}: {source}")]
Derivation {
height: Height,
#[source]
source: HistoricalTreeDerivationError,
},
}
#[derive(Clone, Debug)]
pub struct FrontierGridExport {
pub frontiers: FrontierArtifact,
pub elapsed: Duration,
pub replayed_blocks: u64,
}
fn next_grid_target(
spacing: GridSpacing,
next: Height,
last: Height,
accrued_cost: &mut u64,
mut block_commitments: impl FnMut(Height) -> usize,
) -> Option<Height> {
match spacing {
GridSpacing::Uniform { blocks } => {
let target = Height(next.0.saturating_add(blocks));
(target <= last).then_some(target)
}
GridSpacing::Adaptive { budget_us } => {
let mut candidate = next;
while candidate <= last {
let counts = block_commitments(candidate);
*accrued_cost = accrued_cost
.saturating_add(COST_PER_BLOCK_US)
.saturating_add(COST_PER_COMMITMENT_US.saturating_mul(counts as u64));
if *accrued_cost >= budget_us {
*accrued_cost = 0;
return Some(candidate);
}
if candidate == last {
return None;
}
candidate = Height(candidate.0 + 1);
}
None
}
}
}
pub fn export_frontier_grid_to(
db: &ZakuraDb,
target_checkpoint: Height,
spacing: GridSpacing,
resume_from: Option<&FrontierArtifact>,
mut on_progress: impl FnMut(Height, u64),
) -> Result<FrontierGridExport, FrontierGridExportError> {
match spacing {
GridSpacing::Uniform { blocks: 0 } | GridSpacing::Adaptive { budget_us: 0 } => {
return Err(FrontierGridExportError::ZeroSpacing)
}
_ => {}
}
let tip = db
.finalized_tip_height()
.ok_or(FrontierGridExportError::NoFinalizedTip)?;
if target_checkpoint > tip {
return Err(FrontierGridExportError::TargetAboveTip {
target: target_checkpoint,
tip,
});
}
let last = target_checkpoint.0.checked_sub(1).map(Height).ok_or(
FrontierGridExportError::EmptyRange {
target: target_checkpoint,
},
)?;
let absent_band = db
.vct_synced_below()
.map(|handoff| (db.vct_upgrade_height().unwrap_or(Height(0)), handoff));
let start = Instant::now();
let mut entries: Vec<FrontierEntry> = Vec::new();
let mut replayed_blocks = 0u64;
let mut replay: Option<(DerivedFrontiers, u32)> = None;
let mut next = Height(0);
let mut accrued_cost: u64 = 0;
if let Some(seed) = resume_from {
if let Some(top) = seed.entries.last() {
if top.height > last {
return Err(FrontierGridExportError::ResumeAboveTarget {
seed: top.height,
target: target_checkpoint,
});
}
}
for entry in &seed.entries {
let frontiers = DerivedFrontiers {
sapling: entry.sapling.clone(),
orchard: entry.orchard.clone(),
ironwood: entry.ironwood.clone(),
};
verify_against_available_roots(db, entry.height, &frontiers).map_err(|source| {
FrontierGridExportError::Derivation {
height: entry.height,
source,
}
})?;
if absent_band
.is_some_and(|(upgrade, handoff)| entry.height >= upgrade && entry.height < handoff)
{
replay = Some((frontiers, entry.height.0 + 1));
}
entries.push(entry.clone());
}
if let Some(top) = entries.last() {
next = Height(top.height.0 + 1);
}
}
let mut unretained: Option<Height> = None;
while let Some(target) = next_grid_target(spacing, next, last, &mut accrued_cost, |height| {
match db.block(height.into()) {
Some(block) => {
block.sapling_note_commitments().count()
+ block.orchard_note_commitments().count()
+ block.ironwood_note_commitments().count()
}
None => {
unretained.get_or_insert(height);
0
}
}
}) {
if let Some(missing) = unretained {
return Err(FrontierGridExportError::UnretainedBlockBody { missing });
}
let in_absent_band =
absent_band.is_some_and(|(upgrade, handoff)| target >= upgrade && target < handoff);
let frontiers = if in_absent_band {
let (carried, replay_from) = match replay.take() {
Some(cursor) => cursor,
None => {
let upgrade = absent_band
.expect("a height inside the band implies the band exists")
.0;
match stored_frontier_before_absent_band(db, upgrade).map_err(|source| {
FrontierGridExportError::Derivation {
height: upgrade,
source,
}
})? {
Some((anchor, frontiers)) => (frontiers, anchor.0 + 1),
None => (DerivedFrontiers::empty(), 0),
}
}
};
let derived = replay_with_subtrees(db, target, replay_from, carried, |_, _| {})
.map_err(|source| FrontierGridExportError::Derivation {
height: target,
source,
})?;
replayed_blocks += u64::from(target.0 - replay_from) + 1;
derived
} else {
stored_frontiers_at(db, target).map_err(|source| {
FrontierGridExportError::Derivation {
height: target,
source,
}
})?
};
verify_against_available_roots(db, target, &frontiers).map_err(|source| {
FrontierGridExportError::Derivation {
height: target,
source,
}
})?;
entries.push(FrontierEntry {
height: target,
sapling: frontiers.sapling.clone(),
orchard: frontiers.orchard.clone(),
ironwood: frontiers.ironwood.clone(),
});
on_progress(target, replayed_blocks);
if in_absent_band {
replay = Some((frontiers, target.0 + 1));
}
if target == last {
break;
}
next = Height(target.0 + 1);
}
if let Some(missing) = unretained {
return Err(FrontierGridExportError::UnretainedBlockBody { missing });
}
Ok(FrontierGridExport {
frontiers: FrontierArtifact {
spacing: match spacing {
GridSpacing::Uniform { blocks } => blocks,
GridSpacing::Adaptive { .. } => 0,
},
last_checkpoint: target_checkpoint,
entries,
},
elapsed: start.elapsed(),
replayed_blocks,
})
}
#[cfg(test)]
mod grid_target_tests {
use super::*;
fn published_grid_heights(
spacing: GridSpacing,
start: Height,
last: Height,
mut commitments: impl FnMut(Height) -> usize,
) -> Vec<u32> {
let mut heights = Vec::new();
let mut next = start;
let mut accrued_cost = 0;
while let Some(target) =
next_grid_target(spacing, next, last, &mut accrued_cost, &mut commitments)
{
heights.push(target.0);
if target == last {
break;
}
next = Height(target.0 + 1);
}
heights
}
#[test]
fn uniform_grid_omits_a_partial_final_cell() {
let spacing = GridSpacing::Uniform { blocks: 10 };
assert_eq!(
published_grid_heights(spacing, Height(0), Height(25), |_| 0),
vec![10, 21],
"the cell that would clamp to 25 is not published"
);
assert_eq!(
published_grid_heights(spacing, Height(0), Height(10), |_| 0),
vec![10],
"a target that lands exactly on last is on-grid and is published"
);
assert!(
published_grid_heights(spacing, Height(0), Height(9), |_| 0).is_empty(),
"a band shorter than one step publishes nothing rather than a clamped entry"
);
}
#[test]
fn uniform_grid_heights_are_prefix_compatible_across_tips() {
let spacing = GridSpacing::Uniform { blocks: 10 };
let earlier = published_grid_heights(spacing, Height(0), Height(25), |_| 0);
let later = published_grid_heights(spacing, Height(0), Height(35), |_| 0);
assert_eq!(&later[..earlier.len()], earlier.as_slice());
assert_eq!(later, vec![10, 21, 32]);
assert!(
!later.contains(&25),
"the height a clamp would have published at the earlier tip is not a later grid point"
);
}
#[test]
fn adaptive_grid_omits_a_partial_final_cell() {
let spacing = GridSpacing::Adaptive { budget_us: 700 };
assert_eq!(
published_grid_heights(spacing, Height(0), Height(10), |_| 0),
vec![2, 5, 8],
"the incomplete cell ending at last is omitted"
);
assert_eq!(
published_grid_heights(spacing, Height(0), Height(11), |_| 0),
vec![2, 5, 8, 11],
"last is published when it is a real budget boundary"
);
}
#[test]
fn adaptive_grid_heights_are_prefix_compatible_across_tips() {
let spacing = GridSpacing::Adaptive { budget_us: 700 };
let earlier = published_grid_heights(spacing, Height(0), Height(10), |_| 0);
let later = published_grid_heights(spacing, Height(0), Height(11), |_| 0);
assert_eq!(&later[..earlier.len()], earlier.as_slice());
assert_eq!(later, vec![2, 5, 8, 11]);
assert!(
!later.contains(&10),
"the height a clamp would have published at the earlier tip is not a later grid point"
);
}
}