1use alloc::vec::Vec;
12
13use crate::{ErrorBits, SecurityTerm};
14
15pub const BINARY_BUS_LABEL: &str = "binary-bus";
17
18pub const BUS_FINGERPRINT_LABEL: &str = "bus-tuple-fingerprint";
20
21pub const PRODUCT_GKR_SUMCHECK_LABEL: &str = "bus-product-gkr-sumcheck";
23
24pub const PRODUCT_GKR_BATCHING_LABEL: &str = "bus-product-gkr-tree-batching";
26
27pub const PRODUCT_GKR_COLLAPSE_LABEL: &str = "bus-product-gkr-child-collapse";
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub struct ProductGkrSecurityProfile {
33 log_height: usize,
35 tree_count: usize,
37 sumcheck_rounds: usize,
39 layer_count: usize,
41 collapse_challenges: usize,
43}
44
45impl ProductGkrSecurityProfile {
46 #[must_use]
52 pub const fn new(
53 log_height: usize,
54 tree_count: usize,
55 sumcheck_rounds: usize,
56 layer_count: usize,
57 collapse_challenges: usize,
58 ) -> Option<Self> {
59 if log_height >= usize::BITS as usize
60 || tree_count == 0
61 || tree_count > usize::MAX / 4
62 || collapse_challenges != log_height
63 || (log_height == 0) != (layer_count == 0)
64 {
65 return None;
66 }
67 Some(Self {
68 log_height,
69 tree_count,
70 sumcheck_rounds,
71 layer_count,
72 collapse_challenges,
73 })
74 }
75}
76
77#[derive(Clone, Copy, Debug, PartialEq, Eq)]
79pub struct BusSecurityModel {
80 field_bits: usize,
82 tuple_variables: usize,
84 non_padding_leaf_counts: [usize; 2],
86 product: ProductGkrSecurityProfile,
88}
89
90impl BusSecurityModel {
91 #[must_use]
98 pub const fn new(
99 field_bits: usize,
100 tuple_variables: usize,
101 non_padding_leaf_counts: [usize; 2],
102 product: ProductGkrSecurityProfile,
103 ) -> Option<Self> {
104 if field_bits == 0 || tuple_variables >= usize::BITS as usize {
106 return None;
107 }
108
109 let capacity = 1usize << product.log_height;
111 if non_padding_leaf_counts[0] > capacity
112 || non_padding_leaf_counts[1] > capacity
113 || (non_padding_leaf_counts[0] == 0 && non_padding_leaf_counts[1] == 0)
114 {
115 return None;
116 }
117
118 Some(Self {
119 field_bits,
120 tuple_variables,
121 non_padding_leaf_counts,
122 product,
123 })
124 }
125
126 #[must_use]
131 pub fn components(&self) -> Vec<SecurityTerm> {
132 let factors = self.non_padding_leaf_counts[0].max(self.non_padding_leaf_counts[1]);
135 let fingerprint_numerator = self.tuple_variables.max(1) as u128 * factors as u128;
136 let mut terms = alloc::vec![SecurityTerm::new(
137 BUS_FINGERPRINT_LABEL,
138 error_from_numerator(self.field_bits, fingerprint_numerator),
139 )];
140
141 push_nonzero_term(
143 &mut terms,
144 PRODUCT_GKR_SUMCHECK_LABEL,
145 self.field_bits,
146 5 * self.product.sumcheck_rounds as u128,
147 );
148 push_nonzero_term(
149 &mut terms,
150 PRODUCT_GKR_BATCHING_LABEL,
151 self.field_bits,
152 self.product.layer_count as u128 * self.product.tree_count.saturating_sub(1) as u128,
153 );
154 push_nonzero_term(
155 &mut terms,
156 PRODUCT_GKR_COLLAPSE_LABEL,
157 self.field_bits,
158 self.product.collapse_challenges as u128,
159 );
160
161 terms
162 }
163
164 #[must_use]
166 pub fn combined_term(&self) -> SecurityTerm {
167 let components = self.components();
168 let errors = components
169 .iter()
170 .map(|component| component.bits)
171 .collect::<Vec<_>>();
172 SecurityTerm::new(BINARY_BUS_LABEL, ErrorBits::sum(&errors))
173 }
174}
175
176fn push_nonzero_term(
178 terms: &mut Vec<SecurityTerm>,
179 label: &'static str,
180 field_bits: usize,
181 numerator: u128,
182) {
183 if numerator != 0 {
185 terms.push(SecurityTerm::new(
186 label,
187 error_from_numerator(field_bits, numerator),
188 ));
189 }
190}
191
192fn error_from_numerator(field_bits: usize, numerator: u128) -> ErrorBits {
194 let mut rounded = numerator as f64;
197 if (rounded as u128) < numerator {
198 rounded = rounded.next_up();
199 }
200 ErrorBits::from_log2((field_bits as f64 - libm::log2(rounded)).max(0.0))
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206 use crate::fri::FriRegime;
207 use crate::grinding::GrindingSites;
208 use crate::shape::{InstanceShape, StarkAirParams};
209 use crate::stark::proven_security_report;
210
211 fn profile(
212 log_height: usize,
213 tree_count: usize,
214 sumcheck_rounds: usize,
215 layer_count: usize,
216 ) -> ProductGkrSecurityProfile {
217 ProductGkrSecurityProfile::new(
218 log_height,
219 tree_count,
220 sumcheck_rounds,
221 layer_count,
222 log_height,
223 )
224 .unwrap()
225 }
226
227 fn bits(terms: &[SecurityTerm], label: &str) -> Option<f64> {
228 terms
230 .iter()
231 .find(|term| term.label == label)
232 .map(|term| term.bits.bits())
233 }
234
235 #[test]
236 fn exact_terms_follow_a_twenty_variable_bus() {
237 let model = BusSecurityModel::new(128, 4, [1 << 20, 1 << 18], profile(20, 2, 90, 10))
244 .expect("the dimensions fit the product tree");
245 let terms = model.components();
246
247 assert_eq!(
248 bits(&terms, BUS_FINGERPRINT_LABEL),
249 Some(128.0 - libm::log2((4u64 << 20) as f64))
250 );
251 assert_eq!(
252 bits(&terms, PRODUCT_GKR_SUMCHECK_LABEL),
253 Some(128.0 - libm::log2(450.0))
254 );
255 assert_eq!(
256 bits(&terms, PRODUCT_GKR_BATCHING_LABEL),
257 Some(128.0 - libm::log2(10.0))
258 );
259 assert_eq!(
260 bits(&terms, PRODUCT_GKR_COLLAPSE_LABEL),
261 Some(128.0 - libm::log2(20.0))
262 );
263 }
264
265 #[test]
266 fn zero_height_omits_every_product_reduction_term() {
267 let terms = BusSecurityModel::new(128, 0, [1, 1], profile(0, 2, 0, 0))
270 .expect("one factor fits a zero-height tree")
271 .components();
272
273 assert_eq!(terms.len(), 1);
274 assert_eq!(terms[0].label, BUS_FINGERPRINT_LABEL);
275 assert_eq!(terms[0].bits.bits(), 128.0);
276 }
277
278 #[test]
279 fn odd_height_charges_the_leading_binary_collapse() {
280 let terms = BusSecurityModel::new(128, 1, [8, 8], profile(3, 2, 1, 2))
283 .expect("eight factors fit a height-three tree")
284 .components();
285
286 assert_eq!(
287 bits(&terms, PRODUCT_GKR_SUMCHECK_LABEL),
288 Some(128.0 - libm::log2(5.0))
289 );
290 assert_eq!(bits(&terms, PRODUCT_GKR_BATCHING_LABEL), Some(127.0));
291 assert_eq!(
292 bits(&terms, PRODUCT_GKR_COLLAPSE_LABEL),
293 Some(128.0 - libm::log2(3.0))
294 );
295 }
296
297 #[test]
298 fn malformed_or_oversized_shapes_have_no_model() {
299 assert!(BusSecurityModel::new(0, 1, [1, 1], profile(0, 2, 0, 0)).is_none());
308 assert!(ProductGkrSecurityProfile::new(0, 0, 0, 0, 0).is_none());
309 assert!(ProductGkrSecurityProfile::new(0, usize::MAX, 0, 0, 0).is_none());
310 assert!(
311 BusSecurityModel::new(128, usize::BITS as usize, [1, 1], profile(0, 2, 0, 0),)
312 .is_none()
313 );
314 assert!(ProductGkrSecurityProfile::new(usize::BITS as usize, 2, 0, 0, 0).is_none());
315 assert!(BusSecurityModel::new(128, 1, [2, 1], profile(0, 2, 0, 0)).is_none());
316 assert!(BusSecurityModel::new(128, 1, [0, 0], profile(0, 2, 0, 0)).is_none());
317 assert!(ProductGkrSecurityProfile::new(1, 2, 0, 0, 0).is_none());
318 }
319
320 #[test]
321 fn a_192_bit_field_adds_sixty_four_bits_to_every_term() {
322 let narrow = BusSecurityModel::new(128, 4, [1 << 20, 1 << 18], profile(20, 2, 90, 10))
325 .expect("the 128-bit model is valid")
326 .components();
327 let wide = BusSecurityModel::new(192, 4, [1 << 20, 1 << 18], profile(20, 2, 90, 10))
328 .expect("the 192-bit model is valid")
329 .components();
330
331 assert_eq!(narrow.len(), wide.len());
332 for (narrow_term, wide_term) in narrow.iter().zip(wide) {
333 assert_eq!(narrow_term.label, wide_term.label);
334 assert!((wide_term.bits.bits() - narrow_term.bits.bits() - 64.0).abs() < 1e-12);
335 }
336 }
337
338 #[test]
339 fn combined_term_sums_all_component_probabilities() {
340 let model = BusSecurityModel::new(128, 1, [8, 8], profile(3, 2, 1, 2)).unwrap();
342 let components = model.components();
343 let expected = ErrorBits::sum(
344 &components
345 .iter()
346 .map(|component| component.bits)
347 .collect::<Vec<_>>(),
348 );
349 assert_eq!(model.combined_term().bits, expected);
350 assert_eq!(model.combined_term().label, BINARY_BUS_LABEL);
351 }
352
353 #[test]
354 fn combined_term_composes_as_one_protocol_extra() {
355 let regime = FriRegime {
357 log_blowup: 1,
358 num_queries: 100,
359 log_final_poly_len: 0,
360 max_log_arity: 3,
361 commit_pow_bits: 0,
362 query_pow_bits: 16,
363 };
364 let air = StarkAirParams {
365 num_constraints: 1,
366 max_constraint_degree: 2,
367 num_quotient_chunks: 1,
368 max_combo: 2,
369 };
370 let shape = InstanceShape {
371 log_trace_length: 20,
372 modulus_bits: 64,
373 collision_resistance: 128,
374 num_batched_functions: 1,
375 };
376 let model =
377 BusSecurityModel::new(64, 4, [1 << 20, 1 << 18], profile(20, 2, 90, 10)).unwrap();
378 let term = model.combined_term();
379
380 let report = proven_security_report(®ime, &air, &shape, &[term], &GrindingSites::NONE);
382 assert!(
383 report
384 .udr
385 .terms()
386 .iter()
387 .any(|candidate| candidate.label == BINARY_BUS_LABEL)
388 );
389 assert!(!report.udr.terms().iter().any(|candidate| {
390 [
391 BUS_FINGERPRINT_LABEL,
392 PRODUCT_GKR_SUMCHECK_LABEL,
393 PRODUCT_GKR_BATCHING_LABEL,
394 PRODUCT_GKR_COLLAPSE_LABEL,
395 ]
396 .contains(&candidate.label)
397 }));
398 }
399
400 #[test]
401 fn large_numerator_rounding_branch_is_exercised() {
402 let model = BusSecurityModel::new(128, 3, [(1usize << 62) + 1, 1], profile(63, 2, 961, 32))
404 .unwrap();
405 assert!(model.components()[0].bits.bits().is_finite());
406 }
407}