Ggsw external product not working

Hello, I tried to use the ggsw external product with the function “discard_compute_external_product_glwe_ciphertext_ggsw_ciphertext” using the example from https://docs.rs/concrete-core/latest/concrete_core/backends/fft/engines/struct.FftEngine.html#method.discard_compute_external_product_glwe_ciphertext_ggsw_ciphertext.

However, when I try to decrypt the result with the following code, it seems like the product was not saved in the variable “product” and instead, it only saved the glwe.clone

    let plaintext = engine.decrypt_glwe_ciphertext(&key, &product)?;
    let output: Vec<u32> = engine.retrieve_plaintext_vector(&plaintext)?;

    println!("{}",output[1]>>20);

I would like to know how to solve this problem.
Thank you for your support.

Hello!
Indeed, I can reproduce the behavior on my laptop. The bootstrap tests pass so we know the external product implementation itself should be correct, but I don’t know yet why the external product as a standalone function doesn’t work as expected. We’ll dig into this issue and come back to you as soon as possible.

Best regards,
Agnes

Hello, and thank you for your patience!

We found two issues that explain your problem:

  1. the discarding external product function was not resetting the given output to 0 (and thus it was not discarding the result)
  2. the example in the documentation is misleading because the value encrypted in the GGSW is encoded, whereas it should not be the case.

We merged a fix for those issues, and published a corrective release (1.0.2). I hope it helps!

Cheers,
Agnès

2 Likes

Thank you for the update

Hello,
I tested the function again (after updating the cargo to concrete core 1.02) on the new example and now, what I obtain is a random value that is different every time.

Thank you for your efforts,
Victor

It’s possible that the noise variance you use is too high, in the doc test we give the example with:

let noise = Variance(2_f64.powf(-25.));

but if you use a smaller value, like 2^-50, you should start getting more understandable results.

Also the decrypt function in Concrete-core does not perform the rounding (this is handled in Concrete itself). To decrypt & round a message that was shifted 60 bits to the left and encrypted with 64 bits you can do:

    let plaintext_vector = default_engine.decrypt_glwe_ciphertext(&key, &product).unwrap();
    let mut output: Vec<u64> = default_engine.retrieve_plaintext_vector(&plaintext_vector).unwrap();
    for p in output.iter_mut() {
        *p = *p >> 59;
        let carry = *p % 2;
        *p = ((*p >> 1) + carry) % (1 << (64 - 60));
    }

I tested with a very low noise, and added the rounding to my code which is the following


// DISCLAIMER: the parameters used here are only for test purpose, and are not secure.
    let glwe_dimension = GlweDimension(2);
    let polynomial_size = PolynomialSize(256);
    let level = DecompositionLevelCount(1);
    let base_log = DecompositionBaseLog(4);
// Here a hard-set encoding is applied (shift by 20 bits)
    let input_ggsw = 3_u32;
    let input_glwe = vec![3_u32 << 20; polynomial_size.0];
    let noise = Variance(2_f64.powf(-250.));

    // Unix seeder must be given a secret input.
// Here we just give it 0, which is totally unsafe.
    const UNSAFE_SECRET: u128 = 0;
    let mut default_engine = DefaultEngine::new(Box::new(UnixSeeder::new(UNSAFE_SECRET)))?;
    let mut fft_engine = FftEngine::new(())?;
    let key: GlweSecretKey32 =
        default_engine.generate_new_glwe_secret_key(glwe_dimension, polynomial_size)?;
    let plaintext_ggsw = default_engine.create_plaintext_from(&input_ggsw)?;
    let plaintext_glwe = default_engine.create_plaintext_vector_from(&input_glwe)?;

    let ggsw = default_engine.encrypt_scalar_ggsw_ciphertext(
        &key,
        &plaintext_ggsw,
        noise,
        level,
        base_log,
    )?;
    let complex_ggsw: FftFourierGgswCiphertext32 = fft_engine.convert_ggsw_ciphertext(&ggsw)?;
    let glwe = default_engine.encrypt_glwe_ciphertext(&key, &plaintext_glwe, noise)?;

