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