Преглед изворни кода

research/tfhe: Dark market implementation using FHE

parazyd пре 2 година
родитељ
комит
4762c51f35

+ 1 - 0
script/research/.gitignore

@@ -0,0 +1 @@
+keys

+ 2 - 1
script/research/tfhe/Cargo.toml

@@ -8,4 +8,5 @@ edition = "2021"
 [workspace]
 
 [dependencies]
-tfhe = {version = "0.2.4", features = ["boolean", "shortint", "integer", "x86_64-unix"]}
+tfhe = {version = "0.5.3", features = ["boolean", "shortint", "integer", "x86_64-unix", "internal-keycache"]}
+rayon = "1.9.0"

+ 16 - 0
script/research/tfhe/Makefile

@@ -0,0 +1,16 @@
+.POSIX:
+
+CARGO = cargo
+
+all:
+	@echo "Supported targets:"
+	@echo "  * plain"
+	@echo "  * plain-improved"
+	@echo "  * fhe"
+	@echo "  * fhe-parallel"
+	@echo "  * fhe-improved"
+
+plain plain-improved fhe fhe-parallel fhe-improved:
+	$(CARGO) run --release -- $@
+
+.PHONY: plain plain-improved fhe fhe-parallel fhe-improved

+ 120 - 0
script/research/tfhe/src/fhe.rs

@@ -0,0 +1,120 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2024 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::time::Instant;
+use tfhe::integer::ciphertext::RadixCiphertext;
+use tfhe::integer::{ClientKey, ServerKey};
+
+use crate::NUMBER_OF_BLOCKS;
+
+fn vector_sum(server_key: &ServerKey, orders: &mut [RadixCiphertext]) -> RadixCiphertext {
+    let mut total_volume = server_key.create_trivial_zero_radix(NUMBER_OF_BLOCKS);
+    for order in orders {
+        server_key.smart_add_assign(&mut total_volume, order);
+    }
+    total_volume
+}
+
+fn fill_orders(
+    server_key: &ServerKey,
+    orders: &mut [RadixCiphertext],
+    total_volume: RadixCiphertext,
+) {
+    let mut volume_left_to_transact = total_volume;
+    for order in orders {
+        let mut filled_amount = server_key.smart_min(&mut volume_left_to_transact, order);
+        server_key.smart_sub_assign(&mut volume_left_to_transact, &mut filled_amount);
+        *order = filled_amount;
+    }
+}
+
+/// FHE implementation of the volume matching algorithm.
+///
+/// Matches the given encrypted [sell_orders] with encrypted [buy_orders] using the given
+/// [server_key]. The amount of the orders that are successfully filled is written over the original
+/// order count.
+pub fn volume_match(
+    sell_orders: &mut [RadixCiphertext],
+    buy_orders: &mut [RadixCiphertext],
+    server_key: &ServerKey,
+) {
+    println!("Calculating total sell and buy volumes...");
+    let time = Instant::now();
+
+    let mut total_sell_volume = vector_sum(server_key, sell_orders);
+    let mut total_buy_volume = vector_sum(server_key, buy_orders);
+
+    println!(
+        "Total sell and buy volumes are calculated in {:?}",
+        time.elapsed()
+    );
+
+    println!("Calculating total volume to be matched...");
+    let time = Instant::now();
+    let total_volume = server_key.smart_min(&mut total_sell_volume, &mut total_buy_volume);
+    println!(
+        "Calculated total volume to be matched in {:?}",
+        time.elapsed()
+    );
+
+    println!("Filling orders...");
+    let time = Instant::now();
+    fill_orders(server_key, sell_orders, total_volume.clone());
+    fill_orders(server_key, buy_orders, total_volume);
+    println!("Filled orders in {:?}", time.elapsed());
+}
+
+pub fn tester(
+    client_key: &ClientKey,
+    server_key: &ServerKey,
+    input_sell_orders: &[u16],
+    input_buy_orders: &[u16],
+    expected_filled_sells: &[u16],
+    expected_filled_buys: &[u16],
+    fhe_function: fn(&mut [RadixCiphertext], &mut [RadixCiphertext], &ServerKey),
+) {
+    let encrypt = |pt: u16| client_key.encrypt_radix(pt as u64, NUMBER_OF_BLOCKS);
+
+    let mut encrypted_sell_orders = input_sell_orders
+        .iter()
+        .cloned()
+        .map(encrypt)
+        .collect::<Vec<RadixCiphertext>>();
+    let mut encrypted_buy_orders = input_buy_orders
+        .iter()
+        .cloned()
+        .map(encrypt)
+        .collect::<Vec<RadixCiphertext>>();
+
+    println!("Running FHE implementation...");
+    let time = Instant::now();
+    fhe_function(
+        &mut encrypted_sell_orders,
+        &mut encrypted_buy_orders,
+        server_key,
+    );
+    println!("Ran FHE implementation in {:?}", time.elapsed());
+
+    let decrypt = |ct| client_key.decrypt_radix::<u64>(ct) as u16;
+
+    let decrypted_filled_sells: Vec<u16> = encrypted_sell_orders.iter().map(decrypt).collect();
+    let decrypted_filled_buys: Vec<u16> = encrypted_buy_orders.iter().map(decrypt).collect();
+
+    assert_eq!(decrypted_filled_sells, expected_filled_sells);
+    assert_eq!(decrypted_filled_buys, expected_filled_buys);
+}

