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();
let lookup = round(
LOOKUP_LABEL,
instance,
(air.lookup.max_message_width as u64 + 2) * air.lookup.fractions_per_row as u64,
instance.log_max_height,
params.lookup_pow_bits,
cap,
);
let composition = round(
COMPOSITION_LABEL,
instance,
air.num_composed_constraints as u64,
0,
0,
cap,
);
let out_of_domain = round(
OUT_OF_DOMAIN_LABEL,
instance,
air.max_constraint_degree as u64 + 1,
instance.log_max_height,
0,
cap,
);
let deep_composition = round(
DEEP_COMPOSITION_LABEL,
instance,
match air.num_deep_terms {
Some(n) => n as u64,
None => 0,
},
0,
params.deep_pow_bits,
cap,
);
let folding = round(
FOLDING_LABEL,
instance,
2 * ((1u64 << params.log_folding_arity) - 1),
instance.log_max_height + params.log_blowup,
params.folding_pow_bits,
cap,
);
let query_bits = params.num_queries as u64
* fixed::bits_per_query(params.log_blowup, instance.field_bits)
+ 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 round(
label: &'static str,
instance: &InstanceShape,
coefficient: u64,
log_size: u32,
pow_bits: u32,
cap: u64,
) -> SecurityTerm {
if coefficient == 0 {
return SecurityTerm::new(label, cap);
}
let error = fixed::ceil_log2(coefficient) + 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 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,
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_deep_terms: Some(130),
lookup: LookupShape {
fractions_per_row: 27,
max_message_width: 16,
},
}
}
#[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 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 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_deep_terms: Some(1024),
lookup: 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: LookupShape {
fractions_per_row: 0,
max_message_width: 0,
},
..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);
}
}