Skip to content

refactor: update search algo to latest approach [skip-line-limit]#1600

Merged
0xjei merged 7 commits into
mainfrom
align/search
Jun 16, 2026
Merged

refactor: update search algo to latest approach [skip-line-limit]#1600
0xjei merged 7 commits into
mainfrom
align/search

Conversation

@0xjei

@0xjei 0xjei commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

mirrors latest in https://github.com/gnosisguild/enclave-research/tree/3-moduli

Summary by CodeRabbit

Release Notes

  • New Features

    • Added --min-margin CLI argument to control parameter acceptance margin threshold (default: 1.0)
  • Improvements

    • Redesigned parameter search algorithm for improved efficiency and accuracy
    • Enhanced validation of parameter prime pools
    • Refined prime selection strategy for optimal parameter sets
    • Improved CLI output and startup information display

@0xjei 0xjei self-assigned this Jun 15, 2026
@vercel

vercel Bot commented Jun 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
crisp Ready Ready Preview, Comment Jun 16, 2026 5:21am
enclave-docs Ready Ready Preview, Comment Jun 16, 2026 5:21am
interfold-dashboard Ready Ready Preview, Comment Jun 16, 2026 5:21am

Request Review

@0xjei 0xjei requested a review from ctrlc03 June 15, 2026 09:44
@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The BFV parameter search is overhauled: the NTT_PRIMES_BY_BITS table switches to variable-length prime slices, build_prime_items is narrowed to 50–60-bit primes, and a new is_ntt_friendly helper is added. The first-set search replaces the greedy flow with a ring-dimension–doubling loop over bucketed primes. A new min_margin field gates Eq1 acceptance in both finalize_bfv_candidate and finalize_second_param. The second-set search is rewritten to enforce strict large-gap and disjointness constraints. The CLI gains a --min-margin argument and revised output formatting.

Changes

BFV Parameter Search Overhaul

Layer / File(s) Summary
Prime table, filtering, and supporting utilities
crates/fhe-params/src/search/constants.rs, crates/fhe-params/src/search/prime.rs, crates/fhe-params/src/search/utils.rs
NTT_PRIMES_BY_BITS changes from fixed 6-element arrays to &[&str] slices with updated hex primes and a self-validating test module. build_prime_items is narrowed to 50–60 bits; is_ntt_friendly(p, d) is added as a public helper with tests. log2_big is simplified to use the top 8 bytes directly.
BfvSearchConfig field and first-set search overhaul
crates/fhe-params/src/search/bfv.rs
Adds min_margin: f64 to BfvSearchConfig and RING_DIM = 8192 plus bucket-search constants. The first-set search loop now doubles ring dimensions, filters primes by NTT-friendliness, and scans (num_primes, bitlen) bucket windows. finalize_bfv_candidate computes margin = rhs_log2 - lhs_log2 and rejects below min_margin.
Second parameter set search rewrite
crates/fhe-params/src/search/bfv.rs
bfv_search_second_param sets k_second from the max first-set prime, enforces qi > 2*k_second and disjointness, and scans ascending bit-length buckets. construct_qi_second_param picks the candidate with the smallest qbits. finalize_second_param uses pi.value > 2*k_plain for the large-gap rule and the same min_margin rejection.
CLI --min-margin arg and print_param_set refactor
crates/fhe-params/src/bin/search_params.rs
Adds --min-margin CLI argument (default 1.0), validated as finite and non-negative, and wired into BfvSearchConfig. Fixes uniform-variance formula to B^2/3. print_param_set is refactored to take var_chi and Option<var_enc>. main prints prime pool sizes at startup and adds a trailing separator line.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 Hopping through primes with a bucket in paw,
Old greedy searches dissolved into awe.
The margin now governs—no slipping through cracks,
Each bit-length ascending in neatly sorted stacks.
With variable slices and NTT in line,
The rabbit declares: these parameters are fine! 🌟

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: refactoring the search algorithm to align with the latest approach across multiple search-related modules.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch align/search

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
crates/fhe-params/src/search/bfv.rs (1)

