TFHE-rs v0.4.0: Signed Integers and Encrypted Conditionals

October 17, 2023
Jean-Baptiste Orfila

This new version of TFHE-rs introduces support for signed integers, conditional instructions and significant performance improvements.

New type: FheInt

The new version of TFHE-rs now supports signed integers. Similarly to what is proposed with the [.c-inline-code]FheUint[.c-inline-code] type, the API now includes the signed integer type [.c-inline-code]FheInt[.c-inline-code]. This mimics the behavior of the classical [.c-inline-code]int[.c-inline-code] from many programming languages. In general, an unsigned integer encoded over 32-bits, like a [.c-inline-code]uint32[.c-inline-code], allows representing values between 0 and 232-1. That is, it represents integers modulo 232. When working with 32-bit signed integers, the domain becomes [-231, 231-1].  All operations available in TFHE-rs for unsigned integers are now also available for signed integers, with similar timings. Note that this is also true for operations between a ciphertext and a cleartext. Finally, casting from an [.c-inline-code]FheUint[.c-inline-code] to an [.c-inline-code]FheInt[.c-inline-code] is supported (and vice versa). By default, all precisions up to 256-bits are available. In Table 1, timings of signed operations between two ciphertexts are given:

Table 1: Benchmarks of operations depending on the precision
Operation / Precision FheInt8 FheInt16 FheInt32 FheInt64
add 76 ms 99 ms 120 ms 169 ms
sub 85 ms 100 ms 125 ms 169 ms
mul 139 ms 192 ms 315 ms 744 ms
and/or/xor 19 ms 20 ms 21 ms 25 ms
left/right shift/rotate 99 ms 121 ms 175 ms 233 ms
eq/neq 27 ms 37 ms 56 ms 61 ms
lt/le/gt/ge 71 ms 90 ms 115 ms 145 ms
div/rem 935 ms 2.13 s 5.1 s 12.2 s

Below we outline a small code example on how to use signed integers in the latest version of TFHE-rs:

use tfhe::prelude::*;
use tfhe::{generate_keys, set_server_key, ConfigBuilder, FheInt32, FheUint32};

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

	// Key generation
	let (client_key, server_keys) = generate_keys(config);

	let clear_a = 1344i32;
	let clear_b = -87825i32;
	let clear_c = 118038950i32;

	// Encrypting the input data using the (private) client_key
	// FheInt32: Encrypted equivalent to i32
	let encrypted_a = FheInt32::try_encrypt(clear_a, &client_key)?;
	let encrypted_b = FheInt32::try_encrypt(clear_b, &client_key)?;
	let encrypted_c = FheInt32::try_encrypt(clear_c, &client_key)?;

	// On the server side:
	set_server_key(server_keys);

	// Clear equivalent computation: '1344 * (-87825) = -118036800'
	let encrypted_mul = &encrypted_a * &encrypted_b;

	// Clear equivalent computation: '-118036800 + 118038950 = 2150'
	let encrypted_add = &encrypted_mul + &encrypted_c;

	// Clear equivalent computation: '-118036800 > 2150'
	let encrypted_comp = &encrypted_mul.gt(&encrypted_add);

	//Clear equivalent computation: 'as u32'
	let encrypted_cast: FheUint32 = encrypted_a.cast_into();

	// Client side
	// Decryption
	let clear_res: i32 = encrypted_comp.decrypt(&client_key);
	assert_eq!(
    	clear_res,
    	((clear_a * clear_b) > (clear_a * clear_b + clear_c)) as i32
	);

	let clear_res: u32 = encrypted_cast.decrypt(&client_key);
	assert_eq!(clear_res, clear_a as u32);

	Ok(())
}

Beyond the update of the default API (the one described in the previous example), all of the underlying APIs have been updated with this new type.

Support of conditional instructions

It is now possible to compute simple ternary homomorphic conditional instructions of the form [.c-inline-code]If...Then...Else[.c-inline-code]. The evaluated condition should be an encrypted boolean value represented as a classical [.c-inline-code]FheUint[.c-inline-code] or [.c-inline-code]FheInt[.c-inline-code]. This allows a ciphertext, or an expression, to be chosen depending on the value of the condition. The example below shows how to use this feature:

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

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

	// Key generation
	let (client_key, server_keys) = generate_keys(config);

	let clear_a = 32i32;
	let clear_b = -45i32;

	// Encrypting the input data using the (private) client_key
	// FheInt32: Encrypted equivalent to i32
	let encrypted_a = FheInt32::try_encrypt(clear_a, &client_key)?;
	let encrypted_b = FheInt32::try_encrypt(clear_b, &client_key)?;

	// On the server side:
	set_server_key(server_keys);

	// Clear equivalent computations: 32 > -45
	let encrypted_comp = &encrypted_a.gt(&encrypted_b);
	let clear_res: i32 = encrypted_comp.decrypt(&client_key);
	assert_eq!(clear_res, (clear_a > clear_b) as i32);

	// `encrypted_comp` contains the result of the comparison, i.e.,
	// a boolean value. This acts as a condition on which the
	// `if_then_else` function can be applied on.
	// Clear equivalent computations:
	// if 32 > -45 {result = 32} else {result = -45}
let encrypted_res = &encrypted_comp.if_then_else(&encrypted_a, &encrypted_b);

	let clear_res: i32 = encrypted_res.decrypt(&client_key);
	assert_eq!(clear_res, clear_a);

	Ok(())
}

Performance Improvements

This latest release of TFHE-rs includes many performance improvements. On the low-level primitives, the programmable bootstrapping is now slightly faster. As a result, the latency of all integer operations has been reduced by approximately 8%. The practical gain depends on the operation and precision, ranging from 3 to 20%. 

Some specific operations have undergone significant optimizations, such as the multiplication between a clear value and a ciphertext, which is now 30 to 50% faster.

Additionally, the division between two ciphertexts is almost twice as fast in comparison to the previous version.

Additional links

Read more related posts