Skip to main content

simple_bfv/
lib.rs

1#![forbid(unsafe_code)]
2mod scheme;
3mod config;
4mod plaintext;
5mod find_parameters;
6
7pub use crate::config::BFV as BFV;
8pub use crate::plaintext::BFVPlaintext;
9pub use crate::find_parameters::find_valid_q as find_valid_q;
10
11#[cfg(feature = "parallel")]
12use rayon::prelude::*;
13
14pub const VERSION: &str = env!("CARGO_PKG_VERSION");
15
16
17
18
19/*
20Tests for simple-bfv (yes I could have done a /tests)
21*/
22#[cfg(test)]
23use std::time::*;
24
25#[cfg(test)]
26//For separated key generation : a single function, so that it isn't to loud in the tests
27fn generate_keys(bfv: &BFV) -> (simple_ring::Polynomial, simple_ring::Polynomial, simple_ring::Polynomial) {
28    let s = bfv.generate_secret_key();
29    let a = bfv.generate_public_a();
30    let b = bfv.generate_public_b(&a, &s);
31    (a, b, s)
32}
33
34#[cfg(test)]
35//For asserting a plaintext and it's decoded one
36fn assert_plaintext_eq(got: &simple_ring::Polynomial, expected: &simple_ring::Polynomial, n_check: usize) {
37    for i in 0..n_check.min(got.coeffs.len()) {
38        assert_eq!(
39            got.coeffs[i], expected.coeffs[i],
40            "Coefficient mismatch at index {}: got {}, expected {}",
41            i, got.coeffs[i], expected.coeffs[i]
42        );
43    }
44}
45
46
47#[test]
48//Basic test for only encryption + decryption
49fn basic() {
50    let bfv = BFV::for_medium(); 
51    let (public_a, public_b, secret_s) = generate_keys(&bfv);
52
53    let message = BFVPlaintext::new(
54        "Hello, this is BFV scheme. This basic test is here to ensure that we can encrypt special characters, as #{[|@*$ù%§! and others, like µ~'</&œ. If you want to see it totally, use for_large or for_medium.",
55        &bfv
56    );
57
58    let start = Instant::now();
59
60    let ciphertext = bfv.encrypt(&message, &public_a, &public_b);
61    let decrypted = bfv.decrypt(&ciphertext, &secret_s);
62
63    println!("Recovered: {}", decrypted);
64    println!("Elapsed: {:?}", start.elapsed());
65
66    assert_eq!(
67        bfv.backend_decrypt(&ciphertext, &secret_s).coeffs,
68        message.plain.coeffs,
69        "Decryption mismatch"
70    );
71}
72
73
74//Homomorphic tests
75
76#[test]
77fn test_cipher_addition() {
78    let bfv = BFV::for_test();
79    let (pk_a, pk_b, sk) = generate_keys(&bfv);
80    let n = bfv.params.n;
81
82    let s1 = String::from_utf8(vec![126u8; n.min(50)]).unwrap();
83    let s2 = String::from_utf8(vec![126u8; n.min(50)]).unwrap();
84
85    let pt1 = BFVPlaintext::new(&s1, &bfv);
86    let pt2 = BFVPlaintext::new(&s2, &bfv);
87
88    let ct1 = bfv.encrypt(&pt1, &pk_a, &pk_b);
89    let ct2 = bfv.encrypt(&pt2, &pk_a, &pk_b);
90
91    let ct_sum = bfv.sum_ciphertexts(ct1, ct2);
92    let result = bfv.backend_decrypt(&ct_sum, &sk);
93
94    for i in 0..n.min(50) {
95        assert_eq!(result.coeffs[i], 252, "Addition failed at index {}", i);
96    }
97
98    let s3 = String::from_utf8(vec![64u8; 50]).unwrap();
99    let pt3 = BFVPlaintext::new(&s3, &bfv);
100    let pt4 = BFVPlaintext::new(&s3, &bfv);
101
102    let ct3 = bfv.encrypt(&pt3, &pk_a, &pk_b);
103    let ct4 = bfv.encrypt(&pt4, &pk_a, &pk_b);
104
105    let ct_sum2 = bfv.sum_ciphertexts(ct3, ct4);
106    let result2 = bfv.backend_decrypt(&ct_sum2, &sk);
107
108    for i in 0..50 {
109        assert_eq!(result2.coeffs[i], 128, "Addition failed at index {}", i);
110    }
111}
112
113#[test]
114fn test_add_cipher_plain() {
115    let bfv = BFV::for_test();
116    let (pk_a, pk_b, sk) = generate_keys(&bfv);
117    let n = bfv.params.n;
118
119    let pt1 = BFVPlaintext::new_from_coeffs(vec![60u64; n], &bfv);
120    let pt2 = BFVPlaintext::new_from_coeffs(vec![30u64; n], &bfv);
121
122    let ct1 = bfv.encrypt(&pt1, &pk_a, &pk_b);
123    let result = bfv.backend_decrypt(
124        &bfv.sum_ciphertext_and_plaintext(&ct1, &pt2),
125        &sk
126    );
127
128    for i in 0..n.min(50) {
129        assert_eq!(result.coeffs[i], 90, "C+P addition failed at index {}", i);
130    }
131}
132
133#[test]
134fn test_mul_cipher_plain() {
135    let bfv = BFV::for_test();
136    let (pk_a, pk_b, sk) = generate_keys(&bfv);
137    let n = bfv.params.n;
138
139    let pt1 = BFVPlaintext::new_from_coeffs(vec![10u64; n], &bfv);
140
141    let mut scalar = vec![0u64; n];
142    scalar[0] = 3;
143    let pt_scalar = BFVPlaintext::new_from_coeffs(scalar, &bfv);
144
145    let ct1 = bfv.encrypt(&pt1, &pk_a, &pk_b);
146
147    let result = bfv.backend_decrypt(
148        &bfv.mul_ciphertext_and_plaintext(&ct1, &pt_scalar),
149        &sk
150    );
151
152    for i in 0..n.min(50) {
153        assert_eq!(result.coeffs[i], 30, "C*P multiplication failed at index {}", i);
154    }
155}
156
157#[test]
158fn test_add_then_mul() {
159    let bfv = BFV::for_test();
160    let (pk_a, pk_b, sk) = generate_keys(&bfv);
161
162    let pt_a = BFVPlaintext::new_from_coeffs(vec![20u64; bfv.params.n], &bfv);
163    let pt_b = BFVPlaintext::new_from_coeffs(vec![10u64; bfv.params.n], &bfv);
164
165    let mut scalar = vec![0u64; bfv.params.n];
166    scalar[0] = 3;
167    let pt_s = BFVPlaintext::new_from_coeffs(scalar, &bfv);
168
169    let ct_sum = bfv.sum_ciphertexts(
170        bfv.encrypt(&pt_a, &pk_a, &pk_b),
171        bfv.encrypt(&pt_b, &pk_a, &pk_b),
172    );
173
174    let ct_res = bfv.mul_ciphertext_and_plaintext(&ct_sum, &pt_s);
175    let result = bfv.backend_decrypt(&ct_res, &sk);
176
177    for i in 0..50 {
178        assert_eq!(result.coeffs[i], 90, "(A+B)*C failed at index {}", i);
179    }
180}
181
182#[test]
183fn test_mul_then_add() {
184    let bfv = BFV::for_test();
185    let (pk_a, pk_b, sk) = generate_keys(&bfv);
186
187    let pt5 = BFVPlaintext::new_from_coeffs(vec![5u64; bfv.params.n], &bfv);
188    let pt7 = BFVPlaintext::new_from_coeffs(vec![7u64; bfv.params.n], &bfv);
189
190    let mut s = vec![0u64; bfv.params.n];
191    s[0] = 4;
192    let pt_s = BFVPlaintext::new_from_coeffs(s, &bfv);
193
194    let ct_mul = bfv.mul_ciphertext_and_plaintext(
195        &bfv.encrypt(&pt5, &pk_a, &pk_b),
196        &pt_s,
197    );
198
199    let ct_res = bfv.sum_ciphertexts(ct_mul, bfv.encrypt(&pt7, &pk_a, &pk_b));
200    let result = bfv.backend_decrypt(&ct_res, &sk);
201
202    for i in 0..50 {
203        assert_eq!(result.coeffs[i], 27, "A*B+C failed at index {}", i);
204    }
205}
206
207#[test]
208fn test_chained_additions() {
209    let bfv = BFV::for_test();
210    let (pk_a, pk_b, sk) = generate_keys(&bfv);
211
212    let pt = BFVPlaintext::new_from_coeffs(vec![1u64; bfv.params.n], &bfv);
213
214    let mut acc = bfv.encrypt(&pt, &pk_a, &pk_b);
215
216    for _ in 1..10 {
217        acc = bfv.sum_ciphertexts(
218            acc,
219            bfv.encrypt(&pt, &pk_a, &pk_b)
220        );
221    }
222
223    let result = bfv.backend_decrypt(&acc, &sk);
224
225    for i in 0..50 {
226        assert_eq!(result.coeffs[i], 10, "Chained addition failed at index {}", i);
227    }
228}
229
230#[test]
231fn test_edge_zero() {
232    let bfv = BFV::for_test();
233    let (pk_a, pk_b, sk) = generate_keys(&bfv);
234
235    let pt_x = BFVPlaintext::new_from_coeffs(vec![128u64; bfv.params.n], &bfv);
236    let pt_z = BFVPlaintext::new_from_coeffs(vec![0u64; bfv.params.n], &bfv);
237
238    let ct = bfv.sum_ciphertexts(
239        bfv.encrypt(&pt_x, &pk_a, &pk_b),
240        bfv.encrypt(&pt_z, &pk_a, &pk_b),
241    );
242
243    let result = bfv.backend_decrypt(&ct, &sk);
244
245    for i in 0..50 {
246        assert_eq!(result.coeffs[i], 128, "Zero addition failed at index {}", i);
247    }
248}
249
250#[test]
251fn test_edge_max_value() {
252    let bfv = BFV::for_test();
253    let (pk_a, pk_b, sk) = generate_keys(&bfv);
254
255    let pt = BFVPlaintext::new_from_coeffs(vec![255u64; bfv.params.n], &bfv);
256    let ct = bfv.encrypt(&pt, &pk_a, &pk_b);
257    let result = bfv.backend_decrypt(&ct, &sk);
258
259    for i in 0..50 {
260        assert_eq!(result.coeffs[i], 255, "Max value decryption failed at index {}", i);
261    }
262}
263
264#[test]
265fn test_edge_add_one_to_max() {
266    let bfv = BFV::for_test();
267    let (pk_a, pk_b, sk) = generate_keys(&bfv);
268
269    let t = bfv.t;
270
271    let pt_max = BFVPlaintext::new_from_coeffs(vec![t - 1; bfv.params.n], &bfv);
272    let pt_one = BFVPlaintext::new_from_coeffs(vec![1u64; bfv.params.n], &bfv);
273
274    let ct = bfv.sum_ciphertexts(
275        bfv.encrypt(&pt_max, &pk_a, &pk_b),
276        bfv.encrypt(&pt_one, &pk_a, &pk_b),
277    );
278
279    let result = bfv.backend_decrypt(&ct, &sk);
280
281    for i in 0..50 {
282        assert_eq!(result.coeffs[i], 0, "Overflow wrap failed at index {}", i);
283    }
284}
285
286// "Roundtrip" tests
287
288#[test]
289fn test_roundtrip_full_coeffs() {
290    let bfv = BFV::for_test();
291    let (pk_a, pk_b, sk) = generate_keys(&bfv);
292    let n = bfv.params.n;
293
294    let coeffs: Vec<u64> = (0..n).map(|i| (i % 200) as u64).collect();
295    let plaintext = BFVPlaintext::new_from_coeffs(coeffs.clone(), &bfv);
296
297    let ciphertext = bfv.encrypt(&plaintext, &pk_a, &pk_b);
298    let recovered = bfv.backend_decrypt(&ciphertext, &sk);
299
300    assert_eq!(recovered.coeffs, plaintext.plain.coeffs, "Full roundtrip failed");
301}
302
303#[test]
304fn test_roundtrip_random_small_values() {
305    let bfv = BFV::for_test();
306    let (pk_a, pk_b, sk) = generate_keys(&bfv);
307    let n = bfv.params.n;
308
309    let mut rng = std::collections::hash_map::DefaultHasher::new();
310    let coeffs: Vec<u64> = (0..n)
311        .map(|i| {
312            use std::hash::{Hash, Hasher};
313            i.hash(&mut rng);
314            (rng.finish() % (bfv.t / 2)) as u64
315        })
316        .collect();
317
318    let plaintext = BFVPlaintext::new_from_coeffs(coeffs.clone(), &bfv);
319    let ciphertext = bfv.encrypt(&plaintext, &pk_a, &pk_b);
320    let recovered = bfv.backend_decrypt(&ciphertext, &sk);
321
322    assert_plaintext_eq(&recovered, &plaintext.plain, n);
323}
324
325#[test]
326fn test_roundtrip_binary_plaintext() {
327    let bfv = BFV::for_test();
328    let (pk_a, pk_b, sk) = generate_keys(&bfv);
329    let n = bfv.params.n;
330
331    // Plaintext binaire : idéal pour les opérations multiples
332    let coeffs: Vec<u64> = (0..n).map(|i| (i % 2) as u64).collect();
333    let plaintext = BFVPlaintext::new_from_coeffs(coeffs.clone(), &bfv);
334
335    let ciphertext = bfv.encrypt(&plaintext, &pk_a, &pk_b);
336    let recovered = bfv.backend_decrypt(&ciphertext, &sk);
337
338    assert_eq!(recovered.coeffs, plaintext.plain.coeffs, "Binary roundtrip failed");
339}
340
341//Noise "budget" tests :
342
343#[test]
344fn test_noise_budget_additions() {
345    let bfv = BFV::for_medium(); 
346    let (pk_a, pk_b, sk) = generate_keys(&bfv);
347    let n = bfv.params.n;
348
349    let pt = BFVPlaintext::new_from_coeffs(vec![1u64; n], &bfv);
350    let mut ct = bfv.encrypt(&pt, &pk_a, &pk_b);
351
352    let mut success_count = 0;
353    for i in 1..=100 {
354        ct = bfv.sum_ciphertexts(ct.clone(), ct.clone());
355        let result = bfv.backend_decrypt(&ct, &sk);
356        
357        let expected = (1u64 << i.min(7)) % bfv.t; 
358        
359        if result.coeffs[0] == expected {
360            success_count = i;
361        } else {
362            println!("Decryption failed after {} additions: got {}, expected {}", 
363                     i, result.coeffs[0], expected);
364            break;
365        }
366    }
367
368    println!("Successful additions before noise failure: {}", success_count);
369    assert!(success_count >= 5, "Noise budget too small: failed after {} additions", success_count);
370}
371
372#[test]
373fn test_noise_budget_multiplications() {
374    let bfv = BFV::for_large(); 
375    let (pk_a, pk_b, sk) = generate_keys(&bfv);
376    let n = bfv.params.n;
377
378    let mut m = vec![0u64; n];
379    m[0] = 2;  
380    let pt = BFVPlaintext::new_from_coeffs(m, &bfv);
381
382    let mut scalar = vec![0u64; n];
383    scalar[0] = 2;  
384    let pt_scalar = BFVPlaintext::new_from_coeffs(scalar, &bfv);
385    
386    let mut ct = bfv.encrypt(&pt, &pk_a, &pk_b);
387
388    let mut success_count = 0;
389    for i in 1..=15 {
390        ct = bfv.mul_ciphertext_and_plaintext(&ct, &pt_scalar);
391        let result = bfv.backend_decrypt(&ct, &sk);
392        
393        let expected = (2u64.pow(i as u32 + 1)) % bfv.t;
394        
395        if result.coeffs[0] == expected {
396            success_count = i;
397        } else {
398            println!("Decryption failed after {} multiplications: got {}, expected {}", 
399                     i, result.coeffs[0], expected);
400            break;
401        }
402    }
403
404    println!("Successful multiplications before noise failure: {}", success_count);
405    assert!(success_count >= 8, "Failed after {} multiplications", success_count);
406}
407
408#[test]
409fn test_noise_budget_mixed_operations() {
410    let bfv = BFV::for_test();
411    let (pk_a, pk_b, sk) = generate_keys(&bfv);
412    let n = bfv.params.n;
413
414    let pt_base = BFVPlaintext::new_from_coeffs(vec![3u64; n], &bfv);
415    let pt_add = BFVPlaintext::new_from_coeffs(vec![1u64; n], &bfv);
416    let pt_mul = BFVPlaintext::new_from_coeffs(vec![2u64; n], &bfv);
417
418    let mut ct = bfv.encrypt(&pt_base, &pk_a, &pk_b);
419    let mut operations = 0;
420
421    for i in 0..15 {
422        if i % 2 == 0 {
423            ct = bfv.sum_ciphertexts(ct.clone(), bfv.encrypt(&pt_add, &pk_a, &pk_b));
424        } else {
425            ct = bfv.mul_ciphertext_and_plaintext(&ct, &pt_mul);
426        }
427        operations += 1;
428
429        let result = bfv.backend_decrypt(&ct, &sk);
430        assert!(result.coeffs[0] < bfv.t, "Decryption overflow after {} ops", operations);
431    }
432
433    println!("Completed {} mixed operations without decryption failure", operations);
434}
435
436//Noise estimation tests
437
438#[test]
439fn test_noise_estimation_growth() {
440    let bfv = BFV::for_test();
441    let (pk_a, pk_b, sk) = generate_keys(&bfv);
442    let n = bfv.params.n;
443
444    let pt = BFVPlaintext::new_from_coeffs(vec![1u64; n], &bfv);
445    let mut ct = bfv.encrypt(&pt, &pk_a, &pk_b);
446
447    let mut noises = Vec::new();
448    for i in 0..15 {
449        let noise = bfv.estimate_noise(&sk, &bfv.params, &ct);
450        noises.push(noise);
451        println!("After {} additions, estimated noise: {}", i, noise);
452        ct = bfv.sum_ciphertexts(ct.clone(), ct.clone());
453    }
454
455    assert!(noises[10] >= noises[0], "Noise should grow with operations");
456}
457
458#[test]
459fn test_noise_vs_delta_threshold() {
460    let bfv = BFV::for_test();
461    let (pk_a, pk_b, sk) = generate_keys(&bfv);
462    let n = bfv.params.n;
463
464    let delta = bfv.params.q / bfv.t;
465    let threshold = delta / 2;  
466
467    let pt = BFVPlaintext::new_from_coeffs(vec![1u64; n], &bfv);
468    let mut ct = bfv.encrypt(&pt, &pk_a, &pk_b);
469
470    for i in 0..50 {
471        let noise = bfv.estimate_noise(&sk, &bfv.params, &ct);
472        
473        if noise < threshold {
474            // Décryption devrait fonctionner
475            let result = bfv.backend_decrypt(&ct, &sk);
476            assert!(result.coeffs[0] < bfv.t, "Decryption failed while noise < threshold");
477        } else {
478            println!("Noise exceeded threshold ({}) after {} additions: {}", threshold, i, noise);
479            break;
480        }
481        
482        ct = bfv.sum_ciphertexts(ct.clone(), ct.clone());
483    }
484}
485
486
487//Parameters validation tests
488
489#[test]
490#[should_panic(expected = "NTT requires n to be a power of 2, got 1028 ! For explanations, please read '/docs/simple-ring.pdf")]
491fn test_invalid_n_not_power_of_two() {
492    let _ = simple_ring::RingParams::new(1028, 786_433, 1);
493}
494
495#[test]
496fn test_valid_configurations() {
497    BFV::for_test();   
498    BFV::for_medium(); 
499    BFV::for_large();  
500}
501
502#[test]
503fn test_parameter_consistency() {
504    let bfv = BFV::for_test();
505    
506    assert!(bfv.params.n.is_power_of_two());
507    assert!(bfv.t >= 2 && bfv.t < bfv.params.q);
508    assert!(bfv.eta >= 1 && bfv.eta <= 16);
509    
510    assert_eq!(bfv.ntt_precalculated.twiddles.len(), bfv.params.n);
511    assert_eq!(bfv.ntt_precalculated.twiddles_inv.len(), bfv.params.n);
512}
513
514// Performances tests (not really precise as real benchmarks)
515
516#[test]
517#[ignore] //Ignore by default, it can be really slow 
518fn test_performance_encrypt_decrypt() {
519    let bfv = BFV::for_large(); 
520    let (pk_a, pk_b, sk) = generate_keys(&bfv);
521    
522    let message = BFVPlaintext::new_from_coeffs(vec![42u64; bfv.params.n], &bfv);
523    
524    let start = Instant::now();
525    let ct = bfv.encrypt(&message, &pk_a, &pk_b);
526    let enc_time = start.elapsed();
527    
528    let start = Instant::now();
529    let _ = bfv.backend_decrypt(&ct, &sk);
530    let dec_time = start.elapsed();
531    
532    println!("Encrypt time (n=4096): {:?}", enc_time);
533    println!("Decrypt time (n=4096): {:?}", dec_time);
534    
535    // Seuils approximatifs (à ajuster selon ta machine)
536    assert!(enc_time.as_millis() < 100, "Encryption too slow: {:?}", enc_time);
537    assert!(dec_time.as_millis() < 100, "Decryption too slow: {:?}", dec_time);
538}
539
540#[test]
541#[ignore]
542fn test_performance_homomorphic_add() {
543    let bfv = BFV::for_large();
544    let (pk_a, pk_b, _sk) = generate_keys(&bfv);
545    
546    let pt = BFVPlaintext::new_from_coeffs(vec![1u64; bfv.params.n], &bfv);
547    let ct1 = bfv.encrypt(&pt, &pk_a, &pk_b);
548    let ct2 = bfv.encrypt(&pt, &pk_a, &pk_b);
549    
550    let start = Instant::now();
551    let _ = bfv.sum_ciphertexts(ct1, ct2);
552    let add_time = start.elapsed();
553    
554    println!("Homomorphic add time (n=4096): {:?}", add_time);
555    assert!(add_time.as_millis() < 50, "Addition too slow: {:?}", add_time);
556}
557
558
559//Other tests :
560
561#[test]
562fn test_empty_plaintext() { //Just encrypt empty plaintexts
563    let bfv = BFV::for_test();
564    let (pk_a, pk_b, sk) = generate_keys(&bfv);
565    
566    let pt = BFVPlaintext::new_from_coeffs(vec![0u64; bfv.params.n], &bfv);
567    let ct = bfv.encrypt(&pt, &pk_a, &pk_b);
568    let result = bfv.backend_decrypt(&ct, &sk);
569    
570    for i in 0..50 {
571        assert_eq!(result.coeffs[i], 0, "Empty plaintext decryption failed");
572    }
573}
574
575#[test]
576fn test_single_coefficient_nonzero() { 
577    let bfv = BFV::for_test();
578    let (pk_a, pk_b, sk) = generate_keys(&bfv);
579    let n = bfv.params.n;
580    
581    let mut coeffs = vec![0u64; n];
582    coeffs[n/2] = 123; 
583    
584    let pt = BFVPlaintext::new_from_coeffs(coeffs.clone(), &bfv);
585    let ct = bfv.encrypt(&pt, &pk_a, &pk_b);
586    let result = bfv.backend_decrypt(&ct, &sk);
587    
588    assert_eq!(result.coeffs[n/2], 123, "Single coefficient decryption failed");
589}
590
591#[test]
592fn test_deterministic_encryption_with_same_randomness() {
593    // BFV is probabilistic : even with the same keys, because of the noise, a plaintext shouldn't give two same ciphertexts
594    
595    let bfv = BFV::for_test();
596    let (pk_a, pk_b, sk) = generate_keys(&bfv);
597    
598    let pt = BFVPlaintext::new_from_coeffs(vec![42u64; bfv.params.n], &bfv);
599    
600    let ct1 = bfv.encrypt(&pt, &pk_a, &pk_b);
601    let ct2 = bfv.encrypt(&pt, &pk_a, &pk_b);
602    
603    assert_ne!(ct1.c0.coeffs, ct2.c0.coeffs, "Encryption should be probabilistic");
604    
605    let res1 = bfv.backend_decrypt(&ct1, &sk);
606    let res2 = bfv.backend_decrypt(&ct2, &sk);
607    assert_eq!(res1.coeffs, res2.coeffs, "Decryption should be deterministic");
608}
609