miden_objects/note/
execution_hint.rs

1// NOTE EXECUTION HINT
2// ================================================================================================
3
4use crate::{Felt, NoteError, block::BlockNumber};
5
6/// Specifies the conditions under which a note is ready to be consumed.
7/// These conditions are meant to be encoded in the note script as well.
8///
9/// This struct can be represented as the combination of a tag, and a payload.
10/// The tag specifies the variant of the hint, and the payload encodes the hint data.
11///
12/// # Felt layout
13///
14/// [`NoteExecutionHint`] can be encoded into a [`Felt`] with the following layout:
15///
16/// ```text
17/// [26 zero bits | payload (32 bits) | tag (6 bits)]
18/// ```
19///
20/// This way, hints such as [NoteExecutionHint::Always], are represented by `Felt::new(1)`.
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum NoteExecutionHint {
23    /// Unspecified note execution hint. Implies it is not known under which conditions the note
24    /// is consumable.
25    None,
26    /// The note's script can be executed at any time.
27    Always,
28    /// The note's script can be executed after the specified block number.
29    ///
30    /// The block number cannot be [`u32::MAX`] which is enforced through the [`AfterBlockNumber`]
31    /// type.
32    AfterBlock { block_num: AfterBlockNumber },
33    /// The note's script can be executed in the specified slot within the specified round.
34    ///
35    /// The slot is defined as follows:
36    /// - First we define the length of the round in powers of 2. For example, round_len = 10 is a
37    ///   round of 1024 blocks.
38    /// - Then we define the length of a slot within the round also using powers of 2. For example,
39    ///   slot_len = 7 is a slot of 128 blocks.
40    /// - Lastly, the offset specifies the index of the slot within the round - i.e., 0 is the first
41    ///   slot, 1 is the second slot etc.
42    ///
43    /// For example: { round_len: 10, slot_len: 7, slot_offset: 1 } means that the note can
44    /// be executed in any second 128 block slot of a 1024 block round. These would be blocks
45    /// 128..255, 1152..1279, 2176..2303 etc.
46    OnBlockSlot {
47        round_len: u8,
48        slot_len: u8,
49        slot_offset: u8,
50    },
51}
52
53impl NoteExecutionHint {
54    // CONSTANTS
55    // ------------------------------------------------------------------------------------------------
56
57    pub(crate) const NONE_TAG: u8 = 0;
58    pub(crate) const ALWAYS_TAG: u8 = 1;
59    pub(crate) const AFTER_BLOCK_TAG: u8 = 2;
60    pub(crate) const ON_BLOCK_SLOT_TAG: u8 = 3;
61
62    // CONSTRUCTORS
63    // ------------------------------------------------------------------------------------------------
64
65    /// Creates a [NoteExecutionHint::None] variant
66    pub fn none() -> Self {
67        NoteExecutionHint::None
68    }
69
70    /// Creates a [NoteExecutionHint::Always] variant
71    pub fn always() -> Self {
72        NoteExecutionHint::Always
73    }
74
75    /// Creates a [NoteExecutionHint::AfterBlock] variant based on the given `block_num`
76    ///
77    /// # Errors
78    ///
79    /// Returns an error if `block_num` is equal to [`u32::MAX`].
80    pub fn after_block(block_num: BlockNumber) -> Result<Self, NoteError> {
81        AfterBlockNumber::new(block_num)
82            .map(|block_number| NoteExecutionHint::AfterBlock { block_num: block_number })
83    }
84
85    /// Creates a [NoteExecutionHint::OnBlockSlot] for the given parameters. See the variants
86    /// documentation for details on the parameters.
87    pub fn on_block_slot(round_len: u8, slot_len: u8, slot_offset: u8) -> Self {
88        NoteExecutionHint::OnBlockSlot { round_len, slot_len, slot_offset }
89    }
90
91    pub fn from_parts(tag: u8, payload: u32) -> Result<NoteExecutionHint, NoteError> {
92        match tag {
93            Self::NONE_TAG => {
94                if payload != 0 {
95                    return Err(NoteError::InvalidNoteExecutionHintPayload(tag, payload));
96                }
97                Ok(NoteExecutionHint::None)
98            },
99            Self::ALWAYS_TAG => {
100                if payload != 0 {
101                    return Err(NoteError::InvalidNoteExecutionHintPayload(tag, payload));
102                }
103                Ok(NoteExecutionHint::Always)
104            },
105            Self::AFTER_BLOCK_TAG => NoteExecutionHint::after_block(payload.into()),
106            Self::ON_BLOCK_SLOT_TAG => {
107                let remainder = ((payload >> 24) & 0xff) as u8;
108                if remainder != 0 {
109                    return Err(NoteError::InvalidNoteExecutionHintPayload(tag, payload));
110                }
111
112                let round_len = ((payload >> 16) & 0xff) as u8;
113                let slot_len = ((payload >> 8) & 0xff) as u8;
114                let slot_offset = (payload & 0xff) as u8;
115                let hint = NoteExecutionHint::OnBlockSlot { round_len, slot_len, slot_offset };
116
117                Ok(hint)
118            },
119            _ => Err(NoteError::NoteExecutionHintTagOutOfRange(tag)),
120        }
121    }
122
123    /// Returns whether the note execution conditions validate for the given `block_num`
124    ///
125    /// # Returns
126    /// - `None` if we don't know whether the note can be consumed.
127    /// - `Some(true)` if the note is consumable for the given `block_num`
128    /// - `Some(false)` if the note is not consumable for the given `block_num`
129    pub fn can_be_consumed(&self, block_num: BlockNumber) -> Option<bool> {
130        let block_num = block_num.as_u32();
131        match self {
132            NoteExecutionHint::None => None,
133            NoteExecutionHint::Always => Some(true),
134            NoteExecutionHint::AfterBlock { block_num: hint_block_num } => {
135                Some(block_num >= hint_block_num.as_u32())
136            },
137            NoteExecutionHint::OnBlockSlot { round_len, slot_len, slot_offset } => {
138                let round_len_blocks: u32 = 1 << round_len;
139                let slot_len_blocks: u32 = 1 << slot_len;
140
141                let block_round_index = block_num / round_len_blocks;
142
143                let slot_start_block =
144                    block_round_index * round_len_blocks + (*slot_offset as u32) * slot_len_blocks;
145                let slot_end_block = slot_start_block + slot_len_blocks;
146
147                let can_be_consumed = block_num >= slot_start_block && block_num < slot_end_block;
148                Some(can_be_consumed)
149            },
150        }
151    }
152
153    /// Encodes the [`NoteExecutionHint`] into a 6-bit tag and a 32-bit payload.
154    ///
155    /// # Guarantees
156    ///
157    /// Since the tag has at most 6 bits, the returned byte is guaranteed to have its two most
158    /// significant bits set to `0`.
159    ///
160    /// The payload is guaranteed to contain at least one `0` bit to make encoding it into
161    /// [`NoteMetadata`](crate::note::NoteMetadata) safely possible.
162    pub fn into_parts(&self) -> (u8, u32) {
163        match self {
164            NoteExecutionHint::None => (Self::NONE_TAG, 0),
165            NoteExecutionHint::Always => (Self::ALWAYS_TAG, 0),
166            NoteExecutionHint::AfterBlock { block_num } => {
167                (Self::AFTER_BLOCK_TAG, block_num.as_u32())
168            },
169            NoteExecutionHint::OnBlockSlot { round_len, slot_len, slot_offset } => {
170                let payload: u32 =
171                    ((*round_len as u32) << 16) | ((*slot_len as u32) << 8) | (*slot_offset as u32);
172                (Self::ON_BLOCK_SLOT_TAG, payload)
173            },
174        }
175    }
176}
177
178/// Converts a [`NoteExecutionHint`] into a [`Felt`] with the layout documented on the type.
179impl From<NoteExecutionHint> for Felt {
180    fn from(value: NoteExecutionHint) -> Self {
181        let int_representation: u64 = value.into();
182        Felt::new(int_representation)
183    }
184}
185
186/// Tries to convert a `u64` into a [`NoteExecutionHint`] with the expected layout documented on the
187/// type.
188///
189/// Note: The upper 26 bits are not enforced to be zero.
190impl TryFrom<u64> for NoteExecutionHint {
191    type Error = NoteError;
192    fn try_from(value: u64) -> Result<Self, Self::Error> {
193        let tag = (value & 0b111111) as u8;
194        let payload = ((value >> 6) & 0xffffffff) as u32;
195
196        Self::from_parts(tag, payload)
197    }
198}
199
200/// Converts a [`NoteExecutionHint`] into a `u64` with the layout documented on the type.
201impl From<NoteExecutionHint> for u64 {
202    fn from(value: NoteExecutionHint) -> Self {
203        let (tag, payload) = value.into_parts();
204        ((payload as u64) << 6) | (tag as u64)
205    }
206}
207
208// AFTER BLOCK NUMBER
209// ================================================================================================
210
211/// A wrapper around a block number which enforces that it is not `u32::MAX`.
212///
213/// Used for the [`NoteExecutionHint::AfterBlock`] variant where this constraint is needed.
214#[derive(Debug, Clone, Copy, PartialEq, Eq)]
215pub struct AfterBlockNumber(BlockNumber);
216
217impl AfterBlockNumber {
218    /// Creates a new [`AfterBlockNumber`] from the given `block_number`.
219    ///
220    /// # Errors
221    ///
222    /// Returns an error if:
223    /// - `block_number` is equal to `u32::MAX`.
224    pub fn new(block_number: BlockNumber) -> Result<Self, NoteError> {
225        if block_number.as_u32() == u32::MAX {
226            Err(NoteError::NoteExecutionHintAfterBlockCannotBeU32Max)
227        } else {
228            Ok(Self(block_number))
229        }
230    }
231
232    /// Returns the block number as a `u32`.
233    pub fn as_u32(&self) -> u32 {
234        self.0.as_u32()
235    }
236}
237
238impl From<AfterBlockNumber> for u32 {
239    fn from(block_number: AfterBlockNumber) -> Self {
240        block_number.0.as_u32()
241    }
242}
243
244impl TryFrom<u32> for AfterBlockNumber {
245    type Error = NoteError;
246
247    fn try_from(block_number: u32) -> Result<Self, Self::Error> {
248        Self::new(block_number.into())
249    }
250}
251
252// TESTS
253// ================================================================================================
254
255#[cfg(test)]
256mod tests {
257    use assert_matches::assert_matches;
258
259    use super::*;
260
261    fn assert_hint_serde(note_execution_hint: NoteExecutionHint) {
262        let (tag, payload) = note_execution_hint.into_parts();
263        let deserialized = NoteExecutionHint::from_parts(tag, payload).unwrap();
264        assert_eq!(deserialized, note_execution_hint);
265    }
266
267    #[test]
268    fn test_serialization_round_trip() {
269        assert_hint_serde(NoteExecutionHint::None);
270        assert_hint_serde(NoteExecutionHint::Always);
271        assert_hint_serde(NoteExecutionHint::after_block(15.into()).unwrap());
272        assert_hint_serde(NoteExecutionHint::OnBlockSlot {
273            round_len: 9,
274            slot_len: 12,
275            slot_offset: 18,
276        });
277    }
278
279    #[test]
280    fn test_encode_round_trip() {
281        let hint = NoteExecutionHint::after_block(15.into()).unwrap();
282        let hint_int: u64 = hint.into();
283        let decoded_hint: NoteExecutionHint = hint_int.try_into().unwrap();
284        assert_eq!(hint, decoded_hint);
285
286        let hint = NoteExecutionHint::OnBlockSlot {
287            round_len: 22,
288            slot_len: 33,
289            slot_offset: 44,
290        };
291        let hint_int: u64 = hint.into();
292        let decoded_hint: NoteExecutionHint = hint_int.try_into().unwrap();
293        assert_eq!(hint, decoded_hint);
294
295        let always_int: u64 = NoteExecutionHint::always().into();
296        assert_eq!(always_int, 1u64);
297    }
298
299    #[test]
300    fn test_can_be_consumed() {
301        let none = NoteExecutionHint::none();
302        assert!(none.can_be_consumed(100.into()).is_none());
303
304        let always = NoteExecutionHint::always();
305        assert!(always.can_be_consumed(100.into()).unwrap());
306
307        let after_block = NoteExecutionHint::after_block(12345.into()).unwrap();
308        assert!(!after_block.can_be_consumed(12344.into()).unwrap());
309        assert!(after_block.can_be_consumed(12345.into()).unwrap());
310
311        let on_block_slot = NoteExecutionHint::on_block_slot(10, 7, 1);
312        assert!(!on_block_slot.can_be_consumed(127.into()).unwrap()); // Block 127 is not in the slot 128..255
313        assert!(on_block_slot.can_be_consumed(128.into()).unwrap()); // Block 128 is in the slot 128..255
314        assert!(on_block_slot.can_be_consumed(255.into()).unwrap()); // Block 255 is in the slot 128..255
315        assert!(!on_block_slot.can_be_consumed(256.into()).unwrap()); // Block 256 is not in the slot 128..255
316        assert!(on_block_slot.can_be_consumed(1152.into()).unwrap()); // Block 1152 is in the slot 1152..1279
317        assert!(on_block_slot.can_be_consumed(1279.into()).unwrap()); // Block 1279 is in the slot 1152..1279
318        assert!(on_block_slot.can_be_consumed(2176.into()).unwrap()); // Block 2176 is in the slot 2176..2303
319        assert!(!on_block_slot.can_be_consumed(2175.into()).unwrap()); // Block 1279 is in the slot
320        // 2176..2303
321    }
322
323    #[test]
324    fn test_parts_validity() {
325        NoteExecutionHint::from_parts(NoteExecutionHint::NONE_TAG, 1).unwrap_err();
326        NoteExecutionHint::from_parts(NoteExecutionHint::ALWAYS_TAG, 12).unwrap_err();
327        // 4th byte should be blank for tag 3 (OnBlockSlot)
328        NoteExecutionHint::from_parts(NoteExecutionHint::ON_BLOCK_SLOT_TAG, 1 << 24).unwrap_err();
329        NoteExecutionHint::from_parts(NoteExecutionHint::ON_BLOCK_SLOT_TAG, 0).unwrap();
330
331        NoteExecutionHint::from_parts(10, 1).unwrap_err();
332    }
333
334    #[test]
335    fn test_after_block_fails_on_u32_max() {
336        assert_matches!(
337            NoteExecutionHint::after_block(u32::MAX.into()).unwrap_err(),
338            NoteError::NoteExecutionHintAfterBlockCannotBeU32Max
339        );
340    }
341}