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};
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Prover {
hash_fn: HashFunction,
max_prover_memory_bytes: u64,
max_precompile_prover_memory_bytes: u64,
}
impl Prover {
pub const DEFAULT_MAX_PROVER_MEMORY_BYTES: u64 = trace::DEFAULT_MAX_PROVER_MEMORY_BYTES;
pub const DEFAULT_MAX_PRECOMPILE_PROVER_MEMORY_BYTES: u64 =
miden_precompiles_prover::DEFAULT_MAX_PRECOMPILE_PROVER_MEMORY_BYTES;
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,
}
}
#[must_use]
pub const fn with_hash_fn(mut self, hash_fn: HashFunction) -> Self {
self.hash_fn = hash_fn;
self
}
#[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
}
pub const fn max_prover_memory_bytes(&self) -> u64 {
self.max_prover_memory_bytes
}
#[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
}
pub const fn max_precompile_prover_memory_bytes(&self) -> u64 {
self.max_precompile_prover_memory_bytes
}
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)))
}
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))
}
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))
}
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)
}
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))
}
#[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(¶ms)
);
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 })
}
}
#[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()
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ProverError {
#[error("VM witness contains deferred precompile work")]
VmWitnessHasPrecompiles,
#[error("failed to materialize VM execution trace: {0}")]
TraceGeneration(#[source] ExecutionError),
#[error("failed to prove VM execution trace: {0}")]
VmProofGeneration(#[source] ExecutionError),
#[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);
}
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() {
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:?}"
);
}
}