Combination of multiple `EvalLinearTransform` and `RescaleInPlace` results in "Modulus missmatch"

Hello everyone!

I would like to use EvalLinearTransfrom function to perform arbitrary permutations over a ckks-ciphertext. The problem is that every call of EvalLinearTransfrom results in growth of a noise, so I have to call RescaleInPlace every time GetNoiseScaleDeg is equal to 2. But then if I try to perform EvalLinearTransfrom over the rescaled ciphertext with the same permutation matrix(aka std::vector<ConstPlaintext>), I get Modulus missmatch error. As long as I understand the problem is that permutation matrix (aka std::vector<ConstPlaintext>), that was precomputed in EvalLinearTransformPrecompute, has a different Modulus. How can I change Modulus of the matrix (aka std::vector<ConstPlaintext>) accordingly to the rescaled ciphertext?

I’m experimenting and my understanding of what happens under the hood of all these functions is really superficial, so I would appreciate any ideas!

My code:

#define _USE_MATH_DEFINES
#include <vector>
#include <cassert>
#include <iostream>
#include <cmath>
#include <fstream>
#include <tuple>
#include <string>
#include "openfhe.h"

std::tuple<lbcrypto::CryptoContext<lbcrypto::DCRTPoly>, lbcrypto::KeyPair<lbcrypto::DCRTPoly>, size_t, size_t> default_fhe_setup_local(\
        const size_t ring_dim, const size_t levelsAvailableAfterBootstrap, const size_t data_length, \
        const std::vector<int32_t> & rotation_index_list){
    // openfhe setup from https://github.com/openfheorg/openfhe-development/blob/main/src/pke/examples/advanced-ckks-bootstrapping.cpp
    lbcrypto::CCParams<lbcrypto::CryptoContextCKKSRNS> parameters;
    lbcrypto::SecretKeyDist secretKeyDist = lbcrypto::UNIFORM_TERNARY;
    parameters.SetSecretKeyDist(secretKeyDist);
    //parameters.SetSecurityLevel(lbcrypto::HEStd_128_classic)
    parameters.SetSecurityLevel(lbcrypto::HEStd_NotSet);
    parameters.SetRingDim(ring_dim);
    parameters.SetNumLargeDigits(3);
    parameters.SetKeySwitchTechnique(lbcrypto::HYBRID);
#if NATIVEINT == 128 && !defined(__EMSCRIPTEN__)
    // Currently, only FIXEDMANUAL and FIXEDAUTO modes are supported for 128-bit CKKS bootstrapping.
    lbcrypto::ScalingTechnique rescaleTech = lbcrypto::FIXEDAUTO;
    usint dcrtBits               = 78;
    usint firstMod               = 89;
#else
    // All modes are supported for 64-bit CKKS bootstrapping.
    lbcrypto::ScalingTechnique rescaleTech = lbcrypto::FIXEDMANUAL;
    usint dcrtBits               = 59;
    usint firstMod               = 60;
#endif
    parameters.SetScalingModSize(dcrtBits);
    parameters.SetScalingTechnique(rescaleTech);
    parameters.SetFirstModSize(firstMod);
    std::vector<uint32_t> levelBudget = {1, 1};
    std::vector<uint32_t> bsgsDim = {0, 0};
    size_t depth = levelsAvailableAfterBootstrap + lbcrypto::FHECKKSRNS::GetBootstrapDepth(levelBudget, secretKeyDist);
    parameters.SetMultiplicativeDepth(depth);
    lbcrypto::CryptoContext<lbcrypto::DCRTPoly> cryptoContext = GenCryptoContext(parameters);
    cryptoContext->Enable(lbcrypto::PKE);
    cryptoContext->Enable(lbcrypto::KEYSWITCH);
    cryptoContext->Enable(lbcrypto::LEVELEDSHE);
    cryptoContext->Enable(lbcrypto::ADVANCEDSHE);
    cryptoContext->Enable(lbcrypto::FHE);
    //plaintext has to have length power of two. Round data_length to the nearest upper power of two
    size_t length = std::pow(2, std::ceil(std::log2(data_length)));
    cryptoContext->EvalBootstrapSetup(levelBudget, bsgsDim, length);
    lbcrypto::KeyPair<lbcrypto::DCRTPoly> keyPair = cryptoContext->KeyGen();
    cryptoContext->EvalMultKeyGen(keyPair.secretKey);
    // Generate bootstrapping keys
    cryptoContext->EvalBootstrapKeyGen(keyPair.secretKey, length);
    //specify rotation we need
    cryptoContext->EvalRotateKeyGen(keyPair.secretKey, rotation_index_list);
    return std::tuple<lbcrypto::CryptoContext<lbcrypto::DCRTPoly>, lbcrypto::KeyPair<lbcrypto::DCRTPoly>, size_t, size_t>(cryptoContext, keyPair, depth, length);
}

