From adebcecb6038e4da92c081f6512057a4f2c60918 Mon Sep 17 00:00:00 2001 From: 0xjei Date: Mon, 15 Jun 2026 11:43:15 +0200 Subject: [PATCH 1/6] update search algo to latest 3 mod approach --- crates/fhe-params/src/bin/search_params.rs | 160 +++++---- crates/fhe-params/src/search/bfv.rs | 293 ++++++++++------- crates/fhe-params/src/search/constants.rs | 360 ++++++++++++++++----- crates/fhe-params/src/search/prime.rs | 10 +- crates/fhe-params/src/search/utils.rs | 13 +- 5 files changed, 560 insertions(+), 276 deletions(-) diff --git a/crates/fhe-params/src/bin/search_params.rs b/crates/fhe-params/src/bin/search_params.rs index 9d22e647c6..56366d9527 100644 --- a/crates/fhe-params/src/bin/search_params.rs +++ b/crates/fhe-params/src/bin/search_params.rs @@ -13,7 +13,8 @@ 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 e3_fhe_params::search::prime::{build_prime_items, build_prime_items_for_second}; +use e3_fhe_params::search::utils::{approx_bits_from_log2, fmt_big_summary}; use num_bigint::BigUint; #[derive(Parser, Debug, Clone)] @@ -48,6 +49,10 @@ struct Args { #[arg(long, default_value_t = 1u128)] b_chi: u128, + /// Min margin. + #[arg(long, default_value_t = 1f64)] + min_margin: f64, + /// Verbose per-candidate logging #[arg(long, default_value_t = false)] verbose: bool, @@ -68,8 +73,8 @@ fn variance_uniform_str(b: u128) -> String { } fn variance_uniform_big_str(b: &BigUint) -> String { - let b_plus_one = b + BigUint::from(1u32); - let var = (b * &b_plus_one) / 3u32; + // Variance for Uniform(-B..B): Var = B^2 / 3 + let var = (b * b) / 3u32; var.to_str_radix(10) } @@ -80,87 +85,104 @@ fn print_param_set( result: &BfvSearchResult, dist_b: &str, var_b: &str, - dist_b_chi: &str, var_chi: &str, - dist_benc: Option<(&str, &str)>, + var_enc: Option<&str>, show_common: bool, ) { - println!("\n=== {} ===", title); + println!(); + println!("=== {} ===", title); + println!(); if show_common { - println!("n (number of ciphernodes) = {}", config.n); - println!("z (number of votes) = {}", config.z); + println!(" n (ciphernodes) = {}", config.n); + println!(" z (votes) = {}", config.z); } println!( - "k (plaintext space) = {} ({} bits)", + " k (plaintext space) = {} ({} bits)", result.k_plain_eff, approx_bits_from_log2((result.k_plain_eff as f64).log2()) ); - if show_common { + println!(" λ (statistical sec) = {}", config.lambda); + println!( + " B (error bound) = {} [Dist: {}, Var = {}]", + config.b, dist_b, var_b + ); + println!( + " B_χ (secret bound) = {} [Dist: CBD, Var = {}]", + config.b_chi, var_chi + ); + println!(); + println!(" d (ring dimension) = {}", result.d); + println!( + " q (ciphertext mod) = {}", + result.q_bfv.to_str_radix(10) + ); + println!( + " |q| = {}", + fmt_big_summary(&result.q_bfv) + ); + println!( + " Δ = ⌊q/k⌋ = {}", + result.delta.to_str_radix(10) + ); + println!(" r_k(q) = q mod k = {}", result.rkq); + println!(); + if let Some(var) = var_enc { println!( - "λ (Statistical security parameter) = {}", - config.lambda + " B_Enc = {}", + result.benc_min.to_str_radix(10) ); + println!(" Var(e_1) = {}", var); println!( - "B (bound on e2) = {} [Dist: {}, Var = {}]", - config.b, dist_b, var_b + " B_fresh = {}", + result.b_fresh.to_str_radix(10) ); + println!(" B_C = {}", result.b_c.to_str_radix(10)); println!( - "B_chi (bound on sk) = {} [Dist: {}, Var = {}]", - config.b_chi, dist_b_chi, var_chi + " B_sm = {}", + result.b_sm_min.to_str_radix(10) ); - } - 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!(); + println!(" log₂(LHS) = {:.6}", result.lhs_log2); + } else { println!( - "BEnc (bound on e1) = {} [Dist: {}, Var = {}]", - result.benc_min.to_str_radix(10), - dist, - var + " B_Enc (= B) = {}", + result.benc_min.to_str_radix(10) ); - } else { println!( - "BEnc (bound on e1, taken as B) = {} [Dist: {}, Var = {}]", - config.b, dist_b, var_b + " B_fresh = {}", + result.b_fresh.to_str_radix(10) ); + println!(" B_C (= B_fresh) = {}", result.b_c.to_str_radix(10)); + println!(); + println!(" log₂(2·B_C) = {:.6}", result.lhs_log2); } - 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!(" log₂(Δ) = {:.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(", ") + " Correctness check = {} < {} ✓", + result.lhs_log2, result.rhs_log2 ); + println!(); + println!(" q_i ({} primes):", result.selected_primes.len()); + for (i, p) in result.selected_primes.iter().enumerate() { + println!(" [{}] {} ({} bits)", i + 1, p.hex, p.bitlen); + } } fn main() { let args = Args::parse(); - if args.verbose { - println!( - "== BFV parameter search (NTT-friendly primes 40..60 bits; 61-, 62- and 63-bit primes are excluded) ==" - ); - println!( - "Inputs: n={} z={} k(user)={} λ={} B={} B_chi={}", - args.n, args.z, args.k, args.lambda, args.b, args.b_chi - ); - println!("Constraint: z ≤ k(effective) and z ≤ 2^25 (≈33.5M)\n"); - } + println!("================================================================================"); + println!(" BFV Parameter Search (NTT-friendly primes)"); + println!("================================================================================"); + println!(); + println!("Inputs:"); + println!(" n (ciphernodes) = {}", args.n); + println!(" z (votes) = {}", args.z); + println!(" k (plaintext space) = {}", args.k); + println!(" λ (statistical sec) = {}", args.lambda); + println!(" B (error bound) = {}", args.b); + println!(" B_χ (secret bound) = {}", args.b_chi); + println!(); // Enforce constraints on z and k if args.z == 0 { @@ -186,9 +208,17 @@ fn main() { lambda: args.lambda, b: args.b, b_chi: args.b_chi, + min_margin: args.min_margin, verbose: args.verbose, }; + println!("Prime pools:"); + println!(" First set: {} primes", build_prime_items().len()); + println!( + " Second set: {} primes", + build_prime_items_for_second().len() + ); + // Search across all powers of two; stop at the first feasible candidate let Ok(bfv) = bfv_search(&config) else { eprintln!( @@ -205,14 +235,15 @@ fn main() { ("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 var_chi = variance_cbd_str(args.b_chi); + let var_enc = variance_uniform_big_str(&bfv.benc_min); let bfv2_opt = bfv_search_second_param(&config, &bfv); - println!("\n\n"); + println!(); + println!(); println!("================================================================================"); - println!(" FINAL BFV PARAMETER SETS"); + println!(" FINAL PARAMETER SETS"); println!("================================================================================"); print_param_set( @@ -221,9 +252,8 @@ fn main() { &bfv, dist_b, &var_b, - dist_b_chi, &var_chi, - Some((dist_benc, &var_benc)), + Some(&var_enc), true, ); @@ -234,7 +264,6 @@ fn main() { bfv2, dist_b, &var_b, - dist_b_chi, &var_chi, None, false, @@ -244,5 +273,6 @@ fn main() { println!("No second BFV parameter set found."); } - println!("\n================================================================================"); + println!(); + println!("================================================================================"); } diff --git a/crates/fhe-params/src/search/bfv.rs b/crates/fhe-params/src/search/bfv.rs index ecf54c6817..ac8f2d312d 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::{D_POW2_MAX, D_POW2_START, K_MAX}; +use crate::search::constants::K_MAX; use crate::search::errors::{BfvParamsResult, SearchError, ValidationError}; use crate::search::prime::PrimeItem; use crate::search::prime::{ @@ -22,7 +22,25 @@ use crate::search::utils::{ }; use num_bigint::BigUint; use num_traits::ToPrimitive; -use num_traits::{One, Zero}; +use num_traits::Zero; +use std::collections::HashSet; + +/// Fixed ring dimension used for both parameter sets. +const RING_DIM: u64 = 8192; + +/// First-set prime bit-size bounds. The 50-bit floor avoids the <0.2-bit +/// correctness margin of 49-bit primes; the 60-bit cap leaves 61/62-bit primes +/// for the second set (centered-RNS gap requirement). +const FIRST_MIN_PRIME_BITS: u8 = 50; +const FIRST_MAX_PRIME_BITS: u8 = 60; +const FIRST_TARGET_NUM_PRIMES: usize = 3; +const FIRST_MAX_NUM_PRIMES: usize = 6; + +/// Second-set search bounds. +const SECOND_MIN_PRIME_BITS: u8 = 50; +const SECOND_MAX_PRIME_BITS: u8 = 62; +const SECOND_TARGET_NUM_PRIMES: usize = 2; +const SECOND_MAX_NUM_PRIMES: usize = 8; /// Configuration for BFV parameter search #[derive(Debug, Clone)] @@ -39,6 +57,8 @@ pub struct BfvSearchConfig { 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, + /// Min supported margin. + pub min_margin: f64, /// Verbose output showing detailed parameter search process pub verbose: bool, } @@ -88,8 +108,6 @@ impl BfvSearchResult { /// Note: Some resulting parameter sets from this search are hardcoded as presets /// in the `presets.rs` file for production use (e.g., `BfvPreset::SecureThreshold8192`). 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 { @@ -99,66 +117,90 @@ pub fn bfv_search(bfv_search_config: &BfvSearchConfig) -> BfvParamsResult log2(q) ≤ log2(B) + (d-75)/37.5 - let log2_q_limit = log2_b + ((d as f64) - 75.0) / 37.5; + // Minimum log2(q) required for correctness (Eq1). The exact margin check is + // performed in finalize_bfv_candidate. + let min_log2_q = calculate_min_q_bits(bfv_search_config, d); - if bfv_search_config.verbose { - println!("\n[BFV] d={d} checking for log2_q_limit = {log2_q_limit:.3}"); - } + if bfv_search_config.verbose { + let log2_b = (bfv_search_config.b as f64).log2(); + let log2_q_limit = log2_b + ((d as f64) - 75.0) / 37.5; + println!("\n[BFV-1st] Fixed d={d}"); + println!(" Security limit: log2(q) <= {log2_q_limit:.1}"); + println!(" Correctness requires: log2(q) >= {min_log2_q:.1}"); + } + + // Buckets sorted DESCENDING within each bit-length (largest prime first), so + // taking the first `num_primes` of a bucket maximises q for that prime size. + let by_bits = group_by_bits_desc(&prime_items); + + // Try the fewest primes first, then the smallest prime bit-size that meets + // the correctness bound. This mirrors the reference bucket-scan search. + for num_primes in FIRST_TARGET_NUM_PRIMES..=FIRST_MAX_NUM_PRIMES { + for bb in FIRST_MIN_PRIME_BITS..=FIRST_MAX_PRIME_BITS { + let bucket = match by_bits.get(&bb) { + Some(b) if b.len() >= num_primes => b, + _ => continue, + }; + + // Take the largest `num_primes` primes in this bucket to maximise q. + let sel: Vec = bucket.iter().take(num_primes).cloned().collect(); + let q = product(sel.iter().map(|pi| pi.value.clone())); + if log2_big(&q) < min_log2_q { + continue; + } - // 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})." - ); + if let Some(res) = finalize_bfv_candidate(bfv_search_config, d, sel) { + return Ok(res); } - 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(", ") - ); - } + Err(SearchError::NoFeasibleParameters.into()) +} - if let Some(refined) = - refine_from_initial(bfv_search_config, d, &prime_items, initial_sel) - { - return Ok(refined); - } +/// Minimum log2(q) needed for correctness (Eq1), ignoring r_k(q). +/// +/// finalize_bfv_candidate performs the exact check (including r_k(q) and the +/// margin); this is only used to prune prime selections that are too small. +fn calculate_min_q_bits(bfv_search_config: &BfvSearchConfig, d: u64) -> f64 { + let two_pow_lambda = big_shift_pow2(bfv_search_config.lambda); - // If refinement fails unexpectedly, return the initial feasible result - return Ok(initial_res); - } + 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; - if bfv_search_config.verbose { - println!( - "[BFV] d={} : first (largest-q) candidate failed Eq1 — increasing d…", - d - ); - } + 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_b_chi_n + &term_d_b_b_chi_n; - d <<= 1; - } + let b_c = BigUint::from(bfv_search_config.z) * &b_fresh; + let b_sm_min = &b_c * &two_pow_lambda; - Err(SearchError::NoFeasibleParameters.into()) + let lhs = (&b_c + BigUint::from(bfv_search_config.n) * &b_sm_min) << 1; + let lhs_log2 = log2_big(&lhs); + + let log2_k = (bfv_search_config.k.max(bfv_search_config.z) as f64).log2(); + lhs_log2 + log2_k +} + +/// Group primes by bit-length, sorting each bucket descending by value. +fn group_by_bits_desc(prime_items: &[PrimeItem]) -> BTreeMap> { + let mut by_bits: BTreeMap> = BTreeMap::new(); + for p in prime_items { + 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)); + } + by_bits } /// Validate a candidate parameter set and compute all noise bounds. @@ -211,6 +253,7 @@ pub fn finalize_bfv_candidate( 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 margin = rhs_log2 - lhs_log2; let benc_bits = approx_bits_from_log2(log2_big(&benc_min)); let bfresh_bits = approx_bits_from_log2(log2_big(&b_fresh)); @@ -248,7 +291,7 @@ pub fn finalize_bfv_candidate( ); } - if lhs >= delta { + if lhs >= delta || margin < bfv_search_config.min_margin { return None; } @@ -408,60 +451,86 @@ pub fn construct_qi_for_target_bits( /// Search for a second BFV parameter set with plaintext space derived from the first set. /// -/// The plaintext modulus k is set to the next power of 2 above the maximum qi bit length -/// from the first parameter set. Uses a separate prime pool that includes 62-bit primes. +/// The plaintext modulus k is set to the actual maximum qi value of the first set. +/// fhe.rs centered RNS requires every second-set qi > 2*k (the "large gap" rule), +/// and second-set primes must be disjoint from the first set. The smallest valid +/// primes (fewest and smallest) that satisfy correctness are chosen. Uses a +/// separate prime pool that includes 62-bit primes. pub fn bfv_search_second_param( bfv_search_config: &BfvSearchConfig, first: &BfvSearchResult, ) -> Option { - // Plaintext space for second set: next power of 2 above max qi of first set. - let max_qi_bits_first: u64 = first + let d = RING_DIM; + + // Plaintext space for second set: k = max qi of first set (actual value). + let max_qi_first: BigUint = first .selected_primes .iter() - .map(|pi| pi.value.bits()) + .map(|pi| pi.value.clone()) .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) - }; + .expect("first set has at least one prime"); + let k_second: u128 = max_qi_first.to_u128().unwrap_or(u128::MAX); + + // Centered-RNS gap rule: qi > 2*k. + let min_qi_second = &max_qi_first << 1; 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 + "\n[BFV-2nd] Fixed d={d}, k = max_qi_first = {k_second} ({:.2} bits)", + log2_big(&max_qi_first) + ); + println!( + " Minimum qi required: {:.2} bits (fhe.rs centered RNS: qi > 2*k)", + log2_big(&min_qi_second) ); } - let log2_b = (bfv_search_config.b as f64).log2(); - // Start from the dimension of the first set - let mut d: u64 = first.d; + let prime_items = build_prime_items_for_second(); - 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; + // Exclude primes already used by the first set. + let first_set_primes: HashSet = first + .selected_primes + .iter() + .map(|p| p.hex.clone()) + .collect(); - if bfv_search_config.verbose { - println!("\n[BFV-2nd] d={d} checking for log2_q_limit = {log2_q_limit:.3})."); - } + // Buckets sorted ASCENDING within each bit-length (smallest prime first), so + // taking the first valid `num_primes` minimises prime size. + let mut by_bits: BTreeMap> = BTreeMap::new(); + for p in &prime_items { + by_bits.entry(p.bitlen).or_default().push(p.clone()); + } + for v in by_bits.values_mut() { + v.sort_by(|a, b| a.value.cmp(&b.value)); + } + + // Fewest primes first, then smallest prime bit-size. + for num_primes in SECOND_TARGET_NUM_PRIMES..=SECOND_MAX_NUM_PRIMES { + for bb in SECOND_MIN_PRIME_BITS..=SECOND_MAX_PRIME_BITS { + let bucket = match by_bits.get(&bb) { + Some(b) => b, + None => continue, + }; + + // Valid primes: satisfy the gap rule and not used by the first set. + let valid: Vec = bucket + .iter() + .filter(|pi| pi.value > min_qi_second && !first_set_primes.contains(&pi.hex)) + .cloned() + .collect(); + + if valid.len() < num_primes { + continue; + } - // 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); + // Take the smallest valid primes to minimise prime size. + let sel: Vec = valid.into_iter().take(num_primes).collect(); + if let Some(res) = finalize_second_param(bfv_search_config, d, sel, k_second) { + return Some(res); + } } - d <<= 1; } + None } @@ -587,17 +656,18 @@ pub fn construct_qi_second_param( } } + // Prefer the smallest qualifying primes (minimise prime size), matching the + // old behaviour of taking the smallest valid primes in the smallest bucket. let mut best: Option<(f64, Vec)> = None; for sel in tried { let q = product(sel.iter().map(|pi| pi.value.clone())); let qbits = log2_big(&q); - let diff = (qbits - target_f).abs(); - if let Some((best_diff, _)) = &best { - if diff < *best_diff { - best = Some((diff, sel)); + if let Some((best_qbits, _)) = &best { + if qbits < *best_qbits { + best = Some((qbits, sel)); } } else { - best = Some((diff, sel)); + best = Some((qbits, sel)); } } if let Some((_, sel)) = best { @@ -616,32 +686,17 @@ pub fn finalize_second_param( 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} + // fhe.rs centered RNS requires qi > 2*k to avoid sign-flip errors in the + // centered representation scaler (the "large gap" rule). 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) - }; + let min_qi_threshold = &k_big << 1; // 2 * k 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^{}).", + "[BFV-2nd] d={d} candidate rejected: qi {} does not satisfy the large-gap rule qi > 2*k (k={k_plain}).", pi.value, - k_bits + 1 ); } return None; @@ -665,7 +720,8 @@ pub fn finalize_second_param( let lhs_log2 = log2_big(&lhs); let rhs_log2 = log2_big(&delta); - let ok = lhs < delta; + let margin = rhs_log2 - lhs_log2; + let ok = lhs < delta && margin >= bfv_search_config.min_margin; if !ok { return None; } @@ -693,7 +749,10 @@ pub fn finalize_second_param( b_fresh.to_str_radix(10) ); println!(" B_C = B_fresh = {}", b_c.to_str_radix(10)); - println!(" log2(2*B_C)≈{:.3} log2(Δ)≈{:.3}", lhs_log2, rhs_log2); + println!( + " log2(2*B_C)≈{:.3} log2(Δ)≈{:.3} margin={:.3} bits", + lhs_log2, rhs_log2, margin + ); println!( " 2*B_C {} Δ => {}", @@ -726,6 +785,7 @@ mod tests { use crate::search::prime::build_prime_items; use crate::search::prime::build_prime_items_for_second; use num_bigint::BigUint; + use num_traits::One; fn create_test_config() -> BfvSearchConfig { BfvSearchConfig { @@ -735,6 +795,7 @@ mod tests { lambda: 80, b: 20, b_chi: 1, + min_margin: 1.0, verbose: false, } } diff --git a/crates/fhe-params/src/search/constants.rs b/crates/fhe-params/src/search/constants.rs index 565a11cdc7..d68e6e00e2 100644 --- a/crates/fhe-params/src/search/constants.rs +++ b/crates/fhe-params/src/search/constants.rs @@ -5,269 +5,461 @@ // 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])] = &[ +pub const NTT_PRIMES_BY_BITS: &[(u8, &[&str])] = &[ ( 40u8, - [ + &[ + "0x0000008000020001", "0x00000080004a0001", "0x0000008000fa0001", "0x0000008001ae0001", "0x0000008001b20001", "0x0000008001ee0001", "0x0000008001f60001", + "0x0000008002220001", + "0x0000008002420001", + "0x00000080028a0001", + "0x00000080029a0001", + "0x00000080031e0001", ], ), ( 41u8, - [ + &[ "0x00000100003e0001", "0x0000010000960001", "0x0000010000b60001", "0x0000010000ce0001", "0x0000010000de0001", "0x00000100010a0001", + "0x0000010001120001", + "0x0000010001620001", + "0x00000100019a0001", + "0x00000100021a0001", + "0x0000010002520001", + "0x0000010002620001", ], ), ( 42u8, - [ + &[ "0x0000020000560001", "0x0000020000820001", "0x0000020000920001", "0x0000020000aa0001", "0x0000020001360001", "0x00000200015a0001", + "0x00000200017a0001", + "0x0000020001e60001", + "0x0000020002260001", + "0x0000020002a60001", + "0x0000020002c20001", + "0x0000020003160001", ], ), ( 43u8, - [ + &[ "0x0000040000560001", "0x00000400007a0001", "0x00000400008a0001", "0x0000040000fe0001", "0x0000040001760001", "0x00000400017a0001", + "0x00000400019a0001", + "0x0000040001b20001", + "0x0000040001e20001", + "0x0000040002360001", + "0x0000040002a60001", + "0x0000040002fe0001", ], ), ( 44u8, - [ + &[ "0x00000800009a0001", "0x0000080000ee0001", "0x0000080001060001", "0x0000080001160001", "0x00000800012e0001", "0x0000080001420001", + "0x0000080001720001", + "0x0000080001a60001", + "0x0000080001c20001", + "0x00000800020e0001", + "0x0000080002360001", + "0x00000800025e0001", ], ), ( 45u8, - [ + &[ "0x0000100000020001", "0x00001000001a0001", "0x00001000003e0001", "0x00001000006e0001", "0x0000100000ba0001", "0x0000100000ce0001", + "0x0000100000ea0001", + "0x0000100001220001", + "0x0000100001560001", + "0x0000100001860001", + "0x0000100001a20001", + "0x0000100001da0001", ], ), ( 46u8, - [ + &[ "0x00002000000a0001", "0x00002000000e0001", "0x0000200000620001", "0x00002000006a0001", "0x0000200000860001", "0x0000200000a60001", + "0x0000200000c20001", + "0x0000200000ee0001", + "0x0000200001320001", + "0x0000200001860001", + "0x0000200001a20001", + "0x0000200001c60001", ], ), ( 47u8, - [ + &[ "0x0000400000060001", "0x0000400000420001", "0x0000400000660001", "0x0000400000920001", "0x00004000009e0001", "0x0000400000b60001", + "0x0000400000d20001", + "0x0000400000f20001", + "0x0000400001260001", + "0x0000400001620001", + "0x00000400001920001", + "0x0000400001c20001", ], ), ( 48u8, - [ + &[ "0x0000800000020001", "0x0000800000520001", "0x0000800000aa0001", "0x0000800001360001", "0x0000800001420001", "0x0000800002060001", + "0x0000800002120001", + "0x00008000023e0001", + "0x0000800002620001", + "0x0000800002a60001", + "0x0000800002d60001", + "0x0000800003020001", ], ), ( 49u8, - [ + &[ "0x00010000001a0001", "0x00010000001e0001", "0x0001000000320001", + "0x0001000000380001", + "0x00010000004d0001", + "0x0001000000500001", + "0x0001000000570001", + "0x0001000000690001", + "0x00010000006b0001", "0x0001000000720001", "0x0001000000ba0001", - "0x00010000011a0001", + "0x0001000000c00001", ], ), ( 50u8, - [ + &[ + "0x00020000000b0001", "0x00020000001a0001", + "0x00020000003b0001", "0x00020000005e0001", + "0x00020000006d0001", "0x0002000000860001", + "0x00020000008b0001", + "0x0002000000b00001", "0x0002000000ce0001", + "0x0002000001090001", "0x00020000013a0001", - "0x00020000015a0001", + "0x00020000013c0001", ], ), ( 51u8, - [ + &[ "0x0004000000120001", + "0x00040000001b0001", + "0x0004000000270001", + "0x0004000000350001", "0x0004000000420001", + "0x0004000000450001", "0x0004000000660001", + "0x0004000000750001", "0x00040000007e0001", + "0x0004000000800001", "0x00040000008a0001", - "0x0004000000de0001", + "0x00040000009f0001", ], ), ( 52u8, - [ + &[ + "0x0008000000110001", + "0x0008000000130001", + "0x00080000001c0001", + "0x00080000002c0001", + "0x00080000004d0001", + "0x00080000004f0001", + "0x0008000000500001", + "0x0008000000590001", "0x0008000000820001", - "0x0008000001120001", - "0x00080000012a0001", - "0x0008000001360001", - "0x00080000016a0001", - "0x00080000018a0001", + "0x0008000000940001", + "0x0008000000a30001", + "0x0008000000bb0001", ], ), ( 53u8, - [ + &[ "0x0010000000060001", + "0x00100000000f0001", + "0x0010000000150001", + "0x0010000000180001", + "0x0010000000200001", "0x00100000003e0001", + "0x0010000000500001", + "0x0010000000650001", "0x00100000006e0001", + "0x00100000006f0001", "0x00100000007e0001", "0x0010000000960001", - "0x00100000010e0001", ], ), ( 54u8, - [ + &[ "0x00200000000e0001", + "0x0020000000140001", + "0x0020000000170001", + "0x0020000000280001", + "0x0020000000640001", + "0x00200000007c0001", "0x0020000000820001", - "0x0020000001360001", - "0x0020000001460001", - "0x0020000001520001", - "0x00200000015e0001", + "0x0020000000970001", + "0x0020000000b30001", + "0x0020000000bf0001", + "0x0020000000c10001", + "0x0020000000c70001", ], ), ( 55u8, - [ + &[ "0x0040000000120001", + "0x00400000001d0001", + "0x00400000002c0001", + "0x0040000000480001", + "0x0040000000540001", + "0x00400000005c0001", + "0x00400000006c0001", + "0x00400000007b0001", + "0x0040000000890001", + "0x0040000000b00001", + "0x0040000000e40001", "0x0040000000f60001", - "0x00400000010a0001", - "0x00400000011a0001", - "0x00400000017a0001", - "0x0040000001ca0001", ], ), ( 56u8, - [ + &[ + "0x0080000000080001", + "0x0080000000130001", + "0x0080000000190001", + "0x00800000001d0001", + "0x0080000000440001", + "0x0080000000490001", + "0x0080000000500001", "0x00800000005e0001", - "0x0080000000ca0001", - "0x0080000001f60001", - "0x0080000002120001", - "0x00800000021a0001", - "0x00800000022a0001", + "0x0080000000730001", + "0x0080000000770001", + "0x0080000000850001", + "0x00800000009d0001", ], ), ( 57u8, - [ + &[ "0x0100000000060001", "0x01000000002a0001", - "0x0100000001260001", - "0x01000000016a0001", - "0x0100000001760001", - "0x0100000002a20001", + "0x0100000000450001", + "0x0100000000480001", + "0x01000000005f0001", + "0x0100000000650001", + "0x0100000000980001", + "0x0100000000ab0001", + "0x0100000000bf0001", + "0x0100000000cf0001", + "0x0100000000dd0001", + "0x0100000000ed0001", ], ), ( 58u8, - [ + &[ + "0x02000000002b0001", "0x02000000003a0001", + "0x02000000005b0001", + "0x0200000000640001", + "0x02000000006d0001", + "0x0200000000910001", + "0x0200000000b90001", + "0x0200000000ef0001", + "0x0200000000f80001", + "0x0200000001210001", "0x0200000001460001", "0x02000000015a0001", - "0x02000000015e0001", - "0x0200000001b20001", - "0x0200000001ee0001", ], ), ( 59u8, - [ + &[ + "0x0400000000270001", + "0x0400000000350001", "0x0400000000360001", + "0x04000000004d0001", + "0x0400000000570001", "0x0400000000660001", "0x04000000008a0001", "0x0400000000920001", - "0x0400000000ea0001", - "0x0400000001460001", + "0x0400000000980001", + "0x0400000000990001", + "0x0400000000a40001", + "0x0400000000c00001", ], ), ( 60u8, - [ - "0x08000000004a0001", - "0x0800000000ee0001", - "0x0800000001160001", - "0x08000000018e0001", - "0x08000000025a0001", - "0x08000000029e0001", + &[ + "0x0800000000004001", + "0x0800000000044001", + "0x0800000000124001", + "0x080000000019c001", + "0x080000000020c001", + "0x0800000000244001", + "0x08000000002d4001", + "0x08000000003dc001", + "0x0800000000404001", + "0x0800000000514001", + "0x080000000061c001", + "0x0800000000634001", + "0x08000000006c4001", + "0x080000000082c001", + "0x080000000083c001", + "0x0800000000854001", + "0x080000000092c001", + "0x08000000009bc001", + "0x0800000000a7c001", + "0x0800000000bcc001", + "0x0800000000c94001", + "0x0800000000cbc001", + "0x0800000000d54001", + "0x0800000000db4001", ], ), ( 61u8, - [ - "0x10000000006e0001", - "0x1000000000860001", - "0x1000000000ce0001", - "0x10000000011a0001", - "0x10000000019a0001", - "0x1000000001be0001", + &[ + "0x1000000000024001", + "0x1000000000054001", + "0x100000000005c001", + "0x10000000000a4001", + "0x100000000014c001", + "0x10000000002dc001", + "0x1000000000344001", + "0x100000000035c001", + "0x1000000000414001", + "0x10000000004bc001", + "0x10000000005cc001", + "0x10000000006ec001", + "0x1000000000794001", + "0x10000000007bc001", + "0x100000000090c001", + "0x100000000096c001", + "0x1000000000a14001", + "0x1000000000aec001", + "0x1000000000c2c001", + "0x1000000000c6c001", + "0x1000000000ccc001", + "0x1000000000d74001", + "0x1000000000dac001", + "0x10000000010dc001", ], ), ( 62u8, - [ - "0x2000000000460001", - "0x2000000000620001", - "0x2000000000da0001", - "0x2000000001120001", - "0x2000000001960001", - "0x2000000001be0001", - ], - ), - ( - 63u8, - [ - "0x40000000009e0001", - "0x40000000010a0001", - "0x40000000016a0001", - "0x4000000001ca0001", - "0x40000000020a0001", - "0x4000000002820001", + &[ + // Lower range (log2 ≈ 61.0) + "0x2000000000104001", + "0x200000000013c001", + "0x20000000001f4001", + "0x20000000002e4001", + "0x2000000000454001", + "0x20000000004c4001", + "0x2000000000524001", + "0x200000000052c001", + "0x200000000071c001", + "0x200000000080c001", + "0x200000000089c001", + "0x20000000008b4001", + // Mid range (log2 ≈ 61.2-61.65) - verified NTT-friendly primes + "0x24c2230d75cdc001", // log2 = 61.20 + "0x260dfc1463740001", // log2 = 61.25 + "0x276588f7ff08c001", // log2 = 61.30 + "0x28c9335e63610001", // log2 = 61.35 + "0x2a3968a772a88001", // log2 = 61.40 + "0x2bb69a0e78f64001", // log2 = 61.45 + "0x2d413cccfe794001", // log2 = 61.50 + "0x2ed9ca3ed4188001", // log2 = 61.55 + "0x3080c00765628001", // log2 = 61.60 + "0x3236a0385b67c001", // log2 = 61.65 + // Upper range (log2 ≈ 61.7-62.0) - for second set + "0x3460000000000001", + "0x3630000000000001", + "0x37f0000000000001", + "0x3810000000000001", + "0x3820000000000001", + "0x3960000000000001", + "0x39ae166b9acc8001", // log2 = 61.85 + "0x3a00000000000001", + "0x3a90000000000001", + "0x3ae0000000000001", + "0x3ae45abe677d4001", // log2 = 61.88 + "0x3bb6d0022f594001", // log2 = 61.90 + "0x3c8c355f344d0001", // log2 = 61.92 + "0x3d6495552e9c0001", // log2 = 61.94 + "0x3e10000000000001", + "0x3e3ffa895b7d4001", // log2 = 61.96 + "0x3ea0000000000001", + "0x3f18000000000001", + "0x3f1e6fc702dc8001", // log2 = 61.98 + // Near-top range (log2 ≈ 61.9999) - generated for second parameter set + "0x3fffffffffff0001", + "0x3ffffffffffe8001", + "0x3ffffffffff1c001", + "0x3fffffffffeec001", + "0x3fffffffffe80001", + "0x3fffffffffd9c001", + "0x3fffffffffd78001", + "0x3fffffffffd2c001", ], ), ]; diff --git a/crates/fhe-params/src/search/prime.rs b/crates/fhe-params/src/search/prime.rs index 710109ac35..f1a9cf84e7 100644 --- a/crates/fhe-params/src/search/prime.rs +++ b/crates/fhe-params/src/search/prime.rs @@ -34,7 +34,7 @@ fn build_prime_items_with_filter(filter: BitFilter) -> Vec { if filter(*bits) { continue; } - for &phex in arr { + for &phex in arr.into_iter() { let v = parse_hex_big(phex); vec.push(PrimeItem { bitlen: *bits, @@ -47,10 +47,12 @@ fn build_prime_items_with_filter(filter: BitFilter) -> Vec { vec } -/// Build a flat list of all primes with precomputed log2 and hex strings. -/// Excludes 61, 62, and 63-bit primes. +/// Build the prime pool for the first parameter set. +/// Restricted to 50..=60 bit primes: the floor of 50 bits avoids the <0.2-bit +/// correctness margin of 49-bit primes for worst-case inputs, and 61/62/63-bit +/// primes are reserved for the second set (centered-RNS gap requirement). pub fn build_prime_items() -> Vec { - build_prime_items_with_filter(|bits| bits == 63 || bits == 62 || bits == 61) + build_prime_items_with_filter(|bits| bits < 50 || bits == 61 || bits == 62 || bits == 63) } /// Build prime items for second parameter set (includes 62-bit primes, excludes 61 and 63-bit) diff --git a/crates/fhe-params/src/search/utils.rs b/crates/fhe-params/src/search/utils.rs index eb960c781b..ba6f249fd1 100644 --- a/crates/fhe-params/src/search/utils.rs +++ b/crates/fhe-params/src/search/utils.rs @@ -28,19 +28,18 @@ pub fn log2_big(x: &BigUint) -> f64 { 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 + // Take up to 8 bytes for f64 precision (53 mantissa bits) 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 + + // log2(x) = log2(top * 2^((bytes.len() - take) * 8)) + // = log2(top) + (bytes.len() - take) * 8 + let shift = ((bytes.len() - take) * 8) as f64; + (top as f64).log2() + shift } pub fn approx_bits_from_log2(log2x: f64) -> u64 { From 61c87735264612a5429c0ad96abd3963cf675f1f Mon Sep 17 00:00:00 2001 From: 0xjei Date: Mon, 15 Jun 2026 11:59:15 +0200 Subject: [PATCH 2/6] improve printing --- crates/fhe-params/src/bin/search_params.rs | 29 ++- crates/fhe-params/src/search/bfv.rs | 218 ++++++++++++--------- 2 files changed, 154 insertions(+), 93 deletions(-) diff --git a/crates/fhe-params/src/bin/search_params.rs b/crates/fhe-params/src/bin/search_params.rs index 56366d9527..33c5c74550 100644 --- a/crates/fhe-params/src/bin/search_params.rs +++ b/crates/fhe-params/src/bin/search_params.rs @@ -219,11 +219,20 @@ fn main() { build_prime_items_for_second().len() ); - // 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}}." + // Search for first parameter set + if args.verbose { + println!(); + println!( + "================================================================================" ); + println!(" Searching First Parameter Set"); + println!( + "================================================================================" + ); + } + + let Ok(bfv) = bfv_search(&config) else { + eprintln!("\nERROR: No feasible first parameter set found."); eprintln!("Try increasing d, or reducing n, z, λ, or B."); std::process::exit(1); }; @@ -238,6 +247,18 @@ fn main() { let var_chi = variance_cbd_str(args.b_chi); let var_enc = variance_uniform_big_str(&bfv.benc_min); + // Search for second parameter set + if args.verbose { + println!(); + println!( + "================================================================================" + ); + println!(" Searching Second Parameter Set"); + println!( + "================================================================================" + ); + } + let bfv2_opt = bfv_search_second_param(&config, &bfv); println!(); diff --git a/crates/fhe-params/src/search/bfv.rs b/crates/fhe-params/src/search/bfv.rs index ac8f2d312d..6c38f1d6be 100644 --- a/crates/fhe-params/src/search/bfv.rs +++ b/crates/fhe-params/src/search/bfv.rs @@ -17,9 +17,7 @@ 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::utils::{approx_bits_from_log2, big_shift_pow2, log2_big, product}; use num_bigint::BigUint; use num_traits::ToPrimitive; use num_traits::Zero; @@ -117,6 +115,7 @@ pub fn bfv_search(bfv_search_config: &BfvSearchConfig) -> BfvParamsResult BfvParamsResult BfvParamsResult= num_primes => b, - _ => continue, + Some(b) => b, + None => continue, }; + if bucket.len() < num_primes { + if verbose { + println!( + " {} × {}-bit: only {} primes available (need {})", + num_primes, + bb, + bucket.len(), + num_primes + ); + } + continue; + } + // Take the largest `num_primes` primes in this bucket to maximise q. let sel: Vec = bucket.iter().take(num_primes).cloned().collect(); let q = product(sel.iter().map(|pi| pi.value.clone())); - if log2_big(&q) < min_log2_q { + let q_bits = log2_big(&q); + let max_qi_log2 = sel.iter().map(|p| p.log2).fold(0.0_f64, f64::max); + + if q_bits < min_log2_q { + if verbose { + println!( + " {} × {}-bit: log2(q)={:.2} < {:.1} needed, skipping", + num_primes, bb, q_bits, min_log2_q + ); + } continue; } if let Some(res) = finalize_bfv_candidate(bfv_search_config, d, sel) { + if verbose { + println!( + "\n✓ Found first set: {} × {}-bit primes, log2(q)={:.2}, max_qi={:.2} bits", + num_primes, bb, q_bits, max_qi_log2 + ); + } return Ok(res); + } else if verbose { + println!( + " {} × {}-bit: log2(q)={:.2} ❌ fails correctness or margin < {:.1} bits", + num_primes, bb, q_bits, bfv_search_config.min_margin + ); } } } + if verbose { + eprintln!("\nERROR: No valid first parameter set found"); + } Err(SearchError::NoFeasibleParameters.into()) } @@ -255,42 +311,6 @@ pub fn finalize_bfv_candidate( let rhs_log2 = log2_big(&delta); let margin = rhs_log2 - lhs_log2; - 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 || margin < bfv_search_config.min_margin { return None; } @@ -471,10 +491,14 @@ pub fn bfv_search_second_param( .expect("first set has at least one prime"); let k_second: u128 = max_qi_first.to_u128().unwrap_or(u128::MAX); + let verbose = bfv_search_config.verbose; + // Centered-RNS gap rule: qi > 2*k. let min_qi_second = &max_qi_first << 1; - if bfv_search_config.verbose { + if verbose { + let log2_b = (bfv_search_config.b as f64).log2(); + let log2_q_limit = log2_b + ((d as f64) - 75.0) / 37.5; println!( "\n[BFV-2nd] Fixed d={d}, k = max_qi_first = {k_second} ({:.2} bits)", log2_big(&max_qi_first) @@ -483,6 +507,7 @@ pub fn bfv_search_second_param( " Minimum qi required: {:.2} bits (fhe.rs centered RNS: qi > 2*k)", log2_big(&min_qi_second) ); + println!(" Security limit: log2(q) <= {log2_q_limit:.1}"); } let prime_items = build_prime_items_for_second(); @@ -504,8 +529,35 @@ pub fn bfv_search_second_param( v.sort_by(|a, b| a.value.cmp(&b.value)); } + // Show available primes per bit-length (gap rule applied, first-set excluded). + if verbose { + for bb in SECOND_MIN_PRIME_BITS..=SECOND_MAX_PRIME_BITS { + if let Some(bucket) = by_bits.get(&bb) { + let available: Vec<&PrimeItem> = bucket + .iter() + .filter(|p| p.value > min_qi_second && !first_set_primes.contains(&p.hex)) + .collect(); + if !available.is_empty() { + let min_log2 = available.first().map(|p| p.log2).unwrap_or(0.0); + let max_log2 = available.last().map(|p| p.log2).unwrap_or(0.0); + println!( + " {}-bit bucket: {} primes with qi > 2k, log2 range [{:.2}, {:.2}]", + bb, + available.len(), + min_log2, + max_log2 + ); + } + } + } + } + // Fewest primes first, then smallest prime bit-size. for num_primes in SECOND_TARGET_NUM_PRIMES..=SECOND_MAX_NUM_PRIMES { + if verbose { + println!("\n === Trying {num_primes} primes ==="); + } + for bb in SECOND_MIN_PRIME_BITS..=SECOND_MAX_PRIME_BITS { let bucket = match by_bits.get(&bb) { Some(b) => b, @@ -520,17 +572,49 @@ pub fn bfv_search_second_param( .collect(); if valid.len() < num_primes { + if verbose { + println!( + " {} × {}-bit: only {} valid primes with large gap (need {})", + num_primes, + bb, + valid.len(), + num_primes + ); + } continue; } // Take the smallest valid primes to minimise prime size. let sel: Vec = valid.into_iter().take(num_primes).collect(); + let q = product(sel.iter().map(|pi| pi.value.clone())); + let q_bits = log2_big(&q); + let min_selected = sel.iter().map(|p| &p.value).min().unwrap(); + let gap_bits = log2_big(&(min_selected - &max_qi_first)); + + if verbose { + println!( + " {} × {}-bit: log2(q) = {:.2}, min gap = 2^{:.1}", + num_primes, bb, q_bits, gap_bits + ); + } + if let Some(res) = finalize_second_param(bfv_search_config, d, sel, k_second) { + if verbose { + println!( + "\n✓ Found second set: {} × {}-bit, log2(q)={:.2}, gap=2^{:.1}", + num_primes, bb, q_bits, gap_bits + ); + } return Some(res); + } else if verbose { + println!(" ❌ Fails correctness check"); } } } + if verbose { + eprintln!("\nWARNING: No valid second parameter set found"); + } None } @@ -693,12 +777,6 @@ pub fn finalize_second_param( for pi in &chosen { if pi.value <= min_qi_threshold { - if bfv_search_config.verbose { - println!( - "[BFV-2nd] d={d} candidate rejected: qi {} does not satisfy the large-gap rule qi > 2*k (k={k_plain}).", - pi.value, - ); - } return None; } } @@ -721,48 +799,10 @@ pub fn finalize_second_param( let rhs_log2 = log2_big(&delta); let margin = rhs_log2 - lhs_log2; - let ok = lhs < delta && margin >= bfv_search_config.min_margin; - if !ok { + if lhs >= delta || margin < bfv_search_config.min_margin { return None; } - if bfv_search_config.verbose { - println!("\n[BFV-2nd] d={d} candidate:"); - println!( - " CRT primes ({}): {}", - chosen.len(), - chosen - .iter() - .map(|p| p.hex.clone()) - .collect::>() - .join(", ") - ); - println!(" |q_BFV| {}", fmt_big_summary(&q_bfv)); - println!( - " k(plaintext_space)={} Δ={}", - k_plain, - delta.to_str_radix(10) - ); - println!( - " BEnc(taken as B) = {} B_fresh = {}", - bfv_search_config.b, - b_fresh.to_str_radix(10) - ); - println!(" B_C = B_fresh = {}", b_c.to_str_radix(10)); - println!( - " log2(2*B_C)≈{:.3} log2(Δ)≈{:.3} margin={:.3} bits", - lhs_log2, rhs_log2, margin - ); - - println!( - " 2*B_C {} Δ => {}", - if ok { "<" } else { "≥" }, - if ok { "PASS ✅" } else { "fail ❌" } - ); - - println!("\n*** BFV-2nd FEASIBLE at d={} ***", d); - } - Some(BfvSearchResult { d, k_plain_eff: k_plain, From d38ab2eb477839d07d8961bff8a6791b07ef15da Mon Sep 17 00:00:00 2001 From: 0xjei Date: Mon, 15 Jun 2026 13:35:40 +0200 Subject: [PATCH 3/6] include changes from coderabbit --- crates/fhe-params/src/bin/search_params.rs | 7 +- crates/fhe-params/src/search/bfv.rs | 198 +++++++++++++-------- crates/fhe-params/src/search/constants.rs | 183 +++++++++++++------ 3 files changed, 258 insertions(+), 130 deletions(-) diff --git a/crates/fhe-params/src/bin/search_params.rs b/crates/fhe-params/src/bin/search_params.rs index 33c5c74550..18ec2bd2ce 100644 --- a/crates/fhe-params/src/bin/search_params.rs +++ b/crates/fhe-params/src/bin/search_params.rs @@ -201,6 +201,11 @@ fn main() { std::process::exit(1); } + if !args.min_margin.is_finite() || args.min_margin < 0.0 { + eprintln!("ERROR: --min-margin must be a finite, non-negative number."); + std::process::exit(1); + } + let config = BfvSearchConfig { n: args.n, z: args.z, @@ -233,7 +238,7 @@ fn main() { let Ok(bfv) = bfv_search(&config) else { eprintln!("\nERROR: No feasible first parameter set found."); - eprintln!("Try increasing d, or reducing n, z, λ, or B."); + eprintln!("Try reducing n, z, k, λ, B, B_χ or min_margin."); std::process::exit(1); }; diff --git a/crates/fhe-params/src/search/bfv.rs b/crates/fhe-params/src/search/bfv.rs index 6c38f1d6be..9cea6b5fdd 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::K_MAX; +use crate::search::constants::{D_POW2_MAX, K_MAX}; use crate::search::errors::{BfvParamsResult, SearchError, ValidationError}; use crate::search::prime::PrimeItem; use crate::search::prime::{ @@ -117,25 +117,13 @@ pub fn bfv_search(bfv_search_config: &BfvSearchConfig) -> BfvParamsResult= {min_log2_q:.1}"); - } + let log2_b = (bfv_search_config.b as f64).log2(); // Buckets sorted DESCENDING within each bit-length (largest prime first), so // taking the first `num_primes` of a bucket maximises q for that prime size. let by_bits = group_by_bits_desc(&prime_items); - // Show available buckets. + // Show available buckets (pool is independent of d, so print once). if verbose { for bb in FIRST_MIN_PRIME_BITS..=FIRST_MAX_PRIME_BITS { if let Some(bucket) = by_bits.get(&bb) { @@ -152,63 +140,97 @@ pub fn bfv_search(bfv_search_config: &BfvSearchConfig) -> BfvParamsResult= {min_log2_q:.1}"); } - for bb in FIRST_MIN_PRIME_BITS..=FIRST_MAX_PRIME_BITS { - let bucket = match by_bits.get(&bb) { - Some(b) => b, - None => continue, - }; + // Try the fewest primes first, then the smallest prime bit-size that + // meets the correctness bound. This mirrors the reference bucket scan. + for num_primes in FIRST_TARGET_NUM_PRIMES..=FIRST_MAX_NUM_PRIMES { + if verbose { + println!("\n === Trying {num_primes} primes ==="); + } - if bucket.len() < num_primes { - if verbose { - println!( - " {} × {}-bit: only {} primes available (need {})", - num_primes, - bb, - bucket.len(), - num_primes - ); + for bb in FIRST_MIN_PRIME_BITS..=FIRST_MAX_PRIME_BITS { + let bucket = match by_bits.get(&bb) { + Some(b) => b, + None => continue, + }; + + if bucket.len() < num_primes { + if verbose { + println!( + " {} × {}-bit: only {} primes available (need {})", + num_primes, + bb, + bucket.len(), + num_primes + ); + } + continue; } - continue; - } - // Take the largest `num_primes` primes in this bucket to maximise q. - let sel: Vec = bucket.iter().take(num_primes).cloned().collect(); - let q = product(sel.iter().map(|pi| pi.value.clone())); - let q_bits = log2_big(&q); - let max_qi_log2 = sel.iter().map(|p| p.log2).fold(0.0_f64, f64::max); + // Take the largest `num_primes` primes in this bucket to maximise q. + let sel: Vec = bucket.iter().take(num_primes).cloned().collect(); + let q = product(sel.iter().map(|pi| pi.value.clone())); + let q_bits = log2_big(&q); + let max_qi_log2 = sel.iter().map(|p| p.log2).fold(0.0_f64, f64::max); + + if q_bits < min_log2_q { + if verbose { + println!( + " {} × {}-bit: log2(q)={:.2} < {:.1} needed, skipping", + num_primes, bb, q_bits, min_log2_q + ); + } + continue; + } - if q_bits < min_log2_q { - if verbose { - println!( - " {} × {}-bit: log2(q)={:.2} < {:.1} needed, skipping", - num_primes, bb, q_bits, min_log2_q - ); + // Eq4 security upper bound: reject any q exceeding the security limit. + if q_bits > log2_q_limit { + if verbose { + println!( + " {} × {}-bit: log2(q)={:.2} > {:.1} security limit, skipping", + num_primes, bb, q_bits, log2_q_limit + ); + } + continue; } - continue; - } - if let Some(res) = finalize_bfv_candidate(bfv_search_config, d, sel) { - if verbose { + if let Some(res) = finalize_bfv_candidate(bfv_search_config, d, sel) { + if verbose { + println!( + "\n✓ Found first set: {} × {}-bit primes, d={}, log2(q)={:.2}, max_qi={:.2} bits", + num_primes, bb, d, q_bits, max_qi_log2 + ); + } + return Ok(res); + } else if verbose { println!( - "\n✓ Found first set: {} × {}-bit primes, log2(q)={:.2}, max_qi={:.2} bits", - num_primes, bb, q_bits, max_qi_log2 + " {} × {}-bit: log2(q)={:.2} ❌ fails correctness or margin < {:.1} bits", + num_primes, bb, q_bits, bfv_search_config.min_margin ); } - return Ok(res); - } else if verbose { - println!( - " {} × {}-bit: log2(q)={:.2} ❌ fails correctness or margin < {:.1} bits", - num_primes, bb, q_bits, bfv_search_config.min_margin - ); } } + + if verbose { + println!("\n no feasible set at d={d}; increasing ring dimension…"); + } + d <<= 1; } if verbose { @@ -496,9 +518,11 @@ pub fn bfv_search_second_param( // Centered-RNS gap rule: qi > 2*k. let min_qi_second = &max_qi_first << 1; + // Eq4 security upper bound: log2(q) <= log2(B) + (d-75)/37.5. + let log2_b = (bfv_search_config.b as f64).log2(); + let log2_q_limit = log2_b + ((d as f64) - 75.0) / 37.5; + if verbose { - let log2_b = (bfv_search_config.b as f64).log2(); - let log2_q_limit = log2_b + ((d as f64) - 75.0) / 37.5; println!( "\n[BFV-2nd] Fixed d={d}, k = max_qi_first = {k_second} ({:.2} bits)", log2_big(&max_qi_first) @@ -584,30 +608,48 @@ pub fn bfv_search_second_param( continue; } - // Take the smallest valid primes to minimise prime size. - let sel: Vec = valid.into_iter().take(num_primes).collect(); - let q = product(sel.iter().map(|pi| pi.value.clone())); - let q_bits = log2_big(&q); - let min_selected = sel.iter().map(|p| &p.value).min().unwrap(); - let gap_bits = log2_big(&(min_selected - &max_qi_first)); - - if verbose { - println!( - " {} × {}-bit: log2(q) = {:.2}, min gap = 2^{:.1}", - num_primes, bb, q_bits, gap_bits - ); - } + // Slide a window of `num_primes` over the ascending valid primes, + // starting from the smallest (to minimise prime size). If the + // smallest window fails the correctness/margin check, larger primes + // in the same bucket give a larger Δ and may still pass with the + // same CRT count, so keep trying before abandoning the bucket. + for start in 0..=(valid.len() - num_primes) { + let sel: Vec = valid[start..start + num_primes].to_vec(); + let q = product(sel.iter().map(|pi| pi.value.clone())); + let q_bits = log2_big(&q); + let min_selected = sel.iter().map(|p| &p.value).min().unwrap(); + let gap_bits = log2_big(&(min_selected - &max_qi_first)); - if let Some(res) = finalize_second_param(bfv_search_config, d, sel, k_second) { if verbose { println!( - "\n✓ Found second set: {} × {}-bit, log2(q)={:.2}, gap=2^{:.1}", + " {} × {}-bit: log2(q) = {:.2}, min gap = 2^{:.1}", num_primes, bb, q_bits, gap_bits ); } - return Some(res); - } else if verbose { - println!(" ❌ Fails correctness check"); + + // Eq4 security upper bound: q grows monotonically with `start`, + // so once it exceeds the security limit no later window can pass. + if q_bits > log2_q_limit { + if verbose { + println!( + " log2(q)={:.2} > {:.1} security limit, abandoning bucket", + q_bits, log2_q_limit + ); + } + break; + } + + if let Some(res) = finalize_second_param(bfv_search_config, d, sel, k_second) { + if verbose { + println!( + "\n✓ Found second set: {} × {}-bit, log2(q)={:.2}, gap=2^{:.1}", + num_primes, bb, q_bits, gap_bits + ); + } + return Some(res); + } else if verbose { + println!(" ❌ Fails correctness check"); + } } } } diff --git a/crates/fhe-params/src/search/constants.rs b/crates/fhe-params/src/search/constants.rs index d68e6e00e2..77f2817b2c 100644 --- a/crates/fhe-params/src/search/constants.rs +++ b/crates/fhe-params/src/search/constants.rs @@ -9,18 +9,18 @@ pub const NTT_PRIMES_BY_BITS: &[(u8, &[&str])] = &[ ( 40u8, &[ - "0x0000008000020001", + "0x0000008000034001", "0x00000080004a0001", "0x0000008000fa0001", "0x0000008001ae0001", "0x0000008001b20001", "0x0000008001ee0001", "0x0000008001f60001", - "0x0000008002220001", - "0x0000008002420001", - "0x00000080028a0001", - "0x00000080029a0001", - "0x00000080031e0001", + "0x0000008002228001", + "0x0000008002438001", + "0x00000080028a8001", + "0x00000080029c0001", + "0x000000800320c001", ], ), ( @@ -32,12 +32,12 @@ pub const NTT_PRIMES_BY_BITS: &[(u8, &[&str])] = &[ "0x0000010000ce0001", "0x0000010000de0001", "0x00000100010a0001", - "0x0000010001120001", - "0x0000010001620001", - "0x00000100019a0001", - "0x00000100021a0001", + "0x0000010001128001", + "0x0000010001680001", + "0x00000100019b0001", + "0x0000010002238001", "0x0000010002520001", - "0x0000010002620001", + "0x000001000266c001", ], ), ( @@ -49,12 +49,12 @@ pub const NTT_PRIMES_BY_BITS: &[(u8, &[&str])] = &[ "0x0000020000aa0001", "0x0000020001360001", "0x00000200015a0001", - "0x00000200017a0001", - "0x0000020001e60001", - "0x0000020002260001", - "0x0000020002a60001", - "0x0000020002c20001", - "0x0000020003160001", + "0x00000200017c0001", + "0x0000020001e74001", + "0x000002000227c001", + "0x0000020002a68001", + "0x0000020002c54001", + "0x00000200031ac001", ], ), ( @@ -66,12 +66,12 @@ pub const NTT_PRIMES_BY_BITS: &[(u8, &[&str])] = &[ "0x0000040000fe0001", "0x0000040001760001", "0x00000400017a0001", - "0x00000400019a0001", - "0x0000040001b20001", - "0x0000040001e20001", - "0x0000040002360001", - "0x0000040002a60001", - "0x0000040002fe0001", + "0x00000400019bc001", + "0x0000040001b60001", + "0x0000040001e24001", + "0x000004000236c001", + "0x0000040002a8c001", + "0x0000040003038001", ], ), ( @@ -83,12 +83,12 @@ pub const NTT_PRIMES_BY_BITS: &[(u8, &[&str])] = &[ "0x0000080001160001", "0x00000800012e0001", "0x0000080001420001", - "0x0000080001720001", - "0x0000080001a60001", - "0x0000080001c20001", - "0x00000800020e0001", - "0x0000080002360001", - "0x00000800025e0001", + "0x000008000173c001", + "0x0000080001a6c001", + "0x0000080001c40001", + "0x00000800020ec001", + "0x000008000236c001", + "0x000008000265c001", ], ), ( @@ -100,12 +100,12 @@ pub const NTT_PRIMES_BY_BITS: &[(u8, &[&str])] = &[ "0x00001000006e0001", "0x0000100000ba0001", "0x0000100000ce0001", - "0x0000100000ea0001", - "0x0000100001220001", + "0x0000100000eac001", + "0x000010000122c001", "0x0000100001560001", "0x0000100001860001", - "0x0000100001a20001", - "0x0000100001da0001", + "0x0000100001a58001", + "0x0000100001f10001", ], ), ( @@ -117,12 +117,12 @@ pub const NTT_PRIMES_BY_BITS: &[(u8, &[&str])] = &[ "0x00002000006a0001", "0x0000200000860001", "0x0000200000a60001", - "0x0000200000c20001", - "0x0000200000ee0001", - "0x0000200001320001", - "0x0000200001860001", - "0x0000200001a20001", - "0x0000200001c60001", + "0x0000200000c80001", + "0x0000200000f14001", + "0x0000200001328001", + "0x00002000018ac001", + "0x0000200001ab4001", + "0x0000200001c94001", ], ), ( @@ -134,12 +134,12 @@ pub const NTT_PRIMES_BY_BITS: &[(u8, &[&str])] = &[ "0x0000400000920001", "0x00004000009e0001", "0x0000400000b60001", - "0x0000400000d20001", - "0x0000400000f20001", - "0x0000400001260001", - "0x0000400001620001", - "0x00000400001920001", - "0x0000400001c20001", + "0x0000400000d58001", + "0x0000400000f5c001", + "0x0000400001290001", + "0x000040000164c001", + "0x0000400001938001", + "0x0000400001c4c001", ], ), ( @@ -151,12 +151,12 @@ pub const NTT_PRIMES_BY_BITS: &[(u8, &[&str])] = &[ "0x0000800001360001", "0x0000800001420001", "0x0000800002060001", - "0x0000800002120001", - "0x00008000023e0001", - "0x0000800002620001", - "0x0000800002a60001", - "0x0000800002d60001", - "0x0000800003020001", + "0x000080000214c001", + "0x00008000023f8001", + "0x0000800002624001", + "0x0000800002af8001", + "0x0000800002d70001", + "0x000080000307c001", ], ), ( @@ -464,6 +464,87 @@ pub const NTT_PRIMES_BY_BITS: &[(u8, &[&str])] = &[ ), ]; +#[cfg(test)] +mod tests { + use super::NTT_PRIMES_BY_BITS; + use std::collections::HashSet; + + /// NTT compatibility modulus: primes must satisfy p ≡ 1 (mod 2*RING_DIM), + /// i.e. p % 16384 == 1 for the 8192-degree ring used by the search. + const NTT_MODULUS: u64 = 16384; + + /// Deterministic Miller-Rabin primality test, exact for all u64. + fn is_prime(n: u64) -> bool { + if n < 2 { + return false; + } + for p in [2u64, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37] { + if n % p == 0 { + return n == p; + } + } + let mut d = n - 1; + let mut r = 0; + while d % 2 == 0 { + d /= 2; + r += 1; + } + let mulmod = |a: u64, b: u64| -> u64 { ((a as u128 * b as u128) % n as u128) as u64 }; + let powmod = |mut base: u64, mut exp: u64| -> u64 { + let mut acc = 1u64; + base %= n; + while exp > 0 { + if exp & 1 == 1 { + acc = mulmod(acc, base); + } + base = mulmod(base, base); + exp >>= 1; + } + acc + }; + 'witness: for a in [2u64, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37] { + let mut x = powmod(a, d); + if x == 1 || x == n - 1 { + continue; + } + for _ in 0..r - 1 { + x = mulmod(x, x); + if x == n - 1 { + continue 'witness; + } + } + return false; + } + true + } + + /// Guard: every hardcoded NTT prime must be an actual prime, NTT-compatible, + /// match its declared bit-length, and be globally unique. Prevents composite + /// or mislabelled entries from silently re-entering the table. + #[test] + fn ntt_primes_table_is_valid() { + let mut seen: HashSet = HashSet::new(); + for (bits, primes) in NTT_PRIMES_BY_BITS { + for hex in *primes { + let v = u64::from_str_radix(hex.trim_start_matches("0x"), 16) + .unwrap_or_else(|_| panic!("invalid hex literal {hex}")); + assert!(is_prime(v), "{hex} ({bits}-bit) is not prime"); + assert_eq!( + v % NTT_MODULUS, + 1, + "{hex} is not NTT-compatible (p % 16384 != 1)" + ); + assert_eq!( + v.checked_ilog2().unwrap() + 1, + *bits as u32, + "{hex} bit-length does not match declared {bits} bits" + ); + assert!(seen.insert(v), "{hex} is a duplicate entry"); + } + } + } +} + /// Starting polynomial degree (power of 2) for parameter search pub const D_POW2_START: u64 = 256; /// Maximum polynomial degree (power of 2) to search up to From dc47438bab19932a38de337ed9d90a51777613b9 Mon Sep 17 00:00:00 2001 From: Zara Date: Mon, 15 Jun 2026 09:13:59 -0700 Subject: [PATCH 4/6] fixed second param degree --- crates/fhe-params/src/search/bfv.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/fhe-params/src/search/bfv.rs b/crates/fhe-params/src/search/bfv.rs index 9cea6b5fdd..cab943b9c2 100644 --- a/crates/fhe-params/src/search/bfv.rs +++ b/crates/fhe-params/src/search/bfv.rs @@ -502,7 +502,7 @@ pub fn bfv_search_second_param( bfv_search_config: &BfvSearchConfig, first: &BfvSearchResult, ) -> Option { - let d = RING_DIM; + let d = first.d; // Plaintext space for second set: k = max qi of first set (actual value). let max_qi_first: BigUint = first From b5c89d59f0b8f9c26b00d61694a2b0f7f91be4f2 Mon Sep 17 00:00:00 2001 From: Zara Date: Mon, 15 Jun 2026 10:20:03 -0700 Subject: [PATCH 5/6] added new ntt check based on the degree --- crates/fhe-params/src/search/bfv.rs | 28 ++++++++++++-- crates/fhe-params/src/search/prime.rs | 54 +++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/crates/fhe-params/src/search/bfv.rs b/crates/fhe-params/src/search/bfv.rs index cab943b9c2..0165dce86b 100644 --- a/crates/fhe-params/src/search/bfv.rs +++ b/crates/fhe-params/src/search/bfv.rs @@ -15,7 +15,7 @@ use crate::search::constants::{D_POW2_MAX, 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, + build_prime_items, build_prime_items_for_second, is_ntt_friendly, select_max_q_under_cap, }; use crate::search::utils::{approx_bits_from_log2, big_shift_pow2, log2_big, product}; use num_bigint::BigUint; @@ -146,6 +146,22 @@ pub fn bfv_search(bfv_search_config: &BfvSearchConfig) -> BfvParamsResult> = by_bits + .iter() + .filter_map(|(&bb, bucket)| { + let filtered: Vec = bucket + .iter() + .filter(|p| is_ntt_friendly(&p.value, d)) + .cloned() + .collect(); + (!filtered.is_empty()).then_some((bb, filtered)) + }) + .collect(); + // Minimum log2(q) for correctness (Eq1); exact margin check is in finalize. let min_log2_q = calculate_min_q_bits(bfv_search_config, d); // Eq4 security upper bound: log2(q) <= log2(B) + (d-75)/37.5. @@ -165,7 +181,7 @@ pub fn bfv_search(bfv_search_config: &BfvSearchConfig) -> BfvParamsResult b, None => continue, }; @@ -544,9 +560,15 @@ pub fn bfv_search_second_param( .collect(); // Buckets sorted ASCENDING within each bit-length (smallest prime first), so - // taking the first valid `num_primes` minimises prime size. + // taking the first valid `num_primes` minimises prime size. Only primes that + // are NTT-friendly for this ring dimension (p ≡ 1 mod 2d) are eligible: at + // d == RING_DIM every entry qualifies, but a larger d (from bfv_search) + // excludes some 62-bit primes that only satisfy the smaller modulus. let mut by_bits: BTreeMap> = BTreeMap::new(); for p in &prime_items { + if !is_ntt_friendly(&p.value, d) { + continue; + } by_bits.entry(p.bitlen).or_default().push(p.clone()); } for v in by_bits.values_mut() { diff --git a/crates/fhe-params/src/search/prime.rs b/crates/fhe-params/src/search/prime.rs index f1a9cf84e7..98ccaa90cf 100644 --- a/crates/fhe-params/src/search/prime.rs +++ b/crates/fhe-params/src/search/prime.rs @@ -5,6 +5,7 @@ // 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; @@ -60,6 +61,16 @@ pub fn build_prime_items_for_second() -> Vec { build_prime_items_with_filter(|bits| bits == 63 || bits == 61) } +/// Check whether `p` is NTT-friendly for ring dimension `d`, i.e. `p ≡ 1 (mod 2d)`. +/// +/// All primes in `NTT_PRIMES_BY_BITS` satisfy this for the base ring dimension +/// (2*8192 = 16384), but not every prime remains NTT-friendly for the larger +/// dimensions `bfv_search` may step up to (16384, 32768), so candidates must be +/// re-filtered against the actual `d` before selection. +pub fn is_ntt_friendly(p: &BigUint, d: u64) -> bool { + p % BigUint::from(2u64 * d) == BigUint::one() +} + /// 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), @@ -134,6 +145,49 @@ mod tests { } } + #[test] + fn test_is_ntt_friendly() { + let items = build_prime_items(); + + // Base ring dimension: every first-set prime is NTT-friendly (table-wide invariant). + for item in &items { + assert!( + is_ntt_friendly(&item.value, 8192), + "{} fails NTT check for d=8192", + item.hex + ); + } + + // 60-bit primes are only NTT-friendly for the base dimension; larger d + // (which bfv_search may step up to) excludes them. + let sixty_bit: Vec<&PrimeItem> = items.iter().filter(|p| p.bitlen == 60).collect(); + assert!(!sixty_bit.is_empty()); + for item in &sixty_bit { + assert!( + !is_ntt_friendly(&item.value, 16384), + "{} unexpectedly NTT-friendly for d=16384", + item.hex + ); + assert!( + !is_ntt_friendly(&item.value, 32768), + "{} unexpectedly NTT-friendly for d=32768", + item.hex + ); + } + + // 50..=59-bit primes remain NTT-friendly for every dimension bfv_search may return. + for item in items.iter().filter(|p| p.bitlen < 60) { + for &d in &[8192u64, 16384, 32768] { + assert!( + is_ntt_friendly(&item.value, d), + "{} fails NTT check for d={}", + item.hex, + d + ); + } + } + } + #[test] fn test_select_max_q_under_cap() { let all = build_prime_items(); From 27fb8d5b55dcf19680330492303e3a7d8eb5c89b Mon Sep 17 00:00:00 2001 From: 0xjei Date: Mon, 15 Jun 2026 22:31:22 +0200 Subject: [PATCH 6/6] avoid skipping other windows in the same bucket --- crates/fhe-params/src/search/bfv.rs | 71 +++++++++++++++-------------- 1 file changed, 38 insertions(+), 33 deletions(-) diff --git a/crates/fhe-params/src/search/bfv.rs b/crates/fhe-params/src/search/bfv.rs index 0165dce86b..6f0fcdbe1a 100644 --- a/crates/fhe-params/src/search/bfv.rs +++ b/crates/fhe-params/src/search/bfv.rs @@ -199,46 +199,51 @@ pub fn bfv_search(bfv_search_config: &BfvSearchConfig) -> BfvParamsResult = bucket.iter().take(num_primes).cloned().collect(); - let q = product(sel.iter().map(|pi| pi.value.clone())); - let q_bits = log2_big(&q); - let max_qi_log2 = sel.iter().map(|p| p.log2).fold(0.0_f64, f64::max); - - if q_bits < min_log2_q { - if verbose { - println!( - " {} × {}-bit: log2(q)={:.2} < {:.1} needed, skipping", - num_primes, bb, q_bits, min_log2_q - ); + // Slide a window of `num_primes` over the descending bucket. The + // largest window is tried first; if it exceeds the security cap, + // smaller windows in the same bucket may still fit + // [min_log2_q, log2_q_limit]. + for start in 0..=(bucket.len() - num_primes) { + let sel: Vec = bucket[start..start + num_primes].to_vec(); + let q = product(sel.iter().map(|pi| pi.value.clone())); + let q_bits = log2_big(&q); + let max_qi_log2 = sel.iter().map(|p| p.log2).fold(0.0_f64, f64::max); + + // Because the bucket is descending, later windows only get smaller. + if q_bits < min_log2_q { + if verbose { + println!( + " {} × {}-bit: log2(q)={:.2} < {:.1} needed, abandoning bucket", + num_primes, bb, q_bits, min_log2_q + ); + } + break; } - continue; - } - // Eq4 security upper bound: reject any q exceeding the security limit. - if q_bits > log2_q_limit { - if verbose { - println!( - " {} × {}-bit: log2(q)={:.2} > {:.1} security limit, skipping", - num_primes, bb, q_bits, log2_q_limit - ); + if q_bits > log2_q_limit { + if verbose { + println!( + " {} × {}-bit: log2(q)={:.2} > {:.1} security limit, trying smaller primes", + num_primes, bb, q_bits, log2_q_limit + ); + } + continue; } - continue; - } - if let Some(res) = finalize_bfv_candidate(bfv_search_config, d, sel) { - if verbose { + if let Some(res) = finalize_bfv_candidate(bfv_search_config, d, sel) { + if verbose { + println!( + "\n✓ Found first set: {} × {}-bit primes, d={}, log2(q)={:.2}, max_qi={:.2} bits", + num_primes, bb, d, q_bits, max_qi_log2 + ); + } + return Ok(res); + } else if verbose { println!( - "\n✓ Found first set: {} × {}-bit primes, d={}, log2(q)={:.2}, max_qi={:.2} bits", - num_primes, bb, d, q_bits, max_qi_log2 + " {} × {}-bit: log2(q)={:.2} ❌ fails correctness or margin < {:.1} bits", + num_primes, bb, q_bits, bfv_search_config.min_margin ); } - return Ok(res); - } else if verbose { - println!( - " {} × {}-bit: log2(q)={:.2} ❌ fails correctness or margin < {:.1} bits", - num_primes, bb, q_bits, bfv_search_config.min_margin - ); } } }