Running FHE programs

As seen in the previous sections, we can write FHE programs using the Sunscreen compiler. Now that we have our program compiled into an ELF file, we can run it on the Parasol processor by writing a small Rust program that uses SPF to execute the program.

Rust programs in general are structured as so; we will go through the steps in turn.

#![allow(unused)]
fn main() {
// Generate a secret key for the user. By default this ensures
// 128 bit security.
let secret_key = SecretKey::generate_with_default_params();

// Generate a compute key for the user. These keys are used for
// operations and do not give access to the plaintext data;
// therefore, this key can safely be shared with another party.
let compute_key = ComputeKey::generate_with_default_params(
        &secret_key,
    );

// Get the default encryption and evaluation parameters.
let enc = Encryption::default();
let eval = Evaluation::with_default_params(&compute_key);

// To pass arguments into the TFHE C program, we must use
// the `ArgsBuilder` to specify the arguments.
let arguments = ArgsBuilder::new()
    .arg(UInt8::encrypt_secret(5, &enc, &secret_key))
    .arg(UInt8::encrypt_secret(10, &enc, &secret_key))
    //...
    .return_value::<UInt8>();

// Allocate memory for the FHE program to run on, as well as a pointer to the
// specific function to execute.
let memory = Arc::new(Memory::new_from_elf(&fhe_file)?);
let program = memory.get_function_entry("FUNCTION_NAME")?;

// Generate the FHE computer instance that will run the program.
let computer = FheComputer::new(
    &enc,
    &eval
);

// Run the programs
let encrypted_result = computer.run_program(
    &program,
    memory,
    arguments,
)?;

// Decrypt the result.
let result =
    encrypted_result.decrypt(&enc, &secret_key);
}

Keys

When running a program, we need to generate two types of keys:

  • Secret key: This key is used to encrypt and decrypt data. It should be kept secret as it allows the user to decrypt the results of FHE programs.
  • Compute key: This key is used to run FHE programs on encrypted data. It does not allow decryption of the data, so it can be shared with a computing party that will run the FHE programs.

For simplicity, we assume the encryption key and decryption key are the same. If desired, a public/secret key pair could instead be generated so that the encryption and decryption key are different.

Argument builder

The ArgsBuilder is used to specify the arguments to the FHE program.

Arguments are specified with the arg method, which can be called multiple times to add multiple arguments. These arguments can be either plaintext values or encrypted values, including pointers (see “Memory” below). These values will need to be the same integer sizes as what is specified in the C program.

The return value of the program is specified using either the return_value::<type>() method, or if there is no return value, then no_return_value() can be used. This converts the ArgsBuilder into an Args, which is the bundled up arguments that can be passed to the program.

Memory

You can also pass in pointers to memory locations that will be used by the FHE program. To do this you will need to create an FheComputer instance and allocate memory for the program to use. The memory can be allocated from an ELF file, which contains the compiled FHE program.

// Initialize the memory space, specifying which ELF file to use.
let memory = Arc::new(Memory::new_from_elf(fhe_file))?);

// Create array of 8 elements of 16-bit unsigned integers.
let data = std::array::from_fn::<_, 8, _>(
    |i| UInt16::encrypt_secret(i as u64, &enc, &secret_key)
);

// Allocate memory for `data` in the FHE memory space.
let data_ptr = memory.try_allocate_type(&data)?;

// Now use the `data_ptr` in the `ArgsBuilder` as an argument to the FHE program.

You can also allocate an array of memory that is uninitialized, which will be filled in by the FHE program. This is useful when you want to run a program that produces multiple outputs or to pass a scratch buffer into your program.

// Instead of specifying a value, specify empty memory locations that will be
// filled in by the FHE program.
let data_buffer = std::array::from_fn::<_, 2, _>(|_| UInt16::new(&enc));
let data_buffer_ptr = memory.try_allocate_type(&data_buffer).unwrap();

// Now use the `data_buffer_ptr` in the `ArgsBuilder` as an argument to the FHE program.

Running the program

Once you have your Args and Memory set up, you can run the program using the FheComputer instance. The run_program method takes in the program to run, the memory to use, and the arguments to pass in. It returns an encrypted result (if applicable) that can be decrypted using the secret key.

let encrypted_result = computer.run_program(
    &program,
    memory,
    arguments,
)?;

Testing the program with plaintext values

When testing your FHE programs, you may want to run them with plaintext values instead of encrypted values to ensure that the logic is correct. You can do this by passing in all plaintext values to the ArgsBuilder and then calling run_program. This will run the program without FHE, allowing you to test the logic of your program without the overhead of encryption and decryption.

let first_value = 5u8;
let second_value = 10i8;
// ... plus all other values you want to pass in

// Encrypted version of the arguments
let encrypted_arguments = ArgsBuilder::new()
    .arg(UInt8::encrypt_secret(first_value, &enc, &secret_key))
    .arg(Int8::encrypt_secret(second_value, &enc, &secret_key))
    //... other unsigned or signed values
    .return_value::<UInt8>();

// Plaintext arguments, useful for debugging or testing
let plaintext_arguments = ArgsBuilder::new()
    .arg(first_value)
    .arg(second_value)
    //... other unsigned or signed values
    .return_value::<u8>();

Decrypting the result

After running the program, you will receive an encrypted result. To decrypt this result, you can use the decrypt method on the encrypted value, passing in the encryption parameters and the secret key.

let result = encrypted_result.decrypt(&enc, &secret_key);

You can also decrypt memory values that were allocated in the FHE memory space. This is useful when you have run a program that produces multiple outputs or modifies data in place.

// Get a [UInt16; 8] array from the memory space that you can decrypt
let data_buffer_encrypted = memory.try_load_type::<[UInt16; 8]>(data_buffer_ptr)?;