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