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, FixedSemiring, Semiring, 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, dense::DensePolynomial,
18        dynamic::over_fixed_semiring::DynamicPolynomialFS,
19    },
20};
21use zinc_uair::{
22    ConstraintBuilder, PublicColumnLayout, ShiftSpec, TotalColumnLayout, TraceRow, Uair,
23    UairSignature, UairTrace,
24    ideal::{DegreeOneIdeal, ImpossibleIdeal},
25};
26use zinc_utils::from_ref::FromRef;
27
28#[derive(Clone, Debug)]
29pub struct TestUairSimpleMultiplication<R>(PhantomData<R>);
30
31impl<R> Uair for TestUairSimpleMultiplication<R>
32where
33    R: Semiring + 'static,
34{
35    type Ideal = ImpossibleIdeal; // Not used
36    type Scalar = DensePolynomial<R, 32>;
37
38    fn signature() -> UairSignature {
39        let total = TotalColumnLayout::new(0, 3, 0);
40        let shifts = (0..3).map(|i| ShiftSpec::new(i, 1)).collect();
41        UairSignature::new(total, PublicColumnLayout::default(), shifts, vec![])
42    }
43
44    fn constrain_general<B, FromR, MulByScalar, IFromR>(
45        b: &mut B,
46        up: TraceRow<B::Expr>,
47        down: TraceRow<B::Expr>,
48        _from_ref: FromR,
49        _mbs: MulByScalar,
50        _ideal_from_ref: IFromR,
51    ) where
52        B: ConstraintBuilder,
53    {
54        let up = up.arbitrary_poly;
55        let down = down.arbitrary_poly;
56
57        b.assert_zero(up[0].clone() * &up[1] - &down[0]);
58        b.assert_zero(up[1].clone() * &up[2] - &down[1]);
59        b.assert_zero(up[0].clone() * &up[2] - &down[2]);
60    }
61}
62
63impl<R> GenerateRandomTrace<32> for TestUairSimpleMultiplication<R>
64where
65    R: FixedSemiring + From<i8> + 'static,
66    StandardUniform: Distribution<R>,
67{
68    type PolyCoeff = R;
69    type Int = R;
70
71    fn generate_random_trace<Rng: RngCore + ?Sized>(
72        num_vars: usize,
73        rng: &mut Rng,
74    ) -> UairTrace<'static, R, R, 32> {
75        let mut a: Vec<DynamicPolynomialFS<R>> =
76            vec![DynamicPolynomialFS::new(vec![R::from(rng.random::<i8>())])];
77        let mut b: Vec<DynamicPolynomialFS<R>> = vec![DynamicPolynomialFS::new(vec![
78            R::zero(),
79            R::from(rng.random::<i8>()),
80        ])];
81        let mut c: Vec<DynamicPolynomialFS<R>> = vec![DynamicPolynomialFS::new(vec![
82            R::zero(),
83            R::from(rng.random::<i8>()),
84        ])];
85
86        for i in 1..1 << num_vars {
87            let prev_a = a[i - 1].clone();
88            let prev_b = b[i - 1].clone();
89            let prev_c = c[i - 1].clone();
90
91            a.push(prev_a.clone() * &prev_b);
92            b.push(prev_b * &prev_c);
93            c.push(prev_a * prev_c);
94        }
95
96        let arbitrary_poly = vec![
97            a.into_iter()
98                .map(|x| {
99                    assert!(
100                        x.degree() < Some(32),
101                        "degree bound exceeded: {}",
102                        x.degree().expect("if the degree is large it's not None")
103                    );
104                    DensePolynomial::new(x.coeffs)
105                })
106                .collect(),
107            b.into_iter()
108                .map(|x| {
109                    assert!(
110                        x.degree() < Some(32),
111                        "degree bound exceeded: {}",
112                        x.degree().expect("if the degree is large it's not None"),
113                    );
114                    DensePolynomial::new(x.coeffs)
115                })
116                .collect(),
117            c.into_iter()
118                .map(|x| {
119                    assert!(
120                        x.degree() < Some(32),
121                        "degree bound exceeded: {}",
122                        x.degree().expect("if the degree is large it's not None"),
123                    );
124                    DensePolynomial::new(x.coeffs)
125                })
126                .collect(),
127        ]
128        .into();
129        UairTrace {
130            arbitrary_poly,
131            ..Default::default()
132        }
133    }
134}
135
136#[derive(Clone, Debug)]
137pub struct TestUairNoMultiplication<R>(PhantomData<R>);
138
139impl<R> Uair for TestUairNoMultiplication<R>
140where
141    R: ConstSemiring + From<i32> + 'static,
142{
143    type Ideal = DegreeOneIdeal<R>;
144    type Scalar = DensePolynomial<R, 32>;
145
146    fn signature() -> UairSignature {
147        let total = TotalColumnLayout::new(0, 3, 0);
148        UairSignature::new(total, PublicColumnLayout::default(), vec![], vec![])
149    }
150
151    fn constrain_general<B, FromR, MulByScalar, IFromR>(
152        b: &mut B,
153        up: TraceRow<B::Expr>,
154        _down: TraceRow<B::Expr>,
155        _from_ref: FromR,
156        _mbs: MulByScalar,
157        ideal_from_ref: IFromR,
158    ) where
159        B: ConstraintBuilder,
160        IFromR: Fn(&Self::Ideal) -> B::Ideal,
161    {
162        let up = up.arbitrary_poly;
163
164        b.assert_in_ideal(
165            up[0].clone() + &up[1] - &up[2],
166            &ideal_from_ref(&DegreeOneIdeal::new(R::from(2))),
167        );
168    }
169}
170
171impl<R> GenerateRandomTrace<32> for TestUairNoMultiplication<R>
172where
173    R: ConstSemiring + From<i32> + 'static,
174{
175    type PolyCoeff = R;
176    type Int = R;
177
178    fn generate_random_trace<Rng: rand::RngCore + ?Sized>(
179        num_vars: usize,
180        rng: &mut Rng,
181    ) -> UairTrace<'static, R, R, 32> {
182        let a: DenseMultilinearExtension<DensePolynomial<R, 32>> =
183            DenseMultilinearExtension::rand(num_vars, rng)
184                .into_iter()
185                .map(|x: u32| {
186                    DensePolynomial::from_ref(&DensePolynomial::<Boolean, _>::from(
187                        BinaryPoly::<32>::from(x),
188                    ))
189                })
190                .collect();
191
192        let b: DenseMultilinearExtension<_> = DenseMultilinearExtension::rand(num_vars, rng)
193            .into_iter()
194            .map(|x: u32| {
195                DensePolynomial::from_ref(&DensePolynomial::<Boolean, _>::from(
196                    BinaryPoly::<32>::from(x),
197                ))
198            })
199            .collect();
200
201        let c = a.clone() + b.clone();
202
203        UairTrace {
204            arbitrary_poly: vec![a, b, c].into(),
205            ..Default::default()
206        }
207    }
208}
209
210#[derive(Clone, Debug)]
211pub struct TestUairScalarMultiplications<R>(PhantomData<R>);
212
213impl<R> Uair for TestUairScalarMultiplications<R>
214where
215    R: ConstSemiring + From<i8> + 'static,
216{
217    type Ideal = DegreeOneIdeal<R>;
218    type Scalar = DensePolynomial<R, 32>;
219
220    fn signature() -> UairSignature {
221        let total = TotalColumnLayout::new(0, 3, 0);
222        UairSignature::new(total, PublicColumnLayout::default(), vec![], vec![])
223    }
224
225    fn constrain_general<B, FromR, MulByScalar, IFromR>(
226        b: &mut B,
227        up: TraceRow<B::Expr>,
228        _down: TraceRow<B::Expr>,
229        from_ref: FromR,
230        mbs: MulByScalar,
231        ideal_from_ref: IFromR,
232    ) where
233        B: ConstraintBuilder,
234        IFromR: Fn(&Self::Ideal) -> B::Ideal,
235        FromR: Fn(&DensePolynomial<R, 32>) -> B::Expr,
236        MulByScalar: Fn(&B::Expr, &DensePolynomial<R, 32>) -> Option<B::Expr>,
237    {
238        let up = up.arbitrary_poly;
239
240        b.assert_in_ideal(
241            mbs(
242                &up[0],
243                &DensePolynomial::new([R::from(-1), R::from(0), R::from(1)]),
244            )
245            .expect("arithmetic overflow")
246                + &up[1]
247                - &up[2]
248                + from_ref(&DensePolynomial::new([
249                    R::from(1),
250                    R::from(2),
251                    R::from(3),
252                    R::from(4),
253                ])),
254            &ideal_from_ref(&DegreeOneIdeal::new(R::from(2))),
255        );
256    }
257}
258
259#[derive(Clone, Debug)]
260pub struct BinaryDecompositionUair<R>(PhantomData<R>);
261
262impl<R> Uair for BinaryDecompositionUair<R>
263where
264    R: ConstSemiring + From<u32> + 'static,
265{
266    type Ideal = DegreeOneIdeal<R>;
267    type Scalar = DensePolynomial<R, 32>;
268
269    fn signature() -> UairSignature {
270        let total = TotalColumnLayout::new(1, 0, 1);
271        UairSignature::new(total, PublicColumnLayout::default(), vec![], vec![])
272    }
273
274    fn constrain_general<B, FromR, MulByScalar, IFromR>(
275        b: &mut B,
276        up: TraceRow<B::Expr>,
277        _down: TraceRow<B::Expr>,
278        _from_ref: FromR,
279        _mbs: MulByScalar,
280        ideal_from_ref: IFromR,
281    ) where
282        B: ConstraintBuilder,
283        FromR: Fn(&Self::Scalar) -> B::Expr,
284        MulByScalar: Fn(&B::Expr, &Self::Scalar) -> Option<B::Expr>,
285        IFromR: Fn(&Self::Ideal) -> B::Ideal,
286    {
287        let int_col = &up.int[0];
288        let binary_poly_col = &up.binary_poly[0];
289
290        b.assert_in_ideal(
291            binary_poly_col.clone() - int_col,
292            &ideal_from_ref(&DegreeOneIdeal::new(R::from(2))),
293        );
294    }
295}
296
297impl<R> GenerateRandomTrace<32> for BinaryDecompositionUair<R>
298where
299    R: ConstSemiring + From<u32> + 'static,
300{
301    type PolyCoeff = R;
302    type Int = R;
303
304    fn generate_random_trace<Rng: rand::RngCore + ?Sized>(
305        num_vars: usize,
306        rng: &mut Rng,
307    ) -> UairTrace<'static, R, R, 32> {
308        let int_col_u32: DenseMultilinearExtension<u32> =
309            DenseMultilinearExtension::rand(num_vars, rng);
310
311        let binary_poly_col: DenseMultilinearExtension<BinaryPoly<32>> =
312            int_col_u32.iter().map(|i| BinaryPoly::from(*i)).collect();
313
314        let int_col = int_col_u32.into_iter().map(R::from).collect();
315
316        UairTrace {
317            binary_poly: vec![binary_poly_col].into(),
318            arbitrary_poly: vec![].into(),
319            int: vec![int_col].into(),
320        }
321    }
322}
323
324#[derive(Clone, Debug)]
325pub struct BigLinearUair<R>(PhantomData<R>);
326
327impl<R> Uair for BigLinearUair<R>
328where
329    R: ConstSemiring + From<u32> + 'static,
330{
331    type Ideal = DegreeOneIdeal<R>;
332    type Scalar = DensePolynomial<R, 32>;
333
334    fn signature() -> UairSignature {
335        let total = TotalColumnLayout::new(16, 0, 1);
336        let shifts = (0..16).map(|i| ShiftSpec::new(i, 1)).collect();
337        UairSignature::new(total, PublicColumnLayout::default(), shifts, vec![])
338    }
339
340    fn constrain_general<B, FromR, MulByScalar, IFromR>(
341        b: &mut B,
342        up: TraceRow<B::Expr>,
343        down: TraceRow<B::Expr>,
344        _from_ref: FromR,
345        _mbs: MulByScalar,
346        ideal_from_ref: IFromR,
347    ) where
348        B: ConstraintBuilder,
349        FromR: Fn(&Self::Scalar) -> B::Expr,
350        MulByScalar: Fn(&B::Expr, &Self::Scalar) -> Option<B::Expr>,
351        IFromR: Fn(&Self::Ideal) -> B::Ideal,
352    {
353        let one_ideal = DegreeOneIdeal::new(R::from(1));
354        let two_ideal = DegreeOneIdeal::new(R::from(2));
355
356        let sum_of_binary_polys = up.binary_poly[1..]
357            .iter()
358            .fold(up.binary_poly[0].clone(), |acc, next| acc + next);
359
360        // up.binary_poly[0] + up.binary_poly[1] + ... up.binary_poly[16]
361        //      = up.int[0] mod (X - 1)
362        b.assert_in_ideal(
363            sum_of_binary_polys - &up.int[0],
364            &ideal_from_ref(&one_ideal),
365        );
366
367        // down.binary_poly[0] = up.int[0] mod (X - 1)
368        b.assert_in_ideal(
369            down.binary_poly[0].clone() - &up.int[0],
370            &ideal_from_ref(&two_ideal),
371        );
372
373        // down.binary_poly[i](1) = up.binary_poly[i](1), for all i=1,...,15
374        // (preserves popcount across rows, but allows the bit pattern to change)
375        up.binary_poly[1..]
376            .iter()
377            .zip(&down.binary_poly[1..])
378            .for_each(|(up, down)| {
379                b.assert_in_ideal(up.clone() - down, &ideal_from_ref(&one_ideal));
380            });
381    }
382}
383
384impl<R> GenerateRandomTrace<32> for BigLinearUair<R>
385where
386    R: ConstSemiring + From<u32> + 'static,
387{
388    type PolyCoeff = R;
389    type Int = R;
390
391    fn generate_random_trace<Rng: rand::RngCore + ?Sized>(
392        num_vars: usize,
393        rng: &mut Rng,
394    ) -> UairTrace<'static, R, R, 32> {
395        /// Generate a random binary polynomial with the given number of 1-bits.
396        fn random_binary_poly_with_popcount(
397            popcount: u32,
398            rng: &mut (impl rand::RngCore + ?Sized),
399        ) -> BinaryPoly<32> {
400            let mut positions: [u8; 32] =
401                core::array::from_fn(|i| u8::try_from(i).expect("can't fail"));
402            for i in 0..popcount as usize {
403                let j = i + rng.next_u32() as usize % (32 - i);
404                positions.swap(i, j);
405            }
406            let mut value: u32 = 0;
407            for &pos in &positions[..popcount as usize] {
408                value |= 1u32 << pos;
409            }
410            BinaryPoly::from(value)
411        }
412
413        let mut binary_poly_cols: Vec<DenseMultilinearExtension<BinaryPoly<32>>> =
414            vec![(0..(1 << num_vars)).map(|_| BinaryPoly::zero()).collect(); 16];
415        let mut int_col: DenseMultilinearExtension<Self::Int> =
416            (0..(1 << num_vars)).map(|_| R::ZERO).collect();
417
418        binary_poly_cols.iter_mut().for_each(|col| {
419            col[0] = rng.random();
420        });
421
422        for i in 0..(1 << num_vars) - 1 {
423            let int: u32 = binary_poly_cols
424                .iter()
425                .map(|col| col[i].evaluate_at_point(&1_u32).expect("should be fine"))
426                .sum();
427            int_col[i] = R::from(int);
428
429            binary_poly_cols[0][i + 1] = BinaryPoly::from(int);
430            binary_poly_cols[1..].iter_mut().for_each(|col| {
431                let popcount = col[i].evaluate_at_point(&1_u32).expect("should be fine");
432                col[i + 1] = random_binary_poly_with_popcount(popcount, rng);
433            });
434        }
435
436        let len = int_col.len();
437
438        int_col[len - 1] = R::from(
439            binary_poly_cols
440                .iter()
441                .map(|col| {
442                    col[len - 1]
443                        .evaluate_at_point(&1_u32)
444                        .expect("should be fine")
445                })
446                .sum::<u32>(),
447        );
448
449        UairTrace {
450            binary_poly: binary_poly_cols.into(),
451            arbitrary_poly: vec![].into(),
452            int: vec![int_col].into(),
453        }
454    }
455}
456
457#[derive(Clone, Debug)]
458pub struct BigLinearUairWithPublicInput<R>(PhantomData<R>);
459
460impl<R> Uair for BigLinearUairWithPublicInput<R>
461where
462    R: ConstSemiring + From<u32> + 'static,
463{
464    type Ideal = <BigLinearUair<R> as Uair>::Ideal;
465    type Scalar = <BigLinearUair<R> as Uair>::Scalar;
466
467    fn signature() -> UairSignature {
468        let total = TotalColumnLayout::new(16, 0, 1);
469        let public = PublicColumnLayout::new(4, 0, 0);
470        let shifts = (0..16).map(|i| ShiftSpec::new(i, 1)).collect();
471        UairSignature::new(total, public, shifts, vec![])
472    }
473
474    fn constrain_general<B, FromR, MulByScalar, IFromR>(
475        b: &mut B,
476        up: TraceRow<B::Expr>,
477        down: TraceRow<B::Expr>,
478        from_ref: FromR,
479        mbs: MulByScalar,
480        ideal_from_ref: IFromR,
481    ) where
482        B: ConstraintBuilder,
483        FromR: Fn(&Self::Scalar) -> B::Expr,
484        MulByScalar: Fn(&B::Expr, &Self::Scalar) -> Option<B::Expr>,
485        IFromR: Fn(&Self::Ideal) -> B::Ideal,
486    {
487        BigLinearUair::<R>::constrain_general(b, up, down, from_ref, mbs, ideal_from_ref)
488    }
489}
490
491impl<R> GenerateRandomTrace<32> for BigLinearUairWithPublicInput<R>
492where
493    R: ConstSemiring + From<u32> + 'static,
494{
495    type PolyCoeff = <BigLinearUair<R> as GenerateRandomTrace<32>>::PolyCoeff;
496    type Int = <BigLinearUair<R> as GenerateRandomTrace<32>>::Int;
497
498    fn generate_random_trace<Rng: RngCore + ?Sized>(
499        num_vars: usize,
500        rng: &mut Rng,
501    ) -> UairTrace<'static, Self::PolyCoeff, Self::Int, 32> {
502        BigLinearUair::<R>::generate_random_trace(num_vars, rng)
503    }
504}
505
506/// A second "big linear" UAIR with 14 binary-poly columns and 4 int columns,
507/// used as a benchmarking shape distinct from `BigLinearUair`.
508///
509/// Constraints (0-based; `bp = up.binary_poly`, `int = up.int`):
510///
511/// - `bp[0][t+1] - bp[1] - bp[2] - bp[3] - int[0] - int[1] - int[2] ∈ (X-2)`
512/// - `bp[4][t+4] - bp[5] - bp[6] - bp[7] - int[1] - int[2] - int[3] ∈ (X-2)`
513/// - `bp[8] - int[0] ∈ (X-2)`
514/// - `bp[9] - int[1] ∈ (X-2)`
515/// - `bp[10] - X * bp[11] ∈ (X-1)`
516/// - `bp[12] - X * bp[13] ∈ (X-1)`
517///
518/// Note the asymmetric shift amounts: `bp[0]` is shifted by 1 (used by C1)
519/// and `bp[4]` is shifted by 4 (used by C2).
520#[derive(Clone, Debug)]
521pub struct ShaProxy<R>(PhantomData<R>);
522
523impl<R> Uair for ShaProxy<R>
524where
525    R: ConstSemiring + From<u32> + 'static,
526{
527    type Ideal = DegreeOneIdeal<R>;
528    type Scalar = DensePolynomial<R, 32>;
529
530    fn signature() -> UairSignature {
531        // 14 binary_poly cols, 0 arbitrary_poly cols, 4 int cols.
532        let total = TotalColumnLayout::new(14, 0, 4);
533        // c_1 (bp[0]) is shifted by 1 (used by C1 as bp[0][t+1]); c_5 (bp[4])
534        // is shifted by 4 (used by C2 as bp[4][t+4]).
535        let shifts = vec![ShiftSpec::new(0, 1), ShiftSpec::new(4, 4)];
536        UairSignature::new(total, PublicColumnLayout::default(), shifts, vec![])
537    }
538
539    fn constrain_general<B, FromR, MulByScalar, IFromR>(
540        b: &mut B,
541        up: TraceRow<B::Expr>,
542        down: TraceRow<B::Expr>,
543        _from_ref: FromR,
544        mbs: MulByScalar,
545        ideal_from_ref: IFromR,
546    ) where
547        B: ConstraintBuilder,
548        FromR: Fn(&Self::Scalar) -> B::Expr,
549        MulByScalar: Fn(&B::Expr, &Self::Scalar) -> Option<B::Expr>,
550        IFromR: Fn(&Self::Ideal) -> B::Ideal,
551    {
552        let one_ideal = ideal_from_ref(&DegreeOneIdeal::new(R::ONE));
553        let two_ideal = ideal_from_ref(&DegreeOneIdeal::new(R::from(2)));
554        // The polynomial X = 0 + 1*X, used to express `X * c_k` via `mbs`.
555        let x_scalar = DensePolynomial::<R, 32>::new([R::ZERO, R::from(1)]);
556
557        // `down.binary_poly` is indexed by ShiftSpec position, not source col.
558        // Our shifts vec is [ShiftSpec::new(0, 1), ShiftSpec::new(4, 1)], so
559        // down.binary_poly[0] = bp[0][t+1], down.binary_poly[1] = bp[4][t+1].
560
561        // (C1) dbp[0] - bp[1] - bp[2] - bp[3] - int[0] - int[1] - int[2] ∈ (X-2)
562        b.assert_in_ideal(
563            down.binary_poly[0].clone()
564                - &up.binary_poly[1]
565                - &up.binary_poly[2]
566                - &up.binary_poly[3]
567                - &up.int[0]
568                - &up.int[1]
569                - &up.int[2],
570            &two_ideal,
571        );
572
573        // (C2) dbp[4] - bp[5] - bp[6] - bp[7] - int[1] - int[2] - int[3] ∈ (X-2)
574        b.assert_in_ideal(
575            down.binary_poly[1].clone()
576                - &up.binary_poly[5]
577                - &up.binary_poly[6]
578                - &up.binary_poly[7]
579                - &up.int[1]
580                - &up.int[2]
581                - &up.int[3],
582            &two_ideal,
583        );
584
585        // (C3) bp[8] - int[0] ∈ (X-2)
586        b.assert_in_ideal(up.binary_poly[8].clone() - &up.int[0], &two_ideal);
587
588        // (C4) bp[9] - int[1] ∈ (X-2)
589        b.assert_in_ideal(up.binary_poly[9].clone() - &up.int[1], &two_ideal);
590
591        // (C5) bp[10] - X * bp[11] ∈ (X-1)
592        b.assert_in_ideal(
593            up.binary_poly[10].clone()
594                - &mbs(&up.binary_poly[11], &x_scalar).expect("mul-by-X overflow"),
595            &one_ideal,
596        );
597
598        // (C6) bp[12] - X * bp[13] ∈ (X-1)
599        b.assert_in_ideal(
600            up.binary_poly[12].clone()
601                - &mbs(&up.binary_poly[13], &x_scalar).expect("mul-by-X overflow"),
602            &one_ideal,
603        );
604    }
605}
606
607impl<R> GenerateRandomTrace<32> for ShaProxy<R>
608where
609    R: ConstSemiring + From<u32> + 'static,
610{
611    type PolyCoeff = R;
612    type Int = R;
613
614    #[allow(clippy::needless_range_loop)]
615    fn generate_random_trace<Rng: rand::RngCore + ?Sized>(
616        num_vars: usize,
617        rng: &mut Rng,
618    ) -> UairTrace<'static, R, R, 32> {
619        /// Generate a random binary polynomial with the given number of 1-bits.
620        fn random_binary_poly_with_popcount(
621            popcount: u32,
622            rng: &mut (impl rand::RngCore + ?Sized),
623        ) -> BinaryPoly<32> {
624            let mut positions: [u8; 32] =
625                core::array::from_fn(|i| u8::try_from(i).expect("can't fail"));
626            for i in 0..popcount as usize {
627                let j = i + rng.next_u32() as usize % (32 - i);
628                positions.swap(i, j);
629            }
630            let mut value: u32 = 0;
631            for &pos in &positions[..popcount as usize] {
632                value |= 1_u32 << pos;
633            }
634            BinaryPoly::from(value)
635        }
636
637        // Bits used by the "small" binary polys that feed into C1/C2 sums.
638        // Capping at 28 bits keeps each `eval(2)` value below 2^28 - 1, so the
639        // sum used to construct `bp[0]` / `bp[4]` at the next row stays in u32:
640        //     3 * (2^28 - 1) + 3 * 31  ≈  8.05 * 10^8  <  2^32 - 1.
641        const SMALL_MASK: u32 = (1 << 28) - 1;
642        // Range of values for the int columns (small non-negative).
643        const INT_MAX_EXCL: u32 = 32;
644
645        let len = 1 << num_vars;
646
647        let mut bp_cols: Vec<DenseMultilinearExtension<BinaryPoly<32>>> =
648            vec![(0..len).map(|_| BinaryPoly::zero()).collect(); 14];
649        let mut int_cols: Vec<DenseMultilinearExtension<R>> =
650            vec![(0..len).map(|_| R::ZERO).collect(); 4];
651
652        // Row 0 / "head row" init for the columns whose value at the head of
653        // the trace is unconstrained:
654        //   - `bp[0]` is shifted by 1, so only `bp[0][0]` is unconstrained.
655        //   - `bp[4]` is shifted by 4, so `bp[4][0..4]` are unconstrained (the C2
656        //     fix-up at iteration `i` writes `bp[4][i+4]`, so the first 4 indices are
657        //     never written by the loop).
658        bp_cols[0][0] = BinaryPoly::from(rng.next_u32() & SMALL_MASK);
659        for k in 0..4.min(len) {
660            bp_cols[4][k] = BinaryPoly::from(rng.next_u32() & SMALL_MASK);
661        }
662
663        for i in 0..len {
664            // bp[1..=3]: always small random binary polys (28-bit values).
665            for k in 1..=3 {
666                bp_cols[k][i] = BinaryPoly::from(rng.next_u32() & SMALL_MASK);
667            }
668
669            // For rows where the C2-target `bp[4][i+4]` is past the trace
670            // boundary, the protocol reads it as zero-padded. To still
671            // satisfy C2 at those rows, the C2 RHS sum must vanish at X = 2;
672            // we achieve that by zeroing `bp[5..=7]` and `int[1..=3]` here.
673            // (Mirrors the boundary trick used by `TestUairMixedShifts`.)
674            // Row `len - 1` is exempt from constraint checking by the
675            // protocol's last-row selector, so the zeroing only matters for
676            // rows `len - 4 ..= len - 2`.
677            let c2_target_oob = i + 4 >= len;
678
679            for k in 5..=7 {
680                bp_cols[k][i] = if c2_target_oob {
681                    BinaryPoly::zero()
682                } else {
683                    BinaryPoly::from(rng.next_u32() & SMALL_MASK)
684                };
685            }
686
687            // int[0..=3]: small non-negative values. Zero out int[1..=3] when
688            // we're in the C2 boundary region (int[0] can stay random — it is
689            // not used by C2).
690            let int_vals: [u32; 4] = if c2_target_oob {
691                [rng.next_u32() % INT_MAX_EXCL, 0, 0, 0]
692            } else {
693                [
694                    rng.next_u32() % INT_MAX_EXCL,
695                    rng.next_u32() % INT_MAX_EXCL,
696                    rng.next_u32() % INT_MAX_EXCL,
697                    rng.next_u32() % INT_MAX_EXCL,
698                ]
699            };
700            for (k, v) in int_vals.iter().enumerate() {
701                int_cols[k][i] = R::from(*v);
702            }
703
704            // (C3)/(C4): bp[8] = BinaryPoly::from(int[0]);
705            // bp[9] = BinaryPoly::from(int[1]).
706            // Since `BinaryPoly::from(n).evaluate_at_point(2) == n`,
707            // this makes `bp[k] - int[k]` vanish at X=2, satisfying the (X-2) ideal check.
708            bp_cols[8][i] = BinaryPoly::from(int_vals[0]);
709            bp_cols[9][i] = BinaryPoly::from(int_vals[1]);
710
711            // (C5): popcount(bp[10]) == popcount(bp[11]).
712            let bp11: BinaryPoly<32> = rng.random();
713            let popcount11 = bp11
714                .evaluate_at_point(&1_u32)
715                .expect("popcount eval should fit in u32");
716            bp_cols[11][i] = bp11;
717            bp_cols[10][i] = random_binary_poly_with_popcount(popcount11, rng);
718
719            // (C6): popcount(bp[12]) == popcount(bp[13]).
720            let bp13: BinaryPoly<32> = rng.random();
721            let popcount13 = bp13
722                .evaluate_at_point(&1_u32)
723                .expect("popcount eval should fit in u32");
724            bp_cols[13][i] = bp13;
725            bp_cols[12][i] = random_binary_poly_with_popcount(popcount13, rng);
726
727            // Set bp[0][i+1] and bp[4][i+4] so C1 and C2 respectively hold at
728            // row i. Each summand fits in u32 (bp eval ≤ 2^28 - 1, ints ≤ 31),
729            // so each sum (≤ 3 * (2^28 - 1) + 3 * 31 ≈ 8.05e8) stays well
730            // below 2^32. C1 and C2 have different shift amounts now (1 vs 4)
731            // and so need separate `if` guards.
732            let eval_at_2 = |bp: &BinaryPoly<32>| -> u32 {
733                bp.evaluate_at_point(&2_u32)
734                    .expect("28-bit binary poly eval at 2 fits in u32")
735            };
736
737            // C1 (shift = 1)
738            if i + 1 < len {
739                let s1 = eval_at_2(&bp_cols[1][i])
740                    + eval_at_2(&bp_cols[2][i])
741                    + eval_at_2(&bp_cols[3][i])
742                    + int_vals[0]
743                    + int_vals[1]
744                    + int_vals[2];
745                bp_cols[0][i + 1] = BinaryPoly::from(s1);
746            }
747
748            // C2 (shift = 4)
749            if i + 4 < len {
750                let s2 = eval_at_2(&bp_cols[5][i])
751                    + eval_at_2(&bp_cols[6][i])
752                    + eval_at_2(&bp_cols[7][i])
753                    + int_vals[1]
754                    + int_vals[2]
755                    + int_vals[3];
756                bp_cols[4][i + 4] = BinaryPoly::from(s2);
757            }
758        }
759
760        UairTrace {
761            binary_poly: bp_cols.into(),
762            arbitrary_poly: vec![].into(),
763            int: int_cols.into(),
764        }
765    }
766}
767
768/// Test UAIR with mixed shift amounts.
769/// 3 columns (a, b, c): column a shifts by 1, column b shifts by 2.
770/// Constraints are linear (degree 1).
771#[derive(Clone, Debug)]
772pub struct TestUairMixedShifts<R>(PhantomData<R>);
773
774impl<R> Uair for TestUairMixedShifts<R>
775where
776    R: Semiring + 'static,
777{
778    type Ideal = ImpossibleIdeal;
779    type Scalar = DensePolynomial<R, 32>;
780
781    fn signature() -> UairSignature {
782        let total = TotalColumnLayout::new(0, 3, 0);
783        let shifts = vec![
784            ShiftSpec::new(0, 1), // a shifted by 1
785            ShiftSpec::new(1, 2), // b shifted by 2
786        ];
787        UairSignature::new(total, PublicColumnLayout::default(), shifts, vec![])
788    }
789
790    // Constraints:
791    //   a[i+1] = a[i] + b[i]  →  down[0] - up[0] - up[1] = 0
792    //   c[i]   = b[i+2]       →  up[2] - down[1] = 0
793    fn constrain_general<B, FromR, MulByScalar, IFromR>(
794        builder: &mut B,
795        up: TraceRow<B::Expr>,
796        down: TraceRow<B::Expr>,
797        _from_ref: FromR,
798        _mbs: MulByScalar,
799        _ideal_from_ref: IFromR,
800    ) where
801        B: ConstraintBuilder,
802    {
803        let up = up.arbitrary_poly;
804        let down = down.arbitrary_poly;
805
806        builder.assert_zero(down[0].clone() - &up[0] - &up[1]);
807        builder.assert_zero(up[2].clone() - &down[1]);
808    }
809}
810
811impl<R> GenerateRandomTrace<32> for TestUairMixedShifts<R>
812where
813    R: FixedSemiring + From<i8> + 'static,
814    StandardUniform: Distribution<R>,
815{
816    type PolyCoeff = R;
817    type Int = R;
818
819    // Witness: random b, derive a from a[i+1] = a[i] + b[i], set c[i] = b[i+2].
820    fn generate_random_trace<Rng: rand::RngCore + ?Sized>(
821        num_vars: usize,
822        rng: &mut Rng,
823    ) -> UairTrace<'static, R, R, 32> {
824        let n = 1 << num_vars;
825
826        // Random b column (degree-0 polynomials to stay under degree 32)
827        let b_col: Vec<DynamicPolynomialFS<R>> = (0..n)
828            .map(|_| DynamicPolynomialFS::new(vec![R::from(rng.random::<i8>())]))
829            .collect();
830
831        // a[0] random, a[i+1] = a[i] + b[i]
832        let mut a_col: Vec<DynamicPolynomialFS<R>> =
833            vec![DynamicPolynomialFS::new(vec![R::from(rng.random::<i8>())])];
834        for i in 0..n - 1 {
835            a_col.push(a_col[i].clone() + &b_col[i]);
836        }
837
838        // c[i] = b[i+2], zero-padded for last 2 entries
839        let mut c_col: Vec<DynamicPolynomialFS<R>> = Vec::with_capacity(n);
840        for i in 0..n {
841            if i + 2 < n {
842                c_col.push(b_col[i + 2].clone());
843            } else {
844                c_col.push(DynamicPolynomialFS::zero());
845            }
846        }
847
848        let to_mle = |col: Vec<DynamicPolynomialFS<R>>| -> DenseMultilinearExtension<DensePolynomial<R, 32>> {
849            col.into_iter()
850                .map(|x| DensePolynomial::new(x.coeffs))
851                .collect()
852        };
853
854        UairTrace {
855            arbitrary_poly: vec![to_mle(a_col), to_mle(b_col), to_mle(c_col)].into(),
856            ..Default::default()
857        }
858    }
859}
860
861#[cfg(test)]
862mod tests {
863    use crypto_primitives::crypto_bigint_int::Int;
864    use zinc_uair::{
865        collect_scalars::collect_scalars,
866        constraint_counter::count_constraints,
867        degree_counter::{count_constraint_degrees, count_max_degree},
868    };
869
870    use super::*;
871
872    const LIMBS: usize = 4;
873
874    #[test]
875    fn test_constraint_degrees() {
876        fn assert_uair_shape<U: Uair>(expected_degrees: &[usize]) {
877            assert_eq!(count_constraints::<U>(), expected_degrees.len());
878            assert_eq!(count_constraint_degrees::<U>(), expected_degrees);
879            assert_eq!(
880                count_max_degree::<U>(),
881                *expected_degrees.iter().max().unwrap()
882            );
883        }
884
885        assert_uair_shape::<TestUairSimpleMultiplication<Int<LIMBS>>>(&[2, 2, 2]);
886        assert_uair_shape::<TestUairNoMultiplication<Int<LIMBS>>>(&[1]);
887        assert_uair_shape::<TestUairScalarMultiplications<Int<LIMBS>>>(&[1]);
888        assert_uair_shape::<BinaryDecompositionUair<u32>>(&[1]);
889        assert_uair_shape::<BigLinearUair<u32>>(&[1; 17]);
890        assert_uair_shape::<TestUairMixedShifts<Int<LIMBS>>>(&[1, 1]);
891    }
892
893    #[test]
894    fn test_air_scalar_multiplications_correct_collect_scalars() {
895        assert_eq!(
896            collect_scalars::<TestUairScalarMultiplications<Int<LIMBS>>>(),
897            (vec![
898                DensePolynomial::new([Int::from_i8(-1), Int::from_i8(0), Int::from_i8(1)]),
899                DensePolynomial::new([
900                    Int::from_i8(1),
901                    Int::from_i8(2),
902                    Int::from_i8(3),
903                    Int::from_i8(4),
904                ])
905            ]
906            .into_iter()
907            .collect())
908        );
909    }
910}