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!