Skip to main content

miden_tx/executor/
notes_checker.rs

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