Skip to content

Updates main to v1.6.0 - #1362

Merged
dsuponitskiy merged 74 commits into
mainfrom
dev
Sep 28, 2026
Merged

dsuponitskiy merged 74 commits into
mainfrom
dev

Conversation

@yspolyakov

Copy link
Copy Markdown
Contributor

No description provided.

dsuponitskiy and others added 30 commits April 20, 2026 11:15
…int8_t and removed their aliases from inttypes.h (#1168)

* Remove usshort and uschar from inttypes.h and replaced their usage with uint16_t and uint8_t

* Removed usint from inttypes.h and replaced its usage with uint32_t

* Pass integer parameter by value to function instead of by constant reference

---------

Co-authored-by: Dmitriy Suponitskiy <dsuponitskiy@dualitytech.com>
Added funding acknowledgment for ARPA-H research.
…v5 or v6) (#1182)

* Changes for migration from Node.js 20 to 24 actions

* Start using Doxyfile for the docs build

* Revert "Start using Doxyfile for the docs build"

This reverts commit 1c81c89.

* Do not extract anonymous namespaces

* Additional changes for migration from Node.js 20 to 24 and docs generation

* More changes for docs

* Suppress the anonymous namespace entries

* Test changes

* Revert "Test changes"

This reverts commit 3a2af45.

---------

Co-authored-by: Dmitriy Suponitskiy <dsuponitskiy@dualitytech.com>
Co-authored-by: Dmitriy Suponitskiy <dsuponitskiy@dualitytech.com>
…/cryptocontext-ser.h, so the functions from src/core/include/utils/serial.h can be used directly (#1191)

Co-authored-by: Dmitriy Suponitskiy <dsuponitskiy@dualitytech.com>
…rs of the same class, moved some trivial getters from .cpp to .h files, remove some "this->" for class data and function members, added m_ to a privateKey data member in Cryptocontext, changed a few error messages, removed PrintIntegerConstants (unused function), removed duplicate returns (#1194)

Co-authored-by: Dmitriy Suponitskiy <dsuponitskiy@dualitytech.com>
… to gen-cryptocontext-params-validation.cpp (#1197)

Co-authored-by: Dmitriy Suponitskiy <dsuponitskiy@dualitytech.com>
* Added SerializeEvalBootstrapKey()/DeserializeEvalBootstrapKey() and fixed some bugs.

* Added DeserializeEvalBootstrapKey() overload with number of slots as argument, example file and unittests

* Addressed code review comments

---------

Co-authored-by: Dmitriy Suponitskiy <dsuponitskiy@dualitytech.com>
* Performance: cache paramsQl for HYBRID KeySwitchDown

* Fix: use DCRTPoly::Params instead of private ParmType in helper

* Removed the helper function and simplified the fix

* Additional changes for the cached params

* cached params optimizations

---------

Co-authored-by: Dr. Amneek Singh <amneek@amneek.tech>
Co-authored-by: Dmitriy Suponitskiy <dsuponitskiy@dualitytech.com>
Co-authored-by: Carlo Pascoe <cpascoe@dualitytech.com>
* Corrected exception handling for COMPOSITESCALING*

* Corrected unittests

---------

Co-authored-by: Dmitriy Suponitskiy <dsuponitskiy@dualitytech.com>
* feat: expose ClearBootstrapPrecom to release cached bootstrap data (#533)

FHECKKSRNS::m_bootPrecomMap is populated by EvalBootstrapSetup and EvalBootstrapPrecompute but never released by the existing
ClearEval*Keys or ReleaseAllContexts APIs when a user still holds a CryptoContext reference, so the documented "clear" paths leave that memory alive for the lifetime of the process.

Add a virtual FHEBase::ClearBootstrapPrecom() (default no-op),
override it in FHECKKSRNS to clear m_bootPrecomMap, and expose
matching forwarders on SchemeBase and CryptoContextImpl so callers can free the precomputations without destroying the context.

* feat: release CKKS scheme caches and trim allocator (#533)

Extends the initial ClearBootstrapPrecom work so callers can actually
drop every piece of scheme-level memory a CKKS CryptoContext retains,
without destroying the context itself.

- Add per-slot ClearBootstrapPrecom(slots) and ClearBootstrapPrecomExcept(keep)
  on FHEBase / SchemeBase / CryptoContextImpl so callers can evict a single
  transient precomputation or keep only a hot entry.
- Add HasBootstrapPrecom() inspectors (whole-map and per-slot) for tests and
  memory-diagnostic code.
- Introduce FHEBase::ClearSchemeSwitchPrecom() overridden by SWITCHCKKSRNS
  to drop the intermediate CKKS/BinFHE contexts, CKKS<->FHEW switching keys,
  auxiliary ciphertext, and the precomputed decoding-matrix plaintexts that
  EvalCKKStoFHEWSetup / EvalSchemeSwitchingSetup populate and which the
  existing Clear*Keys paths never released.
- Add CryptoContextFactory::ReleaseAllContextsAndTrim() which extends
  ReleaseAllContexts() by also invoking the scheme-level clears on every
  tracked context and asking the allocator to return pages to the OS via
  the new TrimAllocator() helper (malloc_trim on glibc, no-op elsewhere).
- Mark the cache-clear APIs noexcept so they are safe to call from
  destructors and shutdown paths.

Adds UnitTestCKKSCacheClear (15 tests) covering per-slot / keep-except /
full-release semantics, scheme-switch precom release, inspector agreement,
and the ReleaseAllContextsAndTrim path. Adds ckks-release-memory example
demonstrating the three common usage patterns.

Closes #533

* test: use 128-bit-compatible params in scheme-switch helper

The test helper used modulus sizes and a scaling style that only
work on 32/64-bit builds, so it threw on the 128-bit CI job.
Pick larger moduli and FIXEDAUTO scaling when NATIVEINT == 128,
matching what UnitTestSchemeSwitch.cpp already does.

Production code is unchanged.

* test: skip MemoryDropsAfterBootstrapClear when heap probe is unavailable

When the build links an alternative allocator like TCMalloc
(WITH_TCM=ON), mallinfo2() and malloc_zone_statistics() return 0
because they only see the system malloc that TCMalloc replaced.
The relative-drop check is unmeasurable in that case, so call
GTEST_SKIP() instead of failing the assertion.

The TrimAllocator contract test (TrimAllocatorIsSafe) still runs
and pins the no-throw + capability-flag behavior on every backend.

* Removed TrimAllocator(), s_autoReleaseMode and unnecessary functions.

* Simplified ckks-release-memory.cpp and some functions.

* Simplified UnitTestCKKSCacheClear.cpp

* Added debug statements

* Corrected unittest conditions

---------

Co-authored-by: Dmitriy Suponitskiy <dsuponitskiy@dualitytech.com>
* Modify default policy to keep large allocations on the heap and never auto-trim

* update Best_Performance.md

* Improve explanation of memory allocation policy

Clarify memory allocation behavior and performance impact.

---------

Co-authored-by: yspolyakov <89226542+yspolyakov@users.noreply.github.com>
…ep Horner BSGS and other LT optimizations (#1209)

* Reduce CKKS bootstrapping rotation-key footprint in the homomorphic FFT

Reformulate the baby-step/giant-step (BSGS) evaluation of the CoeffsToSlots
and SlotsToCoeffs linear transforms so that the same slot permutations are
realized with far fewer distinct rotation (automorphism) keys. The keys these
transforms need dominate the memory cost of a bootstrapping context, and
EvalBootstrapKeyGen now generates a smaller, more heavily shared set. The
underlying math is unchanged.

Core changes to EvalCoeffsToSlots / EvalSlotsToCoeffs and their precompute:

- Horner single giant-step. Replace the forward outer sum
  sum_i Aut_{i*t}(block_i) with the nested Horner form
  block_0 + Aut_t(block_1 + Aut_t(...)). Both use the same rotation count, but
  Horner needs only one giant-step key per level (stride t = g*scale) instead
  of the b-1 distinct keys {t, 2t, ..., (b-1)t}.

- Zero-based hoisted inner rotations. Replace the centered inner baby-step
  indices {(j-offset)*sigma} with zero-based {j*sigma}. The j=0 term is now
  always rotation 0 and is handled directly by KeySwitchExt (no key), and the
  per-level offset delta_s = offset*scale is pushed offline into the
  precomputed plaintexts as a pre-rotation.

- Folded per-level corrections. The per-level zero-basing corrections (an
  O(levels) set of runtime EvalAtIndex calls in both transforms) are commuted
  forward and absorbed into the precomputed plaintexts, leaving a single
  accumulated rotation applied once at the end of SlotsToCoeffs. EvalMod is
  equivariant under slot rotations, so the CoeffsToSlots correction passes
  through it unchanged.

- Split, not combined, accumulated correction. Apply Aut_{-(slots-1)} at the
  end of CoeffsToSlots so it always outputs correctly-ordered slots, rather
  than deferring a single combined correction to the end of SlotsToCoeffs.
  This fixes EvalFBTNoDecoding + EvalHomDecoding (FBT_CONSECLEV), where a user
  operation between the two transforms previously saw a residual rotation.

- Sparse-packing index reduction. Reduce all BSGS rotation indices modulo
  min(2*slots, M/4). Under sparse packing the precomputed-plaintext vector has
  cyclic period 2*slots (the concatenated real/imaginary blocks), not M/4, so
  indices reduced only to [0, M/4) could otherwise be inconsistent with the
  period-2*slots plaintext pre-rotations. For full and half packing the modulus
  equals M/4 and behavior is unchanged.

The single-level linear transform (EvalLinearTransform, used when the level
budget is 1) is converted to the same single-giant-step Horner form, and
FindLinearTransformRotationIndices no longer emits the giant-step keys
{2g, 3g, ..., (h-1)g} that the forward form required.

Supporting cleanups (no behavior change):
- Inline the precomputed rot_in index tables in EvalCoeffsToSlots /
  EvalSlotsToCoeffs (compute per level, drop the 2D allocation and a redundant
  scale pass).
- Fix the over-large reserve() in the Find*RotationIndices helpers (they
  reserved ~M entries for a list of a few hundred).
- Hoist a redundant KeySwitchExt out of the EvalLinearTransform giant-step
  loop, and hoist repeated GetParams()/GetElementAtIndex() calls in
  ExtendCiphertext.
- Take crypto parameters by const reference and compute the bootstrap scale
  factor with std::ldexp.
- Add Doxygen for the transform functions and a note in CKKS_BOOTSTRAPPING.md.

Not included: the SlotsToCoeffs decode-layer re-partition that moves the
remainder group to scale 1 to mirror CoeffsToSlots. The StC remainder still
sits at the last (largest-scale) position, so the additional key sharing that
re-partition enables (e.g. dropping the StC-specific large-scale remainder key)
is not realized here.

* Apply single-giant-step Horner BSGS to CKKS/FHEW scheme-switching transforms

* map-hoist + seed-peel of the Horner giant-step loop

* updated comments and simplified scheme-switching LT rotation-index generation to closed form
…he extended basis (#1225)

* unification of EvalFastRotation/EvalRotate/EvalAutomorphism/Conjugate

* lazy extended-basis accumulation (deferred ApproxModDown) for PartialSum, Horner CtS/StC, and scheme-switching linear transforms

* generalize EvalPartialSumInPlace into a radix-parameterized rotation fold

* add EvalPartialSumInPlace radix as a CMake option (CKKS_PARTIAL_SUM_RADIX, default 4)

* extend the radix knob to EvalSum, dedupe multiparty keygen, and rework the *Many operations

Renames CKKS_PARTIAL_SUM_RADIX to PARTIAL_SUM_RADIX: the knob is no
longer CKKS-specific because it now also drives the scheme-generic
EvalSum/EvalSumRows/EvalSumCols family. All four folds route through one
EvalSumRadixFold that, under HYBRID key switching, accumulates lazily in
the extended basis exactly like EvalPartialSumInPlace (one deferred
ApproxModDown for element 0, per-level settle for element 1, one digit
decomposition per level); a hoisted eager fallback covers non-HYBRID via
a new SchemeBase::EvalAutomorphismCore wrapper, needed because EvalSum
keys live in their own key map.

EvalAddMany becomes a null-tolerant left fold with a single clone
(addition is exact and associative, so the pairing tree bought nothing;
~1.5-2x vs dev, growing with tower count) and EvalAddManyInPlace an
actually-in-place serial fold with zero allocations (~1.8-2.3x); its
result lands in slot 0 per the in-place contract, and null-leading
vectors work for the first time. Fixes a size-1 aliasing bug in the
cryptocontext wrappers: EvalAddMany/EvalMultMany returned the caller's
input handle for single-element vectors, so mutating the result silently
corrupted the input; results never alias inputs now (EvalAddMany's early
return is dropped, EvalMultMany's becomes a keyless Clone).

Unifies EvalMultMany: the base tree picks levelsToDrop from
GetCompositeDegree() (1 everywhere except composite-scaling CKKS),
making the AdvancedSHECKKSRNS override redundant; it is removed. The
unified fold keeps the three-phase structure, clones for size 1 (also
fixing an empty-scratch-vector UB at the scheme layer), and frees each
consumed partial immediately: live intermediates drop from n-1 to ~n/2,
measured -46% peak scratch at n=32 with unchanged runtime and pairing
order (bit-identical).

* fix fully-packed EvalSum key coverage for smaller batches

The fully-packed BFV/BGV branch kept the legacy doubling index set, which
no longer covers EvalSum calls at smaller batch sizes once those fold at
the configured radix (radix-2 sets were nested across all batch sizes;
e.g. EvalInnerProduct with default keygen requested key 5^3). The
fully-packed path now radix-folds its first phase (covering batchSize/2)
with only the m-1 conjugation tail kept special, in both keygen and the
fold, making the fully-packed key set a superset of every smaller batch's
set again.

* Update cmake_in_openfhe.rst
…1229)

* made the multiparty limitation explicit in the scheme switching md

* Update memory consumption note in documentation

Clarified memory consumption explanation for scheme switching.

---------

Co-authored-by: yspolyakov <89226542+yspolyakov@users.noreply.github.com>
Co-authored-by: Dmitriy Suponitskiy <dsuponitskiy@dualitytech.com>
…composition (#1238)

* issue #1226 Avoid UB signed left shifts in RingGSWAccumulator::SignedDigitDecompose

* Decouple carry update from digit extraction in SignedDigitDecompose

The carry (d0 - r0) >> gBits equals (d0 + gHalf) >> gBits since the digit
r0 lies in [-gHalf, gHalf). The new form removes the extracted digit from
the loop-carried dependency chain (4 -> 2 cycles per digit) and drops the
now-unneeded digit extraction ahead of the loop. Output is bit-identical.

* Vectorize SignedDigitDecompose via digit-major loop interchange

Process one gadget digit at a time across the whole polynomial, carrying
the running quotient per coefficient in a scratch array. The inner loops
become unit-stride and auto-vectorize (AVX2/AVX-512) when built with
native optimizations; output is bit-identical to the previous order.

* Replace signed digit extraction with excess-half bias in SignedDigitDecompose

Bias each coefficient once by H (gHalf in every digit position); every
balanced digit then reads out as an independent unsigned window
((w >> k*gBits) & gMask) - gHalf. This removes the per-digit carry chain
and all signed-shift arithmetic, drops the SignExtend helper, and speeds
up the decomposition a further 1.2-1.8x over the digit-major form
(up to 10.6x total vs the original). Output is bit-identical. Requires
digitsG*gBits < MaxBits, satisfied by all supported parameter sets.
Co-authored-by: Dmitriy Suponitskiy <dsuponitskiy@dualitytech.com>
…ies (#1244)

Add a bulk NativeVector::BaseDecompose that extracts all base-2^digitSize
digits at once as balanced digits in [-B/2, B/2) of the centered
representative: the excess-half bias is applied once per coefficient and
every digit is then an independent shift-and-mask window, with the top
window absorbing the remaining quotient so reconstruction is exact mod q.
Decompositions whose bias would not fit the native word fall back to
unsigned windows. All predicates are computed as bit masks so the loops
auto-vectorize at the baseline ISA as well as with native optimizations.
Key material is unchanged; balanced digits halve the per-digit magnitude
bound, matching the relinBase/2 factor already assumed by the BGV noise
estimate, and reduce measured BV key-switching noise by ~0.9 bits
(stddev) / ~0.6 bits (max).
…1028) (#1247)

* Use one scalar multiplication in CKKS/BFV rescaling and vectorize NativeVector modular add/sub/SwitchModulus

* add missing edits
* fixes larger noise for some scenarios of composite scaling

* revised the comment

* Fixed ExtendCiphertext

* fixes the bug introduced in ExtendCiphertext

* fixed the precision issue when firstModSize > scalingModSize + 1

* reduced CS error for a deep ct x pt circuit

* reduced the drift in the scaling factor for deeper computations

* added unit tests for fixed CS bugs

* fixed deg > 5 bug

* generalized the key switching fix for composite scaling modes to all scaling techniques + added it to EstimateLogP

* Refactored ExtendCiphertext to move precomputations to CKKS Parameters RNS class

* disabled DeepComputationWithFillers for MATHBACKEND 2 and removed cout statements from CS-vs-FA unit tests

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cleanup

* composite scaling P margin: 4*d -> 6 + ceil(log2(d)) per the RNS-hybrid noise bound

---------

Co-authored-by: Yuriy Polyakov <ypolyakod@dualitytech.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Carlo Pascoe <cpascoe@dualitytech.com>
dsuponitskiy and others added 24 commits September 15, 2026 15:31
- Guard right shifts and truncate tiny scalar magnitudes toward zero. Apply left shifts modulo each CRT tower to avoid overflow,
  and restore negative multiplication factors using modular subtraction.
- Add regression coverage for shift boundaries, subnormal and large operands, complex scalars, and increased ciphertext scales.
- Validated with 115 CKKS tests and shift sanitization.

Co-authored-by: Dmitriy Suponitskiy <dsuponitskiy@dualitytech.com>
Co-authored-by: Dmitriy Suponitskiy <dsuponitskiy@dualitytech.com>
* Remove the K = 768 overflow bound for composite scaling bootstrapping (#1310)

The larger bound (K_UNIFORMEXT with the g_coefficientsUniformExt table) was
selected for UNIFORM_TERNARY with composite degree > 2, or degree 2 at ring
dimension >= 2^17. It compensated for the approximate RNS basis extension of
the original ExtendCiphertext, which inflated the post-ModRaise overflow by
~40%. With the exact HPS basis extension the composite overflow statistics
match the non-composite ones, so K = 512 suffices everywhere.

- remove K_UNIFORMEXT, g_coefficientsUniformExt, and the degree/ring-dimension
  switches in EvalBootstrap and EvalBootstrapStCFirst
- remove the mirrored UNIFORM_TERNARY composite restriction in
  EvalFEFuncBootstrapSetup
- document the uniform K = 512 bound in COMPOSITE_SCALING.md
- add FEFBT unit tests for composite degree 3 with UNIFORM_TERNARY

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Update ckksrns-fhe.cpp

---------

Co-authored-by: Yuriy Polyakov <ypolyakod@dualitytech.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: pascoec <123595534+pascoec@users.noreply.github.com>
…gnment (#1314)

* Fix signed integer overflow and modular conversion in polynomial assignment

* Fixes #1255

---------

Co-authored-by: Dmitriy Suponitskiy <dsuponitskiy@dualitytech.com>
Co-authored-by: Carlo Pascoe <cpascoe@dualitytech.com>
#1320) (#1321)

57 bits is below the m*q^2 ~ 2^58.8 convolution bound for 24-bit towers at
m = 1811, so 2 of the 720 randomly drawn automorphism generators overflowed
and the tests failed about 1 in 720 runs each.
* Add a pluggable diagnostic output channel

Route the library's diagnostic writes through OPENFHE_LOG_ERR /
OPENFHE_LOG_OUT (each a std::ostream&) instead of std::cerr / std::cout
directly, so an embedding application can redirect OpenFHE's diagnostics
without editing call sites. Because the macros expand to std::ostream&,
every operator<< overload and manipulator keeps working unchanged.

The default sink (std::cerr / std::cout) lives in one translation unit,
openfhe_log_default.cpp, compiled only when the new OPENFHE_DEFAULT_LOG_SINK
CMake option is ON (the default). With the default sink ON the behavior is
byte-for-byte identical to before. Embedders may redirect at runtime via
SetOpenFHEErrStream / SetOpenFHEOutStream, or build with
-DOPENFHE_DEFAULT_LOG_SINK=OFF and supply their own accessor definitions to
keep std::cerr / std::cout out of the compiled archive entirely -- e.g. an R
package, where CRAN forbids compiled code that writes to stdout/stderr.

Converts the existing std::cerr / std::cout diagnostic sites in core,
binfhe, and pke to the new macros. No functional change under the default
configuration.

* Clarify diagnostic output naming and simplify documentation

* Complete the diagnostic output migration and fix the sink-off build

Several diagnostic sites still bypassed the channel. ckksrns-fhe.cpp kept
one std::cerr in the sparse-slots branch of EvalBootstrap, and the debug.h
OPENFHE_DEBUG*/PROFILELOG* macros, ubintntl.h, cryptocontext.h and
matrixstrassen-impl.h -- all headers compiled into the library or installed
for downstream use -- were never converted. Routing them through the channel
is what makes the stated goal measurable: with
-DWITH_DEFAULT_DIAGNOSTIC_SINK=OFF, nm now reports zero cerr/cout references
in libOPENFHEcore, libOPENFHEpke and libOPENFHEbinfhe -- the condition
an embedder such as an R package on CRAN has to meet.

That configuration also did not link. Nothing defines the accessors when the
sink is off, so every executable failed at link time. The built-in sink is
now compiled into OpenFHE's own executables through an INTERFACE target,
OPENFHEdiagnosticsink, kept out of the library and not installed. INTERFACE
rather than static, because ld will not extract an archive member to
satisfy an undefined symbol that comes from a shared library. This also
restores the note that OFF is meant for static builds and is rejected when
the library is created on macOS and Windows.

Output returns to its pre-refactor form. Two "Decoding time" messages had
moved from the error to the output channel, splitting one BOOTSTRAPTIMING
report across two differently buffered streams, and two "Encoding time"
messages had gained a leading newline their third sibling did not have. A
TIC(t) added at the tail of EvalBootstrapStCFirst is dropped: that function
has already run SlotToCoeff and returns immediately after, so the timer was
never read.

WITH_DEFAULT_LOG_SINK becomes WITH_DEFAULT_DIAGNOSTIC_SINK to match the
header, macros and namespace, and is added to the CMake summary and the
documented option table. SetOpenFHEErrStream/SetOpenFHEOutStream now return
the stream they replaced, so a caller can restore it.

Adds UnitTestDiagnosticOutput covering per-channel capture, manipulators and
the returned previous stream, and a pre-commit hook,
scripts/maint/check-diagnostic-output.py, that rejects direct stdout/stderr
writes in library sources and installed headers so the invariant cannot be
reintroduced unnoticed.

* update after rebase

---------

Co-authored-by: Balasubramanian Narasimhan <b.naras@gmail.com>
Co-authored-by: Dmitriy Suponitskiy <dsuponitskiy@dualitytech.com>
Co-authored-by: Carlo Pascoe <cpascoe@dualitytech.com>
- Casting through int64_t flipped the sign of positive centered coefficients above INT64_MAX and truncated anything wider. Use ConvertToDouble() instead.
- The DCRTPoly constructor now checks its first-tower shortcut against the remaining towers and falls back to CRT interpolation if a coefficient does
not fit. It also rejects an empty DCRTPoly instead of indexing into it.
- Added regression tests.

Co-authored-by: Dmitriy Suponitskiy <dsuponitskiy@dualitytech.com>
* Replace the K = 25 sparse-secret approximations with K = 28 and select K by the first modulus size

The mod-raise overflow bound of the sparse secret distributions is now chosen
by the failure probability of the Irwin-Hall model (eprint 2022/024, eq. (1)),
which depends only on K and the Hamming weight h of the sparse secret:
- SPARSE_ENCAPSULATED uses K = 16 with h = 32 (first modulus of at most 60
  bits; failure probability below 2^-137 for 2^16 slots) and the K = 28
  tables of SPARSE_TERNARY with h = 64 (first modulus above 60 bits; below
  2^-142 for 2^16 slots). The Hamming weight is fully determined by the size
  of the first modulus (CryptoParametersCKKSRNS::SparseKSHammingWeight) and
  no longer by the composite degree.
- SPARSE_TERNARY functional bootstrapping moves from K = 25 to K = 28
  (h = 192, about 2^-22 for 2^16 slots), the bound already used by regular
  bootstrapping.

K_SPARSE_ALT = 25 and g_coefficientsSparseAlt are removed. The K = 25 FBT
tables are replaced by K = 28 tables with the smallest degrees matching the
previous precision: exp degree 64 (2^-20) and 72 (2^-32), cos degree 64
(2^-21); the degree-64 tables need one more level than their degree-58
predecessors.

EvalFBTSetup now derives its depth from the actual Hamming weight of the
sparse secret (AdjustDepthFBTInternal) instead of the public estimate, which
could not know it; GetFBTDepth, GetFEFBTDepth and AdjustDepthFBT take the
first modulus size instead of the composite degree.

Tests: FBT rows for SPARSE_ENCAPSULATED with composite scaling cover both
Hamming weights; a new FEFBT case covers the 64-weight secret; docs updated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Fix the 14-bit LUT errors and the modular-reduction precision loss at K = 28

The exponential table of functional bootstrapping and the two approximate
modular-reduction tables were selected by approximation error alone, which
is the wrong criterion for the first and too weak for the other two.

coeff_exp_28_double_72 becomes coeff_exp_28_double_69. The digit errors of
the 14-bit LUTs are set by the Paterson-Stockmeyer decomposition rather than
by the approximation error: ComputeDegreesPS maps degrees 63-69 to
(k, m) = (10, 3) and degrees 70-76 to (11, 3), and GetDepthByDegree is 8 for
both. At (11, 3), rows 22-24 of UTCKKSRNS_FBT (SPARSE_TERNARY, 14-bit LUT,
58-bit modulus, ring dimension 2^17) fail; row 22 misses 174 of its 131072
values by one digit at degree 72, and equally at degree 76, at degree 80
((6, 4)), and at K = 25, although those tables are up to twelve bits more
accurate. At (10, 3) the three rows are exact. Degree 69 also needs one
power-basis element fewer than degree 72.

g_coefficientsSparse goes from degree 44 to 48 and
g_coefficientsSparseEncapsulated from degree 32 to 36. At degree 44 the
K = 28 table approximates the modular reduction to 2^-30.8 and the K = 16
table at degree 32 to 2^-33.5, against 2^-38.5 for the K = 25 table they
replace. Both new degrees stay within the depth-7 Paterson-Stockmeyer range,
so the bootstrapping depth is unchanged. At ring dimension 2^16 with
composite scaling (89-bit first modulus, 78-bit scaling factor) the
bootstrapping precision improves from 16.4 to 24.1 bits for
SPARSE_ENCAPSULATED and from 16.2 to 23.6 bits for SPARSE_TERNARY, for a
bootstrapping time within 3% of the previous tables.

* Use one exponential table for every LUT digit size and raise the cosine degree

Both K = 28 tables of functional bootstrapping sat below the accuracy
available at their depth.

coeff_exp_28_double_64 is removed and the digitBitSize > 10 branch with it.
It selected between two tables of the same Paterson-Stockmeyer depth (8), so
the lower degree bought nothing: LUTs of at most 10 bits now approximate
exp(2 Pi i x) to 2^-27.4 instead of 2^-20.0 after the double-angle
iterations, with one table and one branch fewer.

coeff_cos_28_double goes from degree 64 to 68, improving cos(Pi x) from
2^-21.0 to 2^-26.9 after its double-angle iteration. 68 is the largest even
degree whose Paterson-Stockmeyer parameters are (k, m) = (10, 3).

Both tables keep k = 10, so the power basis, the multiplication count and
the depth are unchanged. Neither degree is the most accurate available at
depth 8 -- degree 76 is, for both -- but 76 falls in the (11, 3) cluster
that produces the 14-bit LUT digit errors described in the previous commit.

The cosine tables gain the degree suffix the exponential tables already
carry: coeff_cos_28_double_68, coeff_cos_16_double_50 and
coeff_cos_512_double_92. The degree governs the Paterson-Stockmeyer
parameters, so it belongs at the call site.

---------

Co-authored-by: Yuriy Polyakov <ypolyakod@dualitytech.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Carlo Pascoe <cpascoe@dualitytech.com>
…1319)

* Keep precomputed powers intact in linear series evaluation (#1312)

Below degree 5, EvalPolyWithPrecomp and EvalChebyshevSeriesWithPrecomp
evaluated the series straight from the power (or Chebyshev) basis and
scaled the basis ciphertexts in place. Any caller sharing a precomputation
across several series got a correct first result and silently wrong ones
afterwards. EvalMVB/EvalMVBNoDecoding hit this for Hermite coefficient
vectors of degree 3 (p = 4 at order 1, p = 2 at orders 2 and 3); the
seriesPowers repackaging in EvalMVBNoDecodingInternal shares the same
CiphertextImpl objects, so the corruption reached the caller's powers.

Scale each lower-order term into a temporary instead. EvalMult on a
scalar is Clone plus in-place multiply, so this costs one ciphertext copy
per nonzero lower-order term (at most k-1, never the top term), fewer
copies than guarding in the callers, and it also fixes the two public
APIs. Measured at N = 2^15 the extra copies are under 1 ms per function
against a 100-300 ms series evaluation.

- take the basis by const reference in internalEvalPolyLinearWithPrecomp
  and internalEvalChebyshevSeriesLinearWithPrecomp
- drop the now redundant deep copies in EvalFEFuncBootstrapWithPrecomp
- document that EvalPolyWithPrecomp/EvalChebyshevSeriesWithPrecomp leave
  the precomputation unmodified
- extend UnitTest_MVBReuse to evaluate f1, f2, f1 on one precomputation
  and add p = 4 rows (FIXEDMANUAL full/sparse, FLEXIBLEAUTO,
  COMPOSITESCALINGAUTO), which fail without the fix
- add EVAL_PRECOMP_REUSE to UnitTestPoly for the generic power-basis and
  Chebyshev precomputation APIs

* comments on reusing powers + tiny FIXEDMANUAL optimization

* edits after rebase

---------

Co-authored-by: Andreea Alexandru <aalexandru@dualitytech.com>
…sis conversions (#1322)

* Request consistent OpenMP team widths at DCRTPoly format switches

Consecutive parallel regions that ask for different team sizes make the
OpenMP runtime reconfigure its team, and libgomp charges heavily for it
(issue #1300). DCRTPolyImpl::SwitchFormat was the most frequent offender: it
ran the per-tower loop at the full team count, while the
regions around it in the same operation ran at the tower count or at one of
the tuned caps on the ring-dimension loops.

- ParallelControls::GetThreadLimit clamps to [1, limit]
- SwitchFormat resolves thread_limit == 0 to the tower count, which is what
  the tower-parallel regions around it use. An explicit request is honoured
  as given rather than clamped to the tower count, so a caller can align
  with a wider neighbour.
- The three measured widths for the loops over the ring dimension, written
  until now as the literals 8, 16 and 36 at fourteen sites, are named
  DCRTPolyImpl constants:
    THREADS_SCALE_TO_POLY            8   ScaleAndRound to a single NativePoly
    THREADS_CRT_BASIS_SWITCH        16   SwitchCRTBasis, ScaleAndRound,
                                         ApproxScaleAndRound, CRTInterpolate
    THREADS_APPROX_CRT_BASIS_SWITCH 36   ApproxSwitchCRTBasis
  The values are unchanged.
- Format switches that sit beside one of those loops request its width:
  the internal switch in ExpandCRTBasis, ExpandCRTBasisReverseOrder and
  ApproxModUp; the two switches in KeySwitchHYBRID::EvalKeySwitchPrecomputeCore
  around ApproxSwitchCRTBasis; and BFV's HPS-family switches around
  ScaleAndRound, SwitchCRTBasis and the basis expansions in EvalMult,
  EvalSquare, EvalAutomorphism and EvalFastRotation. BEHZ sites keep the
  tower-count default because their neighbours are tower loops.
- SwitchFormat runs its loop serially when called from inside an active
  parallel region (ParallelControls::InParallelRegion). CRTDecompose switches
  every digit from within its tower-parallel loop, which opened a one-thread
  nested team per digit per tower on every BV key switch.

EvalMult on a 2^14 ring, icelake, time relative to dev (min of 3 medians;
lower is faster, 0.5 = twice as fast):

  36 threads, gcc-14/libgomp    BFV 0.49 .. 0.60
                                BGV/CKKS with BV key switching 0.84 .. 0.95
                                HYBRID 0.99 .. 1.01
  36 threads, clang-18/libomp   BFV 0.84 .. 0.94
                                BGV/CKKS with BV 0.94 .. 1.00
                                HYBRID at parity within the run-to-run band
  8 threads                     0.97 .. 1.00 on every path
  1 thread                      1.00 everywhere

EvalRotate moves with it: BV 0.80 .. 0.95 and BFV 0.68 .. 0.87 at 36 threads
under gcc. On dev, BFV EvalMult under gcc ran slower at 36 threads than at 8;
it no longer does. CKKS bootstrapping (benchmark configs 7, 15, 17 at 36
threads) is unchanged at 1.01 under gcc and 0.99 under clang. Capping the
switches to the tower count without the aligned call sites cost HYBRID
10-23% and BFV at 8 threads 3-7%, so the alignment ships with the mechanism.
The nested-region guard measures neutral and is kept for the regions it
removes.

* Accumulate the BV key switch over towers and digit groups

KeySwitchBV::EvalFastKeySwitchCore ran its parallel region over the digits,
cloning the key's towers for every digit, multiplying in place, and then
summing the digit products serially in a loop the same size as the work it
followed. The region is now over (tower, digit group) tasks: the digits are
split into as many groups as fit the team beside the tower count, each task
accumulates one tower over one group without any clone, and the groups are
summed per tower over coefficient ranges so that the reduction also uses the
whole team. With a team no wider than the tower count this degenerates to
one thread per tower and no reduction, which is the shape
KeySwitchHYBRID::EvalFastKeySwitchCoreExt already has. Modular accumulation
is exact, so the result is bit-identical, and it carries the ciphertext's
own parameter object rather than one cloned from the key.

Two simpler forms were measured and rejected. A pure tower-major loop gains
3-20% at one thread but loses 3-12% at 8 and 36 threads on 2-4 towers,
because for BV the digit count is all digits across towers (about 18 for
three 60-bit towers at a 10-bit base) and was the wider axis there. Adding
the digit groups but reducing per tower recovers 8 threads and leaves 2-6%
on the 2-4 tower cells at 36 threads, where only tower-count threads sum
the partials.

The two format switches in the sparse-encapsulation key switch and modulus
raise of CKKS bootstrapping that sit beside SwitchCRTBasis request
THREADS_CRT_BASIS_SWITCH like their neighbours elsewhere.

EvalMult on a 2^14 ring, icelake, time relative to the previous commit (min
of 3 medians; lower is faster):

  1 thread                      BV paths 0.80 .. 0.98 (both compilers)
  8 threads                     BV paths 0.87 .. 1.01
  36 threads, gcc-14/libgomp    BV 6-7 towers 0.86 .. 0.92
                                BV 2-4 towers 0.99 .. 1.03
  36 threads, clang-18/libomp   BV 0.78 .. 1.04
  HYBRID paths                  unchanged (not touched)

EvalRotate moves further, to 0.71 at 36 threads on 7 towers. The one-thread
gain is the removed clones and reduction passes. Two gcc cells at 36 threads
with 3-4 towers remain 2-3% slower; there the 3-4 towers give too few tasks
to hide the partial-sum traffic.

CKKS bootstrapping (benchmark configs 7, 15, 17 at 36 threads) is unchanged
with the aligned sparse key-switch switches: 0.99 .. 1.02 under gcc, 1.00
under clang.

core_tests and pke_tests pass under both compilers.

* Size the ApproxSwitchCRTBasis team from the work in its loop

ApproxSwitchCRTBasis capped its loop over the ring dimension at 36 threads.
That width was measured on bootstrapping, where the loop converts 20 to 30
towers of a 2^16 ring, and it is right there. On a 2^14 ring converting a
2-tower digit the same loop is 200 times shorter, and running it at 36
beside regions 4 to 8 wide costs HYBRID key switching more than the loop
itself: the sweep over all caps in {4, 8, 16, 36} across rings 2^13 to 2^16
put the optimum at 4 to 8 for every small case and at 36 only for the
bootstrapping-scale ones. The quantity that separates them is the work,
ring dimension times input towers times output towers.

DCRTPolyImpl::ApproxSwitchCRTBasisThreads(ringDim, sizeQ, sizeP) returns one
thread per 2^17 multiply-accumulates, clamped to [8, 36]. ApproxSwitchCRTBasis
uses it for its loop, and the format switches aligned to that loop (inside
ApproxModUp and the two in KeySwitchHYBRID::EvalKeySwitchPrecomputeCore) ask
for the same value, so the alignment holds at every size. The floor of 8
keeps behaviour with 8 or fewer threads unchanged. The divisor and the
bounds were fitted on icelake at 36 threads; the other two caps (8 for the
scale-and-round to a single polynomial, 16 for the exact basis switches)
measured near-optimal in the same sweep and are unchanged.

EvalMult at 36 threads relative to the fixed cap (min of 3 medians; lower is
faster):

  gcc-14/libgomp    HYBRID key switching   2^13 0.50   2^14 0.66   2^15 0.80   2^16 (7 towers) 0.95
                    BFV (via FastExpandCRTBasisPloverQ)   0.58 .. 0.85   0.72 .. 0.94   0.87 .. 0.97
  clang-18/libomp   HYBRID                 2^13 0.83   2^14 0.91   2^15 ~1.0
  8 threads         1.00 everywhere (same requests as before)

EvalRotate with HYBRID keys moves further: 0.37 at 2^13, 0.51 at 2^14, 0.71
at 2^15 under gcc. BV paths, which never reach this loop, are unchanged.
CKKS bootstrapping (benchmark configs 7, 15, 17 at 36 threads), where the
large key switches still resolve to 36: 0.96 .. 0.99 under gcc, 1.00 under
clang.

core_tests and pke_tests pass under both compilers.

* Cap the BV key-switch task count by the work available

The (tower, digit group) split in KeySwitchBV::EvalFastKeySwitchCore sized
its team from the thread limit alone, so a 2^13 ring with four towers at 72
threads spawned 72 tasks of a few microseconds each across two sockets and
ran rotations 1.4x slower than the digit-major loop it replaced. The team is
now also bounded by one task per 2^15 coefficient multiply-accumulates.

EvalMult (rotate) relative to the uncapped form, icelake, BGV with BV keys
and 4 towers (lower is faster):

  2^13   gcc 36t 0.85 (0.76)   gcc 72t 0.84 (0.59)   clang 36t 0.92 (0.90)
  2^14   gcc 36t 0.94 (0.92)   gcc 72t 0.94 (0.79)   clang 36t 0.89 (0.85)
  2^15   1.00 everywhere; 18 threads and below unchanged

Other paths within 0.93 .. 1.01; HYBRID paths do not use this code.
core_tests and pke_tests pass under gcc-14 and clang-18.

* Move the thread-width constants to DCRTPolyInterface and tighten the BV guard

* remove unused DCRTPolyInterfaceType::THREADS_APPROX_CRT_BASIS_SWITCH
* Raise the mod-raise overflow bound K for uniform ternary secrets at unchanged depth

The uniform ternary approximations now use K = 648 for regular bootstrapping,
K = 672 for functional bootstrapping, and K = 696 for FE functional
bootstrapping. The probabilities of failure for fully packed slots (normal
approximation of the overflow, Hamming weight ~2N/3) are 2^-71 / 2^-77 / 2^-84
for N = 2^16 and 2^-28 / 2^-31 / 2^-34 for N = 2^17.

Each K is the largest value whose Chebyshev approximation keeps its precision
without an additional level of multiplicative depth. The regular and
functional bootstrapping tables use degree 104: higher degrees within the
same depth bracket (106 to 119, and exactly 105) fall into a noisier regime of
the Paterson-Stockmeyer evaluation that costs about 3 bits of precision, so
the cap is documented next to the constants. The FE functional bootstrapping
table uses degree 27.

- g_coefficientsUniform: K = 648, degree 104 (approximation error 2^-36)
- coeff_exp_672_double_104 / coeff_cos_672_double_104: functional
  bootstrapping tables, six / five double-angle iterations (2^-37 / 2^-34)
- coeff_exp_696_double_27 with R_func_696_double_27 = 9: FE functional
  bootstrapping table (2^-30)
- K_UNIFORM, K_UNIFORM_FBT, K_UNIFORM_FEFBT select the bound per flavor

Measured bootstrapping precision is unchanged or better (regular FIXEDAUTO at
N = 64: 15.5 -> 16.8 bits; functional bootstrapping noise unchanged; FE
functional bootstrapping within 0.5 bit). Docs and test comments updated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Quote the Security Guidelines failure probabilities for the uniform ternary K values

The probabilities of failure in the comments and docs are now the ones given
by the estimator of the Security Guidelines for Implementing Homomorphic
Encryption (Appendix A: Irwin-Hall model of the mod-raise overflow with the
correction for the spread of the Hamming weight), which are slightly more
conservative than the plain normal approximation quoted before:
K = 648: 2^-67 (N = 2^16) / 2^-27 (N = 2^17); K = 672: 2^-73 / 2^-30;
K = 696: 2^-79 / 2^-33.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Encode degree-2 constants at the scale of the dropped prime for the FIXED scaling techniques

Closes #1333.

With FIXEDMANUAL and FIXEDAUTO the rescale divides by the actual prime q_l of the dropped
tower, not by the scaling factor Delta, so a constant added to a degree-2 ciphertext as
c * Delta^2 came out as c * Delta / q_l. The residual (about 2^-33 for 50-bit primes) is a
constant shift of T_2 = 2x^2 - 1 that the squaring chain of the Paterson-Stockmeyer giant
steps amplifies by roughly the square of the degree. FIXEDAUTO adds every constant of the
Chebyshev recursion before its automatic rescale and lost about 4 bits on the degree-119
bootstrapping polynomial and 5 bits on EvalMod compared with FIXEDMANUAL.

The per-degree factor of a constant is now the modulus of the tower that the corresponding
rescale drops (both NATIVEINT variants of GetElementForEvalAddOrSub, shared helper), and the
FIXEDMANUAL plaintext degree raise in AdjustForAddOrSubInPlace uses the same moduli.

AdjustLevelsAndDepthInPlace gets a FIXEDAUTO-specific path: the tracked scaling factor and
GetModReduceFactor are exactly Delta for the FIXED techniques, so every FLEXIBLE-style
"multiply by Delta, then rescale" maneuver left a Delta / q_l factor on one operand and spent
a scalar multiplication and a rescale for nothing. A degree-1 ciphertext is now truncated to
the towers of the other operand (as in FIXEDMANUAL), a degree-2 ciphertext is rescaled when
the other operand has degree 1, and a degree raise multiplies by the target's last modulus.

Measured at N = 2^12 with 50-bit primes (max error over 2048 slots):
  degree-119 series: FIXEDAUTO 2^-23.9 -> 2^-27.7 (FIXEDMANUAL 2^-27.6)
  series + 6 double-angle iterations: FIXEDAUTO 2^-14.6 -> 2^-19.7 (FIXEDMANUAL 2^-19.7)

New regression test UTCKKSRNS_FIXED_SCALING.DegreeTwoConstantsChebyshev119 requires at
least 26 bits for both techniques; it fails at 2^-24 without the fix. All 2064 pke unit
tests pass; FLEXIBLE* and COMPOSITESCALING* are unchanged. Three pre-existing C-style casts
in the 128-bit constant encoder are converted to static_cast to satisfy cpplint.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* edits after rebase

* Unify FIXEDMANUAL and FIXEDAUTO operand alignment in AdjustLevelsAndDepthInPlace

Both FIXED scaling techniques use the scaling factor Delta at every level, so
two operands of equal noise scale degree only have to agree on the number of
towers: the one with fewer towers dropped is level-reduced, and nothing else is
needed. The FIXEDAUTO branch now handles only the degree mismatches (a degree-2
operand is rescaled, a degree-1 operand is raised by the modulus of the target's
last tower); a degree mismatch under FIXEDMANUAL keeps the general path.

Previously FIXEDMANUAL fell through to the FLEXIBLE-style path for every
ciphertext-ciphertext addition or subtraction with mismatched levels, which
multiplied one operand by Delta/Delta/Delta and rescaled it to reach the level
a truncation reaches directly, spending a scalar multiplication and a rescale
and leaving a Delta/q_l residual on the operand.

The four basis-alignment switches in ckksrns-advancedshe.cpp (the linear and
Paterson-Stockmeyer power and Chebyshev bases) that special-cased FIXEDMANUAL
with LevelReduceInPlace call AdjustLevelsAndDepthInPlace unconditionally.

---------

Co-authored-by: Yuriy Polyakov <ypolyakod@dualitytech.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Carlo Pascoe <cpascoe@dualitytech.com>
Co-authored-by: Dmitriy Suponitskiy <dsuponitskiy@dualitytech.com>
* Review the lint configs and rerun pre-commit over the tree (#1170)

Pin every tool version in .pre-commit-config.yaml and let pre-commit download
them, so a contributor installs only pre-commit and everyone formats with the
same clang-format and cpplint. Previously both ran as language: system, so the
result depended on whatever happened to be installed. The hooks repository moves
to v6.0.0, which renames check-byte-order-marker to fix-byte-order-marker.

.clang-format keeps the existing style. The three pre-12 boolean spellings are
replaced by the enum equivalents, which produce identical output, and
Standard: c++17 is added to match CMAKE_CXX_STANDARD.

CPPLINT.cfg stops filtering build/include_order, build/header_guard and
build/include_what_you_use, sets linelength to match ColumnLimit, and filters
three categories that are new in cpplint 2.x.

Enabling those three checks reported 1780 findings, all fixed here:

  - 239 header guards renamed to the path-based form. 12 guards nested inside a
    WITH_BE2, WITH_BE4 or WITH_NTL check now wrap the whole file.
  - 174 files given the standard headers they use.
  - 428 include blocks regrouped as own header, C system, C++ system, other.
  - explicit added to six single-argument constructors in binfhe.

Requiring each implementation file to include its own header first exposed a
real defect. mubintvecfxd.h and mubintvecntl.h use lbcrypto::BigVectorInterface
without including math/hal/vector.h, relying on their includer to pull in
math/math-hal.h first, which broke the MATHBACKEND 2 and 6 builds with 120
errors. Both headers now include it. mubintvecdyn.h already did, which is why
the default backend 4 build never showed the problem.

external-prng.cpp was not a clang-format fixed point: a banner comment following
a pragma with a trailing comment shifted right on every pass. Its comments now
sit above the pragmas, and the headers both arms of the conditional use are
hoisted out of it.

The contributing documents drop the instructions to install clang-format and
cpplint and the claim that the format was tested with clang-format 9.0.0. The
style guide documents the guard naming rule and the include grouping that are
now enforced. scripts/maint/apply-code-format.sh ran a bare find over the whole
tree, including build directories, with a meaningless --assume-filename; it now
runs the pinned hook.

Verified with builds and the full unit test suite on MATHBACKEND 2, 4 and 6,
under both g++ and clang.

* Make the bootstrap cache-clear test independent of the host thread count

UTCKKSCacheClear.FullBootstrapClear compares heap-in-use before
EvalBootstrapSetup against heap-in-use after ClearBootstrapPrecom and
ReleaseAllContexts, and expects the latter to be no larger. It took that
baseline before any bootstrap setup had run, so the baseline excluded the
per-thread allocator structures that the first setup creates and never returns.

That growth is proportional to the OpenMP thread count:

    threads      8      16      32      72
    growth     77 KB  136 KB  251 KB  543 KB

Above roughly 32 threads it exceeds the memory ReleaseAllContexts gives back, so
the comparison inverts and the test reports a leak that is not there. The test
therefore passed on an 8 or 16 core machine and on CI, and failed on a 72 core
one. It is not a regression; a build predating this branch fails the same way.

Running one setup and clear before the baseline is taken leaves the allocator in
its steady state. A second cycle then returns to within a kilobyte of its own
baseline, and the test passes at 1, 8, 16, 32, 64, 72 and 128 threads.

SchemeSwitchPrecomClear needs no equivalent change. It generates keys before
taking its baseline, so its arenas are already warm; its measurements are
byte-identical at 16 and 72 threads and it has about a megabyte of margin.

* Sort includes with clang-format and make two headers self-contained

.clang-format set SortIncludes: false, so include order was maintained by hand
and checked only by cpplint. Setting SortIncludes: CaseSensitive with the
Google default IncludeBlocks: Regroup lets the formatter maintain it instead.
Regroup's categories are the order cpplint already requires, namely the header
a file implements, C system headers, C++ system headers, then everything else,
so the two tools agree and cpplint becomes a backstop rather than the thing a
contributor has to satisfy by hand. 185 files are reordered as a result.

Sorting exposed two headers that name a type they do not include and that
happened to work only because a caller included something else first:

  - ilelement.h uses NativeInteger. It compiled because callers included
    lattice/lat-hal.h ahead of it; sorting puts lattice/ilelement.h first. It
    now includes math/hal/nativeintbackend.h.
  - gen-cryptocontext-params-defaults.h uses SecretKeyDist. It compiled because
    gen-cryptocontext-params.h was included ahead of it. It now includes
    lattice/constants-lattice.h.

Without those two includes the build fails with 18 errors, so both are defects
in their own right rather than fallout from the reordering.

Verified with builds on MATHBACKEND 2, 4 and 6 and with clang, and with the full
unit test suite.

* Include the standard headers each file uses

cpplint's build/include_what_you_use check works from a table of about thirty
symbols. It never asks for <cstdint>, so satisfying it left most files still
naming standard types they did not include and relying on a transitive path
through a project header. A file like that breaks when an unrelated header
changes what it includes, which is what happened to ilelement.h and
mubintvecfxd.h earlier on this branch.

This adds the missing headers across 348 files, found by scanning for a wider
set of unambiguous symbols:

    <cstdint>   339    <initializer_list>  15    <fstream>       5
    <cmath>      70    <type_traits>       14    <functional>    3
    <complex>    50    <random>            12    <mutex>         2
    <sstream>    38    <iomanip>            6    <cstddef>       2
                       <numeric>            6    <iterator>      2
                                                 <cstdlib>       1

Two related fixes:

  - scheme-utils.h had no includes at all while using uint32_t.
  - UnitTestTransform.cpp included "random" in quotes, which names a file in
    the project's include path rather than the standard header. The correct
    <random> is now present and the quoted form is removed.

The vendored blake2 sources are left alone; they are built as C, where <cstdint>
does not exist.

Verified by building MATHBACKEND 2, 4 and 6, clang, and NATIVE_SIZE 128.

* Check the pre-commit hooks in CI

Nothing enforced the lint configuration: the hooks ran only for contributors who
had installed them, which is how the tree came to need 1780 fixes. This adds a
job that runs them on every pull request and on pushes to main and dev.

It runs on a GitHub-hosted runner rather than the build machines, because it
needs only Python and network access and finishes in about a minute. Change
runs-on to use the RUNNER variable if the build machines are preferred, but note
that pre-commit needs pip and outbound network to fetch the pinned tools.

The job checks and does not push. On failure it prints the diff the hooks would
apply, along with the command to reproduce it locally. Pushing fixes
automatically is not possible for pull requests from organization-owned forks,
where the token is read-only, so it would silently skip part of the traffic.

--all-files rather than only the files a pull request touches, so drift anywhere
in the tree is reported rather than accumulating until someone reruns the hooks
over everything.

* Pin the lint runner image

ubuntu-latest tracks whichever Ubuntu release GitHub currently points it at, and
that target moves. Naming the image keeps the job on one release until it is
changed deliberately.

* Indent wrapped parameter lists instead of aligning them

AlignAfterOpenBracket was Align, which aligns a wrapped parameter list under the
open bracket when that fits inside the column limit and falls back to breaking
after the bracket when it does not. The shape of a declaration therefore depended
on how long the function name happened to be, and neighbouring functions in the
same file were laid out differently.

DontAlign with ContinuationIndentWidth 8 gives every wrapped list the same shape:
the first parameter stays on the declaration line and the rest indent by a fixed
amount. The setting applies to all brackets, so calls and conditions wrap the same
way; clang-format has no way to distinguish declarations from calls here.

Three cpplint findings surfaced because re-wrapping moved code onto different
lines:

  - UnitTestFixedScalingConstants.cpp uses std::max without <algorithm>.
  - UnitTestMatrix.cpp uses std::make_shared without <memory>.
  - In ubintntl.h a // NOLINT sat on a line carrying two is_same checks. Wrapping
    split them, leaving the second without its suppression, so the comment is now
    on the line it applies to.

Verified by building MATHBACKEND 2, 4 and 6, clang, and NATIVE_SIZE 128.

* Fix a heap overflow when serializing NTL integers

myZZ::save took its byte count from _ntl_ALLOC, which returns NTL's packed
alloc_ field rather than a limb count: the low two bits are flags and the
capacity is that field shifted right by two. Multiplying the packed value by a
pointer size and reading from the start of the body ran past the allocation by
roughly a factor of four. AddressSanitizer reports a 256-byte read off the end
of an 80-byte block.

Every binary serialization of an NTL integer does this, so the overflow is
constant. Whether it faults depends on whether the bytes after the block happen
to be mapped, which is why MATHBACKEND 6 fails intermittently in CI with a
segmentation fault that moves between test cases and sometimes does not appear
at all. The code dates to 2022; only its visibility has changed.

The magnitude is now written through NTL's public byte interface instead of as
a copy of NTL's internal memory. That removes the overflow and the dependency
on a layout that was never portable between platforms or NTL builds.

This changes the MATHBACKEND 6 binary archive format. It cannot be preserved,
because the length field the old format records is the defect itself.

Verified under AddressSanitizer with GCC 14: the serialization suite reports no
errors where the same build previously reported the overflow.

* more tweaks

* Handle unsupported heap checks and fix skipped tests on MinGW

- Skip heap-usage tests when allocator statistics are unavailable.
- Report skipped tests separately and include them in the test summary.

---------

Co-authored-by: Dmitriy Suponitskiy <dsuponitskiy@dualitytech.com>
* Recommend SPARSE_ENCAPSULATED for CKKS bootstrapping in the documentation

Add a secret key distribution section to CKKS_BOOTSTRAPPING.md with the
bootstrapping failure probabilities for fully packed N = 2^16 and 2^17,
recommend SPARSE_ENCAPSULATED (failure probability below 2^-128) for all
flavors of CKKS bootstrapping, discourage SPARSE_TERNARY, and state the
UNIFORM_TERNARY failure probabilities for users who need uniform ternary
secrets for compliance with security guidelines. Update the corresponding
notes in the functional bootstrapping and composite scaling docs, the
parameter READMEs, and the secret key distribution comments in the
bootstrapping examples.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Use SPARSE_ENCAPSULATED in the CKKS functional bootstrapping examples

Switch the multi-value functional bootstrapping example and the Fourier
extension functional bootstrapping example from SPARSE_TERNARY to the
recommended SPARSE_ENCAPSULATED secret key distribution, and update the
comments describing the secret key distribution choice accordingly.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Make doxygen comments match the documented declarations

Audit of every doxygen block in the core, pke, and binfhe headers (and the
two library sources that carry doxygen tags) against the declaration it
documents. Most @PARAM tags were written with the reference or pointer
sigil in the name, so doxygen never matched them to an argument; they are
normalized, missing parameters are documented, tags for parameters that
do not exist are removed or renamed, tags are put in signature order, and
@return tags on void functions are dropped. Descriptions copied from
other functions are corrected, including swapped EvalPowers/EvalPoly and
BootstrapFuncCore/BootstrapFunc texts, PackedEncoding text on the CKKS
encoding, DCRTPoly text on PolyImpl methods, and misnamed or nonexistent
parameters in the CryptoContext API. Comments only; no code changes.

Doxygen with WARN_NO_PARAMDOC now reports no parameter warnings for the
library, down from 480.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Fix the Sphinx documentation build with current Breathe

Expand the BLAKE2_PACKED macro of the bundled BLAKE2 header to its body
in the Exhale doxygen configuration. With the packed attribute left in
place, Doxygen emits the struct members without a source location, and
Breathe 5 aborts the build when it reads them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Document the key switching classes

Add doxygen comments for all public methods of KeySwitchBase, KeySwitchRNS,
KeySwitchBV, and KeySwitchHYBRID, and class descriptions explaining how the
BV, GHS, and hybrid methods control the key switching noise, how the RNS
digit decomposition and the extended basis QP are used, and how the exposed
core operations (digit decomposition, inner product with the key, division
by P) are composed for hoisted rotations and accumulation in the extended
basis. The descriptions follow Appendix B of https://eprint.iacr.org/2021/204.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Add doxygen comments for the public methods in pke/include

Document the non-trivial public methods of the pke headers: the algorithm
interfaces in schemebase, the RNS, BFV, BGV, and CKKS implementations
(including the bootstrapping pipeline, functional bootstrapping entry
points, and scheme switching), the precomputation tables with their index
semantics, parameter generation, encodings, keys, ciphertexts, constants,
and the remaining CryptoContext methods. SchemeBase gets a class-level
description of its dispatch to the algorithm objects instead of per-method
blocks for its forwarders. Plain getters and setters, serialization
boilerplate, and defaulted constructors are left undocumented. A few
existing comments that contradicted the code are corrected. Comments only.

Doxygen now reports no parameter, return-type, or compound warnings for
pke/include.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Add doxygen comments for the public methods in binfhe/include

Document the non-trivial public methods of the binfhe headers: the key
and ciphertext containers with the layout of their data (LWE switching
key indices, RingGSW evaluation key rows, accumulator key dimensions per
bootstrapping method), the 32-bit internal key formats and their
conversions, the RingGSW and LWE crypto parameters with their
precomputations and lazy caches, the BinFHE context parameter struct,
and the 32-bit key generation, accumulation, and key switching entry
points. Plain getters and setters, serialization boilerplate, and
member-copying constructors are left undocumented. Comments only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Add doxygen comments for the public methods in core/include

Document the non-trivial public methods of the core headers: the
DCRTPoly and Poly interfaces and default implementations (RNS basis
switching, scaling, modulus raising and reduction, sampling
constructors), element parameters, Field2n, the BigInteger and
BigVector interfaces with the naming conventions of their in-place,
fast, and Barrett variants, the native-integer backend, NTT helpers,
matrices, distribution generators, number theory helpers, trapdoor
parameters, standard lattice parameter tables, thread controls,
exceptions, and the PRNG. Plain getters and setters, serialization
boilerplate, and member-copying constructors are left undocumented; the
bundled BLAKE2 header is untouched. Formulas written with LaTeX-style
commands in dcrtpoly-interface.h are rewritten as plain text, and a
few existing comments that contradicted the code are corrected.
Comments only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Yuriy Polyakov <ypolyakod@dualitytech.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* Update src/pke/examples documentation for v1.6.0 (#1346)

Document Fourier-extension functional bootstrapping, the new bootstrapping examples and APIs, and fix stale names and broken links.

* Address review comments on the functional bootstrapping documentation

- Point to the fefbt-python repository for generating the Fourier
  extension coefficients
- Describe the supported scaling techniques and packing modes outside
  the lists of current limitations, for both the lookup table and the
  Fourier-extension functional bootstrapping
- Remove the duplicate secret key distribution bullet from the lookup
  table limitations; the distributions are described in the section
  above it
- State that the 128-bit build is not supported, without "yet"
cpplint rejects comments like "//comment" (whitespace/comments), but
clang-format left them alone, so the problem only showed up when the
cpplint hook ran. The Google base style already sets
SpacesInLineCommentPrefix to require one space. Our
"ReflowComments: false" was switching that off.

Set ReflowComments to IndentOnly, which applies the comment spacing
rules without rewrapping long comments. The value needs clang-format
20 or newer, so the pinned version moves from 18.1.8 to 23.1.1, and
the docs that give the pip install command are updated to match.

The tree is reformatted with the new settings. Almost all of the diff
is doxygen /** */ comments whose continuation "*" moves one column to
line up under the first "*". The newer version also moves a few line
breaks in ubintnat.h and packedencoding.cpp. No code tokens change.

Editors that run their own clang-format need 23.1.1
(pip3 install clang-format==23.1.1). Version 18 cannot parse the new
.clang-format.

Fixes #160
…1350)

* Re-tune the order-1 FBT benchmark rows and add FLEXIBLEAUTO variants (#1275)

Key arblut_configs by (log p, scaling technique) and carry the secret key
distribution and scaling technique in fbt_config, so the benchmarks read them
from the row instead of hardcoding SPARSE_ENCAPSULATED / FIXEDMANUAL. Register
FBTArbLUT once per technique; the rows appear as FBTArbLUT/FIXEDMANUAL/bits:N
and FBTArbLUT/FLEXIBLEAUTO/bits:N.

The 1-, 4- and 8-bit rows now use the smallest scaling factor whose output
error stays below Q/(2p) with probability 2^-40 per ciphertext: for every input
value m, |mean| + t*sigma of the residual against f(m)*Q/p over all N
coefficients, t the two-sided Gaussian quantile of 2^-40/N, plus zero wrongly
decoded coefficients. Measured on dev with fully packed ciphertexts and inputs
covering every residue, then confirmed with fresh keys and random inputs.

  FIXEDMANUAL   log Q 33 / 38 / 46  (was 33 / 38 / 47)
  FLEXIBLEAUTO  log Q 24 / 28 / 35  (new rows)

Ring dimensions are unchanged. The 1-bit FIXEDMANUAL row uses dnum 4 so that
log(Q'_L P') stays under the 767-bit limit for N = 2^15 (dnum 3 gives 769);
the cost is 5% on the FBT. The 1-bit FLEXIBLEAUTO row stops at log Q 24
because the FLEXIBLEAUTO prime selection fails at 23.

UNIFORM_TERNARY rows are not added, following the discussion in #1275.

* Search ring dimension, level budget and dnum for the FBT benchmark rows (#1275)

Beyond the scaling factor, each order-1 row now uses the ring dimension,
level budget and dnum that give the fastest FBT among the configurations
passing the 2^-40 per-ciphertext criterion and the 128-bit limit on
log(Q'_L P') (767 / 1553 bits at N = 2^15 / 2^16). 267 configurations were
measured for the 1-, 4- and 8-bit rows and 127 for the 2-bit rows, on dev
7c25a23.

  FIXEDMANUAL   2-bit: log Q 32 at N = 2^15, budget {2,2}, dnum 4 (was 35 at
                2^16, {4,4}, dnum 3); 4-bit: dnum 2 (was 3)
  FLEXIBLEAUTO  2-bit: new row, log Q 24 at 2^15, {3,3}, dnum 3;
                4-bit: log Q 27 at 2^15, {3,3}, dnum 4 (was 28 at 2^16,
                {4,4}, dnum 3); 1- and 8-bit: dnum 2 (were 3 and 4)

The 1-, 4- and 8-bit FIXEDMANUAL rows keep their budgets: at every size a
{2,2} budget is 30-70% slower than {3,3} and {5,5} never wins, and the
smallest dnum the security limit admits is the fastest except on the 8-bit
FIXEDMANUAL chain. Moving the 2-bit FIXEDMANUAL and 4-bit FLEXIBLEAUTO rows to
N = 2^15 makes them 1.6x and 2.1x faster; the 4-bit row also passes one bit
lower there. scaleTHI (8 / 32 / 128) does not change the margins.
#1353)

* Fix double rescale when EvalMultMutable gets the same ciphertext twice (#1339)

With the automatic scaling techniques, AdjustLevelsAndDepthToOneInPlace
rescales ciphertext1 and then ciphertext2. When both arguments are the
same object, that object was rescaled twice. In CKKS this made decryption
throw, or with FIXEDAUTO return a wrong value with no error, and it left
the operand corrupted. In BGV the value was correct but an extra level
was used.

- CKKS and BGV AdjustLevelsAndDepthToOneInPlace: skip the second rescale
  when both arguments are the same object
- LeveledSHERNS::EvalMultMutable: send the same-object case to
  EvalSquareMutable, which rescales once and does fewer polynomial
  products
- UnitTestSHE EVAL_MUTABLE: test same-object calls on fresh and
  degree-2 inputs, checking value, level and the operand afterwards;
  add FIXEDAUTO and FLEXIBLEAUTO cases for BGV and CKKS, and use
  EPSILON_HIGH for CKKS in this test, since FIXEDAUTO's error is
  above EPSILON

* Skip the FLEXIBLEAUTO CKKS EVAL_MUTABLE test case in 128-bit builds

128-bit CKKS does not support FLEXIBLEAUTO, so creating the context for
case 06 threw an exception.
* Fix key and metadata state handling

- preserve key tags through evaluation-key copy and move operations
- add explicit move semantics to key base classes
- initialize encryption and decryption result fields
- preserve derived metadata when cloning by key
- complete RingGSW parameter equality checks
- correct the LWE switching-key serialized name
- add regression coverage for the fixes

* Preserve the plaintext modulus of LWE ciphertexts in multi-input gates

LWE ciphertexts did not serialize their plaintext modulus, so inputs
encrypted with p=6 or p=8 came back with the default of 4. Multi-input
gates took their output encoding from that tag, and OR3, AND4 and OR4
then decrypted wrongly after a round trip.

Serialize and compare the modulus, and take it from the gate rather
than from the first input, which also covers copies. Ciphertext
archives written before this change do not load.

* take the Cholesky pivot from the updated trailing block

* Keep DCRTPoly format and parameters consistent

- operator=(uint64_t) and operator=(const PolyType&) now set the
  element's format to match its towers
- CloneWithNoise samples the noise as coefficients and converts to the
  requested format, instead of ignoring it and labelling the samples
  with the element's current format
- SwitchModulusAtIndex updates a copy of the parameters rather than the
  object shared with other elements, which it also left stale
- AddILElementOne's error names EVALUATION, the format it requires
- document that the PolyType and PolyLargeType conversions need
  COEFFICIENT input

* Implement ternary GenerateInteger and fix sparse sampling for h ≤ 3

* make HashUtil::Hash compute SHA-256 and reject SHA-512

* make declared defaults and const getters usable from headers

* Test BGV plaintext multiplication across levels

---------

Co-authored-by: Dmitriy Suponitskiy <dsuponitskiy@dualitytech.com>
Co-authored-by: Carlo Pascoe <cpascoe@dualitytech.com>
…1352)

* Rewrite Best_Performance.md for v1.6 and fix the README links to it

* Address the review of the Best_Performance.md rewrite

Name the two measurement machines once, after the first results table,
so every table inherits them: the Xeon 8360Y at a fixed 2.4 GHz with
clang 18 and the i7-9700 at a fixed 3.0 GHz with clang 15, both with
turbo off, and what turbo changes on the i7 at one and at eight threads.

Recommend the composite scaling techniques over NATIVE_SIZE=128 for CKKS
scaling factors above 60 bits.

Add i7 columns, at the fixed clock and with turbo, to the binfhe gate
table, and the GCC 11 gap on that machine to the compiler paragraph.

Add SlotsToCoeffs-first bootstrapping (BTSlotsEncoding) with its latency
against ModRaise-first at ring dimension 2^16, full packing,
SPARSE_ENCAPSULATED, on both machines and with turbo, and the functional
bootstrapping table for 1-, 2-, 4- and 8-bit lookup tables under
FIXEDMANUAL and FLEXIBLEAUTO on both machines.

* add more bootstrapping benchmark numbers

* Address the second review of Best_Performance.md and favor fast choices

Keep only GINX and LMKCDEY in the binfhe gate table, say what each method
is, and recommend GINX for multithreaded gates.

Replace the two bootstrapping tables with one, taken from the retuned
configurations of the ckks-bootstrapping benchmark (#1355): both variants
for full and sparse packing at ring dimension 2^16, ring dimension 2^17
at 5 and 15 levels, and Set II of the security guidelines for
UNIFORM_TERNARY secrets, with an output-precision column and the levels
left after bootstrapping in each row. Drop the SPARSE_TERNARY rows.

Link each table to the benchmark it comes from: binfhe-paramsets,
ckks-bootstrapping and ckks-functional-bootstrapping.

Turn the trade-off descriptions into recommendations where the
measurements support one: SlotsToCoeffs-first for fully packed
ciphertexts, SPARSE_ENCAPSULATED secrets, level budgets {4, 3} and
{3, 3}, the scaling modulus chosen for precision and the rest for speed,
the functional bootstrapping scaling technique by table size, one
functional bootstrapping stream per socket for throughput, minimal depth
and key-switching digits, fast rotations, about 8 threads for
operations that saturate there, and the default radix, allocator and
NUMA settings. Note the modulus-size trade-off under HEXL.
* Throw on the EstimateLogP check only when the real P exceeds the estimate (#1358)

* Bound the composite-scaling prime searches and fix the clang build of core_tests (#1358)

CompositePrimeModuliGen could run forever: when the running scaling
factor collapsed, the next target prime fell below the cyclotomic
order and the candidate wrapped to just under 2^64, and searches that
started above the register word size walked down one candidate at a
time. Throw the prime sampling error before the wrap, and start each
search at the register word size, which selects the same primes.

PreviousPrime now throws when its input is at or below the step
instead of returning a wrapped value.

Move TernaryUniformGeneratorImpl::GenerateInteger next to the static
distribution it uses, which clang rejected under
-Werror=undefined-var-template.

* Remove a slot's old prime from the record once before searching for its
replacement, so the search cannot accept a prime already used in the same
level, and throw as soon as raising a prime would cross the register word
size, where the search previously walked down to the bottom of the range.

* Start clamped composite prime searches at 2^registerWordSize + 1 (#1358)

When cyclOrder divides 2^registerWordSize, as it must for any prime to fit
the register word, the ceiling is 2^registerWordSize + 1, so the first
candidate is the largest value that is 1 mod cyclOrder at or below the limit
instead of one step above it. Otherwise the ceiling is 1 and PreviousPrime
throws at once. The selected primes do not change.
…posite scaling (#1357)

Replace the configuration table with rows chosen by a parameter-space
exploration over the scaling modulus, level budget, key-switching digits
and levels after bootstrapping, under the 128-bit bound at each ring
dimension, a floor of 15 bits of mean output precision, and the most levels
that fit with at most eight digits. The first five rows keep their
identities and move from uniform to sparse-encapsulated secrets; the table
is ordered by ring dimension and then by one changing parameter per row:
levels, iterations, scaling technique, packing density, secret
distribution. The uniform row is the security guidelines' Set II with four
levels after bootstrapping instead of five, which is what the library's
bound admits. The sparse-ternary rows are gone.

Give each row the single-round precision its second Meta-BTS iteration
needs and pass it to EvalBootstrap; without it the second iteration
refined nothing.

Enable the StC-first composite-scaling row and the corresponding example call.
* updated the documentation to point to 1.6.0

* Use a relative link to Best_Performance.md in the release notes

---------

Co-authored-by: Yuriy Polyakov <ypolyakod@dualitytech.com>
Co-authored-by: Carlo Pascoe <cpascoe@dualitytech.com>
@yspolyakov yspolyakov added this to the Release 1.6.0 milestone Sep 28, 2026
@yspolyakov yspolyakov added the new release new release label Sep 28, 2026
@dsuponitskiy
dsuponitskiy self-requested a review September 28, 2026 20:05
@dsuponitskiy
dsuponitskiy merged commit 6206d24 into main Sep 28, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new release new release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants