Skip to main content

openvm_stark_backend/verifier/
whir.rs

1use core::iter::zip;
2
3use itertools::{izip, Itertools};
4use p3_field::{BasedVectorSpace, ExtensionField, PrimeCharacteristicRing, TwoAdicField};
5use thiserror::Error;
6use tracing::instrument;
7
8use crate::{
9    hasher::MerkleHasher,
10    poly_common::{
11        eval_eq_mle, eval_mle_evals_at_point, eval_mobius_eq_mle, horner_eval,
12        interpolate_quadratic_at_012, Squarable,
13    },
14    proof::WhirProof,
15    FiatShamirTranscript, StarkProtocolConfig,
16};
17
18#[inline]
19fn ensure(cond: bool, err: VerifyWhirError) -> Result<(), VerifyWhirError> {
20    if cond {
21        Ok(())
22    } else {
23        Err(err)
24    }
25}
26
27/// Verify a WHIR proof.
28///
29/// Assumes that all inputs have already been checked to have the correct sizes.
30#[instrument(level = "debug", skip_all)]
31pub fn verify_whir<SC: StarkProtocolConfig, TS: FiatShamirTranscript<SC>>(
32    transcript: &mut TS,
33    config: &SC,
34    whir_proof: &WhirProof<SC>,
35    stacking_openings: &[Vec<SC::EF>],
36    commitments: &[SC::Digest],
37    u: &[SC::EF],
38) -> Result<(), VerifyWhirError> {
39    let params = config.params();
40    // widths.len() = stacking_openings.len() = layouts.len(), which proof shape constructs to equal
41    // the number of commitments, which equals commitments.len() by construction
42    let widths = stacking_openings
43        .iter()
44        .map(|v| v.len())
45        .collect::<Vec<_>>();
46
47    // Check proof-of-work before μ batching challenge
48    if !transcript.check_witness(params.whir.mu_pow_bits, whir_proof.mu_pow_witness) {
49        return Err(VerifyWhirError::MuPoWInvalid);
50    }
51
52    let mu = transcript.sample_ext();
53
54    let WhirProof {
55        mu_pow_witness: _, // Already checked above
56        whir_sumcheck_polys,
57        codeword_commits,
58        ood_values,
59        initial_round_opened_rows,
60        initial_round_merkle_proofs,
61        codeword_opened_values,
62        codeword_merkle_proofs,
63        folding_pow_witnesses,
64        query_phase_pow_witnesses,
65        final_poly,
66    } = whir_proof;
67
68    let m = params.l_skip + params.n_stack;
69    let k_whir = params.k_whir();
70    debug_assert_eq!((m - params.log_final_poly_len()) % k_whir, 0);
71    let num_whir_rounds = params.num_whir_rounds();
72    let mut log_rs_domain_size = m + params.log_blowup;
73    debug_assert!(params.num_whir_sumcheck_rounds() <= m);
74    // Proof shape asserts this:
75    debug_assert_eq!(
76        folding_pow_witnesses.len(),
77        params.num_whir_sumcheck_rounds()
78    );
79
80    // Proof shape asserts whir_sumcheck_polys.len() == params.num_whir_sumcheck_rounds() :=
81    // num_whir_rounds * k_whir
82    let mut sumcheck_poly_iter = whir_sumcheck_polys.iter();
83    let mut folding_pow_iter = folding_pow_witnesses.iter();
84    let mu_pows: Vec<_> = mu.powers().take(widths.iter().sum::<usize>()).collect();
85    let mut claim = stacking_openings
86        .iter()
87        .flatten()
88        .zip(mu_pows.iter())
89        .fold(SC::EF::ZERO, |acc, (&opening, &mu_pow)| {
90            acc + mu_pow * opening
91        });
92
93    let mut gammas = Vec::with_capacity(num_whir_rounds);
94    let mut zs = Vec::with_capacity(num_whir_rounds);
95    let mut z0s = Vec::with_capacity(num_whir_rounds);
96    let mut alphas = Vec::with_capacity(m);
97
98    // Proof shape asserts this:
99    debug_assert_eq!(query_phase_pow_witnesses.len(), num_whir_rounds);
100    // By construction, params.whir.rounds.len() == num_whir_rounds
101    for (whir_round, (query_phase_pow_witness, round_params)) in
102        zip(query_phase_pow_witnesses, &params.whir.rounds).enumerate()
103    {
104        // A WHIR round consists of the following steps:
105        // 1) Run k rounds of sumcheck to obtain polynomial f'.
106        // 2) On non-final rounds, observe commitment f' on shifted domain.
107        // 3) On non-final rounds, sample OOD point z0 and observe claim y0 =?= f'(z0).
108        // 4) Sample in-domain queries z_i and compute f'(z_i) from openings. On the first round,
109        //    the codeword is not committed directly; instead it is derived from the stacking
110        //    commitments. In all other rounds, the previous codeword is committed directly.
111        // 5) On non-final rounds, sample batching parameter gamma to define next codeword and
112        //    derive new WHIR constraint target (`claim`).
113
114        let is_initial_round = whir_round == 0;
115        let is_final_round = whir_round == num_whir_rounds - 1;
116
117        let mut alphas_round = Vec::with_capacity(k_whir);
118
119        for _ in 0..k_whir {
120            // This is never None because num_whir_sumcheck_rounds == num_whir_rounds * k_whir
121            if let Some(evals) = sumcheck_poly_iter.next() {
122                let &[ev1, ev2] = evals;
123
124                transcript.observe_ext(ev1);
125                transcript.observe_ext(ev2);
126
127                let pow_witness = *folding_pow_iter.next().unwrap();
128                if !transcript.check_witness(params.whir.folding_pow_bits, pow_witness) {
129                    return Err(VerifyWhirError::FoldingPoWInvalid);
130                }
131                let alpha = transcript.sample_ext();
132                alphas_round.push(alpha);
133
134                let ev0 = claim - ev1;
135                claim = interpolate_quadratic_at_012(&[ev0, ev1, ev2], alpha);
136            }
137        }
138
139        let y0 = if is_final_round {
140            // Observe the final polynomial before the queries on the final
141            // round.
142            for coeff in final_poly {
143                transcript.observe_ext(*coeff);
144            }
145            None
146        } else {
147            // Proof shape asserts codeword_commits.len() == num_whir_rounds - 1
148            let commit = codeword_commits[whir_round];
149            transcript.observe_commit(commit);
150
151            let z0 = transcript.sample_ext();
152            z0s.push(z0);
153
154            // Proof shape asserts ood_values.len() == num_whir_rounds - 1
155            let y0 = ood_values[whir_round];
156            transcript.observe_ext(y0);
157            Some(y0)
158        };
159
160        if !transcript.check_witness(params.whir.query_phase_pow_bits, *query_phase_pow_witness) {
161            return Err(VerifyWhirError::QueryPhasePoWInvalid);
162        }
163
164        let num_queries = round_params.num_queries;
165        let query_indices =
166            (0..num_queries).map(|_| transcript.sample_bits(log_rs_domain_size - k_whir));
167
168        let mut zs_round = Vec::with_capacity(num_queries);
169        let mut ys_round = Vec::with_capacity(num_queries);
170
171        let hasher = config.hasher();
172        let omega = SC::F::two_adic_generator(log_rs_domain_size);
173        for (query_idx, index) in query_indices.into_iter().enumerate() {
174            let zi_root = omega.exp_u64(index);
175            let zi = zi_root.exp_power_of_2(k_whir);
176
177            let yi = if is_initial_round {
178                let mut codeword_vals = vec![SC::EF::ZERO; 1 << k_whir];
179                let mut mu_pow_iter = mu_pows.iter();
180                // Proof shape asserts everything in the izip! has length equal to layouts.len()
181                for (&commit, &width, opened_rows_per_query, merkle_proofs) in izip!(
182                    commitments,
183                    &widths,
184                    initial_round_opened_rows,
185                    initial_round_merkle_proofs
186                ) {
187                    // Proof shape asserts
188                    // - opened_rows_per_query.len() = whir.rounds[0].num_queries
189                    // - opened_rows.len() = 2^k_whir
190                    let opened_rows = &opened_rows_per_query[query_idx];
191                    let leaf_hashes = opened_rows
192                        .iter()
193                        .map(|opened_row| hasher.hash_slice(opened_row))
194                        .collect_vec();
195                    let query_digest = hasher.tree_compress(leaf_hashes);
196                    // Proof shape asserts
197                    // - merkle_proofs.len() = whir.rounds[0].num_queries
198                    // - merkle_proof.len() = l_skip + n_stack + log_blowup - k_whir =:
199                    //   log_rs_domain_size - k_whir
200                    let merkle_proof = &merkle_proofs[query_idx];
201                    merkle_verify(hasher, commit, index as u32, query_digest, merkle_proof)?;
202
203                    for c in 0..width {
204                        let mu_pow = mu_pow_iter.next().unwrap(); // ok; mu_pows has total_width length
205                        for j in 0..(1 << k_whir) {
206                            codeword_vals[j] += *mu_pow * opened_rows[j][c];
207                        }
208                    }
209                }
210                binary_k_fold::<SC::F, SC::EF>(codeword_vals, &alphas_round, zi_root)
211            } else {
212                // Proof shape asserts
213                // - codeword_opened_values.len() == codeword_merkle_proofs.len() == num_whir_rounds
214                //   - 1
215                // - codeword_opened_values[whir_round - 1].len() ==
216                //   codeword_merkle_proofs[whir_round - 1].len() ==
217                //   whir.rounds[whir_round].num_queries
218                // - codeword_opened_values[whir_round - 1][query_idx].len() = 2^k_whir
219                let opened_values = codeword_opened_values[whir_round - 1][query_idx].clone();
220                let merkle_proof = &codeword_merkle_proofs[whir_round - 1][query_idx];
221                let leaf_hashes = opened_values
222                    .iter()
223                    .map(|opened_value| {
224                        hasher.hash_slice(opened_value.as_basis_coefficients_slice())
225                    })
226                    .collect_vec();
227                let query_digest = hasher.tree_compress(leaf_hashes);
228                // Proof shape asserts `merkle_proof.len() == l_skip + n_stack + log_blowup - k_whir
229                // - round`, which equals `log_rs_domain_size - k_whir` at this point in time
230                merkle_verify(
231                    hasher,
232                    codeword_commits[whir_round - 1],
233                    index as u32,
234                    query_digest,
235                    merkle_proof,
236                )?;
237                binary_k_fold::<SC::F, SC::EF>(opened_values, &alphas_round, zi_root)
238            };
239            zs_round.push(zi);
240            ys_round.push(yi);
241        }
242        // We sample `gamma` even in the final round. There are no observations
243        // after this challenge and strictly serves to unify the verifier logic.
244        // Rather than checking that `final_poly(zi) = yi` for all `i` in the
245        // last round, we accumulate them into `claim`. The final WHIR check
246        // automatically performs this check for us (now with high probability).
247        let gamma = transcript.sample_ext();
248        if let Some(y0) = y0 {
249            claim += y0 * gamma;
250        }
251        for (yi, gamma_pow) in ys_round.iter().zip(gamma.powers().skip(2)) {
252            claim += *yi * gamma_pow;
253        }
254        gammas.push(gamma);
255        zs.push(zs_round);
256        alphas.extend(alphas_round);
257
258        log_rs_domain_size -= 1;
259    }
260    debug_assert!(sumcheck_poly_iter.next().is_none());
261
262    ensure(
263        final_poly.len() == 1 << params.log_final_poly_len(),
264        VerifyWhirError::FinalPolyDegree,
265    )?;
266
267    debug_assert_eq!(alphas.len(), k_whir * num_whir_rounds);
268    debug_assert_eq!(z0s.len(), num_whir_rounds - 1);
269    debug_assert_eq!(zs.len(), num_whir_rounds);
270    debug_assert_eq!(gammas.len(), num_whir_rounds);
271
272    // Here we perform the final WHIR check, which requires us to compute
273    //
274    //  sum_{b in H_{m-t}} f(b) (mobius_eq(u, alpha || b) +
275    //                           sum_i sum_j gamma_{i,j} eq(pow(z_i) alpha[ki..] || b)),
276    //
277    // where || denotes concatenation.
278    //
279    // If we let u' = u[..t] and u'' = u[t..], then by factoring we can rewrite the term
280    // ```text
281    // sum_{b in H_{m-t}} f(b) mobius_eq(u, alpha || b) = mobius_eq(u', alpha) *
282    //                                                    sum_{b in H_{m-t}} f(b) mobius_eq(u'',b)
283    // ```
284    //
285    // For multilinear f with coefficient table `c[S]`, we have the identity:
286    //   sum_{b} f(b) mobius_eq(u'', b) = sum_{S} c[S] eq(u'', S),
287    // i.e. it is the MLE of the table `c` evaluated at `u''`.
288    //
289    // Similar algebra allows us to control the terms with eq(pow(z_i)). Note that here we actually
290    // end up with f(pow(z_i^{2^p})) for some power p, which is a univariate evaluation.
291    let t = k_whir * num_whir_rounds;
292    let prefix = eval_mobius_eq_mle(&u[..t], &alphas[..t]);
293    let suffix_sum = eval_mle_evals_at_point(&mut final_poly.clone(), &u[t..]);
294    let mut acc = prefix * suffix_sum;
295    let mut j = k_whir;
296    for i in 0..num_whir_rounds {
297        let zis = &zs[i];
298        let gamma = gammas[i];
299        let alpha_slc = &alphas[j..t];
300        let slc_len = (t - j) + 1;
301
302        if i != num_whir_rounds - 1 {
303            let z0_pow = z0s[i].exp_powers_of_2().take(slc_len).collect_vec();
304            let (z0_pow_max, z0_pow_left) = z0_pow.split_last().unwrap();
305            acc += gamma
306                * eval_eq_mle(alpha_slc, z0_pow_left)
307                * horner_eval::<SC::EF, SC::EF, SC::EF>(final_poly, *z0_pow_max);
308        }
309
310        debug_assert_eq!(zis.len(), params.whir.rounds[i].num_queries);
311        for (zi, gamma_pow) in zip(zis, gamma.powers().skip(2)) {
312            let zi_pow = zi.exp_powers_of_2().take(slc_len).collect_vec();
313            let (zi_pow_max, zi_pow_left) = zi_pow.split_last().unwrap();
314            acc += gamma_pow
315                * eval_eq_mle(alpha_slc, zi_pow_left)
316                * horner_eval::<SC::EF, SC::F, SC::EF>(final_poly, *zi_pow_max);
317        }
318        j += k_whir;
319    }
320    ensure(acc == claim, VerifyWhirError::FinalPolyConstraint)
321}
322
323#[derive(Debug, Error, PartialEq, Eq)]
324pub enum VerifyWhirError {
325    #[error("final polynomial has wrong degree")]
326    FinalPolyDegree,
327    #[error("μ batching proof-of-work witness check failed")]
328    MuPoWInvalid,
329    #[error("folding proof-of-work witness check failed")]
330    FoldingPoWInvalid,
331    #[error("query phase proof-of-work witness check failed")]
332    QueryPhasePoWInvalid,
333    #[error("final polynomial doesn't explain queries")]
334    FinalPolyQueryMismatch,
335    #[error("final poly is not in the final constrained RS code")]
336    FinalPolyConstraint,
337    #[error("merkle verification failed")]
338    MerkleVerify,
339}
340
341/// Evaluates the k-fold binary fold of `f` at `x^{2^k}` given its evaluations
342/// `values` on the coset `H = {x, ωx, …, ω^{2^k-1}x}` and fold points `alphas`.
343///
344/// Let `g₀ = f`. For `i >= 1` define
345///
346///   gᵢ(Y) = fold(g_{i-1}; α_{i-1})(Y),
347///
348/// where
349///
350///   fold(h; α)(X²) = h(X) + (α - X) * (h(X) - h(-X)) / (2X).
351///
352/// If `values = [f(x), f(ωx), …, f(ω^{2^k-1}x)]`, then
353/// `binary_k_fold(values, alphas, x)` returns `g_k(x^{2^k})`.
354pub fn binary_k_fold<F: TwoAdicField, EF: ExtensionField<F>>(
355    mut values: Vec<EF>,
356    alphas: &[EF],
357    x: F,
358) -> EF {
359    let n = values.len();
360    let k = alphas.len();
361    debug_assert_eq!(n, 1 << k);
362
363    let omega_k = F::two_adic_generator(k);
364    let omega_k_inv = omega_k.inverse();
365
366    let tw = omega_k.powers().take(1 << (k - 1)).collect_vec();
367    let inv_tw = omega_k_inv.powers().take(1 << (k - 1)).collect_vec();
368
369    for (j, (&alpha, x_pow, x_inv_pow)) in izip!(
370        alphas.iter(),
371        x.exp_powers_of_2(),
372        x.inverse().exp_powers_of_2()
373    )
374    .enumerate()
375    {
376        let m = n >> (j + 1);
377        let (lo, hi) = values.split_at_mut(m);
378
379        for i in 0..m {
380            let t = tw[i << j] * x_pow;
381            let t_inv = inv_tw[i << j] * x_inv_pow;
382            lo[i] += (alpha - t) * (lo[i] - hi[i]) * t_inv.halve();
383        }
384    }
385    values[0]
386}
387
388pub fn merkle_verify<H: MerkleHasher>(
389    hasher: &H,
390    root: H::Digest,
391    mut idx: u32,
392    leaf_hash: H::Digest,
393    merkle_proof: &[H::Digest],
394) -> Result<(), VerifyWhirError>
395where
396    H::Digest: Eq,
397{
398    let mut cur = leaf_hash;
399    for &sibling in merkle_proof {
400        cur = if idx & 1 == 0 {
401            hasher.compress(cur, sibling)
402        } else {
403            hasher.compress(sibling, cur)
404        };
405        idx >>= 1;
406    }
407    if root != cur {
408        Err(VerifyWhirError::MerkleVerify)
409    } else {
410        Ok(())
411    }
412}