Skip to main content

binius_circuits/blake3/
compress.rs

1// Copyright 2025 Irreducible Inc.
2// Copyright 2026 The Binius Developers
3//! BLAKE3 compression primitive.
4//!
5//! A BLAKE3 block is 64 bytes (16 × 32-bit words). The compression function mixes an
6//! 8-word chaining value with a 16-word message block, a 64-bit block counter, a byte
7//! count, and a flags word, producing an updated 8-word chaining value.
8//!
9//! The structure mirrors the [reference implementation] from the BLAKE3 crate.
10//!
11//! Every gadget here fills both 32-bit lanes of a word. Where two compressions are available they
12//! run side by side, one per lane ([`blake3_compress_2x`], [`blake3_compress_2x_seq`]); a lone
13//! compression splits its own round sequence across the lanes instead ([`blake3_compress`]).
14//!
15//! [reference implementation]: https://github.com/BLAKE3-team/BLAKE3/blob/master/src/portable.rs
16
17use 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
25/// Rounds [`blake3_compress`] runs in the high lane, the first half of the split.
26const HIGH_LANE_ROUNDS: usize = 3;
27
28/// Rounds [`blake3_compress`] runs in the low lane, the second half of the split.
29///
30/// The longer half, so it also counts the packed rounds: 8 lane-rounds are evaluated for 7 real
31/// ones, and the high lane spends the last of them idle.
32const LOW_LANE_ROUNDS: usize = 7 - HIGH_LANE_ROUNDS;
33
34/// BLAKE3 compression function.
35///
36/// A single compression, evaluated as two lanes of one packed core: the round sequence is cut in
37/// two and the halves run side by side rather than one after the other.
38///
39/// ```text
40///     high lane (bits [32:64]):  round 0 -> round 1 -> round 2 -> (idle)
41///     low  lane (bits  [0:32]):  round 3 -> round 4 -> round 5 -> round 6
42/// ```
43///
44/// Both halves of the split want the same thing: the state after round 2, which is the low lane's
45/// input and the high lane's output. That circular dependency is broken with a
46/// `Blake3RoundSplitHint` that computes the state off-circuit and feeds it in, then constrains it
47/// word-for-word against what the high lane really computed, so the hint cannot lie.
48///
49/// Four packed rounds replace seven single-lane ones. The high lane sits idle through the last of
50/// them — 7 rounds do not divide evenly in two — and its result is discarded.
51///
52/// # Arguments
53///
54/// - `cv`: 8 chaining-value words (32-bit each, stored in the low 32 bits of each wire).
55/// - `block`: 16 message words (32-bit each, low 32 bits of each wire, little-endian).
56/// - `counter`: the 64-bit block counter. Low 32 bits are `t_low`, high 32 are `t_high`. The wire
57///   may carry either a genuinely-64-bit counter (multi-chunk) or a 32-bit value with zero high
58///   half (single-chunk).
59/// - `block_len`: byte count for this block, 0..=64. 32-bit value in low 32 bits.
60/// - `flags`: domain-separation flags. 32-bit value in low 32 bits.
61///
62/// # Preconditions
63///
64/// - Every 32-bit input holds its value in the low 32 bits; `counter` is a full 64-bit value.
65/// - High halves need not be empty.
66///   - A message word's high half is masked off.
67///   - Every other input's is discarded by the shift that lifts it into the high lane.
68///
69/// # Returns
70///
71/// The updated 8-word chaining value in the low 32 bits of each word. The high 32 bits carry the
72/// high lane's discarded round and are not cleared, so a caller that reads them must mask them off
73/// — the same treatment a pair's first compression gets from [`blake3_compress_2x_seq`].
74pub 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	// The hint returns the *merged* initial state directly: each word packs the state after round
83	// `HIGH_LANE_ROUNDS - 1` in the low 32 bits (the low lane's input) and the compression's own
84	// initial state word in the high 32 bits (the high lane's input). Both halves are constrained
85	// below, so the hint itself is untrusted.
86	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	// The compression's own initial state, each word already shifted up into the high lane. This is
96	// what the hinted state is checked against, and the shift is why a dirty high half on an input
97	// wire is harmless: it is shifted out rather than masked off.
98	let up = |w: Wire| builder.shl(w, 32);
99	let iv_up = |i: usize| builder.add_constant(Word((IV[i] as u64) << 32));
100	// `t_high` is the counter's high half, which already sits where the high lane wants it. Two
101	// shifts keep it a linear definition, where masking it out would cost an AND constraint.
102	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		// `t_low` needs no masking: shifting up keeps the counter's low half and drops its high
117		// one.
118		up(counter),
119		counter_high,
120		up(block_len),
121		up(flags),
122	];
123
124	// Both lanes read the same message, under different round schedules, so each word is prepared
125	// once for each lane rather than once per round. Only the low lane's copy needs masking; the
126	// high lane's is a shift, which cleans itself.
127	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		// The high lane is `HIGH_LANE_ROUNDS` rounds behind the low one, so a step applies two
132		// different rounds of the schedule at once. The high lane's last step runs past round 6 and
133		// is the discarded one.
134		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		// The high lane has now produced the state the low lane started from. Bind the hint to it,
141		// one 64-bit equality per state word — a single equality pins both lanes, since the two
142		// halves never overlap.
143		//
144		//     hinted word:  [ high lane = initial state | low lane = state after round 2 ]
145		//                            must equal                    must equal
146		//                     initial state << 32          ^      high lane >> 32
147		//
148		// The high lane is pinned to the caller's own inputs, and the low lane to the state the
149		// high lane provably reached from them, which together leave the hint no freedom.
150		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	// The result is the low lane's. The high lane's is the round the split discards, and is left
159	// where it is rather than masked off, as `blake3_chunk` leaves a pair's first compression.
160	array::from_fn(|i| builder.bxor(v[i], v[i + 8]))
161}
162
163/// BLAKE3 compression function running two independent compressions in parallel.
164///
165/// Each 64-bit input wire packs two 32-bit lanes: bits `[0:32]` hold the lane-0 word,
166/// bits `[32:64]` hold the lane-1 word. This matches the lane layout expected by the
167/// parallel-halves [`iadd_32`](CircuitBuilder::iadd_32) and
168/// [`rotr32`](CircuitBuilder::rotr32) gates, so the 7-round core runs both
169/// compressions at the gate cost of a single one.
170///
171/// The 64-bit block counter is split by the caller into low and high 32-bit halves:
172/// `counter_lo` packs each lane's `t_low`, `counter_hi` packs each lane's `t_high`.
173///
174/// # Arguments
175///
176/// All wires follow the packing convention above.
177///
178/// - `cv`: 8 chaining-value words (per lane).
179/// - `block`: 16 message words (per lane).
180/// - `counter_lo`: low 32 bits of each lane's block counter.
181/// - `counter_hi`: high 32 bits of each lane's block counter.
182/// - `block_len`: byte count (0..=64) per lane.
183/// - `flags`: domain-separation flags per lane.
184///
185/// # Returns
186///
187/// The updated 8-word chaining value, with each word packing both lanes.
188///
189/// # Chips
190///
191/// This is a [`ChipGadget`]. A circuit that calls
192/// [`register_chip`](CircuitBuilder::register_chip) with [`Blake3Compress2x`] before building
193/// turns every compression under it into a chip call, including the ones
194/// [`blake3_compress_2x_seq`] and the chunk and tree gadgets reach.
195pub 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
214/// [`blake3_compress_2x`] as a gadget, so that a circuit can make it a chip.
215///
216/// Its interface is the flat 28 input words `cv[0..8]`, `block[0..16]`, `counter_lo`,
217/// `counter_hi`, `block_len`, `flags`, and the 8 output chaining-value words, all packing two
218/// lanes as [`blake3_compress_2x`] documents.
219pub 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		// Each lane is a 32-bit half of every input word, and the halves never interact: the
230		// core's adds and rotates are 32-bit and its exclusive-ors are bitwise. So a lane is the
231		// reference compression of its own halves.
232		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
256/// [`blake3_compress_2x`] in gates, whatever the building circuit does with the gadget.
257fn 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	// IV constants replicated into both 32-bit halves.
267	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
294/// Shared body: 7 rounds of mixing followed by feed-forward.
295///
296/// Lane-agnostic: `g()` uses parallel-halves `iadd_32` / `rotr32` and bit-parallel
297/// `bxor`, so the same core advances one or two independent compressions depending
298/// on how the caller packed `v` and `block`.
299fn 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/// BLAKE3 G mixing function.
308#[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
329/// One BLAKE3 round: four column G's followed by four diagonal G's.
330///
331/// `msg` is the round's message words in schedule order, not the raw block: the two lanes of
332/// [`blake3_compress`] run different rounds at once, so a round cannot pick its own schedule.
333fn round(builder: &CircuitBuilder, state: &mut [Wire; 16], msg: &[Wire; 16]) {
334	// Mix the columns.
335	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	// Mix the diagonals.
341	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
347/// Two sequential BLAKE3 compressions evaluated as the two lanes of [`blake3_compress_2x`].
348///
349/// Computes `C2 = compress(C1, block2, …)` where `C1 = compress(cv, block1, …)` — the output
350/// chaining value of the first compression is the input chaining value of the second. Both
351/// compressions share the single 7-round core of [`blake3_compress_2x`]: the first runs in the
352/// high lane (bits `[32:64]`), the second in the low lane (bits `[0:32]`).
353///
354/// The data dependency — the second compression needs the first's *output* as its input — is
355/// resolved with a `Blake3CompressHint` that precomputes `C1`'s output chaining value. That
356/// value is fed into the low lane of the merged input chaining value (the second compression's
357/// input) and constrained word-for-word against the first compression's in-circuit output (the
358/// high lane of the result), so the hint cannot lie.
359///
360/// # Arguments
361///
362/// All wires carry 32-bit values in their low 32 bits, matching [`blake3_compress`].
363///
364/// - A wire feeding a lane's low half is masked here, not assumed clean.
365/// - So a caller leaving the high 32 bits dirty cannot steer either compression.
366///
367/// - `cv`: input chaining value for the first compression (8 words).
368/// - `blocks`: the two message blocks (`blocks[0]` for C1, `blocks[1]` for C2), 16 words each.
369/// - `counter`: the 64-bit block counter, shared by both compressions. Sequential chaining only
370///   happens within a single BLAKE3 chunk, where every block carries the chunk counter unchanged.
371/// - `block_lens`: per-compression block lengths.
372/// - `flags`: per-compression flags.
373///
374/// # Returns
375///
376/// The two output chaining values packed into 8 wires: the second compression's output in the
377/// low 32 bits of each wire, the first compression's output in the high 32 bits.
378pub 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	// The hint returns the *merged* input chaining value directly: each word packs the first
387	// compression's output in the low 32 bits (the second compression's input lane) and the first
388	// compression's input `cv` word in the high 32 bits (the first compression's input lane). Both
389	// halves are constrained below, so the hint itself is untrusted.
390	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	// Pack two lane values into one wire: low 32 bits = lane 0 (C2), high 32 bits = lane 1 (C1).
400	// `shl` clears the high operand's upper bits; the low operand is cleared explicitly at each
401	// call site, since block/scalar inputs are not guaranteed to be zero-extended to 64 bits.
402	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	// Both compressions share the same block counter. Sequential chaining (C2 takes C1's output
408	// as its input chaining value) only occurs within a single BLAKE3 chunk, and every block in a
409	// chunk carries the chunk counter unchanged.
410	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	// Bind the hinted chaining value, one 64-bit equality per word.
428	// A single equality pins both lanes, since the two halves never overlap.
429	//
430	//     bits 32..64 <- the first compression's declared input, shifted up
431	//     bits  0..32 <- the first compression's in-circuit output, shifted down
432	//
433	//     hinted word:  [ high lane = input cv | low lane = C1 output ]
434	//                          must equal              must equal
435	//                     input cv << 32     ^     result >> 32
436	//
437	// Consequences, which together leave the hint no freedom:
438	// - The high lane provably compresses the caller's own chaining value.
439	// - The low lane provably chains from what the first compression really produced.
440	//
441	// Each side is one shift of a committed word, so no masking is needed:
442	// - Shifting up discards the input's own high bits.
443	// - Shifting down discards the result's low bits.
444	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
452/// Custom hint computing the merged initial state for [`blake3_compress`].
453///
454/// Runs the compression's first `HIGH_LANE_ROUNDS` rounds off-circuit and packs the state they
455/// reach so the output can be fed directly as the two-lane initial state: the low 32 bits seed the
456/// low lane, which picks the round sequence up from there, and the high 32 bits carry the
457/// compression's own initial state word. Both halves are re-derived in-circuit and constrained, so
458/// the hint only needs to produce the honest result.
459///
460/// Input layout (27 words, value in the low 32 bits of each): `cv[0..8]`, `block[0..16]`,
461/// `counter` (full 64 bits), `block_len`, `flags`. Output: 16 packed words where the low 32 bits
462/// hold the state after round `HIGH_LANE_ROUNDS - 1` and the high 32 bits the initial state.
463struct 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
490/// Custom hint computing the merged input chaining value for [`blake3_compress_2x_seq`].
491///
492/// Runs the first compression off-circuit and packs its result so the output can be fed directly
493/// as the two-lane input chaining value: the low 32 bits seed the second compression's input lane
494/// with the first compression's output, the high 32 bits carry the first compression's input `cv`
495/// word. Both halves are re-derived in-circuit and constrained, so the hint only needs to produce
496/// the honest result.
497///
498/// Input layout (27 words, value in the low 32 bits of each): `cv[0..8]`, `block[0..16]`,
499/// `counter` (full 64 bits), `block_len`, `flags`. Output: 8 packed words where the low 32 bits
500/// hold the compression output chaining value and the high 32 bits hold the corresponding `cv`
501/// input word.
502struct 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
525// --- Pure-Rust reference implementation of BLAKE3 compression ------------------------
526//
527// Shared by [`Blake3CompressHint`] (prover-side witness generation) and the tests.
528
529const 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
554/// The 16-word state a compression starts from, before any round runs.
555const 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
576/// Pure-Rust BLAKE3 compression, matching the in-circuit compression exactly.
577///
578/// - Exposed for callers that use a raw 2-to-1 compression as a tweakable hash.
579/// - It reproduces the same value off-circuit for witness generation.
580pub 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	// --- Circuit-level tests --------------------------------------------------------
605
606	/// Runs `blake3_compress_2x` in gates over its flat packed-word interface.
607	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	/// Build a circuit that computes `blake3_compress` on witness inputs, populate the
634	/// witness with the given values, and return the evaluated 8-word output.
635	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	/// As [`run_compress`], with `dirt` OR'd into the high 32 bits of every 32-bit input wire.
646	///
647	/// Nothing range-constrains a wire that carries a 32-bit value, so a witness is free to set the
648	/// half the gadget does not read.
649	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		// The gadget leaves the discarded lane in the high half of each word, so a caller reading a
666		// whole word clears it first, as every real one does.
667		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		// The counter is a genuine 64-bit input, so it has no unused half to dirty.
683		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		// Invariant: the result is the compression of the low halves alone.
745		//
746		// The rounds now run in both halves of every word, so the half a 32-bit input does not use
747		// is no longer inert padding — it is where half of the mixing happens. A witness that fills
748		// it must not be able to steer the digest.
749		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	// --- 2× SIMD tests -------------------------------------------------------------
769
770	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	/// Run `blake3_compress_2x` with two independent per-lane inputs and return the
779	/// two per-lane 8-word outputs.
780	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	// --- 2× sequential tests -------------------------------------------------------
902
903	/// Run `blake3_compress_2x_seq` and return `(c2_out, c1_out)`: the second and first
904	/// compression outputs, unpacked from the low and high lanes of the packed result.
905	#[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		// Expected: the first compression feeds the second; both share the same counter.
955		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		// Distinct block lengths / flags per compression exercise the lane packing of every
1009		// parameter. The counter has a nonzero high half so both 32-bit halves are packed into
1010		// both lanes.
1011		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	// Spec known-answer vectors
1029	//
1030	// Traces from Appendix B of draft-aumasson-blake3-00, "The BLAKE3 Hashing Framework".
1031	// They pin the compression function against the specification text itself.
1032	// So they hold even if the reference crate and this circuit were wrong together.
1033
1034	#[test]
1035	fn draft_b1_compression_matches_spec_trace() {
1036		// Fixture state: the 4-byte message "IETF", hashed unkeyed.
1037		//
1038		//     word 0     : 46 54 45 49 read little-endian -> 0x46544549
1039		//     words 1..16: zero, the 60 padding bytes
1040		let mut block = [0u32; 16];
1041		block[0] = 0x4654_4549;
1042
1043		// The message is one block, which is therefore the whole chunk and the tree root.
1044		let flags = CHUNK_START | CHUNK_END | ROOT;
1045		assert_eq!(flags, 0x0b, "spec trace records flags 0b");
1046
1047		// The length parameter counts application bytes only, so 4 rather than 64.
1048		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		// Check the off-circuit reference first, then the compiled circuit against the same words.
1059		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		// Fixture state: the tree root of a two-chunk message.
1066		// A parent node's 64-byte block is its two children's chaining values, concatenated.
1067		//
1068		//     words 0..8 : left child's 32-byte chaining value
1069		//     words 8..16: right child's 32-byte chaining value
1070		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		// A parent node that is also the tree root, so it carries both flags.
1089		// A parent always uses counter 0 and a full 64-byte block length.
1090		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		// Check the off-circuit reference first, then the compiled circuit against the same words.
1103		assert_eq!(ref_compress(&IV, &block, 0, 64, flags), expected);
1104		assert_eq!(run_compress(IV, block, 0, 64, flags), expected);
1105	}
1106
1107	// Circuit-versus-reference properties
1108
1109	/// A 32-bit word, weighted towards the values that stress carry propagation.
1110	///
1111	/// One case in four is drawn from the boundary set rather than uniformly.
1112	///
1113	/// - Zero and one exercise the shortest carry chains.
1114	/// - All-ones makes every position carry.
1115	/// - The lone top bit and the all-ones-below-it value straddle the lane boundary.
1116	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		// Each case compiles and evaluates a whole compression circuit.
1133		// So the sample stays small and the boundary weighting above carries the coverage.
1134		#![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			// Invariant: the two lanes share one core but must not leak into each other.
1154			// Every parameter differs per lane, so any carry or rotate crossing bit 32 shows up.
1155			let actual = run_compress_2x([cv0, cv1], [b0, b1], [t0, t1], [l0, l1], [f0, f1]);
1156			// Each lane is checked against a reference run of its own inputs alone.
1157			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		// The chip path takes the outputs from `Blake3Compress2x::execute` and the chip recomputes
1162		// them from its gates, so the two have to agree on every word a circuit can reach them
1163		// with. Words drawn at random are lane pairs no caller would pass, which is the point:
1164		// nothing about the interface stops one.
1165		#[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			// Invariant: the two lanes run one after the other, not side by side.
1181			// The second compression's input chaining value is the first one's output.
1182			//
1183			//     cv --block 1--> C1 --block 2--> C2
1184			//
1185			// The first output arrives through a hint, so this is what pins that hint honest.
1186			let (c2, c1) = run_compress_2x_seq(cv, b1, b2, counter, l1, f1, l2, f2);
1187			// The high lane must reproduce a plain compression of the caller's chaining value.
1188			let exp_c1 = ref_compress(&cv, &b1, counter, l1, f1);
1189			prop_assert_eq!(c1, exp_c1);
1190			// The low lane must then compress that result, not anything else.
1191			prop_assert_eq!(c2, ref_compress(&exp_c1, &b2, counter, l2, f2));
1192		}
1193	}
1194}