+ 155 - 0
script/research/tfhe/src/improved_parallel_fhe.rs

@@ -0,0 +1,155 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2024 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::time::Instant;
+
+use rayon::prelude::*;
+
+use tfhe::integer::ciphertext::RadixCiphertext;
+use tfhe::integer::{IntegerCiphertext, ServerKey};
+
+use crate::NUMBER_OF_BLOCKS;
+
+fn compute_prefix_sum(server_key: &ServerKey, arr: &[RadixCiphertext]) -> Vec<RadixCiphertext> {
+    if arr.is_empty() {
+        return arr.to_vec();
+    }
+    let mut prefix_sum: Vec<RadixCiphertext> = (0..arr.len().next_power_of_two())
+        .into_par_iter()
+        .map(|i| {
+            if i < arr.len() {
+                arr[i].clone()
+            } else {
+                server_key.create_trivial_zero_radix(NUMBER_OF_BLOCKS)
+            }
+        })
+        .collect();
+    for d in 0..prefix_sum.len().ilog2() {
+        prefix_sum
+            .par_chunks_exact_mut(2_usize.pow(d + 1))
+            .for_each(move |chunk| {
+                let length = chunk.len();
+                let mut left = chunk.get((length - 1) / 2).unwrap().clone();
+                server_key.smart_add_assign_parallelized(chunk.last_mut().unwrap(), &mut left)
+            });
+    }
+    let last = prefix_sum.last().unwrap().clone();
+    *prefix_sum.last_mut().unwrap() = server_key.create_trivial_zero_radix(NUMBER_OF_BLOCKS);
+    for d in (0..prefix_sum.len().ilog2()).rev() {
+        prefix_sum
+            .par_chunks_exact_mut(2_usize.pow(d + 1))
+            .for_each(move |chunk| {
+                let length = chunk.len();
+                let temp = chunk.last().unwrap().clone();
+                let mut mid = chunk.get((length - 1) / 2).unwrap().clone();
+                server_key.smart_add_assign_parallelized(chunk.last_mut().unwrap(), &mut mid);
+                chunk[(length - 1) / 2] = temp;
+            });
+    }
+    prefix_sum.push(last);
+    prefix_sum[1..=arr.len()].to_vec()
+}
+
+fn fill_orders(
+    server_key: &ServerKey,
+    total_orders: &RadixCiphertext,
+    orders: &mut [RadixCiphertext],
+    prefix_sum_arr: &[RadixCiphertext],
+) {
+    orders
+        .into_par_iter()
+        .enumerate()
+        .for_each(move |(i, order)| {
+            // (total_orders - previous_prefix_sum).max(0)
+            let mut diff = if i == 0 {
+                total_orders.clone()
+            } else {
+                let previous_prefix_sum = &prefix_sum_arr[i - 1];
+
+                // total_orders - previous_prefix_sum
+                let mut diff = server_key.smart_sub_parallelized(
+                    &mut total_orders.clone(),
+                    &mut previous_prefix_sum.clone(),
+                );
+
+                // total_orders > prefix_sum
+                let mut cond = server_key
+                    .smart_gt_parallelized(
+                        &mut total_orders.clone(),
+                        &mut previous_prefix_sum.clone(),
+                    )
+                    .into_radix(diff.blocks().len(), server_key);
+
+                // (total_orders - previous_prefix_sum) * (total_orders > previous_prefix_sum)
+                // = (total_orders - previous_prefix_sum).max(0)
+                server_key.smart_mul_parallelized(&mut cond, &mut diff)
+            };
+
+            // (total_orders - previous_prefix_sum).max(0).min(*order);
+            *order = server_key.smart_min_parallelized(&mut diff, order);
+        });
+}
+
+/// FHE implementation of the volume matching algorithm.
+///
+/// In this function, the implemented algorithm is modified to utilize more concurrency.
+///
+/// Matches the given encrypted [sell_orders] with encrypted [buy_orders] using the given
+/// [server_key]. The amount of the orders that are successfully filled is written over the original
+/// order count.
+pub fn volume_match(
+    sell_orders: &mut [RadixCiphertext],
+    buy_orders: &mut [RadixCiphertext],
+    server_key: &ServerKey,
+) {
+    println!("Creating prefix sum arrays...");
+    let time = Instant::now();
+    let (prefix_sum_sell_orders, prefix_sum_buy_orders) = rayon::join(
+        || compute_prefix_sum(server_key, sell_orders),
+        || compute_prefix_sum(server_key, buy_orders),
+    );
+    println!("Created prefix sum arrays in {:?}", time.elapsed());
+
+    let zero = server_key.create_trivial_zero_radix(NUMBER_OF_BLOCKS);
+
+    let total_buy_orders = prefix_sum_buy_orders.last().unwrap_or(&zero);
+
+    let total_sell_orders = prefix_sum_sell_orders.last().unwrap_or(&zero);
+
+    println!("Matching orders...");
+    let time = Instant::now();
+    rayon::join(
+        || {
+            fill_orders(
+                server_key,
+                total_sell_orders,
+                buy_orders,
+                &prefix_sum_buy_orders,
+            )
+        },
+        || {
+            fill_orders(
+                server_key,
+                total_buy_orders,
+                sell_orders,
+                &prefix_sum_sell_orders,
+            )
+        },
+    );
+    println!("Matched orders in {:?}", time.elapsed());
+}

