use alloc::vec::Vec;
use crate::{ErrorBits, SecurityTerm};
pub const BINARY_BUS_LABEL: &str = "binary-bus";
pub const BUS_FINGERPRINT_LABEL: &str = "bus-tuple-fingerprint";
pub const PRODUCT_GKR_SUMCHECK_LABEL: &str = "bus-product-gkr-sumcheck";
pub const PRODUCT_GKR_BATCHING_LABEL: &str = "bus-product-gkr-tree-batching";
pub const PRODUCT_GKR_COLLAPSE_LABEL: &str = "bus-product-gkr-child-collapse";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ProductGkrSecurityProfile {
log_height: usize,
tree_count: usize,
sumcheck_rounds: usize,
layer_count: usize,
collapse_challenges: usize,
}
impl ProductGkrSecurityProfile {
#[must_use]
pub const fn new(
log_height: usize,
tree_count: usize,
sumcheck_rounds: usize,
layer_count: usize,
collapse_challenges: usize,
) -> Option<Self> {
if log_height >= usize::BITS as usize
|| tree_count == 0
|| tree_count > usize::MAX / 4
|| collapse_challenges != log_height
|| (log_height == 0) != (layer_count == 0)
{
return None;
}
Some(Self {
log_height,
tree_count,
sumcheck_rounds,
layer_count,
collapse_challenges,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BusSecurityModel {
field_bits: usize,
tuple_variables: usize,
non_padding_leaf_counts: [usize; 2],
product: ProductGkrSecurityProfile,
}
impl BusSecurityModel {
#[must_use]
pub const fn new(
field_bits: usize,
tuple_variables: usize,
non_padding_leaf_counts: [usize; 2],
product: ProductGkrSecurityProfile,
) -> Option<Self> {
if field_bits == 0 || tuple_variables >= usize::BITS as usize {
return None;
}
let capacity = 1usize << product.log_height;
if non_padding_leaf_counts[0] > capacity
|| non_padding_leaf_counts[1] > capacity
|| (non_padding_leaf_counts[0] == 0 && non_padding_leaf_counts[1] == 0)
{
return None;
}
Some(Self {
field_bits,
tuple_variables,
non_padding_leaf_counts,
product,
})
}
#[must_use]
pub fn components(&self) -> Vec<SecurityTerm> {
let factors = self.non_padding_leaf_counts[0].max(self.non_padding_leaf_counts[1]);
let fingerprint_numerator = self.tuple_variables.max(1) as u128 * factors as u128;
let mut terms = alloc::vec![SecurityTerm::new(
BUS_FINGERPRINT_LABEL,
error_from_numerator(self.field_bits, fingerprint_numerator),
)];
push_nonzero_term(
&mut terms,
PRODUCT_GKR_SUMCHECK_LABEL,
self.field_bits,
5 * self.product.sumcheck_rounds as u128,
);
push_nonzero_term(
&mut terms,
PRODUCT_GKR_BATCHING_LABEL,
self.field_bits,
self.product.layer_count as u128 * self.product.tree_count.saturating_sub(1) as u128,
);
push_nonzero_term(
&mut terms,
PRODUCT_GKR_COLLAPSE_LABEL,
self.field_bits,
self.product.collapse_challenges as u128,
);
terms
}
#[must_use]
pub fn combined_term(&self) -> SecurityTerm {
let components = self.components();
let errors = components
.iter()
.map(|component| component.bits)
.collect::<Vec<_>>();
SecurityTerm::new(BINARY_BUS_LABEL, ErrorBits::sum(&errors))
}
}
fn push_nonzero_term(
terms: &mut Vec<SecurityTerm>,
label: &'static str,
field_bits: usize,
numerator: u128,
) {
if numerator != 0 {
terms.push(SecurityTerm::new(
label,
error_from_numerator(field_bits, numerator),
));
}
}
fn error_from_numerator(field_bits: usize, numerator: u128) -> ErrorBits {
let mut rounded = numerator as f64;
if (rounded as u128) < numerator {
rounded = rounded.next_up();
}
ErrorBits::from_log2((field_bits as f64 - libm::log2(rounded)).max(0.0))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fri::FriRegime;
use crate::grinding::GrindingSites;
use crate::shape::{InstanceShape, StarkAirParams};
use crate::stark::proven_security_report;
fn profile(
log_height: usize,
tree_count: usize,
sumcheck_rounds: usize,
layer_count: usize,
) -> ProductGkrSecurityProfile {
ProductGkrSecurityProfile::new(
log_height,
tree_count,
sumcheck_rounds,
layer_count,
log_height,
)
.unwrap()
}
fn bits(terms: &[SecurityTerm], label: &str) -> Option<f64> {
terms
.iter()
.find(|term| term.label == label)
.map(|term| term.bits.bits())
}
#[test]
fn exact_terms_follow_a_twenty_variable_bus() {
let model = BusSecurityModel::new(128, 4, [1 << 20, 1 << 18], profile(20, 2, 90, 10))
.expect("the dimensions fit the product tree");
let terms = model.components();
assert_eq!(
bits(&terms, BUS_FINGERPRINT_LABEL),
Some(128.0 - libm::log2((4u64 << 20) as f64))
);
assert_eq!(
bits(&terms, PRODUCT_GKR_SUMCHECK_LABEL),
Some(128.0 - libm::log2(450.0))
);
assert_eq!(
bits(&terms, PRODUCT_GKR_BATCHING_LABEL),
Some(128.0 - libm::log2(10.0))
);
assert_eq!(
bits(&terms, PRODUCT_GKR_COLLAPSE_LABEL),
Some(128.0 - libm::log2(20.0))
);
}
#[test]
fn zero_height_omits_every_product_reduction_term() {
let terms = BusSecurityModel::new(128, 0, [1, 1], profile(0, 2, 0, 0))
.expect("one factor fits a zero-height tree")
.components();
assert_eq!(terms.len(), 1);
assert_eq!(terms[0].label, BUS_FINGERPRINT_LABEL);
assert_eq!(terms[0].bits.bits(), 128.0);
}
#[test]
fn odd_height_charges_the_leading_binary_collapse() {
let terms = BusSecurityModel::new(128, 1, [8, 8], profile(3, 2, 1, 2))
.expect("eight factors fit a height-three tree")
.components();
assert_eq!(
bits(&terms, PRODUCT_GKR_SUMCHECK_LABEL),
Some(128.0 - libm::log2(5.0))
);
assert_eq!(bits(&terms, PRODUCT_GKR_BATCHING_LABEL), Some(127.0));
assert_eq!(
bits(&terms, PRODUCT_GKR_COLLAPSE_LABEL),
Some(128.0 - libm::log2(3.0))
);
}
#[test]
fn malformed_or_oversized_shapes_have_no_model() {
assert!(BusSecurityModel::new(0, 1, [1, 1], profile(0, 2, 0, 0)).is_none());
assert!(ProductGkrSecurityProfile::new(0, 0, 0, 0, 0).is_none());
assert!(ProductGkrSecurityProfile::new(0, usize::MAX, 0, 0, 0).is_none());
assert!(
BusSecurityModel::new(128, usize::BITS as usize, [1, 1], profile(0, 2, 0, 0),)
.is_none()
);
assert!(ProductGkrSecurityProfile::new(usize::BITS as usize, 2, 0, 0, 0).is_none());
assert!(BusSecurityModel::new(128, 1, [2, 1], profile(0, 2, 0, 0)).is_none());
assert!(BusSecurityModel::new(128, 1, [0, 0], profile(0, 2, 0, 0)).is_none());
assert!(ProductGkrSecurityProfile::new(1, 2, 0, 0, 0).is_none());
}
#[test]
fn a_192_bit_field_adds_sixty_four_bits_to_every_term() {
let narrow = BusSecurityModel::new(128, 4, [1 << 20, 1 << 18], profile(20, 2, 90, 10))
.expect("the 128-bit model is valid")
.components();
let wide = BusSecurityModel::new(192, 4, [1 << 20, 1 << 18], profile(20, 2, 90, 10))
.expect("the 192-bit model is valid")
.components();
assert_eq!(narrow.len(), wide.len());
for (narrow_term, wide_term) in narrow.iter().zip(wide) {
assert_eq!(narrow_term.label, wide_term.label);
assert!((wide_term.bits.bits() - narrow_term.bits.bits() - 64.0).abs() < 1e-12);
}
}
#[test]
fn combined_term_sums_all_component_probabilities() {
let model = BusSecurityModel::new(128, 1, [8, 8], profile(3, 2, 1, 2)).unwrap();
let components = model.components();
let expected = ErrorBits::sum(
&components
.iter()
.map(|component| component.bits)
.collect::<Vec<_>>(),
);
assert_eq!(model.combined_term().bits, expected);
assert_eq!(model.combined_term().label, BINARY_BUS_LABEL);
}
#[test]
fn combined_term_composes_as_one_protocol_extra() {
let regime = FriRegime {
log_blowup: 1,
num_queries: 100,
log_final_poly_len: 0,
max_log_arity: 3,
commit_pow_bits: 0,
query_pow_bits: 16,
};
let air = StarkAirParams {
num_constraints: 1,
max_constraint_degree: 2,
num_quotient_chunks: 1,
max_combo: 2,
};
let shape = InstanceShape {
log_trace_length: 20,
modulus_bits: 64,
collision_resistance: 128,
num_batched_functions: 1,
};
let model =
BusSecurityModel::new(64, 4, [1 << 20, 1 << 18], profile(20, 2, 90, 10)).unwrap();
let term = model.combined_term();
let report = proven_security_report(®ime, &air, &shape, &[term], &GrindingSites::NONE);
assert!(
report
.udr
.terms()
.iter()
.any(|candidate| candidate.label == BINARY_BUS_LABEL)
);
assert!(!report.udr.terms().iter().any(|candidate| {
[
BUS_FINGERPRINT_LABEL,
PRODUCT_GKR_SUMCHECK_LABEL,
PRODUCT_GKR_BATCHING_LABEL,
PRODUCT_GKR_COLLAPSE_LABEL,
]
.contains(&candidate.label)
}));
}
#[test]
fn large_numerator_rounding_branch_is_exercised() {
let model = BusSecurityModel::new(128, 3, [(1usize << 62) + 1, 1], profile(63, 2, 961, 32))
.unwrap();
assert!(model.components()[0].bits.bits().is_finite());
}
}