Skip to main content

sp1_core_machine/utils/
zerocheck_unit_test.rs

1use core::{
2    borrow::{Borrow, BorrowMut},
3    mem::size_of,
4};
5
6use slop_air::{Air, BaseAir};
7use slop_algebra::{AbstractField, PrimeField32};
8use slop_matrix::{dense::RowMajorMatrix, Matrix};
9use slop_maybe_rayon::prelude::{ParallelBridge, ParallelIterator};
10use sp1_core_executor::Program;
11use sp1_derive::AlignedBorrow;
12use sp1_hypercube::air::{MachineAir, SP1AirBuilder};
13
14use crate::utils::zeroed_f_vec;
15
16/// The number of main trace columns for `AddiChip`.
17pub const NUM_MINIMAL_ADD_COLS: usize = size_of::<MinimalAddCols<u8>>();
18
19/// A chip that implements addition for the opcode ADDI.
20#[derive(Default, Clone)]
21pub struct MinimalAddChip;
22
23/// The column layout for the chip.
24#[derive(AlignedBorrow, Default, Clone, Copy)]
25#[repr(C)]
26pub struct MinimalAddCols<T> {
27    op_a: T,
28    op_b: T,
29    op_c: T,
30}
31
32impl<F> BaseAir<F> for MinimalAddChip {
33    fn width(&self) -> usize {
34        NUM_MINIMAL_ADD_COLS
35    }
36}
37
38impl<F: PrimeField32> MachineAir<F> for MinimalAddChip {
39    type Record = Vec<(u32, u32, u32)>;
40
41    type Program = Program;
42
43    fn name(&self) -> &'static str {
44        "MinimalAdd"
45    }
46
47    fn num_rows(&self, input: &Self::Record) -> Option<usize> {
48        Some(input.len().next_multiple_of(32))
49    }
50
51    fn generate_trace_into(
52        &self,
53        _input: &Self::Record,
54        _output: &mut Self::Record,
55        _buffer: &mut [std::mem::MaybeUninit<F>],
56    ) {
57        unimplemented!();
58    }
59
60    fn generate_trace(
61        &self,
62        input: &Vec<(u32, u32, u32)>,
63        _: &mut Vec<(u32, u32, u32)>,
64    ) -> RowMajorMatrix<F> {
65        // Generate the rows for the trace.
66        let chunk_size = std::cmp::max(input.len() / num_cpus::get(), 1);
67        let mut values = zeroed_f_vec(input.len() * NUM_MINIMAL_ADD_COLS);
68
69        values.chunks_mut(chunk_size * NUM_MINIMAL_ADD_COLS).enumerate().par_bridge().for_each(
70            |(i, rows)| {
71                rows.chunks_mut(NUM_MINIMAL_ADD_COLS).enumerate().for_each(|(j, row)| {
72                    let idx = i * chunk_size + j;
73                    let cols: &mut MinimalAddCols<F> = row.borrow_mut();
74
75                    if idx < input.len() {
76                        let event = input[idx];
77                        cols.op_a = F::from_canonical_u32(event.0);
78                        cols.op_b = F::from_canonical_u32(event.1);
79                        cols.op_c = F::from_canonical_u32(event.2);
80                    }
81                });
82            },
83        );
84        // Convert the trace to a row major matrix.
85        RowMajorMatrix::new(values, NUM_MINIMAL_ADD_COLS)
86    }
87
88    fn included(&self, shard: &Self::Record) -> bool {
89        !shard.is_empty()
90    }
91}
92
93impl<AB> Air<AB> for MinimalAddChip
94where
95    AB: SP1AirBuilder,
96{
97    fn eval(&self, builder: &mut AB) {
98        let main = builder.main();
99        let local = main.row_slice(0);
100        let local: &MinimalAddCols<AB::Var> = (*local).borrow();
101
102        builder.assert_eq(local.op_a, local.op_b + local.op_c + AB::Expr::one());
103    }
104}
105
106impl std::fmt::Debug for MinimalAddChip {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        write!(f, "MinimalAddChip")
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    #![allow(clippy::print_stdout)]
115
116    use std::sync::Arc;
117
118    use itertools::Itertools;
119    use rand::Rng;
120    use slop_air::Air;
121    use slop_algebra::{extension::BinomialExtensionField, AbstractField};
122    use slop_alloc::CpuBackend;
123    use slop_challenger::IopCtx;
124    use slop_matrix::dense::{RowMajorMatrix, RowMajorMatrixView};
125    use slop_multilinear::{full_geq, Mle, MleEval, PaddedMle, Padding, Point, VirtualGeq};
126    use slop_sumcheck::{partially_verify_sumcheck_proof, reduce_sumcheck_to_evaluation};
127    use slop_uni_stark::get_symbolic_constraints;
128    use sp1_hypercube::{
129        air::MachineAir,
130        debug_constraints,
131        prover::{ZeroCheckPoly, ZerocheckCpuProverData},
132        AirOpenedValues, Chip, ChipOpenedValues, ConstraintSumcheckFolder, InnerSC, ShardVerifier,
133        PROOF_MAX_NUM_PVS,
134    };
135    use sp1_primitives::{SP1Field, SP1GlobalContext};
136
137    use crate::utils::{setup_logger, MinimalAddChip, NUM_MINIMAL_ADD_COLS};
138
139    type F = sp1_primitives::SP1Field;
140    type EF = BinomialExtensionField<F, 4>;
141
142    #[test]
143    fn test_zerocheck() {
144        setup_logger();
145        let mut rng = rand::thread_rng();
146        let air = MinimalAddChip::default();
147        let num_real_entries = 65;
148        let num_variables = 7;
149
150        let mut shard = Vec::new();
151
152        for _ in 0..num_real_entries {
153            let operand_1 = rand::thread_rng().gen_range(0..(u16::MAX as u32));
154            let operand_2 = rand::thread_rng().gen_range(0..(u16::MAX as u32));
155
156            let result = operand_1.wrapping_add(operand_2) + 1;
157
158            shard.push((result, operand_1, operand_2));
159        }
160
161        let virtually_padded_trace =
162            MinimalAddChip::generate_trace(&MinimalAddChip, &shard, &mut Vec::new());
163
164        assert!(<MinimalAddChip as MachineAir<F>>::preprocessed_width(&air) == 0);
165
166        let alpha = rng.gen::<EF>();
167        let gkr_power = rng.gen::<EF>();
168
169        let num_constraints = get_symbolic_constraints::<F, _>(&air, 0, PROOF_MAX_NUM_PVS).len();
170
171        let mut alpha_powers = alpha.powers().take(num_constraints).collect::<Vec<_>>();
172
173        alpha_powers.reverse();
174
175        let gkr_powers = gkr_power.powers().take(NUM_MINIMAL_ADD_COLS).collect::<Vec<_>>();
176
177        let main_trace = PaddedMle::new(
178            Some(Arc::new(virtually_padded_trace.clone().into())),
179            num_variables,
180            Padding::Constant((F::zero(), NUM_MINIMAL_ADD_COLS, CpuBackend)),
181        );
182
183        let virtual_geq =
184            VirtualGeq::new(num_real_entries as u32, F::one(), F::zero(), num_variables);
185
186        let air_data = ZerocheckCpuProverData::round_prover(
187            Arc::new(air),
188            Arc::new(vec![F::zero(); PROOF_MAX_NUM_PVS]),
189            Arc::new(alpha_powers.clone()),
190            Arc::new(gkr_powers.clone()),
191        );
192
193        let dummy_main = vec![F::zero(); NUM_MINIMAL_ADD_COLS];
194
195        let mut folder = ConstraintSumcheckFolder {
196            preprocessed: RowMajorMatrixView::new_row(&[]),
197            main: RowMajorMatrixView::new_row(&dummy_main),
198            accumulator: EF::zero(),
199            public_values: &vec![F::zero(); PROOF_MAX_NUM_PVS],
200            constraint_index: 0,
201            powers_of_alpha: &alpha_powers,
202        };
203
204        let air = MinimalAddChip::default();
205
206        air.eval(&mut folder);
207        let padded_row_adjustment = folder.accumulator;
208
209        let zeta = Point::rand(&mut rng, num_variables);
210
211        let gkr_openings: MleEval<EF> = main_trace.eval_at(&zeta);
212
213        let sumcheck_claim = gkr_openings
214            .evaluations()
215            .as_slice()
216            .iter()
217            .zip_eq(gkr_powers.iter())
218            .map(|(a, b)| *a * *b)
219            .sum::<EF>();
220
221        let zerocheck_poly = ZeroCheckPoly::<F, F, EF, _>::new(
222            air_data,
223            zeta.clone(),
224            None,
225            main_trace.clone(),
226            EF::one(),
227            EF::zero(),
228            padded_row_adjustment,
229            virtual_geq,
230        );
231
232        let claims = vec![sumcheck_claim];
233        let t = 1;
234        let lambda = EF::zero();
235
236        let mut challenger = SP1GlobalContext::default_challenger();
237
238        let (proof, column_openings) =
239            reduce_sumcheck_to_evaluation(vec![zerocheck_poly], &mut challenger, claims, t, lambda);
240
241        let chip_eval_claim = proof.point_and_eval.1;
242        let chip_eval_point = proof.point_and_eval.0.clone();
243
244        let column_openings = &column_openings[0];
245
246        assert_eq!(column_openings, &main_trace.eval_at(&chip_eval_point).to_vec());
247
248        let opening = ChipOpenedValues::<F, EF> {
249            preprocessed: AirOpenedValues { local: vec![] },
250            main: AirOpenedValues { local: column_openings.clone() },
251            degree: Point::from_usize(num_real_entries as usize, num_variables as usize + 1),
252        };
253
254        let openings_batch = column_openings
255            .iter()
256            .zip_eq(gkr_powers.iter())
257            .map(|(opening, power)| *opening * *power)
258            .sum::<EF>();
259
260        let public_values = vec![F::zero(); PROOF_MAX_NUM_PVS];
261
262        let zerocheck_eq_val = Mle::full_lagrange_eval(&zeta, &chip_eval_point);
263
264        let mut challenger = SP1GlobalContext::default_challenger();
265
266        let padded_row_adjustment =
267            ShardVerifier::<SP1GlobalContext, InnerSC<_>>::compute_padded_row_adjustment(
268                &Chip::new(MinimalAddChip::default()),
269                alpha,
270                &public_values,
271            );
272
273        let mut point_extended = chip_eval_point.clone();
274        point_extended.add_dimension(EF::zero());
275
276        let geq_val = full_geq(&opening.degree, &point_extended);
277
278        let eval = ShardVerifier::<SP1GlobalContext, InnerSC<_>>::eval_constraints(
279            &Chip::new(MinimalAddChip::default()),
280            &opening,
281            alpha,
282            &public_values,
283        );
284
285        let constraint_eval = eval - padded_row_adjustment * geq_val;
286
287        partially_verify_sumcheck_proof(&proof, &mut challenger, num_variables as usize, 4)
288            .unwrap();
289        assert_eq!(chip_eval_claim, zerocheck_eq_val * (constraint_eval + openings_batch));
290    }
291
292    #[test]
293    fn test_debug_constraints() {
294        setup_logger();
295        let num_real_entries = 65;
296
297        let mut shard = Vec::new();
298
299        for _ in 0..num_real_entries {
300            let operand_1 = rand::thread_rng().gen_range(0..(u16::MAX as u32));
301            let operand_2 = rand::thread_rng().gen_range(0..(u16::MAX as u32));
302
303            let result = operand_1.wrapping_add(operand_2) + 1;
304
305            shard.push((result, operand_1, operand_2));
306        }
307
308        let virtually_padded_trace: RowMajorMatrix<SP1Field> =
309            MinimalAddChip::generate_trace(&MinimalAddChip, &shard, &mut Vec::new());
310
311        let main_trace: Mle<SP1Field> = Mle::from(virtually_padded_trace);
312
313        debug_constraints::<SP1GlobalContext, _>(
314            &Chip::new(MinimalAddChip::default()),
315            None,
316            &main_trace,
317            &[],
318        );
319    }
320
321    #[test]
322    fn test_debug_constraints_failing() {
323        setup_logger();
324        let num_real_entries = 65;
325
326        let mut shard = Vec::new();
327
328        for i in 0..num_real_entries {
329            let operand_1 = rand::thread_rng().gen_range(0..(u16::MAX as u32));
330            let operand_2 = rand::thread_rng().gen_range(0..(u16::MAX as u32));
331
332            let mut result = operand_1.wrapping_add(operand_2) + 1;
333
334            if i == 27 {
335                result += 42;
336            }
337
338            shard.push((result, operand_1, operand_2));
339        }
340
341        let virtually_padded_trace: RowMajorMatrix<SP1Field> =
342            MinimalAddChip::generate_trace(&MinimalAddChip, &shard, &mut Vec::new());
343
344        let main_trace: Mle<SP1Field> = Mle::from(virtually_padded_trace);
345
346        debug_constraints::<SP1GlobalContext, _>(
347            &Chip::new(MinimalAddChip::default()),
348            None,
349            &main_trace,
350            &[],
351        );
352    }
353}