miden_objects/note/
execution_hint.rs

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