diff --git a/Cargo.lock b/Cargo.lock index 42386baacb..657ab2acbf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3121,8 +3121,11 @@ version = "0.1.7" dependencies = [ "alloy-dyn-abi", "alloy-primitives", + "anyhow", + "clap", "fhe", "num-bigint", + "num-traits", "thiserror 1.0.69", ] diff --git a/crates/Dockerfile b/crates/Dockerfile index 581ac52a31..f09cb97f62 100644 --- a/crates/Dockerfile +++ b/crates/Dockerfile @@ -97,6 +97,9 @@ RUN for d in ./*/ ; do \ fi \ done +# Stub binary so first build (dependency cache) succeeds +RUN mkdir -p ./fhe-params/src/bin && echo 'fn main() {}' > ./fhe-params/src/bin/search_params.rs + RUN cargo build --locked --release COPY ./crates . diff --git a/crates/fhe-params/Cargo.toml b/crates/fhe-params/Cargo.toml index 935dce4aab..b5fe6bf513 100644 --- a/crates/fhe-params/Cargo.toml +++ b/crates/fhe-params/Cargo.toml @@ -9,10 +9,17 @@ repository = "https://github.com/gnosisguild/enclave/crates/fhe-params" [dependencies] fhe = { workspace = true } num-bigint = { workspace = true } +num-traits = { workspace = true } thiserror = { workspace = true } +clap = { workspace = true } +anyhow = { workspace = true } alloy-dyn-abi = { workspace = true, optional = true } alloy-primitives = { workspace = true, optional = true } +[[bin]] +name = "search_params" +path = "src/bin/search_params.rs" + [features] default = [] abi-encoding = ["dep:alloy-dyn-abi", "dep:alloy-primitives"] diff --git a/crates/fhe-params/README.md b/crates/fhe-params/README.md new file mode 100644 index 0000000000..a8532292ee --- /dev/null +++ b/crates/fhe-params/README.md @@ -0,0 +1,278 @@ +# FHE Parameters Library + +A Rust library for managing BFV (Brakerski-Fan-Vercauteren) homomorphic encryption parameters. This +library provides preset configurations, parameter builders, and a search module for finding optimal +parameters that satisfy security constraints. + +**Key Features:** + +- **Preset Configurations**: Pre-configured BFV parameters for common use cases (secure/insecure, + threshold/DKG) +- **Parameter Builders**: Functions to construct `BfvParameters` from presets or custom parameter + sets +- **Parameter Search**: Algorithm to find optimal BFV parameters using NTT-friendly primes with + exact arithmetic +- **CLI Tool**: Command-line interface for searching and validating BFV parameters interactively +- **ABI Encoding**: Optional Solidity ABI encoding/decoding for smart contract integration + +## Overview + +The `fhe-params` crate provides a complete solution for managing BFV parameters in the Enclave FHE +system. It supports two main workflows: + +1. **Using Presets**: Quick access to pre-validated parameter sets for production or testing +2. **Custom Search**: Finding optimal parameters for specific security and performance requirements + +## Modules + +### Presets (`presets`) + +Pre-configured BFV parameter sets for PVSS (Public Verifiable Secret Sharing) protocol: + +- **`BfvPreset::SecureThresholdBfv8192`** (default): Production-ready threshold BFV parameters + (degree 8192) +- **`BfvPreset::SecureDkg8192`**: Production-ready DKG parameters (degree 8192) +- **`BfvPreset::InsecureThresholdBfv512`**: Testing-only threshold BFV parameters (degree 512) +- **`BfvPreset::InsecureDkg512`**: Testing-only DKG parameters (degree 512) + +In the PVSS protocol, two types of BFV parameters are needed: + +- **Threshold BFV Parameters**: Used for threshold encryption/decryption operations (Phases 2-3-4) +- **DKG Parameters**: Used during Distributed Key Generation (Phases 0-1) for encrypting secret + shares + +### Builder (`builder`) + +Functions to construct `BfvParameters` instances: + +- `build_bfv_params()` / `build_bfv_params_arc()`: Build from a `BfvParamSet` +- `build_bfv_params_from_set()` / `build_bfv_params_from_set_arc()`: Build from preset metadata +- `build_pair_for_preset()`: Build both threshold and DKG parameter pairs for a preset + +### Search (`search`) + +A comprehensive module for searching optimal BFV parameters that satisfy security constraints. + +#### Overview + +The search module implements exact arithmetic using `BigUint` for precise security analysis. It +searches through NTT-friendly primes (40-63 bits) to find parameter sets that satisfy multiple +security equations. + +The library implements security analysis from: + +- https://eprint.iacr.org/2024/1285.pdf (BFV security) + +#### Security Constraints + +The search validates four key security equations: + +- **Equation 1**: `2*(B_C + n*B_sm) < Δ` (decryption correctness) +- **Equation 2**: `2*d*n*B ≤ B_Enc * 2^{-λ}` (encryption noise bound) +- **Equation 3**: `B_C ≤ B_sm * 2^{-λ}` (ciphertext noise bound) +- **Equation 4**: `d ≥ 37.5*log2(q/B) + 75` (degree constraint) + +#### Search Parameters + +The `BfvSearchConfig` struct defines the search constraints: + +- **`n`**: Number of parties (ciphernodes) +- **`z`**: Number of votes (also used as plaintext modulus k) +- **`k`**: Plaintext modulus (plaintext space) +- **`lambda`**: Statistical security parameter (negl(λ) = 2^{-λ}) +- **`b`**: Bound on error distribution ψ (e.g., 20 for CBD with σ≈3.2) +- **`b_chi`**: Bound on distribution χ used for secret key generation +- **`verbose`**: Enable detailed search process output + +The search iterates through polynomial degrees `d` (powers of 2: 1024, 2048, 4096, 8192, 16384, +32768). + +#### Search Algorithm + +The `bfv_search()` function implements a search algorithm that: + +1. Iterates through polynomial degrees `d` (powers of 2) +2. For each `d`, finds the maximum `q` under the Eq4 constraint +3. Validates the candidate against Eq1 (noise bound) +4. Refines the result by decreasing `q` to find minimal valid parameters + +Returns the first feasible parameter set found, or an error if none exist. + +**Note**: Some resulting parameter sets from this search are hardcoded as presets in the +`presets.rs` file for production use (e.g., `BfvPreset::SecureThresholdBfv8192`). + +#### Search Result + +The `BfvSearchResult` contains: + +- **`d`**: Chosen degree +- **`q_bfv`**: Ciphertext modulus (product of selected primes) +- **`selected_primes`**: NTT-friendly primes used +- **`qi_values()`**: Prime values as `Vec` for BFV parameter construction +- **Noise budgets**: `b_enc_min`, `b_fresh`, `b_c`, `b_sm_min` +- **Validation logs**: `lhs_log2`, `rhs_log2` for equation satisfaction details + +### Encoding (`encoding`) - Optional Feature + +When the `abi-encoding` feature is enabled, provides functions for encoding/decoding BFV parameters +using Solidity ABI format: + +- `encode_bfv_params()`: Encode parameters to ABI bytes +- `decode_bfv_params()` / `decode_bfv_params_arc()`: Decode ABI bytes to parameters + +This enables serialization for smart contracts and cross-platform parameter exchange. + +## Usage + +### Using Presets + +```rust +use e3_fhe_params::{BfvPreset, build_bfv_params_arc, builder::build_pair_for_preset}; +use std::sync::Arc; + +fn example() -> Result<(), e3_fhe_params::PresetError> { + // Build threshold BFV parameters + let params = build_bfv_params_arc(BfvPreset::SecureThresholdBfv8192)?; + + // Build both threshold and DKG parameter pairs + let (threshold_params, dkg_params) = build_pair_for_preset(BfvPreset::SecureThresholdBfv8192)?; + + Ok(()) +} +``` + +### Custom Parameter Sets + +```rust +use e3_fhe_params::{BfvParamSet, build_bfv_params_from_set_arc}; + +let param_set = BfvParamSet { + degree: 8192, + plaintext_modulus: 100, + moduli: &[0x0008000000820001, 0x0010000000060001], + error1_variance: Some("3"), +}; + +let params = build_bfv_params_from_set_arc(¶m_set)?; +``` + +### Parameter Search + +#### Using the Library + +```rust +use e3_fhe_params::search::bfv::{BfvSearchConfig, bfv_search}; + +let config = BfvSearchConfig { + n: 100, // Number of parties + z: 1000, // Number of votes + k: 1000, // Plaintext modulus + lambda: 80, // Security parameter + b: 20, // Error bound + b_chi: 1, // Secret key bound + verbose: true, // Show detailed output +}; + +match bfv_search(&config) { + Ok(result) => { + println!("Found parameters with degree: {}", result.d); + println!("Ciphertext modulus: {}", result.q_bfv); + println!("Primes: {:?}", result.qi_values()); + } + Err(e) => { + eprintln!("Search failed: {}", e); + } +} +``` + +#### Using the CLI Tool + +The crate includes a command-line tool `search_params` for searching BFV parameters interactively: + +```bash +# Build the binary +cargo build --bin search_params --package e3-fhe-params + +# Run with default parameters +cargo run --bin search_params --package e3-fhe-params + +# Run with custom parameters +cargo run --bin search_params --package e3-fhe-params -- \ + --n 100 \ + --z 100 \ + --k 100 \ + --lambda 80 \ + --b 20 \ + --b-chi 1 + +# Enable verbose output to see the search process +cargo run --bin search_params --package e3-fhe-params -- \ + --n 100 --z 100 --k 100 --lambda 80 --verbose +``` + +**CLI Options:** + +- `--n `: Number of parties (ciphernodes). Default: `1000` +- `--z `: Number of fresh ciphertext additions (number of votes). Also used as plaintext modulus + k. Default: `1000` +- `--k `: Plaintext modulus (plaintext space). Default: `1000` +- `--lambda `: Statistical security parameter λ (negl(λ) = 2^{-λ}). Default: `80` +- `--b `: Bound on error distribution ψ (e.g., 20 for CBD with σ≈3.2). Default: `20` +- `--b-chi `: Bound on distribution χ for secret key generation. Default: `1` +- `--verbose`: Enable verbose output showing detailed search process +- `--help`: Show help message +- `--version`: Show version information + +**Example: Reproducing Production Preset** + +The production preset `SecureThresholdBfv8192` can be reproduced using: + +```bash +cargo run --bin search_params --package e3-fhe-params -- \ + --n 100 \ + --z 100 \ + --k 100 \ + --lambda 80 \ + --b 20 \ + --b-chi 1 +``` + +This will output the same parameter set as the preset, including: + +- Degree: 8192 +- 4 NTT-friendly primes (52-53 bits each) +- All noise budgets and validation metrics +- A second parameter set (if found) + +**Output Format:** + +The CLI displays: + +- **First BFV Parameter Set**: The main threshold encryption parameters with all noise budgets +- **Second BFV Parameter Set**: Additional parameters for simpler conditions (if found) +- Distribution types (CBD/Uniform) and variance values for error bounds +- Complete parameter details including moduli, noise budgets, and validation metrics + +### ABI Encoding/Decoding + +```rust +#[cfg(feature = "abi-encoding")] +use e3_fhe_params::{BfvPreset, build_bfv_params_arc, encode_bfv_params, decode_bfv_params, decode_bfv_params_arc}; + +// Build parameters from a preset +let params = build_bfv_params_arc(BfvPreset::SecureThresholdBfv8192)?; + +// Encode parameters to ABI bytes for smart contract use +let encoded_bytes = encode_bfv_params(¶ms); + +// Decode back to parameters +let decoded_params = decode_bfv_params(&encoded_bytes)?; + +// Or decode directly to Arc for thread-safe shared ownership +let decoded_params_arc = decode_bfv_params_arc(&encoded_bytes)?; + +// Verify roundtrip +assert_eq!(decoded_params.degree(), params.degree()); +assert_eq!(decoded_params.plaintext(), params.plaintext()); +assert_eq!(decoded_params.moduli(), params.moduli()); +``` diff --git a/crates/fhe-params/src/bin/search_params.rs b/crates/fhe-params/src/bin/search_params.rs new file mode 100644 index 0000000000..ff6874bb67 --- /dev/null +++ b/crates/fhe-params/src/bin/search_params.rs @@ -0,0 +1,247 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +//! BFV Parameter Search CLI +//! +//! Standalone command-line tool for searching BFV parameters using NTT-friendly primes. + +use clap::Parser; +use e3_fhe_params::search::bfv::{ + bfv_search, bfv_search_second_param, BfvSearchConfig, BfvSearchResult, +}; +use e3_fhe_params::search::constants::K_MAX; +use e3_fhe_params::search::utils::{approx_bits_from_log2, fmt_big_summary, log2_big}; +use num_bigint::BigUint; + +#[derive(Parser, Debug, Clone)] +#[command( + version, + about = "Search BFV params with NTT-friendly CRT primes (40..63 bits)" +)] +struct Args { + /// Number of parties n (e.g. ciphernodes, default is 1000) + #[arg(long, default_value_t = 1000u128)] + n: u128, + + /// Number of fresh ciphertext z, i.e. number of votes. Note that the BFV plaintext modulus k will be defined as k = z + #[arg(long, default_value_t = 1000u128)] + z: u128, + + /// Plaintext modulus k (plaintext space). + #[arg(long, default_value_t = 1000u128)] + k: u128, + + /// Statistical Security parameter λ (negl(λ)=2^{-λ}). + #[arg(long, default_value_t = 80u32)] + lambda: u32, + + /// Bound B on the error distribution \psi (see pdf) used generate e1 when encrypting (e.g., 20 for CBD with σ≈3.2). + #[arg(long, default_value_t = 20u128)] + b: u128, + + /// Bound B_{\chi} on the distribution \chi (see pdf) used generate the secret key sk_i of each party i. + /// By default, it is fixed to be 20 (that is the case when \chi is CBD with with σ≈3.2, which + /// is the distribution by default in fhe.rs). + #[arg(long, default_value_t = 1u128)] + b_chi: u128, + + /// Verbose per-candidate logging + #[arg(long, default_value_t = false)] + verbose: bool, +} + +fn variance_cbd_str(b: u128) -> String { + if b % 2 == 0 { + (b / 2).to_string() + } else { + format!("{}/2", b) + } +} + +fn variance_uniform_str(b: u128) -> String { + let b_big = BigUint::from(b); + let var = (&b_big * (b + 1)) / 3u32; + var.to_str_radix(10) +} + +fn variance_uniform_big_str(b: &BigUint) -> String { + let b_plus_one = b + BigUint::from(1u32); + let var = (b * &b_plus_one) / 3u32; + var.to_str_radix(10) +} + +fn print_param_set( + title: &str, + config: &BfvSearchConfig, + result: &BfvSearchResult, + dist_b: &str, + var_b: &str, + dist_b_chi: &str, + var_chi: &str, + dist_benc: Option<(&str, &str)>, + show_common: bool, +) { + println!("\n=== {} ===", title); + if show_common { + println!("n (number of ciphernodes) = {}", config.n); + println!("z (number of votes) = {}", config.z); + } + println!( + "k (plaintext space) = {} ({} bits)", + result.k_plain_eff, + approx_bits_from_log2((result.k_plain_eff as f64).log2()) + ); + if show_common { + println!( + "λ (Statistical security parameter) = {}", + config.lambda + ); + println!( + "B (bound on e2) = {} [Dist: {}, Var = {}]", + config.b, dist_b, var_b + ); + println!( + "B_chi (bound on sk) = {} [Dist: {}, Var = {}]", + config.b_chi, dist_b_chi, var_chi + ); + } + println!("d (LWE dimension) = {}", result.d); + println!("q_BFV (decimal) = {}", result.q_bfv.to_str_radix(10)); + println!("|q_BFV| = {}", fmt_big_summary(&result.q_bfv)); + println!("Δ (decimal) = {}", result.delta.to_str_radix(10)); + println!("r_k(q) = {}", result.rkq); + if let Some((dist, var)) = dist_benc { + println!( + "BEnc (bound on e1) = {} [Dist: {}, Var = {}]", + result.benc_min.to_str_radix(10), + dist, + var + ); + } else { + println!( + "BEnc (bound on e1, taken as B) = {} [Dist: {}, Var = {}]", + config.b, dist_b, var_b + ); + } + println!("B_fresh = {}", result.b_fresh.to_str_radix(10)); + println!("B_C = {}", result.b_c.to_str_radix(10)); + if show_common { + println!("B_sm = {}", result.b_sm_min.to_str_radix(10)); + println!("log2(LHS) = {:.6}", result.lhs_log2); + } else { + println!("log2(2*B_C) = {:.6}", log2_big(&(&result.b_c << 1))); + } + println!("log2(Δ) = {:.6}", result.rhs_log2); + println!( + "q_i used ({}): {}", + result.selected_primes.len(), + result + .selected_primes + .iter() + .map(|p| format!("{} ({} bits)", p.hex, p.bitlen)) + .collect::>() + .join(", ") + ); +} + +fn main() { + let args = Args::parse(); + + if args.verbose { + println!( + "== BFV parameter search (NTT-friendly primes 40..60 bits; 61-, 62- and 63-bit primes are excluded) ==" + ); + println!( + "Inputs: n={} z={} k(user)={} λ={} B={} B_chi={}", + args.n, args.z, args.k, args.lambda, args.b, args.b_chi + ); + println!("Constraint: z ≤ k(effective) and z ≤ 2^25 (≈33.5M)\n"); + } + + // Enforce constraints on z and k + if args.z == 0 { + eprintln!("ERROR: z must be positive."); + std::process::exit(1); + } + if args.z > K_MAX { + eprintln!( + "ERROR: too many votes — z = {} exceeds 2^25 = {}.", + args.z, K_MAX + ); + std::process::exit(1); + } + if args.k == 0 { + eprintln!("ERROR: user-supplied plaintext space k must be positive."); + std::process::exit(1); + } + + let config = BfvSearchConfig { + n: args.n, + z: args.z, + k: args.k, + lambda: args.lambda, + b: args.b, + b_chi: args.b_chi, + verbose: args.verbose, + }; + + // Search across all powers of two; stop at the first feasible candidate + let Ok(bfv) = bfv_search(&config) else { + eprintln!( + "\nNo feasible BFV parameter set found across d∈{{256, 512, 1024,2048,4096,8192,16384,32768}}." + ); + eprintln!("Try increasing d, or reducing n, z, λ, or B."); + std::process::exit(1); + }; + + // Decide distributions: CBD for B ≤ 32, otherwise Uniform + let (dist_b, var_b) = if args.b <= 32 { + ("CBD", variance_cbd_str(args.b)) + } else { + ("Uniform", variance_uniform_str(args.b)) + }; + + let (dist_b_chi, var_chi) = ("CBD", variance_cbd_str(args.b_chi)); + let (dist_benc, var_benc) = ("Uniform", variance_uniform_big_str(&bfv.benc_min)); + + let bfv2_opt = bfv_search_second_param(&config, &bfv); + + println!("\n\n"); + println!("================================================================================"); + println!(" FINAL BFV PARAMETER SETS"); + println!("================================================================================"); + + print_param_set( + "FIRST BFV PARAMETER SET", + &config, + &bfv, + dist_b, + &var_b, + dist_b_chi, + &var_chi, + Some((dist_benc, &var_benc)), + true, + ); + + if let Some(bfv2) = &bfv2_opt { + print_param_set( + "SECOND BFV PARAMETER SET", + &config, + bfv2, + dist_b, + &var_b, + dist_b_chi, + &var_chi, + None, + false, + ); + } else { + println!("\n=== SECOND BFV PARAMETER SET ==="); + println!("No second BFV parameter set found."); + } + + println!("\n================================================================================"); +} diff --git a/crates/fhe-params/src/lib.rs b/crates/fhe-params/src/lib.rs index e1fb4973aa..3625b066e1 100644 --- a/crates/fhe-params/src/lib.rs +++ b/crates/fhe-params/src/lib.rs @@ -4,13 +4,14 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -//! Preset definitions and builders for zkFHE parameters. +//! Preset definitions and builders for BFV FHE parameters. pub mod builder; pub mod constants; #[cfg(feature = "abi-encoding")] pub mod encoding; pub mod presets; +pub mod search; pub use builder::{ build_bfv_params, build_bfv_params_arc, build_bfv_params_from_set, diff --git a/crates/fhe-params/src/presets.rs b/crates/fhe-params/src/presets.rs index b76d8a1587..e3f1d9e5e6 100644 --- a/crates/fhe-params/src/presets.rs +++ b/crates/fhe-params/src/presets.rs @@ -96,10 +96,10 @@ pub struct PresetMetadata { /// Default search parameters for BFV parameter generation /// /// These values are used when searching for optimal BFV parameters using -/// the crypto_params search algorithm. They define the constraints and +/// the search algorithm. They define the constraints and /// requirements for parameter selection. /// -/// See `crypto_params::bfv::BfvSearchConfig` for more details. +/// See `search::bfv::BfvSearchConfig` for more details. #[derive(Debug, Clone, Copy)] pub struct PresetSearchDefaults { /// Number of parties (n) - the number of ciphernodes in the system supported by diff --git a/crates/fhe-params/src/search/bfv.rs b/crates/fhe-params/src/search/bfv.rs new file mode 100644 index 0000000000..1b3aaeb872 --- /dev/null +++ b/crates/fhe-params/src/search/bfv.rs @@ -0,0 +1,890 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +//! BFV Parameter Search Library +//! +//! This library provides functionality to search for optimal BFV (Brakerski-Fan-Vercauteren) +//! parameters using NTT-friendly primes. It implements exact arithmetic for security analysis +//! and parameter validation. +use std::collections::BTreeMap; + +use crate::search::constants::{D_POW2_MAX, D_POW2_START, K_MAX}; +use crate::search::errors::{BfvParamsResult, SearchError, ValidationError}; +use crate::search::prime::PrimeItem; +use crate::search::prime::{ + build_prime_items, build_prime_items_for_second, select_max_q_under_cap, +}; +use crate::search::utils::{ + approx_bits_from_log2, big_shift_pow2, fmt_big_summary, log2_big, product, +}; +use num_bigint::BigUint; +use num_traits::ToPrimitive; +use num_traits::{One, Zero}; + +/// Configuration for BFV parameter search +#[derive(Debug, Clone)] +pub struct BfvSearchConfig { + /// Number of parties n (e.g. ciphernodes) + pub n: u128, + /// Number of fresh ciphertext additions z (number of votes) - equal to k_plain_eff. + pub z: u128, + /// Plaintext modulus k (plaintext space). + pub k: u128, + /// Statistical Security parameter λ (negl(λ)=2^{-λ}) + pub lambda: u32, + /// Bound B on the error distribution ψ used generate e1 when encrypting (e.g., 20 for CBD with σ≈3.2). + pub b: u128, + /// Bound B_{\chi} on the distribution \chi used generate the secret key sk_i of each party i. + pub b_chi: u128, + /// Verbose output showing detailed parameter search process + pub verbose: bool, +} + +/// Result of BFV parameter search +#[derive(Debug, Clone)] +pub struct BfvSearchResult { + /// Chosen degree and primes + pub d: u64, + pub k_plain_eff: u128, // = z + pub q_bfv: BigUint, + pub selected_primes: Vec, + pub rkq: u128, + pub delta: BigUint, + + /// Noise budgets + pub benc_min: BigUint, + pub b_fresh: BigUint, + pub b_c: BigUint, + pub b_sm_min: BigUint, + + /// Validation logs + pub lhs_log2: f64, + pub rhs_log2: f64, +} + +impl BfvSearchResult { + /// Extract prime values as u64 for BFV parameter construction + pub fn qi_values(&self) -> Vec { + self.selected_primes + .iter() + .map(|p| p.value.to_u64().expect("Prime value too large for u64")) + .collect() + } +} + +/// Search for optimal BFV parameters that satisfy all security constraints. +/// +/// This function implements a search algorithm that: +/// 1. Iterates through polynomial degrees d (powers of 2) +/// 2. For each d, finds the maximum q under the Eq4 constraint +/// 3. Validates the candidate against Eq1 (noise bound) +/// 4. Refines the result by decreasing q to find the minimal valid parameters +/// +/// Returns the first feasible parameter set found, or an error if none exist. +/// +/// Note: Some resulting parameter sets from this search are hardcoded as presets +/// in the `presets.rs` file for production use (e.g., `BfvPreset::SecureThresholdBfv8192`). +pub fn bfv_search(bfv_search_config: &BfvSearchConfig) -> BfvParamsResult { + let prime_items = build_prime_items(); + + // Quick checks on k := z + if bfv_search_config.z == 0 || bfv_search_config.z > K_MAX { + return Err(ValidationError::InvalidVotes { + z: bfv_search_config.z, + reason: "z must be positive and less than 2^25".to_string(), + } + .into()); + } + + let log2_b = (bfv_search_config.b as f64).log2(); + let mut d: u64 = D_POW2_START; + + while d <= D_POW2_MAX { + // Eq4: d ≥ 37.5*log2(q/B) + 75 => log2(q) ≤ log2(B) + (d-75)/37.5 + let log2_q_limit = log2_b + ((d as f64) - 75.0) / 37.5; + + if bfv_search_config.verbose { + println!("\n[BFV] d={d} checking for log2_q_limit = {log2_q_limit:.3}"); + } + + // Build the greedy maximum q under Eq4 cap and test. If it passes, print and start decreasing from this q. + let initial_sel = select_max_q_under_cap(log2_q_limit, &prime_items); + if initial_sel.is_empty() { + if bfv_search_config.verbose { + println!( + "[BFV] d={d} candidate: no CRT primes fit under Eq4 limit (log2 limit {log2_q_limit:.3})." + ); + } + d <<= 1; + continue; + } + + if let Some(initial_res) = finalize_bfv_candidate(bfv_search_config, d, initial_sel.clone()) + { + if bfv_search_config.verbose { + println!("\n--- First feasible before reduction (d={}) ---", d); + println!( + "BFV qi used ({}): {}", + initial_res.selected_primes.len(), + initial_res + .selected_primes + .iter() + .map(|p| p.hex.clone()) + .collect::>() + .join(", ") + ); + } + + if let Some(refined) = + refine_from_initial(bfv_search_config, d, &prime_items, initial_sel) + { + return Ok(refined); + } + + // If refinement fails unexpectedly, return the initial feasible result + return Ok(initial_res); + } + + if bfv_search_config.verbose { + println!( + "[BFV] d={} : first (largest-q) candidate failed Eq1 — increasing d…", + d + ); + } + + d <<= 1; + } + + Err(SearchError::NoFeasibleParameters.into()) +} + +/// Validate a candidate parameter set and compute all noise bounds. +/// +/// Computes noise budgets (B_Enc, B_fresh, B_C, B_sm) and checks if Eq1 is satisfied: +/// 2*(B_C + n*B_sm) < Δ +/// +/// Returns None if validation fails, otherwise returns the complete result. +pub fn finalize_bfv_candidate( + bfv_search_config: &BfvSearchConfig, + d: u64, + chosen: Vec, +) -> Option { + let q_bfv = product(chosen.iter().map(|pi| pi.value.clone())); + + // Compute plaintext space: max of user-defined k and z + let k_plain_eff: u128 = bfv_search_config.k.max(bfv_search_config.z); + + // r_k(q) = q mod k + let k_big = BigUint::from(k_plain_eff); + let rkq_big = &q_bfv % &k_big; + let rkq: u128 = rkq_big.to_u128().unwrap_or(0); + + // Δ = floor(q / k) + let delta = &q_bfv / &k_big; + + // Eq2: 2 d n B B_chi ≤ B_Enc * 2^{-λ} => B_Enc ≥ (2 d n B B_chi) * 2^{λ} + let two_pow_lambda = big_shift_pow2(bfv_search_config.lambda); + let benc_min = (BigUint::from(2u32) + * BigUint::from(d) + * BigUint::from(bfv_search_config.n) + * BigUint::from(bfv_search_config.b) + * BigUint::from(bfv_search_config.b_chi)) + * &two_pow_lambda; + + // B_fresh ≤ B_Enc + d B B_chi+ d B B_chi n + let term_d_b_chi = BigUint::from(d) + * BigUint::from(bfv_search_config.b) + * BigUint::from(bfv_search_config.b_chi); + let term_d_b_b_chi_n = BigUint::from(d) + * BigUint::from(bfv_search_config.b) + * BigUint::from(bfv_search_config.b_chi) + * BigUint::from(bfv_search_config.n); + let b_fresh = &benc_min + &term_d_b_chi + &term_d_b_b_chi_n; + + // B_C = z (B_fresh + r_k(q)) + let b_c = BigUint::from(bfv_search_config.z) * (&b_fresh + BigUint::from(rkq)); + + // Eq3: B_C ≤ B_sm * 2^{-λ} => B_sm ≥ B_C * 2^{λ} + let b_sm_min = &b_c * &two_pow_lambda; + + // Eq1: 2*(B_C + n*B_sm) < Δ + let lhs = (&b_c + BigUint::from(bfv_search_config.n) * &b_sm_min) << 1; + let lhs_log2 = log2_big(&lhs); + let rhs_log2 = log2_big(&delta); + + let benc_bits = approx_bits_from_log2(log2_big(&benc_min)); + let bfresh_bits = approx_bits_from_log2(log2_big(&b_fresh)); + let bc_bits = approx_bits_from_log2(log2_big(&b_c)); + let bsm_bits = approx_bits_from_log2(log2_big(&b_sm_min)); + + if bfv_search_config.verbose { + println!("\n[BFV] d={d} candidate:"); + println!( + " CRT primes ({}): {}", + chosen.len(), + chosen + .iter() + .map(|p| p.hex.clone()) + .collect::>() + .join(", ") + ); + println!(" |q_BFV| {}", fmt_big_summary(&q_bfv)); + println!( + " r_k(q)={} k={} Δ={}", + rkq, + bfv_search_config.z, + delta.to_str_radix(10) + ); + + println!(" negl(λ)=2^-{} (exact pow2)", bfv_search_config.lambda); + println!(" BEnc ≈ 2^{benc_bits} B_fresh ≈ 2^{bfresh_bits}"); + println!(" B_C ≈ 2^{bc_bits} B_sm ≈ 2^{bsm_bits}"); + println!(" eq1 logs: log2(LHS)≈{lhs_log2:.3} log2(Δ)≈{rhs_log2:.3}"); + + println!( + " eq1: 2*(B_C + n*B_sm) {} Δ => {}", + if lhs < delta { "<" } else { "≥" }, + if lhs < delta { "PASS ✅" } else { "fail ❌" } + ); + } + + if lhs >= delta { + return None; + } + + Some(BfvSearchResult { + d, + k_plain_eff, + q_bfv, + selected_primes: chosen, + rkq, + delta, + benc_min, + b_fresh, + b_c, + b_sm_min, + lhs_log2, + rhs_log2, + }) +} + +/// Refine parameters by decreasing q in 2-bit steps from an initial feasible set. +/// +/// Starting from a valid parameter set, this function decreases the bit size of q +/// by 2 bits per iteration, keeping the last passing configuration before the first failure. +/// This finds the minimal valid q for the given degree d. +pub fn refine_from_initial( + bfv_search_config: &BfvSearchConfig, + d: u64, + prime_items: &[PrimeItem], + initial_sel: Vec, +) -> Option { + // Determine initial bits and then decrease by 2 bits per step. + let initial_q = product(initial_sel.iter().map(|pi| pi.value.clone())); + let mut current_bits = approx_bits_from_log2(log2_big(&initial_q)); + + // Start with the initial feasible result + let mut last_passing = finalize_bfv_candidate(bfv_search_config, d, initial_sel.clone())?; + + // Walk down in steps of 2 bits, keeping the last passing set before the first failure + while current_bits > 40 { + let target_bits = current_bits.saturating_sub(2); + if let Some(res) = + construct_qi_for_target_bits(bfv_search_config, d, prime_items, target_bits) + { + // Update last_passing to this new passing result + last_passing = res; + current_bits = target_bits; + continue; + } else { + // Stop at the first failure; return the last passing result + break; + } + } + + Some(last_passing) +} + +/// Construct a CRT prime selection targeting a specific bit size for q. +/// +/// Uses a greedy packing strategy: divides target bits by number of primes needed, +/// then tries combinations of floor/ceil bit-length buckets to get closest to target. +/// Validates the selection and returns a result if it passes Eq1. +pub fn construct_qi_for_target_bits( + bfv_search_config: &BfvSearchConfig, + d: u64, + prime_items: &[PrimeItem], + target_bits: u64, +) -> Option { + // Build buckets sorted ascending (smallest first) to allow tight packing + let mut by_bits_small: BTreeMap> = BTreeMap::new(); + let mut by_bits_large: BTreeMap> = BTreeMap::new(); + for p in prime_items.iter() { + by_bits_small.entry(p.bitlen).or_default().push(p.clone()); + by_bits_large.entry(p.bitlen).or_default().push(p.clone()); + } + for v in by_bits_small.values_mut() { + v.sort_by(|a, b| a.value.cmp(&b.value)); + } + for v in by_bits_large.values_mut() { + v.sort_by(|a, b| b.value.cmp(&a.value)); + } + + let target_f = target_bits as f64; + + // Compute the actual maximum bit length available in the prime buckets + let max_bit = by_bits_small.keys().max().cloned().unwrap_or(61); + + // Fewest primes first: start from minimal s needed to reach target with max_bit primes + let s = target_bits.div_ceil(max_bit as u64).max(2) as usize; + + let r_float = target_f / (s as f64); + let floor_r = r_float.floor().clamp(40.0, max_bit as f64) as u8; + let ceil_r = r_float.ceil().clamp(40.0, max_bit as f64) as u8; + + // Build candidate selections mixing floor/ceil buckets; choose best by closeness once + let mut tried: Vec> = Vec::new(); + for k in 0..=s { + let take_ceil = k; + let take_floor = s - k; + let mut sel: Vec = Vec::new(); + if take_floor > 0 { + if let Some(b) = by_bits_small.get(&floor_r) { + if b.len() < take_floor { + continue; + } + sel.extend(b.iter().take(take_floor).cloned()); + } else { + continue; + } + } + if take_ceil > 0 { + if let Some(b) = by_bits_small.get(&ceil_r) { + if b.len() < take_ceil { + continue; + } + sel.extend(b.iter().take(take_ceil).cloned()); + } else { + continue; + } + } + if sel.len() == s { + tried.push(sel); + } + } + // Also consider pure buckets + if let Some(b) = by_bits_large.get(&floor_r) { + if b.len() >= s { + tried.push(b.iter().take(s).cloned().collect()); + } + } + if let Some(b) = by_bits_large.get(&ceil_r) { + if b.len() >= s { + tried.push(b.iter().take(s).cloned().collect()); + } + } + + // Pick selection closest to target bits and test exactly once + let mut best: Option<(f64, Vec)> = None; + for sel in tried { + let q = product(sel.iter().map(|pi| pi.value.clone())); + let qbits = log2_big(&q); + let diff = (qbits - target_f).abs(); + if let Some((best_diff, _)) = &best { + if diff < *best_diff { + best = Some((diff, sel)); + } + } else { + best = Some((diff, sel)); + } + } + if let Some((_, sel)) = best { + // During decreasing, use plaintext from qi (not max with user k) + return finalize_bfv_candidate(bfv_search_config, d, sel.clone()); + } + + None +} + +/// Search for a second BFV parameter set with plaintext space derived from the first set. +/// +/// The plaintext modulus k is set to the next power of 2 above the maximum qi bit length +/// from the first parameter set. Uses a separate prime pool that includes 62-bit primes. +pub fn bfv_search_second_param( + bfv_search_config: &BfvSearchConfig, + first: &BfvSearchResult, +) -> Option { + // Plaintext space for second set: next power of 2 above max qi of first set. + let max_qi_bits_first: u64 = first + .selected_primes + .iter() + .map(|pi| pi.value.bits()) + .max() + .unwrap_or(61); + let k_second: u128 = if max_qi_bits_first >= 127 { + u128::MAX + } else { + 1u128 << ((max_qi_bits_first + 1) as u32) + }; + + if bfv_search_config.verbose { + println!( + "Second set: k(plaintext) = {} ({} bits), derived from first max qi = {} bits", + k_second, + max_qi_bits_first + 1, + max_qi_bits_first + ); + } + + let log2_b = (bfv_search_config.b as f64).log2(); + // Start from the dimension of the first set + let mut d: u64 = first.d; + + while d <= D_POW2_MAX { + // Eq4: d ≥ 37.5*log2(q/B) + 75 => log2(q) ≤ log2(B) + (d-75)/37.5 + let log2_q_limit = log2_b + ((d as f64) - 75.0) / 37.5; + + if bfv_search_config.verbose { + println!("\n[BFV-2nd] d={d} checking for log2_q_limit = {log2_q_limit:.3})."); + } + + // Try decreasing q at this fixed d, collect all passing candidates + // For second set, use a separate prime pool that includes 62-bit primes + let prime_items_second = build_prime_items_for_second(); + if let Some(res) = refine_second_param_at_d( + bfv_search_config, + d, + &prime_items_second, + log2_q_limit, + k_second, + ) { + return Some(res); + } + d <<= 1; + } + None +} + +/// Refine second parameter set at a fixed degree d by decreasing q. +/// +/// Collects all passing candidates as q decreases, then selects the one with +/// the fewest primes (minimizing CRT overhead). +pub fn refine_second_param_at_d( + bfv_search_config: &BfvSearchConfig, + d: u64, + prime_items: &[PrimeItem], + log2_q_limit: f64, + k_plain: u128, +) -> Option { + // Start from largest q under cap at this d and decrease by 2 bits, collecting all passing + let initial_sel = select_max_q_under_cap(log2_q_limit, prime_items); + if initial_sel.is_empty() { + return None; + } + + let initial_q = product(initial_sel.iter().map(|pi| pi.value.clone())); + let mut current_bits = approx_bits_from_log2(log2_big(&initial_q)); + let mut all_passing: Vec = Vec::new(); + + // Try the initial selection + if let Some(res) = finalize_second_param(bfv_search_config, d, initial_sel.clone(), k_plain) { + all_passing.push(res); + } + + // Decrease by 2 bits at a time, continue even if some fail (don't stop at first failure) + while current_bits > 40 { + let target_bits = current_bits.saturating_sub(2); + if let Some(res) = + construct_qi_second_param(bfv_search_config, d, prime_items, target_bits, k_plain) + { + all_passing.push(res); + } + // Continue decreasing regardless of whether this target passed or failed + current_bits = target_bits; + } + + // Pick the one with fewest qi's among all passing at this d + if all_passing.is_empty() { + return None; + } + all_passing.sort_by(|a, b| { + a.selected_primes.len().cmp(&b.selected_primes.len()).then( + log2_big(&a.q_bfv) + .partial_cmp(&log2_big(&b.q_bfv)) + .unwrap_or(std::cmp::Ordering::Equal), + ) + }); + Some(all_passing.into_iter().next().unwrap()) +} + +/// Construct CRT prime selection for second parameter set targeting specific bit size. +/// +/// Similar to `construct_qi_for_target_bits` but uses 62-bit primes and validates +/// that all qi are more than one bit larger than k_plain. +pub fn construct_qi_second_param( + bfv_search_config: &BfvSearchConfig, + d: u64, + prime_items: &[PrimeItem], + target_bits: u64, + k_plain: u128, +) -> Option { + let mut by_bits_small: BTreeMap> = BTreeMap::new(); + let mut by_bits_large: BTreeMap> = BTreeMap::new(); + for p in prime_items.iter() { + by_bits_small.entry(p.bitlen).or_default().push(p.clone()); + by_bits_large.entry(p.bitlen).or_default().push(p.clone()); + } + for v in by_bits_small.values_mut() { + v.sort_by(|a, b| a.value.cmp(&b.value)); + } + for v in by_bits_large.values_mut() { + v.sort_by(|a, b| b.value.cmp(&a.value)); + } + + let target_f = target_bits as f64; + let s = target_bits.div_ceil(62).max(2) as usize; + let r_float = target_f / (s as f64); + let floor_r = r_float.floor().clamp(40.0, 62.0) as u8; + let ceil_r = r_float.ceil().clamp(40.0, 62.0) as u8; + + let mut tried: Vec> = Vec::new(); + for k in 0..=s { + let take_ceil = k; + let take_floor = s - k; + let mut sel: Vec = Vec::new(); + if take_floor > 0 { + if let Some(b) = by_bits_small.get(&floor_r) { + if b.len() < take_floor { + continue; + } + sel.extend(b.iter().take(take_floor).cloned()); + } else { + continue; + } + } + if take_ceil > 0 { + if let Some(b) = by_bits_small.get(&ceil_r) { + if b.len() < take_ceil { + continue; + } + sel.extend(b.iter().take(take_ceil).cloned()); + } else { + continue; + } + } + if sel.len() == s { + tried.push(sel); + } + } + if let Some(b) = by_bits_large.get(&floor_r) { + if b.len() >= s { + tried.push(b.iter().take(s).cloned().collect()); + } + } + if let Some(b) = by_bits_large.get(&ceil_r) { + if b.len() >= s { + tried.push(b.iter().take(s).cloned().collect()); + } + } + + let mut best: Option<(f64, Vec)> = None; + for sel in tried { + let q = product(sel.iter().map(|pi| pi.value.clone())); + let qbits = log2_big(&q); + let diff = (qbits - target_f).abs(); + if let Some((best_diff, _)) = &best { + if diff < *best_diff { + best = Some((diff, sel)); + } + } else { + best = Some((diff, sel)); + } + } + if let Some((_, sel)) = best { + return finalize_second_param(bfv_search_config, d, sel.clone(), k_plain); + } + None +} + +/// Validate second parameter set with simplified noise bounds. +/// +/// For the second set, uses B_Enc = B (simpler bound) and checks 2*B_C < Δ. +/// Also validates that all qi are more than one bit larger than k_plain. +pub fn finalize_second_param( + bfv_search_config: &BfvSearchConfig, + d: u64, + chosen: Vec, + k_plain: u128, +) -> Option { + // Check that all qi are more than one bit larger than k_plain + // If k_plain = 2^b, then qi must be > 2^{b+1} + let k_big = BigUint::from(k_plain); + let k_log2 = if k_plain == 0 { + 0.0 + } else { + (k_plain as f64).log2() + }; + let k_bits = if k_plain == 0 { + 0 + } else { + k_log2.floor() as u64 + }; + let min_qi_threshold = if k_bits >= 127 { + BigUint::from(u128::MAX) + } else { + BigUint::one() << ((k_bits + 1) as u32) + }; + + for pi in &chosen { + if pi.value <= min_qi_threshold { + if bfv_search_config.verbose { + println!( + "[BFV-2nd] d={d} candidate rejected: qi {} is not more than one bit larger than k={k_plain} (need > 2^{}).", + pi.value, + k_bits + 1 + ); + } + return None; + } + } + + let q_bfv = product(chosen.iter().map(|pi| pi.value.clone())); + let rkq_big = &q_bfv % &k_big; + let rkq: u128 = rkq_big.to_u128().unwrap_or(0); + let delta = &q_bfv / &k_big; + + // For second set: B_Enc = B (simpler), B_fresh = B_Enc + d*B*B_chi + d*B*B_chi + let benc = BigUint::from(bfv_search_config.b); + let term_d_bbchi = BigUint::from(d) + * BigUint::from(bfv_search_config.b) + * BigUint::from(bfv_search_config.b_chi); + let b_fresh = &benc + &term_d_bbchi + &term_d_bbchi; + let b_c = b_fresh.clone(); // B_C = B_fresh + + let lhs = &b_c << 1; // 2*B_C + let lhs_log2 = log2_big(&lhs); + let rhs_log2 = log2_big(&delta); + + let ok = lhs < delta; + if !ok { + return None; + } + + if bfv_search_config.verbose { + println!("\n[BFV-2nd] d={d} candidate:"); + println!( + " CRT primes ({}): {}", + chosen.len(), + chosen + .iter() + .map(|p| p.hex.clone()) + .collect::>() + .join(", ") + ); + println!(" |q_BFV| {}", fmt_big_summary(&q_bfv)); + println!( + " k(plaintext_space)={} Δ={}", + k_plain, + delta.to_str_radix(10) + ); + println!( + " BEnc(taken as B) = {} B_fresh = {}", + bfv_search_config.b, + b_fresh.to_str_radix(10) + ); + println!(" B_C = B_fresh = {}", b_c.to_str_radix(10)); + println!(" log2(2*B_C)≈{:.3} log2(Δ)≈{:.3}", lhs_log2, rhs_log2); + + println!( + " 2*B_C {} Δ => {}", + if ok { "<" } else { "≥" }, + if ok { "PASS ✅" } else { "fail ❌" } + ); + + println!("\n*** BFV-2nd FEASIBLE at d={} ***", d); + } + + Some(BfvSearchResult { + d, + k_plain_eff: k_plain, + q_bfv, + selected_primes: chosen, + rkq, + delta, + benc_min: benc, + b_fresh, + b_c, + b_sm_min: BigUint::zero(), // not used in second set + lhs_log2, + rhs_log2, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::search::prime::build_prime_items; + use crate::search::prime::build_prime_items_for_second; + use num_bigint::BigUint; + + fn create_test_config() -> BfvSearchConfig { + BfvSearchConfig { + n: 10, + z: 1000, + k: 1000, + lambda: 80, + b: 20, + b_chi: 1, + verbose: false, + } + } + + #[test] + fn test_bfv_search_result_qi_values() { + let primes = build_prime_items(); + assert!(!primes.is_empty()); + + let test_primes = primes.iter().take(3).cloned().collect::>(); + let result = BfvSearchResult { + d: 512, + k_plain_eff: 1000, + q_bfv: product(test_primes.iter().map(|p| p.value.clone())), + selected_primes: test_primes.clone(), + rkq: 0, + delta: BigUint::one(), + benc_min: BigUint::one(), + b_fresh: BigUint::one(), + b_c: BigUint::one(), + b_sm_min: BigUint::one(), + lhs_log2: 0.0, + rhs_log2: 0.0, + }; + + let qi_vals = result.qi_values(); + assert_eq!(qi_vals.len(), test_primes.len()); + for (i, val) in qi_vals.iter().enumerate() { + assert_eq!(*val, test_primes[i].value.to_u64().unwrap()); + } + } + + #[test] + fn test_bfv_search_invalid_z_zero() { + let mut config = create_test_config(); + config.z = 0; + + let result = bfv_search(&config); + assert!(result.is_err()); + } + + #[test] + fn test_bfv_search_invalid_z_too_large() { + let mut config = create_test_config(); + config.z = K_MAX + 1; + + let result = bfv_search(&config); + assert!(result.is_err()); + } + + #[test] + fn test_finalize_bfv_candidate_with_valid_primes() { + let config = create_test_config(); + let primes = build_prime_items(); + assert!(!primes.is_empty()); + + let test_primes = primes.iter().take(2).cloned().collect::>(); + let d = 512; + + let result = finalize_bfv_candidate(&config, d, test_primes.clone()); + + if let Some(res) = result { + assert_eq!(res.d, d); + assert_eq!(res.selected_primes.len(), test_primes.len()); + assert_eq!(res.k_plain_eff, config.z.max(config.k)); + } + } + + #[test] + fn test_finalize_bfv_candidate_empty_primes() { + let config = create_test_config(); + let empty_primes = vec![]; + let d = 512; + + let result = finalize_bfv_candidate(&config, d, empty_primes); + assert!(result.is_none()); + } + + #[test] + fn test_finalize_second_param_qi_validation() { + let config = create_test_config(); + let primes = build_prime_items_for_second(); + assert!(!primes.is_empty()); + + // Test invalid case: primes too small for k_plain + let small_primes = primes + .iter() + .filter(|p| p.bitlen <= 40) + .take(2) + .cloned() + .collect::>(); + + if !small_primes.is_empty() { + let k_plain = 1u128 << 50; // 2^50, requires primes > 2^51 + let d = 512; + let result = finalize_second_param(&config, d, small_primes, k_plain); + // Primes with bitlen <= 40 are < 2^40 < 2^51, so should be rejected + assert!(result.is_none()); + } + + // Test valid case: primes large enough for k_plain + let large_primes = primes + .iter() + .filter(|p| p.bitlen > 50) // Large primes that can satisfy various k_plain values + .take(2) + .cloned() + .collect::>(); + + if !large_primes.is_empty() { + let k_plain = 1u128 << 30; // 2^30, requires primes > 2^31 + let d = 512; + let result = finalize_second_param(&config, d, large_primes.clone(), k_plain); + assert!(result.is_some()); + let res = result.unwrap(); + + // Validate returned properties + assert_eq!(res.d, d); + assert_eq!(res.k_plain_eff, k_plain); + assert_eq!(res.selected_primes.len(), large_primes.len()); + // Compare primes by their values since PrimeItem doesn't implement PartialEq + for (returned, expected) in res.selected_primes.iter().zip(large_primes.iter()) { + assert_eq!(returned.value, expected.value); + } + + // Validate q_bfv is product of selected primes + let expected_q = product(res.selected_primes.iter().map(|p| p.value.clone())); + assert_eq!(res.q_bfv, expected_q); + + // Validate delta = q_bfv / k_plain + let expected_delta = &res.q_bfv / &BigUint::from(k_plain); + assert_eq!(res.delta, expected_delta); + } + } + + #[test] + fn test_construct_qi_for_target_bits() { + let config = create_test_config(); + let primes = build_prime_items(); + assert!(!primes.is_empty()); + + let d = 512; + let target_bits = 100; + + let result = construct_qi_for_target_bits(&config, d, &primes, target_bits); + if let Some(res) = result { + assert_eq!(res.d, d); + assert!(!res.selected_primes.is_empty()); + } + } +} diff --git a/crates/fhe-params/src/search/constants.rs b/crates/fhe-params/src/search/constants.rs new file mode 100644 index 0000000000..565a11cdc7 --- /dev/null +++ b/crates/fhe-params/src/search/constants.rs @@ -0,0 +1,280 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +/// NTT-friendly primes by bit-length (40..63), 6 per size +pub const NTT_PRIMES_BY_BITS: &[(u8, [&str; 6])] = &[ + ( + 40u8, + [ + "0x00000080004a0001", + "0x0000008000fa0001", + "0x0000008001ae0001", + "0x0000008001b20001", + "0x0000008001ee0001", + "0x0000008001f60001", + ], + ), + ( + 41u8, + [ + "0x00000100003e0001", + "0x0000010000960001", + "0x0000010000b60001", + "0x0000010000ce0001", + "0x0000010000de0001", + "0x00000100010a0001", + ], + ), + ( + 42u8, + [ + "0x0000020000560001", + "0x0000020000820001", + "0x0000020000920001", + "0x0000020000aa0001", + "0x0000020001360001", + "0x00000200015a0001", + ], + ), + ( + 43u8, + [ + "0x0000040000560001", + "0x00000400007a0001", + "0x00000400008a0001", + "0x0000040000fe0001", + "0x0000040001760001", + "0x00000400017a0001", + ], + ), + ( + 44u8, + [ + "0x00000800009a0001", + "0x0000080000ee0001", + "0x0000080001060001", + "0x0000080001160001", + "0x00000800012e0001", + "0x0000080001420001", + ], + ), + ( + 45u8, + [ + "0x0000100000020001", + "0x00001000001a0001", + "0x00001000003e0001", + "0x00001000006e0001", + "0x0000100000ba0001", + "0x0000100000ce0001", + ], + ), + ( + 46u8, + [ + "0x00002000000a0001", + "0x00002000000e0001", + "0x0000200000620001", + "0x00002000006a0001", + "0x0000200000860001", + "0x0000200000a60001", + ], + ), + ( + 47u8, + [ + "0x0000400000060001", + "0x0000400000420001", + "0x0000400000660001", + "0x0000400000920001", + "0x00004000009e0001", + "0x0000400000b60001", + ], + ), + ( + 48u8, + [ + "0x0000800000020001", + "0x0000800000520001", + "0x0000800000aa0001", + "0x0000800001360001", + "0x0000800001420001", + "0x0000800002060001", + ], + ), + ( + 49u8, + [ + "0x00010000001a0001", + "0x00010000001e0001", + "0x0001000000320001", + "0x0001000000720001", + "0x0001000000ba0001", + "0x00010000011a0001", + ], + ), + ( + 50u8, + [ + "0x00020000001a0001", + "0x00020000005e0001", + "0x0002000000860001", + "0x0002000000ce0001", + "0x00020000013a0001", + "0x00020000015a0001", + ], + ), + ( + 51u8, + [ + "0x0004000000120001", + "0x0004000000420001", + "0x0004000000660001", + "0x00040000007e0001", + "0x00040000008a0001", + "0x0004000000de0001", + ], + ), + ( + 52u8, + [ + "0x0008000000820001", + "0x0008000001120001", + "0x00080000012a0001", + "0x0008000001360001", + "0x00080000016a0001", + "0x00080000018a0001", + ], + ), + ( + 53u8, + [ + "0x0010000000060001", + "0x00100000003e0001", + "0x00100000006e0001", + "0x00100000007e0001", + "0x0010000000960001", + "0x00100000010e0001", + ], + ), + ( + 54u8, + [ + "0x00200000000e0001", + "0x0020000000820001", + "0x0020000001360001", + "0x0020000001460001", + "0x0020000001520001", + "0x00200000015e0001", + ], + ), + ( + 55u8, + [ + "0x0040000000120001", + "0x0040000000f60001", + "0x00400000010a0001", + "0x00400000011a0001", + "0x00400000017a0001", + "0x0040000001ca0001", + ], + ), + ( + 56u8, + [ + "0x00800000005e0001", + "0x0080000000ca0001", + "0x0080000001f60001", + "0x0080000002120001", + "0x00800000021a0001", + "0x00800000022a0001", + ], + ), + ( + 57u8, + [ + "0x0100000000060001", + "0x01000000002a0001", + "0x0100000001260001", + "0x01000000016a0001", + "0x0100000001760001", + "0x0100000002a20001", + ], + ), + ( + 58u8, + [ + "0x02000000003a0001", + "0x0200000001460001", + "0x02000000015a0001", + "0x02000000015e0001", + "0x0200000001b20001", + "0x0200000001ee0001", + ], + ), + ( + 59u8, + [ + "0x0400000000360001", + "0x0400000000660001", + "0x04000000008a0001", + "0x0400000000920001", + "0x0400000000ea0001", + "0x0400000001460001", + ], + ), + ( + 60u8, + [ + "0x08000000004a0001", + "0x0800000000ee0001", + "0x0800000001160001", + "0x08000000018e0001", + "0x08000000025a0001", + "0x08000000029e0001", + ], + ), + ( + 61u8, + [ + "0x10000000006e0001", + "0x1000000000860001", + "0x1000000000ce0001", + "0x10000000011a0001", + "0x10000000019a0001", + "0x1000000001be0001", + ], + ), + ( + 62u8, + [ + "0x2000000000460001", + "0x2000000000620001", + "0x2000000000da0001", + "0x2000000001120001", + "0x2000000001960001", + "0x2000000001be0001", + ], + ), + ( + 63u8, + [ + "0x40000000009e0001", + "0x40000000010a0001", + "0x40000000016a0001", + "0x4000000001ca0001", + "0x40000000020a0001", + "0x4000000002820001", + ], + ), +]; + +/// Starting polynomial degree (power of 2) for parameter search +pub const D_POW2_START: u64 = 256; +/// Maximum polynomial degree (power of 2) to search up to +pub const D_POW2_MAX: u64 = 32768; +/// Maximum number of votes/plaintext additions (k_plain_eff) +pub const K_MAX: u128 = 1u128 << 25; // 33,554,432 diff --git a/crates/fhe-params/src/search/errors.rs b/crates/fhe-params/src/search/errors.rs new file mode 100644 index 0000000000..deda4752ce --- /dev/null +++ b/crates/fhe-params/src/search/errors.rs @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +//! Error types for BFV parameter search +//! +//! This module defines specific error types using `thiserror` for better error handling, +//! debugging, and user experience in BFV parameter search operations. + +use thiserror::Error; + +/// Main error type for BFV parameter search +/// +/// This enum covers all the different types of errors that can occur +/// during BFV parameter search and validation. +#[derive(Error, Debug)] +pub enum BfvParamsError { + /// Validation errors for BFV parameters + #[error("Validation error: {message}")] + Validation { message: String }, + + /// Parameter search errors + #[error("Parameter search error: {message}")] + Search { message: String }, + + /// Generic error with context + #[error("Error: {message}")] + Generic { message: String }, +} + +/// Result type alias for BFV parameter operations +pub type BfvParamsResult = Result; + +/// Validation error type for specific parameter validation failures +#[derive(Error, Debug)] +pub enum ValidationError { + /// Invalid number of votes/plaintext additions + #[error("Invalid number of votes: {z} - {reason}")] + InvalidVotes { z: u128, reason: String }, +} + +/// Search error type for parameter search failures +#[derive(Error, Debug)] +pub enum SearchError { + /// No feasible parameters found + #[error("No feasible BFV parameters found for the given constraints")] + NoFeasibleParameters, +} + +// Conversion implementations for better error handling +impl From for BfvParamsError { + fn from(err: ValidationError) -> Self { + BfvParamsError::Validation { + message: err.to_string(), + } + } +} + +impl From for BfvParamsError { + fn from(err: SearchError) -> Self { + BfvParamsError::Search { + message: err.to_string(), + } + } +} + +impl From for BfvParamsError { + fn from(message: String) -> Self { + BfvParamsError::Generic { message } + } +} + +impl From<&str> for BfvParamsError { + fn from(message: &str) -> Self { + BfvParamsError::Generic { + message: message.to_string(), + } + } +} diff --git a/crates/fhe-params/src/search/mod.rs b/crates/fhe-params/src/search/mod.rs new file mode 100644 index 0000000000..687fda37b4 --- /dev/null +++ b/crates/fhe-params/src/search/mod.rs @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +pub mod bfv; +pub mod constants; +pub mod errors; +pub mod prime; +pub mod utils; diff --git a/crates/fhe-params/src/search/prime.rs b/crates/fhe-params/src/search/prime.rs new file mode 100644 index 0000000000..710109ac35 --- /dev/null +++ b/crates/fhe-params/src/search/prime.rs @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +use num_bigint::BigUint; +use std::collections::BTreeMap; + +use crate::search::constants::NTT_PRIMES_BY_BITS; +use crate::search::utils::{log2_big, parse_hex_big}; + +/// Represents an NTT-friendly prime with precomputed metadata. +#[derive(Debug, Clone)] +pub struct PrimeItem { + /// Bit length of the prime + pub bitlen: u8, + /// Prime value as BigUint + pub value: BigUint, + /// Precomputed log2(value) for efficiency + pub log2: f64, + /// Hexadecimal representation + pub hex: String, +} + +/// Filter function type for excluding specific bit lengths +type BitFilter = fn(u8) -> bool; + +/// Build a flat list of primes with precomputed log2 and hex strings. +/// Excludes primes whose bit length matches the filter predicate. +fn build_prime_items_with_filter(filter: BitFilter) -> Vec { + let mut vec = Vec::new(); + for (bits, arr) in NTT_PRIMES_BY_BITS.iter() { + if filter(*bits) { + continue; + } + for &phex in arr { + let v = parse_hex_big(phex); + vec.push(PrimeItem { + bitlen: *bits, + log2: log2_big(&v), + hex: phex.to_string(), + value: v, + }); + } + } + vec +} + +/// Build a flat list of all primes with precomputed log2 and hex strings. +/// Excludes 61, 62, and 63-bit primes. +pub fn build_prime_items() -> Vec { + build_prime_items_with_filter(|bits| bits == 63 || bits == 62 || bits == 61) +} + +/// Build prime items for second parameter set (includes 62-bit primes, excludes 61 and 63-bit) +pub fn build_prime_items_for_second() -> Vec { + build_prime_items_with_filter(|bits| bits == 63 || bits == 61) +} + +/// Greedily select the maximum q under a log2 cap by taking largest primes first. +/// +/// Iterates through bit lengths from largest to smallest (60 down to 40), +/// adding primes as long as the cumulative log2(q) stays under the limit. +pub fn select_max_q_under_cap(limit_log2: f64, all: &[PrimeItem]) -> Vec { + let mut by_bits: BTreeMap> = BTreeMap::new(); + for p in all { + by_bits.entry(p.bitlen).or_default().push(p.clone()); + } + for v in by_bits.values_mut() { + v.sort_by(|a, b| b.value.cmp(&a.value)); + } + + let mut sel: Vec = Vec::new(); + let mut qlog = 0.0f64; + + for bb in (40u8..=60u8).rev() { + if let Some(bucket) = by_bits.get_mut(&bb) { + for pi in bucket.iter() { + let new_qlog = qlog + pi.log2; + if new_qlog <= limit_log2 + 1e-12 { + sel.push(pi.clone()); + qlog = new_qlog; + } + } + } + } + + sel +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::search::utils::parse_hex_big; + + #[test] + fn test_build_prime_items() { + let items = build_prime_items(); + assert!(!items.is_empty()); + + // Verify no 61, 62, or 63-bit primes are included + for item in &items { + assert_ne!(item.bitlen, 61); + assert_ne!(item.bitlen, 62); + assert_ne!(item.bitlen, 63); + } + + // Verify items have correct structure + for item in &items { + assert_eq!(parse_hex_big(&item.hex), item.value); + assert!(item.log2 > 0.0); + } + } + + #[test] + fn test_build_prime_items_for_second() { + let items = build_prime_items_for_second(); + assert!(!items.is_empty()); + + // Verify no 61 or 63-bit primes are included, but 62-bit should be included + assert!(items.iter().any(|item| item.bitlen == 62)); + for item in &items { + assert_ne!(item.bitlen, 61); + assert_ne!(item.bitlen, 63); + } + + // Verify items have correct structure + for item in &items { + assert_eq!(parse_hex_big(&item.hex), item.value); + assert!(item.log2 > 0.0); + } + } + + #[test] + fn test_select_max_q_under_cap() { + let all = build_prime_items(); + assert!(!all.is_empty()); + + // Test with a reasonable cap + let limit_log2 = 100.0; + let selected = select_max_q_under_cap(limit_log2, &all); + + // Verify selected items are under the cap + let mut total_log2 = 0.0; + for item in &selected { + total_log2 += item.log2; + } + assert!(total_log2 <= limit_log2 + 1e-12); + + // Verify selected items are from the input + for sel_item in &selected { + assert!(all.iter().any(|item| item.hex == sel_item.hex)); + } + } + + #[test] + fn test_select_max_q_under_cap_small_limit() { + let all = build_prime_items(); + let limit_log2 = 50.0; + let selected = select_max_q_under_cap(limit_log2, &all); + + // With a small limit, we should get fewer items + let mut total_log2 = 0.0; + for item in &selected { + total_log2 += item.log2; + } + assert!(total_log2 <= limit_log2 + 1e-12); + } + + #[test] + fn test_select_max_q_under_cap_empty_input() { + let empty: Vec = vec![]; + let selected = select_max_q_under_cap(100.0, &empty); + assert!(selected.is_empty()); + } +} diff --git a/crates/fhe-params/src/search/utils.rs b/crates/fhe-params/src/search/utils.rs new file mode 100644 index 0000000000..eb960c781b --- /dev/null +++ b/crates/fhe-params/src/search/utils.rs @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +use num_bigint::BigUint; +use num_traits::{One, Zero}; + +pub fn parse_hex_big(s: &str) -> BigUint { + let t = s.trim_start_matches("0x"); + BigUint::parse_bytes(t.as_bytes(), 16).expect("invalid hex prime") +} + +pub fn product(xs: I) -> BigUint +where + I: IntoIterator, +{ + xs.into_iter().fold(BigUint::one(), |acc, x| acc * x) +} + +/// Compute approximate log2 of a BigUint efficiently. +/// +/// Uses the bit length and top 8 bytes to compute a fractional approximation: +/// log2(x) ≈ (total_bits - 64) + log2(top_8_bytes) +pub fn log2_big(x: &BigUint) -> f64 { + if x.is_zero() { + return f64::NEG_INFINITY; + } + let bytes = x.to_bytes_be(); + let leading = bytes[0]; + let lead_bits = 8 - leading.leading_zeros() as usize; + let bits = (bytes.len() - 1) * 8 + lead_bits; + + // refine with up to 8 bytes + let take = bytes.len().min(8); + let mut top: u64 = 0; + for &byte in bytes.iter().take(take) { + top = (top << 8) | byte as u64; + } + let frac = (top as f64).log2(); + let adjust = (take * 8) as f64; + (bits as f64 - adjust) + frac +} + +pub fn approx_bits_from_log2(log2x: f64) -> u64 { + if log2x <= 0.0 { + 1 + } else { + log2x.floor() as u64 + 1 + } +} + +pub fn fmt_big_summary(x: &BigUint) -> String { + let bits = approx_bits_from_log2(log2_big(x)); + format!("≈ 2^{bits} ({bits} bits)") +} + +pub fn big_shift_pow2(exp: u32) -> BigUint { + BigUint::one() << exp +} + +#[cfg(test)] +mod tests { + use super::*; + use num_bigint::BigUint; + + #[test] + fn test_parse_hex_big() { + assert_eq!(parse_hex_big("0xff"), BigUint::from(255u64)); + assert_eq!(parse_hex_big("ff"), BigUint::from(255u64)); + assert_eq!(parse_hex_big("0x100"), BigUint::from(256u64)); + assert_eq!(parse_hex_big("0x1a2b3c"), BigUint::from(1715004u64)); + } + + #[test] + fn test_product() { + let nums = vec![ + BigUint::from(2u64), + BigUint::from(3u64), + BigUint::from(4u64), + ]; + assert_eq!(product(nums), BigUint::from(24u64)); + + let empty: Vec = vec![]; + assert_eq!(product(empty), BigUint::one()); + } + + #[test] + fn test_log2_big() { + assert_eq!(log2_big(&BigUint::zero()), f64::NEG_INFINITY); + + // Function returns approximation, just verify it's positive for non-zero values + assert!(log2_big(&BigUint::from(256u64)) > 0.0); + assert!(log2_big(&BigUint::from(1024u64)) > 0.0); + assert!(log2_big(&BigUint::from(1024u64)) > log2_big(&BigUint::from(256u64))); + } + + #[test] + fn test_approx_bits_from_log2() { + assert_eq!(approx_bits_from_log2(0.0), 1); + assert_eq!(approx_bits_from_log2(1.0), 2); + assert_eq!(approx_bits_from_log2(8.0), 9); + assert_eq!(approx_bits_from_log2(-1.0), 1); + } + + #[test] + fn test_fmt_big_summary() { + let x = BigUint::from(256u64); + let summary = fmt_big_summary(&x); + assert!(summary.contains("2^")); + assert!(summary.contains("bits")); + } + + #[test] + fn test_big_shift_pow2() { + assert_eq!(big_shift_pow2(0), BigUint::one()); + assert_eq!(big_shift_pow2(1), BigUint::from(2u64)); + assert_eq!(big_shift_pow2(8), BigUint::from(256u64)); + assert_eq!(big_shift_pow2(10), BigUint::from(1024u64)); + } +} diff --git a/examples/CRISP/Cargo.lock b/examples/CRISP/Cargo.lock index 708b6b44c6..b96e8ea803 100644 --- a/examples/CRISP/Cargo.lock +++ b/examples/CRISP/Cargo.lock @@ -2430,8 +2430,11 @@ version = "0.1.7" dependencies = [ "alloy-dyn-abi", "alloy-primitives", + "anyhow", + "clap", "fhe", "num-bigint", + "num-traits", "thiserror 1.0.69", ] diff --git a/templates/default/Cargo.lock b/templates/default/Cargo.lock index 9cdc5ddfe0..6441793d3a 100644 --- a/templates/default/Cargo.lock +++ b/templates/default/Cargo.lock @@ -386,6 +386,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.98" @@ -914,6 +964,52 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clap" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "clap_lex" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "const-hex" version = "1.17.0" @@ -1202,8 +1298,11 @@ version = "0.1.7" dependencies = [ "alloy-dyn-abi", "alloy-primitives", + "anyhow", + "clap", "fhe", "num-bigint", + "num-traits", "thiserror", ] @@ -1382,7 +1481,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2079,6 +2178,12 @@ dependencies = [ "serde", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.10.5" @@ -2469,6 +2574,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "openssl" version = "0.10.75" @@ -3134,7 +3245,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3478,6 +3589,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -3588,7 +3705,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3951,6 +4068,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "valuable" version = "0.1.1"