miden_lib/note/well_known_note.rs
1use alloc::boxed::Box;
2use alloc::string::String;
3use core::error::Error;
4
5use miden_objects::account::AccountId;
6use miden_objects::block::BlockNumber;
7use miden_objects::note::{Note, NoteScript};
8use miden_objects::utils::Deserializable;
9use miden_objects::utils::sync::LazyLock;
10use miden_objects::vm::Program;
11use miden_objects::{Felt, Word};
12
13use crate::account::faucets::{BasicFungibleFaucet, NetworkFungibleFaucet};
14use crate::account::interface::{AccountComponentInterface, AccountInterface};
15use crate::account::wallets::BasicWallet;
16
17// WELL KNOWN NOTE SCRIPTS
18// ================================================================================================
19
20// Initialize the P2ID note script only once
21static P2ID_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
22 let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/P2ID.masb"));
23 let program = Program::read_from_bytes(bytes).expect("Shipped P2ID script is well-formed");
24 NoteScript::new(program)
25});
26
27// Initialize the P2IDE note script only once
28static P2IDE_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
29 let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/P2IDE.masb"));
30 let program = Program::read_from_bytes(bytes).expect("Shipped P2IDE script is well-formed");
31 NoteScript::new(program)
32});
33
34// Initialize the SWAP note script only once
35static SWAP_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
36 let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/SWAP.masb"));
37 let program = Program::read_from_bytes(bytes).expect("Shipped SWAP script is well-formed");
38 NoteScript::new(program)
39});
40
41// Initialize the MINT note script only once
42static MINT_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
43 let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/MINT.masb"));
44 let program = Program::read_from_bytes(bytes).expect("Shipped MINT script is well-formed");
45 NoteScript::new(program)
46});
47
48// Initialize the BURN note script only once
49static BURN_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
50 let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/BURN.masb"));
51 let program = Program::read_from_bytes(bytes).expect("Shipped BURN script is well-formed");
52 NoteScript::new(program)
53});
54
55/// Returns the P2ID (Pay-to-ID) note script.
56fn p2id() -> NoteScript {
57 P2ID_SCRIPT.clone()
58}
59
60/// Returns the P2ID (Pay-to-ID) note script root.
61fn p2id_root() -> Word {
62 P2ID_SCRIPT.root()
63}
64
65/// Returns the P2IDE (Pay-to-ID with optional reclaim & timelock) note script.
66fn p2ide() -> NoteScript {
67 P2IDE_SCRIPT.clone()
68}
69
70/// Returns the P2IDE (Pay-to-ID with optional reclaim & timelock) note script root.
71fn p2ide_root() -> Word {
72 P2IDE_SCRIPT.root()
73}
74
75/// Returns the SWAP (Swap note) note script.
76fn swap() -> NoteScript {
77 SWAP_SCRIPT.clone()
78}
79
80/// Returns the SWAP (Swap note) note script root.
81fn swap_root() -> Word {
82 SWAP_SCRIPT.root()
83}
84
85/// Returns the MINT (Mint note) note script.
86fn mint() -> NoteScript {
87 MINT_SCRIPT.clone()
88}
89
90/// Returns the MINT (Mint note) note script root.
91fn mint_root() -> Word {
92 MINT_SCRIPT.root()
93}
94
95/// Returns the BURN (Burn note) note script.
96fn burn() -> NoteScript {
97 BURN_SCRIPT.clone()
98}
99
100/// Returns the BURN (Burn note) note script root.
101fn burn_root() -> Word {
102 BURN_SCRIPT.root()
103}
104
105// WELL KNOWN NOTE
106// ================================================================================================
107
108/// The enum holding the types of basic well-known notes provided by the `miden-lib`.
109pub enum WellKnownNote {
110 P2ID,
111 P2IDE,
112 SWAP,
113 MINT,
114 BURN,
115}
116
117impl WellKnownNote {
118 // CONSTANTS
119 // --------------------------------------------------------------------------------------------
120
121 /// Expected number of inputs of the P2ID note.
122 const P2ID_NUM_INPUTS: usize = 2;
123
124 /// Expected number of inputs of the P2IDE note.
125 const P2IDE_NUM_INPUTS: usize = 4;
126
127 /// Expected number of inputs of the SWAP note.
128 const SWAP_NUM_INPUTS: usize = 10;
129
130 /// Expected number of inputs of the MINT note.
131 const MINT_NUM_INPUTS: usize = 9;
132
133 /// Expected number of inputs of the BURN note.
134 const BURN_NUM_INPUTS: usize = 0;
135
136 // CONSTRUCTOR
137 // --------------------------------------------------------------------------------------------
138
139 /// Returns a [WellKnownNote] instance based on the note script of the provided [Note]. Returns
140 /// `None` if the provided note is not a basic well-known note.
141 pub fn from_note(note: &Note) -> Option<Self> {
142 let note_script_root = note.script().root();
143
144 if note_script_root == p2id_root() {
145 return Some(Self::P2ID);
146 }
147 if note_script_root == p2ide_root() {
148 return Some(Self::P2IDE);
149 }
150 if note_script_root == swap_root() {
151 return Some(Self::SWAP);
152 }
153 if note_script_root == mint_root() {
154 return Some(Self::MINT);
155 }
156 if note_script_root == burn_root() {
157 return Some(Self::BURN);
158 }
159
160 None
161 }
162
163 // PUBLIC ACCESSORS
164 // --------------------------------------------------------------------------------------------
165
166 /// Returns the expected inputs number of the active note.
167 pub fn num_expected_inputs(&self) -> usize {
168 match self {
169 Self::P2ID => Self::P2ID_NUM_INPUTS,
170 Self::P2IDE => Self::P2IDE_NUM_INPUTS,
171 Self::SWAP => Self::SWAP_NUM_INPUTS,
172 Self::MINT => Self::MINT_NUM_INPUTS,
173 Self::BURN => Self::BURN_NUM_INPUTS,
174 }
175 }
176
177 /// Returns the note script of the current [WellKnownNote] instance.
178 pub fn script(&self) -> NoteScript {
179 match self {
180 Self::P2ID => p2id(),
181 Self::P2IDE => p2ide(),
182 Self::SWAP => swap(),
183 Self::MINT => mint(),
184 Self::BURN => burn(),
185 }
186 }
187
188 /// Returns the script root of the current [WellKnownNote] instance.
189 pub fn script_root(&self) -> Word {
190 match self {
191 Self::P2ID => p2id_root(),
192 Self::P2IDE => p2ide_root(),
193 Self::SWAP => swap_root(),
194 Self::MINT => mint_root(),
195 Self::BURN => burn_root(),
196 }
197 }
198
199 /// Returns a boolean value indicating whether this [WellKnownNote] is compatible with the
200 /// provided [AccountInterface].
201 pub fn is_compatible_with(&self, account_interface: &AccountInterface) -> bool {
202 if account_interface.components().contains(&AccountComponentInterface::BasicWallet) {
203 return true;
204 }
205
206 let interface_proc_digests = account_interface.get_procedure_digests();
207 match self {
208 Self::P2ID | &Self::P2IDE => {
209 // To consume P2ID and P2IDE notes, the `receive_asset` procedure must be present in
210 // the provided account interface.
211 interface_proc_digests.contains(&BasicWallet::receive_asset_digest())
212 },
213 Self::SWAP => {
214 // To consume SWAP note, the `receive_asset` and `move_asset_to_note` procedures
215 // must be present in the provided account interface.
216 interface_proc_digests.contains(&BasicWallet::receive_asset_digest())
217 && interface_proc_digests.contains(&BasicWallet::move_asset_to_note_digest())
218 },
219 Self::MINT => {
220 // MINT notes work only with network fungible faucets. The network faucet uses
221 // note-based authentication (checking if the note sender equals the faucet owner)
222 // to authorize minting, while basic faucets have different mint procedures that
223 // are not compatible with MINT notes.
224 interface_proc_digests.contains(&NetworkFungibleFaucet::distribute_digest())
225 },
226 Self::BURN => {
227 // BURN notes work with both basic and network fungible faucets because both
228 // faucet types export the same `burn` procedure with identical MAST roots.
229 // This allows a single BURN note script to work with either faucet type.
230 interface_proc_digests.contains(&BasicFungibleFaucet::burn_digest())
231 || interface_proc_digests.contains(&NetworkFungibleFaucet::burn_digest())
232 },
233 }
234 }
235
236 /// Performs the inputs check of the provided well-known note against the target account and the
237 /// block number.
238 ///
239 /// This function returns:
240 /// - `Some` if we can definitively determine whether the note can be consumed not by the target
241 /// account.
242 /// - `None` if the consumption status of the note cannot be determined conclusively and further
243 /// checks are necessary.
244 pub fn is_consumable(
245 &self,
246 note: &Note,
247 target_account_id: AccountId,
248 block_ref: BlockNumber,
249 ) -> Option<NoteConsumptionStatus> {
250 match self.is_consumable_inner(note, target_account_id, block_ref) {
251 Ok(status) => status,
252 Err(err) => {
253 let err: Box<dyn Error + Send + Sync + 'static> = Box::from(err);
254 Some(NoteConsumptionStatus::NeverConsumable(err))
255 },
256 }
257 }
258
259 /// Performs the inputs check of the provided note against the target account and the block
260 /// number.
261 ///
262 /// It performs:
263 /// - for `P2ID` note:
264 /// - check that note inputs have correct number of values.
265 /// - assertion that the account ID provided by the note inputs is equal to the target
266 /// account ID.
267 /// - for `P2IDE` note:
268 /// - check that note inputs have correct number of values.
269 /// - check that the target account is either the receiver account or the sender account.
270 /// - check that depending on whether the target account is sender or receiver, it could be
271 /// either consumed, or consumed after timelock height, or consumed after reclaim height.
272 fn is_consumable_inner(
273 &self,
274 note: &Note,
275 target_account_id: AccountId,
276 block_ref: BlockNumber,
277 ) -> Result<Option<NoteConsumptionStatus>, StaticAnalysisError> {
278 match self {
279 WellKnownNote::P2ID => {
280 let input_account_id = parse_p2id_inputs(note.inputs().values())?;
281
282 if input_account_id == target_account_id {
283 Ok(Some(NoteConsumptionStatus::ConsumableWithAuthorization))
284 } else {
285 Ok(Some(NoteConsumptionStatus::NeverConsumable("account ID provided to the P2ID note inputs doesn't match the target account ID".into())))
286 }
287 },
288 WellKnownNote::P2IDE => {
289 let (receiver_account_id, reclaim_height, timelock_height) =
290 parse_p2ide_inputs(note.inputs().values())?;
291
292 let current_block_height = block_ref.as_u32();
293
294 // block height after which sender account can consume the note
295 let consumable_after = reclaim_height.max(timelock_height);
296
297 // handle the case when the target account of the transaction is sender
298 if target_account_id == note.metadata().sender() {
299 // For the sender, the current block height needs to have reached both reclaim
300 // and timelock height to be consumable.
301 if current_block_height >= consumable_after {
302 Ok(Some(NoteConsumptionStatus::ConsumableWithAuthorization))
303 } else {
304 Ok(Some(NoteConsumptionStatus::ConsumableAfter(BlockNumber::from(
305 consumable_after,
306 ))))
307 }
308 // handle the case when the target account of the transaction is receiver
309 } else if target_account_id == receiver_account_id {
310 // For the receiver, the current block height needs to have reached only the
311 // timelock height to be consumable: we can ignore the reclaim height in this
312 // case
313 if current_block_height >= timelock_height {
314 Ok(Some(NoteConsumptionStatus::ConsumableWithAuthorization))
315 } else {
316 Ok(Some(NoteConsumptionStatus::ConsumableAfter(BlockNumber::from(
317 timelock_height,
318 ))))
319 }
320 // if the target account is neither the sender nor the receiver (from the note's
321 // inputs), then this account cannot consume the note
322 } else {
323 Ok(Some(NoteConsumptionStatus::NeverConsumable(
324 "target account of the transaction does not match neither the receiver account specified by the P2IDE inputs, nor the sender account".into()
325 )))
326 }
327 },
328
329 // the consumption status of any other note cannot be determined by the static analysis,
330 // further checks are necessary.
331 _ => Ok(None),
332 }
333 }
334}
335
336// HELPER FUNCTIONS
337// ================================================================================================
338
339/// Returns the receiver account ID parsed from the provided P2ID note inputs.
340///
341/// # Errors
342///
343/// Returns an error if:
344/// - the length of the provided note inputs array is not equal to the expected inputs number of the
345/// P2ID note.
346/// - first two elements of the note inputs array does not form the valid account ID.
347fn parse_p2id_inputs(note_inputs: &[Felt]) -> Result<AccountId, StaticAnalysisError> {
348 if note_inputs.len() != WellKnownNote::P2ID.num_expected_inputs() {
349 return Err(StaticAnalysisError::new(format!(
350 "P2ID note should have {} inputs, but {} was provided",
351 WellKnownNote::P2ID.num_expected_inputs(),
352 note_inputs.len()
353 )));
354 }
355
356 try_read_account_id_from_inputs(note_inputs)
357}
358
359/// Returns the receiver account ID, reclaim height and timelock height parsed from the provided
360/// P2IDE note inputs.
361///
362/// # Errors
363///
364/// Returns an error if:
365/// - the length of the provided note inputs array is not equal to the expected inputs number of the
366/// P2IDE note.
367/// - first two elements of the note inputs array does not form the valid account ID.
368/// - third note inputs array element (reclaim height) is not a valid u32 value.
369/// - fourth note inputs array element (timelock height) is not a valid u32 value.
370fn parse_p2ide_inputs(note_inputs: &[Felt]) -> Result<(AccountId, u32, u32), StaticAnalysisError> {
371 if note_inputs.len() != WellKnownNote::P2IDE.num_expected_inputs() {
372 return Err(StaticAnalysisError::new(format!(
373 "P2IDE note should have {} inputs, but {} was provided",
374 WellKnownNote::P2IDE.num_expected_inputs(),
375 note_inputs.len()
376 )));
377 }
378
379 let receiver_account_id = try_read_account_id_from_inputs(note_inputs)?;
380
381 let reclaim_height = u32::try_from(note_inputs[2])
382 .map_err(|_err| StaticAnalysisError::new("reclaim block height should be a u32"))?;
383
384 let timelock_height = u32::try_from(note_inputs[3])
385 .map_err(|_err| StaticAnalysisError::new("timelock block height should be a u32"))?;
386
387 Ok((receiver_account_id, reclaim_height, timelock_height))
388}
389
390/// Reads the account ID from the first two note input values.
391///
392/// Returns None if the note input values used to construct the account ID are invalid.
393fn try_read_account_id_from_inputs(note_inputs: &[Felt]) -> Result<AccountId, StaticAnalysisError> {
394 if note_inputs.len() < 2 {
395 return Err(StaticAnalysisError::new(format!(
396 "P2ID and P2IDE notes should have at least 2 note inputs, but {} was provided",
397 note_inputs.len()
398 )));
399 }
400
401 AccountId::try_from([note_inputs[1], note_inputs[0]]).map_err(|source| {
402 StaticAnalysisError::with_source(
403 "failed to create an account ID from the first two note inputs",
404 source,
405 )
406 })
407}
408
409// HELPER STRUCTURES
410// ================================================================================================
411
412/// Describes if a note could be consumed under a specific conditions: target account state
413/// and block height.
414///
415/// The status does not account for any authorization that may be required to consume the
416/// note, nor does it indicate whether the account has sufficient fees to consume it.
417#[derive(Debug)]
418pub enum NoteConsumptionStatus {
419 /// The note can be consumed by the account at the specified block height.
420 Consumable,
421 /// The note can be consumed by the account after the required block height is achieved.
422 ConsumableAfter(BlockNumber),
423 /// The note can be consumed by the account if proper authorization is provided.
424 ConsumableWithAuthorization,
425 /// The note cannot be consumed by the account at the specified conditions (i.e., block
426 /// height and account state).
427 UnconsumableConditions,
428 /// The note cannot be consumed by the specified account under any conditions.
429 NeverConsumable(Box<dyn Error + Send + Sync + 'static>),
430}
431
432#[derive(thiserror::Error, Debug)]
433#[error("{message}")]
434struct StaticAnalysisError {
435 /// Stack size of `Box<str>` is smaller than String.
436 message: Box<str>,
437 /// thiserror will return this when calling Error::source on StaticAnalysisError.
438 source: Option<Box<dyn Error + Send + Sync + 'static>>,
439}
440
441impl StaticAnalysisError {
442 /// Creates a new static analysis error from an error message.
443 pub fn new(message: impl Into<String>) -> Self {
444 let message: String = message.into();
445 Self { message: message.into(), source: None }
446 }
447
448 /// Creates a new static analysis error from an error message and a source error.
449 pub fn with_source(
450 message: impl Into<String>,
451 source: impl Error + Send + Sync + 'static,
452 ) -> Self {
453 let message: String = message.into();
454 Self {
455 message: message.into(),
456 source: Some(Box::new(source)),
457 }
458 }
459}