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, WellKnownNote};
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 well-known notes are ordered first.
124        notes.sort_unstable_by_key(|note| WellKnownNote::from_note(note).is_none());
125
126        let notes = InputNotes::from(notes);
127        let tx_inputs = self
128            .0
129            .prepare_tx_inputs(target_account_id, block_ref, notes, tx_args)
130            .await
131            .map_err(NoteCheckerError::TransactionPreparation)?;
132
133        // Attempt to find an executable set of notes.
134        self.find_executable_notes_by_elimination(tx_inputs).await
135    }
136
137    /// Checks whether the provided input note could be consumed by the provided account by
138    /// executing a transaction at the specified block height.
139    ///
140    /// This function takes into account the possibility that the signatures may not be loaded into
141    /// the transaction context and returns the [`NoteConsumptionStatus`] result accordingly.
142    ///
143    /// This function first applies the static analysis of the provided note, and if it doesn't
144    /// reveal any errors next it tries to execute the transaction. Based on the execution result,
145    /// it either returns a [`NoteCheckerError`] or the [`NoteConsumptionStatus`]: depending on
146    /// whether the execution succeeded, failed in the prologue, during the note execution process
147    /// or in the epilogue.
148    pub async fn can_consume(
149        &self,
150        target_account_id: AccountId,
151        block_ref: BlockNumber,
152        note: InputNote,
153        tx_args: TransactionArgs,
154    ) -> Result<NoteConsumptionStatus, NoteCheckerError> {
155        // return the consumption status if we manage to determine it from the well-known note
156        if let Some(well_known_note) = WellKnownNote::from_note(note.note())
157            && let Some(consumption_status) =
158                well_known_note.is_consumable(note.note(), target_account_id, block_ref)
159        {
160            return Ok(consumption_status);
161        }
162
163        // Prepare transaction inputs.
164        let mut tx_inputs = self
165            .0
166            .prepare_tx_inputs(
167                target_account_id,
168                block_ref,
169                InputNotes::new_unchecked(vec![note]),
170                tx_args,
171            )
172            .await
173            .map_err(NoteCheckerError::TransactionPreparation)?;
174
175        // try to consume the provided note
176        match self.try_execute_notes(&mut tx_inputs).await {
177            // execution succeeded
178            Ok(()) => Ok(NoteConsumptionStatus::Consumable),
179            Err(tx_checker_error) => {
180                match tx_checker_error {
181                    // execution failed on the preparation stage, before we actually executed the tx
182                    TransactionCheckerError::TransactionPreparation(e) => {
183                        Err(NoteCheckerError::TransactionPreparation(e))
184                    },
185                    // execution failed during the prologue
186                    TransactionCheckerError::PrologueExecution(e) => {
187                        Err(NoteCheckerError::PrologueExecution(e))
188                    },
189                    // execution failed during the note processing
190                    TransactionCheckerError::NoteExecution { .. } => {
191                        Ok(NoteConsumptionStatus::UnconsumableConditions)
192                    },
193                    // execution failed during the epilogue
194                    TransactionCheckerError::EpilogueExecution(epilogue_error) => {
195                        Ok(handle_epilogue_error(epilogue_error))
196                    },
197                }
198            },
199        }
200    }
201
202    // HELPER METHODS
203    // --------------------------------------------------------------------------------------------
204
205    /// Finds a set of executable notes and eliminates failed notes from the list in the process.
206    ///
207    /// The result contains some combination of the input notes partitioned by whether they
208    /// succeeded or failed to execute.
209    async fn find_executable_notes_by_elimination(
210        &self,
211        mut tx_inputs: TransactionInputs,
212    ) -> Result<NoteConsumptionInfo, NoteCheckerError> {
213        let mut candidate_notes = tx_inputs
214            .input_notes()
215            .iter()
216            .map(|note| note.clone().into_note())
217            .collect::<Vec<_>>();
218        let mut failed_notes = Vec::new();
219
220        // Attempt to execute notes in a loop. Reduce the set of notes based on failures until
221        // either a set of notes executes without failure or the set of notes cannot be
222        // further reduced.
223        loop {
224            // Execute the candidate notes.
225            tx_inputs.set_input_notes(candidate_notes.clone());
226            match self.try_execute_notes(&mut tx_inputs).await {
227                Ok(()) => {
228                    // A full set of successful notes has been found.
229                    let successful = candidate_notes;
230                    return Ok(NoteConsumptionInfo::new(successful, failed_notes));
231                },
232                Err(TransactionCheckerError::NoteExecution { failed_note_index, error }) => {
233                    // SAFETY: Failed note index is in bounds of the candidate notes.
234                    let failed_note = candidate_notes.remove(failed_note_index);
235                    failed_notes.push(FailedNote::new(failed_note, error));
236
237                    // All possible candidate combinations have been attempted.
238                    if candidate_notes.is_empty() {
239                        return Ok(NoteConsumptionInfo::new(Vec::new(), failed_notes));
240                    }
241                    // Continue and process the next set of candidates.
242                },
243                Err(TransactionCheckerError::EpilogueExecution(_)) => {
244                    let consumption_info = self
245                        .find_largest_executable_combination(
246                            candidate_notes,
247                            failed_notes,
248                            tx_inputs,
249                        )
250                        .await;
251                    return Ok(consumption_info);
252                },
253                Err(TransactionCheckerError::PrologueExecution(err)) => {
254                    return Err(NoteCheckerError::PrologueExecution(err));
255                },
256                Err(TransactionCheckerError::TransactionPreparation(err)) => {
257                    return Err(NoteCheckerError::TransactionPreparation(err));
258                },
259            }
260        }
261    }
262
263    /// Attempts to find the largest possible combination of notes that can execute successfully
264    /// together.
265    ///
266    /// This method incrementally tries combinations of increasing size (1 note, 2 notes, 3 notes,
267    /// etc.) and builds upon previously successful combinations to find the maximum executable
268    /// set.
269    async fn find_largest_executable_combination(
270        &self,
271        mut remaining_notes: Vec<Note>,
272        mut failed_notes: Vec<FailedNote>,
273        mut tx_inputs: TransactionInputs,
274    ) -> NoteConsumptionInfo {
275        let mut successful_notes = Vec::new();
276        let mut failed_note_index = BTreeMap::new();
277
278        // Iterate by note count: try 1 note, then 2, then 3, etc.
279        for size in 1..=remaining_notes.len() {
280            // Can't build a combination of size N without at least N-1 successful notes.
281            if successful_notes.len() < size - 1 {
282                break;
283            }
284
285            // Try adding each remaining note to the current successful combination.
286            for (idx, note) in remaining_notes.iter().enumerate() {
287                successful_notes.push(note.clone());
288
289                tx_inputs.set_input_notes(successful_notes.clone());
290                match self.try_execute_notes(&mut tx_inputs).await {
291                    Ok(()) => {
292                        // The successfully added note might have failed earlier. Remove it from the
293                        // failed list.
294                        failed_note_index.remove(&note.id());
295                        // This combination succeeded; remove the most recently added note from
296                        // the remaining set.
297                        remaining_notes.remove(idx);
298                        break;
299                    },
300                    Err(error) => {
301                        // This combination failed; remove the last note from the test set and
302                        // continue to next note.
303                        let failed_note =
304                            successful_notes.pop().expect("successful notes should not be empty");
305                        // Record the failed note (overwrite previous failures for the relevant
306                        // note).
307                        failed_note_index
308                            .insert(failed_note.id(), FailedNote::new(failed_note, error.into()));
309                    },
310                }
311            }
312        }
313
314        // Append failed notes to the list of failed notes provided as input.
315        failed_notes.extend(failed_note_index.into_values());
316        NoteConsumptionInfo::new(successful_notes, failed_notes)
317    }
318
319    /// Attempts to execute a transaction with the provided input notes.
320    ///
321    /// This method executes the full transaction pipeline including prologue, note execution,
322    /// and epilogue phases. It returns `Ok(())` if all notes are successfully consumed,
323    /// or a specific [`NoteExecutionError`] indicating where and why the execution failed.
324    async fn try_execute_notes(
325        &self,
326        tx_inputs: &mut TransactionInputs,
327    ) -> Result<(), TransactionCheckerError> {
328        if tx_inputs.input_notes().is_empty() {
329            return Ok(());
330        }
331
332        let (mut host, stack_inputs, advice_inputs) =
333            self.0
334                .prepare_transaction(tx_inputs)
335                .await
336                .map_err(TransactionCheckerError::TransactionPreparation)?;
337
338        let processor =
339            FastProcessor::new_with_advice_inputs(stack_inputs.as_slice(), advice_inputs);
340        let result = processor
341            .execute(&TransactionKernel::main(), &mut host)
342            .await
343            .map_err(map_execution_error);
344
345        match result {
346            Ok(execution_output) => {
347                // Set the advice inputs from the successful execution as advice inputs for
348                // reexecution. This avoids calls to the data store (to load data lazily) that have
349                // already been done as part of this execution.
350                let (_, advice_map, merkle_store, _) = execution_output.advice.into_parts();
351                let advice_inputs = AdviceInputs {
352                    map: advice_map,
353                    store: merkle_store,
354                    ..Default::default()
355                };
356                tx_inputs.set_advice_inputs(advice_inputs);
357                Ok(())
358            },
359            Err(error) => {
360                let notes = host.tx_progress().note_execution();
361
362                // Empty notes vector means that we didn't process the notes, so an error
363                // occurred.
364                if notes.is_empty() {
365                    return Err(TransactionCheckerError::PrologueExecution(error));
366                }
367
368                let ((_, last_note_interval), success_notes) =
369                    notes.split_last().expect("notes vector is not empty because of earlier check");
370
371                // If the interval end of the last note is specified, then an error occurred after
372                // notes processing.
373                if last_note_interval.end().is_some() {
374                    Err(TransactionCheckerError::EpilogueExecution(error))
375                } else {
376                    // Return the index of the failed note.
377                    let failed_note_index = success_notes.len();
378                    Err(TransactionCheckerError::NoteExecution { failed_note_index, error })
379                }
380            },
381        }
382    }
383}
384
385// HELPER FUNCTIONS
386// ================================================================================================
387
388/// Handle the epilogue error during the note consumption check in the `can_consume` method.
389///
390/// The goal of this helper function is to handle the cases where the account couldn't consume the
391/// note because of some epilogue check failure, e.g. absence of the authenticator.
392fn handle_epilogue_error(epilogue_error: TransactionExecutorError) -> NoteConsumptionStatus {
393    match epilogue_error {
394        // `Unauthorized` is returned for the multisig accounts if the transaction doesn't have
395        // enough signatures.
396        TransactionExecutorError::Unauthorized(_)
397        // `MissingAuthenticator` is returned for the account with the basic auth if the
398        // authenticator was not provided to the executor (UnreachableAuth).
399        | TransactionExecutorError::MissingAuthenticator => {
400            // Both these cases signal that there is a probability that the provided note could be
401            // consumed if the authentication is provided.
402            NoteConsumptionStatus::ConsumableWithAuthorization
403        },
404        // TODO: apply additional checks to get the verbose error reason
405        _ => NoteConsumptionStatus::UnconsumableConditions,
406    }
407}