miden_core/precompile.rs
1//! Precompile framework for deferred verification in the Miden VM.
2//!
3//! This module provides the infrastructure for executing computationally expensive operations
4//! (precompiles) during VM execution while deferring their verification until proof generation.
5//!
6//! # Overview
7//!
8//! Precompiles enable the Miden VM to efficiently handle operations like cryptographic hashing
9//! (e.g., Keccak256) that would be prohibitively expensive to prove directly in the VM. Instead
10//! of proving every step of these computations, the VM uses a deferred verification approach.
11//!
12//! # Workflow
13//!
14//! The precompile system follows a four-stage lifecycle:
15//!
16//! 1. **VM Execution**: When a program calls a precompile (via an event handler), the VM:
17//! - Computes the result non-deterministically using the host
18//! - Creates a [`PrecompileCommitment`] binding inputs and outputs together
19//! - Stores a [`PrecompileRequest`] containing the raw input data for later verification
20//! - Records the commitment into a [`PrecompileTranscript`]
21//!
22//! 2. **Request Storage**: All precompile requests are collected and included in the execution
23//! proof.
24//!
25//! 3. **Proof Generation**: The prover generates a STARK proof of the VM execution. The final
26//! [`PrecompileTranscript`] state (sponge capacity) is a public input. The verifier enforces the
27//! initial (empty) and final state via variable‑length public inputs.
28//!
29//! 4. **Verification**: The verifier:
30//! - Recomputes each precompile commitment using the stored requests via [`PrecompileVerifier`]
31//! - Reconstructs the [`PrecompileTranscript`] by recording all commitments in order
32//! - Verifies the STARK proof with the final transcript state as public input.
33//! - Accepts the proof only if precompile verification succeeds and the STARK proof is valid
34//!
35//! # Key Types
36//!
37//! - [`PrecompileRequest`]: Stores the event ID and raw input bytes for a precompile call
38//! - [`PrecompileCommitment`]: A cryptographic commitment to both inputs and outputs, consisting of
39//! a tag (with event ID and metadata) and a commitment to the request's calldata.
40//! - [`PrecompileVerifier`]: Trait for implementing verification logic for specific precompiles
41//! - [`PrecompileVerifierRegistry`]: Registry mapping event IDs to their verifier implementations
42//! - [`PrecompileTranscript`]: A transcript (implemented via a Poseidon2 sponge) that creates a
43//! sequential commitment to all precompile requests.
44//!
45//! # Example Implementation
46//!
47//! See the Keccak256 precompile in `miden_core_lib::handlers::keccak256` for a complete reference
48//! implementation demonstrating both execution-time event handling and verification-time
49//! commitment recomputation.
50//!
51//! # Security Considerations
52//!
53//! **⚠️ Alpha Status**: This framework is under active development and subject to change. The
54//! security model assumes a fixed set of precompiles supported by the network. User-defined
55//! precompiles cannot be verified in the current architecture.
56
57use alloc::{boxed::Box, collections::BTreeMap, sync::Arc, vec::Vec};
58use core::error::Error;
59
60use miden_crypto::{Felt, Word, ZERO, hash::poseidon2::Poseidon2};
61#[cfg(feature = "serde")]
62use serde::{Deserialize, Serialize};
63
64use crate::{
65 events::{EventId, EventName},
66 serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable},
67};
68
69// PRECOMPILE REQUEST
70// ================================================================================================
71
72/// Represents a single precompile request consisting of an event ID and byte data.
73///
74/// This structure encapsulates the call data for a precompile operation, storing
75/// the raw bytes that will be processed by the precompile verifier when recomputing the
76/// corresponding commitment.
77///
78/// The `EventId` corresponds to the one used by the `EventHandler` that invoked the precompile
79/// during VM execution. The verifier uses this ID to select the appropriate `PrecompileVerifier`
80/// to validate the `calldata`.
81#[derive(Debug, Clone, PartialEq, Eq)]
82#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
83#[cfg_attr(
84 all(feature = "arbitrary", test),
85 miden_test_serde_macros::serde_test(binary_serde(true))
86)]
87pub struct PrecompileRequest {
88 /// Event ID identifying the type of precompile operation
89 event_id: EventId,
90 /// Raw byte data representing the input of the precompile computation
91 calldata: Vec<u8>,
92}
93
94impl PrecompileRequest {
95 pub fn new(event_id: EventId, calldata: Vec<u8>) -> Self {
96 Self { event_id, calldata }
97 }
98
99 pub fn calldata(&self) -> &[u8] {
100 &self.calldata
101 }
102
103 pub fn event_id(&self) -> EventId {
104 self.event_id
105 }
106}
107
108impl Serializable for PrecompileRequest {
109 fn write_into<W: ByteWriter>(&self, target: &mut W) {
110 self.event_id.write_into(target);
111 self.calldata.write_into(target);
112 }
113}
114
115impl Deserializable for PrecompileRequest {
116 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
117 let event_id = EventId::read_from(source)?;
118 let calldata = Vec::<u8>::read_from(source)?;
119 Ok(Self { event_id, calldata })
120 }
121
122 fn min_serialized_size() -> usize {
123 EventId::min_serialized_size() + Vec::<u8>::min_serialized_size()
124 }
125}
126
127// PRECOMPILE TRANSCRIPT TYPES
128// ================================================================================================
129
130/// Type alias representing the precompile transcript state (sponge capacity word).
131///
132/// This is simply a [`Word`] used to track the evolving state of the precompile transcript sponge.
133pub type PrecompileTranscriptState = Word;
134
135/// Type alias representing the finalized transcript digest.
136///
137/// This is simply a [`Word`] representing the final digest of all precompile commitments.
138pub type PrecompileTranscriptDigest = Word;
139
140// PRECOMPILE COMMITMENT
141// ================================================================================================
142
143/// A commitment to the evaluation of [`PrecompileRequest`], representing both the input and result
144/// of the request.
145///
146/// This structure contains both the tag (which includes metadata like event ID)
147/// and the commitment to the input and result (calldata) of the precompile request.
148///
149/// # Tag Structure
150///
151/// The tag is a 4-element word `[event_id, meta1, meta2, meta3]` where:
152///
153/// - **First element**: The [`EventId`] from the corresponding `EventHandler`
154/// - **Remaining 3 elements**: Available for precompile-specific metadata (e.g., `len_bytes` for
155/// hash functions to distinguish actual data from padding)
156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub struct PrecompileCommitment {
158 tag: Word,
159 comm: Word,
160}
161
162impl PrecompileCommitment {
163 /// Creates a new precompile commitment from a `TAG` and `COMM`.
164 ///
165 /// - `TAG`: 4-element word where the first element encodes the [`EventId`]; the remaining
166 /// elements are available as precompile-specific metadata (e.g., `len_bytes`).
167 /// - `COMM`: 4-element word containing the commitment to the calldata (or handler-specific
168 /// witness) for this precompile request.
169 pub fn new(tag: Word, comm: Word) -> Self {
170 Self { tag, comm }
171 }
172
173 /// Returns the `TAG` word which encodes the [`EventId`] in the first element and optional
174 /// precompile-specific metadata in the remaining three elements.
175 pub fn tag(&self) -> Word {
176 self.tag
177 }
178
179 /// Returns the `COMM` word (calldata commitment), i.e., the commitment to the precompile's
180 /// calldata (or other handler-specific witness).
181 pub fn comm_calldata(&self) -> Word {
182 self.comm
183 }
184
185 /// Returns the concatenation of `TAG` and `COMM` as field elements.
186 pub fn to_elements(&self) -> [Felt; 8] {
187 let words = [self.tag, self.comm];
188 Word::words_as_elements(&words).try_into().unwrap()
189 }
190
191 /// Returns the `EventId` used to identify the verifier that produced this commitment from a
192 /// `PrecompileRequest`.
193 pub fn event_id(&self) -> EventId {
194 EventId::from_felt(self.tag[0])
195 }
196}
197
198// PRECOMPILE VERIFIERS REGISTRY
199// ================================================================================================
200
201/// Registry of precompile verifiers.
202///
203/// This struct maintains a map of event IDs to their corresponding event names and verifiers.
204/// It is used to verify precompile requests during proof verification.
205#[derive(Default, Clone)]
206pub struct PrecompileVerifierRegistry {
207 /// Map of event IDs to their corresponding event names and verifiers
208 verifiers: BTreeMap<EventId, (EventName, Arc<dyn PrecompileVerifier>)>,
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214 use crate::{
215 events::EventId,
216 serde::{BudgetedReader, ByteWriter, DeserializationError, SliceReader},
217 };
218
219 #[test]
220 fn precompile_request_rejects_over_budget_calldata_len() {
221 let mut bytes = Vec::new();
222 EventId::from_u64(0).write_into(&mut bytes);
223 bytes.write_usize(2);
224
225 let budget = bytes.len() + 1;
226 let mut reader = BudgetedReader::new(SliceReader::new(&bytes), budget);
227 let err = PrecompileRequest::read_from(&mut reader).unwrap_err();
228 let DeserializationError::InvalidValue(message) = err else {
229 panic!("expected InvalidValue error");
230 };
231 assert!(message.contains("requested 2 elements"));
232 }
233}
234
235impl PrecompileVerifierRegistry {
236 /// Creates a new empty precompile verifiers registry.
237 pub fn new() -> Self {
238 Self { verifiers: BTreeMap::new() }
239 }
240
241 /// Returns a new registry that includes the supplied verifier in addition to existing ones.
242 pub fn with_verifier(
243 mut self,
244 event_name: &EventName,
245 verifier: Arc<dyn PrecompileVerifier>,
246 ) -> Self {
247 let event_id = event_name.to_event_id();
248 self.verifiers.insert(event_id, (event_name.clone(), verifier));
249 self
250 }
251
252 /// Merges another registry into this one, overwriting any conflicting event IDs with the other
253 /// registry's verifiers.
254 pub fn merge(&mut self, other: &Self) {
255 for (event_id, (event_name, verifier)) in other.verifiers.iter() {
256 self.verifiers.insert(*event_id, (event_name.clone(), verifier.clone()));
257 }
258 }
259
260 /// Verifies all precompile requests and returns the resulting precompile transcript state after
261 /// recording all commitments.
262 ///
263 /// # Errors
264 /// Returns a [`PrecompileVerificationError`] if:
265 /// - No verifier is registered for a request's event ID
266 /// - A verifier fails to verify its request
267 pub fn requests_transcript(
268 &self,
269 requests: &[PrecompileRequest],
270 ) -> Result<PrecompileTranscript, PrecompileVerificationError> {
271 let mut transcript = PrecompileTranscript::new();
272 for (index, PrecompileRequest { event_id, calldata }) in requests.iter().enumerate() {
273 let (event_name, verifier) = self.verifiers.get(event_id).ok_or(
274 PrecompileVerificationError::VerifierNotFound { index, event_id: *event_id },
275 )?;
276
277 let precompile_commitment = verifier.verify(calldata).map_err(|error| {
278 PrecompileVerificationError::PrecompileError {
279 index,
280 event_name: event_name.clone(),
281 error,
282 }
283 })?;
284 transcript.record(precompile_commitment);
285 }
286 Ok(transcript)
287 }
288}
289
290// PRECOMPILE VERIFIER TRAIT
291// ================================================================================================
292
293/// Trait for verifying precompile computations.
294///
295/// Each precompile type must implement this trait to enable verification of its
296/// computations during proof verification. The verifier validates that the
297/// computation was performed correctly and returns a precompile commitment.
298///
299/// # Stability
300///
301/// **⚠️ Alpha Status**: This trait and the broader precompile verification framework are under
302/// active development. The interface and behavior may change in future releases as the framework
303/// evolves. Production use should account for potential breaking changes.
304pub trait PrecompileVerifier: Send + Sync {
305 /// Verifies a precompile computation from the given call data.
306 ///
307 /// # Arguments
308 /// * `calldata` - The byte data containing the inputs to evaluate the precompile.
309 ///
310 /// # Returns
311 /// Returns a precompile commitment containing both tag and commitment word on success.
312 ///
313 /// # Errors
314 /// Returns an error if the verification fails.
315 fn verify(&self, calldata: &[u8]) -> Result<PrecompileCommitment, PrecompileError>;
316}
317
318// PRECOMPILE TRANSCRIPT
319// ================================================================================================
320
321/// Precompile transcript implemented with a Poseidon2 sponge.
322///
323/// # Structure
324/// Standard Poseidon2 sponge: 12 elements = rate (8 elements) + capacity (4 elements)
325///
326/// # Operation
327/// - **Record**: Each precompile commitment is recorded by absorbing it into the rate, updating the
328/// capacity
329/// - **State**: The evolving capacity tracks all absorbed commitments in order
330/// - **Finalization**: Squeeze with zero rate to extract a transcript digest (the sequential
331/// commitment)
332///
333/// # Implementation Note
334/// We store only the 4-element capacity portion between absorptions since since the rate is always
335/// overwritten when absorbing blocks that are a multiple of the rate width.
336#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
337pub struct PrecompileTranscript {
338 /// The transcript state (capacity portion of the sponge).
339 state: Word,
340}
341
342impl PrecompileTranscript {
343 /// Creates a new sponge with zero capacity.
344 pub fn new() -> Self {
345 Self::default()
346 }
347
348 /// Creates a transcript from an existing state (for VM operations like `log_precompile`).
349 pub fn from_state(state: PrecompileTranscriptState) -> Self {
350 Self { state }
351 }
352
353 /// Returns the current transcript state (capacity word).
354 pub fn state(&self) -> PrecompileTranscriptState {
355 self.state
356 }
357
358 /// Records a precompile commitment into the transcript, updating the state.
359 pub fn record(&mut self, commitment: PrecompileCommitment) {
360 // Internal Poseidon2 state layout is [RATE0, RATE1, CAPACITY].
361 // For the transcript:
362 // - RATE0 = COMM (commitment to calldata)
363 // - RATE1 = TAG (event metadata)
364 // - CAPACITY = current transcript state.
365 let mut state = [ZERO; Poseidon2::STATE_WIDTH];
366 let comm = commitment.comm_calldata();
367 let tag = commitment.tag();
368
369 state[Poseidon2::RATE0_RANGE].copy_from_slice(comm.as_elements());
370 state[Poseidon2::RATE1_RANGE].copy_from_slice(tag.as_elements());
371 state[Poseidon2::CAPACITY_RANGE].copy_from_slice(self.state.as_elements());
372
373 Poseidon2::apply_permutation(&mut state);
374 // After absorption, update the state.
375 self.state = Word::new(state[Poseidon2::CAPACITY_RANGE].try_into().unwrap());
376 }
377
378 /// Finalizes the transcript to a digest (sequential commitment to all recorded requests).
379 ///
380 /// # Details
381 /// The output is equivalent to the sequential hash of all [`PrecompileCommitment`]s, followed
382 /// by two empty words. This is because
383 /// - Each commitment is represented as two words, a multiple of the rate.
384 /// - The initial capacity is set to the zero word since we absorb full double words when
385 /// calling `record` or `finalize`.
386 pub fn finalize(self) -> PrecompileTranscriptDigest {
387 // Interpret state as [RATE0, RATE1, CAPACITY] with two empty rate words.
388 let mut state = [ZERO; Poseidon2::STATE_WIDTH];
389 state[Poseidon2::CAPACITY_RANGE].copy_from_slice(self.state.as_elements());
390
391 Poseidon2::apply_permutation(&mut state);
392 PrecompileTranscriptDigest::new(state[Poseidon2::DIGEST_RANGE].try_into().unwrap())
393 }
394}
395
396// PRECOMPILE ERROR
397// ================================================================================================
398
399/// Type alias for precompile errors.
400///
401/// Verifiers should return informative, structured errors (e.g., using `thiserror`) so callers
402/// can surface meaningful diagnostics.
403pub type PrecompileError = Box<dyn Error + Send + Sync + 'static>;
404
405#[derive(Debug, thiserror::Error)]
406pub enum PrecompileVerificationError {
407 #[error("no verifier found for request #{index} for event with ID: {event_id}")]
408 VerifierNotFound { index: usize, event_id: EventId },
409
410 #[error("verification error for request #{index} for event '{event_name}'")]
411 PrecompileError {
412 index: usize,
413 event_name: EventName,
414 #[source]
415 error: PrecompileError,
416 },
417}
418
419// TESTS
420// ================================================================================================
421
422#[cfg(all(feature = "arbitrary", test))]
423impl proptest::prelude::Arbitrary for PrecompileRequest {
424 type Parameters = ();
425
426 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
427 use proptest::prelude::*;
428 (any::<EventId>(), proptest::collection::vec(any::<u8>(), 0..=1000))
429 .prop_map(|(event_id, calldata)| PrecompileRequest::new(event_id, calldata))
430 .boxed()
431 }
432
433 type Strategy = proptest::prelude::BoxedStrategy<Self>;
434}