def example_rotation_ckks(): print("Example: Rotation / Rotation in CKKS"); #Rotations in the CKKS scheme work very similarly to rotations in BFV. 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]))) 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() gal_keys = keygen.galois_keys() encryptor = Encryptor(context, public_key) evaluator = Evaluator(context) decryptor = Decryptor(context, secret_key) ckks_encoder = CKKSEncoder(context) slot_count = ckks_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_vector(input) scale = 2.0**50 print("Encode and encrypt.") plain = Plaintext() ckks_encoder.encode(input, scale, plain) encrypted = Ciphertext() encryptor.encrypt(plain, encrypted) rotated = Ciphertext() print("Rotate 2 steps left.") evaluator.rotate_vector(encrypted, 2, gal_keys, rotated) print(" + Decrypt and decode ...... Correct.") decryptor.decrypt(rotated, plain) result = DoubleVector() ckks_encoder.decode(plain, result) print_vector(result)
def example_rotation_bfv(): print("Example: Rotation / Rotation in BFV") parms = EncryptionParameters(scheme_type.BFV) poly_modulus_degree = 8192 parms.set_poly_modulus_degree(poly_modulus_degree) parms.set_coeff_modulus(CoeffModulus.BFVDefault(poly_modulus_degree)) parms.set_plain_modulus(PlainModulus.Batching(poly_modulus_degree, 20)) 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) batch_encoder = BatchEncoder(context) slot_count = batch_encoder.slot_count() row_size = int(slot_count / 2) print("Plaintext matrix row size: {}".format(row_size)) pod_matrix = UInt64Vector([0]*slot_count) pod_matrix[0] = 0 pod_matrix[1] = 1 pod_matrix[2] = 2 pod_matrix[3] = 3 pod_matrix[row_size] = 4 pod_matrix[row_size + 1] = 5 pod_matrix[row_size + 2] = 6 pod_matrix[row_size + 3] = 7 print("Input plaintext matrix:") print_matrix(pod_matrix, row_size) #First we use BatchEncoder to encode the matrix into a plaintext. We encrypt #the plaintext as usual. plain_matrix = Plaintext() print("Encode and encrypt.") batch_encoder.encode(pod_matrix, plain_matrix) encrypted_matrix = Ciphertext() encryptor.encrypt(plain_matrix, encrypted_matrix) print(" + Noise budget in fresh encryption: {} bits".format( decryptor.invariant_noise_budget(encrypted_matrix))) #Rotations require yet another type of special key called `Galois keys'. These #are easily obtained from the KeyGenerator. gal_keys = keygen.galois_keys() #Now rotate both matrix rows 3 steps to the left, decrypt, decode, and print. print("Rotate rows 3 steps left.") evaluator.rotate_rows_inplace(encrypted_matrix, 3, gal_keys) plain_result = Plaintext() print(" + Noise budget after rotation: {} bits".format( decryptor.invariant_noise_budget(encrypted_matrix))) print(" + Decrypt and decode ...... Correct.") decryptor.decrypt(encrypted_matrix, plain_result) batch_encoder.decode(plain_result, pod_matrix) print_matrix(pod_matrix, row_size) #We can also rotate the columns, i.e., swap the rows. print("Rotate columns.") evaluator.rotate_columns_inplace(encrypted_matrix, gal_keys) print(" + Noise budget after rotation: {} bits".format( decryptor.invariant_noise_budget(encrypted_matrix))) print(" + Decrypt and decode ...... Correct.") decryptor.decrypt(encrypted_matrix, plain_result) batch_encoder.decode(plain_result, pod_matrix) print_matrix(pod_matrix, row_size) #Finally, we rotate the rows 4 steps to the right, decrypt, decode, and print. print("Rotate rows 4 steps right.") evaluator.rotate_rows_inplace(encrypted_matrix, -4, gal_keys) print(" + Noise budget after rotation: {} bits".format( decryptor.invariant_noise_budget(encrypted_matrix))) print(" + Decrypt and decode ...... Correct.") decryptor.decrypt(encrypted_matrix, plain_result) batch_encoder.decode(plain_result, pod_matrix) print_matrix(pod_matrix, row_size)
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)
def example_bfv_basics(): print("Example: BFV Basics") #In this example, we demonstrate performing simple computations (a polynomial #evaluation) on encrypted integers using the BFV encryption scheme. # #The first task is to set up an instance of the EncryptionParameters class. #It is critical to understand how the different parameters behave, how they #affect the encryption scheme, performance, and the security level. There are #three encryption parameters that are necessary to set: # # - poly_modulus_degree (degree of polynomial modulus); # - coeff_modulus ([ciphertext] coefficient modulus); # - plain_modulus (plaintext modulus; only for the BFV scheme). # #The BFV scheme cannot perform arbitrary computations on encrypted data. #Instead, each ciphertext has a specific quantity called the `invariant noise #budget' -- or `noise budget' for short -- measured in bits. The noise budget #in a freshly encrypted ciphertext (initial noise budget) is determined by #the encryption parameters. Homomorphic operations consume the noise budget #at a rate also determined by the encryption parameters. In BFV the two basic #operations allowed on encrypted data are additions and multiplications, of #which additions can generally be thought of as being nearly free in terms of #noise budget consumption compared to multiplications. Since noise budget #consumption compounds in sequential multiplications, the most significant #factor in choosing appropriate encryption parameters is the multiplicative #depth of the arithmetic circuit that the user wants to evaluate on encrypted #data. Once the noise budget of a ciphertext reaches zero it becomes too #corrupted to be decrypted. Thus, it is essential to choose the parameters to #be large enough to support the desired computation; otherwise the result is #impossible to make sense of even with the secret key. parms = EncryptionParameters(scheme_type.BFV) #The first parameter we set is the degree of the `polynomial modulus'. This #must be a positive power of 2, representing the degree of a power-of-two #cyclotomic polynomial; it is not necessary to understand what this means. # #Larger poly_modulus_degree makes ciphertext sizes larger and all operations #slower, but enables more complicated encrypted computations. Recommended #values are 1024, 2048, 4096, 8192, 16384, 32768, but it is also possible #to go beyond this range. # #In this example we use a relatively small polynomial modulus. Anything #smaller than this will enable only very restricted encrypted computations. poly_modulus_degree = 4096 parms.set_poly_modulus_degree(poly_modulus_degree) #Next we set the [ciphertext] `coefficient modulus' (coeff_modulus). This #parameter is a large integer, which is a product of distinct prime numbers, #each up to 60 bits in size. It is represented as a vector of these prime #numbers, each represented by an instance of the SmallModulus class. The #bit-length of coeff_modulus means the sum of the bit-lengths of its prime #factors. # #A larger coeff_modulus implies a larger noise budget, hence more encrypted #computation capabilities. However, an upper bound for the total bit-length #of the coeff_modulus is determined by the poly_modulus_degree, as follows: # # +----------------------------------------------------+ # | poly_modulus_degree | max coeff_modulus bit-length | # +---------------------+------------------------------+ # | 1024 | 27 | # | 2048 | 54 | # | 4096 | 109 | # | 8192 | 218 | # | 16384 | 438 | # | 32768 | 881 | # +---------------------+------------------------------+ # #These numbers can also be found in native/src/seal/util/hestdparms.h encoded #in the function SEAL_HE_STD_PARMS_128_TC, and can also be obtained from the #function # # CoeffModulus::MaxBitCount(poly_modulus_degree). # #For example, if poly_modulus_degree is 4096, the coeff_modulus could consist #of three 36-bit primes (108 bits). # #Microsoft SEAL comes with helper functions for selecting the coeff_modulus. #For new users the easiest way is to simply use # # CoeffModulus::BFVDefault(poly_modulus_degree), # #which returns std::vector<SmallModulus> consisting of a generally good choice #for the given poly_modulus_degree. parms.set_coeff_modulus(CoeffModulus.BFVDefault(poly_modulus_degree)) #The plaintext modulus can be any positive integer, even though here we take #it to be a power of two. In fact, in many cases one might instead want it #to be a prime number; we will see this in later examples. The plaintext #modulus determines the size of the plaintext data type and the consumption #of noise budget in multiplications. Thus, it is essential to try to keep the #plaintext data type as small as possible for best performance. The noise #budget in a freshly encrypted ciphertext is # # ~ log2(coeff_modulus/plain_modulus) (bits) # #and the noise budget consumption in a homomorphic multiplication is of the #form log2(plain_modulus) + (other terms). # #The plaintext modulus is specific to the BFV scheme, and cannot be set when #using the CKKS scheme. parms.set_plain_modulus(1024) #Now that all parameters are set, we are ready to construct a SEALContext #object. This is a heavy class that checks the validity and properties of the #parameters we just set. context = SEALContext.Create(parms) #Print the parameters that we have chosen. print("Set encryption parameters and print") print_parameters(context) print("~~~~~~ A naive way to calculate 4(x^2+1)(x+1)^2. ~~~~~~") #The encryption schemes in Microsoft SEAL are public key encryption schemes. #For users unfamiliar with this terminology, a public key encryption scheme #has a separate public key for encrypting data, and a separate secret key for #decrypting data. This way multiple parties can encrypt data using the same #shared public key, but only the proper recipient of the data can decrypt it #with the secret key. # #We are now ready to generate the secret and public keys. For this purpose #we need an instance of the KeyGenerator class. Constructing a KeyGenerator #automatically generates the public and secret key, which can immediately be #read to local variables. keygen = KeyGenerator(context) public_key = keygen.public_key() secret_key = keygen.secret_key() #To be able to encrypt we need to construct an instance of Encryptor. Note #that the Encryptor only requires the public key, as expected. encryptor = Encryptor(context, public_key) #Computations on the ciphertexts are performed with the Evaluator class. In #a real use-case the Evaluator would not be constructed by the same party #that holds the secret key. evaluator = Evaluator(context) #We will of course want to decrypt our results to verify that everything worked, #so we need to also construct an instance of Decryptor. Note that the Decryptor #requires the secret key. decryptor = Decryptor(context, secret_key) #As an example, we evaluate the degree 4 polynomial # # 4x^4 + 8x^3 + 8x^2 + 8x + 4 # #over an encrypted x = 6. The coefficients of the polynomial can be considered #as plaintext inputs, as we will see below. The computation is done modulo the #plain_modulus 1024. # #While this examples is simple and easy to understand, it does not have much #practical value. In later examples we will demonstrate how to compute more #efficiently on encrypted integers and real or complex numbers. # #Plaintexts in the BFV scheme are polynomials of degree less than the degree #of the polynomial modulus, and coefficients integers modulo the plaintext #modulus. For readers with background in ring theory, the plaintext space is #the polynomial quotient ring Z_T[X]/(X^N + 1), where N is poly_modulus_degree #and T is plain_modulus. # #To get started, we create a plaintext containing the constant 6. For the #plaintext element we use a constructor that takes the desired polynomial as #a string with coefficients represented as hexadecimal numbers. x = 6 x_plain = Plaintext(str(x)) print("Express x = {} as a plaintext polynomial 0x{}.".format( x, x_plain.to_string())) #We then encrypt the plaintext, producing a ciphertext. x_encrypted = Ciphertext() print("Encrypt x_plain to x_encrypted.") encryptor.encrypt(x_plain, x_encrypted) #In Microsoft SEAL, a valid ciphertext consists of two or more polynomials #whose coefficients are integers modulo the product of the primes in the #coeff_modulus. The number of polynomials in a ciphertext is called its `size' #and is given by Ciphertext::size(). A freshly encrypted ciphertext always #has size 2. print(" + size of freshly encrypted x: {}".format(x_encrypted.size())) #There is plenty of noise budget left in this freshly encrypted ciphertext. print(" + noise budget in freshly encrypted x: {} bits".format( decryptor.invariant_noise_budget(x_encrypted))) #We decrypt the ciphertext and print the resulting plaintext in order to #demonstrate correctness of the encryption. x_decrypted = Plaintext() decryptor.decrypt(x_encrypted, x_decrypted) print(" + decryption of x_encrypted: 0x{} ...... Correct.".format( x_decrypted.to_string())) #When using Microsoft SEAL, it is typically advantageous to compute in a way #that minimizes the longest chain of sequential multiplications. In other #words, encrypted computations are best evaluated in a way that minimizes #the multiplicative depth of the computation, because the total noise budget #consumption is proportional to the multiplicative depth. For example, for #our example computation it is advantageous to factorize the polynomial as # # 4x^4 + 8x^3 + 8x^2 + 8x + 4 = 4(x + 1)^2 * (x^2 + 1) # #to obtain a simple depth 2 representation. Thus, we compute (x + 1)^2 and #(x^2 + 1) separately, before multiplying them, and multiplying by 4. # #First, we compute x^2 and add a plaintext "1". We can clearly see from the #print-out that multiplication has consumed a lot of noise budget. The user #can vary the plain_modulus parameter to see its effect on the rate of noise #budget consumption. print("Compute x_sq_plus_one (x^2+1).") x_sq_plus_one = Ciphertext() evaluator.square(x_encrypted, x_sq_plus_one) plain_one = Plaintext("1") evaluator.add_plain_inplace(x_sq_plus_one, plain_one) #Encrypted multiplication results in the output ciphertext growing in size. #More precisely, if the input ciphertexts have size M and N, then the output #ciphertext after homomorphic multiplication will have size M+N-1. In this #case we perform a squaring, and observe both size growth and noise budget #consumption. print(" + size of x_sq_plus_one: {}".format(x_sq_plus_one.size())) print(" + noise budget in x_sq_plus_one: {} bits".format( decryptor.invariant_noise_budget(x_sq_plus_one))) #Even though the size has grown, decryption works as usual as long as noise #budget has not reached 0. decrypted_result = Plaintext() decryptor.decrypt(x_sq_plus_one, decrypted_result) print(" + decryption of x_sq_plus_one: 0x{} ...... Correct.".format( decrypted_result.to_string())) #Next, we compute (x + 1)^2. print("Compute x_plus_one_sq ((x+1)^2).") x_plus_one_sq = Ciphertext() evaluator.add_plain(x_encrypted, plain_one, x_plus_one_sq) evaluator.square_inplace(x_plus_one_sq) print(" + size of x_plus_one_sq: {}".format(x_plus_one_sq.size())) print(" + noise budget in x_plus_one_sq: {} bits".format( decryptor.invariant_noise_budget(x_plus_one_sq))) decryptor.decrypt(x_plus_one_sq, decrypted_result) print(" + decryption of x_plus_one_sq: 0x{} ...... Correct.".format( decrypted_result.to_string())) #Finally, we multiply (x^2 + 1) * (x + 1)^2 * 4. print("Compute encrypted_result (4(x^2+1)(x+1)^2).") encrypted_result = Ciphertext() plain_four = Plaintext("4") evaluator.multiply_plain_inplace(x_sq_plus_one, plain_four) evaluator.multiply(x_sq_plus_one, x_plus_one_sq, encrypted_result) print(" + size of encrypted_result: {}".format(encrypted_result.size())) print(" + noise budget in encrypted_result: {} bits".format( decryptor.invariant_noise_budget(encrypted_result))) print("NOTE: Decryption can be incorrect if noise budget is zero.") print("~~~~~~ A better way to calculate 4(x^2+1)(x+1)^2. ~~~~~~") #Noise budget has reached 0, which means that decryption cannot be expected #to give the correct result. This is because both ciphertexts x_sq_plus_one #and x_plus_one_sq consist of 3 polynomials due to the previous squaring #operations, and homomorphic operations on large ciphertexts consume much more #noise budget than computations on small ciphertexts. Computing on smaller #ciphertexts is also computationally significantly cheaper. #`Relinearization' is an operation that reduces the size of a ciphertext after #multiplication back to the initial size, 2. Thus, relinearizing one or both #input ciphertexts before the next multiplication can have a huge positive #impact on both noise growth and performance, even though relinearization has #a significant computational cost itself. It is only possible to relinearize #size 3 ciphertexts down to size 2, so often the user would want to relinearize #after each multiplication to keep the ciphertext sizes at 2. #Relinearization requires special `relinearization keys', which can be thought #of as a kind of public key. Relinearization keys can easily be created with #the KeyGenerator. #Relinearization is used similarly in both the BFV and the CKKS schemes, but #in this example we continue using BFV. We repeat our computation from before, #but this time relinearize after every multiplication. #We use KeyGenerator::relin_keys() to create relinearization keys. print("Generate relinearization keys.") relin_keys = keygen.relin_keys() #We now repeat the computation relinearizing after each multiplication. print("Compute and relinearize x_squared (x^2),") print("then compute x_sq_plus_one (x^2+1)") x_squared = Ciphertext() evaluator.square(x_encrypted, x_squared) print(" + size of x_squared: {}".format(x_squared.size())) evaluator.relinearize_inplace(x_squared, relin_keys) print(" + size of x_squared (after relinearization): {}".format( x_squared.size())) evaluator.add_plain(x_squared, plain_one, x_sq_plus_one) print(" + noise budget in x_sq_plus_one: {} bits".format( decryptor.invariant_noise_budget(x_sq_plus_one))) decryptor.decrypt(x_sq_plus_one, decrypted_result) print(" + decryption of x_sq_plus_one: 0x{} ...... Correct.".format( decrypted_result.to_string())) x_plus_one = Ciphertext() print("Compute x_plus_one (x+1),") print("then compute and relinearize x_plus_one_sq ((x+1)^2).") evaluator.add_plain(x_encrypted, plain_one, x_plus_one) evaluator.square(x_plus_one, x_plus_one_sq) print(" + size of x_plus_one_sq: {}".format(x_plus_one_sq.size())) evaluator.relinearize_inplace(x_plus_one_sq, relin_keys) print(" + noise budget in x_plus_one_sq: {} bits".format( decryptor.invariant_noise_budget(x_plus_one_sq))) decryptor.decrypt(x_plus_one_sq, decrypted_result) print(" + decryption of x_plus_one_sq: 0x{} ...... Correct.".format( decrypted_result.to_string())) print("Compute and relinearize encrypted_result (4(x^2+1)(x+1)^2).") evaluator.multiply_plain_inplace(x_sq_plus_one, plain_four) evaluator.multiply(x_sq_plus_one, x_plus_one_sq, encrypted_result) print(" + size of encrypted_result: {}".format(encrypted_result.size())) evaluator.relinearize_inplace(encrypted_result, relin_keys) print(" + size of encrypted_result (after relinearization): {}".format( encrypted_result.size())) print(" + noise budget in encrypted_result: {} bits".format( decryptor.invariant_noise_budget(encrypted_result))) print("NOTE: Notice the increase in remaining noise budget.") #Relinearization clearly improved our noise consumption. We have still plenty #of noise budget left, so we can expect the correct answer when decrypting. print("Decrypt encrypted_result (4(x^2+1)(x+1)^2).") decryptor.decrypt(encrypted_result, decrypted_result) print(" + decryption of 4(x^2+1)(x+1)^2 = 0x{} ...... Correct.".format( decrypted_result.to_string()))
def example_levels(): print("Example: Levels") #In this examples we describe the concept of `levels' in BFV and CKKS and the #related objects that represent them in Microsoft SEAL. # #In Microsoft SEAL a set of encryption parameters (excluding the random number #generator) is identified uniquely by a 256-bit hash of the parameters. This #hash is called the `parms_id' and can be easily accessed and printed at any #time. The hash will change as soon as any of the parameters is changed. # #When a SEALContext is created from a given EncryptionParameters instance, #Microsoft SEAL automatically creates a so-called `modulus switching chain', #which is a chain of other encryption parameters derived from the original set. #The parameters in the modulus switching chain are the same as the original #parameters with the exception that size of the coefficient modulus is #decreasing going down the chain. More precisely, each parameter set in the #chain attempts to remove the last coefficient modulus prime from the #previous set; this continues until the parameter set is no longer valid #(e.g., plain_modulus is larger than the remaining coeff_modulus). It is easy #to walk through the chain and access all the parameter sets. Additionally, #each parameter set in the chain has a `chain index' that indicates its #position in the chain so that the last set has index 0. We say that a set #of encryption parameters, or an object carrying those encryption parameters, #is at a higher level in the chain than another set of parameters if its the #chain index is bigger, i.e., it is earlier in the chain. # #Each set of parameters in the chain involves unique pre-computations performed #when the SEALContext is created, and stored in a SEALContext::ContextData #object. The chain is basically a linked list of SEALContext::ContextData #objects, and can easily be accessed through the SEALContext at any time. Each #node can be identified by the parms_id of its specific encryption parameters #(poly_modulus_degree remains the same but coeff_modulus varies). parms = EncryptionParameters(scheme_type.BFV) poly_modulus_degree = 8192 parms.set_poly_modulus_degree(poly_modulus_degree) #In this example we use a custom coeff_modulus, consisting of 5 primes of #sizes 50, 30, 30, 50, and 50 bits. Note that this is still OK according to #the explanation in `1_bfv_basics.cpp'. Indeed, # # CoeffModulus::MaxBitCount(poly_modulus_degree) # #returns 218 (greater than 50+30+30+50+50=210). # #Due to the modulus switching chain, the order of the 5 primes is significant. #The last prime has a special meaning and we call it the `special prime'. Thus, #the first parameter set in the modulus switching chain is the only one that #involves the special prime. All key objects, such as SecretKey, are created #at this highest level. All data objects, such as Ciphertext, can be only at #lower levels. The special prime should be as large as the largest of the #other primes in the coeff_modulus, although this is not a strict requirement. # # special prime +---------+ # | # v #coeff_modulus: { 50, 30, 30, 50, 50 } +---+ Level 4 (all keys; `key level') # | # | # coeff_modulus: { 50, 30, 30, 50 } +---+ Level 3 (highest `data level') # | # | # coeff_modulus: { 50, 30, 30 } +---+ Level 2 # | # | # coeff_modulus: { 50, 30 } +---+ Level 1 # | # | # coeff_modulus: { 50 } +---+ Level 0 (lowest level) parms.set_coeff_modulus( CoeffModulus.Create(poly_modulus_degree, IntVector([50, 30, 30, 50, 50]))) #In this example the plain_modulus does not play much of a role; we choose #some reasonable value. parms.set_plain_modulus(PlainModulus.Batching(poly_modulus_degree, 20)) context = SEALContext.Create(parms) print_parameters(context) #There are convenience method for accessing the SEALContext::ContextData for #some of the most important levels: # # SEALContext::key_context_data(): access to key level ContextData # SEALContext::first_context_data(): access to highest data level ContextData # SEALContext::last_context_data(): access to lowest level ContextData # #We iterate over the chain and print the parms_id for each set of parameters. print("Print the modulus switching chain.") #First print the key level parameter information. context_data = context.key_context_data() print("----> Level (chain index): {} ...... key_context_data()".format( context_data.chain_index())) print(" parms_id: {}".format(list2hex(context_data.parms_id()))) print("coeff_modulus primes:", end=' ') for prime in context_data.parms().coeff_modulus(): print(hex(prime.value()), end=' ') print("") print("\\") print(" \\-->") #Next iterate over the remaining (data) levels. context_data = context.first_context_data() while (context_data): print(" Level (chain index): {} ".format(context_data.chain_index()), end='') if context_data.parms_id() == context.first_parms_id(): print(" ...... first_context_data()") elif context_data.parms_id() == context.last_parms_id(): print(" ...... last_context_data()") else: print("") print(" parms_id: {}".format(list2hex(context_data.parms_id()))) print("coeff_modulus primes:", end=' ') for prime in context_data.parms().coeff_modulus(): print(hex(prime.value()), end=' ') print("") print("\\") print(" \\-->") #Step forward in the chain. context_data = context_data.next_context_data() print(" End of chain reached") #We create some keys and check that indeed they appear at the highest level. keygen = KeyGenerator(context) public_key = keygen.public_key() secret_key = keygen.secret_key() relin_keys = keygen.relin_keys() galois_keys = keygen.galois_keys() print("Print the parameter IDs of generated elements.") print(" + public_key: {}".format(list2hex(public_key.parms_id()))) print(" + secret_key: {}".format(list2hex(secret_key.parms_id()))) print(" + relin_keys: {}".format(list2hex(relin_keys.parms_id()))) print(" + galois_keys: {}".format(list2hex(galois_keys.parms_id()))) encryptor = Encryptor(context, public_key) evaluator = Evaluator(context) decryptor = Decryptor(context, secret_key) #In the BFV scheme plaintexts do not carry a parms_id, but ciphertexts do. Note #how the freshly encrypted ciphertext is at the highest data level. plain = Plaintext("1x^3 + 2x^2 + 3x^1 + 4") encrypted = Ciphertext() encryptor.encrypt(plain, encrypted) print(" + plain: {} (not set in BFV)".format( list2hex(plain.parms_id()))) print(" + encrypted: {}".format(list2hex(encrypted.parms_id()))) #`Modulus switching' is a technique of changing the ciphertext parameters down #in the chain. The function Evaluator::mod_switch_to_next always switches to #the next level down the chain, whereas Evaluator::mod_switch_to switches to #a parameter set down the chain corresponding to a given parms_id. However, it #is impossible to switch up in the chain. print("Perform modulus switching on encrypted and print.") context_data = context.first_context_data() print("---->") while (context_data.next_context_data()): print(" Level (chain index): {} ".format(context_data.chain_index())) print(" parms_id of encrypted: {}".format( list2hex(encrypted.parms_id()))) print(" Noise budget at this level: {} bits".format( decryptor.invariant_noise_budget(encrypted))) print("\\") print(" \\-->") evaluator.mod_switch_to_next_inplace(encrypted) context_data = context_data.next_context_data() print(" Level (chain index): {}".format(context_data.chain_index())) print(" parms_id of encrypted: {}".format(encrypted.parms_id())) print(" Noise budget at this level: {} bits".format( decryptor.invariant_noise_budget(encrypted))) print("\\") print(" \\-->") print(" End of chain reached") #At this point it is hard to see any benefit in doing this: we lost a huge #amount of noise budget (i.e., computational power) at each switch and seemed #to get nothing in return. Decryption still works. print("Decrypt still works after modulus switching.") decryptor.decrypt(encrypted, plain) print(" + Decryption of encrypted: {} ...... Correct.".format( plain.to_string())) #However, there is a hidden benefit: the size of the ciphertext depends #linearly on the number of primes in the coefficient modulus. Thus, if there #is no need or intention to perform any further computations on a given #ciphertext, we might as well switch it down to the smallest (last) set of #parameters in the chain before sending it back to the secret key holder for #decryption. # #Also the lost noise budget is actually not an issue at all, if we do things #right, as we will see below. # #First we recreate the original ciphertext and perform some computations. print("Computation is more efficient with modulus switching.") print("Compute the 8th power.") encryptor.encrypt(plain, encrypted) print(" + Noise budget fresh: {} bits".format( decryptor.invariant_noise_budget(encrypted))) evaluator.square_inplace(encrypted) evaluator.relinearize_inplace(encrypted, relin_keys) print(" + Noise budget of the 2nd power: {} bits".format( decryptor.invariant_noise_budget(encrypted))) evaluator.square_inplace(encrypted) evaluator.relinearize_inplace(encrypted, relin_keys) print(" + Noise budget of the 4th power: {} bits".format( decryptor.invariant_noise_budget(encrypted))) #Surprisingly, in this case modulus switching has no effect at all on the #noise budget. evaluator.mod_switch_to_next_inplace(encrypted) print(" + Noise budget after modulus switching: {} bits".format( decryptor.invariant_noise_budget(encrypted))) #This means that there is no harm at all in dropping some of the coefficient #modulus after doing enough computations. In some cases one might want to #switch to a lower level slightly earlier, actually sacrificing some of the #noise budget in the process, to gain computational performance from having #smaller parameters. We see from the print-out that the next modulus switch #should be done ideally when the noise budget is down to around 25 bits. evaluator.square_inplace(encrypted) evaluator.relinearize_inplace(encrypted, relin_keys) print(" + Noise budget of the 8th power: {} bits".format( decryptor.invariant_noise_budget(encrypted))) evaluator.mod_switch_to_next_inplace(encrypted) print(" + Noise budget after modulus switching: {} bits".format( decryptor.invariant_noise_budget(encrypted))) #At this point the ciphertext still decrypts correctly, has very small size, #and the computation was as efficient as possible. Note that the decryptor #can be used to decrypt a ciphertext at any level in the modulus switching #chain. decryptor.decrypt(encrypted, plain) print(" + Decryption of the 8th power (hexadecimal) ...... Correct.") print(" {}".format(plain.to_string())) #In BFV modulus switching is not necessary and in some cases the user might #not want to create the modulus switching chain, except for the highest two #levels. This can be done by passing a bool `false' to SEALContext::Create. context = SEALContext.Create(parms, False) #We can check that indeed the modulus switching chain has been created only #for the highest two levels (key level and highest data level). The following #loop should execute only once. print("Optionally disable modulus switching chain expansion.") print("Print the modulus switching chain.") print("---->") context_data = context.key_context_data() while (context_data): print(" Level (chain index): {}".format(context_data.chain_index())) print(" parms_id: {}".format(list2hex(context_data.parms_id()))) print("coeff_modulus primes:", end=' ') for prime in context_data.parms().coeff_modulus(): print(hex(prime.value()), end=' ') print("") print("\\") print(" \\-->") context_data = context_data.next_context_data() print(" End of chain reached")
def example_integer_encoder(): print("Example: Encoders / Integer Encoder") #[IntegerEncoder] (For BFV scheme only) # #The IntegerEncoder encodes integers to BFV plaintext polynomials as follows. #First, a binary expansion of the integer is computed. Next, a polynomial is #created with the bits as coefficients. For example, the integer # # 26 = 2^4 + 2^3 + 2^1 # #is encoded as the polynomial 1x^4 + 1x^3 + 1x^1. Conversely, plaintext #polynomials are decoded by evaluating them at x=2. For negative numbers the #IntegerEncoder simply stores all coefficients as either 0 or -1, where -1 is #represented by the unsigned integer plain_modulus - 1 in memory. # #Since encrypted computations operate on the polynomials rather than on the #encoded integers themselves, the polynomial coefficients will grow in the #course of such computations. For example, computing the sum of the encrypted #encoded integer 26 with itself will result in an encrypted polynomial with #larger coefficients: 2x^4 + 2x^3 + 2x^1. Squaring the encrypted encoded #integer 26 results also in increased coefficients due to cross-terms, namely, # # (2x^4 + 2x^3 + 2x^1)^2 = 1x^8 + 2x^7 + 1x^6 + 2x^5 + 2x^4 + 1x^2; # #further computations will quickly increase the coefficients much more. #Decoding will still work correctly in this case (evaluating the polynomial #at x=2), but since the coefficients of plaintext polynomials are really #integers modulo plain_modulus, implicit reduction modulo plain_modulus may #yield unexpected results. For example, adding 1x^4 + 1x^3 + 1x^1 to itself #plain_modulus many times will result in the constant polynomial 0, which is #clearly not equal to 26 * plain_modulus. It can be difficult to predict when #such overflow will take place especially when computing several sequential #multiplications. # #The IntegerEncoder is easy to understand and use for simple computations, #and can be a good tool to experiment with for users new to Microsoft SEAL. #However, advanced users will probably prefer more efficient approaches, #such as the BatchEncoder or the CKKSEncoder. parms = EncryptionParameters(scheme_type.BFV) poly_modulus_degree = 4096 parms.set_poly_modulus_degree(poly_modulus_degree) parms.set_coeff_modulus(CoeffModulus.BFVDefault(poly_modulus_degree)) #There is no hidden logic behind our choice of the plain_modulus. The only #thing that matters is that the plaintext polynomial coefficients will not #exceed this value at any point during our computation; otherwise the result #will be incorrect. parms.set_plain_modulus(512) context = SEALContext.Create(parms) print_parameters(context) keygen = KeyGenerator(context) public_key = keygen.public_key() secret_key = keygen.secret_key() encryptor = Encryptor(context, public_key) evaluator = Evaluator(context) decryptor = Decryptor(context, secret_key) #We create an IntegerEncoder. encoder = IntegerEncoder(context) #First, we encode two integers as plaintext polynomials. Note that encoding #is not encryption: at this point nothing is encrypted. value1 = 5 plain1 = encoder.encode(value1) print("Encode {} as polynomial {} (plain1), ".format( value1, plain1.to_string())) value2 = -7 plain2 = encoder.encode(value2) print(" encode {} as polynomial {} (plain2)".format( value2, plain2.to_string())) #Now we can encrypt the plaintext polynomials. encrypted1 = Ciphertext() encrypted2 = Ciphertext() print("Encrypt plain1 to encrypted1 and plain2 to encrypted2.") encryptor.encrypt(plain1, encrypted1) encryptor.encrypt(plain2, encrypted2) print(" + Noise budget in encrypted1: {} bits".format( decryptor.invariant_noise_budget(encrypted1))) print(" + Noise budget in encrypted2: {} bits".format( decryptor.invariant_noise_budget(encrypted2))) #As a simple example, we compute (-encrypted1 + encrypted2) * encrypted2. encryptor.encrypt(plain2, encrypted2) encrypted_result = Ciphertext() print( "Compute encrypted_result = (-encrypted1 + encrypted2) * encrypted2.") evaluator.negate(encrypted1, encrypted_result) evaluator.add_inplace(encrypted_result, encrypted2) evaluator.multiply_inplace(encrypted_result, encrypted2) print(" + Noise budget in encrypted_result: {} bits".format( decryptor.invariant_noise_budget(encrypted_result))) plain_result = Plaintext() print("Decrypt encrypted_result to plain_result.") decryptor.decrypt(encrypted_result, plain_result) #Print the result plaintext polynomial. The coefficients are not even close #to exceeding our plain_modulus, 512. print(" + Plaintext polynomial: {}".format(plain_result.to_string())) #Decode to obtain an integer result. print("Decode plain_result.") print(" + Decoded integer: {} ...... Correct.".format( encoder.decode_int32(plain_result)))
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)
def example_batch_encoder(): print("Example: Encoders / Batch Encoder") #[BatchEncoder] (For BFV scheme only) # #Let N denote the poly_modulus_degree and T denote the plain_modulus. Batching #allows the BFV plaintext polynomials to be viewed as 2-by-(N/2) matrices, with #each element an integer modulo T. In the matrix view, encrypted operations act #element-wise on encrypted matrices, allowing the user to obtain speeds-ups of #several orders of magnitude in fully vectorizable computations. Thus, in all #but the simplest computations, batching should be the preferred method to use #with BFV, and when used properly will result in implementations outperforming #anything done with the IntegerEncoder. parms = EncryptionParameters(scheme_type.BFV) poly_modulus_degree = 8192 parms.set_poly_modulus_degree(poly_modulus_degree) parms.set_coeff_modulus(CoeffModulus.BFVDefault(poly_modulus_degree)) #To enable batching, we need to set the plain_modulus to be a prime number #congruent to 1 modulo 2*poly_modulus_degree. Microsoft SEAL provides a helper #method for finding such a prime. In this example we create a 20-bit prime #that supports batching. parms.set_plain_modulus(PlainModulus.Batching(poly_modulus_degree, 20)) context = SEALContext.Create(parms) print_parameters(context) #We can verify that batching is indeed enabled by looking at the encryption #parameter qualifiers created by SEALContext. ##HERE qualifiers = context.qualifiers() print("Batching enabled: {}".format(qualifiers.using_batching)) 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) #Batching is done through an instance of the BatchEncoder class. batch_encoder = BatchEncoder(context) #The total number of batching `slots' equals the poly_modulus_degree, N, and #these slots are organized into 2-by-(N/2) matrices that can be encrypted and #computed on. Each slot contains an integer modulo plain_modulus. slot_count = batch_encoder.slot_count() row_size = int(slot_count / 2) print("Plaintext matrix row size: {}".format(row_size)) #The matrix plaintext is simply given to BatchEncoder as a flattened vector #of numbers. The first `row_size' many numbers form the first row, and the #rest form the second row. Here we create the following matrix: # # [ 0, 1, 2, 3, 0, 0, ..., 0 ] # [ 4, 5, 6, 7, 0, 0, ..., 0 ] pod_matrix = Int64Vector([0] * slot_count) pod_matrix[0] = 0 pod_matrix[1] = 1 pod_matrix[2] = 2 pod_matrix[3] = 3 pod_matrix[row_size] = 4 pod_matrix[row_size + 1] = 5 pod_matrix[row_size + 2] = 6 pod_matrix[row_size + 3] = 7 print("Input plaintext matrix:") print_matrix(pod_matrix, row_size) #First we use BatchEncoder to encode the matrix into a plaintext polynomial. plain_matrix = Plaintext() print("Encode plaintext matrix:") batch_encoder.encode(pod_matrix, plain_matrix) #We can instantly decode to verify correctness of the encoding. Note that no #encryption or decryption has yet taken place. print(" + Decode plaintext matrix ...... Correct.") pod_result = Int64Vector([0] * slot_count) batch_encoder.decode(plain_matrix, pod_result) print_matrix(pod_result, row_size) #Next we encrypt the encoded plaintext. encrypted_matrix = Ciphertext() print("Encrypt plain_matrix to encrypted_matrix.") encryptor.encrypt(plain_matrix, encrypted_matrix) print(" + Noise budget in encrypted_matrix: {} bits".format( decryptor.invariant_noise_budget(encrypted_matrix))) #Operating on the ciphertext results in homomorphic operations being performed #simultaneously in all 8192 slots (matrix elements). To illustrate this, we #form another plaintext matrix # # [ 1, 2, 1, 2, 1, 2, ..., 2 ] # [ 1, 2, 1, 2, 1, 2, ..., 2 ] # #and encode it into a plaintext. pod_matrix2 = UInt64Vector([1, 2] * row_size) plain_matrix2 = Plaintext() batch_encoder.encode(pod_matrix2, plain_matrix2) print("Second input plaintext matrix:") print_matrix(pod_matrix2, row_size) #We now add the second (plaintext) matrix to the encrypted matrix, and square #the sum. print("Sum, square, and relinearize.") evaluator.add_plain_inplace(encrypted_matrix, plain_matrix2) evaluator.square_inplace(encrypted_matrix) evaluator.relinearize_inplace(encrypted_matrix, relin_keys) #How much noise budget do we have left? print(" + Noise budget in result: {} bits".format( decryptor.invariant_noise_budget(encrypted_matrix))) #We decrypt and decompose the plaintext to recover the result as a matrix. plain_result = Plaintext() print("Decrypt and decode result.") decryptor.decrypt(encrypted_matrix, plain_result) batch_encoder.decode(plain_result, pod_result) print(" + Result plaintext matrix ...... Correct.") print_matrix(pod_result, row_size)