Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
ec320ae
Address clippy warnings
itzmeanjan Apr 12, 2025
0ee6adc
Add function for compressing each row of the matrix
itzmeanjan Apr 23, 2025
2c040a1
Add function for decompressing matrix - to be used only for testing i…
itzmeanjan Apr 23, 2025
56966f1
Correctly compute max. index of the source matrix
itzmeanjan Apr 23, 2025
b10a5b9
Take `num_cols` as parameter in row-wise decompression fn.
itzmeanjan Apr 23, 2025
48adfea
Add test ensuring row-wise matrix compression and decompression works…
itzmeanjan Apr 23, 2025
0b7804e
Multiply query (row) vector and row-wise compressed representation of…
itzmeanjan Apr 26, 2025
86820d0
Update server state to keep encoded database matrix in row-wise compr…
itzmeanjan Apr 26, 2025
8f625b9
Use new row-vector matrix multiplication routine during server-respond
itzmeanjan Apr 26, 2025
e636a1a
Add new error enum for row-wise compression function, if mat-elem-bit…
itzmeanjan Apr 28, 2025
59a6f9b
Update row-wise compression function to compress two elements into si…
itzmeanjan Apr 28, 2025
0aa8a30
Update row-vector x compressed-matrix multiplication algorithm to wor…
itzmeanjan Apr 28, 2025
d6ee441
Increase column limit in rustfmt file
itzmeanjan Apr 29, 2025
3b510d1
Make testing of row-wise compression/ decompression more robust
itzmeanjan Apr 29, 2025
a2ba314
Make row-vector x compressed-matrix multiplication faster
itzmeanjan Apr 29, 2025
eb765ae
Update server state as row-vector x compressed-matrix does not requir…
itzmeanjan Apr 29, 2025
1a65fe5
Reformat sources
itzmeanjan Apr 29, 2025
909a7a8
Increase range from which mat-element-bit-length is random sampled
itzmeanjan Apr 30, 2025
1313b7a
Add missing statement terminator
itzmeanjan Apr 30, 2025
64f2ae7
Extend the scope of testing of the full PIR protocol
itzmeanjan Apr 30, 2025
9a1cd86
When testing full PIR protocol do not query all the keys in the DB - …
itzmeanjan Apr 30, 2025
1c08063
Allow row-wise compression of matrix from factor 2 to 4 (both inclusi…
itzmeanjan May 1, 2025
5c5016a
Add tests for row-wise compression <-> decompression of matrix, with …
itzmeanjan May 1, 2025
cc98414
Add comments for parameter consts
itzmeanjan May 3, 2025
d9583c3
Reuse same consts in compression tests
itzmeanjan May 3, 2025
b852b6f
In end-to-end PIR test, change random test parameter sampling bounds
itzmeanjan May 3, 2025
465bbf7
Implement row-vector x compressed-transposed-matrix multiplication to…
itzmeanjan May 3, 2025
302a08b
Update server state to hold matrix-element-bit-length i.e. cipher-tex…
itzmeanjan May 3, 2025
74d9122
Update how random parameters are sampled for encoding/ recovery of KV…
itzmeanjan May 3, 2025
5eb16c1
Update test which ensures that a row-vector and a compressed-transpos…
itzmeanjan May 3, 2025
d236be1
Update benchmark results
itzmeanjan May 5, 2025
bd92608
Update main documentation of the project
itzmeanjan May 5, 2025
e0204d9
Prepare for v0.6.0 release
itzmeanjan May 5, 2025
2e09178
Bump dependency crates to their latest version
itzmeanjan May 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .rustfmt.toml
Original file line number Diff line number Diff line change
@@ -1 +1 @@
max_width = 160
max_width = 180
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "chalamet_pir"
version = "0.5.0"
version = "0.6.0"
edition = "2024"
resolver = "2"
rust-version = "1.85.0"
Expand All @@ -21,14 +21,14 @@ categories = ["cryptography", "data-structures", "concurrency"]
[dependencies]
turboshake = "=0.4.1"
rayon = "=1.10.0"
rand = "=0.9.0"
rand = "=0.9.1"
rand_chacha = "=0.9.0"
vulkano = { version = "=0.35.1", optional = true }
vulkano-shaders = { version = "=0.35.0", optional = true }

[dev-dependencies]
test-case = "=3.3.1"
divan = "=0.1.17"
divan = "=0.1.21"
unicode-xid = "=0.2.6"

[[bench]]
Expand Down
141 changes: 78 additions & 63 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,31 @@ ChalametPIR allows a client to retrieve a specific value from a key-value databa
The protocol has two participants:

**Server:**
* **`setup`:** Initializes the server with a key-value database, generating a public matrix, a hint matrix, and a Binary Fuse Filter (3-wise XOR or 4-wise XOR, configurable at compile time). It returns serialized representations of the hint matrix and filter parameters. This phase can be completed offline and is completely client-agnostic. But it is very compute-intensive, which is why this library allows you to offload expensive matrix multiplication and transposition to a GPU, gated behind the opt-in `gpu` feature. For large key-value databases (e.g., with >= $2^{18}$ entries), I recommend enabling the `gpu` feature, as it can significantly reduce the cost of the server-setup phase.
* **`respond`:** Processes a client's query and returns an encrypted response vector.
* **`setup`:** Initializes the server with a seed, a key-value database, generating a public matrix, a hint matrix, and a Binary Fuse Filter (3-wise XOR or 4-wise XOR, configurable at compile time). It returns serialized representations of the hint matrix and filter parameters. This phase can be completed offline and is completely client-agnostic. But it is very compute-intensive, which is why this library allows you to offload expensive matrix multiplication and transposition to a GPU, gated behind the opt-in `gpu` feature. For large key-value databases (e.g., with >= $2^{18}$ entries), I recommend enabling the `gpu` feature, as it can significantly reduce the cost of the server-setup phase.
* **`respond`:** Processes a client's encrypted query, returning an encrypted response vector.

**Client:**
* **`setup`:** Initializes the client using the serialized hint matrix and filter parameters received from the server.
* **`query`:** Generates a PIR query for a given key, which can be sent to server.
* **`setup`:** Initializes the client using the seed, serialized hint matrix and filter parameters received from the server.
* **`query`:** Generates an encrypted PIR query for a given key, which can be sent to server.
* **`process_response`:** Decrypts the server's response and extracts the requested value.

To paint a more practical picture, imagine, we have a database with $2^{20}$ (~1 million) keys s.t. each key is 32 -bytes and each value is 1024 -bytes (1kB). We are setting up both server and client(s), on each of
To paint a more practical picture, imagine, we have a database with $2^{20}$ (~1 million) keys s.t. each key is 32 -bytes and each value is 1024 -bytes (1kB).

**ChalametPIR Protocol Steps**

1) Server gets a 32 -bytes seed and the key-value database as input, returns a **6670248 -bytes (~6.36mB)** hint and **68 -bytes** Binary Fuse Filter parameters.
2) Client receives the seed, hint and Binary Fuse Filter parameters, sets itself up.
3) Client wants to privately look up a key in the server held key-value database, it generates an encrypted query of **4718600 -bytes (~4.5mB)**, when 3-wise XOR Binary Fuse Filter is used. If server decided to use a 4-wise XOR Binary Fuse Filter, query size would be **4521992 -bytes (~4.31mB)**. Client sends this encrypted query to server.
4) Server computes encrypted response of **3768 -bytes (~3.68kB)**, touching every single bit of the database.
5) Client receives the encrypted response and decrypts it.

