1use crate::errors;
6use core::fmt;
7
8pub const MIN_PRODUCTION_BITS: usize = 100;
10
11pub const INV_RATE: usize = 2;
14
15pub const OUTER_RATE_LOG2: u32 = 4;
18
19pub const OUTER_ROWS: usize = 32;
21
22pub(crate) const LOG2_FRAC_BITS: u32 = 32;
26
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29pub enum Error {
30 SecurityTooLow {
32 estimated_bits: usize,
33 min_bits: usize,
34 },
35
36 InsufficientSupport {
40 ldt_support_size: usize,
41 num_queries: usize,
42 },
43
44 EmptyOuterStatement,
47
48 ZeroOuterQueries,
51
52 OuterDomainTooLarge { code_len: usize, max_log2: u32 },
55}
56
57impl fmt::Display for Error {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 match self {
60 Self::SecurityTooLow {
61 estimated_bits,
62 min_bits,
63 } => write!(
64 f,
65 "Security too low: estimated {estimated_bits} bits, but {min_bits} required",
66 ),
67 Self::InsufficientSupport {
68 ldt_support_size,
69 num_queries,
70 } => write!(
71 f,
72 "ldt_support_size ({ldt_support_size}) must be >= num_queries ({num_queries})",
73 ),
74 Self::EmptyOuterStatement => {
75 write!(f, "outer statement masks no scalars")
76 }
77 Self::ZeroOuterQueries => write!(f, "outer_queries cannot be zero"),
78 Self::OuterDomainTooLarge { code_len, max_log2 } => write!(
79 f,
80 "outer code length {code_len} needs a domain above the 2^{max_log2} cap",
81 ),
82 }
83 }
84}
85
86#[derive(Clone, Copy, Debug)]
88pub struct SecurityMetrics {
89 pub relative_distance: f64,
91
92 pub num_queries: usize,
94
95 pub ldt_bits: usize,
97
98 pub proximity_bits: usize,
100
101 pub security_bits: usize,
103}
104
105#[derive(Clone, Copy, Debug, Eq, PartialEq)]
109pub struct FoldShape {
110 pub grid_cols: usize,
111 pub grid_rows: usize,
112 pub units: usize,
113}
114
115#[derive(Clone, Copy, Debug, Eq, PartialEq)]
117pub struct TableGeom {
118 pub support_size: usize,
120
121 pub encoded_width: usize,
123}
124
125#[derive(Clone, Debug)]
126pub struct Config {
127 pub num_queries: usize,
129
130 pub ldt_support_size: usize,
134
135 pub min_security_bits: usize,
138
139 pub outer_queries: usize,
141
142 pub zero_knowledge: bool,
145}
146
147impl Default for Config {
148 fn default() -> Self {
149 Self::prod()
150 }
151}
152
153impl Config {
154 pub fn prod() -> Self {
157 Self {
158 num_queries: 287,
159 ldt_support_size: 287,
160 min_security_bits: MIN_PRODUCTION_BITS,
161 outer_queries: 121,
162 zero_knowledge: true,
163 }
164 }
165
166 pub fn dev() -> Self {
169 Self {
170 num_queries: 4,
171 min_security_bits: 0,
172 outer_queries: 4,
173 zero_knowledge: false,
174 ..Self::prod()
175 }
176 }
177
178 pub fn blind_units(&self) -> usize {
180 usize::from(self.zero_knowledge)
181 }
182
183 pub fn encoded_width(&self, grid_cols: usize) -> usize {
185 self.table_geom(grid_cols).encoded_width
186 }
187
188 pub fn table_geom(&self, grid_cols: usize) -> TableGeom {
192 let frac = TableGeom {
193 support_size: self.ldt_support_size,
194 encoded_width: grid_cols * INV_RATE,
195 };
196
197 let frac_msg = frac.support_size + grid_cols;
198
199 if frac.support_size <= grid_cols
200 && self.ldt_bits(frac_msg, frac.encoded_width) >= MIN_PRODUCTION_BITS
201 {
202 return frac;
203 }
204
205 TableGeom {
206 support_size: grid_cols,
207 encoded_width: grid_cols * INV_RATE * 2,
208 }
209 }
210
211 pub fn estimated_security_bits(&self, field_bits: usize, shape: FoldShape) -> usize {
216 let g = self.table_geom(shape.grid_cols);
217
218 self.ldt_bits(g.support_size + shape.grid_cols, g.encoded_width)
219 .min(self.proximity_gap_bits(field_bits, shape))
220 }
221
222 pub fn proximity_gap_bits(&self, field_bits: usize, shape: FoldShape) -> usize {
226 let width = self.table_geom(shape.grid_cols).encoded_width;
227
228 let theta = shape.grid_rows.next_power_of_two().ilog2() as usize;
229 let fold_terms = (theta + shape.units.saturating_sub(1)).max(1);
230
231 field_bits
232 .saturating_sub(width.next_power_of_two().ilog2() as usize)
233 .saturating_sub(fold_terms.next_power_of_two().ilog2() as usize)
234 }
235
236 pub fn security_metrics(&self, field_bits: usize, shape: FoldShape) -> SecurityMetrics {
239 let g = self.table_geom(shape.grid_cols);
240
241 SecurityMetrics {
242 relative_distance: self.estimate_relative_distance(shape.grid_cols),
243 num_queries: self.num_queries,
244 ldt_bits: self.ldt_bits(g.support_size + shape.grid_cols, g.encoded_width),
245 proximity_bits: self.proximity_gap_bits(field_bits, shape),
246 security_bits: self.estimated_security_bits(field_bits, shape),
247 }
248 }
249
250 pub fn check_security(&self, field_bits: usize, shape: FoldShape) -> errors::Result<()> {
253 let support = self.table_geom(shape.grid_cols).support_size;
255 if self.min_security_bits > 0 && support < self.num_queries {
256 return Err(Error::InsufficientSupport {
257 ldt_support_size: support,
258 num_queries: self.num_queries,
259 }
260 .into());
261 }
262
263 let est_bits = self.estimated_security_bits(field_bits, shape);
264 if est_bits < self.min_security_bits {
265 return Err(Error::SecurityTooLow {
266 estimated_bits: est_bits,
267 min_bits: self.min_security_bits,
268 }
269 .into());
270 }
271
272 Ok(())
273 }
274
275 pub fn check_logup_security(&self, field_bits: usize, bus_rows: u64) -> errors::Result<()> {
278 if self.min_security_bits == 0 {
279 return Ok(());
280 }
281
282 let bits = self.logup_gamma_bits(field_bits, bus_rows);
283
284 if bits < self.min_security_bits {
285 return Err(Error::SecurityTooLow {
286 estimated_bits: bits,
287 min_bits: self.min_security_bits,
288 }
289 .into());
290 }
291
292 Ok(())
293 }
294
295 fn estimate_relative_distance(&self, grid_cols: usize) -> f64 {
300 let g = self.table_geom(grid_cols);
301
302 g.encoded_width.saturating_sub(g.support_size + grid_cols) as f64 / g.encoded_width as f64
303 }
304
305 fn ldt_bits(&self, msg_len: usize, code_width: usize) -> usize {
309 if msg_len >= code_width {
310 return 0;
311 }
312
313 let log2_ratio =
314 log2_ratio_fixed(2 * code_width as u128, code_width as u128 + msg_len as u128);
315
316 ((self.num_queries as u128 * log2_ratio) >> LOG2_FRAC_BITS) as usize
317 }
318
319 pub fn logup_gamma_bits(&self, field_bits: usize, bus_rows: u64) -> usize {
323 let rows = bus_rows.max(1).next_power_of_two().ilog2() as usize;
324
325 field_bits.saturating_sub(rows)
326 }
327}
328
329pub(crate) fn log2_ratio_fixed(n: u128, m: u128) -> u128 {
332 const S: u32 = 60;
333
334 let mut scaled_m = m;
335 let mut int_part: u128 = 0;
336
337 while scaled_m <= n / 2 {
338 scaled_m <<= 1;
339 int_part += 1;
340 }
341
342 let mut y = (n << S) / scaled_m;
345 let mut frac: u128 = 0;
346
347 for i in 0..LOG2_FRAC_BITS {
348 y = (y * y) >> S;
349
350 if y >= 2u128 << S {
351 y >>= 1;
352 frac |= 1u128 << (LOG2_FRAC_BITS - 1 - i);
353 }
354 }
355
356 (int_part << LOG2_FRAC_BITS) | frac
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362
363 const GRID_COLS: usize = 1024;
364
365 fn flat(grid_cols: usize) -> FoldShape {
368 FoldShape {
369 grid_cols,
370 grid_rows: 1,
371 units: 1,
372 }
373 }
374
375 #[test]
376 fn default_is_prod() {
377 assert_eq!(Config::default().min_security_bits, MIN_PRODUCTION_BITS);
378 assert_eq!(Config::default().num_queries, Config::prod().num_queries);
379 }
380
381 #[test]
382 fn prod_meets_production_floor() {
383 let prod = Config::prod();
384
385 assert!(prod.estimated_security_bits(128, flat(GRID_COLS)) >= MIN_PRODUCTION_BITS);
386 assert!(prod.check_security(128, flat(GRID_COLS)).is_ok());
387 }
388
389 #[test]
390 fn dev_is_lenient_on_weak_params() {
391 let dev = Config::dev();
392
393 assert!(dev.estimated_security_bits(128, flat(GRID_COLS)) < MIN_PRODUCTION_BITS);
394 assert!(dev.check_security(128, flat(GRID_COLS)).is_ok());
395 }
396
397 #[test]
398 fn prod_threshold_rejects_weak_queries() {
399 let weak = Config {
400 num_queries: 4,
401 ..Config::prod()
402 };
403
404 assert!(weak.check_security(128, flat(GRID_COLS)).is_err());
405 }
406
407 #[test]
408 fn full_half_fallback_admits_ml_dsa_grid() {
409 assert!(Config::prod().check_security(128, flat(512)).is_ok());
410 }
411
412 #[test]
413 fn grid_below_num_queries_rejected() {
414 assert!(Config::prod().check_security(128, flat(128)).is_err());
415 }
416
417 #[test]
418 fn ldt_bits_matches_float_within_one_bit() {
419 let cfg = Config::prod();
420
421 for log_g in 8usize..=20 {
422 let grid_cols = 1usize << log_g;
423 let g = cfg.table_geom(grid_cols);
424 let msg: usize = g.support_size + grid_cols;
425
426 if msg >= g.encoded_width {
427 continue;
428 }
429
430 let rho = msg as f64 / g.encoded_width as f64;
431 let per_query = (1.0 + rho) / 2.0;
432 let reference = (-per_query.powf(cfg.num_queries as f64).log2()).floor();
433 let integer = cfg.ldt_bits(msg, g.encoded_width) as f64;
434
435 assert!(
436 (integer - reference).abs() <= 1.0,
437 "grid 2^{log_g}: integer {integer} vs float {reference}",
438 );
439 }
440 }
441
442 #[test]
443 fn table_geom_selects_integer_stable_modes() {
444 let prod = Config::prod();
445
446 let big = prod.table_geom(1 << 12);
447 assert_eq!(big.support_size, prod.ldt_support_size);
448 assert_eq!(big.encoded_width, INV_RATE << 12);
449
450 let small = prod.table_geom(512);
451 assert_eq!(small.support_size, 512);
452 assert_eq!(small.encoded_width, INV_RATE * 512 * 2);
453 }
454
455 #[test]
456 fn prod_queries_sit_on_fractional_mode_edge() {
457 let at = |t: usize| Config {
458 num_queries: t,
459 ldt_support_size: t,
460 ..Config::prod()
461 };
462
463 let binding = 1usize << 11;
464
465 assert_eq!(Config::prod().num_queries, 287);
466 assert_eq!(at(287).encoded_width(binding), binding * INV_RATE);
467 assert_eq!(at(286).encoded_width(binding), binding * INV_RATE * 2);
468 }
469
470 #[test]
471 fn grid_1024_never_clears_floor_fractionally() {
472 let best = (200..=1200)
473 .map(|t| {
474 let cfg = Config {
475 num_queries: t,
476 ldt_support_size: t,
477 ..Config::prod()
478 };
479
480 cfg.ldt_bits(t + 1024, 2048)
481 })
482 .max()
483 .unwrap();
484
485 assert!(best < MIN_PRODUCTION_BITS, "peaked at {best}");
486 }
487
488 #[test]
489 fn proximity_gap_charges_tensor_and_curve_factors() {
490 let cfg = Config::prod();
491 let grid_cols = 1usize << 14;
492
493 let width_only = cfg.proximity_gap_bits(128, flat(grid_cols));
494
495 let charged = cfg.proximity_gap_bits(
496 128,
497 FoldShape {
498 grid_cols,
499 grid_rows: 1 << 7,
500 units: 80,
501 },
502 );
503
504 assert_eq!(cfg.encoded_width(grid_cols), 1 << 15);
505 assert_eq!(width_only, 128 - 15);
506 assert_eq!(
507 width_only - charged,
508 (7 + 79usize).next_power_of_two().ilog2() as usize
509 );
510 }
511
512 #[test]
513 fn logup_gamma_sits_on_production_floor_edge() {
514 let prod = Config::prod();
515 let edge = 1u64 << (128 - MIN_PRODUCTION_BITS);
516
517 assert_eq!(prod.logup_gamma_bits(128, edge), MIN_PRODUCTION_BITS);
518 assert_eq!(
519 prod.logup_gamma_bits(128, edge + 1),
520 MIN_PRODUCTION_BITS - 1
521 );
522
523 assert!(prod.check_logup_security(128, edge).is_ok());
524 assert!(prod.check_logup_security(128, edge + 1).is_err());
525 }
526
527 #[test]
528 fn logup_gamma_clears_for_busless_and_dev_proofs() {
529 let prod = Config::prod();
530
531 assert_eq!(prod.logup_gamma_bits(128, 0), 128);
532 assert_eq!(prod.logup_gamma_bits(128, 1), 128);
533 assert!(prod.check_logup_security(128, 0).is_ok());
534
535 let dev = Config::dev();
536
537 assert_eq!(dev.min_security_bits, 0);
538 assert!(dev.check_logup_security(128, 1 << 60).is_ok());
539 }
540}