Skip to main content

miden_processor/
execution_options.rs

1use miden_air::trace::MIN_TRACE_LEN;
2use miden_core::program::MIN_STACK_DEPTH;
3
4// EXECUTION OPTIONS
5// ================================================================================================
6
7/// A set of parameters specifying execution parameters of the VM.
8///
9/// - `max_cycles` specifies the maximum number of cycles a program is allowed to execute.
10/// - `expected_cycles` specifies the number of cycles a program is expected to execute.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct ExecutionOptions {
13    max_cycles: u32,
14    expected_cycles: u32,
15    core_trace_fragment_size: usize,
16    /// Maximum number of field elements that can be inserted into the advice map in a single
17    /// `adv.insert_mem` operation.
18    max_adv_map_value_size: usize,
19    /// Maximum total number of field elements allowed in live advice map keys and values.
20    max_adv_map_elements: usize,
21    /// Maximum number of input bytes allowed for a single hash precompile invocation.
22    max_hash_len_bytes: usize,
23    /// Maximum number of continuations allowed on the continuation stack at any point during
24    /// execution.
25    max_num_continuations: usize,
26    /// Maximum number of internal nodes allowed in the advice provider's Merkle store.
27    max_merkle_store_nodes: usize,
28    /// Maximum number of deferred precompile requests allowed during execution.
29    max_precompile_requests: usize,
30    /// Maximum total number of calldata bytes allowed across deferred precompile requests.
31    max_precompile_request_calldata_bytes: usize,
32    /// Maximum number of field elements allowed on the operand stack across the active execution
33    /// context and all suspended contexts.
34    ///
35    /// A `call`, `dyncall`, or `syscall` context switch hides the caller's operand-stack overflow
36    /// (everything below the top 16 elements) until the callee returns. This limit bounds the
37    /// aggregate of the active context's depth plus all such suspended overflow, so nesting
38    /// context switches cannot accumulate hidden operand-stack memory beyond the configured
39    /// budget.
40    max_stack_depth: usize,
41    /// Maximum number of field elements allowed in the processor's memory at any point during
42    /// execution, rounded up to the nearest multiple of 4.
43    max_memory_elements: usize,
44}
45
46impl Default for ExecutionOptions {
47    fn default() -> Self {
48        ExecutionOptions {
49            max_cycles: Self::MAX_CYCLES,
50            expected_cycles: MIN_TRACE_LEN as u32,
51            core_trace_fragment_size: Self::DEFAULT_CORE_TRACE_FRAGMENT_SIZE,
52            max_adv_map_value_size: Self::DEFAULT_MAX_ADV_MAP_VALUE_SIZE,
53            max_adv_map_elements: Self::DEFAULT_MAX_ADV_MAP_ELEMENTS,
54            max_hash_len_bytes: Self::DEFAULT_MAX_HASH_LEN_BYTES,
55            max_num_continuations: Self::DEFAULT_MAX_NUM_CONTINUATIONS,
56            max_merkle_store_nodes: Self::DEFAULT_MAX_MERKLE_STORE_NODES,
57            max_precompile_requests: Self::DEFAULT_MAX_PRECOMPILE_REQUESTS,
58            max_precompile_request_calldata_bytes:
59                Self::DEFAULT_MAX_PRECOMPILE_REQUEST_CALLDATA_BYTES,
60            max_stack_depth: Self::DEFAULT_MAX_STACK_DEPTH,
61            max_memory_elements: Self::DEFAULT_MAX_MEMORY_ELEMENTS,
62        }
63    }
64}
65
66impl ExecutionOptions {
67    // CONSTANTS
68    // --------------------------------------------------------------------------------------------
69
70    /// The maximum number of VM cycles a program is allowed to take.
71    pub const MAX_CYCLES: u32 = 1 << 29;
72
73    /// Default fragment size for core trace generation.
74    pub const DEFAULT_CORE_TRACE_FRAGMENT_SIZE: usize = 4096; // 2^12
75
76    /// Default maximum number of field elements in a single advice map value inserted via
77    /// execution-time advice map mutations. Set to 2^17 (~1 MB given 8-byte field elements).
78    pub const DEFAULT_MAX_ADV_MAP_VALUE_SIZE: usize = 1 << 17;
79
80    /// Default maximum total number of field elements in live advice map keys and values.
81    ///
82    /// Set to 2^20 so the default allows multiple maximum-sized entries while still providing a
83    /// finite host-memory backstop. Each entry contributes 4 key elements plus its value length.
84    pub const DEFAULT_MAX_ADV_MAP_ELEMENTS: usize = 1 << 20;
85
86    /// Default maximum number of input bytes for a single hash precompile invocation (e.g.
87    /// keccak256, sha512, etc.). Set to 2^20 (1 MB).
88    pub const DEFAULT_MAX_HASH_LEN_BYTES: usize = 1 << 20;
89
90    /// Default maximum number of continuations allowed on the continuation stack.
91    /// Set to 2^16 (65536).
92    pub const DEFAULT_MAX_NUM_CONTINUATIONS: usize = 1 << 16;
93
94    /// Default maximum number of internal nodes allowed in the advice provider's Merkle store.
95    ///
96    /// Set to 2^20 so the default allows large Merkle inputs and repeated updates while still
97    /// providing a finite host-memory backstop.
98    pub const DEFAULT_MAX_MERKLE_STORE_NODES: usize = 1 << 20;
99
100    /// Default maximum number of deferred precompile requests allowed during execution.
101    /// Set to 2^16 (65536).
102    pub const DEFAULT_MAX_PRECOMPILE_REQUESTS: usize = 1 << 16;
103
104    /// Default maximum total calldata bytes allowed across deferred precompile requests.
105    /// Set to 2^28 (256 MB).
106    pub const DEFAULT_MAX_PRECOMPILE_REQUEST_CALLDATA_BYTES: usize = 1 << 28;
107
108    /// Default maximum number of field elements allowed on the operand stack.
109    ///
110    /// This preserves the effective stack depth ceiling imposed by the previous fixed
111    /// `FastProcessor` stack buffer.
112    pub const DEFAULT_MAX_STACK_DEPTH: usize = 6615;
113
114    /// Default maximum number of field elements allowed in the processor's memory.
115    ///
116    /// Memory is element-addressable, so this bounds the total number of elements live across all
117    /// contexts. Internally memory is stored at word granularity (4 elements per word), so the
118    /// effective limit is rounded up to a whole number of words. Set to 2^28, which lets programs
119    /// use a large amount of memory while still providing a finite host-memory backstop against
120    /// unbounded growth from writes to arbitrarily many unique addresses.
121    pub const DEFAULT_MAX_MEMORY_ELEMENTS: usize = 1 << 28;
122
123    // CONSTRUCTOR
124    // --------------------------------------------------------------------------------------------
125
126    /// Creates a new instance of [ExecutionOptions] from the specified parameters.
127    ///
128    /// If the `max_cycles` is `None` the maximum number of cycles will be set to 2^29.
129    ///
130    /// # Errors
131    /// Returns an error if:
132    /// - `max_cycles` is outside the valid range
133    /// - after rounding up to the next power of two, `expected_cycles` exceeds `max_cycles`
134    /// - `core_trace_fragment_size` is zero
135    pub fn new(
136        max_cycles: Option<u32>,
137        expected_cycles: u32,
138        core_trace_fragment_size: usize,
139    ) -> Result<Self, ExecutionOptionsError> {
140        // Validate max cycles.
141        let max_cycles = if let Some(max_cycles) = max_cycles {
142            if max_cycles > Self::MAX_CYCLES {
143                return Err(ExecutionOptionsError::MaxCycleNumTooBig {
144                    max_cycles,
145                    max_cycles_limit: Self::MAX_CYCLES,
146                });
147            }
148            if max_cycles < MIN_TRACE_LEN as u32 {
149                return Err(ExecutionOptionsError::MaxCycleNumTooSmall {
150                    max_cycles,
151                    min_cycles_limit: MIN_TRACE_LEN,
152                });
153            }
154            max_cycles
155        } else {
156            Self::MAX_CYCLES
157        };
158        // Round up the expected number of cycles to the next power of two. If it is smaller than
159        // MIN_TRACE_LEN -- pad expected number to it.
160        let expected_cycles = expected_cycles.next_power_of_two().max(MIN_TRACE_LEN as u32);
161        // Validate expected cycles (after rounding) against max_cycles.
162        if max_cycles < expected_cycles {
163            return Err(ExecutionOptionsError::ExpectedCyclesTooBig {
164                max_cycles,
165                expected_cycles,
166            });
167        }
168
169        // Validate core trace fragment size.
170        if core_trace_fragment_size == 0 {
171            return Err(ExecutionOptionsError::CoreTraceFragmentSizeTooSmall);
172        }
173
174        Ok(ExecutionOptions {
175            max_cycles,
176            expected_cycles,
177            core_trace_fragment_size,
178            max_adv_map_value_size: Self::DEFAULT_MAX_ADV_MAP_VALUE_SIZE,
179            max_adv_map_elements: Self::DEFAULT_MAX_ADV_MAP_ELEMENTS,
180            max_hash_len_bytes: Self::DEFAULT_MAX_HASH_LEN_BYTES,
181            max_num_continuations: Self::DEFAULT_MAX_NUM_CONTINUATIONS,
182            max_merkle_store_nodes: Self::DEFAULT_MAX_MERKLE_STORE_NODES,
183            max_precompile_requests: Self::DEFAULT_MAX_PRECOMPILE_REQUESTS,
184            max_precompile_request_calldata_bytes:
185                Self::DEFAULT_MAX_PRECOMPILE_REQUEST_CALLDATA_BYTES,
186            max_stack_depth: Self::DEFAULT_MAX_STACK_DEPTH,
187            max_memory_elements: Self::DEFAULT_MAX_MEMORY_ELEMENTS,
188        })
189    }
190
191    /// Sets the fragment size for core trace generation.
192    ///
193    /// Returns an error if the size is zero.
194    pub fn with_core_trace_fragment_size(
195        mut self,
196        size: usize,
197    ) -> Result<Self, ExecutionOptionsError> {
198        if size == 0 {
199            return Err(ExecutionOptionsError::CoreTraceFragmentSizeTooSmall);
200        }
201        self.core_trace_fragment_size = size;
202        Ok(self)
203    }
204
205    // PUBLIC ACCESSORS
206    // --------------------------------------------------------------------------------------------
207
208    /// Returns maximum number of cycles a program is allowed to execute for.
209    #[inline(always)]
210    pub fn max_cycles(&self) -> u32 {
211        self.max_cycles
212    }
213
214    /// Returns the number of cycles a program is expected to take.
215    ///
216    /// This will serve as a hint to the VM for how much memory to allocate for a program's
217    /// execution trace and may result in performance improvements when the number of expected
218    /// cycles is equal to the number of actual cycles.
219    pub fn expected_cycles(&self) -> u32 {
220        self.expected_cycles
221    }
222
223    /// Returns the fragment size for core trace generation.
224    pub fn core_trace_fragment_size(&self) -> usize {
225        self.core_trace_fragment_size
226    }
227
228    /// Returns the maximum number of field elements allowed in a single live advice map value.
229    #[inline]
230    pub fn max_adv_map_value_size(&self) -> usize {
231        self.max_adv_map_value_size
232    }
233
234    /// Returns the maximum total number of field elements allowed in live advice map keys and
235    /// values.
236    #[inline]
237    pub fn max_adv_map_elements(&self) -> usize {
238        self.max_adv_map_elements
239    }
240
241    /// Returns the maximum number of input bytes allowed for a single hash precompile invocation.
242    #[inline]
243    pub fn max_hash_len_bytes(&self) -> usize {
244        self.max_hash_len_bytes
245    }
246
247    /// Sets the maximum number of field elements allowed in a single live advice map value.
248    pub fn with_max_adv_map_value_size(mut self, size: usize) -> Self {
249        self.max_adv_map_value_size = size;
250        self
251    }
252
253    /// Sets the maximum total number of field elements allowed in live advice map keys and values.
254    pub fn with_max_adv_map_elements(mut self, size: usize) -> Self {
255        self.max_adv_map_elements = size;
256        self
257    }
258
259    /// Sets the maximum number of input bytes allowed for a single hash precompile invocation.
260    pub fn with_max_hash_len_bytes(mut self, size: usize) -> Self {
261        self.max_hash_len_bytes = size;
262        self
263    }
264
265    /// Returns the maximum number of continuations allowed on the continuation stack.
266    #[inline]
267    pub fn max_num_continuations(&self) -> usize {
268        self.max_num_continuations
269    }
270
271    /// Returns the maximum number of internal nodes allowed in the advice provider's Merkle store.
272    #[inline]
273    pub fn max_merkle_store_nodes(&self) -> usize {
274        self.max_merkle_store_nodes
275    }
276
277    /// Returns the maximum number of deferred precompile requests allowed during execution.
278    #[inline]
279    pub fn max_precompile_requests(&self) -> usize {
280        self.max_precompile_requests
281    }
282
283    /// Returns the maximum total calldata bytes allowed across deferred precompile requests.
284    #[inline]
285    pub fn max_precompile_request_calldata_bytes(&self) -> usize {
286        self.max_precompile_request_calldata_bytes
287    }
288
289    /// Returns the maximum number of field elements allowed on the operand stack across the active
290    /// execution context and all suspended contexts.
291    #[inline]
292    pub fn max_stack_depth(&self) -> usize {
293        self.max_stack_depth
294    }
295
296    /// Returns the configured maximum number of field elements allowed in the processor's memory.
297    ///
298    /// This is the raw value as set via [`Self::with_max_memory_elements`]; the effective cap is
299    /// rounded up to a whole number of words (a multiple of 4) when memory is initialized.
300    #[inline]
301    pub fn max_memory_elements(&self) -> usize {
302        self.max_memory_elements
303    }
304
305    /// Sets the maximum number of continuations allowed on the continuation stack.
306    pub fn with_max_num_continuations(mut self, max_num_continuations: usize) -> Self {
307        self.max_num_continuations = max_num_continuations;
308        self
309    }
310
311    /// Sets the maximum number of internal nodes allowed in the advice provider's Merkle store.
312    pub fn with_max_merkle_store_nodes(mut self, max_merkle_store_nodes: usize) -> Self {
313        self.max_merkle_store_nodes = max_merkle_store_nodes;
314        self
315    }
316
317    /// Sets the maximum number of deferred precompile requests allowed during execution.
318    pub fn with_max_precompile_requests(mut self, max_precompile_requests: usize) -> Self {
319        self.max_precompile_requests = max_precompile_requests;
320        self
321    }
322
323    /// Sets the maximum total calldata bytes allowed across deferred precompile requests.
324    pub fn with_max_precompile_request_calldata_bytes(
325        mut self,
326        max_precompile_request_calldata_bytes: usize,
327    ) -> Self {
328        self.max_precompile_request_calldata_bytes = max_precompile_request_calldata_bytes;
329        self
330    }
331
332    /// Sets the maximum number of field elements allowed on the operand stack across the active
333    /// execution context and all suspended contexts.
334    pub fn with_max_stack_depth(
335        mut self,
336        max_stack_depth: usize,
337    ) -> Result<Self, ExecutionOptionsError> {
338        if max_stack_depth < MIN_STACK_DEPTH {
339            return Err(ExecutionOptionsError::MaxStackDepthTooSmall {
340                max_stack_depth,
341                min_stack_depth: MIN_STACK_DEPTH,
342            });
343        }
344        self.max_stack_depth = max_stack_depth;
345        Ok(self)
346    }
347
348    /// Sets the maximum number of field elements allowed in the processor's memory.
349    pub fn with_max_memory_elements(mut self, max_memory_elements: usize) -> Self {
350        self.max_memory_elements = max_memory_elements;
351        self
352    }
353}
354
355// EXECUTION OPTIONS ERROR
356// ================================================================================================
357
358#[derive(Debug, thiserror::Error)]
359pub enum ExecutionOptionsError {
360    #[error(
361        "expected number of cycles {expected_cycles} must be smaller than the maximum number of cycles {max_cycles}"
362    )]
363    ExpectedCyclesTooBig { max_cycles: u32, expected_cycles: u32 },
364    #[error("maximum number of cycles {max_cycles} must be greater than {min_cycles_limit}")]
365    MaxCycleNumTooSmall { max_cycles: u32, min_cycles_limit: usize },
366    #[error("maximum number of cycles {max_cycles} must be less than {max_cycles_limit}")]
367    MaxCycleNumTooBig { max_cycles: u32, max_cycles_limit: u32 },
368    #[error("core trace fragment size must be greater than 0")]
369    CoreTraceFragmentSizeTooSmall,
370    #[error("maximum stack depth {max_stack_depth} must be at least {min_stack_depth}")]
371    MaxStackDepthTooSmall {
372        max_stack_depth: usize,
373        min_stack_depth: usize,
374    },
375}
376
377// TESTS
378// ================================================================================================
379
380#[cfg(test)]
381mod tests {
382    use super::*;
383
384    #[test]
385    fn valid_fragment_size() {
386        // Valid power of two values should succeed
387        let opts = ExecutionOptions::new(None, 64, 1024);
388        assert!(opts.is_ok());
389        assert_eq!(opts.unwrap().core_trace_fragment_size(), 1024);
390
391        let opts = ExecutionOptions::new(None, 64, 4096);
392        assert!(opts.is_ok());
393
394        let opts = ExecutionOptions::new(None, 64, 1);
395        assert!(opts.is_ok());
396    }
397
398    #[test]
399    fn zero_fragment_size_fails() {
400        let opts = ExecutionOptions::new(None, 64, 0);
401        assert!(matches!(opts, Err(ExecutionOptionsError::CoreTraceFragmentSizeTooSmall)));
402    }
403
404    #[test]
405    fn with_core_trace_fragment_size_validates() {
406        // Valid size should succeed
407        let result = ExecutionOptions::default().with_core_trace_fragment_size(2048);
408        assert!(result.is_ok());
409        assert_eq!(result.unwrap().core_trace_fragment_size(), 2048);
410
411        // Zero should fail
412        let result = ExecutionOptions::default().with_core_trace_fragment_size(0);
413        assert!(matches!(result, Err(ExecutionOptionsError::CoreTraceFragmentSizeTooSmall)));
414    }
415
416    #[test]
417    fn expected_cycles_validated_after_rounding() {
418        // expected_cycles=65 rounds to 128; max_cycles=100 -> must fail (128 > 100).
419        let opts = ExecutionOptions::new(Some(100), 65, 1024);
420        assert!(matches!(
421            opts,
422            Err(ExecutionOptionsError::ExpectedCyclesTooBig {
423                max_cycles: 100,
424                expected_cycles: 128
425            })
426        ));
427
428        // expected_cycles=64 rounds to 64; max_cycles=100 -> ok.
429        let opts = ExecutionOptions::new(Some(100), 64, 1024);
430        assert!(opts.is_ok());
431        assert_eq!(opts.unwrap().expected_cycles(), 64);
432    }
433
434    #[test]
435    fn max_stack_depth_validates_minimum_depth() {
436        let result = ExecutionOptions::default().with_max_stack_depth(MIN_STACK_DEPTH - 1);
437        assert!(matches!(
438            result,
439            Err(ExecutionOptionsError::MaxStackDepthTooSmall {
440                max_stack_depth,
441                min_stack_depth: MIN_STACK_DEPTH,
442            }) if max_stack_depth == MIN_STACK_DEPTH - 1
443        ));
444
445        let result = ExecutionOptions::default().with_max_stack_depth(MIN_STACK_DEPTH);
446        assert!(result.is_ok());
447        assert_eq!(result.unwrap().max_stack_depth(), MIN_STACK_DEPTH);
448    }
449}