diff --git a/benches/benchmarks.rs b/benches/benchmarks.rs
index 080f0b8..8a0c4be 100644
--- a/benches/benchmarks.rs
+++ b/benches/benchmarks.rs
@@ -10,6 +10,7 @@
use criterion::criterion_main;
pub mod pfdh;
+pub mod psf;
pub mod regev;
-criterion_main! {regev::benches, pfdh::benches}
+criterion_main! {regev::benches, pfdh::benches, psf::benches}
diff --git a/benches/psf.rs b/benches/psf.rs
new file mode 100644
index 0000000..bfe09cd
--- /dev/null
+++ b/benches/psf.rs
@@ -0,0 +1,100 @@
+// Copyright © 2025 Niklas Siemer
+//
+// This file is part of qFALL-crypto.
+//
+// qFALL-crypto 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 .
+
+use criterion::{criterion_group, Criterion};
+use qfall_crypto::{
+ primitive::psf::{PSFPerturbation, PSF, PSFGPV},
+ sample::g_trapdoor::gadget_parameters::GadgetParameters,
+};
+use qfall_math::{integer_mod_q::MatZq, rational::Q};
+
+/// Benchmark [bench_psf] with `n = 8`.
+///
+/// This benchmark can be run with for example:
+/// - `cargo criterion PSF\ GPV\ n=8`
+/// - `cargo bench --bench benchmarks PSF\ GPV\ n=8`
+/// - `cargo flamegraph --bench benchmarks -- --bench PSF\ GPV\ n=8`
+///
+/// Shorter variants or regex expressions can also be used to specify the
+/// benchmark name. The `\ ` is used to escape the space, alternatively,
+/// quotation marks can be used.
+fn bench_psf(c: &mut Criterion) {
+ let (n, q) = (8, 128);
+
+ let psf = PSFGPV {
+ gp: GadgetParameters::init_default(n, q),
+ // multiply with the rounding parameter from next test to have the same samplign parameter
+ s: Q::from(30) * Q::from(n).log(2).unwrap(),
+ };
+
+ let target = MatZq::sample_uniform(n, 1, q);
+ let (a, r) = psf.trap_gen();
+
+ c.bench_function("PSF GPV n=8", |b| b.iter(|| psf.samp_p(&a, &r, &target)));
+}
+
+/// Benchmark [bench_psf_perturbation] with `n = 8`.
+///
+/// This benchmark can be run with for example:
+/// - `cargo criterion PSF\ Perturbation\ n=8`
+/// - `cargo bench --bench benchmarks PSF\ Perturbation\ n=8`
+/// - `cargo flamegraph --bench benchmarks -- --bench PSF\ Perturbation\ n=8`
+///
+/// Shorter variants or regex expressions can also be used to specify the
+/// benchmark name. The `\ ` is used to escape the space, alternatively,
+/// quotation marks can be used.
+fn bench_psf_perturbation(c: &mut Criterion) {
+ let (n, q) = (8, 128);
+
+ let psf = PSFPerturbation {
+ gp: GadgetParameters::init_default(n, q),
+ s: Q::from(30),
+ r: Q::from(n).log(2).unwrap(),
+ };
+
+ let target = MatZq::sample_uniform(n, 1, q);
+ let (a, r) = psf.trap_gen();
+
+ c.bench_function("PSF Perturbation n=8", |b| {
+ b.iter(|| psf.samp_p(&a, &r, &target))
+ });
+}
+
+/// Benchmark [bench_psf_perturbation] with `n = 64`.
+///
+/// This benchmark can be run with for example:
+/// - `cargo criterion PSF\ Perturbation\ n=64`
+/// - `cargo bench --bench benchmarks PSF\ Perturbation\ n=64`
+/// - `cargo flamegraph --bench benchmarks -- --bench PSF\ Perturbation\ n=64`
+///
+/// Shorter variants or regex expressions can also be used to specify the
+/// benchmark name. The `\ ` is used to escape the space, alternatively,
+/// quotation marks can be used.
+fn bench_psf_perturbation_larger(c: &mut Criterion) {
+ let (n, q) = (64, 128);
+
+ let psf = PSFPerturbation {
+ gp: GadgetParameters::init_default(n, q),
+ s: Q::from(100),
+ r: Q::from(n).log(2).unwrap(),
+ };
+
+ let target = MatZq::sample_uniform(n, 1, q);
+ let (a, r) = psf.trap_gen();
+
+ c.bench_function("PSF Perturbation n=64", |b| {
+ b.iter(|| psf.samp_p(&a, &r, &target))
+ });
+}
+
+criterion_group!(
+ benches,
+ bench_psf,
+ bench_psf_perturbation,
+ bench_psf_perturbation_larger,
+);
diff --git a/src/primitive/psf.rs b/src/primitive/psf.rs
index 3502a18..33cdce4 100644
--- a/src/primitive/psf.rs
+++ b/src/primitive/psf.rs
@@ -20,12 +20,18 @@
//! January. Implementation and evaluation of improved Gaussian sampling for lattice
//! trapdoors. In Proceedings of the 6th Workshop on Encrypted Computing & Applied
//! Homomorphic Cryptography (pp. 61-71).
+//! - \[3\] Peikert, Chris.
+//! An efficient and parallel Gaussian sampler for lattices.
+//! In: Annual Cryptology Conference - CRYPTO 2010.
+//! Springer, Berlin, Heidelberg.
mod gpv;
mod gpv_ring;
+mod mp_perturbation;
pub use gpv::PSFGPV;
pub use gpv_ring::PSFGPVRing;
+pub use mp_perturbation::PSFPerturbation;
/// This trait should be implemented by all constructions that are
/// actual implementations of a preimage sampleable function.
diff --git a/src/primitive/psf/mp_perturbation.rs b/src/primitive/psf/mp_perturbation.rs
new file mode 100644
index 0000000..ac99c94
--- /dev/null
+++ b/src/primitive/psf/mp_perturbation.rs
@@ -0,0 +1,562 @@
+// Copyright © 2025 Niklas Siemer
+//
+// This file is part of qFALL-crypto.
+//
+// qFALL-crypto 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 a Perturbation MP12 PSF according to [\[1\]](<../index.html#:~:text=[1]>)
+//! using G-Trapdoors and corresponding trapdoor.
+
+use super::PSF;
+use crate::sample::g_trapdoor::{
+ gadget_classical::short_basis_gadget,
+ gadget_classical::{find_solution_gadget_mat, gen_trapdoor},
+ gadget_parameters::GadgetParameters,
+};
+use qfall_math::{
+ integer::{MatZ, Z},
+ integer_mod_q::MatZq,
+ rational::{MatQ, Q},
+ traits::{Concatenate, MatrixDimensions, Pow},
+};
+use serde::{Deserialize, Serialize};
+
+/// A lattice-based implementation of a [`PSF`] according to
+/// [\[1\]]() using
+/// G-Trapdoors where D_n = {e ∈ Z^m | |e| <= s sqrt(m)}
+/// and R_n = Z_q^n.
+///
+/// Attributes
+/// - `gp`: Describes the gadget parameters with which the G-Trapdoor is generated
+/// - `r`: The rounding parameter
+/// - `s`: The Gaussian parameter with which is sampled
+///
+/// # Examples
+/// ```
+/// use qfall_crypto::primitive::psf::PSFPerturbation;
+/// use qfall_crypto::sample::g_trapdoor::gadget_parameters::GadgetParameters;
+/// use qfall_math::rational::Q;
+/// use qfall_crypto::primitive::psf::PSF;
+///
+/// let psf = PSFPerturbation {
+/// gp: GadgetParameters::init_default(8, 64),
+/// r: Q::from(3),
+/// s: Q::from(25),
+/// };
+///
+/// let (a, td) = psf.trap_gen();
+/// let domain_sample = psf.samp_d();
+/// let range_fa = psf.f_a(&a, &domain_sample);
+/// let preimage = psf.samp_p(&a, &td, &range_fa);
+///
+/// assert!(psf.check_domain(&preimage));
+/// assert_eq!(a * preimage, range_fa);
+/// ```
+#[derive(Serialize, Deserialize)]
+pub struct PSFPerturbation {
+ pub gp: GadgetParameters,
+ pub r: Q,
+ pub s: Q,
+}
+
+impl PSFPerturbation {
+ /// Computes √Σ_2 = √(r^2 * (b^2 + 1) * [R^t | I]^t * [R^t | I] - r^2 * I)
+ /// to perform non-spherical Gaussian sampling according to Algorithm 1 in
+ /// [\[3\]]().
+ /// This matrix is the second part of the secret key and needs to be precomputed
+ /// to execute [PSFPerturbation::samp_p].
+ /// [PSFPerturbation::trap_gen] outputs this matrix for `s^2 * I`, i.e. for discrete
+ /// Gaussian preimages centered around `0`. This function enables changing the
+ /// covariance matrix to any covariance matrix s.t. Σ_2 is positive definite.
+ ///
+ /// Parameters:
+ /// - `mat_r`: The trapdoor matrix `R`
+ /// - `mat_sigma`: The covariance matrix `Σ` to sample [`Self::samp_p`] with
+ ///
+ /// Returns a [`MatQ`] containing √Σ_2 = √(r^2 * (b^2 + 1) * [R^t | I]^t * [R^t | I] - r^2 * I)
+ /// if Σ_2 was positive definite.
+ ///
+ /// # Examples
+ /// ```
+ /// use qfall_crypto::primitive::psf::PSFPerturbation;
+ /// use qfall_crypto::sample::g_trapdoor::gadget_parameters::GadgetParameters;
+ /// use qfall_math::rational::{Q, MatQ};
+ /// use qfall_crypto::primitive::psf::PSF;
+ /// use qfall_math::traits::*;
+ ///
+ /// let psf = PSFPerturbation {
+ /// gp: GadgetParameters::init_default(8, 64),
+ /// r: Q::from(3),
+ /// s: Q::from(25),
+ /// };
+ /// let different_s: f64 = 35.0;
+ ///
+ /// let (a, td) = psf.trap_gen();
+ ///
+ /// let cov_mat = different_s.powi(2) * MatQ::identity(a.get_num_columns(), a.get_num_columns());
+ /// let mat_sqrt_sigma_2 = psf.compute_sqrt_sigma_2(&td.0, &cov_mat);
+ /// let new_td = (td.0, mat_sqrt_sigma_2, td.2);
+ ///
+ /// let domain_sample = psf.samp_d();
+ /// let range_fa = psf.f_a(&a, &domain_sample);
+ /// let preimage = psf.samp_p(&a, &new_td, &range_fa);
+ ///
+ /// assert!(psf.check_domain(&preimage));
+ /// ```
+ ///
+ /// # Panics ...
+ /// - if Σ_2 is not positive definite.
+ pub fn compute_sqrt_sigma_2(&self, mat_r: &MatZ, mat_sigma: &MatQ) -> MatQ {
+ // Normalization factor according to MP12, Section 2.3
+ let normalization_factor = 1.0 / (2.0 * Q::PI);
+
+ // full_td = [R^t | I]^t
+ let full_td = mat_r
+ .concat_vertical(&MatZ::identity(
+ mat_sigma.get_num_rows() - mat_r.get_num_rows(),
+ mat_r.get_num_columns(),
+ ))
+ .unwrap();
+
+ // Assemble Σ_p = Σ - [R^t|I]^t * Σ_G * [R^t|I] with √Σ_G ≥ η (Λ^⊥(G))
+ // and assumption Σ_G = (base^2 + 1) * I as ||S|| = √(b^2 + 1), Theorem 4.1, MP12
+ let mat_sigma_p: MatQ =
+ mat_sigma - (self.gp.base.pow(2).unwrap() + 1) * &full_td * full_td.transpose();
+
+ // r^2 * Σ_p <=> r * √Σ_p
+ // Compute Σ_2 according to the requirements of Algorithm 1 in [3]
+ // Assume Σ_1 = r^2 * B_1 * B_1^t with B_1 = I as basis for ZZ^n
+ // Then, Σ_2 = Σ - Σ_1, where Σ = r^2 * Σ_p
+ let sigma_2: MatQ = normalization_factor
+ * self.r.pow(2).unwrap()
+ * (&mat_sigma_p
+ - MatQ::identity(mat_sigma_p.get_num_rows(), mat_sigma_p.get_num_columns()));
+
+ // Compute √Σ_2
+ sigma_2.cholesky_decomposition_flint()
+ }
+}
+
+/// Samples a preimage with respect to the gadget matrix `G` of `vec_u`.
+///
+/// Parameters:
+/// - `psf`: The [`PSFPerturbation`] that sets the parameters for this function
+/// - `vec_u`: The target vector
+/// - `short_basis_gadget`: The short basis of the corresponding gadget-matrix - just required to speed up the algorithm
+/// - `short_basis_gadget_gso`: The GSO of the short basis of the gadget-matrix - just required to speed up the algorithm
+///
+/// Returns a discrete Gaussian sampled preimage of `u` under `G` with Gaussian parameter `r * √(b^2 + 1)`,
+/// where `b` is the base used for the G-trapdoor.
+///
+/// # Examples
+/// ```compile_fail
+/// use qfall_crypto::primitive::psf::PSFPerturbation;
+/// use qfall_crypto::sample::g_trapdoor::gadget_parameters::GadgetParameters;
+/// use qfall_crypto::sample::g_trapdoor::gadget_classical::short_basis_gadget;
+/// use qfall_math::rational::Q;
+/// use qfall_crypto::primitive::psf::PSF;
+///
+/// let psf = PSFPerturbation {
+/// gp: GadgetParameters::init_default(8, 64),
+/// r: Q::from(3),
+/// s: Q::from(25),
+/// };
+///
+/// let short_basis_g = short_basis_gadget(&psf.gp);
+/// let short_basis_g_gso = MatQ::from(&short_basis_g).gso();
+///
+/// let target = MatZq::sample_uniform(8, 1, 64);
+/// let preimage = randomized_nearest_plane_gadget(&osf, &target, &short_basis_g, &short_basis_g_gso);
+/// ```
+pub(crate) fn randomized_nearest_plane_gadget(
+ psf: &PSFPerturbation,
+ vec_u: &MatZq,
+ short_basis_gadget: &MatZ,
+ short_basis_gadget_gso: &MatQ,
+) -> MatZ {
+ // s = r * √(b^2 + 1) according to Algorithm 3 in [1]
+ let s = &psf.r * (psf.gp.base.pow(2).unwrap() + Z::ONE).sqrt();
+
+ // find solution s.t. G * long_solution = vec_u
+ let long_solution = find_solution_gadget_mat(vec_u, &psf.gp.k, &psf.gp.base);
+
+ let center = MatQ::from(&(-1 * &long_solution));
+
+ // 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()
+}
+
+impl PSF for PSFPerturbation {
+ type A = MatZq;
+ type Trapdoor = (MatZ, MatQ, (MatZ, MatQ));
+ type Domain = MatZ;
+ type Range = MatZq;
+
+ /// Computes a G-Trapdoor according to the [`GadgetParameters`] defined in the struct.
+ /// It returns a matrix `A` together with the short trapdoor matrix `R` and a precomputed √Σ_2
+ /// for covariance matrix `s^2 * I`, i.e. for preimage sampling centered around `0`.
+ /// The last part of the trapdoor tuple contains a short basis for the gadget matrix `G` and its
+ /// GSO, which removes the need to recompute the GSO for each iteration and speeds up preimage sampling
+ /// drastically.
+ ///
+ /// # Examples
+ /// ```
+ /// use qfall_crypto::primitive::psf::PSFPerturbation;
+ /// use qfall_crypto::sample::g_trapdoor::gadget_parameters::GadgetParameters;
+ /// use qfall_math::rational::Q;
+ /// use qfall_crypto::primitive::psf::PSF;
+ ///
+ /// let psf = PSFPerturbation {
+ /// gp: GadgetParameters::init_default(8, 64),
+ /// r: Q::from(3),
+ /// s: Q::from(25),
+ /// };
+ ///
+ /// let (mat_a, (mat_r, mat_sqrt_sigma_2, (sh_b_g, sh_b_g_gso))) = psf.trap_gen();
+ /// ```
+ fn trap_gen(&self) -> (MatZq, (MatZ, MatQ, (MatZ, MatQ))) {
+ let mat_a_bar = MatZq::sample_uniform(&self.gp.n, &self.gp.m_bar, &self.gp.q);
+ let tag = MatZq::identity(&self.gp.n, &self.gp.n, &self.gp.q);
+
+ let (mat_a, mat_r) = gen_trapdoor(&self.gp, &mat_a_bar, &tag).unwrap();
+
+ let mat_sqrt_sigma_2 = self.compute_sqrt_sigma_2(
+ &mat_r,
+ &(&self.s.pow(2).unwrap()
+ * MatQ::identity(mat_a.get_num_columns(), mat_a.get_num_columns())),
+ );
+
+ let short_basis_gadget = short_basis_gadget(&self.gp);
+ let short_basis_gadget_gso = MatQ::from(&short_basis_gadget).gso();
+
+ (
+ mat_a,
+ (
+ mat_r,
+ mat_sqrt_sigma_2,
+ (short_basis_gadget, short_basis_gadget_gso),
+ ),
+ )
+ }
+
+ /// Samples in the domain using SampleD with the standard basis and center `0`.
+ ///
+ /// # Examples
+ /// ```
+ /// use qfall_crypto::primitive::psf::PSFPerturbation;
+ /// use qfall_crypto::sample::g_trapdoor::gadget_parameters::GadgetParameters;
+ /// use qfall_math::rational::Q;
+ /// use qfall_crypto::primitive::psf::PSF;
+ ///
+ /// let psf = PSFPerturbation {
+ /// gp: GadgetParameters::init_default(8, 64),
+ /// r: Q::from(3),
+ /// s: Q::from(25),
+ /// };
+ /// let (mat_a, td) = psf.trap_gen();
+ ///
+ /// let domain_sample = psf.samp_d();
+ /// ```
+ 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()
+ }
+
+ /// Samples an `e` in the domain using SampleD that is generated
+ /// from the G-Trapdoor from the conditioned
+ /// discrete Gaussian with `f_a(a,e) = u` for a provided syndrome `u`.
+ ///
+ /// *Note*: the provided parameters `mat_a, mat_r, vec_u` must fit together,
+ /// otherwise unexpected behavior such as panics may occur.
+ ///
+ /// Parameters:
+ /// - `mat_a`: The parity-check matrix
+ /// - `mat_r`: The short trapdoor matrix `R`
+ /// - `mat_sqrt_sigma_2`: The precomputed √Σ_2
+ /// - `vec_u`: The syndrome from the range
+ ///
+ /// Returns a sample `e` from the domain on the conditioned discrete
+ /// Gaussian distribution `f_a(a,e) = u` with covariance matrix Σ depending on `mat_sqrt_sigma_2`.
+ ///
+ /// # Examples
+ /// ```
+ /// use qfall_crypto::primitive::psf::PSFPerturbation;
+ /// use qfall_crypto::sample::g_trapdoor::gadget_parameters::GadgetParameters;
+ /// use qfall_math::rational::Q;
+ /// use qfall_crypto::primitive::psf::PSF;
+ ///
+ /// let psf = PSFPerturbation {
+ /// gp: GadgetParameters::init_default(8, 64),
+ /// r: Q::from(3),
+ /// s: Q::from(25),
+ /// };
+ /// let (mat_a, td) = psf.trap_gen();
+ /// let domain_sample = psf.samp_d();
+ /// let range_fa = psf.f_a(&mat_a, &domain_sample);
+ ///
+ /// let preimage = psf.samp_p(&mat_a, &td, &range_fa);
+ /// assert_eq!(range_fa, psf.f_a(&mat_a, &preimage))
+ /// ```
+ fn samp_p(
+ &self,
+ mat_a: &MatZq,
+ (mat_r, mat_sqrt_sigma_2, (short_basis_gadget, short_basis_gadget_gso)): &(
+ MatZ,
+ MatQ,
+ (MatZ, MatQ),
+ ),
+ 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();
+
+ // v = u - A * p
+ let vec_v = vec_u - mat_a * &vec_p;
+
+ // z <- D_{Λ_v^⊥(G), r * √Σ_G}
+ let vec_z = randomized_nearest_plane_gadget(
+ self,
+ &vec_v,
+ short_basis_gadget,
+ short_basis_gadget_gso,
+ );
+
+ let full_td = mat_r
+ .concat_vertical(&MatZ::identity(
+ mat_r.get_num_columns(),
+ mat_r.get_num_columns(),
+ ))
+ .unwrap();
+
+ vec_p + full_td * vec_z
+ }
+
+ /// Implements the efficiently computable function `f_a` which here corresponds to
+ /// `mat_a * sigma`. The sigma must be from the domain, i.e. D_n.
+ ///
+ /// Parameters:
+ /// - `mat_a`: The parity-check matrix of dimensions `n x m`
+ /// - `sigma`: A column vector of length `m`
+ ///
+ /// Returns `mat_a * sigma`.
+ ///
+ /// # Examples
+ /// ```
+ /// use qfall_crypto::primitive::psf::PSFPerturbation;
+ /// use qfall_crypto::sample::g_trapdoor::gadget_parameters::GadgetParameters;
+ /// use qfall_math::rational::Q;
+ /// use qfall_crypto::primitive::psf::PSF;
+ ///
+ /// let psf = PSFPerturbation {
+ /// gp: GadgetParameters::init_default(8, 64),
+ /// r: Q::from(3),
+ /// s: Q::from(25),
+ /// };
+ /// let (mat_a, td) = psf.trap_gen();
+ /// let domain_sample = psf.samp_d();
+ /// let range_fa = psf.f_a(&mat_a, &domain_sample);
+ /// ```
+ ///
+ /// # Panics ...
+ /// - if `sigma` is not in the domain.
+ fn f_a(&self, mat_a: &MatZq, sigma: &MatZ) -> MatZq {
+ assert!(self.check_domain(sigma));
+ mat_a * sigma
+ }
+
+ /// Checks whether a value `sigma` is in D_n = {e ∈ Z^m | |e| <= s * r * sqrt(m)}.
+ ///
+ /// Parameters:
+ /// - `sigma`: The value for which is checked, if it is in the domain
+ ///
+ /// Returns true, if `sigma` is in D_n.
+ ///
+ /// # Examples
+ /// ```
+ /// use qfall_crypto::primitive::psf::PSF;
+ /// use qfall_crypto::primitive::psf::PSFPerturbation;
+ /// use qfall_crypto::sample::g_trapdoor::gadget_parameters::GadgetParameters;
+ /// use qfall_math::rational::Q;
+ ///
+ /// let psf = PSFPerturbation {
+ /// gp: GadgetParameters::init_default(8, 64),
+ /// r: Q::from(3),
+ /// s: Q::from(25),
+ /// };
+ /// let (mat_a, td) = psf.trap_gen();
+ ///
+ /// let vector = psf.samp_d();
+ ///
+ /// assert!(psf.check_domain(&vector));
+ /// ```
+ fn check_domain(&self, sigma: &MatZ) -> bool {
+ let m = &self.gp.n * &self.gp.k + &self.gp.m_bar;
+ sigma.is_column_vector()
+ && m == sigma.get_num_rows()
+ && sigma.norm_eucl_sqrd().unwrap()
+ <= self.s.pow(2).unwrap() * &m * &self.r.pow(2).unwrap()
+ }
+}
+
+#[cfg(test)]
+mod test_psf_perturbation {
+ use super::PSFPerturbation;
+ use super::PSF;
+ use crate::sample::g_trapdoor::gadget_parameters::GadgetParameters;
+ use qfall_math::integer::MatZ;
+ use qfall_math::rational::Q;
+ use qfall_math::traits::*;
+
+ /// Ensures that `samp_d` actually computes values that are in D_n.
+ #[test]
+ fn samp_d_samples_from_dn() {
+ for (n, q) in [(5, 256), (10, 128), (15, 157)] {
+ let psf = PSFPerturbation {
+ gp: GadgetParameters::init_default(n, q),
+ r: Q::from(n).log(2).unwrap(),
+ s: Q::from(25),
+ };
+
+ for _ in 0..5 {
+ assert!(psf.check_domain(&psf.samp_d()));
+ }
+ }
+ }
+
+ /// Ensures that `samp_p` actually computes preimages that are also in the correct
+ /// domain.
+ #[test]
+ fn samp_p_preimage_and_domain() {
+ for (n, q) in [(5, 256), (6, 128)] {
+ let psf = PSFPerturbation {
+ gp: GadgetParameters::init_default(n, q),
+ r: Q::from(n).log(2).unwrap(),
+ s: Q::from(25),
+ };
+ let (a, r) = psf.trap_gen();
+ let domain_sample = psf.samp_d();
+ let range_fa = psf.f_a(&a, &domain_sample);
+
+ let preimage = psf.samp_p(&a, &r, &range_fa);
+ assert_eq!(range_fa, psf.f_a(&a, &preimage));
+ assert!(psf.check_domain(&preimage));
+ }
+ }
+
+ /// Ensures that `f_a` returns `a * sigma`.
+ #[test]
+ fn f_a_works_as_expected() {
+ for (n, q) in [(5, 256), (6, 128)] {
+ let psf = PSFPerturbation {
+ gp: GadgetParameters::init_default(n, q),
+ r: Q::from(n).log(2).unwrap(),
+ s: Q::from(25),
+ };
+ let (a, _) = psf.trap_gen();
+ let domain_sample = psf.samp_d();
+
+ assert_eq!(&a * &domain_sample, psf.f_a(&a, &domain_sample));
+ }
+ }
+
+ /// Ensures that `f_a` panics if a value is provided, that is not within the domain.
+ /// Sigma is not a vector.
+ #[test]
+ #[should_panic]
+ fn f_a_sigma_not_in_domain_matrix() {
+ let psf = PSFPerturbation {
+ gp: GadgetParameters::init_default(8, 128),
+ r: Q::from(8).log(2).unwrap(),
+ s: Q::from(25),
+ };
+ let (a, _) = psf.trap_gen();
+ let not_in_domain = MatZ::new(a.get_num_columns(), 2);
+
+ let _ = psf.f_a(&a, ¬_in_domain);
+ }
+
+ /// Ensures that `f_a` panics if a value is provided, that is not within the domain.
+ /// Sigma is not of the correct length.
+ #[test]
+ #[should_panic]
+ fn f_a_sigma_not_in_domain_incorrect_length() {
+ let psf = PSFPerturbation {
+ gp: GadgetParameters::init_default(8, 128),
+ r: Q::from(8).log(2).unwrap(),
+ s: Q::from(25),
+ };
+ let (a, _) = psf.trap_gen();
+ let not_in_domain = MatZ::new(a.get_num_columns() - 1, 1);
+
+ let _ = psf.f_a(&a, ¬_in_domain);
+ }
+
+ /// Ensures that `f_a` panics if a value is provided, that is not within the domain.
+ /// Sigma is too long.
+ #[test]
+ #[should_panic]
+ fn f_a_sigma_not_in_domain_too_long() {
+ let psf = PSFPerturbation {
+ gp: GadgetParameters::init_default(8, 128),
+ r: Q::from(8).log(2).unwrap(),
+ s: Q::from(25),
+ };
+ let (a, _) = psf.trap_gen();
+ let not_in_domain =
+ psf.s.round() * a.get_num_columns() * MatZ::identity(a.get_num_columns(), 1);
+
+ let _ = psf.f_a(&a, ¬_in_domain);
+ }
+
+ /// Ensures that `check_domain` works for vectors with the correct length.
+ #[test]
+ fn check_domain_as_expected() {
+ let psf = PSFPerturbation {
+ gp: GadgetParameters::init_default(8, 128),
+ r: Q::from(8).log(2).unwrap(),
+ s: Q::from(25),
+ };
+ let (a, _) = psf.trap_gen();
+ let value = psf.s.round();
+ let mut in_domain = MatZ::new(a.get_num_columns(), 1);
+ for i in 0..in_domain.get_num_rows() {
+ in_domain.set_entry(i, 0, &value).unwrap();
+ }
+
+ assert!(psf.check_domain(&MatZ::new(a.get_num_columns(), 1)));
+ assert!(psf.check_domain(&in_domain));
+ }
+
+ /// Ensures that `check_domain` returns false for values that are not in the domain.
+ #[test]
+ fn check_domain_not_in_dn() {
+ let psf = PSFPerturbation {
+ gp: GadgetParameters::init_default(8, 128),
+ r: Q::from(8).log(2).unwrap(),
+ s: Q::from(25),
+ };
+ let (a, _) = psf.trap_gen();
+
+ let matrix = MatZ::new(a.get_num_columns(), 2);
+ let too_short = MatZ::new(a.get_num_columns() - 1, 1);
+ let too_long = MatZ::new(a.get_num_columns() + 1, 1);
+ let entry_too_large =
+ psf.s.round() * a.get_num_columns() * MatZ::identity(a.get_num_columns(), 1);
+
+ assert!(!psf.check_domain(&matrix));
+ assert!(!psf.check_domain(&too_long));
+ assert!(!psf.check_domain(&too_short));
+ assert!(!psf.check_domain(&entry_too_large));
+ }
+}
diff --git a/src/sample/g_trapdoor/gadget_classical.rs b/src/sample/g_trapdoor/gadget_classical.rs
index a56b790..94dcc6a 100644
--- a/src/sample/g_trapdoor/gadget_classical.rs
+++ b/src/sample/g_trapdoor/gadget_classical.rs
@@ -228,6 +228,64 @@ pub fn find_solution_gadget_mat(value: &MatZq, k: &Z, base: &Z) -> MatZ {
out
}
+/// Outputs the short basis `S` of any gadget matrix `G`.
+///
+/// Parameters:
+/// - `params`: The [`GadgetParameters`] that define the gadget matrix `G`
+///
+/// Returns a [`MatZ`] a short basis matrix `S` for `G`.
+///
+/// # Examples
+/// ```
+/// use qfall_crypto::sample::g_trapdoor::{gadget_parameters::GadgetParameters,
+/// gadget_default::gen_trapdoor_default};
+/// use qfall_crypto::sample::g_trapdoor::gadget_classical::short_basis_gadget;
+///
+/// let params = GadgetParameters::init_default(10, 127);
+///
+/// let short_basis_gadget = short_basis_gadget(¶ms);
+/// ```
+pub fn short_basis_gadget(params: &GadgetParameters) -> MatZ {
+ let mut sk = MatZ::new(¶ms.k, ¶ms.k);
+ let n: i64 = (¶ms.n).try_into().unwrap();
+ let k: i64 = (¶ms.k).try_into().unwrap();
+ for j in 0..k {
+ sk.set_entry(j, j, ¶ms.base).unwrap();
+ }
+ for i in 0..k - 1 {
+ sk.set_entry(i + 1, i, Z::MINUS_ONE).unwrap();
+ }
+ sk = if params.base.pow(k).unwrap() == params.q {
+ // compute s in the special case where the modulus is a power of base
+ // i.e. the last column can remain as it is
+ sk
+ } else {
+ // compute s for any arbitrary modulus
+ // represent modulus in `base` and set last row accordingly
+ let mut q = Z::from(¶ms.q);
+ for i in 0..k {
+ let q_i = &q % ¶ms.base;
+ sk.set_entry(i, k - 1, &q_i).unwrap();
+ q = (q - q_i).div_exact(¶ms.base).unwrap();
+ }
+ sk
+ };
+ let mut out = MatZ::new(n * k, n * k);
+ for j in 0..n {
+ out.set_submatrix(
+ j * sk.get_num_rows(),
+ j * sk.get_num_columns(),
+ &sk,
+ 0,
+ 0,
+ -1,
+ -1,
+ )
+ .unwrap();
+ }
+ out
+}
+
#[cfg(test)]
mod test_gen_gadget_vec {
use crate::sample::g_trapdoor::gadget_classical::gen_gadget_vec;
@@ -420,3 +478,96 @@ mod test_find_solution_gadget {
)
}
}
+
+#[cfg(test)]
+mod test_short_basis_gadget {
+ use crate::sample::g_trapdoor::{
+ gadget_classical::short_basis_gadget, gadget_parameters::GadgetParameters,
+ };
+ use qfall_math::integer::{MatZ, Z};
+ use std::str::FromStr;
+
+ /// Ensure that the matrix s is computed correctly for a power-of-two modulus.
+ #[test]
+ fn base_2_power_two() {
+ let params = GadgetParameters::init_default(2, 16);
+
+ let s = short_basis_gadget(¶ms);
+
+ let s_cmp = MatZ::from_str(
+ "[[2, 0, 0, 0, 0, 0, 0, 0],\
+ [-1, 2, 0, 0, 0, 0, 0, 0],\
+ [0, -1, 2, 0, 0, 0, 0, 0],\
+ [0, 0, -1, 2, 0, 0, 0, 0],\
+ [0, 0, 0, 0, 2, 0, 0, 0],\
+ [0, 0, 0, 0, -1, 2, 0, 0],\
+ [0, 0, 0, 0, 0, -1, 2, 0],\
+ [0, 0, 0, 0, 0, 0, -1, 2]]",
+ )
+ .unwrap();
+ assert_eq!(s_cmp, s)
+ }
+
+ /// Ensure that the matrix s is computed correctly for a an arbitrary modulus.
+ #[test]
+ fn base_2_arbitrary() {
+ let q = Z::from(0b1100110);
+ let params = GadgetParameters::init_default(1, q);
+
+ let s = short_basis_gadget(¶ms);
+
+ let s_cmp = MatZ::from_str(
+ "[[2, 0, 0, 0, 0, 0, 0],\
+ [-1, 2, 0, 0, 0, 0, 1],\
+ [0, -1, 2, 0, 0, 0, 1],\
+ [0, 0, -1, 2, 0, 0, 0],\
+ [0, 0, 0, -1, 2, 0, 0],\
+ [0, 0, 0, 0, -1, 2, 1],\
+ [0, 0, 0, 0, 0, -1, 1]]",
+ )
+ .unwrap();
+
+ assert_eq!(s_cmp, s)
+ }
+
+ /// Ensure that the matrix s is computed correctly for a power-of-5 modulus.
+ #[test]
+ fn base_5_power_5() {
+ let mut params = GadgetParameters::init_default(1, 625);
+ params.k = Z::from(4);
+ params.base = Z::from(5);
+
+ let s = short_basis_gadget(¶ms);
+
+ let s_cmp = MatZ::from_str(
+ "[[5, 0, 0, 0],\
+ [-1, 5, 0, 0],\
+ [0, -1, 5, 0],\
+ [0, 0, -1, 5]]",
+ )
+ .unwrap();
+ assert_eq!(s_cmp, s)
+ }
+
+ /// Ensure that the matrix s is computed correctly for an arbitrary modulus with
+ /// base 5.
+ #[test]
+ fn base_5_arbitrary() {
+ let q = Z::from_str_b("4123", 5).unwrap();
+ let mut params = GadgetParameters::init_default(1, q);
+ params.k = Z::from(4);
+ params.base = Z::from(5);
+
+ let s = short_basis_gadget(¶ms);
+
+ let s_cmp = MatZ::from_str(
+ "[[5, 0, 0, 3],\
+ [-1, 5, 0, 2],\
+ [0, -1, 5, 1],\
+ [0, 0, -1, 4]]",
+ )
+ .unwrap();
+
+ assert_eq!(s_cmp, s)
+ }
+}
diff --git a/src/sample/g_trapdoor/short_basis_classical.rs b/src/sample/g_trapdoor/short_basis_classical.rs
index ad229be..ff66b3d 100644
--- a/src/sample/g_trapdoor/short_basis_classical.rs
+++ b/src/sample/g_trapdoor/short_basis_classical.rs
@@ -9,7 +9,10 @@
//! This module contains an implementation to generate a short basis from a G-Trapdoor
//! and its parity check matrix.
-use super::{gadget_classical::find_solution_gadget_mat, gadget_parameters::GadgetParameters};
+use super::{
+ gadget_classical::{find_solution_gadget_mat, short_basis_gadget},
+ gadget_parameters::GadgetParameters,
+};
use qfall_math::{
integer::{MatZ, Z},
integer_mod_q::MatZq,
@@ -37,7 +40,7 @@ use qfall_math::{
/// # Examples
/// ```
/// use qfall_crypto::sample::g_trapdoor::{gadget_parameters::GadgetParameters,
-/// gadget_default::gen_trapdoor_default};
+/// gadget_default::gen_trapdoor_default};
/// use qfall_crypto::sample::g_trapdoor::short_basis_classical::gen_short_basis_for_trapdoor;
/// use qfall_math::integer_mod_q::MatZq;
///
@@ -72,7 +75,7 @@ fn gen_sa_l(r: &MatZ) -> MatZ {
/// Computes `[ 0 | I, S' | W ]`
fn gen_sa_r(params: &GadgetParameters, tag: &MatZq, a: &MatZq) -> MatZ {
- let mut s = compute_s(params);
+ let mut s = short_basis_gadget(params);
// if `base^k = q`, then the reverse of `S` has a shorter diagonalization
if params.base.pow(¶ms.k).unwrap() == params.q {
s.reverse_columns();
@@ -98,48 +101,6 @@ fn gen_sa_r(params: &GadgetParameters, tag: &MatZq, a: &MatZq) -> MatZ {
sa_r
}
-/// Compute S for `[ 0 | I, S' | W ]`
-fn compute_s(params: &GadgetParameters) -> MatZ {
- let mut sk = MatZ::new(¶ms.k, ¶ms.k);
- let n: i64 = (¶ms.n).try_into().unwrap();
- let k: i64 = (¶ms.k).try_into().unwrap();
- for j in 0..k {
- sk.set_entry(j, j, ¶ms.base).unwrap();
- }
- for i in 0..k - 1 {
- sk.set_entry(i + 1, i, Z::MINUS_ONE).unwrap();
- }
- sk = if params.base.pow(k).unwrap() == params.q {
- // compute s in the special case where the modulus is a power of base
- // i.e. the last column can remain as it is
- sk
- } else {
- // compute s for any arbitrary modulus
- // represent modulus in `base` and set last row accordingly
- let mut q = Z::from(¶ms.q);
- for i in 0..k {
- let q_i = &q % ¶ms.base;
- sk.set_entry(i, k - 1, &q_i).unwrap();
- q = (q - q_i).div_exact(¶ms.base).unwrap();
- }
- sk
- };
- let mut out = MatZ::new(n * k, n * k);
- for j in 0..n {
- out.set_submatrix(
- j * sk.get_num_rows(),
- j * sk.get_num_columns(),
- &sk,
- 0,
- 0,
- -1,
- -1,
- )
- .unwrap();
- }
- out
-}
-
/// Computes `W` with `GW = -H^{-1}A [ I | 0 ] mod q`
fn compute_w(params: &GadgetParameters, tag: &MatZq, a: &MatZq) -> MatZ {
let tag_inv = tag.inverse().unwrap();
@@ -388,99 +349,6 @@ mod test_gen_sa {
}
}
-#[cfg(test)]
-mod test_compute_s {
- use crate::sample::g_trapdoor::{
- gadget_parameters::GadgetParameters, short_basis_classical::compute_s,
- };
- use qfall_math::integer::{MatZ, Z};
- use std::str::FromStr;
-
- /// Ensure that the matrix s is computed correctly for a power-of-two modulus.
- #[test]
- fn base_2_power_two() {
- let params = GadgetParameters::init_default(2, 16);
-
- let s = compute_s(¶ms);
-
- let s_cmp = MatZ::from_str(
- "[[2, 0, 0, 0, 0, 0, 0, 0],\
- [-1, 2, 0, 0, 0, 0, 0, 0],\
- [0, -1, 2, 0, 0, 0, 0, 0],\
- [0, 0, -1, 2, 0, 0, 0, 0],\
- [0, 0, 0, 0, 2, 0, 0, 0],\
- [0, 0, 0, 0, -1, 2, 0, 0],\
- [0, 0, 0, 0, 0, -1, 2, 0],\
- [0, 0, 0, 0, 0, 0, -1, 2]]",
- )
- .unwrap();
- assert_eq!(s_cmp, s)
- }
-
- /// Ensure that the matrix s is computed correctly for a an arbitrary modulus.
- #[test]
- fn base_2_arbitrary() {
- let q = Z::from(0b1100110);
- let params = GadgetParameters::init_default(1, q);
-
- let s = compute_s(¶ms);
-
- let s_cmp = MatZ::from_str(
- "[[2, 0, 0, 0, 0, 0, 0],\
- [-1, 2, 0, 0, 0, 0, 1],\
- [0, -1, 2, 0, 0, 0, 1],\
- [0, 0, -1, 2, 0, 0, 0],\
- [0, 0, 0, -1, 2, 0, 0],\
- [0, 0, 0, 0, -1, 2, 1],\
- [0, 0, 0, 0, 0, -1, 1]]",
- )
- .unwrap();
-
- assert_eq!(s_cmp, s)
- }
-
- /// Ensure that the matrix s is computed correctly for a power-of-5 modulus.
- #[test]
- fn base_5_power_5() {
- let mut params = GadgetParameters::init_default(1, 625);
- params.k = Z::from(4);
- params.base = Z::from(5);
-
- let s = compute_s(¶ms);
-
- let s_cmp = MatZ::from_str(
- "[[5, 0, 0, 0],\
- [-1, 5, 0, 0],\
- [0, -1, 5, 0],\
- [0, 0, -1, 5]]",
- )
- .unwrap();
- assert_eq!(s_cmp, s)
- }
-
- /// Ensure that the matrix s is computed correctly for an arbitrary modulus with
- /// base 5.
- #[test]
- fn base_5_arbitrary() {
- let q = Z::from_str_b("4123", 5).unwrap();
- let mut params = GadgetParameters::init_default(1, q);
- params.k = Z::from(4);
- params.base = Z::from(5);
-
- let s = compute_s(¶ms);
-
- let s_cmp = MatZ::from_str(
- "[[5, 0, 0, 3],\
- [-1, 5, 0, 2],\
- [0, -1, 5, 1],\
- [0, 0, -1, 4]]",
- )
- .unwrap();
-
- assert_eq!(s_cmp, s)
- }
-}
-
#[cfg(test)]
mod test_compute_w {
use super::compute_w;