Skip to main content

binius_circuits/sha512/
compress.rs

1// Copyright 2025 Irreducible Inc.
2use binius_core::word::Word;
3use binius_frontend::{ChipGadget, CircuitBuilder, Hint, Wire, WitnessFiller};
4
5const IV: [u64; 8] = [
6	0x6a09e667f3bcc908,
7	0xbb67ae8584caa73b,
8	0x3c6ef372fe94f82b,
9	0xa54ff53a5f1d36f1,
10	0x510e527fade682d1,
11	0x9b05688c2b3e6c1f,
12	0x1f83d9abfb41bd6b,
13	0x5be0cd19137e2179,
14];
15
16const K: [u64; 80] = [
17	0x428a2f98d728ae22,
18	0x7137449123ef65cd,
19	0xb5c0fbcfec4d3b2f,
20	0xe9b5dba58189dbbc,
21	0x3956c25bf348b538,
22	0x59f111f1b605d019,
23	0x923f82a4af194f9b,
24	0xab1c5ed5da6d8118,
25	0xd807aa98a3030242,
26	0x12835b0145706fbe,
27	0x243185be4ee4b28c,
28	0x550c7dc3d5ffb4e2,
29	0x72be5d74f27b896f,
30	0x80deb1fe3b1696b1,
31	0x9bdc06a725c71235,
32	0xc19bf174cf692694,
33	0xe49b69c19ef14ad2,
34	0xefbe4786384f25e3,
35	0x0fc19dc68b8cd5b5,
36	0x240ca1cc77ac9c65,
37	0x2de92c6f592b0275,
38	0x4a7484aa6ea6e483,
39	0x5cb0a9dcbd41fbd4,
40	0x76f988da831153b5,
41	0x983e5152ee66dfab,
42	0xa831c66d2db43210,
43	0xb00327c898fb213f,
44	0xbf597fc7beef0ee4,
45	0xc6e00bf33da88fc2,
46	0xd5a79147930aa725,
47	0x06ca6351e003826f,
48	0x142929670a0e6e70,
49	0x27b70a8546d22ffc,
50	0x2e1b21385c26c926,
51	0x4d2c6dfc5ac42aed,
52	0x53380d139d95b3df,
53	0x650a73548baf63de,
54	0x766a0abb3c77b2a8,
55	0x81c2c92e47edaee6,
56	0x92722c851482353b,
57	0xa2bfe8a14cf10364,
58	0xa81a664bbc423001,
59	0xc24b8b70d0f89791,
60	0xc76c51a30654be30,
61	0xd192e819d6ef5218,
62	0xd69906245565a910,
63	0xf40e35855771202a,
64	0x106aa07032bbd1b8,
65	0x19a4c116b8d2d0c8,
66	0x1e376c085141ab53,
67	0x2748774cdf8eeb99,
68	0x34b0bcb5e19b48a8,
69	0x391c0cb3c5c95a63,
70	0x4ed8aa4ae3418acb,
71	0x5b9cca4f7763e373,
72	0x682e6ff3d6b2b8a3,
73	0x748f82ee5defb2fc,
74	0x78a5636f43172f60,
75	0x84c87814a1f0ab72,
76	0x8cc702081a6439ec,
77	0x90befffa23631e28,
78	0xa4506cebde82bde9,
79	0xbef9a3f7b2c67915,
80	0xc67178f2e372532b,
81	0xca273eceea26619c,
82	0xd186b8c721c0c207,
83	0xeada7dd6cde0eb1e,
84	0xf57d4f7fee6ed178,
85	0x06f067aa72176fba,
86	0x0a637dc5a2c898a6,
87	0x113f9804bef90dae,
88	0x1b710b35131c471b,
89	0x28db77f523047d84,
90	0x32caab7b40c72493,
91	0x3c9ebe0a15c9bebc,
92	0x431d67c49c100d4c,
93	0x4cc5d4becb3e42b6,
94	0x597f299cfc657e2a,
95	0x5fcb6fab3ad6faec,
96	0x6c44198c4a475817,
97];
98
99/// The internal state of SHA-512.
100///
101/// The state size is 512 bits.
102///
103/// The elements are referred to as a–h or H0–H7.
104#[derive(Clone, Copy)]
105pub struct State(pub [Wire; 8]);
106
107impl State {
108	pub const fn new(wires: [Wire; 8]) -> Self {
109		State(wires)
110	}
111
112	pub fn public(builder: &CircuitBuilder) -> Self {
113		State(std::array::from_fn(|_| builder.add_inout()))
114	}
115
116	pub fn private(builder: &CircuitBuilder) -> Self {
117		State(std::array::from_fn(|_| builder.add_witness()))
118	}
119
120	pub fn iv(builder: &CircuitBuilder) -> Self {
121		State(std::array::from_fn(|i| builder.add_constant(Word(IV[i]))))
122	}
123}
124
125/// SHA-512 compression function.
126///
127/// Builds a circuit that updates the chaining state with one 1024-bit message block. Returns the
128/// next chaining state.
129///
130/// # Chips
131///
132/// This is a [`ChipGadget`]. A circuit that calls
133/// [`register_chip`](CircuitBuilder::register_chip) with [`Sha512Compress`] before building turns
134/// every compression under it into a chip call, including the ones `sha512_fixed` and
135/// `sha512_varlen` reach.
136pub fn compress(builder: &CircuitBuilder, state_in: State, m: [Wire; 16]) -> State {
137	let inputs: Vec<Wire> = state_in.0.into_iter().chain(m).collect();
138
139	let outputs = builder.build_gadget(Sha512Compress, &[], &inputs);
140	State(std::array::from_fn(|i| outputs[i]))
141}
142
143/// [`compress`] as a gadget, so that a circuit can make it a chip.
144///
145/// Its interface is the flat 24 input words `state[0..8]`, `m[0..16]`, and the 8 output state
146/// words. Every word is a full 64-bit SHA-512 word; there is no lane packing.
147pub struct Sha512Compress;
148
149impl Hint for Sha512Compress {
150	const NAME: &'static str = "binius.sha512_compress";
151
152	fn shape(&self, _dimensions: &[usize]) -> (usize, usize) {
153		(24, 8)
154	}
155
156	fn execute(&self, _dimensions: &[usize], inputs: &[Word], outputs: &mut [Word]) {
157		let state_in: [u64; 8] = std::array::from_fn(|i| inputs[i].as_u64());
158		let m: [u64; 16] = std::array::from_fn(|i| inputs[8 + i].as_u64());
159
160		for (slot, word) in outputs.iter_mut().zip(ref_compress(state_in, m)) {
161			*slot = Word(word);
162		}
163	}
164}
165
166impl ChipGadget for Sha512Compress {
167	fn build(&self, builder: &CircuitBuilder, _dimensions: &[usize], inputs: &[Wire]) -> Vec<Wire> {
168		let state_in = State(std::array::from_fn(|i| inputs[i]));
169		let m: [Wire; 16] = std::array::from_fn(|i| inputs[8 + i]);
170		compress_gates(builder, state_in, m).0.to_vec()
171	}
172}
173
174/// Pure-Rust SHA-512 compression of a single 1024-bit block.
175///
176/// Matches the in-circuit compression exactly.
177/// Used for prover-side witness generation and as the test reference.
178pub fn ref_compress(state_in: [u64; 8], m: [u64; 16]) -> [u64; 8] {
179	let mut w = [0u64; 80];
180	w[..16].copy_from_slice(&m);
181	for t in 16..80 {
182		let s0 = w[t - 15].rotate_right(1) ^ w[t - 15].rotate_right(8) ^ (w[t - 15] >> 7);
183		let s1 = w[t - 2].rotate_right(19) ^ w[t - 2].rotate_right(61) ^ (w[t - 2] >> 6);
184		w[t] = w[t - 16]
185			.wrapping_add(s0)
186			.wrapping_add(w[t - 7])
187			.wrapping_add(s1);
188	}
189
190	let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut h] = state_in;
191	for t in 0..80 {
192		let big_s1 = e.rotate_right(14) ^ e.rotate_right(18) ^ e.rotate_right(41);
193		let ch = (e & f) ^ ((!e) & g);
194		let t1 = h
195			.wrapping_add(big_s1)
196			.wrapping_add(ch)
197			.wrapping_add(K[t])
198			.wrapping_add(w[t]);
199		let big_s0 = a.rotate_right(28) ^ a.rotate_right(34) ^ a.rotate_right(39);
200		let maj = (a & b) ^ (a & c) ^ (b & c);
201		let t2 = big_s0.wrapping_add(maj);
202		h = g;
203		g = f;
204		f = e;
205		e = d.wrapping_add(t1);
206		d = c;
207		c = b;
208		b = a;
209		a = t1.wrapping_add(t2);
210	}
211
212	[
213		state_in[0].wrapping_add(a),
214		state_in[1].wrapping_add(b),
215		state_in[2].wrapping_add(c),
216		state_in[3].wrapping_add(d),
217		state_in[4].wrapping_add(e),
218		state_in[5].wrapping_add(f),
219		state_in[6].wrapping_add(g),
220		state_in[7].wrapping_add(h),
221	]
222}
223
224/// [`compress`] in gates, whatever the building circuit does with the gadget.
225fn compress_gates(builder: &CircuitBuilder, state_in: State, m: [Wire; 16]) -> State {
226	// ---- message-schedule ----
227	// W[0..15] = block_words
228	// for t = 16 .. 79:
229	//     s0   = σ0(W[t-15])
230	//     s1   = σ1(W[t-2])
231	//     (p, _)  = Add(W[t-16], s0)
232	//     (q, _)  = Add(p, W[t-7])
233	//     (W[t],_) = Add(q, s1)
234	let mut w = Vec::with_capacity(80);
235
236	// W[0..15] = block_words
237	w.extend_from_slice(&m);
238
239	// W[16..79] computed from previous W values
240	for t in 16..80 {
241		let s0 = small_sigma_0(builder, w[t - 15]);
242		let s1 = small_sigma_1(builder, w[t - 2]);
243		let (p, _carry) = builder.iadd(w[t - 16], s0);
244		let (q, _carry) = builder.iadd(p, w[t - 7]);
245		let (w_t, _carry) = builder.iadd(q, s1);
246		w.push(w_t);
247	}
248
249	let w: &[Wire; 80] = (&*w).try_into().unwrap();
250	let mut state = state_in;
251	for t in 0..80 {
252		state = round(builder, t, state, w);
253	}
254
255	// Add the compressed chunk to the current hash value
256	let (a_out, _carry) = builder.iadd(state_in.0[0], state.0[0]);
257	let (b_out, _carry) = builder.iadd(state_in.0[1], state.0[1]);
258	let (c_out, _carry) = builder.iadd(state_in.0[2], state.0[2]);
259	let (d_out, _carry) = builder.iadd(state_in.0[3], state.0[3]);
260	let (e_out, _carry) = builder.iadd(state_in.0[4], state.0[4]);
261	let (f_out, _carry) = builder.iadd(state_in.0[5], state.0[5]);
262	let (g_out, _carry) = builder.iadd(state_in.0[6], state.0[6]);
263	let (h_out, _carry) = builder.iadd(state_in.0[7], state.0[7]);
264
265	State([a_out, b_out, c_out, d_out, e_out, f_out, g_out, h_out])
266}
267
268/// Packs a 128-byte SHA-512 message block into the 16 message wires of [`compress`].
269///
270/// Each output wire receives the 64-bit big-endian word formed from the corresponding 8 input
271/// bytes.
272pub fn pack_message_block(w: &mut WitnessFiller<'_>, m_wires: &[Wire; 16], block: [u8; 128]) {
273	for i in 0..16 {
274		let j = i * 8;
275		// Assemble a 64-bit big-endian word.
276		let limb = ((block[j] as u64) << 56)
277			| ((block[j + 1] as u64) << 48)
278			| ((block[j + 2] as u64) << 40)
279			| ((block[j + 3] as u64) << 32)
280			| ((block[j + 4] as u64) << 24)
281			| ((block[j + 5] as u64) << 16)
282			| ((block[j + 6] as u64) << 8)
283			| (block[j + 7] as u64);
284
285		w[m_wires[i]] = Word(limb);
286	}
287}
288
289fn round(builder: &CircuitBuilder, round: usize, state: State, w: &[Wire; 80]) -> State {
290	let State([a, b, c, d, e, f, g, h]) = state;
291
292	let big_sigma_e = big_sigma_1(builder, e);
293	let ch_efg = ch(builder, e, f, g);
294	let (t1a, _carry) = builder.iadd(h, big_sigma_e);
295	let (t1b, _carry) = builder.iadd(t1a, ch_efg);
296	let rc = builder.add_constant(Word(K[round]));
297	let (t1c, _carry) = builder.iadd(t1b, rc);
298	let (t1, _carry) = builder.iadd(t1c, w[round]);
299
300	let big_sigma_a = big_sigma_0(builder, a);
301	let maj_abc = maj(builder, a, b, c);
302	let (t2, _carry) = builder.iadd(big_sigma_a, maj_abc);
303
304	let h = g;
305	let g = f;
306	let f = e;
307	let (e, _carry) = builder.iadd(d, t1);
308	let d = c;
309	let c = b;
310	let b = a;
311	let (a, _carry) = builder.iadd(t1, t2);
312
313	State([a, b, c, d, e, f, g, h])
314}
315
316/// Ch(x, y, z) = (x AND y) XOR (NOT y AND z)
317///             = z XOR (x AND (y XOR z))
318fn ch(builder: &CircuitBuilder, x: Wire, y: Wire, z: Wire) -> Wire {
319	builder.bxor(z, builder.band(x, builder.bxor(y, z)))
320}
321
322/// Maj(x, y, z) = (x AND y) XOR (x AND z) XOR (y AND z)
323///              = (x XOR z) AND (y XOR z) XOR z.
324fn maj(builder: &CircuitBuilder, x: Wire, y: Wire, z: Wire) -> Wire {
325	builder.bxor(builder.band(builder.bxor(x, z), builder.bxor(y, z)), z)
326}
327
328/// Σ0(x)       = XOR( XOR( ROTR(x, 28), ROTR(x, 34) ), ROTR(x, 39) )
329fn big_sigma_0(b: &CircuitBuilder, x: Wire) -> Wire {
330	let r1 = b.rotr(x, 28);
331	let r2 = b.rotr(x, 34);
332	let r3 = b.rotr(x, 39);
333	let x1 = b.bxor(r1, r2);
334	b.bxor(x1, r3)
335}
336
337/// Σ1(x)       = XOR( XOR( ROTR(x, 14), ROTR(x, 18) ), ROTR(x, 41) )
338fn big_sigma_1(b: &CircuitBuilder, x: Wire) -> Wire {
339	let r1 = b.rotr(x, 14);
340	let r2 = b.rotr(x, 18);
341	let r3 = b.rotr(x, 41);
342	let x1 = b.bxor(r1, r2);
343	b.bxor(x1, r3)
344}
345
346/// σ0(x)       = XOR( XOR( ROTR(x,  1), ROTR(x,  8)), SHR(x,  7) )
347fn small_sigma_0(b: &CircuitBuilder, x: Wire) -> Wire {
348	let r1 = b.rotr(x, 1);
349	let r2 = b.rotr(x, 8);
350	let s1 = b.shr(x, 7);
351	let x1 = b.bxor(r1, r2);
352	b.bxor(x1, s1)
353}
354
355/// σ1(x)       = XOR( XOR( ROTR(x, 19), ROTR(x, 61) ), SHR(x,  6) )
356fn small_sigma_1(b: &CircuitBuilder, x: Wire) -> Wire {
357	let r1 = b.rotr(x, 19);
358	let r2 = b.rotr(x, 61);
359	let s1 = b.shr(x, 6);
360	let x1 = b.bxor(r1, r2);
361	b.bxor(x1, s1)
362}
363
364#[cfg(test)]
365mod tests {
366	use binius_core::word::Word;
367	use binius_frontend::{CircuitBuilder, Hint, Wire};
368	use proptest::prelude::*;
369
370	use super::{Sha512Compress, State, compress, compress_gates, pack_message_block};
371
372	/// A test circuit that proves a knowledge of preimage for a given state vector S in
373	///
374	///     compress1024(preimage) = S
375	///
376	/// without revealing the preimage, only S.
377	#[test]
378	fn proof_preimage() {
379		// Use the test-vector for SHA512 single block message: "abc".
380		let mut preimage: [u8; 128] = [0; 128];
381		preimage[0..3].copy_from_slice(b"abc");
382		preimage[3] = 0x80;
383		preimage[127] = 0x18;
384
385		#[rustfmt::skip]
386		let expected_state: [u64; 8] = [
387			0xddaf35a193617aba, 0xcc417349ae204131, 0x12e6fa4e89a97ea2, 0x0a9eeee64b55d39a,
388			0x2192992a274fc1a8, 0x36ba3c23a3feebbd, 0x454d4423643ce80e, 0x2a9ac94fa54ca49f,
389		];
390
391		let circuit = CircuitBuilder::new();
392		let state = State::iv(&circuit);
393		let input: [Wire; 16] = std::array::from_fn(|_| circuit.add_witness());
394		let output: [Wire; 8] = std::array::from_fn(|_| circuit.add_inout());
395		let state_out = compress(&circuit, state, input);
396
397		for (i, (actual_x, expected_x)) in state_out.0.iter().zip(output).enumerate() {
398			circuit.assert_eq(format!("preimage_eq[{i}]"), *actual_x, expected_x);
399		}
400
401		let circuit = circuit.build();
402		let cs = circuit.constraint_system();
403		let mut w = circuit.new_witness_filler();
404
405		// Populate the input message for the compression function.
406		pack_message_block(&mut w, &input, preimage);
407
408		for (i, &output) in output.iter().enumerate() {
409			w[output] = Word(expected_state[i]);
410		}
411		circuit.populate_wire_witness(&mut w).unwrap();
412
413		cs.verify(&w.into_value_vec()).unwrap();
414	}
415
416	#[test]
417	fn sha512_chain() {
418		// Tests multiple SHA-512 compress1024 invocations where the outputs are linked to the
419		// inputs of the following compression function.
420		const N: usize = 3;
421		let circuit = CircuitBuilder::new();
422
423		let mut m_wires: Vec<[Wire; 16]> = Vec::with_capacity(N);
424
425		// First, declare the initial state.
426		let mut state = State::iv(&circuit);
427		for i in 0..N {
428			// Create a new subcircuit builder. This is not necessary but can improve readability
429			// and diagnostics.
430			let sha512_builder = circuit.subcircuit(format!("sha512[{i}]"));
431
432			// Build a new instance of the sha512 verification subcircuit, passing the inputs `m` to
433			// it. For the first compression `m` is public but everything else if private.
434			let m: [Wire; 16] = if i == 0 {
435				std::array::from_fn(|_| sha512_builder.add_inout())
436			} else {
437				std::array::from_fn(|_| sha512_builder.add_witness())
438			};
439			state = compress(&sha512_builder, state, m);
440			m_wires.push(m);
441		}
442
443		let circuit = circuit.build();
444		let cs = circuit.constraint_system();
445		let mut w = circuit.new_witness_filler();
446
447		for m in &m_wires {
448			pack_message_block(&mut w, m, [0; 128]);
449		}
450		circuit.populate_wire_witness(&mut w).unwrap();
451
452		cs.verify(&w.into_value_vec()).unwrap();
453	}
454
455	#[test]
456	fn sha512_parallel() {
457		// Test multiple SHA-512 compressions in parallel (no chaining)
458		const N: usize = 3;
459		let circuit = CircuitBuilder::new();
460
461		let mut m_wires: Vec<[Wire; 16]> = Vec::with_capacity(N);
462
463		for i in 0..N {
464			// Create a new subcircuit builder
465			let sha512_builder = circuit.subcircuit(format!("sha512[{i}]"));
466
467			// Each SHA-512 instance gets its own IV and input (all committed)
468			let state = State::iv(&sha512_builder);
469			let m: [Wire; 16] = std::array::from_fn(|_| sha512_builder.add_inout());
470			let _state_out = compress(&sha512_builder, state, m);
471			m_wires.push(m);
472		}
473
474		let circuit = circuit.build();
475		let cs = circuit.constraint_system();
476		let mut w = circuit.new_witness_filler();
477
478		for m in &m_wires {
479			pack_message_block(&mut w, m, [0; 128]);
480		}
481		circuit.populate_wire_witness(&mut w).unwrap();
482		cs.verify(&w.into_value_vec()).unwrap();
483	}
484
485	/// Runs [`compress`] in gates over its flat word interface.
486	fn run_compress_words(inputs: [u64; 24]) -> [u64; 8] {
487		let builder = CircuitBuilder::new();
488		let wires: [Wire; 24] = std::array::from_fn(|_| builder.add_witness());
489		let out = compress_gates(
490			&builder,
491			State(std::array::from_fn(|i| wires[i])),
492			std::array::from_fn(|i| wires[8 + i]),
493		);
494		for wire in out.0 {
495			builder.mark_inout(wire);
496		}
497
498		let circuit = builder.build();
499		let mut w = circuit.new_witness_filler();
500		for (wire, word) in std::iter::zip(wires, inputs) {
501			w[wire] = Word(word);
502		}
503		circuit.populate_wire_witness(&mut w).unwrap();
504
505		std::array::from_fn(|i| w[out.0[i]].as_u64())
506	}
507
508	proptest! {
509		// The chip path takes the outputs from `Sha512Compress::execute` and the chip recomputes
510		// them from its gates, so the two have to agree on every word a circuit can reach them
511		// with. Every SHA-512 word is a full 64-bit value, so random words cover the whole
512		// interface.
513		#[test]
514		fn compress_hint_matches_its_gates(words in prop::collection::vec(any::<u64>(), 24)) {
515			let inputs: [u64; 24] = std::array::from_fn(|i| words[i]);
516
517			let mut hinted = [Word::ZERO; 8];
518			Sha512Compress.execute(&[], &inputs.map(Word), &mut hinted);
519
520			prop_assert_eq!(hinted.map(|word| word.as_u64()), run_compress_words(inputs));
521		}
522	}
523}