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::ONE`.
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    /// An encoding that this version does not recognize, preserved verbatim.
51    Unknown(Felt),
52}
53
54impl NoteExecutionHint {
55    // CONSTANTS
56    // ------------------------------------------------------------------------------------------------
57
58    pub(crate) const NONE_TAG: u8 = 0;
59    pub(crate) const ALWAYS_TAG: u8 = 1;
60    pub(crate) const AFTER_BLOCK_TAG: u8 = 2;
61    pub(crate) const ON_BLOCK_SLOT_TAG: u8 = 3;
62
63    // CONSTRUCTORS
64    // ------------------------------------------------------------------------------------------------
65
66    /// Creates a [NoteExecutionHint::None] variant
67    pub fn none() -> Self {
68        NoteExecutionHint::None
69    }
70
71    /// Creates a [NoteExecutionHint::Always] variant
72    pub fn always() -> Self {
73        NoteExecutionHint::Always
74    }
75
76    /// Creates a [NoteExecutionHint::AfterBlock] variant based on the given `block_num`
77    pub fn after_block(block_num: BlockNumber) -> Self {
78        NoteExecutionHint::AfterBlock { block_num }
79    }
80
81    /// Creates a [NoteExecutionHint::OnBlockSlot] for the given parameters. See the variants
82    /// documentation for details on the parameters.
83    pub fn on_block_slot(round_len: u8, slot_len: u8, slot_offset: u8) -> Self {
84        NoteExecutionHint::OnBlockSlot { round_len, slot_len, slot_offset }
85    }
86
87    pub fn from_parts(tag: u8, payload: u32) -> Result<NoteExecutionHint, NoteError> {
88        match tag {
89            Self::NONE_TAG => {
90                if payload != 0 {
91                    return Err(NoteError::InvalidNoteExecutionHintPayload(tag, payload));
92                }
93                Ok(NoteExecutionHint::None)
94            },
95            Self::ALWAYS_TAG => {
96                if payload != 0 {
97                    return Err(NoteError::InvalidNoteExecutionHintPayload(tag, payload));
98                }
99                Ok(NoteExecutionHint::Always)
100            },
101            Self::AFTER_BLOCK_TAG => Ok(NoteExecutionHint::after_block(BlockNumber::from(payload))),
102            Self::ON_BLOCK_SLOT_TAG => {
103                let remainder = ((payload >> 24) & 0xff) as u8;
104                if remainder != 0 {
105                    return Err(NoteError::InvalidNoteExecutionHintPayload(tag, payload));
106                }
107
108                let round_len = ((payload >> 16) & 0xff) as u8;
109                let slot_len = ((payload >> 8) & 0xff) as u8;
110                let slot_offset = (payload & 0xff) as u8;
111                let hint = NoteExecutionHint::OnBlockSlot { round_len, slot_len, slot_offset };
112
113                Ok(hint)
114            },
115            _ => Err(NoteError::other(format!(
116                "note execution hint tag {tag} must be in range 0..={}",
117                Self::ON_BLOCK_SLOT_TAG
118            ))),
119        }
120    }
121
122    /// Returns whether the note execution conditions validate for the given `block_num`
123    ///
124    /// # Returns
125    /// - `None` if we don't know whether the note can be consumed.
126    /// - `Some(true)` if the note is consumable for the given `block_num`
127    /// - `Some(false)` if the note is not consumable for the given `block_num`
128    pub fn can_be_consumed(&self, block_num: BlockNumber) -> Option<bool> {
129        let block_num = block_num.as_u32();
130        match self {
131            NoteExecutionHint::None | NoteExecutionHint::Unknown(_) => None,
132            NoteExecutionHint::Always => Some(true),
133            NoteExecutionHint::AfterBlock { block_num: hint_block_num } => {
134                Some(block_num >= hint_block_num.as_u32())
135            },
136            NoteExecutionHint::OnBlockSlot { round_len, slot_len, slot_offset } => {
137                let round_len_blocks: u32 = 1 << round_len;
138                let slot_len_blocks: u32 = 1 << slot_len;
139
140                let block_round_index = block_num / round_len_blocks;
141
142                let slot_start_block =
143                    block_round_index * round_len_blocks + (*slot_offset as u32) * slot_len_blocks;
144                let slot_end_block = slot_start_block + slot_len_blocks;
145
146                let can_be_consumed = block_num >= slot_start_block && block_num < slot_end_block;
147                Some(can_be_consumed)
148            },
149        }
150    }
151
152    /// Encodes the [`NoteExecutionHint`] into an 8-bit tag and a 32-bit payload, or `None` for
153    /// [`NoteExecutionHint::Unknown`], which by definition has no valid decomposition.
154    pub fn into_parts(&self) -> Option<(u8, u32)> {
155        match self {
156            NoteExecutionHint::None => Some((Self::NONE_TAG, 0)),
157            NoteExecutionHint::Always => Some((Self::ALWAYS_TAG, 0)),
158            NoteExecutionHint::AfterBlock { block_num } => {
159                Some((Self::AFTER_BLOCK_TAG, block_num.as_u32()))
160            },
161            NoteExecutionHint::OnBlockSlot { round_len, slot_len, slot_offset } => {
162                let payload: u32 =
163                    ((*round_len as u32) << 16) | ((*slot_len as u32) << 8) | (*slot_offset as u32);
164                Some((Self::ON_BLOCK_SLOT_TAG, payload))
165            },
166            NoteExecutionHint::Unknown(_) => None,
167        }
168    }
169}
170
171/// Converts a [`NoteExecutionHint`] into a [`Felt`] with the layout documented on the type.
172impl From<NoteExecutionHint> for Felt {
173    fn from(value: NoteExecutionHint) -> Self {
174        match value {
175            NoteExecutionHint::Unknown(felt) => felt,
176            hint => {
177                let (tag, payload) =
178                    hint.into_parts().expect("every hint but `Unknown` decomposes into parts");
179                // The composed value occupies the low 40 bits, so it is always canonical.
180                Felt::new_unchecked(((payload as u64) << 8) | (tag as u64))
181            },
182        }
183    }
184}
185
186/// Converts a [`Felt`] into a [`NoteExecutionHint`] with the layout documented on the type.
187impl From<Felt> for NoteExecutionHint {
188    fn from(value: Felt) -> Self {
189        let encoded = value.as_canonical_u64();
190        let tag = (encoded & 0b1111_1111) as u8;
191
192        // A felt with bits set above the documented layout does not encode a hint, so it must not
193        // truncate into one - that would lose those bits on re-encoding.
194        u32::try_from(encoded >> 8)
195            .ok()
196            .and_then(|payload| Self::from_parts(tag, payload).ok())
197            .unwrap_or(NoteExecutionHint::Unknown(value))
198    }
199}
200
201// TESTS
202// ================================================================================================
203
204#[cfg(test)]
205mod tests {
206
207    use super::*;
208
209    fn assert_hint_serde(note_execution_hint: NoteExecutionHint) {
210        let (tag, payload) = note_execution_hint.into_parts().unwrap();
211        let deserialized = NoteExecutionHint::from_parts(tag, payload).unwrap();
212        assert_eq!(deserialized, note_execution_hint);
213    }
214
215    #[test]
216    fn test_serialization_round_trip() {
217        assert_hint_serde(NoteExecutionHint::None);
218        assert_hint_serde(NoteExecutionHint::Always);
219        assert_hint_serde(NoteExecutionHint::after_block(15.into()));
220        assert_hint_serde(NoteExecutionHint::OnBlockSlot {
221            round_len: 9,
222            slot_len: 12,
223            slot_offset: 18,
224        });
225    }
226
227    #[test]
228    fn test_encode_round_trip() {
229        for hint in [
230            NoteExecutionHint::None,
231            NoteExecutionHint::Always,
232            NoteExecutionHint::after_block(15.into()),
233            NoteExecutionHint::OnBlockSlot {
234                round_len: 22,
235                slot_len: 33,
236                slot_offset: 44,
237            },
238        ] {
239            let encoded = Felt::from(hint);
240            assert_eq!(NoteExecutionHint::from(encoded), hint);
241        }
242
243        assert_eq!(Felt::from(NoteExecutionHint::always()).as_canonical_u64(), 1);
244    }
245
246    /// A felt that does not encode a recognized hint decodes as `Unknown`.
247    #[test]
248    fn unknown_hint_round_trip() {
249        // A tag above the highest known one, a non-zero payload on a tag that requires an empty
250        // one, a non-zero remainder on the `OnBlockSlot` payload, and a felt with bits set above
251        // the documented 40-bit layout.
252        for encoded in [7u64, (1 << 8) | 1, (1 << 32) | 3, 1 << 40] {
253            let encoded = Felt::new(encoded).unwrap();
254            let hint = NoteExecutionHint::from(encoded);
255
256            assert_eq!(hint, NoteExecutionHint::Unknown(encoded));
257            assert_eq!(hint.into_parts(), None);
258            assert_eq!(hint.can_be_consumed(100.into()), None);
259            assert_eq!(Felt::from(hint), encoded);
260        }
261    }
262
263    #[test]
264    fn test_can_be_consumed() {
265        let none = NoteExecutionHint::none();
266        assert!(none.can_be_consumed(100.into()).is_none());
267
268        let always = NoteExecutionHint::always();
269        assert!(always.can_be_consumed(100.into()).unwrap());
270
271        let after_block = NoteExecutionHint::after_block(12345.into());
272        assert!(!after_block.can_be_consumed(12344.into()).unwrap());
273        assert!(after_block.can_be_consumed(12345.into()).unwrap());
274
275        let on_block_slot = NoteExecutionHint::on_block_slot(10, 7, 1);
276        assert!(!on_block_slot.can_be_consumed(127.into()).unwrap()); // Block 127 is not in the slot 128..255
277        assert!(on_block_slot.can_be_consumed(128.into()).unwrap()); // Block 128 is in the slot 128..255
278        assert!(on_block_slot.can_be_consumed(255.into()).unwrap()); // Block 255 is in the slot 128..255
279        assert!(!on_block_slot.can_be_consumed(256.into()).unwrap()); // Block 256 is not in the slot 128..255
280        assert!(on_block_slot.can_be_consumed(1152.into()).unwrap()); // Block 1152 is in the slot 1152..1279
281        assert!(on_block_slot.can_be_consumed(1279.into()).unwrap()); // Block 1279 is in the slot 1152..1279
282        assert!(on_block_slot.can_be_consumed(2176.into()).unwrap()); // Block 2176 is in the slot 2176..2303
283        assert!(!on_block_slot.can_be_consumed(2175.into()).unwrap()); // Block 1279 is in the slot
284        // 2176..2303
285    }
286
287    #[test]
288    fn test_parts_validity() {
289        NoteExecutionHint::from_parts(NoteExecutionHint::NONE_TAG, 1).unwrap_err();
290        NoteExecutionHint::from_parts(NoteExecutionHint::ALWAYS_TAG, 12).unwrap_err();
291        // 4th byte should be blank for tag 3 (OnBlockSlot)
292        NoteExecutionHint::from_parts(NoteExecutionHint::ON_BLOCK_SLOT_TAG, 1 << 24).unwrap_err();
293        NoteExecutionHint::from_parts(NoteExecutionHint::ON_BLOCK_SLOT_TAG, 0).unwrap();
294
295        NoteExecutionHint::from_parts(10, 1).unwrap_err();
296    }
297}