Example #1
0
def example_ckks_basics():
    print("Example: CKKS Basics");

    #In this example we demonstrate evaluating a polynomial function
    #
    #    PI*x^3 + 0.4*x + 1
    #
    #on encrypted floating-point input data x for a set of 4096 equidistant points
    #in the interval [0, 1]. This example demonstrates many of the main features
    #of the CKKS scheme, but also the challenges in using it.
    #
    # We start by setting up the CKKS scheme.

    parms = EncryptionParameters(scheme_type.CKKS)

    #We saw in `2_encoders.cpp' that multiplication in CKKS causes scales
    #in ciphertexts to grow. The scale of any ciphertext must not get too close
    #to the total size of coeff_modulus, or else the ciphertext simply runs out of
    #room to store the scaled-up plaintext. The CKKS scheme provides a `rescale'
    #functionality that can reduce the scale, and stabilize the scale expansion.
    #
    #Rescaling is a kind of modulus switch operation (recall `3_levels.cpp').
    #As modulus switching, it removes the last of the primes from coeff_modulus,
    #but as a side-effect it scales down the ciphertext by the removed prime.
    #Usually we want to have perfect control over how the scales are changed,
    #which is why for the CKKS scheme it is more common to use carefully selected
    #primes for the coeff_modulus.
    #
    #More precisely, suppose that the scale in a CKKS ciphertext is S, and the
    #last prime in the current coeff_modulus (for the ciphertext) is P. Rescaling
    #to the next level changes the scale to S/P, and removes the prime P from the
    #coeff_modulus, as usual in modulus switching. The number of primes limits
    #how many rescalings can be done, and thus limits the multiplicative depth of
    #the computation.
    #
    #It is possible to choose the initial scale freely. One good strategy can be
    #to is to set the initial scale S and primes P_i in the coeff_modulus to be
    #very close to each other. If ciphertexts have scale S before multiplication,
    #they have scale S^2 after multiplication, and S^2/P_i after rescaling. If all
    #P_i are close to S, then S^2/P_i is close to S again. This way we stabilize the
    #scales to be close to S throughout the computation. Generally, for a circuit
    #of depth D, we need to rescale D times, i.e., we need to be able to remove D
    #primes from the coefficient modulus. Once we have only one prime left in the
    #coeff_modulus, the remaining prime must be larger than S by a few bits to
    #preserve the pre-decimal-point value of the plaintext.
    #
    #Therefore, a generally good strategy is to choose parameters for the CKKS
    #scheme as follows: 
    #
    #    (1) Choose a 60-bit prime as the first prime in coeff_modulus. This will
    #        give the highest precision when decrypting;
    #    (2) Choose another 60-bit prime as the last element of coeff_modulus, as
    #        this will be used as the special prime and should be as large as the
    #        largest of the other primes;
    #    (3) Choose the intermediate primes to be close to each other.
    #
    #We use CoeffModulus::Create to generate primes of the appropriate size. Note
    #that our coeff_modulus is 200 bits total, which is below the bound for our
    #poly_modulus_degree: CoeffModulus::MaxBitCount(8192) returns 218.

    poly_modulus_degree = 8192
    parms.set_poly_modulus_degree(poly_modulus_degree)
    parms.set_coeff_modulus(CoeffModulus.Create(
        poly_modulus_degree, IntVector([60, 40, 40, 60])))

    #We choose the initial scale to be 2^40. At the last level, this leaves us
    #60-40=20 bits of precision before the decimal point, and enough (roughly
    #10-20 bits) of precision after the decimal point. Since our intermediate
    #primes are 40 bits (in fact, they are very close to 2^40), we can achieve
    #scale stabilization as described above.

    scale = 2.0**40

    context = SEALContext.Create(parms)
    print_parameters(context)

    keygen = KeyGenerator(context)
    public_key = keygen.public_key()
    secret_key = keygen.secret_key()
    relin_keys = keygen.relin_keys()
    encryptor = Encryptor(context, public_key)
    evaluator = Evaluator(context)
    decryptor = Decryptor(context, secret_key)

    encoder = CKKSEncoder(context)
    slot_count = encoder.slot_count()
    print("Number of slots: {}".format(slot_count))

    step_size = 1.0 / (slot_count - 1)
    input = DoubleVector(list(map(lambda x: x*step_size, range(0, slot_count))))

    print("Input vector: ")
    print_vector(input)

    print("Evaluating polynomial PI*x^3 + 0.4x + 1 ...")

    #We create plaintexts for PI, 0.4, and 1 using an overload of CKKSEncoder::encode
    #that encodes the given floating-point value to every slot in the vector.

    plain_coeff3 = Plaintext()
    plain_coeff1 = Plaintext()
    plain_coeff0 = Plaintext()
    encoder.encode(3.14159265, scale, plain_coeff3)
    encoder.encode(0.4, scale, plain_coeff1)
    encoder.encode(1.0, scale, plain_coeff0)

    x_plain = Plaintext()
    print("Encode input vectors.")
    encoder.encode(input, scale, x_plain)
    x1_encrypted = Ciphertext() 
    encryptor.encrypt(x_plain, x1_encrypted)

    #To compute x^3 we first compute x^2 and relinearize. However, the scale has
    #now grown to 2^80.

    x3_encrypted = Ciphertext() 
    print("Compute x^2 and relinearize:")
    evaluator.square(x1_encrypted, x3_encrypted)
    evaluator.relinearize_inplace(x3_encrypted, relin_keys)
    print("    + Scale of x^2 before rescale: {} bits".format(log2(x3_encrypted.scale())))

    #Now rescale; in addition to a modulus switch, the scale is reduced down by
    #a factor equal to the prime that was switched away (40-bit prime). Hence, the
    #new scale should be close to 2^40. Note, however, that the scale is not equal
    #to 2^40: this is because the 40-bit prime is only close to 2^40.
    print("Rescale x^2.")
    evaluator.rescale_to_next_inplace(x3_encrypted)
    print("    + Scale of x^2 after rescale: {} bits".format(log2(x3_encrypted.scale())))

    #Now x3_encrypted is at a different level than x1_encrypted, which prevents us
    #from multiplying them to compute x^3. We could simply switch x1_encrypted to
    #the next parameters in the modulus switching chain. However, since we still
    #need to multiply the x^3 term with PI (plain_coeff3), we instead compute PI*x
    #first and multiply that with x^2 to obtain PI*x^3. To this end, we compute
    #PI*x and rescale it back from scale 2^80 to something close to 2^40.

    print("Compute and rescale PI*x.")
    x1_encrypted_coeff3 = Ciphertext() 
    evaluator.multiply_plain(x1_encrypted, plain_coeff3, x1_encrypted_coeff3)
    print("    + Scale of PI*x before rescale: {} bits".format(log2(x1_encrypted_coeff3.scale())))
    evaluator.rescale_to_next_inplace(x1_encrypted_coeff3)
    print("    + Scale of PI*x after rescale: {} bits".format(log2(x1_encrypted_coeff3.scale())))

    #Since x3_encrypted and x1_encrypted_coeff3 have the same exact scale and use
    #the same encryption parameters, we can multiply them together. We write the
    #result to x3_encrypted, relinearize, and rescale. Note that again the scale
    #is something close to 2^40, but not exactly 2^40 due to yet another scaling
    #by a prime. We are down to the last level in the modulus switching chain.

    print("Compute, relinearize, and rescale (PI*x)*x^2.")
    evaluator.multiply_inplace(x3_encrypted, x1_encrypted_coeff3)
    evaluator.relinearize_inplace(x3_encrypted, relin_keys)
    print("    + Scale of PI*x^3 before rescale: {} bits".format(log2(x3_encrypted.scale())))

    evaluator.rescale_to_next_inplace(x3_encrypted)
    print("    + Scale of PI*x^3 after rescale: {} bits".format(log2(x3_encrypted.scale())))

    #Next we compute the degree one term. All this requires is one multiply_plain
    #with plain_coeff1. We overwrite x1_encrypted with the result.

    print("Compute and rescale 0.4*x.")
    evaluator.multiply_plain_inplace(x1_encrypted, plain_coeff1)
    print("    + Scale of 0.4*x before rescale: {} bits".format(log2(x1_encrypted.scale())))
    evaluator.rescale_to_next_inplace(x1_encrypted)
    print("    + Scale of 0.4*x after rescale: {} bits".format(log2(x1_encrypted.scale())))

    #Now we would hope to compute the sum of all three terms. However, there is
    #a serious problem: the encryption parameters used by all three terms are
    #different due to modulus switching from rescaling.
    #
    #Encrypted addition and subtraction require that the scales of the inputs are
    #the same, and also that the encryption parameters (parms_id) match. If there
    #is a mismatch, Evaluator will throw an exception.

    print("Parameters used by all three terms are different.")
    print("    + Modulus chain index for x3_encrypted: {}".format(
        context.get_context_data(x3_encrypted.parms_id()).chain_index()))
    print("    + Modulus chain index for x1_encrypted: {}".format(
        context.get_context_data(x1_encrypted.parms_id()).chain_index()))
    print("    + Modulus chain index for plain_coeff0: {}".format(
        context.get_context_data(plain_coeff0.parms_id()).chain_index()))

    #Let us carefully consider what the scales are at this point. We denote the
    #primes in coeff_modulus as P_0, P_1, P_2, P_3, in this order. P_3 is used as
    #the special modulus and is not involved in rescalings. After the computations
    #above the scales in ciphertexts are:
    #
    #    - Product x^2 has scale 2^80 and is at level 2;
    #    - Product PI*x has scale 2^80 and is at level 2;
    #    - We rescaled both down to scale 2^80/P_2 and level 1;
    #    - Product PI*x^3 has scale (2^80/P_2)^2;
    #    - We rescaled it down to scale (2^80/P_2)^2/P_1 and level 0;
    #    - Product 0.4*x has scale 2^80;
    #    - We rescaled it down to scale 2^80/P_2 and level 1;
    #    - The contant term 1 has scale 2^40 and is at level 2.
    #
    #Although the scales of all three terms are approximately 2^40, their exact
    #values are different, hence they cannot be added together.

    print("The exact scales of all three terms are different:")
    print("    + Exact scale in PI*x^3: {0:0.10f}".format(x3_encrypted.scale()))
    print("    + Exact scale in  0.4*x: {0:0.10f}".format(x1_encrypted.scale()))
    print("    + Exact scale in      1: {0:0.10f}".format(plain_coeff0.scale()))

    #There are many ways to fix this problem. Since P_2 and P_1 are really close
    #to 2^40, we can simply "lie" to Microsoft SEAL and set the scales to be the
    #same. For example, changing the scale of PI*x^3 to 2^40 simply means that we
    #scale the value of PI*x^3 by 2^120/(P_2^2*P_1), which is very close to 1.
    #This should not result in any noticeable error.
    #
    #Another option would be to encode 1 with scale 2^80/P_2, do a multiply_plain
    #with 0.4*x, and finally rescale. In this case we would need to additionally
    #make sure to encode 1 with appropriate encryption parameters (parms_id).
    #
    #In this example we will use the first (simplest) approach and simply change
    #the scale of PI*x^3 and 0.4*x to 2^40.
    print("Normalize scales to 2^40.")
    x3_encrypted.set_scale(2.0**40)
    x1_encrypted.set_scale(2.0**40)

    #We still have a problem with mismatching encryption parameters. This is easy
    #to fix by using traditional modulus switching (no rescaling). CKKS supports
    #modulus switching just like the BFV scheme, allowing us to switch away parts
    #of the coefficient modulus when it is simply not needed.

    print("Normalize encryption parameters to the lowest level.")
    last_parms_id = x3_encrypted.parms_id()
    evaluator.mod_switch_to_inplace(x1_encrypted, last_parms_id)
    evaluator.mod_switch_to_inplace(plain_coeff0, last_parms_id)

    #All three ciphertexts are now compatible and can be added.

    print("Compute PI*x^3 + 0.4*x + 1.")
    encrypted_result = Ciphertext()
    evaluator.add(x3_encrypted, x1_encrypted, encrypted_result)
    evaluator.add_plain_inplace(encrypted_result, plain_coeff0)

    #First print the true result.

    plain_result = Plaintext() 
    print("Decrypt and decode PI*x^3 + 0.4x + 1.")
    print("    + Expected result:")
    true_result = DoubleVector(list(map(lambda x: (3.14159265 * x * x + 0.4)* x + 1, input)))
    print_vector(true_result)

    #Decrypt, decode, and print the result.
    decryptor.decrypt(encrypted_result, plain_result)
    result = DoubleVector()
    encoder.decode(plain_result, result)
    print("    + Computed result ...... Correct.")
    print_vector(result)
