Skip to main content

hekate_core/
config.rs

1// SPDX-License-Identifier: Apache-2.0
2// This file is part of the hekate project.
3// Copyright (C) 2026 Andrei Kochergin <andrei@oumuamua.dev>
4// Copyright (C) 2026 Oumuamua Labs <info@oumuamua.dev>. All rights reserved.
5//
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10//     http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18use crate::errors;
19use crate::utils::compute_split_vars;
20use core::fmt;
21use tracing::warn;
22
23/// Production soundness floor:
24/// full GF(2^128) security. `security_bits` caps
25/// at the field size, 128 is the strongest attainable.
26pub const MIN_PRODUCTION_BITS: usize = 128;
27
28/// Failures produced by `Config::check_security`.
29#[derive(Clone, Copy, Debug, Eq, PartialEq)]
30pub enum Error {
31    /// Estimated security fell below `min_security_bits`.
32    SecurityTooLow {
33        estimated_bits: usize,
34        min_bits: usize,
35    },
36
37    /// `ldt_blinding_factor < num_queries`;
38    /// opened columns exhaust the noise
39    /// budget and witness data leaks.
40    InsufficientLdtBlinding {
41        ldt_blinding_factor: usize,
42        num_queries: usize,
43    },
44}
45
46impl fmt::Display for Error {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        match self {
49            Self::SecurityTooLow {
50                estimated_bits,
51                min_bits,
52            } => write!(
53                f,
54                "Security too low: estimated {estimated_bits} bits, but {min_bits} required",
55            ),
56            Self::InsufficientLdtBlinding {
57                ldt_blinding_factor,
58                num_queries,
59            } => write!(
60                f,
61                "ldt_blinding_factor ({ldt_blinding_factor}) must be >= num_queries ({num_queries})",
62            ),
63        }
64    }
65}
66
67/// Security metrics snapshot for a given `Config`.
68#[derive(Clone, Copy, Debug)]
69pub struct SecurityMetrics {
70    /// Estimated relative distance
71    /// δ of the linear code.
72    pub relative_distance: f64,
73
74    /// LDT spot-check count.
75    pub num_queries: usize,
76
77    /// Soundness error:
78    /// `(1 - δ)^q`.
79    pub soundness_error: f64,
80
81    /// LDT proximity bound:
82    /// `-log₂(soundness_error)`.
83    pub ldt_bits: usize,
84
85    /// `min(ldt_bits, field_bits)`. Schwartz-Zippel
86    /// caps Sumcheck / ZeroCheck / LogUp at field size.
87    pub security_bits: usize,
88
89    /// Non-zero entries per row of
90    /// the expander matrix.
91    pub expansion_degree: usize,
92}
93
94#[derive(Clone, Debug)]
95pub struct Config {
96    /// Non-zero entries per row in
97    /// the expander matrix.
98    pub expansion_degree: usize,
99
100    /// Number of LDT spot-check queries.
101    pub num_queries: usize,
102
103    /// Seed for the deterministic RNG
104    /// that samples the expander matrix.
105    pub matrix_seed: [u8; 32],
106
107    /// Blinding columns for algebraic ZK
108    /// (Sumcheck), extends the 1D trace.
109    pub sumcheck_blinding_factor: usize,
110
111    /// Blinding columns for data ZK
112    /// (LDT), extends the 2D grid width.
113    /// Must be `>= num_queries`.
114    pub ldt_blinding_factor: usize,
115
116    /// `check_security` rejects configs
117    /// whose estimated bits fall below this.
118    pub min_security_bits: usize,
119}
120
121impl Default for Config {
122    fn default() -> Self {
123        Self::prod()
124    }
125}
126
127impl Config {
128    /// Production parameters: ≈128-bit soundness
129    /// with the `MIN_PRODUCTION_BITS` acceptance
130    /// threshold. The `Default`.
131    pub fn prod() -> Self {
132        Self {
133            expansion_degree: 16,
134            num_queries: 160,
135            matrix_seed: [42u8; 32],
136            min_security_bits: MIN_PRODUCTION_BITS,
137            sumcheck_blinding_factor: 2,
138            ldt_blinding_factor: 200,
139        }
140    }
141
142    /// Fast, low-soundness parameters for tests
143    /// and experiments; `min_security_bits = 0`
144    /// accepts weak grids. Never deploy.
145    pub fn dev() -> Self {
146        Self {
147            num_queries: 4,
148            min_security_bits: 0,
149            ..Self::prod()
150        }
151    }
152
153    /// `min(-log₂((1 - δ)^q), field_bits)` where
154    /// δ = relative distance, q = num_queries.
155    ///
156    /// Brakedown (Golovnev et al. 2022), Section 3.2.
157    pub fn estimated_security_bits(&self, field_bits: usize) -> usize {
158        let delta = self.estimate_relative_distance();
159        let q = self.num_queries as f64;
160
161        let soundness_error = (1.0 - delta).powf(q);
162        let ldt_bits = (-soundness_error.log2()).floor() as usize;
163
164        ldt_bits.min(field_bits)
165    }
166
167    /// `field_bits`: `size_of::<F>() * 8`.
168    pub fn security_metrics(&self, field_bits: usize) -> SecurityMetrics {
169        let delta = self.estimate_relative_distance();
170        let q = self.num_queries as f64;
171        let soundness_error = (1.0 - delta).powf(q);
172        let ldt_bits = (-soundness_error.log2()).floor() as usize;
173
174        SecurityMetrics {
175            relative_distance: delta,
176            num_queries: self.num_queries,
177            soundness_error,
178            ldt_bits,
179            security_bits: ldt_bits.min(field_bits),
180            expansion_degree: self.expansion_degree,
181        }
182    }
183
184    /// Rejects configs that can't meet
185    /// `min_security_bits` for the
186    /// given trace dimensions.
187    pub fn check_security(
188        &self,
189        num_vars: usize,
190        field_bits: usize,
191        row_bytes: usize,
192    ) -> errors::Result<()> {
193        // ZK-privacy floor;
194        // dev (min_security_bits == 0) waives it.
195        if self.min_security_bits > 0 && self.ldt_blinding_factor < self.num_queries {
196            return Err(Error::InsufficientLdtBlinding {
197                ldt_blinding_factor: self.ldt_blinding_factor,
198                num_queries: self.num_queries,
199            }
200            .into());
201        }
202
203        let split_vars =
204            compute_split_vars(num_vars, self.num_queries, self.expansion_degree, row_bytes);
205        let grid_cols = 1usize << split_vars;
206
207        // Random-expander δ guarantees
208        // break down on very narrow grids.
209        if grid_cols > 0 && grid_cols < 128 && self.min_security_bits > 40 {
210            warn!("Grid width ({grid_cols}) too small for random expander guarantees");
211        }
212
213        // degree >> grid_cols degrades δ.
214        if grid_cols > 0 && self.expansion_degree > grid_cols / 4 {
215            warn!(
216                "Expansion degree ({}) too large for grid width ({}), need < {}",
217                self.expansion_degree,
218                grid_cols,
219                grid_cols / 4
220            );
221        }
222
223        let est_bits = self.estimated_security_bits(field_bits);
224        if est_bits < self.min_security_bits {
225            return Err(Error::SecurityTooLow {
226                estimated_bits: est_bits,
227                min_bits: self.min_security_bits,
228            }
229            .into());
230        }
231
232        Ok(())
233    }
234
235    /// Sipser-Spielman "Expander Codes"
236    /// (1996) bound `δ ≥ (d - 2√(d-1)) / d`,
237    /// scaled by an empirical correction
238    /// for finite random graphs.
239    fn estimate_relative_distance(&self) -> f64 {
240        let d = self.expansion_degree as f64;
241        if d < 2.0 {
242            return 0.01;
243        }
244
245        let sqrt_term = 2.0 * (d - 1.0).sqrt();
246        let theoretical_delta = (d - sqrt_term) / d;
247
248        // Random-graph correction:
249        // tighter as d grows.
250        let correction_factor = if d >= 64.0 {
251            0.95
252        } else if d >= 32.0 {
253            0.90
254        } else if d >= 16.0 {
255            // Standard Brakedown parameters
256            0.85
257        } else if d >= 8.0 {
258            0.75
259        } else {
260            0.60
261        };
262
263        (theoretical_delta * correction_factor).max(0.01)
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270
271    #[test]
272    fn default_is_prod() {
273        assert_eq!(Config::default().min_security_bits, MIN_PRODUCTION_BITS);
274        assert_eq!(Config::default().num_queries, Config::prod().num_queries);
275    }
276
277    #[test]
278    fn prod_meets_production_floor() {
279        let prod = Config::prod();
280
281        assert!(prod.estimated_security_bits(128) >= MIN_PRODUCTION_BITS);
282        assert!(prod.check_security(10, 128, 128).is_ok());
283    }
284
285    #[test]
286    fn dev_is_lenient_on_weak_params() {
287        let dev = Config::dev();
288
289        assert!(dev.estimated_security_bits(128) < MIN_PRODUCTION_BITS);
290        assert!(dev.check_security(10, 128, 128).is_ok());
291    }
292
293    #[test]
294    fn prod_threshold_rejects_weak_queries() {
295        let weak = Config {
296            num_queries: 4,
297            ..Config::prod()
298        };
299
300        assert!(weak.check_security(10, 128, 128).is_err());
301    }
302}