1use sha2::{Sha256, Digest};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum MerkleError {
11 UnknownVersion(u8),
16}
17
18impl std::fmt::Display for MerkleError {
19 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20 match self {
21 Self::UnknownVersion(v) => write!(
22 f,
23 "unknown merkle_version {} (expected {} or {})",
24 v, MERKLE_VERSION_V1, MERKLE_VERSION_V2,
25 ),
26 }
27 }
28}
29
30impl std::error::Error for MerkleError {}
31
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
34pub enum Direction {
35 Left,
36 Right,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct ProofStep {
42 pub direction: Direction,
43 pub hash: String,
45}
46
47pub const MERKLE_ALGORITHM_V1: &str = "sha256-duplicate-last";
49pub const MERKLE_ALGORITHM_V2: &str = "sha256-rfc9162";
50
51pub const MERKLE_VERSION_V1: u8 = 1;
66pub const MERKLE_VERSION_V2: u8 = 2;
67
68pub fn default_merkle_version_v1() -> u8 {
72 MERKLE_VERSION_V1
73}
74
75pub(crate) fn hash_leaf_v1(artifact_id: &str) -> [u8; 32] {
83 Sha256::digest(artifact_id.as_bytes()).into()
84}
85
86pub(crate) fn hash_internal_v1(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] {
88 let mut h = Sha256::new();
89 h.update(left);
90 h.update(right);
91 h.finalize().into()
92}
93
94pub(crate) fn hash_leaf_v2(artifact_id: &str) -> [u8; 32] {
96 let mut h = Sha256::new();
97 h.update([0x00u8]);
98 h.update(artifact_id.as_bytes());
99 h.finalize().into()
100}
101
102pub(crate) fn hash_internal_v2(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] {
104 let mut h = Sha256::new();
105 h.update([0x01u8]);
106 h.update(left);
107 h.update(right);
108 h.finalize().into()
109}
110
111pub(crate) fn hash_leaf(version: u8, artifact_id: &str) -> Result<[u8; 32], MerkleError> {
116 match version {
117 MERKLE_VERSION_V1 => Ok(hash_leaf_v1(artifact_id)),
118 MERKLE_VERSION_V2 => Ok(hash_leaf_v2(artifact_id)),
119 other => Err(MerkleError::UnknownVersion(other)),
120 }
121}
122
123pub(crate) fn hash_internal(
126 version: u8,
127 left: &[u8; 32],
128 right: &[u8; 32],
129) -> Result<[u8; 32], MerkleError> {
130 match version {
131 MERKLE_VERSION_V1 => Ok(hash_internal_v1(left, right)),
132 MERKLE_VERSION_V2 => Ok(hash_internal_v2(left, right)),
133 other => Err(MerkleError::UnknownVersion(other)),
134 }
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct InclusionProof {
140 pub leaf_index: usize,
141 pub leaf_hash: String,
143 pub path: Vec<ProofStep>,
144 #[serde(default, skip_serializing_if = "Option::is_none")]
146 pub algorithm: Option<String>,
147 #[serde(default = "default_merkle_version_v1")]
152 pub merkle_version: u8,
153}
154
155pub struct MerkleTree {
168 leaves: Vec<[u8; 32]>,
170 version: u8,
172}
173
174impl Default for MerkleTree {
175 fn default() -> Self {
176 Self::new()
177 }
178}
179
180impl MerkleTree {
181 pub fn new() -> Self {
183 Self::with_version(MERKLE_VERSION_V2)
185 .expect("MERKLE_VERSION_V2 is always a valid version")
186 }
187
188 pub fn with_version(version: u8) -> Result<Self, MerkleError> {
194 if version != MERKLE_VERSION_V1 && version != MERKLE_VERSION_V2 {
195 return Err(MerkleError::UnknownVersion(version));
196 }
197 Ok(Self { leaves: Vec::new(), version })
198 }
199
200 pub fn version(&self) -> u8 {
202 self.version
203 }
204
205 pub fn append(&mut self, artifact_id: &str) -> usize {
207 let hash = hash_leaf(self.version, artifact_id)
210 .expect("tree version validated at construction");
211 self.leaves.push(hash);
212 self.leaves.len() - 1
213 }
214
215 pub fn len(&self) -> usize {
217 self.leaves.len()
218 }
219
220 pub fn is_empty(&self) -> bool {
222 self.leaves.is_empty()
223 }
224
225 pub fn root(&self) -> Option<[u8; 32]> {
227 if self.leaves.is_empty() {
228 return None;
229 }
230 Some(self.compute_root(&self.leaves))
231 }
232
233 pub fn height(&self) -> usize {
235 if self.leaves.len() <= 1 {
236 return 0;
237 }
238 (self.leaves.len() as f64).log2().ceil() as usize
239 }
240
241 pub fn inclusion_proof(&self, leaf_index: usize) -> Option<InclusionProof> {
243 if leaf_index >= self.leaves.len() {
244 return None;
245 }
246
247 let mut path = Vec::new();
248 let mut idx = leaf_index;
249 let mut level: Vec<[u8; 32]> = self.leaves.clone();
250
251 while level.len() > 1 {
252 if idx + 1 < level.len() && idx % 2 == 0 {
255 path.push(ProofStep {
257 direction: Direction::Right,
258 hash: hex::encode(level[idx + 1]),
259 });
260 } else if idx % 2 == 1 {
261 path.push(ProofStep {
263 direction: Direction::Left,
264 hash: hex::encode(level[idx - 1]),
265 });
266 }
267 let mut next_level = Vec::with_capacity((level.len() + 1) / 2);
273 let mut i = 0;
274 while i + 1 < level.len() {
275 next_level.push(
276 hash_internal(self.version, &level[i], &level[i + 1])
277 .expect("tree version validated at construction"),
278 );
279 i += 2;
280 }
281 if i < level.len() {
282 next_level.push(level[i]);
283 }
284 level = next_level;
285
286 idx /= 2;
287 }
288
289 Some(InclusionProof {
290 leaf_index,
291 leaf_hash: hex::encode(self.leaves[leaf_index]),
292 path,
293 algorithm: Some(match self.version {
294 MERKLE_VERSION_V1 => MERKLE_ALGORITHM_V1.to_string(),
295 _ => MERKLE_ALGORITHM_V2.to_string(),
296 }),
297 merkle_version: self.version,
298 })
299 }
300
301 pub fn verify_proof(
321 expected_version: u8,
322 root_hex: &str,
323 artifact_id: &str,
324 proof: &InclusionProof,
325 ) -> bool {
326 if expected_version != MERKLE_VERSION_V1 && expected_version != MERKLE_VERSION_V2 {
329 return false;
330 }
331
332 if proof.merkle_version != expected_version {
336 return false;
337 }
338
339 if let Some(algo) = proof.algorithm.as_deref() {
344 if algo != MERKLE_ALGORITHM_V1 && algo != MERKLE_ALGORITHM_V2 {
345 return false;
346 }
347 }
348
349 if expected_version == MERKLE_VERSION_V2 && proof.path.is_empty() && proof.leaf_index != 0 {
354 return false;
355 }
356
357 let current: [u8; 32] = match hash_leaf(expected_version, artifact_id) {
358 Ok(h) => h,
359 Err(_) => return false,
360 };
361 if hex::encode(current) != proof.leaf_hash {
363 return false;
364 }
365
366 let mut current = current;
367 for step in &proof.path {
368 let sibling = match hex::decode(&step.hash) {
369 Ok(b) if b.len() == 32 => {
370 let mut arr = [0u8; 32];
371 arr.copy_from_slice(&b);
372 arr
373 }
374 _ => return false,
375 };
376
377 current = match step.direction {
378 Direction::Right => match hash_internal(expected_version, ¤t, &sibling) {
379 Ok(h) => h,
380 Err(_) => return false,
381 },
382 Direction::Left => match hash_internal(expected_version, &sibling, ¤t) {
383 Ok(h) => h,
384 Err(_) => return false,
385 },
386 };
387 }
388
389 hex::encode(current) == root_hex
390 }
391
392 fn compute_root(&self, leaves: &[[u8; 32]]) -> [u8; 32] {
395 if leaves.len() == 1 {
396 return leaves[0];
397 }
398 let mut level = leaves.to_vec();
399 while level.len() > 1 {
400 let mut next_level = Vec::with_capacity((level.len() + 1) / 2);
401 let mut i = 0;
402 while i + 1 < level.len() {
403 next_level.push(
405 hash_internal(self.version, &level[i], &level[i + 1])
406 .expect("tree version validated at construction"),
407 );
408 i += 2;
409 }
410 if i < level.len() {
412 next_level.push(level[i]);
413 }
414 level = next_level;
415 }
416 level[0]
417 }
418
419 pub fn consistency_proof(&self, old_size: usize) -> Option<Vec<String>> {
426 let n = self.leaves.len();
427 if old_size == 0 || old_size > n {
428 return None;
429 }
430 if old_size == n {
431 return Some(Vec::new());
432 }
433 Some(
434 subproof(self.version, &self.leaves[..n], old_size, true)
435 .into_iter()
436 .map(hex::encode)
437 .collect(),
438 )
439 }
440}
441
442fn merkle_root(version: u8, leaves: &[[u8; 32]]) -> [u8; 32] {
445 if leaves.len() == 1 {
446 return leaves[0];
447 }
448 let mut level = leaves.to_vec();
449 while level.len() > 1 {
450 let mut next = Vec::with_capacity(level.len().div_ceil(2));
451 let mut i = 0;
452 while i + 1 < level.len() {
453 next.push(
454 hash_internal(version, &level[i], &level[i + 1])
455 .expect("tree version validated at construction"),
456 );
457 i += 2;
458 }
459 if i < level.len() {
460 next.push(level[i]);
461 }
462 level = next;
463 }
464 level[0]
465}
466
467fn largest_pow2_lt(n: usize) -> usize {
469 let mut k = 1usize;
470 while k < n {
471 k <<= 1;
472 }
473 k >> 1
474}
475
476fn subproof(version: u8, leaves: &[[u8; 32]], m: usize, b: bool) -> Vec<[u8; 32]> {
479 let n = leaves.len();
480 if m == n {
481 return if b {
482 Vec::new()
483 } else {
484 vec![merkle_root(version, leaves)]
485 };
486 }
487 let k = largest_pow2_lt(n);
488 if m <= k {
489 let mut p = subproof(version, &leaves[..k], m, b);
490 p.push(merkle_root(version, &leaves[k..]));
491 p
492 } else {
493 let mut p = subproof(version, &leaves[k..], m - k, false);
494 p.push(merkle_root(version, &leaves[..k]));
495 p
496 }
497}
498
499fn decode_hash(hex_str: &str) -> Option<[u8; 32]> {
500 let bytes = hex::decode(hex_str).ok()?;
501 if bytes.len() != 32 {
502 return None;
503 }
504 let mut arr = [0u8; 32];
505 arr.copy_from_slice(&bytes);
506 Some(arr)
507}
508
509pub fn verify_consistency(
514 version: u8,
515 old_size: usize,
516 old_root_hex: &str,
517 new_size: usize,
518 new_root_hex: &str,
519 proof: &[String],
520) -> bool {
521 if version != MERKLE_VERSION_V1 && version != MERKLE_VERSION_V2 {
522 return false;
523 }
524 let (Some(old_root), Some(new_root)) =
525 (decode_hash(old_root_hex), decode_hash(new_root_hex))
526 else {
527 return false;
528 };
529 if old_size > new_size {
530 return false;
531 }
532 if old_size == new_size {
533 return proof.is_empty() && old_root == new_root;
534 }
535 if old_size == 0 {
536 return proof.is_empty();
537 }
538
539 let mut it = proof.iter();
540 let mut node = old_size - 1;
541 let mut last = new_size - 1;
542 while node & 1 == 1 {
544 node >>= 1;
545 last >>= 1;
546 }
547 let (mut old_hash, mut new_hash) = if node != 0 {
550 let Some(seed) = it.next().and_then(|s| decode_hash(s)) else {
551 return false;
552 };
553 (seed, seed)
554 } else {
555 (old_root, old_root)
556 };
557 while node != 0 {
558 if node & 1 == 1 {
559 let Some(sib) = it.next().and_then(|s| decode_hash(s)) else {
560 return false;
561 };
562 old_hash = hash_internal(version, &sib, &old_hash).expect("version validated");
563 new_hash = hash_internal(version, &sib, &new_hash).expect("version validated");
564 } else if node < last {
565 let Some(sib) = it.next().and_then(|s| decode_hash(s)) else {
566 return false;
567 };
568 new_hash = hash_internal(version, &new_hash, &sib).expect("version validated");
569 }
570 node >>= 1;
571 last >>= 1;
572 }
573 while last != 0 {
575 let Some(sib) = it.next().and_then(|s| decode_hash(s)) else {
576 return false;
577 };
578 new_hash = hash_internal(version, &new_hash, &sib).expect("version validated");
579 last >>= 1;
580 }
581 it.next().is_none() && old_hash == old_root && new_hash == new_root
582}
583
584#[cfg(test)]
585mod tests {
586 use super::*;
587
588 fn prefix_root(version: u8, ids: &[String], m: usize) -> String {
590 let mut t = MerkleTree::with_version(version).unwrap();
591 for id in &ids[..m] {
592 t.append(id);
593 }
594 hex::encode(t.root().unwrap())
595 }
596
597 #[test]
601 fn consistency_roundtrip_and_tamper() {
602 for &version in &[MERKLE_VERSION_V1, MERKLE_VERSION_V2] {
606 for n in 1..=20usize {
607 let ids: Vec<String> = (0..n).map(|i| format!("art_{i:06x}")).collect();
608 let mut tree = MerkleTree::with_version(version).unwrap();
609 for id in &ids {
610 tree.append(id);
611 }
612 let root_n = hex::encode(tree.root().unwrap());
613
614 for m in 1..=n {
615 let root_m = prefix_root(version, &ids, m);
616 let proof = tree.consistency_proof(m).expect("proof exists");
617
618 assert!(
619 verify_consistency(version, m, &root_m, n, &root_n, &proof),
620 "v{version}: consistency m={m} n={n} should verify"
621 );
622
623 if m < n {
624 let zero = "00".repeat(32);
625 assert!(
627 !verify_consistency(version, m, &zero, n, &root_n, &proof),
628 "v{version} m={m} n={n}: tampered old_root must reject"
629 );
630 assert!(
631 !verify_consistency(version, m, &root_m, n, &zero, &proof),
632 "v{version} m={m} n={n}: tampered new_root must reject"
633 );
634 if !proof.is_empty() {
636 let mut bad = proof.clone();
637 bad[0] = zero.clone();
638 assert!(
639 !verify_consistency(version, m, &root_m, n, &root_n, &bad),
640 "v{version} m={m} n={n}: mutated proof must reject"
641 );
642 }
643 }
644 }
645 }
646 }
647 }
648
649 #[test]
650 fn consistency_edge_cases() {
651 let mut t = MerkleTree::default();
652 for i in 0..5 {
653 t.append(&format!("art_{i}"));
654 }
655 let root5 = hex::encode(t.root().unwrap());
656 assert_eq!(t.consistency_proof(5), Some(Vec::new()));
658 assert!(verify_consistency(t.version(), 5, &root5, 5, &root5, &[]));
659 assert_eq!(t.consistency_proof(0), None);
661 assert_eq!(t.consistency_proof(6), None);
662 assert!(!verify_consistency(t.version(), 6, &root5, 5, &root5, &[]));
664 assert!(!verify_consistency(99, 1, &root5, 5, &root5, &[]));
666 }
667
668 #[test]
669 fn single_leaf_root_is_leaf_hash() {
670 let mut tree = MerkleTree::new();
673 tree.append("art_abc123");
674 let root = tree.root().unwrap();
675 let expected = hash_leaf_v2("art_abc123");
676 assert_eq!(root, expected);
677 }
678
679 #[test]
680 fn single_leaf_root_is_leaf_hash_v1_legacy() {
681 let mut tree = MerkleTree::with_version(MERKLE_VERSION_V1).unwrap();
682 tree.append("art_abc123");
683 let root = tree.root().unwrap();
684 let expected = hash_leaf_v1("art_abc123");
685 assert_eq!(root, expected);
686 }
687
688 #[test]
689 fn inclusion_proof_verifies() {
690 let mut tree = MerkleTree::new();
691 let ids = ["art_a", "art_b", "art_c", "art_d"];
692 for id in &ids {
693 tree.append(id);
694 }
695
696 let root = hex::encode(tree.root().unwrap());
697 let proof = tree.inclusion_proof(1).unwrap(); assert!(MerkleTree::verify_proof(MERKLE_VERSION_V2, &root, "art_b", &proof));
700 }
701
702 #[test]
703 fn wrong_artifact_fails_verification() {
704 let mut tree = MerkleTree::new();
705 tree.append("art_a");
706 tree.append("art_b");
707
708 let root = hex::encode(tree.root().unwrap());
709 let proof = tree.inclusion_proof(0).unwrap(); assert!(!MerkleTree::verify_proof(MERKLE_VERSION_V2, &root, "art_WRONG", &proof));
713 }
714
715 #[test]
716 fn tampered_sibling_fails() {
717 let mut tree = MerkleTree::new();
718 tree.append("art_a");
719 tree.append("art_b");
720
721 let root = hex::encode(tree.root().unwrap());
722 let mut proof = tree.inclusion_proof(0).unwrap();
723
724 proof.path[0].hash = "0".repeat(64);
726
727 assert!(!MerkleTree::verify_proof(MERKLE_VERSION_V2, &root, "art_a", &proof));
728 }
729
730 #[test]
737 fn v2_leaf_uses_0x00_prefix() {
738 let got = hash_leaf_v2("art_test");
739 let mut h = Sha256::new();
740 h.update([0x00u8]);
741 h.update(b"art_test");
742 let expected: [u8; 32] = h.finalize().into();
743 assert_eq!(got, expected);
744 }
745
746 #[test]
747 fn v2_internal_uses_0x01_prefix() {
748 let left = [0x11u8; 32];
749 let right = [0x22u8; 32];
750 let got = hash_internal_v2(&left, &right);
751 let mut h = Sha256::new();
752 h.update([0x01u8]);
753 h.update(left);
754 h.update(right);
755 let expected: [u8; 32] = h.finalize().into();
756 assert_eq!(got, expected);
757 }
758
759 #[test]
760 fn v1_legacy_root_unchanged() {
761 let ids = ["art_a", "art_b", "art_c", "art_d"];
764 let mut leaves: Vec<[u8; 32]> = ids.iter().map(|id| hash_leaf_v1(id)).collect();
765 while leaves.len() > 1 {
766 let mut next = Vec::with_capacity((leaves.len() + 1) / 2);
767 let mut i = 0;
768 while i + 1 < leaves.len() {
769 next.push(hash_internal_v1(&leaves[i], &leaves[i + 1]));
770 i += 2;
771 }
772 if i < leaves.len() {
773 next.push(leaves[i]);
774 }
775 leaves = next;
776 }
777 let got = hex::encode(leaves[0]);
782 let expected = "cb4c9e4a9374ea3917b9ba75554ce8908a593db1183f1af48edf41fa3eb67b0d";
786 assert_eq!(
787 got, expected,
788 "v1 root drifted; v0.10.2 receipts will fail to verify",
789 );
790 }
791
792 #[test]
793 fn v2_differs_from_v1_for_same_input() {
794 assert_ne!(hash_leaf_v1("x"), hash_leaf_v2("x"));
796 let l = [0x33u8; 32];
797 let r = [0x44u8; 32];
798 assert_ne!(hash_internal_v1(&l, &r), hash_internal_v2(&l, &r));
799 }
800
801 #[test]
802 fn odd_number_of_leaves() {
803 let mut tree = MerkleTree::new();
805 for i in 0..5 {
806 tree.append(&format!("art_{}", i));
807 }
808
809 let root = hex::encode(tree.root().unwrap());
810 let proof = tree.inclusion_proof(4).unwrap(); assert!(MerkleTree::verify_proof(MERKLE_VERSION_V2, &root, "art_4", &proof));
813 }
814
815 #[test]
818 fn v2_verify_round_trip() {
819 let mut tree = MerkleTree::new();
820 for id in &["art_a", "art_b", "art_c", "art_d"] {
821 tree.append(id);
822 }
823 assert_eq!(tree.version(), MERKLE_VERSION_V2);
824
825 let root = hex::encode(tree.root().unwrap());
826 let proof = tree.inclusion_proof(1).unwrap();
827 assert_eq!(proof.merkle_version, MERKLE_VERSION_V2);
828 assert!(MerkleTree::verify_proof(MERKLE_VERSION_V2, &root, "art_b", &proof));
829 }
830
831 #[test]
832 fn v2_rejects_v1_proof() {
833 let mut tree = MerkleTree::new();
837 for id in &["art_a", "art_b", "art_c", "art_d"] {
838 tree.append(id);
839 }
840 let root = hex::encode(tree.root().unwrap());
841 let mut proof = tree.inclusion_proof(1).unwrap();
842 proof.merkle_version = MERKLE_VERSION_V1;
843 assert!(
844 !MerkleTree::verify_proof(MERKLE_VERSION_V2, &root, "art_b", &proof),
845 "v2 verifier must reject a proof that downgrades itself to v1",
846 );
847 }
848
849 #[test]
850 fn v1_rejects_v2_proof() {
851 let mut tree = MerkleTree::with_version(MERKLE_VERSION_V1).unwrap();
854 for id in &["art_a", "art_b", "art_c", "art_d"] {
855 tree.append(id);
856 }
857 let root = hex::encode(tree.root().unwrap());
858 let mut proof = tree.inclusion_proof(1).unwrap();
859 proof.merkle_version = MERKLE_VERSION_V2;
860 assert!(
861 !MerkleTree::verify_proof(MERKLE_VERSION_V1, &root, "art_b", &proof),
862 "v1 verifier must reject a proof that upgrades itself to v2",
863 );
864 }
865
866 #[test]
867 fn v2_rejects_internal_node_as_leaf() {
868 let left = [0x11u8; 32];
874 let right = [0x22u8; 32];
875 let internal = hash_internal_v2(&left, &right);
876 let internal_hex = hex::encode(internal);
877
878 let fake_proof = InclusionProof {
879 leaf_index: 0,
880 leaf_hash: internal_hex.clone(),
881 path: vec![],
882 algorithm: Some(MERKLE_ALGORITHM_V2.to_string()),
883 merkle_version: MERKLE_VERSION_V2,
884 };
885
886 assert!(
887 !MerkleTree::verify_proof(MERKLE_VERSION_V2, &internal_hex, "art_attacker", &fake_proof),
888 "v2 verifier must reject an internal-node hash impersonating a single-leaf tree",
889 );
890 }
891
892 #[test]
893 fn v2_rejects_empty_path_with_nonzero_leaf_index() {
894 let mut tree = MerkleTree::new();
897 tree.append("art_a");
898 let root = hex::encode(tree.root().unwrap());
899
900 let bad = InclusionProof {
902 leaf_index: 1,
903 leaf_hash: hex::encode(hash_leaf_v2("art_a")),
904 path: vec![],
905 algorithm: Some(MERKLE_ALGORITHM_V2.to_string()),
906 merkle_version: MERKLE_VERSION_V2,
907 };
908 assert!(!MerkleTree::verify_proof(MERKLE_VERSION_V2, &root, "art_a", &bad));
909 }
910
911 #[test]
912 fn unknown_merkle_version_rejected() {
913 let mut tree = MerkleTree::new();
914 tree.append("art_a");
915 tree.append("art_b");
916 let root = hex::encode(tree.root().unwrap());
917 let mut proof = tree.inclusion_proof(0).unwrap();
918 proof.merkle_version = 7; assert!(!MerkleTree::verify_proof(MERKLE_VERSION_V2, &root, "art_a", &proof));
921 let mut proof2 = tree.inclusion_proof(0).unwrap();
923 proof2.merkle_version = 7;
924 assert!(!MerkleTree::verify_proof(7, &root, "art_a", &proof2));
925 }
926
927 #[test]
928 fn default_merkle_version_function_returns_one() {
929 assert_eq!(default_merkle_version_v1(), MERKLE_VERSION_V1);
932 }
933
934 #[test]
935 fn missing_merkle_version_field_defaults_to_v1() {
936 let json = serde_json::json!({
939 "leaf_index": 0,
940 "leaf_hash": "00".repeat(32),
941 "path": [],
942 "algorithm": "sha256-duplicate-last",
943 });
944 let proof: InclusionProof = serde_json::from_value(json).unwrap();
945 assert_eq!(proof.merkle_version, MERKLE_VERSION_V1);
946 }
947
948 #[test]
949 fn mixed_version_in_receipt_explicit() {
950 let mut tree = MerkleTree::new();
954 for id in &["art_a", "art_b"] {
955 tree.append(id);
956 }
957 let root = hex::encode(tree.root().unwrap());
958 let proof = tree.inclusion_proof(0).unwrap();
959 let json = serde_json::to_string(&proof).unwrap();
960 assert!(json.contains("\"merkle_version\":2"), "wire shape must include merkle_version=2");
961 let parsed: InclusionProof = serde_json::from_str(&json).unwrap();
962 assert_eq!(parsed.merkle_version, MERKLE_VERSION_V2);
963 assert!(MerkleTree::verify_proof(MERKLE_VERSION_V2, &root, "art_a", &parsed));
964 }
965
966 #[test]
975 fn cross_version_downgrade_rejected() {
976 let mut tree = MerkleTree::new();
985 for id in &["art_a", "art_b"] {
986 tree.append(id);
987 }
988 let root = hex::encode(tree.root().unwrap());
989
990 let downgrade = InclusionProof {
992 leaf_index: 0,
993 leaf_hash: root.clone(),
994 path: vec![],
995 algorithm: Some(MERKLE_ALGORITHM_V1.to_string()),
996 merkle_version: MERKLE_VERSION_V1,
997 };
998
999 assert!(
1003 !MerkleTree::verify_proof(MERKLE_VERSION_V2, &root, "art_attacker", &downgrade),
1004 "v1 proof must NOT verify when the trusted version is v2 (downgrade vector)",
1005 );
1006 }
1007
1008 #[test]
1009 fn unknown_merkle_version_rejected_at_construct() {
1010 assert!(matches!(
1014 MerkleTree::with_version(99),
1015 Err(MerkleError::UnknownVersion(99)),
1016 ));
1017 assert!(matches!(
1018 MerkleTree::with_version(0),
1019 Err(MerkleError::UnknownVersion(0)),
1020 ));
1021 assert!(MerkleTree::with_version(MERKLE_VERSION_V1).is_ok());
1022 assert!(MerkleTree::with_version(MERKLE_VERSION_V2).is_ok());
1023 }
1024
1025 #[test]
1026 fn unknown_merkle_version_rejected_at_primitive() {
1027 assert!(matches!(
1031 hash_leaf(42, "x"),
1032 Err(MerkleError::UnknownVersion(42)),
1033 ));
1034 let l = [0u8; 32];
1035 let r = [0u8; 32];
1036 assert!(matches!(
1037 hash_internal(42, &l, &r),
1038 Err(MerkleError::UnknownVersion(42)),
1039 ));
1040 }
1041
1042 #[test]
1043 fn checkpoint_canonical_includes_version() {
1044 use crate::merkle::checkpoint::{
1052 Checkpoint, CANONICAL_VERSION_V1, CANONICAL_VERSION_V2, CANONICAL_VERSION_V3,
1053 };
1054 let canonical_v1 = Checkpoint::canonical_for_signing(
1055 CANONICAL_VERSION_V1,
1056 MERKLE_VERSION_V1,
1057 None,
1058 None,
1059 7,
1060 "sha256:abcd",
1061 42,
1062 6,
1063 "key_test",
1064 "2026-05-17T00:00:00Z",
1065 );
1066 let canonical_v2 = Checkpoint::canonical_for_signing(
1067 CANONICAL_VERSION_V2,
1068 MERKLE_VERSION_V2,
1069 None,
1070 None,
1071 7,
1072 "sha256:abcd",
1073 42,
1074 6,
1075 "key_test",
1076 "2026-05-17T00:00:00Z",
1077 );
1078 let canonical_v3 = Checkpoint::canonical_for_signing(
1079 CANONICAL_VERSION_V3,
1080 MERKLE_VERSION_V2,
1081 Some(MERKLE_ALGORITHM_V2),
1082 None,
1083 7,
1084 "sha256:abcd",
1085 42,
1086 6,
1087 "key_test",
1088 "2026-05-17T00:00:00Z",
1089 );
1090
1091 assert_ne!(
1094 canonical_v1, canonical_v2,
1095 "canonical strings for v1 vs v2 must differ — merkle_version must be bound into signing",
1096 );
1097 assert_ne!(
1098 canonical_v2, canonical_v3,
1099 "canonical strings for v2 vs v3 must differ — canonical_version must be bound into signing",
1100 );
1101 assert!(
1104 canonical_v2.starts_with("v2|"),
1105 "v2 canonical must be prefixed with v2| tag, got: {canonical_v2}",
1106 );
1107 assert!(
1108 canonical_v3.starts_with("v3|"),
1109 "v3 canonical must be prefixed with v3| tag, got: {canonical_v3}",
1110 );
1111 assert_eq!(
1114 canonical_v1,
1115 "7|sha256:abcd|42|6|key_test|2026-05-17T00:00:00Z",
1116 "v1 canonical must remain byte-identical to legacy",
1117 );
1118 }
1119}