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}