use alloc::vec::Vec;
use libm::log2;
use crate::{ErrorBits, SecurityTerm};
pub const BIT_RING_SWITCH_LABEL: &str = "bit-ring-switch";
pub const COLUMN_BATCH_LABEL: &str = "column-batching";
#[must_use]
pub fn column_batch_error(num_batches: usize, k: usize, field_bits: usize) -> ErrorBits {
if num_batches == 0 || k == 0 {
return ErrorBits::from_log2(f64::INFINITY);
}
let log_challenges = log2(num_batches as f64) + log2(k as f64);
ErrorBits::from_log2(field_bits as f64 - log_challenges)
}
#[must_use]
pub fn column_batch_term(num_batches: usize, k: usize, field_bits: usize) -> SecurityTerm {
SecurityTerm::new(
COLUMN_BATCH_LABEL,
column_batch_error(num_batches, k, field_bits),
)
}
#[must_use]
pub fn bit_ring_switch_error(
num_reductions: usize,
absorbed_log: usize,
surviving_variables: usize,
field_bits: usize,
) -> ErrorBits {
bit_ring_switch_tensors_error(
num_reductions,
1,
absorbed_log,
surviving_variables,
field_bits,
)
}
#[must_use]
pub fn bit_ring_switch_term(
num_reductions: usize,
absorbed_log: usize,
surviving_variables: usize,
field_bits: usize,
) -> SecurityTerm {
bit_ring_switch_tensors_term(
num_reductions,
1,
absorbed_log,
surviving_variables,
field_bits,
)
}
#[must_use]
pub fn bit_ring_switch_tensors_error(
num_reductions: usize,
num_tensors: usize,
absorbed_log: usize,
surviving_variables: usize,
field_bits: usize,
) -> ErrorBits {
if num_reductions == 0 {
return ErrorBits::from_log2(f64::INFINITY);
}
let per_reduction = absorbed_log as f64
+ num_tensors.saturating_sub(1) as f64
+ 2.0 * surviving_variables as f64;
if per_reduction == 0.0 {
return ErrorBits::from_log2(f64::INFINITY);
}
ErrorBits::from_log2(field_bits as f64 - log2(num_reductions as f64 * per_reduction))
}
#[must_use]
pub fn bit_ring_switch_tensors_term(
num_reductions: usize,
num_tensors: usize,
absorbed_log: usize,
surviving_variables: usize,
field_bits: usize,
) -> SecurityTerm {
SecurityTerm::new(
BIT_RING_SWITCH_LABEL,
bit_ring_switch_tensors_error(
num_reductions,
num_tensors,
absorbed_log,
surviving_variables,
field_bits,
),
)
}
pub const BIT_RING_SWITCH_CLAIM_BATCHING_LABEL: &str = "bit-ring-switch-claim-batching";
#[must_use]
pub fn bit_ring_switch_claim_batching_error(num_claims: usize, field_bits: usize) -> ErrorBits {
if num_claims <= 1 {
return ErrorBits::from_log2(f64::INFINITY);
}
ErrorBits::from_log2(field_bits as f64 - log2((num_claims - 1) as f64))
}
#[must_use]
pub fn bit_ring_switch_claim_batching_term(num_claims: usize, field_bits: usize) -> SecurityTerm {
SecurityTerm::new(
BIT_RING_SWITCH_CLAIM_BATCHING_LABEL,
bit_ring_switch_claim_batching_error(num_claims, field_bits),
)
}
#[derive(Clone, Copy, Debug)]
pub struct MultilinearAirParams {
pub num_instances: usize,
pub max_num_constraints: usize,
pub num_variables: usize,
pub constraint_degree: usize,
pub lookup: Option<MultilinearLookupParams>,
pub skip: Option<MultilinearSkipParams>,
pub logup_star: Option<MultilinearLogupStarParams>,
}
#[derive(Clone, Copy, Debug)]
pub struct MultilinearSkipParams {
pub log_size: usize,
pub log_extended: usize,
pub num_polynomials: usize,
}
#[derive(Clone, Copy, Debug)]
pub struct MultilinearLookupParams {
pub num_variables: usize,
pub num_fractions: usize,
pub max_message_width: usize,
}
const SHIFT_LIMIT: usize = 63;
#[derive(Clone, Copy, Debug)]
pub struct MultilinearLogupStarParams {
pub num_variables: usize,
pub num_leaves: usize,
pub max_readers_per_table: usize,
pub max_reader_variables: usize,
pub max_table_variables: usize,
pub num_column_claims: usize,
}
pub fn reduction_terms(
air: &MultilinearAirParams,
field_bits: usize,
nonzero_field_bits: usize,
) -> Vec<SecurityTerm> {
let skip_is_valid = air.skip.is_none_or(|skip| {
skip.log_extended > skip.log_size
&& skip.log_extended < SHIFT_LIMIT
&& skip.log_size <= air.num_variables
&& skip.num_polynomials > 0
});
if air.num_instances == 0
|| air.num_variables == 0
|| air.constraint_degree == 0
|| !skip_is_valid
{
return alloc::vec![SecurityTerm::new(
"invalid-multilinear-shape",
ErrorBits::from_log2(0.0)
)];
}
let mut terms = Vec::new();
let mut add = |label, degree: f64, bits: usize| {
if degree > 0.0 {
terms.push(SecurityTerm::new(
label,
ErrorBits::from_log2((bits as f64 - log2(degree)).max(0.0)),
));
}
};
add(
"constraint-batching",
air.max_num_constraints.saturating_sub(1) as f64,
field_bits,
);
add(
"air-batching",
air.num_instances.saturating_sub(1) as f64,
field_bits,
);
let skipped = air.skip.map_or(0, |skip| skip.log_size);
let kept = air.num_variables.saturating_sub(skipped);
add("zerocheck", kept as f64, nonzero_field_bits);
if let Some(skip) = air.skip {
let admitted = ((1u64 << skip.log_extended) - 1) as f64;
let honest = air.constraint_degree as f64 * ((1u64 << skip.log_size) - 1) as f64;
add("skip-reconstruction", admitted.max(honest), field_bits);
add("skip-opening", skipped as f64 * 2.0, field_bits);
add(
"skip-opening-batching",
skip.num_polynomials.saturating_sub(1) as f64,
field_bits,
);
}
add(
"constraint-sumcheck",
kept as f64 * (air.constraint_degree as f64 + 1.0),
field_bits,
);
if let Some(lookup) = air.lookup {
add(
"logup-fingerprint",
lookup.num_fractions as f64 * (lookup.max_message_width as f64 + 2.0),
field_bits,
);
let layers = lookup.num_variables as f64;
add(
"fractional-gkr",
2.0 * layers + 3.0 * layers * (layers - 1.0) / 2.0,
nonzero_field_bits,
);
add("lookup-opening-link", 1.0, field_bits); add("lookup-air-link", 1.0, field_bits); }
if let Some(logup_star) = air.logup_star {
add(
"logup-star-reader-batching",
logup_star.max_readers_per_table.saturating_sub(1) as f64,
field_bits,
);
add(
"logup-star-entry-challenge",
logup_star.num_leaves as f64,
nonzero_field_bits,
);
add(
"logup-star-claim-point",
logup_star.max_reader_variables as f64,
field_bits,
);
let layers = logup_star.num_variables as f64;
add(
"logup-star-fractional-gkr",
2.0 * layers + 3.0 * layers * (layers - 1.0) / 2.0,
nonzero_field_bits,
);
add(
"logup-star-product-sumcheck",
2.0 * logup_star.max_table_variables as f64,
field_bits,
);
add(
"logup-star-column-batching",
logup_star.num_column_claims.saturating_sub(1) as f64,
field_bits,
);
}
terms
}
#[cfg(test)]
mod tests {
use super::*;
const fn skip(
log_size: usize,
log_extended: usize,
num_polynomials: usize,
) -> MultilinearSkipParams {
MultilinearSkipParams {
log_size,
log_extended,
num_polynomials,
}
}
#[test]
fn reduction_charges_batching_zerocheck_and_every_sumcheck_round() {
let terms = reduction_terms(
&MultilinearAirParams {
num_instances: 2,
max_num_constraints: 3,
num_variables: 4,
constraint_degree: 3,
lookup: None,
skip: None,
logup_star: None,
},
100,
99,
);
let bits = crate::ErrorBits::sum(
&terms
.iter()
.map(|term| term.bits)
.collect::<alloc::vec::Vec<_>>(),
)
.bits();
assert!((bits - (100.0 - libm::log2(27.0))).abs() < 1e-12);
}
fn plain(num_variables: usize) -> MultilinearAirParams {
MultilinearAirParams {
num_instances: 1,
max_num_constraints: 1,
num_variables,
constraint_degree: 2,
lookup: None,
logup_star: None,
skip: None,
}
}
fn charged(terms: &[SecurityTerm], label: &str) -> Option<f64> {
terms
.iter()
.find(|term| term.label == label)
.map(|term| term.bits.bits())
}
#[test]
fn a_statement_without_a_skip_is_charged_exactly_as_before() {
let terms = reduction_terms(&plain(10), 100, 99);
assert_eq!(charged(&terms, "zerocheck"), Some(99.0 - libm::log2(10.0)));
assert_eq!(
charged(&terms, "constraint-sumcheck"),
Some(100.0 - libm::log2(30.0))
);
assert_eq!(charged(&terms, "skip-reconstruction"), None);
assert_eq!(charged(&terms, "skip-opening"), None);
}
#[test]
fn a_skip_moves_error_out_of_the_rounds_and_into_the_reconstruction() {
let mut params = plain(10);
params.skip = Some(skip(6, 7, 4));
let terms = reduction_terms(¶ms, 100, 99);
assert_eq!(charged(&terms, "zerocheck"), Some(99.0 - libm::log2(4.0)));
assert_eq!(
charged(&terms, "constraint-sumcheck"),
Some(100.0 - libm::log2(12.0))
);
assert_eq!(
charged(&terms, "skip-reconstruction"),
Some(100.0 - libm::log2(127.0))
);
assert_eq!(
charged(&terms, "skip-opening"),
Some(100.0 - libm::log2(12.0))
);
assert_eq!(
charged(&terms, "skip-opening-batching"),
Some(100.0 - libm::log2(3.0))
);
}
#[test]
fn the_honest_degree_binds_when_the_transmitted_domain_is_narrow() {
let mut params = plain(10);
params.constraint_degree = 5;
params.skip = Some(skip(6, 7, 1));
assert_eq!(
charged(&reduction_terms(¶ms, 100, 99), "skip-reconstruction"),
Some(100.0 - libm::log2(315.0))
);
}
#[test]
fn the_admitted_degree_binds_when_the_transmitted_domain_is_wide() {
let mut params = plain(10);
params.constraint_degree = 2;
params.skip = Some(skip(6, 8, 1));
assert_eq!(
charged(&reduction_terms(¶ms, 100, 99), "skip-reconstruction"),
Some(100.0 - libm::log2(255.0))
);
}
#[test]
fn a_single_polynomial_batch_costs_no_separating_draw() {
let mut params = plain(10);
params.skip = Some(skip(6, 7, 1));
assert_eq!(
charged(&reduction_terms(¶ms, 100, 99), "skip-opening-batching"),
None
);
}
#[test]
fn a_skip_shape_no_term_can_charge_honestly_is_refused() {
for (log_size, log_extended, num_polynomials) in
[(6, 6, 1), (6, 5, 1), (6, 63, 1), (11, 12, 1), (6, 7, 0)]
{
let mut params = plain(10);
params.skip = Some(skip(log_size, log_extended, num_polynomials));
let terms = reduction_terms(¶ms, 100, 99);
assert_eq!(
terms.len(),
1,
"{log_size} {log_extended} {num_polynomials}"
);
assert_eq!(terms[0].label, "invalid-multilinear-shape");
assert_eq!(terms[0].bits.bits(), 0.0);
}
}
#[test]
fn a_skip_shape_every_term_can_charge_is_accepted() {
for (log_size, log_extended, num_polynomials) in [(6, 7, 1), (10, 11, 1), (6, 62, 1)] {
let mut params = plain(10);
params.skip = Some(skip(log_size, log_extended, num_polynomials));
let terms = reduction_terms(¶ms, 100, 99);
assert!(
terms.len() > 1,
"{log_size} {log_extended} {num_polynomials}"
);
}
}
#[test]
fn the_reconstruction_term_dominates_what_the_rounds_gave_up() {
let mut params = plain(10);
params.skip = Some(skip(6, 7, 1));
let plain_terms = reduction_terms(&plain(10), 100, 99);
let skip_terms = reduction_terms(¶ms, 100, 99);
let total = |terms: &[SecurityTerm]| {
ErrorBits::sum(
&terms
.iter()
.map(|term| term.bits)
.collect::<alloc::vec::Vec<_>>(),
)
.bits()
};
let error = |bits: f64| libm::exp2(-bits);
let given_up = error(charged(&plain_terms, "zerocheck").unwrap())
+ error(charged(&plain_terms, "constraint-sumcheck").unwrap())
- error(charged(&skip_terms, "zerocheck").unwrap())
- error(charged(&skip_terms, "constraint-sumcheck").unwrap());
let reconstruction = error(charged(&skip_terms, "skip-reconstruction").unwrap());
assert!(
reconstruction > given_up,
"reconstruction={reconstruction} given_up={given_up}"
);
assert!(
total(&skip_terms) < total(&plain_terms),
"the skip costs bits overall: skip={} plain={}",
total(&skip_terms),
total(&plain_terms)
);
}
#[test]
fn skipping_every_variable_leaves_no_rounds_to_charge() {
let mut params = plain(6);
params.skip = Some(skip(6, 7, 1));
let terms = reduction_terms(¶ms, 100, 99);
assert_eq!(charged(&terms, "zerocheck"), None);
assert_eq!(charged(&terms, "constraint-sumcheck"), None);
assert_eq!(
charged(&terms, "skip-reconstruction"),
Some(100.0 - libm::log2(127.0))
);
}
#[test]
fn lookup_charges_fingerprint_gkr_and_both_links() {
let params = MultilinearAirParams {
num_instances: 1,
max_num_constraints: 1,
num_variables: 5,
constraint_degree: 1,
lookup: Some(MultilinearLookupParams {
num_variables: 5,
num_fractions: 64,
max_message_width: 1,
}),
skip: None,
logup_star: None,
};
let terms = reduction_terms(¶ms, 100, 99);
let find = |label| {
terms
.iter()
.find(|term| term.label == label)
.unwrap()
.bits
.bits()
};
assert!((find("logup-fingerprint") - (100.0 - libm::log2(192.0))).abs() < 1e-12);
assert!((find("fractional-gkr") - (99.0 - libm::log2(40.0))).abs() < 1e-12);
assert_eq!(find("lookup-opening-link"), 100.0);
assert_eq!(find("lookup-air-link"), 100.0);
}
fn logup_star_params() -> MultilinearAirParams {
MultilinearAirParams {
num_instances: 1,
max_num_constraints: 1,
num_variables: 5,
constraint_degree: 1,
lookup: None,
skip: None,
logup_star: Some(MultilinearLogupStarParams {
num_variables: 5,
num_leaves: 24,
max_readers_per_table: 3,
max_reader_variables: 3,
max_table_variables: 4,
num_column_claims: 6,
}),
}
}
#[test]
fn logup_star_charges_every_challenge_it_draws() {
let terms = reduction_terms(&logup_star_params(), 100, 99);
let find = |label| {
terms
.iter()
.find(|term| term.label == label)
.unwrap()
.bits
.bits()
};
assert!((find("logup-star-reader-batching") - (100.0 - libm::log2(2.0))).abs() < 1e-12);
assert!((find("logup-star-entry-challenge") - (99.0 - libm::log2(24.0))).abs() < 1e-12);
assert!((find("logup-star-fractional-gkr") - (99.0 - libm::log2(40.0))).abs() < 1e-12);
assert!((find("logup-star-product-sumcheck") - (100.0 - libm::log2(8.0))).abs() < 1e-12);
assert!((find("logup-star-column-batching") - (100.0 - libm::log2(5.0))).abs() < 1e-12);
assert!((find("logup-star-claim-point") - (100.0 - libm::log2(3.0))).abs() < 1e-12);
}
#[test]
fn a_batching_term_follows_the_count_it_separates() {
let mut wider = logup_star_params();
let mut plan = wider.logup_star.unwrap();
plan.max_readers_per_table = 5;
wider.logup_star = Some(plan);
let bits = |params: &MultilinearAirParams| {
reduction_terms(params, 100, 99)
.iter()
.find(|term| term.label == "logup-star-reader-batching")
.unwrap()
.bits
.bits()
};
assert!((bits(&logup_star_params()) - bits(&wider) - 1.0).abs() < 1e-12);
}
#[test]
fn one_reader_per_table_needs_no_batching_term() {
let mut alone = logup_star_params();
let mut plan = alone.logup_star.unwrap();
plan.max_readers_per_table = 1;
alone.logup_star = Some(plan);
let terms = reduction_terms(&alone, 100, 99);
assert!(
!terms
.iter()
.any(|term| term.label == "logup-star-reader-batching")
);
}
#[test]
fn a_statement_without_indexed_lookups_charges_nothing_new() {
let terms = reduction_terms(
&MultilinearAirParams {
num_instances: 2,
max_num_constraints: 3,
num_variables: 4,
constraint_degree: 3,
lookup: None,
skip: None,
logup_star: None,
},
100,
99,
);
assert!(
!terms
.iter()
.any(|term| term.label.starts_with("logup-star-"))
);
}
#[test]
fn the_bit_ring_switch_charges_its_draws_and_its_rounds() {
let one = bit_ring_switch_error(1, 7, 4, 128).bits();
assert!((one - (128.0 - log2(15.0))).abs() < 1e-9, "{one}");
let four = bit_ring_switch_error(4, 7, 4, 128).bits();
assert!((one - four - 2.0).abs() < 1e-9, "{one} {four}");
let gap =
bit_ring_switch_error(4, 7, 4, 128).bits() - bit_ring_switch_error(4, 7, 4, 64).bits();
assert!((gap - 64.0).abs() < 1e-9, "{gap}");
assert!(bit_ring_switch_error(0, 7, 4, 128).bits().is_infinite());
assert!(bit_ring_switch_error(3, 0, 0, 128).bits().is_infinite());
let term = bit_ring_switch_term(4, 7, 4, 128);
assert_eq!(term.label, BIT_RING_SWITCH_LABEL);
assert_eq!(term.bits.bits(), four);
}
#[test]
fn column_batching_charges_one_coordinate_per_batch() {
let one = column_batch_error(1, 3, 128).bits();
let four = column_batch_error(4, 3, 128).bits();
assert!((one - (128.0 - log2(3.0))).abs() < 1e-9, "{one}");
assert!((one - four - 2.0).abs() < 1e-9, "{one} {four}");
assert!(column_batch_error(0, 3, 128).bits().is_infinite());
assert!(column_batch_error(4, 0, 128).bits().is_infinite());
let term = column_batch_term(4, 3, 128);
assert_eq!(term.label, COLUMN_BATCH_LABEL);
assert_eq!(term.bits.bits(), four);
}
#[test]
fn column_batching_does_not_cap_an_overflowing_count() {
let batches = usize::MAX;
let coordinates = usize::MAX;
let expected = 128.0 - log2(batches as f64) - log2(coordinates as f64);
let actual = column_batch_error(batches, coordinates, 128).bits();
assert!((actual - expected).abs() < 1e-9, "{actual} vs {expected}");
}
#[test]
fn successor_tensors_charge_their_batching_degree() {
assert_eq!(
bit_ring_switch_tensors_error(4, 1, 7, 4, 128).bits(),
bit_ring_switch_error(4, 7, 4, 128).bits()
);
let three = bit_ring_switch_tensors_error(1, 3, 7, 4, 128).bits();
assert!((three - (128.0 - libm::log2(17.0))).abs() < 1e-9);
let term = bit_ring_switch_tensors_term(1, 3, 7, 4, 128);
assert_eq!(term.label, BIT_RING_SWITCH_LABEL);
}
#[test]
fn claim_batching_charges_the_degree_of_lambda() {
assert!(
bit_ring_switch_claim_batching_error(0, 128)
.bits()
.is_infinite()
);
assert!(
bit_ring_switch_claim_batching_error(1, 128)
.bits()
.is_infinite()
);
assert_eq!(bit_ring_switch_claim_batching_error(2, 128).bits(), 128.0);
let five = bit_ring_switch_claim_batching_error(5, 128).bits();
assert!((five - 126.0).abs() < 1e-9, "{five}");
let batched = bit_ring_switch_error(1, 7, 4, 128).bits();
let separate = bit_ring_switch_error(5, 7, 4, 128).bits();
let union = -log2(libm::exp2(-batched) + libm::exp2(-five));
assert!((union - (128.0 - log2(19.0))).abs() < 1e-9, "{union}");
assert!(union > separate);
let term = bit_ring_switch_claim_batching_term(5, 128);
assert_eq!(term.label, BIT_RING_SWITCH_CLAIM_BATCHING_LABEL);
assert_eq!(term.bits.bits(), five);
}
}