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