Segmentation Fault when Implementing EvalStC/EvalCtS Methods in CKKS

Problem Overview

I’m trying to implement direct access methods for Slots-to-Coefficients (EvalStC) and Coefficients-to-Slots (EvalCtS) transformations in CKKS bootstrapping. While EvalBootstrap works perfectly, calling EvalStC results in a segmentation fault.

Background

I’m working on a discrete CKKS implementation and need to access the SlotToCoeff and CoeffToSlot transformations separately, rather than as part of the full bootstrapping process. I’ve added the following methods to the OpenFHE library:

  • Added EvalStC and EvalCtS declarations in all required files (CryptoContext, Scheme base classes, etc.)
  • The methods are properly routed through the architecture layers
  • Using OpenFHE development version with CKKS bootstrapping support

Error Details

Error Message:

Segmentation fault (core dumped)

Location: The segfault occurs in EvalSlotsToCoeffs at this line:

inner = EvalMultExt(fastRotation[0], A[s][G]);

Code Implementation

My EvalStC Implementation

Ciphertext<DCRTPoly> FHECKKSRNS::EvalStC(ConstCiphertext<DCRTPoly> ciphertext) const {
    uint32_t slots = ciphertext->GetSlots();
    
    auto pair = m_bootPrecomMap.find(slots);
    if (pair == m_bootPrecomMap.end()) {
        std::string errorMsg(std::string("Precomputations for ") + std::to_string(slots) +
                             std::string(" slots were not generated") +
                             std::string(" Need to call EvalBootstrapSetup and then EvalBootstrapKeyGen to proceed"));
        OPENFHE_THROW(errorMsg);
    }
    const std::shared_ptr<CKKSBootstrapPrecom> precom = pair->second;
    
    bool isLTBootstrap = (precom->m_paramsEnc[CKKS_BOOT_PARAMS::LEVEL_BUDGET] == 1) &&
                         (precom->m_paramsDec[CKKS_BOOT_PARAMS::LEVEL_BUDGET] == 1);
    
    if (isLTBootstrap) {
        return EvalLinearTransform(precom->m_U0Pre, ciphertext);
    } else {
        return EvalSlotsToCoeffs(precom->m_U0PreFFT, ciphertext);
    }
}

Test Code

// Parameter setup
CCParams<CryptoContextCKKSRNS> parameters;
parameters.SetSecretKeyDist(SPARSE_TERNARY);
parameters.SetSecurityLevel(HEStd_NotSet);
parameters.SetRingDim(1 << 12);
parameters.SetNumLargeDigits(3);
parameters.SetKeySwitchTechnique(HYBRID);
parameters.SetScalingModSize(59);
parameters.SetScalingTechnique(FLEXIBLEAUTO);
parameters.SetFirstModSize(60);

std::vector<uint32_t> levelBudget = {2, 2};
std::vector<uint32_t> bsgsDim = {0, 0};
uint32_t numSlots = 8;

// Context creation and setup
CryptoContext<DCRTPoly> cc = GenCryptoContext(parameters);
cc->Enable(PKE);
cc->Enable(KEYSWITCH);
cc->Enable(LEVELEDSHE);
cc->Enable(ADVANCEDSHE);
cc->Enable(FHE);

// Bootstrap setup
cc->EvalBootstrapSetup(levelBudget, bsgsDim, numSlots);
auto keypair = cc->KeyGen();
cc->EvalMultKeyGen(keypair.secretKey);
cc->EvalBootstrapKeyGen(keypair.secretKey, numSlots);
cc->EvalBootstrapPrecompute(numSlots);

// Test
std::vector<double> x = {0.25, 0.5, 0.75, 1.0, 2.0, 3.0, 4.0, 5.0};
Plaintext pt = cc->MakeCKKSPackedPlaintext(x, 1, 1, nullptr, numSlots);
Ciphertext<DCRTPoly> ct = cc->Encrypt(keypair.publicKey, pt);

// This works fine
auto bootstrapped = cc->EvalBootstrap(ct);

// This causes segmentation fault
auto ctAfterStC = cc->EvalStC(ct);

Debug Information

