diff --git a/CHANGELOG.md b/CHANGELOG.md index 341ae4e409..dd99cda976 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ - [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)). + +### Changes + - Added a FungibleTokenMetadata ([#2439](https://github.com/0xMiden/miden-base/pull/2439)) component supporting name, description, logo URI, and external links, along with MASM procedures for retrieving token metadata (get_token_metadata, get_max_supply, get_decimals, get_token_symbol). Also aligned fungible faucet token metadata with the standard by using the canonical storage slot, enabling compatibility with MASM metadata getters. - Added validation of leaf type on CLAIM note processing to prevent message leaves from being processed as asset claims ([#2730](https://github.com/0xMiden/protocol/pull/2730)). - [BREAKING] Reduced `MAX_ASSETS_PER_NOTE` from 255 to 64 and `NOTE_MEM_SIZE` from 3072 to 1024 ([#2741](https://github.com/0xMiden/protocol/issues/2741)). diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm index 9246fd8b56..7f37e1cc12 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm @@ -964,19 +964,15 @@ proc create_mint_note_with_attachment # Set the attachment on the MINT note to target the faucet account # NetworkAccountTarget attachment: targets the faucet so only it can consume the note # network_account_target::new expects [suffix, prefix, exec_hint] - # and returns [attachment_scheme, attachment_kind, ATTACHMENT] + # and returns [attachment_scheme, ATTACHMENT] push.ALWAYS # exec_hint = ALWAYS movdn.2 # => [faucet_id_suffix, faucet_id_prefix, exec_hint, note_idx] exec.network_account_target::new - # => [attachment_scheme, attachment_kind, ATTACHMENT, note_idx] + # => [attachment_scheme, ATTACHMENT, note_idx] - # Rearrange for set_attachment: [note_idx, attachment_scheme, attachment_kind, ATTACHMENT] - movup.6 - # => [note_idx, attachment_scheme, attachment_kind, ATTACHMENT(4)] - - exec.output_note::set_attachment + exec.output_note::add_word_attachment # => [] end diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm index 018e4721e5..c662013c2b 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm @@ -76,7 +76,6 @@ const DESTINATION_NETWORK_LOC=13 const CREATE_BURN_NOTE_BURN_ASSET_LOC=0 const ATTACHMENT_LOC=8 const ATTACHMENT_SCHEME_LOC=12 -const ATTACHMENT_KIND_LOC=13 # Other constants # ------------------------------------------------------------------------------------------------- @@ -540,11 +539,10 @@ proc create_burn_note # => [faucet_id_suffix, faucet_id_prefix, exec_hint, ASSET_KEY] exec.network_account_target::new - # => [attachment_scheme, attachment_kind, NOTE_ATTACHMENT, ASSET_KEY] + # => [attachment_scheme, NOTE_ATTACHMENT, ASSET_KEY] # Save attachment data to locals loc_store.ATTACHMENT_SCHEME_LOC - loc_store.ATTACHMENT_KIND_LOC loc_storew_le.ATTACHMENT_LOC dropw # => [ASSET_KEY] @@ -571,27 +569,27 @@ proc create_burn_note call.output_note::create # => [note_idx, pad(15)] - # duplicate note_idx: one for set_attachment, one for add_asset - dup swapw loc_loadw_le.ATTACHMENT_LOC - # => [NOTE_ATTACHMENT, note_idx, note_idx, pad(11)] + # duplicate note_idx: one for add_word_attachment, one for add_asset + dup + # => [note_idx, note_idx, pad(15)] - loc_load.ATTACHMENT_KIND_LOC - loc_load.ATTACHMENT_SCHEME_LOC - # => [scheme, kind, NOTE_ATTACHMENT, note_idx, note_idx, pad(11)] + padw loc_loadw_le.ATTACHMENT_LOC + # => [NOTE_ATTACHMENT, note_idx, note_idx, pad(15)] - movup.6 - # => [note_idx, scheme, kind, NOTE_ATTACHMENT, note_idx, pad(11)] + loc_load.ATTACHMENT_SCHEME_LOC + # => [scheme, NOTE_ATTACHMENT, note_idx, note_idx, pad(15)] - exec.output_note::set_attachment - # => [note_idx, pad(11)] + # network_account_target is a word-sized attachment + exec.output_note::add_word_attachment + # => [note_idx, pad(15)] locaddr.CREATE_BURN_NOTE_BURN_ASSET_LOC exec.asset::load - # => [ASSET_KEY, ASSET_VALUE, note_idx, pad(11)] + # => [ASSET_KEY, ASSET_VALUE, note_idx, pad(15)] exec.output_note::add_asset - # => [pad(11)] + # => [pad(15)] - dropw dropw drop drop drop + dropw dropw dropw drop drop drop # => [] end diff --git a/crates/miden-agglayer/src/b2agg_note.rs b/crates/miden-agglayer/src/b2agg_note.rs index 392d743bc8..00cb473cfb 100644 --- a/crates/miden-agglayer/src/b2agg_note.rs +++ b/crates/miden-agglayer/src/b2agg_note.rs @@ -3,7 +3,6 @@ //! This module provides helpers for creating B2AGG (Bridge to AggLayer) notes, //! which are used to bridge assets out from Miden to the AggLayer network. -use alloc::string::ToString; use alloc::vec::Vec; use miden_assembly::Library; @@ -16,6 +15,7 @@ use miden_protocol::note::{ Note, NoteAssets, NoteAttachment, + NoteAttachments, NoteMetadata, NoteRecipient, NoteScript, @@ -98,17 +98,17 @@ impl B2AggNote { ) -> Result { let note_storage = build_note_storage(destination_network, destination_address)?; - let attachment = NoteAttachment::from( - NetworkAccountTarget::new(target_account_id, NoteExecutionHint::Always) - .map_err(|e| NoteError::other(e.to_string()))?, - ); + let attachment = NetworkAccountTarget::new(target_account_id, NoteExecutionHint::Always) + .map_err(|error| { + NoteError::other_with_source("failed to create b2agg network account target", error) + })?; + let attachments = NoteAttachments::from(NoteAttachment::from(attachment)); - let metadata = - NoteMetadata::new(sender_account_id, NoteType::Public).with_attachment(attachment); + let metadata = NoteMetadata::new(sender_account_id, NoteType::Public); let recipient = NoteRecipient::new(rng.draw_word(), Self::script(), note_storage); - Ok(Note::new(assets, metadata, recipient)) + Ok(Note::with_attachments(assets, metadata, recipient, attachments)) } } diff --git a/crates/miden-agglayer/src/claim_note.rs b/crates/miden-agglayer/src/claim_note.rs index a3c5702bae..9b88c4b4b8 100644 --- a/crates/miden-agglayer/src/claim_note.rs +++ b/crates/miden-agglayer/src/claim_note.rs @@ -7,7 +7,16 @@ use miden_protocol::account::AccountId; use miden_protocol::crypto::SequentialCommit; use miden_protocol::crypto::rand::FeltRng; use miden_protocol::errors::NoteError; -use miden_protocol::note::{Note, NoteAssets, NoteMetadata, NoteRecipient, NoteStorage, NoteType}; +use miden_protocol::note::{ + Note, + NoteAssets, + NoteAttachment, + NoteAttachments, + NoteMetadata, + NoteRecipient, + NoteStorage, + NoteType, +}; use miden_standards::note::{NetworkAccountTarget, NoteExecutionHint}; use crate::utils::Keccak256Output; @@ -182,14 +191,13 @@ pub fn create_claim_note( let note_storage = NoteStorage::try_from(storage.clone())?; let attachment = NetworkAccountTarget::new(target_bridge_id, NoteExecutionHint::Always) - .map_err(|e| NoteError::other(e.to_string()))? - .into(); + .map_err(|e| NoteError::other(e.to_string()))?; + let attachments = NoteAttachments::from(NoteAttachment::from(attachment)); - let metadata = - NoteMetadata::new(sender_account_id, NoteType::Public).with_attachment(attachment); + let metadata = NoteMetadata::new(sender_account_id, NoteType::Public); let recipient = NoteRecipient::new(rng.draw_word(), claim_script(), note_storage); let assets = NoteAssets::new(vec![])?; - Ok(Note::new(assets, metadata, recipient)) + Ok(Note::with_attachments(assets, metadata, recipient, attachments)) } diff --git a/crates/miden-agglayer/src/config_note.rs b/crates/miden-agglayer/src/config_note.rs index 27eda2e5be..e9beb7d98c 100644 --- a/crates/miden-agglayer/src/config_note.rs +++ b/crates/miden-agglayer/src/config_note.rs @@ -19,6 +19,7 @@ use miden_protocol::note::{ Note, NoteAssets, NoteAttachment, + NoteAttachments, NoteMetadata, NoteRecipient, NoteScript, @@ -113,16 +114,14 @@ impl ConfigAggBridgeNote { let recipient = NoteRecipient::new(serial_num, Self::script(), note_storage); - let attachment = NoteAttachment::from( - NetworkAccountTarget::new(target_account_id, NoteExecutionHint::Always) - .map_err(|e| NoteError::other(e.to_string()))?, - ); - let metadata = - NoteMetadata::new(sender_account_id, NoteType::Public).with_attachment(attachment); + let attachment = NetworkAccountTarget::new(target_account_id, NoteExecutionHint::Always) + .map_err(|e| NoteError::other(e.to_string()))?; + let attachments = NoteAttachments::from(NoteAttachment::from(attachment)); + let metadata = NoteMetadata::new(sender_account_id, NoteType::Public); // CONFIG_AGG_BRIDGE notes don't carry assets let assets = NoteAssets::new(vec![])?; - Ok(Note::new(assets, metadata, recipient)) + Ok(Note::with_attachments(assets, metadata, recipient, attachments)) } } diff --git a/crates/miden-agglayer/src/update_ger_note.rs b/crates/miden-agglayer/src/update_ger_note.rs index 10d5e1bb00..4e5b53dc17 100644 --- a/crates/miden-agglayer/src/update_ger_note.rs +++ b/crates/miden-agglayer/src/update_ger_note.rs @@ -17,6 +17,7 @@ use miden_protocol::note::{ Note, NoteAssets, NoteAttachment, + NoteAttachments, NoteMetadata, NoteRecipient, NoteScript, @@ -100,16 +101,14 @@ impl UpdateGerNote { let recipient = NoteRecipient::new(serial_num, Self::script(), note_storage); - let attachment = NoteAttachment::from( - NetworkAccountTarget::new(target_account_id, NoteExecutionHint::Always) - .map_err(|e| NoteError::other(e.to_string()))?, - ); - let metadata = - NoteMetadata::new(sender_account_id, NoteType::Public).with_attachment(attachment); + let attachment = NetworkAccountTarget::new(target_account_id, NoteExecutionHint::Always) + .map_err(|e| NoteError::other(e.to_string()))?; + let attachments = NoteAttachments::from(NoteAttachment::from(attachment)); + let metadata = NoteMetadata::new(sender_account_id, NoteType::Public); // UPDATE_GER notes don't carry assets let assets = NoteAssets::new(vec![])?; - Ok(Note::new(assets, metadata, recipient)) + Ok(Note::with_attachments(assets, metadata, recipient, attachments)) } } diff --git a/crates/miden-protocol/asm/kernels/transaction/api.masm b/crates/miden-protocol/asm/kernels/transaction/api.masm index acfcdd6f68..f054d56d36 100644 --- a/crates/miden-protocol/asm/kernels/transaction/api.masm +++ b/crates/miden-protocol/asm/kernels/transaction/api.masm @@ -7,7 +7,7 @@ use $kernel::input_note use $kernel::memory use $kernel::output_note use $kernel::tx -# use $kernel::types::AccountId +use $kernel::constants::WORD_SIZE use $kernel::memory::UPCOMING_FOREIGN_PROCEDURE_PTR use $kernel::memory::UPCOMING_FOREIGN_PROC_INPUT_VALUE_15_PTR @@ -912,7 +912,7 @@ end #! Returns the metadata of the specified input note. #! #! Inputs: [is_active_note, note_index, pad(14)] -#! Outputs: [NOTE_ATTACHMENT, METADATA_HEADER, pad(8)] +#! Outputs: [NOTE_ATTACHMENT_0, METADATA_HEADER, pad(8)] #! #! Where: #! - is_active_note is the boolean flag indicating whether we should return the metadata from @@ -920,7 +920,7 @@ end #! - note_index is the index of the input note whose metadata should be returned. Notice that if #! is_active_note is 1, note_index is ignored. #! - METADATA_HEADER is the metadata header of the specified input note. -#! - NOTE_ATTACHMENT is the attachment of the specified input note. +#! - NOTE_ATTACHMENT_0 is the first attachment of the specified input note. #! #! Panics if: #! - the note index is greater or equal to the total number of input notes. @@ -929,7 +929,7 @@ end #! #! Invocation: dynexec pub proc input_note_get_metadata - # get the input note pointer depending on whether the requested note is current or it was + # get the input note pointer depending on whether the requested note is current or it was # requested by index. exec.get_requested_note_ptr # => [input_note_ptr, pad(15)] @@ -948,12 +948,34 @@ pub proc input_note_get_metadata # => [METADATA_HEADER, input_note_ptr, pad(16)] # get the attachment - movup.4 exec.memory::get_input_note_attachment - # => [NOTE_ATTACHMENT, METADATA_HEADER, pad(16)] + movup.4 exec.memory::get_input_note_attachments_commitment + # => [NOTE_ATTACHMENTS_COMMITMENT, METADATA_HEADER, pad(16)] # truncate the stack swapdw dropw dropw - # => [NOTE_ATTACHMENT, METADATA_HEADER, pad(8)] + # => [NOTE_ATTACHMENTS_COMMITMENT, METADATA_HEADER, pad(8)] + + # TODO(multi_attachments): Maintain temporary compatibility with the previous API by returning + # the first attachment. This will be refactored in a follow-up PR. + exec.word::testz not + # => [!is_attachments_commitment_empty, NOTE_ATTACHMENTS_COMMITMENT, METADATA_HEADER, pad(8)] + + # if the attachments commitment is the empty word, the first attachment is also the empty word, + # so we leave the empty word on the stack + # + # otherwise: + if.true + # fetch the first attachment from the advice stack and overwrite the attachments commitment + adv.push_mapval + adv_loadw + # => [ATTACHMENT_COMMITMENT_0, METADATA_HEADER, pad(8)] + + adv.push_mapvaln + adv_push.1 eq.WORD_SIZE assert.err="retrieved attachments must be temporarily word-sized" + adv_loadw + # => [ATTACHMENT_0, METADATA_HEADER, pad(8)] + end + # => [ATTACHMENT_0, METADATA_HEADER, pad(8)] end #! Returns the serial number of the specified input note. @@ -1132,33 +1154,34 @@ pub proc output_note_add_asset # => [pad(16)] end -#! Sets the attachment of the note specified by the index. +#! Adds an attachment to the note specified by the index. #! -#! Inputs: [note_idx, attachment_scheme, attachment_kind, ATTACHMENT, pad(9)] +#! Inputs: [attachment_scheme, ATTACHMENT, note_idx, pad(9)] #! Outputs: [pad(16)] #! #! Where: -#! - note_idx is the index of the note on which the attachment is set. #! - attachment_scheme is the user-defined scheme of the attachment. -#! - attachment_kind is the kind of the attachment content. -#! - ATTACHMENT is the attachment to be set. +#! - ATTACHMENT is the attachment word to store. +#! - 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 kind or scheme does not fit into a u32. -#! - the attachment kind is an unknown variant. +#! - the attachment scheme is 0 or exceeds 65534. +#! - the attachment num_words exceeds 256 or is zero. +#! - the note already has 4 attachments. #! #! Invocation: dynexec -pub proc output_note_set_attachment +pub proc output_note_add_attachment + # assert that the provided note index is less than the total number of output notes + dup.5 exec.output_note::assert_note_index_in_bounds drop + # => [attachment_scheme, ATTACHMENT, note_idx, pad(9)] + # check that this procedure was executed against the native account exec.memory::assert_native_account - # => [note_idx, attachment_scheme, attachment_kind, ATTACHMENT, pad(9)] - - exec.output_note::assert_note_index_in_bounds - # => [note_idx, attachment_scheme, attachment_kind, ATTACHMENT, pad(9)] + # => [attachment_scheme, ATTACHMENT, note_idx, pad(9)] - exec.output_note::set_attachment + exec.output_note::add_attachment # => [pad(16)] end @@ -1254,9 +1277,10 @@ pub proc output_note_get_metadata dup exec.memory::get_output_note_metadata_header # => [METADATA_HEADER, note_ptr, pad(16)] - # get the attachment - movup.4 exec.memory::get_output_note_attachment - # => [NOTE_ATTACHMENT, METADATA_HEADER, pad(16)] + # TODO(multi_attachments): Temporarily maintain compatibility with the old API and return the + # first attachment. + movup.4 push.0 swap exec.memory::get_output_note_attachment_commitment + # => [ATTACHMENT_0, METADATA_HEADER, pad(16)] # truncate the stack swapdw dropw dropw diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm b/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm index 6e68171478..2752d7c478 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm @@ -1,6 +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::types::AccountId use miden::core::mem @@ -254,7 +255,7 @@ const INPUT_NOTE_STORAGE_COMMITMENT_OFFSET=12 const INPUT_NOTE_ASSETS_COMMITMENT_OFFSET=16 const INPUT_NOTE_RECIPIENT_OFFSET=20 const INPUT_NOTE_METADATA_HEADER_OFFSET=24 -const INPUT_NOTE_ATTACHMENT_OFFSET=28 +const INPUT_NOTE_ATTACHMENTS_COMMITMENT_OFFSET=28 const INPUT_NOTE_ARGS_OFFSET=32 const INPUT_NOTE_NUM_STORAGE_ITEMS_OFFSET=36 const INPUT_NOTE_NUM_ASSETS_OFFSET=40 @@ -269,13 +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_METADATA_ATTACHMENT_KIND_SCHEME_OFFSET=OUTPUT_NOTE_METADATA_HEADER_OFFSET + 3 -const OUTPUT_NOTE_ATTACHMENT_OFFSET=8 -const OUTPUT_NOTE_RECIPIENT_OFFSET=12 -const OUTPUT_NOTE_ASSETS_COMMITMENT_OFFSET=16 -const OUTPUT_NOTE_NUM_ASSETS_OFFSET=20 -const OUTPUT_NOTE_DIRTY_FLAG_OFFSET=21 -const OUTPUT_NOTE_ASSETS_OFFSET=24 +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_ASSETS_COMMITMENT_OFFSET=32 +const OUTPUT_NOTE_NUM_ASSETS_OFFSET=36 +const OUTPUT_NOTE_DIRTY_FLAG_OFFSET=37 +const OUTPUT_NOTE_ASSETS_OFFSET=40 # LINK MAP MEMORY # ------------------------------------------------------------------------------------------------- @@ -299,6 +303,16 @@ const LINK_MAP_USED_MEMORY_SIZE=33554432 # The size of each map entry, i.e. four words. const LINK_MAP_ENTRY_SIZE=16 +# 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=67108864 + # MEMORY PROCEDURES # ================================================================================================= @@ -1609,27 +1623,27 @@ end #! Returns the attachment of an input note located at the specified memory address. #! #! Inputs: [note_ptr] -#! Outputs: [NOTE_ATTACHMENT] +#! Outputs: [NOTE_ATTACHMENTS_COMMITMENT] #! #! Where: #! - note_ptr is the memory address at which the input note data begins. -#! - NOTE_ATTACHMENT is the attachment of the input note. -pub proc get_input_note_attachment +#! - NOTE_ATTACHMENTS_COMMITMENT is the commitment to all attachments of the input note. +pub proc get_input_note_attachments_commitment padw - movup.4 add.INPUT_NOTE_ATTACHMENT_OFFSET + movup.4 add.INPUT_NOTE_ATTACHMENTS_COMMITMENT_OFFSET mem_loadw_le end #! Sets the attachment for an input note located at the specified memory address. #! -#! Inputs: [note_ptr, NOTE_ATTACHMENT] -#! Outputs: [NOTE_ATTACHMENT] +#! Inputs: [note_ptr, NOTE_ATTACHMENTS_COMMITMENT] +#! Outputs: [NOTE_ATTACHMENTS_COMMITMENT] #! #! Where: #! - note_ptr is the memory address at which the input note data begins. -#! - NOTE_ATTACHMENT is the attachment of the input note. -pub proc set_input_note_attachment - add.INPUT_NOTE_ATTACHMENT_OFFSET +#! - NOTE_ATTACHMENTS_COMMITMENT is the commitment to all attachments of the input note. +pub proc set_input_note_attachments_commitment + add.INPUT_NOTE_ATTACHMENTS_COMMITMENT_OFFSET mem_storew_le end @@ -1853,46 +1867,84 @@ pub proc set_output_note_metadata_header mem_storew_le end -#! Sets the output note's attachment kind and scheme in the metadata header. +#! Returns the output note's attachment commitment at the given attachment index. +#! +#! This is the commitment to the raw attachment data in the advice inputs. +#! +#! WARNING: Does not check the attachment_idx is within bounds. +#! +#! Inputs: [note_ptr, attachment_idx] +#! Outputs: [ATTACHMENT_COMMITMENT] +#! +#! Where: +#! - note_ptr is the memory address at which the output note data begins. +#! - attachment_idx is the index of the attachment in the note. +#! - ATTACHMENT_COMMITMENT is the note attachment commitment. +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 + # => [attachment_ptr] + + padw movup.4 mem_loadw_le + # => [ATTACHMENT_COMMITMENT] +end + +#! Returns a pointer to the start of the attachment data region for the output note. #! -#! Inputs: [note_ptr, attachment_kind_scheme] +#! Inputs: [note_ptr] +#! Outputs: [attachment_data_ptr] +#! +#! Where: +#! - note_ptr is the memory address at which the output note data begins. +#! - attachment_data_ptr is the memory address of the first attachment slot. +pub proc get_output_note_attachment_commitment_ptr + add.OUTPUT_NOTE_ATTACHMENT_0_OFFSET +end + +#! Sets the output note's attachment at the given slot index. +#! +#! Inputs: [note_ptr, attachment_idx, ATTACHMENT_COMMITMENT] #! Outputs: [] #! #! Where: -#! - attachment_kind_scheme is the type information of the attachment that will be overwritten. #! - note_ptr is the memory address at which the output note data begins. -pub proc set_output_note_attachment_kind_scheme - add.OUTPUT_NOTE_METADATA_ATTACHMENT_KIND_SCHEME_OFFSET - mem_store +#! - attachment_idx is the index of the attachment slot (0..3). +#! - ATTACHMENT_COMMITMENT is the note attachment word. +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 + # => [attachment_ptr, ATTACHMENT_COMMITMENT] + + mem_storew_le dropw + # => [] end -#! Returns the output note's attachment. +#! Returns the number of attachments for the output note. #! #! Inputs: [note_ptr] -#! Outputs: [ATTACHMENT] +#! Outputs: [num_attachments] #! #! Where: -#! - ATTACHMENT is the note attachment. #! - note_ptr is the memory address at which the output note data begins. -pub proc get_output_note_attachment - padw - movup.4 add.OUTPUT_NOTE_ATTACHMENT_OFFSET - mem_loadw_le - # => [ATTACHMENT] +#! - num_attachments is the number of attachments in the output note. +pub proc get_output_note_num_attachments + add.OUTPUT_NOTE_NUM_ATTACHMENTS_OFFSET mem_load end -#! Sets the output note's attachment. +#! Sets the number of attachments for the output note. #! -#! Inputs: [note_ptr, ATTACHMENT] +#! Inputs: [note_ptr, num_attachments] #! Outputs: [] #! #! Where: -#! - ATTACHMENT is the note attachment. #! - note_ptr is the memory address at which the output note data begins. -pub proc set_output_note_attachment - add.OUTPUT_NOTE_ATTACHMENT_OFFSET - mem_storew_le - dropw +#! - num_attachments is the number of attachments in the output note. +pub proc set_output_note_num_attachments + add.OUTPUT_NOTE_NUM_ATTACHMENTS_OFFSET mem_store end #! Returns the number of assets in the output note. diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/note.masm b/crates/miden-protocol/asm/kernels/transaction/lib/note.masm index 6733303dda..06bd03cc4a 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/note.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/note.masm @@ -2,10 +2,14 @@ use miden::core::crypto::hashes::poseidon2 use $kernel::asset::ASSET_SIZE use $kernel::constants::NOTE_MEM_SIZE +use $kernel::constants::WORD_SIZE use $kernel::memory pub use $kernel::util::note::NOTE_TYPE_PUBLIC pub use $kernel::util::note::NOTE_TYPE_PRIVATE +pub use $kernel::util::note::MAX_ATTACHMENT_SCHEME +pub use $kernel::util::note::MAX_ATTACHMENT_WORDS +pub use $kernel::util::note::ATTACHMENT_SCHEME_NONE # ERRORS # ================================================================================================= @@ -81,6 +85,34 @@ end # OUTPUT NOTE PROCEDURES # ================================================================================================= +#! Computes the commitment to the output note's attachments. +#! +#! The commitment is defined as: +#! - 0 attachments: EMPTY_WORD +#! - 1+ attachments: hash(ATTACHMENT_0_COMMITMENT || ... || ATTACHMENT_N_COMMITMENT) +#! i.e., the sequential hash over the individual attachment commitments. +#! +#! Inputs: [note_ptr] +#! Outputs: [ATTACHMENTS_COMMITMENT] +#! +#! Where: +#! - note_ptr is a pointer to the data section of the output note. +#! - ATTACHMENTS_COMMITMENT is the commitment of the note's attachments. +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 + swap exec.memory::get_output_note_attachment_commitment_ptr + # => [start_ptr, num_attachments] + + swap mul.WORD_SIZE dup.1 add swap + # => [start_ptr, end_ptr] + + exec.poseidon2::hash_words + # => [ATTACHMENTS_COMMITMENT] +end + #! Computes the assets commitment of the output note located at the specified memory address. #! #! The hash is computed as a sequential hash of the assets contained in the note. If there is an @@ -199,13 +231,14 @@ pub proc compute_output_notes_commitment dup exec.compute_output_note_id # => [NOTE_ID, current_note_ptr, RATE0, RATE1, CAPACITY, current_index, num_notes] - dup.4 exec.memory::get_output_note_attachment - # => [NOTE_ATTACHMENT, NOTE_ID, current_note_ptr, RATE0, RATE1, CAPACITY, current_index, num_notes] + # compute attachments commitment + dup.4 exec.compute_attachments_commitment + # => [ATTACHMENTS_COMMITMENT, NOTE_ID, current_note_ptr, RATE0, RATE1, CAPACITY, current_index, num_notes] movup.8 exec.memory::get_output_note_metadata_header - # => [NOTE_METADATA_HEADER, NOTE_ATTACHMENT, NOTE_ID, RATE0, RATE1, CAPACITY, current_index, num_notes] + # => [NOTE_METADATA_HEADER, ATTACHMENTS_COMMITMENT, NOTE_ID, RATE0, RATE1, CAPACITY, current_index, num_notes] - # compute hash(NOTE_METADATA_HEADER || NOTE_ATTACHMENT) + # compute hash(NOTE_METADATA_HEADER || ATTACHMENTS_COMMITMENT) exec.poseidon2::merge # => [NOTE_METADATA_COMMITMENT, NOTE_ID, RATE0, RATE1, CAPACITY, current_index, num_notes] 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 1f1b6b41f9..edd327fec4 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm @@ -3,26 +3,32 @@ use $kernel::asset use $kernel::callbacks use $kernel::fungible_asset use $kernel::memory +use $kernel::memory::KERNEL_SCRATCH_PTR use $kernel::note use $kernel::note::NOTE_TYPE_PUBLIC +use $kernel::note::MAX_ATTACHMENT_SCHEME +use $kernel::note::MAX_ATTACHMENT_WORDS use $kernel::constants::MAX_OUTPUT_NOTES_PER_TX -use $kernel::util::note::ATTACHMENT_KIND_NONE -use $kernel::util::note::ATTACHMENT_KIND_ARRAY +use $kernel::constants::WORD_SIZE use $kernel::asset::ASSET_SIZE use $kernel::asset::ASSET_VALUE_MEMORY_OFFSET use miden::core::word +use miden::core::mem -# CONSTANTS +# CONSTANTS # ================================================================================================= -# The default value of the felt at index 3 in the note metadata header when a new note is created. -# All zeros sets the attachment kind to None and the user-defined attachment scheme to "none". -const ATTACHMENT_DEFAULT_KIND_AND_SCHEME=0 +# The maximum number of attachments per note. +const MAX_ATTACHMENTS_PER_NOTE=4 -#! The default attachment scheme, representing the absence of an attachment scheme. -const ATTACHMENT_SCHEME_NONE=0 +# The default value of felt[3] in the metadata header when a new note is created. +# All zeros means no attachment schemes are set. +const ATTACHMENT_DEFAULT_SCHEMES=0 -# ERRORS +#! The attachment scheme value that is reserved to represent an absent attachment. +const ATTACHMENT_RESERVED_SCHEME = 0 + +# ERRORS # ================================================================================================= const ERR_TX_NUMBER_OF_OUTPUT_NOTES_EXCEEDS_LIMIT="number of output notes in the transaction exceeds the maximum limit of 1024" @@ -31,16 +37,22 @@ const ERR_NOTE_INVALID_TYPE="invalid note type" const ERR_OUTPUT_NOTE_INDEX_OUT_OF_BOUNDS="requested output note index should be less than the total number of created output notes" -const ERR_OUTPUT_NOTE_INVALID_ATTACHMENT_SCHEMES="attachment scheme and attachment kind must fit into u32s" +const ERR_OUTPUT_NOTE_ATTACHMENT_SCHEME_MAX_EXCEEDED="attachment scheme must not exceed 65534" + +const ERR_OUTPUT_NOTE_ATTACHMENT_SCHEME_CANNOT_BE_ZERO = "attachment scheme must not be 0" -const ERR_OUTPUT_NOTE_UNKNOWN_ATTACHMENT_KIND="attachment kind variant must be between 0 and 2" +const ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_MAX_EXCEEDED="attachment num_words must not exceed 256" -const ERR_OUTPUT_NOTE_ATTACHMENT_KIND_NONE_MUST_HAVE_ATTACHMENT_SCHEME_NONE="attachment kind none must have attachment scheme none" +const ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_MUST_BE_MULTIPLE_OF_WORD_SIZE="number of elements in an attachment must be a multiple of 4" -const ERR_OUTPUT_NOTE_ATTACHMENT_KIND_NONE_MUST_BE_EMPTY_WORD="attachment kind None requires ATTACHMENT to be set to an empty word" +const ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_CANNOT_BE_ZERO="attachment num_words cannot be zero" + +const ERR_OUTPUT_NOTE_TOO_MANY_ATTACHMENTS="number of attachments per note cannot exceed 4" 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" + const ERR_NON_FUNGIBLE_ASSET_ALREADY_EXISTS="non-fungible asset that already exists in the note cannot be added again" const ERR_NOTE_TAG_MUST_BE_U32="the note's tag must fit into a u32 so the 32 most significant bits of the felt must be zero" @@ -53,19 +65,21 @@ const NOTE_BEFORE_CREATED_EVENT=event("miden::protocol::note::before_created") # Event emitted after a new note is created. const NOTE_AFTER_CREATED_EVENT=event("miden::protocol::note::after_created") -# Event emitted before an asset is added to a note +# Event emitted before an asset is added to a note. const NOTE_BEFORE_ADD_ASSET_EVENT=event("miden::protocol::note::before_add_asset") -# Event emitted after an asset is added to a note +# Event emitted after an asset is added to a note. const NOTE_AFTER_ADD_ASSET_EVENT=event("miden::protocol::note::after_add_asset") -# Event emitted before an ATTACHMENT is added to a note -const NOTE_BEFORE_SET_ATTACHMENT_EVENT=event("miden::protocol::note::before_set_attachment") +# Event emitted before an attachment is added to a note. +const NOTE_BEFORE_ADD_ATTACHMENT_EVENT=event("miden::protocol::note::before_add_attachment") # OUTPUT NOTE PROCEDURES # ================================================================================================= #! Creates a new note and returns the index of the note. #! +#! All attachments are by default set to empty words and the number of attachments is 0. +#! #! Inputs: [tag, note_type, RECIPIENT] #! Outputs: [note_idx] #! @@ -98,7 +112,7 @@ pub proc create movdn.4 # => [NOTE_METADATA_HEADER, note_ptr, RECIPIENT, note_idx] - # emit event to signal that a new note is created + # emit event to signal that a new note is created emit.NOTE_AFTER_CREATED_EVENT # set the metadata for the output note @@ -108,14 +122,6 @@ pub proc create exec.memory::set_output_note_metadata_header dropw # => [note_ptr, RECIPIENT, note_idx] - # set the attachment value of a new note to an empty word - # note that the attachment kind is set to None by build_metadata_header - padw dup.4 - # => [note_ptr, EMPTY_WORD, note_ptr, RECIPIENT, note_idx] - - exec.memory::set_output_note_attachment - # => [note_ptr, RECIPIENT, note_idx] - # set the RECIPIENT for the output note exec.memory::set_output_note_recipient dropw # => [note_idx] @@ -230,44 +236,85 @@ pub proc add_asset # => [] end -#! Sets the attachment of the note specified by the index. +#! Adds an attachment to the note specified by the index. Attachments are append-only. #! -#! Inputs: [note_idx, attachment_scheme, attachment_kind, ATTACHMENT] +#! The attachment elements are fetched from the advice map using ATTACHMENT_COMMITMENT as the key. +#! The number of words (num_words) is derived from the element count and the commitment is verified +#! by hashing the fetched elements. +#! +#! Inputs: +#! Operand Stack: [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] +#! Advice map: { +#! ATTACHMENT_COMMITMENT: [[ATTACHMENT_ELEMENTS]], +#! } #! Outputs: [] #! #! Where: -#! - note_idx is the index of the note on which the attachment is set. #! - attachment_scheme is the user-defined scheme of the attachment. -#! - attachment_kind is the kind of the attachment content. -#! - ATTACHMENT is the attachment to be set. +#! - ATTACHMENT_COMMITMENT is the hash commitment to the attachment elements. +#! - note_idx is the index of the note to which the attachment is added. #! #! Panics if: -#! - the attachment kind or scheme does not fit into a u32. -#! - the attachment kind is an unknown variant. -pub proc set_attachment - exec.memory::get_output_note_ptr dup - # => [note_ptr, note_ptr, attachment_scheme, attachment_kind, ATTACHMENT] +#! - the attachment scheme is 0 or exceeds 65534. +#! - the number of elements is not a multiple of 4, or num_words is zero or exceeds 256. +#! - the computed hash of fetched attachment elements does not match ATTACHMENT_COMMITMENT. +#! - the note already has 4 attachments. +pub proc add_attachment + # validate attachment_scheme does not exceed max + dup u32assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_SCHEME_MAX_EXCEEDED + u32lte.MAX_ATTACHMENT_SCHEME assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_SCHEME_MAX_EXCEEDED + # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] + + # assert the scheme is not 0 which the kernel uses to represent absent attachments + dup neq.ATTACHMENT_RESERVED_SCHEME assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_SCHEME_CANNOT_BE_ZERO + # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] + + movdn.4 + # => [ATTACHMENT_COMMITMENT, attachment_scheme, note_idx] + + # validate preimage for commitment is available and number of committed words is within limits + dupw exec.validate_attachment + # => [ATTACHMENT_COMMITMENT, attachment_scheme, note_idx] + + movup.4 + # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] + + # get note_ptr from note_idx + movup.5 exec.memory::get_output_note_ptr + # => [note_ptr, attachment_scheme, ATTACHMENT_COMMITMENT] + + # validate current number of attachments < 4 + dup exec.memory::get_output_note_num_attachments + # => [num_attachments, note_ptr, attachment_scheme, ATTACHMENT_COMMITMENT] - dupw.1 - # => [ATTACHMENT, note_ptr, note_ptr, attachment_scheme, attachment_kind, ATTACHMENT] + dup lt.MAX_ATTACHMENTS_PER_NOTE assert.err=ERR_OUTPUT_NOTE_TOO_MANY_ATTACHMENTS + # => [num_attachments, note_ptr, attachment_scheme, ATTACHMENT_COMMITMENT] - dup.7 dup.7 - # => [attachment_scheme, attachment_kind, ATTACHMENT, note_ptr, note_ptr, - # attachment_scheme, attachment_kind, ATTACHMENT] + # emit event + emit.NOTE_BEFORE_ADD_ATTACHMENT_EVENT + # => [num_attachments, note_ptr, attachment_scheme, ATTACHMENT_COMMITMENT] - exec.validate_attachment - # => [note_ptr, note_ptr, attachment_scheme, attachment_kind, ATTACHMENT] + # --- Add attachment in output note memory --- - movdn.3 movdn.3 - # => [attachment_scheme, attachment_kind, note_ptr, note_ptr, ATTACHMENT] + movup.6 movup.6 movup.6 movup.6 + # => [ATTACHMENT_COMMITMENT, num_attachments, note_ptr, attachment_scheme] - emit.NOTE_BEFORE_SET_ATTACHMENT_EVENT - # => [attachment_scheme, attachment_kind, note_ptr, note_ptr, ATTACHMENT] + # use attachment_idx = num_attachments + dup.4 dup.6 + # => [note_ptr, attachment_idx, ATTACHMENT_COMMITMENT, num_attachments, note_ptr, attachment_scheme] - exec.set_attachment_kind_scheme - # => [note_ptr, ATTACHMENT] + # store commitment in note memory + exec.memory::set_output_note_attachment_commitment + # => [num_attachments, note_ptr, attachment_scheme] - exec.memory::set_output_note_attachment + # increment number of attachments + dup add.1 + # => [new_num_attachments, num_attachments, note_ptr, attachment_scheme] + + dup.2 exec.memory::set_output_note_num_attachments + # => [num_attachments, note_ptr, attachment_scheme] + + exec.set_attachment_schemes # => [] end @@ -285,17 +332,65 @@ pub proc assert_note_index_in_bounds # => [note_index] end -# HELPER PROCEDURES +# HELPER PROCEDURES # ================================================================================================= +#! Validates the attachment commitment against the advice data. +#! +#! Fetches the attachment elements from the advice map using ATTACHMENT_COMMITMENT as the key, +#! derives num_words from the element count, pipes the elements to scratch memory to compute the +#! commitment, and asserts the computed commitment matches the provided commitment. +#! +#! Inputs: +#! Operand Stack: [ATTACHMENT_COMMITMENT] +#! Advice map: { +#! ATTACHMENT_COMMITMENT: [[ATTACHMENT_ELEMENTS]], +#! } +#! Outputs: [] +#! +#! Panics if: +#! - the number of elements is not a multiple of 4, or num_words is zero or exceeds 256. +#! - the computed hash of fetched elements does not match ATTACHMENT_COMMITMENT. +proc validate_attachment + # push the attachment elements from the advice map onto the advice stack + adv.push_mapvaln + # OS => [ATTACHMENT_COMMITMENT] + # AS => [num_elements, [ATTACHMENT_ELEMENTS]] + + # derive num_words from num_elements + adv_push.1 u32assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_MAX_EXCEEDED + u32divmod.WORD_SIZE + # OS => [remainder, num_words, ATTACHMENT_COMMITMENT] + # AS => [[ATTACHMENT_ELEMENTS]] + + # assert the number of elements is a multiple of WORD_SIZE + eq.0 assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_MUST_BE_MULTIPLE_OF_WORD_SIZE + # OS => [num_words, ATTACHMENT_COMMITMENT] + # AS => [[ATTACHMENT_ELEMENTS]] + + # validate 0 < num_words <= 256 + dup neq.0 assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_CANNOT_BE_ZERO + dup u32lte.MAX_ATTACHMENT_WORDS assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_MAX_EXCEEDED + # OS => [num_words, ATTACHMENT_COMMITMENT] + # AS => [[ATTACHMENT_ELEMENTS]] + + # --- Pipe elements from advice stack to scratch memory while hashing --- + + # 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 + push.KERNEL_SCRATCH_PTR swap + # OS => [num_words, scratch_ptr, ATTACHMENT_COMMITMENT] + # AS => [[ATTACHMENT_ELEMENTS]] + + # validate the sequential hash over the attachment elements is ATTACHMENT_COMMITMENT + exec.mem::pipe_preimage_to_memory drop + # OS => [] +end + #! Builds the provided inputs into the NOTE_METADATA_HEADER word. #! #! - The sender ID is set to the native account's ID. -#! - The attachment scheme is set to 0 (meaning none by convention) and the attachment content -#! type is set to None. -#! -#! Note that this procedure is only exported so it can be tested. It should not be called from -#! non-test code. +#! - Attachment num_words and schemes are initialized to 0 (no attachments). #! #! Inputs: [tag, note_type] #! Outputs: [NOTE_METADATA_HEADER] @@ -317,110 +412,82 @@ pub proc build_metadata_header # Validate the note tag fits into a u32. # -------------------------------------------------------------------------------------------- + # this implies the upper 32 bits are zero, which initializes the attachment num_words to 0 u32assert.err=ERR_NOTE_TAG_MUST_BE_U32 - # => [tag, note_type] + # => [attachment_num_words_and_tag, note_type] # Merge note type, version, and sender ID suffix. # -------------------------------------------------------------------------------------------- exec.account::get_id - # => [sender_id_suffix, sender_id_prefix, tag, note_type] + # => [sender_id_suffix, sender_id_prefix, attachment_num_words_and_tag, note_type] # The lower 8 bits of the account ID suffix are guaranteed to be zero by construction. # Encode note_type at bit 4, leaving version at 0 (in bits 0..=3). # Shifting note_type left by 4 is equivalent to multiplying by 16. movup.3 mul.16 add - # => [sender_id_suffix_type_version, sender_id_prefix, tag] + # => [sender_id_suffix_type_version, sender_id_prefix, attachment_num_words_and_tag] # Build metadata header. # -------------------------------------------------------------------------------------------- - push.ATTACHMENT_DEFAULT_KIND_AND_SCHEME movdn.3 - # => [sender_id_suffix_type_version, sender_id_prefix, tag, attachment_kind_scheme] + # push default absent attachment schemes (four u16 zeros encoded into a zero felt) + push.ATTACHMENT_DEFAULT_SCHEMES movdn.3 + # => [sender_id_suffix_type_version, sender_id_prefix, attachment_num_words_and_tag, attachment_schemes] # => [NOTE_METADATA_HEADER] end -#! Validate the ATTACHMENT against the attachment kind. +#! Sets an output note's attachment scheme in the note metadata header. #! -#! Inputs: [attachment_scheme, attachment_kind, ATTACHMENT] -#! Outputs: [] +#! WARNING: The attachment scheme must be valid. #! -#! Where: -#! - attachment_scheme is the user-defined scheme of the attachment. -#! - attachment_kind is the kind of the attachment content. -#! - ATTACHMENT is the attachment to validate. -#! -#! Panics if: -#! - the attachment kind or scheme does not fit into a u32. -#! - the attachment kind is an unknown variant. -#! - the attachment kind is None and the ATTACHMENT is not an empty word. -proc validate_attachment - u32assert2.err=ERR_OUTPUT_NOTE_INVALID_ATTACHMENT_SCHEMES - # => [attachment_scheme, attachment_kind, ATTACHMENT] - - # assert that the attachment kind is valid - swap dup u32lte.ATTACHMENT_KIND_ARRAY - assert.err=ERR_OUTPUT_NOTE_UNKNOWN_ATTACHMENT_KIND - # => [attachment_kind, attachment_scheme, ATTACHMENT] - - eq.ATTACHMENT_KIND_NONE - # => [is_attachment_none, attachment_scheme, ATTACHMENT] - - if.true - eq.ATTACHMENT_SCHEME_NONE - assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_KIND_NONE_MUST_HAVE_ATTACHMENT_SCHEME_NONE - # => [ATTACHMENT] - - padw assert_eqw.err=ERR_OUTPUT_NOTE_ATTACHMENT_KIND_NONE_MUST_BE_EMPTY_WORD - # => [] - else - drop dropw - # => [] - end - # => [] -end - -#! Sets an output note's attachment kind and scheme in the note metadata header. -#! -#! WARNING: The attachment scheme and kind must be valid. -#! -#! Inputs: [attachment_scheme, attachment_kind, note_ptr] +#! Inputs: [num_attachments, note_ptr, attachment_scheme] #! Outputs: [] #! #! 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_kind is the kind of the attachment content. #! - note_ptr is the memory address at which the output note data begins. -proc set_attachment_kind_scheme - exec.merge_attachment_kind_and_scheme - # => [attachment_kind_scheme, note_ptr] +proc set_attachment_schemes + # the schemes are stored as follows in the third felt: + # 3rd felt: [ + # attachment_3_scheme (16 bits) | attachment_2_scheme (16 bits) | + # attachment_1_scheme (16 bits) | attachment_0_scheme (16 bits) + # ] + # -> current scheme needs to be shifted left by num_attachments * 16 + + # Prepare scheme and num_words. + # -------------------------------------------------------------------------------------------- - swap - # => [note_ptr, attachment_kind_scheme] + movup.2 swap + # => [num_attachments, attachment_scheme, note_ptr] - exec.memory::set_output_note_attachment_kind_scheme - # => [] -end + # shift the scheme left by num_attachments * 16 bits using felt multiplication + # u32shl cannot be used because the shift may be >= 32 + # left shift is done with multiplication by 2^(num_attachments * 16) + mul.16 pow2 mul + # => [attachment_scheme_shifted, note_ptr] -#! Merges the attachment kind and scheme into a single felt with the following layout: -#! -#! [30 zero bits | attachment_kind (2 bits) | attachment_scheme (32 bits)] -#! -#! WARNING: The attachment scheme and kind must be valid. -#! -#! Inputs: [attachment_scheme, attachment_kind] -#! Outputs: [attachment_kind_scheme] -#! -#! Where: -#! - attachment_scheme is the user-defined scheme of the attachment. -#! - attachment_kind is the kind of the attachment content. -#! - attachment_kind_scheme is the felt constructed from the inputs. -proc merge_attachment_kind_and_scheme - # shift the attachment_kind 32 bits to the left, which is the same as multiplying by 2^32 - # and set the lower bits to the attachment_scheme, which is done by adding the values together - swap mul.0x100000000 - add - # => [attachment_kind_scheme] + # Fetch and update metadata header. + # -------------------------------------------------------------------------------------------- + + dup.1 exec.memory::get_output_note_metadata_header + # => [METADATA_HEADER, attachment_scheme_shifted, note_ptr] + # => [ + # [sender_id_suffix_type_version, sender_id_prefix, tag, attachment_schemes], + # attachment_scheme_shifted, note_ptr + # ] + + # merge scheme into existing schemes + # (using add instead of u32or because the shifted values exceed u32; this is safe because the + # bit ranges of the tag, existing sizes, and the new size do not overlap) + movup.3 movup.4 + add movdn.3 + # => [[sender_id_suffix_type_version, sender_id_prefix, tag, new_attachment_schemes], note_ptr] + # => [METADATA_HEADER, note_ptr] + + movup.4 exec.memory::set_output_note_metadata_header dropw + # => [] end #! Increments the number of output notes by one. Returns the index of the next note to be created. diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/prologue.masm b/crates/miden-protocol/asm/kernels/transaction/lib/prologue.masm index ae9e492826..26f56a44b0 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/prologue.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/prologue.masm @@ -582,25 +582,25 @@ end #! #! Inputs: #! Operand stack: [note_ptr] -#! Advice stack: [NOTE_ARGS, NOTE_ATTACHMENT, NOTE_METADATA_HEADER] +#! Advice stack: [NOTE_ARGS, NOTE_ATTACHMENTS_COMMITMENT, NOTE_METADATA_HEADER] #! Outputs: -#! Operand stack: [NOTE_METADATA_HEADER, NOTE_ATTACHMENT] +#! Operand stack: [NOTE_METADATA_HEADER, NOTE_ATTACHMENTS_COMMITMENT] #! Advice stack: [] #! #! Where: #! - note_ptr is the memory location for the input note. #! - NOTE_ARGS are the user arguments passed to the note. #! - NOTE_METADATA_HEADER is the note's metadata header. -#! - NOTE_ATTACHMENT is the note's attachment. +#! - NOTE_ATTACHMENTS_COMMITMENT is the note's attachments commitment. proc process_note_args_and_metadata padw adv_loadw dup.4 exec.memory::set_input_note_args dropw # => [note_ptr] - padw adv_loadw dup.4 exec.memory::set_input_note_attachment - # => [NOTE_ATTACHMENT] + padw adv_loadw dup.4 exec.memory::set_input_note_attachments_commitment + # => [NOTE_ATTACHMENTS_COMMITMENT] padw adv_loadw movup.8 exec.memory::set_input_note_metadata_header - # => [NOTE_METADATA_HEADER, NOTE_ATTACHMENT, note_ptr] + # => [NOTE_METADATA_HEADER, NOTE_ATTACHMENTS_COMMITMENT, note_ptr] end #! Checks that the number of note storage is within limit and stores it to memory. @@ -846,9 +846,9 @@ proc process_input_note # => [note_ptr, NULLIFIER, CAPACITY] dup exec.process_note_args_and_metadata - # => [NOTE_METADATA_HEADER, NOTE_ATTACHMENT, note_ptr, NULLIFIER, CAPACITY] + # => [NOTE_METADATA_HEADER, NOTE_ATTACHMENTS_COMMITMENT, note_ptr, NULLIFIER, CAPACITY] - # compute hash(NOTE_METADATA_HEADER || NOTE_ATTACHMENT) + # compute hash(NOTE_METADATA_HEADER || NOTE_ATTACHMENTS_COMMITMENT) exec.poseidon2::merge # => [NOTE_METADATA_COMMITMENT, note_ptr, NULLIFIER, CAPACITY] diff --git a/crates/miden-protocol/asm/protocol/kernel_proc_offsets.masm b/crates/miden-protocol/asm/protocol/kernel_proc_offsets.masm index cf3e6098ea..61493fd343 100644 --- a/crates/miden-protocol/asm/protocol/kernel_proc_offsets.masm +++ b/crates/miden-protocol/asm/protocol/kernel_proc_offsets.masm @@ -65,7 +65,7 @@ pub const OUTPUT_NOTE_GET_METADATA_OFFSET=35 pub const OUTPUT_NOTE_GET_ASSETS_INFO_OFFSET=36 pub const OUTPUT_NOTE_GET_RECIPIENT_OFFSET=37 pub const OUTPUT_NOTE_ADD_ASSET_OFFSET=38 -pub const OUTPUT_NOTE_SET_ATTACHMENT_OFFSET=39 +pub const OUTPUT_NOTE_ADD_ATTACHMENT_OFFSET=39 ### Tx ########################################## diff --git a/crates/miden-protocol/asm/protocol/note.masm b/crates/miden-protocol/asm/protocol/note.masm index fdf42f4afd..0ce5286e6e 100644 --- a/crates/miden-protocol/asm/protocol/note.masm +++ b/crates/miden-protocol/asm/protocol/note.masm @@ -6,6 +6,9 @@ use miden::core::mem pub use miden::protocol::util::note::MAX_NOTE_STORAGE_ITEMS 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::ATTACHMENT_SCHEME_NONE # ERRORS # ================================================================================================= @@ -194,27 +197,28 @@ pub proc metadata_into_sender # => [sender_id_suffix, sender_id_prefix] end -#! Extracts the attachment kind and scheme from the provided metadata header. +#! Extracts the first attachment's num_words and scheme from the provided metadata header. +#! +#! TODO(multi_attachments): This API temporarily maintains compatibility with the previous approach +#! that supported just one attachment per note. #! #! Inputs: [METADATA_HEADER] -#! Outputs: [attachment_kind, attachment_scheme] +#! Outputs: [attachment_0_scheme] #! #! Where: -#! - METADATA_HEADER is the metadata of a note. -#! - attachment_kind is the attachment kind of the note. -#! - attachment_scheme is the attachment scheme of the note. +#! - METADATA_HEADER is the metadata word of a note. +#! - attachment_0_scheme is the scheme of the first attachment (0 if absent). #! #! Invocation: exec -pub proc metadata_into_attachment_info - # => [sender_id_suffix_type_version, sender_id_prefix, tag, attachment_kind_scheme] +pub proc metadata_into_attachment_header + # => [sender_id_suffix_type_version, sender_id_prefix, tag, schemes] + drop drop drop - # => [attachment_kind_scheme] + # => [schemes] - # deconstruct the attachment_kind_scheme to extract the attachment_scheme - # attachment_kind_scheme = [30 zero bits | attachment_kind (2 bits) | attachment_scheme (32 bits)] - # u32split splits into [lo, hi] where lo is attachment_scheme - u32split swap - # => [attachment_kind, attachment_scheme] + # extract attachment 0 scheme by taking only the lower 16 bits + u32and.0xffff + # [attachment_0_scheme] end #! Extracts the note type from the provided metadata header. @@ -227,7 +231,7 @@ end #! #! Where: #! - METADATA_HEADER is the metadata of a note, laid out on the stack as -#! [sender_id_suffix_type_version, sender_id_prefix, tag, attachment_kind_scheme]. +#! [sender_id_suffix_type_version, sender_id_prefix, tag, attachment_schemes]. #! The first felt (sender_id_suffix_type_version) has the following bit layout: #! [sender_id_suffix (56 bits) | reserved (3 bits) | note_type (1 bit) | version (4 bits)] #! - note_type is the type of the note (0 for private, 1 for public). diff --git a/crates/miden-protocol/asm/protocol/output_note.masm b/crates/miden-protocol/asm/protocol/output_note.masm index 73cb8119dc..c922f3172c 100644 --- a/crates/miden-protocol/asm/protocol/output_note.masm +++ b/crates/miden-protocol/asm/protocol/output_note.masm @@ -1,18 +1,11 @@ use miden::protocol::kernel_proc_offsets::OUTPUT_NOTE_CREATE_OFFSET use miden::protocol::kernel_proc_offsets::OUTPUT_NOTE_GET_ASSETS_INFO_OFFSET use miden::protocol::kernel_proc_offsets::OUTPUT_NOTE_ADD_ASSET_OFFSET -use miden::protocol::kernel_proc_offsets::OUTPUT_NOTE_SET_ATTACHMENT_OFFSET +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::note - -# CONSTANTS -# ================================================================================================= - -# Re-export constants for note attachment kinds -pub use miden::protocol::util::note::ATTACHMENT_KIND_NONE -pub use miden::protocol::util::note::ATTACHMENT_KIND_WORD -pub use miden::protocol::util::note::ATTACHMENT_KIND_ARRAY +use miden::core::crypto::hashes::poseidon2 # PROCEDURES # ================================================================================================= @@ -157,39 +150,39 @@ pub proc add_asset # => [] end -#! Sets the attachment of the note specified by the index. +#! Adds an attachment to the note specified by the note index. #! -#! If attachment_kind == Array, there must be an advice map entry for ATTACHMENT (see below). +#! There must be an advice map entry for ATTACHMENT_COMMITMENT that maps to the raw attachment +#! elements. #! #! Inputs: -#! Operand Stack: [note_idx, attachment_scheme, attachment_kind, ATTACHMENT] +#! Operand Stack: [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] #! Advice map: { -#! ATTACHMENT?: [[ATTACHMENT_ELEMENTS]], +#! ATTACHMENT_COMMITMENT: [[ATTACHMENT_ELEMENTS]], #! } #! Outputs: [] #! #! Where: -#! - note_idx is the index of the note on which the attachment is set. #! - attachment_scheme is the user-defined scheme of the attachment. -#! - attachment_kind is the kind of the attachment content. -#! - ATTACHMENT is the attachment to be set. -#! - ATTACHMENT_ELEMENTS are the elements for which ATTACHMENT is the sequential commitment (only -#! needed if attachment_kind == Array). +#! - ATTACHMENT_COMMITMENT is the hash commitment to the attachment elements. +#! - 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 kind or scheme does not fit into a u32. -#! - the attachment kind is an unknown variant. +#! - the attachment scheme is 0 or exceeds 65534. +#! - the note already has 4 attachments. #! #! Invocation: exec -pub proc set_attachment - push.OUTPUT_NOTE_SET_ATTACHMENT_OFFSET - # => [offset, note_idx, attachment_scheme, attachment_kind, ATTACHMENT] +pub proc add_attachment + push.OUTPUT_NOTE_ADD_ATTACHMENT_OFFSET + # => [offset, attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] # pad the stack before the syscall - padw padw swapdw - # => [offset, note_idx, attachment_scheme, attachment_kind, ATTACHMENT, pad(8)] + push.0 movdn.8 padw padw swapdw + # => [offset, attachment_scheme, ATTACHMENT_COMMITMENT, note_idx, pad(9)] syscall.exec_kernel_proc # => [pad(16)] @@ -199,61 +192,88 @@ pub proc set_attachment # => [] end -#! Sets the attachment of the note specified by the note index to the provided word. +#! Adds a single-word attachment to the note specified by the note index. #! -#! This overwrites any previously set attachment. +#! Hashes the raw attachment word to produce the commitment, inserts the raw elements into the +#! advice map keyed by that commitment, then delegates to `add_attachment`. #! -#! Inputs: [note_idx, attachment_scheme, ATTACHMENT] +#! Inputs: [attachment_scheme, ATTACHMENT, note_idx] #! Outputs: [] #! #! Where: -#! - note_idx is the index of the note on which the attachment is set. -#! - attachment_scheme is the user-defined scheme of the attachment. -#! - ATTACHMENT is the raw attachment to set. +#! - attachment_scheme is the user-defined scheme of the attachment (u16, max 65534). +#! - 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 u32. +#! - the attachment scheme does not fit into a u16. +#! - the note already has 4 attachments. #! #! Invocation: exec -pub proc set_word_attachment - push.ATTACHMENT_KIND_WORD movdn.2 - # => [note_idx, attachment_scheme, attachment_kind, ATTACHMENT] +@locals(4) +pub proc add_word_attachment + # => [attachment_scheme, ATTACHMENT, note_idx] + + # Store ATTACHMENT to local memory for hashing and advice map insertion + movdn.4 + # => [ATTACHMENT, attachment_scheme, note_idx] + + loc_storew_le.0 + # => [ATTACHMENT, attachment_scheme, note_idx] + + exec.poseidon2::hash + # => [ATTACHMENT_COMMITMENT, attachment_scheme, note_idx] - exec.set_attachment + locaddr.0 dup add.4 + # => [end_ptr, start_ptr, ATTACHMENT_COMMITMENT, attachment_scheme, note_idx] + + movdn.5 movdn.4 + # => [ATTACHMENT_COMMITMENT, start_ptr, end_ptr, attachment_scheme, note_idx] + + # Insert the raw attachment elements into the advice map keyed by the commitment. + adv.insert_mem + # => [ATTACHMENT_COMMITMENT, start_ptr, end_ptr, attachment_scheme, note_idx] + + # Clean up and arrange stack for add_attachment + movup.4 drop movup.4 drop + # => [ATTACHMENT_COMMITMENT, attachment_scheme, note_idx] + + movup.4 + # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] + + exec.add_attachment # => [] end -#! Sets the attachment of the note specified by the note index to the provided ATTACHMENT which -#! commits to an array of felts. -#! -#! This overwrites any previously set 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: [note_idx, attachment_scheme, ATTACHMENT] +#! Operand Stack: [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] #! Advice map: { -#! ATTACHMENT: [[ATTACHMENT_ELEMENTS]], +#! ATTACHMENT_COMMITMENT: [[ATTACHMENT_ELEMENTS]], #! } #! Outputs: [] #! #! Where: -#! - note_idx is the index of the note on which the attachment is set. #! - attachment_scheme is the user-defined scheme of the attachment. -#! - ATTACHMENT is the commitment of the set of elements that form the note attachment. -#! - ATTACHMENT_ELEMENTS are the elements for which ATTACHMENT is the sequential commitment. +#! - ATTACHMENT_COMMITMENT is the hash commitment to the attachment elements. +#! - 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 u32. +#! - the attachment scheme does not fit into a u16. +#! - the note already has 4 attachments. +#! - the num_words of the attachment is not at least 2. #! #! Invocation: exec -pub proc set_array_attachment - push.ATTACHMENT_KIND_ARRAY movdn.2 - # => [note_idx, attachment_scheme, attachment_kind, ATTACHMENT] - - exec.set_attachment +pub proc add_array_attachment + exec.add_attachment # => [] end @@ -290,7 +310,7 @@ pub proc get_recipient # => [RECIPIENT] end -#! Returns the metadata of the output note with the specified index. +#! Returns the metadata and first attachment of the output note with the specified index. #! #! Inputs: [note_index] #! Outputs: [NOTE_ATTACHMENT, METADATA_HEADER] @@ -298,7 +318,7 @@ end #! Where: #! - note_index is the index of the output note whose metadata should be returned. #! - METADATA_HEADER is the metadata header of the specified output note. -#! - NOTE_ATTACHMENT is the attachment of the specified output note. +#! - NOTE_ATTACHMENT is the first attachment of the specified output note. #! #! Panics if: #! - the note index is greater or equal to the total number of output notes. diff --git a/crates/miden-protocol/asm/shared_utils/util/note.masm b/crates/miden-protocol/asm/shared_utils/util/note.masm index d0f474b692..3d7a505ab9 100644 --- a/crates/miden-protocol/asm/shared_utils/util/note.masm +++ b/crates/miden-protocol/asm/shared_utils/util/note.masm @@ -4,13 +4,6 @@ # The maximum number of storage values associated with a single note. pub const MAX_NOTE_STORAGE_ITEMS = 1024 -#! Signals the absence of a note attachment. -pub const ATTACHMENT_KIND_NONE=0 -#! A note attachment consisting of a single Word. -pub const ATTACHMENT_KIND_WORD=1 -#! A note attachment consisting of the commitment to a set of felts. -pub const ATTACHMENT_KIND_ARRAY=2 - # Note type constants. These encode the note type in the lower byte of the metadata header. # See NoteType in the Rust protocol crate for details. @@ -19,3 +12,12 @@ pub const NOTE_TYPE_PRIVATE=0 #! The note type of public notes. pub const NOTE_TYPE_PUBLIC=1 + +#! The maximum attachment scheme value. +pub const MAX_ATTACHMENT_SCHEME=65534 + +#! The maximum number of words in an attachment. +pub const MAX_ATTACHMENT_WORDS=256 + +#! The reserved value to signal a `None` note attachment scheme. +pub const ATTACHMENT_SCHEME_NONE = 1 diff --git a/crates/miden-protocol/src/batch/note_tree.rs b/crates/miden-protocol/src/batch/note_tree.rs index e0aa847f01..22eeaf391b 100644 --- a/crates/miden-protocol/src/batch/note_tree.rs +++ b/crates/miden-protocol/src/batch/note_tree.rs @@ -2,7 +2,7 @@ use alloc::vec::Vec; use crate::crypto::merkle::MerkleError; use crate::crypto::merkle::smt::{LeafIndex, SimpleSmt}; -use crate::note::{NoteId, NoteMetadata, compute_note_commitment}; +use crate::note::{NoteId, NoteMetadataHeader, compute_note_commitment}; use crate::utils::serde::{ ByteReader, ByteWriter, @@ -26,11 +26,11 @@ impl BatchNoteTree { /// Returns an error if the number of entries exceeds the maximum tree capacity, that is /// 2^{depth}. pub fn with_contiguous_leaves<'a>( - entries: impl IntoIterator, + entries: impl IntoIterator, ) -> Result { let leaves = entries .into_iter() - .map(|(note_id, metadata)| compute_note_commitment(note_id, metadata)); + .map(|(note_id, metadata_header)| compute_note_commitment(note_id, metadata_header)); SimpleSmt::with_contiguous_leaves(leaves).map(Self) } diff --git a/crates/miden-protocol/src/block/block_body.rs b/crates/miden-protocol/src/block/block_body.rs index 4b10460edd..595f82c722 100644 --- a/crates/miden-protocol/src/block/block_body.rs +++ b/crates/miden-protocol/src/block/block_body.rs @@ -114,7 +114,7 @@ impl BlockBody { pub fn compute_block_note_tree(&self) -> BlockNoteTree { let entries = self .output_notes() - .map(|(note_index, note)| (note_index, note.id(), note.metadata())); + .map(|(note_index, note)| (note_index, note.id(), note.metadata_header())); // SAFETY: We only construct block bodies that: // - do not contain duplicates diff --git a/crates/miden-protocol/src/block/note_tree.rs b/crates/miden-protocol/src/block/note_tree.rs index 81665b238a..d35a714b4a 100644 --- a/crates/miden-protocol/src/block/note_tree.rs +++ b/crates/miden-protocol/src/block/note_tree.rs @@ -6,7 +6,7 @@ use miden_crypto::merkle::SparseMerklePath; use crate::batch::BatchNoteTree; use crate::crypto::merkle::MerkleError; use crate::crypto::merkle::smt::{LeafIndex, SimpleSmt}; -use crate::note::{NoteId, NoteMetadata, compute_note_commitment}; +use crate::note::{NoteId, NoteMetadataHeader, compute_note_commitment}; use crate::utils::serde::{ ByteReader, ByteWriter, @@ -43,11 +43,14 @@ impl BlockNoteTree { /// Returns an error if: /// - The number of entries exceeds the maximum notes tree capacity, that is 2^16. /// - The provided entries contain multiple values for the same key. - pub fn with_entries<'metadata>( - entries: impl IntoIterator, + pub fn with_entries<'a>( + entries: impl IntoIterator, ) -> Result { - let leaves = entries.into_iter().map(|(index, note_id, metadata)| { - (index.leaf_index_value() as u64, compute_note_commitment(note_id, metadata)) + let leaves = entries.into_iter().map(|(index, note_id, metadata_header)| { + ( + index.leaf_index_value() as u64, + compute_note_commitment(note_id, metadata_header), + ) }); SimpleSmt::with_leaves(leaves).map(Self) diff --git a/crates/miden-protocol/src/block/proposed_block.rs b/crates/miden-protocol/src/block/proposed_block.rs index 2147577ec1..3c50d1ca98 100644 --- a/crates/miden-protocol/src/block/proposed_block.rs +++ b/crates/miden-protocol/src/block/proposed_block.rs @@ -423,7 +423,7 @@ impl ProposedBlock { "max batches in block and max notes in batches should be enforced", ), note.id(), - note.metadata(), + note.metadata_header(), ) }) }); diff --git a/crates/miden-protocol/src/errors/mod.rs b/crates/miden-protocol/src/errors/mod.rs index d89e98fc9f..97585356e3 100644 --- a/crates/miden-protocol/src/errors/mod.rs +++ b/crates/miden-protocol/src/errors/mod.rs @@ -5,7 +5,6 @@ use core::error::Error; use miden_assembly::Report; use miden_assembly::diagnostics::reporting::PrintDiagnostic; -use miden_core::Felt; use miden_core::mast::MastForestError; use miden_crypto::merkle::mmr::MmrError; use miden_crypto::merkle::smt::{SmtLeafError, SmtProofError}; @@ -33,9 +32,10 @@ use crate::batch::BatchId; use crate::block::BlockNumber; use crate::note::{ NoteAssets, + NoteAttachment, NoteAttachmentArray, - NoteAttachmentKind, NoteAttachmentScheme, + NoteAttachments, NoteTag, NoteType, Nullifier, @@ -45,6 +45,7 @@ use crate::utils::serde::DeserializationError; use crate::vm::EventId; use crate::{ ACCOUNT_UPDATE_MAX_SIZE, + Felt, MAX_ACCOUNTS_PER_BATCH, MAX_INPUT_NOTES_PER_BATCH, MAX_INPUT_NOTES_PER_TX, @@ -661,28 +662,26 @@ pub enum NoteError { #[error("note tag requires a public note but the note is of type {0}")] PublicNoteRequired(NoteType), #[error( - "note attachment cannot commit to more than {} elements", - NoteAttachmentArray::MAX_NUM_ELEMENTS - )] - NoteAttachmentArraySizeExceeded(usize), - #[error("unknown note attachment kind {0}")] - UnknownNoteAttachmentKind(u8), - #[error("note attachment of kind None must have attachment scheme None")] - AttachmentKindNoneMustHaveAttachmentSchemeNone, - #[error( - "note attachment kind mismatch: header has {header_kind:?} but attachment has {attachment_kind:?}" + "note attachment array must have at least {min} words, got {0}", + min = NoteAttachmentArray::MIN_NUM_WORDS )] - AttachmentKindMismatch { - header_kind: NoteAttachmentKind, - attachment_kind: NoteAttachmentKind, - }, + NoteAttachmentArrayTooFewWords(usize), #[error( - "note attachment scheme mismatch: header has {header_scheme:?} but attachment has {attachment_scheme:?}" + "note attachment array contains {0} words, but the maximum is {max} words", + max = NoteAttachment::MAX_NUM_WORDS )] - AttachmentSchemeMismatch { - header_scheme: NoteAttachmentScheme, - attachment_scheme: NoteAttachmentScheme, - }, + NoteAttachmentArrayTooManyWords(usize), + #[error( + "attachment size {0} exceeds maximum {max}", + max = NoteAttachment::MAX_NUM_WORDS + )] + NoteAttachmentHeaderSizeExceeded(u8), + #[error("{0} attachments were provided but maximum is {max}", max = NoteAttachments::MAX_COUNT)] + TooManyAttachments(usize), + #[error("attachment scheme {0} exceeds maximum value of {max}", max = NoteAttachmentScheme::MAX)] + NoteAttachmentSchemeExceeded(u32), + #[error("attachment scheme value 0 is reserved")] + NoteAttachmentSchemeZeroReserved, #[error("{error_msg}")] Other { error_msg: Box, @@ -859,7 +858,7 @@ pub enum TransactionOutputError { /// Errors that can occur when creating a /// [`PublicOutputNote`](crate::transaction::PublicOutputNote) or -/// [`PrivateNoteHeader`](crate::transaction::PrivateNoteHeader). +/// [`PrivateOutputNote`](crate::transaction::PrivateOutputNote). #[derive(Debug, Error)] pub enum OutputNoteError { #[error("note with id {0} is private but expected a public note")] diff --git a/crates/miden-protocol/src/note/attachment.rs b/crates/miden-protocol/src/note/attachment.rs index fa8e567341..2511bc88f3 100644 --- a/crates/miden-protocol/src/note/attachment.rs +++ b/crates/miden-protocol/src/note/attachment.rs @@ -40,32 +40,28 @@ use crate::{Felt, Hasher, Word}; /// 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 /// content or typing is otherwise undesirable, [`NoteAttachmentScheme::none`] can be used. -#[derive(Debug, Clone, Default, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct NoteAttachment { attachment_scheme: NoteAttachmentScheme, content: NoteAttachmentContent, } impl NoteAttachment { - // CONSTRUCTORS + // CONSTANTS // -------------------------------------------------------------------------------------------- - /// Creates a new [`NoteAttachment`] from a user-defined type and the provided content. + /// The maximum number of words in an attachment. /// - /// # Errors - /// - /// Returns an error if: - /// - The attachment content is [`NoteAttachmentKind::None`] but the scheme is not - /// [`NoteAttachmentScheme::none`]. - pub fn new( - attachment_scheme: NoteAttachmentScheme, - content: NoteAttachmentContent, - ) -> Result { - if content.attachment_kind().is_none() && !attachment_scheme.is_none() { - return Err(NoteError::AttachmentKindNoneMustHaveAttachmentSchemeNone); - } + /// Each element holds roughly 8 bytes of data and so this allows for a maximum of + /// 256 * 32 = 2^13 = 8192 bytes. + pub const MAX_NUM_WORDS: u16 = 256; - Ok(Self { attachment_scheme, content }) + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates a new [`NoteAttachment`] from a user-defined scheme and the provided content. + pub fn new(attachment_scheme: NoteAttachmentScheme, content: NoteAttachmentContent) -> Self { + Self { attachment_scheme, content } } /// Creates a new note attachment with content [`NoteAttachmentContent::Word`] from the provided @@ -78,18 +74,18 @@ impl NoteAttachment { } /// Creates a new note attachment with content [`NoteAttachmentContent::Array`] from the - /// provided set of elements. + /// provided words. /// /// # Errors /// /// Returns an error if: - /// - The maximum number of elements exceeds [`NoteAttachmentArray::MAX_NUM_ELEMENTS`]. + /// - The number of words is less than [`NoteAttachmentArray::MIN_NUM_WORDS`]. + /// - The number of words exceeds [`NoteAttachment::MAX_NUM_WORDS`]. pub fn new_array( attachment_scheme: NoteAttachmentScheme, - elements: Vec, + words: Vec, ) -> Result { - NoteAttachmentContent::new_array(elements) - .map(|content| Self { attachment_scheme, content }) + NoteAttachmentContent::new_array(words).map(|content| Self { attachment_scheme, content }) } // ACCESSORS @@ -100,15 +96,23 @@ impl NoteAttachment { self.attachment_scheme } - /// Returns the attachment kind. - pub fn attachment_kind(&self) -> NoteAttachmentKind { - self.content.attachment_kind() - } - /// Returns a reference to the attachment content. pub fn content(&self) -> &NoteAttachmentContent { &self.content } + + /// Computes the commitment of the attachment. + pub fn to_commitment(&self) -> Word { + 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`]). + pub fn num_words(&self) -> u16 { + self.content.num_words() + } } impl Serializable for NoteAttachment { @@ -127,28 +131,22 @@ impl Deserializable for NoteAttachment { let attachment_scheme = NoteAttachmentScheme::read_from(source)?; let content = NoteAttachmentContent::read_from(source)?; - Self::new(attachment_scheme, content) - .map_err(|err| DeserializationError::InvalidValue(err.to_string())) + Ok(Self::new(attachment_scheme, content)) } } +// NOTE ATTACHMENT CONTENT +// ================================================================================================ + /// The content of a [`NoteAttachment`]. /// -/// If a note attachment is not required, [`NoteAttachmentContent::None`] should be used. -/// -/// When a single [`Word`] has sufficient space, [`NoteAttachmentContent::Word`] should be used, as -/// it does not require any hashing. The word itself is encoded into the -/// [`NoteMetadata`](super::NoteMetadata). +/// 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). -#[derive(Debug, Clone, Default, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum NoteAttachmentContent { - /// Signals the absence of a note attachment. - #[default] - None, - /// A note attachment consisting of a single [`Word`]. Word(Word), @@ -160,77 +158,81 @@ impl NoteAttachmentContent { // CONSTRUCTORS // -------------------------------------------------------------------------------------------- - /// Creates a new [`NoteAttachmentContent::Word`] containing an empty word. - pub fn empty_word() -> Self { - Self::Word(Word::empty()) - } - /// 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 elements. + /// Creates a new [`NoteAttachmentContent::Array`] from the provided words. /// /// # Errors /// /// Returns an error if: - /// - The maximum number of elements exceeds [`NoteAttachmentArray::MAX_NUM_ELEMENTS`]. - pub fn new_array(elements: Vec) -> Result { - NoteAttachmentArray::new(elements).map(Self::from) + /// - The number of words is less than [`NoteAttachmentArray::MIN_NUM_WORDS`]. + /// - The number of words exceeds [`NoteAttachment::MAX_NUM_WORDS`]. + pub fn new_array(words: Vec) -> Result { + NoteAttachmentArray::new(words).map(Self::from) } // ACCESSORS // -------------------------------------------------------------------------------------------- - /// Returns the [`NoteAttachmentKind`]. - pub fn attachment_kind(&self) -> NoteAttachmentKind { - match self { - NoteAttachmentContent::None => NoteAttachmentKind::None, - NoteAttachmentContent::Word(_) => NoteAttachmentKind::Word, - NoteAttachmentContent::Array(_) => NoteAttachmentKind::Array, - } + /// 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 the [`NoteAttachmentContent`] encoded to a [`Word`]. + /// Returns the size of this attachment content in words. /// - /// See the type-level documentation for more details. - pub fn to_word(&self) -> Word { + /// - `1` for [`NoteAttachmentContent::Word`]. + /// - `> 1` for [`NoteAttachmentContent::Array`]. + pub fn num_words(&self) -> u16 { match self { - NoteAttachmentContent::None => Word::empty(), - NoteAttachmentContent::Word(word) => *word, - NoteAttachmentContent::Array(attachment_commitment) => { - attachment_commitment.commitment() - }, + NoteAttachmentContent::Word(_) => 1, + NoteAttachmentContent::Array(array) => array.num_words(), } } + + /// Returns the raw elements of this attachment content. + pub fn to_elements(&self) -> Vec { + ::to_elements(self) + } + + /// Returns the sequential commitment over the content's elements. + pub fn to_commitment(&self) -> Word { + ::to_commitment(self) + } } impl Serializable for NoteAttachmentContent { fn write_into(&self, target: &mut W) { - self.attachment_kind().write_into(target); + // Subtract 1 from num words so we can serialize it as a u8. + let num_words_minus_1 = + 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::None => (), NoteAttachmentContent::Word(word) => { word.write_into(target); }, - NoteAttachmentContent::Array(attachment_commitment) => { - attachment_commitment.num_elements().write_into(target); - target.write_many(&attachment_commitment.elements); + NoteAttachmentContent::Array(array) => { + target.write_many(array.as_words()); }, } } fn get_size_hint(&self) -> usize { - let kind_size = self.attachment_kind().get_size_hint(); + let discriminant_size = core::mem::size_of::(); match self { - NoteAttachmentContent::None => kind_size, - NoteAttachmentContent::Word(word) => kind_size + word.get_size_hint(), - NoteAttachmentContent::Array(attachment_commitment) => { - kind_size - + attachment_commitment.num_elements().get_size_hint() - + attachment_commitment.elements.len() * crate::ZERO.get_size_hint() + NoteAttachmentContent::Word(word) => discriminant_size + word.get_size_hint(), + NoteAttachmentContent::Array(array) => { + discriminant_size + usize::from(array.num_words()) * Word::empty().get_size_hint() }, } } @@ -238,33 +240,53 @@ impl Serializable for NoteAttachmentContent { impl Deserializable for NoteAttachmentContent { fn read_from(source: &mut R) -> Result { - let attachment_kind = NoteAttachmentKind::read_from(source)?; - - match attachment_kind { - NoteAttachmentKind::None => Ok(NoteAttachmentContent::None), - NoteAttachmentKind::Word => { + // Add one to the serialized num words to get the original. + 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)) }, - NoteAttachmentKind::Array => { - let num_elements = u16::read_from(source)?; - let elements = - source.read_many_iter(num_elements as usize)?.collect::>()?; - Self::new_array(elements) + _ => { + let words: Vec = + source.read_many_iter(num_words as usize)?.collect::>()?; + Self::new_array(words) .map_err(|err| DeserializationError::InvalidValue(err.to_string())) }, } } } -// NOTE ATTACHMENT COMMITMENT +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 field -/// elements. +/// The type contained in [`NoteAttachmentContent::Array`] that commits to a set of words. #[derive(Debug, Clone, PartialEq, Eq)] pub struct NoteAttachmentArray { - elements: Vec, + words: Vec, commitment: Word, } @@ -272,44 +294,60 @@ impl NoteAttachmentArray { // CONSTANTS // -------------------------------------------------------------------------------------------- - /// The maximum size of a note attachment that commits to a set of elements. + /// The minimum number of words in a note attachment array. /// - /// Each element holds roughly 8 bytes of data and so this allows for a maximum of - /// 2048 * 8 = 2^14 = 16384 bytes. - pub const MAX_NUM_ELEMENTS: u16 = 2048; + /// 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 elements. + /// Creates a new [`NoteAttachmentArray`] from the provided words. /// /// # Errors /// /// Returns an error if: - /// - The maximum number of elements exceeds [`NoteAttachmentArray::MAX_NUM_ELEMENTS`]. - pub fn new(elements: Vec) -> Result { - if elements.len() > Self::MAX_NUM_ELEMENTS as usize { - return Err(NoteError::NoteAttachmentArraySizeExceeded(elements.len())); + /// - 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 { elements, commitment }) + Ok(Self { words, commitment }) } // ACCESSORS // -------------------------------------------------------------------------------------------- - /// Returns a reference to the elements this note attachment commits to. - pub fn as_slice(&self) -> &[Felt] { - &self.elements + /// Returns a reference to the words this note attachment commits to. + pub fn as_words(&self) -> &[Word] { + &self.words } - /// Returns the number of elements this note attachment commits to. - pub fn num_elements(&self) -> u16 { - u16::try_from(self.elements.len()).expect("type should enforce that size fits in u16") + /// 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 commitment over the contained field 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 } @@ -319,7 +357,7 @@ impl SequentialCommit for NoteAttachmentArray { type Commitment = Word; fn to_elements(&self) -> Vec { - self.elements.clone() + self.as_elements().copied().collect() } fn to_commitment(&self) -> Self::Commitment { @@ -336,28 +374,69 @@ impl From for NoteAttachmentContent { // NOTE ATTACHMENT SCHEME // ================================================================================================ -/// The user-defined type of a [`NoteAttachment`]. +/// The user-defined scheme of a [`NoteAttachment`]. /// -/// A note attachment scheme is an arbitrary 32-bit unsigned integer. +/// A note attachment scheme is an arbitrary 16-bit unsigned integer (max [`Self::MAX`]). It is +/// intended to be used to distinguish one attachment from another, or find a specific attachment in +/// a note's attachments. /// -/// Value `0` is reserved to signal that the scheme is none or absent. Whenever the kind of -/// attachment is not standardized or interoperability is unimportant, this none value can be -/// used. +/// The scheme is purely a hint, and there is no validation with respect to the attachment content. +/// In other words, any scheme can be associated with any attachment content. Hence, users should +/// always validate the contents of an attachment, just like with +/// [`NoteStorage`](super::NoteStorage). +/// +/// Value `0` is reserved to signal that the entire attachment is absent and so it is not a valid +/// scheme. +/// +/// Value `1` is reserved to signal that the scheme is none. Whenever the kind of attachment is not +/// standardized or interoperability is unimportant, this none value can be used. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct NoteAttachmentScheme(u32); +pub struct NoteAttachmentScheme(u16); impl NoteAttachmentScheme { // CONSTANTS // -------------------------------------------------------------------------------------------- - /// The reserved value to signal an absent note attachment scheme. - const NONE: u32 = 0; + /// The reserved value to signal an absent attachment. This is not a valid attachment scheme. + const RESERVED: u16 = 0; + + /// The reserved value to signal a `None` note attachment scheme. + const NONE: u16 = 1; + + /// The maximum value for a note attachment scheme. + /// + /// Limited to `2^16 - 2 = 65534` to ensure the felt encoding remains valid when four + /// schemes are packed into a single felt in the note metadata. Limiting schemes to this value + /// means at least one bit is always unset which ensures felt validity. + pub const MAX: NoteAttachmentScheme = NoteAttachmentScheme(65534); // CONSTRUCTORS // -------------------------------------------------------------------------------------------- - /// Creates a new [`NoteAttachmentScheme`] from a `u32`. - pub const fn new(attachment_scheme: u32) -> Self { + /// Creates a new [`NoteAttachmentScheme`] from a `u16`. + /// + /// # Errors + /// + /// Returns an error if `attachment_scheme` is equal to 0 or exceeds [`Self::MAX`]. + pub fn new(attachment_scheme: u16) -> Result { + if attachment_scheme == Self::RESERVED { + return Err(NoteError::NoteAttachmentSchemeZeroReserved); + } + + if attachment_scheme > Self::MAX.as_u16() { + return Err(NoteError::NoteAttachmentSchemeExceeded(attachment_scheme as u32)); + } + Ok(Self(attachment_scheme)) + } + + /// Creates a new [`NoteAttachmentScheme`] from a `u16`. + /// + /// # Panics + /// + /// Panics if `attachment_scheme` is 0 or exceeds [`Self::MAX`]. + pub const fn new_const(attachment_scheme: u16) -> Self { + assert!(attachment_scheme != Self::RESERVED, "attachment scheme must not be 0"); + assert!(attachment_scheme <= Self::MAX.as_u16(), "attachment scheme exceeds maximum"); Self(attachment_scheme) } @@ -375,12 +454,20 @@ impl NoteAttachmentScheme { // ACCESSORS // -------------------------------------------------------------------------------------------- - /// Returns the note attachment scheme as a u32. - pub const fn as_u32(&self) -> u32 { + /// Returns the note attachment scheme as a u16. + pub const fn as_u16(&self) -> u16 { self.0 } } +impl TryFrom for NoteAttachmentScheme { + type Error = NoteError; + + fn try_from(value: u16) -> Result { + Self::new(value) + } +} + impl Default for NoteAttachmentScheme { /// Returns [`NoteAttachmentScheme::none`]. fn default() -> Self { @@ -396,113 +483,275 @@ impl core::fmt::Display for NoteAttachmentScheme { impl Serializable for NoteAttachmentScheme { fn write_into(&self, target: &mut W) { - self.as_u32().write_into(target); + self.as_u16().write_into(target); } fn get_size_hint(&self) -> usize { - core::mem::size_of::() + core::mem::size_of::() } } impl Deserializable for NoteAttachmentScheme { fn read_from(source: &mut R) -> Result { - let attachment_scheme = u32::read_from(source)?; - Ok(Self::new(attachment_scheme)) + let value = u16::read_from(source)?; + Self::try_from(value).map_err(|err| DeserializationError::InvalidValue(err.to_string())) } } -// NOTE ATTACHMENT KIND +// NOTE ATTACHMENT HEADER // ================================================================================================ -/// The type of [`NoteAttachmentContent`]. +/// The header metadata for a single note attachment. /// -/// See its docs for more details on each type. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -#[repr(u8)] -pub enum NoteAttachmentKind { - /// Signals the absence of a note attachment. - #[default] - None = Self::NONE, +/// Contains the scheme of an attachment, without the actual content data. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct NoteAttachmentHeader { + /// `None` represents an absent note attachment and `Some` a present one. + scheme: Option, +} - /// A note attachment consisting of a single [`Word`]. - Word = Self::WORD, +impl NoteAttachmentHeader { + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- - /// A note attachment consisting of the commitment to a set of felts. - Array = Self::ARRAY, + /// Creates a new [`NoteAttachmentHeader`] from a [`NoteAttachmentScheme`]. + pub fn new(scheme: NoteAttachmentScheme) -> Self { + Self { scheme: Some(scheme) } + } + + /// Creates a new [`NoteAttachmentHeader`] from a [`NoteAttachmentScheme`]. + pub fn new_maybe(scheme: Option) -> Self { + Self { scheme } + } + + /// Returns a header representing the absence of an attachment. + pub const fn absent() -> Self { + Self { scheme: None } + } + + // ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the attachment scheme. + pub const fn scheme(&self) -> Option { + self.scheme + } + + /// Returns the header encoded as a u16. + /// + /// Encodes `None` to 0 using the niche provided by [`NoteAttachmentScheme`]. + pub(super) fn as_u16(&self) -> u16 { + match self.scheme { + None => 0, + Some(scheme) => scheme.as_u16(), + } + } + + /// Returns `true` if this header represents an absent attachment, `false` otherwise. + pub const fn is_absent(&self) -> bool { + self.scheme.is_none() + } } -impl NoteAttachmentKind { +impl Default for NoteAttachmentHeader { + fn default() -> Self { + Self::absent() + } +} + +impl From for NoteAttachmentHeader { + fn from(scheme: NoteAttachmentScheme) -> Self { + NoteAttachmentHeader::new(scheme) + } +} + +impl Serializable for NoteAttachmentHeader { + fn write_into(&self, target: &mut W) { + self.scheme.write_into(target); + } + + fn get_size_hint(&self) -> usize { + self.scheme.get_size_hint() + } +} + +impl Deserializable for NoteAttachmentHeader { + fn read_from(source: &mut R) -> Result { + let scheme = Option::::read_from(source)?; + Ok(Self::new_maybe(scheme)) + } +} + +// NOTE ATTACHMENTS +// ================================================================================================ + +/// A collection of note attachments. +/// +/// Notes can have up to [`Self::MAX_COUNT`] attachments. +/// +/// The commitment to the attachments is defined as: +/// - 0 attachments: `EMPTY_WORD` +/// - 1+ attachments: `hash(ATTACHMENT_0_COMMITMENT || ... || ATTACHMENT_N_COMMITMENT)`, i.e., the +/// sequential hash over the individual attachment commitments. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NoteAttachments { + attachments: Vec, +} + +impl NoteAttachments { // CONSTANTS // -------------------------------------------------------------------------------------------- - const NONE: u8 = 0; - const WORD: u8 = 1; - const ARRAY: u8 = 2; + /// The maximum number of attachments per note. + pub const MAX_COUNT: usize = 4; + + /// The maximum total number of elements across all attachments in a note. + /// + /// Each element holds roughly 8 bytes of data and so this allows for a maximum of + /// 512 * 32 = 2^14 = 16384 bytes. + pub const MAX_NUM_WORDS: u16 = 512; + + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates a new empty [`NoteAttachments`] collection. + pub fn empty() -> Self { + Self { attachments: Vec::new() } + } + + /// Creates a [`NoteAttachments`] from a vector of attachments. + /// + /// # Errors + /// + /// Returns an error if: + /// - The number of attachments exceeds [`Self::MAX_COUNT`]. + /// - The total number of words across all attachments exceeds [`Self::MAX_NUM_WORDS`]. + pub fn new(attachments: Vec) -> Result { + if attachments.len() > Self::MAX_COUNT { + return Err(NoteError::TooManyAttachments(attachments.len())); + } + + let total_num_words = attachments + .iter() + .map(|attachment| attachment.num_words() as usize) + .sum::(); + + if total_num_words > Self::MAX_NUM_WORDS as usize { + return Err(NoteError::NoteAttachmentArrayTooManyWords(total_num_words)); + } + + Ok(Self { attachments }) + } // ACCESSORS // -------------------------------------------------------------------------------------------- - /// Returns the attachment kind as a u8. - pub const fn as_u8(&self) -> u8 { - *self as u8 + /// Returns the attachment at the given index, if it exists. + pub fn get(&self, index: usize) -> Option<&NoteAttachment> { + self.attachments.get(index) } - /// Returns `true` if the attachment kind is `None`, `false` otherwise. - pub const fn is_none(&self) -> bool { - matches!(self, Self::None) + /// Returns the first attachment with the provided scheme, if any. + pub fn find(&self, scheme: NoteAttachmentScheme) -> Option<&NoteAttachment> { + self.attachments + .iter() + .find(|attachment| attachment.attachment_scheme == scheme) } - /// Returns `true` if the attachment kind is `Word`, `false` otherwise. - pub const fn is_word(&self) -> bool { - matches!(self, Self::Word) + /// Returns the number of attachments. + pub fn num_attachments(&self) -> u8 { + u8::try_from(self.attachments.len()) + .expect("constructor should ensure num attachment fits in u8") } - /// Returns `true` if the attachment kind is `Array`, `false` otherwise. - pub const fn is_array(&self) -> bool { - matches!(self, Self::Array) + /// Returns `true` if there are no attachments. + pub fn is_empty(&self) -> bool { + self.attachments.is_empty() } -} -impl TryFrom for NoteAttachmentKind { - type Error = NoteError; + /// Returns an iterator over the attachments. + pub fn iter(&self) -> impl Iterator { + self.attachments.iter() + } + + /// Returns the individual commitment of each contained attachment. + pub fn commitments(&self) -> Vec { + self.attachments + .iter() + .map(|attachment| attachment.content().to_commitment()) + .collect() + } - fn try_from(value: u8) -> Result { - match value { - Self::NONE => Ok(Self::None), - Self::WORD => Ok(Self::Word), - Self::ARRAY => Ok(Self::Array), - _ => Err(NoteError::UnknownNoteAttachmentKind(value)), + /// Returns the commitment over the contained attachments. + pub fn to_commitment(&self) -> Word { + ::to_commitment(self) + } + + /// Returns the attachment headers for all attachment slots. + /// + /// Returns a fixed-size array of [`Self::MAX_COUNT`] headers. Unused slots are filled with + /// [`NoteAttachmentHeader::absent`]. + pub fn to_headers(&self) -> [NoteAttachmentHeader; Self::MAX_COUNT] { + let mut headers = [NoteAttachmentHeader::absent(); Self::MAX_COUNT]; + for (i, attachment) in self.attachments.iter().enumerate() { + headers[i] = NoteAttachmentHeader::new(attachment.attachment_scheme()); } + headers + } + + // CONVERSIONS + // -------------------------------------------------------------------------------------------- + + /// Consumes self and returns the inner vector of attachments. + pub fn into_vec(self) -> Vec { + self.attachments } } -impl core::fmt::Display for NoteAttachmentKind { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - let output = match self { - NoteAttachmentKind::None => "None", - NoteAttachmentKind::Word => "Word", - NoteAttachmentKind::Array => "Array", - }; +impl Default for NoteAttachments { + fn default() -> Self { + Self::empty() + } +} + +impl SequentialCommit for NoteAttachments { + type Commitment = Word; - f.write_str(output) + /// Collects all attachment commitments into a flat vector of field elements. + fn to_elements(&self) -> Vec { + let mut elements = Vec::new(); + for commitment in self.attachments.iter().map(NoteAttachment::to_commitment) { + elements.extend_from_slice(commitment.as_elements()); + } + elements } } -impl Serializable for NoteAttachmentKind { +impl From for NoteAttachments { + fn from(attachment: NoteAttachment) -> Self { + Self::new(vec![attachment]).expect("one attachment does not exceed the max of four") + } +} + +impl Serializable for NoteAttachments { fn write_into(&self, target: &mut W) { - self.as_u8().write_into(target); + self.num_attachments().write_into(target); + target.write_many(&self.attachments); } fn get_size_hint(&self) -> usize { - core::mem::size_of::() + self.num_attachments().get_size_hint() + + self.iter().map(NoteAttachment::get_size_hint).sum::() } } -impl Deserializable for NoteAttachmentKind { +impl Deserializable for NoteAttachments { fn read_from(source: &mut R) -> Result { - let attachment_kind = u8::read_from(source)?; - Self::try_from(attachment_kind) - .map_err(|err| DeserializationError::InvalidValue(err.to_string())) + let num_attachments = u8::read_from(source)? as usize; + let attachments = source + .read_many_iter::(num_attachments)? + .collect::, _>>()?; + Self::new(attachments).map_err(|err| DeserializationError::InvalidValue(err.to_string())) } } @@ -516,11 +765,10 @@ mod tests { use super::*; #[rstest::rstest] - #[case::attachment_none(NoteAttachment::default())] - #[case::attachment_word(NoteAttachment::new_word(NoteAttachmentScheme::new(1), Word::from([3, 4, 5, 6u32])))] + #[case::attachment_word(NoteAttachment::new_word(NoteAttachmentScheme::new(1)?, Word::from([3, 4, 5, 6u32])))] #[case::attachment_array(NoteAttachment::new_array( - NoteAttachmentScheme::new(u32::MAX), - vec![Felt::new(5), Felt::new(6), Felt::new(7)], + NoteAttachmentScheme::MAX, + vec![Word::from([1, 1, 1, 1u32]); 2], )?)] #[test] fn note_attachment_serde(#[case] attachment: NoteAttachment) -> anyhow::Result<()> { @@ -529,22 +777,153 @@ mod tests { } #[test] - fn note_attachment_commitment_fails_on_too_many_elements() -> anyhow::Result<()> { - let too_many_elements = (NoteAttachmentArray::MAX_NUM_ELEMENTS as usize) + 1; - let elements = vec![Felt::from(1u32); too_many_elements]; - let err = NoteAttachmentArray::new(elements).unwrap_err(); + fn note_attachment_array_fails_on_too_many_words() -> anyhow::Result<()> { + let too_many_words = NoteAttachment::MAX_NUM_WORDS as usize + 1; + let words = vec![Word::from([1, 1, 1, 1u32]); too_many_words]; + let err = NoteAttachmentArray::new(words).unwrap_err(); - assert_matches!(err, NoteError::NoteAttachmentArraySizeExceeded(len) => { - len == too_many_elements + assert_matches!(err, NoteError::NoteAttachmentArrayTooManyWords(len) => { + len == too_many_words }); Ok(()) } #[test] - fn note_attachment_kind_fails_on_unknown_variant() -> anyhow::Result<()> { - let err = NoteAttachmentKind::try_from(3u8).unwrap_err(); - assert_matches!(err, NoteError::UnknownNoteAttachmentKind(3u8)); + fn note_attachment_array_fails_on_too_few_words() { + let words = vec![Word::from([1, 1, 1, 1u32]); 1]; + let err = NoteAttachmentArray::new(words).unwrap_err(); + assert_matches!(err, NoteError::NoteAttachmentArrayTooFewWords(1)); + } + + #[test] + fn note_attachment_scheme_max_is_valid() { + let scheme = NoteAttachmentScheme::MAX; + assert_eq!(scheme.as_u16(), 65534); + } + + #[test] + fn note_attachment_scheme_exceeding_max_fails() { + let err = NoteAttachmentScheme::new(u16::MAX).unwrap_err(); + assert_matches!(err, NoteError::NoteAttachmentSchemeExceeded(_)); + } + + #[test] + fn note_attachment_header_serde() -> anyhow::Result<()> { + let header = NoteAttachmentHeader::new(NoteAttachmentScheme::new(42)?); + let deserialized = NoteAttachmentHeader::read_from_bytes(&header.to_bytes())?; + assert_eq!(header, deserialized); + Ok(()) + } + + #[test] + fn note_attachment_header_absent() { + let header = NoteAttachmentHeader::absent(); + assert!(header.is_absent()); + assert!(header.scheme().is_none()); + } + + #[test] + fn note_attachments_up_to_max() -> anyhow::Result<()> { + let scheme = NoteAttachmentScheme::new(1)?; + let attachment = NoteAttachment::new_word(scheme, Word::from([1, 2, 3, 4u32])); + let attachments = NoteAttachments::new(vec![attachment; NoteAttachments::MAX_COUNT])?; + assert_eq!(attachments.num_attachments() as usize, NoteAttachments::MAX_COUNT); + + // Exceeding MAX_COUNT should fail. + let err = + NoteAttachments::new(vec![ + NoteAttachment::new_word(scheme, Word::from([1, 2, 3, 4u32])); + NoteAttachments::MAX_COUNT + 1 + ]) + .unwrap_err(); + assert_matches!(err, NoteError::TooManyAttachments(5)); + + Ok(()) + } + + #[test] + fn note_attachments_serde() -> anyhow::Result<()> { + let attachments = NoteAttachments::new(vec![ + NoteAttachment::new_word(NoteAttachmentScheme::new(1)?, Word::from([1, 2, 3, 4u32])), + NoteAttachment::new_array( + NoteAttachmentScheme::new(100)?, + vec![Word::from([1, 1, 1, 1u32]); 2], + )?, + ])?; + + let deserialized = NoteAttachments::read_from_bytes(&attachments.to_bytes())?; + assert_eq!(attachments, deserialized); + + Ok(()) + } + + #[test] + fn note_attachments_commitment_empty() { + let attachments = NoteAttachments::empty(); + assert_eq!(attachments.to_commitment(), Word::empty()); + } + + #[test] + fn note_attachments_commitment_single_word() -> anyhow::Result<()> { + let word = Word::from([10, 20, 30, 40u32]); + let attachments = NoteAttachments::new(vec![NoteAttachment::new_word( + NoteAttachmentScheme::new(1)?, + word, + )])?; + // Single word attachment: the attachment commitment is hash(word), so the overall + // attachments commitment is hash(hash(word)). + let word_commitment = Hasher::hash_elements(word.as_elements()); + assert_eq!( + attachments.to_commitment(), + Hasher::hash_elements(word_commitment.as_elements()) + ); + + Ok(()) + } + + #[test] + fn note_attachments_to_headers() -> anyhow::Result<()> { + let attachments = NoteAttachments::new(vec![ + NoteAttachment::new_word(NoteAttachmentScheme::new(42)?, Word::from([1, 2, 3, 4u32])), + NoteAttachment::new_array( + NoteAttachmentScheme::new(100)?, + vec![Word::from([1, 1, 1, 1u32]); 2], + )?, + ])?; + + let headers = attachments.to_headers(); + assert_eq!(headers[0].scheme(), Some(NoteAttachmentScheme::new(42)?)); + assert_eq!(headers[1].scheme(), Some(NoteAttachmentScheme::new(100)?)); + assert!(headers[2].is_absent()); + assert!(headers[3].is_absent()); + Ok(()) } + + #[test] + fn note_attachments_into_vec() -> anyhow::Result<()> { + let word_att = + NoteAttachment::new_word(NoteAttachmentScheme::new(1)?, Word::from([1, 2, 3, 4u32])); + let attachments = NoteAttachments::new(vec![word_att.clone()])?; + let vec = attachments.into_vec(); + assert_eq!(vec, vec![word_att]); + + Ok(()) + } + + #[test] + fn note_attachment_num_words() { + // Word => 1 + let word = NoteAttachmentContent::new_word(Word::from([1, 2, 3, 4u32])); + assert_eq!(word.num_words(), 1); + + // Array with 2 words + let array = NoteAttachmentContent::new_array(vec![Word::from([1, 1, 1, 1u32]); 2]).unwrap(); + assert_eq!(array.num_words(), 2); + + // Array with 3 words + let array = NoteAttachmentContent::new_array(vec![Word::from([1, 1, 1, 1u32]); 3]).unwrap(); + assert_eq!(array.num_words(), 3); + } } diff --git a/crates/miden-protocol/src/note/file.rs b/crates/miden-protocol/src/note/file.rs index 44aac4ddfe..8092fafbcb 100644 --- a/crates/miden-protocol/src/note/file.rs +++ b/crates/miden-protocol/src/note/file.rs @@ -25,6 +25,7 @@ const MAGIC: &str = "note"; /// A serialized representation of a note. #[derive(Clone, Debug, PartialEq, Eq)] +#[allow(clippy::large_enum_variant)] pub enum NoteFile { /// The note's details aren't known. NoteId(NoteId), diff --git a/crates/miden-protocol/src/note/header.rs b/crates/miden-protocol/src/note/header.rs index f65b277a2e..6d8db56720 100644 --- a/crates/miden-protocol/src/note/header.rs +++ b/crates/miden-protocol/src/note/header.rs @@ -5,6 +5,7 @@ use super::{ DeserializationError, NoteId, NoteMetadata, + NoteMetadataHeader, Serializable, Word, }; @@ -15,17 +16,17 @@ use crate::Hasher; /// Holds the strictly required, public information of a note. /// -/// See [NoteId] and [NoteMetadata] for additional details. +/// See [NoteId] and [NoteMetadataHeader] for additional details. #[derive(Debug, Clone, PartialEq, Eq)] pub struct NoteHeader { note_id: NoteId, - note_metadata: NoteMetadata, + metadata_header: NoteMetadataHeader, } impl NoteHeader { - /// Returns a new [NoteHeader] instantiated from the specified note ID and metadata. - pub fn new(note_id: NoteId, note_metadata: NoteMetadata) -> Self { - Self { note_id, note_metadata } + /// Returns a new [NoteHeader] instantiated from the specified note ID and metadata header. + pub fn new(note_id: NoteId, metadata_header: NoteMetadataHeader) -> Self { + Self { note_id, metadata_header } } /// Returns the note's identifier. @@ -37,12 +38,22 @@ impl NoteHeader { /// Returns the note's metadata. pub fn metadata(&self) -> &NoteMetadata { - &self.note_metadata + self.metadata_header.metadata() + } + + /// Returns a reference to the note's metadata header. + pub fn metadata_header(&self) -> &NoteMetadataHeader { + &self.metadata_header } /// Consumes self and returns the note header's metadata. pub fn into_metadata(self) -> NoteMetadata { - self.note_metadata + self.metadata_header.into_metadata() + } + + /// Consumes self and returns the note header's metadata header. + pub fn into_metadata_header(self) -> NoteMetadataHeader { + self.metadata_header } /// Returns a commitment to the note and its metadata. @@ -52,7 +63,7 @@ impl NoteHeader { /// This value is used primarily for authenticating notes consumed when they are consumed /// in a transaction. pub fn to_commitment(&self) -> Word { - compute_note_commitment(self.id(), self.metadata()) + compute_note_commitment(self.id(), &self.metadata_header) } } @@ -65,8 +76,8 @@ impl NoteHeader { /// /// This value is used primarily for authenticating notes consumed when they are consumed /// in a transaction. -pub fn compute_note_commitment(id: NoteId, metadata: &NoteMetadata) -> Word { - Hasher::merge(&[id.as_word(), metadata.to_commitment()]) +pub fn compute_note_commitment(id: NoteId, metadata_header: &NoteMetadataHeader) -> Word { + Hasher::merge(&[id.as_word(), metadata_header.to_commitment()]) } // SERIALIZATION @@ -75,19 +86,19 @@ pub fn compute_note_commitment(id: NoteId, metadata: &NoteMetadata) -> Word { impl Serializable for NoteHeader { fn write_into(&self, target: &mut W) { self.note_id.write_into(target); - self.note_metadata.write_into(target); + self.metadata_header.write_into(target); } fn get_size_hint(&self) -> usize { - self.note_id.get_size_hint() + self.note_metadata.get_size_hint() + self.note_id.get_size_hint() + self.metadata_header.get_size_hint() } } impl Deserializable for NoteHeader { fn read_from(source: &mut R) -> Result { let note_id = NoteId::read_from(source)?; - let note_metadata = NoteMetadata::read_from(source)?; + let metadata_header = NoteMetadataHeader::read_from(source)?; - Ok(Self { note_id, note_metadata }) + Ok(Self::new(note_id, metadata_header)) } } diff --git a/crates/miden-protocol/src/note/metadata.rs b/crates/miden-protocol/src/note/metadata.rs index a667f7af85..41d8ba688c 100644 --- a/crates/miden-protocol/src/note/metadata.rs +++ b/crates/miden-protocol/src/note/metadata.rs @@ -11,59 +11,16 @@ use super::{ Word, }; use crate::Hasher; -use crate::errors::NoteError; -use crate::note::{NoteAttachment, NoteAttachmentKind, NoteAttachmentScheme}; - -// CONSTANTS -// ================================================================================================ - -/// The number of bits by which the note type is offset in the first felt of the note metadata. -const NOTE_TYPE_SHIFT: u64 = 4; +use crate::note::{NoteAttachmentHeader, NoteAttachments}; // NOTE METADATA // ================================================================================================ -/// The metadata associated with a note. -/// -/// Note metadata consists of two parts: -/// - The header of the metadata, which consists of: -/// - the sender of the note -/// - the [`NoteType`] -/// - the [`NoteTag`] -/// - type information about the [`NoteAttachment`]. -/// - The optional [`NoteAttachment`]. -/// -/// # Word layout & validity +/// The user-facing metadata associated with a note. /// -/// [`NoteMetadata`] can be encoded into two words, a header and an attachment word. -/// -/// The header word has the following layout: -/// -/// ```text -/// 0th felt: [sender_id_suffix (56 bits) | reserved (3 bits) | note_type (1 bit) | version (4 bits)] -/// 1st felt: [sender_id_prefix (64 bits)] -/// 2nd felt: [32 zero bits | note_tag (32 bits)] -/// 3rd felt: [30 zero bits | attachment_kind (2 bits) | attachment_scheme (32 bits)] -/// ``` -/// -/// The felt validity of each part of the layout is guaranteed: -/// - 1st felt: The lower 8 bits of the account ID suffix are `0` by construction, so that they can -/// be overwritten with other data. The suffix' most significant bit must be zero such that the -/// entire felt retains its validity even if all of its lower 8 bits are set to `1`. So the note -/// type and version can be comfortably encoded. -/// - 2nd felt: Is equivalent to the prefix of the account ID so it inherits its validity. -/// - 3rd felt: The upper 32 bits are always zero. -/// - 4th felt: The upper 30 bits are always zero. -/// -/// The version is hardcoded to 0 and is reserved to make it easier to introduce another version. -/// -/// The value of the attachment word depends on the -/// [`NoteAttachmentKind`](crate::note::NoteAttachmentKind): -/// - [`NoteAttachmentKind::None`](crate::note::NoteAttachmentKind::None): Empty word. -/// - [`NoteAttachmentKind::Word`](crate::note::NoteAttachmentKind::Word): The raw word itself. -/// - [`NoteAttachmentKind::Array`](crate::note::NoteAttachmentKind::Array): The commitment to the -/// elements. -#[derive(Clone, Debug, Eq, PartialEq)] +/// 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)] pub struct NoteMetadata { /// The ID of the account which created the note. sender: AccountId, @@ -73,20 +30,9 @@ pub struct NoteMetadata { /// A value which can be used by the recipient(s) to identify notes intended for them. tag: NoteTag, - - /// The optional attachment of a note's metadata. - /// - /// Defaults to [`NoteAttachment::default`]. - attachment: NoteAttachment, } impl NoteMetadata { - /// Version 1 of the note metadata encoding. - /// - /// If we make this public, we may want to instead consider introducing a `NoteMetadataVersion` - /// struct, similar to `AccountIdVersion`. - const VERSION_1: u8 = 0; - // CONSTRUCTORS // -------------------------------------------------------------------------------------------- @@ -99,42 +45,9 @@ impl NoteMetadata { sender, note_type, tag: NoteTag::default(), - attachment: NoteAttachment::default(), } } - /// Reconstructs a [`NoteMetadata`] from a [`NoteMetadataHeader`] and a - /// [`NoteAttachment`]. - /// - /// # Errors - /// - /// Returns an error if the attachment's kind or scheme do not match those in the header. - pub fn try_from_header( - header: NoteMetadataHeader, - attachment: NoteAttachment, - ) -> Result { - if header.attachment_kind != attachment.attachment_kind() { - return Err(NoteError::AttachmentKindMismatch { - header_kind: header.attachment_kind, - attachment_kind: attachment.attachment_kind(), - }); - } - - if header.attachment_scheme != attachment.attachment_scheme() { - return Err(NoteError::AttachmentSchemeMismatch { - header_scheme: header.attachment_scheme, - attachment_scheme: attachment.attachment_scheme(), - }); - } - - Ok(Self { - sender: header.sender, - note_type: header.note_type, - tag: header.tag, - attachment, - }) - } - // ACCESSORS // -------------------------------------------------------------------------------------------- @@ -153,52 +66,11 @@ impl NoteMetadata { self.tag } - /// Returns the attachment of the note. - pub fn attachment(&self) -> &NoteAttachment { - &self.attachment - } - /// Returns `true` if the note is private. pub fn is_private(&self) -> bool { self.note_type == NoteType::Private } - /// Returns the header of a [`NoteMetadata`] as a [`Word`]. - /// - /// See [`NoteMetadata`] docs for more details. - pub fn to_header(&self) -> NoteMetadataHeader { - NoteMetadataHeader { - sender: self.sender, - note_type: self.note_type, - tag: self.tag, - attachment_kind: self.attachment().content().attachment_kind(), - attachment_scheme: self.attachment.attachment_scheme(), - } - } - - /// Returns the [`Word`] that represents the header of a [`NoteMetadata`]. - /// - /// See [`NoteMetadata`] docs for more details. - pub fn to_header_word(&self) -> Word { - Word::from(self.to_header()) - } - - /// Returns the [`Word`] that represents the attachment of a [`NoteMetadata`]. - /// - /// See [`NoteMetadata`] docs for more details. - pub fn to_attachment_word(&self) -> Word { - self.attachment.content().to_word() - } - - /// Returns the commitment to the note metadata, which is defined as: - /// - /// ```text - /// hash(NOTE_METADATA_HEADER || NOTE_METADATA_ATTACHMENT) - /// ``` - pub fn to_commitment(&self) -> Word { - Hasher::merge(&[self.to_header_word(), self.to_attachment_word()]) - } - // MUTATORS // -------------------------------------------------------------------------------------------- @@ -214,19 +86,6 @@ impl NoteMetadata { self.tag = tag; self } - - /// Mutates the note's attachment by setting it to the provided value. - pub fn set_attachment(&mut self, attachment: NoteAttachment) { - self.attachment = attachment; - } - - /// Returns a new [`NoteMetadata`] with the attachment set to the provided value. - /// - /// This is a builder method that consumes self and returns a new instance for method chaining. - pub fn with_attachment(mut self, attachment: NoteAttachment) -> Self { - self.attachment = attachment; - self - } } // SERIALIZATION @@ -237,14 +96,12 @@ impl Serializable for NoteMetadata { self.note_type().write_into(target); self.sender().write_into(target); self.tag().write_into(target); - self.attachment().write_into(target); } fn get_size_hint(&self) -> usize { self.note_type().get_size_hint() + self.sender().get_size_hint() + self.tag().get_size_hint() - + self.attachment().get_size_hint() } } @@ -253,98 +110,180 @@ impl Deserializable for NoteMetadata { let note_type = NoteType::read_from(source)?; let sender = AccountId::read_from(source)?; let tag = NoteTag::read_from(source)?; - let attachment = NoteAttachment::read_from(source)?; - Ok(NoteMetadata::new(sender, note_type).with_tag(tag).with_attachment(attachment)) + Ok(NoteMetadata::new(sender, note_type).with_tag(tag)) } } // NOTE METADATA HEADER // ================================================================================================ -/// The header representation of [`NoteMetadata`]. +/// Protocol-level note metadata header that combines [`NoteMetadata`] with attachment information. /// -/// See the metadata's type for details on this type's [`Word`] layout. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +/// This type wraps `NoteMetadata` together with attachment headers and an attachment commitment, +/// and knows how to encode them into a [`Word`] and compute commitments. +/// +/// The metadata word is encoded as a single [`Word`] (4 felts) with the following layout: +/// +/// ```text +/// 0th felt: [sender_id_suffix (56 bits) | reserved (3 bits) | note_type (1 bit) | version (4 bits)] +/// 1st felt: [sender_id_prefix (64 bits)] +/// 2nd felt: [reserved (32 bits) | note_tag (32 bits)] +/// 3rd felt: [attachment_3_scheme (16 bits) | attachment_2_scheme (16 bits) | +/// attachment_1_scheme (16 bits) | attachment_0_scheme (16 bits)] +/// ``` +/// +/// Felt validity is guaranteed: +/// - 0th felt: The lower 8 bits of the account ID suffix are `0` by construction, so they can be +/// overwritten. The suffix's MSB is zero so the felt stays valid when lower bits are set. +/// - 1st felt: Equivalent to the account ID prefix, so it inherits its validity. +/// - 2nd felt: The tag is a u32 and the reserved bits are _currently_ set to zero, however users +/// shouldn't assume these are zero. +/// - 3rd felt: Max value is `0xFFFEFFFE_FFFEFFFE` (schemes capped at 65534), which is less than +/// `p`. +/// +/// The version is hardcoded to 0 and is reserved for forward compatibility. +#[derive(Debug, Clone, Eq, PartialEq)] pub struct NoteMetadataHeader { - sender: AccountId, - note_type: NoteType, - tag: NoteTag, - attachment_kind: NoteAttachmentKind, - attachment_scheme: NoteAttachmentScheme, + metadata: NoteMetadata, + attachment_headers: [NoteAttachmentHeader; NoteAttachments::MAX_COUNT], + attachments_commitment: Word, } impl NoteMetadataHeader { + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + /// The number of bits by which the note type is offset in the first felt of the metadata word. + const NOTE_TYPE_SHIFT: u64 = 4; + + /// Version 1 of the note metadata encoding. + /// + /// If we make this public, we may want to instead consider introducing a `NoteMetadataVersion` + /// struct, similar to `AccountIdVersion`. + const VERSION_1: u8 = 0; + + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Returns a new [`NoteMetadataHeader`] derived from the given metadata and attachments. + /// + /// The attachment headers and commitment are derived from the provided attachments. + pub fn new(metadata: NoteMetadata, attachments: &NoteAttachments) -> Self { + Self::from_parts(metadata, attachments.to_headers(), attachments.to_commitment()) + } + + /// Creates a [`NoteMetadataHeader`] from its raw parts. + /// + /// Prefer [`Self::new`] whenever possible. + pub fn from_parts( + metadata: NoteMetadata, + attachment_headers: [NoteAttachmentHeader; NoteAttachments::MAX_COUNT], + attachments_commitment: Word, + ) -> Self { + Self { + metadata, + attachment_headers, + attachments_commitment, + } + } + // ACCESSORS // -------------------------------------------------------------------------------------------- - /// Returns the account which created the note. - pub fn sender(&self) -> AccountId { - self.sender + /// Returns the inner [`NoteMetadata`]. + pub fn metadata(&self) -> &NoteMetadata { + &self.metadata } - /// Returns the note's type. - pub fn note_type(&self) -> NoteType { - self.note_type + /// Returns the attachment headers. + pub fn attachment_headers(&self) -> &[NoteAttachmentHeader; NoteAttachments::MAX_COUNT] { + &self.attachment_headers } - /// Returns the tag associated with the note. - pub fn tag(&self) -> NoteTag { - self.tag + /// Returns the attachments commitment. + pub fn attachments_commitment(&self) -> Word { + self.attachments_commitment + } + + /// Returns the metadata encoded as a [`Word`]. + /// + /// See [`NoteMetadataHeader`] docs for the layout. + pub fn to_metadata_word(&self) -> Word { + let mut word = Word::empty(); + word[0] = merge_sender_suffix_and_note_type( + self.metadata.sender.suffix(), + self.metadata.note_type, + ); + word[1] = self.metadata.sender.prefix().as_felt(); + word[2] = self.metadata.tag.into(); + word[3] = merge_schemes(self.attachment_headers); + word } - /// Returns the attachment kind. - pub fn attachment_kind(&self) -> NoteAttachmentKind { - self.attachment_kind + /// Returns the commitment to the note metadata, which is defined as: + /// + /// ```text + /// hash(NOTE_METADATA_WORD || ATTACHMENTS_COMMITMENT) + /// ``` + pub fn to_commitment(&self) -> Word { + Hasher::merge(&[self.to_metadata_word(), self.attachments_commitment]) } - /// Returns the attachment scheme. - pub fn attachment_scheme(&self) -> NoteAttachmentScheme { - self.attachment_scheme + /// Consumes self and returns the inner [`NoteMetadata`]. + pub fn into_metadata(self) -> NoteMetadata { + self.metadata } } -impl From for Word { - fn from(header: NoteMetadataHeader) -> Self { - let mut metadata = Word::empty(); +impl Serializable for NoteMetadataHeader { + fn write_into(&self, target: &mut W) { + self.metadata.write_into(target); - metadata[0] = merge_sender_suffix_and_note_type(header.sender.suffix(), header.note_type); - metadata[1] = header.sender.prefix().as_felt(); - metadata[2] = Felt::from(header.tag); - metadata[3] = - merge_attachment_kind_scheme(header.attachment_kind, header.attachment_scheme); + let present_headers_iter = + self.attachment_headers.iter().filter(|header| !header.is_absent()); - metadata + let num_headers_present = u8::try_from(present_headers_iter.clone().count()) + .expect("num attachments is validated to be at most 4"); + num_headers_present.write_into(target); + target.write_many(present_headers_iter); + + self.attachments_commitment.write_into(target); + } + + fn get_size_hint(&self) -> usize { + self.metadata.get_size_hint() + + core::mem::size_of::() + + self + .attachment_headers + .iter() + .filter(|header| !header.is_absent()) + .map(NoteAttachmentHeader::get_size_hint) + .sum::() + + self.attachments_commitment.get_size_hint() } } -impl TryFrom for NoteMetadataHeader { - type Error = NoteError; - - /// Decodes a [`NoteMetadataHeader`] from a [`Word`]. - fn try_from(word: Word) -> Result { - let (sender_suffix, note_type) = unmerge_sender_suffix_and_note_type(word[0])?; - let sender_prefix = word[1]; - let tag = u32::try_from(word[2].as_canonical_u64()).map(NoteTag::new).map_err(|_| { - NoteError::other("failed to convert note tag from metadata header to u32") - })?; - let (attachment_kind, attachment_scheme) = unmerge_attachment_kind_scheme(word[3])?; - - let sender = - AccountId::try_from_elements(sender_suffix, sender_prefix).map_err(|source| { - NoteError::other_with_source( - "failed to decode account ID from metadata header", - source, - ) - })?; - - Ok(Self { - sender, - note_type, - tag, - attachment_kind, - attachment_scheme, - }) +impl Deserializable for NoteMetadataHeader { + fn read_from(source: &mut R) -> Result { + let metadata = NoteMetadata::read_from(source)?; + + let num_headers_present = u8::read_from(source)? as usize; + if num_headers_present > NoteAttachments::MAX_COUNT { + return Err(DeserializationError::InvalidValue(format!( + "number of attachment headers ({num_headers_present}) exceeds maximum ({})", + NoteAttachments::MAX_COUNT + ))); + } + + let mut attachment_headers = [NoteAttachmentHeader::absent(); NoteAttachments::MAX_COUNT]; + for header in attachment_headers.iter_mut().take(num_headers_present) { + *header = NoteAttachmentHeader::read_from(source)?; + } + + let attachment_commitment = Word::read_from(source)?; + + Ok(Self::from_parts(metadata, attachment_headers, attachment_commitment)) } } @@ -368,82 +307,32 @@ fn merge_sender_suffix_and_note_type(sender_id_suffix: Felt, note_type: NoteType let note_type_byte = note_type as u8; debug_assert!(note_type_byte < 2, "note type must not contain values >= 2"); - // note_type at bit 4, version at bits 0..=3 (hardcoded to NoteMetadata::VERSION_1) - merged |= (note_type_byte as u64) << NOTE_TYPE_SHIFT; - merged |= NoteMetadata::VERSION_1 as u64; + // note_type at bit 4, version at bits 0..=3 (hardcoded to NoteMetadataHeader::VERSION_1) + merged |= (note_type_byte as u64) << NoteMetadataHeader::NOTE_TYPE_SHIFT; + merged |= NoteMetadataHeader::VERSION_1 as u64; // SAFETY: The most significant bit of the suffix is zero by construction so the u64 will be a // valid felt. Felt::try_from(merged).expect("encoded value should be a valid felt") } -/// Unmerges the sender ID suffix and note metadata (note type and version). -fn unmerge_sender_suffix_and_note_type(element: Felt) -> Result<(Felt, NoteType), NoteError> { - // The mask that clears out the lower 8 bits to recover the sender suffix. - const SENDER_SUFFIX_MASK: u64 = 0xffff_ffff_ffff_ff00; - - let raw = element.as_canonical_u64(); - let version = (raw & 0b1111) as u8; - let note_type_bit = ((raw >> NOTE_TYPE_SHIFT) & 0b1) as u8; - let reserved = ((raw >> 5) & 0b111) as u8; - - if reserved != 0 { - return Err(NoteError::other("reserved bits in note metadata header must be zero")); - } - - if version != NoteMetadata::VERSION_1 { - return Err(NoteError::other(format!( - "unsupported note metadata version {version}, expected {}", - NoteMetadata::VERSION_1 - ))); - } - - let note_type = NoteType::try_from(note_type_bit).map_err(|source| { - NoteError::other_with_source("failed to decode note type from metadata header", source) - })?; - - // No bits were set so felt should still be valid. - let sender_suffix = - Felt::try_from(raw & SENDER_SUFFIX_MASK).expect("felt should still be valid"); - - Ok((sender_suffix, note_type)) -} - -/// Merges the [`NoteAttachmentScheme`] and [`NoteAttachmentKind`] into a single [`Felt`]. +/// Merges four attachment schemes into a single [`Felt`]. /// /// The layout is as follows: /// /// ```text -/// [30 zero bits | attachment_kind (2 bits) | attachment_scheme (32 bits)] +/// [attachment_3_scheme (16 bits) | attachment_2_scheme (16 bits) | +/// attachment_1_scheme (16 bits) | attachment_0_scheme (16 bits)] /// ``` -fn merge_attachment_kind_scheme( - attachment_kind: NoteAttachmentKind, - attachment_scheme: NoteAttachmentScheme, -) -> Felt { - debug_assert!(attachment_kind.as_u8() < 4, "attachment kind should fit into two bits"); - let mut merged = (attachment_kind.as_u8() as u64) << 32; - let attachment_scheme = attachment_scheme.as_u32(); - merged |= attachment_scheme as u64; - - Felt::try_from(merged).expect("the upper bit should be zero and the felt therefore valid") -} - -/// Unmerges the attachment kind and attachment scheme. -fn unmerge_attachment_kind_scheme( - element: Felt, -) -> Result<(NoteAttachmentKind, NoteAttachmentScheme), NoteError> { - let attachment_scheme = element.as_canonical_u64() as u32; - let attachment_kind = (element.as_canonical_u64() >> 32) as u8; - - let attachment_scheme = NoteAttachmentScheme::new(attachment_scheme); - let attachment_kind = NoteAttachmentKind::try_from(attachment_kind).map_err(|source| { - NoteError::other_with_source( - "failed to decode attachment kind from metadata header", - source, - ) - })?; - - Ok((attachment_kind, attachment_scheme)) +/// +/// Max value: `0xFFFEFFFE_FFFEFFFE` < p. Schemes are capped at 65534. +fn merge_schemes(headers: [NoteAttachmentHeader; NoteAttachments::MAX_COUNT]) -> Felt { + let mut merged: u64 = headers[0].as_u16() as u64; + merged |= (headers[1].as_u16() as u64) << 16; + merged |= (headers[2].as_u16() as u64) << 32; + merged |= (headers[3].as_u16() as u64) << 48; + + Felt::try_from(merged).expect("encoded value should be a valid felt (schemes <= 65534)") } // TESTS @@ -453,33 +342,73 @@ fn unmerge_attachment_kind_scheme( mod tests { use super::*; - use crate::note::NoteAttachmentScheme; + use crate::note::{NoteAttachment, NoteAttachmentArray, NoteAttachmentScheme}; use crate::testing::account_id::ACCOUNT_ID_MAX_ONES; + #[test] + fn note_metadata_word_encodes_attachment_header() -> anyhow::Result<()> { + let sender = AccountId::try_from(ACCOUNT_ID_MAX_ONES).unwrap(); + let metadata = NoteMetadata::new(sender, NoteType::Public).with_tag(NoteTag::new(0xff)); + let attachment0 = NoteAttachment::new_word( + NoteAttachmentScheme::new(1)?, + Word::from([10, 20, 30, 40u32]), + ); + let attachment1 = NoteAttachment::new_array( + NoteAttachmentScheme::new(0xfffe)?, + vec![Word::from([10, 20, 30, 40u32]), Word::from([10, 20, 30, 40u32])], + )?; + let attachments = NoteAttachments::new(vec![attachment0, attachment1])?; + let metadata_header = NoteMetadataHeader::new(metadata, &attachments); + + let encoded = metadata_header.to_metadata_word(); + + let tag = encoded[2].as_canonical_u64(); + assert_eq!(tag, 0x0000_0000_0000_00ff); + + let schemes = encoded[3].as_canonical_u64(); + // scheme 3 and 4 are 0, 2 is 0xfffe, 1 is 0x1 + assert_eq!(schemes, 0x0000_0000_fffe_0001); + + Ok(()) + } + #[rstest::rstest] - #[case::attachment_none(NoteAttachment::default())] - #[case::attachment_raw(NoteAttachment::new_word(NoteAttachmentScheme::new(0), Word::from([3, 4, 5, 6u32])))] - #[case::attachment_commitment(NoteAttachment::new_array( - NoteAttachmentScheme::new(u32::MAX), - vec![Felt::new(5), Felt::new(6), Felt::new(7)], - )?)] + #[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])), + ])] + #[case::attachment_word_and_two_arrays([ + 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], + )?, + NoteAttachment::new_array( + NoteAttachmentScheme::MAX, + vec![Word::from([10, 10, 10, 10u32]); NoteAttachment::MAX_NUM_WORDS as usize], + )?, + ])] #[test] - fn note_metadata_serde(#[case] attachment: NoteAttachment) -> anyhow::Result<()> { + fn note_metadata_serde( + #[case] attachments: impl IntoIterator, + ) -> anyhow::Result<()> { // Use the Account ID with the maximum one bits to test if the merge function always // produces valid felts. let sender = AccountId::try_from(ACCOUNT_ID_MAX_ONES).unwrap(); let note_type = NoteType::Public; let tag = NoteTag::new(u32::MAX); - let metadata = - NoteMetadata::new(sender, note_type).with_tag(tag).with_attachment(attachment); + 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); - // Serialization Roundtrip + // Metadata Roundtrip let deserialized = NoteMetadata::read_from_bytes(&metadata.to_bytes())?; assert_eq!(deserialized, metadata); // Metadata Header Roundtrip - let header = NoteMetadataHeader::try_from(metadata.to_header_word())?; - assert_eq!(header, metadata.to_header()); + let header = NoteMetadataHeader::read_from_bytes(&metadata_header.to_bytes())?; + assert_eq!(header, metadata_header); Ok(()) } diff --git a/crates/miden-protocol/src/note/mod.rs b/crates/miden-protocol/src/note/mod.rs index dda18a07ee..bf2888cc8f 100644 --- a/crates/miden-protocol/src/note/mod.rs +++ b/crates/miden-protocol/src/note/mod.rs @@ -31,8 +31,9 @@ pub use attachment::{ NoteAttachment, NoteAttachmentArray, NoteAttachmentContent, - NoteAttachmentKind, + NoteAttachmentHeader, NoteAttachmentScheme, + NoteAttachments, }; mod note_id; @@ -68,9 +69,10 @@ pub use file::NoteFile; /// A note with all the data required for it to be consumed by executing it against the transaction /// kernel. /// -/// Notes consist of note metadata and details. Note metadata is always public, but details may be -/// either public, encrypted, or private, depending on the note type. Note details consist of note -/// assets, script, storage, and a serial number, the three latter grouped into a recipient object. +/// Notes consist of note metadata, attachments and details. Note metadata and attachments are +/// always public, but details are either private or public, depending on the note type. Note +/// details consist of note assets, script, storage, and a serial number, the three latter grouped +/// into a recipient object. /// /// Note details can be reduced to two unique identifiers: [NoteId] and [Nullifier]. The former is /// publicly associated with a note, while the latter is known only to entities which have access @@ -90,6 +92,7 @@ pub use file::NoteFile; pub struct Note { header: NoteHeader, details: NoteDetails, + attachments: NoteAttachments, nullifier: Nullifier, } @@ -98,13 +101,24 @@ impl Note { // CONSTRUCTOR // -------------------------------------------------------------------------------------------- - /// Returns a new [Note] created with the specified parameters. + /// Returns a new [Note] created with the specified parameters and empty attachments. pub fn new(assets: NoteAssets, metadata: NoteMetadata, recipient: NoteRecipient) -> Self { + Self::with_attachments(assets, metadata, recipient, NoteAttachments::default()) + } + + /// Returns a new [Note] created with the specified parameters and attachments. + pub fn with_attachments( + assets: NoteAssets, + metadata: NoteMetadata, + recipient: NoteRecipient, + attachments: NoteAttachments, + ) -> Self { let details = NoteDetails::new(assets, recipient); - let header = NoteHeader::new(details.id(), metadata); + let metadata_header = NoteMetadataHeader::new(metadata, &attachments); + let header = NoteHeader::new(details.id(), metadata_header); let nullifier = details.nullifier(); - Self { header, details, nullifier } + Self { header, details, attachments, nullifier } } // PUBLIC ACCESSORS @@ -159,6 +173,16 @@ impl Note { self.nullifier } + /// Returns the note's attachments. + pub fn attachments(&self) -> &NoteAttachments { + &self.attachments + } + + /// Returns a reference to the note's metadata header. + pub fn metadata_header(&self) -> &NoteMetadataHeader { + self.header.metadata_header() + } + /// Returns a commitment to the note and its metadata. /// /// > hash(NOTE_ID || NOTE_METADATA_COMMITMENT) @@ -178,10 +202,10 @@ impl Note { } /// Consumes self and returns the underlying parts of the [`Note`]. - pub fn into_parts(self) -> (NoteAssets, NoteMetadata, NoteRecipient) { + pub fn into_parts(self) -> (NoteAssets, NoteMetadata, NoteRecipient, NoteAttachments) { let (assets, recipient) = self.details.into_parts(); let metadata = self.header.into_metadata(); - (assets, metadata, recipient) + (assets, metadata, recipient, self.attachments) } } @@ -218,7 +242,13 @@ impl From for NoteDetails { impl From for PartialNote { fn from(note: Note) -> Self { let (assets, recipient, ..) = note.details.into_parts(); - PartialNote::new(note.header.into_metadata(), recipient.digest(), assets) + PartialNote::new(note.header.into_metadata(), recipient.digest(), assets, note.attachments) + } +} + +impl From<&Note> for NoteHeader { + fn from(note: &Note) -> Self { + note.header.clone() } } @@ -230,6 +260,7 @@ impl Serializable for Note { let Self { header, details, + attachments, // nullifier is not serialized as it can be computed from the rest of the data nullifier: _, @@ -238,10 +269,13 @@ impl Serializable for Note { // only metadata is serialized as note ID can be computed from note details header.metadata().write_into(target); details.write_into(target); + attachments.write_into(target); } fn get_size_hint(&self) -> usize { - self.header.metadata().get_size_hint() + self.details.get_size_hint() + self.header.metadata().get_size_hint() + + self.details.get_size_hint() + + self.attachments.get_size_hint() } } @@ -249,8 +283,9 @@ impl Deserializable for Note { fn read_from(source: &mut R) -> Result { let metadata = NoteMetadata::read_from(source)?; let details = NoteDetails::read_from(source)?; + let attachments = NoteAttachments::read_from(source)?; let (assets, recipient) = details.into_parts(); - Ok(Self::new(assets, metadata, recipient)) + Ok(Self::with_attachments(assets, metadata, recipient, attachments)) } } diff --git a/crates/miden-protocol/src/note/partial.rs b/crates/miden-protocol/src/note/partial.rs index a75dfa6591..7a2333ef5c 100644 --- a/crates/miden-protocol/src/note/partial.rs +++ b/crates/miden-protocol/src/note/partial.rs @@ -4,9 +4,11 @@ use super::{ Deserializable, DeserializationError, NoteAssets, + NoteAttachments, NoteHeader, NoteId, NoteMetadata, + NoteMetadataHeader, Serializable, }; use crate::Word; @@ -16,8 +18,8 @@ use crate::Word; /// Partial information about a note. /// -/// Partial note consists of [NoteMetadata], [NoteAssets], and a recipient digest (see -/// [super::NoteRecipient]). However, it does not contain detailed recipient info, including +/// Partial note consists of [NoteMetadata], [NoteAssets], [NoteAttachments], and a recipient digest +/// (see [super::NoteRecipient]). However, it does not contain detailed recipient info, including /// note script, note storage, and note's serial number. This means that a partial note is /// sufficient to compute note ID and note header, but not sufficient to compute note nullifier, /// and generally does not have enough info to execute the note. @@ -26,14 +28,26 @@ pub struct PartialNote { header: NoteHeader, recipient_digest: Word, assets: NoteAssets, + attachments: NoteAttachments, } impl PartialNote { /// Returns a new [PartialNote] instantiated from the provided parameters. - pub fn new(metadata: NoteMetadata, recipient_digest: Word, assets: NoteAssets) -> Self { + pub fn new( + metadata: NoteMetadata, + recipient_digest: Word, + assets: NoteAssets, + attachments: NoteAttachments, + ) -> Self { let note_id = NoteId::new(recipient_digest, assets.commitment()); - let header = NoteHeader::new(note_id, metadata); - Self { header, recipient_digest, assets } + let metadata_header = NoteMetadataHeader::new(metadata, &attachments); + let header = NoteHeader::new(note_id, metadata_header); + Self { + header, + recipient_digest, + assets, + attachments, + } } /// Returns the ID corresponding to this note. @@ -58,14 +72,24 @@ impl PartialNote { &self.assets } + /// Returns the note's attachments. + pub fn attachments(&self) -> &NoteAttachments { + &self.attachments + } + + /// Returns a reference to the [`NoteMetadataHeader`] of this note. + pub fn metadata_header(&self) -> &NoteMetadataHeader { + self.header.metadata_header() + } + /// Returns the [`NoteHeader`] of this note. pub fn header(&self) -> &NoteHeader { &self.header } /// Consumes self and returns the non-Copy parts of this note. - pub fn into_parts(self) -> (NoteAssets, NoteHeader) { - (self.assets, self.header) + pub fn into_parts(self) -> (NoteAssets, NoteHeader, NoteAttachments) { + (self.assets, self.header, self.attachments) } } @@ -78,11 +102,15 @@ impl Serializable for PartialNote { // remaining data. self.header().metadata().write_into(target); self.recipient_digest.write_into(target); - self.assets.write_into(target) + self.assets.write_into(target); + self.attachments.write_into(target); } fn get_size_hint(&self) -> usize { - self.metadata().get_size_hint() + Word::SERIALIZED_SIZE + self.assets.get_size_hint() + self.metadata().get_size_hint() + + Word::SERIALIZED_SIZE + + self.assets.get_size_hint() + + self.attachments.get_size_hint() } } @@ -91,7 +119,8 @@ impl Deserializable for PartialNote { let metadata = NoteMetadata::read_from(source)?; let recipient_digest = Word::read_from(source)?; let assets = NoteAssets::read_from(source)?; + let attachments = NoteAttachments::read_from(source)?; - Ok(Self::new(metadata, recipient_digest, assets)) + Ok(Self::new(metadata, recipient_digest, assets, attachments)) } } diff --git a/crates/miden-protocol/src/testing/block_note_tree.rs b/crates/miden-protocol/src/testing/block_note_tree.rs index 3304527f9a..509ed031ac 100644 --- a/crates/miden-protocol/src/testing/block_note_tree.rs +++ b/crates/miden-protocol/src/testing/block_note_tree.rs @@ -19,7 +19,7 @@ impl BlockNoteTree { // SAFETY: This is only called from test code. Reconsider if this changes. let block_note_index = BlockNoteIndex::new(batch_idx, *note_idx_in_batch) .expect("output note batch indices should fit into a block"); - (block_note_index, note.id(), note.metadata()) + (block_note_index, note.id(), note.metadata_header()) }) }); diff --git a/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs b/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs index 5e480a3368..c407bbe3ed 100644 --- a/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs +++ b/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs @@ -6,7 +6,6 @@ use crate::account::{AccountHeader, PartialAccount}; use crate::block::account_tree::{AccountIdKey, AccountWitness}; use crate::crypto::SequentialCommit; use crate::crypto::merkle::InnerNodeInfo; -use crate::note::NoteAttachmentContent; use crate::transaction::{ AccountInputs, InputNote, @@ -339,14 +338,23 @@ impl TransactionAdviceInputs { self.add_map_entry(recipient.storage().commitment(), recipient.storage().to_elements()); // assets commitments self.add_map_entry(assets.commitment(), assets.to_elements()); - // array attachments - if let NoteAttachmentContent::Array(array_attachment) = - note.metadata().attachment().content() - { - self.add_map_entry( - array_attachment.commitment(), - array_attachment.as_slice().to_vec(), - ); + + // ATTACHMENTS_COMMITMENT |-> [[ATTACHMENT_COMMITMENTS]] + self.add_map_entry( + note.attachments().to_commitment(), + note.attachments() + .commitments() + .iter() + .flat_map(Word::as_elements) + .copied() + .collect(), + ); + + // ATTACHMENT_COMMITMENT |-> [ATTACHMENT_ELEMENTS] for each attachment + for attachment in note.attachments().iter() { + let commitment = attachment.content().to_commitment(); + let elements = attachment.content().to_elements(); + self.add_map_entry(commitment, elements); } // note details / metadata @@ -355,8 +363,8 @@ impl TransactionAdviceInputs { note_data.extend(*recipient.storage().commitment()); note_data.extend(*assets.commitment()); note_data.extend(*note_arg); - note_data.extend(note.metadata().to_attachment_word()); - note_data.extend(note.metadata().to_header_word()); + note_data.extend(note.attachments().to_commitment()); + note_data.extend(note.metadata_header().to_metadata_word()); note_data.push(Felt::from(recipient.storage().num_items())); note_data.push(Felt::from(assets.num_assets() as u32)); note_data.extend(assets.to_elements()); diff --git a/crates/miden-protocol/src/transaction/kernel/memory.rs b/crates/miden-protocol/src/transaction/kernel/memory.rs index 5468eeb018..aa8c1d2066 100644 --- a/crates/miden-protocol/src/transaction/kernel/memory.rs +++ b/crates/miden-protocol/src/transaction/kernel/memory.rs @@ -25,6 +25,7 @@ pub type StorageSlot = u8; // | | | | + 1024 input notes max, 1024 elements each | // | Output notes | 16_777_216 | 1_048_576 | 1024 output notes max, 1024 elements each | // | Link Map Memory | 33_554_432 | 33_554_432 | Enough for 2_097_151 key-value pairs | +// | Scratch Memory | 67_108_864 | 1_024 | Memory for temporary use | // Relative layout of one account // @@ -365,10 +366,10 @@ pub const NOTE_MEM_SIZE: MemoryAddress = 1024; // Each nullifier occupies a single word. A data section for each note consists of exactly 1024 // elements and is laid out like so: // -// ┌──────┬────────┬────────┬─────────┬────────────┬───────────┬──────────┬────────────┬───────┬ -// │ NOTE │ SERIAL │ SCRIPT │ STORAGE │ ASSETS │ RECIPIENT │ METADATA │ ATTACHMENT │ NOTE │ -// │ ID │ NUM │ ROOT │ COMM │ COMMITMENT │ │ HEADER │ │ ARGS │ -// ├──────┼────────┼────────┼─────────┼────────────┼───────────┼──────────┼────────────┼───────┼ +// ┌──────┬────────┬────────┬─────────┬────────────┬───────────┬──────────┬─────────────┬───────┬ +// │ NOTE │ SERIAL │ SCRIPT │ STORAGE │ ASSETS │ RECIPIENT │ METADATA │ ATTACHMENTS │ NOTE │ +// │ ID │ NUM │ ROOT │ COMM │ COMMITMENT │ │ HEADER │ COMMITMENT │ ARGS │ +// ├──────┼────────┼────────┼─────────┼────────────┼───────────┼──────────┼─────────────┼───────┼ // 0 4 8 12 16 20 24 28 32 // // ┬─────────┬────────┬───────┬─────────┬─────┬────────┬─────────┬─────────┐ @@ -430,17 +431,17 @@ 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 │ METADATA │ RECIPIENT │ ASSETS │ NUM │ DIRTY │ -// │ ID │ HEADER │ ATTACHMENT │ │ COMMITMENT │ ASSETS │ FLAG │ -// ├──────┼──────────┼────────────┼───────────┼────────────┼────────┼───────┼ -// 0 4 8 12 16 20 21 +// ┌──────┬──────────┬──────────────┬────────────┬────────────┬────────────┬────────────┬ +// │ NOTE │ METADATA │ NUM │ ATTACHMENT │ ATTACHMENT │ ATTACHMENT │ ATTACHMENT │ +// │ ID │ HEADER │ ATTACHMENTS │ 0 │ 1 │ 2 │ 3 │ +// ├──────┼──────────┼──────────────┼────────────┼────────────┼────────────┼────────────┼ +// 0 4 8 12 16 20 24 // -// ┬───────┬─────────┬─────┬────────┬─────────┬─────────┐ -// │ ASSET │ ASSET │ ... │ ASSET │ ASSET │ PADDING │ -// │ KEY 0 │ VALUE 0 │ │ KEY n │ VALUE n │ │ -// ┼───────┼─────────┼─────┼────────┼─────────┼─────────┘ -// 24 28 24 + 8n 28 + 8n +// ┬───────────┬────────────┬────────┬───────┬───────┬─────────┬─────┬────────┬─────────┬─────────┐ +// │ 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 // // 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 @@ -455,12 +456,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_ATTACHMENT_OFFSET: MemoryOffset = 8; -pub const OUTPUT_NOTE_RECIPIENT_OFFSET: MemoryOffset = 12; -pub const OUTPUT_NOTE_ASSET_COMMITMENT_OFFSET: MemoryOffset = 16; -pub const OUTPUT_NOTE_NUM_ASSETS_OFFSET: MemoryOffset = 20; -pub const OUTPUT_NOTE_DIRTY_FLAG_OFFSET: MemoryOffset = 21; -pub const OUTPUT_NOTE_ASSETS_OFFSET: MemoryOffset = 24; +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_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; // ASSETS // ------------------------------------------------------------------------------------------------ diff --git a/crates/miden-protocol/src/transaction/kernel/tx_event_id.rs b/crates/miden-protocol/src/transaction/kernel/tx_event_id.rs index 1c3f3a6161..f07e79bd71 100644 --- a/crates/miden-protocol/src/transaction/kernel/tx_event_id.rs +++ b/crates/miden-protocol/src/transaction/kernel/tx_event_id.rs @@ -48,7 +48,7 @@ pub enum TransactionEventId { NoteBeforeAddAsset = NOTE_BEFORE_ADD_ASSET_ID, NoteAfterAddAsset = NOTE_AFTER_ADD_ASSET_ID, - NoteBeforeSetAttachment = NOTE_BEFORE_SET_ATTACHMENT_ID, + NoteBeforeAddAttachment = NOTE_BEFORE_ADD_ATTACHMENT_ID, AuthRequest = AUTH_REQUEST_ID, @@ -113,7 +113,7 @@ impl TransactionEventId { Self::NoteAfterCreated => &NOTE_AFTER_CREATED_NAME, Self::NoteBeforeAddAsset => &NOTE_BEFORE_ADD_ASSET_NAME, Self::NoteAfterAddAsset => &NOTE_AFTER_ADD_ASSET_NAME, - Self::NoteBeforeSetAttachment => &NOTE_BEFORE_SET_ATTACHMENT_NAME, + Self::NoteBeforeAddAttachment => &NOTE_BEFORE_ADD_ATTACHMENT_NAME, Self::AuthRequest => &AUTH_REQUEST_NAME, Self::PrologueStart => &PROLOGUE_START_NAME, Self::PrologueEnd => &PROLOGUE_END_NAME, @@ -194,7 +194,7 @@ impl TryFrom for TransactionEventId { NOTE_BEFORE_ADD_ASSET_ID => Ok(TransactionEventId::NoteBeforeAddAsset), NOTE_AFTER_ADD_ASSET_ID => Ok(TransactionEventId::NoteAfterAddAsset), - NOTE_BEFORE_SET_ATTACHMENT_ID => Ok(TransactionEventId::NoteBeforeSetAttachment), + NOTE_BEFORE_ADD_ATTACHMENT_ID => Ok(TransactionEventId::NoteBeforeAddAttachment), AUTH_REQUEST_ID => Ok(TransactionEventId::AuthRequest), diff --git a/crates/miden-protocol/src/transaction/mod.rs b/crates/miden-protocol/src/transaction/mod.rs index 977155e755..f52042a805 100644 --- a/crates/miden-protocol/src/transaction/mod.rs +++ b/crates/miden-protocol/src/transaction/mod.rs @@ -23,7 +23,7 @@ pub use outputs::{ OutputNote, OutputNoteCollection, OutputNotes, - PrivateNoteHeader, + PrivateOutputNote, PublicOutputNote, RawOutputNote, RawOutputNotes, diff --git a/crates/miden-protocol/src/transaction/outputs/mod.rs b/crates/miden-protocol/src/transaction/outputs/mod.rs index c35f70e890..de572055e0 100644 --- a/crates/miden-protocol/src/transaction/outputs/mod.rs +++ b/crates/miden-protocol/src/transaction/outputs/mod.rs @@ -17,7 +17,7 @@ pub use notes::{ OutputNote, OutputNoteCollection, OutputNotes, - PrivateNoteHeader, + PrivateOutputNote, PublicOutputNote, RawOutputNote, RawOutputNotes, diff --git a/crates/miden-protocol/src/transaction/outputs/notes.rs b/crates/miden-protocol/src/transaction/outputs/notes.rs index 68f9490ea5..7431178b7e 100644 --- a/crates/miden-protocol/src/transaction/outputs/notes.rs +++ b/crates/miden-protocol/src/transaction/outputs/notes.rs @@ -8,12 +8,13 @@ use crate::errors::{OutputNoteError, TransactionOutputError}; use crate::note::{ Note, NoteAssets, + NoteAttachments, NoteHeader, NoteId, NoteMetadata, + NoteMetadataHeader, NoteRecipient, PartialNote, - compute_note_commitment, }; use crate::utils::serde::{ ByteReader, @@ -76,8 +77,8 @@ where /// Returns the commitment to the output notes. /// - /// The commitment is computed as a sequential hash of (note ID, metadata) tuples for the notes - /// created in a transaction. + /// The commitment is computed as a sequential hash of (note ID, metadata commitment) tuples + /// for the notes created in a transaction. pub fn commitment(&self) -> Word { self.commitment } @@ -113,7 +114,7 @@ where /// - For an empty list, [`Word::empty`] is returned. /// - For a non-empty list of notes, this is a sequential hash of (note_id, metadata_commitment) /// tuples for the notes created in a transaction, where `metadata_commitment` is the return - /// value of [`NoteMetadata::to_commitment`]. + /// value of [`NoteMetadataHeader::to_commitment`]. pub(crate) fn compute_commitment<'header>( notes: impl ExactSizeIterator, ) -> Word { @@ -124,7 +125,7 @@ where let mut elements: Vec = Vec::with_capacity(notes.len() * 8); for note_header in notes { elements.extend_from_slice(note_header.id().as_elements()); - elements.extend_from_slice(note_header.metadata().to_commitment().as_elements()); + elements.extend_from_slice(note_header.metadata_header().to_commitment().as_elements()); } Hasher::hash_elements(&elements) @@ -255,14 +256,15 @@ impl RawOutputNote { match self { Self::Full(note) if note.metadata().is_private() => { let note_id = note.id(); - let (_, metadata, _) = note.into_parts(); - let note_header = NoteHeader::new(note_id, metadata); - Ok(OutputNote::Private(PrivateNoteHeader::new(note_header)?)) + let (_, metadata, _, attachments) = note.into_parts(); + let metadata_header = NoteMetadataHeader::new(metadata, &attachments); + let note_header = NoteHeader::new(note_id, metadata_header); + Ok(OutputNote::Private(PrivateOutputNote::new(note_header, attachments)?)) }, Self::Full(note) => Ok(OutputNote::Public(PublicOutputNote::new(note)?)), Self::Partial(note) => { - let (_, header) = note.into_parts(); - Ok(OutputNote::Private(PrivateNoteHeader::new(header)?)) + let (_, header, attachments) = note.into_parts(); + Ok(OutputNote::Private(PrivateOutputNote::new(header, attachments)?)) }, } } @@ -275,11 +277,19 @@ impl RawOutputNote { } } + /// Returns a reference to the note's attachments. + pub fn attachments(&self) -> &NoteAttachments { + match self { + Self::Full(note) => note.attachments(), + Self::Partial(note) => note.attachments(), + } + } + /// Returns a commitment to the note and its metadata. /// /// > hash(NOTE_ID || NOTE_METADATA_COMMITMENT) pub fn commitment(&self) -> Word { - compute_note_commitment(self.id(), self.metadata()) + self.header().to_commitment() } } @@ -351,8 +361,8 @@ pub type OutputNotes = OutputNoteCollection; pub enum OutputNote { /// A public note with full details, size-validated. Public(PublicOutputNote), - /// A note private header (for private notes). - Private(PrivateNoteHeader), + /// A private note header (for private notes). + Private(PrivateOutputNote), } impl OutputNote { @@ -387,11 +397,16 @@ impl OutputNote { } } + /// Returns the note's metadata header. + pub fn metadata_header(&self) -> &NoteMetadataHeader { + <&NoteHeader>::from(self).metadata_header() + } + /// Returns a commitment to the note and its metadata. /// /// > hash(NOTE_ID || NOTE_METADATA_COMMITMENT) pub fn to_commitment(&self) -> Word { - compute_note_commitment(self.id(), self.metadata()) + <&NoteHeader>::from(self).to_commitment() } /// Returns the recipient of the public note, if this is a public note. @@ -407,17 +422,17 @@ impl OutputNote { // ------------------------------------------------------------------------------------------------ impl<'note> From<&'note OutputNote> for &'note NoteHeader { - fn from(value: &'note OutputNote) -> Self { - match value { - OutputNote::Public(note) => note.header(), - OutputNote::Private(header) => &header.0, + fn from(note: &'note OutputNote) -> Self { + match note { + OutputNote::Public(public_note) => public_note.header(), + OutputNote::Private(private_note) => private_note.header(), } } } impl From<&OutputNote> for NoteId { - fn from(value: &OutputNote) -> Self { - value.id() + fn from(note: &OutputNote) -> Self { + note.id() } } @@ -451,7 +466,7 @@ impl Deserializable for OutputNote { fn read_from(source: &mut R) -> Result { match source.read_u8()? { Self::PUBLIC => Ok(Self::Public(PublicOutputNote::read_from(source)?)), - Self::PRIVATE => Ok(Self::Private(PrivateNoteHeader::read_from(source)?)), + Self::PRIVATE => Ok(Self::Private(PrivateOutputNote::read_from(source)?)), v => Err(DeserializationError::InvalidValue(format!( "invalid proven output note type: {v}" ))), @@ -554,39 +569,47 @@ impl Deserializable for PublicOutputNote { // PRIVATE NOTE HEADER // ================================================================================================ -/// A [NoteHeader] of a private note. +/// A [`NoteHeader`] of a private note, along with its public attachments. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct PrivateNoteHeader(NoteHeader); +pub struct PrivateOutputNote { + header: NoteHeader, + attachments: NoteAttachments, +} -impl PrivateNoteHeader { - /// Creates a new [`PrivateNoteHeader`] from the given note header. +impl PrivateOutputNote { + /// Creates a new [`PrivateOutputNote`] from the given note header and attachments. /// /// # Errors /// Returns an error if: /// - The provided header is for a public note. - pub fn new(header: NoteHeader) -> Result { + pub fn new(header: NoteHeader, attachments: NoteAttachments) -> Result { if !header.metadata().is_private() { return Err(OutputNoteError::NoteIsPublic(header.id())); } - Ok(Self(header)) + Ok(Self { header, attachments }) } /// Returns the note's identifier. /// /// The [NoteId] value is both an unique identifier and a commitment to the note. pub fn id(&self) -> NoteId { - self.0.id() + self.header.id() } /// Returns the note's metadata. pub fn metadata(&self) -> &NoteMetadata { - self.0.metadata() + self.header.metadata() + } + + /// Returns the note's attachments. + pub fn attachments(&self) -> &NoteAttachments { + &self.attachments } /// Consumes self and returns the note header's metadata. pub fn into_metadata(self) -> NoteMetadata { - self.0.into_metadata() + self.header.into_metadata() } /// Returns a commitment to the note and its metadata. @@ -596,33 +619,36 @@ impl PrivateNoteHeader { /// This value is used primarily for authenticating notes consumed when they are consumed /// in a transaction. pub fn commitment(&self) -> Word { - self.0.to_commitment() + self.header.to_commitment() } /// Returns a reference to the underlying note header. - pub fn as_header(&self) -> &NoteHeader { - &self.0 + pub fn header(&self) -> &NoteHeader { + &self.header } /// Consumes this wrapper and returns the underlying note header. pub fn into_header(self) -> NoteHeader { - self.0 + self.header } } -impl Serializable for PrivateNoteHeader { +impl Serializable for PrivateOutputNote { fn write_into(&self, target: &mut W) { - self.0.write_into(target); + self.header.write_into(target); + self.attachments.write_into(target); } fn get_size_hint(&self) -> usize { - self.0.get_size_hint() + self.header.get_size_hint() + self.attachments.get_size_hint() } } -impl Deserializable for PrivateNoteHeader { +impl Deserializable for PrivateOutputNote { fn read_from(source: &mut R) -> Result { let header = NoteHeader::read_from(source)?; - Self::new(header).map_err(|err| DeserializationError::InvalidValue(err.to_string())) + let attachments = NoteAttachments::read_from(source)?; + Self::new(header, attachments) + .map_err(|err| DeserializationError::InvalidValue(err.to_string())) } } diff --git a/crates/miden-protocol/src/transaction/proven_tx.rs b/crates/miden-protocol/src/transaction/proven_tx.rs index 8b419ffbbe..0d68955be7 100644 --- a/crates/miden-protocol/src/transaction/proven_tx.rs +++ b/crates/miden-protocol/src/transaction/proven_tx.rs @@ -583,7 +583,7 @@ impl Deserializable for InputNoteCommitment { let nullifier = Nullifier::read_from(source)?; let header = >::read_from(source)?; - Ok(Self { nullifier, header }) + Ok(Self::from_parts_unchecked(nullifier, header)) } } diff --git a/crates/miden-standards/asm/standards/attachments/network_account_target.masm b/crates/miden-standards/asm/standards/attachments/network_account_target.masm index d0a2e58eb5..0f00b42e5b 100644 --- a/crates/miden-standards/asm/standards/attachments/network_account_target.masm +++ b/crates/miden-standards/asm/standards/attachments/network_account_target.masm @@ -12,31 +12,21 @@ use miden::protocol::note #! The attachment scheme for NetworkAccountTarget attachments. #! This is a valid u32 that can be compared against an extracted attachment scheme. -pub const NETWORK_ACCOUNT_TARGET_ATTACHMENT_SCHEME = 1 - -#! The attachment kind for NetworkAccountTarget attachments (Word = 1). -#! This is a valid u32 that can be compared against an extracted attachment kind. -pub const NETWORK_ACCOUNT_TARGET_ATTACHMENT_KIND = 1 +pub const NETWORK_ACCOUNT_TARGET_ATTACHMENT_SCHEME = 2 # ERRORS # ================================================================================================ const ERR_NOT_NETWORK_ACCOUNT_TARGET = "attachment is not a valid network account target" -#! Returns a boolean indicating whether the attachment scheme and kind match the expected -#! values for a NetworkAccountTarget attachment. +#! Returns a boolean indicating whether the attachment scheme matches the expected +#! scheme for a NetworkAccountTarget attachment. #! -#! Inputs: [attachment_scheme, attachment_kind] +#! Inputs: [attachment_scheme] #! Outputs: [is_network_account_target] #! #! Invocation: exec pub proc is_network_account_target eq.NETWORK_ACCOUNT_TARGET_ATTACHMENT_SCHEME - # => [is_scheme_valid, attachment_kind] - - swap eq.NETWORK_ACCOUNT_TARGET_ATTACHMENT_KIND - # => [is_kind_valid, is_scheme_valid] - - and # => [is_network_account_target] end @@ -45,8 +35,8 @@ end #! The attachment is expected to have the following layout: #! [account_id_suffix, account_id_prefix, exec_hint_tag, 0] #! -#! WARNING: This procedure does not validate the attachment scheme or kind. The caller -#! should validate these using `is_network_account_target` before calling this procedure. +#! WARNING: This procedure does not validate the attachment scheme. The caller +#! should validate it using `is_network_account_target` before calling this procedure. #! #! WARNING: This procedure does not validate that the returned account ID is well-formed. #! The caller should validate the account ID if needed using `account_id::validate`. @@ -69,28 +59,26 @@ end #! [account_id_suffix, account_id_prefix, exec_hint_tag, 0] #! #! Inputs: [account_id_suffix, account_id_prefix, exec_hint_tag] -#! Outputs: [attachment_scheme, attachment_kind, NOTE_ATTACHMENT] +#! Outputs: [attachment_scheme, NOTE_ATTACHMENT] #! #! Where: #! - account_id_{suffix,prefix} are the suffix and prefix felts of an account ID. #! - exec_hint_tag is the encoded execution hint for the note with its tag. -#! - attachment_kind is the attachment kind (Word = 1) for use with `output_note::set_attachment`. -#! - attachment_scheme is the attachment scheme (1) for use with `output_note::set_attachment`. +#! - attachment_scheme is the attachment scheme (1) for use with `output_note::add_word_attachment`. #! #! Invocation: exec pub proc new # => [account_id_suffix, account_id_prefix, exec_hint_tag] push.0 movdn.3 # => [NOTE_ATTACHMENT] = [account_id_suffix, account_id_prefix, exec_hint_tag, 0] - push.NETWORK_ACCOUNT_TARGET_ATTACHMENT_KIND push.NETWORK_ACCOUNT_TARGET_ATTACHMENT_SCHEME - # => [attachment_scheme, attachment_kind, NOTE_ATTACHMENT] + # => [attachment_scheme, NOTE_ATTACHMENT] end #! Returns a boolean indicating whether the active account matches the target account #! encoded in the active note's attachment. #! -#! Inputs: [] +#! Inputs: [] #! Outputs: [is_equal] #! #! Where: @@ -108,11 +96,8 @@ pub proc active_account_matches_target_account swapw # => [METADATA_HEADER, NOTE_ATTACHMENT] - exec.note::metadata_into_attachment_info - # => [attachment_kind, attachment_scheme, NOTE_ATTACHMENT] - - swap - # => [attachment_scheme, attachment_kind, NOTE_ATTACHMENT] + exec.note::metadata_into_attachment_header + # => [attachment_0_scheme, NOTE_ATTACHMENT] # ensure the attachment is a network account target exec.is_network_account_target assert.err=ERR_NOT_NETWORK_ACCOUNT_TARGET diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index a7ee2643bc..db07591a71 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -7,6 +7,7 @@ use miden::protocol::asset use miden::protocol::note use miden::protocol::output_note use miden::protocol::asset::FUNGIBLE_ASSET_MAX_AMOUNT +use miden::protocol::note::ATTACHMENT_SCHEME_NONE use miden::standards::note_tag use miden::standards::notes::p2id use miden::standards::wallets::basic->wallet @@ -228,20 +229,21 @@ proc create_p2id_note loc_store.P2ID_AMT_NOTE_FILL # => [] - # Set attachment: aux = amt_account_fill + amt_note_fill at Word[0]. - # attachment_scheme = 0 (NoteAttachmentScheme::none). + # Add word-sized attachment [amt_account_fill + amt_note_fill, 0, 0, 0]. + # with attachment scheme set to none. # # The add cannot overflow: `execute_pswap` asserts # `amt_account_fill + amt_note_fill <= requested_amount` before calling # this procedure, and `requested_amount` itself fits in a felt. + loc_load.P2ID_NOTE_IDX push.0.0.0 loc_load.P2ID_AMT_ACCOUNT_FILL loc_load.P2ID_AMT_NOTE_FILL add - # => [total_fill, 0, 0, 0] + # => [[total_fill, 0, 0, 0], note_idx] - push.0 loc_load.P2ID_NOTE_IDX - # => [note_idx, attachment_scheme=0, total_fill, 0, 0, 0] + push.ATTACHMENT_SCHEME_NONE + # => [attachment_scheme_none, [total_fill, 0, 0, 0], note_idx] - exec.output_note::set_word_attachment + exec.output_note::add_word_attachment # => [] # Move account_fill_amount from consumer's vault to P2ID note (if > 0) @@ -355,14 +357,15 @@ proc create_remainder_note loc_store.REMAINDER_NOTE_IDX # => [] - # Set attachment: aux = amt_payout at Word[0] + # Add word-sized attachment with [amt_payout, 0, 0, 0] with attachment scheme set to none. + loc_load.REMAINDER_NOTE_IDX push.0.0.0 loc_load.REMAINDER_AMT_PAYOUT - # => [amt_payout, 0, 0, 0] + # => [[amt_payout, 0, 0, 0], note_idx] - push.0 loc_load.REMAINDER_NOTE_IDX - # => [note_idx, attachment_scheme=0, amt_payout, 0, 0, 0] + push.ATTACHMENT_SCHEME_NONE + # => [attachment_scheme_none, [amt_payout, 0, 0, 0], note_idx] - exec.output_note::set_word_attachment + exec.output_note::add_word_attachment # => [] # Add remaining offered asset: remainder_amount = amt_offered - amt_payout. diff --git a/crates/miden-standards/src/account/interface/component.rs b/crates/miden-standards/src/account/interface/component.rs index 4dc956d9ba..c1a091b013 100644 --- a/crates/miden-standards/src/account/interface/component.rs +++ b/crates/miden-standards/src/account/interface/component.rs @@ -269,21 +269,36 @@ impl AccountComponentInterface { }, } - body.push_str(&format!( - " - push.{ATTACHMENT} - push.{attachment_kind} + 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(); + + body.push_str(&format!( + " + push.{attachment_commitment} push.{attachment_scheme} - movup.6 - # => [note_idx, attachment_scheme, attachment_kind, ATTACHMENT, pad(16)] - exec.::miden::protocol::output_note::set_attachment + # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx, pad(16)] + exec.::miden::protocol::output_note::add_attachment # => [pad(16)] ", - ATTACHMENT = partial_note.metadata().to_attachment_word(), - attachment_scheme = - partial_note.metadata().attachment().attachment_scheme().as_u32(), - attachment_kind = partial_note.metadata().attachment().attachment_kind().as_u8(), - )); + )); + }, + None => { + body.push_str( + " + 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 a5409f34a3..8633da6213 100644 --- a/crates/miden-standards/src/account/interface/mod.rs +++ b/crates/miden-standards/src/account/interface/mod.rs @@ -2,7 +2,7 @@ use alloc::string::String; use alloc::vec::Vec; use miden_protocol::account::{AccountId, AccountType}; -use miden_protocol::note::{NoteAttachmentContent, PartialNote}; +use miden_protocol::note::PartialNote; use miden_protocol::transaction::TransactionScript; use thiserror::Error; @@ -166,8 +166,11 @@ impl AccountInterface { // and the array elements as value. let mut code_builder = CodeBuilder::new(); for note in output_notes { - if let NoteAttachmentContent::Array(array) = note.metadata().attachment().content() { - code_builder.add_advice_map_entry(array.commitment(), array.as_slice().to_vec()); + if let Some(attachment) = note.attachments().iter().next() { + code_builder.add_advice_map_entry( + attachment.content().to_commitment(), + attachment.content().to_elements(), + ); } } @@ -263,4 +266,6 @@ pub enum AccountInterfaceError { "account does not contain the basic fungible faucet or basic wallet interfaces which are needed to support the send_note script generation" )] UnsupportedAccountInterface, + #[error("multiple attachments per note are not supported")] + MultipleAttachmentsUnsupported, } diff --git a/crates/miden-standards/src/account/interface/test.rs b/crates/miden-standards/src/account/interface/test.rs index 4cc5bbcbc6..dd2db4eeda 100644 --- a/crates/miden-standards/src/account/interface/test.rs +++ b/crates/miden-standards/src/account/interface/test.rs @@ -8,7 +8,7 @@ use miden_protocol::errors::NoteError; use miden_protocol::note::{ Note, NoteAssets, - NoteAttachment, + NoteAttachments, NoteMetadata, NoteRecipient, NoteStorage, @@ -102,7 +102,7 @@ fn test_basic_wallet_default_notes() { offered_asset, requested_asset, NoteType::Public, - NoteAttachment::default(), + NoteAttachments::default(), NoteType::Public, &mut RandomCoin::new(Word::from([1, 2, 3, 4u32])), ) @@ -196,7 +196,7 @@ fn test_custom_account_default_note() { offered_asset, requested_asset, NoteType::Public, - NoteAttachment::default(), + NoteAttachments::default(), NoteType::Public, &mut RandomCoin::new(Word::from([1, 2, 3, 4u32])), ) @@ -228,7 +228,7 @@ fn test_required_asset_same_as_offered() { offered_asset, requested_asset, NoteType::Public, - NoteAttachment::default(), + NoteAttachments::default(), NoteType::Public, &mut RandomCoin::new(Word::from([1, 2, 3, 4u32])), ); diff --git a/crates/miden-standards/src/note/burn.rs b/crates/miden-standards/src/note/burn.rs index 22d82c9c5c..eb486744f6 100644 --- a/crates/miden-standards/src/note/burn.rs +++ b/crates/miden-standards/src/note/burn.rs @@ -6,7 +6,7 @@ use miden_protocol::errors::NoteError; use miden_protocol::note::{ Note, NoteAssets, - NoteAttachment, + NoteAttachments, NoteMetadata, NoteRecipient, NoteScript, @@ -78,7 +78,7 @@ impl BurnNote { /// - `sender`: The account ID of the note creator /// - `faucet_id`: The account ID of the faucet that will burn the assets /// - `fungible_asset`: The fungible asset to be burned - /// - `attachment`: The [`NoteAttachment`] of the BURN note + /// - `attachment`: The [`NoteAttachments`] of the BURN note /// - `rng`: Random number generator for creating the serial number /// /// # Errors @@ -87,7 +87,7 @@ impl BurnNote { sender: AccountId, faucet_id: AccountId, fungible_asset: Asset, - attachment: NoteAttachment, + attachments: NoteAttachments, rng: &mut R, ) -> Result { let note_script = Self::script(); @@ -99,11 +99,10 @@ impl BurnNote { let inputs = NoteStorage::new(vec![])?; let tag = NoteTag::with_account_target(faucet_id); - let metadata = - NoteMetadata::new(sender, note_type).with_tag(tag).with_attachment(attachment); + let metadata = NoteMetadata::new(sender, note_type).with_tag(tag); let assets = NoteAssets::new(vec![fungible_asset])?; // BURN notes contain the asset to burn let recipient = NoteRecipient::new(serial_num, note_script, inputs); - Ok(Note::new(assets, metadata, recipient)) + Ok(Note::with_attachments(assets, metadata, recipient, attachments)) } } diff --git a/crates/miden-standards/src/note/mint.rs b/crates/miden-standards/src/note/mint.rs index e944f28732..fa86594e07 100644 --- a/crates/miden-standards/src/note/mint.rs +++ b/crates/miden-standards/src/note/mint.rs @@ -7,7 +7,7 @@ use miden_protocol::errors::NoteError; use miden_protocol::note::{ Note, NoteAssets, - NoteAttachment, + NoteAttachments, NoteMetadata, NoteRecipient, NoteScript, @@ -82,7 +82,7 @@ impl MintNote { /// - `faucet_id`: The account ID of the network faucet that will mint the assets /// - `sender`: The account ID of the note creator (must be the faucet owner) /// - `mint_storage`: The storage configuration specifying private or public output mode - /// - `attachment`: The [`NoteAttachment`] of the MINT note + /// - `attachment`: The [`NoteAttachments`] of the MINT note /// - `rng`: Random number generator for creating the serial number /// /// # Errors @@ -91,7 +91,7 @@ impl MintNote { faucet_id: AccountId, sender: AccountId, mint_storage: MintNoteStorage, - attachment: NoteAttachment, + attachments: NoteAttachments, rng: &mut R, ) -> Result { let note_script = Self::script(); @@ -105,12 +105,11 @@ impl MintNote { let tag = NoteTag::with_account_target(faucet_id); - let metadata = - NoteMetadata::new(sender, note_type).with_tag(tag).with_attachment(attachment); + let metadata = NoteMetadata::new(sender, note_type).with_tag(tag); let assets = NoteAssets::new(vec![])?; // MINT notes have no assets let recipient = NoteRecipient::new(serial_num, note_script, storage); - Ok(Note::new(assets, metadata, recipient)) + Ok(Note::with_attachments(assets, metadata, recipient, attachments)) } } diff --git a/crates/miden-standards/src/note/network_account_target.rs b/crates/miden-standards/src/note/network_account_target.rs index 4471c145f8..038c379578 100644 --- a/crates/miden-standards/src/note/network_account_target.rs +++ b/crates/miden-standards/src/note/network_account_target.rs @@ -4,8 +4,8 @@ use miden_protocol::errors::{AccountIdError, NoteError}; use miden_protocol::note::{ NoteAttachment, NoteAttachmentContent, - NoteAttachmentKind, NoteAttachmentScheme, + NoteAttachments, NoteType, }; @@ -85,6 +85,19 @@ impl From for NoteAttachment { } } +impl TryFrom<&NoteAttachments> for NetworkAccountTarget { + type Error = NetworkAccountTargetError; + + fn try_from(attachments: &NoteAttachments) -> Result { + // Find the first matching attachment. In case of multiple network account target + // attachments, we pick the first one as the canonical one. + let attachment = attachments + .find(NetworkAccountTarget::ATTACHMENT_SCHEME) + .ok_or_else(|| NetworkAccountTargetError::MissingAttachmentScheme)?; + + Self::try_from(attachment) + } +} impl TryFrom<&NoteAttachment> for NetworkAccountTarget { type Error = NetworkAccountTargetError; @@ -109,8 +122,8 @@ impl TryFrom<&NoteAttachment> for NetworkAccountTarget { NetworkAccountTarget::new(target_id, exec_hint) }, - _ => Err(NetworkAccountTargetError::AttachmentKindMismatch( - attachment.content().attachment_kind(), + _ => Err(NetworkAccountTargetError::AttachmentContentNotWord( + attachment.content().num_words(), )), } } @@ -123,16 +136,15 @@ impl TryFrom<&NoteAttachment> for NetworkAccountTarget { pub enum NetworkAccountTargetError { #[error("target account ID must be of type network account")] TargetNotNetwork(AccountId), + #[error("note attachments do not contain a network account target scheme")] + MissingAttachmentScheme, #[error( "attachment scheme {0} did not match expected type {expected}", expected = NetworkAccountTarget::ATTACHMENT_SCHEME )] AttachmentSchemeMismatch(NoteAttachmentScheme), - #[error( - "attachment kind {0} did not match expected type {expected}", - expected = NoteAttachmentKind::Word - )] - AttachmentKindMismatch(NoteAttachmentKind), + #[error("attachment content is not a Word (num_words={0}, expected 1)")] + AttachmentContentNotWord(u16), #[error("failed to decode target account ID")] DecodeTargetId(#[source] AccountIdError), #[error("failed to decode execution hint")] diff --git a/crates/miden-standards/src/note/network_note.rs b/crates/miden-standards/src/note/network_note.rs index c0a1c51559..b367b4bca6 100644 --- a/crates/miden-standards/src/note/network_note.rs +++ b/crates/miden-standards/src/note/network_note.rs @@ -1,5 +1,5 @@ use miden_protocol::account::AccountId; -use miden_protocol::note::{Note, NoteAttachment, NoteMetadata, NoteType}; +use miden_protocol::note::{Note, NoteAttachments, NoteMetadata, NoteType}; use crate::note::{NetworkAccountTarget, NetworkAccountTargetError, NoteExecutionHint}; @@ -27,7 +27,8 @@ impl AccountTargetNetworkNote { } // Validate that the attachment is a valid NetworkAccountTarget. - NetworkAccountTarget::try_from(note.metadata().attachment())?; + NetworkAccountTarget::try_from(note.attachments())?; + Ok(Self { note }) } @@ -53,7 +54,7 @@ impl AccountTargetNetworkNote { /// Returns the decoded [`NetworkAccountTarget`] attachment. pub fn target(&self) -> NetworkAccountTarget { - NetworkAccountTarget::try_from(self.note.metadata().attachment()) + NetworkAccountTarget::try_from(self.note.attachments()) .expect("AccountTargetNetworkNote guarantees valid NetworkAccountTarget attachment") } @@ -62,9 +63,9 @@ impl AccountTargetNetworkNote { self.target().execution_hint() } - /// Returns the raw [`NoteAttachment`] from the note metadata. - pub fn attachment(&self) -> &NoteAttachment { - self.metadata().attachment() + /// Returns the raw [`NoteAttachments`] from the note's attachments. + pub fn attachments(&self) -> &NoteAttachments { + self.note.attachments() } /// Returns the [`NoteType`] of the underlying note. @@ -89,7 +90,7 @@ pub trait NetworkNoteExt { impl NetworkNoteExt for Note { fn is_network_note(&self) -> bool { self.metadata().note_type() == NoteType::Public - && NetworkAccountTarget::try_from(self.metadata().attachment()).is_ok() + && NetworkAccountTarget::try_from(self.attachments()).is_ok() } fn into_account_target_network_note( diff --git a/crates/miden-standards/src/note/p2id.rs b/crates/miden-standards/src/note/p2id.rs index 7710eed3cf..e2f76aa3fb 100644 --- a/crates/miden-standards/src/note/p2id.rs +++ b/crates/miden-standards/src/note/p2id.rs @@ -8,7 +8,7 @@ use miden_protocol::errors::NoteError; use miden_protocol::note::{ Note, NoteAssets, - NoteAttachment, + NoteAttachments, NoteMetadata, NoteRecipient, NoteScript, @@ -79,7 +79,7 @@ impl P2idNote { target: AccountId, assets: Vec, note_type: NoteType, - attachment: NoteAttachment, + attachments: NoteAttachments, rng: &mut R, ) -> Result { let serial_num = rng.draw_word(); @@ -87,11 +87,10 @@ impl P2idNote { let tag = NoteTag::with_account_target(target); - let metadata = - NoteMetadata::new(sender, note_type).with_tag(tag).with_attachment(attachment); + let metadata = NoteMetadata::new(sender, note_type).with_tag(tag); let vault = NoteAssets::new(assets)?; - Ok(Note::new(vault, metadata, recipient)) + Ok(Note::with_attachments(vault, metadata, recipient, attachments)) } } diff --git a/crates/miden-standards/src/note/p2ide.rs b/crates/miden-standards/src/note/p2ide.rs index 16626f340c..a5ece2ffd8 100644 --- a/crates/miden-standards/src/note/p2ide.rs +++ b/crates/miden-standards/src/note/p2ide.rs @@ -9,7 +9,7 @@ use miden_protocol::errors::NoteError; use miden_protocol::note::{ Note, NoteAssets, - NoteAttachment, + NoteAttachments, NoteMetadata, NoteRecipient, NoteScript, @@ -86,18 +86,17 @@ impl P2ideNote { storage: P2ideNoteStorage, assets: Vec, note_type: NoteType, - attachment: NoteAttachment, + attachments: NoteAttachments, rng: &mut R, ) -> Result { let serial_num = rng.draw_word(); let recipient = storage.into_recipient(serial_num)?; let tag = NoteTag::with_account_target(storage.target()); - let metadata = - NoteMetadata::new(sender, note_type).with_tag(tag).with_attachment(attachment); + let metadata = NoteMetadata::new(sender, note_type).with_tag(tag); let vault = NoteAssets::new(assets)?; - Ok(Note::new(vault, metadata, recipient)) + Ok(Note::with_attachments(vault, metadata, recipient, attachments)) } } diff --git a/crates/miden-standards/src/note/pswap.rs b/crates/miden-standards/src/note/pswap.rs index da15c7dcd6..8192a5760a 100644 --- a/crates/miden-standards/src/note/pswap.rs +++ b/crates/miden-standards/src/note/pswap.rs @@ -9,6 +9,7 @@ use miden_protocol::note::{ NoteAssets, NoteAttachment, NoteAttachmentScheme, + NoteAttachments, NoteMetadata, NoteRecipient, NoteScript, @@ -215,8 +216,7 @@ pub struct PswapNote { offered_asset: FungibleAsset, - #[builder(default)] - attachment: NoteAttachment, + attachment: Option, } impl PswapNoteBuilder @@ -316,13 +316,13 @@ impl PswapNote { &self.offered_asset } - /// Returns a reference to the note attachment. + /// Returns a reference to the note attachments. /// /// For notes targeting a network account, this may contain a /// [`NetworkAccountTarget`](crate::note::NetworkAccountTarget) with scheme = 1. - /// For local-only notes, this is typically `NoteAttachmentScheme::none()`. - pub fn attachment(&self) -> &NoteAttachment { - &self.attachment + /// For local-only notes, this is typically empty. + pub fn attachments(&self) -> Option<&NoteAttachment> { + self.attachment.as_ref() } // INSTANCE METHODS @@ -586,10 +586,14 @@ impl PswapNote { let p2id_assets = NoteAssets::new(vec![Asset::Fungible(payback_asset)])?; let p2id_metadata = NoteMetadata::new(consumer_account_id, self.storage.payback_note_type) - .with_tag(payback_note_tag) - .with_attachment(attachment); + .with_tag(payback_note_tag); - Ok(Note::new(p2id_assets, p2id_metadata, recipient)) + Ok(Note::with_attachments( + p2id_assets, + p2id_metadata, + recipient, + NoteAttachments::from(attachment), + )) } /// Builds a remainder PSWAP note carrying the unfilled portion of the swap. @@ -623,14 +627,14 @@ impl PswapNote { let attachment = Self::remainder_attachment(offered_amount_for_fill)?; - Ok(PswapNote { - sender: consumer_account_id, - storage: new_storage, - serial_number: remainder_serial_num, - note_type: self.note_type, - offered_asset: remaining_offered_asset, - attachment, - }) + PswapNote::builder() + .sender(consumer_account_id) + .storage(new_storage) + .serial_number(remainder_serial_num) + .note_type(self.note_type) + .offered_asset(remaining_offered_asset) + .attachment(attachment) + .build() } } @@ -651,11 +655,11 @@ impl From for Note { let assets = NoteAssets::new(vec![Asset::Fungible(pswap.offered_asset)]) .expect("single fungible asset should be valid"); - let metadata = NoteMetadata::new(pswap.sender, pswap.note_type) - .with_tag(tag) - .with_attachment(pswap.attachment); + let metadata = NoteMetadata::new(pswap.sender, pswap.note_type).with_tag(tag); + + let attachments = pswap.attachment.map(NoteAttachments::from).unwrap_or_default(); - Note::new(assets, metadata, recipient) + Note::with_attachments(assets, metadata, recipient, attachments) } } @@ -680,14 +684,22 @@ impl TryFrom<&Note> for PswapNote { }, }; - Ok(Self { - sender: note.metadata().sender(), - storage, - serial_number: note.recipient().serial_num(), - note_type: note.metadata().note_type(), - offered_asset, - attachment: note.metadata().attachment().clone(), - }) + let attachment = match note.attachments().num_attachments() { + 0 => None, + 1 => { + Some(note.attachments().get(0).expect("length should have been validated").clone()) + }, + _ => return Err(NoteError::other("pswap note supports only one attachment")), + }; + + PswapNote::builder() + .sender(note.metadata().sender()) + .storage(storage) + .serial_number(note.recipient().serial_num()) + .note_type(note.metadata().note_type()) + .offered_asset(offered_asset) + .maybe_attachment(attachment) + .build() } } diff --git a/crates/miden-standards/src/note/standard_note_attachment.rs b/crates/miden-standards/src/note/standard_note_attachment.rs index 17ec1332df..76799cb982 100644 --- a/crates/miden-standards/src/note/standard_note_attachment.rs +++ b/crates/miden-standards/src/note/standard_note_attachment.rs @@ -12,7 +12,7 @@ impl StandardNoteAttachment { /// Returns the [`NoteAttachmentScheme`] of the standard attachment. pub const fn attachment_scheme(&self) -> NoteAttachmentScheme { match self { - StandardNoteAttachment::NetworkAccountTarget => NoteAttachmentScheme::new(1u32), + StandardNoteAttachment::NetworkAccountTarget => NoteAttachmentScheme::new_const(2u16), } } } diff --git a/crates/miden-standards/src/note/swap.rs b/crates/miden-standards/src/note/swap.rs index da9c3ab334..ba1903c39a 100644 --- a/crates/miden-standards/src/note/swap.rs +++ b/crates/miden-standards/src/note/swap.rs @@ -9,7 +9,7 @@ use miden_protocol::errors::NoteError; use miden_protocol::note::{ Note, NoteAssets, - NoteAttachment, + NoteAttachments, NoteDetails, NoteMetadata, NoteRecipient, @@ -81,7 +81,7 @@ impl SwapNote { offered_asset: Asset, requested_asset: Asset, swap_note_type: NoteType, - swap_note_attachment: NoteAttachment, + swap_note_attachments: NoteAttachments, payback_note_type: NoteType, rng: &mut R, ) -> Result<(Note, NoteDetails), NoteError> { @@ -101,11 +101,9 @@ impl SwapNote { let tag = Self::build_tag(swap_note_type, &offered_asset, &requested_asset); // build the outgoing note - let metadata = NoteMetadata::new(sender, swap_note_type) - .with_tag(tag) - .with_attachment(swap_note_attachment); + let metadata = NoteMetadata::new(sender, swap_note_type).with_tag(tag); let assets = NoteAssets::new(vec![offered_asset])?; - let note = Note::new(assets, metadata, recipient); + let note = Note::with_attachments(assets, metadata, recipient, swap_note_attachments); // build the payback note details let payback_recipient = P2idNoteStorage::new(sender).into_recipient(payback_serial_num); diff --git a/crates/miden-standards/src/testing/note.rs b/crates/miden-standards/src/testing/note.rs index 240fd61be1..f5f75b0a74 100644 --- a/crates/miden-standards/src/testing/note.rs +++ b/crates/miden-standards/src/testing/note.rs @@ -11,6 +11,7 @@ use miden_protocol::note::{ Note, NoteAssets, NoteAttachment, + NoteAttachments, NoteMetadata, NoteRecipient, NoteScript, @@ -19,7 +20,7 @@ use miden_protocol::note::{ NoteType, }; use miden_protocol::testing::note::DEFAULT_NOTE_SCRIPT; -use miden_protocol::vm::Package; +use miden_protocol::vm::{AdviceMap, Package}; use miden_protocol::{Felt, Word}; use rand::Rng; @@ -46,7 +47,8 @@ pub struct NoteBuilder { serial_num: Word, tag: NoteTag, code: String, - attachment: NoteAttachment, + attachments: NoteAttachments, + advice_map: AdviceMap, source_code: SourceCodeOrigin, } @@ -68,7 +70,8 @@ impl NoteBuilder { // The note tag is not under test, so we choose a value that is always valid. tag: NoteTag::with_account_target(sender), code: DEFAULT_NOTE_SCRIPT.to_string(), - attachment: NoteAttachment::default(), + attachments: NoteAttachments::default(), + advice_map: AdviceMap::default(), source_code: SourceCodeOrigin::Masm { dyn_libraries: Vec::new(), source_manager: Arc::new(DefaultSourceManager::default()), @@ -114,9 +117,18 @@ impl NoteBuilder { self } - /// Overwrites the attachment. + /// Appends an attachment to the existing attachments. pub fn attachment(mut self, attachment: impl Into) -> Self { - self.attachment = attachment.into(); + let mut attachments = core::mem::take(&mut self.attachments).into_vec(); + attachments.push(attachment.into()); + self.attachments = + NoteAttachments::new(attachments).expect("number of attachments exceeds maximum"); + self + } + + /// Sets the advice map entries that will be added to the compiled note script. + pub fn advice_map(mut self, advice_map: AdviceMap) -> Self { + self.advice_map = advice_map; self } @@ -186,13 +198,13 @@ impl NoteBuilder { SourceCodeOrigin::Package(package) => NoteScript::from_package(&package)?, }; + let note_script = note_script.with_advice_map(self.advice_map); + let vault = NoteAssets::new(self.assets)?; - let metadata = NoteMetadata::new(self.sender, self.note_type) - .with_tag(self.tag) - .with_attachment(self.attachment); + let metadata = NoteMetadata::new(self.sender, self.note_type).with_tag(self.tag); let storage = NoteStorage::new(self.storage)?; let recipient = NoteRecipient::new(self.serial_num, note_script, storage); - Ok(Note::new(vault, metadata, recipient)) + Ok(Note::with_attachments(vault, metadata, recipient, self.attachments)) } } diff --git a/crates/miden-testing/src/kernel_tests/block/proposed_block_errors.rs b/crates/miden-testing/src/kernel_tests/block/proposed_block_errors.rs index 4a68ef1f30..710517874d 100644 --- a/crates/miden-testing/src/kernel_tests/block/proposed_block_errors.rs +++ b/crates/miden-testing/src/kernel_tests/block/proposed_block_errors.rs @@ -9,7 +9,7 @@ use miden_protocol::asset::FungibleAsset; use miden_protocol::block::{BlockInputs, BlockNumber, ProposedBlock}; use miden_protocol::crypto::merkle::SparseMerklePath; use miden_protocol::errors::ProposedBlockError; -use miden_protocol::note::{NoteAttachment, NoteInclusionProof, NoteType}; +use miden_protocol::note::{NoteAttachments, NoteInclusionProof, NoteType}; use miden_standards::note::P2idNote; use miden_tx::LocalTransactionProver; @@ -353,7 +353,7 @@ async fn proposed_block_fails_on_invalid_proof_or_missing_note_inclusion_referen account1.id(), vec![], NoteType::Private, - NoteAttachment::default(), + NoteAttachments::default(), builder.rng_mut(), )?; let spawn_note = builder.add_spawn_note([&p2id_note])?; diff --git a/crates/miden-testing/src/kernel_tests/block/proven_block_success.rs b/crates/miden-testing/src/kernel_tests/block/proven_block_success.rs index 19aaea3df2..921a2aa270 100644 --- a/crates/miden-testing/src/kernel_tests/block/proven_block_success.rs +++ b/crates/miden-testing/src/kernel_tests/block/proven_block_success.rs @@ -8,7 +8,7 @@ use miden_protocol::batch::BatchNoteTree; use miden_protocol::block::account_tree::AccountTree; use miden_protocol::block::{BlockInputs, BlockNoteIndex, BlockNoteTree, ProposedBlock}; use miden_protocol::crypto::merkle::smt::Smt; -use miden_protocol::note::{NoteAttachment, NoteType}; +use miden_protocol::note::{NoteAttachments, NoteType}; use miden_protocol::transaction::InputNoteCommitment; use miden_standards::note::P2idNote; @@ -37,7 +37,7 @@ async fn proven_block_success() -> anyhow::Result<()> { account0.id(), vec![asset], NoteType::Private, - NoteAttachment::default(), + NoteAttachments::default(), builder.rng_mut(), )?; let output_note1 = P2idNote::create( @@ -45,7 +45,7 @@ async fn proven_block_success() -> anyhow::Result<()> { account1.id(), vec![asset], NoteType::Private, - NoteAttachment::default(), + NoteAttachments::default(), builder.rng_mut(), )?; let output_note2 = P2idNote::create( @@ -53,7 +53,7 @@ async fn proven_block_success() -> anyhow::Result<()> { account2.id(), vec![asset], NoteType::Private, - NoteAttachment::default(), + NoteAttachments::default(), builder.rng_mut(), )?; let output_note3 = P2idNote::create( @@ -61,7 +61,7 @@ async fn proven_block_success() -> anyhow::Result<()> { account3.id(), vec![asset], NoteType::Private, - NoteAttachment::default(), + NoteAttachments::default(), builder.rng_mut(), )?; @@ -115,7 +115,7 @@ async fn proven_block_success() -> anyhow::Result<()> { ( BlockNoteIndex::new(batch_idx, note_idx_in_batch).unwrap(), note.id(), - note.metadata(), + note.metadata_header(), ) }, )) @@ -343,7 +343,7 @@ async fn proven_block_erasing_unauthenticated_notes() -> anyhow::Result<()> { // Remove the erased note to get the expected batch note tree. let mut batch_tree = BatchNoteTree::with_contiguous_leaves( - batch0.output_notes().iter().map(|note| (note.id(), note.metadata())), + batch0.output_notes().iter().map(|note| (note.id(), note.metadata_header())), ) .unwrap(); batch_tree.remove(erased_note_idx as u64).unwrap(); 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 7deff38b96..f6144ea466 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 @@ -119,9 +119,9 @@ async fn test_active_note_get_metadata() -> anyhow::Result<()> { swapw dropw end "#, - METADATA_HEADER = tx_context.input_notes().get_note(0).note().metadata().to_header_word(), - NOTE_ATTACHMENT = - tx_context.input_notes().get_note(0).note().metadata().to_attachment_word() + METADATA_HEADER = + tx_context.input_notes().get_note(0).note().metadata_header().to_metadata_word(), + NOTE_ATTACHMENT = tx_context.input_notes().get_note(0).note().attachments().to_commitment() ); tx_context.execute_code(&code).await?; diff --git a/crates/miden-testing/src/kernel_tests/tx/test_input_note.rs b/crates/miden-testing/src/kernel_tests/tx/test_input_note.rs index af4f93da6c..52052f50da 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_input_note.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_input_note.rs @@ -133,8 +133,8 @@ async fn test_get_recipient_and_metadata() -> anyhow::Result<()> { end "#, RECIPIENT = p2id_note_1_asset.recipient().digest(), - METADATA_HEADER = p2id_note_1_asset.metadata().to_header_word(), - NOTE_ATTACHMENT = p2id_note_1_asset.metadata().to_attachment_word(), + METADATA_HEADER = p2id_note_1_asset.metadata_header().to_metadata_word(), + NOTE_ATTACHMENT = p2id_note_1_asset.attachments().to_commitment(), ); let tx_script = CodeBuilder::default().compile_tx_script(code)?; 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 e1243a7870..35747e6c18 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_note.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_note.rs @@ -13,7 +13,9 @@ use miden_protocol::errors::MasmError; use miden_protocol::note::{ Note, NoteAssets, + NoteAttachments, NoteMetadata, + NoteMetadataHeader, NoteRecipient, NoteStorage, NoteTag, @@ -399,7 +401,8 @@ async fn test_build_metadata_header() -> anyhow::Result<()> { let metadata_word = exec_output.get_stack_word(0); assert_eq!( - test_metadata.to_header_word(), + NoteMetadataHeader::new(test_metadata.clone(), &NoteAttachments::default()) + .to_metadata_word(), metadata_word, "failed in iteration {iteration}" ); diff --git a/crates/miden-testing/src/kernel_tests/tx/test_output_note.rs b/crates/miden-testing/src/kernel_tests/tx/test_output_note.rs index f6ef4dd5a5..b4d10ad3fc 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 @@ -5,9 +5,14 @@ use miden_protocol::account::auth::AuthScheme; use miden_protocol::account::{Account, AccountId}; use miden_protocol::asset::{Asset, FungibleAsset, NonFungibleAsset}; use miden_protocol::crypto::rand::RandomCoin; +use miden_protocol::errors::MasmError; use miden_protocol::errors::tx_kernel::{ ERR_NON_FUNGIBLE_ASSET_ALREADY_EXISTS, ERR_NOTE_NUM_OF_ASSETS_EXCEED_LIMIT, + ERR_OUTPUT_NOTE_ATTACHMENT_SCHEME_CANNOT_BE_ZERO, + ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_CANNOT_BE_ZERO, + 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_TX_NUMBER_OF_OUTPUT_NOTES_EXCEEDS_LIMIT, }; @@ -15,7 +20,9 @@ use miden_protocol::note::{ Note, NoteAttachment, NoteAttachmentScheme, + NoteAttachments, NoteMetadata, + NoteMetadataHeader, NoteRecipient, NoteStorage, NoteTag, @@ -39,14 +46,14 @@ use miden_protocol::transaction::memory::{ NOTE_MEM_SIZE, NUM_OUTPUT_NOTES_PTR, OUTPUT_NOTE_ASSETS_OFFSET, - OUTPUT_NOTE_ATTACHMENT_OFFSET, + OUTPUT_NOTE_ATTACHMENT_0_OFFSET, OUTPUT_NOTE_METADATA_HEADER_OFFSET, OUTPUT_NOTE_NUM_ASSETS_OFFSET, OUTPUT_NOTE_RECIPIENT_OFFSET, OUTPUT_NOTE_SECTION_OFFSET, }; use miden_protocol::transaction::{RawOutputNote, RawOutputNotes}; -use miden_protocol::{Felt, Word, ZERO}; +use miden_protocol::{Felt, WORD_SIZE, Word, ZERO}; use miden_standards::code_builder::CodeBuilder; use miden_standards::note::{ AccountTargetNetworkNote, @@ -111,18 +118,20 @@ async fn test_create_note() -> anyhow::Result<()> { ); let metadata = NoteMetadata::new(account_id, NoteType::Public).with_tag(tag); - let expected_metadata_header = metadata.to_header_word(); - let expected_note_attachment = metadata.to_attachment_word(); + let expected_metadata_word = + NoteMetadataHeader::new(metadata, &NoteAttachments::default()).to_metadata_word(); + let expected_note_attachment = NoteAttachments::default().to_commitment(); assert_eq!( exec_output .get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_METADATA_HEADER_OFFSET), - expected_metadata_header, + expected_metadata_word, "metadata header must be stored at the correct memory location", ); assert_eq!( - exec_output.get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_ATTACHMENT_OFFSET), + exec_output + .get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_ATTACHMENT_0_OFFSET), expected_note_attachment, "attachment must be stored at the correct memory location", ); @@ -233,17 +242,23 @@ async fn test_get_output_notes_commitment() -> anyhow::Result<()> { .note_type(NoteType::Public) .add_assets([asset_2]) .attachment(NoteAttachment::new_array( - NoteAttachmentScheme::new(5), - [42, 43, 44, 45, 46u32].map(Felt::from).to_vec(), + NoteAttachmentScheme::new(5u16)?, + vec![Word::from([42, 43, 44, 45u32]), Word::from([46, 47, 48, 49u32])], )?) .build()?; + // Build the advice map entry for the array attachment's elements + let attachment = output_note_2.attachments().get(0).unwrap(); + let attachment_commitment = attachment.content().to_commitment(); + let attachment_elements = attachment.content().to_elements(); + let tx_context = TransactionContextBuilder::new(account) .extend_input_notes(vec![input_note_1.clone(), input_note_2.clone()]) .extend_expected_output_notes(vec![ 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 @@ -293,9 +308,8 @@ async fn test_get_output_notes_commitment() -> anyhow::Result<()> { push.{ATTACHMENT2} push.{attachment_scheme2} - movup.5 - # => [note_idx, attachment_scheme, ATTACHMENT] - exec.output_note::set_array_attachment + # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] + exec.output_note::add_array_attachment # => [] # compute the output notes commitment @@ -316,8 +330,9 @@ 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.metadata().to_attachment_word(), - attachment_scheme2 = output_note_2.metadata().attachment().attachment_scheme().as_u32(), + ATTACHMENT2 = output_note_2.attachments().get(0).unwrap().content().to_commitment(), + attachment_scheme2 = + output_note_2.attachments().get(0).unwrap().attachment_scheme().as_u16(), ); let exec_output = &tx_context.execute_code(&code).await?; @@ -330,31 +345,49 @@ async fn test_get_output_notes_commitment() -> anyhow::Result<()> { assert_eq!( exec_output .get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_METADATA_HEADER_OFFSET), - output_note_1.metadata().to_header_word(), + output_note_1.metadata_header().to_metadata_word(), "Validate the output note 1 metadata header", ); - assert_eq!( - exec_output.get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_ATTACHMENT_OFFSET), - output_note_1.metadata().to_attachment_word(), - "Validate the output note 1 attachment", - ); + for attachment_idx in 0..4u32 { + assert_eq!( + exec_output.get_kernel_mem_word( + OUTPUT_NOTE_SECTION_OFFSET + + OUTPUT_NOTE_ATTACHMENT_0_OFFSET + + attachment_idx * WORD_SIZE as u32 + ), + Word::empty(), + "Validate output note 1 attachment {attachment_idx} is empty", + ); + } assert_eq!( exec_output.get_kernel_mem_word( OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_METADATA_HEADER_OFFSET + NOTE_MEM_SIZE ), - output_note_2.metadata().to_header_word(), + output_note_2.metadata_header().to_metadata_word(), "Validate the output note 2 metadata header", ); assert_eq!( exec_output.get_kernel_mem_word( - OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_ATTACHMENT_OFFSET + NOTE_MEM_SIZE + OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_ATTACHMENT_0_OFFSET + NOTE_MEM_SIZE ), - output_note_2.metadata().to_attachment_word(), + output_note_2.attachments().get(0).unwrap().content().to_commitment(), "Validate the output note 2 attachment", ); + for attachment_idx in 1..4u32 { + assert_eq!( + exec_output.get_kernel_mem_word( + OUTPUT_NOTE_SECTION_OFFSET + + OUTPUT_NOTE_ATTACHMENT_0_OFFSET + + attachment_idx * WORD_SIZE as u32 + ), + Word::empty(), + "Validate output note 2 attachment {attachment_idx} is empty", + ); + } assert_eq!(exec_output.get_stack_word(0), expected_output_notes_commitment); + Ok(()) } @@ -828,7 +861,7 @@ async fn test_get_asset_info() -> anyhow::Result<()> { ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into()?, vec![fungible_asset_0], NoteType::Public, - NoteAttachment::default(), + NoteAttachments::default(), &mut RandomCoin::new(Word::from([1, 2, 3, 4u32])), )?; @@ -837,7 +870,7 @@ async fn test_get_asset_info() -> anyhow::Result<()> { ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into()?, vec![fungible_asset_0, fungible_asset_1], NoteType::Public, - NoteAttachment::default(), + NoteAttachments::default(), &mut RandomCoin::new(Word::from([4, 3, 2, 1u32])), )?; @@ -958,7 +991,7 @@ async fn test_get_recipient_and_metadata() -> anyhow::Result<()> { ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into()?, vec![FungibleAsset::mock(5)], NoteType::Public, - NoteAttachment::default(), + NoteAttachments::default(), &mut RandomCoin::new(Word::from([1, 2, 3, 4u32])), )?; @@ -1001,8 +1034,8 @@ async fn test_get_recipient_and_metadata() -> anyhow::Result<()> { "#, output_note = create_output_note(&output_note), RECIPIENT = output_note.recipient().digest(), - METADATA_HEADER = output_note.metadata().to_header_word(), - NOTE_ATTACHMENT = output_note.metadata().to_attachment_word(), + METADATA_HEADER = output_note.metadata_header().to_metadata_word(), + NOTE_ATTACHMENT = output_note.attachments().to_commitment(), ); let tx_script = CodeBuilder::default().compile_tx_script(tx_script_src)?; @@ -1140,69 +1173,94 @@ async fn test_get_assets() -> anyhow::Result<()> { Ok(()) } +#[rstest] +#[case::zero_elements(vec![], ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_CANNOT_BE_ZERO)] +#[case::one_element(vec![1], ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_MUST_BE_MULTIPLE_OF_WORD_SIZE)] +#[case::max_elements_exceeded( + vec![2; WORD_SIZE * (NoteAttachment::MAX_NUM_WORDS as usize + 1)], + ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_MAX_EXCEEDED +)] #[tokio::test] -async fn test_set_none_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::default(); - let output_note = - RawOutputNote::Full(NoteBuilder::new(account.id(), rng).attachment(attachment).build()?); +async fn test_add_attachment_with_invalid_num_elements_fails( + #[case] elements: Vec, + #[case] expected_error: MasmError, +) -> anyhow::Result<()> { + let elements = elements.into_iter().map(Felt::from).collect(); + let commitment = Word::from([42, 43, 44, 45u32]); + let tx_context = TransactionContextBuilder::with_existing_mock_account() + .extend_advice_map(vec![(commitment, elements)]) + .build()?; - let tx_script = format!( + let code = format!( " use miden::protocol::output_note + use miden::standards::note_tag::DEFAULT_TAG + use $kernel::prologue + use mock::util begin - push.{RECIPIENT} - push.{note_type} - push.{tag} - exec.output_note::create + exec.prologue::prepare_transaction + + exec.util::create_default_note # => [note_idx] - push.{ATTACHMENT} - push.{attachment_kind} - push.{attachment_scheme} - movup.6 - # => [note_idx, attachment_scheme, attachment_kind, ATTACHMENT] - exec.output_note::set_attachment + push.{COMMITMENT} + push.5 + # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] + exec.output_note::add_attachment # => [] - - # truncate the stack - swapdw dropw dropw end ", - RECIPIENT = output_note.recipient().unwrap().digest(), - note_type = output_note.metadata().note_type() as u8, - tag = output_note.metadata().tag().as_u32(), - ATTACHMENT = output_note.metadata().to_attachment_word(), - attachment_kind = output_note.metadata().attachment().content().attachment_kind().as_u8(), - attachment_scheme = output_note.metadata().attachment().attachment_scheme().as_u32(), + COMMITMENT = commitment, ); - let tx_script = CodeBuilder::new().compile_tx_script(tx_script)?; + let exec_output = tx_context.execute_code(&code).await; - let tx = TransactionContextBuilder::new(account) - .extend_expected_output_notes(vec![output_note.clone()]) - .tx_script(tx_script) - .build()? - .execute() - .await?; + assert_execution_error!(exec_output, expected_error); - let actual_note = tx.output_notes().get_note(0); - assert_eq!(actual_note.header(), output_note.header()); - assert_eq!(actual_note.assets(), output_note.assets()); + Ok(()) +} + +#[tokio::test] +async fn test_add_attachment_with_scheme_zero_fails() -> anyhow::Result<()> { + let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; + + let code = " + 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] + + push.1.2.3.4 + push.0 + # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] + exec.output_note::add_attachment + # => [] + end + "; + + let exec_output = tx_context.execute_code(code).await; + + assert_execution_error!(exec_output, ERR_OUTPUT_NOTE_ATTACHMENT_SCHEME_CANNOT_BE_ZERO); Ok(()) } #[tokio::test] -async fn test_set_word_attachment() -> 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::new_word(NoteAttachmentScheme::new(u32::MAX), Word::from([3, 4, 5, 6u32])); - let output_note = - RawOutputNote::Full(NoteBuilder::new(account.id(), rng).attachment(attachment).build()?); + NoteAttachment::new_word(NoteAttachmentScheme::MAX, Word::from([3, 4, 5, 6u32])); + let output_note = RawOutputNote::Full( + NoteBuilder::new(account.id(), rng).attachment(attachment.clone()).build()?, + ); let tx_script = format!( " @@ -1217,9 +1275,8 @@ async fn test_set_word_attachment() -> anyhow::Result<()> { push.{ATTACHMENT} push.{attachment_scheme} - movup.5 - # => [note_idx, attachment_scheme, ATTACHMENT] - exec.output_note::set_word_attachment + # => [attachment_scheme, ATTACHMENT, note_idx] + exec.output_note::add_word_attachment # => [] # truncate the stack @@ -1229,8 +1286,8 @@ async fn test_set_word_attachment() -> anyhow::Result<()> { RECIPIENT = output_note.recipient().unwrap().digest(), note_type = output_note.metadata().note_type() as u8, tag = output_note.metadata().tag().as_u32(), - attachment_scheme = output_note.metadata().attachment().attachment_scheme().as_u32(), - ATTACHMENT = output_note.metadata().to_attachment_word(), + attachment_scheme = output_note.attachments().get(0).unwrap().attachment_scheme().as_u16(), + ATTACHMENT = Word::from([3, 4, 5, 6u32]), ); let tx_script = CodeBuilder::new().compile_tx_script(tx_script)?; @@ -1243,6 +1300,9 @@ async fn test_set_word_attachment() -> anyhow::Result<()> { .await?; let actual_note = tx.output_notes().get_note(0); + assert_eq!(actual_note.attachments().num_attachments(), 1); + assert_eq!(actual_note.attachments().get(0).unwrap(), &attachment); + assert_eq!(actual_note.header(), output_note.header()); assert_eq!(actual_note.assets(), output_note.assets()); @@ -1253,11 +1313,13 @@ async fn test_set_word_attachment() -> anyhow::Result<()> { 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 elements = [3, 4, 5, 6, 7, 8, 9u32].map(Felt::from).to_vec(); - let attachment = NoteAttachment::new_array(NoteAttachmentScheme::new(42), elements.clone())?; + 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 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 tx_script = format!( " use miden::protocol::output_note @@ -1271,9 +1333,8 @@ async fn test_set_array_attachment() -> anyhow::Result<()> { push.{ATTACHMENT} push.{attachment_scheme} - movup.5 - # => [note_idx, attachment_scheme, ATTACHMENT] - exec.output_note::set_array_attachment + # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] + exec.output_note::add_array_attachment # => [] # truncate the stack @@ -1283,8 +1344,8 @@ async fn test_set_array_attachment() -> anyhow::Result<()> { RECIPIENT = output_note.recipient().unwrap().digest(), note_type = output_note.metadata().note_type() as u8, tag = output_note.metadata().tag().as_u32(), - attachment_scheme = output_note.metadata().attachment().attachment_scheme().as_u32(), - ATTACHMENT = output_note.metadata().to_attachment_word(), + attachment_scheme = output_note.attachments().get(0).unwrap().attachment_scheme().as_u16(), + ATTACHMENT = attachment_commitment, ); let tx_script = CodeBuilder::new().compile_tx_script(tx_script)?; @@ -1292,7 +1353,7 @@ 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![(output_note.metadata().to_attachment_word(), elements)]) + .extend_advice_map(vec![(attachment_commitment, elements)]) .build()? .execute() .await?; @@ -1330,7 +1391,8 @@ async fn test_set_network_target_account_attachment() -> anyhow::Result<()> { assert_eq!(actual_note.assets(), output_note.assets()); // Make sure we can deserialize the attachment back into its original type. - let actual_attachment = NetworkAccountTarget::try_from(actual_note.metadata().attachment())?; + let actual_attachment = + NetworkAccountTarget::try_from(actual_note.attachments().get(0).unwrap())?; assert_eq!(actual_attachment, attachment); Ok(()) @@ -1412,27 +1474,21 @@ async fn test_network_note() -> anyhow::Result<()> { /// procedure under test with index 1, which is out of bounds. The bounds assertion fires before /// any parameter validation, so dummy values are sufficient. #[rstest] -#[case::add_asset(8, 0, "add_asset")] -#[case::get_assets_info(0, 0, "get_assets_info")] -#[case::get_assets(0, 1, "get_assets")] -#[case::get_recipient(0, 0, "get_recipient")] -#[case::get_metadata(0, 0, "get_metadata")] -#[case::set_attachment(0, 6, "set_attachment")] -#[case::set_word_attachment(0, 5, "set_word_attachment")] -#[case::set_array_attachment(0, 5, "set_array_attachment")] +#[case::add_asset(8, "add_asset")] +#[case::get_assets_info(0, "get_assets_info")] +#[case::get_assets(1, "get_assets")] +#[case::get_recipient(0, "get_recipient")] +#[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")] #[tokio::test] async fn test_output_note_index_out_of_bounds( #[case] params_above: usize, - #[case] params_below: usize, #[case] procedure_name: &str, ) -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; - let push_below = if params_below > 0 { - format!("repeat.{params_below} push.99 end") - } else { - String::new() - }; let push_above = if params_above > 0 { format!("repeat.{params_above} push.99 end") } else { @@ -1455,15 +1511,13 @@ async fn test_output_note_index_out_of_bounds( drop # => [] - # push garbage parameters that should sit below note_idx - {push_below} - # push the out-of-bounds index (1 == num_output_notes) push.1 - # => [note_idx = 1, params_below...] + # => [note_idx = 1] # push garbage parameters that should sit above note_idx {push_above} + # => [params_above(n), note_idx = 1] # call the procedure under test with the invalid index exec.output_note::{procedure_name} diff --git a/crates/miden-testing/src/kernel_tests/tx/test_prologue.rs b/crates/miden-testing/src/kernel_tests/tx/test_prologue.rs index 6673e15064..7fac3753fa 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_prologue.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_prologue.rs @@ -489,13 +489,13 @@ fn input_notes_memory_assertions( assert_eq!( exec_output.get_note_mem_word(note_idx, INPUT_NOTE_METADATA_HEADER_OFFSET), - note.metadata().to_header_word(), + note.metadata_header().to_metadata_word(), "note metadata header should be stored at the correct offset" ); assert_eq!( exec_output.get_note_mem_word(note_idx, INPUT_NOTE_ATTACHMENT_OFFSET), - note.metadata().to_attachment_word(), + note.attachments().to_commitment(), "note attachment should be stored at the correct offset" ); 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 7f2b975414..e5578003f0 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_tx.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_tx.rs @@ -26,7 +26,7 @@ use miden_protocol::note::{ NoteAttachment, NoteAttachmentContent, NoteAttachmentScheme, - NoteHeader, + NoteAttachments, NoteId, NoteMetadata, NoteRecipient, @@ -218,10 +218,10 @@ 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])); + NoteAttachment::new_word(NoteAttachmentScheme::new(28)?, Word::from([2, 3, 4, 5u32])); let attachment3 = NoteAttachment::new_array( - NoteAttachmentScheme::new(29), - [6, 7, 8, 9u32].map(Felt::from).to_vec(), + NoteAttachmentScheme::new(29)?, + vec![Word::from([6, 7, 8, 9u32]), Word::from([10, 11, 12, 13u32])], )?; let note_type1 = NoteType::Private; @@ -237,23 +237,23 @@ async fn executed_transaction_output_notes() -> anyhow::Result<()> { let serial_num_2 = Word::from([1, 2, 3, 4u32]); let note_script_2 = CodeBuilder::default().compile_note_script(DEFAULT_NOTE_SCRIPT)?; let inputs_2 = NoteStorage::new(vec![ONE])?; - let metadata_2 = NoteMetadata::new(account_id, note_type2) - .with_tag(tag2) - .with_attachment(attachment2.clone()); + let metadata_2 = NoteMetadata::new(account_id, note_type2).with_tag(tag2); let vault_2 = NoteAssets::new(vec![removed_asset_3, removed_asset_4])?; let recipient_2 = NoteRecipient::new(serial_num_2, note_script_2, inputs_2); - let expected_output_note_2 = Note::new(vault_2, metadata_2, recipient_2); + let attachments_2 = NoteAttachments::from(attachment2.clone()); + let expected_output_note_2 = + Note::with_attachments(vault_2, metadata_2, recipient_2, attachments_2); // Create the expected output note for Note 3 which is public let serial_num_3 = Word::from([Felt::new(5), Felt::new(6), Felt::new(7), Felt::new(8)]); let note_script_3 = CodeBuilder::default().compile_note_script(DEFAULT_NOTE_SCRIPT)?; let inputs_3 = NoteStorage::new(vec![ONE, Felt::new(2)])?; - let metadata_3 = NoteMetadata::new(account_id, note_type3) - .with_tag(tag3) - .with_attachment(attachment3.clone()); + let metadata_3 = NoteMetadata::new(account_id, note_type3).with_tag(tag3); let vault_3 = NoteAssets::new(vec![])?; let recipient_3 = NoteRecipient::new(serial_num_3, note_script_3, inputs_3); - let expected_output_note_3 = Note::new(vault_3, metadata_3, recipient_3); + let attachments_3 = NoteAttachments::from(attachment3.clone()); + let expected_output_note_3 = + Note::with_attachments(vault_3, metadata_3, recipient_3, attachments_3); let tx_script_src = format!( "\ @@ -307,8 +307,8 @@ async fn executed_transaction_output_notes() -> anyhow::Result<()> { push.{ATTACHMENT2} push.{attachment_scheme2} - movup.5 - exec.output_note::set_word_attachment + # => [attachment_scheme, ATTACHMENT, note_idx] + exec.output_note::add_word_attachment # => [] # create a public note without assets @@ -320,8 +320,8 @@ async fn executed_transaction_output_notes() -> anyhow::Result<()> { push.{ATTACHMENT3} push.{attachment_scheme3} - movup.5 - exec.output_note::set_array_attachment + # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] + exec.output_note::add_array_attachment # => [] end ", @@ -338,10 +338,10 @@ async fn executed_transaction_output_notes() -> anyhow::Result<()> { NOTETYPE1 = note_type1 as u8, NOTETYPE2 = note_type2 as u8, NOTETYPE3 = note_type3 as u8, - attachment_scheme2 = attachment2.attachment_scheme().as_u32(), - ATTACHMENT2 = attachment2.content().to_word(), - attachment_scheme3 = attachment3.attachment_scheme().as_u32(), - ATTACHMENT3 = attachment3.content().to_word(), + 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(), ); let tx_script = CodeBuilder::with_mock_libraries().compile_tx_script(tx_script_src)?; @@ -356,7 +356,7 @@ async fn executed_transaction_output_notes() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::new(executor_account) .tx_script(tx_script) - .extend_advice_map(vec![(attachment3.content().to_word(), array.as_slice().to_vec())]) + .extend_advice_map(vec![(attachment3.content().to_commitment(), array.to_elements())]) .extend_expected_output_notes(vec![ RawOutputNote::Full(expected_output_note_2.clone()), RawOutputNote::Full(expected_output_note_3.clone()), @@ -382,12 +382,7 @@ async fn executed_transaction_output_notes() -> anyhow::Result<()> { // assert that the expected output note 2 is present let resulting_output_note_2 = executed_transaction.output_notes().get_note(1); - let expected_note_id_2 = expected_output_note_2.id(); - let expected_note_metadata_2 = expected_output_note_2.metadata().clone(); - assert_eq!( - *resulting_output_note_2.header(), - NoteHeader::new(expected_note_id_2, expected_note_metadata_2) - ); + assert_eq!(*resulting_output_note_2.header(), *expected_output_note_2.header()); // assert that the expected output note 3 is present and has no assets let resulting_output_note_3 = executed_transaction.output_notes().get_note(2); @@ -472,7 +467,7 @@ async fn user_code_can_abort_transaction_with_summary() -> anyhow::Result<()> { account.id(), vec![], NoteType::Private, - NoteAttachment::default(), + NoteAttachments::default(), &mut rng, )?; let input_note = create_spawn_note(vec![&output_note])?; @@ -517,7 +512,7 @@ async fn tx_summary_commitment_is_signed_by_falcon_auth() -> anyhow::Result<()> account.id(), vec![], NoteType::Private, - NoteAttachment::default(), + NoteAttachments::default(), &mut rng, )?; let spawn_note = builder.add_spawn_note([&p2id_note])?; @@ -576,7 +571,7 @@ async fn tx_summary_commitment_is_signed_by_ecdsa_auth() -> anyhow::Result<()> { account.id(), vec![], NoteType::Private, - NoteAttachment::default(), + NoteAttachments::default(), &mut rng, )?; let spawn_note = builder.add_spawn_note([&p2id_note])?; diff --git a/crates/miden-testing/src/mock_chain/chain_builder.rs b/crates/miden-testing/src/mock_chain/chain_builder.rs index 4e2ef3511b..7eba045dbb 100644 --- a/crates/miden-testing/src/mock_chain/chain_builder.rs +++ b/crates/miden-testing/src/mock_chain/chain_builder.rs @@ -42,7 +42,7 @@ use miden_protocol::block::{ }; use miden_protocol::crypto::merkle::smt::Smt; use miden_protocol::errors::NoteError; -use miden_protocol::note::{Note, NoteAttachment, NoteDetails, NoteType}; +use miden_protocol::note::{Note, NoteAttachments, NoteDetails, NoteType}; use miden_protocol::testing::account_id::ACCOUNT_ID_FEE_FAUCET; use miden_protocol::testing::random_secret_key::random_secret_key; use miden_protocol::transaction::{OrderedTransactionHeaders, RawOutputNote, TransactionKernel}; @@ -626,7 +626,7 @@ impl MockChainBuilder { target_account_id, asset.to_vec(), note_type, - NoteAttachment::default(), + NoteAttachments::default(), &mut self.rng, )?; self.add_output_note(RawOutputNote::Full(note.clone())); @@ -677,7 +677,7 @@ impl MockChainBuilder { offered_asset, requested_asset, NoteType::Public, - NoteAttachment::default(), + NoteAttachments::default(), payback_note_type, &mut self.rng, )?; diff --git a/crates/miden-testing/src/standards/network_account_target.rs b/crates/miden-testing/src/standards/network_account_target.rs index 177c0f8958..b463a0287e 100644 --- a/crates/miden-testing/src/standards/network_account_target.rs +++ b/crates/miden-testing/src/standards/network_account_target.rs @@ -2,7 +2,15 @@ use miden_protocol::Felt; use miden_protocol::account::AccountStorageMode; -use miden_protocol::note::{NoteAttachment, NoteMetadata, NoteTag, NoteType}; +use miden_protocol::note::{ + NoteAttachment, + NoteAttachmentContent, + NoteAttachments, + NoteMetadata, + NoteMetadataHeader, + NoteTag, + NoteType, +}; use miden_protocol::testing::account_id::AccountIdBuilder; use miden_standards::note::{NetworkAccountTarget, NoteExecutionHint}; @@ -16,10 +24,11 @@ async fn network_account_target_get_id() -> anyhow::Result<()> { let exec_hint = NoteExecutionHint::Always; let attachment = NoteAttachment::from(NetworkAccountTarget::new(target_id, exec_hint)?); + let attachments = NoteAttachments::from(attachment.clone()); let metadata = NoteMetadata::new(target_id, NoteType::Public) - .with_tag(NoteTag::with_account_target(target_id)) - .with_attachment(attachment.clone()); - let metadata_header = metadata.to_header_word(); + .with_tag(NoteTag::with_account_target(target_id)); + let metadata_header = NoteMetadataHeader::new(metadata, &attachments); + let metadata_word = metadata_header.to_metadata_word(); let source = format!( r#" @@ -29,12 +38,10 @@ async fn network_account_target_get_id() -> anyhow::Result<()> { const ERR_NOT_NETWORK_ACCOUNT_TARGET = "attachment is not a valid network account target" begin - push.{attachment_word} - push.{metadata_header} - exec.note::metadata_into_attachment_info - # => [attachment_kind, attachment_scheme, NOTE_ATTACHMENT] - swap - # => [attachment_scheme, attachment_kind, NOTE_ATTACHMENT] + push.{attachment_commitment} + push.{metadata_word} + exec.note::metadata_into_attachment_header + # => [attachment_0_scheme, NOTE_ATTACHMENT] exec.network_account_target::is_network_account_target # => [is_valid, NOTE_ATTACHMENT] assert.err=ERR_NOT_NETWORK_ACCOUNT_TARGET @@ -45,8 +52,11 @@ async fn network_account_target_get_id() -> anyhow::Result<()> { movup.2 drop movup.2 drop end "#, - metadata_header = metadata_header, - attachment_word = attachment.content().to_word(), + metadata_word = metadata_word, + attachment_commitment = match attachment.content() { + NoteAttachmentContent::Word(word) => *word, + _ => unreachable!("expected word attachment"), + }, ); let exec_output = CodeExecutor::with_default_host().run(&source).await?; @@ -65,8 +75,10 @@ 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 attachment_word = attachment.content().to_word(); - let expected_attachment_kind = Felt::from(attachment.attachment_kind().as_u8()); + let raw_attachment_word = match attachment.content() { + NoteAttachmentContent::Word(word) => *word, + _ => unreachable!("expected word attachment"), + }; let source = format!( r#" @@ -78,7 +90,7 @@ async fn network_account_target_new_attachment() -> anyhow::Result<()> { push.{target_id_suffix} # => [target_id_suffix, target_id_prefix, exec_hint] exec.network_account_target::new - # => [attachment_scheme, attachment_kind, ATTACHMENT, pad(16)] + # => [attachment_scheme, NOTE_ATTACHMENT, pad(16)] # cleanup stack swapdw dropw dropw @@ -91,14 +103,13 @@ async fn network_account_target_new_attachment() -> anyhow::Result<()> { let exec_output = CodeExecutor::with_default_host().run(&source).await?; - assert_eq!(exec_output.stack[0], expected_attachment_kind); assert_eq!( - exec_output.stack[1], - Felt::from(NetworkAccountTarget::ATTACHMENT_SCHEME.as_u32()) + exec_output.stack[0], + Felt::from(NetworkAccountTarget::ATTACHMENT_SCHEME.as_u16()) ); - let word = exec_output.stack.get_word(2).unwrap(); - assert_eq!(word, attachment_word); + let word = exec_output.stack.get_word(1).unwrap(); + assert_eq!(word, raw_attachment_word); Ok(()) } @@ -122,11 +133,11 @@ async fn network_account_target_attachment_round_trip() -> anyhow::Result<()> { push.{target_id_suffix} # => [target_id_suffix, target_id_prefix, exec_hint] exec.network_account_target::new - # => [attachment_scheme, attachment_kind, ATTACHMENT] + # => [attachment_scheme, NOTE_ATTACHMENT] exec.network_account_target::is_network_account_target - # => [is_valid, ATTACHMENT] + # => [is_valid, NOTE_ATTACHMENT] assert.err=ERR_NOT_NETWORK_ACCOUNT_TARGET - # => [ATTACHMENT] + # => [NOTE_ATTACHMENT] exec.network_account_target::get_id # => [target_id_suffix, target_id_prefix] # cleanup stack diff --git a/crates/miden-testing/src/utils.rs b/crates/miden-testing/src/utils.rs index 67744003cf..d38b3f8284 100644 --- a/crates/miden-testing/src/utils.rs +++ b/crates/miden-testing/src/utils.rs @@ -8,6 +8,7 @@ use miden_protocol::asset::Asset; use miden_protocol::crypto::rand::FeltRng; use miden_protocol::errors::NoteError; use miden_protocol::note::{Note, NoteAssets, NoteMetadata, NoteTag, NoteType}; +use miden_protocol::vm::AdviceMap; use miden_standards::code_builder::CodeBuilder; use miden_standards::note::P2idNoteStorage; use miden_standards::testing::note::NoteBuilder; @@ -189,21 +190,24 @@ where .metadata() .sender(); - let note_code = note_script_that_creates_notes(sender_id, output_notes)?; + let (note_code, advice_map) = note_script_that_creates_notes(sender_id, output_notes)?; let note = NoteBuilder::new(sender_id, SmallRng::from_os_rng()) .code(note_code) + .advice_map(advice_map) .dynamically_linked_libraries(CodeBuilder::mock_libraries()) .build()?; Ok(note) } -/// Returns the code for a note that creates all notes in `output_notes` +/// 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. fn note_script_that_creates_notes<'note>( sender_id: AccountId, output_notes: impl Iterator, -) -> anyhow::Result { +) -> anyhow::Result<(String, AdviceMap)> { + let mut advice_map = AdviceMap::default(); let mut out = String::from("use miden::protocol::output_note\n\n@note_script\npub proc main\n"); for (idx, note) in output_notes.into_iter().enumerate() { @@ -241,20 +245,24 @@ fn note_script_that_creates_notes<'note>( tag = note.metadata().tag(), )); - out.push_str(&format!( - " - push.{ATTACHMENT} - push.{attachment_scheme} - push.{attachment_kind} - dup.6 - # => [note_idx, attachment_kind, attachment_scheme, ATTACHMENT, note_idx] - exec.output_note::set_attachment - # => [note_idx] - ", - ATTACHMENT = note.metadata().to_attachment_word(), - attachment_scheme = note.metadata().attachment().attachment_scheme().as_u32(), - attachment_kind = note.metadata().attachment().content().attachment_kind().as_u8(), - )); + for attachment in note.attachments().iter() { + let attachment_scheme = attachment.attachment_scheme().as_u16(); + let commitment = attachment.content().to_commitment(); + + out.push_str(&format!( + " + dup + push.{commitment} + push.{attachment_scheme} + # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx, note_idx] + exec.output_note::add_attachment + # => [note_idx] + ", + )); + + // Add the elements to the advice map keyed by the commitment. + advice_map.insert(commitment, attachment.content().to_elements()); + } for asset in note.assets().iter() { out.push_str(&format!( @@ -273,7 +281,7 @@ fn note_script_that_creates_notes<'note>( out.push_str("repeat.5 dropw end\nend"); - Ok(out) + Ok((out, advice_map)) } /// Generates a P2ID note - Pay-to-ID note with an exact serial number diff --git a/crates/miden-testing/tests/agglayer/bridge_out.rs b/crates/miden-testing/tests/agglayer/bridge_out.rs index d666fd9a98..ad3a57d6db 100644 --- a/crates/miden-testing/tests/agglayer/bridge_out.rs +++ b/crates/miden-testing/tests/agglayer/bridge_out.rs @@ -24,7 +24,7 @@ use miden_protocol::note::{NoteAssets, NoteType}; use miden_protocol::transaction::RawOutputNote; use miden_standards::account::faucets::TokenMetadata; use miden_standards::account::policies::MintPolicyConfig; -use miden_standards::note::StandardNote; +use miden_standards::note::{NetworkAccountTarget, StandardNote}; use miden_testing::{Auth, MockChain, assert_transaction_executor_error}; use miden_tx::utils::hex_to_bytes; @@ -195,8 +195,12 @@ async fn bridge_out_consecutive() -> anyhow::Result<()> { NoteType::Public, "BURN note should be public" ); - let attachment = burn_note.metadata().attachment(); - let network_target = miden_standards::note::NetworkAccountTarget::try_from(attachment) + assert_eq!( + burn_note.attachments().num_attachments(), + 1, + "BURN note should have one attachment" + ); + let network_target = NetworkAccountTarget::try_from(burn_note.attachments()) .expect("BURN note attachment should be a valid NetworkAccountTarget"); assert_eq!( network_target.target_id(), diff --git a/crates/miden-testing/tests/scripts/faucet.rs b/crates/miden-testing/tests/scripts/faucet.rs index 7dbd3b0ec9..0a201cac78 100644 --- a/crates/miden-testing/tests/scripts/faucet.rs +++ b/crates/miden-testing/tests/scripts/faucet.rs @@ -18,7 +18,7 @@ use miden_protocol::asset::{Asset, FungibleAsset, TokenSymbol}; use miden_protocol::note::{ Note, NoteAssets, - NoteAttachment, + NoteAttachments, NoteId, NoteMetadata, NoteRecipient, @@ -707,7 +707,7 @@ async fn network_faucet_mint() -> anyhow::Result<()> { faucet.id(), faucet_owner_account_id, mint_storage, - NoteAttachment::default(), + NoteAttachments::default(), &mut rng, )?; @@ -799,7 +799,7 @@ async fn test_network_faucet_owner_can_mint() -> anyhow::Result<()> { faucet.id(), owner_account_id, mint_inputs, - NoteAttachment::default(), + NoteAttachments::default(), &mut rng, )?; @@ -951,7 +951,7 @@ async fn test_network_faucet_non_owner_cannot_mint() -> anyhow::Result<()> { faucet.id(), non_owner_account_id, mint_inputs, - NoteAttachment::default(), + NoteAttachments::default(), &mut rng, )?; @@ -1050,7 +1050,7 @@ async fn test_network_faucet_transfer_ownership() -> anyhow::Result<()> { faucet.id(), initial_owner_account_id, mint_inputs.clone(), - NoteAttachment::default(), + NoteAttachments::default(), &mut rng, )?; @@ -1417,7 +1417,7 @@ async fn network_faucet_burn() -> anyhow::Result<()> { faucet_owner_account_id, faucet.id(), fungible_asset.into(), - NoteAttachment::default(), + NoteAttachments::default(), &mut rng, )?; @@ -1493,7 +1493,7 @@ async fn test_network_faucet_non_owner_cannot_burn_when_owner_only_policy_active non_owner_account_id, faucet.id(), fungible_asset.into(), - NoteAttachment::default(), + NoteAttachments::default(), &mut rng, )?; builder.add_output_note(RawOutputNote::Full(set_policy_note.clone())); @@ -1551,7 +1551,7 @@ async fn test_network_faucet_owner_can_burn_when_owner_only_policy_active() -> a owner_account_id, faucet.id(), fungible_asset.into(), - NoteAttachment::default(), + NoteAttachments::default(), &mut rng, )?; builder.add_output_note(RawOutputNote::Full(set_policy_note.clone())); @@ -1643,7 +1643,7 @@ async fn test_mint_note_output_note_types(#[case] note_type: NoteType) -> anyhow faucet.id(), faucet_owner_account_id, mint_storage.clone(), - NoteAttachment::default(), + NoteAttachments::default(), &mut rng, )?; diff --git a/crates/miden-testing/tests/scripts/p2id.rs b/crates/miden-testing/tests/scripts/p2id.rs index 4d6e330b27..35fd440f91 100644 --- a/crates/miden-testing/tests/scripts/p2id.rs +++ b/crates/miden-testing/tests/scripts/p2id.rs @@ -2,7 +2,7 @@ use miden_protocol::account::Account; use miden_protocol::account::auth::AuthScheme; use miden_protocol::asset::{Asset, AssetVault, FungibleAsset}; use miden_protocol::crypto::rand::RandomCoin; -use miden_protocol::note::{NoteAttachment, NoteTag, NoteType}; +use miden_protocol::note::{NoteAttachments, NoteTag, NoteType}; use miden_protocol::testing::account_id::{ ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2, @@ -227,7 +227,7 @@ async fn test_create_consume_multiple_notes() -> anyhow::Result<()> { ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2.try_into()?, vec![asset_1], NoteType::Public, - NoteAttachment::default(), + NoteAttachments::default(), &mut RandomCoin::new(Word::from([1, 2, 3, 4u32])), )?; @@ -236,7 +236,7 @@ async fn test_create_consume_multiple_notes() -> anyhow::Result<()> { ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into()?, vec![asset_2], NoteType::Public, - NoteAttachment::default(), + NoteAttachments::default(), &mut RandomCoin::new(Word::from([4, 3, 2, 1u32])), )?; @@ -370,7 +370,7 @@ async fn test_p2id_new_constructor() -> anyhow::Result<()> { target_account.id(), vec![FungibleAsset::mock(50)], NoteType::Public, - NoteAttachment::default(), + NoteAttachments::default(), &mut RandomCoin::new(serial_num), )?; diff --git a/crates/miden-testing/tests/scripts/pswap.rs b/crates/miden-testing/tests/scripts/pswap.rs index 118f1ce7d9..4abd3c9193 100644 --- a/crates/miden-testing/tests/scripts/pswap.rs +++ b/crates/miden-testing/tests/scripts/pswap.rs @@ -5,7 +5,14 @@ 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, NoteAttachmentScheme, NoteType}; +use miden_protocol::note::{ + Note, + NoteAttachment, + NoteAttachmentContent, + NoteAttachmentScheme, + NoteAttachments, + NoteType, +}; use miden_protocol::transaction::RawOutputNote; use miden_protocol::{Felt, ONE, Word, ZERO}; use miden_standards::account::wallets::BasicWallet; @@ -31,6 +38,15 @@ const BASIC_AUTH: Auth = Auth::BasicAuth { // HELPERS // ================================================================================================ +/// 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"), + } +} + /// Builds a PswapNote, registers it on the builder as an output note, and returns /// both the `PswapNote` (for `.execute()`) and the protocol `Note` (for /// `.id()` / `RawOutputNote::Full`), so callers don't need to round-trip via @@ -177,14 +193,14 @@ async fn pswap_note_alice_reconstructs_and_consumes_p2id() -> anyhow::Result<()> // Read the attachment from the executed transaction's output (not from the // Rust-predicted `p2id_note`) so this actually validates the MASM side. let output_p2id = executed_transaction.output_notes().get_note(0); - let aux_word = output_p2id.metadata().attachment().content().to_word(); - let fill_amount_from_aux = aux_word[0].as_canonical_u64(); + let attachment_word = first_attachment_word(output_p2id.attachments()); + let fill_amount_from_aux = attachment_word[0].as_canonical_u64(); assert_eq!(fill_amount_from_aux, 20, "Fill amount from aux should be 20 ETH"); // Parity check: Rust-predicted P2ID attachment must match the MASM output. assert_eq!( - p2id_note.metadata().attachment().content().to_word(), - aux_word, + first_attachment_word(p2id_note.attachments()), + attachment_word, "Rust-predicted P2ID attachment does not match the MASM-produced one", ); @@ -208,16 +224,16 @@ async fn pswap_note_alice_reconstructs_and_consumes_p2id() -> anyhow::Result<()> // remainder PswapNote. let output_remainder = executed_transaction.output_notes().get_note(1); - let remainder_aux = output_remainder.metadata().attachment().content().to_word(); - let amt_payout_from_aux = remainder_aux[0].as_canonical_u64(); + let remainder_attachment_word = first_attachment_word(output_remainder.attachments()); + let amt_payout_from_attachment = remainder_attachment_word[0].as_canonical_u64(); let expected_payout = pswap.calculate_offered_for_requested(fill_amount_from_aux)?; assert_eq!( - amt_payout_from_aux, expected_payout, + amt_payout_from_attachment, expected_payout, "remainder aux should carry amt_payout matching the Rust-side calc", ); - let remaining_offered = offered_asset.amount() - amt_payout_from_aux; + let remaining_offered = offered_asset.amount() - amt_payout_from_attachment; let remaining_requested = requested_asset.amount() - fill_amount_from_aux; let remainder_storage = PswapNoteStorage::builder() @@ -231,14 +247,13 @@ async fn pswap_note_alice_reconstructs_and_consumes_p2id() -> anyhow::Result<()> Word::from([serial_number[0], serial_number[1], serial_number[2], serial_number[3] + ONE]); let remainder_attachment_word = Word::from([ - Felt::try_from(amt_payout_from_aux).expect("amt_payout fits in a felt"), + Felt::try_from(amt_payout_from_attachment).expect("amt_payout fits in a felt"), ZERO, ZERO, ZERO, ]); let remainder_attachment = NoteAttachment::new_word(NoteAttachmentScheme::none(), remainder_attachment_word); - let reconstructed_remainder: Note = PswapNote::builder() .sender(bob.id()) .storage(remainder_storage) @@ -268,8 +283,8 @@ async fn pswap_note_alice_reconstructs_and_consumes_p2id() -> anyhow::Result<()> // Parity on the attachment word itself. assert_eq!( - reconstructed_remainder.metadata().attachment().content().to_word(), - remainder_aux, + first_attachment_word(reconstructed_remainder.attachments()), + remainder_attachment_word, "reconstructed remainder attachment does not match executed output", ); @@ -343,8 +358,8 @@ async fn pswap_attachment_layout_matches_masm_test() -> anyhow::Result<()> { let output_notes = executed_transaction.output_notes(); assert_eq!(output_notes.num_notes(), 2, "expected P2ID + remainder"); - let p2id_attachment = output_notes.get_note(0).metadata().attachment().content().to_word(); - let remainder_attachment = output_notes.get_note(1).metadata().attachment().content().to_word(); + let p2id_attachment = first_attachment_word(output_notes.get_note(0).attachments()); + let remainder_attachment = first_attachment_word(output_notes.get_note(1).attachments()); // P2ID payback attachment: `[fill_amount, 0, 0, 0]` — fill_amount at Word[0]. let expected_p2id_attachment = Word::from([ @@ -374,12 +389,12 @@ async fn pswap_attachment_layout_matches_masm_test() -> anyhow::Result<()> { // words as the on-chain executed ones. A future drift between either side // would fail here even if the Word[0] position stays correct. assert_eq!( - p2id_note.metadata().attachment().content().to_word(), + first_attachment_word(p2id_note.attachments()), p2id_attachment, "Rust-predicted P2ID attachment does not match MASM output", ); assert_eq!( - remainder_note.metadata().attachment().content().to_word(), + first_attachment_word(remainder_note.attachments()), remainder_attachment, "Rust-predicted remainder attachment does not match MASM output", ); diff --git a/crates/miden-testing/tests/scripts/send_note.rs b/crates/miden-testing/tests/scripts/send_note.rs index 6ca7c04e43..ed43f4093f 100644 --- a/crates/miden-testing/tests/scripts/send_note.rs +++ b/crates/miden-testing/tests/scripts/send_note.rs @@ -1,6 +1,7 @@ use core::slice; use std::collections::BTreeMap; +use miden_protocol::Word; use miden_protocol::account::auth::AuthScheme; use miden_protocol::asset::{Asset, FungibleAsset, NonFungibleAsset}; use miden_protocol::crypto::rand::{FeltRng, RandomCoin}; @@ -9,6 +10,7 @@ use miden_protocol::note::{ NoteAssets, NoteAttachment, NoteAttachmentScheme, + NoteAttachments, NoteMetadata, NoteRecipient, NoteStorage, @@ -18,7 +20,6 @@ use miden_protocol::note::{ }; use miden_protocol::testing::note::DEFAULT_NOTE_SCRIPT; use miden_protocol::transaction::RawOutputNote; -use miden_protocol::{Felt, Word}; use miden_standards::account::interface::{AccountInterface, AccountInterfaceExt}; use miden_standards::code_builder::CodeBuilder; use miden_testing::utils::create_p2any_note; @@ -62,17 +63,17 @@ async fn test_send_note_script_basic_wallet() -> anyhow::Result<()> { let sender_account_interface = AccountInterface::from_account(&sender_basic_wallet_account); let tag = NoteTag::with_account_target(sender_basic_wallet_account.id()); - let elements = [9, 8, 7, 6, 5u32].map(Felt::from).to_vec(); - let attachment = NoteAttachment::new_array(NoteAttachmentScheme::new(42), elements.clone())?; - let metadata = NoteMetadata::new(sender_basic_wallet_account.id(), NoteType::Public) - .with_tag(tag) - .with_attachment(attachment.clone()); + let words = vec![Word::from([9, 8, 7, 6u32]), Word::from([5, 4, 3, 2u32])]; + let attachment = NoteAttachment::new_array(NoteAttachmentScheme::new(42)?, words.clone())?; + let metadata = + NoteMetadata::new(sender_basic_wallet_account.id(), NoteType::Public).with_tag(tag); let assets = NoteAssets::new(vec![sent_asset0, sent_asset1]).unwrap(); let note_script = CodeBuilder::default().compile_note_script(DEFAULT_NOTE_SCRIPT).unwrap(); let serial_num = RandomCoin::new(Word::from([1, 2, 3, 4u32])).draw_word(); let recipient = NoteRecipient::new(serial_num, note_script, NoteStorage::default()); + let attachments = NoteAttachments::from(attachment.clone()); - let note = Note::new(assets.clone(), metadata, recipient); + let note = Note::with_attachments(assets.clone(), metadata, recipient, attachments); let partial_note: PartialNote = note.clone().into(); let expiration_delta = 10u16; @@ -136,18 +137,18 @@ 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::new_word(NoteAttachmentScheme::new(100)?, Word::empty()); let metadata = NoteMetadata::new(sender_basic_fungible_faucet_account.id(), NoteType::Public) - .with_tag(tag) - .with_attachment(attachment); + .with_tag(tag); let assets = NoteAssets::new(vec![Asset::Fungible( FungibleAsset::new(sender_basic_fungible_faucet_account.id(), 10).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); - let note = Note::new(assets.clone(), metadata, recipient); + let note = Note::with_attachments(assets.clone(), metadata, recipient, attachments); let partial_note: PartialNote = note.clone().into(); let expiration_delta = 10u16; diff --git a/crates/miden-tx/src/errors/mod.rs b/crates/miden-tx/src/errors/mod.rs index 72cbfd5935..d93d2963cf 100644 --- a/crates/miden-tx/src/errors/mod.rs +++ b/crates/miden-tx/src/errors/mod.rs @@ -258,12 +258,10 @@ pub enum TransactionKernelError { "public note with metadata {0:?} and recipient digest {1} is missing details in the advice provider" )] PublicNoteMissingDetails(NoteMetadata, Word), - #[error("attachment provided to set_attachment must be empty when attachment kind is None")] - NoteAttachmentNoneIsNotEmpty, #[error( - "commitment of note attachment {actual} does not match attachment {provided} provided to set_attachment" + "commitment of note attachment advice data is {actual} which does not match commitment {provided} provided to add_attachment" )] - NoteAttachmentArrayMismatch { actual: Word, provided: Word }, + NoteAttachmentCommitmentMismatch { actual: Word, provided: Word }, #[error( "note storage in advice provider contains fewer items ({actual}) than specified ({specified}) by its number of storage items" )] diff --git a/crates/miden-tx/src/executor/exec_host.rs b/crates/miden-tx/src/executor/exec_host.rs index ca3e3eaf62..ad71bd78f2 100644 --- a/crates/miden-tx/src/executor/exec_host.rs +++ b/crates/miden-tx/src/executor/exec_host.rs @@ -611,9 +611,9 @@ where self.base_host.on_note_before_add_asset(note_idx, asset) }, - TransactionEvent::NoteBeforeSetAttachment { note_idx, attachment } => self + TransactionEvent::NoteBeforeAddAttachment { note_idx, attachment } => self .base_host - .on_note_before_set_attachment(note_idx, attachment) + .on_note_before_add_attachment(note_idx, attachment) .map(|_| Vec::new()), TransactionEvent::AuthRequest { pub_key_hash, tx_summary, signature } => { diff --git a/crates/miden-tx/src/host/mod.rs b/crates/miden-tx/src/host/mod.rs index b0f40dac3b..6c86a8ab44 100644 --- a/crates/miden-tx/src/host/mod.rs +++ b/crates/miden-tx/src/host/mod.rs @@ -311,8 +311,8 @@ impl<'store, STORE> TransactionBaseHost<'store, STORE> { Ok(Vec::new()) } - /// Sets the attachment on the output note identified by the note index. - pub fn on_note_before_set_attachment( + /// Appends an attachment to the output note identified by the note index. + pub fn on_note_before_add_attachment( &mut self, note_idx: usize, attachment: NoteAttachment, @@ -321,7 +321,7 @@ impl<'store, STORE> TransactionBaseHost<'store, STORE> { TransactionKernelError::other(format!("failed to find output note {note_idx}")) })?; - note_builder.set_attachment(attachment); + note_builder.add_attachment(attachment)?; Ok(Vec::new()) } diff --git a/crates/miden-tx/src/host/note_builder.rs b/crates/miden-tx/src/host/note_builder.rs index d392c16b51..fb42d60106 100644 --- a/crates/miden-tx/src/host/note_builder.rs +++ b/crates/miden-tx/src/host/note_builder.rs @@ -1,3 +1,4 @@ +use alloc::string::ToString; use alloc::vec::Vec; use miden_protocol::asset::Asset; @@ -6,6 +7,7 @@ use miden_protocol::note::{ Note, NoteAssets, NoteAttachment, + NoteAttachments, NoteMetadata, NoteRecipient, PartialNote, @@ -26,6 +28,7 @@ use crate::errors::TransactionKernelError; pub struct OutputNoteBuilder { metadata: NoteMetadata, assets: Vec, + attachments: NoteAttachments, recipient_digest: Word, recipient: Option, } @@ -58,6 +61,7 @@ impl OutputNoteBuilder { recipient_digest, recipient: None, assets: Vec::new(), + attachments: NoteAttachments::empty(), }) } @@ -68,6 +72,7 @@ impl OutputNoteBuilder { recipient_digest: recipient.digest(), recipient: Some(recipient), assets: Vec::new(), + attachments: NoteAttachments::empty(), } } @@ -116,9 +121,21 @@ impl OutputNoteBuilder { Ok(()) } - /// Overwrites the attachment in the note's metadata. - pub fn set_attachment(&mut self, attachment: NoteAttachment) { - self.metadata.set_attachment(attachment); + /// Appends an attachment to the note. + /// + /// # Errors + /// Returns an error if the note already has the maximum number of attachments, or if the + /// total number of words across all attachments exceeds the maximum. + pub fn add_attachment( + &mut self, + attachment: NoteAttachment, + ) -> Result<(), TransactionKernelError> { + let mut attachments = core::mem::take(&mut self.attachments).into_vec(); + attachments.push(attachment); + self.attachments = NoteAttachments::new(attachments) + .map_err(|err| TransactionKernelError::other(err.to_string()))?; + + Ok(()) } /// Converts this builder to an [OutputNote]. @@ -131,11 +148,17 @@ impl OutputNoteBuilder { match self.recipient { Some(recipient) => { - let note = Note::new(assets, self.metadata, recipient); + let note = + Note::with_attachments(assets, self.metadata, recipient, self.attachments); RawOutputNote::Full(note) }, None => { - let note = PartialNote::new(self.metadata, self.recipient_digest, assets); + let note = PartialNote::new( + self.metadata, + self.recipient_digest, + assets, + self.attachments, + ); RawOutputNote::Partial(note) }, } diff --git a/crates/miden-tx/src/host/tx_event.rs b/crates/miden-tx/src/host/tx_event.rs index 93aab405c2..fe07c1e2d2 100644 --- a/crates/miden-tx/src/host/tx_event.rs +++ b/crates/miden-tx/src/host/tx_event.rs @@ -13,9 +13,6 @@ use miden_protocol::account::{ use miden_protocol::asset::{Asset, AssetVault, AssetVaultKey, FungibleAsset}; use miden_protocol::note::{ NoteAttachment, - NoteAttachmentArray, - NoteAttachmentContent, - NoteAttachmentKind, NoteAttachmentScheme, NoteId, NoteMetadata, @@ -28,7 +25,7 @@ use miden_protocol::note::{ use miden_protocol::transaction::memory::{NOTE_MEM_SIZE, OUTPUT_NOTE_SECTION_OFFSET}; use miden_protocol::transaction::{TransactionEventId, TransactionSummary}; use miden_protocol::vm::EventId; -use miden_protocol::{Felt, Hasher, Word}; +use miden_protocol::{Felt, Hasher, WORD_SIZE, Word}; use crate::host::{TransactionBaseHost, TransactionKernelProcess}; use crate::{LinkMap, TransactionKernelError}; @@ -135,10 +132,10 @@ pub(crate) enum TransactionEvent { asset: Asset, }, - NoteBeforeSetAttachment { - /// The note index on which the attachment is set. + NoteBeforeAddAttachment { + /// The note index to which the attachment is appended. note_idx: usize, - /// The attachment that is set. + /// The attachment that is appended to the output note. attachment: NoteAttachment, }, @@ -425,26 +422,23 @@ impl TransactionEvent { TransactionEventId::NoteAfterAddAsset => None, - TransactionEventId::NoteBeforeSetAttachment => { + TransactionEventId::NoteBeforeAddAttachment => { // Expected stack state: [ - // event, attachment_scheme, attachment_kind, - // note_ptr, note_ptr, ATTACHMENT + // event, num_attachments, note_ptr, attachment_scheme, ATTACHMENT_COMMITMENT // ] - let attachment_scheme = process.get_stack_item(1); - let attachment_kind = process.get_stack_item(2); - let note_ptr = process.get_stack_item(3); - let attachment = process.get_stack_word(5); + let note_ptr = process.get_stack_item(2); + let attachment_scheme = process.get_stack_item(3); + let attachment_commitment = process.get_stack_word(4); let (note_idx, attachment) = extract_note_attachment( attachment_scheme, - attachment_kind, - attachment, + attachment_commitment, note_ptr, process.advice_provider(), )?; - Some(TransactionEvent::NoteBeforeSetAttachment { note_idx, attachment }) + Some(TransactionEvent::NoteBeforeAddAttachment { note_idx, attachment }) }, TransactionEventId::AuthRequest => { @@ -737,66 +731,67 @@ fn build_note_metadata( fn extract_note_attachment( attachment_scheme: Felt, - attachment_kind: Felt, - attachment: Word, + attachment_commitment: Word, note_ptr: Felt, advice_provider: &AdviceProvider, ) -> Result<(usize, NoteAttachment), TransactionKernelError> { let note_idx = note_ptr_to_idx(note_ptr)?; - let attachment_kind = u8::try_from(attachment_kind.as_canonical_u64()) - .map_err(|_| TransactionKernelError::other("failed to convert attachment kind to u8")) - .and_then(|attachment_kind| { - NoteAttachmentKind::try_from(attachment_kind).map_err(|source| { + let attachment_scheme = u16::try_from(attachment_scheme.as_canonical_u64()) + .map_err(|_| TransactionKernelError::other("failed to convert attachment scheme to u16")) + .and_then(|scheme| { + NoteAttachmentScheme::try_from(scheme).map_err(|source| { TransactionKernelError::other_with_source( - "failed to convert u8 to attachment kind", + "failed to convert u16 to attachment scheme", source, ) }) })?; - let attachment_scheme = u32::try_from(attachment_scheme.as_canonical_u64()) - .map_err(|_| TransactionKernelError::other("failed to convert attachment scheme to u32")) - .map(NoteAttachmentScheme::new)?; - - let attachment_content = match attachment_kind { - NoteAttachmentKind::None => { - if !attachment.is_empty() { - return Err(TransactionKernelError::NoteAttachmentNoneIsNotEmpty); - } - NoteAttachmentContent::None - }, - NoteAttachmentKind::Word => NoteAttachmentContent::Word(attachment), - NoteAttachmentKind::Array => { - let elements = advice_provider.get_mapped_values(&attachment).ok_or_else(|| { - TransactionKernelError::other( - "elements of a note attachment commitment must be present in the advice provider", - ) - })?; - - let commitment_attachment = - NoteAttachmentArray::new(elements.to_vec()).map_err(|source| { - TransactionKernelError::other_with_source( - "failed to construct note attachment commitment", - source, - ) - })?; + // Fetch the raw elements from the advice provider. + let elements = advice_provider.get_mapped_values(&attachment_commitment).ok_or_else(|| { + TransactionKernelError::other( + "elements of a note attachment commitment must be present in the advice provider", + ) + })?; - if commitment_attachment.commitment() != attachment { - return Err(TransactionKernelError::NoteAttachmentArrayMismatch { - actual: commitment_attachment.commitment(), - provided: attachment, - }); - } + if elements.is_empty() { + return Err(TransactionKernelError::other( + "num elements in attachment advice map value must not be empty", + )); + } + + if !elements.len().is_multiple_of(WORD_SIZE) { + return Err(TransactionKernelError::other( + "num elements in attachment advice map value must be multiple of word size", + )); + } - NoteAttachmentContent::Array(commitment_attachment) - }, + let words: Vec = elements + .chunks_exact(WORD_SIZE) + .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 attachment = - NoteAttachment::new(attachment_scheme, attachment_content).map_err(|source| { - TransactionKernelError::other_with_source("failed to extract note attachment", source) - })?; + let actual_commitment = attachment.to_commitment(); + + // Check the actual commitment of the advice data matches the declared commitment. + if actual_commitment != attachment_commitment { + return Err(TransactionKernelError::NoteAttachmentCommitmentMismatch { + actual: actual_commitment, + provided: attachment_commitment, + }); + } Ok((note_idx as usize, attachment)) } diff --git a/crates/miden-tx/src/prover/prover_host.rs b/crates/miden-tx/src/prover/prover_host.rs index b6b4156678..b024ca3605 100644 --- a/crates/miden-tx/src/prover/prover_host.rs +++ b/crates/miden-tx/src/prover/prover_host.rs @@ -170,9 +170,9 @@ where self.base_host.on_note_before_add_asset(note_idx, asset).map(|_| Vec::new()) }, - TransactionEvent::NoteBeforeSetAttachment { note_idx, attachment } => self + TransactionEvent::NoteBeforeAddAttachment { note_idx, attachment } => self .base_host - .on_note_before_set_attachment(note_idx, attachment) + .on_note_before_add_attachment(note_idx, attachment) .map(|_| Vec::new()), TransactionEvent::AuthRequest { signature, .. } => {