pub mod report;
pub mod shape;
use report::{
COLLISION_LABEL, COMPOSITION_LABEL, DEEP_COMPOSITION_LABEL, FOLDING_LABEL, LOOKUP_LABEL,
OUT_OF_DOMAIN_LABEL, QUERY_LABEL,
};
pub use report::{SecurityReport, SecurityTerm};
pub use shape::{AirShape, InstanceShape, LookupShape, ProtocolParams};
use crate::fixed;
pub const fn security_report(
params: &ProtocolParams,
instance: &InstanceShape,
air: &AirShape,
) -> SecurityReport {
let cap = instance.cap();
if cap == 0 {
return SecurityReport::new([
SecurityTerm::new(LOOKUP_LABEL, 0),
SecurityTerm::new(COMPOSITION_LABEL, 0),
SecurityTerm::new(OUT_OF_DOMAIN_LABEL, 0),
SecurityTerm::new(DEEP_COMPOSITION_LABEL, 0),
SecurityTerm::new(FOLDING_LABEL, 0),
SecurityTerm::new(QUERY_LABEL, 0),
SecurityTerm::new(COLLISION_LABEL, 0),
]);
}
let lookup = round(
LOOKUP_LABEL,
instance,
match air.lookup {
Some(lookup) => {
Some((lookup.max_message_width as u64 + 2) * lookup.fractions_per_row as u64)
}
None => None,
},
instance.log_max_height,
params.lookup_pow_bits,
cap,
);
let composition = round(
COMPOSITION_LABEL,
instance,
Some(air.num_composed_constraints as u64),
0,
0,
cap,
);
let out_of_domain = out_of_domain_round(
instance,
air.max_constraint_degree,
air.num_quotient_chunks,
air.max_combo,
params.ood_pow_bits,
cap,
);
let deep_composition = round(
DEEP_COMPOSITION_LABEL,
instance,
match air.num_deep_terms {
Some(n) => Some(n as u64),
None => None,
},
instance.log_max_height.saturating_add(params.log_blowup),
params.deep_pow_bits,
cap,
);
let folding = match folding_coefficient(params.log_folding_arity) {
Some(coefficient) => round(
FOLDING_LABEL,
instance,
Some(coefficient),
instance.log_max_height.saturating_add(params.log_blowup),
params.folding_pow_bits,
cap,
),
None => SecurityTerm::new(FOLDING_LABEL, 0),
};
let query_bits = (params.num_queries as u64)
.saturating_mul(fixed::bits_per_query(
params.log_blowup,
instance.field_bits,
))
.saturating_add(fixed::from_bits(params.query_pow_bits));
let query = SecurityTerm::new(QUERY_LABEL, min(query_bits, cap));
SecurityReport::new([
lookup,
composition,
out_of_domain,
deep_composition,
folding,
query,
SecurityTerm::new(COLLISION_LABEL, cap),
])
}
const fn folding_coefficient(log_folding_arity: u32) -> Option<u64> {
if log_folding_arity >= u64::BITS - 1 {
return None;
}
Some(2 * ((1u64 << log_folding_arity) - 1))
}
const fn round(
label: &'static str,
instance: &InstanceShape,
coefficient: Option<u64>,
log_size: u32,
pow_bits: u32,
cap: u64,
) -> SecurityTerm {
let coefficient = match coefficient {
None => return SecurityTerm::new(label, cap),
Some(coefficient) => match core::num::NonZeroU64::new(coefficient) {
Some(coefficient) => coefficient,
None => return SecurityTerm::new(label, 0),
},
};
let error = fixed::ceil_log2(coefficient.get()) + fixed::from_bits(log_size);
let bits = if error >= instance.field_bits {
fixed::from_bits(pow_bits)
} else {
instance.field_bits - error + fixed::from_bits(pow_bits)
};
SecurityTerm::new(label, min(bits, cap))
}
const fn out_of_domain_round(
instance: &InstanceShape,
max_constraint_degree: u32,
num_quotient_chunks: u32,
max_combo: u32,
pow_bits: u32,
cap: u64,
) -> SecurityTerm {
if instance.log_max_height >= u64::BITS || !num_quotient_chunks.is_power_of_two() {
return SecurityTerm::new(OUT_OF_DOMAIN_LABEL, 0);
}
let d = if max_constraint_degree == 0 {
1
} else {
max_constraint_degree as u128
};
let combo = if max_combo == 0 { 1 } else { max_combo as u128 };
let height = 1u128 << instance.log_max_height;
let ethstark = d * (height + combo - 1) + (height - 1);
let chunks = num_quotient_chunks as u128;
let chunked = (chunks + 1) * height + combo - 1;
let size = if ethstark > chunked {
ethstark
} else {
chunked
};
if size > u64::MAX as u128 {
return SecurityTerm::new(OUT_OF_DOMAIN_LABEL, 0);
}
let error = fixed::ceil_log2(size as u64);
let bits = instance.field_bits.saturating_sub(error) + fixed::from_bits(pow_bits);
SecurityTerm::new(OUT_OF_DOMAIN_LABEL, min(bits, cap))
}
const fn min(a: u64, b: u64) -> u64 {
if a < b { a } else { b }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::budget::shape::LookupShape;
const FIELD_BITS: u64 = 8_388_607;
fn params() -> ProtocolParams {
ProtocolParams {
log_blowup: 3,
log_folding_arity: 2,
num_queries: 27,
query_pow_bits: 17,
ood_pow_bits: 0,
deep_pow_bits: 12,
folding_pow_bits: 4,
lookup_pow_bits: 0,
}
}
fn instance(log_max_height: u32) -> InstanceShape {
InstanceShape {
log_max_height,
field_bits: FIELD_BITS,
collision_resistance: 128,
}
}
fn air() -> AirShape {
AirShape {
num_composed_constraints: 531,
max_constraint_degree: 9,
num_quotient_chunks: 8,
max_combo: 2,
num_deep_terms: Some(130),
lookup: Some(LookupShape {
fractions_per_row: 27,
max_message_width: 16,
}),
}
}
#[test]
fn malformed_field_bits_report_zero_security_without_panicking() {
for field_bits in [
0,
18_446_744_073_709_391_531,
u64::MAX,
fixed::from_bits(u32::MAX) + 1,
] {
let instance = InstanceShape {
field_bits,
..instance(20)
};
let params = ProtocolParams {
log_blowup: 1,
query_pow_bits: u32::MAX,
..params()
};
assert_eq!(
security_report(¶ms, &instance, &air()).security_level(),
0
);
}
let supported = InstanceShape {
field_bits: fixed::from_bits(u32::MAX),
..instance(20)
};
assert!(security_report(¶ms(), &supported, &air()).security_level() > 0);
}
#[test]
fn present_zero_coefficient_rounds_cannot_claim_the_cap() {
for kind in [
LOOKUP_LABEL,
COMPOSITION_LABEL,
DEEP_COMPOSITION_LABEL,
FOLDING_LABEL,
] {
let mut air = air();
let mut params = params();
match kind {
LOOKUP_LABEL => air.lookup.as_mut().unwrap().fractions_per_row = 0,
COMPOSITION_LABEL => air.num_composed_constraints = 0,
DEEP_COMPOSITION_LABEL => air.num_deep_terms = Some(0),
FOLDING_LABEL => params.log_folding_arity = 0,
_ => unreachable!(),
}
let report = security_report(¶ms, &instance(20), &air);
let term = report
.terms()
.iter()
.find(|term| term.label == kind)
.unwrap();
assert_eq!(term.bits, 0, "malformed present round {kind}");
}
}
#[test]
fn query_phase_binds_at_moderate_heights() {
let report = security_report(¶ms(), &instance(20), &air());
assert_eq!(report.binding_term().label, QUERY_LABEL);
assert_eq!(report.security_level(), 96);
}
#[test]
fn lookup_round_binds_at_large_heights() {
let report = security_report(¶ms(), &instance(29), &air());
assert_eq!(report.binding_term().label, LOOKUP_LABEL);
assert!(
report.security_level() < 96,
"lookup round should fall below the query-phase level at maximum trace height, got {}",
report.security_level()
);
}
#[test]
fn lookup_grinding_lifts_the_lookup_round() {
let ungrounded = security_report(¶ms(), &instance(29), &air());
let grinding = ProtocolParams {
lookup_pow_bits: 8,
..params()
};
let ground = security_report(&grinding, &instance(29), &air());
let before = ungrounded.terms()[0];
let after = ground.terms()[0];
assert_eq!(before.label, LOOKUP_LABEL);
assert_eq!(after.bits, before.bits + fixed::from_bits(8));
}
#[test]
fn absurd_folding_arity_reports_no_security_rather_than_the_cap() {
assert_eq!(folding_coefficient(2), Some(6));
assert_eq!(folding_coefficient(62), Some(u64::MAX - (1 << 63) - 1));
assert_eq!(folding_coefficient(63), None);
assert_eq!(folding_coefficient(u32::MAX), None);
let absurd = ProtocolParams {
log_folding_arity: 64,
..params()
};
let report = security_report(&absurd, &instance(20), &air());
let folding = report
.terms()
.iter()
.find(|t| t.label == FOLDING_LABEL)
.expect("folding term is always present");
assert_eq!(folding.bits, 0, "folding round vanished into the cap");
assert_eq!(report.security_level(), 0);
}
#[test]
fn ood_grinding_lifts_only_the_out_of_domain_round() {
let ungrounded = security_report(¶ms(), &instance(29), &air());
let grinding = ProtocolParams {
ood_pow_bits: 8,
..params()
};
let ground = security_report(&grinding, &instance(29), &air());
for (before, after) in ungrounded.terms().iter().zip(ground.terms()) {
assert_eq!(before.label, after.label, "term order changed");
let expected = if before.label == OUT_OF_DOMAIN_LABEL {
before.bits + fixed::from_bits(8)
} else {
before.bits
};
assert_eq!(after.bits, expected, "{} moved", before.label);
}
}
#[test]
fn deep_composition_round_pays_the_lde_domain_size() {
let deep_bits = |params: &ProtocolParams, log_max_height: u32| {
security_report(params, &instance(log_max_height), &air())
.terms()
.iter()
.find(|t| t.label == DEEP_COMPOSITION_LABEL)
.expect("term present")
.bits
};
let base = deep_bits(¶ms(), 20);
assert!(
base < instance(20).cap(),
"the round must not sit at the cap"
);
assert_eq!(deep_bits(¶ms(), 21), base - fixed::ONE);
let wider = ProtocolParams {
log_blowup: params().log_blowup + 1,
..params()
};
assert_eq!(deep_bits(&wider, 20), base - fixed::ONE);
}
#[test]
fn every_round_is_capped_by_collision_resistance() {
let instance = InstanceShape {
collision_resistance: 96,
..instance(20)
};
let report = security_report(¶ms(), &instance, &air());
for term in report.terms() {
assert!(
term.bits <= fixed::from_bits(96),
"{} exceeds the cap",
term.label
);
}
assert_eq!(report.security_level(), 96);
}
#[test]
fn security_is_monotone_in_trace_height() {
let mut previous = u32::MAX;
for log_height in 6..=30u32 {
let level = security_report(¶ms(), &instance(log_height), &air()).security_level();
assert!(level <= previous, "level rose at log height {log_height}");
previous = level;
}
}
#[test]
fn every_round_is_monotone_in_trace_height() {
let mut previous = [u64::MAX; report::NUM_TERMS];
for log_height in 0..=64u32 {
let report = security_report(¶ms(), &instance(log_height), &air());
for (term, prev) in report.terms().iter().zip(previous.iter_mut()) {
assert!(
term.bits <= *prev,
"{} rose at log height {log_height}",
term.label
);
*prev = term.bits;
}
}
}
#[test]
fn security_is_monotone_in_max_combo() {
let mut previous = u64::MAX;
for max_combo in 1..=16u32 {
let air = AirShape { max_combo, ..air() };
let term = security_report(¶ms(), &instance(6), &air).terms()[2];
assert_eq!(term.label, OUT_OF_DOMAIN_LABEL);
assert!(
term.bits <= previous,
"out-of-domain rose at max_combo {max_combo}"
);
previous = term.bits;
}
}
#[test]
fn security_is_monotone_in_air_size() {
let small = security_report(¶ms(), &instance(24), &air());
let large = AirShape {
num_composed_constraints: 4096,
max_constraint_degree: 9,
num_quotient_chunks: 8,
max_combo: 2,
num_deep_terms: Some(1024),
lookup: Some(LookupShape {
fractions_per_row: 64,
max_message_width: 16,
}),
};
let large = security_report(¶ms(), &instance(24), &large);
assert!(large.security_level() <= small.security_level());
}
#[test]
fn absent_lookup_argument_contributes_no_round() {
let air = AirShape {
lookup: None,
..air()
};
let report = security_report(¶ms(), &instance(29), &air);
assert_eq!(report.terms()[0].bits, instance(29).cap());
assert_ne!(report.binding_term().label, LOOKUP_LABEL);
}
#[test]
fn absent_deep_composition_contributes_no_round() {
let air = AirShape {
num_deep_terms: None,
..air()
};
let report = security_report(¶ms(), &instance(20), &air);
let deep_term = report
.terms()
.iter()
.find(|t| t.label == DEEP_COMPOSITION_LABEL)
.expect("term present");
assert_eq!(deep_term.bits, instance(20).cap());
assert_ne!(report.binding_term().label, DEEP_COMPOSITION_LABEL);
}
}