Example recreating the behavior is included below:
#include "openfhe.h"
using namespace lbcrypto;
int main() {
// Step 1: Setup CryptoContext
// A. Specify main parameters
/* A1) Multiplicative depth:
* The CKKS scheme we setup here will work for any computation
* that has a multiplicative depth equal to 'multDepth'.
* This is the maximum possible depth of a given multiplication,
* but not the total number of multiplications supported by the
* scheme.
*
* For example, computation f(x, y) = x^2 + x*y + y^2 + x + y has
* a multiplicative depth of 1, but requires a total of 3 multiplications.
* On the other hand, computation g(x_i) = x1*x2*x3*x4 can be implemented
* either as a computation of multiplicative depth 3 as
* g(x_i) = ((x1*x2)*x3)*x4, or as a computation of multiplicative depth 2
* as g(x_i) = (x1*x2)*(x3*x4).
*
* For performance reasons, it's generally preferable to perform operations
* in the shorted multiplicative depth possible.
*/
uint32_t multDepth = 0;
/* A2) Bit-length of scaling factor.
* CKKS works for real numbers, but these numbers are encoded as integers.
* For instance, real number m=0.01 is encoded as m'=round(m*D), where D is
* a scheme parameter called scaling factor. Suppose D=1000, then m' is 10 (an
* integer). Say the result of a computation based on m' is 130, then at
* decryption, the scaling factor is removed so the user is presented with
* the real number result of 0.13.
*
* Parameter 'scaleModSize' determines the bit-length of the scaling
* factor D, but not the scaling factor itself. The latter is implementation
* specific, and it may also vary between ciphertexts in certain versions of
* CKKS (e.g., in FLEXIBLEAUTO).
*
* Choosing 'scaleModSize' depends on the desired accuracy of the
* computation, as well as the remaining parameters like multDepth or security
* standard. This is because the remaining parameters determine how much noise
* will be incurred during the computation (remember CKKS is an approximate
* scheme that incurs small amounts of noise with every operation). The
* scaling factor should be large enough to both accommodate this noise and
* support results that match the desired accuracy.
*/
uint32_t scaleModSize = 50;
/* A3) Number of plaintext slots used in the ciphertext.
* CKKS packs multiple plaintext values in each ciphertext.
* The maximum number of slots depends on a security parameter called ring
* dimension. In this instance, we don't specify the ring dimension directly,
* but let the library choose it for us, based on the security level we
* choose, the multiplicative depth we want to support, and the scaling factor
* size.
*
* Please use method GetRingDimension() to find out the exact ring dimension
* being used for these parameters. Give ring dimension N, the maximum batch
* size is N/2, because of the way CKKS works.
*/
uint32_t batchSize = 1;
/* A4) Desired security level based on FHE standards.
* This parameter can take four values. Three of the possible values
* correspond to 128-bit, 192-bit, and 256-bit security, and the fourth value
* corresponds to "NotSet", which means that the user is responsible for
* choosing security parameters. Naturally, "NotSet" should be used only in
* non-production environments, or by experts who understand the security
* implications of their choices.
*
* If a given security level is selected, the library will consult the current
* security parameter tables defined by the FHE standards consortium
* (https://homomorphicencryption.org/introduction/) to automatically
* select the security parameters. Please see "TABLES of RECOMMENDED
* PARAMETERS" in the following reference for more details:
* http://homomorphicencryption.org/wp-content/uploads/2018/11/HomomorphicEncryptionStandardv1.1.pdf
*/
CCParams<CryptoContextCKKSRNS> parameters;
parameters.SetMultiplicativeDepth(multDepth);
parameters.SetScalingModSize(scaleModSize);
parameters.SetScalingTechnique(FLEXIBLEAUTOEXT);
parameters.SetBatchSize(batchSize);
CryptoContext<DCRTPoly> cc = GenCryptoContext(parameters);
// Enable the features that you wish to use
cc->Enable(PKE);
cc->Enable(KEYSWITCH);
cc->Enable(LEVELEDSHE);
std::cout << "CKKS scheme is using ring dimension " << cc->GetRingDimension() << std::endl << std::endl;
std::cout << "first modulus: " << 60 << std::endl << std::endl;
std::cout << "scaling modulus: " << scaleModSize << std::endl << std::endl;
std::cout << "depth: " << multDepth << std::endl << std::endl;
// B. Step 2: Key Generation
/* B1) Generate encryption keys.
* These are used for encryption/decryption, as well as in generating
* different kinds of keys.
*/
auto keys = cc->KeyGen();
/* B2) Generate the digit size
* In CKKS, whenever someone multiplies two ciphertexts encrypted with key s,
* we get a result with some components that are valid under key s, and
* with an additional component that's valid under key s^2.
*
* In most cases, we want to perform relinearization of the multiplicaiton
* result, i.e., we want to transform the s^2 component of the ciphertext so
* it becomes valid under original key s. To do so, we need to create what we
* call a relinearization key with the following line.
*/
cc->EvalMultKeyGen(keys.secretKey);
/* B3) Generate the rotation keys
* CKKS supports rotating the contents of a packed ciphertext, but to do so,
* we need to create what we call a rotation key. This is done with the
* following call, which takes as input a vector with indices that correspond
* to the rotation offset we want to support. Negative indices correspond to
* right shift and positive to left shift. Look at the output of this demo for
* an illustration of this.
*
* Keep in mind that rotations work over the batch size or entire ring dimension (if the batch size is not specified).
* This means that, if ring dimension is 8 and batch
* size is not specified, then an input (1,2,3,4,0,0,0,0) rotated by 2 will become
* (3,4,0,0,0,0,1,2) and not (3,4,1,2,0,0,0,0).
* If ring dimension is 8 and batch
* size is set to 4, then the rotation of (1,2,3,4) by 2 will become (3,4,1,2).
* Also, as someone can observe
* in the output of this demo, since CKKS is approximate, zeros are not exact
* - they're just very small numbers.
*/
cc->EvalRotateKeyGen(keys.secretKey, {1, -2});
// Step 3: Encoding and encryption of inputs
// Inputs
std::vector<double> x1 = {3.5};
std::vector<double> x2 = {1.0};
std::vector<double> x3 = {-2.5};
// Encoding as plaintexts
Plaintext ptxt1 = cc->MakeCKKSPackedPlaintext(x1);
Plaintext ptxt2 = cc->MakeCKKSPackedPlaintext(x2);
Plaintext ptxt3 = cc->MakeCKKSPackedPlaintext(x3);
std::cout << "Input x1: " << ptxt1 << std::endl;
std::cout << "Input x2: " << ptxt2 << std::endl;
std::cout << "Input x3: " << ptxt3 << std::endl;
// Encrypt the encoded vectors
auto c1 = cc->Encrypt(keys.publicKey, ptxt1);
auto c2 = cc->Encrypt(keys.publicKey, ptxt2);
auto c3 = cc->Encrypt(keys.publicKey, ptxt3);
// Step 4: Evaluation
// Homomorphic addition
auto cAdd1 = cc->EvalAdd(c1, c2);
auto cAdd2 = cc->EvalAdd(c1, c3);
cAdd1 = cc->EvalAdd(cAdd1, c3);
cAdd2 = cc->EvalAdd(cAdd2, c2);
// Step 5: Decryption and output
Plaintext result;
// We set the cout precision to 8 decimal digits for a nicer output.
// If you want to see the error/noise introduced by CKKS, bump it up
// to 15 and it should become visible.
std::cout.precision(8);
std::cout << std::endl << "Results of homomorphic computations: " << std::endl;
// Decrypt the result of addition
cc->Decrypt(keys.secretKey, cAdd1, &result);
result->SetLength(8);
std::cout << "(x1 + x2) + x3 = " << result;
std::cout << "Estimated precision in bits: " << result->GetLogPrecision() << std::endl;
cc->Decrypt(keys.secretKey, cAdd2, &result);
result->SetLength(8);
std::cout << "(x1 + x3) + x2 = " << result;
std::cout << "Estimated precision in bits: " << result->GetLogPrecision() << std::endl;
return 0;
}
Example recreating the behavior is included below: