Hi OpenFHE team,
I would like to report a reproducible correctness issue in the CKKS-to-FHEW scheme-switching path.
Summary
OpenFHE v1.5.1 exposes the CKKS-to-FHEW switching path through the public CryptoContext wrappers:
EvalCKKStoFHEWSetup(...)EvalCKKStoFHEWKeyGen(...)EvalCKKStoFHEWPrecompute(...)EvalCKKStoFHEW(...)
Those wrappers eventually reach the internal helper RoundqQAlter(...), which computes
round(v * q / Q) mod q using double:
return NativeInteger(static_cast<BasicInteger>(
std::floor(0.5 + v.ConvertToDouble() * q.ConvertToDouble() / Q.ConvertToDouble())))
.Mod(q);
I modeled that exact formula on ordinary valid 56-bit / 50-bit positive integers and got an off-by-one residue. The exact integer result differs from the double path by 1:
openfhe_double_path=1031628115258748
exact=1031628115258747
This is a source-level correctness bug in the scheme-switching arithmetic. I did not include a full end-to-end CKKS->FHEW runtime probe in this report, but the public path is clearly reachable and the helper formula is directly reproducible.
Environment
- OpenFHE baseline revision:
1306d14f8c26bb6150d3e6ad54f28dfe1007689e - OpenFHE configuration used for validation:
NATIVE_SIZE=64,MATHBACKEND=4,WITH_OPENMP=OFF - Generated config observed locally:
NATIVEINT=64,HAVE_INT128=TRUE,WITH_BE4 - OS: Linux x86_64
Minimal reproduction
The witness model uses the exact formula from RoundqQAlter(...) and compares it to exact big-integer rounding:
static cpp_int round_div(cpp_int numerator, cpp_int denominator) {
return (2 * numerator + denominator) / (2 * denominator);
}
static NativeInteger round_q_q_alter_model(NativeInteger v, NativeInteger q, NativeInteger Q) {
return NativeInteger(static_cast<BasicInteger>(
std::floor(0.5 + v.ConvertToDouble() * q.ConvertToDouble() / Q.ConvertToDouble())))
.Mod(q);
}
The public CKKS-to-FHEW wrapper chain is documented in:
Actual behavior
The witness prints:
round_q_q_alter_witness
v=66024199376568990 q=1125899906842465 Q=72057594037927747
openfhe_double_path=1031628115258748
exact=1031628115258747
So the double path is off by 1 on a valid ordinary input tuple.
Expected behavior
The helper should compute the exact residue implied by round(v * q / Q) mod q.
For the witness above, the correct result is 1031628115258747, not 1031628115258748.
Cause analysis
RoundqQAlter(...) converts three large integers to double, multiplies and divides in floating-point, and only then rounds back to NativeInteger.
That is enough to lose low bits on ordinary 50-bit and 56-bit values. The exact math is not hard:
round(v * q / Q) mod q
The issue is the implementation strategy, not the arithmetic identity.
The public wrapper chain makes this relevant to the CKKS-to-FHEW switching path, even though the faulty helper itself is internal.
Impact
The confirmed impact is a one-unit residue error in CKKS scheme switching for ordinary valid integer inputs.
That can perturb the LWE ciphertexts produced by the switching path and propagate into downstream computations that rely on those residues.
Source locations
Suggested direction
Replace the floating-point rounding path with exact integer arithmetic.
A suitable fix would use integer multiply-and-round logic or another exact modular rounding helper that does not route through double for 50-bit and 56-bit operands.