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 an RPO256 sponge) that creates a
43//! sequential commitment to all precompile requests.
44//!
45//! # Example Implementation
46//!
47//! See the Keccak256 precompile in `miden_stdlib::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, hash::rpo::Rpo256};
61#[cfg(feature = "serde")]
62use serde::{Deserialize, Serialize};
63
64use crate::{
65 EventId, EventName,
66 utils::{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(winter_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
123// PRECOMPILE TRANSCRIPT TYPES
124// ================================================================================================
125
126/// Type alias representing the precompile transcript state (sponge capacity word).
127///
128/// This is simply a [`Word`] used to track the evolving state of the precompile transcript sponge.
129pub type PrecompileTranscriptState = Word;
130
131/// Type alias representing the finalized transcript digest.
132///
133/// This is simply a [`Word`] representing the final digest of all precompile commitments.
134pub type PrecompileTranscriptDigest = Word;
135
136// PRECOMPILE COMMITMENT
137// ================================================================================================
138
139/// A commitment to the evaluation of [`PrecompileRequest`], representing both the input and result
140/// of the request.
141///
142/// This structure contains both the tag (which includes metadata like event ID)
143/// and the commitment to the input and result (calldata) of the precompile request.
144///
145/// # Tag Structure
146///
147/// The tag is a 4-element word `[event_id, meta1, meta2, meta3]` where:
148///
149/// - **First element**: The [`EventId`] from the corresponding `EventHandler`
150/// - **Remaining 3 elements**: Available for precompile-specific metadata (e.g., `len_bytes` for
151/// hash functions to distinguish actual data from padding)
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub struct PrecompileCommitment {
154 tag: Word,
155 comm: Word,
156}
157
158impl PrecompileCommitment {
159 /// Creates a new precompile commitment from a `TAG` and `COMM`.
160 ///
161 /// - `TAG`: 4-element word where the first element encodes the [`EventId`]; the remaining
162 /// elements are available as precompile-specific metadata (e.g., `len_bytes`).
163 /// - `COMM`: 4-element word containing the commitment to the calldata (or handler-specific
164 /// witness) for this precompile request.
165 pub fn new(tag: Word, comm: Word) -> Self {
166 Self { tag, comm }
167 }
168
169 /// Returns the `TAG` word which encodes the [`EventId`] in the first element and optional
170 /// precompile-specific metadata in the remaining three elements.
171 pub fn tag(&self) -> Word {
172 self.tag
173 }
174
175 /// Returns the `COMM` word (calldata commitment), i.e., the commitment to the precompile's
176 /// calldata (or other handler-specific witness).
177 pub fn comm_calldata(&self) -> Word {
178 self.comm
179 }
180
181 /// Returns the concatenation of `TAG` and `COMM` as field elements.
182 pub fn to_elements(&self) -> [Felt; 8] {
183 let words = [self.tag, self.comm];
184 Word::words_as_elements(&words).try_into().unwrap()
185 }
186
187 /// Returns the `EventId` used to identify the verifier that produced this commitment from a
188 /// `PrecompileRequest`.
189 pub fn event_id(&self) -> EventId {
190 EventId::from_felt(self.tag[0])
191 }
192}
193
194// PRECOMPILE VERIFIERS REGISTRY
195// ================================================================================================
196
197/// Registry of precompile verifiers.
198///
199/// This struct maintains a map of event IDs to their corresponding event names and verifiers.
200/// It is used to verify precompile requests during proof verification.
201#[derive(Default, Clone)]
202pub struct PrecompileVerifierRegistry {
203 /// Map of event IDs to their corresponding event names and verifiers
204 verifiers: BTreeMap<EventId, (EventName, Arc<dyn PrecompileVerifier>)>,
205}
206
207impl PrecompileVerifierRegistry {
208 /// Creates a new empty precompile verifiers registry.
209 pub fn new() -> Self {
210 Self { verifiers: BTreeMap::new() }
211 }
212
213 /// Returns a new registry that includes the supplied verifier in addition to existing ones.
214 pub fn with_verifier(
215 mut self,
216 event_name: &EventName,
217 verifier: Arc<dyn PrecompileVerifier>,
218 ) -> Self {
219 let event_id = event_name.to_event_id();
220 self.verifiers.insert(event_id, (event_name.clone(), verifier));
221 self
222 }
223
224 /// Merges another registry into this one, overwriting any conflicting event IDs with the other
225 /// registry's verifiers.
226 pub fn merge(&mut self, other: &Self) {
227 for (event_id, (event_name, verifier)) in other.verifiers.iter() {
228 self.verifiers.insert(*event_id, (event_name.clone(), verifier.clone()));
229 }
230 }
231
232 /// Verifies all precompile requests and returns the resulting precompile transcript state after
233 /// recording all commitments.
234 ///
235 /// # Errors
236 /// Returns a [`PrecompileVerificationError`] if:
237 /// - No verifier is registered for a request's event ID
238 /// - A verifier fails to verify its request
239 pub fn requests_transcript(
240 &self,
241 requests: &[PrecompileRequest],
242 ) -> Result<PrecompileTranscript, PrecompileVerificationError> {
243 let mut transcript = PrecompileTranscript::new();
244 for (index, PrecompileRequest { event_id, calldata }) in requests.iter().enumerate() {
245 let (event_name, verifier) = self.verifiers.get(event_id).ok_or(
246 PrecompileVerificationError::VerifierNotFound { index, event_id: *event_id },
247 )?;
248
249 let precompile_commitment = verifier.verify(calldata).map_err(|error| {
250 PrecompileVerificationError::PrecompileError {
251 index,
252 event_name: event_name.clone(),
253 error,
254 }
255 })?;
256 transcript.record(precompile_commitment);
257 }
258 Ok(transcript)
259 }
260}
261
262// PRECOMPILE VERIFIER TRAIT
263// ================================================================================================
264
265/// Trait for verifying precompile computations.
266///
267/// Each precompile type must implement this trait to enable verification of its
268/// computations during proof verification. The verifier validates that the
269/// computation was performed correctly and returns a precompile commitment.
270///
271/// # Stability
272///
273/// **⚠️ Alpha Status**: This trait and the broader precompile verification framework are under
274/// active development. The interface and behavior may change in future releases as the framework
275/// evolves. Production use should account for potential breaking changes.
276pub trait PrecompileVerifier: Send + Sync {
277 /// Verifies a precompile computation from the given call data.
278 ///
279 /// # Arguments
280 /// * `calldata` - The byte data containing the inputs to evaluate the precompile.
281 ///
282 /// # Returns
283 /// Returns a precompile commitment containing both tag and commitment word on success.
284 ///
285 /// # Errors
286 /// Returns an error if the verification fails.
287 fn verify(&self, calldata: &[u8]) -> Result<PrecompileCommitment, PrecompileError>;
288}
289
290// PRECOMPILE TRANSCRIPT
291// ================================================================================================
292
293/// Precompile transcript implemented with an RPO256 sponge.
294///
295/// # Structure
296/// Standard RPO256 sponge: 12 elements = capacity (4 elements) + rate (8 elements)
297///
298/// # Operation
299/// - **Record**: Each precompile commitment is recorded by absorbing it into the rate, updating the
300/// capacity
301/// - **State**: The evolving capacity tracks all absorbed commitments in order
302/// - **Finalization**: Squeeze with zero rate to extract a transcript digest (the sequential
303/// commitment)
304///
305/// # Implementation Note
306/// We store only the 4-element capacity portion between absorptions since since the rate is always
307/// overwritten when absorbing blocks that are a multiple of the rate width.
308#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
309pub struct PrecompileTranscript {
310 /// The transcript state (capacity portion of the sponge).
311 state: Word,
312}
313
314impl PrecompileTranscript {
315 /// Creates a new sponge with zero capacity.
316 pub fn new() -> Self {
317 Self::default()
318 }
319
320 /// Creates a transcript from an existing state (for VM operations like `log_precompile`).
321 pub fn from_state(state: PrecompileTranscriptState) -> Self {
322 Self { state }
323 }
324
325 /// Returns the current transcript state (capacity word).
326 pub fn state(&self) -> PrecompileTranscriptState {
327 self.state
328 }
329
330 /// Records a precompile commitment into the transcript, updating the state.
331 pub fn record(&mut self, commitment: PrecompileCommitment) {
332 let mut state =
333 Word::words_as_elements(&[self.state, commitment.tag(), commitment.comm_calldata()])
334 .try_into()
335 .unwrap();
336 Rpo256::apply_permutation(&mut state);
337 self.state = Word::new(state[0..4].try_into().unwrap());
338 }
339
340 /// Finalizes the transcript to a digest (sequential commitment to all recorded requests).
341 ///
342 /// # Details
343 /// The output is equivalent to the sequential hash of all [`PrecompileCommitment`]s, followed
344 /// by two empty words. This is because
345 /// - Each commitment is represented as two words, a multiple of the rate.
346 /// - The initial capacity is set to the zero word since we absord full double words when
347 /// calling `record` or `finalize`.
348 pub fn finalize(self) -> PrecompileTranscriptDigest {
349 let mut state = Word::words_as_elements(&[self.state, Word::empty(), Word::empty()])
350 .try_into()
351 .unwrap();
352 Rpo256::apply_permutation(&mut state);
353 PrecompileTranscriptDigest::new(state[4..8].try_into().unwrap())
354 }
355}
356
357// PRECOMPILE ERROR
358// ================================================================================================
359
360/// Type alias for precompile errors.
361///
362/// Verifiers should return informative, structured errors (e.g., using `thiserror`) so callers
363/// can surface meaningful diagnostics.
364pub type PrecompileError = Box<dyn Error + Send + Sync + 'static>;
365
366#[derive(Debug, thiserror::Error)]
367pub enum PrecompileVerificationError {
368 #[error("no verifier found for request #{index} for event with ID: {event_id}")]
369 VerifierNotFound { index: usize, event_id: EventId },
370
371 #[error("verification error for request #{index} for event '{event_name}'")]
372 PrecompileError {
373 index: usize,
374 event_name: EventName,
375 #[source]
376 error: PrecompileError,
377 },
378}
379
380// TESTS
381// ================================================================================================
382
383#[cfg(all(feature = "arbitrary", test))]
384impl proptest::prelude::Arbitrary for PrecompileRequest {
385 type Parameters = ();
386
387 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
388 use proptest::prelude::*;
389 (any::<EventId>(), proptest::collection::vec(any::<u8>(), 0..=1000))
390 .prop_map(|(event_id, calldata)| PrecompileRequest::new(event_id, calldata))
391 .boxed()
392 }
393
394 type Strategy = proptest::prelude::BoxedStrategy<Self>;
395}