miden_tx/executor/
mod.rs

1use alloc::{collections::BTreeSet, sync::Arc, vec::Vec};
2
3use miden_lib::transaction::TransactionKernel;
4use miden_objects::{
5    Felt, MAX_TX_EXECUTION_CYCLES, MIN_TX_EXECUTION_CYCLES, ZERO,
6    account::AccountId,
7    assembly::SourceManager,
8    block::{BlockHeader, BlockNumber},
9    note::NoteId,
10    transaction::{
11        AccountInputs, ExecutedTransaction, InputNote, InputNotes, TransactionArgs,
12        TransactionInputs, TransactionScript,
13    },
14    vm::StackOutputs,
15};
16pub use vm_processor::MastForestStore;
17use vm_processor::{AdviceInputs, ExecutionOptions, MemAdviceProvider, Process, RecAdviceProvider};
18use winter_maybe_async::{maybe_async, maybe_await};
19
20use super::{TransactionExecutorError, TransactionHost};
21use crate::auth::TransactionAuthenticator;
22
23mod data_store;
24pub use data_store::DataStore;
25
26mod notes_checker;
27pub use notes_checker::{NoteConsumptionChecker, NoteInputsCheck};
28
29// TRANSACTION EXECUTOR
30// ================================================================================================
31
32/// The transaction executor is responsible for executing Miden rollup transactions.
33///
34/// Transaction execution consists of the following steps:
35/// - Fetch the data required to execute a transaction from the [DataStore].
36/// - Execute the transaction program and create an [ExecutedTransaction].
37///
38/// The transaction executor uses dynamic dispatch with trait objects for the [DataStore] and
39/// [TransactionAuthenticator], allowing it to be used with different backend implementations.
40/// At the moment of execution, the [DataStore] is expected to provide all required MAST nodes.
41pub struct TransactionExecutor {
42    data_store: Arc<dyn DataStore>,
43    authenticator: Option<Arc<dyn TransactionAuthenticator>>,
44    exec_options: ExecutionOptions,
45}
46
47impl TransactionExecutor {
48    // CONSTRUCTOR
49    // --------------------------------------------------------------------------------------------
50
51    /// Creates a new [TransactionExecutor] instance with the specified [DataStore] and
52    /// [TransactionAuthenticator].
53    pub fn new(
54        data_store: Arc<dyn DataStore>,
55        authenticator: Option<Arc<dyn TransactionAuthenticator>>,
56    ) -> Self {
57        const _: () = assert!(MIN_TX_EXECUTION_CYCLES <= MAX_TX_EXECUTION_CYCLES);
58
59        Self {
60            data_store,
61            authenticator,
62            exec_options: ExecutionOptions::new(
63                Some(MAX_TX_EXECUTION_CYCLES),
64                MIN_TX_EXECUTION_CYCLES,
65                false,
66                false,
67            )
68            .expect("Must not fail while max cycles is more than min trace length"),
69        }
70    }
71
72    /// Puts the [TransactionExecutor] into debug mode.
73    ///
74    /// When transaction executor is in debug mode, all transaction-related code (note scripts,
75    /// account code) will be compiled and executed in debug mode. This will ensure that all debug
76    /// instructions present in the original source code are executed.
77    pub fn with_debug_mode(mut self) -> Self {
78        self.exec_options = self.exec_options.with_debugging(true);
79        self
80    }
81
82    /// Enables tracing for the created instance of [TransactionExecutor].
83    ///
84    /// When tracing is enabled, the executor will receive tracing events as various stages of the
85    /// transaction kernel complete. This enables collecting basic stats about how long different
86    /// stages of transaction execution take.
87    pub fn with_tracing(mut self) -> Self {
88        self.exec_options = self.exec_options.with_tracing();
89        self
90    }
91
92    // TRANSACTION EXECUTION
93    // --------------------------------------------------------------------------------------------
94
95    /// Prepares and executes a transaction specified by the provided arguments and returns an
96    /// [`ExecutedTransaction`].
97    ///
98    /// The method first fetches the data required to execute the transaction from the [`DataStore`]
99    /// and compile the transaction into an executable program. In particular, it fetches the
100    /// account identified by the account ID from the store as well as `block_ref`, the header of
101    /// the reference block of the transaction and the set of headers from the blocks in which the
102    /// provided `notes` were created. Then, it executes the transaction program and creates an
103    /// [`ExecutedTransaction`].
104    ///
105    /// The `source_manager` is used to map potential errors back to their source code. To get the
106    /// most value out of it, use the source manager from the
107    /// [`Assembler`](miden_objects::assembly::Assembler) that assembled the Miden Assembly code
108    /// that should be debugged, e.g. account components, note scripts or transaction scripts. If
109    /// no error-to-source mapping is desired, a default source manager can be passed, e.g.
110    /// [`DefaultSourceManager::default`](miden_objects::assembly::DefaultSourceManager::default).
111    ///
112    /// # Errors:
113    ///
114    /// Returns an error if:
115    /// - If required data can not be fetched from the [`DataStore`].
116    /// - If the transaction arguments contain foreign account data not anchored in the reference
117    ///   block.
118    /// - If any input notes were created in block numbers higher than the reference block.
119    #[maybe_async]
120    pub fn execute_transaction(
121        &self,
122        account_id: AccountId,
123        block_ref: BlockNumber,
124        notes: InputNotes<InputNote>,
125        tx_args: TransactionArgs,
126        source_manager: Arc<dyn SourceManager>,
127    ) -> Result<ExecutedTransaction, TransactionExecutorError> {
128        let mut ref_blocks = validate_input_notes(&notes, block_ref)?;
129        ref_blocks.insert(block_ref);
130
131        let (account, seed, ref_block, mmr) =
132            maybe_await!(self.data_store.get_transaction_inputs(account_id, ref_blocks))
133                .map_err(TransactionExecutorError::FetchTransactionInputsFailed)?;
134
135        validate_account_inputs(&tx_args, &ref_block)?;
136
137        let tx_inputs = TransactionInputs::new(account, seed, ref_block, mmr, notes)
138            .map_err(TransactionExecutorError::InvalidTransactionInputs)?;
139
140        let (stack_inputs, advice_inputs) =
141            TransactionKernel::prepare_inputs(&tx_inputs, &tx_args, None)
142                .map_err(TransactionExecutorError::InvalidTransactionInputs)?;
143
144        let advice_recorder: RecAdviceProvider = advice_inputs.into();
145
146        let mut host = TransactionHost::new(
147            tx_inputs.account().into(),
148            advice_recorder,
149            self.data_store.clone(),
150            self.authenticator.clone(),
151            tx_args.foreign_account_code_commitments(),
152        )
153        .map_err(TransactionExecutorError::TransactionHostCreationFailed)?;
154
155        // Execute the transaction kernel
156        let result = vm_processor::execute(
157            &TransactionKernel::main(),
158            stack_inputs,
159            &mut host,
160            self.exec_options,
161            source_manager,
162        )
163        .map_err(TransactionExecutorError::TransactionProgramExecutionFailed)?;
164
165        build_executed_transaction(tx_args, tx_inputs, result.stack_outputs().clone(), host)
166    }
167
168    // SCRIPT EXECUTION
169    // --------------------------------------------------------------------------------------------
170
171    /// Executes an arbitrary script against the given account and returns the stack state at the
172    /// end of execution.
173    ///
174    /// The `source_manager` is used to map potential errors back to their source code. To get the
175    /// most value out of it, use the source manager from the
176    /// [`Assembler`](miden_objects::assembly::Assembler) that assembled the Miden Assembly code
177    /// that should be debugged, e.g. account components, note scripts or transaction scripts. If
178    /// no error-to-source mapping is desired, a default source manager can be passed, e.g.
179    /// [`DefaultSourceManager::default`](miden_objects::assembly::DefaultSourceManager::default).
180    ///
181    /// # Errors:
182    /// Returns an error if:
183    /// - If required data can not be fetched from the [DataStore].
184    /// - If the transaction host can not be created from the provided values.
185    /// - If the execution of the provided program fails.
186    #[maybe_async]
187    pub fn execute_tx_view_script(
188        &self,
189        account_id: AccountId,
190        block_ref: BlockNumber,
191        tx_script: TransactionScript,
192        advice_inputs: AdviceInputs,
193        foreign_account_inputs: Vec<AccountInputs>,
194        source_manager: Arc<dyn SourceManager>,
195    ) -> Result<[Felt; 16], TransactionExecutorError> {
196        let ref_blocks = [block_ref].into_iter().collect();
197        let (account, seed, ref_block, mmr) =
198            maybe_await!(self.data_store.get_transaction_inputs(account_id, ref_blocks))
199                .map_err(TransactionExecutorError::FetchTransactionInputsFailed)?;
200        let tx_args = TransactionArgs::new(
201            Some(tx_script.clone()),
202            None,
203            Default::default(),
204            foreign_account_inputs,
205        );
206
207        validate_account_inputs(&tx_args, &ref_block)?;
208
209        let tx_inputs = TransactionInputs::new(account, seed, ref_block, mmr, Default::default())
210            .map_err(TransactionExecutorError::InvalidTransactionInputs)?;
211
212        let (stack_inputs, advice_inputs) =
213            TransactionKernel::prepare_inputs(&tx_inputs, &tx_args, Some(advice_inputs))
214                .map_err(TransactionExecutorError::InvalidTransactionInputs)?;
215        let advice_recorder: RecAdviceProvider = advice_inputs.into();
216
217        let mut host = TransactionHost::new(
218            tx_inputs.account().into(),
219            advice_recorder,
220            self.data_store.clone(),
221            self.authenticator.clone(),
222            tx_args.foreign_account_code_commitments(),
223        )
224        .map_err(TransactionExecutorError::TransactionHostCreationFailed)?;
225
226        let mut process = Process::new(
227            TransactionKernel::tx_script_main().kernel().clone(),
228            stack_inputs,
229            self.exec_options,
230        )
231        .with_source_manager(source_manager);
232        let stack_outputs = process
233            .execute(&TransactionKernel::tx_script_main(), &mut host)
234            .map_err(TransactionExecutorError::TransactionProgramExecutionFailed)?;
235
236        Ok(*stack_outputs)
237    }
238
239    // CHECK CONSUMABILITY
240    // ============================================================================================
241
242    /// Executes the transaction with specified notes, returning the [NoteAccountExecution::Success]
243    /// if all notes has been consumed successfully and [NoteAccountExecution::Failure] if some note
244    /// returned an error.
245    ///
246    /// The `source_manager` is used to map potential errors back to their source code. To get the
247    /// most value out of it, use the source manager from the
248    /// [`Assembler`](miden_objects::assembly::Assembler) that assembled the Miden Assembly code
249    /// that should be debugged, e.g. account components, note scripts or transaction scripts. If
250    /// no error-to-source mapping is desired, a default source manager can be passed, e.g.
251    /// [`DefaultSourceManager::default`](miden_objects::assembly::DefaultSourceManager::default).
252    ///
253    /// # Errors:
254    /// Returns an error if:
255    /// - If required data can not be fetched from the [DataStore].
256    /// - If the transaction host can not be created from the provided values.
257    /// - If the execution of the provided program fails on the stage other than note execution.
258    #[maybe_async]
259    pub(crate) fn try_execute_notes(
260        &self,
261        account_id: AccountId,
262        block_ref: BlockNumber,
263        notes: InputNotes<InputNote>,
264        tx_args: TransactionArgs,
265        source_manager: Arc<dyn SourceManager>,
266    ) -> Result<NoteAccountExecution, TransactionExecutorError> {
267        let mut ref_blocks = validate_input_notes(&notes, block_ref)?;
268        ref_blocks.insert(block_ref);
269
270        let (account, seed, ref_block, mmr) =
271            maybe_await!(self.data_store.get_transaction_inputs(account_id, ref_blocks))
272                .map_err(TransactionExecutorError::FetchTransactionInputsFailed)?;
273
274        validate_account_inputs(&tx_args, &ref_block)?;
275
276        let tx_inputs = TransactionInputs::new(account, seed, ref_block, mmr, notes)
277            .map_err(TransactionExecutorError::InvalidTransactionInputs)?;
278
279        let (stack_inputs, advice_inputs) =
280            TransactionKernel::prepare_inputs(&tx_inputs, &tx_args, None)
281                .map_err(TransactionExecutorError::InvalidTransactionInputs)?;
282
283        let advice_provider: MemAdviceProvider = advice_inputs.into();
284
285        let mut host = TransactionHost::new(
286            tx_inputs.account().into(),
287            advice_provider,
288            self.data_store.clone(),
289            self.authenticator.clone(),
290            tx_args.foreign_account_code_commitments(),
291        )
292        .map_err(TransactionExecutorError::TransactionHostCreationFailed)?;
293
294        // execute the transaction kernel
295        let result = vm_processor::execute(
296            &TransactionKernel::main(),
297            stack_inputs,
298            &mut host,
299            self.exec_options,
300            source_manager,
301        )
302        .map_err(TransactionExecutorError::TransactionProgramExecutionFailed);
303
304        match result {
305            Ok(_) => Ok(NoteAccountExecution::Success),
306            Err(tx_execution_error) => {
307                let notes = host.tx_progress().note_execution();
308
309                // empty notes vector means that we didn't process the notes, so an error
310                // occurred somewhere else
311                if notes.is_empty() {
312                    return Err(tx_execution_error);
313                }
314
315                let ((last_note, last_note_interval), success_notes) = notes
316                    .split_last()
317                    .expect("notes vector should not be empty because we just checked");
318
319                // if the interval end of the last note is specified, then an error occurred after
320                // notes processing
321                if last_note_interval.end().is_some() {
322                    return Err(tx_execution_error);
323                }
324
325                Ok(NoteAccountExecution::Failure {
326                    failed_note_id: *last_note,
327                    successful_notes: success_notes.iter().map(|(note, _)| *note).collect(),
328                    error: Some(tx_execution_error),
329                })
330            },
331        }
332    }
333}
334
335// HELPER FUNCTIONS
336// ================================================================================================
337
338/// Creates a new [ExecutedTransaction] from the provided data.
339fn build_executed_transaction(
340    tx_args: TransactionArgs,
341    tx_inputs: TransactionInputs,
342    stack_outputs: StackOutputs,
343    host: TransactionHost<RecAdviceProvider>,
344) -> Result<ExecutedTransaction, TransactionExecutorError> {
345    let (advice_recorder, account_delta, output_notes, generated_signatures, tx_progress) =
346        host.into_parts();
347
348    let (mut advice_witness, _, map, _store) = advice_recorder.finalize();
349
350    let tx_outputs =
351        TransactionKernel::from_transaction_parts(&stack_outputs, &map.into(), output_notes)
352            .map_err(TransactionExecutorError::TransactionOutputConstructionFailed)?;
353
354    let final_account = &tx_outputs.account;
355
356    let initial_account = tx_inputs.account();
357
358    if initial_account.id() != final_account.id() {
359        return Err(TransactionExecutorError::InconsistentAccountId {
360            input_id: initial_account.id(),
361            output_id: final_account.id(),
362        });
363    }
364
365    // make sure nonce delta was computed correctly
366    let nonce_delta = final_account.nonce() - initial_account.nonce();
367    if nonce_delta == ZERO {
368        if account_delta.nonce().is_some() {
369            return Err(TransactionExecutorError::InconsistentAccountNonceDelta {
370                expected: None,
371                actual: account_delta.nonce(),
372            });
373        }
374    } else if final_account.nonce() != account_delta.nonce().unwrap_or_default() {
375        return Err(TransactionExecutorError::InconsistentAccountNonceDelta {
376            expected: Some(final_account.nonce()),
377            actual: account_delta.nonce(),
378        });
379    }
380
381    // introduce generated signatures into the witness inputs
382    advice_witness.extend_map(generated_signatures);
383
384    Ok(ExecutedTransaction::new(
385        tx_inputs,
386        tx_outputs,
387        account_delta,
388        tx_args,
389        advice_witness,
390        tx_progress.into(),
391    ))
392}
393
394/// Validates the account inputs against the reference block header.
395fn validate_account_inputs(
396    tx_args: &TransactionArgs,
397    ref_block: &BlockHeader,
398) -> Result<(), TransactionExecutorError> {
399    // Validate that foreign account inputs are anchored in the reference block
400    for foreign_account in tx_args.foreign_account_inputs() {
401        let computed_account_root = foreign_account.compute_account_root().map_err(|err| {
402            TransactionExecutorError::InvalidAccountWitness(foreign_account.id(), err)
403        })?;
404        if computed_account_root != ref_block.account_root() {
405            return Err(TransactionExecutorError::ForeignAccountNotAnchoredInReference(
406                foreign_account.id(),
407            ));
408        }
409    }
410    Ok(())
411}
412
413/// Validates that input notes were not created after the reference block.
414///
415/// Returns the set of block numbers required to execute the provided notes.
416fn validate_input_notes(
417    notes: &InputNotes<InputNote>,
418    block_ref: BlockNumber,
419) -> Result<BTreeSet<BlockNumber>, TransactionExecutorError> {
420    // Validate that notes were not created after the reference, and build the set of required
421    // block numbers
422    let mut ref_blocks: BTreeSet<BlockNumber> = BTreeSet::new();
423    for note in notes.iter() {
424        if let Some(location) = note.location() {
425            if location.block_num() > block_ref {
426                return Err(TransactionExecutorError::NoteBlockPastReferenceBlock(
427                    note.id(),
428                    block_ref,
429                ));
430            }
431            ref_blocks.insert(location.block_num());
432        }
433    }
434
435    Ok(ref_blocks)
436}
437
438// HELPER ENUM
439// ================================================================================================
440
441/// Describes whether a transaction with a specified set of notes could be executed against target
442/// account.
443///
444/// [NoteAccountExecution::Failure] holds data for error handling: `failing_note_id` is an ID of a
445/// failing note and `successful_notes` is a vector of note IDs which were successfully executed.
446#[derive(Debug)]
447pub enum NoteAccountExecution {
448    Success,
449    Failure {
450        failed_note_id: NoteId,
451        successful_notes: Vec<NoteId>,
452        error: Option<TransactionExecutorError>,
453    },
454}