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}