Run all commands from the zk/ directory.
- Scarb 2.14.0 (Cairo build toolchain)
- Node.js 18+ with npm
- A Starknet Sepolia wallet with testnet ETH/STRK
cd contracts
scarb buildcd zk
npm installcp zk/.env.example zk/.envEdit zk/.env with your Starknet Sepolia wallet credentials:
ACCOUNT_ADDRESS=0x...
PRIVATE_KEY=0x...
RPC_URL=https://starknet-sepolia-rpc.publicnode.comNote: Deploying the Groth16 verifier (~2MB contract) requires a private RPC (e.g. Alchemy, Infura). Public RPCs may reject the large declare transaction.
npm run download-artifactsnpm run gen-identityCreates zk/.local/identity.json with a Semaphore identity (private key + commitment).
npm run deployDeploys all 4 contracts: Groth16VerifierBN254, Semaphore30Verifier, VoterSetRegistry, Poll. Addresses are saved to zk/.local/contract_addresses.json.
Each poll has its own independent voter set. The first caller to add-eligible for a given poll_id becomes its admin. Complete all steps in this phase before creating the poll.
# The first call for poll 3 makes the caller its admin
npm run interact -- add-eligible 3 <your-wallet-address>Each eligible voter self-registers their Semaphore commitment. Use the commitment printed in Step 1:
COMMITMENT=$(jq -r '.commitment' ./.local/identity.json)
npm run interact -- register-commitment 3 $COMMITMENTnpm run interact -- freeze-registry 3Permanently locks the voter set for poll 3. Only the poll admin can freeze. Must be done before creating the poll.
npm run fetch-leaves -- 3Reads all committed leaves for poll 3 from the registry. Saved to zk/.local/leaves.json.
npm run compute-rootBuilds the depth-30 Poseidon Merkle tree from the leaves. Saved to zk/.local/merkle_root.json.
START=$(( $(date +%s) - 60 ))
END=$(( $(date +%s) + 300 ))
ROOT=$(jq -r '.root' ./.local/merkle_root.json)
npm run interact -- create-poll 3 2 $START $END $ROOT "Alice" "Bob"3— poll ID2— number of options (must match the number of labels)$START / $END— Unix timestamps;startslightly in the past so voting is immediately open"Alice" "Bob"— candidate names stored on-chain for UI display only, not part of the ZK proof
Create zk/.local/proof_config.json:
{
"poll_id": 3,
"option": 0,
"leaf_index": 0
}poll_id— must match the poll created in Step 8option— 0-indexed option to vote for (0 = Alice, 1 = Bob)leaf_index— index of your commitment in the leaves list from Step 6
npm run gen-proofGenerates the Semaphore Groth16 proof. Outputs zk/samples/proof.json and zk/samples/public.json.
npm run format-calldataConverts the proof into Garaga-compatible calldata. Outputs zk/samples/worldcoin_calldata.json.
npm run interact -- submit-vote samples/worldcoin_calldata.jsonSubmits the vote transaction with the ZK proof. The contract verifies the proof on-chain.
# Votes for option 0 (Alice)
npm run interact -- get-tally 3 0
# Votes for option 1 (Bob)
npm run interact -- get-tally 3 1# All labels at once (returns { "0": "Alice", "1": "Bob" })
npm run interact -- get-option-labels 3
# Single label
npm run interact -- get-option-label 3 0After end_time has passed, anyone can finalize the poll to compute and store the winner on-chain:
npm run interact -- finalize 3
# View full poll data including winner
npm run interact -- get-poll 3Attempting to submit the same proof again should fail with 'Nullifier already used'.
| Script | Command | Description |
|---|---|---|
| gen-identity | npm run gen-identity |
Generate Semaphore identity |
| deploy | npm run deploy |
Deploy all contracts |
| interact | npm run interact -- <cmd> |
Contract interaction (see subcommands) |
| fetch-leaves | npm run fetch-leaves -- <pollId> |
Fetch voter leaves for a poll |
| compute-root | npm run compute-root |
Compute Merkle root from leaves |
| gen-proof | npm run gen-proof |
Generate Semaphore ZK proof |
| format-calldata | npm run format-calldata |
Format proof as Garaga calldata |
| download-artifacts | npm run download-artifacts |
Download Semaphore30 wasm/zkey artifacts |
| Subcommand | Usage |
|---|---|
| add-eligible | npm run interact -- add-eligible <pollId> <address1> [address2 ...] |
| register-commitment | npm run interact -- register-commitment <pollId> <commitment> |
| freeze-registry | npm run interact -- freeze-registry <pollId> |
| create-poll | npm run interact -- create-poll <pollId> <optionsCount> <startTime> <endTime> <root> <label0> <label1> ... |
| submit-vote | npm run interact -- submit-vote <calldata.json> |
| get-tally | npm run interact -- get-tally <pollId> <option> |
| get-option-label | npm run interact -- get-option-label <pollId> <option> |
| get-option-labels | npm run interact -- get-option-labels <pollId> |
| finalize | npm run interact -- finalize <pollId> |
| get-poll | npm run interact -- get-poll <pollId> |