Skip to main content

miden_prover/
prover.rs

1use alloc::{string::ToString, vec, vec::Vec};
2
3use miden_core::proof::{ExecutionProof, HashFunction, PrecompileProof, PrecompileStatus, VmProof};
4use miden_processor::{
5    ExecutionError, ExecutionOptions, ExecutionWitness, FastProcessor, PrecompileWitness, Program,
6    StackInputs, StackOutputs, SyncHost, VmWitness,
7    advice::AdviceInputs,
8    trace::{self, VmTrace, build_trace_with_budget},
9};
10
11use crate::{config, prove_stark};
12
13/// A synchronous, configurable prover for post-execution Miden VM witnesses.
14///
15/// This type does not execute programs. It owns proof-generation policy — including the memory
16/// budget for materializing a VM execution trace — and consumes witnesses produced by the
17/// processor.
18#[derive(Debug, Clone, Eq, PartialEq)]
19pub struct Prover {
20    hash_fn: HashFunction,
21    max_prover_memory_bytes: u64,
22    max_precompile_prover_memory_bytes: u64,
23}
24
25impl Prover {
26    /// Default maximum memory, in bytes, this prover is permitted to allocate for a proof over a
27    /// VM execution trace.
28    ///
29    /// This bounds only the lifted-STARK Miden VM proof modelled by `miden_air::memory`; the
30    /// precompile prover's memory footprint is bounded separately by
31    /// [`max_precompile_prover_memory_bytes`](Self::max_precompile_prover_memory_bytes).
32    pub const DEFAULT_MAX_PROVER_MEMORY_BYTES: u64 = trace::DEFAULT_MAX_PROVER_MEMORY_BYTES;
33
34    /// Default maximum memory, in bytes, this prover is permitted to allocate for a proof over a
35    /// precompile witness.
36    pub const DEFAULT_MAX_PRECOMPILE_PROVER_MEMORY_BYTES: u64 =
37        miden_precompiles_prover::DEFAULT_MAX_PRECOMPILE_PROVER_MEMORY_BYTES;
38
39    /// Creates a prover with the canonical proof-generation configuration.
40    pub const fn new() -> Self {
41        Self {
42            hash_fn: HashFunction::Blake3_256,
43            max_prover_memory_bytes: Self::DEFAULT_MAX_PROVER_MEMORY_BYTES,
44            max_precompile_prover_memory_bytes: Self::DEFAULT_MAX_PRECOMPILE_PROVER_MEMORY_BYTES,
45        }
46    }
47
48    /// Sets the hash function used for proofs generated by this prover.
49    #[must_use]
50    pub const fn with_hash_fn(mut self, hash_fn: HashFunction) -> Self {
51        self.hash_fn = hash_fn;
52        self
53    }
54
55    /// Sets the maximum memory, in bytes, this prover is permitted to allocate for a proof over a
56    /// VM execution trace.
57    #[must_use]
58    pub const fn with_max_prover_memory_bytes(mut self, max_prover_memory_bytes: u64) -> Self {
59        self.max_prover_memory_bytes = max_prover_memory_bytes;
60        self
61    }
62
63    /// Returns the maximum memory, in bytes, this prover is permitted to allocate for a proof
64    /// over a VM execution trace.
65    pub const fn max_prover_memory_bytes(&self) -> u64 {
66        self.max_prover_memory_bytes
67    }
68
69    /// Sets the maximum memory, in bytes, this prover is permitted to allocate for a proof over a
70    /// precompile witness.
71    #[must_use]
72    pub const fn with_max_precompile_prover_memory_bytes(
73        mut self,
74        max_precompile_prover_memory_bytes: u64,
75    ) -> Self {
76        self.max_precompile_prover_memory_bytes = max_precompile_prover_memory_bytes;
77        self
78    }
79
80    /// Returns the maximum memory, in bytes, this prover is permitted to allocate for a proof
81    /// over a precompile witness.
82    pub const fn max_precompile_prover_memory_bytes(&self) -> u64 {
83        self.max_precompile_prover_memory_bytes
84    }
85
86    /// Proves only the VM portion of an execution witness.
87    ///
88    /// If the execution authenticated deferred precompile work, the returned proof carries its
89    /// portable singleton witness for later proving. Otherwise, it is complete.
90    pub fn prove(&self, witness: ExecutionWitness) -> Result<ExecutionProof, ProverError> {
91        let (vm_witness, precompile_witness) = witness.into_parts();
92        let vm = self.prove_vm(vm_witness)?;
93        let Some(precompile_witness) = precompile_witness else {
94            return Ok(ExecutionProof::new(vm, PrecompileStatus::Empty));
95        };
96        Ok(ExecutionProof::new(vm, PrecompileStatus::Deferred(precompile_witness)))
97    }
98
99    /// Proves a complete execution witness entirely in memory.
100    ///
101    /// VM replay and direct precompile import consume the in-memory witness without serialization.
102    pub fn prove_full(&self, witness: ExecutionWitness) -> Result<ExecutionProof, ProverError> {
103        let (vm_witness, precompile_witness) = witness.into_parts();
104        let vm = self.prove_vm(vm_witness)?;
105        let precompile = precompile_witness
106            .map(|witness| self.prove_precompiles(vec![witness]))
107            .transpose()?;
108        let precompile = match precompile {
109            Some(precompile) => PrecompileStatus::Proven(precompile),
110            None => PrecompileStatus::Empty,
111        };
112        Ok(ExecutionProof::new(vm, precompile))
113    }
114
115    /// Proves a VM witness that does not authenticate deferred precompile work.
116    ///
117    /// The returned execution proof has an empty precompile status. Use [`Self::prove`] or
118    /// [`Self::prove_full`] when the original execution witness contains precompile work.
119    pub fn prove_vm_witness(&self, witness: VmWitness) -> Result<ExecutionProof, ProverError> {
120        if witness.has_precompiles() {
121            return Err(ProverError::VmWitnessHasPrecompiles);
122        }
123
124        let vm = self.prove_vm(witness)?;
125        Ok(ExecutionProof::new(vm, PrecompileStatus::Empty))
126    }
127
128    /// Materializes and proves the VM trace represented by `witness`.
129    fn prove_vm(&self, witness: VmWitness) -> Result<VmProof, ProverError> {
130        let trace = {
131            let _span = tracing::info_span!("build_miden_vm_trace").entered();
132            build_trace_with_budget(witness, self.max_prover_memory_bytes)
133                .map_err(ProverError::TraceGeneration)?
134        };
135
136        self.prove_vm_trace(trace)
137    }
138
139    /// Proves an owned batch of singleton execution obligations in one STARK.
140    ///
141    /// The proof preserves the input roots in order, including repeated roots. An empty batch
142    /// is rejected. Single-execution proving uses this same path with a one-element vector.
143    ///
144    /// Batch-wide limits are checked during import, before STARK generation. Individually valid
145    /// witnesses may exceed these limits when combined. Input accounting counts each supplied
146    /// occurrence before sharing computations, including repeated data across witnesses.
147    pub fn prove_precompiles(
148        &self,
149        witnesses: Vec<PrecompileWitness>,
150    ) -> Result<PrecompileProof, ProverError> {
151        miden_precompiles_prover::prove_precompiles_with_budget(
152            witnesses,
153            self.hash_fn,
154            self.max_precompile_prover_memory_bytes,
155        )
156        .map_err(ProverError::PrecompileProofGeneration)
157    }
158
159    #[cfg(feature = "std")]
160    fn prove_full_trace(
161        &self,
162        trace: VmTrace,
163        precompile: Option<PrecompileWitness>,
164    ) -> Result<ExecutionProof, ProverError> {
165        let vm = self.prove_vm_trace(trace)?;
166        let precompile =
167            precompile.map(|witness| self.prove_precompiles(vec![witness])).transpose()?;
168        let precompile = match precompile {
169            Some(precompile) => PrecompileStatus::Proven(precompile),
170            None => PrecompileStatus::Empty,
171        };
172        Ok(ExecutionProof::new(vm, precompile))
173    }
174
175    /// Proves a fully materialized VM trace.
176    ///
177    /// Buffered and overlapped trace construction share this private implementation so STARK
178    /// generation and VM proof packaging cannot diverge.
179    #[tracing::instrument(name = "miden_vm", skip_all)]
180    fn prove_vm_trace(&self, trace: VmTrace) -> Result<VmProof, ProverError> {
181        let trace_len_summary = trace.trace_len_summary();
182        let params = config::pcs_params();
183        tracing::event!(
184            tracing::Level::INFO,
185            "Generated execution traces: core={}, range={}, chiplets={}, poseidon2={}, padded={}, \
186             estimated_prover_memory_bytes={:?}",
187            trace_len_summary.core_trace_len(),
188            trace_len_summary.range_trace_len(),
189            trace_len_summary.chiplets_trace_len().trace_len(),
190            trace_len_summary.poseidon2_permutation_trace_len(),
191            trace_len_summary.padded_trace_len(),
192            trace_len_summary.prover_memory_bytes(&params)
193        );
194
195        let precompile_root = trace.precompile_root();
196        let (public_values, aux_inputs) = trace.public_inputs().to_air_inputs();
197        let (core_matrix, chiplets_matrix, poseidon2_matrix) = trace.into_air_matrices();
198
199        let proof_bytes = match self.hash_fn {
200            HashFunction::Blake3_256 => {
201                let config = config::blake3_256_config(params, config::RELATION_DIGEST);
202                prove_stark(
203                    &config,
204                    core_matrix,
205                    chiplets_matrix,
206                    poseidon2_matrix,
207                    &public_values,
208                    &aux_inputs,
209                )
210            },
211            HashFunction::Keccak => {
212                let config = config::keccak_config(params, config::RELATION_DIGEST);
213                prove_stark(
214                    &config,
215                    core_matrix,
216                    chiplets_matrix,
217                    poseidon2_matrix,
218                    &public_values,
219                    &aux_inputs,
220                )
221            },
222            HashFunction::Rpo256 => {
223                let config = config::rpo_config(params, config::RELATION_DIGEST);
224                prove_stark(
225                    &config,
226                    core_matrix,
227                    chiplets_matrix,
228                    poseidon2_matrix,
229                    &public_values,
230                    &aux_inputs,
231                )
232            },
233            HashFunction::Poseidon2 => {
234                let config = config::poseidon2_config(params, config::RELATION_DIGEST);
235                prove_stark(
236                    &config,
237                    core_matrix,
238                    chiplets_matrix,
239                    poseidon2_matrix,
240                    &public_values,
241                    &aux_inputs,
242                )
243            },
244            HashFunction::Rpx256 => {
245                let config = config::rpx_config(params, config::RELATION_DIGEST);
246                prove_stark(
247                    &config,
248                    core_matrix,
249                    chiplets_matrix,
250                    poseidon2_matrix,
251                    &public_values,
252                    &aux_inputs,
253                )
254            },
255        }
256        .map_err(ProverError::VmProofGeneration)?;
257
258        let proof = miden_core::proof::StarkProof::new(proof_bytes, self.hash_fn);
259        Ok(VmProof { proof, precompile_root })
260    }
261}
262
263/// Executes and fully proves a program synchronously.
264///
265/// This FastProcessor-backed orchestration function preserves the optimized overlapped
266/// execution/trace-building path. Proving policy belongs on [`Prover`].
267///
268/// When enabled in `execution_options`, the processor may build the hasher chiplet alongside
269/// execution. A caller with no separate Rayon worker uses compact buffered replay. Both cases use
270/// the same private VM STARK and complete-local packaging implementation.
271#[tracing::instrument(name = "prove_program_sync", skip_all)]
272pub fn prove_sync(
273    prover: &Prover,
274    program: &Program,
275    stack_inputs: StackInputs,
276    advice_inputs: AdviceInputs,
277    host: &mut impl SyncHost,
278    execution_options: ExecutionOptions,
279) -> Result<(StackOutputs, ExecutionProof), ExecutionError> {
280    #[cfg(feature = "std")]
281    let overlapped_trace_build = execution_options.overlapped_trace_build();
282    let processor = FastProcessor::new_with_options(stack_inputs, advice_inputs, execution_options)
283        .map_err(ExecutionError::advice_error_no_context)?;
284
285    #[cfg(feature = "std")]
286    if overlapped_trace_build {
287        let (trace, precompile) = {
288            let _span = tracing::info_span!("execute_miden_vm").entered();
289            processor.execute_and_build_trace_sync(
290                program,
291                host,
292                prover.max_prover_memory_bytes(),
293            )?
294        };
295        let stack_outputs = *trace.stack_outputs();
296        let proof = prover
297            .prove_full_trace(trace, precompile)
298            .map_err(ProverError::into_execution_error)?;
299        return Ok((stack_outputs, proof));
300    }
301
302    let witness = {
303        let _span = tracing::info_span!("execute_miden_vm").entered();
304        processor.execute_for_proving_sync(program, host)?
305    };
306    let stack_outputs = *witness.claim().stack_outputs();
307    let proof = prover.prove_full(witness).map_err(ProverError::into_execution_error)?;
308    Ok((stack_outputs, proof))
309}
310
311impl Default for Prover {
312    fn default() -> Self {
313        Self::new()
314    }
315}
316
317/// Errors produced while proving post-execution witnesses.
318#[derive(Debug, thiserror::Error)]
319#[non_exhaustive]
320pub enum ProverError {
321    /// The VM witness authenticates deferred precompile work that this proving path cannot carry.
322    #[error("VM witness contains deferred precompile work")]
323    VmWitnessHasPrecompiles,
324    /// The processor witness could not be materialized into a valid execution trace.
325    #[error("failed to materialize VM execution trace: {0}")]
326    TraceGeneration(#[source] ExecutionError),
327    /// The materialized VM trace could not be proved.
328    #[error("failed to prove VM execution trace: {0}")]
329    VmProofGeneration(#[source] ExecutionError),
330    /// The deferred precompile witness could not be proved.
331    #[error("failed to prove precompile witness: {0}")]
332    PrecompileProofGeneration(#[source] miden_precompiles_prover::PrecompileProvingError),
333}
334
335impl ProverError {
336    fn into_execution_error(self) -> ExecutionError {
337        match self {
338            Self::VmWitnessHasPrecompiles => ExecutionError::ProvingError(self.to_string()),
339            Self::TraceGeneration(error) | Self::VmProofGeneration(error) => error,
340            Self::PrecompileProofGeneration(error) => {
341                ExecutionError::ProvingError(error.to_string())
342            },
343        }
344    }
345}
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350
351    #[test]
352    fn prover_uses_canonical_default_and_allows_hash_override() {
353        let prover = Prover::new();
354        assert_eq!(prover.hash_fn, HashFunction::Blake3_256);
355
356        let prover = prover.with_hash_fn(HashFunction::Poseidon2);
357        assert_eq!(prover.hash_fn, HashFunction::Poseidon2);
358    }
359
360    #[test]
361    fn prover_uses_canonical_memory_budget_and_allows_override() {
362        let prover = Prover::new();
363        assert_eq!(prover.max_prover_memory_bytes(), Prover::DEFAULT_MAX_PROVER_MEMORY_BYTES);
364
365        let prover = prover.with_max_prover_memory_bytes(1 << 20);
366        assert_eq!(prover.max_prover_memory_bytes(), 1 << 20);
367    }
368
369    #[test]
370    fn prover_uses_canonical_precompile_memory_budget_and_allows_override() {
371        let prover = Prover::new();
372        assert_eq!(
373            prover.max_precompile_prover_memory_bytes(),
374            Prover::DEFAULT_MAX_PRECOMPILE_PROVER_MEMORY_BYTES
375        );
376
377        let prover = prover.with_max_precompile_prover_memory_bytes(1 << 20);
378        assert_eq!(prover.max_precompile_prover_memory_bytes(), 1 << 20);
379    }
380
381    /// A minimal non-`TRUE` precompile witness: no keccak/uint/EC ops, just the fixed session
382    /// installs plus one trivial `AND` node. Its chiplet traces still have nonzero (fixed-minimum)
383    /// heights, so it is enough to exercise the memory-budget check without a full precompile
384    /// execution fixture.
385    fn trivial_precompile_witness() -> PrecompileWitness {
386        use alloc::sync::Arc;
387
388        use miden_core::deferred::{DeferredState, Node, PrecompileRegistry, TRUE_DIGEST};
389
390        let registry = Arc::new(PrecompileRegistry::new());
391        let mut state =
392            DeferredState::new(registry).expect("empty registry state should initialize");
393        let statement = state
394            .register(Node::and(TRUE_DIGEST, TRUE_DIGEST))
395            .expect("trivial AND node should register");
396        state
397            .log_statement(statement)
398            .expect("trivial statement should log into the deferred root");
399        state
400            .into_witness()
401            .expect("trivial deferred state should export")
402            .expect("non-TRUE root should export a witness")
403    }
404
405    #[test]
406    fn prove_precompiles_applies_the_configured_memory_budget() {
407        // A 1-byte budget must reject even the minimal chiplet trace shapes, and the returned
408        // error carries the configured budget. Exact-boundary behavior is covered by
409        // `miden-precompiles-prover`.
410        let err = Prover::new()
411            .with_max_precompile_prover_memory_bytes(1)
412            .prove_precompiles(vec![trivial_precompile_witness()])
413            .expect_err("a 1-byte budget must reject even the minimal chiplet trace shapes");
414        assert!(
415            matches!(
416                err,
417                ProverError::PrecompileProofGeneration(
418                    miden_precompiles_prover::PrecompileProvingError::MemoryBudgetExceeded {
419                        budget_bytes: 1,
420                        ..
421                    }
422                )
423            ),
424            "expected MemoryBudgetExceeded {{ budget_bytes: 1, .. }}, got: {err:?}"
425        );
426    }
427}