use miden_core::field::{BasedVectorSpace, PrimeField64, QuadFelt};
use miden_crypto::{hash::poseidon2::Poseidon2, stark::pcs::PcsParams};
pub use p3_security::budget::{
AirShape, InstanceShape, LookupShape, ProtocolParams, SecurityReport, SecurityTerm,
};
use p3_security::{budget::report::LOOKUP_LABEL, fixed};
use crate::{
AIRS, ConstraintCounts, ConstraintDegrees, Felt, MidenAir, config,
constraints::lookup::messages::MIDEN_MAX_MESSAGE_WIDTH,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ProofSecurityParameters {
pub protocol_params: ProtocolParams,
pub log_final_degree: u32,
pub instance_shape: InstanceShape,
pub air_shape: AirShape,
pub num_ood_points: u32,
pub num_lookup_boundary_terms: u32,
}
pub const CHALLENGE_FIELD_BITS: u64 = EXTENSION_DEGREE as u64 * fixed::floor_log2(Felt::ORDER_U64);
const NUM_OOD_POINTS: u32 = 2;
const EXTENSION_DEGREE: usize = <QuadFelt as BasedVectorSpace<Felt>>::DIMENSION;
pub const COMMITMENT_ALIGNMENT: usize = config::SPONGE_RATE;
pub const AIR_SHAPE: AirShape = AirShape {
num_composed_constraints: 427,
max_constraint_degree: 9,
max_combo: NUM_OOD_POINTS,
num_deep_terms: Some(138),
lookup: LookupShape {
fractions_per_row: 28,
max_message_width: 16,
},
};
pub fn derive_air_shape() -> AirShape {
let mut num_constraints = 0;
let mut max_constraint_degree = 0;
let mut num_columns = 0;
let mut fractions_per_row = 0;
for air in AIRS {
num_constraints += ConstraintCounts::from_air::<Felt, QuadFelt, _>(&air).total();
max_constraint_degree =
max_constraint_degree.max(ConstraintDegrees::from_air::<Felt, QuadFelt, _>(&air).max());
num_columns += column_count(air, COMMITMENT_ALIGNMENT);
fractions_per_row += air.column_shape().iter().sum::<usize>();
}
num_columns += quotient_column_count(max_constraint_degree, COMMITMENT_ALIGNMENT);
AirShape {
num_composed_constraints: (num_constraints + AIRS.len() - 1) as u32,
max_constraint_degree: max_constraint_degree as u32,
max_combo: NUM_OOD_POINTS,
num_deep_terms: Some(num_columns as u32 + NUM_OOD_POINTS),
lookup: LookupShape {
fractions_per_row: fractions_per_row as u32,
max_message_width: MIDEN_MAX_MESSAGE_WIDTH as u32,
},
}
}
pub fn num_deep_terms(alignment: usize) -> u32 {
let mut num_columns = 0;
for air in AIRS {
num_columns += column_count(air, alignment);
}
num_columns += quotient_column_count(AIR_SHAPE.max_constraint_degree as usize, alignment);
num_columns as u32 + NUM_OOD_POINTS
}
fn column_count(air: MidenAir, alignment: usize) -> usize {
use miden_crypto::stark::air::{BaseAir, LiftedAir};
aligned(BaseAir::<Felt>::preprocessed_width(&air), alignment)
+ aligned(BaseAir::<Felt>::width(&air), alignment)
+ aligned(LiftedAir::<Felt, QuadFelt>::aux_width(&air) * EXTENSION_DEGREE, alignment)
}
fn quotient_column_count(max_constraint_degree: usize, alignment: usize) -> usize {
let chunks = max_constraint_degree.saturating_sub(1).max(1).next_power_of_two();
aligned(chunks * EXTENSION_DEGREE, alignment)
}
fn aligned(width: usize, alignment: usize) -> usize {
width.next_multiple_of(alignment)
}
pub const FIXED_POINT_FRACTIONAL_BITS: u32 = fixed::FRACTIONAL_BITS;
pub const FIXED_POINT_ONE: u64 = fixed::ONE;
pub const BITS_PER_QUERY: u64 =
fixed::bits_per_query(config::LOG_BLOWUP as u32, CHALLENGE_FIELD_BITS);
pub const COLLISION_RESISTANCE: u32 = Poseidon2::COLLISION_RESISTANCE;
pub const SECURITY_CAP: u64 = deployed_instance(0).cap();
pub const LOOKUP_COEFFICIENT: u64 = fixed::ceil_log2(
(AIR_SHAPE.lookup.max_message_width as u64 + 2) * AIR_SHAPE.lookup.fractions_per_row as u64,
);
pub const COMPOSITION_COEFFICIENT: u64 =
fixed::ceil_log2(AIR_SHAPE.num_composed_constraints as u64);
pub const OOD_COEFFICIENT: u64 =
fixed::ceil_log2(AIR_SHAPE.max_constraint_degree as u64 + AIR_SHAPE.max_combo as u64);
pub const DEEP_COEFFICIENT: u64 = fixed::ceil_log2(match AIR_SHAPE.num_deep_terms {
Some(n) => n as u64,
None => 0,
});
pub const FOLDING_COEFFICIENT: u64 = fixed::ceil_log2(2 * ((1 << config::LOG_FOLDING_ARITY) - 1));
pub const LOOKUP_POW_BITS: u32 = 0;
pub const LOOKUP_BASE: u64 = CHALLENGE_FIELD_BITS - LOOKUP_COEFFICIENT;
pub const COMPOSITION_TERM: u64 = CHALLENGE_FIELD_BITS - COMPOSITION_COEFFICIENT;
pub const OOD_BASE: u64 = CHALLENGE_FIELD_BITS - OOD_COEFFICIENT;
pub const DEEP_BASE: u64 = CHALLENGE_FIELD_BITS - DEEP_COEFFICIENT;
pub const FOLDING_BASE: u64 =
CHALLENGE_FIELD_BITS - FOLDING_COEFFICIENT - fixed::from_bits(config::LOG_BLOWUP as u32);
const fn deployed_instance(log_max_height: u32) -> InstanceShape {
InstanceShape {
log_max_height,
field_bits: CHALLENGE_FIELD_BITS,
collision_resistance: COLLISION_RESISTANCE,
}
}
pub const LOG2_E: u64 = fixed::LOG2_E;
pub const CORE_BOUNDARY_LOOKUP_TERMS: u32 = 3;
fn lookup_boundary_correction(
num_boundary_terms: u32,
fractions_per_row: u32,
log_max_height: u32,
) -> u64 {
if num_boundary_terms == 0 {
return 0;
}
assert!(fractions_per_row > 0, "lookup boundary terms require per-row lookup fractions");
let height = 1u64
.checked_shl(log_max_height)
.expect("maximum trace height must fit in a u64");
(u64::from(num_boundary_terms) * LOG2_E)
.div_ceil(u64::from(fractions_per_row))
.div_ceil(height)
}
fn apply_lookup_correction(report: SecurityReport, correction: u64) -> SecurityReport {
let terms = (*report.terms()).map(|term| {
if term.label == LOOKUP_LABEL {
SecurityTerm::new(term.label, term.bits.saturating_sub(correction))
} else {
term
}
});
SecurityReport::new(terms)
}
impl ProofSecurityParameters {
pub fn conjectured_security_report(&self) -> SecurityReport {
let report = p3_security::budget::security_report(
&self.protocol_params,
&self.instance_shape,
&self.air_shape,
);
let correction = lookup_boundary_correction(
self.num_lookup_boundary_terms,
self.air_shape.lookup.fractions_per_row,
self.instance_shape.log_max_height,
);
apply_lookup_correction(report, correction)
}
pub fn conjectured_security_level(&self) -> u32 {
self.conjectured_security_report().security_level()
}
}
pub fn proof_security_parameters(
pcs_params: &PcsParams,
log_max_height: u32,
num_kernel_procedures: u32,
alignment: usize,
collision_resistance: u32,
) -> ProofSecurityParameters {
mvm_security_parameters_from_protocol(
protocol_params(pcs_params),
u32::from(pcs_params.log_final_degree()),
log_max_height,
num_kernel_procedures,
alignment,
collision_resistance,
)
}
fn mvm_security_parameters_from_protocol(
protocol_params: ProtocolParams,
log_final_degree: u32,
log_max_height: u32,
num_kernel_procedures: u32,
alignment: usize,
collision_resistance: u32,
) -> ProofSecurityParameters {
ProofSecurityParameters {
protocol_params,
log_final_degree,
instance_shape: InstanceShape {
log_max_height,
field_bits: CHALLENGE_FIELD_BITS,
collision_resistance,
},
air_shape: AirShape {
num_deep_terms: Some(num_deep_terms(alignment)),
..AIR_SHAPE
},
num_ood_points: NUM_OOD_POINTS,
num_lookup_boundary_terms: CORE_BOUNDARY_LOOKUP_TERMS + num_kernel_procedures,
}
}
pub fn conjectured_security_level(
num_queries: u32,
query_pow_bits: u32,
deep_pow_bits: u32,
folding_pow_bits: u32,
log_max_height: u32,
num_kernel_procedures: u32,
) -> u32 {
let protocol = ProtocolParams {
log_blowup: config::LOG_BLOWUP as u32,
log_folding_arity: config::LOG_FOLDING_ARITY as u32,
num_queries,
query_pow_bits,
deep_pow_bits,
folding_pow_bits,
lookup_pow_bits: LOOKUP_POW_BITS,
};
mvm_security_parameters_from_protocol(
protocol,
u32::from(config::pcs_params().log_final_degree()),
log_max_height,
num_kernel_procedures,
COMMITMENT_ALIGNMENT,
COLLISION_RESISTANCE,
)
.conjectured_security_level()
}
pub fn conjectured_security_level_for_alignment(
num_queries: u32,
query_pow_bits: u32,
deep_pow_bits: u32,
folding_pow_bits: u32,
log_max_height: u32,
num_kernel_procedures: u32,
alignment: usize,
) -> u32 {
let protocol = ProtocolParams {
log_blowup: config::LOG_BLOWUP as u32,
log_folding_arity: config::LOG_FOLDING_ARITY as u32,
num_queries,
query_pow_bits,
deep_pow_bits,
folding_pow_bits,
lookup_pow_bits: LOOKUP_POW_BITS,
};
mvm_security_parameters_from_protocol(
protocol,
u32::from(config::pcs_params().log_final_degree()),
log_max_height,
num_kernel_procedures,
alignment,
COLLISION_RESISTANCE,
)
.conjectured_security_level()
}
pub fn protocol_params(params: &PcsParams) -> ProtocolParams {
ProtocolParams {
log_blowup: u32::from(params.log_blowup()),
log_folding_arity: u32::from(params.log_folding_arity()),
num_queries: params.num_queries() as u32,
query_pow_bits: params.query_pow_bits() as u32,
deep_pow_bits: params.deep_pow_bits() as u32,
folding_pow_bits: params.folding_pow_bits() as u32,
lookup_pow_bits: LOOKUP_POW_BITS,
}
}
pub fn security_report(
params: &ProtocolParams,
log_max_height: u32,
collision_resistance: u32,
num_kernel_procedures: u32,
) -> SecurityReport {
let instance = InstanceShape {
log_max_height,
field_bits: CHALLENGE_FIELD_BITS,
collision_resistance,
};
let report = p3_security::budget::security_report(params, &instance, &AIR_SHAPE);
let correction = lookup_boundary_correction(
CORE_BOUNDARY_LOOKUP_TERMS + num_kernel_procedures,
AIR_SHAPE.lookup.fractions_per_row,
log_max_height,
);
apply_lookup_correction(report, correction)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn air_shape_matches_symbolic() {
assert_eq!(AIR_SHAPE, derive_air_shape(), "AIR_SHAPE in security.rs is stale");
}
#[test]
fn num_deep_terms_matches_the_pinned_alignment() {
assert_eq!(num_deep_terms(COMMITMENT_ALIGNMENT), AIR_SHAPE.num_deep_terms.unwrap());
assert_eq!(num_deep_terms(1), 123, "Blake3 (alignment 1) DEEP term count moved");
assert_eq!(num_deep_terms(8), 138, "algebraic (alignment 8) DEEP term count moved");
assert_eq!(num_deep_terms(17), 172, "Keccak (alignment 17) DEEP term count moved");
}
#[test]
fn proof_security_parameters_match_mvm_security_report() {
let pcs_params = config::pcs_params();
let expected_protocol_params = protocol_params(&pcs_params);
let security_parameters = proof_security_parameters(
&pcs_params,
22,
255,
COMMITMENT_ALIGNMENT,
COLLISION_RESISTANCE,
);
assert_eq!(
security_parameters.conjectured_security_report(),
security_report(&expected_protocol_params, 22, COLLISION_RESISTANCE, 255)
);
assert_eq!(security_parameters.log_final_degree, u32::from(pcs_params.log_final_degree()));
assert_eq!(security_parameters.num_ood_points, NUM_OOD_POINTS);
}
#[test]
fn deployed_preset_grades_by_trace_height() {
let params = protocol_params(&config::pcs_params());
for (log_height, expected_level, expected_binding) in [
(20, 96, p3_security::budget::report::QUERY_LABEL),
(22, 96, p3_security::budget::report::QUERY_LABEL),
(24, 95, LOOKUP_LABEL),
(29, 90, LOOKUP_LABEL),
] {
let report = security_report(¶ms, log_height, 128, 0);
assert_eq!(
report.security_level(),
expected_level,
"level moved at log height {log_height}"
);
assert_eq!(
report.binding_term().label,
expected_binding,
"binding round moved at log height {log_height}"
);
}
}
#[test]
fn derived_security_constants_match_snapshot() {
const FP_SHIFT: u32 = 16;
const FP_ONE: u64 = 65_536;
const BITS_PER_QUERY_FP: u64 = 193_381;
const SECURITY_CAP_FP: u64 = 8_388_606;
const LOOKUP_BASE_FP: u64 = 7_800_270;
const COMPOSITION_TERM_FP: u64 = 7_815_946;
const OOD_BASE_FP: u64 = 8_161_888;
const DEEP_BASE_FP: u64 = 7_922_741;
const FOLDING_BASE_FP: u64 = 8_022_589;
const LOOKUP_POW_BITS_SNAPSHOT: u32 = 0;
assert_eq!(FIXED_POINT_FRACTIONAL_BITS, FP_SHIFT, "FP_SHIFT is stale");
assert_eq!(FIXED_POINT_ONE, FP_ONE, "FP_ONE is stale");
assert_eq!(BITS_PER_QUERY, BITS_PER_QUERY_FP, "BITS_PER_QUERY_FP is stale");
assert_eq!(SECURITY_CAP, SECURITY_CAP_FP, "SECURITY_CAP_FP is stale");
assert_eq!(LOOKUP_BASE, LOOKUP_BASE_FP, "LOOKUP_BASE_FP is stale");
assert_eq!(COMPOSITION_TERM, COMPOSITION_TERM_FP, "COMPOSITION_TERM_FP is stale");
assert_eq!(OOD_BASE, OOD_BASE_FP, "OOD_BASE_FP is stale");
assert_eq!(DEEP_BASE, DEEP_BASE_FP, "DEEP_BASE_FP is stale");
assert_eq!(FOLDING_BASE, FOLDING_BASE_FP, "FOLDING_BASE_FP is stale");
assert_eq!(
LOOKUP_POW_BITS, LOOKUP_POW_BITS_SNAPSHOT,
"Lifted STARK does not currently support lookup grinding"
);
}
#[test]
fn security_report_matches_reference_vectors() {
const VECTORS: &[((u32, u32, u32, u32, u32), [u64; 7], u32)] = &[
(
(27, 17, 12, 4, 6),
[7_406_895, 7_815_946, 7_776_509, 8_388_606, 7_891_517, 6_335_399, 8_388_606],
96,
),
(
(27, 17, 12, 4, 20),
[6_489_549, 7_815_946, 6_860_180, 8_388_606, 6_974_013, 6_335_399, 8_388_606],
96,
),
(
(27, 17, 12, 4, 23),
[6_292_941, 7_815_946, 6_663_572, 8_388_606, 6_777_405, 6_335_399, 8_388_606],
96,
),
(
(27, 17, 12, 4, 29),
[5_899_725, 7_815_946, 6_270_356, 8_388_606, 6_384_189, 6_335_399, 8_388_606],
90,
),
(
(7, 0, 0, 0, 20),
[6_489_549, 7_815_946, 6_860_180, 7_922_741, 6_711_869, 1_353_667, 8_388_606],
20,
),
(
(150, 31, 31, 31, 29),
[5_899_725, 7_815_946, 6_270_356, 8_388_606, 8_153_661, 8_388_606, 8_388_606],
90,
),
];
let base = protocol_params(&config::pcs_params());
for &(
(num_queries, query_pow_bits, deep_pow_bits, folding_pow_bits, log_height),
rounds,
level,
) in VECTORS
{
let params = ProtocolParams {
num_queries,
query_pow_bits,
deep_pow_bits,
folding_pow_bits,
..base
};
let report = security_report(¶ms, log_height, COLLISION_RESISTANCE, 0);
assert_eq!(
(*report.terms()).map(|term| term.bits),
rounds,
"round bits moved at {params:?}, log height {log_height}"
);
assert_eq!(
report.security_level(),
level,
"level moved at {params:?}, log height {log_height}"
);
}
}
#[test]
fn lookup_round_overtakes_the_query_phase_in_the_low_twenties() {
let params = protocol_params(&config::pcs_params());
let crossover = (6..=30)
.find(|&log_height| {
security_report(¶ms, log_height, 128, 0).binding_term().label == LOOKUP_LABEL
})
.expect("the lookup round must bind at some supported height");
assert_eq!(crossover, 23, "lookup/query crossover moved");
}
#[test]
fn lookup_boundary_correction_lowers_the_lookup_term_with_a_full_kernel_witness() {
let lookup_bits = |report: SecurityReport| {
report.terms().iter().find(|term| term.label == LOOKUP_LABEL).unwrap().bits
};
let params = protocol_params(&config::pcs_params());
let bare = lookup_bits(security_report(¶ms, 6, 128, 0));
let full_kernel = lookup_bits(security_report(¶ms, 6, 128, 255));
assert!(
full_kernel < bare,
"a full kernel witness should lower the lookup round's bound, got {full_kernel} vs \
{bare}"
);
}
}