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