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