Compare commits

...

26 Commits

Author SHA1 Message Date
tmontaigu
6e0f86d737 fix: rotations of 1 blocks of 4_4 2025-02-18 17:54:44 +01:00
Arthur Meyre
81223b08d7 fix: fix compression code for GPU which assumed a CPU data layout
- the CPU data layout is truncated to only store relevant bodies (i.e.
emtpy bodies are assumed to be 0) but the GPU CUDA code manages full GLWEs
only. To fix that we manage the data layout during conversions to have
consistent behavior when copying the list to/from CPU/GPU. Compression code
has been fixed on the CPU side to have the proper length for the output
expected by the CUDA code
2025-02-18 17:54:44 +01:00
tmontaigu
726a7719ae fix(integer): rotations/shifts < 2 blocks
This commit fixes a few bugs

* The shift/rotate functions used when blocks encrypt a number of bits
  that is a power of 2 was causing a panic when working on one block.
  - Also, when the number of blocks was low (e.g 2 blocks with 2_2
    params) a noise cleaning step was wrongly skipped

* The function used when blocks encrypt non power of 2 number of bits
  also had a problem

The test have been updated to test with different block sizes and check
the noise level

Overall these bugs only affected low block counts (e.g FheUint2,
FheUint4) ciphertexts
2025-02-18 17:54:44 +01:00
tmontaigu
f03127281f fix(gpu): compressed list gpu <-> cpu
Some counts where to copied from the correct
source to correct destination.

And more importantly, the list on cuda side was stored
using a GlweCiphertextList but the data was compressed
(so the list was mostly empty). This use of a GlweList
instead of a specialized type lead to problems when converting
to Cpu
2025-02-18 17:54:44 +01:00
tmontaigu
f213bfa127 fix(gpu): fix wrong degree after decompression
For Signed and Unsigned DataKind, the degree was
incorrectly set, leading to unneeded carry propagations
2025-02-18 17:54:44 +01:00
Mayeul@Zama
e52e45ab01 chore: fix typo 2025-02-12 09:34:43 +01:00
Mayeul@Zama
5546888007 chore(tfhe): prepare release 0.11.3 2025-02-12 09:34:43 +01:00
Mayeul@Zama
e73aed8cbb fix: fix compression of trivial ciphertext 2025-02-12 09:34:43 +01:00
Mayeul@Zama
98d31d8f17 chore(tfhe): prepare release 0.11.2 2025-01-30 10:32:10 +01:00
Mayeul@Zama
721ca2fed6 feat(integer): impl Named for compression keys 2025-01-30 10:32:10 +01:00
Nicolas Sarlin
e95399306e chore(doc): add strings feature to doctests 2025-01-20 10:59:27 +01:00
Nicolas Sarlin
f3e3c2d499 chore(tfhe): prepare release 0.11.1 2025-01-20 10:59:27 +01:00
Nicolas Sarlin
626074a16b fix(safe_ser): aliases in named for renamed types deserialization 2025-01-17 13:32:38 +01:00
Pedro Alves
b4fa072d2c chore(docs): Remove mention to NVLink
NVLink is not needed anymore in the CUDA backend.
2025-01-16 09:34:54 +01:00
Arthur Meyre
0377a74103 chore(docs): fix various issues with the docs 2025-01-15 11:37:00 +01:00
Arthur Meyre
8690c625fc docs: indicate PBS benchmarks have Gaussian parameters 2025-01-13 16:57:55 +01:00
Arthur Meyre
ce81c4983e chore(docs): replace tabs by spaces 2025-01-13 16:57:55 +01:00
Arthur Meyre
c271265749 docs: add TUniform distribution and link in benchmarks 2025-01-13 16:57:55 +01:00
Agnes Leroy
f0e5e9fce9 chore(doc): update links to the benchmark tables 2025-01-13 16:57:55 +01:00
Nicolas Sarlin
65dd420ed8 doc(zk): explain how to use zkv1 2025-01-13 13:41:08 +01:00
tmontaigu
478716db7e chore(docs): add strings guides 2025-01-13 13:24:50 +01:00
Arthur Meyre
5392070c2e chore: enable strings for docs.rs generation 2025-01-10 14:33:14 +01:00
Agnes Leroy
1bb7526157 chore(test): modify cpu multi-bit parameters for noise test 2025-01-08 15:16:50 +01:00
David Testé
e99676f294 chore(ci): relocate permission checking after should-run step
This induces a failure if the job has to run AND if the triggering actor isn't a member of the zama-ai organization. That would help tfhe-rs maintainers to re-run only workflows that are supposed to run.

The reference is selected based on the event emitted.

We also now use token with restricted permission to check out the repository.
2025-01-08 09:30:19 +01:00
David Testé
ebc186e7f5 chore(ci): remove pull_request which duplicate pull_request_target
Previously pull_request and pull_request_target events were both
emitted thus leading one cancelling the other because of
concurrency group name format.
Since external contribution needs to be allowed we only need
pull_request_target event.
2025-01-08 09:30:19 +01:00
yuxizama
5652d417c8 chore(docs): update discord link 2025-01-07 18:46:22 +01:00
42 changed files with 1089 additions and 414 deletions

View File

@@ -11,26 +11,17 @@ env:
SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png
SLACK_USERNAME: ${{ secrets.BOT_USERNAME }}
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
IS_PULL_REQUEST: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }}
IS_PULL_REQUEST: ${{ github.event_name == 'pull_request_target' }}
REF: ${{ github.event.pull_request.head.sha || github.sha }}
on:
# Allows you to run this workflow manually from the Actions tab as an alternative.
workflow_dispatch:
pull_request:
pull_request_target:
jobs:
check-user-permission:
if: github.event_name == 'pull_request' || github.event_name == 'pull_request_target'
uses: ./.github/workflows/check_triggering_actor.yml
secrets:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
should-run:
runs-on: ubuntu-latest
needs: check-user-permission
if: github.event_name != 'pull_request_target' ||
needs.check-user-permission.result == 'success'
permissions:
pull-requests: write
outputs:
@@ -64,8 +55,8 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
fetch-depth: 0
token: ${{ secrets.FHE_ACTIONS_TOKEN }}
ref: ${{ github.event.pull_request.head.sha }}
token: ${{ secrets.REPO_CHECKOUT_TOKEN }}
ref: ${{ env.REF }}
- name: Check for file changes
id: changed-files
@@ -133,11 +124,17 @@ jobs:
run: |
echo "any_changed=true" >> "$GITHUB_OUTPUT"
check-user-permission:
needs: should-run
uses: ./.github/workflows/check_triggering_actor.yml
secrets:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
setup-instance:
name: Setup instance (fast-tests)
if: github.event_name != 'pull_request' ||
if: github.event_name != 'pull_request_target' ||
needs.should-run.outputs.any_file_changed == 'true'
needs: should-run
needs: [ should-run, check-user-permission ]
runs-on: ubuntu-latest
outputs:
runner-name: ${{ steps.start-instance.outputs.label }}
@@ -155,8 +152,8 @@ jobs:
fast-tests:
name: Fast CPU tests
if: github.event_name != 'pull_request' ||
(github.event_name == 'pull_request' && needs.setup-instance.result != 'skipped')
if: github.event_name != 'pull_request_target' ||
(github.event_name == 'pull_request_target' && needs.setup-instance.result != 'skipped')
needs: [ should-run, setup-instance ]
concurrency:
group: ${{ github.workflow }}_${{ github.head_ref || github.ref }}
@@ -167,8 +164,8 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
persist-credentials: 'false'
token: ${{ secrets.FHE_ACTIONS_TOKEN }}
ref: ${{ github.event.pull_request.head.sha }}
token: ${{ secrets.REPO_CHECKOUT_TOKEN }}
ref: ${{ env.REF }}
- name: Install latest stable
uses: dtolnay/rust-toolchain@a54c7afa936fefeb4456b2dd8068152669aa8203

View File

@@ -824,7 +824,7 @@ test_strings: install_rs_build_toolchain
.PHONY: test_user_doc # Run tests from the .md documentation
test_user_doc: install_rs_build_toolchain
RUSTFLAGS="$(RUSTFLAGS)" cargo $(CARGO_RS_BUILD_TOOLCHAIN) test --profile $(CARGO_PROFILE) --doc \
--features=boolean,shortint,integer,internal-keycache,pbs-stats,zk-pok \
--features=boolean,shortint,integer,internal-keycache,pbs-stats,zk-pok,strings \
-p $(TFHE_SPEC) \
-- test_user_docs::

View File

@@ -1,6 +1,6 @@
[package]
name = "tfhe"
version = "0.11.0"
version = "0.11.3"
edition = "2021"
readme = "../README.md"
keywords = ["fully", "homomorphic", "encryption", "fhe", "cryptography"]
@@ -53,7 +53,9 @@ strum = { version = "0.26", features = ["derive"] }
cbindgen = { version = "0.26.0", optional = true }
[dependencies]
tfhe-csprng = { version = "0.5.0", path = "../tfhe-csprng", features = ["parallel"] }
tfhe-csprng = { version = "0.5.0", path = "../tfhe-csprng", features = [
"parallel",
] }
serde = { workspace = true, features = ["default", "derive"] }
rayon = { workspace = true }
bincode = "1.3.3"
@@ -140,7 +142,15 @@ split-linked-modules = true
[package.metadata.docs.rs]
# TODO: manage builds for docs.rs based on their documentation https://docs.rs/about
features = ["boolean", "shortint", "integer", "gpu", "zk-pok", "software-prng"]
features = [
"boolean",
"shortint",
"integer",
"gpu",
"zk-pok",
"software-prng",
"strings",
]
rustdoc-args = ["--html-in-header", "katex-header.html"]
###########

View File

