Skip to main content

p3_symmetric/
serializing_hasher.rs

1use alloc::vec::Vec;
2
3use p3_field::Field;
4
5use crate::CryptographicHasher;
6
7/// Byte target for the buffer holding the serialized bytes of one group of rows.
8///
9/// The batched inner hash needs its messages back to back in memory, and field elements reach
10/// byte form only through a serializing iterator, so a group is staged here before it is hashed.
11///
12/// 8 KiB stays inside a typical 32 KiB L1 data cache. It is a target rather than a cap: a group
13/// is always a whole number of lane groups, so one lane group of wider rows overshoots it rather
14/// than leaving lanes idle. The inner hasher is the one that knows what a full call costs.
15const ROW_BYTES_SCRATCH: usize = 8 * 1024;
16
17/// Converts a hasher which can hash bytes, u32's or u64's into a hasher which can hash field elements.
18///
19/// Supports two types of hashing.
20/// - Hashing a sequence of field elements.
21/// - Hashing a sequence of arrays of `N` field elements as if we are hashing `N` sequences of field elements in parallel.
22///   This is useful when the inner hash is able to use vectorized instructions to compute multiple hashes at once.
23#[derive(Copy, Clone, Debug)]
24pub struct SerializingHasher<Inner> {
25    inner: Inner,
26}
27
28impl<Inner> SerializingHasher<Inner> {
29    pub const fn new(inner: Inner) -> Self {
30        Self { inner }
31    }
32}
33
34impl<F, Inner, const N: usize> CryptographicHasher<F, [u8; N]> for SerializingHasher<Inner>
35where
36    F: Field,
37    Inner: CryptographicHasher<u8, [u8; N]>,
38{
39    /// Forwarded from the inner byte hasher, the one digest shape here that batches.
40    ///
41    /// The `[u32; N]` and `[u64; N]` shapes wrap word sponges that hash a single message per call.
42    /// The packed `[[uX; M]; N]` shapes already drive a vector permutation, one lane per packed slot.
43    ///
44    /// Both keep the trait default of one lane, which routes a Merkle tree through its packed arm.
45    const LANES: usize = <Inner as CryptographicHasher<u8, [u8; N]>>::LANES;
46
47    fn hash_iter<I>(&self, input: I) -> [u8; N]
48    where
49        I: IntoIterator<Item = F>,
50    {
51        self.inner.hash_iter(F::into_byte_stream(input))
52    }
53
54    fn hash_many(&self, input: &[F], out: &mut [[u8; N]]) {
55        // No digests requested means there is nothing to read from the input.
56        if out.is_empty() {
57            return;
58        }
59
60        // Every message has the same length, so the split into rows is exact by contract.
61        assert!(
62            input.len().is_multiple_of(out.len()),
63            "input length ({}) must be a whole multiple of the digest count ({})",
64            input.len(),
65            out.len()
66        );
67        let row_len = input.len() / out.len();
68        let row_bytes = row_len * F::NUM_BYTES;
69
70        // Width-zero rows all hash to the same digest of the empty message.
71        if row_len == 0 {
72            for digest in out.iter_mut() {
73                *digest = self.hash_iter(core::iter::empty::<F>());
74            }
75            return;
76        }
77
78        // Serialize as many whole rows at a time as the budget holds.
79        // The inner hasher then sees a long run of equal-length byte messages:
80        //
81        //     rows:    [ r_0 | r_1 | ... ]              row_len field elements each
82        //     scratch: [ b_0 | b_1 | ... ]              row_bytes bytes each
83        //
84        // The group is a whole number of lane groups, so every call fills the inner hasher's
85        // lanes. A row wide enough that not even one lane group fits the target still gets a
86        // full one: splitting it would push the remainder onto the inner scalar path, which is
87        // the cost the grouping exists to avoid. This mirrors `p3-merkle-tree`'s `rows_per_call`,
88        // whose groups arrive here already sized in whole lane groups, so splitting one further
89        // still lands on a lane-group boundary.
90        let lanes = Inner::LANES.max(1);
91        let rows_per_group = (ROW_BYTES_SCRATCH / row_bytes / lanes).max(1) * lanes;
92
93        // One buffer per call, refilled group by group, holding the largest group exactly.
94        // Every byte of a group is written before it is read, so none of it is zeroed first.
95        let mut scratch: Vec<u8> = Vec::with_capacity(rows_per_group.min(out.len()) * row_bytes);
96
97        for (rows, digests) in input
98            .chunks(rows_per_group * row_len)
99            .zip(out.chunks_mut(rows_per_group))
100        {
101            // Serialize row by row so each message starts on its own row boundary.
102            // Boundaries come from the declared serialized width of one field element.
103            //
104            // A stream of any other length shifts every later message.
105            // Checking the running length after each row catches both directions.
106            scratch.clear();
107            for (index, row) in rows.chunks(row_len).enumerate() {
108                scratch.extend(F::into_byte_stream(row.iter().copied()));
109                assert_eq!(
110                    scratch.len(),
111                    (index + 1) * row_bytes,
112                    "field serialization must be its declared width of {} bytes per element",
113                    F::NUM_BYTES
114                );
115            }
116
117            self.inner.hash_many(&scratch, digests);
118        }
119    }
120}
121
122impl<F, Inner, const N: usize> CryptographicHasher<F, [u32; N]> for SerializingHasher<Inner>
123where
124    F: Field,
125    Inner: CryptographicHasher<u32, [u32; N]>,
126{
127    fn hash_iter<I>(&self, input: I) -> [u32; N]
128    where
129        I: IntoIterator<Item = F>,
130    {
131        self.inner.hash_iter(F::into_u32_stream(input))
132    }
133}
134
135impl<F, Inner, const N: usize> CryptographicHasher<F, [u64; N]> for SerializingHasher<Inner>
136where
137    F: Field,
138    Inner: CryptographicHasher<u64, [u64; N]>,
139{
140    fn hash_iter<I>(&self, input: I) -> [u64; N]
141    where
142        I: IntoIterator<Item = F>,
143    {
144        self.inner.hash_iter(F::into_u64_stream(input))
145    }
146}
147
148impl<F, Inner, const N: usize, const M: usize> CryptographicHasher<[F; M], [[u8; M]; N]>
149    for SerializingHasher<Inner>
150where
151    F: Field,
152    Inner: CryptographicHasher<[u8; M], [[u8; M]; N]>,
153{
154    fn hash_iter<I>(&self, input: I) -> [[u8; M]; N]
155    where
156        I: IntoIterator<Item = [F; M]>,
157    {
158        self.inner.hash_iter(F::into_parallel_byte_streams(input))
159    }
160}
161
162impl<F, Inner, const N: usize, const M: usize> CryptographicHasher<[F; M], [[u32; M]; N]>
163    for SerializingHasher<Inner>
164where
165    F: Field,
166    Inner: CryptographicHasher<[u32; M], [[u32; M]; N]>,
167{
168    fn hash_iter<I>(&self, input: I) -> [[u32; M]; N]
169    where
170        I: IntoIterator<Item = [F; M]>,
171    {
172        self.inner.hash_iter(F::into_parallel_u32_streams(input))
173    }
174}
175
176impl<F, Inner, const N: usize, const M: usize> CryptographicHasher<[F; M], [[u64; M]; N]>
177    for SerializingHasher<Inner>
178where
179    F: Field,
180    Inner: CryptographicHasher<[u64; M], [[u64; M]; N]>,
181{
182    // Flattened iterators inhibit vectorization when pairing 32-bit elements into u64s.
183    // Staging does not help the direct u64 mapping of 64-bit fields.
184    const PREFER_CONTIGUOUS_INPUT: bool = M > 1 && F::NUM_BYTES == 4;
185
186    fn hash_iter<I>(&self, input: I) -> [[u64; M]; N]
187    where
188        I: IntoIterator<Item = [F; M]>,
189    {
190        self.inner.hash_iter(F::into_parallel_u64_streams(input))
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use alloc::rc::Rc;
197    use alloc::vec;
198    use alloc::vec::Vec;
199    use core::array;
200    use core::cell::RefCell;
201
202    use p3_field::PrimeCharacteristicRing;
203    use p3_koala_bear::KoalaBear;
204
205    use crate::{CryptographicHasher, SerializingHasher};
206
207    #[derive(Clone)]
208    struct MockHasher;
209
210    impl CryptographicHasher<u8, [u8; 4]> for MockHasher {
211        fn hash_iter<I: IntoIterator<Item = u8>>(&self, iter: I) -> [u8; 4] {
212            let sum: u8 = iter.into_iter().fold(0, |acc, x| acc.wrapping_add(x));
213            // Simplest impl: set every element to the sum
214            [sum; 4]
215        }
216    }
217
218    impl CryptographicHasher<[u8; 4], [[u8; 4]; 4]> for MockHasher {
219        fn hash_iter<I: IntoIterator<Item = [u8; 4]>>(&self, iter: I) -> [[u8; 4]; 4] {
220            let sum: [u8; 4] = iter.into_iter().fold([0, 0, 0, 0], |acc, x| {
221                [
222                    acc[0].wrapping_add(x[0]),
223                    acc[1].wrapping_add(x[1]),
224                    acc[2].wrapping_add(x[2]),
225                    acc[3].wrapping_add(x[3]),
226                ]
227            });
228            // Simplest impl: set every element to the sum
229            [sum; 4]
230        }
231    }
232
233    impl CryptographicHasher<u32, [u32; 4]> for MockHasher {
234        fn hash_iter<I: IntoIterator<Item = u32>>(&self, iter: I) -> [u32; 4] {
235            let sum: u32 = iter.into_iter().fold(0, |acc, x| acc.wrapping_add(x));
236            // Simplest impl: set every element to the sum
237            [sum; 4]
238        }
239    }
240
241    impl CryptographicHasher<[u32; 4], [[u32; 4]; 4]> for MockHasher {
242        fn hash_iter<I: IntoIterator<Item = [u32; 4]>>(&self, iter: I) -> [[u32; 4]; 4] {
243            let sum: [u32; 4] = iter.into_iter().fold([0, 0, 0, 0], |acc, x| {
244                [
245                    acc[0].wrapping_add(x[0]),
246                    acc[1].wrapping_add(x[1]),
247                    acc[2].wrapping_add(x[2]),
248                    acc[3].wrapping_add(x[3]),
249                ]
250            });
251            // Simplest impl: set every element to the sum
252            [sum; 4]
253        }
254    }
255
256    impl CryptographicHasher<u64, [u64; 4]> for MockHasher {
257        fn hash_iter<I: IntoIterator<Item = u64>>(&self, iter: I) -> [u64; 4] {
258            let sum: u64 = iter.into_iter().fold(0, |acc, x| acc.wrapping_add(x));
259            // Simplest impl: set every element to the sum
260            [sum; 4]
261        }
262    }
263
264    impl CryptographicHasher<[u64; 4], [[u64; 4]; 4]> for MockHasher {
265        fn hash_iter<I: IntoIterator<Item = [u64; 4]>>(&self, iter: I) -> [[u64; 4]; 4] {
266            let sum: [u64; 4] = iter.into_iter().fold([0, 0, 0, 0], |acc, x| {
267                [
268                    acc[0].wrapping_add(x[0]),
269                    acc[1].wrapping_add(x[1]),
270                    acc[2].wrapping_add(x[2]),
271                    acc[3].wrapping_add(x[3]),
272                ]
273            });
274            // Simplest impl: set every element to the sum
275            [sum; 4]
276        }
277    }
278
279    #[test]
280    fn test_parallel_hashers() {
281        let mock_hash = MockHasher {};
282        let hasher = SerializingHasher::new(mock_hash);
283        let input: [KoalaBear; 256] = KoalaBear::new_array(array::from_fn(|x| x as u32));
284
285        let parallel_input: [[KoalaBear; 4]; 64] = unsafe { core::mem::transmute(input) };
286        let unzipped_input: [[KoalaBear; 64]; 4] = array::from_fn(|i| parallel_input.map(|x| x[i]));
287
288        let u8_output_parallel: [[u8; 4]; 4] = hasher.hash_iter(parallel_input);
289        let u8_output_individual: [[u8; 4]; 4] = unzipped_input.map(|x| hasher.hash_iter(x));
290        let u8_output_individual_transposed =
291            array::from_fn(|i| u8_output_individual.map(|x| x[i]));
292
293        let u32_output_parallel: [[u32; 4]; 4] = hasher.hash_iter(parallel_input);
294        let u32_output_individual: [[u32; 4]; 4] = unzipped_input.map(|x| hasher.hash_iter(x));
295        let u32_output_individual_transposed =
296            array::from_fn(|i| u32_output_individual.map(|x| x[i]));
297
298        let u64_output_parallel: [[u64; 4]; 4] = hasher.hash_iter(parallel_input);
299        let u64_output_individual: [[u64; 4]; 4] = unzipped_input.map(|x| hasher.hash_iter(x));
300        let u64_output_individual_transposed =
301            array::from_fn(|i| u64_output_individual.map(|x| x[i]));
302
303        assert_eq!(u8_output_parallel, u8_output_individual_transposed);
304        assert_eq!(u32_output_parallel, u32_output_individual_transposed);
305        assert_eq!(u64_output_parallel, u64_output_individual_transposed);
306    }
307
308    /// A byte hasher that records the digest count of every batched call it is handed.
309    ///
310    /// Three lanes is deliberately neither a power of two nor a divisor of the byte budget, so a
311    /// group that is not lane aligned shows up as a count that three does not divide.
312    #[derive(Clone)]
313    struct LaneRecorder {
314        calls: Rc<RefCell<Vec<usize>>>,
315    }
316
317    impl LaneRecorder {
318        fn new() -> (Self, Rc<RefCell<Vec<usize>>>) {
319            let calls = Rc::new(RefCell::new(Vec::new()));
320            (
321                Self {
322                    calls: Rc::clone(&calls),
323                },
324                calls,
325            )
326        }
327    }
328
329    impl CryptographicHasher<u8, [u8; 4]> for LaneRecorder {
330        const LANES: usize = 3;
331
332        fn hash_iter<I: IntoIterator<Item = u8>>(&self, iter: I) -> [u8; 4] {
333            MockHasher.hash_iter(iter)
334        }
335
336        fn hash_many(&self, input: &[u8], out: &mut [[u8; 4]]) {
337            if out.is_empty() {
338                return;
339            }
340            self.calls.borrow_mut().push(out.len());
341
342            let len = input.len() / out.len();
343            for (digest, message) in out.iter_mut().zip(input.chunks_exact(len)) {
344                *digest = self.hash_iter(message.iter().copied());
345            }
346        }
347    }
348
349    /// A run of `rows * row_len` distinct field elements, laid out row by row.
350    fn rows_of(rows: usize, row_len: usize) -> Vec<KoalaBear> {
351        (0..rows * row_len)
352            .map(|i| KoalaBear::from_u32(i as u32))
353            .collect()
354    }
355
356    /// Digests of the same rows taken one at a time through the unbatched path.
357    fn digests_one_by_one<H>(
358        hasher: &SerializingHasher<H>,
359        input: &[KoalaBear],
360        row_len: usize,
361    ) -> Vec<[u8; 4]>
362    where
363        H: CryptographicHasher<u8, [u8; 4]>,
364    {
365        input
366            .chunks(row_len)
367            .map(|row| hasher.hash_iter(row.iter().copied()))
368            .collect()
369    }
370
371    #[test]
372    fn hash_many_matches_the_unbatched_digests() {
373        let hasher = SerializingHasher::new(MockHasher);
374
375        // Row widths around the group boundaries: one element, a lane group, the widest row
376        // that fits the 8 KiB budget, one element past it, and a row several times that width.
377        for row_len in [1, 3, 7, 100, 2048, 2049, 2100, 5000] {
378            for rows in [1, 2, 3, 4, 7] {
379                let input = rows_of(rows, row_len);
380                let mut batched = vec![[0u8; 4]; rows];
381                hasher.hash_many(&input, &mut batched);
382
383                assert_eq!(
384                    batched,
385                    digests_one_by_one(&hasher, &input, row_len),
386                    "rows {rows} of width {row_len}"
387                );
388            }
389        }
390    }
391
392    #[test]
393    fn hash_many_hands_the_inner_hasher_whole_lane_groups() {
394        let (inner, calls) = LaneRecorder::new();
395        let hasher = SerializingHasher::new(inner);
396
397        // 400 bytes a row lets 20 rows fit the 8 KiB budget, which rounds down to six lane
398        // groups of three.
399        let input = rows_of(40, 100);
400        let mut out = vec![[0u8; 4]; 40];
401        hasher.hash_many(&input, &mut out);
402
403        // Only the final call is short, and it is short because the input ran out.
404        assert_eq!(*calls.borrow(), vec![18, 18, 4]);
405        assert_eq!(out, digests_one_by_one(&hasher, &input, 100));
406    }
407
408    #[test]
409    fn hash_many_still_fills_the_lanes_when_a_lane_group_overshoots_the_target() {
410        let (inner, calls) = LaneRecorder::new();
411        let hasher = SerializingHasher::new(inner);
412
413        // 2800 bytes a row leaves room for two rows, fewer than the three a lane group needs.
414        // Overshooting the target beats splitting the group, which would strand the third row
415        // on the inner hasher's scalar path.
416        let input = rows_of(6, 700);
417        let mut out = vec![[0u8; 4]; 6];
418        hasher.hash_many(&input, &mut out);
419
420        assert_eq!(*calls.borrow(), vec![3, 3]);
421        assert_eq!(out, digests_one_by_one(&hasher, &input, 700));
422    }
423
424    #[test]
425    fn hash_many_still_groups_by_one_lane_group_past_the_row_budget() {
426        let (inner, calls) = LaneRecorder::new();
427        let hasher = SerializingHasher::new(inner);
428
429        // 2049 elements are 8196 bytes, past the 8 KiB budget, so a group is one lane group
430        // of rows rather than the whole batch, and the final group is whatever remains.
431        let input = rows_of(4, 2049);
432        let mut out = vec![[0u8; 4]; 4];
433        hasher.hash_many(&input, &mut out);
434
435        assert_eq!(*calls.borrow(), vec![3, 1]);
436        assert_eq!(out, digests_one_by_one(&hasher, &input, 2049));
437    }
438
439    #[test]
440    fn hash_many_still_groups_by_one_lane_group_far_above_the_row_budget() {
441        let (inner, calls) = LaneRecorder::new();
442        let hasher = SerializingHasher::new(inner);
443
444        // 5000 elements are 20000 bytes, several times the 8 KiB budget, and seven rows is
445        // not a whole number of the three-lane group, so the last group is short.
446        let input = rows_of(7, 5000);
447        let mut out = vec![[0u8; 4]; 7];
448        hasher.hash_many(&input, &mut out);
449
450        assert_eq!(*calls.borrow(), vec![3, 3, 1]);
451        assert_eq!(out, digests_one_by_one(&hasher, &input, 5000));
452    }
453
454    #[test]
455    fn hash_many_of_width_zero_rows_hashes_the_empty_message() {
456        let hasher = SerializingHasher::new(MockHasher);
457
458        // No input at all with digests requested means every row is empty.
459        let mut out = vec![[1u8; 4]; 3];
460        hasher.hash_many(&[] as &[KoalaBear], &mut out);
461
462        let empty: [u8; 4] = hasher.hash_iter(core::iter::empty::<KoalaBear>());
463        assert_eq!(out, vec![empty; 3]);
464    }
465
466    #[test]
467    fn hash_many_reads_nothing_when_no_digests_are_requested() {
468        let hasher = SerializingHasher::new(MockHasher);
469
470        // A row length cannot be derived from zero digests, so the input is left untouched
471        // instead of dividing by zero or tripping the multiple check.
472        let mut out: [[u8; 4]; 0] = [];
473        hasher.hash_many(&rows_of(3, 5), &mut out);
474    }
475
476    #[test]
477    #[should_panic(expected = "must be a whole multiple")]
478    fn hash_many_rejects_ragged_input() {
479        let hasher = SerializingHasher::new(MockHasher);
480
481        // Five elements cannot split into two equal rows.
482        let mut out = [[0u8; 4]; 2];
483        hasher.hash_many(&rows_of(5, 1), &mut out);
484    }
485}