Tutorial

Quick Start

The basic steps for using the high-level API of TFHE-rs are:

  1. Importing TFHE-rs prelude;

  2. Client-side: Configuring and creating keys;

  3. Client-side: Encrypting data;

  4. Server-side: Setting the server key;

  5. Server-side: Computing over encrypted data;

  6. Client-side: Decrypting data.

Here is the full example (mixing client and server parts):

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

fn main() {
    let config = ConfigBuilder::all_disabled()
        .enable_default_uint8()
        .build();

    // Client-side
    let (client_key, server_key) = generate_keys(config);

    let clear_a = 27u8;
    let clear_b = 128u8;

    let a = FheUint8::encrypt(clear_a, &client_key);
    let b = FheUint8::encrypt(clear_b, &client_key);

    //Server-side
    set_server_key(server_key);
    let result = a + b;

    //Client-side
    let decrypted_result: u8 = result.decrypt(&client_key);

    let clear_result = clear_a + clear_b;

    assert_eq!(decrypted_result, clear_result);
}

Default configuration for x86 Unix machines:

Other configurations can be found here.

Imports.

tfhe uses traits to have a consistent API for creating FHE types and enable users to write generic functions. To be able to use associated functions and methods of a trait, the trait has to be in scope.

To make it easier, the prelude 'pattern' is used. All tfhe important traits are in a prelude module that you glob import. With this, there is no need to remember or know the traits to import.

1. Configuring and creating keys.

The first step is the creation of the configuration. The configuration is used to declare which type you will use or not use, as well as enabling you to use custom crypto-parameters for these types for more advanced usage / testing.

Creating a configuration is done using the ConfigBuilder type.

In this example, 8-bit unsigned integers with default parameters are used. The integers feature must also be enabled, as per the table on the Getting Started page.

The config is done by first creating a builder with all types deactivated. Then, the uint8 type with default parameters is activated.

The generate_keys command returns a client key and a server key.

The client_key is meant to stay private and not leave the client whereas the server_key can be made public and sent to a server for it to enable FHE computations.

2. Setting the server key.

The next step is to call set_server_key

This function will move the server key to an internal state of the crate and manage the details to give a simpler interface.

3. Encrypting data.

Encrypting data is done via the encrypt associated function of the [FheEncrypt] trait.

Types exposed by this crate implement at least one of [FheEncrypt] or [FheTryEncrypt] to allow enryption.

4. Computation and decryption.

Computations should be as easy as normal Rust to write, thanks to operator overloading.

The decryption is done by using the decrypt method, which comes from the [FheDecrypt] trait.

A first complete example: FheLatinString (Integer)

The goal of this tutorial is to build a data type that represents a Latin string in FHE while implementing the to_lower and to_upper functions.

The allowed characters in a Latin string are:

  • Uppercase letters: A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

  • Lowercase letters: a b c d e f g h i j k l m n o p q r s t u v w x y z

For the code point of the letters,ascii codes are used:

  • The uppercase letters are in the range [65, 90]

  • The lowercase letters are in the range [97, 122]

lower_case = upper_case + 32 <=> upper_case = lower_case - 32

For this type, the FheUint8 type is used.

Types and methods.

This type will hold the encrypted characters as a Vec<FheUint8>, as well as the encrypted constant 32 to implement the functions that change the case.

In the FheLatinString::encrypt function, some data validation is done:

  • The input string can only contain ascii letters (no digit, no special characters).

  • The input string cannot mix lower and upper case letters.

These two points are to work around a limitation of FHE. It is not possible to create branches, meaning the function cannot use conditional statements. Checking if the 'char' is an uppercase letter to modify it to a lowercase one cannot be done, like in the example below.

With these preconditions checked, implementing to_lower and to_upper is rather simple.

To use the FheUint8 type, the integer feature must be activated:

Other configurations can be found here.

A more complex example: Parity Bit (Boolean)

This example is dedicated to the building of a small function that homomorphically computes a parity bit.

First, a non-generic function is written. Then, generics are used to handle the case where the function inputs are both FheBools and clear bools.

The parity bit function takes as input two parameters:

  • A slice of Boolean

  • A mode (Odd or Even)

This function returns a Boolean that will be either true or false so that the sum of Booleans (in the input and the returned one) is either an Odd or Even number, depending on the requested mode.


Non-generic version.

To use Booleans, the booleans feature in our Cargo.toml must be enabled:

Other configurations can be found here.

function definition

First, the verification function is defined.

The way to find the parity bit is to initialize it to false, then XOR it with all the bits, one after the other, adding negation depending on the requested mode.

A validation function is also defined to sum together the number of the bit set within the input with the computed parity bit and check that the sum is an even or odd number, depending on the mode.

final code

After the mandatory configuration steps, the function is called:


Generic version.

To make the compute_parity_bit function compatible with both FheBool and bool, generics have to be used.

Writing a generic function that accepts FHE types as well as clear types can help test the function to see if it is correct. If the function is generic, it can run with clear data, allowing the use of print-debugging or a debugger to spot errors.

Writing generic functions that use operator overloading for our FHE types can be trickier than normal, since FHE types are not copy. So using the reference & is mandatory, even though this is not the case when using native types, which are all Copy.

This will make the generic bounds trickier at first.

writing the correct trait bounds

The function has the following signature:

To make it generic, the first step is:

Next, the generic bounds have to be defined with the where clause.

In the function, the following operators are used:

  • ! (trait: Not)

  • ^ (trait: BitXor)

By adding them to where, this gives:

However, the compiler will complain:

fhe_bit is a reference to a BoolType (&BoolType) since it is borrowed from the fhe_bits slice when iterating over its elements. The first try is to change the BitXor bounds to what the Compiler suggests by requiring &BoolType to implement BitXor and not BoolType.

The Compiler is still not happy:

The way to fix this is to use Higher-Rank Trait Bounds:

The final code will look like this:

final code

Here is a complete example that uses this function for both clear and FHE values:

Last updated

Was this helpful?