miden_tx/executor/
mod.rs

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