Skip to main content

miden_protocol/note/
metadata.rs

1use super::{
2    AccountId,
3    ByteReader,
4    ByteWriter,
5    Deserializable,
6    DeserializationError,
7    Felt,
8    NoteTag,
9    NoteType,
10    Serializable,
11    Word,
12};
13use crate::Hasher;
14use crate::errors::NoteError;
15use crate::note::{NoteAttachment, NoteAttachmentKind, NoteAttachmentScheme};
16
17// NOTE METADATA
18// ================================================================================================
19
20/// The metadata associated with a note.
21///
22/// Note metadata consists of two parts:
23/// - The header of the metadata, which consists of:
24///   - the sender of the note
25///   - the [`NoteType`]
26///   - the [`NoteTag`]
27///   - type information about the [`NoteAttachment`].
28/// - The optional [`NoteAttachment`].
29///
30/// # Word layout & validity
31///
32/// [`NoteMetadata`] can be encoded into two words, a header and an attachment word.
33///
34/// The header word has the following layout:
35///
36/// ```text
37/// 0th felt: [sender_id_suffix (56 bits) | 6 zero bits | note_type (2 bit)]
38/// 1st felt: [sender_id_prefix (64 bits)]
39/// 2nd felt: [32 zero bits | note_tag (32 bits)]
40/// 3rd felt: [30 zero bits | attachment_kind (2 bits) | attachment_scheme (32 bits)]
41/// ```
42///
43/// The felt validity of each part of the layout is guaranteed:
44/// - 1st felt: The lower 8 bits of the account ID suffix are `0` by construction, so that they can
45///   be overwritten with other data. The suffix' most significant bit must be zero such that the
46///   entire felt retains its validity even if all of its lower 8 bits are set to `1`. So the note
47///   type can be comfortably encoded.
48/// - 2nd felt: Is equivalent to the prefix of the account ID so it inherits its validity.
49/// - 3rd felt: The upper 32 bits are always zero.
50/// - 4th felt: The upper 30 bits are always zero.
51///
52/// The value of the attachment word depends on the
53/// [`NoteAttachmentKind`](crate::note::NoteAttachmentKind):
54/// - [`NoteAttachmentKind::None`](crate::note::NoteAttachmentKind::None): Empty word.
55/// - [`NoteAttachmentKind::Word`](crate::note::NoteAttachmentKind::Word): The raw word itself.
56/// - [`NoteAttachmentKind::Array`](crate::note::NoteAttachmentKind::Array): The commitment to the
57///   elements.
58#[derive(Clone, Debug, Eq, PartialEq)]
59pub struct NoteMetadata {
60    /// The ID of the account which created the note.
61    sender: AccountId,
62
63    /// Defines how the note is to be stored (e.g. public or private).
64    note_type: NoteType,
65
66    /// A value which can be used by the recipient(s) to identify notes intended for them.
67    tag: NoteTag,
68
69    /// The optional attachment of a note's metadata.
70    ///
71    /// Defaults to [`NoteAttachment::default`].
72    attachment: NoteAttachment,
73}
74
75impl NoteMetadata {
76    // CONSTRUCTORS
77    // --------------------------------------------------------------------------------------------
78
79    /// Returns a new [`NoteMetadata`] instantiated with the specified parameters.
80    ///
81    /// The tag defaults to [`NoteTag::default()`]. Use [`NoteMetadata::with_tag`] to set a
82    /// specific tag if needed.
83    pub fn new(sender: AccountId, note_type: NoteType) -> Self {
84        Self {
85            sender,
86            note_type,
87            tag: NoteTag::default(),
88            attachment: NoteAttachment::default(),
89        }
90    }
91
92    /// Reconstructs a [`NoteMetadata`] from a [`NoteMetadataHeader`] and a
93    /// [`NoteAttachment`].
94    ///
95    /// # Errors
96    ///
97    /// Returns an error if the attachment's kind or scheme do not match those in the header.
98    pub fn try_from_header(
99        header: NoteMetadataHeader,
100        attachment: NoteAttachment,
101    ) -> Result<Self, NoteError> {
102        if header.attachment_kind != attachment.attachment_kind() {
103            return Err(NoteError::AttachmentKindMismatch {
104                header_kind: header.attachment_kind,
105                attachment_kind: attachment.attachment_kind(),
106            });
107        }
108
109        if header.attachment_scheme != attachment.attachment_scheme() {
110            return Err(NoteError::AttachmentSchemeMismatch {
111                header_scheme: header.attachment_scheme,
112                attachment_scheme: attachment.attachment_scheme(),
113            });
114        }
115
116        Ok(Self {
117            sender: header.sender,
118            note_type: header.note_type,
119            tag: header.tag,
120            attachment,
121        })
122    }
123
124    // ACCESSORS
125    // --------------------------------------------------------------------------------------------
126
127    /// Returns the account which created the note.
128    pub fn sender(&self) -> AccountId {
129        self.sender
130    }
131
132    /// Returns the note's type.
133    pub fn note_type(&self) -> NoteType {
134        self.note_type
135    }
136
137    /// Returns the tag associated with the note.
138    pub fn tag(&self) -> NoteTag {
139        self.tag
140    }
141
142    /// Returns the attachment of the note.
143    pub fn attachment(&self) -> &NoteAttachment {
144        &self.attachment
145    }
146
147    /// Returns `true` if the note is private.
148    pub fn is_private(&self) -> bool {
149        self.note_type == NoteType::Private
150    }
151
152    /// Returns the header of a [`NoteMetadata`] as a [`Word`].
153    ///
154    /// See [`NoteMetadata`] docs for more details.
155    pub fn to_header(&self) -> NoteMetadataHeader {
156        NoteMetadataHeader {
157            sender: self.sender,
158            note_type: self.note_type,
159            tag: self.tag,
160            attachment_kind: self.attachment().content().attachment_kind(),
161            attachment_scheme: self.attachment.attachment_scheme(),
162        }
163    }
164
165    /// Returns the [`Word`] that represents the header of a [`NoteMetadata`].
166    ///
167    /// See [`NoteMetadata`] docs for more details.
168    pub fn to_header_word(&self) -> Word {
169        Word::from(self.to_header())
170    }
171
172    /// Returns the [`Word`] that represents the attachment of a [`NoteMetadata`].
173    ///
174    /// See [`NoteMetadata`] docs for more details.
175    pub fn to_attachment_word(&self) -> Word {
176        self.attachment.content().to_word()
177    }
178
179    /// Returns the commitment to the note metadata, which is defined as:
180    ///
181    /// ```text
182    /// hash(NOTE_METADATA_HEADER || NOTE_METADATA_ATTACHMENT)
183    /// ```
184    pub fn to_commitment(&self) -> Word {
185        Hasher::merge(&[self.to_header_word(), self.to_attachment_word()])
186    }
187
188    // MUTATORS
189    // --------------------------------------------------------------------------------------------
190
191    /// Mutates the note's tag by setting it to the provided value.
192    pub fn set_tag(&mut self, tag: NoteTag) {
193        self.tag = tag;
194    }
195
196    /// Returns a new [`NoteMetadata`] with the tag set to the provided value.
197    ///
198    /// This is a builder method that consumes self and returns a new instance for method chaining.
199    pub fn with_tag(mut self, tag: NoteTag) -> Self {
200        self.tag = tag;
201        self
202    }
203
204    /// Mutates the note's attachment by setting it to the provided value.
205    pub fn set_attachment(&mut self, attachment: NoteAttachment) {
206        self.attachment = attachment;
207    }
208
209    /// Returns a new [`NoteMetadata`] with the attachment set to the provided value.
210    ///
211    /// This is a builder method that consumes self and returns a new instance for method chaining.
212    pub fn with_attachment(mut self, attachment: NoteAttachment) -> Self {
213        self.attachment = attachment;
214        self
215    }
216}
217
218// SERIALIZATION
219// ================================================================================================
220
221impl Serializable for NoteMetadata {
222    fn write_into<W: ByteWriter>(&self, target: &mut W) {
223        self.note_type().write_into(target);
224        self.sender().write_into(target);
225        self.tag().write_into(target);
226        self.attachment().write_into(target);
227    }
228
229    fn get_size_hint(&self) -> usize {
230        self.note_type().get_size_hint()
231            + self.sender().get_size_hint()
232            + self.tag().get_size_hint()
233            + self.attachment().get_size_hint()
234    }
235}
236
237impl Deserializable for NoteMetadata {
238    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
239        let note_type = NoteType::read_from(source)?;
240        let sender = AccountId::read_from(source)?;
241        let tag = NoteTag::read_from(source)?;
242        let attachment = NoteAttachment::read_from(source)?;
243
244        Ok(NoteMetadata::new(sender, note_type).with_tag(tag).with_attachment(attachment))
245    }
246}
247
248// NOTE METADATA HEADER
249// ================================================================================================
250
251/// The header representation of [`NoteMetadata`].
252///
253/// See the metadata's type for details on this type's [`Word`] layout.
254#[derive(Clone, Copy, Debug, Eq, PartialEq)]
255pub struct NoteMetadataHeader {
256    sender: AccountId,
257    note_type: NoteType,
258    tag: NoteTag,
259    attachment_kind: NoteAttachmentKind,
260    attachment_scheme: NoteAttachmentScheme,
261}
262
263impl NoteMetadataHeader {
264    // ACCESSORS
265    // --------------------------------------------------------------------------------------------
266
267    /// Returns the account which created the note.
268    pub fn sender(&self) -> AccountId {
269        self.sender
270    }
271
272    /// Returns the note's type.
273    pub fn note_type(&self) -> NoteType {
274        self.note_type
275    }
276
277    /// Returns the tag associated with the note.
278    pub fn tag(&self) -> NoteTag {
279        self.tag
280    }
281
282    /// Returns the attachment kind.
283    pub fn attachment_kind(&self) -> NoteAttachmentKind {
284        self.attachment_kind
285    }
286
287    /// Returns the attachment scheme.
288    pub fn attachment_scheme(&self) -> NoteAttachmentScheme {
289        self.attachment_scheme
290    }
291}
292
293impl From<NoteMetadataHeader> for Word {
294    fn from(header: NoteMetadataHeader) -> Self {
295        let mut metadata = Word::empty();
296
297        metadata[0] = merge_sender_suffix_and_note_type(header.sender.suffix(), header.note_type);
298        metadata[1] = header.sender.prefix().as_felt();
299        metadata[2] = Felt::from(header.tag);
300        metadata[3] =
301            merge_attachment_kind_scheme(header.attachment_kind, header.attachment_scheme);
302
303        metadata
304    }
305}
306
307impl TryFrom<Word> for NoteMetadataHeader {
308    type Error = NoteError;
309
310    /// Decodes a [`NoteMetadataHeader`] from a [`Word`].
311    fn try_from(word: Word) -> Result<Self, Self::Error> {
312        let (sender_suffix, note_type) = unmerge_sender_suffix_and_note_type(word[0])?;
313        let sender_prefix = word[1];
314        let tag = u32::try_from(word[2].as_canonical_u64()).map(NoteTag::new).map_err(|_| {
315            NoteError::other("failed to convert note tag from metadata header to u32")
316        })?;
317        let (attachment_kind, attachment_scheme) = unmerge_attachment_kind_scheme(word[3])?;
318
319        let sender =
320            AccountId::try_from_elements(sender_suffix, sender_prefix).map_err(|source| {
321                NoteError::other_with_source(
322                    "failed to decode account ID from metadata header",
323                    source,
324                )
325            })?;
326
327        Ok(Self {
328            sender,
329            note_type,
330            tag,
331            attachment_kind,
332            attachment_scheme,
333        })
334    }
335}
336
337// HELPER FUNCTIONS
338// ================================================================================================
339
340/// Merges the suffix of an [`AccountId`] and the [`NoteType`] into a single [`Felt`].
341///
342/// The layout is as follows:
343///
344/// ```text
345/// [sender_id_suffix (56 bits) | 6 zero bits | note_type (2 bits)]
346/// ```
347///
348/// The most significant bit of the suffix is guaranteed to be zero, so the felt retains its
349/// validity.
350///
351/// The `sender_id_suffix` is the suffix of the sender's account ID.
352fn merge_sender_suffix_and_note_type(sender_id_suffix: Felt, note_type: NoteType) -> Felt {
353    let mut merged = sender_id_suffix.as_canonical_u64();
354
355    let note_type_byte = note_type as u8;
356    debug_assert!(note_type_byte < 4, "note type must not contain values >= 4");
357    merged |= note_type_byte as u64;
358
359    // SAFETY: The most significant bit of the suffix is zero by construction so the u64 will be a
360    // valid felt.
361    Felt::try_from(merged).expect("encoded value should be a valid felt")
362}
363
364/// Unmerges the sender ID suffix and note type.
365fn unmerge_sender_suffix_and_note_type(element: Felt) -> Result<(Felt, NoteType), NoteError> {
366    const NOTE_TYPE_MASK: u8 = 0b11;
367    // Inverts the note type mask.
368    const SENDER_SUFFIX_MASK: u64 = !(NOTE_TYPE_MASK as u64);
369
370    let note_type_byte = element.as_canonical_u64() as u8 & NOTE_TYPE_MASK;
371    let note_type = NoteType::try_from(note_type_byte).map_err(|source| {
372        NoteError::other_with_source("failed to decode note type from metadata header", source)
373    })?;
374
375    // No bits were set so felt should still be valid.
376    let sender_suffix = Felt::try_from(element.as_canonical_u64() & SENDER_SUFFIX_MASK)
377        .expect("felt should still be valid");
378
379    Ok((sender_suffix, note_type))
380}
381
382/// Merges the [`NoteAttachmentScheme`] and [`NoteAttachmentKind`] into a single [`Felt`].
383///
384/// The layout is as follows:
385///
386/// ```text
387/// [30 zero bits | attachment_kind (2 bits) | attachment_scheme (32 bits)]
388/// ```
389fn merge_attachment_kind_scheme(
390    attachment_kind: NoteAttachmentKind,
391    attachment_scheme: NoteAttachmentScheme,
392) -> Felt {
393    debug_assert!(attachment_kind.as_u8() < 4, "attachment kind should fit into two bits");
394    let mut merged = (attachment_kind.as_u8() as u64) << 32;
395    let attachment_scheme = attachment_scheme.as_u32();
396    merged |= attachment_scheme as u64;
397
398    Felt::try_from(merged).expect("the upper bit should be zero and the felt therefore valid")
399}
400
401/// Unmerges the attachment kind and attachment scheme.
402fn unmerge_attachment_kind_scheme(
403    element: Felt,
404) -> Result<(NoteAttachmentKind, NoteAttachmentScheme), NoteError> {
405    let attachment_scheme = element.as_canonical_u64() as u32;
406    let attachment_kind = (element.as_canonical_u64() >> 32) as u8;
407
408    let attachment_scheme = NoteAttachmentScheme::new(attachment_scheme);
409    let attachment_kind = NoteAttachmentKind::try_from(attachment_kind).map_err(|source| {
410        NoteError::other_with_source(
411            "failed to decode attachment kind from metadata header",
412            source,
413        )
414    })?;
415
416    Ok((attachment_kind, attachment_scheme))
417}
418
419// TESTS
420// ================================================================================================
421
422#[cfg(test)]
423mod tests {
424
425    use super::*;
426    use crate::note::NoteAttachmentScheme;
427    use crate::testing::account_id::ACCOUNT_ID_MAX_ONES;
428
429    #[rstest::rstest]
430    #[case::attachment_none(NoteAttachment::default())]
431    #[case::attachment_raw(NoteAttachment::new_word(NoteAttachmentScheme::new(0), Word::from([3, 4, 5, 6u32])))]
432    #[case::attachment_commitment(NoteAttachment::new_array(
433        NoteAttachmentScheme::new(u32::MAX),
434        vec![Felt::new(5), Felt::new(6), Felt::new(7)],
435    )?)]
436    #[test]
437    fn note_metadata_serde(#[case] attachment: NoteAttachment) -> anyhow::Result<()> {
438        // Use the Account ID with the maximum one bits to test if the merge function always
439        // produces valid felts.
440        let sender = AccountId::try_from(ACCOUNT_ID_MAX_ONES).unwrap();
441        let note_type = NoteType::Public;
442        let tag = NoteTag::new(u32::MAX);
443        let metadata =
444            NoteMetadata::new(sender, note_type).with_tag(tag).with_attachment(attachment);
445
446        // Serialization Roundtrip
447        let deserialized = NoteMetadata::read_from_bytes(&metadata.to_bytes())?;
448        assert_eq!(deserialized, metadata);
449
450        // Metadata Header Roundtrip
451        let header = NoteMetadataHeader::try_from(metadata.to_header_word())?;
452        assert_eq!(header, metadata.to_header());
453
454        Ok(())
455    }
456}