1mod error;
30mod proof;
31pub mod shape;
32mod tree;
33
34pub use error::{Error, Result};
35pub use shape::{rebalanced_bag, rebalanced_skeleton};
39pub use spine::{Hasher, LeafProof, ProofStep, Seal, verify_inclusion};
42pub use tree::{Cmt, Config};
43
44#[cfg(test)]
45mod tests {
46 use sha2::{Digest, Sha256};
47
48 use super::*;
49
50 #[derive(Debug)]
51 struct Sha256Hasher;
52
53 impl spine::Hasher for Sha256Hasher {
54 fn leaf(&self, data: &[u8]) -> Vec<u8> {
55 Sha256::digest(data).to_vec()
56 }
57
58 fn node(&self, children: &[&[u8]]) -> Vec<u8> {
59 let mut h = Sha256::new();
60 for child in children {
61 h.update(child);
62 }
63 h.finalize().to_vec()
64 }
65
66 fn empty(&self) -> Vec<u8> {
67 Sha256::digest(b"").to_vec()
68 }
69
70 fn hash(&self, data: &[u8]) -> Vec<u8> {
71 Sha256::digest(data).to_vec()
72 }
73
74 fn clone_box(&self) -> Box<dyn spine::Hasher> {
75 Box::new(Sha256Hasher)
76 }
77 }
78
79 const ALG: u64 = 0;
80 const K: u64 = 2;
81
82 fn tree_with(payloads: &[&[u8]]) -> Cmt {
83 let mut t = Cmt::new(Config { arity: K }).expect("valid arity");
84 t.register_algorithm(ALG, Box::new(Sha256Hasher))
85 .expect("fresh alg");
86 for (i, p) in payloads.iter().enumerate() {
87 t.set(i as u64, p.to_vec(), Vec::new()).expect("dense set");
88 }
89 t
90 }
91
92 #[test]
93 fn empty_tree_has_no_root() {
94 let t = Cmt::new(Config::default()).unwrap();
95 assert!(t.is_empty());
96 assert_eq!(t.root(ALG), None);
97 }
98
99 #[test]
100 fn rejects_out_of_range_arity() {
101 assert_eq!(
102 Cmt::new(Config { arity: 1 }).unwrap_err(),
103 Error::InvalidArity(1)
104 );
105 assert_eq!(
106 Cmt::new(Config { arity: 257 }).unwrap_err(),
107 Error::InvalidArity(257)
108 );
109 }
110
111 #[test]
112 fn set_get_round_trips_and_overwrites() {
113 let mut t = tree_with(&[b"a", b"b", b"c"]);
114 assert_eq!(t.get(1), Some(b"b".as_slice()));
115 t.set(1, b"B".to_vec(), Vec::new()).unwrap();
116 assert_eq!(t.get(1), Some(b"B".as_slice()));
117 assert_eq!(t.len(), 3);
118 }
119
120 #[test]
121 fn set_rejects_a_gap() {
122 let mut t = tree_with(&[b"a"]);
123 assert_eq!(
124 t.set(5, b"x".to_vec(), Vec::new()).unwrap_err(),
125 Error::IndexGap { index: 5, len: 1 }
126 );
127 }
128
129 #[test]
130 fn metadata_is_carried_verbatim_and_uninterpreted() {
131 let mut t = tree_with(&[b"a"]);
132 let meta = vec![0xDE, 0xAD, 0xBE, 0xEF];
133 t.set(0, b"a".to_vec(), meta.clone()).unwrap();
134 assert_eq!(t.metadata(0), Some(meta.as_slice()));
135 let r_with_meta = t.root(ALG).unwrap();
138 let mut t2 = tree_with(&[b"a"]);
139 t2.set(0, b"a".to_vec(), vec![0x00]).unwrap();
140 assert_eq!(t2.root(ALG).unwrap(), r_with_meta);
141 }
142
143 #[test]
146 fn root_matches_spine_evaluate() {
147 for size in 1u64..=20 {
148 let payloads: Vec<Vec<u8>> = (0..size).map(|i| format!("p{i}").into_bytes()).collect();
149 let refs: Vec<&[u8]> = payloads.iter().map(Vec::as_slice).collect();
150 let t = tree_with(&refs);
151 let expected = spine_root(&payloads);
152 assert_eq!(t.root(ALG).unwrap(), expected, "size={size}");
153 }
154 }
155
156 fn spine_root(payloads: &[Vec<u8>]) -> Vec<u8> {
159 use spine::{Subtree, frontier_for_size};
160 let h = Sha256Hasher;
161 let leaves: Vec<Subtree> = payloads.iter().map(|p| Subtree::Leaf(p.clone())).collect();
162 let coords = frontier_for_size(payloads.len() as u64, K);
164 let mut frontier: Vec<Subtree> = coords
165 .iter()
166 .map(|&(left, height)| perfect(&leaves, left, height))
167 .collect();
168 let k = K as usize;
169 while frontier.len() > k {
170 let split = frontier.len() - k;
171 let group = frontier.split_off(split);
172 frontier.push(Subtree::Node(group));
173 }
174 let shape = if frontier.len() == 1 {
175 frontier.pop().unwrap()
176 } else {
177 Subtree::Node(frontier)
178 };
179 spine::evaluate(&h, &shape)
180 }
181
182 fn perfect(leaves: &[spine::Subtree], left: u64, height: u32) -> spine::Subtree {
183 use spine::Subtree;
184 if height == 0 {
185 return leaves[left as usize].clone();
186 }
187 let span = K.pow(height - 1);
188 let children = (0..K)
189 .map(|c| perfect(leaves, left + c * span, height - 1))
190 .collect();
191 Subtree::Node(children)
192 }
193
194 #[test]
195 fn inclusion_proof_verifies_against_spine() {
196 for size in 1u64..=20 {
197 let payloads: Vec<Vec<u8>> = (0..size).map(|i| format!("v{i}").into_bytes()).collect();
198 let refs: Vec<&[u8]> = payloads.iter().map(Vec::as_slice).collect();
199 let t = tree_with(&refs);
200 let root = t.root(ALG).unwrap();
201 let h = Sha256Hasher;
202 for index in 0..size {
203 let (leaf, path) = t.inclusion_proof(ALG, index).unwrap();
204 let skeleton = rebalanced_skeleton(size, K, index).expect("valid position");
205 assert!(
206 spine::verify_inclusion(&h, &leaf, &skeleton, &path, &root),
207 "size={size} index={index}"
208 );
209 }
210 }
211 }
212
213 #[test]
219 fn inclusion_proof_uses_cache_zero_misses() {
220 let size = 32u64;
223 let payloads: Vec<Vec<u8>> = (0..size).map(|i| format!("p{i}").into_bytes()).collect();
224 let refs: Vec<&[u8]> = payloads.iter().map(Vec::as_slice).collect();
225 let t = tree_with(&refs);
226 for index in 0..size {
227 let (_, _, misses) = t
228 .inclusion_proof_miss_count(ALG, index)
229 .expect("in-range index");
230 assert_eq!(
231 misses, 0,
232 "inclusion_proof for index={index} size={size} had {misses} cache miss(es): \
233 off-path siblings must be served from the materialized cache (F4)"
234 );
235 }
236 }
237
238 #[test]
239 fn inclusion_proof_rejects_wrong_leaf() {
240 let t = tree_with(&[b"a", b"b", b"c", b"d"]);
241 let root = t.root(ALG).unwrap();
242 let h = Sha256Hasher;
243 let (_, path) = t.inclusion_proof(ALG, 2).unwrap();
244 let forged = h.leaf(b"not-c");
245 let skeleton = rebalanced_skeleton(4, K, 2).expect("valid position");
246 assert!(!spine::verify_inclusion(
247 &h, &forged, &skeleton, &path, &root
248 ));
249 }
250
251 #[test]
252 fn non_membership_of_a_null_cell_verifies() {
253 let h = Sha256Hasher;
256 let mut t = tree_with(&[b"real", b"x", b"y"]);
257 t.set(1, b"null".to_vec(), Vec::new()).unwrap();
258 let root = t.root(ALG).unwrap();
259 let (leaf, path) = t.non_membership_proof(ALG, 1).expect("cell hashes to null");
260 assert_eq!(leaf, h.null());
261 let skeleton = rebalanced_skeleton(t.len(), K, 1).expect("valid position");
262 assert!(spine::verify_inclusion(&h, &leaf, &skeleton, &path, &root));
263 assert_eq!(t.non_membership_proof(ALG, 0), None);
265 }
266
267 #[test]
268 fn leaf_proof_accepts_legit_and_rejects_forged() {
269 let h = Sha256Hasher;
270 for size in 1u64..=20 {
271 let payloads: Vec<Vec<u8>> = (0..size).map(|i| format!("v{i}").into_bytes()).collect();
272 let refs: Vec<&[u8]> = payloads.iter().map(Vec::as_slice).collect();
273 let t = tree_with(&refs);
274 let root = t.root(ALG).unwrap();
275 for index in 0..size {
276 let proof = t.leaf_proof(ALG, index).expect("in range");
277 let skeleton = rebalanced_skeleton(size, K, index).expect("valid position");
278 assert!(
280 proof.verify(&h, &skeleton, &root),
281 "size={size} index={index}"
282 );
283 let mut forged = proof.clone();
285 forged.leaf_hash = h.leaf(b"forged");
286 assert!(
287 !forged.verify(&h, &skeleton, &root),
288 "size={size} index={index}"
289 );
290 }
291 }
292 let t = tree_with(&[b"a", b"b"]);
294 assert!(t.leaf_proof(ALG, 2).is_none());
295 assert!(t.leaf_proof(99, 0).is_none());
296 }
297
298 #[test]
299 fn seal_is_one_way_into_structural_seal() {
300 let t = tree_with(&[b"a", b"b", b"c"]);
301 let root = t.root(ALG).unwrap();
302 let sealed = t.seal().expect("non-empty seal");
303 assert_eq!(sealed.tree_size(), 3);
304 assert_eq!(sealed.arity(), 2);
305 assert!(sealed.peaks(ALG).is_some());
309 assert_eq!(
310 sealed.member_root(ALG, &Sha256Hasher, rebalanced_bag),
311 Some(root)
312 );
313 }
315
316 #[test]
317 fn empty_tree_cannot_be_sealed() {
318 let t = Cmt::new(Config::default()).unwrap();
319 assert_eq!(t.seal().unwrap_err(), Error::EmptySeal);
320 }
321}