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