Skip to main content

tfhe/core_crypto/commons/
ciphertext_modulus.rs

1//! Module containing the definition of the [`CiphertextModulus`].
2
3use tfhe_versionable::Versionize;
4
5use crate::core_crypto::backward_compatibility::commons::ciphertext_modulus::SerializableCiphertextModulusVersions;
6use crate::core_crypto::commons::traits::UnsignedInteger;
7use crate::core_crypto::prelude::CastInto;
8use core::num::NonZeroU128;
9use std::cmp::Ordering;
10use std::fmt::Display;
11use std::marker::PhantomData;
12
13use super::parameters::CiphertextModulusLog;
14
15#[derive(Clone, Copy, PartialEq, Eq, Hash)]
16/// Private enum to avoid end user instantiating a bad CiphertextModulus
17///
18/// NonZeroU128 allows to always have a correct modulus and to have an enum that is no bigger than a
19/// u128 with the 0 optimization as the tag then corresponds to the Native variant.
20enum CiphertextModulusInner {
21    Native,
22    Custom(NonZeroU128),
23}
24
25#[derive(Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Versionize, Hash)]
26#[serde(
27    try_from = "SerializableCiphertextModulus",
28    into = "SerializableCiphertextModulus"
29)]
30#[versionize(
31    SerializableCiphertextModulusVersions,
32    try_from = "SerializableCiphertextModulus",
33    into = "SerializableCiphertextModulus"
34)]
35/// Structure representing a [`CiphertextModulus`] often noted $q$.
36pub struct CiphertextModulus<Scalar: UnsignedInteger> {
37    inner: CiphertextModulusInner,
38    _scalar: PhantomData<Scalar>,
39}
40
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub enum CiphertextModulusKind {
43    Native,
44    NonNativePowerOfTwo,
45    Other,
46}
47
48#[derive(serde::Serialize, serde::Deserialize, Versionize)]
49#[versionize(SerializableCiphertextModulusVersions)]
50/// Actual serialized modulus to be able to carry the UnsignedInteger bitwidth information
51pub struct SerializableCiphertextModulus {
52    pub modulus: u128,
53    pub scalar_bits: usize,
54}
55
56#[derive(Clone, Copy, Debug)]
57pub enum CiphertextModulusDeserializationError {
58    InvalidBitWidth { expected: usize, found: usize },
59    ZeroCustomModulus,
60}
61
62impl Display for CiphertextModulusDeserializationError {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        match self {
65            Self::InvalidBitWidth { expected, found } => write!(
66                f,
67                "Expected an unsigned integer with {expected} bits, \
68            found {found} bits during deserialization of CiphertextModulus, \
69            have you mixed types during deserialization?",
70            ),
71            Self::ZeroCustomModulus => write!(
72                f,
73                "Got zero modulus for CiphertextModulusInner::Custom variant"
74            ),
75        }
76    }
77}
78
79impl std::error::Error for CiphertextModulusDeserializationError {}
80
81impl<Scalar: UnsignedInteger> From<CiphertextModulus<Scalar>> for SerializableCiphertextModulus {
82    fn from(value: CiphertextModulus<Scalar>) -> Self {
83        let modulus = match value.inner {
84            CiphertextModulusInner::Native => 0,
85            CiphertextModulusInner::Custom(modulus) => modulus.get(),
86        };
87
88        Self {
89            modulus,
90            scalar_bits: Scalar::BITS,
91        }
92    }
93}
94
95impl<Scalar: UnsignedInteger> TryFrom<SerializableCiphertextModulus> for CiphertextModulus<Scalar> {
96    type Error = CiphertextModulusDeserializationError;
97
98    fn try_from(value: SerializableCiphertextModulus) -> Result<Self, Self::Error> {
99        if value.scalar_bits != Scalar::BITS {
100            return Err(CiphertextModulusDeserializationError::InvalidBitWidth {
101                expected: Scalar::BITS,
102                found: value.scalar_bits,
103            });
104        }
105
106        let res = if value.modulus == 0 {
107            Self {
108                inner: CiphertextModulusInner::Native,
109                _scalar: PhantomData,
110            }
111        } else {
112            Self {
113                inner: CiphertextModulusInner::Custom(
114                    NonZeroU128::new(value.modulus)
115                        .ok_or(CiphertextModulusDeserializationError::ZeroCustomModulus)?,
116                ),
117                _scalar: PhantomData,
118            }
119        };
120        Ok(res.canonicalize())
121    }
122}
123
124#[derive(Clone, Copy, PartialEq, Eq)]
125pub enum CiphertextModulusCreationError {
126    ModulusTooBig,
127    CustomModuli64BitsOrLessOnly,
128}
129
130impl CiphertextModulusCreationError {
131    pub const fn const_err_msg(self) -> &'static str {
132        match self {
133            Self::ModulusTooBig => {
134                "Modulus is bigger than the maximum value of the associated Scalar type"
135            }
136            Self::CustomModuli64BitsOrLessOnly => {
137                "Non power of 2 moduli are not supported for types wider than u64"
138            }
139        }
140    }
141}
142
143impl From<CiphertextModulusCreationError> for &str {
144    fn from(value: CiphertextModulusCreationError) -> Self {
145        value.const_err_msg()
146    }
147}
148
149impl std::fmt::Debug for CiphertextModulusCreationError {
150    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151        let err_str: &str = (*self).into();
152        write!(f, "{err_str}")
153    }
154}
155
156impl<Scalar: UnsignedInteger> CiphertextModulus<Scalar> {
157    pub const fn new_native() -> Self {
158        Self {
159            inner: CiphertextModulusInner::Native,
160            _scalar: PhantomData,
161        }
162    }
163
164    #[track_caller]
165    pub const fn try_new_power_of_2(
166        exponent: usize,
167    ) -> Result<Self, CiphertextModulusCreationError> {
168        if exponent > Scalar::BITS {
169            Err(CiphertextModulusCreationError::ModulusTooBig)
170        } else {
171            let res = if let Some(modulus) = 1u128.checked_shl(exponent as u32) {
172                let Some(non_zero_modulus) = NonZeroU128::new(modulus) else {
173                    panic!("Got zero modulus for CiphertextModulusInner::Custom variant")
174                };
175
176                Self {
177                    inner: CiphertextModulusInner::Custom(non_zero_modulus),
178                    _scalar: PhantomData,
179                }
180            } else {
181                assert!(exponent == 128);
182                assert!(Scalar::BITS == 128);
183                Self {
184                    inner: CiphertextModulusInner::Native,
185                    _scalar: PhantomData,
186                }
187            };
188            Ok(res.canonicalize())
189        }
190    }
191
192    #[track_caller]
193    pub const fn try_new(modulus: u128) -> Result<Self, CiphertextModulusCreationError> {
194        if Scalar::BITS < 128 && modulus > (1 << Scalar::BITS) {
195            Err(CiphertextModulusCreationError::ModulusTooBig)
196        } else {
197            let res = match modulus {
198                0 => Self::new_native(),
199                modulus => {
200                    let Some(non_zero_modulus) = NonZeroU128::new(modulus) else {
201                        panic!("Got zero modulus for CiphertextModulusInner::Custom variant")
202                    };
203                    Self {
204                        inner: CiphertextModulusInner::Custom(non_zero_modulus),
205                        _scalar: PhantomData,
206                    }
207                }
208            };
209            let canonicalized_result = res.canonicalize();
210            if Scalar::BITS > 64 && !canonicalized_result.is_compatible_with_native_modulus() {
211                return Err(CiphertextModulusCreationError::CustomModuli64BitsOrLessOnly);
212            }
213            Ok(canonicalized_result)
214        }
215    }
216
217    pub const fn canonicalize(self) -> Self {
218        match self.inner {
219            CiphertextModulusInner::Native => self,
220            CiphertextModulusInner::Custom(modulus) => {
221                if Scalar::BITS < 128 && modulus.get() == (1 << Scalar::BITS) {
222                    Self::new_native()
223                } else {
224                    self
225                }
226            }
227        }
228    }
229
230    /// # Panic
231    /// Panics if modulus is not able to fit in the associated Scalar type
232    #[track_caller]
233    pub const fn new(modulus: u128) -> Self {
234        let res = match modulus {
235            0 => Self::new_native(),
236            _ => match Self::try_new(modulus) {
237                Ok(ciphertext_modulus) => ciphertext_modulus,
238                Err(err) => panic!("{}", err.const_err_msg()),
239            },
240        };
241        res.canonicalize()
242    }
243
244    /// # Panic
245    /// Panics if the stored modulus is not a power of 2.
246    #[track_caller]
247    pub fn get_power_of_two_scaling_to_native_torus(&self) -> Scalar {
248        match self.inner {
249            CiphertextModulusInner::Native => Scalar::ONE,
250            CiphertextModulusInner::Custom(modulus) => {
251                assert!(
252                    modulus.is_power_of_two(),
253                    "Cannot get scaling for non power of two modulus {modulus}"
254                );
255                Scalar::ONE.wrapping_shl(Scalar::BITS as u32 - modulus.ilog2())
256            }
257        }
258    }
259
260    /// Depending on the Scalar type used in the call, this function will determine whether the
261    /// current modulus is the native modulus of the given Scalar type allowing for more efficient
262    /// implementations than can rely on wrapping arithmetic operations behavior to compute the
263    /// modulus.
264    pub const fn is_native_modulus(&self) -> bool {
265        matches!(self.inner, CiphertextModulusInner::Native)
266    }
267
268    /// Panics if the modulus is not a custom modulus
269    #[track_caller]
270    pub const fn get_custom_modulus(&self) -> u128 {
271        match self.inner {
272            CiphertextModulusInner::Native => {
273                panic!("Tried getting custom modulus from native modulus")
274            }
275            CiphertextModulusInner::Custom(modulus) => modulus.get(),
276        }
277    }
278
279    pub fn into_modulus_log(self) -> CiphertextModulusLog {
280        match self.inner {
281            CiphertextModulusInner::Native => CiphertextModulusLog(Scalar::BITS),
282            CiphertextModulusInner::Custom(custom_mod) => {
283                CiphertextModulusLog(custom_mod.get().ceil_ilog2() as usize)
284            }
285        }
286    }
287
288    pub fn get_custom_modulus_as_optional_scalar(&self) -> Option<Scalar> {
289        match self.inner {
290            CiphertextModulusInner::Native => None,
291            CiphertextModulusInner::Custom(modulus) => Some(modulus.get().cast_into()),
292        }
293    }
294
295    pub const fn is_compatible_with_native_modulus(&self) -> bool {
296        self.is_native_modulus() || self.is_power_of_two()
297    }
298
299    pub const fn is_non_native_power_of_two(&self) -> bool {
300        match self.inner {
301            CiphertextModulusInner::Native => false,
302            CiphertextModulusInner::Custom(modulus) => modulus.is_power_of_two(),
303        }
304    }
305
306    pub const fn is_power_of_two(&self) -> bool {
307        match self.inner {
308            CiphertextModulusInner::Native => true,
309            CiphertextModulusInner::Custom(modulus) => modulus.is_power_of_two(),
310        }
311    }
312
313    pub fn try_to<ScalarTo: UnsignedInteger + CastInto<u128>>(
314        &self,
315    ) -> Result<CiphertextModulus<ScalarTo>, &'static str> {
316        let error_msg = "failed to convert ciphertext modulus";
317
318        let new_inner = match self.inner {
319            CiphertextModulusInner::Native => match ScalarTo::BITS.cmp(&Scalar::BITS) {
320                Ordering::Greater => {
321                    CiphertextModulusInner::Custom(NonZeroU128::new(1u128 << Scalar::BITS).unwrap())
322                }
323                Ordering::Equal => CiphertextModulusInner::Native,
324                Ordering::Less => {
325                    return Err(error_msg);
326                }
327            },
328            CiphertextModulusInner::Custom(v) => {
329                let max = NonZeroU128::new(ScalarTo::MAX.cast_into()).unwrap();
330                if v <= max {
331                    CiphertextModulusInner::Custom(v)
332                } else if v.is_power_of_two() && v.ilog2() as usize == ScalarTo::BITS {
333                    CiphertextModulusInner::Native
334                } else {
335                    return Err(error_msg);
336                }
337            }
338        };
339
340        Ok(CiphertextModulus {
341            inner: new_inner,
342            _scalar: PhantomData,
343        }
344        .canonicalize())
345    }
346
347    pub const fn kind(&self) -> CiphertextModulusKind {
348        match self.inner {
349            CiphertextModulusInner::Native => CiphertextModulusKind::Native,
350            CiphertextModulusInner::Custom(modulus) => {
351                if modulus.is_power_of_two() {
352                    CiphertextModulusKind::NonNativePowerOfTwo
353                } else {
354                    CiphertextModulusKind::Other
355                }
356            }
357        }
358    }
359
360    pub fn raw_modulus_float(&self) -> f64 {
361        match self.inner {
362            CiphertextModulusInner::Native => 2_f64.powi(Scalar::BITS as i32),
363            CiphertextModulusInner::Custom(non_zero) => non_zero.get() as f64,
364        }
365    }
366
367    pub const fn associated_scalar_bits(&self) -> usize {
368        Scalar::BITS
369    }
370}
371
372impl<Scalar: UnsignedInteger> From<CiphertextModulus<Scalar>> for CiphertextModulusLog {
373    fn from(value: CiphertextModulus<Scalar>) -> Self {
374        value.into_modulus_log()
375    }
376}
377
378impl<Scalar: UnsignedInteger> std::fmt::Display for CiphertextModulus<Scalar> {
379    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
380        match self.inner {
381            CiphertextModulusInner::Native => write!(f, "CiphertextModulus(2^{})", Scalar::BITS),
382            CiphertextModulusInner::Custom(modulus) => {
383                write!(f, "CiphertextModulus({})", modulus.get())
384            }
385        }
386    }
387}
388
389impl<Scalar: UnsignedInteger> std::fmt::Debug for CiphertextModulus<Scalar> {
390    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
391        <Self as std::fmt::Display>::fmt(self, f)
392    }
393}
394
395#[cfg(test)]
396mod tests {
397    use super::CiphertextModulusCreationError;
398    use crate::core_crypto::prelude::CiphertextModulus;
399
400    #[test]
401    fn test_modulus_struct() {
402        assert!(std::mem::size_of::<CiphertextModulus<u32>>() == std::mem::size_of::<u128>());
403        assert!(std::mem::size_of::<CiphertextModulus<u64>>() == std::mem::size_of::<u128>());
404        assert!(std::mem::size_of::<CiphertextModulus<u128>>() == std::mem::size_of::<u128>());
405        assert!(std::mem::align_of::<CiphertextModulus<u32>>() == std::mem::align_of::<u128>());
406        assert!(std::mem::align_of::<CiphertextModulus<u64>>() == std::mem::align_of::<u128>());
407        assert!(std::mem::align_of::<CiphertextModulus<u128>>() == std::mem::align_of::<u128>());
408
409        {
410            let mod_32 = CiphertextModulus::<u32>::try_new_power_of_2(32).unwrap();
411
412            assert!(mod_32.is_native_modulus());
413
414            let std_fmt = format!("{mod_32}");
415            assert_eq!(&std_fmt, "CiphertextModulus(2^32)");
416
417            let dbg_fmt = format!("{mod_32:?}");
418            assert_eq!(&dbg_fmt, "CiphertextModulus(2^32)");
419        }
420
421        {
422            let bad_mod_32 = CiphertextModulus::<u32>::try_new_power_of_2(64);
423            assert!(bad_mod_32.is_err());
424            match bad_mod_32 {
425                Ok(_) => unreachable!(),
426                Err(e) => assert_eq!(e, CiphertextModulusCreationError::ModulusTooBig),
427            }
428        }
429
430        {
431            let native_mod_128 = CiphertextModulus::<u128>::new_native();
432            assert!(native_mod_128.is_native_modulus());
433
434            let ser = bincode::serialize(&native_mod_128).unwrap();
435            let deser: CiphertextModulus<u128> = bincode::deserialize(&ser).unwrap();
436
437            assert_eq!(native_mod_128, deser);
438
439            let deser_error: Result<CiphertextModulus<u32>, _> = bincode::deserialize(&ser);
440            assert!(deser_error.is_err());
441            match deser_error {
442                Ok(_) => unreachable!(),
443                Err(e) => match *e {
444                    bincode::ErrorKind::Custom(err) => {
445                        assert_eq!(
446                            err.as_str(),
447                            "Expected an unsigned integer with 32 bits, \
448                    found 128 bits during deserialization of CiphertextModulus, \
449                    have you mixed types during deserialization?",
450                        );
451                    }
452                    _ => unreachable!(),
453                },
454            }
455        }
456
457        {
458            let mod_128 = CiphertextModulus::<u128>::try_new_power_of_2(64).unwrap();
459
460            assert_eq!(mod_128.get_custom_modulus(), 1 << 64);
461
462            let ser = bincode::serialize(&mod_128).unwrap();
463            let deser: CiphertextModulus<u128> = bincode::deserialize(&ser).unwrap();
464
465            assert_eq!(mod_128, deser);
466
467            let deser_error: Result<CiphertextModulus<u32>, _> = bincode::deserialize(&ser);
468            assert!(deser_error.is_err());
469            match deser_error {
470                Ok(_) => unreachable!(),
471                Err(e) => match *e {
472                    bincode::ErrorKind::Custom(err) => {
473                        assert_eq!(
474                            err.as_str(),
475                            "Expected an unsigned integer with 32 bits, \
476                    found 128 bits during deserialization of CiphertextModulus, \
477                    have you mixed types during deserialization?",
478                        );
479                    }
480                    _ => unreachable!(),
481                },
482            }
483        }
484    }
485
486    #[test]
487    fn test_modulus_casting() {
488        // Native (u64 -> u64) => Native
489        let native_mod = CiphertextModulus::<u64>::try_new_power_of_2(64).unwrap();
490        assert!(native_mod.is_native_modulus());
491        let converted: CiphertextModulus<u64> = native_mod.try_to().unwrap();
492
493        assert!(converted.is_native_modulus());
494
495        // Native (u64 -> u128) => Custom
496        let native_mod = CiphertextModulus::<u64>::try_new_power_of_2(64).unwrap();
497        let converted: CiphertextModulus<u128> = native_mod.try_to().unwrap();
498
499        assert!(!converted.is_native_modulus());
500        assert_eq!(converted.get_custom_modulus(), 1u128 << 64);
501
502        // Native(u64 -> u32) => Impossible
503        let native_mod = CiphertextModulus::<u64>::try_new_power_of_2(64).unwrap();
504        let converted: Result<CiphertextModulus<u32>, _> = native_mod.try_to();
505        assert!(converted.is_err());
506
507        // Custom(u64 -> u64) => Custom
508        let custom_mod = CiphertextModulus::<u64>::try_new(64).unwrap();
509        assert!(!custom_mod.is_native_modulus());
510        let converted: CiphertextModulus<u64> = custom_mod.try_to().unwrap();
511
512        assert!(!converted.is_native_modulus());
513        assert_eq!(converted.get_custom_modulus(), 64);
514
515        // Custom(u64[with value == 2**32] -> u32) => Native
516        let custom_mod = CiphertextModulus::<u64>::try_new_power_of_2(32).unwrap();
517        assert!(!custom_mod.is_native_modulus());
518        let converted: CiphertextModulus<u32> = custom_mod.try_to().unwrap();
519        assert!(converted.is_native_modulus());
520
521        // Custom (u64[with value > 2**32] -> u32) => Impossible
522        let custom_mod = CiphertextModulus::<u64>::try_new(1 << 48).unwrap();
523        assert!(!custom_mod.is_native_modulus());
524        let converted: Result<CiphertextModulus<u32>, _> = custom_mod.try_to();
525        assert!(converted.is_err());
526
527        // Custom (u64[with value < 2**32] -> u32) => Custom
528        let custom_mod = CiphertextModulus::<u64>::try_new(1 << 21).unwrap();
529        assert!(!custom_mod.is_native_modulus());
530        let converted: CiphertextModulus<u32> = custom_mod.try_to().unwrap();
531        assert!(!converted.is_native_modulus());
532        assert_eq!(converted.get_custom_modulus(), 1 << 21);
533    }
534}