The ZK embodiment is the high-assurance optional tier. The preferred non-ZK
tier uses a per-poll voter Ed25519 device keypair with Didit device
attestation, which provides oracle prevention without ZK overhead and supports
biometric recovery on any device. Use the ZK embodiment when even a read-only
linkage store operator must be unable to correlate identity to participation.
What the circuit proves
TheNullifierCircuit is a Groth16 circuit over BN254 that proves two statements simultaneously.
person_secret is the private witness — it never leaves the voter’s device.
commitment and didit_commitment_sig are private circuit witness inputs.
They must never appear in the transaction payload or on-chain in any form. Placing them
in the transaction allows the IDV provider to correlate nullifier → commitment → identity,
defeating the ZK anonymity guarantee. Only nullifier, the Groth16 proof bytes,
voter_pub_key, voter_sig, ballot_nonce, and choices travel on-chain.
Voter enrollment
Generating a proof
tx.BallotCastData.
commitment and didit_commitment_sig stay in the circuit — they are not included in the
transaction struct.
Verifying a proof (canonical runtime)
verifier.Verify returns nil if the proof is valid, an error otherwise. Called by
state.validateBallotCast before any ballot state mutation. The canonical runtime never sees
commitment or didit_commitment_sig.
Trusted setup
Circuit compilation
Recovery trade-off
The ZK embodiment is device-bound:person_secret must be present to generate a proof.
If the voter loses their device, re-enrollment requires an out-of-band backup mechanism
(encrypted keychain export, hardware key backup, etc.).
The preferred device-keypair embodiment does not have this limitation — recovery is anchored
to biometric re-authentication with the IDV provider on any device, with no secret to
remember or backup.