My first FHE program

Now that we have installed the Sunscreen crate as a dependency, let's get started writing our first private app using FHE! Writing our program will be a gradual process and we'll add more code as we progress through this section.

In this example, we'll just multiply two encrypted integers.

use sunscreen::{
    fhe_program,
    types::{bfv::Signed, Cipher},
    Compiler, Error, FheRuntime,
};

#[fhe_program(scheme = "bfv")]
fn simple_multiply(a: Cipher<Signed>, b: Cipher<Signed>) -> Cipher<Signed> {
    a * b
}

fn main() {
}

Notice that the simple_multiply function is like any other function in Rust, except for the #[fhe_program(...)] attribute. This is where the magic happens— it declares your function as an FHE program that can be compiled. The scheme argument should always be "bfv", though we may support additional FHE schemes in the future.

simple_multiply's signature specifies that it takes in two Cipher<Signed> values and returns one. Cipher<T> indicates the contained type T is encrypted (i.e. a ciphertext) and Signed is Sunscreen's signed integer type; thus, Cipher<Signed> indicates that we have an encrypted signed integer. The body of simple_multiply multiplies the ciphertexts a and b together. As with any function in Rust, omitting a ; returns an expression's value from the current scope.

Having specified our program, let's compile it.

use sunscreen::{
    fhe_program,
    types::{bfv::Signed, Cipher},
    Compiler, Error, FheRuntime,
};

#[fhe_program(scheme = "bfv")]
fn simple_multiply(a: Cipher<Signed>, b: Cipher<Signed>) -> Cipher<Signed> {
    a * b
}

fn main() -> Result<(), Error> {
    let app = Compiler::new()
        .fhe_program(simple_multiply)
        .compile()?;

    Ok(())
}

We invoke the compiler to build our simple_multiply FHE program. Compilation translates our program into a runnable format, performs optimizations and fills in implementation details, including figuring out FHE scheme parameters and inserting special operations.

What's the ? after at the end of .compile()? For the uninitiated, the ? operator propagates errors. Fallible expressions in Rust emit Results, which can contain either a value or an error. Using ? unwraps the value in a successful result or immediately returns the error from a failed one, letting the caller of the current function deal with it. We should see the former after compilation, as our program is well-formed.

On success, the compiler emits an Application bundle containing the compiled form of each .fhe_program() argument. In our case, app will contain a single compiled FHE program named simple_multiply.

Next, we need a public and private key pair. In order to generate keys, we'll first construct an FheRuntime with the parameters we got from compilation. This allows us to encrypt/decrypt data and run FHE programs.

use sunscreen::{
    fhe_program,
    types::{bfv::Signed, Cipher},
    Compiler, Error, FheRuntime,
};

#[fhe_program(scheme = "bfv")]
fn simple_multiply(a: Cipher<Signed>, b: Cipher<Signed>) -> Cipher<Signed> {
    a * b
}

fn main() -> Result<(), Error> {
    let app = Compiler::new()
        .fhe_program(simple_multiply)
        .compile()?;

    let runtime = FheRuntime::new(app.params())?;

    let (public_key, private_key) = runtime.generate_keys()?;

    let a = runtime.encrypt(Signed::from(15), &public_key)?;
    let b = runtime.encrypt(Signed::from(5), &public_key)?;

    Ok(())
}

After constructing our runtime, we generate a public and private key pair by calling runtime.generate_keys().

Next, we call Signed::from(15) to make an unencrypted Signed integer equal to 15. Rust doesn't allow implicit type conversion as some languages do, so we use the From trait to explicitly convert a Rust i64 into a Sunscreen Signed.

Once we have our plaintext value 15, we encrypt it by calling runtime.encrypt(...), passing in the value and our public key. We repeat this process for b with the value 5. Now that we have the two ciphertexts a and b to give to simple_multiply, we're ready to run our FHE program!

use sunscreen::{
    fhe_program,
    types::{bfv::Signed, Cipher},
    Compiler, Error, FheRuntime,
};

#[fhe_program(scheme = "bfv")]
fn simple_multiply(a: Cipher<Signed>, b: Cipher<Signed>) -> Cipher<Signed> {
    a * b
}

fn main() -> Result<(), Error> {
    let app = Compiler::new()
        .fhe_program(simple_multiply)
        .compile()?;

    let runtime = FheRuntime::new(app.params())?;

    let (public_key, private_key) = runtime.generate_keys()?;

    let a = runtime.encrypt(Signed::from(15), &public_key)?;
    let b = runtime.encrypt(Signed::from(5), &public_key)?;

    let results = runtime.run(app.get_fhe_program(simple_multiply).unwrap(), vec![a, b], &public_key)?;

    let c: Signed = runtime.decrypt(&results[0], &private_key)?;
    assert_eq!(c, 75.into());

    Ok(())
}

We call runtime.run(...) to execute our FHE program. For the first argument, we pass in our previously compiled program. We retrieve this program by calling app.get_fhe_program() and unwrapping the result. The second argument is always a Vec containing the arguments to the FHE program. In this case, we pass in the encrypted a and b values. You'll need to pass in the public_key as well.

What would happen if we forgot to encrypt one of our values or gave an encrypted Fractional value where the program wanted an encrypted Signed value? Fortunately, the run method first performs some sanity checks to ensure the arguments match the call signature. If the types of the values we pass in don't match the signature, the run method returns an error Result. The ? propagates this error, but our program exits because this is the main() method!

Next, we call runtime.decrypt(...) with the first result and our private key. Programs can return more than one value; hence, results is a Vec. Since our FHE program only returns one value, we decrypt the value at index 0. The left hand side of the assignment denotes the decrypted data is a Signed whereas runtime.decrypt(...) ensures this type matches the ciphertext's encrypted value before decryption. If we had assigned a different type to c, say Fractional, then decrypt would return an error.

Finally, we verify the result equals 75 (i.e. 15 * 5) as expected.