miden_tx/executor/notes_checker.rs
1use alloc::collections::BTreeMap;
2use alloc::vec::Vec;
3
4use miden_processor::ExecutionError;
5use miden_processor::advice::AdviceInputs;
6use miden_protocol::account::AccountId;
7use miden_protocol::block::BlockNumber;
8use miden_protocol::note::Note;
9use miden_protocol::transaction::{
10 InputNote,
11 InputNotes,
12 TransactionArgs,
13 TransactionInputs,
14 TransactionKernel,
15};
16use miden_protocol::vm::PackageDebugInfo;
17use miden_standards::note::{NoteConsumptionStatus, StandardNote};
18
19use super::{ProgramExecutor, TransactionExecutor};
20use crate::auth::TransactionAuthenticator;
21use crate::errors::TransactionCheckerError;
22use crate::executor::map_execution_error;
23use crate::{DataStore, NoteCheckerError, TransactionExecutorError};
24
25// CONSTANTS
26// ================================================================================================
27
28/// Maximum number of notes that can be checked at once.
29///
30/// Fixed at an amount that should keep each run of note consumption checking to a maximum of ~50ms.
31pub const MAX_NUM_CHECKER_NOTES: usize = 20;
32
33// NOTE CONSUMPTION INFO
34// ================================================================================================
35
36/// Represents a successfully consumed note along with the number of cycles it took to execute.
37#[derive(Debug)]
38pub struct SuccessfulNote {
39 note: Note,
40 num_cycles: usize,
41}
42
43impl SuccessfulNote {
44 /// Constructs a new `SuccessfulNote`.
45 pub fn new(note: Note, num_cycles: usize) -> Self {
46 Self { note, num_cycles }
47 }
48
49 /// Returns a reference to the note.
50 pub fn note(&self) -> &Note {
51 &self.note
52 }
53
54 /// Returns the number of cycles consumed during execution.
55 pub fn num_cycles(&self) -> usize {
56 self.num_cycles
57 }
58}
59
60/// Represents a failed note consumption.
61#[derive(Debug)]
62pub struct FailedNote {
63 note: Note,
64 error: TransactionExecutorError,
65 /// The number of cycles consumed by the note before it failed.
66 ///
67 /// This is `Some` when the failure was due to exceeding the cycle limit, and `None`
68 /// for other error types where the cycle count is not meaningful.
69 num_cycles: Option<usize>,
70}
71
72impl FailedNote {
73 /// Constructs a new `FailedNote`.
74 pub fn new(note: Note, error: TransactionExecutorError, num_cycles: Option<usize>) -> Self {
75 Self { note, error, num_cycles }
76 }
77
78 /// Returns a reference to the note.
79 pub fn note(&self) -> &Note {
80 &self.note
81 }
82
83 /// Returns a reference to the error.
84 pub fn error(&self) -> &TransactionExecutorError {
85 &self.error
86 }
87
88 /// Returns the number of cycles consumed before failure, if available.
89 ///
90 /// This is `Some` when the failure was due to exceeding the cycle limit, and `None`
91 /// for other error types where the cycle count is not meaningful.
92 pub fn num_cycles(&self) -> Option<usize> {
93 self.num_cycles
94 }
95}
96
97/// Contains information about the successful and failed consumption of notes.
98#[derive(Default, Debug)]
99pub struct NoteConsumptionInfo {
100 successful: Vec<SuccessfulNote>,
101 failed: Vec<FailedNote>,
102}
103
104impl NoteConsumptionInfo {
105 /// Creates a new [`NoteConsumptionInfo`] instance with the given successful notes.
106 pub fn new_successful(successful: Vec<SuccessfulNote>) -> Self {
107 Self { successful, ..Default::default() }
108 }
109
110 /// Creates a new [`NoteConsumptionInfo`] instance with the given successful and failed notes.
111 pub fn new(successful: Vec<SuccessfulNote>, failed: Vec<FailedNote>) -> Self {
112 Self { successful, failed }
113 }
114
115 /// Returns a reference to the successfully consumed notes.
116 pub fn successful(&self) -> &[SuccessfulNote] {
117 &self.successful
118 }
119
120 /// Returns a reference to the failed notes.
121 pub fn failed(&self) -> &[FailedNote] {
122 &self.failed
123 }
124
125 /// Consumes the struct and returns the successful and failed notes.
126 pub fn into_parts(self) -> (Vec<SuccessfulNote>, Vec<FailedNote>) {
127 (self.successful, self.failed)
128 }
129}
130
131// NOTE CONSUMPTION CHECKER
132// ================================================================================================
133
134/// This struct performs input notes check against provided target account.
135///
136/// The check is performed using the [NoteConsumptionChecker::check_notes_consumability] procedure.
137/// Essentially runs the transaction to make sure that provided input notes could be consumed by the
138/// account.
139pub struct NoteConsumptionChecker<'a, STORE, AUTH, EXEC: ProgramExecutor>(
140 &'a TransactionExecutor<'a, 'a, STORE, AUTH, EXEC>,
141);
142
143impl<'a, STORE, AUTH, EXEC> NoteConsumptionChecker<'a, STORE, AUTH, EXEC>
144where
145 STORE: DataStore + Sync,
146 AUTH: TransactionAuthenticator + Sync,
147 EXEC: ProgramExecutor,
148{
149 /// Creates a new [`NoteConsumptionChecker`] instance with the given transaction executor.
150 pub fn new(tx_executor: &'a TransactionExecutor<'a, 'a, STORE, AUTH, EXEC>) -> Self {
151 NoteConsumptionChecker(tx_executor)
152 }
153
154 /// Checks whether some set of the provided input notes could be consumed by the provided
155 /// account by executing the transaction with varying combination of notes.
156 ///
157 /// This function attempts to find the maximum set of notes that can be successfully executed
158 /// together by the target account.
159 ///
160 /// Because of the runtime complexity involved in this function, a limited range of
161 /// [`MAX_NUM_CHECKER_NOTES`] input notes is allowed.
162 ///
163 /// If some notes succeed and others fail, the failed notes are removed from the candidate set
164 /// and the remaining notes (successful + unattempted) are retried in the next iteration. This
165 /// process continues until either all remaining notes succeed or no notes can be successfully
166 /// executed
167 ///
168 /// For example, given notes A, B, C, D, E, the execution flow would be as follows:
169 /// - Try [A, B, C, D, E] → A, B succeed, C fails → Remove C, try again.
170 /// - Try [A, B, D, E] → A, B, D succeed, E fails → Remove E, try again.
171 /// - Try [A, B, D] → All succeed → Return successful=[A, B, D], failed=[C, E].
172 ///
173 /// If a failure occurs at the epilogue phase of the transaction execution, the relevant set of
174 /// otherwise-successful notes are retried in various combinations in an attempt to find a
175 /// combination that passes the epilogue phase successfully.
176 ///
177 /// Returns a list of successfully consumed notes and a list of failed notes.
178 pub async fn check_notes_consumability(
179 &self,
180 target_account_id: AccountId,
181 block_ref: BlockNumber,
182 mut notes: Vec<Note>,
183 tx_args: TransactionArgs,
184 ) -> Result<NoteConsumptionInfo, NoteCheckerError> {
185 let num_notes = notes.len();
186 if num_notes == 0 || num_notes > MAX_NUM_CHECKER_NOTES {
187 return Err(NoteCheckerError::InputNoteCountOutOfRange(num_notes));
188 }
189 // Ensure standard notes are ordered first.
190 notes.sort_unstable_by_key(|note| {
191 StandardNote::from_script_root(note.script().root()).is_none()
192 });
193
194 let notes = InputNotes::from(notes);
195 let tx_inputs = self
196 .0
197 .prepare_tx_inputs(target_account_id, block_ref, notes, tx_args)
198 .await
199 .map_err(NoteCheckerError::TransactionPreparation)?;
200
201 // Attempt to find an executable set of notes.
202 self.find_executable_notes_by_elimination(tx_inputs).await
203 }
204
205 /// Checks whether the provided input note could be consumed by the provided account by
206 /// executing a transaction at the specified block height.
207 ///
208 /// This function takes into account the possibility that the signatures may not be loaded into
209 /// the transaction context and returns the [`NoteConsumptionStatus`] result accordingly.
210 ///
211 /// This function first applies the static analysis of the provided note, and if it doesn't
212 /// reveal any errors next it tries to execute the transaction. Based on the execution result,
213 /// it either returns a [`NoteCheckerError`] or the [`NoteConsumptionStatus`]: depending on
214 /// whether the execution succeeded, failed in the prologue, during the note execution process
215 /// or in the epilogue.
216 pub async fn can_consume(
217 &self,
218 target_account_id: AccountId,
219 block_ref: BlockNumber,
220 note: InputNote,
221 tx_args: TransactionArgs,
222 ) -> Result<NoteConsumptionStatus, NoteCheckerError> {
223 // Return the consumption status if we manage to determine it from the standard note
224 if let Some(standard_note) = StandardNote::from_script_root(note.note().script().root())
225 && let Some(consumption_status) =
226 standard_note.is_consumable(note.note(), target_account_id, block_ref)
227 {
228 return Ok(consumption_status);
229 }
230
231 // Prepare transaction inputs.
232 let mut tx_inputs = self
233 .0
234 .prepare_tx_inputs(
235 target_account_id,
236 block_ref,
237 InputNotes::new_unchecked(vec![note]),
238 tx_args,
239 )
240 .await
241 .map_err(NoteCheckerError::TransactionPreparation)?;
242
243 // try to consume the provided note
244 match self.try_execute_notes(&mut tx_inputs).await {
245 // execution succeeded
246 Ok(_cycle_counts) => Ok(NoteConsumptionStatus::Consumable),
247 Err(tx_checker_error) => {
248 match tx_checker_error {
249 // execution failed on the preparation stage, before we actually executed the tx
250 TransactionCheckerError::TransactionPreparation(e) => {
251 Err(NoteCheckerError::TransactionPreparation(e))
252 },
253 // execution failed during the prologue
254 TransactionCheckerError::PrologueExecution(e) => {
255 Err(NoteCheckerError::PrologueExecution(e))
256 },
257 // execution failed during the note processing
258 TransactionCheckerError::NoteExecution { .. } => {
259 Ok(NoteConsumptionStatus::UnconsumableConditions)
260 },
261 // execution failed during the epilogue
262 TransactionCheckerError::EpilogueExecution {
263 error: epilogue_error, ..
264 } => Ok(handle_epilogue_error(epilogue_error)),
265 }
266 },
267 }
268 }
269
270 // HELPER METHODS
271 // --------------------------------------------------------------------------------------------
272
273 /// Finds a set of executable notes and eliminates failed notes from the list in the process.
274 ///
275 /// The result contains some combination of the input notes partitioned by whether they
276 /// succeeded or failed to execute.
277 async fn find_executable_notes_by_elimination(
278 &self,
279 mut tx_inputs: TransactionInputs,
280 ) -> Result<NoteConsumptionInfo, NoteCheckerError> {
281 let mut candidate_notes = tx_inputs
282 .input_notes()
283 .iter()
284 .map(|note| note.clone().into_note())
285 .collect::<Vec<_>>();
286 let mut failed_notes = Vec::new();
287
288 // Attempt to execute notes in a loop. Reduce the set of notes based on failures until
289 // either a set of notes executes without failure or the set of notes cannot be
290 // further reduced.
291 loop {
292 // Execute the candidate notes.
293 tx_inputs.set_input_notes(candidate_notes.clone());
294 match self.try_execute_notes(&mut tx_inputs).await {
295 Ok(cycle_counts) => {
296 // A full set of successful notes has been found.
297 let successful = candidate_notes
298 .into_iter()
299 .zip(cycle_counts)
300 .map(|(note, num_cycles)| SuccessfulNote::new(note, num_cycles))
301 .collect();
302 return Ok(NoteConsumptionInfo::new(successful, failed_notes));
303 },
304 Err(TransactionCheckerError::NoteExecution {
305 failed_note_index,
306 error,
307 failed_note_cycle_count,
308 ..
309 }) => {
310 // SAFETY: Failed note index is in bounds of the candidate notes.
311 let failed_note = candidate_notes.remove(failed_note_index);
312 failed_notes.push(FailedNote::new(failed_note, error, failed_note_cycle_count));
313
314 // All possible candidate combinations have been attempted.
315 if candidate_notes.is_empty() {
316 return Ok(NoteConsumptionInfo::new(Vec::new(), failed_notes));
317 }
318 // Continue and process the next set of candidates.
319 },
320 Err(TransactionCheckerError::EpilogueExecution { .. }) => {
321 let consumption_info = self
322 .find_largest_executable_combination(
323 candidate_notes,
324 failed_notes,
325 tx_inputs,
326 )
327 .await;
328 return Ok(consumption_info);
329 },
330 Err(TransactionCheckerError::PrologueExecution(err)) => {
331 return Err(NoteCheckerError::PrologueExecution(err));
332 },
333 Err(TransactionCheckerError::TransactionPreparation(err)) => {
334 return Err(NoteCheckerError::TransactionPreparation(err));
335 },
336 }
337 }
338 }
339
340 /// Attempts to find the largest possible combination of notes that can execute successfully
341 /// together.
342 ///
343 /// This method incrementally tries combinations of increasing size (1 note, 2 notes, 3 notes,
344 /// etc.) and builds upon previously successful combinations to find the maximum executable
345 /// set.
346 async fn find_largest_executable_combination(
347 &self,
348 mut remaining_notes: Vec<Note>,
349 mut failed_notes: Vec<FailedNote>,
350 mut tx_inputs: TransactionInputs,
351 ) -> NoteConsumptionInfo {
352 let mut successful_notes = Vec::new();
353 let mut successful_cycle_counts = Vec::new();
354 let mut failed_note_index = BTreeMap::new();
355
356 // Iterate by note count: try 1 note, then 2, then 3, etc.
357 for size in 1..=remaining_notes.len() {
358 // Can't build a combination of size N without at least N-1 successful notes.
359 if successful_notes.len() < size - 1 {
360 break;
361 }
362
363 // Try adding each remaining note to the current successful combination.
364 for (idx, note) in remaining_notes.iter().enumerate() {
365 successful_notes.push(note.clone());
366
367 tx_inputs.set_input_notes(successful_notes.clone());
368 match self.try_execute_notes(&mut tx_inputs).await {
369 Ok(cycle_counts) => {
370 // The successfully added note might have failed earlier. Remove it from the
371 // failed list.
372 failed_note_index.remove(¬e.id());
373 // Store the cycle counts from the latest successful execution.
374 successful_cycle_counts = cycle_counts;
375 // This combination succeeded; remove the most recently added note from
376 // the remaining set.
377 remaining_notes.remove(idx);
378 break;
379 },
380 Err(error) => {
381 // This combination failed; remove the last note from the test set and
382 // continue to next note.
383 let failed_note =
384 successful_notes.pop().expect("successful notes should not be empty");
385
386 // Extract the failed note's cycle count if available.
387 let num_cycles = match &error {
388 TransactionCheckerError::NoteExecution {
389 failed_note_cycle_count,
390 ..
391 } => *failed_note_cycle_count,
392 _ => None,
393 };
394
395 // Record the failed note (overwrite previous failures for the relevant
396 // note).
397 failed_note_index.insert(
398 failed_note.id(),
399 FailedNote::new(failed_note, error.into(), num_cycles),
400 );
401 },
402 }
403 }
404 }
405
406 // Pair successful notes with their cycle counts from the last successful execution.
407 let successful = successful_notes
408 .into_iter()
409 .zip(successful_cycle_counts)
410 .map(|(note, num_cycles)| SuccessfulNote::new(note, num_cycles))
411 .collect();
412
413 // Append failed notes to the list of failed notes provided as input.
414 failed_notes.extend(failed_note_index.into_values());
415 NoteConsumptionInfo::new(successful, failed_notes)
416 }
417
418 /// Attempts to execute a transaction with the provided input notes.
419 ///
420 /// This method executes the full transaction pipeline including prologue, note execution,
421 /// and epilogue phases. It returns `Ok(cycle_counts)` if all notes are successfully consumed
422 /// (where `cycle_counts` contains the number of cycles for each note), or a specific
423 /// [`TransactionCheckerError`] indicating where and why the execution failed. The order of the
424 /// returned `cycle_counts` is guaranteed to match the order of the input notes.
425 async fn try_execute_notes(
426 &self,
427 tx_inputs: &mut TransactionInputs,
428 ) -> Result<Vec<usize>, TransactionCheckerError> {
429 if tx_inputs.input_notes().is_empty() {
430 return Ok(Vec::new());
431 }
432
433 let (mut host, stack_inputs, advice_inputs) =
434 self.0
435 .prepare_transaction(tx_inputs)
436 .await
437 .map_err(TransactionCheckerError::TransactionPreparation)?;
438
439 let processor = EXEC::new(stack_inputs, advice_inputs, self.0.exec_options);
440 let program = TransactionKernel::main();
441 let kernel_debug_info = TransactionKernel::main_debug_info();
442 let fallback_debug_info = PackageDebugInfo::default();
443 let result = processor
444 .execute_with_package_debug_info(
445 &program,
446 kernel_debug_info.as_deref().unwrap_or(&fallback_debug_info),
447 TransactionKernel::main_entrypoint_source_node(),
448 &mut host,
449 )
450 .await
451 .map_err(map_execution_error);
452
453 match result {
454 Ok(execution_output) => {
455 let cycle_counts = host
456 .tx_progress()
457 .note_execution()
458 .iter()
459 .map(|(_, interval)| interval.len())
460 .collect();
461
462 // Set the advice inputs from the successful execution as advice inputs for
463 // reexecution. This avoids calls to the data store (to load data lazily) that have
464 // already been done as part of this execution.
465 let (_, advice_map, merkle_store, _) = execution_output.advice.into_parts();
466 let advice_inputs = AdviceInputs {
467 map: advice_map,
468 store: merkle_store,
469 ..Default::default()
470 };
471 tx_inputs.set_advice_inputs(advice_inputs);
472 Ok(cycle_counts)
473 },
474 Err(error) => {
475 let notes = host.tx_progress().note_execution();
476
477 // Empty notes vector means that we didn't process the notes, so an error
478 // occurred.
479 if notes.is_empty() {
480 return Err(TransactionCheckerError::PrologueExecution(error));
481 }
482
483 let ((_, last_note_interval), success_notes) =
484 notes.split_last().expect("notes vector is not empty because of earlier check");
485
486 // If the interval end of the last note is specified, then an error occurred after
487 // notes processing. All notes executed successfully in this case.
488 if last_note_interval.end().is_some() {
489 let successful_notes_cycle_counts =
490 notes.iter().map(|(_, interval)| interval.len()).collect();
491 Err(TransactionCheckerError::EpilogueExecution {
492 error,
493 successful_notes_cycle_counts,
494 })
495 } else {
496 // Return the index of the failed note.
497 let failed_note_index = success_notes.len();
498 let successful_notes_cycle_counts =
499 success_notes.iter().map(|(_, interval)| interval.len()).collect();
500
501 // Compute the failed note's cycle count when the failure was due to
502 // exceeding the cycle limit. In this case, the note's interval has a
503 // start but no end, and the total cycles consumed equals the max allowed.
504 let failed_note_cycle_count = match &error {
505 TransactionExecutorError::TransactionProgramExecutionFailed(
506 ExecutionError::CycleLimitExceeded(max_cycles),
507 ) => last_note_interval
508 .start()
509 .map(|start| *max_cycles as usize - usize::from(start)),
510 _ => None,
511 };
512
513 Err(TransactionCheckerError::NoteExecution {
514 failed_note_index,
515 error,
516 successful_notes_cycle_counts,
517 failed_note_cycle_count,
518 })
519 }
520 },
521 }
522 }
523}
524
525// HELPER FUNCTIONS
526// ================================================================================================
527
528/// Handle the epilogue error during the note consumption check in the `can_consume` method.
529///
530/// The goal of this helper function is to handle the cases where the account couldn't consume the
531/// note because of some epilogue check failure, e.g. absence of the authenticator.
532fn handle_epilogue_error(epilogue_error: TransactionExecutorError) -> NoteConsumptionStatus {
533 match epilogue_error {
534 // `Unauthorized` is returned for the multisig accounts if the transaction doesn't have
535 // enough signatures.
536 TransactionExecutorError::Unauthorized(_)
537 // `MissingAuthenticator` is returned for the account with the basic auth if the
538 // authenticator was not provided to the executor (UnreachableAuth).
539 | TransactionExecutorError::MissingAuthenticator => {
540 // Both these cases signal that there is a probability that the provided note could be
541 // consumed if the authentication is provided.
542 NoteConsumptionStatus::ConsumableWithAuthorization
543 },
544 // TODO: apply additional checks to get the verbose error reason
545 _ => NoteConsumptionStatus::UnconsumableConditions,
546 }
547}