diff --git a/.rustfmt.toml b/.rustfmt.toml index b28d8cf..cd016f6 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -1 +1 @@ -max_width = 160 +max_width = 180 diff --git a/Cargo.toml b/Cargo.toml index 4607ab4..3f14c29 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" @@ -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]] diff --git a/README.md b/README.md index caf3b41..db2f9d0 100644 --- a/README.md +++ b/README.md @@ -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). @@ -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. @@ -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. @@ -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" ``` @@ -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 # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- @@ -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 ``` diff --git a/bench-results/aws-ec2-m7i.8xlarge-chalamet-pir.png b/bench-results/aws-ec2-m7i.8xlarge-chalamet-pir.png new file mode 100644 index 0000000..7faee19 Binary files /dev/null and b/bench-results/aws-ec2-m7i.8xlarge-chalamet-pir.png differ diff --git a/bench-results/aws-ec2-m8g.8xlarge-chalamet-pir.png b/bench-results/aws-ec2-m8g.8xlarge-chalamet-pir.png new file mode 100644 index 0000000..25bad10 Binary files /dev/null and b/bench-results/aws-ec2-m8g.8xlarge-chalamet-pir.png differ diff --git a/bench-results/aws-ec2-r8g.8xlarge-chalamet-pir.png b/bench-results/aws-ec2-r8g.8xlarge-chalamet-pir.png new file mode 100644 index 0000000..195b465 Binary files /dev/null and b/bench-results/aws-ec2-r8g.8xlarge-chalamet-pir.png differ diff --git a/bench-results/offline.m8g.8xlarge.png b/bench-results/offline.m8g.8xlarge.png deleted file mode 100644 index ac01b40..0000000 Binary files a/bench-results/offline.m8g.8xlarge.png and /dev/null differ diff --git a/bench-results/online.m8g.8xlarge.png b/bench-results/online.m8g.8xlarge.png deleted file mode 100644 index 4d93329..0000000 Binary files a/bench-results/online.m8g.8xlarge.png and /dev/null differ diff --git a/examples/kw_pir.rs b/examples/kw_pir.rs index 8df1c6d..903b90d 100644 --- a/examples/kw_pir.rs +++ b/examples/kw_pir.rs @@ -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::>>(); - let kv_db_as_ref = kv_db_as_bytes - .iter() - .map(|(k, v)| (k.as_slice(), v.as_slice())) - .collect::>(); + let kv_db_as_ref = kv_db_as_bytes.iter().map(|(k, v)| (k.as_slice(), v.as_slice())).collect::>(); 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()); diff --git a/src/lib.rs b/src/lib.rs index 251e16f..9c57822 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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" //! ``` diff --git a/src/pir_internals/binary_fuse_filter.rs b/src/pir_internals/binary_fuse_filter.rs index da60e80..d6d7c75 100644 --- a/src/pir_internals/binary_fuse_filter.rs +++ b/src/pir_internals/binary_fuse_filter.rs @@ -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) } diff --git a/src/pir_internals/error.rs b/src/pir_internals/error.rs index 19cfbaa..92cbe42 100644 --- a/src/pir_internals/error.rs +++ b/src/pir_internals/error.rs @@ -46,6 +46,7 @@ pub enum ChalametPIRError { ArithmeticOverflowAddingQueryIndicator, UnsupportedArityForBinaryFuseFilter, InvalidResponseVector, + ImpossibleEncodedDBMatrixElementBitLength, } impl Display for ChalametPIRError { @@ -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."), } } } diff --git a/src/pir_internals/gpu.rs b/src/pir_internals/gpu.rs index d22abab..f0629f8 100644 --- a/src/pir_internals/gpu.rs +++ b/src/pir_internals/gpu.rs @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/src/pir_internals/matrix.rs b/src/pir_internals/matrix.rs index 4f78317..915b57b 100644 --- a/src/pir_internals/matrix.rs +++ b/src/pir_internals/matrix.rs @@ -1,6 +1,6 @@ use crate::pir_internals::{ binary_fuse_filter, branch_opt_util, - params::{HASHED_KEY_BYTE_LEN, SEED_BYTE_LEN}, + params::{HASHED_KEY_BYTE_LEN, MAX_CIPHER_TEXT_BIT_LEN, MIN_CIPHER_TEXT_BIT_LEN, SEED_BYTE_LEN}, serialization, }; use rand::prelude::*; @@ -35,7 +35,7 @@ impl Matrix { /// # Returns /// /// * `Result` - A new matrix if the input is valid (rows and cols are positive). - /// Returns an error if either rows or cols is zero. + /// Returns an error if either rows or cols is zero. pub fn new(rows: u32, cols: u32) -> Result { if branch_opt_util::likely((rows > 0) && (cols > 0)) { Ok(Matrix { @@ -59,7 +59,7 @@ impl Matrix { /// # Returns /// /// * `Result` - A new matrix if the input is valid (rows and cols are positive and the number of values matches the number of required elements). - /// Returns an error if either rows or cols is zero, or if the number of values does not match the number of required elements. + /// Returns an error if either rows or cols is zero, or if the number of values does not match the number of required elements. pub fn from_values(rows: u32, cols: u32, values: Vec) -> Result { if branch_opt_util::likely((rows > 0) && (cols > 0)) { if branch_opt_util::likely((rows * cols) as usize == values.len()) { @@ -89,32 +89,391 @@ impl Matrix { std::mem::size_of_val(&self.rows) + std::mem::size_of_val(&self.cols) + std::mem::size_of::() * (self.rows * self.cols) as usize } - /// Performs the multiplication of a row vector (1xN matrix) by the transpose of a matrix (MxN). + pub fn row_wise_compress(self, mat_elem_bit_len: usize) -> Result { + if branch_opt_util::unlikely(!(mat_elem_bit_len >= MIN_CIPHER_TEXT_BIT_LEN && mat_elem_bit_len <= MAX_CIPHER_TEXT_BIT_LEN)) { + return Err(ChalametPIRError::ImpossibleEncodedDBMatrixElementBitLength); + } + + match mat_elem_bit_len { + // Compression factor 2 + 11..=MAX_CIPHER_TEXT_BIT_LEN => { + const COMPRESSION_FACTOR: u32 = 2; + const BITS_PER_UNCOMPRESSED_ELEMENT: usize = (u32::BITS / COMPRESSION_FACTOR) as usize; + + let mat_elem_mask = (1u32 << mat_elem_bit_len) - 1; + + let res_num_rows = self.rows; + let res_num_cols = self.cols.div_ceil(COMPRESSION_FACTOR); + + let mut res = unsafe { Matrix::new(res_num_rows, res_num_cols).unwrap_unchecked() }; + + (0..res_num_rows as usize) + .flat_map(|ridx| (0..res_num_cols as usize).map(move |cidx| (ridx, cidx))) + .for_each(|(ridx, cidx)| { + let decompressed_elem_cidx = cidx * COMPRESSION_FACTOR as usize; + + let mut compressed_elem = self[(ridx, decompressed_elem_cidx)] & mat_elem_mask; + + if branch_opt_util::likely((decompressed_elem_cidx + 1) < self.cols as usize) { + compressed_elem |= (self[(ridx, decompressed_elem_cidx + 1)] & mat_elem_mask) << BITS_PER_UNCOMPRESSED_ELEMENT; + } + + res[(ridx, cidx)] = compressed_elem; + }); + + Ok(res) + } + // Compression factor 3 + 9..=10 => { + const COMPRESSION_FACTOR: u32 = 3; + const BITS_PER_UNCOMPRESSED_ELEMENT: usize = (u32::BITS / COMPRESSION_FACTOR) as usize; + + let mat_elem_mask = (1u32 << mat_elem_bit_len) - 1; + + let res_num_rows = self.rows; + let res_num_cols = self.cols.div_ceil(COMPRESSION_FACTOR); + + let mut res = unsafe { Matrix::new(res_num_rows, res_num_cols).unwrap_unchecked() }; + + (0..res_num_rows as usize) + .flat_map(|ridx| (0..res_num_cols as usize).map(move |cidx| (ridx, cidx))) + .for_each(|(ridx, cidx)| { + let decompressed_elem_cidx = cidx * COMPRESSION_FACTOR as usize; + + let mut compressed_elem = self[(ridx, decompressed_elem_cidx)] & mat_elem_mask; + + if branch_opt_util::likely((decompressed_elem_cidx + 1) < self.cols as usize) { + compressed_elem |= (self[(ridx, decompressed_elem_cidx + 1)] & mat_elem_mask) << BITS_PER_UNCOMPRESSED_ELEMENT; + } + + if branch_opt_util::likely((decompressed_elem_cidx + 2) < self.cols as usize) { + compressed_elem |= (self[(ridx, decompressed_elem_cidx + 2)] & mat_elem_mask) << (2 * BITS_PER_UNCOMPRESSED_ELEMENT); + } + + res[(ridx, cidx)] = compressed_elem; + }); + + Ok(res) + } + // Compression factor 4 + MIN_CIPHER_TEXT_BIT_LEN..=8 => { + const COMPRESSION_FACTOR: u32 = 4; + const BITS_PER_UNCOMPRESSED_ELEMENT: usize = (u32::BITS / COMPRESSION_FACTOR) as usize; + + let mat_elem_mask = (1u32 << mat_elem_bit_len) - 1; + + let res_num_rows = self.rows; + let res_num_cols = self.cols.div_ceil(COMPRESSION_FACTOR); + + let mut res = unsafe { Matrix::new(res_num_rows, res_num_cols).unwrap_unchecked() }; + + (0..res_num_rows as usize) + .flat_map(|ridx| (0..res_num_cols as usize).map(move |cidx| (ridx, cidx))) + .for_each(|(ridx, cidx)| { + let decompressed_elem_cidx = cidx * COMPRESSION_FACTOR as usize; + + let mut compressed_elem = self[(ridx, decompressed_elem_cidx)] & mat_elem_mask; + + if branch_opt_util::likely((decompressed_elem_cidx + 1) < self.cols as usize) { + compressed_elem |= (self[(ridx, decompressed_elem_cidx + 1)] & mat_elem_mask) << BITS_PER_UNCOMPRESSED_ELEMENT; + } + + if branch_opt_util::likely((decompressed_elem_cidx + 2) < self.cols as usize) { + compressed_elem |= (self[(ridx, decompressed_elem_cidx + 2)] & mat_elem_mask) << (2 * BITS_PER_UNCOMPRESSED_ELEMENT); + } + + if branch_opt_util::likely((decompressed_elem_cidx + 3) < self.cols as usize) { + compressed_elem |= (self[(ridx, decompressed_elem_cidx + 3)] & mat_elem_mask) << (3 * BITS_PER_UNCOMPRESSED_ELEMENT); + } + + res[(ridx, cidx)] = compressed_elem; + }); + + Ok(res) + } + _ => { + branch_opt_util::cold(); + panic!("Impossible cipher text bit length provided as input to the compression function !"); + } + } + } + + #[cfg(test)] + pub fn row_wise_decompress(self, mat_elem_bit_len: usize, num_cols: u32) -> Result { + if branch_opt_util::unlikely(!(mat_elem_bit_len >= MIN_CIPHER_TEXT_BIT_LEN && mat_elem_bit_len <= MAX_CIPHER_TEXT_BIT_LEN)) { + return Err(ChalametPIRError::ImpossibleEncodedDBMatrixElementBitLength); + } + + match mat_elem_bit_len { + // Compression factor 2 + 11..=MAX_CIPHER_TEXT_BIT_LEN => { + const COMPRESSION_FACTOR: u32 = 2; + const BITS_PER_UNCOMPRESSED_ELEMENT: usize = (u32::BITS / COMPRESSION_FACTOR) as usize; + + let mat_elem_mask = (1u32 << mat_elem_bit_len) - 1; + + assert_eq!(num_cols.div_ceil(COMPRESSION_FACTOR), self.cols); + + let res_num_rows = self.rows; + let res_num_cols = num_cols; + + let mut res = unsafe { Matrix::new(res_num_rows, res_num_cols).unwrap_unchecked() }; + + (0..self.rows as usize) + .flat_map(|src_ridx| (0..self.cols as usize).map(move |src_cidx| (src_ridx, src_cidx))) + .for_each(|(src_ridx, src_cidx)| { + let decompressed_elem_cidx = src_cidx * COMPRESSION_FACTOR as usize; + let decompressed_elem = self[(src_ridx, src_cidx)]; + + res[(src_ridx, decompressed_elem_cidx)] = decompressed_elem & mat_elem_mask; + + if branch_opt_util::likely((decompressed_elem_cidx + 1) < num_cols as usize) { + res[(src_ridx, decompressed_elem_cidx + 1)] = (decompressed_elem >> BITS_PER_UNCOMPRESSED_ELEMENT) & mat_elem_mask; + } + }); + + Ok(res) + } + // Compression factor 3 + 9..=10 => { + const COMPRESSION_FACTOR: u32 = 3; + const BITS_PER_UNCOMPRESSED_ELEMENT: usize = (u32::BITS / COMPRESSION_FACTOR) as usize; + + let mat_elem_mask = (1u32 << mat_elem_bit_len) - 1; + + assert_eq!(num_cols.div_ceil(COMPRESSION_FACTOR), self.cols); + + let res_num_rows = self.rows; + let res_num_cols = num_cols; + + let mut res = unsafe { Matrix::new(res_num_rows, res_num_cols).unwrap_unchecked() }; + + (0..self.rows as usize) + .flat_map(|src_ridx| (0..self.cols as usize).map(move |src_cidx| (src_ridx, src_cidx))) + .for_each(|(src_ridx, src_cidx)| { + let decompressed_elem_cidx = src_cidx * COMPRESSION_FACTOR as usize; + + res[(src_ridx, decompressed_elem_cidx)] = self[(src_ridx, src_cidx)] & mat_elem_mask; + + if branch_opt_util::likely((decompressed_elem_cidx + 1) < num_cols as usize) { + res[(src_ridx, decompressed_elem_cidx + 1)] = (self[(src_ridx, src_cidx)] >> BITS_PER_UNCOMPRESSED_ELEMENT) & mat_elem_mask; + } + + if branch_opt_util::likely((decompressed_elem_cidx + 2) < num_cols as usize) { + res[(src_ridx, decompressed_elem_cidx + 2)] = (self[(src_ridx, src_cidx)] >> (2 * BITS_PER_UNCOMPRESSED_ELEMENT)) & mat_elem_mask; + } + }); + + Ok(res) + } + // Compression factor 4 + MIN_CIPHER_TEXT_BIT_LEN..=8 => { + const COMPRESSION_FACTOR: u32 = 4; + const BITS_PER_UNCOMPRESSED_ELEMENT: usize = (u32::BITS / COMPRESSION_FACTOR) as usize; + + let mat_elem_mask = (1u32 << mat_elem_bit_len) - 1; + + assert_eq!(num_cols.div_ceil(COMPRESSION_FACTOR), self.cols); + + let res_num_rows = self.rows; + let res_num_cols = num_cols; + + let mut res = unsafe { Matrix::new(res_num_rows, res_num_cols).unwrap_unchecked() }; + + (0..self.rows as usize) + .flat_map(|src_ridx| (0..self.cols as usize).map(move |src_cidx| (src_ridx, src_cidx))) + .for_each(|(src_ridx, src_cidx)| { + let decompressed_elem_cidx = src_cidx * COMPRESSION_FACTOR as usize; + + res[(src_ridx, decompressed_elem_cidx)] = self[(src_ridx, src_cidx)] & mat_elem_mask; + + if branch_opt_util::likely((decompressed_elem_cidx + 1) < num_cols as usize) { + res[(src_ridx, decompressed_elem_cidx + 1)] = (self[(src_ridx, src_cidx)] >> BITS_PER_UNCOMPRESSED_ELEMENT) & mat_elem_mask; + } + + if branch_opt_util::likely((decompressed_elem_cidx + 2) < num_cols as usize) { + res[(src_ridx, decompressed_elem_cidx + 2)] = (self[(src_ridx, src_cidx)] >> (2 * BITS_PER_UNCOMPRESSED_ELEMENT)) & mat_elem_mask; + } + + if branch_opt_util::likely((decompressed_elem_cidx + 3) < num_cols as usize) { + res[(src_ridx, decompressed_elem_cidx + 3)] = (self[(src_ridx, src_cidx)] >> (3 * BITS_PER_UNCOMPRESSED_ELEMENT)) & mat_elem_mask; + } + }); + + Ok(res) + } + _ => { + branch_opt_util::cold(); + panic!("Impossible cipher text bit length provided as input to the compression function !"); + } + } + } + + /// Performs the multiplication of a row vector (1xN matrix) by a compressed representation of the transpose of a matrix (MxN). /// /// # Arguments /// - /// * `rhs` - The matrix to multiply with (MxN). + /// * `rhs` - The compressed matrix to multiply with (MxN). Decompression is performed on the fly. /// /// # Returns /// /// * `Result` - The resulting matrix (1xM) if the input is valid. - /// Returns an error if the input is invalid (self is not a row vector, or the dimensions are incompatible). - pub fn row_vector_x_transposed_matrix(&self, rhs: &Matrix) -> Result { - if branch_opt_util::unlikely(!(self.rows == 1 && self.cols == rhs.cols)) { + /// Returns an error if the input is invalid (self is not a row vector, or the dimensions are incompatible). + pub fn row_vector_x_compressed_transposed_matrix(&self, rhs: &Matrix, decompressed_num_cols: u32, mat_elem_bit_len: usize) -> Result { + if branch_opt_util::unlikely(!(self.rows == 1 && self.cols == decompressed_num_cols)) { return Err(ChalametPIRError::IncompatibleDimensionForRowVectorTransposedMatrixMultiplication); } let res_num_rows = self.rows; let res_num_cols = rhs.rows; + let mat_elem_mask = (1u32 << mat_elem_bit_len) - 1; let mut res_elems = vec![0u32; (res_num_rows * res_num_cols) as usize]; - res_elems.par_iter_mut().enumerate().for_each(|(lin_idx, v)| { - let r_idx = 0; - let c_idx = lin_idx; + match mat_elem_bit_len { + // Compression factor 2 + 11..=MAX_CIPHER_TEXT_BIT_LEN => { + const COMPRESSION_FACTOR: u32 = 2; + const BITS_PER_UNCOMPRESSED_ELEMENT: usize = (u32::BITS / COMPRESSION_FACTOR) as usize; - *v = (0..self.cols as usize).fold(0u32, |acc, k| acc.wrapping_add(self[(r_idx, k)].wrapping_mul(rhs[(c_idx, k)]))); - }); + res_elems.par_iter_mut().enumerate().for_each(|(lin_idx, res_elem)| { + let r_idx = 0; + let c_idx = lin_idx; + + // First (rhs.cols - 1) compressed elements + let acc = (0..(rhs.cols - 1) as usize).fold(0u32, |acc, compressed_elem_cidx| { + let decompressed_elem_cidx = compressed_elem_cidx * COMPRESSION_FACTOR as usize; + let compressed_elem = rhs[(c_idx, compressed_elem_cidx)]; + + let first = self[(r_idx, decompressed_elem_cidx)].wrapping_mul(compressed_elem & mat_elem_mask); + let second = self[(r_idx, decompressed_elem_cidx + 1)].wrapping_mul((compressed_elem >> BITS_PER_UNCOMPRESSED_ELEMENT) & mat_elem_mask); + + acc.wrapping_add(first).wrapping_add(second) + }); + + // Last compressed element + let compressed_elem_cidx = (rhs.cols - 1) as usize; + let compressed_elem = rhs[(c_idx, compressed_elem_cidx)]; + + let mut decompressed_elem_cidx = compressed_elem_cidx * COMPRESSION_FACTOR as usize; + + let first = self[(r_idx, decompressed_elem_cidx)].wrapping_mul(compressed_elem & mat_elem_mask); + decompressed_elem_cidx += 1; + + let second = if branch_opt_util::likely(decompressed_elem_cidx < self.cols as usize) { + self[(r_idx, decompressed_elem_cidx)].wrapping_mul((compressed_elem >> BITS_PER_UNCOMPRESSED_ELEMENT) & mat_elem_mask) + } else { + 0 + }; + + *res_elem = acc.wrapping_add(first).wrapping_add(second); + }); + } + // Compression factor 3 + 9..=10 => { + const COMPRESSION_FACTOR: u32 = 3; + const BITS_PER_UNCOMPRESSED_ELEMENT: usize = (u32::BITS / COMPRESSION_FACTOR) as usize; + + res_elems.par_iter_mut().enumerate().for_each(|(lin_idx, res_elem)| { + let r_idx = 0; + let c_idx = lin_idx; + + // First (rhs.cols - 1) compressed elements + let acc = (0..(rhs.cols - 1) as usize).fold(0u32, |acc, compressed_elem_cidx| { + let decompressed_elem_cidx = compressed_elem_cidx * COMPRESSION_FACTOR as usize; + let compressed_elem = rhs[(c_idx, compressed_elem_cidx)]; + + let first = self[(r_idx, decompressed_elem_cidx)].wrapping_mul(compressed_elem & mat_elem_mask); + let second = self[(r_idx, decompressed_elem_cidx + 1)].wrapping_mul((compressed_elem >> BITS_PER_UNCOMPRESSED_ELEMENT) & mat_elem_mask); + let third = self[(r_idx, decompressed_elem_cidx + 2)].wrapping_mul((compressed_elem >> (2 * BITS_PER_UNCOMPRESSED_ELEMENT)) & mat_elem_mask); + + acc.wrapping_add(first).wrapping_add(second).wrapping_add(third) + }); + + // Last compressed element + let compressed_elem_cidx = (rhs.cols - 1) as usize; + let compressed_elem = rhs[(c_idx, compressed_elem_cidx)]; + + let mut decompressed_elem_cidx = compressed_elem_cidx * COMPRESSION_FACTOR as usize; + + let first = self[(r_idx, decompressed_elem_cidx)].wrapping_mul(compressed_elem & mat_elem_mask); + decompressed_elem_cidx += 1; + + let second = if branch_opt_util::likely(decompressed_elem_cidx < self.cols as usize) { + self[(r_idx, decompressed_elem_cidx)].wrapping_mul((compressed_elem >> BITS_PER_UNCOMPRESSED_ELEMENT) & mat_elem_mask) + } else { + 0 + }; + decompressed_elem_cidx += 1; + + let third = if branch_opt_util::likely(decompressed_elem_cidx < self.cols as usize) { + self[(r_idx, decompressed_elem_cidx)].wrapping_mul((compressed_elem >> (2 * BITS_PER_UNCOMPRESSED_ELEMENT)) & mat_elem_mask) + } else { + 0 + }; + + *res_elem = acc.wrapping_add(first).wrapping_add(second).wrapping_add(third); + }); + } + // Compression factor 4 + MIN_CIPHER_TEXT_BIT_LEN..=8 => { + const COMPRESSION_FACTOR: u32 = 4; + const BITS_PER_UNCOMPRESSED_ELEMENT: usize = (u32::BITS / COMPRESSION_FACTOR) as usize; + + res_elems.par_iter_mut().enumerate().for_each(|(lin_idx, res_elem)| { + let r_idx = 0; + let c_idx = lin_idx; + + // First (rhs.cols - 1) compressed elements + let acc = (0..(rhs.cols - 1) as usize).fold(0u32, |acc, compressed_elem_cidx| { + let decompressed_elem_cidx = compressed_elem_cidx * COMPRESSION_FACTOR as usize; + let compressed_elem = rhs[(c_idx, compressed_elem_cidx)]; + + let first = self[(r_idx, decompressed_elem_cidx)].wrapping_mul(compressed_elem & mat_elem_mask); + let second = self[(r_idx, decompressed_elem_cidx + 1)].wrapping_mul((compressed_elem >> BITS_PER_UNCOMPRESSED_ELEMENT) & mat_elem_mask); + let third = self[(r_idx, decompressed_elem_cidx + 2)].wrapping_mul((compressed_elem >> (2 * BITS_PER_UNCOMPRESSED_ELEMENT)) & mat_elem_mask); + let fourth = self[(r_idx, decompressed_elem_cidx + 3)].wrapping_mul((compressed_elem >> (3 * BITS_PER_UNCOMPRESSED_ELEMENT)) & mat_elem_mask); + + acc.wrapping_add(first).wrapping_add(second).wrapping_add(third).wrapping_add(fourth) + }); + + // Last compressed element + let compressed_elem_cidx = (rhs.cols - 1) as usize; + let compressed_elem = rhs[(c_idx, compressed_elem_cidx)]; + + let mut decompressed_elem_cidx = compressed_elem_cidx * COMPRESSION_FACTOR as usize; + + let first = self[(r_idx, decompressed_elem_cidx)].wrapping_mul(compressed_elem & mat_elem_mask); + decompressed_elem_cidx += 1; + + let second = if branch_opt_util::likely(decompressed_elem_cidx < self.cols as usize) { + self[(r_idx, decompressed_elem_cidx)].wrapping_mul((compressed_elem >> BITS_PER_UNCOMPRESSED_ELEMENT) & mat_elem_mask) + } else { + 0 + }; + decompressed_elem_cidx += 1; + + let third = if branch_opt_util::likely(decompressed_elem_cidx < self.cols as usize) { + self[(r_idx, decompressed_elem_cidx)].wrapping_mul((compressed_elem >> (2 * BITS_PER_UNCOMPRESSED_ELEMENT)) & mat_elem_mask) + } else { + 0 + }; + decompressed_elem_cidx += 1; + + let fourth = if branch_opt_util::likely(decompressed_elem_cidx < self.cols as usize) { + self[(r_idx, decompressed_elem_cidx)].wrapping_mul((compressed_elem >> (3 * BITS_PER_UNCOMPRESSED_ELEMENT)) & mat_elem_mask) + } else { + 0 + }; + + *res_elem = acc.wrapping_add(first).wrapping_add(second).wrapping_add(third).wrapping_add(fourth); + }); + } + _ => { + branch_opt_util::cold(); + panic!("Impossible cipher text bit length provided as input to the compression function !"); + } + }; Matrix::from_values(res_num_rows, res_num_cols, res_elems) } @@ -128,7 +487,7 @@ impl Matrix { /// # Returns /// /// * `Result` - A new identity matrix if the input is valid (rows is positive). - /// Returns an error if rows is zero. + /// Returns an error if rows is zero. #[cfg(test)] pub fn identity(rows: u32) -> Result { if branch_opt_util::unlikely(rows == 0) { @@ -172,7 +531,7 @@ impl Matrix { /// # Returns /// /// * `Result` - A new matrix if the input is valid (rows and cols are positive). - /// Returns an error if either rows or cols is zero. + /// Returns an error if either rows or cols is zero. pub fn generate_from_seed(rows: u32, cols: u32, seed: &[u8; SEED_BYTE_LEN]) -> Result { let mut hasher = TurboShake128::default(); hasher.absorb(seed); @@ -203,7 +562,7 @@ impl Matrix { /// # Returns /// /// * `Result` - A new row/ column vector if the input is valid (rows or cols is 1). - /// Returns an error if neither rows nor cols is 1. + /// Returns an error if neither rows nor cols is 1. pub fn sample_from_uniform_ternary_dist(rows: u32, cols: u32) -> Result { if branch_opt_util::unlikely(!(rows == 1 || cols == 1)) { return Err(ChalametPIRError::InvalidDimensionForVector); @@ -251,7 +610,7 @@ impl Matrix { /// # Returns /// /// * `Result<(Matrix, BinaryFuseFilter), ChalametPIRError>` - A tuple containing the resulting matrix and the Binary Fuse Filter. - /// Returns an error if filter construction fails. + /// Returns an error if filter construction fails. pub fn from_kv_database( db: HashMap<&[u8], &[u8]>, mat_elem_bit_len: usize, @@ -279,13 +638,9 @@ impl Matrix { /// # Returns /// /// * `Result, ChalametPIRError>` - The value associated with the key if found. - /// Returns an error if the key is not found or if an error occurs during value recovery. + /// Returns an error if the key is not found or if an error occurs during value recovery. #[cfg(test)] - fn recover_value_from_encoded_kv_database( - &self, - key: &[u8], - filter: &binary_fuse_filter::BinaryFuseFilter, - ) -> Result, ChalametPIRError> { + fn recover_value_from_encoded_kv_database(&self, key: &[u8], filter: &binary_fuse_filter::BinaryFuseFilter) -> Result, ChalametPIRError> { const { assert!(ARITY == 3 || ARITY == 4) } match ARITY { @@ -309,7 +664,7 @@ impl Matrix { /// # Returns /// /// * `Result<(Matrix, BinaryFuseFilter), ChalametPIRError>` - A tuple containing the resulting matrix and the Binary Fuse Filter. - /// Returns an error if filter construction fails. + /// Returns an error if filter construction fails. fn from_kv_database_with_3_wise_xor_filter( db: HashMap<&[u8], &[u8]>, mat_elem_bit_len: usize, @@ -390,7 +745,7 @@ impl Matrix { /// # Returns /// /// * `Result, ChalametPIRError>` - The value associated with the key if found. - /// Returns an error if the key is not found or if an error occurs during value recovery. + /// Returns an error if the key is not found or if an error occurs during value recovery. #[cfg(test)] fn recover_value_from_3_wise_xor_filter(&self, key: &[u8], filter: &binary_fuse_filter::BinaryFuseFilter) -> Result, ChalametPIRError> { let mat_elem_mask = (1u32 << filter.mat_elem_bit_len) - 1; @@ -441,7 +796,7 @@ impl Matrix { /// # Returns /// /// * `Result<(Matrix, BinaryFuseFilter), ChalametPIRError>` - A tuple containing the resulting matrix and the Binary Fuse Filter. - /// Returns an error if filter construction fails. + /// Returns an error if filter construction fails. fn from_kv_database_with_4_wise_xor_filter( db: HashMap<&[u8], &[u8]>, mat_elem_bit_len: usize, @@ -740,7 +1095,12 @@ impl Neg for &Matrix { pub mod test { use crate::{ SEED_BYTE_LEN, - pir_internals::{binary_fuse_filter::BinaryFuseFilter, error::ChalametPIRError, matrix::Matrix, params::SERVER_SETUP_MAX_ATTEMPT_COUNT}, + pir_internals::{ + binary_fuse_filter::BinaryFuseFilter, + error::ChalametPIRError, + matrix::Matrix, + params::{MAX_CIPHER_TEXT_BIT_LEN, MIN_CIPHER_TEXT_BIT_LEN, SERVER_SETUP_MAX_ATTEMPT_COUNT}, + }, }; use rand::prelude::*; use rand_chacha::ChaCha8Rng; @@ -786,33 +1146,38 @@ pub mod test { fn encode_kv_database_using_3_wise_xor_filter_and_recover_values() { const ARITY: u32 = 3; - const MIN_NUM_KV_PAIRS: usize = 1_000; - const MAX_NUM_KV_PAIRS: usize = 10_000; + const MIN_NUM_KV_PAIRS: usize = 1usize << 8; + const MAX_NUM_KV_PAIRS: usize = 1usize << 16; - const MIN_MAT_ELEM_BIT_LEN: usize = 7; - const MAX_MAT_ELEM_BIT_LEN: usize = 11; + let mut rng = ChaCha8Rng::from_os_rng(); - for num_kv_pairs in (MIN_NUM_KV_PAIRS..=MAX_NUM_KV_PAIRS).step_by(100) { - for mat_elem_bit_len in MIN_MAT_ELEM_BIT_LEN..=MAX_MAT_ELEM_BIT_LEN { - let kv_db = generate_random_kv_database(num_kv_pairs); - let kv_db_as_ref = kv_db.iter().map(|(k, v)| (k.as_slice(), v.as_slice())).collect::>(); + const NUM_TEST_ITERATIONS: usize = 100; - let (db_mat, filter) = Matrix::from_kv_database::(kv_db_as_ref.clone(), mat_elem_bit_len, SERVER_SETUP_MAX_ATTEMPT_COUNT) - .expect("Must be able to encode key-value database as matrix"); + let mut test_iter = 0; + while test_iter < NUM_TEST_ITERATIONS { + let num_kv_pairs_in_db = rng.random_range(MIN_NUM_KV_PAIRS..=MAX_NUM_KV_PAIRS); + let mat_elem_bit_len = rng.random_range(MIN_CIPHER_TEXT_BIT_LEN..=MAX_CIPHER_TEXT_BIT_LEN); - for &key in kv_db_as_ref.keys() { - let expected_value = *kv_db_as_ref.get(key).expect("Value for queried key must be present"); - let computed_value = db_mat - .recover_value_from_encoded_kv_database::(key, &filter) - .expect("Must be able to recover value from encoded key-value database matrix"); + let kv_db = generate_random_kv_database(num_kv_pairs_in_db); + let kv_db_as_ref = kv_db.iter().map(|(k, v)| (k.as_slice(), v.as_slice())).collect::>(); - assert_eq!( - expected_value, computed_value, - "num_kv_pairs = {}, arity = {}, mat_elem_bit_len = {}", - num_kv_pairs, ARITY, mat_elem_bit_len - ); - } + let (db_mat, filter) = Matrix::from_kv_database::(kv_db_as_ref.clone(), mat_elem_bit_len, SERVER_SETUP_MAX_ATTEMPT_COUNT) + .expect("Must be able to encode key-value database as matrix"); + + for &key in kv_db_as_ref.keys() { + let expected_value = *kv_db_as_ref.get(key).expect("Value for queried key must be present"); + let computed_value = db_mat + .recover_value_from_encoded_kv_database::(key, &filter) + .expect("Must be able to recover value from encoded key-value database matrix"); + + assert_eq!( + expected_value, computed_value, + "num_kv_pairs = {}, arity = {}, mat_elem_bit_len = {}", + num_kv_pairs_in_db, ARITY, mat_elem_bit_len + ); } + + test_iter += 1; } } @@ -820,33 +1185,38 @@ pub mod test { fn encode_kv_database_using_4_wise_xor_filter_and_recover_values() { const ARITY: u32 = 4; - const MIN_NUM_KV_PAIRS: usize = 1_000; - const MAX_NUM_KV_PAIRS: usize = 10_000; + const MIN_NUM_KV_PAIRS: usize = 1usize << 8; + const MAX_NUM_KV_PAIRS: usize = 1usize << 16; - const MIN_MAT_ELEM_BIT_LEN: usize = 7; - const MAX_MAT_ELEM_BIT_LEN: usize = 11; + let mut rng = ChaCha8Rng::from_os_rng(); - for num_kv_pairs in (MIN_NUM_KV_PAIRS..=MAX_NUM_KV_PAIRS).step_by(100) { - for mat_elem_bit_len in MIN_MAT_ELEM_BIT_LEN..=MAX_MAT_ELEM_BIT_LEN { - let kv_db = generate_random_kv_database(num_kv_pairs); - let kv_db_as_ref = kv_db.iter().map(|(k, v)| (k.as_slice(), v.as_slice())).collect::>(); + const NUM_TEST_ITERATIONS: usize = 100; - let (db_mat, filter) = Matrix::from_kv_database::(kv_db_as_ref.clone(), mat_elem_bit_len, SERVER_SETUP_MAX_ATTEMPT_COUNT) - .expect("Must be able to encode key-value database as matrix"); + let mut test_iter = 0; + while test_iter < NUM_TEST_ITERATIONS { + let num_kv_pairs_in_db = rng.random_range(MIN_NUM_KV_PAIRS..=MAX_NUM_KV_PAIRS); + let mat_elem_bit_len = rng.random_range(MIN_CIPHER_TEXT_BIT_LEN..=MAX_CIPHER_TEXT_BIT_LEN); - for &key in kv_db_as_ref.keys() { - let expected_value = *kv_db_as_ref.get(key).expect("Value for queried key must be present"); - let computed_value = db_mat - .recover_value_from_encoded_kv_database::(key, &filter) - .expect("Must be able to recover value from encoded key-value database matrix"); + let kv_db = generate_random_kv_database(num_kv_pairs_in_db); + let kv_db_as_ref = kv_db.iter().map(|(k, v)| (k.as_slice(), v.as_slice())).collect::>(); - assert_eq!( - expected_value, computed_value, - "num_kv_pairs = {}, arity = {}, mat_elem_bit_len = {}", - num_kv_pairs, ARITY, mat_elem_bit_len - ); - } + let (db_mat, filter) = Matrix::from_kv_database::(kv_db_as_ref.clone(), mat_elem_bit_len, SERVER_SETUP_MAX_ATTEMPT_COUNT) + .expect("Must be able to encode key-value database as matrix"); + + for &key in kv_db_as_ref.keys() { + let expected_value = *kv_db_as_ref.get(key).expect("Value for queried key must be present"); + let computed_value = db_mat + .recover_value_from_encoded_kv_database::(key, &filter) + .expect("Must be able to recover value from encoded key-value database matrix"); + + assert_eq!( + expected_value, computed_value, + "num_kv_pairs = {}, arity = {}, mat_elem_bit_len = {}", + num_kv_pairs_in_db, ARITY, mat_elem_bit_len + ); } + + test_iter += 1; } } @@ -922,7 +1292,7 @@ pub mod test { } #[test] - fn row_vector_transposed_matrix_multiplication_works() { + fn row_vector_compressed_transposed_matrix_multiplication_works() { const NUM_ATTEMPT_VECTOR_MATRIX_MULTIPLICATIONS: usize = 100; const MIN_ROW_VECTOR_DIM: u32 = 1; const MAX_ROW_VECTOR_DIM: u32 = 1024; @@ -939,13 +1309,15 @@ pub mod test { let mat_num_rows = vec_num_cols; let mat_num_cols = rng.random_range(MIN_ROW_VECTOR_DIM..=MAX_ROW_VECTOR_DIM); let mat_num_elems = (mat_num_rows * mat_num_cols) as usize; + let mat_elem_bit_len = rng.random_range(MIN_CIPHER_TEXT_BIT_LEN..=MAX_CIPHER_TEXT_BIT_LEN); let row_vector = Matrix::generate_from_seed(vec_num_rows, vec_num_cols, &seed).expect("Row vector must be generated from seed"); let all_ones = Matrix::from_values(mat_num_rows, mat_num_cols, vec![1; mat_num_elems]).expect("Matrix of ones must be created"); let transposed_all_ones = all_ones.transpose(); + let compressed_transposed_all_ones = transposed_all_ones.row_wise_compress(mat_elem_bit_len).expect("Must be able to row-wise compress matrix"); let res_row_vector = row_vector - .row_vector_x_transposed_matrix(&transposed_all_ones) + .row_vector_x_compressed_transposed_matrix(&compressed_transposed_all_ones, mat_num_rows, mat_elem_bit_len) .expect("Row vector matrix multiplication must pass"); let expected_res_row_vector = { @@ -1073,4 +1445,70 @@ pub mod test { let computed_bpe = filter.bits_per_entry(); assert!(computed_bpe <= EXPECTED_BPE.ceil()); } + + #[test] + fn row_wise_compressed_matrix_can_be_decompressed_for_3_wise_xor_filter() { + const ARITY: u32 = 3; + + const MIN_NUM_KV_PAIRS: usize = 1_000; + const MAX_NUM_KV_PAIRS: usize = 10_000; + + let mut rng = ChaCha8Rng::from_os_rng(); + + const NUM_TEST_ITERATIONS: usize = 100; + let mut test_iter = 0; + + while test_iter < NUM_TEST_ITERATIONS { + let num_kv_pairs = rng.random_range(MIN_NUM_KV_PAIRS..=MAX_NUM_KV_PAIRS); + let mat_elem_bit_len = rng.random_range(MIN_CIPHER_TEXT_BIT_LEN..=MAX_CIPHER_TEXT_BIT_LEN); + + let kv_db = generate_random_kv_database(num_kv_pairs); + let kv_db_as_ref = kv_db.iter().map(|(k, v)| (k.as_slice(), v.as_slice())).collect::>(); + + let (db_mat, _) = Matrix::from_kv_database::(kv_db_as_ref.clone(), mat_elem_bit_len, SERVER_SETUP_MAX_ATTEMPT_COUNT) + .expect("Must be able to encode key-value database as matrix"); + + let compressed_matrix = db_mat.clone().row_wise_compress(mat_elem_bit_len).expect("Matrix compression must work"); + let decompressed_matrix = compressed_matrix + .row_wise_decompress(mat_elem_bit_len, db_mat.num_cols()) + .expect("Matrix decompresson must work"); + + assert_eq!(db_mat, decompressed_matrix); + + test_iter += 1; + } + } + + #[test] + fn row_wise_compressed_matrix_can_be_decompressed_for_4_wise_xor_filter() { + const ARITY: u32 = 4; + + const MIN_NUM_KV_PAIRS: usize = 1_000; + const MAX_NUM_KV_PAIRS: usize = 10_000; + + let mut rng = ChaCha8Rng::from_os_rng(); + + const NUM_TEST_ITERATIONS: usize = 100; + let mut test_iter = 0; + + while test_iter < NUM_TEST_ITERATIONS { + let num_kv_pairs = rng.random_range(MIN_NUM_KV_PAIRS..=MAX_NUM_KV_PAIRS); + let mat_elem_bit_len = rng.random_range(MIN_CIPHER_TEXT_BIT_LEN..=MAX_CIPHER_TEXT_BIT_LEN); + + let kv_db = generate_random_kv_database(num_kv_pairs); + let kv_db_as_ref = kv_db.iter().map(|(k, v)| (k.as_slice(), v.as_slice())).collect::>(); + + let (db_mat, _) = Matrix::from_kv_database::(kv_db_as_ref.clone(), mat_elem_bit_len, SERVER_SETUP_MAX_ATTEMPT_COUNT) + .expect("Must be able to encode key-value database as matrix"); + + let compressed_matrix = db_mat.clone().row_wise_compress(mat_elem_bit_len).expect("Matrix compression must work"); + let decompressed_matrix = compressed_matrix + .row_wise_decompress(mat_elem_bit_len, db_mat.num_cols()) + .expect("Matrix decompresson must work"); + + assert_eq!(db_mat, decompressed_matrix); + + test_iter += 1; + } + } } diff --git a/src/pir_internals/params.rs b/src/pir_internals/params.rs index f1c4514..8cda57b 100644 --- a/src/pir_internals/params.rs +++ b/src/pir_internals/params.rs @@ -1,7 +1,17 @@ pub const LWE_DIMENSION: u32 = 1774; +/// ChalametPIR's paramater choice provides 128 -bit security. pub const BIT_SECURITY_LEVEL: usize = 128; pub const SEED_BYTE_LEN: usize = (2 * BIT_SECURITY_LEVEL) / u8::BITS as usize; pub const HASHED_KEY_BYTE_LEN: usize = (2 * BIT_SECURITY_LEVEL) / u8::BITS as usize; +/// Maximum number of times PIR server attempts to encode key-value database, +/// using Binary Fuse Filter, before giving up. pub const SERVER_SETUP_MAX_ATTEMPT_COUNT: usize = 100; + +/// For key-value database with maximum number of entries 2^42, +/// computed using https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=dff0acb4b039694b899b48409df01f2c. +pub const MIN_CIPHER_TEXT_BIT_LEN: usize = 4; +/// For key-value database with single entry, which is minimum required number of entries, +/// computed using https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=dff0acb4b039694b899b48409df01f2c. +pub const MAX_CIPHER_TEXT_BIT_LEN: usize = 14; diff --git a/src/server.rs b/src/server.rs index 36b7205..89067fc 100644 --- a/src/server.rs +++ b/src/server.rs @@ -12,11 +12,13 @@ use std::collections::HashMap; /// Represents the server in the Keyword Private Information Retrieval (PIR) scheme ChalametPIR. /// -/// The server stores an encoded database matrix, in transposed form, to optimize query response time. +/// The server stores an encoded database matrix, in transposed form, and then row-wise compressed, to optimize query response time. #[derive(Clone)] pub struct Server { - /// This matrix is kept in transposed form to optimize memory access pattern in vector matrix multiplication of server-respond function. - transposed_parsed_db_mat_d: Matrix, + /// This matrix is kept in transposed and then row-wise compressed form to optimize memory access pattern and address memory bandwidth bottleneck, in vector matrix multiplication of server-respond function. + compressed_transposed_parsed_db_mat_d: Matrix, + decompressed_num_cols: u32, + mat_elem_bit_len: usize, } impl Server { @@ -29,7 +31,7 @@ impl Server { /// 3. **Public Matrix Generation:** Generates a public matrix (`pub_mat_a`) using a provided seed (`seed_μ`). The dimensions of this matrix are determined by `LWE_DIMENSION` and the number of fingerprints in the `filter`. /// 4. **Hint Matrix Calculation:** Computes the hint matrix (`hint_mat_m`) by multiplying the public matrix and the parsed database matrix. /// 5. **Serialization:** Converts the hint matrix and filter parameters into byte vectors for storage and transmission. Returns an error if conversion fails. - /// 6. **Transposition:** Transposes the parsed database matrix (`parsed_db_mat_d`) to optimize memory access patterns during execution of the `respond` function. + /// 6. **Transposition and Compression:** Transposes the parsed database matrix (`parsed_db_mat_d`) and compresses it row-wise to optimize memory access patterns during execution of the `respond` function. /// /// # Arguments /// @@ -62,7 +64,18 @@ impl Server { let filter_param_bytes: Vec = filter.to_bytes(); let transposed_parsed_db_mat_d = parsed_db_mat_d.transpose(); - Ok((Server { transposed_parsed_db_mat_d }, hint_bytes, filter_param_bytes)) + let decompressed_num_cols = transposed_parsed_db_mat_d.num_cols(); + let compressed_transposed_parsed_db_mat_d = transposed_parsed_db_mat_d.row_wise_compress(mat_elem_bit_len)?; + + Ok(( + Server { + compressed_transposed_parsed_db_mat_d, + decompressed_num_cols, + mat_elem_bit_len, + }, + hint_bytes, + filter_param_bytes, + )) } /// Sets up the keyword **P**rivate **I**nformation **R**etrieval scheme's server with a given Key-Value database. @@ -74,7 +87,7 @@ impl Server { /// 3. **Public Matrix Generation:** Generates a public matrix (`pub_mat_a`) using a provided seed (`seed_μ`). The dimensions of this matrix are determined by `LWE_DIMENSION` and the number of fingerprints in the `filter`. /// 4. **Hint Matrix Calculation:** Computes the hint matrix (`hint_mat_m`) by multiplying the public matrix and the parsed database matrix. /// 5. **Serialization:** Converts the hint matrix and filter parameters into byte vectors for storage and transmission. Returns an error if conversion fails. - /// 6. **Transposition:** Transposes the parsed database matrix (`parsed_db_mat_d`) to optimize memory access patterns during execution of the `respond` function. + /// 6. **Transposition and Compression:** Transposes the parsed database matrix (`parsed_db_mat_d`) and compresses it row-wise to optimize memory access patterns during execution of the `respond` function. /// /// # Arguments /// @@ -136,23 +149,30 @@ impl Server { parsed_db_mat_d_wg_count, )?; - let transposed_parsed_db_mat_d = Matrix::from_bytes( - &transposed_parsed_db_mat_d_buf - .read() - .map_err(|_| ChalametPIRError::VulkanReadingFromBufferFailed)?, - )?; + let transposed_parsed_db_mat_d = Matrix::from_bytes(&transposed_parsed_db_mat_d_buf.read().map_err(|_| ChalametPIRError::VulkanReadingFromBufferFailed)?)?; let hint_bytes = hint_mat_m_buf.read().map_err(|_| ChalametPIRError::VulkanReadingFromBufferFailed)?.to_vec(); let filter_param_bytes: Vec = filter.to_bytes(); - Ok((Server { transposed_parsed_db_mat_d }, hint_bytes, filter_param_bytes)) + let decompressed_num_cols = transposed_parsed_db_mat_d.num_cols(); + let compressed_transposed_parsed_db_mat_d = transposed_parsed_db_mat_d.row_wise_compress(mat_elem_bit_len)?; + + Ok(( + Server { + compressed_transposed_parsed_db_mat_d, + decompressed_num_cols, + mat_elem_bit_len, + }, + hint_bytes, + filter_param_bytes, + )) } /// Responds to a client query. /// - /// This function takes a client's query (in byte form) as input and uses the transposed database matrix to compute the response. + /// This function takes a client's query (in byte form) as input and uses the compressed transposed database matrix to compute the response. /// The process involves: /// 1. **Query Vectorization:** Converts the query bytes into a row vector. Returns an error if conversion fails. - /// 2. **Vector-Matrix Multiplication:** Performs a row vector-transposed matrix multiplication of the query vector and the server's transposed database matrix. This is optimized for efficiency due to the transposition performed during server setup. Returns an error if multiplication fails. + /// 2. **Vector-Matrix Multiplication:** Performs a multiplication of the query vector (row vector) and the server's compressed transposed database matrix. This is optimized for efficiency, as both transposition and the compression is performed during server setup i.e. the offline phase. Returns an error if multiplication fails. /// 3. **Response Serialization:** Converts the resulting response vector into a byte vector for transmission to the client. Returns an error if conversion fails. /// /// # Arguments @@ -164,7 +184,8 @@ impl Server { /// A `Result` containing the response as a byte vector. Returns an error if any error occurs during response computation or serialization. pub fn respond(&self, query: &[u8]) -> Result, ChalametPIRError> { let query_vector = Matrix::from_bytes(query)?; - let response_vector = query_vector.row_vector_x_transposed_matrix(&self.transposed_parsed_db_mat_d)?; + let response_vector = + query_vector.row_vector_x_compressed_transposed_matrix(&self.compressed_transposed_parsed_db_mat_d, self.decompressed_num_cols, self.mat_elem_bit_len)?; Ok(response_vector.to_bytes()) } diff --git a/src/test_pir.rs b/src/test_pir.rs index effb873..d8e2978 100644 --- a/src/test_pir.rs +++ b/src/test_pir.rs @@ -6,106 +6,135 @@ use crate::{client::Client, server::Server}; use rand::prelude::*; use rand_chacha::ChaCha8Rng; use std::collections::HashMap; -use test_case::test_case; -#[test_case(2usize.pow(12); "A key-value database with power of 2 many entries")] -#[test_case(2usize.pow(12) + 1; "A key-value database with non-power of 2 many entries")] -fn test_keyword_pir_with_3_wise_xor_filter(num_kv_pairs_in_db: usize) { +#[test] +fn test_keyword_pir_with_3_wise_xor_filter() { const ARITY: u32 = 3; - let kv_db = generate_random_kv_database(num_kv_pairs_in_db); - let kv_db_as_ref = kv_db.iter().map(|(k, v)| (k.as_slice(), v.as_slice())).collect::>(); + const MIN_NUM_KV_PAIRS: usize = 1usize << 8; + const MAX_NUM_KV_PAIRS: usize = 1usize << 16; let mut rng = ChaCha8Rng::from_os_rng(); - let mut seed_μ = [0u8; 32]; - rng.fill_bytes(&mut seed_μ); - - let (server, hint_bytes, filter_param_bytes) = Server::setup::(&seed_μ, kv_db_as_ref.clone()).expect("Server setup failed"); - let mut client = Client::setup(&seed_μ, &hint_bytes, &filter_param_bytes).expect("Client setup failed"); + const NUM_TEST_ITERATIONS: usize = 10; + const NUMBER_OF_PIR_QUERIES: usize = 10; + + let mut test_iter = 0; + while test_iter < NUM_TEST_ITERATIONS { + let num_kv_pairs_in_db = rng.random_range(MIN_NUM_KV_PAIRS..=MAX_NUM_KV_PAIRS); + + let kv_db = generate_random_kv_database(num_kv_pairs_in_db); + let kv_db_as_ref = kv_db.iter().map(|(k, v)| (k.as_slice(), v.as_slice())).collect::>(); + + let mut seed_μ = [0u8; 32]; + rng.fill_bytes(&mut seed_μ); + + let (server, hint_bytes, filter_param_bytes) = Server::setup::(&seed_μ, kv_db_as_ref.clone()).expect("Server setup failed"); + let mut client = Client::setup(&seed_μ, &hint_bytes, &filter_param_bytes).expect("Client setup failed"); + + let all_keys = kv_db_as_ref.keys().collect::>(); + let random_keys = all_keys.choose_multiple(&mut rng, NUMBER_OF_PIR_QUERIES).collect::>(); + + let mut kv_iter = random_keys.iter().map(|&&&k| (k, kv_db_as_ref[k])); + let (mut key, mut value) = kv_iter.next().unwrap(); + let mut is_current_key_processed = false; + + loop { + if is_current_key_processed { + match kv_iter.next() { + Some((k, v)) => { + key = k; + value = v; + } + None => { + // No more KV pairs to test + break; + } + }; + } - let mut kv_iter = kv_db_as_ref.iter(); - let (&(mut key), &(mut value)) = kv_iter.next().unwrap(); - let mut is_current_kv_pair_processed = false; + match client.query(key) { + Ok(query_bytes) => { + let response_bytes = server.respond(&query_bytes).expect("Server can't respond"); + let received_value = client.process_response(key, &response_bytes).expect("Client can't extract value from response"); - loop { - if is_current_kv_pair_processed { - match kv_iter.next() { - Some((&k, &v)) => { - key = k; - value = v; + assert_eq!(value, received_value); + is_current_key_processed = true; } - None => { - // No more KV pairs to test - break; + Err(e) => { + assert_eq!(e, ChalametPIRError::ArithmeticOverflowAddingQueryIndicator); + is_current_key_processed = false; + continue; } - }; - } - - match client.query(key) { - Ok(query_bytes) => { - let response_bytes = server.respond(&query_bytes).expect("Server can't respond"); - let received_value = client.process_response(key, &response_bytes).expect("Client can't extract value from response"); - - assert_eq!(value, received_value); - is_current_kv_pair_processed = true; - } - Err(e) => { - assert_eq!(e, ChalametPIRError::ArithmeticOverflowAddingQueryIndicator); - is_current_kv_pair_processed = false; - continue; } } + + test_iter += 1; } } -#[test_case(2usize.pow(12); "A key-value database with power of 2 many entries")] -#[test_case(2usize.pow(12) + 1; "A key-value database with non-power of 2 many entries")] -fn test_keyword_pir_with_4_wise_xor_filter(num_kv_pairs_in_db: usize) { +#[test] +fn test_keyword_pir_with_4_wise_xor_filter() { const ARITY: u32 = 4; - let kv_db = generate_random_kv_database(num_kv_pairs_in_db); - let kv_db_as_ref = kv_db.iter().map(|(k, v)| (k.as_slice(), v.as_slice())).collect::>(); + const MIN_NUM_KV_PAIRS: usize = 1usize << 8; + const MAX_NUM_KV_PAIRS: usize = 1usize << 16; let mut rng = ChaCha8Rng::from_os_rng(); - let mut seed_μ = [0u8; 32]; - rng.fill_bytes(&mut seed_μ); - - let (server, hint_bytes, filter_param_bytes) = Server::setup::(&seed_μ, kv_db_as_ref.clone()).expect("Server setup failed"); - let mut client = Client::setup(&seed_μ, &hint_bytes, &filter_param_bytes).expect("Client setup failed"); + const NUM_TEST_ITERATIONS: usize = 10; + const NUMBER_OF_PIR_QUERIES: usize = 10; + + let mut test_iter = 0; + while test_iter < NUM_TEST_ITERATIONS { + let num_kv_pairs_in_db = rng.random_range(MIN_NUM_KV_PAIRS..=MAX_NUM_KV_PAIRS); + + let kv_db = generate_random_kv_database(num_kv_pairs_in_db); + let kv_db_as_ref = kv_db.iter().map(|(k, v)| (k.as_slice(), v.as_slice())).collect::>(); + + let mut seed_μ = [0u8; 32]; + rng.fill_bytes(&mut seed_μ); + + let (server, hint_bytes, filter_param_bytes) = Server::setup::(&seed_μ, kv_db_as_ref.clone()).expect("Server setup failed"); + let mut client = Client::setup(&seed_μ, &hint_bytes, &filter_param_bytes).expect("Client setup failed"); + + let all_keys = kv_db_as_ref.keys().collect::>(); + let random_keys = all_keys.choose_multiple(&mut rng, NUMBER_OF_PIR_QUERIES).collect::>(); + + let mut kv_iter = random_keys.iter().map(|&&&k| (k, kv_db_as_ref[k])); + let (mut key, mut value) = kv_iter.next().unwrap(); + let mut is_current_key_processed = false; + + loop { + if is_current_key_processed { + match kv_iter.next() { + Some((k, v)) => { + key = k; + value = v; + } + None => { + // No more KV pairs to test + break; + } + }; + } - let mut kv_iter = kv_db_as_ref.iter(); - let (&(mut key), &(mut value)) = kv_iter.next().unwrap(); - let mut is_current_kv_pair_processed = false; + match client.query(key) { + Ok(query_bytes) => { + let response_bytes = server.respond(&query_bytes).expect("Server can't respond"); + let received_value = client.process_response(key, &response_bytes).expect("Client can't extract value from response"); - loop { - if is_current_kv_pair_processed { - match kv_iter.next() { - Some((&k, &v)) => { - key = k; - value = v; + assert_eq!(value, received_value); + is_current_key_processed = true; } - None => { - // No more KV pairs to test - break; + Err(e) => { + assert_eq!(e, ChalametPIRError::ArithmeticOverflowAddingQueryIndicator); + is_current_key_processed = false; + continue; } - }; - } - - match client.query(key) { - Ok(query_bytes) => { - let response_bytes = server.respond(&query_bytes).expect("Server can't respond"); - let received_value = client.process_response(key, &response_bytes).expect("Client can't extract value from response"); - - assert_eq!(value, received_value); - is_current_kv_pair_processed = true; - } - Err(e) => { - assert_eq!(e, ChalametPIRError::ArithmeticOverflowAddingQueryIndicator); - is_current_kv_pair_processed = false; - continue; } } + + test_iter += 1; } }