Skip to main content

exo_core/
hash.rs

1// Copyright 2026 Exochain Foundation
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at:
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//
15// SPDX-License-Identifier: Apache-2.0
16
17//! Canonical hashing utilities.
18//!
19//! All hashing in EXOCHAIN uses **blake3** and all structured data is
20//! first serialized to **CBOR** (deterministic canonical encoding) before
21//! hashing.  This guarantees that identical logical values always produce
22//! the same hash regardless of serialization order or platform.
23
24use serde::Serialize;
25
26use crate::{
27    error::{ExoError, Result},
28    types::Hash256,
29};
30
31const MERKLE_LEAF_DOMAIN: u8 = 0x00;
32const MERKLE_PARENT_DOMAIN: u8 = 0x01;
33const MERKLE_COUNTED_ROOT_DOMAIN: u8 = 0x02;
34const MERKLE_LEAF_COUNT_BYTES: usize = 16;
35
36/// Compute the blake3 hash of raw bytes.
37#[must_use]
38pub fn canonical_hash(data: &[u8]) -> Hash256 {
39    Hash256::digest(data)
40}
41
42/// Serialize `value` to CBOR, then compute the blake3 hash.
43///
44/// # Errors
45///
46/// Returns `ExoError::SerializationError` if CBOR encoding fails.
47pub fn hash_structured<T: Serialize>(value: &T) -> Result<Hash256> {
48    let mut buf = Vec::new();
49    ciborium::into_writer(value, &mut buf)?;
50    Ok(canonical_hash(&buf))
51}
52
53/// Compute a deterministic Merkle root from a slice of leaf hashes.
54///
55/// - Empty input returns `Hash256::ZERO`.
56/// - Single leaf returns `H(0x00 || leaf)`.
57/// - Otherwise, leaves are paired left-to-right; an odd leaf is promoted
58///   (duplicated) to fill the pair.  This process repeats until one root
59///   remains.
60#[must_use]
61pub fn merkle_root(leaves: &[Hash256]) -> Hash256 {
62    if leaves.is_empty() {
63        return Hash256::ZERO;
64    }
65
66    let mut current: Vec<Hash256> = leaves.iter().map(hash_leaf).collect();
67    while current.len() > 1 {
68        let mut next = Vec::with_capacity(current.len().div_ceil(2));
69        let mut i = 0;
70        while i < current.len() {
71            let left = &current[i];
72            let right = if i + 1 < current.len() {
73                &current[i + 1]
74            } else {
75                // Odd leaf — duplicate
76                left
77            };
78            next.push(hash_pair(left, right));
79            i += 2;
80        }
81        current = next;
82    }
83    current[0]
84}
85
86/// Compute a Merkle root commitment that binds both the tree root and the
87/// number of leaves represented by that tree.
88#[must_use]
89pub fn merkle_root_with_leaf_count(leaves: &[Hash256]) -> Hash256 {
90    bind_merkle_root_to_leaf_count(&merkle_root(leaves), leaves.len())
91}
92
93/// Bind an existing Merkle tree root to a fixed-width leaf-count commitment.
94#[must_use]
95pub fn bind_merkle_root_to_leaf_count(root: &Hash256, leaf_count: usize) -> Hash256 {
96    let mut combined = [0u8; 1 + MERKLE_LEAF_COUNT_BYTES + 32];
97    let leaf_count_bytes = leaf_count.to_be_bytes();
98    let count_end = 1 + MERKLE_LEAF_COUNT_BYTES;
99    let count_start = count_end - leaf_count_bytes.len();
100
101    combined[0] = MERKLE_COUNTED_ROOT_DOMAIN;
102    combined[count_start..count_end].copy_from_slice(&leaf_count_bytes);
103    combined[count_end..].copy_from_slice(root.as_bytes());
104    canonical_hash(&combined)
105}
106
107/// Generate a Merkle proof for the leaf at `index`.
108///
109/// Returns the sibling node hashes needed to reconstruct the root. Leaf
110/// siblings are returned in their domain-separated `H(0x00 || leaf)` form.
111///
112/// # Errors
113///
114/// Returns `ExoError::InvalidMerkleProof` if `index` is out of bounds.
115pub fn merkle_proof(leaves: &[Hash256], index: usize) -> Result<Vec<Hash256>> {
116    if index >= leaves.len() || leaves.is_empty() {
117        return Err(ExoError::InvalidMerkleProof);
118    }
119    if leaves.len() == 1 {
120        return Ok(Vec::new());
121    }
122
123    let mut proof = Vec::new();
124    let mut current: Vec<Hash256> = leaves.iter().map(hash_leaf).collect();
125    let mut idx = index;
126
127    while current.len() > 1 {
128        // If odd number, duplicate the last element
129        if current.len() % 2 != 0 {
130            let last = current[current.len() - 1];
131            current.push(last);
132        }
133        let sibling_idx = if idx % 2 == 0 { idx + 1 } else { idx - 1 };
134        proof.push(current[sibling_idx]);
135
136        // Build next level
137        let mut next = Vec::with_capacity(current.len() / 2);
138        let mut i = 0;
139        while i < current.len() {
140            next.push(hash_pair(&current[i], &current[i + 1]));
141            i += 2;
142        }
143        current = next;
144        idx /= 2;
145    }
146
147    Ok(proof)
148}
149
150/// Verify a Merkle proof.
151///
152/// Given the expected `root`, a raw `leaf` hash as supplied to
153/// [`merkle_root`], the `proof` (domain-separated sibling node hashes),
154/// and the `index` of the leaf in the original tree, returns `true` if
155/// the proof is valid.
156#[must_use]
157pub fn verify_merkle_proof(
158    root: &Hash256,
159    leaf: &Hash256,
160    proof: &[Hash256],
161    index: usize,
162) -> bool {
163    let current = merkle_root_from_proof(leaf, proof, index);
164    hash256_eq_constant_time(&current, root)
165}
166
167/// Reconstruct the Merkle root implied by a leaf, proof path, and leaf index.
168#[must_use]
169pub fn merkle_root_from_proof(leaf: &Hash256, proof: &[Hash256], index: usize) -> Hash256 {
170    let mut current = hash_leaf(leaf);
171    let mut idx = index;
172
173    for sibling in proof {
174        if idx % 2 == 0 {
175            current = hash_pair(&current, sibling);
176        } else {
177            current = hash_pair(sibling, &current);
178        }
179        idx /= 2;
180    }
181
182    current
183}
184
185/// Verify a Merkle proof against a root commitment that binds the claimed leaf
186/// count.
187///
188/// The `root` parameter must be produced by [`merkle_root_with_leaf_count`] or
189/// [`bind_merkle_root_to_leaf_count`]. The compact proof path alone cannot
190/// prove the size of opaque sibling subtrees, so count binding is enforced at
191/// the root-commitment layer.
192#[must_use]
193pub fn verify_merkle_proof_with_leaf_count(
194    root: &Hash256,
195    leaf: &Hash256,
196    proof: &[Hash256],
197    index: usize,
198    leaf_count: usize,
199) -> bool {
200    let Ok((current, duplicate_siblings_match)) =
201        fold_merkle_proof_with_leaf_count(leaf, proof, index, leaf_count)
202    else {
203        return false;
204    };
205    let count_bound_root = bind_merkle_root_to_leaf_count(&current, leaf_count);
206    duplicate_siblings_match && hash256_eq_constant_time(&count_bound_root, root)
207}
208
209/// Reconstruct the leaf-count-bound Merkle root implied by a leaf, proof path,
210/// leaf index, and claimed leaf count.
211///
212/// # Errors
213///
214/// Returns `ExoError::InvalidMerkleProof` when `leaf_count` is zero, when
215/// `index` is outside the claimed tree, or when the proof path length is not
216/// the path length required by `leaf_count`, or when an odd duplicated sibling
217/// does not match the duplicated current node.
218pub fn merkle_root_from_proof_with_leaf_count(
219    leaf: &Hash256,
220    proof: &[Hash256],
221    index: usize,
222    leaf_count: usize,
223) -> Result<Hash256> {
224    let (root, duplicate_siblings_match) =
225        fold_merkle_proof_with_leaf_count(leaf, proof, index, leaf_count)?;
226    if duplicate_siblings_match {
227        Ok(bind_merkle_root_to_leaf_count(&root, leaf_count))
228    } else {
229        Err(ExoError::InvalidMerkleProof)
230    }
231}
232
233fn fold_merkle_proof_with_leaf_count(
234    leaf: &Hash256,
235    proof: &[Hash256],
236    index: usize,
237    leaf_count: usize,
238) -> Result<(Hash256, bool)> {
239    if leaf_count == 0 || index >= leaf_count {
240        return Err(ExoError::InvalidMerkleProof);
241    }
242
243    let mut current = hash_leaf(leaf);
244    let mut idx = index;
245    let mut level_count = leaf_count;
246    let mut duplicate_siblings_match = true;
247
248    for sibling in proof {
249        if level_count == 1 || idx >= level_count {
250            return Err(ExoError::InvalidMerkleProof);
251        }
252
253        let is_duplicated_odd_leaf = level_count % 2 != 0 && idx == level_count - 1;
254        if is_duplicated_odd_leaf {
255            duplicate_siblings_match &= hash256_eq_constant_time(&current, sibling);
256            current = hash_pair(&current, &current);
257        } else if idx % 2 == 0 {
258            current = hash_pair(&current, sibling);
259        } else {
260            current = hash_pair(sibling, &current);
261        }
262
263        idx /= 2;
264        level_count = level_count.div_ceil(2);
265    }
266
267    if level_count == 1 {
268        Ok((current, duplicate_siblings_match))
269    } else {
270        Err(ExoError::InvalidMerkleProof)
271    }
272}
273
274/// Compare two `Hash256` values without data-dependent early exit.
275#[must_use]
276pub fn hash256_eq_constant_time(left: &Hash256, right: &Hash256) -> bool {
277    let mut diff = 0u8;
278    for (left_byte, right_byte) in left.as_bytes().iter().zip(right.as_bytes().iter()) {
279        diff |= left_byte ^ right_byte;
280    }
281    diff == 0
282}
283
284/// Hash a leaf into the Merkle leaf domain: `H(0x00 || leaf)`.
285#[must_use]
286fn hash_leaf(leaf: &Hash256) -> Hash256 {
287    let mut combined = [0u8; 33];
288    combined[0] = MERKLE_LEAF_DOMAIN;
289    combined[1..].copy_from_slice(leaf.as_bytes());
290    canonical_hash(&combined)
291}
292
293/// Hash two nodes together in the Merkle parent domain: `H(0x01 || left || right)`.
294#[must_use]
295fn hash_pair(left: &Hash256, right: &Hash256) -> Hash256 {
296    let mut combined = [0u8; 65];
297    combined[0] = MERKLE_PARENT_DOMAIN;
298    combined[1..33].copy_from_slice(left.as_bytes());
299    combined[33..].copy_from_slice(right.as_bytes());
300    canonical_hash(&combined)
301}
302
303// ===========================================================================
304// Tests
305// ===========================================================================
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310
311    #[test]
312    fn canonical_hash_deterministic() {
313        let h1 = canonical_hash(b"hello world");
314        let h2 = canonical_hash(b"hello world");
315        assert_eq!(h1, h2);
316    }
317
318    #[test]
319    fn canonical_hash_different_inputs() {
320        let h1 = canonical_hash(b"aaa");
321        let h2 = canonical_hash(b"bbb");
322        assert_ne!(h1, h2);
323    }
324
325    #[test]
326    fn hash_structured_deterministic() {
327        #[derive(Serialize)]
328        struct Foo {
329            a: u32,
330            b: String,
331        }
332        let v = Foo {
333            a: 42,
334            b: "hello".into(),
335        };
336        let h1 = hash_structured(&v).expect("ok");
337        let h2 = hash_structured(&v).expect("ok");
338        assert_eq!(h1, h2);
339    }
340
341    #[test]
342    fn hash_structured_different_values() {
343        let h1 = hash_structured(&42u32).expect("ok");
344        let h2 = hash_structured(&43u32).expect("ok");
345        assert_ne!(h1, h2);
346    }
347
348    #[test]
349    fn merkle_root_empty() {
350        assert_eq!(merkle_root(&[]), Hash256::ZERO);
351    }
352
353    #[test]
354    fn merkle_root_single() {
355        let leaf = Hash256::digest(b"only");
356        assert_eq!(merkle_root(&[leaf]), hash_leaf(&leaf));
357    }
358
359    fn raw_concat_pair_hash_for_test(left: &Hash256, right: &Hash256) -> Hash256 {
360        let mut combined = [0u8; 64];
361        combined[..32].copy_from_slice(left.as_bytes());
362        combined[32..].copy_from_slice(right.as_bytes());
363        canonical_hash(&combined)
364    }
365
366    #[test]
367    fn merkle_root_uses_distinct_leaf_and_parent_domains() {
368        let leaf = Hash256::digest(b"domain-separated-leaf");
369        assert_ne!(
370            merkle_root(&[leaf]),
371            leaf,
372            "single-leaf Merkle roots must not be interchangeable with raw leaf hashes"
373        );
374
375        let right = Hash256::digest(b"domain-separated-right");
376        let raw_parent_hash = raw_concat_pair_hash_for_test(&leaf, &right);
377        assert_ne!(
378            merkle_root(&[leaf, right]),
379            raw_parent_hash,
380            "interior Merkle nodes must not use the raw H(left || right) domain"
381        );
382    }
383
384    #[test]
385    fn merkle_root_two_leaves() {
386        let a = Hash256::digest(b"a");
387        let b = Hash256::digest(b"b");
388        let root = merkle_root(&[a, b]);
389        // Should be hash_pair(hash_leaf(a), hash_leaf(b)).
390        let expected = hash_pair(&hash_leaf(&a), &hash_leaf(&b));
391        assert_eq!(root, expected);
392    }
393
394    #[test]
395    fn merkle_root_three_leaves_odd() {
396        let a = Hash256::digest(b"a");
397        let b = Hash256::digest(b"b");
398        let c = Hash256::digest(b"c");
399        let root = merkle_root(&[a, b, c]);
400        // Level 1: hash_pair(hash_leaf(a), hash_leaf(b)), hash_pair(hash_leaf(c), hash_leaf(c))
401        // Level 0: hash_pair(level_1_left, level_1_right)
402        let a_leaf = hash_leaf(&a);
403        let b_leaf = hash_leaf(&b);
404        let c_leaf = hash_leaf(&c);
405        let ab = hash_pair(&a_leaf, &b_leaf);
406        let cc = hash_pair(&c_leaf, &c_leaf);
407        let expected = hash_pair(&ab, &cc);
408        assert_eq!(root, expected);
409    }
410
411    #[test]
412    fn merkle_root_four_leaves() {
413        let leaves: Vec<Hash256> = (0..4u8).map(|i| Hash256::digest(&[i])).collect();
414        let root = merkle_root(&leaves);
415        let leaf_nodes: Vec<Hash256> = leaves.iter().map(hash_leaf).collect();
416        let ab = hash_pair(&leaf_nodes[0], &leaf_nodes[1]);
417        let cd = hash_pair(&leaf_nodes[2], &leaf_nodes[3]);
418        let expected = hash_pair(&ab, &cd);
419        assert_eq!(root, expected);
420    }
421
422    #[test]
423    fn merkle_root_with_leaf_count_binds_count_and_inner_root() {
424        let leaves = [
425            Hash256::digest(b"count-bound-a"),
426            Hash256::digest(b"count-bound-b"),
427            Hash256::digest(b"count-bound-c"),
428            Hash256::digest(b"count-bound-d"),
429        ];
430        let inner_root = merkle_root(&leaves);
431        let bound_root = merkle_root_with_leaf_count(&leaves);
432
433        assert_eq!(
434            bound_root,
435            bind_merkle_root_to_leaf_count(&inner_root, leaves.len())
436        );
437        assert_ne!(
438            bound_root,
439            bind_merkle_root_to_leaf_count(&inner_root, 3),
440            "same inner root must not verify under a different leaf count"
441        );
442        assert_ne!(
443            bound_root, inner_root,
444            "count-bound root must not be interchangeable with the raw Merkle root"
445        );
446    }
447
448    #[test]
449    fn merkle_root_deterministic() {
450        let leaves: Vec<Hash256> = (0..7u8).map(|i| Hash256::digest(&[i])).collect();
451        let r1 = merkle_root(&leaves);
452        let r2 = merkle_root(&leaves);
453        assert_eq!(r1, r2);
454    }
455
456    #[test]
457    fn merkle_proof_empty() {
458        let result = merkle_proof(&[], 0);
459        assert!(result.is_err());
460    }
461
462    #[test]
463    fn merkle_proof_out_of_bounds() {
464        let leaf = Hash256::digest(b"x");
465        let result = merkle_proof(&[leaf], 1);
466        assert!(result.is_err());
467    }
468
469    #[test]
470    fn merkle_proof_single_leaf() {
471        let leaf = Hash256::digest(b"only");
472        let proof = merkle_proof(&[leaf], 0).expect("ok");
473        assert!(proof.is_empty());
474        let root = merkle_root(&[leaf]);
475        assert!(verify_merkle_proof(&root, &leaf, &proof, 0));
476    }
477
478    #[test]
479    fn merkle_proof_two_leaves() {
480        let leaves = vec![Hash256::digest(b"a"), Hash256::digest(b"b")];
481        let root = merkle_root(&leaves);
482
483        for i in 0..2 {
484            let proof = merkle_proof(&leaves, i).expect("ok");
485            assert!(verify_merkle_proof(&root, &leaves[i], &proof, i));
486        }
487    }
488
489    #[test]
490    fn merkle_proof_four_leaves() {
491        let leaves: Vec<Hash256> = (0..4u8).map(|i| Hash256::digest(&[i])).collect();
492        let root = merkle_root(&leaves);
493
494        for i in 0..4 {
495            let proof = merkle_proof(&leaves, i).expect("ok");
496            assert!(
497                verify_merkle_proof(&root, &leaves[i], &proof, i),
498                "proof failed for leaf {i}"
499            );
500        }
501    }
502
503    #[test]
504    fn merkle_proof_odd_leaves() {
505        let leaves: Vec<Hash256> = (0..5u8).map(|i| Hash256::digest(&[i])).collect();
506        let root = merkle_root(&leaves);
507
508        for i in 0..5 {
509            let proof = merkle_proof(&leaves, i).expect("ok");
510            assert!(
511                verify_merkle_proof(&root, &leaves[i], &proof, i),
512                "proof failed for leaf {i}"
513            );
514        }
515    }
516
517    #[test]
518    fn merkle_proof_seven_leaves() {
519        let leaves: Vec<Hash256> = (0..7u8).map(|i| Hash256::digest(&[i])).collect();
520        let root = merkle_root(&leaves);
521
522        for i in 0..7 {
523            let proof = merkle_proof(&leaves, i).expect("ok");
524            assert!(
525                verify_merkle_proof(&root, &leaves[i], &proof, i),
526                "proof failed for leaf {i}"
527            );
528        }
529    }
530
531    #[test]
532    fn verify_merkle_proof_rejects_wrong_leaf() {
533        let leaves: Vec<Hash256> = (0..4u8).map(|i| Hash256::digest(&[i])).collect();
534        let root = merkle_root(&leaves);
535        let proof = merkle_proof(&leaves, 0).expect("ok");
536        let wrong_leaf = Hash256::digest(b"wrong");
537        assert!(!verify_merkle_proof(&root, &wrong_leaf, &proof, 0));
538    }
539
540    #[test]
541    fn verify_merkle_proof_rejects_wrong_index() {
542        let leaves: Vec<Hash256> = (0..4u8).map(|i| Hash256::digest(&[i])).collect();
543        let root = merkle_root(&leaves);
544        let proof = merkle_proof(&leaves, 0).expect("ok");
545        // Use correct leaf but wrong index
546        assert!(!verify_merkle_proof(&root, &leaves[0], &proof, 1));
547    }
548
549    #[test]
550    fn verify_merkle_proof_rejects_wrong_root() {
551        let leaves: Vec<Hash256> = (0..4u8).map(|i| Hash256::digest(&[i])).collect();
552        let proof = merkle_proof(&leaves, 0).expect("ok");
553        let wrong_root = Hash256::digest(b"wrong root");
554        assert!(!verify_merkle_proof(&wrong_root, &leaves[0], &proof, 0));
555    }
556
557    #[test]
558    fn merkle_root_from_proof_matches_canonical_root() {
559        let leaves = vec![
560            Hash256::digest(b"root-proof-a"),
561            Hash256::digest(b"root-proof-b"),
562            Hash256::digest(b"root-proof-c"),
563            Hash256::digest(b"root-proof-d"),
564            Hash256::digest(b"root-proof-e"),
565        ];
566        let root = merkle_root(&leaves);
567
568        for (index, leaf) in leaves.iter().enumerate() {
569            let proof = merkle_proof(&leaves, index).expect("proof");
570            assert_eq!(merkle_root_from_proof(leaf, &proof, index), root);
571        }
572    }
573
574    #[test]
575    fn merkle_root_from_proof_with_leaf_count_matches_canonical_root() {
576        let leaves = vec![
577            Hash256::digest(b"count-proof-a"),
578            Hash256::digest(b"count-proof-b"),
579            Hash256::digest(b"count-proof-c"),
580            Hash256::digest(b"count-proof-d"),
581            Hash256::digest(b"count-proof-e"),
582        ];
583        let root = merkle_root_with_leaf_count(&leaves);
584
585        for (index, leaf) in leaves.iter().enumerate() {
586            let proof = merkle_proof(&leaves, index).expect("proof");
587            let computed =
588                merkle_root_from_proof_with_leaf_count(leaf, &proof, index, leaves.len())
589                    .expect("leaf-count-bound proof root");
590            assert_eq!(computed, root);
591            assert!(verify_merkle_proof_with_leaf_count(
592                &root,
593                leaf,
594                &proof,
595                index,
596                leaves.len()
597            ));
598        }
599    }
600
601    #[test]
602    fn verify_merkle_proof_with_leaf_count_rejects_false_leaf_count() {
603        let leaves = [
604            Hash256::digest(b"false-count-a"),
605            Hash256::digest(b"false-count-b"),
606            Hash256::digest(b"false-count-c"),
607            Hash256::digest(b"false-count-d"),
608        ];
609        let root = merkle_root_with_leaf_count(&leaves);
610        let target_index = 2;
611        let proof = merkle_proof(&leaves, target_index).expect("proof");
612
613        assert!(!verify_merkle_proof_with_leaf_count(
614            &root,
615            &leaves[target_index],
616            &proof,
617            target_index,
618            3
619        ));
620    }
621
622    #[test]
623    fn verify_merkle_proof_with_leaf_count_rejects_false_prefix_leaf_count() {
624        let leaves = [
625            Hash256::digest(b"false-prefix-count-a"),
626            Hash256::digest(b"false-prefix-count-b"),
627            Hash256::digest(b"false-prefix-count-c"),
628            Hash256::digest(b"false-prefix-count-d"),
629        ];
630        let root = merkle_root_with_leaf_count(&leaves);
631        let target_index = 1;
632        let proof = merkle_proof(&leaves, target_index).expect("proof");
633
634        assert!(!verify_merkle_proof_with_leaf_count(
635            &root,
636            &leaves[target_index],
637            &proof,
638            target_index,
639            3
640        ));
641    }
642
643    #[test]
644    fn verify_merkle_proof_with_leaf_count_rejects_bad_odd_duplicate_sibling() {
645        let leaves = [
646            Hash256::digest(b"odd-count-a"),
647            Hash256::digest(b"odd-count-b"),
648            Hash256::digest(b"odd-count-c"),
649        ];
650        let root = merkle_root_with_leaf_count(&leaves);
651        let target_index = 2;
652        let mut proof = merkle_proof(&leaves, target_index).expect("proof");
653        proof[0] = Hash256::digest(b"attacker-supplied-non-duplicate");
654
655        assert!(!verify_merkle_proof_with_leaf_count(
656            &root,
657            &leaves[target_index],
658            &proof,
659            target_index,
660            leaves.len()
661        ));
662    }
663
664    #[test]
665    fn verify_merkle_proof_uses_constant_time_root_comparison() {
666        let source = include_str!("hash.rs");
667        let Some(after_verify_fn) = source.split("pub fn verify_merkle_proof").nth(1) else {
668            panic!("verify_merkle_proof source exists");
669        };
670        let Some(verify_body) = after_verify_fn.split("/// Hash two nodes together").next() else {
671            panic!("hash_pair marker follows verify_merkle_proof");
672        };
673
674        assert!(
675            verify_body.contains("hash256_eq_constant_time(&current, root)"),
676            "verify_merkle_proof must compare the reconstructed root in constant time"
677        );
678        assert!(
679            !verify_body.contains("current == *root"),
680            "verify_merkle_proof must not use direct Hash256 equality for the root check"
681        );
682    }
683
684    #[test]
685    fn hash256_eq_constant_time_matches_hash_equality() {
686        let first = Hash256::digest(b"same");
687        let same = Hash256::digest(b"same");
688        let different_first_byte = Hash256::from_bytes({
689            let mut bytes = *first.as_bytes();
690            bytes[0] ^= 0x80;
691            bytes
692        });
693        let different_last_byte = Hash256::from_bytes({
694            let mut bytes = *first.as_bytes();
695            bytes[31] ^= 0x01;
696            bytes
697        });
698
699        assert!(hash256_eq_constant_time(&first, &same));
700        assert!(!hash256_eq_constant_time(&first, &different_first_byte));
701        assert!(!hash256_eq_constant_time(&first, &different_last_byte));
702    }
703
704    #[test]
705    fn hash_pair_deterministic() {
706        let a = Hash256::digest(b"left");
707        let b = Hash256::digest(b"right");
708        let h1 = hash_pair(&a, &b);
709        let h2 = hash_pair(&a, &b);
710        assert_eq!(h1, h2);
711    }
712
713    #[test]
714    fn hash_pair_not_commutative() {
715        let a = Hash256::digest(b"left");
716        let b = Hash256::digest(b"right");
717        assert_ne!(hash_pair(&a, &b), hash_pair(&b, &a));
718    }
719}