sonobe/folding-schemes/src/folding/circuits/sum_check.rs
folding-schemes/src/folding/circuits/sum_check.rsRaw
1/// Heavily inspired from testudo: https://github.com/cryptonetlab/testudo/tree/master
2/// Some changes:
3/// - Typings to better stick to ark_poly's API
4/// - Uses `folding-schemes`' own `TranscriptVar` trait and `PoseidonTranscriptVar` struct
5/// - API made closer to gadgets found in `folding-schemes`
6use ark_crypto_primitives::sponge::Absorb;
7use ark_ec::{CurveGroup, Group};
8use ark_ff::PrimeField;
9use ark_poly::{univariate::DensePolynomial, DenseUVPolynomial};
10use ark_r1cs_std::{
11 alloc::{AllocVar, AllocationMode},
12 boolean::Boolean,
13 eq::EqGadget,
14 fields::{fp::FpVar, FieldVar},
15};
16use ark_relations::r1cs::{Namespace, SynthesisError};
17use std::{borrow::Borrow, marker::PhantomData};
18
19use crate::utils::espresso::sum_check::SumCheck;
20use crate::utils::virtual_polynomial::VPAuxInfo;
21use crate::{
22 transcript::{poseidon::PoseidonTranscript, TranscriptVar},
23 utils::sum_check::{structs::IOPProof, IOPSumCheck},
24};
25
26#[derive(Clone, Debug)]
27pub struct DensePolynomialVar<F: PrimeField> {
28 pub coeffs: Vec<FpVar<F>>,
29}
30
31impl<F: PrimeField> AllocVar<DensePolynomial<F>, F> for DensePolynomialVar<F> {
32 fn new_variable<T: Borrow<DensePolynomial<F>>>(
33 cs: impl Into<Namespace<F>>,
34 f: impl FnOnce() -> Result<T, SynthesisError>,
35 mode: AllocationMode,
36 ) -> Result<Self, SynthesisError> {
37 f().and_then(|c| {
38 let cs = cs.into();
39 let cp: &DensePolynomial<F> = c.borrow();
40 let mut coeffs_var = Vec::<FpVar<F>>::with_capacity(cp.coeffs.len());
41 for coeff in cp.coeffs.iter() {
42 let coeff_var = FpVar::<F>::new_variable(cs.clone(), || Ok(coeff), mode)?;
43 coeffs_var.push(coeff_var);
44 }
45 Ok(Self { coeffs: coeffs_var })
46 })
47 }
48}
49
50impl<F: PrimeField> DensePolynomialVar<F> {
51 pub fn eval_at_zero(&self) -> FpVar<F> {
52 if self.coeffs.is_empty() {
53 return FpVar::<F>::zero();
54 }
55 self.coeffs[0].clone()
56 }
57
58 pub fn eval_at_one(&self) -> FpVar<F> {
59 if self.coeffs.is_empty() {
60 return FpVar::<F>::zero();
61 }
62 let mut res = self.coeffs[0].clone();
63 for i in 1..self.coeffs.len() {
64 res = &res + &self.coeffs[i];
65 }
66 res
67 }
68
69 pub fn evaluate(&self, r: &FpVar<F>) -> FpVar<F> {
70 if self.coeffs.is_empty() {
71 return FpVar::<F>::zero();
72 }
73 let mut eval = self.coeffs[0].clone();
74 let mut power = r.clone();
75
76 for i in 1..self.coeffs.len() {
77 eval += &power * &self.coeffs[i];
78 power *= r;
79 }
80 eval
81 }
82}
83
84#[derive(Clone, Debug)]
85pub struct IOPProofVar<C: CurveGroup> {
86 // We have to be generic over a CurveGroup because instantiating a IOPProofVar will call IOPSumCheck which requires a CurveGroup
87 pub proofs: Vec<DensePolynomialVar<C::ScalarField>>, // = IOPProof.proofs
88 pub claim: FpVar<C::ScalarField>,
89}
90
91impl<C: CurveGroup> AllocVar<IOPProof<C::ScalarField>, C::ScalarField> for IOPProofVar<C>
92where
93 <C as Group>::ScalarField: Absorb,
94{
95 fn new_variable<T: Borrow<IOPProof<C::ScalarField>>>(
96 cs: impl Into<Namespace<C::ScalarField>>,
97 f: impl FnOnce() -> Result<T, SynthesisError>,
98 mode: AllocationMode,
99 ) -> Result<Self, SynthesisError> {
100 f().and_then(|c| {
101 let cs = cs.into();
102 let cp: &IOPProof<C::ScalarField> = c.borrow();
103 let claim = IOPSumCheck::<C, PoseidonTranscript<C>>::extract_sum(cp);
104 let claim = FpVar::<C::ScalarField>::new_variable(cs.clone(), || Ok(claim), mode)?;
105 let mut proofs =
106 Vec::<DensePolynomialVar<C::ScalarField>>::with_capacity(cp.proofs.len());
107 for proof in cp.proofs.iter() {
108 let poly = DensePolynomial::from_coefficients_slice(&proof.coeffs);
109 let proof = DensePolynomialVar::<C::ScalarField>::new_variable(
110 cs.clone(),
111 || Ok(poly),
112 mode,
113 )?;
114 proofs.push(proof);
115 }
116 Ok(Self { proofs, claim })
117 })
118 }
119}
120
121#[derive(Clone, Debug)]
122pub struct VPAuxInfoVar<F: PrimeField> {
123 pub num_variables: FpVar<F>,
124 pub max_degree: FpVar<F>,
125}
126
127impl<F: PrimeField> AllocVar<VPAuxInfo<F>, F> for VPAuxInfoVar<F> {
128 fn new_variable<T: Borrow<VPAuxInfo<F>>>(
129 cs: impl Into<Namespace<F>>,
130 f: impl FnOnce() -> Result<T, SynthesisError>,
131 mode: AllocationMode,
132 ) -> Result<Self, SynthesisError> {
133 f().and_then(|c| {
134 let cs = cs.into();
135 let cp: &VPAuxInfo<F> = c.borrow();
136 let num_variables = FpVar::<F>::new_variable(
137 cs.clone(),
138 || Ok(F::from(cp.num_variables as u64)),
139 mode,
140 )?;
141 let max_degree =
142 FpVar::<F>::new_variable(cs.clone(), || Ok(F::from(cp.max_degree as u64)), mode)?;
143 Ok(Self {
144 num_variables,
145 max_degree,
146 })
147 })
148 }
149}
150
151#[derive(Debug, Clone)]
152pub struct SumCheckVerifierGadget<C: CurveGroup> {
153 _f: PhantomData<C>,
154}
155
156impl<C: CurveGroup> SumCheckVerifierGadget<C> {
157 #[allow(clippy::type_complexity)]
158 pub fn verify(
159 iop_proof_var: &IOPProofVar<C>,
160 poly_aux_info_var: &VPAuxInfoVar<C::ScalarField>,
161 transcript_var: &mut impl TranscriptVar<C::ScalarField>,
162 enabled: Boolean<C::ScalarField>,
163 ) -> Result<(Vec<FpVar<C::ScalarField>>, Vec<FpVar<C::ScalarField>>), SynthesisError> {
164 let mut e_vars = vec![iop_proof_var.claim.clone()];
165 let mut r_vars: Vec<FpVar<C::ScalarField>> = Vec::new();
166 transcript_var.absorb(poly_aux_info_var.num_variables.clone())?;
167 transcript_var.absorb(poly_aux_info_var.max_degree.clone())?;
168
169 for poly_var in iop_proof_var.proofs.iter() {
170 let res = poly_var.eval_at_one() + poly_var.eval_at_zero();
171 let e_var = e_vars.last().ok_or(SynthesisError::Unsatisfiable)?;
172 res.conditional_enforce_equal(e_var, &enabled)?;
173 transcript_var.absorb_vec(&poly_var.coeffs)?;
174 let r_i_var = transcript_var.get_challenge()?;
175 e_vars.push(poly_var.evaluate(&r_i_var));
176 r_vars.push(r_i_var);
177 }
178
179 Ok((e_vars, r_vars))
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use ark_crypto_primitives::sponge::{poseidon::PoseidonConfig, Absorb};
186 use ark_ec::CurveGroup;
187 use ark_ff::Field;
188 use ark_pallas::{Fr, Projective};
189 use ark_poly::{
190 univariate::DensePolynomial, DenseMultilinearExtension, DenseUVPolynomial,
191 MultilinearExtension, Polynomial,
192 };
193 use ark_r1cs_std::{alloc::AllocVar, R1CSVar};
194 use ark_relations::r1cs::ConstraintSystem;
195 use std::sync::Arc;
196
197 use super::*;
198 use crate::{
199 folding::circuits::sum_check::{IOPProofVar, VPAuxInfoVar},
200 transcript::{
201 poseidon::{poseidon_canonical_config, PoseidonTranscript, PoseidonTranscriptVar},
202 Transcript, TranscriptVar,
203 },
204 utils::{
205 sum_check::{structs::IOPProof, IOPSumCheck, SumCheck},
206 virtual_polynomial::VirtualPolynomial,
207 },
208 };
209
210 pub type TestSumCheckProof<F> = (VirtualPolynomial<F>, PoseidonConfig<F>, IOPProof<F>);
211
212 /// Primarily used for testing the sumcheck gadget
213 /// Returns a random virtual polynomial, the poseidon config used and the associated sumcheck proof
214 pub fn get_test_sumcheck_proof<C: CurveGroup>(
215 num_vars: usize,
216 ) -> TestSumCheckProof<C::ScalarField>
217 where
218 <C as ark_ec::Group>::ScalarField: Absorb,
219 {
220 let mut rng = ark_std::test_rng();
221 let poseidon_config: PoseidonConfig<C::ScalarField> =
222 poseidon_canonical_config::<C::ScalarField>();
223 let mut poseidon_transcript_prove = PoseidonTranscript::<C>::new(&poseidon_config);
224 let poly_mle = DenseMultilinearExtension::rand(num_vars, &mut rng);
225 let virtual_poly =
226 VirtualPolynomial::new_from_mle(&Arc::new(poly_mle), C::ScalarField::ONE);
227 let sum_check: IOPProof<C::ScalarField> = IOPSumCheck::<C, PoseidonTranscript<C>>::prove(
228 &virtual_poly,
229 &mut poseidon_transcript_prove,
230 )
231 .unwrap();
232 (virtual_poly, poseidon_config, sum_check)
233 }
234
235 #[test]
236 fn test_sum_check_circuit() {
237 for num_vars in 1..15 {
238 let cs = ConstraintSystem::<Fr>::new_ref();
239 let (virtual_poly, poseidon_config, sum_check) =
240 get_test_sumcheck_proof::<Projective>(num_vars);
241 let mut poseidon_var: PoseidonTranscriptVar<Fr> =
242 PoseidonTranscriptVar::new(cs.clone(), &poseidon_config);
243 let iop_proof_var =
244 IOPProofVar::<Projective>::new_witness(cs.clone(), || Ok(&sum_check)).unwrap();
245 let poly_aux_info_var =
246 VPAuxInfoVar::<Fr>::new_witness(cs.clone(), || Ok(virtual_poly.aux_info)).unwrap();
247 let enabled = Boolean::<Fr>::new_witness(cs.clone(), || Ok(true)).unwrap();
248 let res = SumCheckVerifierGadget::<Projective>::verify(
249 &iop_proof_var,
250 &poly_aux_info_var,
251 &mut poseidon_var,
252 enabled,
253 );
254
255 assert!(res.is_ok());
256 let (circuit_evals, r_challenges) = res.unwrap();
257
258 // 1. assert claim from circuit is equal to the one from the sum-check
259 let claim: Fr =
260 IOPSumCheck::<Projective, PoseidonTranscript<Projective>>::extract_sum(&sum_check);
261 assert_eq!(circuit_evals[0].value().unwrap(), claim);
262
263 // 2. assert that all in-circuit evaluations are equal to the ones from the sum-check
264 for ((proof, point), circuit_eval) in sum_check
265 .proofs
266 .iter()
267 .zip(sum_check.point.iter())
268 .zip(circuit_evals.iter().skip(1))
269 // we skip the first one since it's the above checked claim
270 {
271 let poly = DensePolynomial::from_coefficients_slice(&proof.coeffs);
272 let eval = poly.evaluate(point);
273 assert_eq!(eval, circuit_eval.value().unwrap());
274 }
275
276 // 3. assert that all challenges are equal to the ones from the sum-check
277 for (point, r_challenge) in sum_check.point.iter().zip(r_challenges.iter()) {
278 assert_eq!(*point, r_challenge.value().unwrap());
279 }
280
281 assert!(cs.is_satisfied().unwrap());
282 }
283 }
284}