p3_batch_stark/
prover.rs

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