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}