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