cml/mountain.rs
1//! The Merkle Mountain Range commitment topology — the append-only log's own
2//! bagging of its perfect-subtree peaks, and the inclusion skeleton that pins a
3//! proof against it.
4//!
5//! The [`spine`] decomposes a log of `n` leaves into a frontier of perfect k-ary
6//! subtrees ([`spine::frontier_for_size`]) — the MMR **mountains**, whose peaks
7//! are permanent (a perfect subtree's hash never changes as the log grows). This
8//! module owns what the spine deliberately does not: how those peaks **bag** into
9//! one root, and what an inclusion proof points at.
10//!
11//! # The peak bag — root-preserving
12//!
13//! The peaks are bagged with the **established fold**: repeatedly group the
14//! rightmost `k` into one [`nary_mr`](spine::nary_mr) node, exactly the shape the
15//! structural `fold_frontier` and `cmt::shape::build` produce. So [`bag_peaks`]
16//! is byte-identical to that fold at every arity, and the MMR commitment **root
17//! is unchanged** from the pre-MMR log — durability is bought entirely in the
18//! *proof*, not by re-rooting the commitment.
19//!
20//! Each `H` is plain `nary_mr` (collapse + promotion, no domain separation, **no
21//! size prefix**) — deliberately unlike the OpenTimestamps/Grin `H(size ‖ peak ‖
22//! acc)` convention: those bag a sole commitment where `size` is otherwise
23//! unbound, so the prefix guards cross-size confusion. Our model binds
24//! `tree_size` as a trusted verifier parameter and closes malleability with the
25//! proven canonical-encoding uniqueness over `nary_mr` (`inclusion_proof_unique`),
26//! so the prefix's job is already discharged. Keeping the bag the established
27//! `nary_mr` fold is also what makes the root preservation hold and lets the Lean
28//! corpus transfer verbatim.
29//!
30//! # Durable witnesses
31//!
32//! An inclusion proof is **prove-to-peak then bag**: the `peakPath` from the leaf
33//! up to its own mountain's peak (entirely inside a permanent perfect subtree —
34//! the durable prefix) followed by the `bagPath` from the peak to the root. As
35//! the log grows the `peakPath` never changes; only the `bagPath` suffix is
36//! re-derived against the current peak set. That is the durable-witness property
37//! RFC-6962's rebalancing tree structurally cannot have — and it is achieved
38//! *without* changing the root: the old construction climbed the leaf through the
39//! rebalanced tree's ephemeral interior nodes, while this stops at the permanent
40//! peak and re-bags, over the very same peak fold.
41//!
42//! # One shape, two readers
43//!
44//! [`bag_shape`] builds the bag as an explicit tree once; [`bag_peaks`] evaluates
45//! it to the member root, and [`mountain_skeleton`] walks it to the per-step
46//! `(position, sibling_count)` skeleton the [`spine`] verifier pins a proof
47//! against. Deriving both from one shape is what keeps the producer's fold and
48//! the verifier's skeleton in lockstep (the same discipline `cmt::shape` uses for
49//! the rebalanced tree).
50
51use spine::{Hasher, ProofStep, SkeletonStep, fold_frontier, frontier_for_size, nary_mr};
52
53/// One node of the bag tree.
54///
55/// A `Peak(f_idx)` is a frontier peak at index `f_idx` (left-to-right, the
56/// [`frontier_for_size`] order); a `Bag(children)` is a hashing node over its
57/// children left-to-right. The shape is uniquely determined by `(peak_count,
58/// arity)` — never stored, always re-derived — so the producer's fold and the
59/// verifier's skeleton cannot drift.
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub enum BagNode {
62 /// A frontier peak at index `f_idx` in the `frontier_for_size` order.
63 Peak(usize),
64 /// A hashing bag node over its children, left to right.
65 Bag(Vec<BagNode>),
66}
67
68/// Build the peak-bag shape over `peak_count` peaks at arity `k`. Returns `None`
69/// for an empty peak set (a non-empty log always has at least one peak).
70///
71/// The peaks are grouped by repeatedly folding the **rightmost `k`** into one bag
72/// node — the same shape `cmt::shape::build` produces — so `bag_peaks` over
73/// this shape is byte-identical to that fold at every arity, and the MMR
74/// commitment **root is unchanged** from the pre-MMR log. Durability does not
75/// come from the bag's associativity; it comes from what the *proof* points at
76/// (prove-to-peak), so the bag stays the established fold and only the inclusion
77/// path is restructured.
78///
79/// Expressed via [`fold_frontier`]: a single peak promotes to itself (no `Bag`
80/// wrapper); `2..=k` or more peaks fold rightmost-`k` until one root bag node
81/// remains.
82#[must_use]
83pub fn bag_shape(peak_count: usize, k: usize) -> Option<BagNode> {
84 if peak_count == 0 {
85 return None;
86 }
87 let frontier: Vec<BagNode> = (0..peak_count).map(BagNode::Peak).collect();
88 Some(fold_frontier(frontier, k, |chunk| {
89 BagNode::Bag(chunk.to_vec())
90 }))
91}
92
93/// Bag a frontier's peaks into the single member root under `hasher`.
94///
95/// Evaluates [`bag_shape`] with each [`nary_mr`] node hashing its children. A
96/// single peak promotes to itself; an empty peak set is the empty-tree root
97/// (`hasher.empty()`).
98#[must_use]
99pub fn bag_peaks(hasher: &dyn Hasher, peaks: &[Vec<u8>], k: u64) -> Vec<u8> {
100 match bag_shape(peaks.len(), k as usize) {
101 None => hasher.empty(),
102 Some(shape) => eval_bag(hasher, &shape, peaks),
103 }
104}
105
106/// Evaluate a bag shape to its digest, reading peak digests from `peaks`.
107fn eval_bag(hasher: &dyn Hasher, node: &BagNode, peaks: &[Vec<u8>]) -> Vec<u8> {
108 match node {
109 BagNode::Peak(f_idx) => peaks[*f_idx].clone(),
110 BagNode::Bag(children) => {
111 let child_digests: Vec<Vec<u8>> = children
112 .iter()
113 .map(|c| eval_bag(hasher, c, peaks))
114 .collect();
115 let refs: Vec<&[u8]> = child_digests.iter().map(Vec::as_slice).collect();
116 nary_mr(hasher, &refs)
117 },
118 }
119}
120
121/// Generate the `bagPath` proof steps that lift the peak at frontier index
122/// `f_idx` to the bagged root, given every peak's digest in `peaks`
123/// (left-to-right). The steps are ordered peak → root, so a caller appends them
124/// after the within-mountain `peakPath` to form the full leaf → root path.
125///
126/// Each step carries the path node's `position` among its bag-node siblings and
127/// those siblings' digests (the other children of that bag node) — exactly the
128/// shape [`spine::reconstruct_inclusion_root`] folds against, pinned by
129/// [`mountain_skeleton`]. Returns an empty path when the peak is the sole peak
130/// (it promotes directly to the root).
131#[must_use]
132pub fn bag_path(peaks: &[Vec<u8>], f_idx: usize, hasher: &dyn Hasher, k: u64) -> Vec<ProofStep> {
133 let mut steps = Vec::new();
134 if let Some(shape) = bag_shape(peaks.len(), k as usize) {
135 // Collected root → peak; reverse to peak → root for the leaf → root path.
136 descend_bag_path(&shape, f_idx, peaks, hasher, &mut steps);
137 steps.reverse();
138 }
139 steps
140}
141
142/// Walk from `node` to `Peak(f_idx)`, pushing one [`ProofStep`] per bag node
143/// crossed (root → peak order), with each step's sibling digests evaluated from
144/// `peaks`. Returns whether `f_idx` is under `node`.
145fn descend_bag_path(
146 node: &BagNode,
147 f_idx: usize,
148 peaks: &[Vec<u8>],
149 hasher: &dyn Hasher,
150 out: &mut Vec<ProofStep>,
151) -> bool {
152 match node {
153 BagNode::Peak(idx) => *idx == f_idx,
154 BagNode::Bag(children) => {
155 let Some(position) = children.iter().position(|c| covers_peak(c, f_idx)) else {
156 return false;
157 };
158 let siblings: Vec<Vec<u8>> = children
159 .iter()
160 .enumerate()
161 .filter(|(i, _)| *i != position)
162 .map(|(_, c)| eval_bag(hasher, c, peaks))
163 .collect();
164 out.push(ProofStep { siblings, position });
165 descend_bag_path(&children[position], f_idx, peaks, hasher, out)
166 },
167 }
168}
169
170/// Compute the MMR inclusion skeleton for a leaf `index` in a tree of
171/// `tree_size` leaves at arity `k`.
172///
173/// The skeleton runs leaf → root: the `peakPath` (the base-k digit steps inside
174/// the leaf's perfect-subtree mountain, each carrying `k - 1` siblings) followed
175/// by the `bagPath` (the bag-node steps from the peak up to the root). Returns
176/// `None` when the inputs cannot describe a valid log position (`k` out of range,
177/// empty frontier, or `index` outside the covered range).
178///
179/// This is the concrete topology the append-only log feeds the spine's abstract
180/// verifier — the MMR analog of the rebalanced skeleton the spine used to derive
181/// internally.
182#[must_use]
183pub fn mountain_skeleton(k: u64, tree_size: u64, index: u64) -> Option<Vec<SkeletonStep>> {
184 if !spine::ARITY_RANGE.contains(&k) {
185 return None;
186 }
187 let coords = frontier_for_size(tree_size, k);
188 if coords.is_empty() {
189 return None;
190 }
191 let k_usize = k as usize;
192
193 // Locate the perfect mountain that contains `index`.
194 let mut target = None;
195 for (f_idx, &(left, height)) in coords.iter().enumerate() {
196 let cap = k.checked_pow(height)?;
197 let limit = left.checked_add(cap)?;
198 if index >= left && index < limit {
199 target = Some((f_idx, left, height));
200 break;
201 }
202 }
203 let (f_idx, left, height) = target?;
204
205 let mut steps = Vec::with_capacity(height as usize);
206
207 // peakPath: base-k digits of the offset, low digit first (leaf → peak). These
208 // are the durable, permanent steps inside the perfect mountain.
209 let mut offset = index - left;
210 for _ in 0..height {
211 steps.push(SkeletonStep {
212 position: (offset % k) as usize,
213 sibling_count: k_usize - 1,
214 });
215 offset /= k;
216 }
217
218 // bagPath: the bag-node steps from the mountain's peak up to the root.
219 let shape = bag_shape(coords.len(), k_usize)?;
220 bag_path_steps(&shape, f_idx, &mut steps);
221
222 Some(steps)
223}
224
225/// Append the bag-path steps that reach `Peak(f_idx)` in `shape`, ordered from
226/// the peak up to the root, to `steps`.
227///
228/// At each bag node on the path, the step records the path child's `position`
229/// among its siblings and the `sibling_count`. Because the recursion descends
230/// from the root, the steps are collected root → peak and reversed into the
231/// leaf → root order the verifier expects.
232fn bag_path_steps(shape: &BagNode, f_idx: usize, steps: &mut Vec<SkeletonStep>) {
233 let mut down = Vec::new();
234 descend_bag(shape, f_idx, &mut down);
235 // `down` is root → peak; the skeleton is leaf → root, so the bag steps
236 // (above the peak) append in peak → root order.
237 down.reverse();
238 steps.extend(down);
239}
240
241/// Walk from `node` to `Peak(target)`, pushing one [`SkeletonStep`] per bag node
242/// crossed, ordered root → peak. Returns whether `target` is under `node`.
243fn descend_bag(node: &BagNode, target: usize, out: &mut Vec<SkeletonStep>) -> bool {
244 match node {
245 BagNode::Peak(f_idx) => *f_idx == target,
246 BagNode::Bag(children) => {
247 let position = children.iter().position(|c| covers_peak(c, target));
248 let Some(position) = position else {
249 return false;
250 };
251 out.push(SkeletonStep {
252 position,
253 sibling_count: children.len() - 1,
254 });
255 descend_bag(&children[position], target, out)
256 },
257 }
258}
259
260/// Whether `node` covers peak index `target`.
261///
262/// Used by the bag-shape descent in both [`descend_bag`] and [`descend_bag_path`].
263fn covers_peak(node: &BagNode, target: usize) -> bool {
264 match node {
265 BagNode::Peak(f_idx) => *f_idx == target,
266 BagNode::Bag(children) => children.iter().any(|c| covers_peak(c, target)),
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use sha2::{Digest, Sha256};
273
274 use super::*;
275
276 #[derive(Debug)]
277 struct H;
278 impl Hasher for H {
279 fn leaf(&self, data: &[u8]) -> Vec<u8> {
280 Sha256::digest(data).to_vec()
281 }
282
283 fn node(&self, children: &[&[u8]]) -> Vec<u8> {
284 let mut h = Sha256::new();
285 for c in children {
286 h.update(c);
287 }
288 h.finalize().to_vec()
289 }
290
291 fn empty(&self) -> Vec<u8> {
292 Sha256::digest(b"").to_vec()
293 }
294
295 fn hash(&self, data: &[u8]) -> Vec<u8> {
296 Sha256::digest(data).to_vec()
297 }
298
299 fn clone_box(&self) -> Box<dyn Hasher> {
300 Box::new(H)
301 }
302 }
303
304 /// A single peak promotes to the member root (no bag node).
305 #[test]
306 fn single_peak_promotes() {
307 let peak = vec![0xAB; 32];
308 assert_eq!(bag_peaks(&H, std::slice::from_ref(&peak), 2), peak);
309 // And its skeleton has no bag steps for a singleton tree.
310 assert_eq!(mountain_skeleton(2, 1, 0), Some(vec![]));
311 }
312
313 /// The bag is right-recursive: for m>k the oldest k-1 peaks are direct
314 /// children and the rest bag into the last child.
315 #[test]
316 fn bag_shape_groups_rightmost_k_binary() {
317 // k=2, 4 peaks: repeatedly group the rightmost 2 →
318 // H(P0, H(P1, H(P2, P3))). (At k=2 this coincides with a right-recursive
319 // fold, which is why the binary root is unchanged from the pre-MMR log.)
320 let shape = bag_shape(4, 2).unwrap();
321 let expected = BagNode::Bag(vec![
322 BagNode::Peak(0),
323 BagNode::Bag(vec![
324 BagNode::Peak(1),
325 BagNode::Bag(vec![BagNode::Peak(2), BagNode::Peak(3)]),
326 ]),
327 ]);
328 assert_eq!(shape, expected);
329 }
330
331 /// The bag fold equals a hand-rolled rightmost-k grouping over the peaks.
332 #[test]
333 fn bag_peaks_equals_hand_fold_binary() {
334 let peaks: Vec<Vec<u8>> = (0u8..4).map(|i| vec![i; 32]).collect();
335 // H(P0, H(P1, H(P2, P3)))
336 let inner = nary_mr(&H, &[peaks[2].as_slice(), peaks[3].as_slice()]);
337 let mid = nary_mr(&H, &[peaks[1].as_slice(), inner.as_slice()]);
338 let expected = nary_mr(&H, &[peaks[0].as_slice(), mid.as_slice()]);
339 assert_eq!(bag_peaks(&H, &peaks, 2), expected);
340 }
341
342 /// k-ary: with k=3 and 5 peaks, level 0 holds P0,P1 + bag(P2..P5);
343 /// the inner bag holds P2,P3,P4 (<= k, one node).
344 #[test]
345 fn bag_shape_kary() {
346 // k=3, 4 peaks — a case where rightmost-k grouping differs from a
347 // right-recursive fold: group the rightmost 3 → H(P0, H(P1, P2, P3)).
348 let shape = bag_shape(4, 3).unwrap();
349 let expected = BagNode::Bag(vec![
350 BagNode::Peak(0),
351 BagNode::Bag(vec![BagNode::Peak(1), BagNode::Peak(2), BagNode::Peak(3)]),
352 ]);
353 assert_eq!(shape, expected);
354 }
355
356 /// The skeleton emitted for every leaf matches the bag/peak structure:
357 /// every step carries at least one sibling (no promoted step) and the
358 /// position is within bounds.
359 #[test]
360 fn skeleton_steps_are_well_formed() {
361 for k in [2u64, 3, 5] {
362 for tree_size in 1..=130u64 {
363 for index in 0..tree_size {
364 let sk = mountain_skeleton(k, tree_size, index).expect("valid position");
365 for step in &sk {
366 assert!(step.sibling_count >= 1, "k={k} n={tree_size} i={index}");
367 assert!(step.position <= step.sibling_count);
368 }
369 }
370 }
371 }
372 }
373
374 #[test]
375 fn rejects_out_of_range() {
376 assert_eq!(mountain_skeleton(1, 4, 0), None);
377 assert_eq!(mountain_skeleton(257, 4, 0), None);
378 assert_eq!(mountain_skeleton(2, 0, 0), None);
379 assert_eq!(mountain_skeleton(2, 4, 4), None);
380 }
381
382 // --- MMR essentials + durability seed (N51; N55 expands these) -----------
383
384 /// Synthetic distinct peak digests `[0x10+i; 32]` for a peak set of `m`.
385 fn peaks(m: usize) -> Vec<Vec<u8>> {
386 (0..m).map(|i| vec![0x10 + i as u8; 32]).collect()
387 }
388
389 /// **Prove-to-peak then bag verifies.** For every peak in every peak set, the
390 /// `bagPath` from that peak folds (under the abstract spine verifier, pinned by
391 /// the bag portion of `mountain_skeleton`) to the bagged root. Modelled at the
392 /// peak granularity: the peak stands in for "a leaf already lifted to its
393 /// mountain peak" (empty within-mountain `peakPath`), so the proof is purely
394 /// the `bagPath` and the skeleton is purely its bag steps.
395 #[test]
396 fn prove_to_peak_then_bag_verifies() {
397 for m in 1..=12usize {
398 let ps = peaks(m);
399 let root = bag_peaks(&H, &ps, 2);
400 for (f_idx, peak) in ps.iter().enumerate() {
401 let path = bag_path(&ps, f_idx, &H, 2);
402 // The bag skeleton for a peak: take the full skeleton of a tree
403 // whose mountain decomposition is exactly `m` height-0 peaks (a
404 // size-`m` tree at k=… would not give m singleton peaks, so build
405 // the skeleton directly from the bag shape instead).
406 let mut sk = Vec::new();
407 bag_path_steps(&bag_shape(m, 2).unwrap(), f_idx, &mut sk);
408 assert!(
409 spine::verify_inclusion(&H, peak, &sk, &path, &root),
410 "m={m} f_idx={f_idx}: prove-to-peak+bag must verify"
411 );
412 // A wrong peak digest at the same position must not verify.
413 let mut wrong = peak.clone();
414 wrong[0] ^= 0xFF;
415 assert!(
416 !spine::verify_inclusion(&H, &wrong, &sk, &path, &root),
417 "m={m} f_idx={f_idx}: a forged peak must not verify"
418 );
419 }
420 }
421 }
422
423 /// **Peak permanence across append.** A formed peak's digest is identical in
424 /// the size-`m` peak set and the size-`m+1` peak set — append adds/merges only
425 /// at the right edge and never rewrites an existing peak. (The leftmost,
426 /// oldest peaks are the most stable.)
427 #[test]
428 fn a_formed_peak_is_permanent_across_append() {
429 for m in 1..=12usize {
430 let before = peaks(m);
431 let after = peaks(m + 1);
432 // The first `m` peaks are byte-identical; only the new (rightmost)
433 // peak is added.
434 assert_eq!(&after[..m], &before[..], "m={m}: existing peaks rewritten");
435 }
436 }
437
438 /// **Durability seed.** A witness issued for a peak at size `m` (its `bagPath`)
439 /// verifies at size `m`, and the SAME peak — with a re-derived `bagPath` —
440 /// still verifies at size `m+1`. The peak's own digest (the durable prefix the
441 /// real `peakPath` would terminate at) never changes; only the short `bagPath`
442 /// suffix is extended. This is the property RFC-6962's rebalancing tree
443 /// structurally cannot have; N55 expands it to full leaf-level witnesses and a
444 /// metamorphic sweep.
445 #[test]
446 fn a_witness_survives_an_append() {
447 for m in 1..=12usize {
448 let f_idx = 0; // the oldest peak — the canonical durable witness.
449 let ps_n = peaks(m);
450 let ps_n1 = peaks(m + 1);
451
452 // The peak digest (the durable prefix endpoint) is unchanged.
453 assert_eq!(ps_n[f_idx], ps_n1[f_idx]);
454
455 // Issued at size m: verifies against the size-m root.
456 let root_n = bag_peaks(&H, &ps_n, 2);
457 let path_n = bag_path(&ps_n, f_idx, &H, 2);
458 let mut sk_n = Vec::new();
459 bag_path_steps(&bag_shape(m, 2).unwrap(), f_idx, &mut sk_n);
460 assert!(spine::verify_inclusion(
461 &H,
462 &ps_n[f_idx],
463 &sk_n,
464 &path_n,
465 &root_n
466 ));
467
468 // After append: the SAME peak, with a re-derived bagPath, verifies
469 // against the size-(m+1) root.
470 let root_n1 = bag_peaks(&H, &ps_n1, 2);
471 let path_n1 = bag_path(&ps_n1, f_idx, &H, 2);
472 let mut sk_n1 = Vec::new();
473 bag_path_steps(&bag_shape(m + 1, 2).unwrap(), f_idx, &mut sk_n1);
474 assert!(
475 spine::verify_inclusion(&H, &ps_n1[f_idx], &sk_n1, &path_n1, &root_n1),
476 "m={m}: a witness for the oldest peak must survive an append"
477 );
478 }
479 }
480}