# \[Bug report\] OpenFHE 128-bit CKKS can silently wrap a scaled real plaintext coefficient modulo the active tower and decode near zero

**URL:** https://openfhe.discourse.group/t/bug-report-openfhe-128-bit-ckks-can-silently-wrap-a-scaled-real-plaintext-coefficient-modulo-the-active-tower-and-decode-near-zero/2375
**Category:** Bug Reports
**Tags:** bugs
**Created:** [September 4, 2026, 9:03am UTC](https://openfhe.discourse.group/t/bug-report-openfhe-128-bit-ckks-can-silently-wrap-a-scaled-real-plaintext-coefficient-modulo-the-active-tower-and-decode-near-zero/2375 "2026-09-04T09:03:11Z")
**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 4, 2026, 9:03am UTC](https://openfhe.discourse.group/t/bug-report-openfhe-128-bit-ckks-can-silently-wrap-a-scaled-real-plaintext-coefficient-modulo-the-active-tower-and-decode-near-zero/2375/1 "2026-09-04T09:03:11Z")

</div>

Hi OpenFHE team,

I would like to report a reproducible CKKS correctness issue in the `NATIVEINT == 128` `COMPLEX` encoding path.

## Summary

With a public parameter set that OpenFHE accepts:

- `multiplicative_depth = 0`
- `scaling_mod_size = 88`
- `first_mod_size = 88`
- `batch_size = 1`
- `scaling technique = FIXEDAUTO`
- finite input first slot `(1048576, 0)`

the public `MakeCKKSPackedPlaintext(...) -> Encrypt(...) -> Decrypt(...)` round trip returns normally, but the decoded first slot is near zero instead of near the input value.

In my local runs, the same boundary case reproduced on both:

- OpenFHE `v1.5.1` (`1306d14f8c26bb6150d3e6ad54f28dfe1007689e`)
- current `main` commit `ed361af22049007db2107e7c69bcff209e8c420d`

Two controls decode correctly:

- a small real input `(16, 0)` with `scaling_mod_size = 80`, `first_mod_size = 96`
- a more ordinary CKKS case `(0.25, 0.125)` with `multiplicative_depth = 1`, `scaling_mod_size = 50`, `first_mod_size = 60`, `batch_size = 8`

This does not require malformed serialized data, mismatched keys, mismatched parameter objects, or non-finite inputs. The input is an ordinary finite real value, and `GenCryptoContext(...)` accepts the parameter set.

The observed behavior appears to come from a range mismatch inside the 128-bit real encode path: the computed scaled coefficient fits the broad 128-bit conversion guard, but exceeds the active single-tower plaintext modulus. The value is then reduced modulo the active tower by `FitToNativeVector(...)`, and `Decode(...)` consumes the residue.

## Environment

- OpenFHE current-main commit tested dynamically:  
`ed361af22049007db2107e7c69bcff209e8c420d`
- Also reproduced on release `v1.5.1` commit: `1306d14f8c26bb6150d3e6ad54f28dfe1007689e`
- OpenFHE configuration: `NATIVE_SIZE=128`, `MATHBACKEND=4`, `WITH_OPENMP=OFF`
- Generated config observed locally: `NATIVEINT=128`, `HAVE_INT128=TRUE`
- OS: Linux x86\_64

## Minimal reproduction

The following standalone program uses only public OpenFHE APIs and runs three cases: the failing boundary case plus two controls.

```cpp
#include "openfhe.h"

#include <cmath>
#include <complex>
#include <iomanip>
#include <iostream>
#include <string>
#include <vector>

using namespace lbcrypto;

namespace {

struct CaseParams {
    const char* name;
    double real;
    double imag;
    uint32_t depth;
    uint32_t scalingModSize;
    uint32_t firstModSize;
    uint32_t batchSize;
};

void runCase(const CaseParams& c) {
    CryptoContextFactory<DCRTPoly>::ReleaseAllContexts();
    CryptoContextImpl<DCRTPoly>::ClearEvalMultKeys();
    CryptoContextImpl<DCRTPoly>::ClearEvalAutomorphismKeys();
    CryptoContextImpl<DCRTPoly>::ClearEvalSumKeys();

    CCParams<CryptoContextCKKSRNS> parameters;
    parameters.SetMultiplicativeDepth(c.depth);
    parameters.SetScalingTechnique(FIXEDAUTO);
    parameters.SetScalingModSize(c.scalingModSize);
    parameters.SetFirstModSize(c.firstModSize);
    parameters.SetBatchSize(c.batchSize);
    parameters.SetCKKSDataType(COMPLEX);

    auto cc = GenCryptoContext(parameters);
    cc->Enable(PKE);

    auto keys = cc->KeyGen();
    std::vector<std::complex<double>> input(c.batchSize, {c.real, c.imag});
    auto pt = cc->MakeCKKSPackedPlaintext(input, 1, 0, nullptr, c.batchSize);
    auto ct = cc->Encrypt(keys.publicKey, pt);

    Plaintext decoded;
    auto result = cc->Decrypt(keys.secretKey, ct, &decoded);
    decoded->SetLength(1);
    const auto& values = decoded->GetCKKSPackedValue();
    const double err = values.empty() ? std::nan("") : std::abs(values[0] - input[0]);

    std::cout << c.name
              << " decrypt_valid=" << (result.isValid ? "true" : "false")
              << " ring_dim=" << cc->GetRingDimension()
              << " decoded0=" << std::setprecision(17)
              << (values.empty() ? std::complex<double>{std::nan(""), std::nan("")} : values[0])
              << " max_abs_error=" << err << "\n";
}

} // namespace

int main() {
    runCase({"real-boundary", 1048576.0, 0.0, 0, 88, 88, 1});
    runCase({"small-real-safe", 16.0, 0.0, 0, 80, 96, 1});
    runCase({"known-valid", 0.25, 0.125, 1, 50, 60, 8});
}

```

Representative local build command against an existing OpenFHE 128-bit build:

```bash
clang++ -std=gnu++17 main.cpp \
  -I/path/to/openfhe-development/src/core/include \
  -I/path/to/openfhe-development/src/pke/include \
  -I/path/to/openfhe-development/src/binfhe/include \
  -I/path/to/openfhe-development/third-party/cereal/include \
  -I/path/to/openfhe-build/src/core \
  -Wl,-rpath,/path/to/openfhe-build/lib \
  /path/to/openfhe-build/lib/libOPENFHEpke.so \
  /path/to/openfhe-build/lib/libOPENFHEcore.so \
  /path/to/openfhe-build/lib/libOPENFHEbinfhe.so \
  -ldl -o openfhe_ckks_real_boundary_probe

```

Run:

```bash
./openfhe_ckks_real_boundary_probe

```

## Actual behavior

On current `main` commit `ed361af22049007db2107e7c69bcff209e8c420d`, the program prints:

```plaintext
real-boundary decrypt_valid=true ring_dim=8192 decoded0=(-5.1625404513301414e-15,7.1085833891275816e-26) max_abs_error=1048576
small-real-safe decrypt_valid=true ring_dim=8192 decoded0=(16,4.0035541647566539e-22) max_abs_error=4.0035541647566539e-22
known-valid decrypt_valid=true ring_dim=16384 decoded0=(0.25000000000281331,0.12499999999878758) max_abs_error=3.0634369257603773e-12

```

On `v1.5.1`, the same failing boundary case also decoded near zero:

```plaintext
real-boundary decrypt_valid=true ring_dim=8192 decoded0=(-5.1625404530749755e-15,-7.5932595292953712e-25) max_abs_error=1048576

```

So the boundary case does not throw, does not mark decryption invalid, and does not produce a normal CKKS approximation of the input. It returns a near-zero decoded first slot while the absolute error remains essentially the full input magnitude.

## Expected behavior

If this input and parameter combination is supported, the decoded first slot should be close to `(1048576, 0)` up to ordinary CKKS approximation error.

If the combination is outside the supported CKKS range because the scaled plaintext coefficient does not fit the active plaintext modulus for the current level/tower set, the API should reject it explicitly or document the unsupported range. It should not appear to succeed while silently reducing the encoded coefficient modulo the active tower and then decoding an unrelated near-zero value.

## Cause analysis

In the 128-bit CKKS encode path, OpenFHE computes the real and imaginary scaled coefficients into a temporary `std::vector<int128_t>` and checks only against a broad 128-bit conversion bound:

```cpp
int64_t re64 = std::llround(dre);
int32_t pRemaining = pCurrent + n1;
int128_t re = 0;
if (pRemaining < 0) {
    re = re64 >> (-pRemaining);
}
else {
    int128_t pPowRemaining = ((int128_t)1) << pRemaining;
    re = pPowRemaining * re64;
}
...
temp[i] = (re < 0) ? MaxBitValue + re : re;
...
if (is128BitOverflow(temp[i]) || is128BitOverflow(temp[i + slots])) {
    OPENFHE_THROW("Overflow, try to decrease scaling factor");
}

```

After that, each active tower is populated via `FitToNativeVector(...)`:

```cpp
FitToNativeVector(temp, MaxBitValue, &nativeVec);

```

and the 128-bit `FitToNativeVector(...)` implementation stores:

```cpp
(*nativeVec)[gap * i] = n.Mod(modulus);

```

or the corresponding signed-path `ModSub(...)` residue, depending on the high-half test.

For the failing case, an auxiliary standalone diagnostic that mirrors the 128-bit real encode path reported:

```plaintext
scaled_coefficient=324518553658426726783156020576256
scaled_coefficient_msb=109
tower0_modulus=309485009821345068726304769
tower0_modulus_msb=89
modeled_real_residue=309485009821343470997422081
real_coefficient_exceeds_tower_modulus=true
recovered_real_coeff=309485009821343470997422081
signed_pre_fft_real=-1597728882688

```

So the computed real-side coefficient fits the broad 128-bit path, but is much larger than the active single-tower modulus. The value entering `Decode(...)` is the residue, not the intended coefficient.

`Decode(...)` then interprets the recovered coefficient relative to `qHalf`, converts it back to `double`, scales it, and runs the final FFT. That is consistent with the observed near-zero decoded slot.

## Relationship to existing issues

This appears distinct from the previously reported 128-bit CKKS imaginary-branch shift issue fixed by PR #1237.

My reproducer is real-only, uses finite input `(1048576, 0)`, and still reproduces on current `main`. The issue here is not a `>= 64` shift expression in the imaginary path; it is the apparent lack of an active-tower range check before the encoded 128-bit coefficient is reduced modulo the active tower.

I also checked the current upstream issue tracker for obvious duplicates around `CKKSPackedEncoding`, `FitToNativeVector`, 128-bit CKKS encoding, and single-tower/depth-0 near-zero decode behavior, but did not find an obvious existing report for this exact issue.

## Impact

The confirmed impact is a silent correctness failure in the public 128-bit CKKS API for an accepted parameter/input combination:

- `GenCryptoContext(...)`, `MakeCKKSPackedPlaintext(...)`, `Encrypt(...)`, and `Decrypt(...)` all return normally;
- the decoded first slot is near zero instead of near the finite input value;
- the boundary failure is input-dependent, while nearby control cases decode correctly.

The confirmed problem is silent wrong-result behavior or missing input-range validation in the 128-bit CKKS encode/decode path.

## Relevant source locations

Current `main` commit `ed361af22049007db2107e7c69bcff209e8c420d`:

- 128-bit CKKS encode path and `temp` construction:  
[openfhe-development/src/pke/lib/encoding/ckkspackedencoding.cpp at ed361af22049007db2107e7c69bcff209e8c420d · openfheorg/openfhe-development · GitHub](https://github.com/openfheorg/openfhe-development/blob/ed361af22049007db2107e7c69bcff209e8c420d/src/pke/lib/encoding/ckkspackedencoding.cpp#L134-L189)
- Tower population through `FitToNativeVector(...)`:  
[openfhe-development/src/pke/lib/encoding/ckkspackedencoding.cpp at ed361af22049007db2107e7c69bcff209e8c420d · openfheorg/openfhe-development · GitHub](https://github.com/openfheorg/openfhe-development/blob/ed361af22049007db2107e7c69bcff209e8c420d/src/pke/lib/encoding/ckkspackedencoding.cpp#L288-L297)
- 128-bit `FitToNativeVector(...)` modulo reduction:  
[openfhe-development/src/pke/lib/encoding/ckkspackedencoding.cpp at ed361af22049007db2107e7c69bcff209e8c420d · openfheorg/openfhe-development · GitHub](https://github.com/openfheorg/openfhe-development/blob/ed361af22049007db2107e7c69bcff209e8c420d/src/pke/lib/encoding/ckkspackedencoding.cpp#L536-L551)
- `Decode(...)` coefficient recovery and scaling:  
[openfhe-development/src/pke/lib/encoding/ckkspackedencoding.cpp at ed361af22049007db2107e7c69bcff209e8c420d · openfheorg/openfhe-development · GitHub](https://github.com/openfheorg/openfhe-development/blob/ed361af22049007db2107e7c69bcff209e8c420d/src/pke/lib/encoding/ckkspackedencoding.cpp#L336-L399)
- CKKS parameter validation accepting this general shape:  
[openfhe-development/src/pke/lib/scheme/gen-cryptocontext-params-validation.cpp at ed361af22049007db2107e7c69bcff209e8c420d · openfheorg/openfhe-development · GitHub](https://github.com/openfheorg/openfhe-development/blob/ed361af22049007db2107e7c69bcff209e8c420d/src/pke/lib/scheme/gen-cryptocontext-params-validation.cpp#L72-L92)

## Suggested direction

If this range is intentionally unsupported, OpenFHE could reject it explicitly before the encoded 128-bit coefficient is reduced modulo the active tower.

If it is intended to be supported, the encode path likely needs an additional range check against the active plaintext modulus for the current level/tower set, rather than only the broad 128-bit conversion guard.

In either case, the public API should not silently return a successful encrypt/decrypt round trip for this combination while decoding an unrelated near-zero value.

## Question

For 128-bit CKKS `COMPLEX` encoding, when the computed scaled plaintext coefficient fits the broad 128-bit conversion guard but exceeds the active plaintext modulus for the current level/tower set, is the intended contract:

- reject the input or parameter combination;
- document the range as unsupported; or
- support it without silent modular wrap during `FitToNativeVector(...)`?
