Skip to main content

p3_sumcheck/
data.rs

1use alloc::vec::Vec;
2
3use p3_challenger::fs::TranscriptField;
4use p3_challenger::{CanObserve, CanSample, FieldChallenger, GrindingChallenger};
5use p3_field::ExtensionField;
6use p3_multilinear_util::point::Point;
7use serde::{Deserialize, Serialize};
8
9use crate::SumcheckError;
10use crate::strategy::Basis;
11use crate::transcript::{ProverTranscript, SumcheckShape, VerifierTranscript};
12
13/// Sumcheck polynomial data
14///
15/// Stores the polynomial evaluations for sumcheck rounds in a compact format.
16/// Each round stores two evaluations: `[h(0), h(inf)]` in the evaluation
17/// basis (`h(1)` derived as `claimed_sum - h(0)`), or `[s(1), s(inf)]` in the
18/// projective basis (`s(0)` derived as `claimed_sum - s(inf)`).
19#[derive(Default, Serialize, Deserialize, Clone, Debug)]
20pub struct SumcheckData<F, EF> {
21    /// Polynomial evaluations for each sumcheck round.
22    ///
23    /// Each entry is `[h(0), h(inf)]`:
24    /// - `h(0)` is the constant term.
25    /// - `h(inf)` is the leading coefficient (evaluation at infinity).
26    ///
27    /// `h(1)` is derived as `claimed_sum - h(0)` by the verifier.
28    ///
29    /// Length: folding_factor
30    pub polynomial_evaluations: Vec<[EF; 2]>,
31
32    /// PoW witnesses for each sumcheck round
33    /// Length: folding_factor
34    pub pow_witnesses: Vec<F>,
35}
36
37impl<F, EF> SumcheckData<F, EF> {
38    /// Returns the polynomial evaluations `[h(0), h(inf)]` for each round.
39    #[must_use]
40    pub fn polynomial_evaluations(&self) -> &[[EF; 2]] {
41        &self.polynomial_evaluations
42    }
43
44    /// Returns the number of rounds stored in this proof data.
45    #[must_use]
46    pub const fn num_rounds(&self) -> usize {
47        self.polynomial_evaluations.len()
48    }
49
50    /// Records one round in this proof and plays the matching transcript step.
51    ///
52    /// This is the only place a round reaches both the proof and the sponge.
53    /// Recording and absorbing therefore cannot drift apart.
54    ///
55    /// # Arguments
56    ///
57    /// * `transcript` - driver of the batch of rounds this one belongs to.
58    /// * `c_a` - finite-point value: `h(0)` (evaluation) or `s(1)` (projective).
59    /// * `c_inf` - leading coefficient `h(inf)` / `s(inf)`.
60    ///
61    /// # Returns
62    ///
63    /// The sampled challenge `r`.
64    ///
65    /// The two values are recorded and absorbed verbatim.
66    /// Which basis defines them is fixed by the shape the driver was built from.
67    ///
68    /// # Panics
69    ///
70    /// When the batch has already played every round it was described with.
71    pub fn observe_and_sample<Challenger>(
72        &mut self,
73        transcript: &mut ProverTranscript<'_, Challenger, F, EF>,
74        c_a: EF,
75        c_inf: EF,
76    ) -> EF
77    where
78        F: TranscriptField,
79        EF: ExtensionField<F>,
80        Challenger: CanObserve<F> + CanSample<F> + GrindingChallenger<Witness = F>,
81    {
82        // Absorb the pair, do the optional grinding, and take this round's challenge.
83        let (challenge, witness) = transcript.round(c_a, c_inf);
84
85        // Record what the round produced alongside what it bound.
86        //
87        // Two of the quadratic's three values cross the wire; the verifier
88        // derives the third from the basis-dependent round identity.
89        self.polynomial_evaluations.push([c_a, c_inf]);
90        self.pow_witnesses.extend(witness);
91
92        challenge
93    }
94
95    /// Verifies standard sumcheck rounds and extracts folding randomness from the transcript.
96    ///
97    /// # Arguments
98    ///
99    /// * `challenger` - sponge of the surrounding protocol, borrowed for the batch.
100    /// * `claimed_sum` - Running claim, folded in place to `h(r)` after each round.
101    /// * `expected_rounds` - Protocol-fixed number of rounds this proof must carry.
102    /// * `pow_bits` - PoW difficulty (0 to skip grinding).
103    /// * `basis` - how the two transmitted values are read.
104    ///
105    /// # Returns
106    ///
107    /// The folding randomness, one challenge per round.
108    ///
109    /// # Shape checks
110    ///
111    /// Both counts this proof carries are attacker-controlled.
112    /// Both are checked before any transcript work.
113    ///
114    /// The round count decides how many steps the batch is described with.
115    /// The witness count is what the round loop indexes into.
116    ///
117    /// Taking either from the proof would let a wrong one desynchronise Fiat-Shamir.
118    /// Both are therefore compared against the caller's own configuration instead.
119    ///
120    /// # Errors
121    ///
122    /// - The proof does not carry exactly `expected_rounds` rounds.
123    /// - The witness count is not the one the difficulty implies.
124    /// - A round carries a witness that misses the required difficulty.
125    pub fn verify_rounds<Challenger>(
126        &self,
127        challenger: &mut Challenger,
128        claimed_sum: &mut EF,
129        expected_rounds: usize,
130        pow_bits: usize,
131        basis: Basis,
132    ) -> Result<Point<EF>, SumcheckError>
133    where
134        F: TranscriptField,
135        EF: ExtensionField<F>,
136        Challenger: FieldChallenger<F> + GrindingChallenger<Witness = F>,
137    {
138        // Bind the round count to the protocol-fixed value before folding anything.
139        //
140        // Centralizing the check here makes the safe path the only path:
141        // every caller is forced to declare how many rounds it expects.
142        if self.polynomial_evaluations.len() != expected_rounds {
143            return Err(SumcheckError::RoundCountMismatch {
144                expected: expected_rounds,
145                actual: self.polynomial_evaluations.len(),
146            });
147        }
148
149        // Canonical proof shape — every accepting proof has a unique form:
150        // - zero difficulty requires an empty witness vector,
151        // - positive difficulty requires exactly one witness per round.
152        //
153        // The loop below indexes the witness vector, so this also keeps it in bounds.
154        let expected_witnesses = if pow_bits > 0 { expected_rounds } else { 0 };
155        if self.pow_witnesses.len() != expected_witnesses {
156            return Err(SumcheckError::PowWitnessCountMismatch {
157                expected: expected_witnesses,
158                actual: self.pow_witnesses.len(),
159            });
160        }
161
162        // Seeded from the same numbers the prover seeded with.
163        let shape = SumcheckShape::new(expected_rounds, pow_bits, basis);
164        let mut transcript = VerifierTranscript::<Challenger, F, EF>::new(challenger, shape);
165
166        let mut randomness = Vec::with_capacity(expected_rounds);
167
168        // Driven by the same number the description was built from, not by the proof's length.
169        //
170        // The two agree only because of the round-count check above.
171        // Reading the count once keeps the loop and the description from ever disagreeing:
172        //
173        //     too few iterations  -> steps left unplayed, and closing the transcript panics
174        //     too many            -> a step past the end of the description, which panics
175        //
176        // Both indices below are in bounds by the two checks above.
177        for round in 0..expected_rounds {
178            let [c_a, c_inf] = self.polynomial_evaluations[round];
179
180            // One call binds both values, re-checks the grind, and draws the challenge.
181            //
182            // A rejection here releases the driver's completeness check on its way out.
183            let witness = (pow_bits > 0).then(|| self.pow_witnesses[round]);
184            let r = transcript.round(c_a, c_inf, witness)?;
185
186            // Reconstruct h(r); the basis-dependent round identity supplies the
187            // third quadratic value (shared with the prover via `Basis::reduce_claim`).
188            *claimed_sum = basis.reduce_claim(c_a, c_inf, r, *claimed_sum);
189            randomness.push(r);
190        }
191
192        // Require that every described step was played.
193        transcript.finish();
194
195        Ok(Point::new(randomness))
196    }
197}
198
199/// Verify the final sumcheck rounds.
200///
201/// This is a free function because a run of no rounds may carry no sumcheck data at all.
202///
203/// # Returns
204///
205/// The folding randomness, one challenge per round.
206///
207/// # Errors
208///
209/// - The run is described with rounds but carries no sumcheck data.
210/// - Any rejection the round replay itself raises.
211pub fn verify_final_sumcheck_rounds<F, EF, Challenger>(
212    final_sumcheck: Option<&SumcheckData<F, EF>>,
213    challenger: &mut Challenger,
214    claimed_sum: &mut EF,
215    rounds: usize,
216    pow_bits: usize,
217    basis: Basis,
218) -> Result<Point<EF>, SumcheckError>
219where
220    F: TranscriptField,
221    EF: ExtensionField<F>,
222    Challenger: FieldChallenger<F> + GrindingChallenger<Witness = F>,
223{
224    if rounds == 0 {
225        return Ok(Point::new(Vec::new()));
226    }
227
228    let sumcheck = final_sumcheck.ok_or(SumcheckError::MissingSumcheckData {
229        expected_rounds: rounds,
230    })?;
231
232    // `verify_rounds` binds the round count to `rounds`.
233    sumcheck.verify_rounds(challenger, claimed_sum, rounds, pow_bits, basis)
234}