Skip to main content

miden_tx/executor/
mod.rs

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