1use core::borrow::Borrow;
2
3use openvm_circuit_primitives::{
4 utils::assert_array_eq, ColumnsAir, StructReflection, StructReflectionHelper, SubAir,
5};
6use openvm_recursion_circuit_derive::AlignedBorrow;
7use openvm_stark_backend::{
8 interaction::InteractionBuilder, BaseAirWithPublicValues, PartitionedBaseAir,
9};
10use openvm_stark_sdk::config::baby_bear_poseidon2::D_EF;
11use p3_air::{Air, AirBuilder, BaseAir};
12use p3_field::{extension::BinomiallyExtendable, Field, PrimeCharacteristicRing};
13use p3_matrix::Matrix;
14
15use crate::{
16 bus::{TranscriptBus, XiRandomnessBus, XiRandomnessMessage},
17 gkr::bus::{
18 GkrSumcheckChallengeBus, GkrSumcheckChallengeMessage, GkrSumcheckInputBus,
19 GkrSumcheckInputMessage, GkrSumcheckOutputBus, GkrSumcheckOutputMessage,
20 },
21 subairs::nested_for_loop::{NestedForLoopIoCols, NestedForLoopSubAir},
22 utils::{
23 assert_one_ext, ext_field_add, ext_field_multiply, ext_field_multiply_scalar,
24 ext_field_one_minus, ext_field_subtract,
25 },
26};
27
28#[repr(C)]
29#[derive(AlignedBorrow, Debug, StructReflection)]
30pub struct GkrLayerSumcheckCols<T> {
31 pub is_enabled: T,
33 pub proof_idx: T,
34 pub layer_idx: T,
35 pub is_proof_start: T,
36 pub is_first_round: T,
37
38 pub is_dummy: T,
41
42 pub is_last_layer: T,
43
44 pub round: T,
47
48 pub tidx: T,
50
51 pub ev1: [T; D_EF],
53 pub ev2: [T; D_EF],
55 pub ev3: [T; D_EF],
57
58 pub claim_in: [T; D_EF],
60 pub claim_out: [T; D_EF],
62
63 pub prev_challenge: [T; D_EF],
66 pub challenge: [T; D_EF],
68
69 pub eq_in: [T; D_EF],
71 pub eq_out: [T; D_EF],
73}
74
75#[derive(ColumnsAir)]
76#[columns_via(GkrLayerSumcheckCols<u8>)]
77pub struct GkrLayerSumcheckAir {
78 pub transcript_bus: TranscriptBus,
79 pub xi_randomness_bus: XiRandomnessBus,
80 pub sumcheck_input_bus: GkrSumcheckInputBus,
81 pub sumcheck_output_bus: GkrSumcheckOutputBus,
82 pub sumcheck_challenge_bus: GkrSumcheckChallengeBus,
83}
84
85impl GkrLayerSumcheckAir {
86 pub fn new(
87 transcript_bus: TranscriptBus,
88 xi_randomness_bus: XiRandomnessBus,
89 sumcheck_input_bus: GkrSumcheckInputBus,
90 sumcheck_output_bus: GkrSumcheckOutputBus,
91 sumcheck_challenge_bus: GkrSumcheckChallengeBus,
92 ) -> Self {
93 Self {
94 transcript_bus,
95 xi_randomness_bus,
96 sumcheck_input_bus,
97 sumcheck_output_bus,
98 sumcheck_challenge_bus,
99 }
100 }
101}
102
103impl<F: Field> BaseAir<F> for GkrLayerSumcheckAir {
104 fn width(&self) -> usize {
105 GkrLayerSumcheckCols::<F>::width()
106 }
107}
108
109impl<F: Field> BaseAirWithPublicValues<F> for GkrLayerSumcheckAir {}
110impl<F: Field> PartitionedBaseAir<F> for GkrLayerSumcheckAir {}
111
112impl<AB: AirBuilder + InteractionBuilder> Air<AB> for GkrLayerSumcheckAir
113where
114 <AB::Expr as PrimeCharacteristicRing>::PrimeSubfield: BinomiallyExtendable<{ D_EF }>,
115{
116 fn eval(&self, builder: &mut AB) {
117 let main = builder.main();
118 let (local, next) = (
119 main.row_slice(0).expect("window should have two elements"),
120 main.row_slice(1).expect("window should have two elements"),
121 );
122 let local: &GkrLayerSumcheckCols<AB::Var> = (*local).borrow();
123 let next: &GkrLayerSumcheckCols<AB::Var> = (*next).borrow();
124
125 builder.assert_bool(local.is_dummy);
130 builder.assert_bool(local.is_last_layer);
131
132 type LoopSubAir = NestedForLoopSubAir<2>;
137 LoopSubAir {}.eval(
138 builder,
139 (
140 NestedForLoopIoCols {
141 is_enabled: local.is_enabled.into(),
142 counter: [local.proof_idx.into(), local.layer_idx - AB::F::ONE],
143 is_first: [local.is_proof_start.into(), local.is_first_round.into()],
144 }
145 .map_into(),
146 NestedForLoopIoCols {
147 is_enabled: next.is_enabled.into(),
148 counter: [next.proof_idx.into(), next.layer_idx - AB::F::ONE],
149 is_first: [next.is_proof_start.into(), next.is_first_round.into()],
150 },
151 ),
152 );
153
154 let is_transition_round =
155 LoopSubAir::local_is_transition(next.is_enabled, next.is_first_round);
156 let is_last_round =
157 LoopSubAir::local_is_last(local.is_enabled, next.is_enabled, next.is_first_round);
158 let is_transition_proof = next.is_enabled - next.is_proof_start;
159
160 builder.when(local.is_first_round).assert_zero(local.round);
162 builder
164 .when(is_transition_round.clone())
165 .assert_eq(next.round, local.round + AB::Expr::ONE);
166 builder
168 .when(is_last_round.clone())
169 .assert_eq(local.round, local.layer_idx - AB::Expr::ONE);
170
171 builder
177 .when(is_transition_proof.clone())
178 .assert_eq(next.is_dummy, local.is_dummy);
179 builder
181 .when(is_transition_proof.clone())
182 .assert_zero(local.is_dummy);
183
184 builder
186 .when(local.is_dummy)
187 .assert_one(local.is_proof_start);
188 builder
189 .when(local.is_dummy)
190 .assert_one(local.is_first_round);
191 builder
192 .when(local.is_dummy)
193 .assert_one(is_last_round.clone());
194 builder.when(local.is_dummy).assert_one(local.is_last_layer);
195 assert_array_eq(&mut builder.when(local.is_dummy), local.eq_out, local.eq_in);
197 assert_array_eq(
198 &mut builder.when(local.is_dummy),
199 local.claim_out,
200 local.claim_in,
201 );
202
203 builder
209 .when(is_transition_round.clone())
210 .assert_eq(next.is_last_layer, local.is_last_layer);
211
212 assert_one_ext(&mut builder.when(local.is_first_round), local.eq_in);
214
215 let eq_out: [AB::Expr; D_EF] =
217 update_eq(local.eq_in, local.prev_challenge, local.challenge);
218 assert_array_eq(&mut builder.when(local.is_enabled), local.eq_out, eq_out);
219
220 assert_array_eq(
222 &mut builder.when(is_transition_round.clone()),
223 local.eq_out,
224 next.eq_in,
225 );
226
227 let ev0: [AB::Expr; D_EF] = ext_field_subtract(local.claim_in, local.ev1);
229
230 let claim_out: [AB::Expr; D_EF] =
232 interpolate_cubic_at_0123(ev0, local.ev1, local.ev2, local.ev3, local.challenge);
233 assert_array_eq(builder, local.claim_out, claim_out);
234
235 assert_array_eq(
237 &mut builder.when(is_transition_round.clone()),
238 local.claim_out,
239 next.claim_in,
240 );
241
242 builder.when(is_transition_round.clone()).assert_eq(
244 next.tidx,
245 local.tidx.into() + AB::Expr::from_usize(4 * D_EF),
246 );
247
248 let is_not_dummy = AB::Expr::ONE - local.is_dummy;
253
254 self.sumcheck_input_bus.receive(
257 builder,
258 local.proof_idx,
259 GkrSumcheckInputMessage {
260 layer_idx: local.layer_idx,
261 is_last_layer: local.is_last_layer,
262 tidx: local.tidx,
263 claim: local.claim_in,
264 },
265 local.is_first_round * is_not_dummy.clone(),
266 );
267 self.sumcheck_output_bus.send(
270 builder,
271 local.proof_idx,
272 GkrSumcheckOutputMessage {
273 layer_idx: local.layer_idx.into(),
274 tidx: local.tidx.into() + AB::Expr::from_usize(4 * D_EF),
275 claim_out: local.claim_out.map(Into::into),
276 eq_at_r_prime: local.eq_out.map(Into::into),
277 },
278 is_last_round.clone() * is_not_dummy.clone(),
279 );
280
281 self.sumcheck_challenge_bus.receive(
284 builder,
285 local.proof_idx,
286 GkrSumcheckChallengeMessage {
287 layer_idx: local.layer_idx - AB::Expr::ONE,
288 sumcheck_round: local.round.into(),
289 challenge: local.prev_challenge.map(Into::into),
290 },
291 local.is_enabled * is_not_dummy.clone(),
292 );
293 self.sumcheck_challenge_bus.send(
295 builder,
296 local.proof_idx,
297 GkrSumcheckChallengeMessage {
298 layer_idx: local.layer_idx.into(),
299 sumcheck_round: local.round.into() + AB::Expr::ONE,
300 challenge: local.challenge.map(Into::into),
301 },
302 local.is_enabled * (AB::Expr::ONE - local.is_last_layer) * is_not_dummy.clone(),
303 );
304
305 let mut tidx = local.tidx.into();
312 for eval in [local.ev1, local.ev2, local.ev3].into_iter() {
313 self.transcript_bus.observe_ext(
314 builder,
315 local.proof_idx,
316 tidx.clone(),
317 eval,
318 local.is_enabled * is_not_dummy.clone(),
319 );
320 tidx += AB::Expr::from_usize(D_EF);
321 }
322 self.transcript_bus.sample_ext(
324 builder,
325 local.proof_idx,
326 tidx,
327 local.challenge,
328 local.is_enabled * is_not_dummy.clone(),
329 );
330
331 self.xi_randomness_bus.send(
334 builder,
335 local.proof_idx,
336 XiRandomnessMessage {
337 idx: local.round + AB::Expr::ONE,
338 xi: local.challenge.map(Into::into),
339 },
340 local.is_enabled * local.is_last_layer * is_not_dummy.clone(),
341 );
342 }
343}
344
345pub(super) fn interpolate_cubic_at_0123<F, FA>(
350 ev0: [FA; D_EF],
351 ev1: [F; D_EF],
352 ev2: [F; D_EF],
353 ev3: [F; D_EF],
354 x: [F; D_EF],
355) -> [FA; D_EF]
356where
357 F: Into<FA> + Copy,
358 FA: PrimeCharacteristicRing,
359 FA::PrimeSubfield: BinomiallyExtendable<{ D_EF }>,
360{
361 let three: FA = FA::from_usize(3);
362 let inv2: FA = FA::from_prime_subfield(FA::PrimeSubfield::from_usize(2).inverse());
363 let inv6: FA = FA::from_prime_subfield(FA::PrimeSubfield::from_usize(6).inverse());
364
365 let s1: [FA; D_EF] = ext_field_subtract(ev1, ev0.clone());
367 let s2: [FA; D_EF] = ext_field_subtract(ev2, ev0.clone());
369 let s3: [FA; D_EF] = ext_field_subtract(ev3, ev0.clone());
371
372 let d3: [FA; D_EF] = ext_field_subtract::<FA>(
374 s3,
375 ext_field_multiply_scalar::<FA>(ext_field_subtract::<FA>(s2.clone(), s1.clone()), three),
376 );
377
378 let p: [FA; D_EF] = ext_field_multiply_scalar(d3.clone(), inv6);
380
381 let q: [FA; D_EF] = ext_field_subtract::<FA>(
383 ext_field_multiply_scalar::<FA>(ext_field_subtract::<FA>(s2, d3), inv2),
384 s1.clone(),
385 );
386
387 let r: [FA; D_EF] = ext_field_subtract::<FA>(s1, ext_field_add::<FA>(p.clone(), q.clone()));
389
390 ext_field_add::<FA>(
392 ext_field_multiply::<FA>(
393 ext_field_add::<FA>(
394 ext_field_multiply::<FA>(ext_field_add::<FA>(ext_field_multiply::<FA>(p, x), q), x),
395 r,
396 ),
397 x,
398 ),
399 ev0,
400 )
401}
402
403pub(super) fn update_eq<F, FA>(
408 eq_in: [F; D_EF],
409 prev_challenge: [F; D_EF],
410 challenge: [F; D_EF],
411) -> [FA; D_EF]
412where
413 F: Into<FA> + Copy,
414 FA: PrimeCharacteristicRing,
415 FA::PrimeSubfield: BinomiallyExtendable<{ D_EF }>,
416{
417 ext_field_multiply::<FA>(
418 eq_in,
419 ext_field_add::<FA>(
420 ext_field_multiply::<FA>(prev_challenge, challenge),
421 ext_field_multiply::<FA>(
422 ext_field_one_minus::<FA>(prev_challenge),
423 ext_field_one_minus::<FA>(challenge),
424 ),
425 ),
426 )
427}