Skip to main content

zinc_test_uair/
lib.rs

1#![allow(clippy::arithmetic_side_effects)] // UAIRs should not care about overflows
2mod generate_trace;
3
4pub use generate_trace::*;
5
6use crypto_primitives::{ConstSemiring, FixedConfig, Semiring, SemiringConfig, boolean::Boolean};
7use num_traits::Zero;
8use rand::{
9    distr::{Distribution, StandardUniform},
10    prelude::*,
11};
12use std::marker::PhantomData;
13use zinc_poly::{
14    EvaluatablePolynomial,
15    mle::{DenseMultilinearExtension, MultilinearExtensionRand},
16    univariate::{
17        binary::BinaryPoly,
18        dense::DensePolynomial,
19        dynamic::{DynamicPolynomial, HasDynamicPolynomialConfig},
20    },
21};
22use zinc_uair::{
23    AffineVirtualSpec, AffineVirtualTerm, BitOp, BitOpSpec, ConstraintBuilder, PublicColumnLayout,
24    ShiftSpec, TotalColumnLayout, TraceRow, Uair, UairSignature, UairTrace,
25    ideal::{DegreeOneIdeal, ImpossibleIdeal},
26};
27use zinc_utils::from_ref::FromRef;
28
29#[derive(Clone, Debug)]
30pub struct TestUairSimpleMultiplication<R, P>(PhantomData<(R, P)>);
31
32impl<R, P> Uair for TestUairSimpleMultiplication<R, P>
33where
34    R: Semiring + 'static,
35    P: Semiring + 'static,
36{
37    type Ideal = ImpossibleIdeal; // Not used
38    type FqIdeal = ImpossibleIdeal;
39    type Scalar = DensePolynomial<R, 32>;
40    type Prime = P;
41
42    fn signature() -> UairSignature<Self::Prime> {
43        let total = TotalColumnLayout::new(0, 3, 0);
44        let shifts = (0..3).map(|i| ShiftSpec::new(i, 1)).collect();
45        UairSignature::new(total, PublicColumnLayout::default(), shifts, vec![])
46    }
47
48    fn constrain_general<C, B, FromR, MulByScalar, IFromR, IFqFromR>(
49        b: &mut B,
50        expr_cfg: &C,
51        up: TraceRow<C::Element>,
52        down: TraceRow<C::Element>,
53        _from_ref: FromR,
54        _mbs: MulByScalar,
55        _ideal_from_ref: IFromR,
56        _fq_ideal_from_ref: IFqFromR,
57    ) where
58        C: SemiringConfig,
59        B: ConstraintBuilder<Expr = C::Element>,
60    {
61        let up = up.arbitrary_poly;
62        let down = down.arbitrary_poly;
63
64        b.assert_zero(expr_cfg.sub(&expr_cfg.mul(&up[0], &up[1]), &down[0]));
65        b.assert_zero(expr_cfg.sub(&expr_cfg.mul(&up[1], &up[2]), &down[1]));
66        b.assert_zero(expr_cfg.sub(&expr_cfg.mul(&up[0], &up[2]), &down[2]));
67    }
68}
69
70impl<R, P> GenerateRandomTrace<32> for TestUairSimpleMultiplication<R, P>
71where
72    R: Semiring + From<i8> + 'static,
73    P: Semiring + 'static,
74    StandardUniform: Distribution<R>,
75{
76    type PolyCoeff = R;
77    type Int = R;
78
79    fn generate_random_trace<G: Rng + ?Sized>(
80        num_vars: usize,
81        rng: &mut G,
82    ) -> UairTrace<'static, R, R, 32, 32> {
83        let ring_cfg = FixedConfig::<R>::default();
84        let poly_cfg = ring_cfg.dyn_poly_cfg();
85
86        let mut a: Vec<DynamicPolynomial<R>> =
87            vec![DynamicPolynomial::new(vec![R::from(rng.random::<i8>())])];
88        let mut b: Vec<DynamicPolynomial<R>> = vec![DynamicPolynomial::new(vec![
89            R::zero(),
90            R::from(rng.random::<i8>()),
91        ])];
92        let mut c: Vec<DynamicPolynomial<R>> = vec![DynamicPolynomial::new(vec![
93            R::zero(),
94            R::from(rng.random::<i8>()),
95        ])];
96
97        for i in 1..1 << num_vars {
98            let prev_a = a[i - 1].clone();
99            let prev_b = b[i - 1].clone();
100            let prev_c = c[i - 1].clone();
101
102            a.push(poly_cfg.mul(&prev_a, &prev_b));
103            b.push(poly_cfg.mul(&prev_b, &prev_c));
104            c.push(poly_cfg.mul(&prev_a, &prev_c));
105        }
106
107        let arbitrary_poly = vec![
108            a.into_iter()
109                .map(|x| {
110                    let deg = poly_cfg.degree(&x);
111                    assert!(
112                        deg < Some(32),
113                        "degree bound exceeded: {}",
114                        deg.expect("if the degree is large it's not None")
115                    );
116                    DensePolynomial::new_with_zero(x.coeffs, R::zero())
117                })
118                .collect(),
119            b.into_iter()
120                .map(|x| {
121                    let deg = poly_cfg.degree(&x);
122                    assert!(
123                        deg < Some(32),
124                        "degree bound exceeded: {}",
125                        deg.expect("if the degree is large it's not None"),
126                    );
127                    DensePolynomial::new_with_zero(x.coeffs, R::zero())
128                })
129                .collect(),
130            c.into_iter()
131                .map(|x| {
132                    let deg = poly_cfg.degree(&x);
133                    assert!(
134                        deg < Some(32),
135                        "degree bound exceeded: {}",
136                        deg.expect("if the degree is large it's not None"),
137                    );
138                    DensePolynomial::new_with_zero(x.coeffs, R::zero())
139                })
140                .collect(),
141        ]
142        .into();
143        UairTrace {
144            arbitrary_poly,
145            ..Default::default()
146        }
147    }
148}
149
150#[derive(Clone, Debug)]
151pub struct TestUairNoMultiplication<R, P>(PhantomData<(R, P)>);
152
153impl<R, P> Uair for TestUairNoMultiplication<R, P>
154where
155    R: ConstSemiring + From<i32> + 'static,
156    P: Semiring + 'static,
157{
158    type Ideal = DegreeOneIdeal<R>;
159    type FqIdeal = ImpossibleIdeal;
160    type Scalar = DensePolynomial<R, 32>;
161    type Prime = P;
162
163    fn signature() -> UairSignature<Self::Prime> {
164        let total = TotalColumnLayout::new(0, 3, 0);
165        UairSignature::new(total, PublicColumnLayout::default(), vec![], vec![])
166    }
167
168    fn constrain_general<C, B, FromR, MulByScalar, IFromR, IFqFromR>(
169        b: &mut B,
170        expr_cfg: &C,
171        up: TraceRow<C::Element>,
172        _down: TraceRow<C::Element>,
173        _from_ref: FromR,
174        _mbs: MulByScalar,
175        ideal_from_ref: IFromR,
176        _fq_ideal_from_ref: IFqFromR,
177    ) where
178        C: SemiringConfig,
179        B: ConstraintBuilder<Expr = C::Element>,
180        IFromR: Fn(&Self::Ideal) -> B::Ideal,
181    {
182        let up = up.arbitrary_poly;
183
184        b.assert_in_ideal(
185            expr_cfg.sub(&expr_cfg.add(&up[0], &up[1]), &up[2]),
186            &ideal_from_ref(&DegreeOneIdeal::new(R::from(2))),
187        );
188    }
189}
190
191impl<R, P> GenerateRandomTrace<32> for TestUairNoMultiplication<R, P>
192where
193    R: ConstSemiring + From<i32> + 'static,
194    P: Semiring + 'static,
195{
196    type PolyCoeff = R;
197    type Int = R;
198
199    fn generate_random_trace<G: Rng + ?Sized>(
200        num_vars: usize,
201        rng: &mut G,
202    ) -> UairTrace<'static, R, R, 32, 32> {
203        let a: DenseMultilinearExtension<DensePolynomial<R, 32>> =
204            DenseMultilinearExtension::rand(num_vars, rng)
205                .into_iter()
206                .map(|x: u32| {
207                    DensePolynomial::from_ref(&DensePolynomial::<Boolean, _>::from(
208                        BinaryPoly::<32>::from(x),
209                    ))
210                })
211                .collect();
212
213        let b: DenseMultilinearExtension<_> = DenseMultilinearExtension::rand(num_vars, rng)
214            .into_iter()
215            .map(|x: u32| {
216                DensePolynomial::from_ref(&DensePolynomial::<Boolean, _>::from(
217                    BinaryPoly::<32>::from(x),
218                ))
219            })
220            .collect();
221
222        let c = a.clone() + b.clone();
223
224        UairTrace {
225            arbitrary_poly: vec![a, b, c].into(),
226            ..Default::default()
227        }
228    }
229}
230
231#[derive(Clone, Debug)]
232pub struct TestUairScalarMultiplications<R, P>(PhantomData<(R, P)>);
233
234impl<R, P> Uair for TestUairScalarMultiplications<R, P>
235where
236    R: ConstSemiring + From<i8> + 'static,
237    P: Semiring + 'static,
238{
239    type Ideal = DegreeOneIdeal<R>;
240    type FqIdeal = ImpossibleIdeal;
241    type Scalar = DensePolynomial<R, 32>;
242    type Prime = P;
243
244    fn signature() -> UairSignature<Self::Prime> {
245        let total = TotalColumnLayout::new(0, 3, 0);
246        UairSignature::new(total, PublicColumnLayout::default(), vec![], vec![])
247    }
248
249    fn constrain_general<C, B, FromR, MulByScalar, IFromR, IFqFromR>(
250        b: &mut B,
251        expr_cfg: &C,
252        up: TraceRow<C::Element>,
253        _down: TraceRow<C::Element>,
254        from_ref: FromR,
255        mbs: MulByScalar,
256        ideal_from_ref: IFromR,
257        _fq_ideal_from_ref: IFqFromR,
258    ) where
259        C: SemiringConfig,
260        B: ConstraintBuilder<Expr = C::Element>,
261        IFromR: Fn(&Self::Ideal) -> B::Ideal,
262        FromR: Fn(&DensePolynomial<R, 32>) -> C::Element,
263        MulByScalar: Fn(&C::Element, &DensePolynomial<R, 32>) -> Option<C::Element>,
264    {
265        let up = up.arbitrary_poly;
266
267        let scaled = mbs(
268            &up[0],
269            &DensePolynomial::new_with_zero([R::from(-1), R::from(0), R::from(1)], R::zero()),
270        )
271        .expect("arithmetic overflow");
272        let constant = from_ref(&DensePolynomial::new_with_zero(
273            [R::from(1), R::from(2), R::from(3), R::from(4)],
274            R::zero(),
275        ));
276        // (up_0 * const[-1,0,1]) + up_1 - up_2 + const[1,2,3,4]
277        b.assert_in_ideal(
278            expr_cfg.add(
279                &expr_cfg.sub(&expr_cfg.add(&scaled, &up[1]), &up[2]),
280                &constant,
281            ),
282            &ideal_from_ref(&DegreeOneIdeal::new(R::from(2))),
283        );
284    }
285}
286
287#[derive(Clone, Debug)]
288pub struct BinaryDecompositionUair<R, P>(PhantomData<(R, P)>);
289
290impl<R, P> Uair for BinaryDecompositionUair<R, P>
291where
292    R: ConstSemiring + From<u32> + 'static,
293    P: Semiring + 'static,
294{
295    type Ideal = DegreeOneIdeal<R>;
296    type FqIdeal = ImpossibleIdeal;
297    type Scalar = DensePolynomial<R, 32>;
298    type Prime = P;
299
300    fn signature() -> UairSignature<Self::Prime> {
301        let total = TotalColumnLayout::new(1, 0, 1);
302        UairSignature::new(total, PublicColumnLayout::default(), vec![], vec![])
303    }
304
305    fn constrain_general<C, B, FromR, MulByScalar, IFromR, IFqFromR>(
306        b: &mut B,
307        expr_cfg: &C,
308        up: TraceRow<C::Element>,
309        _down: TraceRow<C::Element>,
310        _from_ref: FromR,
311        _mbs: MulByScalar,
312        ideal_from_ref: IFromR,
313        _fq_ideal_from_ref: IFqFromR,
314    ) where
315        C: SemiringConfig,
316        B: ConstraintBuilder<Expr = C::Element>,
317        FromR: Fn(&Self::Scalar) -> C::Element,
318        MulByScalar: Fn(&C::Element, &Self::Scalar) -> Option<C::Element>,
319        IFromR: Fn(&Self::Ideal) -> B::Ideal,
320    {
321        let int_col = &up.int[0];
322        let binary_poly_col = &up.binary_poly[0];
323
324        b.assert_in_ideal(
325            expr_cfg.sub(binary_poly_col, int_col),
326            &ideal_from_ref(&DegreeOneIdeal::new(R::from(2))),
327        );
328    }
329}
330
331impl<R, P> GenerateRandomTrace<32> for BinaryDecompositionUair<R, P>
332where
333    R: ConstSemiring + From<u32> + 'static,
334    P: Semiring + 'static,
335{
336    type PolyCoeff = R;
337    type Int = R;
338
339    fn generate_random_trace<G: Rng + ?Sized>(
340        num_vars: usize,
341        rng: &mut G,
342    ) -> UairTrace<'static, R, R, 32, 32> {
343        let int_col_u32: DenseMultilinearExtension<u32> =
344            DenseMultilinearExtension::rand(num_vars, rng);
345
346        let binary_poly_col: DenseMultilinearExtension<BinaryPoly<32>> =
347            int_col_u32.iter().map(|i| BinaryPoly::from(*i)).collect();
348
349        let int_col = int_col_u32.into_iter().map(R::from).collect();
350
351        UairTrace {
352            binary_poly: vec![binary_poly_col].into(),
353            arbitrary_poly: vec![].into(),
354            int: vec![int_col].into(),
355        }
356    }
357}
358
359#[derive(Clone, Debug)]
360pub struct BigLinearUair<R, P>(PhantomData<(R, P)>);
361
362impl<R, P> Uair for BigLinearUair<R, P>
363where
364    R: ConstSemiring + From<u32> + 'static,
365    P: Semiring + 'static,
366{
367    type Ideal = DegreeOneIdeal<R>;
368    type FqIdeal = ImpossibleIdeal;
369    type Scalar = DensePolynomial<R, 32>;
370    type Prime = P;
371
372    fn signature() -> UairSignature<Self::Prime> {
373        let total = TotalColumnLayout::new(16, 0, 1);
374        let shifts = (0..16).map(|i| ShiftSpec::new(i, 1)).collect();
375        UairSignature::new(total, PublicColumnLayout::default(), shifts, vec![])
376    }
377
378    fn constrain_general<C, B, FromR, MulByScalar, IFromR, IFqFromR>(
379        b: &mut B,
380        expr_cfg: &C,
381        up: TraceRow<C::Element>,
382        down: TraceRow<C::Element>,
383        _from_ref: FromR,
384        _mbs: MulByScalar,
385        ideal_from_ref: IFromR,
386        _fq_ideal_from_ref: IFqFromR,
387    ) where
388        C: SemiringConfig,
389        B: ConstraintBuilder<Expr = C::Element>,
390        FromR: Fn(&Self::Scalar) -> C::Element,
391        MulByScalar: Fn(&C::Element, &Self::Scalar) -> Option<C::Element>,
392        IFromR: Fn(&Self::Ideal) -> B::Ideal,
393    {
394        let one_ideal = DegreeOneIdeal::new(R::from(1));
395        let two_ideal = DegreeOneIdeal::new(R::from(2));
396
397        let sum_of_binary_polys = up.binary_poly[1..]
398            .iter()
399            .fold(up.binary_poly[0].clone(), |acc, next| {
400                expr_cfg.add(&acc, next)
401            });
402
403        // up.binary_poly[0] + up.binary_poly[1] + ... up.binary_poly[16]
404        //      = up.int[0] mod (X - 1)
405        b.assert_in_ideal(
406            expr_cfg.sub(&sum_of_binary_polys, &up.int[0]),
407            &ideal_from_ref(&one_ideal),
408        );
409
410        // down.binary_poly[0] = up.int[0] mod (X - 1)
411        b.assert_in_ideal(
412            expr_cfg.sub(&down.binary_poly[0], &up.int[0]),
413            &ideal_from_ref(&two_ideal),
414        );
415
416        // down.binary_poly[i](1) = up.binary_poly[i](1), for all i=1,...,15
417        // (preserves popcount across rows, but allows the bit pattern to change)
418        up.binary_poly[1..]
419            .iter()
420            .zip(&down.binary_poly[1..])
421            .for_each(|(up, down)| {
422                b.assert_in_ideal(expr_cfg.sub(up, down), &ideal_from_ref(&one_ideal));
423            });
424    }
425}
426
427impl<R, P> GenerateRandomTrace<32> for BigLinearUair<R, P>
428where
429    R: ConstSemiring + From<u32> + 'static,
430    P: Semiring + 'static,
431{
432    type PolyCoeff = R;
433    type Int = R;
434
435    fn generate_random_trace<G: Rng + ?Sized>(
436        num_vars: usize,
437        rng: &mut G,
438    ) -> UairTrace<'static, R, R, 32, 32> {
439        /// Generate a random binary polynomial with the given number of 1-bits.
440        fn random_binary_poly_with_popcount(
441            popcount: u32,
442            rng: &mut (impl rand::Rng + ?Sized),
443        ) -> BinaryPoly<32> {
444            let mut positions: [u8; 32] =
445                core::array::from_fn(|i| u8::try_from(i).expect("can't fail"));
446            for i in 0..popcount as usize {
447                let j = i + rng.next_u32() as usize % (32 - i);
448                positions.swap(i, j);
449            }
450            let mut value: u32 = 0;
451            for &pos in &positions[..popcount as usize] {
452                value |= 1u32 << pos;
453            }
454            BinaryPoly::from(value)
455        }
456
457        let mut binary_poly_cols: Vec<DenseMultilinearExtension<BinaryPoly<32>>> =
458            vec![(0..(1 << num_vars)).map(|_| BinaryPoly::zero()).collect(); 16];
459        let mut int_col: DenseMultilinearExtension<Self::Int> =
460            (0..(1 << num_vars)).map(|_| R::ZERO).collect();
461
462        binary_poly_cols.iter_mut().for_each(|col| {
463            col[0] = rng.random();
464        });
465
466        for i in 0..(1 << num_vars) - 1 {
467            let int: u32 = binary_poly_cols
468                .iter()
469                .map(|col| col[i].evaluate_at_point(&1_u32).expect("should be fine"))
470                .sum();
471            int_col[i] = R::from(int);
472
473            binary_poly_cols[0][i + 1] = BinaryPoly::from(int);
474            binary_poly_cols[1..].iter_mut().for_each(|col| {
475                let popcount = col[i].evaluate_at_point(&1_u32).expect("should be fine");
476                col[i + 1] = random_binary_poly_with_popcount(popcount, rng);
477            });
478        }
479
480        let len = int_col.len();
481
482        int_col[len - 1] = R::from(
483            binary_poly_cols
484                .iter()
485                .map(|col| {
486                    col[len - 1]
487                        .evaluate_at_point(&1_u32)
488                        .expect("should be fine")
489                })
490                .sum::<u32>(),
491        );
492
493        UairTrace {
494            binary_poly: binary_poly_cols.into(),
495            arbitrary_poly: vec![].into(),
496            int: vec![int_col].into(),
497        }
498    }
499}
500
501#[derive(Clone, Debug)]
502pub struct BigLinearUairWithPublicInput<R, P>(PhantomData<(R, P)>);
503
504impl<R, P> Uair for BigLinearUairWithPublicInput<R, P>
505where
506    R: ConstSemiring + From<u32> + 'static,
507    P: Semiring + 'static,
508{
509    type Ideal = <BigLinearUair<R, P> as Uair>::Ideal;
510    type FqIdeal = <BigLinearUair<R, P> as Uair>::FqIdeal;
511    type Scalar = <BigLinearUair<R, P> as Uair>::Scalar;
512    type Prime = <BigLinearUair<R, P> as Uair>::Prime;
513
514    fn signature() -> UairSignature<Self::Prime> {
515        let total = TotalColumnLayout::new(16, 0, 1);
516        let public = PublicColumnLayout::new(4, 0, 0);
517        let shifts = (0..16).map(|i| ShiftSpec::new(i, 1)).collect();
518        UairSignature::new(total, public, shifts, vec![])
519    }
520
521    fn constrain_general<C, B, FromR, MulByScalar, IFromR, IFqFromR>(
522        b: &mut B,
523        expr_cfg: &C,
524        up: TraceRow<C::Element>,
525        down: TraceRow<C::Element>,
526        from_ref: FromR,
527        mbs: MulByScalar,
528        ideal_from_ref: IFromR,
529        fq_ideal_from_ref: IFqFromR,
530    ) where
531        C: SemiringConfig,
532        B: ConstraintBuilder<Expr = C::Element>,
533        FromR: Fn(&Self::Scalar) -> C::Element,
534        MulByScalar: Fn(&C::Element, &Self::Scalar) -> Option<C::Element>,
535        IFromR: Fn(&Self::Ideal) -> B::Ideal,
536        IFqFromR: Fn(&Self::FqIdeal) -> B::FqIdeal,
537    {
538        BigLinearUair::<R, P>::constrain_general(
539            b,
540            expr_cfg,
541            up,
542            down,
543            from_ref,
544            mbs,
545            ideal_from_ref,
546            fq_ideal_from_ref,
547        )
548    }
549}
550
551impl<R, P> GenerateRandomTrace<32> for BigLinearUairWithPublicInput<R, P>
552where
553    R: ConstSemiring + From<u32> + 'static,
554    P: Semiring + 'static,
555{
556    type PolyCoeff = <BigLinearUair<R, P> as GenerateRandomTrace<32>>::PolyCoeff;
557    type Int = <BigLinearUair<R, P> as GenerateRandomTrace<32>>::Int;
558
559    fn generate_random_trace<G: Rng + ?Sized>(
560        num_vars: usize,
561        rng: &mut G,
562    ) -> UairTrace<'static, Self::PolyCoeff, Self::Int, 32, 32> {
563        BigLinearUair::<R, P>::generate_random_trace(num_vars, rng)
564    }
565}
566
567/// A second "big linear" UAIR with 14 binary-poly columns and 4 int columns,
568/// used as a benchmarking shape distinct from `BigLinearUair`.
569///
570/// Constraints (0-based; `bp = up.binary_poly`, `int = up.int`):
571///
572/// - `bp[0][t+1] - bp[1] - bp[2] - bp[3] - int[0] - int[1] - int[2] ∈ (X-2)`
573/// - `bp[4][t+4] - bp[5] - bp[6] - bp[7] - int[1] - int[2] - int[3] ∈ (X-2)`
574/// - `bp[8] - int[0] ∈ (X-2)`
575/// - `bp[9] - int[1] ∈ (X-2)`
576/// - `bp[10] - X * bp[11] ∈ (X-1)`
577/// - `bp[12] - X * bp[13] ∈ (X-1)`
578///
579/// Note the asymmetric shift amounts: `bp[0]` is shifted by 1 (used by C1)
580/// and `bp[4]` is shifted by 4 (used by C2).
581#[derive(Clone, Debug)]
582pub struct ShaProxy<R, P>(PhantomData<(R, P)>);
583
584impl<R, P> Uair for ShaProxy<R, P>
585where
586    R: ConstSemiring + From<u32> + 'static,
587    P: Semiring + 'static,
588{
589    type Ideal = DegreeOneIdeal<R>;
590    type FqIdeal = ImpossibleIdeal;
591    type Scalar = DensePolynomial<R, 32>;
592    type Prime = P;
593
594    fn signature() -> UairSignature<Self::Prime> {
595        // 14 binary_poly cols, 0 arbitrary_poly cols, 4 int cols.
596        let total = TotalColumnLayout::new(14, 0, 4);
597        // c_1 (bp[0]) is shifted by 1 (used by C1 as bp[0][t+1]); c_5 (bp[4])
598        // is shifted by 4 (used by C2 as bp[4][t+4]).
599        let shifts = vec![ShiftSpec::new(0, 1), ShiftSpec::new(4, 4)];
600        UairSignature::new(total, PublicColumnLayout::default(), shifts, vec![])
601    }
602
603    fn constrain_general<C, B, FromR, MulByScalar, IFromR, IFqFromR>(
604        b: &mut B,
605        expr_cfg: &C,
606        up: TraceRow<C::Element>,
607        down: TraceRow<C::Element>,
608        _from_ref: FromR,
609        mbs: MulByScalar,
610        ideal_from_ref: IFromR,
611        _fq_ideal_from_ref: IFqFromR,
612    ) where
613        C: SemiringConfig,
614        B: ConstraintBuilder<Expr = C::Element>,
615        FromR: Fn(&Self::Scalar) -> C::Element,
616        MulByScalar: Fn(&C::Element, &Self::Scalar) -> Option<C::Element>,
617        IFromR: Fn(&Self::Ideal) -> B::Ideal,
618    {
619        let one_ideal = ideal_from_ref(&DegreeOneIdeal::new(R::ONE));
620        let two_ideal = ideal_from_ref(&DegreeOneIdeal::new(R::from(2)));
621        // The polynomial X = 0 + 1*X, used to express `X * c_k` via `mbs`.
622        let x_scalar = DensePolynomial::<R, 32>::new_with_zero([R::ZERO, R::from(1)], R::zero());
623
624        // `down.binary_poly` is indexed by ShiftSpec position, not source col.
625        // Our shifts vec is [ShiftSpec::new(0, 1), ShiftSpec::new(4, 1)], so
626        // down.binary_poly[0] = bp[0][t+1], down.binary_poly[1] = bp[4][t+1].
627
628        // (C1) dbp[0] - bp[1] - bp[2] - bp[3] - int[0] - int[1] - int[2] ∈ (X-2)
629        let c1 = [
630            &up.binary_poly[1],
631            &up.binary_poly[2],
632            &up.binary_poly[3],
633            &up.int[0],
634            &up.int[1],
635            &up.int[2],
636        ]
637        .into_iter()
638        .fold(down.binary_poly[0].clone(), |acc, term| {
639            expr_cfg.sub(&acc, term)
640        });
641        b.assert_in_ideal(c1, &two_ideal);
642
643        // (C2) dbp[4] - bp[5] - bp[6] - bp[7] - int[1] - int[2] - int[3] ∈ (X-2)
644        let c2 = [
645            &up.binary_poly[5],
646            &up.binary_poly[6],
647            &up.binary_poly[7],
648            &up.int[1],
649            &up.int[2],
650            &up.int[3],
651        ]
652        .into_iter()
653        .fold(down.binary_poly[1].clone(), |acc, term| {
654            expr_cfg.sub(&acc, term)
655        });
656        b.assert_in_ideal(c2, &two_ideal);
657
658        // (C3) bp[8] - int[0] ∈ (X-2)
659        b.assert_in_ideal(expr_cfg.sub(&up.binary_poly[8], &up.int[0]), &two_ideal);
660
661        // (C4) bp[9] - int[1] ∈ (X-2)
662        b.assert_in_ideal(expr_cfg.sub(&up.binary_poly[9], &up.int[1]), &two_ideal);
663
664        // (C5) bp[10] - X * bp[11] ∈ (X-1)
665        b.assert_in_ideal(
666            expr_cfg.sub(
667                &up.binary_poly[10],
668                &mbs(&up.binary_poly[11], &x_scalar).expect("mul-by-X overflow"),
669            ),
670            &one_ideal,
671        );
672
673        // (C6) bp[12] - X * bp[13] ∈ (X-1)
674        b.assert_in_ideal(
675            expr_cfg.sub(
676                &up.binary_poly[12],
677                &mbs(&up.binary_poly[13], &x_scalar).expect("mul-by-X overflow"),
678            ),
679            &one_ideal,
680        );
681    }
682}
683
684impl<R, P> GenerateRandomTrace<32> for ShaProxy<R, P>
685where
686    R: ConstSemiring + From<u32> + 'static,
687    P: Semiring + 'static,
688{
689    type PolyCoeff = R;
690    type Int = R;
691
692    #[allow(clippy::needless_range_loop)]
693    fn generate_random_trace<G: Rng + ?Sized>(
694        num_vars: usize,
695        rng: &mut G,
696    ) -> UairTrace<'static, R, R, 32, 32> {
697        /// Generate a random binary polynomial with the given number of 1-bits.
698        fn random_binary_poly_with_popcount(
699            popcount: u32,
700            rng: &mut (impl rand::Rng + ?Sized),
701        ) -> BinaryPoly<32> {
702            let mut positions: [u8; 32] =
703                core::array::from_fn(|i| u8::try_from(i).expect("can't fail"));
704            for i in 0..popcount as usize {
705                let j = i + rng.next_u32() as usize % (32 - i);
706                positions.swap(i, j);
707            }
708            let mut value: u32 = 0;
709            for &pos in &positions[..popcount as usize] {
710                value |= 1_u32 << pos;
711            }
712            BinaryPoly::from(value)
713        }
714
715        // Bits used by the "small" binary polys that feed into C1/C2 sums.
716        // Capping at 28 bits keeps each `eval(2)` value below 2^28 - 1, so the
717        // sum used to construct `bp[0]` / `bp[4]` at the next row stays in u32:
718        //     3 * (2^28 - 1) + 3 * 31  ≈  8.05 * 10^8  <  2^32 - 1.
719        const SMALL_MASK: u32 = (1 << 28) - 1;
720        // Range of values for the int columns (small non-negative).
721        const INT_MAX_EXCL: u32 = 32;
722
723        let len = 1 << num_vars;
724
725        let mut bp_cols: Vec<DenseMultilinearExtension<BinaryPoly<32>>> =
726            vec![(0..len).map(|_| BinaryPoly::zero()).collect(); 14];
727        let mut int_cols: Vec<DenseMultilinearExtension<R>> =
728            vec![(0..len).map(|_| R::ZERO).collect(); 4];
729
730        // Row 0 / "head row" init for the columns whose value at the head of
731        // the trace is unconstrained:
732        //   - `bp[0]` is shifted by 1, so only `bp[0][0]` is unconstrained.
733        //   - `bp[4]` is shifted by 4, so `bp[4][0..4]` are unconstrained (the C2
734        //     fix-up at iteration `i` writes `bp[4][i+4]`, so the first 4 indices are
735        //     never written by the loop).
736        bp_cols[0][0] = BinaryPoly::from(rng.next_u32() & SMALL_MASK);
737        for k in 0..4.min(len) {
738            bp_cols[4][k] = BinaryPoly::from(rng.next_u32() & SMALL_MASK);
739        }
740
741        for i in 0..len {
742            // bp[1..=3]: always small random binary polys (28-bit values).
743            for k in 1..=3 {
744                bp_cols[k][i] = BinaryPoly::from(rng.next_u32() & SMALL_MASK);
745            }
746
747            // For rows where the C2-target `bp[4][i+4]` is past the trace
748            // boundary, the protocol reads it as zero-padded. To still
749            // satisfy C2 at those rows, the C2 RHS sum must vanish at X = 2;
750            // we achieve that by zeroing `bp[5..=7]` and `int[1..=3]` here.
751            // (Mirrors the boundary trick used by `TestUairMixedShifts`.)
752            // Row `len - 1` is exempt from constraint checking by the
753            // protocol's last-row selector, so the zeroing only matters for
754            // rows `len - 4 ..= len - 2`.
755            let c2_target_oob = i + 4 >= len;
756
757            for k in 5..=7 {
758                bp_cols[k][i] = if c2_target_oob {
759                    BinaryPoly::zero()
760                } else {
761                    BinaryPoly::from(rng.next_u32() & SMALL_MASK)
762                };
763            }
764
765            // int[0..=3]: small non-negative values. Zero out int[1..=3] when
766            // we're in the C2 boundary region (int[0] can stay random — it is
767            // not used by C2).
768            let int_vals: [u32; 4] = if c2_target_oob {
769                [rng.next_u32() % INT_MAX_EXCL, 0, 0, 0]
770            } else {
771                [
772                    rng.next_u32() % INT_MAX_EXCL,
773                    rng.next_u32() % INT_MAX_EXCL,
774                    rng.next_u32() % INT_MAX_EXCL,
775                    rng.next_u32() % INT_MAX_EXCL,
776                ]
777            };
778            for (k, v) in int_vals.iter().enumerate() {
779                int_cols[k][i] = R::from(*v);
780            }
781
782            // (C3)/(C4): bp[8] = BinaryPoly::from(int[0]);
783            // bp[9] = BinaryPoly::from(int[1]).
784            // Since `BinaryPoly::from(n).evaluate_at_point(2) == n`,
785            // this makes `bp[k] - int[k]` vanish at X=2, satisfying the (X-2) ideal check.
786            bp_cols[8][i] = BinaryPoly::from(int_vals[0]);
787            bp_cols[9][i] = BinaryPoly::from(int_vals[1]);
788
789            // (C5): popcount(bp[10]) == popcount(bp[11]).
790            let bp11: BinaryPoly<32> = rng.random();
791            let popcount11 = bp11
792                .evaluate_at_point(&1_u32)
793                .expect("popcount eval should fit in u32");
794            bp_cols[11][i] = bp11;
795            bp_cols[10][i] = random_binary_poly_with_popcount(popcount11, rng);
796
797            // (C6): popcount(bp[12]) == popcount(bp[13]).
798            let bp13: BinaryPoly<32> = rng.random();
799            let popcount13 = bp13
800                .evaluate_at_point(&1_u32)
801                .expect("popcount eval should fit in u32");
802            bp_cols[13][i] = bp13;
803            bp_cols[12][i] = random_binary_poly_with_popcount(popcount13, rng);
804
805            // Set bp[0][i+1] and bp[4][i+4] so C1 and C2 respectively hold at
806            // row i. Each summand fits in u32 (bp eval ≤ 2^28 - 1, ints ≤ 31),
807            // so each sum (≤ 3 * (2^28 - 1) + 3 * 31 ≈ 8.05e8) stays well
808            // below 2^32. C1 and C2 have different shift amounts now (1 vs 4)
809            // and so need separate `if` guards.
810            let eval_at_2 = |bp: &BinaryPoly<32>| -> u32 {
811                bp.evaluate_at_point(&2_u32)
812                    .expect("28-bit binary poly eval at 2 fits in u32")
813            };
814
815            // C1 (shift = 1)
816            if i + 1 < len {
817                let s1 = eval_at_2(&bp_cols[1][i])
818                    + eval_at_2(&bp_cols[2][i])
819                    + eval_at_2(&bp_cols[3][i])
820                    + int_vals[0]
821                    + int_vals[1]
822                    + int_vals[2];
823                bp_cols[0][i + 1] = BinaryPoly::from(s1);
824            }
825
826            // C2 (shift = 4)
827            if i + 4 < len {
828                let s2 = eval_at_2(&bp_cols[5][i])
829                    + eval_at_2(&bp_cols[6][i])
830                    + eval_at_2(&bp_cols[7][i])
831                    + int_vals[1]
832                    + int_vals[2]
833                    + int_vals[3];
834                bp_cols[4][i + 4] = BinaryPoly::from(s2);
835            }
836        }
837
838        UairTrace {
839            binary_poly: bp_cols.into(),
840            arbitrary_poly: vec![].into(),
841            int: int_cols.into(),
842        }
843    }
844}
845
846/// Test UAIR with mixed shift amounts.
847/// 3 columns (a, b, c): column a shifts by 1, column b shifts by 2.
848/// Constraints are linear (degree 1).
849#[derive(Clone, Debug)]
850pub struct TestUairMixedShifts<R, P>(PhantomData<(R, P)>);
851
852impl<R, P> Uair for TestUairMixedShifts<R, P>
853where
854    R: Semiring + 'static,
855    P: Semiring + 'static,
856{
857    type Ideal = ImpossibleIdeal;
858    type FqIdeal = ImpossibleIdeal;
859    type Scalar = DensePolynomial<R, 32>;
860    type Prime = P;
861
862    fn signature() -> UairSignature<Self::Prime> {
863        let total = TotalColumnLayout::new(0, 3, 0);
864        let shifts = vec![
865            ShiftSpec::new(0, 1), // a shifted by 1
866            ShiftSpec::new(1, 2), // b shifted by 2
867        ];
868        UairSignature::new(total, PublicColumnLayout::default(), shifts, vec![])
869    }
870
871    // Constraints:
872    //   a[i+1] = a[i] + b[i]  →  down[0] - up[0] - up[1] = 0
873    //   c[i]   = b[i+2]       →  up[2] - down[1] = 0
874    fn constrain_general<C, B, FromR, MulByScalar, IFromR, IFqFromR>(
875        builder: &mut B,
876        expr_cfg: &C,
877        up: TraceRow<C::Element>,
878        down: TraceRow<C::Element>,
879        _from_ref: FromR,
880        _mbs: MulByScalar,
881        _ideal_from_ref: IFromR,
882        _fq_ideal_from_ref: IFqFromR,
883    ) where
884        C: SemiringConfig,
885        B: ConstraintBuilder<Expr = C::Element>,
886    {
887        let up = up.arbitrary_poly;
888        let down = down.arbitrary_poly;
889
890        builder.assert_zero(expr_cfg.sub(&expr_cfg.sub(&down[0], &up[0]), &up[1]));
891        builder.assert_zero(expr_cfg.sub(&up[2], &down[1]));
892    }
893}
894
895impl<R, P> GenerateRandomTrace<32> for TestUairMixedShifts<R, P>
896where
897    R: Semiring + From<i8> + 'static,
898    P: Semiring + 'static,
899    StandardUniform: Distribution<R>,
900{
901    type PolyCoeff = R;
902    type Int = R;
903
904    // Witness: random b, derive a from a[i+1] = a[i] + b[i], set c[i] = b[i+2].
905    fn generate_random_trace<G: Rng + ?Sized>(
906        num_vars: usize,
907        rng: &mut G,
908    ) -> UairTrace<'static, R, R, 32, 32> {
909        let n = 1 << num_vars;
910
911        // Random b column (degree-0 polynomials to stay under degree 32)
912        let ring_cfg = FixedConfig::<R>::default();
913        let poly_cfg = ring_cfg.dyn_poly_cfg();
914
915        let b_col: Vec<DynamicPolynomial<R>> = (0..n)
916            .map(|_| DynamicPolynomial::new(vec![R::from(rng.random::<i8>())]))
917            .collect();
918
919        // a[0] random, a[i+1] = a[i] + b[i]
920        let mut a_col: Vec<DynamicPolynomial<R>> =
921            vec![DynamicPolynomial::new(vec![R::from(rng.random::<i8>())])];
922        for i in 0..n - 1 {
923            a_col.push(poly_cfg.add(&a_col[i], &b_col[i]));
924        }
925
926        // c[i] = b[i+2], zero-padded for last 2 entries
927        let mut c_col: Vec<DynamicPolynomial<R>> = Vec::with_capacity(n);
928        for i in 0..n {
929            if i + 2 < n {
930                c_col.push(b_col[i + 2].clone());
931            } else {
932                c_col.push(DynamicPolynomial::ZERO);
933            }
934        }
935
936        let to_mle =
937            |col: Vec<DynamicPolynomial<R>>| -> DenseMultilinearExtension<DensePolynomial<R, 32>> {
938                col.into_iter()
939                    .map(|x| DensePolynomial::new_with_zero(x.coeffs, R::zero()))
940                    .collect()
941            };
942
943        UairTrace {
944            arbitrary_poly: vec![to_mle(a_col), to_mle(b_col), to_mle(c_col)].into(),
945            ..Default::default()
946        }
947    }
948}
949
950/// Mixed-splice UAIR for bit-op virtual columns.
951///
952/// It populates three slots of the canonical down-row ordering at once:
953/// shifted binary, bit-op binary, and shifted arbitrary. This catches
954/// materialization code that appends bit-op virtuals at the tail instead of
955/// inserting them into the binary down slice.
956#[derive(Clone, Debug)]
957pub struct TestUairBitOpsMixedSplice<R, P>(PhantomData<(R, P)>);
958
959impl<R, P> Uair for TestUairBitOpsMixedSplice<R, P>
960where
961    R: ConstSemiring + 'static,
962    P: Semiring + 'static,
963{
964    type Ideal = DegreeOneIdeal<R>;
965    type FqIdeal = ImpossibleIdeal;
966    type Scalar = DensePolynomial<R, 32>;
967    type Prime = P;
968
969    fn signature() -> UairSignature<Self::Prime> {
970        let total = TotalColumnLayout::new(3, 2, 0);
971        let shifts = vec![ShiftSpec::new(0, 1), ShiftSpec::new(3, 1)];
972        let bit_op_specs = vec![BitOpSpec::new(0, BitOp::ShR(3))];
973        let sig = UairSignature::new(total, PublicColumnLayout::default(), shifts, vec![])
974            .with_bit_op_specs(bit_op_specs);
975        debug_assert_eq!(sig.down_cols().num_binary_poly_cols(), 2);
976        debug_assert_eq!(sig.down_cols().num_arbitrary_poly_cols(), 1);
977        debug_assert_eq!(sig.down_cols().num_int_cols(), 0);
978        sig
979    }
980
981    fn constrain_general<C, B, FromR, MulByScalar, IFromR, IFqFromR>(
982        b: &mut B,
983        expr_cfg: &C,
984        up: TraceRow<C::Element>,
985        down: TraceRow<C::Element>,
986        _from_ref: FromR,
987        _mbs: MulByScalar,
988        ideal_from_ref: IFromR,
989        _fq_ideal_from_ref: IFqFromR,
990    ) where
991        C: SemiringConfig,
992        B: ConstraintBuilder<Expr = C::Element>,
993        IFromR: Fn(&Self::Ideal) -> B::Ideal,
994    {
995        let one_ideal = ideal_from_ref(&DegreeOneIdeal::new(R::ONE));
996        b.assert_in_ideal(
997            expr_cfg.sub(&down.binary_poly[0], &up.binary_poly[2]),
998            &one_ideal,
999        );
1000        b.assert_in_ideal(
1001            expr_cfg.sub(&down.binary_poly[1], &up.binary_poly[1]),
1002            &one_ideal,
1003        );
1004        b.assert_in_ideal(
1005            expr_cfg.sub(&down.arbitrary_poly[0], &up.arbitrary_poly[1]),
1006            &one_ideal,
1007        );
1008    }
1009}
1010
1011impl<R, P> GenerateRandomTrace<32> for TestUairBitOpsMixedSplice<R, P>
1012where
1013    R: ConstSemiring + From<i8> + 'static,
1014    P: Semiring + 'static,
1015    StandardUniform: Distribution<R>,
1016{
1017    type PolyCoeff = R;
1018    type Int = R;
1019
1020    fn generate_random_trace<G: Rng + ?Sized>(
1021        num_vars: usize,
1022        rng: &mut G,
1023    ) -> UairTrace<'static, R, R, 32, 32> {
1024        let n = 1usize << num_vars;
1025
1026        let w_u32: Vec<u32> = (0..n).map(|_| rng.next_u32()).collect();
1027        let w_col: DenseMultilinearExtension<BinaryPoly<32>> =
1028            w_u32.iter().map(|w| BinaryPoly::from(*w)).collect();
1029        let s_shr_col: DenseMultilinearExtension<BinaryPoly<32>> =
1030            w_u32.iter().map(|w| BinaryPoly::from(w >> 3)).collect();
1031        let t_col: DenseMultilinearExtension<BinaryPoly<32>> = (0..n)
1032            .map(|i| {
1033                if i + 1 < n {
1034                    BinaryPoly::from(w_u32[i + 1])
1035                } else {
1036                    BinaryPoly::from(0u32)
1037                }
1038            })
1039            .collect();
1040
1041        let a_cells: Vec<DensePolynomial<R, 32>> = (0..n)
1042            .map(|_| DensePolynomial::new_with_zero([R::from(rng.random::<i8>())], R::zero()))
1043            .collect();
1044        let a_next_cells: Vec<DensePolynomial<R, 32>> = (0..n)
1045            .map(|i| {
1046                if i + 1 < n {
1047                    a_cells[i + 1].clone()
1048                } else {
1049                    DensePolynomial::<R, 32>::zero()
1050                }
1051            })
1052            .collect();
1053
1054        UairTrace {
1055            binary_poly: vec![w_col, s_shr_col, t_col].into(),
1056            arbitrary_poly: vec![
1057                a_cells.into_iter().collect(),
1058                a_next_cells.into_iter().collect(),
1059            ]
1060            .into(),
1061            int: vec![].into(),
1062        }
1063    }
1064}
1065
1066/// UAIR combining bit-op virtual columns with both Q[X] and F_q[X]
1067/// constraint families.
1068///
1069/// The single bit-op virtual column is `ShR(w, 3)`. Both families constrain it
1070/// to match the committed expected column `s`, so the full protocol must carry
1071/// the virtual bit-op column through each family, not just through ideal check.
1072#[derive(Clone, Debug)]
1073pub struct TestUairBitOpsFqFamily<R, P>(PhantomData<(R, P)>);
1074
1075impl<R, P> Uair for TestUairBitOpsFqFamily<R, P>
1076where
1077    R: ConstSemiring + 'static,
1078    P: Semiring + From<u64> + 'static,
1079{
1080    type Ideal = DegreeOneIdeal<R>;
1081    type FqIdeal = DegreeOneIdeal<R>;
1082    type Scalar = DensePolynomial<R, 32>;
1083    type Prime = P;
1084
1085    fn signature() -> UairSignature<Self::Prime> {
1086        let total = TotalColumnLayout::new(2, 0, 0);
1087        UairSignature::new(total, PublicColumnLayout::default(), vec![], vec![])
1088            .with_bit_op_specs(vec![BitOpSpec::new(0, BitOp::ShR(3))])
1089            .with_primes(vec![P::from(MERSENNE_61_PRIME)])
1090    }
1091
1092    fn constrain_general<C, B, FromR, MulByScalar, IFromR, IFqFromR>(
1093        b: &mut B,
1094        expr_cfg: &C,
1095        up: TraceRow<C::Element>,
1096        down: TraceRow<C::Element>,
1097        _from_ref: FromR,
1098        _mbs: MulByScalar,
1099        ideal_from_ref: IFromR,
1100        fq_ideal_from_ref: IFqFromR,
1101    ) where
1102        C: SemiringConfig,
1103        B: ConstraintBuilder<Expr = C::Element>,
1104        IFromR: Fn(&Self::Ideal) -> B::Ideal,
1105        IFqFromR: Fn(&Self::FqIdeal) -> B::FqIdeal,
1106    {
1107        let q_ideal = ideal_from_ref(&DegreeOneIdeal::new(R::ONE));
1108        let fq_ideal = fq_ideal_from_ref(&DegreeOneIdeal::new(R::ONE));
1109        let bit_op_matches_expected = expr_cfg.sub(&down.binary_poly[0], &up.binary_poly[1]);
1110
1111        b.assert_in_ideal(bit_op_matches_expected.clone(), &q_ideal);
1112        b.assert_in_fq_ideal(0, bit_op_matches_expected, &fq_ideal);
1113    }
1114}
1115
1116impl<R, P> GenerateRandomTrace<32> for TestUairBitOpsFqFamily<R, P>
1117where
1118    R: ConstSemiring + 'static,
1119    P: Semiring + From<u64> + 'static,
1120{
1121    type PolyCoeff = R;
1122    type Int = R;
1123
1124    fn generate_random_trace<G: Rng + ?Sized>(
1125        num_vars: usize,
1126        rng: &mut G,
1127    ) -> UairTrace<'static, R, R, 32, 32> {
1128        let n = 1usize << num_vars;
1129        let w_u32: Vec<u32> = (0..n).map(|_| rng.next_u32()).collect();
1130        let w_col: DenseMultilinearExtension<BinaryPoly<32>> =
1131            w_u32.iter().map(|w| BinaryPoly::from(*w)).collect();
1132        let s_shr_col: DenseMultilinearExtension<BinaryPoly<32>> =
1133            w_u32.iter().map(|w| BinaryPoly::from(w >> 3)).collect();
1134
1135        UairTrace {
1136            binary_poly: vec![w_col, s_shr_col].into(),
1137            ..Default::default()
1138        }
1139    }
1140}
1141
1142/// UAIR fixture for affine virtual booleanity targets while an independent F_q
1143/// constraint family is present.
1144///
1145/// For public `e` and witness `g`, the witnesses `u_not = (!e) & g` and
1146/// `u_and = e & g` are characterized by the paper's two Ch membership
1147/// expressions
1148///
1149/// ```text
1150/// (1_32 - e) + g - 2u_not in {0, 1}^{<32}[X]
1151/// e + g - 2u_and in {0, 1}^{<32}[X].
1152/// ```
1153#[derive(Clone, Debug)]
1154pub struct TestUairAffineVirtual<R, P, const NUM_PUBLIC_BINARY: usize>(PhantomData<(R, P)>);
1155
1156/// Unshifted variant with one public and three witness binary columns.
1157pub type TestUairAffineVirtualUnshifted<R, P> = TestUairAffineVirtual<R, P, 1>;
1158
1159/// Unshifted variant with all four binary columns public.
1160pub type TestUairAffineVirtualPublicOnly<R, P> = TestUairAffineVirtual<R, P, 4>;
1161
1162impl<R, P, const NUM_PUBLIC_BINARY: usize> Uair for TestUairAffineVirtual<R, P, NUM_PUBLIC_BINARY>
1163where
1164    R: ConstSemiring + 'static,
1165    P: Semiring + From<u64> + 'static,
1166{
1167    type Ideal = DegreeOneIdeal<R>;
1168    type FqIdeal = DegreeOneIdeal<R>;
1169    type Scalar = DensePolynomial<R, 32>;
1170    type Prime = P;
1171
1172    fn signature() -> UairSignature<Self::Prime> {
1173        UairSignature::new(
1174            TotalColumnLayout::new(4, 0, 0),
1175            PublicColumnLayout::new(NUM_PUBLIC_BINARY, 0, 0),
1176            vec![],
1177            vec![],
1178        )
1179        .with_affine_virtual_specs(vec![
1180            AffineVirtualSpec::with_ones_coefficient(
1181                vec![
1182                    AffineVirtualTerm::new(0, -1),
1183                    AffineVirtualTerm::new(1, 1),
1184                    AffineVirtualTerm::new(2, -2),
1185                ],
1186                1,
1187            ),
1188            AffineVirtualSpec::new(vec![
1189                AffineVirtualTerm::new(0, 1),
1190                AffineVirtualTerm::new(1, 1),
1191                AffineVirtualTerm::new(3, -2),
1192            ]),
1193        ])
1194        .with_primes(vec![P::from(MERSENNE_61_PRIME)])
1195    }
1196
1197    fn constrain_general<C, B, FromR, MulByScalar, IFromR, IFqFromR>(
1198        b: &mut B,
1199        expr_cfg: &C,
1200        up: TraceRow<C::Element>,
1201        _down: TraceRow<C::Element>,
1202        _from_ref: FromR,
1203        _mbs: MulByScalar,
1204        ideal_from_ref: IFromR,
1205        fq_ideal_from_ref: IFqFromR,
1206    ) where
1207        C: SemiringConfig,
1208        B: ConstraintBuilder<Expr = C::Element>,
1209        IFromR: Fn(&Self::Ideal) -> B::Ideal,
1210        IFqFromR: Fn(&Self::FqIdeal) -> B::FqIdeal,
1211    {
1212        // Keep independent zero identities in both constraint families so the
1213        // fixture exercises affine booleanity alongside Q and F_q CPR. The Ch
1214        // relations themselves are enforced by booleanity.
1215        let identity = expr_cfg.sub(&up.binary_poly[1], &up.binary_poly[1]);
1216        b.assert_in_ideal(
1217            identity.clone(),
1218            &ideal_from_ref(&DegreeOneIdeal::new(R::ONE)),
1219        );
1220        b.assert_in_fq_ideal(
1221            0,
1222            identity,
1223            &fq_ideal_from_ref(&DegreeOneIdeal::new(R::ONE)),
1224        );
1225    }
1226}
1227
1228impl<R, P, const NUM_PUBLIC_BINARY: usize> GenerateRandomTrace<32>
1229    for TestUairAffineVirtual<R, P, NUM_PUBLIC_BINARY>
1230where
1231    R: ConstSemiring + 'static,
1232    P: Semiring + From<u64> + 'static,
1233{
1234    type PolyCoeff = R;
1235    type Int = R;
1236
1237    fn generate_random_trace<G: Rng + ?Sized>(
1238        num_vars: usize,
1239        rng: &mut G,
1240    ) -> UairTrace<'static, R, R, 32, 32> {
1241        let n = 1usize << num_vars;
1242        let e: Vec<u32> = (0..n).map(|_| rng.next_u32()).collect();
1243        let g: Vec<u32> = (0..n).map(|_| rng.next_u32()).collect();
1244        let u_not: Vec<u32> = e.iter().zip(&g).map(|(e, g)| !e & g).collect();
1245        let u_and: Vec<u32> = e.iter().zip(&g).map(|(e, g)| e & g).collect();
1246
1247        UairTrace {
1248            binary_poly: vec![
1249                e.into_iter().map(BinaryPoly::from).collect(),
1250                g.into_iter().map(BinaryPoly::from).collect(),
1251                u_not.into_iter().map(BinaryPoly::from).collect(),
1252                u_and.into_iter().map(BinaryPoly::from).collect(),
1253            ]
1254            .into(),
1255            ..Default::default()
1256        }
1257    }
1258}
1259
1260/// A UAIR exercising the Flavor-1 $F_{q}[X]$-constraint surface
1261/// for **multiple large primes**.
1262///
1263/// Single arbitrary-poly witness column `a`. Two $F_{q_i}[X]$-
1264/// constraints, one per declared prime $q_i$:
1265///
1266/// $$
1267///   \phi_{q_0}(a) \in (X - 0) \quad \text{in } F_{q_0}[X],
1268///   \qquad
1269///   \phi_{q_1}(a) \in (X - 0) \quad \text{in } F_{q_1}[X],
1270/// $$
1271///
1272/// i.e. the constant term of `a` is zero modulo each of $q_0$ and $q_1$.
1273/// The trace generator builds `a` with its $X^0$ coefficient set to
1274/// integer zero, so both constraints hold simultaneously regardless of the
1275/// chosen primes.
1276///
1277/// Used to exercise the per-prime $F_{q_i}[X]$ ideal-check family
1278/// of the Zinc$+$ protocol end-to-end with multiple primes, including the
1279/// lockstep multi-family sumcheck/MP-eval driver.
1280#[derive(Clone, Debug)]
1281pub struct TestUairFqLargePrime<R, P>(PhantomData<(R, P)>);
1282
1283/// M61 prime (2^61−1); used as $q_0$ in [`TestUairFqLargePrime`].
1284pub const MERSENNE_61_PRIME: u64 = (1 << 61) - 1;
1285
1286/// Goldilocks prime (2^64 - 2^32 + 1); used as $q_1$ in
1287/// [`TestUairFqLargePrime`].
1288#[allow(clippy::cast_possible_truncation)]
1289pub const GOLDILOCKS_PRIME: u64 = ((1_u128 << 64) - (1 << 32) + 1) as u64;
1290
1291impl<R, P> Uair for TestUairFqLargePrime<R, P>
1292where
1293    R: ConstSemiring + From<i32> + 'static,
1294    P: Semiring + From<u64> + 'static,
1295{
1296    type Ideal = ImpossibleIdeal;
1297    type FqIdeal = DegreeOneIdeal<R>;
1298    type Scalar = DensePolynomial<R, 32>;
1299    type Prime = P;
1300
1301    fn signature() -> UairSignature<Self::Prime> {
1302        // 1 arbitrary-poly witness column `a`, no shifts, no lookups.
1303        let total = TotalColumnLayout::new(0, 1, 0);
1304        UairSignature::new(total, PublicColumnLayout::default(), vec![], vec![])
1305            .with_primes(vec![P::from(MERSENNE_61_PRIME), P::from(GOLDILOCKS_PRIME)])
1306    }
1307
1308    fn constrain_general<C, B, FromR, MulByScalar, IFromR, IFqFromR>(
1309        b: &mut B,
1310        _expr_cfg: &C,
1311        up: TraceRow<C::Element>,
1312        _down: TraceRow<C::Element>,
1313        _from_ref: FromR,
1314        _mbs: MulByScalar,
1315        _ideal_from_ref: IFromR,
1316        fq_ideal_from_ref: IFqFromR,
1317    ) where
1318        C: SemiringConfig,
1319        B: ConstraintBuilder<Expr = C::Element>,
1320        FromR: Fn(&Self::Scalar) -> C::Element,
1321        MulByScalar: Fn(&C::Element, &Self::Scalar) -> Option<C::Element>,
1322        IFqFromR: Fn(&Self::FqIdeal) -> B::FqIdeal,
1323    {
1324        // One constraint per declared prime: `phi_{q_i}(a) \in (X - 0)`.
1325        let ideal = fq_ideal_from_ref(&DegreeOneIdeal::<R>::new(R::ZERO));
1326        b.assert_in_fq_ideal(
1327            /* prime_idx = */ 0,
1328            up.arbitrary_poly[0].clone(),
1329            &ideal,
1330        );
1331        b.assert_in_fq_ideal(
1332            /* prime_idx = */ 1,
1333            up.arbitrary_poly[0].clone(),
1334            &ideal,
1335        );
1336    }
1337}
1338
1339impl<R, P> GenerateRandomTrace<32> for TestUairFqLargePrime<R, P>
1340where
1341    R: ConstSemiring + From<i32> + 'static,
1342    P: Semiring + From<u64> + 'static,
1343{
1344    type PolyCoeff = R;
1345    type Int = R;
1346
1347    fn generate_random_trace<G: Rng + ?Sized>(
1348        num_vars: usize,
1349        rng: &mut G,
1350    ) -> UairTrace<'static, R, R, 32, 32> {
1351        // Build the witness column: random polynomials whose constant term
1352        // is forced to zero (so `phi_q(a)(0) = 0` regardless of `q`).
1353        let a: DenseMultilinearExtension<DensePolynomial<R, 32>> =
1354            DenseMultilinearExtension::rand(num_vars, rng)
1355                .into_iter()
1356                .map(|x: u32| {
1357                    let poly = DensePolynomial::from_ref(&DensePolynomial::<Boolean, _>::from(
1358                        BinaryPoly::<32>::from(x),
1359                    ));
1360                    // Zero out the X^0 coefficient.
1361                    let mut coeffs = [R::ZERO; 32];
1362                    coeffs[1..].clone_from_slice(&poly.coeffs[1..]);
1363                    DensePolynomial::new(coeffs)
1364                })
1365                .collect();
1366
1367        UairTrace {
1368            arbitrary_poly: vec![a].into(),
1369            ..Default::default()
1370        }
1371    }
1372}
1373
1374#[cfg(test)]
1375mod tests {
1376    use super::*;
1377    use crypto_primitives::crypto_bigint_int::Int;
1378    use num_traits::ConstZero;
1379    use zinc_uair::{
1380        collect_scalars::collect_scalars,
1381        constraint_counter::count_constraints,
1382        degree_counter::{count_constraint_degrees_flattened, count_max_degree},
1383    };
1384
1385    const LIMBS: usize = 4;
1386
1387    #[test]
1388    fn test_constraint_degrees() {
1389        fn assert_uair_shape<U: Uair>(expected_degrees: &[usize]) {
1390            assert_eq!(count_constraints::<U>().total(), expected_degrees.len());
1391            assert_eq!(count_constraint_degrees_flattened::<U>(), expected_degrees);
1392            assert_eq!(
1393                count_max_degree::<U>(),
1394                *expected_degrees.iter().max().unwrap()
1395            );
1396        }
1397
1398        assert_uair_shape::<TestUairSimpleMultiplication<Int<LIMBS>, u64>>(&[2, 2, 2]);
1399        assert_uair_shape::<TestUairNoMultiplication<Int<LIMBS>, u64>>(&[1]);
1400        assert_uair_shape::<TestUairScalarMultiplications<Int<LIMBS>, u64>>(&[1]);
1401        assert_uair_shape::<BinaryDecompositionUair<u32, u64>>(&[1]);
1402        assert_uair_shape::<BigLinearUair<u32, u64>>(&[1; 17]);
1403        assert_uair_shape::<TestUairMixedShifts<Int<LIMBS>, u64>>(&[1, 1]);
1404        assert_uair_shape::<TestUairBitOpsMixedSplice<Int<LIMBS>, u64>>(&[1, 1, 1]);
1405        assert_uair_shape::<TestUairBitOpsFqFamily<Int<LIMBS>, u64>>(&[1, 1]);
1406        assert_uair_shape::<TestUairAffineVirtualUnshifted<Int<LIMBS>, u64>>(&[1, 1]);
1407        assert_uair_shape::<TestUairAffineVirtualPublicOnly<Int<LIMBS>, u64>>(&[1, 1]);
1408        // TestUairFqLargePrime: two F_{q_i}[X] linear constraints (one per
1409        // declared prime).
1410        assert_uair_shape::<TestUairFqLargePrime<Int<LIMBS>, u64>>(&[1, 1]);
1411    }
1412
1413    #[test]
1414    fn test_air_scalar_multiplications_correct_collect_scalars() {
1415        assert_eq!(
1416            collect_scalars::<TestUairScalarMultiplications<Int<LIMBS>, u64>>(),
1417            (vec![
1418                DensePolynomial::new_with_zero(
1419                    [Int::from_i8(-1), Int::from_i8(0), Int::from_i8(1)],
1420                    Int::ZERO
1421                ),
1422                DensePolynomial::new_with_zero(
1423                    [
1424                        Int::from_i8(1),
1425                        Int::from_i8(2),
1426                        Int::from_i8(3),
1427                        Int::from_i8(4),
1428                    ],
1429                    Int::ZERO
1430                )
1431            ]
1432            .into_iter()
1433            .collect())
1434        );
1435    }
1436}