From 05ab656944ed8e4c4765a1e852252598791771a6 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Tue, 27 Jan 2026 17:08:09 +0100 Subject: [PATCH 01/16] refactor: integrate crypto_params into fhe-params as search module - Move crypto_params crate into fhe-params as search module - Update all imports to use crate::search:: paths - Add required dependencies (num-traits, clap, anyhow) to fhe-params - Remove unused num-integer dependency - Update documentation references from crypto_params to search - Delete standalone crypto_params directory --- Cargo.lock | 3 + crates/fhe-params/Cargo.toml | 3 + crates/fhe-params/src/lib.rs | 1 + crates/fhe-params/src/presets.rs | 4 +- crates/fhe-params/src/search/bfv.rs | 679 ++++++++++++++++++++++ crates/fhe-params/src/search/constants.rs | 282 +++++++++ crates/fhe-params/src/search/errors.rs | 153 +++++ crates/fhe-params/src/search/mod.rs | 5 + crates/fhe-params/src/search/prime.rs | 86 +++ crates/fhe-params/src/search/utils.rs | 52 ++ 10 files changed, 1266 insertions(+), 2 deletions(-) create mode 100644 crates/fhe-params/src/search/bfv.rs create mode 100644 crates/fhe-params/src/search/constants.rs create mode 100644 crates/fhe-params/src/search/errors.rs create mode 100644 crates/fhe-params/src/search/mod.rs create mode 100644 crates/fhe-params/src/search/prime.rs create mode 100644 crates/fhe-params/src/search/utils.rs 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/fhe-params/Cargo.toml b/crates/fhe-params/Cargo.toml index 935dce4aab..9d47641e44 100644 --- a/crates/fhe-params/Cargo.toml +++ b/crates/fhe-params/Cargo.toml @@ -9,7 +9,10 @@ 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 } diff --git a/crates/fhe-params/src/lib.rs b/crates/fhe-params/src/lib.rs index e1fb4973aa..e25b4257bf 100644 --- a/crates/fhe-params/src/lib.rs +++ b/crates/fhe-params/src/lib.rs @@ -11,6 +11,7 @@ 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..753c52736e --- /dev/null +++ b/crates/fhe-params/src/search/bfv.rs @@ -0,0 +1,679 @@ +//! 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, PlaintextMode}; +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() + } +} + +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()) +} + +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()).collect::>()); + + // Compute plaintext space per mode + let k_plain_eff: u128 = match PlaintextMode::FromQi { + PlaintextMode::MaxUserAndQi => bfv_search_config.k.max(bfv_search_config.z), + PlaintextMode::FromQi => 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, + }) +} + +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()) + .collect::>(), + ); + 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) +} + +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; + + // Fewest primes first: start from minimal s needed to reach target with 61-bit primes + let s = target_bits.div_ceil(61).max(2) as usize; + + let r_float = target_f / (s as f64); + let floor_r = r_float.floor().clamp(40.0, 61.0) as u8; + let ceil_r = r_float.ceil().clamp(40.0, 61.0) 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()).collect::>()); + 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 +} + +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 +} + +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()) + .collect::>(), + ); + 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()) +} + +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()).collect::>()); + 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 +} + +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()).collect::>()); + 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); + + 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); + + let ok = lhs < delta; + println!( + " 2*B_C {} Δ => {}", + if ok { "<" } else { "≥" }, + if ok { "PASS ✅" } else { "fail ❌" } + ); + if !ok { + return None; + } + + 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, + }) +} diff --git a/crates/fhe-params/src/search/constants.rs b/crates/fhe-params/src/search/constants.rs new file mode 100644 index 0000000000..7dc79688b5 --- /dev/null +++ b/crates/fhe-params/src/search/constants.rs @@ -0,0 +1,282 @@ +/// 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", + ], + ), +]; + +/// Bounds to search within for the LWE dimension +pub const D_POW2_START: u64 = 256; +pub const D_POW2_MAX: u64 = 32768; +/// Max number of ciphertext +pub const K_MAX: u128 = 1u128 << 25; // 33,554,432 + +/// Plaintext mode +/// - MaxUserAndQi: use the user-defined plaintext modulus k and the largest prime found in the search +/// - FromQi: use the largest prime found in the search +#[derive(Copy, Clone, Debug)] +pub enum PlaintextMode { + MaxUserAndQi, + FromQi, +} diff --git a/crates/fhe-params/src/search/errors.rs b/crates/fhe-params/src/search/errors.rs new file mode 100644 index 0000000000..f8e4374bd9 --- /dev/null +++ b/crates/fhe-params/src/search/errors.rs @@ -0,0 +1,153 @@ +//! 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 }, + + /// Mathematical computation errors + #[error("Mathematical error: {message}")] + Math { message: String }, + + /// Configuration errors + #[error("Configuration error: {message}")] + Config { message: String }, + + /// Prime selection errors + #[error("Prime selection error: {message}")] + PrimeSelection { 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 }, + + /// Invalid number of parties + #[error("Invalid number of parties: {n} - {reason}")] + InvalidParties { n: u128, reason: String }, + + /// Invalid security parameter + #[error("Invalid security parameter: {lambda} - {reason}")] + InvalidSecurity { lambda: u32, reason: String }, + + /// Invalid error bound + #[error("Invalid error bound: {b} - {reason}")] + InvalidErrorBound { b: u128, reason: String }, + + /// General validation error + #[error("Validation failed: {message}")] + General { message: 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, + + /// Equation validation failed + #[error("Equation validation failed: {equation} - {reason}")] + EquationValidation { equation: String, reason: String }, + + /// Prime selection failed + #[error("Failed to select suitable primes: {reason}")] + PrimeSelection { reason: String }, + + /// General search error + #[error("Search failed: {message}")] + General { message: String }, +} + +// 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(), + } + } +} + +// Helper functions for creating errors with context +impl BfvParamsError { + /// Create a validation error with a message + pub fn validation(message: impl Into) -> Self { + BfvParamsError::Validation { + message: message.into(), + } + } + + /// Create a search error with a message + pub fn search(message: impl Into) -> Self { + BfvParamsError::Search { + message: message.into(), + } + } + + /// Create a mathematical error with a message + pub fn math(message: impl Into) -> Self { + BfvParamsError::Math { + message: message.into(), + } + } + + /// Create a configuration error with a message + pub fn config(message: impl Into) -> Self { + BfvParamsError::Config { + message: message.into(), + } + } + + /// Create a prime selection error with a message + pub fn prime_selection(message: impl Into) -> Self { + BfvParamsError::PrimeSelection { + message: message.into(), + } + } +} diff --git a/crates/fhe-params/src/search/mod.rs b/crates/fhe-params/src/search/mod.rs new file mode 100644 index 0000000000..493b8d8ee9 --- /dev/null +++ b/crates/fhe-params/src/search/mod.rs @@ -0,0 +1,5 @@ +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..7994f5d7d5 --- /dev/null +++ b/crates/fhe-params/src/search/prime.rs @@ -0,0 +1,86 @@ +use num_bigint::BigUint; +use num_traits::One; +use std::collections::BTreeMap; + +use crate::search::constants::NTT_PRIMES_BY_BITS; +use crate::search::utils::{log2_big, parse_hex_big}; + +#[derive(Debug, Clone)] +pub struct PrimeItem { + pub bitlen: u8, + pub value: BigUint, + pub log2: f64, + pub hex: String, +} + +/// Build a flat list of all primes with precomputed log2 and hex strings. +pub fn build_prime_items() -> Vec { + let mut vec = Vec::new(); + for (bits, arr) in NTT_PRIMES_BY_BITS.iter() { + if *bits == 63 || *bits == 62 || *bits == 61 { + 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 prime items for second parameter set (includes 62-bit primes, excludes 61 and 63-bit) +pub fn build_prime_items_for_second() -> Vec { + let mut vec = Vec::new(); + for (bits, arr) in NTT_PRIMES_BY_BITS.iter() { + if *bits == 63 || *bits == 61 { + 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 +} + +pub fn select_max_q_under_cap(limit_log2: f64, all: &[PrimeItem]) -> Vec { + // Greedy: take largest primes from larger bit-lengths first, mixing buckets, + // ensuring log2(q) stays under the cap as we add + 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 q = BigUint::one(); + 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() { + // tentative + let new_qlog = qlog + pi.log2; + if new_qlog <= limit_log2 + 1e-12 { + sel.push(pi.clone()); + q *= &pi.value; + qlog = new_qlog; + } + } + } + } + + sel +} diff --git a/crates/fhe-params/src/search/utils.rs b/crates/fhe-params/src/search/utils.rs new file mode 100644 index 0000000000..b12145ed7f --- /dev/null +++ b/crates/fhe-params/src/search/utils.rs @@ -0,0 +1,52 @@ +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: &[BigUint]) -> BigUint { + let mut acc = BigUint::one(); + for x in xs { + acc *= x; + } + acc +} + +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 +} From a6df62e10f7cc725e3320357daa50a12a00b06ba Mon Sep 17 00:00:00 2001 From: Cedoor Date: Tue, 27 Jan 2026 17:09:00 +0100 Subject: [PATCH 02/16] style: fix rustfmt formatting in search module --- crates/fhe-params/src/search/bfv.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/fhe-params/src/search/bfv.rs b/crates/fhe-params/src/search/bfv.rs index 753c52736e..648f6b66dc 100644 --- a/crates/fhe-params/src/search/bfv.rs +++ b/crates/fhe-params/src/search/bfv.rs @@ -5,11 +5,15 @@ //! and parameter validation. use std::collections::BTreeMap; -use crate::search::constants::{D_POW2_MAX, D_POW2_START, K_MAX, PlaintextMode}; +use crate::search::constants::{PlaintextMode, 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 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}; From b5e6b99b7446f1f41b431d2d938221b523bfc4b7 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Tue, 27 Jan 2026 17:10:07 +0100 Subject: [PATCH 03/16] chore: add license headers --- crates/fhe-params/src/search/bfv.rs | 6 ++++++ crates/fhe-params/src/search/constants.rs | 6 ++++++ crates/fhe-params/src/search/errors.rs | 6 ++++++ crates/fhe-params/src/search/mod.rs | 6 ++++++ crates/fhe-params/src/search/prime.rs | 6 ++++++ crates/fhe-params/src/search/utils.rs | 6 ++++++ 6 files changed, 36 insertions(+) diff --git a/crates/fhe-params/src/search/bfv.rs b/crates/fhe-params/src/search/bfv.rs index 648f6b66dc..4fdd579348 100644 --- a/crates/fhe-params/src/search/bfv.rs +++ b/crates/fhe-params/src/search/bfv.rs @@ -1,3 +1,9 @@ +// 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) diff --git a/crates/fhe-params/src/search/constants.rs b/crates/fhe-params/src/search/constants.rs index 7dc79688b5..9070874b32 100644 --- a/crates/fhe-params/src/search/constants.rs +++ b/crates/fhe-params/src/search/constants.rs @@ -1,3 +1,9 @@ +// 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])] = &[ ( diff --git a/crates/fhe-params/src/search/errors.rs b/crates/fhe-params/src/search/errors.rs index f8e4374bd9..2d0fd5a31c 100644 --- a/crates/fhe-params/src/search/errors.rs +++ b/crates/fhe-params/src/search/errors.rs @@ -1,3 +1,9 @@ +// 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, diff --git a/crates/fhe-params/src/search/mod.rs b/crates/fhe-params/src/search/mod.rs index 493b8d8ee9..687fda37b4 100644 --- a/crates/fhe-params/src/search/mod.rs +++ b/crates/fhe-params/src/search/mod.rs @@ -1,3 +1,9 @@ +// 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; diff --git a/crates/fhe-params/src/search/prime.rs b/crates/fhe-params/src/search/prime.rs index 7994f5d7d5..114741ea99 100644 --- a/crates/fhe-params/src/search/prime.rs +++ b/crates/fhe-params/src/search/prime.rs @@ -1,3 +1,9 @@ +// 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; use std::collections::BTreeMap; diff --git a/crates/fhe-params/src/search/utils.rs b/crates/fhe-params/src/search/utils.rs index b12145ed7f..68f3e8dccb 100644 --- a/crates/fhe-params/src/search/utils.rs +++ b/crates/fhe-params/src/search/utils.rs @@ -1,3 +1,9 @@ +// 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}; From 85b3200b00592730d4217fae12c64c6ccafb2695 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Tue, 27 Jan 2026 19:27:38 +0100 Subject: [PATCH 04/16] chore: update lockfiles --- examples/CRISP/Cargo.lock | 3 + templates/default/Cargo.lock | 129 ++++++++++++++++++++++++++++++++++- 2 files changed, 129 insertions(+), 3 deletions(-) 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" From d67b73e84e4a17aecd779d1520b17a41dd3fcb9c Mon Sep 17 00:00:00 2001 From: Cedoor Date: Wed, 28 Jan 2026 11:27:10 +0100 Subject: [PATCH 05/16] refactor: remove unused error variants and helpers - Remove unused BfvParamsError variants (Math, Config, PrimeSelection) - Remove unused ValidationError variants (InvalidParties, InvalidSecurity, InvalidErrorBound, General) - Remove unused SearchError variants (EquationValidation, PrimeSelection, General) - Remove unused helper functions (validation, search, math, config, prime_selection) --- crates/fhe-params/src/search/errors.rs | 78 -------------------------- 1 file changed, 78 deletions(-) diff --git a/crates/fhe-params/src/search/errors.rs b/crates/fhe-params/src/search/errors.rs index 2d0fd5a31c..deda4752ce 100644 --- a/crates/fhe-params/src/search/errors.rs +++ b/crates/fhe-params/src/search/errors.rs @@ -25,18 +25,6 @@ pub enum BfvParamsError { #[error("Parameter search error: {message}")] Search { message: String }, - /// Mathematical computation errors - #[error("Mathematical error: {message}")] - Math { message: String }, - - /// Configuration errors - #[error("Configuration error: {message}")] - Config { message: String }, - - /// Prime selection errors - #[error("Prime selection error: {message}")] - PrimeSelection { message: String }, - /// Generic error with context #[error("Error: {message}")] Generic { message: String }, @@ -51,22 +39,6 @@ pub enum ValidationError { /// Invalid number of votes/plaintext additions #[error("Invalid number of votes: {z} - {reason}")] InvalidVotes { z: u128, reason: String }, - - /// Invalid number of parties - #[error("Invalid number of parties: {n} - {reason}")] - InvalidParties { n: u128, reason: String }, - - /// Invalid security parameter - #[error("Invalid security parameter: {lambda} - {reason}")] - InvalidSecurity { lambda: u32, reason: String }, - - /// Invalid error bound - #[error("Invalid error bound: {b} - {reason}")] - InvalidErrorBound { b: u128, reason: String }, - - /// General validation error - #[error("Validation failed: {message}")] - General { message: String }, } /// Search error type for parameter search failures @@ -75,18 +47,6 @@ pub enum SearchError { /// No feasible parameters found #[error("No feasible BFV parameters found for the given constraints")] NoFeasibleParameters, - - /// Equation validation failed - #[error("Equation validation failed: {equation} - {reason}")] - EquationValidation { equation: String, reason: String }, - - /// Prime selection failed - #[error("Failed to select suitable primes: {reason}")] - PrimeSelection { reason: String }, - - /// General search error - #[error("Search failed: {message}")] - General { message: String }, } // Conversion implementations for better error handling @@ -119,41 +79,3 @@ impl From<&str> for BfvParamsError { } } } - -// Helper functions for creating errors with context -impl BfvParamsError { - /// Create a validation error with a message - pub fn validation(message: impl Into) -> Self { - BfvParamsError::Validation { - message: message.into(), - } - } - - /// Create a search error with a message - pub fn search(message: impl Into) -> Self { - BfvParamsError::Search { - message: message.into(), - } - } - - /// Create a mathematical error with a message - pub fn math(message: impl Into) -> Self { - BfvParamsError::Math { - message: message.into(), - } - } - - /// Create a configuration error with a message - pub fn config(message: impl Into) -> Self { - BfvParamsError::Config { - message: message.into(), - } - } - - /// Create a prime selection error with a message - pub fn prime_selection(message: impl Into) -> Self { - BfvParamsError::PrimeSelection { - message: message.into(), - } - } -} From 8ce29787806e0dd9c0cf1e5a06cc74bb961e36dc Mon Sep 17 00:00:00 2001 From: Cedoor Date: Wed, 28 Jan 2026 11:32:21 +0100 Subject: [PATCH 06/16] refactor(fhe-params): improve product function to accept iterators - Change product function to accept IntoIterator instead of slice - Use Iterator::fold for more idiomatic implementation - Update all call sites to pass iterators directly, avoiding Vec allocations --- crates/fhe-params/src/search/bfv.rs | 22 ++++++---------------- crates/fhe-params/src/search/utils.rs | 11 +++++------ 2 files changed, 11 insertions(+), 22 deletions(-) diff --git a/crates/fhe-params/src/search/bfv.rs b/crates/fhe-params/src/search/bfv.rs index 4fdd579348..ea2124766a 100644 --- a/crates/fhe-params/src/search/bfv.rs +++ b/crates/fhe-params/src/search/bfv.rs @@ -154,7 +154,7 @@ pub fn finalize_bfv_candidate( d: u64, chosen: Vec, ) -> Option { - let q_bfv = product(&chosen.iter().map(|pi| pi.value.clone()).collect::>()); + let q_bfv = product(chosen.iter().map(|pi| pi.value.clone())); // Compute plaintext space per mode let k_plain_eff: u128 = match PlaintextMode::FromQi { @@ -263,12 +263,7 @@ pub fn refine_from_initial( 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()) - .collect::>(), - ); + 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 @@ -367,7 +362,7 @@ pub fn construct_qi_for_target_bits( // 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()).collect::>()); + 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 { @@ -454,12 +449,7 @@ pub fn refine_second_param_at_d( return None; } - let initial_q = product( - &initial_sel - .iter() - .map(|pi| pi.value.clone()) - .collect::>(), - ); + 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(); @@ -562,7 +552,7 @@ pub fn construct_qi_second_param( let mut best: Option<(f64, Vec)> = None; for sel in tried { - let q = product(&sel.iter().map(|pi| pi.value.clone()).collect::>()); + 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 { @@ -617,7 +607,7 @@ pub fn finalize_second_param( } } - let q_bfv = product(&chosen.iter().map(|pi| pi.value.clone()).collect::>()); + 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; diff --git a/crates/fhe-params/src/search/utils.rs b/crates/fhe-params/src/search/utils.rs index 68f3e8dccb..30af8deb6f 100644 --- a/crates/fhe-params/src/search/utils.rs +++ b/crates/fhe-params/src/search/utils.rs @@ -12,12 +12,11 @@ pub fn parse_hex_big(s: &str) -> BigUint { BigUint::parse_bytes(t.as_bytes(), 16).expect("invalid hex prime") } -pub fn product(xs: &[BigUint]) -> BigUint { - let mut acc = BigUint::one(); - for x in xs { - acc *= x; - } - acc +pub fn product(xs: I) -> BigUint +where + I: IntoIterator, +{ + xs.into_iter().fold(BigUint::one(), |acc, x| acc * x) } pub fn log2_big(x: &BigUint) -> f64 { From 06db913afb6ba1b7426f24609f61d72038282364 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Wed, 28 Jan 2026 11:38:01 +0100 Subject: [PATCH 07/16] test: add essential tests for fhe-params search module - Add tests for utils.rs: parse_hex_big, product, log2_big, approx_bits_from_log2, fmt_big_summary, big_shift_pow2 - Add tests for prime.rs: build_prime_items, build_prime_items_for_second, select_max_q_under_cap - Add tests for bfv.rs: qi_values extraction, input validation, candidate finalization, prime construction --- crates/fhe-params/src/search/bfv.rs | 131 ++++++++++++++++++++++++++ crates/fhe-params/src/search/prime.rs | 87 +++++++++++++++++ crates/fhe-params/src/search/utils.rs | 61 ++++++++++++ 3 files changed, 279 insertions(+) diff --git a/crates/fhe-params/src/search/bfv.rs b/crates/fhe-params/src/search/bfv.rs index ea2124766a..400b41c8db 100644 --- a/crates/fhe-params/src/search/bfv.rs +++ b/crates/fhe-params/src/search/bfv.rs @@ -677,3 +677,134 @@ pub fn finalize_second_param( 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()); + + let small_primes = primes + .iter() + .filter(|p| p.bitlen <= 40) + .take(2) + .cloned() + .collect::>(); + + if !small_primes.is_empty() { + let k_plain = 1u128 << 50; + let d = 512; + let result = finalize_second_param(&config, d, small_primes, k_plain); + assert!(result.is_none() || result.is_some()); + } + } + + #[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/prime.rs b/crates/fhe-params/src/search/prime.rs index 114741ea99..4042b9755a 100644 --- a/crates/fhe-params/src/search/prime.rs +++ b/crates/fhe-params/src/search/prime.rs @@ -90,3 +90,90 @@ pub fn select_max_q_under_cap(limit_log2: f64, all: &[PrimeItem]) -> Vec 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 index 30af8deb6f..af01e59920 100644 --- a/crates/fhe-params/src/search/utils.rs +++ b/crates/fhe-params/src/search/utils.rs @@ -55,3 +55,64 @@ pub fn fmt_big_summary(x: &BigUint) -> String { 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)); + } +} From c402732126a94a41c0708c6e6db2bbb31462e897 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Wed, 28 Jan 2026 11:46:45 +0100 Subject: [PATCH 08/16] refactor: remove unused PlaintextMode enum - Remove PlaintextMode enum from constants.rs - Simplify k_plain_eff calculation to max(k, z) - Remove unused mode parameter from finalize_bfv_candidate --- crates/fhe-params/src/search/bfv.rs | 9 +++------ crates/fhe-params/src/search/constants.rs | 9 --------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/crates/fhe-params/src/search/bfv.rs b/crates/fhe-params/src/search/bfv.rs index 400b41c8db..c5fabcd06e 100644 --- a/crates/fhe-params/src/search/bfv.rs +++ b/crates/fhe-params/src/search/bfv.rs @@ -11,7 +11,7 @@ //! and parameter validation. use std::collections::BTreeMap; -use crate::search::constants::{PlaintextMode, D_POW2_MAX, D_POW2_START, K_MAX}; +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::{ @@ -156,11 +156,8 @@ pub fn finalize_bfv_candidate( ) -> Option { let q_bfv = product(chosen.iter().map(|pi| pi.value.clone())); - // Compute plaintext space per mode - let k_plain_eff: u128 = match PlaintextMode::FromQi { - PlaintextMode::MaxUserAndQi => bfv_search_config.k.max(bfv_search_config.z), - PlaintextMode::FromQi => bfv_search_config.k.max(bfv_search_config.z), - }; + // 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); diff --git a/crates/fhe-params/src/search/constants.rs b/crates/fhe-params/src/search/constants.rs index 9070874b32..02bc013c8f 100644 --- a/crates/fhe-params/src/search/constants.rs +++ b/crates/fhe-params/src/search/constants.rs @@ -277,12 +277,3 @@ pub const D_POW2_START: u64 = 256; pub const D_POW2_MAX: u64 = 32768; /// Max number of ciphertext pub const K_MAX: u128 = 1u128 << 25; // 33,554,432 - -/// Plaintext mode -/// - MaxUserAndQi: use the user-defined plaintext modulus k and the largest prime found in the search -/// - FromQi: use the largest prime found in the search -#[derive(Copy, Clone, Debug)] -pub enum PlaintextMode { - MaxUserAndQi, - FromQi, -} From 4ca63c1ae898b871e2f3bbd27357b74695f32492 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Wed, 28 Jan 2026 11:54:27 +0100 Subject: [PATCH 09/16] refactor(search): unify prime building functions and optimize selection - Extract build_prime_items_with_filter to eliminate code duplication - Unify build_prime_items and build_prime_items_for_second using filter function - Remove unused q variable from select_max_q_under_cap (only qlog needed) - Remove unused num_traits::One import --- crates/fhe-params/src/search/prime.rs | 37 ++++++++++----------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/crates/fhe-params/src/search/prime.rs b/crates/fhe-params/src/search/prime.rs index 4042b9755a..7a825dc013 100644 --- a/crates/fhe-params/src/search/prime.rs +++ b/crates/fhe-params/src/search/prime.rs @@ -5,7 +5,6 @@ // or FITNESS FOR A PARTICULAR PURPOSE. use num_bigint::BigUint; -use num_traits::One; use std::collections::BTreeMap; use crate::search::constants::NTT_PRIMES_BY_BITS; @@ -19,11 +18,15 @@ pub struct PrimeItem { pub hex: String, } -/// Build a flat list of all primes with precomputed log2 and hex strings. -pub fn build_prime_items() -> Vec { +/// 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 *bits == 63 || *bits == 62 || *bits == 61 { + if filter(*bits) { continue; } for &phex in arr { @@ -39,24 +42,15 @@ pub fn build_prime_items() -> Vec { 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 { - let mut vec = Vec::new(); - for (bits, arr) in NTT_PRIMES_BY_BITS.iter() { - if *bits == 63 || *bits == 61 { - 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_prime_items_with_filter(|bits| bits == 63 || bits == 61) } pub fn select_max_q_under_cap(limit_log2: f64, all: &[PrimeItem]) -> Vec { @@ -71,17 +65,14 @@ pub fn select_max_q_under_cap(limit_log2: f64, all: &[PrimeItem]) -> Vec = Vec::new(); - let mut q = BigUint::one(); 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() { - // tentative let new_qlog = qlog + pi.log2; if new_qlog <= limit_log2 + 1e-12 { sel.push(pi.clone()); - q *= &pi.value; qlog = new_qlog; } } From 21717fd7211499d0e823498691cafaf77e93e5ee Mon Sep 17 00:00:00 2001 From: Cedoor Date: Wed, 28 Jan 2026 11:58:13 +0100 Subject: [PATCH 10/16] docs: add documentation to search module files - Add function-level docs for public APIs in bfv.rs - Document algorithm strategies and validation logic - Add struct documentation for PrimeItem - Document utility functions and constants - Note relationship between search results and hardcoded presets --- crates/fhe-params/src/search/bfv.rs | 44 +++++++++++++++++++++++ crates/fhe-params/src/search/constants.rs | 5 +-- crates/fhe-params/src/search/prime.rs | 11 ++++-- crates/fhe-params/src/search/utils.rs | 4 +++ 4 files changed, 60 insertions(+), 4 deletions(-) diff --git a/crates/fhe-params/src/search/bfv.rs b/crates/fhe-params/src/search/bfv.rs index c5fabcd06e..e7f8da3db2 100644 --- a/crates/fhe-params/src/search/bfv.rs +++ b/crates/fhe-params/src/search/bfv.rs @@ -75,6 +75,18 @@ impl BfvSearchResult { } } +/// 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(); @@ -149,6 +161,12 @@ pub fn bfv_search(bfv_search_config: &BfvSearchConfig) -> BfvParamsResult 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 { - // Greedy: take largest primes from larger bit-lengths first, mixing buckets, - // ensuring log2(q) stays under the cap as we add let mut by_bits: BTreeMap> = BTreeMap::new(); for p in all { by_bits.entry(p.bitlen).or_default().push(p.clone()); diff --git a/crates/fhe-params/src/search/utils.rs b/crates/fhe-params/src/search/utils.rs index af01e59920..eb960c781b 100644 --- a/crates/fhe-params/src/search/utils.rs +++ b/crates/fhe-params/src/search/utils.rs @@ -19,6 +19,10 @@ where 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; From 70eeeb11f9382c5324f659851744a735253c976a Mon Sep 17 00:00:00 2001 From: Cedoor Date: Wed, 28 Jan 2026 12:14:36 +0100 Subject: [PATCH 11/16] docs: update fhe-params README with comprehensive crate documentation - Replace crypto_params-specific README with fhe-params crate documentation - Add comprehensive documentation for all modules (presets, builder, search, encoding) - Include detailed search module documentation with security constraints and usage - Add usage examples for presets, custom parameter sets, parameter search, and ABI encoding - Fix terminology: change 'zkFHE' to 'FHE' in lib.rs and README --- crates/fhe-params/README.md | 203 +++++++++++++++++++++++++++++++++++ crates/fhe-params/src/lib.rs | 2 +- 2 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 crates/fhe-params/README.md diff --git a/crates/fhe-params/README.md b/crates/fhe-params/README.md new file mode 100644 index 0000000000..6fc25d02bb --- /dev/null +++ b/crates/fhe-params/README.md @@ -0,0 +1,203 @@ +# 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 +- **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}; +use std::sync::Arc; + +// 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)?; +``` + +### 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 + +```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); + } +} +``` + +### 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/lib.rs b/crates/fhe-params/src/lib.rs index e25b4257bf..3625b066e1 100644 --- a/crates/fhe-params/src/lib.rs +++ b/crates/fhe-params/src/lib.rs @@ -4,7 +4,7 @@ // 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; From 1607bf4d23d0b0a55d589824718ee9c8165ddd52 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Wed, 28 Jan 2026 12:59:03 +0100 Subject: [PATCH 12/16] fix(fhe-params): address code review feedback - Add missing build_pair_for_preset import in README example - Wrap README example in function returning Result for ? operator usage - Move Eq1 validation outside verbose block to always execute - Replace tautological test assertion with concrete property validations - Fix hardcoded 61-bit clamp to use actual max bit length from prime buckets --- crates/fhe-params/README.md | 14 ++++--- crates/fhe-params/src/search/bfv.rs | 59 ++++++++++++++++++++++++----- 2 files changed, 58 insertions(+), 15 deletions(-) diff --git a/crates/fhe-params/README.md b/crates/fhe-params/README.md index 6fc25d02bb..b7ec0ca902 100644 --- a/crates/fhe-params/README.md +++ b/crates/fhe-params/README.md @@ -126,14 +126,18 @@ This enables serialization for smart contracts and cross-platform parameter exch ### Using Presets ```rust -use e3_fhe_params::{BfvPreset, build_bfv_params_arc}; +use e3_fhe_params::{BfvPreset, build_bfv_params_arc, builder::build_pair_for_preset}; use std::sync::Arc; -// Build threshold BFV parameters -let params = build_bfv_params_arc(BfvPreset::SecureThresholdBfv8192)?; +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)?; -// Build both threshold and DKG parameter pairs -let (threshold_params, dkg_params) = build_pair_for_preset(BfvPreset::SecureThresholdBfv8192)?; + Ok(()) +} ``` ### Custom Parameter Sets diff --git a/crates/fhe-params/src/search/bfv.rs b/crates/fhe-params/src/search/bfv.rs index e7f8da3db2..1b3aaeb872 100644 --- a/crates/fhe-params/src/search/bfv.rs +++ b/crates/fhe-params/src/search/bfv.rs @@ -335,12 +335,15 @@ pub fn construct_qi_for_target_bits( let target_f = target_bits as f64; - // Fewest primes first: start from minimal s needed to reach target with 61-bit primes - let s = target_bits.div_ceil(61).max(2) as usize; + // 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, 61.0) as u8; - let ceil_r = r_float.ceil().clamp(40.0, 61.0) as u8; + 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(); @@ -665,6 +668,11 @@ pub fn finalize_second_param( 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!( @@ -690,15 +698,11 @@ pub fn finalize_second_param( println!(" B_C = B_fresh = {}", b_c.to_str_radix(10)); println!(" log2(2*B_C)≈{:.3} log2(Δ)≈{:.3}", lhs_log2, rhs_log2); - let ok = lhs < delta; println!( " 2*B_C {} Δ => {}", if ok { "<" } else { "≥" }, if ok { "PASS ✅" } else { "fail ❌" } ); - if !ok { - return None; - } println!("\n*** BFV-2nd FEASIBLE at d={} ***", d); } @@ -818,6 +822,7 @@ mod tests { 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) @@ -826,10 +831,44 @@ mod tests { .collect::>(); if !small_primes.is_empty() { - let k_plain = 1u128 << 50; + 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); - assert!(result.is_none() || result.is_some()); + // 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); } } From ac4ac0bc04a899ba5e2369fc91edbc6f765445ee Mon Sep 17 00:00:00 2001 From: Cedoor Date: Wed, 28 Jan 2026 17:44:45 +0100 Subject: [PATCH 13/16] feat: add BFV parameter search CLI tool - Add search_params binary for interactive BFV parameter search - Refactor CLI code to reduce duplication with helper functions - Fix variance calculations to handle BigUint overflow - Add comprehensive CLI documentation to README - Include example for reproducing SecureThresholdBfv8192 preset --- crates/fhe-params/Cargo.toml | 4 + crates/fhe-params/README.md | 71 ++++++ crates/fhe-params/src/bin/search_params.rs | 241 +++++++++++++++++++++ 3 files changed, 316 insertions(+) create mode 100644 crates/fhe-params/src/bin/search_params.rs diff --git a/crates/fhe-params/Cargo.toml b/crates/fhe-params/Cargo.toml index 9d47641e44..b5fe6bf513 100644 --- a/crates/fhe-params/Cargo.toml +++ b/crates/fhe-params/Cargo.toml @@ -16,6 +16,10 @@ 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 index b7ec0ca902..a8532292ee 100644 --- a/crates/fhe-params/README.md +++ b/crates/fhe-params/README.md @@ -12,6 +12,7 @@ parameters that satisfy security constraints. 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 @@ -157,6 +158,8 @@ 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}; @@ -182,6 +185,74 @@ match bfv_search(&config) { } ``` +#### 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 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..521acd3d32 --- /dev/null +++ b/crates/fhe-params/src/bin/search_params.rs @@ -0,0 +1,241 @@ +//! 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..61 bits; 62-bit and 63-bit 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================================================================================"); +} From 5f35f062d353839f4f07a91485cac202d4a9ab28 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Wed, 28 Jan 2026 17:46:00 +0100 Subject: [PATCH 14/16] chore: add SPDX license header to search_params.rs --- crates/fhe-params/src/bin/search_params.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/fhe-params/src/bin/search_params.rs b/crates/fhe-params/src/bin/search_params.rs index 521acd3d32..126ff85fd3 100644 --- a/crates/fhe-params/src/bin/search_params.rs +++ b/crates/fhe-params/src/bin/search_params.rs @@ -1,3 +1,9 @@ +// 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. From 7c85fa1abd9920623be16e0f74efdb0096958845 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Wed, 28 Jan 2026 18:00:22 +0100 Subject: [PATCH 15/16] fix(docker): stub fhe-params search_params binary for cache build - Add stub src/bin/search_params.rs before first cargo build - Fixes CI build_ciphernode_image when e3-fhe-params declares [[bin]] --- crates/Dockerfile | 3 +++ 1 file changed, 3 insertions(+) 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 . From 1b785f135166eb447eec94004ae7e89ba9708b30 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Wed, 28 Jan 2026 18:04:31 +0100 Subject: [PATCH 16/16] docs(fhe-params): align verbose banner with build_prime_items docs - Change prime range from 40..61 to 40..60 bits - State that 61-, 62- and 63-bit primes are excluded (matches build_prime_items()) --- crates/fhe-params/src/bin/search_params.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/fhe-params/src/bin/search_params.rs b/crates/fhe-params/src/bin/search_params.rs index 126ff85fd3..ff6874bb67 100644 --- a/crates/fhe-params/src/bin/search_params.rs +++ b/crates/fhe-params/src/bin/search_params.rs @@ -152,7 +152,7 @@ fn main() { if args.verbose { println!( - "== BFV parameter search (NTT-friendly primes 40..61 bits; 62-bit and 63-bit are excluded) ==" + "== 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={}",