[Bug report] CKKS scheme switching can round residues incorrectly because `RoundqQAlter()` uses `double`

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.