diff --git a/CHANGELOG.md b/CHANGELOG.md index a6c8a7e880..019e167aab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,11 @@ - [BREAKING] Renamed `native_asset_id` to `fee_faucet_id` ([#2718](https://github.com/0xMiden/protocol/pull/2718)). - [BREAKING] Removed redundant outputs from kernel procedures: `note::write_assets_to_memory`, `active_note::get_assets`, `input_note::get_assets`, `output_note::get_assets`, `active_note::get_storage`, and `faucet::mint` no longer return values identical to their inputs ([#2523](https://github.com/0xMiden/protocol/issues/2523)). - Added PSWAP (partial swap) note for decentralized partial-fill asset exchange with remainder note re-creation ([#2636](https://github.com/0xMiden/protocol/pull/2636)). -- [BREAKING] Add support for multiple attachments per note ([#2795](https://github.com/0xMiden/protocol/pull/2795), [#2849](https://github.com/0xMiden/protocol/pull/2849)): +- [BREAKING] Add support for multiple attachments per note ([#2795](https://github.com/0xMiden/protocol/pull/2795), [#2871](https://github.com/0xMiden/protocol/pull/2871)). - [BREAKING] Renamed `set_attachment` to `add_attachment`, `set_word_attachment` to `add_word_attachment`, and `set_array_attachment` to `add_array_attachment` in `miden::protocol::output_note` ([#2795](https://github.com/0xMiden/protocol/pull/2795), [#2849](https://github.com/0xMiden/protocol/pull/2849)). - [BREAKING] Replaced `metadata_into_attachment_info` with `metadata_into_attachment_schemes` in `miden::protocol::note` ([#2795](https://github.com/0xMiden/protocol/pull/2795), [#2849](https://github.com/0xMiden/protocol/pull/2849)). - [BREAKING] All `get_metadata` procedures (`active_note`, `input_note`, `output_note`) no longer return attachments ([#2795](https://github.com/0xMiden/protocol/pull/2795), [#2849](https://github.com/0xMiden/protocol/pull/2849)). +- Added `metadata_into_tag` helper for extracting the tag from metadata. This should be used instead of extracting the tag manually from the header ([#2871](https://github.com/0xMiden/protocol/pull/2871)). ### Changes diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/constants.masm b/crates/miden-protocol/asm/kernels/transaction/lib/constants.masm index 9c4423f9d7..49636c860e 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/constants.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/constants.masm @@ -1,8 +1,7 @@ # CONSTANTS # ================================================================================================= -# The number of elements in a Word -pub const WORD_SIZE = 4 +pub use $kernel::util::constants::WORD_NUM_ELEMENTS # The maximum number of storage items associated with a single note. pub const MAX_NOTE_STORAGE_ITEMS = 1024 diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm b/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm index 2752d7c478..209054865b 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm @@ -1,7 +1,7 @@ use $kernel::constants::ACCOUNT_PROCEDURE_DATA_LENGTH use $kernel::constants::MAX_ASSETS_PER_NOTE use $kernel::constants::NOTE_MEM_SIZE -use $kernel::constants::WORD_SIZE +use $kernel::constants::WORD_NUM_ELEMENTS # use $kernel::types::AccountId use miden::core::mem @@ -270,16 +270,17 @@ const OUTPUT_NOTE_SECTION_OFFSET=16777216 # The offsets at which data of an output note is stored relative to the start of its data segment. const OUTPUT_NOTE_ID_OFFSET=0 const OUTPUT_NOTE_METADATA_HEADER_OFFSET=4 -const OUTPUT_NOTE_NUM_ATTACHMENTS_OFFSET=8 -const OUTPUT_NOTE_ATTACHMENT_0_OFFSET=12 -const OUTPUT_NOTE_ATTACHMENT_1_OFFSET=16 -const OUTPUT_NOTE_ATTACHMENT_2_OFFSET=20 -const OUTPUT_NOTE_ATTACHMENT_3_OFFSET=24 -const OUTPUT_NOTE_RECIPIENT_OFFSET=28 +const OUTPUT_NOTE_RECIPIENT_OFFSET=8 +const OUTPUT_NOTE_DIRTY_FLAG_OFFSET=12 +const OUTPUT_NOTE_NUM_ASSETS_OFFSET=13 +const OUTPUT_NOTE_NUM_ATTACHMENTS_OFFSET=14 +const OUTPUT_NOTE_TOTAL_ATTACHMENT_WORDS_OFFSET=15 +const OUTPUT_NOTE_ATTACHMENT_0_OFFSET=16 +const OUTPUT_NOTE_ATTACHMENT_1_OFFSET=20 +const OUTPUT_NOTE_ATTACHMENT_2_OFFSET=24 +const OUTPUT_NOTE_ATTACHMENT_3_OFFSET=28 const OUTPUT_NOTE_ASSETS_COMMITMENT_OFFSET=32 -const OUTPUT_NOTE_NUM_ASSETS_OFFSET=36 -const OUTPUT_NOTE_DIRTY_FLAG_OFFSET=37 -const OUTPUT_NOTE_ASSETS_OFFSET=40 +const OUTPUT_NOTE_ASSETS_OFFSET=36 # LINK MAP MEMORY # ------------------------------------------------------------------------------------------------- @@ -1884,7 +1885,7 @@ pub proc get_output_note_attachment_commitment add.OUTPUT_NOTE_ATTACHMENT_0_OFFSET # => [note_ptr + attachment_0_offset, attachment_idx] - swap mul.WORD_SIZE add + swap mul.WORD_NUM_ELEMENTS add # => [attachment_ptr] padw movup.4 mem_loadw_le @@ -1916,7 +1917,7 @@ pub proc set_output_note_attachment_commitment add.OUTPUT_NOTE_ATTACHMENT_0_OFFSET # => [note_ptr + base_offset, attachment_idx, ATTACHMENT_COMMITMENT] - swap mul.WORD_SIZE add + swap mul.WORD_NUM_ELEMENTS add # => [attachment_ptr, ATTACHMENT_COMMITMENT] mem_storew_le dropw @@ -1947,6 +1948,30 @@ pub proc set_output_note_num_attachments add.OUTPUT_NOTE_NUM_ATTACHMENTS_OFFSET mem_store end +#! Returns the total number of attachment words for the output note. +#! +#! Inputs: [note_ptr] +#! Outputs: [total_num_attachment_words] +#! +#! Where: +#! - note_ptr is the memory address at which the output note data begins. +#! - total_num_attachment_words is the total number of words across all attachments. +pub proc get_output_note_total_attachment_words + add.OUTPUT_NOTE_TOTAL_ATTACHMENT_WORDS_OFFSET mem_load +end + +#! Sets the total number of attachment words for the output note. +#! +#! Inputs: [note_ptr, total_num_attachment_words] +#! Outputs: [] +#! +#! Where: +#! - note_ptr is the memory address at which the output note data begins. +#! - total_num_attachment_words is the total number of words across all attachments. +pub proc set_output_note_total_attachment_words + add.OUTPUT_NOTE_TOTAL_ATTACHMENT_WORDS_OFFSET mem_store +end + #! Returns the number of assets in the output note. #! #! Inputs: [note_ptr] diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/note.masm b/crates/miden-protocol/asm/kernels/transaction/lib/note.masm index 06bd03cc4a..b10066e64a 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/note.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/note.masm @@ -2,13 +2,14 @@ use miden::core::crypto::hashes::poseidon2 use $kernel::asset::ASSET_SIZE use $kernel::constants::NOTE_MEM_SIZE -use $kernel::constants::WORD_SIZE +use $kernel::constants::WORD_NUM_ELEMENTS use $kernel::memory pub use $kernel::util::note::NOTE_TYPE_PUBLIC pub use $kernel::util::note::NOTE_TYPE_PRIVATE pub use $kernel::util::note::MAX_ATTACHMENT_SCHEME pub use $kernel::util::note::MAX_ATTACHMENT_WORDS +pub use $kernel::util::note::MAX_ATTACHMENT_TOTAL_WORDS pub use $kernel::util::note::ATTACHMENT_SCHEME_NONE # ERRORS @@ -102,11 +103,11 @@ proc compute_attachments_commitment dup exec.memory::get_output_note_num_attachments # => [num_attachments, note_ptr] - # end_ptr = attachment_data_ptr + num_attachments * WORD_SIZE + # end_ptr = attachment_data_ptr + num_attachments * WORD_NUM_ELEMENTS swap exec.memory::get_output_note_attachment_commitment_ptr # => [start_ptr, num_attachments] - swap mul.WORD_SIZE dup.1 add swap + swap mul.WORD_NUM_ELEMENTS dup.1 add swap # => [start_ptr, end_ptr] exec.poseidon2::hash_words diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm b/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm index e5769f9ad1..3da4e5a04d 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm @@ -8,8 +8,9 @@ use $kernel::note use $kernel::note::NOTE_TYPE_PUBLIC use $kernel::note::MAX_ATTACHMENT_SCHEME use $kernel::note::MAX_ATTACHMENT_WORDS +use $kernel::note::MAX_ATTACHMENT_TOTAL_WORDS use $kernel::constants::MAX_OUTPUT_NOTES_PER_TX -use $kernel::constants::WORD_SIZE +use $kernel::constants::WORD_NUM_ELEMENTS use $kernel::asset::ASSET_SIZE use $kernel::asset::ASSET_VALUE_MEMORY_OFFSET use miden::core::word @@ -49,6 +50,8 @@ const ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_CANNOT_BE_ZERO="attachment num_words canno const ERR_OUTPUT_NOTE_TOO_MANY_ATTACHMENTS="number of attachments per note cannot exceed 4" +const ERR_OUTPUT_NOTE_TOTAL_ATTACHMENT_WORDS_EXCEEDED="total number of attachment words per note cannot exceed 512" + const ERR_NOTE_FUNGIBLE_MAX_AMOUNT_EXCEEDED="adding a fungible asset to a note cannot exceed the max_amount of 9223372036854775807" const ERR_OUTPUT_NOTE_ATTACHMENT_COMMITMENT_MISMATCH="the computed hash of fetched attachment elements does not match the provided commitment" @@ -200,7 +203,7 @@ pub proc get_attachments_commitment # => [start_ptr, num_attachments, note_ptr] # compute start_ptr and end_ptr for the attachment commitments in memory - dup movup.2 mul.WORD_SIZE add swap + dup movup.2 mul.WORD_NUM_ELEMENTS add swap # => [start_ptr, end_ptr, note_ptr] movup.2 exec.note::compute_attachments_commitment @@ -309,13 +312,26 @@ pub proc add_attachment # validate preimage for commitment is available and number of committed words is within limits dupw exec.validate_attachment - # => [ATTACHMENT_COMMITMENT, attachment_scheme, note_idx] + # => [num_words, ATTACHMENT_COMMITMENT, attachment_scheme, note_idx] - movup.4 - # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] + movup.5 + # => [attachment_scheme, num_words, ATTACHMENT_COMMITMENT, note_idx] # get note_ptr from note_idx - movup.5 exec.memory::get_output_note_ptr + movup.6 exec.memory::get_output_note_ptr + # => [note_ptr, attachment_scheme, num_words, ATTACHMENT_COMMITMENT] + + # update and validate total attachment words + dup exec.memory::get_output_note_total_attachment_words + # => [total_num_words, note_ptr, attachment_scheme, num_words, ATTACHMENT_COMMITMENT] + + movup.3 add + # => [new_total_num_words, note_ptr, attachment_scheme, ATTACHMENT_COMMITMENT] + + dup u32lte.MAX_ATTACHMENT_TOTAL_WORDS assert.err=ERR_OUTPUT_NOTE_TOTAL_ATTACHMENT_WORDS_EXCEEDED + # => [new_total_num_words, note_ptr, attachment_scheme, ATTACHMENT_COMMITMENT] + + dup.1 exec.memory::set_output_note_total_attachment_words # => [note_ptr, attachment_scheme, ATTACHMENT_COMMITMENT] # validate current number of attachments < 4 @@ -381,7 +397,7 @@ end #! Advice map: { #! ATTACHMENT_COMMITMENT: [[ATTACHMENT_ELEMENTS]], #! } -#! Outputs: [] +#! Outputs: [num_words] #! #! Panics if: #! - the number of elements is not a multiple of 4, or num_words is zero or exceeds 256. @@ -394,11 +410,11 @@ proc validate_attachment # derive num_words from num_elements adv_push.1 u32assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_MAX_EXCEEDED - u32divmod.WORD_SIZE + u32divmod.WORD_NUM_ELEMENTS # OS => [remainder, num_words, ATTACHMENT_COMMITMENT] # AS => [[ATTACHMENT_ELEMENTS]] - # assert the number of elements is a multiple of WORD_SIZE + # assert the number of elements is a multiple of WORD_NUM_ELEMENTS eq.0 assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_MUST_BE_MULTIPLE_OF_WORD_SIZE # OS => [num_words, ATTACHMENT_COMMITMENT] # AS => [[ATTACHMENT_ELEMENTS]] @@ -413,13 +429,19 @@ proc validate_attachment # we use scratch memory because pipe_preimage_to_memory needs to write to memory, but we only # need to assert that the preimage is available, the exact content itself is unimportant + + # save num_words to return it later + dup movdn.5 + # OS => [num_words, ATTACHMENT_COMMITMENT, num_words] + # AS => [[ATTACHMENT_ELEMENTS]] + push.KERNEL_SCRATCH_PTR swap - # OS => [num_words, scratch_ptr, ATTACHMENT_COMMITMENT] + # OS => [num_words, scratch_ptr, ATTACHMENT_COMMITMENT, num_words] # AS => [[ATTACHMENT_ELEMENTS]] # validate the sequential hash over the attachment elements is ATTACHMENT_COMMITMENT exec.mem::pipe_preimage_to_memory drop - # OS => [] + # OS => [num_words] end #! Builds the provided inputs into the NOTE_METADATA_HEADER word. @@ -481,7 +503,7 @@ end #! #! Where: #! - num_attachments is the number of attachments the note had before this attachment was added. -#! - attachment_scheme is the user-defined type of the attachment. +#! - attachment_scheme is the user-defined scheme of the attachment. #! - note_ptr is the memory address at which the output note data begins. proc set_attachment_schemes # the schemes are stored as follows in the third felt: diff --git a/crates/miden-protocol/asm/protocol/active_note.masm b/crates/miden-protocol/asm/protocol/active_note.masm index 94e3914844..f2dde91b62 100644 --- a/crates/miden-protocol/asm/protocol/active_note.masm +++ b/crates/miden-protocol/asm/protocol/active_note.masm @@ -370,7 +370,7 @@ end #! Writes the attachment commitments of the active note with the specified index to memory and #! returns the pointer to the data. #! -#! This procedure allocates 16 elements of memory (max num attachments * WORD_SIZE) to store all +#! This procedure allocates 16 elements of memory (max num attachments * WORD_NUM_ELEMENTS) to store all #! potential four attachment commitments. #! #! Inputs: [] diff --git a/crates/miden-protocol/asm/protocol/input_note.masm b/crates/miden-protocol/asm/protocol/input_note.masm index 5c8cdac10d..fcdb0bd420 100644 --- a/crates/miden-protocol/asm/protocol/input_note.masm +++ b/crates/miden-protocol/asm/protocol/input_note.masm @@ -370,7 +370,7 @@ end #! Writes the attachment commitments of the input note with the specified index to memory and #! returns the pointer to the data. #! -#! This procedure allocates 16 elements of memory (max num attachments * WORD_SIZE) to store all +#! This procedure allocates 16 elements of memory (max num attachments * WORD_NUM_ELEMENTS) to store all #! potential four attachment commitments. #! #! Inputs: [note_index] diff --git a/crates/miden-protocol/asm/protocol/note.masm b/crates/miden-protocol/asm/protocol/note.masm index 93c72e9443..c11c76f6c8 100644 --- a/crates/miden-protocol/asm/protocol/note.masm +++ b/crates/miden-protocol/asm/protocol/note.masm @@ -1,4 +1,5 @@ use miden::protocol::account_id +use miden::protocol::util::constants::WORD_NUM_ELEMENTS use miden::core::crypto::hashes::poseidon2 use miden::core::mem @@ -8,6 +9,7 @@ pub use miden::protocol::util::note::NOTE_TYPE_PUBLIC pub use miden::protocol::util::note::NOTE_TYPE_PRIVATE pub use miden::protocol::util::note::MAX_ATTACHMENT_SCHEME pub use miden::protocol::util::note::MAX_ATTACHMENT_WORDS +pub use miden::protocol::util::note::MAX_ATTACHMENT_TOTAL_WORDS pub use miden::protocol::util::note::ATTACHMENT_SCHEME_NONE # ERRORS @@ -17,12 +19,6 @@ const ERR_PROLOGUE_NOTE_NUM_STORAGE_ITEMS_EXCEEDED_LIMIT="number of note storage const ERR_OUTPUT_NOTE_ATTACHMENT_IDX_OUT_OF_BOUNDS = "attachment index out of bounds" -# CONSTANTS -# ================================================================================================= - -#! The number of elements in a word. -const WORD_SIZE = 4 - # NOTE UTILITY PROCEDURES # ================================================================================================= @@ -108,7 +104,7 @@ pub proc write_attachment_commitments_to_memory # pipe_preimage_to_memory so we assume validity and only do a basic u32 assertion to protect # against invalid advice inputs. adv_push.1 u32assert.err="invalid attachment num_elements advice input" - u32div.WORD_SIZE + u32div.WORD_NUM_ELEMENTS # OS => [num_words, ATTACHMENTS_COMMITMENT, dest_ptr] # AS => [[ATTACHMENT_COMMITMENT]] @@ -147,7 +143,7 @@ pub proc write_attachment_to_memory # pipe_preimage_to_memory so we assume validity and only do a basic u32 assertion to protect # against invalid advice inputs. adv_push.1 u32assert.err="invalid attachment num_elements advice input" - u32div.WORD_SIZE + u32div.WORD_NUM_ELEMENTS # OS => [num_words, ATTACHMENT_COMMITMENT, dest_ptr] # AS => [[ATTACHMENT_ELEMENTS]] @@ -163,7 +159,7 @@ end #! Writes the attachment with the provided index from to memory and returns the pointer to the #! attachment elements. #! -#! This procedure allocates 1024 elements of memory (MAX_ATTACHMENT_WORDS * WORD_SIZE) to store +#! This procedure allocates 1024 elements of memory (MAX_ATTACHMENT_WORDS * WORD_NUM_ELEMENTS) to store #! all potential attachment elements. #! #! Inputs: [num_attachments, attachment_commitments_ptr, attachment_idx] @@ -190,8 +186,8 @@ pub proc get_attachment_ptr # => [attachment_commitments_ptr, attachment_idx] # compute the memory address of the attachment commitment: - # commitment_ptr = attachment_commitments_ptr + attachment_idx * WORD_SIZE - swap mul.WORD_SIZE add + # commitment_ptr = attachment_commitments_ptr + attachment_idx * WORD_NUM_ELEMENTS + swap mul.WORD_NUM_ELEMENTS add # => [commitment_ptr] # load the ATTACHMENT_COMMITMENT from memory @@ -387,6 +383,27 @@ pub proc metadata_into_note_type # => [note_type] end +#! Extracts the tag from the provided metadata header. +#! +#! The tag is stored in the lower 32 bits of the tag element. The upper 32 bits are reserved. +#! +#! Inputs: [METADATA_HEADER] +#! Outputs: [tag] +#! +#! Where: +#! - METADATA_HEADER is the metadata header word of a note. +#! - tag is the lower 32 bits of the tag element. +#! +#! Invocation: exec +pub proc metadata_into_tag + drop drop swap drop + # => [tag_element] + + # extract the lower 32 bits as the tag + u32split swap drop + # => [tag] +end + #! Searches the metadata header for the specified attachment scheme and returns the index of the #! first matching slot. #! diff --git a/crates/miden-protocol/asm/protocol/output_note.masm b/crates/miden-protocol/asm/protocol/output_note.masm index 822f834e23..551eb811b7 100644 --- a/crates/miden-protocol/asm/protocol/output_note.masm +++ b/crates/miden-protocol/asm/protocol/output_note.masm @@ -5,6 +5,7 @@ use miden::protocol::kernel_proc_offsets::OUTPUT_NOTE_ADD_ATTACHMENT_OFFSET use miden::protocol::kernel_proc_offsets::OUTPUT_NOTE_GET_RECIPIENT_OFFSET use miden::protocol::kernel_proc_offsets::OUTPUT_NOTE_GET_METADATA_OFFSET use miden::protocol::kernel_proc_offsets::OUTPUT_NOTE_GET_ATTACHMENTS_COMMITMENT_OFFSET +use miden::protocol::util::constants::WORD_NUM_ELEMENTS use miden::protocol::note use miden::core::crypto::hashes::poseidon2 @@ -208,6 +209,8 @@ end #! - the note index points to a non-existent output note. #! - the attachment scheme is 0 or exceeds 65534. #! - the note already has 4 attachments. +#! - the number of words in the attachment exceeds 256. +#! - the total number of attachment words in the note exceeds 512. #! #! Invocation: exec pub proc add_attachment @@ -215,7 +218,7 @@ pub proc add_attachment # => [offset, attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] # pad the stack before the syscall - push.0 movdn.8 padw padw swapdw + push.0 movdn.7 padw padw swapdw # => [offset, attachment_scheme, ATTACHMENT_COMMITMENT, note_idx, pad(9)] syscall.exec_kernel_proc @@ -235,15 +238,16 @@ end #! Outputs: [] #! #! Where: -#! - attachment_scheme is the user-defined scheme of the attachment (u16, max 65534). +#! - attachment_scheme is the user-defined scheme of the attachment. #! - ATTACHMENT is the raw attachment word. #! - note_idx is the index of the note to which the attachment is added. #! #! Panics if: #! - the procedure is called when the active account is not the native one. #! - the note index points to a non-existent output note. -#! - the attachment scheme does not fit into a u16. +#! - the attachment scheme is 0 or exceeds 65534. #! - the note already has 4 attachments. +#! - the total number of attachment words in the note exceeds 512. #! #! Invocation: exec @locals(4) @@ -281,32 +285,57 @@ pub proc add_word_attachment # => [] end -#! Adds an array attachment to the note specified by the note index. The ATTACHMENT_COMMITMENT is -#! the hash commitment to a set of elements. +#! Adds a multi-word attachment to the note specified by the note index. #! -#! Inputs: -#! Operand Stack: [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] -#! Advice map: { -#! ATTACHMENT_COMMITMENT: [[ATTACHMENT_ELEMENTS]], -#! } +#! Hashes the raw attachment words starting at attachment_ptr to produce the commitment, inserts the raw +#! elements into the advice map keyed by that commitment, then delegates to `add_attachment`. +#! +#! To add a single word as an attachment, prefer add_word_attachment. +#! +#! Inputs: [attachment_scheme, num_words, attachment_ptr, note_idx] #! Outputs: [] #! #! Where: #! - attachment_scheme is the user-defined scheme of the attachment. -#! - ATTACHMENT_COMMITMENT is the hash commitment to the attachment elements. +#! - attachment_ptr is the pointer to the first word of the raw attachment data in memory. +#! - num_words is the number of words of the attachment data. #! - note_idx is the index of the note to which the attachment is added. -#! - ATTACHMENT_ELEMENTS are the elements for which ATTACHMENT_COMMITMENT is the sequential -#! commitment. #! #! Panics if: #! - the procedure is called when the active account is not the native one. #! - the note index points to a non-existent output note. -#! - the attachment scheme does not fit into a u16. +#! - the attachment scheme is 0 or exceeds 65534. #! - the note already has 4 attachments. -#! - the num_words of the attachment is not at least 2. +#! - the number of words in the attachment exceeds 256. +#! - the total number of attachment words in the note exceeds 512. #! #! Invocation: exec -pub proc add_array_attachment +pub proc add_words_attachment + # Compute num_elements = num_words * WORD_NUM_ELEMENTS + movdn.3 mul.WORD_NUM_ELEMENTS + # => [num_elements, attachment_ptr, note_idx, attachment_scheme] + + # Compute end_ptr = attachment_ptr + num_elements + dup.1 dup.1 add + # => [end_ptr, num_elements, attachment_ptr, note_idx, attachment_scheme] + + movdn.2 dup.1 + # => [attachment_ptr, num_elements, attachment_ptr, end_ptr, note_idx, attachment_scheme] + + exec.poseidon2::hash_elements + # => [ATTACHMENT_COMMITMENT, attachment_ptr, end_ptr, note_idx, attachment_scheme] + + # Insert the raw attachment elements into the advice map keyed by the commitment + adv.insert_mem + # => [ATTACHMENT_COMMITMENT, attachment_ptr, end_ptr, note_idx, attachment_scheme] + + # Clean up attachment_ptr and end_ptr + movup.4 drop movup.4 drop + # => [ATTACHMENT_COMMITMENT, note_idx, attachment_scheme] + + movup.5 + # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] + exec.add_attachment # => [] end @@ -423,7 +452,7 @@ end #! Writes the attachment commitments from the note with note_idx to memory and returns the pointer #! to it. #! -#! This procedure allocates 16 elements of memory (max num attachments * WORD_SIZE) to store all +#! This procedure allocates 16 elements of memory (max num attachments * WORD_NUM_ELEMENTS) to store all #! potential four attachment commitments. #! #! Inputs: [note_index] diff --git a/crates/miden-protocol/asm/shared_utils/util/constants.masm b/crates/miden-protocol/asm/shared_utils/util/constants.masm new file mode 100644 index 0000000000..585a45a453 --- /dev/null +++ b/crates/miden-protocol/asm/shared_utils/util/constants.masm @@ -0,0 +1,5 @@ +# CONSTANTS +# ================================================================================================= + +#! The number of field elements in a Word. +pub const WORD_NUM_ELEMENTS = 4 diff --git a/crates/miden-protocol/asm/shared_utils/util/note.masm b/crates/miden-protocol/asm/shared_utils/util/note.masm index 3d7a505ab9..2dddecc76e 100644 --- a/crates/miden-protocol/asm/shared_utils/util/note.masm +++ b/crates/miden-protocol/asm/shared_utils/util/note.masm @@ -19,5 +19,8 @@ pub const MAX_ATTACHMENT_SCHEME=65534 #! The maximum number of words in an attachment. pub const MAX_ATTACHMENT_WORDS=256 +#! The maximum total number of words across all attachments in a note. +pub const MAX_ATTACHMENT_TOTAL_WORDS=512 + #! The reserved value to signal a `None` note attachment scheme. pub const ATTACHMENT_SCHEME_NONE = 1 diff --git a/crates/miden-protocol/src/errors/mod.rs b/crates/miden-protocol/src/errors/mod.rs index 97585356e3..2ff09d4215 100644 --- a/crates/miden-protocol/src/errors/mod.rs +++ b/crates/miden-protocol/src/errors/mod.rs @@ -33,7 +33,6 @@ use crate::block::BlockNumber; use crate::note::{ NoteAssets, NoteAttachment, - NoteAttachmentArray, NoteAttachmentScheme, NoteAttachments, NoteTag, @@ -661,16 +660,18 @@ pub enum NoteError { InvalidNoteStorageLength { expected: usize, actual: usize }, #[error("note tag requires a public note but the note is of type {0}")] PublicNoteRequired(NoteType), + #[error("note attachment content must have at least one word")] + NoteAttachmentContentEmpty, #[error( - "note attachment array must have at least {min} words, got {0}", - min = NoteAttachmentArray::MIN_NUM_WORDS + "note attachment content contains {0} words, but the maximum is {max} words", + max = NoteAttachment::MAX_NUM_WORDS )] - NoteAttachmentArrayTooFewWords(usize), + NoteAttachmentContentTooManyWords(usize), #[error( - "note attachment array contains {0} words, but the maximum is {max} words", - max = NoteAttachment::MAX_NUM_WORDS + "note attachments contain a total of {0} words, but the maximum allowed is {max} words", + max = NoteAttachments::MAX_NUM_WORDS )] - NoteAttachmentArrayTooManyWords(usize), + NoteAttachmentsTooManyWords(usize), #[error( "attachment size {0} exceeds maximum {max}", max = NoteAttachment::MAX_NUM_WORDS diff --git a/crates/miden-protocol/src/note/attachment.rs b/crates/miden-protocol/src/note/attachment.rs index 2511bc88f3..2b26ad8127 100644 --- a/crates/miden-protocol/src/note/attachment.rs +++ b/crates/miden-protocol/src/note/attachment.rs @@ -1,3 +1,6 @@ +#[cfg(test)] +mod tests; + use alloc::string::ToString; use alloc::vec::Vec; @@ -30,12 +33,6 @@ use crate::{Felt, Hasher, Word}; /// the note is private, nor is a side-channel available. The note attachment can encode those /// details. /// -/// These use cases require different amounts of data, e.g. an account ID takes up just two felts -/// while the details of an encrypted note require many felts. To accommodate these cases, both a -/// computationally efficient [`NoteAttachmentContent::Word`] as well as a more flexible -/// [`NoteAttachmentContent::Array`] variant are available. See the type's docs for more -/// details. -/// /// Next to the content, a note attachment can optionally specify a [`NoteAttachmentScheme`]. This /// allows a note attachment to describe itself. For example, a network account target attachment /// can be identified by a standardized type. For cases when the attachment scheme is known from @@ -64,28 +61,26 @@ impl NoteAttachment { Self { attachment_scheme, content } } - /// Creates a new note attachment with content [`NoteAttachmentContent::Word`] from the provided - /// word. - pub fn new_word(attachment_scheme: NoteAttachmentScheme, word: Word) -> Self { + /// Creates a new note attachment from a single word. + pub fn with_word(attachment_scheme: NoteAttachmentScheme, word: Word) -> Self { Self { attachment_scheme, - content: NoteAttachmentContent::new_word(word), + content: NoteAttachmentContent::new(vec![word]).expect("single word is always valid"), } } - /// Creates a new note attachment with content [`NoteAttachmentContent::Array`] from the - /// provided words. + /// Creates a new note attachment from the provided words. /// /// # Errors /// /// Returns an error if: - /// - The number of words is less than [`NoteAttachmentArray::MIN_NUM_WORDS`]. + /// - `words` is empty. /// - The number of words exceeds [`NoteAttachment::MAX_NUM_WORDS`]. - pub fn new_array( + pub fn with_words( attachment_scheme: NoteAttachmentScheme, words: Vec, ) -> Result { - NoteAttachmentContent::new_array(words).map(|content| Self { attachment_scheme, content }) + NoteAttachmentContent::new(words).map(|content| Self { attachment_scheme, content }) } // ACCESSORS @@ -106,10 +101,12 @@ impl NoteAttachment { self.content().to_commitment() } - /// Returns the size of this attachment in words. - /// - /// - `1` indicates a single word attachment ([`NoteAttachmentContent::Word`]). - /// - `> 1` indicates an array attachment ([`NoteAttachmentContent::Array`]). + /// Returns the raw elements of this attachment content. + pub fn to_elements(&self) -> Vec { + self.content().to_elements() + } + + /// Returns the size of this attachment in words (1 to [`Self::MAX_NUM_WORDS`]). pub fn num_words(&self) -> u16 { self.content.num_words() } @@ -140,62 +137,51 @@ impl Deserializable for NoteAttachment { /// The content of a [`NoteAttachment`]. /// -/// When a single [`Word`] has sufficient space, [`NoteAttachmentContent::Word`] should be used. -/// -/// If the space of a [`Word`] is insufficient, the more flexible -/// [`NoteAttachmentContent::Array`] variant can be used. It contains a set of field elements -/// where only their sequential hash is encoded into the [`NoteMetadata`](super::NoteMetadata). +/// Contains between 1 and [`NoteAttachment::MAX_NUM_WORDS`] words of data. The commitment is +/// the sequential hash over the flattened field elements and is cached at construction time. #[derive(Debug, Clone, PartialEq, Eq)] -pub enum NoteAttachmentContent { - /// A note attachment consisting of a single [`Word`]. - Word(Word), - - /// A note attachment consisting of the commitment to a set of felts. - Array(NoteAttachmentArray), +pub struct NoteAttachmentContent { + words: Vec, + commitment: Word, } impl NoteAttachmentContent { // CONSTRUCTORS // -------------------------------------------------------------------------------------------- - /// Creates a new [`NoteAttachmentContent::Word`] from the provided word. - pub fn new_word(word: Word) -> Self { - Self::Word(word) - } - - /// Creates a new [`NoteAttachmentContent::Array`] from the provided words. + /// Creates a new [`NoteAttachmentContent`] from the provided words. /// /// # Errors /// /// Returns an error if: - /// - The number of words is less than [`NoteAttachmentArray::MIN_NUM_WORDS`]. + /// - `words` is empty. /// - The number of words exceeds [`NoteAttachment::MAX_NUM_WORDS`]. - pub fn new_array(words: Vec) -> Result { - NoteAttachmentArray::new(words).map(Self::from) + pub fn new(words: Vec) -> Result { + if words.is_empty() { + return Err(NoteError::NoteAttachmentContentEmpty); + } + + if words.len() > NoteAttachment::MAX_NUM_WORDS as usize { + return Err(NoteError::NoteAttachmentContentTooManyWords(words.len())); + } + + let elements = attachment_content_to_elements(&words); + let commitment = Hasher::hash_elements(&elements); + + Ok(Self { words, commitment }) } // ACCESSORS // -------------------------------------------------------------------------------------------- - /// Returns `true` if the content is `Word`, `false` otherwise. - pub fn is_word(&self) -> bool { - matches!(self, NoteAttachmentContent::Word(_)) - } - - /// Returns `true` if the content is `Array`, `false` otherwise. - pub fn is_array(&self) -> bool { - matches!(self, NoteAttachmentContent::Array(_)) + /// Returns a reference to the words in this attachment content. + pub fn as_words(&self) -> &[Word] { + &self.words } /// Returns the size of this attachment content in words. - /// - /// - `1` for [`NoteAttachmentContent::Word`]. - /// - `> 1` for [`NoteAttachmentContent::Array`]. pub fn num_words(&self) -> u16 { - match self { - NoteAttachmentContent::Word(_) => 1, - NoteAttachmentContent::Array(array) => array.num_words(), - } + u16::try_from(self.words.len()).expect("num words should fit in u16") } /// Returns the raw elements of this attachment content. @@ -216,25 +202,11 @@ impl Serializable for NoteAttachmentContent { u8::try_from(self.num_words().checked_sub(1).expect("num_words should be at least 1")) .expect("num_words - 1 should fit in u8"); num_words_minus_1.write_into(target); - - match self { - NoteAttachmentContent::Word(word) => { - word.write_into(target); - }, - NoteAttachmentContent::Array(array) => { - target.write_many(array.as_words()); - }, - } + target.write_many(self.as_words()); } fn get_size_hint(&self) -> usize { - let discriminant_size = core::mem::size_of::(); - match self { - NoteAttachmentContent::Word(word) => discriminant_size + word.get_size_hint(), - NoteAttachmentContent::Array(array) => { - discriminant_size + usize::from(array.num_words()) * Word::empty().get_size_hint() - }, - } + core::mem::size_of::() + usize::from(self.num_words()) * Word::empty().get_size_hint() } } @@ -244,21 +216,9 @@ impl Deserializable for NoteAttachmentContent { let num_words_minus_1 = u8::read_from(source)?; let num_words = u16::from(num_words_minus_1) + 1; - match num_words { - 0 => Err(DeserializationError::InvalidValue( - "attachment content num_words must be > 0".into(), - )), - 1 => { - let word = Word::read_from(source)?; - Ok(NoteAttachmentContent::Word(word)) - }, - _ => { - let words: Vec = - source.read_many_iter(num_words as usize)?.collect::>()?; - Self::new_array(words) - .map_err(|err| DeserializationError::InvalidValue(err.to_string())) - }, - } + let words: Vec = + source.read_many_iter(num_words as usize)?.collect::>()?; + Self::new(words).map_err(|err| DeserializationError::InvalidValue(err.to_string())) } } @@ -266,98 +226,7 @@ impl SequentialCommit for NoteAttachmentContent { type Commitment = Word; fn to_elements(&self) -> Vec { - match self { - NoteAttachmentContent::Word(word) => word.as_elements().to_vec(), - NoteAttachmentContent::Array(array) => array.to_elements(), - } - } - - fn to_commitment(&self) -> Self::Commitment { - match self { - NoteAttachmentContent::Word(word) => Hasher::hash_elements(word.as_elements()), - NoteAttachmentContent::Array(array) => array.commitment(), - } - } -} - -// NOTE ATTACHMENT ARRAY -// ================================================================================================ - -/// The type contained in [`NoteAttachmentContent::Array`] that commits to a set of words. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct NoteAttachmentArray { - words: Vec, - commitment: Word, -} - -impl NoteAttachmentArray { - // CONSTANTS - // -------------------------------------------------------------------------------------------- - - /// The minimum number of words in a note attachment array. - /// - /// Array attachments must contain at least 2 words to distinguish them from word attachments. - pub const MIN_NUM_WORDS: u8 = 2; - - // CONSTRUCTORS - // -------------------------------------------------------------------------------------------- - - /// Creates a new [`NoteAttachmentArray`] from the provided words. - /// - /// # Errors - /// - /// Returns an error if: - /// - The number of words is less than [`Self::MIN_NUM_WORDS`]. - /// - The number of words exceeds [`NoteAttachment::MAX_NUM_WORDS`]. - pub fn new(words: Vec) -> Result { - if words.len() < Self::MIN_NUM_WORDS as usize { - return Err(NoteError::NoteAttachmentArrayTooFewWords(words.len())); - } - - if words.len() > NoteAttachment::MAX_NUM_WORDS as usize { - return Err(NoteError::NoteAttachmentArrayTooManyWords(words.len())); - } - - let elements: Vec = words.iter().flat_map(Word::as_elements).copied().collect(); - let commitment = Hasher::hash_elements(&elements); - Ok(Self { words, commitment }) - } - - // ACCESSORS - // -------------------------------------------------------------------------------------------- - - /// Returns a reference to the words this note attachment commits to. - pub fn as_words(&self) -> &[Word] { - &self.words - } - - /// Returns an iterator over the elements this note attachment commits to. - pub fn as_elements(&self) -> impl Iterator { - self.words.iter().flat_map(Word::as_elements) - } - - /// Returns the elements this note attachment commits to. - pub fn to_elements(&self) -> Vec { - ::to_elements(self) - } - - /// Returns the number of words in this note attachment array. - pub fn num_words(&self) -> u16 { - // SAFETY: constructor checks that num_words is less than or equal to 256 - u16::try_from(self.words.len()).expect("num words should fit in u16") - } - - /// Returns the commitment over the contained words. - pub fn commitment(&self) -> Word { - self.commitment - } -} - -impl SequentialCommit for NoteAttachmentArray { - type Commitment = Word; - - fn to_elements(&self) -> Vec { - self.as_elements().copied().collect() + attachment_content_to_elements(&self.words) } fn to_commitment(&self) -> Self::Commitment { @@ -365,10 +234,8 @@ impl SequentialCommit for NoteAttachmentArray { } } -impl From for NoteAttachmentContent { - fn from(array: NoteAttachmentArray) -> Self { - NoteAttachmentContent::Array(array) - } +fn attachment_content_to_elements(content: &[Word]) -> Vec { + content.iter().flat_map(Word::as_elements).copied().collect() } // NOTE ATTACHMENT SCHEME @@ -637,7 +504,7 @@ impl NoteAttachments { .sum::(); if total_num_words > Self::MAX_NUM_WORDS as usize { - return Err(NoteError::NoteAttachmentArrayTooManyWords(total_num_words)); + return Err(NoteError::NoteAttachmentsTooManyWords(total_num_words)); } Ok(Self { attachments }) @@ -754,176 +621,3 @@ impl Deserializable for NoteAttachments { Self::new(attachments).map_err(|err| DeserializationError::InvalidValue(err.to_string())) } } - -// TESTS -// ================================================================================================ - -#[cfg(test)] -mod tests { - use assert_matches::assert_matches; - - use super::*; - - #[rstest::rstest] - #[case::attachment_word(NoteAttachment::new_word(NoteAttachmentScheme::new(1)?, Word::from([3, 4, 5, 6u32])))] - #[case::attachment_array(NoteAttachment::new_array( - NoteAttachmentScheme::MAX, - vec![Word::from([1, 1, 1, 1u32]); 2], - )?)] - #[test] - fn note_attachment_serde(#[case] attachment: NoteAttachment) -> anyhow::Result<()> { - assert_eq!(attachment, NoteAttachment::read_from_bytes(&attachment.to_bytes())?); - Ok(()) - } - - #[test] - fn note_attachment_array_fails_on_too_many_words() -> anyhow::Result<()> { - let too_many_words = NoteAttachment::MAX_NUM_WORDS as usize + 1; - let words = vec![Word::from([1, 1, 1, 1u32]); too_many_words]; - let err = NoteAttachmentArray::new(words).unwrap_err(); - - assert_matches!(err, NoteError::NoteAttachmentArrayTooManyWords(len) => { - len == too_many_words - }); - - Ok(()) - } - - #[test] - fn note_attachment_array_fails_on_too_few_words() { - let words = vec![Word::from([1, 1, 1, 1u32]); 1]; - let err = NoteAttachmentArray::new(words).unwrap_err(); - assert_matches!(err, NoteError::NoteAttachmentArrayTooFewWords(1)); - } - - #[test] - fn note_attachment_scheme_max_is_valid() { - let scheme = NoteAttachmentScheme::MAX; - assert_eq!(scheme.as_u16(), 65534); - } - - #[test] - fn note_attachment_scheme_exceeding_max_fails() { - let err = NoteAttachmentScheme::new(u16::MAX).unwrap_err(); - assert_matches!(err, NoteError::NoteAttachmentSchemeExceeded(_)); - } - - #[test] - fn note_attachment_header_serde() -> anyhow::Result<()> { - let header = NoteAttachmentHeader::new(NoteAttachmentScheme::new(42)?); - let deserialized = NoteAttachmentHeader::read_from_bytes(&header.to_bytes())?; - assert_eq!(header, deserialized); - Ok(()) - } - - #[test] - fn note_attachment_header_absent() { - let header = NoteAttachmentHeader::absent(); - assert!(header.is_absent()); - assert!(header.scheme().is_none()); - } - - #[test] - fn note_attachments_up_to_max() -> anyhow::Result<()> { - let scheme = NoteAttachmentScheme::new(1)?; - let attachment = NoteAttachment::new_word(scheme, Word::from([1, 2, 3, 4u32])); - let attachments = NoteAttachments::new(vec![attachment; NoteAttachments::MAX_COUNT])?; - assert_eq!(attachments.num_attachments() as usize, NoteAttachments::MAX_COUNT); - - // Exceeding MAX_COUNT should fail. - let err = - NoteAttachments::new(vec![ - NoteAttachment::new_word(scheme, Word::from([1, 2, 3, 4u32])); - NoteAttachments::MAX_COUNT + 1 - ]) - .unwrap_err(); - assert_matches!(err, NoteError::TooManyAttachments(5)); - - Ok(()) - } - - #[test] - fn note_attachments_serde() -> anyhow::Result<()> { - let attachments = NoteAttachments::new(vec![ - NoteAttachment::new_word(NoteAttachmentScheme::new(1)?, Word::from([1, 2, 3, 4u32])), - NoteAttachment::new_array( - NoteAttachmentScheme::new(100)?, - vec![Word::from([1, 1, 1, 1u32]); 2], - )?, - ])?; - - let deserialized = NoteAttachments::read_from_bytes(&attachments.to_bytes())?; - assert_eq!(attachments, deserialized); - - Ok(()) - } - - #[test] - fn note_attachments_commitment_empty() { - let attachments = NoteAttachments::empty(); - assert_eq!(attachments.to_commitment(), Word::empty()); - } - - #[test] - fn note_attachments_commitment_single_word() -> anyhow::Result<()> { - let word = Word::from([10, 20, 30, 40u32]); - let attachments = NoteAttachments::new(vec![NoteAttachment::new_word( - NoteAttachmentScheme::new(1)?, - word, - )])?; - // Single word attachment: the attachment commitment is hash(word), so the overall - // attachments commitment is hash(hash(word)). - let word_commitment = Hasher::hash_elements(word.as_elements()); - assert_eq!( - attachments.to_commitment(), - Hasher::hash_elements(word_commitment.as_elements()) - ); - - Ok(()) - } - - #[test] - fn note_attachments_to_headers() -> anyhow::Result<()> { - let attachments = NoteAttachments::new(vec![ - NoteAttachment::new_word(NoteAttachmentScheme::new(42)?, Word::from([1, 2, 3, 4u32])), - NoteAttachment::new_array( - NoteAttachmentScheme::new(100)?, - vec![Word::from([1, 1, 1, 1u32]); 2], - )?, - ])?; - - let headers = attachments.to_headers(); - assert_eq!(headers[0].scheme(), Some(NoteAttachmentScheme::new(42)?)); - assert_eq!(headers[1].scheme(), Some(NoteAttachmentScheme::new(100)?)); - assert!(headers[2].is_absent()); - assert!(headers[3].is_absent()); - - Ok(()) - } - - #[test] - fn note_attachments_into_vec() -> anyhow::Result<()> { - let word_att = - NoteAttachment::new_word(NoteAttachmentScheme::new(1)?, Word::from([1, 2, 3, 4u32])); - let attachments = NoteAttachments::new(vec![word_att.clone()])?; - let vec = attachments.into_vec(); - assert_eq!(vec, vec![word_att]); - - Ok(()) - } - - #[test] - fn note_attachment_num_words() { - // Word => 1 - let word = NoteAttachmentContent::new_word(Word::from([1, 2, 3, 4u32])); - assert_eq!(word.num_words(), 1); - - // Array with 2 words - let array = NoteAttachmentContent::new_array(vec![Word::from([1, 1, 1, 1u32]); 2]).unwrap(); - assert_eq!(array.num_words(), 2); - - // Array with 3 words - let array = NoteAttachmentContent::new_array(vec![Word::from([1, 1, 1, 1u32]); 3]).unwrap(); - assert_eq!(array.num_words(), 3); - } -} diff --git a/crates/miden-protocol/src/note/attachment/tests.rs b/crates/miden-protocol/src/note/attachment/tests.rs new file mode 100644 index 0000000000..1c075f54ed --- /dev/null +++ b/crates/miden-protocol/src/note/attachment/tests.rs @@ -0,0 +1,157 @@ +use assert_matches::assert_matches; + +use super::*; + +#[rstest::rstest] +#[case::attachment_word(NoteAttachment::with_word(NoteAttachmentScheme::new(1)?, Word::from([3, 4, 5, 6u32])))] +#[case::attachment_words(NoteAttachment::with_words( + NoteAttachmentScheme::MAX, + vec![Word::from([1, 1, 1, 1u32]); 2], + )?)] +#[test] +fn note_attachment_serde(#[case] attachment: NoteAttachment) -> anyhow::Result<()> { + assert_eq!(attachment, NoteAttachment::read_from_bytes(&attachment.to_bytes())?); + Ok(()) +} + +#[test] +fn note_attachment_content_fails_on_too_many_words() -> anyhow::Result<()> { + let too_many_words = NoteAttachment::MAX_NUM_WORDS as usize + 1; + let words = vec![Word::from([1, 1, 1, 1u32]); too_many_words]; + let err = NoteAttachmentContent::new(words).unwrap_err(); + + assert_matches!(err, NoteError::NoteAttachmentContentTooManyWords(len) => { + len == too_many_words + }); + + Ok(()) +} + +#[test] +fn note_attachment_scheme_max_is_valid() { + let scheme = NoteAttachmentScheme::MAX; + assert_eq!(scheme.as_u16(), 65534); +} + +#[test] +fn note_attachment_scheme_exceeding_max_fails() { + let err = NoteAttachmentScheme::new(u16::MAX).unwrap_err(); + assert_matches!(err, NoteError::NoteAttachmentSchemeExceeded(_)); +} + +#[test] +fn note_attachment_header_serde() -> anyhow::Result<()> { + let header = NoteAttachmentHeader::new(NoteAttachmentScheme::new(42)?); + let deserialized = NoteAttachmentHeader::read_from_bytes(&header.to_bytes())?; + assert_eq!(header, deserialized); + Ok(()) +} + +#[test] +fn note_attachment_header_absent() { + let header = NoteAttachmentHeader::absent(); + assert!(header.is_absent()); + assert!(header.scheme().is_none()); +} + +#[test] +fn note_attachments_up_to_max() -> anyhow::Result<()> { + let scheme = NoteAttachmentScheme::new(1)?; + let attachment = NoteAttachment::with_word(scheme, Word::from([1, 2, 3, 4u32])); + let attachments = NoteAttachments::new(vec![attachment; NoteAttachments::MAX_COUNT])?; + assert_eq!(attachments.num_attachments() as usize, NoteAttachments::MAX_COUNT); + + // Exceeding MAX_COUNT should fail. + let err = + NoteAttachments::new(vec![ + NoteAttachment::with_word(scheme, Word::from([1, 2, 3, 4u32])); + NoteAttachments::MAX_COUNT + 1 + ]) + .unwrap_err(); + assert_matches!(err, NoteError::TooManyAttachments(5)); + + Ok(()) +} + +#[test] +fn note_attachments_serde() -> anyhow::Result<()> { + let attachments = NoteAttachments::new(vec![ + NoteAttachment::with_word(NoteAttachmentScheme::new(1)?, Word::from([1, 2, 3, 4u32])), + NoteAttachment::with_words( + NoteAttachmentScheme::new(100)?, + vec![Word::from([1, 1, 1, 1u32]); 2], + )?, + ])?; + + let deserialized = NoteAttachments::read_from_bytes(&attachments.to_bytes())?; + assert_eq!(attachments, deserialized); + + Ok(()) +} + +#[test] +fn note_attachments_commitment_empty() { + let attachments = NoteAttachments::empty(); + assert_eq!(attachments.to_commitment(), Word::empty()); +} + +#[test] +fn note_attachments_commitment_single_word() -> anyhow::Result<()> { + let word = Word::from([10, 20, 30, 40u32]); + let attachments = + NoteAttachments::new(vec![NoteAttachment::with_word(NoteAttachmentScheme::new(1)?, word)])?; + // Single word attachment: the attachment commitment is hash(word), so the overall + // attachments commitment is hash(hash(word)). + let word_commitment = Hasher::hash_elements(word.as_elements()); + assert_eq!( + attachments.to_commitment(), + Hasher::hash_elements(word_commitment.as_elements()) + ); + + Ok(()) +} + +#[test] +fn note_attachments_to_headers() -> anyhow::Result<()> { + let attachments = NoteAttachments::new(vec![ + NoteAttachment::with_word(NoteAttachmentScheme::new(42)?, Word::from([1, 2, 3, 4u32])), + NoteAttachment::with_words( + NoteAttachmentScheme::new(100)?, + vec![Word::from([1, 1, 1, 1u32]); 2], + )?, + ])?; + + let headers = attachments.to_headers(); + assert_eq!(headers[0].scheme(), Some(NoteAttachmentScheme::new(42)?)); + assert_eq!(headers[1].scheme(), Some(NoteAttachmentScheme::new(100)?)); + assert!(headers[2].is_absent()); + assert!(headers[3].is_absent()); + + Ok(()) +} + +#[test] +fn note_attachments_into_vec() -> anyhow::Result<()> { + let word_att = + NoteAttachment::with_word(NoteAttachmentScheme::new(1)?, Word::from([1, 2, 3, 4u32])); + let attachments = NoteAttachments::new(vec![word_att.clone()])?; + let vec = attachments.into_vec(); + assert_eq!(vec, vec![word_att]); + + Ok(()) +} + +#[test] +fn note_attachment_num_words() { + // 1 word + let content = NoteAttachmentContent::new(vec![Word::from([1, 2, 3, 4u32])]).unwrap(); + assert_eq!(content.num_words(), 1); + + // 2 words + let content = NoteAttachmentContent::new(vec![Word::from([1, 1, 1, 1u32]); 2]).unwrap(); + assert_eq!(content.num_words(), 2); + + // 3 words + let content = NoteAttachmentContent::new(vec![Word::from([1, 1, 1, 1u32]); 3]).unwrap(); + assert_eq!(content.num_words(), 3); +} diff --git a/crates/miden-protocol/src/note/header.rs b/crates/miden-protocol/src/note/header.rs index 6d8db56720..65baaa9f03 100644 --- a/crates/miden-protocol/src/note/header.rs +++ b/crates/miden-protocol/src/note/header.rs @@ -17,7 +17,7 @@ use crate::Hasher; /// Holds the strictly required, public information of a note. /// /// See [NoteId] and [NoteMetadataHeader] for additional details. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct NoteHeader { note_id: NoteId, metadata_header: NoteMetadataHeader, diff --git a/crates/miden-protocol/src/note/metadata.rs b/crates/miden-protocol/src/note/metadata.rs index ce425b7449..c0f202154d 100644 --- a/crates/miden-protocol/src/note/metadata.rs +++ b/crates/miden-protocol/src/note/metadata.rs @@ -20,7 +20,7 @@ use crate::note::{NoteAttachmentHeader, NoteAttachments}; /// /// Contains the sender, note type, and tag. For the full protocol-level encoding (including /// attachment headers and commitment computation), see [`NoteMetadataHeader`]. -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct NoteMetadata { /// The ID of the account which created the note. sender: AccountId, @@ -143,7 +143,7 @@ impl Deserializable for NoteMetadata { /// `p`. /// /// The version is hardcoded to 0 and is reserved for forward compatibility. -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct NoteMetadataHeader { metadata: NoteMetadata, attachment_headers: [NoteAttachmentHeader; NoteAttachments::MAX_COUNT], @@ -339,18 +339,18 @@ fn merge_schemes(headers: [NoteAttachmentHeader; NoteAttachments::MAX_COUNT]) -> mod tests { use super::*; - use crate::note::{NoteAttachment, NoteAttachmentArray, NoteAttachmentScheme}; + use crate::note::{NoteAttachment, NoteAttachmentScheme}; use crate::testing::account_id::ACCOUNT_ID_MAX_ONES; #[test] fn note_metadata_word_encodes_attachment_header() -> anyhow::Result<()> { let sender = AccountId::try_from(ACCOUNT_ID_MAX_ONES).unwrap(); let metadata = NoteMetadata::new(sender, NoteType::Public).with_tag(NoteTag::new(0xff)); - let attachment0 = NoteAttachment::new_word( + let attachment0 = NoteAttachment::with_word( NoteAttachmentScheme::new(1)?, Word::from([10, 20, 30, 40u32]), ); - let attachment1 = NoteAttachment::new_array( + let attachment1 = NoteAttachment::with_words( NoteAttachmentScheme::new(0xfffe)?, vec![Word::from([10, 20, 30, 40u32]), Word::from([10, 20, 30, 40u32])], )?; @@ -372,16 +372,16 @@ mod tests { #[rstest::rstest] #[case::attachment_none([])] #[case::attachment_two_words([ - NoteAttachment::new_word(NoteAttachmentScheme::none(), Word::from([3, 4, 5, 6u32])), - NoteAttachment::new_word(NoteAttachmentScheme::none(), Word::from([3, 4, 5, 6u32])), + NoteAttachment::with_word(NoteAttachmentScheme::none(), Word::from([3, 4, 5, 6u32])), + NoteAttachment::with_word(NoteAttachmentScheme::none(), Word::from([3, 4, 5, 6u32])), ])] #[case::attachment_word_and_two_arrays([ - NoteAttachment::new_word(NoteAttachmentScheme::none(), Word::from([3, 4, 5, 6u32])), - NoteAttachment::new_array( + NoteAttachment::with_word(NoteAttachmentScheme::none(), Word::from([3, 4, 5, 6u32])), + NoteAttachment::with_words( NoteAttachmentScheme::MAX, - vec![Word::from([5, 5, 5, 5u32]); NoteAttachmentArray::MIN_NUM_WORDS as usize], + vec![Word::from([5, 5, 5, 5u32]); 2], )?, - NoteAttachment::new_array( + NoteAttachment::with_words( NoteAttachmentScheme::MAX, vec![Word::from([10, 10, 10, 10u32]); NoteAttachment::MAX_NUM_WORDS as usize], )?, @@ -397,7 +397,7 @@ mod tests { let tag = NoteTag::new(u32::MAX); let metadata = NoteMetadata::new(sender, note_type).with_tag(tag); let attachments = NoteAttachments::new(attachments.into_iter().collect())?; - let metadata_header = NoteMetadataHeader::new(metadata.clone(), &attachments); + let metadata_header = NoteMetadataHeader::new(metadata, &attachments); // Metadata Roundtrip let deserialized = NoteMetadata::read_from_bytes(&metadata.to_bytes())?; diff --git a/crates/miden-protocol/src/note/mod.rs b/crates/miden-protocol/src/note/mod.rs index bf2888cc8f..fdce643c7c 100644 --- a/crates/miden-protocol/src/note/mod.rs +++ b/crates/miden-protocol/src/note/mod.rs @@ -29,7 +29,6 @@ pub use metadata::{NoteMetadata, NoteMetadataHeader}; mod attachment; pub use attachment::{ NoteAttachment, - NoteAttachmentArray, NoteAttachmentContent, NoteAttachmentHeader, NoteAttachmentScheme, @@ -248,7 +247,7 @@ impl From for PartialNote { impl From<&Note> for NoteHeader { fn from(note: &Note) -> Self { - note.header.clone() + note.header } } diff --git a/crates/miden-protocol/src/transaction/kernel/memory.rs b/crates/miden-protocol/src/transaction/kernel/memory.rs index aa8c1d2066..5382dfaaec 100644 --- a/crates/miden-protocol/src/transaction/kernel/memory.rs +++ b/crates/miden-protocol/src/transaction/kernel/memory.rs @@ -431,17 +431,23 @@ pub const INPUT_NOTE_ASSETS_OFFSET: MemoryOffset = 44; // The total number of output notes for a transaction is stored in the bookkeeping section of the // memory. Data section of each note is laid out like so: // -// ┌──────┬──────────┬──────────────┬────────────┬────────────┬────────────┬────────────┬ -// │ NOTE │ METADATA │ NUM │ ATTACHMENT │ ATTACHMENT │ ATTACHMENT │ ATTACHMENT │ -// │ ID │ HEADER │ ATTACHMENTS │ 0 │ 1 │ 2 │ 3 │ -// ├──────┼──────────┼──────────────┼────────────┼────────────┼────────────┼────────────┼ -// 0 4 8 12 16 20 24 +// ┌──────┬──────────┬───────────┬──────────────────────────┬ +// │ NOTE │ METADATA │ │ [dirty_flag, num_assets, │ +// │ ID │ HEADER │ RECIPIENT │ num_attachments, 0] │ +// ├──────┼──────────┼───────────┼──────────────────────────┼ +// 0 4 8 12 // -// ┬───────────┬────────────┬────────┬───────┬───────┬─────────┬─────┬────────┬─────────┬─────────┐ -// │ RECIPIENT │ ASSETS │ NUM │ DIRTY │ ASSET │ ASSET │ ... │ ASSET │ ASSET │ PADDING │ -// │ │ COMMITMENT │ ASSETS │ FLAG │ KEY 0 │ VALUE 0 │ │ KEY n │ VALUE n │ │ -// ┼───────────┼────────────┼────────┼───────┼───────┼─────────┼─────┼────────┼─────────┼─────────┘ -// 28 32 36 37 40 44 40 + 8n 44 + 8n +// ┬────────────┬────────────┬────────────┬────────────┬────────────┬ +// │ ATTACHMENT │ ATTACHMENT │ ATTACHMENT │ ATTACHMENT │ ASSETS │ +// │ 0 │ 1 │ 2 │ 3 │ COMMITMENT │ +// ┼────────────┼────────────┼────────────┼────────────┼────────────┼ +// 16 20 24 28 32 +// +// ┬───────┬─────────┬─────┬────────┬─────────┬─────────┐ +// │ ASSET │ ASSET │ ... │ ASSET │ ASSET │ PADDING │ +// │ KEY 0 │ VALUE 0 │ │ KEY n │ VALUE n │ │ +// ┼───────┼─────────┼─────┼────────┼─────────┼─────────┘ +// 36 40 36 + 8n 40 + 8n // // The DIRTY_FLAG is the binary flag which specifies whether the assets commitment stored in this // note is outdated. It holds 1 if some changes were made to the note assets since the last @@ -456,16 +462,17 @@ pub const OUTPUT_NOTE_SECTION_OFFSET: MemoryOffset = 16_777_216; /// The offsets at which data of an output note is stored relative to the start of its data segment. pub const OUTPUT_NOTE_ID_OFFSET: MemoryOffset = 0; pub const OUTPUT_NOTE_METADATA_HEADER_OFFSET: MemoryOffset = 4; -pub const OUTPUT_NOTE_NUM_ATTACHMENTS_OFFSET: MemoryOffset = 8; -pub const OUTPUT_NOTE_ATTACHMENT_0_OFFSET: MemoryOffset = 12; -pub const OUTPUT_NOTE_ATTACHMENT_1_OFFSET: MemoryOffset = 16; -pub const OUTPUT_NOTE_ATTACHMENT_2_OFFSET: MemoryOffset = 20; -pub const OUTPUT_NOTE_ATTACHMENT_3_OFFSET: MemoryOffset = 24; -pub const OUTPUT_NOTE_RECIPIENT_OFFSET: MemoryOffset = 28; +pub const OUTPUT_NOTE_RECIPIENT_OFFSET: MemoryOffset = 8; +pub const OUTPUT_NOTE_DIRTY_FLAG_OFFSET: MemoryOffset = 12; +pub const OUTPUT_NOTE_NUM_ASSETS_OFFSET: MemoryOffset = 13; +pub const OUTPUT_NOTE_NUM_ATTACHMENTS_OFFSET: MemoryOffset = 14; +pub const OUTPUT_NOTE_TOTAL_ATTACHMENT_WORDS_OFFSET: MemoryOffset = 15; +pub const OUTPUT_NOTE_ATTACHMENT_0_OFFSET: MemoryOffset = 16; +pub const OUTPUT_NOTE_ATTACHMENT_1_OFFSET: MemoryOffset = 20; +pub const OUTPUT_NOTE_ATTACHMENT_2_OFFSET: MemoryOffset = 24; +pub const OUTPUT_NOTE_ATTACHMENT_3_OFFSET: MemoryOffset = 28; pub const OUTPUT_NOTE_ASSET_COMMITMENT_OFFSET: MemoryOffset = 32; -pub const OUTPUT_NOTE_NUM_ASSETS_OFFSET: MemoryOffset = 36; -pub const OUTPUT_NOTE_DIRTY_FLAG_OFFSET: MemoryOffset = 37; -pub const OUTPUT_NOTE_ASSETS_OFFSET: MemoryOffset = 40; +pub const OUTPUT_NOTE_ASSETS_OFFSET: MemoryOffset = 36; // ASSETS // ------------------------------------------------------------------------------------------------ @@ -503,3 +510,13 @@ const _: () = assert!( (LINK_MAP_REGION_END_PTR - LINK_MAP_REGION_START_PTR).is_multiple_of(LINK_MAP_ENTRY_SIZE), "the link map memory range should cleanly contain a multiple of the entry size" ); + +// KERNEL SCRATCH MEMORY +// ------------------------------------------------------------------------------------------------ + +/// A scratch memory region for temporary use like computing commitments over a number of elements. +/// +/// WARNING: This memory region should be assumed to contain garbage rather than zeros initially. +/// +/// At most 1024 elements should be written to this region, though this number can be increased. +pub const KERNEL_SCRATCH_PTR: MemoryAddress = 67108864; diff --git a/crates/miden-protocol/src/transaction/outputs/notes.rs b/crates/miden-protocol/src/transaction/outputs/notes.rs index 7431178b7e..0a0773f6cc 100644 --- a/crates/miden-protocol/src/transaction/outputs/notes.rs +++ b/crates/miden-protocol/src/transaction/outputs/notes.rs @@ -361,7 +361,7 @@ pub type OutputNotes = OutputNoteCollection; pub enum OutputNote { /// A public note with full details, size-validated. Public(PublicOutputNote), - /// A private note header (for private notes). + /// A private note with a header and attachments. Private(PrivateOutputNote), } diff --git a/crates/miden-protocol/src/transaction/proven_tx.rs b/crates/miden-protocol/src/transaction/proven_tx.rs index e5973e0fd0..b6e353004d 100644 --- a/crates/miden-protocol/src/transaction/proven_tx.rs +++ b/crates/miden-protocol/src/transaction/proven_tx.rs @@ -546,7 +546,7 @@ impl From<&InputNote> for InputNoteCommitment { }, InputNote::Unauthenticated { note } => Self { nullifier: note.nullifier(), - header: Some(note.header().clone()), + header: Some(*note.header()), }, } } diff --git a/crates/miden-protocol/src/transaction/tx_header.rs b/crates/miden-protocol/src/transaction/tx_header.rs index 23a2721e88..2a9a0c1a65 100644 --- a/crates/miden-protocol/src/transaction/tx_header.rs +++ b/crates/miden-protocol/src/transaction/tx_header.rs @@ -188,7 +188,7 @@ impl From<&ExecutedTransaction> for TransactionHeader { tx.initial_account().initial_commitment(), tx.final_account().to_commitment(), tx.input_notes().to_commitments(), - tx.output_notes().iter().map(|n| n.header().clone()).collect(), + tx.output_notes().iter().map(|n| *n.header()).collect(), tx.fee(), ) } diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 4f5c270e86..3a59ce4042 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -618,11 +618,15 @@ proc execute_pswap exec.active_note::get_metadata # => [METADATA_HEADER, remaining_requested] - # where METADATA_HEADER = [sid_suf_ver, sid_pre, tag, att_ks] - # => [sid_suf_ver, sid_pre, tag, att_ks, remaining_requested] - dup.2 movdn.4 - # => [sid_suf_ver, sid_pre, tag, att_ks, tag, remaining_requested] + dupw + # => [METADATA_HEADER, METADATA_HEADER, remaining_requested] + + exec.note::metadata_into_tag + # => [tag, METADATA_HEADER, remaining_requested] + + movdn.4 + # => [METADATA_HEADER, tag, remaining_requested] exec.note::metadata_into_note_type # => [note_type, tag, remaining_requested] diff --git a/crates/miden-standards/src/account/interface/component.rs b/crates/miden-standards/src/account/interface/component.rs index c1a091b013..d8c0d109ac 100644 --- a/crates/miden-standards/src/account/interface/component.rs +++ b/crates/miden-standards/src/account/interface/component.rs @@ -269,36 +269,29 @@ impl AccountComponentInterface { }, } - let attachments = partial_note.attachments(); - // Only support one attachment per note to keep it simple. - if attachments.num_attachments() > 1 { - return Err(AccountInterfaceError::MultipleAttachmentsUnsupported); - } - - match attachments.iter().next() { - Some(attachment) => { - let attachment_scheme = attachment.attachment_scheme().as_u16() as u32; - let attachment_commitment = attachment.content().to_commitment(); + for attachment in partial_note.attachments().iter() { + let attachment_scheme = attachment.attachment_scheme().as_u16(); + let attachment_commitment = attachment.content().to_commitment(); - body.push_str(&format!( - " + body.push_str(&format!( + " + dup push.{attachment_commitment} push.{attachment_scheme} - # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx, pad(16)] + # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx, note_idx, pad(16)] exec.::miden::protocol::output_note::add_attachment - # => [pad(16)] + # => [note_idx, pad(16)] ", - )); - }, - None => { - body.push_str( - " + )); + } + + body.push_str( + " + # drop the note idx drop # => [pad(16)] ", - ); - }, - } + ); } Ok(body) diff --git a/crates/miden-standards/src/account/interface/mod.rs b/crates/miden-standards/src/account/interface/mod.rs index 8633da6213..d0e2043edb 100644 --- a/crates/miden-standards/src/account/interface/mod.rs +++ b/crates/miden-standards/src/account/interface/mod.rs @@ -161,16 +161,13 @@ impl AccountInterface { note_creation_source, ); - // Add attachment array entries to the code builder's advice map. - // For NoteAttachmentContent::Array, the commitment (to_word) is used as key - // and the array elements as value. + // Add attachment entries to the code builder's advice map. + // The commitment is used as key and the elements as value. let mut code_builder = CodeBuilder::new(); for note in output_notes { - if let Some(attachment) = note.attachments().iter().next() { - code_builder.add_advice_map_entry( - attachment.content().to_commitment(), - attachment.content().to_elements(), - ); + for attachment in note.attachments().iter() { + code_builder + .add_advice_map_entry(attachment.to_commitment(), attachment.to_elements()); } } @@ -266,6 +263,4 @@ pub enum AccountInterfaceError { "account does not contain the basic fungible faucet or basic wallet interfaces which are needed to support the send_note script generation" )] UnsupportedAccountInterface, - #[error("multiple attachments per note are not supported")] - MultipleAttachmentsUnsupported, } diff --git a/crates/miden-standards/src/account/interface/test.rs b/crates/miden-standards/src/account/interface/test.rs index dd2db4eeda..995b7cea8c 100644 --- a/crates/miden-standards/src/account/interface/test.rs +++ b/crates/miden-standards/src/account/interface/test.rs @@ -281,7 +281,7 @@ fn test_basic_wallet_custom_notes() { "; let note_script = CodeBuilder::default().compile_note_script(compatible_source_code).unwrap(); let recipient = NoteRecipient::new(serial_num, note_script, NoteStorage::default()); - let compatible_custom_note = Note::new(vault.clone(), metadata.clone(), recipient); + let compatible_custom_note = Note::new(vault.clone(), metadata, recipient); assert_eq!( NoteAccountCompatibility::Maybe, wallet_account_interface.is_compatible_with(&compatible_custom_note) @@ -367,7 +367,7 @@ fn test_basic_fungible_faucet_custom_notes() { "; let note_script = CodeBuilder::default().compile_note_script(compatible_source_code).unwrap(); let recipient = NoteRecipient::new(serial_num, note_script, NoteStorage::default()); - let compatible_custom_note = Note::new(vault.clone(), metadata.clone(), recipient); + let compatible_custom_note = Note::new(vault.clone(), metadata, recipient); assert_eq!( NoteAccountCompatibility::Maybe, faucet_account_interface.is_compatible_with(&compatible_custom_note) @@ -475,7 +475,7 @@ fn test_custom_account_custom_notes() { .compile_note_script(compatible_source_code) .unwrap(); let recipient = NoteRecipient::new(serial_num, note_script, NoteStorage::default()); - let compatible_custom_note = Note::new(vault.clone(), metadata.clone(), recipient); + let compatible_custom_note = Note::new(vault.clone(), metadata, recipient); assert_eq!( NoteAccountCompatibility::Maybe, target_account_interface.is_compatible_with(&compatible_custom_note) @@ -587,7 +587,7 @@ fn test_custom_account_multiple_components_custom_notes() { .compile_note_script(compatible_source_code) .unwrap(); let recipient = NoteRecipient::new(serial_num, note_script, NoteStorage::default()); - let compatible_custom_note = Note::new(vault.clone(), metadata.clone(), recipient); + let compatible_custom_note = Note::new(vault.clone(), metadata, recipient); assert_eq!( NoteAccountCompatibility::Maybe, target_account_interface.is_compatible_with(&compatible_custom_note) diff --git a/crates/miden-standards/src/note/network_account_target.rs b/crates/miden-standards/src/note/network_account_target.rs index 038c379578..75981aa08d 100644 --- a/crates/miden-standards/src/note/network_account_target.rs +++ b/crates/miden-standards/src/note/network_account_target.rs @@ -1,13 +1,7 @@ use miden_protocol::Word; use miden_protocol::account::AccountId; use miden_protocol::errors::{AccountIdError, NoteError}; -use miden_protocol::note::{ - NoteAttachment, - NoteAttachmentContent, - NoteAttachmentScheme, - NoteAttachments, - NoteType, -}; +use miden_protocol::note::{NoteAttachment, NoteAttachmentScheme, NoteAttachments, NoteType}; use crate::note::{NoteExecutionHint, StandardNoteAttachment}; @@ -16,7 +10,7 @@ use crate::note::{NoteExecutionHint, StandardNoteAttachment}; /// A [`NoteAttachment`] for notes targeted at network accounts. /// -/// It can be encoded to and from a [`NoteAttachmentContent::Word`] with the following layout: +/// It can be encoded to and from a single-word attachment content with the following layout: /// /// ```text /// - 0th felt: [target_id_suffix (56 bits) | 8 zero bits] @@ -81,7 +75,7 @@ impl From for NoteAttachment { word[1] = network_attachment.target_id.prefix().as_felt(); word[2] = network_attachment.exec_hint.into(); - NoteAttachment::new_word(NetworkAccountTarget::ATTACHMENT_SCHEME, word) + NoteAttachment::with_word(NetworkAccountTarget::ATTACHMENT_SCHEME, word) } } @@ -108,24 +102,25 @@ impl TryFrom<&NoteAttachment> for NetworkAccountTarget { )); } - match attachment.content() { - NoteAttachmentContent::Word(word) => { - let id_suffix = word[0]; - let id_prefix = word[1]; - let exec_hint = word[2]; + let words = attachment.content().as_words(); + if words.len() != 1 { + return Err(NetworkAccountTargetError::AttachmentContentNumWordsMismatch( + attachment.content().num_words(), + )); + } + let word = words[0]; - let target_id = AccountId::try_from_elements(id_suffix, id_prefix) - .map_err(NetworkAccountTargetError::DecodeTargetId)?; + let id_suffix = word[0]; + let id_prefix = word[1]; + let exec_hint = word[2]; - let exec_hint = NoteExecutionHint::try_from(exec_hint.as_canonical_u64()) - .map_err(NetworkAccountTargetError::DecodeExecutionHint)?; + let target_id = AccountId::try_from_elements(id_suffix, id_prefix) + .map_err(NetworkAccountTargetError::DecodeTargetId)?; - NetworkAccountTarget::new(target_id, exec_hint) - }, - _ => Err(NetworkAccountTargetError::AttachmentContentNotWord( - attachment.content().num_words(), - )), - } + let exec_hint = NoteExecutionHint::try_from(exec_hint.as_canonical_u64()) + .map_err(NetworkAccountTargetError::DecodeExecutionHint)?; + + NetworkAccountTarget::new(target_id, exec_hint) } } @@ -143,8 +138,8 @@ pub enum NetworkAccountTargetError { expected = NetworkAccountTarget::ATTACHMENT_SCHEME )] AttachmentSchemeMismatch(NoteAttachmentScheme), - #[error("attachment content is not a Word (num_words={0}, expected 1)")] - AttachmentContentNotWord(u16), + #[error("network account target expects attachment content with one word, got {0}")] + AttachmentContentNumWordsMismatch(u16), #[error("failed to decode target account ID")] DecodeTargetId(#[source] AccountIdError), #[error("failed to decode execution hint")] diff --git a/crates/miden-standards/src/note/pswap.rs b/crates/miden-standards/src/note/pswap.rs index 33ff13b3e6..a93238f14f 100644 --- a/crates/miden-standards/src/note/pswap.rs +++ b/crates/miden-standards/src/note/pswap.rs @@ -536,7 +536,7 @@ impl PswapNote { ZERO, ZERO, ]); - Ok(NoteAttachment::new_word(NoteAttachmentScheme::none(), word)) + Ok(NoteAttachment::with_word(NoteAttachmentScheme::none(), word)) } /// Creates a [`NoteAttachment`] for a remainder PSWAP note. @@ -552,7 +552,7 @@ impl PswapNote { ZERO, ZERO, ]); - Ok(NoteAttachment::new_word(NoteAttachmentScheme::none(), word)) + Ok(NoteAttachment::with_word(NoteAttachmentScheme::none(), word)) } /// Builds a payback note (P2ID) that delivers the filled assets to the swap creator. diff --git a/crates/miden-testing/src/kernel_tests/tx/test_active_note.rs b/crates/miden-testing/src/kernel_tests/tx/test_active_note.rs index 22b15546ac..ac8b33d37d 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_active_note.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_active_note.rs @@ -1,8 +1,8 @@ use alloc::string::String; use anyhow::Context; -use miden_protocol::account::Account; use miden_protocol::account::auth::AuthScheme; +use miden_protocol::account::{Account, AccountId}; use miden_protocol::asset::FungibleAsset; use miden_protocol::crypto::rand::{FeltRng, RandomCoin}; use miden_protocol::errors::tx_kernel::ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_METADATA_WHILE_NO_NOTE_BEING_PROCESSED; @@ -30,6 +30,7 @@ use miden_standards::testing::mock_account::MockAccountExt; use miden_standards::testing::note::NoteBuilder; use rstest::rstest; +use super::StackInputs; use crate::kernel_tests::tx::ExecutionOutputExt; use crate::utils::create_public_p2any_note; use crate::{ @@ -219,6 +220,36 @@ async fn test_active_note_get_note_type(#[case] note_type: NoteType) -> anyhow:: Ok(()) } +#[tokio::test] +async fn test_metadata_into_tag() -> anyhow::Result<()> { + use miden_protocol::note::{NoteAttachments, NoteMetadataHeader}; + + use crate::executor::CodeExecutor; + + let sender_id: AccountId = ACCOUNT_ID_SENDER.try_into()?; + let tag = NoteTag::new(0xabcd_1234); + let metadata = NoteMetadata::new(sender_id, NoteType::Public).with_tag(tag); + let metadata_header = NoteMetadataHeader::new(metadata, &NoteAttachments::default()); + let metadata_word = metadata_header.to_metadata_word(); + + let code = " + use miden::protocol::note + + begin + exec.note::metadata_into_tag + end + "; + + let exec_output = CodeExecutor::with_default_host() + .stack_inputs(StackInputs::new(metadata_word.as_slice())?) + .run(code) + .await?; + + assert_eq!(exec_output.get_stack_element(0), Felt::from(tag.as_u32())); + + Ok(()) +} + #[tokio::test] async fn test_active_note_get_assets() -> anyhow::Result<()> { // Creates a mockchain with an account and a note that it can consume @@ -645,8 +676,8 @@ async fn test_note_find_attachment( let input_note0 = NoteBuilder::new(account.id(), &mut rng).build()?; let input_note1 = NoteBuilder::new(account.id(), &mut rng) .note_type(NoteType::Public) - .attachment(NoteAttachment::new_word(scheme_0, word_0)) - .attachment(NoteAttachment::new_word(scheme_1, word_1)) + .attachment(NoteAttachment::with_word(scheme_0, word_0)) + .attachment(NoteAttachment::with_word(scheme_1, word_1)) .build()?; TransactionContextBuilder::new(account) diff --git a/crates/miden-testing/src/kernel_tests/tx/test_note.rs b/crates/miden-testing/src/kernel_tests/tx/test_note.rs index d1b1e44fcb..a0b2baabd0 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_note.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_note.rs @@ -404,8 +404,7 @@ async fn test_build_metadata_header() -> anyhow::Result<()> { let metadata_word = exec_output.get_stack_word(0); assert_eq!( - NoteMetadataHeader::new(test_metadata.clone(), &NoteAttachments::default()) - .to_metadata_word(), + NoteMetadataHeader::new(test_metadata, &NoteAttachments::default()).to_metadata_word(), metadata_word, "failed in iteration {iteration}" ); diff --git a/crates/miden-testing/src/kernel_tests/tx/test_output_note.rs b/crates/miden-testing/src/kernel_tests/tx/test_output_note.rs index 219680b657..995ed3187d 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_output_note.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_output_note.rs @@ -14,6 +14,8 @@ use miden_protocol::errors::tx_kernel::{ ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_MAX_EXCEEDED, ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_MUST_BE_MULTIPLE_OF_WORD_SIZE, ERR_OUTPUT_NOTE_INDEX_OUT_OF_BOUNDS, + ERR_OUTPUT_NOTE_TOO_MANY_ATTACHMENTS, + ERR_OUTPUT_NOTE_TOTAL_ATTACHMENT_WORDS_EXCEEDED, ERR_TX_NUMBER_OF_OUTPUT_NOTES_EXCEEDS_LIMIT, }; use miden_protocol::note::{ @@ -41,6 +43,7 @@ use miden_protocol::testing::account_id::{ }; use miden_protocol::testing::constants::NON_FUNGIBLE_ASSET_DATA_2; use miden_protocol::transaction::memory::{ + self, ASSET_SIZE, ASSET_VALUE_OFFSET, NOTE_MEM_SIZE, @@ -69,7 +72,13 @@ use rstest::rstest; use super::{TestSetup, setup_test}; use crate::kernel_tests::tx::ExecutionOutputExt; use crate::utils::{create_public_p2any_note, create_spawn_note}; -use crate::{Auth, MockChain, TransactionContextBuilder, assert_execution_error}; +use crate::{ + Auth, + MockChain, + TransactionContextBuilder, + assert_execution_error, + assert_transaction_executor_error, +}; #[tokio::test] async fn test_create_note() -> anyhow::Result<()> { @@ -241,16 +250,27 @@ async fn test_get_output_notes_commitment() -> anyhow::Result<()> { .tag(NoteTag::with_custom_account_target(account.id(), 2)?.as_u32()) .note_type(NoteType::Public) .add_assets([asset_2]) - .attachment(NoteAttachment::new_array( + .attachment(NoteAttachment::with_words( NoteAttachmentScheme::new(5u16)?, - vec![Word::from([42, 43, 44, 45u32]), Word::from([46, 47, 48, 49u32])], + vec![Word::from([42, 43, 44, 45u32]); NoteAttachment::MAX_NUM_WORDS as usize], )?) .build()?; - // Build the advice map entry for the array attachment's elements let attachment = output_note_2.attachments().get(0).unwrap(); - let attachment_commitment = attachment.content().to_commitment(); - let attachment_elements = attachment.content().to_elements(); + let attachment_words = attachment.content().as_words(); + let attachment_ptr = memory::KERNEL_SCRATCH_PTR as usize; + let store_attachment_words = attachment_words + .iter() + .enumerate() + .map(|(idx, word)| { + format!( + "push.{word} push.{ptr} mem_storew_le dropw", + ptr = attachment_ptr + idx * WORD_SIZE + ) + }) + .collect::>() + .join("\n "); + let num_attachment_words = attachment_words.len(); let tx_context = TransactionContextBuilder::new(account) .extend_input_notes(vec![input_note_1.clone(), input_note_2.clone()]) @@ -258,7 +278,6 @@ async fn test_get_output_notes_commitment() -> anyhow::Result<()> { RawOutputNote::Full(output_note_1.clone()), RawOutputNote::Full(output_note_2.clone()), ]) - .extend_advice_map(vec![(attachment_commitment, attachment_elements)]) .build()?; // compute expected output notes commitment @@ -306,10 +325,14 @@ async fn test_get_output_notes_commitment() -> anyhow::Result<()> { exec.output_note::add_asset # => [note_idx] - push.{ATTACHMENT2} + # Store attachment words to memory + {store_attachment_words} + + push.{attachment_ptr} + push.{num_attachment_words} push.{attachment_scheme2} - # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] - exec.output_note::add_array_attachment + # => [attachment_scheme, num_words, ptr, note_idx] + exec.output_note::add_words_attachment # => [] # compute the output notes commitment @@ -330,7 +353,8 @@ async fn test_get_output_notes_commitment() -> anyhow::Result<()> { tag_2 = output_note_2.metadata().tag(), ASSET_2_KEY = asset_2.to_key_word(), ASSET_2_VALUE = asset_2.to_value_word(), - ATTACHMENT2 = output_note_2.attachments().get(0).unwrap().content().to_commitment(), + store_attachment_words = store_attachment_words, + num_attachment_words = num_attachment_words, attachment_scheme2 = output_note_2.attachments().get(0).unwrap().attachment_scheme().as_u16(), ); @@ -1247,12 +1271,64 @@ async fn test_add_attachment_with_scheme_zero_fails() -> anyhow::Result<()> { Ok(()) } +/// Test that adding a fifth attachment to an output note fails with +/// `ERR_OUTPUT_NOTE_TOO_MANY_ATTACHMENTS`. +#[tokio::test] +async fn test_add_fifth_attachment_fails() -> anyhow::Result<()> { + let tx_script = " + use miden::protocol::output_note + use mock::util + + begin + exec.util::create_default_note + # => [note_idx] + + # add attachment 1 + dup push.1.2.3.4 push.1 + exec.output_note::add_word_attachment + # => [note_idx] + + # add attachment 2 + dup push.5.6.7.8 push.2 + exec.output_note::add_word_attachment + # => [note_idx] + + # add attachment 3 + dup push.9.10.11.12 push.3 + exec.output_note::add_word_attachment + # => [note_idx] + + # add attachment 4 + dup push.13.14.15.16 push.4 + exec.output_note::add_word_attachment + # => [note_idx] + + # add attachment 5 (should fail) + push.17.18.19.20 push.5 + exec.output_note::add_word_attachment + # => [] + end + "; + + let tx_script = CodeBuilder::with_mock_libraries().compile_tx_script(tx_script)?; + + let result = TransactionContextBuilder::with_existing_mock_account() + .tx_script(tx_script) + .build()? + .execute() + .await; + + assert_transaction_executor_error!(result, ERR_OUTPUT_NOTE_TOO_MANY_ATTACHMENTS); + + Ok(()) +} + #[tokio::test] async fn test_add_word_attachment() -> anyhow::Result<()> { let account = Account::mock(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, Auth::IncrNonce); let rng = RandomCoin::new(Word::from([1, 2, 3, 4u32])); - let attachment = - NoteAttachment::new_word(NoteAttachmentScheme::MAX, Word::from([3, 4, 5, 6u32])); + let attachment_word = Word::from([3, 4, 5, 6u32]); + let attachment = NoteAttachment::with_word(NoteAttachmentScheme::MAX, attachment_word); let output_note = RawOutputNote::Full( NoteBuilder::new(account.id(), rng).attachment(attachment.clone()).build()?, ); @@ -1279,10 +1355,10 @@ async fn test_add_word_attachment() -> anyhow::Result<()> { end ", RECIPIENT = output_note.recipient().unwrap().digest(), - note_type = output_note.metadata().note_type() as u8, + note_type = output_note.metadata().note_type().as_u8(), tag = output_note.metadata().tag().as_u32(), attachment_scheme = output_note.attachments().get(0).unwrap().attachment_scheme().as_u16(), - ATTACHMENT = Word::from([3, 4, 5, 6u32]), + ATTACHMENT = attachment_word, ); let tx_script = CodeBuilder::new().compile_tx_script(tx_script)?; @@ -1305,16 +1381,27 @@ async fn test_add_word_attachment() -> anyhow::Result<()> { } #[tokio::test] -async fn test_set_array_attachment() -> anyhow::Result<()> { +async fn test_add_words_attachment() -> anyhow::Result<()> { let account = Account::mock(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, Auth::IncrNonce); let rng = RandomCoin::new(Word::from([1, 2, 3, 4u32])); - let words = vec![Word::from([3, 4, 5, 6u32]), Word::from([7, 8, 9, 10u32])]; - let attachment = NoteAttachment::new_array(NoteAttachmentScheme::new(42)?, words.clone())?; + let words = vec![Word::from([3, 4, 5, 6u32]); NoteAttachment::MAX_NUM_WORDS as usize]; + let attachment = NoteAttachment::with_words(NoteAttachmentScheme::new(42)?, words.clone())?; let output_note = RawOutputNote::Full(NoteBuilder::new(account.id(), rng).attachment(attachment).build()?); - let attachment_commitment = output_note.attachments().get(0).unwrap().content().to_commitment(); - let elements: Vec = words.iter().flat_map(Word::as_elements).copied().collect(); + let attachment_ptr = 1024; + let store_attachment_words = words + .iter() + .enumerate() + .map(|(idx, word)| { + format!( + "push.{word} push.{ptr} mem_storew_le dropw", + ptr = attachment_ptr + idx * WORD_SIZE + ) + }) + .collect::>() + .join("\n"); + let tx_script = format!( " use miden::protocol::output_note @@ -1326,10 +1413,14 @@ async fn test_set_array_attachment() -> anyhow::Result<()> { exec.output_note::create # => [note_idx] - push.{ATTACHMENT} + # Store attachment words to memory + {store_attachment_words} + + push.{attachment_ptr} + push.{num_words} push.{attachment_scheme} - # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] - exec.output_note::add_array_attachment + # => [attachment_scheme, num_words, ptr, note_idx] + exec.output_note::add_words_attachment # => [] # truncate the stack @@ -1337,10 +1428,10 @@ async fn test_set_array_attachment() -> anyhow::Result<()> { end ", RECIPIENT = output_note.recipient().unwrap().digest(), - note_type = output_note.metadata().note_type() as u8, + note_type = output_note.metadata().note_type().as_u8(), tag = output_note.metadata().tag().as_u32(), attachment_scheme = output_note.attachments().get(0).unwrap().attachment_scheme().as_u16(), - ATTACHMENT = attachment_commitment, + num_words = words.len(), ); let tx_script = CodeBuilder::new().compile_tx_script(tx_script)?; @@ -1348,7 +1439,6 @@ async fn test_set_array_attachment() -> anyhow::Result<()> { let tx = TransactionContextBuilder::new(account) .extend_expected_output_notes(vec![output_note.clone()]) .tx_script(tx_script) - .extend_advice_map(vec![(attachment_commitment, elements)]) .build()? .execute() .await?; @@ -1470,9 +1560,9 @@ async fn test_get_attachment_commitments_ptr() -> anyhow::Result<()> { let rng = RandomCoin::new(Word::from([1, 2, 3, 4u32])); let attachment_0 = - NoteAttachment::new_word(NoteAttachmentScheme::new(1)?, Word::from([3, 4, 5, 6u32])); + NoteAttachment::with_word(NoteAttachmentScheme::new(1)?, Word::from([3, 4, 5, 6u32])); let attachment_1 = - NoteAttachment::new_word(NoteAttachmentScheme::new(2)?, Word::from([7, 8, 9, 10u32])); + NoteAttachment::with_word(NoteAttachmentScheme::new(2)?, Word::from([7, 8, 9, 10u32])); let output_note = RawOutputNote::Full( NoteBuilder::new(account.id(), rng) @@ -1571,10 +1661,15 @@ async fn test_get_attachment_ptr() -> anyhow::Result<()> { let account = Account::mock(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, Auth::IncrNonce); let rng = RandomCoin::new(Word::from([1, 2, 3, 4u32])); - let word_0 = Word::from([3, 4, 5, 6u32]); - let word_1 = Word::from([7, 8, 9, 10u32]); - let attachment_0 = NoteAttachment::new_word(NoteAttachmentScheme::new(1)?, word_0); - let attachment_1 = NoteAttachment::new_word(NoteAttachmentScheme::new(2)?, word_1); + let attachment0_word = Word::from([3, 4, 5, 6u32]); + let attachment1_word0 = Word::from([7, 8, 9, 10u32]); + let attachment1_word1 = Word::from([11, 12, 13, 14u32]); + + let attachment_0 = NoteAttachment::with_word(NoteAttachmentScheme::new(1)?, attachment0_word); + let attachment_1 = NoteAttachment::with_words( + NoteAttachmentScheme::new(2)?, + [attachment1_word0, attachment1_word1].to_vec(), + )?; let output_note = RawOutputNote::Full( NoteBuilder::new(account.id(), rng) @@ -1584,7 +1679,7 @@ async fn test_get_attachment_ptr() -> anyhow::Result<()> { ); let tx_script = format!( - " + r#" use miden::protocol::output_note use miden::core::sys @@ -1596,38 +1691,75 @@ async fn test_get_attachment_ptr() -> anyhow::Result<()> { # => [note_idx] # add first word attachment (note_idx = 0) - push.{ATTACHMENT_WORD_0} + push.{attachment0_word} push.{attachment_scheme_0} # => [attachment_scheme, ATTACHMENT, note_idx] exec.output_note::add_word_attachment # => [] - # add second word attachment + # write attachment elements to memory + push.{attachment1_word0} mem_storew_le.1024 dropw + push.{attachment1_word1} mem_storew_le.1028 dropw + # => [] + + # add second attachment push.0 - push.{ATTACHMENT_WORD_1} + push.1024 + push.{attachment1_num_words} push.{attachment_scheme_1} - # => [attachment_scheme, ATTACHMENT, note_idx=0] - exec.output_note::add_word_attachment + # => [attachment_scheme, num_words, attachment_ptr, note_idx=0] + exec.output_note::add_words_attachment # => [] - # --- get attachment 1 first (to debug with non-zero idx) --- + # --- validate attachment 0 --- + push.0 push.0 + # => [attachment_idx=0, note_idx=0] + exec.output_note::get_attachment_ptr + # => [num_words, attachment_ptr] + + eq.{attachment0_num_words} + assert.err="expected attachment 0 to have {attachment0_num_words} words" + # => [attachment_ptr] + + padw movup.4 mem_loadw_le + push.{attachment0_word} + assert_eqw.err="attachment 0 word mismatch" + + # --- validate attachment 1 --- push.0 push.1 # => [attachment_idx=1, note_idx=0] exec.output_note::get_attachment_ptr - # => [attachment_ptr, num_words] - drop drop + # => [num_words, attachment_ptr] + + eq.{attachment1_num_words} + assert.err="expected attachment 1 to have {attachment1_num_words} words" + # => [attachment_ptr] + + # validate first word in attachment_ptr + padw dup.4 mem_loadw_le + # => [ATTACHMENT1_WORD0, attachment_ptr] + push.{attachment1_word0} + assert_eqw.err="attachment 1 word 0 mismatch" + # => [attachment_ptr] + + # validate second word in attachment_ptr (offset by 4) + padw movup.4 add.4 mem_loadw_le + # => [ATTACHMENT1_WORD1] + push.{attachment1_word1} + assert_eqw.err="attachment 1 word 1 mismatch" + # => [] # truncate the stack exec.sys::truncate_stack end - ", + "#, RECIPIENT = output_note.recipient().unwrap().digest(), note_type = output_note.metadata().note_type() as u8, tag = output_note.metadata().tag().as_u32(), attachment_scheme_0 = attachment_0.attachment_scheme().as_u16(), - ATTACHMENT_WORD_0 = word_0, attachment_scheme_1 = attachment_1.attachment_scheme().as_u16(), - ATTACHMENT_WORD_1 = word_1, + attachment0_num_words = attachment_0.num_words(), + attachment1_num_words = attachment_1.num_words(), ); let tx_script = CodeBuilder::new().compile_tx_script(tx_script)?; @@ -1671,8 +1803,8 @@ async fn test_find_attachment( let output_note = NoteBuilder::new(account.id(), RandomCoin::new(Word::from([1, 2, 3, 4u32]))) .note_type(NoteType::Public) - .attachment(NoteAttachment::new_word(scheme_0, word_0)) - .attachment(NoteAttachment::new_word(scheme_1, word_1)) + .attachment(NoteAttachment::with_word(scheme_0, word_0)) + .attachment(NoteAttachment::with_word(scheme_1, word_1)) .build()?; let spawn_note = builder.add_spawn_note([&output_note])?; @@ -1741,6 +1873,66 @@ async fn test_find_attachment( Ok(()) } +#[tokio::test] +async fn test_add_attachments_with_too_many_overall_elements_fails() -> anyhow::Result<()> { + let attachment0 = NoteAttachment::with_words( + NoteAttachmentScheme::new_const(3), + vec![Word::from([1, 2, 3, 4u32]); NoteAttachment::MAX_NUM_WORDS as usize], + )?; + let attachment1 = NoteAttachment::with_words( + NoteAttachmentScheme::new_const(6), + vec![Word::from([2, 3, 4, 5u32]); NoteAttachment::MAX_NUM_WORDS as usize], + )?; + + let tx_context = TransactionContextBuilder::with_existing_mock_account() + .extend_advice_map(vec![(attachment0.to_commitment(), attachment0.content().to_elements())]) + .extend_advice_map(vec![(attachment1.to_commitment(), attachment1.content().to_elements())]) + .build()?; + + let code = format!( + " + use miden::protocol::output_note + use miden::standards::note_tag::DEFAULT_TAG + use $kernel::prologue + use mock::util + + begin + exec.prologue::prepare_transaction + + exec.util::create_default_note + # => [note_idx] + + dup push.{ATTACHMENT_0_COMMITMENT} push.{attachment0_scheme} + # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] + + exec.output_note::add_attachment + # => [note_idx] + + dup push.{ATTACHMENT_1_COMMITMENT} push.{attachment1_scheme} + # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] + + exec.output_note::add_attachment + # => [note_idx] + + # add one more word which pushes the overall limit of 512 words over the edge + push.1.2.3.4 push.5 + exec.output_note::add_word_attachment + # => [] + end + ", + attachment0_scheme = attachment0.attachment_scheme().as_u16(), + attachment1_scheme = attachment1.attachment_scheme().as_u16(), + ATTACHMENT_0_COMMITMENT = attachment0.to_commitment(), + ATTACHMENT_1_COMMITMENT = attachment1.to_commitment(), + ); + + let exec_output = tx_context.execute_code(&code).await; + + assert_execution_error!(exec_output, ERR_OUTPUT_NOTE_TOTAL_ATTACHMENT_WORDS_EXCEEDED); + + Ok(()) +} + /// Test that output_note procedures abort when given an out-of-bounds note index (equal to /// num_output_notes). /// @@ -1755,7 +1947,6 @@ async fn test_find_attachment( #[case::get_metadata(0, "get_metadata")] #[case::add_attachment(5, "add_attachment")] #[case::add_word_attachment(5, "add_word_attachment")] -#[case::add_array_attachment(5, "add_array_attachment")] #[case::find_attachment(1, "find_attachment")] #[case::get_attachment_commitments_ptr(0, "get_attachment_commitments_ptr")] #[case::get_attachments_commitment(0, "get_attachments_commitment")] diff --git a/crates/miden-testing/src/kernel_tests/tx/test_tx.rs b/crates/miden-testing/src/kernel_tests/tx/test_tx.rs index e5578003f0..eef0bbe9a7 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_tx.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_tx.rs @@ -24,7 +24,6 @@ use miden_protocol::note::{ Note, NoteAssets, NoteAttachment, - NoteAttachmentContent, NoteAttachmentScheme, NoteAttachments, NoteId, @@ -218,8 +217,8 @@ async fn executed_transaction_output_notes() -> anyhow::Result<()> { let tag3 = NoteTag::default(); let attachment2 = - NoteAttachment::new_word(NoteAttachmentScheme::new(28)?, Word::from([2, 3, 4, 5u32])); - let attachment3 = NoteAttachment::new_array( + NoteAttachment::with_word(NoteAttachmentScheme::new(28)?, Word::from([2, 3, 4, 5u32])); + let attachment3 = NoteAttachment::with_words( NoteAttachmentScheme::new(29)?, vec![Word::from([6, 7, 8, 9u32]), Word::from([10, 11, 12, 13u32])], )?; @@ -318,10 +317,15 @@ async fn executed_transaction_output_notes() -> anyhow::Result<()> { exec.output_note::create # => [note_idx = 2] - push.{ATTACHMENT3} + # Store attachment3 words to memory at address 1024 + push.{attachment3_word0} mem_storew_le.1024 dropw + push.{attachment3_word1} mem_storew_le.1028 dropw + + push.1024 + push.{num_attachment3_words} push.{attachment_scheme3} - # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] - exec.output_note::add_array_attachment + # => [attachment_scheme, num_words, ptr, note_idx] + exec.output_note::add_words_attachment # => [] end ", @@ -341,7 +345,9 @@ async fn executed_transaction_output_notes() -> anyhow::Result<()> { attachment_scheme2 = attachment2.attachment_scheme().as_u16(), ATTACHMENT2 = Word::from([2, 3, 4, 5u32]), attachment_scheme3 = attachment3.attachment_scheme().as_u16(), - ATTACHMENT3 = attachment3.content().to_commitment(), + attachment3_word0 = attachment3.content().as_words()[0], + attachment3_word1 = attachment3.content().as_words()[1], + num_attachment3_words = attachment3.content().num_words(), ); let tx_script = CodeBuilder::with_mock_libraries().compile_tx_script(tx_script_src)?; @@ -350,13 +356,10 @@ async fn executed_transaction_output_notes() -> anyhow::Result<()> { // -------------------------------------------------------------------------------------------- // execute the transaction and get the witness - let NoteAttachmentContent::Array(array) = attachment3.content() else { - panic!("expected array attachment"); - }; + assert!(attachment3.content().num_words() > 1, "expected multi-word attachment"); let tx_context = TransactionContextBuilder::new(executor_account) .tx_script(tx_script) - .extend_advice_map(vec![(attachment3.content().to_commitment(), array.to_elements())]) .extend_expected_output_notes(vec![ RawOutputNote::Full(expected_output_note_2.clone()), RawOutputNote::Full(expected_output_note_3.clone()), diff --git a/crates/miden-testing/src/mock_chain/chain.rs b/crates/miden-testing/src/mock_chain/chain.rs index f56269851b..e195acdb46 100644 --- a/crates/miden-testing/src/mock_chain/chain.rs +++ b/crates/miden-testing/src/mock_chain/chain.rs @@ -954,7 +954,7 @@ impl MockChain { created_note.id(), MockChainNote::Private( created_note.id(), - created_note.metadata().clone(), + *created_note.metadata(), note_inclusion_proof, ), ); diff --git a/crates/miden-testing/src/standards/network_account_target.rs b/crates/miden-testing/src/standards/network_account_target.rs index 4546bfc571..c1edfcb822 100644 --- a/crates/miden-testing/src/standards/network_account_target.rs +++ b/crates/miden-testing/src/standards/network_account_target.rs @@ -2,7 +2,7 @@ use miden_protocol::Felt; use miden_protocol::account::AccountStorageMode; -use miden_protocol::note::{NoteAttachment, NoteAttachmentContent}; +use miden_protocol::note::NoteAttachment; use miden_protocol::testing::account_id::AccountIdBuilder; use miden_standards::note::{NetworkAccountTarget, NoteExecutionHint}; @@ -38,10 +38,7 @@ async fn network_account_target_into_target_id() -> anyhow::Result<()> { end "#, attachment_scheme = attachment.attachment_scheme().as_u16(), - attachment_word = match attachment.content() { - NoteAttachmentContent::Word(word) => *word, - _ => unreachable!("expected word attachment"), - }, + attachment_word = attachment.content().as_words()[0], ); let exec_output = CodeExecutor::with_default_host().run(&source).await?; @@ -60,10 +57,7 @@ async fn network_account_target_new_attachment() -> anyhow::Result<()> { let exec_hint = NoteExecutionHint::Always; let attachment = NoteAttachment::from(NetworkAccountTarget::new(target_id, exec_hint)?); - let raw_attachment_word = match attachment.content() { - NoteAttachmentContent::Word(word) => *word, - _ => unreachable!("expected word attachment"), - }; + let raw_attachment_word = attachment.content().as_words()[0]; let source = format!( r#" diff --git a/crates/miden-testing/src/utils.rs b/crates/miden-testing/src/utils.rs index d38b3f8284..07c29d8f8b 100644 --- a/crates/miden-testing/src/utils.rs +++ b/crates/miden-testing/src/utils.rs @@ -202,7 +202,7 @@ where } /// Returns the code for a note that creates all notes in `output_notes`, along with an -/// advice map containing the elements for any array attachments keyed by their commitment. +/// advice map containing the elements for any attachments keyed by their commitment. fn note_script_that_creates_notes<'note>( sender_id: AccountId, output_notes: impl Iterator, diff --git a/crates/miden-testing/tests/scripts/pswap.rs b/crates/miden-testing/tests/scripts/pswap.rs index 4abd3c9193..280a89a014 100644 --- a/crates/miden-testing/tests/scripts/pswap.rs +++ b/crates/miden-testing/tests/scripts/pswap.rs @@ -5,14 +5,7 @@ use miden_protocol::account::{Account, AccountId, AccountStorageMode, AccountVau use miden_protocol::asset::{Asset, FungibleAsset}; use miden_protocol::crypto::rand::{FeltRng, RandomCoin}; use miden_protocol::errors::MasmError; -use miden_protocol::note::{ - Note, - NoteAttachment, - NoteAttachmentContent, - NoteAttachmentScheme, - NoteAttachments, - NoteType, -}; +use miden_protocol::note::{Note, NoteAttachment, NoteAttachmentScheme, NoteAttachments, NoteType}; use miden_protocol::transaction::RawOutputNote; use miden_protocol::{Felt, ONE, Word, ZERO}; use miden_standards::account::wallets::BasicWallet; @@ -41,10 +34,8 @@ const BASIC_AUTH: Auth = Auth::BasicAuth { /// Extracts the first attachment's word content from a `NoteAttachments`. fn first_attachment_word(attachments: &NoteAttachments) -> Word { let content = attachments.get(0).expect("expected at least one attachment").content(); - match content { - NoteAttachmentContent::Word(w) => *w, - NoteAttachmentContent::Array(_) => panic!("expected Word attachment, got Array"), - } + assert_eq!(content.num_words(), 1, "expected single word attachment"); + content.as_words()[0] } /// Builds a PswapNote, registers it on the builder as an output note, and returns @@ -253,7 +244,7 @@ async fn pswap_note_alice_reconstructs_and_consumes_p2id() -> anyhow::Result<()> ZERO, ]); let remainder_attachment = - NoteAttachment::new_word(NoteAttachmentScheme::none(), remainder_attachment_word); + NoteAttachment::with_word(NoteAttachmentScheme::none(), remainder_attachment_word); let reconstructed_remainder: Note = PswapNote::builder() .sender(bob.id()) .storage(remainder_storage) diff --git a/crates/miden-testing/tests/scripts/send_note.rs b/crates/miden-testing/tests/scripts/send_note.rs index ed43f4093f..e5695ca844 100644 --- a/crates/miden-testing/tests/scripts/send_note.rs +++ b/crates/miden-testing/tests/scripts/send_note.rs @@ -22,6 +22,7 @@ use miden_protocol::testing::note::DEFAULT_NOTE_SCRIPT; use miden_protocol::transaction::RawOutputNote; use miden_standards::account::interface::{AccountInterface, AccountInterfaceExt}; use miden_standards::code_builder::CodeBuilder; +use miden_standards::note::P2idNote; use miden_testing::utils::create_p2any_note; use miden_testing::{Auth, MockChain}; @@ -51,30 +52,47 @@ async fn test_send_note_script_basic_wallet() -> anyhow::Result<()> { }, [sent_asset0, total_asset], )?; + let mut rng = RandomCoin::new(Word::from([1, 2, 3, 4u32])); let p2any_note = create_p2any_note( sender_basic_wallet_account.id(), NoteType::Private, - [sent_asset2], - &mut RandomCoin::new(Word::from([1, 2, 3, 4u32])), + [sent_asset1], + &mut rng, ); let spawn_note = builder.add_spawn_note([&p2any_note])?; let mock_chain = builder.build()?; let sender_account_interface = AccountInterface::from_account(&sender_basic_wallet_account); - let tag = NoteTag::with_account_target(sender_basic_wallet_account.id()); - let words = vec![Word::from([9, 8, 7, 6u32]), Word::from([5, 4, 3, 2u32])]; - let attachment = NoteAttachment::new_array(NoteAttachmentScheme::new(42)?, words.clone())?; - let metadata = - NoteMetadata::new(sender_basic_wallet_account.id(), NoteType::Public).with_tag(tag); - let assets = NoteAssets::new(vec![sent_asset0, sent_asset1]).unwrap(); - let note_script = CodeBuilder::default().compile_note_script(DEFAULT_NOTE_SCRIPT).unwrap(); - let serial_num = RandomCoin::new(Word::from([1, 2, 3, 4u32])).draw_word(); - let recipient = NoteRecipient::new(serial_num, note_script, NoteStorage::default()); - let attachments = NoteAttachments::from(attachment.clone()); + let attachment_0 = NoteAttachment::with_words( + NoteAttachmentScheme::new(42)?, + vec![Word::from([9, 8, 7, 6u32]), Word::from([5, 4, 3, 2u32])], + )?; + let attachment_1 = + NoteAttachment::with_word(NoteAttachmentScheme::new(43)?, Word::from([1, 2, 3, 4u32])); + let attachment_2 = NoteAttachment::with_words( + NoteAttachmentScheme::new(44)?, + vec![Word::from([10, 11, 12, 13u32])], + )?; + let attachment_3 = + NoteAttachment::with_word(NoteAttachmentScheme::new(45)?, Word::from([20, 21, 22, 23u32])); + let attachments = + NoteAttachments::new(vec![attachment_0, attachment_1, attachment_2, attachment_3])?; + assert_eq!( + attachments.num_attachments() as usize, + NoteAttachments::MAX_COUNT, + "test should use max num of attachments" + ); - let note = Note::with_attachments(assets.clone(), metadata, recipient, attachments); - let partial_note: PartialNote = note.clone().into(); + let p2id_note = P2idNote::create( + sender_basic_wallet_account.id(), + sender_basic_wallet_account.id(), + vec![sent_asset0, sent_asset2], + NoteType::Public, + attachments, + &mut rng, + )?; + let partial_note = PartialNote::from(p2id_note.clone()); let expiration_delta = 10u16; let send_note_transaction_script = sender_account_interface @@ -84,7 +102,7 @@ async fn test_send_note_script_basic_wallet() -> anyhow::Result<()> { .build_tx_context(sender_basic_wallet_account.id(), &[spawn_note.id()], &[]) .expect("failed to build tx context") .tx_script(send_note_transaction_script) - .extend_expected_output_notes(vec![RawOutputNote::Full(note.clone())]) + .extend_expected_output_notes(vec![RawOutputNote::Full(p2id_note.clone())]) .build()? .execute() .await?; @@ -111,7 +129,7 @@ async fn test_send_note_script_basic_wallet() -> anyhow::Result<()> { executed_transaction.output_notes().get_note(0), &RawOutputNote::Partial(p2any_note.into()) ); - assert_eq!(executed_transaction.output_notes().get_note(1), &RawOutputNote::Full(note)); + assert_eq!(executed_transaction.output_notes().get_note(1), &RawOutputNote::Full(p2id_note)); Ok(()) } @@ -137,7 +155,7 @@ async fn test_send_note_script_basic_fungible_faucet() -> anyhow::Result<()> { AccountInterface::from_account(&sender_basic_fungible_faucet_account); let tag = NoteTag::with_account_target(sender_basic_fungible_faucet_account.id()); - let attachment = NoteAttachment::new_word(NoteAttachmentScheme::new(100)?, Word::empty()); + let attachment = NoteAttachment::with_word(NoteAttachmentScheme::new(100)?, Word::empty()); let metadata = NoteMetadata::new(sender_basic_fungible_faucet_account.id(), NoteType::Public) .with_tag(tag); let assets = NoteAssets::new(vec![Asset::Fungible( diff --git a/crates/miden-testing/tests/scripts/swap.rs b/crates/miden-testing/tests/scripts/swap.rs index 0cd95695a9..92d227696b 100644 --- a/crates/miden-testing/tests/scripts/swap.rs +++ b/crates/miden-testing/tests/scripts/swap.rs @@ -130,7 +130,7 @@ async fn consume_swap_note_private_payback_note() -> anyhow::Result<()> { let full_payback_note = Note::new( payback_note.assets().clone(), - output_payback_note.metadata().clone(), + *output_payback_note.metadata(), payback_note.recipient().clone(), ); @@ -210,7 +210,7 @@ async fn consume_swap_note_public_payback_note() -> anyhow::Result<()> { let full_payback_note = Note::new( payback_note.assets().clone(), - output_payback_note.metadata().clone(), + *output_payback_note.metadata(), payback_note.recipient().clone(), ); diff --git a/crates/miden-tx/src/host/tx_event.rs b/crates/miden-tx/src/host/tx_event.rs index fe07c1e2d2..c1891f6b69 100644 --- a/crates/miden-tx/src/host/tx_event.rs +++ b/crates/miden-tx/src/host/tx_event.rs @@ -13,6 +13,7 @@ use miden_protocol::account::{ use miden_protocol::asset::{Asset, AssetVault, AssetVaultKey, FungibleAsset}; use miden_protocol::note::{ NoteAttachment, + NoteAttachmentContent, NoteAttachmentScheme, NoteId, NoteMetadata, @@ -772,16 +773,13 @@ fn extract_note_attachment( .map(|chunk| Word::from([chunk[0], chunk[1], chunk[2], chunk[3]])) .collect(); - let attachment = match words.len() { - 0 => return Err(TransactionKernelError::other("attachment num_words must be > 0")), - 1 => NoteAttachment::new_word(attachment_scheme, words[0]), - _ => NoteAttachment::new_array(attachment_scheme, words).map_err(|source| { - TransactionKernelError::other_with_source( - "failed to construct note attachment array", - source, - ) - })?, - }; + let content = NoteAttachmentContent::new(words).map_err(|source| { + TransactionKernelError::other_with_source( + "failed to construct note attachment content", + source, + ) + })?; + let attachment = NoteAttachment::new(attachment_scheme, content); let actual_commitment = attachment.to_commitment(); diff --git a/docs/src/note.md b/docs/src/note.md index b806faac84..10ca1bd1dc 100644 --- a/docs/src/note.md +++ b/docs/src/note.md @@ -68,21 +68,23 @@ Every note includes metadata: - the account ID of the sender, i.e. the creator of the note. - its note type, i.e. private or public. - the [note tag](#note-discovery) that aids in discovery of the note. -- an optional [note attachment](#attachment). +- optional [note attachments](#attachments) (up to 4). Regardless of [storage mode](#note-storage-mode), these metadata fields are always public. -### Attachment +### Attachments -An attachment is a variable-size part of a note's metadata: -- It can either be absent (`None`), store a single `Word` or an `Array` of field elements. These are the three _kinds_ of attachments. -- The _scheme_ of an attachment is an optional, 32-bit user-defined value that can be used to detect the presence of certain standardized attachments. +A note can have up to 4 attachments. Each attachment is a variable-size, _public_ extension to the note's metadata consisting of: +- **Content**: Between 1 and 256 words of data (up to 8 KB per attachment, 16 KB total across all attachments). The content of an individual attachment is committed to via a sequential hash over its field elements. The full attachment contents are publicly stored on-chain, even for private notes. +- **Scheme**: A 16-bit (limited to 65534) user-defined value that identifies the kind of attachment. This allows consumers to detect the presence of certain standardized attachments. For untyped attachments, the `none = 1` scheme can be used. + +The note commits to all of its attachments via a sequential hash over the individual attachment commitments (the attachments commitment). The attachment schemes are encoded in the note's metadata. When the note is consumed, the actual attachment content is provided via the advice provider. Example use cases for attachments are: - Communicate the note details of a private note in encrypted form. This means the encrypted note is attached publicly to the otherwise private note. - For [network transactions](./transaction.md#network-transaction), encode the ID of the network account that should - consume the note. This is a standardized attachment scheme in miden-standards called `NetworkAccountTarget`. -- Communicate the details of a _private_ note to the receiver so they can derive the note. For example, the payback note of a partially fillable swap note can be private and the receiver already knows a few details: It is a P2ID note, the serial number is derived from the SWAP note's serial number and the note storage is the account ID of the receiver. The receiver only needs to now the exact amount that was filled to derive the full note for consumption. This amount can be encoded in the public attachment of the payback note, which allows this use case to work with private notes and still not require a side-channel. + consume the note. This is a standardized attachment scheme in `miden-standards` called `NetworkAccountTarget`. +- Communicate the details of a _private_ note to the receiver so they can derive the note. For example, the payback note of a partially fillable swap note can be private and the receiver already knows a few details: It is a P2ID note, the serial number is derived from the SWAP note's serial number and the note storage is the account ID of the receiver. The receiver only needs to know the exact amount that was filled to derive the full note for consumption. This amount can be encoded in a public attachment of the payback note, which allows this use case to work with private notes and still not require a side-channel. ## Note Lifecycle diff --git a/docs/src/protocol_library.md b/docs/src/protocol_library.md index 5b2c3f7550..4c6af5f39c 100644 --- a/docs/src/protocol_library.md +++ b/docs/src/protocol_library.md @@ -119,7 +119,7 @@ Output note procedures can be used to fetch data on output notes created by the | `add_asset` | Adds the asset to the output note specified by the index.

**Inputs:** `[ASSET_KEY, ASSET_VALUE, note_idx]`
**Outputs:** `[]` | Native | | `add_attachment` | Adds an attachment to the note specified by the index. There must be an advice map entry for ATTACHMENT_COMMITMENT that maps to the raw attachment elements.

**Inputs:**
`Operand Stack: [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx]`
`Advice map: { ATTACHMENT_COMMITMENT: [[ATTACHMENT_ELEMENTS]] }`
**Outputs:** `[]` | Native | | `add_word_attachment` | Adds a single-word attachment to the note specified by the note index. Hashes the raw attachment word to produce the commitment, inserts the raw elements into the advice map, then delegates to `add_attachment`.

**Inputs:** `[attachment_scheme, ATTACHMENT, note_idx]`
**Outputs:** `[]` | Native | -| `add_array_attachment` | Adds an array attachment to the note specified by the note index. The ATTACHMENT_COMMITMENT is the hash commitment to a set of elements.

**Inputs:**
`Operand Stack: [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx]`
`Advice map: { ATTACHMENT_COMMITMENT: [[ATTACHMENT_ELEMENTS]] }`
**Outputs:** `[]` | Native | +| `add_words_attachment` | Adds a multi-word attachment to the note specified by the note index. Hashes the raw attachment words to produce the commitment, inserts the raw elements into the advice map, then delegates to `add_attachment`.

**Inputs:** `[attachment_scheme, num_words, attachment_ptr, note_idx]`
**Outputs:** `[]` | Native | | `get_attachments_commitment` | Returns the commitment over all attachments of the output note with the specified index.

**Inputs:** `[note_index]`
**Outputs:** `[ATTACHMENTS_COMMITMENT]` | Any | | `get_attachment_commitments` | Writes the attachment commitments of the output note with the specified index to memory and returns the pointer to the data.

**Inputs:** `[note_index]`
**Outputs:** `[num_attachments, attachments_ptr]` | Any | | `get_attachment` | Writes the attachment with the provided index from the output note with the specified index to memory and returns the pointer to the data.

**Inputs:** `[attachment_idx, note_index]`
**Outputs:** `[num_words, attachment_ptr]` | Any |