Skip to main content

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(&params, 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(&params, 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(&params, 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(&params, 6, 128, 0));
699        let full_kernel = lookup_bits(security_report(&params, 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}