1use core::fmt;
6
7use crate::errors;
8use crate::utils::support_floor_vars;
9
10pub const MIN_PRODUCTION_BITS: usize = 100;
12
13pub const MAX_TABLE_VARS: usize = 32;
16
17pub const INV_RATE: usize = 2;
20
21pub const OUTER_RATE_LOG2: u32 = 4;
24
25pub const OUTER_ROWS: usize = 32;
27
28pub(crate) const LOG2_FRAC_BITS: u32 = 32;
32
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
35pub enum Error {
36 SecurityTooLow {
38 estimated_bits: usize,
39 min_bits: usize,
40 },
41
42 InsufficientSupport {
46 ldt_support_size: usize,
47 num_queries: usize,
48 },
49
50 EmptyOuterStatement,
53
54 ZeroOuterQueries,
57
58 OuterDomainTooLarge { code_len: usize, max_log2: u32 },
61}
62
63impl fmt::Display for Error {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 match self {
66 Self::SecurityTooLow {
67 estimated_bits,
68 min_bits,
69 } => write!(
70 f,
71 "Security too low: estimated {estimated_bits} bits, but {min_bits} required",
72 ),
73 Self::InsufficientSupport {
74 ldt_support_size,
75 num_queries,
76 } => write!(
77 f,
78 "ldt_support_size ({ldt_support_size}) must be > num_queries ({num_queries})",
79 ),
80 Self::EmptyOuterStatement => {
81 write!(f, "outer statement masks no scalars")
82 }
83 Self::ZeroOuterQueries => write!(f, "outer_queries cannot be zero"),
84 Self::OuterDomainTooLarge { code_len, max_log2 } => write!(
85 f,
86 "outer code length {code_len} needs a domain above the 2^{max_log2} cap",
87 ),
88 }
89 }
90}
91
92#[derive(Clone, Copy, Debug)]
94pub struct SecurityMetrics {
95 pub relative_distance: f64,
97
98 pub num_queries: usize,
100
101 pub ldt_bits: usize,
103
104 pub proximity_bits: usize,
106
107 pub security_bits: usize,
109}
110
111#[derive(Clone, Copy, Debug, Eq, PartialEq)]
115pub struct FoldShape {
116 pub grid_cols: usize,
117 pub grid_rows: usize,
118 pub units: usize,
119}
120
121#[derive(Clone, Copy, Debug, Eq, PartialEq)]
123pub struct TableGeom {
124 pub support_size: usize,
126
127 pub encoded_width: usize,
129}
130
131#[derive(Clone, Debug)]
132pub struct Config {
133 pub num_queries: usize,
135
136 pub ldt_support_size: usize,
140
141 pub min_security_bits: usize,
144
145 pub outer_queries: usize,
147
148 pub zero_knowledge: bool,
151}
152
153impl Default for Config {
154 fn default() -> Self {
155 Self::prod()
156 }
157}
158
159impl Config {
160 pub fn prod() -> Self {
163 Self {
164 num_queries: 287,
165 ldt_support_size: 288,
166 min_security_bits: MIN_PRODUCTION_BITS,
167 outer_queries: 121,
168 zero_knowledge: true,
169 }
170 }
171
172 pub fn dev() -> Self {
175 Self {
176 num_queries: 4,
177 min_security_bits: 0,
178 outer_queries: 4,
179 zero_knowledge: false,
180 ..Self::prod()
181 }
182 }
183
184 pub fn blind_units(&self) -> usize {
186 usize::from(self.zero_knowledge)
187 }
188
189 pub fn min_table_rows(&self) -> usize {
191 1 << support_floor_vars(self.ldt_support_size)
192 }
193
194 pub fn encoded_width(&self, grid_cols: usize) -> usize {
196 self.table_geom(grid_cols).encoded_width
197 }
198
199 pub fn table_geom(&self, grid_cols: usize) -> TableGeom {
203 let frac = TableGeom {
204 support_size: self.ldt_support_size,
205 encoded_width: grid_cols * INV_RATE,
206 };
207
208 let frac_msg = frac.support_size + grid_cols;
209
210 if frac.support_size <= grid_cols
211 && self.ldt_bits(frac_msg, frac.encoded_width) >= MIN_PRODUCTION_BITS
212 {
213 return frac;
214 }
215
216 TableGeom {
217 support_size: grid_cols,
218 encoded_width: grid_cols * INV_RATE * 2,
219 }
220 }
221
222 pub fn estimated_security_bits(&self, field_bits: usize, shape: FoldShape) -> usize {
227 let g = self.table_geom(shape.grid_cols);
228
229 self.ldt_bits(g.support_size + shape.grid_cols, g.encoded_width)
230 .min(self.proximity_gap_bits(field_bits, shape))
231 }
232
233 pub fn proximity_gap_bits(&self, field_bits: usize, shape: FoldShape) -> usize {
237 let width = self.table_geom(shape.grid_cols).encoded_width;
238
239 let theta = shape.grid_rows.next_power_of_two().ilog2() as usize;
240 let fold_terms = (theta + shape.units.saturating_sub(1)).max(1);
241
242 field_bits
243 .saturating_sub(width.next_power_of_two().ilog2() as usize)
244 .saturating_sub(fold_terms.next_power_of_two().ilog2() as usize)
245 }
246
247 pub fn security_metrics(&self, field_bits: usize, shape: FoldShape) -> SecurityMetrics {
250 let g = self.table_geom(shape.grid_cols);
251
252 SecurityMetrics {
253 relative_distance: self.estimate_relative_distance(shape.grid_cols),
254 num_queries: self.num_queries,
255 ldt_bits: self.ldt_bits(g.support_size + shape.grid_cols, g.encoded_width),
256 proximity_bits: self.proximity_gap_bits(field_bits, shape),
257 security_bits: self.estimated_security_bits(field_bits, shape),
258 }
259 }
260
261 pub fn check_security(&self, field_bits: usize, shape: FoldShape) -> errors::Result<()> {
264 let support = self.table_geom(shape.grid_cols).support_size;
266 if self.min_security_bits > 0 && support <= self.num_queries {
267 return Err(Error::InsufficientSupport {
268 ldt_support_size: support,
269 num_queries: self.num_queries,
270 }
271 .into());
272 }
273
274 let est_bits = self.estimated_security_bits(field_bits, shape);
275 if est_bits < self.min_security_bits {
276 return Err(Error::SecurityTooLow {
277 estimated_bits: est_bits,
278 min_bits: self.min_security_bits,
279 }
280 .into());
281 }
282
283 Ok(())
284 }
285
286 pub fn check_logup_security(&self, field_bits: usize, bus_rows: u64) -> errors::Result<()> {
289 if self.min_security_bits == 0 {
290 return Ok(());
291 }
292
293 let bits = self.logup_gamma_bits(field_bits, bus_rows);
294
295 if bits < self.min_security_bits {
296 return Err(Error::SecurityTooLow {
297 estimated_bits: bits,
298 min_bits: self.min_security_bits,
299 }
300 .into());
301 }
302
303 Ok(())
304 }
305
306 fn estimate_relative_distance(&self, grid_cols: usize) -> f64 {
311 let g = self.table_geom(grid_cols);
312
313 g.encoded_width.saturating_sub(g.support_size + grid_cols) as f64 / g.encoded_width as f64
314 }
315
316 fn ldt_bits(&self, msg_len: usize, code_width: usize) -> usize {
320 if msg_len >= code_width {
321 return 0;
322 }
323
324 let log2_ratio =
325 log2_ratio_fixed(2 * code_width as u128, code_width as u128 + msg_len as u128);
326
327 ((self.num_queries as u128 * log2_ratio) >> LOG2_FRAC_BITS) as usize
328 }
329
330 pub fn logup_gamma_bits(&self, field_bits: usize, bus_rows: u64) -> usize {
334 let rows = bus_rows.max(1).next_power_of_two().ilog2() as usize;
335
336 field_bits.saturating_sub(rows)
337 }
338}
339
340pub(crate) fn log2_ratio_fixed(n: u128, m: u128) -> u128 {
343 const S: u32 = 60;
344
345 let mut scaled_m = m;
346 let mut int_part: u128 = 0;
347
348 while scaled_m <= n / 2 {
349 scaled_m <<= 1;
350 int_part += 1;
351 }
352
353 let mut y = (n << S) / scaled_m;
356 let mut frac: u128 = 0;
357
358 for i in 0..LOG2_FRAC_BITS {
359 y = (y * y) >> S;
360
361 if y >= 2u128 << S {
362 y >>= 1;
363 frac |= 1u128 << (LOG2_FRAC_BITS - 1 - i);
364 }
365 }
366
367 (int_part << LOG2_FRAC_BITS) | frac
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373
374 const GRID_COLS: usize = 1024;
375
376 fn flat(grid_cols: usize) -> FoldShape {
379 FoldShape {
380 grid_cols,
381 grid_rows: 1,
382 units: 1,
383 }
384 }
385
386 #[test]
387 fn default_is_prod() {
388 assert_eq!(Config::default().min_security_bits, MIN_PRODUCTION_BITS);
389 assert_eq!(Config::default().num_queries, Config::prod().num_queries);
390 }
391
392 #[test]
393 fn prod_meets_production_floor() {
394 let prod = Config::prod();
395
396 assert!(prod.estimated_security_bits(128, flat(GRID_COLS)) >= MIN_PRODUCTION_BITS);
397 assert!(prod.check_security(128, flat(GRID_COLS)).is_ok());
398 }
399
400 #[test]
401 fn dev_is_lenient_on_weak_params() {
402 let dev = Config::dev();
403
404 assert!(dev.estimated_security_bits(128, flat(GRID_COLS)) < MIN_PRODUCTION_BITS);
405 assert!(dev.check_security(128, flat(GRID_COLS)).is_ok());
406 }
407
408 #[test]
409 fn prod_threshold_rejects_weak_queries() {
410 let weak = Config {
411 num_queries: 4,
412 ..Config::prod()
413 };
414
415 assert!(weak.check_security(128, flat(GRID_COLS)).is_err());
416 }
417
418 #[test]
419 fn full_half_fallback_admits_ml_dsa_grid() {
420 assert!(Config::prod().check_security(128, flat(512)).is_ok());
421 }
422
423 #[test]
424 fn grid_below_num_queries_rejected() {
425 assert!(Config::prod().check_security(128, flat(128)).is_err());
426 }
427
428 #[test]
429 fn ldt_bits_matches_float_within_one_bit() {
430 let cfg = Config::prod();
431
432 for log_g in 8usize..=20 {
433 let grid_cols = 1usize << log_g;
434 let g = cfg.table_geom(grid_cols);
435 let msg: usize = g.support_size + grid_cols;
436
437 if msg >= g.encoded_width {
438 continue;
439 }
440
441 let rho = msg as f64 / g.encoded_width as f64;
442 let per_query = (1.0 + rho) / 2.0;
443 let reference = (-per_query.powf(cfg.num_queries as f64).log2()).floor();
444 let integer = cfg.ldt_bits(msg, g.encoded_width) as f64;
445
446 assert!(
447 (integer - reference).abs() <= 1.0,
448 "grid 2^{log_g}: integer {integer} vs float {reference}",
449 );
450 }
451 }
452
453 #[test]
454 fn table_geom_selects_integer_stable_modes() {
455 let prod = Config::prod();
456
457 let big = prod.table_geom(1 << 12);
458 assert_eq!(big.support_size, prod.ldt_support_size);
459 assert_eq!(big.encoded_width, INV_RATE << 12);
460
461 let small = prod.table_geom(512);
462 assert_eq!(small.support_size, 512);
463 assert_eq!(small.encoded_width, INV_RATE * 512 * 2);
464 }
465
466 #[test]
467 fn prod_queries_sit_on_fractional_mode_edge() {
468 let at = |t: usize| Config {
469 num_queries: t,
470 ldt_support_size: t + 1,
471 ..Config::prod()
472 };
473
474 let binding = 1usize << 11;
475
476 assert_eq!(Config::prod().num_queries, 287);
477 assert_eq!(at(287).encoded_width(binding), binding * INV_RATE);
478 assert_eq!(at(286).encoded_width(binding), binding * INV_RATE * 2);
479 }
480
481 #[test]
482 fn grid_1024_never_clears_floor_fractionally() {
483 let best = (200..=1200)
484 .map(|t| {
485 let cfg = Config {
486 num_queries: t,
487 ldt_support_size: t,
488 ..Config::prod()
489 };
490
491 cfg.ldt_bits(t + 1024, 2048)
492 })
493 .max()
494 .unwrap();
495
496 assert!(best < MIN_PRODUCTION_BITS, "peaked at {best}");
497 }
498
499 #[test]
500 fn proximity_gap_charges_tensor_and_curve_factors() {
501 let cfg = Config::prod();
502 let grid_cols = 1usize << 14;
503
504 let width_only = cfg.proximity_gap_bits(128, flat(grid_cols));
505
506 let charged = cfg.proximity_gap_bits(
507 128,
508 FoldShape {
509 grid_cols,
510 grid_rows: 1 << 7,
511 units: 80,
512 },
513 );
514
515 assert_eq!(cfg.encoded_width(grid_cols), 1 << 15);
516 assert_eq!(width_only, 128 - 15);
517 assert_eq!(
518 width_only - charged,
519 (7 + 79usize).next_power_of_two().ilog2() as usize
520 );
521 }
522
523 #[test]
524 fn logup_gamma_sits_on_production_floor_edge() {
525 let prod = Config::prod();
526 let edge = 1u64 << (128 - MIN_PRODUCTION_BITS);
527
528 assert_eq!(prod.logup_gamma_bits(128, edge), MIN_PRODUCTION_BITS);
529 assert_eq!(
530 prod.logup_gamma_bits(128, edge + 1),
531 MIN_PRODUCTION_BITS - 1
532 );
533
534 assert!(prod.check_logup_security(128, edge).is_ok());
535 assert!(prod.check_logup_security(128, edge + 1).is_err());
536 }
537
538 #[test]
539 fn logup_gamma_clears_for_busless_and_dev_proofs() {
540 let prod = Config::prod();
541
542 assert_eq!(prod.logup_gamma_bits(128, 0), 128);
543 assert_eq!(prod.logup_gamma_bits(128, 1), 128);
544 assert!(prod.check_logup_security(128, 0).is_ok());
545
546 let dev = Config::dev();
547
548 assert_eq!(dev.min_security_bits, 0);
549 assert!(dev.check_logup_security(128, 1 << 60).is_ok());
550 }
551
552 #[test]
553 fn support_below_queries_rejected() {
554 let config = Config {
555 ldt_support_size: 32,
556 ..Config::prod()
557 };
558
559 assert!(matches!(
560 config.check_security(128, flat(1 << 12)),
561 Err(errors::Error::Config(Error::InsufficientSupport { .. }))
562 ));
563 }
564
565 #[test]
566 fn support_equal_to_queries_rejected() {
567 let config = Config {
568 ldt_support_size: Config::prod().num_queries,
569 ..Config::prod()
570 };
571
572 assert!(matches!(
573 config.check_security(128, flat(1 << 12)),
574 Err(errors::Error::Config(Error::InsufficientSupport { .. }))
575 ));
576 }
577
578 #[test]
579 fn support_one_above_queries_accepted() {
580 let prod = Config::prod();
581
582 assert_eq!(prod.ldt_support_size, prod.num_queries + 1);
583 assert!(prod.check_security(128, flat(1 << 12)).is_ok());
584 }
585
586 #[test]
587 fn support_below_queries_waived_in_dev() {
588 let config = Config {
589 ldt_support_size: 32,
590 min_security_bits: 0,
591 ..Config::prod()
592 };
593
594 assert!(config.check_security(128, flat(1 << 12)).is_ok());
595 }
596}