No exception thrown in CKKS when firstModSize is smaller than scalingModSize

Hello,

Sorry again for another report related to using an invalid context in OpenFHE. I understand these cases can be addressed under AAHE, but I wanted to share one more scenario in CKKS where an invalid context — specifically, when firstModSize is smaller than scalingModSize — does not throw any exceptions and instead leads to silently incorrect results.

Here are two cases of getting incorrect values under insufficient firstModSize.

Case 1: Decrypting immediately after encryption gives slightly incorrect values

#include <iostream>
#include <chrono>

#include <iomanip>

#include "openfhe.h"
#include "../functional_units/functional_units.hpp"

using namespace lbcrypto;
using namespace std;

int main(void) {
  CCParams<CryptoContextCKKSRNS> parameters;
  parameters.SetRingDim(32768);
  parameters.SetMultiplicativeDepth(3);
  parameters.SetFirstModSize(54);
  parameters.SetScalingModSize(59);
  parameters.SetSecurityLevel(HEStd_256_classic);
  parameters.SetScalingTechnique(FLEXIBLEAUTO);
  
  CryptoContext<DCRTPoly> cc = GenCryptoContext(parameters);
  cc->Enable(PKE);
  cc->Enable(KEYSWITCH);
  cc->Enable(LEVELEDSHE);
  KeyPair<DCRTPoly> keyPair;
  keyPair = cc->KeyGen();
  cc->EvalMultKeyGen(keyPair.secretKey);
  size_t slots(7098);
  vector<complex<double>> tmp_vec_(7098);
  Plaintext tmp;
  Ciphertext<DCRTPoly> tmp_;

  Ciphertext<DCRTPoly> x, y;
  double yP;
  double yC;
  int c;
  vector<double> tmp_vec_1 = { 3612029277224623.500000, 2173980662796596.200000, 4324895368175417.000000 };
  tmp = cc->MakeCKKSPackedPlaintext(tmp_vec_1);
  x = cc->Encrypt(keyPair.publicKey, tmp);
  cc->Decrypt(keyPair.secretKey, x, &tmp);
  tmp->SetLength(20);
  tmp_vec_ = tmp->GetCKKSPackedValue();
  for (int64_t tmp_i = 0; tmp_i < 5; ++tmp_i) {
    cout << fixed << setprecision(5) << real(tmp_vec_[tmp_i]) << " ";
  }
  cout << endl;
  return 0;
}
  • Context issue: firstModSize = 54, scalingModSize = 59
  • Observation: Even without any operations, the decrypted data is already slightly different from the original.
    • Expected: 3612029277224623.500000 2173980662796596.200000 4324895368175417.000000 0.000000 0.000000
    • Output: 3612029277224624.00000 2173980662796595.50000 4324895368175416.50000 0.02938 -0.04829
  • Note: Since the initial data is already corrupted, further operations only amplify the errors.

Case 2: Values break after the last multiplication

#include <iostream>
#include <chrono>

#include <iomanip>

#include "openfhe.h"
#include "../functional_units/functional_units.hpp"

using namespace lbcrypto;
using namespace std;

int main(void) {
  CCParams<CryptoContextCKKSRNS> parameters;
  parameters.SetRingDim(16384);
  parameters.SetMultiplicativeDepth(1);
  parameters.SetFirstModSize(40);
  parameters.SetScalingModSize(59);
  parameters.SetSecurityLevel(HEStd_NotSet);
  parameters.SetScalingTechnique(FLEXIBLEAUTO);
  
  CryptoContext<DCRTPoly> cc = GenCryptoContext(parameters);
  cc->Enable(PKE);
  cc->Enable(KEYSWITCH);
  cc->Enable(LEVELEDSHE);
  KeyPair<DCRTPoly> keyPair;
  keyPair = cc->KeyGen();
  cc->EvalMultKeyGen(keyPair.secretKey);
  size_t slots(897);
  vector<complex<double>> tmp_vec_(897);
  Plaintext tmp;
  Ciphertext<DCRTPoly> tmp_;

  Ciphertext<DCRTPoly> x, y;
  double yP;
  double yC;
  int c;
  vector<double> tmp_vec_1 = { 1.177856 };
  tmp = cc->MakeCKKSPackedPlaintext(tmp_vec_1);
  x = cc->Encrypt(keyPair.publicKey, tmp);
  vector<double> tmp_vec_2 = { 1.759079 };
  tmp = cc->MakeCKKSPackedPlaintext(tmp_vec_2);
  y = cc->Encrypt(keyPair.publicKey, tmp);
  x = cc->EvalMultNoRelin(x, y);
  cc->RescaleInPlace(x);
  cc->RelinearizeInPlace(x);
  cc->Decrypt(keyPair.secretKey, x, &tmp);
  tmp->SetLength(5);
  tmp_vec_ = tmp->GetCKKSPackedValue();
  for (int64_t tmp_i = 0; tmp_i < 5; ++tmp_i) {
    cout << fixed << setprecision(5) << real(tmp_vec_[tmp_i]) << " ";
  }
  cout << endl;
  return 0;
}
  • Context issue: firstModSize = 40, scalingModSize = 59
  • Observation: Unlike Case 1, simple encryption and decryption don’t show a problem. But applying the allowed number of multiplication, the value breaks.
    • Expected: 2.071942 0.000000 0.000000 0.000000 0.000000
    • Output: -0.00009 -0.00009 -0.00020 0.00014 0.00002
  • Note: I tested with different multiplicative depths, and in every case, the result becomes incorrect exactly at the n-th multiplication, where n is the multiplicative depth.

Again, I realize that this kind of invalid context may eventually be handled via AAHE. But if this particular check is already implemented and I simply missed it, I’d appreciate it if you could point me to the relevant reference.

__
Thanks as always for your support!

Thank you reporting this. We can certainly throw an exception in this case. I’ve created an issue for this: Improve exception handling for selected scenarios leading to incorrect results · Issue #995 · openfheorg/openfhe-development · GitHub