Introduction to CRISP
This page explains what CRISP is, the problem that it solves, and how its parts map onto an E3. It is for developers who want to study a complete E3 application before they build their own. At the end, you know what each folder in the CRISP repository contains and which page to read next.
CRISP (Coercion-Resistant Impartial Selection Protocol) is a private voting application that runs on the Interfold. Voters encrypt their ballots, the E3 program adds the encrypted ballots, and a committee of ciphernodes decrypts only the final tally. The code is in the CRISP repository (opens in a new tab). CRISP has its own release cycle, separate from the protocol release. Each CRISP release builds against one released version of the Interfold.
The problem
An open ballot shows how each person votes. A briber can then pay for a vote, and a coercer can punish a voter who does not obey. A secret ballot hides the vote. But when a voter can prove the vote to a third party, bribery and coercion still work. That proof is called a receipt.
CRISP protects the secrecy of each ballot and makes receipts weaker. Each property depends on the conditions in Privacy limits:
| Property | How CRISP provides it |
|---|---|
| Secret ballot | The client encrypts each ballot with BFV under the committee public key. Only the encrypted sum is decrypted. Decryption needs T + 1 key shares, so no single ciphernode can decrypt. |
| Receipt resistance | A voter can replace their ballot in the same slot. Anyone can add a mask (an encrypted zero ballot) to any eligible slot. On-chain, a vote, an update, and a mask look the same. |
| Eligibility | A Noir proof shows that the slot is in the round census, or that the slot has on-chain voting power. |
| Verifiable result | CRISPProgram.verify checks an OpenVM proof of the sum. The Interfold contract checks the decryption proof when the committee publishes the result. |
| Sender privacy | The CRISP server can relay inputs, so the chain shows the relay address instead of the voter address. Relaying is configurable, and it is off on Ethereum mainnet by default. |
A mask adds an encryption of zero to the ciphertext that is already in a slot. The result is a new
ciphertext that decrypts to the same value. For this reason, the on-chain ciphertexts alone do not
show which ciphertext holds the final vote of a voter (circuits/bin/crisp/src/main.nr). This makes
a receipt weaker, but only under the conditions that follow.
Privacy limits
The privacy of a CRISP ballot depends on these conditions:
- Committee threshold. Decryption needs T + 1 key shares. If T + 1 or more committee members collude, they can decrypt individual ballots.
- Public result. The committee decrypts only the combined result, and that result is public. In some polls, for example small or one-sided polls, the result can show how individual participants voted.
- Sender. The CRISP server receives every input, because it attests that the input is available, so it sees each request. An input that the voter wallet sends also shows the voter address on-chain. When the CRISP server relays an input, the chain shows the relay address. Two-step inputs describes when the voter wallet must send the input.
How CRISP maps onto an E3
Each ballot goes through the same path. The client encrypts it, the CRISP program contract accepts it, the E3 program adds all ballots, and the committee decrypts the sum.
- EncryptThe client encrypts the ballot and makes a Noir proof.
- CommitThe server stores the bytes.
publishInputchecks the proof. - SumThe E3 program adds one ciphertext per slot.
- DecryptT + 1 ciphernodes decrypt the sum.
- DecodeThe server decodes one total per option.
The table shows which CRISP component fills each part of an E3. The E3 program overview describes these parts in general.
| E3 part | CRISP component | Path |
|---|---|---|
| Client | React and Vite application. It connects the wallet, encrypts and proves ballots, and shows results. | client/ |
| SDK | @crisp-e3/sdk. Ballot encryption, proof generation, server API calls, and decodeTally. | packages/crisp-sdk/ |
| Input circuits | Noir circuits that prove a ballot is valid. crisp and fold serve a census Merkle tree. crisp_onchain and fold_onchain serve on-chain voting power. | circuits/ |
| E3 program contract | CRISPProgram.sol. It implements validate, publishInput, and verify, and adds finalizeInput and decodeTally. | packages/crisp-contracts/ |
| Secure process | fhe_processor adds the BFV ciphertexts. policy defines the input leaf and selects one entry per slot. | program/ |
| Coordination server | Rust crate crisp with three binaries: server (indexer, relay, and REST API on port 4000), cli (requests rounds), and cron (requests a round every 24 hours). | server/ |
The program contract
CRISPProgram.sol holds the state of each round. The Interfold contract calls it when an E3 is
requested and when the encrypted output is published. The
program contract guide explains the IE3Program interface.
| Function | Caller | What it does |
|---|---|---|
validate | Interfold contract, at the request | Decodes the round parameters and starts an input Merkle tree of depth 20. A round must have 2 to 10 options. It sizes the round so that each option total stays below the plaintext modulus t (see Decode the tally). On Avail deployments, it also checks the input window timing. |
setMerkleRoot | Owner (the CRISP server key) | Stores the census Merkle root for a round that uses a census tree. |
publishInput | Voter wallet or server relay | Verifies the Noir proof and the availability signature of the server. Reserves the input leaf and emits InputCommitted. |
finalizeInput | Any account (usually the server) | Verifies the data-availability receipt for the committed ciphertext hash. Emits InputPublished. |
verify | Interfold contract, at the output | Reverts with InputAvailabilityPending while an input waits for finalization. Checks the OpenVM receipt against the parameters hash and the input root. |
decodeTally | Anyone (view) | Decodes the plaintext output into the total weight of each option. |
The round parameters include a census mode. The census mode selects who can vote and which Noir
verifier publishInput uses:
| Census mode | Eligibility source | Verifier |
|---|---|---|
TOKEN | Token holders at a snapshot. The server builds the tree and posts its root. | CRISPVerifier.sol |
BY_REQUESTER | The requesting contract returns the list from getCensus(uint256 e3Id). | CRISPVerifier.sol |
ONCHAIN | The contract reads voting power with getPastVotes for each input. | CRISPOnchainVerifier.sol |
Both verifier files are Honk verifiers generated from the compiled Noir circuits. Both declare a
contract named HonkVerifier, so the deploy script selects them by file name. The CRISP CLI can
request TOKEN rounds and ONCHAIN rounds. For an ONCHAIN round, the CLI uses SelfRegistry.sol
by default. Any account can call register() on it once, and a registered account has a voting
power of 1.
Two-step inputs
Secure BFV ciphertexts are too large for one Ethereum transaction. For this reason, each input goes
on-chain in two transactions, publishInput and finalizeInput. The sequence is
(docs/architecture/data-availability.md (opens in a new tab)
in the Interfold repository):
- The server validates the Noir proof, stores the exact ciphertext bytes, and signs an availability
promise. The promise is valid for 10 minutes at most (
INPUT_AVAILABILITY_ATTESTATION_TTL). - The relay or the voter wallet sends
publishInput. The contract reserves the tree position of the input immediately. - The server publishes the bytes to a data-availability layer. When the receipt is ready, the
server sends
finalizeInput. The voter does not need to stay online for this step.
The computation cannot start while a committed input waits for its receipt.
The relay has per-slot and per-round limits, and anyone can use up the relayed inputs of a slot with
masks. Past a limit, the voter wallet sends publishInput, which shows the voter address. The voter
must confirm it before the commitment cutoff, on the page or on a later visit, or the input is lost.
| Network | Data availability |
|---|---|
| Local chain (31337) | mock. The server keeps the bytes and completes both transactions in the same request. availabilityFinalizationWindow is 0. |
| Sepolia | mock, while Avail Turing is down. The server keeps the bytes, and the program uses MockCrispDataAvailabilityVerifier. |
| Ethereum mainnet | Avail. VectorX anchors the Avail data on Ethereum. |
For contract addresses, see Contract addresses.
Project Structure
The tree shows the top-level content of the CRISP repository (opens in a new tab) that is tracked in Git.
- .env.example
- Cargo.toml
- crisp.dev.env.example
- docker-compose.yaml
- Dockerfile
- interfold-release.json
- interfold.config.yaml
- package.json
- playwright.config.ts
- Readme.md
- RELEASING.md
- rust-toolchain.toml
| Path | Contents |
|---|---|
circuits/ | Noir ballot circuits in bin/ (crisp, crisp_onchain, fold, fold_onchain) and the shared library in lib/. |
client/ | The React and Vite voting client. |
crates/ | Rust libraries: crisp-utils (decode_tally), evm_helpers (contract bindings), zk-inputs and zk-inputs-wasm (circuit input generation). |
docs/ | PROOF_AGGREGATION_AND_ZK.md: local modes for DKG proof aggregation. |
packages/ | crisp-contracts (Hardhat project with CRISPProgram.sol), crisp-sdk (@crisp-e3/sdk), and crisp-zk-inputs (WASM bindings). |
program/ | The E3 program crate e3-user-program: fhe_processor and the input policy. |
runner/ | The crate e3-user-program-runner. Its program-runner binary serves program/ through e3-program-runner, as the dev runner or the OpenVM service. |
guest/ | The OpenVM guest that proves program/. Its own Cargo workspace, built by interfold program compile. |
scripts/ | Shell and Node scripts for setup, local services, circuits, tests, and publication. |
server/ | The coordination server, the CLI, and the cron client. |
test/ | The Playwright and Synpress end-to-end test. |
interfold.config.yaml | Ciphernode configuration: chains, contract addresses, program mode, and the five local nodes cn1 to cn5. |
crisp.dev.env.example | Template for the local development profile (crisp.dev.env). |
.env.example | Template for the keys that scripts/setup_testnet.sh reads. |
interfold-release.json | The Interfold release that CRISP builds against, and the SHA-256 digest of each release asset that CRISP downloads. |
docker-compose.yaml | An optional development container that mounts the repository and publishes ports 8545, 3000, and 4000. |
The OpenVM guest is in guest/, and the program runner (the proving service) is in runner/. Both
link program/, so they run the same fhe_processor and policy as the native host and agree with
CRISPProgram. interfold program compile builds the guest and writes its receipt identity, which
the CRISP deploy script reads unless OPENVM_* settings name another. CRISP builds and publishes
its own program runner image, e3-support, from runner/Dockerfile. The Interfold releases do not
include this image.
Further reading
Next steps
Install the prerequisites, build CRISP, and start the local stack.
CRISPRun a voting roundRequest a round, vote in the client, and decode the tally.
CRISPE3 program anatomyThe parts that every E3 program has, and how they connect.
BuildNoir circuitsHow input circuits prove that an encrypted input is valid.
Build