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
16pub const NUM_MINIMAL_ADD_COLS: usize = size_of::<MinimalAddCols<u8>>();
18
19#[derive(Default, Clone)]
21pub struct MinimalAddChip;
22
23#[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 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 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}