Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
To use TFHE-rs
in your project, you first need to add it as a dependency in your Cargo.toml
.
If you are using an x86
machine:
If you are using an ARM
machine:
You need to use a Rust version >= 1.72 to compile TFHE-rs.
When running code that uses TFHE-rs
, it is highly recommended to run in release mode with cargo's --release
flag to have the best possible performance
TFHE-rs is supported on Linux (x86, aarch64), macOS (x86, aarch64) and Windows (x86 with RDSEED
instruction).
📁 Github | 💛 Community support | 🟨 Zama Bounty Program
TFHE-rs is a pure Rust implementation of TFHE for Boolean and integer arithmetics over encrypted data. It includes a Rust and C API, as well as a client-side WASM API.
TFHE-rs is meant for developers and researchers who want full control over what they can do with TFHE, while not worrying about the low level implementation.
The goal is to have a stable, simple, high-performance, and production-ready library for all the advanced features of TFHE.
The TFHE-rs library implements Zama’s variant of Fully Homomorphic Encryption over the Torus (TFHE). TFHE is based on Learning With Errors (LWE), a well-studied cryptographic primitive believed to be secure even against quantum computers.
In cryptography, a raw value is called a message (also sometimes called a cleartext), while an encoded message is called a plaintext and an encrypted plaintext is called a ciphertext.
Zama's variant of TFHE is fully homomorphic and deals with fixed-precision numbers as messages. It implements all needed homomorphic operations, such as addition and function evaluation via Programmable Bootstrapping. You can read more about Zama's TFHE variant in the preliminary whitepaper.
Using FHE in a Rust program with TFHE-rs consists in:
generating a client key and a server key using secure parameters:
a client key encrypts/decrypts data and must be kept secret
a server key is used to perform operations on encrypted data and could be public (also called an evaluation key)
encrypting plaintexts using the client key to produce ciphertexts
operating homomorphically on ciphertexts with the server key
decrypting the resulting ciphertexts into plaintexts using the client key
If you would like to know more about the problems that FHE solves, we suggest you review our 6 minute introduction to homomorphic encryption.
The basic steps for using the high-level API of TFHE-rs are:
Importing the TFHE-rs prelude;
Client-side: Configuring and creating keys;
Client-side: Encrypting data;
Server-side: Setting the server key;
Server-side: Computing over encrypted data;
Client-side: Decrypting data.
Here is a full example (combining the client and server parts):
The default configuration for x86 Unix machines:
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 of the important tfhe
traits are in a prelude
module that you can glob import. With this, there is no need to remember or know the traits that you want to import.
The first step is the creation of the configuration. The configuration is used to declare which type you will (or will not) use, as well as enabling you to use custom crypto-parameters for these types. Custom parameters should only be used for more advanced usage and/or testing.
A configuration can be created by using the ConfigBuilder type.
The config is generated by first creating a builder with all types deactivated. Then, the integer types with default parameters are activated, since we are going to use FheUint8 values.
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.
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.
Encrypting data is achieved via the encrypt
associated function of the FheEncrypt trait.
Types exposed by this crate implement at least one of FheEncrypt or FheTryEncrypt to allow encryption.
Computations should be as easy as normal Rust to write, thanks to the usage of operator overloading.
The decryption is achieved by using the decrypt
method, which comes from the FheDecrypt trait.
OS | x86 | aarch64 |
---|---|---|
The idea of homomorphic encryption is that you can compute on ciphertexts while not knowing messages encrypted within them. A scheme is said to be fully homomorphic, meaning any program can be evaluated with it, if at least two of the following operations are supported ( is a plaintext and is the corresponding ciphertext):
homomorphic univariate function evaluation:
homomorphic addition:
homomorphic multiplication:
Configuration options for different platforms can be found . Other rust and homomorphic types features can be found .
In this example, 8-bit unsigned integers with default parameters are used. The integers
feature must also be enabled, as per the table on .
Linux
x86_64-unix
aarch64-unix
*
macOS
x86_64-unix
aarch64-unix
*
Windows
x86_64
Unsupported
TFHE-rs
includes two main types to represent encrypted data:
FheUint
: this is the homomorphic equivalent of Rust unsigned integers u8, u16, ...
FheInt
: this is the homomorphic equivalent of Rust (signed) integers i8, i16, ...
In the same manner as many programming languages, the number of bits used to represent the data must be chosen when declaring a variable. For instance:
The table below contains an overview of the available operations in TFHE-rs
. The notation Enc
(for Encypted) either refers to FheInt
or FheUint
, for any size between 1 and 256-bits.
More details, and further examples, are given in the following sections.
In TFHE-rs
, integers are used to encrypt all messages which are larger than 4 bits. All supported operations are listed below.
Homomorphic integer types support arithmetic operations.
The list of supported operations is:
For division by 0, the convention is to return modulus - 1
. For instance, for FheUint8
, the modulus is , so a division by 0 will return an encryption of 255. For the remainder operator, the convention is to return the first input without any modification. For instance, if ct1 = FheUint8(63)
and ct2 = FheUint8(0)
then ct1 % ct2
will return FheUint8(63)
.
A simple example of how to use these operations:
Homomorphic integer types support some bitwise operations.
The list of supported operations is:
A simple example of how to use these operations:
Homomorphic integers support comparison operations.
Due to some Rust limitations, it is not possible to overload the comparison symbols because of the inner definition of the operations. This is because Rust expects to have a Boolean as an output, whereas a ciphertext is returned when using homomorphic types.
You will need to use different methods instead of using symbols for the comparisons. These methods follow the same naming conventions as the two standard Rust traits:
The list of supported operations is:
A simple example of how to use these operations:
Homomorphic integers support the min/max operations.
A simple example of how to use these operations:
The ternary conditional operator allows computing conditional instructions of the form if cond { choice_if } else { choice_else }
.
The syntax is encrypted_condition.if_then_else(encrypted_choice_if, encrypted_choice_else)
. The encrypted_condition
should be an encryption of 0 or 1 in order to be valid.
Casting between integer types is possible via the cast_from
associated function or the cast_into
method.
Native homomorphic Booleans support common Boolean operations.
The list of supported operations is:
name | symbol | type |
---|---|---|
name | symbol | type |
---|---|---|
name | symbol | type |
---|---|---|
name | symbol | type |
---|---|---|
name | symbol | type |
---|---|---|
name | symbol | type |
---|---|---|
-
Unary
+
Binary
-
Binary
*
Binary
Div*
/
Binary
Rem*
%
Binary
!
Unary
&
Binary
|
Binary
^
Binary
>>
Binary
<<
Binary
rotate_right
Binary
rotate_left
Binary
eq
Binary
ne
Binary
gt
Binary
ge
Binary
lt
Binary
le
Binary
Min
min
Binary
Max
max
Binary
Ternary operator
if_then_else
Ternary
&
Binary
|
Binary
^
Binary
!
Unary
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 FheBool
s and clear bool
s.
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.
Other configurations can be found here.
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.
After the mandatory configuration steps, the function is called:
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.
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:
Here is a complete example that uses this function for both clear and FHE values:
name
symbol
Enc
/Enc
Enc
/ Int
Neg
-
Add
+
Sub
-
Mul
*
Div
/
Rem
%
Not
!
BitAnd
&
BitOr
|
BitXor
^
Shr
>>
Shl
<<
Min
min
Max
max
Greater than
gt
Greater or equal than
ge
Lower than
lt
Lower or equal than
le
Equal
eq
Cast (into dest type)
cast_into
Cast (from src type)
cast_from
Ternary operator
if_then_else
TFHE-rs
is a cryptographic library dedicated to Fully Homomorphic Encryption. As its name suggests, it is based on the TFHE scheme.
It is necessary to understand some basics about TFHE in order to consider the limitations. Of particular importance are the precision (number of bits used to represent plaintext values) and execution time (why TFHE operations are slower than native operations).
Although there are many kinds of ciphertexts in TFHE, all of the encrypted values in TFHE-rs
are mainly stored as LWE ciphertexts.
The security of TFHE relies on the LWE problem, which stands for Learning With Errors. The problem is believed to be secure against quantum attacks.
An LWE Ciphertext is a collection of 32-bit or 64-bit unsigned integers. Before encrypting a message in an LWE ciphertext, one must first encode it as a plaintext. This is done by shifting the message to the most significant bits of the unsigned integer type used.
Then, a small random value called noise is added to the least significant bits. This noise is crucial in ensuring the security of the ciphertext.
To go from a plaintext to a ciphertext, one must encrypt the plaintext using a secret key.
An LWE secret key is a list of n
random integers: . is called the
An LWE ciphertext is composed of two parts:
The mask
The body
The mask of a fresh ciphertext (one that is the result of an encryption, and not of an operation such as ciphertext addition) is a list of n
uniformly random values.
The body is computed as follows:
Now that the encryption scheme is defined, let's review the example of the addition between ciphertexts to illustrate why it is slower to compute over encrypted data.
To add two ciphertexts, we must add their $mask$ and $body$:
To add ciphertexts, it is sufficient to add their masks and bodies. Instead of just adding two integers, one needs to add elements. This is an intuitive example to show the slowdown of FHE computation compared to plaintext computation, but other operations are far more expensive (e.g., the computation of a lookup table using Programmable Bootstrapping).
In FHE, there are two types of operations that can be applied to ciphertexts:
leveled operations, which increase the noise in the ciphertext
bootstrapped operations, which reduce the noise in the ciphertext
In FHE, noise must be tracked and managed to guarantee the correctness of the computation.
Bootstrapping operations are used across the computation to decrease noise within the ciphertexts, preventing it from tampering with the message. The rest of the operations are called leveled because they do not need bootstrapping operations and are usually very fast as a result.
The following sections explain the concept of noise and padding in ciphertexts.
For it to be secure, LWE requires random noise to be added to the message at encryption time.
In TFHE, this random noise is drawn from a Centered Normal Distribution, parameterized by a standard deviation. The chosen standard deviation has an impact on the security level. With everything else fixed, increasing the standard deviation will lead to an increase in the security level.
In TFHE-rs
, noise is encoded in the least significant bits of each plaintext. Each leveled computation increases the value of the noise. If too many computations are performed, the noise will eventually overflow into the message bits and lead to an incorrect result.
The figure below illustrates this problem in the case of an addition, where an extra bit of noise is incurred as a result.
TFHE-rs
offers the ability to automatically manage noise by performing bootstrapping operations to reset the noise.
The bootstrapping of TFHE has the particularity of being programmable: this means that any function can be homomorphically computed over an encrypted input, while also reducing the noise. These functions are represented by look-up tables. The computation of a PBS is in general either preceded or followed by a keyswitch, which is an operation used to change the encryption key. The output ciphertext is then encrypted with the same key as the input one. To do this, two (public) evaluation keys are required: a bootstrapping key and a keyswitching key. These operations are quite complex to describe, more information about these operations (or about TFHE in general) can be found here TFHE Deep Dive.
Since encoded values have a fixed precision, operating on them can produce results that are outside of the original interval. To avoid losing precision or wrapping around the interval, TFHE-rs
uses additional bits by defining bits of padding on the most significant bits.
As an example, consider adding two ciphertexts. Adding two values could end up outside the range of either ciphertext, and thus necessitate a carry, which would then be carried onto the first padding bit. In the figure below, each plaintext over 32 bits has one bit of padding on its left (i.e., the most significant bit). After the addition, the padding bit is no longer available, as it has been used in order for the carry. This is referred to as consuming bits of padding. Since no padding is left, there is no guarantee that further additions would yield correct results.
By default, the cryptographic parameters provided by TFHE-rs
ensure at least 128 bits of security. The security has been evaluated using the latest versions of the Lattice Estimator (repository) with red_cost_model = reduction.RC.BDGL16
.
For all sets of parameters, the error probability when computing a univariate function over one ciphertext is . Note that univariate functions might be performed when arithmetic functions are computed (i.e., the multiplication of two ciphertexts).
In classical public key encryption, the public key contains a given number of ciphertexts all encrypting the value 0. By setting the number of encryptions to 0 in the public key at , where is the LWE dimension, is the ciphertext modulus, and is the number of security bits. This construction is secure due to the leftover hash lemma, which relates to the impossibility of breaking the underlying multiple subset sum problem. This guarantees both a high-density subset sum and an exponentially large number of possible associated random vectors per LWE sample .
Due to their nature, homomorphic operations are naturally slower than their cleartext equivalents. Some timings are exposed for basic operations. For completeness, benchmarks for other libraries are also given.
All benchmarks were launched on an AWS hpc7a.96xlarge instance with the following specifications: AMD EPYC 9R14 CPU @ 2.60GHz and 740GB of RAM.
This measures the execution time for some operation sets of tfhe-rs::integer (the unsigned version). Note that the timings for FheInt
(i.e., the signed integers) are similar.
The table below reports the timing when the inputs of the benchmarked operation are encrypted.
The table below reports the timing when the left input of the benchmarked operation is encrypted and the other is a clear scalar of the same size.
All timings are related to parallelized Radix-based integer operations, where each block is encrypted using the default parameters (i.e., PARAM_MESSAGE_2_CARRY_2_KS_PBS, more information about parameters can be found here). To ensure predictable timings, the operation flavor is the default
one: the carry is propagated if needed. The operation costs may be reduced by using unchecked
, checked
, or smart
.
This measures the execution time for some operations using various parameter sets of tfhe-rs::shortint. Except for unchecked_add
, all timings are related to the default
operations. This flavor ensures predictable timings for an operation along the entire circuit by clearing the carry space after each operation.
This uses the Concrete FFT + AVX-512 configuration.
This measures the execution time of a single binary Boolean gate.
Using the same hpc7a.96xlarge machine as the one for tfhe-rs, the timings are:
Following the official instructions from OpenFHE, clang14
and the following command are used to setup the project: cmake -DNATIVE_SIZE=32 -DWITH_NATIVEOPT=ON -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DWITH_OPENMP=OFF ..
To use the HEXL library, the configuration used is as follows:
Using the same hpc7a.96xlarge machine as the one for tfhe-rs, the timings are:
TFHE-rs benchmarks can be easily reproduced from source.
If the host machine does not support AVX512, then turning on AVX512_SUPPORT
will not provide any speed-up.
The goal of this tutorial is to build a data type that represents a ASCII string in FHE while implementing the to_lower
and to_upper
functions.
An ASCII character is stored in 7 bits. To store an encrypted ASCII we use the FheUint8
.
The uppercase letters are in the range [65, 90]
The lowercase letters are in the range [97, 122]
lower_case = upper_case + UP_LOW_DISTANCE
<=> upper_case = lower_case - UP_LOW_DISTANCE
Where UP_LOW_DISTANCE = 32
This type will hold the encrypted characters as a Vec<FheUint8>
to implement the functions that change the case.
To use the FheUint8
type, the integer
feature must be activated:
In the FheAsciiString::encrypt
function, some data validation is done:
The input string can only contain ascii characters.
It is not possible to branch on an encrypted value, however it is possible to evaluate a boolean condition and use it to get the desired result. Checking if the 'char' is an uppercase letter to modify it to a lowercase can be done without using a branch, like this:
We can remove the branch this way:
On an homomorphic integer, this gives
The whole code is:
TFHE-rs only requires a nightly toolchain for building the C API and using advanced SIMD instructions, otherwise you can use a stable toolchain (with version >= 1.72) Install the needed Rust toolchain:
Then, you can either:
Manually specify the toolchain to use in each of the cargo commands:
Or override the toolchain to use for the current project:
To check the toolchain that Cargo will use by default, you can use the following command:
TFHE-rs
exposes different cargo features
to customize the types and features used.
This crate exposes two kinds of data types. Each kind is enabled by activating its corresponding feature in the TOML line. Each kind may have multiple types:
TFHE-rs now includes a GPU backend, featuring a CUDA implementation for performing integer arithmetics on encrypted data. In what follows, a simple tutorial is introduced: it shows how to update your existing program to use GPU acceleration, or how to start a new one using GPU.
Cuda version >= 10
Compute Capability >= 3.0
To use the TFHE-rs GPU backend
in your project, you first need to add it as a dependency in your Cargo.toml
.
If you are using an x86
machine:
If you are using an ARM
machine:
When running code that uses TFHE-rs
, it is highly recommended to run in release mode with cargo's --release
flag to have the best possible performance
TFHE-rs GPU backend is supported on Linux (x86, aarch64).
Here is a full example (combining the client and server parts):
The configuration of the key is different from the CPU. More precisely, if both client and server keys are still generated by the Client (which is assumed to run on a CPU), the server key has then to be decompressed by the Server to be converted into the right format. To do so, the server should run this function: decompressed_to_gpu()
. From then on, there is no difference between the CPU and the GPU.
On the client-side, the method to encrypt the data is exactly the same than the CPU one, i.e.:
Finally, the client gets the decrypted results by computing:
TFHE-rs includes the possibility to leverage the high number of threads given by a GPU. To do so, the configuration should be updated with Rust let config = ConfigBuilder::with_custom_parameters(PARAM_MULTI_BIT_MESSAGE_2_CARRY_2_GROUP_3_KS_PBS, None).build();
The complete example becomes:
The GPU backend includes the following operations:
The tables below contain benchmarks for homomorphic operations running on a single V100 from AWS (p3.2xlarge machines), with the default parameters:
Parameter set | PARAM_MESSAGE_1_CARRY_1 | PARAM_MESSAGE_2_CARRY_2 | PARAM_MESSAGE_3_CARRY_3 | PARAM_MESSAGE_4_CARRY_4 |
---|---|---|---|---|
Parameter set | Concrete FFT + AVX-512 |
---|---|
Parameter set | spqlios-fma |
---|---|
Parameter set | GINX | GINX w/ Intel HEXL |
---|---|---|
Other configurations can be found .
Kind | Features | Type(s) |
---|
In general, the library automatically chooses the best instruction sets available by the host. However, in the case of 'AVX-512', this has to be explicitly chosen as a feature. This requires to use a along with the feature nightly-avx512
.
>= 8.0 - check this for more details about nvcc/gcc compatible versions
>= 3.24
Rust version - check this
OS | x86 | aarch64 |
---|
In comparison with the , the only difference lies into the key creation, which is detailed
The server must first set its keys up, like in the CPU, with: set_server_key(gpu_key);
. Then, homomorphic computations are done with the same code than the one described .
All operations follow the same syntax than the one described in .
Operation \ Size | FheUint8 | FheUint16 | FheUint32 | FheUint64 | FheUint128 | FheUint256 |
---|
Operation \ Size
FheUint8
FheUint16
FheUint32
FheUint64
FheUint128
FheUint256
Negation (-
)
55.4 ms
79.7 ms
105 ms
133 ms
163 ms
199 ms
Add / Sub (+
,-
)
58.9 ms
86.0 ms
106 ms
124 ms
151 ms
193 ms
Mul (x
)
122 ms
164 ms
227 ms
410 ms
1,04 s
3,41 s
Equal / Not Equal (eq
, ne
)
32.0 ms
32.0 ms
50.4 ms
50.9 ms
53.1 ms
54.6 ms
Comparisons (ge
, gt
, le
, lt
)
43.7 ms
65.2 ms
84.3 ms
107 ms
132 ms
159 ms
Max / Min (max
,min
)
68.4 ms
86.8 ms
106 ms
132 ms
160 ms
200 ms
Bitwise operations (&
, |
, ^
)
17.1 ms
17.3 ms
17.8 ms
18.8 ms
20.2 ms
22.2 ms
Div / Rem (/
, %
)
631 ms
1.59 s
3.77 s
8,64 s
20,3 s
53,4 s
Left / Right Shifts (<<
, >>
)
82.8 ms
99.2 ms
121 ms
149 ms
194 ms
401 ms
Left / Right Rotations (left_rotate
, right_rotate
)
82.1 ms
99.4 ms
120 ms
149 ms
194 ms
402 ms
Operation \ Size
FheUint8
FheUint16
FheUint32
FheUint64
FheUint128
FheUint256
Add / Sub (+
,-
)
68.3 ms
82.4 ms
102 ms
122 ms
151 ms
191 ms
Mul (x
)
93.7 ms
139 ms
178 ms
242 ms
516 ms
1.02 s
Equal / Not Equal (eq
, ne
)
30.2 ms
30.8 ms
32.7 ms
50.4 ms
51.2 ms
54.8 ms
Comparisons (ge
, gt
, le
, lt
)
47.3 ms
69.9 ms
96.3 ms
102 ms
138 ms
141 ms
Max / Min (max
,min
)
75.4 ms
99.7 ms
120 ms
126 ms
150 ms
186 ms
Bitwise operations (&
, |
, ^
)
17.1 ms
17.4 ms
18.2 ms
19.2 ms
19.7 ms
22.6 ms
Div (/
)
160 ms
212 ms
272 ms
402 ms
796 ms
2.27 s
Rem (%
)
315 ms
428 ms
556 ms
767 ms
1.27 s
2.86 s
Left / Right Shifts (<<
, >>
)
16.8 ms
16.8 ms
17.3 ms
18.0 ms
18.9 ms
22.6 ms
Left / Right Rotations (left_rotate
, right_rotate
)
16.8 ms
16.9 ms
17.3 ms
18.3 ms
19.0 ms
22.8 ms
unchecked_add
341 ns
555 ns
2.47 µs
9.77 µs
add
5.96 ms
12.6 ms
102 ms
508 ms
mul_lsb
5.99 ms
12.3 ms
101 ms
500 ms
keyswitch_programmable_bootstrap
6.40 ms
12.9 ms
104 ms
489 ms
DEFAULT_PARAMETERS_KS_PBS
8.49 ms
PARAMETERS_ERROR_PROB_2_POW_MINUS_165_KS_PBS
13.7 ms
TFHE_LIB_PARAMETERS
9.90 ms
default_128bit_gate_bootstrapping_parameters
13.5 ms
FHEW_BINGATE/STD128_OR
25.5 ms
21,6 ms
FHEW_BINGATE/STD128_LMKCDEY_OR
25.4 ms
19.9 ms
Booleans |
| Booleans |
ShortInts |
| Short integers |
Integers |
| Arbitrary-sized integers |
Linux |
|
|
macOS | Unsupported | Unsupported* |
Windows | Unsupported | Unsupported |
cuda_add | 103.33 ms | 129.26 ms | 156.83 ms | 186.99 ms | 320.96 ms | 528.15 ms |
cuda_bitand | 26.11 ms | 26.21 ms | 26.63 ms | 27.24 ms | 43.07 ms | 65.01 ms |
cuda_bitor | 26.1 ms | 26.21 ms | 26.57 ms | 27.23 ms | 43.05 ms | 65.0 ms |
cuda_bitxor | 26.08 ms | 26.21 ms | 26.57 ms | 27.25 ms | 43.06 ms | 65.07 ms |
cuda_eq | 52.82 ms | 53.0 ms | 79.4 ms | 79.58 ms | 96.37 ms | 145.25 ms |
cuda_ge | 104.7 ms | 130.23 ms | 156.19 ms | 183.2 ms | 213.43 ms | 288.76 ms |
cuda_gt | 104.93 ms | 130.2 ms | 156.33 ms | 183.38 ms | 213.47 ms | 288.8 ms |
cuda_le | 105.14 ms | 130.47 ms | 156.48 ms | 183.44 ms | 213.33 ms | 288.75 ms |
cuda_lt | 104.73 ms | 130.23 ms | 156.2 ms | 183.14 ms | 213.33 ms | 288.74 ms |
cuda_max | 156.7 ms | 182.65 ms | 210.74 ms | 251.78 ms | 316.9 ms | 442.71 ms |
cuda_min | 156.85 ms | 182.67 ms | 210.39 ms | 252.02 ms | 316.96 ms | 442.95 ms |
cuda_mul | 219.73 ms | 302.11 ms | 465.91 ms | 955.66 ms | 2.71 s | 9.15 s |
cuda_ne | 52.72 ms | 52.91 ms | 79.28 ms | 79.59 ms | 96.37 ms | 145.36 ms |
cuda_neg | 103.26 ms | 129.4 ms | 157.19 ms | 187.09 ms | 321.27 ms | 530.11 ms |
cuda_sub | 103.34 ms | 129.42 ms | 156.87 ms | 187.01 ms | 321.04 ms | 528.13 ms |
As explained in the Introduction, most types are meant to be shared with the server that performs the computations.
The easiest way to send these data to a server is to use the serialization
and deserialization
features. tfhe
uses the serde framework. Serde's Serialize
and Deserialize
functions are implemented on TFHE's types.
To serialize our data, a data format should be picked. Here, bincode is a good choice, mainly because it is a binary format.
For some types, safe serialization and deserialization functions are available. Bincode is used internally.
Safe-deserialization must take as input the output of a safe-serialization. On this condition, validation of the following is done:
type: trying to deserialize type A
from a serialized type B
raises an error along the lines of On deserialization, expected type A, got type B instead of a generic deserialization error (or less likely a meaningless result of type A
)
version: trying to deserialize type A
(version 0.2) from a serialized type A
(incompatible version 0.1) raises an error along the lines of On deserialization, expected serialization version 0.2, got version 0.1 instead of a generic deserialization error (or less likely a meaningless result of type A
(version 0.2))
parameter compatibility: trying to deserialize into an object of type A
with some crypto parameters from a an object of type A
with other crypto parameters raises an error along the lines of Deserialized object of type A not conformant with given parameter set. If both parameters sets 1 and 2 have the same lwe dimension for ciphertexts, a ciphertext from param 1 may not fail this deserialization check with param 2 even if doing this deserialization may not make sense. Also, this check can't distinguish ciphertexts/server keys from independant client keys with the same parameters (which makes no sense combining to do homomorphic operations). This check is meant to prevent runtime errors in server homomorphic operations by checking that server keys and ciphertexts are compatible with the same parameter set.
Moreover, a size limit (in number of bytes) for the serialized data is expected on both serialization and deserialization. On serialization, an error is raised if the serialized output would be bigger than the given limit. On deserialization, an error is raised if the serialized input is bigger than the given limit. It is meant to gracefully return an error in case of an attacker trying to cause an out of memory error on deserialization.
A standalone is_conformant
method is also available on those types to do a parameter compatibility check.
Parameter compatibility check is done by safe_deserialize_conformant
function but a safe_deserialize
function without this check is also available.
In what follows, the process to manage data when upgrading the TFHE-rs version (starting from the 0.5.5 release) is given. This page details the methods to make data, which have initially been generated with an older version of TFHE-rs, usable with a newer version.
The current strategy that has been adopted for TFHE-rs is the following:
TFHE-rs has a global SERIALIZATION_VERSION
constant;
When breaking serialization changes are introduced, this global version is bumped;
Safe serialization primitives check this constant upon deserialization, if the data is incompatible, these primitives return an error.
To be able to use older serialized data with newer versions, the following is done on new major TFHE-rs releases:
A minor update is done to the previously released branch to add the new release as an optional dependency;
Conversion code is added to the previous branch to be able to load old data and convert it to the new data format.
In practice, if we take the 0.6 release as a concrete example, here is what will happen:
0.6.0 is released with breaking changes to the serialization;
0.5.5 has tfhe@0.6.0 as optional dependency gated by the forward_compatibility
feature;
Conversion code is added to 0.5.5, if possible without any user input, but some data migration will likely require some information to be provided by the developer writing the migration code;
0.5.5 is released.
Note that if you do not need forward compatibility 0.5.5 will be equivalent to 0.5.3 from a usability perspective and you can safely update. Note also that the 0.6.0 has no knowledge of previous releases.
A set of generic tooling is given to allow migrating data by using several workflows. The data migration is considered to be an application/protocol layer concern to avoid imposing design choices.
Examples to migrate data:
An Application
uses TFHE-rs 0.5.3 and needs/wants to upgrade to 0.6.0 to benefit from various improvements.
Example timeline of the data migration or Bulk Data Migration
:
A new transition version of the Application
is compiled with the 0.5.5 release of TFHE-rs;
The transition version of the Application
adds code to read previously stored data, convert it to the proper format for 0.6.0 and save it back to disk;
The service enters a maintenance period (if relevant);
Migration of data from 0.5.5 to 0.6.0 is done with the transition version of the Application
, note that depending on the volume of data this transition can take a significant amount of time;
The updated version of the Application
is compiled with the 0.6.0 release of TFHE-rs and put in production;
Service is resumed with the updated Application
(if relevant).
The above case is describing a simple use case, where only a single version of data has to be managed. Moreover, the above strategy is not relevant in the case where the data is so large that migrating it in one go is not doable, or if the service cannot suffer any interruption.
In order to manage more complicated cases, another method called Migrate On Read
can be used.
Here is an example timeline where data is migrated only as needed with the Migrate On Read
approach:
A new version of the Application
is compiled, it has tfhe@0.5.5 as dependency (the dependency will have to be renamed to avoid conflicts, a possible name is to use the major version like tfhe_0_5
) and tfhe@0.6.0 which will not be renamed and can be accessed as tfhe
Code to manage reading the data is added to the Application
:
The code determines whether the data was saved with the 0.5 Application
or the 0.6 Application
, if the data is already up to date with the 0.6 format it can be loaded right away, if it's in the 0.5 format the Application
can check if an updated version of the data is already available in the 0.6 format and loads that if it's available, otherwise it converts the data to 0.6, saves the converted data to avoid having to convert it every time it is accessed and continue processing with the 0.6 data
The above is more complicated to manage as data will be present on disk with several versions, however it allows to run the service continuously or near-continuously once the new Application
is deployed (it will require careful routing or error handling as nodes with outdated Application
won't be able to process the 0.6 data).
Also, if required, several version of TFHE-rs can be "chained" to upgrade very old data to newer formats. The above pattern can be extended to have tfhe_0_5
(tfhe@0.5.5 renamed), tfhe_0_6
(tfhe@0.6.0 renamed) and tfhe
being tfhe@0.7.0, this will require special handling from the developers so that their protocol can handle data from 0.5.5, 0.6.0 and 0.7.0 using all the conversion tooling from the relevant version.
E.g., if some computation requires data from version 0.5.5 a conversion function could be called upgrade_data_from_0_5_to_0_7
and do:
read data from 0.5.5
convert to 0.6.0 format using tfhe_0_6
convert to 0.7.0 format using tfhe_0_7
save to disk in 0.7.0 format
process 0.7.0 data with tfhe
which is tfhe@0.7.0
name | symbol |
|
|
Neg |
| N/A |
Add |
|
Sub |
|
Mul |
|
Div |
|
Rem |
|
Not |
| N/A |
BitAnd |
|
BitOr |
|
BitXor |
|
Shr |
|
Shl |
|
Rotate right |
|
Rotate left |
|
Min |
|
Max |
|
Greater than |
|
Greater or equal than |
|
Lower than |
|
Lower or equal than |
|
Equal |
|
Cast (into dest type) |
| N/A |
Cast (from src type) |
| N/A |
Ternary operator |
|
TFHE-rs includes a list of specific operations to detect overflows. The overall idea is to have a specific ciphertext encrypting a flag reflecting the status of the computations. When an overflow occurs, this flag is set to true. Since the server is not able to evaluate this value (since it is encrypted), the client has to check the flag value when decrypting to determine if an overflow has happened. These operations might be slower than their equivalent which do not detect overflow, so they are not enabled by default (see the table below). In order to use them, specific operators must be called. At the moment, only additions, subtractions, multiplications are supported. Missing operations will be added soon.
The list of operations along with their symbol is:
These operations are then used exactly in the same way than the usual ones. The only difference lies into the decryption, as shown in following example:
The current benchmarks are given in the following tables (the first one for unsigned homomorphic integers and the second one for the signed integers):
TFHE-rs includes features to reduce the size of both keys and ciphertexts, by compressing them. Most TFHE-rs entities contain random numbers generated by a Pseudo Random Number Generator (PRNG). A PRNG is deterministic, therefore storing only the random seed used to generate those numbers is enough to keep all the required information: using the same PRNG and the same seed, the full chain of random values can be reconstructed when decompressing the entity.
In the library, entities that can be compressed are prefixed by Compressed
. For instance, the type of a compressed FheUint256
is CompressedFheUint256
.
In the following example code, we use the bincode
crate dependency to serialize in a binary format and compare serialized sizes.
This example shows how to compress a ciphertext encypting messages over 16 bits.
This example shows how to compress the server keys.
This example shows how to compress the classical public keys.
It is not currently recommended to use the CompressedPublicKey to encrypt ciphertexts without first decompressing it. In case the resulting PublicKey is too large to fit in memory the encryption with the CompressedPublicKey will be very slow, this is a known problem and will be addressed in future releases.
This example shows how to use compressed compact public keys.
name | symbol | type |
---|---|---|
Operation\Size | FheUint8 | FheUint16 | FheUint32 | FheUint64 | FheUint128 | FheUint256 |
---|---|---|---|---|---|---|
Operation\Size | FheInt8 | FheInt16 | FheInt32 | FheInt64 | FheInt128 | FheInt256 |
---|---|---|---|---|---|---|
Public key encryption refers to the cryptographic paradigm where the encryption key can be publicly distributed, whereas the decryption key remains secret to the owner. This differs from usual case where the same secret key is used to encrypt and decrypt the data. In TFHE-rs, there exists two methods for public key encryptions. First, the usual one, where the public key contains ma y encryption of zeroes. More details can be found in . The second method is based on the paper entitled . The main advantage of the latter method in comparison with the former lies into the key sizes, which are drastically reduced.
Note that public keys can be
overflow_add
Binary
overflow_sub
Binary
overflow_mul
Binary
unsigned_overflowing_add
63.67 ms
84.11 ms
107.95 ms
120.8 ms
147.38 ms
191.28 ms
unsigned_overflowing_sub
68.89 ms
81.83 ms
107.63 ms
120.38 ms
150.21 ms
190.39 ms
unsigned_overflowing_mul
140.76 ms
191.85 ms
272.65 ms
510.61 ms
1.34 s
4.51 s
signed_overflowing_add
76.54 ms
84.78 ms
104.23 ms
134.38 ms
162.99 ms
202.56 ms
signed_overflowing_sub
82.46 ms
86.92 ms
104.41 ms
132.21 ms
168.06 ms
201.17 ms
signed_overflowing_mul
277.91 ms
365.67 ms
571.22 ms
1.21 s
3.57 s
12.84 s
Sometimes, the server side needs to initialize a value. For example, when computing the sum of a list of ciphertext, one might want to initialize the sum
variable to 0
.
Instead of asking the client to send a real encryption of zero, the server can do a trivial encryption
A trivial encryption will create a ciphertext that contains the desired value, however, the 'encryption' is trivial that is, it is not really encrypted: anyone, any key can decrypt it.
Note that when you want to do an operation that involves a ciphertext and a clear value, you should only use a trivial encryption of the clear value if the ciphertext/clear-value operation (often called scalar operation) you want to run is not supported.
If you wish to write generic functions which use operators with mixed reference and non-reference, it might get tricky at first to specify the trait bounds. This page should serve as a cookbook to help you.
Operators (+, *, >>, etc) are tied to traits in std:::ops
, e.g. +
is std::ops::Add
, so to write a generic function which uses the +
operator, you need to use add std::ops::Add
as a trait bound.
Then, depending on if the left hand side / right hand side is an owned value or a reference, the trait bound is slightly different. The table below shows the possibilities.
The for<'a>
syntax is something called Higher-Rank Trait Bounds, often shortened as HRTB
Writing generic functions will also allow you to call them using clear inputs, only allowing easier debugging.
In what follows, an example on how to use the parallelized bootstrapping by choosing multi bit PBS parameters:
By construction, the parallelized PBS might not be deterministic: the resulting ciphertext will always decrypt to the same plaintext, but the order of the operations could differ so the output ciphertext might differ. In order to activate the deterministic version, the suffix 'with_deterministic_execution()' should be added to the parameters, as shown in the following example:
This library exposes a C binding to the high-level TFHE-rs primitives to implement Fully Homomorphic Encryption (FHE) programs.
TFHE-rs C API can be built on a Unix x86_64 machine using the following command:
or on a Unix aarch64 machine using the following command:
The tfhe.h
header as well as the static (.a) and dynamic (.so) libtfhe
binaries can then be found in "${REPO_ROOT}/target/release/".
The tfhe-c-api-dynamic-buffer.h
header and the static (.a) and dynamic (.so) libraries will be found in "${REPO_ROOT}/target/release/deps/".
The build system needs to be set up so that the C or C++ program links against TFHE-rs C API binaries and the dynamic buffer library.
Here is a minimal CMakeLists.txt to do just that:
TFHE-rs C API
.WARNING: The following example does not have proper memory management in the error case to make it easier to fit the code on this page.
To run the example below, the above CMakeLists.txt and main.c files need to be in the same directory. The commands to run are:
operation | trait bound |
---|---|
The (PBS) is a sequential operation by nature. However, some showed that parallelism could be added at the cost of having larger keys. Overall, the performance of the PBS are improved. This new PBS is called a multi bit PBS. In TFHE-rs, since integer homomorphic operations are already parallelized, activating this feature may improve performance in the case of high core count CPUs if enough cores are available, or for small input message precision.
T $op T
T: $Op<T, Output=T>
T $op &T
T: for<'a> $Op<&'a T, Output=T>
&T $op T
for<'a> &'a T: $Op<T, Output=T>
&T $op &T
for<'a> &'a T: $Op<&'a T, Output=T>
TFHE-rs supports WASM for the client api, that is, it supports key generation, encryption, decryption but not doing actual computations.
TFHE-rs supports 3 WASM 'targets':
nodejs: to be used in a nodejs app/package
web: to be used in a web browser
web-parallel: to be used in a web browser with multi-threading support
In all cases, the core of the API is same, only few initialization function changes.
When using the Web WASM target, there is an additional init
function to call.
When using the Web WASM target with parallelism enabled, there is also one more initialization function to call initThreadPool
The TFHE-rs repo has a Makefile that contains targets for each of the 3 possible variants of the API:
make build_node_js_api
to build the nodejs API
make build_web_js_api
to build the browser API
make build_web_js_api_parallel
to build the browser API with parallelism
The compiled WASM package will be in tfhe/pkg.
The sequential browser API and the nodejs API are published as npm packages. You can add the browser API to your project using the command npm i tfhe
. You can add the nodejs API to your project using the command npm i node-tfhe
.
TFHE-rs uses WASM to expose a JS binding to the client-side primitives, like key generation and encryption, of the Boolean and shortint modules.
There are several limitations at this time. Due to a lack of threading support in WASM, key generation can be too slow to be practical for bigger parameter sets.
Some parameter sets lead to FHE keys that are too big to fit in the 2GB memory space of WASM. This means that some parameter sets are virtually unusable.
To build the JS on WASM bindings for TFHE-rs, you need to install wasm-pack
in addition to a compatible (>= 1.67) rust toolchain
.
In a shell, then run the following to clone the TFHE-rs repo (one may want to checkout a specific tag, here the default branch is used for the build):
The command above targets nodejs. A binding for a web browser can be generated as well using --target=web
. This use case will not be discussed in this tutorial.
Both Boolean and shortint features are enabled here, but it's possible to use one without the other.
After the build, a new directory pkg is present in the tfhe
directory.
Be sure to update the path of the required clause in the example below for the TFHE package that was just built.
The example.js
script can then be run using node
, like so:
rayon is a popular crate to easily write multi-threaded code in Rust.
It is possible to use rayon to write multi-threaded TFHE-rs code. However due to internal details of rayon
and TFHE-rs
, there is some special setup that needs to be done.
The high level api requires to call set_server_key
on each thread where computations needs to be done. So a first attempt at using rayon with TFHE-rs
might look like this:
However, due to rayon's work stealing mechanism and TFHE-rs's internals, this may create `BorrowMutError'.
The correct way is to call rayon::broadcast
If your application needs to operate on data from different clients concurrently, and that you want each client to use multiple threads, you will need to create different rayon thread pools
This can be useful if you have some rust #[test]
This can greatly improve the pace at which one develops FHE applications.
Keep in mind that trivial ciphertexts are not secure at all, thus an application released/deployed in production must never receive trivial ciphertext from a client.
To use this feature, simply call your circuits/functions with trivially encrypted values (made using encrypt_trivial
) instead of real encryptions (made using encrypt
)
This example is going to print.
If any input to mul_all
is not a trivial ciphertexts, the computations would be done 100% in FHE, and the program would output:
Using trivial encryptions as input, the example runs in 980 ms on a standard 12 cores laptop, using real encryptions it would run in 7.5 seconds on a 128-core machine.
This library makes it possible to execute homomorphic operations over encrypted data, where the data are either Booleans, short integers (named shortint in the rest of this documentation), or integers up to 256 bits. It allows you to execute a circuit on an untrusted server because both circuit inputs and outputs are kept private. Data are indeed encrypted on the client side, before being sent to the server. On the server side, every computation is performed on ciphertexts.
The server, however, has to know the circuit to be evaluated. At the end of the computation, the server returns the encryption of the result to the user. Then the user can decrypt it with the secret key
.
The overall process to write an homomorphic program is the same for all types. The basic steps for using the TFHE-rs library are the following:
Choose a data type (Boolean, shortint, integer)
Import the library
Create client and server keys
Encrypt data with the client key
Compute over encrypted data using the server key
Decrypt data with the client key
This library has different modules, with different levels of abstraction.
There is the core_crypto module, which is the lowest level API with the primitive functions and types of the TFHE scheme.
Above the core_crypto module, there are the Boolean, shortint, and integer modules, which contain easy to use APIs enabling evaluation of Boolean, short integer, and integer circuits.
Finally, there is the high-level module built on top of the Boolean, shortint, integer modules. This module is meant to abstract cryptographic complexities: no cryptographical knowledge is required to start developing an FHE application. Another benefit of the high-level module is the drastically simplified development process compared to lower level modules.
TFHE-rs exposes a high-level API by default that includes datatypes that try to match Rust's native types by having overloaded operators (+, -, ...).
Here is an example of how the high-level API is used:
Use the --release
flag to run this example (eg: cargo run --release
)
Here is an example of how the library can be used to evaluate a Boolean circuit:
Use the --release
flag to run this example (eg: cargo run --release
)
Here is a full example using shortint:
Use the --release
flag to run this example (eg: cargo run --release
)
Use the --release
flag to run this example (eg: cargo run --release
)
Since tfhe-rs 0.5, have another application. They can be used to allow debugging via a debugger or print statements as well as speeding-up execution time so that you won't have to spend minutes waiting for execution to progress.
The library is simple to use and can evaluate homomorphic circuits of arbitrary length. The description of the algorithms can be found in the paper (also available as ).
This contains the operations available in tfhe::boolean, along with code examples.
Let ct_1, ct_2, ct_3
be three Boolean ciphertexts. Then, the MUX gate (abbreviation of MUltipleXer) is equivalent to the operation:
This example shows how to use the MUX ternary gate:
In tfhe::boolean
, the available operations are mainly related to their equivalent Boolean gates (i.e., AND, OR... etc). What follows are examples of a unary gate (NOT) and a binary gate (XOR). The last one is about the ternary MUX gate, which allows homomorphic computation of conditional statements of the form If..Then..Else
.
This library is meant to be used both on the server side and the client side. The typical use case should follow the subsequent steps:
On the client side, generate the client
and server keys
.
Send the server key
to the server.
Then any number of times:
On the client side, encrypt the input data with the client key
.
Transmit the encrypted input to the server.
On the server side, perform homomorphic computation with the server key
.
Transmit the encrypted output to the client.
On the client side, decrypt the output data with the client key
.
In the first step, the client creates two keys, the client key
and the server key
, with the concrete_boolean::gen_keys
function:
The client_key
is of type ClientKey
. It is secret and must never be transmitted. This key will only be used to encrypt and decrypt data.
The server_key
is of type ServerKey
. It is a public key and can be shared with any party. This key has to be sent to the server because it is required for homomorphic computation.
Note that both the client_key
and server_key
implement the Serialize
and Deserialize
traits. This way you can use any compatible serializer to store/send the data. To store the server_key
in a binary file, you can use the bincode
library:
Once the server key is available on the server side, it is possible to perform some homomorphic computations. The client needs to encrypt some data and send it to the server. Again, the Ciphertext
type implements the Serialize
and the Deserialize
traits, so that any serializer and communication tool suiting your use case can be employed:
Anyone (the server or a third party) with the public key can also encrypt some (or all) of the inputs. The public key can only be used to encrypt, not to decrypt.
Once the encrypted inputs are on the server side, the server_key
can be used to homomorphically execute the desired Boolean circuit:
Once the encrypted output is on the client side, the client_key
can be used to decrypt it:
The TFHE cryptographic scheme relies on a variant of Regev cryptosystem and is based on a problem so difficult that it is even post-quantum resistant.
Some cryptographic parameters will require tuning to ensure both the correctness of the result and the security of the computation.
To make it simpler, we've provided two sets of parameters, which ensure correct computations for a certain probability with the standard security of 128 bits. There exists an error probability due to the probabilistic nature of the encryption, which requires adding randomness (noise) following a Gaussian distribution. If this noise is too large, the decryption will not give a correct result. There is a trade-off between efficiency and correctness: generally, using a less efficient parameter set (in terms of computation time) leads to a smaller risk of having an error during homomorphic evaluation.
In the two proposed sets of parameters, the only difference lies in this error probability. The default parameter set ensures an error probability of at most when computing a programmable bootstrapping (i.e., any gates but the not
). The other one is closer to the error probability claimed in the original TFHE paper, namely , but it is up-to-date regarding security requirements.
The following array summarizes this:
You can also create your own set of parameters. This is an unsafe
operation as failing to properly fix the parameters will result in an incorrect and/or insecure computation:
tfhe::shortint
is dedicated to unsigned integers smaller than 8 bits. The steps to homomorphically evaluate a circuit are described below.
tfhe::shortint
provides 3 key types:
ClientKey
ServerKey
PublicKey
The ClientKey
is the key that encrypts and decrypts messages (integer values up to 8 bits here). It is meant to be kept private and should never be shared. This key is created from parameter values that will dictate both the security and efficiency of computations. The parameters also set the maximum number of bits of message encrypted in a ciphertext.
The ServerKey
is the key that is used to evaluate the FHE computations. Most importantly, it contains a bootstrapping key and a keyswitching key. This key is created from a ClientKey
that needs to be shared to the server (it is not meant to be kept private). A user with a ServerKey
can compute on the encrypted data sent by the owner of the associated ClientKey
.
Computation/operation methods are tied to the ServerKey
type.
The PublicKey
is the key used to encrypt messages. It can be publicly shared to allow users to encrypt data such that only the ClientKey
holder will be able to decrypt. Encrypting with the PublicKey
does not alter the homomorphic capabilities associated to the ServerKey
.
Once the keys have been generated, the client key is used to encrypt data:
Once the keys have been generated, the client key is used to encrypt data:
Using the server_key
, addition is possible over encrypted values. The resulting plaintext is recovered after the decryption via the secret client key.
Since the ServerKey
and ClientKey
types both implement the Serialize
and Deserialize
traits, you are free to use any serializer that suits you to save and load the keys to disk.
Here is an example using the bincode
serialization library, which serializes to a binary format:
Parameter set | Error probability |
---|---|