I’ve added debug output before the segfault:

DEBUG: A.size() = 2
DEBUG: A[0].size() = 7
DEBUG: Before EvalMultExt
  s = 0, G = 0
  fastRotation[0] is null? 0
  fastRotation[0] elements size: 2
  A[s][G] is null? 0

All pointers appear to be valid, but the segfault still occurs inside EvalMultExt.

What I’ve Tried

  1. :white_check_mark: Verified that EvalBootstrapPrecompute is called
  2. :white_check_mark: Checked that precomputation map contains the correct slot count
  3. :white_check_mark: Disabled OpenMP to rule out parallelization issues
  4. :white_check_mark: Added extensive null pointer checks
  5. :white_check_mark: Verified that matrix indices are within bounds

Questions

  1. Is there something special about the state of ciphertexts expected by EvalMultExt when used in this context?
  2. Are there additional setup steps required when calling SlotToCoeff/CoeffToSlot transformations directly?

Any insights or suggestions would be greatly appreciated. I’m happy to provide additional code or debug information if needed.

EvalMultExt works with extended ciphertexts, i.e., the ciphertext is extended from modulus Q to QP. The RNS limbs in fastRotation[0] and A[s][G] (precomputed) should match for EvalMultExt to work correctly. In your case, the number of RNS limbs is probably mismatched as you use EvalStC after bootstrapping. At a high level, the precomputation of the linear maps, which is done as part of EvalBootstrapPrecompute, should be also called for your case (at the right CKKS RNS levels). Basically you will need a custom version of EvalLinearTransformPrecompute for this to work.

1 Like

Thanks for the helpful advice, Yuriy! :)

I have checked my code and added a new function called EvalLinearTransformPrecomputeForLevel which is used to precompute the matrix for StC and CtS. Now my functions EvalStC and EvalCtS can work without errors occurring, but there is a strange phenomenon: the ciphertext remains the same after decryption following EvalStC or EvalCtS. I don’t know where I made a mistake. Could you check my code and tell me where I went wrong?

EvalLinearTransformPrecomputeForLevel

