diff --git a/benchmark/src/ckks-functional-bootstrapping.cpp b/benchmark/src/ckks-functional-bootstrapping.cpp index 38c8749a0..c729d0df5 100644 --- a/benchmark/src/ckks-functional-bootstrapping.cpp +++ b/benchmark/src/ckks-functional-bootstrapping.cpp @@ -177,10 +177,10 @@ struct fbt_config { while (state.KeepRunning()) { if (binaryLUT) cc->EvalFBTSetup(coeffint, numSlotsCKKS, t.PInput, t.POutput, t.Bigq, keyPair.publicKey, {0, 0}, t.lvlb, 0, - 0, t.order); + 0, t.order, t.numSlots); else cc->EvalFBTSetup(coeffcomp, numSlotsCKKS, t.PInput, t.POutput, t.Bigq, keyPair.publicKey, {0, 0}, t.lvlb, 0, - 0, t.order); + 0, t.order, t.numSlots); } cc->ClearStaticMapsAndVectors(); @@ -242,10 +242,10 @@ struct fbt_config { if (binaryLUT) cc->EvalFBTSetup(coeffint, numSlotsCKKS, t.PInput, t.POutput, t.Bigq, keyPair.publicKey, {0, 0}, t.lvlb, 0, 0, - t.order); + t.order, t.numSlots); else cc->EvalFBTSetup(coeffcomp, numSlotsCKKS, t.PInput, t.POutput, t.Bigq, keyPair.publicKey, {0, 0}, t.lvlb, 0, 0, - t.order); + t.order, t.numSlots); for (auto _ : state) { state.PauseTiming(); @@ -315,10 +315,10 @@ struct fbt_config { if (binaryLUT) cc->EvalFBTSetup(coeffint, numSlotsCKKS, t.PInput, t.POutput, t.Bigq, keyPair.publicKey, {0, 0}, t.lvlb, 0, 0, - t.order); + t.order, t.numSlots); else cc->EvalFBTSetup(coeffcomp, numSlotsCKKS, t.PInput, t.POutput, t.Bigq, keyPair.publicKey, {0, 0}, t.lvlb, 0, 0, - t.order); + t.order, t.numSlots); cc->EvalBootstrapKeyGen(keyPair.secretKey, numSlotsCKKS); cc->EvalMultKeyGen(keyPair.secretKey); @@ -413,10 +413,10 @@ struct fbt_config { if (binaryLUT) cc->EvalFBTSetup(coeffintMod, numSlotsCKKS, t.POutput, t.PInput, t.Bigq, keyPair.publicKey, {0, 0}, t.lvlb, 0, - 0, t.order); + 0, t.order, t.numSlots); else cc->EvalFBTSetup(coeffcompMod, numSlotsCKKS, t.POutput, t.PInput, t.Bigq, keyPair.publicKey, {0, 0}, t.lvlb, 0, - 0, t.order); + 0, t.order, t.numSlots); cc->EvalBootstrapKeyGen(keyPair.secretKey, numSlotsCKKS); cc->EvalMultKeyGen(keyPair.secretKey); diff --git a/src/pke/examples/CKKS_FUNCTIONAL_BOOTSTRAPING.md b/src/pke/examples/CKKS_FUNCTIONAL_BOOTSTRAPING.md index 1e5fbff74..c9afe69ac 100644 --- a/src/pke/examples/CKKS_FUNCTIONAL_BOOTSTRAPING.md +++ b/src/pke/examples/CKKS_FUNCTIONAL_BOOTSTRAPING.md @@ -46,8 +46,19 @@ The RLWE and CKKS schemes are compatible in the sense that they can share the sa makes the conversion between RLWE and CKKS to essentially be modulus switching. A vector encoded in RLWE can have as many elements as the ring dimension. If this is the case (the number of elements is between half -the ring dimension and the ring dimension), this corresponds to a full complex packing in CKKS, where the complex slots are also -populated. Otherwise, real CKKS packing (full or sparse) is used. +the ring dimension and the ring dimension), this corresponds to a full complex packing in CKKS, where the imaginary parts of the slots are also +populated, and functional bootstrapping processes two ciphertexts (the real and imaginary channels of the homomorphic encoding). +Otherwise, real CKKS packing (full or sparse) is used, and functional bootstrapping processes a single ciphertext. The number of +elements is passed to `EvalFBTSetup` as `numValues`, which selects the layout independently of the CKKS data type; it defaults to +as many elements as the slots can hold (the ring dimension for full packing). In particular, for a number of elements equal to half +the ring dimension, the values occupy the real parts of the N/2 slots, and passing `numValues` = N/2 halves the cost of the +approximate modular reduction and of the look-up table evaluation compared to the full complex packing. + +Functional bootstrapping itself does not need a particular CKKS data type. With full complex packing, however, the imaginary +parts of the slots carry half of the values, so the CKKS data type should be set to COMPLEX (`SetCKKSDataType(COMPLEX)`) if +complex plaintexts are encoded or the CKKS ciphertext is decrypted before `EvalHomDecoding`, e.g., in a leveled computation +between `EvalFBTNoDecoding` and `EvalHomDecoding`: with the default REAL data type, encoding zeroes the imaginary parts of a +plaintext and decryption treats the imaginary parts of the slots as noise. Note that the message is encoded in the coefficients in the RLWE scheme, but when converted to CKKS, the coefficients are held in the slots of the CKKS ciphertexts. To be able to apply CKKS leveled computations, we need to homomorphically transform from @@ -113,6 +124,7 @@ The SPARSE_TERNARY distribution is the distribution used in the original CKKS pa where the Hamming weight of the secret key is set to 192. For the set number of overflows in bootstrapping, K = 28, this distribution leads to a larger probability of failure (about 2^-22 for 2^16 slots), so it should not be used in deployments. Compared to SPARSE_ENCAPSULATED, choosing this distribution requires an extra level in the complex exponential approximation +(a degree-69 Chebyshev interpolation over [-28, 28] instead of degree 46 over [-16, 16], used for all supported LUT sizes) to achieve correctness. This is the distribution used for the benchmarks in [[AKP25]](https://eprint.iacr.org/2024/1623.pdf). The SPARSE_ENCAPSULATED distribution (described in [[BTH22]](https://eprint.iacr.org/2022/024.pdf)) uses a Hamming weight of 32 @@ -134,10 +146,10 @@ used when uniform ternary secrets are required for compliance with them. It is h bootstrapping: the number of overflows is bounded by K = 672 (probability of failure below 2^-73 for N = 2^16 and 2^-30 for N = 2^17 with full packing), and the complex exponential (or cosine, for the binary case) is approximated by a degree-104 Chebyshev interpolation over [-672, 672] followed by six double-angle iterations (instead of degree -64/46 and two double-angle iterations for the sparse distributions). This increases the -multiplicative depth of functional bootstrapping by 5 levels. In addition, since the mod-raised message is scaled down by K -before the homomorphic encoding, larger scaling factors (roughly 5-9 more bits, depending on the parameters) are needed to -achieve the same output noise as for the sparse distributions. +69/46 and two double-angle iterations for the sparse distributions). This increases the multiplicative depth of functional +bootstrapping by 5 levels compared to SPARSE_ENCAPSULATED (4 compared to SPARSE_TERNARY). In addition, since the mod-raised +message is scaled down by K before the homomorphic encoding, larger scaling factors (roughly 5-9 more bits, depending on the +parameters) are needed to achieve the same output noise as for the sparse distributions. **Scaling techniques** All scaling techniques (FIXEDMANUAL, FIXEDAUTO, FLEXIBLEAUTO, FLEXIBLEAUTOEXT, COMPOSITESCALINGAUTO, and @@ -190,8 +202,11 @@ coefficient vector c = (c_0, c_1, ..., c_d) has to satisfy $$g(m) \approx 2 \cdot \mathrm{Re}\left(\sum_{j=0}^{d} c_j e^{\pi i j m}\right), \quad m \in [-1/2, 1/2).$$ In other words, c_0 is half of the constant term of the series and c_j, for j > 0, are the one-sided coefficients of the -series (the conjugate terms are added internally). The output is always real-valued; with CKKSDataType COMPLEX, the imaginary -parts of the input slots are discarded, so CKKSDataType REAL should be used unless complex values are needed elsewhere in the +series (the conjugate terms are added internally). The function is applied slot-wise. With CKKSDataType REAL, it is applied to +the (real) slot values and a single ciphertext goes through the complex exponential and the series. With CKKSDataType COMPLEX, it +is applied to the real and imaginary parts separately (a slot a + ib yields f(a) + i f(b)): the two parts are processed as two +ciphertexts, as in the slots-encoding CKKS bootstrapping and in the complex full packing of the discrete functional bootstrapping, +so CKKSDataType REAL should be used unless complex values are needed elsewhere in the computation. The coefficients are computed offline with the Python scripts in [fefbt-python](https://github.com/openfheorg/fefbt-python), diff --git a/src/pke/examples/FE-functional-bootstrapping-ckks.cpp b/src/pke/examples/FE-functional-bootstrapping-ckks.cpp index a01d91763..6c64c37dc 100644 --- a/src/pke/examples/FE-functional-bootstrapping-ckks.cpp +++ b/src/pke/examples/FE-functional-bootstrapping-ckks.cpp @@ -234,7 +234,8 @@ int main() { levelsAfterBootstrapping; CCParams parameters; - parameters.SetCKKSDataType(COMPLEX); + // the input is real-valued; with COMPLEX the function would also be applied to the (zero) imaginary parts + parameters.SetCKKSDataType(REAL); parameters.SetSecretKeyDist(skd); parameters.SetSecurityLevel(HEStd_NotSet); parameters.SetRingDim(ringDim); diff --git a/src/pke/examples/functional-bootstrapping-ckks.cpp b/src/pke/examples/functional-bootstrapping-ckks.cpp index 61b58d4ee..6ab1e0bfb 100644 --- a/src/pke/examples/functional-bootstrapping-ckks.cpp +++ b/src/pke/examples/functional-bootstrapping-ckks.cpp @@ -68,17 +68,28 @@ int main() { std::cerr << "=====Boolean LUT order 2 sparsely packed=====" << std::endl << std::endl; ArbitraryLUT(QBFVINIT, BigInteger(2), BigInteger(2), (BigInteger(1) << 33), (BigInteger(1) << 33), 1, 2, 8, 4096, [](int64_t x) { return (x % 2 - 2 / 2) % 2; }); - std::cerr << "=====Boolean LUT order 1 fully packed=====" << std::endl << std::endl; - ArbitraryLUT(QBFVINIT, BigInteger(2), BigInteger(2), (BigInteger(1) << 33), (BigInteger(1) << 33), 1, 1, 1024, 4096, + // N values: complex full packing, two ciphertexts go through the modular reduction + std::cerr << "=====Boolean LUT order 1 fully packed (N values)=====" << std::endl << std::endl; + ArbitraryLUT(QBFVINIT, BigInteger(2), BigInteger(2), (BigInteger(1) << 33), (BigInteger(1) << 33), 1, 1, 4096, 4096, + [](int64_t x) { return (x % 2 - 2 / 2) % 2; }); + // N/2 values: real full packing, a single ciphertext goes through the modular reduction + std::cerr << "=====Boolean LUT order 1 fully packed (N/2 values)=====" << std::endl << std::endl; + ArbitraryLUT(QBFVINIT, BigInteger(2), BigInteger(2), (BigInteger(1) << 33), (BigInteger(1) << 33), 1, 1, 2048, 4096, [](int64_t x) { return (x % 2 - 2 / 2) % 2; }); // LUT with 8-bit input and 4-bit output std::cerr << "=====8-to-4 bit LUT order 1 sparsely packed=====" << std::endl << std::endl; ArbitraryLUT(QBFVINIT, BigInteger(256), BigInteger(16), (BigInteger(1) << 47), (BigInteger(1) << 47), 32, 1, 8, 4096, [](int64_t x) { return (x % 256 - 16 / 2) % 16; }); + std::cerr << "=====8-to-4 bit LUT order 1 fully packed (N values)=====" << std::endl << std::endl; + ArbitraryLUT(QBFVINIT, BigInteger(256), BigInteger(16), (BigInteger(1) << 47), (BigInteger(1) << 47), 32, 1, 4096, + 4096, [](int64_t x) { return (x % 256 - 16 / 2) % 16; }); + std::cerr << "=====8-to-4 bit LUT order 1 fully packed (N/2 values)=====" << std::endl << std::endl; + ArbitraryLUT(QBFVINIT, BigInteger(256), BigInteger(16), (BigInteger(1) << 47), (BigInteger(1) << 47), 32, 1, 2048, + 4096, [](int64_t x) { return (x % 256 - 16 / 2) % 16; }); std::cerr << "\n\n*2.* Compute multiple functions over the same ciphertext." << std::endl << std::endl; // Two LUTs with 8-bit input and 8-bit output and intermediate leveled computations - std::cerr << "=====Multivalue bootstrapping for two 8-to-8 bit LUTs order 1 fully packed=====" << std::endl + std::cerr << "=====Multivalue bootstrapping for two 8-to-8 bit LUTs order 1 sparsely packed=====" << std::endl << std::endl; MultiValueBootstrapping(QBFVINIT, BigInteger(256), BigInteger(256), (BigInteger(1) << 47), (BigInteger(1) << 47), 32, 1, 256, 2048, 1); @@ -90,11 +101,11 @@ int main() { << std::endl; MultiPrecisionSign(QBFVINIT, BigInteger(4096), BigInteger(2), (BigInteger(1) << 46), (BigInteger(1) << 35), 1, 1, 1, 32, 2048); - std::cerr << "=====Sign evaluation of a 12-bit input using 4-bit digits order 1 fully packed=====" << std::endl + std::cerr << "=====Sign evaluation of a 12-bit input using 4-bit digits order 1 sparsely packed=====" << std::endl << std::endl; MultiPrecisionSign(QBFVINIT, BigInteger(4096), BigInteger(16), (BigInteger(1) << 48), (BigInteger(1) << 40), 32, 8, 1, 64, 2048); - std::cerr << "=====Sign evaluation of a 32-bit input using 8-bit digits order 1 fully packed=====" << std::endl + std::cerr << "=====Sign evaluation of a 32-bit input using 8-bit digits order 1 sparsely packed=====" << std::endl << std::endl; MultiPrecisionSign(QBFVINITLARGE, BigInteger(1) << 32, BigInteger(256), BigInteger(1) << 71, BigInteger(1) << 47, 256, 32, 1, 64, 2048); @@ -107,7 +118,9 @@ void ArbitraryLUT(BigInteger QBFVInit, BigInteger PInput, BigInteger POutput, Bi std::function func) { /* 1. Figure out whether sparse packing or full packing should be used. * numSlots represents the number of values to be encrypted in BFV. - * If this number is the same as the ring dimension, then the CKKS slots is half. + * If this number is the same as the ring dimension, then the CKKS slots is half (the imaginary parts of + * the slots carry the second half of the values). EvalFBTSetup takes the number of values to select the + * layout: for N/2 values, a single ciphertext goes through the modular reduction. */ bool flagSP = (numSlots <= ringDim / 2); // sparse packing auto numSlotsCKKS = flagSP ? numSlots : numSlots / 2; @@ -198,10 +211,10 @@ void ArbitraryLUT(BigInteger QBFVInit, BigInteger PInput, BigInteger POutput, Bi if (binaryLUT) cc->EvalFBTSetup(coeffint, numSlotsCKKS, PInput, POutput, Bigq, keyPair.publicKey, {0, 0}, lvlb, - levelsAvailableAfterBootstrap, 0, order); + levelsAvailableAfterBootstrap, 0, order, numSlots); else cc->EvalFBTSetup(coeffcomp, numSlotsCKKS, PInput, POutput, Bigq, keyPair.publicKey, {0, 0}, lvlb, - levelsAvailableAfterBootstrap, 0, order); + levelsAvailableAfterBootstrap, 0, order, numSlots); cc->EvalBootstrapKeyGen(keyPair.secretKey, numSlotsCKKS); cc->EvalMultKeyGen(keyPair.secretKey); @@ -255,7 +268,9 @@ void MultiValueBootstrapping(BigInteger QBFVInit, BigInteger PInput, BigInteger uint32_t levelsComputation) { /* 1. Figure out whether sparse packing or full packing should be used. * numSlots represents the number of values to be encrypted in BFV. - * If this number is the same as the ring dimension, then the CKKS slots is half. + * If this number is the same as the ring dimension, then the CKKS slots is half (the imaginary parts of + * the slots carry the second half of the values). EvalFBTSetup takes the number of values to select the + * layout: for N/2 values, a single ciphertext goes through the modular reduction. */ bool flagSP = (numSlots <= ringDim / 2); // sparse packing auto numSlotsCKKS = flagSP ? numSlots : numSlots / 2; @@ -360,10 +375,10 @@ void MultiValueBootstrapping(BigInteger QBFVInit, BigInteger PInput, BigInteger if (binaryLUT) cc->EvalFBTSetup(coeffint1, numSlotsCKKS, PInput, POutput, Bigq, keyPair.publicKey, {0, 0}, lvlb, - levelsAvailableAfterBootstrap, levelsComputation, order); + levelsAvailableAfterBootstrap, levelsComputation, order, numSlots); else cc->EvalFBTSetup(coeffcomp1, numSlotsCKKS, PInput, POutput, Bigq, keyPair.publicKey, {0, 0}, lvlb, - levelsAvailableAfterBootstrap, levelsComputation, order); + levelsAvailableAfterBootstrap, levelsComputation, order, numSlots); cc->EvalBootstrapKeyGen(keyPair.secretKey, numSlotsCKKS); cc->EvalMultKeyGen(keyPair.secretKey); @@ -499,7 +514,9 @@ void MultiPrecisionSign(BigInteger QBFVInit, BigInteger PInput, BigInteger PDigi uint64_t scaleTHI, uint64_t scaleStepTHI, size_t order, uint32_t numSlots, uint32_t ringDim) { /* 1. Figure out whether sparse packing or full packing should be used. * numSlots represents the number of values to be encrypted in BFV. - * If this number is the same as the ring dimension, then the CKKS slots is half. + * If this number is the same as the ring dimension, then the CKKS slots is half (the imaginary parts of + * the slots carry the second half of the values). EvalFBTSetup takes the number of values to select the + * layout: for N/2 values, a single ciphertext goes through the modular reduction. */ bool flagSP = (numSlots <= ringDim / 2); // sparse packing auto numSlotsCKKS = flagSP ? numSlots : numSlots / 2; @@ -614,10 +631,10 @@ void MultiPrecisionSign(BigInteger QBFVInit, BigInteger PInput, BigInteger PDigi if (binaryLUT) cc->EvalFBTSetup(coeffintMod, numSlotsCKKS, PDigit, PInput, Bigq, keyPair.publicKey, {0, 0}, lvlb, - levelsAvailableAfterBootstrap, 0, order); + levelsAvailableAfterBootstrap, 0, order, numSlots); else cc->EvalFBTSetup(coeffcompMod, numSlotsCKKS, PDigit, PInput, Bigq, keyPair.publicKey, {0, 0}, lvlb, - levelsAvailableAfterBootstrap, 0, order); + levelsAvailableAfterBootstrap, 0, order, numSlots); cc->EvalBootstrapKeyGen(keyPair.secretKey, numSlotsCKKS); diff --git a/src/pke/include/cryptocontext.h b/src/pke/include/cryptocontext.h index 4e523e727..7a8838b79 100644 --- a/src/pke/include/cryptocontext.h +++ b/src/pke/include/cryptocontext.h @@ -3774,9 +3774,10 @@ class CryptoContextImpl : public Serializable { * * The message is embedded into half of the series period (t = m/2), so the input must lie in [-1/2, 1/2) * and @p coefficients must be the Fourier extension of the target function over that domain. The result - * is 2*Re(c_0 + sum_{j>=1} c_j exp(2*Pi*i*j*t)) and is therefore always real-valued: with CKKSDataType - * COMPLEX the imaginary part of each input slot is discarded and the output slots have zero imaginary - * part. Use CKKSDataType REAL unless complex intermediates are needed elsewhere in the computation. + * is 2*Re(c_0 + sum_{j>=1} c_j exp(2*Pi*i*j*t)), applied slot-wise. With CKKSDataType REAL it is applied + * to the (real) slot values. With CKKSDataType COMPLEX it is applied to the real and imaginary parts + * separately (a slot a + ib yields f(a) + i f(b)), which processes two ciphertexts instead of one, so use + * CKKSDataType REAL unless complex values are needed. * * @param ciphertext Input ciphertext, with slot values in [-1/2, 1/2). * @param coefficients Fourier coefficients c_j of the target function, c_0 first. @@ -3845,14 +3846,25 @@ class CryptoContextImpl : public Serializable { * @param depthLeveledComputation Depth reserved for a leveled computation between EvalFBTNoDecoding and * EvalHomDecoding. * @param order Order of the trigonometric Hermite interpolation (1, 2 or 3). + * @param numValues Number of values encrypted in the RLWE input (0 = as many as the slots + * hold: N for full packing, numSlots for sparse packing). With full packing, + * more than N/2 values occupy both coefficient halves and two ciphertexts + * are processed; N/2 values occupy the lower half and one ciphertext is + * processed, at half the cost of the modular reduction and the look-up table. + * The layout does not depend on the CKKS data type. With more than N/2 values, + * the imaginary parts of the slots carry data, so set CKKSDataType COMPLEX if + * complex plaintexts are encoded or the CKKS ciphertext is decrypted before + * EvalHomDecoding (REAL encoding zeroes the imaginary parts and REAL + * decryption treats them as noise). */ template void EvalFBTSetup(const std::vector& coeffs, uint32_t numSlots, const BigInteger& PIn, const BigInteger& POut, const BigInteger& Bigq, const PublicKey& pubKey, const std::vector& dim1, const std::vector& levelBudget, - uint32_t lvlsAfterBoot = 0, uint32_t depthLeveledComputation = 0, size_t order = 1) { + uint32_t lvlsAfterBoot = 0, uint32_t depthLeveledComputation = 0, size_t order = 1, + uint32_t numValues = 0) { m_scheme->EvalFBTSetup(*this, coeffs, numSlots, PIn, POut, Bigq, pubKey, dim1, levelBudget, lvlsAfterBoot, - depthLeveledComputation, order); + depthLeveledComputation, order, numValues); } /** diff --git a/src/pke/include/scheme/ckksrns/ckksrns-fhe.h b/src/pke/include/scheme/ckksrns/ckksrns-fhe.h index 065490b70..1caf5f7a3 100644 --- a/src/pke/include/scheme/ckksrns/ckksrns-fhe.h +++ b/src/pke/include/scheme/ckksrns/ckksrns-fhe.h @@ -120,6 +120,11 @@ class CKKSBootstrapPrecom { enum BootstrapVariant : uint32_t { BOOTSTRAP = 0, FBT = 1, FEFBT = 2 }; BootstrapVariant m_variant = BOOTSTRAP; + // FBT only: true when a full packing imports more than N/2 RLWE values (both coefficient halves), so the real + // and imaginary channels of the homomorphic encoding are processed as two ciphertexts. Set by EvalFBTSetup from + // its numValues argument; not serialized, for the same reason as m_variant. + bool m_FBTComplexPacking = false; + template void save(Archive& ar) const { ar(cereal::make_nvp("dim1_Enc", m_paramsEnc.g)); @@ -257,8 +262,9 @@ class FHECKKSRNS : public FHERNS { * Refreshes a ciphertext and evaluates a function on it in one pass by evaluating the Fourier extension of * the function over the bootstrapped message: SlotsToCoeffs, modulus raise, CoeffsToSlots, the complex * exponential exp(2*Pi*i*t) with t = m/2 (the message is embedded into half of the period), the series - * sum_j c_j exp(2*Pi*i*j*t) and twice its real part. The result is real-valued; with CKKSDataType COMPLEX the - * imaginary parts of the input slots are discarded. + * sum_j c_j exp(2*Pi*i*j*t) and twice its real part. The function is applied slot-wise: for CKKSDataType REAL + * to the (real) slot values, and for COMPLEX to the real and imaginary parts separately (a slot a + ib yields + * f(a) + i f(b)), the two channels being processed as two ciphertexts as in EvalBootstrapStCFirst. * * @param ciphertext the input ciphertext, with slot values in [-1/2, 1/2) * @param coefficients Fourier coefficients c_j of the target function over [-1/2, 1/2), c_0 first @@ -316,12 +322,16 @@ class FHECKKSRNS : public FHERNS { * @param depthLeveledComputation multiplicative depth reserved for a leveled computation applied between * EvalFBTNoDecoding (or EvalMVBNoDecoding) and EvalHomDecoding * @param order order of the trigonometric Hermite interpolation (1, 2 or 3) + * @param numValues number of values encrypted in the RLWE input (0 = as many as the slots hold: N for full + * packing, numSlots for sparse packing). With full packing, more than N/2 values occupy both coefficient halves + * (two ciphertexts are processed) and N/2 values the lower half (one ciphertext), independently of the CKKS data + * type (see CryptoContextImpl::EvalFBTSetup for when COMPLEX is needed). */ void EvalFBTSetup(const CryptoContextImpl& cc, const std::vector>& coefficients, uint32_t numSlots, const BigInteger& PIn, const BigInteger& POut, const BigInteger& Bigq, const PublicKey& pubKey, const std::vector& dim1, const std::vector& levelBudget, uint32_t lvlsAfterBoot = 0, - uint32_t depthLeveledComputation = 0, size_t order = 1) override; + uint32_t depthLeveledComputation = 0, size_t order = 1, uint32_t numValues = 0) override; /** * Sets up CKKS functional bootstrapping of a look-up table for a given number of slots: computes the depth of @@ -346,12 +356,16 @@ class FHECKKSRNS : public FHERNS { * @param depthLeveledComputation multiplicative depth reserved for a leveled computation applied between * EvalFBTNoDecoding (or EvalMVBNoDecoding) and EvalHomDecoding * @param order order of the trigonometric Hermite interpolation (1, 2 or 3) + * @param numValues number of values encrypted in the RLWE input (0 = as many as the slots hold: N for full + * packing, numSlots for sparse packing). With full packing, more than N/2 values occupy both coefficient halves + * (two ciphertexts are processed) and N/2 values the lower half (one ciphertext), independently of the CKKS data + * type (see CryptoContextImpl::EvalFBTSetup for when COMPLEX is needed). */ void EvalFBTSetup(const CryptoContextImpl& cc, const std::vector& coefficients, uint32_t numSlots, const BigInteger& PIn, const BigInteger& POut, const BigInteger& Bigq, const PublicKey& pubKey, const std::vector& dim1, const std::vector& levelBudget, uint32_t lvlsAfterBoot = 0, - uint32_t depthLeveledComputation = 0, size_t order = 1) override; + uint32_t depthLeveledComputation = 0, size_t order = 1, uint32_t numValues = 0) override; /** * Functional bootstrapping of a look-up table: modulus raise, CoeffsToSlots, approximation of the complex @@ -460,7 +474,8 @@ class FHECKKSRNS : public FHERNS { * @param initialScaling scale of the imported message (the RLWE ciphertext modulus the input was encrypted * under) * @param order order of the trigonometric Hermite interpolation (1, 2 or 3) - * @return the powers of the complex exponential (real and imaginary parts for full packing) + * @return the powers of the complex exponential (real and imaginary parts for full packing with more than N/2 + * values, see EvalFBTSetup) */ std::shared_ptr> EvalMVBPrecompute(ConstCiphertext& ciphertext, const std::vector>& coeffs, @@ -480,7 +495,8 @@ class FHECKKSRNS : public FHERNS { * @param initialScaling scale of the imported message (the RLWE ciphertext modulus the input was encrypted * under) * @param order order of the trigonometric Hermite interpolation (1, 2 or 3) - * @return the powers of the complex exponential (real and imaginary parts for full packing) + * @return the powers of the complex exponential (real and imaginary parts for full packing with more than N/2 + * values, see EvalFBTSetup) */ std::shared_ptr> EvalMVBPrecompute(ConstCiphertext& ciphertext, const std::vector& coeffs, uint32_t digitBitSize, @@ -1064,9 +1080,11 @@ class FHECKKSRNS : public FHERNS { * CoeffsToSlots and the complex exponential. * * @param ciphertext input ciphertext, with slot values in [-1/2, 1/2) - * @return a ciphertext of exp(2*Pi*i*t), t = mu/2 being the half-period embedding of the message + * @return the ciphertexts of exp(2*Pi*i*t), t = mu/2 being the half-period embedding of the message: one for + * CKKSDataType REAL, and two for COMPLEX (the real and imaginary parts of the message, each moved to the real + * part of its own ciphertext) */ - Ciphertext EvalFEFuncBootstrapExp(ConstCiphertext& ciphertext) const; + std::vector> EvalFEFuncBootstrapExp(ConstCiphertext& ciphertext) const; /** * Twice the real part of an evaluated Fourier series, obtained by adding its conjugate to it. @@ -1105,7 +1123,7 @@ class FHECKKSRNS : public FHERNS { uint32_t numSlots, const BigInteger& PIn, const BigInteger& POut, const BigInteger& Bigq, const PublicKey& pubKey, const std::vector& dim1, const std::vector& levelBudget, uint32_t lvlsAfterBoot = 0, - uint32_t depthLeveledComputation = 0, size_t order = 1); + uint32_t depthLeveledComputation = 0, size_t order = 1, uint32_t numValues = 0); template Ciphertext EvalHermiteTrigSeriesInternal(ConstCiphertext& ciphertext, diff --git a/src/pke/include/schemebase/base-fhe.h b/src/pke/include/schemebase/base-fhe.h index b96bbbfe7..bf74f9120 100644 --- a/src/pke/include/schemebase/base-fhe.h +++ b/src/pke/include/schemebase/base-fhe.h @@ -203,7 +203,8 @@ class FHEBase { /** * Refreshes a ciphertext and evaluates a function on it in one pass, by evaluating the Fourier * extension of the function over the bootstrapped message. The input slot values must lie in - * [-1/2, 1/2) and the result is real-valued. Supported only in CKKS. + * [-1/2, 1/2); for CKKSDataType COMPLEX the function is applied to the real and imaginary parts + * separately. Supported only in CKKS. * * @param ciphertext the input ciphertext, with slot values in [-1/2, 1/2). * @param coefficients Fourier coefficients c_j of the target function, c_0 first. @@ -266,12 +267,16 @@ class FHEBase { * @param depthLeveledComputation depth of the leveled computation applied between * EvalFBTNoDecoding (or EvalMVBNoDecoding) and EvalHomDecoding. * @param order order of the trigonometric Hermite interpolation (1, 2 or 3). + * @param numValues number of values encrypted in the RLWE input (0 = as many as the slots hold: N for + * full packing, numSlots for sparse packing). With full packing, more than N/2 values occupy both + * coefficient halves (two ciphertexts are processed) and N/2 values the lower half (one ciphertext), + * independently of the CKKS data type (see CryptoContextImpl::EvalFBTSetup for when COMPLEX is needed). */ virtual void EvalFBTSetup(const CryptoContextImpl& cc, const std::vector>& coeffs, uint32_t numSlots, const BigInteger& PIn, const BigInteger& POut, const BigInteger& Bigq, const PublicKey& pubKey, const std::vector& dim1, const std::vector& levelBudget, uint32_t lvlsAfterBoot = 0, - uint32_t depthLeveledComputation = 0, size_t order = 1) { + uint32_t depthLeveledComputation = 0, size_t order = 1, uint32_t numValues = 0) { OPENFHE_THROW(NOT_SUPPORTED_SIMPLE_ERROR); } /** @@ -296,12 +301,16 @@ class FHEBase { * @param depthLeveledComputation depth of the leveled computation applied between * EvalFBTNoDecoding (or EvalMVBNoDecoding) and EvalHomDecoding. * @param order order of the trigonometric Hermite interpolation (1, 2 or 3). + * @param numValues number of values encrypted in the RLWE input (0 = as many as the slots hold: N for + * full packing, numSlots for sparse packing). With full packing, more than N/2 values occupy both + * coefficient halves (two ciphertexts are processed) and N/2 values the lower half (one ciphertext), + * independently of the CKKS data type (see CryptoContextImpl::EvalFBTSetup for when COMPLEX is needed). */ virtual void EvalFBTSetup(const CryptoContextImpl& cc, const std::vector& coeffs, uint32_t numSlots, const BigInteger& PIn, const BigInteger& POut, const BigInteger& Bigq, const PublicKey& pubKey, const std::vector& dim1, const std::vector& levelBudget, uint32_t lvlsAfterBoot = 0, - uint32_t depthLeveledComputation = 0, size_t order = 1) { + uint32_t depthLeveledComputation = 0, size_t order = 1, uint32_t numValues = 0) { OPENFHE_THROW(NOT_SUPPORTED_SIMPLE_ERROR); } diff --git a/src/pke/include/schemebase/base-scheme.h b/src/pke/include/schemebase/base-scheme.h index 88d4a0d82..88b8cb3b6 100644 --- a/src/pke/include/schemebase/base-scheme.h +++ b/src/pke/include/schemebase/base-scheme.h @@ -1547,10 +1547,10 @@ class SchemeBase { uint32_t numSlots, const BigInteger& PIn, const BigInteger& POut, const BigInteger& Bigq, const PublicKey& pubKey, const std::vector& dim1, const std::vector& levelBudget, uint32_t lvlsAfterBoot = 0, - uint32_t depthLeveledComputation = 0, size_t order = 1) { + uint32_t depthLeveledComputation = 0, size_t order = 1, uint32_t numValues = 0) { VerifyFHEEnabled(__func__); m_FHE->EvalFBTSetup(cc, coeffs, numSlots, PIn, POut, Bigq, pubKey, dim1, levelBudget, lvlsAfterBoot, - depthLeveledComputation, order); + depthLeveledComputation, order, numValues); } template diff --git a/src/pke/lib/scheme/ckksrns/ckksrns-fhe.cpp b/src/pke/lib/scheme/ckksrns/ckksrns-fhe.cpp index 28df7c1ef..e2a2856e8 100644 --- a/src/pke/lib/scheme/ckksrns/ckksrns-fhe.cpp +++ b/src/pke/lib/scheme/ckksrns/ckksrns-fhe.cpp @@ -1643,7 +1643,7 @@ void FHECKKSRNS::EvalFEFuncBootstrapSetup(const CryptoContextImpl& cc, // modulus raise, CoeffsToSlots, and the complex exponential. The result encrypts exp(2*Pi*i*t) with // t = mu/2 the message embedded into half of the Fourier series period, which is the variable that every // target function's series is then evaluated at. -Ciphertext FHECKKSRNS::EvalFEFuncBootstrapExp(ConstCiphertext& ciphertext) const { +std::vector> FHECKKSRNS::EvalFEFuncBootstrapExp(ConstCiphertext& ciphertext) const { const auto cryptoParams = std::dynamic_pointer_cast(ciphertext->GetCryptoParameters()); if (cryptoParams->GetKeySwitchTechnique() != HYBRID) @@ -1811,20 +1811,29 @@ Ciphertext FHECKKSRNS::EvalFEFuncBootstrapExp(ConstCiphertextGetEvalAutomorphismKeyMap(ctxtEnc->GetKeyTag()); auto conj = Conjugate(ctxtEnc, evalKeyMap); - cc->EvalAddInPlace(ctxtEnc, conj); + std::vector> ctxtChannels; + ctxtChannels.reserve(2); + ctxtChannels.emplace_back(ctxtEnc); + if (cc->GetCKKSDataType() == COMPLEX) { + ctxtChannels.emplace_back(cc->EvalSub(ctxtEnc, conj)); + algo->MultByMonomialInPlace(ctxtChannels[1], 3 * N / 2); + } + cc->EvalAddInPlace(ctxtChannels[0], conj); - if (st == FIXEDMANUAL) { - while (ctxtEnc->GetNoiseScaleDeg() > 1) { - cc->ModReduceInPlace(ctxtEnc); - } - } else { - if (ctxtEnc->GetNoiseScaleDeg() == 2) { - algo->ModReduceInternalInPlace(ctxtEnc, compositeDegree); + for (auto& ctxt : ctxtChannels) { + if (st == FIXEDMANUAL) { + while (ctxt->GetNoiseScaleDeg() > 1) + cc->ModReduceInPlace(ctxt); + } else if (ctxt->GetNoiseScaleDeg() == 2) { + algo->ModReduceInternalInPlace(ctxt, compositeDegree); } } @@ -1835,14 +1844,16 @@ Ciphertext FHECKKSRNS::EvalFEFuncBootstrapExp(ConstCiphertextEvalChebyshevSeries(ctxtEnc, coeffExp, -1, 1); - // double-angle iterations for the complex exponential: each squaring doubles the argument - for (uint32_t i = 0; i < rFunc; ++i) { - cc->EvalSquareInPlace(ctxtExp); - cc->ModReduceInPlace(ctxtExp); + for (auto& ctxt : ctxtChannels) { + ctxt = algo->EvalChebyshevSeries(ctxt, coeffExp, -1, 1); + // double-angle iterations for the complex exponential: each squaring doubles the argument + for (uint32_t i = 0; i < rFunc; ++i) { + cc->EvalSquareInPlace(ctxt); + cc->ModReduceInPlace(ctxt); + } } #ifdef BOOTSTRAPTIMING @@ -1850,7 +1861,19 @@ Ciphertext FHECKKSRNS::EvalFEFuncBootstrapExp(ConstCiphertext TwiceRealPartPair(Ciphertext s0, Ciphertext s1) { + auto cc = s0->GetCryptoContext(); + cc->GetScheme()->MultByMonomialInPlace(s1, cc->GetRingDimension() / 2); + auto diff = cc->EvalSub(s0, s1); + cc->EvalAddInPlace(s0, s1); + cc->EvalAddInPlace(s0, FHECKKSRNS::Conjugate(diff, cc->GetEvalAutomorphismKeyMap(diff->GetKeyTag()))); + return s0; } // Twice the real part of an evaluated Fourier series, which is the FE functional bootstrapping output @@ -1866,7 +1889,7 @@ Ciphertext FHECKKSRNS::TwiceRealPart(const Ciphertext& ctxtS Ciphertext FHECKKSRNS::EvalFEFuncBootstrap(ConstCiphertext& ciphertext, const std::vector>& coefficients) const { auto ctxtExp = EvalFEFuncBootstrapExp(ciphertext); - auto algo = ctxtExp->GetCryptoContext()->GetScheme(); + auto algo = ctxtExp[0]->GetCryptoContext()->GetScheme(); #ifdef BOOTSTRAPTIMING TimeVar t; @@ -1874,7 +1897,9 @@ Ciphertext FHECKKSRNS::EvalFEFuncBootstrap(ConstCiphertext& TIC(t); #endif - auto result = TwiceRealPart(algo->EvalPoly(ctxtExp, coefficients)); + auto result = (ctxtExp.size() > 1) ? TwiceRealPartPair(algo->EvalPoly(ctxtExp[0], coefficients), + algo->EvalPoly(ctxtExp[1], coefficients)) : + TwiceRealPart(algo->EvalPoly(ctxtExp[0], coefficients)); #ifdef BOOTSTRAPTIMING timeSeries = TOC(t); @@ -1895,7 +1920,7 @@ std::shared_ptr> FHECKKSRNS::EvalFEFuncBootstrapPrecomput "complex-exponential powers; use EvalFEFuncBootstrap for a shorter series."); auto ctxtExp = EvalFEFuncBootstrapExp(ciphertext); - auto algo = ctxtExp->GetCryptoContext()->GetScheme(); + auto algo = ctxtExp[0]->GetCryptoContext()->GetScheme(); #ifdef BOOTSTRAPTIMING TimeVar t; @@ -1903,7 +1928,14 @@ std::shared_ptr> FHECKKSRNS::EvalFEFuncBootstrapPrecomput TIC(t); #endif - auto powers = algo->EvalPowers(ctxtExp, coefficients); + auto powers = algo->EvalPowers(ctxtExp[0], coefficients); + if (ctxtExp.size() > 1) { + // CKKSDataType COMPLEX: the powers of the imaginary channel are stored alongside + auto powersIm = algo->EvalPowers(ctxtExp[1], coefficients); + powers = std::make_shared>(powers->powersRe, powers->powers2Re, powers->power2km1Re, + powers->k, powers->m, powersIm->powersRe, powersIm->powers2Re, + powersIm->power2km1Re); + } #ifdef BOOTSTRAPTIMING timePowers = TOC(t); @@ -1949,8 +1981,24 @@ Ciphertext FHECKKSRNS::EvalFEFuncBootstrapWithPrecomp( TIC(t); #endif - auto algo = powers->powersRe.front()->GetCryptoContext()->GetScheme(); - auto result = TwiceRealPart(algo->EvalPolyWithPrecomp(powers, trimmed)); + auto cc = powers->powersRe.front()->GetCryptoContext(); + auto algo = cc->GetScheme(); + Ciphertext result; + if (cc->GetCKKSDataType() == COMPLEX) { + // the series is evaluated on the real and imaginary channels separately and the results recombined + if (powers->powersIm.empty()) + OPENFHE_THROW( + "CKKSDataType COMPLEX requires the powers of both the real and imaginary channels (the series " + "powers were not produced by EvalFEFuncBootstrapPrecompute under this crypto context)."); + auto powersRe = std::make_shared>(powers->powersRe, powers->powers2Re, + powers->power2km1Re, powers->k, powers->m); + auto powersIm = std::make_shared>(powers->powersIm, powers->powers2Im, + powers->power2km1Im, powers->k, powers->m); + result = TwiceRealPartPair(algo->EvalPolyWithPrecomp(powersRe, trimmed), + algo->EvalPolyWithPrecomp(powersIm, trimmed)); + } else { + result = TwiceRealPart(algo->EvalPolyWithPrecomp(powers, trimmed)); + } #ifdef BOOTSTRAPTIMING timeSeries = TOC(t); @@ -3470,7 +3518,8 @@ void FHECKKSRNS::EvalFBTSetupInternal(const CryptoContextImpl& cc, con uint32_t numSlots, const BigInteger& PIn, const BigInteger& POut, const BigInteger& Bigq, const PublicKey& pubKey, const std::vector& dim1, const std::vector& levelBudget, - uint32_t lvlsAfterBoot, uint32_t depthLeveledComputation, size_t order) { + uint32_t lvlsAfterBoot, uint32_t depthLeveledComputation, size_t order, + uint32_t numValues) { const auto cryptoParams = std::dynamic_pointer_cast(cc.GetCryptoParameters()); if (cryptoParams->GetKeySwitchTechnique() != HYBRID) OPENFHE_THROW("CKKS Functional Bootstrapping is only supported for the Hybrid key switching method."); @@ -3479,6 +3528,11 @@ void FHECKKSRNS::EvalFBTSetupInternal(const CryptoContextImpl& cc, con uint32_t M = cc.GetCyclotomicOrder(); uint32_t slots = (numSlots == 0) ? M / 4 : numSlots; + const bool fullPacking = (slots == M / 4); + if (numValues > (fullPacking ? 2 * slots : slots)) + OPENFHE_THROW("The number of RLWE values (" + std::to_string(numValues) + ") exceeds the capacity of " + + std::to_string(slots) + " slots."); + // even for the case of a single slot we need one level for rescaling uint32_t logSlots = (slots < 3) ? 1 : std::log2(slots); @@ -3593,6 +3647,7 @@ void FHECKKSRNS::EvalFBTSetupInternal(const CryptoContextImpl& cc, con auto& precom = m_bootPrecomMap[slots]; precom->m_slots = slots; precom->m_variant = CKKSBootstrapPrecom::FBT; + precom->m_FBTComplexPacking = fullPacking && (numValues == 0 || numValues > slots); precom->m_paramsEnc = GetCollapsedFFTParams(slots, levelBudget[0], dim1[0]); precom->m_paramsDec = GetCollapsedFFTParams(slots, levelBudget[1], dim1[1]); @@ -3635,18 +3690,18 @@ void FHECKKSRNS::EvalFBTSetup(const CryptoContextImpl& cc, const BigInteger& PIn, const BigInteger& POut, const BigInteger& Bigq, const PublicKey& pubKey, const std::vector& dim1, const std::vector& levelBudget, uint32_t lvlsAfterBoot, - uint32_t depthLeveledComputation, size_t order) { + uint32_t depthLeveledComputation, size_t order, uint32_t numValues) { EvalFBTSetupInternal(cc, coefficients, numSlots, PIn, POut, Bigq, pubKey, dim1, levelBudget, lvlsAfterBoot, - depthLeveledComputation, order); + depthLeveledComputation, order, numValues); } void FHECKKSRNS::EvalFBTSetup(const CryptoContextImpl& cc, const std::vector& coefficients, uint32_t numSlots, const BigInteger& PIn, const BigInteger& POut, const BigInteger& Bigq, const PublicKey& pubKey, const std::vector& dim1, const std::vector& levelBudget, uint32_t lvlsAfterBoot, - uint32_t depthLeveledComputation, size_t order) { + uint32_t depthLeveledComputation, size_t order, uint32_t numValues) { EvalFBTSetupInternal(cc, coefficients, numSlots, PIn, POut, Bigq, pubKey, dim1, levelBudget, lvlsAfterBoot, - depthLeveledComputation, order); + depthLeveledComputation, order, numValues); } Ciphertext FHECKKSRNS::EvalHomDecoding(ConstCiphertext& ciphertext, uint64_t postScaling, @@ -3856,165 +3911,115 @@ std::shared_ptr> FHECKKSRNS::EvalMVBPrecomputeInternal( auto& p = GetBootPrecom(slots, CKKSBootstrapPrecom::FBT); bool isLTBootstrap = (p.m_paramsEnc.lvlb == 1) && (p.m_paramsDec.lvlb == 1); - std::vector> ctxtEnc; - ctxtEnc.reserve(2); - std::shared_ptr> ctxtPowers; - - if (slots == M / 4) { - //------------------------------------------------------------------------------ - // FULLY PACKED CASE + // The homomorphic encoding yields one ciphertext whose real channel holds the lower half of the + // coefficients and whose imaginary channel holds the upper half. The approximate modular reduction + // only works on real inputs, so the channels have to be processed as separate ciphertexts. Whether the + // imaginary channel is kept (two ciphertexts) or dropped (one ciphertext) depends on the packing recorded + // by EvalFBTSetup: + // - full packing (N/2 slots) of more than N/2 RLWE values: both halves carry data, and two ciphertexts go + // through the modular reduction and the LUT evaluation; + // - full packing of N/2 RLWE values: they occupy the lower half (the upper half holds no data), so only + // the real channel is kept and a single ciphertext is processed; + // - sparse packing (fewer than N/2 slots): the RLWE encryption duplicates the values into both halves, + // which the partial sum folds onto the real channel, so a single ciphertext is processed. + const bool keepImagChannel = p.m_FBTComplexPacking; + + if (slots != M / 4) { //------------------------------------------------------------------------------ - - //------------------------------------------------------------------------------ - // Running CoeffToSlot + // SPARSELY PACKED CASE: Running PartialSum //------------------------------------------------------------------------------ + EvalPartialSumInPlace(raised, slots); + } - // need to call internal modular reduction so it also works for FLEXIBLEAUTO - algo->ModReduceInternalInPlace(raised, compositeDegree); + //------------------------------------------------------------------------------ + // Running CoeffsToSlots + //------------------------------------------------------------------------------ - // only one linear transform is needed as the other one can be derived - ctxtEnc.emplace_back((isLTBootstrap) ? EvalLinearTransform(p.m_U0hatTPre, raised) : - EvalCoeffsToSlots(p.m_U0hatTPreFFT, raised)); + // need to call internal modular reduction so it also works for FLEXIBLEAUTO + algo->ModReduceInternalInPlace(raised, compositeDegree); - auto conj = Conjugate(ctxtEnc[0], cc->GetEvalAutomorphismKeyMap(ctxtEnc[0]->GetKeyTag())); + // only one linear transform is needed as the other one can be derived + std::vector> ctxtEnc; + ctxtEnc.reserve(2); + ctxtEnc.emplace_back((isLTBootstrap) ? EvalLinearTransform(p.m_U0hatTPre, raised) : + EvalCoeffsToSlots(p.m_U0hatTPreFFT, raised)); + auto conj = Conjugate(ctxtEnc[0], cc->GetEvalAutomorphismKeyMap(ctxtEnc[0]->GetKeyTag())); + if (keepImagChannel) { + // the imaginary channel (upper half of the coefficients), moved to the real part ctxtEnc.emplace_back(cc->EvalSub(ctxtEnc[0], conj)); - cc->EvalAddInPlaceNoCheck(ctxtEnc[0], conj); algo->MultByMonomialInPlace(ctxtEnc[1], 3 * M / 4); + } + // the real channel (lower half of the coefficients) + cc->EvalAddInPlaceNoCheck(ctxtEnc[0], conj); - if (cryptoParams->GetScalingTechnique() == FIXEDMANUAL) { - while (ctxtEnc[0]->GetNoiseScaleDeg() > 1) { - cc->ModReduceInPlace(ctxtEnc[0]); - cc->ModReduceInPlace(ctxtEnc[1]); - } - } else { - if (ctxtEnc[0]->GetNoiseScaleDeg() == 2) { - algo->ModReduceInternalInPlace(ctxtEnc[0], compositeDegree); - algo->ModReduceInternalInPlace(ctxtEnc[1], compositeDegree); - } - } - - //------------------------------------------------------------------------------ - // Computing the powers for Approximate Mod Reduction - //------------------------------------------------------------------------------ - - if (digitBitSize == 1 && order == 1) { - auto& coeff_cos = (skd == UNIFORM_TERNARY) ? coeff_cos_672_double_104 : - IsSparseEncapsulated(skd) ? coeff_cos_16_double_50 : - coeff_cos_28_double_68; - - ctxtEnc[0] = algo->EvalChebyshevSeries(ctxtEnc[0], coeff_cos, coeffLowerBound, coeffUpperBound); - ctxtEnc[1] = algo->EvalChebyshevSeries(ctxtEnc[1], coeff_cos, coeffLowerBound, coeffUpperBound); - - for (auto& ctxt : ctxtEnc) { - // Double angle-iterations to get cos(pi*x) - for (uint32_t i = 1; i < numIter; ++i) { - cc->EvalSquareInPlace(ctxt); - cc->EvalAddInPlaceNoCheck(ctxt, ctxt); - cc->EvalSubInPlace(ctxt, 1.0); - cc->ModReduceInPlace(ctxt); - } - // cos(pi x) after the loop above; square once more to get cos^2(pi x) - cc->EvalSquareInPlace(ctxt); - cc->ModReduceInPlace(ctxt); // cos^2(pi x) - } - } else { - auto& coeff_exp = (skd == UNIFORM_TERNARY) ? coeff_exp_672_double_104 : - IsSparseEncapsulated(skd) ? coeff_exp_16_double_46 : - coeff_exp_28_double_69; - - // Obtain the exp(2*Pi*i*x/2^numIter) approximation via Chebyshev Basis Polynomial Interpolation - ctxtEnc[0] = algo->EvalChebyshevSeries(ctxtEnc[0], coeff_exp, coeffLowerBound, coeffUpperBound); - ctxtEnc[1] = algo->EvalChebyshevSeries(ctxtEnc[1], coeff_exp, coeffLowerBound, coeffUpperBound); - - for (auto& ctxt : ctxtEnc) { - // Double angle-iterations to get exp(2*Pi*i*x) - for (uint32_t i = 0; i < numIter; ++i) { - cc->EvalSquareInPlace(ctxt); - cc->ModReduceInPlace(ctxt); - } - } - } - - auto ctxtPowersRe = algo->EvalPowers(ctxtEnc[0], coefficients); - auto ctxtPowersIm = algo->EvalPowers(ctxtEnc[1], coefficients); - - if (ctxtPowersRe->powers2Re.size() == 0) { - ctxtPowers = std::make_shared>(ctxtPowersRe->powersRe, ctxtPowersIm->powersRe); - } else { - ctxtPowers = std::make_shared>( - ctxtPowersRe->powersRe, ctxtPowersRe->powers2Re, ctxtPowersRe->power2km1Re, ctxtPowersRe->k, - ctxtPowersRe->m, ctxtPowersIm->powersRe, ctxtPowersIm->powers2Re, ctxtPowersIm->power2km1Re); + if (cryptoParams->GetScalingTechnique() == FIXEDMANUAL) { + while (ctxtEnc[0]->GetNoiseScaleDeg() > 1) { + for (auto& ctxt : ctxtEnc) + cc->ModReduceInPlace(ctxt); } } else { - //------------------------------------------------------------------------------ - // SPARSELY PACKED CASE - //------------------------------------------------------------------------------ - - //------------------------------------------------------------------------------ - // Running PartialSum - //------------------------------------------------------------------------------ - - EvalPartialSumInPlace(raised, slots); - - //------------------------------------------------------------------------------ - // Running CoeffsToSlots - //------------------------------------------------------------------------------ - - algo->ModReduceInternalInPlace(raised, compositeDegree); - - ctxtEnc.emplace_back((isLTBootstrap) ? EvalLinearTransform(p.m_U0hatTPre, raised) : - EvalCoeffsToSlots(p.m_U0hatTPreFFT, raised)); - - auto& evalKeyMap = cc->GetEvalAutomorphismKeyMap(ctxtEnc[0]->GetKeyTag()); - cc->EvalAddInPlace(ctxtEnc[0], Conjugate(ctxtEnc[0], evalKeyMap)); - - if (cryptoParams->GetScalingTechnique() == FIXEDMANUAL) { - while (ctxtEnc[0]->GetNoiseScaleDeg() > 1) - cc->ModReduceInPlace(ctxtEnc[0]); - } else { - if (ctxtEnc[0]->GetNoiseScaleDeg() == 2) - algo->ModReduceInternalInPlace(ctxtEnc[0], compositeDegree); + if (ctxtEnc[0]->GetNoiseScaleDeg() == 2) { + for (auto& ctxt : ctxtEnc) + algo->ModReduceInternalInPlace(ctxt, compositeDegree); } + } - //------------------------------------------------------------------------------ - // Running Approximate Mod Reduction - //------------------------------------------------------------------------------ + //------------------------------------------------------------------------------ + // Running Approximate Mod Reduction (on each channel) + //------------------------------------------------------------------------------ - if (digitBitSize == 1 && order == 1) { - auto& coeff_cos = (skd == UNIFORM_TERNARY) ? coeff_cos_672_double_104 : - IsSparseEncapsulated(skd) ? coeff_cos_16_double_50 : - coeff_cos_28_double_68; + if (digitBitSize == 1 && order == 1) { + auto& coeff_cos = (skd == UNIFORM_TERNARY) ? coeff_cos_672_double_104 : + IsSparseEncapsulated(skd) ? coeff_cos_16_double_50 : + coeff_cos_28_double_68; - ctxtEnc[0] = algo->EvalChebyshevSeries(ctxtEnc[0], coeff_cos, coeffLowerBound, coeffUpperBound); + for (auto& ctxt : ctxtEnc) { + ctxt = algo->EvalChebyshevSeries(ctxt, coeff_cos, coeffLowerBound, coeffUpperBound); // Double angle-iterations to get cos(pi*x) for (uint32_t i = 1; i < numIter; ++i) { - cc->EvalSquareInPlace(ctxtEnc[0]); - cc->EvalAddInPlaceNoCheck(ctxtEnc[0], ctxtEnc[0]); - cc->EvalSubInPlace(ctxtEnc[0], 1.0); - cc->ModReduceInPlace(ctxtEnc[0]); + cc->EvalSquareInPlace(ctxt); + cc->EvalAddInPlaceNoCheck(ctxt, ctxt); + cc->EvalSubInPlace(ctxt, 1.0); + cc->ModReduceInPlace(ctxt); } // cos(pi x) after the loop above; square once more to get cos^2(pi x) - cc->EvalSquareInPlace(ctxtEnc[0]); - cc->ModReduceInPlace(ctxtEnc[0]); // cos^2(pi x) - } else { - auto& coeff_exp = (skd == UNIFORM_TERNARY) ? coeff_exp_672_double_104 : - IsSparseEncapsulated(skd) ? coeff_exp_16_double_46 : - coeff_exp_28_double_69; + cc->EvalSquareInPlace(ctxt); + cc->ModReduceInPlace(ctxt); // cos^2(pi x) + } + } else { + auto& coeff_exp = (skd == UNIFORM_TERNARY) ? coeff_exp_672_double_104 : + IsSparseEncapsulated(skd) ? coeff_exp_16_double_46 : + coeff_exp_28_double_69; + for (auto& ctxt : ctxtEnc) { // Obtain the exp(2*Pi*i*x/2^numIter) approximation via Chebyshev Basis Polynomial Interpolation - ctxtEnc[0] = algo->EvalChebyshevSeries(ctxtEnc[0], coeff_exp, coeffLowerBound, coeffUpperBound); + ctxt = algo->EvalChebyshevSeries(ctxt, coeff_exp, coeffLowerBound, coeffUpperBound); // Double angle-iterations to get exp(2*Pi*i*x) for (uint32_t i = 0; i < numIter; ++i) { - cc->EvalSquareInPlace(ctxtEnc[0]); - cc->ModReduceInPlace(ctxtEnc[0]); + cc->EvalSquareInPlace(ctxt); + cc->ModReduceInPlace(ctxt); } } + } - // No need to scale the message back up after Chebyshev interpolation - ctxtPowers = algo->EvalPowers(ctxtEnc[0], coefficients); + //------------------------------------------------------------------------------ + // Computing the powers for the LUT evaluation + //------------------------------------------------------------------------------ + + // No need to scale the message back up after Chebyshev interpolation + auto ctxtPowers = algo->EvalPowers(ctxtEnc[0], coefficients); + if (keepImagChannel) { + auto ctxtPowersIm = algo->EvalPowers(ctxtEnc[1], coefficients); + if (ctxtPowers->powers2Re.size() == 0) { + ctxtPowers = std::make_shared>(ctxtPowers->powersRe, ctxtPowersIm->powersRe); + } else { + ctxtPowers = std::make_shared>( + ctxtPowers->powersRe, ctxtPowers->powers2Re, ctxtPowers->power2km1Re, ctxtPowers->k, ctxtPowers->m, + ctxtPowersIm->powersRe, ctxtPowersIm->powers2Re, ctxtPowersIm->power2km1Re); + } } // 64-bit only: No need to scale back the message to its original scale. @@ -4050,12 +4055,18 @@ Ciphertext FHECKKSRNS::EvalMVBNoDecodingInternal(const std::shared_ptr Ciphertext ctxtEnc; - if (slots == M4) { + // The powers of the imaginary channel (upper half of the coefficients) exist only for full packing of more + // than N/2 RLWE values (see EvalMVBPrecomputeInternal), where the look-up table is evaluated on both + // channels and the results are recombined; sparse packing and full packing of N/2 values evaluate it on + // the real channel alone. + const bool evalImagChannel = GetBootPrecom(slots, CKKSBootstrapPrecom::FBT).m_FBTComplexPacking; + + if (evalImagChannel) { //------------------------------------------------------------------------------ - // FULLY PACKED CASE + // FULLY PACKED COMPLEX CASE //------------------------------------------------------------------------------ if (ciphertexts->powersIm.size() == 0) - OPENFHE_THROW("Full packing requires powers for both the real and imaginary parts."); + OPENFHE_THROW("Full complex packing requires powers for both the real and imaginary parts."); Ciphertext ctxtEncI; //------------------------------------------------------------------------------ @@ -4077,6 +4088,8 @@ Ciphertext FHECKKSRNS::EvalMVBNoDecodingInternal(const std::shared_ptr algo->MultByIntegerInPlace(ctxtEncI, -ToReal(coefficients[1])); ctxtEncI = cc->EvalSub(ToReal(coefficients[0]), ctxtEncI); } + algo->MultByMonomialInPlace(ctxtEncI, M4); + cc->EvalAddInPlace(ctxtEnc, ctxtEncI); } else { // Obtain the complex Hermite Trigonometric Interpolation via Power Basis Polynomial Interpolation // Coefficients are divided by 2 @@ -4095,20 +4108,16 @@ Ciphertext FHECKKSRNS::EvalMVBNoDecodingInternal(const std::shared_ptr ciphertexts->m); } - // Take the real part + // Take the real parts of both channels with a single conjugation // Division by 2 was already performed - ctxtEnc = cc->EvalPolyWithPrecomp(ctxtPowersRe, coefficients); - cc->EvalAddInPlace(ctxtEnc, Conjugate(ctxtEnc, cc->GetEvalAutomorphismKeyMap(ctxtEnc->GetKeyTag()))); - ctxtEncI = cc->EvalPolyWithPrecomp(ctxtPowersIm, coefficients); - cc->EvalAddInPlace(ctxtEncI, Conjugate(ctxtEncI, cc->GetEvalAutomorphismKeyMap(ctxtEnc->GetKeyTag()))); + ctxtEnc = TwiceRealPartPair(cc->EvalPolyWithPrecomp(ctxtPowersRe, coefficients), + cc->EvalPolyWithPrecomp(ctxtPowersIm, coefficients)); } - algo->MultByMonomialInPlace(ctxtEncI, M4); - cc->EvalAddInPlace(ctxtEnc, ctxtEncI); // No need to scale the message back up after Chebyshev interpolation } else { //------------------------------------------------------------------------------ - // SPARSELY PACKED CASE + // SINGLE CHANNEL CASE (sparse packing, or full packing with real data) //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ diff --git a/src/pke/unittest/utckksrns/UnitTestFBT.cpp b/src/pke/unittest/utckksrns/UnitTestFBT.cpp index df18b675f..74c2e0fb7 100644 --- a/src/pke/unittest/utckksrns/UnitTestFBT.cpp +++ b/src/pke/unittest/utckksrns/UnitTestFBT.cpp @@ -239,6 +239,7 @@ static auto testName = [](const testing::TestParamInfo& test) { [[maybe_unused]] constexpr uint32_t BEFOREBOOT(0); [[maybe_unused]] constexpr uint32_t LVLSCOMP(0); [[maybe_unused]] constexpr uint32_t SLOTSPARSE(8); +[[maybe_unused]] constexpr uint32_t SLOTHALF(16); // half the ring dimension: full CKKS packing with real data [[maybe_unused]] constexpr uint32_t SLOTFULL(32); [[maybe_unused]] constexpr uint32_t RINGDM(32); [[maybe_unused]] const std::vector LVLBDFLT = {3, 3}; @@ -277,6 +278,17 @@ static std::vector testCases = { { FBT_MVB, "22", Q60, PINPUT, PINPUT, Q48, Q48, SCALETHI, SCALESTEPTHI, 1, SLOTFULL, RINGDM, AFTERBOOT, BEFOREBOOT, 3, 1, LVLBDFLT, SPARSE_TERNARY}, { FBT_MVB, "23", Q60, 2, 2, Q35, Q35, 1, SCALESTEPTHI, 1, SLOTSPARSE, RINGDM, AFTERBOOT, BEFOREBOOT, 3, 1, LVLBDFLT, SPARSE_TERNARY}, { FBT_MVB, "24", Q60, PINPUT, PINPUT, Q48, Q48, SCALETHI, SCALESTEPTHI, 1, SLOTSPARSE, RINGDM, AFTERBOOT, BEFOREBOOT, 3, 1, LVLBDFLT, SPARSE_TERNARY}, + // Half the ring dimension: full CKKS packing with CKKSDataType REAL, where only the lower half of the RLWE + // coefficients carries data and functional bootstrapping processes a single ciphertext + { FBT_ARBLUT, "41", Q60, 2, 2, Q33, Q33, 1, SCALESTEPTHI, 1, SLOTHALF, RINGDM, AFTERBOOT, BEFOREBOOT, 3, LVLSCOMP, LVLBDFLT, SPARSE_TERNARY}, + { FBT_ARBLUT, "42", Q60, 2, 2, Q33, Q33, 1, SCALESTEPTHI, 2, SLOTHALF, RINGDM, AFTERBOOT, BEFOREBOOT, 3, LVLSCOMP, LVLBDFLT, SPARSE_TERNARY}, + { FBT_ARBLUT, "43", Q60, PINPUT, POUTPUT, Q47, Q47, SCALETHI, SCALESTEPTHI, 1, SLOTHALF, RINGDM, AFTERBOOT, BEFOREBOOT, 3, LVLSCOMP, LVLBDFLT, SPARSE_TERNARY}, + { FBT_ARBLUT, "44", Q60, PINPUT, POUTPUT, Q47, Q47, SCALETHI, SCALESTEPTHI, 2, SLOTHALF, RINGDM, AFTERBOOT, BEFOREBOOT, 3, LVLSCOMP, LVLBDFLT, SPARSE_TERNARY}, + { FBT_SIGNDIGIT, "45", Q71, Q21, 2, Q56, Q36, 1, 1, 1, SLOTHALF, RINGDM, AFTERBOOT, BEFOREBOOT, 3, LVLSCOMP, LVLBDFLT, SPARSE_TERNARY}, + { FBT_CONSECLEV, "46", Q60, PINPUT, PINPUT, Q48, Q48, SCALETHI, SCALESTEPTHI, 1, SLOTHALF, RINGDM, AFTERBOOT, BEFOREBOOT, 3, 1, LVLBDFLT, SPARSE_TERNARY}, + { FBT_MVB, "47", Q60, 2, 2, Q35, Q35, 1, SCALESTEPTHI, 1, SLOTHALF, RINGDM, AFTERBOOT, BEFOREBOOT, 3, 1, LVLBDFLT, SPARSE_TERNARY}, + { FBT_MVB, "48", Q60, PINPUT, PINPUT, Q48, Q48, SCALETHI, SCALESTEPTHI, 1, SLOTHALF, RINGDM, AFTERBOOT, BEFOREBOOT, 3, 1, LVLBDFLT, SPARSE_TERNARY}, + { FBT_MVB_REUSE, "49", Q60, 4, 4, Q36, Q36, 1, SCALESTEPTHI, 1, SLOTHALF, RINGDM, AFTERBOOT, BEFOREBOOT, 3, LVLSCOMP, LVLBDFLT, SPARSE_TERNARY}, // TestCaseType, Desc, QBFVInit, PInput, POutput, Q, Bigq, scaleTHI, scaleStepTHI, order, numSlots, ringDim, lvlsAfterBoot, lvlsBeforeBoot, dnum, lvlsComp, lvlBudget, SecretKeyDist { FBT_ARBLUT, "101", Q60, 2, 2, Q33, Q33, 1, SCALESTEPTHI, 1, SLOTFULL, RINGDM, AFTERBOOT, BEFOREBOOT, 3, LVLSCOMP, LVLBDFLT, SPARSE_ENCAPSULATED}, { FBT_ARBLUT, "H101", Q60, 2, 2, Q33, Q33, 1, SCALESTEPTHI, 1, SLOTFULL, RINGDM, AFTERBOOT, BEFOREBOOT, 3, LVLSCOMP, LVLBDFLT, HYBRID_TERNARY}, @@ -407,6 +419,7 @@ static std::vector testCases = { { FBT_SIGNDIGIT, "935", Q80, Q21, 2, Q64, Q44, 1, 1, 1, SLOTFULL, RINGDM, AFTERBOOT, BEFOREBOOT, 3, LVLSCOMP, LVLBDFLT, UNIFORM_TERNARY, FLEXIBLEAUTO}, { FBT_ARBLUT, "936", Q60, 2, 2, Q40, Q40, 1, SCALESTEPTHI, 1, SLOTFULL, RINGDM, AFTERBOOT, BEFOREBOOT, 3, LVLSCOMP, LVLBDFLT, UNIFORM_TERNARY, FLEXIBLEAUTOEXT}, { FBT_ARBLUT, "937", Q60, PINPUT, POUTPUT, Q54, Q54, SCALETHI, SCALESTEPTHI, 1, SLOTFULL, RINGDM, AFTERBOOT, BEFOREBOOT, 3, LVLSCOMP, LVLBDFLT, UNIFORM_TERNARY, FLEXIBLEAUTOEXT}, + { FBT_ARBLUT, "939", Q60, PINPUT, POUTPUT, Q54, Q54, SCALETHI, SCALESTEPTHI, 1, SLOTHALF, RINGDM, AFTERBOOT, BEFOREBOOT, 3, LVLSCOMP, LVLBDFLT, UNIFORM_TERNARY, FLEXIBLEAUTO}, { FBT_MVB, "938", Q60, PINPUT, PINPUT, Q55, Q55, SCALETHI, SCALESTEPTHI, 1, SLOTFULL, RINGDM, AFTERBOOT, BEFOREBOOT, 3, 1, LVLBDFLT, UNIFORM_TERNARY, FLEXIBLEAUTO}, // UNIFORM_TERNARY, sparse packing (numSlots < ringDim/2), with the other supported scaling techniques { FBT_ARBLUT, "941", Q60, 2, 2, Q40, Q40, 1, SCALESTEPTHI, 1, SLOTSPARSE, RINGDM, AFTERBOOT, BEFOREBOOT, 3, LVLSCOMP, LVLBDFLT, UNIFORM_TERNARY, FIXEDAUTO}, @@ -474,6 +487,7 @@ static std::vector testCases = { { FBT_ARBLUT, "H1051", Q60, 2, 2, Q40, Q40, 1, SCALESTEPTHI, 1, SLOTFULL, RINGDM, AFTERBOOT, BEFOREBOOT, 3, LVLSCOMP, LVLBDFLT, HYBRID_TERNARY, COMPOSITESCALINGAUTO}, { FBT_ARBLUT, "1052", Q60, PINPUT, POUTPUT, Q54, Q54, SCALETHI, SCALESTEPTHI, 1, SLOTFULL, RINGDM, AFTERBOOT, BEFOREBOOT, 3, LVLSCOMP, LVLBDFLT, SPARSE_ENCAPSULATED, COMPOSITESCALINGAUTO}, { FBT_ARBLUT, "H1052", Q60, PINPUT, POUTPUT, Q54, Q54, SCALETHI, SCALESTEPTHI, 1, SLOTFULL, RINGDM, AFTERBOOT, BEFOREBOOT, 3, LVLSCOMP, LVLBDFLT, HYBRID_TERNARY, COMPOSITESCALINGAUTO}, + { FBT_ARBLUT, "1058", Q60, PINPUT, POUTPUT, Q54, Q54, SCALETHI, SCALESTEPTHI, 1, SLOTHALF, RINGDM, AFTERBOOT, BEFOREBOOT, 3, LVLSCOMP, LVLBDFLT, SPARSE_ENCAPSULATED, COMPOSITESCALINGAUTO}, { FBT_ARBLUT, "1053", Q60, PINPUT, POUTPUT, Q54, Q54, SCALETHI, SCALESTEPTHI, 2, SLOTSPARSE, RINGDM, AFTERBOOT, BEFOREBOOT, 3, LVLSCOMP, LVLBDFLT, SPARSE_ENCAPSULATED, COMPOSITESCALINGAUTO}, { FBT_ARBLUT, "H1053", Q60, PINPUT, POUTPUT, Q54, Q54, SCALETHI, SCALESTEPTHI, 2, SLOTSPARSE, RINGDM, AFTERBOOT, BEFOREBOOT, 3, LVLSCOMP, LVLBDFLT, HYBRID_TERNARY, COMPOSITESCALINGAUTO}, { FBT_SIGNDIGIT, "1054", Q80, Q21, 2, Q64, Q44, 1, 1, 1, SLOTFULL, RINGDM, AFTERBOOT, BEFOREBOOT, 3, LVLSCOMP, LVLBDFLT, SPARSE_ENCAPSULATED, COMPOSITESCALINGAUTO}, @@ -721,12 +735,14 @@ class UTCKKSRNS_FBT : public ::testing::TestWithParam { start = std::chrono::high_resolution_clock::now(); #endif + // N values exercise the default numValues (0), which stands for N under full packing + const uint32_t numValues = (t.numSlots == t.ringDim) ? 0 : t.numSlots; if (binaryLUT) cc->EvalFBTSetup(coeffint, numSlotsCKKS, t.PInput, t.POutput, t.Bigq, keyPair.publicKey, {0, 0}, t.lvlb, - t.levelsAvailableAfterBootstrap, 0, t.order); + t.levelsAvailableAfterBootstrap, 0, t.order, numValues); else cc->EvalFBTSetup(coeffcomp, numSlotsCKKS, t.PInput, t.POutput, t.Bigq, keyPair.publicKey, {0, 0}, - t.lvlb, t.levelsAvailableAfterBootstrap, 0, t.order); + t.lvlb, t.levelsAvailableAfterBootstrap, 0, t.order, numValues); #ifdef BENCH stop = std::chrono::high_resolution_clock::now(); @@ -894,10 +910,10 @@ class UTCKKSRNS_FBT : public ::testing::TestWithParam { if (binaryLUT) cc->EvalFBTSetup(coeffintMod, numSlotsCKKS, t.POutput, t.PInput, t.Bigq, keyPair.publicKey, {0, 0}, - t.lvlb, t.levelsAvailableAfterBootstrap, 0, t.order); + t.lvlb, t.levelsAvailableAfterBootstrap, 0, t.order, t.numSlots); else cc->EvalFBTSetup(coeffcompMod, numSlotsCKKS, t.POutput, t.PInput, t.Bigq, keyPair.publicKey, {0, 0}, - t.lvlb, t.levelsAvailableAfterBootstrap, 0, t.order); + t.lvlb, t.levelsAvailableAfterBootstrap, 0, t.order, t.numSlots); #ifdef BENCH stop = std::chrono::high_resolution_clock::now(); @@ -1119,10 +1135,10 @@ class UTCKKSRNS_FBT : public ::testing::TestWithParam { if (binaryLUT) cc->EvalFBTSetup(coeffint, numSlotsCKKS, t.PInput, t.POutput, t.Bigq, keyPair.publicKey, {0, 0}, t.lvlb, - t.levelsAvailableAfterBootstrap, t.levelsComputation, t.order); + t.levelsAvailableAfterBootstrap, t.levelsComputation, t.order, t.numSlots); else cc->EvalFBTSetup(coeffcomp, numSlotsCKKS, t.PInput, t.POutput, t.Bigq, keyPair.publicKey, {0, 0}, - t.lvlb, t.levelsAvailableAfterBootstrap, t.levelsComputation, t.order); + t.lvlb, t.levelsAvailableAfterBootstrap, t.levelsComputation, t.order, t.numSlots); #ifdef BENCH stop = std::chrono::high_resolution_clock::now(); @@ -1347,10 +1363,10 @@ class UTCKKSRNS_FBT : public ::testing::TestWithParam { if (binaryLUT) cc->EvalFBTSetup(coeffint1, numSlotsCKKS, t.PInput, t.POutput, t.Bigq, keyPair.publicKey, {0, 0}, - t.lvlb, t.levelsAvailableAfterBootstrap, t.levelsComputation, t.order); + t.lvlb, t.levelsAvailableAfterBootstrap, t.levelsComputation, t.order, t.numSlots); else cc->EvalFBTSetup(coeffcomp1, numSlotsCKKS, t.PInput, t.POutput, t.Bigq, keyPair.publicKey, {0, 0}, - t.lvlb, t.levelsAvailableAfterBootstrap, t.levelsComputation, t.order); + t.lvlb, t.levelsAvailableAfterBootstrap, t.levelsComputation, t.order, t.numSlots); #ifdef BENCH stop = std::chrono::high_resolution_clock::now(); @@ -1532,10 +1548,10 @@ class UTCKKSRNS_FBT : public ::testing::TestWithParam { if (binaryLUT) cc->EvalFBTSetup(coeffint, numSlotsCKKS, t.PInput, t.POutput, t.Bigq, keyPair.publicKey, {0, 0}, - t.lvlb, t.levelsAvailableAfterBootstrap, 0, t.order); + t.lvlb, t.levelsAvailableAfterBootstrap, 0, t.order, t.numSlots); else cc->EvalFBTSetup(coeffcomp, numSlotsCKKS, t.PInput, t.POutput, t.Bigq, keyPair.publicKey, {0, 0}, - t.lvlb, t.levelsAvailableAfterBootstrap, 0, t.order); + t.lvlb, t.levelsAvailableAfterBootstrap, 0, t.order, t.numSlots); cc->EvalBootstrapKeyGen(keyPair.secretKey, numSlotsCKKS); cc->EvalMultKeyGen(keyPair.secretKey); @@ -1676,7 +1692,7 @@ class UTCKKSRNS_FBT : public ::testing::TestWithParam { auto keyPair = cc->KeyGen(); cc->EvalFBTSetup(coefficients1, numSlotsCKKS, t.PInput, t.POutput, t.Bigq, keyPair.publicKey, {0, 0}, - t.lvlb, t.levelsAvailableAfterBootstrap, 0, t.order); + t.lvlb, t.levelsAvailableAfterBootstrap, 0, t.order, t.numSlots); cc->EvalBootstrapKeyGen(keyPair.secretKey, numSlotsCKKS); cc->EvalMultKeyGen(keyPair.secretKey); @@ -1777,12 +1793,16 @@ class UTCKKSRNS_FBT : public ::testing::TestWithParam { // than reading past the modulus vector and wrapping the level arithmetic const std::vector dim1{0, 0}; EXPECT_THROW(cc->EvalFBTSetup(coeffint, numSlotsCKKS, t.PInput, t.POutput, t.Bigq, keyPair.publicKey, - dim1, t.lvlb, depth + 1, 0, t.order), + dim1, t.lvlb, depth + 1, 0, t.order, t.numSlots), OpenFHEException) << failmsg << " oversized lvlsAfterBoot not rejected"; + EXPECT_THROW(cc->EvalFBTSetup(coeffint, numSlotsCKKS, t.PInput, t.POutput, t.Bigq, keyPair.publicKey, + dim1, t.lvlb, t.levelsAvailableAfterBootstrap, 0, t.order, t.ringDim + 1), + OpenFHEException) + << failmsg << " more RLWE values than the slots hold not rejected"; cc->EvalFBTSetup(coeffint, numSlotsCKKS, t.PInput, t.POutput, t.Bigq, keyPair.publicKey, {0, 0}, t.lvlb, - t.levelsAvailableAfterBootstrap, 0, t.order); + t.levelsAvailableAfterBootstrap, 0, t.order, t.numSlots); std::vector y(numSlotsCKKS, 0.5); auto ctxt = cc->Encrypt(keyPair.publicKey, cc->MakeCKKSPackedPlaintext(y)); EXPECT_THROW(cc->EvalHomDecoding(ctxt, 1, depth), OpenFHEException) @@ -1797,7 +1817,7 @@ class UTCKKSRNS_FBT : public ::testing::TestWithParam { enableAll(cc); auto keyPair = cc->KeyGen(); cc->EvalFBTSetup(coeffint, numSlotsCKKS, t.PInput, t.POutput, t.Bigq, keyPair.publicKey, {0, 0}, t.lvlb, - t.levelsAvailableAfterBootstrap, 0, t.order); + t.levelsAvailableAfterBootstrap, 0, t.order, t.numSlots); cc->EvalBootstrapKeyGen(keyPair.secretKey, numSlotsCKKS); cc->EvalMultKeyGen(keyPair.secretKey); std::vector y(numSlotsCKKS, 0.5); diff --git a/src/pke/unittest/utckksrns/UnitTestFEFBT.cpp b/src/pke/unittest/utckksrns/UnitTestFEFBT.cpp index cfbb4ae4a..b653eb25b 100644 --- a/src/pke/unittest/utckksrns/UnitTestFEFBT.cpp +++ b/src/pke/unittest/utckksrns/UnitTestFEFBT.cpp @@ -220,8 +220,8 @@ TEST_CASE_UTCKKSRNS_FEFBT MakeFEFBTCSvsFACase(const std::string& description, ui return testCase; } -// A CKKSDataType COMPLEX case: the input carries nonzero imaginary parts, which FE functional -// bootstrapping discards (its output is twice the real part of a Fourier series, hence always real). +// A CKKSDataType COMPLEX case: the input carries nonzero imaginary parts, and FE functional +// bootstrapping applies the function to the real and imaginary parts separately. TEST_CASE_UTCKKSRNS_FEFBT MakeFEFBTComplexInputCase(const std::string& description, uint32_t slots, FEFBT_FUNCTION functionType, std::vector levelBudget) { TEST_CASE_UTCKKSRNS_FEFBT testCase = MakeFEFBTCase(FEFBT_COMPLEX_INPUT, description, slots, SPARSE_TERNARY, slots, @@ -762,39 +762,52 @@ class UTCKKSRNS_FEFBT : public ::testing::TestWithParamEvalBootstrapKeyGen(keyPair.secretKey, testData.slots); cc->EvalMultKeyGen(keyPair.secretKey); + // With CKKSDataType COMPLEX the function is applied to the real and imaginary parts separately. + // The imaginary parts are distinct from the real parts, so applying the function to the wrong + // part, or leaking one part into the other, breaks the comparisons below. auto realPart = BuildNormalizedInput(testData.slots); - auto expected = BuildExpectedOutput(testData.functionType, realPart); + std::vector imagPart(testData.slots); + for (uint32_t i = 0; i < testData.slots; ++i) + imagPart[i] = 0.25 * std::sin(6.0 * M_PI * i / testData.slots); + auto expectedReal = BuildExpectedOutput(testData.functionType, realPart); + auto expectedImag = BuildExpectedOutput(testData.functionType, imagPart); - // imaginary parts large enough that leaking any of them into the result would break the - // comparison below by orders of magnitude more than eps std::vector> input(testData.slots); for (uint32_t i = 0; i < testData.slots; ++i) - input[i] = {realPart[i], 0.25 * std::sin(6.0 * M_PI * i / testData.slots)}; + input[i] = {realPart[i], imagPart[i]}; Plaintext plaintext = cc->MakeCKKSPackedPlaintext(input, 1, FEFBTEncodeLevel(cc, testData), nullptr, testData.slots); auto ciphertext = cc->Encrypt(keyPair.publicKey, plaintext); - auto resultCt = cc->EvalFEFuncBootstrap(ciphertext, GetCoefficients(testData.functionType)); - - Plaintext result; - cc->Decrypt(keyPair.secretKey, resultCt, &result); - result->SetLength(expected.size()); - - const auto packed = result->GetCKKSPackedValue(); - std::vector actualReal(packed.size()); - std::vector actualImag(packed.size()); - for (size_t i = 0; i < packed.size(); ++i) { - actualReal[i] = packed[i].real(); - actualImag[i] = packed[i].imag(); - } - - checkEquality(actualReal, expected, eps, - failmsg + " FE functional bootstrapping of a complex input did not apply " + - GetFunctionName(testData.functionType) + " to the real part."); - checkEquality(actualImag, std::vector(packed.size(), 0.0), eps, - failmsg + - " FE functional bootstrapping of a complex input returned a nonzero " - "imaginary part."); + const auto& coefficients = GetCoefficients(testData.functionType); + + auto checkBothParts = [&](const Ciphertext& resultCt, const std::string& path) { + Plaintext result; + cc->Decrypt(keyPair.secretKey, resultCt, &result); + result->SetLength(expectedReal.size()); + + const auto packed = result->GetCKKSPackedValue(); + std::vector actualReal(packed.size()); + std::vector actualImag(packed.size()); + for (size_t i = 0; i < packed.size(); ++i) { + actualReal[i] = packed[i].real(); + actualImag[i] = packed[i].imag(); + } + + checkEquality(actualReal, expectedReal, eps, + failmsg + " " + path + " of a complex input did not apply " + + GetFunctionName(testData.functionType) + " to the real part."); + checkEquality(actualImag, expectedImag, eps, + failmsg + " " + path + " of a complex input did not apply " + + GetFunctionName(testData.functionType) + " to the imaginary part."); + }; + + checkBothParts(cc->EvalFEFuncBootstrap(ciphertext, coefficients), "FE functional bootstrapping"); + + // the precomputed-powers path has to carry both channels as well + auto powers = cc->EvalFEFuncBootstrapPrecompute(ciphertext, coefficients); + checkBothParts(cc->EvalFEFuncBootstrapWithPrecomp(powers, coefficients), + "FE functional bootstrapping with precomputed powers"); } catch (std::exception& e) { std::cerr << "Exception thrown from " << __func__ << "(): " << e.what() << std::endl; EXPECT_TRUE(0 == 1) << failmsg;