openvm_recursion_circuit/batch_constraint/eq_airs/eq_sharp_uni/
air.rs

1use std::borrow::Borrow;
2
3use openvm_circuit_primitives::{
4    utils::{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, F};
12use p3_air::{Air, AirBuilder, BaseAir};
13use p3_field::{extension::BinomiallyExtendable, Field, PrimeCharacteristicRing};
14use p3_matrix::Matrix;
15
16use crate::{
17    batch_constraint::bus::{
18        BatchConstraintConductorBus, BatchConstraintConductorMessage,
19        BatchConstraintInnerMessageType, EqSharpUniBus, EqSharpUniMessage, EqZeroNBus,
20        EqZeroNMessage,
21    },
22    bus::{XiRandomnessBus, XiRandomnessMessage},
23    subairs::nested_for_loop::{NestedForLoopIoCols, NestedForLoopSubAir},
24    utils::{
25        base_to_ext, ext_field_add, ext_field_multiply, ext_field_multiply_scalar,
26        ext_field_one_minus, ext_field_subtract,
27    },
28};
29
30#[derive(AlignedBorrow, Clone, Copy, StructReflection)]
31#[repr(C)]
32pub struct EqSharpUniCols<T> {
33    pub is_valid: T,
34    pub is_first: T,
35    pub proof_idx: T,
36
37    pub root: T,
38    pub root_pow: T,
39    pub root_half_order: T,
40
41    pub xi_idx: T,
42    pub xi: [T; D_EF],
43    pub product_before: [T; D_EF],
44
45    pub iter_idx: T,
46    pub is_first_iter: T,
47}
48
49#[derive(ColumnsAir)]
50#[columns_via(EqSharpUniCols<u8>)]
51pub struct EqSharpUniAir {
52    pub xi_bus: XiRandomnessBus,
53    pub eq_bus: EqSharpUniBus,
54    pub batch_constraint_conductor_bus: BatchConstraintConductorBus,
55    pub l_skip: usize,
56    pub canonical_inverse_generator: F,
57}
58
59impl<F> BaseAirWithPublicValues<F> for EqSharpUniAir {}
60impl<F> PartitionedBaseAir<F> for EqSharpUniAir {}
61
62impl<F> BaseAir<F> for EqSharpUniAir {
63    fn width(&self) -> usize {
64        EqSharpUniCols::<F>::width()
65    }
66}
67
68impl<AB: AirBuilder + InteractionBuilder> Air<AB> for EqSharpUniAir
69where
70    <AB::Expr as PrimeCharacteristicRing>::PrimeSubfield: BinomiallyExtendable<{ D_EF }>,
71    AB::Expr: From<F>,
72{
73    fn eval(&self, builder: &mut AB) {
74        let main = builder.main();
75        let (local, next) = (
76            main.row_slice(0).expect("window should have two elements"),
77            main.row_slice(1).expect("window should have two elements"),
78        );
79
80        let local: &EqSharpUniCols<AB::Var> = (*local).borrow();
81        let next: &EqSharpUniCols<AB::Var> = (*next).borrow();
82
83        // Summary:
84        // - iter_idx consistency: the nested-loop sub-AIR models proof -> round -> iter loops. Here
85        //   round_idx is derived from xi_idx, and iter uses is_first=is_valid so transitions force
86        //   iter_idx += 1 within enabled rounds while allowing wrap at round boundaries. The first
87        //   valid row initializes is_first_iter and iter_idx; invalid rows force iter_idx = 0.
88        // - Root consistency: continuing iterations multiply `root_pow` by `root` while preserving
89        //   `root` and its half order; when a wrap happens, the root squares and the half order
90        //   doubles; the first row fixes `root = -1`, `root_half_order = 1`, and `root_pow = 1`,
91        //   and the final row forces the root to equal `canonical_inverse_generator`.
92        // - Xi/product consistency: the initial product equals one and `xi_idx` starts at `l_skip -
93        //   1`, wraps decrement `xi_idx` until reaching zero on the last row, and bus
94        //   communications consume and emit products based on `xi`, `1 - xi`, and `xi * root_pow`
95        //   combinations.
96
97        let local_round_idx = AB::Expr::from_usize(self.l_skip - 1) - local.xi_idx;
98        let next_round_idx = AB::Expr::from_usize(self.l_skip - 1) - next.xi_idx;
99
100        type LoopSubAir = NestedForLoopSubAir<3>;
101        LoopSubAir {}.eval(
102            builder,
103            (
104                NestedForLoopIoCols {
105                    is_enabled: local.is_valid.into(),
106                    counter: [
107                        local.proof_idx.into(),
108                        local_round_idx,
109                        local.iter_idx.into(),
110                    ],
111                    is_first: [
112                        local.is_first.into(),
113                        local.is_first_iter.into(),
114                        local.is_valid.into(),
115                    ],
116                },
117                NestedForLoopIoCols {
118                    is_enabled: next.is_valid.into(),
119                    counter: [next.proof_idx.into(), next_round_idx, next.iter_idx.into()],
120                    is_first: [
121                        next.is_first.into(),
122                        next.is_first_iter.into(),
123                        next.is_valid.into(),
124                    ],
125                },
126            ),
127        );
128
129        let local_is_last = LoopSubAir::local_is_last(local.is_valid, next.is_valid, next.is_first);
130
131        // =========================== idx consistency =============================
132        // NestedForLoopSubAir enforces proof/round/iter nested-loop shape; remaining constraints
133        // pin initialization and wrap semantics specific to this AIR.
134        builder
135            .when(local.is_first_iter)
136            .assert_zero(local.iter_idx);
137
138        // Either iter_idx becomes zero, or increases by one.
139        builder.assert_zero(next.iter_idx * (next.iter_idx - local.iter_idx - AB::Expr::ONE));
140        // iter_idx is always zero on invalid rows
141        builder
142            .when(not(local.is_valid))
143            .assert_zero(local.iter_idx);
144        // If becomes zero, then it would have become root_half_order
145        builder
146            .when(LoopSubAir::local_is_last(
147                local.is_valid,
148                next.is_valid,
149                next.is_first_iter,
150            ))
151            .assert_eq(local.iter_idx + AB::Expr::ONE, local.root_half_order);
152        // =========================== Root consistency =============================
153        // If iter_idx doesn't become zero (increases by one), then:
154        // - root_pow is multiplied by root,
155        // - root is preserved.
156        builder
157            .when(next.iter_idx)
158            .assert_eq(next.root_pow, local.root_pow * local.root);
159        builder.when(next.iter_idx).assert_eq(local.root, next.root);
160        builder
161            .when(next.iter_idx)
162            .assert_eq(local.root_half_order, next.root_half_order);
163        // Otherwise the new root is the square of the current one, and the root order doubles.
164        builder
165            .when(next.is_first_iter * not(next.is_first))
166            .assert_eq(local.root_half_order * AB::Expr::TWO, next.root_half_order);
167        builder
168            .when(next.is_first_iter * not(next.is_first))
169            .assert_eq(local.root, next.root * next.root);
170        // Important: we need to enforce dropping iter_idx to zero if root_pow is going to become
171        // one. We can leave this to inner interactions, but then we need to guarantee that
172        // the first layer has size 1.
173        builder
174            .when(next.is_valid * local.is_first)
175            .assert_one(next.is_first_iter);
176        // Also, on the first row we have some conditions
177        builder
178            .when(local.is_valid * local.is_first)
179            .assert_eq(local.root, AB::Expr::NEG_ONE);
180        builder
181            .when(local.is_valid * local.is_first)
182            .assert_eq(local.root_half_order, AB::Expr::ONE);
183        // If is_first_iter, then root_pow is 1
184        builder
185            .when(local.is_first_iter)
186            .assert_eq(local.root_pow, AB::Expr::ONE);
187        // Finally, the final root must equal some specific generator
188        builder
189            .when(local.is_valid * local_is_last.clone())
190            .assert_eq::<AB::Var, AB::Expr>(local.root, self.canonical_inverse_generator.into());
191
192        // =========================== Xi and product consistency =============================
193        // Boundary conditions
194        assert_array_eq(
195            &mut builder.when(local.is_valid * local.is_first),
196            local.product_before,
197            base_to_ext::<AB::Expr>(AB::Expr::ONE),
198        );
199        builder.when(local.is_valid * local.is_first).assert_eq(
200            local.xi_idx,
201            AB::Expr::from_usize(self.l_skip) - AB::Expr::ONE,
202        );
203        assert_array_eq(
204            &mut builder.when(LoopSubAir::local_is_transition(
205                next.is_valid,
206                next.is_first_iter,
207            )),
208            local.xi,
209            next.xi,
210        );
211        // When we drop iter_idx, xi_idx decreases
212        builder
213            .when(next.is_first_iter * not(next.is_first))
214            .assert_one(local.xi_idx - next.xi_idx);
215        // The last one must be 0
216        builder
217            .when(local_is_last.clone())
218            .assert_eq(local.xi_idx, AB::Expr::ZERO);
219
220        self.xi_bus.receive(
221            builder,
222            local.proof_idx,
223            XiRandomnessMessage {
224                idx: local.xi_idx,
225                xi: local.xi,
226            },
227            local.is_first_iter,
228        );
229        // Here idx < l_skip and all idx are different within one proof_idx
230        self.batch_constraint_conductor_bus.add_key_with_lookups(
231            builder,
232            local.proof_idx,
233            BatchConstraintConductorMessage {
234                msg_type: BatchConstraintInnerMessageType::Xi.to_field(),
235                idx: local.xi_idx.into(),
236                value: local.xi.map(|x| x.into()),
237            },
238            local.is_valid * local_is_last,
239        );
240
241        self.eq_bus.receive(
242            builder,
243            local.proof_idx,
244            EqSharpUniMessage {
245                xi_idx: local.xi_idx + AB::Expr::ONE,
246                iter_idx: local.iter_idx.into(),
247                product: local.product_before.map(|x| x.into()),
248            },
249            local.is_valid * (AB::Expr::ONE - local.is_first),
250        );
251        let second: [AB::Expr; D_EF] = ext_field_multiply_scalar(local.xi, local.root_pow);
252        let one_minus_xi: [AB::Expr; D_EF] = ext_field_one_minus(local.xi);
253
254        self.eq_bus.send(
255            builder,
256            local.proof_idx,
257            EqSharpUniMessage {
258                xi_idx: local.xi_idx.into(),
259                iter_idx: local.iter_idx.into(),
260                product: ext_field_multiply(
261                    local.product_before,
262                    ext_field_add::<AB::Expr>(one_minus_xi.clone(), second.clone()),
263                ),
264            },
265            local.is_valid,
266        );
267        self.eq_bus.send(
268            builder,
269            local.proof_idx,
270            EqSharpUniMessage {
271                xi_idx: local.xi_idx.into(),
272                iter_idx: local.iter_idx + local.root_half_order,
273                product: ext_field_multiply(
274                    local.product_before,
275                    ext_field_subtract::<AB::Expr>(one_minus_xi.clone(), second.clone()),
276                ),
277            },
278            local.is_valid,
279        );
280    }
281}
282
283#[derive(AlignedBorrow, Clone, Copy, StructReflection)]
284#[repr(C)]
285pub struct EqSharpUniReceiverCols<T> {
286    pub is_valid: T,
287    pub is_first: T,
288    pub is_last: T,
289    pub proof_idx: T,
290
291    pub idx: T,
292    pub coeff: [T; D_EF],
293    pub r: [T; D_EF],
294    pub cur_sum: [T; D_EF],
295}
296
297#[derive(ColumnsAir)]
298#[columns_via(EqSharpUniReceiverCols<u8>)]
299pub struct EqSharpUniReceiverAir {
300    pub r_bus: BatchConstraintConductorBus,
301    pub eq_bus: EqSharpUniBus,
302    pub zero_n_bus: EqZeroNBus,
303
304    pub l_skip: usize,
305}
306
307impl<F> BaseAirWithPublicValues<F> for EqSharpUniReceiverAir {}
308impl<F> PartitionedBaseAir<F> for EqSharpUniReceiverAir {}
309
310impl<F> BaseAir<F> for EqSharpUniReceiverAir {
311    fn width(&self) -> usize {
312        EqSharpUniReceiverCols::<F>::width()
313    }
314}
315
316impl<AB: AirBuilder + InteractionBuilder> Air<AB> for EqSharpUniReceiverAir
317where
318    <AB::Expr as PrimeCharacteristicRing>::PrimeSubfield: BinomiallyExtendable<{ D_EF }>,
319{
320    fn eval(&self, builder: &mut AB) {
321        let main = builder.main();
322        let (local, next) = (
323            main.row_slice(0).expect("window should have two elements"),
324            main.row_slice(1).expect("window should have two elements"),
325        );
326
327        let local: &EqSharpUniReceiverCols<AB::Var> = (*local).borrow();
328        let next: &EqSharpUniReceiverCols<AB::Var> = (*next).borrow();
329
330        // Summary:
331        // - idx consistency: start with `idx = 0` on the first row and increment by one on each
332        //   subsequent non-header row while the proof continues.
333        // - EF values consistency: keep the `r` coefficients constant across transitions and update
334        //   `cur_sum` via `coeff + r * next.cur_sum` on every step past the first.
335
336        type LoopSubAir = NestedForLoopSubAir<1>;
337        LoopSubAir {}.eval(
338            builder,
339            (
340                NestedForLoopIoCols {
341                    is_enabled: local.is_valid,
342                    counter: [local.proof_idx],
343                    is_first: [local.is_first],
344                }
345                .map_into(),
346                NestedForLoopIoCols {
347                    is_enabled: next.is_valid,
348                    counter: [next.proof_idx],
349                    is_first: [next.is_first],
350                }
351                .map_into(),
352            ),
353        );
354
355        builder.assert_bool(local.is_last);
356        let is_same_proof = next.is_valid - next.is_first;
357
358        // ============================= idx consistency ============================
359        builder.when(local.is_first).assert_zero(local.idx);
360        builder
361            .when(is_same_proof.clone())
362            .assert_one(next.idx - local.idx);
363        // ============================= EF values consistency ==========================
364        assert_array_eq(&mut builder.when(is_same_proof.clone()), next.r, local.r);
365        assert_array_eq(
366            &mut builder.when(LoopSubAir::local_is_last(
367                local.is_valid,
368                next.is_valid,
369                next.is_first,
370            )),
371            local.cur_sum,
372            local.coeff,
373        );
374        assert_array_eq(
375            &mut builder.when(is_same_proof),
376            local.cur_sum,
377            ext_field_add(local.coeff, ext_field_multiply(local.r, next.cur_sum)),
378        );
379
380        self.r_bus.lookup_key(
381            builder,
382            local.proof_idx,
383            BatchConstraintConductorMessage {
384                msg_type: BatchConstraintInnerMessageType::R.to_field(),
385                idx: AB::Expr::ZERO,
386                value: local.r.map(|x| x.into()),
387            },
388            local.is_valid * local.is_first,
389        );
390        self.eq_bus.receive(
391            builder,
392            local.proof_idx,
393            EqSharpUniMessage {
394                xi_idx: AB::Expr::ZERO,
395                iter_idx: local.idx.into(),
396                product: local.coeff.map(|x| x.into()),
397            },
398            local.is_valid,
399        );
400        let l_skip_inv: AB::Expr = AB::F::from_usize(1 << self.l_skip).inverse().into();
401        self.zero_n_bus.send(
402            builder,
403            local.proof_idx,
404            EqZeroNMessage {
405                is_sharp: AB::Expr::ONE,
406                value: local.cur_sum.map(|x| l_skip_inv.clone() * x),
407            },
408            local.is_valid * local.is_first,
409        );
410    }
411}