CRISP (Coercion-Resistant Impartial Selection Protocol) is a secret-ballot voting protocol built with the Interfold. It uses fully homomorphic encryption (FHE) and distributed threshold cryptography (DTC). Each voter encrypts a ballot in the browser. The Secure Process adds the encrypted ballots, and a threshold committee of ciphernodes decrypts only the combined result, which is public. On-chain contracts verify the proofs of the tally and of its decryption.
Vote masking makes receipts weaker, which makes coercion and vote buying more difficult. These protections depend on the conditions in Privacy limits. To learn more about CRISP, read our blog post or the documentation.
CRISP follows a modern structure with clear separation of concerns, consistent with the Interfold root structure.
CRISP/
├── client/ # React frontend application (Vite + @crisp-e3/sdk)
├── server/ # Rust coordination server & CLI
├── program/ # FHE policy proved with OpenVM
├── packages/
│ ├── crisp-contracts/ # CRISP program contract + Hardhat deployment scripts
│ └── crisp-sdk/ # TypeScript helpers to generate a ZK proof
├── crates/ # Rust libraries used by the server
├── circuits/ # Noir zero-knowledge circuits
├── scripts/ # Development scripts for running, testing, and deployment
├── interfold.config.yaml # Local ciphernode network config
└── docker-compose.yaml # Optional multi-node deploymentCRISP builds against one release of the Interfold repository. Each kind of dependency names the release in its own file:
interfold-release.json: the version, and the SHA-256 digest of each release asset that CRISP downloads.scripts/interfold-release.mjsdownloads an asset and refuses it when the digest does not match. The assets are the interfold CLI, whichpnpm dev:setupand CI install, and the circuit archive, which supplies the compileduser_data_encryptioncircuits.Cargo.tomlandguest/Cargo.toml: the Rust crates (e3-*), Cargo git dependencies on the release tag.circuits/lib/Nargo.tomlandcircuits/bin/*/Nargo.toml: the Noir library (interfold_lib), a Nargo git dependency on the release tag.package.jsonandpackages/crisp-contracts/package.json: the npm packages@interfold/configand@interfold/contracts.- The ZK toolchain follows the release's
crates/zk/prover/versions.jsonandci.yml:nargo(NOIR_TOOLCHAINin.github/workflows/ci.yml), thebb_proof_verificationtag incircuits/bin/fold*/Nargo.toml, and@aztec-foundation/bb.jsand@noir-lang/noir_jsinpackages/crisp-sdk/package.json. The@noir-lang/noir_jsoverride inclient/package.jsontracks the published SDK and moves only with a production publish (seeRELEASING.md).
To move to release <version>:
-
Set
versionininterfold-release.json, and set each digest to the SHA-256 of the asset in the releasev<version>. -
Set
tag = "v<version>"on everye3-*dependency inCargo.tomlandguest/Cargo.toml, and on everyinterfold_libdependency in theNargo.tomlfiles. -
Set the
@interfold/*versions to<version>, and move the toolchain pins above when the release moved them. -
Update the lockfiles:
cargo update -p e3-sdk (cd guest && cargo update -p e3-safe) pnpm installEach
cargo update -pcommand updates all crates from the interfold repository, because they share one git source. -
Run
pnpm compute:vk-hash secure-8192, thenpnpm compute:vk-hash insecure-64, so thatcircuits/lib/src/preset.nrends on the committedinsecure-64preset. When a printed hash differs from the*_EXPECTED_KEY_HASH_*constant incircuits/bin/fold/src/main.nrorcircuits/bin/fold_onchain/src/main.nr, replace the constant. -
Run
pnpm build:presets:all, which regenerates the verifiers inpackages/crisp-contracts/contracts/verifiers/and restages both presets, then rebuild and check the SDK:pnpm -C packages/crisp-sdk build:prodandCRISP_CHANNEL=latest pnpm -C packages/crisp-sdk check:presets.
You can have an extended explanation of the single folders in the dedicated documentation.
Before getting started, ensure you have installed:
- Rust — see
rust-toolchain.tomlfor the pinned version - Foundry
- NodeJS — CI uses 22.x (
NODE_VERSIONin.github/workflows/ci.yml). Odd-numbered releases such as 25.x make Hardhat print an unsupported version warning on every invocation - pnpm
- MetaMask
yq—scripts/dev_cipher.shreads node addresses with it- Noir toolchain:
nargoandbb. Install the versions CI pins, since other versions may not compile the circuits:nargo:noirup -v v1.0.0-rc.3(NOIR_TOOLCHAINin.github/workflows/ci.yml)bb: version and per-platform checksums live in Interfold'scrates/zk/prover/versions.json
Local development runs the unproved development runner unless CRISP_REAL_PROOFS=1 (see
crisp.dev.env.example). Real proofs use OpenVM: build the workers, configure program.openvm in
interfold.config.yaml, and run interfold program compile. Follow
crates/openvm-prover/README.md. A machine with a working
CUDA GPU proves on it when the CUDA worker is configured; any other machine proves on the CPU.
The CRISP deployment reads the receipt identity interfold program compile wrote, unless
OPENVM_APP_EXE_COMMIT, OPENVM_APP_VM_COMMIT, and either OPENVM_VERIFIER_ARTIFACT with
OPENVM_VERIFIER_SHA256 or OPENVM_HALO2_VERIFIER with OPENVM_HALO2_RUNTIME_CODE_HASH are set.
Outside CRISP_UNPROVED_TEST=1, it never selects a mock compute verifier. CRISP_UNPROVED_TEST=1
deploys MockOpenVmReceiptVerifier, which accepts every receipt. It works on the local chain (chain
ID 31337), and on Sepolia (chain ID 11155111) only with ALLOW_SEPOLIA_UNPROVED_COMPUTE=true. Every
other chain, mainnet included, refuses it. A Sepolia deployment with this verifier does not verify
compute proofs. It runs program-runner dev, the unproved development runner, instead of the OpenVM
service.
The simplest way to run CRISP is:
git clone https://github.com/theinterfold/CRISP.git
cd CRISP
# Optional: choose local profile (copied to crisp.dev.env on first setup)
cp crisp.dev.env.example crisp.dev.env
# Edit CRISP_SKIP_PROOF_AGGREGATION and CRISP_BFV_PRESET (see docs/PROOF_AGGREGATION_AND_ZK.md)
# Install dependencies and build everything (creates server/.env from the example if missing)
pnpm dev:setup
# Start all services (Hardhat, contracts, ciphernodes, program server, coordination server, and UI)
pnpm dev:upThe program server accepts caller-supplied HTTP(S) callback URLs. It is a development-only test service, does not authenticate callers or allowlist callback destinations, and must stay isolated from production and untrusted networks.
dev:up runs scripts/dev.sh, which:
- Starts the Hardhat node in
packages/crisp-contracts - Deploys all contracts (Interfold, CRISPProgram, verifiers, registries) via
scripts/crisp_deploy.sh - Watches the local mock randomness provider and fulfills each request in a later block
- Starts ciphernodes using
interfold.config.yamlviascripts/dev_cipher.sh - Launches the program server via
scripts/dev_program.sh - Starts the coordination server (Rust) via
scripts/dev_server.shon port4000 - Starts the React client via
scripts/dev_client.shon port3000
All services run concurrently and will automatically restart if needed.
While pnpm dev:up runs everything together, you can also run components separately:
# Start only the Hardhat node
cd packages/crisp-contracts && pnpm hardhat node
# Start only the ciphernodes (requires Hardhat running).
# The argument is the ready-file the script creates once the nodes are registered.
./scripts/dev_cipher.sh ./.interfold/ready
# Start only the program server (requires ciphernodes)
./scripts/dev_program.sh
# Start only the coordination server (requires program server)
./scripts/dev_server.sh
# Start only the client (requires coordination server)
./scripts/dev_client.sh# Recompile the Noir circuits and generate verifiers (insecure-64; pass secure-8192 for the other
# preset)
pnpm compile:circuits
# Open the interactive CLI to start voting rounds
pnpm cli
# Run end-to-end tests
pnpm test:e2eThe interfold.config.yaml file in the CRISP root directory configures the ciphernode network. It
sets program.dev: false. The local scripts override that with E3_PROGRAM__DEV=true unless
CRISP_REAL_PROOFS=1, so a local round uses the unproved development runner by default.
The real-proof compute service uses OpenVM. CRISP's guest is guest/, and its service is the
program-runner binary of runner/; the guest and the runner both link program/, the processor
and policy the contract agrees with. Follow the
OpenVM guide to build the workers, then run
interfold program compile for the guest, keys, identity and service.
Set program.dev: false and supply deployment-local program.openvm.prover_bin and, on a GPU
machine, program.openvm.prover_bin_cuda paths. Do not put account keys, proving artifacts, or
machine-specific values in the shared configuration.
A new OpenVM deployment requires matching receipt and ciphertext-duty verifiers. Existing RISC Zero deployments do not become compatible by changing the service configuration. Drain active rounds and use a separate reviewed migration before changing a live verifier route. The old Boundless upload and auction settings are not used by this backend.
Local development uses DATA_AVAILABILITY_MODE=mock. The mock keeps the full input and aggregate
ciphertext in the CRISP server database and produces a deterministic local receipt. It does not
model VectorX latency or Avail fees.
Ethereum mainnet uses Avail. On Sepolia, CRISP and the protocol ciphertext verifier use
MockOpenVmReceiptVerifier, which accepts every OpenVM receipt. Ethereum mainnet uses real RISC
Zero verification. CRISPProgram on Sepolia is 0x25Df8D3C04D67efb6198E215963d19a8d119ADFF. While
Avail Turing is unavailable, it uses MockCrispDataAvailabilityVerifier
(0xB5cDE772357C9dB2d2515Cf94Daa0696c9a2FB17): run its server with DATA_AVAILABILITY_MODE=mock
and ALLOW_SEPOLIA_MOCK_DATA_AVAILABILITY=true, and point the ciphernodes at the server's
/availability route with mode: mock_http. Its two Noir verifiers come from the circuits in this
directory, built with Noir v1.0.0-rc.3 and bb 6.0.0.
Before starting the CRISP server with Avail:
- Register an Avail App ID for CRISP.
- Fund a dedicated Avail account that can pay for every
submit_datatransaction. - Keep the server database durable. It stores each pending publication until its VectorX proof is available and resumes the job after a restart.
- Deploy CRISP with
INPUT_AVAILABILITY_SIGNERset to the Ethereum address derived from the server'sPRIVATE_KEY. On every network, the serverPRIVATE_KEYmust be the key of the signer address that CRISPProgram stores. The current Sepolia deployment usedCRISP_UNPROVED_TEST=1 ALLOW_SEPOLIA_UNPROVED_COMPUTE=true USE_MOCKS=true MOCK_DATA_AVAILABILITY=true.USE_MOCKS=truedeploys the mock voting token and selects the mock data-availability verifier, unlessMOCK_DATA_AVAILABILITY=falsekeeps Avail. It does not select a compute mock: every network except the isolated local chain deploys the real OpenVM receipt verifier, unlessCRISP_UNPROVED_TEST=1andALLOW_SEPOLIA_UNPROVED_COMPUTE=trueopt in to the unproved verifier on Sepolia. Ciphernodes read all inputs on a chain from one data-availability source, so keep Avail on a shared network. - Schedule voting after the current on-chain committee setup budget. The server reads that bound
from
CRISPProgram.earliestVotingStart()and addsVOTING_START_BUFFER_SECONDSfor transaction mining.E3_DURATIONstarts at that fixed voting time; it covers voting plus the VectorX finalization tail, not committee setup.
The server signs a 10-minute commitment payload only after it stores and validates the complete ciphertext. If the commitment does not reach Ethereum before that payload expires, the server waits for Ethereum finality, releases the uncommitted bytes, and lets the voter stage the vote again.
# Sepolia + Avail Turing
DATA_AVAILABILITY_MODE=avail
AVAIL_RPC_URL=https://turing-rpc.avail.so/rpc
AVAIL_BRIDGE_API_URL=https://turing-bridge-api.avail.so
AVAIL_APP_ID=<registered-app-id>
AVAIL_SEED=<dedicated-funded-secret-uri>
AVAIL_PROOF_LEAD_SECONDS=10800
# Unfinished objects are refused once they reach this capacity. Size it for the largest supported
# round and monitor the server volume. Default: 1 GiB.
DATA_AVAILABILITY_MAX_PENDING_BYTES=1073741824
# Input-window duration after voting starts. The minimum production example is 4 hours:
# 1 hour voting + 3 hours VectorX finalization. Twelve hours provides 9 hours of voting.
E3_DURATION=43200
# Extra time for the E3 request transaction to be mined before the fixed voting start.
# Default: 120 seconds.
VOTING_START_BUFFER_SECONDS=120
# Required by the cron client and the protected POST /rounds/request endpoint.
# Use a nonempty secret whenever the endpoint is exposed.
CRON_API_KEY=<random-secret>
# Remote cron targets must use HTTPS. Plain HTTP is accepted only for loopback development.
INTERFOLD_SERVER_URL=https://crisp.example
# Ethereum mainnet uses these two endpoints instead:
# AVAIL_RPC_URL=https://avail-rpc.publicnode.com/
# AVAIL_BRIDGE_API_URL=https://bridge-api.avail.soDo not embed CRON_API_KEY in a browser bundle. Use it only from protected automation. The cron
client and SDK round-request method reject remote plain HTTP and redirects before they send the
secret. Plain HTTP is accepted only for localhost, 127.0.0.0/8, and [::1] development
endpoints.
DAO deployments can set DEFER_PROTOCOL_WIRING=true to deploy CRISP before the governance wiring
transaction. On Ethereum mainnet, the deployment also requires ALLOW_MAINNET_DEFERRED_WIRING=true
as an explicit acknowledgement that CRISP is unusable until the DAO batch is executed and validated.
Keep E3 requests paused throughout that interval.
The server first validates the Noir proof and durably stores the exact encrypted bytes. It signs a
compact proof commitment only after storage succeeds. The server relays that commitment, or the
voter submits it from their wallet, and the voter can then leave. Relaying is configurable in
server/.env.example, and it is off on Ethereum mainnet by default. The server publishes the
ciphertext to Avail, waits for the official VectorX proof, and finalizes the input without the
voter. The proof transaction reserves the input's tree index immediately, so masks and revotes can
still extend it during the VectorX wait. CRISP refuses the aggregate computation while any input is
not finalized.
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's wallet sends the commitment, which shows the voter's address. The voter must confirm it before the commitment cutoff of the round: stay on the page, or come back and repeat the action. Otherwise the input is lost.
Each distinct input gets its own job, so a pending mask cannot block a vote for the same slot. The service limits the total bytes held by unfinished jobs, which bounds abandoned signed inputs without deleting data that Ethereum already accepted. After Avail and Ethereum accept an object, the service removes its staging copy because Avail is then the recovery source.
The aggregate ciphertext follows the Avail and VectorX path after its OpenVM proof is ready.
Each accepted Ethereum reference contains keccak256(exact bytes). The CRISP server and ciphernodes
re-hash retrieved bytes before they use them. An App ID helps indexing, but it is not a security
boundary.
The pnpm dev:setup command automatically creates .env files for the server and client from the
.env.example templates (if they don't already exist).
After pnpm dev:up, contract addresses are written automatically to interfold.config.yaml,
server/.env, and client/.env (no manual copy from deployed_contracts.json).
Edit crisp.dev.env (created from crisp.dev.env.example on first pnpm dev:setup):
| Variable | Default | Effect |
|---|---|---|
CRISP_BFV_PRESET |
insecure-64 |
BFV preset for aggregation circuits and the server E3_PARAM_SET |
CRISP_SKIP_PROOF_AGGREGATION |
true |
Ciphernode-only local-dev skip; also selects mock verifier deployment |
pnpm dev:setup applies this profile and installs the matching interfold CLI. pnpm dev:up
deploys contracts using the same flags.
Re-run pnpm dev:setup after changing CRISP_SKIP_PROOF_AGGREGATION. The setting is only
honoured by an interfold binary built with the matching test-only-skip-proof-aggregation Cargo
feature, so dev:setup selects the build from the profile and reinstalls the CLI. The release
publishes the test-only build for Linux x86_64 only; on other platforms dev:setup builds it from
the release tag with cargo install. Running dev:up against a binary from the other profile makes
every ciphernode exit at startup; dev_cipher.sh now aborts with the node status table instead of
continuing. Note that ~/.cargo/bin/interfold is shared — a dev:setup in another project
overwrites the binary this profile installed.
See docs/PROOF_AGGREGATION_AND_ZK.md for modes, address
sync, and troubleshooting (VkHashMismatch, etc.).
Deploy from client. The build uses the published @crisp-e3/sdk package on
npm (pnpm install --ignore-workspace), not the workspace. The published SDK package
already contains the staged preset circuits, so Vercel does not compile Noir circuits.
- Project root directory:
client vercel buildin CI: run from the repository root (notcd clientfirst)- Optional Vercel env:
ENABLE_EXPERIMENTAL_COREPACK=1
Commit client/pnpm-lock.yaml after dependency bumps
(pnpm install --ignore-workspace in that directory) for reproducible installs.
## Publishing packages to npm
In order to publish a new version of the CRISP packages to npm, select the release channel:
pnpm publish:packages --channel prod x.x.x
pnpm publish:packages --channel testing x.x.x-insecure.0We welcome and encourage community contributions to this repository. Please ensure that you read and understand the Contributor License Agreement (CLA) before submitting any contributions.
To help keep the repository clean and maintainable, we automatically delete merged branches after
7 days.
You can control this behavior using PR labels:
| Label | Effect |
|---|---|
keep-branch |
❌ Branch will not be deleted |
archive-branch |
🏷️ Branch will be tagged and then deleted |
| no label | 🗑️ Branch will be deleted (no tag preserved) |
Only apply these labels before merging your PR if you want to preserve history or keep the branch alive.
This project is provided WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
This repository is licensed under the LGPL-3.0+ license.