openvm_stark_backend/air_builders/symbolic/
mod.rs

1// Originally copied from uni-stark/src/symbolic_builder.rs to allow A: ?Sized
2
3use itertools::Itertools;
4use p3_air::{
5    AirBuilder, AirBuilderWithPublicValues, ExtensionBuilder, PairBuilder, PermutationAirBuilder,
6};
7use p3_field::Field;
8use p3_matrix::{dense::RowMajorMatrix, Matrix};
9use p3_util::log2_ceil_usize;
10use tracing::instrument;
11
12use self::{
13    symbolic_expression::SymbolicExpression,
14    symbolic_variable::{Entry, SymbolicVariable},
15};
16use super::PartitionedAirBuilder;
17use crate::{
18    interaction::{
19        fri_log_up::find_interaction_chunks, rap::InteractionPhaseAirBuilder, Interaction,
20        InteractionBuilder, RapPhaseSeqKind, SymbolicInteraction,
21    },
22    keygen::types::{StarkVerifyingParams, TraceWidth},
23    rap::{BaseAirWithPublicValues, PermutationAirBuilderWithExposedValues, Rap},
24};
25
26mod dag;
27pub mod symbolic_expression;
28pub mod symbolic_variable;
29
30pub use dag::*;
31
32use crate::interaction::BusIndex;
33
34/// Symbolic constraints for a single AIR with interactions.
35/// The constraints contain the constraints on the logup partial sums.
36#[derive(Clone, Debug)]
37pub struct SymbolicConstraints<F> {
38    /// All constraints of the RAP, including the constraints on the logup partial sums.
39    pub constraints: Vec<SymbolicExpression<F>>,
40    /// Symbolic representation of chip interactions. This is used by
41    /// the prover for after challenge trace generation, and some partial
42    /// information may be used by the verifier.
43    ///
44    /// **However**, any contributions to the quotient polynomial from
45    /// logup are already included in `constraints` and do not need to
46    /// be separately calculated from `interactions`.
47    pub interactions: Vec<SymbolicInteraction<F>>,
48}
49
50impl<F: Field> SymbolicConstraints<F> {
51    pub fn max_constraint_degree(&self) -> usize {
52        Iterator::max(self.constraints.iter().map(|c| c.degree_multiple())).unwrap_or(0)
53    }
54
55    pub fn get_log_quotient_degree(&self) -> usize {
56        // We pad to at least degree 2, since a quotient argument doesn't make sense with smaller degrees.
57        let constraint_degree = self.max_constraint_degree().max(2);
58
59        // The quotient's actual degree is approximately (max_constraint_degree - 1) * (trace height),
60        // where subtracting 1 comes from division by the zerofier.
61        // But we pad it to a power of two so that we can efficiently decompose the quotient.
62        log2_ceil_usize(constraint_degree - 1)
63    }
64
65    /// Returns the maximum field degree and count degree across all interactions
66    pub fn max_interaction_degrees(&self) -> (usize, usize) {
67        let max_field_degree = self
68            .interactions
69            .iter()
70            .map(|interaction| {
71                interaction
72                    .message
73                    .iter()
74                    .map(|field| field.degree_multiple())
75                    .max()
76                    .unwrap_or(0)
77            })
78            .max()
79            .unwrap_or(0);
80
81        let max_count_degree = self
82            .interactions
83            .iter()
84            .map(|interaction| interaction.count.degree_multiple())
85            .max()
86            .unwrap_or(0);
87
88        (max_field_degree, max_count_degree)
89    }
90}
91
92#[instrument(name = "evaluate constraints symbolically", skip_all, level = "debug")]
93pub fn get_symbolic_builder<F, R>(
94    rap: &R,
95    width: &TraceWidth,
96    num_challenges_to_sample: &[usize],
97    num_exposed_values_after_challenge: &[usize],
98    rap_phase_seq_kind: RapPhaseSeqKind,
99    max_constraint_degree: usize,
100) -> SymbolicRapBuilder<F>
101where
102    F: Field,
103    R: Rap<SymbolicRapBuilder<F>> + BaseAirWithPublicValues<F> + ?Sized,
104{
105    let mut builder = SymbolicRapBuilder::new(
106        width,
107        rap.num_public_values(),
108        num_challenges_to_sample,
109        num_exposed_values_after_challenge,
110        rap_phase_seq_kind,
111        max_constraint_degree,
112    );
113    Rap::eval(rap, &mut builder);
114    builder
115}
116
117/// An `AirBuilder` for evaluating constraints symbolically, and recording them for later use.
118#[derive(Debug)]
119pub struct SymbolicRapBuilder<F> {
120    preprocessed: RowMajorMatrix<SymbolicVariable<F>>,
121    partitioned_main: Vec<RowMajorMatrix<SymbolicVariable<F>>>,
122    after_challenge: Vec<RowMajorMatrix<SymbolicVariable<F>>>,
123    public_values: Vec<SymbolicVariable<F>>,
124    challenges: Vec<Vec<SymbolicVariable<F>>>,
125    exposed_values_after_challenge: Vec<Vec<SymbolicVariable<F>>>,
126    constraints: Vec<SymbolicExpression<F>>,
127    interactions: Vec<SymbolicInteraction<F>>,
128    max_constraint_degree: usize,
129    rap_phase_seq_kind: RapPhaseSeqKind,
130    trace_width: TraceWidth,
131
132    /// Caching for FRI logup to avoid recomputation during keygen
133    interaction_partitions: Option<Vec<Vec<usize>>>,
134}
135
136impl<F: Field> SymbolicRapBuilder<F> {
137    /// - `num_challenges_to_sample`: for each challenge phase, how many challenges to sample
138    /// - `num_exposed_values_after_challenge`: in each challenge phase, how many values to expose to verifier
139    pub(crate) fn new(
140        width: &TraceWidth,
141        num_public_values: usize,
142        num_challenges_to_sample: &[usize],
143        num_exposed_values_after_challenge: &[usize],
144        rap_phase_seq_kind: RapPhaseSeqKind,
145        max_constraint_degree: usize,
146    ) -> Self {
147        let preprocessed_width = width.preprocessed.unwrap_or(0);
148        let prep_values = [0, 1]
149            .into_iter()
150            .flat_map(|offset| {
151                (0..width.preprocessed.unwrap_or(0))
152                    .map(move |index| SymbolicVariable::new(Entry::Preprocessed { offset }, index))
153            })
154            .collect();
155        let preprocessed = RowMajorMatrix::new(prep_values, preprocessed_width);
156
157        let mut partitioned_main: Vec<_> = width
158            .cached_mains
159            .iter()
160            .enumerate()
161            .map(|(part_index, &width)| gen_main_trace(part_index, width))
162            .collect();
163        if width.common_main != 0 {
164            partitioned_main.push(gen_main_trace(width.cached_mains.len(), width.common_main));
165        }
166        let after_challenge = Self::new_after_challenge(&width.after_challenge);
167
168        let public_values = (0..num_public_values)
169            .map(move |index| SymbolicVariable::new(Entry::Public, index))
170            .collect();
171
172        let challenges = Self::new_challenges(num_challenges_to_sample);
173
174        let exposed_values_after_challenge =
175            Self::new_exposed_values_after_challenge(num_exposed_values_after_challenge);
176
177        Self {
178            preprocessed,
179            partitioned_main,
180            after_challenge,
181            public_values,
182            challenges,
183            exposed_values_after_challenge,
184            constraints: vec![],
185            interactions: vec![],
186            max_constraint_degree,
187            rap_phase_seq_kind,
188            trace_width: width.clone(),
189            interaction_partitions: None,
190        }
191    }
192
193    pub fn constraints(self) -> SymbolicConstraints<F> {
194        SymbolicConstraints {
195            constraints: self.constraints,
196            interactions: self.interactions,
197        }
198    }
199
200    pub fn params(&self) -> StarkVerifyingParams {
201        let width = self.width();
202        let num_exposed_values_after_challenge = self.num_exposed_values_after_challenge();
203        let num_challenges_to_sample = self.num_challenges_to_sample();
204        StarkVerifyingParams {
205            width,
206            num_public_values: self.public_values.len(),
207            num_exposed_values_after_challenge,
208            num_challenges_to_sample,
209        }
210    }
211
212    pub fn width(&self) -> TraceWidth {
213        let mut ret = self.trace_width.clone();
214        ret.after_challenge = self.after_challenge.iter().map(|m| m.width()).collect();
215        ret
216    }
217
218    pub fn num_exposed_values_after_challenge(&self) -> Vec<usize> {
219        self.exposed_values_after_challenge
220            .iter()
221            .map(|c| c.len())
222            .collect()
223    }
224
225    pub fn num_challenges_to_sample(&self) -> Vec<usize> {
226        self.challenges.iter().map(|c| c.len()).collect()
227    }
228
229    fn new_after_challenge(
230        width_after_phase: &[usize],
231    ) -> Vec<RowMajorMatrix<SymbolicVariable<F>>> {
232        width_after_phase
233            .iter()
234            .map(|&width| {
235                let mat_values = [0, 1]
236                    .into_iter()
237                    .flat_map(|offset| {
238                        (0..width).map(move |index| {
239                            SymbolicVariable::new(Entry::Permutation { offset }, index)
240                        })
241                    })
242                    .collect_vec();
243                RowMajorMatrix::new(mat_values, width)
244            })
245            .collect_vec()
246    }
247
248    fn new_challenges(num_challenges_to_sample: &[usize]) -> Vec<Vec<SymbolicVariable<F>>> {
249        num_challenges_to_sample
250            .iter()
251            .map(|&num_challenges| {
252                (0..num_challenges)
253                    .map(|index| SymbolicVariable::new(Entry::Challenge, index))
254                    .collect_vec()
255            })
256            .collect_vec()
257    }
258
259    fn new_exposed_values_after_challenge(
260        num_exposed_values_after_challenge: &[usize],
261    ) -> Vec<Vec<SymbolicVariable<F>>> {
262        num_exposed_values_after_challenge
263            .iter()
264            .map(|&num| {
265                (0..num)
266                    .map(|index| SymbolicVariable::new(Entry::Exposed, index))
267                    .collect_vec()
268            })
269            .collect_vec()
270    }
271}
272
273impl<F: Field> AirBuilder for SymbolicRapBuilder<F> {
274    type F = F;
275    type Expr = SymbolicExpression<Self::F>;
276    type Var = SymbolicVariable<Self::F>;
277    type M = RowMajorMatrix<Self::Var>;
278
279    /// It is difficult to horizontally concatenate matrices when the main trace is partitioned, so we disable this method in that case.
280    fn main(&self) -> Self::M {
281        if self.partitioned_main.len() == 1 {
282            self.partitioned_main[0].clone()
283        } else {
284            panic!("Main trace is either empty or partitioned. This function should not be used.")
285        }
286    }
287
288    fn is_first_row(&self) -> Self::Expr {
289        SymbolicExpression::IsFirstRow
290    }
291
292    fn is_last_row(&self) -> Self::Expr {
293        SymbolicExpression::IsLastRow
294    }
295
296    fn is_transition_window(&self, size: usize) -> Self::Expr {
297        if size == 2 {
298            SymbolicExpression::IsTransition
299        } else {
300            panic!("uni-stark only supports a window size of 2")
301        }
302    }
303
304    fn assert_zero<I: Into<Self::Expr>>(&mut self, x: I) {
305        self.constraints.push(x.into());
306    }
307}
308
309impl<F: Field> PairBuilder for SymbolicRapBuilder<F> {
310    fn preprocessed(&self) -> Self::M {
311        self.preprocessed.clone()
312    }
313}
314
315impl<F: Field> ExtensionBuilder for SymbolicRapBuilder<F> {
316    type EF = F;
317    type ExprEF = SymbolicExpression<F>;
318    type VarEF = SymbolicVariable<F>;
319
320    fn assert_zero_ext<I>(&mut self, x: I)
321    where
322        I: Into<Self::ExprEF>,
323    {
324        self.constraints.push(x.into());
325    }
326}
327
328impl<F: Field> AirBuilderWithPublicValues for SymbolicRapBuilder<F> {
329    type PublicVar = SymbolicVariable<F>;
330
331    fn public_values(&self) -> &[Self::PublicVar] {
332        &self.public_values
333    }
334}
335
336impl<F: Field> PermutationAirBuilder for SymbolicRapBuilder<F> {
337    type MP = RowMajorMatrix<Self::VarEF>;
338    type RandomVar = SymbolicVariable<F>;
339
340    fn permutation(&self) -> Self::MP {
341        self.after_challenge
342            .first()
343            .expect("Challenge phase not supported")
344            .clone()
345    }
346
347    fn permutation_randomness(&self) -> &[Self::RandomVar] {
348        self.challenges
349            .first()
350            .map(|c| c.as_slice())
351            .expect("Challenge phase not supported")
352    }
353}
354
355impl<F: Field> PermutationAirBuilderWithExposedValues for SymbolicRapBuilder<F> {
356    fn permutation_exposed_values(&self) -> &[Self::VarEF] {
357        self.exposed_values_after_challenge
358            .first()
359            .map(|c| c.as_slice())
360            .expect("Challenge phase not supported")
361    }
362}
363
364impl<F: Field> InteractionBuilder for SymbolicRapBuilder<F> {
365    fn push_interaction<E: Into<Self::Expr>>(
366        &mut self,
367        bus_index: BusIndex,
368        fields: impl IntoIterator<Item = E>,
369        count: impl Into<Self::Expr>,
370        count_weight: u32,
371    ) {
372        let fields = fields.into_iter().map(|f| f.into()).collect();
373        let count = count.into();
374        self.interactions.push(Interaction {
375            bus_index,
376            message: fields,
377            count,
378            count_weight,
379        });
380    }
381
382    fn num_interactions(&self) -> usize {
383        self.interactions.len()
384    }
385
386    fn all_interactions(&self) -> &[Interaction<Self::Expr>] {
387        &self.interactions
388    }
389}
390
391impl<F: Field> InteractionPhaseAirBuilder for SymbolicRapBuilder<F> {
392    fn finalize_interactions(&mut self) {
393        let num_interactions = self.num_interactions();
394        if num_interactions != 0 {
395            assert!(
396                self.after_challenge.is_empty(),
397                "after_challenge width should be auto-populated by the InteractionBuilder"
398            );
399            assert!(self.challenges.is_empty());
400            assert!(self.exposed_values_after_challenge.is_empty());
401
402            if self.rap_phase_seq_kind == RapPhaseSeqKind::FriLogUp {
403                let interaction_partitions =
404                    find_interaction_chunks(&self.interactions, self.max_constraint_degree)
405                        .interaction_partitions();
406                let num_chunks = interaction_partitions.len();
407                self.interaction_partitions.replace(interaction_partitions);
408                let perm_width = num_chunks + 1;
409                self.after_challenge = Self::new_after_challenge(&[perm_width]);
410            }
411
412            let phases_shapes = self.rap_phase_seq_kind.shape();
413            let phase_shape = phases_shapes.first().unwrap();
414
415            self.challenges = Self::new_challenges(&[phase_shape.num_challenges]);
416            self.exposed_values_after_challenge =
417                Self::new_exposed_values_after_challenge(&[phase_shape.num_exposed_values]);
418        }
419    }
420
421    fn max_constraint_degree(&self) -> usize {
422        self.max_constraint_degree
423    }
424
425    fn rap_phase_seq_kind(&self) -> RapPhaseSeqKind {
426        self.rap_phase_seq_kind
427    }
428
429    fn symbolic_interactions(&self) -> Vec<SymbolicInteraction<F>> {
430        self.interactions.clone()
431    }
432}
433
434impl<F: Field> PartitionedAirBuilder for SymbolicRapBuilder<F> {
435    fn cached_mains(&self) -> &[Self::M] {
436        &self.partitioned_main[..self.trace_width.cached_mains.len()]
437    }
438    fn common_main(&self) -> &Self::M {
439        assert_ne!(
440            self.trace_width.common_main, 0,
441            "AIR doesn't have a common main trace"
442        );
443        &self.partitioned_main[self.trace_width.cached_mains.len()]
444    }
445}
446
447#[allow(dead_code)]
448struct LocalOnlyChecker;
449
450#[allow(dead_code)]
451impl LocalOnlyChecker {
452    fn check_var<F: Field>(var: SymbolicVariable<F>) -> bool {
453        match var.entry {
454            Entry::Preprocessed { offset } => offset == 0,
455            Entry::Main { offset, .. } => offset == 0,
456            Entry::Permutation { offset } => offset == 0,
457            Entry::Public => true,
458            Entry::Challenge => true,
459            Entry::Exposed => true,
460        }
461    }
462
463    fn check_expr<F: Field>(expr: &SymbolicExpression<F>) -> bool {
464        match expr {
465            SymbolicExpression::Variable(var) => Self::check_var(*var),
466            SymbolicExpression::IsFirstRow => false,
467            SymbolicExpression::IsLastRow => false,
468            SymbolicExpression::IsTransition => false,
469            SymbolicExpression::Constant(_) => true,
470            SymbolicExpression::Add { x, y, .. } => Self::check_expr(x) && Self::check_expr(y),
471            SymbolicExpression::Sub { x, y, .. } => Self::check_expr(x) && Self::check_expr(y),
472            SymbolicExpression::Neg { x, .. } => Self::check_expr(x),
473            SymbolicExpression::Mul { x, y, .. } => Self::check_expr(x) && Self::check_expr(y),
474        }
475    }
476}
477
478fn gen_main_trace<F: Field>(
479    part_index: usize,
480    width: usize,
481) -> RowMajorMatrix<SymbolicVariable<F>> {
482    let mat_values = [0, 1]
483        .into_iter()
484        .flat_map(|offset| {
485            (0..width)
486                .map(move |index| SymbolicVariable::new(Entry::Main { part_index, offset }, index))
487        })
488        .collect_vec();
489    RowMajorMatrix::new(mat_values, width)
490}