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