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, AssetVaultKey};
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::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};
43
44mod program_executor;
45pub use program_executor::ProgramExecutor;
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                false,
100                false,
101            )
102            .expect("Must not fail while max cycles is more than min trace length"),
103            _executor: PhantomData,
104        }
105    }
106}
107
108impl<'store, 'auth, STORE, AUTH, EXEC> TransactionExecutor<'store, 'auth, STORE, AUTH, EXEC>
109where
110    STORE: DataStore + 'store + Sync,
111    AUTH: TransactionAuthenticator + 'auth + Sync,
112    EXEC: ProgramExecutor,
113{
114    /// Replaces the transaction program executor with a different implementation.
115    ///
116    /// This allows plugging in alternative execution engines while preserving the rest of the
117    /// transaction executor configuration.
118    pub fn with_program_executor<EXEC2: ProgramExecutor>(
119        self,
120    ) -> TransactionExecutor<'store, 'auth, STORE, AUTH, EXEC2> {
121        TransactionExecutor::<'store, 'auth, STORE, AUTH, EXEC2> {
122            data_store: self.data_store,
123            authenticator: self.authenticator,
124            source_manager: self.source_manager,
125            exec_options: self.exec_options,
126            _executor: PhantomData,
127        }
128    }
129
130    /// Adds the specified [TransactionAuthenticator] to the executor and returns the resulting
131    /// executor.
132    ///
133    /// This will overwrite any previously set authenticator.
134    #[must_use]
135    pub fn with_authenticator(mut self, authenticator: &'auth AUTH) -> Self {
136        self.authenticator = Some(authenticator);
137        self
138    }
139
140    /// Adds the specified source manager to the executor and returns the resulting executor.
141    ///
142    /// The `source_manager` is used to map potential errors back to their source code. To get the
143    /// most value out of it, use the same source manager as was used with the
144    /// [`Assembler`](miden_protocol::assembly::Assembler) that assembled the Miden Assembly code
145    /// that should be debugged, e.g. account components, note scripts or transaction scripts.
146    ///
147    /// This will overwrite any previously set source manager.
148    #[must_use]
149    pub fn with_source_manager(mut self, source_manager: Arc<dyn SourceManagerSync>) -> Self {
150        self.source_manager = source_manager;
151        self
152    }
153
154    /// Sets the [ExecutionOptions] for the executor to the provided options and returns the
155    /// resulting executor.
156    ///
157    /// # Errors
158    /// Returns an error if the specified cycle values (`max_cycles` and `expected_cycles`) in
159    /// the [ExecutionOptions] are not within the range [`MIN_TX_EXECUTION_CYCLES`] and
160    /// [`MAX_TX_EXECUTION_CYCLES`].
161    pub fn with_options(
162        mut self,
163        exec_options: ExecutionOptions,
164    ) -> Result<Self, TransactionExecutorError> {
165        validate_num_cycles(exec_options.max_cycles())?;
166        validate_num_cycles(exec_options.expected_cycles())?;
167
168        self.exec_options = exec_options;
169        Ok(self)
170    }
171
172    /// Puts the [TransactionExecutor] into debug mode and returns the resulting executor.
173    ///
174    /// When transaction executor is in debug mode, all transaction-related code (note scripts,
175    /// account code) will be compiled and executed in debug mode. This will ensure that all debug
176    /// instructions present in the original source code are executed.
177    #[must_use]
178    pub fn with_debug_mode(mut self) -> Self {
179        self.exec_options = self.exec_options.with_debugging(true);
180        self
181    }
182
183    /// Enables tracing for the created instance of [TransactionExecutor] and returns the resulting
184    /// executor.
185    ///
186    /// When tracing is enabled, the executor will receive tracing events as various stages of the
187    /// transaction kernel complete. This enables collecting basic stats about how long different
188    /// stages of transaction execution take.
189    #[must_use]
190    pub fn with_tracing(mut self) -> Self {
191        self.exec_options = self.exec_options.with_tracing(true);
192        self
193    }
194
195    // TRANSACTION EXECUTION
196    // --------------------------------------------------------------------------------------------
197
198    /// Prepares and executes a transaction specified by the provided arguments and returns an
199    /// [`ExecutedTransaction`].
200    ///
201    /// The method first fetches the data required to execute the transaction from the [`DataStore`]
202    /// and compile the transaction into an executable program. In particular, it fetches the
203    /// account identified by the account ID from the store as well as `block_ref`, the header of
204    /// the reference block of the transaction and the set of headers from the blocks in which the
205    /// provided `notes` were created. Then, it executes the transaction program and creates an
206    /// [`ExecutedTransaction`].
207    ///
208    /// # Errors:
209    ///
210    /// Returns an error if:
211    /// - If required data can not be fetched from the [`DataStore`].
212    /// - If the transaction arguments contain foreign account data not anchored in the reference
213    ///   block.
214    /// - If any input notes were created in block numbers higher than the reference block.
215    pub async fn execute_transaction(
216        &self,
217        account_id: AccountId,
218        block_ref: BlockNumber,
219        notes: InputNotes<InputNote>,
220        tx_args: TransactionArgs,
221    ) -> Result<ExecutedTransaction, TransactionExecutorError> {
222        let tx_inputs = self.prepare_tx_inputs(account_id, block_ref, notes, tx_args).await?;
223
224        let (mut host, stack_inputs, advice_inputs) = self.prepare_transaction(&tx_inputs).await?;
225
226        // instantiate the processor in debug mode only when debug mode is specified via execution
227        // options; this is important because in debug mode execution is almost 100x slower
228        let processor = EXEC::new(stack_inputs, advice_inputs, self.exec_options);
229
230        let output = processor
231            .execute(&TransactionKernel::main(), &mut host)
232            .await
233            .map_err(map_execution_error)?;
234        let stack_outputs = output.stack;
235        let advice_provider = output.advice;
236
237        // The stack is not necessary since it is being reconstructed when re-executing.
238        let (_stack, advice_map, merkle_store, _pc_requests) = advice_provider.into_parts();
239        let advice_inputs = AdviceInputs {
240            map: advice_map,
241            store: merkle_store,
242            ..Default::default()
243        };
244
245        build_executed_transaction(advice_inputs, tx_inputs, stack_outputs, host)
246    }
247
248    // SCRIPT EXECUTION
249    // --------------------------------------------------------------------------------------------
250
251    /// Executes an arbitrary script against the given account and returns the stack state at the
252    /// end of execution.
253    ///
254    /// # Errors:
255    /// Returns an error if:
256    /// - If required data can not be fetched from the [DataStore].
257    /// - If the transaction host can not be created from the provided values.
258    /// - If the execution of the provided program fails.
259    pub async fn execute_tx_view_script(
260        &self,
261        account_id: AccountId,
262        block_ref: BlockNumber,
263        tx_script: TransactionScript,
264        advice_inputs: AdviceInputs,
265    ) -> Result<[Felt; 16], TransactionExecutorError> {
266        let mut tx_args = TransactionArgs::default().with_tx_script(tx_script);
267        tx_args.extend_advice_inputs(advice_inputs);
268
269        let notes = InputNotes::default();
270        let tx_inputs = self.prepare_tx_inputs(account_id, block_ref, notes, tx_args).await?;
271
272        let (mut host, stack_inputs, advice_inputs) = self.prepare_transaction(&tx_inputs).await?;
273
274        let processor = EXEC::new(stack_inputs, advice_inputs, self.exec_options);
275        let output = processor
276            .execute(&TransactionKernel::tx_script_main(), &mut host)
277            .await
278            .map_err(TransactionExecutorError::TransactionProgramExecutionFailed)?;
279        let stack_outputs = output.stack;
280
281        Ok(*stack_outputs)
282    }
283
284    // HELPER METHODS
285    // --------------------------------------------------------------------------------------------
286
287    // Validates input notes and account inputs after retrieving transaction inputs from the store.
288    //
289    // This method has a one-to-many call relationship with the `prepare_transaction` method. This
290    // method needs to be called only once in order to allow many transactions to be prepared based
291    // on the transaction inputs returned by this method.
292    async fn prepare_tx_inputs(
293        &self,
294        account_id: AccountId,
295        block_ref: BlockNumber,
296        input_notes: InputNotes<InputNote>,
297        tx_args: TransactionArgs,
298    ) -> Result<TransactionInputs, TransactionExecutorError> {
299        let (mut asset_vault_keys, mut ref_blocks) = validate_input_notes(&input_notes, block_ref)?;
300        ref_blocks.insert(block_ref);
301
302        let (account, block_header, blockchain) = self
303            .data_store
304            .get_transaction_inputs(account_id, ref_blocks)
305            .await
306            .map_err(TransactionExecutorError::FetchTransactionInputsFailed)?;
307
308        let native_account_vault_root = account.vault().root();
309        let fee_asset_vault_key =
310            AssetVaultKey::new_fungible(block_header.fee_parameters().native_asset_id())
311                .expect("fee asset should be a fungible asset");
312
313        let mut tx_inputs = TransactionInputs::new(account, block_header, blockchain, input_notes)
314            .map_err(TransactionExecutorError::InvalidTransactionInputs)?
315            .with_tx_args(tx_args);
316
317        // Add the vault key for the fee asset to the list of asset vault keys which will need to be
318        // accessed at the end of the transaction.
319        asset_vault_keys.insert(fee_asset_vault_key);
320
321        // filter out any asset vault keys for which we already have witnesses in the advice inputs
322        asset_vault_keys.retain(|asset_key| {
323            !tx_inputs.has_vault_asset_witness(native_account_vault_root, asset_key)
324        });
325
326        // if any of the witnesses are missing, fetch them from the data store and add to tx_inputs
327        if !asset_vault_keys.is_empty() {
328            let asset_witnesses = self
329                .data_store
330                .get_vault_asset_witnesses(account_id, native_account_vault_root, asset_vault_keys)
331                .await
332                .map_err(TransactionExecutorError::FetchAssetWitnessFailed)?;
333
334            tx_inputs = tx_inputs.with_asset_witnesses(asset_witnesses);
335        }
336
337        Ok(tx_inputs)
338    }
339
340    /// Prepares the data needed for transaction execution.
341    ///
342    /// Preparation includes loading transaction inputs from the data store, validating them, and
343    /// instantiating a transaction host.
344    async fn prepare_transaction(
345        &self,
346        tx_inputs: &TransactionInputs,
347    ) -> Result<
348        (TransactionExecutorHost<'store, 'auth, STORE, AUTH>, StackInputs, AdviceInputs),
349        TransactionExecutorError,
350    > {
351        let (stack_inputs, tx_advice_inputs) = TransactionKernel::prepare_inputs(tx_inputs);
352        let input_notes = tx_inputs.input_notes();
353
354        let script_mast_store = ScriptMastForestStore::new(
355            tx_inputs.tx_script(),
356            input_notes.iter().map(|n| n.note().script()),
357        );
358
359        // To start executing the transaction, the procedure index map only needs to contain the
360        // native account's procedures. Foreign accounts are inserted into the map on first access.
361        let account_procedure_index_map =
362            AccountProcedureIndexMap::new([tx_inputs.account().code()]);
363
364        let initial_fee_asset_balance = {
365            let vault_root = tx_inputs.account().vault().root();
366            let native_asset_id = tx_inputs.block_header().fee_parameters().native_asset_id();
367            let fee_asset_vault_key = AssetVaultKey::new_fungible(native_asset_id)
368                .expect("fee asset should be a fungible asset");
369
370            let fee_asset = tx_inputs
371                .read_vault_asset(vault_root, fee_asset_vault_key)
372                .map_err(TransactionExecutorError::FeeAssetRetrievalFailed)?;
373            match fee_asset {
374                Some(Asset::Fungible(fee_asset)) => fee_asset.amount(),
375                Some(Asset::NonFungible(_)) => {
376                    return Err(TransactionExecutorError::FeeAssetMustBeFungible);
377                },
378                // If the asset was not found, its balance is zero.
379                None => 0,
380            }
381        };
382        let host = TransactionExecutorHost::new(
383            tx_inputs.account(),
384            input_notes.clone(),
385            self.data_store,
386            script_mast_store,
387            account_procedure_index_map,
388            self.authenticator,
389            tx_inputs.block_header().block_num(),
390            initial_fee_asset_balance,
391            self.source_manager.clone(),
392        );
393
394        let advice_inputs = tx_advice_inputs.into_advice_inputs();
395
396        Ok((host, stack_inputs, advice_inputs))
397    }
398}
399
400// HELPER FUNCTIONS
401// ================================================================================================
402
403/// Creates a new [ExecutedTransaction] from the provided data.
404fn build_executed_transaction<STORE: DataStore + Sync, AUTH: TransactionAuthenticator + Sync>(
405    mut advice_inputs: AdviceInputs,
406    tx_inputs: TransactionInputs,
407    stack_outputs: StackOutputs,
408    host: TransactionExecutorHost<STORE, AUTH>,
409) -> Result<ExecutedTransaction, TransactionExecutorError> {
410    // Note that the account delta does not contain the removed transaction fee, so it is the
411    // "pre-fee" delta of the transaction.
412
413    let (
414        pre_fee_account_delta,
415        _input_notes,
416        output_notes,
417        accessed_foreign_account_code,
418        generated_signatures,
419        tx_progress,
420        foreign_account_slot_names,
421    ) = host.into_parts();
422
423    let tx_outputs =
424        TransactionKernel::from_transaction_parts(&stack_outputs, &advice_inputs, output_notes)
425            .map_err(TransactionExecutorError::TransactionOutputConstructionFailed)?;
426
427    let pre_fee_delta_commitment = pre_fee_account_delta.to_commitment();
428    if tx_outputs.account_delta_commitment() != pre_fee_delta_commitment {
429        return Err(TransactionExecutorError::InconsistentAccountDeltaCommitment {
430            in_kernel_commitment: tx_outputs.account_delta_commitment(),
431            host_commitment: pre_fee_delta_commitment,
432        });
433    }
434
435    // The full transaction delta is the pre fee delta with the fee asset removed.
436    let mut post_fee_account_delta = pre_fee_account_delta;
437    post_fee_account_delta
438        .vault_mut()
439        .remove_asset(Asset::from(tx_outputs.fee()))
440        .map_err(TransactionExecutorError::RemoveFeeAssetFromDelta)?;
441
442    let initial_account = tx_inputs.account();
443    let final_account = tx_outputs.account();
444
445    if initial_account.id() != final_account.id() {
446        return Err(TransactionExecutorError::InconsistentAccountId {
447            input_id: initial_account.id(),
448            output_id: final_account.id(),
449        });
450    }
451
452    // Make sure nonce delta was computed correctly.
453    let nonce_delta = final_account.nonce() - initial_account.nonce();
454    if nonce_delta != post_fee_account_delta.nonce_delta() {
455        return Err(TransactionExecutorError::InconsistentAccountNonceDelta {
456            expected: nonce_delta,
457            actual: post_fee_account_delta.nonce_delta(),
458        });
459    }
460
461    // Introduce generated signatures into the witness inputs.
462    advice_inputs.map.extend(generated_signatures);
463
464    // Overwrite advice inputs from after the execution on the transaction inputs. This is
465    // guaranteed to be a superset of the original advice inputs.
466    let tx_inputs = tx_inputs
467        .with_foreign_account_code(accessed_foreign_account_code)
468        .with_foreign_account_slot_names(foreign_account_slot_names)
469        .with_advice_inputs(advice_inputs);
470
471    Ok(ExecutedTransaction::new(
472        tx_inputs,
473        tx_outputs,
474        post_fee_account_delta,
475        tx_progress.into(),
476    ))
477}
478
479/// Validates that input notes were not created after the reference block.
480///
481/// Returns the set of block numbers required to execute the provided notes and the set of asset
482/// vault keys that will be needed in the transaction prologue.
483///
484/// The transaction input vault is a copy of the account vault and to mutate the input vault (during
485/// the prologue, for asset preservation), witnesses for the note assets against the account vault
486/// must be requested.
487fn validate_input_notes(
488    notes: &InputNotes<InputNote>,
489    block_ref: BlockNumber,
490) -> Result<(BTreeSet<AssetVaultKey>, BTreeSet<BlockNumber>), TransactionExecutorError> {
491    let mut ref_blocks: BTreeSet<BlockNumber> = BTreeSet::new();
492    let mut asset_vault_keys: BTreeSet<AssetVaultKey> = BTreeSet::new();
493
494    for input_note in notes.iter() {
495        // Validate that notes were not created after the reference, and build the set of required
496        // block numbers
497        if let Some(location) = input_note.location() {
498            if location.block_num() > block_ref {
499                return Err(TransactionExecutorError::NoteBlockPastReferenceBlock(
500                    input_note.id(),
501                    block_ref,
502                ));
503            }
504            ref_blocks.insert(location.block_num());
505        }
506
507        asset_vault_keys.extend(input_note.note().assets().iter().map(Asset::vault_key));
508    }
509
510    Ok((asset_vault_keys, ref_blocks))
511}
512
513/// Validates that the number of cycles specified is within the allowed range.
514fn validate_num_cycles(num_cycles: u32) -> Result<(), TransactionExecutorError> {
515    if !(MIN_TX_EXECUTION_CYCLES..=MAX_TX_EXECUTION_CYCLES).contains(&num_cycles) {
516        Err(TransactionExecutorError::InvalidExecutionOptionsCycles {
517            min_cycles: MIN_TX_EXECUTION_CYCLES,
518            max_cycles: MAX_TX_EXECUTION_CYCLES,
519            actual: num_cycles,
520        })
521    } else {
522        Ok(())
523    }
524}
525
526/// Remaps an execution error to a transaction executor error.
527///
528/// - If the inner error is [`TransactionKernelError::Unauthorized`], it is remapped to
529///   [`TransactionExecutorError::Unauthorized`].
530/// - Otherwise, the execution error is wrapped in
531///   [`TransactionExecutorError::TransactionProgramExecutionFailed`].
532fn map_execution_error(exec_err: ExecutionError) -> TransactionExecutorError {
533    match exec_err {
534        ExecutionError::EventError { ref error, .. } => {
535            match error.downcast_ref::<TransactionKernelError>() {
536                Some(TransactionKernelError::Unauthorized(summary)) => {
537                    TransactionExecutorError::Unauthorized(summary.clone())
538                },
539                Some(TransactionKernelError::InsufficientFee { account_balance, tx_fee }) => {
540                    TransactionExecutorError::InsufficientFee {
541                        account_balance: *account_balance,
542                        tx_fee: *tx_fee,
543                    }
544                },
545                Some(TransactionKernelError::MissingAuthenticator) => {
546                    TransactionExecutorError::MissingAuthenticator
547                },
548                _ => TransactionExecutorError::TransactionProgramExecutionFailed(exec_err),
549            }
550        },
551        _ => TransactionExecutorError::TransactionProgramExecutionFailed(exec_err),
552    }
553}