Enclave is now The Interfold.
CRISP
Introduction

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:

PropertyHow CRISP provides it
Secret ballotThe 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 resistanceA 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.
EligibilityA Noir proof shows that the slot is in the round census, or that the slot has on-chain voting power.
Verifiable resultCRISPProgram.verify checks an OpenVM proof of the sum. The Interfold contract checks the decryption proof when the committee publishes the result.
Sender privacyThe 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.

  1. EncryptThe client encrypts the ballot and makes a Noir proof.
  2. CommitThe server stores the bytes. publishInput checks the proof.
  3. SumThe E3 program adds one ciphertext per slot.
  4. DecryptT + 1 ciphernodes decrypt the sum.
  5. 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 partCRISP componentPath
ClientReact 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 circuitsNoir 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 contractCRISPProgram.sol. It implements validate, publishInput, and verify, and adds finalizeInput and decodeTally.packages/crisp-contracts/
Secure processfhe_processor adds the BFV ciphertexts. policy defines the input leaf and selects one entry per slot.program/
Coordination serverRust 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.

FunctionCallerWhat it does
validateInterfold contract, at the requestDecodes 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.
setMerkleRootOwner (the CRISP server key)Stores the census Merkle root for a round that uses a census tree.
publishInputVoter wallet or server relayVerifies the Noir proof and the availability signature of the server. Reserves the input leaf and emits InputCommitted.
finalizeInputAny account (usually the server)Verifies the data-availability receipt for the committed ciphertext hash. Emits InputPublished.
verifyInterfold contract, at the outputReverts with InputAvailabilityPending while an input waits for finalization. Checks the OpenVM receipt against the parameters hash and the input root.
decodeTallyAnyone (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 modeEligibility sourceVerifier
TOKENToken holders at a snapshot. The server builds the tree and posts its root.CRISPVerifier.sol
BY_REQUESTERThe requesting contract returns the list from getCensus(uint256 e3Id).CRISPVerifier.sol
ONCHAINThe 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):

  1. 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).
  2. The relay or the voter wallet sends publishInput. The contract reserves the tree position of the input immediately.
  3. 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.

NetworkData availability
Local chain (31337)mock. The server keeps the bytes and completes both transactions in the same request. availabilityFinalizationWindow is 0.
Sepoliamock, while Avail Turing is down. The server keeps the bytes, and the program uses MockCrispDataAvailabilityVerifier.
Ethereum mainnetAvail. 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
  • PathContents
    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.yamlCiphernode configuration: chains, contract addresses, program mode, and the five local nodes cn1 to cn5.
    crisp.dev.env.exampleTemplate for the local development profile (crisp.dev.env).
    .env.exampleTemplate for the keys that scripts/setup_testnet.sh reads.
    interfold-release.jsonThe Interfold release that CRISP builds against, and the SHA-256 digest of each release asset that CRISP downloads.
    docker-compose.yamlAn 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