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