miden_tx/executor/
mod.rs

1use alloc::collections::BTreeSet;
2use alloc::sync::Arc;
3
4use miden_lib::transaction::TransactionKernel;
5use miden_objects::account::AccountId;
6use miden_objects::assembly::DefaultSourceManager;
7use miden_objects::assembly::debuginfo::SourceManagerSync;
8use miden_objects::asset::Asset;
9use miden_objects::block::BlockNumber;
10use miden_objects::transaction::{
11    ExecutedTransaction,
12    InputNote,
13    InputNotes,
14    TransactionArgs,
15    TransactionInputs,
16    TransactionScript,
17};
18use miden_objects::vm::StackOutputs;
19use miden_objects::{Felt, MAX_TX_EXECUTION_CYCLES, MIN_TX_EXECUTION_CYCLES};
20use miden_processor::fast::FastProcessor;
21use miden_processor::{AdviceInputs, ExecutionError, StackInputs};
22pub use miden_processor::{ExecutionOptions, MastForestStore};
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                false,
84                false,
85            )
86            .expect("Must not fail while max cycles is more than min trace length"),
87        }
88    }
89
90    /// Adds the specified [TransactionAuthenticator] to the executor and returns the resulting
91    /// executor.
92    ///
93    /// This will overwrite any previously set authenticator.
94    #[must_use]
95    pub fn with_authenticator(mut self, authenticator: &'auth AUTH) -> Self {
96        self.authenticator = Some(authenticator);
97        self
98    }
99
100    /// Adds the specified source manager to the executor and returns the resulting executor.
101    ///
102    /// The `source_manager` is used to map potential errors back to their source code. To get the
103    /// most value out of it, use the same source manager as was used with the
104    /// [`Assembler`](miden_objects::assembly::Assembler) that assembled the Miden Assembly code
105    /// that should be debugged, e.g. account components, note scripts or transaction scripts.
106    ///
107    /// This will overwrite any previously set source manager.
108    #[must_use]
109    pub fn with_source_manager(mut self, source_manager: Arc<dyn SourceManagerSync>) -> Self {
110        self.source_manager = source_manager;
111        self
112    }
113
114    /// Sets the [ExecutionOptions] for the executor to the provided options and returns the
115    /// resulting executor.
116    ///
117    /// # Errors
118    /// Returns an error if the specified cycle values (`max_cycles` and `expected_cycles`) in
119    /// the [ExecutionOptions] are not within the range [`MIN_TX_EXECUTION_CYCLES`] and
120    /// [`MAX_TX_EXECUTION_CYCLES`].
121    pub fn with_options(
122        mut self,
123        exec_options: ExecutionOptions,
124    ) -> Result<Self, TransactionExecutorError> {
125        validate_num_cycles(exec_options.max_cycles())?;
126        validate_num_cycles(exec_options.expected_cycles())?;
127
128        self.exec_options = exec_options;
129        Ok(self)
130    }
131
132    /// Puts the [TransactionExecutor] into debug mode and returns the resulting executor.
133    ///
134    /// When transaction executor is in debug mode, all transaction-related code (note scripts,
135    /// account code) will be compiled and executed in debug mode. This will ensure that all debug
136    /// instructions present in the original source code are executed.
137    #[must_use]
138    pub fn with_debug_mode(mut self) -> Self {
139        self.exec_options = self.exec_options.with_debugging(true);
140        self
141    }
142
143    /// Enables tracing for the created instance of [TransactionExecutor] and returns the resulting
144    /// executor.
145    ///
146    /// When tracing is enabled, the executor will receive tracing events as various stages of the
147    /// transaction kernel complete. This enables collecting basic stats about how long different
148    /// stages of transaction execution take.
149    #[must_use]
150    pub fn with_tracing(mut self) -> Self {
151        self.exec_options = self.exec_options.with_tracing();
152        self
153    }
154
155    // TRANSACTION EXECUTION
156    // --------------------------------------------------------------------------------------------
157
158    /// Prepares and executes a transaction specified by the provided arguments and returns an
159    /// [`ExecutedTransaction`].
160    ///
161    /// The method first fetches the data required to execute the transaction from the [`DataStore`]
162    /// and compile the transaction into an executable program. In particular, it fetches the
163    /// account identified by the account ID from the store as well as `block_ref`, the header of
164    /// the reference block of the transaction and the set of headers from the blocks in which the
165    /// provided `notes` were created. Then, it executes the transaction program and creates an
166    /// [`ExecutedTransaction`].
167    ///
168    /// # Errors:
169    ///
170    /// Returns an error if:
171    /// - If required data can not be fetched from the [`DataStore`].
172    /// - If the transaction arguments contain foreign account data not anchored in the reference
173    ///   block.
174    /// - If any input notes were created in block numbers higher than the reference block.
175    pub async fn execute_transaction(
176        &self,
177        account_id: AccountId,
178        block_ref: BlockNumber,
179        notes: InputNotes<InputNote>,
180        tx_args: TransactionArgs,
181    ) -> Result<ExecutedTransaction, TransactionExecutorError> {
182        let tx_inputs = self.prepare_tx_inputs(account_id, block_ref, notes, tx_args).await?;
183
184        let (mut host, stack_inputs, advice_inputs) = self.prepare_transaction(&tx_inputs).await?;
185
186        let processor = FastProcessor::new_debug(stack_inputs.as_slice(), advice_inputs);
187        let output = processor
188            .execute(&TransactionKernel::main(), &mut host)
189            .await
190            .map_err(map_execution_error)?;
191        let stack_outputs = output.stack;
192        let advice_provider = output.advice;
193
194        // The stack is not necessary since it is being reconstructed when re-executing.
195        let (_stack, advice_map, merkle_store, _pc_requests) = advice_provider.into_parts();
196        let advice_inputs = AdviceInputs {
197            map: advice_map,
198            store: merkle_store,
199            ..Default::default()
200        };
201
202        build_executed_transaction(advice_inputs, tx_inputs, stack_outputs, host)
203    }
204
205    // SCRIPT EXECUTION
206    // --------------------------------------------------------------------------------------------
207
208    /// Executes an arbitrary script against the given account and returns the stack state at the
209    /// end of execution.
210    ///
211    /// # Errors:
212    /// Returns an error if:
213    /// - If required data can not be fetched from the [DataStore].
214    /// - If the transaction host can not be created from the provided values.
215    /// - If the execution of the provided program fails.
216    pub async fn execute_tx_view_script(
217        &self,
218        account_id: AccountId,
219        block_ref: BlockNumber,
220        tx_script: TransactionScript,
221        advice_inputs: AdviceInputs,
222    ) -> Result<[Felt; 16], TransactionExecutorError> {
223        let mut tx_args = TransactionArgs::default().with_tx_script(tx_script);
224        tx_args.extend_advice_inputs(advice_inputs);
225
226        let notes = InputNotes::default();
227        let tx_inputs = self.prepare_tx_inputs(account_id, block_ref, notes, tx_args).await?;
228
229        let (mut host, stack_inputs, advice_inputs) = self.prepare_transaction(&tx_inputs).await?;
230
231        let processor =
232            FastProcessor::new_with_advice_inputs(stack_inputs.as_slice(), advice_inputs);
233        let output = processor
234            .execute(&TransactionKernel::tx_script_main(), &mut host)
235            .await
236            .map_err(TransactionExecutorError::TransactionProgramExecutionFailed)?;
237        let stack_outputs = output.stack;
238
239        Ok(*stack_outputs)
240    }
241
242    // HELPER METHODS
243    // --------------------------------------------------------------------------------------------
244
245    // Validates input notes and account inputs after retrieving transaction inputs from the store.
246    //
247    // This method has a one-to-many call relationship with the `prepare_transaction` method. This
248    // method needs to be called only once in order to allow many transactions to be prepared based
249    // on the transaction inputs returned by this method.
250    async fn prepare_tx_inputs(
251        &self,
252        account_id: AccountId,
253        block_ref: BlockNumber,
254        input_notes: InputNotes<InputNote>,
255        tx_args: TransactionArgs,
256    ) -> Result<TransactionInputs, TransactionExecutorError> {
257        let mut ref_blocks = validate_input_notes(&input_notes, block_ref)?;
258        ref_blocks.insert(block_ref);
259
260        let (account, block_header, blockchain) = self
261            .data_store
262            .get_transaction_inputs(account_id, ref_blocks)
263            .await
264            .map_err(TransactionExecutorError::FetchTransactionInputsFailed)?;
265
266        let tx_inputs = TransactionInputs::new(account, block_header, blockchain, input_notes)
267            .map_err(TransactionExecutorError::InvalidTransactionInputs)?
268            .with_tx_args(tx_args);
269
270        Ok(tx_inputs)
271    }
272
273    /// Prepares the data needed for transaction execution.
274    ///
275    /// Preparation includes loading transaction inputs from the data store, validating them, and
276    /// instantiating a transaction host.
277    async fn prepare_transaction(
278        &self,
279        tx_inputs: &TransactionInputs,
280    ) -> Result<
281        (TransactionExecutorHost<'store, 'auth, STORE, AUTH>, StackInputs, AdviceInputs),
282        TransactionExecutorError,
283    > {
284        let (stack_inputs, tx_advice_inputs) = TransactionKernel::prepare_inputs(tx_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_inputs.tx_script(),
298            input_notes.iter().map(|n| n.note().script()),
299        );
300
301        // To start executing the transaction, the procedure index map only needs to contain the
302        // native account's procedures. Foreign accounts are inserted into the map on first access.
303        let account_procedure_index_map =
304            AccountProcedureIndexMap::new([tx_inputs.account().code()])
305                .map_err(TransactionExecutorError::TransactionHostCreationFailed)?;
306
307        let host = TransactionExecutorHost::new(
308            tx_inputs.account(),
309            input_notes.clone(),
310            self.data_store,
311            script_mast_store,
312            account_procedure_index_map,
313            self.authenticator,
314            tx_inputs.block_header().block_num(),
315            self.source_manager.clone(),
316        );
317
318        let advice_inputs = tx_advice_inputs.into_advice_inputs();
319
320        Ok((host, stack_inputs, advice_inputs))
321    }
322}
323
324// HELPER FUNCTIONS
325// ================================================================================================
326
327/// Creates a new [ExecutedTransaction] from the provided data.
328fn build_executed_transaction<STORE: DataStore + Sync, AUTH: TransactionAuthenticator + Sync>(
329    mut advice_inputs: AdviceInputs,
330    tx_inputs: TransactionInputs,
331    stack_outputs: StackOutputs,
332    host: TransactionExecutorHost<STORE, AUTH>,
333) -> Result<ExecutedTransaction, TransactionExecutorError> {
334    // Note that the account delta does not contain the removed transaction fee, so it is the
335    // "pre-fee" delta of the transaction.
336    let (
337        pre_fee_account_delta,
338        _input_notes,
339        output_notes,
340        accessed_foreign_account_code,
341        generated_signatures,
342        tx_progress,
343    ) = host.into_parts();
344
345    let tx_outputs =
346        TransactionKernel::from_transaction_parts(&stack_outputs, &advice_inputs, output_notes)
347            .map_err(TransactionExecutorError::TransactionOutputConstructionFailed)?;
348
349    let pre_fee_delta_commitment = pre_fee_account_delta.to_commitment();
350    if tx_outputs.account_delta_commitment != pre_fee_delta_commitment {
351        return Err(TransactionExecutorError::InconsistentAccountDeltaCommitment {
352            in_kernel_commitment: tx_outputs.account_delta_commitment,
353            host_commitment: pre_fee_delta_commitment,
354        });
355    }
356
357    // The full transaction delta is the pre fee delta with the fee asset removed.
358    let mut post_fee_account_delta = pre_fee_account_delta;
359    post_fee_account_delta
360        .vault_mut()
361        .remove_asset(Asset::from(tx_outputs.fee))
362        .map_err(TransactionExecutorError::RemoveFeeAssetFromDelta)?;
363
364    let initial_account = tx_inputs.account();
365    let final_account = &tx_outputs.account;
366
367    if initial_account.id() != final_account.id() {
368        return Err(TransactionExecutorError::InconsistentAccountId {
369            input_id: initial_account.id(),
370            output_id: final_account.id(),
371        });
372    }
373
374    // Make sure nonce delta was computed correctly.
375    let nonce_delta = final_account.nonce() - initial_account.nonce();
376    if nonce_delta != post_fee_account_delta.nonce_delta() {
377        return Err(TransactionExecutorError::InconsistentAccountNonceDelta {
378            expected: nonce_delta,
379            actual: post_fee_account_delta.nonce_delta(),
380        });
381    }
382
383    // Introduce generated signatures into the witness inputs.
384    advice_inputs.map.extend(generated_signatures);
385
386    // Overwrite advice inputs from after the execution on the transaction inputs. This is
387    // guaranteed to be a superset of the original advice inputs.
388    let tx_inputs = tx_inputs
389        .with_foreign_account_code(accessed_foreign_account_code)
390        .with_advice_inputs(advice_inputs);
391
392    Ok(ExecutedTransaction::new(
393        tx_inputs,
394        tx_outputs,
395        post_fee_account_delta,
396        tx_progress.into(),
397    ))
398}
399
400/// Validates that input notes were not created after the reference block.
401///
402/// Returns the set of block numbers required to execute the provided notes.
403fn validate_input_notes(
404    notes: &InputNotes<InputNote>,
405    block_ref: BlockNumber,
406) -> Result<BTreeSet<BlockNumber>, TransactionExecutorError> {
407    // Validate that notes were not created after the reference, and build the set of required
408    // block numbers
409    let mut ref_blocks: BTreeSet<BlockNumber> = BTreeSet::new();
410    for note in notes.iter() {
411        if let Some(location) = note.location() {
412            if location.block_num() > block_ref {
413                return Err(TransactionExecutorError::NoteBlockPastReferenceBlock(
414                    note.id(),
415                    block_ref,
416                ));
417            }
418            ref_blocks.insert(location.block_num());
419        }
420    }
421
422    Ok(ref_blocks)
423}
424
425/// Validates that the number of cycles specified is within the allowed range.
426fn validate_num_cycles(num_cycles: u32) -> Result<(), TransactionExecutorError> {
427    if !(MIN_TX_EXECUTION_CYCLES..=MAX_TX_EXECUTION_CYCLES).contains(&num_cycles) {
428        Err(TransactionExecutorError::InvalidExecutionOptionsCycles {
429            min_cycles: MIN_TX_EXECUTION_CYCLES,
430            max_cycles: MAX_TX_EXECUTION_CYCLES,
431            actual: num_cycles,
432        })
433    } else {
434        Ok(())
435    }
436}
437
438/// Remaps an execution error to a transaction executor error.
439///
440/// - If the inner error is [`TransactionKernelError::Unauthorized`], it is remapped to
441///   [`TransactionExecutorError::Unauthorized`].
442/// - Otherwise, the execution error is wrapped in
443///   [`TransactionExecutorError::TransactionProgramExecutionFailed`].
444fn map_execution_error(exec_err: ExecutionError) -> TransactionExecutorError {
445    match exec_err {
446        ExecutionError::EventError { ref error, .. } => {
447            match error.downcast_ref::<TransactionKernelError>() {
448                Some(TransactionKernelError::Unauthorized(summary)) => {
449                    TransactionExecutorError::Unauthorized(summary.clone())
450                },
451                Some(TransactionKernelError::InsufficientFee { account_balance, tx_fee }) => {
452                    TransactionExecutorError::InsufficientFee {
453                        account_balance: *account_balance,
454                        tx_fee: *tx_fee,
455                    }
456                },
457                Some(TransactionKernelError::MissingAuthenticator) => {
458                    TransactionExecutorError::MissingAuthenticator
459                },
460                _ => TransactionExecutorError::TransactionProgramExecutionFailed(exec_err),
461            }
462        },
463        _ => TransactionExecutorError::TransactionProgramExecutionFailed(exec_err),
464    }
465}