Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test(core): add noise formulas and variance tests for KS and PBS #1508

Merged
merged 1 commit into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<Scalar: UnsignedTorus + CastInto<usize>>(
params: ClassicTestParams<Scalar>,
) {
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 * <Scalar as CastInto<usize>>::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,
});
Original file line number Diff line number Diff line change
@@ -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<Scalar>(params: ClassicTestParams<Scalar>)
where
Scalar: UnsignedTorus + Sync + Send + CastFrom<usize> + CastInto<usize>,
{
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 * <Scalar as CastInto<usize>>::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
});
Loading
Loading