1use crate::errors;
19use core::fmt;
20
21pub const MIN_PRODUCTION_BITS: usize = 128;
25
26const LOG2_FRAC_BITS: u32 = 32;
30
31#[derive(Clone, Copy, Debug, Eq, PartialEq)]
33pub enum Error {
34 SecurityTooLow {
36 estimated_bits: usize,
37 min_bits: usize,
38 },
39
40 InsufficientSupport {
44 ldt_support_size: usize,
45 num_queries: usize,
46 },
47
48 InvalidInvRate { inv_rate: usize },
52}
53
54impl fmt::Display for Error {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 match self {
57 Self::SecurityTooLow {
58 estimated_bits,
59 min_bits,
60 } => write!(
61 f,
62 "Security too low: estimated {estimated_bits} bits, but {min_bits} required",
63 ),
64 Self::InsufficientSupport {
65 ldt_support_size,
66 num_queries,
67 } => write!(
68 f,
69 "ldt_support_size ({ldt_support_size}) must be >= num_queries ({num_queries})",
70 ),
71 Self::InvalidInvRate { inv_rate } => {
72 write!(f, "inv_rate ({inv_rate}) must be a power of two >= 2",)
73 }
74 }
75 }
76}
77
78#[derive(Clone, Copy, Debug)]
80pub struct SecurityMetrics {
81 pub relative_distance: f64,
84
85 pub num_queries: usize,
87
88 pub soundness_error: f64,
91
92 pub ldt_bits: usize,
95
96 pub security_bits: usize,
99}
100
101#[derive(Clone, Copy, Debug, Eq, PartialEq)]
103pub struct TableGeom {
104 pub support_size: usize,
106
107 pub encoded_width: usize,
109}
110
111#[derive(Clone, Debug)]
112pub struct Config {
113 pub inv_rate: usize,
116
117 pub num_queries: usize,
119
120 pub sumcheck_blinding_factor: usize,
123
124 pub ldt_support_size: usize,
128
129 pub min_security_bits: usize,
132}
133
134impl Default for Config {
135 fn default() -> Self {
136 Self::prod()
137 }
138}
139
140impl Config {
141 pub fn prod() -> Self {
145 Self {
146 inv_rate: 2,
147 num_queries: 176,
148 min_security_bits: MIN_PRODUCTION_BITS,
149 sumcheck_blinding_factor: 2,
150 ldt_support_size: 200,
151 }
152 }
153
154 pub fn dev() -> Self {
158 Self {
159 num_queries: 4,
160 min_security_bits: 0,
161 ..Self::prod()
162 }
163 }
164
165 pub fn encoded_width(&self, grid_cols: usize) -> usize {
167 self.table_geom(grid_cols).encoded_width
168 }
169
170 pub fn table_geom(&self, grid_cols: usize) -> TableGeom {
174 let frac = TableGeom {
175 support_size: self.ldt_support_size,
176 encoded_width: grid_cols * self.inv_rate,
177 };
178
179 let frac_msg = frac.support_size + grid_cols;
180
181 if frac.support_size <= grid_cols
182 && self.ldt_bits(frac_msg, frac.encoded_width) >= MIN_PRODUCTION_BITS
183 {
184 return frac;
185 }
186
187 TableGeom {
188 support_size: grid_cols,
189 encoded_width: grid_cols * self.inv_rate * 2,
190 }
191 }
192
193 pub fn estimated_security_bits(&self, field_bits: usize, grid_cols: usize) -> usize {
198 let g = self.table_geom(grid_cols);
199
200 self.ldt_bits(g.support_size + grid_cols, g.encoded_width)
201 .min(field_bits)
202 }
203
204 pub fn security_metrics(&self, field_bits: usize, grid_cols: usize) -> SecurityMetrics {
206 let g = self.table_geom(grid_cols);
207 let delta = self.estimate_relative_distance(grid_cols);
208 let bits = self.ldt_bits(g.support_size + grid_cols, g.encoded_width);
209
210 SecurityMetrics {
211 relative_distance: delta,
212 num_queries: self.num_queries,
213 soundness_error: (1.0 - delta).powf(self.num_queries as f64),
214 ldt_bits: bits,
215 security_bits: bits.min(field_bits),
216 }
217 }
218
219 pub fn check_security(&self, field_bits: usize, grid_cols: usize) -> errors::Result<()> {
222 if self.inv_rate < 2 || !self.inv_rate.is_power_of_two() {
223 return Err(Error::InvalidInvRate {
224 inv_rate: self.inv_rate,
225 }
226 .into());
227 }
228
229 let support = self.table_geom(grid_cols).support_size;
231 if self.min_security_bits > 0 && support < self.num_queries {
232 return Err(Error::InsufficientSupport {
233 ldt_support_size: support,
234 num_queries: self.num_queries,
235 }
236 .into());
237 }
238
239 let est_bits = self.estimated_security_bits(field_bits, grid_cols);
240 if est_bits < self.min_security_bits {
241 return Err(Error::SecurityTooLow {
242 estimated_bits: est_bits,
243 min_bits: self.min_security_bits,
244 }
245 .into());
246 }
247
248 Ok(())
249 }
250
251 fn estimate_relative_distance(&self, grid_cols: usize) -> f64 {
255 let g = self.table_geom(grid_cols);
256
257 g.encoded_width.saturating_sub(g.support_size + grid_cols) as f64 / g.encoded_width as f64
258 }
259
260 fn ldt_bits(&self, msg_len: usize, code_width: usize) -> usize {
264 if msg_len >= code_width {
265 return 0;
266 }
267
268 let log2_ratio = log2_ratio_fixed(code_width as u128, msg_len as u128);
269
270 ((self.num_queries as u128 * log2_ratio) >> LOG2_FRAC_BITS) as usize
271 }
272}
273
274fn log2_ratio_fixed(n: u128, m: u128) -> u128 {
277 const S: u32 = 60;
278
279 let mut scaled_m = m;
280 let mut int_part: u128 = 0;
281
282 while scaled_m <= n / 2 {
283 scaled_m <<= 1;
284 int_part += 1;
285 }
286
287 let mut y = (n << S) / scaled_m;
290 let mut frac: u128 = 0;
291
292 for i in 0..LOG2_FRAC_BITS {
293 y = (y * y) >> S;
294
295 if y >= 2u128 << S {
296 y >>= 1;
297 frac |= 1u128 << (LOG2_FRAC_BITS - 1 - i);
298 }
299 }
300
301 (int_part << LOG2_FRAC_BITS) | frac
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 const GRID_COLS: usize = 1024;
309
310 #[test]
311 fn default_is_prod() {
312 assert_eq!(Config::default().min_security_bits, MIN_PRODUCTION_BITS);
313 assert_eq!(Config::default().num_queries, Config::prod().num_queries);
314 }
315
316 #[test]
317 fn prod_meets_production_floor() {
318 let prod = Config::prod();
319
320 assert!(prod.estimated_security_bits(128, GRID_COLS) >= MIN_PRODUCTION_BITS);
321 assert!(prod.check_security(128, GRID_COLS).is_ok());
322 }
323
324 #[test]
325 fn dev_is_lenient_on_weak_params() {
326 let dev = Config::dev();
327
328 assert!(dev.estimated_security_bits(128, GRID_COLS) < MIN_PRODUCTION_BITS);
329 assert!(dev.check_security(128, GRID_COLS).is_ok());
330 }
331
332 #[test]
333 fn prod_threshold_rejects_weak_queries() {
334 let weak = Config {
335 num_queries: 4,
336 ..Config::prod()
337 };
338
339 assert!(weak.check_security(128, GRID_COLS).is_err());
340 }
341
342 #[test]
343 fn full_half_fallback_admits_ml_dsa_grid() {
344 assert!(Config::prod().check_security(128, 512).is_ok());
345 }
346
347 #[test]
348 fn grid_below_num_queries_rejected() {
349 assert!(Config::prod().check_security(128, 128).is_err());
350 }
351
352 #[test]
353 fn rejects_invalid_inv_rate() {
354 for bad in [0usize, 1, 3, 6] {
355 let cfg = Config {
356 inv_rate: bad,
357 ..Config::prod()
358 };
359
360 assert!(
361 cfg.check_security(128, GRID_COLS).is_err(),
362 "inv_rate {bad} must be rejected",
363 );
364 }
365
366 assert!(Config::prod().check_security(128, GRID_COLS).is_ok());
367 }
368
369 #[test]
370 fn ldt_bits_matches_float_within_one_bit() {
371 let cfg = Config::prod();
372
373 for log_g in 8usize..=20 {
374 let grid_cols = 1usize << log_g;
375 let g = cfg.table_geom(grid_cols);
376 let msg: usize = g.support_size + grid_cols;
377
378 if msg >= g.encoded_width {
379 continue;
380 }
381
382 let one_minus_delta = msg as f64 / g.encoded_width as f64;
383 let reference = (-one_minus_delta.powf(cfg.num_queries as f64).log2()).floor();
384 let integer = cfg.ldt_bits(msg, g.encoded_width) as f64;
385
386 assert!(
387 (integer - reference).abs() <= 1.0,
388 "grid 2^{log_g}: integer {integer} vs float {reference}",
389 );
390 }
391 }
392
393 #[test]
394 fn table_geom_selects_integer_stable_modes() {
395 let prod = Config::prod();
396
397 let big = prod.table_geom(1 << 12);
398 assert_eq!(big.support_size, prod.ldt_support_size);
399 assert_eq!(big.encoded_width, prod.inv_rate << 12);
400
401 let small = prod.table_geom(512);
402 assert_eq!(small.support_size, 512);
403 assert_eq!(small.encoded_width, prod.inv_rate * 512 * 2);
404 }
405}