Skip to main content

miden_standards/note/
execution_hint.rs

1// NOTE EXECUTION HINT
2// ================================================================================================
3
4use miden_protocol::Felt;
5use miden_protocol::block::BlockNumber;
6use miden_protocol::errors::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/// [24 zero bits | payload (32 bits) | tag (8 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    AfterBlock { block_num: BlockNumber },
32    /// The note's script can be executed in the specified slot within the specified round.
33    ///
34    /// The slot is defined as follows:
35    /// - First we define the length of the round in powers of 2. For example, round_len = 10 is a
36    ///   round of 1024 blocks.
37    /// - Then we define the length of a slot within the round also using powers of 2. For example,
38    ///   slot_len = 7 is a slot of 128 blocks.
39    /// - Lastly, the offset specifies the index of the slot within the round - i.e., 0 is the first
40    ///   slot, 1 is the second slot etc.
41    ///
42    /// For example: { round_len: 10, slot_len: 7, slot_offset: 1 } means that the note can
43    /// be executed in any second 128 block slot of a 1024 block round. These would be blocks
44    /// 128..255, 1152..1279, 2176..2303 etc.
45    OnBlockSlot {
46        round_len: u8,
47        slot_len: u8,
48        slot_offset: u8,
49    },
50}
51
52impl NoteExecutionHint {
53    // CONSTANTS
54    // ------------------------------------------------------------------------------------------------
55
56    pub(crate) const NONE_TAG: u8 = 0;
57    pub(crate) const ALWAYS_TAG: u8 = 1;
58    pub(crate) const AFTER_BLOCK_TAG: u8 = 2;
59    pub(crate) const ON_BLOCK_SLOT_TAG: u8 = 3;
60
61    // CONSTRUCTORS
62    // ------------------------------------------------------------------------------------------------
63
64    /// Creates a [NoteExecutionHint::None] variant
65    pub fn none() -> Self {
66        NoteExecutionHint::None
67    }
68
69    /// Creates a [NoteExecutionHint::Always] variant
70    pub fn always() -> Self {
71        NoteExecutionHint::Always
72    }
73
74    /// Creates a [NoteExecutionHint::AfterBlock] variant based on the given `block_num`
75    pub fn after_block(block_num: BlockNumber) -> Self {
76        NoteExecutionHint::AfterBlock { block_num }
77    }
78
79    /// Creates a [NoteExecutionHint::OnBlockSlot] for the given parameters. See the variants
80    /// documentation for details on the parameters.
81    pub fn on_block_slot(round_len: u8, slot_len: u8, slot_offset: u8) -> Self {
82        NoteExecutionHint::OnBlockSlot { round_len, slot_len, slot_offset }
83    }
84
85    pub fn from_parts(tag: u8, payload: u32) -> Result<NoteExecutionHint, NoteError> {
86        match tag {
87            Self::NONE_TAG => {
88                if payload != 0 {
89                    return Err(NoteError::InvalidNoteExecutionHintPayload(tag, payload));
90                }
91                Ok(NoteExecutionHint::None)
92            },
93            Self::ALWAYS_TAG => {
94                if payload != 0 {
95                    return Err(NoteError::InvalidNoteExecutionHintPayload(tag, payload));
96                }
97                Ok(NoteExecutionHint::Always)
98            },
99            Self::AFTER_BLOCK_TAG => Ok(NoteExecutionHint::after_block(BlockNumber::from(payload))),
100            Self::ON_BLOCK_SLOT_TAG => {
101                let remainder = ((payload >> 24) & 0xff) as u8;
102                if remainder != 0 {
103                    return Err(NoteError::InvalidNoteExecutionHintPayload(tag, payload));
104                }
105
106                let round_len = ((payload >> 16) & 0xff) as u8;
107                let slot_len = ((payload >> 8) & 0xff) as u8;
108                let slot_offset = (payload & 0xff) as u8;
109                let hint = NoteExecutionHint::OnBlockSlot { round_len, slot_len, slot_offset };
110
111                Ok(hint)
112            },
113            _ => Err(NoteError::other(format!(
114                "note execution hint tag {tag} must be in range 0..={}",
115                Self::ON_BLOCK_SLOT_TAG
116            ))),
117        }
118    }
119
120    /// Returns whether the note execution conditions validate for the given `block_num`
121    ///
122    /// # Returns
123    /// - `None` if we don't know whether the note can be consumed.
124    /// - `Some(true)` if the note is consumable for the given `block_num`
125    /// - `Some(false)` if the note is not consumable for the given `block_num`
126    pub fn can_be_consumed(&self, block_num: BlockNumber) -> Option<bool> {
127        let block_num = block_num.as_u32();
128        match self {
129            NoteExecutionHint::None => None,
130            NoteExecutionHint::Always => Some(true),
131            NoteExecutionHint::AfterBlock { block_num: hint_block_num } => {
132                Some(block_num >= hint_block_num.as_u32())
133            },
134            NoteExecutionHint::OnBlockSlot { round_len, slot_len, slot_offset } => {
135                let round_len_blocks: u32 = 1 << round_len;
136                let slot_len_blocks: u32 = 1 << slot_len;
137
138                let block_round_index = block_num / round_len_blocks;
139
140                let slot_start_block =
141                    block_round_index * round_len_blocks + (*slot_offset as u32) * slot_len_blocks;
142                let slot_end_block = slot_start_block + slot_len_blocks;
143
144                let can_be_consumed = block_num >= slot_start_block && block_num < slot_end_block;
145                Some(can_be_consumed)
146            },
147        }
148    }
149
150    /// Encodes the [`NoteExecutionHint`] into an 8-bit tag and a 32-bit payload.
151    pub fn into_parts(&self) -> (u8, u32) {
152        match self {
153            NoteExecutionHint::None => (Self::NONE_TAG, 0),
154            NoteExecutionHint::Always => (Self::ALWAYS_TAG, 0),
155            NoteExecutionHint::AfterBlock { block_num } => {
156                (Self::AFTER_BLOCK_TAG, block_num.as_u32())
157            },
158            NoteExecutionHint::OnBlockSlot { round_len, slot_len, slot_offset } => {
159                let payload: u32 =
160                    ((*round_len as u32) << 16) | ((*slot_len as u32) << 8) | (*slot_offset as u32);
161                (Self::ON_BLOCK_SLOT_TAG, payload)
162            },
163        }
164    }
165}
166
167/// Converts a [`NoteExecutionHint`] into a [`Felt`] with the layout documented on the type.
168impl From<NoteExecutionHint> for Felt {
169    fn from(value: NoteExecutionHint) -> Self {
170        let int_representation: u64 = value.into();
171        Felt::new(int_representation)
172    }
173}
174
175/// Tries to convert a `u64` into a [`NoteExecutionHint`] with the expected layout documented on the
176/// type.
177///
178/// Note: The upper 24 bits are not enforced to be zero.
179impl TryFrom<u64> for NoteExecutionHint {
180    type Error = NoteError;
181    fn try_from(value: u64) -> Result<Self, Self::Error> {
182        let tag = (value & 0b1111_1111) as u8;
183        // Shift the payload and cut off / ignore the upper 32 bits.
184        let payload = (value >> 8) as u32;
185
186        Self::from_parts(tag, payload)
187    }
188}
189
190/// Converts a [`NoteExecutionHint`] into a `u64` with the layout documented on the type.
191impl From<NoteExecutionHint> for u64 {
192    fn from(value: NoteExecutionHint) -> Self {
193        let (tag, payload) = value.into_parts();
194        ((payload as u64) << 8) | (tag as u64)
195    }
196}
197
198// TESTS
199// ================================================================================================
200
201#[cfg(test)]
202mod tests {
203
204    use super::*;
205
206    fn assert_hint_serde(note_execution_hint: NoteExecutionHint) {
207        let (tag, payload) = note_execution_hint.into_parts();
208        let deserialized = NoteExecutionHint::from_parts(tag, payload).unwrap();
209        assert_eq!(deserialized, note_execution_hint);
210    }
211
212    #[test]
213    fn test_serialization_round_trip() {
214        assert_hint_serde(NoteExecutionHint::None);
215        assert_hint_serde(NoteExecutionHint::Always);
216        assert_hint_serde(NoteExecutionHint::after_block(15.into()));
217        assert_hint_serde(NoteExecutionHint::OnBlockSlot {
218            round_len: 9,
219            slot_len: 12,
220            slot_offset: 18,
221        });
222    }
223
224    #[test]
225    fn test_encode_round_trip() {
226        let hint = NoteExecutionHint::after_block(15.into());
227        let hint_int: u64 = hint.into();
228        let decoded_hint: NoteExecutionHint = hint_int.try_into().unwrap();
229        assert_eq!(hint, decoded_hint);
230
231        let hint = NoteExecutionHint::OnBlockSlot {
232            round_len: 22,
233            slot_len: 33,
234            slot_offset: 44,
235        };
236        let hint_int: u64 = hint.into();
237        let decoded_hint: NoteExecutionHint = hint_int.try_into().unwrap();
238        assert_eq!(hint, decoded_hint);
239
240        let always_int: u64 = NoteExecutionHint::always().into();
241        assert_eq!(always_int, 1u64);
242    }
243
244    #[test]
245    fn test_can_be_consumed() {
246        let none = NoteExecutionHint::none();
247        assert!(none.can_be_consumed(100.into()).is_none());
248
249        let always = NoteExecutionHint::always();
250        assert!(always.can_be_consumed(100.into()).unwrap());
251
252        let after_block = NoteExecutionHint::after_block(12345.into());
253        assert!(!after_block.can_be_consumed(12344.into()).unwrap());
254        assert!(after_block.can_be_consumed(12345.into()).unwrap());
255
256        let on_block_slot = NoteExecutionHint::on_block_slot(10, 7, 1);
257        assert!(!on_block_slot.can_be_consumed(127.into()).unwrap()); // Block 127 is not in the slot 128..255
258        assert!(on_block_slot.can_be_consumed(128.into()).unwrap()); // Block 128 is in the slot 128..255
259        assert!(on_block_slot.can_be_consumed(255.into()).unwrap()); // Block 255 is in the slot 128..255
260        assert!(!on_block_slot.can_be_consumed(256.into()).unwrap()); // Block 256 is not in the slot 128..255
261        assert!(on_block_slot.can_be_consumed(1152.into()).unwrap()); // Block 1152 is in the slot 1152..1279
262        assert!(on_block_slot.can_be_consumed(1279.into()).unwrap()); // Block 1279 is in the slot 1152..1279
263        assert!(on_block_slot.can_be_consumed(2176.into()).unwrap()); // Block 2176 is in the slot 2176..2303
264        assert!(!on_block_slot.can_be_consumed(2175.into()).unwrap()); // Block 1279 is in the slot
265        // 2176..2303
266    }
267
268    #[test]
269    fn test_parts_validity() {
270        NoteExecutionHint::from_parts(NoteExecutionHint::NONE_TAG, 1).unwrap_err();
271        NoteExecutionHint::from_parts(NoteExecutionHint::ALWAYS_TAG, 12).unwrap_err();
272        // 4th byte should be blank for tag 3 (OnBlockSlot)
273        NoteExecutionHint::from_parts(NoteExecutionHint::ON_BLOCK_SLOT_TAG, 1 << 24).unwrap_err();
274        NoteExecutionHint::from_parts(NoteExecutionHint::ON_BLOCK_SLOT_TAG, 0).unwrap();
275
276        NoteExecutionHint::from_parts(10, 1).unwrap_err();
277    }
278}