mirror of
https://github.com/zama-ai/concrete.git
synced 2026-01-09 12:57:55 -05:00
docs(frontend): TFHE-rs compatibility guide v1
This commit is contained in:
committed by
Quentin Bourgerie
parent
a7cf7791d4
commit
1e08d11e17
13
docs/_static/tfhers/tfhers_uc1.excalidraw.svg
vendored
Normal file
13
docs/_static/tfhers/tfhers_uc1.excalidraw.svg
vendored
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 137 KiB |
13
docs/_static/tfhers/tfhers_uc2.excalidraw.svg
vendored
Normal file
13
docs/_static/tfhers/tfhers_uc2.excalidraw.svg
vendored
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 122 KiB |
63
docs/guides/tfhers/README.md
Normal file
63
docs/guides/tfhers/README.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# TFHE-rs Compatibility
|
||||
|
||||
This guide explains how to combine Concrete and [TFHE-rs](https://github.com/zama-ai/tfhe-rs) computations together. This allows you to convert ciphertexts from Concrete to TFHE-rs, and vice versa, and to run a computation with both libraries without requiring a decryption.
|
||||
|
||||
## Overview
|
||||
|
||||
There are differences between Concrete and TFHE-rs, so ensuring compatibility between them involves more than just data serialization. To achieve compatibility, we need to consider two main aspects.
|
||||
|
||||
#### Encoding differences
|
||||
|
||||
Both TFHE-rs and Concrete libraries use Learning with errors(LWE) ciphertexts, but integers are encoded differently:
|
||||
|
||||
- In Concrete, integers are simply encoded in a single ciphertext
|
||||
- In TFHE-rs, integers are encoded into multiple ciphertext using radix decomposition
|
||||
|
||||
Converting between Concrete and TFHE-rs encrypted integers then require doing an encrypted conversion between the two different encodings.
|
||||
|
||||
When working with a TFHE-rs integer type in Concrete, you can use the `.encode(...)` and `.decode(...)` functions to see this in practice:
|
||||
|
||||
```python
|
||||
from concrete.fhe import tfhers
|
||||
|
||||
# don't worry about the API, we will have better examples later.
|
||||
# we just want to show the encoding here
|
||||
tfhers_params = tfhers.CryptoParams(
|
||||
lwe_dimension=909,
|
||||
glwe_dimension=1,
|
||||
polynomial_size=4096,
|
||||
pbs_base_log=15,
|
||||
pbs_level=2,
|
||||
lwe_noise_distribution=9.743962418842052e-07,
|
||||
glwe_noise_distribution=2.168404344971009e-19,
|
||||
encryption_key_choice=tfhers.EncryptionKeyChoice.BIG,
|
||||
)
|
||||
|
||||
# TFHERSInteger using this type will be represented as a vector of 8/2=4 integers
|
||||
tfhers_type = tfhers.TFHERSIntegerType(
|
||||
is_signed=False,
|
||||
bit_width=8,
|
||||
carry_width=3,
|
||||
msg_width=2,
|
||||
params=tfhers_params,
|
||||
)
|
||||
|
||||
assert (tfhers_type.encode(123) == [3, 2, 3, 1]).all()
|
||||
|
||||
assert tfhers_type.decode([3, 2, 3, 1]) == 123
|
||||
```
|
||||
|
||||
#### Parameter match
|
||||
|
||||
The Concrete Optimizer may find parameters which are not in TFHE-rs's pre-computed list. To ensure compatibility, you need to either fix or constrain the search space in parts of the circuit where compatibility is required. This ensures that compatible parameters are used consistently.
|
||||
|
||||
## Scenarios
|
||||
|
||||
There are 2 different approaches to using Concrete and THFE-rs depending on the situation.
|
||||
|
||||
- [Scenario 1: Shared secret key](./shared-key.md): In this scenario, a single party aims to combine both Concrete and TFHE-rs in a computation. In this case, a shared secret key will be used, while different keysets will be held for Concrete and TFHE-rs.
|
||||
- [Scenario 2: Pregenerated TFHE-rs keys](./pregenerated-key.md): This scenario involves two parties, each with a pre-established set of TFHE-rs keysets. The objective is to compute on encrypted TFHE-rs data using Concrete. In this case, there is no shared secret key. The party using Concrete will rely solely on TFHE-rs public keys and must optimize the parameters accordingly, while the party using TFHE-rs handles encryption, decryption, and computation.
|
||||
|
||||
## Serialization of ciphertexts and keys
|
||||
|
||||
Concrete already has its serilization functions (such as `tfhers_bridge.export_value`, `tfhers_bridge.import_value`, `tfhers_bridge.keygen_with_initial_keys`, `tfhers_bridge.serialize_input_secret_key`, and so on). However, when implementing a TFHE-rs computation in Rust, we must use a compatible serialization. Learn more in [Serialization of ciphertexts and keys](./serialization.md).
|
||||
26
docs/guides/tfhers/pregenerated-key.md
Normal file
26
docs/guides/tfhers/pregenerated-key.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Scenario 2: Pregenerated TFHE-rs keys
|
||||
|
||||
This document provides an overview of the second use case for combining TFHE-rs and Concrete, where two parties work with pregenerated TFHE-rs keysets. The focus is on how to perform computations on encrypted TFHE-rs values using Concrete, without the need for a shared secret key.
|
||||
|
||||
This scenario is interesting in the case of threshold cryptography, when the secret key is not known by a single party but split into different parties, and when the secret key cannot be changed (e.g. because it's a permanent key of an encrypted blockchain).
|
||||
|
||||
This use case is not managed currently by Concrete and will be available later.
|
||||
|
||||
In this scenario, two parties have a predetermined set of TFHE-rs keysets. The goal is to compute on encrypted TFHE-rs values in Concrete, but without sharing any secret key. Instead, the party using Concrete will rely only on TFHE-rs public keys and will optimize its operations according to the parameters associated with those keys. Meanwhile, the party using TFHE-rs will have the capacity to perform encryption, decryption, and computation.
|
||||
|
||||
## Workflow
|
||||
|
||||
The workflow for this scenario is as follows:
|
||||
|
||||
1. We generate multiple keysets (including secret keys and public keys) with different sets of parameters.
|
||||
2. Considering the public keys, Concrete's Optimizer will limit its parameter search space to find the best configuration that is compatible with the existing TFHE-rs keys.
|
||||
|
||||

|
||||
|
||||
## Use case
|
||||
|
||||
This approach is particularly useful in scenarios involving threshold cryptography, where the secret key is distributed across multiple parties. This setup is beneficial when the secret key cannot be modified, such as in situations where the key is a permanent fixture, like in encrypted blockchains.
|
||||
|
||||
## Future Support
|
||||
|
||||
Currently, this use case is not supported by Concrete but will be available later.
|
||||
61
docs/guides/tfhers/serialization.md
Normal file
61
docs/guides/tfhers/serialization.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Serialization of Ciphertexts and Keys
|
||||
|
||||
This document explains how to serialize and deserialize ciphertexts and secret keys when working with TFHE-rs in Rust.
|
||||
|
||||
Concrete already has its serilization functions (e.g. `tfhers_bridge.export_value`, `tfhers_bridge.import_value`, `tfhers_bridge.keygen_with_initial_keys`, `tfhers_bridge.serialize_input_secret_key`). However, when implementing a TFHE-rs computation in Rust, we must use a compatible serialization.
|
||||
|
||||
## Ciphertexts
|
||||
|
||||
We can deserialize `FheUint8` (and similarly other types) using `bincode`
|
||||
|
||||
```rust
|
||||
use tfhe::FheUint8;
|
||||
|
||||
/// ...
|
||||
|
||||
fn load_fheuint8(path: &String) -> FheUint8 {
|
||||
let path_fheuint: &Path = Path::new(path);
|
||||
let serialized_fheuint = fs::read(path_fheuint).unwrap();
|
||||
let mut serialized_data = Cursor::new(serialized_fheuint);
|
||||
bincode::deserialize_from(&mut serialized_data).unwrap()
|
||||
}
|
||||
```
|
||||
|
||||
To serialize
|
||||
|
||||
```rust
|
||||
fn save_fheuint8(fheuint: FheUint8, path: &String) {
|
||||
let mut serialized_ct = Vec::new();
|
||||
bincode::serialize_into(&mut serialized_ct, &fheuint).unwrap();
|
||||
let path_ct: &Path = Path::new(path);
|
||||
fs::write(path_ct, serialized_ct).unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
## Secret Key
|
||||
|
||||
We can deserialize `LweSecretKey` using `bincode`
|
||||
|
||||
```rust
|
||||
use tfhe::core_crypto::prelude::LweSecretKey;
|
||||
|
||||
/// ...
|
||||
|
||||
fn load_lwe_sk(path: &String) -> LweSecretKey<Vec<u64>> {
|
||||
let path_sk: &Path = Path::new(path);
|
||||
let serialized_lwe_key = fs::read(path_sk).unwrap();
|
||||
let mut serialized_data = Cursor::new(serialized_lwe_key);
|
||||
bincode::deserialize_from(&mut serialized_data).unwrap()
|
||||
}
|
||||
```
|
||||
|
||||
To serialize
|
||||
|
||||
```rust
|
||||
fn save_lwe_sk(lwe_sk: LweSecretKey<Vec<u64>>, path: &String) {
|
||||
let mut serialized_lwe_key = Vec::new();
|
||||
bincode::serialize_into(&mut serialized_lwe_key, &lwe_sk).unwrap();
|
||||
let path_sk: &Path = Path::new(path);
|
||||
fs::write(path_sk, serialized_lwe_key).unwrap();
|
||||
}
|
||||
```
|
||||
250
docs/guides/tfhers/shared-key.md
Normal file
250
docs/guides/tfhers/shared-key.md
Normal file
@@ -0,0 +1,250 @@
|
||||
# Scenario 1: Shared secret key
|
||||
|
||||
This document explains how to set up a shared secret key between Concrete and TFHE-rs to perform computations.
|
||||
|
||||
In this scenario, a shared secret key will be used, while different keysets will be held for Concrete and TFHE-rs. There are two ways to generate keys, outlined with the following steps
|
||||
|
||||
#### From Concrete (1.1)
|
||||
|
||||
1. Perform a classical key generation in Concrete, which generates a set of secret and public keys.
|
||||
2. Use this secret key to perform a partial key generation in TFHE-rs, starting from the shared secret key and generating the rest of the necessary keys.
|
||||
|
||||
#### From TFHE-rs (1.2)
|
||||
|
||||
1. Perform a classical key generation in TFHE-rs, generating a single secret key and corresponding public keys.
|
||||
2. Use the secret key from TFHE-rs to perform a partial keygen in Concrete.
|
||||
|
||||
{% hint style="info" %} While TFHE-rs does use a single secret key, Concrete may generate more than one, but only one of these should be corresponding to the TFHE-rs key. The API does hide this detail, but will often ask you to provide the position of a given input/output. This will be used to infer which secret key should be used. {% endhint %}
|
||||
|
||||
After the key generation is complete and we have both keysets, we can perform computations, encryption, and decryption on both ends.
|
||||
|
||||

|
||||
|
||||
## Setup and configuration
|
||||
|
||||
The first step is to define the TFHE-rs ciphertext type that will be used in the computation (see [Overview](./README.md)). This includes specifying both cryptographic and encoding parameters. TFHE-rs provides a pre-computed list of recommended parameters, which we will use to avoid manual selection. You can find the parameters used in this guide [here](https://github.com/zama-ai/tfhe-rs/blob/2bd9f7aab412c92d0642a35fa87d1d0f4e2193b9/tfhe/src/shortint/parameters/classic/gaussian/p_fail_2_minus_64/ks_pbs.rs#L255).
|
||||
|
||||
In short, we first determine a suitable set of parameters from TFHE-rs and then apply them in Concrete. This ensures that the ciphertexts generated in both systems will be compatible by using the same cryptographic parameters.
|
||||
|
||||
```python
|
||||
from functools import partial
|
||||
from concrete.fhe import tfhers
|
||||
|
||||
tfhers_params = tfhers.CryptoParams(
|
||||
lwe_dimension=909,
|
||||
glwe_dimension=1,
|
||||
polynomial_size=4096,
|
||||
pbs_base_log=15,
|
||||
pbs_level=2,
|
||||
lwe_noise_distribution=9.743962418842052e-07,
|
||||
glwe_noise_distribution=2.168404344971009e-19,
|
||||
encryption_key_choice=tfhers.EncryptionKeyChoice.BIG,
|
||||
)
|
||||
# creating a TFHE-rs ciphertext type with crypto and encoding params
|
||||
tfhers_type = tfhers.TFHERSIntegerType(
|
||||
is_signed=False,
|
||||
bit_width=8,
|
||||
carry_width=3,
|
||||
msg_width=2,
|
||||
params=tfhers_params,
|
||||
)
|
||||
# this partial will help us create TFHERSInteger with the given type instead of calling
|
||||
# tfhers.TFHERSInteger(tfhers_type, value) every time
|
||||
tfhers_int = partial(tfhers.TFHERSInteger, tfhers_type)
|
||||
```
|
||||
|
||||
## Defining the circuit and compiling
|
||||
|
||||
We will now define a simple modular addition function. This function takes TFHE-rs inputs, converts them to Concrete format (`to_native`), runs a computation, and then converts them back to TFHE-rs. The circuit below is a common example that takes and produces TFHE-rs ciphertexts. However, there are other scenarios where you might not convert back to TFHE-rs, or you might convert to a different type than the input. Another possibility is to take one native ciphertext and one TFHE-rs ciphertext.
|
||||
|
||||
```python
|
||||
def compute(tfhers_x, tfhers_y):
|
||||
####### TFHE-rs to Concrete #########
|
||||
|
||||
# x and y are supposed to be TFHE-rs values.
|
||||
# to_native will use type information from x and y to do
|
||||
# a correct conversion from TFHE-rs to Concrete
|
||||
concrete_x = tfhers.to_native(tfhers_x)
|
||||
concrete_y = tfhers.to_native(tfhers_y)
|
||||
####### TFHE-rs to Concrete #########
|
||||
|
||||
####### Concrete Computation ########
|
||||
concrete_res = (concrete_x + concrete_y) % 213
|
||||
####### Concrete Computation ########
|
||||
|
||||
####### Concrete to TFHE-rs #########
|
||||
tfhers_res = tfhers.from_native(
|
||||
concrete_res, tfhers_type
|
||||
) # we have to specify the type we want to convert to
|
||||
####### Concrete to TFHE-rs #########
|
||||
return tfhers_res
|
||||
```
|
||||
|
||||
We can compile the circuit as usual.
|
||||
|
||||
```python
|
||||
compiler = fhe.Compiler(compute, {"tfhers_x": "encrypted", "tfhers_y": "encrypted"})
|
||||
inputset = [(tfhers_int(120), tfhers_int(120))]
|
||||
circuit = compiler.compile(inputset)
|
||||
```
|
||||
|
||||
You could optionally try the full execution in Concrete
|
||||
|
||||
```python
|
||||
# encode/encrypt
|
||||
encrypted_x, encrypted_y = circuit.encrypt(tfhers_type.encode(7), tfhers_type.encode(9))
|
||||
# run
|
||||
encrypted_result = circuit.run(encrypted_x, encrypted_y)
|
||||
# decrypt
|
||||
result = circuit.decrypt(encrypted_result)
|
||||
# decode
|
||||
decoded = tfhers_type.decode(result)
|
||||
```
|
||||
|
||||
## Connecting Concrete and TFHE-rs
|
||||
|
||||
We are going to create a TFHE-rs bridge that facilitates the seamless transfer of ciphertexts and keys between Concrete and TFHE-rs.
|
||||
|
||||
```python
|
||||
tfhers_bridge = tfhers.new_bridge(circuit=circuit)
|
||||
```
|
||||
|
||||
## Key generation
|
||||
|
||||
In order to establish a shared secret key between Concrete and TFHE-rs, there are two possible methods for key generation. The first method (use case 1.1) involves generating the Concrete keyset first and then using the shared secret key in TFHE-rs to partially generate the TFHE-rs keyset. The second method (use case 1.2) involves doing the opposite. You should only run one of the two following methods.
|
||||
|
||||
{% hint style="info" %} Remember that one key generation need to be a partial keygen, to be sure that there is a unique and common secret key. {% endhint %}
|
||||
|
||||
{% hint style="warning" %} Parameters used in TFHE-rs must be the same as the ones used in Concrete. {% endhint %}
|
||||
|
||||
### KeyGen starts in Concrete (use case 1.1)
|
||||
|
||||
First, we generate the Concrete keyset and then serialize the shared secret key that will be used to encrypt the inputs. In our case, this shared secret key is the same for all inputs and outputs.
|
||||
|
||||
```python
|
||||
# generate all keys from scratch (not using initial secret keys)
|
||||
circuit.keygen()
|
||||
# since both inputs have the same type, they will use the same secret key, thus we serialize it once
|
||||
secret_key: bytes = tfhers_bridge.serialize_input_secret_key(input_idx=0)
|
||||
# we write it to a file to be used by TFHE-rs
|
||||
with open("secret_key_from_concrete", "wb") as f:
|
||||
f.write(secret_key)
|
||||
```
|
||||
|
||||
Next, we generate client and server keys in TFHE-rs using the shared secret key from Concrete. We will cover serialization in a [later section](./serialization.md), so there's no need to worry about how we loaded the secret key. For now, we will consider having 4 functions (`save_lwe_sk`, `save_fheuint8`, `load_lwe_sk`, `load_fheuint8`) which respectively save/load an LWE secret key and an FheUint8 to/from a given path.
|
||||
|
||||
```rust
|
||||
use tfhe::core_crypto::prelude::LweSecretKey;
|
||||
use tfhe::ClientKey;
|
||||
|
||||
/// ...
|
||||
|
||||
let lwe_sk: LweSecretKey<Vec<u64>> = load_lwe_sk("secret_key_from_concrete");
|
||||
let shortint_key =
|
||||
tfhe::shortint::ClientKey::try_from_lwe_encryption_key(
|
||||
lwe_sk,
|
||||
// Concrete uses this parameters to define the TFHE-rs ciphertext type
|
||||
tfhe::shortint::prelude::PARAM_MESSAGE_2_CARRY_3_KS_PBS
|
||||
).unwrap();
|
||||
let client_key = ClientKey::from_raw_parts(shortint_key.into(), None, None);
|
||||
let server_key = client_key.generate_server_key();
|
||||
```
|
||||
|
||||
### KeyGen starts in TFHE-rs (use case 1.2)
|
||||
|
||||
First, we generate the TFHE-rs keyset and then serialize the shared secret key that will be used to encrypt the inputs
|
||||
|
||||
```rust
|
||||
use tfhe::{prelude::*, ConfigBuilder};
|
||||
use tfhe::generate_keys;
|
||||
|
||||
/// ...
|
||||
|
||||
// Concrete uses this parameters to define the TFHE-rs ciphertext type
|
||||
let config = ConfigBuilder::with_custom_parameters(
|
||||
tfhe::shortint::prelude::PARAM_MESSAGE_2_CARRY_3_KS_PBS,
|
||||
)
|
||||
.build();
|
||||
|
||||
let (client_key, server_key) = generate_keys(config);
|
||||
let (integer_ck, _, _) = client_key.clone().into_raw_parts();
|
||||
let shortint_ck = integer_ck.into_raw_parts();
|
||||
let (glwe_secret_key, _, _) = shortint_ck.into_raw_parts();
|
||||
let lwe_secret_key = glwe_secret_key.into_lwe_secret_key();
|
||||
|
||||
save_lwe_sk(lwe_secret_key, "secret_key_from_tfhers");
|
||||
```
|
||||
|
||||
Next, we generate a Concrete keyset using the shared secret key from TFHE-rs.
|
||||
|
||||
```python
|
||||
# this was generated from TFHE-rs
|
||||
with open("secret_key_from_tfhers", "rb") as f:
|
||||
sk_buff = f.read()
|
||||
# maps input indices to their secret key
|
||||
input_idx_to_key = {0: sk_buff, 1: sk_buff}
|
||||
# we do a Concrete keygen starting with an initial set of secret keys
|
||||
tfhers_bridge.keygen_with_initial_keys(input_idx_to_key_buffer=input_idx_to_key)
|
||||
```
|
||||
|
||||
## Using ciphertexts
|
||||
|
||||
At this point, we have everything necessary to encrypt, compute, and decrypt on both Concrete and TFHE-rs. Whether you began key generation in Concrete or in TFHE-rs, the keysets on both sides are compatible.
|
||||
|
||||
Now, we'll walk through an encryption and computation process in TFHE-rs, transition to Concrete to run the circuit, and then return to TFHE-rs for decryption.
|
||||
|
||||
First, we do encryption and a simple addition in TFHE-rs. For more information on how to save ciphertexts, refer to [Serialization](./serialization.md).
|
||||
|
||||
```rust
|
||||
let x = FheUint8::encrypt(162, &client_key);
|
||||
let y = FheUint8::encrypt(73, &client_key);
|
||||
// we will add two encrypted integers in TFHE-rs to showcase
|
||||
// that we are doing some part of the computation in TFHE-rs
|
||||
// and the rest in Concrete
|
||||
let z = FheUint8::encrypt(9, &client_key);
|
||||
y += z;
|
||||
|
||||
save_fheuint8(x, "tfhers_x");
|
||||
save_fheuint8(y, "tfhers_y");
|
||||
```
|
||||
|
||||
Next, we can load these ciphertexts in Concrete and then run our compiled circuit as usual.
|
||||
|
||||
```python
|
||||
with open("tfhers_x", "rb") as f:
|
||||
buff_x = f.read()
|
||||
with open("tfhers_y", "rb") as f:
|
||||
buff_y = f.read()
|
||||
tfhers_uint8_x = tfhers_bridge.import_value(buff_x, input_idx=0)
|
||||
tfhers_uint8_y = tfhers_bridge.import_value(buff_y, input_idx=1)
|
||||
|
||||
encrypted_result = circuit.run(tfhers_uint8_x, tfhers_uint8_y)
|
||||
```
|
||||
|
||||
Finally, we can decrypt and decode in Concrete
|
||||
|
||||
```python
|
||||
result = circuit.decrypt(encrypted_result)
|
||||
decoded = tfhers_type.decode(result)
|
||||
|
||||
assert decoded == (162 + 73 + 9) % 213
|
||||
```
|
||||
|
||||
... or export it to TFHE-rs for computation/decryption
|
||||
|
||||
```python
|
||||
buff_out = tfhers_bridge.export_value(encrypted_result, output_idx=0)
|
||||
# write it to file
|
||||
with open("tfhers_out", "wb") as f:
|
||||
f.write(buff_out)
|
||||
```
|
||||
|
||||
```rust
|
||||
let fheuint = load_fheuint8("tfhers_out");
|
||||
// you can do computation before decryption as well
|
||||
let result: u8 = fheuint.decrypt(&client_key);
|
||||
|
||||
assert!(result == (162 + 73 + 9) % 213)
|
||||
```
|
||||
|
||||
Full working example can be found [here](../../../frontends/concrete-python/examples/tfhers/).
|
||||
@@ -18,6 +18,7 @@
|
||||
* [Levenshtein distance with Modules](../../frontends/concrete-python/examples/levenshtein_distance/README.md)
|
||||
* [Inventory Matching System](../../frontends/concrete-python/examples/prime-match/README.md)
|
||||
* [Private Information Retrieval](../../frontends/concrete-python/examples/pir/README.md)
|
||||
* [TFHE-rs Compatibility](../../frontends/concrete-python/examples/tfhers/README.md)
|
||||
|
||||
#### Blog tutorials
|
||||
|
||||
|
||||
Reference in New Issue
Block a user