From 33c13eff8da1a326bd4f5e8e37c0db635ea6abf9 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Wed, 15 Apr 2026 15:38:28 +0200 Subject: [PATCH 01/48] feat: Prepare note structs for multiple note attachments --- crates/miden-protocol/src/errors/mod.rs | 27 +- crates/miden-protocol/src/note/attachment.rs | 669 +++++++++++++++---- crates/miden-protocol/src/note/file.rs | 1 + crates/miden-protocol/src/note/header.rs | 39 +- crates/miden-protocol/src/note/metadata.rs | 517 +++++++------- crates/miden-protocol/src/note/mod.rs | 52 +- crates/miden-protocol/src/note/partial.rs | 49 +- 7 files changed, 877 insertions(+), 477 deletions(-) diff --git a/crates/miden-protocol/src/errors/mod.rs b/crates/miden-protocol/src/errors/mod.rs index b13ec8d068..fca38fec3c 100644 --- a/crates/miden-protocol/src/errors/mod.rs +++ b/crates/miden-protocol/src/errors/mod.rs @@ -32,13 +32,7 @@ use crate::asset::AssetId; use crate::batch::BatchId; use crate::block::BlockNumber; use crate::note::{ - NoteAssets, - NoteAttachmentArray, - NoteAttachmentKind, - NoteAttachmentScheme, - NoteTag, - NoteType, - Nullifier, + NoteAssets, NoteAttachmentArray, NoteAttachmentKind, NoteAttachmentScheme, NoteAttachments, NoteTag, NoteType, Nullifier }; use crate::transaction::TransactionId; use crate::utils::serde::DeserializationError; @@ -669,20 +663,15 @@ pub enum NoteError { UnknownNoteAttachmentKind(u8), #[error("note attachment of kind None must have attachment scheme None")] AttachmentKindNoneMustHaveAttachmentSchemeNone, + #[error("{0} attachments were provided but maximum is {max}", max = NoteAttachments::MAX_COUNT)] + TooManyAttachments(usize), #[error( - "note attachment kind mismatch: header has {header_kind:?} but attachment has {attachment_kind:?}" + "total attachment elements {0} exceeds maximum of {max}", + max = NoteAttachments::MAX_NUM_WORDS )] - AttachmentKindMismatch { - header_kind: NoteAttachmentKind, - attachment_kind: NoteAttachmentKind, - }, - #[error( - "note attachment scheme mismatch: header has {header_scheme:?} but attachment has {attachment_scheme:?}" - )] - AttachmentSchemeMismatch { - header_scheme: NoteAttachmentScheme, - attachment_scheme: NoteAttachmentScheme, - }, + TooManyAttachmentElements(usize), + #[error("attachment scheme {0} exceeds maximum value of {max}", max = NoteAttachmentScheme::MAX)] + NoteAttachmentSchemeExceeded(u32), #[error("{error_msg}")] Other { error_msg: Box, diff --git a/crates/miden-protocol/src/note/attachment.rs b/crates/miden-protocol/src/note/attachment.rs index fa8e567341..9903d01c4f 100644 --- a/crates/miden-protocol/src/note/attachment.rs +++ b/crates/miden-protocol/src/note/attachment.rs @@ -1,6 +1,8 @@ use alloc::string::ToString; use alloc::vec::Vec; +use miden_core::WORD_SIZE; + use crate::crypto::SequentialCommit; use crate::errors::NoteError; use crate::utils::serde::{ @@ -40,7 +42,7 @@ 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, @@ -50,22 +52,9 @@ impl NoteAttachment { // CONSTRUCTORS // -------------------------------------------------------------------------------------------- - /// Creates a new [`NoteAttachment`] from a user-defined type and the provided content. - /// - /// # 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); - } - - Ok(Self { attachment_scheme, content }) + /// 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 @@ -83,7 +72,8 @@ impl NoteAttachment { /// # Errors /// /// Returns an error if: - /// - The maximum number of elements exceeds [`NoteAttachmentArray::MAX_NUM_ELEMENTS`]. + /// - The number of elements exceeds [`NoteAttachmentArray::MAX_NUM_ELEMENTS`]. + /// - The number of elements is less than [`NoteAttachmentArray::MIN_NUM_ELEMENTS`]. pub fn new_array( attachment_scheme: NoteAttachmentScheme, elements: Vec, @@ -100,15 +90,18 @@ 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 } + + /// 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) -> u8 { + self.content.num_words() + } } impl Serializable for NoteAttachment { @@ -127,28 +120,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), @@ -175,7 +162,8 @@ impl NoteAttachmentContent { /// # Errors /// /// Returns an error if: - /// - The maximum number of elements exceeds [`NoteAttachmentArray::MAX_NUM_ELEMENTS`]. + /// - The number of elements exceeds [`NoteAttachmentArray::MAX_NUM_ELEMENTS`]. + /// - The number of elements is less than [`NoteAttachmentArray::MIN_NUM_ELEMENTS`]. pub fn new_array(elements: Vec) -> Result { NoteAttachmentArray::new(elements).map(Self::from) } @@ -183,12 +171,24 @@ impl NoteAttachmentContent { // ACCESSORS // -------------------------------------------------------------------------------------------- - /// Returns the [`NoteAttachmentKind`]. - pub fn attachment_kind(&self) -> NoteAttachmentKind { + /// 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 size of this attachment content in words. + /// + /// - `1` for [`NoteAttachmentContent::Word`]. + /// - `> 1` for [`NoteAttachmentContent::Array`]. + pub fn word_size(&self) -> u8 { match self { - NoteAttachmentContent::None => NoteAttachmentKind::None, - NoteAttachmentContent::Word(_) => NoteAttachmentKind::Word, - NoteAttachmentContent::Array(_) => NoteAttachmentKind::Array, + NoteAttachmentContent::Word(_) => 1, + NoteAttachmentContent::Array(array) => array.word_size(), } } @@ -197,7 +197,6 @@ impl NoteAttachmentContent { /// See the type-level documentation for more details. pub fn to_word(&self) -> Word { match self { - NoteAttachmentContent::None => Word::empty(), NoteAttachmentContent::Word(word) => *word, NoteAttachmentContent::Array(attachment_commitment) => { attachment_commitment.commitment() @@ -208,29 +207,28 @@ impl NoteAttachmentContent { impl Serializable for NoteAttachmentContent { fn write_into(&self, target: &mut W) { - self.attachment_kind().write_into(target); + // Write word_size as discriminant: 1 = Word, >1 = Array. + self.word_size().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(arr) => { + arr.num_elements().write_into(target); + target.write_many(&arr.elements); }, } } 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 + + array.num_elements().get_size_hint() + + array.elements.len() * Felt::ZERO.get_size_hint() }, } } @@ -238,18 +236,20 @@ impl Serializable for NoteAttachmentContent { impl Deserializable for NoteAttachmentContent { fn read_from(source: &mut R) -> Result { - let attachment_kind = NoteAttachmentKind::read_from(source)?; + let word_size = u8::read_from(source)?; - match attachment_kind { - NoteAttachmentKind::None => Ok(NoteAttachmentContent::None), - NoteAttachmentKind::Word => { + match word_size { + 0 => Err(DeserializationError::InvalidValue( + "attachment content word_size 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::>()?; + source.read_many_iter(usize::from(num_elements))?.collect::>()?; Self::new_array(elements) .map_err(|err| DeserializationError::InvalidValue(err.to_string())) }, @@ -257,11 +257,14 @@ impl Deserializable for NoteAttachmentContent { } } -// NOTE ATTACHMENT COMMITMENT +// NOTE ATTACHMENT ARRAY // ================================================================================================ /// The type contained in [`NoteAttachmentContent::Array`] that commits to a set of field /// elements. +/// +/// The number of elements must be divisible by [`WORD_SIZE`], i.e. the array must contain only +/// whole words. #[derive(Debug, Clone, PartialEq, Eq)] pub struct NoteAttachmentArray { elements: Vec, @@ -272,11 +275,17 @@ impl NoteAttachmentArray { // CONSTANTS // -------------------------------------------------------------------------------------------- - /// The maximum size of a note attachment that commits to a set of elements. + /// The minimum number of elements 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 (8 elements) to distinguish them from word + /// attachments. + pub const MIN_NUM_ELEMENTS: u8 = (WORD_SIZE as u8) * 2; + + /// The maximum number of elements in a note attachment array. + /// + /// Each attachment can be at most [`NoteAttachmentHeader::MAX_SIZE`] words (254), and each + /// word holds 4 elements, so the maximum number of elements is 254 * 4 = 1016. + pub const MAX_NUM_ELEMENTS: u16 = NoteAttachmentHeader::MAX_SIZE as u16 * (WORD_SIZE as u16); // CONSTRUCTORS // -------------------------------------------------------------------------------------------- @@ -286,8 +295,25 @@ impl NoteAttachmentArray { /// # Errors /// /// Returns an error if: - /// - The maximum number of elements exceeds [`NoteAttachmentArray::MAX_NUM_ELEMENTS`]. + /// - The number of elements is not a multiple of [`WORD_SIZE`]. + /// - The number of elements is less than [`Self::MIN_NUM_ELEMENTS`]. + /// - The number of elements exceeds [`Self::MAX_NUM_ELEMENTS`]. pub fn new(elements: Vec) -> Result { + if !elements.len().is_multiple_of(WORD_SIZE) { + return Err(NoteError::other(format!( + "note attachment array length must be divisible by 4, got {}", + elements.len() + ))); + } + + if elements.len() < Self::MIN_NUM_ELEMENTS as usize { + return Err(NoteError::other(format!( + "note attachment array must have at least {} elements, got {}", + Self::MIN_NUM_ELEMENTS, + elements.len() + ))); + } + if elements.len() > Self::MAX_NUM_ELEMENTS as usize { return Err(NoteError::NoteAttachmentArraySizeExceeded(elements.len())); } @@ -309,6 +335,15 @@ impl NoteAttachmentArray { u16::try_from(self.elements.len()).expect("type should enforce that size fits in u16") } + /// Returns the number of elements this note attachment commits to. + pub fn word_size(&self) -> u8 { + // SAFETY: + // - num elements is at most 1016 and 1016/4 = 254, so it fits in a u8 + // - constructor checks that num elements is a multiple of WORD_SIZE, so we don't need to + // check the remainder + u8::try_from(self.elements.len() / WORD_SIZE).expect("word size shoult fit in u8") + } + /// Returns the commitment over the contained field elements. pub fn commitment(&self) -> Word { self.commitment @@ -338,27 +373,41 @@ impl From for NoteAttachmentContent { /// The user-defined type 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`]). /// /// 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. #[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; + const NONE: u16 = 0; + + /// 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 { - Self(attachment_scheme) + /// Creates a new [`NoteAttachmentScheme`] from a `u16`. + /// + /// # Errors + /// + /// Returns an error if `attachment_scheme` exceeds [`Self::MAX`]. + pub fn new(attachment_scheme: u16) -> Result { + if attachment_scheme > Self::MAX.as_u16() { + return Err(NoteError::NoteAttachmentSchemeExceeded(attachment_scheme as u32)); + } + Ok(Self(attachment_scheme)) } /// Returns the [`NoteAttachmentScheme`] that signals the absence of an attachment scheme. @@ -375,12 +424,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 +453,317 @@ 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 and word size of an attachment, without the actual content data. +/// The kind of attachment is inferred from the size: +/// - `size == 0`: absent (no attachment) +/// - `size == 1`: word attachment (a single [`Word`]) +/// - `size > 1`: array attachment (a commitment to a set of felts) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct NoteAttachmentHeader { + scheme: NoteAttachmentScheme, + word_size: u8, +} - /// A note attachment consisting of a single [`Word`]. - Word = Self::WORD, +impl NoteAttachmentHeader { + // CONSTANTS + // -------------------------------------------------------------------------------------------- - /// A note attachment consisting of the commitment to a set of felts. - Array = Self::ARRAY, + /// The maximum attachment size in words. + /// + /// Limited to 254 to ensure the size fits into a u8 and the felt encoding remains valid + /// when four sizes are packed into a single felt in the note metadata. + pub const MAX_SIZE: u8 = 254; + + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates a new [`NoteAttachmentHeader`]. + /// + /// # Errors + /// + /// Returns an error if `size` exceeds [`Self::MAX_SIZE`]. + pub fn new(scheme: NoteAttachmentScheme, word_size: u8) -> Result { + if word_size > Self::MAX_SIZE { + return Err(NoteError::other(alloc::format!( + "attachment size {word_size} exceeds maximum {}", + Self::MAX_SIZE + ))); + } + Ok(Self { scheme, word_size }) + } + + /// Returns a header representing the absence of an attachment. + pub const fn absent() -> Self { + Self { + scheme: NoteAttachmentScheme::none(), + word_size: 0, + } + } + + // ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the attachment scheme. + pub const fn scheme(&self) -> NoteAttachmentScheme { + self.scheme + } + + /// Returns the attachment size in words. + pub const fn word_size(&self) -> u8 { + self.word_size + } + + /// Returns `true` if this header represents an absent attachment, `false` otherwise. + pub const fn is_absent(&self) -> bool { + self.word_size == 0 && self.scheme.is_none() + } } -impl NoteAttachmentKind { +impl Default for NoteAttachmentHeader { + fn default() -> Self { + Self::absent() + } +} + +impl Serializable for NoteAttachmentHeader { + fn write_into(&self, target: &mut W) { + self.scheme.write_into(target); + self.word_size.write_into(target); + } + + fn get_size_hint(&self) -> usize { + self.scheme.get_size_hint() + core::mem::size_of::() + } +} + +impl Deserializable for NoteAttachmentHeader { + fn read_from(source: &mut R) -> Result { + let scheme = NoteAttachmentScheme::read_from(source)?; + let size = u8::read_from(source)?; + Self::new(scheme, size).map_err(|err| DeserializationError::InvalidValue(err.to_string())) + } +} + +// 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: sequential hash over all attachment words +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NoteAttachments { + attachments: Vec, + commitment: Word, +} + +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(), + commitment: Word::empty(), + } + } + + /// 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.word_size() as usize) + .sum::(); + + if total_num_words > Self::MAX_NUM_WORDS as usize { + return Err(NoteError::TooManyAttachmentElements(total_num_words)); + } + + let commitment = compute_commitment(&attachments); + + Ok(Self { attachments, commitment }) + } // 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 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 there are no attachments. + pub fn is_empty(&self) -> bool { + self.attachments.is_empty() + } + + /// Returns an iterator over the attachments. + pub fn iter(&self) -> impl Iterator { + self.attachments.iter() + } + + /// Returns the cached commitment over the contained attachments. + pub fn commitment(&self) -> Word { + self.commitment } - /// Returns `true` if the attachment kind is `Word`, `false` otherwise. - pub const fn is_word(&self) -> bool { - matches!(self, Self::Word) + /// 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(), attachment.word_size()) + .expect( + "attachment word_size should not exceed NoteAttachmentHeader::MAX_SIZE", + ); + } + headers } - /// Returns `true` if the attachment kind is `Array`, `false` otherwise. - pub const fn is_array(&self) -> bool { - matches!(self, Self::Array) + // CONVERSIONS + // -------------------------------------------------------------------------------------------- + + /// Consumes self and returns the inner vector of attachments. + pub fn into_vec(self) -> Vec { + self.attachments } } -impl TryFrom for NoteAttachmentKind { - type Error = NoteError; +impl Default for NoteAttachments { + fn default() -> Self { + Self::empty() + } +} + +impl SequentialCommit for NoteAttachments { + type Commitment = Word; + + fn to_elements(&self) -> Vec { + attachments_to_elements(&self.attachments) + } + + fn to_commitment(&self) -> Self::Commitment { + self.commitment + } +} - 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)), +/// Collects all attachment data into a flat vector of field elements. +fn attachments_to_elements(attachments: &[NoteAttachment]) -> Vec { + let mut elements = Vec::new(); + for attachment in attachments { + match attachment.content() { + NoteAttachmentContent::Word(word) => { + elements.extend_from_slice(word.as_elements()); + }, + NoteAttachmentContent::Array(arr) => { + elements.extend_from_slice(arr.as_slice()); + }, } } + elements } -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", - }; +/// Collects all attachment words into a flat vector of field elements. +/// +/// Each attachment contributes exactly one word (4 felts): the raw content for word attachments, +/// or the commitment for array attachments. +fn attachments_to_words(attachments: &[NoteAttachment]) -> Vec { + let mut elements = Vec::with_capacity(attachments.len() * WORD_SIZE); + for attachment in attachments { + elements.extend_from_slice(attachment.content().to_word().as_elements()); + } + elements +} - f.write_str(output) +/// Computes the commitment over a slice of attachments. +fn compute_commitment(attachments: &[NoteAttachment]) -> Word { + if attachments.is_empty() { + Word::empty() + } else { + Hasher::hash_elements(&attachments_to_words(attachments)) } } -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 +777,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![Felt::new(1); 8], )?)] #[test] fn note_attachment_serde(#[case] attachment: NoteAttachment) -> anyhow::Result<()> { @@ -529,8 +789,8 @@ 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; + fn note_attachment_array_fails_on_too_many_elements() -> anyhow::Result<()> { + let too_many_elements = (NoteAttachmentArray::MAX_NUM_ELEMENTS as usize) + 4; let elements = vec![Felt::from(1u32); too_many_elements]; let err = NoteAttachmentArray::new(elements).unwrap_err(); @@ -542,9 +802,140 @@ mod tests { } #[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_elements() { + let elements = vec![Felt::from(1u32); 4]; + let err = NoteAttachmentArray::new(elements).unwrap_err(); + // Arrays must have at least MIN_NUM_ELEMENTS (8) to distinguish from word attachments. + assert!(err.to_string().contains("at least")); + } + + #[test] + fn note_attachment_array_fails_on_non_word_aligned_length() { + let elements = vec![Felt::from(1u32); 9]; + let err = NoteAttachmentArray::new(elements).unwrap_err(); + assert!(err.to_string().contains("divisible by 4")); + } + + #[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)?, 10)?; + 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_eq!(header.word_size(), 0); + 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![Felt::new(1); 8])?, + ])?; + + let deserialized = NoteAttachments::read_from_bytes(&attachments.to_bytes())?; + assert_eq!(attachments, deserialized); + + Ok(()) + } + + #[test] + fn note_attachments_commitment_empty() { + let attachments = NoteAttachments::empty(); + assert_eq!(attachments.commitment(), Word::empty()); + } + + #[test] + fn note_attachments_commitment_single_word() -> anyhow::Result<()> { + let word = Word::from([10, 20, 30, 40u32]); + let attachments = NoteAttachments::new(vec![NoteAttachment::new_word( + NoteAttachmentScheme::new(1)?, + word, + )])?; + // Single word attachment: commitment is the hash of the word. + assert_eq!(attachments.commitment(), Hasher::hash_elements(word.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![Felt::new(1); 8])?, + ])?; + + let headers = attachments.to_headers(); + assert_eq!(headers[0].scheme(), NoteAttachmentScheme::new(42)?); + assert_eq!(headers[0].word_size(), 1); + assert_eq!(headers[1].scheme(), NoteAttachmentScheme::new(100)?); + assert_eq!(headers[1].word_size(), 2); // 8 felts = 2 words + 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_word_size() { + // Word => 1 + let word = NoteAttachmentContent::new_word(Word::from([1, 2, 3, 4u32])); + assert_eq!(word.word_size(), 1); + + // Array with 8 elements => 8/4 = 2 + let array = NoteAttachmentContent::new_array(vec![Felt::new(1); 8]).unwrap(); + assert_eq!(array.word_size(), 2); + + // Array with 12 elements => 12/4 = 3 + let array = NoteAttachmentContent::new_array(vec![Felt::new(1); 12]).unwrap(); + assert_eq!(array.word_size(), 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 535ff99e4d..aad001f5ca 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, NoteAttachmentScheme, 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 0 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_0: u8 = 0; - // CONSTRUCTORS // -------------------------------------------------------------------------------------------- @@ -99,40 +45,7 @@ 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,104 +110,182 @@ 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. +/// +/// 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: [att_3_size (8b) | att_2_size (8b) | att_1_size (8b) | att_0_size (8b) | note_tag (32b)] +/// 3rd felt: [att_3_scheme (16b) | att_2_scheme (16b) | att_1_scheme (16b) | att_0_scheme (16b)] +/// ``` +/// +/// 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: Max value is `0xFEFEFEFE_FFFFFFFF` (sizes capped at 254, tag at u32::MAX), which is +/// less than the Goldilocks prime `p = 2^64 - 2^32 + 1`. +/// - 3rd felt: Max value is `0xFFFEFFFE_FFFEFFFE` (schemes capped at 65534), which is less than +/// `p`. /// -/// See the metadata's type for details on this type's [`Word`] layout. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +/// 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 0 of the note metadata encoding. + const VERSION_0: 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.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 attachment kind. - pub fn attachment_kind(&self) -> NoteAttachmentKind { - self.attachment_kind + /// Returns the metadata encoded as a [`Word`]. + /// + /// See [`NoteMetadataHeader`] docs for the layout. + pub fn to_metadata_word(&self) -> Word { + let (sizes, schemes) = extract_sizes_and_schemes(&self.attachment_headers); + + 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] = merge_tag_and_sizes(self.metadata.tag, sizes); + word[3] = merge_schemes(schemes); + word } - /// Returns the attachment scheme. - pub fn attachment_scheme(&self) -> NoteAttachmentScheme { - self.attachment_scheme + /// 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]) } -} -impl From for Word { - fn from(header: NoteMetadataHeader) -> Self { - let mut metadata = Word::empty(); + /// Consumes self and returns the inner [`NoteMetadata`]. + pub fn into_metadata(self) -> NoteMetadata { + self.metadata + } +} - 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); +impl Serializable for NoteMetadataHeader { + fn write_into(&self, target: &mut W) { + self.metadata.write_into(target); + target.write_many(self.attachment_headers); + self.attachments_commitment.write_into(target); + } - metadata + fn get_size_hint(&self) -> usize { + self.metadata.get_size_hint() + + self + .attachment_headers + .iter() + .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 attachment_headers = [ + NoteAttachmentHeader::read_from(source)?, + NoteAttachmentHeader::read_from(source)?, + NoteAttachmentHeader::read_from(source)?, + NoteAttachmentHeader::read_from(source)?, + ]; + let attachment_commitment = Word::read_from(source)?; + + Ok(Self::from_parts(metadata, attachment_headers, attachment_commitment)) } } // HELPER FUNCTIONS // ================================================================================================ +/// Extracts the sizes and schemes arrays from the attachment headers. +fn extract_sizes_and_schemes( + headers: &[NoteAttachmentHeader; NoteAttachments::MAX_COUNT], +) -> ( + [u8; NoteAttachments::MAX_COUNT], + [NoteAttachmentScheme; NoteAttachments::MAX_COUNT], +) { + let mut word_sizes = [0u8; NoteAttachments::MAX_COUNT]; + let mut schemes = [NoteAttachmentScheme::none(); NoteAttachments::MAX_COUNT]; + for (i, header) in headers.iter().enumerate() { + word_sizes[i] = header.word_size(); + schemes[i] = header.scheme(); + } + (word_sizes, schemes) +} + /// Merges the suffix of an [`AccountId`] and note metadata into a single [`Felt`]. /// /// The layout is as follows: @@ -368,82 +303,50 @@ 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_0_NUMBER) - merged |= (note_type_byte as u64) << NOTE_TYPE_SHIFT; - merged |= NoteMetadata::VERSION_0 as u64; + // note_type at bit 4, version at bits 0..=3 (hardcoded to NoteMetadataHeader::VERSION_0) + merged |= (note_type_byte as u64) << NoteMetadataHeader::NOTE_TYPE_SHIFT; + merged |= NoteMetadataHeader::VERSION_0 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_0 { - return Err(NoteError::other(format!( - "unsupported note metadata version {version}, expected {}", - NoteMetadata::VERSION_0 - ))); - } - - 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 the note tag and four attachment sizes into a single [`Felt`]. /// /// The layout is as follows: /// /// ```text -/// [30 zero bits | attachment_kind (2 bits) | attachment_scheme (32 bits)] +/// [att_3_size (8b) | att_2_size (8b) | att_1_size (8b) | att_0_size (8b) | note_tag (32b)] /// ``` -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") +/// +/// Max value: `0xFEFEFEFE_FFFFFFFF` < p (Goldilocks prime). Sizes are capped at 254. +fn merge_tag_and_sizes(tag: NoteTag, sizes: [u8; 4]) -> Felt { + let mut merged: u64 = u32::from(tag) as u64; + merged |= (sizes[0] as u64) << 32; + merged |= (sizes[1] as u64) << 40; + merged |= (sizes[2] as u64) << 48; + merged |= (sizes[3] as u64) << 56; + + Felt::try_from(merged).expect("encoded value should be a valid felt (sizes <= 254)") } -/// 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)) +/// Merges four attachment schemes into a single [`Felt`]. +/// +/// The layout is as follows: +/// +/// ```text +/// [att_3_scheme (16b) | att_2_scheme (16b) | att_1_scheme (16b) | att_0_scheme (16b)] +/// ``` +/// +/// Max value: `0xFFFEFFFE_FFFEFFFE` < p. Schemes are capped at 65534. +fn merge_schemes(schemes: [NoteAttachmentScheme; 4]) -> Felt { + let mut merged: u64 = schemes[0].as_u16() as u64; + merged |= (schemes[1].as_u16() as u64) << 16; + merged |= (schemes[2].as_u16() as u64) << 32; + merged |= (schemes[3].as_u16() as u64) << 48; + + Felt::try_from(merged).expect("encoded value should be a valid felt (schemes <= 65534)") } // TESTS @@ -453,33 +356,75 @@ 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 num_words_and_tag = encoded[2].as_canonical_u64(); + // num_words 3 and 4 are 0, 2 encodes to 0x2 and 1 encodes to 0x1 + // tag should encode to 0xff + assert_eq!(num_words_and_tag, 0x0000_0201_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, + [Felt::from_u8(5); NoteAttachmentArray::MIN_NUM_ELEMENTS as usize].to_vec(), + )?, + NoteAttachment::new_array( + NoteAttachmentScheme::MAX, + [Felt::from_u8(10); NoteAttachmentArray::MAX_NUM_ELEMENTS as usize].to_vec(), + )?, + ])] #[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 ab0c9eb3f0..43011b0fa9 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; @@ -90,6 +91,7 @@ pub use file::NoteFile; pub struct Note { header: NoteHeader, details: NoteDetails, + attachments: NoteAttachments, nullifier: Nullifier, } @@ -98,13 +100,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 +172,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 +201,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 +241,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 +259,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 +268,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 +282,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)) } } From b114c18d1431258abb2d4c621aed3698b8c7c700 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Wed, 15 Apr 2026 15:51:55 +0200 Subject: [PATCH 02/48] chore: update block and batch note trees --- crates/miden-protocol/src/batch/note_tree.rs | 6 +++--- crates/miden-protocol/src/block/block_body.rs | 2 +- crates/miden-protocol/src/block/note_tree.rs | 13 ++++++++----- crates/miden-protocol/src/block/proposed_block.rs | 2 +- .../miden-protocol/src/testing/block_note_tree.rs | 2 +- crates/miden-protocol/src/testing/note.rs | 3 ++- 6 files changed, 16 insertions(+), 12 deletions(-) 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/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/testing/note.rs b/crates/miden-protocol/src/testing/note.rs index 913fd0f7ee..685386f3b6 100644 --- a/crates/miden-protocol/src/testing/note.rs +++ b/crates/miden-protocol/src/testing/note.rs @@ -6,6 +6,7 @@ use crate::asset::FungibleAsset; use crate::note::{ Note, NoteAssets, + NoteAttachments, NoteMetadata, NoteRecipient, NoteScript, @@ -29,7 +30,7 @@ impl Note { let inputs = NoteStorage::new(Vec::new()).unwrap(); let recipient = NoteRecipient::new(serial_num, note_script, inputs); - Note::new(assets, metadata, recipient) + Note::new(assets, metadata, recipient, NoteAttachments::default()) } } From 4b42abc50f1bd5762e64d9bf57f8f40bf128d8c9 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Thu, 16 Apr 2026 12:26:14 +0200 Subject: [PATCH 03/48] feat: extend private note header with attachments and rename --- crates/miden-protocol/src/transaction/mod.rs | 2 +- .../src/transaction/outputs/mod.rs | 2 +- .../src/transaction/outputs/notes.rs | 102 +++++++++++------- .../src/transaction/outputs/tests.rs | 5 +- 4 files changed, 69 insertions(+), 42 deletions(-) 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 a784237690..f49700d9c3 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..874b61e279 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.as_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 + &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/outputs/tests.rs b/crates/miden-protocol/src/transaction/outputs/tests.rs index 7e5834f5f6..0e203020e5 100644 --- a/crates/miden-protocol/src/transaction/outputs/tests.rs +++ b/crates/miden-protocol/src/transaction/outputs/tests.rs @@ -11,6 +11,7 @@ use crate::errors::{OutputNoteError, TransactionOutputError}; use crate::note::{ Note, NoteAssets, + NoteAttachments, NoteMetadata, NoteRecipient, NoteScript, @@ -67,7 +68,7 @@ fn output_note_size_hint_matches_serialized_length() -> anyhow::Result<()> { let script = NoteScript::mock(); let recipient = NoteRecipient::new(serial_num, script, storage); - let note = Note::new(assets, metadata, recipient); + let note = Note::new(assets, metadata, recipient, NoteAttachments::default()); let output_note = RawOutputNote::Full(note); let bytes = output_note.to_bytes(); @@ -112,7 +113,7 @@ fn oversized_public_note_triggers_size_limit_error() -> anyhow::Result<()> { .with_tag(NoteTag::with_account_target(sender_id)); let recipient = NoteRecipient::new(serial_num, script, storage); - let oversized_note = Note::new(assets, metadata, recipient); + let oversized_note = Note::new(assets, metadata, recipient, NoteAttachments::default()); // Sanity-check that our constructed note is indeed larger than the configured // maximum. From deaff66cc06641b02455e0909e42484e8ca965f2 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Thu, 16 Apr 2026 14:48:02 +0200 Subject: [PATCH 04/48] chore: update note kernel memory layout --- crates/miden-protocol/src/errors/mod.rs | 21 +++++------ .../src/transaction/kernel/advice_inputs.rs | 25 +++++++------ .../src/transaction/kernel/memory.rs | 36 ++++++++++--------- .../src/transaction/kernel/tx_event_id.rs | 6 ++-- .../src/transaction/proven_tx.rs | 2 +- 5 files changed, 50 insertions(+), 40 deletions(-) diff --git a/crates/miden-protocol/src/errors/mod.rs b/crates/miden-protocol/src/errors/mod.rs index fca38fec3c..98090b814e 100644 --- a/crates/miden-protocol/src/errors/mod.rs +++ b/crates/miden-protocol/src/errors/mod.rs @@ -32,7 +32,17 @@ use crate::asset::AssetId; use crate::batch::BatchId; use crate::block::BlockNumber; use crate::note::{ +<<<<<<< HEAD NoteAssets, NoteAttachmentArray, NoteAttachmentKind, NoteAttachmentScheme, NoteAttachments, NoteTag, NoteType, Nullifier +======= + NoteAssets, + NoteAttachmentArray, + NoteAttachmentScheme, + NoteAttachments, + NoteTag, + NoteType, + Nullifier, +>>>>>>> 3b6219c1 (chore: update note kernel memory layout) }; use crate::transaction::TransactionId; use crate::utils::serde::DeserializationError; @@ -659,17 +669,8 @@ pub enum NoteError { 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("{0} attachments were provided but maximum is {max}", max = NoteAttachments::MAX_COUNT)] TooManyAttachments(usize), - #[error( - "total attachment elements {0} exceeds maximum of {max}", - max = NoteAttachments::MAX_NUM_WORDS - )] - TooManyAttachmentElements(usize), #[error("attachment scheme {0} exceeds maximum value of {max}", max = NoteAttachmentScheme::MAX)] NoteAttachmentSchemeExceeded(u32), #[error("{error_msg}")] @@ -846,7 +847,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/transaction/kernel/advice_inputs.rs b/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs index 27d29f054a..2605b0741b 100644 --- a/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs +++ b/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs @@ -339,14 +339,19 @@ 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_COMMITMENTS |-> [ATTACHMENT_ELEMENTS] + self.add_map_entry(note.attachments().commitment(), note.attachments().to_elements()); + + // elements of each array attachment + for attachment in note.attachments().iter() { + // ARRAY_ATTACHMENT_COMMITMENT |-> [ARRAY_ATTACHMENT_ELEMENTS] + if let NoteAttachmentContent::Array(array_attachment) = attachment.content() { + self.add_map_entry( + array_attachment.commitment(), + array_attachment.as_slice().to_vec(), + ); + } } // note details / metadata @@ -355,8 +360,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 500696a47d..c0e54499af 100644 --- a/crates/miden-protocol/src/transaction/kernel/memory.rs +++ b/crates/miden-protocol/src/transaction/kernel/memory.rs @@ -430,17 +430,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 │ ATTACH 0 │ ATTACH 1 │ ATTACH 2 │ ATTACH 3 │ RECIPIENT │ ASSETS │ NUM │ +// │ ID │ HEADER │ │ │ │ │ │ COMMITMENT │ ASSETS │ +// ├──────┼──────────┼──────────┼──────────┼──────────┼──────────┼───────────┼────────────┼────────┼ +// 0 4 8 12 16 20 24 28 32 // -// ┬───────┬─────────┬─────┬────────┬─────────┬─────────┐ -// │ ASSET │ ASSET │ ... │ ASSET │ ASSET │ PADDING │ -// │ KEY 0 │ VALUE 0 │ │ KEY n │ VALUE n │ │ -// ┼───────┼─────────┼─────┼────────┼─────────┼─────────┘ -// 24 28 24 + 8n 28 + 8n +// ┬───────┬────────┬───────┬─────────┬─────┬────────┬─────────┬─────────┐ +// │ DIRTY │ NUM │ ASSET │ ASSET │ ... │ ASSET │ ASSET │ PADDING │ +// │ FLAG │ ATTACH │ KEY 0 │ VALUE 0 │ │ KEY n │ VALUE n │ │ +// ┼───────┼────────┼───────┼─────────┼─────┼────────┼─────────┼─────────┘ +// 33 34 36 40 36 + 8n 40 + 8n // // The DIRTY_FLAG is the binary flag which specifies whether the assets commitment stored in this // note is outdated. It holds 1 if some changes were made to the note assets since the last @@ -455,12 +455,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_ATTACHMENT_0_OFFSET: MemoryOffset = 8; +pub const OUTPUT_NOTE_ATTACHMENT_1_OFFSET: MemoryOffset = 12; +pub const OUTPUT_NOTE_ATTACHMENT_2_OFFSET: MemoryOffset = 16; +pub const OUTPUT_NOTE_ATTACHMENT_3_OFFSET: MemoryOffset = 20; +pub const OUTPUT_NOTE_RECIPIENT_OFFSET: MemoryOffset = 24; +pub const OUTPUT_NOTE_ASSET_COMMITMENT_OFFSET: MemoryOffset = 28; +pub const OUTPUT_NOTE_NUM_ASSETS_OFFSET: MemoryOffset = 32; +pub const OUTPUT_NOTE_DIRTY_FLAG_OFFSET: MemoryOffset = 33; +pub const OUTPUT_NOTE_NUM_ATTACHMENTS_OFFSET: MemoryOffset = 34; +pub const OUTPUT_NOTE_ASSETS_OFFSET: MemoryOffset = 36; // 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/proven_tx.rs b/crates/miden-protocol/src/transaction/proven_tx.rs index da89162b3e..e5973e0fd0 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)) } } From ed01038b7e37ff976f3db756a7517fa8220469ef Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Thu, 16 Apr 2026 16:27:51 +0200 Subject: [PATCH 05/48] feat: update set_attachment kernel proc to add_attachment --- .../asm/kernels/transaction/api.masm | 32 +- .../asm/kernels/transaction/lib/memory.masm | 96 ++++-- .../asm/kernels/transaction/lib/note.masm | 49 ++- .../kernels/transaction/lib/output_note.masm | 289 +++++++++--------- .../asm/protocol/output_note.masm | 2 +- .../asm/shared_utils/util/note.masm | 13 +- .../src/transaction/kernel/memory.rs | 40 +-- 7 files changed, 311 insertions(+), 210 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction/api.masm b/crates/miden-protocol/asm/kernels/transaction/api.masm index 703e3cdab3..1aaa115909 100644 --- a/crates/miden-protocol/asm/kernels/transaction/api.masm +++ b/crates/miden-protocol/asm/kernels/transaction/api.masm @@ -1129,30 +1129,35 @@ 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_word_size, 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_word_size is the size of the attachment in words. +#! - 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 exceeds 65534. +#! - the attachment word size exceeds 254 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.6 exec.output_note::assert_note_index_in_bounds drop + # => [attachment_scheme, attachment_word_size, 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)] + # => [attachment_scheme, attachment_word_size, ATTACHMENT, note_idx, pad(9)] - exec.output_note::set_attachment + exec.output_note::add_attachment # => [pad(16)] end @@ -1248,9 +1253,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 + # => [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 25f533bddd..8309557c7a 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 @@ -268,13 +269,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 # ------------------------------------------------------------------------------------------------- @@ -1942,46 +1946,82 @@ 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 at the given attachment index. #! -#! Inputs: [note_ptr, attachment_kind_scheme] -#! Outputs: [] +#! WARNING: Assumes the attachment_idx is within bounds: 0..4. +#! +#! Inputs: [note_ptr, attachment_idx] +#! Outputs: [ATTACHMENT] #! #! 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. +#! - ATTACHMENT is the note attachment word. +pub proc get_output_note_attachment + 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] end -#! Returns the output note's attachment. +#! Returns a pointer to the start of the attachment data region for the output note. #! #! Inputs: [note_ptr] -#! Outputs: [ATTACHMENT] +#! Outputs: [attachment_data_ptr] #! #! 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] +#! - attachment_data_ptr is the memory address of the first attachment slot. +pub proc get_output_note_attachment_data_ptr + add.OUTPUT_NOTE_ATTACHMENT_0_OFFSET end -#! Sets the output note's attachment. +#! Sets the output note's attachment at the given slot index. #! -#! Inputs: [note_ptr, ATTACHMENT] +#! Inputs: [note_ptr, attachment_idx, ATTACHMENT] #! Outputs: [] #! #! Where: -#! - ATTACHMENT is the note attachment. #! - note_ptr is the memory address at which the output note data begins. +#! - attachment_idx is the index of the attachment slot (0..3). +#! - ATTACHMENT is the note attachment word. pub proc set_output_note_attachment - add.OUTPUT_NOTE_ATTACHMENT_OFFSET - mem_storew_le - dropw + add.OUTPUT_NOTE_ATTACHMENT_0_OFFSET + # => [note_ptr + base_offset, attachment_idx, ATTACHMENT] + + swap mul.WORD_SIZE add + # => [attachment_ptr, ATTACHMENT] + + mem_storew_le dropw + # => [] +end + +#! Returns the number of attachments for the output note. +#! +#! Inputs: [note_ptr] +#! Outputs: [num_attachments] +#! +#! Where: +#! - note_ptr is the memory address at which the output note data begins. +#! - 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 number of attachments for the output note. +#! +#! Inputs: [note_ptr, num_attachments] +#! Outputs: [] +#! +#! Where: +#! - note_ptr is the memory address at which the output note data begins. +#! - 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..6213533123 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/note.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/note.masm @@ -2,10 +2,13 @@ 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_WORD_SIZE # ERRORS # ================================================================================================= @@ -81,6 +84,43 @@ end # OUTPUT NOTE PROCEDURES # ================================================================================================= +#! Computes the commitment to the output note's attachments. +#! +#! The commitment is defined as: +#! - 0 attachments: EMPTY_WORD +#! - 1+ attachments: sequential hash over all flattened attachment elements +#! +#! 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] + + # TODO(multi_attachments): We probably don't need the if-else branching here. If not, we can + # optimize the MASM a bit. + dup eq.0 + if.true + # 0 attachments: return EMPTY_WORD + drop drop padw + # => [EMPTY_WORD] + else + # 1+ attachments: compute hash via hash_words + # compute end_ptr = attachment_data_ptr + num_attachments * 4 + swap exec.memory::get_output_note_attachment_data_ptr + # => [start_ptr, num_attachments] + + swap mul.WORD_SIZE dup.1 add swap + # => [start_ptr, end_ptr] + + exec.poseidon2::hash_words + # => [ATTACHMENTS_COMMITMENT] + end +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 +239,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 0d44b877a5..353b06840a 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm @@ -5,24 +5,24 @@ use $kernel::fungible_asset use $kernel::memory use $kernel::note use $kernel::note::NOTE_TYPE_PUBLIC +use $kernel::note::MAX_ATTACHMENT_SCHEME +use $kernel::note::MAX_ATTACHMENT_WORD_SIZE use $kernel::constants::MAX_OUTPUT_NOTES_PER_TX -use $kernel::util::note::ATTACHMENT_KIND_NONE -use $kernel::util::note::ATTACHMENT_KIND_ARRAY use $kernel::asset::ASSET_SIZE use $kernel::asset::ASSET_VALUE_MEMORY_OFFSET use miden::core::word -# 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 +# ERRORS # ================================================================================================= const ERR_TX_NUMBER_OF_OUTPUT_NOTES_EXCEEDS_LIMIT="number of output notes in the transaction exceeds the maximum limit of 1024" @@ -31,13 +31,13 @@ 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_UNKNOWN_ATTACHMENT_KIND="attachment kind variant must be between 0 and 2" +const ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_MAX_EXCEEDED="attachment word size must not exceed 254" -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_CANNOT_BE_ZERO="attachment word size cannot be zero" -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_TOO_MANY_ATTACHMENTS="number of attachments per note cannot exceed 4" const ERR_NOTE_INVALID_INDEX="failed to find note at the given index; index must be within [0, num_of_notes]" @@ -53,19 +53,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 +100,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 +110,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] @@ -235,48 +229,75 @@ 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] +#! Inputs: [attachment_scheme, attachment_word_size, 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_kind is the kind of the attachment content. -#! - ATTACHMENT is the attachment to be set. +#! - attachment_word_size is the size of the attachment in words. +#! - ATTACHMENT is the attachment word to store. +#! - note_idx is the index of the note to which the attachment is added. #! #! Panics if: -#! - 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. -pub proc set_attachment - dup exec.memory::get_num_output_notes lte assert.err=ERR_NOTE_INVALID_INDEX - # => [note_idx, attachment_scheme, attachment_kind, ATTACHMENT] +#! - the attachment scheme exceeds 65534. +#! - the attachment word size exceeds 254 or is zero. +#! - 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_word_size, ATTACHMENT, note_idx] + + # validate 0 < attachment_word_size <= 254 + dup.1 neq.0 assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_CANNOT_BE_ZERO + dup.1 u32assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_MAX_EXCEEDED + u32lte.MAX_ATTACHMENT_WORD_SIZE assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_MAX_EXCEEDED + # => [attachment_scheme, attachment_word_size, ATTACHMENT, note_idx] + + # get note_ptr from note_idx + movup.6 exec.memory::get_output_note_ptr + # => [note_ptr, attachment_scheme, attachment_word_size, ATTACHMENT] + + # validate current number of attachments < 4 which means we can add one more to stay below the + # limit + dup exec.memory::get_output_note_num_attachments + # => [num_attachments, note_ptr, attachment_scheme, attachment_word_size, ATTACHMENT] + + dup lt.MAX_ATTACHMENTS_PER_NOTE assert.err=ERR_OUTPUT_NOTE_TOO_MANY_ATTACHMENTS + # => [num_attachments, note_ptr, attachment_scheme, attachment_word_size, ATTACHMENT] + + # emit event + emit.NOTE_BEFORE_ADD_ATTACHMENT_EVENT + # => [num_attachments, note_ptr, attachment_scheme, attachment_word_size, ATTACHMENT] - exec.memory::get_output_note_ptr dup - # => [note_ptr, note_ptr, attachment_scheme, attachment_kind, ATTACHMENT] - - dupw.1 - # => [ATTACHMENT, note_ptr, note_ptr, attachment_scheme, attachment_kind, ATTACHMENT] - - dup.7 dup.7 - # => [attachment_scheme, attachment_kind, ATTACHMENT, note_ptr, note_ptr, - # attachment_scheme, attachment_kind, ATTACHMENT] - - exec.validate_attachment - # => [note_ptr, note_ptr, attachment_scheme, attachment_kind, ATTACHMENT] - - movdn.3 movdn.3 - # => [attachment_scheme, attachment_kind, note_ptr, note_ptr, ATTACHMENT] + swapw + # => [ATTACHMENT, num_attachments, note_ptr, attachment_scheme, attachment_word_size] - emit.NOTE_BEFORE_SET_ATTACHMENT_EVENT - # => [attachment_scheme, attachment_kind, note_ptr, note_ptr, ATTACHMENT] + # TODO(multi_attachments): Validate attachment + word size against advice data. - exec.set_attachment_kind_scheme - # => [note_ptr, ATTACHMENT] + # use attachment_idx = num_attachments + dup.4 dup.6 + # => [note_ptr, attachment_idx, ATTACHMENT, num_attachments, note_ptr, attachment_scheme, attachment_word_size] exec.memory::set_output_note_attachment + # => [num_attachments, note_ptr, attachment_scheme, attachment_word_size] + + # compute new number of attachments + # we have previously validated num_attachments to be < 4, so the new number is guaranteed to be + # at most 4 + dup add.1 + # => [new_num_attachments, num_attachments, note_ptr, attachment_scheme, attachment_word_size] + + # set new number of attachments in memory + dup.2 exec.memory::set_output_note_num_attachments + # => [num_attachments, note_ptr, attachment_scheme, attachment_word_size] + + # update attachment size and scheme in the note metadata + # note that this needs to take the previous number of attachments, so passing num_attachments + # is correct + exec.set_attachment_size_and_scheme # => [] end @@ -294,17 +315,13 @@ pub proc assert_note_index_in_bounds # => [note_index] end -# HELPER PROCEDURES +# HELPER PROCEDURES # ================================================================================================= #! 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 sizes and schemes are initialized to 0 (no attachments). #! #! Inputs: [tag, note_type] #! Outputs: [NOTE_METADATA_HEADER] @@ -326,112 +343,110 @@ 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 sizes to 0 u32assert.err=ERR_NOTE_TAG_MUST_BE_U32 - # => [tag, note_type] + # => [attachment_size_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_size_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_size_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 (all zeros) + push.ATTACHMENT_DEFAULT_SCHEMES movdn.3 + # => [sender_id_suffix_type_version, sender_id_prefix, attachment_size_and_tag, attachment_schemes] # => [NOTE_METADATA_HEADER] end -#! Validate the ATTACHMENT against the attachment kind. +#! Sets an output note's attachment word size and scheme in the note metadata header. +#! +#! WARNING: The attachment size and scheme must be valid. #! -#! Inputs: [attachment_scheme, attachment_kind, ATTACHMENT] +#! Inputs: [num_attachments, note_ptr, attachment_scheme, attachment_word_size] #! Outputs: [] #! #! 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] +#! - 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_word_size is the size in words of the attachment content. +#! - note_ptr is the memory address at which the output note data begins. +proc set_attachment_size_and_scheme + # the size and schemes are stored as follows in the second and third felt: + # 2nd felt: [ + # att_3_size (8 bits) | att_2_size (8 bits) | att_1_size (8 bits) | + # att_0_size (8 bits) | note_tag (32 bits) + # ] + # 3rd felt: [ + # att_3_scheme (16 bits) | att_2_scheme (16 bits) | + # att_1_scheme (16 bits) | att_0_scheme (16 bits) + # ] + # -> the word_size needs to be shifted left by 32 + (num_attachments * 8) + # -> the scheme needs to be shifted left by num_attachments * 16 + + # Prepare scheme and word size. + # -------------------------------------------------------------------------------------------- - eq.ATTACHMENT_KIND_NONE - # => [is_attachment_none, attachment_scheme, ATTACHMENT] + # 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) + movup.2 dup.1 + # => [num_attachments, attachment_scheme, num_attachments, note_ptr, attachment_word_size] - if.true - eq.ATTACHMENT_SCHEME_NONE - assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_KIND_NONE_MUST_HAVE_ATTACHMENT_SCHEME_NONE - # => [ATTACHMENT] + mul.16 pow2 mul + # => [attachment_scheme_shifted, num_attachments, note_ptr, attachment_word_size] - padw assert_eqw.err=ERR_OUTPUT_NOTE_ATTACHMENT_KIND_NONE_MUST_BE_EMPTY_WORD - # => [] - else - drop dropw - # => [] - end - # => [] -end + # shift the word size left by num_attachments * 8 + 32 bits + # split into a u32shl (by num_attachments * 8, max 24) and a multiplication by 2^32 + movup.3 movup.2 mul.8 + # => [num_attachments * 8, attachment_word_size, attachment_scheme_shifted, note_ptr] -#! 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] -#! Outputs: [] -#! -#! Where: -#! - 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] + u32shl mul.0x100000000 + # => [attachment_word_size_shifted, attachment_scheme_shifted, note_ptr] - swap - # => [note_ptr, attachment_kind_scheme] + # Fetch and update metadata header. + # -------------------------------------------------------------------------------------------- - exec.memory::set_output_note_attachment_kind_scheme + dup.2 exec.memory::get_output_note_metadata_header + # => [METADATA_HEADER, attachment_word_size_shifted, attachment_scheme_shifted, note_ptr] + # => [ + # [sender_id_suffix_type_version, sender_id_prefix, attachment_size_and_tag, attachment_schemes], + # attachment_word_size_shifted, attachment_scheme_shifted, note_ptr + # ] + + # merge word size into existing sizes and tag + # (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.2 movup.4 + add movdn.2 + # => [ + # [sender_id_suffix_type_version, sender_id_prefix, new_attachment_size_and_tag, attachment_schemes], + # attachment_scheme_shifted, note_ptr + # ] + + # merge scheme into existing schemes + # (using add for the same reason as above) + movup.3 movup.4 + add movdn.3 + # => [ + # [sender_id_suffix_type_version, sender_id_prefix, new_attachment_size_and_tag, new_attachment_schemes], + # note_ptr + # ] + # => [METADATA_HEADER, note_ptr] + + movup.4 exec.memory::set_output_note_metadata_header dropw # => [] end -#! 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] -end - #! Increments the number of output notes by one. Returns the index of the next note to be created. #! #! Inputs: [] diff --git a/crates/miden-protocol/asm/protocol/output_note.masm b/crates/miden-protocol/asm/protocol/output_note.masm index d9ce865024..3899e60ae4 100644 --- a/crates/miden-protocol/asm/protocol/output_note.masm +++ b/crates/miden-protocol/asm/protocol/output_note.masm @@ -158,7 +158,7 @@ end #! If attachment_kind == Array, there must be an advice map entry for ATTACHMENT (see below). #! #! Inputs: -#! Operand Stack: [note_idx, attachment_scheme, attachment_kind, ATTACHMENT] +#! Operand Stack: [attachment_scheme, attachment_word_size, ATTACHMENT, note_idx] #! Advice map: { #! ATTACHMENT?: [[ATTACHMENT_ELEMENTS]], #! } diff --git a/crates/miden-protocol/asm/shared_utils/util/note.masm b/crates/miden-protocol/asm/shared_utils/util/note.masm index d0f474b692..f059028719 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,9 @@ 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 attachment word size. +pub const MAX_ATTACHMENT_WORD_SIZE=254 diff --git a/crates/miden-protocol/src/transaction/kernel/memory.rs b/crates/miden-protocol/src/transaction/kernel/memory.rs index c0e54499af..fa2aff3311 100644 --- a/crates/miden-protocol/src/transaction/kernel/memory.rs +++ b/crates/miden-protocol/src/transaction/kernel/memory.rs @@ -430,17 +430,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 │ ATTACH 0 │ ATTACH 1 │ ATTACH 2 │ ATTACH 3 │ RECIPIENT │ ASSETS │ NUM │ -// │ ID │ HEADER │ │ │ │ │ │ COMMITMENT │ ASSETS │ -// ├──────┼──────────┼──────────┼──────────┼──────────┼──────────┼───────────┼────────────┼────────┼ -// 0 4 8 12 16 20 24 28 32 +// ┌──────┬──────────┬──────────────┬────────────┬────────────┬────────────┬────────────┬───────────┬────────────┬────────┬ +// │ NOTE │ METADATA │ NUM │ ATTACHMENT │ ATTACHMENT │ ATTACHMENT │ ATTACHMENT │ RECIPIENT │ ASSETS │ NUM │ +// │ ID │ HEADER │ ATTACHMENTS │ 0 │ 1 │ 2 │ 3 │ │ COMMITMENT │ ASSETS │ +// ├──────┼──────────┼──────────────┼────────────┼────────────┼────────────┼────────────┼───────────┼────────────┼────────┼ +// 0 4 8 12 16 20 24 28 32 36 // -// ┬───────┬────────┬───────┬─────────┬─────┬────────┬─────────┬─────────┐ -// │ DIRTY │ NUM │ ASSET │ ASSET │ ... │ ASSET │ ASSET │ PADDING │ -// │ FLAG │ ATTACH │ KEY 0 │ VALUE 0 │ │ KEY n │ VALUE n │ │ -// ┼───────┼────────┼───────┼─────────┼─────┼────────┼─────────┼─────────┘ -// 33 34 36 40 36 + 8n 40 + 8n +// ┬───────┬───────┬─────────┬─────┬────────┬─────────┬─────────┐ +// │ DIRTY │ ASSET │ ASSET │ ... │ ASSET │ ASSET │ PADDING │ +// │ FLAG │ KEY 0 │ VALUE 0 │ │ KEY n │ VALUE n │ │ +// ┼───────┼───────┼─────────┼─────┼────────┼─────────┼─────────┘ +// 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,16 +455,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_0_OFFSET: MemoryOffset = 8; -pub const OUTPUT_NOTE_ATTACHMENT_1_OFFSET: MemoryOffset = 12; -pub const OUTPUT_NOTE_ATTACHMENT_2_OFFSET: MemoryOffset = 16; -pub const OUTPUT_NOTE_ATTACHMENT_3_OFFSET: MemoryOffset = 20; -pub const OUTPUT_NOTE_RECIPIENT_OFFSET: MemoryOffset = 24; -pub const OUTPUT_NOTE_ASSET_COMMITMENT_OFFSET: MemoryOffset = 28; -pub const OUTPUT_NOTE_NUM_ASSETS_OFFSET: MemoryOffset = 32; -pub const OUTPUT_NOTE_DIRTY_FLAG_OFFSET: MemoryOffset = 33; -pub const OUTPUT_NOTE_NUM_ATTACHMENTS_OFFSET: MemoryOffset = 34; -pub const OUTPUT_NOTE_ASSETS_OFFSET: MemoryOffset = 36; +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 // ------------------------------------------------------------------------------------------------ From 59020c64a8b6f09d4547cc00e8118ceb635c1217 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Thu, 16 Apr 2026 16:55:44 +0200 Subject: [PATCH 06/48] chore: update miden::protocol attachment handlers --- .../asm/protocol/kernel_proc_offsets.masm | 2 +- crates/miden-protocol/asm/protocol/note.masm | 45 ++++++---- .../asm/protocol/output_note.masm | 85 +++++++++---------- 3 files changed, 69 insertions(+), 63 deletions(-) diff --git a/crates/miden-protocol/asm/protocol/kernel_proc_offsets.masm b/crates/miden-protocol/asm/protocol/kernel_proc_offsets.masm index eeb370179c..9c732ddbec 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 37c7e6081a..8ccb624fed 100644 --- a/crates/miden-protocol/asm/protocol/note.masm +++ b/crates/miden-protocol/asm/protocol/note.masm @@ -6,6 +6,8 @@ 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_WORD_SIZE # ERRORS # ================================================================================================= @@ -194,27 +196,40 @@ 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 size 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: [attachment0_size, attachment0_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. +#! - attachment0_size is the word size of the first attachment (0 if absent). +#! - attachment0_scheme is the scheme of the first attachment. #! #! Invocation: exec -pub proc metadata_into_attachment_info - # => [sender_id_suffix_type_version, sender_id_prefix, tag, attachment_kind_scheme] - drop drop drop - # => [attachment_kind_scheme] - - # 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] +pub proc metadata_into_attachment_header + # => [sender_id_suffix_type_version, sender_id_prefix, tag_and_sizes, schemes] + + drop drop + # => [tag_and_sizes, schemes] + + # extract att_0_size from tag_and_sizes: + # u32split converts into sizes_packed (upper 32 bits) and tag (lower 32 bits) + u32split swap drop + # => [sizes_packed, schemes] + # => [attachment3_size (8 bits) | attachment2_size (8 bits) | + # attachment1_size (8 bits) | attachment0_size (8 bits)] + + # extract attachment 0 size by taking only the lower 8 bits + u32and.0xff + # [attachment0_size, schemes] + + # extract attachment 0 scheme by taking only the lower 16 bits + swap u32and.0xffff swap + # [attachment0_size, attachment0_scheme] end #! Extracts the note type from the provided metadata header. diff --git a/crates/miden-protocol/asm/protocol/output_note.masm b/crates/miden-protocol/asm/protocol/output_note.masm index 3899e60ae4..ad8ace2b9a 100644 --- a/crates/miden-protocol/asm/protocol/output_note.masm +++ b/crates/miden-protocol/asm/protocol/output_note.masm @@ -1,19 +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 - # PROCEDURES # ================================================================================================= @@ -153,9 +145,9 @@ 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). +#! If attachment_word_size > 1, there must be an advice map entry for ATTACHMENT. #! #! Inputs: #! Operand Stack: [attachment_scheme, attachment_word_size, ATTACHMENT, note_idx] @@ -165,27 +157,28 @@ end #! 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_word_size is the number of words that make up the attachment. +#! - ATTACHMENT is the attachment word to store or the commitment to an array of attachment +#! elements. +#! - note_idx is the index of the note to which the attachment is added. #! - ATTACHMENT_ELEMENTS are the elements for which ATTACHMENT is the sequential commitment (only -#! needed if attachment_kind == Array). +#! needed if attachment_word_size > 1). #! #! 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 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_word_size, ATTACHMENT, note_idx] # pad the stack before the syscall padw padw swapdw - # => [offset, note_idx, attachment_scheme, attachment_kind, ATTACHMENT, pad(8)] + # => [offset, attachment_scheme, attachment_word_size, ATTACHMENT, note_idx, pad(8)] syscall.exec_kernel_proc # => [pad(16)] @@ -195,61 +188,59 @@ pub proc set_attachment # => [] end -#! Sets the attachment of the note specified by the note index to the provided word. -#! -#! This overwrites any previously set attachment. +#! Adds a single-word attachment to the note specified by the note index. #! -#! 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 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_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] +pub proc add_word_attachment + # insert attachment_word_size = 1 after attachment_scheme + push.1 swap + # => [attachment_scheme, 1, ATTACHMENT, note_idx] - exec.set_attachment + 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 is the +#! commitment to a set of elements. #! #! Inputs: -#! Operand Stack: [note_idx, attachment_scheme, ATTACHMENT] +#! Operand Stack: [attachment_scheme, attachment_word_size, ATTACHMENT, note_idx] #! Advice map: { #! ATTACHMENT: [[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_word_size is the size of the attachment in words. #! - ATTACHMENT is the commitment of the set of elements that form the note attachment. +#! - note_idx is the index of the note to which the attachment is added. #! - ATTACHMENT_ELEMENTS are the elements for which ATTACHMENT 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 word_size 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 @@ -286,7 +277,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] @@ -294,7 +285,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. From d378e139324ebce9b491fae5ea57d64910806a49 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Fri, 17 Apr 2026 08:13:51 +0200 Subject: [PATCH 07/48] chore: introduce dedicated attachment error variants --- crates/miden-protocol/src/errors/mod.rs | 20 +++++++++++++++++--- crates/miden-protocol/src/note/attachment.rs | 20 +++++--------------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/crates/miden-protocol/src/errors/mod.rs b/crates/miden-protocol/src/errors/mod.rs index 98090b814e..b7c395376f 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}; @@ -37,6 +36,7 @@ use crate::note::{ ======= NoteAssets, NoteAttachmentArray, + NoteAttachmentHeader, NoteAttachmentScheme, NoteAttachments, NoteTag, @@ -49,12 +49,14 @@ 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, MAX_NOTE_STORAGE_ITEMS, MAX_OUTPUT_NOTES_PER_TX, NOTE_MAX_SIZE, + WORD_SIZE, }; #[cfg(any(feature = "testing", test))] @@ -664,11 +666,23 @@ pub enum NoteError { InvalidNoteStorageLength { expected: usize, actual: usize }, #[error("note tag requires a public note but the note is of type {0}")] PublicNoteRequired(NoteType), + #[error("note attachment array length {0} is not divisible by {WORD_SIZE}")] + NoteAttachmentArrayNotWordAligned(usize), #[error( - "note attachment cannot commit to more than {} elements", - NoteAttachmentArray::MAX_NUM_ELEMENTS + "note attachment array must have at least {min} elements, got {0}", + min = NoteAttachmentArray::MIN_NUM_ELEMENTS + )] + NoteAttachmentArrayTooFewElements(usize), + #[error( + "note attachment array contains {0} elements, but the maximum is {max} elements", + max = NoteAttachmentArray::MAX_NUM_ELEMENTS )] NoteAttachmentArraySizeExceeded(usize), + #[error( + "attachment size {0} exceeds maximum {max}", + max = NoteAttachmentHeader::MAX_SIZE + )] + 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)] diff --git a/crates/miden-protocol/src/note/attachment.rs b/crates/miden-protocol/src/note/attachment.rs index 9903d01c4f..ea0bead8eb 100644 --- a/crates/miden-protocol/src/note/attachment.rs +++ b/crates/miden-protocol/src/note/attachment.rs @@ -300,18 +300,11 @@ impl NoteAttachmentArray { /// - The number of elements exceeds [`Self::MAX_NUM_ELEMENTS`]. pub fn new(elements: Vec) -> Result { if !elements.len().is_multiple_of(WORD_SIZE) { - return Err(NoteError::other(format!( - "note attachment array length must be divisible by 4, got {}", - elements.len() - ))); + return Err(NoteError::NoteAttachmentArrayNotWordAligned(elements.len())); } if elements.len() < Self::MIN_NUM_ELEMENTS as usize { - return Err(NoteError::other(format!( - "note attachment array must have at least {} elements, got {}", - Self::MIN_NUM_ELEMENTS, - elements.len() - ))); + return Err(NoteError::NoteAttachmentArrayTooFewElements(elements.len())); } if elements.len() > Self::MAX_NUM_ELEMENTS as usize { @@ -504,10 +497,7 @@ impl NoteAttachmentHeader { /// Returns an error if `size` exceeds [`Self::MAX_SIZE`]. pub fn new(scheme: NoteAttachmentScheme, word_size: u8) -> Result { if word_size > Self::MAX_SIZE { - return Err(NoteError::other(alloc::format!( - "attachment size {word_size} exceeds maximum {}", - Self::MAX_SIZE - ))); + return Err(NoteError::NoteAttachmentHeaderSizeExceeded(word_size)); } Ok(Self { scheme, word_size }) } @@ -806,14 +796,14 @@ mod tests { let elements = vec![Felt::from(1u32); 4]; let err = NoteAttachmentArray::new(elements).unwrap_err(); // Arrays must have at least MIN_NUM_ELEMENTS (8) to distinguish from word attachments. - assert!(err.to_string().contains("at least")); + assert_matches!(err, NoteError::NoteAttachmentArrayTooFewElements(4)); } #[test] fn note_attachment_array_fails_on_non_word_aligned_length() { let elements = vec![Felt::from(1u32); 9]; let err = NoteAttachmentArray::new(elements).unwrap_err(); - assert!(err.to_string().contains("divisible by 4")); + assert_matches!(err, NoteError::NoteAttachmentArrayNotWordAligned(9)); } #[test] From 5d8a4ea060f238eec78e13f235760d63e8309e6d Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Fri, 17 Apr 2026 13:56:24 +0200 Subject: [PATCH 08/48] feat: update standards types for multiple attachments --- crates/miden-protocol/src/note/attachment.rs | 10 +++++ .../attachments/network_account_target.masm | 39 +++++++----------- .../src/account/interface/component.rs | 41 +++++++++++++------ .../src/account/interface/mod.rs | 9 +++- .../src/account/interface/test.rs | 8 ++-- crates/miden-standards/src/note/burn.rs | 9 ++-- crates/miden-standards/src/note/mint.rs | 9 ++-- .../src/note/network_account_target.rs | 31 ++++++++++---- .../miden-standards/src/note/network_note.rs | 15 +++---- crates/miden-standards/src/note/p2id.rs | 9 ++-- crates/miden-standards/src/note/p2ide.rs | 9 ++-- .../src/note/standard_note_attachment.rs | 2 +- crates/miden-standards/src/note/swap.rs | 10 ++--- crates/miden-standards/src/testing/note.rs | 18 ++++---- 14 files changed, 126 insertions(+), 93 deletions(-) diff --git a/crates/miden-protocol/src/note/attachment.rs b/crates/miden-protocol/src/note/attachment.rs index ea0bead8eb..7cdb562fb4 100644 --- a/crates/miden-protocol/src/note/attachment.rs +++ b/crates/miden-protocol/src/note/attachment.rs @@ -403,6 +403,16 @@ impl NoteAttachmentScheme { Ok(Self(attachment_scheme)) } + /// Creates a new [`NoteAttachmentScheme`] from a `u16`. + /// + /// # Panics + /// + /// Panics if `attachment_scheme` exceeds [`Self::MAX`]. + pub const fn new_const(attachment_scheme: u16) -> Self { + assert!(attachment_scheme <= Self::MAX.as_u16(), "attachment scheme exceeds maximum"); + Self(attachment_scheme) + } + /// Returns the [`NoteAttachmentScheme`] that signals the absence of an attachment scheme. pub const fn none() -> Self { Self(Self::NONE) 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..c225c7e653 100644 --- a/crates/miden-standards/asm/standards/attachments/network_account_target.masm +++ b/crates/miden-standards/asm/standards/attachments/network_account_target.masm @@ -14,29 +14,19 @@ use miden::protocol::note #! 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 - # 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,12 @@ pub proc active_account_matches_target_account swapw # => [METADATA_HEADER, NOTE_ATTACHMENT] - exec.note::metadata_into_attachment_info - # => [attachment_kind, attachment_scheme, NOTE_ATTACHMENT] + exec.note::metadata_into_attachment_header + # => [attachment0_size, attachment0_scheme, NOTE_ATTACHMENT] - swap - # => [attachment_scheme, attachment_kind, NOTE_ATTACHMENT] + # make sure the word size of the attachment is 1 + eq.1 assert.err=ERR_NOT_NETWORK_ACCOUNT_TARGET + # => [attachment0_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/src/account/interface/component.rs b/crates/miden-standards/src/account/interface/component.rs index 6ce052af26..4155008023 100644 --- a/crates/miden-standards/src/account/interface/component.rs +++ b/crates/miden-standards/src/account/interface/component.rs @@ -253,21 +253,38 @@ 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_word = attachment.content().to_word(); + let attachment_scheme = attachment.attachment_scheme().as_u16() as u32; + let word_size = attachment.content().word_size(); + + body.push_str(&format!( + " + push.{attachment_word} + push.{word_size} push.{attachment_scheme} - movup.6 - # => [note_idx, attachment_scheme, attachment_kind, ATTACHMENT, pad(16)] - exec.::miden::protocol::output_note::set_attachment + # => [attachment_scheme, word_size, ATTACHMENT, 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..583811d1f1 100644 --- a/crates/miden-standards/src/account/interface/mod.rs +++ b/crates/miden-standards/src/account/interface/mod.rs @@ -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() { + if let NoteAttachmentContent::Array(array) = attachment.content() { + code_builder + .add_advice_map_entry(array.commitment(), array.as_slice().to_vec()); + } } } @@ -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 23e1f8201f..52ed7cb551 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, @@ -98,7 +98,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])), ) @@ -192,7 +192,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])), ) @@ -224,7 +224,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 d9b22572a1..7c68333811 100644 --- a/crates/miden-standards/src/note/burn.rs +++ b/crates/miden-standards/src/note/burn.rs @@ -7,7 +7,7 @@ use miden_protocol::errors::NoteError; use miden_protocol::note::{ Note, NoteAssets, - NoteAttachment, + NoteAttachments, NoteMetadata, NoteRecipient, NoteScript, @@ -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 a020802833..4709916fc5 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, @@ -90,7 +90,7 @@ impl MintNote { faucet_id: AccountId, sender: AccountId, mint_storage: MintNoteStorage, - attachment: NoteAttachment, + attachments: NoteAttachments, rng: &mut R, ) -> Result { let note_script = Self::script(); @@ -104,12 +104,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..0470b9adf9 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,22 @@ 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 + .iter() + .find(|attachment| { + attachment.attachment_scheme() == NetworkAccountTarget::ATTACHMENT_SCHEME + }) + .ok_or_else(|| NetworkAccountTargetError::MissingAttachmentScheme)?; + + Self::try_from(attachment) + } +} impl TryFrom<&NoteAttachment> for NetworkAccountTarget { type Error = NetworkAccountTargetError; @@ -109,8 +125,8 @@ impl TryFrom<&NoteAttachment> for NetworkAccountTarget { NetworkAccountTarget::new(target_id, exec_hint) }, - _ => Err(NetworkAccountTargetError::AttachmentKindMismatch( - attachment.content().attachment_kind(), + _ => Err(NetworkAccountTargetError::AttachmentContentNotWord( + attachment.content().word_size(), )), } } @@ -123,16 +139,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 (word_size={0}, expected 1)")] + AttachmentContentNotWord(u8), #[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..1d6933bed4 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 [`NoteAttachment`] 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 82ea64f41a..84acc3924d 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, @@ -78,7 +78,7 @@ impl P2idNote { target: AccountId, assets: Vec, note_type: NoteType, - attachment: NoteAttachment, + attachments: NoteAttachments, rng: &mut R, ) -> Result { let serial_num = rng.draw_word(); @@ -86,11 +86,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 aa1bdafe15..dd1456e3f8 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, @@ -85,18 +85,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/standard_note_attachment.rs b/crates/miden-standards/src/note/standard_note_attachment.rs index 17ec1332df..740935d87e 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(1u16), } } } diff --git a/crates/miden-standards/src/note/swap.rs b/crates/miden-standards/src/note/swap.rs index 1c156651ac..f4f04294db 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, @@ -80,7 +80,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> { @@ -100,11 +100,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 6c8d4ef1a1..30be2a581d 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, @@ -46,7 +47,7 @@ pub struct NoteBuilder { serial_num: Word, tag: NoteTag, code: String, - attachment: NoteAttachment, + attachments: NoteAttachments, source_code: SourceCodeOrigin, } @@ -68,7 +69,7 @@ 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_CODE.to_string(), - attachment: NoteAttachment::default(), + attachments: NoteAttachments::default(), source_code: SourceCodeOrigin::Masm { dyn_libraries: Vec::new(), source_manager: Arc::new(DefaultSourceManager::default()), @@ -114,9 +115,12 @@ 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 } @@ -187,12 +191,10 @@ impl NoteBuilder { }; 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)) } } From bba9952e0103600c555717cc26067182dac6654e Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Fri, 17 Apr 2026 14:06:11 +0200 Subject: [PATCH 09/48] feat: update tx host attachment handler for multiples --- crates/miden-tx/src/errors/mod.rs | 4 +- crates/miden-tx/src/executor/exec_host.rs | 4 +- crates/miden-tx/src/host/mod.rs | 6 +- crates/miden-tx/src/host/note_builder.rs | 32 +++++++-- crates/miden-tx/src/host/tx_event.rs | 85 ++++++++++++----------- crates/miden-tx/src/prover/prover_host.rs | 4 +- 6 files changed, 78 insertions(+), 57 deletions(-) diff --git a/crates/miden-tx/src/errors/mod.rs b/crates/miden-tx/src/errors/mod.rs index 4013fc52fe..ef5d7a4a3f 100644 --- a/crates/miden-tx/src/errors/mod.rs +++ b/crates/miden-tx/src/errors/mod.rs @@ -258,10 +258,8 @@ 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 {actual} does not match attachment {provided} provided to add_attachment" )] NoteAttachmentArrayMismatch { actual: Word, provided: Word }, #[error( diff --git a/crates/miden-tx/src/executor/exec_host.rs b/crates/miden-tx/src/executor/exec_host.rs index a277eb8f11..7bcff0c8ef 100644 --- a/crates/miden-tx/src/executor/exec_host.rs +++ b/crates/miden-tx/src/executor/exec_host.rs @@ -609,9 +609,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..9905a5f3cd 100644 --- a/crates/miden-tx/src/host/note_builder.rs +++ b/crates/miden-tx/src/host/note_builder.rs @@ -6,6 +6,7 @@ use miden_protocol::note::{ Note, NoteAssets, NoteAttachment, + NoteAttachments, NoteMetadata, NoteRecipient, PartialNote, @@ -26,6 +27,7 @@ use crate::errors::TransactionKernelError; pub struct OutputNoteBuilder { metadata: NoteMetadata, assets: Vec, + attachments: Vec, recipient_digest: Word, recipient: Option, } @@ -58,6 +60,7 @@ impl OutputNoteBuilder { recipient_digest, recipient: None, assets: Vec::new(), + attachments: Vec::default(), }) } @@ -68,6 +71,7 @@ impl OutputNoteBuilder { recipient_digest: recipient.digest(), recipient: Some(recipient), assets: Vec::new(), + attachments: Vec::default(), } } @@ -116,9 +120,24 @@ 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. + pub fn add_attachment( + &mut self, + attachment: NoteAttachment, + ) -> Result<(), TransactionKernelError> { + if self.attachments.len() >= NoteAttachments::MAX_COUNT { + return Err(TransactionKernelError::other(format!( + "number of attachments exceeded max {}", + NoteAttachments::MAX_COUNT + ))); + } + + self.attachments.push(attachment); + + Ok(()) } /// Converts this builder to an [OutputNote]. @@ -128,14 +147,17 @@ impl OutputNoteBuilder { pub fn build(self) -> RawOutputNote { let assets = NoteAssets::new(self.assets) .expect("assets should be valid since add_asset validates them"); + let attachments = NoteAttachments::new(self.attachments) + .expect("attachments should be valid since add_attachment validates them"); match self.recipient { Some(recipient) => { - let note = Note::new(assets, self.metadata, recipient); + let note = Note::with_attachments(assets, self.metadata, recipient, 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, 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..518d9ccf22 100644 --- a/crates/miden-tx/src/host/tx_event.rs +++ b/crates/miden-tx/src/host/tx_event.rs @@ -15,7 +15,6 @@ use miden_protocol::note::{ NoteAttachment, NoteAttachmentArray, NoteAttachmentContent, - NoteAttachmentKind, NoteAttachmentScheme, NoteId, NoteMetadata, @@ -135,10 +134,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 +424,26 @@ 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_word_size, ATTACHMENT, note_idx // ] - 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 note_ptr = process.get_stack_item(2); + let attachment_scheme = process.get_stack_item(3); + let attachment_word_size = process.get_stack_item(4); let attachment = process.get_stack_word(5); let (note_idx, attachment) = extract_note_attachment( attachment_scheme, - attachment_kind, + attachment_word_size, attachment, note_ptr, process.advice_provider(), )?; - Some(TransactionEvent::NoteBeforeSetAttachment { note_idx, attachment }) + Some(TransactionEvent::NoteBeforeAddAttachment { note_idx, attachment }) }, TransactionEventId::AuthRequest => { @@ -737,66 +736,68 @@ fn build_note_metadata( fn extract_note_attachment( attachment_scheme: Felt, - attachment_kind: Felt, + attachment_word_size: Felt, attachment: 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 word_size = u8::try_from(attachment_word_size.as_canonical_u64()).map_err(|_| { + TransactionKernelError::other("failed to convert attachment word size to u8") + })?; + + 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 + let attachment_content = match word_size { + 0 => { + return Err(TransactionKernelError::other("attachment word_size must be > 0")); }, - NoteAttachmentKind::Word => NoteAttachmentContent::Word(attachment), - NoteAttachmentKind::Array => { + 1 => NoteAttachmentContent::Word(attachment), + _ => { 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", - ) + TransactionKernelError::other( + "elements of a note attachment commitment must be present in the advice provider", + ) })?; - let commitment_attachment = + let array_attachment = NoteAttachmentArray::new(elements.to_vec()).map_err(|source| { TransactionKernelError::other_with_source( - "failed to construct note attachment commitment", + "failed to construct note attachment array", source, ) })?; - if commitment_attachment.commitment() != attachment { + if array_attachment.commitment() != attachment { return Err(TransactionKernelError::NoteAttachmentArrayMismatch { - actual: commitment_attachment.commitment(), + actual: array_attachment.commitment(), provided: attachment, }); } - NoteAttachmentContent::Array(commitment_attachment) + if array_attachment.word_size() != word_size { + return Err(TransactionKernelError::other(format!( + "array attachment word_size {} does not match declared word size {}", + array_attachment.word_size(), + word_size + ))); + } + + NoteAttachmentContent::Array(array_attachment) }, }; - let attachment = - NoteAttachment::new(attachment_scheme, attachment_content).map_err(|source| { - TransactionKernelError::other_with_source("failed to extract note attachment", source) - })?; + let attachment = NoteAttachment::new(attachment_scheme, attachment_content); 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, .. } => { From 5147042b655b6d7c61e1d2ade74a5e3a598c12ad Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Fri, 17 Apr 2026 14:29:38 +0200 Subject: [PATCH 10/48] chore: update agglayer code for multiple attachments --- .../asm/agglayer/bridge/bridge_in.masm | 10 ++----- .../asm/agglayer/bridge/bridge_out.masm | 30 +++++++++---------- crates/miden-agglayer/src/b2agg_note.rs | 16 +++++----- crates/miden-agglayer/src/claim_note.rs | 20 +++++++++---- crates/miden-agglayer/src/config_note.rs | 13 ++++---- crates/miden-agglayer/src/update_ger_note.rs | 13 ++++---- 6 files changed, 51 insertions(+), 51 deletions(-) diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm index e4abf8e986..68239d6986 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm @@ -950,19 +950,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 db367d415f..1b35a7137c 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm @@ -70,7 +70,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 # ------------------------------------------------------------------------------------------------- @@ -508,11 +507,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] @@ -539,27 +537,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 b407cc6991..0d85f561d5 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::serde::Deserializable; @@ -16,6 +15,7 @@ use miden_protocol::note::{ Note, NoteAssets, NoteAttachment, + NoteAttachments, NoteMetadata, NoteRecipient, NoteScript, @@ -96,17 +96,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 1ee5d31d2e..20b41a89c8 100644 --- a/crates/miden-agglayer/src/config_note.rs +++ b/crates/miden-agglayer/src/config_note.rs @@ -18,6 +18,7 @@ use miden_protocol::note::{ Note, NoteAssets, NoteAttachment, + NoteAttachments, NoteMetadata, NoteRecipient, NoteScript, @@ -112,16 +113,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 c965c5348b..4b0044bf94 100644 --- a/crates/miden-agglayer/src/update_ger_note.rs +++ b/crates/miden-agglayer/src/update_ger_note.rs @@ -18,6 +18,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)) } } From 52f8b962c42ed31cf863fbcb6c7af6a3e10ecf19 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Fri, 17 Apr 2026 15:12:30 +0200 Subject: [PATCH 11/48] chore: update tests for multiple attachments --- crates/miden-standards/src/testing/note.rs | 12 +- .../block/proposed_block_errors.rs | 4 +- .../block/proven_block_success.rs | 14 +- .../src/kernel_tests/tx/test_active_note.rs | 7 +- .../src/kernel_tests/tx/test_input_note.rs | 5 +- .../src/kernel_tests/tx/test_note.rs | 5 +- .../src/kernel_tests/tx/test_output_note.rs | 143 +++++++++--------- .../src/kernel_tests/tx/test_prologue.rs | 5 +- .../src/kernel_tests/tx/test_tx.rs | 51 +++---- .../src/mock_chain/chain_builder.rs | 6 +- .../src/standards/network_account_target.rs | 44 +++--- crates/miden-testing/src/utils.rs | 57 ++++--- .../tests/agglayer/bridge_out.rs | 5 +- crates/miden-testing/tests/scripts/faucet.rs | 14 +- crates/miden-testing/tests/scripts/p2id.rs | 8 +- .../miden-testing/tests/scripts/send_note.rs | 21 +-- 16 files changed, 218 insertions(+), 183 deletions(-) diff --git a/crates/miden-standards/src/testing/note.rs b/crates/miden-standards/src/testing/note.rs index 30be2a581d..cd7777d0f5 100644 --- a/crates/miden-standards/src/testing/note.rs +++ b/crates/miden-standards/src/testing/note.rs @@ -20,7 +20,7 @@ use miden_protocol::note::{ NoteType, }; use miden_protocol::testing::note::DEFAULT_NOTE_CODE; -use miden_protocol::vm::Package; +use miden_protocol::vm::{AdviceMap, Package}; use miden_protocol::{Felt, Word}; use rand::Rng; @@ -48,6 +48,7 @@ pub struct NoteBuilder { tag: NoteTag, code: String, attachments: NoteAttachments, + advice_map: AdviceMap, source_code: SourceCodeOrigin, } @@ -70,6 +71,7 @@ impl NoteBuilder { tag: NoteTag::with_account_target(sender), code: DEFAULT_NOTE_CODE.to_string(), attachments: NoteAttachments::default(), + advice_map: AdviceMap::default(), source_code: SourceCodeOrigin::Masm { dyn_libraries: Vec::new(), source_manager: Arc::new(DefaultSourceManager::default()), @@ -124,6 +126,12 @@ impl NoteBuilder { 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 + } + /// Extends the set of dynamically linked libraries that are passed to the assembler at /// build-time. pub fn dynamically_linked_libraries( @@ -190,6 +198,8 @@ 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); let storage = NoteStorage::new(self.storage)?; 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 779083be69..be4394f080 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 @@ -4,6 +4,7 @@ use anyhow::Context; use miden_protocol::account::Account; use miden_protocol::account::auth::AuthScheme; use miden_protocol::asset::FungibleAsset; +use miden_protocol::crypto::SequentialCommit; use miden_protocol::crypto::rand::{FeltRng, RandomCoin}; use miden_protocol::errors::tx_kernel::ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_METADATA_WHILE_NO_NOTE_BEING_PROCESSED; use miden_protocol::note::{ @@ -118,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 51d746748c..bf853b5e7d 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 @@ -1,6 +1,7 @@ use alloc::string::String; use miden_protocol::Word; +use miden_protocol::crypto::SequentialCommit; use miden_protocol::note::Note; use miden_protocol::transaction::memory::{ASSET_SIZE, ASSET_VALUE_OFFSET}; use miden_standards::code_builder::CodeBuilder; @@ -133,8 +134,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 4e5f301c2f..713e169935 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, @@ -398,7 +400,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 e03c3454ff..c1b231ef4c 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 @@ -4,17 +4,21 @@ use alloc::vec::Vec; use miden_protocol::account::auth::AuthScheme; use miden_protocol::account::{Account, AccountId}; use miden_protocol::asset::{Asset, FungibleAsset, NonFungibleAsset}; +use miden_protocol::crypto::SequentialCommit; use miden_protocol::crypto::rand::RandomCoin; use miden_protocol::errors::tx_kernel::{ ERR_NON_FUNGIBLE_ASSET_ALREADY_EXISTS, ERR_NOTE_NUM_OF_ASSETS_EXCEED_LIMIT, + ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_CANNOT_BE_ZERO, ERR_TX_NUMBER_OF_OUTPUT_NOTES_EXCEEDS_LIMIT, }; use miden_protocol::note::{ Note, NoteAttachment, NoteAttachmentScheme, + NoteAttachments, NoteMetadata, + NoteMetadataHeader, NoteRecipient, NoteStorage, NoteTag, @@ -38,7 +42,7 @@ 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, @@ -109,18 +113,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", ); @@ -231,8 +237,8 @@ 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)?, + [42, 43, 44, 45, 46, 47, 48, 49u32].map(Felt::from).to_vec(), )?) .build()?; @@ -290,10 +296,10 @@ async fn test_get_output_notes_commitment() -> anyhow::Result<()> { # => [note_idx] push.{ATTACHMENT2} + push.{attachment2_word_size} push.{attachment_scheme2} - movup.5 - # => [note_idx, attachment_scheme, ATTACHMENT] - exec.output_note::set_array_attachment + # => [attachment_scheme, attachment_word_size, ATTACHMENT, note_idx] + exec.output_note::add_array_attachment # => [] # compute the output notes commitment @@ -314,8 +320,10 @@ 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_word(), + attachment2_word_size = output_note_2.attachments().get(0).unwrap().word_size(), + attachment_scheme2 = + output_note_2.attachments().get(0).unwrap().attachment_scheme().as_u16(), ); let exec_output = &tx_context.execute_code(&code).await?; @@ -328,12 +336,13 @@ 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(), + exec_output + .get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_ATTACHMENT_0_OFFSET), + output_note_1.attachments().to_commitment(), "Validate the output note 1 attachment", ); @@ -341,14 +350,14 @@ async fn test_get_output_notes_commitment() -> anyhow::Result<()> { 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().to_commitment(), "Validate the output note 2 attachment", ); @@ -826,7 +835,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])), )?; @@ -835,7 +844,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])), )?; @@ -956,7 +965,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])), )?; @@ -999,8 +1008,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)?; @@ -1135,66 +1144,47 @@ async fn test_get_assets() -> anyhow::Result<()> { } #[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_zero_word_size_fails() -> anyhow::Result<()> { + let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; - let tx_script = format!( + let code = format!( " use miden::protocol::output_note + use miden::standards::note_tag::DEFAULT_TAG + use $kernel::prologue begin - push.{RECIPIENT} - push.{note_type} - push.{tag} + exec.prologue::prepare_transaction + + push.1.2.3.4 + push.{NOTE_TYPE_PUBLIC} + push.DEFAULT_TAG exec.output_note::create # => [note_idx] - push.{ATTACHMENT} - push.{attachment_kind} - push.{attachment_scheme} - movup.6 - # => [note_idx, attachment_scheme, attachment_kind, ATTACHMENT] - exec.output_note::set_attachment + # try to add an attachment with word_size = 0 + padw push.0 push.0 + # => [attachment_scheme, attachment_word_size, ATTACHMENT, note_idx] + exec.output_note::add_array_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(), + NOTE_TYPE_PUBLIC = NoteType::Public as u8, ); - let tx_script = CodeBuilder::new().compile_tx_script(tx_script)?; - - let tx = TransactionContextBuilder::new(account) - .extend_expected_output_notes(vec![output_note.clone()]) - .tx_script(tx_script) - .build()? - .execute() - .await?; + let exec_output = tx_context.execute_code(&code).await; - 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()); + assert_execution_error!(exec_output, ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_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])); + NoteAttachment::new_word(NoteAttachmentScheme::MAX, Word::from([3, 4, 5, 6u32])); let output_note = RawOutputNote::Full(NoteBuilder::new(account.id(), rng).attachment(attachment).build()?); @@ -1211,9 +1201,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 @@ -1223,8 +1212,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 = output_note.attachments().get(0).unwrap().content().to_word(), ); let tx_script = CodeBuilder::new().compile_tx_script(tx_script)?; @@ -1247,11 +1236,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 elements = [3, 4, 5, 6, 7, 8, 9, 10u32].map(Felt::from).to_vec(); + let attachment = NoteAttachment::new_array(NoteAttachmentScheme::new(42)?, elements.clone())?; let output_note = RawOutputNote::Full(NoteBuilder::new(account.id(), rng).attachment(attachment).build()?); + let attachment_content_word = output_note.attachments().get(0).unwrap().content().to_word(); + let attachment_word_size = output_note.attachments().get(0).unwrap().word_size(); let tx_script = format!( " use miden::protocol::output_note @@ -1264,10 +1255,10 @@ async fn test_set_array_attachment() -> anyhow::Result<()> { # => [note_idx] push.{ATTACHMENT} + push.{attachment_word_size} push.{attachment_scheme} - movup.5 - # => [note_idx, attachment_scheme, ATTACHMENT] - exec.output_note::set_array_attachment + # => [attachment_scheme, attachment_word_size, ATTACHMENT, note_idx] + exec.output_note::add_array_attachment # => [] # truncate the stack @@ -1277,8 +1268,9 @@ 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_word_size = attachment_word_size, + ATTACHMENT = attachment_content_word, ); let tx_script = CodeBuilder::new().compile_tx_script(tx_script)?; @@ -1286,7 +1278,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_content_word, elements)]) .build()? .execute() .await?; @@ -1324,7 +1316,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(()) 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 4f762445d5..fa37c1424f 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_prologue.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_prologue.rs @@ -16,6 +16,7 @@ use miden_protocol::account::{ }; use miden_protocol::asset::{FungibleAsset, NonFungibleAsset}; use miden_protocol::block::account_tree::AccountIdKey; +use miden_protocol::crypto::SequentialCommit; use miden_protocol::errors::tx_kernel::ERR_ACCOUNT_SEED_AND_COMMITMENT_DIGEST_MISMATCH; use miden_protocol::note::NoteId; use miden_protocol::testing::account_id::{ @@ -489,13 +490,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 d0e3ab8ffd..c5780a76ac 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)?, + [6, 7, 8, 9, 10, 11, 12, 13u32].map(Felt::from).to_vec(), )?; 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_CODE)?; 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_CODE)?; 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 @@ -319,9 +319,10 @@ async fn executed_transaction_output_notes() -> anyhow::Result<()> { # => [note_idx = 2] push.{ATTACHMENT3} + push.{attachment3_word_size} push.{attachment_scheme3} - movup.5 - exec.output_note::set_array_attachment + # => [attachment_scheme, attachment_word_size, ATTACHMENT, note_idx] + exec.output_note::add_array_attachment # => [] end ", @@ -338,9 +339,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(), + attachment_scheme2 = attachment2.attachment_scheme().as_u16(), ATTACHMENT2 = attachment2.content().to_word(), - attachment_scheme3 = attachment3.attachment_scheme().as_u32(), + attachment_scheme3 = attachment3.attachment_scheme().as_u16(), + attachment3_word_size = attachment3.word_size(), ATTACHMENT3 = attachment3.content().to_word(), ); @@ -382,12 +384,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 +469,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 +514,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 +573,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 9b9baef042..e3e0ec5c05 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_NATIVE_ASSET_FAUCET; use miden_protocol::testing::random_secret_key::random_secret_key; use miden_protocol::transaction::{OrderedTransactionHeaders, RawOutputNote, TransactionKernel}; @@ -566,7 +566,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())); @@ -617,7 +617,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..f95758648a 100644 --- a/crates/miden-testing/src/standards/network_account_target.rs +++ b/crates/miden-testing/src/standards/network_account_target.rs @@ -2,7 +2,14 @@ use miden_protocol::Felt; use miden_protocol::account::AccountStorageMode; -use miden_protocol::note::{NoteAttachment, NoteMetadata, NoteTag, NoteType}; +use miden_protocol::note::{ + NoteAttachment, + NoteAttachments, + NoteMetadata, + NoteMetadataHeader, + NoteTag, + NoteType, +}; use miden_protocol::testing::account_id::AccountIdBuilder; use miden_standards::note::{NetworkAccountTarget, NoteExecutionHint}; @@ -16,10 +23,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#" @@ -30,11 +38,11 @@ async fn network_account_target_get_id() -> anyhow::Result<()> { 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.{metadata_word} + exec.note::metadata_into_attachment_header + # => [attachment_0_size, attachment_0_scheme, NOTE_ATTACHMENT] + drop + # => [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,7 +53,7 @@ async fn network_account_target_get_id() -> anyhow::Result<()> { movup.2 drop movup.2 drop end "#, - metadata_header = metadata_header, + metadata_word = metadata_word, attachment_word = attachment.content().to_word(), ); @@ -66,7 +74,6 @@ async fn network_account_target_new_attachment() -> anyhow::Result<()> { 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 source = format!( r#" @@ -78,7 +85,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,13 +98,12 @@ 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(); + let word = exec_output.stack.get_word(1).unwrap(); assert_eq!(word, attachment_word); Ok(()) @@ -122,11 +128,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 b16bc5a880..5424043464 100644 --- a/crates/miden-testing/src/utils.rs +++ b/crates/miden-testing/src/utils.rs @@ -7,7 +7,15 @@ use miden_protocol::account::AccountId; 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::note::{ + Note, + NoteAssets, + NoteAttachmentContent, + 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; @@ -186,22 +194,25 @@ 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 out = String::from("use miden::protocol::output_note\n\nbegin\n"); + let mut advice_map = AdviceMap::default(); for (idx, note) in output_notes.into_iter().enumerate() { anyhow::ensure!( @@ -238,20 +249,28 @@ 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 word_size = attachment.word_size(); + + out.push_str(&format!( + " + dup + push.{ATTACHMENT} + push.{word_size} + push.{attachment_scheme} + # => [attachment_scheme, word_size, ATTACHMENT, note_idx, note_idx] + exec.output_note::add_attachment + # => [note_idx] + ", + ATTACHMENT = attachment.content().to_word(), + attachment_scheme = attachment.attachment_scheme().as_u16(), + )); + + // For array attachments, add the elements to the advice map keyed by the commitment. + if let NoteAttachmentContent::Array(array) = attachment.content() { + advice_map.insert(array.commitment(), array.as_slice().to_vec()); + } + } for asset in note.assets().iter() { out.push_str(&format!( @@ -270,7 +289,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 e0a61d3e47..9b14ec6c38 100644 --- a/crates/miden-testing/tests/agglayer/bridge_out.rs +++ b/crates/miden-testing/tests/agglayer/bridge_out.rs @@ -191,7 +191,10 @@ async fn bridge_out_consecutive() -> anyhow::Result<()> { NoteType::Public, "BURN note should be public" ); - let attachment = burn_note.metadata().attachment(); + let attachment = burn_note + .attachments() + .get(0) + .expect("BURN note should have at least one attachment"); let network_target = miden_standards::note::NetworkAccountTarget::try_from(attachment) .expect("BURN note attachment should be a valid NetworkAccountTarget"); assert_eq!( diff --git a/crates/miden-testing/tests/scripts/faucet.rs b/crates/miden-testing/tests/scripts/faucet.rs index 675bdada2d..2c12071052 100644 --- a/crates/miden-testing/tests/scripts/faucet.rs +++ b/crates/miden-testing/tests/scripts/faucet.rs @@ -17,7 +17,7 @@ use miden_protocol::asset::{Asset, FungibleAsset}; use miden_protocol::note::{ Note, NoteAssets, - NoteAttachment, + NoteAttachments, NoteId, NoteMetadata, NoteRecipient, @@ -632,7 +632,7 @@ async fn network_faucet_mint() -> anyhow::Result<()> { faucet.id(), faucet_owner_account_id, mint_storage, - NoteAttachment::default(), + NoteAttachments::default(), &mut rng, )?; @@ -724,7 +724,7 @@ async fn test_network_faucet_owner_can_mint() -> anyhow::Result<()> { faucet.id(), owner_account_id, mint_inputs, - NoteAttachment::default(), + NoteAttachments::default(), &mut rng, )?; @@ -836,7 +836,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, )?; @@ -935,7 +935,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, )?; @@ -1269,7 +1269,7 @@ async fn network_faucet_burn() -> anyhow::Result<()> { faucet_owner_account_id, faucet.id(), fungible_asset.into(), - NoteAttachment::default(), + NoteAttachments::default(), &mut rng, )?; @@ -1370,7 +1370,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 a7989a81ef..92771fc89a 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/send_note.rs b/crates/miden-testing/tests/scripts/send_note.rs index 9859302858..325cc4b531 100644 --- a/crates/miden-testing/tests/scripts/send_note.rs +++ b/crates/miden-testing/tests/scripts/send_note.rs @@ -9,6 +9,7 @@ use miden_protocol::note::{ NoteAssets, NoteAttachment, NoteAttachmentScheme, + NoteAttachments, NoteMetadata, NoteRecipient, NoteStorage, @@ -61,17 +62,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 elements = [9, 8, 7, 6, 5, 4, 3, 2u32].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); let assets = NoteAssets::new(vec![sent_asset0, sent_asset1]).unwrap(); let note_script = CodeBuilder::default().compile_note_script("begin nop end").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; @@ -135,18 +136,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("begin nop end").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; From d209a779559d94b487b5d2fbc14e643754d43b6d Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Fri, 17 Apr 2026 15:54:04 +0200 Subject: [PATCH 12/48] chore: rename word_size to num_words --- .../asm/kernels/transaction/api.masm | 10 +- .../kernels/transaction/lib/output_note.masm | 96 +++++++++---------- crates/miden-protocol/asm/protocol/note.masm | 28 +++--- .../asm/protocol/output_note.masm | 20 ++-- .../asm/shared_utils/util/note.masm | 2 +- crates/miden-protocol/src/errors/mod.rs | 2 +- crates/miden-protocol/src/note/attachment.rs | 93 +++++++++--------- crates/miden-protocol/src/note/metadata.rs | 47 ++++----- .../attachments/network_account_target.masm | 6 +- .../src/account/interface/component.rs | 6 +- .../src/note/network_account_target.rs | 4 +- .../src/kernel_tests/tx/test_output_note.rs | 20 ++-- .../src/kernel_tests/tx/test_tx.rs | 6 +- .../src/standards/network_account_target.rs | 2 +- crates/miden-testing/src/utils.rs | 6 +- crates/miden-tx/src/host/tx_event.rs | 24 ++--- 16 files changed, 188 insertions(+), 184 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction/api.masm b/crates/miden-protocol/asm/kernels/transaction/api.masm index 1aaa115909..215ed9360b 100644 --- a/crates/miden-protocol/asm/kernels/transaction/api.masm +++ b/crates/miden-protocol/asm/kernels/transaction/api.masm @@ -1131,12 +1131,12 @@ end #! Adds an attachment to the note specified by the index. #! -#! Inputs: [attachment_scheme, attachment_word_size, ATTACHMENT, note_idx, pad(9)] +#! Inputs: [attachment_scheme, attachment_num_words, ATTACHMENT, note_idx, pad(9)] #! Outputs: [pad(16)] #! #! Where: #! - attachment_scheme is the user-defined scheme of the attachment. -#! - attachment_word_size is the size of the attachment in words. +#! - attachment_num_words is the number of words of the attachment. #! - ATTACHMENT is the attachment word to store. #! - note_idx is the index of the note to which the attachment is added. #! @@ -1144,18 +1144,18 @@ end #! - 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 exceeds 65534. -#! - the attachment word size exceeds 254 or is zero. +#! - the attachment num_words exceeds 254 or is zero. #! - the note already has 4 attachments. #! #! Invocation: dynexec pub proc output_note_add_attachment # assert that the provided note index is less than the total number of output notes dup.6 exec.output_note::assert_note_index_in_bounds drop - # => [attachment_scheme, attachment_word_size, ATTACHMENT, note_idx, pad(9)] + # => [attachment_scheme, attachment_num_words, ATTACHMENT, note_idx, pad(9)] # check that this procedure was executed against the native account exec.memory::assert_native_account - # => [attachment_scheme, attachment_word_size, ATTACHMENT, note_idx, pad(9)] + # => [attachment_scheme, attachment_num_words, ATTACHMENT, note_idx, pad(9)] exec.output_note::add_attachment # => [pad(16)] 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 353b06840a..97d94033f7 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm @@ -33,9 +33,9 @@ const ERR_OUTPUT_NOTE_INDEX_OUT_OF_BOUNDS="requested output note index should be const ERR_OUTPUT_NOTE_ATTACHMENT_SCHEME_MAX_EXCEEDED="attachment scheme must not exceed 65534" -const ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_MAX_EXCEEDED="attachment word size must not exceed 254" +const ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_MAX_EXCEEDED="attachment num_words must not exceed 254" -const ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_CANNOT_BE_ZERO="attachment word size cannot be zero" +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" @@ -231,73 +231,73 @@ end #! Adds an attachment to the note specified by the index. Attachments are append-only. #! -#! Inputs: [attachment_scheme, attachment_word_size, ATTACHMENT, note_idx] +#! Inputs: [attachment_scheme, attachment_num_words, ATTACHMENT, note_idx] #! Outputs: [] #! #! Where: #! - attachment_scheme is the user-defined scheme of the attachment. -#! - attachment_word_size is the size of the attachment in words. +#! - attachment_num_words is the number of words of the attachment. #! - ATTACHMENT is the attachment word to store. #! - note_idx is the index of the note to which the attachment is added. #! #! Panics if: #! - the attachment scheme exceeds 65534. -#! - the attachment word size exceeds 254 or is zero. +#! - the attachment num_words exceeds 254 or is zero. #! - 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_word_size, ATTACHMENT, note_idx] + # => [attachment_scheme, attachment_num_words, ATTACHMENT, note_idx] - # validate 0 < attachment_word_size <= 254 + # validate 0 < attachment_num_words <= 254 dup.1 neq.0 assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_CANNOT_BE_ZERO dup.1 u32assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_MAX_EXCEEDED u32lte.MAX_ATTACHMENT_WORD_SIZE assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_MAX_EXCEEDED - # => [attachment_scheme, attachment_word_size, ATTACHMENT, note_idx] + # => [attachment_scheme, attachment_num_words, ATTACHMENT, note_idx] # get note_ptr from note_idx movup.6 exec.memory::get_output_note_ptr - # => [note_ptr, attachment_scheme, attachment_word_size, ATTACHMENT] + # => [note_ptr, attachment_scheme, attachment_num_words, ATTACHMENT] # validate current number of attachments < 4 which means we can add one more to stay below the # limit dup exec.memory::get_output_note_num_attachments - # => [num_attachments, note_ptr, attachment_scheme, attachment_word_size, ATTACHMENT] + # => [num_attachments, note_ptr, attachment_scheme, attachment_num_words, ATTACHMENT] dup lt.MAX_ATTACHMENTS_PER_NOTE assert.err=ERR_OUTPUT_NOTE_TOO_MANY_ATTACHMENTS - # => [num_attachments, note_ptr, attachment_scheme, attachment_word_size, ATTACHMENT] + # => [num_attachments, note_ptr, attachment_scheme, attachment_num_words, ATTACHMENT] # emit event emit.NOTE_BEFORE_ADD_ATTACHMENT_EVENT - # => [num_attachments, note_ptr, attachment_scheme, attachment_word_size, ATTACHMENT] + # => [num_attachments, note_ptr, attachment_scheme, attachment_num_words, ATTACHMENT] swapw - # => [ATTACHMENT, num_attachments, note_ptr, attachment_scheme, attachment_word_size] + # => [ATTACHMENT, num_attachments, note_ptr, attachment_scheme, attachment_num_words] - # TODO(multi_attachments): Validate attachment + word size against advice data. + # TODO(multi_attachments): Validate attachment + num_words against advice data. # use attachment_idx = num_attachments dup.4 dup.6 - # => [note_ptr, attachment_idx, ATTACHMENT, num_attachments, note_ptr, attachment_scheme, attachment_word_size] + # => [note_ptr, attachment_idx, ATTACHMENT, num_attachments, note_ptr, attachment_scheme, attachment_num_words] exec.memory::set_output_note_attachment - # => [num_attachments, note_ptr, attachment_scheme, attachment_word_size] + # => [num_attachments, note_ptr, attachment_scheme, attachment_num_words] # compute new number of attachments # we have previously validated num_attachments to be < 4, so the new number is guaranteed to be # at most 4 dup add.1 - # => [new_num_attachments, num_attachments, note_ptr, attachment_scheme, attachment_word_size] + # => [new_num_attachments, num_attachments, note_ptr, attachment_scheme, attachment_num_words] # set new number of attachments in memory dup.2 exec.memory::set_output_note_num_attachments - # => [num_attachments, note_ptr, attachment_scheme, attachment_word_size] + # => [num_attachments, note_ptr, attachment_scheme, attachment_num_words] - # update attachment size and scheme in the note metadata + # update attachment num_words and scheme in the note metadata # note that this needs to take the previous number of attachments, so passing num_attachments # is correct - exec.set_attachment_size_and_scheme + exec.set_attachment_num_words_and_scheme # => [] end @@ -321,7 +321,7 @@ end #! Builds the provided inputs into the NOTE_METADATA_HEADER word. #! #! - The sender ID is set to the native account's ID. -#! - Attachment sizes and schemes are initialized to 0 (no attachments). +#! - Attachment num_words and schemes are initialized to 0 (no attachments). #! #! Inputs: [tag, note_type] #! Outputs: [NOTE_METADATA_HEADER] @@ -343,93 +343,93 @@ 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 sizes to 0 + # this implies the upper 32 bits are zero, which initializes the attachment num_words to 0 u32assert.err=ERR_NOTE_TAG_MUST_BE_U32 - # => [attachment_size_and_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, attachment_size_and_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, attachment_size_and_tag] + # => [sender_id_suffix_type_version, sender_id_prefix, attachment_num_words_and_tag] # Build metadata header. # -------------------------------------------------------------------------------------------- # push default absent attachment schemes (all zeros) push.ATTACHMENT_DEFAULT_SCHEMES movdn.3 - # => [sender_id_suffix_type_version, sender_id_prefix, attachment_size_and_tag, attachment_schemes] + # => [sender_id_suffix_type_version, sender_id_prefix, attachment_num_words_and_tag, attachment_schemes] # => [NOTE_METADATA_HEADER] end -#! Sets an output note's attachment word size and scheme in the note metadata header. +#! Sets an output note's attachment num_words and scheme in the note metadata header. #! -#! WARNING: The attachment size and scheme must be valid. +#! WARNING: The attachment num_words and scheme must be valid. #! -#! Inputs: [num_attachments, note_ptr, attachment_scheme, attachment_word_size] +#! Inputs: [num_attachments, note_ptr, attachment_scheme, attachment_num_words] #! 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_word_size is the size in words of the attachment content. +#! - attachment_num_words is the number of words of the attachment content. #! - note_ptr is the memory address at which the output note data begins. -proc set_attachment_size_and_scheme +proc set_attachment_num_words_and_scheme # the size and schemes are stored as follows in the second and third felt: # 2nd felt: [ - # att_3_size (8 bits) | att_2_size (8 bits) | att_1_size (8 bits) | - # att_0_size (8 bits) | note_tag (32 bits) + # attachment_3_num_words (8 bits) | attachment_2_num_words (8 bits) | attachment_1_num_words (8 bits) | + # attachment_0_num_words (8 bits) | note_tag (32 bits) # ] # 3rd felt: [ - # att_3_scheme (16 bits) | att_2_scheme (16 bits) | - # att_1_scheme (16 bits) | att_0_scheme (16 bits) + # attachment_3_scheme (16 bits) | attachment_2_scheme (16 bits) | + # attachment_1_scheme (16 bits) | attachment_0_scheme (16 bits) # ] - # -> the word_size needs to be shifted left by 32 + (num_attachments * 8) + # -> the num_words needs to be shifted left by 32 + (num_attachments * 8) # -> the scheme needs to be shifted left by num_attachments * 16 - # Prepare scheme and word size. + # Prepare scheme and num_words. # -------------------------------------------------------------------------------------------- # 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) movup.2 dup.1 - # => [num_attachments, attachment_scheme, num_attachments, note_ptr, attachment_word_size] + # => [num_attachments, attachment_scheme, num_attachments, note_ptr, attachment_num_words] mul.16 pow2 mul - # => [attachment_scheme_shifted, num_attachments, note_ptr, attachment_word_size] + # => [attachment_scheme_shifted, num_attachments, note_ptr, attachment_num_words] - # shift the word size left by num_attachments * 8 + 32 bits + # shift the num_words left by num_attachments * 8 + 32 bits # split into a u32shl (by num_attachments * 8, max 24) and a multiplication by 2^32 movup.3 movup.2 mul.8 - # => [num_attachments * 8, attachment_word_size, attachment_scheme_shifted, note_ptr] + # => [num_attachments * 8, attachment_num_words, attachment_scheme_shifted, note_ptr] u32shl mul.0x100000000 - # => [attachment_word_size_shifted, attachment_scheme_shifted, note_ptr] + # => [attachment_num_words_shifted, attachment_scheme_shifted, note_ptr] # Fetch and update metadata header. # -------------------------------------------------------------------------------------------- dup.2 exec.memory::get_output_note_metadata_header - # => [METADATA_HEADER, attachment_word_size_shifted, attachment_scheme_shifted, note_ptr] + # => [METADATA_HEADER, attachment_num_words_shifted, attachment_scheme_shifted, note_ptr] # => [ - # [sender_id_suffix_type_version, sender_id_prefix, attachment_size_and_tag, attachment_schemes], - # attachment_word_size_shifted, attachment_scheme_shifted, note_ptr + # [sender_id_suffix_type_version, sender_id_prefix, attachment_num_words_and_tag, attachment_schemes], + # attachment_num_words_shifted, attachment_scheme_shifted, note_ptr # ] - # merge word size into existing sizes and tag + # merge num_words into existing sizes and tag # (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.2 movup.4 add movdn.2 # => [ - # [sender_id_suffix_type_version, sender_id_prefix, new_attachment_size_and_tag, attachment_schemes], + # [sender_id_suffix_type_version, sender_id_prefix, new_attachment_num_words_and_tag, attachment_schemes], # attachment_scheme_shifted, note_ptr # ] @@ -438,7 +438,7 @@ proc set_attachment_size_and_scheme movup.3 movup.4 add movdn.3 # => [ - # [sender_id_suffix_type_version, sender_id_prefix, new_attachment_size_and_tag, new_attachment_schemes], + # [sender_id_suffix_type_version, sender_id_prefix, new_attachment_num_words_and_tag, new_attachment_schemes], # note_ptr # ] # => [METADATA_HEADER, note_ptr] diff --git a/crates/miden-protocol/asm/protocol/note.masm b/crates/miden-protocol/asm/protocol/note.masm index 8ccb624fed..b6d58a1836 100644 --- a/crates/miden-protocol/asm/protocol/note.masm +++ b/crates/miden-protocol/asm/protocol/note.masm @@ -196,40 +196,40 @@ pub proc metadata_into_sender # => [sender_id_suffix, sender_id_prefix] end -#! Extracts the first attachment's size 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: [attachment0_size, attachment0_scheme] +#! Outputs: [attachment_0_num_words, attachment_0_scheme] #! #! Where: #! - METADATA_HEADER is the metadata word of a note. -#! - attachment0_size is the word size of the first attachment (0 if absent). -#! - attachment0_scheme is the scheme of the first attachment. +#! - attachment_0_num_words is the number of words of the first attachment (0 if absent). +#! - attachment_0_scheme is the scheme of the first attachment. #! #! Invocation: exec pub proc metadata_into_attachment_header - # => [sender_id_suffix_type_version, sender_id_prefix, tag_and_sizes, schemes] + # => [sender_id_suffix_type_version, sender_id_prefix, tag_and_num_words, schemes] drop drop - # => [tag_and_sizes, schemes] + # => [tag_and_num_words, schemes] - # extract att_0_size from tag_and_sizes: - # u32split converts into sizes_packed (upper 32 bits) and tag (lower 32 bits) + # extract attachment_0_num_words from tag_and_num_words: + # u32split converts into num_words_packed (upper 32 bits) and tag (lower 32 bits) u32split swap drop - # => [sizes_packed, schemes] - # => [attachment3_size (8 bits) | attachment2_size (8 bits) | - # attachment1_size (8 bits) | attachment0_size (8 bits)] + # => [num_words_packed, schemes] + # => [attachment_3_num_words (8 bits) | attachment_2_num_words (8 bits) | + # attachment_1_num_words (8 bits) | attachment_0_num_words (8 bits)] - # extract attachment 0 size by taking only the lower 8 bits + # extract attachment 0 num_words by taking only the lower 8 bits u32and.0xff - # [attachment0_size, schemes] + # [attachment_0_num_words, schemes] # extract attachment 0 scheme by taking only the lower 16 bits swap u32and.0xffff swap - # [attachment0_size, attachment0_scheme] + # [attachment_0_num_words, attachment_0_scheme] end #! Extracts the note type from the provided metadata header. diff --git a/crates/miden-protocol/asm/protocol/output_note.masm b/crates/miden-protocol/asm/protocol/output_note.masm index ad8ace2b9a..d1b64531fa 100644 --- a/crates/miden-protocol/asm/protocol/output_note.masm +++ b/crates/miden-protocol/asm/protocol/output_note.masm @@ -147,10 +147,10 @@ end #! Adds an attachment to the note specified by the note index. #! -#! If attachment_word_size > 1, there must be an advice map entry for ATTACHMENT. +#! If attachment_num_words > 1, there must be an advice map entry for ATTACHMENT. #! #! Inputs: -#! Operand Stack: [attachment_scheme, attachment_word_size, ATTACHMENT, note_idx] +#! Operand Stack: [attachment_scheme, attachment_num_words, ATTACHMENT, note_idx] #! Advice map: { #! ATTACHMENT?: [[ATTACHMENT_ELEMENTS]], #! } @@ -158,12 +158,12 @@ end #! #! Where: #! - attachment_scheme is the user-defined scheme of the attachment. -#! - attachment_word_size is the number of words that make up the attachment. +#! - attachment_num_words is the number of words that make up the attachment. #! - ATTACHMENT is the attachment word to store or the commitment to an array of attachment #! elements. #! - note_idx is the index of the note to which the attachment is added. #! - ATTACHMENT_ELEMENTS are the elements for which ATTACHMENT is the sequential commitment (only -#! needed if attachment_word_size > 1). +#! needed if attachment_num_words > 1). #! #! Panics if: #! - the procedure is called when the active account is not the native one. @@ -174,11 +174,11 @@ end #! Invocation: exec pub proc add_attachment push.OUTPUT_NOTE_ADD_ATTACHMENT_OFFSET - # => [offset, attachment_scheme, attachment_word_size, ATTACHMENT, note_idx] + # => [offset, attachment_scheme, attachment_num_words, ATTACHMENT, note_idx] # pad the stack before the syscall padw padw swapdw - # => [offset, attachment_scheme, attachment_word_size, ATTACHMENT, note_idx, pad(8)] + # => [offset, attachment_scheme, attachment_num_words, ATTACHMENT, note_idx, pad(8)] syscall.exec_kernel_proc # => [pad(16)] @@ -206,7 +206,7 @@ end #! #! Invocation: exec pub proc add_word_attachment - # insert attachment_word_size = 1 after attachment_scheme + # insert attachment_num_words = 1 after attachment_scheme push.1 swap # => [attachment_scheme, 1, ATTACHMENT, note_idx] @@ -218,7 +218,7 @@ end #! commitment to a set of elements. #! #! Inputs: -#! Operand Stack: [attachment_scheme, attachment_word_size, ATTACHMENT, note_idx] +#! Operand Stack: [attachment_scheme, attachment_num_words, ATTACHMENT, note_idx] #! Advice map: { #! ATTACHMENT: [[ATTACHMENT_ELEMENTS]], #! } @@ -226,7 +226,7 @@ end #! #! Where: #! - attachment_scheme is the user-defined scheme of the attachment. -#! - attachment_word_size is the size of the attachment in words. +#! - attachment_num_words is the number of words of the attachment. #! - ATTACHMENT is the commitment of the set of elements that form the note attachment. #! - note_idx is the index of the note to which the attachment is added. #! - ATTACHMENT_ELEMENTS are the elements for which ATTACHMENT is the sequential commitment. @@ -236,7 +236,7 @@ end #! - the note index points to a non-existent output note. #! - the attachment scheme does not fit into a u16. #! - the note already has 4 attachments. -#! - the word_size of the attachment is not at least 2. +#! - the num_words of the attachment is not at least 2. #! #! Invocation: exec pub proc add_array_attachment diff --git a/crates/miden-protocol/asm/shared_utils/util/note.masm b/crates/miden-protocol/asm/shared_utils/util/note.masm index f059028719..b627a78fb5 100644 --- a/crates/miden-protocol/asm/shared_utils/util/note.masm +++ b/crates/miden-protocol/asm/shared_utils/util/note.masm @@ -16,5 +16,5 @@ pub const NOTE_TYPE_PUBLIC=1 # The maximum attachment scheme value. pub const MAX_ATTACHMENT_SCHEME=65534 -# The maximum attachment word size. +# The maximum number of words in an attachment. pub const MAX_ATTACHMENT_WORD_SIZE=254 diff --git a/crates/miden-protocol/src/errors/mod.rs b/crates/miden-protocol/src/errors/mod.rs index b7c395376f..48b41732ad 100644 --- a/crates/miden-protocol/src/errors/mod.rs +++ b/crates/miden-protocol/src/errors/mod.rs @@ -680,7 +680,7 @@ pub enum NoteError { NoteAttachmentArraySizeExceeded(usize), #[error( "attachment size {0} exceeds maximum {max}", - max = NoteAttachmentHeader::MAX_SIZE + max = NoteAttachmentHeader::MAX_NUM_WORDS )] NoteAttachmentHeaderSizeExceeded(u8), #[error("{0} attachments were provided but maximum is {max}", max = NoteAttachments::MAX_COUNT)] diff --git a/crates/miden-protocol/src/note/attachment.rs b/crates/miden-protocol/src/note/attachment.rs index 7cdb562fb4..d7ac923618 100644 --- a/crates/miden-protocol/src/note/attachment.rs +++ b/crates/miden-protocol/src/note/attachment.rs @@ -185,10 +185,10 @@ impl NoteAttachmentContent { /// /// - `1` for [`NoteAttachmentContent::Word`]. /// - `> 1` for [`NoteAttachmentContent::Array`]. - pub fn word_size(&self) -> u8 { + pub fn num_words(&self) -> u8 { match self { NoteAttachmentContent::Word(_) => 1, - NoteAttachmentContent::Array(array) => array.word_size(), + NoteAttachmentContent::Array(array) => array.num_words(), } } @@ -207,8 +207,8 @@ impl NoteAttachmentContent { impl Serializable for NoteAttachmentContent { fn write_into(&self, target: &mut W) { - // Write word_size as discriminant: 1 = Word, >1 = Array. - self.word_size().write_into(target); + // Write num_words as discriminant: 1 = Word, >1 = Array. + self.num_words().write_into(target); match self { NoteAttachmentContent::Word(word) => { @@ -236,11 +236,11 @@ impl Serializable for NoteAttachmentContent { impl Deserializable for NoteAttachmentContent { fn read_from(source: &mut R) -> Result { - let word_size = u8::read_from(source)?; + let num_words = u8::read_from(source)?; - match word_size { + match num_words { 0 => Err(DeserializationError::InvalidValue( - "attachment content word_size must be > 0".into(), + "attachment content num_words must be > 0".into(), )), 1 => { let word = Word::read_from(source)?; @@ -283,9 +283,10 @@ impl NoteAttachmentArray { /// The maximum number of elements in a note attachment array. /// - /// Each attachment can be at most [`NoteAttachmentHeader::MAX_SIZE`] words (254), and each + /// Each attachment can be at most [`NoteAttachmentHeader::MAX_NUM_WORDS`] words (254), and each /// word holds 4 elements, so the maximum number of elements is 254 * 4 = 1016. - pub const MAX_NUM_ELEMENTS: u16 = NoteAttachmentHeader::MAX_SIZE as u16 * (WORD_SIZE as u16); + pub const MAX_NUM_ELEMENTS: u16 = + NoteAttachmentHeader::MAX_NUM_WORDS as u16 * (WORD_SIZE as u16); // CONSTRUCTORS // -------------------------------------------------------------------------------------------- @@ -328,13 +329,13 @@ impl NoteAttachmentArray { u16::try_from(self.elements.len()).expect("type should enforce that size fits in u16") } - /// Returns the number of elements this note attachment commits to. - pub fn word_size(&self) -> u8 { + /// Returns the number of words this note attachment commits to. + pub fn num_words(&self) -> u8 { // SAFETY: // - num elements is at most 1016 and 1016/4 = 254, so it fits in a u8 // - constructor checks that num elements is a multiple of WORD_SIZE, so we don't need to // check the remainder - u8::try_from(self.elements.len() / WORD_SIZE).expect("word size shoult fit in u8") + u8::try_from(self.elements.len() / WORD_SIZE).expect("num words should fit in u8") } /// Returns the commitment over the contained field elements. @@ -476,26 +477,26 @@ impl Deserializable for NoteAttachmentScheme { /// The header metadata for a single note attachment. /// -/// Contains the scheme and word size of an attachment, without the actual content data. -/// The kind of attachment is inferred from the size: -/// - `size == 0`: absent (no attachment) -/// - `size == 1`: word attachment (a single [`Word`]) -/// - `size > 1`: array attachment (a commitment to a set of felts) +/// Contains the scheme and number of words of an attachment, without the actual content data. +/// The kind of attachment is inferred from the number of words: +/// - `num_words == 0`: absent (no attachment) +/// - `num_words == 1`: word attachment (a single [`Word`]) +/// - `num_words > 1`: array attachment (a commitment to a set of felts) #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct NoteAttachmentHeader { scheme: NoteAttachmentScheme, - word_size: u8, + num_words: u8, } impl NoteAttachmentHeader { // CONSTANTS // -------------------------------------------------------------------------------------------- - /// The maximum attachment size in words. + /// The maximum number of words in an attachment. /// - /// Limited to 254 to ensure the size fits into a u8 and the felt encoding remains valid - /// when four sizes are packed into a single felt in the note metadata. - pub const MAX_SIZE: u8 = 254; + /// Limited to 254 to ensure the value fits into a u8 and the felt encoding remains valid + /// when four num_words values are packed into a single felt in the note metadata. + pub const MAX_NUM_WORDS: u8 = 254; // CONSTRUCTORS // -------------------------------------------------------------------------------------------- @@ -504,19 +505,19 @@ impl NoteAttachmentHeader { /// /// # Errors /// - /// Returns an error if `size` exceeds [`Self::MAX_SIZE`]. - pub fn new(scheme: NoteAttachmentScheme, word_size: u8) -> Result { - if word_size > Self::MAX_SIZE { - return Err(NoteError::NoteAttachmentHeaderSizeExceeded(word_size)); + /// Returns an error if `num_words` exceeds [`Self::MAX_NUM_WORDS`]. + pub fn new(scheme: NoteAttachmentScheme, num_words: u8) -> Result { + if num_words > Self::MAX_NUM_WORDS { + return Err(NoteError::NoteAttachmentHeaderSizeExceeded(num_words)); } - Ok(Self { scheme, word_size }) + Ok(Self { scheme, num_words }) } /// Returns a header representing the absence of an attachment. pub const fn absent() -> Self { Self { scheme: NoteAttachmentScheme::none(), - word_size: 0, + num_words: 0, } } @@ -528,14 +529,14 @@ impl NoteAttachmentHeader { self.scheme } - /// Returns the attachment size in words. - pub const fn word_size(&self) -> u8 { - self.word_size + /// Returns the number of words in the attachment. + pub const fn num_words(&self) -> u8 { + self.num_words } /// Returns `true` if this header represents an absent attachment, `false` otherwise. pub const fn is_absent(&self) -> bool { - self.word_size == 0 && self.scheme.is_none() + self.num_words == 0 && self.scheme.is_none() } } @@ -548,7 +549,7 @@ impl Default for NoteAttachmentHeader { impl Serializable for NoteAttachmentHeader { fn write_into(&self, target: &mut W) { self.scheme.write_into(target); - self.word_size.write_into(target); + self.num_words.write_into(target); } fn get_size_hint(&self) -> usize { @@ -618,7 +619,7 @@ impl NoteAttachments { let total_num_words = attachments .iter() - .map(|attachment| attachment.word_size() as usize) + .map(|attachment| attachment.num_words() as usize) .sum::(); if total_num_words > Self::MAX_NUM_WORDS as usize { @@ -666,11 +667,11 @@ impl NoteAttachments { 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(), attachment.word_size()) - .expect( - "attachment word_size should not exceed NoteAttachmentHeader::MAX_SIZE", - ); + headers[i] = NoteAttachmentHeader::new( + attachment.attachment_scheme(), + attachment.num_words(), + ) + .expect("attachment num_words should not exceed NoteAttachmentHeader::MAX_NUM_WORDS"); } headers } @@ -840,7 +841,7 @@ mod tests { fn note_attachment_header_absent() { let header = NoteAttachmentHeader::absent(); assert!(header.is_absent()); - assert_eq!(header.word_size(), 0); + assert_eq!(header.num_words(), 0); assert!(header.scheme().is_none()); } @@ -904,9 +905,9 @@ mod tests { let headers = attachments.to_headers(); assert_eq!(headers[0].scheme(), NoteAttachmentScheme::new(42)?); - assert_eq!(headers[0].word_size(), 1); + assert_eq!(headers[0].num_words(), 1); assert_eq!(headers[1].scheme(), NoteAttachmentScheme::new(100)?); - assert_eq!(headers[1].word_size(), 2); // 8 felts = 2 words + assert_eq!(headers[1].num_words(), 2); // 8 felts = 2 words assert!(headers[2].is_absent()); assert!(headers[3].is_absent()); @@ -925,17 +926,17 @@ mod tests { } #[test] - fn note_attachment_word_size() { + fn note_attachment_num_words() { // Word => 1 let word = NoteAttachmentContent::new_word(Word::from([1, 2, 3, 4u32])); - assert_eq!(word.word_size(), 1); + assert_eq!(word.num_words(), 1); // Array with 8 elements => 8/4 = 2 let array = NoteAttachmentContent::new_array(vec![Felt::new(1); 8]).unwrap(); - assert_eq!(array.word_size(), 2); + assert_eq!(array.num_words(), 2); // Array with 12 elements => 12/4 = 3 let array = NoteAttachmentContent::new_array(vec![Felt::new(1); 12]).unwrap(); - assert_eq!(array.word_size(), 3); + assert_eq!(array.num_words(), 3); } } diff --git a/crates/miden-protocol/src/note/metadata.rs b/crates/miden-protocol/src/note/metadata.rs index aad001f5ca..25b5248156 100644 --- a/crates/miden-protocol/src/note/metadata.rs +++ b/crates/miden-protocol/src/note/metadata.rs @@ -128,16 +128,19 @@ impl Deserializable for NoteMetadata { /// ```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: [att_3_size (8b) | att_2_size (8b) | att_1_size (8b) | att_0_size (8b) | note_tag (32b)] -/// 3rd felt: [att_3_scheme (16b) | att_2_scheme (16b) | att_1_scheme (16b) | att_0_scheme (16b)] +/// 2nd felt: [attachment_3_num_words (8 bits) | attachment_2_num_words (8 bits) | +/// attachment_1_num_words (8 bits) | attachment_0_num_words (8 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: Max value is `0xFEFEFEFE_FFFFFFFF` (sizes capped at 254, tag at u32::MAX), which is -/// less than the Goldilocks prime `p = 2^64 - 2^32 + 1`. +/// - 2nd felt: Max value is `0xFEFEFEFE_FFFFFFFF` (num_words capped at 254, tag at u32::MAX), which +/// is less than the Goldilocks prime `p = 2^64 - 2^32 + 1`. /// - 3rd felt: Max value is `0xFFFEFFFE_FFFEFFFE` (schemes capped at 65534), which is less than /// `p`. /// @@ -206,7 +209,7 @@ impl NoteMetadataHeader { /// /// See [`NoteMetadataHeader`] docs for the layout. pub fn to_metadata_word(&self) -> Word { - let (sizes, schemes) = extract_sizes_and_schemes(&self.attachment_headers); + let (num_words, schemes) = extract_num_words_and_schemes(&self.attachment_headers); let mut word = Word::empty(); word[0] = merge_sender_suffix_and_note_type( @@ -214,7 +217,7 @@ impl NoteMetadataHeader { self.metadata.note_type, ); word[1] = self.metadata.sender.prefix().as_felt(); - word[2] = merge_tag_and_sizes(self.metadata.tag, sizes); + word[2] = merge_tag_and_num_words(self.metadata.tag, num_words); word[3] = merge_schemes(schemes); word } @@ -270,20 +273,20 @@ impl Deserializable for NoteMetadataHeader { // HELPER FUNCTIONS // ================================================================================================ -/// Extracts the sizes and schemes arrays from the attachment headers. -fn extract_sizes_and_schemes( +/// Extracts the num_words and schemes arrays from the attachment headers. +fn extract_num_words_and_schemes( headers: &[NoteAttachmentHeader; NoteAttachments::MAX_COUNT], ) -> ( [u8; NoteAttachments::MAX_COUNT], [NoteAttachmentScheme; NoteAttachments::MAX_COUNT], ) { - let mut word_sizes = [0u8; NoteAttachments::MAX_COUNT]; + let mut num_words = [0u8; NoteAttachments::MAX_COUNT]; let mut schemes = [NoteAttachmentScheme::none(); NoteAttachments::MAX_COUNT]; for (i, header) in headers.iter().enumerate() { - word_sizes[i] = header.word_size(); + num_words[i] = header.num_words(); schemes[i] = header.scheme(); } - (word_sizes, schemes) + (num_words, schemes) } /// Merges the suffix of an [`AccountId`] and note metadata into a single [`Felt`]. @@ -312,23 +315,23 @@ fn merge_sender_suffix_and_note_type(sender_id_suffix: Felt, note_type: NoteType Felt::try_from(merged).expect("encoded value should be a valid felt") } -/// Merges the note tag and four attachment sizes into a single [`Felt`]. +/// Merges the note tag and four attachment num_words into a single [`Felt`]. /// /// The layout is as follows: /// /// ```text -/// [att_3_size (8b) | att_2_size (8b) | att_1_size (8b) | att_0_size (8b) | note_tag (32b)] +/// [attachment_3_num_words (8 bits) | attachment_2_num_words (8 bits) | +/// attachment_1_num_words (8 bits) | attachment_0_num_words (8 bits) | +/// note_tag (32 bits)] /// ``` -/// -/// Max value: `0xFEFEFEFE_FFFFFFFF` < p (Goldilocks prime). Sizes are capped at 254. -fn merge_tag_and_sizes(tag: NoteTag, sizes: [u8; 4]) -> Felt { +fn merge_tag_and_num_words(tag: NoteTag, num_words: [u8; 4]) -> Felt { let mut merged: u64 = u32::from(tag) as u64; - merged |= (sizes[0] as u64) << 32; - merged |= (sizes[1] as u64) << 40; - merged |= (sizes[2] as u64) << 48; - merged |= (sizes[3] as u64) << 56; + merged |= (num_words[0] as u64) << 32; + merged |= (num_words[1] as u64) << 40; + merged |= (num_words[2] as u64) << 48; + merged |= (num_words[3] as u64) << 56; - Felt::try_from(merged).expect("encoded value should be a valid felt (sizes <= 254)") + Felt::try_from(merged).expect("encoded value should be a valid felt (num_words <= 254)") } /// Merges four attachment schemes into a single [`Felt`]. @@ -336,7 +339,7 @@ fn merge_tag_and_sizes(tag: NoteTag, sizes: [u8; 4]) -> Felt { /// The layout is as follows: /// /// ```text -/// [att_3_scheme (16b) | att_2_scheme (16b) | att_1_scheme (16b) | att_0_scheme (16b)] +/// [attachment_3_scheme (16 bits) | attachment_2_scheme (16 bits) | attachment_1_scheme (16 bits) | attachment_0_scheme (16 bits)] /// ``` /// /// Max value: `0xFFFEFFFE_FFFEFFFE` < p. Schemes are capped at 65534. 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 c225c7e653..23130d509b 100644 --- a/crates/miden-standards/asm/standards/attachments/network_account_target.masm +++ b/crates/miden-standards/asm/standards/attachments/network_account_target.masm @@ -97,11 +97,11 @@ pub proc active_account_matches_target_account # => [METADATA_HEADER, NOTE_ATTACHMENT] exec.note::metadata_into_attachment_header - # => [attachment0_size, attachment0_scheme, NOTE_ATTACHMENT] + # => [attachment_0_num_words, attachment_0_scheme, NOTE_ATTACHMENT] - # make sure the word size of the attachment is 1 + # make sure the num_words of the attachment is 1 eq.1 assert.err=ERR_NOT_NETWORK_ACCOUNT_TARGET - # => [attachment0_scheme, NOTE_ATTACHMENT] + # => [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/src/account/interface/component.rs b/crates/miden-standards/src/account/interface/component.rs index 4155008023..0d784b3d28 100644 --- a/crates/miden-standards/src/account/interface/component.rs +++ b/crates/miden-standards/src/account/interface/component.rs @@ -263,14 +263,14 @@ impl AccountComponentInterface { Some(attachment) => { let attachment_word = attachment.content().to_word(); let attachment_scheme = attachment.attachment_scheme().as_u16() as u32; - let word_size = attachment.content().word_size(); + let num_words = attachment.content().num_words(); body.push_str(&format!( " push.{attachment_word} - push.{word_size} + push.{num_words} push.{attachment_scheme} - # => [attachment_scheme, word_size, ATTACHMENT, note_idx, pad(16)] + # => [attachment_scheme, num_words, ATTACHMENT, note_idx, pad(16)] exec.::miden::protocol::output_note::add_attachment # => [pad(16)] ", diff --git a/crates/miden-standards/src/note/network_account_target.rs b/crates/miden-standards/src/note/network_account_target.rs index 0470b9adf9..8acd5ba4a3 100644 --- a/crates/miden-standards/src/note/network_account_target.rs +++ b/crates/miden-standards/src/note/network_account_target.rs @@ -126,7 +126,7 @@ impl TryFrom<&NoteAttachment> for NetworkAccountTarget { NetworkAccountTarget::new(target_id, exec_hint) }, _ => Err(NetworkAccountTargetError::AttachmentContentNotWord( - attachment.content().word_size(), + attachment.content().num_words(), )), } } @@ -146,7 +146,7 @@ pub enum NetworkAccountTargetError { expected = NetworkAccountTarget::ATTACHMENT_SCHEME )] AttachmentSchemeMismatch(NoteAttachmentScheme), - #[error("attachment content is not a Word (word_size={0}, expected 1)")] + #[error("attachment content is not a Word (num_words={0}, expected 1)")] AttachmentContentNotWord(u8), #[error("failed to decode target account ID")] DecodeTargetId(#[source] AccountIdError), 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 c1b231ef4c..45bc71135c 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 @@ -296,9 +296,9 @@ async fn test_get_output_notes_commitment() -> anyhow::Result<()> { # => [note_idx] push.{ATTACHMENT2} - push.{attachment2_word_size} + push.{attachment2_num_words} push.{attachment_scheme2} - # => [attachment_scheme, attachment_word_size, ATTACHMENT, note_idx] + # => [attachment_scheme, attachment_num_words, ATTACHMENT, note_idx] exec.output_note::add_array_attachment # => [] @@ -321,7 +321,7 @@ async fn test_get_output_notes_commitment() -> anyhow::Result<()> { ASSET_2_KEY = asset_2.to_key_word(), ASSET_2_VALUE = asset_2.to_value_word(), ATTACHMENT2 = output_note_2.attachments().get(0).unwrap().content().to_word(), - attachment2_word_size = output_note_2.attachments().get(0).unwrap().word_size(), + attachment2_num_words = output_note_2.attachments().get(0).unwrap().num_words(), attachment_scheme2 = output_note_2.attachments().get(0).unwrap().attachment_scheme().as_u16(), ); @@ -1144,7 +1144,7 @@ async fn test_get_assets() -> anyhow::Result<()> { } #[tokio::test] -async fn test_add_attachment_with_zero_word_size_fails() -> anyhow::Result<()> { +async fn test_add_attachment_with_zero_num_words_fails() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; let code = format!( @@ -1162,9 +1162,9 @@ async fn test_add_attachment_with_zero_word_size_fails() -> anyhow::Result<()> { exec.output_note::create # => [note_idx] - # try to add an attachment with word_size = 0 + # try to add an attachment with num_words = 0 padw push.0 push.0 - # => [attachment_scheme, attachment_word_size, ATTACHMENT, note_idx] + # => [attachment_scheme, attachment_num_words, ATTACHMENT, note_idx] exec.output_note::add_array_attachment # => [] end @@ -1242,7 +1242,7 @@ async fn test_set_array_attachment() -> anyhow::Result<()> { RawOutputNote::Full(NoteBuilder::new(account.id(), rng).attachment(attachment).build()?); let attachment_content_word = output_note.attachments().get(0).unwrap().content().to_word(); - let attachment_word_size = output_note.attachments().get(0).unwrap().word_size(); + let attachment_num_words = output_note.attachments().get(0).unwrap().num_words(); let tx_script = format!( " use miden::protocol::output_note @@ -1255,9 +1255,9 @@ async fn test_set_array_attachment() -> anyhow::Result<()> { # => [note_idx] push.{ATTACHMENT} - push.{attachment_word_size} + push.{attachment_num_words} push.{attachment_scheme} - # => [attachment_scheme, attachment_word_size, ATTACHMENT, note_idx] + # => [attachment_scheme, attachment_num_words, ATTACHMENT, note_idx] exec.output_note::add_array_attachment # => [] @@ -1269,7 +1269,7 @@ async fn test_set_array_attachment() -> anyhow::Result<()> { note_type = output_note.metadata().note_type() as u8, tag = output_note.metadata().tag().as_u32(), attachment_scheme = output_note.attachments().get(0).unwrap().attachment_scheme().as_u16(), - attachment_word_size = attachment_word_size, + attachment_num_words = attachment_num_words, ATTACHMENT = attachment_content_word, ); 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 c5780a76ac..5dafc1f085 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_tx.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_tx.rs @@ -319,9 +319,9 @@ async fn executed_transaction_output_notes() -> anyhow::Result<()> { # => [note_idx = 2] push.{ATTACHMENT3} - push.{attachment3_word_size} + push.{attachment3_num_words} push.{attachment_scheme3} - # => [attachment_scheme, attachment_word_size, ATTACHMENT, note_idx] + # => [attachment_scheme, attachment_num_words, ATTACHMENT, note_idx] exec.output_note::add_array_attachment # => [] end @@ -342,7 +342,7 @@ async fn executed_transaction_output_notes() -> anyhow::Result<()> { attachment_scheme2 = attachment2.attachment_scheme().as_u16(), ATTACHMENT2 = attachment2.content().to_word(), attachment_scheme3 = attachment3.attachment_scheme().as_u16(), - attachment3_word_size = attachment3.word_size(), + attachment3_num_words = attachment3.num_words(), ATTACHMENT3 = attachment3.content().to_word(), ); diff --git a/crates/miden-testing/src/standards/network_account_target.rs b/crates/miden-testing/src/standards/network_account_target.rs index f95758648a..ef8b10c011 100644 --- a/crates/miden-testing/src/standards/network_account_target.rs +++ b/crates/miden-testing/src/standards/network_account_target.rs @@ -40,7 +40,7 @@ async fn network_account_target_get_id() -> anyhow::Result<()> { push.{attachment_word} push.{metadata_word} exec.note::metadata_into_attachment_header - # => [attachment_0_size, attachment_0_scheme, NOTE_ATTACHMENT] + # => [attachment_0_num_words, attachment_0_scheme, NOTE_ATTACHMENT] drop # => [attachment_0_scheme, NOTE_ATTACHMENT] exec.network_account_target::is_network_account_target diff --git a/crates/miden-testing/src/utils.rs b/crates/miden-testing/src/utils.rs index 5424043464..95603c8b48 100644 --- a/crates/miden-testing/src/utils.rs +++ b/crates/miden-testing/src/utils.rs @@ -250,15 +250,15 @@ fn note_script_that_creates_notes<'note>( )); for attachment in note.attachments().iter() { - let word_size = attachment.word_size(); + let num_words = attachment.num_words(); out.push_str(&format!( " dup push.{ATTACHMENT} - push.{word_size} + push.{num_words} push.{attachment_scheme} - # => [attachment_scheme, word_size, ATTACHMENT, note_idx, note_idx] + # => [attachment_scheme, num_words, ATTACHMENT, note_idx, note_idx] exec.output_note::add_attachment # => [note_idx] ", diff --git a/crates/miden-tx/src/host/tx_event.rs b/crates/miden-tx/src/host/tx_event.rs index 518d9ccf22..c4b6214992 100644 --- a/crates/miden-tx/src/host/tx_event.rs +++ b/crates/miden-tx/src/host/tx_event.rs @@ -427,17 +427,17 @@ impl TransactionEvent { TransactionEventId::NoteBeforeAddAttachment => { // Expected stack state: [ // event, num_attachments, note_ptr, attachment_scheme, - // attachment_word_size, ATTACHMENT, note_idx + // attachment_num_words, ATTACHMENT, note_idx // ] let note_ptr = process.get_stack_item(2); let attachment_scheme = process.get_stack_item(3); - let attachment_word_size = process.get_stack_item(4); + let attachment_num_words = process.get_stack_item(4); let attachment = process.get_stack_word(5); let (note_idx, attachment) = extract_note_attachment( attachment_scheme, - attachment_word_size, + attachment_num_words, attachment, note_ptr, process.advice_provider(), @@ -736,15 +736,15 @@ fn build_note_metadata( fn extract_note_attachment( attachment_scheme: Felt, - attachment_word_size: Felt, + attachment_num_words: Felt, attachment: Word, note_ptr: Felt, advice_provider: &AdviceProvider, ) -> Result<(usize, NoteAttachment), TransactionKernelError> { let note_idx = note_ptr_to_idx(note_ptr)?; - let word_size = u8::try_from(attachment_word_size.as_canonical_u64()).map_err(|_| { - TransactionKernelError::other("failed to convert attachment word size to u8") + let num_words = u8::try_from(attachment_num_words.as_canonical_u64()).map_err(|_| { + TransactionKernelError::other("failed to convert attachment num_words to u8") })?; let attachment_scheme = u16::try_from(attachment_scheme.as_canonical_u64()) @@ -758,9 +758,9 @@ fn extract_note_attachment( }) })?; - let attachment_content = match word_size { + let attachment_content = match num_words { 0 => { - return Err(TransactionKernelError::other("attachment word_size must be > 0")); + return Err(TransactionKernelError::other("attachment num_words must be > 0")); }, 1 => NoteAttachmentContent::Word(attachment), _ => { @@ -785,11 +785,11 @@ fn extract_note_attachment( }); } - if array_attachment.word_size() != word_size { + if array_attachment.num_words() != num_words { return Err(TransactionKernelError::other(format!( - "array attachment word_size {} does not match declared word size {}", - array_attachment.word_size(), - word_size + "array attachment num_words {} does not match declared num_words {}", + array_attachment.num_words(), + num_words ))); } From 69a365455a4bba8b70ea554fc3afb923998097e5 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Fri, 17 Apr 2026 18:43:07 +0200 Subject: [PATCH 13/48] chore: refactor array attachment to contain Vec Also optimizes metadata header serialization. --- crates/miden-protocol/src/errors/mod.rs | 15 +- crates/miden-protocol/src/note/attachment.rs | 167 ++++++++---------- crates/miden-protocol/src/note/metadata.rs | 36 +++- .../src/transaction/kernel/advice_inputs.rs | 2 +- .../src/account/interface/mod.rs | 3 +- .../src/kernel_tests/tx/test_output_note.rs | 7 +- .../src/kernel_tests/tx/test_tx.rs | 4 +- crates/miden-testing/src/utils.rs | 2 +- .../miden-testing/tests/scripts/send_note.rs | 4 +- crates/miden-tx/src/host/tx_event.rs | 19 +- 10 files changed, 128 insertions(+), 131 deletions(-) diff --git a/crates/miden-protocol/src/errors/mod.rs b/crates/miden-protocol/src/errors/mod.rs index 48b41732ad..b8c24697b1 100644 --- a/crates/miden-protocol/src/errors/mod.rs +++ b/crates/miden-protocol/src/errors/mod.rs @@ -56,7 +56,6 @@ use crate::{ MAX_NOTE_STORAGE_ITEMS, MAX_OUTPUT_NOTES_PER_TX, NOTE_MAX_SIZE, - WORD_SIZE, }; #[cfg(any(feature = "testing", test))] @@ -666,18 +665,16 @@ pub enum NoteError { InvalidNoteStorageLength { expected: usize, actual: usize }, #[error("note tag requires a public note but the note is of type {0}")] PublicNoteRequired(NoteType), - #[error("note attachment array length {0} is not divisible by {WORD_SIZE}")] - NoteAttachmentArrayNotWordAligned(usize), #[error( - "note attachment array must have at least {min} elements, got {0}", - min = NoteAttachmentArray::MIN_NUM_ELEMENTS + "note attachment array must have at least {min} words, got {0}", + min = NoteAttachmentArray::MIN_NUM_WORDS )] - NoteAttachmentArrayTooFewElements(usize), + NoteAttachmentArrayTooFewWords(usize), #[error( - "note attachment array contains {0} elements, but the maximum is {max} elements", - max = NoteAttachmentArray::MAX_NUM_ELEMENTS + "note attachment array contains {0} words, but the maximum is {max} words", + max = NoteAttachmentHeader::MAX_NUM_WORDS )] - NoteAttachmentArraySizeExceeded(usize), + NoteAttachmentArrayTooManyWords(usize), #[error( "attachment size {0} exceeds maximum {max}", max = NoteAttachmentHeader::MAX_NUM_WORDS diff --git a/crates/miden-protocol/src/note/attachment.rs b/crates/miden-protocol/src/note/attachment.rs index d7ac923618..e734ea1460 100644 --- a/crates/miden-protocol/src/note/attachment.rs +++ b/crates/miden-protocol/src/note/attachment.rs @@ -67,19 +67,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 number of elements exceeds [`NoteAttachmentArray::MAX_NUM_ELEMENTS`]. - /// - The number of elements is less than [`NoteAttachmentArray::MIN_NUM_ELEMENTS`]. + /// - The number of words is less than [`NoteAttachmentArray::MIN_NUM_WORDS`]. + /// - The number of words exceeds [`NoteAttachmentHeader::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 @@ -157,15 +156,15 @@ impl NoteAttachmentContent { 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 number of elements exceeds [`NoteAttachmentArray::MAX_NUM_ELEMENTS`]. - /// - The number of elements is less than [`NoteAttachmentArray::MIN_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 [`NoteAttachmentHeader::MAX_NUM_WORDS`]. + pub fn new_array(words: Vec) -> Result { + NoteAttachmentArray::new(words).map(Self::from) } // ACCESSORS @@ -214,9 +213,8 @@ impl Serializable for NoteAttachmentContent { NoteAttachmentContent::Word(word) => { word.write_into(target); }, - NoteAttachmentContent::Array(arr) => { - arr.num_elements().write_into(target); - target.write_many(&arr.elements); + NoteAttachmentContent::Array(array) => { + target.write_many(array.as_words()); }, } } @@ -226,9 +224,7 @@ impl Serializable for NoteAttachmentContent { match self { NoteAttachmentContent::Word(word) => discriminant_size + word.get_size_hint(), NoteAttachmentContent::Array(array) => { - discriminant_size - + array.num_elements().get_size_hint() - + array.elements.len() * Felt::ZERO.get_size_hint() + discriminant_size + usize::from(array.num_words()) * Word::empty().get_size_hint() }, } } @@ -247,10 +243,9 @@ impl Deserializable for NoteAttachmentContent { Ok(NoteAttachmentContent::Word(word)) }, _ => { - let num_elements = u16::read_from(source)?; - let elements = - source.read_many_iter(usize::from(num_elements))?.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())) }, } @@ -260,14 +255,10 @@ impl Deserializable for NoteAttachmentContent { // NOTE ATTACHMENT ARRAY // ================================================================================================ -/// The type contained in [`NoteAttachmentContent::Array`] that commits to a set of field -/// elements. -/// -/// The number of elements must be divisible by [`WORD_SIZE`], i.e. the array must contain only -/// whole words. +/// 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, } @@ -275,70 +266,60 @@ impl NoteAttachmentArray { // CONSTANTS // -------------------------------------------------------------------------------------------- - /// The minimum number of elements in a note attachment array. - /// - /// Array attachments must contain at least 2 words (8 elements) to distinguish them from word - /// attachments. - pub const MIN_NUM_ELEMENTS: u8 = (WORD_SIZE as u8) * 2; - - /// The maximum number of elements in a note attachment array. + /// The minimum number of words in a note attachment array. /// - /// Each attachment can be at most [`NoteAttachmentHeader::MAX_NUM_WORDS`] words (254), and each - /// word holds 4 elements, so the maximum number of elements is 254 * 4 = 1016. - pub const MAX_NUM_ELEMENTS: u16 = - NoteAttachmentHeader::MAX_NUM_WORDS as u16 * (WORD_SIZE as u16); + /// 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 number of elements is not a multiple of [`WORD_SIZE`]. - /// - The number of elements is less than [`Self::MIN_NUM_ELEMENTS`]. - /// - The number of elements exceeds [`Self::MAX_NUM_ELEMENTS`]. - pub fn new(elements: Vec) -> Result { - if !elements.len().is_multiple_of(WORD_SIZE) { - return Err(NoteError::NoteAttachmentArrayNotWordAligned(elements.len())); - } - - if elements.len() < Self::MIN_NUM_ELEMENTS as usize { - return Err(NoteError::NoteAttachmentArrayTooFewElements(elements.len())); + /// - The number of words is less than [`Self::MIN_NUM_WORDS`]. + /// - The number of words exceeds [`NoteAttachmentHeader::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 elements.len() > Self::MAX_NUM_ELEMENTS as usize { - return Err(NoteError::NoteAttachmentArraySizeExceeded(elements.len())); + if words.len() > NoteAttachmentHeader::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 number of words this note attachment commits to. + /// 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) -> u8 { - // SAFETY: - // - num elements is at most 1016 and 1016/4 = 254, so it fits in a u8 - // - constructor checks that num elements is a multiple of WORD_SIZE, so we don't need to - // check the remainder - u8::try_from(self.elements.len() / WORD_SIZE).expect("num words should fit in u8") + // SAFETY: constructor checks that num_words is less than 255 + u8::try_from(self.words.len()).expect("num words should fit in u8") } - /// Returns the commitment over the contained field elements. + /// Returns the commitment over the contained words. pub fn commitment(&self) -> Word { self.commitment } @@ -348,7 +329,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 { @@ -711,8 +692,8 @@ fn attachments_to_elements(attachments: &[NoteAttachment]) -> Vec { NoteAttachmentContent::Word(word) => { elements.extend_from_slice(word.as_elements()); }, - NoteAttachmentContent::Array(arr) => { - elements.extend_from_slice(arr.as_slice()); + NoteAttachmentContent::Array(array) => { + elements.extend(array.as_elements()); }, } } @@ -781,7 +762,7 @@ mod tests { #[case::attachment_word(NoteAttachment::new_word(NoteAttachmentScheme::new(1)?, Word::from([3, 4, 5, 6u32])))] #[case::attachment_array(NoteAttachment::new_array( NoteAttachmentScheme::MAX, - vec![Felt::new(1); 8], + vec![Word::from([1, 1, 1, 1u32]); 2], )?)] #[test] fn note_attachment_serde(#[case] attachment: NoteAttachment) -> anyhow::Result<()> { @@ -790,31 +771,23 @@ mod tests { } #[test] - fn note_attachment_array_fails_on_too_many_elements() -> anyhow::Result<()> { - let too_many_elements = (NoteAttachmentArray::MAX_NUM_ELEMENTS as usize) + 4; - 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 = NoteAttachmentHeader::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_array_fails_on_too_few_elements() { - let elements = vec![Felt::from(1u32); 4]; - let err = NoteAttachmentArray::new(elements).unwrap_err(); - // Arrays must have at least MIN_NUM_ELEMENTS (8) to distinguish from word attachments. - assert_matches!(err, NoteError::NoteAttachmentArrayTooFewElements(4)); - } - - #[test] - fn note_attachment_array_fails_on_non_word_aligned_length() { - let elements = vec![Felt::from(1u32); 9]; - let err = NoteAttachmentArray::new(elements).unwrap_err(); - assert_matches!(err, NoteError::NoteAttachmentArrayNotWordAligned(9)); + 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] @@ -868,7 +841,10 @@ mod tests { 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![Felt::new(1); 8])?, + NoteAttachment::new_array( + NoteAttachmentScheme::new(100)?, + vec![Word::from([1, 1, 1, 1u32]); 2], + )?, ])?; let deserialized = NoteAttachments::read_from_bytes(&attachments.to_bytes())?; @@ -900,14 +876,17 @@ mod tests { 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![Felt::new(1); 8])?, + NoteAttachment::new_array( + NoteAttachmentScheme::new(100)?, + vec![Word::from([1, 1, 1, 1u32]); 2], + )?, ])?; let headers = attachments.to_headers(); assert_eq!(headers[0].scheme(), NoteAttachmentScheme::new(42)?); assert_eq!(headers[0].num_words(), 1); assert_eq!(headers[1].scheme(), NoteAttachmentScheme::new(100)?); - assert_eq!(headers[1].num_words(), 2); // 8 felts = 2 words + assert_eq!(headers[1].num_words(), 2); assert!(headers[2].is_absent()); assert!(headers[3].is_absent()); @@ -931,12 +910,12 @@ mod tests { let word = NoteAttachmentContent::new_word(Word::from([1, 2, 3, 4u32])); assert_eq!(word.num_words(), 1); - // Array with 8 elements => 8/4 = 2 - let array = NoteAttachmentContent::new_array(vec![Felt::new(1); 8]).unwrap(); + // 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 12 elements => 12/4 = 3 - let array = NoteAttachmentContent::new_array(vec![Felt::new(1); 12]).unwrap(); + // 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/metadata.rs b/crates/miden-protocol/src/note/metadata.rs index 25b5248156..9a1e037261 100644 --- a/crates/miden-protocol/src/note/metadata.rs +++ b/crates/miden-protocol/src/note/metadata.rs @@ -240,15 +240,25 @@ impl NoteMetadataHeader { impl Serializable for NoteMetadataHeader { fn write_into(&self, target: &mut W) { self.metadata.write_into(target); - target.write_many(self.attachment_headers); + + let present_headers_iter = + self.attachment_headers.iter().filter(|header| !header.is_absent()); + + 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() @@ -258,12 +268,20 @@ impl Serializable for NoteMetadataHeader { impl Deserializable for NoteMetadataHeader { fn read_from(source: &mut R) -> Result { let metadata = NoteMetadata::read_from(source)?; - let attachment_headers = [ - NoteAttachmentHeader::read_from(source)?, - NoteAttachmentHeader::read_from(source)?, - NoteAttachmentHeader::read_from(source)?, - NoteAttachmentHeader::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)) @@ -401,11 +419,11 @@ mod tests { NoteAttachment::new_word(NoteAttachmentScheme::none(), Word::from([3, 4, 5, 6u32])), NoteAttachment::new_array( NoteAttachmentScheme::MAX, - [Felt::from_u8(5); NoteAttachmentArray::MIN_NUM_ELEMENTS as usize].to_vec(), + vec![Word::from([5, 5, 5, 5u32]); NoteAttachmentArray::MIN_NUM_WORDS as usize], )?, NoteAttachment::new_array( NoteAttachmentScheme::MAX, - [Felt::from_u8(10); NoteAttachmentArray::MAX_NUM_ELEMENTS as usize].to_vec(), + vec![Word::from([10, 10, 10, 10u32]); NoteAttachmentHeader::MAX_NUM_WORDS as usize], )?, ])] #[test] diff --git a/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs b/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs index 2605b0741b..ffca595a5e 100644 --- a/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs +++ b/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs @@ -349,7 +349,7 @@ impl TransactionAdviceInputs { if let NoteAttachmentContent::Array(array_attachment) = attachment.content() { self.add_map_entry( array_attachment.commitment(), - array_attachment.as_slice().to_vec(), + array_attachment.to_elements(), ); } } diff --git a/crates/miden-standards/src/account/interface/mod.rs b/crates/miden-standards/src/account/interface/mod.rs index 583811d1f1..835611e6bc 100644 --- a/crates/miden-standards/src/account/interface/mod.rs +++ b/crates/miden-standards/src/account/interface/mod.rs @@ -168,8 +168,7 @@ impl AccountInterface { for note in output_notes { if let Some(attachment) = note.attachments().iter().next() { if let NoteAttachmentContent::Array(array) = attachment.content() { - code_builder - .add_advice_map_entry(array.commitment(), array.as_slice().to_vec()); + code_builder.add_advice_map_entry(array.commitment(), array.to_elements()); } } } 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 45bc71135c..4ec5e0cce1 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 @@ -238,7 +238,7 @@ async fn test_get_output_notes_commitment() -> anyhow::Result<()> { .add_assets([asset_2]) .attachment(NoteAttachment::new_array( NoteAttachmentScheme::new(5u16)?, - [42, 43, 44, 45, 46, 47, 48, 49u32].map(Felt::from).to_vec(), + vec![Word::from([42, 43, 44, 45u32]), Word::from([46, 47, 48, 49u32])], )?) .build()?; @@ -1236,13 +1236,14 @@ async fn test_add_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, 9, 10u32].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_content_word = output_note.attachments().get(0).unwrap().content().to_word(); let attachment_num_words = output_note.attachments().get(0).unwrap().num_words(); + let elements: Vec = words.iter().flat_map(Word::as_elements).copied().collect(); let tx_script = format!( " use miden::protocol::output_note 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 5dafc1f085..6ff95b364f 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_tx.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_tx.rs @@ -221,7 +221,7 @@ async fn executed_transaction_output_notes() -> anyhow::Result<()> { NoteAttachment::new_word(NoteAttachmentScheme::new(28)?, Word::from([2, 3, 4, 5u32])); let attachment3 = NoteAttachment::new_array( NoteAttachmentScheme::new(29)?, - [6, 7, 8, 9, 10, 11, 12, 13u32].map(Felt::from).to_vec(), + vec![Word::from([6, 7, 8, 9u32]), Word::from([10, 11, 12, 13u32])], )?; let note_type1 = NoteType::Private; @@ -358,7 +358,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_word(), array.to_elements())]) .extend_expected_output_notes(vec![ RawOutputNote::Full(expected_output_note_2.clone()), RawOutputNote::Full(expected_output_note_3.clone()), diff --git a/crates/miden-testing/src/utils.rs b/crates/miden-testing/src/utils.rs index 95603c8b48..794491f5c9 100644 --- a/crates/miden-testing/src/utils.rs +++ b/crates/miden-testing/src/utils.rs @@ -268,7 +268,7 @@ fn note_script_that_creates_notes<'note>( // For array attachments, add the elements to the advice map keyed by the commitment. if let NoteAttachmentContent::Array(array) = attachment.content() { - advice_map.insert(array.commitment(), array.as_slice().to_vec()); + advice_map.insert(array.commitment(), array.to_elements()); } } diff --git a/crates/miden-testing/tests/scripts/send_note.rs b/crates/miden-testing/tests/scripts/send_note.rs index 325cc4b531..74a64d7ac5 100644 --- a/crates/miden-testing/tests/scripts/send_note.rs +++ b/crates/miden-testing/tests/scripts/send_note.rs @@ -62,8 +62,8 @@ 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, 5, 4, 3, 2u32].map(Felt::from).to_vec(); - let attachment = NoteAttachment::new_array(NoteAttachmentScheme::new(42)?, elements.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(); diff --git a/crates/miden-tx/src/host/tx_event.rs b/crates/miden-tx/src/host/tx_event.rs index c4b6214992..f7a995c633 100644 --- a/crates/miden-tx/src/host/tx_event.rs +++ b/crates/miden-tx/src/host/tx_event.rs @@ -27,7 +27,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}; @@ -770,13 +770,16 @@ fn extract_note_attachment( ) })?; - let array_attachment = - NoteAttachmentArray::new(elements.to_vec()).map_err(|source| { - TransactionKernelError::other_with_source( - "failed to construct note attachment array", - source, - ) - })?; + let words: Vec = elements + .chunks_exact(WORD_SIZE) + .map(|chunk| Word::from([chunk[0], chunk[1], chunk[2], chunk[3]])) + .collect(); + let array_attachment = NoteAttachmentArray::new(words).map_err(|source| { + TransactionKernelError::other_with_source( + "failed to construct note attachment array", + source, + ) + })?; if array_attachment.commitment() != attachment { return Err(TransactionKernelError::NoteAttachmentArrayMismatch { From 4fd0a6e3d25e2d3288bd95fded4affd7cc7451fb Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Fri, 17 Apr 2026 18:51:01 +0200 Subject: [PATCH 14/48] chore: move MAX_NUM_WORDS from header to NoteAttachment --- crates/miden-protocol/src/errors/mod.rs | 10 ++--- crates/miden-protocol/src/note/attachment.rs | 42 +++++++++---------- crates/miden-protocol/src/note/metadata.rs | 2 +- crates/miden-protocol/src/testing/note.rs | 3 +- .../src/transaction/outputs/tests.rs | 5 +-- .../src/account/interface/mod.rs | 8 ++-- .../miden-testing/tests/scripts/send_note.rs | 2 +- 7 files changed, 32 insertions(+), 40 deletions(-) diff --git a/crates/miden-protocol/src/errors/mod.rs b/crates/miden-protocol/src/errors/mod.rs index b8c24697b1..ac38acd85d 100644 --- a/crates/miden-protocol/src/errors/mod.rs +++ b/crates/miden-protocol/src/errors/mod.rs @@ -31,18 +31,14 @@ use crate::asset::AssetId; use crate::batch::BatchId; use crate::block::BlockNumber; use crate::note::{ -<<<<<<< HEAD - NoteAssets, NoteAttachmentArray, NoteAttachmentKind, NoteAttachmentScheme, NoteAttachments, NoteTag, NoteType, Nullifier -======= NoteAssets, + NoteAttachment, NoteAttachmentArray, - NoteAttachmentHeader, NoteAttachmentScheme, NoteAttachments, NoteTag, NoteType, Nullifier, ->>>>>>> 3b6219c1 (chore: update note kernel memory layout) }; use crate::transaction::TransactionId; use crate::utils::serde::DeserializationError; @@ -672,12 +668,12 @@ pub enum NoteError { NoteAttachmentArrayTooFewWords(usize), #[error( "note attachment array contains {0} words, but the maximum is {max} words", - max = NoteAttachmentHeader::MAX_NUM_WORDS + max = NoteAttachment::MAX_NUM_WORDS )] NoteAttachmentArrayTooManyWords(usize), #[error( "attachment size {0} exceeds maximum {max}", - max = NoteAttachmentHeader::MAX_NUM_WORDS + max = NoteAttachment::MAX_NUM_WORDS )] NoteAttachmentHeaderSizeExceeded(u8), #[error("{0} attachments were provided but maximum is {max}", max = NoteAttachments::MAX_COUNT)] diff --git a/crates/miden-protocol/src/note/attachment.rs b/crates/miden-protocol/src/note/attachment.rs index e734ea1460..6c5bf4a76c 100644 --- a/crates/miden-protocol/src/note/attachment.rs +++ b/crates/miden-protocol/src/note/attachment.rs @@ -49,6 +49,15 @@ pub struct NoteAttachment { } impl NoteAttachment { + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + /// The maximum number of words in an attachment. + /// + /// Limited to 254 to ensure the value fits into a u8 and the felt encoding remains valid + /// when four num_words values are packed into a single felt in the note metadata. + pub const MAX_NUM_WORDS: u8 = 254; + // CONSTRUCTORS // -------------------------------------------------------------------------------------------- @@ -73,7 +82,7 @@ impl NoteAttachment { /// /// Returns an error if: /// - The number of words is less than [`NoteAttachmentArray::MIN_NUM_WORDS`]. - /// - The number of words exceeds [`NoteAttachmentHeader::MAX_NUM_WORDS`]. + /// - The number of words exceeds [`NoteAttachment::MAX_NUM_WORDS`]. pub fn new_array( attachment_scheme: NoteAttachmentScheme, words: Vec, @@ -162,7 +171,7 @@ impl NoteAttachmentContent { /// /// Returns an error if: /// - The number of words is less than [`NoteAttachmentArray::MIN_NUM_WORDS`]. - /// - The number of words exceeds [`NoteAttachmentHeader::MAX_NUM_WORDS`]. + /// - The number of words exceeds [`NoteAttachment::MAX_NUM_WORDS`]. pub fn new_array(words: Vec) -> Result { NoteAttachmentArray::new(words).map(Self::from) } @@ -280,13 +289,13 @@ impl NoteAttachmentArray { /// /// Returns an error if: /// - The number of words is less than [`Self::MIN_NUM_WORDS`]. - /// - The number of words exceeds [`NoteAttachmentHeader::MAX_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() > NoteAttachmentHeader::MAX_NUM_WORDS as usize { + if words.len() > NoteAttachment::MAX_NUM_WORDS as usize { return Err(NoteError::NoteAttachmentArrayTooManyWords(words.len())); } @@ -470,15 +479,6 @@ pub struct NoteAttachmentHeader { } impl NoteAttachmentHeader { - // CONSTANTS - // -------------------------------------------------------------------------------------------- - - /// The maximum number of words in an attachment. - /// - /// Limited to 254 to ensure the value fits into a u8 and the felt encoding remains valid - /// when four num_words values are packed into a single felt in the note metadata. - pub const MAX_NUM_WORDS: u8 = 254; - // CONSTRUCTORS // -------------------------------------------------------------------------------------------- @@ -486,9 +486,9 @@ impl NoteAttachmentHeader { /// /// # Errors /// - /// Returns an error if `num_words` exceeds [`Self::MAX_NUM_WORDS`]. + /// Returns an error if `num_words` exceeds [`NoteAttachment::MAX_NUM_WORDS`]. pub fn new(scheme: NoteAttachmentScheme, num_words: u8) -> Result { - if num_words > Self::MAX_NUM_WORDS { + if num_words > NoteAttachment::MAX_NUM_WORDS { return Err(NoteError::NoteAttachmentHeaderSizeExceeded(num_words)); } Ok(Self { scheme, num_words }) @@ -604,7 +604,7 @@ impl NoteAttachments { .sum::(); if total_num_words > Self::MAX_NUM_WORDS as usize { - return Err(NoteError::TooManyAttachmentElements(total_num_words)); + return Err(NoteError::NoteAttachmentArrayTooManyWords(total_num_words)); } let commitment = compute_commitment(&attachments); @@ -648,11 +648,9 @@ impl NoteAttachments { 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(), - attachment.num_words(), - ) - .expect("attachment num_words should not exceed NoteAttachmentHeader::MAX_NUM_WORDS"); + headers[i] = + NoteAttachmentHeader::new(attachment.attachment_scheme(), attachment.num_words()) + .expect("attachment num_words should not exceed NoteAttachment::MAX_NUM_WORDS"); } headers } @@ -772,7 +770,7 @@ mod tests { #[test] fn note_attachment_array_fails_on_too_many_words() -> anyhow::Result<()> { - let too_many_words = NoteAttachmentHeader::MAX_NUM_WORDS as usize + 1; + 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(); diff --git a/crates/miden-protocol/src/note/metadata.rs b/crates/miden-protocol/src/note/metadata.rs index 9a1e037261..2e0988799f 100644 --- a/crates/miden-protocol/src/note/metadata.rs +++ b/crates/miden-protocol/src/note/metadata.rs @@ -423,7 +423,7 @@ mod tests { )?, NoteAttachment::new_array( NoteAttachmentScheme::MAX, - vec![Word::from([10, 10, 10, 10u32]); NoteAttachmentHeader::MAX_NUM_WORDS as usize], + vec![Word::from([10, 10, 10, 10u32]); NoteAttachment::MAX_NUM_WORDS as usize], )?, ])] #[test] diff --git a/crates/miden-protocol/src/testing/note.rs b/crates/miden-protocol/src/testing/note.rs index 685386f3b6..913fd0f7ee 100644 --- a/crates/miden-protocol/src/testing/note.rs +++ b/crates/miden-protocol/src/testing/note.rs @@ -6,7 +6,6 @@ use crate::asset::FungibleAsset; use crate::note::{ Note, NoteAssets, - NoteAttachments, NoteMetadata, NoteRecipient, NoteScript, @@ -30,7 +29,7 @@ impl Note { let inputs = NoteStorage::new(Vec::new()).unwrap(); let recipient = NoteRecipient::new(serial_num, note_script, inputs); - Note::new(assets, metadata, recipient, NoteAttachments::default()) + Note::new(assets, metadata, recipient) } } diff --git a/crates/miden-protocol/src/transaction/outputs/tests.rs b/crates/miden-protocol/src/transaction/outputs/tests.rs index 0e203020e5..7e5834f5f6 100644 --- a/crates/miden-protocol/src/transaction/outputs/tests.rs +++ b/crates/miden-protocol/src/transaction/outputs/tests.rs @@ -11,7 +11,6 @@ use crate::errors::{OutputNoteError, TransactionOutputError}; use crate::note::{ Note, NoteAssets, - NoteAttachments, NoteMetadata, NoteRecipient, NoteScript, @@ -68,7 +67,7 @@ fn output_note_size_hint_matches_serialized_length() -> anyhow::Result<()> { let script = NoteScript::mock(); let recipient = NoteRecipient::new(serial_num, script, storage); - let note = Note::new(assets, metadata, recipient, NoteAttachments::default()); + let note = Note::new(assets, metadata, recipient); let output_note = RawOutputNote::Full(note); let bytes = output_note.to_bytes(); @@ -113,7 +112,7 @@ fn oversized_public_note_triggers_size_limit_error() -> anyhow::Result<()> { .with_tag(NoteTag::with_account_target(sender_id)); let recipient = NoteRecipient::new(serial_num, script, storage); - let oversized_note = Note::new(assets, metadata, recipient, NoteAttachments::default()); + let oversized_note = Note::new(assets, metadata, recipient); // Sanity-check that our constructed note is indeed larger than the configured // maximum. diff --git a/crates/miden-standards/src/account/interface/mod.rs b/crates/miden-standards/src/account/interface/mod.rs index 835611e6bc..be775f882a 100644 --- a/crates/miden-standards/src/account/interface/mod.rs +++ b/crates/miden-standards/src/account/interface/mod.rs @@ -166,10 +166,10 @@ impl AccountInterface { // and the array elements as value. let mut code_builder = CodeBuilder::new(); for note in output_notes { - if let Some(attachment) = note.attachments().iter().next() { - if let NoteAttachmentContent::Array(array) = attachment.content() { - code_builder.add_advice_map_entry(array.commitment(), array.to_elements()); - } + if let Some(attachment) = note.attachments().iter().next() + && let NoteAttachmentContent::Array(array) = attachment.content() + { + code_builder.add_advice_map_entry(array.commitment(), array.to_elements()); } } diff --git a/crates/miden-testing/tests/scripts/send_note.rs b/crates/miden-testing/tests/scripts/send_note.rs index 74a64d7ac5..e1e4d0c077 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}; @@ -18,7 +19,6 @@ use miden_protocol::note::{ PartialNote, }; 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; From cd1389a5cc06fe75b6ccf7d751f107702d11dafa Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Sun, 19 Apr 2026 18:32:39 +0200 Subject: [PATCH 15/48] feat: define attachments commitment over attachment commitments --- .../asm/kernels/transaction/lib/note.masm | 7 ++-- crates/miden-protocol/src/note/attachment.rs | 38 ++++++------------- .../src/transaction/kernel/memory.rs | 28 +++++++------- .../src/kernel_tests/tx/test_output_note.rs | 33 ++++++++++++---- 4 files changed, 55 insertions(+), 51 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/note.masm b/crates/miden-protocol/asm/kernels/transaction/lib/note.masm index 6213533123..4a5c73c7c8 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/note.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/note.masm @@ -88,7 +88,8 @@ end #! #! The commitment is defined as: #! - 0 attachments: EMPTY_WORD -#! - 1+ attachments: sequential hash over all flattened attachment elements +#! - 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] @@ -108,8 +109,8 @@ proc compute_attachments_commitment drop drop padw # => [EMPTY_WORD] else - # 1+ attachments: compute hash via hash_words - # compute end_ptr = attachment_data_ptr + num_attachments * 4 + # 1+ attachments: hash the present attachment commitment words + # end_ptr = attachment_data_ptr + num_attachments * WORD_SIZE swap exec.memory::get_output_note_attachment_data_ptr # => [start_ptr, num_attachments] diff --git a/crates/miden-protocol/src/note/attachment.rs b/crates/miden-protocol/src/note/attachment.rs index 6c5bf4a76c..c42a588e3a 100644 --- a/crates/miden-protocol/src/note/attachment.rs +++ b/crates/miden-protocol/src/note/attachment.rs @@ -1,8 +1,6 @@ use alloc::string::ToString; use alloc::vec::Vec; -use miden_core::WORD_SIZE; - use crate::crypto::SequentialCommit; use crate::errors::NoteError; use crate::utils::serde::{ @@ -555,7 +553,8 @@ impl Deserializable for NoteAttachmentHeader { /// /// The commitment to the attachments is defined as: /// - 0 attachments: `EMPTY_WORD` -/// - 1+ attachments: sequential hash over all attachment words +/// - 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, @@ -685,37 +684,24 @@ impl SequentialCommit for NoteAttachments { /// Collects all attachment data into a flat vector of field elements. fn attachments_to_elements(attachments: &[NoteAttachment]) -> Vec { let mut elements = Vec::new(); - for attachment in attachments { - match attachment.content() { - NoteAttachmentContent::Word(word) => { - elements.extend_from_slice(word.as_elements()); - }, - NoteAttachmentContent::Array(array) => { - elements.extend(array.as_elements()); - }, - } - } - elements -} - -/// Collects all attachment words into a flat vector of field elements. -/// -/// Each attachment contributes exactly one word (4 felts): the raw content for word attachments, -/// or the commitment for array attachments. -fn attachments_to_words(attachments: &[NoteAttachment]) -> Vec { - let mut elements = Vec::with_capacity(attachments.len() * WORD_SIZE); - for attachment in attachments { - elements.extend_from_slice(attachment.content().to_word().as_elements()); + for attachment_commitment in attachments.iter().map(|attachment| attachment.content().to_word()) + { + elements.extend_from_slice(attachment_commitment.as_elements()); } elements } /// Computes the commitment over a slice of attachments. +/// +/// The commitment is defined as `hash(ATTACHMENT_0_COMMITMENT || ... || ATTACHMENT_N_COMMITMENT)`, +/// i.e., the sequential hash over the individual attachment commitments. Returns `EMPTY_WORD` if +/// no attachments are present. fn compute_commitment(attachments: &[NoteAttachment]) -> Word { if attachments.is_empty() { Word::empty() } else { - Hasher::hash_elements(&attachments_to_words(attachments)) + let elements = attachments_to_elements(attachments); + Hasher::hash_elements(&elements) } } @@ -864,7 +850,7 @@ mod tests { NoteAttachmentScheme::new(1)?, word, )])?; - // Single word attachment: commitment is the hash of the word. + // Single word attachment: commitment is hash of the word. assert_eq!(attachments.commitment(), Hasher::hash_elements(word.as_elements())); Ok(()) diff --git a/crates/miden-protocol/src/transaction/kernel/memory.rs b/crates/miden-protocol/src/transaction/kernel/memory.rs index fa2aff3311..1a94d6423f 100644 --- a/crates/miden-protocol/src/transaction/kernel/memory.rs +++ b/crates/miden-protocol/src/transaction/kernel/memory.rs @@ -365,10 +365,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 +430,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 │ NUM │ ATTACHMENT │ ATTACHMENT │ ATTACHMENT │ ATTACHMENT │ RECIPIENT │ ASSETS │ NUM │ -// │ ID │ HEADER │ ATTACHMENTS │ 0 │ 1 │ 2 │ 3 │ │ COMMITMENT │ ASSETS │ -// ├──────┼──────────┼──────────────┼────────────┼────────────┼────────────┼────────────┼───────────┼────────────┼────────┼ -// 0 4 8 12 16 20 24 28 32 36 +// ┌──────┬──────────┬──────────────┬────────────┬────────────┬────────────┬────────────┬ +// │ NOTE │ METADATA │ NUM │ ATTACHMENT │ ATTACHMENT │ ATTACHMENT │ ATTACHMENT │ +// │ ID │ HEADER │ ATTACHMENTS │ 0 │ 1 │ 2 │ 3 │ +// ├──────┼──────────┼──────────────┼────────────┼────────────┼────────────┼────────────┼ +// 0 4 8 12 16 20 24 // -// ┬───────┬───────┬─────────┬─────┬────────┬─────────┬─────────┐ -// │ DIRTY │ ASSET │ ASSET │ ... │ ASSET │ ASSET │ PADDING │ -// │ FLAG │ KEY 0 │ VALUE 0 │ │ KEY n │ VALUE n │ │ -// ┼───────┼───────┼─────────┼─────┼────────┼─────────┼─────────┘ -// 37 40 44 40 + 8n 44 + 8n +// ┬───────────┬────────────┬────────┬───────┬───────┬─────────┬─────┬────────┬─────────┬─────────┐ +// │ RECIPIENT │ ASSETS │ NUM │ DIRTY │ ASSET │ ASSET │ ... │ ASSET │ ASSET │ PADDING │ +// │ │ COMMITMENT │ ASSETS │ FLAG │ KEY 0 │ VALUE 0 │ │ KEY n │ VALUE n │ │ +// ┼───────────┼────────────┼────────┼───────┼───────┼─────────┼─────┼────────┼─────────┼─────────┘ +// 8 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 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 4ec5e0cce1..4fde2ef0b5 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 @@ -49,7 +49,7 @@ use miden_protocol::transaction::memory::{ 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, @@ -339,12 +339,17 @@ async fn test_get_output_notes_commitment() -> anyhow::Result<()> { 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_0_OFFSET), - output_note_1.attachments().to_commitment(), - "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( @@ -357,11 +362,23 @@ async fn test_get_output_notes_commitment() -> anyhow::Result<()> { exec_output.get_kernel_mem_word( OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_ATTACHMENT_0_OFFSET + NOTE_MEM_SIZE ), - output_note_2.attachments().to_commitment(), + output_note_2.attachments().get(0).unwrap().content().to_word(), "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(()) } From 9260d37a921c9beb0f705de5d6c6b78e9e192bc8 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Sun, 19 Apr 2026 18:46:32 +0200 Subject: [PATCH 16/48] chore: rename input note memory attachments comm offset --- .../asm/kernels/transaction/api.masm | 2 +- .../asm/kernels/transaction/lib/memory.masm | 20 +++++++++---------- .../asm/kernels/transaction/lib/prologue.masm | 16 +++++++-------- crates/miden-protocol/src/note/attachment.rs | 8 ++++++++ .../src/transaction/kernel/advice_inputs.rs | 12 +++++++++-- 5 files changed, 37 insertions(+), 21 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction/api.masm b/crates/miden-protocol/asm/kernels/transaction/api.masm index 215ed9360b..10200a7b21 100644 --- a/crates/miden-protocol/asm/kernels/transaction/api.masm +++ b/crates/miden-protocol/asm/kernels/transaction/api.masm @@ -949,7 +949,7 @@ pub proc input_note_get_metadata # => [METADATA_HEADER, input_note_ptr, pad(16)] # get the attachment - movup.4 exec.memory::get_input_note_attachment + movup.4 exec.memory::get_input_note_attachments_commitment # => [NOTE_ATTACHMENT, METADATA_HEADER, pad(16)] # truncate the stack diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm b/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm index 8309557c7a..d165107cbe 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm @@ -254,7 +254,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 @@ -1691,27 +1691,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 diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/prologue.masm b/crates/miden-protocol/asm/kernels/transaction/lib/prologue.masm index 01d808d09c..349eaad5d5 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/prologue.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/prologue.masm @@ -577,25 +577,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. @@ -839,9 +839,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/src/note/attachment.rs b/crates/miden-protocol/src/note/attachment.rs index c42a588e3a..db8b40889d 100644 --- a/crates/miden-protocol/src/note/attachment.rs +++ b/crates/miden-protocol/src/note/attachment.rs @@ -635,6 +635,14 @@ impl NoteAttachments { self.attachments.iter() } + /// Returns the cached commitment over the contained attachments. + pub fn attachment_words(&self) -> Vec { + self.attachments + .iter() + .map(|attachment| attachment.content().to_word()) + .collect() + } + /// Returns the cached commitment over the contained attachments. pub fn commitment(&self) -> Word { self.commitment diff --git a/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs b/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs index ffca595a5e..98b9ded674 100644 --- a/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs +++ b/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs @@ -340,8 +340,16 @@ impl TransactionAdviceInputs { // assets commitments self.add_map_entry(assets.commitment(), assets.to_elements()); - // ATTACHMENTS_COMMITMENTS |-> [ATTACHMENT_ELEMENTS] - self.add_map_entry(note.attachments().commitment(), note.attachments().to_elements()); + // ATTACHMENTS_COMMITMENTS |-> [[ATTACHMENT_WORDS]] + self.add_map_entry( + note.attachments().commitment(), + note.attachments() + .attachment_words() + .iter() + .flat_map(Word::as_elements) + .copied() + .collect(), + ); // elements of each array attachment for attachment in note.attachments().iter() { From 60f3bfa6bd29851463537e5df6c0d1f10771f588 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Sun, 19 Apr 2026 20:20:54 +0200 Subject: [PATCH 17/48] fix: num words extraction from metadata header --- .../asm/kernels/transaction/api.masm | 27 +++++++++++++++---- crates/miden-protocol/asm/protocol/note.masm | 8 +++--- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction/api.masm b/crates/miden-protocol/asm/kernels/transaction/api.masm index 10200a7b21..a6b3958b24 100644 --- a/crates/miden-protocol/asm/kernels/transaction/api.masm +++ b/crates/miden-protocol/asm/kernels/transaction/api.masm @@ -913,7 +913,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 @@ -921,7 +921,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. @@ -930,7 +930,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)] @@ -950,11 +950,28 @@ pub proc input_note_get_metadata # get the attachment movup.4 exec.memory::get_input_note_attachments_commitment - # => [NOTE_ATTACHMENT, METADATA_HEADER, pad(16)] + # => [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_0, METADATA_HEADER, pad(8)] + end + # => [ATTACHMENT_0, METADATA_HEADER, pad(8)] end #! Returns the serial number of the specified input note. diff --git a/crates/miden-protocol/asm/protocol/note.masm b/crates/miden-protocol/asm/protocol/note.masm index b6d58a1836..de4a2cbf4c 100644 --- a/crates/miden-protocol/asm/protocol/note.masm +++ b/crates/miden-protocol/asm/protocol/note.masm @@ -211,14 +211,14 @@ end #! #! Invocation: exec pub proc metadata_into_attachment_header - # => [sender_id_suffix_type_version, sender_id_prefix, tag_and_num_words, schemes] + # => [sender_id_suffix_type_version, sender_id_prefix, attachment_num_words_and_tag, schemes] drop drop - # => [tag_and_num_words, schemes] + # => [attachment_num_words_and_tag, schemes] - # extract attachment_0_num_words from tag_and_num_words: + # extract attachment_0_num_words from attachment_num_words_and_tag: # u32split converts into num_words_packed (upper 32 bits) and tag (lower 32 bits) - u32split swap drop + u32split drop # => [num_words_packed, schemes] # => [attachment_3_num_words (8 bits) | attachment_2_num_words (8 bits) | # attachment_1_num_words (8 bits) | attachment_0_num_words (8 bits)] From 1b6607890b10336a2760b04222c2164221e73d8f Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Mon, 20 Apr 2026 15:06:46 +0200 Subject: [PATCH 18/48] feat: always hash attachments --- .../asm/kernels/transaction/api.masm | 14 +- .../asm/kernels/transaction/lib/memory.masm | 10 ++ .../asm/kernels/transaction/lib/note.masm | 24 +--- .../kernels/transaction/lib/output_note.masm | 132 ++++++++++++++---- .../asm/protocol/output_note.masm | 80 ++++++++--- crates/miden-protocol/src/note/attachment.rs | 55 +++++--- .../src/transaction/kernel/advice_inputs.rs | 17 +-- .../src/transaction/kernel/memory.rs | 1 + .../src/transaction/outputs/notes.rs | 4 +- .../src/account/interface/component.rs | 8 +- .../src/account/interface/mod.rs | 11 +- .../src/kernel_tests/tx/test_output_note.rs | 78 ++++++++--- .../src/kernel_tests/tx/test_tx.rs | 10 +- .../src/standards/network_account_target.rs | 15 +- crates/miden-testing/src/utils.rs | 35 ++--- crates/miden-tx/src/errors/mod.rs | 6 +- crates/miden-tx/src/host/tx_event.rs | 74 +++++++--- 17 files changed, 386 insertions(+), 188 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction/api.masm b/crates/miden-protocol/asm/kernels/transaction/api.masm index a6b3958b24..9144cf2d7b 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 @@ -969,6 +969,11 @@ pub proc input_note_get_metadata # 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)] @@ -1148,12 +1153,11 @@ end #! Adds an attachment to the note specified by the index. #! -#! Inputs: [attachment_scheme, attachment_num_words, ATTACHMENT, note_idx, pad(9)] +#! Inputs: [attachment_scheme, ATTACHMENT, note_idx, pad(9)] #! Outputs: [pad(16)] #! #! Where: #! - attachment_scheme is the user-defined scheme of the attachment. -#! - attachment_num_words is the number of words of the attachment. #! - ATTACHMENT is the attachment word to store. #! - note_idx is the index of the note to which the attachment is added. #! @@ -1168,11 +1172,11 @@ end pub proc output_note_add_attachment # assert that the provided note index is less than the total number of output notes dup.6 exec.output_note::assert_note_index_in_bounds drop - # => [attachment_scheme, attachment_num_words, ATTACHMENT, note_idx, pad(9)] + # => [attachment_scheme, ATTACHMENT, note_idx, pad(9)] # check that this procedure was executed against the native account exec.memory::assert_native_account - # => [attachment_scheme, attachment_num_words, ATTACHMENT, note_idx, pad(9)] + # => [attachment_scheme, ATTACHMENT, note_idx, pad(9)] exec.output_note::add_attachment # => [pad(16)] diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm b/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm index d165107cbe..1a83a9f5c1 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm @@ -302,6 +302,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 # ================================================================================================= diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/note.masm b/crates/miden-protocol/asm/kernels/transaction/lib/note.masm index 4a5c73c7c8..5737f5fb1f 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/note.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/note.masm @@ -101,25 +101,15 @@ proc compute_attachments_commitment dup exec.memory::get_output_note_num_attachments # => [num_attachments, note_ptr] - # TODO(multi_attachments): We probably don't need the if-else branching here. If not, we can - # optimize the MASM a bit. - dup eq.0 - if.true - # 0 attachments: return EMPTY_WORD - drop drop padw - # => [EMPTY_WORD] - else - # 1+ attachments: hash the present attachment commitment words - # end_ptr = attachment_data_ptr + num_attachments * WORD_SIZE - swap exec.memory::get_output_note_attachment_data_ptr - # => [start_ptr, num_attachments] + # end_ptr = attachment_data_ptr + num_attachments * WORD_SIZE + swap exec.memory::get_output_note_attachment_data_ptr + # => [start_ptr, num_attachments] - swap mul.WORD_SIZE dup.1 add swap - # => [start_ptr, end_ptr] + swap mul.WORD_SIZE dup.1 add swap + # => [start_ptr, end_ptr] - exec.poseidon2::hash_words - # => [ATTACHMENTS_COMMITMENT] - end + exec.poseidon2::hash_words + # => [ATTACHMENTS_COMMITMENT] end #! Computes the assets commitment of the output note located at the specified memory address. 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 97d94033f7..753d41e8d3 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm @@ -3,14 +3,17 @@ 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_WORD_SIZE use $kernel::constants::MAX_OUTPUT_NOTES_PER_TX +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 # ================================================================================================= @@ -35,12 +38,16 @@ const ERR_OUTPUT_NOTE_ATTACHMENT_SCHEME_MAX_EXCEEDED="attachment scheme must not const ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_MAX_EXCEEDED="attachment num_words must not exceed 254" +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_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_INVALID_INDEX="failed to find note at the given index; index must be within [0, num_of_notes]" +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" @@ -231,72 +238,81 @@ end #! Adds an attachment to the note specified by the index. Attachments are append-only. #! -#! Inputs: [attachment_scheme, attachment_num_words, ATTACHMENT, note_idx] +#! 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: #! - attachment_scheme is the user-defined scheme of the attachment. -#! - attachment_num_words is the number of words of the attachment. -#! - ATTACHMENT is the attachment word to store. +#! - 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 scheme exceeds 65534. -#! - the attachment num_words exceeds 254 or is zero. +#! - the number of elements is not a multiple of 4, or num_words is zero or exceeds 254. +#! - 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_num_words, ATTACHMENT, note_idx] + # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] + + movdn.4 + # => [ATTACHMENT_COMMITMENT, attachment_scheme, note_idx] + + # validate commitment against advice data and derive num_words + dupw exec.validate_attachment + # => [num_words, ATTACHMENT_COMMITMENT, note_idx] - # validate 0 < attachment_num_words <= 254 - dup.1 neq.0 assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_CANNOT_BE_ZERO - dup.1 u32assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_MAX_EXCEEDED - u32lte.MAX_ATTACHMENT_WORD_SIZE assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_MAX_EXCEEDED - # => [attachment_scheme, attachment_num_words, ATTACHMENT, note_idx] + movup.5 swap + # => [num_words, attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] # get note_ptr from note_idx movup.6 exec.memory::get_output_note_ptr - # => [note_ptr, attachment_scheme, attachment_num_words, ATTACHMENT] + # => [note_ptr, num_words, attachment_scheme, ATTACHMENT_COMMITMENT] - # validate current number of attachments < 4 which means we can add one more to stay below the - # limit + # validate current number of attachments < 4 dup exec.memory::get_output_note_num_attachments - # => [num_attachments, note_ptr, attachment_scheme, attachment_num_words, ATTACHMENT] + # => [num_attachments, note_ptr, num_words, attachment_scheme, ATTACHMENT_COMMITMENT] dup lt.MAX_ATTACHMENTS_PER_NOTE assert.err=ERR_OUTPUT_NOTE_TOO_MANY_ATTACHMENTS - # => [num_attachments, note_ptr, attachment_scheme, attachment_num_words, ATTACHMENT] + # => [num_attachments, note_ptr, num_words, attachment_scheme, ATTACHMENT_COMMITMENT] # emit event emit.NOTE_BEFORE_ADD_ATTACHMENT_EVENT - # => [num_attachments, note_ptr, attachment_scheme, attachment_num_words, ATTACHMENT] + # => [num_attachments, note_ptr, num_words, attachment_scheme, ATTACHMENT_COMMITMENT] - swapw - # => [ATTACHMENT, num_attachments, note_ptr, attachment_scheme, attachment_num_words] + # --- Update attachment into in output note memory --- - # TODO(multi_attachments): Validate attachment + num_words against advice data. + swapw + # => [ATTACHMENT_COMMITMENT, num_attachments, note_ptr, num_words, attachment_scheme] # use attachment_idx = num_attachments dup.4 dup.6 - # => [note_ptr, attachment_idx, ATTACHMENT, num_attachments, note_ptr, attachment_scheme, attachment_num_words] + # => [note_ptr, attachment_idx, ATTACHMENT_COMMITMENT, num_attachments, note_ptr, num_words, attachment_scheme] + # store commitment in note memory exec.memory::set_output_note_attachment - # => [num_attachments, note_ptr, attachment_scheme, attachment_num_words] + # => [num_attachments, note_ptr, num_words, attachment_scheme] - # compute new number of attachments - # we have previously validated num_attachments to be < 4, so the new number is guaranteed to be - # at most 4 + # increment number of attachments dup add.1 - # => [new_num_attachments, num_attachments, note_ptr, attachment_scheme, attachment_num_words] + # => [new_num_attachments, num_attachments, note_ptr, num_words, attachment_scheme] - # set new number of attachments in memory dup.2 exec.memory::set_output_note_num_attachments - # => [num_attachments, note_ptr, attachment_scheme, attachment_num_words] + # => [num_attachments, note_ptr, num_words, attachment_scheme] + + movup.3 movdn.2 + # => [num_attachments, note_ptr, attachment_scheme, num_words] - # update attachment num_words and scheme in the note metadata - # note that this needs to take the previous number of attachments, so passing num_attachments - # is correct exec.set_attachment_num_words_and_scheme # => [] end @@ -318,6 +334,62 @@ end # 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: [num_words] +#! +#! Panics if: +#! - the number of elements is not a multiple of 4, or num_words is zero or exceeds 254. +#! - the computed hash of fetched elements does not match ATTACHMENT_COMMITMENT. +@locals(1) +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 <= 254 + dup neq.0 assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_CANNOT_BE_ZERO + dup u32lte.MAX_ATTACHMENT_WORD_SIZE assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_MAX_EXCEEDED + # OS => [num_words, ATTACHMENT_COMMITMENT] + + # save num_words for later + dup movdn.5 + # OS => [num_words, ATTACHMENT_COMMITMENT, num_words] + + # --- 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, num_words] + # AS => [[ATTACHMENT_ELEMENTS]] + + # validate the sequential hash over the attachment elements is ATTACHMENT_COMMITMENT + exec.mem::pipe_preimage_to_memory drop + # OS => [num_words] +end + #! Builds the provided inputs into the NOTE_METADATA_HEADER word. #! #! - The sender ID is set to the native account's ID. diff --git a/crates/miden-protocol/asm/protocol/output_note.masm b/crates/miden-protocol/asm/protocol/output_note.masm index d1b64531fa..714f0d74ff 100644 --- a/crates/miden-protocol/asm/protocol/output_note.masm +++ b/crates/miden-protocol/asm/protocol/output_note.masm @@ -5,6 +5,7 @@ use miden::protocol::kernel_proc_offsets::OUTPUT_NOTE_ADD_ATTACHMENT_OFFSET use miden::protocol::kernel_proc_offsets::OUTPUT_NOTE_GET_RECIPIENT_OFFSET use miden::protocol::kernel_proc_offsets::OUTPUT_NOTE_GET_METADATA_OFFSET use miden::protocol::note +use miden::core::crypto::hashes::poseidon2 # PROCEDURES # ================================================================================================= @@ -147,23 +148,22 @@ end #! Adds an attachment to the note specified by the note index. #! -#! If attachment_num_words > 1, there must be an advice map entry for ATTACHMENT. +#! There must be an advice map entry for ATTACHMENT_COMMITMENT that maps to the raw attachment +#! elements. #! #! Inputs: -#! Operand Stack: [attachment_scheme, attachment_num_words, ATTACHMENT, note_idx] +#! Operand Stack: [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] #! Advice map: { -#! ATTACHMENT?: [[ATTACHMENT_ELEMENTS]], +#! ATTACHMENT_COMMITMENT: [[ATTACHMENT_ELEMENTS]], #! } #! Outputs: [] #! #! Where: #! - attachment_scheme is the user-defined scheme of the attachment. -#! - attachment_num_words is the number of words that make up the attachment. -#! - ATTACHMENT is the attachment word to store or the commitment to an array of attachment -#! elements. +#! - 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 is the sequential commitment (only -#! needed if attachment_num_words > 1). +#! - 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. @@ -174,11 +174,11 @@ end #! Invocation: exec pub proc add_attachment push.OUTPUT_NOTE_ADD_ATTACHMENT_OFFSET - # => [offset, attachment_scheme, attachment_num_words, ATTACHMENT, note_idx] + # => [offset, attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] # pad the stack before the syscall - padw padw swapdw - # => [offset, attachment_scheme, attachment_num_words, ATTACHMENT, note_idx, pad(8)] + push.0 movdn.8 padw padw swapdw + # => [offset, attachment_scheme, ATTACHMENT_COMMITMENT, note_idx, pad(9)] syscall.exec_kernel_proc # => [pad(16)] @@ -190,12 +190,15 @@ end #! Adds a single-word attachment to the note specified by the note index. #! +#! Hashes the raw attachment word to produce the commitment, inserts the raw elements into the +#! advice map keyed by that commitment, then delegates to `add_attachment`. +#! #! Inputs: [attachment_scheme, ATTACHMENT, note_idx] #! Outputs: [] #! #! Where: #! - attachment_scheme is the user-defined scheme of the attachment (u16, max 65534). -#! - ATTACHMENT is the raw attachment word to store. +#! - ATTACHMENT is the raw attachment word. #! - note_idx is the index of the note to which the attachment is added. #! #! Panics if: @@ -205,31 +208,64 @@ end #! - the note already has 4 attachments. #! #! Invocation: exec +@locals(4) pub proc add_word_attachment - # insert attachment_num_words = 1 after attachment_scheme - push.1 swap - # => [attachment_scheme, 1, ATTACHMENT, note_idx] + # => [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] + + # Hash the attachment word + # push.4 locaddr.0 + # => [local_ptr, 4, attachment_scheme, note_idx] + + # exec.poseidon2::hash_elements + # => [ATTACHMENT_COMMITMENT, attachment_scheme, note_idx] + + 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 -#! Adds an array attachment to the note specified by the note index. The ATTACHMENT is the -#! commitment to a set of elements. +#! Adds an array attachment to the note specified by the note index. The ATTACHMENT_COMMITMENT is +#! the hash commitment to a set of elements. #! #! Inputs: -#! Operand Stack: [attachment_scheme, attachment_num_words, ATTACHMENT, note_idx] +#! Operand Stack: [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] #! Advice map: { -#! ATTACHMENT: [[ATTACHMENT_ELEMENTS]], +#! ATTACHMENT_COMMITMENT: [[ATTACHMENT_ELEMENTS]], #! } #! Outputs: [] #! #! Where: #! - attachment_scheme is the user-defined scheme of the attachment. -#! - attachment_num_words is the number of words of the attachment. -#! - ATTACHMENT is the commitment of the set of elements that form the note attachment. +#! - 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 is the sequential commitment. +#! - 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. diff --git a/crates/miden-protocol/src/note/attachment.rs b/crates/miden-protocol/src/note/attachment.rs index db8b40889d..248eea7a42 100644 --- a/crates/miden-protocol/src/note/attachment.rs +++ b/crates/miden-protocol/src/note/attachment.rs @@ -198,16 +198,14 @@ impl NoteAttachmentContent { } } - /// Returns the [`NoteAttachmentContent`] encoded to a [`Word`]. - /// - /// See the type-level documentation for more details. - pub fn to_word(&self) -> Word { - match self { - NoteAttachmentContent::Word(word) => *word, - NoteAttachmentContent::Array(attachment_commitment) => { - attachment_commitment.commitment() - }, - } + /// 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) } } @@ -259,6 +257,24 @@ impl Deserializable for NoteAttachmentContent { } } +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 // ================================================================================================ @@ -635,11 +651,11 @@ impl NoteAttachments { self.attachments.iter() } - /// Returns the cached commitment over the contained attachments. - pub fn attachment_words(&self) -> Vec { + /// Returns the individual commitment of each contained attachment. + pub fn commitments(&self) -> Vec { self.attachments .iter() - .map(|attachment| attachment.content().to_word()) + .map(|attachment| attachment.content().to_commitment()) .collect() } @@ -689,12 +705,11 @@ impl SequentialCommit for NoteAttachments { } } -/// Collects all attachment data into a flat vector of field elements. +/// Collects all attachment commitments into a flat vector of field elements. fn attachments_to_elements(attachments: &[NoteAttachment]) -> Vec { let mut elements = Vec::new(); - for attachment_commitment in attachments.iter().map(|attachment| attachment.content().to_word()) - { - elements.extend_from_slice(attachment_commitment.as_elements()); + for commitment in attachments.iter().map(|attachment| attachment.content().to_commitment()) { + elements.extend_from_slice(commitment.as_elements()); } elements } @@ -858,8 +873,10 @@ mod tests { NoteAttachmentScheme::new(1)?, word, )])?; - // Single word attachment: commitment is hash of the word. - assert_eq!(attachments.commitment(), Hasher::hash_elements(word.as_elements())); + // Single word attachment: the attachment commitment is hash(word), so the overall + // attachments commitment is hash(hash(word)). + let word_commitment = Hasher::hash_elements(word.as_elements()); + assert_eq!(attachments.commitment(), Hasher::hash_elements(word_commitment.as_elements())); Ok(()) } diff --git a/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs b/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs index 98b9ded674..9920583c5f 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, @@ -340,26 +339,22 @@ impl TransactionAdviceInputs { // assets commitments self.add_map_entry(assets.commitment(), assets.to_elements()); - // ATTACHMENTS_COMMITMENTS |-> [[ATTACHMENT_WORDS]] + // ATTACHMENTS_COMMITMENT |-> [[ATTACHMENT_COMMITMENTS]] self.add_map_entry( note.attachments().commitment(), note.attachments() - .attachment_words() + .commitments() .iter() .flat_map(Word::as_elements) .copied() .collect(), ); - // elements of each array attachment + // ATTACHMENT_COMMITMENT |-> [ATTACHMENT_ELEMENTS] for each attachment for attachment in note.attachments().iter() { - // ARRAY_ATTACHMENT_COMMITMENT |-> [ARRAY_ATTACHMENT_ELEMENTS] - if let NoteAttachmentContent::Array(array_attachment) = attachment.content() { - self.add_map_entry( - array_attachment.commitment(), - array_attachment.to_elements(), - ); - } + let commitment = attachment.content().to_commitment(); + let elements = attachment.content().to_elements(); + self.add_map_entry(commitment, elements); } // note details / metadata diff --git a/crates/miden-protocol/src/transaction/kernel/memory.rs b/crates/miden-protocol/src/transaction/kernel/memory.rs index 1a94d6423f..aa4682dcf5 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 // diff --git a/crates/miden-protocol/src/transaction/outputs/notes.rs b/crates/miden-protocol/src/transaction/outputs/notes.rs index 874b61e279..7431178b7e 100644 --- a/crates/miden-protocol/src/transaction/outputs/notes.rs +++ b/crates/miden-protocol/src/transaction/outputs/notes.rs @@ -425,7 +425,7 @@ impl<'note> From<&'note OutputNote> for &'note NoteHeader { fn from(note: &'note OutputNote) -> Self { match note { OutputNote::Public(public_note) => public_note.header(), - OutputNote::Private(private_note) => private_note.as_header(), + OutputNote::Private(private_note) => private_note.header(), } } } @@ -623,7 +623,7 @@ impl PrivateOutputNote { } /// Returns a reference to the underlying note header. - pub fn as_header(&self) -> &NoteHeader { + pub fn header(&self) -> &NoteHeader { &self.header } diff --git a/crates/miden-standards/src/account/interface/component.rs b/crates/miden-standards/src/account/interface/component.rs index 0d784b3d28..455667f2c5 100644 --- a/crates/miden-standards/src/account/interface/component.rs +++ b/crates/miden-standards/src/account/interface/component.rs @@ -261,16 +261,14 @@ impl AccountComponentInterface { match attachments.iter().next() { Some(attachment) => { - let attachment_word = attachment.content().to_word(); let attachment_scheme = attachment.attachment_scheme().as_u16() as u32; - let num_words = attachment.content().num_words(); + let attachment_commitment = attachment.content().to_commitment(); body.push_str(&format!( " - push.{attachment_word} - push.{num_words} + push.{attachment_commitment} push.{attachment_scheme} - # => [attachment_scheme, num_words, ATTACHMENT, note_idx, pad(16)] + # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx, pad(16)] exec.::miden::protocol::output_note::add_attachment # => [pad(16)] ", diff --git a/crates/miden-standards/src/account/interface/mod.rs b/crates/miden-standards/src/account/interface/mod.rs index be775f882a..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,10 +166,11 @@ impl AccountInterface { // and the array elements as value. let mut code_builder = CodeBuilder::new(); for note in output_notes { - if let Some(attachment) = note.attachments().iter().next() - && let NoteAttachmentContent::Array(array) = attachment.content() - { - code_builder.add_advice_map_entry(array.commitment(), array.to_elements()); + if let Some(attachment) = note.attachments().iter().next() { + code_builder.add_advice_map_entry( + attachment.content().to_commitment(), + attachment.content().to_elements(), + ); } } 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 4fde2ef0b5..1f727f6633 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 @@ -242,12 +242,18 @@ async fn test_get_output_notes_commitment() -> anyhow::Result<()> { )?) .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 @@ -296,9 +302,8 @@ async fn test_get_output_notes_commitment() -> anyhow::Result<()> { # => [note_idx] push.{ATTACHMENT2} - push.{attachment2_num_words} push.{attachment_scheme2} - # => [attachment_scheme, attachment_num_words, ATTACHMENT, note_idx] + # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] exec.output_note::add_array_attachment # => [] @@ -320,8 +325,7 @@ async fn test_get_output_notes_commitment() -> anyhow::Result<()> { tag_2 = output_note_2.metadata().tag(), ASSET_2_KEY = asset_2.to_key_word(), ASSET_2_VALUE = asset_2.to_value_word(), - ATTACHMENT2 = output_note_2.attachments().get(0).unwrap().content().to_word(), - attachment2_num_words = output_note_2.attachments().get(0).unwrap().num_words(), + ATTACHMENT2 = output_note_2.attachments().get(0).unwrap().content().to_commitment(), attachment_scheme2 = output_note_2.attachments().get(0).unwrap().attachment_scheme().as_u16(), ); @@ -362,7 +366,7 @@ async fn test_get_output_notes_commitment() -> anyhow::Result<()> { exec_output.get_kernel_mem_word( OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_ATTACHMENT_0_OFFSET + NOTE_MEM_SIZE ), - output_note_2.attachments().get(0).unwrap().content().to_word(), + output_note_2.attachments().get(0).unwrap().content().to_commitment(), "Validate the output note 2 attachment", ); for attachment_idx in 1..4u32 { @@ -1161,7 +1165,7 @@ async fn test_get_assets() -> anyhow::Result<()> { } #[tokio::test] -async fn test_add_attachment_with_zero_num_words_fails() -> anyhow::Result<()> { +async fn test_add_attachment_with_missing_advice_map_entry_fails() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; let code = format!( @@ -1179,14 +1183,57 @@ async fn test_add_attachment_with_zero_num_words_fails() -> anyhow::Result<()> { exec.output_note::create # => [note_idx] - # try to add an attachment with num_words = 0 - padw push.0 push.0 - # => [attachment_scheme, attachment_num_words, ATTACHMENT, note_idx] + # try to add an attachment with a commitment that is not in the advice map + padw push.0 + # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] + exec.output_note::add_array_attachment + # => [] + end + ", + NOTE_TYPE_PUBLIC = NoteType::Public as u8, + ); + + let exec_output = tx_context.execute_code(&code).await; + + // The adv.push_mapvaln instruction will fail when the commitment is not in the advice map + assert!(exec_output.is_err()); + + Ok(()) +} + +#[tokio::test] +async fn test_add_attachment_with_zero_num_words_fails() -> anyhow::Result<()> { + // Insert an empty entry in the advice map so that adv.push_mapvaln succeeds but + // the kernel rejects the attachment because num_felts / 4 == 0. + let commitment = Word::from([42, 43, 44, 45u32]); + let tx_context = TransactionContextBuilder::with_existing_mock_account() + .extend_advice_map(vec![(commitment, vec![])]) + .build()?; + + let code = format!( + " + use miden::protocol::output_note + use miden::standards::note_tag::DEFAULT_TAG + use $kernel::prologue + + begin + exec.prologue::prepare_transaction + + push.1.2.3.4 + push.{NOTE_TYPE_PUBLIC} + push.DEFAULT_TAG + exec.output_note::create + # => [note_idx] + + push.{COMMITMENT} + push.0 + # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] exec.output_note::add_array_attachment # => [] end ", NOTE_TYPE_PUBLIC = NoteType::Public as u8, + COMMITMENT = commitment, ); let exec_output = tx_context.execute_code(&code).await; @@ -1230,7 +1277,7 @@ async fn test_add_word_attachment() -> anyhow::Result<()> { note_type = output_note.metadata().note_type() as u8, tag = output_note.metadata().tag().as_u32(), attachment_scheme = output_note.attachments().get(0).unwrap().attachment_scheme().as_u16(), - ATTACHMENT = output_note.attachments().get(0).unwrap().content().to_word(), + ATTACHMENT = Word::from([3, 4, 5, 6u32]), ); let tx_script = CodeBuilder::new().compile_tx_script(tx_script)?; @@ -1258,8 +1305,7 @@ async fn test_set_array_attachment() -> anyhow::Result<()> { let output_note = RawOutputNote::Full(NoteBuilder::new(account.id(), rng).attachment(attachment).build()?); - let attachment_content_word = output_note.attachments().get(0).unwrap().content().to_word(); - let attachment_num_words = output_note.attachments().get(0).unwrap().num_words(); + 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!( " @@ -1273,9 +1319,8 @@ async fn test_set_array_attachment() -> anyhow::Result<()> { # => [note_idx] push.{ATTACHMENT} - push.{attachment_num_words} push.{attachment_scheme} - # => [attachment_scheme, attachment_num_words, ATTACHMENT, note_idx] + # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] exec.output_note::add_array_attachment # => [] @@ -1287,8 +1332,7 @@ async fn test_set_array_attachment() -> anyhow::Result<()> { note_type = output_note.metadata().note_type() as u8, tag = output_note.metadata().tag().as_u32(), attachment_scheme = output_note.attachments().get(0).unwrap().attachment_scheme().as_u16(), - attachment_num_words = attachment_num_words, - ATTACHMENT = attachment_content_word, + ATTACHMENT = attachment_commitment, ); let tx_script = CodeBuilder::new().compile_tx_script(tx_script)?; @@ -1296,7 +1340,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![(attachment_content_word, elements)]) + .extend_advice_map(vec![(attachment_commitment, elements)]) .build()? .execute() .await?; 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 6ff95b364f..71794db6cc 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_tx.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_tx.rs @@ -319,9 +319,8 @@ async fn executed_transaction_output_notes() -> anyhow::Result<()> { # => [note_idx = 2] push.{ATTACHMENT3} - push.{attachment3_num_words} push.{attachment_scheme3} - # => [attachment_scheme, attachment_num_words, ATTACHMENT, note_idx] + # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] exec.output_note::add_array_attachment # => [] end @@ -340,10 +339,9 @@ async fn executed_transaction_output_notes() -> anyhow::Result<()> { NOTETYPE2 = note_type2 as u8, NOTETYPE3 = note_type3 as u8, attachment_scheme2 = attachment2.attachment_scheme().as_u16(), - ATTACHMENT2 = attachment2.content().to_word(), + ATTACHMENT2 = Word::from([2, 3, 4, 5u32]), attachment_scheme3 = attachment3.attachment_scheme().as_u16(), - attachment3_num_words = attachment3.num_words(), - ATTACHMENT3 = attachment3.content().to_word(), + ATTACHMENT3 = attachment3.content().to_commitment(), ); let tx_script = CodeBuilder::with_mock_libraries().compile_tx_script(tx_script_src)?; @@ -358,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.to_elements())]) + .extend_advice_map(vec![(attachment3.content().to_commitment(), array.to_elements())]) .extend_expected_output_notes(vec![ RawOutputNote::Full(expected_output_note_2.clone()), RawOutputNote::Full(expected_output_note_3.clone()), diff --git a/crates/miden-testing/src/standards/network_account_target.rs b/crates/miden-testing/src/standards/network_account_target.rs index ef8b10c011..df9647bd35 100644 --- a/crates/miden-testing/src/standards/network_account_target.rs +++ b/crates/miden-testing/src/standards/network_account_target.rs @@ -4,6 +4,7 @@ use miden_protocol::Felt; use miden_protocol::account::AccountStorageMode; use miden_protocol::note::{ NoteAttachment, + NoteAttachmentContent, NoteAttachments, NoteMetadata, NoteMetadataHeader, @@ -37,7 +38,7 @@ 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.{attachment_commitment} push.{metadata_word} exec.note::metadata_into_attachment_header # => [attachment_0_num_words, attachment_0_scheme, NOTE_ATTACHMENT] @@ -54,7 +55,10 @@ async fn network_account_target_get_id() -> anyhow::Result<()> { end "#, metadata_word = metadata_word, - attachment_word = attachment.content().to_word(), + attachment_commitment = match attachment.content() { + NoteAttachmentContent::Word(word) => *word, + _ => unreachable!("expected word attachment"), + }, ); let exec_output = CodeExecutor::with_default_host().run(&source).await?; @@ -73,7 +77,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 raw_attachment_word = match attachment.content() { + NoteAttachmentContent::Word(word) => *word, + _ => unreachable!("expected word attachment"), + }; let source = format!( r#" @@ -104,7 +111,7 @@ async fn network_account_target_new_attachment() -> anyhow::Result<()> { ); let word = exec_output.stack.get_word(1).unwrap(); - assert_eq!(word, attachment_word); + assert_eq!(word, raw_attachment_word); Ok(()) } diff --git a/crates/miden-testing/src/utils.rs b/crates/miden-testing/src/utils.rs index 794491f5c9..4ee52ba108 100644 --- a/crates/miden-testing/src/utils.rs +++ b/crates/miden-testing/src/utils.rs @@ -7,14 +7,7 @@ use miden_protocol::account::AccountId; use miden_protocol::asset::Asset; use miden_protocol::crypto::rand::FeltRng; use miden_protocol::errors::NoteError; -use miden_protocol::note::{ - Note, - NoteAssets, - NoteAttachmentContent, - NoteMetadata, - NoteTag, - NoteType, -}; +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; @@ -250,26 +243,22 @@ fn note_script_that_creates_notes<'note>( )); for attachment in note.attachments().iter() { - let num_words = attachment.num_words(); + let attachment_scheme = attachment.attachment_scheme().as_u16(); + let commitment = attachment.content().to_commitment(); out.push_str(&format!( " - dup - push.{ATTACHMENT} - push.{num_words} - push.{attachment_scheme} - # => [attachment_scheme, num_words, ATTACHMENT, note_idx, note_idx] - exec.output_note::add_attachment - # => [note_idx] - ", - ATTACHMENT = attachment.content().to_word(), - attachment_scheme = attachment.attachment_scheme().as_u16(), + dup + push.{commitment} + push.{attachment_scheme} + # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx, note_idx] + exec.output_note::add_attachment + # => [note_idx] + ", )); - // For array attachments, add the elements to the advice map keyed by the commitment. - if let NoteAttachmentContent::Array(array) = attachment.content() { - advice_map.insert(array.commitment(), array.to_elements()); - } + // 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() { diff --git a/crates/miden-tx/src/errors/mod.rs b/crates/miden-tx/src/errors/mod.rs index ef5d7a4a3f..ecf814fb3e 100644 --- a/crates/miden-tx/src/errors/mod.rs +++ b/crates/miden-tx/src/errors/mod.rs @@ -259,7 +259,11 @@ pub enum TransactionKernelError { )] PublicNoteMissingDetails(NoteMetadata, Word), #[error( - "commitment of note attachment {actual} does not match attachment {provided} provided to add_attachment" + "commitment of note word attachment {actual} does not match attachment {provided} provided to add_attachment" + )] + NoteAttachmentWordMismatch { actual: Word, provided: Word }, + #[error( + "commitment of note array attachment {actual} does not match attachment {provided} provided to add_attachment" )] NoteAttachmentArrayMismatch { actual: Word, provided: Word }, #[error( diff --git a/crates/miden-tx/src/host/tx_event.rs b/crates/miden-tx/src/host/tx_event.rs index f7a995c633..5b2ae612e6 100644 --- a/crates/miden-tx/src/host/tx_event.rs +++ b/crates/miden-tx/src/host/tx_event.rs @@ -426,19 +426,19 @@ impl TransactionEvent { TransactionEventId::NoteBeforeAddAttachment => { // Expected stack state: [ - // event, num_attachments, note_ptr, attachment_scheme, - // attachment_num_words, ATTACHMENT, note_idx + // event, num_attachments, note_ptr, num_words, attachment_scheme, + // ATTACHMENT_COMMITMENT // ] let note_ptr = process.get_stack_item(2); - let attachment_scheme = process.get_stack_item(3); - let attachment_num_words = process.get_stack_item(4); - let attachment = process.get_stack_word(5); + let num_words = process.get_stack_item(3); + let attachment_scheme = process.get_stack_item(4); + let attachment_commitment = process.get_stack_word(5); let (note_idx, attachment) = extract_note_attachment( attachment_scheme, - attachment_num_words, - attachment, + num_words, + attachment_commitment, note_ptr, process.advice_provider(), )?; @@ -737,7 +737,7 @@ fn build_note_metadata( fn extract_note_attachment( attachment_scheme: Felt, attachment_num_words: Felt, - attachment: Word, + attachment_commitment: Word, note_ptr: Felt, advice_provider: &AdviceProvider, ) -> Result<(usize, NoteAttachment), TransactionKernelError> { @@ -758,22 +758,54 @@ fn extract_note_attachment( }) })?; + // 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 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", + )); + } + + let words: Vec = elements + .chunks_exact(WORD_SIZE) + .map(|chunk| Word::from([chunk[0], chunk[1], chunk[2], chunk[3]])) + .collect(); + let attachment_content = match num_words { 0 => { return Err(TransactionKernelError::other("attachment num_words must be > 0")); }, - 1 => NoteAttachmentContent::Word(attachment), - _ => { - 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", - ) - })?; + 1 => { + if words.len() != 1 { + return Err(TransactionKernelError::other(format!( + "word attachment expected 1 word from advice provider, got {}", + words.len() + ))); + } - let words: Vec = elements - .chunks_exact(WORD_SIZE) - .map(|chunk| Word::from([chunk[0], chunk[1], chunk[2], chunk[3]])) - .collect(); + // Verify the commitment matches hash(word) + let computed_commitment = Hasher::hash_elements(elements); + if computed_commitment != attachment_commitment { + return Err(TransactionKernelError::NoteAttachmentWordMismatch { + actual: computed_commitment, + provided: attachment_commitment, + }); + } + + NoteAttachmentContent::Word(words[0]) + }, + _ => { let array_attachment = NoteAttachmentArray::new(words).map_err(|source| { TransactionKernelError::other_with_source( "failed to construct note attachment array", @@ -781,10 +813,10 @@ fn extract_note_attachment( ) })?; - if array_attachment.commitment() != attachment { + if array_attachment.commitment() != attachment_commitment { return Err(TransactionKernelError::NoteAttachmentArrayMismatch { actual: array_attachment.commitment(), - provided: attachment, + provided: attachment_commitment, }); } From 002a4d4ce11b45f613158ed786eb4ce954c015ec Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Mon, 20 Apr 2026 16:18:59 +0200 Subject: [PATCH 19/48] fix: broken doc links --- crates/miden-standards/src/note/burn.rs | 2 +- crates/miden-standards/src/note/mint.rs | 2 +- crates/miden-standards/src/note/network_note.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/miden-standards/src/note/burn.rs b/crates/miden-standards/src/note/burn.rs index 7c68333811..eb5270867a 100644 --- a/crates/miden-standards/src/note/burn.rs +++ b/crates/miden-standards/src/note/burn.rs @@ -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 diff --git a/crates/miden-standards/src/note/mint.rs b/crates/miden-standards/src/note/mint.rs index 4709916fc5..b7b1dc640c 100644 --- a/crates/miden-standards/src/note/mint.rs +++ b/crates/miden-standards/src/note/mint.rs @@ -81,7 +81,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 diff --git a/crates/miden-standards/src/note/network_note.rs b/crates/miden-standards/src/note/network_note.rs index 1d6933bed4..b367b4bca6 100644 --- a/crates/miden-standards/src/note/network_note.rs +++ b/crates/miden-standards/src/note/network_note.rs @@ -63,7 +63,7 @@ impl AccountTargetNetworkNote { self.target().execution_hint() } - /// Returns the raw [`NoteAttachment`] from the note's attachments. + /// Returns the raw [`NoteAttachments`] from the note's attachments. pub fn attachments(&self) -> &NoteAttachments { self.note.attachments() } From f0eccbce17f71cc0c5631043ab94aaef3232cbd6 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Mon, 20 Apr 2026 16:44:32 +0200 Subject: [PATCH 20/48] chore: add changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f5c8dc9ce..a1a1db4013 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ ### Features - 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 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)). From e1fdeb5a0211222b2befdd796443de125a6ee750 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Mon, 20 Apr 2026 19:41:21 +0200 Subject: [PATCH 21/48] chore: update PSWAP note with multiple attachments --- .../asm/standards/notes/pswap.masm | 18 +++-- crates/miden-standards/src/note/pswap.rs | 73 +++++++++++-------- crates/miden-testing/tests/scripts/pswap.rs | 35 ++++++--- 3 files changed, 79 insertions(+), 47 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 377fbf2a96..fe61a410ef 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -234,14 +234,15 @@ proc create_p2id_note # 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.0 + # => [attachment_scheme=0, [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) @@ -356,13 +357,14 @@ proc create_remainder_note # => [] # Set attachment: aux = amt_payout at Word[0] + 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.0 + # => [attachment_scheme=0, [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/note/pswap.rs b/crates/miden-standards/src/note/pswap.rs index d235b0feb5..a2e2a517ec 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, @@ -214,8 +215,7 @@ pub struct PswapNote { offered_asset: FungibleAsset, - #[builder(default)] - attachment: NoteAttachment, + attachment: Option, } impl PswapNoteBuilder @@ -315,13 +315,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 @@ -585,10 +585,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. @@ -622,14 +626,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() } } @@ -650,11 +654,14 @@ 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) + .expect("PswapNote attachments should be valid"); - Note::new(assets, metadata, recipient) + Note::with_attachments(assets, metadata, recipient, attachments) } } @@ -679,14 +686,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-testing/tests/scripts/pswap.rs b/crates/miden-testing/tests/scripts/pswap.rs index 118f1ce7d9..e24cddaabd 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,13 +193,13 @@ 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 aux_word = first_attachment_word(output_p2id.attachments()); let fill_amount_from_aux = aux_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(), + first_attachment_word(p2id_note.attachments()), aux_word, "Rust-predicted P2ID attachment does not match the MASM-produced one", ); @@ -208,7 +224,7 @@ 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 remainder_aux = first_attachment_word(output_remainder.attachments()); let amt_payout_from_aux = remainder_aux[0].as_canonical_u64(); let expected_payout = pswap.calculate_offered_for_requested(fill_amount_from_aux)?; @@ -238,7 +254,6 @@ async fn pswap_note_alice_reconstructs_and_consumes_p2id() -> anyhow::Result<()> ]); let remainder_attachment = NoteAttachment::new_word(NoteAttachmentScheme::none(), remainder_attachment_word); - let reconstructed_remainder: Note = PswapNote::builder() .sender(bob.id()) .storage(remainder_storage) @@ -268,7 +283,7 @@ 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(), + first_attachment_word(reconstructed_remainder.attachments()), remainder_aux, "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", ); From 4e0495f5b7cad303b656224ff273c1dd9ba0c37a Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Fri, 24 Apr 2026 10:01:34 +0200 Subject: [PATCH 22/48] fix: default to empty attachments in pswap conversion --- crates/miden-standards/src/note/pswap.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/miden-standards/src/note/pswap.rs b/crates/miden-standards/src/note/pswap.rs index a2e2a517ec..3fcf123867 100644 --- a/crates/miden-standards/src/note/pswap.rs +++ b/crates/miden-standards/src/note/pswap.rs @@ -656,10 +656,7 @@ impl From for Note { let metadata = NoteMetadata::new(pswap.sender, pswap.note_type).with_tag(tag); - let attachments = pswap - .attachment - .map(NoteAttachments::from) - .expect("PswapNote attachments should be valid"); + let attachments = pswap.attachment.map(NoteAttachments::from).unwrap_or_default(); Note::with_attachments(assets, metadata, recipient, attachments) } From 9a347acdbfa7e1ab11567d266ced43ab75954374 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Fri, 24 Apr 2026 10:29:24 +0200 Subject: [PATCH 23/48] fix: post-merge NoteAttachment -> NoteAttachments --- crates/miden-testing/tests/scripts/faucet.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/miden-testing/tests/scripts/faucet.rs b/crates/miden-testing/tests/scripts/faucet.rs index 367c3ee3bd..bb71e6aee7 100644 --- a/crates/miden-testing/tests/scripts/faucet.rs +++ b/crates/miden-testing/tests/scripts/faucet.rs @@ -1427,7 +1427,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())); @@ -1485,7 +1485,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())); From 3887e3de6d4eef229ca8275399ae8aed9a5dfbfa Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Mon, 27 Apr 2026 16:55:16 +0200 Subject: [PATCH 24/48] chore: remove num_words encoding from kernel MASM --- .../kernels/transaction/lib/output_note.masm | 100 ++++++------------ crates/miden-protocol/src/note/attachment.rs | 5 + crates/miden-protocol/src/note/metadata.rs | 10 +- crates/miden-tx/src/errors/mod.rs | 8 +- crates/miden-tx/src/host/tx_event.rs | 83 ++++----------- 5 files changed, 67 insertions(+), 139 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm b/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm index 21155991ba..64191ac5a2 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm @@ -263,52 +263,52 @@ pub proc add_attachment movdn.4 # => [ATTACHMENT_COMMITMENT, attachment_scheme, note_idx] - # validate commitment against advice data and derive num_words + # validate preimage for commitment is available and number of committed words is within limits dupw exec.validate_attachment - # => [num_words, ATTACHMENT_COMMITMENT, note_idx] + # => [ATTACHMENT_COMMITMENT, attachment_scheme, note_idx] - movup.5 swap - # => [num_words, attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] + movup.4 + # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] # get note_ptr from note_idx - movup.6 exec.memory::get_output_note_ptr - # => [note_ptr, num_words, attachment_scheme, ATTACHMENT_COMMITMENT] + 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, num_words, attachment_scheme, ATTACHMENT_COMMITMENT] + # => [num_attachments, note_ptr, attachment_scheme, ATTACHMENT_COMMITMENT] dup lt.MAX_ATTACHMENTS_PER_NOTE assert.err=ERR_OUTPUT_NOTE_TOO_MANY_ATTACHMENTS - # => [num_attachments, note_ptr, num_words, attachment_scheme, ATTACHMENT_COMMITMENT] + # => [num_attachments, note_ptr, attachment_scheme, ATTACHMENT_COMMITMENT] # emit event emit.NOTE_BEFORE_ADD_ATTACHMENT_EVENT - # => [num_attachments, note_ptr, num_words, attachment_scheme, ATTACHMENT_COMMITMENT] + # => [num_attachments, note_ptr, attachment_scheme, ATTACHMENT_COMMITMENT] - # --- Update attachment into in output note memory --- + # --- Add attachment in output note memory --- - swapw - # => [ATTACHMENT_COMMITMENT, num_attachments, note_ptr, num_words, attachment_scheme] + movup.6 movup.6 movup.6 movup.6 + # => [ATTACHMENT_COMMITMENT, num_attachments, note_ptr, attachment_scheme] # use attachment_idx = num_attachments dup.4 dup.6 - # => [note_ptr, attachment_idx, ATTACHMENT_COMMITMENT, num_attachments, note_ptr, num_words, attachment_scheme] + # => [note_ptr, attachment_idx, ATTACHMENT_COMMITMENT, num_attachments, note_ptr, attachment_scheme] # store commitment in note memory exec.memory::set_output_note_attachment - # => [num_attachments, note_ptr, num_words, attachment_scheme] + # => [num_attachments, note_ptr, attachment_scheme] # increment number of attachments dup add.1 - # => [new_num_attachments, num_attachments, note_ptr, num_words, attachment_scheme] + # => [new_num_attachments, num_attachments, note_ptr, attachment_scheme] dup.2 exec.memory::set_output_note_num_attachments - # => [num_attachments, note_ptr, num_words, attachment_scheme] + # => [num_attachments, note_ptr, attachment_scheme] movup.3 movdn.2 - # => [num_attachments, note_ptr, attachment_scheme, num_words] + # => [num_attachments, note_ptr, attachment_scheme] - exec.set_attachment_num_words_and_scheme + exec.set_attachment_schemes # => [] end @@ -340,12 +340,11 @@ end #! Advice map: { #! ATTACHMENT_COMMITMENT: [[ATTACHMENT_ELEMENTS]], #! } -#! Outputs: [num_words] +#! Outputs: [] #! #! Panics if: #! - the number of elements is not a multiple of 4, or num_words is zero or exceeds 254. #! - the computed hash of fetched elements does not match ATTACHMENT_COMMITMENT. -@locals(1) proc validate_attachment # push the attachment elements from the advice map onto the advice stack adv.push_mapvaln @@ -367,22 +366,19 @@ proc validate_attachment dup neq.0 assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_CANNOT_BE_ZERO dup u32lte.MAX_ATTACHMENT_WORD_SIZE assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_MAX_EXCEEDED # OS => [num_words, ATTACHMENT_COMMITMENT] - - # save num_words for later - dup movdn.5 - # OS => [num_words, ATTACHMENT_COMMITMENT, num_words] + # 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, num_words] + # 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 => [num_words] + # OS => [] end #! Builds the provided inputs into the NOTE_METADATA_HEADER word. @@ -439,75 +435,49 @@ end #! #! WARNING: The attachment num_words and scheme must be valid. #! -#! Inputs: [num_attachments, note_ptr, attachment_scheme, attachment_num_words] +#! 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_num_words is the number of words of the attachment content. #! - note_ptr is the memory address at which the output note data begins. -proc set_attachment_num_words_and_scheme - # the size and schemes are stored as follows in the second and third felt: - # 2nd felt: [ - # attachment_3_num_words (8 bits) | attachment_2_num_words (8 bits) | attachment_1_num_words (8 bits) | - # attachment_0_num_words (8 bits) | note_tag (32 bits) - # ] +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) # ] - # -> the num_words needs to be shifted left by 32 + (num_attachments * 8) - # -> the scheme needs to be shifted left by num_attachments * 16 + # -> current scheme needs to be shifted left by num_attachments * 16 # Prepare scheme and num_words. # -------------------------------------------------------------------------------------------- + movup.2 swap + # => [num_attachments, attachment_scheme, note_ptr] + # 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) - movup.2 dup.1 - # => [num_attachments, attachment_scheme, num_attachments, note_ptr, attachment_num_words] - mul.16 pow2 mul - # => [attachment_scheme_shifted, num_attachments, note_ptr, attachment_num_words] - - # shift the num_words left by num_attachments * 8 + 32 bits - # split into a u32shl (by num_attachments * 8, max 24) and a multiplication by 2^32 - movup.3 movup.2 mul.8 - # => [num_attachments * 8, attachment_num_words, attachment_scheme_shifted, note_ptr] - - u32shl mul.0x100000000 - # => [attachment_num_words_shifted, attachment_scheme_shifted, note_ptr] + # => [attachment_scheme_shifted, note_ptr] # Fetch and update metadata header. # -------------------------------------------------------------------------------------------- dup.2 exec.memory::get_output_note_metadata_header - # => [METADATA_HEADER, attachment_num_words_shifted, attachment_scheme_shifted, note_ptr] - # => [ - # [sender_id_suffix_type_version, sender_id_prefix, attachment_num_words_and_tag, attachment_schemes], - # attachment_num_words_shifted, attachment_scheme_shifted, note_ptr - # ] - - # merge num_words into existing sizes and tag - # (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.2 movup.4 - add movdn.2 + # => [METADATA_HEADER, attachment_scheme_shifted, note_ptr] # => [ - # [sender_id_suffix_type_version, sender_id_prefix, new_attachment_num_words_and_tag, attachment_schemes], + # [sender_id_suffix_type_version, sender_id_prefix, tag, attachment_schemes], # attachment_scheme_shifted, note_ptr # ] # merge scheme into existing schemes - # (using add for the same reason as above) + # (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, new_attachment_num_words_and_tag, new_attachment_schemes], - # note_ptr - # ] + # => [[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 diff --git a/crates/miden-protocol/src/note/attachment.rs b/crates/miden-protocol/src/note/attachment.rs index 248eea7a42..1589c4e71d 100644 --- a/crates/miden-protocol/src/note/attachment.rs +++ b/crates/miden-protocol/src/note/attachment.rs @@ -101,6 +101,11 @@ impl NoteAttachment { &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`]). diff --git a/crates/miden-protocol/src/note/metadata.rs b/crates/miden-protocol/src/note/metadata.rs index 2e0988799f..217f8899e5 100644 --- a/crates/miden-protocol/src/note/metadata.rs +++ b/crates/miden-protocol/src/note/metadata.rs @@ -128,9 +128,7 @@ impl Deserializable for NoteMetadata { /// ```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: [attachment_3_num_words (8 bits) | attachment_2_num_words (8 bits) | -/// attachment_1_num_words (8 bits) | attachment_0_num_words (8 bits) | -/// note_tag (32 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)] /// ``` @@ -139,8 +137,8 @@ impl Deserializable for NoteMetadata { /// - 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: Max value is `0xFEFEFEFE_FFFFFFFF` (num_words capped at 254, tag at u32::MAX), which -/// is less than the Goldilocks prime `p = 2^64 - 2^32 + 1`. +/// - 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`. /// @@ -217,7 +215,7 @@ impl NoteMetadataHeader { self.metadata.note_type, ); word[1] = self.metadata.sender.prefix().as_felt(); - word[2] = merge_tag_and_num_words(self.metadata.tag, num_words); + word[2] = self.metadata.tag.into(); word[3] = merge_schemes(schemes); word } diff --git a/crates/miden-tx/src/errors/mod.rs b/crates/miden-tx/src/errors/mod.rs index ecf814fb3e..08242858a3 100644 --- a/crates/miden-tx/src/errors/mod.rs +++ b/crates/miden-tx/src/errors/mod.rs @@ -259,13 +259,9 @@ pub enum TransactionKernelError { )] PublicNoteMissingDetails(NoteMetadata, Word), #[error( - "commitment of note word attachment {actual} does not match attachment {provided} provided to add_attachment" + "commitment of note attachment advice data is {actual} which does not match commitment {provided} provided to add_attachment" )] - NoteAttachmentWordMismatch { actual: Word, provided: Word }, - #[error( - "commitment of note array attachment {actual} does not match attachment {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/host/tx_event.rs b/crates/miden-tx/src/host/tx_event.rs index 5b2ae612e6..fe07c1e2d2 100644 --- a/crates/miden-tx/src/host/tx_event.rs +++ b/crates/miden-tx/src/host/tx_event.rs @@ -13,8 +13,6 @@ use miden_protocol::account::{ use miden_protocol::asset::{Asset, AssetVault, AssetVaultKey, FungibleAsset}; use miden_protocol::note::{ NoteAttachment, - NoteAttachmentArray, - NoteAttachmentContent, NoteAttachmentScheme, NoteId, NoteMetadata, @@ -426,18 +424,15 @@ impl TransactionEvent { TransactionEventId::NoteBeforeAddAttachment => { // Expected stack state: [ - // event, num_attachments, note_ptr, num_words, attachment_scheme, - // ATTACHMENT_COMMITMENT + // event, num_attachments, note_ptr, attachment_scheme, ATTACHMENT_COMMITMENT // ] let note_ptr = process.get_stack_item(2); - let num_words = process.get_stack_item(3); - let attachment_scheme = process.get_stack_item(4); - let attachment_commitment = process.get_stack_word(5); + 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, - num_words, attachment_commitment, note_ptr, process.advice_provider(), @@ -736,17 +731,12 @@ fn build_note_metadata( fn extract_note_attachment( attachment_scheme: Felt, - attachment_num_words: Felt, attachment_commitment: Word, note_ptr: Felt, advice_provider: &AdviceProvider, ) -> Result<(usize, NoteAttachment), TransactionKernelError> { let note_idx = note_ptr_to_idx(note_ptr)?; - let num_words = u8::try_from(attachment_num_words.as_canonical_u64()).map_err(|_| { - TransactionKernelError::other("failed to convert attachment num_words to u8") - })?; - 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| { @@ -782,57 +772,26 @@ fn extract_note_attachment( .map(|chunk| Word::from([chunk[0], chunk[1], chunk[2], chunk[3]])) .collect(); - let attachment_content = match num_words { - 0 => { - return Err(TransactionKernelError::other("attachment num_words must be > 0")); - }, - 1 => { - if words.len() != 1 { - return Err(TransactionKernelError::other(format!( - "word attachment expected 1 word from advice provider, got {}", - words.len() - ))); - } - - // Verify the commitment matches hash(word) - let computed_commitment = Hasher::hash_elements(elements); - if computed_commitment != attachment_commitment { - return Err(TransactionKernelError::NoteAttachmentWordMismatch { - actual: computed_commitment, - provided: attachment_commitment, - }); - } - - NoteAttachmentContent::Word(words[0]) - }, - _ => { - let array_attachment = NoteAttachmentArray::new(words).map_err(|source| { - TransactionKernelError::other_with_source( - "failed to construct note attachment array", - source, - ) - })?; - - if array_attachment.commitment() != attachment_commitment { - return Err(TransactionKernelError::NoteAttachmentArrayMismatch { - actual: array_attachment.commitment(), - provided: attachment_commitment, - }); - } - - if array_attachment.num_words() != num_words { - return Err(TransactionKernelError::other(format!( - "array attachment num_words {} does not match declared num_words {}", - array_attachment.num_words(), - num_words - ))); - } - - NoteAttachmentContent::Array(array_attachment) - }, + 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); + 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)) } From 4d0aba034fe16e227b8ebe2e5759bdfc6236d62b Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Mon, 27 Apr 2026 19:41:22 +0200 Subject: [PATCH 25/48] chore: drop num words from note attachment header --- .../kernels/transaction/lib/output_note.masm | 5 +- crates/miden-protocol/asm/protocol/note.masm | 28 ++----- crates/miden-protocol/src/note/attachment.rs | 82 ++++++++++--------- crates/miden-protocol/src/note/metadata.rs | 60 +++----------- .../attachments/network_account_target.masm | 6 +- .../asm/standards/notes/pswap.masm | 8 +- .../src/note/standard_note_attachment.rs | 2 +- .../src/standards/network_account_target.rs | 2 - 8 files changed, 71 insertions(+), 122 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm b/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm index 64191ac5a2..50fc2f635d 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm @@ -305,9 +305,6 @@ pub proc add_attachment dup.2 exec.memory::set_output_note_num_attachments # => [num_attachments, note_ptr, attachment_scheme] - movup.3 movdn.2 - # => [num_attachments, note_ptr, attachment_scheme] - exec.set_attachment_schemes # => [] end @@ -465,7 +462,7 @@ proc set_attachment_schemes # Fetch and update metadata header. # -------------------------------------------------------------------------------------------- - dup.2 exec.memory::get_output_note_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], diff --git a/crates/miden-protocol/asm/protocol/note.masm b/crates/miden-protocol/asm/protocol/note.masm index de4a2cbf4c..ad88bc8f24 100644 --- a/crates/miden-protocol/asm/protocol/note.masm +++ b/crates/miden-protocol/asm/protocol/note.masm @@ -202,34 +202,22 @@ end #! that supported just one attachment per note. #! #! Inputs: [METADATA_HEADER] -#! Outputs: [attachment_0_num_words, attachment_0_scheme] +#! Outputs: [attachment_0_scheme] #! #! Where: #! - METADATA_HEADER is the metadata word of a note. -#! - attachment_0_num_words is the number of words of the first attachment (0 if absent). -#! - attachment_0_scheme is the scheme of the first attachment. +#! - attachment_0_scheme is the scheme of the first attachment (0 if absent). #! #! Invocation: exec pub proc metadata_into_attachment_header - # => [sender_id_suffix_type_version, sender_id_prefix, attachment_num_words_and_tag, schemes] + # => [sender_id_suffix_type_version, sender_id_prefix, tag, schemes] - drop drop - # => [attachment_num_words_and_tag, schemes] - - # extract attachment_0_num_words from attachment_num_words_and_tag: - # u32split converts into num_words_packed (upper 32 bits) and tag (lower 32 bits) - u32split drop - # => [num_words_packed, schemes] - # => [attachment_3_num_words (8 bits) | attachment_2_num_words (8 bits) | - # attachment_1_num_words (8 bits) | attachment_0_num_words (8 bits)] - - # extract attachment 0 num_words by taking only the lower 8 bits - u32and.0xff - # [attachment_0_num_words, schemes] + drop drop drop + # => [schemes] # extract attachment 0 scheme by taking only the lower 16 bits - swap u32and.0xffff swap - # [attachment_0_num_words, attachment_0_scheme] + u32and.0xffff + # [attachment_0_scheme] end #! Extracts the note type from the provided metadata header. @@ -242,7 +230,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/src/note/attachment.rs b/crates/miden-protocol/src/note/attachment.rs index 1589c4e71d..0a1fa265d4 100644 --- a/crates/miden-protocol/src/note/attachment.rs +++ b/crates/miden-protocol/src/note/attachment.rs @@ -388,8 +388,10 @@ impl NoteAttachmentScheme { // CONSTANTS // -------------------------------------------------------------------------------------------- - /// The reserved value to signal an absent note attachment scheme. - const NONE: u16 = 0; + 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. /// @@ -405,8 +407,14 @@ impl NoteAttachmentScheme { /// /// # Errors /// + /// TODO /// Returns an error if `attachment_scheme` exceeds [`Self::MAX`]. pub fn new(attachment_scheme: u16) -> Result { + if attachment_scheme == Self::RESERVED { + // TODO: Replace with new error type. + return Err(NoteError::NoteAttachmentSchemeExceeded(attachment_scheme as u32)); + } + if attachment_scheme > Self::MAX.as_u16() { return Err(NoteError::NoteAttachmentSchemeExceeded(attachment_scheme as u32)); } @@ -417,8 +425,10 @@ impl NoteAttachmentScheme { /// /// # Panics /// + /// TODO /// Panics if `attachment_scheme` 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) } @@ -493,50 +503,49 @@ impl Deserializable for NoteAttachmentScheme { /// - `num_words > 1`: array attachment (a commitment to a set of felts) #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct NoteAttachmentHeader { - scheme: NoteAttachmentScheme, - num_words: u8, + scheme: Option, } impl NoteAttachmentHeader { // CONSTRUCTORS // -------------------------------------------------------------------------------------------- - /// Creates a new [`NoteAttachmentHeader`]. - /// - /// # Errors - /// - /// Returns an error if `num_words` exceeds [`NoteAttachment::MAX_NUM_WORDS`]. - pub fn new(scheme: NoteAttachmentScheme, num_words: u8) -> Result { - if num_words > NoteAttachment::MAX_NUM_WORDS { - return Err(NoteError::NoteAttachmentHeaderSizeExceeded(num_words)); - } - Ok(Self { scheme, num_words }) + /// 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: NoteAttachmentScheme::none(), - num_words: 0, - } + Self { scheme: None } } // ACCESSORS // -------------------------------------------------------------------------------------------- /// Returns the attachment scheme. - pub const fn scheme(&self) -> NoteAttachmentScheme { + pub const fn scheme(&self) -> Option { self.scheme } - /// Returns the number of words in the attachment. - pub const fn num_words(&self) -> u8 { - self.num_words + /// 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.num_words == 0 && self.scheme.is_none() + self.scheme.is_none() } } @@ -546,22 +555,26 @@ impl Default for NoteAttachmentHeader { } } +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); - self.num_words.write_into(target); } fn get_size_hint(&self) -> usize { - self.scheme.get_size_hint() + core::mem::size_of::() + self.scheme.get_size_hint() } } impl Deserializable for NoteAttachmentHeader { fn read_from(source: &mut R) -> Result { - let scheme = NoteAttachmentScheme::read_from(source)?; - let size = u8::read_from(source)?; - Self::new(scheme, size).map_err(|err| DeserializationError::InvalidValue(err.to_string())) + let scheme = Option::::read_from(source)?; + Ok(Self::new_maybe(scheme)) } } @@ -676,9 +689,7 @@ impl NoteAttachments { 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(), attachment.num_words()) - .expect("attachment num_words should not exceed NoteAttachment::MAX_NUM_WORDS"); + headers[i] = NoteAttachmentHeader::new(attachment.attachment_scheme()); } headers } @@ -816,7 +827,7 @@ mod tests { #[test] fn note_attachment_header_serde() -> anyhow::Result<()> { - let header = NoteAttachmentHeader::new(NoteAttachmentScheme::new(42)?, 10)?; + let header = NoteAttachmentHeader::new(NoteAttachmentScheme::new(42)?); let deserialized = NoteAttachmentHeader::read_from_bytes(&header.to_bytes())?; assert_eq!(header, deserialized); Ok(()) @@ -826,7 +837,6 @@ mod tests { fn note_attachment_header_absent() { let header = NoteAttachmentHeader::absent(); assert!(header.is_absent()); - assert_eq!(header.num_words(), 0); assert!(header.scheme().is_none()); } @@ -897,10 +907,8 @@ mod tests { ])?; let headers = attachments.to_headers(); - assert_eq!(headers[0].scheme(), NoteAttachmentScheme::new(42)?); - assert_eq!(headers[0].num_words(), 1); - assert_eq!(headers[1].scheme(), NoteAttachmentScheme::new(100)?); - assert_eq!(headers[1].num_words(), 2); + 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()); diff --git a/crates/miden-protocol/src/note/metadata.rs b/crates/miden-protocol/src/note/metadata.rs index 217f8899e5..3409c8ce20 100644 --- a/crates/miden-protocol/src/note/metadata.rs +++ b/crates/miden-protocol/src/note/metadata.rs @@ -11,7 +11,7 @@ use super::{ Word, }; use crate::Hasher; -use crate::note::{NoteAttachmentHeader, NoteAttachmentScheme, NoteAttachments}; +use crate::note::{NoteAttachmentHeader, NoteAttachments}; // NOTE METADATA // ================================================================================================ @@ -207,8 +207,6 @@ impl NoteMetadataHeader { /// /// See [`NoteMetadataHeader`] docs for the layout. pub fn to_metadata_word(&self) -> Word { - let (num_words, schemes) = extract_num_words_and_schemes(&self.attachment_headers); - let mut word = Word::empty(); word[0] = merge_sender_suffix_and_note_type( self.metadata.sender.suffix(), @@ -216,7 +214,7 @@ impl NoteMetadataHeader { ); word[1] = self.metadata.sender.prefix().as_felt(); word[2] = self.metadata.tag.into(); - word[3] = merge_schemes(schemes); + word[3] = merge_schemes(self.attachment_headers); word } @@ -289,22 +287,6 @@ impl Deserializable for NoteMetadataHeader { // HELPER FUNCTIONS // ================================================================================================ -/// Extracts the num_words and schemes arrays from the attachment headers. -fn extract_num_words_and_schemes( - headers: &[NoteAttachmentHeader; NoteAttachments::MAX_COUNT], -) -> ( - [u8; NoteAttachments::MAX_COUNT], - [NoteAttachmentScheme; NoteAttachments::MAX_COUNT], -) { - let mut num_words = [0u8; NoteAttachments::MAX_COUNT]; - let mut schemes = [NoteAttachmentScheme::none(); NoteAttachments::MAX_COUNT]; - for (i, header) in headers.iter().enumerate() { - num_words[i] = header.num_words(); - schemes[i] = header.scheme(); - } - (num_words, schemes) -} - /// Merges the suffix of an [`AccountId`] and note metadata into a single [`Felt`]. /// /// The layout is as follows: @@ -331,39 +313,21 @@ fn merge_sender_suffix_and_note_type(sender_id_suffix: Felt, note_type: NoteType Felt::try_from(merged).expect("encoded value should be a valid felt") } -/// Merges the note tag and four attachment num_words into a single [`Felt`]. -/// -/// The layout is as follows: -/// -/// ```text -/// [attachment_3_num_words (8 bits) | attachment_2_num_words (8 bits) | -/// attachment_1_num_words (8 bits) | attachment_0_num_words (8 bits) | -/// note_tag (32 bits)] -/// ``` -fn merge_tag_and_num_words(tag: NoteTag, num_words: [u8; 4]) -> Felt { - let mut merged: u64 = u32::from(tag) as u64; - merged |= (num_words[0] as u64) << 32; - merged |= (num_words[1] as u64) << 40; - merged |= (num_words[2] as u64) << 48; - merged |= (num_words[3] as u64) << 56; - - Felt::try_from(merged).expect("encoded value should be a valid felt (num_words <= 254)") -} - /// Merges four attachment schemes into a single [`Felt`]. /// /// The layout is as follows: /// /// ```text -/// [attachment_3_scheme (16 bits) | attachment_2_scheme (16 bits) | attachment_1_scheme (16 bits) | attachment_0_scheme (16 bits)] +/// [attachment_3_scheme (16 bits) | attachment_2_scheme (16 bits) | +/// attachment_1_scheme (16 bits) | attachment_0_scheme (16 bits)] /// ``` /// /// Max value: `0xFFFEFFFE_FFFEFFFE` < p. Schemes are capped at 65534. -fn merge_schemes(schemes: [NoteAttachmentScheme; 4]) -> Felt { - let mut merged: u64 = schemes[0].as_u16() as u64; - merged |= (schemes[1].as_u16() as u64) << 16; - merged |= (schemes[2].as_u16() as u64) << 32; - merged |= (schemes[3].as_u16() as u64) << 48; +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)") } @@ -395,10 +359,8 @@ mod tests { let encoded = metadata_header.to_metadata_word(); - let num_words_and_tag = encoded[2].as_canonical_u64(); - // num_words 3 and 4 are 0, 2 encodes to 0x2 and 1 encodes to 0x1 - // tag should encode to 0xff - assert_eq!(num_words_and_tag, 0x0000_0201_0000_00ff); + 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 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 23130d509b..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,7 +12,7 @@ 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 +pub const NETWORK_ACCOUNT_TARGET_ATTACHMENT_SCHEME = 2 # ERRORS # ================================================================================================ @@ -97,10 +97,6 @@ pub proc active_account_matches_target_account # => [METADATA_HEADER, NOTE_ATTACHMENT] exec.note::metadata_into_attachment_header - # => [attachment_0_num_words, attachment_0_scheme, NOTE_ATTACHMENT] - - # make sure the num_words of the attachment is 1 - eq.1 assert.err=ERR_NOT_NETWORK_ACCOUNT_TARGET # => [attachment_0_scheme, NOTE_ATTACHMENT] # ensure the attachment is a network account target diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index fe61a410ef..57c2da6898 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -239,8 +239,8 @@ proc create_p2id_note loc_load.P2ID_AMT_ACCOUNT_FILL loc_load.P2ID_AMT_NOTE_FILL add # => [[total_fill, 0, 0, 0], note_idx] - push.0 - # => [attachment_scheme=0, [total_fill, 0, 0, 0], note_idx] + push.1 + # => [attachment_scheme=1, [total_fill, 0, 0, 0], note_idx] exec.output_note::add_word_attachment # => [] @@ -361,8 +361,8 @@ proc create_remainder_note push.0.0.0 loc_load.REMAINDER_AMT_PAYOUT # => [[amt_payout, 0, 0, 0], note_idx] - push.0 - # => [attachment_scheme=0, [amt_payout, 0, 0, 0], note_idx] + push.1 + # => [attachment_scheme=1, [amt_payout, 0, 0, 0], note_idx] exec.output_note::add_word_attachment # => [] diff --git a/crates/miden-standards/src/note/standard_note_attachment.rs b/crates/miden-standards/src/note/standard_note_attachment.rs index 740935d87e..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_const(1u16), + StandardNoteAttachment::NetworkAccountTarget => NoteAttachmentScheme::new_const(2u16), } } } diff --git a/crates/miden-testing/src/standards/network_account_target.rs b/crates/miden-testing/src/standards/network_account_target.rs index df9647bd35..b463a0287e 100644 --- a/crates/miden-testing/src/standards/network_account_target.rs +++ b/crates/miden-testing/src/standards/network_account_target.rs @@ -41,8 +41,6 @@ async fn network_account_target_get_id() -> anyhow::Result<()> { push.{attachment_commitment} push.{metadata_word} exec.note::metadata_into_attachment_header - # => [attachment_0_num_words, attachment_0_scheme, NOTE_ATTACHMENT] - drop # => [attachment_0_scheme, NOTE_ATTACHMENT] exec.network_account_target::is_network_account_target # => [is_valid, NOTE_ATTACHMENT] From c175c695bbe97b287d0420b15a55b2c0db3be777 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Mon, 27 Apr 2026 19:43:08 +0200 Subject: [PATCH 26/48] chore: MAX_ATTACHMENT_WORD_SIZE -> MAX_ATTACHMENT_WORDS --- crates/miden-protocol/asm/kernels/transaction/lib/note.masm | 2 +- .../asm/kernels/transaction/lib/output_note.masm | 4 ++-- crates/miden-protocol/asm/protocol/note.masm | 2 +- crates/miden-protocol/asm/shared_utils/util/note.masm | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/note.masm b/crates/miden-protocol/asm/kernels/transaction/lib/note.masm index 5737f5fb1f..3c59233fff 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/note.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/note.masm @@ -8,7 +8,7 @@ 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_WORD_SIZE +pub use $kernel::util::note::MAX_ATTACHMENT_WORDS # ERRORS # ================================================================================================= diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm b/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm index 50fc2f635d..2c79ae96ed 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm @@ -7,7 +7,7 @@ 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_WORD_SIZE +use $kernel::note::MAX_ATTACHMENT_WORDS use $kernel::constants::MAX_OUTPUT_NOTES_PER_TX use $kernel::constants::WORD_SIZE use $kernel::asset::ASSET_SIZE @@ -361,7 +361,7 @@ proc validate_attachment # validate 0 < num_words <= 254 dup neq.0 assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_CANNOT_BE_ZERO - dup u32lte.MAX_ATTACHMENT_WORD_SIZE assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_MAX_EXCEEDED + dup u32lte.MAX_ATTACHMENT_WORDS assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_MAX_EXCEEDED # OS => [num_words, ATTACHMENT_COMMITMENT] # AS => [[ATTACHMENT_ELEMENTS]] diff --git a/crates/miden-protocol/asm/protocol/note.masm b/crates/miden-protocol/asm/protocol/note.masm index ad88bc8f24..1e02ed8b1a 100644 --- a/crates/miden-protocol/asm/protocol/note.masm +++ b/crates/miden-protocol/asm/protocol/note.masm @@ -7,7 +7,7 @@ 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_WORD_SIZE +pub use miden::protocol::util::note::MAX_ATTACHMENT_WORDS # ERRORS # ================================================================================================= diff --git a/crates/miden-protocol/asm/shared_utils/util/note.masm b/crates/miden-protocol/asm/shared_utils/util/note.masm index b627a78fb5..1254b721b3 100644 --- a/crates/miden-protocol/asm/shared_utils/util/note.masm +++ b/crates/miden-protocol/asm/shared_utils/util/note.masm @@ -17,4 +17,4 @@ pub const NOTE_TYPE_PUBLIC=1 pub const MAX_ATTACHMENT_SCHEME=65534 # The maximum number of words in an attachment. -pub const MAX_ATTACHMENT_WORD_SIZE=254 +pub const MAX_ATTACHMENT_WORDS=254 From b4a5af71639b1566e2ae009cac570cdab2b99d52 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Mon, 27 Apr 2026 19:55:41 +0200 Subject: [PATCH 27/48] chore: use ATTACHMENT_COMMITMENT over just ATTACHMENT --- .../asm/kernels/transaction/api.masm | 2 +- .../asm/kernels/transaction/lib/memory.masm | 28 ++++++++++--------- .../asm/kernels/transaction/lib/note.masm | 2 +- .../kernels/transaction/lib/output_note.masm | 2 +- crates/miden-protocol/src/note/attachment.rs | 5 ---- 5 files changed, 18 insertions(+), 21 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction/api.masm b/crates/miden-protocol/asm/kernels/transaction/api.masm index 34487d236c..fa6f4b593a 100644 --- a/crates/miden-protocol/asm/kernels/transaction/api.masm +++ b/crates/miden-protocol/asm/kernels/transaction/api.masm @@ -1281,7 +1281,7 @@ pub proc output_note_get_metadata # 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 + movup.4 push.0 swap exec.memory::get_output_note_attachment_commitment # => [ATTACHMENT_0, METADATA_HEADER, pad(16)] # truncate the stack diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm b/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm index f78969bd39..e9d1c29ff6 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm @@ -1968,18 +1968,20 @@ pub proc set_output_note_metadata_header mem_storew_le end -#! Returns the output note's attachment at the given attachment index. +#! Returns the output note's attachment commitment at the given attachment index. #! -#! WARNING: Assumes the attachment_idx is within bounds: 0..4. +#! 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] +#! 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 slot. -#! - ATTACHMENT is the note attachment word. -pub proc get_output_note_attachment +#! - 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] @@ -1987,7 +1989,7 @@ pub proc get_output_note_attachment # => [attachment_ptr] padw movup.4 mem_loadw_le - # => [ATTACHMENT] + # => [ATTACHMENT_COMMITMENT] end #! Returns a pointer to the start of the attachment data region for the output note. @@ -1998,25 +2000,25 @@ end #! 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_data_ptr +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] +#! Inputs: [note_ptr, attachment_idx, ATTACHMENT_COMMITMENT] #! Outputs: [] #! #! Where: #! - note_ptr is the memory address at which the output note data begins. #! - attachment_idx is the index of the attachment slot (0..3). -#! - ATTACHMENT is the note attachment word. -pub proc set_output_note_attachment +#! - 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] + # => [note_ptr + base_offset, attachment_idx, ATTACHMENT_COMMITMENT] swap mul.WORD_SIZE add - # => [attachment_ptr, ATTACHMENT] + # => [attachment_ptr, ATTACHMENT_COMMITMENT] mem_storew_le dropw # => [] diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/note.masm b/crates/miden-protocol/asm/kernels/transaction/lib/note.masm index 3c59233fff..441159c8e6 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/note.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/note.masm @@ -102,7 +102,7 @@ proc compute_attachments_commitment # => [num_attachments, note_ptr] # end_ptr = attachment_data_ptr + num_attachments * WORD_SIZE - swap exec.memory::get_output_note_attachment_data_ptr + swap exec.memory::get_output_note_attachment_commitment_ptr # => [start_ptr, num_attachments] swap mul.WORD_SIZE dup.1 add swap 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 2c79ae96ed..7ee4076c4c 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm @@ -295,7 +295,7 @@ pub proc add_attachment # => [note_ptr, attachment_idx, ATTACHMENT_COMMITMENT, num_attachments, note_ptr, attachment_scheme] # store commitment in note memory - exec.memory::set_output_note_attachment + exec.memory::set_output_note_attachment_commitment # => [num_attachments, note_ptr, attachment_scheme] # increment number of attachments diff --git a/crates/miden-protocol/src/note/attachment.rs b/crates/miden-protocol/src/note/attachment.rs index 0a1fa265d4..924089c3f7 100644 --- a/crates/miden-protocol/src/note/attachment.rs +++ b/crates/miden-protocol/src/note/attachment.rs @@ -158,11 +158,6 @@ 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) From 1f64760bb6b52bd918784d0fe57ffb7037df74fc Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Mon, 27 Apr 2026 19:57:55 +0200 Subject: [PATCH 28/48] chore: remove unused code --- crates/miden-protocol/asm/protocol/output_note.masm | 7 ------- 1 file changed, 7 deletions(-) diff --git a/crates/miden-protocol/asm/protocol/output_note.masm b/crates/miden-protocol/asm/protocol/output_note.masm index 714f0d74ff..143a0fe85b 100644 --- a/crates/miden-protocol/asm/protocol/output_note.masm +++ b/crates/miden-protocol/asm/protocol/output_note.masm @@ -222,13 +222,6 @@ pub proc add_word_attachment exec.poseidon2::hash # => [ATTACHMENT_COMMITMENT, attachment_scheme, note_idx] - # Hash the attachment word - # push.4 locaddr.0 - # => [local_ptr, 4, attachment_scheme, note_idx] - - # exec.poseidon2::hash_elements - # => [ATTACHMENT_COMMITMENT, attachment_scheme, note_idx] - locaddr.0 dup add.4 # => [end_ptr, start_ptr, ATTACHMENT_COMMITMENT, attachment_scheme, note_idx] From 84cfd6a11da653275a8a260b2eeafea067cc36d1 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Mon, 27 Apr 2026 20:07:31 +0200 Subject: [PATCH 29/48] fix: typo in output note memory layout --- crates/miden-protocol/src/transaction/kernel/memory.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/miden-protocol/src/transaction/kernel/memory.rs b/crates/miden-protocol/src/transaction/kernel/memory.rs index aa4682dcf5..e7e8087e6d 100644 --- a/crates/miden-protocol/src/transaction/kernel/memory.rs +++ b/crates/miden-protocol/src/transaction/kernel/memory.rs @@ -441,7 +441,7 @@ pub const INPUT_NOTE_ASSETS_OFFSET: MemoryOffset = 44; // │ RECIPIENT │ ASSETS │ NUM │ DIRTY │ ASSET │ ASSET │ ... │ ASSET │ ASSET │ PADDING │ // │ │ COMMITMENT │ ASSETS │ FLAG │ KEY 0 │ VALUE 0 │ │ KEY n │ VALUE n │ │ // ┼───────────┼────────────┼────────┼───────┼───────┼─────────┼─────┼────────┼─────────┼─────────┘ -// 8 32 36 37 40 44 40 + 8n 44 + 8n +// 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 From 30901e179a8f8c17de32a65f1f23db9d7a80da07 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Mon, 27 Apr 2026 20:09:13 +0200 Subject: [PATCH 30/48] chore: update `Note` docs --- crates/miden-protocol/src/note/mod.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/miden-protocol/src/note/mod.rs b/crates/miden-protocol/src/note/mod.rs index 43011b0fa9..6a5b53bb2e 100644 --- a/crates/miden-protocol/src/note/mod.rs +++ b/crates/miden-protocol/src/note/mod.rs @@ -69,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 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 From bbf00b81cb296277a1c8f176b50c1b222582055b Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Mon, 27 Apr 2026 20:50:48 +0200 Subject: [PATCH 31/48] chore: validate attachment scheme is not 0 --- .../asm/kernels/transaction/api.masm | 2 +- .../kernels/transaction/lib/output_note.masm | 8 +++++++- .../asm/protocol/output_note.masm | 2 +- crates/miden-protocol/src/errors/mod.rs | 2 ++ crates/miden-protocol/src/note/attachment.rs | 18 +++++++++--------- 5 files changed, 20 insertions(+), 12 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction/api.masm b/crates/miden-protocol/asm/kernels/transaction/api.masm index c8aadf3e7d..fb3f308861 100644 --- a/crates/miden-protocol/asm/kernels/transaction/api.masm +++ b/crates/miden-protocol/asm/kernels/transaction/api.masm @@ -1169,7 +1169,7 @@ end #! 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 exceeds 65534. +#! - the attachment scheme is 0 or exceeds 65534. #! - the attachment num_words exceeds 254 or is zero. #! - the note already has 4 attachments. #! 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 7ee4076c4c..1d4aab93db 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm @@ -36,6 +36,8 @@ const ERR_OUTPUT_NOTE_INDEX_OUT_OF_BOUNDS="requested output note index should be 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_ATTACHMENT_SIZE_MAX_EXCEEDED="attachment num_words must not exceed 254" const ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_MUST_BE_MULTIPLE_OF_WORD_SIZE="number of elements in an attachment must be a multiple of 4" @@ -250,7 +252,7 @@ end #! - note_idx is the index of the note to which the attachment is added. #! #! Panics if: -#! - the attachment scheme exceeds 65534. +#! - 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 254. #! - the computed hash of fetched attachment elements does not match ATTACHMENT_COMMITMENT. #! - the note already has 4 attachments. @@ -260,6 +262,10 @@ pub proc add_attachment u32lte.MAX_ATTACHMENT_SCHEME assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_SCHEME_MAX_EXCEEDED # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] + # assert the scheme is not value 0 which the kernel uses to represent absent attachments + dup eq.0 assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_SCHEME_CANNOT_BE_ZERO + # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] + movdn.4 # => [ATTACHMENT_COMMITMENT, attachment_scheme, note_idx] diff --git a/crates/miden-protocol/asm/protocol/output_note.masm b/crates/miden-protocol/asm/protocol/output_note.masm index 143a0fe85b..e3a140af53 100644 --- a/crates/miden-protocol/asm/protocol/output_note.masm +++ b/crates/miden-protocol/asm/protocol/output_note.masm @@ -168,7 +168,7 @@ end #! 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 exceeds 65534. +#! - the attachment scheme is 0 or exceeds 65534. #! - the note already has 4 attachments. #! #! Invocation: exec diff --git a/crates/miden-protocol/src/errors/mod.rs b/crates/miden-protocol/src/errors/mod.rs index 6fa6dc0433..4b614db968 100644 --- a/crates/miden-protocol/src/errors/mod.rs +++ b/crates/miden-protocol/src/errors/mod.rs @@ -680,6 +680,8 @@ pub enum NoteError { 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, diff --git a/crates/miden-protocol/src/note/attachment.rs b/crates/miden-protocol/src/note/attachment.rs index 924089c3f7..f72d54614a 100644 --- a/crates/miden-protocol/src/note/attachment.rs +++ b/crates/miden-protocol/src/note/attachment.rs @@ -373,9 +373,11 @@ impl From for NoteAttachmentContent { /// /// A note attachment scheme is an arbitrary 16-bit unsigned integer (max [`Self::MAX`]). /// -/// 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. +/// 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(u16); @@ -383,6 +385,7 @@ impl NoteAttachmentScheme { // CONSTANTS // -------------------------------------------------------------------------------------------- + /// 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. @@ -402,12 +405,10 @@ impl NoteAttachmentScheme { /// /// # Errors /// - /// TODO - /// Returns an error if `attachment_scheme` exceeds [`Self::MAX`]. + /// 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 { - // TODO: Replace with new error type. - return Err(NoteError::NoteAttachmentSchemeExceeded(attachment_scheme as u32)); + return Err(NoteError::NoteAttachmentSchemeZeroReserved); } if attachment_scheme > Self::MAX.as_u16() { @@ -420,8 +421,7 @@ impl NoteAttachmentScheme { /// /// # Panics /// - /// TODO - /// Panics if `attachment_scheme` exceeds [`Self::MAX`]. + /// 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"); From 6fa4468b7098c3e8a5d5921d1a2832cb4ecf2162 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Mon, 27 Apr 2026 20:57:20 +0200 Subject: [PATCH 32/48] fix: assert scheme is _not_ zero --- .../asm/kernels/transaction/lib/output_note.masm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm b/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm index 1d4aab93db..6ae57ad334 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm @@ -262,8 +262,8 @@ pub proc add_attachment u32lte.MAX_ATTACHMENT_SCHEME assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_SCHEME_MAX_EXCEEDED # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] - # assert the scheme is not value 0 which the kernel uses to represent absent attachments - dup eq.0 assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_SCHEME_CANNOT_BE_ZERO + # assert the scheme is not 0 which the kernel uses to represent absent attachments + dup neq.0 assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_SCHEME_CANNOT_BE_ZERO # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] movdn.4 From 9e15c8913b3cad5917812da2e27921ad9314fce4 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Tue, 28 Apr 2026 07:42:01 +0200 Subject: [PATCH 33/48] chore: add tests for scheme = 0 and invalid num elements --- .../src/kernel_tests/tx/test_output_note.rs | 78 ++++++++++--------- 1 file changed, 43 insertions(+), 35 deletions(-) diff --git a/crates/miden-testing/src/kernel_tests/tx/test_output_note.rs b/crates/miden-testing/src/kernel_tests/tx/test_output_note.rs index 2d512a094f..572352ca46 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 @@ -6,10 +6,14 @@ use miden_protocol::account::{Account, AccountId}; use miden_protocol::asset::{Asset, FungibleAsset, NonFungibleAsset}; use miden_protocol::crypto::SequentialCommit; 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, }; @@ -1166,81 +1170,81 @@ async fn test_get_assets() -> anyhow::Result<()> { Ok(()) } +#[rstest] +#[case::zero_elemenets(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_add_attachment_with_missing_advice_map_entry_fails() -> anyhow::Result<()> { - let tx_context = TransactionContextBuilder::with_existing_mock_account().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 code = format!( " use miden::protocol::output_note use miden::standards::note_tag::DEFAULT_TAG use $kernel::prologue + use mock::util begin exec.prologue::prepare_transaction - push.1.2.3.4 - push.{NOTE_TYPE_PUBLIC} - push.DEFAULT_TAG - exec.output_note::create + exec.util::create_default_note # => [note_idx] - # try to add an attachment with a commitment that is not in the advice map - padw push.0 + push.{COMMITMENT} + push.5 # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] - exec.output_note::add_array_attachment + exec.output_note::add_attachment # => [] end ", - NOTE_TYPE_PUBLIC = NoteType::Public as u8, + COMMITMENT = commitment, ); let exec_output = tx_context.execute_code(&code).await; - // The adv.push_mapvaln instruction will fail when the commitment is not in the advice map - assert!(exec_output.is_err()); + assert_execution_error!(exec_output, expected_error); Ok(()) } #[tokio::test] -async fn test_add_attachment_with_zero_num_words_fails() -> anyhow::Result<()> { - // Insert an empty entry in the advice map so that adv.push_mapvaln succeeds but - // the kernel rejects the attachment because num_felts / 4 == 0. - let commitment = Word::from([42, 43, 44, 45u32]); - let tx_context = TransactionContextBuilder::with_existing_mock_account() - .extend_advice_map(vec![(commitment, vec![])]) - .build()?; +async fn test_add_attachment_with_scheme_zero_fails() -> anyhow::Result<()> { + let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; - let code = format!( - " + 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 - push.1.2.3.4 - push.{NOTE_TYPE_PUBLIC} - push.DEFAULT_TAG - exec.output_note::create + exec.util::create_default_note # => [note_idx] - push.{COMMITMENT} + push.1.2.3.4 push.0 # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] - exec.output_note::add_array_attachment + exec.output_note::add_attachment # => [] end - ", - NOTE_TYPE_PUBLIC = NoteType::Public as u8, - COMMITMENT = commitment, - ); + "; - let exec_output = tx_context.execute_code(&code).await; + let exec_output = tx_context.execute_code(code).await; - assert_execution_error!(exec_output, ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_CANNOT_BE_ZERO); + assert_execution_error!(exec_output, ERR_OUTPUT_NOTE_ATTACHMENT_SCHEME_CANNOT_BE_ZERO); Ok(()) } @@ -1251,8 +1255,9 @@ async fn test_add_word_attachment() -> anyhow::Result<()> { let rng = RandomCoin::new(Word::from([1, 2, 3, 4u32])); let attachment = NoteAttachment::new_word(NoteAttachmentScheme::MAX, Word::from([3, 4, 5, 6u32])); - let output_note = - RawOutputNote::Full(NoteBuilder::new(account.id(), rng).attachment(attachment).build()?); + let output_note = RawOutputNote::Full( + NoteBuilder::new(account.id(), rng).attachment(attachment.clone()).build()?, + ); let tx_script = format!( " @@ -1292,6 +1297,9 @@ async fn test_add_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()); From c0a3a410bbcd52fd4d2f43a212b97f403344ca15 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Tue, 28 Apr 2026 07:59:45 +0200 Subject: [PATCH 34/48] chore: increase max words per attachment to 256 --- crates/miden-protocol/asm/shared_utils/util/note.masm | 2 +- crates/miden-protocol/src/note/attachment.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/miden-protocol/asm/shared_utils/util/note.masm b/crates/miden-protocol/asm/shared_utils/util/note.masm index 1254b721b3..37d9c8c8c2 100644 --- a/crates/miden-protocol/asm/shared_utils/util/note.masm +++ b/crates/miden-protocol/asm/shared_utils/util/note.masm @@ -17,4 +17,4 @@ pub const NOTE_TYPE_PUBLIC=1 pub const MAX_ATTACHMENT_SCHEME=65534 # The maximum number of words in an attachment. -pub const MAX_ATTACHMENT_WORDS=254 +pub const MAX_ATTACHMENT_WORDS=256 diff --git a/crates/miden-protocol/src/note/attachment.rs b/crates/miden-protocol/src/note/attachment.rs index f72d54614a..86d3a285ad 100644 --- a/crates/miden-protocol/src/note/attachment.rs +++ b/crates/miden-protocol/src/note/attachment.rs @@ -52,9 +52,9 @@ impl NoteAttachment { /// The maximum number of words in an attachment. /// - /// Limited to 254 to ensure the value fits into a u8 and the felt encoding remains valid - /// when four num_words values are packed into a single felt in the note metadata. - pub const MAX_NUM_WORDS: u8 = 254; + /// 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; // CONSTRUCTORS // -------------------------------------------------------------------------------------------- From fdb0d62cc811d29cc23ae98ff9f16c6145abad7f Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Tue, 28 Apr 2026 08:01:42 +0200 Subject: [PATCH 35/48] fix: typo in test name --- crates/miden-testing/src/kernel_tests/tx/test_output_note.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/miden-testing/src/kernel_tests/tx/test_output_note.rs b/crates/miden-testing/src/kernel_tests/tx/test_output_note.rs index 572352ca46..ac3ac20d45 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 @@ -1171,7 +1171,7 @@ async fn test_get_assets() -> anyhow::Result<()> { } #[rstest] -#[case::zero_elemenets(vec![], ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_CANNOT_BE_ZERO)] +#[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)], From b493c558facafc1d497665ec7dda5fe2d96dc973 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Tue, 28 Apr 2026 08:49:06 +0200 Subject: [PATCH 36/48] chore: serialize num words in attachment as u8 --- crates/miden-protocol/src/note/attachment.rs | 21 ++++++++++++------- .../src/note/network_account_target.rs | 2 +- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/crates/miden-protocol/src/note/attachment.rs b/crates/miden-protocol/src/note/attachment.rs index 86d3a285ad..4084e162a2 100644 --- a/crates/miden-protocol/src/note/attachment.rs +++ b/crates/miden-protocol/src/note/attachment.rs @@ -110,7 +110,7 @@ impl NoteAttachment { /// /// - `1` indicates a single word attachment ([`NoteAttachmentContent::Word`]). /// - `> 1` indicates an array attachment ([`NoteAttachmentContent::Array`]). - pub fn num_words(&self) -> u8 { + pub fn num_words(&self) -> u16 { self.content.num_words() } } @@ -191,7 +191,7 @@ impl NoteAttachmentContent { /// /// - `1` for [`NoteAttachmentContent::Word`]. /// - `> 1` for [`NoteAttachmentContent::Array`]. - pub fn num_words(&self) -> u8 { + pub fn num_words(&self) -> u16 { match self { NoteAttachmentContent::Word(_) => 1, NoteAttachmentContent::Array(array) => array.num_words(), @@ -211,8 +211,11 @@ impl NoteAttachmentContent { impl Serializable for NoteAttachmentContent { fn write_into(&self, target: &mut W) { - // Write num_words as discriminant: 1 = Word, >1 = Array. - self.num_words().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::Word(word) => { @@ -237,7 +240,9 @@ impl Serializable for NoteAttachmentContent { impl Deserializable for NoteAttachmentContent { fn read_from(source: &mut R) -> Result { - let num_words = u8::read_from(source)?; + // 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( @@ -337,9 +342,9 @@ impl NoteAttachmentArray { } /// Returns the number of words in this note attachment array. - pub fn num_words(&self) -> u8 { - // SAFETY: constructor checks that num_words is less than 255 - u8::try_from(self.words.len()).expect("num words should fit in u8") + 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. diff --git a/crates/miden-standards/src/note/network_account_target.rs b/crates/miden-standards/src/note/network_account_target.rs index 8acd5ba4a3..2095e33df7 100644 --- a/crates/miden-standards/src/note/network_account_target.rs +++ b/crates/miden-standards/src/note/network_account_target.rs @@ -147,7 +147,7 @@ pub enum NetworkAccountTargetError { )] AttachmentSchemeMismatch(NoteAttachmentScheme), #[error("attachment content is not a Word (num_words={0}, expected 1)")] - AttachmentContentNotWord(u8), + AttachmentContentNotWord(u16), #[error("failed to decode target account ID")] DecodeTargetId(#[source] AccountIdError), #[error("failed to decode execution hint")] From 192c15fb0fd8dacb9ad043a80f7e5d3cf902c67a Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Wed, 29 Apr 2026 16:13:50 +0200 Subject: [PATCH 37/48] chore: improve docs on Note and NoteAttachmentHeader --- crates/miden-protocol/src/note/attachment.rs | 7 ++----- crates/miden-protocol/src/note/mod.rs | 8 ++++---- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/crates/miden-protocol/src/note/attachment.rs b/crates/miden-protocol/src/note/attachment.rs index 4084e162a2..089f697357 100644 --- a/crates/miden-protocol/src/note/attachment.rs +++ b/crates/miden-protocol/src/note/attachment.rs @@ -496,13 +496,10 @@ impl Deserializable for NoteAttachmentScheme { /// The header metadata for a single note attachment. /// -/// Contains the scheme and number of words of an attachment, without the actual content data. -/// The kind of attachment is inferred from the number of words: -/// - `num_words == 0`: absent (no attachment) -/// - `num_words == 1`: word attachment (a single [`Word`]) -/// - `num_words > 1`: array attachment (a commitment to a set of felts) +/// 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, } diff --git a/crates/miden-protocol/src/note/mod.rs b/crates/miden-protocol/src/note/mod.rs index 6a5b53bb2e..8188a4a618 100644 --- a/crates/miden-protocol/src/note/mod.rs +++ b/crates/miden-protocol/src/note/mod.rs @@ -69,10 +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 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. +/// 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 From e7a51ad300ced9abaf48df4fe1208d878bee510e Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Wed, 29 Apr 2026 16:20:20 +0200 Subject: [PATCH 38/48] chore: add docs around validation on attachment scheme --- crates/miden-protocol/src/note/attachment.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/miden-protocol/src/note/attachment.rs b/crates/miden-protocol/src/note/attachment.rs index 089f697357..42886f4086 100644 --- a/crates/miden-protocol/src/note/attachment.rs +++ b/crates/miden-protocol/src/note/attachment.rs @@ -374,9 +374,16 @@ 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 16-bit unsigned integer (max [`Self::MAX`]). +/// 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. +/// +/// 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 conent. 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. From 2950f7c152c06b227b1276123b2cd89663efef7c Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Wed, 29 Apr 2026 16:28:12 +0200 Subject: [PATCH 39/48] chore: improve name of attachments_to_commitment_elements --- crates/miden-protocol/src/note/attachment.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/miden-protocol/src/note/attachment.rs b/crates/miden-protocol/src/note/attachment.rs index 42886f4086..5960734349 100644 --- a/crates/miden-protocol/src/note/attachment.rs +++ b/crates/miden-protocol/src/note/attachment.rs @@ -717,7 +717,7 @@ impl SequentialCommit for NoteAttachments { type Commitment = Word; fn to_elements(&self) -> Vec { - attachments_to_elements(&self.attachments) + attachments_to_commitment_elements(&self.attachments) } fn to_commitment(&self) -> Self::Commitment { @@ -726,9 +726,9 @@ impl SequentialCommit for NoteAttachments { } /// Collects all attachment commitments into a flat vector of field elements. -fn attachments_to_elements(attachments: &[NoteAttachment]) -> Vec { +fn attachments_to_commitment_elements(attachments: &[NoteAttachment]) -> Vec { let mut elements = Vec::new(); - for commitment in attachments.iter().map(|attachment| attachment.content().to_commitment()) { + for commitment in attachments.iter().map(NoteAttachment::to_commitment) { elements.extend_from_slice(commitment.as_elements()); } elements @@ -743,7 +743,7 @@ fn compute_commitment(attachments: &[NoteAttachment]) -> Word { if attachments.is_empty() { Word::empty() } else { - let elements = attachments_to_elements(attachments); + let elements = attachments_to_commitment_elements(attachments); Hasher::hash_elements(&elements) } } From 1230499370e6e12b8c5cbf62e813d08f8b2be1ae Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Wed, 29 Apr 2026 16:36:57 +0200 Subject: [PATCH 40/48] fix: typo in scheme docs --- crates/miden-protocol/src/note/attachment.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/miden-protocol/src/note/attachment.rs b/crates/miden-protocol/src/note/attachment.rs index 5960734349..da07d1216f 100644 --- a/crates/miden-protocol/src/note/attachment.rs +++ b/crates/miden-protocol/src/note/attachment.rs @@ -381,7 +381,7 @@ impl From for NoteAttachmentContent { /// a note's attachments. /// /// 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 conent. Hence, users should +/// 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). /// From 2124cc9d36e5646dd932afa7a1e04341c359a7e0 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Mon, 4 May 2026 09:02:59 +0200 Subject: [PATCH 41/48] chore: add ATTACHMENT_SCHEME_NONE constant --- .../asm/kernels/transaction/lib/note.masm | 1 + crates/miden-protocol/asm/protocol/note.masm | 1 + .../asm/shared_utils/util/note.masm | 7 +++++-- .../asm/standards/notes/pswap.masm | 15 ++++++++------- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/note.masm b/crates/miden-protocol/asm/kernels/transaction/lib/note.masm index 441159c8e6..06bd03cc4a 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/note.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/note.masm @@ -9,6 +9,7 @@ pub use $kernel::util::note::NOTE_TYPE_PUBLIC pub use $kernel::util::note::NOTE_TYPE_PRIVATE pub use $kernel::util::note::MAX_ATTACHMENT_SCHEME pub use $kernel::util::note::MAX_ATTACHMENT_WORDS +pub use $kernel::util::note::ATTACHMENT_SCHEME_NONE # ERRORS # ================================================================================================= diff --git a/crates/miden-protocol/asm/protocol/note.masm b/crates/miden-protocol/asm/protocol/note.masm index f0df01ff63..4621f1e574 100644 --- a/crates/miden-protocol/asm/protocol/note.masm +++ b/crates/miden-protocol/asm/protocol/note.masm @@ -8,6 +8,7 @@ pub use miden::protocol::util::note::NOTE_TYPE_PUBLIC pub use miden::protocol::util::note::NOTE_TYPE_PRIVATE pub use miden::protocol::util::note::MAX_ATTACHMENT_SCHEME pub use miden::protocol::util::note::MAX_ATTACHMENT_WORDS +pub use miden::protocol::util::note::ATTACHMENT_SCHEME_NONE # ERRORS # ================================================================================================= diff --git a/crates/miden-protocol/asm/shared_utils/util/note.masm b/crates/miden-protocol/asm/shared_utils/util/note.masm index 37d9c8c8c2..3d7a505ab9 100644 --- a/crates/miden-protocol/asm/shared_utils/util/note.masm +++ b/crates/miden-protocol/asm/shared_utils/util/note.masm @@ -13,8 +13,11 @@ pub const NOTE_TYPE_PRIVATE=0 #! The note type of public notes. pub const NOTE_TYPE_PUBLIC=1 -# The maximum attachment scheme value. +#! The maximum attachment scheme value. pub const MAX_ATTACHMENT_SCHEME=65534 -# The maximum number of words in an attachment. +#! 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-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 52196d6248..f01c910947 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,8 +229,8 @@ 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 @@ -239,8 +240,8 @@ proc create_p2id_note loc_load.P2ID_AMT_ACCOUNT_FILL loc_load.P2ID_AMT_NOTE_FILL add # => [[total_fill, 0, 0, 0], note_idx] - push.1 - # => [attachment_scheme=1, [total_fill, 0, 0, 0], note_idx] + push.ATTACHMENT_SCHEME_NONE + # => [attachment_scheme_none, [total_fill, 0, 0, 0], note_idx] exec.output_note::add_word_attachment # => [] @@ -356,13 +357,13 @@ 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], note_idx] - push.1 - # => [attachment_scheme=1, [amt_payout, 0, 0, 0], note_idx] + push.ATTACHMENT_SCHEME_NONE + # => [attachment_scheme_none, [amt_payout, 0, 0, 0], note_idx] exec.output_note::add_word_attachment # => [] From 0689724d87155a7abd6ec0f6f1924a9929a52af0 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Mon, 4 May 2026 09:07:15 +0200 Subject: [PATCH 42/48] fix: remove outdated num_words reference --- .../asm/kernels/transaction/lib/output_note.masm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm b/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm index 6ae57ad334..03f39e9a30 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm @@ -434,9 +434,9 @@ pub proc build_metadata_header # => [NOTE_METADATA_HEADER] end -#! Sets an output note's attachment num_words and scheme in the note metadata header. +#! Sets an output note's attachment scheme in the note metadata header. #! -#! WARNING: The attachment num_words and scheme must be valid. +#! WARNING: The attachment scheme must be valid. #! #! Inputs: [num_attachments, note_ptr, attachment_scheme] #! Outputs: [] From 4be6b5e03c5f7c6a5d2e53a0074a6ea771c27e29 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Tue, 5 May 2026 13:15:02 +0200 Subject: [PATCH 43/48] fix: update num words from 254 to 256 --- .../miden-protocol/asm/kernels/transaction/api.masm | 2 +- .../asm/kernels/transaction/lib/output_note.masm | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction/api.masm b/crates/miden-protocol/asm/kernels/transaction/api.masm index a607e045e9..f054d56d36 100644 --- a/crates/miden-protocol/asm/kernels/transaction/api.masm +++ b/crates/miden-protocol/asm/kernels/transaction/api.masm @@ -1168,7 +1168,7 @@ end #! - 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 is 0 or exceeds 65534. -#! - the attachment num_words exceeds 254 or is zero. +#! - the attachment num_words exceeds 256 or is zero. #! - the note already has 4 attachments. #! #! Invocation: dynexec 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 03f39e9a30..29e977dfc1 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm @@ -25,6 +25,9 @@ const MAX_ATTACHMENTS_PER_NOTE=4 # All zeros means no attachment schemes are set. const ATTACHMENT_DEFAULT_SCHEMES=0 +#! The attachment scheme value that is reserved to represent an absent attachment. +const ATTACHMENT_RESERVED_SCHEME = 0 + # ERRORS # ================================================================================================= @@ -38,7 +41,7 @@ const ERR_OUTPUT_NOTE_ATTACHMENT_SCHEME_MAX_EXCEEDED="attachment scheme must not const ERR_OUTPUT_NOTE_ATTACHMENT_SCHEME_CANNOT_BE_ZERO = "attachment scheme must not be 0" -const ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_MAX_EXCEEDED="attachment num_words must not exceed 254" +const ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_MAX_EXCEEDED="attachment num_words must not exceed 256" const ERR_OUTPUT_NOTE_ATTACHMENT_SIZE_MUST_BE_MULTIPLE_OF_WORD_SIZE="number of elements in an attachment must be a multiple of 4" @@ -253,7 +256,7 @@ end #! #! Panics if: #! - 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 254. +#! - 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 @@ -263,7 +266,7 @@ pub proc add_attachment # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] # assert the scheme is not 0 which the kernel uses to represent absent attachments - dup neq.0 assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_SCHEME_CANNOT_BE_ZERO + dup neq.ATTACHMENT_RESERVED_SCHEME assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_SCHEME_CANNOT_BE_ZERO # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx] movdn.4 @@ -346,7 +349,7 @@ end #! Outputs: [] #! #! Panics if: -#! - the number of elements is not a multiple of 4, or num_words is zero or exceeds 254. +#! - 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 @@ -365,7 +368,7 @@ proc validate_attachment # OS => [num_words, ATTACHMENT_COMMITMENT] # AS => [[ATTACHMENT_ELEMENTS]] - # validate 0 < num_words <= 254 + # 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] From 90514d138b39ebb144602ddf37120da8328bce6c Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Tue, 5 May 2026 13:29:26 +0200 Subject: [PATCH 44/48] chore: validate all attachments conditions in note builder --- crates/miden-tx/src/host/note_builder.rs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/crates/miden-tx/src/host/note_builder.rs b/crates/miden-tx/src/host/note_builder.rs index 9905a5f3cd..84b1f32324 100644 --- a/crates/miden-tx/src/host/note_builder.rs +++ b/crates/miden-tx/src/host/note_builder.rs @@ -128,14 +128,29 @@ impl OutputNoteBuilder { &mut self, attachment: NoteAttachment, ) -> Result<(), TransactionKernelError> { - if self.attachments.len() >= NoteAttachments::MAX_COUNT { + self.attachments.push(attachment); + + if self.attachments.len() > NoteAttachments::MAX_COUNT { return Err(TransactionKernelError::other(format!( - "number of attachments exceeded max {}", + "number of attachments {} exceeded max {}", + self.attachments.len(), NoteAttachments::MAX_COUNT ))); } - self.attachments.push(attachment); + let total_num_words = self + .attachments + .iter() + .map(|attachment| attachment.num_words() as usize) + .sum::(); + + if total_num_words > NoteAttachments::MAX_NUM_WORDS as usize { + return Err(TransactionKernelError::other(format!( + "number of total words {} in all attachments exceeds max of {}", + total_num_words, + NoteAttachments::MAX_NUM_WORDS + ))); + } Ok(()) } From c9fbf939e53231e5e5dde388cd93e914b6a51ba8 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Thu, 7 May 2026 15:03:44 +0200 Subject: [PATCH 45/48] Update crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm Co-authored-by: Marti --- .../miden-protocol/asm/kernels/transaction/lib/output_note.masm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 29e977dfc1..edd327fec4 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm @@ -431,7 +431,7 @@ pub proc build_metadata_header # Build metadata header. # -------------------------------------------------------------------------------------------- - # push default absent attachment schemes (all zeros) + # 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] From 982e57eb51444d79f5fa168cd40b86d435f025f2 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Thu, 7 May 2026 15:14:36 +0200 Subject: [PATCH 46/48] chore: remove cached commitment from note attachments --- crates/miden-protocol/src/note/attachment.rs | 27 ++++------ crates/miden-protocol/src/note/metadata.rs | 2 +- .../src/transaction/kernel/advice_inputs.rs | 2 +- crates/miden-tx/src/host/note_builder.rs | 50 +++++++------------ 4 files changed, 30 insertions(+), 51 deletions(-) diff --git a/crates/miden-protocol/src/note/attachment.rs b/crates/miden-protocol/src/note/attachment.rs index da07d1216f..347461deee 100644 --- a/crates/miden-protocol/src/note/attachment.rs +++ b/crates/miden-protocol/src/note/attachment.rs @@ -596,7 +596,6 @@ impl Deserializable for NoteAttachmentHeader { #[derive(Debug, Clone, PartialEq, Eq)] pub struct NoteAttachments { attachments: Vec, - commitment: Word, } impl NoteAttachments { @@ -617,10 +616,7 @@ impl NoteAttachments { /// Creates a new empty [`NoteAttachments`] collection. pub fn empty() -> Self { - Self { - attachments: Vec::new(), - commitment: Word::empty(), - } + Self { attachments: Vec::new() } } /// Creates a [`NoteAttachments`] from a vector of attachments. @@ -644,9 +640,7 @@ impl NoteAttachments { return Err(NoteError::NoteAttachmentArrayTooManyWords(total_num_words)); } - let commitment = compute_commitment(&attachments); - - Ok(Self { attachments, commitment }) + Ok(Self { attachments }) } // ACCESSORS @@ -681,9 +675,9 @@ impl NoteAttachments { .collect() } - /// Returns the cached commitment over the contained attachments. - pub fn commitment(&self) -> Word { - self.commitment + /// Returns the commitment over the contained attachments. + pub fn to_commitment(&self) -> Word { + ::to_commitment(&self) } /// Returns the attachment headers for all attachment slots. @@ -719,10 +713,6 @@ impl SequentialCommit for NoteAttachments { fn to_elements(&self) -> Vec { attachments_to_commitment_elements(&self.attachments) } - - fn to_commitment(&self) -> Self::Commitment { - self.commitment - } } /// Collects all attachment commitments into a flat vector of field elements. @@ -882,7 +872,7 @@ mod tests { #[test] fn note_attachments_commitment_empty() { let attachments = NoteAttachments::empty(); - assert_eq!(attachments.commitment(), Word::empty()); + assert_eq!(attachments.to_commitment(), Word::empty()); } #[test] @@ -895,7 +885,10 @@ mod tests { // Single word attachment: the attachment commitment is hash(word), so the overall // attachments commitment is hash(hash(word)). let word_commitment = Hasher::hash_elements(word.as_elements()); - assert_eq!(attachments.commitment(), Hasher::hash_elements(word_commitment.as_elements())); + assert_eq!( + attachments.to_commitment(), + Hasher::hash_elements(word_commitment.as_elements()) + ); Ok(()) } diff --git a/crates/miden-protocol/src/note/metadata.rs b/crates/miden-protocol/src/note/metadata.rs index 3409c8ce20..ce425b7449 100644 --- a/crates/miden-protocol/src/note/metadata.rs +++ b/crates/miden-protocol/src/note/metadata.rs @@ -167,7 +167,7 @@ impl NoteMetadataHeader { /// /// 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.commitment()) + Self::from_parts(metadata, attachments.to_headers(), attachments.to_commitment()) } /// Creates a [`NoteMetadataHeader`] from its raw parts. diff --git a/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs b/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs index 6d1d0c05e4..c407bbe3ed 100644 --- a/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs +++ b/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs @@ -341,7 +341,7 @@ impl TransactionAdviceInputs { // ATTACHMENTS_COMMITMENT |-> [[ATTACHMENT_COMMITMENTS]] self.add_map_entry( - note.attachments().commitment(), + note.attachments().to_commitment(), note.attachments() .commitments() .iter() diff --git a/crates/miden-tx/src/host/note_builder.rs b/crates/miden-tx/src/host/note_builder.rs index 84b1f32324..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; @@ -27,7 +28,7 @@ use crate::errors::TransactionKernelError; pub struct OutputNoteBuilder { metadata: NoteMetadata, assets: Vec, - attachments: Vec, + attachments: NoteAttachments, recipient_digest: Word, recipient: Option, } @@ -60,7 +61,7 @@ impl OutputNoteBuilder { recipient_digest, recipient: None, assets: Vec::new(), - attachments: Vec::default(), + attachments: NoteAttachments::empty(), }) } @@ -71,7 +72,7 @@ impl OutputNoteBuilder { recipient_digest: recipient.digest(), recipient: Some(recipient), assets: Vec::new(), - attachments: Vec::default(), + attachments: NoteAttachments::empty(), } } @@ -123,34 +124,16 @@ impl OutputNoteBuilder { /// Appends an attachment to the note. /// /// # Errors - /// Returns an error if the note already has the maximum number of attachments. + /// 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> { - self.attachments.push(attachment); - - if self.attachments.len() > NoteAttachments::MAX_COUNT { - return Err(TransactionKernelError::other(format!( - "number of attachments {} exceeded max {}", - self.attachments.len(), - NoteAttachments::MAX_COUNT - ))); - } - - let total_num_words = self - .attachments - .iter() - .map(|attachment| attachment.num_words() as usize) - .sum::(); - - if total_num_words > NoteAttachments::MAX_NUM_WORDS as usize { - return Err(TransactionKernelError::other(format!( - "number of total words {} in all attachments exceeds max of {}", - total_num_words, - NoteAttachments::MAX_NUM_WORDS - ))); - } + 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(()) } @@ -162,17 +145,20 @@ impl OutputNoteBuilder { pub fn build(self) -> RawOutputNote { let assets = NoteAssets::new(self.assets) .expect("assets should be valid since add_asset validates them"); - let attachments = NoteAttachments::new(self.attachments) - .expect("attachments should be valid since add_attachment validates them"); match self.recipient { Some(recipient) => { - let note = Note::with_attachments(assets, self.metadata, recipient, attachments); + 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, attachments); + let note = PartialNote::new( + self.metadata, + self.recipient_digest, + assets, + self.attachments, + ); RawOutputNote::Partial(note) }, } From de3559596959aaf0ce25b87c645555a10b864d7e Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Thu, 7 May 2026 15:20:54 +0200 Subject: [PATCH 47/48] chore: add `NoteAttachments::find` --- crates/miden-protocol/src/note/attachment.rs | 37 +++++++------------ .../src/note/network_account_target.rs | 5 +-- .../src/kernel_tests/tx/test_active_note.rs | 1 - .../src/kernel_tests/tx/test_input_note.rs | 1 - .../src/kernel_tests/tx/test_output_note.rs | 1 - .../src/kernel_tests/tx/test_prologue.rs | 1 - .../tests/agglayer/bridge_out.rs | 13 ++++--- crates/miden-testing/tests/scripts/pswap.rs | 18 ++++----- 8 files changed, 30 insertions(+), 47 deletions(-) diff --git a/crates/miden-protocol/src/note/attachment.rs b/crates/miden-protocol/src/note/attachment.rs index 347461deee..57b5e97326 100644 --- a/crates/miden-protocol/src/note/attachment.rs +++ b/crates/miden-protocol/src/note/attachment.rs @@ -651,6 +651,13 @@ impl NoteAttachments { self.attachments.get(index) } + /// 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 the number of attachments. pub fn num_attachments(&self) -> u8 { u8::try_from(self.attachments.len()) @@ -710,31 +717,13 @@ impl Default for NoteAttachments { impl SequentialCommit for NoteAttachments { type Commitment = Word; + /// Collects all attachment commitments into a flat vector of field elements. fn to_elements(&self) -> Vec { - attachments_to_commitment_elements(&self.attachments) - } -} - -/// Collects all attachment commitments into a flat vector of field elements. -fn attachments_to_commitment_elements(attachments: &[NoteAttachment]) -> Vec { - let mut elements = Vec::new(); - for commitment in attachments.iter().map(NoteAttachment::to_commitment) { - elements.extend_from_slice(commitment.as_elements()); - } - elements -} - -/// Computes the commitment over a slice of attachments. -/// -/// The commitment is defined as `hash(ATTACHMENT_0_COMMITMENT || ... || ATTACHMENT_N_COMMITMENT)`, -/// i.e., the sequential hash over the individual attachment commitments. Returns `EMPTY_WORD` if -/// no attachments are present. -fn compute_commitment(attachments: &[NoteAttachment]) -> Word { - if attachments.is_empty() { - Word::empty() - } else { - let elements = attachments_to_commitment_elements(attachments); - Hasher::hash_elements(&elements) + let mut elements = Vec::new(); + for commitment in self.attachments.iter().map(NoteAttachment::to_commitment) { + elements.extend_from_slice(commitment.as_elements()); + } + elements } } diff --git a/crates/miden-standards/src/note/network_account_target.rs b/crates/miden-standards/src/note/network_account_target.rs index 2095e33df7..038c379578 100644 --- a/crates/miden-standards/src/note/network_account_target.rs +++ b/crates/miden-standards/src/note/network_account_target.rs @@ -92,10 +92,7 @@ impl TryFrom<&NoteAttachments> for NetworkAccountTarget { // 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 - .iter() - .find(|attachment| { - attachment.attachment_scheme() == NetworkAccountTarget::ATTACHMENT_SCHEME - }) + .find(NetworkAccountTarget::ATTACHMENT_SCHEME) .ok_or_else(|| NetworkAccountTargetError::MissingAttachmentScheme)?; Self::try_from(attachment) 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 86c258ae44..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 @@ -4,7 +4,6 @@ use anyhow::Context; use miden_protocol::account::Account; use miden_protocol::account::auth::AuthScheme; use miden_protocol::asset::FungibleAsset; -use miden_protocol::crypto::SequentialCommit; use miden_protocol::crypto::rand::{FeltRng, RandomCoin}; use miden_protocol::errors::tx_kernel::ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_METADATA_WHILE_NO_NOTE_BEING_PROCESSED; use miden_protocol::note::{ 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 500a724e6d..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 @@ -1,7 +1,6 @@ use alloc::string::String; use miden_protocol::Word; -use miden_protocol::crypto::SequentialCommit; use miden_protocol::note::Note; use miden_protocol::transaction::memory::{ASSET_SIZE, ASSET_VALUE_OFFSET}; use miden_standards::code_builder::CodeBuilder; 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 261859aa1d..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 @@ -4,7 +4,6 @@ use alloc::vec::Vec; use miden_protocol::account::auth::AuthScheme; use miden_protocol::account::{Account, AccountId}; use miden_protocol::asset::{Asset, FungibleAsset, NonFungibleAsset}; -use miden_protocol::crypto::SequentialCommit; use miden_protocol::crypto::rand::RandomCoin; use miden_protocol::errors::MasmError; use miden_protocol::errors::tx_kernel::{ 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 e41bd65aa4..7fac3753fa 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_prologue.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_prologue.rs @@ -16,7 +16,6 @@ use miden_protocol::account::{ }; use miden_protocol::asset::{FungibleAsset, NonFungibleAsset}; use miden_protocol::block::account_tree::AccountIdKey; -use miden_protocol::crypto::SequentialCommit; use miden_protocol::errors::tx_kernel::ERR_ACCOUNT_SEED_AND_COMMITMENT_DIGEST_MISMATCH; use miden_protocol::note::NoteId; use miden_protocol::testing::account_id::{ diff --git a/crates/miden-testing/tests/agglayer/bridge_out.rs b/crates/miden-testing/tests/agglayer/bridge_out.rs index 89b45834e2..3a755c9ebc 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,11 +195,12 @@ async fn bridge_out_consecutive() -> anyhow::Result<()> { NoteType::Public, "BURN note should be public" ); - let attachment = burn_note - .attachments() - .get(0) - .expect("BURN note should have at least one 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/pswap.rs b/crates/miden-testing/tests/scripts/pswap.rs index e24cddaabd..4abd3c9193 100644 --- a/crates/miden-testing/tests/scripts/pswap.rs +++ b/crates/miden-testing/tests/scripts/pswap.rs @@ -193,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 = first_attachment_word(output_p2id.attachments()); - 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!( first_attachment_word(p2id_note.attachments()), - aux_word, + attachment_word, "Rust-predicted P2ID attachment does not match the MASM-produced one", ); @@ -224,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 = first_attachment_word(output_remainder.attachments()); - 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() @@ -247,7 +247,7 @@ 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, @@ -284,7 +284,7 @@ async fn pswap_note_alice_reconstructs_and_consumes_p2id() -> anyhow::Result<()> // Parity on the attachment word itself. assert_eq!( first_attachment_word(reconstructed_remainder.attachments()), - remainder_aux, + remainder_attachment_word, "reconstructed remainder attachment does not match executed output", ); From 151ad51d3695e90315d53252fd4c9af299783d4f Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Thu, 7 May 2026 15:28:44 +0200 Subject: [PATCH 48/48] fix: clippy lint --- crates/miden-protocol/src/note/attachment.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/miden-protocol/src/note/attachment.rs b/crates/miden-protocol/src/note/attachment.rs index 57b5e97326..2511bc88f3 100644 --- a/crates/miden-protocol/src/note/attachment.rs +++ b/crates/miden-protocol/src/note/attachment.rs @@ -684,7 +684,7 @@ impl NoteAttachments { /// Returns the commitment over the contained attachments. pub fn to_commitment(&self) -> Word { - ::to_commitment(&self) + ::to_commitment(self) } /// Returns the attachment headers for all attachment slots.