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