void FHECKKSRNS::EvalLinearTransformPrecomputeForLevel(const CryptoContextImpl<DCRTPoly>& cc,
                                                       uint32_t slots,
                                                       uint32_t current_level,
                                                       std::vector<uint32_t> levelBudget,
                                                       std::vector<uint32_t> dim1) {
    // Create unique key for level-slot pair
    auto key = std::make_pair(slots, current_level);
   
    // If already exists, return directly
    if (m_linearTransformPrecomMap.find(key) != m_linearTransformPrecomMap.end()) {
        return;
    }
    
    const auto cryptoParams = std::dynamic_pointer_cast<CryptoParametersCKKSRNS>(cc.GetCryptoParameters());
    
    // Create new linear transform precomputation object
    m_linearTransformPrecomMap[key] = std::make_shared<CKKSLinearTransformPrecom>();
    auto ltPrecom = m_linearTransformPrecomMap[key];
    ltPrecom->m_slots = slots;
    ltPrecom->m_level = current_level;
    
    // Fix: use dim1[0] as the dimension parameter for encoding
    ltPrecom->m_dim1 = dim1[0];

    uint32_t logSlots = std::log2(slots);
    if (logSlots == 0) {
        logSlots = 1;
    }
    
    std::vector<uint32_t> newBudget = levelBudget;

    if (newBudget[0] > logSlots) {
        std::cerr << "\nWarning, the level budget for encoding is too large. Setting it to " << logSlots << std::endl;
        newBudget[0] = logSlots;
    }
    if (newBudget[0] < 1) {
        std::cerr << "\nWarning, the level budget for encoding can not be zero. Setting it to 1" << std::endl;
        newBudget[0] = 1;
    }

    if (newBudget[1] > logSlots) {
        std::cerr << "\nWarning, the level budget for decoding is too large. Setting it to " << logSlots << std::endl;
        newBudget[1] = logSlots;
    }
    if (newBudget[1] < 1) {
        std::cerr << "\nWarning, the level budget for decoding can not be zero. Setting it to 1" << std::endl;
        newBudget[1] = 1;
    }
    // Calculate FFT parameters using correct dimension parameters
    ltPrecom->m_paramsEnc = GetCollapsedFFTParams(slots, newBudget[0], dim1[0]);
    ltPrecom->m_paramsDec = GetCollapsedFFTParams(slots, newBudget[1], dim1[1]);
   
    // Check if sparse
    uint32_t m = 4 * slots;
    bool isSparse = (cc.GetCyclotomicOrder() != m);
    
    // Generate rotation group and Ksi powers
    std::vector<uint32_t> rotGroup(slots);
    uint32_t fivePows = 1;
    for (uint32_t i = 0; i < slots; ++i) {
        rotGroup[i] = fivePows;
        fivePows *= 5;
        fivePows %= m;
    }
    
    std::vector<std::complex<double>> ksiPows(m + 1);
    for (uint32_t j = 0; j < m; ++j) {
        double angle = 2.0 * M_PI * j / m;
        ksiPows[j].real(cos(angle));
        ksiPows[j].imag(sin(angle));
    }
    ksiPows[m] = ksiPows[0];
    
    // Get current RNS parameters
    auto elementParams = *(cryptoParams->GetElementParams());
    // Exactly same calculation as in bootstrap
    NativeInteger q = elementParams.GetParams()[0]->GetModulus().ConvertToInt();
    double qDouble = q.ConvertToDouble();
    uint128_t factor = ((uint128_t)1 << ((uint32_t)std::round(std::log2(qDouble))));
    double pre = qDouble / factor;
    double k = (cryptoParams->GetSecretKeyDist() == SPARSE_TERNARY) ? K_SPARSE : 1.0;
    double scaleEnc = pre / k;
    double scaleDec = 1 / pre;
   
    // L0 is the total number of RNS, same as bootstrap. Found that L0 is 1 more than our actual set level, 
    // possibly due to some special considerations, such as extra scaling factor?
    uint32_t L0 = elementParams.GetParams().size();
    // for FLEXIBLEAUTOEXT we do not need extra modulus in auxiliary plaintexts
    if (cryptoParams->GetScalingTechnique() == FLEXIBLEAUTOEXT)
        L0 -= 1;
    std::cout << "L0: " << L0 << std::endl;
    std::cout << "current_level: " << current_level << std::endl;

    // Target level - needs modification, currently it seems we don't need extra L0-1
    uint32_t lStC = L0 - current_level - ltPrecom->m_paramsDec[CKKS_BOOT_PARAMS::LEVEL_BUDGET] ;
    uint32_t lCtS = L0 - current_level - ltPrecom->m_paramsEnc[CKKS_BOOT_PARAMS::LEVEL_BUDGET] ;
    std::cout << "lSTC: " << lStC << ", lCtS: " << lCtS << std::endl;

    // Determine transform mode
    bool isLTBootstrap = (ltPrecom->m_paramsEnc[CKKS_BOOT_PARAMS::LEVEL_BUDGET] == 1) &&
                         (ltPrecom->m_paramsDec[CKKS_BOOT_PARAMS::LEVEL_BUDGET] == 1);
    
    // Generate precomputation matrices based on mode
    if (isLTBootstrap) {
        // Linear transform mode: generate transform matrices
        std::vector<std::vector<std::complex<double>>> U0(slots, std::vector<std::complex<double>>(slots));
        std::vector<std::vector<std::complex<double>>> U0hatT(slots, std::vector<std::complex<double>>(slots));
        std::vector<std::vector<std::complex<double>>> U1(slots, std::vector<std::complex<double>>(slots));
        std::vector<std::vector<std::complex<double>>> U1hatT(slots, std::vector<std::complex<double>>(slots));
        
        for (size_t i = 0; i < slots; i++) {
            for (size_t j = 0; j < slots; j++) {
                U0[i][j] = ksiPows[(j * rotGroup[i]) % m];
                U0hatT[j][i] = std::conj(U0[i][j]);
                U1[i][j] = std::complex<double>(0, 1) * U0[i][j];
                U1hatT[j][i] = std::conj(U1[i][j]);
            }
        }
        
        if (!isSparse) {
            // The function will adjust RNS basis count based on depth
            ltPrecom->m_U0hatTPre = EvalLinearTransformPrecompute(cc, U0hatT, scaleEnc, lCtS);
            ltPrecom->m_U0Pre = EvalLinearTransformPrecompute(cc, U0, scaleDec, lStC);
        } else {
            ltPrecom->m_U0hatTPre = EvalLinearTransformPrecompute(cc, U0hatT, U1hatT, 0, scaleEnc, lCtS);
            ltPrecom->m_U0Pre = EvalLinearTransformPrecompute(cc, U0, U1, 1, scaleDec, lStC);
        }
    } else {
        // FFT mode: generate layered FFT matrices
        ltPrecom->m_U0hatTPreFFT = EvalCoeffsToSlotsPrecompute(cc, ksiPows, rotGroup, false, scaleEnc, lCtS);
        ltPrecom->m_U0PreFFT = EvalSlotsToCoeffsPrecompute(cc, ksiPows, rotGroup, false, scaleDec, lStC);
    }
}

