use crate::core_crypto::commons::generators::DeterministicSeeder;
use crate::core_crypto::commons::math::random::RandomGenerator;
use crate::high_level_api::prelude::*;
use crate::integer::server_key::radix_parallel::tests_long_run::{
get_long_test_iterations, get_user_defined_seed,
};
use crate::shortint::engine::ShortintEngine;
use crate::shortint::parameters::test_params::{
TEST_LEGACY_RERAND_META_PARAM_CPU_2_2_KS_PBS_PKE_TO_SMALL_ZKV2_TUNIFORM_2M128,
TEST_LEGACY_RERAND_META_PARAM_GPU_2_2_MULTI_BIT_GROUP_4_KS_PBS_PKE_TO_BIG_ZKV2_TUNIFORM_2M128,
TEST_META_PARAM_CPU_2_2_KS_PBS_PKE_TO_SMALL_ZKV2_TUNIFORM_2M128,
TEST_META_PARAM_GPU_2_2_MULTI_BIT_GROUP_4_KS_PBS_PKE_TO_BIG_ZKV2_TUNIFORM_2M128,
};
use crate::shortint::parameters::MetaParameters;
use crate::{
clear_gpu_thread_locals, set_server_key, ClientKey, CompactPublicKey,
CompressedCiphertextListBuilder, CompressedServerKey,
CompressedSquashedNoiseCiphertextListBuilder, Config, FheBool, FheUint64, GpuIndex,
ReRandomizationContext, ReRandomizationSeedGen, SquashedNoiseFheUint,
};
use rayon::iter::{IndexedParallelIterator, ParallelIterator};
use rayon::slice::ParallelSlice;
use std::sync::atomic::{AtomicUsize, Ordering};
use tfhe_csprng::generators::DefaultRandomGenerator;
use tfhe_csprng::seeders::{Seed, Seeder};
const RERAND_DOMAIN_SEPARATOR: [u8; 8] = *b"TFHE_Rrd";
const CPK_ENCRYPTION_DOMAIN_SEPARATOR: [u8; 8] = *b"TFHE_Enc";
const METADATA_LEN: usize = 256 / 8;
fn num_transactions() -> usize {
std::env::var("TFHE_RS_PROTOCOL_TRANSACTIONS")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.filter(|v| *v > 0)
.unwrap_or_else(get_long_test_iterations)
}
const TEST_GPU: u32 = 0;
const NUM_CONCURRENT_TRANSACTIONS: usize = 4;
fn num_concurrent_transactions() -> usize {
std::env::var("TFHE_RS_PROTOCOL_CONCURRENT_TRANSACTIONS")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.filter(|v| *v > 0)
.unwrap_or(NUM_CONCURRENT_TRANSACTIONS)
}
type NodeOpcode = u32;
const OPCODE_GE: NodeOpcode = 1;
const OPCODE_IF_THEN_ELSE: NodeOpcode = 2;
const OPCODE_ADD: NodeOpcode = 3;
const OPCODE_SUB: NodeOpcode = 4;
fn node_re_randomization_context(opcode: NodeOpcode) -> ReRandomizationContext {
ReRandomizationContext::new(
RERAND_DOMAIN_SEPARATOR,
[opcode.to_be_bytes().as_slice()],
CPK_ENCRYPTION_DOMAIN_SEPARATOR,
)
}
fn install_seeded_engine(seed: Seed) {
let mut seeder = DeterministicSeeder::<DefaultRandomGenerator>::new(seed);
let engine = ShortintEngine::new_from_seeder(&mut seeder);
ShortintEngine::with_thread_local_mut(|local_engine| {
let _ = std::mem::replace(local_engine, engine);
});
}
fn random_bytes<const N: usize>(datagen: &mut RandomGenerator<DefaultRandomGenerator>) -> [u8; N] {
let mut out = [0u8; N];
datagen.fill_slice_with_random_uniform(&mut out);
out
}
fn draw_amount(from_amount: u64, affordable: bool, draw: u64) -> u64 {
let above_balance = u64::MAX - from_amount;
if affordable || above_balance == 0 {
draw % from_amount.saturating_add(1)
} else {
from_amount + 1 + draw % above_balance
}
}
struct Transaction {
from_amount: u64,
to_amount: u64,
amount: u64,
metadata: [[u8; METADATA_LEN]; 3],
}
impl Transaction {
fn draw(index: usize, datagen: &mut RandomGenerator<DefaultRandomGenerator>) -> Self {
let from_amount = datagen.random_uniform::<u64>();
let to_amount = datagen.random_uniform::<u64>();
let amount = draw_amount(
from_amount,
!index.is_multiple_of(5),
datagen.random_uniform::<u64>(),
);
let metadata = core::array::from_fn(|_| random_bytes::<METADATA_LEN>(datagen));
Self {
from_amount,
to_amount,
amount,
metadata,
}
}
fn expected_result(&self) -> (u64, u64) {
let amount_to_transfer = if self.from_amount >= self.amount {
self.amount
} else {
0
};
(
self.from_amount.wrapping_sub(amount_to_transfer),
self.to_amount.wrapping_add(amount_to_transfer),
)
}
}
fn re_randomize_operand<T: ReRandomize>(
ct: &mut T,
cpk: &CompactPublicKey,
seed_gen: &mut ReRandomizationSeedGen,
) {
ct.re_randomize(cpk, seed_gen.next_seed().unwrap()).unwrap();
}
#[allow(
clippy::redundant_clone,
reason = "every node owns and re-randomizes its own copy of its operands, including the nodes \
that happen to be the last reader of a value"
)]
fn run_transfer_graph(
from_amount: &FheUint64,
to_amount: &FheUint64,
amount: &FheUint64,
cpk: &CompactPublicKey,
) -> (FheUint64, FheUint64) {
let has_enough_funds = {
let mut lhs = from_amount.clone();
let mut rhs = amount.clone();
let mut ctx = node_re_randomization_context(OPCODE_GE);
ctx.add_ciphertext(&lhs);
ctx.add_ciphertext(&rhs);
let mut seed_gen = ctx.finalize();
re_randomize_operand(&mut lhs, cpk, &mut seed_gen);
re_randomize_operand(&mut rhs, cpk, &mut seed_gen);
lhs.ge(&rhs)
};
let amount_to_transfer = {
let mut condition: FheBool = has_enough_funds.clone();
let mut then_value = amount.clone();
let mut else_value = FheUint64::encrypt_trivial(0u64);
let mut ctx = node_re_randomization_context(OPCODE_IF_THEN_ELSE);
ctx.add_ciphertext(&condition);
ctx.add_ciphertext(&then_value);
ctx.add_ciphertext(&else_value);
let mut seed_gen = ctx.finalize();
re_randomize_operand(&mut condition, cpk, &mut seed_gen);
re_randomize_operand(&mut then_value, cpk, &mut seed_gen);
re_randomize_operand(&mut else_value, cpk, &mut seed_gen);
condition.select(&then_value, &else_value)
};
let (new_to_amount, new_from_amount) = rayon::join(
|| {
let mut lhs = to_amount.clone();
let mut rhs = amount_to_transfer.clone();
let mut ctx = node_re_randomization_context(OPCODE_ADD);
ctx.add_ciphertext(&lhs);
ctx.add_ciphertext(&rhs);
let mut seed_gen = ctx.finalize();
re_randomize_operand(&mut lhs, cpk, &mut seed_gen);
re_randomize_operand(&mut rhs, cpk, &mut seed_gen);
&lhs + &rhs
},
|| {
let mut lhs = from_amount.clone();
let mut rhs = amount_to_transfer.clone();
let mut ctx = node_re_randomization_context(OPCODE_SUB);
ctx.add_ciphertext(&lhs);
ctx.add_ciphertext(&rhs);
let mut seed_gen = ctx.finalize();
re_randomize_operand(&mut lhs, cpk, &mut seed_gen);
re_randomize_operand(&mut rhs, cpk, &mut seed_gen);
&lhs - &rhs
},
);
(new_from_amount, new_to_amount)
}
fn run_one_transaction(
transaction: &Transaction,
index: usize,
cks: &ClientKey,
cpk: &CompactPublicKey,
) {
let Transaction {
from_amount: clear_from_amount,
to_amount: clear_to_amount,
amount: clear_amount,
metadata,
} = transaction;
let (clear_from_amount, clear_to_amount, clear_amount) =
(*clear_from_amount, *clear_to_amount, *clear_amount);
let (expected_new_from, expected_new_to) = transaction.expected_result();
let mut from_amount = FheUint64::encrypt(clear_from_amount, cks);
let mut to_amount = FheUint64::encrypt(clear_to_amount, cks);
let mut amount = FheUint64::encrypt(clear_amount, cks);
from_amount
.re_randomization_metadata_mut()
.set_data(&metadata[0]);
to_amount
.re_randomization_metadata_mut()
.set_data(&metadata[1]);
amount
.re_randomization_metadata_mut()
.set_data(&metadata[2]);
let compressed_inputs = CompressedCiphertextListBuilder::new()
.push(from_amount)
.push(to_amount)
.push(amount)
.build()
.unwrap();
let from_amount: FheUint64 = compressed_inputs.get(0).unwrap().unwrap();
let to_amount: FheUint64 = compressed_inputs.get(1).unwrap().unwrap();
let amount: FheUint64 = compressed_inputs.get(2).unwrap().unwrap();
for (operand, (ct, expected)) in [&from_amount, &to_amount, &amount]
.into_iter()
.zip(metadata.iter())
.enumerate()
{
assert_eq!(
ct.re_randomization_metadata().data(),
expected,
"{index}: input {operand} lost its re-randomization metadata through compression",
);
}
let decrypted_from: u64 = from_amount.decrypt(cks);
let decrypted_to: u64 = to_amount.decrypt(cks);
let decrypted_amount: u64 = amount.decrypt(cks);
assert_eq!(
(decrypted_from, decrypted_to, decrypted_amount),
(clear_from_amount, clear_to_amount, clear_amount),
"{index}: inputs changed value through compression/decompression",
);
let (new_from_amount, new_to_amount) =
run_transfer_graph(&from_amount, &to_amount, &amount, cpk);
let decrypted_new_from: u64 = new_from_amount.decrypt(cks);
let decrypted_new_to: u64 = new_to_amount.decrypt(cks);
assert_eq!(
decrypted_new_from, expected_new_from,
"{index}: invalid transfer result on from amount, \
from: {clear_from_amount}, to: {clear_to_amount}, amount: {clear_amount}",
);
assert_eq!(
decrypted_new_to, expected_new_to,
"{index}: invalid transfer result on to amount, \
from: {clear_from_amount}, to: {clear_to_amount}, amount: {clear_amount}",
);
{
let (new_from_amount_bis, new_to_amount_bis) =
run_transfer_graph(&from_amount, &to_amount, &amount, cpk);
assert_eq!(
bincode::serialize(&new_from_amount).unwrap(),
bincode::serialize(&new_from_amount_bis).unwrap(),
"{index}: determinism check failed on transfer from amount",
);
assert_eq!(
bincode::serialize(&new_to_amount).unwrap(),
bincode::serialize(&new_to_amount_bis).unwrap(),
"{index}: determinism check failed on transfer to amount",
);
}
let compressed_outputs = CompressedCiphertextListBuilder::new()
.push(new_from_amount)
.push(new_to_amount)
.build()
.unwrap();
let new_from_amount: FheUint64 = compressed_outputs.get(0).unwrap().unwrap();
let new_to_amount: FheUint64 = compressed_outputs.get(1).unwrap().unwrap();
let decrypted_new_from: u64 = new_from_amount.decrypt(cks);
let decrypted_new_to: u64 = new_to_amount.decrypt(cks);
assert_eq!(
(decrypted_new_from, decrypted_new_to),
(expected_new_from, expected_new_to),
"{index}: transfer results changed value through compression/decompression",
);
let ns_new_from = new_from_amount.squash_noise().unwrap();
let ns_new_to = new_to_amount.squash_noise().unwrap();
let decrypted_new_from: u64 = ns_new_from.decrypt(cks);
let decrypted_new_to: u64 = ns_new_to.decrypt(cks);
assert_eq!(
(decrypted_new_from, decrypted_new_to),
(expected_new_from, expected_new_to),
"{index}: transfer results changed value through noise squashing",
);
let compressed_ns_outputs = CompressedSquashedNoiseCiphertextListBuilder::new()
.push(ns_new_from)
.push(ns_new_to)
.build()
.unwrap();
let ns_new_from: SquashedNoiseFheUint = compressed_ns_outputs.get(0).unwrap().unwrap();
let ns_new_to: SquashedNoiseFheUint = compressed_ns_outputs.get(1).unwrap().unwrap();
let decrypted_new_from: u64 = ns_new_from.decrypt(cks);
let decrypted_new_to: u64 = ns_new_to.decrypt(cks);
assert_eq!(
decrypted_new_from, expected_new_from,
"{index}: invalid end to end result on from amount, \
from: {clear_from_amount}, to: {clear_to_amount}, amount: {clear_amount}",
);
assert_eq!(
decrypted_new_to, expected_new_to,
"{index}: invalid end to end result on to amount, \
from: {clear_from_amount}, to: {clear_to_amount}, amount: {clear_amount}",
);
}
fn setup(
meta_params: MetaParameters,
test_name: &str,
) -> (
ClientKey,
CompressedServerKey,
CompactPublicKey,
Vec<Transaction>,
DeterministicSeeder<DefaultRandomGenerator>,
) {
let seed = get_user_defined_seed().unwrap_or_else(|| {
let mut seeder = crate::core_crypto::prelude::new_seeder();
seeder.seed()
});
println!("{test_name}::seed = {}", seed.0);
println!(
"{test_name}: replay with TFHE_RS_LONGRUN_TESTS_SEED={}",
seed.0
);
let mut root = DeterministicSeeder::<DefaultRandomGenerator>::new(seed);
let key_seed = root.seed();
let encryption_seed = root.seed();
let data_seed = root.seed();
let config = Config::from(meta_params);
let cks = ClientKey::generate_with_seed(config, key_seed);
install_seeded_engine(encryption_seed);
let compressed_sks = CompressedServerKey::new(&cks);
let cpk = CompactPublicKey::new(&cks);
let mut datagen = RandomGenerator::<DefaultRandomGenerator>::new(data_seed);
let transactions = (0..num_transactions())
.map(|index| Transaction::draw(index, &mut datagen))
.collect();
(cks, compressed_sks, cpk, transactions, root)
}
fn protocol_transfer_workflow_test(meta_params: MetaParameters) {
let (cks, compressed_sks, cpk, transactions, _root) =
setup(meta_params, "protocol_transfer_workflow_test");
let gpu_sks = compressed_sks.decompress_to_specific_gpu(GpuIndex::new(TEST_GPU));
rayon::broadcast(|_| set_server_key(gpu_sks.clone()));
set_server_key(gpu_sks);
let num_transactions = transactions.len();
for (index, transaction) in transactions.iter().enumerate() {
run_one_transaction(transaction, index, &cks, &cpk);
if index.is_multiple_of(100) {
println!("protocol_transfer_workflow_test: {index}/{num_transactions} transactions");
}
}
}
fn protocol_concurrent_transfer_workflow_test(meta_params: MetaParameters) {
let (cks, compressed_sks, cpk, transactions, mut root) =
setup(meta_params, "protocol_concurrent_transfer_workflow_test");
let gpu_sks = compressed_sks.decompress_to_specific_gpu(GpuIndex::new(TEST_GPU));
let concurrency = num_concurrent_transactions();
println!("protocol_concurrent_transfer_workflow_test: {concurrency} transactions in flight");
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(2 * concurrency)
.exit_handler(|_| clear_gpu_thread_locals())
.build()
.unwrap();
pool.broadcast(|_| set_server_key(gpu_sks.clone()));
let num_transactions = transactions.len();
let chunk_size = num_transactions.div_ceil(concurrency);
let done = AtomicUsize::new(0);
let chunk_seeds: Vec<Seed> = (0..num_transactions.div_ceil(chunk_size))
.map(|_| root.seed())
.collect();
pool.install(|| {
transactions
.par_chunks(chunk_size)
.enumerate()
.for_each(|(chunk_index, chunk)| {
install_seeded_engine(chunk_seeds[chunk_index]);
for (offset, transaction) in chunk.iter().enumerate() {
let index = chunk_index * chunk_size + offset;
run_one_transaction(transaction, index, &cks, &cpk);
let completed = done.fetch_add(1, Ordering::Relaxed) + 1;
if completed.is_multiple_of(100) {
println!(
"protocol_concurrent_transfer_workflow_test: \
{completed}/{num_transactions} transactions",
);
}
}
});
});
}
#[test]
fn test_gpu_protocol_erc7984_transfer_workflow_multi_bit() {
protocol_transfer_workflow_test(
TEST_META_PARAM_GPU_2_2_MULTI_BIT_GROUP_4_KS_PBS_PKE_TO_BIG_ZKV2_TUNIFORM_2M128,
);
}
#[test]
fn test_gpu_protocol_erc7984_transfer_workflow_multi_bit_legacy_rerand() {
protocol_transfer_workflow_test(
TEST_LEGACY_RERAND_META_PARAM_GPU_2_2_MULTI_BIT_GROUP_4_KS_PBS_PKE_TO_BIG_ZKV2_TUNIFORM_2M128,
);
}
#[test]
fn test_gpu_protocol_erc7984_transfer_workflow_classical() {
protocol_transfer_workflow_test(
TEST_META_PARAM_CPU_2_2_KS_PBS_PKE_TO_SMALL_ZKV2_TUNIFORM_2M128,
);
}
#[test]
fn test_gpu_protocol_erc7984_transfer_workflow_classical_legacy_rerand() {
protocol_transfer_workflow_test(
TEST_LEGACY_RERAND_META_PARAM_CPU_2_2_KS_PBS_PKE_TO_SMALL_ZKV2_TUNIFORM_2M128,
);
}
#[test]
fn test_gpu_protocol_erc7984_concurrent_transfer_workflow_classical() {
protocol_concurrent_transfer_workflow_test(
TEST_META_PARAM_CPU_2_2_KS_PBS_PKE_TO_SMALL_ZKV2_TUNIFORM_2M128,
);
}