Skip to main content

binius_circuits/keccak/
fixed_length.rs

1// Copyright 2025 Irreducible Inc.
2
3use binius_core::word::Word;
4use binius_frontend::{CircuitBuilder, Wire};
5
6use super::{
7	N_WORDS_PER_BLOCK, N_WORDS_PER_DIGEST, N_WORDS_PER_STATE, RATE_BYTES, permutation::keccak_f1600,
8};
9
10/// Computes the Keccak-256 hash of a fixed-length message.
11///
12/// This function creates a circuit that computes the Keccak-256 digest of a message
13/// with a known, fixed length at circuit construction time. It's more efficient than
14/// the variable-length version as it doesn't need runtime length checks or multiplexing.
15///
16/// # Arguments
17/// * `builder` - Circuit builder for constructing constraints
18/// * `message` - Input message as packed 64-bit words (8 bytes per wire)
19/// * `len_bytes` - The exact length of the message in bytes
20///
21/// # Returns
22/// * `[Wire; 4]` - The Keccak-256 digest as 4 wires of 64 bits each
23///
24/// # Panics
25/// * If `message.len()` does not equal exactly `len_bytes.div_ceil(8)`
26///
27/// # Example
28/// ```rust,ignore
29/// use binius_circuits::keccak::fixed_length::keccak256;
30/// use binius_frontend::CircuitBuilder;
31///
32/// let builder = CircuitBuilder::new();
33///
34/// // Create input wires for a 32-byte message
35/// let message: Vec<_> = (0..4).map(|_| builder.add_witness()).collect();
36///
37/// // Compute Keccak-256 of the 32-byte message
38/// let digest = keccak256(&builder, &message, 32);
39/// ```
40pub fn keccak256(
41	builder: &CircuitBuilder,
42	message: &[Wire],
43	len_bytes: usize,
44) -> [Wire; N_WORDS_PER_DIGEST] {
45	// Validate that message.len() equals exactly len_bytes.div_ceil(8)
46	assert_eq!(
47		message.len(),
48		len_bytes.div_ceil(8),
49		"message.len() ({}) must equal len_bytes.div_ceil(8) ({})",
50		message.len(),
51		len_bytes.div_ceil(8)
52	);
53
54	// Calculate number of blocks needed
55	let n_blocks = (len_bytes + 1).div_ceil(RATE_BYTES);
56	let n_padded_words = n_blocks * N_WORDS_PER_BLOCK;
57
58	// Create padded message
59	let mut padded_message = Vec::with_capacity(n_padded_words);
60
61	// Apply Keccak padding within the circuit
62	// The padding consists of 0x01 byte after the message and 0x80 in the final byte of the block
63	if len_bytes.is_multiple_of(8) {
64		// Message ends on a word boundary - all words are complete
65		padded_message.extend_from_slice(message);
66		// The 0x01 byte goes at the start of the next word
67		padded_message.push(builder.add_constant(Word(0x01)));
68	} else {
69		// Message ends mid-word - need to handle boundary word
70		padded_message.extend_from_slice(&message[..message.len() - 1]);
71
72		// Handle the last message word which is partial
73		let last_idx = message.len() - 1;
74		let byte_in_word = len_bytes % 8;
75
76		// Mask out the invalid bytes from the original word
77		// Create a mask with 1s for valid bytes and 0s for invalid bytes
78		let mask = (1u64 << (byte_in_word * 8)) - 1;
79		let masked_word = builder.band(message[last_idx], builder.add_constant(Word(mask)));
80
81		// Add 0x01 padding byte right after the valid bytes
82		let padding_bit = 1u64 << (byte_in_word * 8);
83		let boundary_word = builder.bxor(masked_word, builder.add_constant(Word(padding_bit)));
84		padded_message.push(boundary_word);
85	}
86
87	// Fill with zeros to complete the padded message
88	let zero = builder.add_constant(Word::ZERO);
89	padded_message.resize(n_padded_words, zero);
90
91	// XOR 0x80 into the last byte of the last block
92	// This correctly handles the case where 0x01 is already in that position
93	let last_byte_mask = 0x80u64 << 56; // 0x80 in the most significant byte
94	let last_idx = n_padded_words - 1;
95	padded_message[last_idx] =
96		builder.bxor(padded_message[last_idx], builder.add_constant(Word(last_byte_mask)));
97
98	// Initialize state to zeros
99	let zero = builder.add_constant(Word::ZERO);
100	let mut state = [zero; N_WORDS_PER_STATE];
101
102	// Process each block
103	for block in padded_message.chunks(N_WORDS_PER_BLOCK) {
104		// XOR the block into the state (first N_WORDS_PER_BLOCK words)
105		for (i, &word) in block.iter().enumerate() {
106			state[i] = builder.bxor(state[i], word);
107		}
108
109		// Apply Keccak-f[1600] permutation
110		keccak_f1600(builder, &mut state);
111	}
112
113	// Return the first 4 words (256 bits) of the state as the digest
114	[state[0], state[1], state[2], state[3]]
115}
116
117#[cfg(test)]
118mod tests {
119	use binius_frontend::CircuitBuilder;
120	use rand::prelude::*;
121	use rstest::rstest;
122	use sha3::{Digest, Keccak256};
123
124	use super::*;
125
126	#[rstest]
127	#[case(0)] // Empty message
128	#[case(1)] // Single byte
129	#[case(8)] // Exactly one word
130	#[case(135)] // One byte before block boundary
131	#[case(136)] // Exactly one block
132	#[case(137)] // One byte over block boundary
133	#[case(272)] // Exactly two blocks
134	#[case(500)] // Arbitrary larger message
135	fn test_keccak256_fixed(#[case] message_len_bytes: usize) {
136		// Create test message with deterministic random bytes
137		let seed = message_len_bytes as u64;
138		let mut rng = StdRng::seed_from_u64(seed);
139		let mut message = vec![0u8; message_len_bytes];
140		rng.fill_bytes(&mut message);
141
142		// Compute expected digest using sha3 crate
143		let mut hasher = Keccak256::new();
144		hasher.update(&message);
145		let expected_digest: [u8; 32] = hasher.finalize().into();
146
147		// Build circuit
148		let builder = CircuitBuilder::new();
149
150		// Create message wires
151		let n_words = message_len_bytes.div_ceil(8);
152		let message_wires: Vec<_> = (0..n_words).map(|_| builder.add_witness()).collect();
153
154		// Create expected digest wires
155		let expected_digest_wires: [Wire; 4] = std::array::from_fn(|_| builder.add_witness());
156
157		// Compute digest using fixed-length function
158		let computed_digest = keccak256(&builder, &message_wires, message_len_bytes);
159
160		// Assert computed digest equals expected
161		for i in 0..4 {
162			builder.assert_eq(format!("digest[{i}]"), computed_digest[i], expected_digest_wires[i]);
163		}
164
165		// Build and verify circuit
166		let circuit = builder.build();
167		let cs = circuit.constraint_system();
168		let mut witness = circuit.new_witness_filler();
169
170		// Populate message witness
171		for (i, chunk) in message.chunks(8).enumerate() {
172			let mut word_bytes = [0u8; 8];
173			word_bytes[..chunk.len()].copy_from_slice(chunk);
174			let word = u64::from_le_bytes(word_bytes);
175			witness[message_wires[i]] = Word(word);
176		}
177
178		// Populate expected digest witness
179		for (i, chunk) in expected_digest.chunks(8).enumerate() {
180			let word = u64::from_le_bytes(chunk.try_into().unwrap());
181			witness[expected_digest_wires[i]] = Word(word);
182		}
183
184		circuit.populate_wire_witness(&mut witness).unwrap();
185		cs.verify(&witness.into_value_vec())
186			.expect("Circuit constraints should be satisfied");
187	}
188
189	#[test]
190	#[should_panic(expected = "message.len() (1) must equal len_bytes.div_ceil(8) (2)")]
191	fn test_keccak256_fixed_wrong_wire_count() {
192		let builder = CircuitBuilder::new();
193
194		// Create only 1 wire but claim message is 10 bytes (needs 2 wires)
195		let message_wires = vec![builder.add_witness()];
196
197		// This should panic
198		keccak256(&builder, &message_wires, 10);
199	}
200
201	#[test]
202	fn test_keccak256_fixed_exact_wire_count() {
203		let builder = CircuitBuilder::new();
204
205		// Empty message: 0 bytes requires 0 wires
206		let empty: Vec<Wire> = vec![];
207		let _ = keccak256(&builder, &empty, 0);
208
209		// 8 bytes requires exactly 1 wire
210		let one_wire = vec![builder.add_witness()];
211		let _ = keccak256(&builder, &one_wire, 8);
212
213		// 10 bytes requires exactly 2 wires
214		let two_wires = vec![builder.add_witness(), builder.add_witness()];
215		let _ = keccak256(&builder, &two_wires, 10);
216
217		// 17 bytes requires exactly 3 wires
218		let three_wires = vec![
219			builder.add_witness(),
220			builder.add_witness(),
221			builder.add_witness(),
222		];
223		let _ = keccak256(&builder, &three_wires, 17);
224	}
225}