Enclave is now The Interfold.
Build
Hello World

Hello World

This tutorial changes the FHE computation in the default template and runs the E3 again. It is for developers who have completed the Quick Start. At the end, your E3 returns 2 × (a + b) instead of a + b. You also know which files hold the computation, its test, and its labels in the client.

Before you begin

You need the my-first-e3 project from the Quick Start, with no changes. All paths on this page are relative to the project folder.

How the template computes

The FHE computation is one Rust function, fhe_processor, in program/src/lib.rs. The other parts of the template move ciphertexts to this function and move its result to the committee.

  1. EncryptThe client encrypts each number to the committee key.
  2. PublishMyProgram.publishInput records each ciphertext.
  3. ComputeThe program server runs fhe_processor on the ciphertexts.
  4. DecryptThe committee decrypts only the output.
  1. The client encrypts each number with the committee public key and calls MyProgram.publishInput.
  2. When the input window closes, the coordination server (server/index.ts) reads the published inputs and sends them to the program server on port 13151.
  3. The program runner (runner/, in dev mode) calls fhe_processor and sends the encrypted result back to the coordination server.
  4. The coordination server publishes the encrypted result. The committee decrypts it.

This is the default computation in program/src/lib.rs. The doc comment above policy is removed here:

program/src/lib.rs
use e3_compute_provider::{FHEProcessorInput, InputPolicy};
use fhe::bfv::Ciphertext;
use fhe_traits::{DeserializeParametrized, Serialize};
 
pub fn policy() -> InputPolicy {
    InputPolicy::default()
}
 
/// Implementation of the CiphertextProcessor function
pub fn fhe_processor(fhe_inputs: FHEProcessorInput<'_>) -> Vec<u8> {
    let mut sum = Ciphertext::zero(fhe_inputs.params);
    for (bytes, _) in fhe_inputs.ciphertexts {
        let ciphertext = Ciphertext::from_bytes(&bytes, fhe_inputs.params).unwrap();
        sum += &ciphertext;
    }
 
    sum.to_bytes()
}
  • FHEProcessorInput has two fields. ciphertexts yields (Vec<u8>, u64) pairs one at a time: the serialized ciphertext and its input index. params holds the BFV parameters of the E3.
  • Ciphertext::zero makes the zero ciphertext, the start value. The += operator adds each input ciphertext to it. This addition is homomorphic: the function adds the hidden values, but it cannot see them.
  • sum.to_bytes() serializes the encrypted result.
  • policy() returns the input policy. The default policy uses every published input.

The template requests E3s with the insecure-64 parameter set. Its plaintext modulus is 17,000,000, so every result is calculated modulo 17,000,000. For this tutorial, 2 × (a + b) must be less than 17,000,000.

Change the computation

Stop the stack

If pnpm dev:all runs, press Ctrl+C in its terminal.

Double the encrypted sum

In program/src/lib.rs, replace the fhe_processor function with this version:

program/src/lib.rs
/// Implementation of the CiphertextProcessor function
pub fn fhe_processor(fhe_inputs: FHEProcessorInput<'_>) -> Vec<u8> {
    let mut sum = Ciphertext::zero(fhe_inputs.params);
    for (bytes, _) in fhe_inputs.ciphertexts {
        let ciphertext = Ciphertext::from_bytes(&bytes, fhe_inputs.params).unwrap();
        sum += &ciphertext;
    }
 
    // Add the encrypted sum to itself. The hidden value becomes 2 × (a + b).
    let doubled = &sum + &sum;
    doubled.to_bytes()
}

&sum + &sum adds two ciphertexts and returns a new ciphertext. The function needs no new imports.

Update the unit test

The test at the end of program/src/lib.rs encrypts 3 and 2 and calls fhe_processor. Change the expected value from 5 to 10:

program/src/lib.rs
        let tally = Vec::<u64>::try_decode(&decrypted, Encoding::poly())?;
        assert_eq!(tally[0], 10);
        Ok(())

Run the unit test

cargo test -p e3-user-program

Cargo prints test tests::test ... ok. If the test fails, compare your fhe_processor with the code in the previous step.

Update the label on the input step

In client/src/pages/steps/EnterInputs.tsx, find this line:

client/src/pages/steps/EnterInputs.tsx
<strong className='font-semibold'>Ready to compute:</strong> {input1} + {input2} = ?

Change it to:

client/src/pages/steps/EnterInputs.tsx
<strong className='font-semibold'>Ready to compute:</strong> 2 × ({input1} + {input2}) = ?

Update the label on the results step

In client/src/pages/steps/Results.tsx, find this line:

client/src/pages/steps/Results.tsx
{
  submittedInputs
    ? `${submittedInputs.input1} + ${submittedInputs.input2} = ${result !== null ? result : '…'}`
    : '…'
}

Change it to:

client/src/pages/steps/Results.tsx
{
  submittedInputs
    ? `2 × (${submittedInputs.input1} + ${submittedInputs.input2}) = ${result !== null ? result : '…'}`
    : '…'
}

Start the stack

pnpm dev:all

The PROGRAM process runs interfold program start. In dev mode, this command runs cargo run --bin program-runner -- dev, so Cargo compiles the runner again with your new fhe_processor.

Run an E3

Open http://localhost:3000 (opens in a new tab). If the client shows the result of an earlier E3, select Start over in the top bar. Request an E3, then enter 5 and 3.

The input step shows Ready to compute: 2 × (5 + 3) = ?. The results step shows 2 × (5 + 3) = 16.

Why nothing else changes

  • The contract. MyProgram.publishInput stores a commitment for each input, and MyProgram.verify checks the compute proof. Neither function contains the computation.
  • The coordination server. server/index.ts sends every published input to the program server and publishes the output that it gets back. It does not know what the computation does.
  • The deployment. pnpm dev:all deploys the contracts again on a new local chain at each start.

In dev mode, the program server runs fhe_processor without a proof, and the template deploys MockOpenVmReceiptVerifier, which accepts every receipt on the local chain. With real proofs, MyProgram.verify checks the OpenVM receipt against the imageId that MyProgram receives when it is deployed. A change to the program changes that identity, so compile and deploy again. See Compute provider.

Update the integration test

The template has an end-to-end test in tests/integration.spec.ts. It encrypts 1 and 2 and expects their sum. After your change, update the expected value:

tests/integration.spec.ts
expect(BigInt(result)).toBe(2n * (num1 + num2))

Run the test with pnpm test:integration. This test needs a CLI that is built with the test-only-skip-proof-aggregation Cargo feature. Of the release archives, only the test-only archive for Linux x86_64 has it. See Installation for this archive and for a source build.

Operations on ciphertexts

The template uses the fhe crate from gnosisguild/fhe.rs at tag v0.6.0. For a Ciphertext, that crate implements these operators:

OperationOperatorExample
Add two ciphertexts+, +=&a + &b
Subtract two ciphertexts-, -=&a - &b
Negate a ciphertextunary --&a
Add or subtract a plaintext+, +=, -, -=&a + &plaintext
Multiply by a plaintext*, *=&a * &plaintext

A Plaintext must use the same parameters object as the ciphertext, that is fhe_inputs.params. If the parameters objects are different, the operator panics.

All results wrap modulo the plaintext modulus. A negative result, for example from &a - &b with a < b, wraps to a large value. The crate also implements * for two ciphertexts. The default template does not use it, and this page does not cover it.

Next steps