Skip to main content

merkle_core/traits/
hash_function.rs

1//! # merkle-core :: traits :: `hash_function`
2//!
3//! The `HashFunction` trait is the **pluggable crypto abstraction** at the
4//! heart of the library.  By parameterising every tree type over
5//! `H: HashFunction`, callers can swap SHA-256 for BLAKE3 with zero changes
6//! to tree logic and zero runtime cost (Rust monomorphises the generics).
7//!
8//! ## Implementing `HashFunction`
9//!
10//! ```rust,ignore
11//! use merkle_core::traits::HashFunction;
12//!
13//! pub struct MyHash;
14//!
15//! impl HashFunction for MyHash {
16//!     type Digest = [u8; 32];
17//!
18//!     fn hash(data: &[u8]) -> Self::Digest { /* ... */ }
19//!
20//!     fn hash_nodes(left: &Self::Digest, right: &Self::Digest) -> Self::Digest {
21//!         let mut combined = Vec::with_capacity(64);
22//!         combined.extend_from_slice(left);
23//!         combined.extend_from_slice(right);
24//!         Self::hash(&combined)
25//!     }
26//!
27//!     fn algorithm_name() -> &'static str { "MyHash" }
28//! }
29//! ```
30
31use std::fmt::Debug;
32
33/// Abstraction over a cryptographic hash function used by Merkle trees.
34///
35/// # Type parameter `Digest`
36/// The associated type `Digest` represents a fixed-size hash output.  It
37/// must be:
38/// - `Clone` — nodes are cloned when building the tree.
39/// - `PartialEq + Eq` — so roots and proof hashes can be compared.
40/// - `AsRef<[u8]>` — for serialisation and concatenation before hashing.
41/// - `Debug` — for test output and diagnostic messages.
42/// - `Send + Sync` — so trees can be used across thread boundaries.
43///
44/// # Zero-cost abstractions
45/// All methods are `#[inline]` by convention in implementors.  Rust's
46/// monomorphisation ensures that no vtable dispatch overhead occurs when
47/// calling through `H: HashFunction` bounds.
48pub trait HashFunction: Send + Sync + 'static {
49    /// The concrete digest type produced by this hash function.
50    type Digest: AsRef<[u8]> + Clone + Debug + PartialEq + Eq + Send + Sync + 'static;
51
52    // ── Core hashing ──────────────────────────────────────────────────────
53
54    /// Hash arbitrary bytes and return the digest.
55    #[must_use]
56    fn hash(data: &[u8]) -> Self::Digest;
57
58    /// Hash two child digests together to produce a parent digest.
59    ///
60    /// The default implementation concatenates `left || right` and calls
61    /// `Self::hash`.  Override this to use domain separation or a different
62    /// concatenation strategy.
63    #[must_use]
64    fn hash_nodes(left: &Self::Digest, right: &Self::Digest) -> Self::Digest {
65        let mut combined = Vec::with_capacity(left.as_ref().len() + right.as_ref().len());
66        combined.extend_from_slice(left.as_ref());
67        combined.extend_from_slice(right.as_ref());
68        Self::hash(&combined)
69    }
70
71    /// Return the canonical "empty" digest used to pad trees to a
72    /// power-of-two size, or to represent vacant slots in sparse trees.
73    ///
74    /// The default is the hash of a zero-length byte slice.
75    #[must_use]
76    fn empty() -> Self::Digest {
77        Self::hash(&[])
78    }
79
80    // ── Metadata ──────────────────────────────────────────────────────────
81
82    /// Human-readable name of the algorithm, e.g. `"SHA-256"` or `"BLAKE3"`.
83    #[must_use]
84    fn algorithm_name() -> &'static str;
85
86    /// Output size of the digest in bytes.
87    #[must_use]
88    fn digest_size() -> usize;
89}
90
91// ── Tests ──────────────────────────────────────────────────────────────────
92
93#[cfg(test)]
94mod tests {
95    /// A minimal stub `HashFunction` used only in unit tests so that
96    /// `merkle-core` has no dependency on `merkle-hash` at test time.
97    use super::*;
98
99    struct Xor8;
100
101    impl HashFunction for Xor8 {
102        type Digest = [u8; 1];
103
104        fn hash(data: &[u8]) -> [u8; 1] {
105            [data.iter().fold(0u8, |acc, &b| acc ^ b)]
106        }
107
108        fn algorithm_name() -> &'static str {
109            "XOR8"
110        }
111
112        fn digest_size() -> usize {
113            1
114        }
115    }
116
117    #[test]
118    fn hash_deterministic() {
119        let a = Xor8::hash(b"hello");
120        let b = Xor8::hash(b"hello");
121        assert_eq!(a, b);
122    }
123
124    #[test]
125    fn hash_nodes_uses_concatenation() {
126        let l = Xor8::hash(b"left");
127        let r = Xor8::hash(b"right");
128        let parent = Xor8::hash_nodes(&l, &r);
129        // Manually: xor(l, r) because XOR8(a || b) = XOR(a, b) for single bytes
130        let expected = [l[0] ^ r[0]];
131        assert_eq!(parent, expected);
132    }
133
134    #[test]
135    fn empty_is_hash_of_empty_slice() {
136        assert_eq!(Xor8::empty(), Xor8::hash(&[]));
137    }
138
139    #[test]
140    fn metadata() {
141        assert_eq!(Xor8::algorithm_name(), "XOR8");
142        assert_eq!(Xor8::digest_size(), 1);
143    }
144}