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}