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/// Failures produced by `Config::check_security`.
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25pub enum Error {
26    /// Estimated security fell below `min_security_bits`.
27    SecurityTooLow {
28        estimated_bits: usize,
29        min_bits: usize,
30    },
31}
32
33impl fmt::Display for Error {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        match self {
36            Self::SecurityTooLow {
37                estimated_bits,
38                min_bits,
39            } => write!(
40                f,
41                "Security too low: estimated {estimated_bits} bits, but {min_bits} required",
42            ),
43        }
44    }
45}
46
47/// Security metrics snapshot for a given `Config`.
48#[derive(Clone, Copy, Debug)]
49pub struct SecurityMetrics {
50    /// Estimated relative distance
51    /// δ of the linear code.
52    pub relative_distance: f64,
53
54    /// LDT spot-check count.
55    pub num_queries: usize,
56
57    /// Soundness error:
58    /// `(1 - δ)^q`.
59    pub soundness_error: f64,
60
61    /// LDT proximity bound:
62    /// `-log₂(soundness_error)`.
63    pub ldt_bits: usize,
64
65    /// `min(ldt_bits, field_bits)`. Schwartz-Zippel
66    /// caps Sumcheck / ZeroCheck / LogUp at field size.
67    pub security_bits: usize,
68
69    /// Non-zero entries per row of
70    /// the expander matrix.
71    pub expansion_degree: usize,
72}
73
74#[derive(Clone, Debug)]
75pub struct Config {
76    /// Non-zero entries per row in
77    /// the expander matrix.
78    pub expansion_degree: usize,
79
80    /// Number of LDT spot-check queries.
81    pub num_queries: usize,
82
83    /// Seed for the deterministic RNG
84    /// that samples the expander matrix.
85    pub matrix_seed: [u8; 32],
86
87    /// Blinding columns for algebraic ZK
88    /// (Sumcheck), extends the 1D trace.
89    pub sumcheck_blinding_factor: usize,
90
91    /// Blinding columns for data ZK
92    /// (LDT), extends the 2D grid width.
93    /// Must be `>= num_queries`.
94    pub ldt_blinding_factor: usize,
95
96    /// `check_security` rejects configs
97    /// whose estimated bits fall below this.
98    pub min_security_bits: usize,
99}
100
101impl Default for Config {
102    fn default() -> Self {
103        Self {
104            expansion_degree: 16,
105            num_queries: 160,
106            matrix_seed: [42u8; 32],
107            min_security_bits: 99,
108            sumcheck_blinding_factor: 2,
109            ldt_blinding_factor: 200,
110        }
111    }
112}
113
114impl Config {
115    /// `min(-log₂((1 - δ)^q), field_bits)` where
116    /// δ = relative distance, q = num_queries.
117    ///
118    /// Brakedown (Golovnev et al. 2022), Section 3.2.
119    pub fn estimated_security_bits(&self, field_bits: usize) -> usize {
120        let delta = self.estimate_relative_distance();
121        let q = self.num_queries as f64;
122
123        let soundness_error = (1.0 - delta).powf(q);
124        let ldt_bits = (-soundness_error.log2()).floor() as usize;
125
126        ldt_bits.min(field_bits)
127    }
128
129    /// `field_bits`: `size_of::<F>() * 8`.
130    pub fn security_metrics(&self, field_bits: usize) -> SecurityMetrics {
131        let delta = self.estimate_relative_distance();
132        let q = self.num_queries as f64;
133        let soundness_error = (1.0 - delta).powf(q);
134        let ldt_bits = (-soundness_error.log2()).floor() as usize;
135
136        SecurityMetrics {
137            relative_distance: delta,
138            num_queries: self.num_queries,
139            soundness_error,
140            ldt_bits,
141            security_bits: ldt_bits.min(field_bits),
142            expansion_degree: self.expansion_degree,
143        }
144    }
145
146    /// Rejects configs that can't meet
147    /// `min_security_bits` for the given
148    /// trace dimensions.
149    pub fn check_security(&self, num_vars: usize, field_bits: usize) -> errors::Result<()> {
150        let split_vars = compute_split_vars(num_vars, self.num_queries);
151        let grid_cols = 1usize << split_vars;
152
153        // Random-expander δ guarantees
154        // break down on very narrow grids.
155        if grid_cols > 0 && grid_cols < 128 && self.min_security_bits > 40 {
156            warn!("Grid width ({grid_cols}) too small for random expander guarantees");
157        }
158
159        // degree >> grid_cols degrades δ.
160        if grid_cols > 0 && self.expansion_degree > grid_cols / 4 {
161            warn!(
162                "Expansion degree ({}) too large for grid width ({}), need < {}",
163                self.expansion_degree,
164                grid_cols,
165                grid_cols / 4
166            );
167        }
168
169        let est_bits = self.estimated_security_bits(field_bits);
170        if est_bits < self.min_security_bits {
171            return Err(Error::SecurityTooLow {
172                estimated_bits: est_bits,
173                min_bits: self.min_security_bits,
174            }
175            .into());
176        }
177
178        Ok(())
179    }
180
181    /// Sipser-Spielman "Expander Codes"
182    /// (1996) bound `δ ≥ (d - 2√(d-1)) / d`,
183    /// scaled by an empirical correction
184    /// for finite random graphs.
185    fn estimate_relative_distance(&self) -> f64 {
186        let d = self.expansion_degree as f64;
187        if d < 2.0 {
188            return 0.01;
189        }
190
191        let sqrt_term = 2.0 * (d - 1.0).sqrt();
192        let theoretical_delta = (d - sqrt_term) / d;
193
194        // Random-graph correction:
195        // tighter as d grows.
196        let correction_factor = if d >= 64.0 {
197            0.95
198        } else if d >= 32.0 {
199            0.90
200        } else if d >= 16.0 {
201            // Standard Brakedown parameters
202            0.85
203        } else if d >= 8.0 {
204            0.75
205        } else {
206            0.60
207        };
208
209        (theoretical_delta * correction_factor).max(0.01)
210    }
211}