From 4aa8e87b94c01a0d3a301cf2b5c7b8d1604e92a4 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Mon, 4 May 2026 12:22:03 +0200 Subject: [PATCH 01/18] chore: reorder memory layout of output notes --- .../asm/kernels/transaction/lib/memory.masm | 18 ++++---- .../src/transaction/kernel/memory.rs | 44 +++++++++++-------- 2 files changed, 34 insertions(+), 28 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm b/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm index 2752d7c478..fb88790906 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm @@ -270,16 +270,16 @@ 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_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 # ------------------------------------------------------------------------------------------------- diff --git a/crates/miden-protocol/src/transaction/kernel/memory.rs b/crates/miden-protocol/src/transaction/kernel/memory.rs index aa8c1d2066..f16c127fc9 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,16 @@ 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_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 // ------------------------------------------------------------------------------------------------ From 44719b3f2b0bc1e2908a196c80285ae911071121 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Mon, 4 May 2026 13:05:45 +0200 Subject: [PATCH 02/18] chore: enforce total number of attachment words per note --- .../asm/kernels/transaction/lib/memory.masm | 25 ++++++++ .../asm/kernels/transaction/lib/note.masm | 1 + .../kernels/transaction/lib/output_note.masm | 36 ++++++++--- crates/miden-protocol/asm/protocol/note.masm | 1 + .../asm/shared_utils/util/note.masm | 3 + .../src/transaction/kernel/memory.rs | 1 + .../src/kernel_tests/tx/test_output_note.rs | 61 +++++++++++++++++++ 7 files changed, 121 insertions(+), 7 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm b/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm index fb88790906..b445411c7b 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm @@ -274,6 +274,7 @@ 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 @@ -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..1b41dddcd4 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/note.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/note.masm @@ -9,6 +9,7 @@ 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 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 617d632146..dfc05145c4 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm @@ -8,6 +8,7 @@ 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::asset::ASSET_SIZE @@ -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" @@ -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. @@ -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. diff --git a/crates/miden-protocol/asm/protocol/note.masm b/crates/miden-protocol/asm/protocol/note.masm index 36d8098956..6bfc6072d9 100644 --- a/crates/miden-protocol/asm/protocol/note.masm +++ b/crates/miden-protocol/asm/protocol/note.masm @@ -8,6 +8,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 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/transaction/kernel/memory.rs b/crates/miden-protocol/src/transaction/kernel/memory.rs index f16c127fc9..12c4262659 100644 --- a/crates/miden-protocol/src/transaction/kernel/memory.rs +++ b/crates/miden-protocol/src/transaction/kernel/memory.rs @@ -466,6 +466,7 @@ 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; 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 d9166f961c..94083c71bb 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 @@ -15,6 +15,7 @@ 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_TOTAL_ATTACHMENT_WORDS_EXCEEDED, ERR_TX_NUMBER_OF_OUTPUT_NOTES_EXCEEDS_LIMIT, }; use miden_protocol::note::{ @@ -1743,6 +1744,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::new_array( + NoteAttachmentScheme::new_const(3), + vec![Word::from([1, 2, 3, 4u32]); NoteAttachment::MAX_NUM_WORDS as usize], + )?; + let attachment1 = NoteAttachment::new_array( + 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 = attachment0.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). /// From d8a5c0991a2280c78511980473b505da3c05134c Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Mon, 4 May 2026 14:07:04 +0200 Subject: [PATCH 03/18] feat: unify attachment word and array variants --- crates/miden-protocol/src/errors/mod.rs | 12 +- crates/miden-protocol/src/note/attachment.rs | 255 ++++-------------- crates/miden-protocol/src/note/metadata.rs | 4 +- crates/miden-protocol/src/note/mod.rs | 1 - .../src/account/interface/mod.rs | 12 +- .../src/note/network_account_target.rs | 45 ++-- .../src/kernel_tests/tx/test_output_note.rs | 2 +- .../src/kernel_tests/tx/test_tx.rs | 8 +- .../src/standards/network_account_target.rs | 12 +- crates/miden-testing/src/utils.rs | 2 +- crates/miden-testing/tests/scripts/pswap.rs | 15 +- crates/miden-tx/src/host/tx_event.rs | 18 +- 12 files changed, 106 insertions(+), 280 deletions(-) diff --git a/crates/miden-protocol/src/errors/mod.rs b/crates/miden-protocol/src/errors/mod.rs index 97585356e3..0fd37fcb33 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,13 @@ 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 - )] - NoteAttachmentArrayTooFewWords(usize), - #[error( - "note attachment array contains {0} words, but the maximum is {max} words", + "note attachment content contains {0} words, but the maximum is {max} words", max = NoteAttachment::MAX_NUM_WORDS )] - NoteAttachmentArrayTooManyWords(usize), + NoteAttachmentContentTooManyWords(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 da07d1216f..2bfe7b6c33 100644 --- a/crates/miden-protocol/src/note/attachment.rs +++ b/crates/miden-protocol/src/note/attachment.rs @@ -30,12 +30,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 +58,26 @@ impl NoteAttachment { Self { attachment_scheme, content } } - /// Creates a new note attachment with content [`NoteAttachmentContent::Word`] from the provided - /// word. + /// Creates a new note attachment from a single word. pub fn new_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( 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 +98,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 +134,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 +199,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 +213,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 +223,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 +231,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 @@ -641,7 +505,7 @@ impl NoteAttachments { .sum::(); if total_num_words > Self::MAX_NUM_WORDS as usize { - return Err(NoteError::NoteAttachmentArrayTooManyWords(total_num_words)); + return Err(NoteError::NoteAttachmentContentTooManyWords(total_num_words)); } let commitment = compute_commitment(&attachments); @@ -798,25 +662,18 @@ mod tests { } #[test] - fn note_attachment_array_fails_on_too_many_words() -> anyhow::Result<()> { + 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 = NoteAttachmentArray::new(words).unwrap_err(); + let err = NoteAttachmentContent::new(words).unwrap_err(); - assert_matches!(err, NoteError::NoteAttachmentArrayTooManyWords(len) => { + assert_matches!(err, NoteError::NoteAttachmentContentTooManyWords(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; @@ -932,16 +789,16 @@ mod tests { #[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); + // 1 word + let content = NoteAttachmentContent::new(vec![Word::from([1, 2, 3, 4u32])]).unwrap(); + assert_eq!(content.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); + // 2 words + let content = NoteAttachmentContent::new(vec![Word::from([1, 1, 1, 1u32]); 2]).unwrap(); + assert_eq!(content.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); + // 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/metadata.rs b/crates/miden-protocol/src/note/metadata.rs index 3409c8ce20..97b3d2bba5 100644 --- a/crates/miden-protocol/src/note/metadata.rs +++ b/crates/miden-protocol/src/note/metadata.rs @@ -339,7 +339,7 @@ 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] @@ -379,7 +379,7 @@ mod tests { NoteAttachment::new_word(NoteAttachmentScheme::none(), Word::from([3, 4, 5, 6u32])), NoteAttachment::new_array( 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( NoteAttachmentScheme::MAX, diff --git a/crates/miden-protocol/src/note/mod.rs b/crates/miden-protocol/src/note/mod.rs index bf2888cc8f..5e62c27c02 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, diff --git a/crates/miden-standards/src/account/interface/mod.rs b/crates/miden-standards/src/account/interface/mod.rs index 8633da6213..ffed31391f 100644 --- a/crates/miden-standards/src/account/interface/mod.rs +++ b/crates/miden-standards/src/account/interface/mod.rs @@ -161,16 +161,14 @@ 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 { + // Only support one attachment per note to keep it simple. if let Some(attachment) = note.attachments().iter().next() { - code_builder.add_advice_map_entry( - attachment.content().to_commitment(), - attachment.content().to_elements(), - ); + code_builder + .add_advice_map_entry(attachment.to_commitment(), attachment.to_elements()); } } diff --git a/crates/miden-standards/src/note/network_account_target.rs b/crates/miden-standards/src/note/network_account_target.rs index 2095e33df7..a4a64cebb2 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] @@ -111,24 +105,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) } } @@ -146,8 +141,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-testing/src/kernel_tests/tx/test_output_note.rs b/crates/miden-testing/src/kernel_tests/tx/test_output_note.rs index 94083c71bb..666ac3cfca 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 @@ -249,7 +249,7 @@ async fn test_get_output_notes_commitment() -> anyhow::Result<()> { )?) .build()?; - // Build the advice map entry for the array attachment's elements + // Build the advice map entry for the 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(); 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..af812498ab 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, @@ -350,13 +349,12 @@ 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"); - }; + let content = attachment3.content(); + assert!(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_advice_map(vec![(content.to_commitment(), content.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/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 e24cddaabd..49d3f71a8f 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 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(); From ec7031486a02761051acaaa5717990edc09fc0ea Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Mon, 4 May 2026 14:15:47 +0200 Subject: [PATCH 04/18] chore: `new_word` -> `with_word` & `new_array` -> `with_words` --- crates/miden-protocol/src/note/attachment.rs | 36 ++++++++++--------- crates/miden-protocol/src/note/metadata.rs | 14 ++++---- .../src/note/network_account_target.rs | 2 +- crates/miden-standards/src/note/pswap.rs | 4 +-- .../src/kernel_tests/tx/test_active_note.rs | 4 +-- .../src/kernel_tests/tx/test_output_note.rs | 22 ++++++------ .../src/kernel_tests/tx/test_tx.rs | 4 +-- crates/miden-testing/tests/scripts/pswap.rs | 2 +- .../miden-testing/tests/scripts/send_note.rs | 4 +-- 9 files changed, 47 insertions(+), 45 deletions(-) diff --git a/crates/miden-protocol/src/note/attachment.rs b/crates/miden-protocol/src/note/attachment.rs index 2bfe7b6c33..8bc1e993f8 100644 --- a/crates/miden-protocol/src/note/attachment.rs +++ b/crates/miden-protocol/src/note/attachment.rs @@ -59,7 +59,7 @@ impl NoteAttachment { } /// Creates a new note attachment from a single word. - pub fn new_word(attachment_scheme: NoteAttachmentScheme, word: Word) -> Self { + pub fn with_word(attachment_scheme: NoteAttachmentScheme, word: Word) -> Self { Self { attachment_scheme, content: NoteAttachmentContent::new(vec![word]).expect("single word is always valid"), @@ -73,7 +73,7 @@ impl NoteAttachment { /// Returns an error if: /// - `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 { @@ -650,8 +650,8 @@ mod tests { 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( + #[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], )?)] @@ -704,17 +704,19 @@ mod tests { #[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 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::new_word(scheme, Word::from([1, 2, 3, 4u32])); - NoteAttachments::MAX_COUNT + 1 - ]) - .unwrap_err(); + 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(()) @@ -723,8 +725,8 @@ mod tests { #[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( + 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], )?, @@ -745,7 +747,7 @@ mod tests { #[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( + let attachments = NoteAttachments::new(vec![NoteAttachment::with_word( NoteAttachmentScheme::new(1)?, word, )])?; @@ -760,8 +762,8 @@ mod tests { #[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( + 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], )?, @@ -779,7 +781,7 @@ mod tests { #[test] fn note_attachments_into_vec() -> anyhow::Result<()> { let word_att = - NoteAttachment::new_word(NoteAttachmentScheme::new(1)?, Word::from([1, 2, 3, 4u32])); + 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]); diff --git a/crates/miden-protocol/src/note/metadata.rs b/crates/miden-protocol/src/note/metadata.rs index 97b3d2bba5..b97495a4e7 100644 --- a/crates/miden-protocol/src/note/metadata.rs +++ b/crates/miden-protocol/src/note/metadata.rs @@ -346,11 +346,11 @@ mod tests { 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]); 2], )?, - NoteAttachment::new_array( + NoteAttachment::with_words( NoteAttachmentScheme::MAX, vec![Word::from([10, 10, 10, 10u32]); NoteAttachment::MAX_NUM_WORDS as usize], )?, diff --git a/crates/miden-standards/src/note/network_account_target.rs b/crates/miden-standards/src/note/network_account_target.rs index a4a64cebb2..36187b4743 100644 --- a/crates/miden-standards/src/note/network_account_target.rs +++ b/crates/miden-standards/src/note/network_account_target.rs @@ -75,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) } } 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..9c381050c7 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 @@ -645,8 +645,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_output_note.rs b/crates/miden-testing/src/kernel_tests/tx/test_output_note.rs index 666ac3cfca..2fa97c7e3f 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 @@ -243,7 +243,7 @@ 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])], )?) @@ -1254,7 +1254,7 @@ 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])); + NoteAttachment::with_word(NoteAttachmentScheme::MAX, Word::from([3, 4, 5, 6u32])); let output_note = RawOutputNote::Full( NoteBuilder::new(account.id(), rng).attachment(attachment.clone()).build()?, ); @@ -1311,7 +1311,7 @@ async fn test_set_array_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 attachment = NoteAttachment::with_words(NoteAttachmentScheme::new(42)?, words.clone())?; let output_note = RawOutputNote::Full(NoteBuilder::new(account.id(), rng).attachment(attachment).build()?); @@ -1472,9 +1472,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) @@ -1576,8 +1576,8 @@ async fn test_get_attachment_ptr() -> anyhow::Result<()> { 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 attachment_0 = NoteAttachment::with_word(NoteAttachmentScheme::new(1)?, word_0); + let attachment_1 = NoteAttachment::with_word(NoteAttachmentScheme::new(2)?, word_1); let output_note = RawOutputNote::Full( NoteBuilder::new(account.id(), rng) @@ -1674,8 +1674,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])?; @@ -1746,11 +1746,11 @@ async fn test_find_attachment( #[tokio::test] async fn test_add_attachments_with_too_many_overall_elements_fails() -> anyhow::Result<()> { - let attachment0 = NoteAttachment::new_array( + 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::new_array( + let attachment1 = NoteAttachment::with_words( NoteAttachmentScheme::new_const(6), vec![Word::from([2, 3, 4, 5u32]); NoteAttachment::MAX_NUM_WORDS as usize], )?; 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 af812498ab..45e434dc6b 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_tx.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_tx.rs @@ -217,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])], )?; diff --git a/crates/miden-testing/tests/scripts/pswap.rs b/crates/miden-testing/tests/scripts/pswap.rs index 49d3f71a8f..7df12ba71e 100644 --- a/crates/miden-testing/tests/scripts/pswap.rs +++ b/crates/miden-testing/tests/scripts/pswap.rs @@ -244,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..d514b765e3 100644 --- a/crates/miden-testing/tests/scripts/send_note.rs +++ b/crates/miden-testing/tests/scripts/send_note.rs @@ -64,7 +64,7 @@ async fn test_send_note_script_basic_wallet() -> anyhow::Result<()> { 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 attachment = NoteAttachment::with_words(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(); @@ -137,7 +137,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( From 9094c5243d6200e47fdedc6d2cb4ec657cd154cb Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Mon, 4 May 2026 14:25:22 +0200 Subject: [PATCH 05/18] chore: update attachment docs in note.md --- docs/src/note.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/src/note.md b/docs/src/note.md index b806faac84..21daa5a727 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. +- **Scheme**: A 16-bit user-defined value that identifies the kind of attachment. This allows consumers to detect the presence of certain standardized attachments. For untyped attachments, a `none = 1` scheme can be used. + +The attachment schemes are encoded in the note's metadata and only the commitment of the attachment contents is stored on-chain. The actual content is provided via the advice provider when the note is consumed. The note commits to all of its attachments via a sequential hash over the individual attachment commitments (the attachments commitment). 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 From ea91dbed928510a5c866d0a72c168a32b58aa2ac Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Tue, 5 May 2026 07:58:11 +0200 Subject: [PATCH 06/18] chore: refactor add_array_attachment to add_words_attachment --- .../kernels/transaction/lib/output_note.masm | 2 +- crates/miden-protocol/asm/protocol/note.masm | 2 +- .../asm/protocol/output_note.masm | 59 ++++++++++---- .../src/transaction/kernel/memory.rs | 10 +++ .../src/kernel_tests/tx/test_output_note.rs | 77 +++++++++++++------ .../src/kernel_tests/tx/test_tx.rs | 19 +++-- docs/src/protocol_library.md | 2 +- 7 files changed, 122 insertions(+), 49 deletions(-) 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 dfc05145c4..4219adee5d 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm @@ -503,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/note.masm b/crates/miden-protocol/asm/protocol/note.masm index 6bfc6072d9..3d9e9ed9a8 100644 --- a/crates/miden-protocol/asm/protocol/note.masm +++ b/crates/miden-protocol/asm/protocol/note.masm @@ -22,7 +22,7 @@ const ERR_OUTPUT_NOTE_ATTACHMENT_IDX_OUT_OF_BOUNDS = "attachment index out of bo # ================================================================================================= #! The number of elements in a word. -const WORD_SIZE = 4 +pub const WORD_SIZE = 4 # NOTE UTILITY PROCEDURES # ================================================================================================= diff --git a/crates/miden-protocol/asm/protocol/output_note.masm b/crates/miden-protocol/asm/protocol/output_note.masm index 822f834e23..fcaaba7459 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::note::WORD_SIZE 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 @@ -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_SIZE + movdn.3 mul.WORD_SIZE + # => [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 diff --git a/crates/miden-protocol/src/transaction/kernel/memory.rs b/crates/miden-protocol/src/transaction/kernel/memory.rs index 12c4262659..5382dfaaec 100644 --- a/crates/miden-protocol/src/transaction/kernel/memory.rs +++ b/crates/miden-protocol/src/transaction/kernel/memory.rs @@ -510,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-testing/src/kernel_tests/tx/test_output_note.rs b/crates/miden-testing/src/kernel_tests/tx/test_output_note.rs index 2fa97c7e3f..8c1cdaf98e 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 @@ -43,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, @@ -245,14 +246,25 @@ async fn test_get_output_notes_commitment() -> anyhow::Result<()> { .add_assets([asset_2]) .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 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()]) @@ -260,7 +272,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 @@ -308,10 +319,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 @@ -332,7 +347,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(), ); @@ -1253,8 +1269,8 @@ async fn test_add_attachment_with_scheme_zero_fails() -> anyhow::Result<()> { 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::with_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()?, ); @@ -1281,10 +1297,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)?; @@ -1307,16 +1323,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 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 @@ -1328,10 +1355,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 @@ -1339,10 +1370,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)?; @@ -1350,7 +1381,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?; @@ -1818,7 +1848,6 @@ async fn test_add_attachments_with_too_many_overall_elements_fails() -> anyhow:: #[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 45e434dc6b..eef0bbe9a7 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_tx.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_tx.rs @@ -317,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 ", @@ -340,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)?; @@ -349,12 +356,10 @@ async fn executed_transaction_output_notes() -> anyhow::Result<()> { // -------------------------------------------------------------------------------------------- // execute the transaction and get the witness - let content = attachment3.content(); - assert!(content.num_words() > 1, "expected multi-word 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![(content.to_commitment(), content.to_elements())]) .extend_expected_output_notes(vec![ RawOutputNote::Full(expected_output_note_2.clone()), RawOutputNote::Full(expected_output_note_3.clone()), diff --git a/docs/src/protocol_library.md b/docs/src/protocol_library.md index 7221dce318..ebcc70b5b6 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 | From 8d48d1c9839d6b470227f83a5d3f46fdefc58e3f Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Tue, 5 May 2026 08:10:41 +0200 Subject: [PATCH 07/18] feat: create shared constants.masm for WORD_NUM_ELEMENTS --- .../asm/kernels/transaction/lib/constants.masm | 3 +-- .../asm/kernels/transaction/lib/memory.masm | 6 +++--- .../asm/kernels/transaction/lib/note.masm | 6 +++--- .../kernels/transaction/lib/output_note.masm | 8 ++++---- .../asm/protocol/active_note.masm | 2 +- .../miden-protocol/asm/protocol/input_note.masm | 2 +- crates/miden-protocol/asm/protocol/note.masm | 17 ++++++----------- .../asm/protocol/output_note.masm | 8 ++++---- .../asm/shared_utils/util/constants.masm | 5 +++++ 9 files changed, 28 insertions(+), 29 deletions(-) create mode 100644 crates/miden-protocol/asm/shared_utils/util/constants.masm 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 b445411c7b..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 @@ -1885,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 @@ -1917,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 diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/note.masm b/crates/miden-protocol/asm/kernels/transaction/lib/note.masm index 1b41dddcd4..b10066e64a 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/note.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/note.masm @@ -2,7 +2,7 @@ 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 @@ -103,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 4219adee5d..5cf234dc84 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm @@ -10,7 +10,7 @@ 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 @@ -203,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 @@ -410,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]] 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 3d9e9ed9a8..3c469459f6 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 @@ -18,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. -pub const WORD_SIZE = 4 - # NOTE UTILITY PROCEDURES # ================================================================================================= @@ -109,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]] @@ -148,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]] @@ -164,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] @@ -191,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 diff --git a/crates/miden-protocol/asm/protocol/output_note.masm b/crates/miden-protocol/asm/protocol/output_note.masm index fcaaba7459..0f584b449c 100644 --- a/crates/miden-protocol/asm/protocol/output_note.masm +++ b/crates/miden-protocol/asm/protocol/output_note.masm @@ -5,7 +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::note::WORD_SIZE +use miden::protocol::util::constants::WORD_NUM_ELEMENTS use miden::protocol::note use miden::core::crypto::hashes::poseidon2 @@ -311,8 +311,8 @@ end #! #! Invocation: exec pub proc add_words_attachment - # Compute num_elements = num_words * WORD_SIZE - movdn.3 mul.WORD_SIZE + # 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 @@ -452,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 From 16662210841520ab29c6e753bdd37db8ccb0df6a Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Tue, 5 May 2026 10:03:23 +0200 Subject: [PATCH 08/18] chore: add test exceeding max attachment count --- .../src/kernel_tests/tx/test_output_note.rs | 61 ++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) 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 8c1cdaf98e..f52a75b888 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 @@ -15,6 +15,7 @@ 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, }; @@ -72,7 +73,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<()> { @@ -1265,6 +1272,58 @@ 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); From 09355b62f0c9be692679283da688b86446dada71 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Tue, 5 May 2026 12:46:03 +0200 Subject: [PATCH 09/18] chore: make note header and metadata `Copy` --- crates/miden-protocol/src/note/header.rs | 2 +- crates/miden-protocol/src/note/metadata.rs | 6 +++--- crates/miden-protocol/src/note/mod.rs | 2 +- crates/miden-protocol/src/transaction/proven_tx.rs | 2 +- crates/miden-protocol/src/transaction/tx_header.rs | 2 +- crates/miden-standards/src/account/interface/test.rs | 8 ++++---- crates/miden-testing/src/kernel_tests/tx/test_note.rs | 3 +-- crates/miden-testing/src/mock_chain/chain.rs | 2 +- crates/miden-testing/tests/scripts/swap.rs | 4 ++-- 9 files changed, 15 insertions(+), 16 deletions(-) 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 b97495a4e7..33ba2073d3 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], @@ -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 5e62c27c02..fdce643c7c 100644 --- a/crates/miden-protocol/src/note/mod.rs +++ b/crates/miden-protocol/src/note/mod.rs @@ -247,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/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/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-testing/src/kernel_tests/tx/test_note.rs b/crates/miden-testing/src/kernel_tests/tx/test_note.rs index ceaea3012e..97a0dc8f52 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/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/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(), ); From f9cb55d163ceab3376b079d56aa3a9527ec7c8e6 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Tue, 5 May 2026 12:53:20 +0200 Subject: [PATCH 10/18] chore: move attachment tests to dedicated module --- crates/miden-protocol/src/note/attachment.rs | 168 +----------------- .../src/note/attachment/tests.rs | 154 ++++++++++++++++ 2 files changed, 157 insertions(+), 165 deletions(-) create mode 100644 crates/miden-protocol/src/note/attachment/tests.rs diff --git a/crates/miden-protocol/src/note/attachment.rs b/crates/miden-protocol/src/note/attachment.rs index 8bc1e993f8..bfa24681de 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; @@ -639,168 +642,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::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.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.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/attachment/tests.rs b/crates/miden-protocol/src/note/attachment/tests.rs new file mode 100644 index 0000000000..d9d9a7939d --- /dev/null +++ b/crates/miden-protocol/src/note/attachment/tests.rs @@ -0,0 +1,154 @@ +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.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.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); +} From fe46dde19f8222a6469d0b6a6efd524a9e72ff94 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Tue, 5 May 2026 13:06:59 +0200 Subject: [PATCH 11/18] feat: implement metadata_into_tag --- crates/miden-protocol/asm/protocol/note.masm | 21 ++++++++++++ .../asm/standards/notes/pswap.masm | 12 ++++--- .../src/kernel_tests/tx/test_active_note.rs | 33 ++++++++++++++++++- 3 files changed, 61 insertions(+), 5 deletions(-) diff --git a/crates/miden-protocol/asm/protocol/note.masm b/crates/miden-protocol/asm/protocol/note.masm index 3c469459f6..cd5724c948 100644 --- a/crates/miden-protocol/asm/protocol/note.masm +++ b/crates/miden-protocol/asm/protocol/note.masm @@ -383,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-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 74b446dd5e..cfe1a93334 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-testing/src/kernel_tests/tx/test_active_note.rs b/crates/miden-testing/src/kernel_tests/tx/test_active_note.rs index 9c381050c7..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 From c629d84cf1e6dc4bd15be49dd2a618cd4fc3163e Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Tue, 5 May 2026 13:18:57 +0200 Subject: [PATCH 12/18] chore: add dedicated error for total num words exceeded --- crates/miden-protocol/src/errors/mod.rs | 5 +++++ crates/miden-protocol/src/note/attachment.rs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/miden-protocol/src/errors/mod.rs b/crates/miden-protocol/src/errors/mod.rs index 0fd37fcb33..2ff09d4215 100644 --- a/crates/miden-protocol/src/errors/mod.rs +++ b/crates/miden-protocol/src/errors/mod.rs @@ -667,6 +667,11 @@ pub enum NoteError { max = NoteAttachment::MAX_NUM_WORDS )] NoteAttachmentContentTooManyWords(usize), + #[error( + "note attachments contain a total of {0} words, but the maximum allowed is {max} words", + max = NoteAttachments::MAX_NUM_WORDS + )] + 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 bfa24681de..41dd13028f 100644 --- a/crates/miden-protocol/src/note/attachment.rs +++ b/crates/miden-protocol/src/note/attachment.rs @@ -508,7 +508,7 @@ impl NoteAttachments { .sum::(); if total_num_words > Self::MAX_NUM_WORDS as usize { - return Err(NoteError::NoteAttachmentContentTooManyWords(total_num_words)); + return Err(NoteError::NoteAttachmentsTooManyWords(total_num_words)); } let commitment = compute_commitment(&attachments); From 2dd982e89571accdb0a6c6172f80249809b91d64 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Tue, 5 May 2026 13:19:09 +0200 Subject: [PATCH 13/18] fix: OutputNote::Private docs --- crates/miden-protocol/src/transaction/outputs/notes.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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), } From 12293c5a13ad2554db53029977156c108e75502f Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Tue, 5 May 2026 14:02:34 +0200 Subject: [PATCH 14/18] chore: add changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 001f774df1..8fc5a95034 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 From 6dd9c49cdcb89640066bfda163d2840b574fc891 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Wed, 6 May 2026 07:58:04 +0200 Subject: [PATCH 15/18] chore: validate returned data in test_get_attachment_ptr --- .../src/kernel_tests/tx/test_output_note.rs | 58 ++++++++++++++----- 1 file changed, 43 insertions(+), 15 deletions(-) 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 f52a75b888..98b04c1309 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 @@ -1663,10 +1663,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::with_word(NoteAttachmentScheme::new(1)?, word_0); - let attachment_1 = NoteAttachment::with_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) @@ -1676,7 +1681,7 @@ async fn test_get_attachment_ptr() -> anyhow::Result<()> { ); let tx_script = format!( - " + r#" use miden::protocol::output_note use miden::core::sys @@ -1694,32 +1699,55 @@ async fn test_get_attachment_ptr() -> anyhow::Result<()> { 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.2 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) --- + # --- get attachment 1 first (to use a non-zero idx) --- 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_WORD_0 = attachment0_word, attachment_scheme_1 = attachment_1.attachment_scheme().as_u16(), - ATTACHMENT_WORD_1 = word_1, + attachment1_num_words = attachment_1.num_words(), ); let tx_script = CodeBuilder::new().compile_tx_script(tx_script)?; From 460db96a2e4caf2d07fc713a94f2484c181abf96 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Thu, 7 May 2026 11:37:43 +0200 Subject: [PATCH 16/18] feat: support multiple attachments in send_note --- .../asm/protocol/output_note.masm | 2 +- .../src/account/interface/component.rs | 37 ++++++-------- .../src/account/interface/mod.rs | 5 +- .../miden-testing/tests/scripts/send_note.rs | 50 +++++++++++++------ 4 files changed, 51 insertions(+), 43 deletions(-) diff --git a/crates/miden-protocol/asm/protocol/output_note.masm b/crates/miden-protocol/asm/protocol/output_note.masm index 0f584b449c..551eb811b7 100644 --- a/crates/miden-protocol/asm/protocol/output_note.masm +++ b/crates/miden-protocol/asm/protocol/output_note.masm @@ -218,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 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 ffed31391f..d0e2043edb 100644 --- a/crates/miden-standards/src/account/interface/mod.rs +++ b/crates/miden-standards/src/account/interface/mod.rs @@ -165,8 +165,7 @@ impl AccountInterface { // The commitment is used as key and the elements as value. let mut code_builder = CodeBuilder::new(); for note in output_notes { - // Only support one attachment per note to keep it simple. - if let Some(attachment) = note.attachments().iter().next() { + for attachment in note.attachments().iter() { code_builder .add_advice_map_entry(attachment.to_commitment(), attachment.to_elements()); } @@ -264,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-testing/tests/scripts/send_note.rs b/crates/miden-testing/tests/scripts/send_note.rs index d514b765e3..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::with_words(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(()) } From 326e708c20da3052b1e098703adb820af4e8cec3 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Thu, 7 May 2026 15:37:56 +0200 Subject: [PATCH 17/18] chore: validate all attachments in test_get_attachment_ptr --- .../src/note/attachment/tests.rs | 5 +++- .../src/kernel_tests/tx/test_output_note.rs | 24 +++++++++++++++---- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/crates/miden-protocol/src/note/attachment/tests.rs b/crates/miden-protocol/src/note/attachment/tests.rs index 832ee63ac5..1c075f54ed 100644 --- a/crates/miden-protocol/src/note/attachment/tests.rs +++ b/crates/miden-protocol/src/note/attachment/tests.rs @@ -103,7 +103,10 @@ fn note_attachments_commitment_single_word() -> anyhow::Result<()> { // 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())); + assert_eq!( + attachments.to_commitment(), + Hasher::hash_elements(word_commitment.as_elements()) + ); Ok(()) } 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 e6cf1ef084..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 @@ -1691,7 +1691,7 @@ 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 @@ -1705,13 +1705,27 @@ async fn test_get_attachment_ptr() -> anyhow::Result<()> { # add second attachment push.0 push.1024 - push.2 + push.{attachment1_num_words} push.{attachment_scheme_1} # => [attachment_scheme, num_words, attachment_ptr, note_idx=0] exec.output_note::add_words_attachment # => [] - # --- get attachment 1 first (to use a 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 @@ -1743,8 +1757,8 @@ async fn test_get_attachment_ptr() -> anyhow::Result<()> { 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 = attachment0_word, attachment_scheme_1 = attachment_1.attachment_scheme().as_u16(), + attachment0_num_words = attachment_0.num_words(), attachment1_num_words = attachment_1.num_words(), ); @@ -1907,7 +1921,7 @@ async fn test_add_attachments_with_too_many_overall_elements_fails() -> anyhow:: end ", attachment0_scheme = attachment0.attachment_scheme().as_u16(), - attachment1_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(), ); From f0b063fd015739a2db74d8fc9818f4d6e9aca915 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Thu, 7 May 2026 15:47:33 +0200 Subject: [PATCH 18/18] fix: attachment docs --- docs/src/note.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/note.md b/docs/src/note.md index 21daa5a727..10ca1bd1dc 100644 --- a/docs/src/note.md +++ b/docs/src/note.md @@ -75,10 +75,10 @@ Regardless of [storage mode](#note-storage-mode), these metadata fields are alwa ### 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. -- **Scheme**: A 16-bit user-defined value that identifies the kind of attachment. This allows consumers to detect the presence of certain standardized attachments. For untyped attachments, a `none = 1` scheme can be used. +- **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 attachment schemes are encoded in the note's metadata and only the commitment of the attachment contents is stored on-chain. The actual content is provided via the advice provider when the note is consumed. The note commits to all of its attachments via a sequential hash over the individual attachment commitments (the attachments commitment). +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.