Skip to main content

binius_circuits/blake3/
mod.rs

1// Copyright 2025 Irreducible Inc.
2// Copyright 2026 The Binius Developers
3
4//! BLAKE3 circuit gadgets.
5//!
6//! This module provides circuit primitives for the BLAKE3 hash function. The primitives
7//! are exposed as free functions that take input wires and return output wires — no
8//! wrapping structs.
9//!
10//! The entry points are:
11//! - [`blake3_compress`] — single-block compression primitive, its rounds split across the two
12//!   32-bit lanes.
13//! - [`blake3_compress_2x_seq`] — two sequential compressions sharing one parallel core.
14//! - [`blake3_chunk`] — single-chunk (up to 16 blocks) chaining-value gadget.
15//! - [`blake3_fixed`] — full hash gadget for messages of compile-time-known length, spanning any
16//!   number of chunks via BLAKE3's parent tree.
17//! - [`blake3_keyed_fixed`] — the same gadget in BLAKE3's keyed mode.
18//! - [`blake3_keyed_fixed_2x`] — two keyed hashes of equal-length messages, run side by side.
19
20use binius_core::word::Word;
21use binius_frontend::{CircuitBuilder, Wire};
22
23use crate::{
24	fixed_byte_vec::ByteVec,
25	util::{clear_high_bits, zeroed_u32_words},
26};
27
28pub mod compress;
29
30pub use compress::{
31	Blake3Compress2x, blake3_compress, blake3_compress_2x, blake3_compress_2x_seq, ref_compress,
32};
33
34/// BLAKE3 initial chaining value. Same as the SHA-256 IV.
35pub const IV: [u32; 8] = [
36	0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
37];
38
39/// Message schedule for each of the 7 rounds of the BLAKE3 compression function.
40///
41/// Matches the `MSG_SCHEDULE` constant in the [reference implementation].
42///
43/// [reference implementation]: https://github.com/BLAKE3-team/BLAKE3/blob/master/src/portable.rs
44pub const MSG_SCHEDULE: [[usize; 16]; 7] = [
45	[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
46	[2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8],
47	[3, 4, 10, 12, 13, 2, 7, 14, 6, 5, 9, 0, 11, 15, 8, 1],
48	[10, 7, 12, 9, 14, 3, 13, 15, 4, 0, 11, 2, 5, 8, 1, 6],
49	[12, 13, 9, 11, 15, 10, 14, 8, 7, 2, 5, 3, 0, 1, 6, 4],
50	[9, 14, 11, 5, 8, 12, 15, 1, 13, 3, 0, 10, 2, 6, 4, 7],
51	[11, 15, 5, 0, 1, 9, 8, 6, 14, 10, 2, 12, 3, 4, 7, 13],
52];
53
54// Domain separation flags.
55pub const CHUNK_START: u32 = 1 << 0;
56pub const CHUNK_END: u32 = 1 << 1;
57pub const PARENT: u32 = 1 << 2;
58pub const ROOT: u32 = 1 << 3;
59pub const KEYED_HASH: u32 = 1 << 4;
60pub const DERIVE_KEY_CONTEXT: u32 = 1 << 5;
61pub const DERIVE_KEY_MATERIAL: u32 = 1 << 6;
62
63/// Byte length of a BLAKE3 block.
64pub const BLOCK_BYTES: usize = 64;
65
66/// Byte length of a BLAKE3 chunk.
67pub const CHUNK_BYTES: usize = 1024;
68
69/// Byte length of a BLAKE3 key.
70pub const KEY_BYTES: usize = 32;
71
72/// The chaining value a chunk or parent node starts from: the key in keyed mode, the [`IV`]
73/// otherwise.
74fn init_cv(builder: &CircuitBuilder, key: Option<[Wire; 8]>) -> [Wire; 8] {
75	key.unwrap_or_else(|| std::array::from_fn(|i| builder.add_constant(Word(IV[i] as u64))))
76}
77
78/// The flag every compression carries in keyed mode, and nothing in unkeyed mode.
79///
80/// Keying a BLAKE3 hash is exactly two substitutions — the key replaces the [`IV`] as the starting
81/// chaining value, and [`KEYED_HASH`] joins the flags — so the two travel together as one
82/// `Option`.
83const fn key_flag(key: Option<[Wire; 8]>) -> u32 {
84	if key.is_some() { KEYED_HASH } else { 0 }
85}
86
87/// Packs `lo` into bits `[0:32]` and `hi` into bits `[32:64]` of one wire.
88///
89/// The shift clears `hi`'s high half, so only `lo` must arrive with a zero one.
90fn pack_lanes(builder: &CircuitBuilder, lo: Wire, hi: Wire) -> Wire {
91	builder.bxor(lo, builder.shl(hi, 32))
92}
93
94/// A constant holding `value` in both 32-bit lanes, the form a shared parameter takes.
95fn dup32(builder: &CircuitBuilder, value: u32) -> Wire {
96	let value = value as u64;
97	builder.add_constant(Word(value | (value << 32)))
98}
99
100/// Computes the BLAKE3 chaining value of a single chunk.
101///
102/// A BLAKE3 chunk is up to 16 blocks (1024 bytes) compressed in a chain: the chaining value is
103/// threaded block-to-block starting from `key`, or from the [`IV`] when unkeyed. The first block
104/// carries [`CHUNK_START`] and the last carries [`CHUNK_END`]; every block carries the chunk's
105/// `counter` (its chunk index). `last_flags_extra` is OR'd into the last block's flags — pass
106/// [`ROOT`] when this chunk is the entire message (no parent tree), otherwise `0`.
107///
108/// # Arguments
109///
110/// - `builder`: Circuit builder.
111/// - `key`: the 8-word key in keyed mode, `None` in unkeyed mode.
112/// - `blocks`: the chunk's message blocks (1..=16), each 16 little-endian 32-bit words.
113/// - `block_lens`: the byte length (0..=64) of each block; the trailing block may be partial.
114/// - `counter`: the chunk index, used as the 64-bit block counter for every block.
115/// - `last_flags_extra`: extra flags OR'd into the last block (e.g. [`ROOT`] for a lone chunk).
116///
117/// # Returns
118///
119/// The chunk's 8-word chaining value, each word a 32-bit value in its low 32 bits.
120pub fn blake3_chunk(
121	builder: &CircuitBuilder,
122	key: Option<[Wire; 8]>,
123	blocks: &[[Wire; 16]],
124	block_lens: &[Wire],
125	counter: u64,
126	last_flags_extra: u32,
127) -> [Wire; 8] {
128	let n_blocks = blocks.len();
129	assert!((1..=16).contains(&n_blocks), "blake3_chunk: n_blocks ({n_blocks}) must be in 1..=16",);
130	assert_eq!(
131		block_lens.len(),
132		n_blocks,
133		"blake3_chunk: block_lens.len() ({}) must equal blocks.len() ({n_blocks})",
134		block_lens.len(),
135	);
136
137	let counter = builder.add_constant_64(counter);
138
139	let flags: Vec<Wire> = (0..n_blocks)
140		.map(|j| {
141			let start = if j == 0 { CHUNK_START } else { 0 };
142			let end = if j + 1 == n_blocks {
143				CHUNK_END | last_flags_extra
144			} else {
145				0
146			};
147			builder.add_constant(Word((start | end | key_flag(key)) as u64))
148		})
149		.collect();
150
151	let mut cv = init_cv(builder, key);
152
153	// Compress two blocks at a time: `blake3_compress_2x_seq` chains two sequential block
154	// compressions through a single parallel core, roughly halving the per-block cost.
155	//
156	// The threaded chaining value carries the pair's first compression in its high half, and
157	// that half is left as it is rather than masked off. Nothing downstream reads it:
158	//
159	// - A compression never lets a carry or a rotate cross bit 32, so the halves stay apart.
160	// - The paired core takes an input chaining value's low half only, through a left shift.
161	//
162	// So the low half of a result depends on the low halves of its inputs alone.
163	let n_pairs = n_blocks / 2;
164	for pair in 0..n_pairs {
165		let (lo, hi) = (2 * pair, 2 * pair + 1);
166		// The chaining value after the pair is the second compression's output, in the low half.
167		cv = blake3_compress_2x_seq(
168			&builder.subcircuit(format!("blake3_chunk_compress[{pair}]")),
169			cv,
170			[blocks[lo], blocks[hi]],
171			counter,
172			[block_lens[lo], block_lens[hi]],
173			[flags[lo], flags[hi]],
174		);
175	}
176
177	// A trailing block with no partner is compressed on its own.
178	//
179	//     6 blocks: [0,1] [2,3] [4,5]              -> 3 paired cores
180	//     7 blocks: [0,1] [2,3] [4,5] then 6 alone -> 3 paired cores + 1 lone compression
181	//
182	// Why not pad to an even count with a dummy block:
183	// - The paired core runs all 7 rounds, where a lone compression splits its own rounds across
184	//   the two lanes and runs 4.
185	// - The pair also packs a second lane's counter, length and flags.
186	// - All of that would be spent producing a result nothing reads.
187	if n_blocks % 2 == 1 {
188		let last = n_blocks - 1;
189		cv = blake3_compress(
190			&builder.subcircuit("blake3_chunk_compress[last]"),
191			cv,
192			blocks[last],
193			counter,
194			block_lens[last],
195			flags[last],
196		);
197	}
198
199	// The escaping value is the one place a clean high half is required, so mask once here
200	// rather than after every pair. Callers do read those bits:
201	//
202	// - A parent node merges two children with a left shift and an exclusive-or.
203	// - A digest is compared over all 64 bits of each word.
204	std::array::from_fn(|i| clear_high_bits(builder, cv[i], 32))
205}
206
207/// One BLAKE3 parent-node compression: combines two child chaining values into one.
208///
209/// The parent block is the two children concatenated (16 words); the chaining value is the key (or
210/// the [`IV`] when unkeyed), the counter is 0, the block length is [`BLOCK_BYTES`], and the flags
211/// are [`PARENT`] (plus [`ROOT`] for the tree root).
212///
213/// The result's high halves are cleared, since a parent's value is read whole: the level above
214/// merges it with a shift, and the root is the digest.
215fn blake3_parent(
216	builder: &CircuitBuilder,
217	key: Option<[Wire; 8]>,
218	left: [Wire; 8],
219	right: [Wire; 8],
220	is_root: bool,
221) -> [Wire; 8] {
222	let cv = init_cv(builder, key);
223	let block: [Wire; 16] = std::array::from_fn(|i| if i < 8 { left[i] } else { right[i - 8] });
224	let counter = builder.add_constant(Word::ZERO);
225	let block_len = builder.add_constant(Word(BLOCK_BYTES as u64));
226	let root_flag = if is_root { ROOT } else { 0 };
227	let flags = builder.add_constant(Word((PARENT | root_flag | key_flag(key)) as u64));
228	let out = blake3_compress(builder, cv, block, counter, block_len, flags);
229	std::array::from_fn(|i| clear_high_bits(builder, out[i], 32))
230}
231
232/// Two independent BLAKE3 parent-node compressions of one hash, evaluated in the two lanes of
233/// [`blake3_compress_2x`].
234///
235/// Lane 0 combines the pair `a`, lane 1 combines the pair `b`. Each child holds a 32-bit value in
236/// its low bits, so a pair is packed into a 64-bit wire by placing lane 0 in bits `[0:32]` and
237/// lane 1 in bits `[32:64]`. Returns the two parent chaining values, unpacked back into the
238/// low-32 layout.
239fn blake3_parent_pair(
240	builder: &CircuitBuilder,
241	key: Option<[Wire; 8]>,
242	a: ([Wire; 8], [Wire; 8]),
243	b: ([Wire; 8], [Wire; 8]),
244) -> ([Wire; 8], [Wire; 8]) {
245	// Both lanes start from the same chaining value, so each word is that word in both halves.
246	// The key's words carry zero high bits, so they pack by the same shift-and-XOR as a child.
247	let cv: [Wire; 8] = key.map_or_else(
248		|| std::array::from_fn(|i| dup32(builder, IV[i])),
249		|key| std::array::from_fn(|i| pack_lanes(builder, key[i], key[i])),
250	);
251	let block: [Wire; 16] = std::array::from_fn(|i| {
252		if i < 8 {
253			pack_lanes(builder, a.0[i], b.0[i])
254		} else {
255			pack_lanes(builder, a.1[i - 8], b.1[i - 8])
256		}
257	});
258	let zero = builder.add_constant(Word::ZERO);
259	let block_len = dup32(builder, BLOCK_BYTES as u32);
260	let flags = dup32(builder, PARENT | key_flag(key));
261	let out = blake3_compress_2x(builder, cv, block, zero, zero, block_len, flags);
262	let cv_a: [Wire; 8] = std::array::from_fn(|i| clear_high_bits(builder, out[i], 32));
263	let cv_b: [Wire; 8] = std::array::from_fn(|i| builder.shr(out[i], 32));
264	(cv_a, cv_b)
265}
266
267/// Folds chunk chaining values into the root digest through BLAKE3's binary parent tree.
268///
269/// The tree is built bottom-up: at each level, adjacent chaining values are paired and combined by
270/// a parent compression, and a lone trailing value is promoted unchanged to the next level. This
271/// bottom-up pairing reproduces BLAKE3's canonical left-full tree exactly. Parent compressions are
272/// batched two at a time through [`blake3_parent_pair`]; the final root — the last level's single
273/// 2->1 compression — carries [`ROOT`].
274///
275/// Requires at least two chunk chaining values (a single chunk needs no tree).
276fn blake3_tree_root(
277	builder: &CircuitBuilder,
278	key: Option<[Wire; 8]>,
279	chunk_cvs: Vec<[Wire; 8]>,
280) -> [Wire; 8] {
281	assert!(chunk_cvs.len() >= 2, "blake3_tree_root: needs at least two chunks");
282
283	let mut level = chunk_cvs;
284	let mut depth = 0;
285	loop {
286		// The root is the compression that reduces the final two subtree CVs to one.
287		if level.len() == 2 {
288			return blake3_parent(
289				&builder.subcircuit("blake3_tree_root"),
290				key,
291				level[0],
292				level[1],
293				true,
294			);
295		}
296
297		let sub = builder.subcircuit(format!("blake3_tree_level[{depth}]"));
298		let n = level.len();
299		let n_pairs = n / 2;
300		let mut next: Vec<[Wire; 8]> = Vec::with_capacity(n.div_ceil(2));
301
302		// Combine two independent parents per `blake3_compress_2x` call.
303		let mut p = 0;
304		while p + 1 < n_pairs {
305			let (cv_a, cv_b) = blake3_parent_pair(
306				&sub,
307				key,
308				(level[2 * p], level[2 * p + 1]),
309				(level[2 * p + 2], level[2 * p + 3]),
310			);
311			next.push(cv_a);
312			next.push(cv_b);
313			p += 2;
314		}
315		// A leftover unpaired parent (odd number of pairs) is done single-lane.
316		if p < n_pairs {
317			next.push(blake3_parent(&sub, key, level[2 * p], level[2 * p + 1], false));
318		}
319		// A lone trailing chaining value with no sibling is promoted unchanged.
320		if n % 2 == 1 {
321			next.push(level[n - 1]);
322		}
323
324		level = next;
325		depth += 1;
326	}
327}
328
329/// Computes the BLAKE3 hash of a compile-time fixed-length message.
330///
331/// The BLAKE3 analog of [`sha256_fixed`](crate::sha256::sha256_fixed): the message length is known
332/// at circuit construction time, which fixes the chunk/tree shape and eliminates runtime padding
333/// logic.
334///
335/// The message is split into 1024-byte chunks ([`blake3_chunk`]); each chunk's chaining value is
336/// folded into the digest by BLAKE3's binary parent tree, two independent parent compressions at a
337/// time via [`blake3_compress_2x`]. The single [`ROOT`] flag lands on the final compression: the
338/// lone chunk when the message fits in one chunk, otherwise the tree's root parent.
339///
340/// # Arguments
341///
342/// - `builder`: Circuit builder.
343/// - `message`: Input message as 32-bit little-endian words (4 bytes per wire). The high 32 bits of
344///   each wire must be zero. Length must equal `len_bytes.div_ceil(4)`.
345/// - `len_bytes`: The compile-time-known length of the message in bytes.
346///
347/// # Returns
348///
349/// The BLAKE3 digest as 8 wires, each holding a 32-bit little-endian word in its
350/// low 32 bits.
351pub fn blake3_fixed(builder: &CircuitBuilder, message: &[Wire], len_bytes: usize) -> [Wire; 8] {
352	blake3_hash_fixed(builder, None, message, len_bytes)
353}
354
355/// Computes the keyed BLAKE3 hash of a compile-time fixed-length message.
356///
357/// The keyed mode of [`blake3_fixed`]: the 32-byte key replaces the [`IV`] as the chaining value
358/// every chunk and parent node starts from, and every compression carries [`KEYED_HASH`]. This is
359/// BLAKE3's native keying, so the digest matches [`blake3::keyed_hash`].
360///
361/// A key shorter than 32 bytes is zero-padded to 32 — the length of the key vector is not itself
362/// hashed, so keys that agree after padding (`b"k"` and `b"k\0"`) produce the same digest.
363///
364/// [`blake3::keyed_hash`]: https://docs.rs/blake3/latest/blake3/fn.keyed_hash.html
365///
366/// # Arguments
367///
368/// - `builder`: Circuit builder.
369/// - `message`: as in [`blake3_fixed`].
370/// - `len_bytes`: as in [`blake3_fixed`].
371/// - `key`: the key, 0 to 32 bytes packed little-endian 8 per wire. Its length must be fixed at
372///   circuit construction time (a point `len_range`, as [`ByteVec::new_const_len`] builds). Bytes
373///   past that length are masked to zero, so a prover cannot steer the digest through them.
374///
375/// # Returns
376///
377/// As in [`blake3_fixed`].
378pub fn blake3_keyed_fixed(
379	builder: &CircuitBuilder,
380	message: &[Wire],
381	len_bytes: usize,
382	key: &ByteVec,
383) -> [Wire; 8] {
384	blake3_hash_fixed(builder, Some(key_words(builder, key)), message, len_bytes)
385}
386
387/// The 8 words a keyed hash seeds its chaining value with.
388///
389/// - A byte vector packs 8 bytes per wire and a key is 8 little-endian 32-bit words, so splitting
390///   each wire in half is the whole conversion.
391/// - Bytes past the key length are masked to zero, so a prover cannot steer the digest with them.
392/// - Every word comes out with a zero high half, so a caller can pack two keys one per lane.
393///
394/// # Panics
395///
396/// - If the key length is not fixed at circuit construction time.
397/// - If the key is longer than [`KEY_BYTES`].
398fn key_words(builder: &CircuitBuilder, key: &ByteVec) -> [Wire; 8] {
399	assert_eq!(
400		key.len_range.start(),
401		key.len_range.end(),
402		"BLAKE3: the key length must be fixed at circuit construction time, but len_range is {:?}",
403		key.len_range,
404	);
405	let key_len = *key.len_range.start();
406	assert!(key_len <= KEY_BYTES, "BLAKE3: key length ({key_len}) exceeds {KEY_BYTES}");
407
408	let words = zeroed_u32_words(builder, &key.data, key_len, 8);
409	std::array::from_fn(|i| words[i])
410}
411
412/// The message zero-padded to whole blocks, as 32-bit little-endian words.
413///
414/// BLAKE3 appends no length field: the real byte count travels in each block's length parameter.
415///
416/// Masking the high halves costs a gate per message word, and is needed only for a two-lane
417/// packing, where that half is the other lane rather than dead space.
418///
419/// # Panics
420///
421/// If the message is not `len_bytes.div_ceil(4)` wires long.
422fn padded_message_words(
423	builder: &CircuitBuilder,
424	message: &[Wire],
425	len_bytes: usize,
426	mask_high_halves: bool,
427) -> Vec<Wire> {
428	assert_eq!(
429		message.len(),
430		len_bytes.div_ceil(4),
431		"blake3: message.len() ({}) must equal len_bytes.div_ceil(4) ({})",
432		message.len(),
433		len_bytes.div_ceil(4),
434	);
435
436	let n_padded_words = len_bytes.div_ceil(BLOCK_BYTES).max(1) * 16;
437	let n_whole_words = len_bytes / 4;
438	let boundary_bytes = len_bytes % 4;
439
440	let mut padded: Vec<Wire> = Vec::with_capacity(n_padded_words);
441	padded.extend(message[..n_whole_words].iter().map(|&w| {
442		if mask_high_halves {
443			clear_high_bits(builder, w, 32)
444		} else {
445			w
446		}
447	}));
448	if boundary_bytes > 0 {
449		// Partial trailing word: a little-endian word holds its valid bytes low, so mask the rest.
450		// At most three survive, so the high half comes out clean either way.
451		let mask_value = (1u64 << (boundary_bytes * 8)) - 1;
452		let mask = builder.add_constant(Word(mask_value));
453		padded.push(builder.band(message[n_whole_words], mask));
454	}
455	// The padding words are the zero constant, so their high halves are already clean.
456	padded.resize(n_padded_words, builder.add_constant(Word::ZERO));
457	padded
458}
459
460/// The length of block `j`: a whole block, or the remainder for the final one.
461fn block_len_bytes(len_bytes: usize, j: usize) -> usize {
462	(len_bytes - j * BLOCK_BYTES).min(BLOCK_BYTES)
463}
464
465/// The body shared by [`blake3_fixed`] and [`blake3_keyed_fixed`]: the two differ only in the
466/// chaining value chunks and parent nodes start from, and the [`KEYED_HASH`] flag that rides along
467/// with it.
468fn blake3_hash_fixed(
469	builder: &CircuitBuilder,
470	key: Option<[Wire; 8]>,
471	message: &[Wire],
472	len_bytes: usize,
473) -> [Wire; 8] {
474	let n_blocks = len_bytes.div_ceil(BLOCK_BYTES).max(1);
475	// One lane, so a dirty high half never reaches the result and needs no masking.
476	let padded = padded_message_words(builder, message, len_bytes, false);
477
478	let block = |j: usize| -> [Wire; 16] { std::array::from_fn(|i| padded[j * 16 + i]) };
479	let block_len =
480		|j: usize| -> Wire { builder.add_constant(Word(block_len_bytes(len_bytes, j) as u64)) };
481
482	// One chaining value per chunk. Every chunk but the last is a full 16 blocks (1024 bytes).
483	let n_chunks = len_bytes.div_ceil(CHUNK_BYTES).max(1);
484	let blocks_per_chunk = CHUNK_BYTES / BLOCK_BYTES;
485	let chunk_cvs: Vec<[Wire; 8]> = (0..n_chunks)
486		.map(|c| {
487			let block_start = c * blocks_per_chunk;
488			let block_end = ((c + 1) * blocks_per_chunk).min(n_blocks);
489			let blocks: Vec<[Wire; 16]> = (block_start..block_end).map(block).collect();
490			let block_lens: Vec<Wire> = (block_start..block_end).map(block_len).collect();
491			// ROOT lands on the lone chunk directly; with multiple chunks it moves to the tree
492			// root.
493			let last_flags_extra = if n_chunks == 1 { ROOT } else { 0 };
494			blake3_chunk(
495				&builder.subcircuit(format!("blake3_chunk[{c}]")),
496				key,
497				&blocks,
498				&block_lens,
499				c as u64,
500				last_flags_extra,
501			)
502		})
503		.collect();
504
505	// A single chunk is its own digest; otherwise fold the chunk chaining values through the tree.
506	if n_chunks == 1 {
507		chunk_cvs[0]
508	} else {
509		blake3_tree_root(builder, key, chunk_cvs)
510	}
511}
512
513/// Computes two keyed BLAKE3 hashes of equal-length messages side by side.
514///
515/// Each digest matches [`blake3_keyed_fixed`] on its own message and key.
516///
517/// ```text
518///     bits [0:32]  = lane 0: keys[0], messages[0]  --\
519///                                                     >-- one paired core per block
520///     bits [32:64] = lane 1: keys[1], messages[1]  --/
521/// ```
522///
523/// Equal lengths are what put the two hashes in lockstep.
524/// - The block count, the block lengths, the flags and the tree shape all agree.
525/// - So every compression of one hash has exactly one partner in the other.
526///
527/// The saving lands where a single hash has an odd block with no partner to pair with.
528/// - That block goes through [`blake3_compress`], which fills the lanes by splitting its own rounds
529///   across them and so evaluates 3 of its 7 rounds twice.
530/// - Pairing two hashes puts a real second hash in that lane instead, and the duplicated rounds are
531///   what it recovers: 336 AND constraints against 384, for a one-block message.
532///
533/// # Arguments
534///
535/// - `builder`: Circuit builder.
536/// - `messages`: the two messages, each as in [`blake3_fixed`], their high halves masked here so
537///   one cannot spill into the other lane.
538/// - `len_bytes`: the compile-time-known length both messages share.
539/// - `keys`: the two keys, each as in [`blake3_keyed_fixed`], and free to differ in length.
540///
541/// # Returns
542///
543/// The two digests, each as in [`blake3_fixed`].
544///
545/// # Panics
546///
547/// - If either message is not `len_bytes.div_ceil(4)` wires long.
548/// - If either key's length is not fixed at circuit construction time.
549/// - If either key is longer than [`KEY_BYTES`].
550pub fn blake3_keyed_fixed_2x(
551	builder: &CircuitBuilder,
552	messages: [&[Wire]; 2],
553	len_bytes: usize,
554	keys: [&ByteVec; 2],
555) -> [[Wire; 8]; 2] {
556	// The key words carry zero high halves, so packing is one shift and one exclusive-or.
557	let lanes = keys.map(|key| key_words(builder, key));
558	let key_2x: [Wire; 8] = std::array::from_fn(|i| pack_lanes(builder, lanes[0][i], lanes[1][i]));
559
560	// Pad each message on its own, then merge word by word into the two-lane layout.
561	let padded = messages.map(|message| padded_message_words(builder, message, len_bytes, true));
562	let n_blocks = len_bytes.div_ceil(BLOCK_BYTES).max(1);
563	let n_content_words = len_bytes.div_ceil(4);
564	let zero = builder.add_constant(Word::ZERO);
565	let block = |j: usize| -> [Wire; 16] {
566		std::array::from_fn(|i| {
567			let k = j * 16 + i;
568			// Padding is the zero constant in both lanes, so packing it would only spend shifts.
569			if k >= n_content_words {
570				zero
571			} else {
572				pack_lanes(builder, padded[0][k], padded[1][k])
573			}
574		})
575	};
576	// Every parameter but the message itself is shared, so it enters both lanes as one constant.
577	let block_len = |j: usize| -> Wire { dup32(builder, block_len_bytes(len_bytes, j) as u32) };
578
579	let n_chunks = len_bytes.div_ceil(CHUNK_BYTES).max(1);
580	let blocks_per_chunk = CHUNK_BYTES / BLOCK_BYTES;
581	let chunk_cvs: Vec<[Wire; 8]> = (0..n_chunks)
582		.map(|c| {
583			let block_start = c * blocks_per_chunk;
584			let block_end = ((c + 1) * blocks_per_chunk).min(n_blocks);
585			let blocks: Vec<[Wire; 16]> = (block_start..block_end).map(block).collect();
586			let block_lens: Vec<Wire> = (block_start..block_end).map(block_len).collect();
587			// ROOT lands on the lone chunk directly; with multiple chunks it moves to the tree
588			// root.
589			let last_flags_extra = if n_chunks == 1 { ROOT } else { 0 };
590			blake3_chunk_2x(
591				&builder.subcircuit(format!("blake3_chunk_2x[{c}]")),
592				key_2x,
593				&blocks,
594				&block_lens,
595				c as u64,
596				last_flags_extra,
597			)
598		})
599		.collect();
600
601	let root = if n_chunks == 1 {
602		chunk_cvs[0]
603	} else {
604		blake3_tree_root_2x(builder, key_2x, chunk_cvs)
605	};
606
607	// Unpack the digests into the one-lane layout the single-hash gadgets return.
608	[
609		std::array::from_fn(|i| clear_high_bits(builder, root[i], 32)),
610		std::array::from_fn(|i| builder.shr(root[i], 32)),
611	]
612}
613
614/// Computes one chunk's chaining value in each of two keyed hashes at once.
615///
616/// The two-lane counterpart of [`blake3_chunk`], with one hash per lane.
617///
618/// Blocks still chain one into the next, but the lanes hold two hashes rather than two blocks, so
619/// the paired core needs no hint to resolve that dependency.
620///
621/// # Arguments
622///
623/// - `builder`: Circuit builder.
624/// - `key_2x`: the two keys packed one per lane, the chaining value both hashes start from.
625/// - `blocks`: the chunk's message blocks (1..=16), each 16 words with both lanes packed.
626/// - `block_lens`: each block's byte length, the same value in both lanes.
627/// - `counter`: the chunk index, used as the 64-bit block counter for every block.
628/// - `last_flags_extra`: extra flags OR'd into the last block, e.g. [`ROOT`] for a lone chunk.
629///
630/// # Returns
631///
632/// The chunk's chaining value for both hashes, packed one per lane.
633fn blake3_chunk_2x(
634	builder: &CircuitBuilder,
635	key_2x: [Wire; 8],
636	blocks: &[[Wire; 16]],
637	block_lens: &[Wire],
638	counter: u64,
639	last_flags_extra: u32,
640) -> [Wire; 8] {
641	let n_blocks = blocks.len();
642	assert!(
643		(1..=16).contains(&n_blocks),
644		"blake3_chunk_2x: n_blocks ({n_blocks}) must be in 1..=16",
645	);
646	assert_eq!(
647		block_lens.len(),
648		n_blocks,
649		"blake3_chunk_2x: block_lens.len() ({}) must equal blocks.len() ({n_blocks})",
650		block_lens.len(),
651	);
652
653	let counter_lo = dup32(builder, counter as u32);
654	let counter_hi = dup32(builder, (counter >> 32) as u32);
655
656	let mut cv = key_2x;
657	for (j, block) in blocks.iter().enumerate() {
658		let start = if j == 0 { CHUNK_START } else { 0 };
659		let end = if j + 1 == n_blocks {
660			CHUNK_END | last_flags_extra
661		} else {
662			0
663		};
664		let flags = dup32(builder, start | end | KEYED_HASH);
665		cv = blake3_compress_2x(
666			&builder.subcircuit(format!("blake3_chunk_2x_compress[{j}]")),
667			cv,
668			*block,
669			counter_lo,
670			counter_hi,
671			block_lens[j],
672			flags,
673		);
674	}
675
676	// Both halves of every word are live — one hash each — so nothing is masked off here.
677	cv
678}
679
680/// One parent-node compression in each of two keyed hashes at once.
681///
682/// The children already carry one hash per lane, so the parent block is the two concatenated.
683fn blake3_parent_2x(
684	builder: &CircuitBuilder,
685	key_2x: [Wire; 8],
686	left: [Wire; 8],
687	right: [Wire; 8],
688	is_root: bool,
689) -> [Wire; 8] {
690	let block: [Wire; 16] = std::array::from_fn(|i| if i < 8 { left[i] } else { right[i - 8] });
691	let zero = builder.add_constant(Word::ZERO);
692	let block_len = dup32(builder, BLOCK_BYTES as u32);
693	let root_flag = if is_root { ROOT } else { 0 };
694	let flags = dup32(builder, PARENT | root_flag | KEYED_HASH);
695	blake3_compress_2x(builder, key_2x, block, zero, zero, block_len, flags)
696}
697
698/// Folds two hashes' chunk chaining values into their root digests, one hash per lane.
699///
700/// Simpler than the one-lane tree: the two shapes agree, so every parent already has a partner
701/// and none is left over to batch.
702///
703/// - Levels are built bottom-up, and a lone trailing value is promoted unchanged.
704/// - The final 2->1 compression carries [`ROOT`].
705/// - Requires at least two chunk chaining values, since a single chunk needs no tree.
706fn blake3_tree_root_2x(
707	builder: &CircuitBuilder,
708	key_2x: [Wire; 8],
709	chunk_cvs: Vec<[Wire; 8]>,
710) -> [Wire; 8] {
711	assert!(chunk_cvs.len() >= 2, "blake3_tree_root_2x: needs at least two chunks");
712
713	let mut level = chunk_cvs;
714	let mut depth = 0;
715	loop {
716		// The root is the compression that reduces the final two subtree CVs to one.
717		if level.len() == 2 {
718			return blake3_parent_2x(
719				&builder.subcircuit("blake3_tree_root_2x"),
720				key_2x,
721				level[0],
722				level[1],
723				true,
724			);
725		}
726
727		let sub = builder.subcircuit(format!("blake3_tree_level_2x[{depth}]"));
728		let n = level.len();
729		let mut next: Vec<[Wire; 8]> = Vec::with_capacity(n.div_ceil(2));
730		for p in 0..n / 2 {
731			next.push(blake3_parent_2x(&sub, key_2x, level[2 * p], level[2 * p + 1], false));
732		}
733		// A lone trailing chaining value with no sibling is promoted unchanged.
734		if n % 2 == 1 {
735			next.push(level[n - 1]);
736		}
737
738		level = next;
739		depth += 1;
740	}
741}
742
743#[cfg(test)]
744mod tests {
745	use binius_frontend::CircuitStat;
746	use hex_literal::hex;
747	use proptest::prelude::*;
748
749	use super::*;
750
751	/// Convert a byte slice into the 32-bit LE word encoding expected by [`blake3_fixed`].
752	/// The last word is zero-padded in its high bytes if the length is not a multiple of 4.
753	fn bytes_to_le_words(bytes: &[u8]) -> Vec<u64> {
754		let n_words = bytes.len().div_ceil(4);
755		(0..n_words)
756			.map(|i| {
757				let mut buf = [0u8; 4];
758				let start = i * 4;
759				let end = (start + 4).min(bytes.len());
760				buf[..end - start].copy_from_slice(&bytes[start..end]);
761				u32::from_le_bytes(buf) as u64
762			})
763			.collect()
764	}
765
766	/// Hashes `input` in-circuit and asserts the digest equals `expected`.
767	///
768	/// The digest wires are public inputs, so filling them with the expected bytes turns the
769	/// in-circuit equality into the assertion under test.
770	///
771	/// A disagreement therefore surfaces as a failure to populate the witness.
772	fn check_digest(input: &[u8], expected: [u8; 32]) {
773		let builder = CircuitBuilder::new();
774		// The message is private, one 32-bit little-endian word per wire.
775		let message: Vec<Wire> = (0..input.len().div_ceil(4))
776			.map(|_| builder.add_witness())
777			.collect();
778		let digest = blake3_fixed(&builder, &message, input.len());
779		// The expected digest is public, and pinned word for word against the computed one.
780		let digest_out: [Wire; 8] = std::array::from_fn(|_| builder.add_inout());
781		for i in 0..8 {
782			builder.assert_eq("digest_match", digest[i], digest_out[i]);
783		}
784
785		let circuit = builder.build();
786		let mut w = circuit.new_witness_filler();
787		for (wire, word) in message.iter().zip(bytes_to_le_words(input)) {
788			w[*wire] = Word(word);
789		}
790		// Read the vector back in the same little-endian word order the circuit produces.
791		for i in 0..8 {
792			let bytes: [u8; 4] = expected[i * 4..i * 4 + 4].try_into().unwrap();
793			w[digest_out[i]] = Word(u32::from_le_bytes(bytes) as u64);
794		}
795		circuit
796			.populate_wire_witness(&mut w)
797			.unwrap_or_else(|e| panic!("digest disagreed with the specification vector: {e:?}"));
798	}
799
800	#[test]
801	fn draft_b1_digest_matches_spec() {
802		// Fixture state: Appendix B.1 of the specification draft.
803		// A 4-byte message is one block, one chunk, and its own tree root.
804		check_digest(
805			b"IETF",
806			hex!("83a2de1ee6f4e6ab686889248f4ec0cf4cc5709446a682ffd1cbb4d6165181e2"),
807		);
808	}
809
810	#[test]
811	fn draft_b2_digest_matches_spec() {
812		// Fixture state: Appendix B.2 of the specification draft.
813		// Two full chunks, so 32 block compressions folded through one parent node.
814		//
815		//     chunk 0: 1024 bytes of 0xaa --\
816		//                                    >-- parent (root) --> digest
817		//     chunk 1: 1024 bytes of 0xbb --/
818		//
819		// The trace is unkeyed despite its section title.
820		// - Its chaining value is the standard initial value, not a key.
821		// - Its flags carry no keyed-hash bit.
822		let mut input = vec![0xaau8; CHUNK_BYTES];
823		input.extend_from_slice(&[0xbbu8; CHUNK_BYTES]);
824		check_digest(
825			&input,
826			hex!("e79d2838915accd3b21bb0ba76b5edf8dc08d3d78d0db65b713f0f37ec58c346"),
827		);
828	}
829
830	proptest! {
831		// Every case compiles a whole hashing circuit, so the sample stays small.
832		// The length range spans 0 to 10 blocks, which covers both parities.
833		// Odd block counts are what reach the one-lane trailing compression.
834		#![proptest_config(ProptestConfig::with_cases(12))]
835
836		#[test]
837		fn fixed_matches_blake3_crate(input in prop::collection::vec(any::<u8>(), 0..=600)) {
838			// Random content at a random length, checked against the reference crate.
839			check(&input);
840		}
841	}
842
843	/// Run `blake3_fixed` over `input` and assert it matches `blake3::hash(input)`.
844	fn check(input: &[u8]) {
845		let builder = CircuitBuilder::new();
846		let message: Vec<Wire> = (0..input.len().div_ceil(4))
847			.map(|_| builder.add_witness())
848			.collect();
849		let digest = blake3_fixed(&builder, &message, input.len());
850		let digest_out: [Wire; 8] = std::array::from_fn(|_| builder.add_inout());
851		for i in 0..8 {
852			builder.assert_eq("digest_match", digest[i], digest_out[i]);
853		}
854
855		let circuit = builder.build();
856		let mut w = circuit.new_witness_filler();
857		let words = bytes_to_le_words(input);
858		for (wire, word) in message.iter().zip(words.iter()) {
859			w[*wire] = Word(*word);
860		}
861
862		let expected = blake3::hash(input);
863		let expected_words: [u32; 8] = std::array::from_fn(|i| {
864			u32::from_le_bytes(expected.as_bytes()[i * 4..i * 4 + 4].try_into().unwrap())
865		});
866		for i in 0..8 {
867			w[digest_out[i]] = Word(expected_words[i] as u64);
868		}
869		circuit
870			.populate_wire_witness(&mut w)
871			.unwrap_or_else(|e| panic!("blake3_fixed failed for len_bytes={}: {e:?}", input.len()));
872	}
873
874	#[test]
875	fn empty() {
876		check(b"");
877	}
878
879	#[test]
880	fn one_byte() {
881		check(&[0x5a]);
882	}
883
884	#[test]
885	fn abc() {
886		check(b"abc");
887	}
888
889	#[test]
890	fn block_boundaries() {
891		// Lengths chosen to cover 1..=16 blocks, including odd block counts (3, 5, 7) that
892		// exercise the trailing single-block compression after the 2x-sequential pairs.
893		for &len in &[
894			1usize, 63, 64, 65, 127, 128, 129, 192, 256, 257, 320, 448, 511, 512, 1023, 1024,
895		] {
896			let input: Vec<u8> = (0..len).map(|i| (i * 37 + 1) as u8).collect();
897			check(&input);
898		}
899	}
900
901	/// Run `blake3_keyed_fixed` over `input` and assert it matches the reference crate keyed with
902	/// `key` zero-padded to 32 bytes.
903	///
904	/// The key wires are witness, so `garbage_padding` populates the bytes past the key length
905	/// with 0xff instead of zero: the digest must not move, since the gadget masks them.
906	fn check_keyed(input: &[u8], key: &[u8], garbage_padding: bool) {
907		let builder = CircuitBuilder::new();
908		let message: Vec<Wire> = (0..input.len().div_ceil(4))
909			.map(|_| builder.add_witness())
910			.collect();
911		let key_data: Vec<Wire> = (0..key.len().div_ceil(8))
912			.map(|_| builder.add_witness())
913			.collect();
914		let key_vec = ByteVec::new_const_len(&builder, key_data, key.len());
915		let digest = blake3_keyed_fixed(&builder, &message, input.len(), &key_vec);
916		let digest_out: [Wire; 8] = std::array::from_fn(|_| builder.add_inout());
917		for i in 0..8 {
918			builder.assert_eq("digest_match", digest[i], digest_out[i]);
919		}
920
921		let circuit = builder.build();
922		let mut w = circuit.new_witness_filler();
923		for (wire, word) in message.iter().zip(bytes_to_le_words(input)) {
924			w[*wire] = Word(word);
925		}
926		// Pad out to whole wires, so the bytes past the key length are populated too.
927		let mut key_bytes = key.to_vec();
928		key_bytes.resize(key.len().next_multiple_of(8), if garbage_padding { 0xff } else { 0 });
929		for (i, chunk) in key_bytes.chunks(8).enumerate() {
930			w[key_vec.data[i]] = Word(u64::from_le_bytes(chunk.try_into().unwrap()));
931		}
932
933		let mut padded_key = [0u8; KEY_BYTES];
934		padded_key[..key.len()].copy_from_slice(key);
935		let expected = blake3::keyed_hash(&padded_key, input);
936		for i in 0..8 {
937			let bytes: [u8; 4] = expected.as_bytes()[i * 4..i * 4 + 4].try_into().unwrap();
938			w[digest_out[i]] = Word(u32::from_le_bytes(bytes) as u64);
939		}
940		circuit.populate_wire_witness(&mut w).unwrap_or_else(|e| {
941			panic!(
942				"blake3_keyed_fixed failed for len_bytes={}, key_len={}: {e:?}",
943				input.len(),
944				key.len()
945			)
946		});
947	}
948
949	#[test]
950	fn keyed_full_length_key() {
951		// The native case: a 32-byte key, across message lengths spanning the block and chunk
952		// boundaries that switch the chunk and tree shape.
953		let key: Vec<u8> = (0..KEY_BYTES).map(|i| (i * 7 + 3) as u8).collect();
954		for &len in &[0usize, 1, 64, 65, 192, 1024, 1025, 3072] {
955			let input: Vec<u8> = (0..len).map(|i| (i * 37 + 1) as u8).collect();
956			check_keyed(&input, &key, false);
957		}
958	}
959
960	#[test]
961	fn keyed_short_keys() {
962		// A key shorter than 32 bytes is zero-padded, including the empty key — which is the
963		// all-zero key, and still not the unkeyed hash, since [`KEYED_HASH`] separates them. The
964		// lengths cover both sides of the 8-byte wire boundary and the 4-byte word boundary the
965		// padding mask straddles.
966		for key_len in [0usize, 1, 4, 5, 8, 9, 16, 23, 31] {
967			let key: Vec<u8> = (0..key_len).map(|i| (i * 11 + 5) as u8).collect();
968			check_keyed(b"abc", &key, false);
969		}
970	}
971
972	#[test]
973	fn keyed_ignores_bytes_past_the_key_length() {
974		// The key wires are witness, so a prover picks the bytes past the key length. Masking them
975		// is what stops those bytes from steering the digest.
976		for key_len in [0usize, 1, 5, 9, 23, 31] {
977			let key: Vec<u8> = (0..key_len).map(|i| (i * 11 + 5) as u8).collect();
978			check_keyed(b"abc", &key, true);
979		}
980	}
981
982	proptest! {
983		// Every case compiles a whole hashing circuit, so the sample stays small.
984		#![proptest_config(ProptestConfig::with_cases(12))]
985
986		#[test]
987		fn keyed_matches_blake3_crate(
988			input in prop::collection::vec(any::<u8>(), 0..=300),
989			key in prop::collection::vec(any::<u8>(), 0..=KEY_BYTES),
990		) {
991			check_keyed(&input, &key, true);
992		}
993	}
994
995	#[test]
996	#[should_panic(expected = "key length (33) exceeds 32")]
997	fn keyed_rejects_an_oversized_key() {
998		let builder = CircuitBuilder::new();
999		let data: Vec<Wire> = (0..5).map(|_| builder.add_witness()).collect();
1000		let key = ByteVec::new_const_len(&builder, data, KEY_BYTES + 1);
1001		blake3_keyed_fixed(&builder, &[], 0, &key);
1002	}
1003
1004	#[test]
1005	#[should_panic(expected = "key length must be fixed at circuit construction time")]
1006	fn keyed_rejects_a_runtime_length_key() {
1007		let builder = CircuitBuilder::new();
1008		// `new_witness` leaves the length range at the full `0..=capacity`, so the gadget cannot
1009		// know which bytes to mask.
1010		let key = ByteVec::new_witness(&builder, 4);
1011		blake3_keyed_fixed(&builder, &[], 0, &key);
1012	}
1013
1014	/// Run the two-lane gadget over both inputs and assert each digest matches the reference crate,
1015	/// keyed with the matching key zero-padded to 32 bytes.
1016	///
1017	/// `garbage` sets the bits a prover picks but the specification never reads: the high half of
1018	/// each message word, and the key bytes past each key length.
1019	fn check_keyed_2x(inputs: [&[u8]; 2], keys: [&[u8]; 2], garbage: bool) {
1020		let len_bytes = inputs[0].len();
1021		assert_eq!(len_bytes, inputs[1].len(), "the two messages must have equal length");
1022
1023		let builder = CircuitBuilder::new();
1024		let messages: [Vec<Wire>; 2] = std::array::from_fn(|_| {
1025			(0..len_bytes.div_ceil(4))
1026				.map(|_| builder.add_witness())
1027				.collect()
1028		});
1029		let key_vecs: [ByteVec; 2] = std::array::from_fn(|l| {
1030			let data = (0..keys[l].len().div_ceil(8))
1031				.map(|_| builder.add_witness())
1032				.collect();
1033			ByteVec::new_const_len(&builder, data, keys[l].len())
1034		});
1035		let digests = blake3_keyed_fixed_2x(
1036			&builder,
1037			[&messages[0], &messages[1]],
1038			len_bytes,
1039			[&key_vecs[0], &key_vecs[1]],
1040		);
1041		// Each digest is pinned word for word against a public expected value.
1042		let digests_out: [[Wire; 8]; 2] =
1043			std::array::from_fn(|_| std::array::from_fn(|_| builder.add_inout()));
1044		for l in 0..2 {
1045			for i in 0..8 {
1046				builder.assert_eq("digest_match_2x", digests[l][i], digests_out[l][i]);
1047			}
1048		}
1049
1050		let circuit = builder.build();
1051		let mut w = circuit.new_witness_filler();
1052		// A message word occupies the low half of its wire; the high half is the other lane's.
1053		let dirty_high = if garbage { 0xFFFF_FFFF_0000_0000 } else { 0 };
1054		for l in 0..2 {
1055			for (wire, word) in messages[l].iter().zip(bytes_to_le_words(inputs[l])) {
1056				w[*wire] = Word(word | dirty_high);
1057			}
1058			// Pad out to whole wires, so the bytes past the key length are populated too.
1059			let mut key_bytes = keys[l].to_vec();
1060			key_bytes.resize(keys[l].len().next_multiple_of(8), if garbage { 0xff } else { 0 });
1061			for (i, chunk) in key_bytes.chunks(8).enumerate() {
1062				w[key_vecs[l].data[i]] = Word(u64::from_le_bytes(chunk.try_into().unwrap()));
1063			}
1064
1065			let mut padded_key = [0u8; KEY_BYTES];
1066			padded_key[..keys[l].len()].copy_from_slice(keys[l]);
1067			let expected = blake3::keyed_hash(&padded_key, inputs[l]);
1068			for i in 0..8 {
1069				let bytes: [u8; 4] = expected.as_bytes()[i * 4..i * 4 + 4].try_into().unwrap();
1070				w[digests_out[l][i]] = Word(u32::from_le_bytes(bytes) as u64);
1071			}
1072		}
1073		circuit.populate_wire_witness(&mut w).unwrap_or_else(|e| {
1074			panic!(
1075				"blake3_keyed_fixed_2x failed for len_bytes={len_bytes}, key_lens=({}, {}): {e:?}",
1076				keys[0].len(),
1077				keys[1].len()
1078			)
1079		});
1080	}
1081
1082	/// Two distinct messages of `len` bytes, and two distinct 32-byte keys.
1083	fn distinct_pair(len: usize) -> ([Vec<u8>; 2], [Vec<u8>; 2]) {
1084		let messages =
1085			std::array::from_fn(|l| (0..len).map(|i| (i * 37 + 1 + l * 91) as u8).collect());
1086		let keys =
1087			std::array::from_fn(|l| (0..KEY_BYTES).map(|i| (i * 7 + 3 + l * 53) as u8).collect());
1088		(messages, keys)
1089	}
1090
1091	#[test]
1092	fn keyed_2x_block_boundaries() {
1093		// Lengths covering 0..=16 blocks in one chunk, both parities, and the block boundaries
1094		// where the trailing block turns partial.
1095		for &len in &[
1096			0usize, 1, 3, 63, 64, 65, 127, 128, 129, 192, 320, 511, 512, 1023, 1024,
1097		] {
1098			let (messages, keys) = distinct_pair(len);
1099			check_keyed_2x([&messages[0], &messages[1]], [&keys[0], &keys[1]], false);
1100		}
1101	}
1102
1103	#[test]
1104	fn keyed_2x_multi_chunk() {
1105		// Lengths spanning 2..=10 chunks, odd counts and a partial final chunk included.
1106		// They reach every part of the tree: the paired parents, the lone-value promotion, the
1107		// root.
1108		for &len in &[1025usize, 2048, 2049, 3072, 5121, 7168, 8192, 9217] {
1109			let (messages, keys) = distinct_pair(len);
1110			check_keyed_2x([&messages[0], &messages[1]], [&keys[0], &keys[1]], false);
1111		}
1112	}
1113
1114	#[test]
1115	fn keyed_2x_lanes_take_different_key_lengths() {
1116		// The keys are independent, so their lengths need not agree.
1117		// Each is zero-padded to 32 bytes on its own, the empty key included.
1118		for (len0, len1) in [(0usize, KEY_BYTES), (1, 31), (5, 9), (23, 4), (16, 16)] {
1119			let key0: Vec<u8> = (0..len0).map(|i| (i * 11 + 5) as u8).collect();
1120			let key1: Vec<u8> = (0..len1).map(|i| (i * 13 + 2) as u8).collect();
1121			check_keyed_2x([b"abc", b"xyz"], [&key0, &key1], false);
1122		}
1123	}
1124
1125	#[test]
1126	fn keyed_2x_lanes_are_independent() {
1127		// Invariant: a lane's digest depends on its own inputs alone.
1128		//
1129		//     message wire = [ high 32: the other lane's word | low 32: this lane's word ]
1130		//
1131		// A prover picks that high half, so masking it is what keeps the two lanes apart.
1132		for &len in &[1usize, 64, 65, 1025] {
1133			let (messages, keys) = distinct_pair(len);
1134			check_keyed_2x([&messages[0], &messages[1]], [&keys[0], &keys[1]], true);
1135		}
1136	}
1137
1138	#[test]
1139	fn keyed_2x_lanes_hashing_the_same_input_agree() {
1140		// Identical inputs must give identical digests, since the packing treats the lanes alike.
1141		// A bug that favours one lane shows up here as a disagreement.
1142		let key: Vec<u8> = (0..KEY_BYTES).map(|i| (i * 7 + 3) as u8).collect();
1143		let message: Vec<u8> = (0..200).map(|i| (i * 37 + 1) as u8).collect();
1144		check_keyed_2x([&message, &message], [&key, &key], true);
1145	}
1146
1147	proptest! {
1148		// Every case compiles a whole two-lane hashing circuit, so the sample stays small.
1149		#![proptest_config(ProptestConfig::with_cases(12))]
1150
1151		#[test]
1152		fn keyed_2x_matches_blake3_crate(
1153			len in 0usize..=300,
1154			bytes0 in prop::collection::vec(any::<u8>(), 300),
1155			bytes1 in prop::collection::vec(any::<u8>(), 300),
1156			key0 in prop::collection::vec(any::<u8>(), 0..=KEY_BYTES),
1157			key1 in prop::collection::vec(any::<u8>(), 0..=KEY_BYTES),
1158		) {
1159			// Random content at a random shared length, against the reference crate's keyed hash.
1160			check_keyed_2x([&bytes0[..len], &bytes1[..len]], [&key0, &key1], true);
1161		}
1162	}
1163
1164	#[test]
1165	#[should_panic(expected = "key length (33) exceeds 32")]
1166	fn keyed_2x_rejects_an_oversized_key() {
1167		let builder = CircuitBuilder::new();
1168		let good = ByteVec::new_const_len(&builder, vec![], 0);
1169		let data: Vec<Wire> = (0..5).map(|_| builder.add_witness()).collect();
1170		let oversized = ByteVec::new_const_len(&builder, data, KEY_BYTES + 1);
1171		blake3_keyed_fixed_2x(&builder, [&[], &[]], 0, [&good, &oversized]);
1172	}
1173
1174	#[test]
1175	#[should_panic(expected = "key length must be fixed at circuit construction time")]
1176	fn keyed_2x_rejects_a_runtime_length_key() {
1177		let builder = CircuitBuilder::new();
1178		let good = ByteVec::new_const_len(&builder, vec![], 0);
1179		// `new_witness` leaves the length range at the full `0..=capacity`, so the gadget cannot
1180		// know which bytes to mask.
1181		let runtime = ByteVec::new_witness(&builder, 4);
1182		blake3_keyed_fixed_2x(&builder, [&[], &[]], 0, [&good, &runtime]);
1183	}
1184
1185	/// AND-constraint counts for hashing two messages under two 32-byte keys.
1186	///
1187	/// The one-lane count, from two calls, then the two-lane count.
1188	fn and_counts(len_bytes: usize) -> (usize, usize) {
1189		let key = |builder: &CircuitBuilder| {
1190			let data: Vec<Wire> = (0..KEY_BYTES / 8).map(|_| builder.add_witness()).collect();
1191			ByteVec::new_const_len(builder, data, KEY_BYTES)
1192		};
1193		let message = |builder: &CircuitBuilder| -> Vec<Wire> {
1194			(0..len_bytes.div_ceil(4))
1195				.map(|_| builder.add_witness())
1196				.collect()
1197		};
1198		// Pin the digests to public wires, so no compression is dead code the builder may drop.
1199		let expose = |builder: &CircuitBuilder, digest: [Wire; 8]| {
1200			for word in digest {
1201				let out = builder.add_inout();
1202				builder.assert_eq("digest", word, out);
1203			}
1204		};
1205
1206		let single = {
1207			let builder = CircuitBuilder::new();
1208			for _ in 0..2 {
1209				let digest =
1210					blake3_keyed_fixed(&builder, &message(&builder), len_bytes, &key(&builder));
1211				expose(&builder, digest);
1212			}
1213			CircuitStat::collect(&builder.build()).n_and_constraints
1214		};
1215		let paired = {
1216			let builder = CircuitBuilder::new();
1217			let (m0, m1) = (message(&builder), message(&builder));
1218			let (k0, k1) = (key(&builder), key(&builder));
1219			for digest in blake3_keyed_fixed_2x(&builder, [&m0, &m1], len_bytes, [&k0, &k1]) {
1220				expose(&builder, digest);
1221			}
1222			CircuitStat::collect(&builder.build()).n_and_constraints
1223		};
1224		(single, paired)
1225	}
1226
1227	#[test]
1228	fn keyed_2x_never_costs_more_than_two_single_hashes() {
1229		// The shapes where the two paths differ:
1230		//
1231		//     0..=64 bytes  one block, the shape a tweakable hash uses
1232		//     65..=128      two blocks, where the single-hash path also fills both lanes
1233		//     129..=192     three blocks, where it falls back to a lone compression
1234		//     1025+         several chunks, so the parent tree runs too
1235		for &len in &[0usize, 1, 64, 65, 128, 192, 1024, 2048] {
1236			let (single, paired) = and_counts(len);
1237			assert!(
1238				paired <= single,
1239				"blake3_keyed_fixed_2x costs {paired} AND constraints at len={len}, \
1240				 against {single} for two single hashes"
1241			);
1242		}
1243
1244		// A one-block message is the shape the gadget exists for, and the only one where it wins
1245		// outright: a lone compression splits its own rounds across the lanes, so the single-hash
1246		// path leaves nothing idle for the pair to reclaim, and the pair's saving is the 3 rounds
1247		// the split spends twice rather than a whole second core.
1248		let (single, paired) = and_counts(BLOCK_BYTES);
1249		assert!(
1250			paired < single,
1251			"a one-block pair costs {paired} AND constraints, no less than the {single} two \
1252			 single hashes cost"
1253		);
1254	}
1255
1256	#[test]
1257	#[should_panic(expected = "message.len() (2) must equal len_bytes.div_ceil(4) (3)")]
1258	fn keyed_2x_rejects_a_message_of_the_wrong_length() {
1259		// Both messages must be exactly the shared length.
1260		let builder = CircuitBuilder::new();
1261		let key = ByteVec::new_const_len(&builder, vec![], 0);
1262		let long: Vec<Wire> = (0..3).map(|_| builder.add_witness()).collect();
1263		let short: Vec<Wire> = (0..2).map(|_| builder.add_witness()).collect();
1264		blake3_keyed_fixed_2x(&builder, [&long, &short], 9, [&key, &key]);
1265	}
1266
1267	#[test]
1268	fn multi_chunk() {
1269		// Lengths spanning 2..=10 chunks, including odd chunk counts (3, 5, 7, 9) and a partial
1270		// final chunk, to exercise the parent tree: the 2x-batched parents, the single-lane
1271		// leftover parent, the lone-chaining-value promotion, and the ROOT node.
1272		for &len in &[
1273			1025usize, // 2 chunks: 16 blocks + 1 block
1274			2048,      // 2 full chunks
1275			2049,      // 3 chunks
1276			3072,      // 3 full chunks
1277			4096,      // 4 full chunks
1278			5121,      // 5 chunks (odd), partial final chunk
1279			7168,      // 7 full chunks
1280			8192,      // 8 full chunks (balanced tree)
1281			9217,      // 9 chunks (odd), partial final chunk
1282			10240,     // 10 full chunks
1283		] {
1284			let input: Vec<u8> = (0..len).map(|i| (i * 37 + 1) as u8).collect();
1285			check(&input);
1286		}
1287	}
1288
1289	/// Hashes `input` with [`Blake3Compress2x`] as a chip, and checks the digest and the system.
1290	///
1291	/// The digest wires are public and filled with the reference digest, as in [`check`], so a
1292	/// disagreement fails to populate. What the chip adds is checked after: the calls each
1293	/// compression made have to be served by an instance that recomputes the same words.
1294	fn check_with_compress_chip(input: &[u8]) {
1295		let builder = CircuitBuilder::new();
1296		builder.register_chip(Blake3Compress2x, &[]);
1297
1298		let message: Vec<Wire> = (0..input.len().div_ceil(4))
1299			.map(|_| builder.add_witness())
1300			.collect();
1301		let digest = blake3_fixed(&builder, &message, input.len());
1302		let digest_out: [Wire; 8] = std::array::from_fn(|_| builder.add_inout());
1303		for i in 0..8 {
1304			builder.assert_eq("digest_match", digest[i], digest_out[i]);
1305		}
1306
1307		let circuit = builder.build_m4();
1308		circuit.validate().unwrap();
1309		let cs = circuit.to_constraint_system();
1310		cs.validate().unwrap();
1311
1312		let expected = blake3::hash(input);
1313		let expected_words: [u32; 8] = std::array::from_fn(|i| {
1314			u32::from_le_bytes(expected.as_bytes()[i * 4..i * 4 + 4].try_into().unwrap())
1315		});
1316
1317		let witness = circuit
1318			.generate_witness(|w| {
1319				for (wire, word) in message.iter().zip(bytes_to_le_words(input)) {
1320					w[*wire] = Word(word);
1321				}
1322				for i in 0..8 {
1323					w[digest_out[i]] = Word(expected_words[i] as u64);
1324				}
1325			})
1326			.unwrap_or_else(|e| panic!("blake3_fixed failed for len_bytes={}: {e:?}", input.len()));
1327
1328		witness.verify(&cs).unwrap();
1329	}
1330
1331	// The gadgets between `blake3_fixed` and `blake3_compress_2x` are untouched by the chip: the
1332	// compressions the chunk pairs reach and the ones the parent tree reaches both land as calls
1333	// because the builder holds the chip, not because anything in between was told.
1334	//
1335	// Lengths run from two blocks, which is the shortest message reaching a paired compression at
1336	// all, up through an odd chunk count, whose tree carries a two-lane parent as well as a
1337	// single-lane one.
1338	#[test]
1339	fn a_registered_chip_serves_every_compression() {
1340		for &len in &[128usize, 320, 1025, 5121] {
1341			let input: Vec<u8> = (0..len).map(|i| (i * 37 + 1) as u8).collect();
1342			check_with_compress_chip(&input);
1343		}
1344	}
1345
1346	// A message short enough to compress one block at a time never reaches the paired gadget, so
1347	// its chip goes uncalled and the system it leaves is not one that can be populated.
1348	#[test]
1349	fn a_chip_no_compression_reaches_leaves_an_uncalled_chip() {
1350		let builder = CircuitBuilder::new();
1351		builder.register_chip(Blake3Compress2x, &[]);
1352		blake3_fixed(&builder, &[builder.add_witness()], 4);
1353
1354		let error = builder.build_m4().validate().unwrap_err();
1355		assert!(matches!(error, binius_frontend::CircuitM4Error::NeverCalled { .. }), "{error:?}");
1356	}
1357}