From d6539f97018fc65752f6f2a62ced2234c22fc9f6 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Sat, 26 Sep 2026 18:36:07 +0500 Subject: [PATCH] chore(docs): rewrite Noir circuits and the build tutorials Rewrite the Noir circuits page and the custom circuit, complete E3 program, and Sepolia deployment tutorials. The tutorial circuits and the modified MyProgram.sol were compiled with nargo 1.0.0-beta.26 and solc 0.8.28. The Sepolia tutorial explains that registerE3Program and setCiphertextVerifier are owner-only on the shared deployment, and it warns that the current Sepolia contracts predate the node release gate (#2011). Addresses come from deployments/manifest.json. --- docs/pages/build/noir-circuits.mdx | 459 ++++++++--- .../build/tutorials/custom-zk-circuits.mdx | 767 +++++++++++++----- .../build/tutorials/deploy-to-testnet.mdx | 496 +++++++---- .../build/tutorials/write-e3-program.mdx | 585 +++++++------ 4 files changed, 1595 insertions(+), 712 deletions(-) diff --git a/docs/pages/build/noir-circuits.mdx b/docs/pages/build/noir-circuits.mdx index 1bda85d5e2..f12f8c8e29 100644 --- a/docs/pages/build/noir-circuits.mdx +++ b/docs/pages/build/noir-circuits.mdx @@ -1,173 +1,406 @@ --- title: 'Noir Circuits' description: - 'Toolchain, repo layout, compile/lint scripts, and integration hooks for Interfold Noir circuits' + 'The Noir circuits workspace, the pinned toolchain, the build and verifier scripts, and how the + circuits connect to the Rust prover and to Solidity verifiers.' --- -# Noir Circuits +import { Callout, FileTree, Steps } from 'nextra/components' +import { Flow, LinkCard, LinkCards } from '@/components/ui' -This page is for **building and integrating** the Noir workspace under `circuits/`: where packages -live, which scripts to run, and how they connect to the Rust prover and on-chain verifiers. +# Noir Circuits -For **what** the circuits prove (PV-TBFV, phases **P1–P4**, **C0–C7**, commitments), read -[Cryptography](./cryptography) first. For **Rust types and events** (`ProofType`, `CircuitName`, -public inputs), use the repo’s -[`agent/flow-trace/04_DKG_AND_COMPUTATION.md`](https://github.com/theinterfold/interfold/blob/main/agent/flow-trace/04_DKG_AND_COMPUTATION.md). +This page is for developers who build, change, or reuse the Noir circuits in the `circuits/` +directory of the Interfold repository. It shows where each package lives and which tool versions to +install. It also lists the scripts that build the circuits and the Solidity verifiers, and it shows +how the Rust prover finds the artifacts. At the end, you can build the circuits for a preset and a +committee size. You can also use the circuit library in your own Noir project. -The canonical **package index** (paths, **CircuitName**, C0–C7 IDs) is -[`circuits/README.md`](https://github.com/theinterfold/interfold/blob/main/circuits/README.md) in -the open-source tree—Noir sources are the source of truth; treat docs as orientation only. +For what the circuits prove (the PV-TBFV phases and the circuit IDs C0 to C7), read +[Cryptography](/learn/cryptography) first. The Noir sources and their tests are the source of truth +for constraints and public inputs. ## Workspace layout -```text -circuits/ -├── lib/ # Shared library (all `bin/` packages depend on this) -│ └── src/ -│ ├── lib.nr -│ ├── configs/ # BFV presets (default / secure / insecure / committee) -│ ├── core/dkg/ # C0, C2–C4 (core logic) -│ ├── core/threshold/ # C1, C5, P3, C6, C7 (core logic) -│ └── math/ # Polynomials, SAFE sponge, ModU128, commitments -├── bin/ -│ ├── config/ # Pre-deployment preset consistency (`config` circuit) -│ ├── dkg/ # C0, C2 pipeline, C3, C4 -│ ├── threshold/ # C1, C5, user_data_encryption*, C6, C7 -│ └── recursive_aggregation/ # AB combiners (c2ab/c3ab/c4ab_fold), per-step -│ # folds (c3_fold[_kernel], c6_fold[_kernel]), -│ # per-node node_fold + cross-node nodes_fold -│ # [_kernel], dkg_aggregator, decryption_aggregator -└── benchmarks/ # Timing scripts and reports -``` +All binary packages depend on one shared library. The Nargo package name of the library is `lib` +(`circuits/lib/Nargo.toml`). + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +| Directory | Contents | +| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `lib/src/math` | Polynomials, the SAFE sponge (Poseidon2 and Keccak256), modular arithmetic, and the commitment functions with their domain separators. | +| `lib/src/core` | The circuit logic that the binary packages call. `core/dkg` holds C0 and C2 to C4. `core/threshold` holds C1, C5, the two user-encryption legs, C6, and C7. | +| `lib/src/configs` | The BFV presets (`insecure`, `secure`) and the committee sizes. `default/mod.nr` and `committee/active.nr` select the active pair. The build script writes both files. | +| `bin/dkg` | Nargo workspace: `pk` (C0), `sk_share_computation` (C2a), `e_sm_share_computation` (C2b), `share_encryption` (C3), `share_decryption` (C4). | +| `bin/threshold` | Nargo workspace: `pk_generation` (C1), `pk_aggregation` (C5), `user_data_encryption_ct0` and `user_data_encryption_ct1` with their wrapper `user_data_encryption` (P3), `share_decryption` (C6), `decrypted_shares_aggregation` (C7). | +| `bin/recursive_aggregation` | Twelve packages that fold proofs together. The two final circuits are `dkg_aggregator` and `decryption_aggregator`. | +| `bin/config` | Package `validate_secure_configs`. It checks the secure preset constants (CRT moduli, bounds, and parity matrices). | +| `benchmarks` | Timing scripts and reports. | + +The package directory names match the `CircuitName` enum in +`crates/events/src/interfold_event/proof.rs`. For example, `CircuitName::PkBfv` is `pk`, and +`CircuitName::DkgAggregator` is `dkg_aggregator`. The full package index is in +[`circuits/README.md`](https://github.com/theinterfold/interfold/blob/main/circuits/README.md). + +## How the proofs combine + +Each ciphernode proves its DKG steps with the inner circuits. The recursive aggregation packages +fold these proofs, first for each ciphernode and then across the committee. Only the two aggregator +proofs go on-chain. + + + +The decryption path is shorter. The C6 proofs of the committee go through `c6_fold`, and +`decryption_aggregator` adds C7. The `*_kernel` packages make the first proof of each sequential +fold (`c3_fold`, `c6_fold`, `nodes_fold`). + +The Solidity verifiers for the two aggregators are committed to the repository as +`DkgAggregatorVerifier.sol` and `DecryptionAggregatorVerifier.sol`. `BfvPkVerifier` and +`BfvDecryptionVerifier` hold the address of one of these verifiers and call it through +`ICircuitVerifier`. -Binary packages map to **C0–C7** and operational phases as on the [Cryptography](./cryptography) -page (**Phases & Circuits** section). Large statements (especially **C2**) use **inner** proofs that -are then combined inside `recursive_aggregation/` (AB combiners → per-step folds → `node_fold` per -party → `nodes_fold` across parties → `dkg_aggregator` / `decryption_aggregator`). +## Toolchain -## Using `circuits/lib` from another Noir project +A circuit build needs two tools: `nargo` compiles Noir, and `bb` (Barretenberg) writes verification +keys, proofs, and Solidity verifiers. Use the pinned versions. Artifacts from other versions do not +match the prover or the committed verifiers. -Add a path or git dependency in your `Nargo.toml` (pin a **tag or rev** that matches your prover): +| Component | Version | Pinned in | +| ------------------------------------ | ------------------------ | ------------------------------------------------------------------------------------------------------------------ | +| `nargo` | `1.0.0-beta.26` | `NOIR_TOOLCHAIN` in `.github/workflows/ci.yml`. The Noir crates in `crates/zk-prover/Cargo.toml` use the same tag. | +| `bb` | `5.1.0` | `required_bb_version` in `crates/zk-prover/versions.json`, and `BB_VERSION` in `ci.yml` | +| Circuit release archive | `0.18.0` | `required_circuits_version` in `crates/zk-prover/versions.json` | +| `bb_proof_verification` (Noir) | `v5.1.0` | The `Nargo.toml` of each package that verifies a proof | +| `@aztec/bb.js`, `@noir-lang/noir_js` | `5.1.0`, `1.0.0-beta.26` | `packages/interfold-sdk/package.json` | -```toml -[dependencies] -interfold_lib = { git = "https://github.com/theinterfold/interfold", tag = "v0.2.2", directory = "circuits/lib" } -``` + -Check `crates/zk-prover/Cargo.toml` / `versions.json` in the same release for compatible **nargo** -and **bb** versions. +### Install nargo -## Toolchain +Install `noirup`, then use it to install the pinned compiler: -Building the circuits needs two tools, and each has its own installer (or rely on CI / Docker): +```bash +curl -L https://raw.githubusercontent.com/noir-lang/noirup/main/install | bash +export PATH="$PATH:$HOME/.nargo/bin" +noirup -v v1.0.0-beta.26 +``` -- **nargo** — the Noir compiler, installed through `noirup` -- **bb** — the [Barretenberg](https://barretenberg.aztec.network/docs/getting_started) proving - backend, installed through the Interfold CLI +The installer adds `~/.nargo/bin` to your shell profile. The `export` line makes it available in the +current shell. -### nargo +### Install bb -The script below installs `noirup`, the version manager. Run `noirup` afterwards to get the pinned -compiler: +Run `interfold noir setup`. It downloads `bb` 5.1.0, checks it against the checksum in +`versions.json`, and installs the circuit release archive: ```bash -curl -L https://raw.githubusercontent.com/noir-lang/noirup/main/install | bash -export PATH="$PATH:$HOME/.nargo/bin" # installer writes this to your shell profile, not this shell -noirup -v v1.0.0-beta.26 # NOIR_TOOLCHAIN in .github/workflows/ci.yml -nargo --version +interfold noir setup ``` -Both `noirup` and `nargo` land in `~/.nargo/bin`, so the export above covers the rest of this -session. New shells pick it up from the profile the installer edited. +Without an Interfold config file, the CLI installs into `~/.interfold/noir`: `bin/bb`, `circuits/`, +and `version.json`. With a config file, it uses `.interfold/noir` next to that file. -### bb +### Put bb on your PATH -`interfold noir setup` fetches the Barretenberg version pinned in `crates/zk-prover/versions.json` -(`required_bb_version`), so your prover and your local builds agree: +The repository scripts call `bb` from your `PATH`. The CLI does not add its copy to your `PATH`. Add +it, or install `bb` from the Barretenberg release as CI does: ```bash -interfold noir status -interfold noir setup -interfold noir setup --force # reinstall +export PATH="$HOME/.interfold/noir/bin:$PATH" +``` + +### Check the versions + +```bash +nargo --version +bb --version ``` -The CLI keeps this copy under `~/.interfold/noir` instead of on your `PATH`, so -`interfold noir status` is what reports the installed version. +`nargo` reports `1.0.0-beta.26`. `bb` reports `5.1.0`. + + + +## `interfold noir` commands + +`interfold noir` manages the `bb` binary and the circuit artifacts that a ciphernode uses +(`crates/cli/src/noir.rs`). + +| Command | What it does | +| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `interfold noir status` | Prints the path and version of `bb` and of the circuits, then the status: `Ready`, `Barretenberg needs update`, `Circuits need update`, or `Setup required`. | +| `interfold noir setup` | Installs what is missing or out of date. If everything matches `versions.json`, it prints the versions and stops. | +| `interfold noir setup --force` | Downloads `bb` and the circuit archive again. | +| `interfold noir setup --circuits-archive ` | Installs the circuits from a local release archive instead of the download. | + +The circuit archive comes from the GitHub release `v0.18.0` (`circuits-0.18.0.tar.gz`). The node +selects the artifact directory from the BFV parameter set and the committee size of each E3. -## Compile, format, test +## Build the circuits in the repository -From the **repository root**: +Run the build from the repository root. It needs `nargo` and `bb` on your `PATH`, the Rust toolchain +(the script runs two Rust generators), and the JavaScript dependencies (`pnpm install`). ```bash -pnpm build:circuits # default: --preset insecure-512 --committee minimum +pnpm build:circuits # insecure-512, minimum pnpm build:circuits --preset secure-8192 --committee small -pnpm check:committee # verify active.nr, utils.ts, parity files agree -./scripts/lint-circuits.sh # nargo fmt --check && nargo check (lib + bin areas) -./scripts/test-circuits.sh # nargo test on circuits/lib ``` -**Preset** (`insecure-512` | `secure-8192`) selects the BFV parameter set; **committee** (`minimum` -| `micro` | `small`) selects `(N, T, H)` for secret sharing. Always switch committee via -`pnpm build:circuits --committee ` — see -[`scripts/README.md`](https://github.com/theinterfold/interfold/blob/main/scripts/README.md#circuit-builder) -and -[`circuits/benchmarks/README.md`](https://github.com/theinterfold/interfold/blob/main/circuits/benchmarks/README.md). +A build selects one BFV preset and one committee size: + +| Option | Values | Default | +| ------------- | ------------------------------------ | -------------- | +| `--preset` | `insecure-512`, `secure-8192`, `all` | `insecure-512` | +| `--committee` | `minimum`, `micro`, `small`, `all` | `minimum` | | Committee | N | T | H | | --------- | --- | --- | --- | -| Minimum | 3 | 1 | 2 | -| Micro | 9 | 4 | 5 | -| Small | 19 | 9 | 14 | +| `minimum` | 3 | 1 | 2 | +| `micro` | 9 | 4 | 5 | +| `small` | 19 | 9 | 14 | + +N is the committee size, T is the threshold, and H is the number of honest parties that the DKG +expects. Decryption needs T + 1 shares. The mainnet contracts accept only the secure parameter set. +Sepolia and local chains accept both sets (`ActiveCryptoConfig.sol`). + +For each pair, the build does these steps (`scripts/build-circuits.ts`): + +1. It writes `configs/default/mod.nr` and `configs/committee/active.nr` for the pair. +2. It regenerates the parity matrices and the C1 and C2 bounds with the Rust generators. +3. It updates `packages/interfold-contracts/scripts/utils.ts` and + `packages/interfold-contracts/contracts/lib/ActiveCryptoConfig.sol`. +4. It compiles every package in `bin/dkg`, `bin/threshold`, and `bin/recursive_aggregation` with + `nargo compile`. +5. It writes verification keys with `bb write_vk`. Each base circuit gets `evm`, + `noir-recursive-no-zk`, and `noir-recursive` keys. Each fold circuit gets a + `noir-recursive-no-zk` key, and the two aggregators also get an `evm` key. +6. It copies the artifacts to `dist/circuits///`, 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. + +