From 56c8cae8338433a0098293e67fba0d4857101911 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 30 Aug 2024 18:06:05 +0200 Subject: [PATCH] test(core): add noise formulas and variance tests for KS and PBS --- .../noise_distribution/lwe_keyswitch_noise.rs | 175 +++++++++++++++ .../lwe_programmable_boostrapping_noise.rs | 205 ++++++++++++++++++ .../algorithms/test/noise_distribution/mod.rs | 27 +++ tfhe/src/core_crypto/commons/mod.rs | 1 + .../commons/noise_formulas/lwe_keyswitch.rs | 45 ++++ .../lwe_programmable_bootstrap.rs | 62 ++++++ .../core_crypto/commons/noise_formulas/mod.rs | 4 + .../commons/noise_formulas/secure_noise.rs | 29 +++ 8 files changed, 548 insertions(+) create mode 100644 tfhe/src/core_crypto/algorithms/test/noise_distribution/lwe_keyswitch_noise.rs create mode 100644 tfhe/src/core_crypto/algorithms/test/noise_distribution/lwe_programmable_boostrapping_noise.rs create mode 100644 tfhe/src/core_crypto/commons/noise_formulas/lwe_keyswitch.rs create mode 100644 tfhe/src/core_crypto/commons/noise_formulas/lwe_programmable_bootstrap.rs create mode 100644 tfhe/src/core_crypto/commons/noise_formulas/mod.rs create mode 100644 tfhe/src/core_crypto/commons/noise_formulas/secure_noise.rs diff --git a/tfhe/src/core_crypto/algorithms/test/noise_distribution/lwe_keyswitch_noise.rs b/tfhe/src/core_crypto/algorithms/test/noise_distribution/lwe_keyswitch_noise.rs new file mode 100644 index 0000000000..cc82e2b4c6 --- /dev/null +++ b/tfhe/src/core_crypto/algorithms/test/noise_distribution/lwe_keyswitch_noise.rs @@ -0,0 +1,175 @@ +use super::*; +use crate::core_crypto::commons::noise_formulas::lwe_keyswitch::keyswitch_additive_variance_132_bits_security_gaussian; +use crate::core_crypto::commons::noise_formulas::secure_noise::minimal_lwe_variance_for_132_bits_security_gaussian; +use crate::core_crypto::commons::test_tools::{torus_modular_diff, variance}; +use rayon::prelude::*; + +// This is 1 / 16 which is exactly representable in an f64 (even an f32) +// 1 / 32 is too strict and fails the tests +const RELATIVE_TOLERANCE: f64 = 0.0625; + +const NB_TESTS: usize = 1000; + +fn lwe_encrypt_ks_decrypt_noise_distribution_custom_mod>( + params: ClassicTestParams, +) { + let lwe_dimension = params.lwe_dimension; + let lwe_noise_distribution = params.lwe_noise_distribution; + let glwe_noise_distribution = params.glwe_noise_distribution; + let ciphertext_modulus = params.ciphertext_modulus; + let message_modulus_log = params.message_modulus_log; + let encoding_with_padding = get_encoding_with_padding(ciphertext_modulus); + let glwe_dimension = params.glwe_dimension; + let polynomial_size = params.polynomial_size; + let ks_decomp_base_log = params.ks_base_log; + let ks_decomp_level_count = params.ks_level; + + let input_lwe_dimension = glwe_dimension.to_equivalent_lwe_dimension(polynomial_size); + let output_lwe_dimension = lwe_dimension; + + let modulus_as_f64 = if ciphertext_modulus.is_native_modulus() { + 2.0f64.powi(Scalar::BITS as i32) + } else { + ciphertext_modulus.get_custom_modulus() as f64 + }; + + let encryption_variance = Variance(glwe_noise_distribution.gaussian_std_dev().get_variance()); + let expected_variance = Variance( + encryption_variance.0 + + keyswitch_additive_variance_132_bits_security_gaussian( + input_lwe_dimension, + output_lwe_dimension, + ks_decomp_base_log, + ks_decomp_level_count, + modulus_as_f64, + ) + .0, + ); + + let mut rsc = TestResources::new(); + + let msg_modulus = Scalar::ONE.shl(message_modulus_log.0); + let mut msg = msg_modulus; + let delta: Scalar = encoding_with_padding / msg_modulus; + + let num_samples = NB_TESTS * >::cast_into(msg); + let mut noise_samples = Vec::with_capacity(num_samples); + + let lwe_sk = allocate_and_generate_new_binary_lwe_secret_key( + lwe_dimension, + &mut rsc.secret_random_generator, + ); + + let glwe_sk = allocate_and_generate_new_binary_glwe_secret_key( + glwe_dimension, + polynomial_size, + &mut rsc.secret_random_generator, + ); + + let big_lwe_sk = glwe_sk.into_lwe_secret_key(); + + let ksk_big_to_small = allocate_and_generate_new_lwe_keyswitch_key( + &big_lwe_sk, + &lwe_sk, + ks_decomp_base_log, + ks_decomp_level_count, + lwe_noise_distribution, + ciphertext_modulus, + &mut rsc.encryption_random_generator, + ); + + assert!(check_encrypted_content_respects_mod( + &ksk_big_to_small, + ciphertext_modulus + )); + + while msg != Scalar::ZERO { + msg = msg.wrapping_sub(Scalar::ONE); + let current_run_samples: Vec<_> = (0..NB_TESTS) + .into_par_iter() + .map(|_| { + let mut rsc = TestResources::new(); + + let plaintext = Plaintext(msg * delta); + + let ct = allocate_and_encrypt_new_lwe_ciphertext( + &big_lwe_sk, + plaintext, + glwe_noise_distribution, + ciphertext_modulus, + &mut rsc.encryption_random_generator, + ); + + assert!(check_encrypted_content_respects_mod( + &ct, + ciphertext_modulus + )); + + let mut output_ct = LweCiphertext::new( + Scalar::ZERO, + lwe_sk.lwe_dimension().to_lwe_size(), + ciphertext_modulus, + ); + + keyswitch_lwe_ciphertext(&ksk_big_to_small, &ct, &mut output_ct); + + assert!(check_encrypted_content_respects_mod( + &output_ct, + ciphertext_modulus + )); + + let decrypted = decrypt_lwe_ciphertext(&lwe_sk, &output_ct); + + let decoded = round_decode(decrypted.0, delta) % msg_modulus; + + assert_eq!(msg, decoded); + + torus_modular_diff(plaintext.0, decrypted.0, ciphertext_modulus) + }) + .collect(); + + noise_samples.extend(current_run_samples); + } + + let measured_variance = variance(&noise_samples); + + let minimal_variance = minimal_lwe_variance_for_132_bits_security_gaussian( + ksk_big_to_small.output_key_lwe_dimension(), + if ciphertext_modulus.is_native_modulus() { + 2.0f64.powi(Scalar::BITS as i32) + } else { + ciphertext_modulus.get_custom_modulus() as f64 + }, + ); + + // Have a log even if it's a test to have a trace in no capture mode to eyeball variances + println!("measured_variance={measured_variance:?}"); + println!("expected_variance={expected_variance:?}"); + println!("minimal_variance={minimal_variance:?}"); + + if measured_variance.0 < expected_variance.0 { + // We are in the clear as long as we have at least the noise for security + assert!( + measured_variance.0 >= minimal_variance.0, + "Found insecure variance after keyswitch\n\ + measure_variance={measured_variance:?}\n\ + minimal_variance={minimal_variance:?}" + ); + } else { + // Check we are not too far from the expected variance if we are bigger + let var_abs_diff = (expected_variance.0 - measured_variance.0).abs(); + let tolerance_threshold = RELATIVE_TOLERANCE * expected_variance.0; + + assert!( + var_abs_diff < tolerance_threshold, + "Absolute difference for variance: {var_abs_diff}, \ + tolerance threshold: {tolerance_threshold}, \ + got variance: {measured_variance:?}, \ + expected variance: {expected_variance:?}" + ); + } +} + +create_parametrized_test!(lwe_encrypt_ks_decrypt_noise_distribution_custom_mod { + NOISE_TEST_PARAMS_4_BITS_NATIVE_U64_132_BITS_GAUSSIAN, +}); diff --git a/tfhe/src/core_crypto/algorithms/test/noise_distribution/lwe_programmable_boostrapping_noise.rs b/tfhe/src/core_crypto/algorithms/test/noise_distribution/lwe_programmable_boostrapping_noise.rs new file mode 100644 index 0000000000..01e6c26060 --- /dev/null +++ b/tfhe/src/core_crypto/algorithms/test/noise_distribution/lwe_programmable_boostrapping_noise.rs @@ -0,0 +1,205 @@ +use super::*; +use crate::core_crypto::commons::noise_formulas::lwe_programmable_bootstrap::pbs_variance_132_bits_security_gaussian; +use crate::core_crypto::commons::noise_formulas::secure_noise::minimal_lwe_variance_for_132_bits_security_gaussian; +use crate::core_crypto::commons::test_tools::{torus_modular_diff, variance}; +use rayon::prelude::*; + +// This is 1 / 16 which is exactly representable in an f64 (even an f32) +// 1 / 32 is too strict and fails the tests +const RELATIVE_TOLERANCE: f64 = 0.0625; + +const NB_TESTS: usize = 1000; + +fn lwe_encrypt_pbs_decrypt_custom_mod(params: ClassicTestParams) +where + Scalar: UnsignedTorus + Sync + Send + CastFrom + CastInto, +{ + let input_lwe_dimension = params.lwe_dimension; + let lwe_noise_distribution = params.lwe_noise_distribution; + let glwe_noise_distribution = params.glwe_noise_distribution; + let ciphertext_modulus = params.ciphertext_modulus; + let message_modulus_log = params.message_modulus_log; + let msg_modulus = Scalar::ONE.shl(message_modulus_log.0); + let encoding_with_padding = get_encoding_with_padding(ciphertext_modulus); + let glwe_dimension = params.glwe_dimension; + let polynomial_size = params.polynomial_size; + let pbs_decomposition_base_log = params.pbs_base_log; + let pbs_decomposition_level_count = params.pbs_level; + + let modulus_as_f64 = if ciphertext_modulus.is_native_modulus() { + 2.0f64.powi(Scalar::BITS as i32) + } else { + ciphertext_modulus.get_custom_modulus() as f64 + }; + + let expected_variance = pbs_variance_132_bits_security_gaussian( + input_lwe_dimension, + glwe_dimension, + polynomial_size, + pbs_decomposition_base_log, + pbs_decomposition_level_count, + modulus_as_f64, + ); + + let mut rsc = TestResources::new(); + + let f = |x: Scalar| x; + + let delta: Scalar = encoding_with_padding / msg_modulus; + let mut msg = msg_modulus; + + let num_samples = NB_TESTS * >::cast_into(msg); + let mut noise_samples = Vec::with_capacity(num_samples); + + let input_lwe_secret_key = allocate_and_generate_new_binary_lwe_secret_key( + input_lwe_dimension, + &mut rsc.secret_random_generator, + ); + + let output_glwe_secret_key = allocate_and_generate_new_binary_glwe_secret_key( + glwe_dimension, + polynomial_size, + &mut rsc.secret_random_generator, + ); + + let output_lwe_secret_key = output_glwe_secret_key.as_lwe_secret_key(); + + let fbsk = { + let bsk = allocate_and_generate_new_lwe_bootstrap_key( + &input_lwe_secret_key, + &output_glwe_secret_key, + pbs_decomposition_base_log, + pbs_decomposition_level_count, + glwe_noise_distribution, + ciphertext_modulus, + &mut rsc.encryption_random_generator, + ); + + assert!(check_encrypted_content_respects_mod( + &*bsk, + ciphertext_modulus + )); + + let mut fbsk = FourierLweBootstrapKey::new( + bsk.input_lwe_dimension(), + bsk.glwe_size(), + bsk.polynomial_size(), + bsk.decomposition_base_log(), + bsk.decomposition_level_count(), + ); + + par_convert_standard_lwe_bootstrap_key_to_fourier(&bsk, &mut fbsk); + + fbsk + }; + + let accumulator = generate_programmable_bootstrap_glwe_lut( + polynomial_size, + glwe_dimension.to_glwe_size(), + msg_modulus.cast_into(), + ciphertext_modulus, + delta, + f, + ); + + assert!(check_encrypted_content_respects_mod( + &accumulator, + ciphertext_modulus + )); + + while msg != Scalar::ZERO { + msg = msg.wrapping_sub(Scalar::ONE); + + let current_run_samples: Vec<_> = (0..NB_TESTS) + .into_par_iter() + .map(|_| { + let mut rsc = TestResources::new(); + + let plaintext = Plaintext(msg * delta); + + let lwe_ciphertext_in = allocate_and_encrypt_new_lwe_ciphertext( + &input_lwe_secret_key, + plaintext, + lwe_noise_distribution, + ciphertext_modulus, + &mut rsc.encryption_random_generator, + ); + + assert!(check_encrypted_content_respects_mod( + &lwe_ciphertext_in, + ciphertext_modulus + )); + + let mut out_pbs_ct = LweCiphertext::new( + Scalar::ZERO, + output_lwe_secret_key.lwe_dimension().to_lwe_size(), + ciphertext_modulus, + ); + + programmable_bootstrap_lwe_ciphertext( + &lwe_ciphertext_in, + &mut out_pbs_ct, + &accumulator, + &fbsk, + ); + + assert!(check_encrypted_content_respects_mod( + &out_pbs_ct, + ciphertext_modulus + )); + + let decrypted = decrypt_lwe_ciphertext(&output_lwe_secret_key, &out_pbs_ct); + + let decoded = round_decode(decrypted.0, delta) % msg_modulus; + + assert_eq!(decoded, f(msg)); + + torus_modular_diff(plaintext.0, decrypted.0, ciphertext_modulus) + }) + .collect(); + + noise_samples.extend(current_run_samples); + } + + let measured_variance = variance(&noise_samples); + + let minimal_variance = minimal_lwe_variance_for_132_bits_security_gaussian( + fbsk.output_lwe_dimension(), + if ciphertext_modulus.is_native_modulus() { + 2.0f64.powi(Scalar::BITS as i32) + } else { + ciphertext_modulus.get_custom_modulus() as f64 + }, + ); + + // Have a log even if it's a test to have a trace in no capture mode to eyeball variances + println!("measured_variance={measured_variance:?}"); + println!("expected_variance={expected_variance:?}"); + println!("minimal_variance={minimal_variance:?}"); + + if measured_variance.0 < expected_variance.0 { + // We are in the clear as long as we have at least the noise for security + assert!( + measured_variance.0 >= minimal_variance.0, + "Found insecure variance after PBS\n\ + measure_variance={measured_variance:?}\n\ + minimal_variance={minimal_variance:?}" + ); + } else { + // Check we are not too far from the expected variance if we are bigger + let var_abs_diff = (expected_variance.0 - measured_variance.0).abs(); + let tolerance_threshold = RELATIVE_TOLERANCE * expected_variance.0; + + assert!( + var_abs_diff < tolerance_threshold, + "Absolute difference for variance: {var_abs_diff}, \ + tolerance threshold: {tolerance_threshold}, \ + got variance: {measured_variance:?}, \ + expected variance: {expected_variance:?}" + ); + } +} + +create_parametrized_test!(lwe_encrypt_pbs_decrypt_custom_mod { + NOISE_TEST_PARAMS_4_BITS_NATIVE_U64_132_BITS_GAUSSIAN +}); diff --git a/tfhe/src/core_crypto/algorithms/test/noise_distribution/mod.rs b/tfhe/src/core_crypto/algorithms/test/noise_distribution/mod.rs index b4b36a41b2..5e7adb7a0d 100644 --- a/tfhe/src/core_crypto/algorithms/test/noise_distribution/mod.rs +++ b/tfhe/src/core_crypto/algorithms/test/noise_distribution/mod.rs @@ -1,3 +1,30 @@ use super::*; mod lwe_encryption_noise; +mod lwe_keyswitch_noise; +mod lwe_programmable_boostrapping_noise; + +#[allow(clippy::excessive_precision)] +pub const NOISE_TEST_PARAMS_4_BITS_NATIVE_U64_132_BITS_GAUSSIAN: ClassicTestParams = + ClassicTestParams { + lwe_dimension: LweDimension(841), + glwe_dimension: GlweDimension(1), + polynomial_size: PolynomialSize(2048), + lwe_noise_distribution: DynamicDistribution::new_gaussian_from_std_dev(StandardDev( + 3.1496674685772435e-06, + )), + glwe_noise_distribution: DynamicDistribution::new_gaussian_from_std_dev(StandardDev( + 2.845267479601915e-15, + )), + pbs_base_log: DecompositionBaseLog(22), + pbs_level: DecompositionLevelCount(1), + ks_level: DecompositionLevelCount(5), + ks_base_log: DecompositionBaseLog(3), + pfks_level: DecompositionLevelCount(0), + pfks_base_log: DecompositionBaseLog(0), + pfks_noise_distribution: DynamicDistribution::new_gaussian_from_std_dev(StandardDev(0.0)), + cbs_level: DecompositionLevelCount(0), + cbs_base_log: DecompositionBaseLog(0), + message_modulus_log: MessageModulusLog(4), + ciphertext_modulus: CiphertextModulus::new_native(), + }; diff --git a/tfhe/src/core_crypto/commons/mod.rs b/tfhe/src/core_crypto/commons/mod.rs index 8eafaef37e..3a0964a9b5 100644 --- a/tfhe/src/core_crypto/commons/mod.rs +++ b/tfhe/src/core_crypto/commons/mod.rs @@ -14,6 +14,7 @@ pub mod computation_buffers; pub mod dispersion; pub mod generators; pub mod math; +pub mod noise_formulas; pub mod numeric; pub mod parameters; pub mod utils; diff --git a/tfhe/src/core_crypto/commons/noise_formulas/lwe_keyswitch.rs b/tfhe/src/core_crypto/commons/noise_formulas/lwe_keyswitch.rs new file mode 100644 index 0000000000..de7eb0bfe6 --- /dev/null +++ b/tfhe/src/core_crypto/commons/noise_formulas/lwe_keyswitch.rs @@ -0,0 +1,45 @@ +// This file was autogenerated, do not modify by hand. +use crate::core_crypto::commons::dispersion::Variance; +use crate::core_crypto::commons::parameters::*; + +/// This formula is only valid if the proper noise distributions are used and +/// if the keys used are encrypted using secure noise given by the +/// [`minimal_glwe_variance`](`super::secure_noise`) +/// and [`minimal_lwe_variance`](`super::secure_noise`) family of functions. +pub fn keyswitch_additive_variance_132_bits_security_gaussian( + input_lwe_dimension: LweDimension, + output_lwe_dimension: LweDimension, + decomposition_base_log: DecompositionBaseLog, + decomposition_level_count: DecompositionLevelCount, + modulus: f64, +) -> Variance { + Variance(keyswitch_additive_variance_132_bits_security_gaussian_impl( + input_lwe_dimension.0 as f64, + output_lwe_dimension.0 as f64, + 2.0f64.powi(decomposition_base_log.0 as i32), + decomposition_level_count.0 as f64, + modulus, + )) +} + +/// This formula is only valid if the proper noise distributions are used and +/// if the keys used are encrypted using secure noise given by the +/// [`minimal_glwe_variance`](`super::secure_noise`) +/// and [`minimal_lwe_variance`](`super::secure_noise`) family of functions. +pub fn keyswitch_additive_variance_132_bits_security_gaussian_impl( + input_lwe_dimension: f64, + output_lwe_dimension: f64, + decomposition_base: f64, + decomposition_level_count: f64, + modulus: f64, +) -> f64 { + (1_f64 / 3.0) + * decomposition_level_count + * input_lwe_dimension + * ((5.31469187675068 - 0.0497829131652661 * output_lwe_dimension).exp2() + + 16.0 * modulus.powf(-2.0)) + * ((1_f64 / 4.0) * decomposition_base.powf(2.0) + 0.5) + + input_lwe_dimension + * (0.0208333333333333 * modulus.powf(-2.0) + + 0.0416666666666667 * decomposition_base.powf(-2.0 * decomposition_level_count)) +} diff --git a/tfhe/src/core_crypto/commons/noise_formulas/lwe_programmable_bootstrap.rs b/tfhe/src/core_crypto/commons/noise_formulas/lwe_programmable_bootstrap.rs new file mode 100644 index 0000000000..4e72086915 --- /dev/null +++ b/tfhe/src/core_crypto/commons/noise_formulas/lwe_programmable_bootstrap.rs @@ -0,0 +1,62 @@ +// This file was autogenerated, do not modify by hand. +use crate::core_crypto::commons::dispersion::Variance; +use crate::core_crypto::commons::parameters::*; + +/// This formula is only valid if the proper noise distributions are used and +/// if the keys used are encrypted using secure noise given by the +/// [`minimal_glwe_variance`](`super::secure_noise`) +/// and [`minimal_lwe_variance`](`super::secure_noise`) family of functions. +pub fn pbs_variance_132_bits_security_gaussian( + input_lwe_dimension: LweDimension, + output_glwe_dimension: GlweDimension, + output_polynomial_size: PolynomialSize, + decomposition_base_log: DecompositionBaseLog, + decomposition_level_count: DecompositionLevelCount, + modulus: f64, +) -> Variance { + Variance(pbs_variance_132_bits_security_gaussian_impl( + input_lwe_dimension.0 as f64, + output_glwe_dimension.0 as f64, + output_polynomial_size.0 as f64, + 2.0f64.powi(decomposition_base_log.0 as i32), + decomposition_level_count.0 as f64, + modulus, + )) +} + +/// This formula is only valid if the proper noise distributions are used and +/// if the keys used are encrypted using secure noise given by the +/// [`minimal_glwe_variance`](`super::secure_noise`) +/// and [`minimal_lwe_variance`](`super::secure_noise`) family of functions. +pub fn pbs_variance_132_bits_security_gaussian_impl( + input_lwe_dimension: f64, + output_glwe_dimension: f64, + output_polynomial_size: f64, + decomposition_base: f64, + decomposition_level_count: f64, + modulus: f64, +) -> f64 { + input_lwe_dimension + * (2.06537277069845e-33 + * decomposition_base.powf(2.0) + * decomposition_level_count + * output_polynomial_size.powf(2.0) + * (output_glwe_dimension + 1.0) + + (1_f64 / 3.0) + * decomposition_level_count + * output_polynomial_size + * ((-0.0497829131652661 * output_glwe_dimension * output_polynomial_size + + 5.31469187675068) + .exp2() + + 16.0 * modulus.powf(-2.0)) + * ((1_f64 / 4.0) * decomposition_base.powf(2.0) + 0.5) + * (output_glwe_dimension + 1.0) + + (1_f64 / 12.0) * modulus.powf(-2.0) + + (1_f64 / 2.0) + * output_glwe_dimension + * output_polynomial_size + * (0.0208333333333333 * modulus.powf(-2.0) + + 0.0416666666666667 + * decomposition_base.powf(-2.0 * decomposition_level_count)) + + (1_f64 / 24.0) * decomposition_base.powf(-2.0 * decomposition_level_count)) +} diff --git a/tfhe/src/core_crypto/commons/noise_formulas/mod.rs b/tfhe/src/core_crypto/commons/noise_formulas/mod.rs new file mode 100644 index 0000000000..c0a0526a77 --- /dev/null +++ b/tfhe/src/core_crypto/commons/noise_formulas/mod.rs @@ -0,0 +1,4 @@ +// This file was autogenerated, do not modify by hand. +pub mod lwe_keyswitch; +pub mod lwe_programmable_bootstrap; +pub mod secure_noise; diff --git a/tfhe/src/core_crypto/commons/noise_formulas/secure_noise.rs b/tfhe/src/core_crypto/commons/noise_formulas/secure_noise.rs new file mode 100644 index 0000000000..c4629c5a04 --- /dev/null +++ b/tfhe/src/core_crypto/commons/noise_formulas/secure_noise.rs @@ -0,0 +1,29 @@ +// This file was autogenerated, do not modify by hand. +use crate::core_crypto::commons::dispersion::Variance; +use crate::core_crypto::commons::parameters::*; + +pub fn minimal_glwe_variance_for_132_bits_security_gaussian( + glwe_dimension: GlweDimension, + polynomial_size: PolynomialSize, + modulus: f64, +) -> Variance { + let lwe_dimension = glwe_dimension.to_equivalent_lwe_dimension(polynomial_size); + minimal_lwe_variance_for_132_bits_security_gaussian(lwe_dimension, modulus) +} + +pub fn minimal_lwe_variance_for_132_bits_security_gaussian( + lwe_dimension: LweDimension, + modulus: f64, +) -> Variance { + Variance(minimal_variance_for_132_bits_security_gaussian_impl( + lwe_dimension.0 as f64, + modulus, + )) +} + +pub fn minimal_variance_for_132_bits_security_gaussian_impl( + lwe_dimension: f64, + modulus: f64, +) -> f64 { + (5.31469187675068 - 0.0497829131652661 * lwe_dimension).exp2() + 16.0 * modulus.powf(-2.0) +}