mirror of
https://github.com/zama-ai/tfhe-rs.git
synced 2026-01-07 22:04:10 -05:00
379 lines
13 KiB
Markdown
379 lines
13 KiB
Markdown
# How Shortint are represented
|
|
|
|
In `shortint`, the encrypted data is stored in an LWE ciphertext.
|
|
|
|
Conceptually, the message stored in an LWE ciphertext, is divided into
|
|
a **carry buffer** and a **message buffer**.
|
|
|
|

|
|
|
|
The message buffer is the space where the actual message is stored. This represents the modulus of the input messages (denoted by `MessageModulus` in the code). When doing computations on a ciphertext, the encrypted message can overflow the message modulus: the exceeding information is stored in the carry buffer. The size of the carry buffer is defined by another modulus, called `CarryModulus`.
|
|
|
|
Together, the message modulus and the carry modulus form the plaintext space that is available in a ciphertext. This space cannot be overflowed, otherwise the computation may result in incorrect outputs.
|
|
|
|
In order to ensure the computation correctness, we keep track of the maximum value encrypted in a
|
|
ciphertext via an associated attribute called the **degree**. When the degree reaches a defined threshold, the carry buffer may be emptied to resume safely the computations. Therefore, in `shortint` the carry modulus is mainly considered as a means to do more computations.
|
|
|
|
# Types of operations
|
|
|
|
The operations available via a `ServerKey` may come in different variants:
|
|
|
|
- operations that take their inputs as encrypted values.
|
|
- scalar operations take at least one non-encrypted value as input.
|
|
|
|
For example, the addition has both variants:
|
|
|
|
- `ServerKey::unchecked_add` which takes two encrypted values and adds them.
|
|
- `ServerKey::unchecked_scalar_add` which takes an encrypted value and a clear value (the
|
|
so-called scalar) and adds them.
|
|
|
|
Each operation may come in different 'flavors':
|
|
|
|
- `unchecked`: Always does the operation, without checking if the result may exceed the capacity of
|
|
the plaintext space. Using this operations might have an impact on the correctness of the
|
|
following operations;
|
|
- `checked`: Checks are done before computing the operation, returning an error if operation
|
|
cannot be done safely;
|
|
- `smart`: Always does the operation, if the operation cannot be computed safely, the smart operation
|
|
will clear the carry modulus to make the operation possible.
|
|
|
|
Not all operations have these 3 flavors, as some of them are implemented in a way
|
|
that the operation is always possible without ever exceeding the plaintext space capacity.
|
|
|
|
|
|
# How to use operation types
|
|
|
|
Let's try to do a circuit evaluation using the different flavours of operations we already introduced.
|
|
For a very small circuit, the `unchecked` flavour may be enough to do the computation correctly.
|
|
Otherwise, the `checked` and `smart` are the best options.
|
|
|
|
As an example, let's do a scalar multiplication, a subtraction and a multiplication.
|
|
|
|
|
|
```rust
|
|
use tfhe::shortint::prelude::*;
|
|
|
|
|
|
fn main() {
|
|
// We generate a set of client/server keys, using the default parameters:
|
|
let (client_key, server_key) = gen_keys(PARAM_MESSAGE_2_CARRY_2);
|
|
|
|
let msg1 = 3;
|
|
let msg2 = 3;
|
|
let scalar = 4;
|
|
|
|
let modulus = client_key.parameters.message_modulus.0;
|
|
|
|
// We use the client key to encrypt two messages:
|
|
let mut ct_1 = client_key.encrypt(msg1);
|
|
let ct_2 = client_key.encrypt(msg2);
|
|
|
|
server_key.unchecked_scalar_mul_assign(&mut ct_1, scalar);
|
|
server_key.unchecked_sub_assign(&mut ct_1, &ct_2);
|
|
server_key.unchecked_mul_lsb_assign(&mut ct_1, &ct_2);
|
|
|
|
// We use the client key to decrypt the output of the circuit:
|
|
let output = client_key.decrypt(&ct_1);
|
|
println!("expected {}, found {}", ((msg1 * scalar as u64 - msg2) * msg2) % modulus as u64, output);
|
|
}
|
|
```
|
|
|
|
During this computation the carry buffer has been overflowed and as all the operations were `unchecked` the output
|
|
may be incorrect.
|
|
|
|
If we redo this same circuit but using the `checked` flavour, a panic will occur.
|
|
|
|
```rust
|
|
use tfhe::shortint::prelude::*;
|
|
|
|
use std::error::Error;
|
|
|
|
fn main() {
|
|
// We generate a set of client/server keys, using the default parameters:
|
|
let (client_key, server_key) = gen_keys(PARAM_MESSAGE_2_CARRY_2);
|
|
|
|
let msg1 = 3;
|
|
let msg2 = 3;
|
|
let scalar = 4;
|
|
|
|
let modulus = client_key.parameters.message_modulus.0;
|
|
|
|
// We use the client key to encrypt two messages:
|
|
let mut ct_1 = client_key.encrypt(msg1);
|
|
let ct_2 = client_key.encrypt(msg2);
|
|
|
|
let mut ops = || -> Result<(), Box<dyn Error>> {
|
|
server_key.checked_scalar_mul_assign(&mut ct_1, scalar)?;
|
|
server_key.checked_sub_assign(&mut ct_1, &ct_2)?;
|
|
server_key.checked_mul_lsb_assign(&mut ct_1, &ct_2)?;
|
|
Ok(())
|
|
};
|
|
|
|
match ops() {
|
|
Ok(_) => (),
|
|
Err(e) => {
|
|
println!("correctness of operations is not guaranteed due to error: {}", e);
|
|
return;
|
|
},
|
|
}
|
|
|
|
// We use the client key to decrypt the output of the circuit:
|
|
let output = client_key.decrypt(&ct_1);
|
|
assert_eq!(output, ((msg1 * scalar as u64 - msg2) * msg2) % modulus as u64);
|
|
}
|
|
```
|
|
|
|
Therefore, the `checked` flavour permits to manually manage the overflow of the carry buffer
|
|
by raising an error if the correctness is not guaranteed.
|
|
|
|
Lastly, using the `smart` flavour will output the correct result all the time. However, the
|
|
computation may be slower
|
|
as the carry buffer may be cleaned during the computations.
|
|
|
|
```rust
|
|
use tfhe::shortint::prelude::*;
|
|
|
|
|
|
fn main() {
|
|
// We generate a set of client/server keys, using the default parameters:
|
|
let (client_key, server_key) = gen_keys(PARAM_MESSAGE_2_CARRY_2);
|
|
|
|
let msg1 = 3;
|
|
let msg2 = 3;
|
|
let scalar = 4;
|
|
|
|
let modulus = client_key.parameters.message_modulus.0;
|
|
|
|
// We use the client key to encrypt two messages:
|
|
let mut ct_1 = client_key.encrypt(msg1);
|
|
let mut ct_2 = client_key.encrypt(msg2);
|
|
|
|
server_key.smart_scalar_mul_assign(&mut ct_1, scalar);
|
|
server_key.smart_sub_assign(&mut ct_1, &mut ct_2);
|
|
server_key.smart_mul_lsb_assign(&mut ct_1, &mut ct_2);
|
|
|
|
// We use the client key to decrypt the output of the circuit:
|
|
let output = client_key.decrypt(&ct_1);
|
|
assert_eq!(output, ((msg1 * scalar as u64 - msg2) * msg2) % modulus as u64);
|
|
}
|
|
```
|
|
#List of available operations
|
|
{% hint style="warning" %}
|
|
|
|
Currently, certain operations can only be used if the parameter set chosen is compatible with the
|
|
bivariate programmable bootstrapping, meaning the carry buffer is larger or equal than the
|
|
message buffer. These operations are marked with a star (*).
|
|
|
|
{% endhint %}
|
|
|
|
|
|
The list of implemented operations for shortints is:
|
|
- addition between two ciphertexts
|
|
- addition between a ciphertext and an unencrypted scalar
|
|
- comparisons `<`, `<=`, `>`, `>=`, `==` between a ciphertext and an unencrypted scalar
|
|
- division of a ciphertext by an unencrypted scalar
|
|
- LSB multiplication between two ciphertexts returning the result truncated to fit in the `message buffer`
|
|
- multiplication of a ciphertext by an unencrypted scalar
|
|
- bitwise shift `<<`, `>>`
|
|
- subtraction of a ciphertext by another ciphertext
|
|
- subtraction of a ciphertext by an unencrypted scalar
|
|
- negation of a ciphertext
|
|
- bitwise and, or and xor (*)
|
|
- comparisons `<`, `<=`, `>`, `>=`, `==` between two ciphertexts (*)
|
|
- division between two ciphertexts (*)
|
|
- MSB multiplication between two ciphertexts returning the part overflowing the `message buffer` (*)
|
|
|
|
In what follows, some simple code examples are given.
|
|
|
|
## Public key encryption
|
|
TFHE-rs supports both private and public key encryption methods. Note that the only difference
|
|
between both lies into the encryption step: in this case, the encryption method is called using
|
|
`public_key` instead of `client_key`.
|
|
|
|
Here a small example on how to use public encryption:
|
|
```rust
|
|
use tfhe::boolean::prelude::*;
|
|
|
|
fn main() {
|
|
// Generate the client key and the server key:
|
|
let (cks, mut sks) = gen_keys();
|
|
let pks = PublicKey::new(&cks);
|
|
// Encryption of one message:
|
|
let ct = pks.encrypt(true);
|
|
// Decryption:
|
|
let dec = cks.decrypt(&ct);
|
|
assert_eq!(true, dec);
|
|
}
|
|
```
|
|
|
|
|
|
|
|
In what follows, all examples are related to private key encryption.
|
|
|
|
## Arithmetic operations
|
|
Classical arithmetic operations are supported by shortints:
|
|
|
|
```rust
|
|
use tfhe::shortint::prelude::*;
|
|
|
|
fn main() {
|
|
// We generate a set of client/server keys to compute over Z/2^2Z, with 2 carry bits
|
|
let (client_key, server_key) = gen_keys(PARAM_MESSAGE_2_CARRY_2);
|
|
|
|
let msg1 = 2;
|
|
let msg2 = 1;
|
|
|
|
let modulus = client_key.parameters.message_modulus.0;
|
|
|
|
// We use the private client key to encrypt two messages:
|
|
let ct_1 = client_key.encrypt(msg1);
|
|
let ct_2 = client_key.encrypt(msg2);
|
|
|
|
// We use the server public key to execute an integer circuit:
|
|
let ct_3 = server_key.unchecked_add(&ct_1, &ct_2);
|
|
|
|
// We use the client key to decrypt the output of the circuit:
|
|
let output = client_key.decrypt(&ct_3);
|
|
assert_eq!(output, (msg1 + msg2) % modulus as u64);
|
|
}
|
|
```
|
|
|
|
|
|
|
|
### Bitwise operations
|
|
|
|
Short homomorphic integer types support some bitwise operations.
|
|
|
|
A simple example on how to use these operations:
|
|
```rust
|
|
|
|
use tfhe::shortint::prelude::*;
|
|
|
|
fn main() {
|
|
// We generate a set of client/server keys to compute over Z/2^2Z, with 2 carry bits
|
|
let (client_key, server_key) = gen_keys(PARAM_MESSAGE_2_CARRY_2);
|
|
|
|
let msg1 = 2;
|
|
let msg2 = 1;
|
|
|
|
let modulus = client_key.parameters.message_modulus.0;
|
|
|
|
// We use the private client key to encrypt two messages:
|
|
let ct_1 = client_key.encrypt(msg1);
|
|
let ct_2 = client_key.encrypt(msg2);
|
|
|
|
// We use the server public key to homomorphically compute a bitwise AND:
|
|
let ct_3 = server_key.unchecked_bitand(&ct_1, &ct_2);
|
|
|
|
// We use the client key to decrypt the output of the circuit:
|
|
let output = client_key.decrypt(&ct_3);
|
|
assert_eq!(output, (msg1 & msg2) % modulus as u64);
|
|
}
|
|
```
|
|
|
|
### Comparisons
|
|
|
|
Short homomorphic integer types support comparison operations.
|
|
|
|
A simple example on how to use these operations:
|
|
|
|
```rust
|
|
|
|
use tfhe::shortint::prelude::*;
|
|
|
|
fn main() {
|
|
// We generate a set of client/server keys to compute over Z/2^2Z, with 2 carry bits
|
|
let (client_key, server_key) = gen_keys(PARAM_MESSAGE_2_CARRY_2);
|
|
|
|
let msg1 = 2;
|
|
let msg2 = 1;
|
|
|
|
let modulus = client_key.parameters.message_modulus.0;
|
|
|
|
// We use the private client key to encrypt two messages:
|
|
let ct_1 = client_key.encrypt(msg1);
|
|
let ct_2 = client_key.encrypt(msg2);
|
|
|
|
// We use the server public key to execute an integer circuit:
|
|
let ct_3 = server_key.unchecked_greater_or_equal(&ct_1, &ct_2);
|
|
|
|
// We use the client key to decrypt the output of the circuit:
|
|
let output = client_key.decrypt(&ct_3);
|
|
assert_eq!(output, (msg1 >= msg2) as u64 % modulus as u64);
|
|
}
|
|
```
|
|
|
|
### Univariate function evaluations
|
|
|
|
A simple example on how to use this operation to homomorphically compute
|
|
the hamming weight (i.e., the number of bit equals to one) of an encrypted
|
|
number.
|
|
|
|
```rust
|
|
|
|
use tfhe::shortint::prelude::*;
|
|
|
|
fn main() {
|
|
// We generate a set of client/server keys to compute over Z/2^2Z, with 2 carry bits
|
|
let (client_key, server_key) = gen_keys(PARAM_MESSAGE_2_CARRY_2);
|
|
|
|
let msg1 = 3;
|
|
|
|
let modulus = client_key.parameters.message_modulus.0;
|
|
|
|
// We use the private client key to encrypt two messages:
|
|
let ct_1 = client_key.encrypt(msg1);
|
|
|
|
//define the accumulator as the
|
|
let acc = server_key.generate_accumulator(|n| n.count_ones().into());
|
|
|
|
// add the two ciphertexts
|
|
let ct_res = server_key.keyswitch_programmable_bootstrap(&ct_1, &acc);
|
|
|
|
|
|
// We use the client key to decrypt the output of the circuit:
|
|
let output = client_key.decrypt(&ct_res);
|
|
assert_eq!(output, msg1.count_ones() as u64);
|
|
}
|
|
```
|
|
|
|
### Bi-variate function evaluations
|
|
|
|
Using the shortint types offers the possibility to evaluate bi-variate functions, i.e.,
|
|
functions that takes two ciphertexts as input. This requires to choose a parameter set
|
|
such that the carry buffer size is at least as large as the message one i.e.,
|
|
PARAM_MESSAGE_X_CARRY_Y with X <= Y.
|
|
|
|
In what follows, a simple code example:
|
|
|
|
```rust
|
|
|
|
use tfhe::shortint::prelude::*;
|
|
|
|
fn main() {
|
|
// We generate a set of client/server keys to compute over Z/2^2Z, with 2 carry bits
|
|
let (client_key, server_key) = gen_keys(PARAM_MESSAGE_2_CARRY_2);
|
|
|
|
let msg1 = 3;
|
|
let msg2 = 2;
|
|
|
|
let modulus = client_key.parameters.message_modulus.0 as u64;
|
|
|
|
// We use the private client key to encrypt two messages:
|
|
let ct_1 = client_key.encrypt(msg1);
|
|
let ct_2 = client_key.encrypt(msg2);
|
|
|
|
// Compute the accumulator for the bivariate functions
|
|
let acc = server_key.generate_accumulator_bivariate(|x,y| (x.count_ones()
|
|
+ y.count_ones()) as u64 % modulus );
|
|
|
|
let ct_res = server_key.keyswitch_programmable_bootstrap_bivariate(&ct_1, &ct_2, &acc);
|
|
|
|
// We use the client key to decrypt the output of the circuit:
|
|
let output = client_key.decrypt(&ct_res);
|
|
assert_eq!(output, (msg1.count_ones() as u64 + msg2.count_ones() as u64) % modulus);
|
|
}
|
|
```
|
|
|
|
|