Skip to main content

binius_circuits/blake2s/
mod.rs

1// Copyright 2026 The Binius Developers
2// Copyright 2025 Irreducible Inc.
3//! BLAKE2s hash function circuit.
4//!
5//! BLAKE2s is a cryptographic hash function optimized for 32-bit platforms.
6//!
7//! It produces digests from 1 to 32 bytes.
8//!
9//! This implementation follows RFC 7693.
10//!
11//! It supports fixed-length messages with unkeyed hashing.
12//!
13//! ## RFC 7693 compliance
14//!
15//! This implementation is fully compliant with RFC 7693 for the core BLAKE2s-256 hash function.
16//!
17//! The compression function, the G mixing function, and the message scheduling all match the
18//! specification.
19//!
20//! ## Excluded features
21//!
22//! This circuit intentionally excludes the following optional features from RFC 7693:
23//!
24//! - Keyed hashing, also called MAC mode: only unkeyed hash verification is supported.
25//! - The 8-byte salt field.
26//! - The 8-byte personalization field.
27//! - Tree hashing mode: only sequential mode is supported.
28//! - Runtime-variable message length: the length is fixed at circuit construction time instead.
29//! - Variable output length: the digest is fixed at 256 bits.
30//! - Messages of 4 GiB or more.
31//!
32//! The high half of the byte counter is always the zero constant, which caps the supported
33//! message length at just under 4 GiB.
34//!
35//! These exclusions suit a circuit whose job is hash verification, rather than
36//! general-purpose hashing.
37//!
38//! # Algorithm overview
39//!
40//! BLAKE2s processes a message in 64-byte blocks.
41//!
42//! Each block goes through a compression function built from a modified ChaCha cipher core.
43//!
44//! A compression runs ten mixing rounds, and each round mixes the internal state with the
45//! message block through eight calls to the G mixing function.
46//!
47//! # Circuit design
48//!
49//! This circuit verifies that a message of a fixed, compile-time-known length produces a
50//! specific BLAKE2s digest.
51//!
52//! Blocks chain sequentially: each one's input state is the previous one's output.
53//!
54//! So consecutive blocks are compressed two at a time, packing both compressions into the two
55//! 32-bit lanes of one parallel core.
56//!
57//! A trailing block with no partner runs through the same paired core with its second lane
58//! left dead, except when the whole message is a single block, which stays fully single-lane.
59
60mod compress;
61mod constants;
62#[cfg(test)]
63mod tests;
64
65use binius_core::word::Word;
66use binius_frontend::{CircuitBuilder, Wire, WitnessFiller};
67pub use compress::{
68	Blake2sCompress2x, blake2s_compress, blake2s_compress_2x, blake2s_compress_2x_seq, ref_compress,
69};
70use constants::IV;
71
72use crate::util::clear_high_bits;
73
74/// One message block's padded words, plus the counter and flag values its compression needs.
75struct BlockInput {
76	/// The 16-word padded message block, one 32-bit value per wire.
77	m: [Wire; 16],
78	/// The low 32 bits of the byte counter after absorbing this block.
79	t_lo: Wire,
80	/// The finalization flag: all-ones if this is the final block, zero otherwise.
81	last: Wire,
82}
83
84/// BLAKE2s hash function circuit for a fixed-length message.
85///
86/// This struct is a complete circuit that verifies a message of a fixed, compile-time-known
87/// length produces a specific 256-bit digest.
88///
89/// The message bytes are packed little-endian into 64-bit words.
90pub struct Blake2s {
91	/// Message size in bytes this circuit supports.
92	pub length: usize,
93	/// Witness wires for the input message, packed little-endian into 64-bit words.
94	pub message: Vec<Wire>,
95	/// Witness wires for the expected 256-bit digest, as 8 32-bit words.
96	pub digest: [Wire; 8],
97}
98
99impl Blake2s {
100	/// Creates a new BLAKE2s circuit with witness variables.
101	///
102	/// The message length is fixed at circuit construction time, so it shapes the circuit
103	/// rather than being a witness value itself.
104	///
105	/// # Arguments
106	/// * `builder` - Circuit builder to add constraints to.
107	/// * `length` - The exact message size, in bytes, this circuit will verify.
108	///
109	/// # Returns
110	/// A struct holding the witness wires for the message and the expected digest.
111	pub fn new_witness(builder: &mut CircuitBuilder, length: usize) -> Self {
112		// One witness wire per 8 bytes of the message.
113		let message: Vec<Wire> = (0..length.div_ceil(8))
114			.map(|_| builder.add_witness())
115			.collect();
116		let digest = std::array::from_fn(|_| builder.add_witness());
117
118		Self::build_circuit(builder, length, &message, digest);
119
120		Self {
121			length,
122			message,
123			digest,
124		}
125	}
126
127	/// Builds the constraints that verify the message hashes to the expected digest.
128	fn build_circuit(
129		builder: &CircuitBuilder,
130		length: usize,
131		message: &[Wire],
132		expected_digest: [Wire; 8],
133	) {
134		// A message that exactly fills whole blocks still needs at least one block.
135		let num_blocks = length.div_ceil(64).max(1);
136		let zero = builder.add_constant(Word(0));
137
138		// The BLAKE2s-256 parameter block folds into the first IV word: unkeyed (key length
139		// zero), 32-byte digest, sequential mode (fanout 1, depth 1).
140		let init_state = [
141			builder.add_constant_64((IV[0] ^ 0x01010020) as u64),
142			builder.add_constant_64(IV[1] as u64),
143			builder.add_constant_64(IV[2] as u64),
144			builder.add_constant_64(IV[3] as u64),
145			builder.add_constant_64(IV[4] as u64),
146			builder.add_constant_64(IV[5] as u64),
147			builder.add_constant_64(IV[6] as u64),
148			builder.add_constant_64(IV[7] as u64),
149		];
150
151		// Every block's padded message words and counter/flag values, computed up front.
152		//
153		// That lets the compression chain below pair consecutive blocks without interleaving
154		// padding logic.
155		let blocks: Vec<BlockInput> = (0..num_blocks)
156			.map(|block_idx| Self::block_input(builder, message, length, block_idx, zero))
157			.collect();
158
159		// Consecutive blocks chain: each one's input state is the previous one's output.
160		//
161		// So a pair of blocks is compressed through one parallel core, at roughly half the AND
162		// cost of two single-lane compressions.
163		//
164		// The threaded state carries the pair's first compression in its high half.
165		//
166		// That half is left as it is, rather than masked off, since nothing downstream reads it
167		// before the final digest:
168		//
169		// - A compression never lets a carry or a rotate cross bit 32, so the halves stay apart.
170		// - The paired core takes an input state's low half only, through a left shift.
171		//
172		// So the low half of a result depends on the low halves of its inputs alone.
173		let mut h = init_state;
174		let mut block_idx = 0;
175		while block_idx + 1 < num_blocks {
176			let sub =
177				builder.subcircuit(format!("blake2s_compress[{block_idx}..{}]", block_idx + 2));
178			let a = &blocks[block_idx];
179			let b = &blocks[block_idx + 1];
180			h = blake2s_compress_2x_seq(
181				&sub,
182				h,
183				[a.m, b.m],
184				[a.t_lo, b.t_lo],
185				[zero, zero],
186				[a.last, b.last],
187			);
188			block_idx += 2;
189		}
190		if block_idx < num_blocks {
191			let sub = builder.subcircuit(format!("blake2s_compress[{block_idx}]"));
192			let b = &blocks[block_idx];
193			h = if block_idx > 0 {
194				// The trailing odd block has no partner.
195				//
196				// It still runs through the paired core with its second lane dead, so a
197				// registered chip serves every compression uniformly.
198				//
199				// In gates the two cores emit the same circuit, so nothing changes without a
200				// chip.
201				blake2s_compress_2x(&sub, h, b.m, b.t_lo, zero, b.last)
202			} else {
203				// A single-block message keeps the single-lane core.
204				//
205				// Its empty high halves are what lets the digest skip the clearing below.
206				blake2s_compress(&sub, h, b.m, b.t_lo, zero, b.last)
207			};
208		}
209
210		// The escaping digest is the one place a clean high half is required.
211		//
212		// So clear it once here, rather than after every pair.
213		//
214		// A single-block message never enters the paired core, so its one-lane result already
215		// has an empty high half and needs no clearing.
216		let final_digest: [Wire; 8] = if num_blocks < 2 {
217			h
218		} else {
219			std::array::from_fn(|i| clear_high_bits(builder, h[i], 32))
220		};
221
222		for i in 0..8 {
223			builder.assert_eq("digest_match", final_digest[i], expected_digest[i]);
224		}
225	}
226
227	/// Builds one block's padded message words, and its counter and finalization-flag values.
228	///
229	/// The high half of the byte counter is not part of the returned value.
230	///
231	/// It is always the zero constant, per this module's 4 GiB message-size limit, so every
232	/// caller shares one wire for it.
233	fn block_input(
234		builder: &CircuitBuilder,
235		message: &[Wire],
236		length: usize,
237		block_idx: usize,
238		zero: Wire,
239	) -> BlockInput {
240		let mut m = [zero; 16];
241
242		for word_idx in 0..16 {
243			// The message is packed 8 bytes per wire, so two consecutive 32-bit words share
244			// one 64-bit wire.
245			let message_qword = *message.get(block_idx << 3 | word_idx >> 1).unwrap_or(&zero);
246
247			// Take the low or the high 32 bits of that wire, depending on which of the pair
248			// this word is.
249			let message_dword = if word_idx % 2 == 0 {
250				clear_high_bits(builder, message_qword, 32)
251			} else {
252				builder.shr(message_qword, 32)
253			};
254
255			// Mask off any bytes past the message's true length.
256			//
257			// A word entirely past the end becomes the zero constant.
258			//
259			// A word straddling the end keeps only its valid leading bytes.
260			let first_byte_offset = block_idx * 64 + word_idx * 4;
261			let padded_message_dword = if first_byte_offset + 4 > length {
262				if first_byte_offset < length {
263					let nonzero_bytes = (length - first_byte_offset) as u32;
264					builder.band(
265						message_dword,
266						builder.add_constant(Word::ALL_ONE >> (64 - nonzero_bytes * 8)),
267					)
268				} else {
269					zero
270				}
271			} else {
272				message_dword
273			};
274
275			m[word_idx] = padded_message_dword;
276		}
277
278		// The final block is the one whose byte range contains the message's true length.
279		//
280		// Block 0 always counts as in range, so a zero-length message still has one block.
281		let block_start = (block_idx * 64) as u64;
282		let block_end = block_start + 64;
283		let is_final_block =
284			(block_idx == 0 || block_start < length as u64) && length as u64 <= block_end;
285
286		// The byte counter after this block is the block's end offset, except on the final
287		// block, where it is the message's true length instead.
288		let t_lo = builder.add_constant_64(if is_final_block {
289			length as u64
290		} else {
291			block_end
292		});
293
294		// The finalization flag is all-ones on the final block, and zero otherwise.
295		let flag_value = builder.add_constant(Word(0xFFFFFFFF));
296		let last = if is_final_block { flag_value } else { zero };
297
298		BlockInput { m, t_lo, last }
299	}
300
301	/// Populates the message witness wires.
302	///
303	/// # Arguments
304	/// * `witness` - Witness filler to populate.
305	/// * `message` - The message bytes to hash.
306	///
307	/// # Panics
308	/// * If `message.len()` does not equal the circuit's fixed message length.
309	pub fn populate_message(&self, witness: &mut WitnessFiller<'_>, message: &[u8]) {
310		assert!(
311			message.len() == self.length,
312			"Only messages of length {} supported while given {} bytes",
313			self.length,
314			message.len(),
315		);
316
317		for (i, bytes) in message.chunks(8).enumerate() {
318			let mut le_bytes = [0; 8];
319			le_bytes[..bytes.len()].copy_from_slice(bytes);
320			witness[self.message[i]] = Word(u64::from_le_bytes(le_bytes));
321		}
322	}
323
324	/// Populates the expected-digest witness wires.
325	///
326	/// # Arguments
327	/// * `witness` - Witness filler to populate.
328	/// * `digest` - The expected 32-byte BLAKE2s digest.
329	pub fn populate_digest(&self, witness: &mut WitnessFiller<'_>, digest: &[u8; 32]) {
330		for i in 0..8 {
331			let word_bytes = &digest[i * 4..(i + 1) * 4];
332			let word = u32::from_le_bytes(word_bytes.try_into().unwrap());
333			witness[self.digest[i]] = Word(word as u64);
334		}
335	}
336}