接下来介绍 BFV 和 CKKS 中的“级别”概念,以及在Microsoft SEAL中表示这些级别的相关对象。当然本篇涉及内容较为深入,不太容易看懂,大家按照自己需求参考即可。
一、概念引入
在Microsoft SEAL中,一组加密参数(不包括随机数生成器)通过参数的256位哈希值唯一标识。这个哈希值称为`parms_id`,可以随时轻松访问和打印。只要任何参数发生变化,哈希值也会随之改变。
当从给定的 Encryption Parameters 实例创建 SEALContext 时,Microsoft SEAL会自动创建所谓的“模数切换链”,这是从原始参数集派生的一系列其他加密参数。模数切换链中的参数与原始参数相同,唯一区别是系数模数的大小沿着链向下递减。
更确切地说,链中的每个参数集试图从前一个集的最后一个系数模数素数中移除;这种情况持续到参数集不再有效(例如,plain_modulus 大于剩余的 coeff_modulus)。可以轻松遍历链并访问所有参数集。
此外,链中的每个参数集都有一个“链索引”,指示其在链中的位置,因此最后一个集的索引为0。我们说一组加密参数或携带这些加密参数的对象在链中的级别高于另一组参数,如果其链索引更大,即它在链中更早。
链中的每组参数在创建 SEAL Context 时都会涉及唯一的预计算,并存储在SEALContext::ContextData 对象中。该链基本上是 SEALContext::ContextData 对象的链表,可以随时通过 SEALContext 轻松访问。每个节点可以通过其特定加密参数的 parms_id 来标识(poly_modulus_degree保持不变,但coeff_modulus变化)。
二、级别示例演示
EncryptionParameters parms(scheme_type::bfv);
size_t poly_modulus_degree = 8192;
parms.set_poly_modulus_degree(poly_modulus_degree);
parms.set_plain_modulus(PlainModulus::Batching(poly_modulus_degree, 20));
parms.set_coeff_modulus(CoeffModulus::Create(poly_modulus_degree, { 50, 30, 30, 50, 50 }));
SEALContext context(parms);
在这个例子中,我们使用自定义的coeff_modulus,由大小为 50、30、30、50 和 50位的5个素数组成。实际上,CoeffModulus::MaxBitCount(poly_modulus_degree) 返回 218 ( 大于50+30+30+50+50=210)。
由于模数切换链,5个素数的顺序是重要的。最后一个素数具有特殊意义,我们称之为“特殊素数”。因此,模数切换链中的第一个参数集是唯一涉及特殊素数的参数集。所有密钥对象,如SecretKey,都在这个最高级别创建。所有数据对象,如Ciphertext,只能在较低级别。特殊素数应该与 coeff_modulus 中其他最大素数一样大,尽管这不是严格要求。
这里对创建的上下文进行输出,plain_modulus 是随机生成的一个合理值:
库里提供了一些便捷的方法,直接能访问一些重要级别的SEALContext::ContextData:
- SEALContext::key_context_data(): 访问密钥级别的 ContextData
- SEALContext::first_context_data(): 访问最高数据级别的 ContextData
- SEALContext::last_context_data(): 访问最低级别的 ContextData
首先输出密钥级别的参数信息:
auto context_data = context.key_context_data();
cout << "----> Level (chain index): " << context_data->chain_index();
cout << " ...... key_context_data()" << endl;
cout << " parms_id: " << context_data->parms_id() << endl;
cout << " coeff_modulus primes: ";
cout << hex;
for (const auto &prime : context_data->parms().coeff_modulus())
{
cout << prime.value() << " ";
}
接下来遍历其余(数据)级别:
context_data = context.first_context_data();
while (context_data)
{
cout << " Level (chain index): " << context_data->chain_index();
if (context_data->parms_id() == context.first_parms_id())
{
cout << " ...... first_context_data()" << endl;
}
else if (context_data->parms_id() == context.last_parms_id())
{
cout << " ...... last_context_data()" << endl;
}
else
{
cout << endl;
}
cout << " parms_id: " << context_data->parms_id() << endl;
cout << " coeff_modulus primes: ";
cout << hex;
for (const auto &prime : context_data->parms().coeff_modulus())
{
cout << prime.value() << " ";
}
cout << dec << endl;
cout << "\\" << endl;
cout << " \\-->";
// 在链中向前一步。
context_data = context_data->next_context_data();
}
cout << " End of chain reached" << endl << endl;
创建一些密钥,并检查它们确实出现在最高级别:
KeyGenerator keygen(context);
auto secret_key = keygen.secret_key();
PublicKey public_key;
keygen.create_public_key(public_key);
RelinKeys relin_keys;
keygen.create_relin_keys(relin_keys);
cout << "Print the parameter IDs of generated elements." << endl;
cout << " + public_key: " << public_key.parms_id() << endl;
cout << " + secret_key: " << secret_key.parms_id() << endl;
cout << " + relin_keys: " << relin_keys.parms_id() << endl;
在BFV方案中,明文不携带 parms_id,但密文携带。注意新加密的密文在最高数据级别。
Encryptor encryptor(context, public_key);
Evaluator evaluator(context);
Decryptor decryptor(context, secret_key);
Plaintext plain("1x^3 + 2x^2 + 3x^1 + 4");
Ciphertext encrypted;
encryptor.encrypt(plain, encrypted);
cout << " + plain: " << plain.parms_id() << " (not set in BFV)" << endl;
cout << " + encrypted: " << encrypted.parms_id() << endl;
上面的plain中,传入的相当于 。输出展示如下:(明文没有)
三、模数切换解析
“模数切换”是一种在链中向下改变密文参数的技术。
- Evaluator::mod_switch_to_next 函数总是切换到链中的下一级;
- Evaluator::mod_switch_to 函数则切换到链中对应于给定parms_id的参数集。
然而,不可能在链中向上切换。
cout << "Perform modulus switching on encrypted and print." << endl;
context_data = context.first_context_data();
cout << "---->";
while (context_data->next_context_data())
{
cout << " Level (chain index): " << context_data->chain_index() << endl;
cout << " parms_id of encrypted: " << encrypted.parms_id() << endl;
cout << " Noise budget at this level: " << decryptor.invariant_noise_budget(encrypted) << " bits" << endl;
cout << "\\" << endl;
cout << " \\-->";
evaluator.mod_switch_to_next_inplace(encrypted);
context_data = context_data->next_context_data();
}
cout << " Level (chain index): " << context_data->chain_index() << endl;
cout << " parms_id of encrypted: " << encrypted.parms_id() << endl;
cout << " Noise budget at this level: " << decryptor.invariant_noise_budget(encrypted) << " bits" << endl;
cout << "\\" << endl;
cout << " \\-->";
cout << " End of chain reached" << endl << endl;
在每次切换时损失了大量的噪声预算(即计算能力),似乎没有直观得到任何回报。
不过先解密验证是否有效:
cout << "Decrypt still works after modulus switching." << endl;
decryptor.decrypt(encrypted, plain);
cout << " + Decryption of encrypted: " << plain.to_string();
cout << " ...... Correct." << endl;
模数切换带来的好处:密文的大小线性取决于系数模数中的素数数量。因此,如果不需要或打算对给定密文进行任何进一步计算,我们可以在将其发送回密钥持有者进行解密之前,将其切换到链中的最小(最后)参数集。
这里计算进行检验:
cout << "Computation is more efficient with modulus switching." << endl;
cout << "Compute the 8th power." << endl;
encryptor.encrypt(plain, encrypted);
cout << " + Noise budget fresh: " << decryptor.invariant_noise_budget(encrypted) << " bits" << endl;
evaluator.square_inplace(encrypted);
evaluator.relinearize_inplace(encrypted, relin_keys);
cout << " + Noise budget of the 2nd power: " << decryptor.invariant_noise_budget(encrypted) << " bits" << endl;
evaluator.square_inplace(encrypted);
evaluator.relinearize_inplace(encrypted, relin_keys);
cout << " + Noise budget of the 4th power: " << decryptor.invariant_noise_budget(encrypted) << " bits" << endl;
evaluator.mod_switch_to_next_inplace(encrypted);
cout << " + Noise budget after modulus switching: " << decryptor.invariant_noise_budget(encrypted) << " bits" << endl;
evaluator.square_inplace(encrypted);
evaluator.relinearize_inplace(encrypted, relin_keys);
cout << " + Noise budget of the 8th power: " << decryptor.invariant_noise_budget(encrypted) << " bits" << endl;
evaluator.mod_switch_to_next_inplace(encrypted);
cout << " + Noise budget after modulus switching: " << decryptor.invariant_noise_budget(encrypted) << " bits" << endl;
令人惊讶的是,在这种情况下,模数切换对噪声预算没有影响。这意味着在进行足够的计算后,删除一些系数模数没有任何坏处。
在某些情况下,可能希望稍早切换到较低级别,实际上牺牲一些噪声预算,以从较小的参数中获得计算性能。我们从打印输出中看到,下一次模数切换应该在噪声预算降至约25位时进行。
此时,密文仍能正确解密,但是密文的大小非常小,计算尽可能高效。请注意,解密器可以用来解密模数切换链中任何级别的密文。
decryptor.decrypt(encrypted, plain);
cout << " + Decryption of the 8th power (hexadecimal) ...... Correct." << endl;
cout << " " << plain.to_string() << endl ;
这里其实能看出来,该计算是在原先 多项式基础上 算的八次方,即为多项式运算,不是点积了,这与 Batch Encoder 编码后的运算不同。
四、不配置模数的切换
在BFV中,模数切换不是必须的,有时用户可能不想创建除最高两个级别之外的模数切换链。这可以通过向SEALContext构造函数传递一个bool `false`来实现。
context = SEALContext(parms, false);
这里可以输出检验一下,检查模数切换链确实只为最高两个级别(密钥级别和最高数据级别)创建。以下循环应仅执行一次:
cout << "Optionally disable modulus switching chain expansion." << endl;
cout << "Print the modulus switching chain." << endl;
cout << "---->";
for (context_data = context.key_context_data(); context_data; context_data = context_data->next_context_data())
{
cout << " Level (chain index): " << context_data->chain_index() << endl;
cout << " parms_id: " << context_data->parms_id() << endl;
cout << " coeff_modulus primes: ";
cout << hex;
for (const auto &prime : context_data->parms().coeff_modulus())
{
cout << prime.value() << " ";
}
cout << dec << endl;
cout << "\\" << endl;
cout << " \\-->";
}
cout << " End of chain reached" << endl << endl;
理解这个例子有助于理解后面的BGV方案,因为在BGV中 模数切换具有更基本的目的,