Skip to main content

miden_processor/
execution_options.rs

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