openvm_circuit/arch/
config.rs

1use std::{
2    fs::File,
3    io::{self, Write},
4    path::Path,
5};
6
7use derive_new::new;
8use getset::{Setters, WithSetters};
9use openvm_instructions::{
10    riscv::{RV32_IMM_AS, RV32_MEMORY_AS, RV32_REGISTER_AS},
11    NATIVE_AS,
12};
13use openvm_poseidon2_air::Poseidon2Config;
14use openvm_stark_backend::{
15    config::{StarkGenericConfig, Val},
16    engine::StarkEngine,
17    p3_field::Field,
18    p3_util::log2_strict_usize,
19};
20use serde::{de::DeserializeOwned, Deserialize, Serialize};
21
22use super::{AnyEnum, VmChipComplex, PUBLIC_VALUES_AIR_ID};
23use crate::{
24    arch::{
25        execution_mode::metered::segment_ctx::SegmentationLimits, AirInventory, AirInventoryError,
26        Arena, ChipInventoryError, ExecutorInventory, ExecutorInventoryError,
27    },
28    system::{
29        memory::{
30            merkle::public_values::PUBLIC_VALUES_AS, num_memory_airs, CHUNK, POINTER_MAX_BITS,
31        },
32        SystemChipComplex,
33    },
34};
35
36// sbox is decomposed to have this max degree for Poseidon2. We set to 3 so quotient_degree = 2
37// allows log_blowup = 1
38const DEFAULT_POSEIDON2_MAX_CONSTRAINT_DEGREE: usize = 3;
39pub const DEFAULT_MAX_NUM_PUBLIC_VALUES: usize = 32;
40/// Width of Poseidon2 VM uses.
41pub const POSEIDON2_WIDTH: usize = 16;
42/// Offset for address space indices. This is used to distinguish between different memory spaces.
43pub const ADDR_SPACE_OFFSET: u32 = 1;
44/// Returns a Poseidon2 config for the VM.
45pub fn vm_poseidon2_config<F: Field>() -> Poseidon2Config<F> {
46    Poseidon2Config::default()
47}
48
49/// A VM configuration is the minimum serializable format to be able to create the execution
50/// environment and circuit for a zkVM supporting a fixed set of instructions.
51/// This trait contains the sub-traits [VmExecutionConfig] and [VmCircuitConfig].
52/// The [InitFileGenerator] sub-trait provides custom build hooks to generate code for initializing
53/// some VM extensions. The `VmConfig` is expected to contain the [SystemConfig] internally.
54///
55/// For users who only need to create an execution environment, use the sub-trait
56/// [VmExecutionConfig] to avoid the `SC` generic.
57///
58/// This trait does not contain the [VmBuilder] trait, because a single VM configuration may
59/// implement multiple [VmBuilder]s for different prover backends.
60pub trait VmConfig<SC>:
61    Clone
62    + Serialize
63    + DeserializeOwned
64    + InitFileGenerator
65    + VmExecutionConfig<Val<SC>>
66    + VmCircuitConfig<SC>
67    + AsRef<SystemConfig>
68    + AsMut<SystemConfig>
69where
70    SC: StarkGenericConfig,
71{
72}
73
74pub trait VmExecutionConfig<F> {
75    type Executor: AnyEnum + Send + Sync;
76
77    fn create_executors(&self)
78        -> Result<ExecutorInventory<Self::Executor>, ExecutorInventoryError>;
79}
80
81pub trait VmCircuitConfig<SC: StarkGenericConfig> {
82    fn create_airs(&self) -> Result<AirInventory<SC>, AirInventoryError>;
83}
84
85/// This trait is intended to be implemented on a new type wrapper of the VmConfig struct to get
86/// around Rust orphan rules.
87pub trait VmBuilder<E: StarkEngine>: Sized {
88    type VmConfig: VmConfig<E::SC>;
89    type RecordArena: Arena;
90    type SystemChipInventory: SystemChipComplex<Self::RecordArena, E::PB>;
91
92    /// Create a [VmChipComplex] from the full [AirInventory], which should be the output of
93    /// [VmCircuitConfig::create_airs].
94    #[allow(clippy::type_complexity)]
95    fn create_chip_complex(
96        &self,
97        config: &Self::VmConfig,
98        circuit: AirInventory<E::SC>,
99    ) -> Result<
100        VmChipComplex<E::SC, Self::RecordArena, E::PB, Self::SystemChipInventory>,
101        ChipInventoryError,
102    >;
103}
104
105impl<SC, VC> VmConfig<SC> for VC
106where
107    SC: StarkGenericConfig,
108    VC: Clone
109        + Serialize
110        + DeserializeOwned
111        + InitFileGenerator
112        + VmExecutionConfig<Val<SC>>
113        + VmCircuitConfig<SC>
114        + AsRef<SystemConfig>
115        + AsMut<SystemConfig>,
116{
117}
118
119pub const OPENVM_DEFAULT_INIT_FILE_BASENAME: &str = "openvm_init";
120pub const OPENVM_DEFAULT_INIT_FILE_NAME: &str = "openvm_init.rs";
121/// The minimum block size is 4, but RISC-V `lb` only requires alignment of 1 and `lh` only requires
122/// alignment of 2 because the instructions are implemented by doing an access of block size 4.
123const DEFAULT_U8_BLOCK_SIZE: usize = 4;
124const DEFAULT_NATIVE_BLOCK_SIZE: usize = 1;
125
126/// Trait for generating a init.rs file that contains a call to moduli_init!,
127/// complex_init!, sw_init! with the supported moduli and curves.
128/// Should be implemented by all VM config structs.
129pub trait InitFileGenerator {
130    // Default implementation is no init file.
131    fn generate_init_file_contents(&self) -> Option<String> {
132        None
133    }
134
135    // Do not override this method's default implementation.
136    // This method is called by cargo openvm and the SDK before building the guest package.
137    fn write_to_init_file(
138        &self,
139        manifest_dir: &Path,
140        init_file_name: Option<&str>,
141    ) -> io::Result<()> {
142        if let Some(contents) = self.generate_init_file_contents() {
143            let dest_path = Path::new(manifest_dir)
144                .join(init_file_name.unwrap_or(OPENVM_DEFAULT_INIT_FILE_NAME));
145            let mut f = File::create(&dest_path)?;
146            write!(f, "{}", contents)?;
147        }
148        Ok(())
149    }
150}
151
152/// Each address space in guest memory may be configured with a different type `T` to represent a
153/// memory cell in the address space. On host, the address space will be mapped to linear host
154/// memory in bytes. The type `T` must be plain old data (POD) and be safely transmutable from a
155/// fixed size array of bytes. Moreover, each type `T` must be convertible to a field element `F`.
156///
157/// We currently implement this trait on the enum [MemoryCellType], which includes all cell types
158/// that we expect to be used in the VM context.
159pub trait AddressSpaceHostLayout {
160    /// Size in bytes of the memory cell type.
161    fn size(&self) -> usize;
162
163    /// # Safety
164    /// - This function must only be called when `value` is guaranteed to be of size `self.size()`.
165    /// - Alignment of `value` must be a multiple of the alignment of `F`.
166    /// - The field type `F` must be plain old data.
167    unsafe fn to_field<F: Field>(&self, value: &[u8]) -> F;
168}
169
170#[derive(Debug, Serialize, Deserialize, Clone, new)]
171pub struct MemoryConfig {
172    /// The maximum height of the address space. This means the trie has `addr_space_height` layers
173    /// for searching the address space. The allowed address spaces are those in the range `[1,
174    /// 1 + 2^addr_space_height)` where it starts from 1 to not allow address space 0 in memory.
175    pub addr_space_height: usize,
176    /// It is expected that the size of the list is `(1 << addr_space_height) + 1` and the first
177    /// element is 0, which means no address space.
178    pub addr_spaces: Vec<AddressSpaceHostConfig>,
179    pub pointer_max_bits: usize,
180    /// All timestamps must be in the range `[0, 2^timestamp_max_bits)`. Maximum allowed: 29.
181    pub timestamp_max_bits: usize,
182    /// Limb size used by the range checker
183    pub decomp: usize,
184    /// Maximum N AccessAdapter AIR to support.
185    pub max_access_adapter_n: usize,
186}
187
188impl Default for MemoryConfig {
189    fn default() -> Self {
190        let mut addr_spaces =
191            Self::empty_address_space_configs((1 << 3) + ADDR_SPACE_OFFSET as usize);
192        const MAX_CELLS: usize = 1 << 29;
193        addr_spaces[RV32_REGISTER_AS as usize].num_cells = 32 * size_of::<u32>();
194        addr_spaces[RV32_MEMORY_AS as usize].num_cells = MAX_CELLS;
195        addr_spaces[PUBLIC_VALUES_AS as usize].num_cells = DEFAULT_MAX_NUM_PUBLIC_VALUES;
196        addr_spaces[NATIVE_AS as usize].num_cells = MAX_CELLS;
197        Self::new(3, addr_spaces, POINTER_MAX_BITS, 29, 17, 32)
198    }
199}
200
201impl MemoryConfig {
202    pub fn empty_address_space_configs(num_addr_spaces: usize) -> Vec<AddressSpaceHostConfig> {
203        // All except address spaces 0..4 default to native 32-bit field.
204        // By default only address spaces 1..=4 have non-empty cell counts.
205        let mut addr_spaces = vec![
206            AddressSpaceHostConfig::new(
207                0,
208                DEFAULT_NATIVE_BLOCK_SIZE,
209                MemoryCellType::native32()
210            );
211            num_addr_spaces
212        ];
213        addr_spaces[RV32_IMM_AS as usize] = AddressSpaceHostConfig::new(0, 1, MemoryCellType::Null);
214        addr_spaces[RV32_REGISTER_AS as usize] =
215            AddressSpaceHostConfig::new(0, DEFAULT_U8_BLOCK_SIZE, MemoryCellType::U8);
216
217        #[cfg(feature = "legacy-v1-3-mem-align")]
218        {
219            addr_spaces[RV32_MEMORY_AS as usize] =
220                AddressSpaceHostConfig::new(0, 1, MemoryCellType::U8);
221        }
222        #[cfg(not(feature = "legacy-v1-3-mem-align"))]
223        {
224            addr_spaces[RV32_MEMORY_AS as usize] =
225                AddressSpaceHostConfig::new(0, DEFAULT_U8_BLOCK_SIZE, MemoryCellType::U8);
226        }
227
228        addr_spaces[PUBLIC_VALUES_AS as usize] =
229            AddressSpaceHostConfig::new(0, DEFAULT_U8_BLOCK_SIZE, MemoryCellType::U8);
230
231        addr_spaces
232    }
233
234    /// Config for aggregation usage with only native address space.
235    pub fn aggregation() -> Self {
236        let mut addr_spaces =
237            Self::empty_address_space_configs((1 << 3) + ADDR_SPACE_OFFSET as usize);
238        addr_spaces[NATIVE_AS as usize].num_cells = 1 << 29;
239        Self::new(3, addr_spaces, POINTER_MAX_BITS, 29, 17, 8)
240    }
241
242    pub fn min_block_size_bits(&self) -> Vec<u8> {
243        self.addr_spaces
244            .iter()
245            .map(|addr_sp| log2_strict_usize(addr_sp.min_block_size) as u8)
246            .collect()
247    }
248}
249
250/// System-level configuration for the virtual machine. Contains all configuration parameters that
251/// are managed by the architecture, including configuration for continuations support.
252#[derive(Debug, Clone, Serialize, Deserialize, Setters, WithSetters)]
253pub struct SystemConfig {
254    /// The maximum constraint degree any chip is allowed to use.
255    #[getset(set_with = "pub")]
256    pub max_constraint_degree: usize,
257    /// True if the VM is in continuation mode. In this mode, an execution could be segmented and
258    /// each segment is proved by a proof. Each proof commits the before and after state of the
259    /// corresponding segment.
260    /// False if the VM is in single segment mode. In this mode, an execution is proved by a single
261    /// proof.
262    pub continuation_enabled: bool,
263    /// Memory configuration
264    pub memory_config: MemoryConfig,
265    /// `num_public_values` has different meanings in single segment mode and continuation mode.
266    /// In single segment mode, `num_public_values` is the number of public values of
267    /// `PublicValuesChip`. In this case, verifier can read public values directly.
268    /// In continuation mode, public values are stored in a special address space.
269    /// `num_public_values` indicates the number of allowed addresses in that address space. The
270    /// verifier cannot read public values directly, but they can decommit the public values
271    /// from the memory merkle root.
272    pub num_public_values: usize,
273    /// Whether to collect detailed profiling metrics.
274    /// **Warning**: this slows down the runtime.
275    pub profiling: bool,
276    /// Segmentation limits
277    /// This field is skipped in serde as it's only used in execution and
278    /// not needed after any serialize/deserialize.
279    #[serde(skip, default = "SegmentationLimits::default")]
280    #[getset(set = "pub")]
281    pub segmentation_limits: SegmentationLimits,
282}
283
284impl SystemConfig {
285    pub fn new(
286        max_constraint_degree: usize,
287        mut memory_config: MemoryConfig,
288        num_public_values: usize,
289    ) -> Self {
290        assert!(
291            memory_config.timestamp_max_bits <= 29,
292            "Timestamp max bits must be <= 29 for LessThan to work in 31-bit field"
293        );
294        memory_config.addr_spaces[PUBLIC_VALUES_AS as usize].num_cells = num_public_values;
295        Self {
296            max_constraint_degree,
297            continuation_enabled: true,
298            memory_config,
299            num_public_values,
300            profiling: false,
301            segmentation_limits: SegmentationLimits::default(),
302        }
303    }
304
305    pub fn default_from_memory(memory_config: MemoryConfig) -> Self {
306        Self::new(
307            DEFAULT_POSEIDON2_MAX_CONSTRAINT_DEGREE,
308            memory_config,
309            DEFAULT_MAX_NUM_PUBLIC_VALUES,
310        )
311    }
312
313    pub fn with_continuations(mut self) -> Self {
314        self.continuation_enabled = true;
315        self
316    }
317
318    pub fn without_continuations(mut self) -> Self {
319        self.continuation_enabled = false;
320        self
321    }
322
323    pub fn with_public_values(mut self, num_public_values: usize) -> Self {
324        self.num_public_values = num_public_values;
325        self.memory_config.addr_spaces[PUBLIC_VALUES_AS as usize].num_cells = num_public_values;
326        self
327    }
328
329    pub fn with_max_segment_len(mut self, max_segment_len: usize) -> Self {
330        self.segmentation_limits.max_trace_height = max_segment_len as u32;
331        self
332    }
333
334    pub fn with_profiling(mut self) -> Self {
335        self.profiling = true;
336        self
337    }
338
339    pub fn without_profiling(mut self) -> Self {
340        self.profiling = false;
341        self
342    }
343
344    pub fn has_public_values_chip(&self) -> bool {
345        !self.continuation_enabled && self.num_public_values > 0
346    }
347
348    /// Returns the AIR ID of the memory boundary AIR. Panic if the boundary AIR is not enabled.
349    pub fn memory_boundary_air_id(&self) -> usize {
350        PUBLIC_VALUES_AIR_ID + usize::from(self.has_public_values_chip())
351    }
352
353    /// Returns the AIR ID of the memory merkle AIR. Returns None if continuations are not enabled.
354    pub fn memory_merkle_air_id(&self) -> Option<usize> {
355        let boundary_idx = self.memory_boundary_air_id();
356        if self.continuation_enabled {
357            Some(boundary_idx + 1)
358        } else {
359            None
360        }
361    }
362
363    /// AIR ID for the first memory access adapter AIR.
364    pub fn access_adapter_air_id_offset(&self) -> usize {
365        let boundary_idx = self.memory_boundary_air_id();
366        // boundary, (if persistent memory) merkle AIRs
367        boundary_idx + 1 + usize::from(self.continuation_enabled)
368    }
369
370    /// This is O(1) and returns the length of
371    /// [`SystemAirInventory::into_airs`](crate::system::SystemAirInventory::into_airs).
372    pub fn num_airs(&self) -> usize {
373        self.memory_boundary_air_id()
374            + num_memory_airs(
375                self.continuation_enabled,
376                self.memory_config.max_access_adapter_n,
377            )
378    }
379
380    pub fn initial_block_size(&self) -> usize {
381        match self.continuation_enabled {
382            true => CHUNK,
383            false => 1,
384        }
385    }
386}
387
388impl Default for SystemConfig {
389    fn default() -> Self {
390        Self::default_from_memory(MemoryConfig::default())
391    }
392}
393
394impl AsRef<SystemConfig> for SystemConfig {
395    fn as_ref(&self) -> &SystemConfig {
396        self
397    }
398}
399
400impl AsMut<SystemConfig> for SystemConfig {
401    fn as_mut(&mut self) -> &mut SystemConfig {
402        self
403    }
404}
405
406// Default implementation uses no init file
407impl InitFileGenerator for SystemConfig {}
408
409#[derive(Debug, Serialize, Deserialize, Clone, Copy, new)]
410pub struct AddressSpaceHostConfig {
411    /// The number of memory cells in each address space, where a memory cell refers to a single
412    /// addressable unit of memory as defined by the ISA.
413    pub num_cells: usize,
414    /// Minimum block size for memory accesses supported. This is a property of the address space
415    /// that is determined by the ISA.
416    ///
417    /// **Note**: Block size is in terms of memory cells.
418    pub min_block_size: usize,
419    pub layout: MemoryCellType,
420}
421
422impl AddressSpaceHostConfig {
423    /// The total size in bytes of the address space in a linear memory layout.
424    pub fn size(&self) -> usize {
425        self.num_cells * self.layout.size()
426    }
427}
428
429pub(crate) const MAX_CELL_BYTE_SIZE: usize = 8;
430
431#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
432pub enum MemoryCellType {
433    Null,
434    U8,
435    U16,
436    /// Represented in little-endian format.
437    U32,
438    /// `size` is the size in bytes of the native field type. This should not exceed 8.
439    Native {
440        size: u8,
441    },
442}
443
444impl MemoryCellType {
445    pub fn native32() -> Self {
446        Self::Native {
447            size: size_of::<u32>() as u8,
448        }
449    }
450}
451
452impl AddressSpaceHostLayout for MemoryCellType {
453    fn size(&self) -> usize {
454        match self {
455            Self::Null => 1, // to avoid divide by zero
456            Self::U8 => size_of::<u8>(),
457            Self::U16 => size_of::<u16>(),
458            Self::U32 => size_of::<u32>(),
459            Self::Native { size } => *size as usize,
460        }
461    }
462
463    /// # Safety
464    /// - This function must only be called when `value` is guaranteed to be of size `self.size()`.
465    /// - Alignment of `value` must be a multiple of the alignment of `F`.
466    /// - The field type `F` must be plain old data.
467    ///
468    /// # Panics
469    /// If the value is of integer type and overflows the field.
470    unsafe fn to_field<F: Field>(&self, value: &[u8]) -> F {
471        match self {
472            Self::Null => unreachable!(),
473            Self::U8 => F::from_canonical_u8(*value.get_unchecked(0)),
474            Self::U16 => F::from_canonical_u16(core::ptr::read(value.as_ptr() as *const u16)),
475            Self::U32 => F::from_canonical_u32(core::ptr::read(value.as_ptr() as *const u32)),
476            Self::Native { .. } => core::ptr::read(value.as_ptr() as *const F),
477        }
478    }
479}