arnaucube
arnaucube/fhe-studyPublic Self-hosted repository
ckks addition & substraction of ciphertexts
arnaucube git@arnaucube.com
Sat, 05 Jul 2025 16:58:41 +0200
commit

84c54e8edd47b1755063a016fca70e1baf6a3a23

parent

6090116a8bba60c73f2bf597e65c5d1a09c50fe7

2 files changed, 113 insertions(+), 11 deletions(-)

jump to
M ckks/src/encoder.rs → ckks/src/encoder.rs

@@ -153,10 +153,9 @@

#[test] fn test_encode_decode() -> Result<()> { const Q: u64 = 1024; - // const N: usize = 4; // ie. m=2*n=8 - const N: usize = 16; + const N: usize = 32; - let T = 16; // WIP + let T = 128; // WIP let mut rng = rand::thread_rng(); for _ in 0..100 {
M ckks/src/lib.rs → ckks/src/lib.rs

@@ -72,7 +72,7 @@ }

fn decrypt( &self, // TODO maybe rm? - sk: SecretKey<Q, N>, + sk: &SecretKey<Q, N>, c: (Rq<Q, N>, Rq<Q, N>), ) -> Result<R<N>> { let m = c.0.clone() + c.1 * sk.0;

@@ -95,10 +95,25 @@ &self,

sk: SecretKey<Q, N>, c: (Rq<Q, N>, Rq<Q, N>), ) -> Result<Vec<C<f64>>> { - let d = self.decrypt(sk, c)?; + let d = self.decrypt(&sk, c)?; self.encoder.decode(&d) } + + pub fn add( + &self, + c0: &(Rq<Q, N>, Rq<Q, N>), + c1: &(Rq<Q, N>, Rq<Q, N>), + ) -> Result<(Rq<Q, N>, Rq<Q, N>)> { + Ok((&c0.0 + &c1.0, &c0.1 + &c1.1)) + } + pub fn sub( + &self, + c0: &(Rq<Q, N>, Rq<Q, N>), + c1: &(Rq<Q, N>, Rq<Q, N>), + ) -> Result<(Rq<Q, N>, Rq<Q, N>)> { + Ok((&c0.0 - &c1.0, &c0.1 + &c1.1)) + } } #[cfg(test)]

@@ -108,10 +123,10 @@

#[test] fn test_encrypt_decrypt() -> Result<()> { const Q: u64 = 2u64.pow(16) + 1; - const T: u64 = 16; - const N: usize = 8; + const N: usize = 32; + const T: u64 = 50; let scale_factor_u64 = 512_u64; // delta - let scale_factor = C::<f64>::new(512.0, 0.0); // delta + let scale_factor = C::<f64>::new(scale_factor_u64 as f64, 0.0); // delta let mut rng = rand::thread_rng();

@@ -124,7 +139,7 @@ let m_raw: R<N> = Rq::<Q, N>::rand_f64(&mut rng, Uniform::new(0_f64, T as f64))?.to_r();

let m = m_raw * scale_factor_u64; let ct = ckks.encrypt(&mut rng, &pk, &m)?; - let m_decrypted = ckks.decrypt(sk, ct)?; + let m_decrypted = ckks.decrypt(&sk, ct)?; let m_decrypted: Vec<u64> = m_decrypted .coeffs()

@@ -141,8 +156,8 @@

#[test] fn test_encode_encrypt_decrypt_decode() -> Result<()> { const Q: u64 = 2u64.pow(16) + 1; + const N: usize = 16; const T: u64 = 16; - const N: usize = 4; let scale_factor = C::<f64>::new(512.0, 0.0); // delta let mut rng = rand::thread_rng();

@@ -155,6 +170,7 @@ let z: Vec<C<f64>> = std::iter::repeat_with(|| C::<f64>::rand(&mut rng, T))

.take(N / 2) .collect(); let m: R<N> = ckks.encoder.encode(&z)?; + println!("{}", m); // sanity check {

