From 04b5c2358627fb20a3679e3b25bcd0205673d739 Mon Sep 17 00:00:00 2001 From: jnsiemer Date: Fri, 15 Aug 2025 15:01:42 +0100 Subject: [PATCH 1/8] First draft --- src/primitive/psf.rs | 2 + src/primitive/psf/perturbation.rs | 508 ++++++++++++++++++++++++++++++ 2 files changed, 510 insertions(+) create mode 100644 src/primitive/psf/perturbation.rs diff --git a/src/primitive/psf.rs b/src/primitive/psf.rs index 3502a18..71ffe81 100644 --- a/src/primitive/psf.rs +++ b/src/primitive/psf.rs @@ -23,9 +23,11 @@ mod gpv; mod gpv_ring; +mod perturbation; pub use gpv::PSFGPV; pub use gpv_ring::PSFGPVRing; +pub use 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/perturbation.rs b/src/primitive/psf/perturbation.rs new file mode 100644 index 0000000..f093db3 --- /dev/null +++ b/src/primitive/psf/perturbation.rs @@ -0,0 +1,508 @@ +// 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::{gen_gadget_vec, gen_trapdoor}, + gadget_parameters::GadgetParameters, +}; +use qfall_math::{ + integer::{MatZ, Z}, + integer_mod_q::MatZq, + rational::{MatQ, Q}, + traits::{Concatenate, MatrixDimensions, MatrixGetEntry, 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 +/// - `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), +/// s: Q::from(12), +/// }; +/// +/// 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)); +/// ``` +#[derive(Serialize, Deserialize)] +pub struct PSFPerturbation { + pub gp: GadgetParameters, + pub r: Q, + pub s: Q, +} + +impl PSFPerturbation { + /// Returns the Cholesky decomposition of `Σ_p = Σ - [R^t|I]^t * Σ_G * [R^t|I]`. + 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); + + // TODO: Identity matrix potentially has wrong row-dimensions + // 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 2010/088 + // 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() + } +} + +pub fn sample_gaussian_gadget(psf: &PSFPerturbation, vec_u: &MatZq) -> MatZ { + // make sure size of u and psf.n fit + assert_eq!(vec_u.get_num_rows(), i64::try_from(&psf.gp.n).unwrap()); + + // Assemble g^t + let vec_g_t = MatZq::from(( + gen_gadget_vec(&psf.gp.k, &Z::from(&psf.gp.base)).transpose(), + &psf.gp.q, + )); + + // Setup mutable buckets to store preimage vectors in + let mut buckets = vec![vec![]; u64::try_from(Z::from(&psf.gp.q)).unwrap() as usize]; + // Setup storage for preimage vectors for each entry in order + let mut vectors = vec![]; + + for i in 0..vec_u.get_num_rows() { + // Find a preimage x s.t. = u_i for each entry of u + let u_i: Z = vec_u.get_entry(i, 0).unwrap(); + let u_i = i64::try_from(u_i).unwrap() as usize; + + if buckets[u_i].is_empty() { + // if no sampled vector x \in ZZ_q^k is available s.t. = u_i, + // sample until we found one + fill_bucket_until_ui_filled(psf, &mut buckets, &vec_g_t, u_i); + } + + let vec_x_i = buckets[u_i].pop().unwrap(); + vectors.push(vec_x_i); + } + + // assemble vec_x \in ZZ_q^{m} s.t. G * vec_x = vec_u + let mut vec_x = vectors.first().unwrap().clone(); + for i in 1..vectors.len() { + vec_x = vec_x.concat_vertical(vectors.get(i).unwrap()).unwrap(); + } + + // return vec_x + vec_x +} + +/// Samples short vectors discrete Gaussian and computes `` until the value equals `u_i`. +/// Any sample is put into a bucket s.t. none is thrown out. +/// +/// Parameters: +/// - `psf`: simple passing of parameters `n, k, q, s_g` +/// - `buckets`: mutable collection of vectors to store preimage-vectors in +/// - `vec_g_t`: transposed gadget vector +/// - `u_i`: i-th entry of target vector u +/// +/// This algorithm is a subroutine of the `bucketing` approach +/// described in Section 4.1 and 4.2 in [MP12](https://eprint.iacr.org/2011/501.pdf). +fn fill_bucket_until_ui_filled( + psf: &PSFPerturbation, + buckets: &mut [Vec], + vec_g_t: &MatZq, + u_i: usize, +) { + // Sample with width r * √Σ_G + let s = &psf.r * (psf.gp.base.pow(2).unwrap() + Z::ONE).sqrt(); + + while buckets[u_i].is_empty() { + // until no x \in ZZ_q^k is found s.t. = u_i, + // sample discrete Gaussian vectors x + let vec_x = MatZ::sample_discrete_gauss(&psf.gp.k, 1, &psf.gp.n, 0, &s).unwrap(); + + let dot_product: Z = (vec_g_t * &vec_x).get_entry(0, 0).unwrap(); + let dot_product: usize = i64::try_from(dot_product).unwrap() as usize; + + // put x in bucket s.t. it can be used later + buckets[dot_product].push(vec_x); + } +} + +impl PSF for PSFPerturbation { + type A = MatZq; + type Trapdoor = (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 a short base and its GSO. + /// + /// # 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(12), + /// }; + /// + /// let (a, (sh_b, sh_b_gso)) = psf.trap_gen(); + /// ``` + fn trap_gen(&self) -> (MatZq, (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())), + ); + + (mat_a, (mat_r, mat_sqrt_sigma_2)) + } + + /// 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(12), + /// }; + /// let (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).unwrap() + } + + /// Samples an `e` in the domain using SampleD with a short basis that is generated + /// from the G-Trapdoor from the conditioned conditioned + /// discrete Gaussian with `f_a(a,e) = u` for a provided syndrome `u`. + /// + /// *Note*: the provided parameters `a,r,u` must fit together, + /// otherwise unexpected behavior such as panics may occur. + /// + /// Parameters: + /// - `a`: The parity-check matrix + /// - `short_base`: The short base for `Λ^⟂(A)` + /// - `short_base_gso`: The precomputed GSO of the short_base + /// - `u`: The syndrome from the range + /// + /// Returns a sample `e` from the domain on the conditioned discrete + /// Gaussian distribution `f_a(a,e) = u`. + /// + /// # 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(12), + /// }; + /// 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_eq!(range_fa, psf.f_a(&a, &preimage)) + /// ``` + fn samp_p( + &self, + mat_a: &MatZq, + (mat_r, mat_sqrt_sigma_2): &(MatZ, MatQ), + vec_u: &MatZq, + ) -> MatZ { + // Sample perturbation p <- D_{ZZ^m, r * √Σ_p} - not correct for now. √Σ_p := as √Σ_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 = sample_gaussian_gadget(self, &vec_v); + + 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 + /// `a*sigma`. The sigma must be from the domain, i.e. D_n. + /// + /// Parameters: + /// - `a`: The parity-check matrix of dimensions `n x m` + /// - `sigma`: A column vector of length `m` + /// + /// Returns `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(12), + /// }; + /// let (a, td) = psf.trap_gen(); + /// let domain_sample = psf.samp_d(); + /// let range_fa = psf.f_a(&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 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(12), + /// }; + /// let (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)); + } +} From b86e517fd8844dbd2110116c797eeab55e72c9dc Mon Sep 17 00:00:00 2001 From: jnsiemer Date: Fri, 15 Aug 2025 15:50:51 +0100 Subject: [PATCH 2/8] Update doc comments --- src/primitive/psf.rs | 4 ++ src/primitive/psf/perturbation.rs | 91 +++++++++++++++++++++++-------- 2 files changed, 72 insertions(+), 23 deletions(-) diff --git a/src/primitive/psf.rs b/src/primitive/psf.rs index 71ffe81..5ed278f 100644 --- a/src/primitive/psf.rs +++ b/src/primitive/psf.rs @@ -20,6 +20,10 @@ //! 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; diff --git a/src/primitive/psf/perturbation.rs b/src/primitive/psf/perturbation.rs index f093db3..d62cf35 100644 --- a/src/primitive/psf/perturbation.rs +++ b/src/primitive/psf/perturbation.rs @@ -29,6 +29,7 @@ use serde::{Deserialize, Serialize}; /// /// 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 @@ -40,7 +41,8 @@ use serde::{Deserialize, Serialize}; /// /// let psf = PSFPerturbation { /// gp: GadgetParameters::init_default(8, 64), -/// s: Q::from(12), +/// r: Q::from(3), +/// s: Q::from(25), /// }; /// /// let (a, td) = psf.trap_gen(); @@ -58,12 +60,55 @@ pub struct PSFPerturbation { } impl PSFPerturbation { - /// Returns the Cholesky decomposition of `Σ_p = Σ - [R^t|I]^t * Σ_G * [R^t|I]`. + /// 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 (a, td) = psf.trap_gen(); + /// + /// let cov_mat = psf.s.pow(2).unwrap() * &psf.r * 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); + /// + /// 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); - // TODO: Identity matrix potentially has wrong row-dimensions // full_td = [R^t | I]^t let full_td = mat_r .concat_vertical(&MatZ::identity( @@ -170,9 +215,9 @@ impl PSF for PSFPerturbation { 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 a short base and its GSO. + /// 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`. /// /// # Examples /// ``` @@ -184,7 +229,7 @@ impl PSF for PSFPerturbation { /// let psf = PSFPerturbation { /// gp: GadgetParameters::init_default(8, 64), /// r: Q::from(3), - /// s: Q::from(12), + /// s: Q::from(25), /// }; /// /// let (a, (sh_b, sh_b_gso)) = psf.trap_gen(); @@ -216,7 +261,7 @@ impl PSF for PSFPerturbation { /// let psf = PSFPerturbation { /// gp: GadgetParameters::init_default(8, 64), /// r: Q::from(3), - /// s: Q::from(12), + /// s: Q::from(25), /// }; /// let (a, td) = psf.trap_gen(); /// @@ -227,21 +272,21 @@ impl PSF for PSFPerturbation { MatZ::sample_d_common(&m, &self.gp.n, &self.s).unwrap() } - /// Samples an `e` in the domain using SampleD with a short basis that is generated - /// from the G-Trapdoor from the conditioned conditioned + /// 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 `a,r,u` must fit together, + /// *Note*: the provided parameters `mat_a, mat_r, vec_u` must fit together, /// otherwise unexpected behavior such as panics may occur. /// /// Parameters: - /// - `a`: The parity-check matrix - /// - `short_base`: The short base for `Λ^⟂(A)` - /// - `short_base_gso`: The precomputed GSO of the short_base - /// - `u`: The syndrome from the range + /// - `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`. + /// Gaussian distribution `f_a(a,e) = u` with covariance matrix Σ depending on `mat_sqrt_sigma_2`. /// /// # Examples /// ``` @@ -253,7 +298,7 @@ impl PSF for PSFPerturbation { /// let psf = PSFPerturbation { /// gp: GadgetParameters::init_default(8, 64), /// r: Q::from(3), - /// s: Q::from(12), + /// s: Q::from(25), /// }; /// let (a, td) = psf.trap_gen(); /// let domain_sample = psf.samp_d(); @@ -289,13 +334,13 @@ impl PSF for PSFPerturbation { } /// Implements the efficiently computable function `f_a` which here corresponds to - /// `a*sigma`. The sigma must be from the domain, i.e. D_n. + /// `mat_a * sigma`. The sigma must be from the domain, i.e. D_n. /// /// Parameters: - /// - `a`: The parity-check matrix of dimensions `n x m` + /// - `mat_a`: The parity-check matrix of dimensions `n x m` /// - `sigma`: A column vector of length `m` /// - /// Returns `a*sigma` + /// Returns `mat_a * sigma`. /// /// # Examples /// ``` @@ -307,7 +352,7 @@ impl PSF for PSFPerturbation { /// let psf = PSFPerturbation { /// gp: GadgetParameters::init_default(8, 64), /// r: Q::from(3), - /// s: Q::from(12), + /// s: Q::from(25), /// }; /// let (a, td) = psf.trap_gen(); /// let domain_sample = psf.samp_d(); @@ -321,7 +366,7 @@ impl PSF for PSFPerturbation { mat_a * sigma } - /// Checks whether a value `sigma` is in D_n = {e ∈ Z^m | |e| <= s sqrt(m)}. + /// 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 @@ -338,7 +383,7 @@ impl PSF for PSFPerturbation { /// let psf = PSFPerturbation { /// gp: GadgetParameters::init_default(8, 64), /// r: Q::from(3), - /// s: Q::from(12), + /// s: Q::from(25), /// }; /// let (a, td) = psf.trap_gen(); /// From ebc479294a581da8a97fe019061e158caf8275b8 Mon Sep 17 00:00:00 2001 From: jnsiemer Date: Fri, 15 Aug 2025 16:46:04 +0100 Subject: [PATCH 3/8] Remove memory-intensive behaviour --- src/primitive/psf/perturbation.rs | 104 ++++++------------ .../g_trapdoor/short_basis_classical.rs | 34 ++++-- 2 files changed, 61 insertions(+), 77 deletions(-) diff --git a/src/primitive/psf/perturbation.rs b/src/primitive/psf/perturbation.rs index d62cf35..0572898 100644 --- a/src/primitive/psf/perturbation.rs +++ b/src/primitive/psf/perturbation.rs @@ -11,14 +11,15 @@ use super::PSF; use crate::sample::g_trapdoor::{ - gadget_classical::{gen_gadget_vec, gen_trapdoor}, + gadget_classical::{find_solution_gadget_mat, gen_trapdoor}, gadget_parameters::GadgetParameters, + short_basis_classical::short_basis_gadget, }; use qfall_math::{ integer::{MatZ, Z}, integer_mod_q::MatZq, rational::{MatQ, Q}, - traits::{Concatenate, MatrixDimensions, MatrixGetEntry, Pow}, + traits::{Concatenate, MatrixDimensions, Pow}, }; use serde::{Deserialize, Serialize}; @@ -51,6 +52,7 @@ use serde::{Deserialize, Serialize}; /// 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 { @@ -123,7 +125,7 @@ impl PSFPerturbation { 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 2010/088 + // 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 @@ -136,77 +138,43 @@ impl PSFPerturbation { } } -pub fn sample_gaussian_gadget(psf: &PSFPerturbation, vec_u: &MatZq) -> MatZ { - // make sure size of u and psf.n fit - assert_eq!(vec_u.get_num_rows(), i64::try_from(&psf.gp.n).unwrap()); - - // Assemble g^t - let vec_g_t = MatZq::from(( - gen_gadget_vec(&psf.gp.k, &Z::from(&psf.gp.base)).transpose(), - &psf.gp.q, - )); - - // Setup mutable buckets to store preimage vectors in - let mut buckets = vec![vec![]; u64::try_from(Z::from(&psf.gp.q)).unwrap() as usize]; - // Setup storage for preimage vectors for each entry in order - let mut vectors = vec![]; - - for i in 0..vec_u.get_num_rows() { - // Find a preimage x s.t. = u_i for each entry of u - let u_i: Z = vec_u.get_entry(i, 0).unwrap(); - let u_i = i64::try_from(u_i).unwrap() as usize; - - if buckets[u_i].is_empty() { - // if no sampled vector x \in ZZ_q^k is available s.t. = u_i, - // sample until we found one - fill_bucket_until_ui_filled(psf, &mut buckets, &vec_g_t, u_i); - } - - let vec_x_i = buckets[u_i].pop().unwrap(); - vectors.push(vec_x_i); - } - - // assemble vec_x \in ZZ_q^{m} s.t. G * vec_x = vec_u - let mut vec_x = vectors.first().unwrap().clone(); - for i in 1..vectors.len() { - vec_x = vec_x.concat_vertical(vectors.get(i).unwrap()).unwrap(); - } - - // return vec_x - vec_x -} - -/// Samples short vectors discrete Gaussian and computes `` until the value equals `u_i`. -/// Any sample is put into a bucket s.t. none is thrown out. +/// Samples a preimage with respect to the gadget matrix `G` of `vec_u`. /// /// Parameters: -/// - `psf`: simple passing of parameters `n, k, q, s_g` -/// - `buckets`: mutable collection of vectors to store preimage-vectors in -/// - `vec_g_t`: transposed gadget vector -/// - `u_i`: i-th entry of target vector u +/// - `psf`: The [`PSFPerturbation`] that sets the parameters for this function +/// - `vec_u`: The target vector +/// +/// Returns a discrete Gaussian sampled preimage of `u` under `G` with Gaussian parameter `r * √(b^2 + 1)`. /// -/// This algorithm is a subroutine of the `bucketing` approach -/// described in Section 4.1 and 4.2 in [MP12](https://eprint.iacr.org/2011/501.pdf). -fn fill_bucket_until_ui_filled( - psf: &PSFPerturbation, - buckets: &mut [Vec], - vec_g_t: &MatZq, - u_i: usize, -) { - // Sample with width r * √Σ_G +/// # Examples +/// ```compile_fail +/// 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 target = MatZq::sample_uniform(8, 1, 64); +/// let preimage = randomized_nearest_plane_gadget(&osf, &target); +/// ``` +pub(crate) fn randomized_nearest_plane_gadget(psf: &PSFPerturbation, vec_u: &MatZq) -> 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(); - while buckets[u_i].is_empty() { - // until no x \in ZZ_q^k is found s.t. = u_i, - // sample discrete Gaussian vectors x - let vec_x = MatZ::sample_discrete_gauss(&psf.gp.k, 1, &psf.gp.n, 0, &s).unwrap(); + // 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 dot_product: Z = (vec_g_t * &vec_x).get_entry(0, 0).unwrap(); - let dot_product: usize = i64::try_from(dot_product).unwrap() as usize; + // Get S as short basis of G + let short_base = short_basis_gadget(&psf.gp); + let center = MatQ::from(&(-1 * &long_solution)); - // put x in bucket s.t. it can be used later - buckets[dot_product].push(vec_x); - } + // just as PSFGPV::samp_p + long_solution + MatZ::sample_d(&short_base, &psf.gp.n, ¢er, &s).unwrap() } impl PSF for PSFPerturbation { @@ -321,7 +289,7 @@ impl PSF for PSFPerturbation { let vec_v = vec_u - mat_a * &vec_p; // z <- D_{Λ_v^⊥(G), r * √Σ_G} - let vec_z = sample_gaussian_gadget(self, &vec_v); + let vec_z = randomized_nearest_plane_gadget(self, &vec_v); let full_td = mat_r .concat_vertical(&MatZ::identity( diff --git a/src/sample/g_trapdoor/short_basis_classical.rs b/src/sample/g_trapdoor/short_basis_classical.rs index ad229be..f3d8f81 100644 --- a/src/sample/g_trapdoor/short_basis_classical.rs +++ b/src/sample/g_trapdoor/short_basis_classical.rs @@ -37,7 +37,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 +72,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,8 +98,24 @@ 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 { +/// 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::short_basis_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(); @@ -391,7 +407,7 @@ mod test_gen_sa { #[cfg(test)] mod test_compute_s { use crate::sample::g_trapdoor::{ - gadget_parameters::GadgetParameters, short_basis_classical::compute_s, + gadget_parameters::GadgetParameters, short_basis_classical::short_basis_gadget, }; use qfall_math::integer::{MatZ, Z}; use std::str::FromStr; @@ -401,7 +417,7 @@ mod test_compute_s { fn base_2_power_two() { let params = GadgetParameters::init_default(2, 16); - let s = compute_s(¶ms); + let s = short_basis_gadget(¶ms); let s_cmp = MatZ::from_str( "[[2, 0, 0, 0, 0, 0, 0, 0],\ @@ -423,7 +439,7 @@ mod test_compute_s { let q = Z::from(0b1100110); let params = GadgetParameters::init_default(1, q); - let s = compute_s(¶ms); + let s = short_basis_gadget(¶ms); let s_cmp = MatZ::from_str( "[[2, 0, 0, 0, 0, 0, 0],\ @@ -446,7 +462,7 @@ mod test_compute_s { params.k = Z::from(4); params.base = Z::from(5); - let s = compute_s(¶ms); + let s = short_basis_gadget(¶ms); let s_cmp = MatZ::from_str( "[[5, 0, 0, 0],\ @@ -467,7 +483,7 @@ mod test_compute_s { params.k = Z::from(4); params.base = Z::from(5); - let s = compute_s(¶ms); + let s = short_basis_gadget(¶ms); let s_cmp = MatZ::from_str( "[[5, 0, 0, 3],\ From 06d712c884a4362af396a3de6a52b4177630adaa Mon Sep 17 00:00:00 2001 From: jnsiemer Date: Fri, 15 Aug 2025 16:50:06 +0100 Subject: [PATCH 4/8] Relocate function --- src/primitive/psf/perturbation.rs | 2 +- src/sample/g_trapdoor/gadget_classical.rs | 151 +++++++++++++++++ .../g_trapdoor/short_basis_classical.rs | 156 +----------------- 3 files changed, 156 insertions(+), 153 deletions(-) diff --git a/src/primitive/psf/perturbation.rs b/src/primitive/psf/perturbation.rs index 0572898..71f480e 100644 --- a/src/primitive/psf/perturbation.rs +++ b/src/primitive/psf/perturbation.rs @@ -11,9 +11,9 @@ use super::PSF; use crate::sample::g_trapdoor::{ + gadget_classical::short_basis_gadget, gadget_classical::{find_solution_gadget_mat, gen_trapdoor}, gadget_parameters::GadgetParameters, - short_basis_classical::short_basis_gadget, }; use qfall_math::{ integer::{MatZ, Z}, 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 f3d8f81..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, @@ -98,64 +101,6 @@ fn gen_sa_r(params: &GadgetParameters, tag: &MatZq, a: &MatZq) -> MatZ { sa_r } -/// 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::short_basis_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 -} - /// 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(); @@ -404,99 +349,6 @@ mod test_gen_sa { } } -#[cfg(test)] -mod test_compute_s { - use crate::sample::g_trapdoor::{ - gadget_parameters::GadgetParameters, short_basis_classical::short_basis_gadget, - }; - 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) - } -} - #[cfg(test)] mod test_compute_w { use super::compute_w; From 654e896e95f5de2d79b0681a639b697573e428ec Mon Sep 17 00:00:00 2001 From: jnsiemer Date: Fri, 15 Aug 2025 17:23:13 +0100 Subject: [PATCH 5/8] Add benchmarks --- benches/benchmarks.rs | 3 +- benches/psf.rs | 100 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 benches/psf.rs 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, +); From 7d71f780b3642129b7e59f3f3c40fc77826aac5b Mon Sep 17 00:00:00 2001 From: jnsiemer Date: Fri, 15 Aug 2025 17:45:18 +0100 Subject: [PATCH 6/8] Performance upgrade by keeping GSO of short basis of G --- src/primitive/psf/perturbation.rs | 79 +++++++++++++++++++++++-------- 1 file changed, 59 insertions(+), 20 deletions(-) diff --git a/src/primitive/psf/perturbation.rs b/src/primitive/psf/perturbation.rs index 71f480e..4ec5a9a 100644 --- a/src/primitive/psf/perturbation.rs +++ b/src/primitive/psf/perturbation.rs @@ -96,7 +96,7 @@ impl PSFPerturbation { /// /// let cov_mat = psf.s.pow(2).unwrap() * &psf.r * 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); + /// 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); @@ -143,6 +143,8 @@ impl PSFPerturbation { /// 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)`. /// @@ -150,6 +152,7 @@ impl PSFPerturbation { /// ```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; /// @@ -158,34 +161,51 @@ impl PSFPerturbation { /// 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); +/// 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) -> MatZ { +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); - // Get S as short basis of G - let short_base = short_basis_gadget(&psf.gp); let center = MatQ::from(&(-1 * &long_solution)); // just as PSFGPV::samp_p - long_solution + MatZ::sample_d(&short_base, &psf.gp.n, ¢er, &s).unwrap() + 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); + 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 /// ``` @@ -200,9 +220,9 @@ impl PSF for PSFPerturbation { /// s: Q::from(25), /// }; /// - /// let (a, (sh_b, sh_b_gso)) = psf.trap_gen(); + /// 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)) { + 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); @@ -214,7 +234,17 @@ impl PSF for PSFPerturbation { * MatQ::identity(mat_a.get_num_columns(), mat_a.get_num_columns())), ); - (mat_a, (mat_r, mat_sqrt_sigma_2)) + 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`. @@ -231,7 +261,7 @@ impl PSF for PSFPerturbation { /// r: Q::from(3), /// s: Q::from(25), /// }; - /// let (a, td) = psf.trap_gen(); + /// let (mat_a, td) = psf.trap_gen(); /// /// let domain_sample = psf.samp_d(); /// ``` @@ -268,17 +298,21 @@ impl PSF for PSFPerturbation { /// r: Q::from(3), /// s: Q::from(25), /// }; - /// let (a, td) = psf.trap_gen(); + /// let (mat_a, td) = psf.trap_gen(); /// let domain_sample = psf.samp_d(); - /// let range_fa = psf.f_a(&a, &domain_sample); + /// let range_fa = psf.f_a(&mat_a, &domain_sample); /// - /// let preimage = psf.samp_p(&a, &td, &range_fa); - /// assert_eq!(range_fa, psf.f_a(&a, &preimage)) + /// 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): &(MatZ, MatQ), + (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 * √Σ_p} - not correct for now. √Σ_p := as √Σ_2 @@ -289,7 +323,12 @@ impl PSF for PSFPerturbation { 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); + 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( @@ -322,9 +361,9 @@ impl PSF for PSFPerturbation { /// r: Q::from(3), /// s: Q::from(25), /// }; - /// let (a, td) = psf.trap_gen(); + /// let (mat_a, td) = psf.trap_gen(); /// let domain_sample = psf.samp_d(); - /// let range_fa = psf.f_a(&a, &domain_sample); + /// let range_fa = psf.f_a(&mat_a, &domain_sample); /// ``` /// /// # Panics ... @@ -353,7 +392,7 @@ impl PSF for PSFPerturbation { /// r: Q::from(3), /// s: Q::from(25), /// }; - /// let (a, td) = psf.trap_gen(); + /// let (mat_a, td) = psf.trap_gen(); /// /// let vector = psf.samp_d(); /// From 7dc0a40f73521b8309a5ad9968cd6a081b556b00 Mon Sep 17 00:00:00 2001 From: jnsiemer Date: Fri, 15 Aug 2025 17:49:49 +0100 Subject: [PATCH 7/8] Format --- src/primitive/psf/perturbation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/primitive/psf/perturbation.rs b/src/primitive/psf/perturbation.rs index 4ec5a9a..14303e7 100644 --- a/src/primitive/psf/perturbation.rs +++ b/src/primitive/psf/perturbation.rs @@ -161,7 +161,7 @@ impl PSFPerturbation { /// 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(); /// From 3215d9f957ed74134503324bfe1e705b76c75c0f Mon Sep 17 00:00:00 2001 From: jnsiemer Date: Fri, 29 Aug 2025 12:44:25 +0100 Subject: [PATCH 8/8] Apply suggestions from code review --- src/primitive/psf.rs | 4 ++-- .../psf/{perturbation.rs => mp_perturbation.rs} | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) rename src/primitive/psf/{perturbation.rs => mp_perturbation.rs} (98%) diff --git a/src/primitive/psf.rs b/src/primitive/psf.rs index 5ed278f..33cdce4 100644 --- a/src/primitive/psf.rs +++ b/src/primitive/psf.rs @@ -27,11 +27,11 @@ mod gpv; mod gpv_ring; -mod perturbation; +mod mp_perturbation; pub use gpv::PSFGPV; pub use gpv_ring::PSFGPVRing; -pub use perturbation::PSFPerturbation; +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/perturbation.rs b/src/primitive/psf/mp_perturbation.rs similarity index 98% rename from src/primitive/psf/perturbation.rs rename to src/primitive/psf/mp_perturbation.rs index 14303e7..ac99c94 100644 --- a/src/primitive/psf/perturbation.rs +++ b/src/primitive/psf/mp_perturbation.rs @@ -91,10 +91,11 @@ impl PSFPerturbation { /// r: Q::from(3), /// s: Q::from(25), /// }; + /// let different_s: f64 = 35.0; /// /// let (a, td) = psf.trap_gen(); /// - /// let cov_mat = psf.s.pow(2).unwrap() * &psf.r * MatQ::identity(a.get_num_columns(), a.get_num_columns()); + /// 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); /// @@ -146,7 +147,8 @@ impl PSFPerturbation { /// - `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)`. +/// 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 @@ -267,7 +269,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).unwrap() + MatZ::sample_d_common(&m, &self.gp.n, &self.s * &self.r).unwrap() } /// Samples an `e` in the domain using SampleD that is generated @@ -315,7 +317,7 @@ impl PSF for PSFPerturbation { ), vec_u: &MatZq, ) -> MatZ { - // Sample perturbation p <- D_{ZZ^m, r * √Σ_p} - not correct for now. √Σ_p := as √Σ_2 + // 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();