tfhe: ciphertext-plaintext multiplication
arnaucube git@arnaucube.com
Tue, 22 Jul 2025 15:42:24 +0000
5 files changed,
88 insertions(+),
15 deletions(-)
M
README.md
→
README.md
@@ -8,3 +8,46 @@ - `ckks`: https://eprint.iacr.org/2016/421.pdf scheme implementation
- `tfhe`: https://eprint.iacr.org/2018/421.pdf scheme implementation `cargo test --release` + + +## Example of usage +> the repo is a work in progress, interfaces will change. + +This example shows usage of TFHE, but the idea is that the same interface would +work for using CKKS & BFV, the only thing to be changed would be the parameters +and the line `type S = TWLE<K>` to use `CKKS<Q, N>` or `BFV<Q, N, T>`. + +```rust +const T: u64 = 128; // msg space (msg modulus) +const K: usize = 16; +type S = TLWE<K>; + +let mut rng = rand::thread_rng(); +let msg_dist = Uniform::new(0_u64, T); + +let (sk, pk) = S::new_key(&mut rng)?; + +// get two random msgs in Z_t +let m1 = Rq::<T, 1>::rand_u64(&mut rng, msg_dist)?; +let m2 = Rq::<T, 1>::rand_u64(&mut rng, msg_dist)?; +let m3 = Rq::<T, 1>::rand_u64(&mut rng, msg_dist)?; + +// encode the msgs into the plaintext space +let p1: Tn<1> = S::encode::<T>(&m1); // plaintext +let p2: Tn<1> = S::encode::<T>(&m2); // plaintext +let c3_const: Tn<1> = Tn(array::from_fn(|i| T64(m3.coeffs()[i].0))); // encode it as constant value + +let c1 = S::encrypt(&mut rng, &pk, &p1)?; +let c2 = S::encrypt(&mut rng, &pk, &p2)?; + +// now we can do encrypted operations (notice that we do them using simple +// operations by operator overloading): +let c3 = c1 + c2; +let c4 = c2 * c3_const; + +// decrypt & decode +let p4_recovered = c4.decrypt(&sk); +let m4 = S::decode::<T>(&p4_recovered); + +// m4 is equal to (m1+m2)*m3 +```
M
arith/src/ring_torus.rs
→
arith/src/ring_torus.rs
@@ -167,15 +167,6 @@
// apply mod (X^N + 1)) modulus_u128::<N>(&mut result); - // sanity check: check that there are no coeffs > i64_max - assert_eq!( - result, - Tn::<N>(array::from_fn(|i| T64(result[i] as u64))) - .coeffs() - .iter() - .map(|c| c.0 as u128) - .collect::<Vec<_>>() - ); Tn(array::from_fn(|i| T64(result[i] as u64))) } fn modulus_u128<const N: usize>(p: &mut Vec<u128>) {
M
arith/src/torus.rs
→
arith/src/torus.rs
@@ -44,7 +44,7 @@ }
} impl AddAssign for T64 { fn add_assign(&mut self, rhs: Self) { - self.0 += rhs.0; + self.0 = self.0.wrapping_add(rhs.0) } }@@ -57,7 +57,7 @@ }
} impl SubAssign for T64 { fn sub_assign(&mut self, rhs: Self) { - self.0 -= rhs.0; + self.0 = self.0.wrapping_sub(rhs.0) } }@@ -87,7 +87,7 @@ }
} impl MulAssign for T64 { fn mul_assign(&mut self, rhs: Self) { - self.0 *= rhs.0; + self.0 = self.0.wrapping_mul(rhs.0) } }@@ -96,14 +96,14 @@ impl Mul<u64> for T64 {
type Output = Self; fn mul(self, s: u64) -> Self { - Self(self.0 * s) + Self(self.0.wrapping_mul(s)) } } impl Mul<&u64> for &T64 { type Output = T64; fn mul(self, s: &u64) -> Self::Output { - T64(self.0 * s) + T64(self.0.wrapping_mul(*s)) } }
M
arith/src/tuple_ring.rs
→
arith/src/tuple_ring.rs
@@ -55,7 +55,7 @@ Self(zip_eq(self.0, other.0).map(|(s, o)| s - o).collect())
} } -/// for (TR,TR), the Mul operation is defined as: +/// for (TR,TR), the Mul operation is defined as the dot product: /// for A, B \in R^k, result = Σ A_i * B_i \in R impl<R: Ring, const K: usize> Mul<TR<R, K>> for TR<R, K> { type Output = R;
M
tfhe/src/tlwe.rs
→
tfhe/src/tlwe.rs
@@ -133,6 +133,15 @@ let b: Tn<1> = self.1 - plaintext;
Self(a, b) } } +// plaintext multiplication +impl<const K: usize> Mul<Tn<1>> for TLWE<K> { + type Output = Self; + fn mul(self, plaintext: Tn<1>) -> Self { + let a: TR<Tn<1>, K> = TR(self.0 .0.iter().map(|r_i| *r_i * plaintext).collect()); + let b: Tn<1> = self.1 * plaintext; + Self(a, b) + } +} #[cfg(test)] mod tests {@@ -231,6 +240,36 @@ let p3_recovered = c3.decrypt(&sk);
let m3_recovered = S::decode::<T>(&p3_recovered); assert_eq!((m1 + m2).remodule::<T>(), m3_recovered); + } + + Ok(()) + } + + #[test] + fn test_mul_plaintext() -> Result<()> { + const T: u64 = 128; + const K: usize = 16; + type S = TLWE<K>; + + let mut rng = rand::thread_rng(); + + for _ in 0..200 { + let (sk, pk) = S::new_key(&mut rng)?; + + let msg_dist = Uniform::new(0_u64, T); + let m1 = Rq::<T, 1>::rand_u64(&mut rng, msg_dist)?; + let m2 = Rq::<T, 1>::rand_u64(&mut rng, msg_dist)?; + let p1: Tn<1> = S::encode::<T>(&m1); + // don't scale up p2, set it directly from m2 + let p2: Tn<1> = Tn(array::from_fn(|i| T64(m2.coeffs()[i].0))); + + let c1 = S::encrypt(&mut rng, &pk, &p1)?; + + let c3 = c1 * p2; + + let p3_recovered: Tn<1> = c3.decrypt(&sk); + let m3_recovered = S::decode::<T>(&p3_recovered); + assert_eq!((m1.to_r() * m2.to_r()).to_rq::<T>(), m3_recovered); } Ok(())