miden_tx/executor/
notes_checker.rs

1use alloc::vec::Vec;
2
3use miden_lib::note::well_known_note::WellKnownNote;
4use miden_lib::transaction::TransactionKernel;
5use miden_objects::account::AccountId;
6use miden_objects::block::BlockNumber;
7use miden_objects::note::Note;
8use miden_objects::transaction::{InputNote, InputNotes, TransactionArgs};
9use miden_processor::fast::FastProcessor;
10
11use super::TransactionExecutor;
12use crate::auth::TransactionAuthenticator;
13use crate::{DataStore, NoteCheckerError, TransactionExecutorError};
14
15// NOTE CONSUMPTION INFO
16// ================================================================================================
17
18/// Represents a failed note consumption.
19#[derive(Debug)]
20pub struct FailedNote {
21    pub note: Note,
22    pub error: Option<TransactionExecutorError>,
23}
24
25impl FailedNote {
26    /// Constructs a new `FailedNote`.
27    pub fn new(note: Note, error: Option<TransactionExecutorError>) -> Self {
28        Self { note, error }
29    }
30}
31
32/// Contains information about the successful and failed consumption of notes.
33#[derive(Default, Debug)]
34pub struct NoteConsumptionInfo {
35    pub successful: Vec<Note>,
36    pub failed: Vec<FailedNote>,
37}
38
39impl NoteConsumptionInfo {
40    /// Creates a new [`NoteConsumptionInfo`] instance with the given successful notes.
41    pub fn new_successful(successful: Vec<Note>) -> Self {
42        Self { successful, ..Default::default() }
43    }
44
45    /// Creates a new [`NoteConsumptionInfo`] instance with the given successful and failed notes.
46    pub fn new(successful: Vec<Note>, failed: Vec<FailedNote>) -> Self {
47        Self { successful, failed }
48    }
49}
50
51// NOTE CONSUMPTION CHECKER
52// ================================================================================================
53
54/// This struct performs input notes check against provided target account.
55///
56/// The check is performed using the [NoteConsumptionChecker::check_notes_consumability] procedure.
57/// Essentially runs the transaction to make sure that provided input notes could be consumed by the
58/// account.
59pub struct NoteConsumptionChecker<'a, STORE, AUTH>(&'a TransactionExecutor<'a, 'a, STORE, AUTH>);
60
61impl<'a, STORE, AUTH> NoteConsumptionChecker<'a, STORE, AUTH>
62where
63    STORE: DataStore + Sync,
64    AUTH: TransactionAuthenticator + Sync,
65{
66    /// Creates a new [`NoteConsumptionChecker`] instance with the given transaction executor.
67    pub fn new(tx_executor: &'a TransactionExecutor<'a, 'a, STORE, AUTH>) -> Self {
68        NoteConsumptionChecker(tx_executor)
69    }
70
71    /// Checks whether some set of the provided input notes could be consumed by the provided
72    /// account by executing the transaction with varying combination of notes.
73    ///
74    /// This function attempts to find the maximum set of notes that can be successfully executed
75    /// together by the target account.
76    ///
77    /// If some notes succeed but others fail, the failed notes are removed from the candidate set
78    /// and the remaining notes (successful + unattempted) are retried in the next iteration. This
79    /// process continues until either all remaining notes succeed or no notes can be successfully
80    /// executed
81    ///
82    /// # Example Execution Flow
83    ///
84    /// Given notes A, B, C, D, E:
85    /// - Try [A, B, C, D, E] → A, B succeed, C fails → Remove C, try again.
86    /// - Try [A, B, D, E] → A, B, D succeed, E fails → Remove E, try again.
87    /// - Try [A, B, D] → All succeed → Return successful=[A, B, D], failed=[C, E].
88    ///
89    /// # Returns
90    ///
91    /// Returns [`NoteConsumptionInfo`] containing:
92    /// - `successful`: Notes that can be consumed together by the account.
93    /// - `failed`: Notes that failed during execution attempts.
94    pub async fn check_notes_consumability(
95        &self,
96        target_account_id: AccountId,
97        block_ref: BlockNumber,
98        input_notes: InputNotes<InputNote>,
99        tx_args: TransactionArgs,
100    ) -> Result<NoteConsumptionInfo, NoteCheckerError> {
101        // Ensure well-known notes are ordered first.
102        let mut notes = input_notes.into_vec();
103        notes.sort_unstable_by_key(|note| WellKnownNote::from_note(note.note()).is_none());
104        let notes = InputNotes::<InputNote>::new_unchecked(notes);
105
106        // Attempt to find an executable set of notes.
107        self.find_executable_notes_by_elimination(target_account_id, block_ref, notes, tx_args)
108            .await
109    }
110
111    // HELPER METHODS
112    // --------------------------------------------------------------------------------------------
113
114    /// Finds a set of executable notes and eliminates failed notes from the list in the process.
115    ///
116    /// The result contains some combination of the input notes partitioned by whether they
117    /// succeeded or failed to execute.
118    async fn find_executable_notes_by_elimination(
119        &self,
120        target_account_id: AccountId,
121        block_ref: BlockNumber,
122        notes: InputNotes<InputNote>,
123        tx_args: TransactionArgs,
124    ) -> Result<NoteConsumptionInfo, NoteCheckerError> {
125        let mut candidate_notes = notes.into_vec();
126        let mut failed_notes = Vec::new();
127
128        // Attempt to execute notes in a loop. Reduce the set of notes based on failures until
129        // either a set of notes executes without failure or the set of notes cannot be
130        // further reduced.
131        loop {
132            // Execute the candidate notes.
133            match self
134                .try_execute_notes(
135                    target_account_id,
136                    block_ref,
137                    InputNotes::<InputNote>::new_unchecked(candidate_notes.clone()),
138                    &tx_args,
139                )
140                .await
141            {
142                Ok(()) => {
143                    // A full set of successful notes has been found.
144                    let successful =
145                        candidate_notes.into_iter().map(InputNote::into_note).collect::<Vec<_>>();
146                    return Ok(NoteConsumptionInfo::new(successful, failed_notes));
147                },
148                Err(NoteCheckerError::NoteExecutionFailed { failed_note_index, error }) => {
149                    // SAFETY: Failed note index is in bounds of the candidate notes.
150                    let failed_note = candidate_notes.remove(failed_note_index).into_note();
151                    failed_notes.push(FailedNote::new(failed_note, Some(error)));
152
153                    // All possible candidate combinations have been attempted.
154                    if candidate_notes.is_empty() {
155                        return Ok(NoteConsumptionInfo::new(Vec::new(), failed_notes));
156                    }
157                    // Continue and process the next set of candidates.
158                },
159                Err(NoteCheckerError::EpilogueExecutionFailed(_)) => {
160                    let consumption_info = self
161                        .find_largest_executable_combination(
162                            target_account_id,
163                            block_ref,
164                            candidate_notes,
165                            failed_notes,
166                            &tx_args,
167                        )
168                        .await;
169                    return Ok(consumption_info);
170                },
171                Err(error) => return Err(error),
172            }
173        }
174    }
175
176    /// Attempts to find the largest possible combination of notes that can execute successfully
177    /// together.
178    ///
179    /// This method incrementally tries combinations of increasing size (1 note, 2 notes, 3 notes,
180    /// etc.) and builds upon previously successful combinations to find the maximum executable
181    /// set.
182    async fn find_largest_executable_combination(
183        &self,
184        target_account_id: AccountId,
185        block_ref: BlockNumber,
186        input_notes: Vec<InputNote>,
187        mut failed_notes: Vec<FailedNote>,
188        tx_args: &TransactionArgs,
189    ) -> NoteConsumptionInfo {
190        let mut successful_notes: Vec<InputNote> = Vec::new();
191        let mut remaining_notes = input_notes;
192
193        // Iterate by note count: try 1 note, then 2, then 3, etc.
194        for size in 1..=remaining_notes.len() {
195            // Can't build a combination of size N without at least N-1 successful notes.
196            if successful_notes.len() < size - 1 {
197                break;
198            }
199
200            // Try adding each remaining note to the current successful combination.
201            let mut test_notes = successful_notes.clone();
202            for (idx, note) in remaining_notes.iter().enumerate() {
203                test_notes.push(note.clone());
204
205                match self
206                    .try_execute_notes(
207                        target_account_id,
208                        block_ref,
209                        InputNotes::<InputNote>::new_unchecked(test_notes.clone()),
210                        tx_args,
211                    )
212                    .await
213                {
214                    Ok(()) => {
215                        // This combination succeeded; remove the most recently added note from
216                        // the remaining set.
217                        remaining_notes.remove(idx);
218                        successful_notes = test_notes;
219                        break;
220                    },
221                    _ => {
222                        // This combination failed; remove the last note from the test set and
223                        // continue to next note.
224                        test_notes.pop();
225                    },
226                }
227            }
228        }
229
230        // Convert successful InputNotes to Notes.
231        let successful = successful_notes.into_iter().map(InputNote::into_note).collect::<Vec<_>>();
232
233        // Update failed_notes with notes that weren't included in successful combination.
234        // TODO: Replace `None` with meaningful error for `FailedNote` below.
235        let newly_failed_notes =
236            remaining_notes.into_iter().map(|note| FailedNote::new(note.into_note(), None));
237        failed_notes.extend(newly_failed_notes);
238
239        NoteConsumptionInfo::new(successful, failed_notes)
240    }
241
242    /// Attempts to execute a transaction with the provided input notes.
243    ///
244    /// This method executes the full transaction pipeline including prologue, note execution,
245    /// and epilogue phases. It returns `Ok(())` if all notes are successfully consumed,
246    /// or a specific [`NoteExecutionError`] indicating where and why the execution failed.
247    async fn try_execute_notes(
248        &self,
249        account_id: AccountId,
250        block_ref: BlockNumber,
251        notes: InputNotes<InputNote>,
252        tx_args: &TransactionArgs,
253    ) -> Result<(), NoteCheckerError> {
254        if notes.is_empty() {
255            return Ok(());
256        }
257
258        // TODO: ideally, we should prepare the inputs only once for the whole note consumption
259        // check (rather than doing this every time when we try to execute some subset of notes),
260        // but we currently cannot do this because transaction preparation includes input notes;
261        // we should refactor the preparation process to separate input note preparation from the
262        // rest, and then we can prepare the rest of the inputs once for the whole check
263        let (mut host, _, stack_inputs, advice_inputs) = self
264            .0
265            .prepare_transaction(account_id, block_ref, notes, tx_args, None)
266            .await
267            .map_err(NoteCheckerError::TransactionPreparationFailed)?;
268
269        let processor =
270            FastProcessor::new_with_advice_inputs(stack_inputs.as_slice(), advice_inputs);
271        let result = processor
272            .execute(&TransactionKernel::main(), &mut host)
273            .await
274            .map_err(TransactionExecutorError::TransactionProgramExecutionFailed);
275
276        match result {
277            Ok(_) => Ok(()),
278            Err(error) => {
279                let notes = host.tx_progress().note_execution();
280
281                // Empty notes vector means that we didn't process the notes, so an error
282                // occurred.
283                if notes.is_empty() {
284                    return Err(NoteCheckerError::PrologueExecutionFailed(error));
285                }
286
287                let ((_, last_note_interval), success_notes) =
288                    notes.split_last().expect("notes vector is not empty because of earlier check");
289
290                // If the interval end of the last note is specified, then an error occurred after
291                // notes processing.
292                if last_note_interval.end().is_some() {
293                    Err(NoteCheckerError::EpilogueExecutionFailed(error))
294                } else {
295                    // Return the index of the failed note.
296                    let failed_note_index = success_notes.len();
297                    Err(NoteCheckerError::NoteExecutionFailed { failed_note_index, error })
298                }
299            },
300        }
301    }
302}