use spine::{ARITY_RANGE, Hasher, ProofStep, constant_time_eq, nary_mr, verify_inclusion};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NullRun {
pub left: u64,
pub height: u32,
}
#[must_use]
pub fn null_runs_for_alg(eps: &[(u64, u64)], tree_size: u64, arity: u64) -> Vec<NullRun> {
let mut active: Vec<(u64, u64)> = eps
.iter()
.filter_map(|&(s, e)| {
let s = s.min(tree_size);
let e = e.min(tree_size);
(s < e).then_some((s, e))
})
.collect();
active.sort_unstable();
let mut merged: Vec<(u64, u64)> = Vec::with_capacity(active.len());
for (s, e) in active {
match merged.last_mut() {
Some(last) if s <= last.1 => last.1 = last.1.max(e),
_ => merged.push((s, e)),
}
}
let mut runs = Vec::new();
let mut cursor = 0u64;
for (s, e) in merged {
decompose_inactive_gap(cursor, s, arity, &mut runs);
cursor = e;
}
decompose_inactive_gap(cursor, tree_size, arity, &mut runs);
runs
}
fn decompose_inactive_gap(left_bound: u64, right: u64, arity: u64, runs: &mut Vec<NullRun>) {
let mut left = left_bound;
while left < right {
let mut height = 0u32;
loop {
let next_h = height + 1;
let span = match arity.checked_pow(next_h) {
Some(s) => s,
None => break,
};
if left % span == 0 && left + span <= right {
height = next_h;
} else {
break;
}
}
runs.push(NullRun { left, height });
left += arity.pow(height);
}
}
#[must_use]
pub fn all_null_runs(
alg_epochs: &[(u64, Vec<(u64, u64)>)],
tree_size: u64,
arity: u64,
) -> Vec<(u64, Vec<NullRun>)> {
let mut out: Vec<(u64, Vec<NullRun>)> = alg_epochs
.iter()
.filter_map(|(id, eps)| {
let runs = null_runs_for_alg(eps, tree_size, arity);
if runs.is_empty() {
None
} else {
Some((*id, runs))
}
})
.collect();
out.sort_unstable_by_key(|&(id, _)| id);
out
}
#[must_use]
pub fn null_runs_are_trivial(
alg_epochs: &[(u64, Vec<(u64, u64)>)],
tree_size: u64,
arity: u64,
) -> bool {
all_null_runs(alg_epochs, tree_size, arity).is_empty()
}
#[must_use]
pub fn serialize_null_runs(
alg_epochs: &[(u64, Vec<(u64, u64)>)],
tree_size: u64,
arity: u64,
) -> Vec<u8> {
let all = all_null_runs(alg_epochs, tree_size, arity);
let mut buf = Vec::new();
buf.extend_from_slice(&arity.to_be_bytes());
buf.extend_from_slice(&tree_size.to_be_bytes());
buf.extend_from_slice(&(all.len() as u64).to_be_bytes());
for (id, runs) in &all {
buf.extend_from_slice(&id.to_be_bytes());
buf.extend_from_slice(&(runs.len() as u64).to_be_bytes());
for r in runs {
buf.extend_from_slice(&r.left.to_be_bytes());
buf.extend_from_slice(&r.height.to_be_bytes());
}
}
buf
}
#[must_use]
pub fn combined_root(
hasher: &dyn Hasher,
member_roots: &[(u64, Vec<u8>)],
alg_epochs: &[(u64, Vec<(u64, u64)>)],
tree_size: u64,
arity: u64,
) -> Vec<u8> {
let mut children: Vec<Vec<u8>> = member_roots.iter().map(|(_, r)| r.clone()).collect();
if !null_runs_are_trivial(alg_epochs, tree_size, arity) {
children.push(hasher.hash(&serialize_null_runs(alg_epochs, tree_size, arity)));
}
let refs: Vec<&[u8]> = children.iter().map(|c| c.as_slice()).collect();
nary_mr(hasher, &refs)
}
#[must_use]
pub fn validate_committed_epochs(alg_epochs: &[(u64, Vec<(u64, u64)>)], tree_size: u64) -> bool {
if alg_epochs.windows(2).any(|w| w[0].0 >= w[1].0) {
return false;
}
for (_, epochs) in alg_epochs {
if epochs.is_empty() {
return false;
}
let mut last_end = 0u64;
for (i, &(start, end)) in epochs.iter().enumerate() {
if start > end || start < last_end {
return false;
}
if end == u64::MAX {
if i != epochs.len() - 1 || start > tree_size {
return false;
}
} else if end > tree_size {
return false;
}
last_end = end;
}
}
true
}
#[must_use]
pub fn committed_active_at(
alg_epochs: &[(u64, Vec<(u64, u64)>)],
alg_id: u64,
index: u64,
) -> Option<bool> {
let idx = alg_epochs
.binary_search_by_key(&alg_id, |&(id, _)| id)
.ok()?;
Some(
alg_epochs[idx]
.1
.iter()
.any(|&(start, end)| start <= index && index < end),
)
}
#[must_use]
pub fn committed_active_algs(alg_epochs: &[(u64, Vec<(u64, u64)>)], tree_size: u64) -> Vec<u64> {
if tree_size == 0 {
return Vec::new();
}
let last = tree_size - 1;
alg_epochs
.iter()
.filter(|(_, epochs)| {
epochs
.iter()
.any(|&(start, end)| start <= last && last < end)
})
.map(|&(id, _)| id)
.collect()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VerifierConfig {
pub max_active_algorithms: usize,
pub max_algorithms: usize,
pub max_epochs_per_algorithm: usize,
}
impl Default for VerifierConfig {
fn default() -> Self {
Self {
max_active_algorithms: 8,
max_algorithms: 64,
max_epochs_per_algorithm: 1024,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CouplingProof {
pub active_roots: Vec<(u64, Vec<u8>)>,
pub alg_epochs: Vec<(u64, Vec<(u64, u64)>)>,
}
impl CouplingProof {
#[must_use]
pub fn authenticate(
&self,
hasher: &dyn Hasher,
tree_size: u64,
arity: u64,
expected_combined_root: &[u8],
expected_active_algs: &[u64],
config: VerifierConfig,
) -> bool {
if tree_size == 0 {
return false;
}
if !ARITY_RANGE.contains(&arity) {
return false;
}
if self.active_roots.len() > config.max_active_algorithms {
return false;
}
if self.alg_epochs.len() > config.max_algorithms {
return false;
}
if self
.alg_epochs
.iter()
.any(|(_, e)| e.len() > config.max_epochs_per_algorithm)
{
return false;
}
if self.active_roots.len() != expected_active_algs.len() {
return false;
}
for ((id, _), &expected_id) in self.active_roots.iter().zip(expected_active_algs.iter()) {
if *id != expected_id {
return false;
}
}
for (_, r) in &self.active_roots {
if r.len() > 64 {
return false;
}
}
if self.active_roots.windows(2).any(|w| w[0].0 >= w[1].0) {
return false;
}
if !validate_committed_epochs(&self.alg_epochs, tree_size) {
return false;
}
let derived = committed_active_algs(&self.alg_epochs, tree_size);
if derived.len() != self.active_roots.len()
|| derived
.iter()
.zip(self.active_roots.iter())
.any(|(&d, &(id, _))| d != id)
{
return false;
}
let computed = combined_root(
hasher,
&self.active_roots,
&self.alg_epochs,
tree_size,
arity,
);
constant_time_eq(&computed, expected_combined_root)
}
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn verify(
&self,
hasher: &dyn Hasher,
target_alg_id: u64,
tree_size: u64,
arity: u64,
combined_root: &[u8],
expected_active_algs: &[u64],
config: VerifierConfig,
) -> Option<Vec<u8>> {
if !self.authenticate(
hasher,
tree_size,
arity,
combined_root,
expected_active_algs,
config,
) {
return None;
}
self.active_roots
.iter()
.find(|&&(id, _)| id == target_alg_id)
.map(|(_, r)| r.clone())
}
}
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn verify_inclusion_with_coupling(
hasher: &dyn Hasher,
alg_id: u64,
leaf_hash: &[u8],
index: u64,
tree_size: u64,
arity: u64,
skeleton: &[spine::SkeletonStep],
path: &[ProofStep],
coupling: &CouplingProof,
combined_root: &[u8],
expected_active_algs: &[u64],
config: VerifierConfig,
) -> bool {
let raw_root = match coupling.verify(
hasher,
alg_id,
tree_size,
arity,
combined_root,
expected_active_algs,
config,
) {
Some(r) => r,
None => return false,
};
match committed_active_at(&coupling.alg_epochs, alg_id, index) {
Some(false) => {
if !constant_time_eq(leaf_hash, &hasher.null()) {
return false;
}
},
Some(true) => {},
None => return false,
}
verify_inclusion(hasher, leaf_hash, skeleton, path, &raw_root)
}
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn verify_inactivity_with_coupling(
hasher: &dyn Hasher,
alg_id: u64,
index: u64,
tree_size: u64,
arity: u64,
skeleton: &[spine::SkeletonStep],
path: &[ProofStep],
coupling: &CouplingProof,
combined_root: &[u8],
expected_active_algs: &[u64],
config: VerifierConfig,
) -> bool {
if index >= tree_size {
return false;
}
if !coupling.authenticate(
hasher,
tree_size,
arity,
combined_root,
expected_active_algs,
config,
) {
return false;
}
match committed_active_at(&coupling.alg_epochs, alg_id, index) {
Some(false) => {},
_ => return false,
}
if let Some((_, raw_root)) = coupling.active_roots.iter().find(|&&(id, _)| id == alg_id) {
verify_inclusion(hasher, &hasher.null(), skeleton, path, raw_root)
} else {
path.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuditPayload {
pub log_id: [u8; 32],
pub tree_size: u64,
pub active_algs: Vec<u64>,
pub combined_roots: Vec<(u64, Vec<u8>)>,
pub alg_epochs: Vec<(u64, Vec<(u64, u64)>)>,
}
#[cfg(test)]
mod tests {
use sha2::{Digest, Sha256};
use super::*;
const MAX: u64 = u64::MAX;
#[derive(Debug)]
struct H;
impl Hasher for H {
fn leaf(&self, data: &[u8]) -> Vec<u8> {
Sha256::digest(data).to_vec()
}
fn node(&self, children: &[&[u8]]) -> Vec<u8> {
let mut h = Sha256::new();
for c in children {
h.update(c);
}
h.finalize().to_vec()
}
fn empty(&self) -> Vec<u8> {
Sha256::digest(b"").to_vec()
}
fn hash(&self, data: &[u8]) -> Vec<u8> {
Sha256::digest(data).to_vec()
}
fn clone_box(&self) -> Box<dyn Hasher> {
Box::new(H)
}
}
#[test]
fn activation_trivial_is_informativeness_not_cardinality() {
assert!(null_runs_are_trivial(&[(0, vec![(0, MAX)])], 8, 2));
assert!(null_runs_are_trivial(
&[
(0, vec![(0, MAX)]),
(1, vec![(0, MAX)]),
(5, vec![(0, MAX)])
],
8,
2
));
assert!(!null_runs_are_trivial(&[(0, vec![(2, MAX)])], 8, 2));
assert!(!null_runs_are_trivial(&[(0, vec![(0, 5)])], 8, 2));
assert!(!null_runs_are_trivial(&[(0, vec![(0, 3), (5, MAX)])], 8, 2));
assert!(null_runs_are_trivial(&[], 8, 2));
}
#[test]
fn combined_root_is_the_fold_over_member_children() {
let mr0 = vec![0xAA; 32];
let mr1 = vec![0xBB; 32];
let members = vec![(0u64, mr0.clone()), (1u64, mr1.clone())];
let trivial = vec![(0u64, vec![(0u64, MAX)]), (1, vec![(0, MAX)])];
let got = combined_root(&H, &members, &trivial, 4, 2);
let expected = nary_mr(&H, &[mr0.as_slice(), mr1.as_slice()]);
assert_eq!(got, expected);
}
#[test]
fn combined_root_singleton_promotes_with_no_predicate() {
let mr0 = vec![0xCD; 32];
let members = vec![(0u64, mr0.clone())];
let got = combined_root(&H, &members, &[(0, vec![(0, MAX)])], 4, 2);
assert_eq!(got, mr0);
}
#[test]
fn combined_root_appends_coverage_child_iff_non_trivial() {
let mr0 = vec![0x11; 32];
let members = vec![(0u64, mr0.clone())];
let non_trivial = vec![(0u64, vec![(0u64, 5u64)])];
let got = combined_root(&H, &members, &non_trivial, 8, 2);
let coverage = H.hash(&serialize_null_runs(&non_trivial, 8, 2));
let expected = nary_mr(&H, &[mr0.as_slice(), coverage.as_slice()]);
assert_eq!(got, expected);
assert_ne!(got, mr0);
let active = combined_root(&H, &members, &[(0, vec![(0, MAX)])], 8, 2);
assert_eq!(active, mr0);
}
#[test]
fn serialize_null_runs_is_injective_over_activation() {
let a = serialize_null_runs(&[(0, vec![(0, MAX)])], 8, 2); let b = serialize_null_runs(&[(0, vec![(0, 4)])], 8, 2); let c = serialize_null_runs(&[(0, vec![(0, 6)])], 8, 2); assert_ne!(a, b);
assert_ne!(b, c);
}
#[test]
fn test_validate_committed_epochs() {
assert!(validate_committed_epochs(&[(0, vec![(0, MAX)])], 10));
assert!(validate_committed_epochs(&[(0, vec![(0, 5)])], 10));
assert!(validate_committed_epochs(
&[(0, vec![(0, 3), (5, MAX)]), (1, vec![(2, 2)])],
10
));
assert!(validate_committed_epochs(
&[(0, vec![(0, 5), (5, MAX)])],
10
));
assert!(!validate_committed_epochs(
&[(1, vec![(0, MAX)]), (0, vec![(0, MAX)])],
10
));
assert!(!validate_committed_epochs(
&[(0, vec![(0, MAX)]), (0, vec![(0, MAX)])],
10
));
assert!(!validate_committed_epochs(&[(0, vec![])], 10));
assert!(!validate_committed_epochs(
&[(0, vec![(0, 5), (4, MAX)])],
10
));
assert!(!validate_committed_epochs(&[(0, vec![(5, 3)])], 10));
assert!(!validate_committed_epochs(
&[(0, vec![(0, MAX), (1, 2)])],
10
));
assert!(!validate_committed_epochs(&[(0, vec![(0, 11)])], 10));
assert!(!validate_committed_epochs(&[(0, vec![(11, MAX)])], 10));
assert!(validate_committed_epochs(&[(0, vec![(0, 10)])], 10));
}
#[test]
fn test_committed_activity_reads() {
let timeline = vec![(0u64, vec![(0u64, 3u64), (5, MAX)]), (1, vec![(0, 10)])];
assert_eq!(committed_active_at(&timeline, 0, 2), Some(true));
assert_eq!(committed_active_at(&timeline, 0, 3), Some(false));
assert_eq!(committed_active_at(&timeline, 0, 4), Some(false));
assert_eq!(committed_active_at(&timeline, 0, 5), Some(true));
assert_eq!(committed_active_at(&timeline, 1, 9), Some(true));
assert_eq!(committed_active_at(&timeline, 2, 0), None);
assert_eq!(committed_active_algs(&timeline, 10), vec![0, 1]);
assert_eq!(committed_active_algs(&timeline, 5), vec![1]);
assert_eq!(committed_active_algs(&timeline, 2), vec![0, 1]);
assert!(committed_active_algs(&timeline, 0).is_empty());
}
#[test]
fn null_runs_cover_exactly_the_inactive_positions() {
let active_at = |eps: &[(u64, u64)], i: u64| eps.iter().any(|&(s, e)| s <= i && i < e);
let cases: Vec<Vec<(u64, u64)>> = vec![
vec![(0, MAX)], vec![(0, 0)], vec![(3, MAX)], vec![(0, 5)], vec![(0, 3), (5, MAX)], vec![(0, 1), (2, 3), (4, 5)], vec![(2, 4), (7, 9)], ];
for eps in &cases {
for &tree_size in &[0u64, 1, 2, 3, 4, 7, 8, 9, 16, 27] {
for &k in &[2u64, 3, 4] {
let runs = null_runs_for_alg(eps, tree_size, k);
let mut covered = vec![false; tree_size as usize];
for r in &runs {
let span = k.pow(r.height);
assert_eq!(r.left % span, 0, "run not aligned: {r:?} k={k}");
assert!(r.left + span <= tree_size, "run overruns size: {r:?}");
for p in r.left..r.left + span {
assert!(!covered[p as usize], "runs overlap at {p}");
covered[p as usize] = true;
}
}
for i in 0..tree_size {
let active = active_at(eps, i);
assert_eq!(
covered[i as usize], !active,
"position {i} (eps={eps:?} size={tree_size} k={k}): null-run cover {} \
disagrees with inactive {}",
covered[i as usize], !active
);
}
}
}
}
}
#[test]
fn null_runs_serialization_binds_arity_and_size() {
let eps = vec![(0u64, vec![(0u64, 4u64)])]; let a = serialize_null_runs(&eps, 8, 2);
let b = serialize_null_runs(&eps, 8, 4);
let c = serialize_null_runs(&eps, 16, 2);
assert_ne!(a, b);
assert_ne!(a, c);
let triv = vec![(0u64, vec![(0u64, MAX)])];
assert!(null_runs_are_trivial(&triv, 8, 2));
assert!(!null_runs_are_trivial(&eps, 8, 2));
}
fn null_runs_by_scan(eps: &[(u64, u64)], tree_size: u64, arity: u64) -> Vec<NullRun> {
let active_at = |i: u64| eps.iter().any(|&(s, e)| s <= i && i < e);
let mut runs = Vec::new();
let mut i = 0u64;
while i < tree_size {
if active_at(i) {
i += 1;
continue;
}
let mut j = i;
while j < tree_size && !active_at(j) {
j += 1;
}
let mut left = i;
while left < j {
let mut height = 0u32;
loop {
let next_h = height + 1;
let span = match arity.checked_pow(next_h) {
Some(s) => s,
None => break,
};
if left % span == 0 && left + span <= j {
height = next_h;
} else {
break;
}
}
runs.push(NullRun { left, height });
left += arity.pow(height);
}
i = j;
}
runs
}
#[test]
fn null_runs_interval_wise_equals_position_scan() {
let mut state: u64 = 0x9E37_79B9_7F4A_7C15;
let mut next = || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
};
for _ in 0..50_000 {
let tree_size = next() % 130;
let arity = 2 + next() % 7; let n_eps = (next() % 5) as usize;
let mut eps: Vec<(u64, u64)> = Vec::with_capacity(n_eps);
for _ in 0..n_eps {
let start = next() % (tree_size + 1);
let end = if next() % 8 == 0 {
MAX
} else {
next() % (tree_size + 2)
};
eps.push((start, end));
}
let scanned = null_runs_by_scan(&eps, tree_size, arity);
let computed = null_runs_for_alg(&eps, tree_size, arity);
assert_eq!(
computed, scanned,
"interval-wise null_runs_for_alg diverged from position scan: eps={eps:?} \
tree_size={tree_size} arity={arity}"
);
}
assert!(null_runs_for_alg(&[(0, MAX)], 1_000_000, 2).is_empty());
}
}