miden-prover 0.35.0

Miden VM prover
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
use alloc::{string::ToString, vec, vec::Vec};

use miden_core::proof::{ExecutionProof, HashFunction, PrecompileProof, PrecompileStatus, VmProof};
use miden_processor::{
    ExecutionError, ExecutionOptions, ExecutionWitness, FastProcessor, PrecompileWitness, Program,
    StackInputs, StackOutputs, SyncHost, VmWitness,
    advice::AdviceInputs,
    trace::{self, VmTrace, build_trace_with_budget},
};

use crate::{config, prove_stark};

/// A synchronous, configurable prover for post-execution Miden VM witnesses.
///
/// This type does not execute programs. It owns proof-generation policy — including the memory
/// budget for materializing a VM execution trace — and consumes witnesses produced by the
/// processor.
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Prover {
    hash_fn: HashFunction,
    max_prover_memory_bytes: u64,
    max_precompile_prover_memory_bytes: u64,
}

impl Prover {
    /// Default maximum memory, in bytes, this prover is permitted to allocate for a proof over a
    /// VM execution trace.
    ///
    /// This bounds only the lifted-STARK Miden VM proof modelled by `miden_air::memory`; the
    /// precompile prover's memory footprint is bounded separately by
    /// [`max_precompile_prover_memory_bytes`](Self::max_precompile_prover_memory_bytes).
    pub const DEFAULT_MAX_PROVER_MEMORY_BYTES: u64 = trace::DEFAULT_MAX_PROVER_MEMORY_BYTES;

    /// Default maximum memory, in bytes, this prover is permitted to allocate for a proof over a
    /// precompile witness.
    pub const DEFAULT_MAX_PRECOMPILE_PROVER_MEMORY_BYTES: u64 =
        miden_precompiles_prover::DEFAULT_MAX_PRECOMPILE_PROVER_MEMORY_BYTES;

    /// Creates a prover with the canonical proof-generation configuration.
    pub const fn new() -> Self {
        Self {
            hash_fn: HashFunction::Blake3_256,
            max_prover_memory_bytes: Self::DEFAULT_MAX_PROVER_MEMORY_BYTES,
            max_precompile_prover_memory_bytes: Self::DEFAULT_MAX_PRECOMPILE_PROVER_MEMORY_BYTES,
        }
    }

    /// Sets the hash function used for proofs generated by this prover.
    #[must_use]
    pub const fn with_hash_fn(mut self, hash_fn: HashFunction) -> Self {
        self.hash_fn = hash_fn;
        self
    }

    /// Sets the maximum memory, in bytes, this prover is permitted to allocate for a proof over a
    /// VM execution trace.
    #[must_use]
    pub const fn with_max_prover_memory_bytes(mut self, max_prover_memory_bytes: u64) -> Self {
        self.max_prover_memory_bytes = max_prover_memory_bytes;
        self
    }

    /// Returns the maximum memory, in bytes, this prover is permitted to allocate for a proof
    /// over a VM execution trace.
    pub const fn max_prover_memory_bytes(&self) -> u64 {
        self.max_prover_memory_bytes
    }

    /// Sets the maximum memory, in bytes, this prover is permitted to allocate for a proof over a
    /// precompile witness.
    #[must_use]
    pub const fn with_max_precompile_prover_memory_bytes(
        mut self,
        max_precompile_prover_memory_bytes: u64,
    ) -> Self {
        self.max_precompile_prover_memory_bytes = max_precompile_prover_memory_bytes;
        self
    }

    /// Returns the maximum memory, in bytes, this prover is permitted to allocate for a proof
    /// over a precompile witness.
    pub const fn max_precompile_prover_memory_bytes(&self) -> u64 {
        self.max_precompile_prover_memory_bytes
    }

    /// Proves only the VM portion of an execution witness.
    ///
    /// If the execution authenticated deferred precompile work, the returned proof carries its
    /// portable singleton witness for later proving. Otherwise, it is complete.
    pub fn prove(&self, witness: ExecutionWitness) -> Result<ExecutionProof, ProverError> {
        let (vm_witness, precompile_witness) = witness.into_parts();
        let vm = self.prove_vm(vm_witness)?;
        let Some(precompile_witness) = precompile_witness else {
            return Ok(ExecutionProof::new(vm, PrecompileStatus::Empty));
        };
        Ok(ExecutionProof::new(vm, PrecompileStatus::Deferred(precompile_witness)))
    }

