Skip to main content

miden_protocol/note/
attachment.rs

1use alloc::string::ToString;
2use alloc::vec::Vec;
3
4use crate::crypto::SequentialCommit;
5use crate::errors::NoteError;
6use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable};
7use crate::{Felt, Hasher, Word};
8
9// NOTE ATTACHMENT
10// ================================================================================================
11
12/// The optional attachment for a [`Note`](super::Note).
13///
14/// An attachment is a _public_ extension to a note's [`NoteMetadata`](super::NoteMetadata).
15///
16/// Example use cases:
17/// - Communicate the [`NoteDetails`](super::NoteDetails) of a private note in encrypted form.
18/// - In the context of network transactions, encode the ID of the network account that should
19///   consume the note.
20/// - Communicate details to the receiver of a _private_ note to allow deriving the
21///   [`NoteDetails`](super::NoteDetails) of that note. For instance, the payback note of a partial
22///   swap note can be private, but the receiver needs to know additional details to fully derive
23///   the content of the payback note. They can neither fetch those details from the network, since
24///   the note is private, nor is a side-channel available. The note attachment can encode those
25///   details.
26///
27/// These use cases require different amounts of data, e.g. an account ID takes up just two felts
28/// while the details of an encrypted note require many felts. To accommodate these cases, both a
29/// computationally efficient [`NoteAttachmentContent::Word`] as well as a more flexible
30/// [`NoteAttachmentContent::Array`] variant are available. See the type's docs for more
31/// details.
32///
33/// Next to the content, a note attachment can optionally specify a [`NoteAttachmentScheme`]. This
34/// allows a note attachment to describe itself. For example, a network account target attachment
35/// can be identified by a standardized type. For cases when the attachment scheme is known from
36/// content or typing is otherwise undesirable, [`NoteAttachmentScheme::none`] can be used.
37#[derive(Debug, Clone, Default, PartialEq, Eq)]
38pub struct NoteAttachment {
39    attachment_scheme: NoteAttachmentScheme,
40    content: NoteAttachmentContent,
41}
42
43impl NoteAttachment {
44    // CONSTRUCTORS
45    // --------------------------------------------------------------------------------------------
46
47    /// Creates a new [`NoteAttachment`] from a user-defined type and the provided content.
48    ///
49    /// # Errors
50    ///
51    /// Returns an error if:
52    /// - The attachment content is [`NoteAttachmentKind::None`] but the scheme is not
53    ///   [`NoteAttachmentScheme::none`].
54    pub fn new(
55        attachment_scheme: NoteAttachmentScheme,
56        content: NoteAttachmentContent,
57    ) -> Result<Self, NoteError> {
58        if content.attachment_kind().is_none() && !attachment_scheme.is_none() {
59            return Err(NoteError::AttachmentKindNoneMustHaveAttachmentSchemeNone);
60        }
61
62        Ok(Self { attachment_scheme, content })
63    }
64
65    /// Creates a new note attachment with content [`NoteAttachmentContent::Word`] from the provided
66    /// word.
67    pub fn new_word(attachment_scheme: NoteAttachmentScheme, word: Word) -> Self {
68        Self {
69            attachment_scheme,
70            content: NoteAttachmentContent::new_word(word),
71        }
72    }
73
74    /// Creates a new note attachment with content [`NoteAttachmentContent::Array`] from the
75    /// provided set of elements.
76    ///
77    /// # Errors
78    ///
79    /// Returns an error if:
80    /// - The maximum number of elements exceeds [`NoteAttachmentArray::MAX_NUM_ELEMENTS`].
81    pub fn new_array(
82        attachment_scheme: NoteAttachmentScheme,
83        elements: Vec<Felt>,
84    ) -> Result<Self, NoteError> {
85        NoteAttachmentContent::new_array(elements)
86            .map(|content| Self { attachment_scheme, content })
87    }
88
89    // ACCESSORS
90    // --------------------------------------------------------------------------------------------
91
92    /// Returns the attachment scheme.
93    pub fn attachment_scheme(&self) -> NoteAttachmentScheme {
94        self.attachment_scheme
95    }
96
97    /// Returns the attachment kind.
98    pub fn attachment_kind(&self) -> NoteAttachmentKind {
99        self.content.attachment_kind()
100    }
101
102    /// Returns a reference to the attachment content.
103    pub fn content(&self) -> &NoteAttachmentContent {
104        &self.content
105    }
106}
107
108impl Serializable for NoteAttachment {
109    fn write_into<W: ByteWriter>(&self, target: &mut W) {
110        self.attachment_scheme().write_into(target);
111        self.content().write_into(target);
112    }
113}
114
115impl Deserializable for NoteAttachment {
116    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
117        let attachment_scheme = NoteAttachmentScheme::read_from(source)?;
118        let content = NoteAttachmentContent::read_from(source)?;
119
120        Self::new(attachment_scheme, content)
121            .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
122    }
123}
124
125/// The content of a [`NoteAttachment`].
126///
127/// If a note attachment is not required, [`NoteAttachmentContent::None`] should be used.
128///
129/// When a single [`Word`] has sufficient space, [`NoteAttachmentContent::Word`] should be used, as
130/// it does not require any hashing. The word itself is encoded into the
131/// [`NoteMetadata`](super::NoteMetadata).
132///
133/// If the space of a [`Word`] is insufficient, the more flexible
134/// [`NoteAttachmentContent::Array`] variant can be used. It contains a set of field elements
135/// where only their sequential hash is encoded into the [`NoteMetadata`](super::NoteMetadata).
136#[derive(Debug, Clone, Default, PartialEq, Eq)]
137pub enum NoteAttachmentContent {
138    /// Signals the absence of a note attachment.
139    #[default]
140    None,
141
142    /// A note attachment consisting of a single [`Word`].
143    Word(Word),
144
145    /// A note attachment consisting of the commitment to a set of felts.
146    Array(NoteAttachmentArray),
147}
148
149impl NoteAttachmentContent {
150    // CONSTRUCTORS
151    // --------------------------------------------------------------------------------------------
152
153    /// Creates a new [`NoteAttachmentContent::Word`] containing an empty word.
154    pub fn empty_word() -> Self {
155        Self::Word(Word::empty())
156    }
157
158    /// Creates a new [`NoteAttachmentContent::Word`] from the provided word.
159    pub fn new_word(word: Word) -> Self {
160        Self::Word(word)
161    }
162
163    /// Creates a new [`NoteAttachmentContent::Array`] from the provided elements.
164    ///
165    /// # Errors
166    ///
167    /// Returns an error if:
168    /// - The maximum number of elements exceeds [`NoteAttachmentArray::MAX_NUM_ELEMENTS`].
169    pub fn new_array(elements: Vec<Felt>) -> Result<Self, NoteError> {
170        NoteAttachmentArray::new(elements).map(Self::from)
171    }
172
173    // ACCESSORS
174    // --------------------------------------------------------------------------------------------
175
176    /// Returns the [`NoteAttachmentKind`].
177    pub fn attachment_kind(&self) -> NoteAttachmentKind {
178        match self {
179            NoteAttachmentContent::None => NoteAttachmentKind::None,
180            NoteAttachmentContent::Word(_) => NoteAttachmentKind::Word,
181            NoteAttachmentContent::Array(_) => NoteAttachmentKind::Array,
182        }
183    }
184
185    /// Returns the [`NoteAttachmentContent`] encoded to a [`Word`].
186    ///
187    /// See the type-level documentation for more details.
188    pub fn to_word(&self) -> Word {
189        match self {
190            NoteAttachmentContent::None => Word::empty(),
191            NoteAttachmentContent::Word(word) => *word,
192            NoteAttachmentContent::Array(attachment_commitment) => {
193                attachment_commitment.commitment()
194            },
195        }
196    }
197}
198
199impl Serializable for NoteAttachmentContent {
200    fn write_into<W: ByteWriter>(&self, target: &mut W) {
201        self.attachment_kind().write_into(target);
202
203        match self {
204            NoteAttachmentContent::None => (),
205            NoteAttachmentContent::Word(word) => {
206                word.write_into(target);
207            },
208            NoteAttachmentContent::Array(attachment_commitment) => {
209                attachment_commitment.num_elements().write_into(target);
210                target.write_many(&attachment_commitment.elements);
211            },
212        }
213    }
214}
215
216impl Deserializable for NoteAttachmentContent {
217    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
218        let attachment_kind = NoteAttachmentKind::read_from(source)?;
219
220        match attachment_kind {
221            NoteAttachmentKind::None => Ok(NoteAttachmentContent::None),
222            NoteAttachmentKind::Word => {
223                let word = Word::read_from(source)?;
224                Ok(NoteAttachmentContent::Word(word))
225            },
226            NoteAttachmentKind::Array => {
227                let num_elements = u16::read_from(source)?;
228                let elements = source.read_many(num_elements as usize)?;
229                Self::new_array(elements)
230                    .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
231            },
232        }
233    }
234}
235
236// NOTE ATTACHMENT COMMITMENT
237// ================================================================================================
238
239/// The type contained in [`NoteAttachmentContent::Array`] that commits to a set of field
240/// elements.
241#[derive(Debug, Clone, PartialEq, Eq)]
242pub struct NoteAttachmentArray {
243    elements: Vec<Felt>,
244    commitment: Word,
245}
246
247impl NoteAttachmentArray {
248    // CONSTANTS
249    // --------------------------------------------------------------------------------------------
250
251    /// The maximum size of a note attachment that commits to a set of elements.
252    ///
253    /// Each element holds roughly 8 bytes of data and so this allows for a maximum of
254    /// 2048 * 8 = 2^14 = 16384 bytes.
255    pub const MAX_NUM_ELEMENTS: u16 = 2048;
256
257    // CONSTRUCTORS
258    // --------------------------------------------------------------------------------------------
259
260    /// Creates a new [`NoteAttachmentArray`] from the provided elements.
261    ///
262    /// # Errors
263    ///
264    /// Returns an error if:
265    /// - The maximum number of elements exceeds [`NoteAttachmentArray::MAX_NUM_ELEMENTS`].
266    pub fn new(elements: Vec<Felt>) -> Result<Self, NoteError> {
267        if elements.len() > Self::MAX_NUM_ELEMENTS as usize {
268            return Err(NoteError::NoteAttachmentArraySizeExceeded(elements.len()));
269        }
270
271        let commitment = Hasher::hash_elements(&elements);
272        Ok(Self { elements, commitment })
273    }
274
275    // ACCESSORS
276    // --------------------------------------------------------------------------------------------
277
278    /// Returns a reference to the elements this note attachment commits to.
279    pub fn as_slice(&self) -> &[Felt] {
280        &self.elements
281    }
282
283    /// Returns the number of elements this note attachment commits to.
284    pub fn num_elements(&self) -> u16 {
285        u16::try_from(self.elements.len()).expect("type should enforce that size fits in u16")
286    }
287
288    /// Returns the commitment over the contained field elements.
289    pub fn commitment(&self) -> Word {
290        self.commitment
291    }
292}
293
294impl SequentialCommit for NoteAttachmentArray {
295    type Commitment = Word;
296
297    fn to_elements(&self) -> Vec<Felt> {
298        self.elements.clone()
299    }
300
301    fn to_commitment(&self) -> Self::Commitment {
302        self.commitment
303    }
304}
305
306impl From<NoteAttachmentArray> for NoteAttachmentContent {
307    fn from(array: NoteAttachmentArray) -> Self {
308        NoteAttachmentContent::Array(array)
309    }
310}
311
312// NOTE ATTACHMENT SCHEME
313// ================================================================================================
314
315/// The user-defined type of a [`NoteAttachment`].
316///
317/// A note attachment scheme is an arbitrary 32-bit unsigned integer.
318///
319/// Value `0` is reserved to signal that the scheme is none or absent. Whenever the kind of
320/// attachment is not standardized or interoperability is unimportant, this none value can be
321/// used.
322#[derive(Debug, Clone, Copy, PartialEq, Eq)]
323pub struct NoteAttachmentScheme(u32);
324
325impl NoteAttachmentScheme {
326    // CONSTANTS
327    // --------------------------------------------------------------------------------------------
328
329    /// The reserved value to signal an absent note attachment scheme.
330    const NONE: u32 = 0;
331
332    // CONSTRUCTORS
333    // --------------------------------------------------------------------------------------------
334
335    /// Creates a new [`NoteAttachmentScheme`] from a `u32`.
336    pub const fn new(attachment_scheme: u32) -> Self {
337        Self(attachment_scheme)
338    }
339
340    /// Returns the [`NoteAttachmentScheme`] that signals the absence of an attachment scheme.
341    pub const fn none() -> Self {
342        Self(Self::NONE)
343    }
344
345    /// Returns `true` if the attachment scheme is the reserved value that signals an absent scheme,
346    /// `false` otherwise.
347    pub const fn is_none(&self) -> bool {
348        self.0 == Self::NONE
349    }
350
351    // ACCESSORS
352    // --------------------------------------------------------------------------------------------
353
354    /// Returns the note attachment scheme as a u32.
355    pub const fn as_u32(&self) -> u32 {
356        self.0
357    }
358}
359
360impl Default for NoteAttachmentScheme {
361    /// Returns [`NoteAttachmentScheme::none`].
362    fn default() -> Self {
363        Self::none()
364    }
365}
366
367impl core::fmt::Display for NoteAttachmentScheme {
368    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
369        f.write_fmt(format_args!("{}", self.0))
370    }
371}
372
373impl Serializable for NoteAttachmentScheme {
374    fn write_into<W: ByteWriter>(&self, target: &mut W) {
375        self.as_u32().write_into(target);
376    }
377}
378
379impl Deserializable for NoteAttachmentScheme {
380    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
381        let attachment_scheme = u32::read_from(source)?;
382        Ok(Self::new(attachment_scheme))
383    }
384}
385
386// NOTE ATTACHMENT KIND
387// ================================================================================================
388
389/// The type of [`NoteAttachmentContent`].
390///
391/// See its docs for more details on each type.
392#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
393#[repr(u8)]
394pub enum NoteAttachmentKind {
395    /// Signals the absence of a note attachment.
396    #[default]
397    None = Self::NONE,
398
399    /// A note attachment consisting of a single [`Word`].
400    Word = Self::WORD,
401
402    /// A note attachment consisting of the commitment to a set of felts.
403    Array = Self::ARRAY,
404}
405
406impl NoteAttachmentKind {
407    // CONSTANTS
408    // --------------------------------------------------------------------------------------------
409
410    const NONE: u8 = 0;
411    const WORD: u8 = 1;
412    const ARRAY: u8 = 2;
413
414    // ACCESSORS
415    // --------------------------------------------------------------------------------------------
416
417    /// Returns the attachment kind as a u8.
418    pub const fn as_u8(&self) -> u8 {
419        *self as u8
420    }
421
422    /// Returns `true` if the attachment kind is `None`, `false` otherwise.
423    pub const fn is_none(&self) -> bool {
424        matches!(self, Self::None)
425    }
426
427    /// Returns `true` if the attachment kind is `Word`, `false` otherwise.
428    pub const fn is_word(&self) -> bool {
429        matches!(self, Self::Word)
430    }
431
432    /// Returns `true` if the attachment kind is `Array`, `false` otherwise.
433    pub const fn is_array(&self) -> bool {
434        matches!(self, Self::Array)
435    }
436}
437
438impl TryFrom<u8> for NoteAttachmentKind {
439    type Error = NoteError;
440
441    fn try_from(value: u8) -> Result<Self, Self::Error> {
442        match value {
443            Self::NONE => Ok(Self::None),
444            Self::WORD => Ok(Self::Word),
445            Self::ARRAY => Ok(Self::Array),
446            _ => Err(NoteError::UnknownNoteAttachmentKind(value)),
447        }
448    }
449}
450
451impl core::fmt::Display for NoteAttachmentKind {
452    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
453        let output = match self {
454            NoteAttachmentKind::None => "None",
455            NoteAttachmentKind::Word => "Word",
456            NoteAttachmentKind::Array => "Array",
457        };
458
459        f.write_str(output)
460    }
461}
462
463impl Serializable for NoteAttachmentKind {
464    fn write_into<W: ByteWriter>(&self, target: &mut W) {
465        self.as_u8().write_into(target);
466    }
467}
468
469impl Deserializable for NoteAttachmentKind {
470    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
471        let attachment_kind = u8::read_from(source)?;
472        Self::try_from(attachment_kind)
473            .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
474    }
475}
476
477// TESTS
478// ================================================================================================
479
480#[cfg(test)]
481mod tests {
482    use assert_matches::assert_matches;
483
484    use super::*;
485
486    #[rstest::rstest]
487    #[case::attachment_none(NoteAttachment::default())]
488    #[case::attachment_word(NoteAttachment::new_word(NoteAttachmentScheme::new(1), Word::from([3, 4, 5, 6u32])))]
489    #[case::attachment_array(NoteAttachment::new_array(
490        NoteAttachmentScheme::new(u32::MAX),
491        vec![Felt::new(5), Felt::new(6), Felt::new(7)],
492    )?)]
493    #[test]
494    fn note_attachment_serde(#[case] attachment: NoteAttachment) -> anyhow::Result<()> {
495        assert_eq!(attachment, NoteAttachment::read_from_bytes(&attachment.to_bytes())?);
496        Ok(())
497    }
498
499    #[test]
500    fn note_attachment_commitment_fails_on_too_many_elements() -> anyhow::Result<()> {
501        let too_many_elements = (NoteAttachmentArray::MAX_NUM_ELEMENTS as usize) + 1;
502        let elements = vec![Felt::from(1u32); too_many_elements];
503        let err = NoteAttachmentArray::new(elements).unwrap_err();
504
505        assert_matches!(err, NoteError::NoteAttachmentArraySizeExceeded(len) => {
506            len == too_many_elements
507        });
508
509        Ok(())
510    }
511
512    #[test]
513    fn note_attachment_kind_fails_on_unknown_variant() -> anyhow::Result<()> {
514        let err = NoteAttachmentKind::try_from(3u8).unwrap_err();
515        assert_matches!(err, NoteError::UnknownNoteAttachmentKind(3u8));
516        Ok(())
517    }
518}