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