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}