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. + +