Skip to main content

hekate_core/
config.rs

1// SPDX-FileCopyrightText: 2026 Andrei Kochergin <andrei@oumuamua.dev>
2// SPDX-FileCopyrightText: 2026 Oumuamua Labs <info@oumuamua.dev>
3// SPDX-License-Identifier: AGPL-3.0-only
4
5use crate::errors;
6use core::fmt;
7
8/// Production soundness floor.
9pub const MIN_PRODUCTION_BITS: usize = 100;
10
11/// Brakedown row-code rate `1/INV_RATE`;
12/// small grids fall back to `1/(2·INV_RATE)`.
13pub const INV_RATE: usize = 2;
14
15/// Outer row-code rate `1/2^OUTER_RATE_LOG2`.
16/// Below 2 the quadratic test has no soundness.
17pub const OUTER_RATE_LOG2: u32 = 4;
18
19/// Target row count of the outer AUX oracle.
20pub const OUTER_ROWS: usize = 32;
21
22/// Precision of `log2_ratio_fixed`:
23/// 32 holds the truncation error below
24/// `num_queries · 2⁻³²`, under one bit.
25pub(crate) const LOG2_FRAC_BITS: u32 = 32;
26
27/// Failures produced by `Config::check_security`.
28#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29pub enum Error {
30    /// Estimated security fell below `min_security_bits`.
31    SecurityTooLow {
32        estimated_bits: usize,
33        min_bits: usize,
34    },
35
36    /// `ldt_support_size < num_queries`;
37    /// opened columns exhaust the noise
38    /// budget and witness data leaks.
39    InsufficientSupport {
40        ldt_support_size: usize,
41        num_queries: usize,
42    },
43
44    /// No scalar to mask;
45    /// a pad of length zero hides nothing.
46    EmptyOuterStatement,
47
48    /// No column opening;
49    /// an unopened oracle proves nothing.
50    ZeroOuterQueries,
51
52    /// The outer code outgrew the largest
53    /// additive-FFT domain we encode.
54    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/// Security metrics snapshot for a given `Config`.
87#[derive(Clone, Copy, Debug)]
88pub struct SecurityMetrics {
89    /// Estimated relative distance δ of the linear code.
90    pub relative_distance: f64,
91
92    /// LDT spot-check count.
93    pub num_queries: usize,
94
95    /// LDT query bound: `-log₂(((1 + ρ)/2)^q)`.
96    pub ldt_bits: usize,
97
98    /// Proximity-gap term `(log2(rows) + units - 1) · n / |F|`.
99    pub proximity_bits: usize,
100
101    /// `min(ldt_bits, proximity_bits)`.
102    pub security_bits: usize,
103}
104
105/// Fold the proximity term is stated over: the committed
106/// grid, the eq-tensor's row count and the `eta` walk's
107/// unit count. Both sides derive it from `RingSwitchPlan`.
108#[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/// Per-table row-code geometry chosen by `Config::table_geom`.
116#[derive(Clone, Copy, Debug, Eq, PartialEq)]
117pub struct TableGeom {
118    /// Random low-coord support (LDT opening mask) length.
119    pub support_size: usize,
120
121    /// Committed codeword width (power of two).
122    pub encoded_width: usize,
123}
124
125#[derive(Clone, Debug)]
126pub struct Config {
127    /// Number of LDT spot-check queries.
128    pub num_queries: usize,
129
130    /// Random low-coord support that masks the LDT
131    /// column openings (data ZK). Lives inside the
132    /// message, it does not widen the codeword.
133    pub ldt_support_size: usize,
134
135    /// `check_security` rejects configs
136    /// whose estimated bits fall below this.
137    pub min_security_bits: usize,
138
139    /// Column openings of the outer argument.
140    pub outer_queries: usize,
141
142    /// Blind units, one-time pad and outer
143    /// argument on; `false` proves in the clear.
144    pub zero_knowledge: bool,
145}
146
147impl Default for Config {
148    fn default() -> Self {
149        Self::prod()
150    }
151}
152
153impl Config {
154    /// Production parameters: 100-bit soundness with the
155    /// `MIN_PRODUCTION_BITS` acceptance threshold. The `Default`.
156    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    /// Fast, low-soundness parameters for tests and experiments;
167    /// `min_security_bits = 0` accepts weak grids. Never deploy.
168    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    /// Blind columns committed with the whole eval master.
179    pub fn blind_units(&self) -> usize {
180        usize::from(self.zero_knowledge)
181    }
182
183    /// Committed row-code width for the chosen per-table mode.
184    pub fn encoded_width(&self, grid_cols: usize) -> usize {
185        self.table_geom(grid_cols).encoded_width
186    }
187
188    /// Mode must derive from transcript-bound inputs and
189    /// a fixed target only, never an unabsorbed field, or
190    /// prover and verifier silently diverge on the geometry.
191    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    /// `min(-log₂(((1 + ρ)/2)^q), proximity_gap_bits)`
212    /// where ρ = msg_len / code_width, q = num_queries.
213    ///
214    /// Brakedown (Golovnev et al. 2022), Appendix B Theorem 7.
215    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    /// `(log2(rows) + units - 1) · n / |F|`: BCIKS20's `n / |F|`,
223    /// Diamond-Gruen 2024/1351's eq-tensor factor over the rows,
224    /// BCIKS20 Thm 1.5's curve factor over the `eta` walk.
225    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    /// Per-table terms at `shape`; the LogUp `γ` term is
237    /// charged separately by [`Config::logup_gamma_bits`].
238    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    /// Rejects configs whose estimated soundness at
251    /// `grid_cols` falls below `min_security_bits`.
252    pub fn check_security(&self, field_bits: usize, shape: FoldShape) -> errors::Result<()> {
253        // dev (min_security_bits == 0) waives the ZK floor
254        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    /// Rejects a proof whose total bus height drops
276    /// the `γ` term below `min_security_bits`.
277    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    /// Exact MDS (Singleton) distance of the chosen geometry:
296    /// `δ = (encoded_width − support − grid_cols) / encoded_width`.
297    /// Holds for both modes (full-half yields exactly 0.5).
298    /// MDS: `hekate-math` `verus/fft.rs::fwd_semantics`.
299    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    /// `floor(-log₂(((1 + ρ)/2)^q))` in bits, ρ = msg_len / code_width.
306    /// Integer-only, prover and verifier derive identical geometry;
307    /// libm `powf`/`log2` are not bit-reproducible across platforms.
308    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    /// Schwartz-Zippel over the global LogUp `γ`, in bits:
320    /// the bus argument collapses if `γ` roots any one of the
321    /// `γ + key[i]` denominators, over every bus row in the proof.
322    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
329/// `floor(log₂(n / m) · 2^LOG2_FRAC_BITS)` for `n > m >= 1`.
330/// The Q60 mantissa keeps the squaring `y² < 2¹²²`, inside `u128`.
331pub(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    // m·2^int_part ∈ (n/2, n],
343    // y = n/(m·2^int_part) ∈ [1, 2) in Q_S.
344    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    /// A single-row, single-unit fold contributes no tensor
366    /// and no curve factor, isolating the terms under test.
367    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}