    /// Proves a complete execution witness entirely in memory.
    ///
    /// VM replay and direct precompile import consume the in-memory witness without serialization.
    pub fn prove_full(&self, witness: ExecutionWitness) -> Result<ExecutionProof, ProverError> {
        let (vm_witness, precompile_witness) = witness.into_parts();
        let vm = self.prove_vm(vm_witness)?;
        let precompile = precompile_witness
            .map(|witness| self.prove_precompiles(vec![witness]))
            .transpose()?;
        let precompile = match precompile {
            Some(precompile) => PrecompileStatus::Proven(precompile),
            None => PrecompileStatus::Empty,
        };
        Ok(ExecutionProof::new(vm, precompile))
    }

    /// Proves a VM witness that does not authenticate deferred precompile work.
    ///
    /// The returned execution proof has an empty precompile status. Use [`Self::prove`] or
    /// [`Self::prove_full`] when the original execution witness contains precompile work.
    pub fn prove_vm_witness(&self, witness: VmWitness) -> Result<ExecutionProof, ProverError> {
        if witness.has_precompiles() {
            return Err(ProverError::VmWitnessHasPrecompiles);
        }

        let vm = self.prove_vm(witness)?;
        Ok(ExecutionProof::new(vm, PrecompileStatus::Empty))
    }

    /// Materializes and proves the VM trace represented by `witness`.
    fn prove_vm(&self, witness: VmWitness) -> Result<VmProof, ProverError> {
        let trace = {
            let _span = tracing::info_span!("build_miden_vm_trace").entered();
            build_trace_with_budget(witness, self.max_prover_memory_bytes)
                .map_err(ProverError::TraceGeneration)?
        };

        self.prove_vm_trace(trace)
    }

    /// Proves an owned batch of singleton execution obligations in one STARK.
    ///
    /// The proof preserves the input roots in order, including repeated roots. An empty batch
    /// is rejected. Single-execution proving uses this same path with a one-element vector.
    ///
    /// Batch-wide limits are checked during import, before STARK generation. Individually valid
    /// witnesses may exceed these limits when combined. Input accounting counts each supplied
    /// occurrence before sharing computations, including repeated data across witnesses.
    pub fn prove_precompiles(
        &self,
        witnesses: Vec<PrecompileWitness>,
    ) -> Result<PrecompileProof, ProverError> {
        miden_precompiles_prover::prove_precompiles_with_budget(
            witnesses,
            self.hash_fn,
            self.max_precompile_prover_memory_bytes,
        )
        .map_err(ProverError::PrecompileProofGeneration)
    }

    #[cfg(feature = "std")]
    fn prove_full_trace(
        &self,
        trace: VmTrace,
        precompile: Option<PrecompileWitness>,
    ) -> Result<ExecutionProof, ProverError> {
        let vm = self.prove_vm_trace(trace)?;
        let precompile =
            precompile.map(|witness| self.prove_precompiles(vec![witness])).transpose()?;
        let precompile = match precompile {
            Some(precompile) => PrecompileStatus::Proven(precompile),
            None => PrecompileStatus::Empty,
        };
        Ok(ExecutionProof::new(vm, precompile))
    }

