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}