openvm_stark_backend/verifier/
mod.rs

1use std::iter::zip;
2
3use itertools::{izip, zip_eq, Itertools};
4use p3_challenger::{CanObserve, FieldChallenger, GrindingChallenger};
5use p3_commit::{Pcs, PolynomialSpace};
6use p3_field::{BasedVectorSpace, PrimeCharacteristicRing};
7use p3_util::log2_strict_usize;
8use tracing::instrument;
9
10use crate::{
11    config::{Com, Domain, StarkGenericConfig, Val},
12    interaction::RapPhaseSeq,
13    keygen::{types::MultiStarkVerifyingKey, view::MultiStarkVerifyingKeyView},
14    proof::{AdjacentOpenedValues, Proof},
15    verifier::constraints::verify_single_rap_constraints,
16};
17
18pub mod constraints;
19mod error;
20/// Constraint folder
21pub mod folder;
22
23pub use error::*;
24pub use folder::GenericVerifierConstraintFolder;
25
26/// Verifies a partitioned proof of multi-matrix AIRs.
27pub struct MultiTraceStarkVerifier<'c, SC: StarkGenericConfig> {
28    config: &'c SC,
29}
30
31impl<'c, SC: StarkGenericConfig> MultiTraceStarkVerifier<'c, SC> {
32    pub fn new(config: &'c SC) -> Self {
33        Self { config }
34    }
35    /// Verify collection of InteractiveAIRs and check the permutation
36    /// cumulative sum is equal to zero across all AIRs.
37    #[instrument(name = "MultiTraceStarkVerifier::verify", level = "debug", skip_all)]
38    pub fn verify(
39        &self,
40        challenger: &mut SC::Challenger,
41        mvk: &MultiStarkVerifyingKey<SC>,
42        proof: &Proof<SC>,
43    ) -> Result<(), VerificationError> {
44        // Note: construction of view panics if any air_id exceeds number of AIRs in provided
45        // `MultiStarkVerifyingKey`
46        let mvk = mvk.view(&proof.get_air_ids());
47        self.verify_raps(challenger, &mvk, proof)?;
48        Ok(())
49    }
50
51    /// Verify general RAPs without checking any relations (e.g., cumulative sum) between exposed
52    /// values of different RAPs.
53    ///
54    /// Public values is a global list shared across all AIRs.
55    ///
56    /// - `num_challenges_to_sample[i]` is the number of challenges to sample in the trace challenge
57    ///   phase corresponding to `proof.commitments.after_challenge[i]`. This must have length equal
58    /// to `proof.commitments.after_challenge`.
59    #[instrument(level = "debug", skip_all)]
60    pub fn verify_raps(
61        &self,
62        challenger: &mut SC::Challenger,
63        mvk: &MultiStarkVerifyingKeyView<Val<SC>, Com<SC>>,
64        proof: &Proof<SC>,
65    ) -> Result<(), VerificationError> {
66        challenger.observe(mvk.pre_hash.clone());
67        let air_ids = proof.get_air_ids();
68        let num_airs = air_ids.len();
69        challenger.observe(Val::<SC>::from_usize(num_airs));
70        for &air_id in &air_ids {
71            challenger.observe(Val::<SC>::from_usize(air_id));
72        }
73        // Enforce trace height linear inequalities
74        for constraint in mvk.trace_height_constraints {
75            let sum = proof
76                .per_air
77                .iter()
78                .map(|ap| constraint.coefficients[ap.air_id] as u64 * ap.degree as u64)
79                .sum::<u64>();
80            if sum >= constraint.threshold as u64 {
81                return Err(VerificationError::InvalidProofShape);
82            }
83        }
84        // (T01a): Check that all `air_id`s are different and contained in `MultiStarkVerifyingKey`
85        {
86            let mut air_ids = air_ids;
87            air_ids.sort();
88            for ids in air_ids.windows(2) {
89                if ids[0] >= ids[1] {
90                    return Err(VerificationError::DuplicateAirs);
91                }
92            }
93        }
94        // Note: (T02) is implicit in use of BTreeMap in FRI PCS verify
95
96        let public_values = proof.get_public_values();
97        // (T03a): verify shape of public values
98        {
99            for (pvs_per_air, vk) in zip_eq(&public_values, &mvk.per_air) {
100                if pvs_per_air.len() != vk.params.num_public_values {
101                    return Err(VerificationError::InvalidProofShape);
102                }
103            }
104        }
105        // Challenger must observe public values
106        for pis in &public_values {
107            challenger.observe_slice(pis);
108        }
109
110        for preprocessed_commit in mvk.flattened_preprocessed_commits() {
111            challenger.observe(preprocessed_commit);
112        }
113
114        // (T04a): validate shapes of `main_trace_commits`:
115        {
116            let num_cached_mains = mvk
117                .per_air
118                .iter()
119                .map(|vk| vk.params.width.cached_mains.len())
120                .sum::<usize>();
121            let num_main_commits = num_cached_mains + 1; // always 1 common main
122            if proof.commitments.main_trace.len() != num_main_commits {
123                return Err(VerificationError::InvalidProofShape);
124            }
125        }
126        // Observe main trace commitments
127        challenger.observe_slice(&proof.commitments.main_trace);
128        challenger.observe_slice(
129            &proof
130                .per_air
131                .iter()
132                .map(|ap| Val::<SC>::from_usize(log2_strict_usize(ap.degree)))
133                .collect_vec(),
134        );
135
136        // Verification of challenge phase (except openings, which are done next).
137        let rap_phase = self.config.rap_phase_seq();
138        let exposed_values_per_air_per_phase = proof
139            .per_air
140            .iter()
141            .map(|proof| proof.exposed_values_after_challenge.clone())
142            .collect_vec();
143        let permutation_opened_values = proof
144            .opening
145            .values
146            .after_challenge
147            .iter()
148            .map(|after_challenge_per_matrix| {
149                after_challenge_per_matrix
150                    .iter()
151                    .map(|after_challenge| {
152                        vec![after_challenge.local.clone(), after_challenge.next.clone()]
153                    })
154                    .collect_vec()
155            })
156            .collect_vec();
157
158        // (T01b): `num_phases < 2`.
159        // Assumption: valid mvk has num_phases consistent between num_challenges_to_sample and
160        // exposed_values
161        let num_phases = mvk.num_phases();
162        if num_phases != proof.commitments.after_challenge.len() || num_phases > 1 {
163            return Err(VerificationError::InvalidProofShape);
164        }
165        // (T01c): validate shape of `exposed_values_after_challenge`
166        if !zip_eq(&exposed_values_per_air_per_phase, &mvk.per_air).all(|(ev_per_phase, vk)| {
167            ev_per_phase.len() == vk.params.num_exposed_values_after_challenge.len()
168                && zip_eq(ev_per_phase, &vk.params.num_exposed_values_after_challenge)
169                    .all(|(ev, &n)| ev.len() == n)
170        }) {
171            return Err(VerificationError::InvalidProofShape);
172        }
173
174        let (after_challenge_data, rap_phase_seq_result) = rap_phase.partially_verify(
175            challenger,
176            proof.rap_phase_seq_proof.as_ref(),
177            &exposed_values_per_air_per_phase,
178            &proof.commitments.after_challenge,
179            &permutation_opened_values,
180        );
181        // We don't want to bail on error yet; `OodEvaluationMismatch` should take precedence over
182        // `ChallengePhaseError`, but we won't know if the former happens until later.
183        let rap_phase_seq_result =
184            rap_phase_seq_result.map_err(|_| VerificationError::ChallengePhaseError);
185
186        // Draw `alpha` challenge
187        let alpha: SC::Challenge = challenger.sample_algebra_element();
188        tracing::debug!("alpha: {alpha:?}");
189
190        // Observe quotient commitments
191        challenger.observe(proof.commitments.quotient.clone());
192
193        if !challenger.check_witness(
194            self.config.deep_ali_params().deep_pow_bits,
195            proof.opening.deep_pow_witness,
196        ) {
197            return Err(VerificationError::InvalidDeepPowWitness);
198        }
199        // Draw `zeta` challenge
200        let zeta: SC::Challenge = challenger.sample_algebra_element();
201        tracing::debug!("zeta: {zeta:?}");
202
203        let pcs = self.config.pcs();
204        // Build domains
205        let (domains, quotient_chunks_domains): (Vec<_>, Vec<Vec<_>>) = mvk
206            .per_air
207            .iter()
208            .zip_eq(&proof.per_air)
209            .map(|(vk, air_proof)| {
210                let degree = air_proof.degree;
211                let quotient_degree = vk.quotient_degree;
212                let domain = pcs.natural_domain_for_degree(degree);
213                let quotient_domain =
214                    domain.create_disjoint_domain(degree * quotient_degree as usize);
215                let qc_domains = quotient_domain.split_domains(quotient_degree as usize);
216                (domain, qc_domains)
217            })
218            .unzip();
219        // Verify all opening proofs
220        let opened_values = &proof.opening.values;
221        let trace_domain_and_openings =
222            |domain: Domain<SC>,
223             zeta: SC::Challenge,
224             values: &AdjacentOpenedValues<SC::Challenge>| {
225                (
226                    domain,
227                    vec![
228                        (zeta, values.local.clone()),
229                        (domain.next_point(zeta).unwrap(), values.next.clone()),
230                    ],
231                )
232            };
233        // Build the opening rounds
234        // 1. First the preprocessed trace openings
235        // Assumption: each AIR with preprocessed trace has its own commitment and opening values
236        // T05a: validate `opened_values.preprocessed` shape
237        let preprocessed_widths: Vec<usize> = mvk
238            .per_air
239            .iter()
240            .filter_map(|vk| vk.params.width.preprocessed)
241            .collect();
242        if preprocessed_widths.len() != opened_values.preprocessed.len()
243            || zip_eq(preprocessed_widths, &opened_values.preprocessed)
244                .any(|(w, ov)| w != ov.local.len() || w != ov.next.len())
245        {
246            return Err(VerificationError::InvalidProofShape);
247        }
248        let mut rounds: Vec<_> = mvk
249            .preprocessed_commits()
250            .into_iter()
251            .zip_eq(&domains)
252            .flat_map(|(commit, domain)| commit.map(|commit| (commit, *domain)))
253            .zip_eq(&opened_values.preprocessed)
254            .map(|((commit, domain), values)| {
255                let domain_and_openings = trace_domain_and_openings(domain, zeta, values);
256                (commit, vec![domain_and_openings])
257            })
258            .collect();
259
260        // 2. Then the main trace openings
261        // T05b: validate `opened_values.main` shape inline below
262        let num_main_commits = opened_values.main.len();
263        if num_main_commits != proof.commitments.main_trace.len() {
264            return Err(VerificationError::InvalidProofShape);
265        }
266        let mut main_commit_idx = 0;
267        // All commits except the last one are cached main traces.
268        for (vk, domain) in zip_eq(&mvk.per_air, &domains) {
269            for &cached_main_width in &vk.params.width.cached_mains {
270                let commit = proof.commitments.main_trace[main_commit_idx].clone();
271                if opened_values.main[main_commit_idx].len() != 1 {
272                    return Err(VerificationError::InvalidProofShape);
273                }
274                let value = &opened_values.main[main_commit_idx][0];
275                if cached_main_width != value.local.len() || cached_main_width != value.next.len() {
276                    return Err(VerificationError::InvalidProofShape);
277                }
278                let domains_and_openings = vec![trace_domain_and_openings(*domain, zeta, value)];
279                rounds.push((commit.clone(), domains_and_openings));
280                main_commit_idx += 1;
281            }
282        }
283        // In the last commit, each matrix corresponds to an AIR with a common main trace.
284        {
285            let values_per_mat = &opened_values.main[main_commit_idx];
286            let commit = proof.commitments.main_trace[main_commit_idx].clone();
287            let domains_and_openings = zip(&mvk.per_air, &domains)
288                .filter(|(vk, _)| vk.has_common_main())
289                .zip(values_per_mat)
290                .map(|((vk, domain), values)| {
291                    let width = vk.params.width.common_main;
292                    if width != values.local.len() || width != values.next.len() {
293                        Err(VerificationError::InvalidProofShape)
294                    } else {
295                        Ok(trace_domain_and_openings(*domain, zeta, values))
296                    }
297                })
298                .collect::<Result<Vec<_>, _>>()?;
299            if domains_and_openings.len() != values_per_mat.len() {
300                return Err(VerificationError::InvalidProofShape);
301            }
302            rounds.push((commit.clone(), domains_and_openings));
303        }
304
305        let ext_degree = <SC::Challenge as BasedVectorSpace<Val<SC>>>::DIMENSION;
306        // 3. Then after_challenge trace openings, at most 1 phase for now.
307        // All AIRs with interactions should an after challenge trace.
308        let mut after_challenge_vk_domain_per_air = zip_eq(&mvk.per_air, &domains)
309            .filter(|(vk, _)| vk.has_interaction())
310            .peekable();
311        if after_challenge_vk_domain_per_air.peek().is_none() {
312            if !proof.commitments.after_challenge.is_empty()
313                || !opened_values.after_challenge.is_empty()
314            {
315                return Err(VerificationError::InvalidProofShape);
316            }
317            assert_eq!(num_phases, 0);
318        } else {
319            if num_phases != 1 || opened_values.after_challenge.len() != 1 {
320                return Err(VerificationError::InvalidProofShape);
321            }
322            let after_challenge_commit = proof.commitments.after_challenge[0].clone();
323            let domains_and_openings = zip(
324                after_challenge_vk_domain_per_air,
325                &opened_values.after_challenge[0],
326            )
327            .map(|((vk, domain), values)| {
328                let width = vk.params.width.after_challenge[0] * ext_degree;
329                if width != values.local.len() || width != values.next.len() {
330                    Err(VerificationError::InvalidProofShape)
331                } else {
332                    Ok(trace_domain_and_openings(*domain, zeta, values))
333                }
334            })
335            .collect::<Result<Vec<_>, _>>()?;
336            if domains_and_openings.len() != opened_values.after_challenge[0].len() {
337                return Err(VerificationError::InvalidProofShape);
338            }
339            rounds.push((after_challenge_commit, domains_and_openings));
340        }
341        if opened_values.quotient.len() != num_airs {
342            return Err(VerificationError::InvalidProofShape);
343        }
344        // All opened_values.quotient should have width D
345        if zip_eq(&opened_values.quotient, &mvk.per_air).any(|(per_air, vk)| {
346            per_air.len() != vk.quotient_degree as usize || {
347                per_air
348                    .iter()
349                    .any(|per_chunk| per_chunk.len() != ext_degree)
350            }
351        }) {
352            return Err(VerificationError::InvalidProofShape);
353        }
354        let quotient_domains_and_openings =
355            zip_eq(&opened_values.quotient, &quotient_chunks_domains)
356                .flat_map(|(chunk, quotient_chunks_domains_per_air)| {
357                    chunk
358                        .iter()
359                        .zip_eq(quotient_chunks_domains_per_air)
360                        .map(|(values, &domain)| (domain, vec![(zeta, values.clone())]))
361                })
362                .collect_vec();
363        rounds.push((
364            proof.commitments.quotient.clone(),
365            quotient_domains_and_openings,
366        ));
367
368        pcs.verify(rounds, &proof.opening.proof, challenger)
369            .map_err(|e| VerificationError::InvalidOpeningArgument(format!("{:?}", e)))?;
370
371        let mut preprocessed_idx = 0usize; // preprocessed commit idx
372        let mut after_challenge_idx = vec![0usize; num_phases];
373        let mut cached_main_commit_idx = 0;
374        let mut common_main_matrix_idx = 0;
375
376        // Verify each RAP's constraints
377        for (domain, qc_domains, quotient_chunks, vk, air_proof) in izip!(
378            domains,
379            quotient_chunks_domains,
380            &opened_values.quotient,
381            &mvk.per_air,
382            &proof.per_air
383        ) {
384            let preprocessed_values = vk.preprocessed_data.as_ref().map(|_| {
385                let values = &opened_values.preprocessed[preprocessed_idx];
386                preprocessed_idx += 1;
387                values
388            });
389            let mut partitioned_main_values = Vec::with_capacity(vk.num_cached_mains());
390            for _ in 0..vk.num_cached_mains() {
391                partitioned_main_values.push(&opened_values.main[cached_main_commit_idx][0]);
392                cached_main_commit_idx += 1;
393            }
394            if vk.has_common_main() {
395                partitioned_main_values
396                    .push(&opened_values.main.last().unwrap()[common_main_matrix_idx]);
397                common_main_matrix_idx += 1;
398            }
399            // loop through challenge phases of this single RAP
400            let after_challenge_values = if vk.has_interaction() {
401                (0..num_phases)
402                    .map(|phase_idx| {
403                        let matrix_idx = after_challenge_idx[phase_idx];
404                        after_challenge_idx[phase_idx] += 1;
405                        &opened_values.after_challenge[phase_idx][matrix_idx]
406                    })
407                    .collect_vec()
408            } else {
409                vec![]
410            };
411            verify_single_rap_constraints::<SC>(
412                &vk.symbolic_constraints.constraints,
413                preprocessed_values,
414                partitioned_main_values,
415                after_challenge_values,
416                quotient_chunks,
417                domain,
418                &qc_domains,
419                zeta,
420                alpha,
421                &after_challenge_data.challenges_per_phase,
422                &air_proof.public_values,
423                &air_proof.exposed_values_after_challenge,
424            )?;
425        }
426
427        // If we made it this far, use the `rap_phase_result` as the final result.
428        rap_phase_seq_result
429    }
430}