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}