openvm_stark_backend/verifier/
fractional_sumcheck_gkr.rs1use p3_field::PrimeCharacteristicRing;
2use thiserror::Error;
3use tracing::debug;
4
5use crate::{
6 poly_common::{eval_eq_mle, interpolate_cubic_at_0123, interpolate_linear_at_01},
7 proof::{GkrLayerClaims, GkrProof},
8 FiatShamirTranscript, StarkProtocolConfig,
9};
10
11#[derive(Debug, Error, PartialEq, Eq)]
12pub enum GkrVerificationError<EF: core::fmt::Debug + core::fmt::Display + PartialEq + Eq> {
13 #[error("Zero-round proof: q0_claim should be 1, got {actual}")]
14 InvalidZeroRoundValue { actual: EF },
15 #[error("Zero-check failed: numerator at root should be zero, got {actual}")]
16 ZeroCheckFailed { actual: EF },
17 #[error("Denominator consistency check failed at root: expected {expected}, got {actual}")]
18 RootConsistencyCheckFailed { expected: EF, actual: EF },
19 #[error("Layer consistency check failed at round {round}: expected {expected}, got {actual}")]
20 LayerConsistencyCheckFailed {
21 round: usize,
22 expected: EF,
23 actual: EF,
24 },
25 #[error("Expected {expected} layers, got {actual}")]
26 IncorrectLayerCount { expected: usize, actual: usize },
27 #[error("Expected {expected} sumcheck polynomial entries, got {actual}")]
28 IncorrectSumcheckPolyCount { expected: usize, actual: usize },
29 #[error("Round {round} expected {expected} sumcheck sub-rounds, got {actual}")]
30 IncorrectSubroundCount {
31 round: usize,
32 expected: usize,
33 actual: usize,
34 },
35}
36
37#[allow(clippy::type_complexity)]
47pub fn verify_gkr<SC: StarkProtocolConfig, TS: FiatShamirTranscript<SC>>(
48 proof: &GkrProof<SC>,
49 transcript: &mut TS,
50 total_rounds: usize,
51) -> Result<(SC::EF, SC::EF, Vec<SC::EF>), GkrVerificationError<SC::EF>> {
52 assert!(total_rounds > 0);
53
54 if proof.claims_per_layer.len() != total_rounds {
56 return Err(GkrVerificationError::IncorrectLayerCount {
57 expected: total_rounds,
58 actual: proof.claims_per_layer.len(),
59 });
60 }
61
62 let expected_sumcheck_entries = total_rounds.saturating_sub(1);
65 if proof.sumcheck_polys.len() != expected_sumcheck_entries {
66 return Err(GkrVerificationError::IncorrectSumcheckPolyCount {
67 expected: expected_sumcheck_entries,
68 actual: proof.sumcheck_polys.len(),
69 });
70 }
71
72 transcript.observe_ext(proof.q0_claim);
73
74 let layer_claims = &proof.claims_per_layer[0];
76 observe_layer_claims::<SC, TS>(transcript, layer_claims);
77
78 let (p_cross_term, q_cross_term) = compute_recursive_relations::<SC>(layer_claims);
80
81 if p_cross_term != SC::EF::ZERO {
83 return Err(GkrVerificationError::ZeroCheckFailed {
84 actual: p_cross_term,
85 });
86 }
87
88 if q_cross_term != proof.q0_claim {
90 return Err(GkrVerificationError::RootConsistencyCheckFailed {
91 expected: proof.q0_claim,
92 actual: q_cross_term,
93 });
94 }
95
96 let mu = transcript.sample_ext();
98 debug!(gkr_round = 0, %mu);
99 let (mut numer_claim, mut denom_claim) = reduce_to_single_evaluation::<SC>(layer_claims, mu);
100 debug!(%numer_claim, %denom_claim);
101 let mut gkr_r = vec![mu];
102
103 for round in 1..total_rounds {
105 let lambda = transcript.sample_ext();
107 debug!(gkr_round = round, %lambda);
108 let claim = numer_claim + lambda * denom_claim;
109
110 let (new_claim, round_r, eq_at_r_prime) =
113 verify_gkr_sumcheck::<SC, TS>(proof, transcript, round, claim, &gkr_r)?;
114 debug_assert_eq!(eq_at_r_prime, eval_eq_mle(&gkr_r, &round_r));
115
116 let layer_claims = &proof.claims_per_layer[round];
118 observe_layer_claims::<SC, TS>(transcript, layer_claims);
119
120 let (p_cross_term, q_cross_term) = compute_recursive_relations::<SC>(layer_claims);
122
123 let expected_claim = (p_cross_term + lambda * q_cross_term) * eq_at_r_prime;
125 if expected_claim != new_claim {
126 return Err(GkrVerificationError::LayerConsistencyCheckFailed {
127 round,
128 expected: expected_claim,
129 actual: new_claim,
130 });
131 }
132
133 let mu = transcript.sample_ext();
135 debug!(gkr_round = round, %mu);
136 (numer_claim, denom_claim) = reduce_to_single_evaluation::<SC>(layer_claims, mu);
137 gkr_r = std::iter::once(mu).chain(round_r).collect();
139 }
140
141 Ok((numer_claim, denom_claim, gkr_r))
142}
143
144#[allow(clippy::type_complexity)]
152fn verify_gkr_sumcheck<SC: StarkProtocolConfig, TS: FiatShamirTranscript<SC>>(
153 proof: &GkrProof<SC>,
154 transcript: &mut TS,
155 round: usize,
156 mut claim: SC::EF,
157 gkr_r: &[SC::EF],
158) -> Result<(SC::EF, Vec<SC::EF>, SC::EF), GkrVerificationError<SC::EF>> {
159 debug_assert!(
160 round > 0,
161 "verify_gkr_sumcheck should not be called for round 0"
162 );
163 debug_assert_eq!(
164 gkr_r.len(),
165 round,
166 "gkr_r should have exactly round elements"
167 );
168
169 let expected_subrounds = round;
171 let polys = &proof.sumcheck_polys[round - 1];
172 if polys.len() != expected_subrounds {
174 return Err(GkrVerificationError::IncorrectSubroundCount {
175 round,
176 expected: expected_subrounds,
177 actual: polys.len(),
178 });
179 }
180 let mut gkr_r_prime = Vec::with_capacity(round);
181 let mut eq = SC::EF::ONE; for (sumcheck_round, poly_evals) in polys.iter().enumerate() {
184 debug!(gkr_round = round, %sumcheck_round, sum_claim = %claim);
185 for &eval in poly_evals {
187 transcript.observe_ext(eval);
188 }
189
190 let ri = transcript.sample_ext();
191 gkr_r_prime.push(ri);
192 debug!(gkr_round = round, %sumcheck_round, r_round = %ri);
193
194 let ev0 = claim - poly_evals[0]; let evals = [ev0, poly_evals[0], poly_evals[1], poly_evals[2]];
196 claim = interpolate_cubic_at_0123(&evals, ri);
197
198 let xi = gkr_r[sumcheck_round];
200 eq *= xi * ri + (SC::EF::ONE - xi) * (SC::EF::ONE - ri);
201 }
202
203 Ok((claim, gkr_r_prime, eq))
204}
205
206fn observe_layer_claims<SC: StarkProtocolConfig, TS: FiatShamirTranscript<SC>>(
208 transcript: &mut TS,
209 claims: &GkrLayerClaims<SC>,
210) {
211 transcript.observe_ext(claims.p_xi_0);
212 transcript.observe_ext(claims.q_xi_0);
213 transcript.observe_ext(claims.p_xi_1);
214 transcript.observe_ext(claims.q_xi_1);
215}
216
217fn compute_recursive_relations<SC: StarkProtocolConfig>(
219 claims: &GkrLayerClaims<SC>,
220) -> (SC::EF, SC::EF) {
221 let p_cross_term = claims.p_xi_0 * claims.q_xi_1 + claims.p_xi_1 * claims.q_xi_0;
222 let q_cross_term = claims.q_xi_0 * claims.q_xi_1;
223 (p_cross_term, q_cross_term)
224}
225
226fn reduce_to_single_evaluation<SC: StarkProtocolConfig>(
228 claims: &GkrLayerClaims<SC>,
229 mu: SC::EF,
230) -> (SC::EF, SC::EF) {
231 let numer = interpolate_linear_at_01(&[claims.p_xi_0, claims.p_xi_1], mu);
232 let denom = interpolate_linear_at_01(&[claims.q_xi_0, claims.q_xi_1], mu);
233 (numer, denom)
234}