Skip to main content

miden_tx/executor/
notes_checker.rs

1use alloc::collections::BTreeMap;
2use alloc::vec::Vec;
3
4use miden_processor::ExecutionError;
5use miden_processor::advice::AdviceInputs;
6use miden_protocol::account::AccountId;
7use miden_protocol::block::BlockNumber;
8use miden_protocol::note::Note;
9use miden_protocol::transaction::{
10    InputNote,
11    InputNotes,
12    TransactionArgs,
13    TransactionInputs,
14    TransactionKernel,
15};
16use miden_protocol::vm::PackageDebugInfo;
17use miden_standards::note::{NoteConsumptionStatus, StandardNote};
18
19use super::{ProgramExecutor, TransactionExecutor};
20use crate::auth::TransactionAuthenticator;
21use crate::errors::TransactionCheckerError;
22use crate::executor::map_execution_error;
23use crate::{DataStore, NoteCheckerError, TransactionExecutorError};
24
25// CONSTANTS
26// ================================================================================================
27
28/// Maximum number of notes that can be checked at once.
29///
30/// Fixed at an amount that should keep each run of note consumption checking to a maximum of ~50ms.
31pub const MAX_NUM_CHECKER_NOTES: usize = 20;
32
33// NOTE CONSUMPTION INFO
34// ================================================================================================
35
36/// Represents a successfully consumed note along with the number of cycles it took to execute.
37#[derive(Debug)]
38pub struct SuccessfulNote {
39    note: Note,
40    num_cycles: usize,
41}
42
43impl SuccessfulNote {
44    /// Constructs a new `SuccessfulNote`.
45    pub fn new(note: Note, num_cycles: usize) -> Self {
46        Self { note, num_cycles }
47    }
48
49    /// Returns a reference to the note.
50    pub fn note(&self) -> &Note {
51        &self.note
52    }
53
54    /// Returns the number of cycles consumed during execution.
55    pub fn num_cycles(&self) -> usize {
56        self.num_cycles
57    }
58}
59
60/// Represents a failed note consumption.
61#[derive(Debug)]
62pub struct FailedNote {
63    note: Note,
64    error: TransactionExecutorError,
65    /// The number of cycles consumed by the note before it failed.
66    ///
67    /// This is `Some` when the failure was due to exceeding the cycle limit, and `None`
68    /// for other error types where the cycle count is not meaningful.
69    num_cycles: Option<usize>,
70}
71
72impl FailedNote {
73    /// Constructs a new `FailedNote`.
74    pub fn new(note: Note, error: TransactionExecutorError, num_cycles: Option<usize>) -> Self {
75        Self { note, error, num_cycles }
76    }
77
78    /// Returns a reference to the note.
79    pub fn note(&self) -> &Note {
80        &self.note
81    }
82
83    /// Returns a reference to the error.
84    pub fn error(&self) -> &TransactionExecutorError {
85        &self.error
86    }
87
88    /// Returns the number of cycles consumed before failure, if available.
89    ///
90    /// This is `Some` when the failure was due to exceeding the cycle limit, and `None`
91    /// for other error types where the cycle count is not meaningful.
92    pub fn num_cycles(&self) -> Option<usize> {
93        self.num_cycles
94    }
95}
96
97/// Contains information about the successful and failed consumption of notes.
98#[derive(Default, Debug)]
99pub struct NoteConsumptionInfo {
100    successful: Vec<SuccessfulNote>,
101    failed: Vec<FailedNote>,
102}
103
104impl NoteConsumptionInfo {
105    /// Creates a new [`NoteConsumptionInfo`] instance with the given successful notes.
106    pub fn new_successful(successful: Vec<SuccessfulNote>) -> Self {
107        Self { successful, ..Default::default() }
108    }
109
110    /// Creates a new [`NoteConsumptionInfo`] instance with the given successful and failed notes.
111    pub fn new(successful: Vec<SuccessfulNote>, failed: Vec<FailedNote>) -> Self {
112        Self { successful, failed }
113    }
114
115    /// Returns a reference to the successfully consumed notes.
116    pub fn successful(&self) -> &[SuccessfulNote] {
117        &self.successful
118    }
119
120    /// Returns a reference to the failed notes.
121    pub fn failed(&self) -> &[FailedNote] {
122        &self.failed
123    }
124
125    /// Consumes the struct and returns the successful and failed notes.
126    pub fn into_parts(self) -> (Vec<SuccessfulNote>, Vec<FailedNote>) {
127        (self.successful, self.failed)
128    }
129}
130
131// NOTE CONSUMPTION CHECKER
132// ================================================================================================
133
134/// This struct performs input notes check against provided target account.
135///
136/// The check is performed using the [NoteConsumptionChecker::check_notes_consumability] procedure.
137/// Essentially runs the transaction to make sure that provided input notes could be consumed by the
138/// account.
139pub struct NoteConsumptionChecker<'a, STORE, AUTH, EXEC: ProgramExecutor>(
140    &'a TransactionExecutor<'a, 'a, STORE, AUTH, EXEC>,
141);
142
143impl<'a, STORE, AUTH, EXEC> NoteConsumptionChecker<'a, STORE, AUTH, EXEC>
144where
145    STORE: DataStore + Sync,
146    AUTH: TransactionAuthenticator + Sync,
147    EXEC: ProgramExecutor,
148{
149    /// Creates a new [`NoteConsumptionChecker`] instance with the given transaction executor.
150    pub fn new(tx_executor: &'a TransactionExecutor<'a, 'a, STORE, AUTH, EXEC>) -> Self {
151        NoteConsumptionChecker(tx_executor)
152    }
153
154    /// Checks whether some set of the provided input notes could be consumed by the provided
155    /// account by executing the transaction with varying combination of notes.
156    ///
157    /// This function attempts to find the maximum set of notes that can be successfully executed
158    /// together by the target account.
159    ///
160    /// Because of the runtime complexity involved in this function, a limited range of
161    /// [`MAX_NUM_CHECKER_NOTES`] input notes is allowed.
162    ///
163    /// If some notes succeed and others fail, the failed notes are removed from the candidate set
164    /// and the remaining notes (successful + unattempted) are retried in the next iteration. This
165    /// process continues until either all remaining notes succeed or no notes can be successfully
166    /// executed
167    ///
168    /// For example, given notes A, B, C, D, E, the execution flow would be as follows:
169    /// - Try [A, B, C, D, E] → A, B succeed, C fails → Remove C, try again.
170    /// - Try [A, B, D, E] → A, B, D succeed, E fails → Remove E, try again.
171    /// - Try [A, B, D] → All succeed → Return successful=[A, B, D], failed=[C, E].
172    ///
173    /// If a failure occurs at the epilogue phase of the transaction execution, the relevant set of
174    /// otherwise-successful notes are retried in various combinations in an attempt to find a
175    /// combination that passes the epilogue phase successfully.
176    ///
177    /// Returns a list of successfully consumed notes and a list of failed notes.
178    pub async fn check_notes_consumability(
179        &self,
180        target_account_id: AccountId,
181        block_ref: BlockNumber,
182        mut notes: Vec<Note>,
183        tx_args: TransactionArgs,
184    ) -> Result<NoteConsumptionInfo, NoteCheckerError> {
185        let num_notes = notes.len();
186        if num_notes == 0 || num_notes > MAX_NUM_CHECKER_NOTES {
187            return Err(NoteCheckerError::InputNoteCountOutOfRange(num_notes));
188        }
189        // Ensure standard notes are ordered first.
190        notes.sort_unstable_by_key(|note| {
191            StandardNote::from_script_root(note.script().root()).is_none()
192        });
193
194        let notes = InputNotes::from(notes);
195        let tx_inputs = self
196            .0
197            .prepare_tx_inputs(target_account_id, block_ref, notes, tx_args)
198            .await
199            .map_err(NoteCheckerError::TransactionPreparation)?;
200
201        // Attempt to find an executable set of notes.
202        self.find_executable_notes_by_elimination(tx_inputs).await
203    }
204
205    /// Checks whether the provided input note could be consumed by the provided account by
206    /// executing a transaction at the specified block height.
207    ///
208    /// This function takes into account the possibility that the signatures may not be loaded into
209    /// the transaction context and returns the [`NoteConsumptionStatus`] result accordingly.
210    ///
211    /// This function first applies the static analysis of the provided note, and if it doesn't
212    /// reveal any errors next it tries to execute the transaction. Based on the execution result,
213    /// it either returns a [`NoteCheckerError`] or the [`NoteConsumptionStatus`]: depending on
214    /// whether the execution succeeded, failed in the prologue, during the note execution process
215    /// or in the epilogue.
216    pub async fn can_consume(
217        &self,
218        target_account_id: AccountId,
219        block_ref: BlockNumber,
220        note: InputNote,
221        tx_args: TransactionArgs,
222    ) -> Result<NoteConsumptionStatus, NoteCheckerError> {
223        // Return the consumption status if we manage to determine it from the standard note
224        if let Some(standard_note) = StandardNote::from_script_root(note.note().script().root())
225            && let Some(consumption_status) =
226                standard_note.is_consumable(note.note(), target_account_id, block_ref)
227        {
228            return Ok(consumption_status);
229        }
230
231        // Prepare transaction inputs.
232        let mut tx_inputs = self
233            .0
234            .prepare_tx_inputs(
235                target_account_id,
236                block_ref,
237                InputNotes::new_unchecked(vec![note]),
238                tx_args,
239            )
240            .await
241            .map_err(NoteCheckerError::TransactionPreparation)?;
242
243        // try to consume the provided note
244        match self.try_execute_notes(&mut tx_inputs).await {
245            // execution succeeded
246            Ok(_cycle_counts) => Ok(NoteConsumptionStatus::Consumable),
247            Err(tx_checker_error) => {
248                match tx_checker_error {
249                    // execution failed on the preparation stage, before we actually executed the tx
250                    TransactionCheckerError::TransactionPreparation(e) => {
251                        Err(NoteCheckerError::TransactionPreparation(e))
252                    },
253                    // execution failed during the prologue
254                    TransactionCheckerError::PrologueExecution(e) => {
255                        Err(NoteCheckerError::PrologueExecution(e))
256                    },
257                    // execution failed during the note processing
258                    TransactionCheckerError::NoteExecution { .. } => {
259                        Ok(NoteConsumptionStatus::UnconsumableConditions)
260                    },
261                    // execution failed during the epilogue
262                    TransactionCheckerError::EpilogueExecution {
263                        error: epilogue_error, ..
264                    } => Ok(handle_epilogue_error(epilogue_error)),
265                }
266            },
267        }
268    }
269
270    // HELPER METHODS
271    // --------------------------------------------------------------------------------------------
272
273    /// Finds a set of executable notes and eliminates failed notes from the list in the process.
274    ///
275    /// The result contains some combination of the input notes partitioned by whether they
276    /// succeeded or failed to execute.
277    async fn find_executable_notes_by_elimination(
278        &self,
279        mut tx_inputs: TransactionInputs,
280    ) -> Result<NoteConsumptionInfo, NoteCheckerError> {
281        let mut candidate_notes = tx_inputs
282            .input_notes()
283            .iter()
284            .map(|note| note.clone().into_note())
285            .collect::<Vec<_>>();
286        let mut failed_notes = Vec::new();
287
288        // Attempt to execute notes in a loop. Reduce the set of notes based on failures until
289        // either a set of notes executes without failure or the set of notes cannot be
290        // further reduced.
291        loop {
292            // Execute the candidate notes.
293            tx_inputs.set_input_notes(candidate_notes.clone());
294            match self.try_execute_notes(&mut tx_inputs).await {
295                Ok(cycle_counts) => {
296                    // A full set of successful notes has been found.
297                    let successful = candidate_notes
298                        .into_iter()
299                        .zip(cycle_counts)
300                        .map(|(note, num_cycles)| SuccessfulNote::new(note, num_cycles))
301                        .collect();
302                    return Ok(NoteConsumptionInfo::new(successful, failed_notes));
303                },
304                Err(TransactionCheckerError::NoteExecution {
305                    failed_note_index,
306                    error,
307                    failed_note_cycle_count,
308                    ..
309                }) => {
310                    // SAFETY: Failed note index is in bounds of the candidate notes.
311                    let failed_note = candidate_notes.remove(failed_note_index);
312                    failed_notes.push(FailedNote::new(failed_note, error, failed_note_cycle_count));
313
314                    // All possible candidate combinations have been attempted.
315                    if candidate_notes.is_empty() {
316                        return Ok(NoteConsumptionInfo::new(Vec::new(), failed_notes));
317                    }
318                    // Continue and process the next set of candidates.
319                },
320                Err(TransactionCheckerError::EpilogueExecution { .. }) => {
321                    let consumption_info = self
322                        .find_largest_executable_combination(
323                            candidate_notes,
324                            failed_notes,
325                            tx_inputs,
326                        )
327                        .await;
328                    return Ok(consumption_info);
329                },
330                Err(TransactionCheckerError::PrologueExecution(err)) => {
331                    return Err(NoteCheckerError::PrologueExecution(err));
332                },
333                Err(TransactionCheckerError::TransactionPreparation(err)) => {
334                    return Err(NoteCheckerError::TransactionPreparation(err));
335                },
336            }
337        }
338    }
339
340    /// Attempts to find the largest possible combination of notes that can execute successfully
341    /// together.
342    ///
343    /// This method incrementally tries combinations of increasing size (1 note, 2 notes, 3 notes,
344    /// etc.) and builds upon previously successful combinations to find the maximum executable
345    /// set.
346    async fn find_largest_executable_combination(
347        &self,
348        mut remaining_notes: Vec<Note>,
349        mut failed_notes: Vec<FailedNote>,
350        mut tx_inputs: TransactionInputs,
351    ) -> NoteConsumptionInfo {
352        let mut successful_notes = Vec::new();
353        let mut successful_cycle_counts = Vec::new();
354        let mut failed_note_index = BTreeMap::new();
355
356        // Iterate by note count: try 1 note, then 2, then 3, etc.
357        for size in 1..=remaining_notes.len() {
358            // Can't build a combination of size N without at least N-1 successful notes.
359            if successful_notes.len() < size - 1 {
360                break;
361            }
362
363            // Try adding each remaining note to the current successful combination.
364            for (idx, note) in remaining_notes.iter().enumerate() {
365                successful_notes.push(note.clone());
366
367                tx_inputs.set_input_notes(successful_notes.clone());
368                match self.try_execute_notes(&mut tx_inputs).await {
369                    Ok(cycle_counts) => {
370                        // The successfully added note might have failed earlier. Remove it from the
371                        // failed list.
372                        failed_note_index.remove(&note.id());
373                        // Store the cycle counts from the latest successful execution.
374                        successful_cycle_counts = cycle_counts;
375                        // This combination succeeded; remove the most recently added note from
376                        // the remaining set.
377                        remaining_notes.remove(idx);
378                        break;
379                    },
380                    Err(error) => {
381                        // This combination failed; remove the last note from the test set and
382                        // continue to next note.
383                        let failed_note =
384                            successful_notes.pop().expect("successful notes should not be empty");
385
386                        // Extract the failed note's cycle count if available.
387                        let num_cycles = match &error {
388                            TransactionCheckerError::NoteExecution {
389                                failed_note_cycle_count,
390                                ..
391                            } => *failed_note_cycle_count,
392                            _ => None,
393                        };
394
395                        // Record the failed note (overwrite previous failures for the relevant
396                        // note).
397                        failed_note_index.insert(
398                            failed_note.id(),
399                            FailedNote::new(failed_note, error.into(), num_cycles),
400                        );
401                    },
402                }
403            }
404        }
405
406        // Pair successful notes with their cycle counts from the last successful execution.
407        let successful = successful_notes
408            .into_iter()
409            .zip(successful_cycle_counts)
410            .map(|(note, num_cycles)| SuccessfulNote::new(note, num_cycles))
411            .collect();
412
413        // Append failed notes to the list of failed notes provided as input.
414        failed_notes.extend(failed_note_index.into_values());
415        NoteConsumptionInfo::new(successful, failed_notes)
416    }
417
418    /// Attempts to execute a transaction with the provided input notes.
419    ///
420    /// This method executes the full transaction pipeline including prologue, note execution,
421    /// and epilogue phases. It returns `Ok(cycle_counts)` if all notes are successfully consumed
422    /// (where `cycle_counts` contains the number of cycles for each note), or a specific
423    /// [`TransactionCheckerError`] indicating where and why the execution failed. The order of the
424    /// returned `cycle_counts` is guaranteed to match the order of the input notes.
425    async fn try_execute_notes(
426        &self,
427        tx_inputs: &mut TransactionInputs,
428    ) -> Result<Vec<usize>, TransactionCheckerError> {
429        if tx_inputs.input_notes().is_empty() {
430            return Ok(Vec::new());
431        }
432
433        let (mut host, stack_inputs, advice_inputs) =
434            self.0
435                .prepare_transaction(tx_inputs)
436                .await
437                .map_err(TransactionCheckerError::TransactionPreparation)?;
438
439        let processor = EXEC::new(stack_inputs, advice_inputs, self.0.exec_options);
440        let program = TransactionKernel::main();
441        let kernel_debug_info = TransactionKernel::main_debug_info();
442        let fallback_debug_info = PackageDebugInfo::default();
443        let result = processor
444            .execute_with_package_debug_info(
445                &program,
446                kernel_debug_info.as_deref().unwrap_or(&fallback_debug_info),
447                TransactionKernel::main_entrypoint_source_node(),
448                &mut host,
449            )
450            .await
451            .map_err(map_execution_error);
452
453        match result {
454            Ok(execution_output) => {
455                let cycle_counts = host
456                    .tx_progress()
457                    .note_execution()
458                    .iter()
459                    .map(|(_, interval)| interval.len())
460                    .collect();
461
462                // Set the advice inputs from the successful execution as advice inputs for
463                // reexecution. This avoids calls to the data store (to load data lazily) that have
464                // already been done as part of this execution.
465                let (_, advice_map, merkle_store, _) = execution_output.advice.into_parts();
466                let advice_inputs = AdviceInputs {
467                    map: advice_map,
468                    store: merkle_store,
469                    ..Default::default()
470                };
471                tx_inputs.set_advice_inputs(advice_inputs);
472                Ok(cycle_counts)
473            },
474            Err(error) => {
475                let notes = host.tx_progress().note_execution();
476
477                // Empty notes vector means that we didn't process the notes, so an error
478                // occurred.
479                if notes.is_empty() {
480                    return Err(TransactionCheckerError::PrologueExecution(error));
481                }
482
483                let ((_, last_note_interval), success_notes) =
484                    notes.split_last().expect("notes vector is not empty because of earlier check");
485
486                // If the interval end of the last note is specified, then an error occurred after
487                // notes processing. All notes executed successfully in this case.
488                if last_note_interval.end().is_some() {
489                    let successful_notes_cycle_counts =
490                        notes.iter().map(|(_, interval)| interval.len()).collect();
491                    Err(TransactionCheckerError::EpilogueExecution {
492                        error,
493                        successful_notes_cycle_counts,
494                    })
495                } else {
496                    // Return the index of the failed note.
497                    let failed_note_index = success_notes.len();
498                    let successful_notes_cycle_counts =
499                        success_notes.iter().map(|(_, interval)| interval.len()).collect();
500
501                    // Compute the failed note's cycle count when the failure was due to
502                    // exceeding the cycle limit. In this case, the note's interval has a
503                    // start but no end, and the total cycles consumed equals the max allowed.
504                    let failed_note_cycle_count = match &error {
505                        TransactionExecutorError::TransactionProgramExecutionFailed(
506                            ExecutionError::CycleLimitExceeded(max_cycles),
507                        ) => last_note_interval
508                            .start()
509                            .map(|start| *max_cycles as usize - usize::from(start)),
510                        _ => None,
511                    };
512
513                    Err(TransactionCheckerError::NoteExecution {
514                        failed_note_index,
515                        error,
516                        successful_notes_cycle_counts,
517                        failed_note_cycle_count,
518                    })
519                }
520            },
521        }
522    }
523}
524
525// HELPER FUNCTIONS
526// ================================================================================================
527
528/// Handle the epilogue error during the note consumption check in the `can_consume` method.
529///
530/// The goal of this helper function is to handle the cases where the account couldn't consume the
531/// note because of some epilogue check failure, e.g. absence of the authenticator.
532fn handle_epilogue_error(epilogue_error: TransactionExecutorError) -> NoteConsumptionStatus {
533    match epilogue_error {
534        // `Unauthorized` is returned for the multisig accounts if the transaction doesn't have
535        // enough signatures.
536        TransactionExecutorError::Unauthorized(_)
537        // `MissingAuthenticator` is returned for the account with the basic auth if the
538        // authenticator was not provided to the executor (UnreachableAuth).
539        | TransactionExecutorError::MissingAuthenticator => {
540            // Both these cases signal that there is a probability that the provided note could be
541            // consumed if the authentication is provided.
542            NoteConsumptionStatus::ConsumableWithAuthorization
543        },
544        // TODO: apply additional checks to get the verbose error reason
545        _ => NoteConsumptionStatus::UnconsumableConditions,
546    }
547}