Skip to main content

miden_tx/executor/
mod.rs

1use alloc::boxed::Box;
2use alloc::collections::BTreeSet;
3use alloc::sync::Arc;
4use core::marker::PhantomData;
5
6use miden_processor::advice::AdviceInputs;
7use miden_processor::{ExecutionError, FastProcessor, StackInputs};
8pub use miden_processor::{ExecutionOptions, MastForestStore, ProgramExecutor};
9use miden_protocol::account::AccountId;
10use miden_protocol::assembly::DefaultSourceManager;
11use miden_protocol::assembly::debuginfo::SourceManagerSync;
12use miden_protocol::asset::{Asset, AssetId};
13use miden_protocol::block::BlockNumber;
14use miden_protocol::transaction::{
15    ExecutedTransaction,
16    InputNote,
17    InputNotes,
18    TransactionArgs,
19    TransactionInputs,
20    TransactionKernel,
21    TransactionScript,
22};
23use miden_protocol::vm::StackOutputs;
24use miden_protocol::{Felt, MAX_TX_EXECUTION_CYCLES, MIN_TX_EXECUTION_CYCLES};
25
26use super::TransactionExecutorError;
27use crate::auth::TransactionAuthenticator;
28use crate::errors::TransactionKernelError;
29use crate::host::{AccountProcedureIndexMap, ScriptMastForestStore};
30
31mod exec_host;
32pub use exec_host::TransactionExecutorHost;
33
34mod data_store;
35pub use data_store::DataStore;
36
37mod notes_checker;
38pub use notes_checker::{
39    FailedNote,
40    MAX_NUM_CHECKER_NOTES,
41    NoteConsumptionChecker,
42    NoteConsumptionInfo,
43    NoteFailure,
44    SuccessfulNote,
45};
46
47// TRANSACTION EXECUTOR
48// ================================================================================================
49
50/// The transaction executor is responsible for executing Miden blockchain transactions.
51///
52/// Transaction execution consists of the following steps:
53/// - Fetch the data required to execute a transaction from the [DataStore].
54/// - Execute the transaction program and create an [ExecutedTransaction].
55///
56/// The transaction executor uses dynamic dispatch with trait objects for the [DataStore] and
57/// [TransactionAuthenticator], allowing it to be used with different backend implementations.
58/// At the moment of execution, the [DataStore] is expected to provide all required MAST nodes.
59pub struct TransactionExecutor<
60    'store,
61    'auth,
62    STORE: 'store,
63    AUTH: 'auth,
64    EXEC: ProgramExecutor = FastProcessor,
65> {
66    data_store: &'store STORE,
67    authenticator: Option<&'auth AUTH>,
68    source_manager: Arc<dyn SourceManagerSync>,
69    exec_options: ExecutionOptions,
70    _executor: PhantomData<EXEC>,
71}
72
73impl<'store, 'auth, STORE, AUTH> TransactionExecutor<'store, 'auth, STORE, AUTH>
74where
75    STORE: DataStore + 'store + Sync,
76    AUTH: TransactionAuthenticator + 'auth + Sync,
77{
78    // CONSTRUCTORS
79    // --------------------------------------------------------------------------------------------
80
81    /// Creates a new [TransactionExecutor] instance with the specified [DataStore].
82    ///
83    /// The created executor will not have the authenticator or source manager set, and tracing and
84    /// debug mode will be turned off.
85    ///
86    /// By default, the executor uses [`FastProcessor`](miden_processor::FastProcessor) for program
87    /// execution. Use [`with_program_executor`](Self::with_program_executor) to plug in a
88    /// different execution engine.
89    pub fn new(data_store: &'store STORE) -> Self {
90        const _: () = assert!(MIN_TX_EXECUTION_CYCLES <= MAX_TX_EXECUTION_CYCLES);
91        Self {
92            data_store,
93            authenticator: None,
94            source_manager: Arc::new(DefaultSourceManager::default()),
95            exec_options: ExecutionOptions::new(
96                Some(MAX_TX_EXECUTION_CYCLES),
97                MIN_TX_EXECUTION_CYCLES,
98                ExecutionOptions::DEFAULT_CORE_TRACE_FRAGMENT_SIZE,
99            )
100            .expect("Must not fail while max cycles is more than min trace length"),
101            _executor: PhantomData,
102        }
103    }
104}
105
106impl<'store, 'auth, STORE, AUTH, EXEC> TransactionExecutor<'store, 'auth, STORE, AUTH, EXEC>
107where
108    STORE: DataStore + 'store + Sync,
109    AUTH: TransactionAuthenticator + 'auth + Sync,
110    EXEC: ProgramExecutor,
111{
112    /// Replaces the transaction program executor with a different implementation.
113    ///
114    /// This allows plugging in alternative execution engines while preserving the rest of the
115    /// transaction executor configuration.
116    pub fn with_program_executor<EXEC2: ProgramExecutor>(
117        self,
118    ) -> TransactionExecutor<'store, 'auth, STORE, AUTH, EXEC2> {
119        TransactionExecutor::<'store, 'auth, STORE, AUTH, EXEC2> {
120            data_store: self.data_store,
121            authenticator: self.authenticator,
122            source_manager: self.source_manager,
123            exec_options: self.exec_options,
124            _executor: PhantomData,
125        }
126    }
127
128    /// Adds the specified [TransactionAuthenticator] to the executor and returns the resulting
129    /// executor.
130    ///
131    /// This will overwrite any previously set authenticator.
132    #[must_use]
133    pub fn with_authenticator(mut self, authenticator: &'auth AUTH) -> Self {
134        self.authenticator = Some(authenticator);
135        self
136    }
137
138    /// Adds the specified source manager to the executor and returns the resulting executor.
139    ///
140    /// The `source_manager` is used to map potential errors back to their source code. To get the
141    /// most value out of it, use the same source manager as was used with the
142    /// [`Assembler`](miden_protocol::assembly::Assembler) that assembled the Miden Assembly code
143    /// that should be debugged, e.g. account components, note scripts or transaction scripts.
144    ///
145    /// This will overwrite any previously set source manager.
146    #[must_use]
147    pub fn with_source_manager(mut self, source_manager: Arc<dyn SourceManagerSync>) -> Self {
148        self.source_manager = source_manager;
149        self
150    }
151
152    /// Sets the [ExecutionOptions] for the executor to the provided options and returns the
153    /// resulting executor.
154    ///
155    /// # Errors
156    /// Returns an error if the specified cycle values (`max_cycles` and `expected_cycles`) in
157    /// the [ExecutionOptions] are not within the range [`MIN_TX_EXECUTION_CYCLES`] and
158    /// [`MAX_TX_EXECUTION_CYCLES`].
159    pub fn with_options(
160        mut self,
161        exec_options: ExecutionOptions,
162    ) -> Result<Self, TransactionExecutorError> {
163        validate_num_cycles(exec_options.max_cycles())?;
164        validate_num_cycles(exec_options.expected_cycles())?;
165
166        self.exec_options = exec_options;
167        Ok(self)
168    }
169
170    // TRANSACTION EXECUTION
171    // --------------------------------------------------------------------------------------------
172
173    /// Prepares and executes a transaction specified by the provided arguments and returns an
174    /// [`ExecutedTransaction`].
175    ///
176    /// The method first fetches the data required to execute the transaction from the [`DataStore`]
177    /// and compile the transaction into an executable program. In particular, it fetches the
178    /// account identified by the account ID from the store as well as `block_ref`, the header of
179    /// the reference block of the transaction and the set of headers from the blocks in which the
180    /// provided `notes` were created. Then, it executes the transaction program and creates an
181    /// [`ExecutedTransaction`].
182    ///
183    /// # Errors:
184    ///
185    /// Returns an error if:
186    /// - If required data can not be fetched from the [`DataStore`].
187    /// - If the transaction arguments contain foreign account data not anchored in the reference
188    ///   block.
189    /// - If any input notes were created in block numbers higher than the reference block.
190    pub async fn execute_transaction(
191        &self,
192        account_id: AccountId,
193        block_ref: BlockNumber,
194        notes: InputNotes<InputNote>,
195        tx_args: TransactionArgs,
196    ) -> Result<ExecutedTransaction, TransactionExecutorError> {
197        let tx_inputs = self.prepare_tx_inputs(account_id, block_ref, notes, tx_args).await?;
198
199        let (mut host, stack_inputs, advice_inputs) = self.prepare_transaction(&tx_inputs).await?;
200
201        // Use the package-debug execution API even when the embedded release kernel has no debug
202        // sections. This enables package-owned debug info for dynamically loaded scripts.
203        let program = TransactionKernel::main();
204        let kernel_debug_info = TransactionKernel::main_debug_info();
205        let processor = EXEC::new(stack_inputs, advice_inputs, self.exec_options)
206            .map_err(ExecutionError::advice_error_no_context)
207            .map_err(map_execution_error)?
208            .with_debug_info(kernel_debug_info.as_deref().cloned().unwrap_or_default())
209            .with_entrypoint_source_node(TransactionKernel::main_entrypoint_source_node());
210        let output = processor.execute(&program, &mut host).await.map_err(map_execution_error)?;
211        let stack_outputs = output.stack;
212        let advice_provider = output.advice;
213
214        // The stack is not necessary since it is being reconstructed when re-executing.
215        let (_stack, advice_map, merkle_store) = advice_provider.into_parts();
216        let advice_inputs = AdviceInputs::from(advice_map).with_merkle_store(merkle_store);
217
218        build_executed_transaction(advice_inputs, tx_inputs, stack_outputs, host)
219    }
220
221    // SCRIPT EXECUTION
222    // --------------------------------------------------------------------------------------------
223
224    /// Executes an arbitrary script against the given account and returns the stack state at the
225    /// end of execution.
226    ///
227    /// # Errors:
228    /// Returns an error if:
229    /// - If required data can not be fetched from the [DataStore].
230    /// - If the transaction host can not be created from the provided values.
231    /// - If the execution of the provided program fails.
232    pub async fn execute_tx_view_script(
233        &self,
234        account_id: AccountId,
235        block_ref: BlockNumber,
236        tx_script: TransactionScript,
237        advice_inputs: AdviceInputs,
238    ) -> Result<[Felt; 16], TransactionExecutorError> {
239        let mut tx_args = TransactionArgs::default().with_tx_script(tx_script);
240        tx_args.extend_advice_inputs(advice_inputs);
241
242        let notes = InputNotes::default();
243        let tx_inputs = self.prepare_tx_inputs(account_id, block_ref, notes, tx_args).await?;
244
245        let (mut host, stack_inputs, advice_inputs) = self.prepare_transaction(&tx_inputs).await?;
246
247        let program = TransactionKernel::tx_script_main();
248        let kernel_debug_info = TransactionKernel::tx_script_main_debug_info();
249        let processor =
250            EXEC::new(stack_inputs, advice_inputs, self.exec_options)
251                .map_err(ExecutionError::advice_error_no_context)
252                .map_err(TransactionExecutorError::TransactionProgramExecutionFailed)?
253                .with_debug_info(kernel_debug_info.as_deref().cloned().unwrap_or_default())
254                .with_entrypoint_source_node(
255                    TransactionKernel::tx_script_main_entrypoint_source_node(),
256                );
257        let output = processor
258            .execute(&program, &mut host)
259            .await
260            .map_err(TransactionExecutorError::TransactionProgramExecutionFailed)?;
261        let stack_outputs = output.stack;
262
263        Ok(*stack_outputs)
264    }
265
266    // HELPER METHODS
267    // --------------------------------------------------------------------------------------------
268
269    // Validates input notes and account inputs after retrieving transaction inputs from the store.
270    //
271    // This method has a one-to-many call relationship with the `prepare_transaction` method. This
272    // method needs to be called only once in order to allow many transactions to be prepared based
273    // on the transaction inputs returned by this method.
274    async fn prepare_tx_inputs(
275        &self,
276        account_id: AccountId,
277        block_ref: BlockNumber,
278        input_notes: InputNotes<InputNote>,
279        tx_args: TransactionArgs,
280    ) -> Result<TransactionInputs, TransactionExecutorError> {
281        let (mut asset_ids, mut ref_blocks) = validate_input_notes(&input_notes, block_ref)?;
282        ref_blocks.insert(block_ref);
283
284        let (account, block_header, protocol_config, blockchain) = self
285            .data_store
286            .get_transaction_inputs(account_id, ref_blocks)
287            .await
288            .map_err(TransactionExecutorError::FetchTransactionInputsFailed)?;
289
290        let native_account_vault_root = account.vault().root();
291
292        let mut tx_inputs =
293            TransactionInputs::new(account, block_header, protocol_config, blockchain, input_notes)
294                .map_err(TransactionExecutorError::InvalidTransactionInputs)?
295                .with_tx_args(tx_args);
296
297        // filter out any asset IDs for which we already have witnesses in the advice inputs
298        asset_ids.retain(|asset_id| {
299            !tx_inputs.has_vault_asset_witness(native_account_vault_root, asset_id)
300        });
301
302        // if any of the witnesses are missing, fetch them from the data store and add to tx_inputs
303        if !asset_ids.is_empty() {
304            let asset_witnesses = self
305                .data_store
306                .get_vault_asset_witnesses(account_id, native_account_vault_root, asset_ids)
307                .await
308                .map_err(TransactionExecutorError::FetchAssetWitnessFailed)?;
309
310            tx_inputs = tx_inputs.with_asset_witnesses(asset_witnesses);
311        }
312
313        Ok(tx_inputs)
314    }
315
316    /// Prepares the data needed for transaction execution.
317    ///
318    /// Preparation includes loading transaction inputs from the data store, validating them, and
319    /// instantiating a transaction host.
320    async fn prepare_transaction(
321        &self,
322        tx_inputs: &TransactionInputs,
323    ) -> Result<
324        (TransactionExecutorHost<'store, 'auth, STORE, AUTH>, StackInputs, AdviceInputs),
325        TransactionExecutorError,
326    > {
327        let (stack_inputs, tx_advice_inputs) = TransactionKernel::prepare_inputs(tx_inputs);
328        let input_notes = tx_inputs.input_notes();
329
330        let script_mast_store = ScriptMastForestStore::new(
331            tx_inputs.tx_script(),
332            input_notes.iter().map(|n| n.note().script()),
333        );
334
335        // To start executing the transaction, the procedure index map only needs to contain the
336        // native account's procedures. Foreign accounts are inserted into the map on first access.
337        let account_procedure_index_map =
338            AccountProcedureIndexMap::new([tx_inputs.account().code()]);
339
340        let host = TransactionExecutorHost::new(
341            tx_inputs.account(),
342            input_notes.clone(),
343            self.data_store,
344            script_mast_store,
345            account_procedure_index_map,
346            self.authenticator,
347            tx_inputs.block_header().block_num(),
348            tx_inputs.collect_block_commitments(),
349            self.source_manager.clone(),
350        )
351        .map_err(|err| TransactionExecutorError::TransactionHostCreationFailed(Box::new(err)))?;
352
353        let advice_inputs = tx_advice_inputs.into_advice_inputs();
354
355        Ok((host, stack_inputs, advice_inputs))
356    }
357}
358
359// HELPER FUNCTIONS
360// ================================================================================================
361
362/// Creates a new [ExecutedTransaction] from the provided data.
363fn build_executed_transaction<STORE: DataStore + Sync, AUTH: TransactionAuthenticator + Sync>(
364    mut advice_inputs: AdviceInputs,
365    tx_inputs: TransactionInputs,
366    stack_outputs: StackOutputs,
367    host: TransactionExecutorHost<STORE, AUTH>,
368) -> Result<ExecutedTransaction, TransactionExecutorError> {
369    let (
370        account_patch,
371        _input_notes,
372        output_notes,
373        accessed_foreign_account_code,
374        generated_signatures,
375        tx_progress,
376        foreign_account_slot_names,
377    ) = host.into_parts();
378
379    let tx_outputs =
380        TransactionKernel::from_transaction_parts(&stack_outputs, &advice_inputs, output_notes)
381            .map_err(TransactionExecutorError::TransactionOutputConstructionFailed)?;
382
383    let patch_commitment = account_patch.to_commitment();
384    if tx_outputs.account_patch_commitment() != patch_commitment {
385        return Err(TransactionExecutorError::InconsistentAccountPatchCommitment {
386            in_kernel_commitment: tx_outputs.account_patch_commitment(),
387            host_commitment: patch_commitment,
388        });
389    }
390
391    let initial_account = tx_inputs.account();
392    let final_account = tx_outputs.account();
393
394    if initial_account.id() != final_account.id() {
395        return Err(TransactionExecutorError::InconsistentAccountId {
396            input_id: initial_account.id(),
397            output_id: final_account.id(),
398        });
399    }
400
401    // Introduce generated signatures into the witness inputs.
402    advice_inputs.extend(AdviceInputs::default().with_map(generated_signatures));
403
404    // Overwrite advice inputs from after the execution on the transaction inputs. This is
405    // guaranteed to be a superset of the original advice inputs.
406    let tx_inputs = tx_inputs
407        .with_foreign_account_code(accessed_foreign_account_code)
408        .with_foreign_account_slot_names(foreign_account_slot_names)
409        .with_advice_inputs(advice_inputs);
410
411    Ok(ExecutedTransaction::new(
412        tx_inputs,
413        tx_outputs,
414        account_patch,
415        tx_progress.into(),
416    ))
417}
418
419/// Validates that input notes were not created after the reference block.
420///
421/// Returns the set of block numbers required to execute the provided notes and the set of asset
422/// asset IDs that will be needed in the transaction prologue.
423///
424/// The transaction input vault is a copy of the account vault and to mutate the input vault (during
425/// the prologue, for asset preservation), witnesses for the note assets against the account vault
426/// must be requested.
427fn validate_input_notes(
428    notes: &InputNotes<InputNote>,
429    block_ref: BlockNumber,
430) -> Result<(BTreeSet<AssetId>, BTreeSet<BlockNumber>), TransactionExecutorError> {
431    let mut ref_blocks: BTreeSet<BlockNumber> = BTreeSet::new();
432    let mut asset_ids: BTreeSet<AssetId> = BTreeSet::new();
433
434    for input_note in notes.iter() {
435        // Validate that notes were not created after the reference, and build the set of required
436        // block numbers
437        if let Some(location) = input_note.location() {
438            if location.block_num() > block_ref {
439                return Err(TransactionExecutorError::NoteBlockPastReferenceBlock(
440                    input_note.id(),
441                    block_ref,
442                ));
443            }
444            ref_blocks.insert(location.block_num());
445        }
446
447        asset_ids.extend(input_note.note().assets().iter().map(Asset::id));
448    }
449
450    Ok((asset_ids, ref_blocks))
451}
452
453/// Validates that the number of cycles specified is within the allowed range.
454fn validate_num_cycles(num_cycles: u32) -> Result<(), TransactionExecutorError> {
455    if !(MIN_TX_EXECUTION_CYCLES..=MAX_TX_EXECUTION_CYCLES).contains(&num_cycles) {
456        Err(TransactionExecutorError::InvalidExecutionOptionsCycles {
457            min_cycles: MIN_TX_EXECUTION_CYCLES,
458            max_cycles: MAX_TX_EXECUTION_CYCLES,
459            actual: num_cycles,
460        })
461    } else {
462        Ok(())
463    }
464}
465
466/// Remaps an execution error to a transaction executor error.
467///
468/// - If the inner error is [`TransactionKernelError::Unauthorized`], it is remapped to
469///   [`TransactionExecutorError::Unauthorized`].
470/// - If the inner error is [`TransactionKernelError::AuthRequestOutsideAuthProcedure`], it is
471///   remapped to [`TransactionExecutorError::AuthRequestOutsideAuthProcedure`].
472/// - If the inner error is
473///   [`TransactionKernelError::PrivilegedEventFromOutsideTransactionKernelContext`], it is remapped
474///   to [`TransactionExecutorError::PrivilegedEventFromOutsideTransactionKernelContext`].
475/// - Otherwise, the execution error is wrapped in
476///   [`TransactionExecutorError::TransactionProgramExecutionFailed`].
477fn map_execution_error(exec_err: ExecutionError) -> TransactionExecutorError {
478    match exec_err {
479        ExecutionError::EventError { ref error, .. } => {
480            match error.downcast_ref::<TransactionKernelError>() {
481                Some(TransactionKernelError::Unauthorized(summary)) => {
482                    TransactionExecutorError::Unauthorized(summary.clone())
483                },
484                Some(TransactionKernelError::MissingAuthenticator) => {
485                    TransactionExecutorError::MissingAuthenticator
486                },
487                Some(TransactionKernelError::AuthRequestOutsideAuthProcedure) => {
488                    TransactionExecutorError::AuthRequestOutsideAuthProcedure
489                },
490                Some(
491                    TransactionKernelError::PrivilegedEventFromOutsideTransactionKernelContext(
492                        event_id,
493                    ),
494                ) => TransactionExecutorError::PrivilegedEventFromOutsideTransactionKernelContext(
495                    event_id.clone(),
496                ),
497                _ => TransactionExecutorError::TransactionProgramExecutionFailed(exec_err),
498            }
499        },
500        _ => TransactionExecutorError::TransactionProgramExecutionFailed(exec_err),
501    }
502}