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    DEFERRAL_AS,
12};
13use openvm_poseidon2_air::Poseidon2Config;
14use openvm_stark_backend::{
15    p3_field::Field, EngineDeviceCtx, StarkEngine, StarkProtocolConfig, Val,
16};
17use serde::{de::DeserializeOwned, Deserialize, Serialize};
18
19use super::{AnyEnum, VmChipComplex, BOUNDARY_AIR_ID, CONNECTOR_AIR_ID, PROGRAM_AIR_ID};
20use crate::{
21    arch::{
22        execution_mode::metered::segment_ctx::DEFAULT_MAX_MEMORY, AirInventory, AirInventoryError,
23        Arena, ChipInventoryError, ExecutorInventory, ExecutorInventoryError,
24    },
25    system::{
26        memory::{merkle::public_values::PUBLIC_VALUES_AS, num_memory_airs, POINTER_MAX_BITS},
27        SystemChipComplex,
28    },
29};
30
31// sbox is decomposed to have this max degree for Poseidon2. We set to 3 so quotient_degree = 2
32// allows log_blowup = 1
33const DEFAULT_POSEIDON2_MAX_CONSTRAINT_DEGREE: usize = 3;
34pub const DEFAULT_MAX_NUM_PUBLIC_VALUES: usize = 32;
35/// Max number of deferral address space cells
36pub const DEFAULT_DEFERRAL_ADDR_SPACE_CELLS: usize = 1 << 14;
37/// Width of Poseidon2 VM uses.
38pub const POSEIDON2_WIDTH: usize = 16;
39/// Offset for address space indices. This is used to distinguish between different memory spaces.
40pub const ADDR_SPACE_OFFSET: u32 = 1;
41
42fn default_segmentation_max_memory() -> usize {
43    DEFAULT_MAX_MEMORY
44}
45/// Returns a Poseidon2 config for the VM.
46pub fn vm_poseidon2_config<F: Field>() -> Poseidon2Config<F> {
47    Poseidon2Config::default()
48}
49
50/// A VM configuration is the minimum serializable format to be able to create the execution
51/// environment and circuit for a zkVM supporting a fixed set of instructions.
52/// This trait contains the sub-traits [VmExecutionConfig] and [VmCircuitConfig].
53/// The [InitFileGenerator] sub-trait provides custom build hooks to generate code for initializing
54/// some VM extensions. The `VmConfig` is expected to contain the [SystemConfig] internally.
55///
56/// For users who only need to create an execution environment, use the sub-trait
57/// [VmExecutionConfig] to avoid the `SC` generic.
58///
59/// This trait does not contain the [VmBuilder] trait, because a single VM configuration may
60/// implement multiple [VmBuilder]s for different prover backends.
61pub trait VmConfig<SC>:
62    Clone
63    + Serialize
64    + DeserializeOwned
65    + InitFileGenerator
66    + VmExecutionConfig<Val<SC>>
67    + VmCircuitConfig<SC>
68    + AsRef<SystemConfig>
69    + AsMut<SystemConfig>
70where
71    SC: StarkProtocolConfig,
72{
73}
74
75pub trait VmExecutionConfig<F> {
76    type Executor: AnyEnum;
77
78    fn create_executors(&self)
79        -> Result<ExecutorInventory<Self::Executor>, ExecutorInventoryError>;
80}
81
82pub trait VmCircuitConfig<SC: StarkProtocolConfig> {
83    fn create_airs(&self) -> Result<AirInventory<SC>, AirInventoryError>;
84}
85
86/// This trait is intended to be implemented on a new type wrapper of the VmConfig struct to get
87/// around Rust orphan rules.
88pub trait VmBuilder<E: StarkEngine>: Sized {
89    type VmConfig: VmConfig<E::SC>;
90    type RecordArena: Arena;
91    type SystemChipInventory: SystemChipComplex<Self::RecordArena, E::PB>;
92
93    /// Create a [VmChipComplex] from the full [AirInventory], which should be the output of
94    /// [VmCircuitConfig::create_airs].
95    #[allow(clippy::type_complexity)]
96    fn create_chip_complex(
97        &self,
98        config: &Self::VmConfig,
99        circuit: AirInventory<E::SC>,
100        device_ctx: &EngineDeviceCtx<E>,
101    ) -> Result<
102        VmChipComplex<E::SC, Self::RecordArena, E::PB, Self::SystemChipInventory>,
103        ChipInventoryError,
104    >;
105}
106
107impl<SC, VC> VmConfig<SC> for VC
108where
109    SC: StarkProtocolConfig,
110    VC: Clone
111        + Serialize
112        + DeserializeOwned
113        + InitFileGenerator
114        + VmExecutionConfig<Val<SC>>
115        + VmCircuitConfig<SC>
116        + AsRef<SystemConfig>
117        + AsMut<SystemConfig>,
118{
119}
120
121pub const OPENVM_DEFAULT_INIT_FILE_BASENAME: &str = "openvm_init";
122pub const OPENVM_DEFAULT_INIT_FILE_NAME: &str = "openvm_init.rs";
123/// Default block size for memory bus interactions. RISC-V byte/halfword loads (`lb`/`lh`) need
124/// fewer bytes, but the adapter always reads a full 4-byte block from memory.
125pub const DEFAULT_BLOCK_SIZE: usize = 4;
126
127/// Trait for generating a init.rs file that contains a call to moduli_init!,
128/// complex_init!, sw_init! with the supported moduli and curves.
129/// Should be implemented by all VM config structs.
130pub trait InitFileGenerator {
131    // Default implementation is no init file.
132    fn generate_init_file_contents(&self) -> Option<String> {
133        None
134    }
135
136    // Do not override this method's default implementation.
137    // This method is called by cargo openvm and the SDK before building the guest package.
138    fn write_to_init_file(
139        &self,
140        manifest_dir: &Path,
141        init_file_name: Option<&str>,
142    ) -> io::Result<()> {
143        if let Some(contents) = self.generate_init_file_contents() {
144            let dest_path = Path::new(manifest_dir)
145                .join(init_file_name.unwrap_or(OPENVM_DEFAULT_INIT_FILE_NAME));
146            let mut f = File::create(&dest_path)?;
147            write!(f, "{contents}")?;
148        }
149        Ok(())
150    }
151}
152
153/// Each address space in guest memory may be configured with a different type `T` to represent a
154/// memory cell in the address space. On host, the address space will be mapped to linear host
155/// memory in bytes. The type `T` must be plain old data (POD) and be safely transmutable from a
156/// fixed size array of bytes. Moreover, each type `T` must be convertible to a field element `F`.
157///
158/// We currently implement this trait on the enum [MemoryCellType], which includes all cell types
159/// that we expect to be used in the VM context.
160pub trait AddressSpaceHostLayout {
161    /// Size in bytes of the memory cell type.
162    fn size(&self) -> usize;
163
164    /// # Safety
165    /// - This function must only be called when `value` is guaranteed to be of size `self.size()`.
166    /// - Alignment of `value` must be a multiple of the alignment of `F`.
167    /// - The field type `F` must be plain old data.
168    unsafe fn to_field<F: Field>(&self, value: &[u8]) -> F;
169}
170
171#[derive(Debug, Serialize, Deserialize, Clone, new)]
172pub struct MemoryConfig {
173    /// The maximum height of the address space. This means the trie has `addr_space_height` layers
174    /// for searching the address space. The allowed address spaces are those in the range `[1,
175    /// 1 + 2^addr_space_height)` where it starts from 1 to not allow address space 0 in memory.
176    pub addr_space_height: usize,
177    /// It is expected that the size of the list is `(1 << addr_space_height) + 1` and the first
178    /// element is 0, which means no address space.
179    pub addr_spaces: Vec<AddressSpaceHostConfig>,
180    pub pointer_max_bits: usize,
181    /// All timestamps must be in the range `[0, 2^timestamp_max_bits)`. Maximum allowed: 29.
182    pub timestamp_max_bits: usize,
183    /// Limb size used by the range checker
184    pub decomp: usize,
185}
186
187impl Default for MemoryConfig {
188    fn default() -> Self {
189        let mut addr_spaces =
190            Self::empty_address_space_configs((1 << 3) + ADDR_SPACE_OFFSET as usize);
191        const MAX_CELLS: usize = 1 << 29;
192        addr_spaces[RV32_REGISTER_AS as usize].num_cells = 32 * size_of::<u32>();
193        addr_spaces[RV32_MEMORY_AS as usize].num_cells = MAX_CELLS;
194        addr_spaces[PUBLIC_VALUES_AS as usize].num_cells = DEFAULT_MAX_NUM_PUBLIC_VALUES;
195        addr_spaces[DEFERRAL_AS as usize].num_cells = DEFAULT_DEFERRAL_ADDR_SPACE_CELLS;
196        Self::new(3, addr_spaces, POINTER_MAX_BITS, 29, 17)
197    }
198}
199
200impl MemoryConfig {
201    pub fn empty_address_space_configs(num_addr_spaces: usize) -> Vec<AddressSpaceHostConfig> {
202        // By default only address spaces 1..=4 have non-empty cell counts.
203        let mut addr_spaces =
204            vec![AddressSpaceHostConfig::new(0, MemoryCellType::field32()); num_addr_spaces];
205        addr_spaces[RV32_IMM_AS as usize] = AddressSpaceHostConfig::new(0, MemoryCellType::Null);
206        addr_spaces[RV32_REGISTER_AS as usize] = AddressSpaceHostConfig::new(0, MemoryCellType::U8);
207
208        addr_spaces[RV32_MEMORY_AS as usize] = AddressSpaceHostConfig::new(0, MemoryCellType::U8);
209
210        addr_spaces[PUBLIC_VALUES_AS as usize] = AddressSpaceHostConfig::new(0, MemoryCellType::U8);
211
212        addr_spaces
213    }
214
215    /// Config for aggregation usage with only native address space.
216    pub fn aggregation() -> Self {
217        let mut addr_spaces =
218            Self::empty_address_space_configs((1 << 3) + ADDR_SPACE_OFFSET as usize);
219        addr_spaces[openvm_instructions::DEFERRAL_AS as usize].num_cells = 1 << 29;
220        Self::new(3, addr_spaces, POINTER_MAX_BITS, 29, 17)
221    }
222}
223
224/// System-level configuration for the virtual machine. Contains all configuration parameters that
225/// are managed by the architecture.
226#[derive(Debug, Clone, Serialize, Deserialize, Setters, WithSetters)]
227pub struct SystemConfig {
228    /// The maximum constraint degree any chip is allowed to use.
229    #[getset(set_with = "pub")]
230    pub max_constraint_degree: usize,
231    /// Memory configuration
232    pub memory_config: MemoryConfig,
233    /// Public values are stored in a special address space.
234    /// `num_public_values` indicates the number of allowed addresses in that address space.
235    pub num_public_values: usize,
236    /// Max memory in bytes used across all chips for triggering segmentation.
237    /// This field is skipped in serde as it's only used in execution and
238    /// not needed after any serialize/deserialize.
239    #[serde(skip, default = "default_segmentation_max_memory")]
240    #[getset(set = "pub")]
241    pub segmentation_max_memory: usize,
242}
243
244impl SystemConfig {
245    pub fn new(
246        max_constraint_degree: usize,
247        mut memory_config: MemoryConfig,
248        num_public_values: usize,
249    ) -> Self {
250        assert!(
251            memory_config.timestamp_max_bits <= 29,
252            "Timestamp max bits must be <= 29 for LessThan to work in 31-bit field"
253        );
254        memory_config.addr_spaces[PUBLIC_VALUES_AS as usize].num_cells = num_public_values;
255        Self {
256            max_constraint_degree,
257            memory_config,
258            num_public_values,
259            segmentation_max_memory: DEFAULT_MAX_MEMORY,
260        }
261    }
262
263    pub fn default_from_memory(memory_config: MemoryConfig) -> Self {
264        Self::new(
265            DEFAULT_POSEIDON2_MAX_CONSTRAINT_DEGREE,
266            memory_config,
267            DEFAULT_MAX_NUM_PUBLIC_VALUES,
268        )
269    }
270
271    pub fn with_public_values(mut self, num_public_values: usize) -> Self {
272        self.num_public_values = num_public_values;
273        self.memory_config.addr_spaces[PUBLIC_VALUES_AS as usize].num_cells = num_public_values;
274        self
275    }
276
277    /// Returns the AIR ID of the memory boundary AIR. Panic if the boundary AIR is not enabled.
278    pub fn memory_boundary_air_id(&self) -> usize {
279        BOUNDARY_AIR_ID
280    }
281
282    /// Returns the AIR ID of the memory merkle AIR.
283    pub fn memory_merkle_air_id(&self) -> usize {
284        self.memory_boundary_air_id() + 1
285    }
286
287    /// Whether the AIR ID must be present in a valid v2 proof.
288    pub fn is_required_air_id(&self, air_id: usize) -> bool {
289        air_id == PROGRAM_AIR_ID
290            || air_id == CONNECTOR_AIR_ID
291            || air_id == self.memory_boundary_air_id()
292            || air_id == self.memory_merkle_air_id()
293    }
294
295    /// This is O(1) and returns the length of
296    /// [`SystemAirInventory::into_airs`](crate::system::SystemAirInventory::into_airs).
297    pub fn num_airs(&self) -> usize {
298        self.memory_boundary_air_id() + num_memory_airs()
299    }
300}
301
302impl Default for SystemConfig {
303    fn default() -> Self {
304        Self::default_from_memory(MemoryConfig::default())
305    }
306}
307
308impl AsRef<SystemConfig> for SystemConfig {
309    fn as_ref(&self) -> &SystemConfig {
310        self
311    }
312}
313
314impl AsMut<SystemConfig> for SystemConfig {
315    fn as_mut(&mut self) -> &mut SystemConfig {
316        self
317    }
318}
319
320// Default implementation uses no init file
321impl InitFileGenerator for SystemConfig {}
322
323#[derive(Debug, Serialize, Deserialize, Clone, Copy, new)]
324pub struct AddressSpaceHostConfig {
325    /// The number of memory cells in each address space, where a memory cell refers to a single
326    /// addressable unit of memory as defined by the ISA.
327    pub num_cells: usize,
328    pub layout: MemoryCellType,
329}
330
331impl AddressSpaceHostConfig {
332    /// The total size in bytes of the address space in a linear memory layout.
333    pub fn size(&self) -> usize {
334        self.num_cells * self.layout.size()
335    }
336}
337
338#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
339pub enum MemoryCellType {
340    Null,
341    U8,
342    U16,
343    /// Represented in little-endian format.
344    U32,
345    /// `size` is the size in bytes of the native field type. This should not exceed 8.
346    F {
347        size: u8,
348    },
349}
350
351impl MemoryCellType {
352    pub fn field32() -> Self {
353        Self::F {
354            size: size_of::<u32>() as u8,
355        }
356    }
357}
358
359impl AddressSpaceHostLayout for MemoryCellType {
360    fn size(&self) -> usize {
361        match self {
362            Self::Null => 1, // to avoid divide by zero
363            Self::U8 => size_of::<u8>(),
364            Self::U16 => size_of::<u16>(),
365            Self::U32 => size_of::<u32>(),
366            Self::F { size } => *size as usize,
367        }
368    }
369
370    /// # Safety
371    /// - This function must only be called when `value` is guaranteed to be of size `self.size()`.
372    /// - Alignment of `value` must be a multiple of the alignment of `F`.
373    /// - The field type `F` must be plain old data.
374    ///
375    /// # Panics
376    /// If the value is of integer type and overflows the field.
377    unsafe fn to_field<F: Field>(&self, value: &[u8]) -> F {
378        match self {
379            Self::Null => unreachable!(),
380            Self::U8 => F::from_u8(*value.get_unchecked(0)),
381            Self::U16 => F::from_u16(core::ptr::read(value.as_ptr() as *const u16)),
382            Self::U32 => F::from_u32(core::ptr::read(value.as_ptr() as *const u32)),
383            Self::F { .. } => core::ptr::read(value.as_ptr() as *const F),
384        }
385    }
386}