diff --git a/CHANGELOG.md b/CHANGELOG.md index c62340b150..d1e6a9dc32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - [BREAKING] Extracted `BackendReader`, allowing `LargeSmtForest` to work with read-only storage backends ([#986](https://github.com/0xMiden/crypto/pull/986)). - [BREAKING] Refactored `miden-lifted-stark::domain` around a uniform `Coset` trait shared by `TwoAdicSubgroup` and `TwoAdicCoset`, slimmed the `LiftedDomain` surface (drops dead getters, removes silently-dispatched `points`/`bit_reversed_points`/`vanishing_at` in favour of explicit `trace_subgroup()` / `lde_coset()` access), made `LiftedDomain` constructors fallible, moved selector logic onto `LiftedDomain`, and changed `log_blowup` to return `u8` ([#993](https://github.com/0xMiden/crypto/pull/993)). - Added `MerkleFrontier` as append-only `len + peaks` state with a raw Merkle root, append support, legacy `MmrPeaks` conversion, and standard Merkle proof bridging that authenticates `(len, root)` during verification ([#984](https://github.com/0xMiden/crypto/pull/984)). +- Added internal Merkle Mountain Belt prototype benchmarks for evaluating append, opening, summary, and recency-sensitive proof verification tradeoffs ([#1028](https://github.com/0xMiden/crypto/pull/1028)). ## 0.25.0 (2026-05-01) diff --git a/miden-crypto/Cargo.toml b/miden-crypto/Cargo.toml index b3935bfd87..36012ca6da 100644 --- a/miden-crypto/Cargo.toml +++ b/miden-crypto/Cargo.toml @@ -44,6 +44,11 @@ harness = false name = "mmr" required-features = ["std"] +[[bench]] +harness = false +name = "mmr_belt" +required-features = ["internal"] + [[bench]] harness = false name = "partial_mt" diff --git a/miden-crypto/benches/mmr_belt.rs b/miden-crypto/benches/mmr_belt.rs new file mode 100644 index 0000000000..198f2d92bd --- /dev/null +++ b/miden-crypto/benches/mmr_belt.rs @@ -0,0 +1,537 @@ +use std::{hint, time::Duration}; + +use criterion::{BatchSize, BenchmarkId, Criterion, criterion_group, criterion_main}; +use miden_crypto::{ + Word, + merkle::mmr::{ + MerkleFrontier, Mmr, MmrPeaks, + belt::{BeltSummary, MmrBelt}, + }, +}; + +mod common; +use common::data::{WordPattern, generate_word_pattern, generate_words_pattern}; + +const MMR_BELT_SIZES: &[usize] = &[1_000, 1_023, 1_024, 50_000, 65_535, 65_536]; +const RECENCIES: &[usize] = &[1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1_024, 4_096, 16_384]; +const APPEND_SEQUENCE_LEN: usize = 1_024; +const MEASUREMENT_TIME: Duration = Duration::from_secs(2); +const SAMPLE_SIZE: usize = 20; + +#[derive(Clone)] +struct MmrBeltBenchData { + leaves: Vec, + mmr: Mmr, + peaks: MmrPeaks, + frontier: MerkleFrontier, + belt: MmrBelt, + belt_summary: BeltSummary, +} + +impl MmrBeltBenchData { + fn build(size: usize) -> Self { + let leaves = generate_words_pattern(size, WordPattern::Sequential); + let mmr = Mmr::try_from_iter(leaves.iter().copied()).unwrap(); + let peaks = mmr.peaks(); + let frontier = mmr.frontier(); + + let mut belt = MmrBelt::new(); + for leaf in leaves.iter().copied() { + belt.add(leaf).unwrap(); + } + let belt_summary = belt.summary(); + + Self { + leaves, + mmr, + peaks, + frontier, + belt, + belt_summary, + } + } +} + +fn append_sequence(start: usize) -> Vec { + (start..start + APPEND_SEQUENCE_LEN) + .map(|idx| generate_word_pattern(idx as u64, WordPattern::Sequential)) + .collect() +} + +fn bench_mmr_belt_build(c: &mut Criterion) { + let mut group = c.benchmark_group("mmr-belt-build"); + configure_group(&mut group); + + for &size in MMR_BELT_SIZES { + group.bench_with_input(BenchmarkId::new("current-mmr", size), &size, |b, &size| { + b.iter_batched( + || generate_words_pattern(size, WordPattern::Sequential), + |leaves| { + hint::black_box(Mmr::try_from_iter(leaves).unwrap()); + }, + BatchSize::SmallInput, + ); + }); + + group.bench_with_input(BenchmarkId::new("belt-prototype", size), &size, |b, &size| { + b.iter_batched( + || generate_words_pattern(size, WordPattern::Sequential), + |leaves| { + let mut belt = MmrBelt::new(); + for leaf in leaves { + belt.add(leaf).unwrap(); + } + hint::black_box(belt); + }, + BatchSize::SmallInput, + ); + }); + + group.bench_with_input(BenchmarkId::new("belt-lazy-bagging", size), &size, |b, &size| { + b.iter_batched( + || generate_words_pattern(size, WordPattern::Sequential), + |leaves| { + let mut belt = MmrBelt::new(); + for leaf in leaves { + belt.add_deferred(leaf).unwrap(); + } + hint::black_box(belt); + }, + BatchSize::SmallInput, + ); + }); + } + + group.finish(); +} + +fn bench_mmr_belt_append(c: &mut Criterion) { + let mut group = c.benchmark_group("mmr-belt-append"); + configure_group(&mut group); + + for &size in MMR_BELT_SIZES { + let data = MmrBeltBenchData::build(size); + let next_leaf = generate_word_pattern(size as u64, WordPattern::Sequential); + + group.bench_with_input(BenchmarkId::new("current-mmr", size), &size, |b, _| { + b.iter_batched_ref( + || data.mmr.clone(), + |mmr| { + mmr.add(hint::black_box(next_leaf)).unwrap(); + hint::black_box(mmr.forest().num_leaves()); + }, + BatchSize::LargeInput, + ); + }); + + group.bench_with_input(BenchmarkId::new("frontier", size), &size, |b, _| { + b.iter_batched_ref( + || data.frontier.clone(), + |frontier| { + frontier.append(hint::black_box(next_leaf)).unwrap(); + hint::black_box(frontier.num_leaves()); + }, + BatchSize::LargeInput, + ); + }); + + group.bench_with_input(BenchmarkId::new("belt-prototype", size), &size, |b, _| { + b.iter_batched_ref( + || data.belt.clone(), + |belt| { + belt.add(hint::black_box(next_leaf)).unwrap(); + hint::black_box(belt.num_leaves()); + }, + BatchSize::LargeInput, + ); + }); + + group.bench_with_input(BenchmarkId::new("belt-lazy-bagging", size), &size, |b, _| { + b.iter_batched_ref( + || data.belt.clone(), + |belt| { + belt.add_deferred(hint::black_box(next_leaf)).unwrap(); + hint::black_box(belt.num_leaves()); + }, + BatchSize::LargeInput, + ); + }); + + group.bench_with_input( + BenchmarkId::new("current-mmr-and-commitment", size), + &size, + |b, _| { + b.iter_batched( + || data.mmr.clone(), + |mut mmr| { + mmr.add(hint::black_box(next_leaf)).unwrap(); + hint::black_box(mmr.peaks().hash_peaks()); + }, + BatchSize::SmallInput, + ); + }, + ); + + group.bench_with_input(BenchmarkId::new("frontier-and-root", size), &size, |b, _| { + b.iter_batched( + || data.frontier.clone(), + |mut frontier| { + frontier.append(hint::black_box(next_leaf)).unwrap(); + hint::black_box(frontier.root()); + }, + BatchSize::SmallInput, + ); + }); + + group.bench_with_input( + BenchmarkId::new("belt-prototype-and-summary", size), + &size, + |b, _| { + b.iter_batched( + || data.belt.clone(), + |mut belt| { + belt.add(hint::black_box(next_leaf)).unwrap(); + hint::black_box(belt.summary()); + }, + BatchSize::SmallInput, + ); + }, + ); + + group.bench_with_input( + BenchmarkId::new("belt-lazy-bagging-and-rebuilt-summary", size), + &size, + |b, _| { + b.iter_batched( + || data.belt.clone(), + |mut belt| { + belt.add_deferred(hint::black_box(next_leaf)).unwrap(); + hint::black_box(belt.summary()); + }, + BatchSize::SmallInput, + ); + }, + ); + + group.bench_with_input(BenchmarkId::new("belt-prototype-and-root", size), &size, |b, _| { + b.iter_batched( + || data.belt.clone(), + |mut belt| { + belt.add(hint::black_box(next_leaf)).unwrap(); + hint::black_box(belt.commitment_root()); + }, + BatchSize::SmallInput, + ); + }); + + group.bench_with_input( + BenchmarkId::new("belt-lazy-bagging-and-rebuilt-root", size), + &size, + |b, _| { + b.iter_batched( + || data.belt.clone(), + |mut belt| { + belt.add_deferred(hint::black_box(next_leaf)).unwrap(); + hint::black_box(belt.commitment_root()); + }, + BatchSize::SmallInput, + ); + }, + ); + } + + group.finish(); +} + +fn bench_mmr_belt_append_sequence(c: &mut Criterion) { + let mut group = c.benchmark_group("mmr-belt-append-sequence"); + configure_group(&mut group); + group.throughput(criterion::Throughput::Elements(APPEND_SEQUENCE_LEN as u64)); + + for &size in MMR_BELT_SIZES { + let data = MmrBeltBenchData::build(size); + let leaves = append_sequence(size); + + group.bench_with_input(BenchmarkId::new("current-mmr", size), &size, |b, _| { + b.iter_batched_ref( + || (data.mmr.clone(), leaves.clone()), + |(mmr, leaves)| { + for leaf in leaves.iter().copied() { + mmr.add(hint::black_box(leaf)).unwrap(); + } + hint::black_box(mmr); + }, + BatchSize::LargeInput, + ); + }); + + group.bench_with_input(BenchmarkId::new("belt-prototype", size), &size, |b, _| { + b.iter_batched_ref( + || (data.belt.clone(), leaves.clone()), + |(belt, leaves)| { + for leaf in leaves.iter().copied() { + belt.add(hint::black_box(leaf)).unwrap(); + } + hint::black_box(belt); + }, + BatchSize::LargeInput, + ); + }); + + group.bench_with_input(BenchmarkId::new("belt-lazy-bagging", size), &size, |b, _| { + b.iter_batched_ref( + || (data.belt.clone(), leaves.clone()), + |(belt, leaves)| { + for leaf in leaves.iter().copied() { + belt.add_deferred(hint::black_box(leaf)).unwrap(); + } + hint::black_box(belt); + }, + BatchSize::LargeInput, + ); + }); + } + + group.finish(); +} + +fn bench_mmr_belt_commitment(c: &mut Criterion) { + let mut group = c.benchmark_group("mmr-belt-commitment"); + configure_group(&mut group); + + for &size in MMR_BELT_SIZES { + let data = MmrBeltBenchData::build(size); + + group.bench_with_input(BenchmarkId::new("current-mmr-hash-peaks", size), &size, |b, _| { + b.iter(|| { + hint::black_box(data.peaks.hash_peaks()); + }); + }); + + group.bench_with_input(BenchmarkId::new("frontier-root", size), &size, |b, _| { + b.iter(|| { + hint::black_box(data.frontier.root()); + }); + }); + + group.bench_with_input(BenchmarkId::new("belt-summary", size), &size, |b, _| { + b.iter(|| { + hint::black_box(data.belt.summary()); + }); + }); + + group.bench_with_input(BenchmarkId::new("belt-commitment-root", size), &size, |b, _| { + b.iter(|| { + hint::black_box(data.belt.commitment_root()); + }); + }); + } + + group.finish(); +} + +fn bench_mmr_belt_open(c: &mut Criterion) { + let mut group = c.benchmark_group("mmr-belt-open"); + configure_group(&mut group); + + for &size in MMR_BELT_SIZES { + let data = MmrBeltBenchData::build(size); + + group.bench_with_input(BenchmarkId::new("current-mmr-oldest", size), &size, |b, _| { + b.iter(|| { + hint::black_box(data.mmr.open(0).unwrap()); + }); + }); + + group.bench_with_input(BenchmarkId::new("belt-oldest", size), &size, |b, _| { + b.iter(|| { + hint::black_box(data.belt.open(0).unwrap()); + }); + }); + + group.bench_with_input(BenchmarkId::new("current-mmr-newest", size), &size, |b, _| { + b.iter(|| { + hint::black_box(data.mmr.open(size - 1).unwrap()); + }); + }); + + group.bench_with_input(BenchmarkId::new("belt-newest", size), &size, |b, _| { + b.iter(|| { + hint::black_box(data.belt.open(size - 1).unwrap()); + }); + }); + } + + group.finish(); +} + +fn bench_mmr_belt_verify(c: &mut Criterion) { + let mut group = c.benchmark_group("mmr-belt-verify"); + configure_group(&mut group); + + for &size in MMR_BELT_SIZES { + let data = MmrBeltBenchData::build(size); + let oldest_mmr_proof = data.mmr.open(0).unwrap(); + let newest_mmr_proof = data.mmr.open(size - 1).unwrap(); + let oldest_belt_proof = data.belt.open(0).unwrap(); + let newest_belt_proof = data.belt.open(size - 1).unwrap(); + + group.bench_with_input(BenchmarkId::new("current-mmr-oldest", size), &size, |b, _| { + b.iter(|| { + data.peaks + .verify( + hint::black_box(data.leaves[0]), + hint::black_box(oldest_mmr_proof.clone()), + ) + .unwrap(); + }); + }); + + group.bench_with_input( + BenchmarkId::new("current-mmr-committed-oldest", size), + &size, + |b, _| { + let commitment = data.peaks.hash_peaks(); + b.iter(|| { + assert_eq!(hint::black_box(data.peaks.hash_peaks()), commitment); + data.peaks + .verify( + hint::black_box(data.leaves[0]), + hint::black_box(oldest_mmr_proof.clone()), + ) + .unwrap(); + }); + }, + ); + + group.bench_with_input(BenchmarkId::new("belt-oldest", size), &size, |b, _| { + b.iter(|| { + assert!( + hint::black_box(&oldest_belt_proof).verify(hint::black_box(&data.belt_summary)) + ); + }); + }); + + group.bench_with_input(BenchmarkId::new("current-mmr-newest", size), &size, |b, _| { + b.iter(|| { + data.peaks + .verify( + hint::black_box(data.leaves[size - 1]), + hint::black_box(newest_mmr_proof.clone()), + ) + .unwrap(); + }); + }); + + group.bench_with_input( + BenchmarkId::new("current-mmr-committed-newest", size), + &size, + |b, _| { + let commitment = data.peaks.hash_peaks(); + b.iter(|| { + assert_eq!(hint::black_box(data.peaks.hash_peaks()), commitment); + data.peaks + .verify( + hint::black_box(data.leaves[size - 1]), + hint::black_box(newest_mmr_proof.clone()), + ) + .unwrap(); + }); + }, + ); + + group.bench_with_input(BenchmarkId::new("belt-newest", size), &size, |b, _| { + b.iter(|| { + assert!( + hint::black_box(&newest_belt_proof).verify(hint::black_box(&data.belt_summary)) + ); + }); + }); + } + + group.finish(); +} + +fn bench_mmr_belt_recency(c: &mut Criterion) { + let mut group = c.benchmark_group("mmr-belt-recency"); + configure_group(&mut group); + + for &size in &[50_000, 65_536] { + let data = MmrBeltBenchData::build(size); + + for &recency in RECENCIES { + if recency > size { + continue; + } + let position = size - recency; + let mmr_proof = data.mmr.open(position).unwrap(); + let belt_proof = data.belt.open(position).unwrap(); + + group.bench_with_input( + BenchmarkId::new("current-mmr-verify", format!("{size}/{recency}")), + &(size, recency), + |b, _| { + b.iter(|| { + data.peaks + .verify( + hint::black_box(data.leaves[position]), + hint::black_box(mmr_proof.clone()), + ) + .unwrap(); + }); + }, + ); + + let commitment = data.peaks.hash_peaks(); + group.bench_with_input( + BenchmarkId::new("current-mmr-committed-verify", format!("{size}/{recency}")), + &(size, recency), + |b, _| { + b.iter(|| { + assert_eq!(hint::black_box(data.peaks.hash_peaks()), commitment); + data.peaks + .verify( + hint::black_box(data.leaves[position]), + hint::black_box(mmr_proof.clone()), + ) + .unwrap(); + }); + }, + ); + + group.bench_with_input( + BenchmarkId::new("belt-verify", format!("{size}/{recency}")), + &(size, recency), + |b, _| { + b.iter(|| { + assert!( + hint::black_box(&belt_proof) + .verify(hint::black_box(&data.belt_summary)) + ); + }); + }, + ); + } + } + + group.finish(); +} + +fn configure_group( + group: &mut criterion::BenchmarkGroup<'_, M>, +) { + group.measurement_time(MEASUREMENT_TIME); + group.sample_size(SAMPLE_SIZE); +} + +criterion_group!( + mmr_belt_benches, + bench_mmr_belt_build, + bench_mmr_belt_append, + bench_mmr_belt_append_sequence, + bench_mmr_belt_commitment, + bench_mmr_belt_open, + bench_mmr_belt_verify, + bench_mmr_belt_recency, +); +criterion_main!(mmr_belt_benches); diff --git a/miden-crypto/src/merkle/mmr/belt/bagging.rs b/miden-crypto/src/merkle/mmr/belt/bagging.rs new file mode 100644 index 0000000000..dcf19f0541 --- /dev/null +++ b/miden-crypto/src/merkle/mmr/belt/bagging.rs @@ -0,0 +1,586 @@ +use alloc::{format, vec::Vec}; +use core::ops::Range; + +use super::{BeltMountain, MmrError, shape::*}; +use crate::{EMPTY_WORD, Word, hash::poseidon2::Poseidon2}; + +struct AppendChangedRanges { + ranges: [Range; 2], + len: usize, +} + +impl AppendChangedRanges { + fn new() -> Self { + Self { ranges: [0..0, 0..0], len: 0 } + } + + fn push(&mut self, range: Range) -> Result<(), MmrError> { + if self.len == self.ranges.len() { + return Err(MmrError::InvalidUpdate); + } + + self.ranges[self.len] = range; + self.len += 1; + + Ok(()) + } + + fn iter(&self) -> impl Iterator> + '_ { + self.ranges[..self.len].iter().cloned() + } +} + +#[derive(Debug, Clone, Default)] +pub(super) struct BeltBaggingState { + num_leaves: usize, + range_len: usize, + pub(super) ranges: Vec>, + range_roots: Vec, + range_nodes: Vec>, + belt_nodes: Vec, +} + +impl BeltBaggingState { + pub(super) fn from_roots(num_leaves: usize, roots: &[Word]) -> Result { + Self::from_roots_with_stats(num_leaves, roots).map(|(state, _)| state) + } + + pub(super) fn from_roots_with_stats( + num_leaves: usize, + roots: &[Word], + ) -> Result<(Self, usize), MmrError> { + let shape = shape_mountains(num_leaves); + if roots.len() != shape.len() { + return Err(MmrError::InvalidPeaks(format!( + "expected {} peaks for {num_leaves} leaves but got {}", + shape.len(), + roots.len() + ))); + } + + let mut hashes = 0; + let ranges = shape_ranges(&shape); + let mut range_roots = Vec::new(); + let mut range_nodes = Vec::new(); + for range in ranges.iter().cloned() { + let mut nodes = Vec::with_capacity(range.len()); + let mut prefix = EMPTY_WORD; + for &root in &roots[range] { + let node = RangeNode::new(prefix, root); + hashes += 1; + prefix = node.root; + nodes.push(node); + } + range_roots.push(prefix); + range_nodes.push(nodes); + } + + let range_len = range_roots.len(); + let mut belt_nodes = Vec::with_capacity(range_len); + let mut prefix = EMPTY_WORD; + for &range_root in &range_roots { + let node = BeltNode::new(prefix, range_root); + hashes += 1; + prefix = node.root; + belt_nodes.push(node); + } + + Ok(( + Self { + num_leaves, + range_len, + ranges, + range_roots, + range_nodes, + belt_nodes, + }, + hashes, + )) + } + + pub(super) fn append_update( + &mut self, + old_num_leaves: usize, + new_num_leaves: usize, + changed: &[ChangedMountain], + ) -> usize { + debug_assert_eq!(old_num_leaves + 1, new_num_leaves); + debug_assert!(!changed.is_empty()); + + if old_num_leaves == 0 { + debug_assert_eq!(changed.len(), 1); + debug_assert_eq!(changed[0].mountain, (ShapeMountain { start: 0, height: 0 })); + + let range_node = RangeNode::new(EMPTY_WORD, changed[0].root); + let belt_node = BeltNode::new(EMPTY_WORD, range_node.root); + *self = Self { + num_leaves: new_num_leaves, + range_len: 1, + ranges: core::iter::once(0..1).collect(), + range_roots: Vec::from([range_node.root]), + range_nodes: Vec::from([Vec::from([range_node])]), + belt_nodes: Vec::from([belt_node]), + }; + return 2; + } + + debug_assert_eq!(self.num_leaves, old_num_leaves); + debug_assert!(self.range_len <= self.ranges.len()); + debug_assert!(self.range_len <= self.range_roots.len()); + debug_assert!(self.range_len <= self.range_nodes.len()); + debug_assert!(self.range_len <= self.belt_nodes.len()); + + let recompute_idx = append_shape_recompute_index_trusted(old_num_leaves, changed); + let common = shape_range_index_for_num_leaves(old_num_leaves, recompute_idx) + .expect("append recompute index must have an old range"); + let suffix_start = self.ranges[common].start; + + self.num_leaves = new_num_leaves; + + let old_ranges_len = self.range_len; + + let mut hashes = 0; + let mut range_idx = common; + let changed_ranges = append_changed_ranges_trusted(new_num_leaves, suffix_start, changed); + for range in changed_ranges.iter() { + let changed_root = changed_root_for_range_trusted(new_num_leaves, &range, changed); + let range_hashes = self.update_changed_range_trusted( + old_num_leaves, + new_num_leaves, + range_idx, + range.clone(), + changed_root, + old_ranges_len, + ); + hashes += range_hashes; + range_idx += 1; + } + + let new_range_len = range_idx; + for range_idx in common..new_range_len { + let range_root = self.range_roots[range_idx]; + let left = if range_idx == 0 { + EMPTY_WORD + } else { + self.belt_nodes[range_idx - 1].root + }; + let node = BeltNode::new(left, range_root); + hashes += 1; + if range_idx == self.belt_nodes.len() { + self.belt_nodes.push(node); + } else { + self.belt_nodes[range_idx] = node; + } + } + self.range_len = new_range_len; + + hashes + } + + pub(super) fn update_changed_range_trusted( + &mut self, + old_num_leaves: usize, + new_num_leaves: usize, + range_idx: usize, + new_range: Range, + changed_root: Word, + old_ranges_len: usize, + ) -> usize { + debug_assert!(new_range.start < new_range.end); + debug_assert!(range_idx <= self.ranges.len()); + debug_assert!(range_idx <= self.range_roots.len()); + debug_assert!(range_idx <= self.range_nodes.len()); + + let changed_idx = new_range.end - 1; + let changed = changed_idx - new_range.start; + let source_range_idx = if changed == 0 { + None + } else { + let old_idx = shape_mountain_index_for_num_leaves( + old_num_leaves, + shape_mountain_at_index(new_num_leaves, new_range.start) + .expect("new range prefix must start with a mountain"), + ) + .expect("new range prefix must reuse an old mountain"); + let old_range_idx = shape_range_index_for_num_leaves(old_num_leaves, old_idx) + .expect("reused mountain must belong to an old range"); + debug_assert!((range_idx..old_ranges_len).contains(&old_range_idx)); + let old_range = self.ranges[old_range_idx].clone(); + debug_assert_eq!(old_idx, old_range.start); + debug_assert!(old_range.start + changed <= old_range.end); + debug_assert!(self.range_nodes[old_range_idx].len() >= changed); + Some(old_range_idx) + }; + + if range_idx == self.range_nodes.len() { + self.range_nodes.push(Vec::new()); + } + if let Some(source_range_idx) = source_range_idx + && source_range_idx != range_idx + { + self.range_nodes.swap(source_range_idx, range_idx); + } + + let nodes = &mut self.range_nodes[range_idx]; + if changed == 0 { + nodes.clear(); + } else { + nodes.truncate(changed); + } + + let left = nodes.last().map_or(EMPTY_WORD, |node| node.root); + let node = RangeNode::new(left, changed_root); + let root = node.root; + nodes.push(node); + + if range_idx == self.ranges.len() { + self.ranges.push(new_range); + } else { + self.ranges[range_idx] = new_range; + } + if range_idx == self.range_roots.len() { + self.range_roots.push(root); + } else { + self.range_roots[range_idx] = root; + } + + 1 + } + + #[cfg(test)] + pub(super) fn changed_range_nodes( + &mut self, + old_num_leaves: usize, + new_num_leaves: usize, + new_range: Range, + changed_roots: &[ChangedMountain], + first_old_range_idx: usize, + old_ranges_len: usize, + ) -> Result<(Vec, Word, usize), MmrError> { + if new_range.start >= new_range.end { + return Err(MmrError::InvalidUpdate); + } + + let changed_idx = new_range.end - 1; + let changed_mountain = + shape_mountain_at_index(new_num_leaves, changed_idx).ok_or(MmrError::InvalidUpdate)?; + let changed_root = changed_roots + .iter() + .find(|changed| changed.mountain == changed_mountain) + .map(|changed| changed.root) + .ok_or(MmrError::InvalidUpdate)?; + + for changed in changed_roots { + if changed.mountain == changed_mountain { + continue; + } + if let Some(idx) = shape_mountain_index_for_num_leaves(new_num_leaves, changed.mountain) + && new_range.contains(&idx) + { + return Err(MmrError::InvalidUpdate); + } + } + + Ok(self.changed_range_nodes_trusted( + old_num_leaves, + new_num_leaves, + new_range, + changed_root, + first_old_range_idx, + old_ranges_len, + )) + } + + #[cfg(test)] + pub(super) fn changed_range_nodes_trusted( + &mut self, + old_num_leaves: usize, + new_num_leaves: usize, + new_range: Range, + changed_root: Word, + first_old_range_idx: usize, + old_ranges_len: usize, + ) -> (Vec, Word, usize) { + debug_assert!(new_range.start < new_range.end); + + let changed_idx = new_range.end - 1; + let changed = changed_idx - new_range.start; + let mut nodes = if changed == 0 { + Vec::new() + } else { + let old_idx = shape_mountain_index_for_num_leaves( + old_num_leaves, + shape_mountain_at_index(new_num_leaves, new_range.start) + .expect("new range prefix must start with a mountain"), + ) + .expect("new range prefix must reuse an old mountain"); + let old_range_idx = shape_range_index_for_num_leaves(old_num_leaves, old_idx) + .expect("reused mountain must belong to an old range"); + debug_assert!((first_old_range_idx..old_ranges_len).contains(&old_range_idx)); + let old_range = self.ranges[old_range_idx].clone(); + let old_range_nodes = &mut self.range_nodes[old_range_idx]; + debug_assert_eq!(old_idx, old_range.start); + debug_assert!(old_range.start + changed <= old_range.end); + debug_assert!(old_range_nodes.len() >= changed); + + old_range_nodes.truncate(changed); + core::mem::take(old_range_nodes) + }; + + let left = nodes.last().map_or(EMPTY_WORD, |node| node.root); + let node = RangeNode::new(left, changed_root); + let root = node.root; + nodes.push(node); + + (nodes, root, 1) + } + + #[cfg(test)] + pub(super) fn range_roots(&self) -> &[Word] { + &self.range_roots[..self.range_len] + } + + #[cfg(test)] + pub(super) fn range_nodes(&self) -> &[Vec] { + &self.range_nodes[..self.range_len] + } + + #[cfg(test)] + pub(super) fn belt_nodes(&self) -> &[BeltNode] { + &self.belt_nodes[..self.range_len] + } + + pub(super) fn root(&self) -> Word { + if self.range_len == 0 { + EMPTY_WORD + } else { + self.belt_nodes[self.range_len - 1].root + } + } +} + +impl PartialEq for BeltBaggingState { + fn eq(&self, other: &Self) -> bool { + self.num_leaves == other.num_leaves + && self.range_len == other.range_len + && self.ranges.get(..self.range_len) == other.ranges.get(..other.range_len) + && self.range_roots.get(..self.range_len) == other.range_roots.get(..other.range_len) + && self.range_nodes.get(..self.range_len) == other.range_nodes.get(..other.range_len) + && self.belt_nodes.get(..self.range_len) == other.belt_nodes.get(..other.range_len) + } +} + +impl Eq for BeltBaggingState {} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) struct RangeNode { + pub(super) left: Word, + pub(super) right: Word, + pub(super) root: Word, +} + +impl RangeNode { + fn new(left: Word, right: Word) -> Self { + Self { + left, + right, + root: Poseidon2::merge(&[left, right]), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) struct BeltNode { + pub(super) left: Word, + pub(super) right: Word, + pub(super) root: Word, +} + +impl BeltNode { + fn new(left: Word, right: Word) -> Self { + Self { + left, + right, + root: Poseidon2::merge(&[left, right]), + } + } +} + +#[derive(Debug, Clone, Copy)] +pub(super) struct ChangedMountain { + pub(super) mountain: ShapeMountain, + pub(super) root: Word, +} + +impl ChangedMountain { + pub(super) fn new(start: usize, height: usize, root: Word) -> Self { + Self { + mountain: ShapeMountain { start, height }, + root, + } + } + + pub(super) fn from_mountain(mountain: &BeltMountain) -> Self { + Self::new(mountain.start, mountain.height, mountain.root) + } + + pub(super) fn contains_position(&self, position: usize) -> bool { + self.mountain.contains_position(position) + } +} + +fn append_shape_recompute_index_trusted( + old_num_leaves: usize, + changed: &[ChangedMountain], +) -> usize { + let old_shape_len = shape_len_for_num_leaves(old_num_leaves); + debug_assert!(old_shape_len > 0); + + let Some(merged) = changed + .iter() + .map(|changed| changed.mountain) + .find(|mountain| mountain.height > 0) + else { + return old_shape_len - 1; + }; + + let child_height = merged.height - 1; + let left = ShapeMountain { + start: merged.start, + height: child_height, + }; + let left_idx = shape_mountain_index_for_num_leaves(old_num_leaves, left) + .expect("merged left child must be present in the old shape"); + if left_idx == 0 { + return 0; + } + + let old_split = shape_range_split_after_num_leaves(old_num_leaves, left_idx - 1) + .expect("old split before merged child must exist"); + let previous_left = + shape_mountain_at_index(old_num_leaves, left_idx - 1).expect("left neighbor must exist"); + let previous_previous_left = if left_idx >= 2 { + Some( + shape_mountain_at_index(old_num_leaves, left_idx - 2) + .expect("previous left neighbor must exist") + .height, + ) + } else { + None + }; + let new_split = shape_range_split(previous_left.height, merged.height, previous_previous_left); + if old_split == new_split { left_idx } else { left_idx - 1 } +} + +fn append_changed_ranges_trusted( + new_num_leaves: usize, + suffix_start: usize, + changed: &[ChangedMountain], +) -> AppendChangedRanges { + let new_shape_len = shape_len_for_num_leaves(new_num_leaves); + debug_assert!(!changed.is_empty()); + debug_assert!(changed.len() <= 2); + debug_assert!(suffix_start < new_shape_len); + + let mut changed_indices = [0usize; 2]; + let mut changed_len = 0; + for changed in changed { + let idx = shape_mountain_index_for_num_leaves(new_num_leaves, changed.mountain) + .expect("changed append mountain must exist in the new shape"); + debug_assert!(idx >= suffix_start); + changed_indices[changed_len] = idx; + changed_len += 1; + } + + if changed_len == 2 && changed_indices[0] > changed_indices[1] { + changed_indices.swap(0, 1); + } + debug_assert!(changed_len == 1 || changed_indices[0] != changed_indices[1]); + debug_assert_eq!(changed_indices[changed_len - 1], new_shape_len - 1); + + let mut ranges = AppendChangedRanges::new(); + let mut start = suffix_start; + for &end in &changed_indices[..changed_len] { + debug_assert!(end >= start); + ranges.push(start..end + 1).expect("append changes at most two ranges"); + start = end + 1; + } + debug_assert_eq!(start, new_shape_len); + + ranges +} + +fn changed_root_for_range_trusted( + new_num_leaves: usize, + range: &Range, + changed: &[ChangedMountain], +) -> Word { + let changed_mountain = shape_mountain_at_index(new_num_leaves, range.end - 1) + .expect("changed range must end at a mountain"); + changed + .iter() + .find(|changed| changed.mountain == changed_mountain) + .expect("changed range must end at an append-changed mountain") + .root +} + +#[cfg(test)] +pub(super) fn append_shape_in_place( + shape: &mut Vec, + old_num_leaves: usize, + changed: &[ChangedMountain], +) -> Result<(), MmrError> { + let leaf = ShapeMountain { start: old_num_leaves, height: 0 }; + let merged = changed + .iter() + .map(|changed| changed.mountain) + .find(|mountain| mountain.height > 0); + let leaf_expected = merged.is_none_or(|merged| !merged.contains_position(old_num_leaves)); + let expected_len = usize::from(merged.is_some()) + usize::from(leaf_expected); + if changed.len() != expected_len + || (leaf_expected && !changed.iter().any(|changed| changed.mountain == leaf)) + || changed.iter().any(|changed| { + Some(changed.mountain) != merged && !(leaf_expected && changed.mountain == leaf) + }) + { + return Err(MmrError::InvalidUpdate); + } + + let Some(merged) = merged else { + shape.push(leaf); + return Ok(()); + }; + + let child_height = merged.height - 1; + let left = ShapeMountain { + start: merged.start, + height: child_height, + }; + let right = ShapeMountain { + start: merged.start + (1usize << child_height), + height: child_height, + }; + let left_idx = + shape_mountain_index_for_num_leaves(old_num_leaves, left).ok_or(MmrError::InvalidUpdate)?; + if leaf_expected { + let right_idx = left_idx + 1; + let old_len = shape.len(); + if shape.get(right_idx).copied() != Some(right) { + return Err(MmrError::InvalidUpdate); + } + + shape[left_idx] = merged; + if right_idx + 1 < old_len { + shape.copy_within(right_idx + 1..old_len, right_idx); + } + shape[old_len - 1] = leaf; + } else { + if shape.get(left_idx).copied() != Some(left) || left_idx + 1 != shape.len() { + return Err(MmrError::InvalidUpdate); + } + + shape[left_idx] = merged; + } + + Ok(()) +} diff --git a/miden-crypto/src/merkle/mmr/belt/delta.rs b/miden-crypto/src/merkle/mmr/belt/delta.rs new file mode 100644 index 0000000000..27ac5beede --- /dev/null +++ b/miden-crypto/src/merkle/mmr/belt/delta.rs @@ -0,0 +1,348 @@ +use alloc::{ + collections::{BTreeMap, BTreeSet}, + vec::Vec, +}; + +use super::{BeltProof, BeltSummary, MmrError, proof::*, shape::*}; +use crate::Word; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MmrBeltDelta { + pub(super) from_num_leaves: usize, + pub(super) to_num_leaves: usize, + pub(super) new_tail_peaks: Vec, + pub(super) merge_auth: BTreeMap<(usize, usize), Word>, +} + +impl MmrBeltDelta { + pub fn from_parts( + from_num_leaves: usize, + to_num_leaves: usize, + new_tail_peaks: Vec, + merge_auth_nodes: I, + ) -> Result + where + I: IntoIterator, + { + if from_num_leaves > to_num_leaves { + return Err(MmrError::ForestOutOfBounds(from_num_leaves, to_num_leaves)); + } + // `to_num_leaves` bounds `from_num_leaves`, so validating it guards both shape derivations. + validate_num_leaves(to_num_leaves)?; + + let common = common_peak_prefix_len(from_num_leaves, to_num_leaves); + let new_len = shape_len_for_num_leaves(to_num_leaves); + if common + new_tail_peaks.len() != new_len { + return Err(MmrError::InvalidUpdate); + } + + let mut merge_auth = BTreeMap::new(); + for ((start, height), value) in merge_auth_nodes { + if merge_auth.insert((start, height), value).is_some() { + return Err(MmrError::InvalidUpdate); + } + } + + Ok(Self { + from_num_leaves, + to_num_leaves, + new_tail_peaks, + merge_auth, + }) + } + + pub fn from_num_leaves(&self) -> usize { + self.from_num_leaves + } + + pub fn to_num_leaves(&self) -> usize { + self.to_num_leaves + } + + pub fn new_tail_peaks(&self) -> &[Word] { + &self.new_tail_peaks + } + + pub fn merge_auth_nodes(&self) -> impl Iterator + '_ { + self.merge_auth + .iter() + .map(|(&(start, height), &value)| ((start, height), value)) + } + + pub fn num_merge_auth_nodes(&self) -> usize { + self.merge_auth.len() + } + + pub fn verify_transition( + &self, + old_summary: &BeltSummary, + new_summary: &BeltSummary, + ) -> Result { + if old_summary.num_leaves() != self.from_num_leaves + || new_summary.num_leaves() != self.to_num_leaves + { + return Ok(false); + } + + let old_shape = shape_mountains(self.from_num_leaves); + let new_shape = shape_mountains(self.to_num_leaves); + let common = common_peak_prefix_len_from_shapes(&old_shape, &new_shape); + + let roots = self.apply_with_common(common, old_summary.roots())?; + let derived = BeltSummary::from_roots(self.to_num_leaves, &roots)?; + + Ok(&derived == new_summary + && self.authenticates_absorbed_roots( + old_summary, + new_summary, + &old_shape, + &new_shape, + common, + )?) + } + + fn authenticates_absorbed_roots( + &self, + old_summary: &BeltSummary, + new_summary: &BeltSummary, + old_shape: &[ShapeMountain], + new_shape: &[ShapeMountain], + common: usize, + ) -> Result { + let mut required = BTreeSet::new(); + for (old_mountain, &old_root) in old_shape.iter().zip(old_summary.roots()).skip(common) { + let new_idx = shape_mountain_for_position(new_shape, old_mountain.start) + .ok_or(MmrError::InvalidUpdate)?; + let new_mountain = new_shape[new_idx]; + let old_end = old_mountain + .start + .checked_add(old_mountain.size()) + .and_then(|end| end.checked_sub(1)) + .ok_or(MmrError::InvalidUpdate)?; + if new_mountain.height < old_mountain.height || !new_mountain.contains_position(old_end) + { + return Err(MmrError::InvalidUpdate); + } + + let mut root = old_root; + for (side, sibling_start, height) in + climb_to_peak(old_mountain.start, old_mountain.height, new_mountain.height) + { + required.insert((sibling_start, height)); + let &sibling = + self.merge_auth.get(&(sibling_start, height)).ok_or(MmrError::InvalidUpdate)?; + root = merge_with_side(side, root, sibling); + } + + if root != new_summary.roots()[new_idx] { + return Ok(false); + } + } + + // Every required key is present (the climb would have errored otherwise), so an equal count + // rejects deltas padded with auth nodes outside the required set. + if self.merge_auth.len() != required.len() { + return Ok(false); + } + + Ok(true) + } + + pub fn apply(&self, old_peaks: &[Word]) -> Result, MmrError> { + let common = common_peak_prefix_len(self.from_num_leaves, self.to_num_leaves); + self.apply_with_common(common, old_peaks) + } + + fn apply_with_common(&self, common: usize, old_peaks: &[Word]) -> Result, MmrError> { + let old_len = shape_len_for_num_leaves(self.from_num_leaves); + if old_peaks.len() != old_len { + return Err(MmrError::InvalidPeaks(format!( + "expected {old_len} peaks for {} leaves but got {}", + self.from_num_leaves, + old_peaks.len() + ))); + } + + let new_len = shape_len_for_num_leaves(self.to_num_leaves); + if common + self.new_tail_peaks.len() != new_len { + return Err(MmrError::InvalidUpdate); + } + + let mut peaks = old_peaks[..common].to_vec(); + peaks.extend_from_slice(&self.new_tail_peaks); + + Ok(peaks) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PartialMmrBelt { + num_leaves: usize, + peaks: Vec, + tracked: BTreeMap, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct TrackedLeaf { + leaf: Word, + mountain_start: usize, + mountain_height: usize, + within_path: Vec, +} + +impl PartialMmrBelt { + pub fn from_peaks(num_leaves: usize, peaks: Vec) -> Result { + validate_num_leaves(num_leaves)?; + let expected = shape_len_for_num_leaves(num_leaves); + if peaks.len() != expected { + return Err(MmrError::InvalidPeaks(format!( + "expected {expected} peaks for {num_leaves} leaves but got {}", + peaks.len() + ))); + } + + Ok(Self { + num_leaves, + peaks, + tracked: BTreeMap::new(), + }) + } + + pub fn num_leaves(&self) -> usize { + self.num_leaves + } + + pub fn peaks(&self) -> &[Word] { + &self.peaks + } + + pub fn summary(&self) -> BeltSummary { + BeltSummary::from_roots(self.num_leaves, &self.peaks) + .expect("partial mountain-order summary must match the current shape") + } + + pub fn is_tracked(&self, pos: usize) -> bool { + self.tracked.contains_key(&pos) + } + + pub fn num_tracked(&self) -> usize { + self.tracked.len() + } + + pub fn get(&self, pos: usize) -> Option { + self.tracked.get(&pos).map(|tracked| tracked.leaf) + } + + pub fn track(&mut self, proof: &BeltProof) -> Result<(), MmrError> { + // Like `PartialMmr::track`, an out-of-range or otherwise invalid proof is rejected by + // verification (`BeltProof::verify` returns `false` for `position >= num_leaves`) rather + // than by a dedicated position check. + if !proof.verify(&self.summary()) { + return Err(MmrError::PeakPathMismatch); + } + + let shape = shape_mountains(self.num_leaves); + let mountain_idx = shape_mountain_for_position(&shape, proof.position()) + .ok_or(MmrError::PositionNotFound(proof.position()))?; + let mountain = shape[mountain_idx]; + + self.tracked.insert( + proof.position(), + TrackedLeaf { + leaf: proof.leaf(), + mountain_start: mountain.start, + mountain_height: mountain.height, + within_path: proof.nodes[..mountain.height].to_vec(), + }, + ); + + Ok(()) + } + + pub fn untrack(&mut self, pos: usize) -> bool { + self.tracked.remove(&pos).is_some() + } + + pub fn open(&self, pos: usize) -> Result, MmrError> { + if pos >= self.num_leaves { + return Err(MmrError::PositionNotFound(pos)); + } + + let Some(tracked) = self.tracked.get(&pos) else { + return Ok(None); + }; + + let shape = shape_mountains(self.num_leaves); + let mountain_idx = + shape_mountain_for_position(&shape, pos).ok_or(MmrError::PositionNotFound(pos))?; + + let mut nodes = tracked.within_path.clone(); + nodes.extend(bagging_path_nodes(&self.peaks, &shape_ranges(&shape), mountain_idx)); + + Ok(Some(BeltProof { position: pos, leaf: tracked.leaf, nodes })) + } + + pub fn apply_verified( + &mut self, + delta: &MmrBeltDelta, + new_summary: &BeltSummary, + ) -> Result<(), MmrError> { + if !delta.verify_transition(&self.summary(), new_summary)? { + return Err(MmrError::InvalidUpdate); + } + + // `apply` is transactional, so a failure here cannot leave the view partially advanced. + self.apply(delta) + } + + pub fn apply(&mut self, delta: &MmrBeltDelta) -> Result<(), MmrError> { + if delta.from_num_leaves() != self.num_leaves { + // Mirror `PartialMmr::apply`, which reports a descriptive error when a delta does not + // line up with the current state rather than a bare `InvalidUpdate`. + return Err(MmrError::InvalidPeaks(format!( + "delta starts at {} leaves but the partial belt has {} leaves", + delta.from_num_leaves(), + self.num_leaves + ))); + } + + // Build the new state in locals and commit only after every fallible step succeeds. + let new_peaks = delta.apply(&self.peaks)?; + let new_num_leaves = delta.to_num_leaves(); + let new_shape = shape_mountains(new_num_leaves); + + let mut new_tracked = BTreeMap::new(); + for (&pos, tracked) in &self.tracked { + let mountain_idx = shape_mountain_for_position(&new_shape, pos) + .ok_or(MmrError::PositionNotFound(pos))?; + let new_mountain = new_shape[mountain_idx]; + + let mut within_path = tracked.within_path.clone(); + for (side, sibling_start, height) in + climb_to_peak(tracked.mountain_start, tracked.mountain_height, new_mountain.height) + { + let &value = delta + .merge_auth + .get(&(sibling_start, height)) + .ok_or(MmrError::InvalidUpdate)?; + within_path.push(BeltProofNode { value, side }); + } + + new_tracked.insert( + pos, + TrackedLeaf { + leaf: tracked.leaf, + mountain_start: new_mountain.start, + mountain_height: new_mountain.height, + within_path, + }, + ); + } + + self.peaks = new_peaks; + self.num_leaves = new_num_leaves; + self.tracked = new_tracked; + + Ok(()) + } +} diff --git a/miden-crypto/src/merkle/mmr/belt/mod.rs b/miden-crypto/src/merkle/mmr/belt/mod.rs new file mode 100644 index 0000000000..99d664c0c5 --- /dev/null +++ b/miden-crypto/src/merkle/mmr/belt/mod.rs @@ -0,0 +1,619 @@ +use alloc::{collections::BTreeMap, vec::Vec}; + +use super::{Forest, MmrError}; +use crate::{EMPTY_WORD, Word, hash::poseidon2::Poseidon2}; + +mod bagging; +mod delta; +mod proof; +mod shape; +use bagging::*; +pub use delta::{MmrBeltDelta, PartialMmrBelt}; +pub use proof::BeltProof; +use proof::{BeltProofNode, bagging_path_nodes}; +use shape::*; + +/// Experimental Merkle Mountain Belt prototype. +#[derive(Debug, Clone, Default)] +pub struct MmrBelt { + mountains: Vec>, + hashes: BeltHashArray, + bagging: BeltBaggingState, + bagging_dirty: bool, + last_bagging_update_hashes: usize, + free_mountain_slots: Vec, + head: Option, + tail: Option, + rightmost_mergeable: Option, + num_leaves: usize, +} + +impl MmrBelt { + pub fn new() -> Self { + Self::default() + } + + pub fn add(&mut self, leaf: Word) -> Result { + self.add_with_bagging_mode(leaf, true) + } + + /// Appends a leaf without refreshing the live summary state. + /// + /// [`Self::summary`] and [`Self::commitment_root`] remain safe to call; they rebuild the + /// derived summary from current mountain roots while the live summary state is dirty. + pub fn add_deferred(&mut self, leaf: Word) -> Result { + self.add_with_bagging_mode(leaf, false) + } + + /// Benchmark-only alias for deferred append. + #[cfg(any(test, feature = "internal"))] + pub fn add_without_bagging_for_benchmark(&mut self, leaf: Word) -> Result { + self.add_deferred(leaf) + } + + fn add_with_bagging_mode( + &mut self, + leaf: Word, + refresh_bagging: bool, + ) -> Result { + if self.num_leaves >= Forest::MAX_LEAVES { + return Err(MmrError::ForestSizeExceeded { + requested: self.num_leaves.saturating_add(1), + max: Forest::MAX_LEAVES, + }); + } + + let old_num_leaves = self.num_leaves; + let leaf_position = self.num_leaves; + let leaf_index = leaf_hash_index(leaf_position); + self.hashes.set(leaf_index, leaf); + + let new_idx = self.push_mountain(BeltMountain::new(leaf_position, leaf_index, leaf)); + self.num_leaves += 1; + if let Some(prev_idx) = self.mountain_slot(new_idx).prev { + self.track_mergeable_pair(prev_idx, new_idx); + } + + let merged = self.rightmost_mergeable.map(|right_idx| self.merge_pair(right_idx)); + let num_merges = usize::from(merged.is_some()); + + if refresh_bagging { + if self.bagging_dirty { + self.rebuild_bagging_state(); + } else { + let leaf_change = ChangedMountain::new(leaf_position, 0, leaf); + let mut changed = [leaf_change; 2]; + let mut changed_len = 0; + + if let Some(merged) = merged { + changed[changed_len] = merged; + changed_len += 1; + if !merged.contains_position(leaf_position) { + changed[changed_len] = leaf_change; + changed_len += 1; + } + } else { + changed[changed_len] = leaf_change; + changed_len += 1; + } + + self.refresh_bagging_state(old_num_leaves, &changed[..changed_len]); + } + } else { + self.bagging_dirty = true; + self.last_bagging_update_hashes = 0; + } + + Ok(num_merges) + } + + #[cfg(test)] + fn mountain_heights(&self) -> Vec { + self.ordered_mountains().iter().map(|mountain| mountain.height).collect() + } + + #[cfg(test)] + fn storage_slots_for_testing(&self) -> usize { + self.mountains.len() + } + + #[cfg(test)] + fn rightmost_mergeable_pair_for_testing(&self) -> Option<(usize, usize)> { + let right_idx = self.rightmost_mergeable?; + let right = self.mountain_slot(right_idx); + let left_idx = right.prev?; + Some((self.mountain_slot(left_idx).mountain.start, right.mountain.start)) + } + + #[cfg(test)] + fn live_range_roots_for_testing(&self) -> &[Word] { + self.bagging.range_roots() + } + + #[cfg(test)] + fn live_commitment_root_for_testing(&self) -> Word { + self.bagging.root() + } + + #[cfg(test)] + fn last_bagging_update_hashes_for_testing(&self) -> usize { + self.last_bagging_update_hashes + } + + pub fn num_leaves(&self) -> usize { + self.num_leaves + } + + pub fn peaks(&self) -> Vec { + self.ordered_mountains().iter().map(|mountain| mountain.root).collect() + } + + pub fn commitment_root(&self) -> Word { + if self.bagging_dirty { + return self.rebuilt_summary().commitment_root(); + } + + self.bagging.root() + } + + pub fn summary(&self) -> BeltSummary { + if self.bagging_dirty { + return self.rebuilt_summary(); + } + + BeltSummary::from_roots_and_bagging(self.num_leaves, self.peaks(), &self.bagging) + .expect("live mountain-order summary must match the current shape") + } + + pub fn delta(&self, from_num_leaves: usize) -> Result { + if from_num_leaves > self.num_leaves { + return Err(MmrError::ForestOutOfBounds(from_num_leaves, self.num_leaves)); + } + + let common = common_peak_prefix_len(from_num_leaves, self.num_leaves); + let peaks = self.peaks(); + + let from_shape = shape_mountains(from_num_leaves); + let to_shape = shape_mountains(self.num_leaves); + let mut merge_auth = BTreeMap::new(); + + for absorbed in &from_shape[common..] { + let to_idx = shape_mountain_for_position(&to_shape, absorbed.start) + .expect("absorbed leaves still exist in the new state"); + let to_height = to_shape[to_idx].height; + + for (_, sibling_start, height) in + climb_to_peak(absorbed.start, absorbed.height, to_height) + { + merge_auth + .entry((sibling_start, height)) + .or_insert_with(|| self.node_at(sibling_start, height)); + } + } + + Ok(MmrBeltDelta { + from_num_leaves, + to_num_leaves: self.num_leaves, + new_tail_peaks: peaks[common..].to_vec(), + merge_auth, + }) + } + + fn node_at(&self, start: usize, height: usize) -> Word { + self.hash_at(node_hash_index(start, height)) + } + + fn hash_at(&self, index: HashIndex) -> Word { + self.hashes.get(index).expect("hash must be present in storage") + } + + fn refresh_bagging_state(&mut self, old_num_leaves: usize, changed: &[ChangedMountain]) { + let hashes = self.bagging.append_update(old_num_leaves, self.num_leaves, changed); + self.bagging_dirty = false; + self.last_bagging_update_hashes = hashes; + } + + fn rebuild_bagging_state(&mut self) { + let peaks = self.peaks(); + let (bagging, hashes) = BeltBaggingState::from_roots_with_stats(self.num_leaves, &peaks) + .expect("current mountain roots must match the belt shape"); + self.bagging = bagging; + self.bagging_dirty = false; + self.last_bagging_update_hashes = hashes; + } + + fn rebuilt_summary(&self) -> BeltSummary { + BeltSummary::from_roots(self.num_leaves, &self.peaks()) + .expect("current mountain roots must match the belt shape") + } + + pub fn open(&self, position: usize) -> Result { + if position >= self.num_leaves { + return Err(MmrError::PositionNotFound(position)); + } + + let mountains = self.ordered_mountains(); + let shape = shape_from_mountains(&mountains); + let mountain_idx = shape_mountain_for_position(&shape, position) + .ok_or(MmrError::PositionNotFound(position))?; + let mountain = shape[mountain_idx]; + let (leaf, mut nodes) = self.open_within_mountain(position, mountain); + + let peaks = mountains.iter().map(|mountain| mountain.root).collect::>(); + nodes.extend(bagging_path_nodes(&peaks, &shape_ranges(&shape), mountain_idx)); + + Ok(BeltProof { position, leaf, nodes }) + } + + fn open_within_mountain( + &self, + position: usize, + mountain: ShapeMountain, + ) -> (Word, Vec) { + let leaf = self.node_at(position, 0); + let nodes = climb_to_peak(position, 0, mountain.height) + .map(|(side, sibling_start, height)| BeltProofNode { + value: self.node_at(sibling_start, height), + side, + }) + .collect(); + + (leaf, nodes) + } + + #[cfg(test)] + fn range_heights(&self) -> Vec> { + let mountains = self.ordered_mountains(); + let shape = shape_from_mountains(&mountains); + shape_ranges(&shape) + .into_iter() + .map(|range| shape[range].iter().map(|mountain| mountain.height).collect()) + .collect() + } + + fn push_mountain(&mut self, mountain: BeltMountain) -> usize { + let slot = BeltMountainSlot { + mountain, + prev: self.tail, + next: None, + mergeable_prev: None, + mergeable_next: None, + in_mergeable_list: false, + }; + + let idx = if let Some(idx) = self.free_mountain_slots.pop() { + self.mountains[idx] = Some(slot); + idx + } else { + let idx = self.mountains.len(); + self.mountains.push(Some(slot)); + idx + }; + + if let Some(tail_idx) = self.tail { + self.mountain_slot_mut(tail_idx).next = Some(idx); + } else { + self.head = Some(idx); + } + self.tail = Some(idx); + + idx + } + + fn merge_pair(&mut self, right_idx: usize) -> ChangedMountain { + let left_idx = self + .mountain_slot(right_idx) + .prev + .expect("right member of mergeable pair must have left neighbor"); + let right_next = self.mountain_slot(right_idx).next; + let left_prev = self.mountain_slot(left_idx).prev; + let insert_after = if self.mountain_slot(left_idx).in_mergeable_list { + self.mountain_slot(left_idx).mergeable_prev + } else { + self.mountain_slot(right_idx).mergeable_prev + }; + + self.untrack_mergeable_pair(right_idx); + self.untrack_mergeable_pair(left_idx); + + let right = self.mountains[right_idx] + .take() + .expect("right member of mergeable pair must be active"); + let left = self.mountains[left_idx] + .take() + .expect("left member of mergeable pair must be active"); + let root = Poseidon2::merge(&[left.mountain.root, right.mountain.root]); + let merged = left.mountain.merge(right.mountain, root); + let changed = ChangedMountain::from_mountain(&merged); + self.hashes.set(merged.index, root); + + self.mountains[left_idx] = Some(BeltMountainSlot { + mountain: merged, + prev: left_prev, + next: right_next, + mergeable_prev: None, + mergeable_next: None, + in_mergeable_list: false, + }); + + if let Some(next_idx) = right_next { + self.mountain_slot_mut(next_idx).prev = Some(left_idx); + } else { + self.tail = Some(left_idx); + } + self.free_mountain_slots.push(right_idx); + + if let Some(prev_idx) = left_prev { + self.track_mergeable_pair_after(insert_after, prev_idx, left_idx); + } + if let Some(next_idx) = right_next { + self.track_mergeable_pair_after(self.rightmost_mergeable, left_idx, next_idx); + } + + changed + } + + fn track_mergeable_pair(&mut self, left_idx: usize, right_idx: usize) { + self.track_mergeable_pair_after(self.rightmost_mergeable, left_idx, right_idx); + } + + fn track_mergeable_pair_after( + &mut self, + prev_pair: Option, + left_idx: usize, + right_idx: usize, + ) { + if !self.is_mergeable_pair(left_idx, right_idx) + || self.mountain_slot(right_idx).in_mergeable_list + { + return; + } + + if let Some(prev_idx) = prev_pair { + self.mountain_slot_mut(prev_idx).mergeable_next = Some(right_idx); + } + + let slot = self.mountain_slot_mut(right_idx); + slot.mergeable_prev = prev_pair; + slot.mergeable_next = None; + slot.in_mergeable_list = true; + self.rightmost_mergeable = Some(right_idx); + } + + fn untrack_mergeable_pair(&mut self, right_idx: usize) { + let Some(slot) = self.mountains.get(right_idx).and_then(Option::as_ref) else { + return; + }; + if !slot.in_mergeable_list { + return; + } + + let prev = slot.mergeable_prev; + let next = slot.mergeable_next; + + if let Some(prev_idx) = prev { + self.mountain_slot_mut(prev_idx).mergeable_next = next; + } + if let Some(next_idx) = next { + self.mountain_slot_mut(next_idx).mergeable_prev = prev; + } else { + self.rightmost_mergeable = prev; + } + + let slot = self.mountain_slot_mut(right_idx); + slot.mergeable_prev = None; + slot.mergeable_next = None; + slot.in_mergeable_list = false; + } + + fn is_mergeable_pair(&self, left_idx: usize, right_idx: usize) -> bool { + let Some(left_slot) = self.mountains.get(left_idx).and_then(Option::as_ref) else { + return false; + }; + let Some(right_slot) = self.mountains.get(right_idx).and_then(Option::as_ref) else { + return false; + }; + + left_slot.next == Some(right_idx) + && right_slot.prev == Some(left_idx) + && left_slot.mountain.height == right_slot.mountain.height + } + + fn ordered_mountains(&self) -> Vec<&BeltMountain> { + let mut mountains = Vec::new(); + let mut next = self.head; + while let Some(idx) = next { + let slot = self.mountain_slot(idx); + mountains.push(&slot.mountain); + next = slot.next; + } + mountains + } + + fn mountain_slot(&self, idx: usize) -> &BeltMountainSlot { + self.mountains[idx].as_ref().expect("mountain slot must be active") + } + + fn mountain_slot_mut(&mut self, idx: usize) -> &mut BeltMountainSlot { + self.mountains[idx].as_mut().expect("mountain slot must be active") + } +} + +#[derive(Debug, Clone)] +struct BeltMountainSlot { + mountain: BeltMountain, + prev: Option, + next: Option, + mergeable_prev: Option, + mergeable_next: Option, + in_mergeable_list: bool, +} + +#[derive(Debug, Clone, Default)] +struct BeltHashArray { + nodes: Vec, + present: Vec, +} + +impl BeltHashArray { + fn get(&self, index: HashIndex) -> Option { + self.is_present(index).then(|| self.nodes[index.0]) + } + + fn set(&mut self, index: HashIndex, value: Word) { + if self.nodes.len() <= index.0 { + self.nodes.resize(index.0 + 1, EMPTY_WORD); + } + let present_word = index.0 / u64::BITS as usize; + if self.present.len() <= present_word { + self.present.resize(present_word + 1, 0); + } + + self.nodes[index.0] = value; + self.present[present_word] |= 1u64 << (index.0 % u64::BITS as usize); + } + + fn is_present(&self, index: HashIndex) -> bool { + let present_word = index.0 / u64::BITS as usize; + self.present + .get(present_word) + .is_some_and(|word| word & (1u64 << (index.0 % u64::BITS as usize)) != 0) + } + + #[cfg(test)] + fn clear(&mut self) { + self.nodes.clear(); + self.present.clear(); + } + + #[cfg(test)] + fn slot_count(&self) -> usize { + self.nodes.len() + } + + #[cfg(test)] + fn live_count(&self) -> usize { + self.present.iter().map(|word| word.count_ones() as usize).sum() + } + + #[cfg(test)] + fn value_slot_bytes_for_testing() -> usize { + size_of::() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BeltSummary { + pub(super) num_leaves: usize, + roots: Vec, + commitment_root: Word, + #[cfg(test)] + range_roots: Vec, +} + +impl BeltSummary { + pub fn from_roots(num_leaves: usize, roots: &[Word]) -> Result { + validate_num_leaves(num_leaves)?; + let bagging = BeltBaggingState::from_roots(num_leaves, roots)?; + + Ok(Self { + num_leaves, + roots: roots.to_vec(), + commitment_root: bagging.root(), + #[cfg(test)] + range_roots: bagging.range_roots().to_vec(), + }) + } + + pub fn from_peaks(num_leaves: usize, peaks: &[Word]) -> Result { + Self::from_roots(num_leaves, peaks) + } + + fn from_roots_and_bagging( + num_leaves: usize, + roots: Vec, + bagging: &BeltBaggingState, + ) -> Result { + let expected = shape_len_for_num_leaves(num_leaves); + if roots.len() != expected { + return Err(MmrError::InvalidPeaks(format!( + "expected {expected} peaks for {num_leaves} leaves but got {}", + roots.len() + ))); + } + + Ok(Self { + num_leaves, + roots, + commitment_root: bagging.root(), + #[cfg(test)] + range_roots: bagging.range_roots().to_vec(), + }) + } + + pub fn num_leaves(&self) -> usize { + self.num_leaves + } + + pub fn roots(&self) -> &[Word] { + &self.roots + } + + pub fn commitment_root(&self) -> Word { + self.commitment_root + } + + pub fn root(&self) -> Word { + self.commitment_root() + } + + #[cfg(test)] + fn range_roots_for_testing(&self) -> &[Word] { + &self.range_roots + } +} + +#[derive(Debug, Clone)] +pub(super) struct BeltMountain { + pub(super) start: usize, + pub(super) height: usize, + index: HashIndex, + pub(super) root: Word, +} + +impl BeltMountain { + fn new(start: usize, index: HashIndex, root: Word) -> Self { + Self { start, height: 0, index, root } + } + + fn merge(self, other: Self, root: Word) -> Self { + debug_assert_eq!(self.height, other.height); + debug_assert_eq!(self.start + self.size(), other.start); + + let index = parent_hash_index(self.index); + debug_assert_eq!(index, parent_hash_index(other.index)); + Self { + start: self.start, + height: self.height + 1, + index, + root, + } + } + + fn size(&self) -> usize { + 1usize << self.height + } +} + +fn shape_from_mountains(mountains: &[&BeltMountain]) -> Vec { + mountains + .iter() + .map(|mountain| ShapeMountain { + start: mountain.start, + height: mountain.height, + }) + .collect() +} + +#[cfg(test)] +mod tests; diff --git a/miden-crypto/src/merkle/mmr/belt/proof.rs b/miden-crypto/src/merkle/mmr/belt/proof.rs new file mode 100644 index 0000000000..25482f185e --- /dev/null +++ b/miden-crypto/src/merkle/mmr/belt/proof.rs @@ -0,0 +1,138 @@ +use alloc::vec::Vec; +use core::ops::Range; + +use super::{BeltSummary, shape::*}; +use crate::Word; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BeltProof { + pub(super) position: usize, + pub(super) leaf: Word, + pub(super) nodes: Vec, +} + +impl BeltProof { + pub fn position(&self) -> usize { + self.position + } + + pub fn leaf(&self) -> Word { + self.leaf + } + + pub fn verify(&self, summary: &BeltSummary) -> bool { + if self.position >= summary.num_leaves { + return false; + } + + let Some(steps) = proof_steps_for_position(summary.num_leaves, self.position) else { + return false; + }; + + if self.nodes.len() != steps.len() + || self.nodes.iter().zip(&steps).any(|(node, side)| node.side != *side) + { + return false; + } + + let root = + self.nodes.iter().zip(&steps).fold(self.leaf, |current, (node, &side)| { + merge_with_side(side, current, node.value) + }); + + root == summary.commitment_root() + } + + #[cfg(test)] + pub(super) fn set_leaf_for_testing(&mut self, leaf: Word) { + self.leaf = leaf; + } + + #[cfg(test)] + pub(super) fn set_position_for_testing(&mut self, position: usize) { + self.position = position; + } + + #[cfg(test)] + pub(super) fn tamper_node_value_for_testing(&mut self, index: usize, value: Word) { + self.nodes[index].value = value; + } + + #[cfg(test)] + pub(super) fn node_count_for_testing(&self) -> usize { + self.nodes.len() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) struct BeltProofNode { + pub(super) value: Word, + pub(super) side: SiblingSide, +} + +pub(super) fn bagging_path_nodes( + peaks: &[Word], + ranges: &[Range], + mountain_idx: usize, +) -> Vec { + let range_idx = ranges + .iter() + .position(|range| range.contains(&mountain_idx)) + .expect("mountain must be part of a range"); + let range = ranges[range_idx].clone(); + + let mut nodes = Vec::new(); + + let prefix = bag_range(&peaks[range.start..mountain_idx]); + nodes.push(BeltProofNode { value: prefix, side: SiblingSide::Left }); + for &peak in &peaks[mountain_idx + 1..range.end] { + nodes.push(BeltProofNode { value: peak, side: SiblingSide::Right }); + } + + // Exclude the proven range root from the belt path. + let range_root = |range: &Range| bag_range(&peaks[range.clone()]); + let prefix_belt = bag_belt(&ranges[..range_idx].iter().map(range_root).collect::>()); + nodes.push(BeltProofNode { + value: prefix_belt, + side: SiblingSide::Left, + }); + for range in &ranges[range_idx + 1..] { + nodes.push(BeltProofNode { + value: range_root(range), + side: SiblingSide::Right, + }); + } + + nodes +} + +pub(super) fn proof_steps_for_position( + num_leaves: usize, + position: usize, +) -> Option> { + if position >= num_leaves { + return None; + } + + let shape = shape_mountains(num_leaves); + let mountain_idx = shape_mountain_for_position(&shape, position)?; + let mountain = shape[mountain_idx]; + + let mut steps = balanced_tree_sides(mountain.height, position - mountain.start); + + let ranges = shape_ranges(&shape); + let range_idx = ranges.iter().position(|range| range.contains(&mountain_idx))?; + let range = ranges[range_idx].clone(); + + steps.push(SiblingSide::Left); + for _ in &shape[mountain_idx + 1..range.end] { + steps.push(SiblingSide::Right); + } + + steps.push(SiblingSide::Left); + for _ in (range_idx + 1)..ranges.len() { + steps.push(SiblingSide::Right); + } + + Some(steps) +} diff --git a/miden-crypto/src/merkle/mmr/belt/shape.rs b/miden-crypto/src/merkle/mmr/belt/shape.rs new file mode 100644 index 0000000000..245da5f520 --- /dev/null +++ b/miden-crypto/src/merkle/mmr/belt/shape.rs @@ -0,0 +1,315 @@ +use alloc::vec::Vec; +use core::ops::Range; + +use super::super::{Forest, MmrError}; +use crate::{EMPTY_WORD, Word, hash::poseidon2::Poseidon2}; + +/// Rejects forest sizes past [`Forest::MAX_LEAVES`] so `num_leaves + 1` cannot overflow in the +/// shape helpers, and the public constructors error rather than panic on oversized counts. +pub(super) fn validate_num_leaves(num_leaves: usize) -> Result<(), MmrError> { + if Forest::is_valid_size(num_leaves) { + Ok(()) + } else { + Err(MmrError::ForestSizeExceeded { + requested: num_leaves, + max: Forest::MAX_LEAVES, + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum SiblingSide { + Left, + Right, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) struct HashIndex(pub(super) usize); + +pub(super) fn leaf_hash_index(position: usize) -> HashIndex { + HashIndex(2 * (position + 1) + 1) +} + +pub(super) fn parent_hash_index(child: HashIndex) -> HashIndex { + let span = 1usize << (child.0.trailing_zeros() as usize + 2); + HashIndex(child.0 + child.0 % span) +} + +#[cfg(test)] +pub(super) fn hash_children(parent: HashIndex) -> (HashIndex, HashIndex) { + debug_assert!(parent.0.is_multiple_of(2)); + let span = 1usize << (parent.0.trailing_zeros() as usize - 1); + (HashIndex(parent.0 - 3 * span), HashIndex(parent.0 - span)) +} + +pub(super) fn node_hash_index(start: usize, height: usize) -> HashIndex { + // Lemma 24 closed form for the node covering `[start, start + 2^height)`. + HashIndex((2 * (start >> height) + 3) << height) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) struct ShapeMountain { + pub(super) start: usize, + pub(super) height: usize, +} + +impl ShapeMountain { + pub(super) fn size(&self) -> usize { + 1usize << self.height + } + + pub(super) fn contains_position(&self, position: usize) -> bool { + position >= self.start && position < self.start + self.size() + } +} + +pub(super) fn shape_mountains(num_leaves: usize) -> Vec { + if num_leaves == 0 { + return Vec::new(); + } + + let bits = num_leaves + 1; + let num_mountains = bits.ilog2() as usize; + + let mut mountains = Vec::with_capacity(num_mountains); + let mut start = 0; + for position in (0..num_mountains).rev() { + let bit = (bits >> position) & 1; + let height = position + bit; + mountains.push(ShapeMountain { start, height }); + start += 1usize << height; + } + debug_assert_eq!(start, num_leaves); + + mountains +} + +pub(super) fn shape_mountain_for_position( + mountains: &[ShapeMountain], + position: usize, +) -> Option { + mountains + .iter() + .position(|mountain| position < mountain.start + mountain.size()) +} + +#[cfg(test)] +pub(super) fn shape_mountain_index( + mountains: &[ShapeMountain], + target: ShapeMountain, +) -> Option { + mountains.iter().position(|&mountain| mountain == target) +} + +pub(super) fn shape_mountain_index_for_num_leaves( + num_leaves: usize, + target: ShapeMountain, +) -> Option { + if num_leaves == 0 { + return None; + } + + let bits = num_leaves + 1; + let num_mountains = bits.ilog2() as usize; + + let mut index = None; + if target.height < num_mountains && (bits >> target.height) & 1 == 0 { + index = shape_mountain_index_candidate(bits, num_mountains, target.height, target); + } + if index.is_none() && target.height > 0 { + let position = target.height - 1; + if position < num_mountains && (bits >> position) & 1 == 1 { + index = shape_mountain_index_candidate(bits, num_mountains, position, target); + } + } + + index +} + +/// The mountain at bit `position` (counted from the right) of `bits = num_leaves + 1` (Lemma 6). +fn mountain_at_position(bits: usize, num_mountains: usize, position: usize) -> ShapeMountain { + let height = position + ((bits >> position) & 1); + + let lower_mask = (1usize << (position + 1)) - 1; + let mountain_mask = (1usize << num_mountains) - 1; + let higher_mask = mountain_mask ^ lower_mask; + let base = (1usize << num_mountains) - (1usize << (position + 1)); + let start = base + (bits & higher_mask); + + ShapeMountain { start, height } +} + +pub(super) fn shape_mountain_index_candidate( + bits: usize, + num_mountains: usize, + position: usize, + target: ShapeMountain, +) -> Option { + (mountain_at_position(bits, num_mountains, position) == target) + .then_some(num_mountains - 1 - position) +} + +pub(super) fn shape_len_for_num_leaves(num_leaves: usize) -> usize { + if num_leaves == 0 { + 0 + } else { + (num_leaves + 1).ilog2() as usize + } +} + +pub(super) fn shape_mountain_at_index(num_leaves: usize, index: usize) -> Option { + if num_leaves == 0 { + return None; + } + + let bits = num_leaves + 1; + let num_mountains = bits.ilog2() as usize; + if index >= num_mountains { + return None; + } + + let position = num_mountains - 1 - index; + Some(mountain_at_position(bits, num_mountains, position)) +} + +#[cfg(test)] +#[allow(dead_code)] +pub(super) fn shape_range_index_for_mountain( + ranges: &[Range], + mountain_idx: usize, +) -> Option { + ranges.iter().position(|range| range.contains(&mountain_idx)) +} + +pub(super) fn shape_range_index_for_num_leaves( + num_leaves: usize, + mountain_idx: usize, +) -> Option { + let shape_len = shape_len_for_num_leaves(num_leaves); + if mountain_idx >= shape_len { + return None; + } + + let bits = num_leaves + 1; + let split_mask = bits & (!(bits << 1) | !(bits >> 1)); + let mountain_mask = (1usize << shape_len) - 1; + let prefix_mask = (1usize << (shape_len - mountain_idx)) - 1; + + Some((split_mask & (mountain_mask ^ prefix_mask)).count_ones() as usize) +} + +pub(super) fn shape_ranges(mountains: &[ShapeMountain]) -> Vec> { + if mountains.is_empty() { + return Vec::new(); + } + + let mut ranges = Vec::new(); + let mut start = 0; + for idx in 0..mountains.len() - 1 { + if shape_range_split_after(mountains, idx) { + ranges.push(start..idx + 1); + start = idx + 1; + } + } + ranges.push(start..mountains.len()); + ranges +} + +pub(super) fn shape_range_split_after(mountains: &[ShapeMountain], idx: usize) -> bool { + shape_range_split( + mountains[idx].height, + mountains[idx + 1].height, + (idx > 0).then(|| mountains[idx - 1].height), + ) +} + +pub(super) fn shape_range_split_after_num_leaves(num_leaves: usize, idx: usize) -> Option { + Some(shape_range_split( + shape_mountain_at_index(num_leaves, idx)?.height, + shape_mountain_at_index(num_leaves, idx + 1)?.height, + if idx == 0 { + None + } else { + Some(shape_mountain_at_index(num_leaves, idx - 1)?.height) + }, + )) +} + +pub(super) fn shape_range_split(left: usize, right: usize, previous_left: Option) -> bool { + let drops_by_two = left == right + 2; + let left_is_right_member_of_mergeable_pair = previous_left == Some(left); + + drops_by_two || left_is_right_member_of_mergeable_pair +} + +pub(super) fn sibling_and_parent_start(start: usize, height: usize) -> (SiblingSide, usize, usize) { + let span = 1usize << height; + if (start >> height) & 1 == 0 { + (SiblingSide::Right, start + span, start) + } else { + (SiblingSide::Left, start - span, start - span) + } +} + +pub(super) fn climb_to_peak( + start: usize, + from_height: usize, + to_height: usize, +) -> impl Iterator { + let mut start = start; + (from_height..to_height).map(move |height| { + let (side, sibling_start, parent_start) = sibling_and_parent_start(start, height); + start = parent_start; + (side, sibling_start, height) + }) +} + +pub(super) fn bag_range(peaks: &[Word]) -> Word { + peaks.iter().fold(EMPTY_WORD, |acc, &peak| Poseidon2::merge(&[acc, peak])) +} + +pub(super) fn bag_belt(range_roots: &[Word]) -> Word { + range_roots.iter().fold(EMPTY_WORD, |acc, &root| Poseidon2::merge(&[acc, root])) +} + +/// Folds `sibling` into `current` on the given side, the shared step of every belt path climb. +pub(super) fn merge_with_side(side: SiblingSide, current: Word, sibling: Word) -> Word { + match side { + SiblingSide::Left => Poseidon2::merge(&[sibling, current]), + SiblingSide::Right => Poseidon2::merge(&[current, sibling]), + } +} + +pub(super) fn common_peak_prefix_len(from_num_leaves: usize, to_num_leaves: usize) -> usize { + let from_shape = shape_mountains(from_num_leaves); + let to_shape = shape_mountains(to_num_leaves); + common_peak_prefix_len_from_shapes(&from_shape, &to_shape) +} + +pub(super) fn common_peak_prefix_len_from_shapes( + from_shape: &[ShapeMountain], + to_shape: &[ShapeMountain], +) -> usize { + from_shape + .iter() + .zip(to_shape.iter()) + .take_while(|(from, to)| from.start == to.start && from.height == to.height) + .count() +} + +pub(super) fn sibling_side(is_left_child: bool) -> SiblingSide { + if is_left_child { + SiblingSide::Right + } else { + SiblingSide::Left + } +} + +pub(super) fn balanced_tree_sides(height: usize, mut local_position: usize) -> Vec { + let mut sides = Vec::with_capacity(height); + for _ in 0..height { + sides.push(sibling_side(local_position & 1 == 0)); + local_position >>= 1; + } + sides +} diff --git a/miden-crypto/src/merkle/mmr/belt/tests.rs b/miden-crypto/src/merkle/mmr/belt/tests.rs new file mode 100644 index 0000000000..ee61a6a9d9 --- /dev/null +++ b/miden-crypto/src/merkle/mmr/belt/tests.rs @@ -0,0 +1,1360 @@ +use alloc::{vec, vec::Vec}; + +use assert_matches::assert_matches; + +use super::{ + BeltBaggingState, BeltHashArray, BeltSummary, ChangedMountain, HashIndex, MmrBelt, + MmrBeltDelta, MmrError, PartialMmrBelt, append_shape_in_place, bag_belt, bag_range, + common_peak_prefix_len, hash_children, leaf_hash_index, node_hash_index, parent_hash_index, + shape_mountain_index, shape_mountain_index_for_num_leaves, shape_mountains, + shape_range_index_for_mountain, shape_range_index_for_num_leaves, shape_ranges, +}; +use crate::{EMPTY_WORD, Word, hash::poseidon2::Poseidon2, merkle::int_to_node}; + +/// Builds a belt with leaves appended for positions `0..n`. +fn belt_with_leaves(n: usize) -> MmrBelt { + let mut belt = MmrBelt::new(); + for idx in 0..n { + belt.add(int_to_node(idx as u64)).unwrap(); + } + belt +} + +fn shape_heights(num_leaves: usize) -> Vec { + shape_mountains(num_leaves).iter().map(|mountain| mountain.height).collect() +} + +fn shape_hash_indices(num_leaves: usize) -> Vec { + shape_mountains(num_leaves) + .iter() + .map(|mountain| node_hash_index(mountain.start, mountain.height).0) + .collect() +} + +fn hash_range(leaves: &[Word], start: usize, height: usize) -> Word { + if height == 0 { + return leaves[start]; + } + + let half = 1usize << (height - 1); + Poseidon2::merge(&[ + hash_range(leaves, start, height - 1), + hash_range(leaves, start + half, height - 1), + ]) +} + +#[test] +fn belt_hash_indices_match_clojure_layout() { + let leaf_indices = (0..6).map(|position| leaf_hash_index(position).0).collect::>(); + assert_eq!(leaf_indices, vec![3, 5, 7, 9, 11, 13]); + + assert_eq!(parent_hash_index(HashIndex(3)), HashIndex(6)); + assert_eq!(parent_hash_index(HashIndex(5)), HashIndex(6)); + assert_eq!(hash_children(HashIndex(6)), (HashIndex(3), HashIndex(5))); + + assert_eq!(parent_hash_index(HashIndex(6)), HashIndex(12)); + assert_eq!(parent_hash_index(HashIndex(10)), HashIndex(12)); + assert_eq!(hash_children(HashIndex(12)), (HashIndex(6), HashIndex(10))); +} + +#[test] +fn belt_shape_hash_indices_match_peak_layout() { + assert_eq!(shape_hash_indices(1), vec![3]); + assert_eq!(shape_hash_indices(2), vec![6]); + assert_eq!(shape_hash_indices(3), vec![6, 7]); + assert_eq!(shape_hash_indices(4), vec![6, 10]); + assert_eq!(shape_hash_indices(5), vec![12, 11]); + assert_eq!(shape_hash_indices(9), vec![12, 20, 19]); + assert_eq!(shape_hash_indices(10), vec![12, 20, 22]); +} + +#[test] +fn belt_hash_array_preserves_holes_with_compact_value_slots() { + let mut hashes = BeltHashArray::default(); + let value = int_to_node(42); + + hashes.set(HashIndex(5), value); + + assert_eq!(hashes.get(HashIndex(4)), None); + assert_eq!(hashes.get(HashIndex(5)), Some(value)); + assert_eq!(hashes.get(HashIndex(6)), None); + assert_eq!(BeltHashArray::value_slot_bytes_for_testing(), size_of::()); +} + +#[test] +fn belt_shape_index_from_leaf_count_matches_shape_scan() { + for num_leaves in 1..4096 { + let shape = shape_mountains(num_leaves); + for &mountain in &shape { + assert_eq!( + shape_mountain_index_for_num_leaves(num_leaves, mountain), + shape_mountain_index(&shape, mountain), + "index mismatch for {mountain:?} at {num_leaves} leaves" + ); + } + } +} + +#[test] +fn belt_range_index_from_leaf_count_matches_range_scan() { + for num_leaves in 1..4096 { + let shape = shape_mountains(num_leaves); + let ranges = shape_ranges(&shape); + + for mountain_idx in 0..shape.len() { + assert_eq!( + shape_range_index_for_num_leaves(num_leaves, mountain_idx), + shape_range_index_for_mountain(&ranges, mountain_idx), + "range index mismatch for mountain {mountain_idx} at {num_leaves} leaves" + ); + } + } +} + +#[test] +fn belt_hash_array_stores_live_mountain_nodes() { + let mut belt = MmrBelt::new(); + let leaves = (0..64).map(int_to_node).collect::>(); + + for leaf in leaves.iter().copied() { + belt.add(leaf).unwrap(); + } + + for mountain in belt.ordered_mountains() { + for height in 0..=mountain.height { + let width = 1usize << height; + for start in (mountain.start..mountain.start + mountain.size()).step_by(width) { + assert_eq!( + belt.hashes.get(node_hash_index(start, height)), + Some(hash_range(&leaves, start, height)), + "missing hash for node [{start}, {})", + start + width + ); + } + } + } +} + +#[test] +fn belt_lazy_append_height_sequence() { + let mut belt = MmrBelt::new(); + let expected = [ + vec![0], + vec![1], + vec![1, 0], + vec![1, 1], + vec![2, 0], + vec![2, 1], + vec![2, 1, 0], + vec![2, 1, 1], + vec![2, 2, 0], + vec![2, 2, 1], + vec![3, 1, 0], + vec![3, 1, 1], + vec![3, 2, 0], + ]; + + for (idx, expected_heights) in expected.into_iter().enumerate() { + belt.add(int_to_node(idx as u64)).unwrap(); + assert_eq!(belt.mountain_heights(), expected_heights); + } +} + +#[test] +fn belt_append_performs_at_most_one_mountain_merge() { + let mut belt = MmrBelt::new(); + + for idx in 0..128 { + let merge_count = belt.add(int_to_node(idx)).unwrap(); + assert!(merge_count <= 1); + } +} + +#[test] +fn belt_append_touches_constant_local_storage() { + let mut belt = MmrBelt::new(); + + for idx in 0..128 { + let before = belt.storage_slots_for_testing(); + belt.add(int_to_node(idx)).unwrap(); + let after = belt.storage_slots_for_testing(); + + assert!(after - before <= 2); + } +} + +#[test] +fn belt_tracks_rightmost_mergeable_pair_without_stale_stack() { + let mut belt = MmrBelt::new(); + + for idx in 0..512 { + belt.add(int_to_node(idx)).unwrap(); + + let mountains = belt.ordered_mountains(); + let expected = mountains + .windows(2) + .rev() + .find(|pair| pair[0].height == pair[1].height) + .map(|pair| (pair[0].start, pair[1].start)); + + assert_eq!(belt.rightmost_mergeable_pair_for_testing(), expected, "after {idx}"); + } +} + +#[test] +fn belt_range_splits_follow_mmb_rules() { + let mut belt = MmrBelt::new(); + let expected = [ + vec![vec![0]], + vec![vec![1]], + vec![vec![1, 0]], + vec![vec![1, 1]], + vec![vec![2], vec![0]], + vec![vec![2, 1]], + vec![vec![2, 1, 0]], + vec![vec![2, 1, 1]], + vec![vec![2, 2], vec![0]], + vec![vec![2, 2], vec![1]], + vec![vec![3], vec![1, 0]], + vec![vec![3], vec![1, 1]], + vec![vec![3, 2], vec![0]], + vec![vec![3, 2, 1]], + vec![vec![3, 2, 1, 0]], + vec![vec![3, 2, 1, 1]], + ]; + + for (idx, expected_ranges) in expected.into_iter().enumerate() { + belt.add(int_to_node(idx as u64)).unwrap(); + assert_eq!(belt.range_heights(), expected_ranges); + } +} + +#[test] +fn belt_summary_root_is_stable_for_same_leaves() { + let mut first = MmrBelt::new(); + let mut second = MmrBelt::new(); + + for idx in 0..32 { + let leaf = int_to_node(idx); + first.add(leaf).unwrap(); + second.add(leaf).unwrap(); + } + + assert_eq!(first.summary().root(), second.summary().root()); + assert_eq!(first.summary().num_leaves(), 32); +} + +#[test] +fn belt_openings_verify_for_all_leaves() { + let mut belt = MmrBelt::new(); + let leaves = (0..37).map(int_to_node).collect::>(); + for leaf in leaves.iter().copied() { + belt.add(leaf).unwrap(); + } + let summary = belt.summary(); + + for (position, leaf) in leaves.into_iter().enumerate() { + let proof = belt.open(position).unwrap(); + assert_eq!(proof.position(), position); + assert_eq!(proof.leaf(), leaf); + assert!(proof.verify(&summary)); + } +} + +#[test] +fn belt_opening_rejects_wrong_leaf() { + let belt = belt_with_leaves(16); + let summary = belt.summary(); + let mut proof = belt.open(7).unwrap(); + + proof.set_leaf_for_testing(int_to_node(999)); + + assert!(!proof.verify(&summary)); +} + +#[test] +fn belt_proof_rejects_tampered_node() { + let belt = belt_with_leaves(37); + let summary = belt.summary(); + + let pristine = belt.open(13).unwrap(); + assert!(pristine.verify(&summary)); + + for node_idx in 0..pristine.node_count_for_testing() { + let mut proof = belt.open(13).unwrap(); + proof.tamper_node_value_for_testing(node_idx, int_to_node(777_000 + node_idx as u64)); + assert!(!proof.verify(&summary), "tampering node {node_idx} should fail verification"); + } +} + +#[test] +fn belt_opening_rejects_wrong_position() { + let belt = belt_with_leaves(37); + let summary = belt.summary(); + let mut proof = belt.open(7).unwrap(); + + proof.set_position_for_testing(5); + + assert!(!proof.verify(&summary)); +} + +#[test] +fn belt_root_binds_shape_without_belt_domain_separation() { + let belt = belt_with_leaves(2); + let summary = belt.summary(); + let peaks = belt.peaks(); + assert_eq!(peaks.len(), 1); + assert_ne!(summary.root(), peaks[0], "root must not be transparent to its peak"); + + let bigger = belt_with_leaves(3); + assert_ne!(bigger.summary().root(), summary.root()); +} + +#[test] +fn belt_second_bagging_uses_plain_merkle_merge() { + let belt = belt_with_leaves(5); + + let shape = shape_mountains(belt.num_leaves()); + let peaks = belt.peaks(); + let range_roots = shape_ranges(&shape) + .into_iter() + .map(|range| bag_range(&peaks[range])) + .collect::>(); + assert!(range_roots.len() > 1); + + let expected = range_roots.iter().fold(EMPTY_WORD, |acc, &root| Poseidon2::merge(&[acc, root])); + + assert_eq!(belt.summary().root(), expected); +} + +#[test] +fn belt_range_bagging_uses_plain_merkle_merge() { + let belt = belt_with_leaves(9); + + let shape = shape_mountains(belt.num_leaves()); + let peaks = belt.peaks(); + let first_range = shape_ranges(&shape).into_iter().next().unwrap(); + assert!(first_range.len() > 1); + + let expected = peaks[first_range.clone()] + .iter() + .fold(EMPTY_WORD, |acc, &peak| Poseidon2::merge(&[acc, peak])); + + assert_eq!(bag_range(&peaks[first_range]), expected); +} + +#[test] +fn belt_shape_derivation_matches_live_structure_across_pow2() { + let mut belt = MmrBelt::new(); + let leaves = (0..4100u64).map(int_to_node).collect::>(); + for leaf in leaves.iter().copied() { + belt.add(leaf).unwrap(); + } + let summary = belt.summary(); + + for (position, leaf) in leaves.into_iter().enumerate() { + let proof = belt.open(position).unwrap(); + assert_eq!(proof.leaf(), leaf, "leaf mismatch at {position}"); + assert!(proof.verify(&summary), "verify failed at position {position}"); + } +} + +#[test] +fn belt_summary_from_peaks_matches_full_summary() { + let mut belt = MmrBelt::new(); + for idx in 0..100 { + belt.add(int_to_node(idx)).unwrap(); + let summary = belt.summary(); + let from_peaks = BeltSummary::from_peaks(summary.num_leaves(), &belt.peaks()).unwrap(); + assert_eq!(from_peaks, summary); + } +} + +#[test] +fn belt_summary_from_roots_matches_full_summary() { + let mut belt = MmrBelt::new(); + for idx in 0..100 { + belt.add(int_to_node(idx)).unwrap(); + let summary = belt.summary(); + let roots = summary.roots().to_vec(); + let from_roots = BeltSummary::from_roots(summary.num_leaves(), &roots).unwrap(); + assert_eq!(from_roots, summary); + } +} + +#[test] +fn belt_summary_exposes_mountain_order_roots() { + let belt = belt_with_leaves(37); + + let summary = belt.summary(); + let peaks = belt.peaks(); + + assert_eq!(summary.roots(), peaks.as_slice()); + assert_eq!(summary.commitment_root(), summary.root()); + assert_eq!( + summary.commitment_root(), + BeltSummary::from_roots(summary.num_leaves(), summary.roots()).unwrap().root() + ); +} + +#[test] +fn belt_summary_uses_maintained_mountain_order_roots() { + let mut belt = belt_with_leaves(37); + + let summary = belt.summary(); + let roots = belt.peaks(); + + belt.hashes.clear(); + + assert_eq!(belt.peaks(), roots); + assert_eq!(belt.summary(), summary); +} + +#[test] +fn belt_summary_carries_explicit_range_bagging_state() { + let belt = belt_with_leaves(190); + + let summary = belt.summary(); + let shape = shape_mountains(summary.num_leaves()); + let expected_range_roots = shape_ranges(&shape) + .into_iter() + .map(|range| bag_range(&summary.roots()[range])) + .collect::>(); + + assert!(expected_range_roots.len() > 1); + assert_eq!(summary.range_roots_for_testing(), expected_range_roots.as_slice()); + assert_eq!(summary.commitment_root(), bag_belt(&expected_range_roots)); +} + +#[test] +fn belt_bagging_state_uses_explicit_range_and_belt_nodes() { + let belt = belt_with_leaves(190); + + for (nodes, &range_root) in belt.bagging.range_nodes().iter().zip(belt.bagging.range_roots()) { + let mut prefix = EMPTY_WORD; + for node in nodes { + assert_eq!(node.left, prefix); + assert_eq!(node.root, Poseidon2::merge(&[node.left, node.right])); + prefix = node.root; + } + assert_eq!(prefix, range_root); + } + + let mut prefix = EMPTY_WORD; + for (node, &range_root) in belt.bagging.belt_nodes().iter().zip(belt.bagging.range_roots()) { + assert_eq!(node.left, prefix); + assert_eq!(node.right, range_root); + assert_eq!(node.root, Poseidon2::merge(&[node.left, node.right])); + prefix = node.root; + } + assert_eq!(prefix, belt.summary().commitment_root()); +} + +#[test] +fn changed_range_nodes_rejects_extra_changed_mountain_in_range_prefix() { + let old_num_leaves = 190; + let new_num_leaves = old_num_leaves + 1; + let roots = vec![EMPTY_WORD; shape_mountains(old_num_leaves).len()]; + let mut state = BeltBaggingState::from_roots(old_num_leaves, &roots).unwrap(); + + let new_shape = shape_mountains(new_num_leaves); + let range = shape_ranges(&new_shape) + .into_iter() + .find(|range| range.end - range.start > 1) + .unwrap(); + let prefix = new_shape[range.start]; + let last = new_shape[range.end - 1]; + let old_idx = + shape_mountain_index_for_num_leaves(old_num_leaves, prefix).expect("prefix is reused"); + let old_range_idx = shape_range_index_for_num_leaves(old_num_leaves, old_idx) + .expect("reused prefix has an old range"); + let changed = [ + ChangedMountain::new(last.start, last.height, int_to_node(1)), + ChangedMountain::new(prefix.start, prefix.height, int_to_node(2)), + ]; + + assert!( + state + .changed_range_nodes( + old_num_leaves, + new_num_leaves, + range, + &changed, + old_range_idx, + state.ranges.len(), + ) + .is_err() + ); +} + +#[test] +fn append_shape_in_place_rejects_extra_unchanged_changed_mountain() { + let old_num_leaves = 3; + let mut shape = shape_mountains(old_num_leaves); + let unchanged = shape[0]; + let leaf = ChangedMountain::new(old_num_leaves, 0, int_to_node(old_num_leaves as u64)); + let extra = ChangedMountain { + mountain: unchanged, + root: int_to_node(99), + }; + + assert!(append_shape_in_place(&mut shape, old_num_leaves, &[leaf, extra]).is_err()); +} + +#[test] +fn belt_maintains_live_bagging_state_after_each_append() { + let mut belt = MmrBelt::new(); + + for idx in 0..512 { + belt.add(int_to_node(idx)).unwrap(); + + let rebuilt = BeltSummary::from_roots(belt.num_leaves(), &belt.peaks()).unwrap(); + + assert_eq!(belt.live_range_roots_for_testing(), rebuilt.range_roots_for_testing()); + assert_eq!(belt.live_commitment_root_for_testing(), rebuilt.commitment_root()); + assert_eq!(belt.summary(), rebuilt); + } +} + +#[test] +fn belt_lazy_bagging_append_keeps_mountain_state() { + let mut normal = MmrBelt::new(); + let mut lazy = MmrBelt::new(); + + for idx in 0..512 { + let leaf = int_to_node(idx); + normal.add(leaf).unwrap(); + lazy.add_without_bagging_for_benchmark(leaf).unwrap(); + + assert_eq!(lazy.num_leaves(), normal.num_leaves()); + assert_eq!(lazy.peaks(), normal.peaks()); + + let rebuilt = BeltSummary::from_roots(lazy.num_leaves(), &lazy.peaks()).unwrap(); + assert_eq!(rebuilt, normal.summary()); + } +} + +#[test] +fn belt_deferred_append_summary_matches_live_append() { + let mut live = MmrBelt::new(); + let mut deferred = MmrBelt::new(); + + for idx in 0..512 { + let leaf = int_to_node(idx); + live.add(leaf).unwrap(); + deferred.add_deferred(leaf).unwrap(); + + assert_eq!(deferred.summary(), live.summary()); + assert_eq!(deferred.commitment_root(), live.commitment_root()); + } +} + +#[test] +fn belt_live_append_after_deferred_append_refreshes_summary() { + let mut live = MmrBelt::new(); + let mut mixed = MmrBelt::new(); + + for idx in 0..128 { + let leaf = int_to_node(idx); + live.add(leaf).unwrap(); + if idx % 4 == 3 { + mixed.add(leaf).unwrap(); + } else { + mixed.add_deferred(leaf).unwrap(); + } + + assert_eq!(mixed.summary(), live.summary()); + assert_eq!(mixed.commitment_root(), live.commitment_root()); + } +} + +#[test] +fn belt_lazy_bagging_append_does_not_touch_live_bagging() { + let mut belt = MmrBelt::new(); + + for idx in 0..512 { + belt.add_without_bagging_for_benchmark(int_to_node(idx)).unwrap(); + + assert_eq!(belt.last_bagging_update_hashes_for_testing(), 0); + } +} + +#[test] +fn belt_commitment_root_returns_live_bagged_root() { + let mut belt = MmrBelt::new(); + + for idx in 0..512 { + belt.add(int_to_node(idx)).unwrap(); + + let rebuilt = BeltSummary::from_roots(belt.num_leaves(), &belt.peaks()).unwrap(); + + assert_eq!(belt.commitment_root(), rebuilt.commitment_root()); + } +} + +#[test] +fn belt_live_bagging_update_touches_constant_commitment_hashes() { + const MAX_INCREMENTAL_BAGGING_HASHES: usize = 4; + + let mut belt = MmrBelt::new(); + for idx in 0..4096 { + belt.add(int_to_node(idx)).unwrap(); + + assert!( + belt.last_bagging_update_hashes_for_testing() <= MAX_INCREMENTAL_BAGGING_HASHES, + "append {idx} touched {} commitment-layer hashes", + belt.last_bagging_update_hashes_for_testing() + ); + } +} + +#[test] +fn belt_delta_resyncs_client_summary() { + let total = 600usize; + for from in [0usize, 1, 7, 64, 255, 256, 511] { + let mut belt = belt_with_leaves(from); + let client_peaks = belt.peaks(); + + for idx in from..total { + belt.add(int_to_node(idx as u64)).unwrap(); + } + + let delta = belt.delta(from).unwrap(); + assert_eq!(delta.from_num_leaves(), from); + assert_eq!(delta.to_num_leaves(), total); + + let updated = delta.apply(&client_peaks).unwrap(); + assert_eq!(updated, belt.peaks(), "resynced peaks must match (from {from})"); + assert_eq!( + BeltSummary::from_roots(total, &updated).unwrap(), + belt.summary(), + "resynced commitment must match (from {from})" + ); + } +} + +#[test] +fn belt_delta_verifies_summary_transition() { + let from = 128usize; + let to = 191usize; + let mut belt = belt_with_leaves(from); + let old_summary = belt.summary(); + + for idx in from..to { + belt.add(int_to_node(idx as u64)).unwrap(); + } + let new_summary = belt.summary(); + let delta = belt.delta(from).unwrap(); + + assert!(delta.verify_transition(&old_summary, &new_summary).unwrap()); +} + +#[test] +fn belt_delta_rejects_wrong_transition_endpoint() { + let from = 64usize; + let to = 96usize; + let mut belt = belt_with_leaves(from); + let old_summary = belt.summary(); + + for idx in from..to { + belt.add(int_to_node(idx as u64)).unwrap(); + } + let delta = belt.delta(from).unwrap(); + + let mut other = MmrBelt::new(); + for idx in 0..to { + other.add(int_to_node(10_000 + idx as u64)).unwrap(); + } + let wrong_new_summary = other.summary(); + + assert!(!delta.verify_transition(&old_summary, &wrong_new_summary).unwrap()); +} + +#[test] +fn belt_delta_rejects_mutated_tail_peak() { + let from = 128usize; + let to = 191usize; + let mut belt = belt_with_leaves(from); + let old_summary = belt.summary(); + + for idx in from..to { + belt.add(int_to_node(idx as u64)).unwrap(); + } + let new_summary = belt.summary(); + let mut delta = belt.delta(from).unwrap(); + delta.new_tail_peaks[0] = int_to_node(999_999); + + assert!(!delta.verify_transition(&old_summary, &new_summary).unwrap()); +} + +#[test] +fn belt_delta_verify_rejects_padded_auth_nodes() { + let from = 128usize; + let to = 191usize; + let mut belt = belt_with_leaves(from); + let old_summary = belt.summary(); + + for idx in from..to { + belt.add(int_to_node(idx as u64)).unwrap(); + } + let new_summary = belt.summary(); + let delta = belt.delta(from).unwrap(); + assert!(delta.verify_transition(&old_summary, &new_summary).unwrap()); + + // Padding a valid delta with an auth node outside the required climb set must be rejected, so a + // peer cannot inflate a sync delta with bogus data. + let mut padded_auth = delta.merge_auth_nodes().collect::>(); + padded_auth.push(((usize::MAX, 0), int_to_node(123_456))); + let padded = MmrBeltDelta::from_parts( + delta.from_num_leaves(), + delta.to_num_leaves(), + delta.new_tail_peaks().to_vec(), + padded_auth, + ) + .unwrap(); + + assert!(!padded.verify_transition(&old_summary, &new_summary).unwrap()); +} + +#[test] +fn belt_constructors_reject_oversized_num_leaves() { + // `shape_mountains` computes `num_leaves + 1`; oversized counts must error, not panic. + assert!(BeltSummary::from_roots(usize::MAX, &[]).is_err()); + assert!(PartialMmrBelt::from_peaks(usize::MAX, Vec::new()).is_err()); + assert!(MmrBeltDelta::from_parts(0, usize::MAX, Vec::new(), core::iter::empty()).is_err()); +} + +#[test] +fn partial_belt_apply_is_transactional_on_missing_auth() { + let from = 200usize; + let to = 260usize; + let mut belt = belt_with_leaves(from); + + let mut partial = PartialMmrBelt::from_peaks(belt.num_leaves(), belt.peaks()).unwrap(); + for pos in 0..from { + partial.track(&belt.open(pos).unwrap()).unwrap(); + } + + for idx in from..to { + belt.add(int_to_node(idx as u64)).unwrap(); + } + + let mut delta = belt.delta(from).unwrap(); + assert!(delta.num_merge_auth_nodes() > 0, "increment must merge some tracked mountains"); + // Drop the auth nodes a now-merged tracked leaf needs, so the update cannot complete. + delta.merge_auth.clear(); + + let before = partial.clone(); + assert!(partial.apply(&delta).is_err()); + // The failure must leave the view untouched — no partial advance of num_leaves/peaks/tracked. + assert_eq!(partial, before); +} + +#[test] +fn partial_belt_apply_rejects_delta_with_mismatched_from() { + // Snapshot the partial view at 16 leaves, then grow the belt and build a delta that starts at + // 20 leaves, so the delta's `from` does not line up with the partial's current state. + let mut belt = belt_with_leaves(16); + let mut partial = PartialMmrBelt::from_peaks(belt.num_leaves(), belt.peaks()).unwrap(); + + for idx in 16..24 { + belt.add(int_to_node(idx)).unwrap(); + } + let delta = belt.delta(20).unwrap(); + + // Like `PartialMmr::apply`, a delta that does not line up with the current state is rejected + // with a descriptive error rather than a bare `InvalidUpdate`. + let err = partial.apply(&delta).unwrap_err(); + assert_matches!(err, MmrError::InvalidPeaks(_)); +} + +#[test] +fn belt_delta_rejects_wrong_absorbed_old_summary_root() { + let from = 128usize; + let to = 191usize; + let mut belt = belt_with_leaves(from); + let old_summary = belt.summary(); + let common = common_peak_prefix_len(from, to); + assert!(common < old_summary.roots().len()); + + let mut wrong_old_roots = old_summary.roots().to_vec(); + wrong_old_roots[common] = int_to_node(999_999); + let wrong_old_summary = BeltSummary::from_roots(from, &wrong_old_roots).unwrap(); + + for idx in from..to { + belt.add(int_to_node(idx as u64)).unwrap(); + } + let new_summary = belt.summary(); + let delta = belt.delta(from).unwrap(); + + assert!(!delta.verify_transition(&wrong_old_summary, &new_summary).unwrap()); +} + +#[test] +fn belt_delta_transition_requires_merge_auth_for_absorbed_old_roots() { + let from = 200usize; + let to = 260usize; + let mut belt = belt_with_leaves(from); + let old_summary = belt.summary(); + + for idx in from..to { + belt.add(int_to_node(idx as u64)).unwrap(); + } + let new_summary = belt.summary(); + let mut delta = belt.delta(from).unwrap(); + assert!(common_peak_prefix_len(from, to) < old_summary.roots().len()); + delta.merge_auth.clear(); + + assert!(delta.verify_transition(&old_summary, &new_summary).is_err()); +} + +#[test] +fn belt_delta_rejects_forged_merge_auth_value() { + let from = 200usize; + let to = 260usize; + let mut belt = belt_with_leaves(from); + let old_summary = belt.summary(); + + for idx in from..to { + belt.add(int_to_node(idx as u64)).unwrap(); + } + let new_summary = belt.summary(); + + let mut delta = belt.delta(from).unwrap(); + assert!(delta.verify_transition(&old_summary, &new_summary).unwrap()); + + let key = *delta.merge_auth.keys().next().expect("absorbed roots need merge-auth nodes"); + delta.merge_auth.insert(key, int_to_node(999_999)); + + assert!(!delta.verify_transition(&old_summary, &new_summary).unwrap()); +} + +#[test] +fn belt_zero_and_single_leaf_summaries() { + let empty = MmrBelt::new(); + let empty_summary = empty.summary(); + assert_eq!(empty_summary.num_leaves(), 0); + assert!(empty_summary.roots().is_empty()); + assert_eq!(empty_summary.commitment_root(), EMPTY_WORD); + assert_eq!( + empty_summary, + BeltSummary::from_roots(0, &[]).unwrap(), + "live empty summary must match a from-scratch rebuild" + ); + + let mut single = MmrBelt::new(); + single.add(int_to_node(42)).unwrap(); + let single_summary = single.summary(); + assert_eq!(single_summary.num_leaves(), 1); + assert_eq!(single_summary, BeltSummary::from_roots(1, &single.peaks()).unwrap()); + + let proof = single.open(0).unwrap(); + assert!(proof.verify(&single_summary)); +} + +#[test] +fn belt_delta_from_same_leaf_count_is_noop() { + let belt = belt_with_leaves(50); + let summary = belt.summary(); + + let delta = belt.delta(belt.num_leaves()).unwrap(); + assert!(delta.verify_transition(&summary, &summary).unwrap()); + assert_eq!(delta.apply(summary.roots()).unwrap(), summary.roots()); + + let empty = MmrBelt::new(); + let empty_summary = empty.summary(); + let empty_delta = empty.delta(0).unwrap(); + assert!(empty_delta.verify_transition(&empty_summary, &empty_summary).unwrap()); + assert!(empty_delta.apply(empty_summary.roots()).unwrap().is_empty()); +} + +#[test] +fn belt_delta_is_logarithmic_in_increment() { + let belt = belt_with_leaves(100_000); + + for k in [1usize, 2, 10, 100, 1000] { + let delta = belt.delta(100_000 - k).unwrap(); + let bound = 2 * (usize::BITS - k.leading_zeros()) as usize + 4; + assert!( + delta.new_tail_peaks().len() <= bound, + "k={k}: tail {} exceeded bound {bound}", + delta.new_tail_peaks().len() + ); + } +} + +#[test] +fn belt_delta_rejects_future_origin() { + let belt = belt_with_leaves(10); + assert!(belt.delta(11).is_err()); +} + +#[test] +fn partial_belt_tracks_and_opens_like_full_belt() { + let mut belt = MmrBelt::new(); + let leaves = (0..50u64).map(int_to_node).collect::>(); + for leaf in leaves.iter().copied() { + belt.add(leaf).unwrap(); + } + + let mut partial = PartialMmrBelt::from_peaks(belt.num_leaves(), belt.peaks()).unwrap(); + assert_eq!(partial.summary(), belt.summary()); + + for position in 0..leaves.len() { + partial.track(&belt.open(position).unwrap()).unwrap(); + } + assert_eq!(partial.num_tracked(), leaves.len()); + + for (position, leaf) in leaves.iter().copied().enumerate() { + let proof = partial.open(position).unwrap().unwrap(); + assert_eq!(proof, belt.open(position).unwrap()); + assert_eq!(partial.get(position), Some(leaf)); + assert!(proof.verify(&partial.summary())); + } +} + +#[test] +fn partial_belt_from_peaks_rejects_wrong_count() { + let belt = belt_with_leaves(7); + let mut peaks = belt.peaks(); + peaks.pop(); + assert!(PartialMmrBelt::from_peaks(belt.num_leaves(), peaks).is_err()); +} + +#[test] +fn partial_belt_track_rejects_unauthenticated_proof() { + let belt = belt_with_leaves(16); + let mut partial = PartialMmrBelt::from_peaks(belt.num_leaves(), belt.peaks()).unwrap(); + + let mut proof = belt.open(5).unwrap(); + proof.set_leaf_for_testing(int_to_node(999)); + assert!(partial.track(&proof).is_err()); + assert!(!partial.is_tracked(5)); +} + +#[test] +fn partial_belt_track_rejects_out_of_range_position() { + let belt = belt_with_leaves(16); + let mut partial = PartialMmrBelt::from_peaks(belt.num_leaves(), belt.peaks()).unwrap(); + + let mut proof = belt.open(5).unwrap(); + proof.set_position_for_testing(belt.num_leaves()); + let err = partial.track(&proof).unwrap_err(); + + // Like `PartialMmr::track`, an out-of-range position is rejected by verification rather than a + // dedicated position check. + assert_matches!(err, MmrError::PeakPathMismatch); +} + +#[test] +fn partial_belt_apply_extends_all_tracks_in_place() { + let from = 200usize; + let to = 260usize; + + let mut belt = belt_with_leaves(from); + + let mut partial = PartialMmrBelt::from_peaks(belt.num_leaves(), belt.peaks()).unwrap(); + for position in 0..from { + partial.track(&belt.open(position).unwrap()).unwrap(); + } + + for idx in from..to { + belt.add(int_to_node(idx as u64)).unwrap(); + } + + partial.apply(&belt.delta(from).unwrap()).unwrap(); + + assert_eq!(partial.num_leaves(), to); + assert_eq!(partial.summary(), belt.summary()); + assert_eq!(partial.num_tracked(), from); + + for position in 0..from { + assert!(partial.is_tracked(position), "leaf {position} must still be tracked"); + assert_eq!(partial.open(position).unwrap().unwrap(), belt.open(position).unwrap()); + } +} + +#[test] +fn partial_belt_apply_extends_across_many_increments() { + let mut belt = belt_with_leaves(40); + + let mut partial = PartialMmrBelt::from_peaks(belt.num_leaves(), belt.peaks()).unwrap(); + let tracked = [0usize, 1, 17, 38, 39]; + for &position in &tracked { + partial.track(&belt.open(position).unwrap()).unwrap(); + } + + let mut next = 40u64; + for _ in 0..30 { + let from = belt.num_leaves(); + for _ in 0..7 { + belt.add(int_to_node(next)).unwrap(); + next += 1; + } + partial.apply(&belt.delta(from).unwrap()).unwrap(); + + assert_eq!(partial.summary(), belt.summary()); + for &position in &tracked { + assert_eq!(partial.open(position).unwrap().unwrap(), belt.open(position).unwrap()); + } + } +} + +#[test] +fn partial_belt_protocol_model_resyncs_after_offline_increment() { + let from = 128usize; + let to = 191usize; + let leaves = (0..to as u64).map(int_to_node).collect::>(); + + let mut full_node = MmrBelt::new(); + for &leaf in &leaves[..from] { + full_node.add(leaf).unwrap(); + } + + let mut client = PartialMmrBelt::from_peaks(full_node.num_leaves(), full_node.peaks()).unwrap(); + for &position in &[0usize, 1, 7, 63, 64, 100, 127] { + client.track(&full_node.open(position).unwrap()).unwrap(); + } + assert_eq!(client.summary(), full_node.summary()); + + for &leaf in &leaves[from..to] { + full_node.add(leaf).unwrap(); + } + let server_delta = full_node.delta(from).unwrap(); + + assert_ne!(client.summary(), full_node.summary()); + client.apply_verified(&server_delta, &full_node.summary()).unwrap(); + assert_eq!(client.summary(), full_node.summary()); + + for &position in &[0usize, 1, 7, 63, 64, 100, 127] { + let client_proof = client.open(position).unwrap().unwrap(); + assert!(client_proof.verify(&client.summary())); + assert_eq!(client_proof, full_node.open(position).unwrap()); + assert_eq!(client.get(position), Some(leaves[position])); + } + + let newest_position = to - 1; + client.track(&full_node.open(newest_position).unwrap()).unwrap(); + assert_eq!(client.get(newest_position), Some(leaves[newest_position])); + assert!(client.open(newest_position).unwrap().unwrap().verify(&client.summary())); +} + +#[test] +fn belt_delta_and_summary_verify_sync_response_transition() { + let from = 128usize; + let to = 191usize; + let mut belt = belt_with_leaves(from); + let old_summary = belt.summary(); + + for idx in from..to { + belt.add(int_to_node(idx as u64)).unwrap(); + } + + let belt_delta = belt.delta(from).unwrap(); + let belt_summary = belt.summary(); + assert_eq!(belt_delta.from_num_leaves(), from); + assert_eq!(belt_delta.to_num_leaves(), to); + assert!(belt_delta.verify_transition(&old_summary, &belt_summary).unwrap()); +} + +#[test] +fn partial_belt_apply_verified_resyncs_sync_response() { + let from = 128usize; + let to = 191usize; + let leaves = (0..to as u64).map(int_to_node).collect::>(); + + let mut full_node = MmrBelt::new(); + for &leaf in &leaves[..from] { + full_node.add(leaf).unwrap(); + } + let mut client = PartialMmrBelt::from_peaks(full_node.num_leaves(), full_node.peaks()).unwrap(); + client.track(&full_node.open(from - 1).unwrap()).unwrap(); + + for &leaf in &leaves[from..to] { + full_node.add(leaf).unwrap(); + } + let belt_delta = full_node.delta(from).unwrap(); + let belt_summary = full_node.summary(); + + client.apply_verified(&belt_delta, &belt_summary).unwrap(); + assert_eq!(client.summary(), full_node.summary()); + assert_eq!(client.open(from - 1).unwrap().unwrap(), full_node.open(from - 1).unwrap()); +} + +#[test] +fn belt_delta_from_parts_reconstructs_sync_response() { + let from = 128usize; + let to = 191usize; + let leaves = (0..to as u64).map(int_to_node).collect::>(); + + let mut full_node = MmrBelt::new(); + for &leaf in &leaves[..from] { + full_node.add(leaf).unwrap(); + } + let old_summary = full_node.summary(); + let mut client = PartialMmrBelt::from_peaks(full_node.num_leaves(), full_node.peaks()).unwrap(); + client.track(&full_node.open(from - 1).unwrap()).unwrap(); + + for &leaf in &leaves[from..to] { + full_node.add(leaf).unwrap(); + } + + let server_delta = full_node.delta(from).unwrap(); + let server_summary = full_node.summary(); + let belt_delta = MmrBeltDelta::from_parts( + server_delta.from_num_leaves(), + server_delta.to_num_leaves(), + server_delta.new_tail_peaks().to_vec(), + server_delta.merge_auth_nodes(), + ) + .unwrap(); + let summary_roots = server_summary.roots().to_vec(); + let belt_summary = + BeltSummary::from_roots(server_summary.num_leaves(), &summary_roots).unwrap(); + + assert_eq!(belt_delta, server_delta); + assert_eq!(belt_summary, server_summary); + assert!(belt_delta.verify_transition(&old_summary, &belt_summary).unwrap()); + + client.apply_verified(&belt_delta, &belt_summary).unwrap(); + assert_eq!(client.summary(), full_node.summary()); + assert_eq!(client.open(from - 1).unwrap().unwrap(), full_node.open(from - 1).unwrap()); +} + +#[test] +fn belt_delta_from_parts_rejects_invalid_tail_shape() { + let belt = belt_with_leaves(191); + let server_delta = belt.delta(128).unwrap(); + + assert!( + MmrBeltDelta::from_parts( + server_delta.from_num_leaves(), + server_delta.to_num_leaves(), + vec![], + server_delta.merge_auth_nodes() + ) + .is_err() + ); +} + +#[test] +fn partial_belt_apply_verified_rejects_wrong_new_summary() { + let from = 64usize; + let to = 96usize; + let mut belt = belt_with_leaves(from); + let mut client = PartialMmrBelt::from_peaks(belt.num_leaves(), belt.peaks()).unwrap(); + client.track(&belt.open(from - 1).unwrap()).unwrap(); + let old_summary = client.summary(); + + for idx in from..to { + belt.add(int_to_node(idx as u64)).unwrap(); + } + let delta = belt.delta(from).unwrap(); + + let mut other = MmrBelt::new(); + for idx in 0..to { + other.add(int_to_node(10_000 + idx as u64)).unwrap(); + } + let wrong_new_summary = other.summary(); + + assert!(client.apply_verified(&delta, &wrong_new_summary).is_err()); + assert_eq!(client.summary(), old_summary); +} + +#[test] +fn partial_belt_apply_verified_rejects_missing_merge_auth_without_mutating() { + let from = 200usize; + let to = 260usize; + let mut belt = belt_with_leaves(from); + let mut client = PartialMmrBelt::from_peaks(belt.num_leaves(), belt.peaks()).unwrap(); + client.track(&belt.open(from - 1).unwrap()).unwrap(); + let old_summary = client.summary(); + + for idx in from..to { + belt.add(int_to_node(idx as u64)).unwrap(); + } + let new_summary = belt.summary(); + let mut delta = belt.delta(from).unwrap(); + delta.merge_auth.clear(); + + assert!(client.apply_verified(&delta, &new_summary).is_err()); + assert_eq!(client.summary(), old_summary); +} + +#[test] +fn partial_belt_delta_merge_auth_is_polylogarithmic() { + let belt = belt_with_leaves(100_000); + + for k in [1usize, 2, 16, 256, 4096] { + let delta = belt.delta(100_000 - k).unwrap(); + let log_k = (usize::BITS - k.leading_zeros()) as usize; + let bound = 4 * log_k * log_k + 8; + assert!( + delta.num_merge_auth_nodes() <= bound, + "k={k}: {} auth nodes exceeded bound {bound}", + delta.num_merge_auth_nodes() + ); + } +} + +// Gated behind `std` so the no-std test build (which has no `println!`) still compiles. +#[cfg(feature = "std")] +#[test] +#[ignore = "prints deterministic MMB issue measurement tables"] +fn mmb_issue_measurement_report() { + use std::println; + + const STORAGE_SIZES: &[usize] = &[1_000, 65_536, 100_000]; + const PROOF_SIZE: usize = 65_536; + const DELTA_SIZE: usize = 100_000; + const RECENCIES: &[usize] = &[1, 2, 4, 8, 16, 64, 512, 4_096, 16_384, 65_536]; + const GAPS: &[usize] = &[1, 2, 4, 8, 16, 32, 64, 128, 512, 4_096, 16_384, 65_536]; + + println!("# MMB issue measurement report"); + println!(); + println!("## Storage shape"); + println!( + "| leaves | MMR nodes | MMR peaks | Frontier peaks | Belt hash slots | Belt live hashes | Belt mountains | Belt ranges | Belt range nodes | Belt belt nodes |" + ); + println!("|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|"); + for &size in STORAGE_SIZES { + let (mmr, belt) = build_mmr_and_belt(size); + let frontier = mmr.frontier(); + let range_nodes = belt.bagging.range_nodes().iter().map(Vec::len).sum::(); + println!( + "| {size} | {} | {} | {} | {} | {} | {} | {} | {} | {} |", + mmr.forest().num_nodes(), + mmr.peaks().num_peaks(), + frontier.num_peaks(), + belt.hashes.slot_count(), + belt.hashes.live_count(), + belt.ordered_mountains().len(), + belt.bagging.range_roots().len(), + range_nodes, + belt.bagging.belt_nodes().len() + ); + } + + let (mmr, belt) = build_mmr_and_belt(PROOF_SIZE); + let summary = belt.summary(); + println!(); + println!("## Proof nodes by recency"); + println!("n = {PROOF_SIZE}"); + println!("| recency k | position | MMR path nodes | Frontier proof nodes | Belt proof nodes |"); + println!("|---:|---:|---:|---:|---:|"); + for &recency in RECENCIES { + if recency > PROOF_SIZE { + continue; + } + let position = PROOF_SIZE - recency; + let mmr_nodes = mmr.open(position).unwrap().merkle_path().nodes().len(); + let frontier_proof_nodes = mmr.open_frontier(position).unwrap().path.nodes().len(); + let belt_proof = belt.open(position).unwrap(); + assert!(belt_proof.verify(&summary)); + println!( + "| {recency} | {position} | {mmr_nodes} | {frontier_proof_nodes} | {} |", + belt_proof.nodes.len() + ); + } + + let (mmr, belt) = build_mmr_and_belt(DELTA_SIZE); + println!(); + println!("## Delta words by offline gap"); + println!("target n = {DELTA_SIZE}"); + println!( + "| gap k | from leaves | Current MMR delta words | Belt tail roots | Belt merge auth nodes | Belt total words |" + ); + println!("|---:|---:|---:|---:|---:|---:|"); + for &gap in GAPS { + if gap > DELTA_SIZE { + continue; + } + let from = DELTA_SIZE - gap; + let mmr_delta = mmr + .get_delta(crate::merkle::mmr::Forest::new(from).unwrap(), mmr.forest()) + .unwrap(); + let belt_delta = belt.delta(from).unwrap(); + let tail_roots = belt_delta.new_tail_peaks().len(); + let merge_auth = belt_delta.num_merge_auth_nodes(); + println!( + "| {gap} | {from} | {} | {tail_roots} | {merge_auth} | {} |", + mmr_delta.data.len(), + tail_roots + merge_auth + ); + } +} + +#[cfg(feature = "std")] +fn build_mmr_and_belt(size: usize) -> (crate::merkle::mmr::Mmr, MmrBelt) { + let leaves = (0..size as u64).map(int_to_node).collect::>(); + let mmr = crate::merkle::mmr::Mmr::try_from_iter(leaves.iter().copied()).unwrap(); + let mut belt = MmrBelt::new(); + for leaf in leaves { + belt.add(leaf).unwrap(); + } + (mmr, belt) +} + +#[test] +fn partial_belt_open_untracked_returns_none() { + let belt = belt_with_leaves(16); + let partial = PartialMmrBelt::from_peaks(belt.num_leaves(), belt.peaks()).unwrap(); + assert!(partial.open(3).unwrap().is_none()); +} + +#[test] +fn partial_belt_open_rejects_out_of_range_position() { + let belt = belt_with_leaves(16); + + let partial = PartialMmrBelt::from_peaks(belt.num_leaves(), belt.peaks()).unwrap(); + let err = partial.open(belt.num_leaves()).unwrap_err(); + + assert_matches!(err, MmrError::PositionNotFound(16)); +} + +#[test] +fn belt_height_sequences_match_paper() { + // Golden S_n sequences from arXiv:2511.13582, §3.1 and Figures 5/7/9. + let golden: [(usize, &[usize]); 4] = [ + (9, &[2, 2, 0]), + (10, &[2, 2, 1]), + (11, &[3, 1, 0]), + (1337, &[9, 9, 7, 6, 6, 5, 4, 2, 2, 0]), + ]; + + for (num_leaves, expected) in golden { + assert_eq!(shape_heights(num_leaves), expected, "shape S_{num_leaves}"); + + let belt = belt_with_leaves(num_leaves); + assert_eq!(belt.mountain_heights(), expected, "live S_{num_leaves}"); + } +} + +#[test] +fn belt_merge_peak_lands_in_last_two_ranges() { + // Lemma 16. + for num_leaves in 2..4096usize { + if (num_leaves + 1).is_power_of_two() { + continue; + } + + let shape = shape_mountains(num_leaves); + let merge_idx = shape.len() - 1 - (num_leaves + 1).trailing_zeros() as usize; + + let ranges = shape_ranges(&shape); + let range_idx = ranges + .iter() + .position(|range| range.contains(&merge_idx)) + .expect("merge peak must lie in a range"); + + assert_eq!( + ranges[range_idx].end, + merge_idx + 1, + "n={num_leaves}: merge peak must sit at the right end of its range" + ); + assert!( + range_idx + 2 >= ranges.len(), + "n={num_leaves}: merge peak must be in the rightmost or second-rightmost range" + ); + } +} diff --git a/miden-crypto/src/merkle/mmr/mod.rs b/miden-crypto/src/merkle/mmr/mod.rs index 1bdf44b532..fe6aebbf06 100644 --- a/miden-crypto/src/merkle/mmr/mod.rs +++ b/miden-crypto/src/merkle/mmr/mod.rs @@ -1,5 +1,11 @@ //! Merkle Mountain Range (MMR) data structures. +// The belt prototype is gated behind `internal`, where it is part of the public API (so its items +// are never dead). Compiling it under non-`internal` builds would leave an unused module that trips +// `-D warnings` in the no-default-features feature check. Its tests run via `make test-default`, +// which enables `internal`. +#[cfg(feature = "internal")] +pub mod belt; mod delta; mod error; mod forest;