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}