Example #2
0
def example_ckks_encoder():
    print("Example: Encoders / CKKS Encoder")

    #[CKKSEncoder] (For CKKS scheme only)
    #
    #In this example we demonstrate the Cheon-Kim-Kim-Song (CKKS) scheme for
    #computing on encrypted real or complex numbers. We start by creating
    #encryption parameters for the CKKS scheme. There are two important
    #differences compared to the BFV scheme:
    #
    #    (1) CKKS does not use the plain_modulus encryption parameter;
    #    (2) Selecting the coeff_modulus in a specific way can be very important
    #        when using the CKKS scheme. We will explain this further in the file
    #        `ckks_basics.cpp'. In this example we use CoeffModulus::Create to
    #        generate 5 40-bit prime numbers.

    parms = EncryptionParameters(scheme_type.CKKS)

    poly_modulus_degree = 8192
    parms.set_poly_modulus_degree(poly_modulus_degree)
    parms.set_coeff_modulus(
        CoeffModulus.Create(poly_modulus_degree,
                            IntVector([40, 40, 40, 40, 40])))

    #We create the SEALContext as usual and print the parameters.
    context = SEALContext.Create(parms)
    print_parameters(context)

    #Keys are created the same way as for the BFV scheme.
    keygen = KeyGenerator(context)
    public_key = keygen.public_key()
    secret_key = keygen.secret_key()
    relin_keys = keygen.relin_keys()

    #We also set up an Encryptor, Evaluator, and Decryptor as usual.
    encryptor = Encryptor(context, public_key)
    evaluator = Evaluator(context)
    decryptor = Decryptor(context, secret_key)

    #To create CKKS plaintexts we need a special encoder: there is no other way
    #to create them. The IntegerEncoder and BatchEncoder cannot be used with the
    #CKKS scheme. The CKKSEncoder encodes vectors of real or complex numbers into
    #Plaintext objects, which can subsequently be encrypted. At a high level this
    #looks a lot like what BatchEncoder does for the BFV scheme, but the theory
    #behind it is completely different.

    encoder = CKKSEncoder(context)

    #In CKKS the number of slots is poly_modulus_degree / 2 and each slot encodes
    #one real or complex number. This should be contrasted with BatchEncoder in
    #the BFV scheme, where the number of slots is equal to poly_modulus_degree
    #and they are arranged into a matrix with two rows.

    slot_count = encoder.slot_count()
    print("Number of slots: {}".format(slot_count))

    #We create a small vector to encode; the CKKSEncoder will implicitly pad it
    #with zeros to full size (poly_modulus_degree / 2) when encoding.

    input = DoubleVector([0.0, 1.1, 2.2, 3.3])
    print("Input vector: ")
    print_vector(input)

    #Now we encode it with CKKSEncoder. The floating-point coefficients of `input'
    #will be scaled up by the parameter `scale'. This is necessary since even in
    #the CKKS scheme the plaintext elements are fundamentally polynomials with
    #integer coefficients. It is instructive to think of the scale as determining
    #the bit-precision of the encoding; naturally it will affect the precision of
    #the result.
    #
    #In CKKS the message is stored modulo coeff_modulus (in BFV it is stored modulo
    #plain_modulus), so the scaled message must not get too close to the total size
    #of coeff_modulus. In this case our coeff_modulus is quite large (200 bits) so
    #we have little to worry about in this regard. For this simple example a 30-bit
    #scale is more than enough.

    plain = Plaintext()
    scale = 2.0**30
    print("Encode input vector.")
    encoder.encode(input, scale, plain)

    #We can instantly decode to check the correctness of encoding.
    output = DoubleVector()
    print("    + Decode input vector ...... Correct.")
    encoder.decode(plain, output)
    print_vector(output)

    #The vector is encrypted the same was as in BFV.
    encrypted = Ciphertext()
    print("Encrypt input vector, square, and relinearize.")
    encryptor.encrypt(plain, encrypted)

    #Basic operations on the ciphertexts are still easy to do. Here we square the
    #ciphertext, decrypt, decode, and print the result. We note also that decoding
    #returns a vector of full size (poly_modulus_degree / 2); this is because of
    #the implicit zero-padding mentioned above.

    evaluator.square_inplace(encrypted)
    evaluator.relinearize_inplace(encrypted, relin_keys)

    #We notice that the scale in the result has increased. In fact, it is now the
    #square of the original scale: 2^60.

    print("    + Scale in squared input: {} ( {} bits)".format(
        encrypted.scale(), log2(encrypted.scale())))

    print("Decrypt and decode.")
    decryptor.decrypt(encrypted, plain)
    encoder.decode(plain, output)
    print("    + Result vector ...... Correct.")
    print_vector(output)