openvm_recursion_circuit/stacking/opening/
air.rs

1use std::borrow::Borrow;
2
3use openvm_circuit_primitives::{
4    utils::{and, assert_array_eq, not, or},
5    ColumnsAir, StructReflection, StructReflectionHelper, SubAir,
6};
7use openvm_recursion_circuit_derive::AlignedBorrow;
8use openvm_stark_backend::{
9    interaction::InteractionBuilder, BaseAirWithPublicValues, PartitionedBaseAir,
10};
11use openvm_stark_sdk::config::baby_bear_poseidon2::{D_EF, F};
12use p3_air::{Air, AirBuilder, BaseAir};
13use p3_field::{extension::BinomiallyExtendable, PrimeCharacteristicRing, PrimeField32};
14use p3_matrix::Matrix;
15
16use crate::{
17    bus::{
18        AirShapeBus, AirShapeBusMessage, AirShapeProperty, ColumnClaimsBus, ColumnClaimsMessage,
19        LiftedHeightsBus, LiftedHeightsBusMessage, StackingModuleBus, StackingModuleMessage,
20        TranscriptBus, TranscriptBusMessage,
21    },
22    stacking::bus::{
23        ClaimCoefficientsBus, ClaimCoefficientsMessage, EqBitsLookupBus, EqBitsLookupMessage,
24        EqKernelLookupBus, EqKernelLookupMessage, StackingModuleTidxBus, StackingModuleTidxMessage,
25        SumcheckClaimsBus, SumcheckClaimsMessage,
26    },
27    subairs::nested_for_loop::{NestedForLoopIoCols, NestedForLoopSubAir},
28    utils::{assert_one_ext, ext_field_add, ext_field_multiply, ext_field_multiply_scalar},
29};
30
31#[repr(C)]
32#[derive(AlignedBorrow, StructReflection)]
33pub struct OpeningClaimsCols<F> {
34    // Proof index columns for continuations
35    pub proof_idx: F,
36    pub is_valid: F,
37    pub is_first: F,
38    pub is_last: F,
39
40    // Received from batch constraints module
41    pub sort_idx: F,
42    pub part_idx: F,
43    pub col_idx: F,
44    pub col_claim: [F; D_EF],
45    pub rot_claim: [F; D_EF],
46    pub need_rot: F,
47
48    // Used to constrain the order of received messages
49    pub is_main: F,
50    pub is_transition_main: F,
51
52    // From proof shape (n, n_lift + l_skip, 2^{n_lift + l_skip}, 2^{- (n_lift + l_skip)})
53    pub hypercube_dim: F,
54    pub log_lifted_height: F,
55    pub lifted_height: F,
56    pub lifted_height_inv: F,
57
58    // Sampled transcript values
59    pub tidx: F,
60    pub lambda: [F; D_EF],
61    pub lambda_pow: [F; D_EF],
62
63    // Location in stacked matrices
64    pub commit_idx: F,
65    pub stacked_col_idx: F,
66    pub row_idx: F,
67    pub is_last_for_claim: F,
68
69    // Lookups to compute claim coefficient
70    pub eq_in: [F; D_EF],
71    pub k_rot_in: [F; D_EF],
72    pub eq_bits: [F; D_EF],
73
74    // This is either `k_rot_in` or zero, depending on `need_rot`
75    pub k_rot_in_when_needed: [F; D_EF],
76
77    // Intermediate values to compute claim coefficient
78    pub lambda_pow_eq_bits: [F; D_EF],
79
80    // Stacking claim coefficient to be sent
81    pub stacking_claim_coefficient: [F; D_EF],
82
83    // RLC of column claims * coefficient using lambda
84    pub s_0: [F; D_EF],
85}
86
87#[derive(ColumnsAir)]
88#[columns_via(OpeningClaimsCols<u8>)]
89pub struct OpeningClaimsAir {
90    // External buses
91    pub lifted_heights_bus: LiftedHeightsBus,
92    pub stacking_module_bus: StackingModuleBus,
93    pub column_claims_bus: ColumnClaimsBus,
94    pub transcript_bus: TranscriptBus,
95    pub air_shape_bus: AirShapeBus,
96
97    // Internal buses
98    pub stacking_tidx_bus: StackingModuleTidxBus,
99    pub claim_coefficients_bus: ClaimCoefficientsBus,
100    pub sumcheck_claims_bus: SumcheckClaimsBus,
101    pub eq_kernel_lookup_bus: EqKernelLookupBus,
102    pub eq_bits_lookup_bus: EqBitsLookupBus,
103
104    // Other fields
105    pub n_stack: usize,
106    pub l_skip: usize,
107}
108
109impl BaseAirWithPublicValues<F> for OpeningClaimsAir {}
110impl PartitionedBaseAir<F> for OpeningClaimsAir {}
111
112impl<F> BaseAir<F> for OpeningClaimsAir {
113    fn width(&self) -> usize {
114        OpeningClaimsCols::<F>::width()
115    }
116}
117
118impl<AB: AirBuilder + InteractionBuilder> Air<AB> for OpeningClaimsAir
119where
120    AB::F: PrimeField32,
121    <AB::Expr as PrimeCharacteristicRing>::PrimeSubfield: BinomiallyExtendable<{ D_EF }>,
122{
123    fn eval(&self, builder: &mut AB) {
124        let main = builder.main();
125        let (local, next) = (
126            main.row_slice(0).expect("window should have two elements"),
127            main.row_slice(1).expect("window should have two elements"),
128        );
129
130        let local: &OpeningClaimsCols<AB::Var> = (*local).borrow();
131        let next: &OpeningClaimsCols<AB::Var> = (*next).borrow();
132
133        NestedForLoopSubAir::<1> {}.eval(
134            builder,
135            (
136                NestedForLoopIoCols {
137                    is_enabled: local.is_valid,
138                    counter: [local.proof_idx],
139                    is_first: [local.is_first],
140                }
141                .map_into(),
142                NestedForLoopIoCols {
143                    is_enabled: next.is_valid,
144                    counter: [next.proof_idx],
145                    is_first: [next.is_first],
146                }
147                .map_into(),
148            ),
149        );
150
151        builder.when(local.is_valid).assert_eq(
152            local.is_last,
153            NestedForLoopSubAir::<1>::local_is_last(local.is_valid, next.is_valid, next.is_first),
154        );
155
156        builder.assert_bool(local.is_last);
157        builder.assert_bool(local.need_rot);
158        builder
159            .when(and(local.is_valid, local.is_last))
160            .assert_zero((local.proof_idx + AB::F::ONE - next.proof_idx) * next.proof_idx);
161        builder
162            .when(and(not(local.is_valid), local.is_last))
163            .assert_zero(next.proof_idx);
164
165        /*
166         * Constrain the sortedness of each ColumnClaimsMessage. Main claims (i.e part_idx = 0)
167         * should be first and sorted by sort_idx and then col_idx. The remaining claims should
168         * be sorted by sort_idx, then part_idx, and finally col_idx. Note that each proof must
169         * have at least one main claim.
170         */
171        builder.assert_bool(local.is_main);
172        builder.when(local.is_main).assert_one(local.is_valid);
173        builder.when(local.is_main).assert_zero(local.part_idx);
174        builder.when(local.is_main).assert_zero(local.commit_idx);
175
176        builder.when(local.is_first).assert_one(local.is_main);
177        builder.when(local.is_first).assert_zero(local.sort_idx);
178        builder.when(local.is_first).assert_zero(local.col_idx);
179
180        builder.assert_bool(local.is_transition_main);
181        builder
182            .when(local.is_transition_main)
183            .assert_eq(local.is_main, next.is_main);
184        builder
185            .when(local.is_transition_main)
186            .assert_one(and(local.is_valid, next.is_valid));
187        builder
188            .when(and::<AB::Expr>(
189                and(local.is_main, next.is_main),
190                not(local.is_last),
191            ))
192            .assert_one(local.is_transition_main);
193        builder
194            .when(and(
195                and::<AB::Expr>(not(local.is_main), not(next.is_main)),
196                next.is_valid,
197            ))
198            .assert_one(local.is_transition_main);
199        builder
200            .when(local.is_transition_main)
201            .assert_zero(local.is_last);
202        builder
203            .when(and(not(local.is_main), next.is_main))
204            .assert_one(local.is_last);
205
206        let mut when_both_main = builder.when(and(local.is_main, local.is_transition_main));
207        when_both_main.assert_bool(next.sort_idx - local.sort_idx);
208        when_both_main
209            .when_ne(local.sort_idx, next.sort_idx)
210            .assert_zero(next.col_idx);
211        when_both_main
212            .when_ne(local.sort_idx + AB::F::ONE, next.sort_idx)
213            .assert_one(next.col_idx - local.col_idx);
214
215        let mut when_last_main = builder.when(and(local.is_main, not(local.is_transition_main)));
216        when_last_main.assert_zero((next.part_idx - AB::F::ONE) * not(local.is_last));
217        when_last_main.assert_zero(next.col_idx * not(local.is_last));
218
219        /*
220         * Note that we utilize the LiftedHeightsBus interaction to constrain the (sort_idx,
221         * part_idx) sorting for non-main commits. Each non-zero commit_idx is constrained to
222         * its exact sort_idx and part_idx, so we only need to constrain that (a) commit_idx
223         * increases by 0/1 within a proof and (b) col_idx increases by 1 within a commit and
224         * is reset between commits.
225         */
226        builder
227            .when(and(local.is_valid, not(local.is_last)))
228            .assert_bool(next.commit_idx - local.commit_idx);
229        builder
230            .when(and(local.is_transition_main, not(local.is_main)))
231            .when_ne(local.commit_idx + AB::F::ONE, next.commit_idx)
232            .assert_one(next.col_idx - local.col_idx);
233        builder
234            .when(and(local.is_transition_main, not(local.is_main)))
235            .when_ne(local.commit_idx, next.commit_idx)
236            .assert_zero(next.col_idx);
237
238        /*
239         * Compute col_claim[0] + lambda * rot_claim + ... (i.e. RLC of column/rotation claims)
240         * and sent it to UnivariateRoundAir, which will constrain that the RLC is equal to the
241         * sum of the proof's s_0 polynomial evaluated at each z in D.
242         */
243        self.column_claims_bus.send(
244            builder,
245            local.proof_idx,
246            ColumnClaimsMessage {
247                sort_idx: local.sort_idx.into(),
248                part_idx: local.part_idx.into(),
249                col_idx: local.col_idx.into(),
250                claim: local.col_claim.map(Into::into),
251                is_rot: AB::Expr::ZERO,
252            },
253            local.is_valid,
254        );
255
256        for i in 0..D_EF {
257            builder
258                .when(and(local.is_valid, not(local.need_rot)))
259                .assert_zero(local.rot_claim[i]);
260        }
261        self.column_claims_bus.send(
262            builder,
263            local.proof_idx,
264            ColumnClaimsMessage {
265                sort_idx: local.sort_idx.into(),
266                part_idx: local.part_idx.into(),
267                col_idx: local.col_idx.into(),
268                claim: local.rot_claim.map(Into::into),
269                is_rot: AB::Expr::ONE,
270            },
271            local.is_valid * local.need_rot,
272        );
273
274        assert_array_eq(
275            &mut builder.when(not(local.is_last)),
276            local.lambda,
277            next.lambda,
278        );
279
280        assert_array_eq(
281            &mut builder.when(not(local.is_last)),
282            ext_field_multiply(
283                ext_field_multiply(local.lambda, local.lambda),
284                local.lambda_pow,
285            ),
286            next.lambda_pow,
287        );
288
289        assert_one_ext(&mut builder.when(local.is_first), local.lambda_pow);
290
291        assert_array_eq(
292            &mut builder.when(local.is_first),
293            ext_field_add::<AB::Expr>(
294                local.col_claim,
295                ext_field_multiply(local.lambda, local.rot_claim),
296            ),
297            local.s_0,
298        );
299
300        assert_array_eq(
301            &mut builder.when(not(local.is_last)),
302            ext_field_add(
303                local.s_0,
304                ext_field_add::<AB::Expr>(
305                    ext_field_multiply(next.lambda_pow, next.col_claim),
306                    ext_field_multiply(
307                        ext_field_multiply(next.lambda_pow, next.lambda),
308                        next.rot_claim,
309                    ),
310                ),
311            ),
312            next.s_0,
313        );
314
315        self.sumcheck_claims_bus.send(
316            builder,
317            local.proof_idx,
318            SumcheckClaimsMessage {
319                module_idx: AB::Expr::ZERO,
320                value: local.s_0.map(Into::into),
321            },
322            and(local.is_last, local.is_valid),
323        );
324
325        /*
326         * Compute coefficients for stacking commits and send them to StackingClaimsAir. We do
327         * this by mapping each (sort_idx, part_idx, col_idx) to their (commit_idx, col_idx,
328         * row_idx) tuple. We can compute this tuple by recording the start and end point of
329         * each trace slice in the commit matrix. Note that we assume that column claims are
330         * properly ordered to achieve this.
331         */
332        builder.when(local.is_first).assert_zero(local.commit_idx);
333        builder
334            .when(local.is_first)
335            .assert_zero(local.stacked_col_idx);
336        builder.when(local.is_first).assert_zero(local.row_idx);
337
338        builder
339            .when(not(local.is_last))
340            .assert_bool(next.commit_idx - local.commit_idx);
341        builder
342            .when(next.commit_idx - local.commit_idx)
343            .assert_zero(next.stacked_col_idx);
344
345        builder
346            .when(and::<AB::Expr>(
347                not(local.is_last),
348                not::<AB::Expr>(next.commit_idx - local.commit_idx),
349            ))
350            .assert_bool(next.stacked_col_idx - local.stacked_col_idx);
351        builder
352            .when(and(local.is_last_for_claim, not(local.is_last)))
353            .assert_one(or::<AB::Expr>(
354                next.commit_idx - local.commit_idx,
355                next.stacked_col_idx - local.stacked_col_idx,
356            ));
357
358        builder.assert_bool(local.is_last_for_claim);
359        builder
360            .when(next.commit_idx - local.commit_idx)
361            .assert_one(local.is_last_for_claim);
362        builder
363            .when(next.stacked_col_idx - local.stacked_col_idx)
364            .assert_one(local.is_last_for_claim);
365        builder
366            .when(and(local.is_valid, local.is_last))
367            .assert_one(local.is_last_for_claim);
368
369        builder
370            .when(local.is_last_for_claim)
371            .assert_zero(next.row_idx);
372        builder
373            .when(not::<AB::Expr>(local.is_last_for_claim))
374            .assert_eq(local.row_idx + local.lifted_height, next.row_idx);
375
376        assert_array_eq(
377            builder,
378            ext_field_multiply(local.lambda_pow, local.eq_bits),
379            local.lambda_pow_eq_bits,
380        );
381
382        assert_array_eq(
383            builder,
384            local.k_rot_in_when_needed,
385            ext_field_multiply_scalar(local.k_rot_in, local.need_rot),
386        );
387
388        assert_array_eq(
389            &mut builder.when(local.is_first),
390            ext_field_multiply(
391                local.lambda_pow_eq_bits,
392                ext_field_add::<AB::Expr>(
393                    local.eq_in,
394                    ext_field_multiply(local.lambda, local.k_rot_in_when_needed),
395                ),
396            ),
397            local.stacking_claim_coefficient,
398        );
399
400        assert_array_eq(
401            &mut builder.when(local.is_last_for_claim),
402            ext_field_multiply(
403                next.lambda_pow_eq_bits,
404                ext_field_add::<AB::Expr>(
405                    next.eq_in,
406                    ext_field_multiply(next.lambda, next.k_rot_in_when_needed),
407                ),
408            ),
409            next.stacking_claim_coefficient,
410        );
411
412        assert_array_eq(
413            &mut builder.when(not::<AB::Expr>(local.is_last_for_claim)),
414            ext_field_add(
415                local.stacking_claim_coefficient,
416                ext_field_multiply(
417                    next.lambda_pow_eq_bits,
418                    ext_field_add::<AB::Expr>(
419                        next.eq_in,
420                        ext_field_multiply(next.lambda, next.k_rot_in_when_needed),
421                    ),
422                ),
423            ),
424            next.stacking_claim_coefficient,
425        );
426
427        self.claim_coefficients_bus.send(
428            builder,
429            local.proof_idx,
430            ClaimCoefficientsMessage {
431                commit_idx: local.commit_idx,
432                stacked_col_idx: local.stacked_col_idx,
433                coefficient: local.stacking_claim_coefficient,
434            },
435            and(local.is_valid, local.is_last_for_claim),
436        );
437
438        /*
439         * Constrain that each non-terminal is_last_for_claim wrap corresponds to the
440         * stacked column being filled, i.e. local.row_idx + local.lifted_height ==
441         * 2^{n_stack + l_skip}. The converse is implicit, from the interaction with
442         * EqBitsAir we must have row_idx < 2^{num_bits + log_lifted_height}, which
443         * is 2^{n_stack + l_skip}.
444         */
445        builder
446            .when(and(local.is_valid, not(local.is_last_for_claim)))
447            .assert_one(next.is_valid);
448        builder
449            .when(and::<AB::Expr>(
450                and(local.is_last_for_claim, not(local.is_last)),
451                not::<AB::Expr>(next.commit_idx - local.commit_idx),
452            ))
453            .assert_eq(
454                local.row_idx + local.lifted_height,
455                AB::F::from_usize(1 << (self.n_stack + self.l_skip)),
456            );
457
458        /*
459         * Constrain correctness of lookup values via interactions. Heights are received
460         * from ProofShapeAir, while eq(u, r), k_rot(u, r), and eq_>(u, b) values are
461         * computed and provided via lookup by other stacking module AIRs.
462         */
463        self.lifted_heights_bus.lookup_key(
464            builder,
465            local.proof_idx,
466            LiftedHeightsBusMessage {
467                sort_idx: local.sort_idx,
468                part_idx: local.part_idx,
469                commit_idx: local.commit_idx,
470                hypercube_dim: local.hypercube_dim,
471                lifted_height: local.lifted_height,
472                log_lifted_height: local.log_lifted_height,
473            },
474            local.is_valid,
475        );
476
477        self.eq_kernel_lookup_bus.lookup_key(
478            builder,
479            local.proof_idx,
480            EqKernelLookupMessage {
481                n: local.hypercube_dim,
482                eq_in: local.eq_in,
483                k_rot_in: local.k_rot_in,
484            },
485            local.is_valid,
486        );
487
488        builder
489            .when(local.is_valid)
490            .assert_one(local.lifted_height * local.lifted_height_inv);
491
492        self.eq_bits_lookup_bus.lookup_key(
493            builder,
494            local.proof_idx,
495            EqBitsLookupMessage {
496                b_value: local.row_idx
497                    * local.lifted_height_inv
498                    * AB::F::from_usize(1 << self.l_skip),
499                num_bits: AB::Expr::from_usize(self.n_stack + self.l_skip)
500                    - local.log_lifted_height,
501                eval: local.eq_bits.map(Into::into),
502            },
503            local.is_valid,
504        );
505
506        self.air_shape_bus.lookup_key(
507            builder,
508            local.proof_idx,
509            AirShapeBusMessage {
510                sort_idx: local.sort_idx.into(),
511                property_idx: AirShapeProperty::NeedRot.to_field(),
512                value: local.need_rot.into(),
513            },
514            local.is_valid,
515        );
516
517        /*
518         * Constrain transcript operations and send the final tidx to UnivariateRoundAir.
519         */
520        self.stacking_module_bus.receive(
521            builder,
522            local.proof_idx,
523            StackingModuleMessage {
524                tidx: local.tidx.into(),
525            },
526            and(local.is_first, local.is_valid),
527        );
528
529        builder
530            .when(and(local.is_valid, not(local.is_last)))
531            .assert_eq(local.tidx + AB::Expr::from_usize(2 * D_EF), next.tidx);
532
533        for i in 0..D_EF {
534            self.transcript_bus.receive(
535                builder,
536                local.proof_idx,
537                TranscriptBusMessage {
538                    tidx: local.tidx + AB::Expr::from_usize(i),
539                    value: local.col_claim[i].into(),
540                    is_sample: AB::Expr::ZERO,
541                },
542                local.is_valid,
543            );
544
545            self.transcript_bus.receive(
546                builder,
547                local.proof_idx,
548                TranscriptBusMessage {
549                    tidx: local.tidx + AB::Expr::from_usize(D_EF + i),
550                    value: local.rot_claim[i].into(),
551                    is_sample: AB::Expr::ZERO,
552                },
553                local.is_valid,
554            );
555
556            self.transcript_bus.receive(
557                builder,
558                local.proof_idx,
559                TranscriptBusMessage {
560                    tidx: AB::Expr::from_usize(2 * D_EF + i) + local.tidx,
561                    value: local.lambda[i].into(),
562                    is_sample: AB::Expr::ONE,
563                },
564                and(local.is_last, local.is_valid),
565            );
566        }
567
568        self.stacking_tidx_bus.send(
569            builder,
570            local.proof_idx,
571            StackingModuleTidxMessage {
572                module_idx: AB::Expr::ZERO,
573                tidx: local.tidx + AB::Expr::from_usize(3 * D_EF),
574            },
575            and(local.is_last, local.is_valid),
576        );
577    }
578}