743-758: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate candidates in ascending qbits, not just the smallest one.

best is chosen before finalize_second_param; if the smallest candidate fails correctness/margin, this returns None even when a slightly larger tried selection would pass. Sort by qbits and return the first finalized candidate.

Proposed candidate validation order
-    // 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<PrimeItem>)> = None;
-    for sel in tried {
+    // Prefer the smallest passing selection.
+    let mut candidates: Vec<(f64, Vec<PrimeItem>)> = Vec::new();
+    for sel in tried {
         let q = product(sel.iter().map(|pi| pi.value.clone()));
         let qbits = log2_big(&q);
-        if let Some((best_qbits, _)) = &best {
-            if qbits < *best_qbits {
-                best = Some((qbits, sel));
-            }
-        } else {
-            best = Some((qbits, sel));
-        }
+        candidates.push((qbits, sel));
     }
-    if let Some((_, sel)) = best {
-        return finalize_second_param(bfv_search_config, d, sel.clone(), k_plain);
+    candidates.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
+
+    for (_, sel) in candidates {
+        if let Some(res) = finalize_second_param(bfv_search_config, d, sel, k_plain) {
+            return Some(res);
+        }
     }
     None
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fhe-params/src/search/bfv.rs` around lines 743 - 758, The current code
selects only the single candidate with the smallest qbits value and immediately
calls finalize_second_param on it, returning None if that candidate fails
validation even though larger candidates in the tried set might pass. Instead,
sort the tried selections by their computed qbits values in ascending order,
then iterate through them sequentially, calling finalize_second_param on each
candidate and returning the first one that succeeds (does not return None). If
all candidates fail validation, then return None.
crates/fhe-params/src/bin/search_params.rs (2)

21-24: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update the CLI help range to match the new prime pool.

Line 23 still advertises 40..63 bits, but this refactor’s prime builder now targets the narrowed 50..60-bit pool. The --help text should not describe an unavailable search range.

Proposed fix
 #[command(
     version,
-    about = "Search BFV params with NTT-friendly CRT primes (40..63 bits)"
+    about = "Search BFV params with NTT-friendly CRT primes (50..60 bits)"
 )]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fhe-params/src/bin/search_params.rs` around lines 21 - 24, The `about`
field in the `#[command(...)]` macro on line 23 advertises a search range of
`40..63 bits`, but the refactored prime builder now targets a narrowed `50..60`
bit pool. Update the help text string in the `about` attribute to accurately
reflect the new range of `50..60 bits` instead of the outdated `40..63 bits`
range.

69-78: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep both Uniform variance helpers on the same formula.

The new big-int path uses B²/3, but variance_uniform_str still reports B*(B+1)/3, so the final output can show inconsistent variance formulas for B vs B_Enc.

Proposed fix
 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)
+    variance_uniform_big_str(&b_big)
 }
 
 fn variance_uniform_big_str(b: &BigUint) -> String {
     // Variance for Uniform(-B..B): Var = B^2 / 3
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fhe-params/src/bin/search_params.rs` around lines 69 - 78, The two
variance computation functions variance_uniform_str and variance_uniform_big_str
are using different formulas. The variance_uniform_big_str function correctly
uses B²/3 for uniform variance, but variance_uniform_str uses B*(B+1)/3 instead.
Update variance_uniform_str to use the same formula as variance_uniform_big_str
by changing the variance calculation from (b_big * (b + 1)) / 3u32 to (b_big *
b_big) / 3u32 to ensure both functions report consistent variance formulas.
🧹 Nitpick comments (3)
crates/fhe-params/src/search/prime.rs (1)

100-109: ⚡ Quick win

Assert the new first-set bit range in the regression test.

build_prime_items() now promises only 50..=60 bit primes, but the test still only rejects 61/62/63. Add the lower-bound assertion so the new correctness-margin contract cannot regress silently.

Proposed test tightening
-        // Verify no 61, 62, or 63-bit primes are included
+        // Verify first-set primes are restricted to 50..=60 bits.
         for item in &items {
-            assert_ne!(item.bitlen, 61);
-            assert_ne!(item.bitlen, 62);
-            assert_ne!(item.bitlen, 63);
+            assert!(
+                (50..=60).contains(&item.bitlen),
+                "unexpected first-set prime bit length: {}",
+                item.bitlen
+            );
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fhe-params/src/search/prime.rs` around lines 100 - 109, The regression
test test_build_prime_items() currently only verifies the upper bounds by
asserting that bitlen is not 61, 62, or 63, but does not validate the lower
bound. Since build_prime_items() now promises only 50..=60 bit primes, add
assertions to the test loop to verify that each item has a bitlen greater than
or equal to 50. This ensures the lower-bound contract cannot regress silently.
crates/fhe-params/src/search/bfv.rs (1)

96-104: ⚡ Quick win

Refresh the public bfv_search docs for the fixed-d bucket scan.

The docs still describe degree iteration, Eq4 max-q selection, and refinement, but the implementation now fixes d = RING_DIM and scans bit buckets. That stale contract is misleading for callers tuning parameters.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fhe-params/src/search/bfv.rs` around lines 96 - 104, Update the
documentation comment for the `bfv_search` function to accurately reflect the
current implementation. Replace the description that mentions iterating through
polynomial degrees d and Eq4 max-q selection with documentation that correctly
describes the fixed-d bucket scan approach. The docs should clearly state that
the function now fixes d to RING_DIM and scans through bit buckets to find
optimal parameters, rather than the old degree iteration and refinement process
described in the current comment block.
crates/fhe-params/src/search/utils.rs (1)

88-96: ⚡ Quick win

Cover byte-boundary cases for log2_big.

The new approximation drives margin checks, but the test only checks positivity/order. Add exact power-of-two and >8-byte cases so future changes don’t skew q/margin decisions near bucket boundaries.

Proposed regression cases
         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)));
+
+        assert_eq!(log2_big(&(BigUint::one() << 64u32)), 64.0);
+        let x = (BigUint::one() << 80u32) + BigUint::from(123u64);
+        assert!((log2_big(&x) - 80.0).abs() < 1e-12);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fhe-params/src/search/utils.rs` around lines 88 - 96, The
test_log2_big function only validates basic behavior (positivity and ordering)
but lacks coverage for exact power-of-two cases and large byte-boundary
scenarios. Expand the test by adding assertions for specific power-of-two values
(such as 2^8, 2^16, 2^32, 2^64) to verify precise calculations at key
boundaries, and include test cases with large BigUint values that exceed 8 bytes
to ensure the approximation remains accurate for oversized inputs. This ensures
future modifications to the log2_big function do not inadvertently affect margin
calculations and bucket boundary decisions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/fhe-params/src/bin/search_params.rs`:
- Around line 234-237: The error message in the bfv_search error handler on line
236 suggests users "increase d", but the CLI no longer exposes ring dimension
(d) as a tunable parameter. Remove the "increase d" suggestion from the
eprintln! statement and keep only the parameters that are actually exposed in
the CLI for users to adjust.
- Around line 52-54: The `min_margin` field in the CLI argument parser accepts
any f64 value without validation, but it is used to gate candidate acceptance in
the search logic. Add validation after the argument is parsed to reject negative
and non-finite values (such as infinity and NaN) before passing `min_margin`
into `BfvSearchConfig`. Implement this check before the search is executed to
ensure only valid margin values reach the search logic that depends on it.

In `@crates/fhe-params/src/search/bfv.rs`:
- Around line 126-128: The code in bfv.rs computes `log2_q_limit` as a security
limit based on correctness margins, but currently only logs this value without
enforcing it as a filter. You need to add validation logic at all locations
where `log2_q_limit` is computed (at lines 126-128, 181-197, 499-511, and
587-601) to reject any candidate where the logarithm of q exceeds the computed
security limit. This means before a candidate is returned or accepted, check
that its log2(q) value does not exceed log2_q_limit, and skip or reject the
candidate if this security constraint is violated.
- Around line 567-601: The current implementation takes only the smallest valid
primes using `.take(num_primes)` and if the `finalize_second_param()` call
returns None, it immediately continues to the next bucket without trying larger
valid primes from the same bucket. To fix this, wrap the section from where
`sel` is created through the `finalize_second_param()` call in a loop that
iterates through different selections of primes from the `valid` vector. Instead
of just taking the first `num_primes`, try multiple consecutive selections of
`num_primes` primes starting from different offsets in the sorted `valid`
vector, so that larger candidates from the same bucket are tested before
abandoning the bucket entirely.
- Around line 58-59: Add input validation for the min_margin field in
search_params.rs before BfvSearchConfig construction (lines 204–213). Insert a
validation check that ensures min_margin.is_finite() && min_margin >= 0.0,
following the same validation pattern already used for z and k parameters in the
preceding lines (187–202). This prevents non-finite and negative min_margin
values from bypassing the margin gate comparisons that occur later at lines 314
and 802.

In `@crates/fhe-params/src/search/constants.rs`:
- Around line 8-465: The NTT_PRIMES_BY_BITS constant contains 51 composite
(non-prime) entries that will cause silent failures in parameter generation,
with high concentrations in the 40–48 bit ranges (40–50% composite rates) and
all entries being valid primes in the 49–62 bit ranges. Replace all composite
entries in the tuples for bit-lengths 40–48 with validated prime numbers that
satisfy the NTT-compatibility requirement (p % 16384 == 1) and their declared
bit-length, ensuring all entries in each bit-length group are actual primes
before merge.

---

Outside diff comments:
In `@crates/fhe-params/src/bin/search_params.rs`:
- Around line 21-24: The `about` field in the `#[command(...)]` macro on line 23
advertises a search range of `40..63 bits`, but the refactored prime builder now
targets a narrowed `50..60` bit pool. Update the help text string in the `about`
attribute to accurately reflect the new range of `50..60 bits` instead of the
outdated `40..63 bits` range.
- Around line 69-78: The two variance computation functions variance_uniform_str
and variance_uniform_big_str are using different formulas. The
variance_uniform_big_str function correctly uses B²/3 for uniform variance, but
variance_uniform_str uses B*(B+1)/3 instead. Update variance_uniform_str to use
the same formula as variance_uniform_big_str by changing the variance
calculation from (b_big * (b + 1)) / 3u32 to (b_big * b_big) / 3u32 to ensure
both functions report consistent variance formulas.

In `@crates/fhe-params/src/search/bfv.rs`:
- Around line 743-758: The current code selects only the single candidate with
the smallest qbits value and immediately calls finalize_second_param on it,
returning None if that candidate fails validation even though larger candidates
in the tried set might pass. Instead, sort the tried selections by their
computed qbits values in ascending order, then iterate through them
sequentially, calling finalize_second_param on each candidate and returning the
first one that succeeds (does not return None). If all candidates fail
validation, then return None.

---

Nitpick comments:
In `@crates/fhe-params/src/search/bfv.rs`:
- Around line 96-104: Update the documentation comment for the `bfv_search`
function to accurately reflect the current implementation. Replace the
description that mentions iterating through polynomial degrees d and Eq4 max-q
selection with documentation that correctly describes the fixed-d bucket scan
approach. The docs should clearly state that the function now fixes d to
RING_DIM and scans through bit buckets to find optimal parameters, rather than
the old degree iteration and refinement process described in the current comment
block.

In `@crates/fhe-params/src/search/prime.rs`:
- Around line 100-109: The regression test test_build_prime_items() currently
only verifies the upper bounds by asserting that bitlen is not 61, 62, or 63,
but does not validate the lower bound. Since build_prime_items() now promises
only 50..=60 bit primes, add assertions to the test loop to verify that each
item has a bitlen greater than or equal to 50. This ensures the lower-bound
contract cannot regress silently.

In `@crates/fhe-params/src/search/utils.rs`:
- Around line 88-96: The test_log2_big function only validates basic behavior
(positivity and ordering) but lacks coverage for exact power-of-two cases and
large byte-boundary scenarios. Expand the test by adding assertions for specific
power-of-two values (such as 2^8, 2^16, 2^32, 2^64) to verify precise
calculations at key boundaries, and include test cases with large BigUint values
that exceed 8 bytes to ensure the approximation remains accurate for oversized
inputs. This ensures future modifications to the log2_big function do not
inadvertently affect margin calculations and bucket boundary decisions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1321bba5-4625-434a-93a5-22a9febd774c

📥 Commits

Reviewing files that changed from the base of the PR and between 585dc47 and 6d004c6.

📒 Files selected for processing (5)
  • crates/fhe-params/src/bin/search_params.rs
  • crates/fhe-params/src/search/bfv.rs
  • crates/fhe-params/src/search/constants.rs
  • crates/fhe-params/src/search/prime.rs
  • crates/fhe-params/src/search/utils.rs

Comment thread crates/fhe-params/src/bin/search_params.rs
Comment thread crates/fhe-params/src/bin/search_params.rs
Comment thread crates/fhe-params/src/search/bfv.rs
Comment thread crates/fhe-params/src/search/bfv.rs Outdated
Comment thread crates/fhe-params/src/search/bfv.rs Outdated
Comment thread crates/fhe-params/src/search/constants.rs
zahrajavar
zahrajavar previously approved these changes Jun 15, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
crates/fhe-params/src/search/constants.rs (1)

7-8: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update the table doc to match variable-length buckets.

Line 7 still says “6 per size”, but the new &[&str] buckets are variable-length and several buckets contain far more than six entries.

Proposed wording update
-/// NTT-friendly primes by bit-length (40..63), 6 per size
+/// NTT-friendly primes by bit-length (40..63), variable count per size.
 pub const NTT_PRIMES_BY_BITS: &[(u8, &[&str])] = &[
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fhe-params/src/search/constants.rs` around lines 7 - 8, Update the
documentation comment for the NTT_PRIMES_BY_BITS constant to accurately reflect
that the buckets contain variable-length entries rather than a fixed count of 6
per size. Replace the phrase "6 per size" with language that describes the
variable-length nature of the buckets in the data structure.
crates/fhe-params/src/search/bfv.rs (2)

785-800: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Choose the smallest passing second-set candidate, not just the smallest tried candidate.

The current code picks the lowest qbits from tried and finalizes only that one. If the smallest candidate fails the gap, correctness, or margin checks, a larger tried candidate can still be valid but is never tested.

Proposed candidate selection fix
-    // 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<PrimeItem>)> = None;
+    // Prefer the smallest candidate that actually passes validation.
+    let mut best: Option<(f64, BfvSearchResult)> = None;
     for sel in tried {
         let q = product(sel.iter().map(|pi| pi.value.clone()));
         let qbits = log2_big(&q);
-        if let Some((best_qbits, _)) = &best {
-            if qbits < *best_qbits {
-                best = Some((qbits, sel));
+        if let Some(res) = finalize_second_param(bfv_search_config, d, sel, k_plain) {
+            if let Some((best_qbits, _)) = &best {
+                if qbits < *best_qbits {
+                    best = Some((qbits, res));
+                }
+            } else {
+                best = Some((qbits, res));
             }
-        } else {
-            best = Some((qbits, sel));
         }
     }
-    if let Some((_, sel)) = best {
-        return finalize_second_param(bfv_search_config, d, sel.clone(), k_plain);
-    }
-    None
+    best.map(|(_, res)| res)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fhe-params/src/search/bfv.rs` around lines 785 - 800, The current code
in the loop around line 785-800 selects the single candidate with the smallest
qbits from the `tried` set and immediately returns after calling
finalize_second_param on it. If that smallest candidate fails internal
validation checks within finalize_second_param (such as gap, correctness, or
margin checks), no fallback to larger candidates occurs. Instead of picking one
best candidate and returning immediately, sort the candidates in `tried` by
qbits in ascending order, then iterate through them in that order, calling
finalize_second_param on each candidate until one succeeds and returns a valid
result. Only if all candidates fail should the function return without a
solution. This ensures the smallest valid candidate is selected rather than just
the smallest tried candidate.

430-459: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid duplicating a CRT prime when floor and ceil buckets are the same.

When floor_r == ceil_r, the mixed selection loop takes from the same bucket twice, so candidates like [p0, p0] can be pushed into tried. The finalizers do not reject duplicate qi, so this can produce an invalid CRT modulus.

Proposed shape for both constructors
-    for k in 0..=s {
-        let take_ceil = k;
-        let take_floor = s - k;
-        let mut sel: Vec<PrimeItem> = 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 floor_r == ceil_r {
+        if let Some(b) = by_bits_small.get(&floor_r) {
+            if b.len() >= s {
+                tried.push(b.iter().take(s).cloned().collect());
+            }
+        }
+    } else {
+        for k in 0..=s {
+            let take_ceil = k;
+            let take_floor = s - k;
+            let mut sel: Vec<PrimeItem> = 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 applies to: 745-772

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fhe-params/src/search/bfv.rs` around lines 430 - 459, The loop that
builds candidate selections by mixing floor and ceil buckets can produce invalid
CRT moduli with duplicate primes when floor_r equals ceil_r, because both the
take_floor and take_ceil selections pull from the same bucket. Add a check at
the beginning of the loop (before the if statements for take_floor and
take_ceil) to skip the iteration when floor_r == ceil_r. This same fix must also
be applied to the analogous loop structure in the second location (the parallel
code that handles the identical selection logic).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/fhe-params/src/search/bfv.rs`:
- Around line 147-152: The prime pools used in the BFV search are validated only
for specific ring dimensions but the search can now return larger dimensions
like 16384 or 32768. Filter the first-set and second-set prime pools based on
the current ring dimension value d before selecting candidates, ensuring that
each prime p satisfies the NTT constraint p % (2 * d) == 1 for the requested
dimension. Additionally, update the table validator in constants.rs to validate
that all primes in the pools meet this constraint for every dimension that
bfv_search may return, not just the base dimension.
- Around line 186-210: The current implementation skips the entire bucket with a
`continue` statement when the largest `num_primes` primes selection exceeds the
`log2_q_limit` security cap. However, smaller windows within the same bucket may
still satisfy both the security limit and the correctness lower bound. Instead
of immediately continuing to the next bucket when `q_bits > log2_q_limit`,
introduce a loop that progressively tries smaller windows (fewer primes) from
the same bucket by reducing the window size, retesting the q_bits constraint
against `log2_q_limit`, and checking the lower bound constraint against
`min_log2_q` for each smaller window before eventually moving to the next bucket
if no valid selection is found.

---

Outside diff comments:
In `@crates/fhe-params/src/search/bfv.rs`:
- Around line 785-800: The current code in the loop around line 785-800 selects
the single candidate with the smallest qbits from the `tried` set and
immediately returns after calling finalize_second_param on it. If that smallest
candidate fails internal validation checks within finalize_second_param (such as
gap, correctness, or margin checks), no fallback to larger candidates occurs.
Instead of picking one best candidate and returning immediately, sort the
candidates in `tried` by qbits in ascending order, then iterate through them in
that order, calling finalize_second_param on each candidate until one succeeds
and returns a valid result. Only if all candidates fail should the function
return without a solution. This ensures the smallest valid candidate is selected
rather than just the smallest tried candidate.
- Around line 430-459: The loop that builds candidate selections by mixing floor
and ceil buckets can produce invalid CRT moduli with duplicate primes when
floor_r equals ceil_r, because both the take_floor and take_ceil selections pull
from the same bucket. Add a check at the beginning of the loop (before the if
statements for take_floor and take_ceil) to skip the iteration when floor_r ==
ceil_r. This same fix must also be applied to the analogous loop structure in
the second location (the parallel code that handles the identical selection
logic).

In `@crates/fhe-params/src/search/constants.rs`:
- Around line 7-8: Update the documentation comment for the NTT_PRIMES_BY_BITS
constant to accurately reflect that the buckets contain variable-length entries
rather than a fixed count of 6 per size. Replace the phrase "6 per size" with
language that describes the variable-length nature of the buckets in the data
structure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2a5af242-13c6-41d6-9868-3981002b9134

📥 Commits

Reviewing files that changed from the base of the PR and between 6d004c6 and f01939f.

📒 Files selected for processing (3)
  • crates/fhe-params/src/bin/search_params.rs
  • crates/fhe-params/src/search/bfv.rs
  • crates/fhe-params/src/search/constants.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/fhe-params/src/bin/search_params.rs

Comment thread crates/fhe-params/src/search/bfv.rs
Comment thread crates/fhe-params/src/search/bfv.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
crates/fhe-params/src/search/prime.rs (1)

51-56: ⚡ Quick win

Lock the new 50-bit lower bound in tests.

build_prime_items() now enforces 50..=60 bits, but test_build_prime_items only asserts exclusion of 61/62/63. Please add an explicit bitlen >= 50 assertion so this contract can’t regress silently.

Suggested test tightening
@@
         for item in &items {
+            assert!(item.bitlen >= 50);
+            assert!(item.bitlen <= 60);
             assert_ne!(item.bitlen, 61);
             assert_ne!(item.bitlen, 62);
             assert_ne!(item.bitlen, 63);
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fhe-params/src/search/prime.rs` around lines 51 - 56, The
test_build_prime_items test function only verifies that primes with 61, 62, and
63 bits are excluded, but does not verify the new 50-bit lower bound enforced by
build_prime_items(). Add an explicit assertion in test_build_prime_items to
verify that all returned prime items have a bitlen of at least 50, ensuring the
lower bound contract cannot regress silently in future changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/fhe-params/src/search/prime.rs`:
- Around line 70-72: The is_ntt_friendly function lacks input validation and can
panic when d is zero (causing modulo by zero) or miscompute when d is extremely
large (due to unchecked overflow in 2 * d). Add explicit input validation at the
start of the is_ntt_friendly function to check that d is greater than zero and
that 2 * d does not overflow a u64. Return a safe default or handle invalid
cases explicitly to prevent panics and silent errors, making the function's
behavior safe and predictable for all inputs.

---

Nitpick comments:
In `@crates/fhe-params/src/search/prime.rs`:
- Around line 51-56: The test_build_prime_items test function only verifies that
primes with 61, 62, and 63 bits are excluded, but does not verify the new 50-bit
lower bound enforced by build_prime_items(). Add an explicit assertion in
test_build_prime_items to verify that all returned prime items have a bitlen of
at least 50, ensuring the lower bound contract cannot regress silently in future
changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 097b7c35-9694-48d2-ab15-569d1ca93b79

📥 Commits

Reviewing files that changed from the base of the PR and between f01939f and 27fb8d5.

📒 Files selected for processing (5)
  • crates/fhe-params/src/bin/search_params.rs
  • crates/fhe-params/src/search/bfv.rs
  • crates/fhe-params/src/search/constants.rs
  • crates/fhe-params/src/search/prime.rs
  • crates/fhe-params/src/search/utils.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • crates/fhe-params/src/search/utils.rs
  • crates/fhe-params/src/search/constants.rs
  • crates/fhe-params/src/search/bfv.rs
  • crates/fhe-params/src/bin/search_params.rs

Comment thread crates/fhe-params/src/search/prime.rs
@0xjei 0xjei merged commit 641c7ef into main Jun 16, 2026
33 checks passed
@ctrlc03 ctrlc03 deleted the align/search branch June 16, 2026 08:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants