1use std::{array, mem::MaybeUninit};
31
32use binius_hash::Blake3Compression;
33use binius_utils::{
34 FixedSizeSerializeBytes, SerializeBytes,
35 rayon::{
36 iter::{IndexedParallelIterator, ParallelIterator},
37 slice::{ParallelSlice, ParallelSliceMut},
38 },
39};
40use blake3::{BLOCK_LEN, CHUNK_LEN, OUT_LEN};
41use digest::Output;
42
43#[cfg(all(target_arch = "x86_64", target_feature = "avx512f"))]
44use super::avx512;
45#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
46use super::neon;
47use super::{CHUNK_END, CHUNK_START, IV, MSG_SCHEDULE, ROOT};
48use crate::{
49 parallel_compression::ParallelPseudoCompression,
50 parallel_digest::{
51 MultiDigest, ParallelDigest, ParallelDigestAdapter, ParallelMultidigestImpl,
52 },
53};
54
55#[inline(always)]
60fn quarter_round<const N: usize>(
61 v: &mut [[u32; N]; 16],
62 a: usize,
63 b: usize,
64 c: usize,
65 d: usize,
66 mx: &[u32; N],
67 my: &[u32; N],
68) {
69 for i in 0..N {
71 v[a][i] = v[a][i].wrapping_add(v[b][i]).wrapping_add(mx[i]);
72 v[d][i] = (v[d][i] ^ v[a][i]).rotate_right(16);
73 v[c][i] = v[c][i].wrapping_add(v[d][i]);
74 v[b][i] = (v[b][i] ^ v[c][i]).rotate_right(12);
75 v[a][i] = v[a][i].wrapping_add(v[b][i]).wrapping_add(my[i]);
76 v[d][i] = (v[d][i] ^ v[a][i]).rotate_right(8);
77 v[c][i] = v[c][i].wrapping_add(v[d][i]);
78 v[b][i] = (v[b][i] ^ v[c][i]).rotate_right(7);
79 }
80}
81
82#[inline(always)]
86fn round<const R: usize, const N: usize>(v: &mut [[u32; N]; 16], m: &[[u32; N]; 16]) {
87 let s = MSG_SCHEDULE[R];
89
90 quarter_round(v, 0, 4, 8, 12, &m[s[0]], &m[s[1]]);
92 quarter_round(v, 1, 5, 9, 13, &m[s[2]], &m[s[3]]);
93 quarter_round(v, 2, 6, 10, 14, &m[s[4]], &m[s[5]]);
94 quarter_round(v, 3, 7, 11, 15, &m[s[6]], &m[s[7]]);
95 quarter_round(v, 0, 5, 10, 15, &m[s[8]], &m[s[9]]);
97 quarter_round(v, 1, 6, 11, 12, &m[s[10]], &m[s[11]]);
98 quarter_round(v, 2, 7, 8, 13, &m[s[12]], &m[s[13]]);
99 quarter_round(v, 3, 4, 9, 14, &m[s[14]], &m[s[15]]);
100}
101
102#[inline(always)]
107fn load_block_words<const N: usize>(block: &[[u8; BLOCK_LEN]; N]) -> [[u32; N]; 16] {
108 #[cfg(all(target_arch = "x86_64", target_feature = "avx512f"))]
109 if avx512::handles_lanes(N) {
110 return avx512::load_block_words(block);
111 }
112
113 #[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
114 if neon::transposes_lanes(N) {
115 return neon::load_block_words(block);
116 }
117
118 load_block_words_portable(block)
119}
120
121#[inline(always)]
127fn load_block_words_portable<const N: usize>(block: &[[u8; BLOCK_LEN]; N]) -> [[u32; N]; 16] {
128 let mut m = [[0u32; N]; 16];
129 for lane in 0..N {
130 for (w, slot) in m.iter_mut().enumerate() {
131 let off = w * 4;
132 slot[lane] = u32::from_le_bytes([
133 block[lane][off],
134 block[lane][off + 1],
135 block[lane][off + 2],
136 block[lane][off + 3],
137 ]);
138 }
139 }
140 m
141}
142
143#[inline(always)]
148fn compress_block<const N: usize>(
149 cv: &mut [[u32; N]; 8],
150 block: &[[u32; N]; 16],
151 counter: u64,
152 block_len: u32,
153 flags: u32,
154) {
155 #[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
158 if neon::handles_lanes(N) {
159 neon::compress_block(cv, block, counter, block_len, flags);
160 return;
161 }
162
163 compress_block_portable(cv, block, counter, block_len, flags);
164}
165
166#[inline(always)]
171fn compress_block_portable<const N: usize>(
172 cv: &mut [[u32; N]; 8],
173 block: &[[u32; N]; 16],
174 counter: u64,
175 block_len: u32,
176 flags: u32,
177) {
178 let counter_lo = counter as u32;
180 let counter_hi = (counter >> 32) as u32;
181
182 let mut v: [[u32; N]; 16] = [
184 cv[0],
185 cv[1],
186 cv[2],
187 cv[3],
188 cv[4],
189 cv[5],
190 cv[6],
191 cv[7],
192 [IV[0]; N],
193 [IV[1]; N],
194 [IV[2]; N],
195 [IV[3]; N],
196 [counter_lo; N],
197 [counter_hi; N],
198 [block_len; N],
199 [flags; N],
200 ];
201
202 round::<0, N>(&mut v, block);
205 round::<1, N>(&mut v, block);
206 round::<2, N>(&mut v, block);
207 round::<3, N>(&mut v, block);
208 round::<4, N>(&mut v, block);
209 round::<5, N>(&mut v, block);
210 round::<6, N>(&mut v, block);
211
212 for i in 0..8 {
214 for lane in 0..N {
215 cv[i][lane] = v[i][lane] ^ v[i + 8][lane];
216 }
217 }
218}
219
220#[inline(always)]
222fn broadcast_iv<const N: usize>() -> [[u32; N]; 8] {
223 array::from_fn(|w| [IV[w]; N])
224}
225
226#[inline(always)]
228fn serialize_cv_lane<const N: usize>(cv: &[[u32; N]; 8], lane: usize) -> [u8; OUT_LEN] {
229 let mut digest = [0u8; OUT_LEN];
230 for (w, chunk) in digest.chunks_exact_mut(4).enumerate() {
231 chunk.copy_from_slice(&cv[w][lane].to_le_bytes());
232 }
233 digest
234}
235
236#[derive(Clone)]
244pub struct PortableBlake3MultiDigest<const N: usize> {
245 cv: [[u32; N]; 8],
247 block: [[u8; BLOCK_LEN]; N],
249 block_len: usize,
251 blocks_compressed: usize,
253}
254
255impl<const N: usize> Default for PortableBlake3MultiDigest<N> {
256 fn default() -> Self {
257 Self {
259 cv: broadcast_iv(),
260 block: [[0u8; BLOCK_LEN]; N],
261 block_len: 0,
262 blocks_compressed: 0,
263 }
264 }
265}
266
267impl<const N: usize> PortableBlake3MultiDigest<N> {
268 fn compress_full_block(&mut self) {
270 let flags = if self.blocks_compressed == 0 {
272 CHUNK_START
273 } else {
274 0
275 };
276 let m = load_block_words(&self.block);
277 compress_block(&mut self.cv, &m, 0, BLOCK_LEN as u32, flags);
278 self.blocks_compressed += 1;
279 self.block_len = 0;
280 }
281
282 fn write_root(&self, out: &mut [MaybeUninit<Output<blake3::Hasher>>; N]) {
286 let mut cv = self.cv;
287 let mut block = self.block;
288 for lane in 0..N {
290 block[lane][self.block_len..].fill(0);
291 }
292 let start = if self.blocks_compressed == 0 {
294 CHUNK_START
295 } else {
296 0
297 };
298 let m = load_block_words(&block);
299 compress_block(&mut cv, &m, 0, self.block_len as u32, start | CHUNK_END | ROOT);
300
301 for lane in 0..N {
303 out[lane].write(serialize_cv_lane(&cv, lane).into());
304 }
305 }
306}
307
308impl<const N: usize> MultiDigest<N> for PortableBlake3MultiDigest<N> {
309 type Digest = blake3::Hasher;
310
311 fn new() -> Self {
312 Self::default()
313 }
314
315 fn update(&mut self, data: [&[u8]; N]) {
316 let mut consumed = [0usize; N];
318 loop {
319 let remaining = (0..N)
321 .map(|i| data[i].len() - consumed[i])
322 .max()
323 .unwrap_or(0);
324 if remaining == 0 {
325 break;
326 }
327 if self.block_len == BLOCK_LEN {
329 self.compress_full_block();
330 }
331 let take = (BLOCK_LEN - self.block_len).min(remaining);
333 for lane in 0..N {
334 let avail = data[lane].len() - consumed[lane];
335 let n = take.min(avail);
336 self.block[lane][self.block_len..self.block_len + n]
337 .copy_from_slice(&data[lane][consumed[lane]..consumed[lane] + n]);
338 consumed[lane] += n;
339 }
340 self.block_len += take;
341 }
342 }
343
344 fn finalize_into(self, out: &mut [MaybeUninit<Output<Self::Digest>>; N]) {
345 self.write_root(out);
346 }
347
348 fn finalize_into_reset(&mut self, out: &mut [MaybeUninit<Output<Self::Digest>>; N]) {
349 self.write_root(out);
350 self.reset();
351 }
352
353 fn reset(&mut self) {
354 self.cv = broadcast_iv();
357 self.block_len = 0;
358 self.blocks_compressed = 0;
359 }
360
361 fn digest(data: [&[u8]; N], out: &mut [MaybeUninit<Output<Self::Digest>>; N]) {
362 let mut hasher = Self::new();
363 hasher.update(data);
364 hasher.finalize_into(out);
365 }
366}
367
368#[derive(Debug, Clone, Default)]
375pub struct PortableBlake3ParallelDigest<const LANES: usize>;
376
377impl<const LANES: usize> ParallelDigest for PortableBlake3ParallelDigest<LANES> {
378 type Digest = blake3::Hasher;
379
380 fn new() -> Self {
381 Self
382 }
383
384 fn digest<I: IntoIterator<Item: SerializeBytes>>(
385 &self,
386 source: impl IndexedParallelIterator<Item = I>,
387 out: &mut [MaybeUninit<Output<Self::Digest>>],
388 ) {
389 ParallelDigestAdapter::<blake3::Hasher>::new().digest(source, out);
392 }
393
394 fn digest_with_const_len<I: IntoIterator<Item: FixedSizeSerializeBytes>>(
395 &self,
396 n_items_per_input: usize,
397 source: impl IndexedParallelIterator<Item = I>,
398 out: &mut [MaybeUninit<Output<Self::Digest>>],
399 ) {
400 let leaf_len = n_items_per_input * I::Item::BYTE_SIZE;
402
403 if leaf_len <= CHUNK_LEN {
404 ParallelMultidigestImpl::<PortableBlake3MultiDigest<LANES>, LANES>::new()
406 .digest(source, out);
407 } else {
408 ParallelDigestAdapter::<blake3::Hasher>::new().digest(source, out);
410 }
411 }
412}
413
414#[inline]
428fn compress_node_pairs<const N: usize>(
429 inputs: &[Output<blake3::Hasher>],
430 out: &mut [MaybeUninit<Output<blake3::Hasher>>],
431) {
432 let mut blocks = [[0u8; BLOCK_LEN]; N];
434 for (lane, block) in blocks.iter_mut().enumerate().take(out.len()) {
435 block[..OUT_LEN].copy_from_slice(inputs[2 * lane].as_slice());
436 block[OUT_LEN..].copy_from_slice(inputs[2 * lane + 1].as_slice());
437 }
438
439 let m = load_block_words(&blocks);
441 let mut cv = broadcast_iv::<N>();
442 compress_block(&mut cv, &m, 0, BLOCK_LEN as u32, CHUNK_START | CHUNK_END | ROOT);
443
444 for (lane, slot) in out.iter_mut().enumerate() {
445 slot.write(serialize_cv_lane(&cv, lane).into());
446 }
447}
448
449#[derive(Debug, Clone, Default)]
458pub struct PortableBlake3ParallelCompression<const LANES: usize> {
459 compression: Blake3Compression,
461}
462
463impl<const LANES: usize> ParallelPseudoCompression<Output<blake3::Hasher>, 2>
464 for PortableBlake3ParallelCompression<LANES>
465{
466 type Compression = Blake3Compression;
467
468 fn compression(&self) -> &Self::Compression {
469 &self.compression
470 }
471
472 fn parallel_compress(
473 &self,
474 inputs: &[Output<blake3::Hasher>],
475 out: &mut [MaybeUninit<Output<blake3::Hasher>>],
476 ) {
477 assert_eq!(inputs.len(), 2 * out.len(), "Input length must be 2 * output length");
478
479 inputs
482 .par_chunks(2 * LANES)
483 .zip(out.par_chunks_mut(LANES))
484 .for_each(|(in_batch, out_batch)| compress_node_pairs::<LANES>(in_batch, out_batch));
485 }
486}
487
488#[cfg(test)]
489mod tests {
490 use std::iter::repeat_with;
491
492 use binius_hash::CompressionFunction;
493 use binius_utils::rayon::iter::{IntoParallelRefIterator, ParallelIterator};
494 use proptest::prelude::*;
495 use rand::{Rng, SeedableRng, rngs::StdRng};
496
497 use super::*;
498
499 fn check_parallel_compression<const N: usize>(pairs: &[[[u8; OUT_LEN]; 2]]) {
502 let inputs: Vec<Output<blake3::Hasher>> = pairs
504 .iter()
505 .flat_map(|[l, r]| [(*l).into(), (*r).into()])
506 .collect();
507 let mut out = repeat_with(MaybeUninit::<Output<blake3::Hasher>>::uninit)
508 .take(pairs.len())
509 .collect::<Vec<_>>();
510
511 PortableBlake3ParallelCompression::<N>::default().parallel_compress(&inputs, &mut out);
512
513 for (slot, [l, r]) in out.into_iter().zip(pairs) {
515 let expected = Blake3Compression.compress([(*l).into(), (*r).into()]);
516 assert_eq!(unsafe { slot.assume_init() }.as_slice(), expected.as_slice());
517 }
518 }
519
520 #[test]
521 fn test_parallel_compression_boundaries() {
522 let zero = [0u8; OUT_LEN];
524 let ones = [0xffu8; OUT_LEN];
525 check_parallel_compression::<16>(&[[zero, zero], [ones, ones], [zero, ones], [ones, zero]]);
526
527 let mut rng = StdRng::seed_from_u64(7);
529 for count in [0usize, 1, 15, 16, 17, 33] {
530 let pairs: Vec<[[u8; OUT_LEN]; 2]> = (0..count)
531 .map(|_| {
532 let mut pair = [[0u8; OUT_LEN]; 2];
533 rng.fill_bytes(&mut pair[0]);
534 rng.fill_bytes(&mut pair[1]);
535 pair
536 })
537 .collect();
538 check_parallel_compression::<16>(&pairs);
539 }
540 }
541
542 proptest! {
543 #[test]
544 fn parallel_compression_matches_scalar(
545 pairs in prop::collection::vec(
546 (prop::array::uniform32(any::<u8>()), prop::array::uniform32(any::<u8>())),
547 0..40usize,
548 ),
549 ) {
550 let pairs: Vec<[[u8; OUT_LEN]; 2]> = pairs.into_iter().map(|(l, r)| [l, r]).collect();
552 check_parallel_compression::<4>(&pairs);
553 check_parallel_compression::<8>(&pairs);
554 check_parallel_compression::<16>(&pairs);
555 }
556 }
557
558 #[cfg(all(target_arch = "x86_64", target_feature = "avx512f"))]
565 fn check_avx512_transpose<const N: usize>(rng: &mut StdRng) {
566 use rand::RngExt;
567
568 let block: [[u8; BLOCK_LEN]; N] = array::from_fn(|_| array::from_fn(|_| rng.random()));
571
572 let want = load_block_words_portable(&block);
574
575 let got = super::avx512::load_block_words(&block);
577
578 assert_eq!(got, want, "vector transpose diverged from the byte-wise loader at {N} lanes");
580 }
581
582 #[cfg(all(target_arch = "x86_64", target_feature = "avx512f"))]
583 #[test]
584 fn test_avx512_transpose_matches_portable() {
585 let mut rng = StdRng::seed_from_u64(13);
586
587 for _ in 0..64 {
589 check_avx512_transpose::<16>(&mut rng);
590 }
591 }
592
593 #[cfg(all(target_arch = "x86_64", target_feature = "avx512f"))]
594 #[test]
595 fn test_avx512_transpose_places_every_word() {
596 let block: [[u8; BLOCK_LEN]; 16] = array::from_fn(|lane| {
599 array::from_fn(|byte| {
600 let word = byte / 4;
601 match byte % 4 {
603 0 => (lane * 16 + word) as u8,
604 _ => 0,
605 }
606 })
607 });
608
609 let m = super::avx512::load_block_words(&block);
610
611 for lane in 0..16 {
613 for word in 0..16 {
614 assert_eq!(
615 m[word][lane],
616 (lane * 16 + word) as u32,
617 "word {word} of lane {lane} landed wrongly"
618 );
619 }
620 }
621 }
622
623 #[cfg(all(target_arch = "x86_64", target_feature = "avx512f"))]
624 proptest! {
625 #[test]
626 fn avx512_transpose_matches_portable_proptest(seed in any::<u64>()) {
627 let mut rng = StdRng::seed_from_u64(seed);
629 check_avx512_transpose::<16>(&mut rng);
630 }
631 }
632
633 #[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
642 fn check_neon_core<const N: usize>(rng: &mut StdRng, counter: u64, block_len: u32, flags: u32) {
643 use rand::RngExt;
644
645 let cv_in: [[u32; N]; 8] = array::from_fn(|_| array::from_fn(|_| rng.random()));
648 let block: [[u32; N]; 16] = array::from_fn(|_| array::from_fn(|_| rng.random()));
649
650 let mut want = cv_in;
652 compress_block_portable(&mut want, &block, counter, block_len, flags);
653
654 let mut got = cv_in;
656 super::neon::compress_block(&mut got, &block, counter, block_len, flags);
657
658 assert_eq!(got, want, "vector kernel diverged from the lane loops at {N} lanes");
660 }
661
662 #[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
664 fn check_neon_transpose<const N: usize>(rng: &mut StdRng) {
665 let mut blocks = [[0u8; BLOCK_LEN]; N];
667 for block in blocks.iter_mut() {
668 rng.fill_bytes(block);
669 }
670
671 assert_eq!(
673 super::neon::load_block_words(&blocks),
674 load_block_words_portable(&blocks),
675 "the shuffle network diverged from the byte-wise loader at {N} lanes"
676 );
677 }
678
679 #[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
680 #[test]
681 fn test_neon_transpose_matches_portable() {
682 let mut blocks = [[0u8; BLOCK_LEN]; 16];
693 for (lane, block) in blocks.iter_mut().enumerate() {
694 for (w, word) in block.chunks_exact_mut(4).enumerate() {
695 word.copy_from_slice(&((lane * 16 + w) as u32).to_le_bytes());
696 }
697 }
698
699 let m = super::neon::load_block_words(&blocks);
701 for (w, row) in m.iter().enumerate() {
702 for (lane, got) in row.iter().enumerate() {
703 assert_eq!(*got, (lane * 16 + w) as u32, "row {w}, lane {lane}");
704 }
705 }
706
707 let mut rng = StdRng::seed_from_u64(23);
709 check_neon_transpose::<4>(&mut rng);
710 check_neon_transpose::<8>(&mut rng);
711 check_neon_transpose::<12>(&mut rng);
712 check_neon_transpose::<16>(&mut rng);
713 }
714
715 #[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
716 proptest! {
717 #[test]
718 fn neon_transpose_matches_portable_proptest(seed in any::<u64>()) {
719 let mut rng = StdRng::seed_from_u64(seed);
722 check_neon_transpose::<4>(&mut rng);
723 check_neon_transpose::<8>(&mut rng);
724 check_neon_transpose::<12>(&mut rng);
725 check_neon_transpose::<16>(&mut rng);
726 }
727 }
728
729 #[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
730 #[test]
731 fn test_neon_core_matches_portable() {
732 let mut rng = StdRng::seed_from_u64(11);
733
734 let cases = [
739 (0u64, 64u32, 0u32),
741 (0, 64, CHUNK_START),
743 (0, 64, CHUNK_END | ROOT),
745 (0, 64, CHUNK_START | CHUNK_END | ROOT),
747 (0, 1, CHUNK_START | CHUNK_END | ROOT),
749 (0, 0, CHUNK_START | CHUNK_END | ROOT),
751 (u64::MAX, 64, CHUNK_END),
753 (1 << 32, 63, CHUNK_START),
755 ];
756
757 for (counter, block_len, flags) in cases {
759 check_neon_core::<4>(&mut rng, counter, block_len, flags);
760 check_neon_core::<8>(&mut rng, counter, block_len, flags);
761 check_neon_core::<12>(&mut rng, counter, block_len, flags);
762 check_neon_core::<16>(&mut rng, counter, block_len, flags);
763 }
764 }
765
766 #[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
767 proptest! {
768 #[test]
769 fn neon_core_matches_portable_proptest(
770 seed in any::<u64>(),
771 counter in any::<u64>(),
772 block_len in 0..=64u32,
774 flags in any::<u8>(),
776 ) {
777 let mut rng = StdRng::seed_from_u64(seed);
779 check_neon_core::<4>(&mut rng, counter, block_len, flags as u32);
780 check_neon_core::<8>(&mut rng, counter, block_len, flags as u32);
781 check_neon_core::<12>(&mut rng, counter, block_len, flags as u32);
782 check_neon_core::<16>(&mut rng, counter, block_len, flags as u32);
783 }
784 }
785
786 fn check_portable_batch<const N: usize>(rng: &mut StdRng, len: usize) {
789 let messages: [Vec<u8>; N] = array::from_fn(|_| {
791 let mut m = vec![0u8; len];
792 rng.fill_bytes(&mut m);
793 m
794 });
795 let refs: [&[u8]; N] = array::from_fn(|i| messages[i].as_slice());
796 let mut out = array::from_fn::<_, N, _>(|_| MaybeUninit::uninit());
797 PortableBlake3MultiDigest::<N>::digest(refs, &mut out);
798
799 for (o, message) in out.iter().zip(messages.iter()) {
801 let got = unsafe { o.assume_init_ref() };
802 assert_eq!(got.as_slice(), blake3::hash(message).as_bytes(), "len = {len}, N = {N}");
803 }
804 }
805
806 #[test]
807 fn test_portable_lengths_match_reference() {
808 let mut rng = StdRng::seed_from_u64(0);
809
810 for len in [0, 1, 31, 63, 64, 65, 100, 127, 128, 1000, 1024] {
818 check_portable_batch::<4>(&mut rng, len);
819 check_portable_batch::<8>(&mut rng, len);
820 check_portable_batch::<16>(&mut rng, len);
821 }
822 }
823
824 #[test]
825 fn test_portable_chained_update() {
826 let mut rng = StdRng::seed_from_u64(2);
827 let messages: [Vec<u8>; 4] = array::from_fn(|_| {
829 let mut m = vec![0u8; 200];
830 rng.fill_bytes(&mut m);
831 m
832 });
833
834 let mut hasher = PortableBlake3MultiDigest::<4>::new();
837 hasher.update(array::from_fn(|i| &messages[i][..50]));
838 hasher.update(array::from_fn(|i| &messages[i][50..]));
839 let mut out = array::from_fn::<_, 4, _>(|_| MaybeUninit::uninit());
840 hasher.finalize_into(&mut out);
841
842 for (o, message) in out.iter().zip(messages.iter()) {
843 assert_eq!(unsafe { o.assume_init_ref() }.as_slice(), blake3::hash(message).as_bytes());
844 }
845 }
846
847 #[test]
848 fn test_portable_routing_matches_reference() {
849 let mut rng = StdRng::seed_from_u64(3);
850 let mut check = |leaf_len: usize| {
852 let leaves: Vec<Vec<u8>> = (0..50)
853 .map(|_| {
854 let mut m = vec![0u8; leaf_len];
855 rng.fill_bytes(&mut m);
856 m
857 })
858 .collect();
859 let digest = PortableBlake3ParallelDigest::<8>::new();
860 let mut results = repeat_with(MaybeUninit::<Output<blake3::Hasher>>::uninit)
861 .take(50)
862 .collect::<Vec<_>>();
863 digest.digest_with_const_len(
864 leaf_len,
865 leaves.par_iter().map(|leaf| leaf.iter().copied()),
866 &mut results,
867 );
868 for (result, leaf) in results.into_iter().zip(&leaves) {
869 let got = unsafe { result.assume_init() };
870 assert_eq!(got.as_slice(), blake3::hash(leaf).as_bytes(), "leaf_len {leaf_len}");
871 }
872 };
873
874 for leaf_len in [0, 1, 63, 64, 65, 100, 1000, 1024, 1025, 2048] {
880 check(leaf_len);
881 }
882 }
883}