/`, with one directory for each
+ variant (`default`, `recursive`, `evm`). Then it writes `SHA256SUMS` and `checksums.json`.
+
+The build records the active pair in `circuits/bin/.active-preset.json`.
+
+
+ The preset and the committee size stay selected after the build, and they change tracked files.
+ Always switch them with `pnpm build:circuits --preset --committee `. Do not edit
+ `active.nr`, `utils.ts`, or a `parity_*.nr` file by hand. The pre-push hook runs
+ `pnpm check:committee`, which rejects files that disagree. Circuit artifacts and verifiers are
+ specific to the pair, so a changed H needs new circuits and new verifiers.
+
+
+Other circuit commands:
+
+| Command | What it does |
+| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
+| `pnpm build:circuits --dry-run` | Lists the circuits that a build compiles, and stops. |
+| `pnpm build:circuits --group dkg --skip-vk` | Compiles one group and skips the verification keys. |
+| `pnpm build:circuits hash` | Prints the source hash that identifies a build. |
+| `pnpm build:circuits sync-config --preset --committee ` | Updates `utils.ts` and `ActiveCryptoConfig.sol` only. It does not compile. |
+| `pnpm check:committee` | Checks that the committee and BFV values agree across Noir, Rust, TypeScript, and Solidity. |
+| `pnpm noir:lint` (`scripts/lint-circuits.sh`) | Runs `nargo fmt --check` and `nargo check` on the library and each package. It exits with success if `nargo` is missing. |
+| `pnpm noir:test` (`scripts/test-circuits.sh`) | Runs `nargo test` on `circuits/lib` and `decryption_aggregator`, then the `dkg_aggregator` tests for each preset and committee. |
+
+To make sample inputs for one circuit, use the `zk_cli` generator. Its `--committee` value must
+match the active committee of the library:
+
+```bash
+cargo run -p e3-zk-helpers --bin zk_cli -- --circuit pk-generation --preset insecure --committee minimum --toml
+```
-The generated circuit artifacts and Solidity verifiers are specific to both choices. Changing H
-requires rebuilding every affected circuit and verifier; it is not a runtime-only setting.
+`zk_cli --list-circuits` prints the circuit names that it accepts.
-Generate per-circuit witness/config artifacts with `zk_cli` (must pass matching `--committee`):
+## Generate the Solidity verifiers
+
+The Honk verifiers for `dkg_aggregator` and `decryption_aggregator` are committed under
+`packages/interfold-contracts/contracts/verifiers/bfv/honk/`. The files at the top of that directory
+are for `insecure-512` with `minimum`. Every other pair has its own directory,
+`honk///`.
```bash
-cargo run -p e3-zk-helpers --bin zk_cli -- --circuit pk-generation --preset insecure --committee small --toml
+pnpm build:circuits --preset insecure-512 --committee minimum
+pnpm generate:verifiers # rewrite the committed verifiers
+pnpm check:verifiers # compare them with the current keys, write nothing
```
-`lint-circuits.sh` and `test-circuits.sh` exit successfully when **nargo** is missing so
-optional-Noir CI stays green.
+`generate:verifiers` compiles each circuit if necessary, runs `bb write_vk -t evm` and
+`bb write_solidity_verifier`, and renames the contract from `HonkVerifier` to a name such as
+`DkgAggregatorVerifier`. It refuses to run when the build stamp in `dist/circuits` or
+`circuits/bin/.active-preset.json` does not match the requested pair. For another pair, add
+`--preset --committee `.
-## Solidity verifiers (example: CRISP)
+Each generated verifier has this function, the same as `ICircuitVerifier`
+(`contracts/interfaces/ICircuitVerifier.sol`):
-The `@interfold/sdk` `generateProof()` helper embeds user-data-encryption circuits built with
-`--preset insecure-512 --committee minimum`. It is a convenience for that path only. For on-chain
-Honk verification, compile your own UDE, app, and fold circuits for the preset and committee your
-deployment uses — see
-[Proving: embedded circuits or your own](./sdk#proving-embedded-circuits-or-your-own).
+```solidity
+function verify(
+ bytes calldata _proof,
+ bytes32[] calldata _publicInputs
+) external view returns (bool);
+```
-Projects such as CRISP compile circuits with **bb** and emit a verifier contract. Example
-entrypoint:
+## Verifiers for your own circuits
-```bash
-cd examples/CRISP
-./scripts/compile_circuits.sh
+An E3 program can check its inputs with its own circuits. CRISP does this in
+`examples/CRISP/scripts/compile_circuits.sh`:
+
+1. It compiles `user_data_encryption_ct0`, `user_data_encryption_ct1`, and `user_data_encryption`
+ from `circuits/bin/threshold`.
+2. It writes their verification keys with `bb write_vk -t noir-recursive-no-zk`.
+3. It compiles the application circuit and writes its key the same way.
+4. It compiles the fold circuit, which verifies both proofs, and writes its key with
+ `bb write_vk --oracle_hash keccak`.
+5. It runs `bb write_solidity_verifier` on the fold key and copies the result to
+ `packages/crisp-contracts/contracts/verifiers/`.
+
+The script makes two verifiers: `CRISPVerifier.sol` from the `crisp` and `fold` circuits, and
+`CRISPOnchainVerifier.sol` from `crisp_onchain` and `fold_onchain`. The verifiers do not depend on
+the BFV preset. The fold circuit accepts the key hashes of both presets.
+
+The [custom circuits tutorial](/build/tutorials/custom-zk-circuits) follows these steps for a new
+circuit.
+
+The SDK function `generateProof()` proves only the three user-data-encryption circuits. It embeds
+artifacts built for `insecure-512` with `minimum`. See the [SDK](/build/sdk) page.
+
+## Use circuits/lib in another Noir project
+
+Add the library as a dependency in your `Nargo.toml`. Use the release tag that matches the prover
+and the circuits that your deployment uses. CRISP uses the name `interfold_lib`:
+
+```toml filename="Nargo.toml"
+[dependencies]
+interfold_lib = { git = "https://github.com/theinterfold/interfold", tag = "v0.18.0", directory = "circuits/lib" }
```
-That flow typically: compile dependency circuits (`user_data_encryption_ct0` / `ct1` / wrapper),
-compile the app-specific and fold circuits, `bb write_vk` (e.g. `--oracle_hash keccak` on the fold
-target), `bb write_solidity_verifier`, then copy/format into the package contracts. Adapt paths for
-your own fold root and output dir.
+Then import from it by that name:
-On-chain verification matches the usual pattern:
+```noir
+use interfold_lib::configs::default::threshold::{L, N};
+use interfold_lib::math::commitments::compute_ciphertext_commitment;
+use interfold_lib::math::polynomial::Polynomial;
+```
-```solidity
-interface ICircuitVerifier {
- function verify(
- bytes calldata _proof,
- bytes32[] calldata _publicInputs
- ) external view returns (bool);
-}
+
+ The library at the tag `v0.18.0` selects `insecure-512` with `minimum`. The build script writes
+ that selection into the source. For another pair, use a path dependency on a local checkout.
+
+
+To use another pair, run `pnpm build:circuits` for the pair in the checkout, then point the
+dependency at it:
+
+```toml filename="Nargo.toml"
+[dependencies]
+interfold_lib = { path = "/circuits/lib" }
```
+The library itself depends on `poseidon` `v0.3.0` and `keccak256` `v0.1.2` from `noir-lang`, and on
+`bignum` `v0.4.1` from `gnosisguild/noir-bignum`.
+
## Rust integration
-- **`e3-zk-prover`** (`crates/zk-prover`) — witness wiring, `bb prove` / `bb verify`, artifact paths
- aligned with `interfold noir setup`.
-- **Events / API** — `CircuitName`, `ProofType`, and request payloads are defined next to the node.
+The crate `e3-zk-prover` (`crates/zk-prover`) runs the circuits for the ciphernode.
+
+| Item | Role |
+| ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
+| `ZkBackend` | Finds `bb` and the circuit directory. `check_status`, `ensure_installed`, `download_bb`, and `download_circuits` implement `interfold noir`. |
+| `ZkProver` | Runs `bb prove` and `bb verify` as a subprocess. It reads artifacts from `////`. |
+| `Provable` | The trait for one circuit. It names the `CircuitName` and builds the witness inputs. |
+| `CircuitVariant` (`e3-events`) | `Default` (`noir-recursive-no-zk`), `Recursive` (`noir-recursive`), and `Evm` (`evm`). These are the directory names in the archive. |
+| `ProofRequestActor`, `ProofVerificationActor`, `ShareVerificationActor` | The actors that request proofs and check the proofs of other ciphernodes. |
+
+The event types (`CircuitName`, `ProofType`) are in `crates/events`. The protocol flow that uses
+them is in
+[`agent/flow-trace/04_DKG_AND_COMPUTATION.md`](https://github.com/theinterfold/interfold/blob/main/agent/flow-trace/04_DKG_AND_COMPUTATION.md).
-When you add or rename a circuit package, update root **`package.json`** / CI scripts that call
-`build-circuits.ts` or `lint-circuits.sh` so dev and deployment stay in sync with Noir sources.
+## Add or rename a circuit
+
+1. Put the package in `bin/dkg`, `bin/threshold`, or `bin/recursive_aggregation`. The build script
+ finds packages in these groups automatically.
+2. Add the package to the `members` list of the group workspace, for `bin/dkg` and `bin/threshold`.
+3. Add the directory to `DIRS` in `scripts/lint-circuits.sh`, for a package in
+ `bin/recursive_aggregation`.
+4. Add a `CircuitName` variant in `crates/events` if the Rust prover must run the circuit.
+5. Rebuild each pair that you ship, and regenerate the verifiers if the circuit goes on-chain.
+
+## Next steps
+
+
+
+ Write an input-validation circuit, make its verifier, and call it from an E3 program.
+
+
+ The PV-TBFV phases and what each circuit proves.
+
+
+ Encryption functions and the embedded user-data-encryption prover.
+
+
diff --git a/docs/pages/build/tutorials/custom-zk-circuits.mdx b/docs/pages/build/tutorials/custom-zk-circuits.mdx
index d934a4c35c..04c6e58117 100644
--- a/docs/pages/build/tutorials/custom-zk-circuits.mdx
+++ b/docs/pages/build/tutorials/custom-zk-circuits.mdx
@@ -1,269 +1,598 @@
---
title: 'Custom Noir Circuits for Input Validation'
description:
- 'How CRISP uses Noir zero-knowledge circuits to prove vote validity on-chain — circuit structure,
- proof composition, and on-chain verification'
+ 'Write a Noir circuit that checks each encrypted input, fold it with the user-data-encryption
+ proof, generate a Solidity verifier, and call the verifier from an E3 program contract.'
---
-# Custom Noir Circuits for Input Validation
-
-Every E3 program must validate that user inputs are correctly encrypted under the committee's
-threshold public key — this is the baseline. On top of that, programs can add application-specific
-checks. CRISP, for example, also proves that each vote is valid (within the voter's balance), that
-the voter is eligible (Merkle membership), and for mask votes that the ciphertext is an encryption
-of zero correctly added to the previous slot ciphertext. All without revealing the vote itself.
-
-This tutorial walks through how CRISP's Noir circuits work, as a template for building your own
-input validation circuits.
-
-> **What you'll learn:** How CRISP composes several Noir circuits into a single on-chain-verifiable
-> proof, which library modules are reusable, and how to wire a custom circuit into `publishInput`.
->
-> **Prerequisites:** Basic familiarity with [Noir](https://noir-lang.org/docs) and the
-> [E3 program structure](./write-e3-program).
-
----
+import { Callout, FileTree, Steps } from 'nextra/components'
+import { Flow, LinkCard, LinkCards } from '@/components/ui'
-## What the Circuits Prove
-
-CRISP's circuit system proves one relation for every submission, and four things hold on every path:
-
-1. **Voter eligibility** — the slot's address is in a Merkle tree of eligible participants
-2. **Valid encryption** — the ballot ciphertext is a well-formed BFV encryption
-3. **Correct ciphertext addition** — the published ciphertext is `addend + ballot`, computed
- correctly
-4. **A well-formed plaintext** — every coefficient of the ballot is constrained, not only the ones
- the vote rules read
+# Custom Noir Circuits for Input Validation
-What changes between the two cases is the addend and the authorisation:
+An E3 program contract cannot read the plaintext of an input. If the program must reject bad inputs,
+the submitter must attach a zero-knowledge proof. This tutorial adds that proof to the default
+template: each input must encrypt 0 or 1, so the sum is a count of "yes" answers. The steps follow
+what CRISP does in `examples/CRISP/circuits` and `packages/crisp-contracts`.
+
+At the end, you have two Noir circuits, a generated Solidity verifier, and a `publishInput` that
+rejects an input without a valid proof.
+
+**Prerequisites:**
+
+- A project from the default template. See
+ [Build a Complete E3 Program](/build/tutorials/write-e3-program).
+- `nargo` `1.0.0-beta.26` and `bb` `5.1.0` on your `PATH`. See
+ [Noir Circuits](/build/noir-circuits).
+- A checkout of the Interfold repository at the tag `v0.18.0`, with Rust installed. You compile the
+ user-data-encryption circuits and run one Rust tool from it.
+
+## What the proof shows
+
+The final proof combines three proofs. The Interfold repository supplies the first two circuits. You
+write the last two.
+
+| Circuit | Source | Proves |
+| ------------------------------------------------------ | ------------------------ | --------------------------------------------------------------------------------------------- |
+| `user_data_encryption_ct0`, `user_data_encryption_ct1` | `circuits/bin/threshold` | Each part of the BFV ciphertext is a correct encryption under the committee key. |
+| `user_data_encryption` | `circuits/bin/threshold` | Both part proofs are valid and use the same randomness. |
+| `binary_input` | Your project | The plaintext is 0 or 1. |
+| `binary_input_fold` | Your project | Both proofs describe the same ciphertext and plaintext, and each circuit is the expected one. |
+
+The circuits connect through commitments. `user_data_encryption` outputs five public values:
+`ct0_key_hash`, `ct1_key_hash`, `pk_commitment`, `ct_commitment`, and `k1_commitment`. Your input
+circuit computes `ct_commitment` and `k1_commitment` from the same ciphertext and plaintext. The
+fold circuit checks that the values are equal.
+
+
+
+In CRISP, the `crisp` circuit has the role of `binary_input`, and `fold` has the role of
+`binary_input_fold`. The CRISP rules are larger (eligibility, signatures, and ciphertext addition),
+but the structure is the same.
+
+## Project layout
+
+Add a `circuits` directory to the project. The verifier goes in `contracts/verifiers`.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+### Write the input circuit
+
+Create `circuits/binary_input/Nargo.toml`. The library at `v0.18.0` uses the `insecure-512` preset,
+which the template requests (`paramSet: 0`).
+
+```toml filename="circuits/binary_input/Nargo.toml"
+[package]
+name = "binary_input"
+type = "bin"
+
+[dependencies]
+interfold_lib = { git = "https://github.com/theinterfold/interfold", tag = "v0.18.0", directory = "circuits/lib" }
+```
-**Actual vote** — the addend is the zero ciphertext, so the ballot replaces what the slot holds.
-Additionally proves:
+Create `circuits/binary_input/src/main.nr`:
+
+```noir filename="circuits/binary_input/src/main.nr"
+use interfold_lib::configs::default::threshold::{
+ L, N, Q_MOD_T_CENTERED, USER_DATA_ENCRYPTION_BIT_CT, USER_DATA_ENCRYPTION_BIT_K,
+};
+use interfold_lib::math::{
+ commitments::{
+ compute_ciphertext_commitment, compute_single_polynomial_commitment,
+ DS_USER_DATA_ENCRYPTION_COMMITMENT,
+ },
+ polynomial::Polynomial,
+};
+
+/// Proves that a ciphertext encrypts 0 or 1.
+///
+/// Returns the ciphertext commitment and the plaintext commitment. The fold circuit compares both
+/// values with the outputs of the user_data_encryption proof.
+fn main(
+ ct0is: [Polynomial; L],
+ ct1is: [Polynomial; L],
+ k1: Polynomial,
+) -> pub (Field, Field) {
+ let ct_commitment =
+ compute_ciphertext_commitment::(ct0is, ct1is);
+ let k1_commitment = compute_single_polynomial_commitment::(
+ k1,
+ DS_USER_DATA_ENCRYPTION_COMMITMENT,
+ );
+
+ // `k1` holds the message coefficients in descending order. `encryptNumber` writes the value
+ // into message coefficient 0, which is `k1.coefficients[N - 1]`. All other coefficients are 0.
+ for i in 0..N - 1 {
+ assert(k1.coefficients[i] == 0);
+ }
+
+ // A message bit of 1 appears in `k1` as `Q_MOD_T_CENTERED`. A bit of 0 appears as 0.
+ let value = k1.coefficients[N - 1];
+ assert(value * (Q_MOD_T_CENTERED - value) == 0);
+
+ (ct_commitment, k1_commitment)
+}
+```
-1. **Correct signature** — the submission was signed by the voter (ECDSA recovery)
-2. **Within balance** — the vote amount does not exceed the voter's token balance
+The input `k1` is the plaintext polynomial scaled to `[q * m]_t` (`k1 = [q*m]_t` in
+`crates/zk-helpers/src/circuits/threshold/user_data_encryption/computation.rs`). CRISP uses the same
+test for each vote bit in `check_coefficient_values_with_balance`. The two commitment functions are
+the ones that `user_data_encryption` uses, so equal inputs give equal outputs.
-**Mask vote** (for
-[receipt-freeness](https://blog.theinterfold.com/vote-masking-receipt-freeness-secret-ballots/)) —
-the addend is the ciphertext already in the slot, so the vote there is unchanged. Additionally
-proves:
+Check the circuit:
-1. **Encryption of zero** — the plaintext is zero in every coefficient, so a mask can neither change
- the tally nor carry a payload
+```bash
+cd circuits/binary_input && nargo check
+```
-No signature is checked on the mask path, which is what lets a third party mask a slot at all. The
-flag that chooses between the two is private, so the two cases are not distinguishable once
-published.
+The command exits without an error.
----
+### Write the fold circuit
-## Circuit Structure
+Create `circuits/binary_input_fold/Nargo.toml`. The fold verifies proofs, so it also needs
+`bb_proof_verification` at the `bb` version:
-CRISP's circuits live in `examples/CRISP/circuits/` and are organized into a main circuit plus
-reusable library modules:
+```toml filename="circuits/binary_input_fold/Nargo.toml"
+[package]
+name = "binary_input_fold"
+type = "bin"
-```text
-circuits/
-├── bin/crisp/src/main.nr # Main circuit — orchestrates all checks
-└── lib/src/
- ├── merkle_tree.nr # Poseidon-based Merkle membership proof (depth 20)
- ├── ecdsa.nr # secp256k1 signature validation + Ethereum address derivation
- ├── ciphertext_addition.nr # Homomorphic addition verification (Schwartz-Zippel)
- ├── utils.nr # Vote validation (binary coefficients, balance checks)
- └── constants.nr # FHE parameters (N, L, Q, bit-widths)
+[dependencies]
+interfold_lib = { git = "https://github.com/theinterfold/interfold", tag = "v0.18.0", directory = "circuits/lib" }
+bb_proof_verification = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v5.1.0", directory = "barretenberg/noir/bb_proof_verification" }
```
-### The Main Circuit
+Create `circuits/binary_input_fold/src/main.nr`. It follows `examples/CRISP/circuits/bin/fold`:
-The main circuit (`bin/crisp/src/main.nr`) takes all inputs and orchestrates the verification:
+```noir filename="circuits/binary_input_fold/src/main.nr"
+use bb_proof_verification::{UltraHonkProof, UltraHonkVerificationKey, verify_honk_proof_non_zk};
+use interfold_lib::math::commitments::compute_vk_hash;
-```noir
-fn main(
- // Ciphertext components
- prev_ct0is: [Polynomial; L], // Previous ciphertext (for updates)
- prev_ct1is: [Polynomial; L],
- prev_ct_commitment: pub Field,
- sum_ct0is: [Polynomial; L], // Sum after addition
- sum_ct1is: [Polynomial; L],
- ct0is: [Polynomial; L], // New vote ciphertext
- ct1is: [Polynomial; L],
+/// The hash of the four inner verification keys. Set it from the compute-vk-hash output.
+pub global EXPECTED_KEY_HASH: Field = 0;
- // ECDSA signature
- public_key_x: [u8; 32],
- public_key_y: [u8; 32],
- signature: [u8; 64],
- hashed_message: [u8; 32],
-
- // Merkle proof of eligibility
- merkle_root: pub Field,
- merkle_proof_indices: [u1; 20],
- merkle_proof_siblings: [Field; 20],
-
- // Vote metadata
- slot_address: pub Field,
- balance: Field,
- is_first_vote: pub bool,
- is_mask_vote: bool,
- num_options: pub u32,
-) -> pub (Field, Field, Field) // (final_ct_commitment, ct_commitment, k1_commitment)
-```
-
-The circuit proves one relation for every operation — voting, changing a vote, and masking:
-
-```
-published ciphertext = addend + ballot ciphertext
+fn main(
+ // The user_data_encryption proof. Its public inputs are
+ // [ct0_key_hash, ct1_key_hash, pk_commitment, ct_commitment, k1_commitment].
+ user_data_encryption_verification_key: UltraHonkVerificationKey,
+ user_data_encryption_proof: UltraHonkProof,
+ user_data_encryption_public_inputs: [Field; 5],
+ user_data_encryption_key_hash: Field,
+ // The binary_input proof. Its public inputs are [ct_commitment, k1_commitment].
+ binary_input_verification_key: UltraHonkVerificationKey,
+ binary_input_proof: UltraHonkProof,
+ binary_input_key_hash: Field,
+ ct_commitment: pub Field,
+ k1_commitment: Field,
+) -> pub Field {
+ verify_honk_proof_non_zk(
+ user_data_encryption_verification_key,
+ user_data_encryption_proof,
+ user_data_encryption_public_inputs,
+ user_data_encryption_key_hash,
+ );
+ verify_honk_proof_non_zk(
+ binary_input_verification_key,
+ binary_input_proof,
+ [ct_commitment, k1_commitment],
+ binary_input_key_hash,
+ );
+
+ // Both proofs must describe the same ciphertext and the same plaintext.
+ assert(ct_commitment == user_data_encryption_public_inputs[3]);
+ assert(k1_commitment == user_data_encryption_public_inputs[4]);
+
+ // Pin the four inner circuits. Without this check, a prover can supply any circuit.
+ let vk_hashes = [
+ user_data_encryption_key_hash,
+ binary_input_key_hash,
+ user_data_encryption_public_inputs[0],
+ user_data_encryption_public_inputs[1],
+ ]
+ .as_vector();
+ assert(compute_vk_hash(vk_hashes) == EXPECTED_KEY_HASH);
+
+ // The committee public key commitment. The E3 program compares it with e3.committeePublicKey.
+ user_data_encryption_public_inputs[2]
+}
```
-`is_mask_vote` chooses the addend and is **private**:
+The public inputs of the fold proof are `ct_commitment` and then the return value, `pk_commitment`.
+The contract must pass them in this order.
-- **Actual vote**: Verifies the signature matches the slot address, checks that the vote amount
- doesn't exceed the voter's balance, and validates Merkle membership. The addend is the zero
- ciphertext, so a vote replaces what the slot holds rather than adding to it
-- **Mask vote**: Verifies a zero-value ballot, in every plaintext coefficient. The addend is the
- ciphertext already in the slot, so the vote there is unchanged. Anyone can submit mask votes to
- any slot, which provides
- [receipt-freeness](https://blog.theinterfold.com/vote-masking-receipt-freeness-secret-ballots/) —
- voters cannot prove which ciphertext is theirs
+
+ Do not remove the key-hash check. `verify_honk_proof_non_zk` accepts any verification key that the
+ prover supplies. The check against `EXPECTED_KEY_HASH` is what makes the fold accept only your
+ circuits.
+
-Because both branches return the same commitment and publish the same shape of ciphertext, an
-observer cannot tell them apart. Doing so would mean distinguishing a fresh BFV ciphertext from a
-sum, which the encryption scheme hides.
+### Compile the inner circuits and write their keys
-### Library Modules
+In the Interfold checkout, compile the three user-data-encryption circuits. They share the workspace
+target `circuits/bin/threshold/target`:
-**`merkle_tree.nr`** — Poseidon-based Merkle membership proof with a 20-level tree. Proves that a
-leaf (voter address) exists at a specific index without revealing other leaves.
-
-**`ecdsa.nr`** — secp256k1 ECDSA signature verification and Ethereum address derivation. Recovers
-the signer address from the signature and verifies it matches the expected voter.
-
-**`ciphertext_addition.nr`** — Proves that `sum = addend + new` for BFV ciphertexts. Asserted on
-every path, not only for masks: the addend is the slot's ciphertext for a mask and the zero
-ciphertext otherwise, so the same relation covers a vote, a re-vote, and a mask. Uses the
-Schwartz-Zippel lemma with a Fiat-Shamir challenge point to verify the polynomial identity at a
-single random point, avoiding the cost of checking every coefficient.
-
-**`utils.nr`** — Vote validation helpers. Checks that vote coefficients are binary (0 or 1) and that
-the total doesn't exceed the voter's balance.
+```bash
+cd /circuits/bin/threshold
+(cd user_data_encryption_ct0 && nargo compile)
+(cd user_data_encryption_ct1 && nargo compile)
+(cd user_data_encryption && nargo compile)
+```
----
+Write a `noir-recursive-no-zk` key for each one. `bb` writes `vk` and `vk_hash`, so rename them
+before the next call, as `compile_circuits.sh` does:
-## Proof Composition
+```bash
+T=/circuits/bin/threshold/target
+for name in user_data_encryption_ct0 user_data_encryption_ct1 user_data_encryption; do
+ bb write_vk -b "$T/$name.json" -o "$T" -t noir-recursive-no-zk
+ mv "$T/vk" "$T/$name.vk_recursive"
+ mv "$T/vk_hash" "$T/$name.vk_recursive_hash"
+done
+```
-A single vote submission requires proving multiple things, which means multiple circuit executions
-composed together. CRISP uses recursive proof folding to produce a single EVM-verifiable proof:
+In your project, compile `binary_input` and write its key the same way:
-```text
-ct0 circuit ─→ user_data_encryption ─┐
-ct1 circuit ─→ user_data_encryption ─┤
- ├─→ crisp circuit ─→ fold ─→ Single proof
-merkle witness (verified inside crisp) ─┤
-ecdsa witness (verified inside crisp) ──┘
+```bash
+cd circuits/binary_input
+nargo compile
+bb write_vk -b target/binary_input.json -o target -t noir-recursive-no-zk
+mv target/vk target/binary_input.vk_recursive
+mv target/vk_hash target/binary_input.vk_recursive_hash
```
-The `ct0` and `ct1` circuits verify encryption of each ciphertext component. The main `crisp`
-circuit takes those results plus the Merkle membership witness and ECDSA signature as private
-inputs, verifying eligibility and signature correctness internally. The `fold` circuit recursively
-aggregates everything into a single proof that can be verified on-chain.
+
+ The checkout must select `insecure-512` with `minimum`, as the tag does. If you ran `pnpm
+ build:circuits` for another pair, the user-data-encryption circuits no longer match the library
+ that your circuits use.
+
-The TypeScript SDK orchestrates this in `packages/crisp-sdk/src/vote.ts`:
+### Pin the key hash
-```typescript
-// 1. Generate circuit inputs (BFV encryption + Merkle proof + signature)
-const { circuitInputs, encryptedVote } = await generateCircuitInputs(proofInputs)
+From the root of the Interfold checkout, hash the four key hashes. The order must match the order in
+the fold circuit:
-// 2. Execute circuits sequentially, each building on the last
-const ct0Result = await executeCircuit(ct0Circuit, ct0Inputs)
-const ct1Result = await executeCircuit(ct1Circuit, ct1Inputs)
-const crispResult = await executeCircuit(crispCircuit, crispInputs)
-const foldResult = await executeCircuit(foldCircuit, foldInputs)
+```bash
+cargo run -q -p e3-zk-helpers --bin compute-vk-hash -- \
+ circuits/bin/threshold/target/user_data_encryption.vk_recursive_hash \
+ /circuits/binary_input/target/binary_input.vk_recursive_hash \
+ circuits/bin/threshold/target/user_data_encryption_ct0.vk_recursive_hash \
+ circuits/bin/threshold/target/user_data_encryption_ct1.vk_recursive_hash
+```
-// 3. Generate the final Honk proof
-const proof = await honkBackend.generateProof(foldResult.witness)
+The tool prints one field value as 32-byte hexadecimal. Set `EXPECTED_KEY_HASH` in
+`circuits/binary_input_fold/src/main.nr` to this value. Then compile the fold:
-// 4. Encode for Solidity
-const solidityProof = encodeSolidityProof(proof, encryptedVote)
+```bash
+cd circuits/binary_input_fold && nargo compile
```
-The Barretenberg WASM API (`@aztec/bb.js`) handles the UltraHonk proof generation. The SDK caches
-the API instance and SRS (Structured Reference String) to avoid re-initialisation on every proof.
-
----
+Each change to `binary_input` changes its key. Repeat this step after each change.
-## On-chain Verification
+### Generate the Solidity verifier
-The final proof is verified on-chain in `CRISPProgram.publishInput()` using an auto-generated Honk
-verifier contract (`contracts/verifiers//CRISPVerifier.sol`, one per BFV preset because it
-encodes the circuit's verification key). This verifier is generated by Barretenberg and contains the
-verification key baked in.
+Write the fold key for the EVM, then generate the verifier. These are the commands in
+`examples/CRISP/scripts/compile_circuits.sh`:
-```solidity
-// In publishInput():
-honkVerifier.verify(proof, publicInputs);
+```bash
+cd circuits/binary_input_fold
+bb write_vk -b target/binary_input_fold.json -o target --oracle_hash keccak
+mkdir -p ../../contracts/verifiers
+bb write_solidity_verifier -k target/vk -o ../../contracts/verifiers/BinaryInputVerifier.sol
```
-The public inputs include:
-
-- `prev_ct_commitment` — commitment to the previous ciphertext in the slot
-- `digest_hi` and `digest_lo` — the two field-sized halves of the ballot authorisation digest
-- `slot_address` — which vote slot is being written to
-- `merkle_root` or `voting_power` — the eligibility value for the round's census mode
-- `is_first_vote` — whether this input names an earlier entry in the slot
-- `num_options` — number of voting options
-- `sum_ct_commitment` — commitment to the encrypted vote that becomes the new slot value
-- `committee_public_key_commitment` — the key under which the vote must be encrypted
-
-The large encrypted vote is not a Noir public input and is not placed in this Ethereum transaction.
-`publishInput()` commits its Keccak hash together with `sum_ct_commitment` and the slot metadata.
-`finalizeInput()` later accepts only a VectorX proof that the same hash was published to Avail. The
-RISC Zero tally program retrieves those bytes and recomputes both the Keccak hash and the BFV
-commitment before it includes the vote.
-
----
+The generated file declares a contract named `HonkVerifier` and the libraries `ZKTranscriptLib` and
+`RelationsLib`. The deployment must link the two libraries.
+
+CRISP also changes each generated verifier with `patch_verifier` in `compile_circuits.sh`. The patch
+corrects the `ModExpFailed()` error selector that `bb` emits, and it makes the constructor check the
+key sizes. Copy that function if you want the same changes.
+
+### Call the verifier from publishInput
+
+Change `contracts/MyProgram.sol`. The contract stores the verifier, and `publishInput` decodes a
+third field, the proof. This version also adds the stage and window checks that `CRISPProgram`
+makes.
+
+```solidity filename="contracts/MyProgram.sol"
+import { ICircuitVerifier } from '@interfold/contracts/contracts/interfaces/ICircuitVerifier.sol';
+
+contract MyProgram is IE3Program, IE3ProgramDataAvailability, IERC165, Ownable {
+ // ...
+ ICircuitVerifier public immutable inputVerifier;
+
+ error KeyNotPublished(uint256 e3Id);
+ error InputWindowNotOpen(uint256 e3Id);
+ error InvalidInputProof();
+
+ constructor(
+ IInterfold _interfold,
+ IRiscZeroVerifier _verifier,
+ ICircuitVerifier _inputVerifier,
+ bytes32 _imageId
+ ) Ownable(msg.sender) {
+ require(address(_verifier) != address(0), VerifierAddressZero());
+ require(address(_inputVerifier) != address(0), VerifierAddressZero());
+ inputVerifier = _inputVerifier;
+ // ... the rest of the template constructor
+ }
+
+ function publishInput(uint256 e3Id, bytes memory data) external override {
+ E3 memory e3 = interfold.getE3(e3Id);
+
+ if (interfold.getE3Stage(e3Id) != IInterfold.E3Stage.KeyPublished) revert KeyNotPublished(e3Id);
+ if (block.timestamp < e3.inputWindow[0]) revert InputWindowNotOpen(e3Id);
+ if (block.timestamp > e3.inputWindow[1]) revert InputDeadlineReached();
+
+ (bytes memory ciphertext, bytes32 ciphertextCommitment, bytes memory proof) = abi.decode(
+ data,
+ (bytes, bytes32, bytes)
+ );
+ if (ciphertext.length == 0) revert EmptyInputData();
+
+ // The order must match the public inputs of the fold circuit.
+ bytes32[] memory publicInputs = new bytes32[](2);
+ publicInputs[0] = ciphertextCommitment;
+ publicInputs[1] = e3.committeePublicKey;
+ if (!inputVerifier.verify(proof, publicInputs)) revert InvalidInputProof();
+
+ uint256 index = inputs[e3Id].numberOfLeaves;
+ inputs[e3Id]._insert(uint256(ciphertextCommitment));
+
+ emit InputPublished(e3Id, ciphertext, index);
+ }
+ // ...
+}
+```
-## Generating Circuit Inputs from WASM
-
-Circuit inputs require BFV encryption internals (polynomial coefficients, RNS representations) that
-are implemented in Rust. To reuse the same code in the browser, CRISP compiles it to WASM via
-`@crisp-e3/zk-inputs`:
-
-```typescript
-import init from '@crisp-e3/zk-inputs/init'
-import { ZKInputsGenerator } from '@crisp-e3/zk-inputs'
-
-await init()
-const generator = ZKInputsGenerator.withDefaults()
-// `previousCiphertext` is the end of the slot's chain of usable entries, or `undefined` when the
-// slot holds nothing. `keepPrevious` is set only for a mask over an occupied slot: it is what makes
-// the ballot add to that ciphertext rather than replace it.
-const { encryptedVote, inputs } = generator.generateInputs(
- previousCiphertext,
- publicKey,
- voteCoefficients,
- keepPrevious,
+`e3.committeePublicKey` holds the commitment that `CiphernodeRegistryOwnable.publishCommittee` sent
+to the Interfold contract. The fold proof returns the same commitment, so a proof for another
+committee key fails. The value in `publicInputs[0]` is the commitment that the program inserts into
+its input tree.
+
+
+ The proof binds the commitment, not the ciphertext bytes. A submitter can send a valid proof with
+ other bytes. The default input policy of the template (`InputPolicy::default()`) cannot detect
+ this, because its leaf is the commitment alone. The CRISP Secure Process drops an input whose
+ bytes do not reproduce its commitment (`matches_commitment` in
+ `examples/CRISP/program/src/lib.rs`). See [The Secure Process](/build/e3-program/secure-process).
+
+
+### Deploy the verifier with the program
+
+In `deploy/default.ts`, deploy the two libraries and the verifier before `MyProgram`. Use fully
+qualified names, as `examples/CRISP/packages/crisp-contracts/scripts/verifiers.ts` does. Every
+generated verifier declares the same contract names.
+
+```ts filename="deploy/default.ts"
+const source = 'contracts/verifiers/BinaryInputVerifier.sol'
+
+const zkTranscriptLib = await ethers.deployContract(`${source}:ZKTranscriptLib`)
+await zkTranscriptLib.waitForDeployment()
+const relationsLib = await ethers.deployContract(`${source}:RelationsLib`)
+await relationsLib.waitForDeployment()
+
+const inputVerifierFactory = await ethers.getContractFactory(`${source}:HonkVerifier`, {
+ libraries: {
+ [`project/${source}:ZKTranscriptLib`]: await zkTranscriptLib.getAddress(),
+ [`project/${source}:RelationsLib`]: await relationsLib.getAddress(),
+ },
+})
+const inputVerifier = await inputVerifierFactory.deploy()
+await inputVerifier.waitForDeployment()
+
+// Pass the verifier as the new third constructor argument.
+const e3Program = await e3ProgramFactory.deploy(
+ await interfold.getAddress(),
+ await verifier.getAddress(),
+ await inputVerifier.getAddress(),
+ programId,
)
```
-The WASM module handles BFV encryption and produces the exact polynomial decompositions the Noir
-circuits expect. See the
-[crisp-zk-inputs README](https://github.com/theinterfold/interfold/tree/main/examples/CRISP/packages/crisp-zk-inputs)
-for the full API.
-
----
-
-## Building Your Own Circuits
-
-If your E3 program needs custom input validation:
-
-1. **Define what to prove** — eligibility? correct format? value bounds?
-2. **Write Noir circuits** — use CRISP's library modules as building blocks (Merkle proofs, ECDSA,
- ciphertext operations are all reusable)
-3. **Generate a verifier** — compile your circuit with `nargo` and generate the Solidity verifier
- with `bb`
-4. **Call it from `publishInput`** — deploy the verifier and call it in your E3 program contract
-5. **Build a TypeScript SDK** — wrap circuit input generation in a package your frontend can use
-
-For simpler cases (e.g. just checking a value is in range), you may not need custom circuits at all,
-you can reuse the user-data-encryption circuit and the standard Interfold SDK.
+Run `pnpm compile` to regenerate the TypeChain types before you deploy.
+
+### Prove each input in the client
+
+The client makes five proofs: three for the encryption, one for the rule, and the fold. Then it
+encodes the input for `publishInput`. This function is adapted from `generateProof` in
+`examples/CRISP/packages/crisp-sdk/src/vote.ts`.
+
+Before you use it, do these two things:
+
+1. Copy the five compiled JSON files (`user_data_encryption_ct0`, `user_data_encryption_ct1`,
+ `user_data_encryption`, `binary_input`, and `binary_input_fold`) into `client/src/circuits`.
+2. Add `@aztec/bb.js` `5.1.0` and `@noir-lang/noir_js` `1.0.0-beta.26` to `client/package.json`.
+ These are the versions that `@interfold/sdk` uses.
+
+```ts filename="client/src/utils/prove-input.ts"
+import { Barretenberg, UltraHonkBackend } from '@aztec/bb.js'
+import { Noir, type CompiledCircuit } from '@noir-lang/noir_js'
+import type { InterfoldSDK } from '@interfold/sdk'
+import { bytesToHex, encodeAbiParameters, parseAbiParameters } from 'viem'
+import ct0Circuit from '@/circuits/user_data_encryption_ct0.json'
+import ct1Circuit from '@/circuits/user_data_encryption_ct1.json'
+import udeCircuit from '@/circuits/user_data_encryption.json'
+import binaryInputCircuit from '@/circuits/binary_input.json'
+import foldCircuit from '@/circuits/binary_input_fold.json'
+
+const proofToFields = (proof: Uint8Array): string[] => {
+ const fields: string[] = []
+ for (let i = 0; i < proof.length; i += 32) fields.push(bytesToHex(proof.slice(i, i + 32)))
+ return fields
+}
+
+export async function proveBinaryInput(sdk: InterfoldSDK, value: bigint, publicKey: Uint8Array) {
+ const { encryptedData, circuitInputs: c } = await sdk.encryptNumberAndGenInputs(value, publicKey)
+
+ const api = await Barretenberg.new()
+ await api.initSRSChonk(2 ** 21) // the fold circuit needs 2^21 points
+ const inner = { verifierTarget: 'noir-recursive-no-zk' } as const
+
+ const prove = async (circuit: CompiledCircuit, inputs: Record) => {
+ const { witness, returnValue } = await new Noir(circuit).execute(inputs as any)
+ const backend = new UltraHonkBackend(circuit.bytecode, api)
+ const proof = await backend.generateProof(witness, inner)
+ const artifacts = await backend.generateRecursiveProofArtifacts(
+ proof.proof,
+ proof.publicInputs.length,
+ inner,
+ )
+ return { proof, artifacts, returnValue }
+ }
+
+ try {
+ const ct0 = await prove(ct0Circuit as CompiledCircuit, {
+ pk0is: c.pk0is,
+ ct0is: c.ct0is,
+ u: c.u,
+ e0: c.e0,
+ e0is: c.e0is,
+ e0_quotients: c.e0_quotients,
+ k1: c.k1,
+ r1is: c.r1is,
+ r2is: c.r2is,
+ })
+ const ct1 = await prove(ct1Circuit as CompiledCircuit, {
+ pk1is: c.pk1is,
+ ct1is: c.ct1is,
+ u: c.u,
+ e1: c.e1,
+ p1is: c.p1is,
+ p2is: c.p2is,
+ })
+ const ude = await prove(udeCircuit as CompiledCircuit, {
+ ct0_verification_key: ct0.artifacts.vkAsFields,
+ ct0_proof: proofToFields(ct0.proof.proof),
+ ct0_public_inputs: ct0.proof.publicInputs,
+ ct0_key_hash: ct0.artifacts.vkHash,
+ ct1_verification_key: ct1.artifacts.vkAsFields,
+ ct1_proof: proofToFields(ct1.proof.proof),
+ ct1_public_inputs: ct1.proof.publicInputs,
+ ct1_key_hash: ct1.artifacts.vkHash,
+ })
+ const rule = await prove(binaryInputCircuit as CompiledCircuit, {
+ ct0is: c.ct0is,
+ ct1is: c.ct1is,
+ k1: c.k1,
+ })
+ const [ctCommitment, k1Commitment] = rule.returnValue as string[]
+
+ const { witness } = await new Noir(foldCircuit as CompiledCircuit).execute({
+ user_data_encryption_verification_key: ude.artifacts.vkAsFields,
+ user_data_encryption_proof: proofToFields(ude.proof.proof),
+ user_data_encryption_public_inputs: ude.proof.publicInputs,
+ user_data_encryption_key_hash: ude.artifacts.vkHash,
+ binary_input_verification_key: rule.artifacts.vkAsFields,
+ binary_input_proof: proofToFields(rule.proof.proof),
+ binary_input_key_hash: rule.artifacts.vkHash,
+ ct_commitment: String(ctCommitment),
+ k1_commitment: String(k1Commitment),
+ })
+ const fold = await new UltraHonkBackend(
+ (foldCircuit as CompiledCircuit).bytecode,
+ api,
+ ).generateProof(witness, {
+ verifierTarget: 'evm',
+ })
+
+ // publicInputs[0] is ct_commitment, the value that publishInput inserts into its tree.
+ return encodeAbiParameters(parseAbiParameters('bytes, bytes32, bytes'), [
+ bytesToHex(encryptedData),
+ fold.publicInputs[0] as `0x${string}`,
+ bytesToHex(fold.proof),
+ ])
+ } finally {
+ await api.destroy()
+ }
+}
+```
-See [Noir Circuits](/noir-circuits) for toolchain setup and compilation instructions.
+Send the result as `data` in `publishInput(e3Id, data)`. The template encodes inputs in
+`client/src/utils/input.ts` and `server/input.ts` as `(bytes, bytes32)`. Change both files to the
+new format.
+
+
+
+## Test the circuit
+
+Put Noir tests (`#[test]` functions) next to the circuit code and run them with `nargo test` in the
+package. CRISP does this in `examples/CRISP/circuits/lib/src/merkle_tree.nr` and runs the tests with
+`pnpm test:circuits`.
+
+## Compare with CRISP
+
+| Part | This tutorial | CRISP |
+| -------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------- |
+| Rule circuit | `binary_input`: the value is 0 or 1 | `crisp`: census membership, signature, vote rules, and the ciphertext addition |
+| Fold circuit | 2 public inputs | 9 public inputs, from `prev_ct_commitment` to the committee public key |
+| Key-hash check | One constant | Two constants, one for each BFV preset, so one verifier serves both presets |
+| Verifiers | `BinaryInputVerifier.sol` | `CRISPVerifier.sol` (census Merkle tree) and `CRISPOnchainVerifier.sol` (on-chain voting power) |
+| Input flow | One `publishInput` transaction | `publishInput`, then `finalizeInput` after the ciphertext is available on Avail |
+
+For small checks, you do not always need a circuit. A contract can check public data itself, for
+example an allow list of submitters.
+
+## Next steps
+
+
+
+ The circuits workspace, the toolchain, and the verifier scripts.
+
+
+ The full `IE3Program` interface and what each function must check.
+
+
+ A complete E3 application that uses these patterns for private voting.
+
+
+ Deploy the program and its verifier to the testnet.
+
+
diff --git a/docs/pages/build/tutorials/deploy-to-testnet.mdx b/docs/pages/build/tutorials/deploy-to-testnet.mdx
index 8f9b046d17..115d1802cf 100644
--- a/docs/pages/build/tutorials/deploy-to-testnet.mdx
+++ b/docs/pages/build/tutorials/deploy-to-testnet.mdx
@@ -1,220 +1,432 @@
---
title: 'Deploy to Sepolia'
description:
- 'Move your E3 program from local development to the Sepolia testnet — contracts, server
- configuration, and BFV parameter switching'
+ 'Deploy an E3 program contract against the Interfold contracts on Sepolia, get it registered, and
+ point the coordination server and the client at the testnet.'
---
-# Deploy Your E3 to Sepolia
+import { Callout, Steps } from 'nextra/components'
+import { Badge, LinkCard, LinkCards } from '@/components/ui'
-This tutorial takes you from a working local E3 (from [Quick Start](/quick-start) or CRISP) to a
-live deployment on Sepolia testnet.
+# Deploy to Sepolia
-> **What you'll build:** Your E3 program contract deployed to Sepolia, a coordination server pointed
-> at a Sepolia RPC, and a verified end-to-end E3 round against the live testnet ciphernode
-> committee.
->
-> **Prerequisites:** A working local E3, Sepolia ETH for gas, a WebSocket RPC endpoint (e.g.
-> Alchemy, Infura), and your deployer private key funded.
+Testnet
----
+This tutorial moves an E3 program from the local stack to the Sepolia testnet. It is for developers
+who have a working program from the default template. On Sepolia, you use the Interfold contracts
+that the Interfold team deployed and the ciphernodes that operators run. You deploy only your
+program contract. At the end, your program is on Sepolia, the owner of the Interfold contract can
+register it, and your server and client use the testnet.
-## 1. Get Testnet Tokens
+
+ Do not submit real private data on Sepolia. The `insecure-512` preset is not secure, and the
+ manifest marks the Sepolia deployment with `mocks: true`, which means that it records mock
+ verifier contracts.
+
-You'll need:
+
+ On 25 September 2026, the Sepolia protocol contracts predate the node release gate.
+ `nodeReleaseRegistry()` on the Sepolia `Interfold` contract reverts. A 0.18.0 ciphernode reads
+ this value at startup, so it stops with `failed to read Interfold release dependencies`.
+ Ciphernodes on this release cannot serve Sepolia E3s until the Sepolia deployment is upgraded.
+
-- **Sepolia ETH** — for gas. Any public Sepolia faucet works, e.g.
- [sepoliafaucet.com](https://sepoliafaucet.com),
- [Google Cloud faucet](https://cloud.google.com/application/web3/faucet/ethereum/sepolia), or
- [PoW faucet](https://sepolia-faucet.pk910.de).
-- **Test FOLD** — for ciphernode bonding (only if you're running your own nodes). The InterfoldToken
- (FOLD) is deployed at `0x9752444A6a955D420402EaFf5f818afCddb9c340`.
-- **Test USDC** — for E3 fees and tickets. Mock USDC at
- `0xC35B783cA97710be47Fc81D10dADc895EfcD865c`.
+## Sepolia protocol contracts
-> **Faucet:** the quickest way to get both test tokens is the on-chain faucet at
-> `0x6e281411C055BEEbD74bDFcB9aB095aa98907F85`. Run `interfold faucet` (with the `faucet` address
-> set in your config) and it sends a fixed amount of FOLD and mock USDC to your signer. Each token
-> is only topped up if your balance is below the per-token threshold. If the faucet runs dry, reach
-> out on the [community Telegram](https://t.me/enclave_e3).
+These addresses come from `deployments/manifest.json` (chain ID 11155111). The
+[Contract Addresses](/reference/contracts) page has the full list for each network.
-> The full list of deployed Sepolia addresses lives in
+| Contract | Address | Deploy block | Role |
+| ------------------------------ | -------------------------------------------- | ------------ | ---------------------------------------------------------- |
+| `Interfold` | `0x3E856E24c7a95d0e04d387f847DA6FA9f6F6c20C` | 11534993 | Requests, stages, fees, and outputs of each E3 |
+| `CiphernodeRegistry` | `0x374F4542eC634d5437Dd65020781A9D9Df9c2AB8` | 11534965 | `CiphernodeRegistryOwnable`: committees and committee keys |
+| `BondingRegistry` | `0x90250Dc48CBe109fFaA02AeAFbFBdbF12D7BD4d4` | 11534966 | Operator bonds and tickets |
+| `SlashingManager` | `0x489FB674B143E48eaCAaC999B3DF9EA6E95eA79f` | 11534963 | Slashing of ciphernodes that break protocol rules |
+| `MockUSDC` (fee token) | `0xC35B783cA97710be47Fc81D10dADc895EfcD865c` | 11534960 | The token that pays E3 fees |
+| `Faucet` | `0x6e281411C055BEEbD74bDFcB9aB095aa98907F85` | 11534982 | Sends test FOLD and fee tokens |
+| `InterfoldToken` (FOLD) | `0x9752444A6a955D420402EaFf5f818afCddb9c340` | 11534976 | Governance and utility token. Operators bond FOLD. |
+| `InterfoldTicketToken` (tFOLD) | `0xfFFFc3BB04aEe950c0ed5DEe3214408f09fff488` | 11534961 | Non-transferable operator tickets for sortition |
+| `E3RefundManager` | `0x79f9EA83Db0fbe3a2cEB765b18285915331A9dc2` | 11534995 | Refunds for failed E3s |
+| `MockE3Program` | `0x5874CD49929ffcf380C82cDE1e74188dEaff9791` | 11534990 | A test E3 program |
-> [Ciphernode Operators → Sepolia Contract Addresses](/ciphernode-operators#sepolia-contract-addresses)
-> and in `packages/interfold-contracts/deployed_contracts.json` inside the repo.
+## What changes on Sepolia
----
+Locally, `pnpm dev:all` deploys its own Interfold contracts, and the deployer owns them. On Sepolia,
+the Interfold team owns the protocol contracts. This changes four steps of the local flow.
-## 2. Configure Environment
+| Step | Local (`pnpm dev:all`) | Sepolia |
+| ---------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
+| Program registration | `deployTemplate` calls `registerE3Program`. | `registerE3Program` is `onlyOwner`. The owner of the Interfold contract must call it. |
+| Protocol ciphertext verifier | `deployTemplate` calls `setCiphertextVerifier` with your image ID. | `setCiphertextVerifier` is `onlyOwner`. One verifier serves every BFV program on the deployment. |
+| Data availability | The coordination server stores the output (`mode: mock_http`). | Ciphernodes read the output from Avail (`mode: avail`). |
+| Ciphernodes | Five local nodes from `scripts/dev_ciphernodes.sh`. | The nodes of registered operators. Sortition needs enough of them for the committee size. |
-Create or update your `.env` file with Sepolia settings:
+The first two rows mean that you cannot run `deployTemplate` on Sepolia unchanged. The owner-only
+calls revert for any other signer. The last two rows decide if an E3 of your program can complete.
+The procedure below covers each row.
-```bash
-# Sepolia RPC (WebSocket required for event listening)
-WS_RPC_URL=wss://eth-sepolia.g.alchemy.com/v2/YOUR_KEY
-HTTP_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY
+## Before you start
-# Deployer private key (has Sepolia ETH)
-PRIVATE_KEY=0x...
+- A project from the default template. See
+ [Build a Complete E3 Program](/build/tutorials/write-e3-program).
+- A deployer account with Sepolia ETH. Use any public Sepolia faucet.
+- A Sepolia RPC endpoint. Hardhat uses HTTPS. The server and the client can use HTTPS or WebSocket.
+- Foundry (`cast`) for the checks.
-# Chain
-CHAIN_ID=11155111
+
-# Deployed Interfold contract addresses (Sepolia)
-INTERFOLD_ADDRESS=0x3E856E24c7a95d0e04d387f847DA6FA9f6F6c20C
-CIPHERNODE_REGISTRY_ADDRESS=0x374F4542eC634d5437Dd65020781A9D9Df9c2AB8
-FEE_TOKEN_ADDRESS=0xC35B783cA97710be47Fc81D10dADc895EfcD865c
+### Set the Sepolia variables
+
+The Hardhat configuration of the template reads `RPC_URL`, `PRIVATE_KEY`, and `MNEMONIC` from `.env`
+through `dotenv`. Create `.env` in the project root. The template `.gitignore` excludes it.
+
+```bash filename=".env"
+RPC_URL=https://
+PRIVATE_KEY=0x
```
----
+Set the same values in your shell for the `cast` commands, and add the Interfold address:
-## 3. Deploy Your E3 Program Contract
+```bash
+source .env
+INTERFOLD_ADDRESS=0x3E856E24c7a95d0e04d387f847DA6FA9f6F6c20C
+```
-If you're deploying your own E3 program contract (not using an already-deployed one):
+### Get test tokens
+
+E3 fees are paid in the fee token (`MockUSDC`). Call `faucet()` on the Faucet contract:
```bash
-# From your project root
-pnpm deploy:contracts --network sepolia
+cast send 0x6e281411C055BEEbD74bDFcB9aB095aa98907F85 "faucet()" --rpc-url $RPC_URL --private-key $PRIVATE_KEY
```
-This uses Hardhat to deploy your contracts to Sepolia. Make sure `hardhat.config.ts` has the Sepolia
-network configured:
+The faucet sends 200 FOLD and 200 fee tokens (`contracts/test/Faucet.sol`). It sends each token only
+if your balance of that token is below 200. It reverts with `You have enough tokens` if you need
+neither. An operator with a configured node can run `interfold faucet` instead.
-```typescript
-// hardhat.config.ts
-networks: {
- sepolia: {
- url: process.env.HTTP_RPC_URL,
- accounts: [process.env.PRIVATE_KEY],
- },
+### Record the Interfold address for the deploy script
+
+The template deploy helpers read addresses from `deployed_contracts.json`, one object for each
+Hardhat network name (`readDeploymentArgs`). Add a `sepolia` object next to `localhost`:
+
+```json filename="deployed_contracts.json"
+{
+ "localhost": { "...": "..." },
+ "sepolia": {
+ "Interfold": {
+ "address": "0x3E856E24c7a95d0e04d387f847DA6FA9f6F6c20C",
+ "blockNumber": 11534993
+ }
+ }
}
```
-Note the deployed contract address — you'll need it for the E3 request and the server config.
+### Add the Sepolia chain to interfold.config.yaml
+
+`interfold print-env` reads addresses from `interfold.config.yaml`. Add a `sepolia` entry to
+`chains`. The deploy script adds `e3_program` to it.
+
+```yaml filename="interfold.config.yaml"
+chains:
+ # - name: 'localhost' ... (keep the local entry)
+ - name: 'sepolia'
+ rpc_url: 'wss://'
+ chain_id: 11155111
+ contracts:
+ interfold:
+ address: '0x3E856E24c7a95d0e04d387f847DA6FA9f6F6c20C'
+ deploy_block: 11534993
+ ciphernode_registry:
+ address: '0x374F4542eC634d5437Dd65020781A9D9Df9c2AB8'
+ deploy_block: 11534965
+ bonding_registry:
+ address: '0x90250Dc48CBe109fFaA02AeAFbFBdbF12D7BD4d4'
+ deploy_block: 11534966
+ fee_token:
+ address: '0xC35B783cA97710be47Fc81D10dADc895EfcD865c'
+ deploy_block: 11534960
+```
-### Verify on Etherscan
+### Write a Sepolia deploy script
+
+Create `scripts/deploy-sepolia.ts`. It is `deploy/default.ts` without the two owner-only calls. Like
+the CRISP deploy script, it deploys `PoseidonT3` when the network has no record for it.
+
+```ts filename="scripts/deploy-sepolia.ts"
+import {
+ getDeploymentChain,
+ readDeploymentArgs,
+ storeDeploymentArgs,
+ updateE3Config,
+} from '@interfold/contracts/scripts'
+import hre from 'hardhat'
+
+async function main() {
+ const { MyProgram__factory: MyProgramFactory } = await import('../types/factories/contracts')
+ const { ensureTemplateCwd, INTERFOLD_CONFIG_FILE } = await import('./template-paths')
+ ensureTemplateCwd()
+ const { ethers } = await hre.network.connect()
+ const [owner] = await ethers.getSigners()
+ const chain = getDeploymentChain(hre)
+
+ const interfoldAddress = readDeploymentArgs('Interfold', chain)?.address
+ if (!interfoldAddress)
+ throw new Error(`No Interfold record for ${chain} in deployed_contracts.json`)
+
+ let poseidonT3Address = readDeploymentArgs('PoseidonT3', chain)?.address
+ if (!poseidonT3Address || (await ethers.provider.getCode(poseidonT3Address)) === '0x') {
+ const poseidonT3 = await ethers.deployContract('PoseidonT3')
+ await poseidonT3.waitForDeployment()
+ poseidonT3Address = await poseidonT3.getAddress()
+ storeDeploymentArgs(
+ { address: poseidonT3Address, blockNumber: await ethers.provider.getBlockNumber() },
+ 'PoseidonT3',
+ chain,
+ )
+ }
+
+ // MockRISC0Verifier accepts every seal. Use a real RISC Zero verifier for real proofs.
+ const verifier = await ethers.deployContract('MockRISC0Verifier')
+ await verifier.waitForDeployment()
+ const imageId = await ethers.deployContract('ImageID')
+ await imageId.waitForDeployment()
+ const programId = await imageId.PROGRAM_ID()
+
+ const factory = await ethers.getContractFactory(
+ MyProgramFactory.abi,
+ MyProgramFactory.linkBytecode({
+ 'npm/poseidon-solidity@0.0.5/PoseidonT3.sol:PoseidonT3': poseidonT3Address,
+ }),
+ owner,
+ )
+ const program = await factory.deploy(interfoldAddress, await verifier.getAddress(), programId)
+ await program.waitForDeployment()
+ const programAddress = await program.getAddress()
+
+ storeDeploymentArgs(
+ { address: programAddress, blockNumber: await ethers.provider.getBlockNumber() },
+ 'MyProgram',
+ chain,
+ )
+ updateE3Config(chain, INTERFOLD_CONFIG_FILE, { MyProgram: 'e3_program' })
+ console.log(`MyProgram deployed at ${programAddress}`)
+}
-Publishing source makes your contract inspectable and lets users interact with it from Etherscan's
-UI. Set an Etherscan API key and run:
+main().catch((error) => {
+ console.error(error)
+ process.exit(1)
+})
+```
-```bash
-# .env
-ETHERSCAN_API_KEY=your_key
+If your `MyProgram` constructor has more arguments (for example an input verifier), deploy them
+first and add them to `factory.deploy`.
-# Verify
-pnpm hardhat verify --network sepolia
+### Deploy the program
+
+Compile, then run the script on the `sepolia` network of `hardhat.config.ts`:
+
+```bash
+pnpm compile
+pnpm exec hardhat run scripts/deploy-sepolia.ts --network sepolia
```
-Constructor args are whatever you passed to your program's constructor — typically the Interfold
-contract address and any verifier addresses.
+The script prints `MyProgram deployed at 0x...`. It records the address in `deployed_contracts.json`
+and adds `e3_program` to the `sepolia` entry of `interfold.config.yaml`.
-### Save the address
+### Get the program registered
-Persist the deployed address in `.env` so your server and frontend pick it up automatically:
+Check the registration and the owner of the Interfold contract:
```bash
-# Append the fresh address — don't forget this step
-echo "E3_PROGRAM_ADDRESS=" >> .env
+cast call $INTERFOLD_ADDRESS "e3Programs(address)(bool)" --rpc-url $RPC_URL
+cast call $INTERFOLD_ADDRESS "owner()(address)" --rpc-url $RPC_URL
```
----
+The first call returns `false` until the owner calls `registerE3Program`. Send the program address
+to the Interfold team and ask for registration. The task `interfold:enableE3` in
+`packages/interfold-contracts` (`pnpm e3:enable --e3-address --network sepolia`) makes the
+call when its signer is the owner. For any other signer, it stops with
+`Submit registerE3Program through the owner Safe.`
-## 4. Keep the Insecure BFV Preset on Sepolia
+At registration, the Interfold contract checks with ERC-165 that the program supports `IE3Program`
+and `IE3ProgramDataAvailability`. The template does. A request for an unregistered program reverts
+with `E3ProgramNotAllowed`.
-Sepolia runs the insecure preset. Keep `INSECURE_THRESHOLD_512` in your SDK initialisation, and
-request E3s with `paramSet: 0`:
+### Check the protocol ciphertext verifier
-```typescript
-const sdk = new InterfoldSDK({
- // ... other config
- thresholdBfvParamsPresetName: 'INSECURE_THRESHOLD_512',
-})
+Every output proof must pass the ciphertext verifier that the owner set for BFV. Read it:
+
+```bash
+cast call $INTERFOLD_ADDRESS "getCiphertextVerifier(bytes32)(address)" $(cast keccak "fhe.rs:BFV") --rpc-url $RPC_URL
```
-```typescript
-const requestParams = {
- committeeSize: CommitteeSize.Minimum,
- inputWindow: [startTime, endTime] as const,
- e3Program: YOUR_PROGRAM_ADDRESS,
- computeProviderParams: '0x...',
- paramSet: 0, // ParamSet.Insecure512
+If the address is a `Risc0BfvCiphertextVerifier`, read its image ID and its RISC Zero verifier:
+
+```bash
+cast call "imageId()(bytes32)" --rpc-url $RPC_URL
+cast call "risc0Verifier()(address)" --rpc-url $RPC_URL
+```
+
+Your output proofs must pass that RISC Zero verifier for that image ID. The program server in dev
+mode (`program.dev: true`) makes mock proofs, which only a mock RISC Zero verifier accepts. The
+Interfold contract copies the current verifier into each E3 at request time, so a later change
+applies only to new requests. See
+[Verify the Compute Provider](/build/e3-program/verify-compute-provider).
+
+### Replace the local data-availability check
+
+Sepolia ciphernodes read the output ciphertext from Avail (`data_availability.mode: avail` in the
+Sepolia configurations). They use the block and leaf index that `verifyDataAvailability` returns.
+The template version returns zero for both, so a Sepolia committee cannot read your output.
+
+Delegate the check to `AvailVectorXDataAvailabilityVerifier` from `@interfold/contracts`, as
+`CRISPProgram` does:
+
+```solidity filename="contracts/MyProgram.sol"
+IDataAvailabilityVerifier public immutable dataAvailabilityVerifier;
+
+function verifyDataAvailability(
+ bytes32 expectedContentHash,
+ bytes calldata proof
+) external view override returns (IDataAvailabilityVerifier.DataReference memory receipt) {
+ receipt = dataAvailabilityVerifier.verifyDataAvailability(expectedContentHash, proof);
+ if (receipt.contentHash != expectedContentHash) revert DataAvailabilityHashMismatch(expectedContentHash, receipt.contentHash);
}
-const quote = await sdk.getE3Quote(requestParams)
-const approvalHash = await sdk.approveFeeToken(quote)
-await sdk.waitForTransaction(approvalHash)
-const hash = await sdk.requestE3({ ...requestParams, maxFee: quote })
```
-The SDK preset and the `paramSet` in the request must agree, and both must match what the ciphernode
-network runs. Sepolia is a functional testnet: it exercises the full E3 lifecycle, but it gives no
-cryptographic security, so do not put real secrets in it.
+Set `dataAvailabilityVerifier` in the constructor. Deploy the verifier with the Sepolia bridge and
+VectorX addresses from `AVAIL_VECTORX.sepolia` in `@interfold/contracts/scripts`. Add
+`@interfold/contracts/contracts/verifiers/AvailVectorXDataAvailabilityVerifier.sol` to
+`npmFilesToBuild` in `hardhat.config.ts` so that Hardhat compiles it.
-`SECURE_THRESHOLD_8192` (`paramSet: 1`) is the production preset. It uses a larger polynomial ring
-(N=8192 instead of N=512), so proof generation is much slower, and it needs `secure-8192` circuit
-artifacts that do not ship in `@interfold/sdk` — see
-[Proving: embedded circuits or your own](../sdk#proving-embedded-circuits-or-your-own).
+The coordination server must then publish the output bytes to Avail and pass the VectorX proof as
+`availabilityProof` to `publishCiphertextOutput`. The template server does not do this. The CRISP
+server in `examples/CRISP/server` does, and
+[`agent/flow-trace/08_DATA_AVAILABILITY.md`](https://github.com/theinterfold/interfold/blob/main/agent/flow-trace/08_DATA_AVAILABILITY.md)
+describes the flow.
----
+### Point the server at Sepolia
-## 5. Configure the Server
+In `server/index.ts`, change the chain of the SDK from `hardhat` to `sepolia`:
-Update your coordination server to point to Sepolia:
+```ts filename="server/index.ts"
+import { sepolia } from 'viem/chains'
-```bash
-# .env for the server
-WS_RPC_URL=wss://eth-sepolia.g.alchemy.com/v2/YOUR_KEY
-HTTP_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY
-INTERFOLD_ADDRESS=0x3E856E24c7a95d0e04d387f847DA6FA9f6F6c20C
-E3_PROGRAM_ADDRESS=
-CHAIN_ID=11155111
+// in createPrivateSDK()
+sdkInstance = InterfoldSDK.create({
+ rpcUrl: RPC_URL,
+ privateKey: PRIVATE_KEY as `0x${string}`,
+ contracts: {
+ interfold: INTERFOLD_CONTRACT as `0x${string}`,
+ ciphernodeRegistry: CIPHERNODE_REGISTRY_CONTRACT as `0x${string}`,
+ feeToken: FEE_TOKEN_CONTRACT as `0x${string}`,
+ },
+ chain: sepolia,
+ thresholdBfvParamsPresetName: 'INSECURE_THRESHOLD_512',
+})
```
-Start the server:
+`runProgram` reads the `InputPublished` events from block 0. Some RPC providers limit the block
+range of a log query. If yours does, set `fromBlock` to the deploy block of your program.
+
+Start the program server and the coordination server. `interfold print-env` prints `RPC_URL`,
+`INTERFOLD_ADDRESS`, `REGISTRY_ADDRESS`, `BONDING_REGISTRY_ADDRESS`, `E3_PROGRAM_ADDRESS`, and
+`FEE_TOKEN_ADDRESS` for the chain:
```bash
-pnpm dev:server
+interfold program start
```
-The server will connect to Sepolia via WebSocket and begin listening for E3 events.
+```bash
+export $(interfold print-env --chain sepolia)
+export PRIVATE_KEY=0x CHAIN_ID=11155111
+pnpm tsx ./server/index.ts
+```
----
+The server account pays gas for `publishCiphertextOutput`, so it needs Sepolia ETH.
+
+### Point the client at Sepolia
+
+The client already includes the `sepolia` chain in its wagmi configuration. Start it with the
+Sepolia addresses:
+
+```bash
+cd client
+export $(interfold print-env --vite --chain sepolia)
+pnpm dev
+```
-## 6. Test End-to-End
+The client requests E3s with `paramSet: 0`, and the SDK uses `INSECURE_THRESHOLD_512` by default
+(`VITE_THRESHOLD_BFV_PARAMS_PRESET_NAME`). Keep the two values in agreement. Sepolia accepts the
+param sets 0 (`insecure-512`) and 1 (`secure-8192`). The mainnet contracts accept only 1.
-1. **Request an E3** — via the SDK or your frontend
-2. **Wait for committee formation** — the Sepolia ciphernode committee will perform sortition and
- DKG (this takes longer than local dev due to real block times)
-3. **Submit encrypted inputs** — your frontend encrypts under the threshold public key and submits
-4. **Wait for computation** — the compute provider runs the FHE program
-5. **Check the result** — the committee decrypts and publishes the plaintext
+### Request an E3
-Monitor progress by watching for events:
+Open `http://localhost:3000`, connect a wallet on Sepolia with fee tokens, and request an E3. Each
+stage waits for Sepolia blocks, so an E3 takes longer than on the local chain, where `anvil` makes
+one block each second. To follow the E3 from a script, listen to the same events as the template:
-```typescript
-sdk.onInterfoldEvent(InterfoldEventType.E3_REQUESTED, (e) => console.log('E3 requested'))
-sdk.onInterfoldEvent(RegistryEventType.COMMITTEE_PUBLISHED, (e) => console.log('Committee ready'))
-sdk.onInterfoldEvent(InterfoldEventType.CIPHERTEXT_OUTPUT_PUBLISHED, (e) =>
- console.log('Output ready'),
+```ts
+await sdk.onInterfoldEvent(InterfoldEventType.E3_REQUESTED, (event) =>
+ console.log('requested', event.data.e3Id),
+)
+await sdk.onInterfoldEvent(RegistryEventType.COMMITTEE_PUBLIC_KEY_CHUNK_PUBLISHED, (event) =>
+ console.log('key chunk', event.data.e3Id),
)
-sdk.onInterfoldEvent(InterfoldEventType.PLAINTEXT_OUTPUT_PUBLISHED, (e) =>
- console.log('Result:', e.data),
+await sdk.onInterfoldEvent(InterfoldEventType.CIPHERTEXT_OUTPUT_REFERENCE_PUBLISHED, (event) =>
+ console.log('output', event.data.e3Id),
+)
+await sdk.onInterfoldEvent(InterfoldEventType.PLAINTEXT_OUTPUT_PUBLISHED, (event) =>
+ console.log('result', event.data.plaintextOutput),
)
```
----
+
-## Troubleshooting
+## Deploy scripts and tasks
-| Issue | Possible cause | Fix |
-| --------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------- |
-| E3 request reverts | Insufficient fee token approval | Call `sdk.approveFeeToken()` first |
-| Committee never forms | Not enough active ciphernodes on Sepolia | Check the registry for active operators |
-| Proof generation fails | Preset mismatch with the SDK's circuits | The embedded circuits are `insecure-512`; compile your own for other presets |
-| Decryption produces garbage | BFV preset mismatch | Verify both SDK and E3 request use `INSECURE_THRESHOLD_512` / `paramSet: 0` |
+| Location | Command | What it does |
+| ------------------------------ | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
+| Template | `pnpm dev:all` | Runs `scripts/deploy-local.ts` on `localhost`: `deployInterfold(true, false)`, then `deployTemplate()`. |
+| Template | `pnpm hardhat ciphernode:admin-add --ciphernode-address --network localhost` | Adds a ciphernode with admin privileges. It needs account impersonation (Anvil or Hardhat), so it works only on a local chain. |
+| `packages/interfold-contracts` | `pnpm e3:enable --e3-address --network ` | Registers an E3 program. The signer must be the owner of the Interfold contract. |
+| `packages/interfold-contracts` | `pnpm run deploy --network ` | Deploys the protocol contracts (`scripts/run.ts`, `deployInterfold`). This is for the protocol owner. |
+| `packages/crisp-contracts` | `pnpm deploy:contracts --network sepolia` | Deploys CRISP and its verifiers, and wires them only when the signer owns the Interfold contract. |
----
+`deployInterfold` accepts mock contracts on chain ID 11155111. On other chains it refuses mocks
+unless `ALLOW_MOCKS_ON_PRODUCTION=true` is set.
-## Next Steps
+## Troubleshooting
-- [Manage Tickets](./manage-tickets) — if you're also running a ciphernode on Sepolia
-- [Troubleshoot Your Node](./operator-troubleshooting) — debugging ciphernode issues on testnet
+| Error or symptom | Cause | Fix |
+| -------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
+| `OwnableUnauthorizedAccount` from the deploy | The script calls `registerE3Program` or `setCiphertextVerifier`. | Use `scripts/deploy-sepolia.ts`, which leaves out the owner-only calls. |
+| `E3ProgramNotAllowed` on request | The program is not registered. | Ask the owner of the Interfold contract to call `registerE3Program`. |
+| `ERC20InsufficientAllowance` on request | The fee token allowance is lower than the fee. | Call `approveFeeToken(fee)` with the fee from `getE3Quote`, and wait for the receipt. |
+| `FeeExceedsMaximum` on request | The fee changed after the quote. | Get a new quote and request again. |
+| `InvalidInputDeadlineStart` on request | `inputWindow[0]` is earlier than the block time. | Compute the window with `calculateInputWindow(publicClient, duration)`. |
+| No committee key | Sortition did not select a full committee, or the DKG did not finish. | Read the stage with `getE3Stage`. If it is `Failed`, request a new E3. |
+| `InvalidOutput` on output publication | The protocol verifier or `MyProgram.verify` rejected the compute proof. | Check the protocol ciphertext verifier and your image ID (see the steps above). |
+| The E3 does not finish after the output | The committee cannot read the output from Avail. | Replace the local data-availability check, and publish the output to Avail. |
+
+## Next steps
+
+
+
+ Every deployed contract for each network.
+
+
+ Run the program server with real RISC Zero proofs.
+
+
+ Request parameters, quotes, and fees in detail.
+
+
+ Join the Sepolia network as an operator.
+
+
diff --git a/docs/pages/build/tutorials/write-e3-program.mdx b/docs/pages/build/tutorials/write-e3-program.mdx
index 90e69822a3..ff7c50ea02 100644
--- a/docs/pages/build/tutorials/write-e3-program.mdx
+++ b/docs/pages/build/tutorials/write-e3-program.mdx
@@ -1,295 +1,404 @@
---
title: 'Build a Complete E3 Program'
description:
- 'Walk through CRISP to understand how each component of an E3 program works — contracts, FHE
- computation, and verification'
+ 'Create an E3 program from the default template, change its Secure Process, program contract, and
+ client, and run a complete E3 on a local chain.'
---
-# Build a Complete E3 Program
-
-This tutorial walks through [CRISP](/CRISP/introduction) (Coercion-Resistant Impartial Selection
-Protocol), a private voting application built on the Interfold, to show how each component of an E3
-program fits together. By the end, you'll understand the contract interface, FHE computation, and
-verification flow well enough to build your own.
+import { Callout, FileTree, Steps } from 'nextra/components'
+import { Layers, LinkCard, LinkCards } from '@/components/ui'
-> **What you'll learn:** How the three `IE3Program` entry points fit together, what the FHE
-> computation looks like inside a RISC Zero guest, and how the final proof is verified on-chain.
->
-> **Prerequisites:** Familiarity with Solidity and a basic understanding of
-> [what an E3 is](/what-is-e3).
+# Build a Complete E3 Program
----
+This tutorial builds an E3 program from the default template. You go through each part that you own:
+the Secure Process in Rust, the program contract in Solidity, the coordination server, and the
+client in TypeScript. You make a change to the contract, check that the parts still agree, and run a
+complete E3 on a local chain. At the end, you know which file does what, and which values must stay
+the same across the parts.
+
+**Prerequisites:** the Interfold CLI, Rust, Node.js, pnpm, and Foundry (`pnpm dev:all` starts
+`anvil`). See [Installation](/build/installation). Read [E3 Program Anatomy](/build/e3-program) for
+the concepts.
+
+## The parts of the program
+
+
+
+| Part | File | Role |
+| ------------------- | ------------------------------ | ----------------------------------------------------------------------------------- |
+| Secure Process | `program/src/lib.rs` | The FHE computation over the encrypted inputs, and the input policy. |
+| Program contract | `contracts/MyProgram.sol` | Accepts inputs, keeps the input tree, and verifies the compute proof. |
+| Deploy script | `deploy/default.ts` | Deploys the program and registers it with the Interfold contract. |
+| Coordination server | `server/index.ts` | Starts the computation after the input window and publishes the ciphertext output. |
+| Client | `client/src/pages/steps/*.tsx` | The five-step web flow: connect, request, enter inputs, submit, and results. |
+| Configuration | `interfold.config.yaml` | Chain, contract addresses, local ciphernodes, and the program mode (`program.dev`). |
+
+
+
+### Create the project
+
+Create a project from the default template:
+
+```bash
+interfold init my-e3
+cd my-e3
+```
-## The IE3Program Interface
-
-Every E3 program implements the `IE3Program` interface from `@interfold/contracts`. It has three
-functions that the Interfold contract calls at different stages of the E3 lifecycle:
-
-```solidity
-interface IE3Program {
- function validate(
- uint256 e3Id,
- uint256 seed,
- bytes calldata e3ProgramParams,
- bytes calldata computeProviderParams,
- bytes calldata customParams
- ) external returns (bytes32 encryptionSchemeId);
-
- function publishInput(uint256 e3Id, bytes memory data) external;
-
- function verify(
- uint256 e3Id,
- bytes32 ciphertextOutputHash,
- bytes32 ciphertextCommitment,
- bytes memory proof
- ) external returns (bool success);
-}
+`interfold init` downloads `templates/default` at the tag of your CLI version (for example
+`v0.18.0`). It pins the `@interfold/*` packages and the Rust crates to that release, runs
+`pnpm install`, and makes the first Git commit.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+### Run the template one time
+
+Start the complete local stack:
+
+```bash
+pnpm dev:all
```
-| Function | When called | Purpose |
-| -------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
-| `validate` | When someone requests a new E3 | Validate parameters and initialise round state. Returns the encryption scheme ID (e.g. `keccak256("fhe.rs:BFV")`) |
-| `publishInput` | Each time a user submits encrypted data | Validate the input (e.g. check a ZK proof, verify eligibility) and store it |
-| `verify` | When the compute provider publishes the result | Verify the computation proof and confirm the ciphertext output is correct |
+The command starts `anvil`, deploys the Interfold contracts and `MyProgram`, starts five local
+ciphernodes, the program server, the coordination server, and the client. Open
+`http://localhost:3000`, connect a wallet, and run one E3 with two numbers. The result is their sum.
+The [Quick Start](/build/quick-start) has the wallet setup.
----
+Stop the stack with `Ctrl+C` before you change the code.
-## Step 1: validate — Initialise the Round
+### Read the Secure Process
-When a new E3 is requested, the Interfold contract calls `validate` on your program. This is where
-you parse parameters, set up state, and decide whether the request is valid.
+`program/src/lib.rs` exports two functions. The program server calls both.
-CRISP's implementation (`CRISPProgram.sol`):
+```rust filename="program/src/lib.rs"
+pub fn policy() -> InputPolicy {
+ InputPolicy::default()
+}
-```solidity
-function validate(
- uint256 e3Id,
- uint256,
- bytes calldata e3ProgramParams,
- bytes calldata,
- bytes calldata customParams
-) external returns (bytes32) {
- if (msg.sender != address(interfold) && msg.sender != owner()) revert CallerNotAuthorized();
- if (e3Data[e3Id].paramsHash != bytes32(0)) revert E3AlreadyInitialized();
+pub fn fhe_processor(fhe_inputs: &FHEProcessorInput<'_>) -> Vec {
+ let mut sum = Ciphertext::zero(fhe_inputs.params);
+ for ciphertext_bytes in fhe_inputs.ciphertexts {
+ let ciphertext = Ciphertext::from_bytes(&ciphertext_bytes.0, fhe_inputs.params).unwrap();
+ sum += &ciphertext;
+ }
- // Decode voting parameters from customParams
- (, , uint256 numOptions, CreditMode creditMode, ) = abi.decode(
- customParams,
- (address, uint256, uint256, CreditMode, uint256)
- );
+ sum.to_bytes()
+}
+```
- // Store round configuration
- e3Data[e3Id].numOptions = numOptions;
- e3Data[e3Id].creditMode = creditMode;
- e3Data[e3Id].paramsHash = keccak256(e3ProgramParams);
+- `fhe_processor` adds the ciphertexts homomorphically and returns the encrypted sum. It never sees
+ a plaintext.
+- `policy` returns `InputPolicy::default()`. The input tree leaf is the SAFE commitment of each
+ ciphertext, and the processor uses every input. This matches `MyProgram.publishInput`, which
+ inserts the commitment.
- // Initialise the Merkle tree that will track vote commitments
- e3Data[e3Id].votes._init(TREE_DEPTH);
+Run the unit test. It encrypts 3 and 2 with a test key, runs `fhe_processor`, decrypts, and expects
+5:
- return ENCRYPTION_SCHEME_ID; // keccak256("fhe.rs:BFV")
-}
+```bash
+cargo test -p e3-user-program
```
-Key points:
+When you change the computation, keep these rules:
-- Only the Interfold contract (or the program owner) can call `validate`
-- `customParams` carries application-specific configuration — CRISP uses it for voting options and
- credit mode
-- The function returns an encryption scheme identifier so the Interfold contract knows which scheme
- the ciphernodes should use
+- Return one BFV ciphertext with two parts. The decryption circuit (C6) takes `ct0` and `ct1` only.
+ The processor receives no relinearization key, so do not multiply two ciphertexts.
+- Select inputs only through `policy`. The compute proof commits to the root of the inputs that the
+ processor used, and `MyProgram.verify` compares it with the on-chain tree.
----
+[The Secure Process](/build/e3-program/secure-process) explains the input policy in detail.
-## Step 2: publishInput — Validate User Inputs
+### Harden publishInput
-Each time a user submits encrypted data to the E3, the Interfold contract calls `publishInput`. This
-is where you enforce application-specific rules.
+`contracts/MyProgram.sol` implements `IE3Program` and `IE3ProgramDataAvailability`:
-CRISP's `publishInput` does several things:
+| Function | Called by | What the template does |
+| ------------------------ | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
+| `supportsInterface` | Interfold contract, at registration | Reports `IE3Program`, `IE3ProgramDataAvailability`, and `IERC165`. Registration fails without them. |
+| `validate` | Interfold contract, at request | Stores `keccak256(e3ProgramParams)`, starts an input tree of depth 20, returns `keccak256("fhe.rs:BFV")`. |
+| `publishInput` | The data provider, directly | Decodes `(bytes ciphertext, bytes32 commitment)`, inserts the commitment, emits `InputPublished`. |
+| `verify` | Interfold contract, at output publication | Checks the parameter hash and the input root in the proof, rebuilds the journal, calls the RISC Zero verifier. |
+| `verifyDataAvailability` | Interfold contract, at output publication | Local only. It accepts the output bytes as their own receipt. |
-1. **Checks the E3 stage** — only accepts inputs after the committee's public key is published
-2. **Checks the input window** — rejects submissions before `inputWindow[0]` or after
- `inputWindow[1]`
-3. **Decodes the payload** — unpacks the Noir proof, slot address, commitment, and ciphertext
-4. **Processes the vote slot** — inserts or overrides the voter's entry in the Merkle tree
-5. **Verifies a ZK proof** — calls an on-chain Honk verifier to prove the vote was encrypted
- correctly and the voter is eligible (see [Custom Noir Circuits](./custom-zk-circuits))
+The template `publishInput` checks only the end of the input window. Add the stage check and the
+start-of-window check that `CRISPProgram` makes. Add two errors next to the other errors:
-```solidity
-function publishInput(uint256 e3Id, bytes memory data) external {
+```solidity filename="contracts/MyProgram.sol"
+error KeyNotPublished(uint256 e3Id);
+error InputWindowNotOpen(uint256 e3Id);
+```
+
+Then replace the first check in `publishInput`:
+
+```solidity filename="contracts/MyProgram.sol"
+function publishInput(uint256 e3Id, bytes memory data) external override {
E3 memory e3 = interfold.getE3(e3Id);
- // Stage and window checks
if (interfold.getE3Stage(e3Id) != IInterfold.E3Stage.KeyPublished) revert KeyNotPublished(e3Id);
- if (block.timestamp > e3.inputWindow[1]) revert InputDeadlinePassed(e3Id, e3.inputWindow[1]);
- if (block.timestamp < e3.inputWindow[0]) revert E3NotAcceptingInputs(e3Id);
- if (e3Data[e3Id].merkleRoot == 0) revert MerkleRootNotSet();
- if (data.length == 0) revert EmptyInputData();
-
- (
- bytes memory noirProof,
- address slotAddress,
- bytes32 encryptedVoteCommitment,
- bytes memory encryptedVote
- ) = abi.decode(data, (bytes, address, bytes32, bytes));
-
- (uint40 voteIndex, bytes32 previousCommitment) = _processVote(
- e3Id,
- slotAddress,
- encryptedVoteCommitment
- );
-
- // Build the Noir public inputs (order must match the circuit)
- bytes32[] memory publicInputs = new bytes32[](7);
- publicInputs[0] = previousCommitment;
- publicInputs[1] = bytes32(e3Data[e3Id].merkleRoot);
- publicInputs[2] = bytes32(uint256(uint160(slotAddress)));
- publicInputs[3] = bytes32(uint256(previousCommitment == bytes32(0) ? 1 : 0));
- publicInputs[4] = bytes32(e3Data[e3Id].numOptions);
- publicInputs[5] = encryptedVoteCommitment;
- publicInputs[6] = e3.committeePublicKey;
-
- if (!honkVerifier.verify(noirProof, publicInputs)) revert InvalidNoirProof();
-
- emit InputPublished(e3Id, encryptedVote, voteIndex);
-}
-```
+ if (block.timestamp < e3.inputWindow[0]) revert InputWindowNotOpen(e3Id);
+ if (block.timestamp > e3.inputWindow[1]) revert InputDeadlineReached();
-Every E3 program should verify that the submitted ciphertext is correctly encrypted under the
-committee's threshold public key. Beyond that baseline, your `publishInput` can add
-application-specific checks — CRISP adds eligibility (Merkle membership), vote validity (balance
-checks), and signature verification. Simpler programs may only need the encryption validity check.
+ (bytes memory ciphertext, bytes32 ciphertextCommitment) = abi.decode(data, (bytes, bytes32));
+ if (ciphertext.length == 0) revert EmptyInputData();
----
+ uint256 index = inputs[e3Id].numberOfLeaves;
+ inputs[e3Id]._insert(uint256(ciphertextCommitment));
-## Step 3: The FHE Computation
+ emit InputPublished(e3Id, ciphertext, index);
+}
+```
-After the input window closes, the compute provider runs the E3 program over the collected encrypted
-inputs. For CRISP, this is a RISC Zero guest that sums all encrypted votes homomorphically:
+Compile the contracts. The command also regenerates the TypeChain types in `types/`, which the
+server and the deploy script import:
-```rust
-pub fn fhe_processor(fhe_inputs: &FHEInputs) -> Vec {
- let params = decode_bfv_params_arc(&fhe_inputs.params).unwrap();
+```bash
+pnpm compile
+```
- // Start with a zero ciphertext
- let mut sum = Ciphertext::zero(¶ms);
+
+ The template does not check that the ciphertext bytes match the commitment, as the comment in
+ `publishInput` says. An input with a wrong commitment can stop the E3 from completing. A
+ production program must check that binding, for example with a proof. See [Custom Noir
+ Circuits](/build/tutorials/custom-zk-circuits).
+
+
+Keep the `verify` function as it is. It compares `computeProof.inputRoot` with
+`inputs[e3Id]._root()`. The protocol ciphertext verifier does not check the input root, so this
+comparison is the only check that the output covers your inputs.
+
+### Read the deploy script
+
+`deploy/default.ts` exports `deployTemplate`. `scripts/deploy-local.ts` calls it after
+`deployInterfold(true, false)`, which deploys the protocol with mocks. `deployTemplate` does these
+steps:
+
+1. It deploys `MockRISC0Verifier`, which accepts every proof, and the `ImageID` library.
+2. It deploys `Risc0BfvCiphertextVerifier` for the image ID and calls `setCiphertextVerifier` on the
+ Interfold contract.
+3. It deploys `MyProgram` with the Interfold address, the verifier, and the image ID. It links
+ `PoseidonT3` for the input tree.
+4. It calls `registerE3Program` and checks `e3Programs(program)`.
+5. It records the addresses in `deployed_contracts.json` and `interfold.config.yaml`.
+
+`setCiphertextVerifier` and `registerE3Program` are `onlyOwner` functions. They work locally because
+the deployer owns the local Interfold contract. If you add a constructor argument to `MyProgram`,
+add it to the `e3ProgramFactory.deploy(...)` call.
+
+### Read the coordination server
+
+`server/index.ts` runs the computation when the input window closes. It needs a private key to
+publish the output, so it runs on a server and not in the browser.
+
+1. It listens for `CommitteePublicKeyChunkPublished`. When a key is complete, it checks the key
+ against its commitment with `validatePublicKeyCommitment`.
+2. It waits until the chain time passes `inputWindow[1]`.
+3. It reads the parameter bytes from `paramSetRegistry` and every `InputPublished` event of the E3.
+ It skips an E3 with one input or fewer.
+4. It sends the job to the program server at `PROGRAM_RUNNER_URL` (`POST /run_compute`).
+5. The program server calls back at `CALLBACK_URL`. The server stores the output bytes and calls
+ `publishCiphertextOutput`.
+
+| Variable | Source |
+| ---------------------------------------------------------------------------------- | ----------------------------------------------------- |
+| `INTERFOLD_ADDRESS`, `REGISTRY_ADDRESS`, `E3_PROGRAM_ADDRESS`, `FEE_TOKEN_ADDRESS` | `interfold print-env --chain localhost` |
+| `RPC_URL`, `PRIVATE_KEY`, `CHAIN_ID` | `scripts/dev_server.sh` sets them for the local chain |
+| `PROGRAM_RUNNER_URL` | Default `http://127.0.0.1:13151` |
+| `CALLBACK_URL` | Default `http://127.0.0.1:8080` |
+| `PORT` | Default `8080` |
+
+The server reads the `data` field of `InputPublished` as the ciphertext. If you change the event,
+change `runProgram` in `server/index.ts` too.
+
+
+ The program server that `pnpm dev:all` starts is a development service. It accepts any callback
+ URL and does not authenticate callers. Keep it isolated from production and untrusted networks.
+
+
+### Read the client
+
+The client uses `useInterfoldSDK` from `@interfold/react`. The SDK configuration is in
+`client/src/utils/sdk-config.ts`, and the addresses come from the `VITE_*` variables that
+`interfold print-env --vite` prints.
+
+The request step (`RequestComputation.tsx`) gets a quote, approves the fee token, and requests the
+E3:
+
+```ts filename="client/src/pages/steps/RequestComputation.tsx"
+const inputWindow = await calculateInputWindow(publicClient, 100) // 100 Seconds
+const computeProviderParams = encodeComputeProviderParams(DEFAULT_COMPUTE_PROVIDER_PARAMS)
+
+const requestParams = {
+ committeeSize: DEFAULT_E3_CONFIG.committeeSize,
+ inputWindow,
+ e3Program: contracts.e3Program,
+ paramSet: 0, // ParamSet.Insecure512
+ computeProviderParams,
+}
- // Homomorphically add every encrypted vote — no decryption needed
- for ciphertext_bytes in &fhe_inputs.ciphertexts {
- let ciphertext = Ciphertext::from_bytes(&ciphertext_bytes.0, ¶ms).unwrap();
- sum += &ciphertext;
- }
+const fee = await sdk.sdk.getE3Quote(requestParams)
+const approveTx = await sdk.sdk.approveFeeToken(fee)
+await publicClient.waitForTransactionReceipt({ hash: approveTx })
- // Return the encrypted tally
- sum.to_bytes()
-}
+const hash = await requestE3(requestParams)
```
-This is the core of what makes FHE powerful: the compute provider processes encrypted data without
-ever seeing the plaintext. The `+=` operator performs homomorphic addition — when the ciphernode
-committee later decrypts the sum, the result is the sum of all the original votes.
+The same step assembles the committee public key from `COMMITTEE_PUBLIC_KEY_CHUNK_PUBLISHED` events
+with `CommitteePublicKeyAssembler`. It accepts the key only if `validatePublicKeyCommitment` returns
+`true`.
+
+The input step (`EnterInputs.tsx`) encrypts each number to that key, computes its commitment, and
+calls `publishInput` on the program contract:
+
+```ts filename="client/src/pages/steps/EnterInputs.tsx"
+const encryptedInput1 = await sdk.sdk?.encryptNumber(num1, publicKeyBytes)
+const commitment1 = await sdk.sdk.computeCiphertextCommitment(encryptedInput1)
+await publishInput(
+ walletClient,
+ e3State.id,
+ toHex(encryptedInput1),
+ toHex(commitment1),
+ address,
+ contracts.e3Program,
+)
+```
-The RISC Zero guest generates a ZK proof that this computation was performed correctly. In
-development, set `RISC0_DEV_MODE=1` to skip real proof generation, and ensure that the contracts
-were deployed using full mock mode, otherwise a real proof will be needed.
+`publishInput` in `client/src/utils/input.ts` encodes `(bytes, bytes32)`, the format that
+`MyProgram.publishInput` decodes. The submit step (`EncryptSubmit.tsx`) waits for
+`PLAINTEXT_OUTPUT_PUBLISHED`, decodes the value with `decodePlaintextOutput`, and opens the results
+step.
----
+To follow the output publication, listen for
+`InterfoldEventType.CIPHERTEXT_OUTPUT_REFERENCE_PUBLISHED`. The Interfold contract emits
+`CiphertextOutputReferencePublished` when it accepts the ciphertext output. It does not emit
+`CiphertextOutputPublished`.
-## Step 4: verify — Confirm the Result
+### Run the changed program
-After the compute provider publishes its result, the Interfold contract calls `verify` on your
-program. CRISP's implementation checks the RISC Zero proof:
+Start the stack again:
-```solidity
-function verify(
- uint256 e3Id,
- bytes32 ciphertextOutputHash,
- bytes32 ciphertextCommitment,
- bytes memory proof
-) external view override returns (bool) {
- E3 memory e3 = interfold.getE3(e3Id);
- bytes32 paramsHash = getParamsHash(e3Id);
- bytes32 inputRoot = bytes32(e3Data[e3Id].votes._root());
-
- // The Secure Process derives this root from the ciphertexts it processed, and the proof envelope
- // carries that derived value. Compare it against the root this program accumulated on chain: the
- // protocol verifier does not, so this is the only check that the result covers your inputs.
- Risc0ComputeProof.Proof memory computeProof = Risc0ComputeProof.decode(proof);
- if (computeProof.paramsHash != paramsHash || computeProof.inputRoot != inputRoot)
- revert InvalidComputeContext();
-
- // Rebuild the exact RISC Zero journal from on-chain state.
- bytes memory journal = Risc0ComputeProof.journal(
- bytes32(block.chainid),
- bytes32(uint256(uint160(address(interfold)))),
- bytes32(e3Id),
- e3.encryptionSchemeId,
- e3.committeePublicKey,
- ciphertextOutputHash,
- ciphertextCommitment,
- paramsHash,
- inputRoot
- );
-
- risc0Verifier.verify(computeProof.seal, imageId, sha256(journal));
- return true;
-}
+```bash
+pnpm dev:all
```
-The guest commits nine values as 32-byte Rust vectors. RISC Zero serializes each vector into 132
-bytes, so the journal is 1,188 bytes. `Risc0ComputeProof.journal` builds that layout, and
-`Risc0ComputeProof.FIELD_COUNT` holds the field count, so a program never hardcodes the length.
+The deploy step compiles and deploys the changed `MyProgram`. Run one E3 in the client. The result
+is the sum of the two inputs, as before. The new checks reject an input that arrives before the
+committee key is published or before `inputWindow[0]`.
-Eight of the nine fields come from on-chain state, which is why a front-runner cannot replace the
-commitment while reusing a valid proof. The ninth, `inputRoot`, arrives inside the proof envelope.
-The protocol verifier does not constrain it, so **every E3 program must compare it against its own
-input root**, as the check above does. A program that skips that comparison accepts a result
-computed over an input set it never saw.
+
----
+## What must stay the same across the parts
+
+A change in one part often needs a change in another part. Check this table after each change.
-## Putting It Together
+| Value | Where it is set | Where it must match |
+| ----------------- | ------------------------------------------------------ | -------------------------------------------------------------------------------- |
+| Input encoding | `abi.decode(data, (bytes, bytes32))` in `publishInput` | `encodeAbiParameters` in `client/src/utils/input.ts` and `server/input.ts` |
+| Input tree leaf | `inputs[e3Id]._insert(uint256(ciphertextCommitment))` | `policy()` in `program/src/lib.rs` |
+| Input event | `InputPublished(e3Id, data, index)` | `getContractEvents` in `server/index.ts` |
+| BFV parameter set | `paramSet: 0` in the request | `thresholdBfvParamsPresetName: 'INSECURE_THRESHOLD_512'` in the SDK config |
+| Guest image ID | `.interfold/generated/contracts/ImageID.sol` | The `imageId` in the `MyProgram` constructor and in `Risc0BfvCiphertextVerifier` |
-The full lifecycle for a CRISP voting round:
+## The E3 flow
```mermaid
sequenceDiagram
- participant U as User
- participant E as Interfold contract
- participant P as CRISPProgram
- participant C as Ciphernode committee
- participant CP as Compute provider
-
- U->>E: requestE3(params)
- E->>P: validate(e3Id, params)
- Note over P: store config,
init Merkle tree
- C->>C: sortition → DKG
- C->>E: publish threshold public key
- U->>E: publishInput(e3Id, proof + ct)
- E->>P: publishInput(e3Id, data)
- Note over P: verify Noir proof,
insert into Merkle tree
- Note over CP: input window closes
- CP->>CP: fhe_processor()
homomorphic sum + RISC Zero proof
- CP->>E: publishCiphertextOutput
- E->>P: verify(e3Id, ctHash, proof)
- C->>E: threshold-decrypt result
- Note over E: plaintext tally on-chain
+ participant C as Client
+ participant I as Interfold contract
+ participant P as MyProgram
+ participant N as Ciphernodes
+ participant S as Server and program server
+ C->>I: request(params)
+ I->>P: validate(e3Id, ...)
+ N->>I: publish committee key (through the registry)
+ C->>P: publishInput(e3Id, ciphertext, commitment)
+ S->>S: run fhe_processor after inputWindow[1]
+ S->>I: publishCiphertextOutput(e3Id, output)
+ I->>P: verifyDataAvailability, then verify
+ N->>I: publish plaintext output
```
----
-
-## Building Your Own
-
-To create a new E3 program:
-
-1. **Implement `IE3Program`** — start from the
- [MockE3Program](https://github.com/theinterfold/interfold/blob/main/packages/interfold-contracts/contracts/test/MockE3Program.sol)
- for a minimal example, or study `CRISPProgram.sol` for a full implementation
-2. **Write your FHE computation** — implement the `fhe_processor` function for your use case
- (aggregation, comparison, etc.)
-3. **Choose your validation strategy** — decide what `publishInput` should check (ZK proofs, token
- balances, allow-lists, or nothing)
-4. **Set up verification** — implement `verify` to check the compute provider's proof
-
-See [Writing the E3 Program Contract](/write-e3-contract) and
-[Writing the Secure Process](/write-secure-program) for more details on each step.
+The client calls `publishInput` on the program contract. The Interfold contract does not forward
+inputs. See [E3 Lifecycle](/learn/e3-lifecycle) for the stages and deadlines.
+
+## Next steps
+
+
+
+ Add a proof that each input is valid, and check it in `publishInput`.
+
+
+ Encrypt inputs and submit them with the SDK.
+
+
+ Deploy the program to the testnet and request an E3 there.
+
+
+ The full `IE3Program` interface and what each function must check.
+
+