miden_air/security.rs
1//! Conjectured security level computation for the Miden VM STARK configuration.
2//!
3//! The AIR shape entering the security calculation is stored in [`AIR_SHAPE`], allowing the MASM
4//! estimator to use it without evaluating the AIRs symbolically. [`derive_air_shape`] performs
5//! that evaluation in Rust, and `air_shape_matches_symbolic` checks the stored value against it.
6
7use miden_core::field::{BasedVectorSpace, PrimeField64, QuadFelt};
8use miden_crypto::{hash::poseidon2::Poseidon2, stark::pcs::PcsParams};
9/// Security-estimation types used by verified Miden proofs.
10pub use p3_security::budget::{
11 AirShape, InstanceShape, LookupShape, ProtocolParams, SecurityReport, SecurityTerm,
12};
13use p3_security::{budget::report::LOOKUP_LABEL, fixed};
14
15use crate::{
16 AIRS, ConstraintCounts, ConstraintDegrees, Felt, MidenAir, config,
17 constraints::lookup::messages::MIDEN_MAX_MESSAGE_WIDTH,
18};
19
20/// Security parameters of a verified Miden STARK proof.
21///
22/// Native MVM and PVM verifiers return these parameters after deriving them from the proof, the
23/// commitment scheme used to verify it, and the AIR relation selected by the verifier. Callers
24/// pass the returned value to a security estimator and apply their own acceptance policy.
25/// Constructing this type directly does not authenticate its contents.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct ProofSecurityParameters {
28 /// Protocol parameters bound by the proof transcript.
29 pub protocol_params: ProtocolParams,
30 /// Log2 of the configured final FRI polynomial degree.
31 pub log_final_degree: u32,
32 /// Instance shape derived from the proof and its commitment scheme.
33 pub instance_shape: InstanceShape,
34 /// Security-relevant shape of the AIR relation and commitment scheme.
35 pub air_shape: AirShape,
36 /// Number of out-of-domain points opened per committed column.
37 pub num_ood_points: u32,
38 /// Lookup fractions consumed once per proof in addition to the per-row fractions.
39 pub num_lookup_boundary_terms: u32,
40}
41
42/// Conservative Q16 lower bound on the log2 of the challenge-field cardinality.
43///
44/// The challenge field is the quadratic extension of the Goldilocks base field. This value doubles
45/// the rounded-down Q16 value for the base field. Rounding before doubling keeps the result
46/// conservative.
47pub const CHALLENGE_FIELD_BITS: u64 = EXTENSION_DEGREE as u64 * fixed::floor_log2(Felt::ORDER_U64);
48
49/// Number of out-of-domain points opened per committed column.
50///
51/// The AIRs use `local` and `next` rotations only.
52const NUM_OOD_POINTS: u32 = 2;
53
54/// Base field elements per challenge-field element.
55const EXTENSION_DEGREE: usize = <QuadFelt as BasedVectorSpace<Felt>>::DIMENSION;
56
57/// Column alignment of the commitment scheme, in base field elements.
58///
59/// The commitment sponge absorbs whole rates, so a committed matrix is padded up to a multiple of
60/// the rate.
61pub const COMMITMENT_ALIGNMENT: usize = config::SPONGE_RATE;
62
63/// Shape of the Miden VM multi-AIR statement used by the security estimator.
64///
65/// This is stored rather than derived during verification. `air_shape_matches_symbolic` checks it
66/// against the shape obtained by symbolically evaluating the AIRs.
67pub const AIR_SHAPE: AirShape = AirShape {
68 num_composed_constraints: 428,
69 max_constraint_degree: 9,
70 num_quotient_chunks: 8,
71 max_combo: NUM_OOD_POINTS,
72 num_deep_terms: Some(138),
73 lookup: Some(LOOKUP_SHAPE),
74};
75
76/// Lookup argument shape of the Miden VM multi-AIR statement, as stored in [`AIR_SHAPE`].
77pub const LOOKUP_SHAPE: LookupShape = LookupShape {
78 fractions_per_row: 28,
79 max_message_width: 16,
80};
81
82/// Computes the AIR shape by symbolically evaluating every AIR in the statement.
83///
84/// Tests compare [`AIR_SHAPE`] with this result. The symbolic pass allocates and evaluates every
85/// AIR, so verifiers use the checked constant instead of calling this function.
86pub fn derive_air_shape() -> AirShape {
87 let mut num_constraints = 0;
88 let mut max_constraint_degree = 0;
89 let mut num_columns = 0;
90 let mut fractions_per_row = 0;
91
92 for air in AIRS {
93 num_constraints += ConstraintCounts::from_air::<Felt, QuadFelt, _>(&air).total();
94 max_constraint_degree =
95 max_constraint_degree.max(ConstraintDegrees::from_air::<Felt, QuadFelt, _>(&air).max());
96 num_columns += column_count(air, COMMITMENT_ALIGNMENT);
97 fractions_per_row += air.column_shape().iter().sum::<usize>();
98 }
99 num_columns += quotient_column_count(max_constraint_degree, COMMITMENT_ALIGNMENT);
100
101 AirShape {
102 // One batching slot per AIR beyond the first sits alongside the constraints themselves:
103 // constraints are folded by powers of one challenge and the AIRs by a second, so a
104 // single-AIR statement needs no cross-AIR batching challenge.
105 num_composed_constraints: (num_constraints + AIRS.len() - 1) as u32,
106 max_constraint_degree: max_constraint_degree as u32,
107 num_quotient_chunks: quotient_chunk_count(max_constraint_degree) as u32,
108 max_combo: NUM_OOD_POINTS,
109 num_deep_terms: Some(num_columns as u32 + NUM_OOD_POINTS),
110 lookup: Some(LookupShape {
111 fractions_per_row: fractions_per_row as u32,
112 max_message_width: MIDEN_MAX_MESSAGE_WIDTH as u32,
113 }),
114 }
115}
116
117/// Number of DEEP-quotient batching terms for a commitment scheme with the given column
118/// alignment, holding every other AIR shape input fixed at [`AIR_SHAPE`]'s stored values.
119///
120/// Only the per-column padding is alignment-dependent, so this recomputes committed column counts
121/// from the AIRs' own width accessors — no symbolic constraint pass — reusing
122/// `AIR_SHAPE::max_constraint_degree` for the quotient group's chunk count. A native verifier
123/// computing the security level of a proof committed under a different LMCS (Blake3, alignment 1;
124/// Keccak, alignment 17) calls this instead of using the alignment-8 [`AIR_SHAPE`], which is fixed
125/// for the Poseidon2-only recursive verifier.
126pub fn num_deep_terms(alignment: usize) -> u32 {
127 let mut num_columns = 0;
128 for air in AIRS {
129 num_columns += column_count(air, alignment);
130 }
131 num_columns += quotient_column_count(AIR_SHAPE.max_constraint_degree as usize, alignment);
132
133 num_columns as u32 + NUM_OOD_POINTS
134}
135
136/// Committed base columns for one AIR: preprocessed, main, and auxiliary traces, each its own
137/// matrix within its commitment group and so each padded on its own.
138fn column_count(air: MidenAir, alignment: usize) -> usize {
139 use miden_crypto::stark::air::{BaseAir, LiftedAir};
140
141 aligned(BaseAir::<Felt>::preprocessed_width(&air), alignment)
142 + aligned(BaseAir::<Felt>::width(&air), alignment)
143 + aligned(LiftedAir::<Felt, QuadFelt>::aux_width(&air) * EXTENSION_DEGREE, alignment)
144}
145
146/// Committed base columns in the quotient group: one chunk per unit of degree above the vanishing
147/// polynomial, rounded up to a power of two, committed as a single extension-valued matrix.
148fn quotient_column_count(max_constraint_degree: usize, alignment: usize) -> usize {
149 aligned(quotient_chunk_count(max_constraint_degree) * EXTENSION_DEGREE, alignment)
150}
151
152/// Committed quotient chunks: one per unit of degree above the vanishing polynomial, rounded up to
153/// a power of two.
154fn quotient_chunk_count(max_constraint_degree: usize) -> usize {
155 max_constraint_degree.saturating_sub(1).max(1).next_power_of_two()
156}
157
158/// Pads a committed width up to the commitment scheme's column alignment.
159///
160/// The DEEP reduction batches every element of each opened, alignment-padded row, so padding also
161/// contributes batching slots.
162fn aligned(width: usize, alignment: usize) -> usize {
163 width.next_multiple_of(alignment)
164}
165
166// SECURITY MODEL CONSTANTS
167// ================================================================================================
168//
169// The MASM recursive estimator consumes the raw AIR shape. Tests in
170// `crates/lib/core/tests/stark/security.rs` compare it with the native calculation over the ranges
171// accepted by the recursive verifiers. `derived_security_constants_match_snapshot` checks the
172// native constants independently.
173
174/// Fractional bits in the fixed-point representation shared with the MASM estimator.
175pub const FIXED_POINT_FRACTIONAL_BITS: u32 = fixed::FRACTIONAL_BITS;
176
177/// Fixed-point representation of one, shared with the MASM estimator.
178pub const FIXED_POINT_ONE: u64 = fixed::ONE;
179
180/// Conjectured security contributed per FRI query, in fixed point.
181pub const BITS_PER_QUERY: u64 =
182 fixed::bits_per_query(config::LOG_BLOWUP as u32, CHALLENGE_FIELD_BITS);
183
184/// Collision resistance of the Poseidon2 commitment used by the recursive verifier.
185pub const COLLISION_RESISTANCE: u32 = Poseidon2::COLLISION_RESISTANCE;
186
187/// Upper bound on every reported level, in fixed point.
188pub const SECURITY_CAP: u64 = deployed_instance(0).cap();
189
190/// Q16 upper bound on the log2 of the lookup round's error coefficient.
191pub const LOOKUP_COEFFICIENT: u64 = fixed::ceil_log2(
192 (LOOKUP_SHAPE.max_message_width as u64 + 2) * LOOKUP_SHAPE.fractions_per_row as u64,
193);
194
195/// Q16 upper bound on the log2 of the constraint-composition round's error coefficient.
196pub const COMPOSITION_COEFFICIENT: u64 =
197 fixed::ceil_log2(AIR_SHAPE.num_composed_constraints as u64);
198
199/// Q16 upper bound on the log2 of the out-of-domain round's error coefficient.
200///
201/// The round's error size is `max(d * (H + combo - 1) + (H - 1), (c + 1) * H + combo - 1)` for a
202/// trace height `H`, maximum constraint degree `d`, out-of-domain point count `combo`, and
203/// quotient chunk count `c <= d`. At `combo = 2` that is at most `(d + 1) * H + (d - 1)`, which
204/// `(d + 2) * H` bounds for every trace height at or above `d - 1`. Dividing out `H` leaves this
205/// height-independent coefficient, so `OOD_BASE - log_max_height` stays a lower bound on the
206/// round.
207pub const OOD_COEFFICIENT: u64 =
208 fixed::ceil_log2(AIR_SHAPE.max_constraint_degree as u64 + AIR_SHAPE.max_combo as u64);
209
210/// Q16 upper bound on the log2 of the DEEP round's error coefficient.
211pub const DEEP_COEFFICIENT: u64 = fixed::ceil_log2(match AIR_SHAPE.num_deep_terms {
212 Some(n) => n as u64,
213 None => 0,
214});
215
216/// Q16 upper bound on the log2 of the FRI folding round's error coefficient.
217pub const FOLDING_COEFFICIENT: u64 = fixed::ceil_log2(2 * ((1 << config::LOG_FOLDING_ARITY) - 1));
218
219/// Grinding applied before the out-of-domain point is sampled.
220///
221/// Lifted STARK samples the point directly after the quotient commitment; its DEEP grinding runs
222/// only after the out-of-domain evaluations are bound.
223pub const OOD_POW_BITS: u32 = 0;
224
225/// Lookup grinding applied before the lookup challenges are sampled.
226///
227/// Lifted STARK currently samples them directly after the main-trace commitment and exposes no
228/// lookup-grinding parameter.
229pub const LOOKUP_POW_BITS: u32 = 0;
230
231/// The configured challenge-field bound less the lookup round's coefficient, in fixed point.
232pub const LOOKUP_BASE: u64 = CHALLENGE_FIELD_BITS - LOOKUP_COEFFICIENT;
233
234/// The configured challenge-field bound less the constraint-composition round's coefficient, in
235/// fixed point.
236pub const COMPOSITION_TERM: u64 = CHALLENGE_FIELD_BITS - COMPOSITION_COEFFICIENT;
237
238/// The configured challenge-field bound less the out-of-domain round's coefficient, in fixed
239/// point.
240pub const OOD_BASE: u64 = CHALLENGE_FIELD_BITS - OOD_COEFFICIENT;
241
242/// The configured challenge-field bound less the DEEP round's coefficient, in fixed point.
243pub const DEEP_BASE: u64 = CHALLENGE_FIELD_BITS - DEEP_COEFFICIENT;
244
245/// The configured challenge-field bound less the FRI folding round's coefficient and fixed
246/// blowup, in fixed point.
247///
248/// The common MASM estimator uses the whole-bit floor of this value when proving that FRI folding
249/// cannot determine the result. Drift tests keep the MASM constant used by that proof synchronized
250/// with this value.
251pub const FOLDING_BASE: u64 =
252 CHALLENGE_FIELD_BITS - FOLDING_COEFFICIENT - fixed::from_bits(config::LOG_BLOWUP as u32);
253
254/// The instance shape of a deployed Miden VM proof at the given maximum AIR log height.
255const fn deployed_instance(log_max_height: u32) -> InstanceShape {
256 InstanceShape {
257 log_max_height,
258 field_bits: CHALLENGE_FIELD_BITS,
259 collision_resistance: COLLISION_RESISTANCE,
260 }
261}
262
263/// `log2(e)`, rounded up, in fixed point. Matches the common MASM estimator's `LOG2_E_FP`.
264pub const LOG2_E: u64 = fixed::LOG2_E;
265
266/// Number of lookup fractions `emit_core_boundary` emits unconditionally: the block-hash seed and
267/// the two log-deferred-root terminals. Matches `sys::vm::mod.masm`'s
268/// `CORE_BOUNDARY_LOOKUP_TERMS`.
269pub const CORE_BOUNDARY_LOOKUP_TERMS: u32 = 3;
270
271/// Upper bound on `log2(1 + boundary / (fractions_per_row · 2^log_max_height))`, in fixed point,
272/// via `log2(1 + x) <= x · log2(e)`.
273///
274/// `num_boundary_terms` is the number of one-time lookup fractions consumed on top of the
275/// per-row terms counted by `fractions_per_row`. Both divisions round up, so the correction is
276/// never smaller than the true log term, keeping the corrected round conservative. The two-step
277/// division order (first by `fractions_per_row`, then by `2^log_max_height`) is what the common
278/// MASM estimator mirrors bit-for-bit: a single combined divisor overflows a `u32` at the deployed
279/// shape's larger heights.
280fn lookup_boundary_correction(
281 num_boundary_terms: u32,
282 fractions_per_row: u32,
283 log_max_height: u32,
284) -> u64 {
285 if num_boundary_terms == 0 {
286 return 0;
287 }
288 assert!(fractions_per_row > 0, "lookup boundary terms require per-row lookup fractions");
289 let height = 1u64
290 .checked_shl(log_max_height)
291 .expect("maximum trace height must fit in a u64");
292 (u64::from(num_boundary_terms) * LOG2_E)
293 .div_ceil(u64::from(fractions_per_row))
294 .div_ceil(height)
295}
296
297fn apply_lookup_correction(report: SecurityReport, correction: u64) -> SecurityReport {
298 let terms = (*report.terms()).map(|term| {
299 if term.label == LOOKUP_LABEL {
300 SecurityTerm::new(term.label, term.bits.saturating_sub(correction))
301 } else {
302 term
303 }
304 });
305 SecurityReport::new(terms)
306}
307
308impl ProofSecurityParameters {
309 /// Computes the conjectured security report for the verified proof.
310 ///
311 /// The same estimator handles MVM and PVM proofs because the parameters include the protocol,
312 /// instance, and AIR shapes. Callers must use parameters returned by the verifier that
313 /// authenticated the proof rather than values assembled independently.
314 pub fn conjectured_security_report(&self) -> SecurityReport {
315 let report = p3_security::budget::security_report(
316 &self.protocol_params,
317 &self.instance_shape,
318 &self.air_shape,
319 );
320 let correction = lookup_boundary_correction(
321 self.num_lookup_boundary_terms,
322 self.air_shape.lookup.map_or(0, |lookup| lookup.fractions_per_row),
323 self.instance_shape.log_max_height,
324 );
325 apply_lookup_correction(report, correction)
326 }
327
328 /// Returns the conjectured security level for the verified proof.
329 pub fn conjectured_security_level(&self) -> u32 {
330 self.conjectured_security_report().security_level()
331 }
332}
333
334/// Builds MVM security parameters from values obtained during proof verification.
335///
336/// `log_max_height` and `alignment` must come from successful STARK verification,
337/// `num_kernel_procedures` from the authenticated execution claim, and `collision_resistance`
338/// from the commitment hash used to verify the proof.
339pub fn proof_security_parameters(
340 pcs_params: &PcsParams,
341 log_max_height: u32,
342 num_kernel_procedures: u32,
343 alignment: usize,
344 collision_resistance: u32,
345) -> ProofSecurityParameters {
346 mvm_security_parameters_from_protocol(
347 protocol_params(pcs_params),
348 u32::from(pcs_params.log_final_degree()),
349 log_max_height,
350 num_kernel_procedures,
351 alignment,
352 collision_resistance,
353 )
354}
355
356fn mvm_security_parameters_from_protocol(
357 protocol_params: ProtocolParams,
358 log_final_degree: u32,
359 log_max_height: u32,
360 num_kernel_procedures: u32,
361 alignment: usize,
362 collision_resistance: u32,
363) -> ProofSecurityParameters {
364 ProofSecurityParameters {
365 protocol_params,
366 log_final_degree,
367 instance_shape: InstanceShape {
368 log_max_height,
369 field_bits: CHALLENGE_FIELD_BITS,
370 collision_resistance,
371 },
372 air_shape: AirShape {
373 num_deep_terms: Some(num_deep_terms(alignment)),
374 ..AIR_SHAPE
375 },
376 num_ood_points: NUM_OOD_POINTS,
377 num_lookup_boundary_terms: CORE_BOUNDARY_LOOKUP_TERMS + num_kernel_procedures,
378 }
379}
380
381/// Computes a Poseidon2 Miden VM proof's conjectured security level, in whole bits.
382///
383/// The Fiat-Shamir transcript binds the PCS parameters and AIR log heights. The authenticated
384/// kernel witness determines the kernel procedure count. The remaining inputs are fixed by the
385/// deployed AIR and commitment configuration. The result therefore describes the proof and claim
386/// that were verified rather than an independently supplied parameter preset.
387///
388/// Mirrored bit-for-bit by the common MASM estimator when supplied with the MVM descriptor. The
389/// recursive verifier admits only 7..=150 queries, 0..=31 query/DEEP/folding grinding bits, fixed
390/// zero lookup grinding, log trace height in `6..=29`, and 0..=255 kernel procedures. This function
391/// also accepts configurations outside that domain; such inputs are not part of the recursive
392/// estimator's contract.
393pub fn conjectured_security_level(
394 num_queries: u32,
395 query_pow_bits: u32,
396 deep_pow_bits: u32,
397 folding_pow_bits: u32,
398 log_max_height: u32,
399 num_kernel_procedures: u32,
400) -> u32 {
401 let protocol = ProtocolParams {
402 log_blowup: config::LOG_BLOWUP as u32,
403 log_folding_arity: config::LOG_FOLDING_ARITY as u32,
404 num_queries,
405 query_pow_bits,
406 ood_pow_bits: OOD_POW_BITS,
407 deep_pow_bits,
408 folding_pow_bits,
409 lookup_pow_bits: LOOKUP_POW_BITS,
410 };
411 mvm_security_parameters_from_protocol(
412 protocol,
413 u32::from(config::pcs_params().log_final_degree()),
414 log_max_height,
415 num_kernel_procedures,
416 COMMITMENT_ALIGNMENT,
417 COLLISION_RESISTANCE,
418 )
419 .conjectured_security_level()
420}
421
422/// Computes a deployed Miden VM proof's conjectured security level, in whole bits, for a proof
423/// committed under a commitment scheme with the given column alignment.
424///
425/// Every AIR shape input but `num_deep_terms` is alignment-independent, so this reuses
426/// [`AIR_SHAPE`] otherwise. Not mirrored in MASM: the recursive verifier accepts only Poseidon2
427/// proofs, which `conjectured_security_level` computes at alignment
428/// [`COMMITMENT_ALIGNMENT`] (and this function is identical at that alignment, since
429/// `num_deep_terms(COMMITMENT_ALIGNMENT)` equals `AIR_SHAPE.num_deep_terms` —
430/// `num_deep_terms_matches_the_pinned_alignment` checks it). This helper assumes the commitment
431/// scheme has [`COLLISION_RESISTANCE`] bits; verification returns [`ProofSecurityParameters`] built
432/// with the collision resistance of the proof's actual hash function.
433pub fn conjectured_security_level_for_alignment(
434 num_queries: u32,
435 query_pow_bits: u32,
436 deep_pow_bits: u32,
437 folding_pow_bits: u32,
438 log_max_height: u32,
439 num_kernel_procedures: u32,
440 alignment: usize,
441) -> u32 {
442 let protocol = ProtocolParams {
443 log_blowup: config::LOG_BLOWUP as u32,
444 log_folding_arity: config::LOG_FOLDING_ARITY as u32,
445 num_queries,
446 query_pow_bits,
447 ood_pow_bits: OOD_POW_BITS,
448 deep_pow_bits,
449 folding_pow_bits,
450 lookup_pow_bits: LOOKUP_POW_BITS,
451 };
452 mvm_security_parameters_from_protocol(
453 protocol,
454 u32::from(config::pcs_params().log_final_degree()),
455 log_max_height,
456 num_kernel_procedures,
457 alignment,
458 COLLISION_RESISTANCE,
459 )
460 .conjectured_security_level()
461}
462
463/// Maps PCS parameters onto the protocol parameters the round budget reads.
464///
465/// The transcript observes every field of [`PcsParams`], so computing a proof's security level
466/// under these parameters uses the parameters it was actually produced with.
467pub fn protocol_params(params: &PcsParams) -> ProtocolParams {
468 ProtocolParams {
469 log_blowup: u32::from(params.log_blowup()),
470 log_folding_arity: u32::from(params.log_folding_arity()),
471 num_queries: params.num_queries() as u32,
472 query_pow_bits: params.query_pow_bits() as u32,
473 ood_pow_bits: OOD_POW_BITS,
474 deep_pow_bits: params.deep_pow_bits() as u32,
475 folding_pow_bits: params.folding_pow_bits() as u32,
476 // The protocol samples the lookup challenges directly after the main-trace commitment,
477 // with no grinding in between.
478 lookup_pow_bits: LOOKUP_POW_BITS,
479 }
480}
481
482/// Computes the conjectured security level of a Miden VM statement proof, for each protocol
483/// round.
484///
485/// `log_max_height` is the largest AIR trace height in the proof; the Fiat-Shamir transcript binds
486/// every AIR's log height, so a prover cannot understate it to inflate the reported level.
487/// `collision_resistance` is that of the commitment hash, in bits. `num_kernel_procedures` is the
488/// proof's kernel procedure count, transcript-bound through the kernel witness.
489pub fn security_report(
490 params: &ProtocolParams,
491 log_max_height: u32,
492 collision_resistance: u32,
493 num_kernel_procedures: u32,
494) -> SecurityReport {
495 let instance = InstanceShape {
496 log_max_height,
497 field_bits: CHALLENGE_FIELD_BITS,
498 collision_resistance,
499 };
500 let report = p3_security::budget::security_report(params, &instance, &AIR_SHAPE);
501 let correction = lookup_boundary_correction(
502 CORE_BOUNDARY_LOOKUP_TERMS + num_kernel_procedures,
503 LOOKUP_SHAPE.fractions_per_row,
504 log_max_height,
505 );
506 apply_lookup_correction(report, correction)
507}
508
509#[cfg(test)]
510mod tests {
511 use super::*;
512
513 /// Checks that [`AIR_SHAPE`] matches the current AIRs. A stale shape can make the reported
514 /// security level differ from the level implied by the relation being verified.
515 #[test]
516 fn air_shape_matches_symbolic() {
517 assert_eq!(AIR_SHAPE, derive_air_shape(), "AIR_SHAPE in security.rs is stale");
518 }
519
520 /// `num_deep_terms` at [`COMMITMENT_ALIGNMENT`] (algebraic sponges) must reproduce
521 /// [`AIR_SHAPE`]'s stored `num_deep_terms` exactly, so
522 /// `conjectured_security_level_for_alignment` computes the same level for a Poseidon2 proof
523 /// as `conjectured_security_level`.
524 ///
525 /// The other two are the deployed non-algebraic configurations' actual alignments: Blake3's
526 /// `ChainingHasher` (1, no padding) and Keccak's `SerializingStatefulSponge` over its 17-word
527 /// rate (`lcm(8, 17·8)/8 = 17`).
528 #[test]
529 fn num_deep_terms_matches_the_pinned_alignment() {
530 assert_eq!(num_deep_terms(COMMITMENT_ALIGNMENT), AIR_SHAPE.num_deep_terms.unwrap());
531 assert_eq!(num_deep_terms(1), 123, "Blake3 (alignment 1) DEEP term count moved");
532 assert_eq!(num_deep_terms(8), 138, "algebraic (alignment 8) DEEP term count moved");
533 assert_eq!(num_deep_terms(17), 172, "Keccak (alignment 17) DEEP term count moved");
534 }
535
536 /// Parameters built for an MVM proof must reproduce the independent MVM security report.
537 #[test]
538 fn proof_security_parameters_match_mvm_security_report() {
539 let pcs_params = config::pcs_params();
540 let expected_protocol_params = protocol_params(&pcs_params);
541 let security_parameters = proof_security_parameters(
542 &pcs_params,
543 22,
544 255,
545 COMMITMENT_ALIGNMENT,
546 COLLISION_RESISTANCE,
547 );
548
549 assert_eq!(
550 security_parameters.conjectured_security_report(),
551 security_report(&expected_protocol_params, 22, COLLISION_RESISTANCE, 255)
552 );
553 assert_eq!(security_parameters.log_final_degree, u32::from(pcs_params.log_final_degree()));
554 assert_eq!(security_parameters.num_ood_points, NUM_OOD_POINTS);
555 }
556
557 /// The deployed preset's computed security level, per trace height, with the round that
558 /// determines it at each. The preset was calibrated against the query phase alone; this test
559 /// checks what it actually computes once the trace-height-dependent rounds are counted, so any
560 /// parameter or AIR change that moves the real figure is visible rather than absorbed into an
561 /// unchanged constant.
562 #[test]
563 fn deployed_preset_grades_by_trace_height() {
564 let params = protocol_params(&config::pcs_params());
565
566 for (log_height, expected_level, expected_binding) in [
567 (20, 96, p3_security::budget::report::QUERY_LABEL),
568 (22, 96, p3_security::budget::report::QUERY_LABEL),
569 (24, 95, LOOKUP_LABEL),
570 (29, 90, LOOKUP_LABEL),
571 ] {
572 let report = security_report(¶ms, log_height, 128, 0);
573 assert_eq!(
574 report.security_level(),
575 expected_level,
576 "level moved at log height {log_height}"
577 );
578 assert_eq!(
579 report.binding_term().label,
580 expected_binding,
581 "binding round moved at log height {log_height}"
582 );
583 }
584 }
585
586 /// Every derived Rust security constant, checked against a fixed numeric snapshot.
587 ///
588 /// This test does not read the MASM source; it checks that the Rust-side values below have not
589 /// silently drifted from the reviewed snapshot.
590 #[test]
591 fn derived_security_constants_match_snapshot() {
592 const FP_SHIFT: u32 = 16;
593 const FP_ONE: u64 = 65_536;
594 const BITS_PER_QUERY_FP: u64 = 193_381;
595 const SECURITY_CAP_FP: u64 = 8_388_606;
596 const LOOKUP_BASE_FP: u64 = 7_800_270;
597 const COMPOSITION_TERM_FP: u64 = 7_815_725;
598 const OOD_BASE_FP: u64 = 8_161_888;
599 const DEEP_BASE_FP: u64 = 7_922_741;
600 const FOLDING_BASE_FP: u64 = 8_022_589;
601 const LOOKUP_POW_BITS_SNAPSHOT: u32 = 0;
602
603 assert_eq!(FIXED_POINT_FRACTIONAL_BITS, FP_SHIFT, "FP_SHIFT is stale");
604 assert_eq!(FIXED_POINT_ONE, FP_ONE, "FP_ONE is stale");
605 assert_eq!(BITS_PER_QUERY, BITS_PER_QUERY_FP, "BITS_PER_QUERY_FP is stale");
606 assert_eq!(SECURITY_CAP, SECURITY_CAP_FP, "SECURITY_CAP_FP is stale");
607 assert_eq!(LOOKUP_BASE, LOOKUP_BASE_FP, "LOOKUP_BASE_FP is stale");
608 assert_eq!(COMPOSITION_TERM, COMPOSITION_TERM_FP, "COMPOSITION_TERM_FP is stale");
609 assert_eq!(OOD_BASE, OOD_BASE_FP, "OOD_BASE_FP is stale");
610 assert_eq!(DEEP_BASE, DEEP_BASE_FP, "DEEP_BASE_FP is stale");
611 assert_eq!(FOLDING_BASE, FOLDING_BASE_FP, "FOLDING_BASE_FP is stale");
612 assert_eq!(
613 LOOKUP_POW_BITS, LOOKUP_POW_BITS_SNAPSHOT,
614 "Lifted STARK does not currently support lookup grinding"
615 );
616 }
617
618 /// Checks every round against values computed independently from its documented formula.
619 ///
620 /// Final-level and monotonicity tests do not expose an error in a term that never determines
621 /// the minimum. These vectors therefore include parameters that move the query, DEEP, and
622 /// FRI folding terms away from the security cap and make their individual values
623 /// observable.
624 #[test]
625 fn security_report_matches_reference_vectors() {
626 // (queries, query PoW, DEEP PoW, folding PoW, log height)
627 // -> [lookup, composition, ood, deep, folding, query, collision], level
628 const VECTORS: &[((u32, u32, u32, u32, u32), [u64; 7], u32)] = &[
629 (
630 (27, 17, 12, 4, 6),
631 [7_406_895, 7_815_725, 7_776_509, 8_119_349, 7_891_517, 6_335_399, 8_388_606],
632 96,
633 ),
634 (
635 (27, 17, 12, 4, 20),
636 [6_489_549, 7_815_725, 6_860_180, 7_201_845, 6_974_013, 6_335_399, 8_388_606],
637 96,
638 ),
639 (
640 (27, 17, 12, 4, 23),
641 [6_292_941, 7_815_725, 6_663_572, 7_005_237, 6_777_405, 6_335_399, 8_388_606],
642 96,
643 ),
644 (
645 (27, 17, 12, 4, 29),
646 [5_899_725, 7_815_725, 6_270_356, 6_612_021, 6_384_189, 6_335_399, 8_388_606],
647 90,
648 ),
649 (
650 (7, 0, 0, 0, 20),
651 [6_489_549, 7_815_725, 6_860_180, 6_415_413, 6_711_869, 1_353_667, 8_388_606],
652 20,
653 ),
654 (
655 (150, 31, 31, 31, 29),
656 [5_899_725, 7_815_725, 6_270_356, 7_857_205, 8_153_661, 8_388_606, 8_388_606],
657 90,
658 ),
659 ];
660
661 let base = protocol_params(&config::pcs_params());
662 for &(
663 (num_queries, query_pow_bits, deep_pow_bits, folding_pow_bits, log_height),
664 rounds,
665 level,
666 ) in VECTORS
667 {
668 let params = ProtocolParams {
669 num_queries,
670 query_pow_bits,
671 deep_pow_bits,
672 folding_pow_bits,
673 ..base
674 };
675 let report = security_report(¶ms, log_height, COLLISION_RESISTANCE, 0);
676
677 assert_eq!(
678 (*report.terms()).map(|term| term.bits),
679 rounds,
680 "round bits moved at {params:?}, log height {log_height}"
681 );
682 assert_eq!(
683 report.security_level(),
684 level,
685 "level moved at {params:?}, log height {log_height}"
686 );
687 }
688 }
689
690 /// The lookup round overtakes the query phase as the bottleneck somewhere in the low twenties,
691 /// which is what makes the computed security level height-dependent at all. This test checks
692 /// the crossover height against a fixed value: below it the preset reaches its design target,
693 /// above it it does not.
694 #[test]
695 fn lookup_round_overtakes_the_query_phase_in_the_low_twenties() {
696 let params = protocol_params(&config::pcs_params());
697 let crossover = (6..=30)
698 .find(|&log_height| {
699 security_report(¶ms, log_height, 128, 0).binding_term().label == LOOKUP_LABEL
700 })
701 .expect("the lookup round must bind at some supported height");
702
703 assert_eq!(crossover, 23, "lookup/query crossover moved");
704 }
705
706 /// A proof with the maximum kernel witness reports a lower lookup-round bound than a bare one
707 /// at the same height, since `emit_chiplets_boundary` adds one lookup fraction per kernel
708 /// procedure digest on top of the per-row bus terms `AIR_SHAPE` counts.
709 #[test]
710 fn lookup_boundary_correction_lowers_the_lookup_term_with_a_full_kernel_witness() {
711 let lookup_bits = |report: SecurityReport| {
712 report.terms().iter().find(|term| term.label == LOOKUP_LABEL).unwrap().bits
713 };
714
715 let params = protocol_params(&config::pcs_params());
716 let bare = lookup_bits(security_report(¶ms, 6, 128, 0));
717 let full_kernel = lookup_bits(security_report(¶ms, 6, 128, 255));
718 assert!(
719 full_kernel < bare,
720 "a full kernel witness should lower the lookup round's bound, got {full_kernel} vs \
721 {bare}"
722 );
723 }
724}