EvalStC

Ciphertext<DCRTPoly> FHECKKSRNS::EvalStC(ConstCiphertext<DCRTPoly> ciphertext) const {
    
    const auto cryptoParams = std::dynamic_pointer_cast<CryptoParametersCKKSRNS>(ciphertext->GetCryptoParameters());
    if (cryptoParams->GetKeySwitchTechnique() != HYBRID)
        OPENFHE_THROW("CKKS Bootstrapping is only supported for the Hybrid key switching method.");

    auto cc        = ciphertext->GetCryptoContext();
    uint32_t slots = ciphertext->GetSlots();
    uint32_t current_level = ciphertext->GetLevel();

    // Find precomputed data
    auto key = std::make_pair(slots, current_level);
    auto ltPeomIt = m_linearTransformPrecomMap.find(key);
    if (ltPeomIt == m_linearTransformPrecomMap.end()) {
        OPENFHE_THROW("Linear transform precomputations not found for slots=" + 
                      std::to_string(slots) + ", level=" + std::to_string(current_level) + 
                      ". Please call EvalLinearTransformPrecomputeForLevel first." +
                      " Please note that current_level is not the depth remaining!");
    }

    const auto& ltPrecom = ltPeomIt->second;

    auto ctAfterStC = ciphertext->Clone();

    auto algo = cc->GetScheme();

    // Determine computation mode
    bool isLTBootstrap = (ltPrecom->m_paramsEnc[CKKS_BOOT_PARAMS::LEVEL_BUDGET] == 1) &&
                         (ltPrecom->m_paramsDec[CKKS_BOOT_PARAMS::LEVEL_BUDGET] == 1);
    
    // Execute linear transformation
    if(isLTBootstrap){
        EvalLinearTransform(ltPrecom->m_U0Pre, ctAfterStC);
    } else{
        EvalSlotsToCoeffs(ltPrecom->m_U0PreFFT, ctAfterStC);
    }

    // Post-processing
    // #1: Conjugate to eliminate imaginary part, as we don't actually need it, perform error cleanup
    // auto evalkeymap = cc->GetEvalAutomorphismKeyMap(ctAfterStC->GetKeyTag());
    // auto conj       = Conjugate(ctAfterStC, evalkeymap);
    // cc->EvalAddInPlace(ctAfterStC, conj);
    // #2: Noise cleanup - I think?
    if(cryptoParams->GetScalingTechnique() == FIXEDMANUAL) {
        while(ctAfterStC->GetNoiseScaleDeg() > 1){
            cc->ModReduceInPlace(ctAfterStC);
        }
    } else{
        if(ctAfterStC->GetNoiseScaleDeg() == 2){
            algo->ModReduceInternalInPlace(ctAfterStC, BASE_NUM_LEVELS_TO_DROP);
        }
    }

    return ctAfterStC;
}

EvalCtS