    /// Proves a fully materialized VM trace.
    ///
    /// Buffered and overlapped trace construction share this private implementation so STARK
    /// generation and VM proof packaging cannot diverge.
    #[tracing::instrument(name = "miden_vm", skip_all)]
    fn prove_vm_trace(&self, trace: VmTrace) -> Result<VmProof, ProverError> {
        let trace_len_summary = trace.trace_len_summary();
        let params = config::pcs_params();
        tracing::event!(
            tracing::Level::INFO,
            "Generated execution traces: core={}, range={}, chiplets={}, poseidon2={}, padded={}, \
             estimated_prover_memory_bytes={:?}",
            trace_len_summary.core_trace_len(),
            trace_len_summary.range_trace_len(),
            trace_len_summary.chiplets_trace_len().trace_len(),
            trace_len_summary.poseidon2_permutation_trace_len(),
            trace_len_summary.padded_trace_len(),
            trace_len_summary.prover_memory_bytes(&params)
        );

        let precompile_root = trace.precompile_root();
        let (public_values, aux_inputs) = trace.public_inputs().to_air_inputs();
        let (core_matrix, chiplets_matrix, poseidon2_matrix) = trace.into_air_matrices();

        let proof_bytes = match self.hash_fn {
            HashFunction::Blake3_256 => {
                let config = config::blake3_256_config(params, config::RELATION_DIGEST);
                prove_stark(
                    &config,
                    core_matrix,
                    chiplets_matrix,
                    poseidon2_matrix,
                    &public_values,
                    &aux_inputs,
                )
            },
            HashFunction::Keccak => {
                let config = config::keccak_config(params, config::RELATION_DIGEST);
                prove_stark(
                    &config,
                    core_matrix,
                    chiplets_matrix,
                    poseidon2_matrix,
                    &public_values,
                    &aux_inputs,
                )
            },
            HashFunction::Rpo256 => {
                let config = config::rpo_config(params, config::RELATION_DIGEST);
                prove_stark(
                    &config,
                    core_matrix,
                    chiplets_matrix,
                    poseidon2_matrix,
                    &public_values,
                    &aux_inputs,
                )
            },
            HashFunction::Poseidon2 => {
                let config = config::poseidon2_config(params, config::RELATION_DIGEST);
                prove_stark(
                    &config,
                    core_matrix,
                    chiplets_matrix,
                    poseidon2_matrix,
                    &public_values,
                    &aux_inputs,
                )
            },
            HashFunction::Rpx256 => {
                let config = config::rpx_config(params, config::RELATION_DIGEST);
                prove_stark(
                    &config,
                    core_matrix,
                    chiplets_matrix,
                    poseidon2_matrix,
                    &public_values,
                    &aux_inputs,
                )
            },
        }
        .map_err(ProverError::VmProofGeneration)?;

        let proof = miden_core::proof::StarkProof::new(proof_bytes, self.hash_fn);
        Ok(VmProof { proof, precompile_root })
    }
}

/// Executes and fully proves a program synchronously.
///
/// This FastProcessor-backed orchestration function preserves the optimized overlapped
/// execution/trace-building path. Proving policy belongs on [`Prover`].
///
/// When enabled in `execution_options`, the processor may build the hasher chiplet alongside
/// execution. A caller with no separate Rayon worker uses compact buffered replay. Both cases use
/// the same private VM STARK and complete-local packaging implementation.
#[tracing::instrument(name = "prove_program_sync", skip_all)]
pub fn prove_sync(
    prover: &Prover,
    program: &Program,
    stack_inputs: StackInputs,
    advice_inputs: AdviceInputs,
    host: &mut impl SyncHost,
    execution_options: ExecutionOptions,
) -> Result<(StackOutputs, ExecutionProof), ExecutionError> {
    #[cfg(feature = "std")]
    let overlapped_trace_build = execution_options.overlapped_trace_build();
    let processor = FastProcessor::new_with_options(stack_inputs, advice_inputs, execution_options)
        .map_err(ExecutionError::advice_error_no_context)?;

    #[cfg(feature = "std")]
    if overlapped_trace_build {
        let (trace, precompile) = {
            let _span = tracing::info_span!("execute_miden_vm").entered();
            processor.execute_and_build_trace_sync(
                program,
                host,
                prover.max_prover_memory_bytes(),
            )?
        };
        let stack_outputs = *trace.stack_outputs();
        let proof = prover
            .prove_full_trace(trace, precompile)
            .map_err(ProverError::into_execution_error)?;
        return Ok((stack_outputs, proof));
    }

    let witness = {
        let _span = tracing::info_span!("execute_miden_vm").entered();
        processor.execute_for_proving_sync(program, host)?
    };
    let stack_outputs = *witness.claim().stack_outputs();
    let proof = prover.prove_full(witness).map_err(ProverError::into_execution_error)?;
    Ok((stack_outputs, proof))
}

impl Default for Prover {
    fn default() -> Self {
        Self::new()
    }
}

