miden_objects/note/
metadata.rs

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