Skip to main content

ssz/
merkle.rs

1//! SSZ merkleization primitives.
2//!
3//! All hashes are 32-byte digests, threaded through the [`digest::Digest`]
4//! trait with `OutputSize = U32`. No domain bytes or prefixes are mixed in
5//! at the node level — concatenation is the only operation.
6
7use alloc::vec::Vec;
8use digest::Digest;
9use digest::typenum::U32;
10
11use crate::BYTES_PER_CHUNK;
12
13/// Hash two 32-byte children into their parent node.
14#[inline]
15pub fn hash_pair<D: Digest<OutputSize = U32>>(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] {
16    let mut hasher = D::new();
17    hasher.update(left);
18    hasher.update(right);
19    let out = hasher.finalize();
20    let mut arr = [0u8; 32];
21    arr.copy_from_slice(out.as_slice());
22    arr
23}
24
25/// Pack a byte slice into 32-byte chunks, zero-padding the tail.
26pub fn pack_bytes(bytes: &[u8]) -> Vec<[u8; 32]> {
27    if bytes.is_empty() {
28        return Vec::new();
29    }
30    let n = bytes.len().div_ceil(BYTES_PER_CHUNK);
31    let mut out = Vec::with_capacity(n);
32    let mut cursor = 0;
33    for _ in 0..n {
34        let mut chunk = [0u8; 32];
35        let take = core::cmp::min(BYTES_PER_CHUNK, bytes.len() - cursor);
36        chunk[..take].copy_from_slice(&bytes[cursor..cursor + take]);
37        out.push(chunk);
38        cursor += take;
39    }
40    out
41}
42
43/// Number of leaves in a balanced tree of `depth` levels.
44#[inline]
45pub fn leaves_at_depth(depth: usize) -> u64 {
46    1u64 << depth
47}
48
49/// Returns `ceil(log2(max(1, n)))`.
50#[inline]
51pub fn ceil_log2(n: u64) -> usize {
52    if n <= 1 {
53        return 0;
54    }
55    (64 - (n - 1).leading_zeros()) as usize
56}
57
58/// SSZ `merkleize` — pad `chunks` to `max(limit, chunks.len())` rounded up
59/// to the next power of two, build a balanced binary tree using
60/// `hash(left || right)`, and return the root.
61///
62/// `limit` is the type-level chunk cap (e.g. `ceil(N * size_of_T / 32)` for
63/// `List<T, N>`). When `chunks.len() > limit`, the limit is bumped up to
64/// `chunks.len()` (callers should validate the type-level cap separately).
65///
66/// Empty input with `limit == 0` returns a zero hash (the root of a single
67/// zero chunk).
68///
69/// Complexity: `O(chunks.len() + depth)` hash operations, independent of
70/// `limit`. This is achieved by only materialising the "real" prefix at
71/// each level; the implicit zero-padded suffix folds into `zero_hash(d)`
72/// without iteration.
73pub fn merkleize<D: Digest<OutputSize = U32>>(chunks: &[[u8; 32]], limit: usize) -> [u8; 32] {
74    let target = core::cmp::max(limit, chunks.len()).max(1);
75    let padded_len = target.next_power_of_two();
76    let depth = padded_len.trailing_zeros() as usize;
77
78    if padded_len == 1 {
79        return chunks.first().copied().unwrap_or([0u8; 32]);
80    }
81
82    // Empty input with depth > 0 → entire tree is implicit zero padding.
83    if chunks.is_empty() {
84        return zero_hash::<D>(depth);
85    }
86
87    // Precompute the zero-hash table for this call's `depth`. Each level's
88    // zero_hash is `H(prev || prev)`; computing it once up-front turns the
89    // per-level lookup from O(d) into O(1) and the total from O(depth²)
90    // into O(depth). Without this, a depth-32 merkleize (e.g. a Vec<T> with
91    // MAX_VEC_LEN = 1 << 32) burns ~496 redundant SHA-256s per call just
92    // recomputing the same zero hashes.
93    let mut zero_h_table: Vec<[u8; 32]> = Vec::with_capacity(depth);
94    let mut cur_zero = [0u8; 32];
95    zero_h_table.push(cur_zero);
96    for _ in 1..depth {
97        cur_zero = hash_pair::<D>(&cur_zero, &cur_zero);
98        zero_h_table.push(cur_zero);
99    }
100
101    // Iterative bottom-up reduction. At each level we only iterate over the
102    // "real" entries; missing right siblings draw from `zero_h_table[d]`. The
103    // implicit padding to `padded_len` is handled by continuing to fold for
104    // the full `depth` iterations even after `level.len()` reaches 1.
105    let mut level: Vec<[u8; 32]> = Vec::new();
106    level.extend_from_slice(chunks);
107
108    for &zero_h in zero_h_table.iter().take(depth) {
109        let next_count = level.len().div_ceil(2);
110        let mut next: Vec<[u8; 32]> = Vec::with_capacity(next_count);
111        for i in 0..next_count {
112            let l = level[2 * i];
113            let r = level.get(2 * i + 1).copied().unwrap_or(zero_h);
114            next.push(hash_pair::<D>(&l, &r));
115        }
116        level = next;
117    }
118
119    level[0]
120}
121
122/// `mix_in_length(root, len) = hash(root || u256_le(len))`.
123#[inline]
124pub fn mix_in_length<D: Digest<OutputSize = U32>>(root: [u8; 32], len: u64) -> [u8; 32] {
125    let mut buf = [0u8; 32];
126    buf[..8].copy_from_slice(&len.to_le_bytes());
127    hash_pair::<D>(&root, &buf)
128}
129
130/// `mix_in_selector(root, sel) = hash(root || u256_le(sel))`.
131///
132/// Note that the selector is padded to a full 32-byte little-endian u256,
133/// matching the spec (not just one byte).
134#[inline]
135pub fn mix_in_selector<D: Digest<OutputSize = U32>>(root: [u8; 32], selector: u8) -> [u8; 32] {
136    let mut buf = [0u8; 32];
137    buf[0] = selector;
138    hash_pair::<D>(&root, &buf)
139}
140
141/// Cached zero-hash table: `zero_hash(d) = hash(zero_hash(d-1), zero_hash(d-1))`,
142/// with `zero_hash(0) == [0u8; 32]`.
143pub fn zero_hash<D: Digest<OutputSize = U32>>(depth: usize) -> [u8; 32] {
144    // We rebuild the table per call. This is sufficient for jar's tree
145    // depths (≤ 64 in practice); a future optimisation could memoize a
146    // `OnceLock<[u8; 32]; 64>` per hash type, but no_std forbids
147    // unconditional `OnceLock`. Callers that hash hot paths should cache
148    // their own zero-hash array.
149    let mut current = [0u8; 32];
150    for _ in 0..depth {
151        current = hash_pair::<D>(&current, &current);
152    }
153    current
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159    use sha2::Sha256;
160
161    #[test]
162    fn zero_hash_depth_0_is_zero() {
163        assert_eq!(zero_hash::<Sha256>(0), [0u8; 32]);
164    }
165
166    #[test]
167    fn zero_hash_depth_1_is_h00() {
168        let expected = hash_pair::<Sha256>(&[0u8; 32], &[0u8; 32]);
169        assert_eq!(zero_hash::<Sha256>(1), expected);
170    }
171
172    #[test]
173    fn merkleize_single_chunk_returns_chunk() {
174        let chunk = [0xAAu8; 32];
175        assert_eq!(merkleize::<Sha256>(&[chunk], 1), chunk);
176    }
177
178    #[test]
179    fn merkleize_two_chunks() {
180        let a = [0xAAu8; 32];
181        let b = [0xBBu8; 32];
182        let expected = hash_pair::<Sha256>(&a, &b);
183        assert_eq!(merkleize::<Sha256>(&[a, b], 2), expected);
184    }
185
186    #[test]
187    fn merkleize_empty_with_limit_zero_is_zero_chunk() {
188        assert_eq!(merkleize::<Sha256>(&[], 0), [0u8; 32]);
189    }
190
191    #[test]
192    fn merkleize_padded_with_zero_hashes() {
193        // 3 chunks → padded to 4. Right-subtree right leaf is zero_hash(0).
194        let a = [1u8; 32];
195        let b = [2u8; 32];
196        let c = [3u8; 32];
197        let left = hash_pair::<Sha256>(&a, &b);
198        let right = hash_pair::<Sha256>(&c, &[0u8; 32]);
199        let root = hash_pair::<Sha256>(&left, &right);
200        assert_eq!(merkleize::<Sha256>(&[a, b, c], 4), root);
201    }
202
203    #[test]
204    fn pack_bytes_pads_tail_with_zeros() {
205        let chunks = pack_bytes(&[1, 2, 3]);
206        assert_eq!(chunks.len(), 1);
207        let mut expected = [0u8; 32];
208        expected[0] = 1;
209        expected[1] = 2;
210        expected[2] = 3;
211        assert_eq!(chunks[0], expected);
212    }
213
214    #[test]
215    fn merkleize_huge_limit_finishes_fast() {
216        // Regression: previous algorithm was O(padded_len), so a 4-billion
217        // limit would try to allocate a 2-billion-entry Vec. Now O(depth).
218        // limit = 1 << 32 → padded_len = 2^32, depth = 32 hash operations
219        // after the initial input fold. Should complete in microseconds.
220        let chunk = [0xAAu8; 32];
221        let root = merkleize::<Sha256>(&[chunk], 1usize << 32);
222
223        // Equivalent: a single real leaf in a depth-32 tree; sibling at
224        // each level is zero_hash(d).
225        let mut current = chunk;
226        for d in 0..32 {
227            current = hash_pair::<Sha256>(&current, &zero_hash::<Sha256>(d));
228        }
229        assert_eq!(root, current);
230    }
231
232    #[test]
233    fn merkleize_empty_with_large_limit_is_zero_subtree() {
234        // Empty input, limit = 1024 → root must be zero_hash(10) without
235        // materializing 1024 zero leaves.
236        let root = merkleize::<Sha256>(&[], 1024);
237        assert_eq!(root, zero_hash::<Sha256>(10));
238    }
239
240    #[test]
241    fn merkleize_three_chunks_with_large_limit() {
242        // 3 chunks, limit = 16 → padded_len = 16, depth = 4.
243        let a = [1u8; 32];
244        let b = [2u8; 32];
245        let c = [3u8; 32];
246        let root = merkleize::<Sha256>(&[a, b, c], 16);
247
248        // Compute by hand. Level 0 has [a, b, c]; level 1 has
249        // [H(a,b), H(c, zero_h0)]; level 2 has [H(L1[0], L1[1])];
250        // level 3 has [H(L2[0], zero_h2)]; level 4 has [H(L3[0], zero_h3)].
251        let l1_0 = hash_pair::<Sha256>(&a, &b);
252        let l1_1 = hash_pair::<Sha256>(&c, &zero_hash::<Sha256>(0));
253        let l2_0 = hash_pair::<Sha256>(&l1_0, &l1_1);
254        let l3_0 = hash_pair::<Sha256>(&l2_0, &zero_hash::<Sha256>(2));
255        let l4_0 = hash_pair::<Sha256>(&l3_0, &zero_hash::<Sha256>(3));
256        assert_eq!(root, l4_0);
257    }
258
259    #[test]
260    fn ceil_log2_examples() {
261        assert_eq!(ceil_log2(0), 0);
262        assert_eq!(ceil_log2(1), 0);
263        assert_eq!(ceil_log2(2), 1);
264        assert_eq!(ceil_log2(3), 2);
265        assert_eq!(ceil_log2(4), 2);
266        assert_eq!(ceil_log2(5), 3);
267        assert_eq!(ceil_log2(8), 3);
268        assert_eq!(ceil_log2(9), 4);
269    }
270}