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.
- EncryptThe client encrypts each number to the committee key.
- PublishMyProgram.publishInput records each ciphertext.
- ComputeThe program server runs fhe_processor on the ciphertexts.
- DecryptThe committee decrypts only the output.
- The client encrypts each number with the committee public key and calls
MyProgram.publishInput. - 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. - The program runner (
runner/, indevmode) callsfhe_processorand sends the encrypted result back to the coordination server. - 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:
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()
}FHEProcessorInputhas two fields.ciphertextsyields(Vec<u8>, u64)pairs one at a time: the serialized ciphertext and its input index.paramsholds the BFV parameters of the E3.Ciphertext::zeromakes 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:
/// 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 + ∑
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:
let tally = Vec::<u64>::try_decode(&decrypted, Encoding::poly())?;
assert_eq!(tally[0], 10);
Ok(())Run the unit test
cargo test -p e3-user-programCargo 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:
<strong className='font-semibold'>Ready to compute:</strong> {input1} + {input2} = ?Change it to:
<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:
{
submittedInputs
? `${submittedInputs.input1} + ${submittedInputs.input2} = ${result !== null ? result : '…'}`
: '…'
}Change it to:
{
submittedInputs
? `2 × (${submittedInputs.input1} + ${submittedInputs.input2}) = ${result !== null ? result : '…'}`
: '…'
}Start the stack
pnpm dev:allThe 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.publishInputstores a commitment for each input, andMyProgram.verifychecks the compute proof. Neither function contains the computation. - The coordination server.
server/index.tssends 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:alldeploys 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:
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:
| Operation | Operator | Example |
|---|---|---|
| Add two ciphertexts | +, += | &a + &b |
| Subtract two ciphertexts | -, -= | &a - &b |
| Negate a ciphertext | unary - | -&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.