Skip to main content

binius_circuits/blake2s/
compress.rs

1// Copyright 2026 The Binius Developers
2//! BLAKE2s compression primitive.
3//!
4//! A BLAKE2s block is 64 bytes: 16 32-bit words.
5//!
6//! The compression function mixes an 8-word chained state with a 16-word message block, a
7//! 64-bit byte counter split into two 32-bit halves, and a finalization flag.
8//!
9//! It produces an updated 8-word state.
10//!
11//! Every gate here is lane-agnostic.
12//!
13//! A 32-bit add, a 32-bit rotate, and a bitwise exclusive-or all act independently on each
14//! 32-bit half of a 64-bit wire.
15//!
16//! That lets one mixing core serve three shapes:
17//!
18//! - One compression, packed into the low 32 bits of each wire.
19//! - Two independent compressions, one per 32-bit lane.
20//! - Two sequential compressions, where the second's input state is the first's output.
21//!
22//! The sequential pair is packed into the same two lanes through a hint that breaks the
23//! circular dependency between them.
24
25use std::{array, iter};
26
27use binius_core::word::Word;
28use binius_frontend::{ChipGadget, CircuitBuilder, Hint, Wire};
29
30use super::constants::{IV, SIGMA};
31use crate::util::clear_high_bits;
32
33/// BLAKE2s G mixing function.
34///
35/// Every operation here is a parallel-halves gate or a bitwise one.
36///
37/// A parallel-halves gate acts independently on each 32-bit half of a 64-bit wire.
38///
39/// So the same code mixes either one compression alone, or two compressions packed side by
40/// side.
41#[allow(clippy::too_many_arguments)]
42fn g(
43	builder: &CircuitBuilder,
44	v: &mut [Wire; 16],
45	a: usize,
46	b: usize,
47	c: usize,
48	d: usize,
49	x: Wire,
50	y: Wire,
51) {
52	// Mix the first message word into a, then rotate d by 16 bits.
53	v[a] = builder.iadd_32(builder.iadd_32(v[a], v[b]), x);
54	v[d] = builder.rotr32(builder.bxor(v[d], v[a]), 16);
55	// Fold d back into c, then rotate b by 12 bits.
56	v[c] = builder.iadd_32(v[c], v[d]);
57	v[b] = builder.rotr32(builder.bxor(v[b], v[c]), 12);
58	// Mix the second message word into a, then rotate d by 8 bits.
59	v[a] = builder.iadd_32(builder.iadd_32(v[a], v[b]), y);
60	v[d] = builder.rotr32(builder.bxor(v[d], v[a]), 8);
61	// Fold d back into c again, then rotate b by 7 bits.
62	v[c] = builder.iadd_32(v[c], v[d]);
63	v[b] = builder.rotr32(builder.bxor(v[b], v[c]), 7);
64}
65
66/// One mixing round.
67///
68/// Four column mixes, followed by four diagonal mixes, using the round's own message-word
69/// schedule.
70fn round(builder: &CircuitBuilder, v: &mut [Wire; 16], m: &[Wire; 16], round_idx: usize) {
71	let s = &SIGMA[round_idx];
72	// Mix the four columns: (0,4,8,12), (1,5,9,13), (2,6,10,14), (3,7,11,15).
73	g(builder, v, 0, 4, 8, 12, m[s[0]], m[s[1]]);
74	g(builder, v, 1, 5, 9, 13, m[s[2]], m[s[3]]);
75	g(builder, v, 2, 6, 10, 14, m[s[4]], m[s[5]]);
76	g(builder, v, 3, 7, 11, 15, m[s[6]], m[s[7]]);
77	// Mix the four diagonals: (0,5,10,15), (1,6,11,12), (2,7,8,13), (3,4,9,14).
78	g(builder, v, 0, 5, 10, 15, m[s[8]], m[s[9]]);
79	g(builder, v, 1, 6, 11, 12, m[s[10]], m[s[11]]);
80	g(builder, v, 2, 7, 8, 13, m[s[12]], m[s[13]]);
81	g(builder, v, 3, 4, 9, 14, m[s[14]], m[s[15]]);
82}
83
84/// The compression's initial 16-word working vector.
85///
86/// The chained state fills the low half.
87///
88/// The IV fills the high half.
89///
90/// The byte counter and the finalization flag are then folded into the last four words.
91///
92/// The `iv` argument carries one lane's worth of IV words for a single compression, or the
93/// same words replicated into both lanes for two packed compressions.
94///
95/// That replication is the only difference between those two shapes.
96fn init_v(
97	builder: &CircuitBuilder,
98	h: [Wire; 8],
99	iv: [Wire; 8],
100	t_lo: Wire,
101	t_hi: Wire,
102	last: Wire,
103) -> [Wire; 16] {
104	let mut v: [Wire; 16] = array::from_fn(|i| if i < 8 { h[i] } else { iv[i - 8] });
105	// Mix the low 32 bits of the byte counter into word 12.
106	v[12] = builder.bxor(v[12], t_lo);
107	// Mix the high 32 bits of the byte counter into word 13.
108	v[13] = builder.bxor(v[13], t_hi);
109	// Mix the finalization flag into word 14: an all-ones flag flips every bit of that word.
110	v[14] = builder.bxor(v[14], last);
111	v
112}
113
114/// Runs the ten mixing rounds, then folds the working vector's two halves back into the state.
115///
116/// Shared by every shape this module offers.
117///
118/// The state and the working vector are single-lane for one compression, or packed two per
119/// wire for a pair.
120///
121/// This code never needs to know which.
122fn compress_core(
123	builder: &CircuitBuilder,
124	h: [Wire; 8],
125	mut v: [Wire; 16],
126	m: [Wire; 16],
127) -> [Wire; 8] {
128	for round_idx in 0..10 {
129		round(builder, &mut v, &m, round_idx);
130	}
131	// Fold the vector's two halves back into the chained state.
132	array::from_fn(|i| builder.bxor(h[i], builder.bxor(v[i], v[i + 8])))
133}
134
135/// BLAKE2s compression function.
136///
137/// # Arguments
138/// * `builder` - Circuit builder.
139/// * `h` - The 8-word chained state, one 32-bit value per wire.
140/// * `m` - The 16-word message block, one 32-bit value per wire.
141/// * `t_lo` - Low 32 bits of the byte counter.
142/// * `t_hi` - High 32 bits of the byte counter.
143/// * `last` - The finalization flag.
144///
145/// All-ones for the final block, zero otherwise.
146///
147/// # Preconditions
148/// The high 32 bits of every input wire must be empty.
149///
150/// Ensuring this is the caller's responsibility.
151///
152/// Violating it leaves the gadget's behavior undefined, and unsafe to rely on.
153///
154/// # Returns
155/// The updated 8-word state.
156///
157/// Every wire's high 32 bits are empty.
158pub fn blake2s_compress(
159	builder: &CircuitBuilder,
160	h: [Wire; 8],
161	m: [Wire; 16],
162	t_lo: Wire,
163	t_hi: Wire,
164	last: Wire,
165) -> [Wire; 8] {
166	// The IV constant occupies only the low 32 bits of each wire, satisfying the precondition
167	// this compression relies on.
168	let iv: [Wire; 8] = array::from_fn(|i| builder.add_constant(Word(IV[i] as u64)));
169	let v = init_v(builder, h, iv, t_lo, t_hi, last);
170	compress_core(builder, h, v, m)
171}
172
173/// BLAKE2s compression function running two independent compressions in parallel.
174///
175/// Each 64-bit input wire packs two 32-bit lanes.
176///
177/// Bits 0 through 31 hold lane 0's word.
178///
179/// Bits 32 through 63 hold lane 1's word.
180///
181/// Every gate the mixing core uses already acts independently on each half.
182///
183/// So the ten mixing rounds compute both compressions for the gate cost of a single one.
184///
185/// # Arguments
186/// Every wire packs two lanes, as described above.
187///
188/// * `h` - The 8-word chained state.
189/// * `m` - The 16-word message block.
190/// * `t_lo` - Low 32 bits of the byte counter.
191/// * `t_hi` - High 32 bits of the byte counter.
192/// * `last` - The finalization flag.
193///
194/// # Returns
195/// The updated 8-word state.
196///
197/// Each wire packs both lanes' results.
198///
199/// # Chips
200/// This gadget can be registered as a chip.
201///
202/// Doing so turns every paired compression under it into a chip call, including the ones a
203/// sequential pairing and the fixed-length hasher reach.
204pub fn blake2s_compress_2x(
205	builder: &CircuitBuilder,
206	h: [Wire; 8],
207	m: [Wire; 16],
208	t_lo: Wire,
209	t_hi: Wire,
210	last: Wire,
211) -> [Wire; 8] {
212	let inputs: Vec<Wire> = h.into_iter().chain(m).chain([t_lo, t_hi, last]).collect();
213	let outputs = builder.build_gadget(Blake2sCompress2x, &[], &inputs);
214	array::from_fn(|i| outputs[i])
215}
216
217/// The two-lane compression above, in a form a circuit can register as a chip.
218///
219/// Its interface is the flat 27 input words: the 8-word state, the 16-word message block, the
220/// counter's low half, the counter's high half, and the finalization flag.
221///
222/// The 8 output words pack two lanes the same way the inputs do.
223pub struct Blake2sCompress2x;
224
225impl Hint for Blake2sCompress2x {
226	const NAME: &'static str = "binius.blake2s_compress_2x";
227
228	fn shape(&self, _dimensions: &[usize]) -> (usize, usize) {
229		(27, 8)
230	}
231
232	fn execute(&self, _dimensions: &[usize], inputs: &[Word], outputs: &mut [Word]) {
233		// Every input word packs one lane per 32-bit half, and the two halves never interact.
234		//
235		// Every add, rotate, and mix this core uses is either 32-bit or bitwise.
236		//
237		// So computing one lane is exactly a plain compression of that lane's own words.
238		let compress_lane = |i: usize| {
239			let lane = |word: Word| (word.as_u64() >> (32 * i)) as u32;
240			let h: [u32; 8] = array::from_fn(|j| lane(inputs[j]));
241			let m: [u32; 16] = array::from_fn(|j| lane(inputs[8 + j]));
242			ref_compress(h, m, lane(inputs[24]), lane(inputs[25]), lane(inputs[26]))
243		};
244
245		// Compute both lanes, then repack them into one word per output.
246		let (lane_0, lane_1) = (compress_lane(0), compress_lane(1));
247		for (slot, (low, high)) in iter::zip(outputs, iter::zip(lane_0, lane_1)) {
248			*slot = Word(low as u64 | ((high as u64) << 32));
249		}
250	}
251}
252
253impl ChipGadget for Blake2sCompress2x {
254	fn build(&self, builder: &CircuitBuilder, _dimensions: &[usize], inputs: &[Wire]) -> Vec<Wire> {
255		// Split the flat input list back into the state and the message block, then run the
256		// shared gate path a direct call would use.
257		let h: [Wire; 8] = array::from_fn(|i| inputs[i]);
258		let m: [Wire; 16] = array::from_fn(|i| inputs[8 + i]);
259		compress_2x_gates(builder, h, m, inputs[24], inputs[25], inputs[26]).to_vec()
260	}
261}
262
263/// The two-lane compression, expressed directly in gates.
264///
265/// Used both by a direct call and by the chip's gate path, so the two stay identical by
266/// construction.
267fn compress_2x_gates(
268	builder: &CircuitBuilder,
269	h: [Wire; 8],
270	m: [Wire; 16],
271	t_lo: Wire,
272	t_hi: Wire,
273	last: Wire,
274) -> [Wire; 8] {
275	// Replicate the IV into both 32-bit halves, so each lane mixes in its own copy.
276	let iv_2x: [Wire; 8] = array::from_fn(|i| {
277		let w = IV[i] as u64;
278		builder.add_constant(Word(w | (w << 32)))
279	});
280	let v = init_v(builder, h, iv_2x, t_lo, t_hi, last);
281	compress_core(builder, h, v, m)
282}
283
284/// Two sequential BLAKE2s block compressions, evaluated in one parallel core.
285///
286/// The second block's compression takes the first block's output state as its own input
287/// state.
288///
289/// Both compressions run as the two 32-bit lanes of a single parallel compression.
290///
291/// ```text
292///     high lane [32:64]:  S1 = compress(input state, first block)
293///     low  lane [0:32] :  S2 = compress(S1,          second block)
294/// ```
295///
296/// So two chained blocks cost one compression, instead of two.
297///
298/// The two lanes run at the same time.
299///
300/// Yet the low lane needs the high lane's *output* as its own input, before that output
301/// exists.
302///
303/// A hint breaks this circular dependency by computing that output off-circuit first.
304///
305/// The hinted value seeds the low lane's input.
306///
307/// It is then constrained two ways, so it cannot lie:
308///
309/// - Its high half must equal the real input state.
310/// - Its low half must equal what the first compression actually produces in-circuit.
311///
312/// # Arguments
313/// * `builder` - Circuit builder.
314/// * `h` - The 8-word input state for the first compression, one 32-bit value per wire.
315/// * `blocks` - The two 16-word message blocks.
316///
317/// The first feeds the first compression, the second feeds the second.
318///
319/// * `t_los` - Each compression's low 32 bits of its byte counter.
320/// * `t_his` - Each compression's high 32 bits of its byte counter.
321/// * `lasts` - Each compression's finalization flag.
322///
323/// # Preconditions
324/// Every input wire holds a valid 32-bit value in its low 32 bits.
325///
326/// High halves need not be empty:
327///
328/// - `h`'s high half is discarded by the shift that lifts it into the high lane.
329/// - The first block's words, and the first compression's counter and flag, are likewise only ever
330///   shifted, never read directly.
331/// - The second block's words, and the second compression's counter and flag, are masked before
332///   use.
333///
334/// # Returns
335/// 8 wires, each packing both output states.
336///
337/// - Low 32 bits: the second compression's output.
338/// - High 32 bits: the first compression's output.
339pub fn blake2s_compress_2x_seq(
340	builder: &CircuitBuilder,
341	h: [Wire; 8],
342	blocks: [[Wire; 16]; 2],
343	t_los: [Wire; 2],
344	t_his: [Wire; 2],
345	lasts: [Wire; 2],
346) -> [Wire; 8] {
347	// The hint returns the merged state directly, one word at a time:
348	//
349	//     low 32 bits : first compression's output  = second compression's input
350	//     high 32 bits: first compression's input state word
351	//
352	// Both halves are re-derived and constrained below, so the hint itself need not be
353	// trusted.
354	let mut hint_inputs = Vec::with_capacity(27);
355	hint_inputs.extend_from_slice(&h);
356	hint_inputs.extend_from_slice(&blocks[0]);
357	hint_inputs.push(t_los[0]);
358	hint_inputs.push(t_his[0]);
359	hint_inputs.push(lasts[0]);
360	let merged_vec = builder.call_hint(Blake2sCompressHint, &[], &hint_inputs);
361	let merged: [Wire; 8] = array::from_fn(|i| merged_vec[i]);
362
363	// Pack a lane pair into one wire: the low 32 bits hold lane 0, the high 32 bits hold
364	// lane 1.
365	//
366	// Shifting left by 32 already clears the shifted operand's own high bits.
367	//
368	// The operand placed in the low half is cleared explicitly, since nothing here guarantees
369	// its high bits start empty.
370	let pack = |lo: Wire, hi: Wire| builder.bxor(lo, builder.shl(hi, 32));
371	let clear = |w: Wire| clear_high_bits(builder, w, 32);
372
373	// Merge each pair of per-compression values into its own two-lane wire: the second
374	// compression's value in the low lane, the first's in the high lane.
375	let merged_block: [Wire; 16] = array::from_fn(|i| pack(clear(blocks[1][i]), blocks[0][i]));
376	let merged_t_lo = pack(clear(t_los[1]), t_los[0]);
377	let merged_t_hi = pack(clear(t_his[1]), t_his[0]);
378	let merged_last = pack(clear(lasts[1]), lasts[0]);
379
380	let out =
381		blake2s_compress_2x(builder, merged, merged_block, merged_t_lo, merged_t_hi, merged_last);
382
383	// Bind the hinted state, one 64-bit equality per word.
384	//
385	// A single equality pins both halves at once, since they never overlap.
386	//
387	//     hinted word:  [ high lane = input state | low lane = S1 output ]
388	//                          must equal                must equal
389	//                     input state << 32       ^     result >> 32
390	//
391	// Together these leave the hint no freedom:
392	//
393	// - The high lane provably compresses the caller's own input state.
394	// - The low lane provably chains from what the first compression really produced.
395	//
396	// Each side is one shift of an already-committed word, so nothing needs masking:
397	//
398	// - Shifting up discards the input's own high bits.
399	// - Shifting down discards the result's low bits.
400	for (m, (s, o)) in iter::zip(merged, iter::zip(h, out)) {
401		let expected = builder.bxor(builder.shl(s, 32), builder.shr(o, 32));
402		builder.assert_eq("blake2s_compress_2x_seq.merged_state", m, expected);
403	}
404
405	out
406}
407
408/// Precomputes the merged input state for the sequential two-lane compression above.
409///
410/// It runs the first compression off-circuit, then packs each output word to seed both
411/// lanes at once.
412///
413/// - Low 32 bits: the first compression's output, which is the second compression's input.
414/// - High 32 bits: the first compression's input state word.
415///
416/// Both halves are re-derived and constrained in-circuit, so this hint only needs to be
417/// honest, not trusted.
418///
419/// # Input layout
420/// 27 words, with the value in the low 32 bits of each.
421///
422/// - Words 0 through 7: the input state.
423/// - Words 8 through 23: the first message block.
424/// - Word 24: the first compression's low counter half.
425/// - Word 25: the first compression's high counter half.
426/// - Word 26: the first compression's finalization flag.
427struct Blake2sCompressHint;
428
429impl Hint for Blake2sCompressHint {
430	const NAME: &'static str = "binius.blake2s_compress";
431
432	fn shape(&self, _dimensions: &[usize]) -> (usize, usize) {
433		(27, 8)
434	}
435
436	fn execute(&self, _dimensions: &[usize], inputs: &[Word], outputs: &mut [Word]) {
437		// Read the first compression's inputs out of the flat 27-word layout.
438		let h: [u32; 8] = array::from_fn(|i| inputs[i].as_u64() as u32);
439		let m: [u32; 16] = array::from_fn(|i| inputs[8 + i].as_u64() as u32);
440		let t_lo = inputs[24].as_u64() as u32;
441		let t_hi = inputs[25].as_u64() as u32;
442		let last = inputs[26].as_u64() as u32;
443
444		// Pack each output word with the compression's result in the low half and the
445		// original input state word in the high half.
446		let out = ref_compress(h, m, t_lo, t_hi, last);
447		for (i, slot) in outputs.iter_mut().enumerate() {
448			*slot = Word(out[i] as u64 | ((h[i] as u64) << 32));
449		}
450	}
451}
452
453/// Pure-Rust BLAKE2s compression of a single 64-byte block.
454///
455/// Matches the in-circuit compression exactly, per RFC 7693 Section 3.2.
456///
457/// Used for prover-side witness generation, and as the test reference.
458///
459/// # Arguments
460/// * `h` - The 8-word input state.
461/// * `m` - The 16-word message block.
462/// * `t_lo` - Low 32 bits of the byte counter.
463/// * `t_hi` - High 32 bits of the byte counter.
464/// * `last` - The finalization flag.
465///
466/// All-ones for the final block, zero otherwise.
467///
468/// # Returns
469/// The updated 8-word state.
470pub fn ref_compress(h: [u32; 8], m: [u32; 16], t_lo: u32, t_hi: u32, last: u32) -> [u32; 8] {
471	const fn ref_g(v: &mut [u32; 16], a: usize, b: usize, c: usize, d: usize, x: u32, y: u32) {
472		// Mix the first message word into a, then rotate d by 16 bits.
473		v[a] = v[a].wrapping_add(v[b]).wrapping_add(x);
474		v[d] = (v[d] ^ v[a]).rotate_right(16);
475		// Fold d back into c, then rotate b by 12 bits.
476		v[c] = v[c].wrapping_add(v[d]);
477		v[b] = (v[b] ^ v[c]).rotate_right(12);
478		// Mix the second message word into a, then rotate d by 8 bits.
479		v[a] = v[a].wrapping_add(v[b]).wrapping_add(y);
480		v[d] = (v[d] ^ v[a]).rotate_right(8);
481		// Fold d back into c again, then rotate b by 7 bits.
482		v[c] = v[c].wrapping_add(v[d]);
483		v[b] = (v[b] ^ v[c]).rotate_right(7);
484	}
485
486	let mut v = [0u32; 16];
487	// The chained state fills the low half of the working vector, the IV the high half.
488	v[..8].copy_from_slice(&h);
489	v[8..].copy_from_slice(&IV);
490	// Mix the byte counter and the finalization flag into the last four words.
491	v[12] ^= t_lo;
492	v[13] ^= t_hi;
493	v[14] ^= last;
494
495	// Ten rounds: four column mixes, then four diagonal mixes, per round.
496	for s in &SIGMA {
497		ref_g(&mut v, 0, 4, 8, 12, m[s[0]], m[s[1]]);
498		ref_g(&mut v, 1, 5, 9, 13, m[s[2]], m[s[3]]);
499		ref_g(&mut v, 2, 6, 10, 14, m[s[4]], m[s[5]]);
500		ref_g(&mut v, 3, 7, 11, 15, m[s[6]], m[s[7]]);
501		ref_g(&mut v, 0, 5, 10, 15, m[s[8]], m[s[9]]);
502		ref_g(&mut v, 1, 6, 11, 12, m[s[10]], m[s[11]]);
503		ref_g(&mut v, 2, 7, 8, 13, m[s[12]], m[s[13]]);
504		ref_g(&mut v, 3, 4, 9, 14, m[s[14]], m[s[15]]);
505	}
506
507	// Fold the vector's two halves back into the chained state.
508	array::from_fn(|i| h[i] ^ v[i] ^ v[i + 8])
509}
510
511#[cfg(test)]
512mod tests {
513	use binius_frontend::CircuitBuilder;
514	use hex_literal::hex;
515	use proptest::prelude::*;
516
517	use super::*;
518
519	// Circuit-level tests.
520
521	// Builds a circuit around the single-lane compression, populates the witness with the
522	// given values, and returns the evaluated 8-word output.
523	//
524	// Every input is fed in with an empty high half, satisfying the gadget's precondition.
525	fn run_compress(h: [u32; 8], m: [u32; 16], t_lo: u32, t_hi: u32, last: u32) -> [u32; 8] {
526		let builder = CircuitBuilder::new();
527		let h_wires: [Wire; 8] = array::from_fn(|_| builder.add_witness());
528		let m_wires: [Wire; 16] = array::from_fn(|_| builder.add_witness());
529		let t_lo_w = builder.add_witness();
530		let t_hi_w = builder.add_witness();
531		let last_w = builder.add_witness();
532
533		// Wire the gadget under test, and pin its output to a public value the witness fills
534		// with the reference result: a disagreement then surfaces as a failure to populate.
535		let out = blake2s_compress(&builder, h_wires, m_wires, t_lo_w, t_hi_w, last_w);
536		let out_inout: [Wire; 8] = array::from_fn(|_| builder.add_inout());
537		for i in 0..8 {
538			builder.assert_eq("out_match", out[i], out_inout[i]);
539		}
540
541		let circuit = builder.build();
542		let mut w = circuit.new_witness_filler();
543		for i in 0..8 {
544			w[h_wires[i]] = Word(h[i] as u64);
545		}
546		for i in 0..16 {
547			w[m_wires[i]] = Word(m[i] as u64);
548		}
549		w[t_lo_w] = Word(t_lo as u64);
550		w[t_hi_w] = Word(t_hi as u64);
551		w[last_w] = Word(last as u64);
552
553		let expected = ref_compress(h, m, t_lo, t_hi, last);
554		for i in 0..8 {
555			w[out_inout[i]] = Word(expected[i] as u64);
556		}
557		circuit.populate_wire_witness(&mut w).unwrap();
558		array::from_fn(|i| w[out_inout[i]].0 as u32)
559	}
560
561	#[test]
562	fn rfc7693_appendix_b_trace_matches_spec() {
563		// Trace from RFC 7693 Appendix B: the unkeyed BLAKE2s-256 compression of "abc".
564		//
565		// A 3-byte message is one block.
566		//
567		// So the whole hash is a single compression, run from the parameter-mixed IV.
568		//
569		// This pins the compression against the specification text itself.
570		//
571		// It therefore holds even if the reference crate and this circuit were wrong
572		// together.
573
574		// h[0] carries the parameter block: unkeyed (key length 0), 32-byte digest.
575		let h: [u32; 8] = [
576			IV[0] ^ 0x0101_0020,
577			IV[1],
578			IV[2],
579			IV[3],
580			IV[4],
581			IV[5],
582			IV[6],
583			IV[7],
584		];
585		// "abc" packed little-endian into the first message word; the rest of the block is
586		// zero.
587		let mut m = [0u32; 16];
588		m[0] = 0x0063_6261;
589		// A single block carries the whole 3-byte length, and the finalization flag.
590		let (t_lo, t_hi, last) = (3u32, 0u32, 0xFFFF_FFFFu32);
591
592		let expected: [u32; 8] = [
593			0x8C5E_8C50,
594			0xE214_7C32,
595			0xA32B_A7E1,
596			0x2F45_EB4E,
597			0x208B_4537,
598			0x293A_D69E,
599			0x4C9B_994D,
600			0x8259_6786,
601		];
602		assert_eq!(ref_compress(h, m, t_lo, t_hi, last), expected);
603		assert_eq!(run_compress(h, m, t_lo, t_hi, last), expected);
604
605		// The published digest is these words read little-endian, byte for byte.
606		let digest_bytes: Vec<u8> = expected.iter().flat_map(|w| w.to_le_bytes()).collect();
607		assert_eq!(
608			digest_bytes,
609			hex!("508c5e8c327c14e2e1a72ba34eeb452f37458b209ed63a294d999b4c86675982")
610		);
611	}
612
613	// 2x SIMD tests.
614
615	fn pack2x(lo: u32, hi: u32) -> u64 {
616		(lo as u64) | ((hi as u64) << 32)
617	}
618
619	fn unpack2x(w: u64) -> (u32, u32) {
620		(w as u32, (w >> 32) as u32)
621	}
622
623	// Runs the two-lane compression with two independent per-lane inputs, and returns the two
624	// per-lane 8-word outputs.
625	fn run_compress_2x(
626		h: [[u32; 8]; 2],
627		m: [[u32; 16]; 2],
628		t_lo: [u32; 2],
629		t_hi: [u32; 2],
630		last: [u32; 2],
631	) -> [[u32; 8]; 2] {
632		let builder = CircuitBuilder::new();
633		let h_wires: [Wire; 8] = array::from_fn(|_| builder.add_witness());
634		let m_wires: [Wire; 16] = array::from_fn(|_| builder.add_witness());
635		let t_lo_w = builder.add_witness();
636		let t_hi_w = builder.add_witness();
637		let last_w = builder.add_witness();
638
639		let out = blake2s_compress_2x(&builder, h_wires, m_wires, t_lo_w, t_hi_w, last_w);
640		let out_inout: [Wire; 8] = array::from_fn(|_| builder.add_inout());
641		for i in 0..8 {
642			builder.assert_eq("out_match_2x", out[i], out_inout[i]);
643		}
644
645		// Pack each pair of per-lane values into its two-lane wire before populating.
646		let circuit = builder.build();
647		let mut w = circuit.new_witness_filler();
648		for i in 0..8 {
649			w[h_wires[i]] = Word(pack2x(h[0][i], h[1][i]));
650		}
651		for i in 0..16 {
652			w[m_wires[i]] = Word(pack2x(m[0][i], m[1][i]));
653		}
654		w[t_lo_w] = Word(pack2x(t_lo[0], t_lo[1]));
655		w[t_hi_w] = Word(pack2x(t_hi[0], t_hi[1]));
656		w[last_w] = Word(pack2x(last[0], last[1]));
657
658		let exp0 = ref_compress(h[0], m[0], t_lo[0], t_hi[0], last[0]);
659		let exp1 = ref_compress(h[1], m[1], t_lo[1], t_hi[1], last[1]);
660		for i in 0..8 {
661			w[out_inout[i]] = Word(pack2x(exp0[i], exp1[i]));
662		}
663		circuit.populate_wire_witness(&mut w).unwrap();
664
665		// Unpack the two lanes of the evaluated result back into separate per-lane arrays.
666		let mut actual = [[0u32; 8]; 2];
667		for i in 0..8 {
668			let (lo, hi) = unpack2x(w[out_inout[i]].0);
669			actual[0][i] = lo;
670			actual[1][i] = hi;
671		}
672		actual
673	}
674
675	#[test]
676	fn compress_2x_distinct_lanes() {
677		// Lane 0 reruns the RFC trace.
678		//
679		// Lane 1 runs a completely different chained-state and message-block pair.
680		//
681		// This confirms the lanes are independent: no bits cross the 32-bit boundary.
682		let h0: [u32; 8] = [
683			IV[0] ^ 0x0101_0020,
684			IV[1],
685			IV[2],
686			IV[3],
687			IV[4],
688			IV[5],
689			IV[6],
690			IV[7],
691		];
692		let mut m0 = [0u32; 16];
693		m0[0] = 0x0063_6261;
694
695		let h1: [u32; 8] = [
696			0xDEAD_BEEF,
697			0xCAFE_BABE,
698			0x1234_5678,
699			0x9ABC_DEF0,
700			0x0BAD_F00D,
701			0xFEED_FACE,
702			0x0123_4567,
703			0x89AB_CDEF,
704		];
705		let m1: [u32; 16] = array::from_fn(|i| (i as u32).wrapping_mul(0x0101_0101));
706
707		let actual = run_compress_2x([h0, h1], [m0, m1], [3, 64], [0, 0], [0xFFFF_FFFF, 0]);
708		assert_eq!(actual[0], ref_compress(h0, m0, 3, 0, 0xFFFF_FFFF));
709		assert_eq!(actual[1], ref_compress(h1, m1, 64, 0, 0));
710	}
711
712	#[test]
713	fn compress_2x_lane_independence() {
714		// Lane 0 is the RFC trace.
715		//
716		// Lane 1 is an all-zero block, compressed from the same parameter-mixed IV.
717		//
718		// Each lane must match its own reference.
719		//
720		// That proves the zero lane does not perturb the "abc" lane, and vice versa.
721		let h: [u32; 8] = [
722			IV[0] ^ 0x0101_0020,
723			IV[1],
724			IV[2],
725			IV[3],
726			IV[4],
727			IV[5],
728			IV[6],
729			IV[7],
730		];
731		let mut m = [0u32; 16];
732		m[0] = 0x0063_6261;
733		let actual =
734			run_compress_2x([h, h], [m, [0u32; 16]], [3, 0], [0, 0], [0xFFFF_FFFF, 0xFFFF_FFFF]);
735		assert_eq!(actual[0], ref_compress(h, m, 3, 0, 0xFFFF_FFFF));
736		assert_eq!(actual[1], ref_compress(h, [0u32; 16], 0, 0, 0xFFFF_FFFF));
737	}
738
739	// 2x sequential tests.
740
741	// Runs the sequential two-lane compression, and returns the second and first compression
742	// outputs, unpacked from the low and high lanes of the packed result.
743	#[allow(clippy::too_many_arguments)]
744	fn run_compress_2x_seq(
745		h: [u32; 8],
746		m1: [u32; 16],
747		m2: [u32; 16],
748		t_lo1: u32,
749		t_hi1: u32,
750		last1: u32,
751		t_lo2: u32,
752		t_hi2: u32,
753		last2: u32,
754	) -> ([u32; 8], [u32; 8]) {
755		let builder = CircuitBuilder::new();
756		let h_wires: [Wire; 8] = array::from_fn(|_| builder.add_witness());
757		let m1_wires: [Wire; 16] = array::from_fn(|_| builder.add_witness());
758		let m2_wires: [Wire; 16] = array::from_fn(|_| builder.add_witness());
759		let t_lo1_w = builder.add_witness();
760		let t_hi1_w = builder.add_witness();
761		let last1_w = builder.add_witness();
762		let t_lo2_w = builder.add_witness();
763		let t_hi2_w = builder.add_witness();
764		let last2_w = builder.add_witness();
765
766		let out = blake2s_compress_2x_seq(
767			&builder,
768			h_wires,
769			[m1_wires, m2_wires],
770			[t_lo1_w, t_lo2_w],
771			[t_hi1_w, t_hi2_w],
772			[last1_w, last2_w],
773		);
774		let out_inout: [Wire; 8] = array::from_fn(|_| builder.add_inout());
775		for i in 0..8 {
776			builder.assert_eq("out_match_2x_seq", out[i], out_inout[i]);
777		}
778
779		let circuit = builder.build();
780		let mut w = circuit.new_witness_filler();
781		for i in 0..8 {
782			w[h_wires[i]] = Word(h[i] as u64);
783		}
784		for i in 0..16 {
785			w[m1_wires[i]] = Word(m1[i] as u64);
786			w[m2_wires[i]] = Word(m2[i] as u64);
787		}
788		w[t_lo1_w] = Word(t_lo1 as u64);
789		w[t_hi1_w] = Word(t_hi1 as u64);
790		w[last1_w] = Word(last1 as u64);
791		w[t_lo2_w] = Word(t_lo2 as u64);
792		w[t_hi2_w] = Word(t_hi2 as u64);
793		w[last2_w] = Word(last2 as u64);
794
795		// The expected result chains two plain compressions: the first compression's output
796		// becomes the second compression's input state.
797		let s1 = ref_compress(h, m1, t_lo1, t_hi1, last1);
798		let s2 = ref_compress(s1, m2, t_lo2, t_hi2, last2);
799		for i in 0..8 {
800			w[out_inout[i]] = Word(pack2x(s2[i], s1[i]));
801		}
802		circuit.populate_wire_witness(&mut w).unwrap();
803
804		let mut s2_out = [0u32; 8];
805		let mut s1_out = [0u32; 8];
806		for i in 0..8 {
807			let (lo, hi) = unpack2x(w[out_inout[i]].0);
808			s2_out[i] = lo;
809			s1_out[i] = hi;
810		}
811		(s2_out, s1_out)
812	}
813
814	#[test]
815	fn compress_2x_seq_chains_two_blocks() {
816		// A two-block message: the first block is not final, the second is.
817		let h: [u32; 8] = [
818			IV[0] ^ 0x0101_0020,
819			IV[1],
820			IV[2],
821			IV[3],
822			IV[4],
823			IV[5],
824			IV[6],
825			IV[7],
826		];
827		let m1 = [0u32; 16];
828		let m2: [u32; 16] = array::from_fn(|i| i as u32);
829		let (s2, s1) = run_compress_2x_seq(h, m1, m2, 64, 0, 0, 100, 0, 0xFFFF_FFFF);
830		let exp_s1 = ref_compress(h, m1, 64, 0, 0);
831		let exp_s2 = ref_compress(exp_s1, m2, 100, 0, 0xFFFF_FFFF);
832		assert_eq!(s1, exp_s1);
833		assert_eq!(s2, exp_s2);
834	}
835
836	#[test]
837	fn compress_2x_seq_distinct_params() {
838		// Invariant: the hinted first output is bound twice.
839		//
840		// Once as the second compression's input state.
841		//
842		// Once against the first compression's own in-circuit output.
843		//
844		// Fixture: a non-IV starting state and two unrelated blocks exercise the full lane
845		// packing.
846		let h: [u32; 8] = [
847			0xDEAD_BEEF,
848			0xCAFE_BABE,
849			0x1234_5678,
850			0x9ABC_DEF0,
851			0x0BAD_F00D,
852			0xFEED_FACE,
853			0x0123_4567,
854			0x89AB_CDEF,
855		];
856		let m1: [u32; 16] = array::from_fn(|i| (i as u32).wrapping_mul(0xDEAD_BEEF));
857		let m2: [u32; 16] = array::from_fn(|i| (i as u32).wrapping_mul(0x0101_0101));
858		let (s2, s1) = run_compress_2x_seq(h, m1, m2, 64, 0, 0, 40, 0, 0xFFFF_FFFF);
859		let exp_s1 = ref_compress(h, m1, 64, 0, 0);
860		let exp_s2 = ref_compress(exp_s1, m2, 40, 0, 0xFFFF_FFFF);
861		assert_eq!(s1, exp_s1);
862		assert_eq!(s2, exp_s2);
863	}
864
865	// Runs the two-lane compression's own gate path directly, over its flat packed-word
866	// interface, bypassing the chip machinery.
867	fn run_compress_2x_words(inputs: [u64; 27]) -> [u64; 8] {
868		let builder = CircuitBuilder::new();
869		let wires: [Wire; 27] = array::from_fn(|_| builder.add_witness());
870		let out = compress_2x_gates(
871			&builder,
872			array::from_fn(|i| wires[i]),
873			array::from_fn(|i| wires[8 + i]),
874			wires[24],
875			wires[25],
876			wires[26],
877		);
878		for wire in out {
879			builder.mark_inout(wire);
880		}
881
882		let circuit = builder.build();
883		let mut w = circuit.new_witness_filler();
884		for (wire, word) in iter::zip(wires, inputs) {
885			w[wire] = Word(word);
886		}
887		circuit.populate_wire_witness(&mut w).unwrap();
888
889		array::from_fn(|i| w[out[i]].as_u64())
890	}
891
892	// A 32-bit word, weighted towards the values that stress carry propagation.
893	//
894	// One case in four is drawn from the boundary set rather than uniformly.
895	//
896	// - Zero and one exercise the shortest carry chains.
897	// - All-ones makes every position carry.
898	// - The lone top bit and the all-ones-below-it value straddle the lane boundary.
899	fn word32() -> impl Strategy<Value = u32> {
900		prop_oneof![
901			3 => any::<u32>(),
902			1 => prop_oneof![Just(0), Just(1), Just(u32::MAX), Just(1 << 31), Just(u32::MAX >> 1)],
903		]
904	}
905
906	fn h8() -> impl Strategy<Value = [u32; 8]> {
907		prop::array::uniform8(word32())
908	}
909
910	fn block16() -> impl Strategy<Value = [u32; 16]> {
911		prop::array::uniform16(word32())
912	}
913
914	proptest! {
915		// Every case compiles and evaluates a whole compression circuit.
916		//
917		// So the sample stays small, and the boundary weighting above carries the coverage.
918		#![proptest_config(ProptestConfig::with_cases(16))]
919
920		#[test]
921		fn compress_matches_reference(
922			h in h8(), m in block16(), t_lo in any::<u32>(), t_hi in any::<u32>(), last in any::<u32>(),
923		) {
924			prop_assert_eq!(run_compress(h, m, t_lo, t_hi, last), ref_compress(h, m, t_lo, t_hi, last));
925		}
926
927		#[test]
928		fn compress_2x_lanes_are_independent(
929			h0 in h8(), h1 in h8(), m0 in block16(), m1 in block16(),
930			t0 in any::<u32>(), t1 in any::<u32>(), l0 in any::<u32>(), l1 in any::<u32>(),
931		) {
932			// Invariant: the two lanes share one core, but must not leak into each other.
933			//
934			// Every parameter differs per lane, so any carry or rotate crossing bit 32 shows
935			// up.
936			let actual = run_compress_2x([h0, h1], [m0, m1], [t0, t1], [0, 0], [l0, l1]);
937			prop_assert_eq!(actual[0], ref_compress(h0, m0, t0, 0, l0));
938			prop_assert_eq!(actual[1], ref_compress(h1, m1, t1, 0, l1));
939		}
940
941		#[test]
942		fn compress_2x_hint_matches_its_gates(words in prop::collection::vec(any::<u64>(), 27)) {
943			// Invariant: the chip's off-circuit computation and its in-circuit gate path
944			// must agree on every word a circuit can reach them with.
945			//
946			// A random 64-bit word is a valid lane pair here, so this covers the whole
947			// interface.
948			let inputs: [u64; 27] = array::from_fn(|i| words[i]);
949
950			let mut hinted = [Word::ZERO; 8];
951			Blake2sCompress2x.execute(&[], &inputs.map(Word), &mut hinted);
952
953			prop_assert_eq!(hinted.map(|word| word.as_u64()), run_compress_2x_words(inputs));
954		}
955
956		#[test]
957		fn compress_2x_seq_matches_two_chained_references(
958			h in h8(), m1 in block16(), m2 in block16(),
959			t1 in any::<u32>(), l1 in any::<u32>(), t2 in any::<u32>(), l2 in any::<u32>(),
960		) {
961			// Invariant: the two lanes run one after the other, not side by side.
962			//
963			// The second compression's input state is the first one's output.
964			//
965			//     h --block 1--> S1 --block 2--> S2
966			//
967			// The first output arrives through a hint, so this is what pins that hint honest.
968			let (s2, s1) = run_compress_2x_seq(h, m1, m2, t1, 0, l1, t2, 0, l2);
969			let exp_s1 = ref_compress(h, m1, t1, 0, l1);
970			prop_assert_eq!(s1, exp_s1);
971			prop_assert_eq!(s2, ref_compress(exp_s1, m2, t2, 0, l2));
972		}
973	}
974}