@@ -167,7 +183,8 @@ assert_eq!(rounded_z_decoded, z);

} let ct = ckks.encrypt(&mut rng, &pk, &m)?; - let m_decrypted = ckks.decrypt(sk, ct)?; + let m_decrypted = ckks.decrypt(&sk, ct)?; + println!("{}", m_decrypted); let z_decrypted = ckks.encoder.decode(&m_decrypted)?;

@@ -176,6 +193,92 @@ .iter()

.map(|&c| C::<f64>::new(c.re.round(), c.im.round())) .collect(); assert_eq!(rounded_z_decrypted, z); + } + + Ok(()) + } + + #[test] + fn test_add() -> Result<()> { + const Q: u64 = 2u64.pow(16) + 1; + const N: usize = 16; + const T: u64 = 10; + let scale_factor = C::<f64>::new(1024.0, 0.0); // delta + + let mut rng = rand::thread_rng(); + + for _ in 0..1000 { + let ckks = CKKS::<Q, N>::new(scale_factor); + + let (sk, pk) = ckks.new_key(&mut rng)?; + + let z0: Vec<C<f64>> = std::iter::repeat_with(|| C::<f64>::rand(&mut rng, T)) + .take(N / 2) + .collect(); + let z1: Vec<C<f64>> = std::iter::repeat_with(|| C::<f64>::rand(&mut rng, T)) + .take(N / 2) + .collect(); + let m0: R<N> = ckks.encoder.encode(&z0)?; + let m1: R<N> = ckks.encoder.encode(&z1)?; + + let ct0 = ckks.encrypt(&mut rng, &pk, &m0)?; + let ct1 = ckks.encrypt(&mut rng, &pk, &m1)?; + + let ct2 = ckks.add(&ct0, &ct1)?; + + let m2_decrypted = ckks.decrypt(&sk, ct2)?; + + let z_decrypted = ckks.encoder.decode(&m2_decrypted)?; + let rounded_z_decrypted: Vec<C<f64>> = z_decrypted + .iter() + .map(|&c| C::<f64>::new(c.re.round(), c.im.round())) + .collect(); + + let expected_z2: Vec<C<f64>> = itertools::zip_eq(z0, z1).map(|(a, b)| a + b).collect(); + assert_eq!(rounded_z_decrypted, expected_z2); + } + + Ok(()) + } + + #[test] + fn test_sub() -> Result<()> { + const Q: u64 = 2u64.pow(16) + 1; + const N: usize = 16; + const T: u64 = 10; + let scale_factor = C::<f64>::new(1024.0, 0.0); // delta + + let mut rng = rand::thread_rng(); + + for _ in 0..1000 { + let ckks = CKKS::<Q, N>::new(scale_factor); + + let (sk, pk) = ckks.new_key(&mut rng)?; + + let z0: Vec<C<f64>> = std::iter::repeat_with(|| C::<f64>::rand(&mut rng, T)) + .take(N / 2) + .collect(); + let z1: Vec<C<f64>> = std::iter::repeat_with(|| C::<f64>::rand(&mut rng, T)) + .take(N / 2) + .collect(); + let m0: R<N> = ckks.encoder.encode(&z0)?; + let m1: R<N> = ckks.encoder.encode(&z1)?; + + let ct0 = ckks.encrypt(&mut rng, &pk, &m0)?; + let ct1 = ckks.encrypt(&mut rng, &pk, &m1)?; + + let ct2 = ckks.sub(&ct0, &ct1)?; + + let m2_decrypted = ckks.decrypt(&sk, ct2)?; + + let z_decrypted = ckks.encoder.decode(&m2_decrypted)?; + let rounded_z_decrypted: Vec<C<f64>> = z_decrypted + .iter() + .map(|&c| C::<f64>::new(c.re.round(), c.im.round())) + .collect(); + + let expected_z2: Vec<C<f64>> = itertools::zip_eq(z0, z1).map(|(a, b)| a - b).collect(); + assert_eq!(rounded_z_decrypted, expected_z2); } Ok(())