Skip to main content

miden_tx/executor/
mod.rs

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