#[cfg(test)]
use std::collections::HashMap;
use std::{
fmt,
sync::{
atomic::{AtomicU64, Ordering},
Arc,
},
};
use thiserror::Error;
use zakura_chain::{block, ironwood, orchard, sapling, sprout};
#[cfg(test)]
use zakura_chain::block::merkle::AuthDataRoot;
use super::{FromDisk, IntoDisk, ZakuraDb};
pub(super) use zakura_chain::parallel::commitment_aux::BlockCommitmentRoots;
#[derive(Clone, Debug)]
pub(super) struct FinalFrontiers {
pub(super) height: block::Height,
pub(super) sapling: Arc<sapling::tree::NoteCommitmentTree>,
pub(super) orchard: Arc<orchard::tree::NoteCommitmentTree>,
pub(super) sprout: Arc<sprout::tree::NoteCommitmentTree>,
pub(super) ironwood: Arc<ironwood::tree::NoteCommitmentTree>,
}
#[derive(Clone, Debug, Eq, Error, PartialEq)]
pub enum FinalFrontiersGenerationError {
#[error("missing Sapling final frontier tree at height {height:?}")]
MissingSaplingTree {
height: block::Height,
},
#[error("missing Orchard final frontier tree at height {height:?}")]
MissingOrchardTree {
height: block::Height,
},
#[error("missing Ironwood final frontier tree at height {height:?}")]
MissingIronwoodTree {
height: block::Height,
},
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(super) enum FinalFrontiersParseError {
MissingHeight {
actual_len: usize,
},
MissingLength {
tree: &'static str,
offset: usize,
remaining: usize,
},
TruncatedBlob {
tree: &'static str,
offset: usize,
expected_len: usize,
remaining: usize,
},
LengthOverflow {
tree: &'static str,
offset: usize,
len: usize,
},
TrailingBytes {
offset: usize,
trailing_len: usize,
},
}
impl fmt::Display for FinalFrontiersParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FinalFrontiersParseError::MissingHeight { actual_len } => write!(
f,
"missing final frontier height: expected 4 bytes, got {actual_len}"
),
FinalFrontiersParseError::MissingLength {
tree,
offset,
remaining,
} => write!(
f,
"missing {tree} frontier length prefix at byte {offset}: expected 4 bytes, got {remaining}"
),
FinalFrontiersParseError::TruncatedBlob {
tree,
offset,
expected_len,
remaining,
} => write!(
f,
"truncated {tree} frontier blob at byte {offset}: length prefix says {expected_len} bytes, but only {remaining} remain"
),
FinalFrontiersParseError::LengthOverflow { tree, offset, len } => write!(
f,
"{tree} frontier blob length overflows at byte {offset}: {len} bytes"
),
FinalFrontiersParseError::TrailingBytes {
offset,
trailing_len,
} => write!(
f,
"unexpected trailing final frontier bytes at byte {offset}: {trailing_len} bytes"
),
}
}
}
impl std::error::Error for FinalFrontiersParseError {}
impl FinalFrontiers {
pub(super) fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&self.height.0.to_le_bytes());
let blobs: [Vec<u8>; 4] = [
IntoDisk::as_bytes(&*self.sapling),
IntoDisk::as_bytes(&*self.orchard),
IntoDisk::as_bytes(&*self.sprout),
IntoDisk::as_bytes(&*self.ironwood),
];
for blob in blobs {
let len = u32::try_from(blob.len()).expect("note commitment tree fits in u32 bytes");
out.extend_from_slice(&len.to_le_bytes());
out.extend_from_slice(&blob);
}
out
}
pub(super) fn from_bytes(bytes: &[u8]) -> Result<Self, FinalFrontiersParseError> {
let height_bytes = bytes
.get(0..4)
.ok_or(FinalFrontiersParseError::MissingHeight {
actual_len: bytes.len(),
})?;
let height_bytes: [u8; 4] =
height_bytes
.try_into()
.map_err(|_| FinalFrontiersParseError::MissingHeight {
actual_len: bytes.len(),
})?;
let height = block::Height(u32::from_le_bytes(height_bytes));
fn read_blob(
bytes: &[u8],
cursor: usize,
tree: &'static str,
) -> Result<(Vec<u8>, usize), FinalFrontiersParseError> {
let len_end =
cursor
.checked_add(4)
.ok_or(FinalFrontiersParseError::LengthOverflow {
tree,
offset: cursor,
len: 4,
})?;
let len_bytes =
bytes
.get(cursor..len_end)
.ok_or(FinalFrontiersParseError::MissingLength {
tree,
offset: cursor,
remaining: bytes.len().saturating_sub(cursor),
})?;
let len_bytes: [u8; 4] =
len_bytes
.try_into()
.map_err(|_| FinalFrontiersParseError::MissingLength {
tree,
offset: cursor,
remaining: bytes.len().saturating_sub(cursor),
})?;
let len = u32::from_le_bytes(len_bytes) as usize;
let cursor = len_end;
let blob_end =
cursor
.checked_add(len)
.ok_or(FinalFrontiersParseError::LengthOverflow {
tree,
offset: cursor,
len,
})?;
let blob =
bytes
.get(cursor..blob_end)
.ok_or(FinalFrontiersParseError::TruncatedBlob {
tree,
offset: cursor,
expected_len: len,
remaining: bytes.len().saturating_sub(cursor),
})?;
Ok((blob.to_vec(), blob_end))
}
let (sapling, cursor) = read_blob(bytes, 4, "sapling")?;
let (orchard, cursor) = read_blob(bytes, cursor, "orchard")?;
let (sprout, cursor) = read_blob(bytes, cursor, "sprout")?;
let (ironwood, cursor) = if cursor == bytes.len() {
(None, cursor)
} else {
let (blob, cursor) = read_blob(bytes, cursor, "ironwood")?;
(Some(blob), cursor)
};
if cursor != bytes.len() {
return Err(FinalFrontiersParseError::TrailingBytes {
offset: cursor,
trailing_len: bytes.len() - cursor,
});
}
Ok(FinalFrontiers {
height,
sapling: Arc::new(<sapling::tree::NoteCommitmentTree as FromDisk>::from_bytes(
sapling,
)),
orchard: Arc::new(<orchard::tree::NoteCommitmentTree as FromDisk>::from_bytes(
orchard,
)),
sprout: Arc::new(<sprout::tree::NoteCommitmentTree as FromDisk>::from_bytes(
sprout,
)),
ironwood: Arc::new(match ironwood {
Some(ironwood) => {
<ironwood::tree::NoteCommitmentTree as FromDisk>::from_bytes(ironwood)
}
None => ironwood::tree::NoteCommitmentTree::default(),
}),
})
}
}
pub(super) trait CommitmentRootSource: std::fmt::Debug + Send + Sync {
fn vct_root(
&self,
height: block::Height,
) -> Option<(
sapling::tree::Root,
orchard::tree::Root,
ironwood::tree::Root,
)>;
fn vct_last_checkpoint_height(&self) -> block::Height {
self.final_frontiers().height
}
fn final_frontiers(&self) -> &FinalFrontiers;
fn invalidate(&self, _height: block::Height) {}
}
#[cfg(test)]
#[derive(Debug)]
pub(super) struct FixtureSource {
roots: HashMap<
u32,
(
sapling::tree::Root,
orchard::tree::Root,
ironwood::tree::Root,
),
>,
frontiers: FinalFrontiers,
}
#[cfg(test)]
impl FixtureSource {
pub(super) fn new(
roots: HashMap<
u32,
(
sapling::tree::Root,
orchard::tree::Root,
ironwood::tree::Root,
),
>,
frontiers: FinalFrontiers,
) -> Self {
FixtureSource { roots, frontiers }
}
}
#[cfg(test)]
impl CommitmentRootSource for FixtureSource {
fn vct_root(
&self,
height: block::Height,
) -> Option<(
sapling::tree::Root,
orchard::tree::Root,
ironwood::tree::Root,
)> {
self.roots.get(&height.0).copied()
}
fn final_frontiers(&self) -> &FinalFrontiers {
&self.frontiers
}
}
#[derive(Debug)]
pub(super) struct PeerSource {
db: ZakuraDb,
frontiers: FinalFrontiers,
}
impl PeerSource {
pub(super) fn new(db: ZakuraDb, frontiers: FinalFrontiers) -> Self {
PeerSource { db, frontiers }
}
}
impl CommitmentRootSource for PeerSource {
fn vct_root(
&self,
height: block::Height,
) -> Option<(
sapling::tree::Root,
orchard::tree::Root,
ironwood::tree::Root,
)> {
self.db
.zakura_header_commitment_roots_by_height_range(height..=height)
.into_iter()
.next()
.map(|roots| (roots.sapling_root, roots.orchard_root, roots.ironwood_root))
}
fn final_frontiers(&self) -> &FinalFrontiers {
&self.frontiers
}
fn invalidate(&self, height: block::Height) {
if let Err(error) = self.db.delete_zakura_header_commitment_roots([height]) {
tracing::debug!(?error, ?height, "failed to delete rejected VCT root");
}
}
}
fn ironwood_root_or_empty(
tree: Option<Arc<zakura_chain::ironwood::tree::NoteCommitmentTree>>,
) -> ironwood::tree::Root {
tree.map(|tree| tree.root())
.unwrap_or_else(|| ironwood::tree::NoteCommitmentTree::default().root())
}
pub(crate) fn produce_block_roots(
db: &ZakuraDb,
range: std::ops::RangeInclusive<block::Height>,
) -> Vec<BlockCommitmentRoots> {
let (start, end) = (range.start().0, range.end().0);
let mut roots = Vec::new();
for h in start..=end {
let height = block::Height(h);
let (Some(sapling), Some(orchard)) = (
db.sapling_tree_by_height(&height),
db.orchard_tree_by_height(&height),
) else {
break;
};
let ironwood_root = ironwood_root_or_empty(db.ironwood_tree_by_height(&height));
let Some(block) = db.block(height.into()) else {
metrics::counter!("state.block_roots.missing_body").increment(1);
static MISSING_BODIES: AtomicU64 = AtomicU64::new(0);
let occurrences = MISSING_BODIES.fetch_add(1, Ordering::Relaxed) + 1;
if occurrences.is_power_of_two() {
tracing::error!(
?height,
occurrences,
"stopping tree-aux root serving because the finalized block body is unavailable"
);
}
break;
};
let (sapling_tx, orchard_tx, ironwood_tx, auth_data_root) = (
block.sapling_transactions_count(),
block.orchard_transactions_count(),
block.ironwood_transactions_count(),
block.auth_data_root(),
);
roots.push(BlockCommitmentRoots {
height,
sapling_root: sapling.root(),
orchard_root: orchard.root(),
ironwood_root,
sapling_tx,
orchard_tx,
ironwood_tx,
auth_data_root,
});
}
roots
}
pub(crate) fn serve_block_roots(
db: &ZakuraDb,
range: std::ops::RangeInclusive<block::Height>,
) -> Vec<BlockCommitmentRoots> {
let Some(upgrade) = db.vct_upgrade_height() else {
return produce_block_roots(db, range);
};
let (start, end) = (*range.start(), *range.end());
if start >= upgrade {
return db.commitment_roots_by_height_range(range);
}
let trees_end = block::Height(end.0.min(upgrade.0 - 1));
let mut roots = produce_block_roots(db, start..=trees_end);
if roots.last().map(|root| root.height) == Some(trees_end) && end >= upgrade {
roots.extend(db.commitment_roots_by_height_range(upgrade..=end));
}
roots
}
pub(super) fn produce_final_frontiers(
db: &ZakuraDb,
height: block::Height,
) -> Result<FinalFrontiers, FinalFrontiersGenerationError> {
let sapling = db
.sapling_tree_by_height(&height)
.ok_or(FinalFrontiersGenerationError::MissingSaplingTree { height })?;
let orchard = db
.orchard_tree_by_height(&height)
.ok_or(FinalFrontiersGenerationError::MissingOrchardTree { height })?;
let ironwood = db
.ironwood_tree_by_height(&height)
.ok_or(FinalFrontiersGenerationError::MissingIronwoodTree { height })?;
Ok(FinalFrontiers {
height,
sapling,
orchard,
sprout: db.sprout_tree_for_tip(),
ironwood,
})
}
pub fn produce_final_frontiers_bytes(
db: &ZakuraDb,
height: block::Height,
) -> Result<Vec<u8>, FinalFrontiersGenerationError> {
Ok(produce_final_frontiers(db, height)?.to_bytes())
}
#[cfg(test)]
mod tests {
use zakura_chain::{ironwood, parameters::Network, serialization::ZcashDeserializeInto};
use crate::{
constants::{state_database_format_version_in_code, STATE_DATABASE_KIND},
request::{CheckpointVerifiedBlock, FinalizedBlock, Treestate},
service::finalized_state::{
disk_db::WriteDisk, DiskWriteBatch, STATE_COLUMN_FAMILIES_IN_CODE,
},
Config,
};
use super::*;
fn ephemeral_mainnet_db() -> ZakuraDb {
let network = Network::Mainnet;
ZakuraDb::new(
&Config::ephemeral(),
STATE_DATABASE_KIND,
&state_database_format_version_in_code(),
&network,
true,
STATE_COLUMN_FAMILIES_IN_CODE
.iter()
.map(ToString::to_string),
false,
)
.expect("opening the finalized state database should succeed")
}
fn sapling_note_commitment(value: u64) -> sapling::tree::NoteCommitmentUpdate {
let mut bytes = [0; 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 Jubjub field elements")
}
fn sapling_tree(value: u64) -> sapling::tree::NoteCommitmentTree {
let mut tree = sapling::tree::NoteCommitmentTree::default();
tree.append(sapling_note_commitment(value))
.expect("single-note Sapling tree is not full");
tree
}
fn orchard_tree(value: u64) -> orchard::tree::NoteCommitmentTree {
let mut tree = orchard::tree::NoteCommitmentTree::default();
tree.append(halo2::pasta::pallas::Base::from(value))
.expect("single-note Orchard tree is not full");
tree
}
fn seed_trees(db: &ZakuraDb, heights: impl IntoIterator<Item = u32>) {
let mut batch = DiskWriteBatch::new();
for height in heights {
let height = block::Height(height);
batch.create_sapling_tree(db, &height, &sapling_tree(u64::from(height.0)));
batch.create_orchard_tree(db, &height, &orchard_tree(u64::from(height.0)));
batch.create_ironwood_tree(db, &height, &ironwood::tree::NoteCommitmentTree::default());
}
db.write_batch(batch).expect("seeding trees succeeds");
}
fn seed_block_bodies(db: &ZakuraDb, heights: impl IntoIterator<Item = u32>) {
let blocks = Network::Mainnet.blockchain_map();
for height in heights {
let block: Arc<block::Block> = blocks
.get(&height)
.expect("block height has test data")
.zcash_deserialize_into()
.expect("test data deserializes");
let finalized = FinalizedBlock::from_checkpoint_verified(
CheckpointVerifiedBlock::from(block),
Treestate::default(),
);
let mut batch = DiskWriteBatch::new();
batch
.prepare_block_header_and_transaction_data_batch(db, &finalized, true, None)
.expect("test block header and transactions are valid");
db.write_batch(batch).expect("seeding block body succeeds");
}
}
fn seed_sprout_tree(db: &ZakuraDb, tree: &sprout::tree::NoteCommitmentTree) {
let mut batch = DiskWriteBatch::new();
batch.update_sprout_tree(db, tree);
db.write_batch(batch).expect("seeding Sprout tree succeeds");
}
fn seed_finalized_tip(db: &ZakuraDb, height: block::Height) {
let hash_by_height = db.db().cf_handle("hash_by_height").unwrap();
let height_byte =
u8::try_from(height.0).expect("test heights fit in a byte for hash fixtures");
let mut batch = DiskWriteBatch::new();
batch.zs_insert(&hash_by_height, height, block::Hash([height_byte; 32]));
db.write_batch(batch)
.expect("seeding finalized tip succeeds");
}
fn seed_index_roots(db: &ZakuraDb, heights: impl IntoIterator<Item = u32>) {
let mut batch = DiskWriteBatch::new();
for height in heights {
let height_byte =
u8::try_from(height).expect("test heights fit in a byte for auth root fixtures");
batch.insert_commitment_roots_by_height(
db,
block::Height(height),
&sapling_tree(u64::from(height) + 100).root(),
&orchard_tree(u64::from(height) + 100).root(),
&ironwood::tree::NoteCommitmentTree::default().root(),
u64::from(height),
u64::from(height) + 1,
u64::from(height) + 2,
&AuthDataRoot::from([height_byte; 32]),
);
}
db.write_batch(batch)
.expect("seeding commitment root index succeeds");
}
fn set_upgrade_height(db: &ZakuraDb, height: block::Height) {
let mut batch = DiskWriteBatch::new();
batch.update_vct_upgrade_marker(db, height);
db.write_batch(batch)
.expect("seeding VCT upgrade marker succeeds");
}
fn expected_tree_roots(height: u32) -> BlockCommitmentRoots {
let block: Arc<block::Block> = Network::Mainnet
.blockchain_map()
.get(&height)
.expect("block height has test data")
.zcash_deserialize_into()
.expect("test data deserializes");
BlockCommitmentRoots {
height: block::Height(height),
sapling_root: sapling_tree(u64::from(height)).root(),
orchard_root: orchard_tree(u64::from(height)).root(),
ironwood_root: ironwood::tree::NoteCommitmentTree::default().root(),
sapling_tx: block.sapling_transactions_count(),
orchard_tx: block.orchard_transactions_count(),
ironwood_tx: block.ironwood_transactions_count(),
auth_data_root: block.auth_data_root(),
}
}
fn expected_index_roots(height: u32) -> BlockCommitmentRoots {
let height_byte =
u8::try_from(height).expect("test heights fit in a byte for auth root fixtures");
BlockCommitmentRoots {
height: block::Height(height),
sapling_root: sapling_tree(u64::from(height) + 100).root(),
orchard_root: orchard_tree(u64::from(height) + 100).root(),
ironwood_root: ironwood::tree::NoteCommitmentTree::default().root(),
sapling_tx: u64::from(height),
orchard_tx: u64::from(height) + 1,
ironwood_tx: u64::from(height) + 2,
auth_data_root: AuthDataRoot::from([height_byte; 32]),
}
}
#[test]
fn produce_block_roots_derives_contiguous_tree_roots() {
let _init_guard = zakura_test::init();
let db = ephemeral_mainnet_db();
seed_block_bodies(&db, [1, 2]);
seed_trees(&db, [1, 2]);
seed_finalized_tip(&db, block::Height(2));
let roots = produce_block_roots(&db, block::Height(1)..=block::Height(4));
assert_eq!(
roots,
vec![expected_tree_roots(1), expected_tree_roots(2)],
"tree-derived roots stop at the first missing height"
);
}
fn non_empty_ironwood_tree(value: u64) -> ironwood::tree::NoteCommitmentTree {
let mut tree = ironwood::tree::NoteCommitmentTree::default();
tree.append(halo2::pasta::pallas::Base::from(value))
.expect("single-note Ironwood tree is not full");
tree
}
#[test]
fn ironwood_root_or_empty_reads_tree_or_falls_back_to_empty() {
let empty_root = ironwood::tree::NoteCommitmentTree::default().root();
assert_eq!(
ironwood_root_or_empty(None),
empty_root,
"a missing per-height Ironwood tree falls back to the empty-tree root"
);
let non_empty_tree = Arc::new(non_empty_ironwood_tree(1));
let non_empty_root = non_empty_tree.root();
assert_ne!(
non_empty_root, empty_root,
"test needs a root distinct from the empty-tree root"
);
assert_eq!(
ironwood_root_or_empty(Some(non_empty_tree)),
non_empty_root,
"a present per-height Ironwood tree contributes its own root, not the empty one"
);
}
#[test]
fn produce_block_roots_reads_real_ironwood_tree_when_present() {
let _init_guard = zakura_test::init();
let db = ephemeral_mainnet_db();
seed_block_bodies(&db, [1]);
seed_trees(&db, [1]);
let non_empty_ironwood = non_empty_ironwood_tree(7);
let non_empty_root = non_empty_ironwood.root();
let mut batch = DiskWriteBatch::new();
batch.create_ironwood_tree(&db, &block::Height(1), &non_empty_ironwood);
db.write_batch(batch)
.expect("overwriting the seeded Ironwood tree succeeds");
seed_finalized_tip(&db, block::Height(1));
let roots = produce_block_roots(&db, block::Height(1)..=block::Height(1));
assert_eq!(
roots,
vec![BlockCommitmentRoots {
ironwood_root: non_empty_root,
..expected_tree_roots(1)
}],
"produce_block_roots surfaces the real per-height Ironwood root, not the empty one"
);
}
#[test]
fn serve_block_roots_without_upgrade_marker_uses_tree_fallback() {
let _init_guard = zakura_test::init();
let db = ephemeral_mainnet_db();
seed_block_bodies(&db, [1, 2]);
seed_trees(&db, [1, 2]);
seed_index_roots(&db, [1, 2]);
seed_finalized_tip(&db, block::Height(2));
let roots = serve_block_roots(&db, block::Height(1)..=block::Height(2));
assert_eq!(
roots,
vec![expected_tree_roots(1), expected_tree_roots(2)],
"pre-index archive databases derive roots from per-height trees"
);
}
#[test]
fn serve_block_roots_stitches_trees_to_index_at_upgrade() {
let _init_guard = zakura_test::init();
let db = ephemeral_mainnet_db();
seed_block_bodies(&db, [1, 2]);
seed_trees(&db, [1, 2]);
seed_index_roots(&db, [3, 4]);
seed_finalized_tip(&db, block::Height(4));
set_upgrade_height(&db, block::Height(3));
let roots = serve_block_roots(&db, block::Height(1)..=block::Height(4));
assert_eq!(
roots,
vec![
expected_tree_roots(1),
expected_tree_roots(2),
expected_index_roots(3),
expected_index_roots(4),
],
"ranges crossing U are served as one contiguous tree/index run"
);
}
#[test]
fn serve_block_roots_does_not_cross_short_tree_prefix_below_upgrade() {
let _init_guard = zakura_test::init();
let db = ephemeral_mainnet_db();
seed_block_bodies(&db, [1]);
seed_trees(&db, [1]);
seed_index_roots(&db, [3, 4]);
seed_finalized_tip(&db, block::Height(1));
set_upgrade_height(&db, block::Height(3));
let roots = serve_block_roots(&db, block::Height(1)..=block::Height(4));
assert_eq!(
roots,
vec![expected_tree_roots(1)],
"a short tree-derived prefix below U is not extended with index rows"
);
}
#[test]
fn serve_block_roots_at_or_above_upgrade_uses_index() {
let _init_guard = zakura_test::init();
let db = ephemeral_mainnet_db();
seed_trees(&db, [3, 4]);
seed_index_roots(&db, [3, 4]);
seed_finalized_tip(&db, block::Height(4));
set_upgrade_height(&db, block::Height(3));
let roots = serve_block_roots(&db, block::Height(3)..=block::Height(4));
assert_eq!(
roots,
vec![expected_index_roots(3), expected_index_roots(4)],
"requests at or above U are served from the compact index"
);
}
#[test]
fn produce_final_frontiers_reads_requested_trees_and_tip_sprout() {
let _init_guard = zakura_test::init();
let db = ephemeral_mainnet_db();
let height = block::Height(2);
let sprout = sprout::tree::NoteCommitmentTree::default();
seed_trees(&db, [2]);
seed_sprout_tree(&db, &sprout);
seed_finalized_tip(&db, height);
let frontiers =
produce_final_frontiers(&db, height).expect("seeded frontiers should be produced");
assert_eq!(frontiers.height, height);
assert_eq!(frontiers.sapling.root(), sapling_tree(2).root());
assert_eq!(frontiers.orchard.root(), orchard_tree(2).root());
assert_eq!(frontiers.sprout.root(), sprout.root());
assert_eq!(
frontiers.ironwood.root(),
ironwood::tree::NoteCommitmentTree::default().root(),
"the seeded fixture's pre-Nu6_3 Ironwood tree is empty"
);
}
#[test]
fn produce_final_frontiers_reports_missing_trees() {
let _init_guard = zakura_test::init();
let db = ephemeral_mainnet_db();
let height = block::Height(2);
assert_eq!(
produce_final_frontiers(&db, height).expect_err("Sapling absence is reported first"),
FinalFrontiersGenerationError::MissingSaplingTree { height },
);
}
#[test]
fn produce_final_frontiers_bytes_serializes_generated_frontiers() {
let _init_guard = zakura_test::init();
let db = ephemeral_mainnet_db();
let height = block::Height(2);
seed_trees(&db, [2]);
seed_sprout_tree(&db, &sprout::tree::NoteCommitmentTree::default());
seed_finalized_tip(&db, height);
let frontiers =
produce_final_frontiers(&db, height).expect("seeded frontiers should be produced");
let bytes = produce_final_frontiers_bytes(&db, height)
.expect("seeded frontiers should serialize to bytes");
assert_eq!(
bytes,
frontiers.to_bytes(),
"public byte producer serializes the generated final frontiers"
);
}
#[test]
fn final_frontiers_bytes_round_trips() {
let mut ironwood = ironwood::tree::NoteCommitmentTree::default();
ironwood
.append(halo2::pasta::pallas::Base::from(9u64))
.expect("single-note Ironwood tree is not full");
let frontiers = FinalFrontiers {
height: block::Height(1_687_200),
sapling: Arc::new(Default::default()),
orchard: Arc::new(Default::default()),
sprout: Arc::new(Default::default()),
ironwood: Arc::new(ironwood),
};
let parsed =
FinalFrontiers::from_bytes(&frontiers.to_bytes()).expect("frontiers should parse");
assert_eq!(parsed.height, frontiers.height, "height round-trips");
assert_eq!(
parsed.sapling.root(),
frontiers.sapling.root(),
"sapling frontier round-trips"
);
assert_eq!(
parsed.orchard.root(),
frontiers.orchard.root(),
"orchard frontier round-trips"
);
assert_eq!(
parsed.sprout.root(),
frontiers.sprout.root(),
"sprout frontier round-trips"
);
assert_eq!(
parsed.ironwood.root(),
frontiers.ironwood.root(),
"ironwood frontier round-trips"
);
assert_ne!(
parsed.ironwood.root(),
ironwood::tree::NoteCommitmentTree::default().root(),
"test needs a non-empty ironwood root to prove it round-trips, not just defaults"
);
}
#[test]
fn final_frontiers_bytes_parses_pre_ironwood_format_with_empty_default() {
let frontiers = FinalFrontiers {
height: block::Height(1_687_200),
sapling: Arc::new(Default::default()),
orchard: Arc::new(Default::default()),
sprout: Arc::new(Default::default()),
ironwood: Arc::new(Default::default()),
};
let four_blob_bytes = frontiers.to_bytes();
let ironwood_blob_len = 4 + IntoDisk::as_bytes(&*frontiers.ironwood).len() as u32 as usize;
let three_blob_bytes =
four_blob_bytes[..four_blob_bytes.len() - ironwood_blob_len].to_vec();
let parsed = FinalFrontiers::from_bytes(&three_blob_bytes)
.expect("pre-Ironwood 3-blob bytes should still parse");
assert_eq!(parsed.height, frontiers.height, "height round-trips");
assert_eq!(
parsed.ironwood.root(),
ironwood::tree::NoteCommitmentTree::default().root(),
"a pre-Ironwood frontier defaults to the empty Ironwood tree"
);
}
#[test]
fn final_frontiers_bytes_reject_malformed_payloads() {
assert_eq!(
FinalFrontiers::from_bytes(&[0, 0, 0]).expect_err("short height is rejected"),
FinalFrontiersParseError::MissingHeight { actual_len: 3 }
);
assert_eq!(
FinalFrontiers::from_bytes(&block::Height(1).0.to_le_bytes())
.expect_err("missing first length prefix is rejected"),
FinalFrontiersParseError::MissingLength {
tree: "sapling",
offset: 4,
remaining: 0,
}
);
let mut truncated = block::Height(1).0.to_le_bytes().to_vec();
truncated.extend_from_slice(&1u32.to_le_bytes());
assert_eq!(
FinalFrontiers::from_bytes(&truncated).expect_err("truncated first blob is rejected"),
FinalFrontiersParseError::TruncatedBlob {
tree: "sapling",
offset: 8,
expected_len: 1,
remaining: 0,
}
);
let frontiers = FinalFrontiers {
height: block::Height(1_687_200),
sapling: Arc::new(Default::default()),
orchard: Arc::new(Default::default()),
sprout: Arc::new(Default::default()),
ironwood: Arc::new(Default::default()),
};
let mut trailing = frontiers.to_bytes();
trailing.push(0);
assert_eq!(
FinalFrontiers::from_bytes(&trailing).expect_err("trailing bytes are rejected"),
FinalFrontiersParseError::TrailingBytes {
offset: frontiers.to_bytes().len(),
trailing_len: 1,
}
);
}
#[test]
fn fixture_source_round_trips_payload() {
let roots = vec![
BlockCommitmentRoots {
height: block::Height(10),
sapling_root: sapling::tree::NoteCommitmentTree::default().root(),
orchard_root: orchard::tree::NoteCommitmentTree::default().root(),
ironwood_root: zakura_chain::ironwood::tree::NoteCommitmentTree::default().root(),
sapling_tx: 0,
orchard_tx: 0,
ironwood_tx: 0,
auth_data_root: AuthDataRoot::from([0u8; 32]),
},
BlockCommitmentRoots {
height: block::Height(11),
sapling_root: sapling::tree::NoteCommitmentTree::default().root(),
orchard_root: orchard::tree::NoteCommitmentTree::default().root(),
ironwood_root: zakura_chain::ironwood::tree::NoteCommitmentTree::default().root(),
sapling_tx: 0,
orchard_tx: 0,
ironwood_tx: 0,
auth_data_root: AuthDataRoot::from([0u8; 32]),
},
];
let roots = roots
.into_iter()
.map(|root| {
(
root.height.0,
(root.sapling_root, root.orchard_root, root.ironwood_root),
)
})
.collect();
let frontiers = FinalFrontiers {
height: block::Height(11),
sapling: Arc::new(Default::default()),
orchard: Arc::new(Default::default()),
sprout: Arc::new(Default::default()),
ironwood: Arc::new(Default::default()),
};
let source = FixtureSource::new(roots, frontiers);
assert!(
source.vct_root(block::Height(10)).is_some(),
"produced root is looked up by height"
);
assert!(
source.vct_root(block::Height(99)).is_none(),
"absent height has no root"
);
assert_eq!(
source.vct_last_checkpoint_height(),
block::Height(11),
"handoff height comes from the supplied frontiers"
);
}
#[test]
fn peer_source_reads_and_invalidates_header_sync_roots() {
let db = ephemeral_mainnet_db();
db.insert_zakura_header_commitment_roots([BlockCommitmentRoots {
height: block::Height(42),
sapling_root: sapling::tree::NoteCommitmentTree::default().root(),
orchard_root: orchard::tree::NoteCommitmentTree::default().root(),
ironwood_root: zakura_chain::ironwood::tree::NoteCommitmentTree::default().root(),
sapling_tx: 0,
orchard_tx: 0,
ironwood_tx: 0,
auth_data_root: AuthDataRoot::from([0u8; 32]),
}])
.expect("writing header-sync roots to an ephemeral database succeeds");
let frontiers = FinalFrontiers {
height: block::Height(50),
sapling: Arc::new(Default::default()),
orchard: Arc::new(Default::default()),
sprout: Arc::new(Default::default()),
ironwood: Arc::new(Default::default()),
};
let source = PeerSource::new(db, frontiers);
assert!(
source.vct_root(block::Height(42)).is_some(),
"a header-sync-persisted root is read back by height"
);
assert!(
source.vct_root(block::Height(43)).is_none(),
"an absent height has no root"
);
source.invalidate(block::Height(42));
assert!(
source.vct_root(block::Height(42)).is_none(),
"an invalidated root is gone, so the next read misses and a re-fetch can replace it"
);
}
}