[Bug report] `NativeIntegerT::MultiplyAndRound()` can mis-round ordinary 60-bit inputs because it routes through `double`

Hi OpenFHE team,

I would like to report a reproducible correctness issue in the public low-level integer helper.

Summary

NativeIntegerT::MultiplyAndRound() is implemented in ubintnat.h using floating-point arithmetic:

return static_cast<NativeInt>(p.ConvertToDouble() * (this->ConvertToDouble() / q.ConvertToDouble()) + 0.5);

On ordinary valid 60-bit-scale inputs, that double path can return the wrong rounded result.

In my local witness, the OpenFHE path returned:

openfhe_double_path=844720439426725632

while exact integer rounding gave:

exact=844720439426725580

So the helper is off by 52 on a valid input tuple. This is a correctness bug in a public arithmetic helper.

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 compares OpenFHE’s helper against exact cpp_int rounding:

static cpp_int round_div(cpp_int numerator, cpp_int denominator) {
    return (2 * numerator + denominator) / (2 * denominator);
}

auto got = NativeInteger(x).MultiplyAndRound(NativeInteger(p), NativeInteger(q)).ConvertToInt();
cpp_int exact = round_div(cpp_int(x) * p, q);

The helper is defined in:

and is used by multiple low-level and scheme-level code paths, including RLWE multi-party helpers and BFV modulus-switching code:

Actual behavior

The witness prints:

multiply_and_round_witness
x=13198756866041503 p=72057594037927903 q=1125899906842531
openfhe_double_path=844720439426725632
exact=844720439426725580

The helper is therefore returning an incorrect rounded value for an ordinary 60-bit-scale tuple.

Expected behavior

The helper should return the exact rounded quotient implied by round(x * p / q).

For the witness above, the correct result is 844720439426725580, not 844720439426725632.

Cause analysis

MultiplyAndRound() routes the computation through double, which only has 53 bits of mantissa.

That is not enough to preserve exact rounding for 60-bit-scale operands. The problem is visible even when the final integer result still fits in 64 bits; the loss happens before the cast back to NativeInt.

Impact

The confirmed impact is silent wrong output from a public arithmetic helper on valid ordinary inputs.

Because this helper is reused in modulus-switching and polynomial arithmetic code, the wrong result can propagate into higher-level CKKS/BFV/RLWE computations.

Source locations

Suggested direction

Replace the floating-point implementation with exact integer arithmetic.

The existing double conversion is not precise enough for 60-bit-scale operands, even when the final answer fits in NativeInt.