We are setting up both server and client(s), on each of

Machine Type | Machine | Kernel | Compiler | Memory Read Speed
--- | --- | --- | --- | ---
aarch64 server | AWS EC2 `m8g.8xlarge` | `Linux 6.8.0-1021-aws aarch64` | `rustc 1.85.1 (e71f9a9a9 2025-01-27)` | 28.25 GB/s
x86_64 server | AWS EC2 `m7i.8xlarge` | `Linux 6.8.0-1021-aws x86_64` | `rustc 1.85.1 (e71f9a9a9 2025-01-27)` | 10.33 GB/s
(a) aarch64 server | AWS EC2 `m8g.8xlarge` | `Linux 6.8.0-1028-aws aarch64` | `rustc 1.86.0 (05f9846f8 2025-03-31)` | 28.25 GB/s
(b) x86_64 server | AWS EC2 `m7i.8xlarge` | `Linux 6.8.0-1028-aws x86_64` | `rustc 1.86.0 (05f9846f8 2025-03-31)` | 10.33 GB/s
(c) aarch64 server | AWS EC2 `r8g.8xlarge` | `Linux 6.8.0-1028-aws aarch64` | `rustc 1.86.0 (05f9846f8 2025-03-31)` | 28.25 GB/s

and this implementation of ChalametPIR is compiled with specified compiler, in `optimized` profile. See [Cargo.toml](./Cargo.toml).

