1use std::{array, iter};
18
19use binius_core::word::Word;
20use binius_frontend::{ChipGadget, CircuitBuilder, Hint, Wire};
21
22use super::{IV, MSG_SCHEDULE};
23use crate::util::clear_high_bits;
24
25const HIGH_LANE_ROUNDS: usize = 3;
27
28const LOW_LANE_ROUNDS: usize = 7 - HIGH_LANE_ROUNDS;
33
34pub fn blake3_compress(
75 builder: &CircuitBuilder,
76 cv: [Wire; 8],
77 block: [Wire; 16],
78 counter: Wire,
79 block_len: Wire,
80 flags: Wire,
81) -> [Wire; 8] {
82 let mut hint_inputs = Vec::with_capacity(27);
87 hint_inputs.extend_from_slice(&cv);
88 hint_inputs.extend_from_slice(&block);
89 hint_inputs.push(counter);
90 hint_inputs.push(block_len);
91 hint_inputs.push(flags);
92 let merged = builder.call_hint(Blake3RoundSplitHint, &[], &hint_inputs);
93 let mut v: [Wire; 16] = array::from_fn(|i| merged[i]);
94
95 let up = |w: Wire| builder.shl(w, 32);
99 let iv_up = |i: usize| builder.add_constant(Word((IV[i] as u64) << 32));
100 let counter_high = builder.shl(builder.shr(counter, 32), 32);
103 let init_up: [Wire; 16] = [
104 up(cv[0]),
105 up(cv[1]),
106 up(cv[2]),
107 up(cv[3]),
108 up(cv[4]),
109 up(cv[5]),
110 up(cv[6]),
111 up(cv[7]),
112 iv_up(0),
113 iv_up(1),
114 iv_up(2),
115 iv_up(3),
116 up(counter),
119 counter_high,
120 up(block_len),
121 up(flags),
122 ];
123
124 let msg_low: [Wire; 16] = array::from_fn(|i| clear_high_bits(builder, block[i], 32));
128 let msg_high: [Wire; 16] = array::from_fn(|i| up(block[i]));
129
130 for step in 0..LOW_LANE_ROUNDS {
131 let high_schedule = MSG_SCHEDULE[step];
135 let low_schedule = MSG_SCHEDULE[step + HIGH_LANE_ROUNDS];
136 let msg: [Wire; 16] =
137 array::from_fn(|k| builder.bxor(msg_low[low_schedule[k]], msg_high[high_schedule[k]]));
138 round(builder, &mut v, &msg);
139
140 if step + 1 == HIGH_LANE_ROUNDS {
151 for (hinted, (init, split)) in iter::zip(&merged, iter::zip(init_up, v)) {
152 let expected = builder.bxor(init, builder.shr(split, 32));
153 builder.assert_eq("blake3_compress.split_state", *hinted, expected);
154 }
155 }
156 }
157
158 array::from_fn(|i| builder.bxor(v[i], v[i + 8]))
161}
162
163pub fn blake3_compress_2x(
196 builder: &CircuitBuilder,
197 cv: [Wire; 8],
198 block: [Wire; 16],
199 counter_lo: Wire,
200 counter_hi: Wire,
201 block_len: Wire,
202 flags: Wire,
203) -> [Wire; 8] {
204 let inputs = cv
205 .into_iter()
206 .chain(block)
207 .chain([counter_lo, counter_hi, block_len, flags])
208 .collect::<Vec<_>>();
209
210 let outputs = builder.build_gadget(Blake3Compress2x, &[], &inputs);
211 array::from_fn(|i| outputs[i])
212}
213
214pub struct Blake3Compress2x;
220
221impl Hint for Blake3Compress2x {
222 const NAME: &'static str = "binius.blake3_compress_2x";
223
224 fn shape(&self, _dimensions: &[usize]) -> (usize, usize) {
225 (28, 8)
226 }
227
228 fn execute(&self, _dimensions: &[usize], inputs: &[Word], outputs: &mut [Word]) {
229 let compress_lane = |i: usize| {
233 let lane = |word: Word| (word.as_u64() >> (32 * i)) as u32;
234 let cv: [u32; 8] = array::from_fn(|j| lane(inputs[j]));
235 let block: [u32; 16] = array::from_fn(|j| lane(inputs[8 + j]));
236 let counter = lane(inputs[24]) as u64 | ((lane(inputs[25]) as u64) << 32);
237 ref_compress(&cv, &block, counter, lane(inputs[26]), lane(inputs[27]))
238 };
239
240 let (lane_0, lane_1) = (compress_lane(0), compress_lane(1));
241 for (slot, (low, high)) in iter::zip(outputs, iter::zip(lane_0, lane_1)) {
242 *slot = Word(low as u64 | ((high as u64) << 32));
243 }
244 }
245}
246
247impl ChipGadget for Blake3Compress2x {
248 fn build(&self, builder: &CircuitBuilder, _dimensions: &[usize], inputs: &[Wire]) -> Vec<Wire> {
249 let cv: [Wire; 8] = array::from_fn(|i| inputs[i]);
250 let block: [Wire; 16] = array::from_fn(|i| inputs[8 + i]);
251 compress_2x_gates(builder, cv, block, inputs[24], inputs[25], inputs[26], inputs[27])
252 .to_vec()
253 }
254}
255
256fn compress_2x_gates(
258 builder: &CircuitBuilder,
259 cv: [Wire; 8],
260 block: [Wire; 16],
261 counter_lo: Wire,
262 counter_hi: Wire,
263 block_len: Wire,
264 flags: Wire,
265) -> [Wire; 8] {
266 let iv_2x = |i: usize| {
268 let w = IV[i] as u64;
269 builder.add_constant(Word(w | (w << 32)))
270 };
271
272 let v: [Wire; 16] = [
273 cv[0],
274 cv[1],
275 cv[2],
276 cv[3],
277 cv[4],
278 cv[5],
279 cv[6],
280 cv[7],
281 iv_2x(0),
282 iv_2x(1),
283 iv_2x(2),
284 iv_2x(3),
285 counter_lo,
286 counter_hi,
287 block_len,
288 flags,
289 ];
290
291 compress_core(builder, v, block)
292}
293
294fn compress_core(builder: &CircuitBuilder, mut v: [Wire; 16], block: [Wire; 16]) -> [Wire; 8] {
300 for i in 0..7 {
301 let schedule = MSG_SCHEDULE[i];
302 round(builder, &mut v, &array::from_fn(|k| block[schedule[k]]));
303 }
304 array::from_fn(|i| builder.bxor(v[i], v[i + 8]))
305}
306
307#[allow(clippy::too_many_arguments)]
309fn g(
310 builder: &CircuitBuilder,
311 v: &mut [Wire; 16],
312 a: usize,
313 b: usize,
314 c: usize,
315 d: usize,
316 x: Wire,
317 y: Wire,
318) {
319 v[a] = builder.iadd_32(builder.iadd_32(v[a], v[b]), x);
320 v[d] = builder.rotr32(builder.bxor(v[d], v[a]), 16);
321 v[c] = builder.iadd_32(v[c], v[d]);
322 v[b] = builder.rotr32(builder.bxor(v[b], v[c]), 12);
323 v[a] = builder.iadd_32(builder.iadd_32(v[a], v[b]), y);
324 v[d] = builder.rotr32(builder.bxor(v[d], v[a]), 8);
325 v[c] = builder.iadd_32(v[c], v[d]);
326 v[b] = builder.rotr32(builder.bxor(v[b], v[c]), 7);
327}
328
329fn round(builder: &CircuitBuilder, state: &mut [Wire; 16], msg: &[Wire; 16]) {
334 g(builder, state, 0, 4, 8, 12, msg[0], msg[1]);
336 g(builder, state, 1, 5, 9, 13, msg[2], msg[3]);
337 g(builder, state, 2, 6, 10, 14, msg[4], msg[5]);
338 g(builder, state, 3, 7, 11, 15, msg[6], msg[7]);
339
340 g(builder, state, 0, 5, 10, 15, msg[8], msg[9]);
342 g(builder, state, 1, 6, 11, 12, msg[10], msg[11]);
343 g(builder, state, 2, 7, 8, 13, msg[12], msg[13]);
344 g(builder, state, 3, 4, 9, 14, msg[14], msg[15]);
345}
346
347pub fn blake3_compress_2x_seq(
379 builder: &CircuitBuilder,
380 cv: [Wire; 8],
381 blocks: [[Wire; 16]; 2],
382 counter: Wire,
383 block_lens: [Wire; 2],
384 flags: [Wire; 2],
385) -> [Wire; 8] {
386 let mut hint_inputs = Vec::with_capacity(27);
391 hint_inputs.extend_from_slice(&cv);
392 hint_inputs.extend_from_slice(&blocks[0]);
393 hint_inputs.push(counter);
394 hint_inputs.push(block_lens[0]);
395 hint_inputs.push(flags[0]);
396 let merged_cv_vec = builder.call_hint(Blake3CompressHint, &[], &hint_inputs);
397 let merged_cv: [Wire; 8] = array::from_fn(|i| merged_cv_vec[i]);
398
399 let pack = |lo: Wire, hi: Wire| builder.bxor(lo, builder.shl(hi, 32));
403 let clear = |w: Wire| clear_high_bits(builder, w, 32);
404
405 let merged_block: [Wire; 16] = array::from_fn(|i| pack(clear(blocks[1][i]), blocks[0][i]));
406
407 let counter_lo = clear(counter);
411 let counter_hi = builder.shr(counter, 32);
412 let merged_counter_lo = pack(counter_lo, counter_lo);
413 let merged_counter_hi = pack(counter_hi, counter_hi);
414 let merged_block_len = pack(clear(block_lens[1]), block_lens[0]);
415 let merged_flags = pack(clear(flags[1]), flags[0]);
416
417 let out = blake3_compress_2x(
418 builder,
419 merged_cv,
420 merged_block,
421 merged_counter_lo,
422 merged_counter_hi,
423 merged_block_len,
424 merged_flags,
425 );
426
427 for (merged, (cv_word, out_word)) in iter::zip(merged_cv, iter::zip(cv, out)) {
445 let expected = builder.bxor(builder.shl(cv_word, 32), builder.shr(out_word, 32));
446 builder.assert_eq("blake3_compress_2x_seq.merged_cv", merged, expected);
447 }
448
449 out
450}
451
452struct Blake3RoundSplitHint;
464
465impl Hint for Blake3RoundSplitHint {
466 const NAME: &'static str = "binius.blake3_compress_round_split";
467
468 fn shape(&self, _dimensions: &[usize]) -> (usize, usize) {
469 (27, 16)
470 }
471
472 fn execute(&self, _dimensions: &[usize], inputs: &[Word], outputs: &mut [Word]) {
473 let cv: [u32; 8] = array::from_fn(|i| inputs[i].as_u64() as u32);
474 let block: [u32; 16] = array::from_fn(|i| inputs[8 + i].as_u64() as u32);
475 let counter = inputs[24].as_u64();
476 let block_len = inputs[25].as_u64() as u32;
477 let flags = inputs[26].as_u64() as u32;
478
479 let init = ref_init_state(&cv, counter, block_len, flags);
480 let mut split = init;
481 for i in 0..HIGH_LANE_ROUNDS {
482 ref_round(&mut split, &block, i);
483 }
484 for (i, slot) in outputs.iter_mut().enumerate() {
485 *slot = Word(split[i] as u64 | ((init[i] as u64) << 32));
486 }
487 }
488}
489
490struct Blake3CompressHint;
503
504impl Hint for Blake3CompressHint {
505 const NAME: &'static str = "binius.blake3_compress";
506
507 fn shape(&self, _dimensions: &[usize]) -> (usize, usize) {
508 (27, 8)
509 }
510
511 fn execute(&self, _dimensions: &[usize], inputs: &[Word], outputs: &mut [Word]) {
512 let cv: [u32; 8] = array::from_fn(|i| inputs[i].as_u64() as u32);
513 let block: [u32; 16] = array::from_fn(|i| inputs[8 + i].as_u64() as u32);
514 let counter = inputs[24].as_u64();
515 let block_len = inputs[25].as_u64() as u32;
516 let flags = inputs[26].as_u64() as u32;
517
518 let out = ref_compress(&cv, &block, counter, block_len, flags);
519 for (i, slot) in outputs.iter_mut().enumerate() {
520 *slot = Word(out[i] as u64 | ((cv[i] as u64) << 32));
521 }
522 }
523}
524
525const fn ref_g(v: &mut [u32; 16], a: usize, b: usize, c: usize, d: usize, mx: u32, my: u32) {
530 v[a] = v[a].wrapping_add(v[b]).wrapping_add(mx);
531 v[d] = (v[d] ^ v[a]).rotate_right(16);
532 v[c] = v[c].wrapping_add(v[d]);
533 v[b] = (v[b] ^ v[c]).rotate_right(12);
534 v[a] = v[a].wrapping_add(v[b]).wrapping_add(my);
535 v[d] = (v[d] ^ v[a]).rotate_right(8);
536 v[c] = v[c].wrapping_add(v[d]);
537 v[b] = (v[b] ^ v[c]).rotate_right(7);
538}
539
540const fn ref_round(state: &mut [u32; 16], msg: &[u32; 16], round: usize) {
541 let schedule = MSG_SCHEDULE[round];
542
543 ref_g(state, 0, 4, 8, 12, msg[schedule[0]], msg[schedule[1]]);
544 ref_g(state, 1, 5, 9, 13, msg[schedule[2]], msg[schedule[3]]);
545 ref_g(state, 2, 6, 10, 14, msg[schedule[4]], msg[schedule[5]]);
546 ref_g(state, 3, 7, 11, 15, msg[schedule[6]], msg[schedule[7]]);
547
548 ref_g(state, 0, 5, 10, 15, msg[schedule[8]], msg[schedule[9]]);
549 ref_g(state, 1, 6, 11, 12, msg[schedule[10]], msg[schedule[11]]);
550 ref_g(state, 2, 7, 8, 13, msg[schedule[12]], msg[schedule[13]]);
551 ref_g(state, 3, 4, 9, 14, msg[schedule[14]], msg[schedule[15]]);
552}
553
554const fn ref_init_state(cv: &[u32; 8], counter: u64, block_len: u32, flags: u32) -> [u32; 16] {
556 [
557 cv[0],
558 cv[1],
559 cv[2],
560 cv[3],
561 cv[4],
562 cv[5],
563 cv[6],
564 cv[7],
565 IV[0],
566 IV[1],
567 IV[2],
568 IV[3],
569 counter as u32,
570 (counter >> 32) as u32,
571 block_len,
572 flags,
573 ]
574}
575
576pub fn ref_compress(
581 cv: &[u32; 8],
582 block: &[u32; 16],
583 counter: u64,
584 block_len: u32,
585 flags: u32,
586) -> [u32; 8] {
587 let mut v = ref_init_state(cv, counter, block_len, flags);
588 for i in 0..7 {
589 ref_round(&mut v, block, i);
590 }
591 array::from_fn(|i| v[i] ^ v[i + 8])
592}
593
594#[cfg(test)]
595mod tests {
596 use std::array;
597
598 use binius_frontend::CircuitBuilder;
599 use proptest::prelude::*;
600
601 use super::*;
602 use crate::blake3::{CHUNK_END, CHUNK_START, PARENT, ROOT};
603
604 fn run_compress_2x_words(inputs: [u64; 28]) -> [u64; 8] {
608 let builder = CircuitBuilder::new();
609 let wires: [Wire; 28] = array::from_fn(|_| builder.add_witness());
610 let out = compress_2x_gates(
611 &builder,
612 array::from_fn(|i| wires[i]),
613 array::from_fn(|i| wires[8 + i]),
614 wires[24],
615 wires[25],
616 wires[26],
617 wires[27],
618 );
619 for wire in out {
620 builder.mark_inout(wire);
621 }
622
623 let circuit = builder.build();
624 let mut w = circuit.new_witness_filler();
625 for (wire, word) in iter::zip(wires, inputs) {
626 w[wire] = Word(word);
627 }
628 circuit.populate_wire_witness(&mut w).unwrap();
629
630 array::from_fn(|i| w[out[i]].as_u64())
631 }
632
633 fn run_compress(
636 cv: [u32; 8],
637 block: [u32; 16],
638 counter: u64,
639 block_len: u32,
640 flags: u32,
641 ) -> [u32; 8] {
642 run_compress_with_dirt(cv, block, counter, block_len, flags, 0)
643 }
644
645 fn run_compress_with_dirt(
650 cv: [u32; 8],
651 block: [u32; 16],
652 counter: u64,
653 block_len: u32,
654 flags: u32,
655 dirt: u32,
656 ) -> [u32; 8] {
657 let dirt = (dirt as u64) << 32;
658 let builder = CircuitBuilder::new();
659 let cv_wires: [Wire; 8] = array::from_fn(|_| builder.add_witness());
660 let block_wires: [Wire; 16] = array::from_fn(|_| builder.add_witness());
661 let counter_w = builder.add_witness();
662 let block_len_w = builder.add_witness();
663 let flags_w = builder.add_witness();
664
665 let out = blake3_compress(&builder, cv_wires, block_wires, counter_w, block_len_w, flags_w);
668 let out = out.map(|word| clear_high_bits(&builder, word, 32));
669 let out_inout: [Wire; 8] = array::from_fn(|_| builder.add_inout());
670 for i in 0..8 {
671 builder.assert_eq("out_match", out[i], out_inout[i]);
672 }
673
674 let circuit = builder.build();
675 let mut w = circuit.new_witness_filler();
676 for i in 0..8 {
677 w[cv_wires[i]] = Word(cv[i] as u64 | dirt);
678 }
679 for i in 0..16 {
680 w[block_wires[i]] = Word(block[i] as u64 | dirt);
681 }
682 w[counter_w] = Word(counter);
684 w[block_len_w] = Word(block_len as u64 | dirt);
685 w[flags_w] = Word(flags as u64 | dirt);
686
687 let expected = ref_compress(&cv, &block, counter, block_len, flags);
688 for i in 0..8 {
689 w[out_inout[i]] = Word(expected[i] as u64);
690 }
691 circuit.populate_wire_witness(&mut w).unwrap();
692 array::from_fn(|i| w[out_inout[i]].0 as u32)
693 }
694
695 #[test]
696 fn zero_block_chunk_start_end_root() {
697 let cv = IV;
698 let block = [0u32; 16];
699 let flags = super::super::CHUNK_START | super::super::CHUNK_END | super::super::ROOT;
700 let actual = run_compress(cv, block, 0, 0, flags);
701 let expected = ref_compress(&cv, &block, 0, 0, flags);
702 assert_eq!(actual, expected);
703 }
704
705 #[test]
706 fn all_ones_block() {
707 let cv = IV;
708 let block = [0xFFFF_FFFFu32; 16];
709 let actual = run_compress(cv, block, 0, 64, 0);
710 let expected = ref_compress(&cv, &block, 0, 64, 0);
711 assert_eq!(actual, expected);
712 }
713
714 #[test]
715 fn nonzero_counter_splits_correctly() {
716 let cv = IV;
717 let block = array::from_fn(|i| i as u32 * 0x0101_0101);
718 let counter: u64 = 0x0123_4567_89AB_CDEF;
719 let actual = run_compress(cv, block, counter, 64, super::super::CHUNK_END);
720 let expected = ref_compress(&cv, &block, counter, 64, super::super::CHUNK_END);
721 assert_eq!(actual, expected);
722 }
723
724 #[test]
725 fn nontrivial_cv() {
726 let cv = [
727 0xDEAD_BEEF,
728 0xCAFE_BABE,
729 0x1234_5678,
730 0x9ABC_DEF0,
731 0x0BAD_F00D,
732 0xFEED_FACE,
733 0x0123_4567,
734 0x89AB_CDEF,
735 ];
736 let block = array::from_fn(|i| (i as u32).wrapping_mul(0xDEAD_BEEFu32));
737 let actual = run_compress(cv, block, 42, 32, super::super::CHUNK_START);
738 let expected = ref_compress(&cv, &block, 42, 32, super::super::CHUNK_START);
739 assert_eq!(actual, expected);
740 }
741
742 #[test]
743 fn compress_ignores_dirty_input_high_halves() {
744 let cv = [
750 0xDEAD_BEEF,
751 0xCAFE_BABE,
752 0x1234_5678,
753 0x9ABC_DEF0,
754 0x0BAD_F00D,
755 0xFEED_FACE,
756 0x0123_4567,
757 0x89AB_CDEF,
758 ];
759 let block: [u32; 16] = array::from_fn(|i| (i as u32).wrapping_mul(0xDEAD_BEEFu32));
760 let counter: u64 = 0x0123_4567_89AB_CDEF;
761 let expected = ref_compress(&cv, &block, counter, 64, CHUNK_END);
762 for dirt in [1, 0xFFFF_FFFF, 0x8000_0000] {
763 let actual = run_compress_with_dirt(cv, block, counter, 64, CHUNK_END, dirt);
764 assert_eq!(actual, expected, "dirt {dirt:#x} changed the result");
765 }
766 }
767
768 fn pack2x(lo: u32, hi: u32) -> u64 {
771 (lo as u64) | ((hi as u64) << 32)
772 }
773
774 fn unpack2x(w: u64) -> (u32, u32) {
775 (w as u32, (w >> 32) as u32)
776 }
777
778 fn run_compress_2x(
781 cv: [[u32; 8]; 2],
782 block: [[u32; 16]; 2],
783 counter: [u64; 2],
784 block_len: [u32; 2],
785 flags: [u32; 2],
786 ) -> [[u32; 8]; 2] {
787 let builder = CircuitBuilder::new();
788 let cv_wires: [Wire; 8] = array::from_fn(|_| builder.add_witness());
789 let block_wires: [Wire; 16] = array::from_fn(|_| builder.add_witness());
790 let counter_lo_w = builder.add_witness();
791 let counter_hi_w = builder.add_witness();
792 let block_len_w = builder.add_witness();
793 let flags_w = builder.add_witness();
794
795 let out = blake3_compress_2x(
796 &builder,
797 cv_wires,
798 block_wires,
799 counter_lo_w,
800 counter_hi_w,
801 block_len_w,
802 flags_w,
803 );
804 let out_inout: [Wire; 8] = array::from_fn(|_| builder.add_inout());
805 for i in 0..8 {
806 builder.assert_eq("out_match_2x", out[i], out_inout[i]);
807 }
808
809 let circuit = builder.build();
810 let mut w = circuit.new_witness_filler();
811 for i in 0..8 {
812 w[cv_wires[i]] = Word(pack2x(cv[0][i], cv[1][i]));
813 }
814 for i in 0..16 {
815 w[block_wires[i]] = Word(pack2x(block[0][i], block[1][i]));
816 }
817 w[counter_lo_w] = Word(pack2x(counter[0] as u32, counter[1] as u32));
818 w[counter_hi_w] = Word(pack2x((counter[0] >> 32) as u32, (counter[1] >> 32) as u32));
819 w[block_len_w] = Word(pack2x(block_len[0], block_len[1]));
820 w[flags_w] = Word(pack2x(flags[0], flags[1]));
821
822 let exp0 = ref_compress(&cv[0], &block[0], counter[0], block_len[0], flags[0]);
823 let exp1 = ref_compress(&cv[1], &block[1], counter[1], block_len[1], flags[1]);
824 for i in 0..8 {
825 w[out_inout[i]] = Word(pack2x(exp0[i], exp1[i]));
826 }
827 circuit.populate_wire_witness(&mut w).unwrap();
828
829 let mut actual = [[0u32; 8]; 2];
830 for i in 0..8 {
831 let (lo, hi) = unpack2x(w[out_inout[i]].0);
832 actual[0][i] = lo;
833 actual[1][i] = hi;
834 }
835 actual
836 }
837
838 #[test]
839 fn compress_2x_identical_lanes() {
840 let cv = IV;
841 let block = [0u32; 16];
842 let flags = super::super::CHUNK_START | super::super::CHUNK_END | super::super::ROOT;
843 let actual = run_compress_2x([cv, cv], [block, block], [0, 0], [0, 0], [flags, flags]);
844 let expected = ref_compress(&cv, &block, 0, 0, flags);
845 assert_eq!(actual[0], expected);
846 assert_eq!(actual[1], expected);
847 }
848
849 #[test]
850 fn compress_2x_distinct_lanes() {
851 let cv0 = IV;
852 let cv1 = [
853 0xDEAD_BEEF,
854 0xCAFE_BABE,
855 0x1234_5678,
856 0x9ABC_DEF0,
857 0x0BAD_F00D,
858 0xFEED_FACE,
859 0x0123_4567,
860 0x89AB_CDEF,
861 ];
862 let block0: [u32; 16] = array::from_fn(|i| i as u32 * 0x0101_0101);
863 let block1: [u32; 16] = array::from_fn(|i| (i as u32).wrapping_mul(0xDEAD_BEEFu32));
864
865 let actual = run_compress_2x(
866 [cv0, cv1],
867 [block0, block1],
868 [0, 42],
869 [64, 32],
870 [super::super::CHUNK_END, super::super::CHUNK_START],
871 );
872 let exp0 = ref_compress(&cv0, &block0, 0, 64, super::super::CHUNK_END);
873 let exp1 = ref_compress(&cv1, &block1, 42, 32, super::super::CHUNK_START);
874 assert_eq!(actual[0], exp0);
875 assert_eq!(actual[1], exp1);
876 }
877
878 #[test]
879 fn compress_2x_counter_across_32bit_boundary() {
880 let cv = IV;
881 let block: [u32; 16] = array::from_fn(|i| i as u32);
882 let counter0: u64 = 0x0123_4567_89AB_CDEF;
883 let counter1: u64 = 0;
884 let actual = run_compress_2x(
885 [cv, cv],
886 [block, block],
887 [counter0, counter1],
888 [64, 64],
889 [
890 super::super::CHUNK_START | super::super::ROOT,
891 super::super::CHUNK_END,
892 ],
893 );
894 let exp0 =
895 ref_compress(&cv, &block, counter0, 64, super::super::CHUNK_START | super::super::ROOT);
896 let exp1 = ref_compress(&cv, &block, counter1, 64, super::super::CHUNK_END);
897 assert_eq!(actual[0], exp0);
898 assert_eq!(actual[1], exp1);
899 }
900
901 #[allow(clippy::too_many_arguments)]
906 fn run_compress_2x_seq(
907 cv: [u32; 8],
908 block1: [u32; 16],
909 block2: [u32; 16],
910 counter: u64,
911 block_len1: u32,
912 flags1: u32,
913 block_len2: u32,
914 flags2: u32,
915 ) -> ([u32; 8], [u32; 8]) {
916 let builder = CircuitBuilder::new();
917 let cv_wires: [Wire; 8] = array::from_fn(|_| builder.add_witness());
918 let block1_wires: [Wire; 16] = array::from_fn(|_| builder.add_witness());
919 let block2_wires: [Wire; 16] = array::from_fn(|_| builder.add_witness());
920 let counter_w = builder.add_witness();
921 let block_len1_w = builder.add_witness();
922 let flags1_w = builder.add_witness();
923 let block_len2_w = builder.add_witness();
924 let flags2_w = builder.add_witness();
925
926 let out = blake3_compress_2x_seq(
927 &builder,
928 cv_wires,
929 [block1_wires, block2_wires],
930 counter_w,
931 [block_len1_w, block_len2_w],
932 [flags1_w, flags2_w],
933 );
934 let out_inout: [Wire; 8] = array::from_fn(|_| builder.add_inout());
935 for i in 0..8 {
936 builder.assert_eq("out_match_2x_seq", out[i], out_inout[i]);
937 }
938
939 let circuit = builder.build();
940 let mut w = circuit.new_witness_filler();
941 for i in 0..8 {
942 w[cv_wires[i]] = Word(cv[i] as u64);
943 }
944 for i in 0..16 {
945 w[block1_wires[i]] = Word(block1[i] as u64);
946 w[block2_wires[i]] = Word(block2[i] as u64);
947 }
948 w[counter_w] = Word(counter);
949 w[block_len1_w] = Word(block_len1 as u64);
950 w[flags1_w] = Word(flags1 as u64);
951 w[block_len2_w] = Word(block_len2 as u64);
952 w[flags2_w] = Word(flags2 as u64);
953
954 let c1 = ref_compress(&cv, &block1, counter, block_len1, flags1);
956 let c2 = ref_compress(&c1, &block2, counter, block_len2, flags2);
957 for i in 0..8 {
958 w[out_inout[i]] = Word(pack2x(c2[i], c1[i]));
959 }
960 circuit.populate_wire_witness(&mut w).unwrap();
961
962 let mut c2_out = [0u32; 8];
963 let mut c1_out = [0u32; 8];
964 for i in 0..8 {
965 let (lo, hi) = unpack2x(w[out_inout[i]].0);
966 c2_out[i] = lo;
967 c1_out[i] = hi;
968 }
969 (c2_out, c1_out)
970 }
971
972 #[test]
973 fn compress_2x_seq_chains_two_blocks() {
974 let cv = IV;
975 let block1 = [0u32; 16];
976 let block2: [u32; 16] = array::from_fn(|i| i as u32);
977 let (c2, c1) = run_compress_2x_seq(
978 cv,
979 block1,
980 block2,
981 0,
982 64,
983 super::super::CHUNK_START,
984 64,
985 super::super::CHUNK_END | super::super::ROOT,
986 );
987 let exp_c1 = ref_compress(&cv, &block1, 0, 64, super::super::CHUNK_START);
988 let exp_c2 =
989 ref_compress(&exp_c1, &block2, 0, 64, super::super::CHUNK_END | super::super::ROOT);
990 assert_eq!(c1, exp_c1);
991 assert_eq!(c2, exp_c2);
992 }
993
994 #[test]
995 fn compress_2x_seq_distinct_params() {
996 let cv = [
997 0xDEAD_BEEF,
998 0xCAFE_BABE,
999 0x1234_5678,
1000 0x9ABC_DEF0,
1001 0x0BAD_F00D,
1002 0xFEED_FACE,
1003 0x0123_4567,
1004 0x89AB_CDEF,
1005 ];
1006 let block1: [u32; 16] = array::from_fn(|i| (i as u32).wrapping_mul(0x0101_0101));
1007 let block2: [u32; 16] = array::from_fn(|i| (i as u32).wrapping_mul(0xDEAD_BEEFu32));
1008 let counter: u64 = 0x0000_0001_FFFF_FFFF;
1012 let (c2, c1) = run_compress_2x_seq(
1013 cv,
1014 block1,
1015 block2,
1016 counter,
1017 64,
1018 super::super::CHUNK_START,
1019 40,
1020 super::super::CHUNK_END,
1021 );
1022 let exp_c1 = ref_compress(&cv, &block1, counter, 64, super::super::CHUNK_START);
1023 let exp_c2 = ref_compress(&exp_c1, &block2, counter, 40, super::super::CHUNK_END);
1024 assert_eq!(c1, exp_c1);
1025 assert_eq!(c2, exp_c2);
1026 }
1027
1028 #[test]
1035 fn draft_b1_compression_matches_spec_trace() {
1036 let mut block = [0u32; 16];
1041 block[0] = 0x4654_4549;
1042
1043 let flags = CHUNK_START | CHUNK_END | ROOT;
1045 assert_eq!(flags, 0x0b, "spec trace records flags 0b");
1046
1047 let expected = [
1049 0x1ede_a283,
1050 0xabe6_f4e6,
1051 0x2489_6868,
1052 0xcfc0_4e8f,
1053 0x9470_c54c,
1054 0xff82_a646,
1055 0xd6b4_cbd1,
1056 0xe281_5116,
1057 ];
1058 assert_eq!(ref_compress(&IV, &block, 0, 4, flags), expected);
1060 assert_eq!(run_compress(IV, block, 0, 4, flags), expected);
1061 }
1062
1063 #[test]
1064 fn draft_b2_parent_compression_matches_spec_trace() {
1065 let block = [
1071 0xc8d6_3b32,
1072 0xb1d9_fecb,
1073 0xdbf2_dac7,
1074 0x7fba_1e91,
1075 0xa71a_614b,
1076 0x022d_5eb6,
1077 0x43b8_8567,
1078 0x5fb9_8dbb,
1079 0x70dc_03d8,
1080 0xbe50_bb38,
1081 0x4a0f_7bf3,
1082 0xdb9d_008b,
1083 0xc02b_11fb,
1084 0xf2ae_5f91,
1085 0x4c20_d218,
1086 0x5f7d_b224,
1087 ];
1088 let flags = PARENT | ROOT;
1091 assert_eq!(flags, 0x0c, "spec trace records flags 0c");
1092 let expected = [
1093 0x3828_9de7,
1094 0xd3cc_5a91,
1095 0xbab0_1bb2,
1096 0xf8ed_b576,
1097 0xd7d3_08dc,
1098 0x5bb6_0d8d,
1099 0x370f_3f71,
1100 0x46c3_58ec,
1101 ];
1102 assert_eq!(ref_compress(&IV, &block, 0, 64, flags), expected);
1104 assert_eq!(run_compress(IV, block, 0, 64, flags), expected);
1105 }
1106
1107 fn word32() -> impl Strategy<Value = u32> {
1117 prop_oneof![
1118 3 => any::<u32>(),
1119 1 => prop_oneof![Just(0), Just(1), Just(u32::MAX), Just(1 << 31), Just(u32::MAX >> 1)],
1120 ]
1121 }
1122
1123 fn cv8() -> impl Strategy<Value = [u32; 8]> {
1124 prop::array::uniform8(word32())
1125 }
1126
1127 fn block16() -> impl Strategy<Value = [u32; 16]> {
1128 prop::array::uniform16(word32())
1129 }
1130
1131 proptest! {
1132 #![proptest_config(ProptestConfig::with_cases(16))]
1135
1136 #[test]
1137 fn compress_matches_reference(
1138 cv in cv8(), block in block16(), counter in any::<u64>(),
1139 block_len in 0u32..=64, flags in any::<u32>(),
1140 ) {
1141 prop_assert_eq!(
1142 run_compress(cv, block, counter, block_len, flags),
1143 ref_compress(&cv, &block, counter, block_len, flags)
1144 );
1145 }
1146
1147 #[test]
1148 fn compress_2x_lanes_are_independent(
1149 cv0 in cv8(), cv1 in cv8(), b0 in block16(), b1 in block16(),
1150 t0 in any::<u64>(), t1 in any::<u64>(),
1151 l0 in 0u32..=64, l1 in 0u32..=64, f0 in any::<u32>(), f1 in any::<u32>(),
1152 ) {
1153 let actual = run_compress_2x([cv0, cv1], [b0, b1], [t0, t1], [l0, l1], [f0, f1]);
1156 prop_assert_eq!(actual[0], ref_compress(&cv0, &b0, t0, l0, f0));
1158 prop_assert_eq!(actual[1], ref_compress(&cv1, &b1, t1, l1, f1));
1159 }
1160
1161 #[test]
1166 fn compress_2x_hint_matches_its_gates(words in prop::collection::vec(any::<u64>(), 28)) {
1167 let inputs: [u64; 28] = array::from_fn(|i| words[i]);
1168
1169 let mut hinted = [Word::ZERO; 8];
1170 Blake3Compress2x.execute(&[], &inputs.map(Word), &mut hinted);
1171
1172 prop_assert_eq!(hinted.map(|word| word.as_u64()), run_compress_2x_words(inputs));
1173 }
1174
1175 #[test]
1176 fn compress_2x_seq_matches_two_chained_references(
1177 cv in cv8(), b1 in block16(), b2 in block16(), counter in any::<u64>(),
1178 l1 in 0u32..=64, l2 in 0u32..=64, f1 in any::<u32>(), f2 in any::<u32>(),
1179 ) {
1180 let (c2, c1) = run_compress_2x_seq(cv, b1, b2, counter, l1, f1, l2, f2);
1187 let exp_c1 = ref_compress(&cv, &b1, counter, l1, f1);
1189 prop_assert_eq!(c1, exp_c1);
1190 prop_assert_eq!(c2, ref_compress(&exp_c1, &b2, counter, l2, f2));
1192 }
1193 }
1194}