Merge pull request #39 from Sunscreen-tech/rweber/plaintext

Can divide fractional by constant
This commit is contained in:
rickwebiii
2022-01-24 13:18:47 -08:00
committed by GitHub
12 changed files with 558 additions and 18 deletions

18
.vscode/launch.json vendored
View File

@@ -151,6 +151,24 @@
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug fractional integration tests in library 'sunscreen_compiler'",
"cargo": {
"args": [
"test",
"--no-run",
"--package=sunscreen_compiler",
],
"filter": {
"name": "fractional",
"kind": "test"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",

10
Cargo.lock generated
View File

@@ -304,6 +304,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "398ea4fabe40b9b0d885340a2a991a44c8a645624075ad966d21f88688e2b69e"
[[package]]
name = "float-cmp"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
dependencies = [
"num-traits",
]
[[package]]
name = "funty"
version = "1.1.0"
@@ -869,6 +878,7 @@ version = "0.1.0"
dependencies = [
"bumpalo",
"clap",
"float-cmp",
"log",
"num",
"petgraph",

View File

@@ -19,4 +19,5 @@ seal = { path = "../seal" }
serde = { version = "1.0.130", features = ["derive"] }
[dev-dependencies]
serde_json = "1.0.72"
serde_json = "1.0.72"
float-cmp = "0.9.0"

View File

@@ -1,8 +1,10 @@
use seal::Plaintext as SealPlaintext;
use crate::types::{
ops::{GraphCipherAdd, GraphCipherPlainAdd},
Cipher, GraphCipherMul, GraphCipherSub,
ops::{
GraphCipherAdd, GraphCipherConstAdd, GraphCipherMul, GraphCipherPlainMul, GraphCipherPlainAdd, GraphCipherSub, GraphCipherConstMul, GraphCipherConstDiv
},
Cipher,
};
use crate::{
crate_version,
@@ -213,7 +215,26 @@ impl<const INT_BITS: usize> GraphCipherPlainAdd for Fractional<INT_BITS> {
b: CircuitNode<Self::Right>,
) -> CircuitNode<Cipher<Self::Left>> {
with_ctx(|ctx| {
let n = ctx.add_addition(a.ids[0], b.ids[0]);
let n = ctx.add_addition_plaintext(a.ids[0], b.ids[0]);
CircuitNode::new(&[n])
})
}
}
impl<const INT_BITS: usize> GraphCipherConstAdd for Fractional<INT_BITS> {
type Left = Fractional<INT_BITS>;
type Right = f64;
fn graph_cipher_const_add(
a: CircuitNode<Cipher<Self::Left>>,
b: Self::Right,
) -> CircuitNode<Cipher<Self::Left>> {
with_ctx(|ctx| {
let b = Self::from(b).try_into_plaintext(&ctx.params).unwrap();
let lit = ctx.add_plaintext_literal(b.inner);
let n = ctx.add_addition_plaintext(a.ids[0], lit);
CircuitNode::new(&[n])
})
@@ -252,6 +273,61 @@ impl<const INT_BITS: usize> GraphCipherMul for Fractional<INT_BITS> {
}
}
impl<const INT_BITS: usize> GraphCipherPlainMul for Fractional<INT_BITS> {
type Left = Fractional<INT_BITS>;
type Right = Fractional<INT_BITS>;
fn graph_cipher_plain_mul(
a: CircuitNode<Cipher<Self::Left>>,
b: CircuitNode<Self::Right>,
) -> CircuitNode<Cipher<Self::Left>> {
with_ctx(|ctx| {
let n = ctx.add_multiplication_plaintext(a.ids[0], b.ids[0]);
CircuitNode::new(&[n])
})
}
}
impl<const INT_BITS: usize> GraphCipherConstMul for Fractional<INT_BITS> {
type Left = Fractional<INT_BITS>;
type Right = f64;
fn graph_cipher_const_mul(
a: CircuitNode<Cipher<Self::Left>>,
b: Self::Right,
) -> CircuitNode<Cipher<Self::Left>> {
with_ctx(|ctx| {
let b = Self::from(b).try_into_plaintext(&ctx.params).unwrap();
let lit = ctx.add_plaintext_literal(b.inner);
let n = ctx.add_multiplication_plaintext(a.ids[0], lit);
CircuitNode::new(&[n])
})
}
}
impl<const INT_BITS: usize> GraphCipherConstDiv for Fractional<INT_BITS> {
type Left = Fractional<INT_BITS>;
type Right = f64;
fn graph_cipher_const_div(
a: CircuitNode<Cipher<Self::Left>>,
b: f64,
) -> CircuitNode<Cipher<Self::Left>> {
with_ctx(|ctx| {
let b = Self::try_from(1. / b).unwrap().try_into_plaintext(&ctx.params).unwrap();
let lit = ctx.add_plaintext_literal(b.inner);
let n = ctx.add_multiplication_plaintext(a.ids[0], lit);
CircuitNode::new(&[n])
})
}
}
impl<const INT_BITS: usize> TryIntoPlaintext for Fractional<INT_BITS> {
fn try_into_plaintext(
&self,

View File

@@ -1,6 +1,12 @@
use seal::Plaintext as SealPlaintext;
use crate::types::{ops::{GraphCipherAdd, GraphCipherMul, GraphCipherPlainAdd, GraphCipherPlainMul, GraphCipherConstAdd, GraphCipherConstMul}, Cipher, };
use crate::types::{
ops::{
GraphCipherAdd, GraphCipherConstAdd, GraphCipherConstMul, GraphCipherMul,
GraphCipherPlainAdd, GraphCipherPlainMul,
},
Cipher,
};
use crate::{
types::{intern::CircuitNode, BfvType, FheType, TypeNameInstance},
with_ctx, CircuitInputTrait, Params, TypeName as DeriveTypeName, WithContext,
@@ -221,4 +227,4 @@ impl GraphCipherPlainMul for Signed {
CircuitNode::new(&[n])
})
}
}
}

View File

@@ -299,3 +299,15 @@ where
T::graph_cipher_div(self, rhs)
}
}
impl<T, U> Div<U> for CircuitNode<Cipher<T>>
where
U: FheLiteral,
T: FheType + GraphCipherConstDiv<Left = T, Right = U>,
{
type Output = Self;
fn div(self, rhs: U) -> Self::Output {
T::graph_cipher_const_div(self, rhs)
}
}

View File

@@ -28,9 +28,8 @@
* efficiently as [`Signed`](crate::types::bfv::Signed) and
* [`Unsigned`](crate::types::bfv::Unsigned) types. This type has complex overflow
* conditions. This type intrinsically supports homomorphic addition
* multiplication, and negation. Dividing by plaintext is possible by
* computing the divisor's reciprical and multiplying. Dividing by ciphertext
* is not possible.
* multiplication, and negation. Dividing by an [`f64`] constant is supported.
* Dividing by ciphertext is not possible.
* * The [`Rational`](crate::types::bfv::Rational) type allows quasi fixed-point
* representation. This type interally uses 2 ciphertexts, and is thus requires
* twice as much space as other types. Its overflow semantics are effectively
@@ -49,7 +48,7 @@
* | Fractional | 1 | complex | signed decimal | 1 add | 1 mul | 1 sub | 1 neg | 1 mul* |
* | Rational | 2 | moderate | signed decimal | 2 muls + 1 sub | 2 muls | 2 muls + 1 sub | 1 neg | 2 muls |
*
* `* Plaintext division only.`
* `* Division by constant only.`
*
* The set of feasible computations under FHE with BFV is fairly limited. For
* example, comparisons, modulus, transcendentals, are generally very difficult

View File

@@ -1,4 +1,4 @@
use crate::types::{intern::CircuitNode, Cipher, FheType};
use crate::types::{intern::{CircuitNode, FheLiteral}, Cipher, FheType};
/**
* Called when a circuit encounters a / operation on two encrypted types.
@@ -22,3 +22,26 @@ pub trait GraphCipherDiv {
b: CircuitNode<Cipher<Self::Right>>,
) -> CircuitNode<Cipher<Self::Left>>;
}
/**
* Called when a circuit encounters a / operation on an encrypted numerator and plaintext denominator.
*/
pub trait GraphCipherConstDiv {
/**
* The type of the left operand
*/
type Left: FheType + From<Self::Right>;
/**
* The type of the right operand
*/
type Right: FheLiteral;
/**
* Process the + operation
*/
fn graph_cipher_const_div(
a: CircuitNode<Cipher<Self::Left>>,
b: Self::Right,
) -> CircuitNode<Cipher<Self::Left>>;
}

View File

@@ -1,4 +1,7 @@
use crate::types::{intern::{CircuitNode, FheLiteral}, Cipher, FheType};
use crate::types::{
intern::{CircuitNode, FheLiteral},
Cipher, FheType,
};
/**
* Called when a circuit encounters a * operation on two encrypted types.

View File

@@ -1,13 +1,14 @@
use float_cmp::ApproxEq;
use sunscreen_compiler::{
circuit,
types::{bfv::Fractional, Cipher},
Compiler, PlainModulusConstraint, Runtime,
CircuitInput, Compiler, PlainModulusConstraint, Runtime,
};
type CipherFractional = Cipher<Fractional<64>>;
#[test]
fn can_add_fractional_numbers() {
fn can_add_cipher_cipher() {
#[circuit(scheme = "bfv")]
fn add(a: CipherFractional, b: CipherFractional) -> CipherFractional {
a + b
@@ -52,7 +53,177 @@ fn can_add_fractional_numbers() {
}
#[test]
fn can_sub_fractional_numbers() {
fn can_add_cipher_plain() {
#[circuit(scheme = "bfv")]
fn add(a: CipherFractional, b: Fractional<64>) -> CipherFractional {
a + b
}
let circuit = Compiler::with_circuit(add)
.noise_margin_bits(5)
.plain_modulus_constraint(PlainModulusConstraint::Raw(500))
.compile()
.unwrap();
let runtime = Runtime::new(&circuit.metadata.params).unwrap();
let (public, secret) = runtime.generate_keys().unwrap();
let do_add = |a: f64, b: f64| {
let a_c = runtime
.encrypt(Fractional::<64>::try_from(a).unwrap(), &public)
.unwrap();
let b_p = Fractional::<64>::try_from(b).unwrap();
let args: Vec<CircuitInput> = vec![a_c.into(), b_p.into()];
let result = runtime.run(&circuit, args, &public).unwrap();
let c: Fractional<64> = runtime.decrypt(&result[0], &secret).unwrap();
assert_eq!(c, (a + b).try_into().unwrap());
};
do_add(3.14, 3.14);
do_add(-3.14, 3.14);
do_add(0., 0.);
do_add(7., 3.);
do_add(1e9, 1e9);
do_add(1e-8, 1e-7);
do_add(-3.14, -3.14);
do_add(3.14, -3.14);
do_add(-7., -3.);
do_add(-1e9, -1e9);
do_add(-1e-8, -1e-7);
}
#[test]
fn can_add_plain_cipher() {
#[circuit(scheme = "bfv")]
fn add(a: CipherFractional, b: Fractional<64>) -> CipherFractional {
b + a
}
let circuit = Compiler::with_circuit(add)
.noise_margin_bits(5)
.plain_modulus_constraint(PlainModulusConstraint::Raw(500))
.compile()
.unwrap();
let runtime = Runtime::new(&circuit.metadata.params).unwrap();
let (public, secret) = runtime.generate_keys().unwrap();
let do_add = |a: f64, b: f64| {
let a_c = runtime
.encrypt(Fractional::<64>::try_from(a).unwrap(), &public)
.unwrap();
let b_p = Fractional::<64>::try_from(b).unwrap();
let args: Vec<CircuitInput> = vec![a_c.into(), b_p.into()];
let result = runtime.run(&circuit, args, &public).unwrap();
let c: Fractional<64> = runtime.decrypt(&result[0], &secret).unwrap();
assert_eq!(c, (a + b).try_into().unwrap());
};
do_add(3.14, 3.14);
do_add(-3.14, 3.14);
do_add(0., 0.);
do_add(7., 3.);
do_add(1e9, 1e9);
do_add(1e-8, 1e-7);
do_add(-3.14, -3.14);
do_add(3.14, -3.14);
do_add(-7., -3.);
do_add(-1e9, -1e9);
do_add(-1e-8, -1e-7);
}
#[test]
fn can_add_cipher_literal() {
#[circuit(scheme = "bfv")]
fn add(a: CipherFractional) -> CipherFractional {
a + 3.14
}
let circuit = Compiler::with_circuit(add)
.noise_margin_bits(5)
.plain_modulus_constraint(PlainModulusConstraint::Raw(500))
.compile()
.unwrap();
let runtime = Runtime::new(&circuit.metadata.params).unwrap();
let (public, secret) = runtime.generate_keys().unwrap();
let do_add = |a: f64| {
let a_c = runtime
.encrypt(Fractional::<64>::try_from(a).unwrap(), &public)
.unwrap();
let args: Vec<CircuitInput> = vec![a_c.into()];
let result = runtime.run(&circuit, args, &public).unwrap();
let c: Fractional<64> = runtime.decrypt(&result[0], &secret).unwrap();
// Allow up to 1 ULP of error
assert!(c.approx_eq((a + 3.14).try_into().unwrap(), (0.0, 1)));
};
do_add(3.14);
do_add(-3.14);
do_add(0.);
do_add(7.);
do_add(1e9);
do_add(1e-8);
}
#[test]
fn can_add_literal_cipher() {
#[circuit(scheme = "bfv")]
fn add(a: CipherFractional) -> CipherFractional {
3.14 + a
}
let circuit = Compiler::with_circuit(add)
.noise_margin_bits(5)
.plain_modulus_constraint(PlainModulusConstraint::Raw(500))
.compile()
.unwrap();
let runtime = Runtime::new(&circuit.metadata.params).unwrap();
let (public, secret) = runtime.generate_keys().unwrap();
let do_add = |a: f64| {
let a_c = runtime
.encrypt(Fractional::<64>::try_from(a).unwrap(), &public)
.unwrap();
let args: Vec<CircuitInput> = vec![a_c.into()];
let result = runtime.run(&circuit, args, &public).unwrap();
let c: Fractional<64> = runtime.decrypt(&result[0], &secret).unwrap();
// Allow up to 1 ULP of error
assert!(c.approx_eq((a + 3.14).try_into().unwrap(), (0.0, 1)));
};
do_add(3.14);
do_add(-3.14);
do_add(0.);
do_add(7.);
do_add(1e9);
do_add(1e-8);
}
#[test]
fn can_sub_cipher_cipher() {
#[circuit(scheme = "bfv")]
fn sub(a: Cipher<Fractional<64>>, b: Cipher<Fractional<64>>) -> Cipher<Fractional<64>> {
a - b
@@ -97,7 +268,7 @@ fn can_sub_fractional_numbers() {
}
#[test]
fn can_mul_fractional_numbers() {
fn can_mul_cipher_cipher() {
#[circuit(scheme = "bfv")]
fn mul(a: Cipher<Fractional<64>>, b: Cipher<Fractional<64>>) -> Cipher<Fractional<64>> {
a * b
@@ -140,3 +311,224 @@ fn can_mul_fractional_numbers() {
// can do with 64-bits of precision for the integer.
test_mul(4294967295., 4294967296.);
}
#[test]
fn can_mul_cipher_plain() {
#[circuit(scheme = "bfv")]
fn mul(a: Cipher<Fractional<64>>, b: Fractional<64>) -> Cipher<Fractional<64>> {
a * b
}
let circuit = Compiler::with_circuit(mul)
.noise_margin_bits(5)
.plain_modulus_constraint(PlainModulusConstraint::Raw(100000))
.compile()
.unwrap();
let runtime = Runtime::new(&circuit.metadata.params).unwrap();
let (public, secret) = runtime.generate_keys().unwrap();
let test_mul = |a: f64, b: f64| {
let a_c = runtime
.encrypt(Fractional::<64>::try_from(a).unwrap(), &public)
.unwrap();
let b_p = Fractional::<64>::try_from(b).unwrap();
let args: Vec<CircuitInput> = vec![a_c.into(), b_p.into()];
let result = runtime.run(&circuit, args, &public).unwrap();
let c: Fractional<64> = runtime.decrypt(&result[0], &secret).unwrap();
assert_eq!(c, (a * b).try_into().unwrap());
};
test_mul(-3.14, -3.14);
test_mul(1234., 5678.);
test_mul(-1234., 5678.);
test_mul(0., -3.14);
// Can't multiply by 0 plaintext as this will result in a transparent
// ciphetext.
test_mul(0., 1.);
test_mul(1., -3.14);
test_mul(1., 3.14);
test_mul(1e-23, 1.234e-4);
// 4294967296 is 2^32. This should be about the largest multiplication we
// can do with 64-bits of precision for the integer.
test_mul(4294967295., 4294967296.);
}
#[test]
fn can_mul_plain_cipher() {
#[circuit(scheme = "bfv")]
fn mul(a: Cipher<Fractional<64>>, b: Fractional<64>) -> Cipher<Fractional<64>> {
b * a
}
let circuit = Compiler::with_circuit(mul)
.noise_margin_bits(5)
.plain_modulus_constraint(PlainModulusConstraint::Raw(100000))
.compile()
.unwrap();
let runtime = Runtime::new(&circuit.metadata.params).unwrap();
let (public, secret) = runtime.generate_keys().unwrap();
let test_mul = |a: f64, b: f64| {
let a_c = runtime
.encrypt(Fractional::<64>::try_from(a).unwrap(), &public)
.unwrap();
let b_p = Fractional::<64>::try_from(b).unwrap();
let args: Vec<CircuitInput> = vec![a_c.into(), b_p.into()];
let result = runtime.run(&circuit, args, &public).unwrap();
let c: Fractional<64> = runtime.decrypt(&result[0], &secret).unwrap();
assert_eq!(c, (a * b).try_into().unwrap());
};
test_mul(-3.14, -3.14);
test_mul(1234., 5678.);
test_mul(-1234., 5678.);
test_mul(0., -3.14);
// Can't multiply by 0 plaintext as this will result in a transparent
// ciphetext.
test_mul(0., 1.);
test_mul(1., -3.14);
test_mul(1., 3.14);
test_mul(1e-23, 1.234e-4);
// 4294967296 is 2^32. This should be about the largest multiplication we
// can do with 64-bits of precision for the integer.
test_mul(4294967295., 4294967296.);
}
#[test]
fn can_mul_cipher_literal() {
#[circuit(scheme = "bfv")]
fn mul(a: Cipher<Fractional<64>>) -> Cipher<Fractional<64>> {
a * 3.14
}
let circuit = Compiler::with_circuit(mul)
.noise_margin_bits(5)
.plain_modulus_constraint(PlainModulusConstraint::Raw(100000))
.compile()
.unwrap();
let runtime = Runtime::new(&circuit.metadata.params).unwrap();
let (public, secret) = runtime.generate_keys().unwrap();
let test_mul = |a: f64| {
let a_c = runtime
.encrypt(Fractional::<64>::try_from(a).unwrap(), &public)
.unwrap();
let args: Vec<CircuitInput> = vec![a_c.into()];
let result = runtime.run(&circuit, args, &public).unwrap();
let c: Fractional<64> = runtime.decrypt(&result[0], &secret).unwrap();
// Allow up to 1 ULP of error in computations.
assert!(c.approx_eq((a * 3.14).try_into().unwrap(), (0.0, 1)));
};
test_mul(-3.14);
test_mul(1234.);
test_mul(-1234.);
test_mul(0.);
// Can't multiply by 0 plaintext as this will result in a transparent
// ciphetext.
test_mul(1.);
test_mul(1e-23);
test_mul(4294967295.);
}
#[test]
fn can_mul_literal_cipher() {
#[circuit(scheme = "bfv")]
fn mul(a: Cipher<Fractional<64>>) -> Cipher<Fractional<64>> {
3.14 * a
}
let circuit = Compiler::with_circuit(mul)
.noise_margin_bits(5)
.plain_modulus_constraint(PlainModulusConstraint::Raw(100000))
.compile()
.unwrap();
let runtime = Runtime::new(&circuit.metadata.params).unwrap();
let (public, secret) = runtime.generate_keys().unwrap();
let test_mul = |a: f64| {
let a_c = runtime
.encrypt(Fractional::<64>::try_from(a).unwrap(), &public)
.unwrap();
let args: Vec<CircuitInput> = vec![a_c.into()];
let result = runtime.run(&circuit, args, &public).unwrap();
let c: Fractional<64> = runtime.decrypt(&result[0], &secret).unwrap();
// Allow up to 1 ULP of error in computations.
assert!(c.approx_eq((a * 3.14).try_into().unwrap(), (0.0, 1)));
};
test_mul(-3.14);
test_mul(1234.);
test_mul(-1234.);
test_mul(0.);
// Can't multiply by 0 plaintext as this will result in a transparent
// ciphetext.
test_mul(1.);
test_mul(1e-23);
test_mul(4294967295.);
}
#[test]
fn can_div_cipher_const() {
#[circuit(scheme = "bfv")]
fn mul(a: Cipher<Fractional<64>>) -> Cipher<Fractional<64>> {
a / 3.14
}
let circuit = Compiler::with_circuit(mul)
.noise_margin_bits(5)
.plain_modulus_constraint(PlainModulusConstraint::Raw(100000))
.compile()
.unwrap();
let runtime = Runtime::new(&circuit.metadata.params).unwrap();
let (public, secret) = runtime.generate_keys().unwrap();
let test_div = |a: f64| {
let a_c = runtime
.encrypt(Fractional::<64>::try_from(a).unwrap(), &public)
.unwrap();
let args: Vec<CircuitInput> = vec![a_c.into()];
let result = runtime.run(&circuit, args, &public).unwrap();
let c: Fractional<64> = runtime.decrypt(&result[0], &secret).unwrap();
assert!(c.approx_eq((a / 3.14).try_into().unwrap(), (0.0, 1)));
};
test_div(-3.14);
test_div(1234.);
test_div(-1234.);
test_div(0.);
test_div(1.);
test_div(-1.);
test_div(1e-23);
test_div(4294967295.);
}

View File

@@ -228,4 +228,4 @@ fn can_multiply_literal_cipher() {
let c: Signed = runtime.decrypt(&result[0], &secret).unwrap();
assert_eq!(c, (-45).into());
}
}

View File

@@ -228,4 +228,4 @@ fn can_add_multiply_literal_cipher() {
let c: Unsigned = runtime.decrypt(&result[0], &secret).unwrap();
assert_eq!(c, 45.into());
}
}