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