Skip to main content

openvm_stark_backend/keygen/
mod.rs

1use std::{cmp::max, collections::HashMap, sync::Arc};
2
3use itertools::Itertools;
4use p3_air::BaseAir;
5use p3_field::{Field, PrimeCharacteristicRing};
6use p3_util::log2_strict_usize;
7use tracing::instrument;
8
9use crate::{
10    air_builders::symbolic::{
11        get_symbolic_builder,
12        symbolic_variable::{Entry, SymbolicVariable},
13        SymbolicConstraintsDag, SymbolicExpressionNode,
14    },
15    hasher::MerkleHasher,
16    keygen::types::{
17        KeygenError, LinearConstraint, MultiStarkProvingKey, MultiStarkVerifyingKey0,
18        StarkProvingKey, StarkVerifyingKey, StarkVerifyingParams, TraceWidth,
19        VerifierSinglePreprocessedData,
20    },
21    proof::CODEC_VERSION,
22    prover::{
23        stacked_pcs::{stacked_commit, StackedPcsData},
24        ColMajorMatrix, MatrixDimensions,
25    },
26    AirRef, AnyAir, StarkProtocolConfig, SystemParams,
27};
28
29pub mod types;
30
31struct AirKeygenBuilder<SC: StarkProtocolConfig> {
32    pub is_required: bool,
33    air: AirRef<SC>,
34    prep_keygen_data: PrepKeygenData<SC>,
35}
36
37/// Stateful builder to create multi-stark proving and verifying keys
38/// for system of multiple RAPs with multiple multi-matrix commitments
39pub struct MultiStarkKeygenBuilder<SC: StarkProtocolConfig> {
40    pub config: SC,
41    /// Information for partitioned AIRs.
42    partitioned_airs: Vec<AirKeygenBuilder<SC>>,
43}
44
45impl<SC: StarkProtocolConfig> MultiStarkKeygenBuilder<SC> {
46    pub fn new(config: SC) -> Self {
47        Self {
48            config,
49            partitioned_airs: vec![],
50        }
51    }
52
53    pub fn params(&self) -> &SystemParams {
54        self.config.params()
55    }
56
57    /// Default way to add a single Interactive AIR.
58    /// Returns `air_id`
59    pub fn add_air(&mut self, air: AirRef<SC>) -> usize {
60        self.add_air_impl(air, false)
61    }
62
63    pub fn add_required_air(&mut self, air: AirRef<SC>) -> usize {
64        self.add_air_impl(air, true)
65    }
66
67    #[instrument(level = "debug", skip_all, fields(name = air.name(), is_required = is_required))]
68    fn add_air_impl(&mut self, air: AirRef<SC>, is_required: bool) -> usize {
69        self.partitioned_airs
70            .push(AirKeygenBuilder::new(&self.config, air, is_required));
71        self.partitioned_airs.len() - 1
72    }
73
74    /// Consume the builder and generate proving key.
75    /// The verifying key can be obtained from the proving key.
76    pub fn generate_pk(self) -> Result<MultiStarkProvingKey<SC>, KeygenError> {
77        let params = self.params().clone();
78        let max_constraint_degree = params.max_constraint_degree;
79        let pk_per_air: Vec<_> = self
80            .partitioned_airs
81            .into_iter()
82            .map(|keygen_builder| {
83                // Second pass: get final constraints, where RAP phase constraints may have changed
84                keygen_builder.generate_pk(max_constraint_degree)
85            })
86            .collect::<Result<Vec<_>, KeygenError>>()?;
87
88        let mut air_max_constraint_degree = 0;
89        #[allow(unused)]
90        for (air_id, pk) in pk_per_air.iter().enumerate() {
91            let width = &pk.vk.params.width;
92            tracing::info!("{:<20} | Constraint Deg = {:<2} | Prep Cols = {:<2} | Main Cols = {:<8} | {:4} Constraints | {:3} Interactions",
93                pk.air_name,
94                pk.vk.max_constraint_degree,
95                width.preprocessed.unwrap_or(0),
96                format!("{:?}",width.main_widths()),
97                pk.vk.symbolic_constraints.constraints.constraint_idx.len(),
98                pk.vk.symbolic_constraints.interactions.len(),
99            );
100            air_max_constraint_degree = max(air_max_constraint_degree, pk.vk.max_constraint_degree);
101            tracing::debug!(
102                "On Buses {:?}",
103                pk.vk
104                    .symbolic_constraints
105                    .interactions
106                    .iter()
107                    .map(|i| i.bus_index)
108                    .collect_vec()
109            );
110            #[cfg(feature = "metrics")]
111            {
112                let labels = [
113                    ("air_name", pk.air_name.clone()),
114                    ("air_id", air_id.to_string()),
115                ];
116                metrics::counter!("constraint_deg", &labels)
117                    .absolute(pk.vk.max_constraint_degree as u64);
118                // column info will be logged by prover later
119                metrics::counter!("constraints", &labels)
120                    .absolute(pk.vk.symbolic_constraints.constraints.constraint_idx.len() as u64);
121                metrics::counter!("interactions", &labels)
122                    .absolute(pk.vk.symbolic_constraints.interactions.len() as u64);
123                metrics::counter!("need_rot", &labels).absolute(pk.vk.params.need_rot as u64);
124            }
125        }
126        if max_constraint_degree != air_max_constraint_degree as usize {
127            tracing::info!(
128            "Actual max constraint degree across all AIRs ({air_max_constraint_degree}) does not match configured max constraint degree ({max_constraint_degree})",
129        );
130        }
131
132        let num_airs = pk_per_air.len();
133        let base_order = SC::F::order().to_u32_digits()[0];
134        let mut count_weight_per_air_per_bus_index = HashMap::new();
135
136        let mut num_interactions_per_air: Vec<u32> = Vec::with_capacity(num_airs);
137        // We compute the a_i's for the constraints of the form a_0 n_0 + ... + a_{k-1} n_{k-1} <
138        // a_k, First the constraints that the total number of interactions on each bus is
139        // at most the base field order.
140        for (air_idx, pk) in pk_per_air.iter().enumerate() {
141            let constraints = &pk.vk.symbolic_constraints;
142            num_interactions_per_air.push(constraints.interactions.len().try_into().unwrap());
143            for interaction in &constraints.interactions {
144                // Also make sure that this of interaction is valid given the security params.
145                // +1 because of the bus
146                let max_msg_len = params.logup.max_message_length();
147                // plus one because of the bus
148                let total_message_length = interaction.message.len() + 1;
149                assert!(
150                    total_message_length <= max_msg_len,
151                    "interaction message with bus has length {}, which is more than max {max_msg_len}",
152                    total_message_length,
153                );
154
155                let b = interaction.bus_index;
156                let constraint = count_weight_per_air_per_bus_index
157                    .entry(b)
158                    .or_insert_with(|| LinearConstraint {
159                        coefficients: vec![0; num_airs],
160                        threshold: base_order,
161                    });
162                constraint.coefficients[air_idx] += interaction.count_weight;
163            }
164        }
165
166        let log_up_security_params = params.logup;
167
168        // Collect all constraints: per-bus sorted by bus index, then global.
169        let mut all_constraints: Vec<LinearConstraint> = count_weight_per_air_per_bus_index
170            .into_iter()
171            .sorted_by_key(|(bus_index, _)| *bus_index)
172            .map(|(_, constraint)| constraint)
173            .collect_vec();
174
175        all_constraints.push(LinearConstraint {
176            coefficients: num_interactions_per_air,
177            threshold: log_up_security_params.max_interaction_count,
178        });
179
180        // Minimize: keep only constraints not implied by any other.
181        let mut trace_height_constraints: Vec<LinearConstraint> = Vec::new();
182        for constraint in all_constraints {
183            if trace_height_constraints
184                .iter()
185                .any(|c| constraint.is_implied_by(c))
186            {
187                continue;
188            }
189            trace_height_constraints.retain(|c| !c.is_implied_by(&constraint));
190            trace_height_constraints.push(constraint);
191        }
192
193        let pre_vk: MultiStarkVerifyingKey0<SC> = MultiStarkVerifyingKey0 {
194            params: params.clone(),
195            per_air: pk_per_air.iter().map(|pk| pk.vk.clone()).collect(),
196            trace_height_constraints: trace_height_constraints.clone(),
197        };
198        // To protect against weak Fiat-Shamir, we hash the "pre"-verifying key and include it in
199        // the final verifying key. This just needs to commit to the verifying key and does
200        // not need to be verified by the verifier, so we just use postcard to serialize it.
201        let vk_bytes = postcard::to_allocvec(&pre_vk).unwrap();
202        tracing::debug!("pre-vkey: {} bytes", vk_bytes.len());
203        // Since postcard serialization is not self-describing, we include a version for domain
204        // separation. For further domain separation, we include the byte length.
205        let versioned_vk_bytes = CODEC_VERSION
206            .to_le_bytes()
207            .into_iter()
208            .chain((vk_bytes.len() as u64).to_le_bytes())
209            .chain(vk_bytes);
210        // Purely to get type compatibility and convenience, we hash using the native hash
211        let vk_pre_hash = self.config.hasher().hash_slice(
212            &versioned_vk_bytes
213                .into_iter()
214                .map(SC::F::from_u8)
215                .collect_vec(),
216        );
217
218        Ok(MultiStarkProvingKey {
219            params,
220            per_air: pk_per_air,
221            trace_height_constraints,
222            max_constraint_degree,
223            vk_pre_hash,
224        })
225    }
226}
227
228impl<SC: StarkProtocolConfig> AirKeygenBuilder<SC> {
229    pub fn new(config: &SC, air: AirRef<SC>, is_required: bool) -> Self {
230        let prep_keygen_data = PrepKeygenData::new(config.hasher(), config.params(), air.as_ref());
231        Self {
232            is_required,
233            air,
234            prep_keygen_data,
235        }
236    }
237
238    /// `max_constraint_degree` is the global max constraint degree. If this AIR's constraint degree
239    /// exceeds it, an error will be returned.
240    pub fn generate_pk(
241        self,
242        max_constraint_degree: usize,
243    ) -> Result<StarkProvingKey<SC>, KeygenError> {
244        let air_name = self.air.name();
245
246        let width = self.trace_width();
247        if width.main_width() == 0 {
248            return Err(KeygenError::AirWidthZero { name: air_name });
249        }
250
251        let symbolic_builder = get_symbolic_builder(self.air.as_ref(), &width);
252        let num_public_values = symbolic_builder.num_public_values();
253
254        let symbolic_constraints = symbolic_builder.constraints();
255        if symbolic_constraints.constraints.is_empty()
256            && symbolic_constraints.interactions.is_empty()
257        {
258            return Err(KeygenError::AirNoConstraintsOrInteractions { name: air_name });
259        }
260        if let Some(interaction_index) = symbolic_constraints
261            .interactions
262            .iter()
263            .position(|interaction| interaction.message.is_empty())
264        {
265            return Err(KeygenError::InteractionMessageEmpty {
266                name: air_name,
267                interaction_index,
268            });
269        }
270        let constraint_degree = symbolic_constraints.max_constraint_degree();
271        if constraint_degree > max_constraint_degree {
272            return Err(KeygenError::MaxConstraintDegreeExceeded {
273                name: air_name.clone(),
274                degree: constraint_degree,
275                max_degree: max_constraint_degree,
276            });
277        }
278
279        let Self {
280            prep_keygen_data:
281                PrepKeygenData {
282                    verifier_data: preprocessed_vdata,
283                    prover_data: prep_prover_data,
284                },
285            ..
286        } = self;
287
288        let dag = SymbolicConstraintsDag::from(symbolic_constraints);
289        let max_rotation = dag.constraints.max_rotation();
290        debug_assert!(max_rotation <= 1);
291        let need_rot = max_rotation == 1;
292        let vparams = StarkVerifyingParams {
293            width,
294            num_public_values,
295            need_rot,
296        };
297
298        let unused_variables = find_unused_vars(&dag, &vparams.width, need_rot);
299        let vk = StarkVerifyingKey {
300            preprocessed_data: preprocessed_vdata,
301            params: vparams,
302            symbolic_constraints: dag,
303            max_constraint_degree: constraint_degree
304                .try_into()
305                .expect("constraint degree should fit in u8"),
306            is_required: self.is_required,
307            unused_variables,
308        };
309        Ok(StarkProvingKey {
310            air_name,
311            vk,
312            preprocessed_data: prep_prover_data,
313        })
314    }
315
316    fn trace_width(&self) -> TraceWidth {
317        TraceWidth {
318            preprocessed: self.prep_keygen_data.width(),
319            cached_mains: self.air.cached_main_widths(),
320            common_main: self.air.common_main_width(),
321        }
322    }
323}
324
325pub(super) struct PrepKeygenData<SC: StarkProtocolConfig> {
326    pub verifier_data: Option<VerifierSinglePreprocessedData<SC::Digest>>,
327    pub prover_data: Option<Arc<StackedPcsData<SC::F, SC::Digest>>>,
328}
329
330impl<SC: StarkProtocolConfig> PrepKeygenData<SC> {
331    fn new(hasher: &SC::Hasher, params: &SystemParams, air: &dyn AnyAir<SC>) -> Self {
332        let preprocessed_trace = BaseAir::<SC::F>::preprocessed_trace(air);
333        let vpdata_opt = preprocessed_trace.map(|trace| {
334            let trace = ColMajorMatrix::from_row_major(&trace);
335            let (commit, data) = stacked_commit(
336                hasher,
337                params.l_skip,
338                params.n_stack,
339                params.log_blowup,
340                params.k_whir(),
341                &[&trace],
342            )
343            .unwrap();
344            debug_assert_eq!(trace.width(), data.mat_view(0).width());
345            let vdata = VerifierSinglePreprocessedData {
346                commit,
347                hypercube_dim: log2_strict_usize(trace.height()) as isize - params.l_skip as isize,
348                stacking_width: data.matrix.width(),
349            };
350            let pdata = Arc::new(data);
351            (vdata, pdata)
352        });
353        if let Some((vdata, pdata)) = vpdata_opt {
354            Self {
355                prover_data: Some(pdata),
356                verifier_data: Some(vdata),
357            }
358        } else {
359            Self {
360                prover_data: None,
361                verifier_data: None,
362            }
363        }
364    }
365
366    fn width(&self) -> Option<usize> {
367        self.prover_data.as_ref().map(|d| d.mat_view(0).width())
368    }
369}
370
371pub(crate) fn find_unused_vars<F: Field>(
372    constraints: &SymbolicConstraintsDag<F>,
373    width: &TraceWidth,
374    need_rot: bool,
375) -> Vec<SymbolicVariable<F>> {
376    let preprocessed_width = width.preprocessed.unwrap_or(0);
377    let mut preprocessed_present = vec![vec![false; 2]; preprocessed_width];
378
379    let mut main_present = vec![];
380    for width in width.main_widths() {
381        main_present.push(vec![vec![false; 2]; width]);
382    }
383
384    for node in &constraints.constraints.nodes {
385        let SymbolicExpressionNode::Variable(var) = node else {
386            continue;
387        };
388
389        match var.entry {
390            Entry::Preprocessed { offset } => {
391                preprocessed_present[var.index][offset] = true;
392            }
393            Entry::Main { part_index, offset } => {
394                main_present[part_index][var.index][offset] = true;
395            }
396            Entry::Public => {}
397            Entry::Challenge => unreachable!(),
398        }
399    }
400
401    let mut missing = vec![];
402    for (index, presents) in preprocessed_present.iter().enumerate() {
403        for (offset, present) in presents.iter().enumerate() {
404            if !present && (offset == 0 || need_rot) {
405                missing.push(SymbolicVariable::new(Entry::Preprocessed { offset }, index));
406            }
407        }
408    }
409    for (part_index, present_per_part) in main_present.iter().enumerate() {
410        for (index, presents) in present_per_part.iter().enumerate() {
411            for (offset, present) in presents.iter().enumerate() {
412                if !present && (offset == 0 || need_rot) {
413                    missing.push(SymbolicVariable::new(
414                        Entry::Main { part_index, offset },
415                        index,
416                    ));
417                }
418            }
419        }
420    }
421    missing
422}