/// Errors produced while proving post-execution witnesses.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ProverError {
    /// The VM witness authenticates deferred precompile work that this proving path cannot carry.
    #[error("VM witness contains deferred precompile work")]
    VmWitnessHasPrecompiles,
    /// The processor witness could not be materialized into a valid execution trace.
    #[error("failed to materialize VM execution trace: {0}")]
    TraceGeneration(#[source] ExecutionError),
    /// The materialized VM trace could not be proved.
    #[error("failed to prove VM execution trace: {0}")]
    VmProofGeneration(#[source] ExecutionError),
    /// The deferred precompile witness could not be proved.
    #[error("failed to prove precompile witness: {0}")]
    PrecompileProofGeneration(#[source] miden_precompiles_prover::PrecompileProvingError),
}

impl ProverError {
    fn into_execution_error(self) -> ExecutionError {
        match self {
            Self::VmWitnessHasPrecompiles => ExecutionError::ProvingError(self.to_string()),
            Self::TraceGeneration(error) | Self::VmProofGeneration(error) => error,
            Self::PrecompileProofGeneration(error) => {
                ExecutionError::ProvingError(error.to_string())
            },
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn prover_uses_canonical_default_and_allows_hash_override() {
        let prover = Prover::new();
        assert_eq!(prover.hash_fn, HashFunction::Blake3_256);

        let prover = prover.with_hash_fn(HashFunction::Poseidon2);
        assert_eq!(prover.hash_fn, HashFunction::Poseidon2);
    }

    #[test]
    fn prover_uses_canonical_memory_budget_and_allows_override() {
        let prover = Prover::new();
        assert_eq!(prover.max_prover_memory_bytes(), Prover::DEFAULT_MAX_PROVER_MEMORY_BYTES);

        let prover = prover.with_max_prover_memory_bytes(1 << 20);
        assert_eq!(prover.max_prover_memory_bytes(), 1 << 20);
    }

    #[test]
    fn prover_uses_canonical_precompile_memory_budget_and_allows_override() {
        let prover = Prover::new();
        assert_eq!(
            prover.max_precompile_prover_memory_bytes(),
            Prover::DEFAULT_MAX_PRECOMPILE_PROVER_MEMORY_BYTES
        );

        let prover = prover.with_max_precompile_prover_memory_bytes(1 << 20);
        assert_eq!(prover.max_precompile_prover_memory_bytes(), 1 << 20);
    }

    /// A minimal non-`TRUE` precompile witness: no keccak/uint/EC ops, just the fixed session
    /// installs plus one trivial `AND` node. Its chiplet traces still have nonzero (fixed-minimum)
    /// heights, so it is enough to exercise the memory-budget check without a full precompile
    /// execution fixture.
    fn trivial_precompile_witness() -> PrecompileWitness {
        use alloc::sync::Arc;

        use miden_core::deferred::{DeferredState, Node, PrecompileRegistry, TRUE_DIGEST};

        let registry = Arc::new(PrecompileRegistry::new());
        let mut state =
            DeferredState::new(registry).expect("empty registry state should initialize");
        let statement = state
            .register(Node::and(TRUE_DIGEST, TRUE_DIGEST))
            .expect("trivial AND node should register");
        state
            .log_statement(statement)
            .expect("trivial statement should log into the deferred root");
        state
            .into_witness()
            .expect("trivial deferred state should export")
            .expect("non-TRUE root should export a witness")
    }

    #[test]
    fn prove_precompiles_applies_the_configured_memory_budget() {
        // A 1-byte budget must reject even the minimal chiplet trace shapes, and the returned
        // error carries the configured budget. Exact-boundary behavior is covered by
        // `miden-precompiles-prover`.
        let err = Prover::new()
            .with_max_precompile_prover_memory_bytes(1)
            .prove_precompiles(vec![trivial_precompile_witness()])
            .expect_err("a 1-byte budget must reject even the minimal chiplet trace shapes");
        assert!(
            matches!(
                err,
                ProverError::PrecompileProofGeneration(
                    miden_precompiles_prover::PrecompileProvingError::MemoryBudgetExceeded {
                        budget_bytes: 1,
                        ..
                    }
                )
            ),
            "expected MemoryBudgetExceeded {{ budget_bytes: 1, .. }}, got: {err:?}"
        );
    }
}