openvm_recursion_circuit/batch_constraint/eq_airs/eq_neg/
air.rs

1use std::borrow::Borrow;
2
3use openvm_circuit_primitives::{
4    utils::{and, assert_array_eq, not},
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;
12use p3_air::{Air, AirBuilder, BaseAir};
13use p3_field::{
14    extension::BinomiallyExtendable, PrimeCharacteristicRing, PrimeField32, TwoAdicField,
15};
16use p3_matrix::Matrix;
17
18use crate::{
19    batch_constraint::bus::{EqNegInternalBus, EqNegInternalMessage},
20    bus::{
21        EqNegBaseRandBus, EqNegBaseRandMessage, EqNegResultBus, EqNegResultMessage, SelUniBus,
22        SelUniBusMessage,
23    },
24    subairs::nested_for_loop::{NestedForLoopIoCols, NestedForLoopSubAir},
25    utils::{
26        base_to_ext, ext_field_add, ext_field_add_scalar, ext_field_multiply,
27        ext_field_multiply_scalar, ext_field_subtract,
28    },
29};
30
31#[repr(C)]
32#[derive(AlignedBorrow, Debug, StructReflection)]
33pub struct EqNegCols<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    // Negative hypercube of the current section + section row index
41    pub neg_hypercube: F,
42    pub neg_hypercube_nz_inv: F,
43    pub row_index: F,
44    pub is_first_hypercube: F,
45    pub is_last_hypercube: F,
46
47    // Value of u^{2^row}, r'^{2^row}, and (r' * omega)^{2^row}, where
48    // r' = r^{2^neg_hypercube}
49    pub u_pow: [F; D_EF],
50    pub r_pow: [F; D_EF],
51    pub r_omega_pow: [F; D_EF],
52
53    // Running product of (u^{2^i} + r'^{2^i}) from i to row_index
54    pub prod_u_r: [F; D_EF],
55    pub prod_u_r_omega: [F; D_EF],
56    pub prod_1_r: [F; D_EF],
57    pub prod_1_r_omega: [F; D_EF],
58    pub sel_first_count: F,
59    pub sel_last_trans_count: F,
60    pub one_half_pow: F,
61}
62
63#[derive(ColumnsAir)]
64#[columns_via(EqNegCols<u8>)]
65pub struct EqNegAir {
66    pub result_bus: EqNegResultBus,
67    pub base_rand_bus: EqNegBaseRandBus,
68    pub internal_bus: EqNegInternalBus,
69    pub sel_uni_bus: SelUniBus,
70    pub l_skip: usize,
71}
72
73impl<F> BaseAirWithPublicValues<F> for EqNegAir {}
74impl<F> PartitionedBaseAir<F> for EqNegAir {}
75impl<F> BaseAir<F> for EqNegAir {
76    fn width(&self) -> usize {
77        EqNegCols::<F>::width()
78    }
79}
80
81impl<AB: AirBuilder + InteractionBuilder> Air<AB> for EqNegAir
82where
83    AB::F: PrimeField32 + TwoAdicField,
84    <AB::Expr as PrimeCharacteristicRing>::PrimeSubfield: BinomiallyExtendable<{ D_EF }>,
85{
86    fn eval(&self, builder: &mut AB) {
87        let main = builder.main();
88        let (local, next) = (
89            main.row_slice(0).expect("window should have two elements"),
90            main.row_slice(1).expect("window should have two elements"),
91        );
92
93        let local: &EqNegCols<AB::Var> = (*local).borrow();
94        let next: &EqNegCols<AB::Var> = (*next).borrow();
95
96        NestedForLoopSubAir::<3> {}.eval(
97            builder,
98            (
99                NestedForLoopIoCols {
100                    is_enabled: local.is_valid.into(),
101                    counter: [
102                        local.proof_idx.into(),
103                        local.neg_hypercube.into(),
104                        local.row_index.into(),
105                    ],
106                    is_first: [
107                        local.is_first.into(),
108                        local.is_first_hypercube.into(),
109                        local.is_valid.into(),
110                    ],
111                },
112                NestedForLoopIoCols {
113                    is_enabled: next.is_valid.into(),
114                    counter: [
115                        next.proof_idx.into(),
116                        next.neg_hypercube.into(),
117                        next.row_index.into(),
118                    ],
119                    is_first: [
120                        next.is_first.into(),
121                        next.is_first_hypercube.into(),
122                        next.is_valid.into(),
123                    ],
124                },
125            ),
126        );
127
128        // Constrain is_last and is_last_hypercube: these are kept as columns (not derived
129        // inline) because they appear as next.is_last / next.is_last_hypercube in bus
130        // multiplicities below.
131        type LoopSubAir = NestedForLoopSubAir<3>;
132        builder.assert_eq(
133            local.is_last,
134            LoopSubAir::local_is_last(local.is_valid, next.is_valid, next.is_first),
135        );
136        builder.assert_eq(
137            local.is_last_hypercube,
138            LoopSubAir::local_is_last(local.is_valid, next.is_valid, next.is_first_hypercube),
139        );
140        builder.when(local.is_last).assert_one(local.is_valid);
141        builder
142            .when(local.is_last_hypercube)
143            .assert_one(local.is_valid);
144
145        /*
146         * Constrain that neg_hypercube dimension starts at 0 and increments, and
147         * that row_idx increments by 1 in each neg_hypercube section. Additionally,
148         * each section should have exactly l_skip - neg_hypercube + 1 rows.
149         */
150        builder
151            .when(local.is_first)
152            .assert_zero(local.neg_hypercube);
153        builder
154            .when(local.is_last)
155            .assert_eq(local.neg_hypercube, AB::F::from_usize(self.l_skip - 1));
156        builder
157            .when(local.is_first_hypercube)
158            .assert_zero(local.row_index);
159
160        builder.when(local.is_last_hypercube).assert_eq(
161            local.row_index,
162            AB::Expr::from_usize(self.l_skip) - local.neg_hypercube,
163        );
164
165        builder
166            .when(local.is_valid - local.is_last_hypercube)
167            .assert_one(next.row_index - local.row_index);
168        builder
169            .when(local.is_valid - local.is_last_hypercube)
170            .assert_eq(next.neg_hypercube, local.neg_hypercube);
171
172        /*
173         * Receive u_0 and r_0 from the AIRs that sample them on the first row. Also,
174         * send (u, r^2, (r * omega)^2) on the first row of non-last hypercubes and
175         * receive (u, r, r * omega) on the first row of non-first hypercubes.
176         */
177        let initial_omega = AB::F::two_adic_generator(self.l_skip);
178        assert_array_eq(
179            &mut builder.when(local.is_first),
180            local.r_omega_pow,
181            ext_field_multiply_scalar(local.r_pow, initial_omega),
182        );
183
184        self.base_rand_bus.receive(
185            builder,
186            local.proof_idx,
187            EqNegBaseRandMessage {
188                u: local.u_pow,
189                r: local.r_pow,
190            },
191            local.is_first,
192        );
193
194        self.internal_bus.send(
195            builder,
196            local.proof_idx,
197            EqNegInternalMessage {
198                neg_n: local.neg_hypercube + AB::F::ONE,
199                u: local.u_pow.map(Into::into),
200                r: ext_field_multiply(local.r_pow, local.r_pow),
201                r_omega: ext_field_multiply(local.r_omega_pow, local.r_omega_pow),
202            },
203            and(local.is_first_hypercube, not(next.is_last)),
204        );
205
206        self.internal_bus.receive(
207            builder,
208            local.proof_idx,
209            EqNegInternalMessage {
210                neg_n: local.neg_hypercube,
211                u: local.u_pow,
212                r: local.r_pow,
213                r_omega: local.r_omega_pow,
214            },
215            and(local.is_first_hypercube, not(local.is_first)),
216        );
217
218        // Constrain one_half_pow
219        builder
220            .when(local.is_first_hypercube)
221            .assert_one(local.one_half_pow * AB::Expr::TWO);
222        builder
223            .when(local.is_valid - local.is_last_hypercube)
224            .assert_eq(next.one_half_pow * AB::Expr::TWO, local.one_half_pow);
225
226        /*
227         * Constrain the running product of (u^{2^i} + r'^{2^i})
228         */
229        assert_array_eq(
230            &mut builder.when(local.is_first_hypercube),
231            local.prod_u_r,
232            ext_field_multiply(local.u_pow, ext_field_add(local.u_pow, local.r_pow)),
233        );
234
235        assert_array_eq(
236            &mut builder.when(local.is_first_hypercube),
237            local.prod_1_r,
238            ext_field_add(local.r_pow, base_to_ext::<AB::Expr>(AB::F::ONE)),
239        );
240
241        assert_array_eq(
242            &mut builder.when(local.is_valid - local.is_last_hypercube),
243            ext_field_multiply(local.u_pow, local.u_pow),
244            next.u_pow,
245        );
246
247        assert_array_eq(
248            &mut builder.when(local.is_valid - local.is_last_hypercube),
249            ext_field_multiply(local.r_pow, local.r_pow),
250            next.r_pow,
251        );
252
253        assert_array_eq(
254            &mut builder.when(local.is_valid - local.is_last_hypercube),
255            ext_field_multiply(local.prod_u_r, ext_field_add(next.u_pow, next.r_pow)),
256            next.prod_u_r,
257        );
258
259        assert_array_eq(
260            &mut builder.when(local.is_valid - local.is_last_hypercube),
261            ext_field_multiply(
262                local.prod_1_r,
263                ext_field_add(next.r_pow, base_to_ext::<AB::Expr>(AB::F::ONE)),
264            ),
265            next.prod_1_r,
266        );
267
268        /*
269         * Constrain the running product that is used to compute eq_0(u, r * omega).
270         */
271        assert_array_eq(
272            &mut builder.when(local.is_first_hypercube),
273            local.prod_u_r_omega,
274            ext_field_multiply(local.u_pow, ext_field_add(local.u_pow, local.r_omega_pow)),
275        );
276        assert_array_eq(
277            &mut builder.when(local.is_first_hypercube),
278            local.prod_1_r_omega,
279            ext_field_add_scalar(local.r_omega_pow, AB::Expr::ONE),
280        );
281
282        assert_array_eq(
283            &mut builder.when(local.is_valid - local.is_last_hypercube),
284            ext_field_multiply(local.r_omega_pow, local.r_omega_pow),
285            next.r_omega_pow,
286        );
287
288        assert_array_eq(
289            &mut builder.when(local.is_valid - local.is_last_hypercube),
290            ext_field_multiply(
291                local.prod_u_r_omega,
292                ext_field_add(next.u_pow, next.r_omega_pow),
293            ),
294            next.prod_u_r_omega,
295        );
296        assert_array_eq(
297            &mut builder.when(local.is_valid - local.is_last_hypercube),
298            ext_field_multiply(
299                local.prod_1_r_omega,
300                ext_field_add_scalar(next.r_omega_pow, AB::Expr::ONE),
301            ),
302            next.prod_1_r_omega,
303        );
304
305        self.sel_uni_bus.add_key_with_lookups(
306            builder,
307            local.proof_idx,
308            SelUniBusMessage {
309                n: -local.neg_hypercube.into(),
310                is_first: AB::Expr::ONE,
311                value: local.prod_1_r.map(|x| x * local.one_half_pow),
312            },
313            next.is_last_hypercube * next.sel_first_count,
314        );
315        self.sel_uni_bus.add_key_with_lookups(
316            builder,
317            local.proof_idx,
318            SelUniBusMessage {
319                n: -local.neg_hypercube.into(),
320                is_first: AB::Expr::ZERO,
321                value: local.prod_1_r_omega.map(|x| x * local.one_half_pow),
322            },
323            next.is_last_hypercube * next.sel_last_trans_count,
324        );
325
326        // This is kind of ugly. But we use the first row as the lookup table for
327        // selector for log_height=0.
328        self.sel_uni_bus.add_key_with_lookups(
329            builder,
330            local.proof_idx,
331            SelUniBusMessage {
332                n: -AB::Expr::from_usize(self.l_skip),
333                is_first: AB::Expr::ONE,
334                value: [
335                    AB::Expr::ONE,
336                    AB::Expr::ZERO,
337                    AB::Expr::ZERO,
338                    AB::Expr::ZERO,
339                ],
340            },
341            local.is_first * local.sel_first_count,
342        );
343        self.sel_uni_bus.add_key_with_lookups(
344            builder,
345            local.proof_idx,
346            SelUniBusMessage {
347                n: -AB::Expr::from_usize(self.l_skip),
348                is_first: AB::Expr::ZERO,
349                value: [
350                    AB::Expr::ONE,
351                    AB::Expr::ZERO,
352                    AB::Expr::ZERO,
353                    AB::Expr::ZERO,
354                ],
355            },
356            local.is_first * local.sel_last_trans_count,
357        );
358
359        /*
360         * Compute eq_n(u, r) and k_rot_n(u, r), and send them to EqBaseAir (without
361         * the omega_pow_inv).
362         */
363        let eq = ext_field_add_scalar::<AB::Expr>(
364            ext_field_subtract(local.prod_u_r, next.u_pow),
365            AB::F::ONE,
366        );
367        let k_rot = ext_field_add_scalar::<AB::Expr>(
368            ext_field_subtract(local.prod_u_r_omega, next.u_pow),
369            AB::F::ONE,
370        );
371
372        let is_neg = local.neg_hypercube * local.neg_hypercube_nz_inv;
373        builder.when(local.neg_hypercube).assert_one(is_neg.clone());
374        self.result_bus.send(
375            builder,
376            local.proof_idx,
377            EqNegResultMessage {
378                n: AB::Expr::ZERO - local.neg_hypercube,
379                eq,
380                k_rot,
381            },
382            is_neg * next.is_last_hypercube,
383        );
384    }
385}