Skip to main content

miden_tx/executor/
notes_checker.rs

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