miden_tx/executor/
mod.rs

1use alloc::{collections::BTreeSet, sync::Arc, vec::Vec};
2
3use miden_lib::transaction::TransactionKernel;
4use miden_objects::{
5    Felt, MAX_TX_EXECUTION_CYCLES, MIN_TX_EXECUTION_CYCLES,
6    account::AccountId,
7    assembly::SourceManager,
8    block::{BlockHeader, BlockNumber},
9    note::{NoteId, NoteScript},
10    transaction::{
11        AccountInputs, ExecutedTransaction, InputNote, InputNotes, TransactionArgs,
12        TransactionInputs, TransactionScript,
13    },
14    vm::{AdviceMap, StackOutputs},
15};
16use vm_processor::{AdviceInputs, MemAdviceProvider, Process, RecAdviceProvider};
17pub use vm_processor::{ExecutionOptions, MastForestStore};
18use winter_maybe_async::{maybe_async, maybe_await};
19
20use super::{TransactionExecutorError, TransactionHost};
21use crate::{auth::TransactionAuthenticator, host::ScriptMastForestStore};
22
23mod data_store;
24pub use data_store::DataStore;
25
26mod notes_checker;
27pub use notes_checker::{NoteConsumptionChecker, NoteInputsCheck};
28
29// TRANSACTION EXECUTOR
30// ================================================================================================
31
32/// The transaction executor is responsible for executing Miden blockchain transactions.
33///
34/// Transaction execution consists of the following steps:
35/// - Fetch the data required to execute a transaction from the [DataStore].
36/// - Execute the transaction program and create an [ExecutedTransaction].
37///
38/// The transaction executor uses dynamic dispatch with trait objects for the [DataStore] and
39/// [TransactionAuthenticator], allowing it to be used with different backend implementations.
40/// At the moment of execution, the [DataStore] is expected to provide all required MAST nodes.
41pub struct TransactionExecutor<'store, 'auth> {
42    data_store: &'store dyn DataStore,
43    authenticator: Option<&'auth dyn TransactionAuthenticator>,
44    exec_options: ExecutionOptions,
45}
46
47impl<'store, 'auth> TransactionExecutor<'store, 'auth> {
48    // CONSTRUCTOR
49    // --------------------------------------------------------------------------------------------
50
51    /// Creates a new [TransactionExecutor] instance with the specified [DataStore] and
52    /// [TransactionAuthenticator].
53    pub fn new(
54        data_store: &'store dyn DataStore,
55        authenticator: Option<&'auth dyn TransactionAuthenticator>,
56    ) -> Self {
57        const _: () = assert!(MIN_TX_EXECUTION_CYCLES <= MAX_TX_EXECUTION_CYCLES);
58
59        Self {
60            data_store,
61            authenticator,
62            exec_options: ExecutionOptions::new(
63                Some(MAX_TX_EXECUTION_CYCLES),
64                MIN_TX_EXECUTION_CYCLES,
65                false,
66                false,
67            )
68            .expect("Must not fail while max cycles is more than min trace length"),
69        }
70    }
71
72    /// Creates a new [TransactionExecutor] instance with the specified [DataStore],
73    /// [TransactionAuthenticator] and [ExecutionOptions].
74    ///
75    /// The specified cycle values (`max_cycles` and `expected_cycles`) in the [ExecutionOptions]
76    /// must be within the range [`MIN_TX_EXECUTION_CYCLES`] and [`MAX_TX_EXECUTION_CYCLES`].
77    pub fn with_options(
78        data_store: &'store dyn DataStore,
79        authenticator: Option<&'auth dyn TransactionAuthenticator>,
80        exec_options: ExecutionOptions,
81    ) -> Result<Self, TransactionExecutorError> {
82        validate_num_cycles(exec_options.max_cycles())?;
83        validate_num_cycles(exec_options.expected_cycles())?;
84
85        Ok(Self { data_store, authenticator, exec_options })
86    }
87
88    /// Puts the [TransactionExecutor] into debug mode.
89    ///
90    /// When transaction executor is in debug mode, all transaction-related code (note scripts,
91    /// account code) will be compiled and executed in debug mode. This will ensure that all debug
92    /// instructions present in the original source code are executed.
93    pub fn with_debug_mode(mut self) -> Self {
94        self.exec_options = self.exec_options.with_debugging(true);
95        self
96    }
97
98    /// Enables tracing for the created instance of [TransactionExecutor].
99    ///
100    /// When tracing is enabled, the executor will receive tracing events as various stages of the
101    /// transaction kernel complete. This enables collecting basic stats about how long different
102    /// stages of transaction execution take.
103    pub fn with_tracing(mut self) -> Self {
104        self.exec_options = self.exec_options.with_tracing();
105        self
106    }
107
108    // TRANSACTION EXECUTION
109    // --------------------------------------------------------------------------------------------
110
111    /// Prepares and executes a transaction specified by the provided arguments and returns an
112    /// [`ExecutedTransaction`].
113    ///
114    /// The method first fetches the data required to execute the transaction from the [`DataStore`]
115    /// and compile the transaction into an executable program. In particular, it fetches the
116    /// account identified by the account ID from the store as well as `block_ref`, the header of
117    /// the reference block of the transaction and the set of headers from the blocks in which the
118    /// provided `notes` were created. Then, it executes the transaction program and creates an
119    /// [`ExecutedTransaction`].
120    ///
121    /// The `source_manager` is used to map potential errors back to their source code. To get the
122    /// most value out of it, use the source manager from the
123    /// [`Assembler`](miden_objects::assembly::Assembler) that assembled the Miden Assembly code
124    /// that should be debugged, e.g. account components, note scripts or transaction scripts. If
125    /// no error-to-source mapping is desired, a default source manager can be passed, e.g.
126    /// [`DefaultSourceManager::default`](miden_objects::assembly::DefaultSourceManager::default).
127    ///
128    /// # Errors:
129    ///
130    /// Returns an error if:
131    /// - If required data can not be fetched from the [`DataStore`].
132    /// - If the transaction arguments contain foreign account data not anchored in the reference
133    ///   block.
134    /// - If any input notes were created in block numbers higher than the reference block.
135    #[maybe_async]
136    pub fn execute_transaction(
137        &self,
138        account_id: AccountId,
139        block_ref: BlockNumber,
140        notes: InputNotes<InputNote>,
141        tx_args: TransactionArgs,
142        source_manager: Arc<dyn SourceManager>,
143    ) -> Result<ExecutedTransaction, TransactionExecutorError> {
144        let mut ref_blocks = validate_input_notes(&notes, block_ref)?;
145        ref_blocks.insert(block_ref);
146
147        let (account, seed, ref_block, mmr) =
148            maybe_await!(self.data_store.get_transaction_inputs(account_id, ref_blocks))
149                .map_err(TransactionExecutorError::FetchTransactionInputsFailed)?;
150
151        validate_account_inputs(&tx_args, &ref_block)?;
152
153        let tx_inputs = TransactionInputs::new(account, seed, ref_block, mmr, notes)
154            .map_err(TransactionExecutorError::InvalidTransactionInputs)?;
155
156        let (stack_inputs, advice_inputs) =
157            TransactionKernel::prepare_inputs(&tx_inputs, &tx_args, None);
158
159        let advice_recorder = RecAdviceProvider::from(advice_inputs.into_inner());
160
161        let script_mast_store = ScriptMastForestStore::new(
162            tx_args.tx_script(),
163            tx_inputs.input_notes().iter().map(|n| n.note().script()),
164        );
165
166        let mut host = TransactionHost::new(
167            &tx_inputs.account().into(),
168            advice_recorder,
169            self.data_store,
170            script_mast_store,
171            self.authenticator,
172            tx_args.foreign_account_code_commitments(),
173        )
174        .map_err(TransactionExecutorError::TransactionHostCreationFailed)?;
175
176        // Execute the transaction kernel
177        let trace = vm_processor::execute(
178            &TransactionKernel::main(),
179            stack_inputs,
180            &mut host,
181            self.exec_options,
182            source_manager,
183        )
184        .map_err(TransactionExecutorError::TransactionProgramExecutionFailed)?;
185
186        build_executed_transaction(tx_args, tx_inputs, trace.stack_outputs().clone(), host)
187    }
188
189    // SCRIPT EXECUTION
190    // --------------------------------------------------------------------------------------------
191
192    /// Executes an arbitrary script against the given account and returns the stack state at the
193    /// end of execution.
194    ///
195    /// The `source_manager` is used to map potential errors back to their source code. To get the
196    /// most value out of it, use the source manager from the
197    /// [`Assembler`](miden_objects::assembly::Assembler) that assembled the Miden Assembly code
198    /// that should be debugged, e.g. account components, note scripts or transaction scripts. If
199    /// no error-to-source mapping is desired, a default source manager can be passed, e.g.
200    /// [`DefaultSourceManager::default`](miden_objects::assembly::DefaultSourceManager::default).
201    ///
202    /// # Errors:
203    /// Returns an error if:
204    /// - If required data can not be fetched from the [DataStore].
205    /// - If the transaction host can not be created from the provided values.
206    /// - If the execution of the provided program fails.
207    #[maybe_async]
208    pub fn execute_tx_view_script(
209        &self,
210        account_id: AccountId,
211        block_ref: BlockNumber,
212        tx_script: TransactionScript,
213        advice_inputs: AdviceInputs,
214        foreign_account_inputs: Vec<AccountInputs>,
215        source_manager: Arc<dyn SourceManager>,
216    ) -> Result<[Felt; 16], TransactionExecutorError> {
217        let ref_blocks = [block_ref].into_iter().collect();
218        let (account, seed, ref_block, mmr) =
219            maybe_await!(self.data_store.get_transaction_inputs(account_id, ref_blocks))
220                .map_err(TransactionExecutorError::FetchTransactionInputsFailed)?;
221        let tx_args = TransactionArgs::new(Default::default(), foreign_account_inputs)
222            .with_tx_script(tx_script);
223
224        validate_account_inputs(&tx_args, &ref_block)?;
225
226        let tx_inputs = TransactionInputs::new(account, seed, ref_block, mmr, Default::default())
227            .map_err(TransactionExecutorError::InvalidTransactionInputs)?;
228
229        let (stack_inputs, advice_inputs) =
230            TransactionKernel::prepare_inputs(&tx_inputs, &tx_args, Some(advice_inputs));
231        let advice_recorder = RecAdviceProvider::from(advice_inputs.into_inner());
232
233        let scripts_mast_store =
234            ScriptMastForestStore::new(tx_args.tx_script(), core::iter::empty::<&NoteScript>());
235
236        let mut host = TransactionHost::new(
237            &tx_inputs.account().into(),
238            advice_recorder,
239            self.data_store,
240            scripts_mast_store,
241            self.authenticator,
242            tx_args.foreign_account_code_commitments(),
243        )
244        .map_err(TransactionExecutorError::TransactionHostCreationFailed)?;
245
246        let mut process = Process::new(
247            TransactionKernel::tx_script_main().kernel().clone(),
248            stack_inputs,
249            self.exec_options,
250        )
251        .with_source_manager(source_manager);
252        let stack_outputs = process
253            .execute(&TransactionKernel::tx_script_main(), &mut host)
254            .map_err(TransactionExecutorError::TransactionProgramExecutionFailed)?;
255
256        Ok(*stack_outputs)
257    }
258
259    // CHECK CONSUMABILITY
260    // ============================================================================================
261
262    /// Executes the transaction with specified notes, returning the [NoteAccountExecution::Success]
263    /// if all notes has been consumed successfully and [NoteAccountExecution::Failure] if some note
264    /// returned an error.
265    ///
266    /// The `source_manager` is used to map potential errors back to their source code. To get the
267    /// most value out of it, use the source manager from the
268    /// [`Assembler`](miden_objects::assembly::Assembler) that assembled the Miden Assembly code
269    /// that should be debugged, e.g. account components, note scripts or transaction scripts. If
270    /// no error-to-source mapping is desired, a default source manager can be passed, e.g.
271    /// [`DefaultSourceManager::default`](miden_objects::assembly::DefaultSourceManager::default).
272    ///
273    /// # Errors:
274    /// Returns an error if:
275    /// - If required data can not be fetched from the [DataStore].
276    /// - If the transaction host can not be created from the provided values.
277    /// - If the execution of the provided program fails on the stage other than note execution.
278    #[maybe_async]
279    pub(crate) fn try_execute_notes(
280        &self,
281        account_id: AccountId,
282        block_ref: BlockNumber,
283        notes: InputNotes<InputNote>,
284        tx_args: TransactionArgs,
285        source_manager: Arc<dyn SourceManager>,
286    ) -> Result<NoteAccountExecution, TransactionExecutorError> {
287        let mut ref_blocks = validate_input_notes(&notes, block_ref)?;
288        ref_blocks.insert(block_ref);
289
290        let (account, seed, ref_block, mmr) =
291            maybe_await!(self.data_store.get_transaction_inputs(account_id, ref_blocks))
292                .map_err(TransactionExecutorError::FetchTransactionInputsFailed)?;
293
294        validate_account_inputs(&tx_args, &ref_block)?;
295
296        let tx_inputs = TransactionInputs::new(account, seed, ref_block, mmr, notes)
297            .map_err(TransactionExecutorError::InvalidTransactionInputs)?;
298
299        let (stack_inputs, advice_inputs) =
300            TransactionKernel::prepare_inputs(&tx_inputs, &tx_args, None);
301
302        let advice_provider = MemAdviceProvider::from(advice_inputs.into_inner());
303
304        let scripts_mast_store = ScriptMastForestStore::new(
305            tx_args.tx_script(),
306            tx_inputs.input_notes().iter().map(|n| n.note().script()),
307        );
308
309        let mut host = TransactionHost::new(
310            &tx_inputs.account().into(),
311            advice_provider,
312            self.data_store,
313            scripts_mast_store,
314            self.authenticator,
315            tx_args.foreign_account_code_commitments(),
316        )
317        .map_err(TransactionExecutorError::TransactionHostCreationFailed)?;
318
319        // execute the transaction kernel
320        let result = vm_processor::execute(
321            &TransactionKernel::main(),
322            stack_inputs,
323            &mut host,
324            self.exec_options,
325            source_manager,
326        )
327        .map_err(TransactionExecutorError::TransactionProgramExecutionFailed);
328
329        match result {
330            Ok(_) => Ok(NoteAccountExecution::Success),
331            Err(tx_execution_error) => {
332                let notes = host.tx_progress().note_execution();
333
334                // empty notes vector means that we didn't process the notes, so an error
335                // occurred somewhere else
336                if notes.is_empty() {
337                    return Err(tx_execution_error);
338                }
339
340                let ((last_note, last_note_interval), success_notes) = notes
341                    .split_last()
342                    .expect("notes vector should not be empty because we just checked");
343
344                // if the interval end of the last note is specified, then an error occurred after
345                // notes processing
346                if last_note_interval.end().is_some() {
347                    return Err(tx_execution_error);
348                }
349
350                Ok(NoteAccountExecution::Failure {
351                    failed_note_id: *last_note,
352                    successful_notes: success_notes.iter().map(|(note, _)| *note).collect(),
353                    error: Some(tx_execution_error),
354                })
355            },
356        }
357    }
358}
359
360// HELPER FUNCTIONS
361// ================================================================================================
362
363/// Creates a new [ExecutedTransaction] from the provided data.
364fn build_executed_transaction(
365    tx_args: TransactionArgs,
366    tx_inputs: TransactionInputs,
367    stack_outputs: StackOutputs,
368    host: TransactionHost<RecAdviceProvider>,
369) -> Result<ExecutedTransaction, TransactionExecutorError> {
370    let (advice_recorder, account_delta, output_notes, generated_signatures, tx_progress) =
371        host.into_parts();
372
373    let (mut advice_witness, _, map, _store) = advice_recorder.finalize();
374
375    let advice_map = AdviceMap::from(map);
376    let tx_outputs =
377        TransactionKernel::from_transaction_parts(&stack_outputs, &advice_map, output_notes)
378            .map_err(TransactionExecutorError::TransactionOutputConstructionFailed)?;
379
380    let initial_account = tx_inputs.account();
381    let final_account = &tx_outputs.account;
382
383    let host_delta_commitment = account_delta.commitment();
384    if tx_outputs.account_delta_commitment != host_delta_commitment {
385        return Err(TransactionExecutorError::InconsistentAccountDeltaCommitment {
386            in_kernel_commitment: tx_outputs.account_delta_commitment,
387            host_commitment: host_delta_commitment,
388        });
389    }
390
391    if initial_account.id() != final_account.id() {
392        return Err(TransactionExecutorError::InconsistentAccountId {
393            input_id: initial_account.id(),
394            output_id: final_account.id(),
395        });
396    }
397
398    // make sure nonce delta was computed correctly
399    let nonce_delta = final_account.nonce() - initial_account.nonce();
400    if nonce_delta != account_delta.nonce_delta() {
401        return Err(TransactionExecutorError::InconsistentAccountNonceDelta {
402            expected: nonce_delta,
403            actual: account_delta.nonce_delta(),
404        });
405    }
406
407    // introduce generated signatures into the witness inputs
408    advice_witness.extend_map(generated_signatures);
409
410    Ok(ExecutedTransaction::new(
411        tx_inputs,
412        tx_outputs,
413        account_delta,
414        tx_args,
415        advice_witness,
416        tx_progress.into(),
417    ))
418}
419
420/// Validates the account inputs against the reference block header.
421fn validate_account_inputs(
422    tx_args: &TransactionArgs,
423    ref_block: &BlockHeader,
424) -> Result<(), TransactionExecutorError> {
425    // Validate that foreign account inputs are anchored in the reference block
426    for foreign_account in tx_args.foreign_account_inputs() {
427        let computed_account_root = foreign_account.compute_account_root().map_err(|err| {
428            TransactionExecutorError::InvalidAccountWitness(foreign_account.id(), err)
429        })?;
430        if computed_account_root != ref_block.account_root() {
431            return Err(TransactionExecutorError::ForeignAccountNotAnchoredInReference(
432                foreign_account.id(),
433            ));
434        }
435    }
436    Ok(())
437}
438
439/// Validates that input notes were not created after the reference block.
440///
441/// Returns the set of block numbers required to execute the provided notes.
442fn validate_input_notes(
443    notes: &InputNotes<InputNote>,
444    block_ref: BlockNumber,
445) -> Result<BTreeSet<BlockNumber>, TransactionExecutorError> {
446    // Validate that notes were not created after the reference, and build the set of required
447    // block numbers
448    let mut ref_blocks: BTreeSet<BlockNumber> = BTreeSet::new();
449    for note in notes.iter() {
450        if let Some(location) = note.location() {
451            if location.block_num() > block_ref {
452                return Err(TransactionExecutorError::NoteBlockPastReferenceBlock(
453                    note.id(),
454                    block_ref,
455                ));
456            }
457            ref_blocks.insert(location.block_num());
458        }
459    }
460
461    Ok(ref_blocks)
462}
463
464/// Validates that the number of cycles specified is within the allowed range.
465fn validate_num_cycles(num_cycles: u32) -> Result<(), TransactionExecutorError> {
466    if !(MIN_TX_EXECUTION_CYCLES..=MAX_TX_EXECUTION_CYCLES).contains(&num_cycles) {
467        Err(TransactionExecutorError::InvalidExecutionOptionsCycles {
468            min_cycles: MIN_TX_EXECUTION_CYCLES,
469            max_cycles: MAX_TX_EXECUTION_CYCLES,
470            actual: num_cycles,
471        })
472    } else {
473        Ok(())
474    }
475}
476
477// HELPER ENUM
478// ================================================================================================
479
480/// Describes whether a transaction with a specified set of notes could be executed against target
481/// account.
482///
483/// [NoteAccountExecution::Failure] holds data for error handling: `failing_note_id` is an ID of a
484/// failing note and `successful_notes` is a vector of note IDs which were successfully executed.
485#[derive(Debug)]
486pub enum NoteAccountExecution {
487    Success,
488    Failure {
489        failed_note_id: NoteId,
490        successful_notes: Vec<NoteId>,
491        error: Option<TransactionExecutorError>,
492    },
493}