spine/mr.rs
1//! Merkle root and evaluation over the n-ary subtree, with canonicalization.
2//!
3//! Canonicalization is one generic reduction composed of **two distinct
4//! primitives**, applied structurally on every fold (always-on, never a
5//! toggle):
6//!
7//! - **promotion** — a lone (single) child is lifted in place of the wrapping hashed node.
8//! Structurally deterministic: a verifier re-derives it.
9//! - **collapse** — children of the *same value* fold to that value. The all-null case is *one
10//! instance* of general same-value collapse, not a separate operation; an all-null run is just
11//! the dominant instance in a sparse log.
12//!
13//! The literal `nary_mr` symbol predates this vocabulary and is kept verbatim;
14//! the two primitives are named in this prose, not split in the code here.
15
16use crate::hasher::Hasher;
17use crate::proof::ProofStep;
18use crate::subtree::Subtree;
19
20/// Compute the Merkle root of an ordered sequence of child digests.
21///
22/// Applies the two canonicalization primitives: **promotion** (the lone-child
23/// case) and **collapse** (the same-value fold). Collapse is general —
24/// *any* run of equal children folds to that value, with the all-null run the
25/// dominant instance in a sparse log, not a special case. The collapse is
26/// value-dependent and always-on; there is no toggle.
27///
28/// The collapsed value's *multiplicity* (how many leaves the run spans) is not
29/// in the digest — it is committed separately as the minimal run-extent
30/// (INV-AUTH-BOUNDARY), so distinct equal-entry runs are never conflated on
31/// unroll. For non-null runs that extent is mirrored across every algorithm's
32/// tree (equal logical data ⇒ equal digest under every hash) and rides free;
33/// only null runs are per-tree-divergent and are committed (the null-run-extents
34/// in the binding root).
35#[must_use]
36pub fn nary_mr(hasher: &dyn Hasher, children: &[&[u8]]) -> Vec<u8> {
37 match children.len() {
38 0 => hasher.empty(),
39 1 => children[0].to_vec(),
40 _ => {
41 // Fixed-width contract (see `Hasher`): the node hash concatenates
42 // these child digests with no length prefix, so the child *boundaries*
43 // are recoverable only if the children share a width — otherwise a
44 // different split of the same bytes is an equally valid child list and
45 // the node digest fails to bind its children. Checked here, at the one
46 // fold boundary, so a contract violation trips in debug/test builds
47 // rather than silently producing an unbinding root.
48 //
49 // The check is mutual sibling-width-equality, not equality to
50 // `hasher.digest_len()`: a binding-root node folds *other* algorithms'
51 // member roots (raw, opaque digests — D9) under this hasher, so the
52 // children are not this hasher's own outputs. Within a single tree
53 // they are, and either way the soundness property is that the siblings
54 // agree on a width.
55 debug_assert!(
56 children.windows(2).all(|w| w[0].len() == w[1].len()),
57 "node() children must share a digest width (got widths {:?}); the unprefixed \
58 concatenation is otherwise not uniquely parseable",
59 children.iter().map(|c| c.len()).collect::<Vec<_>>()
60 );
61 // Collapse: if every child is the same value, the parent is that
62 // value. The all-null run is the dominant instance of this one rule.
63 let first = children[0];
64 if children.iter().all(|&c| c == first) {
65 first.to_vec()
66 } else {
67 hasher.node(children)
68 }
69 },
70 }
71}
72
73/// Recursively evaluate the root hash of a structured subtree.
74#[must_use]
75pub fn evaluate(hasher: &dyn Hasher, subtree: &Subtree) -> Vec<u8> {
76 match subtree {
77 Subtree::Leaf(data) => hasher.leaf(data),
78 Subtree::Node(children) => {
79 let child_hashes: Vec<Vec<u8>> = children.iter().map(|c| evaluate(hasher, c)).collect();
80 let child_refs: Vec<&[u8]> = child_hashes.iter().map(|c| c.as_slice()).collect();
81 nary_mr(hasher, &child_refs)
82 },
83 }
84}
85
86/// Count the total number of leaves in a structured subtree.
87#[must_use]
88pub fn count_leaves(subtree: &Subtree) -> u64 {
89 match subtree {
90 Subtree::Leaf(_) => 1,
91 Subtree::Node(children) => children.iter().map(count_leaves).sum(),
92 }
93}
94
95/// Generate the inclusion proof path for a leaf index within a structured subtree.
96///
97/// Returns `Some(path)` if the leaf index is found in the subtree, otherwise `None`.
98/// The `leaf_index` represents the flat 0-based leaf index inside this subtree.
99#[must_use]
100pub fn within_subtree_path(
101 hasher: &dyn Hasher,
102 subtree: &Subtree,
103 leaf_index: u64,
104) -> Option<Vec<ProofStep>> {
105 match subtree {
106 Subtree::Leaf(_) => {
107 if leaf_index == 0 {
108 Some(Vec::new())
109 } else {
110 None
111 }
112 },
113 Subtree::Node(children) => {
114 let mut cumulative_leaves = 0;
115 for (child_idx, child) in children.iter().enumerate() {
116 let child_leaves = count_leaves(child);
117 if leaf_index < cumulative_leaves + child_leaves {
118 let mut path =
119 within_subtree_path(hasher, child, leaf_index - cumulative_leaves)?;
120
121 // Canonical proof encoding: a promoted (lone-child) node is
122 // lifted to its parent without hashing, so it contributes no
123 // proof step (the recursion already returns the correct
124 // sub-path). Only genuinely hashing (multi-child) nodes emit
125 // a step.
126 if children.len() > 1 {
127 let mut child_hashes = Vec::with_capacity(children.len());
128 for c in children {
129 child_hashes.push(evaluate(hasher, c));
130 }
131 child_hashes.remove(child_idx);
132 path.push(ProofStep {
133 siblings: child_hashes,
134 position: child_idx,
135 });
136 }
137 return Some(path);
138 }
139 cumulative_leaves += child_leaves;
140 }
141 None
142 },
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use sha2::{Digest, Sha256};
149
150 use super::*;
151
152 #[derive(Debug)]
153 struct Sha256Hasher;
154
155 impl Hasher for Sha256Hasher {
156 fn leaf(&self, data: &[u8]) -> Vec<u8> {
157 Sha256::digest(data).to_vec()
158 }
159
160 fn node(&self, children: &[&[u8]]) -> Vec<u8> {
161 let mut h = Sha256::new();
162 for child in children {
163 h.update(child);
164 }
165 h.finalize().to_vec()
166 }
167
168 fn empty(&self) -> Vec<u8> {
169 Sha256::digest(b"").to_vec()
170 }
171
172 fn hash(&self, data: &[u8]) -> Vec<u8> {
173 Sha256::digest(data).to_vec()
174 }
175
176 fn clone_box(&self) -> Box<dyn Hasher> {
177 Box::new(Sha256Hasher)
178 }
179 }
180
181 #[test]
182 fn test_nary_mr_empty() {
183 let hasher = Sha256Hasher;
184 let expected = Sha256::digest(b"").to_vec();
185 assert_eq!(nary_mr(&hasher, &[]), expected);
186 }
187
188 #[test]
189 fn test_nary_mr_promotion() {
190 let hasher = Sha256Hasher;
191 let leaf_hash = hasher.leaf(b"hello");
192 assert_eq!(nary_mr(&hasher, &[&leaf_hash]), leaf_hash);
193 }
194
195 #[test]
196 fn test_nary_mr_null_collapse() {
197 let hasher = Sha256Hasher;
198 let null = hasher.null();
199 // A node with two null children should collapse to null
200 assert_eq!(nary_mr(&hasher, &[&null, &null]), null);
201
202 // A node with a mix of null and non-null should NOT collapse to null
203 let leaf = hasher.leaf(b"hello");
204 let expected = hasher.node(&[&null, &leaf]);
205 assert_eq!(nary_mr(&hasher, &[&null, &leaf]), expected);
206 }
207
208 #[test]
209 fn test_nary_mr_general_same_value_collapse() {
210 let hasher = Sha256Hasher;
211 // General collapse: all children EQUAL to the same non-null value fold to
212 // that value. Null is one instance of this, not the rule.
213 let v = hasher.leaf(b"hello");
214 assert_eq!(nary_mr(&hasher, &[&v, &v]), v);
215 assert_eq!(nary_mr(&hasher, &[&v, &v, &v]), v);
216
217 // A mix of two distinct non-null values must NOT collapse — it hashes.
218 let w = hasher.leaf(b"world");
219 let expected = hasher.node(&[&v, &w]);
220 assert_eq!(nary_mr(&hasher, &[&v, &w]), expected);
221 assert_ne!(nary_mr(&hasher, &[&v, &w]), v);
222
223 // The null collapse is exactly the same rule at value = null().
224 let null = hasher.null();
225 assert_eq!(nary_mr(&hasher, &[&null, &null]), null);
226 }
227
228 #[test]
229 fn null_is_distinct_from_empty_data_leaf() {
230 // Hard constraint: null() = H(b"null") MUST stay distinct from a genuine
231 // empty-data leaf leaf(b"") = H(b""), so the null subset of collapses is
232 // unambiguous. The preimages differ (4 bytes vs 0), so the digests differ.
233 let hasher = Sha256Hasher;
234 assert_ne!(hasher.null(), hasher.leaf(b""));
235 // And a two-empty-leaf node collapses to the empty-leaf value (general
236 // collapse), which is itself distinct from null() — collapse never
237 // conflates an empty-data run with a null run.
238 let empty_leaf = hasher.leaf(b"");
239 assert_eq!(nary_mr(&hasher, &[&empty_leaf, &empty_leaf]), empty_leaf);
240 assert_ne!(nary_mr(&hasher, &[&empty_leaf, &empty_leaf]), hasher.null());
241 }
242
243 #[test]
244 fn test_evaluate_promotion_chain() {
245 let hasher = Sha256Hasher;
246 // Node([Node([Leaf("x")])])
247 let tree = Subtree::Node(vec![Subtree::Node(vec![Subtree::Leaf(b"x".to_vec())])]);
248 let expected = hasher.leaf(b"x");
249 assert_eq!(evaluate(&hasher, &tree), expected);
250 }
251
252 #[test]
253 fn test_within_subtree_path() {
254 let hasher = Sha256Hasher;
255 // Subtree: Node([Leaf("a"), Node([Leaf("b"), Leaf("c")]), Leaf("d")])
256 let subtree = Subtree::Node(vec![
257 Subtree::Leaf(b"a".to_vec()),
258 Subtree::Node(vec![
259 Subtree::Leaf(b"b".to_vec()),
260 Subtree::Leaf(b"c".to_vec()),
261 ]),
262 Subtree::Leaf(b"d".to_vec()),
263 ]);
264
265 let path = within_subtree_path(&hasher, &subtree, 1).unwrap();
266 assert_eq!(path.len(), 2);
267
268 let leaf_hash = hasher.leaf(b"b");
269 let root = evaluate(&hasher, &subtree);
270 let proof = crate::proof::InclusionProof { path };
271 // An explicit subtree carries no frontier grouping, so every step is a
272 // hash-chained subtree-prefix step and the skeleton is empty.
273 assert!(crate::proof::verify_inclusion(
274 &hasher,
275 &leaf_hash,
276 &[],
277 &proof.path,
278 &root
279 ));
280 }
281}