diff --git a/Cargo.toml b/Cargo.toml
index 4ac6218..fb6e8e1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,12 +7,13 @@ autobenches = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-qfall-math = { git = "https://github.com/qfall/math", rev="5f50c9cd31c869462d959774fb4b51fcd1727dbe" }
+qfall-math = { git = "https://github.com/qfall/math", branch = "dev" }
+flint-sys = "0.7.3"
sha2 = "0.10.6"
serde = {version="1.0", features=["derive"]}
serde_json = "1.0"
typetag = "0.2"
-criterion = { version = "0.7", features = ["html_reports"] }
+criterion = { version = "0.8", features = ["html_reports"] }
[profile.bench]
debug = true
diff --git a/src/primitive/psf/gpv.rs b/src/primitive/psf/gpv.rs
index 74fba71..2f22162 100644
--- a/src/primitive/psf/gpv.rs
+++ b/src/primitive/psf/gpv.rs
@@ -112,7 +112,7 @@ impl PSF for PSFGPV {
/// ```
fn samp_d(&self) -> MatZ {
let m = &self.gp.n * &self.gp.k + &self.gp.m_bar;
- MatZ::sample_d_common(&m, &self.gp.n, &self.s).unwrap()
+ MatZ::sample_discrete_gauss(&m, 1, 0, &self.s).unwrap()
}
/// Samples an `e` in the domain using SampleD with a short basis that is generated
@@ -157,14 +157,7 @@ impl PSF for PSFGPV {
let center = MatQ::from(&(-1 * &sol));
- sol + MatZ::sample_d_precomputed_gso(
- short_base,
- short_base_gso,
- &self.gp.n,
- ¢er,
- &self.s,
- )
- .unwrap()
+ sol + MatZ::sample_d_precomputed_gso(short_base, short_base_gso, ¢er, &self.s).unwrap()
}
/// Implements the efficiently computable function `f_a` which here corresponds to
diff --git a/src/primitive/psf/gpv_ring.rs b/src/primitive/psf/gpv_ring.rs
index 435ada4..60a77bc 100644
--- a/src/primitive/psf/gpv_ring.rs
+++ b/src/primitive/psf/gpv_ring.rs
@@ -117,7 +117,7 @@ impl PSF for PSFGPVRing {
/// ```
fn samp_d(&self) -> MatPolyOverZ {
let dimension = self.gp.modulus.get_degree() * (&self.gp.k + 2);
- let sample = MatZ::sample_d_common(dimension, &self.gp.n, &self.s).unwrap();
+ let sample = MatZ::sample_discrete_gauss(dimension, 1, 0, &self.s).unwrap();
MatPolyOverZ::from_coefficient_embedding((&sample, self.gp.modulus.get_degree() - 1))
}
@@ -205,7 +205,6 @@ impl PSF for PSFGPVRing {
+ MatPolyOverZ::sample_d(
&short_basis,
self.gp.modulus.get_degree(),
- &self.gp.n,
¢er_embedded,
&self.s,
)
diff --git a/src/primitive/psf/mp_perturbation.rs b/src/primitive/psf/mp_perturbation.rs
index b902309..c5c65e4 100644
--- a/src/primitive/psf/mp_perturbation.rs
+++ b/src/primitive/psf/mp_perturbation.rs
@@ -186,14 +186,8 @@ pub(crate) fn randomized_nearest_plane_gadget(
// just as PSFGPV::samp_p
long_solution
- + MatZ::sample_d_precomputed_gso(
- short_basis_gadget,
- short_basis_gadget_gso,
- &psf.gp.n,
- ¢er,
- &s,
- )
- .unwrap()
+ + MatZ::sample_d_precomputed_gso(short_basis_gadget, short_basis_gadget_gso, ¢er, &s)
+ .unwrap()
}
impl PSF for PSFPerturbation {
@@ -269,7 +263,7 @@ impl PSF for PSFPerturbation {
/// ```
fn samp_d(&self) -> MatZ {
let m = &self.gp.n * &self.gp.k + &self.gp.m_bar;
- MatZ::sample_d_common(&m, &self.gp.n, &self.s * &self.r).unwrap()
+ MatZ::sample_discrete_gauss(m, 1, 0, &self.s * &self.r).unwrap()
}
/// Samples an `e` in the domain using SampleD that is generated
@@ -318,8 +312,7 @@ impl PSF for PSFPerturbation {
vec_u: &MatZq,
) -> MatZ {
// Sample perturbation p <- D_{ZZ^m, r * √Σ_2}
- let vec_p =
- MatZ::sample_d_common_non_spherical(&self.gp.n, mat_sqrt_sigma_2, &self.r).unwrap();
+ let vec_p = MatZ::sample_d_common_non_spherical(mat_sqrt_sigma_2, &self.r).unwrap();
// v = u - A * p
let vec_v = vec_u - mat_a * &vec_p;
diff --git a/src/sample/g_trapdoor/trapdoor_distribution.rs b/src/sample/g_trapdoor/trapdoor_distribution.rs
index 039022b..4c73539 100644
--- a/src/sample/g_trapdoor/trapdoor_distribution.rs
+++ b/src/sample/g_trapdoor/trapdoor_distribution.rs
@@ -114,7 +114,7 @@ impl TrapdoorDistributionRing for SampleZ {
let nr_cols = i64::try_from(nr_cols).unwrap();
let mut out_mat = MatPolyOverZ::new(1, nr_cols);
for j in 0..nr_cols {
- let sample = PolyOverZ::sample_discrete_gauss(n - 1, n, 0, s).unwrap();
+ let sample = PolyOverZ::sample_discrete_gauss(n - 1, 0, s).unwrap();
out_mat.set_entry(0, j, &sample).unwrap();
}
diff --git a/src/utils.rs b/src/utils.rs
index 8df1a87..4177430 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -12,4 +12,5 @@
pub mod common_encodings;
pub mod common_moduli;
+pub mod lossy_compression;
pub mod rotation_matrix;
diff --git a/src/utils/lossy_compression.rs b/src/utils/lossy_compression.rs
new file mode 100644
index 0000000..f393ecc
--- /dev/null
+++ b/src/utils/lossy_compression.rs
@@ -0,0 +1,392 @@
+// Copyright © 2025 Niklas Siemer
+//
+// This file is part of qFALL-tools.
+//
+// qFALL-tools is free software: you can redistribute it and/or modify it under
+// the terms of the Mozilla Public License Version 2.0 as published by the
+// Mozilla Foundation. See .
+
+//! Implements lossy (de-)compression as specified in ML-KEM.
+//!
+//! Reference:
+//! - \[1\] National Institute of Standards and Technology (2024).
+//! Module-Lattice-Based Key-Encapsulation Mechanism Standard.
+//! Federal Information Processing Standards Publication (FIPS 203).
+//!
+
+use flint_sys::fmpz_poly::fmpz_poly_set_coeff_fmpz;
+use qfall_math::{
+ integer::{MatPolyOverZ, PolyOverZ, Z},
+ integer_mod_q::{MatPolynomialRingZq, ModulusPolynomialRingZq, PolynomialRingZq},
+ traits::{GetCoefficient, MatrixDimensions, MatrixGetEntry, MatrixSetEntry, Pow},
+};
+
+/// This trait is implemented by data-structures, which may use lossy compression by dropping lower order bits
+/// as specified in [\[1\]]().
+pub trait LossyCompressionFIPS203 {
+ /// Defines the datatype that the compressed value will have.
+ type CompressedType;
+ /// Defines the type of the modulus object.
+ type ModulusType;
+
+ /// Compresses by keeping only `d` higher-order bits.
+ /// This function modifies the value of `self` directly.
+ ///
+ /// The function is specified by `Compress_d(x) := ⌈(2^d / q) * x⌋ mod 2^d`.
+ ///
+ /// Parameters:
+ /// - `d`: specifies the number of bits that is kept to represent values
+ ///
+ /// Returns a new instance of type [`Self::CompressedType`] containing the compressed coefficients with a loss-factor
+ /// defined by `q` and `d`.
+ ///
+ /// # Panics ...
+ /// - if `d` is smaller than `1`.
+ fn lossy_compress(&self, d: impl Into) -> Self::CompressedType;
+
+ /// Decompresses a previously compressed value by mapping it to the closest recoverable value over the ring `Z_q`.
+ /// This function modifies the value of `self` directly.
+ ///
+ /// The function is specified by `Decompress_d(y) := ⌈(q / 2^d) * y⌋`.
+ ///
+ /// Parameters:
+ /// - `d`: specifies the number of bits that was kept during compression
+ ///
+ /// Returns a new instance of type [`Self`] with decompressed values according to the loss-factor
+ /// defined by `q` and `d`.
+ ///
+ /// # Panics ...
+ /// - if `d` is smaller than `1`.
+ fn lossy_decompress(
+ compressed: &Self::CompressedType,
+ d: impl Into,
+ modulus: &Self::ModulusType,
+ ) -> Self;
+}
+
+impl LossyCompressionFIPS203 for PolynomialRingZq {
+ type CompressedType = PolyOverZ;
+ type ModulusType = ModulusPolynomialRingZq;
+
+ /// Compresses by keeping only `d` higher-order bits.
+ /// This function modifies the value of `self` directly.
+ ///
+ /// The function is specified by `Compress_d(x) := ⌈(2^d / q) * x⌋ mod 2^d`.
+ ///
+ /// Parameters:
+ /// - `d`: specifies the number of bits that is kept to represent each value
+ ///
+ /// Returns a [`PolyOverZ`] containing the compressed coefficients with a loss-factor
+ /// defined by `q` and `d`.
+ ///
+ /// # Examples
+ /// ```
+ /// use qfall_math::integer_mod_q::PolynomialRingZq;
+ /// use qfall_tools::utils::{common_moduli::new_anticyclic, lossy_compression::LossyCompressionFIPS203};
+ ///
+ /// let modulus = new_anticyclic(16, 257).unwrap();
+ /// let mut poly = PolynomialRingZq::sample_uniform(&modulus);
+ ///
+ /// let compressed = poly.lossy_compress(4);
+ /// ```
+ ///
+ /// # Panics ...
+ /// - if `d` is smaller than `1`.
+ fn lossy_compress(&self, d: impl Into) -> Self::CompressedType {
+ let d = d.into();
+ assert!(d >= Z::ONE, "Performing this function with d < 1 implies reducing mod 1, leaving no information to recover. Choose a larger parameter d.");
+ let two_pow_d = Z::from(2).pow(d).unwrap();
+ let q = self.get_mod().get_q();
+ let q_div_2 = q.div_floor(2);
+
+ let mut out = PolyOverZ::default();
+
+ for coeff_i in 0..=self.get_degree() {
+ let mut coeff: Z = unsafe { self.get_coeff_unchecked(coeff_i) };
+
+ coeff *= &two_pow_d;
+ coeff += &q_div_2;
+ let mut res = coeff.div_floor(&q) % &two_pow_d;
+
+ unsafe {
+ fmpz_poly_set_coeff_fmpz(out.get_fmpz_poly_struct(), coeff_i, res.get_fmpz());
+ };
+ }
+
+ out
+ }
+
+ /// Decompresses a previously compressed value by mapping it to the closest recoverable value over the ring `Z_q`.
+ /// This function modifies the value of `self` directly.
+ ///
+ /// The function is specified by `Decompress_d(y) := ⌈(q / 2^d) * y⌋`.
+ ///
+ /// Parameters:
+ /// - `compressed`: specifies the compressed value
+ /// - `d`: specifies the number of bits that was kept during compression
+ /// - `modulus`: specifies the modulus of the returned value
+ ///
+ /// Returns a [`PolynomialRingZq`] with decompressed values according to the loss-factor
+ /// defined by `q` and `d`.
+ ///
+ /// # Examples
+ /// ```
+ /// use qfall_math::integer_mod_q::PolynomialRingZq;
+ /// use qfall_tools::utils::{common_moduli::new_anticyclic, lossy_compression::LossyCompressionFIPS203};
+ ///
+ /// let modulus = new_anticyclic(16, 257).unwrap();
+ /// let mut poly = PolynomialRingZq::sample_uniform(&modulus);
+ ///
+ /// let compressed = poly.lossy_compress(4);
+ /// let decompressed = PolynomialRingZq::lossy_decompress(&compressed, 4, &modulus);
+ /// ```
+ ///
+ /// # Panics ...
+ /// - if `d` is smaller than `1`.
+ fn lossy_decompress(
+ compressed: &Self::CompressedType,
+ d: impl Into,
+ modulus: &Self::ModulusType,
+ ) -> Self {
+ let d = d.into();
+ assert!(d >= Z::ONE, "Performing this function with d < 1 implies reducing mod 1, leaving no information to recover. Choose a larger parameter d.");
+ let two_pow_d_minus_1 = Z::from(2).pow(d - 1).unwrap();
+ let two_pow_d = &two_pow_d_minus_1 * 2;
+ let q = modulus.get_q();
+
+ let mut out = Self::from(modulus);
+
+ for coeff_i in 0..=compressed.get_degree() {
+ let mut coeff: Z = unsafe { compressed.get_coeff_unchecked(coeff_i) };
+
+ coeff *= &q;
+ coeff += &two_pow_d_minus_1;
+ let mut res = coeff.div_floor(&two_pow_d);
+
+ unsafe {
+ fmpz_poly_set_coeff_fmpz(out.get_fmpz_poly_struct(), coeff_i, res.get_fmpz());
+ };
+ }
+
+ out
+ }
+}
+
+impl LossyCompressionFIPS203 for MatPolynomialRingZq {
+ type CompressedType = MatPolyOverZ;
+ type ModulusType = ModulusPolynomialRingZq;
+
+ /// Compresses by keeping only `d` higher-order bits.
+ /// This function modifies the value of `self` directly.
+ ///
+ /// The function is specified by `Compress_d(x) := ⌈(2^d / q) * x⌋ mod 2^d`.
+ ///
+ /// Parameters:
+ /// - `d`: specifies the number of bits that is kept to represent each value
+ ///
+ /// Returns a [`MatPolyOverZ`] containing the compressed coefficients with a loss-factor
+ /// defined by `q` and `d`.
+ ///
+ /// # Examples
+ /// ```
+ /// use qfall_math::integer_mod_q::MatPolynomialRingZq;
+ /// use qfall_tools::utils::{common_moduli::new_anticyclic, lossy_compression::LossyCompressionFIPS203};
+ ///
+ /// let modulus = new_anticyclic(16, 257).unwrap();
+ /// let mut poly = MatPolynomialRingZq::sample_uniform(2, 3, &modulus);
+ ///
+ /// let compressed = poly.lossy_compress(4);
+ /// ```
+ ///
+ /// # Panics ...
+ /// - if `d` is smaller than `1`.
+ fn lossy_compress(&self, d: impl Into) -> Self::CompressedType {
+ let d = d.into();
+
+ let mut out = MatPolyOverZ::new(self.get_num_rows(), self.get_num_columns());
+
+ for row in 0..self.get_num_rows() {
+ for col in 0..self.get_num_columns() {
+ let entry: PolynomialRingZq = unsafe { self.get_entry_unchecked(row, col) };
+ let res = entry.lossy_compress(&d);
+ unsafe { out.set_entry_unchecked(row, col, res) };
+ }
+ }
+
+ out
+ }
+
+ /// Decompresses a previously compressed value by mapping it to the closest recoverable value over the ring `Z_q`.
+ /// This function modifies the value of `self` directly.
+ ///
+ /// The function is specified by `Decompress_d(y) := ⌈(q / 2^d) * y⌋`.
+ ///
+ /// Parameters:
+ /// - `compressed`: specifies the compressed matrix
+ /// - `d`: specifies the number of bits that was kept during compression
+ /// - `modulus`: specifies the modulus of the returned value
+ ///
+ /// Returns a [`MatPolynomialRingZq`] with decompressed values according to the loss-factor
+ /// defined by `q` and `d`.
+ ///
+ /// # Examples
+ /// ```
+ /// use qfall_math::integer_mod_q::MatPolynomialRingZq;
+ /// use qfall_tools::utils::{common_moduli::new_anticyclic, lossy_compression::LossyCompressionFIPS203};
+ ///
+ /// let modulus = new_anticyclic(16, 257).unwrap();
+ /// let mut poly = MatPolynomialRingZq::sample_uniform(2, 3, &modulus);
+ ///
+ /// let compressed = poly.lossy_compress(4);
+ /// let decompressed = MatPolynomialRingZq::lossy_decompress(&compressed, 4, &modulus);
+ /// ```
+ ///
+ /// # Panics ...
+ /// - if `d` is smaller than `1`.
+ fn lossy_decompress(
+ compressed: &Self::CompressedType,
+ d: impl Into,
+ modulus: &Self::ModulusType,
+ ) -> Self {
+ let d = d.into();
+
+ let mut out = MatPolynomialRingZq::new(
+ compressed.get_num_rows(),
+ compressed.get_num_columns(),
+ modulus,
+ );
+
+ for row in 0..compressed.get_num_rows() {
+ for col in 0..compressed.get_num_columns() {
+ let entry: PolyOverZ = unsafe { compressed.get_entry_unchecked(row, col) };
+ let res = PolynomialRingZq::lossy_decompress(&entry, &d, modulus);
+ unsafe { out.set_entry_unchecked(row, col, res) };
+ }
+ }
+
+ out
+ }
+}
+
+#[cfg(test)]
+mod test_compression_poly {
+ use crate::utils::{common_moduli::new_anticyclic, lossy_compression::LossyCompressionFIPS203};
+ use qfall_math::{
+ integer::Z,
+ integer_mod_q::PolynomialRingZq,
+ traits::{Distance, GetCoefficient, Pow},
+ };
+
+ /// Ensures that decompressing compressed values results in values close to the original for small d.
+ #[test]
+ fn round_trip_small_d() {
+ let d = 4;
+ let q = Z::from(257);
+ let modulus = new_anticyclic(16, &q).unwrap();
+ let poly = PolynomialRingZq::sample_uniform(&modulus);
+
+ let compressed = poly.lossy_compress(d);
+ let decompressed = PolynomialRingZq::lossy_decompress(&compressed, d, &modulus);
+
+ for i in 0..modulus.get_degree() {
+ let orig_coeff: Z = poly.get_coeff(i).unwrap();
+ let coeff: Z = decompressed.get_coeff(i).unwrap();
+
+ let mut distance = orig_coeff.distance(coeff);
+ if distance > &q / 2 {
+ distance = &q - distance;
+ }
+
+ assert!(distance <= Z::from(2).pow(q.log_ceil(2).unwrap() - d - 1).unwrap());
+ }
+ }
+
+ /// Ensures that decompressing compressed values results in values close to the original.
+ #[test]
+ fn round_trip() {
+ let d = 11;
+ let q = Z::from(3329);
+ let modulus = new_anticyclic(16, &q).unwrap();
+ let poly = PolynomialRingZq::sample_uniform(&modulus);
+
+ let compressed = poly.lossy_compress(d);
+ let decompressed = PolynomialRingZq::lossy_decompress(&compressed, d, &modulus);
+
+ for i in 0..modulus.get_degree() {
+ let orig_coeff: Z = poly.get_coeff(i).unwrap();
+ let coeff: Z = decompressed.get_coeff(i).unwrap();
+
+ let mut distance = orig_coeff.distance(coeff);
+ if distance > &q / 2 {
+ distance = &q - distance;
+ }
+
+ assert!(distance <= Z::from(2).pow(q.log_ceil(2).unwrap() - d - 1).unwrap());
+ }
+ }
+
+ /// Ensures that the function panics if `d = 0` or smaller.
+ #[test]
+ #[should_panic]
+ fn too_small_d() {
+ let d = 0;
+ let q = Z::from(3329);
+ let modulus = new_anticyclic(16, &q).unwrap();
+ let poly = PolynomialRingZq::sample_uniform(&modulus);
+
+ poly.lossy_compress(d);
+ }
+}
+
+#[cfg(test)]
+mod test_compression_matrix {
+ use crate::utils::{common_moduli::new_anticyclic, lossy_compression::LossyCompressionFIPS203};
+ use qfall_math::{
+ integer::Z,
+ integer_mod_q::{MatPolynomialRingZq, PolynomialRingZq},
+ traits::{Distance, GetCoefficient, MatrixDimensions, MatrixGetEntry, Pow},
+ };
+
+ /// Ensures that decompressing compressed values results in values close to the original.
+ #[test]
+ fn round_trip() {
+ let d = 11;
+ let q = Z::from(3329);
+ let modulus = new_anticyclic(16, &q).unwrap();
+ let matrix = MatPolynomialRingZq::sample_uniform(2, 2, &modulus);
+
+ let compressed = matrix.lossy_compress(d);
+ let decompressed = MatPolynomialRingZq::lossy_decompress(&compressed, d, &modulus);
+
+ for row in 0..matrix.get_num_rows() {
+ for col in 0..matrix.get_num_columns() {
+ let orig_entry: PolynomialRingZq = matrix.get_entry(row, col).unwrap();
+ let entry: PolynomialRingZq = decompressed.get_entry(row, col).unwrap();
+
+ for i in 0..modulus.get_degree() {
+ let orig_coeff: Z = orig_entry.get_coeff(i).unwrap();
+ let coeff: Z = entry.get_coeff(i).unwrap();
+
+ let mut distance = orig_coeff.distance(coeff);
+ if distance > &q / 2 {
+ distance = &q - distance;
+ }
+
+ assert!(distance <= Z::from(2).pow(q.log_ceil(2).unwrap() - d - 1).unwrap());
+ }
+ }
+ }
+ }
+
+ /// Ensures that the function panics if `d = 0` or smaller.
+ #[test]
+ #[should_panic]
+ fn too_small_d() {
+ let d = 0;
+ let q = Z::from(3329);
+ let modulus = new_anticyclic(16, &q).unwrap();
+ let poly = MatPolynomialRingZq::sample_uniform(2, 3, &modulus);
+
+ poly.lossy_compress(d);
+ }
+}