Skip to main content

miden_standards/note/
mod.rs

1use alloc::boxed::Box;
2use alloc::string::ToString;
3use core::error::Error;
4
5use miden_protocol::Felt;
6use miden_protocol::account::AccountId;
7use miden_protocol::block::BlockNumber;
8use miden_protocol::note::{Note, NoteScript, NoteScriptRoot};
9
10use self::config::{
11    AllowlistConfigNote,
12    BlocklistConfigNote,
13    ConstantFeePolicyConfigNote,
14    FaucetMetadataConfigNote,
15    FaucetPolicyConfigNote,
16    MinBurnAmountConfigNote,
17    NetworkAccountConfigNote,
18    OwnerConfigNote,
19    PauseConfigNote,
20    RbacConfigNote,
21};
22
23pub mod config;
24pub mod costs;
25
26mod burn;
27pub use burn::BurnNote;
28
29mod fee_sponsorship;
30pub use fee_sponsorship::{FeeSponsorshipNote, FeeSponsorshipNoteStorage};
31
32mod execution_hint;
33pub use execution_hint::NoteExecutionHint;
34
35mod file;
36pub use file::{NoteFile, NoteSyncHint};
37
38mod mint;
39pub use mint::{MintNote, MintNoteStorage};
40
41mod p2id;
42pub use p2id::{P2idNote, P2idNoteStorage};
43
44mod p2ide;
45pub use p2ide::{P2ideNote, P2ideNoteStorage};
46
47mod pswap;
48pub use pswap::{PswapNote, PswapNoteAttachment, PswapNoteStorage};
49
50mod swap;
51pub use swap::{SwapNote, SwapNoteStorage, SwapPayback, payback_serial_from_swap};
52
53mod tx_fee;
54pub use tx_fee::TxFeeNote;
55
56mod network_account_target;
57pub use network_account_target::{NetworkAccountTarget, NetworkAccountTargetError};
58
59mod network_note;
60pub use network_note::{AccountTargetNetworkNote, NetworkNoteExt};
61
62mod standard_note_attachment;
63use miden_protocol::errors::NoteError;
64pub use standard_note_attachment::StandardNoteAttachment;
65// STANDARD NOTE
66// ================================================================================================
67
68/// The enum holding the types of standard notes provided by `miden-standards`.
69#[allow(non_camel_case_types)]
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum StandardNote {
72    P2ID,
73    P2IDE,
74    SWAP,
75    PSWAP,
76    MINT,
77    BURN,
78    CONSTANT_FEE_POLICY_CONFIG,
79    FAUCET_POLICY_CONFIG,
80    FAUCET_METADATA_CONFIG,
81    MIN_BURN_AMOUNT_CONFIG,
82    ALLOWLIST_CONFIG,
83    BLOCKLIST_CONFIG,
84    PAUSE_CONFIG,
85    OWNER_CONFIG,
86    RBAC_CONFIG,
87    NETWORK_ACCOUNT_CONFIG,
88    FEE_SPONSORSHIP,
89    TX_FEE,
90}
91
92impl StandardNote {
93    // CONSTRUCTOR
94    // --------------------------------------------------------------------------------------------
95
96    /// Returns a [`StandardNote`] instance based on the provided [`NoteScript`]. Returns `None`
97    /// if the provided script does not match any standard note script.
98    pub fn from_script(script: &NoteScript) -> Option<Self> {
99        Self::from_script_root(script.root())
100    }
101
102    /// Returns a [`StandardNote`] instance based on the provided script root. Returns `None` if
103    /// the provided root does not match any standard note script.
104    pub fn from_script_root(root: NoteScriptRoot) -> Option<Self> {
105        if root == P2idNote::script_root() {
106            return Some(Self::P2ID);
107        }
108        if root == P2ideNote::script_root() {
109            return Some(Self::P2IDE);
110        }
111        if root == SwapNote::script_root() {
112            return Some(Self::SWAP);
113        }
114        if root == PswapNote::script_root() {
115            return Some(Self::PSWAP);
116        }
117        if root == MintNote::script_root() {
118            return Some(Self::MINT);
119        }
120        if root == BurnNote::script_root() {
121            return Some(Self::BURN);
122        }
123        if root == ConstantFeePolicyConfigNote::script_root() {
124            return Some(Self::CONSTANT_FEE_POLICY_CONFIG);
125        }
126        if root == FaucetPolicyConfigNote::script_root() {
127            return Some(Self::FAUCET_POLICY_CONFIG);
128        }
129        if root == FaucetMetadataConfigNote::script_root() {
130            return Some(Self::FAUCET_METADATA_CONFIG);
131        }
132        if root == MinBurnAmountConfigNote::script_root() {
133            return Some(Self::MIN_BURN_AMOUNT_CONFIG);
134        }
135        if root == AllowlistConfigNote::script_root() {
136            return Some(Self::ALLOWLIST_CONFIG);
137        }
138        if root == BlocklistConfigNote::script_root() {
139            return Some(Self::BLOCKLIST_CONFIG);
140        }
141        if root == PauseConfigNote::script_root() {
142            return Some(Self::PAUSE_CONFIG);
143        }
144        if root == OwnerConfigNote::script_root() {
145            return Some(Self::OWNER_CONFIG);
146        }
147        if root == RbacConfigNote::script_root() {
148            return Some(Self::RBAC_CONFIG);
149        }
150        if root == NetworkAccountConfigNote::script_root() {
151            return Some(Self::NETWORK_ACCOUNT_CONFIG);
152        }
153        if root == FeeSponsorshipNote::script_root() {
154            return Some(Self::FEE_SPONSORSHIP);
155        }
156        if root == TxFeeNote::script_root() {
157            return Some(Self::TX_FEE);
158        }
159
160        None
161    }
162
163    // PUBLIC ACCESSORS
164    // --------------------------------------------------------------------------------------------
165
166    /// Returns the name of this [`StandardNote`] variant as a string.
167    pub fn name(&self) -> &'static str {
168        match self {
169            Self::P2ID => "P2ID",
170            Self::P2IDE => "P2IDE",
171            Self::SWAP => "SWAP",
172            Self::PSWAP => "PSWAP",
173            Self::MINT => "MINT",
174            Self::BURN => "BURN",
175            Self::CONSTANT_FEE_POLICY_CONFIG => "CONSTANT_FEE_POLICY_CONFIG",
176            Self::FAUCET_POLICY_CONFIG => "FAUCET_POLICY_CONFIG",
177            Self::FAUCET_METADATA_CONFIG => "FAUCET_METADATA_CONFIG",
178            Self::MIN_BURN_AMOUNT_CONFIG => "MIN_BURN_AMOUNT_CONFIG",
179            Self::ALLOWLIST_CONFIG => "ALLOWLIST_CONFIG",
180            Self::BLOCKLIST_CONFIG => "BLOCKLIST_CONFIG",
181            Self::PAUSE_CONFIG => "PAUSE_CONFIG",
182            Self::OWNER_CONFIG => "OWNER_CONFIG",
183            Self::RBAC_CONFIG => "RBAC_CONFIG",
184            Self::NETWORK_ACCOUNT_CONFIG => "NETWORK_ACCOUNT_CONFIG",
185            Self::FEE_SPONSORSHIP => "FEE_SPONSORSHIP",
186            Self::TX_FEE => "TX_FEE",
187        }
188    }
189
190    /// Returns the [`NumStorageItems`] items this kind of note accepts.
191    pub fn num_storage_items(&self) -> NumStorageItems {
192        match self {
193            Self::P2ID => NumStorageItems::Exact(P2idNote::NUM_STORAGE_ITEMS),
194            Self::P2IDE => NumStorageItems::Exact(P2ideNote::NUM_STORAGE_ITEMS),
195            Self::SWAP => NumStorageItems::Exact(SwapNote::NUM_STORAGE_ITEMS),
196            Self::PSWAP => NumStorageItems::Exact(PswapNote::NUM_STORAGE_ITEMS),
197            Self::MINT => MintNote::NUM_STORAGE_ITEMS,
198            Self::BURN => NumStorageItems::Exact(BurnNote::NUM_STORAGE_ITEMS),
199            Self::CONSTANT_FEE_POLICY_CONFIG => {
200                NumStorageItems::Exact(ConstantFeePolicyConfigNote::NUM_STORAGE_ITEMS)
201            },
202            Self::FAUCET_POLICY_CONFIG => {
203                NumStorageItems::Exact(FaucetPolicyConfigNote::NUM_STORAGE_ITEMS)
204            },
205            Self::FAUCET_METADATA_CONFIG => FaucetMetadataConfigNote::NUM_STORAGE_ITEMS,
206            Self::MIN_BURN_AMOUNT_CONFIG => {
207                NumStorageItems::Exact(MinBurnAmountConfigNote::NUM_STORAGE_ITEMS)
208            },
209            Self::ALLOWLIST_CONFIG => {
210                NumStorageItems::Exact(AllowlistConfigNote::NUM_STORAGE_ITEMS)
211            },
212            Self::BLOCKLIST_CONFIG => {
213                NumStorageItems::Exact(BlocklistConfigNote::NUM_STORAGE_ITEMS)
214            },
215            Self::PAUSE_CONFIG => NumStorageItems::Exact(PauseConfigNote::NUM_STORAGE_ITEMS),
216            Self::OWNER_CONFIG => OwnerConfigNote::NUM_STORAGE_ITEMS,
217            Self::RBAC_CONFIG => RbacConfigNote::NUM_STORAGE_ITEMS,
218            Self::NETWORK_ACCOUNT_CONFIG => {
219                NumStorageItems::Exact(NetworkAccountConfigNote::NUM_STORAGE_ITEMS)
220            },
221            Self::FEE_SPONSORSHIP => NumStorageItems::Exact(FeeSponsorshipNote::NUM_STORAGE_ITEMS),
222            Self::TX_FEE => NumStorageItems::Exact(TxFeeNote::NUM_STORAGE_ITEMS),
223        }
224    }
225
226    /// Returns the note script of the current [StandardNote] instance.
227    pub fn script(&self) -> NoteScript {
228        match self {
229            Self::P2ID => P2idNote::script(),
230            Self::P2IDE => P2ideNote::script(),
231            Self::SWAP => SwapNote::script(),
232            Self::PSWAP => PswapNote::script(),
233            Self::MINT => MintNote::script(),
234            Self::BURN => BurnNote::script(),
235            Self::CONSTANT_FEE_POLICY_CONFIG => ConstantFeePolicyConfigNote::script(),
236            Self::FAUCET_POLICY_CONFIG => FaucetPolicyConfigNote::script(),
237            Self::FAUCET_METADATA_CONFIG => FaucetMetadataConfigNote::script(),
238            Self::MIN_BURN_AMOUNT_CONFIG => MinBurnAmountConfigNote::script(),
239            Self::ALLOWLIST_CONFIG => AllowlistConfigNote::script(),
240            Self::BLOCKLIST_CONFIG => BlocklistConfigNote::script(),
241            Self::PAUSE_CONFIG => PauseConfigNote::script(),
242            Self::OWNER_CONFIG => OwnerConfigNote::script(),
243            Self::RBAC_CONFIG => RbacConfigNote::script(),
244            Self::NETWORK_ACCOUNT_CONFIG => NetworkAccountConfigNote::script(),
245            Self::FEE_SPONSORSHIP => FeeSponsorshipNote::script(),
246            Self::TX_FEE => TxFeeNote::script(),
247        }
248    }
249
250    /// Returns the script root of the current [StandardNote] instance.
251    pub fn script_root(&self) -> NoteScriptRoot {
252        match self {
253            Self::P2ID => P2idNote::script_root(),
254            Self::P2IDE => P2ideNote::script_root(),
255            Self::SWAP => SwapNote::script_root(),
256            Self::PSWAP => PswapNote::script_root(),
257            Self::MINT => MintNote::script_root(),
258            Self::BURN => BurnNote::script_root(),
259            Self::CONSTANT_FEE_POLICY_CONFIG => ConstantFeePolicyConfigNote::script_root(),
260            Self::FAUCET_POLICY_CONFIG => FaucetPolicyConfigNote::script_root(),
261            Self::FAUCET_METADATA_CONFIG => FaucetMetadataConfigNote::script_root(),
262            Self::MIN_BURN_AMOUNT_CONFIG => MinBurnAmountConfigNote::script_root(),
263            Self::ALLOWLIST_CONFIG => AllowlistConfigNote::script_root(),
264            Self::BLOCKLIST_CONFIG => BlocklistConfigNote::script_root(),
265            Self::PAUSE_CONFIG => PauseConfigNote::script_root(),
266            Self::OWNER_CONFIG => OwnerConfigNote::script_root(),
267            Self::RBAC_CONFIG => RbacConfigNote::script_root(),
268            Self::NETWORK_ACCOUNT_CONFIG => NetworkAccountConfigNote::script_root(),
269            Self::FEE_SPONSORSHIP => FeeSponsorshipNote::script_root(),
270            Self::TX_FEE => TxFeeNote::script_root(),
271        }
272    }
273
274    /// Performs the inputs check of the provided standard note against the target account and the
275    /// block number.
276    ///
277    /// This function returns:
278    /// - `Some` if we can definitively determine whether the note can be consumed not by the target
279    ///   account.
280    /// - `None` if the consumption status of the note cannot be determined conclusively and further
281    ///   checks are necessary.
282    pub fn is_consumable(
283        &self,
284        note: &Note,
285        target_account_id: AccountId,
286        block_ref: BlockNumber,
287    ) -> Option<NoteConsumptionStatus> {
288        match self.is_consumable_inner(note, target_account_id, block_ref) {
289            Ok(status) => status,
290            Err(err) => {
291                let err: Box<dyn Error + Send + Sync + 'static> = Box::from(err);
292                Some(NoteConsumptionStatus::NeverConsumable(err))
293            },
294        }
295    }
296
297    /// Performs the inputs check of the provided note against the target account and the block
298    /// number.
299    ///
300    /// It performs:
301    /// - for `P2ID` note:
302    ///     - check that note storage has correct number of values.
303    ///     - assertion that the account ID provided by the note storage is equal to the target
304    ///       account ID.
305    /// - for `P2IDE` note:
306    ///     - check that note storage has correct number of values.
307    ///     - check that the target account is either the receiver account or the reclaimer account.
308    ///     - check that depending on whether the target account is reclaimer or receiver, it could
309    ///       be either consumed, or consumed after timelock height, or consumed after reclaim
310    ///       height.
311    /// - for `TX_FEE` note:
312    ///     - check that note storage is empty; the note is otherwise consumable by any account.
313    fn is_consumable_inner(
314        &self,
315        note: &Note,
316        target_account_id: AccountId,
317        block_ref: BlockNumber,
318    ) -> Result<Option<NoteConsumptionStatus>, NoteError> {
319        match self {
320            StandardNote::P2ID => {
321                let input_account_id = P2idNoteStorage::try_from(note.storage().items())
322                    .map_err(|e| NoteError::other_with_source("invalid P2ID note storage", e))?;
323
324                if input_account_id.target() == target_account_id {
325                    Ok(Some(NoteConsumptionStatus::ConsumableWithAuthorization))
326                } else {
327                    Ok(Some(NoteConsumptionStatus::NeverConsumable("account ID provided to the P2ID note storage doesn't match the target account ID".into())))
328                }
329            },
330            StandardNote::P2IDE => {
331                let storage = P2ideNoteStorage::try_from(note.storage().items())
332                    .map_err(|e| NoteError::other_with_source("invalid P2IDE note storage", e))?;
333
334                let reclaimer_account_id = storage.reclaimer();
335                let receiver_account_id = storage.target();
336
337                let current_block_height = block_ref.as_u32();
338                let reclaim_height = storage.reclaim_height().unwrap_or_default().as_u32();
339                let timelock_height = storage.timelock_height().unwrap_or_default().as_u32();
340
341                // block height after which the reclaimer account can consume the note
342                let consumable_after = reclaim_height.max(timelock_height);
343
344                // handle the case when the target account of the transaction is the reclaimer
345                if target_account_id == reclaimer_account_id {
346                    // For the reclaimer, the current block height needs to have reached both
347                    // reclaim and timelock height to be consumable.
348                    if current_block_height >= consumable_after {
349                        Ok(Some(NoteConsumptionStatus::ConsumableWithAuthorization))
350                    } else {
351                        Ok(Some(NoteConsumptionStatus::ConsumableAfter(BlockNumber::from(
352                            consumable_after,
353                        ))))
354                    }
355                // handle the case when the target account of the transaction is receiver
356                } else if target_account_id == receiver_account_id {
357                    // For the receiver, the current block height needs to have reached only the
358                    // timelock height to be consumable: we can ignore the reclaim height in this
359                    // case
360                    if current_block_height >= timelock_height {
361                        Ok(Some(NoteConsumptionStatus::ConsumableWithAuthorization))
362                    } else {
363                        Ok(Some(NoteConsumptionStatus::ConsumableAfter(BlockNumber::from(
364                            timelock_height,
365                        ))))
366                    }
367                // if the target account is neither the reclaimer nor the receiver (from the
368                // note's storage), then this account cannot consume the note
369                } else {
370                    Ok(Some(NoteConsumptionStatus::NeverConsumable(
371            "target account of the transaction does not match neither the receiver account specified by the P2IDE storage, nor the reclaimer account".into()
372        )))
373                }
374            },
375
376            // TX_FEE notes carry no target restriction: any account can consume them, as long as
377            // the note carries no storage items (the note script rejects any other
378            // storage shape).
379            StandardNote::TX_FEE => {
380                if usize::from(note.storage().num_items()) != TxFeeNote::NUM_STORAGE_ITEMS {
381                    Ok(Some(NoteConsumptionStatus::NeverConsumable(
382                        "TX_FEE note carries unexpected storage items".into(),
383                    )))
384                } else {
385                    Ok(Some(NoteConsumptionStatus::ConsumableWithAuthorization))
386                }
387            },
388
389            // the consumption status of any other note cannot be determined by the static analysis,
390            // further checks are necessary.
391            _ => Ok(None),
392        }
393    }
394}
395
396// NUM STORAGE ITEMS
397// ================================================================================================
398
399/// The number of storage items a [`StandardNote`] accepts.
400///
401/// A note script asserts the size of the storage it is handed, and some scripts accept more than
402/// one size: they branch on it, or hold a variable-length tail. This is the set of sizes one of
403/// them accepts, so that a caller can check a note against it instead of comparing against a
404/// single constant.
405#[derive(Debug, Clone, Copy, PartialEq, Eq)]
406pub enum NumStorageItems {
407    /// The note holds exactly this many storage items.
408    Exact(usize),
409    /// The note holds any number of storage items in this inclusive range.
410    Range { min: usize, max: usize },
411    /// The note holds a number of storage items accepted by any of these, and by none of the
412    /// sizes in between them.
413    AnyOf(&'static [NumStorageItems]),
414}
415
416impl NumStorageItems {
417    /// Returns `true` if `num_items` is one of the accepted numbers of storage items.
418    pub fn accepts(&self, num_items: usize) -> bool {
419        match self {
420            Self::Exact(expected) => num_items == *expected,
421            Self::Range { min, max } => (*min..=*max).contains(&num_items),
422            Self::AnyOf(accepted) => accepted.iter().any(|accepted| accepted.accepts(num_items)),
423        }
424    }
425}
426
427// HELPER FUNCTIONS
428// ================================================================================================
429
430/// Decodes an optional block height stored as a single storage item, where zero encodes `None`.
431///
432/// `error_msg` names the field being decoded so that a caller can tell the heights apart.
433pub(crate) fn decode_optional_block_height(
434    item: Felt,
435    error_msg: &'static str,
436) -> Result<Option<BlockNumber>, NoteError> {
437    if item == Felt::ZERO {
438        return Ok(None);
439    }
440
441    let height: u32 = item
442        .as_canonical_u64()
443        .try_into()
444        .map_err(|e| NoteError::other_with_source(error_msg, e))?;
445
446    Ok(Some(BlockNumber::from(height)))
447}
448
449// HELPER STRUCTURES
450// ================================================================================================
451
452/// Describes if a note could be consumed under a specific conditions: target account state
453/// and block height.
454///
455/// The status does not account for any authorization that may be required to consume the
456/// note, nor does it indicate whether the account has sufficient fees to consume it.
457#[derive(Debug)]
458pub enum NoteConsumptionStatus {
459    /// The note can be consumed by the account at the specified block height.
460    Consumable,
461    /// The note can be consumed by the account after the required block height is achieved.
462    ConsumableAfter(BlockNumber),
463    /// The note can be consumed by the account if proper authorization is provided.
464    ConsumableWithAuthorization,
465    /// The note cannot be consumed by the account at the specified conditions (i.e., block
466    /// height and account state).
467    UnconsumableConditions,
468    /// The note cannot be consumed by the specified account under any conditions.
469    NeverConsumable(Box<dyn Error + Send + Sync + 'static>),
470}
471
472impl Clone for NoteConsumptionStatus {
473    fn clone(&self) -> Self {
474        match self {
475            NoteConsumptionStatus::Consumable => NoteConsumptionStatus::Consumable,
476            NoteConsumptionStatus::ConsumableAfter(block_height) => {
477                NoteConsumptionStatus::ConsumableAfter(*block_height)
478            },
479            NoteConsumptionStatus::ConsumableWithAuthorization => {
480                NoteConsumptionStatus::ConsumableWithAuthorization
481            },
482            NoteConsumptionStatus::UnconsumableConditions => {
483                NoteConsumptionStatus::UnconsumableConditions
484            },
485            NoteConsumptionStatus::NeverConsumable(error) => {
486                let err = error.to_string();
487                NoteConsumptionStatus::NeverConsumable(err.into())
488            },
489        }
490    }
491}
492
493// TESTS
494// ================================================================================================
495
496#[cfg(test)]
497mod tests {
498    use miden_protocol::MAX_NOTE_STORAGE_ITEMS;
499
500    use super::*;
501
502    /// A MINT note holds exactly 13 items when it creates a private output note, and 20 or more
503    /// when it creates a public one, so the sizes in between are the only invalid ones below the
504    /// protocol limit.
505    #[test]
506    fn mint_accepts_both_the_private_and_the_public_storage_sizes() {
507        for num_items in [MintNote::NUM_STORAGE_ITEMS_PRIVATE, 20, 21, MAX_NOTE_STORAGE_ITEMS] {
508            assert!(
509                StandardNote::MINT.num_storage_items().accepts(num_items),
510                "{num_items} items should be accepted"
511            );
512        }
513
514        for num_items in [0, 12, 14, 19, MAX_NOTE_STORAGE_ITEMS + 1] {
515            assert!(
516                !StandardNote::MINT.num_storage_items().accepts(num_items),
517                "{num_items} items should be rejected"
518            );
519        }
520    }
521
522    /// The config notes size their storage per action, and the sizes no action uses must be
523    /// rejected even when they fall between the bounds.
524    #[test]
525    fn config_notes_accept_only_the_sizes_their_actions_use() {
526        for (note, accepted, rejected) in [
527            (StandardNote::OWNER_CONFIG, [1, 3].as_slice(), [0, 2, 4].as_slice()),
528            (StandardNote::RBAC_CONFIG, [2, 3, 4].as_slice(), [0, 1, 5].as_slice()),
529            (
530                StandardNote::FAUCET_METADATA_CONFIG,
531                [2, 32].as_slice(),
532                [0, 3, 31, 33].as_slice(),
533            ),
534        ] {
535            for &num_items in accepted {
536                assert!(
537                    note.num_storage_items().accepts(num_items),
538                    "{} should accept {num_items} items",
539                    note.name()
540                );
541            }
542
543            for &num_items in rejected {
544                assert!(
545                    !note.num_storage_items().accepts(num_items),
546                    "{} should reject {num_items} items",
547                    note.name()
548                );
549            }
550        }
551    }
552
553    /// A note of fixed layout reports its size as exact, so no other size is accepted.
554    #[test]
555    fn fixed_size_notes_report_an_exact_size() {
556        for (note, num_items) in [
557            (StandardNote::P2ID, P2idNote::NUM_STORAGE_ITEMS),
558            (StandardNote::P2IDE, P2ideNote::NUM_STORAGE_ITEMS),
559            (StandardNote::TX_FEE, TxFeeNote::NUM_STORAGE_ITEMS),
560        ] {
561            assert_eq!(note.num_storage_items(), NumStorageItems::Exact(num_items));
562            assert!(!note.num_storage_items().accepts(num_items + 1));
563        }
564    }
565}