Ciphertext<DCRTPoly> FHECKKSRNS::EvalCtS(ConstCiphertext<DCRTPoly> ciphertext) const {
    
    const auto cryptoParams = std::dynamic_pointer_cast<CryptoParametersCKKSRNS>(ciphertext->GetCryptoParameters());
    if (cryptoParams->GetKeySwitchTechnique() != HYBRID)
        OPENFHE_THROW("CKKS Bootstrapping is only supported for the Hybrid key switching method.");
// #if NATIVEINT == 128 && !defined(__EMSCRIPTEN__)
//     if (cryptoParams->GetScalingTechnique() == FLEXIBLEAUTO || cryptoParams->GetScalingTechnique() == FLEXIBLEAUTOEXT)
//         OPENFHE_THROW("128-bit CKKS Bootstrapping is supported for FIXEDMANUAL and FIXEDAUTO methods only.");
// #endif
    auto cc        = ciphertext->GetCryptoContext();
    uint32_t slots = ciphertext->GetSlots();
    uint32_t current_level = ciphertext->GetLevel();

    // Find precomputed data
    auto key = std::make_pair(slots, current_level);
    auto ltPeomIt = m_linearTransformPrecomMap.find(key);
    if (ltPeomIt == m_linearTransformPrecomMap.end()) {
        OPENFHE_THROW("Linear transform precomputations not found for slots=" + 
                      std::to_string(slots) + ", level=" + std::to_string(current_level) + 
                      ". Please call EvalLinearTransformPrecomputeForLevel first.");
    }

    const auto& ltPrecom = ltPeomIt->second;

    auto ctAfterCtS = ciphertext->Clone();

    auto algo = cc->GetScheme();

    // Determine computation mode
    bool isLTBootstrap = (ltPrecom->m_paramsEnc[CKKS_BOOT_PARAMS::LEVEL_BUDGET] == 1) &&
                         (ltPrecom->m_paramsDec[CKKS_BOOT_PARAMS::LEVEL_BUDGET] == 1);
    
    // Execute linear transformation
    if(isLTBootstrap){
        EvalLinearTransform(ltPrecom->m_U0hatTPre, ctAfterCtS);
    } else{
        EvalCoeffsToSlots(ltPrecom->m_U0hatTPreFFT, ctAfterCtS);
    }
    // Post-processing
    // #1: Conjugate to eliminate imaginary part, as we don't actually need it, perform error cleanup
    auto evalkeymap = cc->GetEvalAutomorphismKeyMap(ctAfterCtS->GetKeyTag());
    auto conj       = Conjugate(ctAfterCtS, evalkeymap);
    cc->EvalAddInPlace(ctAfterCtS, conj);
    // #2: Noise cleanup - I think?
    if(cryptoParams->GetScalingTechnique() == FIXEDMANUAL) {
        while(ctAfterCtS->GetNoiseScaleDeg() > 1){
            cc->ModReduceInPlace(ctAfterCtS);
        }
    } else{
        if(ctAfterCtS->GetNoiseScaleDeg() == 2){
            algo->ModReduceInternalInPlace(ctAfterCtS, BASE_NUM_LEVELS_TO_DROP);
        }
    }

    return ctAfterCtS;
}

Test Code

