1use std::{array, iter};
3
4use binius_core::word::Word;
5use binius_frontend::{ChipGadget, CircuitBuilder, Hint, Wire, WitnessFiller};
6
7use crate::util::{clear_high_bits, pack_u32_words};
8
9const IV: [u32; 8] = [
10 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
11];
12
13const K: [u32; 64] = [
14 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
15 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
16 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
17 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
18 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
19 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
20 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
21 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
22];
23
24#[derive(Clone, Copy)]
31pub struct State(pub [Wire; 8]);
32
33impl State {
34 pub const fn new(wires: [Wire; 8]) -> Self {
35 State(wires)
36 }
37
38 pub fn public(builder: &CircuitBuilder) -> Self {
39 State(std::array::from_fn(|_| builder.add_inout()))
40 }
41
42 pub fn private(builder: &CircuitBuilder) -> Self {
43 State(std::array::from_fn(|_| builder.add_witness()))
44 }
45
46 pub fn iv(builder: &CircuitBuilder) -> Self {
47 State(std::array::from_fn(|i| builder.add_constant(Word(IV[i] as u64))))
48 }
49
50 pub fn pack_4x64b(&self, builder: &CircuitBuilder) -> [Wire; 4] {
63 array::from_fn(|j| builder.bxor(self.0[2 * j + 1], builder.shl(self.0[2 * j], 32)))
64 }
65}
66
67const HIGH_LANE_ROUNDS: usize = 32;
71
72const SCHEDULE_LANE_GAP: usize = 24;
81
82const PACKED_SCHEDULE_WORDS: usize = 64 - SCHEDULE_LANE_GAP;
84
85const ROUND_WORD_LAG: usize = HIGH_LANE_ROUNDS - SCHEDULE_LANE_GAP;
90
91pub fn sha256_compress(builder: &CircuitBuilder, state_in: State, m: [Wire; 16]) -> State {
138 let mut hint_inputs = Vec::with_capacity(24);
145 hint_inputs.extend_from_slice(&state_in.0);
146 hint_inputs.extend_from_slice(&m);
147 let seeds = builder.call_hint(Sha256SplitHint, &[], &hint_inputs);
148
149 let mut w: Vec<Wire> = seeds[..16].to_vec();
156 for t in 16..PACKED_SCHEDULE_WORDS {
157 let s0 = small_sigma_0(builder, w[t - 15]);
158 let s1 = small_sigma_1(builder, w[t - 2]);
159 let p = builder.iadd_32(w[t - 16], s0);
160 let q = builder.iadd_32(p, w[t - 7]);
161 w.push(builder.iadd_32(q, s1));
162 }
163
164 for (i, seed) in seeds[..16].iter().enumerate() {
176 let low = clear_high_bits(builder, m[i], 32);
177 let high = builder.shl(w[i + SCHEDULE_LANE_GAP], 32);
178 builder.assert_eq("sha256_compress.schedule_seed", *seed, builder.bxor(low, high));
179 }
180
181 let k: [Wire; HIGH_LANE_ROUNDS] = array::from_fn(|j| {
183 let packed = K[HIGH_LANE_ROUNDS + j] as u64 | ((K[j] as u64) << 32);
184 builder.add_constant(Word(packed))
185 });
186
187 let merged: [Wire; 8] = array::from_fn(|i| seeds[16 + i]);
188 let mut state = State(merged);
189 for j in 0..HIGH_LANE_ROUNDS {
190 let w_t = builder.bxor(builder.shl(w[j], 32), builder.shr(w[j + ROUND_WORD_LAG], 32));
197 state = round(builder, k[j], w_t, state);
198 }
199
200 for (hinted, (input, split)) in iter::zip(merged, iter::zip(state_in.0, state.0)) {
212 let expected = builder.bxor(builder.shl(input, 32), builder.shr(split, 32));
213 builder.assert_eq("sha256_compress.round_state", hinted, expected);
214 }
215
216 let sums: [Wire; 4] = array::from_fn(|j| {
229 builder.iadd_32(
230 pack_u32_words(builder, state_in.0[2 * j], state_in.0[2 * j + 1]),
231 pack_u32_words(builder, state.0[2 * j], state.0[2 * j + 1]),
232 )
233 });
234
235 State(array::from_fn(|i| {
237 let sum = sums[i / 2];
238 if i % 2 == 0 {
239 clear_high_bits(builder, sum, 32)
240 } else {
241 builder.shr(sum, 32)
242 }
243 }))
244}
245
246struct Sha256SplitHint;
263
264impl Hint for Sha256SplitHint {
265 const NAME: &'static str = "binius.sha256_compress_split";
266
267 fn shape(&self, _dimensions: &[usize]) -> (usize, usize) {
268 (24, 24)
269 }
270
271 fn execute(&self, _dimensions: &[usize], inputs: &[Word], outputs: &mut [Word]) {
272 let state_in: [u32; 8] = array::from_fn(|i| inputs[i].as_u64() as u32);
274 let m: [u32; 16] = array::from_fn(|i| inputs[8 + i].as_u64() as u32);
275
276 let w = ref_schedule(m);
278 for (i, slot) in outputs[..16].iter_mut().enumerate() {
279 *slot = Word(w[i] as u64 | ((w[i + SCHEDULE_LANE_GAP] as u64) << 32));
280 }
281
282 let split = ref_rounds(state_in, &w, HIGH_LANE_ROUNDS);
284 for (i, slot) in outputs[16..].iter_mut().enumerate() {
285 *slot = Word(split[i] as u64 | ((state_in[i] as u64) << 32));
286 }
287 }
288}
289
290pub fn sha256_compress_2x(builder: &CircuitBuilder, state_in: State, m: [Wire; 16]) -> State {
319 let inputs = state_in.0.into_iter().chain(m).collect::<Vec<_>>();
320
321 let outputs = builder.build_gadget(Sha256Compress2x, &[], &inputs);
322 State(array::from_fn(|i| outputs[i]))
323}
324
325pub struct Sha256Compress2x;
330
331impl Hint for Sha256Compress2x {
332 const NAME: &'static str = "binius.sha256_compress_2x";
333
334 fn shape(&self, _dimensions: &[usize]) -> (usize, usize) {
335 (24, 8)
336 }
337
338 fn execute(&self, _dimensions: &[usize], inputs: &[Word], outputs: &mut [Word]) {
339 let compress_lane = |i: usize| {
343 let lane = |word: Word| (word.as_u64() >> (32 * i)) as u32;
344 let state: [u32; 8] = array::from_fn(|j| lane(inputs[j]));
345 let m: [u32; 16] = array::from_fn(|j| lane(inputs[8 + j]));
346 ref_compress(state, m)
347 };
348
349 let (lane_0, lane_1) = (compress_lane(0), compress_lane(1));
350 for (slot, (low, high)) in iter::zip(outputs, iter::zip(lane_0, lane_1)) {
351 *slot = Word(low as u64 | ((high as u64) << 32));
352 }
353 }
354}
355
356impl ChipGadget for Sha256Compress2x {
357 fn build(&self, builder: &CircuitBuilder, _dimensions: &[usize], inputs: &[Wire]) -> Vec<Wire> {
358 let state_in = State(array::from_fn(|i| inputs[i]));
359 let m: [Wire; 16] = array::from_fn(|i| inputs[8 + i]);
360 compress_2x_gates(builder, state_in, m).0.to_vec()
361 }
362}
363
364fn compress_2x_gates(builder: &CircuitBuilder, state_in: State, m: [Wire; 16]) -> State {
366 let k: [Wire; 64] = std::array::from_fn(|t| {
368 let kt = K[t] as u64;
369 builder.add_constant(Word(kt | (kt << 32)))
370 });
371 compress_inner(builder, state_in, m, &k)
372}
373
374pub fn sha256_compress_2x_seq(
412 builder: &CircuitBuilder,
413 state_in: State,
414 blocks: [[Wire; 16]; 2],
415) -> State {
416 let mut hint_inputs = Vec::with_capacity(24);
422 hint_inputs.extend_from_slice(&state_in.0);
423 hint_inputs.extend_from_slice(&blocks[0]);
424 let merged_vec = builder.call_hint(Sha256CompressHint, &[], &hint_inputs);
425 let merged: [Wire; 8] = array::from_fn(|i| merged_vec[i]);
426
427 let merged_block: [Wire; 16] =
432 array::from_fn(|i| pack_u32_words(builder, blocks[1][i], blocks[0][i]));
433
434 let out = sha256_compress_2x(builder, State::new(merged), merged_block);
435
436 for (m, (s, o)) in iter::zip(merged, iter::zip(state_in.0, out.0)) {
454 let expected = builder.bxor(builder.shl(s, 32), builder.shr(o, 32));
455 builder.assert_eq("sha256_compress_2x_seq.merged_state", m, expected);
456 }
457
458 out
459}
460
461struct Sha256CompressHint;
475
476impl Hint for Sha256CompressHint {
477 const NAME: &'static str = "binius.sha256_compress";
478
479 fn shape(&self, _dimensions: &[usize]) -> (usize, usize) {
480 (24, 8)
481 }
482
483 fn execute(&self, _dimensions: &[usize], inputs: &[Word], outputs: &mut [Word]) {
484 let state_in: [u32; 8] = array::from_fn(|i| inputs[i].as_u64() as u32);
485 let block: [u32; 16] = array::from_fn(|i| inputs[8 + i].as_u64() as u32);
486
487 let out = ref_compress(state_in, block);
488 for (i, slot) in outputs.iter_mut().enumerate() {
489 *slot = Word(out[i] as u64 | ((state_in[i] as u64) << 32));
490 }
491 }
492}
493
494pub fn ref_compress(state_in: [u32; 8], m: [u32; 16]) -> [u32; 8] {
508 let w = ref_schedule(m);
509 let out = ref_rounds(state_in, &w, 64);
510 array::from_fn(|i| state_in[i].wrapping_add(out[i]))
511}
512
513fn ref_schedule(m: [u32; 16]) -> [u32; 64] {
515 let mut w = [0u32; 64];
516 w[..16].copy_from_slice(&m);
517 for t in 16..64 {
518 let s0 = w[t - 15].rotate_right(7) ^ w[t - 15].rotate_right(18) ^ (w[t - 15] >> 3);
519 let s1 = w[t - 2].rotate_right(17) ^ w[t - 2].rotate_right(19) ^ (w[t - 2] >> 10);
520 w[t] = w[t - 16]
521 .wrapping_add(s0)
522 .wrapping_add(w[t - 7])
523 .wrapping_add(s1);
524 }
525 w
526}
527
528fn ref_rounds(state_in: [u32; 8], w: &[u32; 64], rounds: usize) -> [u32; 8] {
534 let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut h] = state_in;
535 for t in 0..rounds {
536 let big_s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
537 let ch = (e & f) ^ ((!e) & g);
538 let t1 = h
539 .wrapping_add(big_s1)
540 .wrapping_add(ch)
541 .wrapping_add(K[t])
542 .wrapping_add(w[t]);
543 let big_s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
544 let maj = (a & b) ^ (a & c) ^ (b & c);
545 let t2 = big_s0.wrapping_add(maj);
546 h = g;
547 g = f;
548 f = e;
549 e = d.wrapping_add(t1);
550 d = c;
551 c = b;
552 b = a;
553 a = t1.wrapping_add(t2);
554 }
555
556 [a, b, c, d, e, f, g, h]
557}
558
559fn compress_inner(
565 builder: &CircuitBuilder,
566 state_in: State,
567 m: [Wire; 16],
568 k: &[Wire; 64],
569) -> State {
570 let mut w: Vec<Wire> = Vec::with_capacity(64);
580 w.extend_from_slice(&m);
582
583 for t in 16..64 {
585 let s0 = small_sigma_0(builder, w[t - 15]);
586 let s1 = small_sigma_1(builder, w[t - 2]);
587 let p = builder.iadd_32(w[t - 16], s0);
588 let q = builder.iadd_32(p, w[t - 7]);
589 w.push(builder.iadd_32(q, s1));
590 }
591
592 let w: &[Wire; 64] = (&*w).try_into().unwrap();
593 let mut state = state_in;
594 for t in 0..64 {
595 state = round(builder, k[t], w[t], state);
596 }
597
598 State([
600 builder.iadd_32(state_in.0[0], state.0[0]),
601 builder.iadd_32(state_in.0[1], state.0[1]),
602 builder.iadd_32(state_in.0[2], state.0[2]),
603 builder.iadd_32(state_in.0[3], state.0[3]),
604 builder.iadd_32(state_in.0[4], state.0[4]),
605 builder.iadd_32(state_in.0[5], state.0[5]),
606 builder.iadd_32(state_in.0[6], state.0[6]),
607 builder.iadd_32(state_in.0[7], state.0[7]),
608 ])
609}
610
611pub fn populate_message_block(w: &mut WitnessFiller<'_>, m: &[Wire; 16], bytes: [u8; 64]) {
616 for (wire, chunk) in m.iter().zip(bytes.chunks_exact(4)) {
617 let word = u32::from_be_bytes(chunk.try_into().unwrap());
618 w[*wire] = Word(word as u64);
619 }
620}
621
622fn round(builder: &CircuitBuilder, k_t: Wire, w_t: Wire, state: State) -> State {
623 let State([a, b, c, d, e, f, g, h]) = state;
624
625 let big_sigma_e = big_sigma_1(builder, e);
626 let ch_efg = ch(builder, e, f, g);
627 let t1a = builder.iadd_32(h, big_sigma_e);
628 let t1b = builder.iadd_32(t1a, ch_efg);
629 let t1c = builder.iadd_32(t1b, k_t);
630 let t1 = builder.iadd_32(t1c, w_t);
631
632 let big_sigma_a = big_sigma_0(builder, a);
633 let maj_abc = maj(builder, a, b, c);
634 let t2 = builder.iadd_32(big_sigma_a, maj_abc);
635
636 let h = g;
637 let g = f;
638 let f = e;
639 let e = builder.iadd_32(d, t1);
640 let d = c;
641 let c = b;
642 let b = a;
643 let a = builder.iadd_32(t1, t2);
644
645 State([a, b, c, d, e, f, g, h])
646}
647
648fn ch(builder: &CircuitBuilder, x: Wire, y: Wire, z: Wire) -> Wire {
651 builder.bxor(z, builder.band(x, builder.bxor(y, z)))
652}
653
654fn maj(builder: &CircuitBuilder, x: Wire, y: Wire, z: Wire) -> Wire {
657 builder.bxor(builder.band(builder.bxor(x, z), builder.bxor(y, z)), z)
658}
659
660fn big_sigma_0(b: &CircuitBuilder, x: Wire) -> Wire {
662 let r1 = b.rotr32(x, 2);
663 let r2 = b.rotr32(x, 13);
664 let r3 = b.rotr32(x, 22);
665 let x1 = b.bxor(r1, r2);
666 b.bxor(x1, r3)
667}
668
669fn big_sigma_1(b: &CircuitBuilder, x: Wire) -> Wire {
671 let r1 = b.rotr32(x, 6);
672 let r2 = b.rotr32(x, 11);
673 let r3 = b.rotr32(x, 25);
674 let x1 = b.bxor(r1, r2);
675 b.bxor(x1, r3)
676}
677
678fn small_sigma_0(b: &CircuitBuilder, x: Wire) -> Wire {
680 let r1 = b.rotr32(x, 7);
681 let r2 = b.rotr32(x, 18);
682 let s1 = b.srl32(x, 3);
683 let x1 = b.bxor(r1, r2);
684 b.bxor(x1, s1)
685}
686
687fn small_sigma_1(b: &CircuitBuilder, x: Wire) -> Wire {
689 let r1 = b.rotr32(x, 17);
690 let r2 = b.rotr32(x, 19);
691 let s1 = b.srl32(x, 10);
692 let x1 = b.bxor(r1, r2);
693 b.bxor(x1, s1)
694}
695
696#[cfg(test)]
697mod tests {
698 use binius_core::word::Word;
699 use binius_frontend::{CircuitBuilder, CircuitStat, Hint, Wire};
700 use proptest::prelude::*;
701
702 use super::{
703 IV, Sha256Compress2x, State, compress_2x_gates, populate_message_block, ref_compress,
704 sha256_compress, sha256_compress_2x, sha256_compress_2x_seq,
705 };
706
707 const AND_PER_COMPRESSION: usize = 364;
729
730 fn and_cost(f: impl FnOnce(&CircuitBuilder) -> State) -> usize {
735 let builder = CircuitBuilder::new();
736 let out = f(&builder);
737 for wire in out.0 {
738 let public = builder.add_inout();
739 builder.assert_eq("out", wire, public);
740 }
741 CircuitStat::collect(&builder.build()).n_and_constraints
742 }
743
744 #[test]
745 fn a_compression_spends_only_its_nonlinear_work() {
746 let inputs = |b: &CircuitBuilder| {
747 let state = State::public(b);
748 let m: [Wire; 16] = std::array::from_fn(|_| b.add_inout());
749 (state, m)
750 };
751
752 let single = and_cost(|b| {
754 let (state, m) = inputs(b);
755 sha256_compress(b, state, m)
756 });
757 assert_eq!(single, AND_PER_COMPRESSION);
758
759 let paired = and_cost(|b| {
761 let (state, m) = inputs(b);
762 sha256_compress_2x(b, state, m)
763 });
764 assert_eq!(paired, 2 * AND_PER_COMPRESSION);
765
766 let chained = and_cost(|b| {
768 let (state, _) = inputs(b);
769 let blocks: [[Wire; 16]; 2] =
770 std::array::from_fn(|_| std::array::from_fn(|_| b.add_inout()));
771 sha256_compress_2x_seq(b, state, blocks)
772 });
773 assert_eq!(chained, 2 * AND_PER_COMPRESSION);
774 }
775
776 #[test]
782 fn proof_preimage() {
783 let mut preimage: [u8; 64] = [0; 64];
785 preimage[0..3].copy_from_slice(b"abc");
786 preimage[3] = 0x80;
787 preimage[63] = 0x18;
788
789 #[rustfmt::skip]
790 let expected_state: [u32; 8] = [
791 0xba7816bf, 0x8f01cfea, 0x414140de, 0x5dae2223,
792 0xb00361a3, 0x96177a9c, 0xb410ff61, 0xf20015ad,
793 ];
794
795 let circuit = CircuitBuilder::new();
796 let state = State::iv(&circuit);
797 let input: [Wire; 16] = std::array::from_fn(|_| circuit.add_witness());
798 let output: [Wire; 8] = std::array::from_fn(|_| circuit.add_inout());
799 let state_out = sha256_compress(&circuit, state, input);
800
801 let mask32 = circuit.add_constant(Word::MASK_32);
803 for (i, (actual_x, expected_x)) in state_out.0.iter().zip(output).enumerate() {
804 circuit.assert_eq(
805 format!("preimage_eq[{i}]"),
806 circuit.band(*actual_x, mask32),
807 expected_x,
808 );
809 }
810
811 let circuit = circuit.build();
812 let cs = circuit.constraint_system();
813 let mut w = circuit.new_witness_filler();
814
815 populate_message_block(&mut w, &input, preimage);
817
818 for (i, &output) in output.iter().enumerate() {
819 w[output] = Word(expected_state[i] as u64);
820 }
821 circuit.populate_wire_witness(&mut w).unwrap();
822
823 cs.verify(&w.into_value_vec()).unwrap();
824 }
825
826 #[test]
827 fn sha256_chain() {
828 const N: usize = 3;
831 let circuit = CircuitBuilder::new();
832
833 let mut m_vec = Vec::with_capacity(N);
834
835 let mut state = State::iv(&circuit);
837 for i in 0..N {
838 let sha256_builder = circuit.subcircuit(format!("sha256[{i}]"));
841
842 let m: [Wire; 16] = if i == 0 {
845 std::array::from_fn(|_| sha256_builder.add_inout())
846 } else {
847 std::array::from_fn(|_| sha256_builder.add_witness())
848 };
849 state = sha256_compress(&sha256_builder, state, m);
850
851 m_vec.push(m);
852 }
853
854 let circuit = circuit.build();
855 let cs = circuit.constraint_system();
856 let mut w = circuit.new_witness_filler();
857
858 for m in &m_vec {
859 populate_message_block(&mut w, m, [0; 64]);
860 }
861 circuit.populate_wire_witness(&mut w).unwrap();
862
863 cs.verify(&w.into_value_vec()).unwrap();
864 }
865
866 #[test]
867 fn sha256_parallel() {
868 const N: usize = 3;
870 let circuit = CircuitBuilder::new();
871
872 let mut m_vec = Vec::with_capacity(N);
873
874 for i in 0..N {
875 let sha256_builder = circuit.subcircuit(format!("sha256[{i}]"));
877
878 let state = State::iv(&sha256_builder);
880 let m: [Wire; 16] = std::array::from_fn(|_| sha256_builder.add_inout());
881 sha256_compress(&sha256_builder, state, m);
882
883 m_vec.push(m);
884 }
885
886 let circuit = circuit.build();
887 let cs = circuit.constraint_system();
888 let mut w = circuit.new_witness_filler();
889
890 for m in &m_vec {
891 populate_message_block(&mut w, m, [0; 64]);
892 }
893 circuit.populate_wire_witness(&mut w).unwrap();
894
895 cs.verify(&w.into_value_vec()).unwrap();
896 }
897
898 fn run_compress_with_dirt(state_in: [u32; 8], m: [u32; 16], dirt: u32) -> [u64; 8] {
903 let dirt = (dirt as u64) << 32;
904
905 let circuit = CircuitBuilder::new();
906
907 let state_wires: [Wire; 8] = std::array::from_fn(|_| circuit.add_witness());
909 let m_wires: [Wire; 16] = std::array::from_fn(|_| circuit.add_witness());
910 let out = sha256_compress(&circuit, State::new(state_wires), m_wires);
911
912 for wire in out.0 {
914 circuit.mark_inout(wire);
915 }
916
917 let circuit = circuit.build();
918 let cs = circuit.constraint_system();
919 let mut w = circuit.new_witness_filler();
920 for i in 0..8 {
921 w[state_wires[i]] = Word(state_in[i] as u64 | dirt);
922 }
923 for i in 0..16 {
924 w[m_wires[i]] = Word(m[i] as u64 | dirt);
925 }
926
927 circuit.populate_wire_witness(&mut w).unwrap();
929 let words: [u64; 8] = std::array::from_fn(|i| w[out.0[i]].as_u64());
930
931 cs.verify(&w.into_value_vec()).unwrap();
933 words
934 }
935
936 const NONTRIVIAL_STATE: [u32; 8] = [
938 0xdead_beef,
939 0xcafe_babe,
940 0x1234_5678,
941 0x9abc_def0,
942 0x0bad_f00d,
943 0xfeed_face,
944 0x0123_4567,
945 0x89ab_cdef,
946 ];
947
948 #[test]
949 fn compress_ignores_dirty_input_high_halves() {
950 let m: [u32; 16] = std::array::from_fn(|i| (i as u32).wrapping_mul(0xdead_beef));
958 let expected = ref_compress(NONTRIVIAL_STATE, m);
959
960 for dirt in [0, 1, 0x8000_0000, 0xffff_ffff] {
961 let actual = run_compress_with_dirt(NONTRIVIAL_STATE, m, dirt);
962 assert_eq!(actual, expected.map(u64::from), "dirt {dirt:#x} changed the result");
963 }
964 }
965
966 proptest! {
967 #[test]
970 fn compress_matches_reference(
971 state_words in prop::collection::vec(any::<u32>(), 8),
972 block_words in prop::collection::vec(any::<u32>(), 16),
973 ) {
974 let state_in: [u32; 8] = std::array::from_fn(|i| state_words[i]);
975 let m: [u32; 16] = std::array::from_fn(|i| block_words[i]);
976
977 let actual = run_compress_with_dirt(state_in, m, 0);
978 prop_assert_eq!(actual, ref_compress(state_in, m).map(u64::from));
979 }
980 }
981
982 fn pack2x(lo: u32, hi: u32) -> u64 {
983 (lo as u64) | ((hi as u64) << 32)
984 }
985
986 fn run_2x(state0: [u32; 8], m0: [u32; 16], state1: [u32; 8], m1: [u32; 16]) {
989 let exp0 = ref_compress(state0, m0);
990 let exp1 = ref_compress(state1, m1);
991
992 let circuit = CircuitBuilder::new();
993 let state_in: [Wire; 8] = std::array::from_fn(|_| circuit.add_witness());
994 let m: [Wire; 16] = std::array::from_fn(|_| circuit.add_witness());
995 let out = sha256_compress_2x(&circuit, State::new(state_in), m);
996 let out_inout: [Wire; 8] = std::array::from_fn(|_| circuit.add_inout());
997 for i in 0..8 {
998 circuit.assert_eq(format!("out[{i}]"), out.0[i], out_inout[i]);
999 }
1000
1001 let circuit = circuit.build();
1002 let cs = circuit.constraint_system();
1003 let mut w = circuit.new_witness_filler();
1004 for i in 0..8 {
1005 w[state_in[i]] = Word(pack2x(state0[i], state1[i]));
1006 }
1007 for i in 0..16 {
1008 w[m[i]] = Word(pack2x(m0[i], m1[i]));
1009 }
1010 for i in 0..8 {
1011 w[out_inout[i]] = Word(pack2x(exp0[i], exp1[i]));
1012 }
1013 circuit.populate_wire_witness(&mut w).unwrap();
1014
1015 cs.verify(&w.into_value_vec()).unwrap();
1016 }
1017
1018 fn abc_block() -> [u32; 16] {
1021 let mut m = [0u32; 16];
1022 m[0] = 0x6162_6380;
1023 m[15] = 0x0000_0018;
1024 m
1025 }
1026
1027 const ABC_DIGEST: [u32; 8] = [
1029 0xba78_16bf,
1030 0x8f01_cfea,
1031 0x4141_40de,
1032 0x5dae_2223,
1033 0xb003_61a3,
1034 0x9617_7a9c,
1035 0xb410_ff61,
1036 0xf200_15ad,
1037 ];
1038
1039 #[test]
1040 fn compress_2x_distinct_lanes() {
1041 assert_eq!(ref_compress(IV, abc_block()), ABC_DIGEST);
1044
1045 let state1 = NONTRIVIAL_STATE;
1048 let m1: [u32; 16] = std::array::from_fn(|i| (i as u32).wrapping_mul(0x0101_0101));
1049
1050 run_2x(IV, abc_block(), state1, m1);
1051 }
1052
1053 #[test]
1054 fn compress_2x_lane_independence() {
1055 run_2x(IV, abc_block(), IV, [0; 16]);
1058 }
1059
1060 fn run_2x_seq(state_in: [u32; 8], block1: [u32; 16], block2: [u32; 16]) {
1069 let s1 = ref_compress(state_in, block1);
1071 let s2 = ref_compress(s1, block2);
1072
1073 let circuit = CircuitBuilder::new();
1075 let state_wires: [Wire; 8] = std::array::from_fn(|_| circuit.add_witness());
1076 let block1_wires: [Wire; 16] = std::array::from_fn(|_| circuit.add_witness());
1077 let block2_wires: [Wire; 16] = std::array::from_fn(|_| circuit.add_witness());
1078
1079 let out =
1081 sha256_compress_2x_seq(&circuit, State::new(state_wires), [block1_wires, block2_wires]);
1082
1083 let out_inout: [Wire; 8] = std::array::from_fn(|_| circuit.add_inout());
1085 for i in 0..8 {
1086 circuit.assert_eq(format!("out[{i}]"), out.0[i], out_inout[i]);
1087 }
1088
1089 let circuit = circuit.build();
1090 let cs = circuit.constraint_system();
1091 let mut w = circuit.new_witness_filler();
1092
1093 for i in 0..8 {
1095 w[state_wires[i]] = Word(state_in[i] as u64);
1096 }
1097 for i in 0..16 {
1098 w[block1_wires[i]] = Word(block1[i] as u64);
1099 w[block2_wires[i]] = Word(block2[i] as u64);
1100 }
1101
1102 for i in 0..8 {
1104 w[out_inout[i]] = Word(pack2x(s2[i], s1[i]));
1105 }
1106
1107 circuit.populate_wire_witness(&mut w).unwrap();
1109 cs.verify(&w.into_value_vec()).unwrap();
1110 }
1111
1112 fn pack_block_be(bytes: &[u8; 64]) -> [u32; 16] {
1114 std::array::from_fn(|i| u32::from_be_bytes(bytes[i * 4..i * 4 + 4].try_into().unwrap()))
1115 }
1116
1117 #[test]
1118 fn compress_2x_seq_matches_rfc_two_block_kat() {
1119 let msg = b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq";
1123
1124 let mut padded = [0u8; 128];
1130 padded[..56].copy_from_slice(msg);
1131 padded[56] = 0x80;
1132 padded[120..128].copy_from_slice(&(56u64 * 8).to_be_bytes());
1133 let block0 = pack_block_be(padded[0..64].try_into().unwrap());
1134 let block1 = pack_block_be(padded[64..128].try_into().unwrap());
1135
1136 let expected: [u32; 8] = [
1138 0x248d_6a61,
1139 0xd206_38b8,
1140 0xe5c0_2693,
1141 0x0c3e_6039,
1142 0xa33c_e459,
1143 0x64ff_2167,
1144 0xf6ec_edd4,
1145 0x19db_06c1,
1146 ];
1147
1148 let expected_s1: [u32; 8] = [
1155 0x85e6_55d6,
1156 0x417a_1795,
1157 0x3363_376a,
1158 0x624c_de5c,
1159 0x76e0_9589,
1160 0xcac5_f811,
1161 0xcc4b_32c1,
1162 0xf20e_533a,
1163 ];
1164
1165 let s1 = ref_compress(IV, block0);
1167 assert_eq!(s1, expected_s1);
1168 let s2 = ref_compress(s1, block1);
1169 assert_eq!(s2, expected);
1170
1171 run_2x_seq(IV, block0, block1);
1173 }
1174
1175 #[test]
1176 fn compress_2x_seq_distinct_params() {
1177 let state_in = NONTRIVIAL_STATE;
1183 let block1: [u32; 16] = std::array::from_fn(|i| (i as u32).wrapping_mul(0xdead_beef));
1184 let block2: [u32; 16] = std::array::from_fn(|i| (i as u32).wrapping_mul(0x0101_0101));
1185 run_2x_seq(state_in, block1, block2);
1186 }
1187
1188 fn run_compress_2x_words(inputs: [u64; 24]) -> [u64; 8] {
1190 let builder = CircuitBuilder::new();
1191 let wires: [Wire; 24] = std::array::from_fn(|_| builder.add_witness());
1192 let out = compress_2x_gates(
1193 &builder,
1194 State(std::array::from_fn(|i| wires[i])),
1195 std::array::from_fn(|i| wires[8 + i]),
1196 );
1197 for wire in out.0 {
1198 builder.mark_inout(wire);
1199 }
1200
1201 let circuit = builder.build();
1202 let mut w = circuit.new_witness_filler();
1203 for (wire, word) in std::iter::zip(wires, inputs) {
1204 w[wire] = Word(word);
1205 }
1206 circuit.populate_wire_witness(&mut w).unwrap();
1207
1208 std::array::from_fn(|i| w[out.0[i]].as_u64())
1209 }
1210
1211 proptest! {
1212 #[test]
1217 fn compress_2x_hint_matches_its_gates(words in prop::collection::vec(any::<u64>(), 24)) {
1218 let inputs: [u64; 24] = std::array::from_fn(|i| words[i]);
1219
1220 let mut hinted = [Word::ZERO; 8];
1221 Sha256Compress2x.execute(&[], &inputs.map(Word), &mut hinted);
1222
1223 prop_assert_eq!(hinted.map(|word| word.as_u64()), run_compress_2x_words(inputs));
1224 }
1225 }
1226}