Skip to main content

miden_protocol/note/attachment/
mod.rs

1#[cfg(test)]
2mod tests;
3
4use alloc::string::ToString;
5use alloc::vec::Vec;
6
7use crate::crypto::SequentialCommit;
8use crate::errors::NoteError;
9use crate::utils::serde::{
10    ByteReader,
11    ByteWriter,
12    Deserializable,
13    DeserializationError,
14    Serializable,
15};
16use crate::{Felt, Hasher, Word};
17
18// NOTE ATTACHMENT
19// ================================================================================================
20
21/// The optional attachment for a [`Note`](super::Note).
22///
23/// An attachment is a _public_ extension to a note.
24///
25/// Example use cases:
26/// - Communicate the [`NoteDetails`](super::NoteDetails) of a private note in encrypted form.
27/// - In the context of network transactions, encode the ID of the network account that should
28///   consume the note.
29/// - Communicate details to the receiver of a _private_ note to allow deriving the
30///   [`NoteDetails`](super::NoteDetails) of that note. For instance, the payback note of a partial
31///   swap note can be private, but the receiver needs to know additional details to fully derive
32///   the content of the payback note. They can neither fetch those details from the network, since
33///   the note is private, nor is a side-channel available. The note attachment can encode those
34///   details.
35///
36/// Next to the content, a note attachment can optionally specify a [`NoteAttachmentScheme`]. This
37/// allows a note attachment to describe itself. For example, a network account target attachment
38/// can be identified by a standardized type. For cases when the attachment scheme is known from
39/// content or typing is otherwise undesirable, [`NoteAttachmentScheme::none`] can be used.
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct NoteAttachment {
42    attachment_scheme: NoteAttachmentScheme,
43    content: NoteAttachmentContent,
44}
45
46impl NoteAttachment {
47    // CONSTANTS
48    // --------------------------------------------------------------------------------------------
49
50    /// The maximum number of words in an attachment.
51    ///
52    /// Each element holds roughly 8 bytes of data and so this allows for a maximum of
53    /// 256 * 32 = 2^13 = 8192 bytes.
54    pub const MAX_NUM_WORDS: u16 = 256;
55
56    // CONSTRUCTORS
57    // --------------------------------------------------------------------------------------------
58
59    /// Creates a new [`NoteAttachment`] from a user-defined scheme and the provided content.
60    pub fn new(attachment_scheme: NoteAttachmentScheme, content: NoteAttachmentContent) -> Self {
61        Self { attachment_scheme, content }
62    }
63
64    /// Creates a new note attachment from a single word.
65    pub fn with_word(attachment_scheme: NoteAttachmentScheme, word: Word) -> Self {
66        Self {
67            attachment_scheme,
68            content: NoteAttachmentContent::new(vec![word]).expect("single word is always valid"),
69        }
70    }
71
72    /// Creates a new note attachment from the provided words.
73    ///
74    /// # Errors
75    ///
76    /// Returns an error if:
77    /// - `words` is empty.
78    /// - The number of words exceeds [`NoteAttachment::MAX_NUM_WORDS`].
79    pub fn with_words(
80        attachment_scheme: NoteAttachmentScheme,
81        words: Vec<Word>,
82    ) -> Result<Self, NoteError> {
83        NoteAttachmentContent::new(words).map(|content| Self { attachment_scheme, content })
84    }
85
86    // ACCESSORS
87    // --------------------------------------------------------------------------------------------
88
89    /// Returns the attachment scheme.
90    pub fn attachment_scheme(&self) -> NoteAttachmentScheme {
91        self.attachment_scheme
92    }
93
94    /// Returns a reference to the attachment content.
95    pub fn content(&self) -> &NoteAttachmentContent {
96        &self.content
97    }
98
99    /// Computes the commitment of the attachment.
100    pub fn to_commitment(&self) -> Word {
101        self.content().to_commitment()
102    }
103
104    /// Returns the raw elements of this attachment content.
105    pub fn as_elements(&self) -> &[Felt] {
106        self.content.as_elements()
107    }
108
109    /// Returns the raw elements of this attachment content.
110    pub fn to_elements(&self) -> Vec<Felt> {
111        self.content().to_elements()
112    }
113
114    /// Returns the size of this attachment in words (1 to [`Self::MAX_NUM_WORDS`]).
115    pub fn num_words(&self) -> u16 {
116        self.content.num_words()
117    }
118}
119
120impl Serializable for NoteAttachment {
121    fn write_into<W: ByteWriter>(&self, target: &mut W) {
122        self.attachment_scheme().write_into(target);
123        self.content().write_into(target);
124    }
125
126    fn get_size_hint(&self) -> usize {
127        self.attachment_scheme().get_size_hint() + self.content().get_size_hint()
128    }
129}
130
131impl Deserializable for NoteAttachment {
132    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
133        let attachment_scheme = NoteAttachmentScheme::read_from(source)?;
134        let content = NoteAttachmentContent::read_from(source)?;
135
136        Ok(Self::new(attachment_scheme, content))
137    }
138}
139
140// NOTE ATTACHMENT CONTENT
141// ================================================================================================
142
143/// The content of a [`NoteAttachment`].
144///
145/// Contains between 1 and [`NoteAttachment::MAX_NUM_WORDS`] words of data. The commitment is
146/// the sequential hash over the flattened field elements and is cached at construction time.
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct NoteAttachmentContent {
149    words: Vec<Word>,
150    commitment: Word,
151}
152
153impl NoteAttachmentContent {
154    // CONSTRUCTORS
155    // --------------------------------------------------------------------------------------------
156
157    /// Creates a new [`NoteAttachmentContent`] from the provided words.
158    ///
159    /// # Errors
160    ///
161    /// Returns an error if:
162    /// - `words` is empty.
163    /// - The number of words exceeds [`NoteAttachment::MAX_NUM_WORDS`].
164    pub fn new(words: Vec<Word>) -> Result<Self, NoteError> {
165        if words.is_empty() {
166            return Err(NoteError::NoteAttachmentContentEmpty);
167        }
168
169        if words.len() > NoteAttachment::MAX_NUM_WORDS as usize {
170            return Err(NoteError::NoteAttachmentContentTooManyWords(words.len()));
171        }
172
173        let elements = Word::words_as_elements(&words).to_vec();
174        let commitment = Hasher::hash_elements(&elements);
175
176        Ok(Self { words, commitment })
177    }
178
179    // ACCESSORS
180    // --------------------------------------------------------------------------------------------
181
182    /// Returns a reference to the words in this attachment content.
183    pub fn as_words(&self) -> &[Word] {
184        &self.words
185    }
186
187    /// Returns the size of this attachment content in words.
188    pub fn num_words(&self) -> u16 {
189        u16::try_from(self.words.len()).expect("num words should fit in u16")
190    }
191
192    /// Returns the raw elements of this attachment content.
193    pub fn as_elements(&self) -> &[Felt] {
194        Word::words_as_elements(&self.words)
195    }
196
197    /// Returns the raw elements of this attachment content.
198    pub fn to_elements(&self) -> Vec<Felt> {
199        <Self as SequentialCommit>::to_elements(self)
200    }
201
202    /// Returns the sequential commitment over the content's elements.
203    pub fn to_commitment(&self) -> Word {
204        <Self as SequentialCommit>::to_commitment(self)
205    }
206}
207
208impl Serializable for NoteAttachmentContent {
209    fn write_into<W: ByteWriter>(&self, target: &mut W) {
210        // Subtract 1 from num words so we can serialize it as a u8.
211        let num_words_minus_1 =
212            u8::try_from(self.num_words().checked_sub(1).expect("num_words should be at least 1"))
213                .expect("num_words - 1 should fit in u8");
214        num_words_minus_1.write_into(target);
215        target.write_many(self.as_words());
216    }
217
218    fn get_size_hint(&self) -> usize {
219        core::mem::size_of::<u8>() + usize::from(self.num_words()) * Word::empty().get_size_hint()
220    }
221}
222
223impl Deserializable for NoteAttachmentContent {
224    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
225        // Add one to the serialized num words to get the original.
226        let num_words_minus_1 = u8::read_from(source)?;
227        let num_words = u16::from(num_words_minus_1) + 1;
228
229        let words: Vec<Word> =
230            source.read_many_iter(num_words as usize)?.collect::<Result<_, _>>()?;
231        Self::new(words).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
232    }
233}
234
235impl SequentialCommit for NoteAttachmentContent {
236    type Commitment = Word;
237
238    fn to_elements(&self) -> Vec<Felt> {
239        Word::words_as_elements(&self.words).to_vec()
240    }
241
242    fn to_commitment(&self) -> Self::Commitment {
243        self.commitment
244    }
245}
246
247// NOTE ATTACHMENT SCHEME
248// ================================================================================================
249
250/// The user-defined scheme of a [`NoteAttachment`].
251///
252/// A note attachment scheme is an arbitrary 16-bit unsigned integer (max [`Self::MAX`]). It is
253/// intended to be used to distinguish one attachment from another, or find a specific attachment in
254/// a note's attachments.
255///
256/// The scheme is purely a hint, and there is no validation with respect to the attachment content.
257/// In other words, any scheme can be associated with any attachment content. Hence, users should
258/// always validate the contents of an attachment, just like with
259/// [`NoteStorage`](super::NoteStorage).
260///
261/// Value `0` is reserved to signal that the entire attachment is absent and so it is not a valid
262/// scheme.
263///
264/// Value `1` is reserved to signal that the scheme is none. Whenever the kind of attachment is not
265/// standardized or interoperability is unimportant, this none value can be used.
266#[derive(Debug, Clone, Copy, PartialEq, Eq)]
267pub struct NoteAttachmentScheme(u16);
268
269impl NoteAttachmentScheme {
270    // CONSTANTS
271    // --------------------------------------------------------------------------------------------
272
273    /// The reserved value to signal an absent attachment. This is not a valid attachment scheme.
274    const RESERVED: u16 = 0;
275
276    /// The reserved value to signal a `None` note attachment scheme.
277    const NONE: u16 = 1;
278
279    /// The maximum value for a note attachment scheme.
280    ///
281    /// Limited to `2^16 - 2 = 65534` to ensure the felt encoding remains valid when four
282    /// schemes are packed into a single felt in the note metadata. Limiting schemes to this value
283    /// means at least one bit is always unset which ensures felt validity.
284    pub const MAX: NoteAttachmentScheme = NoteAttachmentScheme(65534);
285
286    // CONSTRUCTORS
287    // --------------------------------------------------------------------------------------------
288
289    /// Creates a new [`NoteAttachmentScheme`] from a `u16`.
290    ///
291    /// # Errors
292    ///
293    /// Returns an error if `attachment_scheme` is equal to 0 or exceeds [`Self::MAX`].
294    pub fn new(attachment_scheme: u16) -> Result<Self, NoteError> {
295        if attachment_scheme == Self::RESERVED {
296            return Err(NoteError::NoteAttachmentSchemeZeroReserved);
297        }
298
299        if attachment_scheme > Self::MAX.as_u16() {
300            return Err(NoteError::NoteAttachmentSchemeExceeded(attachment_scheme as u32));
301        }
302        Ok(Self(attachment_scheme))
303    }
304
305    /// Creates a new [`NoteAttachmentScheme`] from a `u16`.
306    ///
307    /// # Panics
308    ///
309    /// Panics if `attachment_scheme` is 0 or exceeds [`Self::MAX`].
310    pub const fn new_const(attachment_scheme: u16) -> Self {
311        assert!(attachment_scheme != Self::RESERVED, "attachment scheme must not be 0");
312        assert!(attachment_scheme <= Self::MAX.as_u16(), "attachment scheme exceeds maximum");
313        Self(attachment_scheme)
314    }
315
316    /// Returns the [`NoteAttachmentScheme`] that signals the absence of an attachment scheme.
317    pub const fn none() -> Self {
318        Self(Self::NONE)
319    }
320
321    /// Returns `true` if the attachment scheme is the reserved value that signals an absent scheme,
322    /// `false` otherwise.
323    pub const fn is_none(&self) -> bool {
324        self.0 == Self::NONE
325    }
326
327    // ACCESSORS
328    // --------------------------------------------------------------------------------------------
329
330    /// Returns the note attachment scheme as a u16.
331    pub const fn as_u16(&self) -> u16 {
332        self.0
333    }
334}
335
336impl TryFrom<u16> for NoteAttachmentScheme {
337    type Error = NoteError;
338
339    fn try_from(value: u16) -> Result<Self, Self::Error> {
340        Self::new(value)
341    }
342}
343
344impl Default for NoteAttachmentScheme {
345    /// Returns [`NoteAttachmentScheme::none`].
346    fn default() -> Self {
347        Self::none()
348    }
349}
350
351impl core::fmt::Display for NoteAttachmentScheme {
352    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
353        f.write_fmt(format_args!("{}", self.0))
354    }
355}
356
357impl Serializable for NoteAttachmentScheme {
358    fn write_into<W: ByteWriter>(&self, target: &mut W) {
359        self.as_u16().write_into(target);
360    }
361
362    fn get_size_hint(&self) -> usize {
363        core::mem::size_of::<u16>()
364    }
365}
366
367impl Deserializable for NoteAttachmentScheme {
368    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
369        let value = u16::read_from(source)?;
370        Self::try_from(value).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
371    }
372}
373
374// NOTE ATTACHMENT HEADER
375// ================================================================================================
376
377/// The header metadata for a single note attachment.
378///
379/// Contains the scheme of an attachment, without the actual content data.
380#[derive(Debug, Clone, Copy, PartialEq, Eq)]
381pub struct NoteAttachmentHeader {
382    /// `None` represents an absent note attachment and `Some` a present one.
383    scheme: Option<NoteAttachmentScheme>,
384}
385
386impl NoteAttachmentHeader {
387    // CONSTRUCTORS
388    // --------------------------------------------------------------------------------------------
389
390    /// Creates a new [`NoteAttachmentHeader`] from a [`NoteAttachmentScheme`].
391    pub fn new(scheme: NoteAttachmentScheme) -> Self {
392        Self { scheme: Some(scheme) }
393    }
394
395    /// Creates a new [`NoteAttachmentHeader`] from a [`NoteAttachmentScheme`].
396    pub fn new_maybe(scheme: Option<NoteAttachmentScheme>) -> Self {
397        Self { scheme }
398    }
399
400    /// Returns a header representing the absence of an attachment.
401    pub const fn absent() -> Self {
402        Self { scheme: None }
403    }
404
405    // ACCESSORS
406    // --------------------------------------------------------------------------------------------
407
408    /// Returns the attachment scheme.
409    pub const fn scheme(&self) -> Option<NoteAttachmentScheme> {
410        self.scheme
411    }
412
413    /// Returns the header encoded as a u16.
414    ///
415    /// Encodes `None` to 0 using the niche provided by [`NoteAttachmentScheme`].
416    pub(super) fn as_u16(&self) -> u16 {
417        match self.scheme {
418            None => 0,
419            Some(scheme) => scheme.as_u16(),
420        }
421    }
422
423    /// Returns `true` if this header represents an absent attachment, `false` otherwise.
424    pub const fn is_absent(&self) -> bool {
425        self.scheme.is_none()
426    }
427}
428
429impl Default for NoteAttachmentHeader {
430    fn default() -> Self {
431        Self::absent()
432    }
433}
434
435impl From<NoteAttachmentScheme> for NoteAttachmentHeader {
436    fn from(scheme: NoteAttachmentScheme) -> Self {
437        NoteAttachmentHeader::new(scheme)
438    }
439}
440
441impl Serializable for NoteAttachmentHeader {
442    fn write_into<W: ByteWriter>(&self, target: &mut W) {
443        self.scheme.write_into(target);
444    }
445
446    fn get_size_hint(&self) -> usize {
447        self.scheme.get_size_hint()
448    }
449}
450
451impl Deserializable for NoteAttachmentHeader {
452    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
453        let scheme = Option::<NoteAttachmentScheme>::read_from(source)?;
454        Ok(Self::new_maybe(scheme))
455    }
456}
457
458// NOTE ATTACHMENTS
459// ================================================================================================
460
461/// A collection of note attachments.
462///
463/// Notes can have up to [`Self::MAX_COUNT`] attachments.
464///
465/// The commitment to the attachments is defined as:
466/// - 0 attachments: `EMPTY_WORD`
467/// - 1+ attachments: `hash(ATTACHMENT_0_COMMITMENT || ... || ATTACHMENT_N_COMMITMENT)`, i.e., the
468///   sequential hash over the individual attachment commitments.
469#[derive(Debug, Clone, PartialEq, Eq)]
470pub struct NoteAttachments {
471    attachments: Vec<NoteAttachment>,
472}
473
474impl NoteAttachments {
475    // CONSTANTS
476    // --------------------------------------------------------------------------------------------
477
478    /// The maximum number of attachments per note.
479    pub const MAX_COUNT: usize = 4;
480
481    /// The maximum total number of elements across all attachments in a note.
482    ///
483    /// Each element holds roughly 8 bytes of data and so this allows for a maximum of
484    /// 512 * 32 = 2^14 = 16384 bytes.
485    pub const MAX_NUM_WORDS: u16 = 512;
486
487    // CONSTRUCTORS
488    // --------------------------------------------------------------------------------------------
489
490    /// Creates a new empty [`NoteAttachments`] collection.
491    pub fn empty() -> Self {
492        Self { attachments: Vec::new() }
493    }
494
495    /// Creates a [`NoteAttachments`] from a vector of attachments.
496    ///
497    /// # Errors
498    ///
499    /// Returns an error if:
500    /// - The number of attachments exceeds [`Self::MAX_COUNT`].
501    /// - The total number of words across all attachments exceeds [`Self::MAX_NUM_WORDS`].
502    pub fn new(attachments: Vec<NoteAttachment>) -> Result<Self, NoteError> {
503        if attachments.len() > Self::MAX_COUNT {
504            return Err(NoteError::TooManyAttachments(attachments.len()));
505        }
506
507        let total_num_words = attachments
508            .iter()
509            .map(|attachment| attachment.num_words() as usize)
510            .sum::<usize>();
511
512        if total_num_words > Self::MAX_NUM_WORDS as usize {
513            return Err(NoteError::NoteAttachmentsTooManyWords(total_num_words));
514        }
515
516        Ok(Self { attachments })
517    }
518
519    // ACCESSORS
520    // --------------------------------------------------------------------------------------------
521
522    /// Returns the attachment at the given index, if it exists.
523    pub fn get(&self, index: usize) -> Option<&NoteAttachment> {
524        self.attachments.get(index)
525    }
526
527    /// Returns the first attachment with the provided scheme, if any.
528    pub fn find(&self, scheme: NoteAttachmentScheme) -> Option<&NoteAttachment> {
529        self.attachments
530            .iter()
531            .find(|attachment| attachment.attachment_scheme == scheme)
532    }
533
534    /// Returns the number of attachments.
535    pub fn num_attachments(&self) -> u8 {
536        u8::try_from(self.attachments.len())
537            .expect("constructor should ensure num attachment fits in u8")
538    }
539
540    /// Returns `true` if there are no attachments.
541    pub fn is_empty(&self) -> bool {
542        self.attachments.is_empty()
543    }
544
545    /// Returns an iterator over the attachments.
546    pub fn iter(&self) -> impl Iterator<Item = &NoteAttachment> {
547        self.attachments.iter()
548    }
549
550    /// Returns the individual commitment of each contained attachment.
551    pub fn commitments(&self) -> Vec<Word> {
552        self.attachments
553            .iter()
554            .map(|attachment| attachment.content().to_commitment())
555            .collect()
556    }
557
558    /// Returns the commitment over the contained attachments.
559    pub fn to_commitment(&self) -> Word {
560        <Self as SequentialCommit>::to_commitment(self)
561    }
562
563    /// Returns the attachment headers for all attachment slots.
564    ///
565    /// Returns a fixed-size array of [`Self::MAX_COUNT`] headers. Unused slots are filled with
566    /// [`NoteAttachmentHeader::absent`].
567    pub fn to_headers(&self) -> [NoteAttachmentHeader; Self::MAX_COUNT] {
568        let mut headers = [NoteAttachmentHeader::absent(); Self::MAX_COUNT];
569        for (i, attachment) in self.attachments.iter().enumerate() {
570            headers[i] = NoteAttachmentHeader::new(attachment.attachment_scheme());
571        }
572        headers
573    }
574
575    // CONVERSIONS
576    // --------------------------------------------------------------------------------------------
577
578    /// Consumes self and returns the inner vector of attachments.
579    pub fn into_vec(self) -> Vec<NoteAttachment> {
580        self.attachments
581    }
582}
583
584impl Default for NoteAttachments {
585    fn default() -> Self {
586        Self::empty()
587    }
588}
589
590impl SequentialCommit for NoteAttachments {
591    type Commitment = Word;
592
593    /// Collects all attachment commitments into a flat vector of field elements.
594    fn to_elements(&self) -> Vec<Felt> {
595        let mut elements = Vec::new();
596        for commitment in self.attachments.iter().map(NoteAttachment::to_commitment) {
597            elements.extend_from_slice(commitment.as_elements());
598        }
599        elements
600    }
601}
602
603impl From<NoteAttachment> for NoteAttachments {
604    fn from(attachment: NoteAttachment) -> Self {
605        Self::new(vec![attachment]).expect("one attachment does not exceed the max of four")
606    }
607}
608
609impl Serializable for NoteAttachments {
610    fn write_into<W: ByteWriter>(&self, target: &mut W) {
611        self.num_attachments().write_into(target);
612        target.write_many(&self.attachments);
613    }
614
615    fn get_size_hint(&self) -> usize {
616        self.num_attachments().get_size_hint()
617            + self.iter().map(NoteAttachment::get_size_hint).sum::<usize>()
618    }
619}
620
621impl Deserializable for NoteAttachments {
622    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
623        let num_attachments = u8::read_from(source)? as usize;
624        let attachments = source
625            .read_many_iter::<NoteAttachment>(num_attachments)?
626            .collect::<Result<Vec<_>, _>>()?;
627        Self::new(attachments).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
628    }
629}