Skip to main content

zinc_piop/sumcheck/
prover.rs

1//! Prover
2
3use crypto_primitives::{SemiringConfig, SetConfig};
4use itertools::Itertools;
5#[cfg(feature = "parallel")]
6use rayon::iter::*;
7use std::slice;
8use zinc_poly::mle::{DenseMultilinearExtension, MultilinearExtension};
9use zinc_transcript::{delegate_transcribable, traits::ConstTranscribable};
10use zinc_utils::{cfg_into_iter, cfg_iter_mut};
11
12/// Evaluation of a polynomial on natural points without the constant term.
13#[repr(transparent)]
14#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct NatEvaluatedPolyWithoutConstant<F> {
16    /// Evaluations at 1, 2, ... (P(0) is omitted).
17    pub tail_evaluations: Vec<F>,
18}
19
20impl<F> NatEvaluatedPolyWithoutConstant<F> {
21    pub fn new(tail_evaluations: Vec<F>) -> Self {
22        Self { tail_evaluations }
23    }
24
25    /// Maps every field element through `f`, preserving structure — used to
26    /// lift elements into wire integers and to project wire integers back
27    /// into elements at the (de)serialization boundary.
28    pub fn try_map<T, E>(
29        &self,
30        f: impl FnMut(&F) -> Result<T, E> + Copy,
31    ) -> Result<NatEvaluatedPolyWithoutConstant<T>, E> {
32        Ok(NatEvaluatedPolyWithoutConstant {
33            tail_evaluations: self.tail_evaluations.iter().map(f).try_collect()?,
34        })
35    }
36}
37
38impl<F> std::ops::Deref for NatEvaluatedPolyWithoutConstant<F> {
39    type Target = [F];
40
41    fn deref(&self) -> &Self::Target {
42        &self.tail_evaluations
43    }
44}
45
46impl<F> std::ops::DerefMut for NatEvaluatedPolyWithoutConstant<F> {
47    fn deref_mut(&mut self) -> &mut Self::Target {
48        &mut self.tail_evaluations
49    }
50}
51
52delegate_transcribable!(NatEvaluatedPolyWithoutConstant<F> { tail_evaluations: Vec<F> }
53    where F: ConstTranscribable);
54
55#[repr(transparent)]
56#[derive(Clone, Debug, PartialEq, Eq)]
57pub struct ProverMsg<F>(pub NatEvaluatedPolyWithoutConstant<F>);
58
59delegate_transcribable!(ProverMsg<F>(NatEvaluatedPolyWithoutConstant<F>)
60    where F: ConstTranscribable);
61
62impl<F> ProverMsg<F> {
63    /// Maps every field element through `f`, preserving structure — used to
64    /// lift elements into wire integers and to project wire integers back
65    /// into elements at the (de)serialization boundary.
66    pub fn try_map<T, E>(
67        &self,
68        f: impl FnMut(&F) -> Result<T, E> + Copy,
69    ) -> Result<ProverMsg<T>, E> {
70        Ok(ProverMsg(self.0.try_map(f)?))
71    }
72}
73
74/// Sumcheck Prover State, generic over the field config `C`.
75pub struct ProverState<C: SetConfig> {
76    /// Sampled randomness given by the verifier.
77    pub randomness: Vec<C::Element>,
78    /// Stores the list of multilinear extensions
79    /// the sumcheck polynomial is comprised of.
80    pub mles: Vec<DenseMultilinearExtension<C::Element>>,
81    /// Number of variables.
82    pub num_vars: usize,
83    /// Max degree.
84    pub max_degree: usize,
85    /// The current round number.
86    pub round: usize,
87    /// Claimed sum for the first round polynomial.
88    pub asserted_sum: Option<C::Element>,
89    /// When `true`, the next [`Self::prove_round`] invocation pushes the
90    /// verifier challenge into `randomness` but skips the
91    /// `fix_variables` fold of `mles`. Used by round-1 fast
92    /// paths (see that pre-fold the MLEs as part of their setup.
93    /// The flag is reset to `false` after the skipped fold.
94    pub skip_next_fold: bool,
95}
96
97impl<C: SetConfig> ProverState<C> {
98    /// Initialize the prover to argue for the sum of products of
99    /// MLE's in {0,1}^`num_vars`.
100    pub fn new(
101        mles: Vec<DenseMultilinearExtension<C::Element>>,
102        nvars: usize,
103        degree: usize,
104    ) -> Self {
105        Self {
106            randomness: Vec::with_capacity(nvars),
107            mles,
108            num_vars: nvars,
109            max_degree: degree,
110            round: 0,
111            asserted_sum: None,
112            skip_next_fold: false,
113        }
114    }
115}
116
117impl<C: SemiringConfig> ProverState<C> {
118    /// Receive message from verifier, generate prover message, and proceed to
119    /// next round.
120    ///
121    /// Adapted Jolt's sumcheck implementation, with some additions like round 1
122    /// fast path.
123    #[allow(clippy::arithmetic_side_effects)]
124    pub fn prove_round(
125        &mut self,
126        v_msg: &Option<C::Element>,
127        comb_fn: impl Fn(&[C::Element]) -> C::Element + Send + Sync,
128        config: &C,
129    ) -> ProverMsg<C::Element> {
130        if let Some(msg) = v_msg {
131            if self.round == 0 {
132                panic!("first round should be prover first.");
133            }
134            self.randomness.push(msg.clone());
135
136            if self.skip_next_fold {
137                // A fast path already produced the post-fold MLEs
138                self.skip_next_fold = false;
139            } else {
140                // fix the next variable at the verifier randomness for this round
141                let i = self.round;
142                let r = self.randomness[i - 1].clone();
143
144                cfg_iter_mut!(self.mles).for_each(|multiplicand| {
145                    multiplicand.fix_variables(config, slice::from_ref(&r));
146                });
147            }
148        } else if self.round > 0 {
149            panic!("verifier message is empty");
150        }
151
152        self.round += 1;
153
154        if self.round > self.num_vars {
155            panic!("Prover is not active");
156        }
157
158        let i = self.round;
159        let nv = self.num_vars;
160        let degree = self.max_degree;
161
162        let polys = &self.mles;
163
164        struct Scratch<R> {
165            evals: Vec<R>,
166            steps: Vec<R>,
167            vals0: Vec<R>,
168            vals1: Vec<R>,
169            vals: Vec<R>,
170            levals: Vec<R>,
171        }
172        let zero = config.zero();
173        let zero_vec_deg = vec![zero.clone(); degree + 1];
174        let zero_vec_poly = vec![zero.clone(); polys.len()];
175        let scratch = || Scratch {
176            evals: zero_vec_deg.clone(),
177            steps: zero_vec_poly.clone(),
178            vals0: zero_vec_poly.clone(),
179            vals1: zero_vec_poly.clone(),
180            vals: zero_vec_poly.clone(),
181            levals: zero_vec_deg.clone(),
182        };
183
184        #[cfg(not(feature = "parallel"))]
185        let zeros = scratch();
186        #[cfg(feature = "parallel")]
187        let zeros = scratch;
188
189        let summer = cfg_into_iter!(0..1 << (nv - i)).fold(zeros, |mut s, b| {
190            let index = b << 1;
191
192            // TODO(Alex): Once you have benches set,
193            //             could please try getting rid of vals0 and vals1 fields in the
194            // structs, replacing them with
195            //
196            //             ```rust
197            //             let vals0: Vec<_> = polys.iter().map(|poly|
198            // poly[index].clone()).collect();             let vals1: Vec<_> =
199            // polys.iter().map(|poly| poly[index + 1].clone()).collect();
200            //             ```
201            //             My bet is that it won't affect running time, but better safe than
202            // sorry.
203
204            s.vals0
205                .iter_mut()
206                .zip(polys.iter())
207                .for_each(|(v0, poly)| *v0 = poly[index].clone());
208            s.levals[0] = comb_fn(&s.vals0);
209
210            if degree > 0 {
211                s.vals1
212                    .iter_mut()
213                    .zip(polys.iter())
214                    .for_each(|(v1, poly)| *v1 = poly[index + 1].clone());
215                s.levals[1] = comb_fn(&s.vals1);
216
217                for (i, (v1, v0)) in s.vals1.iter().zip(s.vals0.iter()).enumerate() {
218                    s.steps[i] = config.sub(v1, v0);
219                    s.vals[i] = v1.clone();
220                }
221
222                for eval_point in s.levals.iter_mut().take(degree + 1).skip(2) {
223                    for poly_i in 0..polys.len() {
224                        config.add_assign(&mut s.vals[poly_i], &s.steps[poly_i]);
225                    }
226                    *eval_point = comb_fn(&s.vals);
227                }
228            }
229
230            // TODO(Alex): It seems that the only thing
231            //             we pass around meaningfully is evals,
232            //             so this loop could be reworked to map/reduce - maybe even without
233            //             #[cfg(feature = "parallel")]. Would help to get benchmarks up and
234            //             running first though.
235            s.evals
236                .iter_mut()
237                .zip(s.levals.iter())
238                .for_each(|(e, l)| config.add_assign(e, l));
239
240            s
241        });
242
243        // Rayon's fold outputs an iter which still needs to be summed over
244        #[cfg(feature = "parallel")]
245        let evaluations = summer.map(|s| s.evals).reduce(
246            || vec![zero.clone(); degree + 1],
247            |mut evaluations, evals| {
248                evaluations
249                    .iter_mut()
250                    .zip(evals)
251                    .for_each(|(e, l)| config.add_assign(e, &l));
252                evaluations
253            },
254        );
255
256        #[cfg(not(feature = "parallel"))]
257        let evaluations = summer.evals;
258
259        // Record the claimed sum once during the first round.
260        if self.round == 1 {
261            let p0 = evaluations
262                .first()
263                .expect("evaluations should always contain the constant term");
264            let sum = if degree > 0 {
265                let eval = evaluations
266                    .get(1)
267                    .expect("degree > 0 implies evaluation at 1 is present");
268                config.add(p0, eval)
269            } else {
270                p0.clone()
271            };
272            self.asserted_sum = Some(sum);
273        }
274
275        // Strip the constant term before sending, without re-allocating all elements.
276        let mut tail = evaluations;
277        tail.remove(0); // leaves P(0) behind; tail holds P(1..)
278
279        ProverMsg(NatEvaluatedPolyWithoutConstant::new(tail))
280    }
281}