p3_batch_stark/
common.rs

1//! Shared data between batch-STARK prover and verifier.
2//!
3//! This module is intended to store per-instance data that is common to both
4//! proving and verification, such as lookup tables and preprocessed traces.
5//!
6//! The preprocessed support integrates with `p3-uni-stark`'s transparent
7//! preprocessed columns API, but batches all preprocessed traces into a single
8//! global commitment (one matrix per instance that uses preprocessed columns).
9
10use alloc::vec;
11use alloc::vec::Vec;
12
13use hashbrown::HashMap;
14use p3_challenger::FieldChallenger;
15use p3_commit::Pcs;
16use p3_field::BasedVectorSpace;
17use p3_lookup::lookup_traits::{AirLookupHandler, Kind, Lookup, LookupGadget};
18use p3_matrix::Matrix;
19use p3_uni_stark::{SymbolicAirBuilder, SymbolicExpression, Val};
20use p3_util::log2_strict_usize;
21
22use crate::config::{Challenge, Commitment, Domain, StarkGenericConfig as SGC};
23use crate::prover::StarkInstance;
24
25/// Per-instance metadata for a preprocessed trace that lives inside a
26/// global preprocessed commitment.
27#[derive(Clone)]
28pub struct PreprocessedInstanceMeta {
29    /// Index of this instance's preprocessed matrix inside the global [`Pcs`]
30    /// commitment / prover data.
31    pub matrix_index: usize,
32    /// Width (number of columns) of the preprocessed trace.
33    pub width: usize,
34    /// Log2 of the base trace degree for this instance's preprocessed trace.
35    ///
36    /// This matches the log2 of the main trace degree (without ZK padding)
37    /// for that instance.
38    pub degree_bits: usize,
39}
40
41/// Global preprocessed data shared by all batch-STARK instances.
42///
43/// This batches all per-instance preprocessed traces into a single [`Pcs`]
44/// commitment and prover data object, while keeping a mapping from instance
45/// index to matrix index and per-matrix metadata.
46pub struct GlobalPreprocessed<SC: SGC> {
47    /// Single [`Pcs`] commitment to all preprocessed traces (one matrix per
48    /// instance that defines preprocessed columns).
49    pub commitment: Commitment<SC>,
50    /// [`Pcs`] prover data for the batched preprocessed commitment.
51    pub prover_data: <SC::Pcs as Pcs<Challenge<SC>, SC::Challenger>>::ProverData,
52    /// For each STARK instance, optional metadata describing its preprocessed
53    /// trace inside the global commitment.
54    ///
55    /// `instances[i] == None` means instance `i` has no preprocessed columns.
56    pub instances: Vec<Option<PreprocessedInstanceMeta>>,
57    /// Mapping from preprocessed matrix index to the corresponding instance index.
58    ///
59    /// This allows building per-matrix opening schedules and routing opened
60    /// values back to instances.
61    pub matrix_to_instance: Vec<usize>,
62}
63
64// TODO: Local-only preprocessed
65// Some AIRs only need local preprocessed openings and never use the "next"
66// row for preprocessed columns. At the moment we always open both [zeta, zeta_next]
67// per preprocessed matrix, which is sound but wastes openings.
68
69/// Struct storing data common to both the prover and verifier.
70// TODO: Optionally cache a single challenger seed for transparent
71//       preprocessed data (per-instance widths + global root), so
72//       prover and verifier don't have to recompute/rehash it each run.
73pub struct CommonData<SC: SGC> {
74    /// Optional global preprocessed commitment shared by all instances.
75    ///
76    /// When `None`, no instance uses preprocessed columns.
77    pub preprocessed: Option<GlobalPreprocessed<SC>>,
78    /// The lookups used by each STARK instance.
79    /// There is one `Vec<Lookup<Val<SC>>>` per STARK instance.
80    /// They are stored in the same order as the STARK instance inputs provided to `new`.
81    pub lookups: Vec<Vec<Lookup<Val<SC>>>>,
82}
83
84impl<SC: SGC> CommonData<SC> {
85    pub const fn new(
86        preprocessed: Option<GlobalPreprocessed<SC>>,
87        lookups: Vec<Vec<Lookup<Val<SC>>>>,
88    ) -> Self {
89        Self {
90            preprocessed,
91            lookups,
92        }
93    }
94
95    /// Create [`CommonData`] with no preprocessed columns or lookups.
96    ///
97    /// Use this when none of your [`Air`] implementations have preprocessed columns or lookups.
98    pub fn empty(num_instances: usize) -> Self {
99        let lookups = vec![Vec::new(); num_instances];
100        Self {
101            preprocessed: None,
102            lookups,
103        }
104    }
105}
106
107impl<SC> CommonData<SC>
108where
109    SC: SGC,
110    Challenge<SC>: BasedVectorSpace<Val<SC>>,
111{
112    /// Build [`CommonData`] directly from STARK instances.
113    ///
114    /// This automatically:
115    /// - Derives trace degrees from trace heights
116    /// - Computes extended degrees (base + ZK padding)
117    /// - Sets up preprocessed columns for [`Air`] implementations that define them, committing
118    ///   to them in a single global [`Pcs`] commitment.
119    /// - Deduces symbolic lookups from the STARKs
120    ///
121    /// This is a convenience function mainly used for tests.
122    pub fn from_instances<A>(config: &SC, instances: &[StarkInstance<'_, SC, A>]) -> Self
123    where
124        SymbolicExpression<SC::Challenge>: From<SymbolicExpression<Val<SC>>>,
125        A: AirLookupHandler<SymbolicAirBuilder<Val<SC>, SC::Challenge>> + Clone,
126    {
127        let degrees: Vec<usize> = instances.iter().map(|i| i.trace.height()).collect();
128        let log_ext_degrees: Vec<usize> = degrees
129            .iter()
130            .map(|&d| log2_strict_usize(d) + config.is_zk())
131            .collect();
132        let mut airs: Vec<A> = instances.iter().map(|i| i.air.clone()).collect();
133        Self::from_airs_and_degrees(config, &mut airs, &log_ext_degrees)
134    }
135
136    /// Build [`CommonData`] from [`Air`] implementations and their extended trace degree bits.
137    ///
138    /// # Arguments
139    ///
140    /// * `trace_ext_degree_bits` - Log2 of extended trace degrees (including ZK padding)
141    ///
142    /// # Returns
143    ///
144    /// Global preprocessed data shared by all instances. The global commitment
145    /// is present only if at least one [`Air`] defines preprocessed columns.
146    pub fn from_airs_and_degrees<A>(
147        config: &SC,
148        airs: &mut [A],
149        trace_ext_degree_bits: &[usize],
150    ) -> Self
151    where
152        SymbolicExpression<SC::Challenge>: From<SymbolicExpression<Val<SC>>>,
153        A: AirLookupHandler<SymbolicAirBuilder<Val<SC>, SC::Challenge>>,
154    {
155        assert_eq!(
156            airs.len(),
157            trace_ext_degree_bits.len(),
158            "airs and trace_ext_degree_bits must have the same length"
159        );
160
161        let pcs = config.pcs();
162        let is_zk = config.is_zk();
163
164        let mut instances_meta: Vec<Option<PreprocessedInstanceMeta>> =
165            Vec::with_capacity(airs.len());
166        let mut matrix_to_instance: Vec<usize> = Vec::new();
167        let mut domains_and_traces: Vec<(Domain<SC>, _)> = Vec::new();
168
169        for (i, (air, &ext_db)) in airs.iter().zip(trace_ext_degree_bits.iter()).enumerate() {
170            // Derive base trace degree bits from extended degree bits.
171            let base_db = ext_db - is_zk;
172            let maybe_prep = air.preprocessed_trace();
173
174            let Some(preprocessed) = maybe_prep else {
175                instances_meta.push(None);
176                continue;
177            };
178
179            let width = preprocessed.width();
180            if width == 0 {
181                instances_meta.push(None);
182                continue;
183            }
184
185            let degree = 1 << base_db;
186            let ext_degree = 1 << ext_db;
187            assert_eq!(
188                preprocessed.height(),
189                degree,
190                "preprocessed trace height must equal trace degree for instance {}",
191                i
192            );
193
194            let domain = pcs.natural_domain_for_degree(ext_degree);
195            let matrix_index = domains_and_traces.len();
196
197            domains_and_traces.push((domain, preprocessed));
198            matrix_to_instance.push(i);
199
200            instances_meta.push(Some(PreprocessedInstanceMeta {
201                matrix_index,
202                width,
203                degree_bits: ext_db,
204            }));
205        }
206
207        let preprocessed = if domains_and_traces.is_empty() {
208            None
209        } else {
210            let (commitment, prover_data) = pcs.commit_preprocessing(domains_and_traces);
211            Some(GlobalPreprocessed {
212                commitment,
213                prover_data,
214                instances: instances_meta,
215                matrix_to_instance,
216            })
217        };
218
219        let lookups = airs.iter_mut().map(|air| air.get_lookups()).collect();
220
221        Self {
222            preprocessed,
223            lookups,
224        }
225    }
226}
227
228pub fn get_perm_challenges<SC: SGC, LG: LookupGadget>(
229    challenger: &mut SC::Challenger,
230    all_lookups: &[Vec<Lookup<Val<SC>>>],
231    lookup_gadget: &LG,
232) -> Vec<Vec<SC::Challenge>> {
233    let num_challenges_per_lookup = lookup_gadget.num_challenges();
234    let mut global_perm_challenges = HashMap::new();
235
236    all_lookups
237        .iter()
238        .map(|contexts| {
239            // Pre-allocate for the instance's challenges.
240            let num_challenges = contexts.len() * num_challenges_per_lookup;
241            let mut instance_challenges = Vec::with_capacity(num_challenges);
242
243            for context in contexts {
244                match &context.kind {
245                    Kind::Global(name) => {
246                        // Get or create the global challenges.
247                        let challenges: &mut Vec<SC::Challenge> =
248                            global_perm_challenges.entry(name).or_insert_with(|| {
249                                (0..num_challenges_per_lookup)
250                                    .map(|_| challenger.sample_algebra_element())
251                                    .collect()
252                            });
253                        instance_challenges.extend_from_slice(challenges);
254                    }
255                    Kind::Local => {
256                        instance_challenges.extend(
257                            (0..num_challenges_per_lookup)
258                                .map(|_| challenger.sample_algebra_element::<SC::Challenge>()),
259                        );
260                    }
261                }
262            }
263            instance_challenges
264        })
265        .collect()
266}