Skip to main content

miden_core_lib/handlers/
keccak256.rs

1//! Keccak256 precompile for the Miden VM.
2//!
3//! This module provides both execution-time and verification-time support for Keccak256 hashing.
4//!
5//! ## Architecture
6//!
7//! ### Event Handler (Execution-Time)
8//! When the VM emits a Keccak event requesting non-deterministic hash results, the processor calls
9//! [`KeccakPrecompile`] which reads input data from memory, computes the hash, provides the digest
10//! via the advice stack, and logs the raw preimage bytes as a precompile request.
11//!
12//! ### Precompile Verifier (Verification-Time)
13//! During verification, the [`PrecompileVerifier`] receives the stored preimage bytes, recomputes
14//! the hash, and generates a commitment `P2(P2(input) || P2(digest))`, where P2 stands
15//! for Poseidon2, that validates the computation was performed correctly.
16//!
17//! ### Commitment Tag Format
18//! Each request is tagged as `[event_id, len_bytes, 0, 0]`. The `len_bytes` field prevents
19//! collisions: since bytes are packed into 32-bit limbs, we must distinguish actual data bytes
20//! from padding in the final limb.
21//!
22//! ## Digest Representation
23//! A Keccak256 digest (256 bits) is represented as 8 field elements `[h0, ..., h7]`,
24//! each containing a u32 value where `hi = u32::from_le_bytes([b_{4i}, ..., b_{4i+3}])`.
25
26use alloc::{format, vec, vec::Vec};
27use core::array;
28
29use miden_core::{
30    Felt, Word, ZERO,
31    crypto::hash::{Keccak256, Poseidon2},
32    events::EventName,
33    precompile::{PrecompileCommitment, PrecompileError, PrecompileRequest, PrecompileVerifier},
34    utils::bytes_to_packed_u32_elements,
35};
36use miden_processor::{
37    ProcessorState,
38    advice::AdviceMutation,
39    event::{EventError, EventHandler},
40};
41
42use crate::handlers::{BYTES_PER_U32, read_memory_packed_u32};
43
44/// Event name for the keccak256 hash_bytes operation.
45pub const KECCAK_HASH_BYTES_EVENT_NAME: EventName =
46    EventName::new("miden::core::hash::keccak256::hash_bytes");
47
48pub struct KeccakPrecompile;
49
50impl EventHandler for KeccakPrecompile {
51    /// Keccak256 event handler called by the processor when the VM emits a hash request event.
52    ///
53    /// Reads packed input data from memory, computes the Keccak256 hash, provides the digest via
54    /// the advice stack, and stores the raw preimage for verification (see [`PrecompileVerifier`]).
55    ///
56    /// ## Input Format
57    /// - **Stack**: `[event_id, ptr, len_bytes, ...]` where `ptr` is word-aligned (divisible by 4)
58    /// - **Memory**: Input bytes packed as u32 field elements (4 bytes per element, little-endian)
59    ///   from `ptr` to `ptr+ceil(len_bytes/4)`, with unused bytes in the final u32 set to zero
60    ///
61    /// ## Output Format
62    /// - **Advice Stack**: Extended with digest `[h_0, ..., h_7]` (least significant u32 on top)
63    /// - **Precompile Request**: Stores tag `[event_id, len_bytes, 0, 0]` and raw preimage bytes
64    ///   for verification time
65    fn on_event(&self, process: &ProcessorState) -> Result<Vec<AdviceMutation>, EventError> {
66        // Stack: [event_id, ptr, len_bytes, ...]
67        let ptr = process.get_stack_item(1).as_canonical_u64();
68        let len_bytes = process.get_stack_item(2).as_canonical_u64();
69
70        // Enforce the maximum precompile input size to prevent host-side resource exhaustion. Make
71        // sure to compare in u64 to avoid truncation on 32-bit targets.
72        let max = process.execution_options().max_hash_len_bytes();
73        if len_bytes > max as u64 {
74            return Err(format!(
75                "keccak256 input length {len_bytes} bytes exceeds maximum of {max} bytes"
76            )
77            .into());
78        }
79        let len_bytes = usize::try_from(len_bytes).map_err(|_| {
80            EventError::from(format!(
81                "keccak256 input length {len_bytes} exceeds addressable range"
82            ))
83        })?;
84
85        // Read input bytes from memory using the shared helper (u32-packed, LE, zero-padded)
86        let input_bytes = read_memory_packed_u32(process, ptr, len_bytes)?;
87
88        // Build preimage from bytes and compute digest
89        let preimage = KeccakPreimage::new(input_bytes);
90        let digest = preimage.digest();
91
92        // Extend the stack with the digest [h_0, ..., h_7] for consumption via adv_pipe
93        let advice_stack_extension = AdviceMutation::extend_stack(digest.0);
94
95        // Store the precompile data for deferred verification.
96        let precompile_request_extension =
97            AdviceMutation::extend_precompile_requests([preimage.into()]);
98
99        Ok(vec![advice_stack_extension, precompile_request_extension])
100    }
101}
102
103// KECCAK VERIFIER
104// ================================================================================================
105
106impl PrecompileVerifier for KeccakPrecompile {
107    /// Verifier for Keccak256 precompile computations at verification time.
108    ///
109    /// Receives the raw preimage bytes stored during execution (see [`EventHandler::on_event`]),
110    /// recomputes the Keccak256 hash, and generates a commitment `P2(P2(input) || P2(digest))`
111    /// with tag `[event_id, len_bytes, 0, 0]` that validates against the execution trace.
112    fn verify(&self, calldata: &[u8]) -> Result<PrecompileCommitment, PrecompileError> {
113        let preimage = KeccakPreimage::new(calldata.to_vec());
114        Ok(preimage.precompile_commitment())
115    }
116}
117
118// KECCAK DIGEST
119// ================================================================================================
120
121/// Keccak256 digest representation in the Miden VM.
122///
123/// Represents a 256-bit Keccak digest as 8 field elements, each containing a u32 value
124/// packed in little-endian order: `[d_0, ..., d_7]` where
125/// `d_0 = u32::from_le_bytes([b_0, b_1, b_2, b_3])` and so on.
126#[derive(Debug, Copy, Clone, Eq, PartialEq)]
127pub struct KeccakFeltDigest([Felt; 8]);
128
129impl KeccakFeltDigest {
130    /// Creates a digest from a 32-byte Keccak256 hash output.
131    pub fn from_bytes(bytes: &[u8]) -> Self {
132        assert_eq!(bytes.len(), 32, "input must be 32 bytes");
133        let packed: [u32; 8] = array::from_fn(|i| {
134            let limbs = array::from_fn(|j| bytes[BYTES_PER_U32 * i + j]);
135            u32::from_le_bytes(limbs)
136        });
137        Self(packed.map(Felt::from_u32))
138    }
139
140    /// Creates a commitment of the digest using Poseidon2 over `[d_0, ..., d_7]`.
141    pub fn to_commitment(&self) -> Word {
142        Poseidon2::hash_elements(&self.0)
143    }
144}
145
146// KECCAK PREIMAGE
147// ================================================================================================
148
149/// Keccak256 preimage structure representing the raw input data to be hashed.
150///
151/// This structure encapsulates the raw bytes that will be passed to the Keccak256
152/// hash function, providing utilities for:
153/// - Converting between bytes and field element representations
154/// - Computing the Keccak256 digest
155/// - Generating precompile commitments for verification
156/// - Handling the data packing format used by the VM
157#[derive(Debug, Clone, PartialEq, Eq)]
158pub struct KeccakPreimage(Vec<u8>);
159
160impl KeccakPreimage {
161    /// Creates a new `KeccakPreimage` from a vector of bytes.
162    pub fn new(bytes: Vec<u8>) -> Self {
163        Self(bytes)
164    }
165
166    /// Consumes the preimage and returns the inner byte vector.
167    pub fn into_inner(self) -> Vec<u8> {
168        self.0
169    }
170
171    /// Converts the preimage bytes to field elements using u32 packing.
172    ///
173    /// Each field element contains a u32 value representing 4 bytes in little-endian format.
174    /// The last chunk is padded with zeros if the byte length is not a multiple of 4.
175    ///
176    /// Produces the same u32‑packed format expected by the MASM wrappers.
177    pub fn as_felts(&self) -> Vec<Felt> {
178        bytes_to_packed_u32_elements(self.as_ref())
179    }
180
181    /// Computes the Poseidon2 hash of the input data in field element format.
182    ///
183    /// This creates a cryptographic commitment to the input data that can be
184    /// used for verification purposes. The input is first converted to field
185    /// elements using the same packing format as the VM.
186    pub fn input_commitment(&self) -> Word {
187        Poseidon2::hash_elements(&self.as_felts())
188    }
189
190    /// Computes the Keccak256 hash of the preimage bytes.
191    ///
192    /// Returns the digest formatted as 8 field elements, each containing a u32 value
193    /// in little-endian byte order. This matches the format expected by the VM
194    /// and can be directly used on the operand stack.
195    pub fn digest(&self) -> KeccakFeltDigest {
196        let hash_u8 = Keccak256::hash(self.as_ref());
197        KeccakFeltDigest::from_bytes(&hash_u8)
198    }
199
200    /// Computes the precompile commitment: `P2(P2(input) || P2(keccak_hash))` with tag
201    /// `[event_id, len_bytes, 0, 0]`.
202    ///
203    /// Generated by the [`PrecompileVerifier`] at verification time and validated against
204    /// commitments tracked during execution by the [`EventHandler`]. The double hash binds
205    /// input and output together, preventing tampering.
206    pub fn precompile_commitment(&self) -> PrecompileCommitment {
207        let tag = self.precompile_tag();
208        let comm = Poseidon2::merge(&[self.input_commitment(), self.digest().to_commitment()]);
209        PrecompileCommitment::new(tag, comm)
210    }
211
212    /// Returns the tag used to identify the commitment to the precompile. defined as
213    /// `[event_id, preimage_u8.len(), 0, 0]` where event_id is computed from the event name.
214    fn precompile_tag(&self) -> Word {
215        [
216            KECCAK_HASH_BYTES_EVENT_NAME.to_event_id().as_felt(),
217            Felt::new(self.as_ref().len() as u64),
218            ZERO,
219            ZERO,
220        ]
221        .into()
222    }
223}
224
225impl From<KeccakPreimage> for PrecompileRequest {
226    fn from(preimage: KeccakPreimage) -> Self {
227        let event_id = KECCAK_HASH_BYTES_EVENT_NAME.to_event_id();
228        PrecompileRequest::new(event_id, preimage.into_inner())
229    }
230}
231
232impl AsRef<[u8]> for KeccakPreimage {
233    fn as_ref(&self) -> &[u8] {
234        &self.0
235    }
236}
237
238impl From<Vec<u8>> for KeccakPreimage {
239    fn from(bytes: Vec<u8>) -> Self {
240        Self::new(bytes)
241    }
242}
243
244impl AsRef<[Felt]> for KeccakFeltDigest {
245    fn as_ref(&self) -> &[Felt] {
246        &self.0
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253
254    // KECCAK FELT DIGEST TESTS
255    // ============================================================================================
256
257    #[test]
258    fn test_keccak_felt_digest_from_bytes() {
259        // Test with a known 32-byte sequence
260        let bytes: Vec<u8> = (1..=32).collect();
261        let digest = KeccakFeltDigest::from_bytes(&bytes);
262
263        // Verify each u32 is packed correctly in little-endian order
264        // Each u32 is constructed from 4 consecutive bytes: byte[i] + byte[i+1]<<8 + byte[i+2]<<16
265        // + byte[i+3]<<24
266        let expected = [
267            u32::from_le_bytes([1, 2, 3, 4]),
268            u32::from_le_bytes([5, 6, 7, 8]),
269            u32::from_le_bytes([9, 10, 11, 12]),
270            u32::from_le_bytes([13, 14, 15, 16]),
271            u32::from_le_bytes([17, 18, 19, 20]),
272            u32::from_le_bytes([21, 22, 23, 24]),
273            u32::from_le_bytes([25, 26, 27, 28]),
274            u32::from_le_bytes([29, 30, 31, 32]),
275        ]
276        .map(Felt::from_u32);
277
278        assert_eq!(digest.0, expected);
279    }
280
281    // KECCAK PREIMAGE TESTS
282    // ============================================================================================
283
284    #[test]
285    fn test_keccak_preimage_packing_cases() {
286        // Table of inputs and expected u32-packed felts (little-endian)
287        let cases: &[(&[u8], &[u32])] = &[
288            (&[], &[]),
289            (&[0x42], &[0x0000_0042]),
290            (&[1, 2, 3, 4], &[0x0403_0201]),
291            (&[1, 2, 3, 4, 5], &[0x0403_0201, 0x0000_0005]),
292        ];
293
294        for (input, expected_u32) in cases {
295            let preimage = KeccakPreimage::new((*input).to_vec());
296            let felts = preimage.as_felts();
297            assert_eq!(felts.len(), expected_u32.len());
298            for (felt, &u) in felts.iter().zip((*expected_u32).iter()) {
299                assert_eq!(*felt, Felt::from_u32(u));
300            }
301
302            if input.is_empty() {
303                assert_eq!(preimage.input_commitment(), Word::empty());
304            }
305        }
306
307        // 32-byte boundary sanity check
308        let input: Vec<u8> = (1..=32).collect();
309        let preimage = KeccakPreimage::new(input);
310        let felts = preimage.as_felts();
311        assert_eq!(felts.len(), 8);
312        assert_eq!(felts[0], Felt::from_u32(u32::from_le_bytes([1, 2, 3, 4])));
313        assert_eq!(felts[7], Felt::from_u32(u32::from_le_bytes([29, 30, 31, 32])));
314    }
315
316    #[test]
317    fn test_keccak_preimage_digest_consistency() {
318        // Test that digest computation is consistent with direct Keccak256
319        let input = b"hello world";
320        let preimage = KeccakPreimage::new(input.to_vec());
321
322        // Compute digest using preimage
323        let preimage_digest = preimage.digest();
324
325        // Compute digest directly using Keccak256
326        let direct_hash = Keccak256::hash(input);
327        let direct_digest = KeccakFeltDigest::from_bytes(&direct_hash);
328
329        assert_eq!(preimage_digest, direct_digest);
330    }
331
332    #[test]
333    fn test_keccak_preimage_commitments() {
334        let input = b"test input for commitments";
335        let preimage = KeccakPreimage::new(input.to_vec());
336
337        // Test input commitment
338        let felts = preimage.as_felts();
339        let expected_input_commitment = Poseidon2::hash_elements(&felts);
340        assert_eq!(preimage.input_commitment(), expected_input_commitment);
341
342        // Test digest commitment
343        let digest = preimage.digest();
344        let expected_digest_commitment = Poseidon2::hash_elements(digest.as_ref());
345        assert_eq!(digest.to_commitment(), expected_digest_commitment);
346
347        // Test precompile commitment (double hash)
348        let expected_precompile_commitment = PrecompileCommitment::new(
349            preimage.precompile_tag(),
350            Poseidon2::merge(&[preimage.input_commitment(), digest.to_commitment()]),
351        );
352
353        assert_eq!(preimage.precompile_commitment(), expected_precompile_commitment);
354    }
355
356    #[test]
357    fn test_keccak_verifier() {
358        let input = b"test verifier input";
359        let preimage = KeccakPreimage::new(input.to_vec());
360        let expected_commitment = preimage.precompile_commitment();
361
362        let commitment = KeccakPrecompile.verify(input).unwrap();
363        assert_eq!(commitment, expected_commitment);
364    }
365}