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

**URL:** https://openfhe.discourse.group/t/bug-report-ckks-scheme-switching-can-round-residues-incorrectly-because-roundqqalter-uses-double/2378
**Category:** Bug Reports
**Created:** [September 8, 2026, 12:58pm UTC](https://openfhe.discourse.group/t/bug-report-ckks-scheme-switching-can-round-residues-incorrectly-because-roundqqalter-uses-double/2378 "2026-09-08T12:58:52Z")
**Posts on this page:** 1
**Page:** 1

<div class="post-metadata">

### Author: ![CCYJ](https://avatars.discourse-cdn.com/v4/letter/c/b5ac83/32.png) [@CCYJ](https://openfhe.discourse.group/u/CCYJ)
#### Post date: [September 8, 2026, 12:58pm UTC](https://openfhe.discourse.group/t/bug-report-ckks-scheme-switching-can-round-residues-incorrectly-because-roundqqalter-uses-double/2378/1 "2026-09-08T12:58:53Z")

</div>

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`:

```cpp
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:

```plaintext
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:

```cpp
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:

- [`CryptoContext` CKKS-to-FHEW wrappers](https://github.com/openfheorg/openfhe-development/blob/1306d14f8c26bb6150d3e6ad54f28dfe1007689e/src/pke/include/cryptocontext.h#L3630-L3704)
- [`RoundqQAlter(...)` definition](https://github.com/openfheorg/openfhe-development/blob/1306d14f8c26bb6150d3e6ad54f28dfe1007689e/src/pke/lib/scheme/ckksrns/ckksrns-schemeswitching.cpp#L166-L169)
- [`RoundqQAlter(...)` call site](https://github.com/openfheorg/openfhe-development/blob/1306d14f8c26bb6150d3e6ad54f28dfe1007689e/src/pke/lib/scheme/ckksrns/ckksrns-schemeswitching.cpp#L927-L928)

## Actual behavior

The witness prints:

```plaintext
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:

```plaintext
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

- [`CryptoContext` CKKS-to-FHEW wrappers](https://github.com/openfheorg/openfhe-development/blob/1306d14f8c26bb6150d3e6ad54f28dfe1007689e/src/pke/include/cryptocontext.h#L3630-L3704)
- [`RoundqQAlter(...)` definition](https://github.com/openfheorg/openfhe-development/blob/1306d14f8c26bb6150d3e6ad54f28dfe1007689e/src/pke/lib/scheme/ckksrns/ckksrns-schemeswitching.cpp#L166-L169)
- [`RoundqQAlter(...)` call site](https://github.com/openfheorg/openfhe-development/blob/1306d14f8c26bb6150d3e6ad54f28dfe1007689e/src/pke/lib/scheme/ckksrns/ckksrns-schemeswitching.cpp#L927-L928)

## 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.
