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}