ZK Circuits
The four core circuits
Design in Progress
These circuit specifications are under active development and may change significantly. We are exploring several approaches to achieve compliant note merging/splitting while ensuring the bona fide rule: a note that is compliant now was always compliant in its entire history. The implementation at preview.upd.io represents one sample approach - not the final design.
Overview
Four circuits handle all UPP operations:
| Circuit | Purpose | In → Out |
|---|---|---|
| Shield | Deposit tokens | Public → 1 note |
| Transfer | Private send | 1 note → 2 notes |
| Merge | Consolidate | 2 notes → 1 note |
| Withdraw | Exit pool | 1 note → Public |
Common Constraints
All circuits enforce:
Token consistency: commitment = Poseidon(amount, blinding, origin, token)
Amount conservation: Inputs = Outputs
Merkle membership: Spent notes must exist in tree
Nullifier uniqueness: Each note produces unique nullifier
Shield Circuit
Deposit public tokens into the pool.
Public inputs: commitment, token, amount
Private inputs: blinding, origin
Constraints:
- commitment = Poseidon(amount, blinding, origin, token)
- origin = msg.senderNo proof of prior note ownership - this is a fresh deposit.
Transfer Circuit
Send to another user. 1 input note, 2 output notes (recipient + change).
Public inputs: nullifier, newCommitments[2], merkleRoot, aspRoot
Private inputs: note, merkleProof, recipientPubKey, amounts[2]
Constraints:
- Note exists in tree (Merkle proof)
- Nullifier correctly computed
- Input amount = sum of output amounts
- If origin NOT in ASP: recipient must be origin (restricted transfer)Merge Circuit
Combine two notes into one. Origin becomes the merger.
Public inputs: nullifiers[2], newCommitment, merkleRoot
Private inputs: notes[2], merkleProofs[2], newBlinding
Constraints:
- Both notes exist in tree
- Nullifiers correctly computed
- Output amount = sum of input amounts
- New origin = msg.sender (merger takes responsibility)Merge is the only way to change origin. The merger vouches for the funds.
Withdrawal Circuit
Exit the pool to a public address.
Public inputs: nullifier, recipient, amount, token, merkleRoot, aspRoot
Private inputs: note, merkleProof, aspProof
Constraints:
- Note exists in tree
- Nullifier correctly computed
- Amount matches note
ASP check (one of):
- recipient == origin (ragequit - always allowed)
- origin in ASP allowlist (normal withdrawal)ASP Integration
Transfer and Withdrawal check ASP compliance:
Performance
| Circuit | Constraints | Proving Time |
|---|---|---|
| Shield | ~10k | ~5s |
| Transfer | ~100k | ~20s |
| Merge | ~120k | ~25s |
| Withdraw | ~100k | ~20s |
Proof generation is client-side using snarkjs/Groth16.