Nullifiers
How nullifiers prevent double-spending while preserving privacy
Nullifiers
Section titled “Nullifiers”Nullifiers are the key mechanism that prevents double-spending without revealing which deposit was spent.
The Problem
Section titled “The Problem”In a private system, we can’t simply mark a deposit as “spent” - that would reveal which deposit corresponds to which withdrawal, breaking privacy.
The Solution: Nullifier Hashes
Section titled “The Solution: Nullifier Hashes”When you withdraw, you reveal the hash of your nullifier (not the nullifier itself):
nullifierHash = poseidon(nullifier)This hash is stored on-chain. If someone tries to withdraw the same deposit again, the same nullifierHash would be submitted, and the transaction would fail.
Why This Works
Section titled “Why This Works”- Different nullifiers → Different hashes: Each deposit has a unique nullifier, so each has a unique nullifierHash
- Can’t reverse the hash: Seeing nullifierHash doesn’t reveal the nullifier or which commitment it corresponds to
- Proven in ZK: The withdrawal proof proves you know a valid nullifier without revealing it
The Zero-Knowledge Proof
Section titled “The Zero-Knowledge Proof”When withdrawing, your proof proves:
- “I know a
nullifierandsecretsuch thatcommitment = poseidon(nullifier, secret)” - “This
commitmentexists in the Merkle tree with rootR” - “The
nullifierHashI’m revealing equalsposeidon(nullifier)”
The verifier learns only:
- A valid withdrawal is happening
- The nullifierHash (to prevent reuse)
- The Merkle root being used
The verifier does NOT learn:
- Which commitment is being spent
- The nullifier or secret values
- Any link to the original depositor
Nullifier Storage
Section titled “Nullifier Storage”The zklsol program stores nullifier hashes in a NullifierHash PDA:
seeds = ["NullifierHash", mint, depth, nullifier_hash_bytes]Before executing a withdrawal, the program checks if this PDA exists:
- If it exists → Reject (already spent)
- If it doesn’t exist → Create it and proceed
Security Considerations
Section titled “Security Considerations”| Risk | Mitigation |
|---|---|
| Nullifier leak | Keep your nullifier secret. If leaked, someone could front-run your withdrawal. |
| Weak randomness | Use cryptographically secure random number generation for nullifiers. |
| Nullifier reuse | Never reuse a nullifier across deposits - each deposit needs fresh randomness. |
Code Example
Section titled “Code Example”// Generating nullifier and computing its hashimport { poseidon } from '[PLACEHOLDER: POSEIDON_NPM_PACKAGE]';import { randomBytes } from 'crypto';
// Generate random nullifier (32 bytes)const nullifier = randomBytes(32);
// Compute nullifier hash for withdrawalconst nullifierHash = poseidon([nullifier]);
// This nullifierHash is what gets submitted with your withdrawal proofNext Steps
Section titled “Next Steps”- Learn about Relayers and why they’re essential for privacy
- See the Withdraw Guide for implementation details