Skip to main content

binius_circuits/keccak/
permutation.rs

1// Copyright 2025 Irreducible Inc.
2use std::{array, iter};
3
4use binius_core::word::Word;
5use binius_frontend::{ChipGadget, CircuitBuilder, Hint, Wire};
6
7// ι round constants
8pub const RC: [u64; 24] = [
9	0x0000_0000_0000_0001,
10	0x0000_0000_0000_8082,
11	0x8000_0000_0000_808A,
12	0x8000_0000_8000_8000,
13	0x0000_0000_0000_808B,
14	0x0000_0000_8000_0001,
15	0x8000_0000_8000_8081,
16	0x8000_0000_0000_8009,
17	0x0000_0000_0000_008A,
18	0x0000_0000_0000_0088,
19	0x0000_0000_8000_8009,
20	0x0000_0000_8000_000A,
21	0x0000_0000_8000_808B,
22	0x8000_0000_0000_008B,
23	0x8000_0000_0000_8089,
24	0x8000_0000_0000_8003,
25	0x8000_0000_0000_8002,
26	0x8000_0000_0000_0080,
27	0x0000_0000_0000_800A,
28	0x8000_0000_8000_000A,
29	0x8000_0000_8000_8081,
30	0x8000_0000_0000_8080,
31	0x0000_0000_8000_0001,
32	0x8000_0000_8000_8008,
33];
34
35// ρ rotation offsets r[x,y] in lane order (i = x + 5*y)
36#[rustfmt::skip]
37pub const R: [u32; 25] =  [
38	 0,  1, 62, 28, 27,
39	36, 44,  6, 55, 20,
40	 3, 10, 43, 25, 39,
41	41, 45, 15, 21,  8,
42	18,  2, 61, 56, 14,
43];
44
45#[inline(always)]
46pub const fn idx(x: usize, y: usize) -> usize {
47	x + 5 * y
48}
49
50/// Perform the Keccak f\[1600\] permutation in place on a 25-lane state.
51///
52/// # Arguments
53///
54/// * `b` - The circuit builder to use.
55/// * `state` - The 25-word state to permute.
56///
57/// # Chips
58///
59/// This is a [`ChipGadget`]. A circuit that calls
60/// [`register_chip`](CircuitBuilder::register_chip) with [`KeccakF1600`] before building turns
61/// every permutation under it into a chip call, including the ones
62/// [`keccak256_varlen`](super::keccak256_varlen) and
63/// [`fixed_length::keccak256`](super::fixed_length::keccak256) reach.
64pub fn keccak_f1600(b: &CircuitBuilder, state: &mut [Wire; 25]) {
65	let outputs = b.build_gadget(KeccakF1600, &[], state);
66	state.copy_from_slice(&outputs);
67}
68
69/// [`keccak_f1600`] as a gadget, so that a circuit can make it a chip.
70///
71/// Its interface is the 25 input lanes and the 25 output lanes, both in lane order `x + 5 * y`.
72/// Every lane is a full 64-bit word; there is no lane packing.
73pub struct KeccakF1600;
74
75impl Hint for KeccakF1600 {
76	const NAME: &'static str = "binius.keccak_f1600";
77
78	fn shape(&self, _dimensions: &[usize]) -> (usize, usize) {
79		(25, 25)
80	}
81
82	fn execute(&self, _dimensions: &[usize], inputs: &[Word], outputs: &mut [Word]) {
83		let mut state: [u64; 25] = array::from_fn(|i| inputs[i].as_u64());
84		ref_keccak_f1600(&mut state);
85
86		for (slot, lane) in iter::zip(outputs, state) {
87			*slot = Word(lane);
88		}
89	}
90}
91
92impl ChipGadget for KeccakF1600 {
93	fn build(&self, builder: &CircuitBuilder, _dimensions: &[usize], inputs: &[Wire]) -> Vec<Wire> {
94		let mut state: [Wire; 25] = array::from_fn(|i| inputs[i]);
95		f1600_gates(builder, &mut state);
96		state.to_vec()
97	}
98}
99
100/// [`keccak_f1600`] in gates, whatever the building circuit does with the gadget.
101fn f1600_gates(b: &CircuitBuilder, state: &mut [Wire; 25]) {
102	for round in 0..24 {
103		keccak_permutation_round(b, state, round);
104	}
105}
106
107pub fn keccak_permutation_round(b: &CircuitBuilder, state: &mut [Wire; 25], round: usize) {
108	theta(b, state);
109	rho_pi(b, state);
110	chi_iota(b, state, round);
111}
112
113fn theta(b: &CircuitBuilder, state: &mut [Wire; 25]) {
114	let c0 = b.bxor_multi(&array::from_fn::<_, 5, _>(|y| state[idx(0, y)]));
115	let c1 = b.bxor_multi(&array::from_fn::<_, 5, _>(|y| state[idx(1, y)]));
116	let c2 = b.bxor_multi(&array::from_fn::<_, 5, _>(|y| state[idx(2, y)]));
117	let c3 = b.bxor_multi(&array::from_fn::<_, 5, _>(|y| state[idx(3, y)]));
118	let c4 = b.bxor_multi(&array::from_fn::<_, 5, _>(|y| state[idx(4, y)]));
119
120	// D[x] = C[x-1] ^ rotl1(C[x+1])
121	let d0 = b.bxor(c4, b.rotl(c1, 1));
122	let d1 = b.bxor(c0, b.rotl(c2, 1));
123	let d2 = b.bxor(c1, b.rotl(c3, 1));
124	let d3 = b.bxor(c2, b.rotl(c4, 1));
125	let d4 = b.bxor(c3, b.rotl(c0, 1));
126
127	// A'[x,y] = A[x,y] ^ D[x]
128	for y in 0..5 {
129		state[idx(0, y)] = b.bxor(state[idx(0, y)], d0);
130		state[idx(1, y)] = b.bxor(state[idx(1, y)], d1);
131		state[idx(2, y)] = b.bxor(state[idx(2, y)], d2);
132		state[idx(3, y)] = b.bxor(state[idx(3, y)], d3);
133		state[idx(4, y)] = b.bxor(state[idx(4, y)], d4);
134	}
135}
136
137fn chi_iota(b: &CircuitBuilder, state: &mut [Wire; 25], round: usize) {
138	let rc = b.add_constant(Word(RC[round]));
139
140	for y in 0..5 {
141		let a0 = state[idx(0, y)];
142		let a1 = state[idx(1, y)];
143		let a2 = state[idx(2, y)];
144		let a3 = state[idx(3, y)];
145		let a4 = state[idx(4, y)];
146
147		// For the state element at (0, 0), the output state has the round constant accumulated in.
148		// For circuit efficiency, we do this as part of the fused AND-XOR.
149		let a0_iota = if y == 0 { b.bxor(a0, rc) } else { a0 };
150
151		state[idx(0, y)] = b.fax(b.bnot(a1), a2, a0_iota);
152		state[idx(1, y)] = b.fax(b.bnot(a2), a3, a1);
153		state[idx(2, y)] = b.fax(b.bnot(a3), a4, a2);
154		state[idx(3, y)] = b.fax(b.bnot(a4), a0, a3);
155		state[idx(4, y)] = b.fax(b.bnot(a0), a1, a4);
156	}
157}
158
159fn rho_pi(b: &CircuitBuilder, state: &mut [Wire; 25]) {
160	let mut temp = [state[0]; 25];
161	for y in 0..5 {
162		for x in 0..5 {
163			// no need to rotate if rotating by 0
164			if R[idx(x, y)] == 0 {
165				continue;
166			}
167			temp[idx(y, (2 * x + 3 * y) % 5)] = b.rotl(state[idx(x, y)], R[idx(x, y)]);
168		}
169	}
170	*state = temp;
171}
172
173// --- Pure-Rust reference implementation of Keccak-f[1600] ---------------------------
174//
175// Shared by [`KeccakF1600`] (prover-side witness generation) and the tests. One function per
176// circuit step, so that each step can be checked against its gates alone.
177
178fn ref_theta(state: &mut [u64; 25]) {
179	let mut c = [0u64; 5];
180	for x in 0..5 {
181		c[x] = state[idx(x, 0)]
182			^ state[idx(x, 1)]
183			^ state[idx(x, 2)]
184			^ state[idx(x, 3)]
185			^ state[idx(x, 4)];
186	}
187	let d = [
188		c[4] ^ c[1].rotate_left(1),
189		c[0] ^ c[2].rotate_left(1),
190		c[1] ^ c[3].rotate_left(1),
191		c[2] ^ c[4].rotate_left(1),
192		c[3] ^ c[0].rotate_left(1),
193	];
194
195	for y in 0..5 {
196		for x in 0..5 {
197			state[idx(x, y)] ^= d[x];
198		}
199	}
200}
201
202fn ref_rho_pi(state: &mut [u64; 25]) {
203	let mut temp = [state[0]; 25];
204	for y in 0..5 {
205		for x in 0..5 {
206			temp[idx(y, (2 * x + 3 * y) % 5)] = state[idx(x, y)].rotate_left(R[idx(x, y)]);
207		}
208	}
209	*state = temp;
210}
211
212fn ref_chi(state: &mut [u64; 25]) {
213	for y in 0..5 {
214		let a0 = state[idx(0, y)];
215		let a1 = state[idx(1, y)];
216		let a2 = state[idx(2, y)];
217		let a3 = state[idx(3, y)];
218		let a4 = state[idx(4, y)];
219		state[idx(0, y)] = a0 ^ ((!a1) & a2);
220		state[idx(1, y)] = a1 ^ ((!a2) & a3);
221		state[idx(2, y)] = a2 ^ ((!a3) & a4);
222		state[idx(3, y)] = a3 ^ ((!a4) & a0);
223		state[idx(4, y)] = a4 ^ ((!a0) & a1);
224	}
225}
226
227const fn ref_iota(state: &mut [u64; 25], round: usize) {
228	state[0] ^= RC[round];
229}
230
231fn ref_keccak_permutation_round(state: &mut [u64; 25], round: usize) {
232	ref_theta(state);
233	ref_rho_pi(state);
234	ref_chi(state);
235	ref_iota(state, round);
236}
237
238/// Pure-Rust Keccak-f\[1600\] permutation of a 25-lane state, matching the in-circuit
239/// permutation exactly.
240///
241/// Used for prover-side witness generation and as the test reference.
242pub fn ref_keccak_f1600(state: &mut [u64; 25]) {
243	for round in 0..24 {
244		ref_keccak_permutation_round(state, round);
245	}
246}
247
248#[cfg(test)]
249mod tests {
250	use binius_core::word::Word;
251	use binius_frontend::CircuitBuilder;
252	use proptest::prelude::*;
253	use rand::prelude::*;
254
255	use super::*;
256
257	fn validate_circuit_component(
258		circuit_fn: impl FnOnce(&CircuitBuilder, &mut [Wire; 25]),
259		reference_fn: impl FnOnce(&mut [u64; 25]),
260		input_state: [u64; 25],
261	) {
262		let builder = CircuitBuilder::new();
263
264		let input_wires: [Wire; 25] = std::array::from_fn(|_| builder.add_inout());
265
266		let mut state_wires = input_wires;
267		circuit_fn(&builder, &mut state_wires);
268		// The test reads these directly, so pin them or pooling could reclaim their slots first.
269		for &wire in &state_wires {
270			builder.force_commit(wire);
271		}
272		let circuit = builder.build();
273
274		let mut expected_output = input_state;
275		reference_fn(&mut expected_output);
276
277		let mut w = circuit.new_witness_filler();
278		for i in 0..25 {
279			w[input_wires[i]] = Word(input_state[i]);
280		}
281		circuit.populate_wire_witness(&mut w).unwrap();
282
283		for i in 0..25 {
284			assert_eq!(
285				w[state_wires[i]],
286				Word(expected_output[i]),
287				"Output mismatch at index {}: circuit={:?}, expected={:?}",
288				i,
289				w[state_wires[i]],
290				Word(expected_output[i])
291			);
292		}
293
294		let cs = circuit.constraint_system();
295		cs.verify(&w.into_value_vec()).unwrap();
296	}
297
298	#[test]
299	fn test_keccak_f1600() {
300		let mut rng = StdRng::seed_from_u64(0);
301		let input_state = rng.random::<[u64; 25]>();
302
303		validate_circuit_component(keccak_f1600, ref_keccak_f1600, input_state);
304	}
305
306	#[test]
307	fn test_keccak_permutation_round() {
308		let mut rng = StdRng::seed_from_u64(0);
309		let input_state = rng.random::<[u64; 25]>();
310
311		validate_circuit_component(
312			|b, state| keccak_permutation_round(b, state, 0),
313			|state| ref_keccak_permutation_round(state, 0),
314			input_state,
315		);
316	}
317
318	#[test]
319	fn test_theta() {
320		let mut rng = StdRng::seed_from_u64(0);
321		let input_state = rng.random::<[u64; 25]>();
322
323		validate_circuit_component(theta, ref_theta, input_state);
324	}
325
326	#[test]
327	fn test_rho_pi() {
328		let mut rng = StdRng::seed_from_u64(0);
329		let input_state = rng.random::<[u64; 25]>();
330
331		validate_circuit_component(rho_pi, ref_rho_pi, input_state);
332	}
333
334	#[test]
335	fn test_chi_iota() {
336		let mut rng = StdRng::seed_from_u64(0);
337		let input_state = rng.random::<[u64; 25]>();
338
339		// The circuit fuses χ and ι into one pass, so it is checked against the two reference
340		// steps run back to back. Round 6 has a round constant with bits spread across all four
341		// 16-bit halves, so a misplaced constant shows up in the output.
342		const ROUND: usize = 6;
343
344		validate_circuit_component(
345			|b, state| chi_iota(b, state, ROUND),
346			|state| {
347				ref_chi(state);
348				ref_iota(state, ROUND);
349			},
350			input_state,
351		);
352	}
353
354	/// Runs [`keccak_f1600`] in gates over its flat lane interface.
355	fn run_f1600_words(inputs: [u64; 25]) -> [u64; 25] {
356		let builder = CircuitBuilder::new();
357		let wires: [Wire; 25] = std::array::from_fn(|_| builder.add_witness());
358		let mut out = wires;
359		f1600_gates(&builder, &mut out);
360		for wire in out {
361			builder.mark_inout(wire);
362		}
363
364		let circuit = builder.build();
365		let mut w = circuit.new_witness_filler();
366		for (wire, word) in std::iter::zip(wires, inputs) {
367			w[wire] = Word(word);
368		}
369		circuit.populate_wire_witness(&mut w).unwrap();
370
371		std::array::from_fn(|i| w[out[i]].as_u64())
372	}
373
374	proptest! {
375		// The chip path takes the outputs from `KeccakF1600::execute` and the chip recomputes them
376		// from its gates, so the two have to agree on every word a circuit can reach them with.
377		// Every lane is a full 64-bit value, so random words cover the whole interface.
378		#[test]
379		fn f1600_hint_matches_its_gates(words in prop::collection::vec(any::<u64>(), 25)) {
380			let inputs: [u64; 25] = std::array::from_fn(|i| words[i]);
381
382			let mut hinted = [Word::ZERO; 25];
383			KeccakF1600.execute(&[], &inputs.map(Word), &mut hinted);
384
385			prop_assert_eq!(hinted.map(|word| word.as_u64()), run_f1600_words(inputs));
386		}
387	}
388}