+ 50 - 0
script/research/tfhe/src/improved_plain.rs

@@ -0,0 +1,50 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2024 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+fn compute_prefix_sum(arr: &[u16]) -> Vec<u16> {
+    let mut sum = 0;
+    arr.iter()
+        .map(|a| {
+            sum += a;
+            sum
+        })
+        .collect()
+}
+
+fn fill_orders(total_orders: u16, orders: &mut [u16], prefix_sum_arr: &[u16]) {
+    for (i, order) in orders.iter_mut().enumerate() {
+        let previous_prefix_sum = if i == 0 { 0 } else { prefix_sum_arr[i - 1] };
+
+        *order = (total_orders as i64 - previous_prefix_sum as i64)
+            .max(0)
+            .min(*order as i64) as u16;
+    }
+}
+
+pub fn volume_match(sell_orders: &mut [u16], buy_orders: &mut [u16]) {
+    let prefix_sum_sell_orders = compute_prefix_sum(sell_orders);
+
+    let prefix_sum_buy_orders = compute_prefix_sum(buy_orders);
+
+    let total_buy_orders = *prefix_sum_buy_orders.last().unwrap_or(&0);
+
+    let total_sell_orders = *prefix_sum_sell_orders.last().unwrap_or(&0);
+
+    fill_orders(total_sell_orders, buy_orders, &prefix_sum_buy_orders);
+    fill_orders(total_buy_orders, sell_orders, &prefix_sum_sell_orders);
+}

+ 107 - 76
script/research/tfhe/src/main.rs

