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}