Skip to main content

zinc_piop/
sumcheck.rs

1pub mod multi_degree;
2pub mod prover;
3// pub mod utils;
4pub mod verifier;
5
6#[cfg(test)]
7mod tests;
8
9use self::verifier::Subclaim;
10use crate::sumcheck::{
11    prover::{NatEvaluatedPolyWithoutConstant, ProverMsg},
12    verifier::VerifierState,
13};
14use crypto_primitives::{BaseFieldConfig, ProjectPrimitiveIntegersWithConfig};
15use prover::ProverState;
16use std::marker::PhantomData;
17use thiserror::Error;
18use zinc_poly::{EvaluationError, mle::DenseMultilinearExtension, utils::ArithErrors};
19use zinc_transcript::traits::{ConstTranscribable, GenTranscribable, Transcribable, Transcript};
20use zinc_utils::mul;
21
22/// Sumcheck for products of multilinear polynomial, generic over the field
23/// config `C`.
24pub struct MLSumcheck<C>(PhantomData<C>);
25
26/// Proof generated by the sumcheck prover.
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub struct SumcheckProof<F> {
29    /// List of prover messages, one for each round.
30    pub messages: Vec<ProverMsg<F>>,
31    /// The claimed sum for the first round polynomial.
32    pub claimed_sum: F,
33}
34
35/// The proof is transcribed as raw field elements without field metadata:
36/// the field config is bound into the transcript separately, at sampling
37/// time / at the top level of the surrounding proof.
38impl<F: ConstTranscribable> GenTranscribable for SumcheckProof<F> {
39    fn read_transcription_bytes_exact(bytes: &[u8]) -> Self {
40        let (n_msgs, mut bytes) = u32::read_transcription_bytes_subset(bytes);
41        let n_msgs = usize::try_from(n_msgs).expect("message count must fit into usize");
42
43        let mut messages = Vec::with_capacity(n_msgs);
44        for _ in 0..n_msgs {
45            let (len, rest) = u32::read_transcription_bytes_subset(bytes);
46            let len = usize::try_from(len).expect("polynomial length must fit into usize");
47            bytes = rest;
48            let end = mul!(len, F::NUM_BYTES);
49            let tail_evaluations: Vec<F> = Vec::read_transcription_bytes_exact(&bytes[..end]);
50            messages.push(ProverMsg(NatEvaluatedPolyWithoutConstant {
51                tail_evaluations,
52            }));
53            bytes = &bytes[end..];
54        }
55
56        let claimed_sum = F::read_transcription_bytes_exact(bytes);
57        Self {
58            messages,
59            claimed_sum,
60        }
61    }
62
63    fn write_transcription_bytes_exact(&self, mut buf: &mut [u8]) {
64        buf = {
65            let len =
66                u32::try_from(self.messages.len()).expect("messages length must fit into u32");
67            len.write_transcription_bytes_exact(&mut buf[0..u32::NUM_BYTES]);
68            &mut buf[u32::NUM_BYTES..]
69        };
70        for msg in &self.messages {
71            let evals = &msg.0.tail_evaluations;
72            buf = {
73                let len = u32::try_from(evals.len()).expect("messages length must fit into u32");
74                len.write_transcription_bytes_exact(&mut buf[0..u32::NUM_BYTES]);
75                &mut buf[u32::NUM_BYTES..]
76            };
77            let end = mul!(evals.len(), F::NUM_BYTES);
78            evals.write_transcription_bytes_exact(&mut buf[..end]);
79            buf = &mut buf[end..];
80        }
81        self.claimed_sum.write_transcription_bytes_exact(buf);
82    }
83}
84
85impl<F: ConstTranscribable> Transcribable for SumcheckProof<F> {
86    #[allow(clippy::arithmetic_side_effects)]
87    fn get_num_bytes(&self) -> usize {
88        let n_msgs = self.messages.len();
89        let total_evals: usize = self
90            .messages
91            .iter()
92            .map(|m| m.0.tail_evaluations.len())
93            .sum();
94        u32::NUM_BYTES // n_msgs
95            + n_msgs * u32::NUM_BYTES // n_evals for each message
96            + total_evals * F::NUM_BYTES // evals
97            + F::NUM_BYTES // claimed_sum
98    }
99}
100
101impl<C> MLSumcheck<C>
102where
103    C: BaseFieldConfig + ProjectPrimitiveIntegersWithConfig,
104    C::Integer: ConstTranscribable,
105{
106    /// Sumcheck prover main entry point.
107    ///
108    /// This function executes the Prover side of the Sumcheck protocol.
109    /// It verifies a claim of the form:
110    ///
111    /// $$
112    /// \sum_{x \in \{0, 1\}^{\text{nvars}}} \text{comb\\_fn}(\text{mles}(x)) =
113    /// \text{claimed\\_sum}. $$
114    ///
115    /// It is designed to be used as a subprotocol within a larger system
116    /// since it takes the FS transcript (`transcript` argument) as input
117    /// and returns the **internal ProverState** alongside the final proof.
118    ///
119    /// The claimed sum is derived by the prover.
120    ///
121    /// ---
122    ///
123    /// # Arguments
124    ///
125    /// * `transcript`: A mutable reference to a Fiat-Shamir `Transcript`.
126    /// * `mles`: A `Vec` of dense multilinear extension over the base field
127    ///   `F`. The sumcheck polynomial is made over the combined result of these
128    ///   multilinear extensions.
129    /// * `nvars`: The number of variables over which the `mles` are defined.
130    ///   This must be consistent across all `mles`.
131    /// * `degree`: The maximum combined degree of the `mles` under the
132    ///   `comb_fn`.
133    /// * `comb_fn`: A closure that defines the combination function
134    ///   $G(\text{mles}(x))$. It takes a slice of field elements (the
135    ///   evaluations of the `mles` at a point $x$) and returns a single field
136    ///   element.
137    /// * `config`: The configuration for the underlying field used in the
138    ///   protocol.
139    ///
140    /// ---
141    ///
142    /// # Returns
143    ///
144    /// A tuple containing:
145    ///
146    /// 1. `SumcheckProof<F>`: The final sumcheck proof.
147    /// 2. `ProverState<F>`: The state of the Prover after the protocol
148    ///    completes.
149    ///
150    /// ---
151    ///
152    /// # Panics
153    ///
154    /// * Panics if the number of variables is `0`.
155    pub fn prove_as_subprotocol(
156        transcript: &mut impl Transcript,
157        mles: Vec<DenseMultilinearExtension<C::Element>>,
158        nvars: usize,
159        degree: usize,
160        comb_fn: impl Fn(&[C::Element]) -> C::Element + Send + Sync,
161        config: &C,
162    ) -> (SumcheckProof<C::Element>, ProverState<C>) {
163        if nvars == 0 {
164            panic!("Attempt to prove a constant")
165        }
166
167        let mut buf = vec![0; <C::Integer as ConstTranscribable>::NUM_BYTES];
168        let nvars_field = config.project(&(nvars as u64));
169        let degree_field = config.project(&(degree as u64));
170
171        transcript.absorb_field_element(config, &nvars_field, &mut buf);
172        transcript.absorb_field_element(config, &degree_field, &mut buf);
173
174        let mut prover_state = ProverState::new(mles, nvars, degree);
175        let mut verifier_msg = None;
176        let mut prover_msgs = Vec::with_capacity(nvars);
177
178        for _ in 0..nvars {
179            let prover_msg = prover_state.prove_round(&verifier_msg, &comb_fn, config);
180            transcript.absorb_field_element_slice(config, &prover_msg.0.tail_evaluations, &mut buf);
181            prover_msgs.push(prover_msg);
182            let next_verifier_msg = transcript.get_field_challenge(config);
183            transcript.absorb_field_element(config, &next_verifier_msg, &mut buf);
184
185            verifier_msg = Some(next_verifier_msg);
186        }
187        let asserted_sum = prover_state
188            .asserted_sum
189            .clone()
190            .expect("asserted sum should be recorded after the first prover round");
191        if let Some(vmsg) = verifier_msg {
192            prover_state.randomness.push(vmsg);
193        }
194
195        (
196            SumcheckProof {
197                messages: prover_msgs,
198                claimed_sum: asserted_sum,
199            },
200            prover_state,
201        )
202    }
203
204    /// Sumcheck verifier main entry point.
205    ///
206    /// This function executes the Verifier side of the Sumcheck protocol.
207    /// It takes a `proof` and a `claimed_sum` and verifies the
208    /// intermediate steps of the sumcheck.
209    ///
210    /// The sumcheck verifies the claim:
211    ///
212    /// $$
213    /// \sum_{x \in \{0, 1\}^{\text{num\\_vars}}} G(x) = \text{claimed\\_sum}.
214    /// $$
215    ///
216    /// It is designed to be used as a subprotocol within a larger system.
217    /// If successful, it returns a **Subclaim**, a final equation that the
218    /// outer protocol must satisfy for the overall proof to be valid.
219    ///
220    /// ---
221    ///
222    /// # Arguments
223    ///
224    /// * `transcript`: A mutable reference to a Fiat-Shamir `Transcript`.
225    /// * `num_vars`: The number of variables over which the sum was originally
226    ///   computed.
227    /// * `degree`: The maximum combined degree of the underlying polynomial
228    ///   $G(x)$. This must match the degree used by the Prover.
229    /// * `proof`: A reference to the `SumcheckProof<F>` generated by the
230    ///   Prover.
231    /// * `config`: The configuration for the underlying field used in the
232    ///   protocol.
233    ///
234    /// ---
235    ///
236    /// # Returns
237    ///
238    /// A `Result` which is:
239    ///
240    /// * `Ok(Subclaim<F>)`: If the Sumcheck protocol passes successfully, it
241    ///   returns a `Subclaim`. This claim consists of:
242    ///     1. The final random challenge point $r \in
243    ///        \text{F}^{\text{num\\_vars}}$.
244    ///     2. The expected evaluation $v$ of the combined polynomial $G(r)$ at
245    ///        that point.
246    ///
247    /// * `Err(SumCheckError<F>)`: If any of the round checks fail during the
248    ///   protocol.
249    ///
250    /// ---
251    ///
252    /// # Panics
253    ///
254    /// * Panics if the number of variables is `0`.
255    pub fn verify_as_subprotocol(
256        transcript: &mut impl Transcript,
257        num_vars: usize,
258        degree: usize,
259        proof: &SumcheckProof<C::Element>,
260        config: &C,
261    ) -> Result<Subclaim<C::Element>, SumCheckError<C::Element>> {
262        if num_vars == 0 {
263            panic!("Attempt to verify a sumcheck claim for 0 variables")
264        }
265
266        let mut buf = vec![0; <C::Integer as ConstTranscribable>::NUM_BYTES];
267
268        let (nvars_field, degree_field) = {
269            (
270                config.project(&(num_vars as u64)),
271                config.project(&(degree as u64)),
272            )
273        };
274        transcript.absorb_field_element(config, &nvars_field, &mut buf);
275        transcript.absorb_field_element(config, &degree_field, &mut buf);
276
277        if proof.messages.len() != num_vars {
278            return Err(SumCheckError::InvalidProofLength {
279                expected: num_vars,
280                got: proof.messages.len(),
281            });
282        }
283
284        let mut verifier_state = VerifierState::new(num_vars, degree, config);
285
286        for i in 0..num_vars {
287            let prover_msg = &proof.messages[i];
288            transcript.absorb_field_element_slice(config, &prover_msg.0.tail_evaluations, &mut buf);
289            let verifier_msg = verifier_state.verify_round(prover_msg, transcript);
290            transcript.absorb_field_element(config, &verifier_msg, &mut buf);
291        }
292
293        verifier_state.check_and_generate_subclaim(proof.claimed_sum.clone())
294    }
295}
296
297#[derive(Error, Debug)]
298pub enum SumCheckError<F> {
299    #[error("univariate polynomial evaluation error")]
300    EvaluationError(ArithErrors),
301    #[error("incorrect sumcheck sum at round {0}. Expected `{1}`. Received `{2}`")]
302    SumCheckFailed(usize, Box<F>, Box<F>),
303    #[error("max degree exceeded")]
304    MaxDegreeExceeded,
305    #[error("invalid proof length: expected {expected}, got {got}")]
306    InvalidProofLength { expected: usize, got: usize },
307    #[error("verifier failed to evaluate a round polynomial: {0}")]
308    UnivariateEvaluationError(EvaluationError),
309}
310
311impl<F> From<ArithErrors> for SumCheckError<F> {
312    fn from(arith_error: ArithErrors) -> Self {
313        Self::EvaluationError(arith_error)
314    }
315}
316
317impl<F> From<EvaluationError> for SumCheckError<F> {
318    fn from(error: EvaluationError) -> Self {
319        Self::UnivariateEvaluationError(error)
320    }
321}