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