sp1_stark/
verifier.rs

1use core::fmt::Display;
2use std::{
3    fmt::{Debug, Formatter},
4    marker::PhantomData,
5};
6
7use itertools::Itertools;
8use p3_air::{Air, BaseAir};
9use p3_challenger::{CanObserve, FieldChallenger};
10use p3_commit::{LagrangeSelectors, Pcs, PolynomialSpace};
11use p3_field::{AbstractExtensionField, AbstractField, Field, PrimeField64, TwoAdicField};
12
13use super::{
14    folder::VerifierConstraintFolder,
15    types::{AirOpenedValues, ChipOpenedValues, ShardCommitment, ShardProof},
16    Domain, OpeningError, StarkGenericConfig, StarkVerifyingKey, Val,
17};
18use crate::{
19    air::{InteractionScope, MachineAir},
20    InteractionKind, MachineChip,
21};
22
23/// A verifier for a collection of air chips.
24pub struct Verifier<SC, A>(PhantomData<SC>, PhantomData<A>);
25
26impl<SC: StarkGenericConfig, A: MachineAir<Val<SC>>> Verifier<SC, A> {
27    /// Verify a proof for a collection of air chips.
28    #[allow(clippy::too_many_lines)]
29    pub fn verify_shard(
30        config: &SC,
31        vk: &StarkVerifyingKey<SC>,
32        chips: &[&MachineChip<SC, A>],
33        challenger: &mut SC::Challenger,
34        proof: &ShardProof<SC>,
35    ) -> Result<(), VerificationError<SC>>
36    where
37        A: for<'a> Air<VerifierConstraintFolder<'a, SC>>,
38    {
39        use itertools::izip;
40
41        let ShardProof {
42            commitment,
43            opened_values,
44            opening_proof,
45            chip_ordering,
46            public_values,
47            ..
48        } = proof;
49
50        let pcs = config.pcs();
51
52        if chips.len() != opened_values.chips.len() {
53            return Err(VerificationError::ChipOpeningLengthMismatch);
54        }
55
56        let log_degrees = opened_values.chips.iter().map(|val| val.log_degree).collect::<Vec<_>>();
57
58        let log_quotient_degrees =
59            chips.iter().map(|chip| chip.log_quotient_degree()).collect::<Vec<_>>();
60
61        // Assert that the log degree of chips are valid.
62        for ((chip, log_degree), log_quotient_degree) in
63            chips.iter().zip_eq(log_degrees.iter()).zip_eq(log_quotient_degrees.iter())
64        {
65            if log_degree.saturating_add(*log_quotient_degree) > SC::Val::TWO_ADICITY {
66                return Err(VerificationError::InvalidLogDegree(chip.name(), *log_degree));
67            }
68        }
69
70        // Assert that the lookup multiplicities don't overflow.
71        // SAFETY: The number of chips is bounded by the fixed `StarkMachine` structure.
72        // Also, `val.log_degree` is checked to be at most `min(SC::Val::TWO_ADICITY, 32)`.
73        // Therefore, this summation cannot overflow u64. Use saturating ops for extra safety.
74        for kind in InteractionKind::all_kinds() {
75            let mut max_lookup_mult = 0u64;
76            chips.iter().zip(opened_values.chips.iter()).for_each(|(chip, val)| {
77                max_lookup_mult = max_lookup_mult.saturating_add(
78                    (chip.num_sends_by_kind(kind) as u64 + chip.num_receives_by_kind(kind) as u64)
79                        .saturating_mul(1u64 << val.log_degree),
80                );
81            });
82            if max_lookup_mult >= SC::Val::ORDER_U64 {
83                return Err(VerificationError::LookupMultiplicityOverflow);
84            }
85        }
86
87        let trace_domains = log_degrees
88            .iter()
89            .map(|log_degree| pcs.natural_domain_for_degree(1 << log_degree))
90            .collect::<Vec<_>>();
91
92        let ShardCommitment { main_commit, permutation_commit, quotient_commit } = commitment;
93
94        challenger.observe(main_commit.clone());
95
96        let local_permutation_challenges =
97            (0..2).map(|_| challenger.sample_ext_element::<SC::Challenge>()).collect::<Vec<_>>();
98
99        challenger.observe(permutation_commit.clone());
100        // Observe the cumulative sums and constrain any sum without a corresponding scope to be
101        // zero.
102        for (opening, chip) in opened_values.chips.iter().zip_eq(chips.iter()) {
103            let local_sum = opening.local_cumulative_sum;
104            let global_sum = opening.global_cumulative_sum;
105
106            challenger.observe_slice(local_sum.as_base_slice());
107            challenger.observe_slice(&global_sum.0.x.0);
108            challenger.observe_slice(&global_sum.0.y.0);
109
110            if chip.commit_scope() == InteractionScope::Local && !global_sum.is_zero() {
111                return Err(VerificationError::CumulativeSumsError(
112                    "global cumulative sum is non-zero, but chip is Local",
113                ));
114            }
115
116            let has_local_interactions = chip
117                .sends()
118                .iter()
119                .chain(chip.receives())
120                .any(|i| i.scope == InteractionScope::Local);
121            if !has_local_interactions && !local_sum.is_zero() {
122                return Err(VerificationError::CumulativeSumsError(
123                    "local cumulative sum is non-zero, but no local interactions",
124                ));
125            }
126        }
127
128        let alpha = challenger.sample_ext_element::<SC::Challenge>();
129
130        // Observe the quotient commitments.
131        challenger.observe(quotient_commit.clone());
132
133        let zeta = challenger.sample_ext_element::<SC::Challenge>();
134
135        let preprocessed_domains_points_and_opens = vk
136            .chip_information
137            .iter()
138            .map(|(name, domain, _)| {
139                let i = *chip_ordering.get(name).filter(|&&i| i < chips.len()).ok_or(
140                    VerificationError::PreprocessedChipIdMismatch(name.clone(), String::new()),
141                )?;
142                if name != &chips[i].name() {
143                    return Err(VerificationError::PreprocessedChipIdMismatch(
144                        name.clone(),
145                        chips[i].name(),
146                    ));
147                }
148                let values = opened_values.chips[i].preprocessed.clone();
149                if !chips[i].local_only() {
150                    Ok((
151                        *domain,
152                        vec![(zeta, values.local), (domain.next_point(zeta).unwrap(), values.next)],
153                    ))
154                } else {
155                    Ok((*domain, vec![(zeta, values.local)]))
156                }
157            })
158            .collect::<Result<Vec<_>, _>>()?;
159
160        let main_domains_points_and_opens = trace_domains
161            .iter()
162            .zip_eq(opened_values.chips.iter())
163            .zip_eq(chips.iter())
164            .map(|((domain, values), chip)| {
165                if !chip.local_only() {
166                    (
167                        *domain,
168                        vec![
169                            (zeta, values.main.local.clone()),
170                            (domain.next_point(zeta).unwrap(), values.main.next.clone()),
171                        ],
172                    )
173                } else {
174                    (*domain, vec![(zeta, values.main.local.clone())])
175                }
176            })
177            .collect::<Vec<_>>();
178
179        let perm_domains_points_and_opens = trace_domains
180            .iter()
181            .zip_eq(opened_values.chips.iter())
182            .map(|(domain, values)| {
183                (
184                    *domain,
185                    vec![
186                        (zeta, values.permutation.local.clone()),
187                        (domain.next_point(zeta).unwrap(), values.permutation.next.clone()),
188                    ],
189                )
190            })
191            .collect::<Vec<_>>();
192
193        let quotient_chunk_domains = trace_domains
194            .iter()
195            .zip_eq(log_degrees)
196            .zip_eq(log_quotient_degrees)
197            .map(|((domain, log_degree), log_quotient_degree)| {
198                let quotient_degree = 1 << log_quotient_degree;
199                let quotient_domain =
200                    domain.create_disjoint_domain(1 << (log_degree + log_quotient_degree));
201                quotient_domain.split_domains(quotient_degree)
202            })
203            .collect::<Vec<_>>();
204
205        let quotient_domains_points_and_opens = proof
206            .opened_values
207            .chips
208            .iter()
209            .zip_eq(quotient_chunk_domains.iter())
210            .flat_map(|(values, qc_domains)| {
211                values
212                    .quotient
213                    .iter()
214                    .zip_eq(qc_domains)
215                    .map(move |(values, q_domain)| (*q_domain, vec![(zeta, values.clone())]))
216            })
217            .collect::<Vec<_>>();
218
219        let rounds = vec![
220            (vk.commit.clone(), preprocessed_domains_points_and_opens),
221            (main_commit.clone(), main_domains_points_and_opens),
222            (permutation_commit.clone(), perm_domains_points_and_opens),
223            (quotient_commit.clone(), quotient_domains_points_and_opens),
224        ];
225
226        config
227            .pcs()
228            .verify(rounds, opening_proof, challenger)
229            .map_err(|e| VerificationError::InvalidopeningArgument(e))?;
230
231        let permutation_challenges = local_permutation_challenges;
232
233        // Verify the constrtaint evaluations.
234        for (chip, trace_domain, qc_domains, values) in
235            izip!(chips.iter(), trace_domains, quotient_chunk_domains, opened_values.chips.iter(),)
236        {
237            // Verify the shape of the opening arguments matches the expected values.
238            Self::verify_opening_shape(chip, values)
239                .map_err(|e| VerificationError::OpeningShapeError(chip.name(), e))?;
240            // Verify the constraint evaluation.
241            Self::verify_constraints(
242                chip,
243                values,
244                trace_domain,
245                qc_domains,
246                zeta,
247                alpha,
248                &permutation_challenges,
249                public_values,
250            )
251            .map_err(|_| VerificationError::OodEvaluationMismatch(chip.name()))?;
252        }
253        // Verify that the local cumulative sum is zero.
254        let local_cumulative_sum = proof.local_cumulative_sum();
255        if local_cumulative_sum != SC::Challenge::zero() {
256            return Err(VerificationError::CumulativeSumsError("local cumulative sum is not zero"));
257        }
258
259        Ok(())
260    }
261
262    fn verify_opening_shape(
263        chip: &MachineChip<SC, A>,
264        opening: &ChipOpenedValues<Val<SC>, SC::Challenge>,
265    ) -> Result<(), OpeningShapeError> {
266        // Verify that the preprocessed width matches the expected value for the chip.
267        if opening.preprocessed.local.len() != chip.preprocessed_width() {
268            return Err(OpeningShapeError::PreprocessedWidthMismatch(
269                chip.preprocessed_width(),
270                opening.preprocessed.local.len(),
271            ));
272        }
273        if opening.preprocessed.next.len() != chip.preprocessed_width() {
274            return Err(OpeningShapeError::PreprocessedWidthMismatch(
275                chip.preprocessed_width(),
276                opening.preprocessed.next.len(),
277            ));
278        }
279
280        // Verify that the main width matches the expected value for the chip.
281        if opening.main.local.len() != chip.width() {
282            return Err(OpeningShapeError::MainWidthMismatch(
283                chip.width(),
284                opening.main.local.len(),
285            ));
286        }
287        if opening.main.next.len() != chip.width() {
288            return Err(OpeningShapeError::MainWidthMismatch(chip.width(), opening.main.next.len()));
289        }
290
291        // Verify that the permutation width matches the expected value for the chip.
292        if opening.permutation.local.len() != chip.permutation_width() * SC::Challenge::D {
293            return Err(OpeningShapeError::PermutationWidthMismatch(
294                chip.permutation_width(),
295                opening.permutation.local.len(),
296            ));
297        }
298        if opening.permutation.next.len() != chip.permutation_width() * SC::Challenge::D {
299            return Err(OpeningShapeError::PermutationWidthMismatch(
300                chip.permutation_width(),
301                opening.permutation.next.len(),
302            ));
303        }
304        // Verift that the number of quotient chunks matches the expected value for the chip.
305        if opening.quotient.len() != chip.quotient_width() {
306            return Err(OpeningShapeError::QuotientWidthMismatch(
307                chip.quotient_width(),
308                opening.quotient.len(),
309            ));
310        }
311        // For each quotient chunk, verify that the number of elements is equal to the degree of the
312        // challenge extension field over the value field.
313        for slice in &opening.quotient {
314            if slice.len() != SC::Challenge::D {
315                return Err(OpeningShapeError::QuotientChunkSizeMismatch(
316                    SC::Challenge::D,
317                    slice.len(),
318                ));
319            }
320        }
321
322        Ok(())
323    }
324
325    #[allow(clippy::too_many_arguments)]
326    #[allow(clippy::needless_pass_by_value)]
327    fn verify_constraints(
328        chip: &MachineChip<SC, A>,
329        opening: &ChipOpenedValues<Val<SC>, SC::Challenge>,
330        trace_domain: Domain<SC>,
331        qc_domains: Vec<Domain<SC>>,
332        zeta: SC::Challenge,
333        alpha: SC::Challenge,
334        permutation_challenges: &[SC::Challenge],
335        public_values: &[Val<SC>],
336    ) -> Result<(), OodEvaluationMismatch>
337    where
338        A: for<'a> Air<VerifierConstraintFolder<'a, SC>>,
339    {
340        let sels = trace_domain.selectors_at_point(zeta);
341
342        // Recompute the quotient at zeta from the chunks.
343        let quotient = Self::recompute_quotient(opening, &qc_domains, zeta);
344        // Calculate the evaluations of the constraints at zeta.
345        let folded_constraints = Self::eval_constraints(
346            chip,
347            opening,
348            &sels,
349            alpha,
350            permutation_challenges,
351            public_values,
352        );
353
354        // Check that the constraints match the quotient, i.e.
355        //     folded_constraints(zeta) / Z_H(zeta) = quotient(zeta)
356        if folded_constraints * sels.inv_zeroifier == quotient {
357            Ok(())
358        } else {
359            Err(OodEvaluationMismatch)
360        }
361    }
362
363    /// Evaluates the constraints for a chip and opening.
364    pub fn eval_constraints(
365        chip: &MachineChip<SC, A>,
366        opening: &ChipOpenedValues<Val<SC>, SC::Challenge>,
367        selectors: &LagrangeSelectors<SC::Challenge>,
368        alpha: SC::Challenge,
369        permutation_challenges: &[SC::Challenge],
370        public_values: &[Val<SC>],
371    ) -> SC::Challenge
372    where
373        A: for<'a> Air<VerifierConstraintFolder<'a, SC>>,
374    {
375        // Reconstruct the permutation opening values as extension elements.
376        // SAFETY: The opening shapes are already verified.
377        let unflatten = |v: &[SC::Challenge]| {
378            v.chunks_exact(SC::Challenge::D)
379                .map(|chunk| {
380                    chunk.iter().enumerate().map(|(e_i, &x)| SC::Challenge::monomial(e_i) * x).sum()
381                })
382                .collect::<Vec<SC::Challenge>>()
383        };
384
385        let perm_opening = AirOpenedValues {
386            local: unflatten(&opening.permutation.local),
387            next: unflatten(&opening.permutation.next),
388        };
389
390        let mut folder = VerifierConstraintFolder::<SC> {
391            preprocessed: opening.preprocessed.view(),
392            main: opening.main.view(),
393            perm: perm_opening.view(),
394            perm_challenges: permutation_challenges,
395            local_cumulative_sum: &opening.local_cumulative_sum,
396            global_cumulative_sum: &opening.global_cumulative_sum,
397            is_first_row: selectors.is_first_row,
398            is_last_row: selectors.is_last_row,
399            is_transition: selectors.is_transition,
400            alpha,
401            accumulator: SC::Challenge::zero(),
402            public_values,
403            _marker: PhantomData,
404        };
405
406        chip.eval(&mut folder);
407
408        folder.accumulator
409    }
410
411    /// Recomputes the quotient for a chip and opening.
412    pub fn recompute_quotient(
413        opening: &ChipOpenedValues<Val<SC>, SC::Challenge>,
414        qc_domains: &[Domain<SC>],
415        zeta: SC::Challenge,
416    ) -> SC::Challenge {
417        use p3_field::Field;
418
419        let zps = qc_domains
420            .iter()
421            .enumerate()
422            .map(|(i, domain)| {
423                qc_domains
424                    .iter()
425                    .enumerate()
426                    .filter(|(j, _)| *j != i)
427                    .map(|(_, other_domain)| {
428                        other_domain.zp_at_point(zeta) *
429                            other_domain.zp_at_point(domain.first_point()).inverse()
430                    })
431                    .product::<SC::Challenge>()
432            })
433            .collect_vec();
434
435        opening
436            .quotient
437            .iter()
438            .enumerate()
439            .map(|(ch_i, ch)| {
440                // SAFETY: The opening shapes are already verified.
441                assert_eq!(ch.len(), SC::Challenge::D);
442                ch.iter()
443                    .enumerate()
444                    .map(|(e_i, &c)| zps[ch_i] * SC::Challenge::monomial(e_i) * c)
445                    .sum::<SC::Challenge>()
446            })
447            .sum::<SC::Challenge>()
448    }
449}
450
451/// An error that occurs when the openings do not match the expected shape.
452pub struct OodEvaluationMismatch;
453
454/// An error that occurs when the shape of the openings does not match the expected shape.
455pub enum OpeningShapeError {
456    /// The width of the preprocessed trace does not match the expected width.
457    PreprocessedWidthMismatch(usize, usize),
458    /// The width of the main trace does not match the expected width.
459    MainWidthMismatch(usize, usize),
460    /// The width of the permutation trace does not match the expected width.
461    PermutationWidthMismatch(usize, usize),
462    /// The width of the quotient trace does not match the expected width.
463    QuotientWidthMismatch(usize, usize),
464    /// The chunk size of the quotient trace does not match the expected chunk size.
465    QuotientChunkSizeMismatch(usize, usize),
466}
467
468/// An error that occurs during the verification.
469pub enum VerificationError<SC: StarkGenericConfig> {
470    /// opening proof is invalid.
471    InvalidopeningArgument(OpeningError<SC>),
472    /// Out-of-domain evaluation mismatch.
473    ///
474    /// `constraints(zeta)` did not match `quotient(zeta) Z_H(zeta)`.
475    OodEvaluationMismatch(String),
476    /// The shape of the opening arguments is invalid.
477    OpeningShapeError(String, OpeningShapeError),
478    /// The cpu chip is missing.
479    MissingCpuChip,
480    /// The length of the chip opening does not match the expected length.
481    ChipOpeningLengthMismatch,
482    /// The preprocessed chip id does not match the claimed opening id.
483    PreprocessedChipIdMismatch(String, String),
484    /// Cumulative sums error
485    CumulativeSumsError(&'static str),
486    /// The log degree of a chip is invalid.
487    InvalidLogDegree(String, usize),
488    /// The lookup multiplicity can overflow.
489    LookupMultiplicityOverflow,
490}
491
492impl Debug for OpeningShapeError {
493    #[allow(clippy::uninlined_format_args)]
494    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
495        match self {
496            OpeningShapeError::PreprocessedWidthMismatch(expected, actual) => {
497                write!(f, "Preprocessed width mismatch: expected {}, got {}", expected, actual)
498            }
499            OpeningShapeError::MainWidthMismatch(expected, actual) => {
500                write!(f, "Main width mismatch: expected {}, got {}", expected, actual)
501            }
502            OpeningShapeError::PermutationWidthMismatch(expected, actual) => {
503                write!(f, "Permutation width mismatch: expected {}, got {}", expected, actual)
504            }
505            OpeningShapeError::QuotientWidthMismatch(expected, actual) => {
506                write!(f, "Quotient width mismatch: expected {}, got {}", expected, actual)
507            }
508            OpeningShapeError::QuotientChunkSizeMismatch(expected, actual) => {
509                write!(f, "Quotient chunk size mismatch: expected {}, got {}", expected, actual)
510            }
511        }
512    }
513}
514
515impl Display for OpeningShapeError {
516    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
517        write!(f, "{self:?}")
518    }
519}
520
521impl<SC: StarkGenericConfig> Debug for VerificationError<SC> {
522    #[allow(clippy::uninlined_format_args)]
523    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
524        match self {
525            VerificationError::InvalidopeningArgument(e) => {
526                write!(f, "Invalid opening argument: {:?}", e)
527            }
528            VerificationError::OodEvaluationMismatch(chip) => {
529                write!(f, "Out-of-domain evaluation mismatch on chip {}", chip)
530            }
531            VerificationError::OpeningShapeError(chip, e) => {
532                write!(f, "Invalid opening shape for chip {}: {:?}", chip, e)
533            }
534            VerificationError::MissingCpuChip => {
535                write!(f, "Missing CPU chip")
536            }
537            VerificationError::ChipOpeningLengthMismatch => {
538                write!(f, "Chip opening length mismatch")
539            }
540            VerificationError::PreprocessedChipIdMismatch(expected, actual) => {
541                write!(f, "Preprocessed chip id mismatch: expected {}, got {}", expected, actual)
542            }
543            VerificationError::CumulativeSumsError(s) => write!(f, "cumulative sums error: {}", s),
544            VerificationError::InvalidLogDegree(chip, log_degree) => {
545                write!(f, "Invalid log degree for chip {}: got {}", chip, log_degree)
546            }
547            VerificationError::LookupMultiplicityOverflow => {
548                write!(f, "Lookup multiplicity overflow")
549            }
550        }
551    }
552}
553
554impl<SC: StarkGenericConfig> Display for VerificationError<SC> {
555    #[allow(clippy::uninlined_format_args)]
556    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
557        match self {
558            VerificationError::InvalidopeningArgument(_) => {
559                write!(f, "Invalid opening argument")
560            }
561            VerificationError::OodEvaluationMismatch(chip) => {
562                write!(f, "Out-of-domain evaluation mismatch on chip {}", chip)
563            }
564            VerificationError::OpeningShapeError(chip, e) => {
565                write!(f, "Invalid opening shape for chip {}: {}", chip, e)
566            }
567            VerificationError::MissingCpuChip => {
568                write!(f, "Missing CPU chip in shard")
569            }
570            VerificationError::ChipOpeningLengthMismatch => {
571                write!(f, "Chip opening length mismatch")
572            }
573            VerificationError::CumulativeSumsError(s) => write!(f, "cumulative sums error: {}", s),
574            VerificationError::PreprocessedChipIdMismatch(expected, actual) => {
575                write!(f, "Preprocessed chip id mismatch: expected {}, got {}", expected, actual)
576            }
577            VerificationError::InvalidLogDegree(chip, log_degree) => {
578                write!(f, "Invalid log degree for chip {}: got {}", chip, log_degree)
579            }
580            VerificationError::LookupMultiplicityOverflow => {
581                write!(f, "Lookup multiplicity overflow")
582            }
583        }
584    }
585}
586
587impl<SC: StarkGenericConfig> std::error::Error for VerificationError<SC> {}