Documents

ZK privacy

Anonymous signals with zero-knowledge proofs.

Status: IN DEVELOPMENT. Deployed on Base Sepolia at 0xFa5901E9dfD813b910061F29ef1f65a1faA0b268; not on Base Mainnet.

Members of a group can signal (vote in a poll, attest, give feedback) without revealing which member they are, and each member signals once per scope. This follows the Semaphore protocol.

How it works

Part What it is
Identity A secret number. Its public commitment is Poseidon(secret). In the app the secret is derived from a wallet signature over a fixed message, so it is never stored and can be recreated.
Group A Merkle tree (LeanIMT) of commitments. The admin publishes the new commitments (MembersAdded event) and the tree's root; the contract stores only the root.
Proof A zero-knowledge proof (Noir circuit, UltraHonk) that "I know the secret of one commitment under this root", bound to a scope and a message. It reveals neither the secret nor the member.
Nullifier Poseidon(scope, secret): the same every time a member signals in a scope, different across scopes and members. The contract accepts each nullifier once.
Contract ARLAnonymousSignal: groups (admin and root), a one-hour grace period for a replaced root, one signal per nullifier, and the generated verifier.

Code

Path Contents
zk/semaphore/ Noir circuit (max depth 16, 65,536 members); 5 circuit tests
zk/check.sh Runs the circuit tests and rebuilds the circuit and verifier; fails if the committed artifacts differ
packages/zk/ @arl/zk: identities, groups, circuit inputs, proving (noir_js + bb.js) and verification; circuit/ holds the compiled circuit
contracts/src/ARLAnonymousSignal.sol The signal contract (Apache-2.0)
contracts/zk/ARLSemaphoreVerifier.sol The UltraHonk verifier, generated by bb 5.2.0 (evm target: keccak transcript, zero knowledge), committed as generated
contracts/zk-script/DevZk.s.sol Deploys the verifier and the contract on local Anvil only (the app's fixture)
contracts/test-zk/ Contract tests with real proofs (FOUNDRY_PROFILE=zk forge test); fixture from npm run fixture -w @arl/zk

The verifier lives in a separate Foundry profile because Slither cannot parse the generated file; Slither still analyses ARLAnonymousSignal.

Choices

  • Noir and Barretenberg, not circom and snarkjs. snarkjs and circomlib are GPL-3.0; the Noir compiler, noir_js, bb.js and the libraries used are MIT or Apache-2.0.
  • Poseidon(secret) identities (as in Semaphore v3) instead of Semaphore v4's Baby Jubjub keys. The Noir Baby Jubjub library used by Semaphore-Noir has no license, so it is not used. The circuit is also smaller (about 19,000 gates), which keeps proving in a browser practical.
  • Root published by the admin instead of an on-chain tree: no on-chain Poseidon, and group membership rules (for example verified whitelist members) stay with the group's admin.

In the app

The app's Private screen runs an anonymous poll with this contract; see app.md.

Costs and limits

  • Proving takes a few seconds (about 4 s for prove and verify in the tests).
  • On-chain verification costs about 3 million gas per signal.
  • The admin decides who is in the group and can publish a new root; the admin cannot signal for members, learn who signalled or reuse a nullifier. Renouncing the admin role freezes the group.
  • Anyone who sees a signal transaction can resubmit the same proof; it records the same signal and is then refused as a used nullifier.

Tests

Suite Command Covers
Circuit nargo test in zk/semaphore Single-member group, sibling order, nullifier per scope, zero secret refused, over-long proof refused
TypeScript npm test -w @arl/zk Commitments, LeanIMT, field hashing, non-members refused, a real proof verified, a changed message refused
Contract FOUNDRY_PROFILE=zk forge test Valid signals accepted once per scope; changed message, scope, nullifier, proof byte or root refused; root grace period; admin

Partnership