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::{
18 bus::{
19 GkrLayerInputBus, GkrLayerInputMessage, GkrLayerOutputBus, GkrLayerOutputMessage,
20 GkrSumcheckInputBus, GkrSumcheckInputMessage, GkrSumcheckOutputBus,
21 GkrSumcheckOutputMessage,
22 },
23 GkrSumcheckChallengeBus, GkrSumcheckChallengeMessage,
24 },
25 subairs::nested_for_loop::{NestedForLoopIoCols, NestedForLoopSubAir},
26 utils::{assert_zeros, ext_field_add, ext_field_multiply, ext_field_subtract},
27};
28
29#[repr(C)]
30#[derive(AlignedBorrow, Debug, StructReflection)]
31pub struct GkrLayerCols<T> {
32 pub is_enabled: T,
34 pub proof_idx: T,
35 pub is_first: T,
36
37 pub is_dummy: T,
40
41 pub layer_idx: T,
43
44 pub tidx: T,
46
47 pub lambda: [T; D_EF],
49
50 pub p_xi_0: [T; D_EF],
52 pub q_xi_0: [T; D_EF],
53 pub p_xi_1: [T; D_EF],
54 pub q_xi_1: [T; D_EF],
55
56 pub numer_claim: [T; D_EF],
58 pub denom_claim: [T; D_EF],
60
61 pub sumcheck_claim_in: [T; D_EF],
63
64 pub eq_at_r_prime: [T; D_EF],
66
67 pub mu: [T; D_EF],
69}
70
71#[derive(ColumnsAir)]
73#[columns_via(GkrLayerCols<u8>)]
74pub struct GkrLayerAir {
75 pub xi_randomness_bus: XiRandomnessBus,
77 pub transcript_bus: TranscriptBus,
78 pub layer_input_bus: GkrLayerInputBus,
80 pub layer_output_bus: GkrLayerOutputBus,
81 pub sumcheck_input_bus: GkrSumcheckInputBus,
82 pub sumcheck_output_bus: GkrSumcheckOutputBus,
83 pub sumcheck_challenge_bus: GkrSumcheckChallengeBus,
84}
85
86impl<F: Field> BaseAir<F> for GkrLayerAir {
87 fn width(&self) -> usize {
88 GkrLayerCols::<F>::width()
89 }
90}
91
92impl<F: Field> BaseAirWithPublicValues<F> for GkrLayerAir {}
93impl<F: Field> PartitionedBaseAir<F> for GkrLayerAir {}
94
95impl<AB: AirBuilder + InteractionBuilder> Air<AB> for GkrLayerAir
96where
97 <AB::Expr as PrimeCharacteristicRing>::PrimeSubfield: BinomiallyExtendable<{ D_EF }>,
98{
99 fn eval(&self, builder: &mut AB) {
100 let main = builder.main();
101 let (local, next) = (
102 main.row_slice(0).expect("window should have two elements"),
103 main.row_slice(1).expect("window should have two elements"),
104 );
105 let local: &GkrLayerCols<AB::Var> = (*local).borrow();
106 let next: &GkrLayerCols<AB::Var> = (*next).borrow();
107
108 builder.assert_bool(local.is_dummy);
113
114 type LoopSubAir = NestedForLoopSubAir<1>;
119
120 LoopSubAir {}.eval(
125 builder,
126 (
127 NestedForLoopIoCols {
128 is_enabled: local.is_enabled,
129 counter: [local.proof_idx],
130 is_first: [local.is_first],
131 }
132 .map_into(),
133 NestedForLoopIoCols {
134 is_enabled: next.is_enabled,
135 counter: [next.proof_idx],
136 is_first: [next.is_first],
137 }
138 .map_into(),
139 ),
140 );
141
142 let is_transition = LoopSubAir::local_is_transition(next.is_enabled, next.is_first);
143 let is_last = LoopSubAir::local_is_last(local.is_enabled, next.is_enabled, next.is_first);
144
145 builder.when(local.is_first).assert_zero(local.layer_idx);
147 builder
149 .when(is_transition.clone())
150 .assert_eq(next.layer_idx, local.layer_idx + AB::Expr::ONE);
151
152 builder
158 .when(is_transition.clone())
159 .assert_eq(next.is_dummy, local.is_dummy);
160 builder
162 .when(is_transition.clone())
163 .assert_zero(local.is_dummy);
164
165 builder.when(local.is_dummy).assert_one(local.is_first);
167 builder.when(local.is_dummy).assert_one(is_last.clone());
168
169 let (p_cross_term, q_cross_term) =
176 compute_recursive_relations(local.p_xi_0, local.q_xi_0, local.p_xi_1, local.q_xi_1);
177
178 assert_zeros(&mut builder.when(local.is_first), p_cross_term.clone());
180
181 assert_array_eq(
183 &mut builder.when(local.is_first),
184 q_cross_term.clone(),
185 local.sumcheck_claim_in,
186 );
187
188 let (numer_claim, denom_claim) = reduce_to_single_evaluation(
196 local.p_xi_0,
197 local.p_xi_1,
198 local.q_xi_0,
199 local.q_xi_1,
200 local.mu,
201 );
202 assert_array_eq(builder, local.numer_claim, numer_claim);
203 assert_array_eq(builder, local.denom_claim, denom_claim);
204
205 assert_array_eq(
211 &mut builder.when(is_transition.clone()),
212 next.sumcheck_claim_in,
213 ext_field_add::<AB::Expr>(
214 local.numer_claim,
215 ext_field_multiply::<AB::Expr>(next.lambda, local.denom_claim),
216 ),
217 );
218
219 let tidx_after_sumcheck = local.tidx
221 + (AB::Expr::ONE - local.is_first) * AB::Expr::from_usize(D_EF)
223 + local.layer_idx * AB::Expr::from_usize(4 * D_EF);
224 let tidx_end = tidx_after_sumcheck.clone() + AB::Expr::from_usize(5 * D_EF);
225 builder
226 .when(is_transition.clone())
227 .assert_eq(next.tidx, tidx_end.clone());
228
229 let is_not_dummy = AB::Expr::ONE - local.is_dummy;
234 let is_non_root_layer = local.is_enabled * (AB::Expr::ONE - local.is_first);
235
236 self.layer_input_bus.receive(
239 builder,
240 local.proof_idx,
241 GkrLayerInputMessage {
242 tidx: local.tidx,
243 q0_claim: local.sumcheck_claim_in,
244 },
245 local.is_first * is_not_dummy.clone(),
246 );
247 self.layer_output_bus.send(
250 builder,
251 local.proof_idx,
252 GkrLayerOutputMessage {
253 tidx: tidx_end,
254 layer_idx_end: local.layer_idx.into(),
255 input_layer_claim: [
256 local.numer_claim.map(Into::into),
257 local.denom_claim.map(Into::into),
258 ],
259 },
260 is_last.clone() * is_not_dummy.clone(),
261 );
262 self.sumcheck_input_bus.send(
265 builder,
266 local.proof_idx,
267 GkrSumcheckInputMessage {
268 layer_idx: local.layer_idx.into(),
269 is_last_layer: is_last.clone(),
270 tidx: local.tidx + AB::Expr::from_usize(D_EF),
271 claim: local.sumcheck_claim_in.map(Into::into),
272 },
273 is_non_root_layer.clone() * is_not_dummy.clone(),
274 );
275 let sumcheck_claim_out = ext_field_multiply::<AB::Expr>(
278 ext_field_add::<AB::Expr>(
279 p_cross_term,
280 ext_field_multiply::<AB::Expr>(local.lambda, q_cross_term),
281 ),
282 local.eq_at_r_prime,
283 );
284 self.sumcheck_output_bus.receive(
285 builder,
286 local.proof_idx,
287 GkrSumcheckOutputMessage {
288 layer_idx: local.layer_idx.into(),
289 tidx: tidx_after_sumcheck.clone(),
290 claim_out: sumcheck_claim_out.map(Into::into),
291 eq_at_r_prime: local.eq_at_r_prime.map(Into::into),
292 },
293 is_non_root_layer.clone() * is_not_dummy.clone(),
294 );
295 self.sumcheck_challenge_bus.send(
298 builder,
299 local.proof_idx,
300 GkrSumcheckChallengeMessage {
301 layer_idx: local.layer_idx.into(),
302 sumcheck_round: AB::Expr::ZERO,
303 challenge: local.mu.map(Into::into),
304 },
305 is_transition.clone() * is_not_dummy.clone(),
306 );
307
308 self.transcript_bus.sample_ext(
315 builder,
316 local.proof_idx,
317 local.tidx,
318 local.lambda,
319 is_non_root_layer.clone() * is_not_dummy.clone(),
320 );
321 let mut tidx = tidx_after_sumcheck;
323 for claim in [local.p_xi_0, local.q_xi_0, local.p_xi_1, local.q_xi_1].into_iter() {
324 self.transcript_bus.observe_ext(
325 builder,
326 local.proof_idx,
327 tidx.clone(),
328 claim,
329 local.is_enabled * is_not_dummy.clone(),
330 );
331 tidx += AB::Expr::from_usize(D_EF);
332 }
333 self.transcript_bus.sample_ext(
335 builder,
336 local.proof_idx,
337 tidx,
338 local.mu,
339 local.is_enabled * is_not_dummy.clone(),
340 );
341
342 self.xi_randomness_bus.send(
345 builder,
346 local.proof_idx,
347 XiRandomnessMessage {
348 idx: AB::Expr::ZERO,
349 xi: local.mu.map(Into::into),
350 },
351 is_last * is_not_dummy.clone(),
352 );
353 }
354}
355
356fn compute_recursive_relations<F, FA>(
362 p_xi_0: [F; D_EF],
363 q_xi_0: [F; D_EF],
364 p_xi_1: [F; D_EF],
365 q_xi_1: [F; D_EF],
366) -> ([FA; D_EF], [FA; D_EF])
367where
368 F: Into<FA> + Copy,
369 FA: PrimeCharacteristicRing,
370 FA::PrimeSubfield: BinomiallyExtendable<{ D_EF }>,
371{
372 let p_cross_term = ext_field_add::<FA>(
373 ext_field_multiply::<FA>(p_xi_0, q_xi_1),
374 ext_field_multiply::<FA>(p_xi_1, q_xi_0),
375 );
376 let q_cross_term = ext_field_multiply::<FA>(q_xi_0, q_xi_1);
377 (p_cross_term, q_cross_term)
378}
379
380fn interpolate_linear_at_01<F, FA>(evals: [[F; D_EF]; 2], x: [F; D_EF]) -> [FA; D_EF]
382where
383 F: Into<FA> + Copy,
384 FA: PrimeCharacteristicRing,
385 FA::PrimeSubfield: BinomiallyExtendable<{ D_EF }>,
386{
387 let p: [FA; D_EF] = ext_field_subtract(evals[1], evals[0]);
388 ext_field_add(ext_field_multiply::<FA>(p, x), evals[0])
389}
390
391pub(super) fn reduce_to_single_evaluation<F, FA>(
397 p_xi_0: [F; D_EF],
398 p_xi_1: [F; D_EF],
399 q_xi_0: [F; D_EF],
400 q_xi_1: [F; D_EF],
401 mu: [F; D_EF],
402) -> ([FA; D_EF], [FA; D_EF])
403where
404 F: Into<FA> + Copy,
405 FA: PrimeCharacteristicRing,
406 FA::PrimeSubfield: BinomiallyExtendable<{ D_EF }>,
407{
408 let numer = interpolate_linear_at_01([p_xi_0, p_xi_1], mu);
409 let denom = interpolate_linear_at_01([q_xi_0, q_xi_1], mu);
410 (numer, denom)
411}