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:
rlwe-mp.cppcallersrlwe-mp.cpprescaling callersrlwe-mp.cppmodulus-switching callersrlwe-mp.cppconversion callersbfvrns-pke.cppcallerbfvrns-multiparty.cppcaller
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
NativeIntegerT::MultiplyAndRound(...)definitionrlwe-mp.cppcallersrlwe-mp.cpprescaling callersrlwe-mp.cppmodulus-switching callersrlwe-mp.cppconversion callersbfvrns-pke.cppcallerbfvrns-multiparty.cppcaller
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.