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(¬e.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}