forked from Lab41/PySEAL
-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.cpp
2026 lines (1765 loc) · 85.3 KB
/
main.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#include <iostream>
#include <iomanip>
#include <vector>
#include <string>
#include <chrono>
#include <random>
#include <thread>
#include <mutex>
#include <random>
#include <limits>
#include "seal/seal.h"
using namespace std;
using namespace seal;
/*
Helper function: Prints the name of the example in a fancy banner.
*/
void print_example_banner(string title)
{
if (!title.empty())
{
size_t title_length = title.length();
size_t banner_length = title_length + 2 + 2 * 10;
string banner_top(banner_length, '*');
string banner_middle = string(10, '*') + " " + title + " " + string(10, '*');
cout << endl
<< banner_top << endl
<< banner_middle << endl
<< banner_top << endl
<< endl;
}
}
/*
Helper function: Prints the parameters in a SEALContext.
*/
void print_parameters(const SEALContext &context)
{
cout << "/ Encryption parameters:" << endl;
cout << "| poly_modulus: " << context.poly_modulus().to_string() << endl;
/*
Print the size of the true (product) coefficient modulus
*/
cout << "| coeff_modulus size: "
<< context.total_coeff_modulus().significant_bit_count() << " bits" << endl;
cout << "| plain_modulus: " << context.plain_modulus().value() << endl;
cout << "\\ noise_standard_deviation: " << context.noise_standard_deviation() << endl;
cout << endl;
}
/*
Introduces basic concepts in SEAL and shows how to perform simple
arithmetic operations on encrypted data.
*/
void example_basics_i();
/*
Introduces relinearization and evaluation keys, demonstrates why they
are needed, and how to use them.
*/
void example_basics_ii();
/*
Shows how to compute a simple weighted average of encrypted rational
numbers using the FractionalEncoder.
*/
void example_weighted_average();
/*
Introduces the important concept of batching, which allows a plaintext
to be viewed as a matrix of integer data types. The example also
demonstrates SIMD operations on the matrix elements, as well as matrix
row and column rotations using Galois keys.
*/
void example_batching();
/*
Demonstrates the automatic parameter selection tools in SEAL.
*/
void example_parameter_selection();
/*
A single-threaded performance test benchmarking basic operations.
*/
void example_performance_st();
/*
In this example we run the performance benchmark on several threads
concurrently. We also explain the correct use of memory pools for
better performance in multi-threaded applications.
*/
void example_performance_mt(int th_count);
int main()
{
cout << "SEAL version: " << SEAL_VERSION_STRING << endl;
while (true)
{
cout << "\nSEAL Examples:" << endl << endl;
cout << " 1. Basics I" << endl;
cout << " 2. Basics II" << endl;
cout << " 3. Weighted Average" << endl;
cout << " 4. Batching with PolyCRTBuilder" << endl;
cout << " 5. Automatic Parameter Selection" << endl;
cout << " 6. Single-Threaded Performance Test" << endl;
cout << " 7. Multi-Threaded Performance Test" << endl;
cout << " 0. Exit" << endl;
/*
Print how much memory we have allocated in global memory pool.
*/
cout << "\nTotal memory allocated by global memory pool: "
<< (MemoryPoolHandle::Global().alloc_byte_count() >> 20) << " MB" << endl;
int selection = 0;
cout << endl << "Run example: ";
if (!(cin >> selection))
{
cout << "Invalid option." << endl;
cin.clear();
cin.ignore(numeric_limits<streamsize>::max(), '\n');
continue;
}
switch (selection)
{
case 1:
example_basics_i();
break;
case 2:
example_basics_ii();
break;
case 3:
example_weighted_average();
break;
case 4:
example_batching();
break;
case 5:
example_parameter_selection();
break;
case 6:
example_performance_st();
break;
case 7: {
int th_count;
cout << "Thread count: ";
if (!(cin >> th_count) || (th_count < 1))
{
cout << "Invalid option." << endl;
break;
}
example_performance_mt(th_count);
break;
}
case 0:
return 0;
default:
cout << "Invalid option."<< endl;
break;
}
}
return 0;
}
void example_basics_i()
{
print_example_banner("Example: Basics I");
/*
In this example we demonstrate setting up encryption parameters and other
relevant objects for performing simple computations on encrypted integers.
SEAL uses the Fan-Vercauteren (FV) homomorphic encryption scheme. We refer to
https://eprint.iacr.org/2012/144 for full details on how the FV scheme works.
For better performance, SEAL implements the "FullRNS" optimization of FV, as
described in https://eprint.iacr.org/2016/510.
*/
/*
The first task is to set up an instance of the EncryptionParameters class.
It is critical to understand how these 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 (polynomial modulus);
- coeff_modulus ([ciphertext] coefficient modulus);
- plain_modulus (plaintext modulus).
A fourth parameter -- noise_standard_deviation -- has a default value of 3.19
and should not be necessary to modify unless the user has a specific reason
to and knows what they are doing.
The encryption scheme implemented in SEAL 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 of 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 SEAL
the two basic homomorphic operations 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 is compounding in sequential multiplications, the most significant
factor in choosing appropriate encryption parameters is the multiplicative
depth of the arithmetic circuit that needs to be evaluated. Once the noise
budget in 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.
*/
EncryptionParameters parms;
/*
We first set the polynomial modulus. This must be a power-of-2 cyclotomic
polynomial, i.e. a polynomial of the form "1x^(power-of-2) + 1". The polynomial
modulus should be thought of mainly affecting the security level of the scheme;
larger polynomial modulus makes the scheme more secure. At the same time, it
makes ciphertext sizes larger, and consequently all operations slower.
Recommended degrees for poly_modulus are 1024, 2048, 4096, 8192, 16384, 32768,
but it is also possible to go beyond this. Since we perform only a very small
computation in this example, it suffices to use a very small polynomial modulus.
*/
parms.set_poly_modulus("1x^2048 + 1");
/*
Next we choose the [ciphertext] coefficient modulus (coeff_modulus). The size
of the coefficient modulus should be thought of as the most significant factor
in determining the noise budget in a freshly encrypted ciphertext: bigger means
more noise budget. Unfortunately, a larger coefficient modulus also lowers the
security level of the scheme. Thus, if a large noise budget is required for
complicated computations, a large coefficient modulus needs to be used, and the
reduction in the security level must be countered by simultaneously increasing
the polynomial modulus.
To make parameter selection easier for the user, we have constructed sets of
largest allowed coefficient moduli for 128-bit and 192-bit security levels
for different choices of the polynomial modulus. These recommended parameters
follow the Security white paper at http://HomomorphicEncryption.org. However,
due to the complexity of this topic, we highly recommend the user to directly
consult an expert in homomorphic encryption and RLWE-based encryption schemes
to determine the security of their parameter choices.
Our recommended values for the coefficient modulus can be easily accessed
through the functions
coeff_modulus_128bit(int)
coeff_modulus_192bit(int)
for 128-bit and 192-bit security levels. The integer parameter is the degree
of the polynomial modulus.
In SEAL the coefficient modulus is a positive composite number -- a product
of distinct primes of size up to 60 bits. When we talk about the size of the
coefficient modulus we mean the bit length of the product of the small primes.
The small primes are represented by instances of the SmallModulus class; for
example coeff_modulus_128bit(int) returns a vector of SmallModulus instances.
It is possible for the user to select their own small primes. Since SEAL uses
the Number Theoretic Transform (NTT) for polynomial multiplications modulo the
factors of the coefficient modulus, the factors need to be prime numbers
congruent to 1 modulo 2*degree(poly_modulus). We have generated a list of such
prime numbers of various sizes, that the user can easily access through the
functions
small_mods_60bit(int)
small_mods_50bit(int)
small_mods_40bit(int)
small_mods_30bit(int)
each of which gives access to an array of primes of the denoted size. These
primes are located in the source file util/globals.cpp.
Performance is mainly affected by the size of the polynomial modulus, and the
number of prime factors in the coefficient modulus. Thus, it is important to
use as few factors in the coefficient modulus as possible.
In this example we use the default coefficient modulus for a 128-bit security
level. Concretely, this coefficient modulus consists of only one 56-bit prime
factor: 0xfffffffff00001.
*/
parms.set_coeff_modulus(coeff_modulus_128(2048));
/*
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 example_batching(). The plaintext
modulus determines the size of the plaintext data type, but it also affects
the noise budget in a freshly encrypted ciphertext, and the consumption of
the noise budget in homomorphic multiplication. Thus, it is essential to try
to keep the plaintext data type as small as possible for good 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).
*/
parms.set_plain_modulus(1 << 8);
/*
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, and performs and stores several important
pre-computations.
*/
SEALContext context(parms);
/*
Print the parameters that we have chosen.
*/
print_parameters(context);
/*
Plaintexts in the FV scheme are polynomials with coefficients integers modulo
plain_modulus. To encrypt for example integers instead, one can use an
`encoding scheme' to represent the integers as such polynomials. SEAL comes
with a few basic encoders:
[IntegerEncoder]
Given an integer base b, encodes integers as plaintext polynomials as follows.
First, a base-b expansion of the integer is computed. This expansion uses
a `balanced' set of representatives of integers modulo b as the coefficients.
Namely, when b is odd the coefficients are integers between -(b-1)/2 and
(b-1)/2. When b is even, the integers are between -b/2 and (b-1)/2, except
when b is two and the usual binary expansion is used (coefficients 0 and 1).
Decoding amounts to evaluating the polynomial at x=b. For example, if b=2,
the integer
26 = 2^4 + 2^3 + 2^1
is encoded as the polynomial 1x^4 + 1x^3 + 1x^1. When b=3,
26 = 3^3 - 3^0
is encoded as the polynomial 1x^3 - 1. In memory polynomial coefficients are
always stored as unsigned integers by storing their smallest non-negative
representatives modulo plain_modulus. To create a base-b integer encoder,
use the constructor IntegerEncoder(plain_modulus, b). If no b is given, b=2
is used.
[FractionalEncoder]
The FractionalEncoder encodes fixed-precision rational numbers as follows.
It expands the number in a given base b, possibly truncating an infinite
fractional part to finite precision, e.g.
26.75 = 2^4 + 2^3 + 2^1 + 2^(-1) + 2^(-2)
when b=2. For the sake of the example, suppose poly_modulus is 1x^1024 + 1.
It then represents the integer part of the number in the same way as in
IntegerEncoder (with b=2 here), and moves the fractional part instead to the
highest degree part of the polynomial, but with signs of the coefficients
changed. In this example we would represent 26.75 as the polynomial
-1x^1023 - 1x^1022 + 1x^4 + 1x^3 + 1x^1.
In memory the negative coefficients of the polynomial will be represented as
their negatives modulo plain_modulus.
[PolyCRTBuilder]
If plain_modulus is a prime congruent to 1 modulo 2*degree(poly_modulus), the
plaintext elements can be viewed as 2-by-(degree(poly_modulus) / 2) matrices
with elements integers modulo plain_modulus. When a desired computation can be
vectorized, using PolyCRTBuilder can result in massive performance improvements
over naively encrypting and operating on each input number separately. Thus,
in more complicated computations this is likely to be by far the most important
and useful encoder. In example_batching() we show how to use and operate on
encrypted matrix plaintexts.
For performance reasons, in homomorphic encryption one typically wants to keep
the plaintext data types as small as possible, which can make it challenging to
prevent data type overflow in more complicated computations, especially when
operating on rational numbers that have been scaled to integers. When using
PolyCRTBuilder estimating whether an overflow occurs is a fairly standard task,
as the matrix slots are integers modulo plain_modulus, and each slot is operated
on independently of the others. When using IntegerEncoder or FractionalEncoder
it is substantially more difficult to estimate when an overflow occurs in the
plaintext, and choosing the plaintext modulus very carefully to be large enough
is critical to avoid unexpected results. Specifically, one needs to estimate how
large the largest coefficient in the polynomial view of all of the plaintext
elements becomes, and choose the plaintext modulus to be larger than this value.
SEAL comes with an automatic parameter selection tool that can help with this
task, as is demonstrated in example_parameter_selection().
Here we choose to create an IntegerEncoder with base b=2.
*/
IntegerEncoder encoder(context.plain_modulus());
/*
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 then be read to local variables.
To create a fresh pair of keys one can call KeyGenerator::generate() at any time.
*/
KeyGenerator keygen(context);
PublicKey public_key = keygen.public_key();
SecretKey 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.
*/
Encryptor encryptor(context, public_key);
/*
Computations on the ciphertexts are performed with the Evaluator class.
*/
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);
/*
We start by encoding two integers as plaintext polynomials.
*/
int value1 = 5;
Plaintext plain1 = encoder.encode(value1);
cout << "Encoded " << value1 << " as polynomial " << plain1.to_string() << " (plain1)" << endl;
int value2 = -7;
Plaintext plain2 = encoder.encode(value2);
cout << "Encoded " << value2 << " as polynomial " << plain2.to_string() << " (plain2)" << endl;
/*
Encrypting the values is easy.
*/
Ciphertext encrypted1, encrypted2;
cout << "Encrypting plain1: ";
encryptor.encrypt(plain1, encrypted1);
cout << "Done (encrypted1)" << endl;
cout << "Encrypting plain2: ";
encryptor.encrypt(plain2, encrypted2);
cout << "Done (encrypted2)" << endl;
/*
To illustrate the concept of noise budget, we print the budgets in the fresh
encryptions.
*/
cout << "Noise budget in encrypted1: "
<< decryptor.invariant_noise_budget(encrypted1) << " bits" << endl;
cout << "Noise budget in encrypted2: "
<< decryptor.invariant_noise_budget(encrypted2) << " bits" << endl;
/*
As a simple example, we compute (-encrypted1 + encrypted2) * encrypted2.
*/
/*
Negation is a unary operation.
*/
evaluator.negate(encrypted1);
/*
Negation does not consume any noise budget.
*/
cout << "Noise budget in -encrypted1: "
<< decryptor.invariant_noise_budget(encrypted1) << " bits" << endl;
/*
Addition can be done in-place (overwriting the first argument with the result,
or alternatively a three-argument overload with a separate destination variable
can be used. The in-place variants are always more efficient. Here we overwrite
encrypted1 with the sum.
*/
evaluator.add(encrypted1, encrypted2);
/*
It is instructive to think that addition sets the noise budget to the minimum
of the input noise budgets. In this case both inputs had roughly the same
budget going on, and the output (in encrypted1) has just slightly lower budget.
Depending on probabilistic effects, the noise growth consumption may or may
not be visible when measured in whole bits.
*/
cout << "Noise budget in -encrypted1 + encrypted2: "
<< decryptor.invariant_noise_budget(encrypted1) << " bits" << endl;
/*
Finally multiply with encrypted2. Again, we use the in-place version of the
function, overwriting encrypted1 with the product.
*/
evaluator.multiply(encrypted1, encrypted2);
/*
Multiplication consumes a lot of noise budget. This is clearly seen in the
print-out. The user can change the plain_modulus to see its effect on the
rate of noise budget consumption.
*/
cout << "Noise budget in (-encrypted1 + encrypted2) * encrypted2: "
<< decryptor.invariant_noise_budget(encrypted1) << " bits" << endl;
/*
Now we decrypt and decode our result.
*/
Plaintext plain_result;
cout << "Decrypting result: ";
decryptor.decrypt(encrypted1, plain_result);
cout << "Done" << endl;
/*
Print the result plaintext polynomial.
*/
cout << "Plaintext polynomial: " << plain_result.to_string() << endl;
/*
Decode to obtain an integer result.
*/
cout << "Decoded integer: " << encoder.decode_int32(plain_result) << endl;
}
void 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.
*/
EncryptionParameters parms;
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(coeff_modulus_128(8192));
parms.set_plain_modulus(1 << 10);
SEALContext context(parms);
print_parameters(context);
KeyGenerator keygen(context);
auto public_key = keygen.public_key();
auto 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.
*/
Plaintext plain1("1x^2 + 2x^1 + 3");
Ciphertext encrypted;
cout << "Encrypting " << plain1.to_string() << ": ";
encryptor.encrypt(plain1, encrypted);
cout << "Done" << endl;
cout << "Size of a fresh encryption: " << encrypted.size() << endl;
cout << "Noise budget in fresh encryption: "
<< decryptor.invariant_noise_budget(encrypted) << " bits" << endl;
/*
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);
cout << "Size after squaring: " << encrypted.size() << endl;
cout << "Noise budget after squaring: "
<< decryptor.invariant_noise_budget(encrypted) << " bits" << endl;
evaluator.square(encrypted);
cout << "Size after second squaring: " << encrypted.size() << endl;
cout << "Noise budget after second squaring: "
<< decryptor.invariant_noise_budget(encrypted) << " bits" << endl;
/*
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.
*/
Plaintext plain2;
decryptor.decrypt(encrypted, plain2);
cout << "Fourth power: " << plain2.to_string() << endl;
cout << endl;
/*
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.
*/
EvaluationKeys ev_keys16;
/*
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);
cout << "Encrypting " << plain1.to_string() << ": ";
encryptor.encrypt(plain1, encrypted);
cout << "Done" << endl;
cout << "Size of a fresh encryption: " << encrypted.size() << endl;
cout << "Noise budget in fresh encryption: "
<< decryptor.invariant_noise_budget(encrypted) << " bits" << endl;
evaluator.square(encrypted);
cout << "Size after squaring: " << encrypted.size() << endl;
cout << "Noise budget after squaring: "
<< decryptor.invariant_noise_budget(encrypted) << " bits" << endl;
evaluator.relinearize(encrypted, ev_keys16);
cout << "Size after relinearization: " << encrypted.size() << endl;
cout << "Noise budget after relinearizing (dbc = "
<< ev_keys16.decomposition_bit_count() << "): "
<< decryptor.invariant_noise_budget(encrypted) << " bits" << endl;
evaluator.square(encrypted);
cout << "Size after second squaring: " << encrypted.size() << endl;
cout << "Noise budget after second squaring: "
<< decryptor.invariant_noise_budget(encrypted) << " bits" << endl;
evaluator.relinearize(encrypted, ev_keys16);
cout << "Size after relinearization: " << encrypted.size() << endl;
cout << "Noise budget after relinearizing (dbc = "
<< ev_keys16.decomposition_bit_count() << "): "
<< decryptor.invariant_noise_budget(encrypted) << " bits" << endl;
decryptor.decrypt(encrypted, plain2);
cout << "Fourth power: " << plain2.to_string() << endl;
cout << endl;
/*
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.
*/
EvaluationKeys ev_keys60;
keygen.generate_evaluation_keys(dbc_max(), ev_keys60);
cout << "Encrypting " << plain1.to_string() << ": ";
encryptor.encrypt(plain1, encrypted);
cout << "Done" << endl;
cout << "Size of a fresh encryption: " << encrypted.size() << endl;
cout << "Noise budget in fresh encryption: "
<< decryptor.invariant_noise_budget(encrypted) << " bits" << endl;
evaluator.square(encrypted);
cout << "Size after squaring: " << encrypted.size() << endl;
cout << "Noise budget after squaring: "
<< decryptor.invariant_noise_budget(encrypted) << " bits" << endl;
evaluator.relinearize(encrypted, ev_keys60);
cout << "Size after relinearization: " << encrypted.size() << endl;
cout << "Noise budget after relinearizing (dbc = "
<< ev_keys60.decomposition_bit_count() << "): "
<< decryptor.invariant_noise_budget(encrypted) << " bits" << endl;
evaluator.square(encrypted);
cout << "Size after second squaring: " << encrypted.size() << endl;
cout << "Noise budget after second squaring: "
<< decryptor.invariant_noise_budget(encrypted) << " bits" << endl;
evaluator.relinearize(encrypted, ev_keys60);
cout << "Size after relinearization: " << encrypted.size() << endl;
cout << "Noise budget after relinearizing (dbc = "
<< ev_keys60.decomposition_bit_count() << "): "
<< decryptor.invariant_noise_budget(encrypted) << " bits" << endl;
decryptor.decrypt(encrypted, plain2);
cout << "Fourth power: " << plain2.to_string() << endl;
cout << endl;
/*
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);
cout << "Size after third squaring: " << encrypted.size() << endl;
cout << "Noise budget after third squaring: "
<< decryptor.invariant_noise_budget(encrypted) << " bits" << endl;
evaluator.relinearize(encrypted, ev_keys60);
cout << "Size after relinearization: " << encrypted.size() << endl;
cout << "Noise budget after relinearizing (dbc = "
<< ev_keys60.decomposition_bit_count() << "): "
<< decryptor.invariant_noise_budget(encrypted) << " bits" << endl;
decryptor.decrypt(encrypted, plain2);
cout << "Eighth power: " << plain2.to_string() << endl;
/*
Observe from the print-out that the polynomial coefficients are no longer
correct as integers: they have been reduced modulo plain_modulus, and there
was no warning sign about this. It might be necessary to carefully analyze
the computation to make sure such overflow does not occur unexpectedly.
These experiments suggest that an optimal strategy might be to relinearize
first with evaluation keys with a small decomposition bit count, and later
with evaluation keys with a larger decomposition bit count (for performance)
when noise budget has already been consumed past the bound determined by the
larger decomposition bit count. For example, above the best strategy might
have been to use ev_keys16 in the first relinearization, and ev_keys60 in the
next two relinearizations for optimal noise budget consumption/performance
trade-off. Luckily, in most use-cases it is not so critical to squeeze out
every last bit of performance, especially when slightly larger parameters
are used.
*/
}
void example_weighted_average()
{
print_example_banner("Example: Weighted Average");
/*
In this example we demonstrate the FractionalEncoder, and use it to compute
a weighted average of 10 encrypted rational numbers. In this computation we
perform homomorphic multiplications of ciphertexts by plaintexts, which is
much faster than regular multiplications of ciphertexts by ciphertexts.
Moreover, such `plain multiplications' never increase the ciphertext size,
which is why we have no need for evaluation keys in this example.
We start by creating encryption parameters, setting up the SEALContext, keys,
and other relevant objects. Since our computation has multiplicative depth of
only two, it suffices to use a small poly_modulus.
*/
EncryptionParameters parms;
parms.set_poly_modulus("1x^2048 + 1");
parms.set_coeff_modulus(coeff_modulus_128(2048));
parms.set_plain_modulus(1 << 8);
SEALContext context(parms);
print_parameters(context);
KeyGenerator keygen(context);
auto public_key = keygen.public_key();
auto secret_key = keygen.secret_key();
/*
We also set up an Encryptor, Evaluator, and Decryptor here.
*/
Encryptor encryptor(context, public_key);
Evaluator evaluator(context);
Decryptor decryptor(context, secret_key);
/*
Create a vector of 10 rational numbers (as doubles).
*/
const vector<double> rational_numbers {
3.1, 4.159, 2.65, 3.5897, 9.3, 2.3, 8.46, 2.64, 3.383, 2.795
};
/*
Create a vector of weights.
*/
const vector<double> coefficients {
0.1, 0.05, 0.05, 0.2, 0.05, 0.3, 0.1, 0.025, 0.075, 0.05
};
/*
We need a FractionalEncoder to encode the rational numbers into plaintext
polynomials. In this case we decide to reserve 64 coefficients of the
polynomial for the integral part (low-degree terms) and expand the fractional
part to 32 digits of precision (in base 3) (high-degree terms). These numbers
can be changed according to the precision that is needed; note that these
choices leave a lot of unused space in the 2048-coefficient polynomials.
*/
FractionalEncoder encoder(context.plain_modulus(), context.poly_modulus(), 64, 32, 3);
/*
We create a vector of ciphertexts for encrypting the rational numbers.
*/
vector<Ciphertext> encrypted_rationals;
cout << "Encoding and encrypting: ";
for (int i = 0; i < 10; i++)
{
/*
We create our Ciphertext objects into the vector by passing the
encryption parameters as an argument to the constructor. This ensures
that enough memory is allocated for a size 2 ciphertext. In this example
our ciphertexts never grow in size (plain multiplication does not cause
ciphertext growth), so we can expect the ciphertexts to remain in the same
location in memory throughout the computation. In more complicated examples
one might want to call a constructor that reserves enough memory for the
ciphertext to grow to a specified size to avoid costly memory moves when
multiplications and relinearizations are performed.
*/
encrypted_rationals.emplace_back(parms);
encryptor.encrypt(encoder.encode(rational_numbers[i]), encrypted_rationals[i]);
cout << to_string(rational_numbers[i]).substr(0,6) << ((i < 9) ? ", " : "\n");
}
/*
Next we encode the coefficients. There is no reason to encrypt these since they
are not private data.
*/
vector<Plaintext> encoded_coefficients;
cout << "Encoding plaintext coefficients: ";
for (int i = 0; i < 10; i++)
{
encoded_coefficients.emplace_back(encoder.encode(coefficients[i]));
cout << to_string(coefficients[i]).substr(0,6) << ((i < 9) ? ", " : "\n");
}
/*
We also need to encode 0.1. Multiplication by this plaintext will have the
effect of dividing by 10. Note that in SEAL it is impossible to divide
ciphertext by another ciphertext, but in this way division by a plaintext is
possible.
*/
Plaintext div_by_ten = encoder.encode(0.1);
/*
Now compute each multiplication.
*/
cout << "Computing products: ";
for (int i = 0; i < 10; i++)
{
/*
Note how we use plain multiplication instead of usual multiplication. The
result overwrites the first argument in the function call.
*/
evaluator.multiply_plain(encrypted_rationals[i], encoded_coefficients[i]);
}
cout << "Done" << endl;
/*
To obtain the linear sum we need to still compute the sum of the ciphertexts
in encrypted_rationals. There is an easy way to add together a vector of
Ciphertexts.
*/
Ciphertext encrypted_result;
cout << "Adding up all 10 ciphertexts: ";
evaluator.add_many(encrypted_rationals, encrypted_result);
cout << "Done" << endl;
/*
Perform division by 10 by plain multiplication with div_by_ten.
*/
cout << "Dividing by 10: ";
evaluator.multiply_plain(encrypted_result, div_by_ten);
cout << "Done" << endl;
/*
How much noise budget do we have left?
*/
cout << "Noise budget in result: "
<< decryptor.invariant_noise_budget(encrypted_result) << " bits" << endl;
/*
Decrypt, decode, and print result.
*/
Plaintext plain_result;
cout << "Decrypting result: ";
decryptor.decrypt(encrypted_result, plain_result);
cout << "Done" << endl;
double result = encoder.decode(plain_result);
cout << "Weighted average: " << result << endl;
}
void example_batching()
{
print_example_banner("Example: Batching with PolyCRTBuilder");
/*
In this fundamental example we discuss and demonstrate a powerful technique
called `batching'. If N denotes the degree of the polynomial modulus, and T
the plaintext modulus, then batching is automatically enabled in SEAL if
T is a prime and congruent to 1 modulo 2*N. In batching the plaintexts are
viewed as matrices of size 2-by-(N/2) with each element an integer modulo T.
Homomorphic operations act element-wise between encrypted matrices, allowing
the user to obtain speeds-ups of several orders of magnitude in naively
vectorizable computations. We demonstrate two more homomorphic operations
which act on encrypted matrices by rotating the rows cyclically, or rotate
the columns (i.e. swap the rows). These operations require the construction
of so-called `Galois keys', which are very similar to evaluation keys.
*/
EncryptionParameters parms;
parms.set_poly_modulus("1x^4096 + 1");
parms.set_coeff_modulus(coeff_modulus_128(4096));
/*
Note that 40961 is a prime number and 2*4096 divides 40960.
*/
parms.set_plain_modulus(40961);
SEALContext context(parms);
print_parameters(context);
/*
We can see that batching is indeed enabled by looking at the encryption
parameter qualifiers created by SEALContext.
*/
auto qualifiers = context.qualifiers();
cout << "Batching enabled: " << boolalpha << qualifiers.enable_batching << endl;
KeyGenerator keygen(context);
auto public_key = keygen.public_key();
auto secret_key = keygen.secret_key();
/*
We need to create Galois keys for performing matrix row and column rotations.
Like evaluation keys, the behavior of Galois keys depends on a decomposition
bit count. The noise budget consumption behavior of matrix row and column