use spine::{ARITY_RANGE, Hasher, frontier_for_size};
pub use spine::{
InclusionProof, ProofStep, constant_time_eq, reconstruct_inclusion_root, verify_inclusion,
verify_inclusion_path_structure,
};
use crate::mountain::{bag_peaks, mountain_skeleton};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConsistencyProof {
pub boundary_hash: Vec<u8>,
pub peak_path: Vec<ProofStep>,
pub new_peaks: Vec<Vec<u8>>,
pub split_index: usize,
}
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn verify_consistency(
hasher: &dyn Hasher,
old_size: u64,
new_size: u64,
arity: u64,
boundary_hash: &[u8],
peak_path: &[ProofStep],
new_peaks: &[Vec<u8>],
split_index: usize,
old_root: &[u8],
new_root: &[u8],
) -> bool {
reconstruct_consistency_roots(
hasher,
old_size,
new_size,
arity,
boundary_hash,
peak_path,
new_peaks,
split_index,
)
.is_some_and(|(computed_old, computed_new)| {
constant_time_eq(&computed_old, old_root) & constant_time_eq(&computed_new, new_root)
})
}
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn reconstruct_consistency_roots(
hasher: &dyn Hasher,
old_size: u64,
new_size: u64,
arity: u64,
boundary_hash: &[u8],
peak_path: &[ProofStep],
new_peaks: &[Vec<u8>],
split_index: usize,
) -> Option<(Vec<u8>, Vec<u8>)> {
let digest_len = hasher.empty().len();
if digest_len == 0 || digest_len > 64 {
return None;
}
if boundary_hash.len() != digest_len {
return None;
}
if old_size == 0 || old_size >= new_size {
return None;
}
if !ARITY_RANGE.contains(&arity) {
return None;
}
let k = arity;
let old_coords = frontier_for_size(old_size, k);
let new_coords = frontier_for_size(new_size, k);
let &(boundary_left, boundary_height) = old_coords.last()?;
if new_peaks.len() != new_coords.len() {
return None;
}
if split_index >= new_coords.len() {
return None;
}
if new_peaks.iter().any(|p| p.len() != digest_len) {
return None;
}
let (new_left, new_height) = new_coords[split_index];
let cap = k.checked_pow(new_height)?;
let limit = new_left.checked_add(cap)?;
if boundary_left < new_left || boundary_left >= limit {
return None;
}
if new_height < boundary_height {
return None;
}
let full_skeleton = mountain_skeleton(k, new_size, boundary_left)?;
let bh = boundary_height as usize;
let nh = new_height as usize;
if full_skeleton.len() < nh {
return None;
}
let climb = &full_skeleton[bh..nh];
if peak_path.len() != climb.len() {
return None;
}
let recovered = reconstruct_inclusion_root(hasher, boundary_hash, climb, peak_path)?;
if !constant_time_eq(&recovered, &new_peaks[split_index]) {
return None;
}
let computed_new_root = bag_peaks(hasher, new_peaks, k);
let mut old_peaks: Vec<Vec<u8>> = new_peaks[..split_index].to_vec();
for step in peak_path.iter().rev() {
let left = step.position.min(step.siblings.len());
for sib in &step.siblings[..left] {
old_peaks.push(sib.clone());
}
}
old_peaks.push(boundary_hash.to_vec());
let computed_old_root = bag_peaks(hasher, &old_peaks, k);
Some((computed_old_root, computed_new_root))
}