// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System; using Microsoft.Research.SEAL; namespace SEALNetExamples { partial class Examples { private static void ExampleLevels() { Utilities.PrintExampleBanner("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 `ParmsId' 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., PlainModulus is larger than the remaining CoeffModulus). 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 ParmsId of its specific encryption parameters (PolyModulusDegree remains the same but CoeffModulus varies). */ using EncryptionParameters parms = new EncryptionParameters(SchemeType.BFV); ulong polyModulusDegree = 8192; parms.PolyModulusDegree = polyModulusDegree; /* In this example we use a custom CoeffModulus, 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.cs'. Indeed, CoeffModulus.MaxBitCount(polyModulusDegree) 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 modulus should be as large as the largest of the other primes in the CoeffModulus, although this is not a strict requirement. special prime +---------+ | v CoeffModulus: { 50, 30, 30, 50, 50 } +---+ Level 4 (all keys; `key level') | | CoeffModulus: { 50, 30, 30, 50 } +---+ Level 3 (highest `data level') | | CoeffModulus: { 50, 30, 30 } +---+ Level 2 | | CoeffModulus: { 50, 30 } +---+ Level 1 | | CoeffModulus: { 50 } +---+ Level 0 (lowest level) */ parms.CoeffModulus = CoeffModulus.Create( polyModulusDegree, new int[] { 50, 30, 30, 50, 50 }); /* In this example the PlainModulus does not play much of a role; we choose some reasonable value. */ parms.PlainModulus = PlainModulus.Batching(polyModulusDegree, 20); using SEALContext context = new SEALContext(parms); Utilities.PrintParameters(context); /* There are convenience method for accessing the SEALContext.ContextData for some of the most important levels: SEALContext.KeyContextData: access to key level ContextData SEALContext.FirstContextData: access to highest data level ContextData SEALContext.LastContextData: access to lowest level ContextData We iterate over the chain and print the ParmsId for each set of parameters. */ Console.WriteLine(); Utilities.PrintLine(); Console.WriteLine("Print the modulus switching chain."); /* First print the key level parameter information. */ SEALContext.ContextData contextData = context.KeyContextData; Console.WriteLine("----> Level (chain index): {0} ...... KeyContextData", contextData.ChainIndex); Console.WriteLine($" ParmsId: {contextData.ParmsId}"); Console.Write(" CoeffModulus primes: "); foreach (Modulus prime in contextData.Parms.CoeffModulus) { Console.Write($"{Utilities.ULongToString(prime.Value)} "); } Console.WriteLine(); Console.WriteLine("\\"); Console.Write(" \\--> "); /* Next iterate over the remaining (data) levels. */ contextData = context.FirstContextData; while (null != contextData) { Console.Write($"Level (chain index): {contextData.ChainIndex}"); if (contextData.ParmsId.Equals(context.FirstParmsId)) { Console.WriteLine(" ...... FirstContextData"); } else if (contextData.ParmsId.Equals(context.LastParmsId)) { Console.WriteLine(" ...... LastContextData"); } else { Console.WriteLine(); } Console.WriteLine($" ParmsId: {contextData.ParmsId}"); Console.Write(" CoeffModulus primes: "); foreach (Modulus prime in contextData.Parms.CoeffModulus) { Console.Write($"{Utilities.ULongToString(prime.Value)} "); } Console.WriteLine(); Console.WriteLine("\\"); Console.Write(" \\--> "); /* Step forward in the chain. */ contextData = contextData.NextContextData; } Console.WriteLine("End of chain reached"); Console.WriteLine(); /* We create some keys and check that indeed they appear at the highest level. */ using KeyGenerator keygen = new KeyGenerator(context); using SecretKey secretKey = keygen.SecretKey; keygen.CreatePublicKey(out PublicKey publicKey); keygen.CreateRelinKeys(out RelinKeys relinKeys); Utilities.PrintLine(); Console.WriteLine("Print the parameter IDs of generated elements."); Console.WriteLine($" + publicKey: {publicKey.ParmsId}"); Console.WriteLine($" + secretKey: {secretKey.ParmsId}"); Console.WriteLine($" + relinKeys: {relinKeys.ParmsId}"); using Encryptor encryptor = new Encryptor(context, publicKey); using Evaluator evaluator = new Evaluator(context); using Decryptor decryptor = new Decryptor(context, secretKey); /* In the BFV scheme plaintexts do not carry a ParmsId, but ciphertexts do. Note how the freshly encrypted ciphertext is at the highest data level. */ using Plaintext plain = new Plaintext("1x^3 + 2x^2 + 3x^1 + 4"); using Ciphertext encrypted = new Ciphertext(); encryptor.Encrypt(plain, encrypted); Console.WriteLine($" + plain: {plain.ParmsId} (not set in BFV)"); Console.WriteLine($" + encrypted: {encrypted.ParmsId}"); Console.WriteLine(); /* `Modulus switching' is a technique of changing the ciphertext parameters down in the chain. The function Evaluator.ModSwitchToNext always switches to the next level down the chain, whereas Evaluator.ModSwitchTo switches to a parameter set down the chain corresponding to a given ParmsId. However, it is impossible to switch up in the chain. */ Utilities.PrintLine(); Console.WriteLine("Perform modulus switching on encrypted and print."); contextData = context.FirstContextData; Console.Write("----> "); while (null != contextData.NextContextData) { Console.WriteLine($"Level (chain index): {contextData.ChainIndex}"); Console.WriteLine($" ParmsId of encrypted: {contextData.ParmsId}"); Console.WriteLine(" Noise budget at this level: {0} bits", decryptor.InvariantNoiseBudget(encrypted)); Console.WriteLine("\\"); Console.Write(" \\--> "); evaluator.ModSwitchToNextInplace(encrypted); contextData = contextData.NextContextData; } Console.WriteLine($"Level (chain index): {contextData.ChainIndex}"); Console.WriteLine($" ParmsId of encrypted: {contextData.ParmsId}"); Console.WriteLine(" Noise budget at this level: {0} bits", decryptor.InvariantNoiseBudget(encrypted)); Console.WriteLine("\\"); Console.Write(" \\--> "); Console.WriteLine("End of chain reached"); Console.WriteLine(); /* 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. */ Utilities.PrintLine(); Console.WriteLine("Decrypt still works after modulus switching."); decryptor.Decrypt(encrypted, plain); Console.WriteLine($" + Decryption of encrypted: {plain} ...... Correct."); Console.WriteLine(); /* 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. */ Console.WriteLine("Computation is more efficient with modulus switching."); Utilities.PrintLine(); Console.WriteLine("Compute the eight power."); encryptor.Encrypt(plain, encrypted); Console.WriteLine(" + Noise budget fresh: {0} bits", decryptor.InvariantNoiseBudget(encrypted)); evaluator.SquareInplace(encrypted); evaluator.RelinearizeInplace(encrypted, relinKeys); Console.WriteLine(" + Noise budget of the 2nd power: {0} bits", decryptor.InvariantNoiseBudget(encrypted)); evaluator.SquareInplace(encrypted); evaluator.RelinearizeInplace(encrypted, relinKeys); Console.WriteLine(" + Noise budget of the 4th power: {0} bits", decryptor.InvariantNoiseBudget(encrypted)); /* Surprisingly, in this case modulus switching has no effect at all on the noise budget. */ evaluator.ModSwitchToNextInplace(encrypted); Console.WriteLine(" + Noise budget after modulus switching: {0} bits", decryptor.InvariantNoiseBudget(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.SquareInplace(encrypted); evaluator.RelinearizeInplace(encrypted, relinKeys); Console.WriteLine(" + Noise budget of the 8th power: {0} bits", decryptor.InvariantNoiseBudget(encrypted)); evaluator.ModSwitchToNextInplace(encrypted); Console.WriteLine(" + Noise budget after modulus switching: {0} bits", decryptor.InvariantNoiseBudget(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); Console.WriteLine(" + Decryption of the 8th power (hexadecimal) ...... Correct."); Console.WriteLine($" {plain}"); Console.WriteLine(); /* 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 constructor. */ using SEALContext context2 = new SEALContext(parms, expandModChain: 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. */ Console.WriteLine("Optionally disable modulus switching chain expansion."); Utilities.PrintLine(); Console.WriteLine("Print the modulus switching chain."); Console.Write("----> "); for (contextData = context2.KeyContextData; null != contextData; contextData = contextData.NextContextData) { Console.WriteLine($"Level (chain index): {contextData.ChainIndex}"); Console.WriteLine($" ParmsId of encrypted: {contextData.ParmsId}"); Console.Write(" CoeffModulus primes: "); foreach (Modulus prime in contextData.Parms.CoeffModulus) { Console.Write($"{Utilities.ULongToString(prime.Value)} "); } Console.WriteLine(); Console.WriteLine("\\"); Console.Write(" \\--> "); } Console.WriteLine("End of chain reached"); Console.WriteLine(); /* It is very important to understand how this example works since in the BGV scheme modulus switching has a much more fundamental purpose and the next examples will be difficult to understand unless these basic properties are totally clear. */ } } }