openvm_stark_backend/verifier/
mod.rs

1use std::iter::zip;
2
3use itertools::{izip, zip_eq, Itertools};
4use p3_challenger::{CanObserve, FieldChallenger};
5use p3_commit::{Pcs, PolynomialSpace};
6use p3_field::{FieldAlgebra, FieldExtensionAlgebra};
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_canonical_usize(num_airs));
70        for &air_id in &air_ids {
71            challenger.observe(Val::<SC>::from_canonical_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_canonical_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_ext_element();
188        tracing::debug!("alpha: {alpha:?}");
189
190        // Observe quotient commitments
191        challenger.observe(proof.commitments.quotient.clone());
192
193        // Draw `zeta` challenge
194        let zeta: SC::Challenge = challenger.sample_ext_element();
195        tracing::debug!("zeta: {zeta:?}");
196
197        let pcs = self.config.pcs();
198        // Build domains
199        let (domains, quotient_chunks_domains): (Vec<_>, Vec<Vec<_>>) = mvk
200            .per_air
201            .iter()
202            .zip_eq(&proof.per_air)
203            .map(|(vk, air_proof)| {
204                let degree = air_proof.degree;
205                let quotient_degree = vk.quotient_degree;
206                let domain = pcs.natural_domain_for_degree(degree);
207                let quotient_domain =
208                    domain.create_disjoint_domain(degree * quotient_degree as usize);
209                let qc_domains = quotient_domain.split_domains(quotient_degree as usize);
210                (domain, qc_domains)
211            })
212            .unzip();
213        // Verify all opening proofs
214        let opened_values = &proof.opening.values;
215        let trace_domain_and_openings =
216            |domain: Domain<SC>,
217             zeta: SC::Challenge,
218             values: &AdjacentOpenedValues<SC::Challenge>| {
219                (
220                    domain,
221                    vec![
222                        (zeta, values.local.clone()),
223                        (domain.next_point(zeta).unwrap(), values.next.clone()),
224                    ],
225                )
226            };
227        // Build the opening rounds
228        // 1. First the preprocessed trace openings
229        // Assumption: each AIR with preprocessed trace has its own commitment and opening values
230        // T05a: validate `opened_values.preprocessed` shape
231        let preprocessed_widths: Vec<usize> = mvk
232            .per_air
233            .iter()
234            .filter_map(|vk| vk.params.width.preprocessed)
235            .collect();
236        if preprocessed_widths.len() != opened_values.preprocessed.len()
237            || zip_eq(preprocessed_widths, &opened_values.preprocessed)
238                .any(|(w, ov)| w != ov.local.len() || w != ov.next.len())
239        {
240            return Err(VerificationError::InvalidProofShape);
241        }
242        let mut rounds: Vec<_> = mvk
243            .preprocessed_commits()
244            .into_iter()
245            .zip_eq(&domains)
246            .flat_map(|(commit, domain)| commit.map(|commit| (commit, *domain)))
247            .zip_eq(&opened_values.preprocessed)
248            .map(|((commit, domain), values)| {
249                let domain_and_openings = trace_domain_and_openings(domain, zeta, values);
250                (commit, vec![domain_and_openings])
251            })
252            .collect();
253
254        // 2. Then the main trace openings
255        // T05b: validate `opened_values.main` shape inline below
256        let num_main_commits = opened_values.main.len();
257        if num_main_commits != proof.commitments.main_trace.len() {
258            return Err(VerificationError::InvalidProofShape);
259        }
260        let mut main_commit_idx = 0;
261        // All commits except the last one are cached main traces.
262        for (vk, domain) in zip_eq(&mvk.per_air, &domains) {
263            for &cached_main_width in &vk.params.width.cached_mains {
264                let commit = proof.commitments.main_trace[main_commit_idx].clone();
265                if opened_values.main[main_commit_idx].len() != 1 {
266                    return Err(VerificationError::InvalidProofShape);
267                }
268                let value = &opened_values.main[main_commit_idx][0];
269                if cached_main_width != value.local.len() || cached_main_width != value.next.len() {
270                    return Err(VerificationError::InvalidProofShape);
271                }
272                let domains_and_openings = vec![trace_domain_and_openings(*domain, zeta, value)];
273                rounds.push((commit.clone(), domains_and_openings));
274                main_commit_idx += 1;
275            }
276        }
277        // In the last commit, each matrix corresponds to an AIR with a common main trace.
278        {
279            let values_per_mat = &opened_values.main[main_commit_idx];
280            let commit = proof.commitments.main_trace[main_commit_idx].clone();
281            let domains_and_openings = zip(&mvk.per_air, &domains)
282                .filter(|(vk, _)| vk.has_common_main())
283                .zip(values_per_mat)
284                .map(|((vk, domain), values)| {
285                    let width = vk.params.width.common_main;
286                    if width != values.local.len() || width != values.next.len() {
287                        Err(VerificationError::InvalidProofShape)
288                    } else {
289                        Ok(trace_domain_and_openings(*domain, zeta, values))
290                    }
291                })
292                .collect::<Result<Vec<_>, _>>()?;
293            if domains_and_openings.len() != values_per_mat.len() {
294                return Err(VerificationError::InvalidProofShape);
295            }
296            rounds.push((commit.clone(), domains_and_openings));
297        }
298
299        let ext_degree = <SC::Challenge as FieldExtensionAlgebra<Val<SC>>>::D;
300        // 3. Then after_challenge trace openings, at most 1 phase for now.
301        // All AIRs with interactions should an after challenge trace.
302        let mut after_challenge_vk_domain_per_air = zip_eq(&mvk.per_air, &domains)
303            .filter(|(vk, _)| vk.has_interaction())
304            .peekable();
305        if after_challenge_vk_domain_per_air.peek().is_none() {
306            if !proof.commitments.after_challenge.is_empty()
307                || !opened_values.after_challenge.is_empty()
308            {
309                return Err(VerificationError::InvalidProofShape);
310            }
311            assert_eq!(num_phases, 0);
312        } else {
313            if num_phases != 1 || opened_values.after_challenge.len() != 1 {
314                return Err(VerificationError::InvalidProofShape);
315            }
316            let after_challenge_commit = proof.commitments.after_challenge[0].clone();
317            let domains_and_openings = zip(
318                after_challenge_vk_domain_per_air,
319                &opened_values.after_challenge[0],
320            )
321            .map(|((vk, domain), values)| {
322                let width = vk.params.width.after_challenge[0] * ext_degree;
323                if width != values.local.len() || width != values.next.len() {
324                    Err(VerificationError::InvalidProofShape)
325                } else {
326                    Ok(trace_domain_and_openings(*domain, zeta, values))
327                }
328            })
329            .collect::<Result<Vec<_>, _>>()?;
330            if domains_and_openings.len() != opened_values.after_challenge[0].len() {
331                return Err(VerificationError::InvalidProofShape);
332            }
333            rounds.push((after_challenge_commit, domains_and_openings));
334        }
335        if opened_values.quotient.len() != num_airs {
336            return Err(VerificationError::InvalidProofShape);
337        }
338        // All opened_values.quotient should have width D
339        if zip_eq(&opened_values.quotient, &mvk.per_air).any(|(per_air, vk)| {
340            per_air.len() != vk.quotient_degree as usize || {
341                per_air
342                    .iter()
343                    .any(|per_chunk| per_chunk.len() != ext_degree)
344            }
345        }) {
346            return Err(VerificationError::InvalidProofShape);
347        }
348        let quotient_domains_and_openings =
349            zip_eq(&opened_values.quotient, &quotient_chunks_domains)
350                .flat_map(|(chunk, quotient_chunks_domains_per_air)| {
351                    chunk
352                        .iter()
353                        .zip_eq(quotient_chunks_domains_per_air)
354                        .map(|(values, &domain)| (domain, vec![(zeta, values.clone())]))
355                })
356                .collect_vec();
357        rounds.push((
358            proof.commitments.quotient.clone(),
359            quotient_domains_and_openings,
360        ));
361
362        pcs.verify(rounds, &proof.opening.proof, challenger)
363            .map_err(|e| VerificationError::InvalidOpeningArgument(format!("{:?}", e)))?;
364
365        let mut preprocessed_idx = 0usize; // preprocessed commit idx
366        let mut after_challenge_idx = vec![0usize; num_phases];
367        let mut cached_main_commit_idx = 0;
368        let mut common_main_matrix_idx = 0;
369
370        // Verify each RAP's constraints
371        for (domain, qc_domains, quotient_chunks, vk, air_proof) in izip!(
372            domains,
373            quotient_chunks_domains,
374            &opened_values.quotient,
375            &mvk.per_air,
376            &proof.per_air
377        ) {
378            let preprocessed_values = vk.preprocessed_data.as_ref().map(|_| {
379                let values = &opened_values.preprocessed[preprocessed_idx];
380                preprocessed_idx += 1;
381                values
382            });
383            let mut partitioned_main_values = Vec::with_capacity(vk.num_cached_mains());
384            for _ in 0..vk.num_cached_mains() {
385                partitioned_main_values.push(&opened_values.main[cached_main_commit_idx][0]);
386                cached_main_commit_idx += 1;
387            }
388            if vk.has_common_main() {
389                partitioned_main_values
390                    .push(&opened_values.main.last().unwrap()[common_main_matrix_idx]);
391                common_main_matrix_idx += 1;
392            }
393            // loop through challenge phases of this single RAP
394            let after_challenge_values = if vk.has_interaction() {
395                (0..num_phases)
396                    .map(|phase_idx| {
397                        let matrix_idx = after_challenge_idx[phase_idx];
398                        after_challenge_idx[phase_idx] += 1;
399                        &opened_values.after_challenge[phase_idx][matrix_idx]
400                    })
401                    .collect_vec()
402            } else {
403                vec![]
404            };
405            verify_single_rap_constraints::<SC>(
406                &vk.symbolic_constraints.constraints,
407                preprocessed_values,
408                partitioned_main_values,
409                after_challenge_values,
410                quotient_chunks,
411                domain,
412                &qc_domains,
413                zeta,
414                alpha,
415                &after_challenge_data.challenges_per_phase,
416                &air_proof.public_values,
417                &air_proof.exposed_values_after_challenge,
418            )?;
419        }
420
421        // If we made it this far, use the `rap_phase_result` as the final result.
422        rap_phase_seq_result
423    }
424}