PrivacyArchitecture

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:

CircuitPurposeIn → Out
ShieldDeposit tokensPublic → 1 note
TransferPrivate send1 note → 2 notes
MergeConsolidate2 notes → 1 note
WithdrawExit pool1 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.sender

No 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

CircuitConstraintsProving Time
Shield~10k~5s
Transfer~100k~20s
Merge~120k~25s
Withdraw~100k~20s

Proof generation is client-side using snarkjs/Groth16.

On this page