Expand All @@ -36,15 +47,15 @@ and this implementation of ChalametPIR is compiled with specified compiler, in `

Step | `(a)` Time Taken on `aarch64` server | `(b)` Time Taken on `x86_64` server | Ratio `a / b`
:-- | --: | --: | --:
`server_setup` | 9.73 minutes | 22.11 minutes | 0.44
`client_setup` | 9.43 seconds | 10.47 seconds | 0.9
`client_query` | 309 milliseconds | 2.57 seconds | 0.12
`server_respond` | 18.01 milliseconds | 32.16 milliseconds | 0.56
`client_process_response` | 11.73 microseconds | 16.75 microseconds | 0.7
`server_setup` | 9.62 minutes | 21.37 minutes | 0.45
`client_setup` | 9.48 seconds | 8.31 seconds | 1.14
`client_query` | 323.5 milliseconds | 2.08 seconds | 0.16
`server_respond` | 10.06 milliseconds | 14.06 milliseconds | 0.72
`client_process_response` | 9.44 microseconds | 13.96 microseconds | 0.68

So, the median bandwidth of the `server_respond` algorithm, which needs to traverse through the whole processed database, is
- (a) For `aarch64` server: 53.82 GB/s
- (b) For `x86_64` server: 30.12 GB/s
- (a) For `aarch64` server: 102.51 GB/s
- (b) For `x86_64` server: 73.35 GB/s

For demonstrating the effectiveness of offloading parts of the server-setup phase to a GPU, I benchmark it on AWS EC2 instance `g6e.8xlarge`, which features a NVIDIA L40S Tensor Core GPU and $3^{rd}$ generation AMD EPYC CPUs.

Expand Down Expand Up @@ -106,9 +117,13 @@ cargo bench --features gpu --profile optimized --bench offline_phase -q server_s
> When benchmarking make sure you've disabled CPU frequency scaling, otherwise numbers you see can be misleading. I find https://github.com/google/benchmark/blob/b40db869/docs/reducing_variance.md helpful.

### On AWS EC2 Instance `m8g.8xlarge` (aarch64)
![offline-phase](./bench-results/offline.m8g.8xlarge.png)
---
![online-phase](./bench-results/online.m8g.8xlarge.png)
![chalamet-pir-on-aws-ec2-m8g.8xlarge](./bench-results/aws-ec2-m8g.8xlarge-chalamet-pir.png)

### On AWS EC2 Instance `m7i.8xlarge` (x86_64)
![chalamet-pir-on-aws-ec2-m7i.8xlarge](./bench-results/aws-ec2-m7i.8xlarge-chalamet-pir.png)

### On AWS EC2 Instance `r8g.8xlarge` (aarch64)
![chalamet-pir-on-aws-ec2-r8g.8xlarge](./bench-results/aws-ec2-r8g.8xlarge-chalamet-pir.png)

> [!NOTE]
> More about AWS EC2 instances @ https://aws.amazon.com/ec2/instance-types.
Expand All @@ -118,9 +133,9 @@ First, add this library crate as a dependency in your Cargo.toml file.

```toml
[dependencies]
chalamet_pir = "=0.5.0"
chalamet_pir = "=0.6.0"
# Or, if you want to offload server-setup to a GPU.
# chalamet_pir = { version = "=0.5.0", features = ["gpu"] }
# chalamet_pir = { version = "=0.6.0", features = ["gpu"] }
rand = "=0.9.0"
rand_chacha = "=0.9.0"
```
Expand Down Expand Up @@ -188,28 +203,28 @@ Seed size : 32.0B
Hint size : 207.9KB
Filter parameters size : 68.0B
Query size : 304.0KB
Response size : 144.0B

⚠️ Random key '115560' is not present in DB
✅ '29520' maps to 'L', in 417.284µs
⚠️ Random key '97022' is not present in DB
⚠️ Random key '79601' is not present in DB
✅ '57270' maps to 'מ', in 570.426µs
⚠️ Random key '95069' is not present in DB
⚠️ Random key '102703' is not present in DB
⚠️ Random key '113549' is not present in DB
✅ '2293' maps to 'T', in 647.202µs
⚠️ Random key '92678' is not present in DB
⚠️ Random key '90071' is not present in DB
✅ '61493' maps to 'f', in 552.899µs
✅ '41360' maps to 'c', in 533.403µs
⚠️ Random key '67047' is not present in DB
✅ '55793' maps to 'z', in 531.056µs
⚠️ Random key '72809' is not present in DB
✅ '32741' maps to 'P', in 672.91µs
✅ '29361' maps to 'T', in 530.348µs
✅ '33143' maps to 'W', in 355.938µs
⚠️ Random key '87591' is not present in DB
Response size : 128.0B

✅ '64187' maps to 'b', in 274.995µs
⚠️ Random key '112599' is not present in DB
⚠️ Random key '108662' is not present in DB
⚠️ Random key '79395' is not present in DB
⚠️ Random key '72638' is not present in DB
⚠️ Random key '123690' is not present in DB
⚠️ Random key '69344' is not present in DB
⚠️ Random key '69155' is not present in DB
✅ '5918' maps to 'J', in 165.606µs
⚠️ Random key '128484' is not present in DB
⚠️ Random key '79290' is not present in DB
⚠️ Random key '104015' is not present in DB
⚠️ Random key '111256' is not present in DB
⚠️ Random key '124342' is not present in DB
⚠️ Random key '74982' is not present in DB
⚠️ Random key '93082' is not present in DB
✅ '32800' maps to 'b', in 233.29µs
✅ '20236' maps to 'Q', in 233.531µs
✅ '47334' maps to 'p', in 223.548µs
✅ '12225' maps to 'U', in 209.217µs

# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---

Expand All @@ -223,26 +238,26 @@ Seed size : 32.0B
Hint size : 207.9KB
Filter parameters size : 68.0B
Query size : 292.0KB
Response size : 144.0B

✅ '9445' maps to 'x', in 416.617µs
⚠️ Random key '120774' is not present in DB
⚠️ Random key '81310' is not present in DB
✅ '29502' maps to 'S', in 292.054µs
✅ '58360' maps to 'c', in 237.823µs
⚠️ Random key '74424' is not present in DB
⚠️ Random key '96217' is not present in DB
✅ '60430' maps to 'X', in 380.674µs
✅ '47703' maps to 'X', in 252.425µs
✅ '13076' maps to 'V', in 312.977µs
✅ '53385' maps to 'o', in 255.729µs
⚠️ Random key '90470' is not present in DB
✅ '46869' maps to 'h', in 275.5µs
⚠️ Random key '127543' is not present in DB
⚠️ Random key '105528' is not present in DB
⚠️ Random key '76357' is not present in DB
✅ '56523' maps to 'a', in 254.195µs
✅ '11499' maps to 'K', in 286.938µs
✅ '44878' maps to 'J', in 258.759µs
⚠️ Random key '74422' is not present in DB
Response size : 128.0B

✅ '13239' maps to 'T', in 241.21µs
⚠️ Random key '112983' is not present in DB
⚠️ Random key '89821' is not present in DB
✅ '63385' maps to 'I', in 188.06µs
⚠️ Random key '123914' is not present in DB
⚠️ Random key '119919' is not present in DB
⚠️ Random key '72903' is not present in DB
⚠️ Random key '93634' is not present in DB
⚠️ Random key '68582' is not present in DB
✅ '55692' maps to 'n', in 359.112µs
⚠️ Random key '68191' is not present in DB
⚠️ Random key '92762' is not present in DB
✅ '997' maps to 'v', in 302.626µs
⚠️ Random key '123011' is not present in DB
✅ '37638' maps to 'F', in 240.428µs
⚠️ Random key '75802' is not present in DB
⚠️ Random key '80496' is not present in DB
✅ '42586' maps to 'T', in 224.29µs
✅ '25911' maps to 'u', in 250.494µs
✅ '15478' maps to 'S', in 257.656µs
```
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed bench-results/offline.m8g.8xlarge.png
Binary file not shown.
Binary file removed bench-results/online.m8g.8xlarge.png
Binary file not shown.
5 changes: 1 addition & 4 deletions examples/kw_pir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,7 @@ fn main() {
.iter()
.map(|(k, v)| (k.to_le_bytes(), v.encode_utf8(&mut [0u8; 4]).as_bytes().to_vec()))
.collect::<HashMap<[u8; 8], Vec<u8>>>();
let kv_db_as_ref = kv_db_as_bytes
.iter()
.map(|(k, v)| (k.as_slice(), v.as_slice()))
.collect::<HashMap<&[u8], &[u8]>>();
let kv_db_as_ref = kv_db_as_bytes.iter().map(|(k, v)| (k.as_slice(), v.as_slice())).collect::<HashMap<&[u8], &[u8]>>();

let key_byte_len = std::mem::size_of_val(kv_db.keys().next().unwrap());
let value_byte_len = std::mem::size_of_val(kv_db.values().next().unwrap());
Expand Down
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
//!
//! ```toml
//! [dependencies]
//! chalametpir = "=0.5.0"
//! chalametpir = "=0.6.0"
//! # Or, if you want to offload server-setup to GPU.
//! # chalamet_pir = { version = "=0.5.0", features = ["gpu"] }
//! # chalamet_pir = { version = "=0.6.0", features = ["gpu"] }
//! rand = "=0.9.0"
//! rand_chacha = "=0.9.0"
//! ```
Expand Down
6 changes: 1 addition & 5 deletions src/pir_internals/binary_fuse_filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -577,11 +577,7 @@ pub fn mix256(key: &[u64; 4], seed: &[u8; 32]) -> u64 {
};

key.iter()
.map(|&k| {
seed_words
.into_iter()
.fold(0u64, |acc, seed_word| murmur64(acc.wrapping_add(mix(k, seed_word))))
})
.map(|&k| seed_words.into_iter().fold(0u64, |acc, seed_word| murmur64(acc.wrapping_add(mix(k, seed_word)))))
.fold(0, |acc, r| acc.overflowing_add(r).0)
}

