Skip to main content

nectar_primitives/bmt/
hasher.rs

1//! Binary Merkle Tree hasher implementation
2//!
3//! This module provides an implementation of a BMT hasher that uses Keccak256
4//! for computing content-addressed hashes of arbitrary data.
5
6use alloy_primitives::{B256, Keccak256};
7use bytes::Bytes;
8use digest::{FixedOutput, FixedOutputReset, OutputSizeUser, Reset, Update};
9use hybrid_array::{Array, sizes::U32};
10use std::io::{self, Write};
11use std::sync::LazyLock;
12
13// Use rayon for parallel processing on non-WASM platforms
14#[cfg(not(target_arch = "wasm32"))]
15use rayon;
16
17use super::constants::*;
18
19/// Number of zero tree levels for the default body size.
20const ZERO_TREE_LEVELS: usize = zero_tree_levels(DEFAULT_BODY_SIZE);
21
22/// Pre-computed zero hashes for the default body size tree.
23static ZERO_HASHES: LazyLock<[B256; ZERO_TREE_LEVELS]> = LazyLock::new(|| {
24    let mut hashes = [B256::ZERO; ZERO_TREE_LEVELS];
25
26    // Level 0: hash of 64 zero bytes (one segment pair)
27    let mut hasher = Keccak256::new();
28    hasher.update([0u8; SEGMENT_PAIR_LENGTH]);
29    hashes[0] = B256::from_slice(hasher.finalize().as_slice());
30
31    // Each subsequent level: hash of two copies of previous level's hash
32    for i in 1..ZERO_TREE_LEVELS {
33        let mut hasher = Keccak256::new();
34        hasher.update(hashes[i - 1].as_slice());
35        hasher.update(hashes[i - 1].as_slice());
36        hashes[i] = B256::from_slice(hasher.finalize().as_slice());
37    }
38
39    hashes
40});
41
42/// BMT hasher with configurable body size.
43#[derive(Debug, Clone)]
44pub struct Hasher<const BODY_SIZE: usize = DEFAULT_BODY_SIZE> {
45    span: u64,
46    prefix: Option<Vec<u8>>,
47    buffer: [u8; BODY_SIZE],
48    cursor: usize,
49}
50
51impl<const BODY_SIZE: usize> Default for Hasher<BODY_SIZE> {
52    #[inline]
53    fn default() -> Self {
54        Self::new()
55    }
56}
57
58impl<const BODY_SIZE: usize> Hasher<BODY_SIZE> {
59    /// Create a new BMT hasher.
60    #[inline]
61    pub const fn new() -> Self {
62        Self {
63            span: 0,
64            prefix: None,
65            buffer: [0u8; BODY_SIZE],
66            cursor: 0,
67        }
68    }
69
70    /// Set the span of data to be hashed
71    #[inline]
72    pub const fn set_span(&mut self, span: u64) {
73        self.span = span;
74    }
75
76    /// Get the current span
77    #[inline(always)]
78    pub const fn span(&self) -> u64 {
79        self.span
80    }
81
82    /// Add a prefix to the hash calculation.
83    ///
84    /// The prefix is applied to *every* Keccak256 invocation in the tree (leaf
85    /// sections, internal nodes and the final span wrap), matching bee's
86    /// `swarm.NewPrefixHasher` semantics where `Reset()` re-writes the prefix as
87    /// the first bytes before each node hash. This makes the resulting root
88    /// byte-identical to bee's `transformedAddress`.
89    #[inline]
90    pub fn prefix_with(&mut self, prefix: &[u8]) {
91        self.prefix = Some(prefix.to_vec());
92    }
93
94    /// Create a new BMT hasher pre-configured with an anchor `prefix`.
95    ///
96    /// Equivalent to [`Hasher::new`] followed by [`Hasher::prefix_with`]. The
97    /// prefix is mixed into every node hash (see [`Hasher::prefix_with`]), so
98    /// the produced root matches bee's anchor-keyed `transformedAddress`.
99    #[inline]
100    pub fn with_prefix(prefix: &[u8]) -> Self {
101        let mut hasher = Self::new();
102        hasher.prefix_with(prefix);
103        hasher
104    }
105
106    /// Construct a fresh Keccak256, seeded with the prefix when one is set.
107    ///
108    /// Every node in the tree is hashed as `keccak(prefix || data)`; this helper
109    /// centralises that so the prefix can never be forgotten at an individual
110    /// hash site.
111    #[inline(always)]
112    fn node_hasher(prefix: Option<&[u8]>) -> Keccak256 {
113        let mut hasher = Keccak256::new();
114        if let Some(p) = prefix {
115            hasher.update(p);
116        }
117        hasher
118    }
119
120    /// Get the current prefix
121    #[inline(always)]
122    pub fn prefix(&self) -> &[u8] {
123        self.prefix.as_deref().unwrap_or(&[])
124    }
125
126    /// Get the current cursor position
127    #[inline(always)]
128    pub const fn position(&self) -> usize {
129        self.cursor
130    }
131
132    /// Get the amount of data currently in the buffer
133    #[inline(always)]
134    pub const fn len(&self) -> usize {
135        self.cursor
136    }
137
138    /// Check if the buffer is empty
139    #[inline(always)]
140    pub const fn is_empty(&self) -> bool {
141        self.cursor == 0
142    }
143
144    /// Update the hasher with more data (non-destructive)
145    #[inline]
146    pub fn update(&mut self, data: &[u8]) {
147        if data.is_empty() {
148            return;
149        }
150
151        // Calculate how much data we can actually copy
152        let available_space = BODY_SIZE - self.cursor;
153        let bytes_to_copy = data.len().min(available_space);
154
155        if bytes_to_copy > 0 {
156            // Copy data at cursor position
157            self.buffer[self.cursor..self.cursor + bytes_to_copy]
158                .copy_from_slice(&data[..bytes_to_copy]);
159
160            // Update cursor position
161            self.cursor += bytes_to_copy;
162        }
163    }
164
165    /// Compute the BMT hash and write to output buffer.
166    #[allow(clippy::should_implement_trait)] // BMT hash, not std::hash::Hash
167    #[inline]
168    pub fn hash(&self, out: &mut [u8]) {
169        let hash = self.sum();
170        out.copy_from_slice(hash.as_slice());
171    }
172
173    /// Compute the BMT hash and return the result (non-destructive)
174    #[inline]
175    #[must_use]
176    pub fn sum(&self) -> B256 {
177        self.finalize_with_prefix(self.hash_internal())
178    }
179
180    /// Check if a byte slice is all zeros.
181    /// Uses chunk-based iteration which LLVM optimizes to SIMD on supported platforms.
182    #[inline(always)]
183    fn is_all_zeros(data: &[u8]) -> bool {
184        // Fold with bitwise OR - any non-zero byte makes the result non-zero
185        // LLVM vectorizes this pattern into efficient SIMD code
186        data.iter().fold(0u8, |acc, &b| acc | b) == 0
187    }
188
189    /// Hash data using a binary merkle tree (internal implementation)
190    ///
191    /// This uses an optimized algorithm that:
192    /// 1. Finds the smallest power-of-2 subtree containing all data
193    /// 2. Hashes only that subtree
194    /// 3. Iteratively combines with pre-computed zero hashes to reach the root
195    #[inline(always)]
196    fn hash_internal(&self) -> B256 {
197        let prefix = self.prefix.as_deref();
198
199        // Zero fast paths rely on the precomputed prefix-independent ZERO_HASHES
200        // table, which is only valid for plain (unprefixed) hashing. Under a
201        // non-empty prefix every zero section hashes as keccak(prefix||zeros),
202        // so we must compute the zero subtrees with the prefix instead.
203        let zero_hashes = self.zero_hashes(prefix);
204
205        // Special case: no data means entire tree is zeros
206        if self.cursor == 0 {
207            return zero_hashes[ZERO_TREE_LEVELS - 1];
208        }
209
210        // Fast path: if all data is zeros, return the zero tree root.
211        // Valid for both plain and prefixed hashing because `zero_hashes`
212        // already accounts for the prefix.
213        if Self::is_all_zeros(&self.buffer[..self.cursor]) {
214            return zero_hashes[ZERO_TREE_LEVELS - 1];
215        }
216
217        // Find the smallest power-of-2 subtree that contains all data
218        let effective_size = self
219            .cursor
220            .next_power_of_two()
221            .max(SEGMENT_PAIR_LENGTH)
222            .min(BODY_SIZE);
223
224        // Hash only the effective subtree (which contains all actual data).
225        // The parallel zero-shortcut path uses the prefix-independent
226        // ZERO_HASHES; with a prefix set we recurse fully via the sequential
227        // path so every zero section is hashed under the prefix.
228        #[cfg(not(target_arch = "wasm32"))]
229        let mut result = if prefix.is_some() {
230            self.hash_subtree_sequential(&self.buffer[..effective_size], effective_size)
231        } else {
232            self.hash_subtree_parallel(&self.buffer[..effective_size], effective_size)
233        };
234
235        #[cfg(target_arch = "wasm32")]
236        let mut result =
237            self.hash_subtree_sequential(&self.buffer[..effective_size], effective_size);
238
239        // Roll up with zero hashes until we reach the full tree size
240        let mut current_size = effective_size;
241        while current_size < BODY_SIZE {
242            // The current result is a left child, combine with zero hash for right sibling
243            let sibling_level = Self::zero_tree_level(current_size);
244            let mut hasher = Self::node_hasher(prefix);
245            hasher.update(result.as_slice());
246            hasher.update(zero_hashes[sibling_level].as_slice());
247            result = B256::from_slice(hasher.finalize().as_slice());
248            current_size *= 2;
249        }
250
251        result
252    }
253
254    /// Return the per-level zero subtree hashes for the current prefix.
255    ///
256    /// With no prefix this returns the shared precomputed [`ZERO_HASHES`]. With
257    /// a prefix set it computes the table on demand so that each level is
258    /// `keccak(prefix || left || right)` (the level-0 entry being
259    /// `keccak(prefix || 64 zero bytes)`), matching bee's per-prefix
260    /// `zerohashes`.
261    #[inline(always)]
262    fn zero_hashes(&self, prefix: Option<&[u8]>) -> [B256; ZERO_TREE_LEVELS] {
263        let Some(p) = prefix else {
264            return *ZERO_HASHES;
265        };
266
267        let mut hashes = [B256::ZERO; ZERO_TREE_LEVELS];
268
269        let mut hasher = Self::node_hasher(Some(p));
270        hasher.update([0u8; SEGMENT_PAIR_LENGTH]);
271        hashes[0] = B256::from_slice(hasher.finalize().as_slice());
272
273        for i in 1..ZERO_TREE_LEVELS {
274            let mut hasher = Self::node_hasher(Some(p));
275            hasher.update(hashes[i - 1].as_slice());
276            hasher.update(hashes[i - 1].as_slice());
277            hashes[i] = B256::from_slice(hasher.finalize().as_slice());
278        }
279
280        hashes
281    }
282
283    /// Hash a subtree of exactly `length` bytes (must be power of 2, >= 64)
284    ///
285    /// For sizes < BODY_SIZE: uses sequential hashing (no rayon overhead).
286    /// For BODY_SIZE (4096): uses recursive parallel hashing for maximum throughput.
287    #[cfg(not(target_arch = "wasm32"))]
288    #[inline(always)]
289    fn hash_subtree_parallel(&self, data: &[u8], length: usize) -> B256 {
290        debug_assert!(length.is_power_of_two());
291        debug_assert!(length >= SEGMENT_PAIR_LENGTH);
292
293        // For sizes < BODY_SIZE, use sequential (avoids rayon overhead for small/medium sizes)
294        if length < BODY_SIZE {
295            return self.hash_subtree_sequential(data, length);
296        }
297
298        // For BODY_SIZE (4096): use recursive parallel hashing
299        // Pass cursor as parameter to avoid self indirection in hot loop
300        Self::hash_subtree_recursive_parallel_inner(data, length, self.cursor)
301    }
302
303    /// Recursively hash a subtree using rayon for parallelism.
304    /// Only called for full BODY_SIZE chunks where parallelism pays off.
305    /// Takes cursor as parameter to avoid self indirection in recursive calls.
306    #[cfg(not(target_arch = "wasm32"))]
307    #[inline(always)]
308    fn hash_subtree_recursive_parallel_inner(data: &[u8], length: usize, cursor: usize) -> B256 {
309        debug_assert!(length.is_power_of_two());
310        debug_assert!(length >= SEGMENT_PAIR_LENGTH);
311
312        // Base case: 64 bytes (one segment pair)
313        if length == SEGMENT_PAIR_LENGTH {
314            let mut hasher = Keccak256::new();
315            hasher.update(data);
316            return B256::from_slice(hasher.finalize().as_slice());
317        }
318
319        let half = length / 2;
320        let (left, right) = data.split_at(half);
321
322        // Check if right half is entirely beyond cursor (all zeros in buffer)
323        // cursor is relative to the start of this subtree
324        let (left_hash, right_hash) = if half >= cursor {
325            // Right side is all zeros - compute left only, use precomputed right
326            let left_hash = Self::hash_subtree_recursive_parallel_inner(left, half, cursor);
327            let right_hash = ZERO_HASHES[Self::zero_tree_level(half)];
328            (left_hash, right_hash)
329        } else {
330            // Both sides have data, use parallel execution
331            // Left cursor is capped at half (can't exceed subtree size)
332            // Right cursor is adjusted by half (relative to right subtree start)
333            rayon::join(
334                || Self::hash_subtree_recursive_parallel_inner(left, half, half),
335                || Self::hash_subtree_recursive_parallel_inner(right, half, cursor - half),
336            )
337        };
338
339        let mut hasher = Keccak256::new();
340        hasher.update(left_hash.as_slice());
341        hasher.update(right_hash.as_slice());
342        B256::from_slice(hasher.finalize().as_slice())
343    }
344
345    /// Hash a subtree of exactly `length` bytes (must be power of 2, >= 64) - sequential version
346    #[inline(always)]
347    fn hash_subtree_sequential(&self, data: &[u8], length: usize) -> B256 {
348        debug_assert!(length.is_power_of_two());
349        debug_assert!(length >= SEGMENT_PAIR_LENGTH);
350
351        let prefix = self.prefix.as_deref();
352
353        if length == SEGMENT_PAIR_LENGTH {
354            let mut hasher = Self::node_hasher(prefix);
355            hasher.update(data);
356            return B256::from_slice(hasher.finalize().as_slice());
357        }
358
359        let half = length / 2;
360        let (left, right) = data.split_at(half);
361
362        // Check if right half is entirely beyond cursor (all zeros in buffer).
363        // The zero-sibling shortcut is only safe when the zero table matches the
364        // prefix; under a prefix we recurse over the literal zero section so it
365        // is hashed as keccak(prefix||...).
366        let (left_hash, right_hash) = if half >= self.cursor && prefix.is_none() {
367            // Right side is all zeros (plain hashing only)
368            let left_hash = self.hash_subtree_sequential(left, half);
369            let right_hash = ZERO_HASHES[Self::zero_tree_level(half)];
370            (left_hash, right_hash)
371        } else {
372            let left_hash = self.hash_subtree_sequential(left, half);
373            let right_hash = self.hash_subtree_sequential(right, half);
374            (left_hash, right_hash)
375        };
376
377        let mut hasher = Self::node_hasher(prefix);
378        hasher.update(left_hash.as_slice());
379        hasher.update(right_hash.as_slice());
380        B256::from_slice(hasher.finalize().as_slice())
381    }
382
383    /// Calculate the zero-tree level for a given subtree length.
384    /// Length must be a power of 2 between 64 and 4096.
385    #[inline(always)]
386    const fn zero_tree_level(length: usize) -> usize {
387        // length = 64 * 2^level, so level = log2(length) - log2(64) = log2(length) - 6
388        length.trailing_zeros() as usize - 6
389    }
390
391    /// Finalize with span and optional prefix
392    #[inline(always)]
393    fn finalize_with_prefix(&self, intermediate_hash: B256) -> B256 {
394        let mut hasher = Keccak256::new();
395
396        // Add prefix if present
397        if let Some(prefix) = &self.prefix {
398            hasher.update(prefix);
399        }
400
401        // Add span as little-endian bytes
402        hasher.update(self.span.to_le_bytes());
403
404        // Add the intermediate hash
405        hasher.update(intermediate_hash.as_slice());
406
407        // Finalize to get the result
408        B256::from_slice(hasher.finalize().as_slice())
409    }
410
411    /// Reset the hasher's internal state
412    #[inline(always)]
413    const fn reset_internal(&mut self) {
414        // Simply reset cursor - no need to clear the buffer as it will be overwritten
415        self.cursor = 0;
416        self.span = 0;
417        // Don't reset prefix, as it's considered a configuration parameter
418    }
419
420    /// Get the current data as Bytes (immutable reference)
421    #[inline]
422    #[must_use]
423    pub fn data(&self) -> Bytes {
424        if self.cursor == 0 {
425            return Bytes::new();
426        }
427
428        // Create Bytes from slice
429        Bytes::copy_from_slice(&self.buffer[..self.cursor])
430    }
431
432    /// Get segments for the current level of data
433    #[inline]
434    pub fn get_level_segments(&self, data: &[u8]) -> Vec<B256> {
435        let branches = branches_for_body_size(BODY_SIZE);
436
437        #[cfg(not(target_arch = "wasm32"))]
438        {
439            use rayon::prelude::*;
440            (0..branches)
441                .into_par_iter()
442                .map(|i| self.compute_segment_hash(data, i))
443                .collect()
444        }
445
446        #[cfg(target_arch = "wasm32")]
447        {
448            (0..branches)
449                .map(|i| self.compute_segment_hash(data, i))
450                .collect()
451        }
452    }
453
454    /// Compute the hash for a single segment at given index
455    #[inline(always)]
456    fn compute_segment_hash(&self, data: &[u8], i: usize) -> B256 {
457        let start = i << SEGMENT_SIZE_LOG2; // Equivalent to i * SEGMENT_SIZE
458        let mut hasher = Self::node_hasher(self.prefix.as_deref());
459
460        if start < data.len() {
461            let end = (start + SEGMENT_SIZE).min(data.len());
462            let segment_data = &data[start..end];
463
464            // Update with segment data
465            hasher.update(segment_data);
466
467            // If segment is shorter than SEGMENT_SIZE, the remaining bytes are zeros
468            if segment_data.len() < SEGMENT_SIZE {
469                hasher.update(&[0u8; SEGMENT_SIZE][..(SEGMENT_SIZE - segment_data.len())]);
470            }
471        } else {
472            // Empty segment (all zeros)
473            hasher.update([0u8; SEGMENT_SIZE]);
474        }
475
476        B256::from_slice(hasher.finalize().as_slice())
477    }
478}
479
480impl<const BODY_SIZE: usize> Write for Hasher<BODY_SIZE> {
481    #[inline]
482    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
483        let available = BODY_SIZE - self.cursor;
484        let to_write = buf.len().min(available);
485        if to_write > 0 {
486            self.buffer[self.cursor..self.cursor + to_write].copy_from_slice(&buf[..to_write]);
487            self.cursor += to_write;
488        }
489        Ok(to_write)
490    }
491
492    #[inline]
493    fn flush(&mut self) -> io::Result<()> {
494        Ok(())
495    }
496}
497
498impl<const BODY_SIZE: usize> OutputSizeUser for Hasher<BODY_SIZE> {
499    type OutputSize = U32;
500}
501
502impl<const BODY_SIZE: usize> Update for Hasher<BODY_SIZE> {
503    #[inline]
504    fn update(&mut self, data: &[u8]) {
505        self.update(data);
506    }
507}
508
509impl<const BODY_SIZE: usize> Reset for Hasher<BODY_SIZE> {
510    #[inline]
511    fn reset(&mut self) {
512        self.reset_internal();
513    }
514}
515
516impl<const BODY_SIZE: usize> FixedOutput for Hasher<BODY_SIZE> {
517    #[inline]
518    fn finalize_into(self, out: &mut Array<u8, Self::OutputSize>) {
519        let b256 = self.sum();
520        out.copy_from_slice(b256.as_slice());
521    }
522}
523
524impl<const BODY_SIZE: usize> FixedOutputReset for Hasher<BODY_SIZE> {
525    #[inline]
526    fn finalize_into_reset(&mut self, out: &mut Array<u8, Self::OutputSize>) {
527        let b256 = self.sum();
528        out.copy_from_slice(b256.as_slice());
529        self.reset_internal();
530    }
531}
532
533impl<const BODY_SIZE: usize> digest::HashMarker for Hasher<BODY_SIZE> {}
534
535/// Factory for creating BMT hashers.
536#[derive(Debug, Default, Clone)]
537pub struct HasherFactory<const BODY_SIZE: usize = DEFAULT_BODY_SIZE>;
538
539impl<const BODY_SIZE: usize> HasherFactory<BODY_SIZE> {
540    /// Create a new factory.
541    #[inline]
542    pub const fn new() -> Self {
543        Self
544    }
545
546    /// Create a new BMT hasher.
547    #[inline]
548    pub const fn create_hasher(&self) -> Hasher<BODY_SIZE> {
549        Hasher::new()
550    }
551}