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