miden_tx/executor/notes_checker/checker_utils.rs
1use alloc::collections::BTreeMap;
2use alloc::vec::Vec;
3
4use miden_protocol::note::{Note, NoteId};
5use miden_standards::note::{FeeSponsorshipNote, NoteConsumptionStatus};
6
7use crate::TransactionExecutorError;
8
9// CONSTANTS
10// ================================================================================================
11
12/// Maximum number of notes that can be checked at once.
13///
14/// Fixed at an amount that should keep each run of note consumption checking to a maximum of ~50ms.
15pub const MAX_NUM_CHECKER_NOTES: usize = 20;
16
17// NOTE CONSUMPTION INFO
18// ================================================================================================
19
20/// Represents a successfully consumed note along with the number of cycles it took to execute.
21#[derive(Debug)]
22pub struct SuccessfulNote {
23 note: Note,
24 num_cycles: usize,
25}
26
27impl SuccessfulNote {
28 /// Constructs a new `SuccessfulNote`.
29 pub fn new(note: Note, num_cycles: usize) -> Self {
30 Self { note, num_cycles }
31 }
32
33 /// Returns a reference to the note.
34 pub fn note(&self) -> &Note {
35 &self.note
36 }
37
38 /// Returns the number of cycles consumed during execution.
39 pub fn num_cycles(&self) -> usize {
40 self.num_cycles
41 }
42}
43
44/// Contains the reason why a note is not included in the successful set.
45///
46/// The variants separate the note the executor blamed from the ones that merely shared its bundle.
47#[derive(Debug)]
48#[non_exhaustive]
49pub enum NoteFailure {
50 /// The note the executor blamed for the failure.
51 Blamed {
52 /// The error the failing execution produced.
53 error: TransactionExecutorError,
54 /// The number of cycles consumed by the note before it failed.
55 ///
56 /// This is `Some` when the failure was due to exceeding the cycle limit, and `None` for
57 /// other error types where the cycle count is not meaningful.
58 num_cycles: Option<usize>,
59 },
60 /// The note was rejected along with the bundle of the note it is bound to, and may well be
61 /// consumable in a different set.
62 Collateral {
63 /// The blamed note this one shared its bundle with.
64 blamed_by: NoteId,
65 },
66}
67
68/// Represents a failed note consumption.
69#[derive(Debug)]
70pub struct FailedNote {
71 note: Note,
72 failure: NoteFailure,
73}
74
75impl FailedNote {
76 /// Constructs a new `FailedNote`.
77 pub fn new(note: Note, failure: NoteFailure) -> Self {
78 Self { note, failure }
79 }
80
81 /// Returns a reference to the note.
82 pub fn note(&self) -> &Note {
83 &self.note
84 }
85
86 /// Returns why the note was not consumed.
87 pub fn failure(&self) -> &NoteFailure {
88 &self.failure
89 }
90
91 /// Returns `true` if the executor blamed this note for the failure.
92 pub fn is_blamed(&self) -> bool {
93 matches!(self.failure, NoteFailure::Blamed { .. })
94 }
95
96 /// Returns `true` if this note was rejected along with the bundle of the note it is bound to.
97 ///
98 /// This is not the negation of [`FailedNote::is_blamed`]: [`NoteFailure`] is non-exhaustive, so
99 /// a note may in future fail for a reason that is neither.
100 pub fn is_collateral(&self) -> bool {
101 matches!(self.failure, NoteFailure::Collateral { .. })
102 }
103
104 /// Returns a reference to the error, if this note was the one blamed for the failure. `None`
105 /// otherwise.
106 pub fn error(&self) -> Option<&TransactionExecutorError> {
107 match &self.failure {
108 NoteFailure::Blamed { error, .. } => Some(error),
109 NoteFailure::Collateral { .. } => None,
110 }
111 }
112
113 /// Returns the number of cycles consumed before failure, if available.
114 ///
115 /// This is `Some` when the failure was due to exceeding the cycle limit, and `None`
116 /// for other error types where the cycle count is not meaningful.
117 pub fn num_cycles(&self) -> Option<usize> {
118 match &self.failure {
119 NoteFailure::Blamed { num_cycles, .. } => *num_cycles,
120 NoteFailure::Collateral { .. } => None,
121 }
122 }
123}
124
125/// Contains information about the successful and failed consumption of notes.
126#[derive(Default, Debug)]
127pub struct NoteConsumptionInfo {
128 successful: Vec<SuccessfulNote>,
129 failed: Vec<FailedNote>,
130}
131
132impl NoteConsumptionInfo {
133 /// Creates a new [`NoteConsumptionInfo`] instance with the given successful notes.
134 pub fn new_successful(successful: Vec<SuccessfulNote>) -> Self {
135 Self { successful, ..Default::default() }
136 }
137
138 /// Creates a new [`NoteConsumptionInfo`] instance with the given successful and failed notes.
139 pub fn new(successful: Vec<SuccessfulNote>, failed: Vec<FailedNote>) -> Self {
140 Self { successful, failed }
141 }
142
143 /// Returns a reference to the successfully consumed notes.
144 pub fn successful(&self) -> &[SuccessfulNote] {
145 &self.successful
146 }
147
148 /// Returns a reference to the failed notes.
149 pub fn failed(&self) -> &[FailedNote] {
150 &self.failed
151 }
152
153 /// Consumes the struct and returns the successful and failed notes.
154 pub fn into_parts(self) -> (Vec<SuccessfulNote>, Vec<FailedNote>) {
155 (self.successful, self.failed)
156 }
157}
158
159// NOTE BUNDLE
160// ================================================================================================
161
162/// A group of input notes that has to be tested for consumability as a unit, such as a feature note
163/// and the notes which sponsor it.
164#[derive(Debug)]
165pub(super) struct NoteBundle {
166 notes: Vec<Note>,
167}
168
169impl NoteBundle {
170 /// Groups `notes` into bundles that must be consumed together.
171 ///
172 /// A FEE_SPONSORSHIP note joins the bundle of the feature note it sponsors; an unpaired
173 /// sponsorship note forms a bundle of its own, so that it fails alone rather than dropping the
174 /// notes it would otherwise have been grouped with. Every other note type forms a bundle of its
175 /// own.
176 ///
177 /// The feature note is always first in the resulting bundle (if any); bundle preserves the
178 /// relative order of the sponsorship notes in it.
179 pub(super) fn group(notes: Vec<Note>) -> Vec<Self> {
180 let note_indices: BTreeMap<NoteId, usize> =
181 notes.iter().enumerate().map(|(idx, note)| (note.id(), idx)).collect();
182
183 // Put the feature notes and orphan notes to the values with keys equal to this note index
184 // in the `note_indices`. Sponsorship notes are appended to the values which contain the
185 // corresponding feature note.
186 // Keying by index rather than by note ID keeps the bundles in the caller's order.
187 let mut bundles: BTreeMap<usize, Vec<Note>> = BTreeMap::new();
188 for (idx, note) in notes.into_iter().enumerate() {
189 // A sponsorship is only bundled when the note it sponsors is actually an input;
190 // otherwise it can only be reclaimed, which is something it has to attempt on its own.
191 match FeeSponsorshipNote::try_from(¬e)
192 .ok()
193 .and_then(|sponsorship| note_indices.get(&sponsorship.feature_note_id()).copied())
194 {
195 Some(head_idx) => bundles.entry(head_idx).or_default().push(note),
196 // This note heads its own bundle, so it goes first whichever side of the notes
197 // bound to it it arrives on.
198 None => bundles.entry(idx).or_default().insert(0, note),
199 }
200 }
201
202 bundles.into_values().map(|notes| Self { notes }).collect()
203 }
204
205 /// Returns the notes forming the bundle.
206 pub(super) fn notes(&self) -> &[Note] {
207 &self.notes
208 }
209}
210
211// HELPER FUNCTIONS
212// ================================================================================================
213
214/// Handle the epilogue error during the note consumption check in the `can_consume` method.
215///
216/// The goal of this helper function is to handle the cases where the account couldn't consume the
217/// note because of some epilogue check failure, e.g. absence of the authenticator.
218pub(super) fn handle_epilogue_error(
219 epilogue_error: TransactionExecutorError,
220) -> NoteConsumptionStatus {
221 match epilogue_error {
222 // `Unauthorized` is returned for the multisig accounts if the transaction doesn't have
223 // enough signatures.
224 TransactionExecutorError::Unauthorized(_)
225 // `MissingAuthenticator` is returned for the account with the basic auth if the
226 // authenticator was not provided to the executor (UnreachableAuth).
227 | TransactionExecutorError::MissingAuthenticator => {
228 // Both these cases signal that there is a probability that the provided note could be
229 // consumed if the authentication is provided.
230 NoteConsumptionStatus::ConsumableWithAuthorization
231 },
232 // TODO: apply additional checks to get the verbose error reason
233 _ => NoteConsumptionStatus::UnconsumableConditions,
234 }
235}