@@ -16,84 +16,115 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use tfhe::{
-    boolean::prelude::{gen_keys as boolean_gen_keys, *},
-    integer::gen_keys_radix,
-    shortint::prelude::{gen_keys as shortint_gen_keys, *},
-};
-
-fn main() {
-    // ===============
-    // Boolean circuit
-    // ===============
-    // Generate a set of client/server keys, using the default parameters.
-    // The client generates both keys. The server key is meant to be published
-    // so that homomorphic circuits can be computed.
-    let (client_key, server_key) = boolean_gen_keys();
-
-    // Encrypt two messages using the (private) client key:
-    let msg1 = true;
-    let msg2 = false;
-    let ct_1 = client_key.encrypt(msg1);
-    let ct_2 = client_key.encrypt(msg2);
-
-    // We use the server public key to execute a boolean circuit:
-    // if ((NOT ct_2) NAND (ct_1 AND ct_2)) then (NOT ct_2) else (ct_1 AND ct_2)
-    let ct_3 = server_key.not(&ct_2);
-    let ct_4 = server_key.and(&ct_1, &ct_2);
-    let ct_5 = server_key.nand(&ct_3, &ct_4);
-    let ct_6 = server_key.mux(&ct_5, &ct_3, &ct_4);
-
-    // We use the client key to decrypt the output of the circuit
-    let output = client_key.decrypt(&ct_6);
-    assert!(output);
-
-    // ================
-    // Shortint circuit
-    // ================
-    // Generate a set of client/server keys
-    // with 2 bits of message and 2 bits of carry
-    let (client_key, server_key) = shortint_gen_keys(PARAM_MESSAGE_2_CARRY_2);
-
-    let msg1 = 3;
-    let msg2 = 2;
-
-    // Encrypt two messages using the (private) client key:
-    let ct_1 = client_key.encrypt(msg1);
-    let ct_2 = client_key.encrypt(msg2);
-
-    // Homomorphically compute an addition
-    let ct_add = server_key.unchecked_add(&ct_1, &ct_2);
-
-    // Define the Hamming weight function
-    // f: x -> sum of the bits of x
-    let f = |x: u64| x.count_ones() as u64;
-
-    // Generate the accumulator for the function
-    let acc = server_key.generate_accumulator(f);
-
-    // Compute the function over the ciphertext using the PBS
-    let ct_res = server_key.apply_lookup_table(&ct_add, &acc);
-
-    // Decrypt the ciphertext using the (private) client key
-    let output = client_key.decrypt(&ct_res);
-    assert_eq!(output, f(msg1 + msg2));
-
-    // ===============
-    // Integer circuit
-    // ===============
-    // We create keys to create 16 bits integers
-    // using 8 blocks of 2 bits
-    let (cks, sks) = gen_keys_radix(&PARAM_MESSAGE_2_CARRY_2, 8);
+use std::time::Instant;
+
+use tfhe::integer::ciphertext::RadixCiphertext;
+use tfhe::integer::keycache::IntegerKeyCache;
+use tfhe::integer::{IntegerKeyKind, ServerKey};
+use tfhe::shortint::parameters::PARAM_MESSAGE_2_CARRY_2_KS_PBS;
+
+mod fhe;
+mod improved_parallel_fhe;
+mod improved_plain;
+mod parallel_fhe;
+mod plain;
+
+/// The number of blocks to be used in the Radix.
+const NUMBER_OF_BLOCKS: usize = 8;
+
+#[allow(clippy::type_complexity)]
+fn test_cases() -> Vec<(String, (Vec<u16>, Vec<u16>, Vec<u16>, Vec<u16>))> {
+    vec![
+        (
+            "empty sell orders".to_owned(),
+            (vec![], (1..11).collect::<Vec<_>>(), vec![], vec![0; 10]),
+        ),
+        (
+            "empty buy orders".to_owned(),
+            ((1..11).collect::<Vec<_>>(), vec![], vec![0; 10], vec![]),
+        ),
+        (
+            "exact matching of sell and buy orders".to_owned(),
+            (
+                (1..11).collect::<Vec<_>>(),
+                (1..11).collect::<Vec<_>>(),
+                (1..11).collect::<Vec<_>>(),
+                (1..11).collect::<Vec<_>>(),
+            ),
+        ),
+        (
+            "a case where there are more buy orders than sell orders".to_owned(),
+            (vec![10; 10], vec![200], vec![10; 10], vec![100]),
+        ),
+        (
+            "a case where there are more sell orders than buy orders".to_owned(),
+            (vec![200], vec![10; 10], vec![100], vec![10; 10]),
+        ),
+        (
+            "maximum input size for sell and buy orders".to_owned(),
+            (
+                vec![100; 499],
+                vec![100; 499],
+                vec![100; 499],
+                vec![100; 499],
+            ),
+        ),
+    ]
+}
 
-    let clear_a = 2382u16;
-    let clear_b = 29374u16;
+/// Runs the given [tester] function with the test cases for volume matching algorithm.
+fn run_test_cases(tester: impl Fn(&[u16], &[u16], &[u16], &[u16])) {
+    for (test_name, test_case) in &test_cases() {
+        println!("Testing {test_name}...");
+        tester(&test_case.0, &test_case.1, &test_case.2, &test_case.3);
+        println!();
+    }
+}
 
-    let mut a = cks.encrypt(clear_a as u64);
-    let mut b = cks.encrypt(clear_b as u64);
+fn test_volume_match_plain(function: fn(&mut [u16], &mut [u16])) {
+    println!("Running test cases for the plain implementation");
+    run_test_cases(|a, b, c, d| plain::tester(a, b, c, d, function));
+}
 
-    let encrypted_max = sks.smart_max_parallelized(&mut a, &mut b);
-    let decrypted_max: u64 = cks.decrypt(&encrypted_max);
+fn test_volume_match_fhe(
+    fhe_function: fn(&mut [RadixCiphertext], &mut [RadixCiphertext], &ServerKey),
+) {
+    println!("Generating keys...");
+    let time = Instant::now();
+    let (client_key, server_key) =
+        IntegerKeyCache.get_from_params(PARAM_MESSAGE_2_CARRY_2_KS_PBS, IntegerKeyKind::Radix);
+    println!("Keys generated in {:?}", time.elapsed());
+
+    println!("Running test cases for the FHE implementation");
+    run_test_cases(|a, b, c, d| fhe::tester(&client_key, &server_key, a, b, c, d, fhe_function));
+}
 
-    assert_eq!(decrypted_max as u16, clear_a.max(clear_b))
+fn main() {
+    for argument in std::env::args() {
+        if argument == "plain" {
+            println!("Running plain version");
+            test_volume_match_plain(plain::volume_match);
+            println!();
+        }
+        if argument == "plain-improved" {
+            println!("Running plain improved version");
+            test_volume_match_plain(improved_plain::volume_match);
+            println!();
+        }
+        if argument == "fhe" {
+            println!("Running fhe version");
+            test_volume_match_fhe(fhe::volume_match);
+            println!();
+        }
+        if argument == "fhe-parallel" {
+            println!("Running parallelized fhe version");
+            test_volume_match_fhe(parallel_fhe::volume_match);
+            println!();
+        }
+        if argument == "fhe-improved" {
+            println!("Running improved parallelized fhe fhe version");
+            test_volume_match_fhe(improved_parallel_fhe::volume_match);
+            println!();
+        }
+    }
 }

+ 93 - 0
script/research/tfhe/src/parallel_fhe.rs

@@ -0,0 +1,93 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2024 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::time::Instant;
+
+use rayon::prelude::*;
+
+use tfhe::integer::ciphertext::RadixCiphertext;
+use tfhe::integer::ServerKey;
+
+use crate::NUMBER_OF_BLOCKS;
+
+// Calculate the element sum of the given vector in parallel
+fn vector_sum(server_key: &ServerKey, orders: Vec<RadixCiphertext>) -> RadixCiphertext {
+    orders.into_par_iter().reduce(
+        || server_key.create_trivial_zero_radix(NUMBER_OF_BLOCKS),
+        |mut acc: RadixCiphertext, mut ele: RadixCiphertext| {
+            server_key.smart_add_parallelized(&mut acc, &mut ele)
+        },
+    )
+}
+
+fn fill_orders(
+    server_key: &ServerKey,
+    orders: &mut [RadixCiphertext],
+    total_volume: RadixCiphertext,
+) {
+    let mut volume_left_to_transact = total_volume;
+    for order in orders {
+        let mut filled_amount =
+            server_key.smart_min_parallelized(&mut volume_left_to_transact, order);
+        server_key.smart_sub_assign_parallelized(&mut volume_left_to_transact, &mut filled_amount);
+        *order = filled_amount;
+    }
+}
+
+/// FHE implementation of the volume matching algorithm.
+///
+/// This version of the algorithm utilizes parallelization to speed up the computation.
+///
+/// Matches the given encrypted [sell_orders] with encrypted [buy_orders] using the given
+/// [server_key]. The amount of the orders that are successfully filled is written over the original
+/// order count.
+pub fn volume_match(
+    sell_orders: &mut [RadixCiphertext],
+    buy_orders: &mut [RadixCiphertext],
+    server_key: &ServerKey,
+) {
+    println!("Calculating total sell and buy volumes...");
+    let time = Instant::now();
+    // Total sell and buy volumes can be calculated in parallel because they have no dependency on
+    // each other.
+    let (mut total_sell_volume, mut total_buy_volume) = rayon::join(
+        || vector_sum(server_key, sell_orders.to_owned()),
+        || vector_sum(server_key, buy_orders.to_owned()),
+    );
+    println!(
+        "Total sell and buy volumes are calculated in {:?}",
+        time.elapsed()
+    );
+
+    println!("Calculating total volume to be matched...");
+    let time = Instant::now();
+    let total_volume =
+        server_key.smart_min_parallelized(&mut total_sell_volume, &mut total_buy_volume);
+    println!(
+        "Calculated total volume to be matched in {:?}",
+        time.elapsed()
+    );
+
+    println!("Filling orders...");
+    let time = Instant::now();
+    rayon::join(
+        || fill_orders(server_key, sell_orders, total_volume.clone()),
+        || fill_orders(server_key, buy_orders, total_volume.clone()),
+    );
+    println!("Filled orders in {:?}", time.elapsed());
+}

+ 61 - 0
script/research/tfhe/src/plain.rs

@@ -0,0 +1,61 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2024 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::time::Instant;
+
+fn fill_orders(orders: &mut [u16], total_volume: u16) {
+    let mut volume_left_to_transact = total_volume;
+    for order in orders {
+        let filled_amount = std::cmp::min(volume_left_to_transact, *order);
+        *order = filled_amount;
+        volume_left_to_transact -= filled_amount;
+    }
+}
+
+/// Plain implementation of the volume matching algorithm.
+///
+/// Matches the given [sell_orders] with [buy_orders].
+/// The amount of the orders that are successfully filled is written over the original order count.
+pub fn volume_match(sell_orders: &mut [u16], buy_orders: &mut [u16]) {
+    let total_sell_volume: u16 = sell_orders.iter().sum();
+    let total_buy_volume: u16 = buy_orders.iter().sum();
+
+    let total_volume = std::cmp::min(total_buy_volume, total_sell_volume);
+
+    fill_orders(sell_orders, total_volume);
+    fill_orders(buy_orders, total_volume);
+}
+
+pub fn tester(
+    input_sell_orders: &[u16],
+    input_buy_orders: &[u16],
+    expected_filled_sells: &[u16],
+    expected_filled_buys: &[u16],
+    function: fn(&mut [u16], &mut [u16]),
+) {
+    let mut sell_orders = input_sell_orders.to_vec();
+    let mut buy_orders = input_buy_orders.to_vec();
+
+    println!("Running plain implementation...");
+    let time = Instant::now();
+    function(&mut sell_orders, &mut buy_orders);
+    println!("Ran plain implementation in {:?}", time.elapsed());
+
+    assert_eq!(sell_orders, expected_filled_sells);
+    assert_eq!(buy_orders, expected_filled_buys);
+}