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 be set to `1`. So the
47///   note 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    // ACCESSORS
93    // --------------------------------------------------------------------------------------------
94
95    /// Returns the account which created the note.
96    pub fn sender(&self) -> AccountId {
97        self.sender
98    }
99
100    /// Returns the note's type.
101    pub fn note_type(&self) -> NoteType {
102        self.note_type
103    }
104
105    /// Returns the tag associated with the note.
106    pub fn tag(&self) -> NoteTag {
107        self.tag
108    }
109
110    /// Returns the attachment of the note.
111    pub fn attachment(&self) -> &NoteAttachment {
112        &self.attachment
113    }
114
115    /// Returns `true` if the note is private.
116    pub fn is_private(&self) -> bool {
117        self.note_type == NoteType::Private
118    }
119
120    /// Returns the header of a [`NoteMetadata`] as a [`Word`].
121    ///
122    /// See [`NoteMetadata`] docs for more details.
123    fn to_header(&self) -> NoteMetadataHeader {
124        NoteMetadataHeader {
125            sender: self.sender,
126            note_type: self.note_type,
127            tag: self.tag,
128            attachment_kind: self.attachment().content().attachment_kind(),
129            attachment_scheme: self.attachment.attachment_scheme(),
130        }
131    }
132
133    /// Returns the [`Word`] that represents the header of a [`NoteMetadata`].
134    ///
135    /// See [`NoteMetadata`] docs for more details.
136    pub fn to_header_word(&self) -> Word {
137        Word::from(self.to_header())
138    }
139
140    /// Returns the [`Word`] that represents the attachment of a [`NoteMetadata`].
141    ///
142    /// See [`NoteMetadata`] docs for more details.
143    pub fn to_attachment_word(&self) -> Word {
144        self.attachment.content().to_word()
145    }
146
147    /// Returns the commitment to the note metadata, which is defined as:
148    ///
149    /// ```text
150    /// hash(NOTE_METADATA_HEADER || NOTE_METADATA_ATTACHMENT)
151    /// ```
152    pub fn to_commitment(&self) -> Word {
153        Hasher::merge(&[self.to_header_word(), self.to_attachment_word()])
154    }
155
156    // MUTATORS
157    // --------------------------------------------------------------------------------------------
158
159    /// Mutates the note's tag by setting it to the provided value.
160    pub fn set_tag(&mut self, tag: NoteTag) {
161        self.tag = tag;
162    }
163
164    /// Returns a new [`NoteMetadata`] with the tag set to the provided value.
165    ///
166    /// This is a builder method that consumes self and returns a new instance for method chaining.
167    pub fn with_tag(mut self, tag: NoteTag) -> Self {
168        self.tag = tag;
169        self
170    }
171
172    /// Mutates the note's attachment by setting it to the provided value.
173    pub fn set_attachment(&mut self, attachment: NoteAttachment) {
174        self.attachment = attachment;
175    }
176
177    /// Returns a new [`NoteMetadata`] with the attachment set to the provided value.
178    ///
179    /// This is a builder method that consumes self and returns a new instance for method chaining.
180    pub fn with_attachment(mut self, attachment: NoteAttachment) -> Self {
181        self.attachment = attachment;
182        self
183    }
184}
185
186// SERIALIZATION
187// ================================================================================================
188
189impl Serializable for NoteMetadata {
190    fn write_into<W: ByteWriter>(&self, target: &mut W) {
191        self.note_type().write_into(target);
192        self.sender().write_into(target);
193        self.tag().write_into(target);
194        self.attachment().write_into(target);
195    }
196}
197
198impl Deserializable for NoteMetadata {
199    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
200        let note_type = NoteType::read_from(source)?;
201        let sender = AccountId::read_from(source)?;
202        let tag = NoteTag::read_from(source)?;
203        let attachment = NoteAttachment::read_from(source)?;
204
205        Ok(NoteMetadata::new(sender, note_type).with_tag(tag).with_attachment(attachment))
206    }
207}
208
209// NOTE METADATA HEADER
210// ================================================================================================
211
212/// The header representation of [`NoteMetadata`].
213///
214/// See the metadata's type for details on this type's [`Word`] layout.
215///
216/// This is intended to be a private type meant for encapsulating the conversion from and to words.
217#[derive(Clone, Copy, Debug, Eq, PartialEq)]
218struct NoteMetadataHeader {
219    sender: AccountId,
220    note_type: NoteType,
221    tag: NoteTag,
222    attachment_kind: NoteAttachmentKind,
223    attachment_scheme: NoteAttachmentScheme,
224}
225
226impl From<NoteMetadataHeader> for Word {
227    fn from(header: NoteMetadataHeader) -> Self {
228        let mut metadata = Word::empty();
229
230        metadata[0] = merge_sender_suffix_and_note_type(header.sender.suffix(), header.note_type);
231        metadata[1] = header.sender.prefix().as_felt();
232        metadata[2] = Felt::from(header.tag);
233        metadata[3] =
234            merge_attachment_kind_scheme(header.attachment_kind, header.attachment_scheme);
235
236        metadata
237    }
238}
239
240impl TryFrom<Word> for NoteMetadataHeader {
241    type Error = NoteError;
242
243    /// Decodes a [`NoteMetadataHeader`] from a [`Word`].
244    fn try_from(word: Word) -> Result<Self, Self::Error> {
245        let (sender_suffix, note_type) = unmerge_sender_suffix_and_note_type(word[0])?;
246        let sender_prefix = word[1];
247        let tag = u32::try_from(word[2]).map(NoteTag::new).map_err(|_| {
248            NoteError::other("failed to convert note tag from metadata header to u32")
249        })?;
250        let (attachment_kind, attachment_scheme) = unmerge_attachment_kind_scheme(word[3])?;
251
252        let sender = AccountId::try_from([sender_prefix, sender_suffix]).map_err(|source| {
253            NoteError::other_with_source("failed to decode account ID from metadata header", source)
254        })?;
255
256        Ok(Self {
257            sender,
258            note_type,
259            tag,
260            attachment_kind,
261            attachment_scheme,
262        })
263    }
264}
265
266// HELPER FUNCTIONS
267// ================================================================================================
268
269/// Merges the suffix of an [`AccountId`] and the [`NoteType`] into a single [`Felt`].
270///
271/// The layout is as follows:
272///
273/// ```text
274/// [sender_id_suffix (56 bits) | 6 zero bits | note_type (2 bits)]
275/// ```
276///
277/// The most significant bit of the suffix is guaranteed to be zero, so the felt retains its
278/// validity.
279///
280/// The `sender_id_suffix` is the suffix of the sender's account ID.
281fn merge_sender_suffix_and_note_type(sender_id_suffix: Felt, note_type: NoteType) -> Felt {
282    let mut merged = sender_id_suffix.as_int();
283
284    let note_type_byte = note_type as u8;
285    debug_assert!(note_type_byte < 4, "note type must not contain values >= 4");
286    merged |= note_type_byte as u64;
287
288    // SAFETY: The most significant bit of the suffix is zero by construction so the u64 will be a
289    // valid felt.
290    Felt::try_from(merged).expect("encoded value should be a valid felt")
291}
292
293/// Unmerges the sender ID suffix and note type.
294fn unmerge_sender_suffix_and_note_type(element: Felt) -> Result<(Felt, NoteType), NoteError> {
295    const NOTE_TYPE_MASK: u8 = 0b11;
296    // Inverts the note type mask.
297    const SENDER_SUFFIX_MASK: u64 = !(NOTE_TYPE_MASK as u64);
298
299    let note_type_byte = element.as_int() as u8 & NOTE_TYPE_MASK;
300    let note_type = NoteType::try_from(note_type_byte).map_err(|source| {
301        NoteError::other_with_source("failed to decode note type from metadata header", source)
302    })?;
303
304    // No bits were set so felt should still be valid.
305    let sender_suffix =
306        Felt::try_from(element.as_int() & SENDER_SUFFIX_MASK).expect("felt should still be valid");
307
308    Ok((sender_suffix, note_type))
309}
310
311/// Merges the [`NoteAttachmentScheme`] and [`NoteAttachmentKind`] into a single [`Felt`].
312///
313/// The layout is as follows:
314///
315/// ```text
316/// [30 zero bits | attachment_kind (2 bits) | attachment_scheme (32 bits)]
317/// ```
318fn merge_attachment_kind_scheme(
319    attachment_kind: NoteAttachmentKind,
320    attachment_scheme: NoteAttachmentScheme,
321) -> Felt {
322    debug_assert!(attachment_kind.as_u8() < 4, "attachment kind should fit into two bits");
323    let mut merged = (attachment_kind.as_u8() as u64) << 32;
324    let attachment_scheme = attachment_scheme.as_u32();
325    merged |= attachment_scheme as u64;
326
327    Felt::try_from(merged).expect("the upper bit should be zero and the felt therefore valid")
328}
329
330/// Unmerges the attachment kind and attachment scheme.
331fn unmerge_attachment_kind_scheme(
332    element: Felt,
333) -> Result<(NoteAttachmentKind, NoteAttachmentScheme), NoteError> {
334    let attachment_scheme = element.as_int() as u32;
335    let attachment_kind = (element.as_int() >> 32) as u8;
336
337    let attachment_scheme = NoteAttachmentScheme::new(attachment_scheme);
338    let attachment_kind = NoteAttachmentKind::try_from(attachment_kind).map_err(|source| {
339        NoteError::other_with_source(
340            "failed to decode attachment kind from metadata header",
341            source,
342        )
343    })?;
344
345    Ok((attachment_kind, attachment_scheme))
346}
347
348// TESTS
349// ================================================================================================
350
351#[cfg(test)]
352mod tests {
353
354    use super::*;
355    use crate::note::NoteAttachmentScheme;
356    use crate::testing::account_id::ACCOUNT_ID_MAX_ONES;
357
358    #[rstest::rstest]
359    #[case::attachment_none(NoteAttachment::default())]
360    #[case::attachment_raw(NoteAttachment::new_word(NoteAttachmentScheme::new(0), Word::from([3, 4, 5, 6u32])))]
361    #[case::attachment_commitment(NoteAttachment::new_array(
362        NoteAttachmentScheme::new(u32::MAX),
363        vec![Felt::new(5), Felt::new(6), Felt::new(7)],
364    )?)]
365    #[test]
366    fn note_metadata_serde(#[case] attachment: NoteAttachment) -> anyhow::Result<()> {
367        // Use the Account ID with the maximum one bits to test if the merge function always
368        // produces valid felts.
369        let sender = AccountId::try_from(ACCOUNT_ID_MAX_ONES).unwrap();
370        let note_type = NoteType::Public;
371        let tag = NoteTag::new(u32::MAX);
372        let metadata =
373            NoteMetadata::new(sender, note_type).with_tag(tag).with_attachment(attachment);
374
375        // Serialization Roundtrip
376        let deserialized = NoteMetadata::read_from_bytes(&metadata.to_bytes())?;
377        assert_eq!(deserialized, metadata);
378
379        // Metadata Header Roundtrip
380        let header = NoteMetadataHeader::try_from(metadata.to_header_word())?;
381        assert_eq!(header, metadata.to_header());
382
383        Ok(())
384    }
385}