Skip to main content

binius_circuits/
ripemd.rs

1// Copyright 2025 Irreducible Inc.
2use binius_core::word::Word;
3use binius_frontend::{CircuitBuilder, Wire};
4
5/// Computes the RIPEMD-160 compression function on a single 512-bit block.
6///
7/// This function implements the core compression function of RIPEMD-160, which processes
8/// a single 512-bit (64-byte) message block and updates the 160-bit internal state.
9///
10/// # Arguments
11/// * `builder` - Circuit builder for constructing constraints
12/// * `state` - Current hash state as 5 wires, each containing a 32-bit word in the low 32 bits
13///   (high 32 bits must be zero). The state words are in little-endian byte order.
14/// * `message_block` - Message block as 16 wires, each containing a 32-bit word in the low 32 bits
15///   (high 32 bits must be zero). The words are in little-endian byte order.
16///
17/// # Returns
18/// * `[Wire; 5]` - Updated hash state as 5 wires, each containing a 32-bit word in the low 32 bits
19///   (high 32 bits are zero). The state words are in little-endian byte order.
20///
21/// # Preconditions
22/// * All input wires must have their high 32 bits set to zero (i.e., `wire & 0xFFFFFFFF == wire`)
23/// * This is the caller's responsibility to ensure
24///
25/// # Implementation Notes
26/// * RIPEMD-160 uses two parallel computation paths (left and right lines) that are combined
27/// * Each line performs 80 rounds (5 groups of 16 rounds each)
28/// * The final state is computed by adding the results of both lines to the input state
29fn ripemd160_compress(
30	builder: &CircuitBuilder,
31	state: [Wire; 5],
32	message_block: [Wire; 16],
33) -> [Wire; 5] {
34	// Constants for left line rounds
35	const KL: [u32; 5] = [0x00000000, 0x5A827999, 0x6ED9EBA1, 0x8F1BBCDC, 0xA953FD4E];
36
37	// Constants for right line rounds
38	const KR: [u32; 5] = [0x50A28BE6, 0x5C4DD124, 0x6D703EF3, 0x7A6D76E9, 0x00000000];
39
40	// Message permutation for left line
41	const ZL: [usize; 80] = [
42		0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, // rounds 0-15
43		7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8, // rounds 16-31
44		3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12, // rounds 32-47
45		1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2, // rounds 48-63
46		4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13, // rounds 64-79
47	];
48
49	// Message permutation for right line
50	const ZR: [usize; 80] = [
51		5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12, // rounds 0-15
52		6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2, // rounds 16-31
53		15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13, // rounds 32-47
54		8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14, // rounds 48-63
55		12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11, // rounds 64-79
56	];
57
58	// Rotation amounts for left line
59	const SL: [u32; 80] = [
60		11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8, // rounds 0-15
61		7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12, // rounds 16-31
62		11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5, // rounds 32-47
63		11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12, // rounds 48-63
64		9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6, // rounds 64-79
65	];
66
67	// Rotation amounts for right line
68	const SR: [u32; 80] = [
69		8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6, // rounds 0-15
70		9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11, // rounds 16-31
71		9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5, // rounds 32-47
72		15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8, // rounds 48-63
73		8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11, // rounds 64-79
74	];
75
76	// Initialize working variables for left line
77	let mut al = state[0];
78	let mut bl = state[1];
79	let mut cl = state[2];
80	let mut dl = state[3];
81	let mut el = state[4];
82
83	// Initialize working variables for right line
84	let mut ar = state[0];
85	let mut br = state[1];
86	let mut cr = state[2];
87	let mut dr = state[3];
88	let mut er = state[4];
89
90	// Process 80 rounds for each line
91	for round_num in 0..80 {
92		let round_group = round_num / 16;
93
94		// Left line
95		let func_l = match round_group {
96			0 => f,
97			1 => g,
98			2 => h,
99			3 => i,
100			4 => j,
101			_ => unreachable!(),
102		};
103
104		let new_bl = round(
105			builder,
106			al,
107			bl,
108			cl,
109			dl,
110			el,
111			message_block[ZL[round_num]],
112			SL[round_num],
113			KL[round_group],
114			func_l,
115		);
116
117		// Rotate variables for left line
118		al = el;
119		el = dl;
120		dl = builder.rotl32(cl, 10);
121		cl = bl;
122		bl = new_bl;
123
124		// Right line
125		let func_r = match round_group {
126			0 => j,
127			1 => i,
128			2 => h,
129			3 => g,
130			4 => f,
131			_ => unreachable!(),
132		};
133
134		let new_br = round(
135			builder,
136			ar,
137			br,
138			cr,
139			dr,
140			er,
141			message_block[ZR[round_num]],
142			SR[round_num],
143			KR[round_group],
144			func_r,
145		);
146
147		// Rotate variables for right line
148		ar = er;
149		er = dr;
150		dr = builder.rotl32(cr, 10);
151		cr = br;
152		br = new_br;
153	}
154
155	// Combine results: state[i] = state[i] + cl + dr (and appropriate permutation)
156	// Mask to 32 bits: intermediate round values may have non-zero upper halves
157	// (e.g. from bnot which produces a full 64-bit NOT), so clear them before output.
158	[
159		builder.iadd_32(builder.iadd_32(state[1], cl), dr),
160		builder.iadd_32(builder.iadd_32(state[2], dl), er),
161		builder.iadd_32(builder.iadd_32(state[3], el), ar),
162		builder.iadd_32(builder.iadd_32(state[4], al), br),
163		builder.iadd_32(builder.iadd_32(state[0], bl), cr),
164	]
165	.map(|w| builder.shr(builder.shl(w, 32), 32))
166}
167
168// Selection function f(x, y, z) = x XOR y XOR z
169fn f(b: &CircuitBuilder, x: Wire, y: Wire, z: Wire) -> Wire {
170	b.bxor(b.bxor(x, y), z)
171}
172
173// Selection function g(x, y, z) = (x AND y) OR (NOT x AND z) = z XOR (x AND (y XOR z))
174fn g(b: &CircuitBuilder, x: Wire, y: Wire, z: Wire) -> Wire {
175	b.bxor(z, b.band(x, b.bxor(y, z)))
176}
177
178// Selection function h(x, y, z) = (x OR NOT y) XOR z
179fn h(b: &CircuitBuilder, x: Wire, y: Wire, z: Wire) -> Wire {
180	b.bxor(b.bor(x, b.bnot(y)), z)
181}
182
183// Selection function i(x, y, z) = (x AND z) OR (y AND NOT z) = y XOR (z AND (x XOR y))
184fn i(b: &CircuitBuilder, x: Wire, y: Wire, z: Wire) -> Wire {
185	b.bxor(y, b.band(z, b.bxor(x, y)))
186}
187
188// Selection function j(x, y, z) = x XOR (y OR NOT z)
189fn j(b: &CircuitBuilder, x: Wire, y: Wire, z: Wire) -> Wire {
190	b.bxor(x, b.bor(y, b.bnot(z)))
191}
192
193// RIPEMD-160 round function
194#[allow(clippy::too_many_arguments)]
195fn round(
196	builder: &CircuitBuilder,
197	a: Wire,
198	b: Wire,
199	c: Wire,
200	d: Wire,
201	e: Wire,
202	x: Wire,
203	s: u32,
204	k: u32,
205	func: fn(&CircuitBuilder, Wire, Wire, Wire) -> Wire,
206) -> Wire {
207	// T = A + func(B, C, D) + X + K
208	let f_val = func(builder, b, c, d);
209	let t1 = builder.iadd_32(a, f_val);
210	let t2 = builder.iadd_32(t1, x);
211	let t = builder.iadd_32(t2, builder.add_constant_64(k as u64));
212
213	// T = (T << s) | (T >> (32 - s)) (rotate left by s)
214	let t_rot = builder.rotl32(t, s);
215
216	// T = T + E (this becomes the new B value)
217	builder.iadd_32(t_rot, e)
218}
219
220/// Computes RIPEMD-160 hash of a fixed-length message.
221///
222/// This function creates a subcircuit that computes the RIPEMD-160 hash of a message
223/// with a compile-time known length. Unlike a variable-length RIPEMD-160 implementation,
224/// this function is optimized for fixed-length inputs where the length is known at
225/// circuit construction time.
226///
227/// See [Pseudo-code for RIPEMD-160](https://homes.esat.kuleuven.be/~bosselae/ripemd/rmd160.txt)
228/// for reference.
229///
230/// # Arguments
231/// * `builder` - Circuit builder for constructing constraints
232/// * `message` - Input message as 32-bit words (4 bytes per wire) in little-endian format. Each
233///   wire must have the high 32 bits set to zero (precondition).
234/// * `len_bytes` - The fixed length of the message in bytes (known at compile time)
235///
236/// # Returns
237/// * `[Wire; 5]` - The RIPEMD-160 digest as 5 wires, each containing a 32-bit word in the low 32
238///   bits (high 32 bits are zero) in little-endian order
239///
240/// # Panics
241/// * If `message.len()` does not equal exactly `len_bytes.div_ceil(4)`
242/// * If the message length in bits cannot fit in 32 bits
243///
244/// # Preconditions
245/// * The caller must constrain the `message` wires to have their high 32 bits set to zero.
246///
247/// # Example
248/// ```rust,ignore
249/// use binius_circuits::ripemd::ripemd160_fixed;
250/// use binius_frontend::compiler::CircuitBuilder;
251///
252/// let mut builder = CircuitBuilder::new();
253///
254/// // Create input wires for a 32-byte message (8 32-bit words)
255/// let message: Vec<_> = (0..8).map(|_| builder.add_witness()).collect();
256///
257/// // Compute RIPEMD-160 of the 32-byte message
258/// let digest = ripemd160_fixed(&builder, &message, 32);
259/// ```
260pub fn ripemd160_fixed(builder: &CircuitBuilder, message: &[Wire], len_bytes: usize) -> [Wire; 5] {
261	// RIPEMD-160 initialization vector
262	const H0: [u32; 5] = [
263		0x6745_2301,
264		0xEFCD_AB89,
265		0x98BA_DCFE,
266		0x1032_5476,
267		0xC3D2_E1F0,
268	];
269
270	// Validate that message.len() equals exactly len_bytes.div_ceil(4)
271	assert_eq!(
272		message.len(),
273		len_bytes.div_ceil(4),
274		"message.len() ({}) must equal len_bytes.div_ceil(4) ({})",
275		message.len(),
276		len_bytes.div_ceil(4)
277	);
278
279	// Ensure message length in bits fits in 32 bits
280	assert!(
281		(len_bytes as u64)
282			.checked_mul(8)
283			.is_some_and(|bits| bits <= u32::MAX as u64),
284		"Message length in bits must fit in 32 bits"
285	);
286
287	// Calculate padding requirements
288	// RIPEMD-160 requires: message || 0x80 || zeros || 64-bit length field
289	// The 64-bit length field goes in the last 8 bytes of a block
290	// We need at least 9 bytes for padding (1 for 0x80 + 8 for length)
291	let n_blocks = (len_bytes + 9).div_ceil(64);
292	let n_padded_words = n_blocks * 16; // 16 32-bit words per block
293
294	// Create padded message
295	let mut padded_message = Vec::with_capacity(n_padded_words);
296
297	// Add message words
298	let n_message_words = len_bytes / 4;
299	let boundary_bytes = len_bytes % 4;
300
301	// Add complete message words
302	padded_message.extend_from_slice(&message[0..n_message_words]);
303
304	// Handle partial word at boundary
305	if boundary_bytes > 0 {
306		// The last message word contains partial data
307		let last_word = message[n_message_words];
308
309		// Mask out the unused bytes and add delimiter
310		// For little-endian, the delimiter goes in the byte after the message
311		let shift_amount = boundary_bytes * 8;
312		let mask = builder.add_constant(Word((1u64 << shift_amount) - 1));
313		let masked = builder.band(last_word, mask);
314
315		// Add 0x80 delimiter at the right position (little-endian)
316		let delimiter = builder.add_constant(Word(0x80u64 << shift_amount));
317		let boundary_word = builder.bxor(masked, delimiter);
318
319		padded_message.push(boundary_word);
320	} else {
321		// Message ends at word boundary - delimiter goes in new word
322		padded_message.push(builder.add_constant(Word(0x80)));
323	}
324
325	// Fill with zeros until we reach the length field position
326	let zero = builder.add_constant(Word::ZERO);
327	padded_message.resize(n_padded_words - 2, zero);
328
329	// Add the length field (64 bits total, little-endian)
330	let bitlen = (len_bytes as u64) * 8;
331	padded_message.push(builder.add_constant(Word(bitlen & 0xFFFFFFFF))); // Low 32 bits
332	padded_message.push(builder.add_constant(Word(bitlen >> 32))); // High 32 bits (always 0 for us)
333
334	// Initialize state with RIPEMD-160 IV
335	let initial_state = [
336		builder.add_constant(Word(H0[0] as u64)),
337		builder.add_constant(Word(H0[1] as u64)),
338		builder.add_constant(Word(H0[2] as u64)),
339		builder.add_constant(Word(H0[3] as u64)),
340		builder.add_constant(Word(H0[4] as u64)),
341	];
342
343	// Process compression blocks
344	padded_message
345		.chunks_exact(16)
346		.enumerate()
347		.fold(initial_state, |state, (block_idx, block)| {
348			let block_message: [Wire; 16] = block
349				.try_into()
350				.expect("length of padded_message is a multiple of 16 by construction");
351
352			ripemd160_compress(
353				&builder.subcircuit(format!("ripemd160_compress[{block_idx}]")),
354				state,
355				block_message,
356			)
357		})
358}
359
360#[cfg(test)]
361mod tests {
362	use std::{array, iter, iter::repeat_with};
363
364	use rand::prelude::*;
365	use ripemd::Digest;
366
367	use super::*;
368
369	// Helper function for ripemd160_fixed tests
370	fn test_ripemd160_fixed_with_input(message: &[u8], expected_bytes: [u8; 20]) {
371		let b = CircuitBuilder::new();
372
373		// Pack message into 32-bit words (little-endian)
374		let n_words = message.len().div_ceil(4);
375
376		let message_wires = repeat_with(|| b.add_inout())
377			.take(n_words)
378			.collect::<Vec<_>>();
379
380		// Create expected digest wires (5 32-bit words)
381		let expected_digest_wires = array::from_fn::<_, 5, _>(|_| b.add_inout());
382
383		// Compute the digest
384		assert_eq!(message_wires.len(), message.len().div_ceil(4));
385		assert_eq!(message_wires.len(), n_words);
386		let computed_digest = ripemd160_fixed(&b, &message_wires, message.len());
387
388		// Assert that computed digest equals expected digest
389		for i in 0..5 {
390			b.assert_eq(format!("digest[{i}]"), computed_digest[i], expected_digest_wires[i]);
391		}
392
393		let circuit = b.build();
394		let cs = circuit.constraint_system();
395		let mut w = circuit.new_witness_filler();
396
397		// Populate the message wires (little-endian)
398		for (&wire, chunk) in iter::zip(&message_wires, message.chunks(4)) {
399			let mut word_bytes = [0u8; 4];
400			word_bytes[..chunk.len()].copy_from_slice(chunk);
401			let word = u32::from_le_bytes(word_bytes);
402			w[wire] = Word(word as u64);
403		}
404
405		// Populate the expected digest wires (little-endian)
406		for (&wire, chunk) in iter::zip(&expected_digest_wires, expected_bytes.chunks(4)) {
407			let mut word_bytes = [0u8; 4];
408			word_bytes[..chunk.len()].copy_from_slice(chunk);
409			let word = u32::from_le_bytes(word_bytes);
410			w[wire] = Word(word as u64);
411		}
412
413		circuit.populate_wire_witness(&mut w).unwrap();
414		cs.verify(&w.into_value_vec()).unwrap();
415	}
416
417	#[test]
418	fn test_ripemd160_fixed_various_sizes() {
419		let mut rng = StdRng::seed_from_u64(0);
420
421		// Test various message sizes to ensure padding works correctly
422		let sizes = vec![
423			0,   // empty
424			1,   // single byte
425			3,   // "abc" test vector
426			4,   // exactly one word
427			5,   // just over word boundary
428			31,  // just under half block
429			32,  // exactly half block
430			33,  // just over half block
431			55,  // max single block
432			56,  // forces two blocks
433			63,  // one byte from block boundary
434			64,  // exactly one block
435			65,  // just over one block
436			119, // max two blocks
437			120, // forces three blocks
438			128, // exactly two blocks
439			256, // exactly four blocks
440		];
441
442		for size in sizes {
443			// Generate random payload
444			let mut message = vec![0u8; size];
445			rng.fill(&mut message[..]);
446
447			// Compute expected hash using ripemd crate
448			let expected = ripemd::Ripemd160::digest(&message);
449			let expected_bytes: [u8; 20] = expected.into();
450
451			// Test with our circuit
452			test_ripemd160_fixed_with_input(&message, expected_bytes);
453		}
454	}
455
456	#[test]
457	#[should_panic(expected = "message.len() (1) must equal len_bytes.div_ceil(4) (2)")]
458	fn test_ripemd160_fixed_with_insufficient_wires() {
459		let builder = CircuitBuilder::new();
460
461		// Create only 1 wire but claim message is 5 bytes (which needs 2 wires)
462		let message_wires: Vec<Wire> = vec![builder.add_witness()];
463
464		// This should panic because message.len() (1) != len_bytes.div_ceil(4) (2)
465		ripemd160_fixed(&builder, &message_wires, 5);
466	}
467}