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