Skip to main content

p3_sumcheck/
data.rs

1use alloc::vec::Vec;
2
3use p3_challenger::{FieldChallenger, GrindingChallenger};
4use p3_field::{ExtensionField, Field, TwoAdicField};
5use p3_multilinear_util::point::Point;
6use serde::{Deserialize, Serialize};
7
8use crate::{SumcheckError, extrapolate_01inf};
9
10/// Sumcheck polynomial data
11///
12/// Stores the polynomial evaluations for sumcheck rounds in a compact format.
13/// Each round stores `[h(0), h(inf)]` where `h(1)` is derived as `claimed_sum - h(0)`.
14#[derive(Default, Serialize, Deserialize, Clone, Debug)]
15pub struct SumcheckData<F, EF> {
16    /// Polynomial evaluations for each sumcheck round.
17    ///
18    /// Each entry is `[h(0), h(inf)]`:
19    /// - `h(0)` is the constant term.
20    /// - `h(inf)` is the leading coefficient (evaluation at infinity).
21    ///
22    /// `h(1)` is derived as `claimed_sum - h(0)` by the verifier.
23    ///
24    /// Length: folding_factor
25    pub polynomial_evaluations: Vec<[EF; 2]>,
26
27    /// PoW witnesses for each sumcheck round
28    /// Length: folding_factor
29    pub pow_witnesses: Vec<F>,
30}
31
32impl<F, EF> SumcheckData<F, EF> {
33    /// Returns the polynomial evaluations `[h(0), h(inf)]` for each round.
34    #[must_use]
35    pub fn polynomial_evaluations(&self) -> &[[EF; 2]] {
36        &self.polynomial_evaluations
37    }
38
39    /// Returns the number of rounds stored in this proof data.
40    #[must_use]
41    pub const fn num_rounds(&self) -> usize {
42        self.polynomial_evaluations.len()
43    }
44
45    /// Commits polynomial coefficients to the transcript and returns a challenge.
46    ///
47    /// This helper function handles the Fiat-Shamir interaction for a sumcheck round.
48    ///
49    /// # Arguments
50    ///
51    /// * `challenger` - Fiat-Shamir transcript.
52    /// * `c0` - Constant coefficient `h(0)`.
53    /// * `c_inf` - Leading coefficient `h(inf)`.
54    /// * `pow_bits` - PoW difficulty (0 to skip grinding).
55    ///
56    /// # Returns
57    ///
58    /// The sampled challenge `r`.
59    pub fn observe_and_sample<Challenger, BF>(
60        &mut self,
61        challenger: &mut Challenger,
62        c0: EF,
63        c_inf: EF,
64        pow_bits: usize,
65    ) -> EF
66    where
67        BF: Field,
68        EF: ExtensionField<BF>,
69        F: Clone,
70        Challenger: FieldChallenger<BF> + GrindingChallenger<Witness = F>,
71    {
72        // Record the polynomial coefficients in the proof.
73        self.polynomial_evaluations.push([c0, c_inf]);
74
75        // Absorb coefficients into the transcript.
76        //
77        // We send (h(0), h(inf)). The verifier derives h(1) from the sum constraint.
78        challenger.observe_algebra_slice(&[c0, c_inf]);
79
80        // Optional proof-of-work to increase prover cost.
81        //
82        // This makes it expensive for a malicious prover to "mine" favorable challenges.
83        if pow_bits > 0 {
84            self.pow_witnesses.push(challenger.grind(pow_bits));
85        }
86
87        // Sample the verifier's challenge for this round.
88        challenger.sample_algebra_element()
89    }
90
91    /// Verifies standard sumcheck rounds and extracts folding randomness from the transcript.
92    ///
93    /// # Returns
94    ///
95    /// A `Point` of folding randomness values.
96    pub fn verify_rounds<Challenger>(
97        &self,
98        challenger: &mut Challenger,
99        claimed_sum: &mut EF,
100        pow_bits: usize,
101    ) -> Result<Point<EF>, SumcheckError>
102    where
103        F: TwoAdicField,
104        EF: ExtensionField<F> + TwoAdicField,
105        Challenger: FieldChallenger<F> + GrindingChallenger<Witness = F>,
106    {
107        let mut randomness = Vec::with_capacity(self.polynomial_evaluations.len());
108
109        // Grinding pushes one witness per round;
110        //
111        // Reject upfront if the proof is short so the loop below cannot panic on out-of-bounds indexing.
112        if pow_bits > 0 && self.pow_witnesses.len() != self.polynomial_evaluations.len() {
113            return Err(SumcheckError::PowWitnessCountMismatch {
114                expected: self.polynomial_evaluations.len(),
115                actual: self.pow_witnesses.len(),
116            });
117        }
118
119        for (i, &[c0, c_inf]) in self.polynomial_evaluations.iter().enumerate() {
120            // Observe only the sent polynomial evaluations (h(0) and h(inf)).
121            challenger.observe_algebra_slice(&[c0, c_inf]);
122
123            // Verify PoW (only if pow_bits > 0)
124            if pow_bits > 0 && !challenger.check_witness(pow_bits, self.pow_witnesses[i]) {
125                return Err(SumcheckError::InvalidPowWitness);
126            }
127
128            // Sample challenge and reconstruct h(r) from (h(0), h(1), h(inf)).
129            let r: EF = challenger.sample_algebra_element();
130            *claimed_sum = extrapolate_01inf(c0, *claimed_sum - c0, c_inf, r);
131            randomness.push(r);
132        }
133
134        Ok(Point::new(randomness))
135    }
136}
137
138/// Verify the final sumcheck rounds.
139///
140/// This is a free function because the caller may not have a `SumcheckData` at all when `rounds == 0`.
141///
142/// # Returns
143///
144/// A `Point` of folding randomness values.
145pub fn verify_final_sumcheck_rounds<F, EF, Challenger>(
146    final_sumcheck: Option<&SumcheckData<F, EF>>,
147    challenger: &mut Challenger,
148    claimed_sum: &mut EF,
149    rounds: usize,
150    pow_bits: usize,
151) -> Result<Point<EF>, SumcheckError>
152where
153    F: TwoAdicField,
154    EF: ExtensionField<F> + TwoAdicField,
155    Challenger: FieldChallenger<F> + GrindingChallenger<Witness = F>,
156{
157    if rounds == 0 {
158        return Ok(Point::new(Vec::new()));
159    }
160
161    let sumcheck = final_sumcheck.ok_or(SumcheckError::MissingSumcheckData {
162        expected_rounds: rounds,
163    })?;
164
165    if sumcheck.polynomial_evaluations.len() != rounds {
166        return Err(SumcheckError::RoundCountMismatch {
167            expected: rounds,
168            actual: sumcheck.polynomial_evaluations.len(),
169        });
170    }
171    sumcheck.verify_rounds(challenger, claimed_sum, pow_bits)
172}