Inconsistent Integer API when using (Ciphertext, Ciphertext) versus (Ciphertext, Scalar) operations

Hi all,

TLDR: I’d like to be able to call the Ciphertext.op(&Scalar) version of the Integer API comparison operations just like I can call Ciphertext.op(&Ciphertext). Right now, only Ciphertext.op(Scalar) (owned not borrowed) works.


I would like to evaluate two different settings for the same code using the Integer API and involving two actors:

  • Alice) has sensitive data
  • Bob) has proprietary logic that could be protected by the values used in the circuit (no need for circuit privacy)

I have a working prototype where both users encrypt their data / logic (to the same key for simplicity) and we assume execution takes place at a third-party server.

I’d like to evaluate a different setting where one of the two actors acts as the execution server, and doesn’t need to encrypt its input anymore.

The current design of the library won’t allow me to switch seamlessly between the encrypted and cleartext version, without adding tons of boilerplate (and even with that, I haven’t had much luck…). Consider the following example:

    let (cks, sks) = crate::utils::serde::gen_keys(false);
    set_server_key(sks);

    let ct_1 = FheUint64::try_encrypt(88_u64, &cks).unwrap();
    let constant2 = 1000_u64;
    let ct_3 = FheUint64::try_encrypt(99_u64, &cks).unwrap();

    let test_ge_ok        = ct_1.ge(constant2); // works
    let test_ge2_dead = ct_1.ge(&constant2); // doesn't compile
    let test_ge3_ok        = ct_1.ge(ct_3); // works
    let test_ge4_ok = ct_1.ge(&ct_3); // works

The second line doesn’t compile since the u64 is borrowed and FheOrd is only implemented for u64 (Clear) but not &u64 :frowning:

the trait bound `&u64: tfhe::integer::block_decomposition::DecomposableInto<u64>` is not satisfied
the following other types implement trait `tfhe::integer::block_decomposition::DecomposableInto<T>`:
  <u64 as tfhe::integer::block_decomposition::DecomposableInto<u64>>
  <u64 as tfhe::integer::block_decomposition::DecomposableInto<u8>>
required for `tfhe::high_level_api::integers::types::base::GenericInteger<tfhe::high_level_api::integers::types::static_::FheUint64Parameters>` to implement `tfhe::prelude::FheOrd<&u64>`

On the other hand, calling ge() on an owned or borrowed FheUint since both implementations are available here.

My question (finally!) is thus:

  • would you consider supporting borrowed Clear as well and adding a new implementation?
  • can you suggest a workaround that would allow me to get the same behaviour without adding tons of branching boilerplate to my code?

Thanks,
J

Hello,

Yes I think we can consider adding possibilities for operations using &Scalar.
Rust has them, and since we are trying to match it, we could add them too.

 let a = -1i8;
 let b = -2i8;
 
 let c: i8 = &a + &b;
 println!("c: {}", c);

As for the workaround I don’t think there’s an easy one/boiler-plate-free one until we add this feature

Hello @tmontaigu !

Great to hear that you’d be considering it, happy to assist with early tests or other discussions if needed.

I’ll report any breakthrough on a workaround if I find one in the meatime :slight_smile:

I think the solution with the lowest boiler plate is to play with references and dereferencing.

For u64 type, you take a &u64 and for Ciphertext you take a &&FheUint64, you dereference it,
which will give you a u64 or a &FheUint64.

use tfhe::prelude::*;
use tfhe::{generate_keys, set_server_key, ConfigBuilder, FheUint64, FheUint8};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Basic configuration to use homomorphic integers
    let config = ConfigBuilder::all_disabled()
        .enable_default_integers()
        .build();

    // Key generation
    let (cks, sks) = generate_keys(config);

    set_server_key(sks);

    let ct_1 = FheUint64::try_encrypt(88_u64, &cks).unwrap();
    
    let constant2 = 1000_u64;
    let constant2: &u64 = &constant2;
    
    let ct_3 = FheUint64::try_encrypt(99_u64, &cks).unwrap();
    let ct_3: &&FheUint64 = &&ct_3;

    let test_ge_ok = ct_1.ge(*constant2); // works
    let test_ge2_ok = ct_1.ge(*ct_3); // works

    Ok(())
}

This example compile, i’m not sure how applicable its in your code