diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9ee7b577cc..273f2fdb5a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,8 @@
### Features
+- Added `active_note::get_storage_info` and `active_note::get_bounded_storage`, and switched the standard and agglayer note scripts with a bounded storage layout over to the latter ([#3563](https://github.com/0xMiden/protocol/pull/3563)).
+
### Changes
- [BREAKING] Added the `miden::standards::expiration` MASM module with `apply_default` and used it to apply a default 20-block transaction expiration limit to the standard allowlist and blocklist transfer policies and the fee manager's `estimate_note_fee` procedure ([#3512](https://github.com/0xMiden/protocol/pull/3512)).
diff --git a/crates/miden-agglayer/asm/agglayer/notes/b2agg.masm b/crates/miden-agglayer/asm/agglayer/notes/b2agg.masm
index 3d46b41b33..25655850a6 100644
--- a/crates/miden-agglayer/asm/agglayer/notes/b2agg.masm
+++ b/crates/miden-agglayer/asm/agglayer/notes/b2agg.masm
@@ -78,7 +78,8 @@ pub proc main
# => [pad(16)]
# Store note storage -> mem[8..14]
- push.B2AGG_NOTE_STORAGE_PTR exec.active_note::get_storage
+ push.B2AGG_NOTE_NUM_STORAGE_ITEMS push.B2AGG_NOTE_STORAGE_PTR
+ exec.active_note::get_bounded_storage
# => [num_storage_items, pad(16)]
# Validate the number of storage items
diff --git a/crates/miden-agglayer/asm/agglayer/notes/claim.masm b/crates/miden-agglayer/asm/agglayer/notes/claim.masm
index 15ede064b9..3901281a24 100644
--- a/crates/miden-agglayer/asm/agglayer/notes/claim.masm
+++ b/crates/miden-agglayer/asm/agglayer/notes/claim.masm
@@ -10,6 +10,9 @@ const PROOF_DATA_SIZE = 536
const LEAF_DATA_SIZE = 32
const OUTPUT_NOTE_SIZE = 8
+# The number of storage items of a CLAIM note, see the layout in the script's doc comment.
+const CLAIM_NOTE_NUM_STORAGE_ITEMS = 569
+
const CLAIM_NOTE_STORAGE_PTR = 0
const PROOF_DATA_START_PTR = CLAIM_NOTE_STORAGE_PTR
const LEAF_DATA_START_PTR = 536
@@ -75,6 +78,7 @@ const ERR_CLAIM_TARGET_ACCT_MISMATCH = "CLAIM note attachment target account doe
#! Panics if:
#! - account does not expose claim procedure.
#! - note attachment target account does not match the consuming account.
+#! - the note carries more than CLAIM_NOTE_NUM_STORAGE_ITEMS storage items.
@note_script
pub proc main
dropw
@@ -86,7 +90,8 @@ pub proc main
# => [pad(16)]
# Load CLAIM note storage into memory, starting at address 0
- push.CLAIM_NOTE_STORAGE_PTR exec.active_note::get_storage drop
+ push.CLAIM_NOTE_NUM_STORAGE_ITEMS push.CLAIM_NOTE_STORAGE_PTR
+ exec.active_note::get_bounded_storage drop
# => [pad(16)]
exec.write_claim_data_into_advice_map_by_key
diff --git a/crates/miden-agglayer/asm/agglayer/notes/config_agg_bridge.masm b/crates/miden-agglayer/asm/agglayer/notes/config_agg_bridge.masm
index 2171635c88..3d1ccf0759 100644
--- a/crates/miden-agglayer/asm/agglayer/notes/config_agg_bridge.masm
+++ b/crates/miden-agglayer/asm/agglayer/notes/config_agg_bridge.masm
@@ -84,7 +84,8 @@ pub proc main
# => [pad(16)]
# Load note storage to memory
- push.STORAGE_START_PTR exec.active_note::get_storage
+ push.CONFIG_AGG_BRIDGE_NUM_STORAGE_ITEMS push.STORAGE_START_PTR
+ exec.active_note::get_bounded_storage
# => [num_storage_items, pad(16)]
# Validate the number of storage items
diff --git a/crates/miden-agglayer/asm/agglayer/notes/deregister_agg_faucet.masm b/crates/miden-agglayer/asm/agglayer/notes/deregister_agg_faucet.masm
index 212e6e0e66..f10d590032 100644
--- a/crates/miden-agglayer/asm/agglayer/notes/deregister_agg_faucet.masm
+++ b/crates/miden-agglayer/asm/agglayer/notes/deregister_agg_faucet.masm
@@ -49,7 +49,8 @@ pub proc main
# => [pad(16)]
# Load note storage to memory
- push.STORAGE_START_PTR exec.active_note::get_storage
+ push.DEREGISTER_AGG_FAUCET_NUM_STORAGE_ITEMS push.STORAGE_START_PTR
+ exec.active_note::get_bounded_storage
# => [num_storage_items, pad(16)]
# Validate the number of storage items
diff --git a/crates/miden-agglayer/asm/agglayer/notes/remove_ger.masm b/crates/miden-agglayer/asm/agglayer/notes/remove_ger.masm
index e591675646..2169bd9002 100644
--- a/crates/miden-agglayer/asm/agglayer/notes/remove_ger.masm
+++ b/crates/miden-agglayer/asm/agglayer/notes/remove_ger.masm
@@ -52,7 +52,8 @@ pub proc main
# => [pad(16)]
# Load note storage to memory
- push.STORAGE_PTR_GER_LOWER exec.active_note::get_storage
+ push.REMOVE_GER_NOTE_NUM_STORAGE_ITEMS push.STORAGE_PTR_GER_LOWER
+ exec.active_note::get_bounded_storage
# => [num_storage_items, pad(16)]
# Validate the number of storage items
diff --git a/crates/miden-agglayer/asm/agglayer/notes/update_ger.masm b/crates/miden-agglayer/asm/agglayer/notes/update_ger.masm
index c3c2804749..1ae78a3c3e 100644
--- a/crates/miden-agglayer/asm/agglayer/notes/update_ger.masm
+++ b/crates/miden-agglayer/asm/agglayer/notes/update_ger.masm
@@ -51,7 +51,8 @@ pub proc main
# => [pad(16)]
# Load note storage to memory
- push.STORAGE_PTR_GER_LOWER exec.active_note::get_storage
+ push.UPDATE_GER_NOTE_NUM_STORAGE_ITEMS push.STORAGE_PTR_GER_LOWER
+ exec.active_note::get_bounded_storage
# => [num_storage_items, pad(16)]
# Validate the number of storage items
diff --git a/crates/miden-protocol/asm/protocol/src/active_note.masm b/crates/miden-protocol/asm/protocol/src/active_note.masm
index 97a0b0eb5c..23704aea20 100644
--- a/crates/miden-protocol/asm/protocol/src/active_note.masm
+++ b/crates/miden-protocol/asm/protocol/src/active_note.masm
@@ -14,6 +14,9 @@ const ERR_NOTE_DATA_DOES_NOT_MATCH_COMMITMENT = "note data does not match the co
const ERR_NOTE_INVALID_NUMBER_OF_STORAGE_ITEMS =
"the specified number of note storage items does not match the actual number"
+const ERR_NOTE_TOO_MANY_STORAGE_ITEMS =
+ "the number of note storage items exceeds the maximum accepted by the note script"
+
# ACTIVE NOTE PROCEDURES
# =================================================================================================
#
@@ -242,6 +245,28 @@ pub proc get_note_id() -> NoteId
# => [NOTE_ID]
end
+#! Returns the storage commitment and the number of storage items of the active note.
+#!
+#! Inputs: []
+#! Outputs: [NOTE_STORAGE_COMMITMENT, num_storage_items]
+#!
+#! Where:
+#! - NOTE_STORAGE_COMMITMENT is the commitment to the note's storage.
+#! - num_storage_items is the number of storage items the active note was created with.
+#!
+#! Panics if:
+#! - no note is currently active.
+#!
+#! Invocation: exec
+pub proc get_storage_info() -> (word, u16)
+ # push a placeholder note_index (ignored when is_active_note = 1) and the active note flag
+ push.0.1
+ # => [is_active_note = 1, note_index = 0]
+
+ exec.input_note_internal::get_storage_info_raw
+ # => [NOTE_STORAGE_COMMITMENT, num_storage_items]
+end
+
#! Writes the active note's storage to memory starting at the specified address.
#!
#! Inputs:
@@ -276,6 +301,44 @@ pub proc get_storage(dest_ptr: MemoryAddress) -> u16
# => [num_storage_items]
end
+#! Writes the active note's storage to memory starting at the specified address, provided the note
+#! carries at most `max_num_storage_items` items.
+#!
+#! Inputs:
+#! Stack: [dest_ptr, max_num_storage_items]
+#! Advice Map: { NOTE_STORAGE_COMMITMENT: [STORAGE] }
+#! Outputs:
+#! Stack: [num_storage_items]
+#!
+#! Where:
+#! - dest_ptr is the memory address to write the note storage.
+#! - max_num_storage_items is the largest number of storage items the caller accepts.
+#! - NOTE_STORAGE_COMMITMENT is the commitment to the note's storage.
+#! - STORAGE is the data corresponding to the note's storage.
+#! - num_storage_items is the number of storage items the active note was created with.
+#!
+#! Panics if:
+#! - no note is currently active.
+#! - num_storage_items is greater than max_num_storage_items.
+#!
+#! Invocation: exec
+pub proc get_bounded_storage(dest_ptr: MemoryAddress, max_num_storage_items: u16) -> u16
+ exec.get_storage_info
+ # => [NOTE_STORAGE_COMMITMENT, num_storage_items, dest_ptr, max_num_storage_items]
+
+ # reject an oversized note before its storage preimage is loaded and hashed
+ dup.4 movup.7 lte assert.err=ERR_NOTE_TOO_MANY_STORAGE_ITEMS
+ # => [NOTE_STORAGE_COMMITMENT, num_storage_items, dest_ptr]
+
+ # save num_storage_items for the return value
+ dup.4 movdn.6
+ # => [NOTE_STORAGE_COMMITMENT, num_storage_items, dest_ptr, num_storage_items]
+
+ # write the inputs to the provided destination pointer
+ exec.write_storage_to_memory
+ # => [num_storage_items]
+end
+
#! Returns the metadata of the active note.
#!
#! Inputs: []
diff --git a/crates/miden-standards/asm/standards/notes/allowlist_config.masm b/crates/miden-standards/asm/standards/notes/allowlist_config.masm
index 242a53f2ad..24d3a21226 100644
--- a/crates/miden-standards/asm/standards/notes/allowlist_config.masm
+++ b/crates/miden-standards/asm/standards/notes/allowlist_config.masm
@@ -5,7 +5,7 @@ use miden::standards::faucets::policies::transfer::allowlist::manager
# CONSTANTS
# =================================================================================================
-# Memory base at which the note storage is written by `active_note::get_storage`.
+# Memory base at which the note storage is written by `active_note::get_bounded_storage`.
const STORAGE_PTR = 0
# Memory addresses of the AllowlistConfig note storage. Both actions carry the same arguments: the
@@ -23,6 +23,9 @@ const SELECTOR_DISALLOW_ACCOUNT = 1
const NUM_ITEMS_ALLOW_ACCOUNT = 3
const NUM_ITEMS_DISALLOW_ACCOUNT = 3
+# Largest storage item count any action uses
+const MAX_NUM_ITEMS = NUM_ITEMS_ALLOW_ACCOUNT
+
# ERRORS
# =================================================================================================
@@ -57,6 +60,7 @@ const ERR_ALLOWLIST_CONFIG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS = "allowlist confi
#!
#! Panics if:
#! - the consuming account is not the note's target account, or the target attachment is missing.
+#! - the number of storage items exceeds MAX_NUM_ITEMS.
#! - the selector is not a known action.
#! - the number of storage items does not match the selected action.
#! - the caller is not authorized for the selected action per the installed `Authority` component.
@@ -72,7 +76,7 @@ pub proc main
# => [pad(16)]
# write the note storage to memory starting at STORAGE_PTR
- push.STORAGE_PTR exec.active_note::get_storage
+ push.MAX_NUM_ITEMS push.STORAGE_PTR exec.active_note::get_bounded_storage
# => [num_storage_items]
mem_load.SELECTOR_PTR
diff --git a/crates/miden-standards/asm/standards/notes/blocklist_config.masm b/crates/miden-standards/asm/standards/notes/blocklist_config.masm
index 846c50171d..d2f3d627bf 100644
--- a/crates/miden-standards/asm/standards/notes/blocklist_config.masm
+++ b/crates/miden-standards/asm/standards/notes/blocklist_config.masm
@@ -5,7 +5,7 @@ use miden::standards::faucets::policies::transfer::blocklist::manager
# CONSTANTS
# =================================================================================================
-# Memory base at which the note storage is written by `active_note::get_storage`.
+# Memory base at which the note storage is written by `active_note::get_bounded_storage`.
const STORAGE_PTR = 0
# Memory addresses of the BlocklistConfig note storage. Both actions carry the same arguments: the
@@ -23,6 +23,9 @@ const SELECTOR_UNBLOCK_ACCOUNT = 1
const NUM_ITEMS_BLOCK_ACCOUNT = 3
const NUM_ITEMS_UNBLOCK_ACCOUNT = 3
+# Largest storage item count any action uses
+const MAX_NUM_ITEMS = NUM_ITEMS_BLOCK_ACCOUNT
+
# ERRORS
# =================================================================================================
@@ -57,6 +60,7 @@ const ERR_BLOCKLIST_CONFIG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS = "blocklist confi
#!
#! Panics if:
#! - the consuming account is not the note's target account, or the target attachment is missing.
+#! - the number of storage items exceeds MAX_NUM_ITEMS.
#! - the selector is not a known action.
#! - the number of storage items does not match the selected action.
#! - the caller is not authorized for the selected action per the installed `Authority` component.
@@ -72,7 +76,7 @@ pub proc main
# => [pad(16)]
# write the note storage to memory starting at STORAGE_PTR
- push.STORAGE_PTR exec.active_note::get_storage
+ push.MAX_NUM_ITEMS push.STORAGE_PTR exec.active_note::get_bounded_storage
# => [num_storage_items]
mem_load.SELECTOR_PTR
diff --git a/crates/miden-standards/asm/standards/notes/burn.masm b/crates/miden-standards/asm/standards/notes/burn.masm
index e0689f8ef0..f0ef06507e 100644
--- a/crates/miden-standards/asm/standards/notes/burn.masm
+++ b/crates/miden-standards/asm/standards/notes/burn.masm
@@ -52,7 +52,7 @@ pub proc main
# => [pad(16)]
# Load the asset embedded in storage and require the canonical eight-felt asset layout.
- push.STORAGE_PTR exec.active_note::get_storage
+ push.NUM_STORAGE_ITEMS push.STORAGE_PTR exec.active_note::get_bounded_storage
eq.NUM_STORAGE_ITEMS assert.err=ERR_BURN_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS
# => [pad(16)]
diff --git a/crates/miden-standards/asm/standards/notes/constant_fee_policy_config.masm b/crates/miden-standards/asm/standards/notes/constant_fee_policy_config.masm
index e537113306..20e1248dfd 100644
--- a/crates/miden-standards/asm/standards/notes/constant_fee_policy_config.masm
+++ b/crates/miden-standards/asm/standards/notes/constant_fee_policy_config.masm
@@ -5,7 +5,7 @@ use miden::standards::fees::policies::constant_fee_manager
# CONSTANTS
# =================================================================================================
-# Memory base at which the note storage is written by `active_note::get_storage`.
+# Memory base at which the note storage is written by `active_note::get_bounded_storage`.
const STORAGE_PTR = 0
# Memory addresses of the ConstantFeePolicyConfig note storage: the note script root word
@@ -71,7 +71,7 @@ pub proc main
# => [pad(16)]
# write the note storage to memory starting at STORAGE_PTR
- push.STORAGE_PTR exec.active_note::get_storage
+ push.NUM_STORAGE_ITEMS push.STORAGE_PTR exec.active_note::get_bounded_storage
# => [num_storage_items, pad(16)]
eq.NUM_STORAGE_ITEMS
diff --git a/crates/miden-standards/asm/standards/notes/faucet_metadata_config.masm b/crates/miden-standards/asm/standards/notes/faucet_metadata_config.masm
index fdc036f8bf..1ffacfdc1e 100644
--- a/crates/miden-standards/asm/standards/notes/faucet_metadata_config.masm
+++ b/crates/miden-standards/asm/standards/notes/faucet_metadata_config.masm
@@ -10,7 +10,7 @@ use miden::standards::faucets::fungible
# Number of felts encoding a metadata string: 7 Words.
const STRING_NUM_ELEMENTS = 28
-# Memory base at which the note storage is written by `active_note::get_storage`.
+# Memory base at which the note storage is written by `active_note::get_bounded_storage`.
const STORAGE_PTR = 0
# Memory addresses of the FaucetMetadataConfig note storage. The selector is one felt padded out to
@@ -32,6 +32,9 @@ const SELECTOR_SET_EXTERNAL_LINK = 3
const NUM_ITEMS_SET_MAX_SUPPLY = 2
const NUM_ITEMS_SET_STRING = 32
+# Largest storage item count any action uses
+const MAX_NUM_ITEMS = NUM_ITEMS_SET_STRING
+
# ERRORS
# =================================================================================================
@@ -61,7 +64,7 @@ const ERR_FAUCET_METADATA_CONFIG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS = "faucet me
#! - `3` SetExternalLink: `[3, 0, 0, 0, external_link(28)]`.
#!
#! The three string setters take the Poseidon2 commitment of their new value on the operand stack
-#! and read the preimage from the advice map. This script commits to the payload `get_storage` wrote
+#! and read the preimage from the advice map. This script commits to the payload `get_bounded_storage` wrote
#! to memory and inserts it into the advice map under that commitment, so the value is bound into
#! the note commitment through the note storage and needs no out-of-band advice inputs.
#!
@@ -78,6 +81,7 @@ const ERR_FAUCET_METADATA_CONFIG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS = "faucet me
#!
#! Panics if:
#! - the consuming account is not the note's target account, or the target attachment is missing.
+#! - the number of storage items exceeds MAX_NUM_ITEMS.
#! - the selector is not a known action.
#! - the number of storage items does not match the selected action.
#! - the selected field's mutability flag is not set, or the account is paused.
@@ -94,7 +98,7 @@ pub proc main
# => [pad(16)]
# write the note storage to memory starting at STORAGE_PTR
- push.STORAGE_PTR exec.active_note::get_storage
+ push.MAX_NUM_ITEMS push.STORAGE_PTR exec.active_note::get_bounded_storage
# => [num_storage_items]
mem_load.SELECTOR_PTR
diff --git a/crates/miden-standards/asm/standards/notes/faucet_policy_config.masm b/crates/miden-standards/asm/standards/notes/faucet_policy_config.masm
index 2472ce0bb8..31b18492b1 100644
--- a/crates/miden-standards/asm/standards/notes/faucet_policy_config.masm
+++ b/crates/miden-standards/asm/standards/notes/faucet_policy_config.masm
@@ -5,7 +5,7 @@ use miden::standards::faucets::policies::policy_manager
# CONSTANTS
# =================================================================================================
-# Memory base at which the note storage is written by `active_note::get_storage`.
+# Memory base at which the note storage is written by `active_note::get_bounded_storage`.
const STORAGE_PTR = 0
# Memory addresses of the FaucetPolicyConfig note storage. The policy root word comes first so that
@@ -60,6 +60,7 @@ const ERR_FAUCET_POLICY_CONFIG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS = "faucet poli
#!
#! Panics if:
#! - the consuming account is not the note's target account, or the target attachment is missing.
+#! - the number of storage items exceeds NUM_ITEMS_SET_POLICY.
#! - the selector is not a known action.
#! - the number of storage items does not match the selected action.
#! - the caller is not authorized per the installed `Authority` component, or the policy root is not
@@ -76,7 +77,7 @@ pub proc main
# => [pad(16)]
# write the note storage to memory starting at STORAGE_PTR
- push.STORAGE_PTR exec.active_note::get_storage
+ push.NUM_ITEMS_SET_POLICY push.STORAGE_PTR exec.active_note::get_bounded_storage
# => [num_storage_items]
mem_load.SELECTOR_PTR
diff --git a/crates/miden-standards/asm/standards/notes/fee_sponsorship.masm b/crates/miden-standards/asm/standards/notes/fee_sponsorship.masm
index 6578d74ab7..013b1405e7 100644
--- a/crates/miden-standards/asm/standards/notes/fee_sponsorship.masm
+++ b/crates/miden-standards/asm/standards/notes/fee_sponsorship.masm
@@ -15,7 +15,7 @@ use {AccountId, BlockNumber, NoteRecipient, NoteId, NoteTag, NoteType} from mide
#! The memory address at which the note storage is written.
const STORAGE_PTR = 0
-# Note storage layout (7 items, loaded at STORAGE_PTR by get_storage):
+# Note storage layout (7 items, loaded at STORAGE_PTR by get_bounded_storage):
# - FEATURE_NOTE_ID [0..3] : ID of the feature note this sponsorship pays for
# - reclaimer_suffix [4] : suffix of the account allowed to reclaim the note
# - reclaimer_prefix [5] : prefix of the account allowed to reclaim the note
@@ -138,7 +138,7 @@ pub proc main
dropw
# => [pad(16)]
- push.STORAGE_PTR exec.active_note::get_storage
+ push.NUM_STORAGE_ITEMS push.STORAGE_PTR exec.active_note::get_bounded_storage
# => [num_storage_items, pad(16)]
eq.NUM_STORAGE_ITEMS assert.err=ERR_FEE_SPONSORSHIP_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS
diff --git a/crates/miden-standards/asm/standards/notes/min_burn_amount_config.masm b/crates/miden-standards/asm/standards/notes/min_burn_amount_config.masm
index ffd20275f2..07cc4a58bd 100644
--- a/crates/miden-standards/asm/standards/notes/min_burn_amount_config.masm
+++ b/crates/miden-standards/asm/standards/notes/min_burn_amount_config.masm
@@ -5,7 +5,7 @@ use miden::standards::faucets::policies::burn::min_burn_amount
# CONSTANTS
# =================================================================================================
-# Memory base at which the note storage is written by `active_note::get_storage`.
+# Memory base at which the note storage is written by `active_note::get_bounded_storage`.
const STORAGE_PTR = 0
# Memory address of the new minimum burn amount in the MinBurnAmountConfig note storage.
@@ -63,7 +63,7 @@ pub proc main
# => [pad(16)]
# write the note storage to memory starting at STORAGE_PTR
- push.STORAGE_PTR exec.active_note::get_storage
+ push.NUM_STORAGE_ITEMS push.STORAGE_PTR exec.active_note::get_bounded_storage
# => [num_storage_items, pad(16)]
eq.NUM_STORAGE_ITEMS
diff --git a/crates/miden-standards/asm/standards/notes/network_account_config.masm b/crates/miden-standards/asm/standards/notes/network_account_config.masm
index 26febb902f..9c65bc03b8 100644
--- a/crates/miden-standards/asm/standards/notes/network_account_config.masm
+++ b/crates/miden-standards/asm/standards/notes/network_account_config.masm
@@ -6,7 +6,7 @@ use miden::standards::fees::fee_manager
# CONSTANTS
# =================================================================================================
-# Memory base at which the note storage is written by `active_note::get_storage`.
+# Memory base at which the note storage is written by `active_note::get_bounded_storage`.
const STORAGE_PTR = 0
# Memory addresses of the NetworkAccountConfig note storage. The root word occupies the first four
@@ -89,7 +89,7 @@ pub proc main
# => [pad(16)]
# write the note storage to memory starting at STORAGE_PTR
- push.STORAGE_PTR exec.active_note::get_storage
+ push.NUM_STORAGE_ITEMS push.STORAGE_PTR exec.active_note::get_bounded_storage
# => [num_storage_items, pad(16)]
mem_load.SELECTOR_PTR
diff --git a/crates/miden-standards/asm/standards/notes/owner_config.masm b/crates/miden-standards/asm/standards/notes/owner_config.masm
index b6b4c4d0a7..21d80c034a 100644
--- a/crates/miden-standards/asm/standards/notes/owner_config.masm
+++ b/crates/miden-standards/asm/standards/notes/owner_config.masm
@@ -5,7 +5,7 @@ use miden::standards::attachments::network_account_target
# CONSTANTS
# =================================================================================================
-# Memory base at which the note storage is written by `active_note::get_storage`.
+# Memory base at which the note storage is written by `active_note::get_bounded_storage`.
const STORAGE_PTR = 0
# Memory addresses of the OwnerConfig note storage.
@@ -24,6 +24,9 @@ const NUM_ITEMS_TRANSFER_OWNERSHIP = 3
const NUM_ITEMS_ACCEPT_OWNERSHIP = 1
const NUM_ITEMS_RENOUNCE_OWNERSHIP = 1
+# Largest storage item count any action uses
+const MAX_NUM_ITEMS = NUM_ITEMS_TRANSFER_OWNERSHIP
+
# ERRORS
# =================================================================================================
@@ -58,6 +61,7 @@ const ERR_OWNER_CONFIG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS = "owner config note s
#!
#! Panics if:
#! - the consuming account is not the note's target account, or the target attachment is missing.
+#! - the number of storage items exceeds MAX_NUM_ITEMS.
#! - the selector is not a known action.
#! - the number of storage items does not match the selected action.
#! - the note sender is not authorized for the selected action (per the `Ownable2Step` procedures).
@@ -73,7 +77,7 @@ pub proc main
# => [pad(16)]
# write the note storage to memory starting at STORAGE_PTR
- push.STORAGE_PTR exec.active_note::get_storage
+ push.MAX_NUM_ITEMS push.STORAGE_PTR exec.active_note::get_bounded_storage
# => [num_storage_items]
mem_load.SELECTOR_PTR
diff --git a/crates/miden-standards/asm/standards/notes/p2id.masm b/crates/miden-standards/asm/standards/notes/p2id.masm
index d377650b7a..7e65d8141e 100644
--- a/crates/miden-standards/asm/standards/notes/p2id.masm
+++ b/crates/miden-standards/asm/standards/notes/p2id.masm
@@ -14,6 +14,9 @@ const ERR_P2ID_TARGET_ACCT_MISMATCH="P2ID's target account address and transacti
# CONSTANTS
# =================================================================================================
+# The number of storage items of a P2ID note.
+const NUM_STORAGE_ITEMS = 2
+
const STORAGE_PTR = 0
const TARGET_ACCOUNT_ID_SUFFIX_PTR = STORAGE_PTR
const TARGET_ACCOUNT_ID_PREFIX_PTR = STORAGE_PTR + 1
@@ -42,11 +45,11 @@ const TARGET_ACCOUNT_ID_PREFIX_PTR = STORAGE_PTR + 1
@note_script
pub proc main
# store the note storage to memory starting at address 0
- push.STORAGE_PTR exec.active_note::get_storage
+ push.NUM_STORAGE_ITEMS push.STORAGE_PTR exec.active_note::get_bounded_storage
# => [num_storage_items]
- # make sure the number of storage items is 2
- eq.2 assert.err=ERR_P2ID_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS
+ # make sure the number of storage items is NUM_STORAGE_ITEMS
+ eq.NUM_STORAGE_ITEMS assert.err=ERR_P2ID_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS
# => []
# read the target account ID from the note storage
diff --git a/crates/miden-standards/asm/standards/notes/p2ide.masm b/crates/miden-standards/asm/standards/notes/p2ide.masm
index 1d5e918ce0..194d581cc5 100644
--- a/crates/miden-standards/asm/standards/notes/p2ide.masm
+++ b/crates/miden-standards/asm/standards/notes/p2ide.masm
@@ -9,7 +9,7 @@ use miden::standards::wallets::basic as basic_wallet
const NUM_STORAGE_ITEMS=6
-# Note storage layout (6 felts, loaded at STORAGE_PTR by get_storage):
+# Note storage layout (6 felts, loaded at STORAGE_PTR by get_bounded_storage):
# - reclaimer_suffix [0] : suffix of the account allowed to reclaim the note
# - reclaimer_prefix [1] : prefix of the account allowed to reclaim the note
# - target_suffix [2] : suffix of the account for which the note is intended
@@ -129,7 +129,7 @@ end
@note_script
pub proc main
# store the note storage to memory starting at STORAGE_PTR
- push.STORAGE_PTR exec.active_note::get_storage
+ push.NUM_STORAGE_ITEMS push.STORAGE_PTR exec.active_note::get_bounded_storage
# => [num_storage_items]
# make sure the number of storage items is 6
diff --git a/crates/miden-standards/asm/standards/notes/pause_config.masm b/crates/miden-standards/asm/standards/notes/pause_config.masm
index 2859fea29a..629b48331b 100644
--- a/crates/miden-standards/asm/standards/notes/pause_config.masm
+++ b/crates/miden-standards/asm/standards/notes/pause_config.masm
@@ -5,7 +5,7 @@ use miden::standards::attachments::network_account_target
# CONSTANTS
# =================================================================================================
-# Memory base at which the note storage is written by `active_note::get_storage`.
+# Memory base at which the note storage is written by `active_note::get_bounded_storage`.
const STORAGE_PTR = 0
# Memory address of the PauseConfig note storage selector.
@@ -20,6 +20,9 @@ const SELECTOR_UNPAUSE = 1
const NUM_ITEMS_PAUSE = 1
const NUM_ITEMS_UNPAUSE = 1
+# Largest storage item count any action uses
+const MAX_NUM_ITEMS = NUM_ITEMS_PAUSE
+
# ERRORS
# =================================================================================================
@@ -51,6 +54,7 @@ const ERR_PAUSE_CONFIG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS = "pause config note s
#!
#! Panics if:
#! - the consuming account is not the note's target account, or the target attachment is missing.
+#! - the number of storage items exceeds MAX_NUM_ITEMS.
#! - the selector is not a known action.
#! - the number of storage items does not match the selected action.
#! - the caller is not authorized for the selected action per the installed `Authority` component.
@@ -66,7 +70,7 @@ pub proc main
# => [pad(16)]
# write the note storage to memory starting at STORAGE_PTR
- push.STORAGE_PTR exec.active_note::get_storage
+ push.MAX_NUM_ITEMS push.STORAGE_PTR exec.active_note::get_bounded_storage
# => [num_storage_items]
mem_load.SELECTOR_PTR
diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm
index 62bb20873a..3752be1512 100644
--- a/crates/miden-standards/asm/standards/notes/pswap.masm
+++ b/crates/miden-standards/asm/standards/notes/pswap.masm
@@ -23,7 +23,7 @@ const MAX_U32=0x0000000100000000
# StandardNoteAttachment::PswapAttachment in the Rust standards crate.
const PSWAP_ATTACHMENT_SCHEME = 3
-# Note storage layout (7 felts, loaded at STORAGE_PTR by get_storage):
+# Note storage layout (7 felts, loaded at STORAGE_PTR by get_bounded_storage):
# - requested_faucet_suffix [0] : 1 felt
# - requested_faucet_prefix [1] : 1 felt
# - min_requested_amount [2] : 1 felt
@@ -816,7 +816,7 @@ pub proc main
# => [account_fill_amount, note_fill_amount]
# Load all note storage items to memory starting at address 0
- push.STORAGE_PTR exec.active_note::get_storage
+ push.NUM_STORAGE_ITEMS push.STORAGE_PTR exec.active_note::get_bounded_storage
# => [num_storage_items, account_fill_amount, note_fill_amount]
eq.NUM_STORAGE_ITEMS assert.err=ERR_PSWAP_WRONG_NUMBER_OF_STORAGE_ITEMS
diff --git a/crates/miden-standards/asm/standards/notes/rbac_config.masm b/crates/miden-standards/asm/standards/notes/rbac_config.masm
index 3f6cb5551a..876b74f69a 100644
--- a/crates/miden-standards/asm/standards/notes/rbac_config.masm
+++ b/crates/miden-standards/asm/standards/notes/rbac_config.masm
@@ -5,7 +5,7 @@ use miden::standards::attachments::network_account_target
# CONSTANTS
# =================================================================================================
-# Memory base at which the note storage is written by `active_note::get_storage`.
+# Memory base at which the note storage is written by `active_note::get_bounded_storage`.
const STORAGE_PTR = 0
# Memory addresses of the RbacConfig note storage. `ROLE_SYMBOL_PTR` is shared by every action;
@@ -30,6 +30,9 @@ const NUM_ITEMS_REVOKE_ROLE = 4
const NUM_ITEMS_SET_ROLE_ADMIN = 3
const NUM_ITEMS_RENOUNCE_ROLE = 2
+# Largest storage item count any action uses
+const MAX_NUM_ITEMS = NUM_ITEMS_GRANT_ROLE
+
# ERRORS
# =================================================================================================
@@ -64,6 +67,7 @@ const ERR_RBAC_CONFIG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS = "rbac config note sto
#!
#! Panics if:
#! - the consuming account is not the note's target account, or the target attachment is missing.
+#! - the number of storage items exceeds MAX_NUM_ITEMS.
#! - the selector is not a known action.
#! - the number of storage items does not match the selected action.
#! - the note sender is not authorized for the selected action (per the `rbac` procedures).
@@ -79,7 +83,7 @@ pub proc main
# => [pad(16)]
# write the note storage to memory starting at STORAGE_PTR
- push.STORAGE_PTR exec.active_note::get_storage
+ push.MAX_NUM_ITEMS push.STORAGE_PTR exec.active_note::get_bounded_storage
# => [num_storage_items]
mem_load.SELECTOR_PTR
diff --git a/crates/miden-standards/asm/standards/notes/swap.masm b/crates/miden-standards/asm/standards/notes/swap.masm
index ad7e800828..a218913ebd 100644
--- a/crates/miden-standards/asm/standards/notes/swap.masm
+++ b/crates/miden-standards/asm/standards/notes/swap.masm
@@ -9,7 +9,7 @@ use miden::standards::wallets::basic as wallet
const SWAP_NOTE_NUM_STORAGE_ITEMS=16
-# Note storage layout (16 felts, loaded at STORAGE_PTR by get_storage):
+# Note storage layout (16 felts, loaded at STORAGE_PTR by get_bounded_storage):
# - requested_asset_id [0..3]
# - requested_asset_value [4..7]
# - payback_recipient [8..11] (private mode only; zero in public mode)
@@ -69,7 +69,7 @@ pub proc main
# => []
# store note storage into memory starting at STORAGE_PTR
- push.STORAGE_PTR exec.active_note::get_storage
+ push.SWAP_NOTE_NUM_STORAGE_ITEMS push.STORAGE_PTR exec.active_note::get_bounded_storage
# => [num_storage_items]
# check number of storage items
diff --git a/crates/miden-standards/asm/standards/notes/tx_fee.masm b/crates/miden-standards/asm/standards/notes/tx_fee.masm
index d75d6219ed..8f9530701a 100644
--- a/crates/miden-standards/asm/standards/notes/tx_fee.masm
+++ b/crates/miden-standards/asm/standards/notes/tx_fee.masm
@@ -20,11 +20,6 @@ const STORAGE_PTR = 0
# Must be kept in sync with the Rust `TxFeeNote::TAG` constant.
const TX_FEE_NOTE_TAG = 0xFEE
-# ERRORS
-# =================================================================================================
-
-const ERR_TX_FEE_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS = "TX_FEE note expects exactly 0 note storage items"
-
# PROCEDURES
# =================================================================================================
@@ -49,10 +44,7 @@ const ERR_TX_FEE_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS = "TX_FEE note expects exact
@note_script
pub proc main
# make sure the note carries no storage items
- push.STORAGE_PTR exec.active_note::get_storage
- # => [num_storage_items]
-
- eq.0 assert.err=ERR_TX_FEE_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS
+ push.NUM_STORAGE_ITEMS push.STORAGE_PTR exec.active_note::get_bounded_storage drop
# => []
exec.basic_wallet::move_note_assets_to_account
diff --git a/crates/miden-testing/tests/scripts/constant_fee_policy_config.rs b/crates/miden-testing/tests/scripts/constant_fee_policy_config.rs
index a92511e847..385f376b32 100644
--- a/crates/miden-testing/tests/scripts/constant_fee_policy_config.rs
+++ b/crates/miden-testing/tests/scripts/constant_fee_policy_config.rs
@@ -8,6 +8,8 @@ use std::collections::BTreeSet;
use miden_processor::crypto::random::RandomCoin;
use miden_protocol::account::{AccountId, AccountType};
use miden_protocol::asset::FungibleAsset;
+use miden_protocol::errors::MasmError;
+use miden_protocol::errors::protocol::ERR_NOTE_TOO_MANY_STORAGE_ITEMS;
use miden_protocol::note::Note;
use miden_protocol::transaction::RawOutputNote;
use miden_protocol::{Felt, Word};
@@ -137,16 +139,22 @@ async fn non_owner_config_note_is_rejected() -> anyhow::Result<()> {
}
/// A note carrying the config-note script but a storage item count other than `NUM_STORAGE_ITEMS`
-/// (too few, empty, or too many) is rejected by the script's storage-count guard before it can call
-/// `set_note_fee`. The note's script root is unchanged by the storage, so it is still allowlisted,
-/// priced, and account-targeted like a real config note; only the storage layout is malformed.
+/// (too few, empty, or too many) is rejected before it can call `set_note_fee`. An oversized note
+/// is rejected by the bound the script passes to `get_bounded_storage`, before its storage is
+/// loaded; the other counts reach the script's own storage-count guard. The note's script root is
+/// unchanged by the storage, so it is still allowlisted, priced, and account-targeted like a real
+/// config note; only the storage layout is malformed.
#[rstest]
-#[case::empty(0)]
-#[case::too_few(ConstantFeePolicyConfigNote::NUM_STORAGE_ITEMS - 4)]
-#[case::too_many(ConstantFeePolicyConfigNote::NUM_STORAGE_ITEMS + 4)]
+#[case::empty(0, ERR_CONSTANT_FEE_POLICY_CONFIG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS)]
+#[case::too_few(
+ ConstantFeePolicyConfigNote::NUM_STORAGE_ITEMS - 4,
+ ERR_CONSTANT_FEE_POLICY_CONFIG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS
+)]
+#[case::too_many(ConstantFeePolicyConfigNote::NUM_STORAGE_ITEMS + 4, ERR_NOTE_TOO_MANY_STORAGE_ITEMS)]
#[tokio::test]
async fn config_note_with_wrong_storage_item_count_is_rejected(
#[case] num_items: usize,
+ #[case] expected_error: MasmError,
) -> anyhow::Result<()> {
let owner = owner_id();
let account = build_manageable_fee_account(
@@ -168,10 +176,7 @@ async fn config_note_with_wrong_storage_item_count_is_rejected(
.execute()
.await;
- assert_transaction_executor_error!(
- result,
- ERR_CONSTANT_FEE_POLICY_CONFIG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS
- );
+ assert_transaction_executor_error!(result, expected_error);
Ok(())
}
diff --git a/crates/miden-testing/tests/scripts/faucet_policy_config.rs b/crates/miden-testing/tests/scripts/faucet_policy_config.rs
index 9a5ed3b3f6..88216bc48b 100644
--- a/crates/miden-testing/tests/scripts/faucet_policy_config.rs
+++ b/crates/miden-testing/tests/scripts/faucet_policy_config.rs
@@ -14,9 +14,10 @@ use alloc::vec::Vec;
use miden_processor::crypto::random::RandomCoin;
use miden_protocol::account::{Account, AccountBuilder, AccountId, AccountType, AssetCallbackFlag};
use miden_protocol::asset::AssetAmount;
+use miden_protocol::errors::protocol::ERR_NOTE_TOO_MANY_STORAGE_ITEMS;
use miden_protocol::note::Note;
use miden_protocol::testing::account_id::AccountIdBuilder;
-use miden_protocol::{Felt, Word};
+use miden_protocol::{Felt, MAX_NOTE_STORAGE_ITEMS, Word};
use miden_standards::account::access::{Authority, Ownable2Step};
use miden_standards::account::faucets::{FungibleFaucet, TokenName};
use miden_standards::account::policies::{
@@ -248,6 +249,30 @@ async fn wrong_storage_item_count_fails() -> anyhow::Result<()> {
Ok(())
}
+/// A note carrying more storage items than any action accepts is rejected before its storage is
+/// loaded, so the work an oversized note can impose on whoever attempts to consume it is bounded by
+/// the layout the script accepts rather than by `MAX_NOTE_STORAGE_ITEMS`.
+#[tokio::test]
+async fn oversized_storage_is_rejected_before_the_storage_is_loaded() -> anyhow::Result<()> {
+ let owner = AccountIdBuilder::new().build_with_seed([1; 32]);
+
+ let mut builder = MockChain::builder();
+ let faucet = create_faucet_with_policies(&mut builder, owner)?;
+ let mock_chain = builder.build()?;
+ let mut rng = RandomCoin::new([Felt::from(100u32); 4].into());
+
+ let storage = vec![Felt::from(0u32); MAX_NOTE_STORAGE_ITEMS];
+ let note = malformed_faucet_policy_config_note(owner, faucet.id(), storage, &mut rng)?;
+ let tx = mock_chain
+ .build_transaction(faucet.clone())
+ .unauthenticated_input_note(note)
+ .build()?;
+ let result = tx.execute().await;
+
+ assert_transaction_executor_error!(result, ERR_NOTE_TOO_MANY_STORAGE_ITEMS);
+ Ok(())
+}
+
/// The note is bound to its target faucet, so a decoy faucet cannot consume a note meant for
/// another one. The decoy carries the same `TokenPolicyManager` setup with the same owner, so the
/// sender-based authorization would pass; consuming a note targeted at a different faucet aborts at
diff --git a/crates/miden-testing/tests/scripts/min_burn_amount_config.rs b/crates/miden-testing/tests/scripts/min_burn_amount_config.rs
index 0e04a04fc5..6162e41534 100644
--- a/crates/miden-testing/tests/scripts/min_burn_amount_config.rs
+++ b/crates/miden-testing/tests/scripts/min_burn_amount_config.rs
@@ -13,9 +13,10 @@ use alloc::vec::Vec;
use miden_processor::crypto::random::RandomCoin;
use miden_protocol::account::{Account, AccountBuilder, AccountId, AccountType, AssetCallbackFlag};
use miden_protocol::asset::AssetAmount;
+use miden_protocol::errors::protocol::ERR_NOTE_TOO_MANY_STORAGE_ITEMS;
use miden_protocol::note::Note;
use miden_protocol::testing::account_id::AccountIdBuilder;
-use miden_protocol::{Felt, Word};
+use miden_protocol::{Felt, MAX_NOTE_STORAGE_ITEMS, Word};
use miden_standards::account::access::AccessControl;
use miden_standards::account::faucets::{FungibleFaucet, TokenName};
use miden_standards::account::policies::{
@@ -141,7 +142,7 @@ async fn owner_sets_min_burn_amount() -> anyhow::Result<()> {
Ok(())
}
-/// A note whose storage carries more than the single threshold item is rejected by the count
+/// A note whose storage does not carry exactly the single threshold item is rejected by the count
/// guard, before anything is written to the faucet.
#[tokio::test]
async fn wrong_storage_item_count_fails() -> anyhow::Result<()> {
@@ -153,13 +154,8 @@ async fn wrong_storage_item_count_fails() -> anyhow::Result<()> {
let mock_chain = builder.build()?;
let mut rng = RandomCoin::new([Felt::from(100u32); 4].into());
- // a threshold followed by a trailing item instead of the expected single item
- let note = malformed_min_burn_amount_config_note(
- owner,
- faucet.id(),
- vec![Felt::from(5u32), Felt::from(0u32)],
- &mut rng,
- )?;
+ // no threshold at all instead of the expected single item
+ let note = malformed_min_burn_amount_config_note(owner, faucet.id(), Vec::new(), &mut rng)?;
let result = mock_chain
.build_transaction(faucet.clone())
.unauthenticated_input_note(note)
@@ -174,6 +170,32 @@ async fn wrong_storage_item_count_fails() -> anyhow::Result<()> {
Ok(())
}
+/// A note carrying more storage items than the script accepts is rejected before its storage is
+/// loaded, so the work an oversized note can impose on whoever attempts to consume it is bounded by
+/// the layout the script accepts rather than by `MAX_NOTE_STORAGE_ITEMS`.
+#[tokio::test]
+async fn oversized_storage_is_rejected_before_the_storage_is_loaded() -> anyhow::Result<()> {
+ let owner = AccountIdBuilder::new().build_with_seed([1; 32]);
+
+ let faucet = create_faucet_with_min_burn_amount(owner)?;
+ let mut builder = MockChain::builder();
+ builder.add_account(faucet.clone())?;
+ let mock_chain = builder.build()?;
+ let mut rng = RandomCoin::new([Felt::from(100u32); 4].into());
+
+ let storage = vec![Felt::from(0u32); MAX_NOTE_STORAGE_ITEMS];
+ let note = malformed_min_burn_amount_config_note(owner, faucet.id(), storage, &mut rng)?;
+ let result = mock_chain
+ .build_transaction(faucet.clone())
+ .unauthenticated_input_note(note)
+ .build()?
+ .execute()
+ .await;
+
+ assert_transaction_executor_error!(result, ERR_NOTE_TOO_MANY_STORAGE_ITEMS);
+ Ok(())
+}
+
/// The note is bound to its target account, so a decoy account cannot consume a note meant for
/// another account. The decoy carries the same `min_burn_amount` setup with the same owner, so the
/// sender-based authorization would pass; consuming a note targeted at a different account aborts
diff --git a/crates/miden-testing/tests/scripts/ownable2step/config.rs b/crates/miden-testing/tests/scripts/ownable2step/config.rs
index 5936e03124..c80801acc9 100644
--- a/crates/miden-testing/tests/scripts/ownable2step/config.rs
+++ b/crates/miden-testing/tests/scripts/ownable2step/config.rs
@@ -3,10 +3,11 @@ extern crate alloc;
use alloc::vec::Vec;
use miden_processor::crypto::random::RandomCoin;
-use miden_protocol::Felt;
use miden_protocol::account::{Account, AccountId, AccountType};
+use miden_protocol::errors::protocol::ERR_NOTE_TOO_MANY_STORAGE_ITEMS;
use miden_protocol::note::Note;
use miden_protocol::testing::account_id::AccountIdBuilder;
+use miden_protocol::{Felt, MAX_NOTE_STORAGE_ITEMS};
use miden_standards::errors::standards::{
ERR_OWNER_CONFIG_TARGET_ACCOUNT_MISMATCH,
ERR_OWNER_CONFIG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS,
@@ -182,6 +183,31 @@ async fn wrong_storage_item_count_fails() -> anyhow::Result<()> {
Ok(())
}
+/// A note carrying more storage items than any action accepts is rejected before its storage is
+/// loaded, so the work an oversized note can impose on whoever attempts to consume it is bounded by
+/// the longest layout the script accepts rather than by `MAX_NOTE_STORAGE_ITEMS`.
+#[tokio::test]
+async fn oversized_storage_is_rejected_before_the_storage_is_loaded() -> anyhow::Result<()> {
+ let owner = AccountIdBuilder::new().build_with_seed([1; 32]);
+
+ let account = create_ownable_account(owner)?;
+ let mut builder = MockChain::builder();
+ builder.add_account(account.clone())?;
+ let mock_chain = builder.build()?;
+ let mut rng = RandomCoin::new([Felt::from(100u32); 4].into());
+
+ let storage = vec![Felt::from(0u32); MAX_NOTE_STORAGE_ITEMS];
+ let note = malformed_owner_config_note(owner, account.id(), storage, &mut rng)?;
+ let tx = mock_chain
+ .build_transaction(account.clone())
+ .unauthenticated_input_note(note)
+ .build()?;
+ let result = tx.execute().await;
+
+ assert_transaction_executor_error!(result, ERR_NOTE_TOO_MANY_STORAGE_ITEMS);
+ Ok(())
+}
+
/// The note is bound to its target account, so a decoy account cannot consume a note meant for
/// another account. The decoy carries the same `Ownable2Step` setup with the same owner, so the
/// sender-based authorization would pass; consuming a note targeted at a different account aborts
diff --git a/crates/miden-testing/tests/scripts/pausable/config.rs b/crates/miden-testing/tests/scripts/pausable/config.rs
index c81c5a5f49..4d01166370 100644
--- a/crates/miden-testing/tests/scripts/pausable/config.rs
+++ b/crates/miden-testing/tests/scripts/pausable/config.rs
@@ -4,9 +4,10 @@ use alloc::vec::Vec;
use miden_processor::crypto::random::RandomCoin;
use miden_protocol::account::{Account, AccountBuilder, AccountId, AccountType};
+use miden_protocol::errors::protocol::ERR_NOTE_TOO_MANY_STORAGE_ITEMS;
use miden_protocol::note::Note;
use miden_protocol::testing::account_id::AccountIdBuilder;
-use miden_protocol::{Felt, Word};
+use miden_protocol::{Felt, MAX_NOTE_STORAGE_ITEMS, Word};
use miden_standards::account::access::AccessControl;
use miden_standards::account::access::pausable::{Pausable, PausableManager, PausableStorage};
use miden_standards::errors::standards::{
@@ -156,13 +157,8 @@ async fn wrong_storage_item_count_fails() -> anyhow::Result<()> {
let mock_chain = builder.build()?;
let mut rng = RandomCoin::new([Felt::from(100u32); 4].into());
- // Pause selector (0) but two storage items instead of the expected one
- let note = malformed_pause_config_note(
- owner,
- account.id(),
- vec![Felt::from(0u32), Felt::from(0u32)],
- &mut rng,
- )?;
+ // no storage items at all instead of the single expected selector item
+ let note = malformed_pause_config_note(owner, account.id(), Vec::new(), &mut rng)?;
let tx = mock_chain
.build_transaction(account.clone())
.unauthenticated_input_note(note)
@@ -173,6 +169,31 @@ async fn wrong_storage_item_count_fails() -> anyhow::Result<()> {
Ok(())
}
+/// A note carrying more storage items than any action accepts is rejected before its storage is
+/// loaded, so the work an oversized note can impose on whoever attempts to consume it is bounded by
+/// the longest layout the script accepts rather than by `MAX_NOTE_STORAGE_ITEMS`.
+#[tokio::test]
+async fn oversized_storage_is_rejected_before_the_storage_is_loaded() -> anyhow::Result<()> {
+ let owner = AccountIdBuilder::new().build_with_seed([1; 32]);
+
+ let account = create_pausable_account(owner)?;
+ let mut builder = MockChain::builder();
+ builder.add_account(account.clone())?;
+ let mock_chain = builder.build()?;
+ let mut rng = RandomCoin::new([Felt::from(100u32); 4].into());
+
+ let storage = vec![Felt::from(0u32); MAX_NOTE_STORAGE_ITEMS];
+ let note = malformed_pause_config_note(owner, account.id(), storage, &mut rng)?;
+ let tx = mock_chain
+ .build_transaction(account.clone())
+ .unauthenticated_input_note(note)
+ .build()?;
+ let result = tx.execute().await;
+
+ assert_transaction_executor_error!(result, ERR_NOTE_TOO_MANY_STORAGE_ITEMS);
+ Ok(())
+}
+
/// The note is bound to its target account, so a decoy account cannot consume a note meant for
/// another account. The decoy carries the same `PausableManager` setup with the same owner, so the
/// sender-based authorization would pass; consuming a note targeted at a different account aborts
diff --git a/docs/src/protocol_library.md b/docs/src/protocol_library.md
index dbf2ddd6f4..03b3c3a60f 100644
--- a/docs/src/protocol_library.md
+++ b/docs/src/protocol_library.md
@@ -95,7 +95,9 @@ Active note procedures can be used to fetch data from the note that is currently
| `remove_asset` | Removes an [asset](note.md#assets) from the active note and returns the value remaining in the note, which is the empty word if the entire asset was removed.
**Inputs:** `[ASSET_ID, ASSET_VALUE]`
**Outputs:** `[FINAL_ASSET_VALUE]` | Note |
| `remove_all_assets` | Removes all remaining [assets](note.md#assets) from the active note and writes them into memory starting at the specified address.
**Inputs:** `[dest_ptr]`
**Outputs:** `[num_assets]` | Note |
| `get_recipient` | Returns the [recipient](note.md#note-recipient-restricting-consumption) of the active note.
**Inputs:** `[]`
**Outputs:** `[RECIPIENT]` | Note |
+| `get_storage_info` | Returns the [inputs](note.md#storage) commitment and length of the active note without loading the storage itself.
**Inputs:** `[]`
**Outputs:** `[NOTE_STORAGE_COMMITMENT, num_storage_items]` | Note |
| `get_storage` | Writes the note's [inputs](note.md#storage) to the specified memory address.
**Inputs:** `[dest_ptr]`
**Outputs:** `[num_storage_items, dest_ptr]` | Note |
+| `get_bounded_storage` | Writes the note's [inputs](note.md#storage) to the specified memory address, provided the note carries at most the specified number of items. Rejects a longer note before its storage is loaded.
**Inputs:** `[dest_ptr, max_num_storage_items]`
**Outputs:** `[num_storage_items]` | Note |
| `get_metadata` | Returns the [metadata](note.md#metadata) of the active note.
**Inputs:** `[]`
**Outputs:** `[METADATA]` | Note |
| `get_sender` | Returns the sender of the active note.
**Inputs:** `[]`
**Outputs:** `[sender_id_suffix, sender_id_prefix]` | Note |
| `get_serial_number` | Returns the [serial number](note.md#serial-number) of the active note.
**Inputs:** `[]`
**Outputs:** `[SERIAL_NUMBER]` | Note |
diff --git a/docs/src/transaction.md b/docs/src/transaction.md
index c46b13aadc..77f9914ce6 100644
--- a/docs/src/transaction.md
+++ b/docs/src/transaction.md
@@ -79,7 +79,7 @@ To start the transaction process, the executor fetches and prepares all the inpu
In the transaction's prologue the data is being authenticated by re-hashing the provided values and comparing them to the blockchain's data (this is how private data can be used and verified during the execution of transaction without actually revealing it to the network).
-Then the P2ID note script is being executed. The script starts by reading the note storage `active_note::get_storage` — in our case the account ID of the intended target account. It checks if the provided target account ID equals the account ID of the executing account. This is the first time the note invokes a method exposed by the `Transaction` kernel, `active_account::get_id`.
+Then the P2ID note script is being executed. The script starts by reading the note storage `active_note::get_bounded_storage` — in our case the account ID of the intended target account. It checks if the provided target account ID equals the account ID of the executing account. This is the first time the note invokes a method exposed by the `Transaction` kernel, `active_account::get_id`.
If the check passes, the note script pushes the assets it holds into the account's vault. For every asset the note contains, the script calls the `wallets::basic::receive_asset` method exposed by the account's wallet component. The `wallets::basic::receive_asset` procedure calls `native_account::add_asset`, which cannot be called from the note itself. This allows accounts to control what functionality to expose, e.g. whether the account supports receiving assets or not, and the note cannot bypass that.