int main(){

    size_t N = 8;
    auto cryptoContext_key_depth_length = default_fhe_setup_local(2<<11, 30, N, {});
    auto cc = std::get<0>(cryptoContext_key_depth_length);
    //get keys
    auto keyPair = std::get<1>(cryptoContext_key_depth_length);
    
    //this determines the size of the permutation matrix.
    auto size_of_permutation = 8;
    std::vector<double> a({0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8});

    lbcrypto::Plaintext plaintext = cc->MakeCKKSPackedPlaintext(a, 1, 0, nullptr, size_of_permutation);
    auto ciphertext     = cc->Encrypt(keyPair.publicKey, plaintext);

    //permutation matrix
    std::vector<std::vector<std::complex<double>>> matrix({{0, 1, 0, 0,0,0,0,0}, {1, 0, 0, 0,0,0,0,0}, {0, 0, 0, 1,0,0,0,0},\
     {0, 0, 1, 0,0,0,0,0}, {0, 0, 0, 0,1,0,0,0}, {0, 0, 0, 0,0,1,0,0}, {0, 0, 0, 0,0,0,1,0}, {0, 0, 0, 0,0,0,0,1}});


    lbcrypto::FHECKKSRNS ckksrns;

    uint32_t l = 0;

    ckksrns.EvalBootstrapSetup(*cc, {1, 1}, {0, 0}, size_of_permutation, 0, true);
    
    auto matrix_pre = ckksrns.EvalLinearTransformPrecompute(*cc, matrix, 1.0, l);    

    lbcrypto::Plaintext result;
    cc->Decrypt(keyPair.secretKey, ciphertext, &result);
    result->SetLength(N);
    std::cout << "Before permutation = " << result;

    for(int i=0; i<10; i++){
        if(ciphertext->GetNoiseScaleDeg() ==2){
            std::cout<<"Rescale"<<std::endl;
            cc->RescaleInPlace(ciphertext);
        }
        ciphertext = ckksrns.EvalLinearTransform(matrix_pre, ciphertext);
        std::cout<<"Linear transformed "<<i+1<<" times"<<std::endl;
    }
    
    cc->Decrypt(keyPair.secretKey, ciphertext, &result);
    result->SetLength(N);
    std::cout << "After permutation = " << result;
    
    return 0;    
    
}

Output:

$ OMP_NUM_THREADS=1 ./test/test_EvalLinearTransform.exe
Before permutation = (0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8,  ... ); Estimated precision: 49 bits
Linear transformed 1 times
Rescale
terminate called after throwing an instance of 'lbcrypto::OpenFHEException'
  what():  D:/work/homomorphic_encryption/openfhe-development/src/core/include/lattice/hal/default/poly.h:l.274:operator*=(): Modulus missmatch

OpenFHE provides four different modes for CKKS: FIXEDMANUAL, 'FIXEDAUTO, FLEXIBLEAUTO, FLEXIBLEAUTOEXT. All AUTO modes run rescaling automatically. So you should not need to worry about manually doing it. Also, for CKKS bootstrapping, I recommend using FLEXIBLEAUTO which provides the best accuracy (and faster than FLEXIBLEAUTOEXT).