commit acbaad8f4cfdaa62e03ca6847189d37575e12201 Author: Mayeul@Zama Date: Thu Dec 8 12:18:53 2022 +0100 add concrete-cpu-noise-model crate diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 000000000..c2215fb7a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +# see https://doc.rust-lang.org/cargo/reference/manifest.html +name = "concrete-cpu-noise-model" +version = "0.1.0" +authors = [""] +edition = "2021" + +[dependencies] + +[dev-dependencies] +approx = "0.5" diff --git a/src/gaussian_noise.rs b/src/gaussian_noise.rs new file mode 100644 index 000000000..d955f49af --- /dev/null +++ b/src/gaussian_noise.rs @@ -0,0 +1,3 @@ +pub mod conversion; +pub mod noise; +pub mod security; diff --git a/src/gaussian_noise/conversion.rs b/src/gaussian_noise/conversion.rs new file mode 100644 index 000000000..548cd6d0c --- /dev/null +++ b/src/gaussian_noise/conversion.rs @@ -0,0 +1,15 @@ +fn modular_variance_variance_ratio(ciphertext_modulus_log: u32) -> f64 { + 2_f64.powi(2 * ciphertext_modulus_log as i32) +} + +pub fn modular_variance_to_variance(modular_variance: f64, ciphertext_modulus_log: u32) -> f64 { + modular_variance / modular_variance_variance_ratio(ciphertext_modulus_log) +} + +pub fn variance_to_modular_variance(variance: f64, ciphertext_modulus_log: u32) -> f64 { + variance * modular_variance_variance_ratio(ciphertext_modulus_log) +} + +pub fn variance_to_std_dev(variance: f64) -> f64 { + variance.sqrt() +} diff --git a/src/gaussian_noise/noise.rs b/src/gaussian_noise/noise.rs new file mode 100644 index 000000000..0c4b5976e --- /dev/null +++ b/src/gaussian_noise/noise.rs @@ -0,0 +1,7 @@ +pub mod blind_rotate; +pub mod cmux; +pub mod external_product_glwe; +pub mod keyswitch; +pub mod keyswitch_one_bit; +pub mod modulus_switching; +pub mod private_packing_keyswitch; diff --git a/src/gaussian_noise/noise/blind_rotate.rs b/src/gaussian_noise/noise/blind_rotate.rs new file mode 100644 index 000000000..18f1cc780 --- /dev/null +++ b/src/gaussian_noise/noise/blind_rotate.rs @@ -0,0 +1,100 @@ +use super::cmux::variance_cmux; + +pub const FFT_SCALING_WEIGHT: f64 = -2.577_224_94; + +/// Final reduced noise generated by the final bootstrap step. +/// Note that it does not depends from input noise, assuming the bootstrap is successful +pub fn variance_blind_rotate( + in_lwe_dimension: u64, + out_glwe_dimension: u64, + out_polynomial_size: u64, + log2_base: u64, + level: u64, + ciphertext_modulus_log: u32, + variance_bsk: f64, +) -> f64 { + in_lwe_dimension as f64 + * variance_cmux( + out_glwe_dimension, + out_polynomial_size, + log2_base, + level, + ciphertext_modulus_log, + variance_bsk, + ) +} + +#[cfg(test)] +mod tests { + use crate::gaussian_noise::conversion::variance_to_modular_variance; + use crate::gaussian_noise::security::minimal_variance_glwe; + + use super::*; + + #[test] + fn security_variance_bootstrap_1() { + let ref_modular_variance = 4.078_296_369_990_673e31; + + let polynomial_size = 1 << 12; + let glwe_dimension = 2; + + let ciphertext_modulus_log = 64; + let security = 128; + let variance_bsk = minimal_variance_glwe( + glwe_dimension, + polynomial_size, + ciphertext_modulus_log, + security, + ); + + let actual = variance_blind_rotate( + 2048, + glwe_dimension, + polynomial_size, + 24, + 2, + ciphertext_modulus_log, + variance_bsk, + ); + + approx::assert_relative_eq!( + variance_to_modular_variance(actual, ciphertext_modulus_log), + ref_modular_variance, + max_relative = 1e-8 + ); + } + + #[test] + fn golden_python_prototype_security_variance_bootstrap_2() { + // golden value include fft correction + let golden_modular_variance = 3.269_722_907_894_341e55; + + let polynomial_size = 1 << 12; + let glwe_dimension = 4; + + let ciphertext_modulus_log = 128; + let security = 128; + let variance_bsk = minimal_variance_glwe( + glwe_dimension, + polynomial_size, + ciphertext_modulus_log, + security, + ); + + let actual = variance_blind_rotate( + 1024, + glwe_dimension, + polynomial_size, + 5, + 9, + ciphertext_modulus_log, + variance_bsk, + ); + + approx::assert_relative_eq!( + variance_to_modular_variance(actual, ciphertext_modulus_log), + golden_modular_variance, + max_relative = 1e-8 + ); + } +} diff --git a/src/gaussian_noise/noise/cmux.rs b/src/gaussian_noise/noise/cmux.rs new file mode 100644 index 000000000..23791d7fc --- /dev/null +++ b/src/gaussian_noise/noise/cmux.rs @@ -0,0 +1,20 @@ +use super::external_product_glwe::variance_external_product_glwe; + +// only valid in the blind rotate case +pub fn variance_cmux( + glwe_dimension: u64, + polynomial_size: u64, + log2_base: u64, + level: u64, + ciphertext_modulus_log: u32, + variance_ggsw: f64, +) -> f64 { + variance_external_product_glwe( + glwe_dimension, + polynomial_size, + log2_base, + level, + ciphertext_modulus_log, + variance_ggsw, + ) +} diff --git a/src/gaussian_noise/noise/external_product_glwe.rs b/src/gaussian_noise/noise/external_product_glwe.rs new file mode 100644 index 000000000..fc76923e0 --- /dev/null +++ b/src/gaussian_noise/noise/external_product_glwe.rs @@ -0,0 +1,84 @@ +use crate::gaussian_noise::conversion::modular_variance_to_variance; +use crate::utils::square; + +pub fn variance_external_product_glwe( + glwe_dimension: u64, + polynomial_size: u64, + log2_base: u64, + level: u64, + ciphertext_modulus_log: u32, + variance_ggsw: f64, +) -> f64 { + theoretical_variance_external_product_glwe( + glwe_dimension, + polynomial_size, + log2_base, + level, + ciphertext_modulus_log, + variance_ggsw, + ) + fft_noise_variance_external_product_glwe( + glwe_dimension, + polynomial_size, + log2_base, + level, + ciphertext_modulus_log, + ) +} + +fn theoretical_variance_external_product_glwe( + glwe_dimension: u64, + polynomial_size: u64, + log2_base: u64, + level: u64, + ciphertext_modulus_log: u32, + variance_ggsw: f64, +) -> f64 { + let variance_key_coefficient_binary: f64 = + modular_variance_to_variance(1. / 4., ciphertext_modulus_log); + + let square_expectation_key_coefficient_binary: f64 = + modular_variance_to_variance(square(1. / 2.), ciphertext_modulus_log); + + let k = glwe_dimension as f64; + let b = 2_f64.powi(log2_base as i32); + let b2l = 2_f64.powi((log2_base * 2 * level) as i32); + let l = level as f64; + let big_n = polynomial_size as f64; + let q_square = 2_f64.powi(2 * ciphertext_modulus_log as i32); + + let res_1 = l * (k + 1.) * big_n * (square(b) + 2.) / 12. * variance_ggsw; + let res_2 = (q_square - b2l) / (24. * b2l) + * (modular_variance_to_variance(1., ciphertext_modulus_log) + + k * big_n + * (variance_key_coefficient_binary + square_expectation_key_coefficient_binary)) + + k * big_n / 8. * variance_key_coefficient_binary + + 1. / 16. * square(1. - k * big_n) * square_expectation_key_coefficient_binary; + + res_1 + res_2 +} + +const FFT_SCALING_WEIGHT: f64 = -2.577_224_94; + +/// Additional noise generated by fft computation + +fn fft_noise_variance_external_product_glwe( + glwe_dimension: u64, + polynomial_size: u64, + log2_base: u64, + level: u64, + ciphertext_modulus_log: u32, +) -> f64 { + // https://github.com/zama-ai/concrete-optimizer/blob/prototype/python/optimizer/noise_formulas/bootstrap.py#L25 + let b = 2_f64.powi(log2_base as i32); + let l = level as f64; + let big_n = polynomial_size as f64; + let k = glwe_dimension; + assert!(k > 0, "k = {k}"); + assert!(k < 7, "k = {k}"); + + // 22 = 2 x 11, 11 = 64 -53 + let scale_margin = (1_u64 << 22) as f64; + let res = + f64::exp2(FFT_SCALING_WEIGHT) * scale_margin * l * b * b * big_n.powi(2) * (k as f64 + 1.); + modular_variance_to_variance(res, ciphertext_modulus_log) +} diff --git a/src/gaussian_noise/noise/keyswitch.rs b/src/gaussian_noise/noise/keyswitch.rs new file mode 100644 index 000000000..b8170eb18 --- /dev/null +++ b/src/gaussian_noise/noise/keyswitch.rs @@ -0,0 +1,76 @@ +use super::keyswitch_one_bit::variance_keyswitch_one_bit; + +/// Additional noise generated by the keyswitch step. +pub fn variance_keyswitch( + input_lwe_dimension: u64, //n_big + log2_base: u64, + level: u64, + ciphertext_modulus_log: u32, + variance_ksk: f64, +) -> f64 { + input_lwe_dimension as f64 + * variance_keyswitch_one_bit(log2_base, level, ciphertext_modulus_log, variance_ksk) +} + +#[cfg(test)] +mod tests { + + use crate::gaussian_noise::conversion::variance_to_modular_variance; + use crate::gaussian_noise::security::minimal_variance_lwe; + + use super::*; + + #[test] + fn golden_python_prototype_security_variance_keyswitch_1() { + let golden_modular_variance = 5.997_880_135_602_194e68; + let internal_ks_output_lwe_dimension = 1024; + let ciphertext_modulus_log = 128; + let security = 128; + + let actual = variance_keyswitch( + 4096, + 5, + 9, + ciphertext_modulus_log, + minimal_variance_lwe( + internal_ks_output_lwe_dimension, + ciphertext_modulus_log, + security, + ), + ); + + approx::assert_relative_eq!( + variance_to_modular_variance(actual, ciphertext_modulus_log), + golden_modular_variance, + max_relative = 1e-8 + ); + } + + #[test] + fn golden_python_prototype_security_variance_keyswitch_2() { + // let golden_modular_variance = 8.580795457940938e+66; + // the full npe implements a part of the full estimation + let golden_modular_variance = 7.407_691_550_271_225e48; // full estimation + let internal_ks_output_lwe_dimension = 512; + let ciphertext_modulus_log = 64; + let security = 128; + + let actual = variance_keyswitch( + 2048, + 24, + 2, + ciphertext_modulus_log, + minimal_variance_lwe( + internal_ks_output_lwe_dimension, + ciphertext_modulus_log, + security, + ), + ); + + approx::assert_relative_eq!( + variance_to_modular_variance(actual, ciphertext_modulus_log), + golden_modular_variance, + max_relative = 1e-8 + ); + } +} diff --git a/src/gaussian_noise/noise/keyswitch_one_bit.rs b/src/gaussian_noise/noise/keyswitch_one_bit.rs new file mode 100644 index 000000000..dbaad3437 --- /dev/null +++ b/src/gaussian_noise/noise/keyswitch_one_bit.rs @@ -0,0 +1,32 @@ +use crate::gaussian_noise::conversion::modular_variance_to_variance; +use crate::utils::square; + +/// Additional noise generated by the bit multiplication +pub fn variance_keyswitch_one_bit( + log2_base: u64, + level: u64, + ciphertext_modulus_log: u32, + variance_ksk: f64, +) -> f64 { + let variance_key_coefficient_binary: f64 = + modular_variance_to_variance(1. / 4., ciphertext_modulus_log); + + let square_expectation_key_coefficient_binary: f64 = + modular_variance_to_variance(square(1. / 2.), ciphertext_modulus_log); + + let base = 2_f64.powi(log2_base as i32); + let b2l = 2_f64.powi((log2_base * 2 * level) as i32); + let q_square = 2_f64.powi((2 * ciphertext_modulus_log) as i32); + + // res 2 + let res_2 = (q_square / (12. * b2l) - 1. / 12.) + * (variance_key_coefficient_binary + square_expectation_key_coefficient_binary); + + // res 3 + let res_3 = 1. / 4. * variance_key_coefficient_binary; + + // res 4 + let res_4 = (level as f64) * variance_ksk * (square(base) + 2.) / 12.; + + res_2 + res_3 + res_4 +} diff --git a/src/gaussian_noise/noise/modulus_switching.rs b/src/gaussian_noise/noise/modulus_switching.rs new file mode 100644 index 000000000..27557e4d6 --- /dev/null +++ b/src/gaussian_noise/noise/modulus_switching.rs @@ -0,0 +1,16 @@ +use crate::gaussian_noise::conversion::modular_variance_to_variance; +use crate::utils::square; + +pub fn estimate_modulus_switching_noise_with_binary_key( + internal_ks_output_lwe_dimension: u64, + glwe_log2_polynomial_size: u64, + ciphertext_modulus_log: u32, +) -> f64 { + let nb_msb = glwe_log2_polynomial_size + 1; + + let w = 2_f64.powi(nb_msb as i32); + let n = internal_ks_output_lwe_dimension as f64; + + (1. / 12. + n / 24.) / square(w) + + modular_variance_to_variance(-1. / 12. + n / 48., ciphertext_modulus_log) +} diff --git a/src/gaussian_noise/noise/private_packing_keyswitch.rs b/src/gaussian_noise/noise/private_packing_keyswitch.rs new file mode 100644 index 000000000..2a78b6bdc --- /dev/null +++ b/src/gaussian_noise/noise/private_packing_keyswitch.rs @@ -0,0 +1,39 @@ +use crate::gaussian_noise::conversion::modular_variance_to_variance; +use crate::utils::square; + +// packing private keyswitch for WoP-PBS, described in algorithm 3 of https://eprint.iacr.org/2018/421.pdf (TFHE paper) +pub fn estimate_packing_private_keyswitch( + var_glwe: f64, + var_ggsw: f64, + log2_base: u64, + level: u64, + output_glwe_dimension: u64, + output_polynomial_size: u64, + ciphertext_modulus_log: u32, +) -> f64 { + let variance_key_coefficient_binary: f64 = 1. / 4.; + let expectation_key_coefficient_binary: f64 = 1. / 2.; + + let l = level as f64; + let b = (1 << log2_base) as f64; + let n = (output_glwe_dimension * output_polynomial_size) as f64; // param.internal_lwe_dimension.0 as f64; + let b2l = f64::powi(b, 2 * level as i32); + let var_s_w = 1. / 4.; + let mean_s_w = 1. / 2.; + let res_1 = l * (n + 1.) * var_ggsw * (square(b) + 2.) / 12.; + + #[allow(clippy::cast_possible_wrap)] + let res_3 = (f64::powi(2., 2 * ciphertext_modulus_log as i32) - b2l) / (12. * b2l) + * modular_variance_to_variance( + 1. + n * variance_key_coefficient_binary + square(expectation_key_coefficient_binary), + ciphertext_modulus_log, + ) + + n / 4. + * modular_variance_to_variance(variance_key_coefficient_binary, ciphertext_modulus_log) + + var_glwe * (var_s_w + square(mean_s_w)); + + let res_5 = modular_variance_to_variance(var_s_w, ciphertext_modulus_log) * 1. / 4. + * square(1. - n * expectation_key_coefficient_binary); + + res_1 + res_3 + res_5 +} diff --git a/src/gaussian_noise/security.rs b/src/gaussian_noise/security.rs new file mode 100644 index 000000000..61bbde09f --- /dev/null +++ b/src/gaussian_noise/security.rs @@ -0,0 +1,65 @@ +mod security_weight; + +pub use security_weight::{security_weight, supported_security_levels}; + +/// Noise ensuring security +pub fn minimal_variance_lwe( + lwe_dimension: u64, + ciphertext_modulus_log: u32, + security_level: u64, +) -> f64 { + minimal_variance_glwe(lwe_dimension, 1, ciphertext_modulus_log, security_level) +} + +/// Noise ensuring security +pub fn minimal_variance_glwe( + glwe_dimension: u64, + polynomial_size: u64, + ciphertext_modulus_log: u32, + security_level: u64, +) -> f64 { + let equiv_lwe_dimension = glwe_dimension * polynomial_size; + let security_weights = security_weight(security_level) + .unwrap_or_else(|| panic!("{security_level} bits of security is not supported")); + + let secure_log2_std = + security_weights.secure_log2_std(equiv_lwe_dimension, ciphertext_modulus_log as f64); + let log2_var = 2.0 * secure_log2_std; + f64::exp2(log2_var) +} + +#[cfg(test)] +mod tests { + use super::super::conversion::variance_to_std_dev; + use super::minimal_variance_glwe; + + #[test] + fn golden_python_prototype_security_security_glwe_variance_low() { + // python securityFunc(10,14,64)= 0.3120089883926036 + let integer_size = 64; + let golden_std_dev = 2.168_404_344_971_009e-19; + let security_level = 128; + + let actual = minimal_variance_glwe(10, 1 << 14, integer_size, security_level); + approx::assert_relative_eq!( + golden_std_dev, + variance_to_std_dev(actual), + epsilon = f64::EPSILON + ); + } + + #[test] + fn golden_python_prototype_security_security_glwe_variance_high() { + // python securityFunc(3,8,32)= 2.6011445832514504 + let integer_size = 32; + let golden_std_dev = 4.392_824_146_816_922_4e-6; + let security_level = 128; + + let actual = minimal_variance_glwe(3, 1 << 8, integer_size, security_level); + approx::assert_relative_eq!( + golden_std_dev, + variance_to_std_dev(actual), + epsilon = f64::EPSILON + ); + } +} diff --git a/src/gaussian_noise/security/security_weight.rs b/src/gaussian_noise/security/security_weight.rs new file mode 100644 index 000000000..74c1a294a --- /dev/null +++ b/src/gaussian_noise/security/security_weight.rs @@ -0,0 +1,116 @@ +#[derive(Clone, Copy)] +pub struct SecurityWeights { + slope: f64, + bias: f64, + minimal_lwe_dimension: u64, +} + +impl SecurityWeights { + pub fn secure_log2_std(&self, lwe_dimension: u64, ciphertext_modulus_log: f64) -> f64 { + // ensure to have a minimal on std deviation covering the 2 lowest bits on modular scale + let epsilon_log2_std_modular = 2.0; + let epsilon_log2_std = epsilon_log2_std_modular - (ciphertext_modulus_log); + // ensure the requested lwe_dimension is bigger than the minimal lwe dimension + if self.minimal_lwe_dimension <= lwe_dimension { + f64::max( + self.slope * lwe_dimension as f64 + self.bias, + epsilon_log2_std, + ) + } else { + ciphertext_modulus_log + } + } +} + +// Security curves generated using the lattice-estimator +// (https://github.com/malb/lattice-estimator) on the 24th of June 2022 +const SECURITY_WEIGHTS_ARRAY: [(u64, SecurityWeights); 9] = [ + ( + 80, + SecurityWeights { + slope: -0.040_426_331_193_645_89, + bias: 1.660_978_864_143_672_2, + minimal_lwe_dimension: 450, + }, + ), + ( + 96, + SecurityWeights { + slope: -0.034_147_803_608_670_51, + bias: 2.017_310_258_660_345, + minimal_lwe_dimension: 450, + }, + ), + ( + 112, + SecurityWeights { + slope: -0.029_670_137_081_135_885, + bias: 2.162_463_714_083_856, + minimal_lwe_dimension: 450, + }, + ), + ( + 128, + SecurityWeights { + slope: -0.026_405_028_765_226_22, + bias: 2.482_642_269_104_317_7, + minimal_lwe_dimension: 450, + }, + ), + ( + 144, + SecurityWeights { + slope: -0.023_821_437_305_989_134, + bias: 2.717_778_944_063_667_3, + minimal_lwe_dimension: 450, + }, + ), + ( + 160, + SecurityWeights { + slope: -0.021_743_582_187_160_36, + bias: 2.938_810_548_493_322, + minimal_lwe_dimension: 498, + }, + ), + ( + 176, + SecurityWeights { + slope: -0.019_904_056_582_117_684, + bias: 2.816_125_280_154_224_7, + minimal_lwe_dimension: 551, + }, + ), + ( + 192, + SecurityWeights { + slope: -0.018_610_403_247_590_085, + bias: 3.299_623_684_839_900_8, + minimal_lwe_dimension: 606, + }, + ), + ( + 256, + SecurityWeights { + slope: -0.014_606_812_351_714_953, + bias: 3.849_362_923_469_300_3, + minimal_lwe_dimension: 826, + }, + ), +]; + +pub fn supported_security_levels() -> impl std::iter::Iterator { + SECURITY_WEIGHTS_ARRAY + .iter() + .map(|(security_level, _)| *security_level) +} + +pub fn security_weight(security_level: u64) -> Option { + let index = SECURITY_WEIGHTS_ARRAY + .binary_search_by_key(&security_level, |(security_level, _weights)| { + *security_level + }) + .ok()?; + + Some(SECURITY_WEIGHTS_ARRAY[index].1) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 000000000..8db3e23b0 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,23 @@ +#![warn(clippy::nursery)] +#![warn(clippy::pedantic)] +#![warn(clippy::style)] +#![allow(clippy::cast_lossless)] +#![allow(clippy::cast_precision_loss)] // u64 to f64 +#![allow(clippy::cast_possible_truncation)] // u64 to usize +#![allow(clippy::missing_panics_doc)] +#![allow(clippy::module_name_repetitions)] +#![allow(clippy::must_use_candidate)] +#![allow(clippy::suboptimal_flops)] +#![allow(clippy::cast_possible_wrap)] +#![warn(unused_results)] + +pub mod gaussian_noise; + +pub(crate) mod utils { + pub fn square(v: V) -> V + where + V: std::ops::Mul + Copy, + { + v * v + } +}