miden_objects/note/
metadata.rs

1use alloc::string::ToString;
2
3use super::{
4    execution_hint::NoteExecutionHint, AccountId, ByteReader, ByteWriter, Deserializable,
5    DeserializationError, Felt, NoteError, NoteTag, NoteType, Serializable, Word,
6};
7
8// NOTE METADATA
9// ================================================================================================
10
11/// Metadata associated with a note.
12///
13/// Note type and tag must be internally consistent according to the following rules:
14///
15/// - For private and encrypted notes, the two most significant bits of the tag must be `0b11`.
16/// - For public notes, the two most significant bits of the tag can be set to any value.
17///
18/// # Word layout & validity
19///
20/// [`NoteMetadata`] can be encoded into a [`Word`] with the following layout:
21///
22/// ```text
23/// 1st felt: [sender_id_prefix (64 bits)]
24/// 2nd felt: [sender_id_suffix (56 bits) | note_type (2 bits) | note_execution_hint_tag (6 bits)]
25/// 3rd felt: [note_execution_hint_payload (32 bits) | note_tag (32 bits)]
26/// 4th felt: [aux (64 bits)]
27/// ```
28///
29/// The rationale for the above layout is to ensure the validity of each felt:
30/// - 1st felt: Is equivalent to the prefix of the account ID so it inherits its validity.
31/// - 2nd felt: The lower 8 bits of the account ID suffix are `0` by construction, so that they can
32///   be overwritten with other data. The suffix is designed such that it retains its felt validity
33///   even if all of its lower 8 bits are be set to `1`. This is because the anchor epoch in the
34///   upper 16 bits always contains at least one `0` bit.
35/// - 3rd felt: The note execution hint payload must contain at least one `0` bit in its encoding,
36///   so the upper 32 bits of the felt will contain at least one `0` bit making the entire felt
37///   valid.
38/// - 4th felt: The `aux` value must be a felt itself.
39#[derive(Clone, Copy, Debug, Eq, PartialEq)]
40pub struct NoteMetadata {
41    /// The ID of the account which created the note.
42    sender: AccountId,
43
44    /// Defines how the note is to be stored (e.g. public or private).
45    note_type: NoteType,
46
47    /// A value which can be used by the recipient(s) to identify notes intended for them.
48    tag: NoteTag,
49
50    /// An arbitrary user-defined value.
51    aux: Felt,
52
53    /// Specifies when a note is ready to be consumed.
54    execution_hint: NoteExecutionHint,
55}
56
57impl NoteMetadata {
58    /// Returns a new [NoteMetadata] instantiated with the specified parameters.
59    ///
60    /// # Errors
61    /// Returns an error if the note type and note tag are inconsistent.
62    pub fn new(
63        sender: AccountId,
64        note_type: NoteType,
65        tag: NoteTag,
66        execution_hint: NoteExecutionHint,
67        aux: Felt,
68    ) -> Result<Self, NoteError> {
69        let tag = tag.validate(note_type)?;
70        Ok(Self {
71            sender,
72            note_type,
73            tag,
74            aux,
75            execution_hint,
76        })
77    }
78
79    /// Returns the account which created the note.
80    pub fn sender(&self) -> AccountId {
81        self.sender
82    }
83
84    /// Returns the note's type.
85    pub fn note_type(&self) -> NoteType {
86        self.note_type
87    }
88
89    /// Returns the tag associated with the note.
90    pub fn tag(&self) -> NoteTag {
91        self.tag
92    }
93
94    /// Returns the execution hint associated with the note.
95    pub fn execution_hint(&self) -> NoteExecutionHint {
96        self.execution_hint
97    }
98
99    /// Returns the note's aux field.
100    pub fn aux(&self) -> Felt {
101        self.aux
102    }
103
104    /// Returns `true` if the note is private.
105    pub fn is_private(&self) -> bool {
106        self.note_type == NoteType::Private
107    }
108}
109
110impl From<NoteMetadata> for Word {
111    /// Convert a [`NoteMetadata`] into a [`Word`].
112    ///
113    /// The produced layout of the word is documented on the [`NoteMetadata`] type.
114    fn from(metadata: NoteMetadata) -> Self {
115        (&metadata).into()
116    }
117}
118
119impl From<&NoteMetadata> for Word {
120    /// Convert a [`NoteMetadata`] into a [`Word`].
121    ///
122    /// The produced layout of the word is documented on the [`NoteMetadata`] type.
123    fn from(metadata: &NoteMetadata) -> Self {
124        let mut elements = Word::default();
125        elements[0] = metadata.sender.prefix().as_felt();
126        elements[1] = merge_id_type_and_hint_tag(
127            metadata.sender.suffix(),
128            metadata.note_type,
129            metadata.execution_hint,
130        );
131        elements[2] = merge_note_tag_and_hint_payload(metadata.execution_hint, metadata.tag);
132        elements[3] = metadata.aux;
133        elements
134    }
135}
136
137impl TryFrom<Word> for NoteMetadata {
138    type Error = NoteError;
139
140    /// Tries to decode a [`Word`] into a [`NoteMetadata`].
141    ///
142    /// The expected layout of the word is documented on the [`NoteMetadata`] type.
143    fn try_from(elements: Word) -> Result<Self, Self::Error> {
144        let sender_id_prefix: Felt = elements[0];
145
146        let (sender_id_suffix, note_type, execution_hint_tag) =
147            unmerge_id_type_and_hint_tag(elements[1])?;
148
149        let sender = AccountId::try_from([sender_id_prefix, sender_id_suffix])
150            .map_err(NoteError::NoteSenderInvalidAccountId)?;
151
152        let (execution_hint, note_tag) =
153            unmerge_note_tag_and_hint_payload(elements[2], execution_hint_tag)?;
154
155        Self::new(sender, note_type, note_tag, execution_hint, elements[3])
156    }
157}
158
159// SERIALIZATION
160// ================================================================================================
161
162impl Serializable for NoteMetadata {
163    fn write_into<W: ByteWriter>(&self, target: &mut W) {
164        Word::from(self).write_into(target);
165    }
166}
167
168impl Deserializable for NoteMetadata {
169    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
170        let word = Word::read_from(source)?;
171        Self::try_from(word).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
172    }
173}
174
175// HELPER FUNCTIONS
176// ================================================================================================
177
178/// Merges the suffix of an [`AccountId`], a [`NoteType`] and the tag of a
179/// [`NoteExecutionHint`] into a single [`Felt`].
180///
181/// The layout is as follows:
182///
183/// ```text
184/// [sender_id_suffix (56 bits) | note_type (2 bits) | note_execution_hint_tag (6 bits)]
185/// ```
186///
187/// One of the upper 16 bits is guaranteed to be zero due to the guarantees of the epoch in the
188/// account ID.
189///
190/// Note that `sender_id_suffix` is the suffix of the sender's account ID.
191fn merge_id_type_and_hint_tag(
192    sender_id_suffix: Felt,
193    note_type: NoteType,
194    note_execution_hint: NoteExecutionHint,
195) -> Felt {
196    let mut merged = sender_id_suffix.as_int();
197
198    let type_bits = note_type as u8;
199    let (tag_bits, _) = note_execution_hint.into_parts();
200
201    debug_assert!(type_bits & 0b1111_1100 == 0, "note type must not contain values >= 4");
202    debug_assert!(
203        tag_bits & 0b1100_0000 == 0,
204        "note execution hint tag must not contain values >= 64"
205    );
206
207    // Note: The least significant byte of the second AccountId felt is zero by construction so we
208    // can overwrite it.
209    merged |= (type_bits << 6) as u64;
210    merged |= tag_bits as u64;
211
212    // SAFETY: One of the top 16 bits (the anchor epoch) of the suffix is zero by construction
213    // so the bytes will be a valid felt.
214    Felt::try_from(merged).expect("encoded value should be a valid felt")
215}
216
217/// Unmerges the given felt into the suffix of an [`AccountId`], a [`NoteType`] and the tag of
218/// a [`NoteExecutionHint`].
219fn unmerge_id_type_and_hint_tag(element: Felt) -> Result<(Felt, NoteType, u8), NoteError> {
220    let element = element.as_int();
221
222    // Cut off the least significant byte.
223    let least_significant_byte = element as u8;
224    let note_type_bits = (least_significant_byte & 0b1100_0000) >> 6;
225    let tag_bits = least_significant_byte & 0b0011_1111;
226
227    let note_type = NoteType::try_from(note_type_bits)?;
228
229    // Set least significant byte to zero.
230    let element = element & 0xffff_ffff_ffff_ff00;
231
232    // SAFETY: The input was a valid felt and and we cleared additional bits and did not set any
233    // bits, so it must still be a valid felt.
234    let sender_id_suffix = Felt::try_from(element).expect("element should still be valid");
235
236    Ok((sender_id_suffix, note_type, tag_bits))
237}
238
239/// Merges the [`NoteExecutionHint`] payload and a [`NoteTag`] into a single [`Felt`].
240///
241/// The layout is as follows:
242///
243/// ```text
244/// [note_execution_hint_payload (32 bits) | note_tag (32 bits)]
245/// ```
246///
247/// One of the upper 32 bits is guaranteed to be zero.
248fn merge_note_tag_and_hint_payload(
249    note_execution_hint: NoteExecutionHint,
250    note_tag: NoteTag,
251) -> Felt {
252    let (_, payload) = note_execution_hint.into_parts();
253    let note_tag: u32 = note_tag.into();
254
255    debug_assert_ne!(
256        payload,
257        u32::MAX,
258        "payload should never be u32::MAX as it would produce an invalid felt"
259    );
260
261    let felt_int = ((payload as u64) << 32) | (note_tag as u64);
262
263    // SAFETY: The payload is guaranteed to never be u32::MAX so at least one of the upper 32 bits
264    // is zero, hence the felt is valid even if note_tag is u32::MAX.
265    Felt::try_from(felt_int).expect("bytes should be a valid felt")
266}
267
268/// Unmerges the given felt into a [`NoteExecutionHint`] payload and a [`NoteTag`] and constructs a
269/// [`NoteExecutionHint`] from the unmerged payload and the given `note_execution_hint_tag`.
270fn unmerge_note_tag_and_hint_payload(
271    element: Felt,
272    note_execution_hint_tag: u8,
273) -> Result<(NoteExecutionHint, NoteTag), NoteError> {
274    let element = element.as_int();
275
276    let payload = (element >> 32) as u32;
277    let note_tag = (element & 0xffff_ffff) as u32;
278
279    let execution_hint = NoteExecutionHint::from_parts(note_execution_hint_tag, payload)?;
280    let note_tag = NoteTag::from(note_tag);
281
282    Ok((execution_hint, note_tag))
283}
284
285// TESTS
286// ================================================================================================
287
288#[cfg(test)]
289mod tests {
290
291    use anyhow::Context;
292
293    use super::*;
294    use crate::{note::NoteExecutionMode, testing::account_id::ACCOUNT_ID_MAX_ONES};
295
296    #[test]
297    fn note_metadata_serde() -> anyhow::Result<()> {
298        // Use the Account ID with the maximum one bits to test if the merge function always
299        // produces valid felts.
300        let sender = AccountId::try_from(ACCOUNT_ID_MAX_ONES).unwrap();
301        let note_type = NoteType::Public;
302        let tag = NoteTag::from_account_id(sender, NoteExecutionMode::Local).unwrap();
303        let aux = Felt::try_from(0xffff_ffff_0000_0000u64).unwrap();
304
305        for execution_hint in [
306            NoteExecutionHint::always(),
307            NoteExecutionHint::none(),
308            NoteExecutionHint::on_block_slot(10, 11, 12),
309            NoteExecutionHint::after_block((u32::MAX - 1).into()).unwrap(),
310        ] {
311            let metadata = NoteMetadata::new(sender, note_type, tag, execution_hint, aux).unwrap();
312            NoteMetadata::read_from_bytes(&metadata.to_bytes())
313                .context(format!("failed for execution hint {execution_hint:?}"))?;
314        }
315
316        Ok(())
317    }
318
319    #[test]
320    fn merge_and_unmerge_id_type_and_hint() {
321        // Use the Account ID with the maximum one bits to test if the merge function always
322        // produces valid felts.
323        let sender = AccountId::try_from(ACCOUNT_ID_MAX_ONES).unwrap();
324        let sender_id_suffix = sender.suffix();
325
326        let note_type = NoteType::Public;
327        let note_execution_hint = NoteExecutionHint::OnBlockSlot {
328            round_len: 10,
329            slot_len: 11,
330            slot_offset: 12,
331        };
332
333        let merged_value =
334            merge_id_type_and_hint_tag(sender_id_suffix, note_type, note_execution_hint);
335        let (extracted_suffix, extracted_note_type, extracted_note_execution_hint_tag) =
336            unmerge_id_type_and_hint_tag(merged_value).unwrap();
337
338        assert_eq!(note_type, extracted_note_type);
339        assert_eq!(note_execution_hint.into_parts().0, extracted_note_execution_hint_tag);
340        assert_eq!(sender_id_suffix, extracted_suffix);
341
342        let note_type = NoteType::Private;
343        let note_execution_hint = NoteExecutionHint::Always;
344
345        let merged_value =
346            merge_id_type_and_hint_tag(sender_id_suffix, note_type, note_execution_hint);
347        let (extracted_suffix, extracted_note_type, extracted_note_execution_hint_tag) =
348            unmerge_id_type_and_hint_tag(merged_value).unwrap();
349
350        assert_eq!(note_type, extracted_note_type);
351        assert_eq!(note_execution_hint.into_parts().0, extracted_note_execution_hint_tag);
352        assert_eq!(sender_id_suffix, extracted_suffix);
353
354        let note_type = NoteType::Private;
355        let note_execution_hint = NoteExecutionHint::None;
356
357        let merged_value =
358            merge_id_type_and_hint_tag(sender_id_suffix, note_type, note_execution_hint);
359        let (extracted_suffix, extracted_note_type, extracted_note_execution_hint_tag) =
360            unmerge_id_type_and_hint_tag(merged_value).unwrap();
361
362        assert_eq!(note_type, extracted_note_type);
363        assert_eq!(note_execution_hint.into_parts().0, extracted_note_execution_hint_tag);
364        assert_eq!(sender_id_suffix, extracted_suffix);
365    }
366}