// We allocate an output ciphertext simply by cloning the input.
// The content of this output ciphertext will by wiped by the external product.
    let mut product = glwe.clone();
    fft_engine.discard_compute_external_product_glwe_ciphertext_ggsw_ciphertext(
        &glwe,
        &complex_ggsw,
        &mut product,
    )?;


    let plaintext = default_engine.decrypt_glwe_ciphertext(&key, &product)?;
    let mut output: Vec<u32> = default_engine.retrieve_plaintext_vector(&plaintext)?;
    for p in output.iter_mut() {
        *p = *p >> 19;
        let carry = *p % 2;
        *p = ((*p >> 1) + carry) % (1 << (32 - 20));
    }

    println!("{}", output[3]);


    Ok(())
}```

Yet, what I retrieve is a multiple of 256 instead of 9.

Thank you again,
Victor

What if you try to replace
let mut product = glwe.clone();
by

let zero_input_glwe = vec![0_u64; polynomial_size.0];
let zero_plaintext_glwe = default_engine.create_plaintext_vector_from(&zero_input_glwe).unwrap();
let mut product = default_engine.trivially_encrypt_glwe_ciphertext(glwe_dimension.to_glwe_size(), &zero_plaintext_glwe).unwrap();

?

I get the same results, only multiple of 256. Also, I work with 32 bits and not 64.

Strange, here’s the example code I have that works with Concrete-core 1.0.2:

use concrete_core::prelude::*;

/*
    cargo run --release
*/
fn main() {
    let glwe_dimension = GlweDimension(1);
    let polynomial_size = PolynomialSize(256);
    let level = DecompositionLevelCount(3);
    let base_log = DecompositionBaseLog(8);
    // Here a hard-set encoding is applied (shift by 20 bits)
    let input_ggsw = 3_u64;
    let input_glwe = vec![2_u64 << 60; polynomial_size.0];
    let zero_input_glwe = vec![1_u64 << 60; polynomial_size.0];
    let noise = Variance(2_f64.powf(-10000.));

    // Unix seeder must be given a secret input.
    // Here we just give it 0, which is totally unsafe.
    const UNSAFE_SECRET: u128 = 0;
    let mut default_engine = DefaultEngine::new(Box::new(UnixSeeder::new(UNSAFE_SECRET))).unwrap();
    let mut fft_engine = FftEngine::new(()).unwrap();
    let mut key: GlweSecretKey64 =
        default_engine.generate_new_glwe_secret_key(glwe_dimension, polynomial_size).unwrap();
    let plaintext_ggsw = default_engine.create_plaintext_from(&input_ggsw).unwrap();
    let plaintext_glwe = default_engine.create_plaintext_vector_from(&input_glwe).unwrap();
    let zero_plaintext_glwe = default_engine.create_plaintext_vector_from(&zero_input_glwe).unwrap();

    let ggsw = default_engine.encrypt_scalar_ggsw_ciphertext(
        &key,
        &plaintext_ggsw,
        noise,
        level,
        base_log,
        ).unwrap();
    let complex_ggsw: FftFourierGgswCiphertext64 =
        fft_engine.convert_ggsw_ciphertext(&ggsw).unwrap();
    let glwe = default_engine.encrypt_glwe_ciphertext(&key,
                                                      &plaintext_glwe, noise).unwrap();

    // We allocate an output ciphertext simply by cloning the input.
    // The content of this output ciphertext will by wiped by the
    // external product.
    let mut product = default_engine.trivially_encrypt_glwe_ciphertext(glwe_dimension.to_glwe_size(), &zero_plaintext_glwe).unwrap();

    fft_engine.discard_compute_external_product_glwe_ciphertext_ggsw_ciphertext(
        &glwe,
        &complex_ggsw,
        &mut product,
        ).unwrap();

    let plaintext = default_engine.decrypt_glwe_ciphertext(&key, &product).unwrap();
    let mut output: Vec<u64> = default_engine.retrieve_plaintext_vector(&plaintext).unwrap();
    for p in output.iter_mut() {
        *p = *p >> 59;
        let carry = *p % 2;
        *p = ((*p >> 1) + carry) % (1 << (64 - 60));
    }

    println!("{:?}",output);


}

I tried your example and it is working.
The problem in my example was coming from the parameter DecompositionLevelCount (At 1 it outputs random results, at 2 it outputs close results and at 3 it seems to always work).

Thank you a lot for your help and patience.
Victor

2 Likes