def example_batching(): print_example_banner("Example: Batching with PolyCRTBuilder"); parms = EncryptionParameters() parms.set_poly_modulus("1x^4096 + 1") parms.set_coeff_modulus(seal.coeff_modulus_128(4096)) parms.set_plain_modulus(40961) context = SEALContext(parms) print_parameters(context) qualifiers = context.qualifiers() keygen = KeyGenerator(context) public_key = keygen.public_key() secret_key = keygen.secret_key() gal_keys = GaloisKeys() keygen.generate_galois_keys(30, gal_keys) #ev_keys = EvaluationKeys() #keygen.generate_evaluation_keys(30, ev_keys) encryptor = Encryptor(context, public_key) evaluator = Evaluator(context) decryptor = Decryptor(context, secret_key) crtbuilder = PolyCRTBuilder(context) slot_count = (int)(crtbuilder.slot_count()) row_size = (int)(slot_count / 2) print("Plaintext matrix row size: " + (str)(row_size)) def print_matrix(matrix): print("") print_size = 5 current_line = " [" for i in range(print_size): current_line += ((str)(matrix[i]) + ", ") current_line += ("..., ") for i in range(row_size - print_size, row_size): current_line += ((str)(matrix[i])) if i != row_size-1: current_line += ", " else: current_line += "]" print(current_line) current_line = " [" for i in range(row_size, row_size + print_size): current_line += ((str)(matrix[i]) + ", ") current_line += ("..., ") for i in range(2*row_size - print_size, 2*row_size): current_line += ((str)(matrix[i])) if i != 2*row_size-1: current_line += ", " else: current_line += "]" print(current_line) print("") # [ 0, 1, 2, 3, 0, 0, ..., 0 ] # [ 4, 5, 6, 7, 0, 0, ..., 0 ] pod_matrix = [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) plain_matrix = Plaintext() crtbuilder.compose(pod_matrix, plain_matrix) encrypted_matrix = Ciphertext() print("Encrypting: ") encryptor.encrypt(plain_matrix, encrypted_matrix) print("Done") print("Noise budget in fresh encryption: " + (str)(decryptor.invariant_noise_budget(encrypted_matrix)) + " bits") pod_matrix2 = [] for i in range(slot_count): pod_matrix2.append((i % 2) + 1) plain_matrix2 = Plaintext() crtbuilder.compose(pod_matrix2, plain_matrix2) print("Second input plaintext matrix:") print_matrix(pod_matrix2) print("Adding and squaring: ") evaluator.add_plain(encrypted_matrix, plain_matrix2) evaluator.square(encrypted_matrix) evaluator.relinearize(encrypted_matrix, ev_keys) print("Done") print("Noise budget in result: " + (str)(decryptor.invariant_noise_budget(encrypted_matrix)) + " bits") plain_result = Plaintext() print("Decrypting result: ") decryptor.decrypt(encrypted_matrix, plain_result) print("Done") crtbuilder.decompose(plain_result) pod_result = [plain_result.coeff_at(i) for i in range(plain_result.coeff_count())] print("Result plaintext matrix:") print_matrix(pod_result) encryptor.encrypt(plain_matrix, encrypted_matrix) print("Unrotated matrix: ") print_matrix(pod_matrix) print("Noise budget in fresh encryption: " + (str)(decryptor.invariant_noise_budget(encrypted_matrix)) + " bits") # Now rotate the rows to the left 3 steps, decrypt, decompose, and print. evaluator.rotate_rows(encrypted_matrix, 3, gal_keys) print("Rotated rows 3 steps left: ") decryptor.decrypt(encrypted_matrix, plain_result) crtbuilder.decompose(plain_result) pod_result = [plain_result.coeff_at(i) for i in range(plain_result.coeff_count())] print_matrix(pod_result) print("Noise budget after rotation" + (str)(decryptor.invariant_noise_budget(encrypted_matrix)) + " bits") # Rotate columns (swap rows), decrypt, decompose, and print. evaluator.rotate_columns(encrypted_matrix, gal_keys) print("Rotated columns: ") decryptor.decrypt(encrypted_matrix, plain_result) crtbuilder.decompose(plain_result) pod_result = [plain_result.coeff_at(i) for i in range(plain_result.coeff_count())] print_matrix(pod_result) print("Noise budget after rotation: " + (str)(decryptor.invariant_noise_budget(encrypted_matrix)) + " bits") # Rotate rows to the right 4 steps, decrypt, decompose, and print. evaluator.rotate_rows(encrypted_matrix, -4, gal_keys) print("Rotated rows 4 steps right: ") decryptor.decrypt(encrypted_matrix, plain_result) crtbuilder.decompose(plain_result) pod_result = [plain_result.coeff_at(i) for i in range(plain_result.coeff_count())] print_matrix(pod_result) print("Noise budget after rotation: " + (str)(decryptor.invariant_noise_budget(encrypted_matrix)) + " bits")
def example_basics_ii(): print_example_banner("Example: Basics II") # In this example we explain what relinearization is, how to use it, and how # it affects noise budget consumption. # First we set the parameters, create a SEALContext, and generate the public # and secret keys. We use slightly larger parameters than be fore to be able # to do more homomorphic multiplications. parms = EncryptionParameters() parms.set_poly_modulus("1x^8192 + 1") # The default coefficient modulus consists of the following primes: # 0x7fffffffba0001, # 0x7fffffffaa0001, # 0x7fffffff7e0001, # 0x3fffffffd60001. # The total size is 219 bits. parms.set_coeff_modulus(seal.coeff_modulus_128(8192)) parms.set_plain_modulus(1 << 10) context = SEALContext(parms) print_parameters(context) keygen = KeyGenerator(context) public_key = keygen.public_key() secret_key = keygen.secret_key() # We also set up an Encryptor, Evaluator, and Decryptor here. We will # encrypt polynomials directly in this example, so there is no need for # an encoder. encryptor = Encryptor(context, public_key) evaluator = Evaluator(context) decryptor = Decryptor(context, secret_key) # There are actually two more types of keys in SEAL: `evaluation keys' and # `Galois keys'. Here we will discuss evaluation keys, and Galois keys will # be discussed later in example_batching(). # In SEAL, a valid ciphertext consists of two or more polynomials with # coefficients integers modulo the product of the primes in coeff_modulus. # The current size of a ciphertext can be found using Ciphertext::size(). # A freshly encrypted ciphertext always has size 2. #plain1 = Plaintext("1x^2 + 2x^1 + 3") plain1 = Plaintext("1x^2 + 2x^1 + 3") encrypted = Ciphertext() print("") print("Encrypting " + plain1.to_string() + ": ") encryptor.encrypt(plain1, encrypted) print("Done") print("Size of a fresh encryption: " + (str)(encrypted.size())) print("Noise budget in fresh encryption: " + (str)(decryptor.invariant_noise_budget(encrypted)) + " bits") # Homomorphic 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 square encrypted twice to observe this growth (also observe noise # budget consumption). evaluator.square(encrypted) print("Size after squaring: " + (str)(encrypted.size())) print("Noise budget after squaring: " + (str)(decryptor.invariant_noise_budget(encrypted)) + " bits") plain2 = Plaintext() decryptor.decrypt(encrypted, plain2) print("Second power: " + plain2.to_string()) evaluator.square(encrypted) print("Size after squaring: " + (str)(encrypted.size())) print("Noise budget after squaring: " + (str)(decryptor.invariant_noise_budget(encrypted)) + " bits") # It does not matter that the size has grown -- decryption works as usual. # Observe from the print-out that the coefficients in the plaintext have # grown quite large. One more squaring would cause some of them to wrap # around plain_modulus (0x400), and as a result we would no longer obtain # the expected result as an integer-coefficient polynomial. We can fix this # problem to some extent by increasing plain_modulus. This would make sense, # since we still have plenty of noise budget left. plain2 = Plaintext() decryptor.decrypt(encrypted, plain2) print("Fourth power: " + plain2.to_string()) # The problem here is that homomorphic operations on large ciphertexts are # computationally much more costly than on small ciphertexts. Specifically, # homomorphic multiplication on input ciphertexts of size M and N will require # O(M*N) polynomial multiplications to be performed, and an addition will # require O(M+N) additions. Relinearization reduces the size of the ciphertexts # after multiplication back to the initial size (2). Thus, relinearizing one # or both inputs before the next multiplication, or e.g. before serializing the # ciphertexts, can have a huge positive impact on performance. # Another problem is that the noise budget consumption in multiplication is # bigger when the input ciphertexts sizes are bigger. In a complicated # computation the contribution of the sizes to the noise budget consumption # can actually become the dominant term. We will point this out again below # once we get to our example. # Relinearization itself has both a computational cost and a noise budget cost. # These both depend on a parameter called `decomposition bit count', which can # be any integer at least 1 [dbc_min()] and at most 60 [dbc_max()]. A large # decomposition bit count makes relinearization fast, but consumes more noise # budget. A small decomposition bit count can make relinearization slower, but # might not change the noise budget by any observable amount. # Relinearization requires a special type of key called `evaluation keys'. # These can be created by the KeyGenerator for any decomposition bit count. # To relinearize a ciphertext of size M >= 2 back to size 2, we actually need # M-2 evaluation keys. Attempting to relinearize a too large ciphertext with # too few evaluation keys will result in an exception being thrown. # We repeat our computation, but this time relinearize after both squarings. # Since our ciphertext never grows past size 3 (we relinearize after every # multiplication), it suffices to generate only one evaluation key. # First, we need to create evaluation keys. We use a decomposition bit count # of 16 here, which can be thought of as quite small. ev_keys16 = EvaluationKeys() # This function generates one single evaluation key. Another overload takes # the number of keys to be generated as an argument, but one is all we need # in this example (see above). keygen.generate_evaluation_keys(16, ev_keys16) print("") print("Encrypting " + plain1.to_string() + ": ") encryptor.encrypt(plain1, encrypted) print("Done") print("Size of a fresh encryption: " + (str)(encrypted.size())) print("Noise budget in fresh encryption: " + (str)(decryptor.invariant_noise_budget(encrypted)) + " bits") evaluator.square(encrypted) print("Size after squaring: " + (str)(encrypted.size())) print("Noise budget after squaring: " + (str)(decryptor.invariant_noise_budget(encrypted)) + " bits") evaluator.relinearize(encrypted, ev_keys16) print("Size after relinearization: " + (str)(encrypted.size())) print("Noise budget after relinearizing (dbs = " + (str)(ev_keys16.decomposition_bit_count()) + "): " + (str)(decryptor.invariant_noise_budget(encrypted)) + " bits") evaluator.square(encrypted) print("Size after second squaring: " + (str)(encrypted.size()) + " bits") print("Noise budget after second squaring: " + (str)(decryptor.invariant_noise_budget(encrypted)) + " bits") evaluator.relinearize(encrypted, ev_keys16) print("Size after relinearization: " + (str)(encrypted.size())) print("Noise budget after relinearizing (dbs = " + (str)(ev_keys16.decomposition_bit_count()) + "): " + (str)(decryptor.invariant_noise_budget(encrypted)) + " bits") decryptor.decrypt(encrypted, plain2) print("Fourth power: " + plain2.to_string()) # Of course the result is still the same, but this time we actually # used less of our noise budget. This is not surprising for two reasons: # - We used a very small decomposition bit count, which is why # relinearization itself did not consume the noise budget by any # observable amount; # - Since our ciphertext sizes remain small throughout the two # squarings, the noise budget consumption rate in multiplication # remains as small as possible. Recall from above that operations # on larger ciphertexts actually cause more noise growth. # To make matters even more clear, we repeat the computation a third time, # now using the largest possible decomposition bit count (60). We are not # measuring the time here, but relinearization with these evaluation keys # is significantly faster than with ev_keys16. ev_keys60 = EvaluationKeys() keygen.generate_evaluation_keys(seal.dbc_max(), ev_keys60) print("") print("Encrypting " + plain1.to_string() + ": ") encryptor.encrypt(plain1, encrypted) print("Done") print("Size of a fresh encryption: " + (str)(encrypted.size())) print("Noise budget in fresh encryption: " + (str)(decryptor.invariant_noise_budget(encrypted)) + " bits") evaluator.square(encrypted) print("Size after squaring: " + (str)(encrypted.size())) print("Noise budget after squaring: " + (str)(decryptor.invariant_noise_budget(encrypted)) + " bits") evaluator.relinearize(encrypted, ev_keys60) print("Size after relinearization: " + (str)(encrypted.size())) print("Noise budget after relinearizing (dbc = " + (str)(ev_keys60.decomposition_bit_count()) + "): " + (str)(decryptor.invariant_noise_budget(encrypted)) + " bits") evaluator.square(encrypted) print("Size after second squaring: " + (str)(encrypted.size())) print("Noise budget after second squaring: " + (str)(decryptor.invariant_noise_budget) + " bits") evaluator.relinearize(encrypted, ev_keys60) print("Size after relinearization: " + (str)(encrypted.size())) print("Noise budget after relinearizing (dbc = " + (str)(ev_keys60.decomposition_bit_count()) + "): " + (str)(decryptor.invariant_noise_budget(encrypted)) + " bits") decryptor.decrypt(encrypted, plain2) print("Fourth power: " + plain2.to_string()) # Observe from the print-out that we have now used significantly more of our # noise budget than in the two previous runs. This is again not surprising, # since the first relinearization chops off a huge part of the noise budget. # However, note that the second relinearization does not change the noise # budget by any observable amount. This is very important to understand when # optimal performance is desired: relinearization always drops the noise # budget from the maximum (freshly encrypted ciphertext) down to a fixed # amount depending on the encryption parameters and the decomposition bit # count. On the other hand, homomorphic multiplication always consumes the # noise budget from its current level. This is why the second relinearization # does not change the noise budget anymore: it is already consumed past the # fixed amount determinted by the decomposition bit count and the encryption # parameters. # We now perform a third squaring and observe an even further compounded # decrease in the noise budget. Again, relinearization does not consume the # noise budget at this point by any observable amount, even with the largest # possible decomposition bit count. evaluator.square(encrypted) print("Size after third squaring " + (str)(encrypted.size())) print("Noise budget after third squaring: " + (str)(decryptor.invariant_noise_budget(encrypted)) + " bits") evaluator.relinearize(encrypted, ev_keys60) print("Size after relinearization: " + (str)(encrypted.size())) print("Noise budget after relinearizing (dbc = " + (str)(ev_keys60.decomposition_bit_count()) + "): " + (str)(decryptor.invariant_noise_budget(encrypted)) + " bits") decryptor.decrypt(encrypted, plain2) print("Eighth power: " + plain2.to_string())