arnaucube
arnaucube/poulpyPublic Self-hosted repository
poulpy/poulpy-core/src/glwe_packing.rs
poulpy-core/src/glwe_packing.rsRaw
  1use std::collections::HashMap;
  2
  3use poulpy_hal::{
  4    api::{
  5        ScratchAvailable, VecZnxAddInplace, VecZnxAutomorphismInplace, VecZnxBigAddSmallInplace, VecZnxBigAutomorphismInplace,
  6        VecZnxBigNormalize, VecZnxBigNormalizeTmpBytes, VecZnxBigSubSmallNegateInplace, VecZnxCopy, VecZnxDftApply,
  7        VecZnxDftBytesOf, VecZnxDftCopy, VecZnxIdftApplyConsume, VecZnxIdftApplyTmpA, VecZnxNegateInplace, VecZnxNormalize,
  8        VecZnxNormalizeInplace, VecZnxNormalizeTmpBytes, VecZnxRotate, VecZnxRotateInplace, VecZnxRshInplace, VecZnxSub,
  9        VecZnxSubInplace, VecZnxSwitchRing, VmpApplyDftToDft, VmpApplyDftToDftAdd, VmpApplyDftToDftTmpBytes,
 10    },
 11    layouts::{Backend, DataMut, DataRef, Module, Scratch},
 12};
 13
 14use crate::{
 15    GLWEOperations,
 16    layouts::{GGLWEInfos, GLWE, GLWEInfos, LWEInfos, prepared::AutomorphismKeyPrepared},
 17};
 18
 19/// [GLWEPacker] enables only the fly GLWE packing
 20/// with constant memory of Log(N) ciphertexts.
 21/// Main difference with usual GLWE packing is that
 22/// the output is bit-reversed.
 23pub struct GLWEPacker {
 24    accumulators: Vec<Accumulator>,
 25    log_batch: usize,
 26    counter: usize,
 27}
 28
 29/// [Accumulator] stores intermediate packing result.
 30/// There are Log(N) such accumulators in a [GLWEPacker].
 31struct Accumulator {
 32    data: GLWE<Vec<u8>>,
 33    value: bool,   // Implicit flag for zero ciphertext
 34    control: bool, // Can be combined with incoming value
 35}
 36
 37impl Accumulator {
 38    /// Allocates a new [Accumulator].
 39    ///
 40    /// #Arguments
 41    ///
 42    /// * `module`: static backend FFT tables.
 43    /// * `base2k`: base 2 logarithm of the GLWE ciphertext in memory digit representation.
 44    /// * `k`: base 2 precision of the GLWE ciphertext precision over the Torus.
 45    /// * `rank`: rank of the GLWE ciphertext.
 46    pub fn alloc<A, B: Backend>(module: &Module<B>, infos: &A) -> Self
 47    where
 48        A: GLWEInfos,
 49    {
 50        Self {
 51            data: GLWE::alloc_from_infos(module, infos),
 52            value: false,
 53            control: false,
 54        }
 55    }
 56}
 57
 58impl GLWEPacker {
 59    /// Instantiates a new [GLWEPacker].
 60    ///
 61    /// # Arguments
 62    ///
 63    /// * `module`: static backend FFT tables.
 64    /// * `log_batch`: packs coefficients which are multiples of X^{N/2^log_batch}.
 65    ///   i.e. with `log_batch=0` only the constant coefficient is packed
 66    ///   and N GLWE ciphertext can be packed. With `log_batch=2` all coefficients
 67    ///   which are multiples of X^{N/4} are packed. Meaning that N/4 ciphertexts
 68    ///   can be packed.
 69    pub fn new<A, B: Backend>(module: Module<B>, infos: &A, log_batch: usize) -> Self
 70    where
 71        A: GLWEInfos,
 72    {
 73        let mut accumulators: Vec<Accumulator> = Vec::<Accumulator>::new();
 74        let log_n: usize = infos.n().log2();
 75        (0..log_n - log_batch).for_each(|_| accumulators.push(Accumulator::alloc(module, infos)));
 76        Self {
 77            accumulators,
 78            log_batch,
 79            counter: 0,
 80        }
 81    }
 82
 83    /// Implicit reset of the internal state (to be called before a new packing procedure).
 84    fn reset(&mut self) {
 85        for i in 0..self.accumulators.len() {
 86            self.accumulators[i].value = false;
 87            self.accumulators[i].control = false;
 88        }
 89        self.counter = 0;
 90    }
 91
 92    /// Number of scratch space bytes required to call [Self::add].
 93    pub fn scratch_space<B: Backend, OUT, KEY>(module: &Module<B>, out_infos: &OUT, key_infos: &KEY) -> usize
 94    where
 95        OUT: GLWEInfos,
 96        KEY: GGLWEInfos,
 97        Module<B>: VecZnxDftBytesOf + VmpApplyDftToDftTmpBytes + VecZnxBigNormalizeTmpBytes + VecZnxNormalizeTmpBytes,
 98    {
 99        pack_core_scratch_space(module, out_infos, key_infos)
100    }
101
102    pub fn galois_elements<B: Backend>(module: &Module<B>) -> Vec<i64> {
103        GLWE::trace_galois_elements(module)
104    }
105
106    /// Adds a GLWE ciphertext to the [GLWEPacker].
107    /// #Arguments
108    ///
109    /// * `module`: static backend FFT tables.
110    /// * `res`: space to append fully packed ciphertext. Only when the number
111    ///   of packed ciphertexts reaches N/2^log_batch is a result written.
112    /// * `a`: ciphertext to pack. Can optionally give None to pack a 0 ciphertext.
113    /// * `auto_keys`: a [HashMap] containing the [AutomorphismKeyExec]s.
114    /// * `scratch`: scratch space of size at least [Self::scratch_space].
115    pub fn add<DataA: DataRef, DataAK: DataRef, B: Backend>(
116        &mut self,
117        module: &Module<B>,
118        a: Option<&GLWE<DataA>>,
119        auto_keys: &HashMap<i64, AutomorphismKeyPrepared<DataAK, B>>,
120        scratch: &mut Scratch<B>,
121    ) where
122        Module<B>: VecZnxDftBytesOf
123            + VmpApplyDftToDftTmpBytes
124            + VecZnxBigNormalizeTmpBytes
125            + VmpApplyDftToDft<B>
126            + VmpApplyDftToDftAdd<B>
127            + VecZnxDftApply<B>
128            + VecZnxIdftApplyConsume<B>
129            + VecZnxBigAddSmallInplace<B>
130            + VecZnxBigNormalize<B>
131            + VecZnxCopy
132            + VecZnxRotateInplace<B>
133            + VecZnxSub
134            + VecZnxNegateInplace
135            + VecZnxRshInplace<B>
136            + VecZnxAddInplace
137            + VecZnxNormalizeInplace<B>
138            + VecZnxSubInplace
139            + VecZnxRotate
140            + VecZnxAutomorphismInplace<B>
141            + VecZnxBigSubSmallNegateInplace<B>
142            + VecZnxBigAutomorphismInplace<B>
143            + VecZnxNormalize<B>
144            + VecZnxNormalizeTmpBytes,
145        Scratch<B>: ScratchAvailable,
146    {
147        assert!(
148            (self.counter as u32) < self.accumulators[0].data.n(),
149            "Packing limit of {} reached",
150            self.accumulators[0].data.n().0 as usize >> self.log_batch
151        );
152
153        pack_core(
154            module,
155            a,
156            &mut self.accumulators,
157            self.log_batch,
158            auto_keys,
159            scratch,
160        );
161        self.counter += 1 << self.log_batch;
162    }
163
164    /// Flush result to`res`.
165    pub fn flush<Data: DataMut, B: Backend>(&mut self, module: &Module<B>, res: &mut GLWE<Data>)
166    where
167        Module<B>: VecZnxCopy,
168    {
169        assert!(self.counter as u32 == self.accumulators[0].data.n());
170        // Copy result GLWE into res GLWE
171        res.copy(
172            module,
173            &self.accumulators[module.log_n() - self.log_batch - 1].data,
174        );
175
176        self.reset();
177    }
178}
179
180fn pack_core_scratch_space<B: Backend, OUT, KEY>(module: &Module<B>, out_infos: &OUT, key_infos: &KEY) -> usize
181where
182    OUT: GLWEInfos,
183    KEY: GGLWEInfos,
184    Module<B>: VecZnxDftBytesOf + VmpApplyDftToDftTmpBytes + VecZnxBigNormalizeTmpBytes + VecZnxNormalizeTmpBytes,
185{
186    combine_scratch_space(module, out_infos, key_infos)
187}
188
189fn pack_core<D: DataRef, DataAK: DataRef, B: Backend>(
190    module: &Module<B>,
191    a: Option<&GLWE<D>>,
192    accumulators: &mut [Accumulator],
193    i: usize,
194    auto_keys: &HashMap<i64, AutomorphismKeyPrepared<DataAK, B>>,
195    scratch: &mut Scratch<B>,
196) where
197    Module<B>: VecZnxDftBytesOf
198        + VmpApplyDftToDftTmpBytes
199        + VecZnxBigNormalizeTmpBytes
200        + VmpApplyDftToDft<B>
201        + VmpApplyDftToDftAdd<B>
202        + VecZnxDftApply<B>
203        + VecZnxIdftApplyConsume<B>
204        + VecZnxBigAddSmallInplace<B>
205        + VecZnxBigNormalize<B>
206        + VecZnxCopy
207        + VecZnxRotateInplace<B>
208        + VecZnxSub
209        + VecZnxNegateInplace
210        + VecZnxRshInplace<B>
211        + VecZnxAddInplace
212        + VecZnxNormalizeInplace<B>
213        + VecZnxSubInplace
214        + VecZnxRotate
215        + VecZnxAutomorphismInplace<B>
216        + VecZnxBigSubSmallNegateInplace<B>
217        + VecZnxBigAutomorphismInplace<B>
218        + VecZnxNormalize<B>
219        + VecZnxNormalizeTmpBytes,
220    Scratch<B>: ScratchAvailable,
221{
222    let log_n: usize = module.log_n();
223
224    if i == log_n {
225        return;
226    }
227
228    // Isolate the first accumulator
229    let (acc_prev, acc_next) = accumulators.split_at_mut(1);
230
231    // Control = true accumlator is free to overide
232    if !acc_prev[0].control {
233        let acc_mut_ref: &mut Accumulator = &mut acc_prev[0]; // from split_at_mut
234
235        // No previous value -> copies and sets flags accordingly
236        if let Some(a_ref) = a {
237            acc_mut_ref.data.copy(module, a_ref);
238            acc_mut_ref.value = true
239        } else {
240            acc_mut_ref.value = false
241        }
242        acc_mut_ref.control = true; // Able to be combined on next call
243    } else {
244        // Compresses acc_prev <- combine(acc_prev, a).
245        combine(module, &mut acc_prev[0], a, i, auto_keys, scratch);
246        acc_prev[0].control = false;
247
248        // Propagates to next accumulator
249        if acc_prev[0].value {
250            pack_core(
251                module,
252                Some(&acc_prev[0].data),
253                acc_next,
254                i + 1,
255                auto_keys,
256                scratch,
257            );
258        } else {
259            pack_core(
260                module,
261                None::<&GLWE<Vec<u8>>>,
262                acc_next,
263                i + 1,
264                auto_keys,
265                scratch,
266            );
267        }
268    }
269}
270
271fn combine_scratch_space<B: Backend, OUT, KEY>(module: &Module<B>, out_infos: &OUT, key_infos: &KEY) -> usize
272where
273    OUT: GLWEInfos,
274    KEY: GGLWEInfos,
275    Module<B>: VecZnxDftBytesOf + VmpApplyDftToDftTmpBytes + VecZnxBigNormalizeTmpBytes + VecZnxNormalizeTmpBytes,
276{
277    GLWE::bytes_of_from_infos(module, out_infos)
278        + (GLWE::rsh_scratch_space(module.n()) | GLWE::automorphism_inplace_scratch_space(module, out_infos, key_infos))
279}
280
281/// [combine] merges two ciphertexts together.
282fn combine<D: DataRef, DataAK: DataRef, B: Backend>(
283    module: &Module<B>,
284    acc: &mut Accumulator,
285    b: Option<&GLWE<D>>,
286    i: usize,
287    auto_keys: &HashMap<i64, AutomorphismKeyPrepared<DataAK, B>>,
288    scratch: &mut Scratch<B>,
289) where
290    Module<B>: VecZnxDftBytesOf
291        + VmpApplyDftToDftTmpBytes
292        + VecZnxBigNormalizeTmpBytes
293        + VmpApplyDftToDft<B>
294        + VmpApplyDftToDftAdd<B>
295        + VecZnxDftApply<B>
296        + VecZnxIdftApplyConsume<B>
297        + VecZnxBigAddSmallInplace<B>
298        + VecZnxBigNormalize<B>
299        + VecZnxCopy
300        + VecZnxRotateInplace<B>
301        + VecZnxSub
302        + VecZnxNegateInplace
303        + VecZnxRshInplace<B>
304        + VecZnxAddInplace
305        + VecZnxNormalizeInplace<B>
306        + VecZnxSubInplace
307        + VecZnxRotate
308        + VecZnxAutomorphismInplace<B>
309        + VecZnxBigSubSmallNegateInplace<B>
310        + VecZnxBigAutomorphismInplace<B>
311        + VecZnxNormalize<B>
312        + VecZnxNormalizeTmpBytes,
313    Scratch<B>: ScratchAvailable,
314{
315    let log_n: usize = acc.data.n().log2();
316    let a: &mut GLWE<Vec<u8>> = &mut acc.data;
317
318    let gal_el: i64 = if i == 0 {
319        -1
320    } else {
321        module.galois_element(1 << (i - 1))
322    };
323
324    let t: i64 = 1 << (log_n - i - 1);
325
326    // Goal is to evaluate: a = a + b*X^t + phi(a - b*X^t))
327    // We also use the identity: AUTO(a * X^t, g) = -X^t * AUTO(a, g)
328    // where t = 2^(log_n - i - 1) and g = 5^{2^(i - 1)}
329    // Different cases for wether a and/or b are zero.
330    //
331    // Implicite RSH without modulus switch, introduces extra I(X) * Q/2 on decryption.
332    // Necessary so that the scaling of the plaintext remains constant.
333    // It however is ok to do so here because coefficients are eventually
334    // either mapped to garbage or twice their value which vanishes I(X)
335    // since 2*(I(X) * Q/2) = I(X) * Q = 0 mod Q.
336    if acc.value {
337        if let Some(b) = b {
338            let (mut tmp_b, scratch_1) = scratch.take_glwe_ct(a);
339
340            // a = a * X^-t
341            a.rotate_inplace(module, -t, scratch_1);
342
343            // tmp_b = a * X^-t - b
344            tmp_b.sub(module, a, b);
345            tmp_b.rsh(module, 1, scratch_1);
346
347            // a = a * X^-t + b
348            a.add_inplace(module, b);
349            a.rsh(module, 1, scratch_1);
350
351            tmp_b.normalize_inplace(module, scratch_1);
352
353            // tmp_b = phi(a * X^-t - b)
354            if let Some(key) = auto_keys.get(&gal_el) {
355                tmp_b.automorphism_inplace(module, key, scratch_1);
356            } else {
357                panic!("auto_key[{gal_el}] not found");
358            }
359
360            // a = a * X^-t + b - phi(a * X^-t - b)
361            a.sub_inplace_ab(module, &tmp_b);
362            a.normalize_inplace(module, scratch_1);
363
364            // a = a + b * X^t - phi(a * X^-t - b) * X^t
365            //   = a + b * X^t - phi(a * X^-t - b) * - phi(X^t)
366            //   = a + b * X^t + phi(a - b * X^t)
367            a.rotate_inplace(module, t, scratch_1);
368        } else {
369            a.rsh(module, 1, scratch);
370            // a = a + phi(a)
371            if let Some(key) = auto_keys.get(&gal_el) {
372                a.automorphism_add_inplace(module, key, scratch);
373            } else {
374                panic!("auto_key[{gal_el}] not found");
375            }
376        }
377    } else if let Some(b) = b {
378        let (mut tmp_b, scratch_1) = scratch.take_glwe_ct(a);
379        tmp_b.rotate(module, 1 << (log_n - i - 1), b);
380        tmp_b.rsh(module, 1, scratch_1);
381
382        // a = (b* X^t - phi(b* X^t))
383        if let Some(key) = auto_keys.get(&gal_el) {
384            a.automorphism_sub_negate(module, &tmp_b, key, scratch_1);
385        } else {
386            panic!("auto_key[{gal_el}] not found");
387        }
388
389        acc.value = true;
390    }
391}
392
393/// Packs [x_0: GLWE(m_0), x_1: GLWE(m_1), ..., x_i: GLWE(m_i)]
394/// to [0: GLWE(m_0 * X^x_0 + m_1 * X^x_1 + ... + m_i * X^x_i)]
395pub fn glwe_packing<D: DataMut, ATK, B: Backend>(
396    module: &Module<B>,
397    cts: &mut HashMap<usize, &mut GLWE<D>>,
398    log_gap_out: usize,
399    auto_keys: &HashMap<i64, AutomorphismKeyPrepared<ATK, B>>,
400    scratch: &mut Scratch<B>,
401) where
402    ATK: DataRef,
403    Module<B>: VecZnxRotateInplace<B>
404        + VecZnxNormalizeInplace<B>
405        + VecZnxNormalizeTmpBytes
406        + VecZnxSwitchRing
407        + VecZnxBigAutomorphismInplace<B>
408        + VecZnxRshInplace<B>
409        + VecZnxDftCopy<B>
410        + VecZnxIdftApplyTmpA<B>
411        + VecZnxSub
412        + VecZnxAddInplace
413        + VecZnxNegateInplace
414        + VecZnxCopy
415        + VecZnxSubInplace
416        + VecZnxDftBytesOf
417        + VmpApplyDftToDftTmpBytes
418        + VecZnxBigNormalizeTmpBytes
419        + VmpApplyDftToDft<B>
420        + VmpApplyDftToDftAdd<B>
421        + VecZnxDftApply<B>
422        + VecZnxIdftApplyConsume<B>
423        + VecZnxBigAddSmallInplace<B>
424        + VecZnxBigNormalize<B>
425        + VecZnxAutomorphismInplace<B>
426        + VecZnxBigSubSmallNegateInplace<B>
427        + VecZnxRotate
428        + VecZnxNormalize<B>,
429    Scratch<B>: ScratchAvailable,
430{
431    #[cfg(debug_assertions)]
432    {
433        assert!(*cts.keys().max().unwrap() < module.n())
434    }
435
436    let log_n: usize = module.log_n();
437
438    (0..log_n - log_gap_out).for_each(|i| {
439        let t: usize = (1 << log_n).min(1 << (log_n - 1 - i));
440
441        let auto_key: &AutomorphismKeyPrepared<ATK, B> = if i == 0 {
442            auto_keys.get(&-1).unwrap()
443        } else {
444            auto_keys.get(&module.galois_element(1 << (i - 1))).unwrap()
445        };
446
447        (0..t).for_each(|j| {
448            let mut a: Option<&mut GLWE<D>> = cts.remove(&j);
449            let mut b: Option<&mut GLWE<D>> = cts.remove(&(j + t));
450
451            pack_internal(module, &mut a, &mut b, i, auto_key, scratch);
452
453            if let Some(a) = a {
454                cts.insert(j, a);
455            } else if let Some(b) = b {
456                cts.insert(j, b);
457            }
458        });
459    });
460}
461
462#[allow(clippy::too_many_arguments)]
463fn pack_internal<A: DataMut, D: DataMut, DataAK: DataRef, B: Backend>(
464    module: &Module<B>,
465    a: &mut Option<&mut GLWE<A>>,
466    b: &mut Option<&mut GLWE<D>>,
467    i: usize,
468    auto_key: &AutomorphismKeyPrepared<DataAK, B>,
469    scratch: &mut Scratch<B>,
470) where
471    Module<B>: VecZnxRotateInplace<B>
472        + VecZnxNormalizeInplace<B>
473        + VecZnxNormalizeTmpBytes
474        + VecZnxBigAutomorphismInplace<B>
475        + VecZnxRshInplace<B>
476        + VecZnxDftCopy<B>
477        + VecZnxIdftApplyTmpA<B>
478        + VecZnxSub
479        + VecZnxAddInplace
480        + VecZnxNegateInplace
481        + VecZnxCopy
482        + VecZnxSubInplace
483        + VecZnxDftBytesOf
484        + VmpApplyDftToDftTmpBytes
485        + VecZnxBigNormalizeTmpBytes
486        + VmpApplyDftToDft<B>
487        + VmpApplyDftToDftAdd<B>
488        + VecZnxDftApply<B>
489        + VecZnxIdftApplyConsume<B>
490        + VecZnxBigAddSmallInplace<B>
491        + VecZnxBigNormalize<B>
492        + VecZnxAutomorphismInplace<B>
493        + VecZnxBigSubSmallNegateInplace<B>
494        + VecZnxRotate
495        + VecZnxNormalize<B>,
496    Scratch<B>: ScratchAvailable,
497{
498    // Goal is to evaluate: a = a + b*X^t + phi(a - b*X^t))
499    // We also use the identity: AUTO(a * X^t, g) = -X^t * AUTO(a, g)
500    // where t = 2^(log_n - i - 1) and g = 5^{2^(i - 1)}
501    // Different cases for wether a and/or b are zero.
502    //
503    // Implicite RSH without modulus switch, introduces extra I(X) * Q/2 on decryption.
504    // Necessary so that the scaling of the plaintext remains constant.
505    // It however is ok to do so here because coefficients are eventually
506    // either mapped to garbage or twice their value which vanishes I(X)
507    // since 2*(I(X) * Q/2) = I(X) * Q = 0 mod Q.
508    if let Some(a) = a.as_deref_mut() {
509        let t: i64 = 1 << (a.n().log2() - i - 1);
510
511        if let Some(b) = b.as_deref_mut() {
512            let (mut tmp_b, scratch_1) = scratch.take_glwe_ct(a);
513
514            // a = a * X^-t
515            a.rotate_inplace(module, -t, scratch_1);
516
517            // tmp_b = a * X^-t - b
518            tmp_b.sub(module, a, b);
519            tmp_b.rsh(module, 1, scratch_1);
520
521            // a = a * X^-t + b
522            a.add_inplace(module, b);
523            a.rsh(module, 1, scratch_1);
524
525            tmp_b.normalize_inplace(module, scratch_1);
526
527            // tmp_b = phi(a * X^-t - b)
528            tmp_b.automorphism_inplace(module, auto_key, scratch_1);
529
530            // a = a * X^-t + b - phi(a * X^-t - b)
531            a.sub_inplace_ab(module, &tmp_b);
532            a.normalize_inplace(module, scratch_1);
533
534            // a = a + b * X^t - phi(a * X^-t - b) * X^t
535            //   = a + b * X^t - phi(a * X^-t - b) * - phi(X^t)
536            //   = a + b * X^t + phi(a - b * X^t)
537            a.rotate_inplace(module, t, scratch_1);
538        } else {
539            a.rsh(module, 1, scratch);
540            // a = a + phi(a)
541            a.automorphism_add_inplace(module, auto_key, scratch);
542        }
543    } else if let Some(b) = b.as_deref_mut() {
544        let t: i64 = 1 << (b.n().log2() - i - 1);
545
546        let (mut tmp_b, scratch_1) = scratch.take_glwe_ct(b);
547        tmp_b.rotate(module, t, b);
548        tmp_b.rsh(module, 1, scratch_1);
549
550        // a = (b* X^t - phi(b* X^t))
551        b.automorphism_sub_negate(module, &tmp_b, auto_key, scratch_1);
552    }
553}