Skip to main content

binius_examples/circuits/
keccak.rs

1// Copyright 2026 The Binius Developers
2// Copyright 2025 Irreducible Inc.
3use std::array;
4
5use anyhow::Result;
6use binius_circuits::{
7	fixed_byte_vec::ByteVec,
8	keccak::{N_WORDS_PER_DIGEST, fixed_length::keccak256, keccak256_varlen},
9};
10use binius_core::word::Word;
11use binius_frontend::{CircuitBuilder, Wire, WitnessFiller};
12use sha3::Digest;
13
14use super::utils::{self, HasherInstance, HasherMode, HasherParams};
15use crate::ExampleCircuit;
16
17/// Keccak-256 hash circuit example
18pub struct KeccakExample {
19	circuit: KeccakCircuit,
20	mode: HasherMode,
21}
22
23/// Either the fixed-length (default) or variable-length Keccak-256 circuit.
24enum KeccakCircuit {
25	/// Fixed-length gadget: message length is a compile-time constant.
26	Fixed {
27		message: Vec<Wire>,
28		digest: [Wire; N_WORDS_PER_DIGEST],
29	},
30	/// Variable-length gadget: message length is a runtime witness.
31	Variable {
32		message: ByteVec,
33		digest: [Wire; N_WORDS_PER_DIGEST],
34	},
35}
36
37impl ExampleCircuit for KeccakExample {
38	type Params = HasherParams;
39	type Instance = HasherInstance;
40
41	fn build(params: HasherParams, builder: &mut CircuitBuilder) -> Result<Self> {
42		let mode = utils::resolve_hasher_mode(&params, "Keccak-256", true)?;
43
44		let circuit = match mode {
45			// Fixed-length (default): message length is a compile-time constant.
46			HasherMode::Fixed { len_bytes } => {
47				let n_words = len_bytes.div_ceil(8);
48				let message: Vec<Wire> = (0..n_words).map(|_| builder.add_inout()).collect();
49				let computed_digest = keccak256(builder, &message, len_bytes);
50				let digest: [Wire; N_WORDS_PER_DIGEST] = array::from_fn(|_| builder.add_inout());
51				for i in 0..N_WORDS_PER_DIGEST {
52					builder.assert_eq(format!("digest[{i}]"), computed_digest[i], digest[i]);
53				}
54				KeccakCircuit::Fixed { message, digest }
55			}
56			// Variable-length: message length is a runtime witness.
57			HasherMode::Variable { max_len_bytes } => {
58				let n_words = max_len_bytes.div_ceil(8);
59				let len_bytes = builder.add_witness();
60				let data = (0..n_words).map(|_| builder.add_inout()).collect();
61				let message = ByteVec::new(data, len_bytes);
62				let digest: [Wire; N_WORDS_PER_DIGEST] = array::from_fn(|_| builder.add_inout());
63				let computed_digest = keccak256_varlen(builder, &message);
64				for i in 0..N_WORDS_PER_DIGEST {
65					builder.assert_eq(format!("digest[{i}]"), computed_digest[i], digest[i]);
66				}
67				KeccakCircuit::Variable { message, digest }
68			}
69		};
70
71		Ok(Self { circuit, mode })
72	}
73
74	fn populate_witness(&self, instance: HasherInstance, w: &mut WitnessFiller<'_>) -> Result<()> {
75		let message = utils::resolve_hasher_message(&self.mode, &instance)?;
76		let mut hasher = sha3::Keccak256::new();
77		hasher.update(&message);
78		let digest: [u8; 32] = hasher.finalize().into();
79
80		match &self.circuit {
81			KeccakCircuit::Fixed {
82				message: message_wires,
83				digest: digest_wires,
84			} => {
85				// Message: 64-bit little-endian words, 8 bytes per wire.
86				for (wire, word) in message_wires
87					.iter()
88					.zip(utils::pack_bytes_u64words(&message, false))
89				{
90					w[*wire] = word;
91				}
92				// Digest: 4 x 64-bit little-endian words.
93				for (i, chunk) in digest.chunks(8).enumerate() {
94					w[digest_wires[i]] = Word(u64::from_le_bytes(chunk.try_into().unwrap()));
95				}
96			}
97			KeccakCircuit::Variable {
98				message: byte_vec,
99				digest: digest_wires,
100			} => {
101				byte_vec.populate_data(w, &message);
102				byte_vec.populate_len_bytes(w, message.len());
103				// Digest: 4 x 64-bit little-endian words.
104				for (i, chunk) in digest.chunks(8).enumerate() {
105					w[digest_wires[i]] = Word(u64::from_le_bytes(chunk.try_into().unwrap()));
106				}
107			}
108		}
109
110		Ok(())
111	}
112
113	fn param_summary(params: &Self::Params) -> Option<String> {
114		utils::hasher_param_summary(params)
115	}
116}