Skip to main content

p3_batch_stark/
prover.rs

1use alloc::vec;
2use alloc::vec::Vec;
3
4#[cfg(debug_assertions)]
5use p3_air::DebugConstraintBuilder;
6use p3_air::symbolic::{AirLayout, SymbolicAirBuilder, SymbolicExpressionExt};
7use p3_air::{Air, RowWindow};
8use p3_challenger::{CanObserve, FieldChallenger};
9use p3_commit::{Pcs, PolynomialSpace};
10use p3_field::{
11    Algebra, BasedVectorSpace, PackedFieldExtension, PackedValue, PrimeCharacteristicRing,
12};
13use p3_lookup::AirWithLookups;
14use p3_lookup::folder::ProverConstraintFolderWithLookups;
15use p3_lookup::logup::LogUpGadget;
16use p3_lookup::lookup_traits::{Kind, Lookup, LookupData, LookupGadget};
17use p3_matrix::Matrix;
18use p3_matrix::dense::{RowMajorMatrix, RowMajorMatrixView};
19use p3_maybe_rayon::prelude::*;
20use p3_uni_stark::{OpenedValues, PackedChallenge, PackedVal, ProverConstraintFolder};
21use p3_util::log2_strict_usize;
22use tracing::{debug_span, info_span, instrument};
23
24use crate::common::{CommonData, ProverData, get_perm_challenges};
25use crate::config::{Challenge, Domain, StarkGenericConfig as SGC, Val, observe_instance_binding};
26use crate::proof::{BatchCommitments, BatchOpenedValues, BatchProof, OpenedValuesWithLookups};
27use crate::symbolic::{
28    get_constraint_layout, get_log_num_quotient_chunks, get_symbolic_constraints,
29};
30
31#[derive(Debug)]
32pub struct StarkInstance<'a, SC: SGC, A> {
33    pub air: &'a A,
34    pub trace: &'a RowMajorMatrix<Val<SC>>,
35    pub public_values: Vec<Val<SC>>,
36    pub lookups: Vec<Lookup<Val<SC>>>,
37}
38
39impl<'a, SC: SGC, A> StarkInstance<'a, SC, A> {
40    pub fn new_multiple(
41        airs: &'a [A],
42        traces: &'a [&'a RowMajorMatrix<Val<SC>>],
43        public_values: &[Vec<Val<SC>>],
44        common_data: &CommonData<SC>,
45    ) -> Vec<Self> {
46        airs.iter()
47            .zip(traces.iter())
48            .zip(public_values.iter())
49            .zip(common_data.lookups.iter())
50            .map(|(((air, trace), public_values), lookups)| Self {
51                air,
52                trace,
53                public_values: public_values.clone(),
54                lookups: lookups.clone(),
55            })
56            .collect()
57    }
58}
59
60#[instrument(skip_all)]
61pub fn prove_batch<
62    SC,
63    #[cfg(debug_assertions)] A: for<'a> Air<DebugConstraintBuilder<'a, Val<SC>, SC::Challenge>>
64        + Air<SymbolicAirBuilder<Val<SC>, SC::Challenge>>
65        + for<'a> Air<ProverConstraintFolderWithLookups<'a, SC>>
66        + Clone,
67    #[cfg(not(debug_assertions))] A: for<'a> Air<SymbolicAirBuilder<Val<SC>, SC::Challenge>>
68        + for<'a> Air<ProverConstraintFolderWithLookups<'a, SC>>
69        + Clone,
70>(
71    config: &SC,
72    instances: &[StarkInstance<'_, SC, A>],
73    prover_data: &ProverData<SC>,
74) -> BatchProof<SC>
75where
76    SC: SGC,
77    SymbolicExpressionExt<Val<SC>, SC::Challenge>: Algebra<SC::Challenge>,
78{
79    let common = &prover_data.common;
80    // TODO: Extend if additional lookup gadgets are added.
81    let lookup_gadget = LogUpGadget::new();
82
83    let pcs = config.pcs();
84    let mut challenger = config.initialise_challenger();
85
86    // Use instances in provided order.
87    let degrees: Vec<usize> = instances.iter().map(|i| i.trace.height()).collect();
88    let log_degrees: Vec<usize> = degrees.iter().copied().map(log2_strict_usize).collect();
89    let log_ext_degrees: Vec<usize> = log_degrees.iter().map(|&d| d + config.is_zk()).collect();
90
91    // Extract lookups and create lookup data in one pass.
92    let (all_lookups, mut lookup_data): (Vec<Vec<_>>, Vec<Vec<_>>) = instances
93        .iter()
94        .map(|inst| {
95            (
96                inst.lookups.clone(),
97                // We only get `LookupData` for global lookups, since we only need it for the expected cumulated value.
98                inst.lookups
99                    .iter()
100                    .filter_map(|lookup| match &lookup.kind {
101                        Kind::Global(name) => Some(LookupData {
102                            name: name.clone(),
103                            aux_idx: lookup.columns[0],
104                            expected_cumulated: SC::Challenge::ZERO,
105                        }),
106                        _ => None,
107                    })
108                    .collect::<Vec<_>>(),
109            )
110        })
111        .unzip();
112
113    // Domains for each instance (base and extended) in one pass.
114    let (trace_domains, ext_trace_domains): (Vec<Domain<SC>>, Vec<Domain<SC>>) = degrees
115        .iter()
116        .map(|&deg| {
117            (
118                pcs.natural_domain_for_degree(deg),
119                pcs.natural_domain_for_degree(deg * (config.is_zk() + 1)),
120            )
121        })
122        .unzip();
123
124    // Extract AIRs and public values; consume traces later without cloning.
125    let airs: Vec<&A> = instances.iter().map(|i| i.air).collect();
126    let pub_vals: Vec<Vec<Val<SC>>> = instances.iter().map(|i| i.public_values.clone()).collect();
127
128    let mut preprocessed_widths = Vec::with_capacity(airs.len());
129    let (log_num_quotient_chunks, num_quotient_chunks): (Vec<usize>, Vec<usize>) = airs
130        .iter()
131        .zip(pub_vals.iter())
132        .enumerate()
133        .map(|(i, (air, _pv))| {
134            let pre_w = common
135                .preprocessed
136                .as_ref()
137                .and_then(|g| g.instances[i].as_ref().map(|m| m.width))
138                .unwrap_or(0);
139            preprocessed_widths.push(pre_w);
140            let layout = AirLayout {
141                preprocessed_width: pre_w,
142                main_width: air.width(),
143                num_public_values: air.num_public_values(),
144                ..Default::default()
145            };
146            let lq_chunks =
147                info_span!("infer log of constraint degree", air_idx = i).in_scope(|| {
148                    get_log_num_quotient_chunks::<Val<SC>, SC::Challenge, A, LogUpGadget>(
149                        air,
150                        layout,
151                        &all_lookups[i],
152                        config.is_zk(),
153                        &lookup_gadget,
154                    )
155                });
156            let n_chunks = 1 << (lq_chunks + config.is_zk());
157            (lq_chunks, n_chunks)
158        })
159        .unzip();
160
161    // Observe the number of instances up front so the transcript can't be reinterpreted
162    // with a different partitioning.
163    let n_instances = airs.len();
164    challenger.observe_base_as_algebra_element::<Challenge<SC>>(Val::<SC>::from_usize(n_instances));
165
166    // Observe per-instance binding data: (log_ext_degree, log_degree), width, num quotient chunks.
167    for i in 0..n_instances {
168        observe_instance_binding::<SC>(
169            &mut challenger,
170            log_ext_degrees[i],
171            log_degrees[i],
172            A::width(airs[i]),
173            num_quotient_chunks[i],
174        );
175    }
176
177    // Commit to all traces using a single batched commitment, preserving input order.
178    let main_commit_inputs = instances
179        .iter()
180        .zip(ext_trace_domains.iter().cloned())
181        .map(|(inst, dom)| (dom, inst.trace.clone()))
182        .collect::<Vec<_>>();
183    let (main_commit, main_data) = pcs.commit(main_commit_inputs);
184
185    // Observe main commitment and all public values (as base field elements).
186    challenger.observe(main_commit.clone());
187    for pv in &pub_vals {
188        challenger.observe_slice(pv);
189    }
190
191    // Observe preprocessed widths for each instance, to bind transparent
192    // preprocessed columns into the transcript. If a global preprocessed
193    // commitment exists, observe it once.
194    for &pre_w in preprocessed_widths.iter() {
195        challenger.observe_base_as_algebra_element::<Challenge<SC>>(Val::<SC>::from_usize(pre_w));
196    }
197    if let Some(global) = &common.preprocessed {
198        challenger.observe(global.commitment.clone());
199    }
200
201    // Sample the lookup challenges.
202    let challenges_per_instance =
203        get_perm_challenges::<SC, LogUpGadget>(&mut challenger, &all_lookups, &lookup_gadget);
204
205    // Get permutation matrices, if any, along with their associated trace domain
206    let mut permutation_commit_inputs = Vec::with_capacity(n_instances);
207    instances
208        .iter()
209        .enumerate()
210        .zip(ext_trace_domains.iter().cloned())
211        .for_each(|((i, inst), ext_domain)| {
212            if !all_lookups[i].is_empty() {
213                let generated_perm = lookup_gadget.generate_permutation::<SC>(
214                    inst.trace,
215                    &inst.air.preprocessed_trace(),
216                    &inst.public_values,
217                    &all_lookups[i],
218                    &mut lookup_data[i],
219                    &challenges_per_instance[i],
220                );
221                permutation_commit_inputs
222                    .push((ext_domain, generated_perm.clone().flatten_to_base()));
223
224                #[cfg(debug_assertions)]
225                {
226                    use crate::check_constraints::check_constraints;
227
228                    let preprocessed_trace = inst.air.preprocessed_trace();
229
230                    let perm_vals: Vec<SC::Challenge> = lookup_data[i]
231                        .iter()
232                        .map(|ld| ld.expected_cumulated)
233                        .collect();
234                    let lookup_constraints_inputs = (all_lookups[i].as_slice(), &lookup_gadget);
235                    check_constraints(
236                        inst.air,
237                        inst.trace,
238                        &preprocessed_trace,
239                        &generated_perm,
240                        &challenges_per_instance[i],
241                        &perm_vals,
242                        &inst.public_values,
243                        lookup_constraints_inputs,
244                    );
245                }
246            }
247        });
248
249    // Check that all lookups are balanced.
250    #[cfg(debug_assertions)]
251    {
252        use p3_lookup::debug_util::{LookupDebugInstance, check_lookups};
253
254        let preprocessed_traces: Vec<Option<RowMajorMatrix<Val<SC>>>> = instances
255            .iter()
256            .map(|inst| inst.air.preprocessed_trace())
257            .collect();
258        let debug_instances: Vec<LookupDebugInstance<'_, Val<SC>>> = instances
259            .iter()
260            .zip(preprocessed_traces.iter())
261            .map(|(inst, prep)| LookupDebugInstance {
262                main_trace: inst.trace,
263                preprocessed_trace: prep,
264                public_values: &inst.public_values,
265                lookups: &inst.lookups,
266                permutation_challenges: &[],
267            })
268            .collect();
269        check_lookups(&debug_instances);
270    }
271
272    // Commit to all traces in one multi-matrix commitment, preserving input order.
273    let permutation_commit_and_data = if !permutation_commit_inputs.is_empty() {
274        let commitment = pcs.commit(permutation_commit_inputs);
275        challenger.observe(commitment.0.clone());
276        for data in lookup_data.iter().flatten() {
277            challenger.observe_algebra_element(data.expected_cumulated);
278        }
279        Some(commitment)
280    } else {
281        None
282    };
283
284    // Compute quotient chunk counts and domains per instance inline in the loop below.
285
286    // Get the random alpha to fold constraints.
287    let alpha: Challenge<SC> = challenger.sample_algebra_element();
288
289    // Build per-instance quotient domains and values, and split into chunks.
290    let mut quotient_chunk_domains: Vec<Domain<SC>> = Vec::new();
291    let mut quotient_chunk_mats: Vec<RowMajorMatrix<Val<SC>>> = Vec::new();
292    // Track ranges so we can map openings back to instances.
293    let mut quotient_chunk_ranges: Vec<(usize, usize)> = Vec::with_capacity(n_instances);
294
295    let mut perm_counter = 0;
296
297    // TODO: Parallelize this loop for better performance with many instances.
298    for (i, trace_domain) in trace_domains.iter().enumerate() {
299        let _air_span = info_span!("compute quotient", air_idx = i).entered();
300
301        let log_chunks = log_num_quotient_chunks[i];
302        let n_chunks = num_quotient_chunks[i];
303        // Disjoint domain of size ext_degree * num_quotient_chunks
304        // (log size = log_ext_degrees[i] + log_num_quotient_chunks[i]); use ext domain for shift.
305        let quotient_domain =
306            ext_trace_domains[i].create_disjoint_domain(1 << (log_ext_degrees[i] + log_chunks));
307
308        let sym_layout = AirLayout {
309            preprocessed_width: preprocessed_widths[i],
310            main_width: airs[i].width(),
311            num_public_values: airs[i].num_public_values(),
312            ..Default::default()
313        };
314
315        // In debug builds, cross-check the static hint against symbolic evaluation.
316        debug_assert!(
317            airs[i].num_constraints().is_none_or(|n| {
318                n == get_symbolic_constraints(airs[i], sym_layout, &all_lookups[i], &lookup_gadget)
319                    .0
320                    .len()
321            }),
322            "num_constraints() = {} but symbolic evaluation found {} base constraints",
323            airs[i].num_constraints().unwrap(),
324            get_symbolic_constraints(airs[i], sym_layout, &all_lookups[i], &lookup_gadget,)
325                .0
326                .len(),
327        );
328
329        // Get evaluations on quotient domain from the main commitment.
330        let trace_on_quotient_domain =
331            pcs.get_evaluations_on_domain(&main_data, i, quotient_domain);
332
333        let permutation_on_quotient_domain = permutation_commit_and_data
334            .as_ref()
335            .filter(|_| !all_lookups[i].is_empty())
336            .map(|(_, perm_data)| {
337                let evals = pcs.get_evaluations_on_domain(perm_data, perm_counter, quotient_domain);
338                perm_counter += 1;
339                evals
340            });
341
342        // Get preprocessed evaluations if this instance has preprocessed columns.
343        let preprocessed_on_quotient_domain = common
344            .preprocessed
345            .as_ref()
346            .and_then(|g| g.instances[i].as_ref())
347            .map(|meta| {
348                let preprocessed_prover_data = prover_data
349                    .prover_only
350                    .preprocessed_prover_data
351                    .as_ref()
352                    .expect("preprocessed_prover_data must exist when preprocessed columns exist");
353                pcs.get_evaluations_on_domain_no_random(
354                    preprocessed_prover_data,
355                    meta.matrix_index,
356                    quotient_domain,
357                )
358            });
359
360        // Compute quotient(x) = constraints(x)/Z_H(x) over quotient_domain, as extension values.
361        let perm_vals: Vec<SC::Challenge> = lookup_data[i]
362            .iter()
363            .map(|ld| ld.expected_cumulated)
364            .collect();
365        let q_values = quotient_values::<SC, A, _, LogUpGadget>(
366            airs[i],
367            &pub_vals[i],
368            sym_layout,
369            *trace_domain,
370            quotient_domain,
371            &trace_on_quotient_domain,
372            permutation_on_quotient_domain.as_ref(),
373            &all_lookups[i],
374            &perm_vals,
375            &lookup_gadget,
376            &challenges_per_instance[i],
377            preprocessed_on_quotient_domain.as_ref(),
378            alpha,
379        );
380
381        // Flatten to base field and split into chunks.
382        let q_flat = RowMajorMatrix::new_col(q_values).flatten_to_base();
383        let chunk_mats = quotient_domain.split_evals(n_chunks, q_flat);
384        let chunk_domains = quotient_domain.split_domains(n_chunks);
385
386        let evals = chunk_domains.iter().zip(chunk_mats).map(|(d, m)| (*d, m));
387        let ldes = pcs.get_quotient_ldes(evals, n_chunks);
388
389        let start = quotient_chunk_domains.len();
390        quotient_chunk_domains.extend(chunk_domains);
391        quotient_chunk_mats.extend(ldes);
392        let end = quotient_chunk_domains.len();
393        quotient_chunk_ranges.push((start, end));
394    }
395
396    // Commit to all quotient chunks together.
397    let (quotient_commit, quotient_data) = pcs.commit_ldes(quotient_chunk_mats);
398    challenger.observe(quotient_commit.clone());
399
400    // If zk is enabled, we generate random extension field values of the size of the randomized trace. If `n` is the degree of the initial trace,
401    // then the randomized trace has degree `2n`. To randomize the FRI batch polynomial, we then need an extension field random polynomial of degree `2n -1`.
402    // So we can generate a random polynomial of degree `2n`, and provide it to `open` as is.
403    // Then the method will add `(R(X) - R(z)) / (X - z)` (which is of the desired degree `2n - 1`), to the batch of polynomials.
404    // Since we need a random polynomial defined over the extension field, and the `commit` method is over the base field,
405    // we actually need to commit to `SC::Challenge::D` base field random polynomials.
406    // This is similar to what is done for the quotient polynomials.
407    // TODO: This approach is only statistically zk. To make it perfectly zk, `R` would have to truly be an extension field polynomial.
408    let (opt_r_commit, opt_r_data) = if SC::Pcs::ZK {
409        let (r_commit, r_data) = pcs
410            .get_opt_randomization_poly_commitment(ext_trace_domains.iter().copied())
411            .expect("ZK is enabled, so we should have randomization commitments");
412        (Some(r_commit), Some(r_data))
413    } else {
414        (None, None)
415    };
416
417    if let Some(r_commit) = &opt_r_commit {
418        challenger.observe(r_commit.clone());
419    }
420
421    // Sample OOD point.
422    let zeta: Challenge<SC> = challenger.sample_algebra_element();
423
424    // Build opening rounds, including optional global preprocessed commitment.
425    let (opened_values, opening_proof) = {
426        let mut rounds = Vec::new();
427
428        let round0 = opt_r_data.as_ref().map(|r_data| {
429            let round0_points = trace_domains.iter().map(|_| vec![zeta]).collect::<Vec<_>>();
430            (r_data, round0_points)
431        });
432        rounds.extend(round0);
433        // Main trace round: per instance, open at zeta and (conditionally) its next point.
434        let round1_points = trace_domains
435            .iter()
436            .enumerate()
437            .map(|(i, dom)| {
438                if !airs[i].main_next_row_columns().is_empty() {
439                    vec![
440                        zeta,
441                        dom.next_point(zeta)
442                            .expect("domain should support next_point operation"),
443                    ]
444                } else {
445                    vec![zeta]
446                }
447            })
448            .collect::<Vec<_>>();
449        rounds.push((&main_data, round1_points));
450
451        // Quotient chunks round: one point per chunk at zeta.
452        let round2_points = quotient_chunk_ranges
453            .iter()
454            .cloned()
455            .flat_map(|(s, e)| (s..e).map(|_| vec![zeta]))
456            .collect::<Vec<_>>();
457        rounds.push((&quotient_data, round2_points));
458
459        // Optional global preprocessed round: one matrix per instance that
460        // has preprocessed columns. For AIRs that only use the local
461        // preprocessed row, we open only at `zeta`; otherwise we open at
462        // both `zeta` and `zeta_next`.
463        if let Some(global) = &common.preprocessed {
464            let preprocessed_prover_data = prover_data
465                .prover_only
466                .preprocessed_prover_data
467                .as_ref()
468                .expect("preprocessed_prover_data must exist when preprocessed columns exist");
469            let pre_points = global
470                .matrix_to_instance
471                .iter()
472                .map(|&inst_idx| {
473                    if !airs[inst_idx].preprocessed_next_row_columns().is_empty() {
474                        let zeta_next_i = trace_domains[inst_idx]
475                            .next_point(zeta)
476                            .expect("domain should support next_point operation");
477                        vec![zeta, zeta_next_i]
478                    } else {
479                        vec![zeta]
480                    }
481                })
482                .collect::<Vec<_>>();
483            rounds.push((preprocessed_prover_data, pre_points));
484        }
485
486        let lookup_points: Vec<_> = trace_domains
487            .iter()
488            .zip(&all_lookups)
489            .filter(|&(_, lookups)| !lookups.is_empty())
490            .map(|(dom, _)| {
491                vec![
492                    zeta,
493                    dom.next_point(zeta)
494                        .expect("domain should support next_point operation"),
495                ]
496            })
497            .collect();
498
499        if let Some((_, perm_data)) = &permutation_commit_and_data {
500            let lookup_round = (perm_data, lookup_points);
501            rounds.push(lookup_round);
502        }
503
504        pcs.open_with_preprocessing(rounds, &mut challenger, common.preprocessed.is_some())
505    };
506
507    // Rely on PCS indices for opened value groups: main trace, quotient, preprocessed.
508    let trace_idx = SC::Pcs::TRACE_IDX;
509    let quotient_idx = SC::Pcs::QUOTIENT_IDX;
510    let preprocessed_idx = SC::Pcs::PREPROCESSED_TRACE_IDX;
511    let permutation_idx = if common.preprocessed.is_some() {
512        preprocessed_idx + 1
513    } else {
514        preprocessed_idx
515    };
516
517    // Parse trace opened values per instance.
518    let trace_values_for_mats = &opened_values[trace_idx];
519    assert_eq!(trace_values_for_mats.len(), n_instances);
520
521    // Parse quotient chunk opened values and map per instance.
522    let mut per_instance: Vec<OpenedValuesWithLookups<Challenge<SC>>> =
523        Vec::with_capacity(n_instances);
524
525    // Preprocessed openings, if a global preprocessed commitment exists.
526    let preprocessed_openings = common
527        .preprocessed
528        .as_ref()
529        .map(|_| &opened_values[SC::Pcs::PREPROCESSED_TRACE_IDX]);
530
531    let is_lookup = permutation_commit_and_data.is_some();
532    let permutation_values_for_mats = if is_lookup {
533        &opened_values[permutation_idx]
534    } else {
535        &vec![]
536    };
537    let mut permutation_values_for_mats = permutation_values_for_mats.iter();
538
539    let mut quotient_openings_iter = opened_values[quotient_idx].iter();
540    for (i, (s, e)) in quotient_chunk_ranges.iter().copied().enumerate() {
541        let random = if opt_r_data.is_some() {
542            Some(opened_values[0][i][0].clone())
543        } else {
544            None
545        };
546        // Trace locals
547        let tv = &trace_values_for_mats[i];
548        let trace_local = tv[0].clone();
549        let trace_next = if !airs[i].main_next_row_columns().is_empty() {
550            Some(tv[1].clone())
551        } else {
552            None
553        };
554
555        // Quotient chunks: for each chunk matrix, take the first point (zeta) values.
556        let mut qcs = Vec::with_capacity(e - s);
557        for _ in s..e {
558            let mat_vals = quotient_openings_iter
559                .next()
560                .expect("chunk index in bounds");
561            qcs.push(mat_vals[0].clone());
562        }
563
564        // Preprocessed openings (if present).
565        let (preprocessed_local, preprocessed_next) = if let (Some(global), Some(pre_round)) =
566            (&common.preprocessed, preprocessed_openings)
567        {
568            global.instances[i].as_ref().map_or((None, None), |meta| {
569                let vals = &pre_round[meta.matrix_index];
570                if !airs[i].preprocessed_next_row_columns().is_empty() {
571                    assert_eq!(
572                        vals.len(),
573                        2,
574                        "expected two opening points (zeta, zeta_next) for preprocessed trace"
575                    );
576                    (Some(vals[0].clone()), Some(vals[1].clone()))
577                } else {
578                    assert_eq!(
579                        vals.len(),
580                        1,
581                        "expected one opening point (zeta) for preprocessed trace"
582                    );
583                    (Some(vals[0].clone()), None)
584                }
585            })
586        } else {
587            (None, None)
588        };
589
590        // Not all AIRs have lookups, so for each instance, we first need to check whether it has lookups.
591        let (permutation_local, permutation_next) = if !all_lookups[i].is_empty() {
592            let perm_v = permutation_values_for_mats
593                .next()
594                .expect("instance should have permutation openings");
595            (perm_v[0].clone(), perm_v[1].clone())
596        } else {
597            (vec![], vec![])
598        };
599
600        let base_opened = OpenedValues {
601            trace_local,
602            trace_next,
603            preprocessed_local,
604            preprocessed_next,
605            quotient_chunks: qcs,
606            random,
607        };
608
609        per_instance.push(OpenedValuesWithLookups {
610            base_opened_values: base_opened,
611            permutation_local,
612            permutation_next,
613        });
614    }
615
616    let permutation = permutation_commit_and_data
617        .as_ref()
618        .map(|(comm, _)| comm.clone());
619
620    BatchProof {
621        commitments: BatchCommitments {
622            main: main_commit,
623            quotient_chunks: quotient_commit,
624            random: opt_r_commit,
625            permutation,
626        },
627        opened_values: BatchOpenedValues {
628            instances: per_instance,
629        },
630        opening_proof,
631        global_lookup_data: lookup_data,
632        degree_bits: log_ext_degrees,
633    }
634}
635
636#[instrument(name = "compute quotient polynomial", skip_all)]
637// TODO: Group some arguments to remove the `allow`?
638#[allow(clippy::too_many_arguments)]
639pub fn quotient_values<SC, A, Mat, LG>(
640    air: &A,
641    public_values: &[Val<SC>],
642    layout: AirLayout,
643    trace_domain: Domain<SC>,
644    quotient_domain: Domain<SC>,
645    trace_on_quotient_domain: &Mat,
646    opt_permutation_on_quotient_domain: Option<&Mat>,
647    lookups: &[Lookup<Val<SC>>],
648    permutation_vals: &[SC::Challenge],
649    lookup_gadget: &LG,
650    permutation_challenges: &[SC::Challenge],
651    preprocessed_on_quotient_domain: Option<&Mat>,
652    alpha: SC::Challenge,
653) -> Vec<SC::Challenge>
654where
655    SC: SGC,
656    A: Air<SymbolicAirBuilder<Val<SC>, SC::Challenge>>
657        + for<'a> Air<ProverConstraintFolderWithLookups<'a, SC>>,
658    Mat: Matrix<Val<SC>> + Sync,
659    LG: LookupGadget + Sync,
660    SymbolicExpressionExt<Val<SC>, SC::Challenge>: Algebra<SC::Challenge>,
661{
662    let quotient_size = quotient_domain.size();
663    let main_width = trace_on_quotient_domain.width();
664    let (perm_width, perm_height) = opt_permutation_on_quotient_domain
665        .as_ref()
666        .map_or((0, 0), |mat| (mat.width(), mat.height()));
667
668    let ext_degree = SC::Challenge::DIMENSION;
669
670    let mut sels = debug_span!("Compute Selectors")
671        .in_scope(|| trace_domain.selectors_on_coset(quotient_domain));
672
673    let qdb = log2_strict_usize(quotient_domain.size()) - log2_strict_usize(trace_domain.size());
674    let next_step = 1 << qdb;
675
676    // Pad selectors with default values if the domain is smaller than the packing width.
677    let pack_width = PackedVal::<SC>::WIDTH;
678    if quotient_size < pack_width {
679        let pad_len = pack_width;
680        // Helper to resize a specific selector vector
681        let pad = |v: &mut Vec<_>| v.resize(pad_len, Val::<SC>::default());
682        pad(&mut sels.is_first_row);
683        pad(&mut sels.is_last_row);
684        pad(&mut sels.is_transition);
685        pad(&mut sels.inv_vanishing);
686    }
687
688    let constraint_layout = get_constraint_layout(air, layout, lookups, lookup_gadget);
689    let (base_alpha_powers, ext_alpha_powers) = constraint_layout.decompose_alpha(alpha);
690
691    // Precompute per-instance data used by the hot inner loop to avoid repeated allocations.
692    let packed_perm_challenges: Vec<PackedChallenge<SC>> = permutation_challenges
693        .iter()
694        .map(|&p_c| PackedChallenge::<SC>::from(p_c))
695        .collect();
696    let permutation_vals_packed: Vec<PackedChallenge<SC>> = permutation_vals
697        .iter()
698        .map(|&v| PackedChallenge::<SC>::from(v))
699        .collect();
700
701    (0..quotient_size)
702        .into_par_iter()
703        .step_by(PackedVal::<SC>::WIDTH)
704        .flat_map_iter(|i_start| {
705            let i_range = i_start..i_start + PackedVal::<SC>::WIDTH;
706
707            let is_first_row = *PackedVal::<SC>::from_slice(&sels.is_first_row[i_range.clone()]);
708            let is_last_row = *PackedVal::<SC>::from_slice(&sels.is_last_row[i_range.clone()]);
709            let is_transition = *PackedVal::<SC>::from_slice(&sels.is_transition[i_range.clone()]);
710            let inv_vanishing = *PackedVal::<SC>::from_slice(&sels.inv_vanishing[i_range]);
711
712            // Retrieve main trace as a matrix evaluated on the quotient domain.
713            let main = RowMajorMatrix::new(
714                trace_on_quotient_domain.vertically_packed_row_pair(i_start, next_step),
715                main_width,
716            );
717
718            let preprocessed = preprocessed_on_quotient_domain.map(|preprocessed| {
719                let preprocessed_width = preprocessed.width();
720                RowMajorMatrix::new(
721                    preprocessed.vertically_packed_row_pair(i_start, next_step),
722                    preprocessed_width,
723                )
724            });
725
726            // Retrieve permutation trace as a matrix evaluated on the quotient domain.
727            let permutation = opt_permutation_on_quotient_domain.as_ref().map_or_else(
728                || RowMajorMatrix::new(vec![], 0),
729                |permutation_on_quotient_domain| {
730                    let perms = (0..perm_width)
731                        .step_by(ext_degree)
732                        .map(|col| {
733                            PackedChallenge::<SC>::from_basis_coefficients_fn(|i| {
734                                PackedVal::<SC>::from_fn(|offset| {
735                                    permutation_on_quotient_domain
736                                        .get((i_start + offset) % perm_height, col + i)
737                                        .unwrap()
738                                })
739                            })
740                        })
741                        .chain((0..perm_width).step_by(ext_degree).map(|col| {
742                            PackedChallenge::<SC>::from_basis_coefficients_fn(|i| {
743                                PackedVal::<SC>::from_fn(|offset| {
744                                    permutation_on_quotient_domain
745                                        .get((i_start + next_step + offset) % perm_height, col + i)
746                                        .unwrap()
747                                })
748                            })
749                        }));
750
751                    RowMajorMatrix::new(perms.collect::<Vec<_>>(), perm_width / ext_degree)
752                },
753            );
754
755            let preprocessed_view = preprocessed
756                .as_ref()
757                .map_or_else(|| RowMajorMatrixView::new(&[], 0), |m| m.as_view());
758            let inner_folder = ProverConstraintFolder {
759                main: main.as_view(),
760                preprocessed: preprocessed_view,
761                preprocessed_window: RowWindow::from_view(&preprocessed_view),
762                public_values,
763                is_first_row,
764                is_last_row,
765                is_transition,
766                base_alpha_powers: &base_alpha_powers,
767                ext_alpha_powers: &ext_alpha_powers,
768                base_constraints: Vec::with_capacity(constraint_layout.base_indices.len()),
769                ext_constraints: Vec::with_capacity(constraint_layout.ext_indices.len()),
770                constraint_index: 0,
771                constraint_count: constraint_layout.total_constraints(),
772            };
773
774            let mut folder = ProverConstraintFolderWithLookups {
775                inner: inner_folder,
776                permutation: permutation.as_view(),
777                permutation_challenges: &packed_perm_challenges,
778                permutation_values: &permutation_vals_packed,
779            };
780            air.eval_with_lookups(&mut folder, lookups, lookup_gadget);
781
782            // quotient(x) = constraints(x) / Z_H(x)
783            let quotient = folder.inner.finalize_constraints() * inv_vanishing;
784
785            // "Transpose" D packed base coefficients into WIDTH scalar extension coefficients.
786            (0..core::cmp::min(quotient_size, PackedVal::<SC>::WIDTH))
787                .map(move |idx_in_packing| quotient.extract(idx_in_packing))
788        })
789        .collect()
790}