void StC_CtS_example() {
    std::cout << "--------------------------------- STC CTS EXAMPLE ---------------------------------"
              << std::endl;

    CCParams<CryptoContextCKKSRNS> parameters;

    SecretKeyDist secretKeyDist = SPARSE_TERNARY;

    parameters.SetSecretKeyDist(secretKeyDist);

    parameters.SetSecurityLevel(HEStd_NotSet);
    parameters.SetRingDim(1 << 12);
   
    parameters.SetNumLargeDigits(3);
    parameters.SetKeySwitchTechnique(HYBRID);


#if NATIVEINT == 128 && !defined(__EMSCRIPTEN__)
    ScalingTechnique rescaleTech = FIXEDAUTO;
    usint dcrtBits               = 78;
    usint firstMod               = 89;
#else
    ScalingTechnique rescaleTech = FLEXIBLEAUTO;
    usint dcrtBits               = 59;
    usint firstMod               = 60;
#endif
    parameters.SetScalingModSize(dcrtBits);
    parameters.SetScalingTechnique(rescaleTech);
    parameters.SetFirstModSize(firstMod);

    std::vector<uint32_t> levelBudget = {2, 2};

    std::vector<uint32_t> bsgsDim = {0, 0};

    uint32_t levelsAvailableAfterBootstrap = 8;
    usint depth = levelsAvailableAfterBootstrap + FHECKKSRNS::GetBootstrapDepth(levelBudget, secretKeyDist);
    parameters.SetMultiplicativeDepth(depth);
    std::cout << "BTS depth is: " << FHECKKSRNS::GetBootstrapDepth(levelBudget, secretKeyDist) << std::endl;
    std::cout << "Multiplicative Depth is:" << depth << std::endl;


    CryptoContext<DCRTPoly> cc = GenCryptoContext(parameters);
 
    cc ->Enable(PKE);
    cc ->Enable(KEYSWITCH);
    cc ->Enable(LEVELEDSHE);
    cc ->Enable(ADVANCEDSHE);
    cc ->Enable(FHE);
    cc ->Enable(DISCRETECKKS);

   
    uint32_t numSlots = 1 << 3;
    std::cout << "Slot Number is:" << numSlots << std::endl;

    cc->EvalBootstrapSetup(levelBudget, bsgsDim, numSlots);
    std::cout << "Bootstrap Setup is done." << std::endl;
    

 
    auto keypair = cc->KeyGen();
    cc->EvalMultKeyGen(keypair.secretKey);
    cc->EvalBootstrapKeyGen(keypair.secretKey, numSlots);

    cc->EvalBootstrapPrecompute(numSlots);

    std::vector<double> x = {0.25, 0.5, 0.75, 1.0, 2.0, 3.0, 4.0, 5.0};


    Plaintext pt = cc->MakeCKKSPackedPlaintext(x, 1, 1, nullptr, numSlots);
    pt->SetLength(numSlots);
    std::cout << "Input: " << pt;
    
    Ciphertext<DCRTPoly> ct = cc->Encrypt(keypair.publicKey, pt);
    // std::cout << "Ciphertext: " << ct << std::endl;
    std::cout << "Initial number of levels remaining: " << depth - ct->GetLevel() << std::endl;

    uint32_t current_level = ct->GetLevel();
    std::cout << "Current level is: " << current_level << std::endl;
    uint32_t current_depth = depth - current_level;
    std::cout << "Current depth is: " << current_depth << std::endl;
    
    // Precompute
    std::cout << "Start Linear Transform Precomputation..." << std::endl;
    cc->EvalLinearTransformPrecomputeForLevel(numSlots, current_level, levelBudget, bsgsDim);
    cc->EvalLinearTransformPrecomputeForLevel(numSlots, current_level + 2, levelBudget, bsgsDim);
    std::cout << "Linear Transform Precomputation is done." << std::endl;

    // StC
    auto ctAfterStC = cc->EvalStC(ct);
    std::cout << "SlotToCoeff SUCESS!" << std::endl;

    // CtS
    // auto ctAfterCtS = cc->EvalCtS(ctAfterStC);
    // auto ctAfterCtS = cc->EvalCtS(ct);
    // std::cout << "CoeffToSlot SUCESS!" << std::endl;

    Plaintext result;
    cc->Decrypt(keypair.secretKey, ctAfterStC, &result);
    result->SetLength(numSlots);
    std::cout << "Output after StC \n\t" << result << std::endl;
}

Terminal Output

--------------------------------- STC CTS EXAMPLE ---------------------------------
BTS depth is: 14
Multiplicative Depth is:22
Slot Number is:8
Bootstrap Setup is done.
Input: (0.25, 0.5, 0.75, 1, 2, 3, 4, 5,  ... ); Estimated precision: 59 bits
Initial number of levels remaining: 21
Current level is: 1
Current depth is: 21
Start Linear Transform Precomputation...
L0: 23
current_level: 1
lSTC: 20, lCtS: 20
Linear Transform Precomputation is done.
SlotToCoeff SUCESS!
Output after StC
        (0.25, 0.5, 0.75, 1, 2, 3, 4, 5,  ... ); Estimated precision: 49 bits

Any advice would be helpful! Please feel free to share your insights!