@@ -48,7 +48,7 @@ Take a deep dive into TFHE-rs, exploring APIs from the highest to the lowest lev
Ask technical questions and discuss with the community. Our team of experts usually answers within 24 hours during working days.
* [Community forum](https://community.zama.ai/)
* [Discord channel](https://discord.com/invite/fhe-org)
* [Discord channel](https://discord.com/invite/zama)
### Developers

View File

@@ -42,6 +42,7 @@
* [Trivial ciphertexts](guides/trivial\_ciphertext.md)
* [PBS statistics](guides/pbs-stats.md)
* [Array](guides/array.md)
* [Strings](guides/strings.md)
## Tutorials

View File

@@ -24,7 +24,7 @@ fn main() {
let mut buffer = vec![];
// The last argument is the max allowed size for the serialized buffer
// The last argument is the max allowed size for the serialized buffer
safe_serialize(&server_key, &mut buffer, 1 << 30).unwrap();
let _server_key_deser: ServerKey =
@@ -158,7 +158,7 @@ In the following example, we use [bincode](https://crates.io/crates/bincode) for
[dependencies]
# ...
tfhe = { version = "0.11.0", features = ["integer"] }
tfhe = { version = "0.11.3", features = ["integer"] }
bincode = "1.3.3"
```

View File

@@ -14,11 +14,11 @@ The following tables benchmark the execution time of some operation sets using `
The next table shows the operation timings on CPU when all inputs are encrypted
{% embed url="https://docs.google.com/spreadsheets/d/1Z2NZvWEkDnbHPYE4Su0Oh2Zz1VBnT9dWbo3E29-LcDg/edit?usp=sharing" %}
{% embed url="https://docs.google.com/spreadsheets/d/1b_-72ArnSdaqfr-gJOnMmVdcBokYZohnylO4LUj2PMw/edit?usp=sharing" %}
The next table shows the operation timings on CPU when the left input is encrypted and the right is a clear scalar of the same size:
{% embed url="https://docs.google.com/spreadsheets/d/1NGPnuBhRasES9Ghaij4ixJJTpXVMqDzbqMniX-qIMGc/edit?usp=sharing" %}
{% embed url="https://docs.google.com/spreadsheets/d/1m3tjCi_2GSIHop2zZLAtVbhdDn5wqTGd2lOA3CcJe-U/edit?usp=sharing" %}
All timings are based on parallelized Radix-based integer operations where each block is encrypted using the default parameters `PARAM_MESSAGE_2_CARRY_2_KS_PBS`. To ensure predictable timings, we perform operations in the `default` mode, which ensures that the input and output encoding are similar (i.e., the carries are always emptied).
@@ -28,7 +28,9 @@ You can minimize operational costs by selecting from 'unchecked', 'checked', or
The next table shows the execution time of a keyswitch followed by a programmable bootstrapping depending on the precision of the input message. The associated parameter set is given. The configuration is Concrete FFT + AVX-512.
{% embed url="https://docs.google.com/spreadsheets/d/1OdZrsk0dHTWSLLvstkpiv0u5G5tE0mCqItTb7WixGdg/edit?usp=sharing" %}
Note that these benchmarks use Gaussian parameters.
{% embed url="https://docs.google.com/spreadsheets/d/1o6MWpbzbYhDs3Pnoq-2hlNEgO9G8wGR5niW-OOZ6c_4/edit?usp=sharing" %}
## Reproducing TFHE-rs benchmarks

View File

@@ -8,26 +8,28 @@ All GPU benchmarks presented here were obtained on H100 GPUs, and rely on the mu
Below come the results for the execution on a single H100.
The following table shows the performance when the inputs of the benchmarked operation are encrypted:
{% embed url="https://docs.google.com/spreadsheets/d/1dhNYXm7oY0l2qjX3dNpSZKjIBJElkEZtPDIWHZ4FA_A/edit?usp=sharing" %}
{% embed url="https://docs.google.com/spreadsheets/d/1xGWykMa8fZ7RWUjkCl-52FJ-BNge8cB-5CSHrVZ6XRo/edit?usp=sharing" %}
The following table shows the performance when the left input of the benchmarked operation is encrypted and the other is a clear scalar of the same size:
{% embed url="https://docs.google.com/spreadsheets/d/1wtnFnOwHrSOvfTWluUEaDoTULyveseVl1ZsYo3AOFKk/edit?usp=sharing" %}
{% embed url="https://docs.google.com/spreadsheets/d/1MZfE9c-cQw3yAP55tu0i8uLl4lTAiH9zW3gRFp0ve7s/edit?usp=sharing" %}
## 2xH100
Below come the results for the execution on two H100's.
The following table shows the performance when the inputs of the benchmarked operation are encrypted:
{% embed url="https://docs.google.com/spreadsheets/d/1_2AUeu3ua8_PXxMfeJCh-pp6b9e529PGVEYUuZRAThg/edit?usp=sharing" %}
{% embed url="https://docs.google.com/spreadsheets/d/1bcL0wgFk-cfR4asGSCWDFt7JaqDYJT-l4pH58A-yBkc/edit?usp=sharing" %}
The following table shows the performance when the left input of the benchmarked operation is encrypted and the other is a clear scalar of the same size:
{% embed url="https://docs.google.com/spreadsheets/d/1nLPt_m1MbkSdhMop0iKDnSN_c605l_JdMpK5JC90N_Q/edit?usp=sharing" %}
{% embed url="https://docs.google.com/spreadsheets/d/1_8VIoStixns22lQq_RBSjVm-0iFHjJpntQTrvEHZpSg/edit?usp=sharing" %}
## Programmable bootstrapping
The next table shows the execution time of a keyswitch followed by a programmable bootstrapping depending on the precision of the input message. The associated parameter set is given.
{% embed url="https://docs.google.com/spreadsheets/d/11JfbPxJ8XMMfob4AZIWhDSglTO9X_YX8R7dNQK73uuk/edit?usp=sharing" %}
Note that these benchmarks use Gaussian parameters.
{% embed url="https://docs.google.com/spreadsheets/d/1KhElQ7sIsShUSVQw5bKFoP-x5BgMaWh1pZtrVAdC3T4/edit?usp=sharing" %}

View File

@@ -2,6 +2,8 @@
This document summarizes the timings of some homomorphic operations over 64-bit encrypted integers, depending on the hardware. More details are given for [the CPU](cpu\_benchmarks.md), [the GPU](gpu\_benchmarks.md), or [zeros-knowledge proofs](zk\_proof\_benchmarks.md).
The cryptographic parameters used for benchmarking follow a tweaked uniform (TUniform) noise distribution instead of a Gaussian. The main advantage of this distribution is to be bounded, whereas the usual Gaussian one is not. In some practical cases, this can simplify the use of homomorphic computation. See the [noise section](../security\_and\_cryptography.md#noise) of the Security and cryptography documentation page for more information on the noise distributions.
You can get the parameters used for benchmarks by cloning the repository and checking out the commit you want to use (starting with the v0.11.0 release) and run the following make command:
```console
@@ -10,4 +12,4 @@ make print_doc_bench_parameters
### Operation time (ms) over FheUint 64
{% embed url="https://docs.google.com/spreadsheets/d/1ZbgsKnFH8eKrFjy9khFeaLYnUhbSV8Xu4H6rwulo0o8/edit?usp=sharing" %}
{% embed url="https://docs.google.com/spreadsheets/d/1OMdGSakEUbIFSEQKhAinTolJjvmPBbafi3DEe3UfzsQ/edit?usp=sharing" %}

View File

@@ -4,4 +4,4 @@ This document details the performance benchmarks of [zero-knowledge proofs](../.
Benchmarks for the zero-knowledge proofs have been run on a `m6i.4xlarge` with 16 cores to simulate an usual client configuration. The verification are done on a `hpc7a.96xlarge` AWS instances to mimic a powerful server.
{% embed url="https://docs.google.com/spreadsheets/d/1llCYHCz2CyLdTwXkiqhjVzJLzxW_RqdjHxmk72m1jm4/edit?usp=sharing" %}
{% embed url="https://docs.google.com/spreadsheets/d/1x12I7Tkdx63Q6sNllygg6urSd5KC1sj1wj4L9jWiET4/edit?usp=sharing" %}

View File

@@ -7,7 +7,7 @@ This document provides instructions to set up **TFHE-rs** in your project.
First, add **TFHE-rs** as a dependency in your `Cargo.toml`.
```toml
tfhe = { version = "0.11.0", features = ["boolean", "shortint", "integer"] }
tfhe = { version = "0.11.3", features = ["boolean", "shortint", "integer"] }
```
{% hint style="info" %}
@@ -35,5 +35,5 @@ By default, **TFHE-rs** makes the assumption that hardware AES features are enab
To add support for older CPU, import **TFHE-rs** with the `software-prng` feature in your `Cargo.toml`:
```toml
tfhe = { version = "0.11.0", features = ["boolean", "shortint", "integer", "software-prng"] }
tfhe = { version = "0.11.3", features = ["boolean", "shortint", "integer", "software-prng"] }
```

View File

@@ -274,39 +274,39 @@ use tfhe::prelude::*;
use tfhe::{generate_keys, set_server_key, ConfigBuilder, FheInt32};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Basic configuration to use homomorphic integers
// Basic configuration to use homomorphic integers
let config = ConfigBuilder::default().build();
// Key generation
let (client_key, server_keys) = generate_keys(config);
let clear_a = 32i32;
let clear_b = -45i32;
// Encrypting the input data using the (private) client_key
// FheInt32: Encrypted equivalent to i32
let encrypted_a = FheInt32::try_encrypt(clear_a, &client_key)?;
let encrypted_b = FheInt32::try_encrypt(clear_b, &client_key)?;
// On the server side:
set_server_key(server_keys);
// Clear equivalent computations: 32 > -45
let encrypted_comp = &encrypted_a.gt(&encrypted_b);
let clear_res = encrypted_comp.decrypt(&client_key);
assert_eq!(clear_res, clear_a > clear_b);
// `encrypted_comp` is a FheBool, thus it encrypts a boolean value.
// Key generation
let (client_key, server_keys) = generate_keys(config);
let clear_a = 32i32;
let clear_b = -45i32;
// Encrypting the input data using the (private) client_key
// FheInt32: Encrypted equivalent to i32
let encrypted_a = FheInt32::try_encrypt(clear_a, &client_key)?;
let encrypted_b = FheInt32::try_encrypt(clear_b, &client_key)?;
// On the server side:
set_server_key(server_keys);
// Clear equivalent computations: 32 > -45
let encrypted_comp = &encrypted_a.gt(&encrypted_b);
let clear_res = encrypted_comp.decrypt(&client_key);
assert_eq!(clear_res, clear_a > clear_b);
// `encrypted_comp` is a FheBool, thus it encrypts a boolean value.
// This acts as a condition on which the
// `select` function can be applied on.
// Clear equivalent computations:
// if 32 > -45 {result = 32} else {result = -45}
let encrypted_res = &encrypted_comp.select(&encrypted_a, &encrypted_b);
let clear_res: i32 = encrypted_res.decrypt(&client_key);
assert_eq!(clear_res, clear_a);
Ok(())
// `select` function can be applied on.
// Clear equivalent computations:
// if 32 > -45 {result = 32} else {result = -45}
let encrypted_res = &encrypted_comp.select(&encrypted_a, &encrypted_b);
let clear_res: i32 = encrypted_res.decrypt(&client_key);
assert_eq!(clear_res, clear_a);
Ok(())
}
```

View File

@@ -59,7 +59,7 @@ edition = "2021"
Then add the following configuration to include **TFHE-rs**:
```toml
tfhe = { version = "0.11.0", features = ["integer"] }
tfhe = { version = "0.11.3", features = ["integer"] }
```
Your updated `Cargo.toml` file should look like this:
@@ -71,7 +71,7 @@ version = "0.1.0"
edition = "2021"
[dependencies]
tfhe = { version = "0.11.0", features = ["integer"] }
tfhe = { version = "0.11.3", features = ["integer"] }
```
If you are on a different platform please refer to the [installation documentation](installation.md) for configuration options of other supported platforms.

View File

@@ -40,7 +40,7 @@ $$b = (\sum_{i = 0}^{n-1}{a_i * s_i}) + plaintext$$
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 two ciphertexts, we must add their $$mask$$ and $$body$$:
$$
ct_0 = (a_{0}, ..., a_{n-1}, b) \\ ct_1 = (a_{0}^{\prime}, ..., a_{n-1}^{\prime}, b^{\prime}) \\ ct_{2} = ct_0 + ct_1 \\ ct_{2} = (a_{0} + a_{0}^{\prime}, ..., a_{n-1} + a_{n-1}^{\prime}, b + b^{\prime})\\ b + b^{\prime} = (\sum_{i = 0}^{n-1}{a_i * s_i}) + plaintext + (\sum_{i = 0}^{n-1}{a_i^{\prime} * s_i}) + plaintext^{\prime}\\ b + b^{\prime} = (\sum_{i = 0}^{n-1}{(a_i + a_i^{\prime})* s_i}) + \Delta m + \Delta m^{\prime} + e + e^{\prime}\\
@@ -63,7 +63,9 @@ The following sections explain the concept of noise and padding in ciphertexts.
To ensure security, LWE requires random noise to be added to the message during encryption.
TFHE scheme draws this random noise from a Centered Normal Distribution with a standard deviation parameter. The choice of standard deviation impacts the security level: increasing the standard deviation enhances security while keeping other factors constant.
TFHE scheme draws this random noise either from:
- A Centered Normal Distribution with a standard deviation parameter. The choice of standard deviation impacts the security level: increasing the standard deviation enhances security while keeping other factors constant.
- A Tweaked Uniform (TUniform) Distribution with a bound parameter $$2^b$$ defined as follows: any value in the interval $$(2^b, ... , 2^b)$$ is selected with probability $$1/2^{b+1}$$, with the two end points $$2^b$$ and $$2^b$$ being selected with probability $$1/2^{b+2}$$. The main advantage of this distribution is to be bounded, whereas the usual Central Normal Distribution one is not. In some practical cases, this can simplify the use of homomorphic computation. The choice of the bound impacts the security level: increasing the bound enhances security while keeping other factors constant.
**TFHE-rs** encodes the noise 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.
@@ -93,7 +95,7 @@ For example, when adding two ciphertexts, the sum could exceed the range of eith
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](https://github.com/malb/lattice-estimator)) with `red_cost_model = reduction.RC.BDGL16`.
The default parameters for the **TFHE-rs** library are chosen considering the IND-CPA security model, and are selected with a bootstrapping failure probability fixed at p\_error = $2^{-40}$. In particular, it is assumed that the results of decrypted computations are not shared by the secret key owner with any third parties, as such an action can lead to leakage of the secret encryption key. If you are designing an application where decryptions must be shared, you will need to craft custom encryption parameters which are chosen in consideration of the IND-CPA^D security model \[1].
The default parameters for the **TFHE-rs** library are chosen considering the IND-CPA security model, and are selected with a bootstrapping failure probability fixed at p\_error = $$2^{-64}$$. In particular, it is assumed that the results of decrypted computations are not shared by the secret key owner with any third parties, as such an action can lead to leakage of the secret encryption key. If you are designing an application where decryptions must be shared, you will need to craft custom encryption parameters which are chosen in consideration of the IND-CPA^D security model \[1].
\[1][ Li, Baiyu, et al. "Securing approximate homomorphic encryption using differential privacy." Annual International Cryptology Conference. Cham: Springer Nature Switzerland, 2022.](https://eprint.iacr.org/2022/816.pdf)

View File

@@ -19,7 +19,7 @@ The following example shows a complete workflow of working with encrypted arrays
# Cargo.toml
[dependencies]
tfhe = { version = "0.11.0", features = ["integer"] }
tfhe = { version = "0.11.3", features = ["integer"] }
```
```rust

View File

@@ -16,7 +16,7 @@ You can load serialized data with the `unversionize` function, even in newer ver
[dependencies]
# ...
tfhe = { version = "0.11.0", features = ["integer"] }
tfhe = { version = "0.11.3", features = ["integer"] }
tfhe-versionable = "0.4.0"
bincode = "1.3.3"
```

View File

@@ -35,8 +35,8 @@ let (result, overflowed) = (&a).overflowing_add(&b);
let result: u16 = result.decrypt(&client_key);
assert_eq!(result, u16::MAX.wrapping_add(1u16));
assert_eq!(
overflowed.decrypt(&client_key),
u16::MAX.overflowing_add(1u16).1
overflowed.decrypt(&client_key),
u16::MAX.overflowing_add(1u16).1
);
assert!(overflowed.decrypt(&client_key));
```

View File

@@ -19,7 +19,7 @@ To use the **TFHE-rs** GPU backend in your project, add the following dependency
```toml
tfhe = { version = "0.11.0", features = ["boolean", "shortint", "integer", "gpu"] }
tfhe = { version = "0.11.3", features = ["boolean", "shortint", "integer", "gpu"] }
```
{% hint style="success" %}
@@ -164,13 +164,7 @@ All operations follow the same syntax than the one described in [here](../gettin
## Multi-GPU support
TFHE-rs supports platforms with multiple GPUs with some restrictions at the moment:
the platform should have NVLink support, and only GPUs that have peer access to GPU 0 via NVLink
will be used for the computation.
Depending on the platform, this can restrict the number of GPUs used to perform the computation.
There is **nothing to change in the code to execute on multiple GPUs**, when
they are available and have peer access to GPU 0 via NVLink. To keep the API as user-friendly as possible, the configuration is automatically set, i.e., the user has no fine-grained control over the number of GPUs to be used.
TFHE-rs supports platforms with multiple GPUs. There is **nothing to change in the code to execute on such platforms**. To keep the API as user-friendly as possible, the configuration is automatically set, i.e., the user has no fine-grained control over the number of GPUs to be used.
## Benchmark
Please refer to the [GPU benchmarks](../getting_started/benchmarks/gpu_benchmarks.md) for detailed performance benchmark results.

View File

@@ -0,0 +1,97 @@
# Strings
This document explains the FheAsciiString type for handling encrypted strings in TFHE-rs.
TFHE-rs has supports for ASCII strings with the type FheAsciiString.
You can enable this feature using the flag: --features=strings
{% hint style="warning" %}
Strings are not yet compatible with `CompactCiphertextList` and `CompressedCiphertextList`
{% endhint %}
## Supported Operations
A variety of common operations are supported for `FheAsciiString`. These include:
- **Comparisons**: `eq`, `ne`, `lt`, `le`, `gt`, `ge`, `eq_ignore_case`
- **Case conversion**: `to_lowercase` / `to_uppercase`
- **String checks**: `starts_with` / `ends_with` / `contains`
- **Trimming**: `trim_start` / `trim_end` / `trim`
- **Prefix/suffix operations**: `strip_prefix` / `strip_suffix`
- **Search**: `find` / `rfind`
When encrypting strings, you can add padding to hide the actual length of strings.
The null character (b'\0') is used as the padding.
Here is an example:
```toml
# Cargo.toml
[dependencies]
tfhe = { version = "0.11.3", features = ["integer", "strings"] }
```
```rust
use tfhe::{ConfigBuilder, generate_keys, set_server_key, FheAsciiString, FheStringLen, ClearString};
use tfhe::prelude::*;
use tfhe::safe_serialization::safe_serialize;
fn main() {
let config = ConfigBuilder::default().build();
let (cks, sks) = generate_keys(config);
set_server_key(sks);
let r = FheAsciiString::try_encrypt("café is french for coffee", &cks);
// As the input string is not strictly ASCII, it is not compatible
assert!(r.is_err());
let string = FheAsciiString::try_encrypt("tfhe-rs", &cks).unwrap();
// This adds 3 chars of padding to the chars of the input string
let padded_string = FheAsciiString::try_encrypt_with_padding("tfhe-rs", 3, &cks).unwrap();
// This makes it so the string has 10 chars (adds padding or truncates input as necessary)
let other_string = FheAsciiString::try_encrypt_with_fixed_sized("tfhe", 10, &cks).unwrap();
let mut buffer1 = vec![];
safe_serialize(&padded_string, &mut buffer1, 1 << 30).unwrap();
let mut buffer2 = vec![];
safe_serialize(&other_string, &mut buffer2, 1 << 30).unwrap();
// The two strings created with padding, have the same
// memory/disk footprint, even though the lengths are not the same
assert_eq!(buffer1.len(), buffer2.len());
// When a string has no padding, its length is known in clear
let len = string.len();
assert!(matches!(len, FheStringLen::NoPadding(7)));
// When a string has padding, its length is only known as an encrypted value
let FheStringLen::Padding(encrypted_len) = padded_string.len() else {
panic!("Expected len to be encrypted");
};
let padded_string_len: u16 = encrypted_len.decrypt(&cks);
assert_eq!(padded_string_len, 7); // Note padding chars are not counted
// The enum resulting of a len() / is_empty() call can be transformed
// to a FheUint16 using `into_ciphertext`
assert!(string.len().into_ciphertext().is_trivial());
assert!(!padded_string.len().into_ciphertext().is_trivial());
let other_string_len: u16 = other_string.len().into_ciphertext().decrypt(&cks);
assert_eq!(other_string_len, 4);
// Padded and un-padded strings are equal if the content is
assert!(padded_string.eq(&string).decrypt(&cks));
let prefix = ClearString::new("tfhe".to_string());
let (stripped_string, has_been_stripped) = string.strip_prefix(&prefix);
// Notice that stripping, makes the string as being considered as padded
// as it is not possible to homomorphically remove chars
let FheStringLen::Padding(encrypted_len) = stripped_string.len() else {
panic!("Expected len to be encrypted");
};
let stripped_string_len: u16 = encrypted_len.decrypt(&cks);
assert_eq!(stripped_string_len, 3);
let decrypted = stripped_string.decrypt(&cks);
assert_eq!(decrypted, "-rs");
assert!(has_been_stripped.decrypt(&cks));
}
```

View File

@@ -8,10 +8,14 @@ This document explains how to implement the zero-knowledge proofs function for c
You can enable this feature using the flag: `--features=zk-pok` when building **TFHE-rs**.
{% endhint %}
Using this feature is straightforward: during encryption, the client generates the proof, and the server validates it before conducting any homomorphic computations. The following example demonstrates how a client can encrypt and prove a ciphertext, and how a server can verify the ciphertext and compute it:
To use this feature, you must first generate a **CRS** (Common Reference String). The CRS is a piece of cryptographic data that is necessary to ensure the security of zero-knowledge proofs. The CRS should be generated in advance and shared between all the clients and the server. A CRS can be reused for multiple encryptions with the same parameters.
Once the CRS is generated, using zero-knowledge proofs is straightforward: during encryption, the client generates the proof, and the server validates it before performing any homomorphic computations.
Note that you need to use dedicated parameters for the compact public key encryption. This helps to reduce the size of encrypted data and speed up the zero-knowledge proof computation.
The following example shows how a client can encrypt and prove a ciphertext, and how a server can verify and compute the ciphertext:
```rust
use rand::prelude::*;
use tfhe::prelude::*;
@@ -28,12 +32,13 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
let casting_params = tfhe::shortint::parameters::key_switching::p_fail_2_minus_64::ks_pbs::V0_11_PARAM_KEYSWITCH_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M64;
// Enable the dedicated parameters on the config
let config = tfhe::ConfigBuilder::with_custom_parameters(params)
.use_dedicated_compact_public_key_parameters((cpk_params, casting_params));
.use_dedicated_compact_public_key_parameters((cpk_params, casting_params)).build();
// The CRS should be generated in an offline phase then shared to all clients and the server
let crs = CompactPkeCrs::from_config(config, 64).unwrap();
// Then use TFHE-rs as usual
let client_key = tfhe::ClientKey::generate(config.clone());
// This is done in an offline phase and the CRS is shared to all clients and the server
let crs = CompactPkeCrs::from_config(config.into(), 64).unwrap();
let client_key = tfhe::ClientKey::generate(config);
let server_key = tfhe::ServerKey::new(&client_key);
let public_key = tfhe::CompactPublicKey::try_new(&client_key).unwrap();
// This can be left empty, but if provided allows to tie the proof to arbitrary data
@@ -79,5 +84,71 @@ and by building the code for the native CPU architecture and in release mode, e.
You can choose a more costly proof with `ZkComputeLoad::Proof`, which has a faster verification time. Alternatively, you can select `ZkComputeLoad::Verify` for a faster proof and slower verification.
{% endhint %}
## Scheme version
The ZK scheme used to generate and verify proofs is available in two versions:
- ZKV1: This version is close to the original paper from [Libert](https://eprint.iacr.org/2023/800).
- ZKV2: Differing from the paper, this version provides better performance for provers and verifiers.
**TFHE-rs** selects automatically the scheme to use based on the encryption parameters during the CRS generation. With default parameters, ZKV2 is selected.
The following example shows how to generate a CRS and proofs for ZKV1. Compared to the previous example, only the parameters are changed:
```rust
use rand::prelude::*;
use tfhe::prelude::*;
use tfhe::set_server_key;
use tfhe::zk::{CompactPkeCrs, ZkComputeLoad};
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut rng = thread_rng();
let params = tfhe::shortint::parameters::PARAM_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M64;
// Indicate which parameters to use for the Compact Public Key encryption
let cpk_params = tfhe::shortint::parameters::compact_public_key_only::p_fail_2_minus_64::ks_pbs::V0_11_PARAM_PKE_TO_SMALL_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M64_ZKV1;
// And parameters allowing to keyswitch/cast to the computation parameters.
let casting_params = tfhe::shortint::parameters::key_switching::p_fail_2_minus_64::ks_pbs::V0_11_PARAM_KEYSWITCH_PKE_TO_SMALL_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M64_ZKV1;
// Enable the dedicated parameters on the config
let config = tfhe::ConfigBuilder::with_custom_parameters(params)
.use_dedicated_compact_public_key_parameters((cpk_params, casting_params)).build();
// The CRS should be generated in an offline phase then shared to all clients and the server
let crs = CompactPkeCrs::from_config(config, 64).unwrap();
// Then use TFHE-rs as usual
let client_key = tfhe::ClientKey::generate(config);
let server_key = tfhe::ServerKey::new(&client_key);
let public_key = tfhe::CompactPublicKey::try_new(&client_key).unwrap();
// This can be left empty, but if provided allows to tie the proof to arbitrary data
let metadata = [b'T', b'F', b'H', b'E', b'-', b'r', b's'];
let clear_a = rng.gen::<u64>();
let clear_b = rng.gen::<u64>();
let proven_compact_list = tfhe::ProvenCompactCiphertextList::builder(&public_key)
.push(clear_a)
.push(clear_b)
.build_with_proof_packed(&crs, &metadata, ZkComputeLoad::Verify)?;
// Server side
let result = {
set_server_key(server_key);
// Verify the ciphertexts
let expander =
proven_compact_list.verify_and_expand(&crs, &public_key, &metadata)?;
let a: tfhe::FheUint64 = expander.get(0)?.unwrap();
let b: tfhe::FheUint64 = expander.get(1)?.unwrap();
a + b
};
// Back on the client side
let a_plus_b: u64 = result.decrypt(&client_key);
assert_eq!(a_plus_b, clear_a.wrapping_add(clear_b));
Ok(())
}
```
## Benchmark
Please refer to the [Zero-knowledge proof benchmarks](../getting_started/benchmarks/zk_proof_benchmarks.md) for detailed performance benchmark results.

View File

@@ -9,7 +9,7 @@ Welcome to this tutorial about `TFHE-rs` `core_crypto` module.
To use `TFHE-rs`, it first has to be added as a dependency in the `Cargo.toml`:
```toml
tfhe = { version = "0.11.0" }
tfhe = { version = "0.11.3" }
```
### Commented code to double a 2-bit message in a leveled fashion and using a PBS with the `core_crypto` module.

View File

@@ -8,13 +8,13 @@ Some cryptographic parameters will require tuning to ensure both the correctness
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 $$2^{-40}$$ 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](https://eprint.iacr.org/2018/421), namely $$2^{-165}$$, but it is up-to-date regarding security requirements.
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 $$2^{-64}$$ 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](https://eprint.iacr.org/2018/421), namely $$2^{-165}$$, but it is up-to-date regarding security requirements.
The following array summarizes this:
| Parameter set | Error probability |
| :-------------------: | :---------------: |
| DEFAULT\_PARAMETERS | $$2^{-40}$$ |
| DEFAULT\_PARAMETERS | $$2^{-64}$$ |
| TFHE\_LIB\_PARAMETERS | $$2^{-165}$$ |
## User-defined parameters

View File

@@ -1,6 +1,6 @@
# Cryptographic Parameters
All parameter sets provide at least 128-bits of security according to the [Lattice-Estimator](https://github.com/malb/lattice-estimator), with an error probability equal to $$2^{-40}$$ when using programmable bootstrapping. This error probability is due to the randomness added at each encryption (see [here](../../../getting\_started/security\_and\_cryptography.md) for more details about the encryption process).
All parameter sets provide at least 128-bits of security according to the [Lattice-Estimator](https://github.com/malb/lattice-estimator), with an error probability equal to $$2^{-64}$$ when using programmable bootstrapping. This error probability is due to the randomness added at each encryption (see [here](../../../getting\_started/security\_and\_cryptography.md) for more details about the encryption process).
## Parameters and message precision
@@ -57,26 +57,22 @@ use tfhe::shortint::parameters::DynamicDistribution;
fn main() {
// WARNING: might be insecure and/or incorrect
// You can create your own set of parameters
let param = ClassicPBSParameters::new(
LweDimension(656),
GlweDimension(2),
PolynomialSize(512),
DynamicDistribution::new_gaussian_from_std_dev(
StandardDev(0.000034119201269311964),
),
DynamicDistribution::new_gaussian_from_std_dev(
StandardDev(0.00000004053919869756513),
),
DecompositionBaseLog(8),
DecompositionLevelCount(2),
DecompositionBaseLog(3),
DecompositionLevelCount(4),
MessageModulus(4),
CarryModulus(1),
MaxNoiseLevel::new(2),
2.0f64.powi(-40),
CiphertextModulus::new_native(),
EncryptionKeyChoice::Big,
);
let param = ClassicPBSParameters {
lwe_dimension: LweDimension(879),
glwe_dimension: GlweDimension(1),
polynomial_size: PolynomialSize(2048),
lwe_noise_distribution: DynamicDistribution::new_t_uniform(46),
glwe_noise_distribution: DynamicDistribution::new_t_uniform(17),
pbs_base_log: DecompositionBaseLog(23),
pbs_level: DecompositionLevelCount(1),
ks_base_log: DecompositionBaseLog(3),
ks_level: DecompositionLevelCount(5),
message_modulus: MessageModulus(4),
carry_modulus: CarryModulus(4),
max_noise_level: MaxNoiseLevel::new(5),
log2_p_fail: -71.625,
ciphertext_modulus: CiphertextModulus::new_native(),
encryption_key_choice: EncryptionKeyChoice::Big,
};
}
```

View File

@@ -1,8 +1,13 @@
# Homomorphic case changing on Ascii string
This tutorial demonstrates how to build a data type that represents an ASCII string in Fully Homomorphic Encryption (FHE) by implementing to\_lower and to\_upper functions.
This tutorial demonstrates how to build your own data type that represents an ASCII string in Fully Homomorphic Encryption (FHE) by implementing to\_lower and to\_upper functions.
An ASCII character is stored in 7 bits. To store an encrypted ASCII, we use the `FheUint8`:
{% hint style="info" %}
Since version 0.11, **TFHE-rs** has introduced the `strings` feature, which provides an easy to use FHE strings API. See the [fhe strings guide](../guides/strings.md) for more information.
{% endhint %}
An ASCII character is stored in 7 bits. In this tutorial, we use the `FheUint8` to store an encrypted ASCII:
* The uppercase letters are in the range \[65, 90]
* The lowercase letters are in the range \[97, 122]
@@ -24,13 +29,10 @@ To use the `FheUint8` type, enable the `integer` feature:
# Cargo.toml
[dependencies]
# Default configuration for x86 Unix machines:
tfhe = { version = "0.11.0", features = ["integer"] }
tfhe = { version = "0.11.3", features = ["integer"] }
```
Refer to the [installation guide](../getting\_started/installation.md) for other configurations.
The `FheAsciiString::encrypt` function performs data validation to ensure the input string contains only ASCII characters.
The `MyFheString::encrypt` function performs data validation to ensure the input string contains only ASCII characters.
In FHE operations, direct branching on encrypted values is not possible. However, you can evaluate a boolean condition to obtain the desired outcome. Here is an example to check and convert the 'char' to a lowercase without using a branch:
@@ -83,7 +85,7 @@ use tfhe::{generate_keys, set_server_key, ClientKey, ConfigBuilder, FheUint8};
const UP_LOW_DISTANCE: u8 = 32;
struct FheAsciiString {
struct MyFheString {
bytes: Vec<FheUint8>,
}
@@ -95,7 +97,7 @@ fn to_lower(c: &FheUint8) -> FheUint8 {
c + FheUint8::cast_from(c.gt(64) & c.lt(91)) * UP_LOW_DISTANCE
}
impl FheAsciiString {
impl MyFheString {
fn encrypt(string: &str, client_key: &ClientKey) -> Self {
assert!(
string.is_ascii(),
@@ -140,7 +142,7 @@ fn main() {
set_server_key(server_key);
let my_string = FheAsciiString::encrypt("Hello Zama, how is it going?", &client_key);
let my_string = MyFheString::encrypt("Hello Zama, how is it going?", &client_key);
let verif_string = my_string.decrypt(&client_key);
println!("Start string: {verif_string}");
@@ -155,3 +157,45 @@ fn main() {
assert_eq!(verif_string, "hello zama, how is it going?");
}
```
## Using **TFHE-rs** strings feature
This code can be greatly simplified by using the `strings` feature from **TFHE-rs**.
First, add the feature in your `Cargo.toml`
```toml
# Cargo.toml
[dependencies]
tfhe = { version = "0.11.3", features = ["strings"] }
```
The `FheAsciiString` type allows to simply do homomorphic case changing of encrypted strings (and much more!):
```rust
use tfhe::prelude::*;
use tfhe::{generate_keys, set_server_key, ConfigBuilder, FheAsciiString};
fn main() {
let config = ConfigBuilder::default().build();
let (client_key, server_key) = generate_keys(config);
set_server_key(server_key);
let my_string =
FheAsciiString::try_encrypt("Hello Zama, how is it going?", &client_key).unwrap();
let verif_string = my_string.decrypt(&client_key);
println!("Start string: {verif_string}");
let my_string_upper = my_string.to_uppercase();
let verif_string = my_string_upper.decrypt(&client_key);
println!("Upper string: {verif_string}");
assert_eq!(verif_string, "HELLO ZAMA, HOW IS IT GOING?");
let my_string_lower = my_string_upper.to_lowercase();
let verif_string = my_string_lower.decrypt(&client_key);
println!("Lower string: {verif_string}");
assert_eq!(verif_string, "hello zama, how is it going?");
}
```
You can read more about this in the [FHE strings documentation](../guides/strings.md)

View File

@@ -17,7 +17,7 @@ This function returns a Boolean (`true` or `false`) so that the total count of `
```toml
# Cargo.toml
tfhe = { version = "0.11.0", features = ["integer"] }
tfhe = { version = "0.11.3", features = ["integer"] }
```
First, define the verification function.

View File

@@ -36,16 +36,16 @@ pub const NOISE_TEST_PARAMS_4_BITS_NATIVE_U64_132_BITS_GAUSSIAN: ClassicTestPara
#[allow(dead_code)]
pub const NOISE_TEST_PARAMS_MULTI_BIT_GROUP_3_2_BITS_NATIVE_U64_132_BITS_GAUSSIAN:
MultiBitTestParams<u64> = MultiBitTestParams {
input_lwe_dimension: LweDimension(256 * 3),
input_lwe_dimension: LweDimension(759),
lwe_noise_distribution: DynamicDistribution::new_gaussian_from_std_dev(StandardDev(
1.1098369627275701e-05,
1.296274149494132e-05,
)),
decomp_base_log: DecompositionBaseLog(17),
decomp_base_log: DecompositionBaseLog(22),
decomp_level_count: DecompositionLevelCount(1),
glwe_dimension: GlweDimension(3),
polynomial_size: PolynomialSize(512),
glwe_dimension: GlweDimension(2),
polynomial_size: PolynomialSize(1024),
glwe_noise_distribution: DynamicDistribution::new_gaussian_from_std_dev(StandardDev(
1.9524392655548086e-11,
2.845267479601915e-15,
)),
message_modulus_log: MessageModulusLog(2),
ciphertext_modulus: CiphertextModulus::new_native(),
@@ -56,9 +56,9 @@ pub const NOISE_TEST_PARAMS_MULTI_BIT_GROUP_3_2_BITS_NATIVE_U64_132_BITS_GAUSSIA
#[allow(clippy::excessive_precision)]
pub const NOISE_TEST_PARAMS_MULTI_BIT_GROUP_3_4_BITS_NATIVE_U64_132_BITS_GAUSSIAN:
MultiBitTestParams<u64> = MultiBitTestParams {
input_lwe_dimension: LweDimension(279 * 3),
input_lwe_dimension: LweDimension(912),
lwe_noise_distribution: DynamicDistribution::new_gaussian_from_std_dev(StandardDev(
3.3747142481837397e-06,
9.252442079345288e-07,
)),
decomp_base_log: DecompositionBaseLog(22),
decomp_level_count: DecompositionLevelCount(1),
@@ -77,7 +77,7 @@ pub const NOISE_TEST_PARAMS_MULTI_BIT_GROUP_3_4_BITS_NATIVE_U64_132_BITS_GAUSSIA
#[allow(dead_code)]
pub const NOISE_TEST_PARAMS_MULTI_BIT_GROUP_3_6_BITS_NATIVE_U64_132_BITS_GAUSSIAN:
MultiBitTestParams<u64> = MultiBitTestParams {
input_lwe_dimension: LweDimension(326 * 3),
input_lwe_dimension: LweDimension(978),
lwe_noise_distribution: DynamicDistribution::new_gaussian_from_std_dev(StandardDev(
2.962875621642539e-07,
)),

View File

@@ -219,6 +219,12 @@ pub fn glwe_ciphertext_size(glwe_size: GlweSize, polynomial_size: PolynomialSize
glwe_size.0 * polynomial_size.0
}
/// Return the number of elements in the **mask** of a [`GlweCiphertext`]
/// given a [`GlweDimension`] and [`PolynomialSize`].
pub fn glwe_mask_size(glwe_dim: GlweDimension, polynomial_size: PolynomialSize) -> usize {
glwe_dim.0 * polynomial_size.0
}
/// Return the number of elements in a [`GlweMask`] given a [`GlweDimension`] and
/// [`PolynomialSize`].
pub fn glwe_ciphertext_mask_size(

View File

@@ -661,6 +661,18 @@ pub struct CudaGlweList<T: UnsignedInteger> {
pub ciphertext_modulus: CiphertextModulus<T>,
}
impl<T: UnsignedInteger> CudaGlweList<T> {
pub fn duplicate(&self, streams: &CudaStreams) -> Self {
Self {
d_vec: self.d_vec.duplicate(streams),
glwe_ciphertext_count: self.glwe_ciphertext_count,
glwe_dimension: self.glwe_dimension,
polynomial_size: self.polynomial_size,
ciphertext_modulus: self.ciphertext_modulus,
}
}
}
/// Get the number of GPUs on the machine
pub fn get_number_of_gpus() -> i32 {
unsafe { cuda_get_number_of_gpus() }

View File

@@ -424,6 +424,16 @@ impl<T: Numeric> CudaVec<T> {
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn duplicate(&self, streams: &CudaStreams) -> Self {
let d_vec = unsafe {
let mut d_vec = Self::new_async(self.len(), streams, 0);
d_vec.copy_from_gpu_async(self, streams, 0);
d_vec
};
streams.synchronize();
d_vec
}
}
// SAFETY

View File

@@ -1,6 +1,7 @@
use super::ClientKey;
use crate::conformance::ParameterSetConformant;
use crate::integer::backward_compatibility::list_compression::*;
use crate::named::Named;
use serde::{Deserialize, Serialize};
use tfhe_versionable::Versionize;
@@ -34,6 +35,26 @@ pub struct CompressedDecompressionKey {
pub(crate) key: crate::shortint::list_compression::CompressedDecompressionKey,
}
impl Named for CompressionPrivateKeys {
const NAME: &'static str = "high_level_api::CompressionPrivateKeys";
}
impl Named for CompressionKey {
const NAME: &'static str = "high_level_api::CompressionKey";
}
impl Named for DecompressionKey {
const NAME: &'static str = "high_level_api::DecompressionKey";
}
impl Named for CompressedCompressionKey {
const NAME: &'static str = "high_level_api::CompressedCompressionKey";
}
impl Named for CompressedDecompressionKey {
const NAME: &'static str = "high_level_api::CompressedDecompressionKey";
}
impl CompressedCompressionKey {
pub fn decompress(&self) -> CompressionKey {
CompressionKey {

View File

@@ -1,11 +1,8 @@
use crate::core_crypto::entities::packed_integers::PackedIntegers;
use crate::core_crypto::entities::GlweCiphertextList;
use crate::core_crypto::gpu::glwe_ciphertext_list::CudaGlweCiphertextList;
use crate::core_crypto::gpu::vec::{CudaVec, GpuIndex};
use crate::core_crypto::gpu::CudaStreams;
use crate::core_crypto::prelude::compressed_modulus_switched_glwe_ciphertext::CompressedModulusSwitchedGlweCiphertext;
use crate::core_crypto::prelude::{
glwe_ciphertext_size, CiphertextCount, ContiguousEntityContainer, LweCiphertextCount,
};
use crate::core_crypto::prelude::{glwe_ciphertext_size, CiphertextCount, LweCiphertextCount};
use crate::integer::ciphertext::{CompressedCiphertextList, DataKind};
use crate::integer::gpu::ciphertext::boolean_value::CudaBooleanBlock;
use crate::integer::gpu::ciphertext::{
@@ -13,7 +10,7 @@ use crate::integer::gpu::ciphertext::{
CudaUnsignedRadixCiphertext,
};
use crate::integer::gpu::list_compression::server_keys::{
CudaCompressionKey, CudaDecompressionKey, CudaPackedGlweCiphertext,
CudaCompressionKey, CudaDecompressionKey, CudaPackedGlweCiphertextList,
};
use crate::shortint::ciphertext::CompressedCiphertextList as ShortintCompressedCiphertextList;
use crate::shortint::PBSOrder;
@@ -61,11 +58,14 @@ impl CudaExpandable for CudaBooleanBlock {
}
}
pub struct CudaCompressedCiphertextList {
pub(crate) packed_list: CudaPackedGlweCiphertext,
pub(crate) packed_list: CudaPackedGlweCiphertextList,
pub(crate) info: Vec<DataKind>,
}
impl CudaCompressedCiphertextList {
pub fn gpu_indexes(&self) -> &[GpuIndex] {
&self.packed_list.data.gpu_indexes
}
pub fn len(&self) -> usize {
self.info.len()
}
@@ -178,39 +178,45 @@ impl CudaCompressedCiphertextList {
/// let converted_compressed = cuda_compressed.to_compressed_ciphertext_list(&streams);
/// ```
pub fn to_compressed_ciphertext_list(&self, streams: &CudaStreams) -> CompressedCiphertextList {
let glwe_list = self
.packed_list
.glwe_ciphertext_list
.to_glwe_ciphertext_list(streams);
let ciphertext_modulus = self.packed_list.glwe_ciphertext_list.ciphertext_modulus();
let ciphertext_modulus = self.packed_list.ciphertext_modulus;
let message_modulus = self.packed_list.message_modulus;
let carry_modulus = self.packed_list.carry_modulus;
let lwe_per_glwe = self.packed_list.lwe_per_glwe;
let storage_log_modulus = self.packed_list.storage_log_modulus;
let glwe_dimension = self.packed_list.glwe_dimension;
let polynomial_size = self.packed_list.polynomial_size;
let mut modulus_switched_glwe_ciphertext_list =
Vec::with_capacity(self.packed_list.glwe_ciphertext_count().0);
let initial_len = self.packed_list.initial_len;
let number_bits_to_pack = initial_len * storage_log_modulus.0;
let len = number_bits_to_pack.div_ceil(u64::BITS as usize);
let flat_cpu_data = unsafe {
let mut v = vec![0u64; self.packed_list.data.len()];
self.packed_list.data.copy_to_cpu_async(&mut v, streams, 0);
streams.synchronize();
v
};
let modulus_switched_glwe_ciphertext_list = glwe_list
.iter()
.map(|x| {
let glwe_dimension = x.glwe_size().to_glwe_dimension();
let polynomial_size = x.polynomial_size();
CompressedModulusSwitchedGlweCiphertext {
packed_integers: PackedIntegers {
packed_coeffs: x.into_container()[0..len].to_vec(),
log_modulus: storage_log_modulus,
initial_len,
},
glwe_dimension,
polynomial_size,
bodies_count: LweCiphertextCount(self.packed_list.bodies_count),
uncompressed_ciphertext_modulus: ciphertext_modulus,
}
})
.collect_vec();
let mut num_bodies_left = self.packed_list.bodies_count;
let mut chunk_start = 0;
while num_bodies_left != 0 {
let bodies_count = LweCiphertextCount(num_bodies_left.min(lwe_per_glwe.0));
let initial_len = (glwe_dimension.0 * polynomial_size.0) + bodies_count.0;
let number_bits_to_pack = initial_len * storage_log_modulus.0;
let len = number_bits_to_pack.div_ceil(u64::BITS as usize);
let chunk_end = chunk_start + len;
modulus_switched_glwe_ciphertext_list.push(CompressedModulusSwitchedGlweCiphertext {
packed_integers: PackedIntegers {
packed_coeffs: flat_cpu_data[chunk_start..chunk_end].to_vec(),
log_modulus: storage_log_modulus,
initial_len,
},
glwe_dimension,
polynomial_size,
bodies_count,
uncompressed_ciphertext_modulus: ciphertext_modulus,
});
num_bodies_left = num_bodies_left.saturating_sub(lwe_per_glwe.0);
chunk_start = chunk_end;
}
let count = CiphertextCount(self.packed_list.bodies_count);
let pbs_order = PBSOrder::KeyswitchBootstrap;
@@ -312,39 +318,48 @@ impl CompressedCiphertextList {
let first_ct = modulus_switched_glwe_ciphertext_list.first().unwrap();
let storage_log_modulus = first_ct.packed_integers.log_modulus;
let initial_len = first_ct.packed_integers.initial_len;
let bodies_count = first_ct.bodies_count.0;
let initial_len = modulus_switched_glwe_ciphertext_list
.iter()
.map(|glwe| glwe.packed_integers.initial_len)
.sum();
let message_modulus = self.packed_list.message_modulus;
let carry_modulus = self.packed_list.carry_modulus;
let mut data = modulus_switched_glwe_ciphertext_list
let mut flat_cpu_data = modulus_switched_glwe_ciphertext_list
.iter()
.flat_map(|ct| ct.packed_integers.packed_coeffs.clone())
.collect_vec();
let glwe_ciphertext_size = glwe_ciphertext_size(
first_ct.glwe_dimension.to_glwe_size(),
first_ct.polynomial_size,
);
data.resize(
self.packed_list.modulus_switched_glwe_ciphertext_list.len() * glwe_ciphertext_size,
0,
);
let glwe_ciphertext_list = GlweCiphertextList::from_container(
data.as_slice(),
first_ct.glwe_dimension.to_glwe_size(),
first_ct.polynomial_size,
self.packed_list.ciphertext_modulus,
);
let glwe_ciphertext_count = self.packed_list.modulus_switched_glwe_ciphertext_list.len();
let glwe_size = self.packed_list.modulus_switched_glwe_ciphertext_list[0]
.glwe_dimension()
.to_glwe_size();
let polynomial_size =
self.packed_list.modulus_switched_glwe_ciphertext_list[0].polynomial_size();
// FIXME: have a more precise memory handling, this is too long and should be "just" the
// original flat_cpu_data.len()
let unpacked_glwe_ciphertext_flat_len =
glwe_ciphertext_count * glwe_ciphertext_size(glwe_size, polynomial_size);
flat_cpu_data.resize(unpacked_glwe_ciphertext_flat_len, 0u64);
let flat_gpu_data = unsafe {
let v = CudaVec::from_cpu_async(flat_cpu_data.as_slice(), streams, 0);
streams.synchronize();
v
};
CudaCompressedCiphertextList {
packed_list: CudaPackedGlweCiphertext {
glwe_ciphertext_list: CudaGlweCiphertextList::from_glwe_ciphertext_list(
&glwe_ciphertext_list,
streams,
),
packed_list: CudaPackedGlweCiphertextList {
data: flat_gpu_data,
glwe_dimension: first_ct.glwe_dimension(),
polynomial_size: first_ct.polynomial_size(),
message_modulus,
carry_modulus,
bodies_count,
ciphertext_modulus: self.packed_list.ciphertext_modulus,
bodies_count: self.packed_list.count.0,
storage_log_modulus,
lwe_per_glwe,
initial_len,
@@ -484,7 +499,9 @@ impl<'de> serde::Deserialize<'de> for CudaCompressedCiphertextList {
#[cfg(test)]
mod tests {
use super::*;
use crate::integer::ciphertext::CompressedCiphertextListBuilder;
use crate::integer::gpu::gen_keys_radix_gpu;
use crate::integer::{ClientKey, RadixCiphertext, RadixClientKey};
use crate::shortint::parameters::list_compression::COMP_PARAM_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M64;
use crate::shortint::parameters::PARAM_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M64;
use rand::Rng;
@@ -492,6 +509,154 @@ mod tests {
const NB_TESTS: usize = 10;
const NB_OPERATOR_TESTS: usize = 10;
#[test]
fn test_cpu_to_gpu_compressed_ciphertext_list() {
const NUM_BLOCKS: usize = 32;
let streams = CudaStreams::new_multi_gpu();
let params = PARAM_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M64;
let comp_params = COMP_PARAM_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M64;
let cks = ClientKey::new(params);
let private_compression_key = cks.new_compression_private_key(comp_params);
let (compressed_compression_key, compressed_decompression_key) =
cks.new_compressed_compression_decompression_keys(&private_compression_key);
let cuda_compression_key = compressed_compression_key.decompress_to_cuda(&streams);
let cuda_decompression_key = compressed_decompression_key.decompress_to_cuda(
cks.parameters().glwe_dimension(),
cks.parameters().polynomial_size(),
cks.parameters().message_modulus(),
cks.parameters().carry_modulus(),
cks.parameters().ciphertext_modulus(),
&streams,
);
let cpu_compression_key = compressed_compression_key.decompress();
let cpu_decompression_key = compressed_decompression_key.decompress();
let radix_cks = RadixClientKey::from((cks, NUM_BLOCKS));
// How many uints of NUM_BLOCKS we have to push in the list to ensure it
// internally has more than one packed GLWE
const MAX_NB_MESSAGES: usize = 1 + 2 * COMP_PARAM_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M64
.lwe_per_glwe
.0
/ NUM_BLOCKS;
let mut rng = rand::thread_rng();
let message_modulus: u128 = radix_cks.parameters().message_modulus().0 as u128;
let modulus = message_modulus.pow(NUM_BLOCKS as u32);
let messages = (0..MAX_NB_MESSAGES)
.map(|_| rng.gen::<u128>() % modulus)
.collect::<Vec<_>>();
let cpu_cts = messages
.iter()
.map(|message| radix_cks.encrypt(*message))
.collect_vec();
let cuda_cts = cpu_cts
.iter()
.map(|ct| CudaUnsignedRadixCiphertext::from_radix_ciphertext(ct, &streams))
.collect_vec();
let cpu_compressed_list = {
let mut builder = CompressedCiphertextListBuilder::new();
for d_ct in cpu_cts {
builder.push(d_ct);
}
builder.build(&cpu_compression_key)
};
let cuda_compressed_list = {
let mut builder = CudaCompressedCiphertextListBuilder::new();
for d_ct in cuda_cts {
builder.push(d_ct, &streams);
}
builder.build(&cuda_compression_key, &streams)
};
// Test Decompression on Gpu
{
// Roundtrip Gpu->Cpu->Gpu
let cuda_compressed_list = cuda_compressed_list
.to_compressed_ciphertext_list(&streams)
.to_cuda_compressed_ciphertext_list(&streams);
let cuda_compressed_list_2 =
cpu_compressed_list.to_cuda_compressed_ciphertext_list(&streams);
for (i, message) in messages.iter().enumerate() {
let d_decompressed: CudaUnsignedRadixCiphertext = cuda_compressed_list
.get(i, &cuda_decompression_key, &streams)
.unwrap()
.unwrap();
let decompressed = d_decompressed.to_radix_ciphertext(&streams);
let decrypted: u128 = radix_cks.decrypt(&decompressed);
assert_eq!(
decrypted, *message,
"Invalid decompression for cuda list that roundtripped Cuda->Cpu->Cuda"
);
let d_decompressed: CudaUnsignedRadixCiphertext = cuda_compressed_list_2
.get(i, &cuda_decompression_key, &streams)
.unwrap()
.unwrap();
let decompressed = d_decompressed.to_radix_ciphertext(&streams);
let decrypted: u128 = radix_cks.decrypt(&decompressed);
assert_eq!(
decrypted, *message,
"Invalid decompression for cuda list that originated from Cpu"
);
}
}
// Test Decompression on CPU (to test conversions)
{
let expected_flat_len = cpu_compressed_list.packed_list.flat_len();
// Roundtrip Cpu->Gpu->Cpu
let cpu_compressed_list = cpu_compressed_list
.to_cuda_compressed_ciphertext_list(&streams)
.to_compressed_ciphertext_list(&streams);
assert_eq!(
cpu_compressed_list.packed_list.flat_len(),
expected_flat_len,
"Invalid flat len after Cpu->Gpu->Cpu"
);
let cpu_compressed_list_2 =
cuda_compressed_list.to_compressed_ciphertext_list(&streams);
assert_eq!(
cpu_compressed_list_2.packed_list.flat_len(),
expected_flat_len,
"Invalid flat len after Gpu->Cpu"
);
for (i, message) in messages.iter().enumerate() {
let decompressed: RadixCiphertext = cpu_compressed_list
.get(i, &cpu_decompression_key)
.unwrap()
.unwrap();
let decrypted: u128 = radix_cks.decrypt(&decompressed);
assert_eq!(
decrypted, *message,
"Invalid decompression for cpu list that roundtripped Cpu->Gpu->Cpu"
);
let decompressed: RadixCiphertext = cpu_compressed_list_2
.get(i, &cpu_decompression_key)
.unwrap()
.unwrap();
let decrypted: u128 = radix_cks.decrypt(&decompressed);
assert_eq!(
decrypted, *message,
"Invalid decompression for cpu list that originated from Gpu"
);
}
}
}
#[test]
fn test_gpu_ciphertext_compression() {
const NUM_BLOCKS: usize = 32;
@@ -550,6 +715,10 @@ mod tests {
.get(i, &cuda_decompression_key, &streams)
.unwrap()
.unwrap();
assert!(
d_decompressed.block_carries_are_empty(),
"Expected carries to be empty"
);
let decompressed = d_decompressed.to_radix_ciphertext(&streams);
let decrypted: u128 = radix_cks.decrypt(&decompressed);
assert_eq!(decrypted, *message);
@@ -586,6 +755,10 @@ mod tests {
.get(i, &cuda_decompression_key, &streams)
.unwrap()
.unwrap();
assert!(
d_decompressed.block_carries_are_empty(),
"Expected carries to be empty"
);
let decompressed = d_decompressed.to_signed_radix_ciphertext(&streams);
let decrypted: i128 = radix_cks.decrypt_signed(&decompressed);
assert_eq!(decrypted, *message);
@@ -624,6 +797,10 @@ mod tests {
.get(i, &cuda_decompression_key, &streams)
.unwrap()
.unwrap();
assert!(
d_decompressed.0.holds_boolean_value(),
"Expected boolean block to have the degree of a boolean value"
);
let decompressed = d_decompressed.to_boolean_block(&streams);
let decrypted = radix_cks.decrypt_bool(&decompressed);
assert_eq!(decrypted, *message);

View File

@@ -1,11 +1,10 @@
use crate::core_crypto::gpu::entities::lwe_packing_keyswitch_key::CudaLwePackingKeyswitchKey;
use crate::core_crypto::gpu::glwe_ciphertext_list::CudaGlweCiphertextList;
use crate::core_crypto::gpu::lwe_ciphertext_list::CudaLweCiphertextList;
use crate::core_crypto::gpu::vec::CudaVec;
use crate::core_crypto::gpu::CudaStreams;
use crate::core_crypto::prelude::{
CiphertextModulus, CiphertextModulusLog, GlweCiphertextCount, LweCiphertextCount,
PolynomialSize,
glwe_ciphertext_size, CiphertextModulus, CiphertextModulusLog, GlweCiphertextCount,
LweCiphertextCount, PolynomialSize,
};
use crate::integer::ciphertext::DataKind;
use crate::integer::compression_keys::CompressionKey;
@@ -37,22 +36,56 @@ pub struct CudaDecompressionKey {
pub ciphertext_modulus: CiphertextModulus<u64>,
}
pub struct CudaPackedGlweCiphertext {
pub glwe_ciphertext_list: CudaGlweCiphertextList<u64>,
pub struct CudaPackedGlweCiphertextList {
// The compressed GLWE list's elements
pub data: CudaVec<u64>,
pub glwe_dimension: GlweDimension,
pub polynomial_size: PolynomialSize,
pub message_modulus: MessageModulus,
pub carry_modulus: CarryModulus,
pub bodies_count: usize,
pub ciphertext_modulus: CiphertextModulus<u64>,
pub storage_log_modulus: CiphertextModulusLog,
pub lwe_per_glwe: LweCiphertextCount,
// Number of lwe bodies that are compressed in this list
pub bodies_count: usize,
// Number of elements (u64) the uncompressed GLWE list had
// keep in mind the last GLWE may not be full
pub initial_len: usize,
}
impl Clone for CudaPackedGlweCiphertext {
fn clone(&self) -> Self {
impl CudaPackedGlweCiphertextList {
pub fn glwe_ciphertext_count(&self) -> GlweCiphertextCount {
let uncompressed_glwe_size =
glwe_ciphertext_size(self.glwe_dimension.to_glwe_size(), self.polynomial_size);
GlweCiphertextCount(self.initial_len.div_ceil(uncompressed_glwe_size))
}
pub fn duplicate(&self, streams: &CudaStreams) -> Self {
Self {
glwe_ciphertext_list: CudaGlweCiphertextList(self.glwe_ciphertext_list.0.clone()),
data: self.data.duplicate(streams),
glwe_dimension: self.glwe_dimension,
polynomial_size: self.polynomial_size,
message_modulus: self.message_modulus,
carry_modulus: self.carry_modulus,
ciphertext_modulus: self.ciphertext_modulus,
bodies_count: self.bodies_count,
storage_log_modulus: self.storage_log_modulus,
lwe_per_glwe: self.lwe_per_glwe,
initial_len: self.initial_len,
}
}
}
impl Clone for CudaPackedGlweCiphertextList {
fn clone(&self) -> Self {
Self {
data: self.data.clone(),
glwe_dimension: self.glwe_dimension,
polynomial_size: self.polynomial_size,
message_modulus: self.message_modulus,
carry_modulus: self.carry_modulus,
ciphertext_modulus: self.ciphertext_modulus,
bodies_count: self.bodies_count,
storage_log_modulus: self.storage_log_modulus,
lwe_per_glwe: self.lwe_per_glwe,
@@ -120,12 +153,12 @@ impl CudaCompressionKey {
&self,
ciphertexts: &[CudaRadixCiphertext],
streams: &CudaStreams,
) -> CudaPackedGlweCiphertext {
) -> CudaPackedGlweCiphertextList {
let lwe_pksk = &self.packing_key_switching_key;
let ciphertext_modulus = lwe_pksk.ciphertext_modulus();
let compress_polynomial_size = lwe_pksk.output_polynomial_size();
let compress_glwe_size = lwe_pksk.output_glwe_size();
let compressed_polynomial_size = lwe_pksk.output_polynomial_size();
let compressed_glwe_size = lwe_pksk.output_glwe_size();
let first_ct = ciphertexts.first().unwrap();
let first_ct_info = first_ct.info.blocks.first().unwrap();
@@ -140,27 +173,28 @@ impl CudaCompressionKey {
.sum();
let num_glwes = num_lwes.div_ceil(self.lwe_per_glwe.0);
let mut output_glwe = CudaGlweCiphertextList::new(
compress_glwe_size.to_glwe_dimension(),
compress_polynomial_size,
GlweCiphertextCount(num_glwes),
ciphertext_modulus,
streams,
);
let glwe_ciphertext_size =
glwe_ciphertext_size(compressed_glwe_size, compressed_polynomial_size);
// The number of u64 (both mask and bodies)
// FIXME: have a more precise memory handling, this is too long and should be
// num_glwes * glwe_mask_size + num_lwes
let uncompressed_len = num_glwes * glwe_ciphertext_size;
let number_bits_to_pack = uncompressed_len * self.storage_log_modulus.0;
let compressed_len = number_bits_to_pack.div_ceil(u64::BITS as usize);
let mut packed_glwe_list = CudaVec::new(compressed_len, streams, 0);
unsafe {
let input_lwes = Self::flatten_async(ciphertexts, streams);
compress_integer_radix_async(
streams,
&mut output_glwe.0.d_vec,
&mut packed_glwe_list,
&input_lwes.0.d_vec,
&self.packing_key_switching_key.d_vec,
message_modulus,
carry_modulus,
compress_glwe_size.to_glwe_dimension(),
compress_polynomial_size,
compressed_glwe_size.to_glwe_dimension(),
compressed_polynomial_size,
lwe_dimension,
lwe_pksk.decomposition_base_log(),
lwe_pksk.decomposition_level_count(),
@@ -172,17 +206,17 @@ impl CudaCompressionKey {
streams.synchronize();
};
let initial_len =
compress_glwe_size.to_glwe_dimension().0 * compress_polynomial_size.0 + num_lwes;
CudaPackedGlweCiphertext {
glwe_ciphertext_list: output_glwe,
CudaPackedGlweCiphertextList {
data: packed_glwe_list,
glwe_dimension: compressed_glwe_size.to_glwe_dimension(),
polynomial_size: compressed_polynomial_size,
message_modulus,
carry_modulus,
ciphertext_modulus,
bodies_count: num_lwes,
storage_log_modulus: self.storage_log_modulus,
lwe_per_glwe: LweCiphertextCount(compress_polynomial_size.0),
initial_len,
lwe_per_glwe: LweCiphertextCount(compressed_polynomial_size.0),
initial_len: uncompressed_len,
}
}
}
@@ -190,7 +224,7 @@ impl CudaCompressionKey {
impl CudaDecompressionKey {
pub fn unpack(
&self,
packed_list: &CudaPackedGlweCiphertext,
packed_list: &CudaPackedGlweCiphertextList,
kind: DataKind,
start_block_index: usize,
end_block_index: usize,
@@ -218,9 +252,8 @@ impl CudaDecompressionKey {
let encryption_glwe_dimension = self.glwe_dimension;
let encryption_polynomial_size = self.polynomial_size;
let glwe_ciphertext_list = &packed_list.glwe_ciphertext_list;
let compression_glwe_dimension = glwe_ciphertext_list.glwe_dimension();
let compression_polynomial_size = glwe_ciphertext_list.polynomial_size();
let compression_glwe_dimension = packed_list.glwe_dimension;
let compression_polynomial_size = packed_list.polynomial_size;
let indexes_array_len = LweCiphertextCount(indexes_array.len());
let message_modulus = self.message_modulus;
@@ -243,7 +276,7 @@ impl CudaDecompressionKey {
decompress_integer_radix_async(
streams,
&mut output_lwe.0.d_vec,
&glwe_ciphertext_list.0.d_vec,
&packed_list.data,
&bsk.d_vec,
packed_list.bodies_count as u32,
message_modulus,
@@ -265,7 +298,7 @@ impl CudaDecompressionKey {
let degree = match kind {
DataKind::Unsigned(_) | DataKind::Signed(_) => {
Degree::new(message_modulus.0 * carry_modulus.0 - 1)
Degree::new(message_modulus.0 - 1)
}
DataKind::Boolean => Degree::new(1),
};

View File

@@ -61,7 +61,13 @@ impl ServerKey {
T: IntegerRadixCiphertext,
{
if d_range.is_empty() {
return ct.clone();
let mut result = ct.clone();
result
.blocks_mut()
.par_iter_mut()
.filter(|b| b.noise_level > NoiseLevel::NOMINAL)
.for_each(|block| self.key.message_extract_assign(block));
return result;
}
assert!(

View File

@@ -370,22 +370,61 @@ impl ServerKey {
where
T: IntegerRadixCiphertext,
{
if amount.blocks.is_empty() || ct.blocks().is_empty() {
return ct.clone();
}
let message_bits_per_block = self.key.message_modulus.0.ilog2() as u64;
let carry_bits_per_block = self.key.carry_modulus.0.ilog2() as u64;
assert!(carry_bits_per_block >= message_bits_per_block);
// Extracts bits and put them in the bit index 2 (=> bit number 3)
// so that it is already aligned to the correct position of the cmux input,
// and we reduce noise growth
let mut shift_bit_extractor = BitExtractor::with_final_offset(
&amount.blocks,
self,
message_bits_per_block as usize,
message_bits_per_block as usize,
);
assert!(message_bits_per_block.is_power_of_two());
if ct.blocks().len() == 1 {
let lut = self
.key
.generate_lookup_table_bivariate(|input, first_shift_block| {
let shift_within_block = first_shift_block % message_bits_per_block;
match operation {
BarrelShifterOperation::LeftShift => {
(input << shift_within_block) % self.message_modulus().0
}
BarrelShifterOperation::LeftRotate => {
let shifted = (input << shift_within_block) % self.message_modulus().0;
let wrapped = input >> (message_bits_per_block - shift_within_block);
shifted | wrapped
}
BarrelShifterOperation::RightRotate => {
let shifted = input >> shift_within_block;
let wrapped = (input << (message_bits_per_block - shift_within_block))
% self.message_modulus().0;
wrapped | shifted
}
BarrelShifterOperation::RightShift => {
if T::IS_SIGNED {
let sign_bit_pos = message_bits_per_block - 1;
let sign_bit = (input >> sign_bit_pos) & 1;
let padding_block = (self.message_modulus().0 - 1) * sign_bit;
// Pad with sign bits to 'simulate' an arithmetic shift
let input = (padding_block << message_bits_per_block) | input;
(input >> shift_within_block) % self.message_modulus().0
} else {
input >> shift_within_block
}
}
}
});
let block = self.key.unchecked_apply_lookup_table_bivariate(
&ct.blocks()[0],
&amount.blocks[0],
&lut,
);
return T::from_blocks(vec![block]);
}
let message_for_block =
self.key
.generate_lookup_table_bivariate(|input, first_shift_block| {
@@ -408,6 +447,45 @@ impl ServerKey {
b
}
});
// When doing right shift of a signed ciphertext, we do an arithmetic shift
// Thus, we need some special luts to be used on the last block
// (which has the sign bit)
let message_for_block_right_shift_signed =
if T::IS_SIGNED && operation == BarrelShifterOperation::RightShift {
let lut = self
.key
.generate_lookup_table_bivariate(|input, first_shift_block| {
let shift_within_block = first_shift_block % message_bits_per_block;
let shift_to_next_block = (first_shift_block / message_bits_per_block) % 2;
let sign_bit_pos = message_bits_per_block - 1;
let sign_bit = (input >> sign_bit_pos) & 1;
let padding_block = (self.message_modulus().0 - 1) * sign_bit;
if shift_to_next_block == 1 {
padding_block
} else {
// Pad with sign bits to 'simulate' an arithmetic shift
let input = (padding_block << message_bits_per_block) | input;
(input >> shift_within_block) % self.message_modulus().0
}
});
Some(lut)
} else {
None
};
// Extracts bits and put them in the bit index 2 (=> bit number 3)
// so that it is already aligned to the correct position of the cmux input,
// and we reduce noise growth
let mut shift_bit_extractor = BitExtractor::with_final_offset(
&amount.blocks,
self,
message_bits_per_block as usize,
message_bits_per_block as usize,
);
let message_for_next_block =
self.key
.generate_lookup_table_bivariate(|previous, first_shift_block| {
@@ -467,34 +545,6 @@ impl ServerKey {
}
});
// When doing right shift of a signed ciphertext, we do an arithmetic shift
// Thus, we need some special luts to be used on the last block
// (which has the sign big)
let message_for_block_right_shift_signed =
if T::IS_SIGNED && operation == BarrelShifterOperation::RightShift {
let lut = self
.key
.generate_lookup_table_bivariate(|input, first_shift_block| {
let shift_within_block = first_shift_block % message_bits_per_block;
let shift_to_next_block = (first_shift_block / message_bits_per_block) % 2;
let sign_bit_pos = message_bits_per_block - 1;
let sign_bit = (input >> sign_bit_pos) & 1;
let padding_block = (self.message_modulus().0 - 1) * sign_bit;
if shift_to_next_block == 1 {
padding_block
} else {
// Pad with sign bits to 'simulate' an arithmetic shift
let input = (padding_block << message_bits_per_block) | input;
(input >> shift_within_block) % self.message_modulus().0
}
});
Some(lut)
} else {
None
};
let message_for_next_block_right_shift_signed = if T::IS_SIGNED
&& operation == BarrelShifterOperation::RightShift
{
@@ -693,7 +743,8 @@ impl ServerKey {
) where
T: IntegerRadixCiphertext,
{
let num_blocks = shift.blocks.len();
// What matters is the len of the ct to shift, not the `shift` len
let num_blocks = ct.blocks().len();
let message_bits_per_block = self.key.message_modulus.0.ilog2() as u64;
let carry_bits_per_block = self.key.carry_modulus.0.ilog2() as u64;
let total_nb_bits = message_bits_per_block * num_blocks as u64;

View File

@@ -7,7 +7,7 @@ use crate::integer::block_decomposition::BlockDecomposer;
use crate::integer::ciphertext::boolean_value::BooleanBlock;
use crate::integer::keycache::KEY_CACHE;
use crate::integer::{
IntegerKeyKind, IntegerRadixCiphertext, RadixCiphertext, RadixClientKey, ServerKey,
ClientKey, IntegerKeyKind, IntegerRadixCiphertext, RadixCiphertext, RadixClientKey, ServerKey,
};
use crate::shortint::parameters::*;
use rand::Rng;
@@ -23,14 +23,14 @@ pub(crate) const NB_CTXT: usize = 2;
pub(crate) trait FunctionExecutor<TestInput, TestOutput> {
/// Setups the executor
///
/// Implementors are expected to be fully functional after this
/// Implementers are expected to be fully functional after this
/// function has been called.
fn setup(&mut self, cks: &RadixClientKey, sks: Arc<ServerKey>);
/// Executes the function
///
/// The function receives some inputs and return some output.
/// Implementors may have to do more than just calling the function
/// Implementers may have to do more than just calling the function
/// that is being tested (for example input/output may need to be converted)
///
/// Look at the test case function to know what are the expected inputs and outputs.
@@ -418,55 +418,77 @@ where
T: for<'a> FunctionExecutor<(&'a RadixCiphertext, &'a RadixCiphertext), RadixCiphertext>,
{
let param = param.into();
let nb_tests = nb_tests_for_params(param);
let nb_tests = nb_tests_smaller_for_params(param);
let (cks, sks) = KEY_CACHE.get_from_params(param, IntegerKeyKind::Radix);
let sks = Arc::new(sks);
let cks = RadixClientKey::from((cks, NB_CTXT));
let mut rng = rand::thread_rng();
// message_modulus^vec_length
let modulus = cks.parameters().message_modulus().0.pow(NB_CTXT as u32);
assert!(modulus.is_power_of_two());
let nb_bits = modulus.ilog2();
executor.setup(&cks, sks);
for _ in 0..nb_tests {
let clear = rng.gen::<u64>() % modulus;
let clear_shift = rng.gen::<u32>();
let cks: ClientKey = cks.into();
let ct = cks.encrypt(clear);
for num_blocks in 1..MAX_NB_CTXT {
// message_modulus^vec_length
let modulus = cks.parameters().message_modulus().0.pow(num_blocks as u32);
assert!(modulus.is_power_of_two());
let nb_bits = modulus.ilog2();
for _ in 0..nb_tests {
let clear = rng.gen::<u64>() % modulus;
let clear_shift = rng.gen::<u32>();
// case when 0 <= shift < nb_bits
{
let clear_shift = clear_shift % nb_bits;
let shift = cks.encrypt(clear_shift as u64);
let ct = cks.encrypt_radix(clear, num_blocks);
let encrypted_result = executor.execute((&ct, &shift));
let decrypted_result: u64 = cks.decrypt(&encrypted_result);
assert_eq!((clear << clear_shift) % modulus, decrypted_result);
}
// case when 0 <= shift < nb_bits
{
let clear_shift = clear_shift % nb_bits;
let shift = cks.encrypt_radix(clear_shift as u64, num_blocks);
// case when shift >= nb_bits
{
let clear_shift = clear_shift.saturating_add(nb_bits);
let shift = cks.encrypt(clear_shift as u64);
let encrypted_result = executor.execute((&ct, &shift));
let decrypted_result: u64 = cks.decrypt(&encrypted_result);
// When nb_bits is not a power of two
// then the behaviour is not the same
let mut nb_bits = modulus.ilog2();
if !nb_bits.is_power_of_two() {
nb_bits = nb_bits.next_power_of_two();
let encrypted_result = executor.execute((&ct, &shift));
for (i, b) in encrypted_result.blocks.iter().enumerate() {
if b.noise_level > NoiseLevel::NOMINAL {
println!("{i}: {:?}", b.noise_level);
}
}
assert!(
encrypted_result
.blocks
.iter()
.all(|b| b.noise_level <= NoiseLevel::NOMINAL),
"Expected all blocks to have at most NOMINAL noise level"
);
let decrypted_result: u64 = cks.decrypt_radix(&encrypted_result);
assert_eq!((clear << clear_shift) % modulus, decrypted_result);
}
// case when shift >= nb_bits
{
let clear_shift = rng.gen_range(nb_bits..modulus as u32);
let shift = cks.encrypt_radix(clear_shift as u64, num_blocks);
let encrypted_result = executor.execute((&ct, &shift));
assert!(
encrypted_result
.blocks
.iter()
.all(|b| b.noise_level <= NoiseLevel::NOMINAL),
"Expected all blocks to have at most NOMINAL noise level"
);
let decrypted_result: u64 = cks.decrypt_radix(&encrypted_result);
// When nb_bits is not a power of two
// then the behaviour is not the same
let mut nb_bits = modulus.ilog2();
if !nb_bits.is_power_of_two() {
nb_bits = nb_bits.next_power_of_two();
}
// We mimic wrapping_shl manually as we use a bigger type
// than the nb_bits we actually simulate in this test
assert_eq!(
(clear << (clear_shift % nb_bits)) % modulus,
decrypted_result
);
}
// We mimic wrapping_shl manually as we use a bigger type
// than the nb_bits we actually simulate in this test
assert_eq!(
(clear << (clear_shift % nb_bits)) % modulus,
decrypted_result
);
}
}
}
@@ -477,54 +499,71 @@ where
T: for<'a> FunctionExecutor<(&'a RadixCiphertext, &'a RadixCiphertext), RadixCiphertext>,
{
let param = param.into();
let nb_tests = nb_tests_for_params(param);
let nb_tests = nb_tests_smaller_for_params(param);
let (cks, sks) = KEY_CACHE.get_from_params(param, IntegerKeyKind::Radix);
let sks = Arc::new(sks);
let cks = RadixClientKey::from((cks, NB_CTXT));
let mut rng = rand::thread_rng();
// message_modulus^vec_length
let modulus = cks.parameters().message_modulus().0.pow(NB_CTXT as u32);
assert!(modulus.is_power_of_two());
let nb_bits = modulus.ilog2();
executor.setup(&cks, sks);
for _ in 0..nb_tests {
let clear = rng.gen::<u64>() % modulus;
let clear_shift = rng.gen::<u32>();
let cks: ClientKey = cks.into();
let ct = cks.encrypt(clear);
for num_blocks in 1..MAX_NB_CTXT {
// message_modulus^vec_length
let modulus = cks.parameters().message_modulus().0.pow(num_blocks as u32);
assert!(modulus.is_power_of_two());
let nb_bits = modulus.ilog2();
for _ in 0..nb_tests {
let clear = rng.gen::<u64>() % modulus;
let clear_shift = rng.gen::<u32>();
// case when 0 <= shift < nb_bits
{
let clear_shift = clear_shift % nb_bits;
let shift = cks.encrypt(clear_shift as u64);
let encrypted_result = executor.execute((&ct, &shift));
let decrypted_result: u64 = cks.decrypt(&encrypted_result);
assert_eq!((clear >> clear_shift) % modulus, decrypted_result);
}
let ct = cks.encrypt_radix(clear, num_blocks);
// case when shift >= nb_bits
{
let clear_shift = clear_shift.saturating_add(nb_bits);
let shift = cks.encrypt(clear_shift as u64);
let encrypted_result = executor.execute((&ct, &shift));
let decrypted_result: u64 = cks.decrypt(&encrypted_result);
// When nb_bits is not a power of two
// then the behaviour is not the same
let mut nb_bits = modulus.ilog2();
if !nb_bits.is_power_of_two() {
nb_bits = nb_bits.next_power_of_two();
// case when 0 <= shift < nb_bits
{
let clear_shift = clear_shift % nb_bits;
let shift = cks.encrypt_radix(clear_shift as u64, num_blocks);
let encrypted_result = executor.execute((&ct, &shift));
assert!(
encrypted_result
.blocks
.iter()
.all(|b| b.noise_level <= NoiseLevel::NOMINAL),
"Expected all blocks to have at most NOMINAL noise level"
);
let decrypted_result: u64 = cks.decrypt_radix(&encrypted_result);
assert_eq!((clear >> clear_shift) % modulus, decrypted_result);
}
// case when shift >= nb_bits
{
let clear_shift = rng.gen_range(nb_bits..modulus as u32);
let shift = cks.encrypt_radix(clear_shift as u64, num_blocks);
let encrypted_result = executor.execute((&ct, &shift));
assert!(
encrypted_result
.blocks
.iter()
.all(|b| b.noise_level <= NoiseLevel::NOMINAL),
"Expected all blocks to have at most NOMINAL noise level"
);
let decrypted_result: u64 = cks.decrypt_radix(&encrypted_result);
// When nb_bits is not a power of two
// then the behaviour is not the same
let mut nb_bits = modulus.ilog2();
if !nb_bits.is_power_of_two() {
nb_bits = nb_bits.next_power_of_two();
}
// We mimic wrapping_shr manually as we use a bigger type
// than the nb_bits we actually simulate in this test
assert_eq!(
(clear >> (clear_shift % nb_bits)) % modulus,
decrypted_result
);
}
// We mimic wrapping_shr manually as we use a bigger type
// than the nb_bits we actually simulate in this test
assert_eq!(
(clear >> (clear_shift % nb_bits)) % modulus,
decrypted_result
);
}
}
}
@@ -535,51 +574,68 @@ where
T: for<'a> FunctionExecutor<(&'a RadixCiphertext, &'a RadixCiphertext), RadixCiphertext>,
{
let param = param.into();
let nb_tests = nb_tests_for_params(param);
let nb_tests = nb_tests_smaller_for_params(param);
let (cks, sks) = KEY_CACHE.get_from_params(param, IntegerKeyKind::Radix);
let sks = Arc::new(sks);
let cks = RadixClientKey::from((cks, NB_CTXT));
let mut rng = rand::thread_rng();
// message_modulus^vec_length
let modulus = cks.parameters().message_modulus().0.pow(NB_CTXT as u32);
assert!(modulus.is_power_of_two());
let nb_bits = modulus.ilog2();
executor.setup(&cks, sks);
for _ in 0..nb_tests {
let clear = rng.gen::<u64>() % modulus;
let clear_shift = rng.gen::<u32>();
let cks: ClientKey = cks.into();
let ct = cks.encrypt(clear);
for num_blocks in 1..MAX_NB_CTXT {
// message_modulus^vec_length
let modulus = cks.parameters().message_modulus().0.pow(num_blocks as u32);
assert!(modulus.is_power_of_two());
let nb_bits = modulus.ilog2();
for _ in 0..nb_tests {
let clear = rng.gen::<u64>() % modulus;
let clear_shift = rng.gen::<u32>();
// case when 0 <= rotate < nb_bits
{
let clear_shift = clear_shift % nb_bits;
let shift = cks.encrypt(clear_shift as u64);
let encrypted_result = executor.execute((&ct, &shift));
let decrypted_result: u64 = cks.decrypt(&encrypted_result);
let expected = rotate_left_helper(clear, clear_shift, nb_bits);
assert_eq!(expected, decrypted_result);
}
let ct = cks.encrypt_radix(clear, num_blocks);
// case when shift >= nb_bits
{
let clear_shift = clear_shift.saturating_add(nb_bits);
let shift = cks.encrypt(clear_shift as u64);
let encrypted_result = executor.execute((&ct, &shift));
let decrypted_result: u64 = cks.decrypt(&encrypted_result);
// When nb_bits is not a power of two
// then the behaviour is not the same
let true_nb_bits = nb_bits;
let mut nb_bits = nb_bits;
if !nb_bits.is_power_of_two() {
nb_bits = nb_bits.next_power_of_two();
// case when 0 <= rotate < nb_bits
{
let clear_shift = clear_shift % nb_bits;
let shift = cks.encrypt_radix(clear_shift as u64, num_blocks);
let encrypted_result = executor.execute((&ct, &shift));
let decrypted_result: u64 = cks.decrypt_radix(&encrypted_result);
assert!(
encrypted_result
.blocks
.iter()
.all(|b| b.noise_level <= NoiseLevel::NOMINAL),
"Expected all blocks to have at most NOMINAL noise level"
);
let expected = rotate_left_helper(clear, clear_shift, nb_bits);
assert_eq!(expected, decrypted_result);
}
// case when shift >= nb_bits
{
let clear_shift = rng.gen_range(nb_bits..modulus as u32);
let shift = cks.encrypt_radix(clear_shift as u64, num_blocks);
let encrypted_result = executor.execute((&ct, &shift));
assert!(
encrypted_result
.blocks
.iter()
.all(|b| b.noise_level <= NoiseLevel::NOMINAL),
"Expected all blocks to have at most NOMINAL noise level"
);
let decrypted_result: u64 = cks.decrypt_radix(&encrypted_result);
// When nb_bits is not a power of two
// then the behaviour is not the same
let true_nb_bits = nb_bits;
let mut nb_bits = nb_bits;
if !nb_bits.is_power_of_two() {
nb_bits = nb_bits.next_power_of_two();
}
let expected = rotate_left_helper(clear, clear_shift % nb_bits, true_nb_bits);
assert_eq!(expected, decrypted_result);
}
let expected = rotate_left_helper(clear, clear_shift % nb_bits, true_nb_bits);
assert_eq!(expected, decrypted_result);
}
}
}
@@ -590,51 +646,68 @@ where
T: for<'a> FunctionExecutor<(&'a RadixCiphertext, &'a RadixCiphertext), RadixCiphertext>,
{
let param = param.into();
let nb_tests = nb_tests_for_params(param);
let nb_tests = nb_tests_smaller_for_params(param);
let (cks, sks) = KEY_CACHE.get_from_params(param, IntegerKeyKind::Radix);
let sks = Arc::new(sks);
let cks = RadixClientKey::from((cks, NB_CTXT));
let mut rng = rand::thread_rng();
// message_modulus^vec_length
let modulus = cks.parameters().message_modulus().0.pow(NB_CTXT as u32);
assert!(modulus.is_power_of_two());
let nb_bits = modulus.ilog2();
executor.setup(&cks, sks);
for _ in 0..nb_tests {
let clear = rng.gen::<u64>() % modulus;
let clear_shift = rng.gen::<u32>();
let cks: ClientKey = cks.into();
let ct = cks.encrypt(clear);
for num_blocks in 1..MAX_NB_CTXT {
// message_modulus^vec_length
let modulus = cks.parameters().message_modulus().0.pow(num_blocks as u32);
assert!(modulus.is_power_of_two());
let nb_bits = modulus.ilog2();
for _ in 0..nb_tests {
let clear = rng.gen::<u64>() % modulus;
let clear_shift = rng.gen::<u32>();
// case when 0 <= rotate < nb_bits
{
let clear_shift = clear_shift % nb_bits;
let shift = cks.encrypt(clear_shift as u64);
let encrypted_result = executor.execute((&ct, &shift));
let decrypted_result: u64 = cks.decrypt(&encrypted_result);
let expected = rotate_right_helper(clear, clear_shift, nb_bits);
assert_eq!(expected, decrypted_result);
}
let ct = cks.encrypt_radix(clear, num_blocks);
// case when shift >= nb_bits
{
let clear_shift = clear_shift.saturating_add(nb_bits);
let shift = cks.encrypt(clear_shift as u64);
let encrypted_result = executor.execute((&ct, &shift));
let decrypted_result: u64 = cks.decrypt(&encrypted_result);
// When nb_bits is not a power of two
// then the behaviour is not the same
let true_nb_bits = nb_bits;
let mut nb_bits = nb_bits;
if !nb_bits.is_power_of_two() {
nb_bits = nb_bits.next_power_of_two();
// case when 0 <= rotate < nb_bits
{
let clear_shift = clear_shift % nb_bits;
let shift = cks.encrypt_radix(clear_shift as u64, num_blocks);
let encrypted_result = executor.execute((&ct, &shift));
assert!(
encrypted_result
.blocks
.iter()
.all(|b| b.noise_level <= NoiseLevel::NOMINAL),
"Expected all blocks to have at most NOMINAL noise level"
);
let decrypted_result: u64 = cks.decrypt_radix(&encrypted_result);
let expected = rotate_right_helper(clear, clear_shift, nb_bits);
assert_eq!(expected, decrypted_result);
}
// case when shift >= nb_bits
{
let clear_shift = rng.gen_range(nb_bits..modulus as u32);
let shift = cks.encrypt_radix(clear_shift as u64, num_blocks);
let encrypted_result = executor.execute((&ct, &shift));
assert!(
encrypted_result
.blocks
.iter()
.all(|b| b.noise_level <= NoiseLevel::NOMINAL),
"Expected all blocks to have at most NOMINAL noise level"
);
let decrypted_result: u64 = cks.decrypt_radix(&encrypted_result);
// When nb_bits is not a power of two
// then the behaviour is not the same
let true_nb_bits = nb_bits;
let mut nb_bits = nb_bits;
if !nb_bits.is_power_of_two() {
nb_bits = nb_bits.next_power_of_two();
}
let expected = rotate_right_helper(clear, clear_shift % nb_bits, true_nb_bits);
assert_eq!(expected, decrypted_result);
}
let expected = rotate_right_helper(clear, clear_shift % nb_bits, true_nb_bits);
assert_eq!(expected, decrypted_result);
}
}
}

View File

@@ -120,7 +120,8 @@ mod js_on_wasm_api;
feature = "shortint",
feature = "boolean",
feature = "integer",
feature = "zk-pok"
feature = "zk-pok",
feature = "strings"
))]
mod test_user_docs;

View File

@@ -1,3 +1,7 @@
pub trait Named {
/// Default name for the type
const NAME: &'static str;
/// Aliases that should also be accepted for backward compatibility when checking the name of
/// values of this type
const BACKWARD_COMPATIBILITY_ALIASES: &'static [&'static str] = &[];
}

View File

@@ -122,7 +122,11 @@ Please use the versioned serialization mode for backward compatibility.",
}
}
if self.name != T::NAME {
if self.name != T::NAME
&& T::BACKWARD_COMPATIBILITY_ALIASES
.iter()
.all(|alias| self.name != *alias)
{
return Err(format!(
"On deserialization, expected type {}, got type {}",
T::NAME,
@@ -494,13 +498,17 @@ pub fn safe_deserialize_conformant<
#[cfg(all(test, feature = "shortint"))]
mod test_shortint {
use crate::safe_serialization::{DeserializationConfig, SerializationConfig};
use tfhe_versionable::Versionize;
use crate::named::Named;
use crate::shortint::parameters::{
PARAM_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M64,
V0_11_PARAM_MESSAGE_3_CARRY_3_KS_PBS_GAUSSIAN_2M64,
};
use crate::shortint::{gen_keys, Ciphertext};
use super::*;
#[test]
fn safe_deserialization_ct_unversioned() {
let (ck, _sk) = gen_keys(PARAM_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M64);
@@ -626,6 +634,46 @@ mod test_shortint {
let dec = ck.decrypt(&ct2);
assert_eq!(msg, dec);
}
#[test]
fn safe_deserialization_named() {
#[derive(Serialize, Deserialize, Versionize)]
#[repr(transparent)]
struct Foo(u64);
impl Named for Foo {
const NAME: &'static str = "Foo";
}
#[derive(Deserialize, Versionize)]
#[repr(transparent)]
struct Bar(u64);
impl Named for Bar {
const NAME: &'static str = "Bar";
const BACKWARD_COMPATIBILITY_ALIASES: &'static [&'static str] = &["Foo"];
}
#[derive(Deserialize, Versionize)]
#[repr(transparent)]
struct Baz(u64);
impl Named for Baz {
const NAME: &'static str = "Baz";
}
let foo = Foo(3);
let mut foo_ser = Vec::new();
safe_serialize(&foo, &mut foo_ser, 0x1000).unwrap();
let foo_deser: Foo = safe_deserialize(foo_ser.as_slice(), 0x1000).unwrap();
let bar_deser: Bar = safe_deserialize(foo_ser.as_slice(), 0x1000).unwrap();
assert_eq!(foo_deser.0, bar_deser.0);
assert!(safe_deserialize::<Baz>(foo_ser.as_slice(), 0x1000).is_err());
}
}
#[cfg(all(test, feature = "integer"))]

View File

@@ -19,6 +19,17 @@ pub struct CompressedCiphertextList {
pub count: CiphertextCount,
}
impl CompressedCiphertextList {
/// Returns how many u64 are needed to store the packed elements
#[cfg(all(test, feature = "gpu"))]
pub(crate) fn flat_len(&self) -> usize {
self.modulus_switched_glwe_ciphertext_list
.iter()
.map(|glwe| glwe.packed_integers.packed_coeffs.len())
.sum()
}
}
impl ParameterSetConformant for CompressedCiphertextList {
type ParameterSet = CompressedCiphertextConformanceParams;

View File

@@ -58,7 +58,8 @@ impl CompressionKey {
for ct in ct_list {
assert!(
ct.noise_level() == NoiseLevel::NOMINAL,
ct.noise_level() == NoiseLevel::NOMINAL
|| ct.noise_level() == NoiseLevel::ZERO,
"Ciphertexts must have a nominal (post PBS) noise to be compressed"
);

View File

@@ -57,6 +57,7 @@ mod test_cpu_doc {
doctest!("../docs/guides/pbs-stats.md", guides_pbs_stats);
doctest!("../docs/guides/public_key.md", guides_public_key);
doctest!("../docs/guides/rayon_crate.md", guides_rayon_crate);
doctest!("../docs/guides/strings.md", guides_strings);
doctest!("../docs/guides/trait_bounds.md", guides_trait_bounds);
doctest!(
"../docs/guides/trivial_ciphertext.md",

View File

@@ -196,6 +196,8 @@ pub enum CompactPkeCrs {
impl Named for CompactPkeCrs {
const NAME: &'static str = "zk::CompactPkeCrs";
const BACKWARD_COMPATIBILITY_ALIASES: &'static [&'static str] = &["zk::CompactPkePublicParams"];
}
impl From<ZkCompactPkeV1PublicParams> for CompactPkeCrs {