2021SC@SDUSC
2021-12-26
前言
前几篇我们讨论了SEAL对于BFV、CKKS的实现。本篇开始我将开始讨论SEAL中encoders.cpp代码实现。
背景
在“1 _bfv_basics。我们展示了如何使用BFV方案执行一个非常简单的计算。计算以明文模为参数,仅利用一个BFV明文多项式的系数。这种方法有两个值得注意的问题:
(1)实际应用中一般采用整数或实数算法,而不是模运算;
(2)我们只使用了明文多项式的一个系数。这是非常浪费的,因为明文多项式很大,而且在任何情况下都将全部加密。
对于(1),有人可能会问,为什么不直接增加plain_modules参数,直到没有溢出发生,并且计算的行为就像整数算术一样。问题是增加plain_module会增加噪声预算消耗,同时降低初始噪声预算。
在这些示例中,我们将讨论将数据放入明文元素(编码)的其他方法,这些方法允许进行更多的计算,而不会出现数据类型溢出,并且允许使用完整的明文多项式。
源码分析
void example_batch_encoder()
void example_batch_encoder()
{
print_example_banner("Example: Encoders / Batch Encoder");
设N为poly_modulus_degree,T为plain_modulus。批处理允许BFV明文多项式被视为2×(N/2)矩阵,每个元素都是一个模T整数。在矩阵视图中,加密操作在加密矩阵上逐个执行element-wise,允许用户在完全向量化计算中获得几个数量级的速度提升。因此,除了最简单的计算之外,批处理应该是首选的方法。使用BFV时,如果使用得当,实现的性能将超过使用IntegerEncoder完成的任何工作。
EncryptionParameters parms(scheme_type::BFV);
size_t poly_modulus_degree = 8192;
parms.set_poly_modulus_degree(poly_modulus_degree);
parms.set_coeff_modulus(CoeffModulus::BFVDefault(poly_modulus_degree));
为了启用批处理,我们需要将plain_modules设置为一个素数,它全等于1模2*poly_modulus_degree。(这里是指要求素数满足的条件)。Microsoft SEAL提供了一种辅助方法来查找这样的素数。在本例中,我们创建了一个支持批处理的20位素数。
parms.set_plain_modulus(PlainModulus::Batching(poly_modulus_degree, 20));
SEALContext context(parms);
print_parameters(context);
cout << endl;
我们可以通过查看由SEALContext创建的加密参数限定符来验证是否确实启用了批处理。
auto qualifiers = context.first_context_data()->qualifiers();
cout << "Batching enabled: " << boolalpha << qualifiers.using_batching << endl;
KeyGenerator keygen(context);
SecretKey secret_key = keygen.secret_key();
PublicKey public_key;
keygen.create_public_key(public_key);
RelinKeys relin_keys;
keygen.create_relin_keys(relin_keys);
Encryptor encryptor(context, public_key);
Evaluator evaluator(context);
Decryptor decryptor(context, secret_key);
通过上述BatchEncoder 实例的构造,构造批处理已经完成。
BatchEncoder batch_encoder(context);
批处理“槽slot”的总数等于poly_modulus_degree, N,这些槽被组织成2×(N/2)矩阵,可以对其进行加密和计算。每个槽都包含一个整数 modulo plain_modulus。
size_t slot_count = batch_encoder.slot_count();
size_t row_size = slot_count / 2;
cout << "Plaintext matrix row size: " << row_size << endl;
矩阵明文被简单地作为一个扁平的向量提供给BatchEncoder 的数字。第一 row_size 多的数字组成第一行,其余的组成第二行。这里我们创建以下矩阵:
[ 0, 1, 2, 3, 0, 0, ..., 0 ]
[ 4, 5, 6, 7, 0, 0, ..., 0 ]
vector<uint64_t> pod_matrix(slot_count, 0ULL);
pod_matrix[0] = 0ULL;
pod_matrix[1] = 1ULL;
pod_matrix[2] = 2ULL;
pod_matrix[3] = 3ULL;
pod_matrix[row_size] = 4ULL;
pod_matrix[row_size + 1] = 5ULL;
pod_matrix[row_size + 2] = 6ULL;
pod_matrix[row_size + 3] = 7ULL;
cout << "Input plaintext matrix:" << endl;
print_matrix(pod_matrix, row_size);
首先,我们使用BatchEncoder将矩阵编码为明文多项式。
Plaintext plain_matrix;
print_line(__LINE__);
cout << "Encode plaintext matrix:" << endl;
batch_encoder.encode(pod_matrix, plain_matrix);
我们可以立即解码,以验证编码的正确性。注意,还没有进行加密或解密。
vector<uint64_t> pod_result;
cout << " + Decode plaintext matrix ...... Correct." << endl;
batch_encoder.decode(plain_matrix, pod_result);
print_matrix(pod_result, row_size);
接下来我们加密编码后的明文。
Ciphertext encrypted_matrix;
print_line(__LINE__);
cout << "Encrypt plain_matrix to encrypted_matrix." << endl;
encryptor.encrypt(plain_matrix, encrypted_matrix);
cout << " + Noise budget in encrypted_matrix: " << decryptor.invariant_noise_budget(encrypted_matrix) << " bits"
<< endl;
对密文的操作导致在所有8192个槽(矩阵元素)中同时执行同态操作。为了说明这一点,我们构造了另一个明文矩阵:
[ 1, 2, 1, 2, 1, 2, ..., 2 ]
[ 1, 2, 1, 2, 1, 2, ..., 2 ]
并将其编码为明文。
vector<uint64_t> pod_matrix2;
for (size_t i = 0; i < slot_count; i++)
{
pod_matrix2.push_back((i % 2) + 1);
}
Plaintext plain_matrix2;
batch_encoder.encode(pod_matrix2, plain_matrix2);
cout << endl;
cout << "Second input plaintext matrix:" << endl;
print_matrix(pod_matrix2, row_size);
现在,我们将第二个(明文)矩阵添加到加密矩阵中,并将其平方
print_line(__LINE__);
cout << "Sum, square, and relinearize." << endl;
evaluator.add_plain_inplace(encrypted_matrix, plain_matrix2);
evaluator.square_inplace(encrypted_matrix);
evaluator.relinearize_inplace(encrypted_matrix, relin_keys);
输出:我们还剩下多少噪音预算?
cout << " + Noise budget in result: " << decryptor.invariant_noise_budget(encrypted_matrix) << " bits" << endl;
接下来,解密并分解明文以将结果恢复为矩阵。
Plaintext plain_result;
print_line(__LINE__);
cout << "Decrypt and decode result." << endl;
decryptor.decrypt(encrypted_matrix, plain_result);
batch_encoder.decode(plain_result, pod_result);
cout << " + Result plaintext matrix ...... Correct." << endl;
print_matrix(pod_result, row_size);
当所需的加密计算高度可并行化时,批处理允许我们有效地使用全明文多项式。但是,它并没有解决这个文件开头提到的另一个问题:每个槽只包含一个整数模的明文模量,除非明文模量非常大,否则我们可以很快遇到数据类型溢出,并在需要整数计算时会得到意外的结果。注意,溢出并不能以加密的形式检测到。CKKS方案(以及CKKSEncoder)解决了数据类型溢出问题,但代价是只产生近似的结果。
void example_ckks_encoder()
example_ckks_encoder演示了用于计算加密的实数或复数的Cheon-Kim-Kim-Song
(CKKS)方案。我们首先为CKKS方案创建加密参数。与BFV方案相比,有两个重要的区别:
(1) CKKS不使用明文模数加密参数;
(2)在使用CKKS方案时,以特定的方式选择coeff_modules是非常重要的。我们将在“ckks_basic.cpp”文件中进一步解释这一点。在这个例子中,我们使用CoeffModulus::Create来生成5个40位素数。
这是其有别于batch_encoder的地方,不过两者作用大同小异,限于时间,暂时不对其展开分析了,这篇分析报告到此告一段落。