use std::sync::{
atomic::{AtomicU64, Ordering},
Arc,
};
use thiserror::Error;
#[cfg(test)]
use zakura_chain::parallel::tree::NoteCommitmentTrees;
use zakura_chain::{
block::{self, merkle::AuthDataRoot, Block, Header},
ironwood, orchard,
parameters::{Network, NetworkUpgrade},
sapling, sprout,
};
use super::{
commitment_aux::{CommitmentRootSource, FinalFrontiers, PeerSource},
ZakuraDb,
};
#[derive(Clone, Debug)]
pub struct NextVctBlock {
pub(crate) header: Arc<Header>,
pub(crate) height: block::Height,
pub(crate) hash: block::Hash,
pub(crate) auth_data_root: Option<AuthDataRoot>,
}
impl NextVctBlock {
pub(crate) fn from_block(block: Arc<Block>, auth_data_root: Option<AuthDataRoot>) -> Self {
let height = block
.coinbase_height()
.expect("checkpoint-verified successor blocks have a coinbase height");
let auth_data_root = Some(auth_data_root.unwrap_or_else(|| block.auth_data_root()));
Self {
header: block.header.clone(),
height,
hash: block.hash(),
auth_data_root,
}
}
pub(crate) fn from_header(
header: Arc<Header>,
height: block::Height,
auth_data_root: AuthDataRoot,
) -> Self {
let hash = block::Hash::from(&header);
Self {
header,
height,
hash,
auth_data_root: Some(auth_data_root),
}
}
}
const MAINNET_FINAL_FRONTIERS: &[u8] = include_bytes!("vct/mainnet-frontier.bin");
#[derive(Clone, Debug, Eq, Error, PartialEq)]
pub enum FinalFrontiersValidationError {
#[error("invalid VCT final frontier bytes: {error}")]
InvalidBytes {
error: String,
},
#[error("embedded VCT final frontier height must match the network's max checkpoint height")]
HeightMismatch {
actual: block::Height,
expected: block::Height,
},
}
#[derive(Debug)]
pub(crate) struct VctState {
enabled: bool,
source: Box<dyn CommitmentRootSource>,
requires_verified_successor: bool,
vct_count: AtomicU64,
prevalidated_count: AtomicU64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SourceMode {
Legacy,
Peer,
}
fn select_source_mode(
checkpoint_sync: bool,
vct_fast_sync: bool,
has_embedded_frontiers: bool,
) -> SourceMode {
if !checkpoint_sync || !vct_fast_sync || !has_embedded_frontiers {
SourceMode::Legacy
} else {
SourceMode::Peer
}
}
impl VctState {
pub(super) fn from_config(
checkpoint_sync: bool,
vct_fast_sync: bool,
network: &Network,
db: ZakuraDb,
) -> Option<Arc<Self>> {
let embedded = embedded_final_frontiers(network);
match select_source_mode(checkpoint_sync, vct_fast_sync, embedded.is_some()) {
SourceMode::Peer => {
let parsed = embedded?;
tracing::info!(
handoff_height = parsed.height.0,
"VCT: peer (tree_aux) source enabled by default — roots fetched from peers"
);
let source = PeerSource::new(db, parsed);
Some(Arc::new(VctState {
enabled: true,
source: Box::new(source),
requires_verified_successor: true,
vct_count: AtomicU64::new(0),
prevalidated_count: AtomicU64::new(0),
}))
}
SourceMode::Legacy => None,
}
}
pub(super) fn is_enabled(&self) -> bool {
self.enabled
}
pub(super) fn vct_roots_at_height(
&self,
height: block::Height,
) -> Option<(
sapling::tree::Root,
orchard::tree::Root,
ironwood::tree::Root,
)> {
if !self.enabled {
return None;
}
if height > self.source.vct_last_checkpoint_height() {
return None;
}
self.source.vct_root(height)
}
pub(super) fn vct_root_needs_successor(
&self,
height: block::Height,
network: &Network,
) -> bool {
self.enabled
&& self.vct_roots_at_height(height).is_some()
&& self.requires_verified_successor
&& self.source.final_frontiers().height != height
&& Some(height) >= NetworkUpgrade::Heartwood.activation_height(network)
}
pub(super) fn invalidate_fast_root(&self, height: block::Height) {
self.source.invalidate(height);
}
pub(super) fn vct_sync_last_checkpoint_height(&self) -> block::Height {
self.source.vct_last_checkpoint_height()
}
#[allow(clippy::type_complexity)]
pub(super) fn final_frontiers_for_last_checkpoint(
&self,
height: block::Height,
) -> Option<(
Arc<sapling::tree::NoteCommitmentTree>,
Arc<orchard::tree::NoteCommitmentTree>,
Arc<sprout::tree::NoteCommitmentTree>,
Arc<ironwood::tree::NoteCommitmentTree>,
)> {
let frontiers = self.source.final_frontiers();
(frontiers.height == height).then(|| {
(
frontiers.sapling.clone(),
frontiers.orchard.clone(),
frontiers.sprout.clone(),
frontiers.ironwood.clone(),
)
})
}
pub(super) fn record_fast_block(&self) {
self.vct_count.fetch_add(1, Ordering::Relaxed);
}
pub(super) fn record_prevalidated(&self) {
self.prevalidated_count.fetch_add(1, Ordering::Relaxed);
}
pub(super) fn vct_count(&self) -> u64 {
self.vct_count.load(Ordering::Relaxed)
}
#[cfg(test)]
pub(super) fn prevalidated_count(&self) -> u64 {
self.prevalidated_count.load(Ordering::Relaxed)
}
#[cfg(test)]
pub(super) fn test_with_source(
source: Box<dyn CommitmentRootSource>,
requires_verified_successor: bool,
) -> Arc<Self> {
Arc::new(VctState {
enabled: true,
source,
requires_verified_successor,
vct_count: AtomicU64::new(0),
prevalidated_count: AtomicU64::new(0),
})
}
}
#[derive(Clone, Debug)]
pub(crate) struct VctCommitState {
source: Option<Arc<VctState>>,
prevalidated_next: Option<(block::Height, block::Hash)>,
is_vct_sync_below_last_checkpoint: bool,
}
impl VctCommitState {
pub(super) fn new(
source: Option<Arc<VctState>>,
is_vct_sync_below_last_checkpoint: bool,
) -> Self {
VctCommitState {
source,
prevalidated_next: None,
is_vct_sync_below_last_checkpoint,
}
}
pub(super) fn source(&self) -> Option<&Arc<VctState>> {
self.source.as_ref()
}
pub(super) fn is_below_last_checkpoint(&self) -> bool {
self.is_vct_sync_below_last_checkpoint
}
pub(super) fn prevalidated_next(&self) -> Option<(block::Height, block::Hash)> {
self.prevalidated_next
}
pub(super) fn mark_prevalidated(&mut self, height: block::Height, hash: block::Hash) {
self.prevalidated_next = Some((height, hash));
}
pub(super) fn clear_prevalidated_next(&mut self) {
self.prevalidated_next = None;
}
#[cfg(test)]
pub(super) fn set_prevalidated_next(&mut self, next: Option<(block::Height, block::Hash)>) {
self.prevalidated_next = next;
}
pub(super) fn start_vct_sync_below_last_checkpoint(&mut self) {
self.is_vct_sync_below_last_checkpoint = true;
}
pub(super) fn stop_vct_sync_at_last_checkpoint(&mut self) {
self.is_vct_sync_below_last_checkpoint = false;
}
#[cfg(test)]
pub(super) fn install_test_source(
&mut self,
source: Box<dyn CommitmentRootSource>,
requires_verified_successor: bool,
) {
self.source = Some(VctState::test_with_source(
source,
requires_verified_successor,
));
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct VctWriteData {
pub anchor_roots: Option<(
sapling::tree::Root,
orchard::tree::Root,
ironwood::tree::Root,
)>,
pub sync_below: Option<block::Height>,
}
fn embedded_final_frontiers(network: &Network) -> Option<FinalFrontiers> {
match network {
Network::Mainnet => Some(parse_embedded_final_frontiers(
MAINNET_FINAL_FRONTIERS,
network.checkpoint_list().max_height(),
)),
Network::Testnet(params) if params.is_regtest() => {
let path = std::env::var_os("VCT_REGTEST_FRONTIER")?;
Some(load_frontier_file(
path.as_ref(),
network.checkpoint_list().max_height(),
))
}
Network::Testnet(_) => None,
}
}
fn load_frontier_file(path: &std::ffi::OsStr, expected_height: block::Height) -> FinalFrontiers {
let bytes =
std::fs::read(path).expect("VCT_REGTEST_FRONTIER must name a readable final-frontier file");
parse_embedded_final_frontiers(&bytes, expected_height)
}
fn parse_embedded_final_frontiers(bytes: &[u8], expected_height: block::Height) -> FinalFrontiers {
parse_final_frontiers_bytes(bytes, expected_height).unwrap_or_else(|error| panic!("{error}"))
}
fn parse_final_frontiers_bytes(
bytes: &[u8],
expected_height: block::Height,
) -> Result<FinalFrontiers, FinalFrontiersValidationError> {
let parsed = FinalFrontiers::from_bytes(bytes).map_err(|error| {
FinalFrontiersValidationError::InvalidBytes {
error: error.to_string(),
}
})?;
if parsed.height != expected_height {
return Err(FinalFrontiersValidationError::HeightMismatch {
actual: parsed.height,
expected: expected_height,
});
}
Ok(parsed)
}
pub fn validate_final_frontiers_bytes(
bytes: &[u8],
expected_height: block::Height,
) -> Result<(), FinalFrontiersValidationError> {
parse_final_frontiers_bytes(bytes, expected_height).map(|_| ())
}
#[cfg(test)]
fn final_frontiers_bytes(height: block::Height, trees: &NoteCommitmentTrees) -> Vec<u8> {
FinalFrontiers {
height,
sapling: trees.sapling.clone(),
orchard: trees.orchard.clone(),
sprout: trees.sprout.clone(),
ironwood: trees.ironwood.clone(),
}
.to_bytes()
}
#[cfg(test)]
mod tests {
use super::*;
const EXPECTED_MAINNET_FINAL_SAPLING_ROOT: [u8; 32] = [
5, 88, 219, 64, 134, 21, 57, 124, 234, 59, 83, 8, 7, 143, 19, 29, 247, 58, 105, 80, 119,
139, 242, 243, 206, 137, 211, 94, 151, 126, 154, 13,
];
const EXPECTED_MAINNET_FINAL_ORCHARD_ROOT: [u8; 32] = [
177, 173, 139, 203, 63, 186, 47, 172, 148, 107, 150, 204, 211, 212, 33, 155, 172, 108, 132,
148, 70, 210, 120, 97, 219, 160, 58, 242, 198, 124, 44, 3,
];
const EXPECTED_MAINNET_FINAL_SPROUT_ROOT: [u8; 32] = [
77, 239, 224, 205, 90, 67, 51, 216, 15, 139, 120, 78, 55, 17, 177, 22, 246, 34, 206, 184,
49, 7, 97, 172, 28, 178, 69, 208, 13, 101, 55, 169,
];
#[test]
fn source_mode_precedence() {
use SourceMode::*;
assert_eq!(select_source_mode(true, true, true), Peer);
assert_eq!(select_source_mode(true, false, true), Legacy);
assert_eq!(select_source_mode(true, false, false), Legacy);
assert_eq!(select_source_mode(false, true, true), Legacy);
assert_eq!(select_source_mode(false, true, false), Legacy);
assert_eq!(select_source_mode(false, false, true), Legacy);
assert_eq!(select_source_mode(false, false, false), Legacy);
assert_eq!(select_source_mode(true, true, false), Legacy);
}
#[test]
fn successor_policy_is_vct_state_data() {
let network = Network::Mainnet;
let height = NetworkUpgrade::Heartwood
.activation_height(&network)
.expect("mainnet has a Heartwood activation height");
let root_map = || {
std::iter::once((
height.0,
(Default::default(), Default::default(), Default::default()),
))
.collect()
};
let frontiers = || FinalFrontiers {
height: (height + 1_000).expect("test height is valid"),
sapling: Arc::new(Default::default()),
orchard: Arc::new(Default::default()),
sprout: Arc::new(Default::default()),
ironwood: Arc::new(Default::default()),
};
let trusted = VctState::test_with_source(
Box::new(super::super::commitment_aux::FixtureSource::new(
root_map(),
frontiers(),
)),
false,
);
assert!(
!trusted.vct_root_needs_successor(height, &network),
"trusted fixture roots can commit without a buffered successor"
);
let untrusted = VctState::test_with_source(
Box::new(super::super::commitment_aux::FixtureSource::new(
root_map(),
frontiers(),
)),
true,
);
assert!(
untrusted.vct_root_needs_successor(height, &network),
"untrusted roots defer until a buffered successor verifies them"
);
}
#[test]
fn vct_root_is_bounded_by_handoff_height() {
let handoff = block::Height(10);
let after_handoff = (handoff + 1).expect("test height is valid");
let roots = std::collections::HashMap::from([
(
handoff.0,
(Default::default(), Default::default(), Default::default()),
),
(
after_handoff.0,
(Default::default(), Default::default(), Default::default()),
),
]);
let frontiers = FinalFrontiers {
height: handoff,
sapling: Arc::new(sapling::tree::NoteCommitmentTree::default()),
orchard: Arc::new(orchard::tree::NoteCommitmentTree::default()),
sprout: Arc::new(sprout::tree::NoteCommitmentTree::default()),
ironwood: Arc::new(ironwood::tree::NoteCommitmentTree::default()),
};
let bounded = VctState::test_with_source(
Box::new(super::super::commitment_aux::FixtureSource::new(
roots, frontiers,
)),
false,
);
assert!(
bounded.vct_roots_at_height(handoff).is_some(),
"the handoff root remains fast-path eligible"
);
assert!(
bounded.vct_roots_at_height(after_handoff).is_none(),
"roots above the handoff are ignored even when the source has them"
);
}
#[test]
fn embedded_mainnet_final_frontiers_parse() {
let frontiers = embedded_final_frontiers(&Network::Mainnet)
.expect("mainnet has embedded final frontiers");
assert_eq!(
frontiers.height,
Network::Mainnet.checkpoint_list().max_height(),
"embedded frontier is tied to the last mainnet checkpoint"
);
assert_eq!(
<[u8; 32]>::from(frontiers.sapling.root()),
EXPECTED_MAINNET_FINAL_SAPLING_ROOT,
"embedded mainnet final Sapling frontier root is pinned"
);
assert_eq!(
<[u8; 32]>::from(frontiers.orchard.root()),
EXPECTED_MAINNET_FINAL_ORCHARD_ROOT,
"embedded mainnet final Orchard frontier root is pinned"
);
assert_eq!(
<[u8; 32]>::from(frontiers.sprout.root()),
EXPECTED_MAINNET_FINAL_SPROUT_ROOT,
"embedded mainnet final Sprout frontier root is pinned"
);
assert_eq!(
frontiers.ironwood.root(),
ironwood::tree::NoteCommitmentTree::default().root(),
"the embedded mainnet-frontier.bin predates Ironwood, so it parses (backward \
compatibly) with the Ironwood frontier defaulted to the empty tree"
);
}
#[test]
fn final_frontiers_capture_helper_serializes_tip_trees() {
let height = block::Height(3_358_006);
let trees = NoteCommitmentTrees::default();
let parsed = FinalFrontiers::from_bytes(&final_frontiers_bytes(height, &trees))
.expect("captured final frontiers should parse");
assert_eq!(parsed.height, height, "captured height round-trips");
assert_eq!(
parsed.sapling.root(),
trees.sapling.root(),
"captured sapling frontier round-trips"
);
assert_eq!(
parsed.orchard.root(),
trees.orchard.root(),
"captured orchard frontier round-trips"
);
assert_eq!(
parsed.sprout.root(),
trees.sprout.root(),
"captured sprout frontier round-trips"
);
assert_eq!(
parsed.ironwood.root(),
trees.ironwood.root(),
"captured ironwood frontier round-trips"
);
}
#[test]
#[should_panic(expected = "embedded VCT final frontier height must match")]
fn embedded_final_frontiers_reject_checkpoint_height_mismatch() {
let frontiers = FinalFrontiers {
height: block::Height(1),
sapling: Arc::new(Default::default()),
orchard: Arc::new(Default::default()),
sprout: Arc::new(Default::default()),
ironwood: Arc::new(Default::default()),
};
let _ = parse_embedded_final_frontiers(&frontiers.to_bytes(), block::Height(2));
}
#[test]
fn final_frontiers_parser_rejects_short_height() {
let error =
FinalFrontiers::from_bytes(&[0, 1, 2]).expect_err("short height should be rejected");
assert_eq!(
error.to_string(),
"missing final frontier height: expected 4 bytes, got 3"
);
}
#[test]
fn final_frontiers_parser_rejects_missing_tree_length() {
let bytes = block::Height(1).0.to_le_bytes();
let error =
FinalFrontiers::from_bytes(&bytes).expect_err("missing length should be rejected");
assert_eq!(
error.to_string(),
"missing sapling frontier length prefix at byte 4: expected 4 bytes, got 0"
);
}
#[test]
fn final_frontiers_parser_rejects_truncated_tree_blob() {
let mut bytes = block::Height(1).0.to_le_bytes().to_vec();
bytes.extend_from_slice(&3u32.to_le_bytes());
bytes.extend_from_slice(&[0, 1]);
let error =
FinalFrontiers::from_bytes(&bytes).expect_err("truncated blob should be rejected");
assert_eq!(
error.to_string(),
"truncated sapling frontier blob at byte 8: length prefix says 3 bytes, but only 2 remain"
);
}
#[test]
fn final_frontiers_parser_rejects_trailing_bytes() {
let bytes = FinalFrontiers {
height: block::Height(1),
sapling: Arc::new(Default::default()),
orchard: Arc::new(Default::default()),
sprout: Arc::new(Default::default()),
ironwood: Arc::new(Default::default()),
}
.to_bytes()
.into_iter()
.chain([0])
.collect::<Vec<_>>();
let error =
FinalFrontiers::from_bytes(&bytes).expect_err("trailing bytes should be rejected");
assert_eq!(
error.to_string(),
format!(
"unexpected trailing final frontier bytes at byte {}: 1 bytes",
bytes.len() - 1
)
);
}
#[test]
#[should_panic(expected = "invalid VCT final frontier bytes: truncated sapling frontier blob")]
fn embedded_final_frontiers_reject_malformed_bytes_with_context() {
let mut bytes = block::Height(1).0.to_le_bytes().to_vec();
bytes.extend_from_slice(&3u32.to_le_bytes());
bytes.extend_from_slice(&[0, 1]);
let _ = parse_embedded_final_frontiers(&bytes, block::Height(1));
}
#[test]
fn embedded_final_frontiers_are_network_specific() {
assert!(
embedded_final_frontiers(&Network::new_default_testnet()).is_none(),
"testnet has no embedded final frontier until VCT fast sync supports it"
);
}
#[test]
fn load_frontier_file_round_trips_a_captured_frontier() {
let height = block::Height(123);
let bytes = FinalFrontiers {
height,
sapling: Arc::new(Default::default()),
orchard: Arc::new(Default::default()),
sprout: Arc::new(Default::default()),
ironwood: Arc::new(Default::default()),
}
.to_bytes();
let path =
std::env::temp_dir().join(format!("vct-frontier-load-test-{}.bin", std::process::id()));
std::fs::write(&path, &bytes).expect("write temp frontier file");
let loaded = load_frontier_file(path.as_os_str(), height);
assert_eq!(loaded.height, height, "loaded frontier height matches");
assert_eq!(
loaded.sapling.root(),
sapling::tree::NoteCommitmentTree::default().root(),
"loaded sapling frontier round-trips"
);
let _ = std::fs::remove_file(&path);
}
#[test]
#[should_panic(expected = "embedded VCT final frontier height must match")]
fn load_frontier_file_rejects_height_mismatch() {
let bytes = FinalFrontiers {
height: block::Height(5),
sapling: Arc::new(Default::default()),
orchard: Arc::new(Default::default()),
sprout: Arc::new(Default::default()),
ironwood: Arc::new(Default::default()),
}
.to_bytes();
let path = std::env::temp_dir().join(format!(
"vct-frontier-mismatch-test-{}.bin",
std::process::id()
));
std::fs::write(&path, &bytes).expect("write temp frontier file");
let _ = load_frontier_file(path.as_os_str(), block::Height(6));
}
}