Skip to main content

miden_tx/prover/
mod.rs

1use alloc::boxed::Box;
2use alloc::vec::Vec;
3
4use miden_processor::{ExecutionError, ExecutionOptions, FastProcessor};
5use miden_protocol::account::{AccountPatch, AccountUpdateDetails, PartialAccount};
6use miden_protocol::block::BlockNumber;
7use miden_protocol::transaction::{
8    InputNote,
9    InputNotes,
10    ProvenTransaction,
11    TransactionInputs,
12    TransactionKernel,
13    TransactionOutputs,
14    TxAccountUpdate,
15};
16use miden_prover::HashFunction::Poseidon2;
17pub use miden_prover::Prover;
18use miden_prover::{ExecutionProof, Word};
19
20use super::TransactionProverError;
21use crate::host::{AccountProcedureIndexMap, ScriptMastForestStore};
22
23mod prover_host;
24pub use prover_host::TransactionProverHost;
25
26mod mast_store;
27pub use mast_store::TransactionMastStore;
28
29// LOCAL TRANSACTION PROVER
30// ------------------------------------------------------------------------------------------------
31
32/// Local Transaction prover is a stateless component which is responsible for proving transactions.
33///
34/// The produced proof covers the VM execution only. Precompile claims are left deferred, because a
35/// batch settles the claims of all its transactions with a single precompile proof.
36///
37/// Each `prove()` call creates a fresh [`TransactionMastStore`] loaded with only the current
38/// transaction's account code, ensuring no state accumulates across calls. This is important
39/// in WASM environments where accumulated MAST forests fragment the linear memory.
40#[derive(Debug, Clone)]
41pub struct LocalTransactionProver {
42    prover: Prover,
43    execution_options: ExecutionOptions,
44}
45
46impl Default for LocalTransactionProver {
47    fn default() -> Self {
48        Self::new(Prover::new().with_hash_fn(Poseidon2))
49    }
50}
51
52impl LocalTransactionProver {
53    /// Creates a new [LocalTransactionProver] instance with the default [`ExecutionOptions`].
54    pub fn new(prover: Prover) -> Self {
55        Self {
56            prover,
57            execution_options: ExecutionOptions::default(),
58        }
59    }
60
61    /// Sets the [`ExecutionOptions`] used while proving and returns the resulting prover.
62    ///
63    /// This lets a caller tune the VM limits enforced during proving, so that proving can use the
64    /// same options as execution.
65    ///
66    /// This will overwrite any previously set options.
67    #[must_use]
68    pub fn with_execution_options(mut self, execution_options: ExecutionOptions) -> Self {
69        self.execution_options = execution_options;
70        self
71    }
72
73    /// Returns the [`ExecutionOptions`] this prover uses.
74    pub fn execution_options(&self) -> ExecutionOptions {
75        self.execution_options
76    }
77
78    fn build_proven_transaction(
79        &self,
80        input_notes: &InputNotes<InputNote>,
81        tx_outputs: TransactionOutputs,
82        account_patch: AccountPatch,
83        account: PartialAccount,
84        ref_block_num: BlockNumber,
85        ref_block_commitment: Word,
86        proof: ExecutionProof,
87    ) -> Result<ProvenTransaction, TransactionProverError> {
88        let expiration_block_num = tx_outputs.expiration_block_num();
89        let (account_header, output_notes) = tx_outputs.into_parts();
90
91        // erase private note information (convert private full notes to just headers)
92        let output_notes: Vec<_> = output_notes
93            .into_iter()
94            .map(|note| note.into_output_note())
95            .collect::<Result<Vec<_>, _>>()
96            .map_err(TransactionProverError::OutputNoteShrinkFailed)?;
97
98        // Compute the commitment of the patch, which goes into the proven transaction since it is
99        // the output of the transaction and so is needed for proof verification.
100        let patch_commitment: Word = account_patch.to_commitment();
101
102        let account_update_details = if account.id().is_public() {
103            AccountUpdateDetails::Public(account_patch)
104        } else {
105            AccountUpdateDetails::Private
106        };
107
108        let account_update = TxAccountUpdate::new(
109            account.id(),
110            account.initial_commitment(),
111            account_header.to_commitment(),
112            patch_commitment,
113            account_update_details,
114        )
115        .map_err(TransactionProverError::ProvenTransactionBuildFailed)?;
116
117        ProvenTransaction::new(
118            account_update,
119            input_notes.iter(),
120            output_notes,
121            ref_block_num,
122            ref_block_commitment,
123            expiration_block_num,
124            proof,
125        )
126        .map_err(TransactionProverError::ProvenTransactionBuildFailed)
127    }
128
129    pub fn prove(
130        &self,
131        tx_inputs: impl Into<TransactionInputs>,
132    ) -> Result<ProvenTransaction, TransactionProverError> {
133        let tx_inputs = tx_inputs.into();
134        let (stack_inputs, advice_inputs) = TransactionKernel::prepare_inputs(&tx_inputs);
135
136        // Create a per-call MAST store to avoid accumulating forests across prove
137        // calls. Using the shared self.mast_store would grow monotonically (each
138        // call adds account code that is never removed), fragmenting WASM linear
139        // memory and eventually causing capacity_overflow panics. A per-call store
140        // also avoids races: prove() takes &self, so concurrent calls would
141        // conflict on a shared mutable store.
142        let mast_store = TransactionMastStore::new();
143        mast_store.load_account_code(tx_inputs.account().code());
144        for account_code in tx_inputs.foreign_account_code() {
145            mast_store.load_account_code(account_code);
146        }
147
148        let script_mast_store = ScriptMastForestStore::new(
149            tx_inputs.tx_script(),
150            tx_inputs.input_notes().iter().map(|n| n.note().script()),
151        );
152
153        let account_procedure_index_map = AccountProcedureIndexMap::new(
154            tx_inputs.foreign_account_code().iter().chain([tx_inputs.account().code()]),
155        );
156
157        let block_commitments = tx_inputs.collect_block_commitments();
158
159        let (partial_account, ref_block, _, input_notes, _) = tx_inputs.into_parts();
160        let mut host = TransactionProverHost::new(
161            &partial_account,
162            input_notes,
163            block_commitments,
164            &mast_store,
165            script_mast_store,
166            account_procedure_index_map,
167        )
168        .map_err(|err| TransactionProverError::TransactionHostCreationFailed(Box::new(err)))?;
169
170        let advice_inputs = advice_inputs.into_advice_inputs();
171
172        let processor = FastProcessor::new_with_options(
173            stack_inputs,
174            advice_inputs.clone(),
175            self.execution_options,
176        )
177        .map_err(ExecutionError::advice_error_no_context)
178        .map_err(TransactionProverError::TransactionProgramExecutionFailed)?;
179
180        let witness = processor
181            .execute_for_proving_sync(&TransactionKernel::main(), &mut host)
182            .map_err(TransactionProverError::TransactionProgramExecutionFailed)?;
183        let stack_outputs = *witness.claim().stack_outputs();
184
185        let proof = self
186            .prover
187            .prove(witness)
188            .map_err(TransactionProverError::TransactionProofGenerationFailed)?;
189
190        // Extract transaction outputs and process transaction data.
191        let (account_patch, input_notes, output_notes) = host.into_parts();
192        let tx_outputs =
193            TransactionKernel::from_transaction_parts(&stack_outputs, &advice_inputs, output_notes)
194                .map_err(TransactionProverError::TransactionOutputConstructionFailed)?;
195
196        self.build_proven_transaction(
197            &input_notes,
198            tx_outputs,
199            account_patch,
200            partial_account,
201            ref_block.block_num(),
202            ref_block.commitment(),
203            proof,
204        )
205    }
206}
207
208#[cfg(any(feature = "testing", test))]
209impl LocalTransactionProver {
210    pub fn prove_dummy(
211        &self,
212        executed_transaction: miden_protocol::transaction::ExecutedTransaction,
213    ) -> Result<ProvenTransaction, TransactionProverError> {
214        self.prove_with_dummy(
215            executed_transaction,
216            miden_protocol::testing::dummy_execution_proof(),
217        )
218    }
219
220    /// Returns a proven transaction carrying a structurally incomplete proof for verifier tests.
221    pub fn prove_dummy_deferred(
222        &self,
223        executed_transaction: miden_protocol::transaction::ExecutedTransaction,
224    ) -> Result<ProvenTransaction, TransactionProverError> {
225        self.prove_with_dummy(
226            executed_transaction,
227            miden_protocol::testing::dummy_deferred_execution_proof(),
228        )
229    }
230
231    /// Returns a proven transaction carrying a complete proof with precompile work for verifier
232    /// tests.
233    pub fn prove_dummy_precompile(
234        &self,
235        executed_transaction: miden_protocol::transaction::ExecutedTransaction,
236    ) -> Result<ProvenTransaction, TransactionProverError> {
237        self.prove_with_dummy(
238            executed_transaction,
239            miden_protocol::testing::dummy_precompile_execution_proof(),
240        )
241    }
242
243    fn prove_with_dummy(
244        &self,
245        executed_transaction: miden_protocol::transaction::ExecutedTransaction,
246        proof: ExecutionProof,
247    ) -> Result<ProvenTransaction, TransactionProverError> {
248        let (tx_inputs, tx_outputs, account_patch, _) = executed_transaction.into_parts();
249
250        let (partial_account, ref_block, _, input_notes, _) = tx_inputs.into_parts();
251
252        self.build_proven_transaction(
253            &input_notes,
254            tx_outputs,
255            account_patch,
256            partial_account,
257            ref_block.block_num(),
258            ref_block.commitment(),
259            proof,
260        )
261    }
262}
263
264// TESTS
265// ================================================================================================
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270
271    #[test]
272    fn default_execution_options_are_used_unless_replaced() {
273        let prover = LocalTransactionProver::default();
274        assert_eq!(prover.execution_options(), ExecutionOptions::default());
275
276        let custom_options = ExecutionOptions::default().with_max_advice_size_bytes(1);
277        let prover = prover.with_execution_options(custom_options);
278        assert_eq!(prover.execution_options(), custom_options);
279    }
280}