Skip to main content

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}