Expand Down
2 changes: 2 additions & 0 deletions src/pir_internals/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ pub enum ChalametPIRError {
ArithmeticOverflowAddingQueryIndicator,
UnsupportedArityForBinaryFuseFilter,
InvalidResponseVector,
ImpossibleEncodedDBMatrixElementBitLength,
}

impl Display for ChalametPIRError {
Expand Down Expand Up @@ -95,6 +96,7 @@ impl Display for ChalametPIRError {
}
Self::UnsupportedArityForBinaryFuseFilter => write!(f, "Binary Fuse Filter supports arity of either 3 or 4."),
Self::InvalidResponseVector => write!(f, "Unexpected dimension of the response vector."),
Self::ImpossibleEncodedDBMatrixElementBitLength => write!(f, "Encoded database matrix's element bit length mustn't ever exceed 16."),
}
}
}
Expand Down
18 changes: 6 additions & 12 deletions src/pir_internals/gpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,8 @@ pub fn mat_x_mat(
.map_err(|_| ChalametPIRError::VulkanDescriptorSetCreationFailed)?;

let command_buffer = {
let mut command_buffer_builder =
AutoCommandBufferBuilder::primary(command_buffer_allocator, queue.queue_family_index(), CommandBufferUsage::OneTimeSubmit)
.map_err(|_| ChalametPIRError::VulkanCommandBufferBuilderCreationFailed)?;
let mut command_buffer_builder = AutoCommandBufferBuilder::primary(command_buffer_allocator, queue.queue_family_index(), CommandBufferUsage::OneTimeSubmit)
.map_err(|_| ChalametPIRError::VulkanCommandBufferBuilderCreationFailed)?;

unsafe {
command_buffer_builder
Expand All @@ -209,9 +208,7 @@ pub fn mat_x_mat(
.map_err(|_| ChalametPIRError::VulkanCommandBufferRecordingFailed)?;
}

command_buffer_builder
.build()
.map_err(|_| ChalametPIRError::VulkanCommandBufferBuildingFailed)?
command_buffer_builder.build().map_err(|_| ChalametPIRError::VulkanCommandBufferBuildingFailed)?
};

command_buffer
Expand Down Expand Up @@ -259,9 +256,8 @@ pub fn mat_transpose(
.map_err(|_| ChalametPIRError::VulkanDescriptorSetCreationFailed)?;

let command_buffer = {
let mut command_buffer_builder =
AutoCommandBufferBuilder::primary(command_buffer_allocator, queue.queue_family_index(), CommandBufferUsage::OneTimeSubmit)
.map_err(|_| ChalametPIRError::VulkanCommandBufferBuilderCreationFailed)?;
let mut command_buffer_builder = AutoCommandBufferBuilder::primary(command_buffer_allocator, queue.queue_family_index(), CommandBufferUsage::OneTimeSubmit)
.map_err(|_| ChalametPIRError::VulkanCommandBufferBuilderCreationFailed)?;

unsafe {
command_buffer_builder
Expand All @@ -273,9 +269,7 @@ pub fn mat_transpose(
.map_err(|_| ChalametPIRError::VulkanCommandBufferRecordingFailed)?;
}

command_buffer_builder
.build()
.map_err(|_| ChalametPIRError::VulkanCommandBufferBuildingFailed)?
command_buffer_builder.build().map_err(|_| ChalametPIRError::VulkanCommandBufferBuildingFailed)?
};

command_buffer
Expand Down
Loading