Skip to main content

miden_protocol/note/attachment/
mod.rs

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