openvm_benchmark_synthetic/
synthetic_air.rs1use std::path::Path;
25
26use openvm_stark_backend::{
27 interaction::{BusIndex, InteractionBuilder},
28 p3_air::{Air, BaseAir, BaseAirWithPublicValues},
29 p3_field::{Field, PrimeCharacteristicRing},
30 p3_matrix::{dense::RowMajorMatrix, Matrix},
31 PartitionedBaseAir,
32};
33use serde::{Deserialize, Serialize};
34
35#[derive(Clone, Debug, Serialize, Deserialize)]
39pub struct SyntheticShape {
40 pub air_name: String,
41 pub log_height: usize,
42 pub preprocessed_width: usize,
43 pub cached_main_widths: Vec<usize>,
44 pub common_main_width: usize,
45 pub after_challenge_widths: Vec<usize>,
46 pub num_constraints: usize,
47 pub num_interactions: usize,
48 pub num_distinct_buses: usize,
49 pub max_constraint_degree: usize,
50 #[serde(default)]
53 pub interaction_message_lens: Vec<usize>,
54 #[serde(default)]
57 pub interaction_count_weights: Vec<u32>,
58 pub occurrences: usize,
59}
60
61#[derive(Clone, Debug, Serialize, Deserialize)]
62pub struct ShapeAtlas {
63 pub source: String,
64 pub shapes: Vec<SyntheticShape>,
65}
66
67impl ShapeAtlas {
68 pub fn read(path: impl AsRef<Path>) -> std::io::Result<Self> {
69 let f = std::fs::File::open(path)?;
70 Ok(serde_json::from_reader(f)?)
71 }
72}
73
74#[derive(Clone, Debug)]
77pub struct SyntheticAir {
78 width: usize,
79 num_constraints: usize,
80 max_constraint_degree: usize,
81 interactions: Vec<(BusIndex, bool, usize, u32)>,
83}
84
85impl SyntheticAir {
86 pub fn from_shape(s: &SyntheticShape) -> Self {
87 let max_captured_msg_len = s
96 .interaction_message_lens
97 .iter()
98 .copied()
99 .max()
100 .unwrap_or(0);
101 let width = s.common_main_width.max(max_captured_msg_len + 1).max(1);
102 let num_buses = s.num_distinct_buses.max(1);
103 let max_field_count = width - 1;
107 let fallback_len = max_field_count.min(4);
108 let interactions = (0..s.num_interactions)
109 .map(|i| {
110 let bus = (i % num_buses) as BusIndex;
111 let is_send = i % 2 == 0;
112 let captured_len = s
113 .interaction_message_lens
114 .get(i)
115 .copied()
116 .unwrap_or(fallback_len);
117 let msg_len = captured_len.min(max_field_count);
118 let cw = s.interaction_count_weights.get(i).copied().unwrap_or(0);
119 (bus, is_send, msg_len, cw)
120 })
121 .collect();
122 Self {
123 width,
124 num_constraints: s.num_constraints,
125 max_constraint_degree: s.max_constraint_degree.max(1),
126 interactions,
127 }
128 }
129
130 pub fn width(&self) -> usize {
131 self.width
132 }
133 pub fn num_constraints(&self) -> usize {
134 self.num_constraints
135 }
136 pub fn num_interactions(&self) -> usize {
137 self.interactions.len()
138 }
139
140 pub fn generate_trace<F: Field>(&self, log_height: usize) -> RowMajorMatrix<F> {
144 let h = 1usize << log_height;
145 RowMajorMatrix::new(vec![F::ZERO; h * self.width], self.width)
146 }
147}
148
149impl<F: Field> BaseAir<F> for SyntheticAir {
150 fn width(&self) -> usize {
151 self.width
152 }
153}
154impl<F: Field> BaseAirWithPublicValues<F> for SyntheticAir {}
155impl<F: Field> PartitionedBaseAir<F> for SyntheticAir {}
156
157impl<AB: InteractionBuilder> Air<AB> for SyntheticAir {
158 fn eval(&self, builder: &mut AB) {
159 let main = builder.main();
160 let local = main.row_slice(0).expect("trace has at least one row");
161 let next = main.row_slice(1).expect("trace has at least two rows");
162
163 let degree = self.max_constraint_degree;
184 let slot_count = 2 * self.width;
185 for c in 0..self.num_constraints {
186 let mut idx = c;
187 let mut term: Option<AB::Expr> = None;
188 for _ in 0..degree {
189 let slot = idx % slot_count;
190 idx /= slot_count;
191 let col = if slot < self.width {
192 local[slot]
193 } else {
194 next[slot - self.width]
195 };
196 term = Some(match term {
197 None => col.into(),
198 Some(t) => t * col.into(),
199 });
200 }
201 let term = term.unwrap_or_else(|| local[0].into());
202 builder.assert_zero(term);
203 }
204
205 for (bus, is_send, msg_len, count_weight) in &self.interactions {
213 let count = local[0];
214 let fields: Vec<AB::Expr> = (0..*msg_len)
215 .map(|i| local[(i + 1) % self.width].into())
216 .collect();
217 if *is_send {
218 builder.push_interaction(*bus, fields, count, *count_weight);
219 } else {
220 builder.push_interaction(
221 *bus,
222 fields,
223 AB::Expr::NEG_ONE * count.into(),
224 *count_weight,
225 );
226 }
227 }
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 fn shape_for_test() -> SyntheticShape {
236 SyntheticShape {
239 air_name: "TestSynthetic".into(),
240 log_height: 4,
241 preprocessed_width: 0,
242 cached_main_widths: vec![],
243 common_main_width: 6,
244 after_challenge_widths: vec![],
245 num_constraints: 7,
246 num_interactions: 3,
247 num_distinct_buses: 2,
248 max_constraint_degree: 3,
249 interaction_message_lens: vec![3, 3, 3],
250 interaction_count_weights: vec![0, 0, 0],
251 occurrences: 1,
252 }
253 }
254
255 #[test]
256 fn synthetic_air_metadata_matches_shape() {
257 let s = shape_for_test();
258 let air = SyntheticAir::from_shape(&s);
259 assert_eq!(air.width(), s.common_main_width);
260 assert_eq!(air.num_constraints(), s.num_constraints);
261 assert_eq!(air.num_interactions(), s.num_interactions);
262 }
263
264 #[test]
265 fn width_widens_to_fit_captured_message_len() {
266 let s = SyntheticShape {
271 air_name: "WidthOneWithMsg".into(),
272 log_height: 4,
273 preprocessed_width: 0,
274 cached_main_widths: vec![],
275 common_main_width: 1,
276 after_challenge_widths: vec![],
277 num_constraints: 0,
278 num_interactions: 1,
279 num_distinct_buses: 1,
280 max_constraint_degree: 1,
281 interaction_message_lens: vec![9],
282 interaction_count_weights: vec![0],
283 occurrences: 1,
284 };
285 let air = SyntheticAir::from_shape(&s);
286 assert_eq!(air.width(), 10, "kill col + 9 message fields");
287 }
288
289 #[test]
290 fn synthetic_air_keygen_prove_verify_round_trip() {
291 use std::sync::Arc;
292
293 use eyre::eyre;
294 use openvm_stark_backend::{
295 prover::{AirProvingContext, DeviceDataTransporter, ProvingContext},
296 StarkEngine,
297 };
298 use openvm_stark_sdk::config::{
299 app_params_with_100_bits_security, baby_bear_poseidon2::BabyBearPoseidon2CpuEngine,
300 };
301 use p3_baby_bear::BabyBear;
302
303 let s = shape_for_test();
304 let air = SyntheticAir::from_shape(&s);
305 let trace = air.generate_trace::<BabyBear>(s.log_height);
306
307 let params = app_params_with_100_bits_security(15);
310 let engine: BabyBearPoseidon2CpuEngine = StarkEngine::new(params);
311
312 let air_arc = Arc::new(air);
313 let (pk, vk) = engine.keygen(&[air_arc]);
314
315 let inner_pk = &pk.per_air[0];
316 assert_eq!(
317 inner_pk
318 .vk
319 .symbolic_constraints
320 .constraints
321 .constraint_idx
322 .len(),
323 s.num_constraints,
324 "keygen-reported num_constraints must match the shape's num_constraints",
325 );
326 assert_eq!(
327 inner_pk.vk.symbolic_constraints.interactions.len(),
328 s.num_interactions,
329 "keygen-reported num_interactions must match the shape's num_interactions",
330 );
331
332 let trace_ctx = AirProvingContext::simple_no_pis(trace);
333 let d_pk = engine.device().transport_pk_to_device(&pk);
334 let proof = engine
335 .prove(&d_pk, ProvingContext::new(vec![(0, trace_ctx)]))
336 .map_err(|e| eyre!("Proving failed: {e:?}"))
337 .unwrap();
338 engine
339 .verify(&vk, &proof)
340 .expect("synthetic proof verifies");
341 }
342}