ZK Circuits
There are two major components to the ZK Circuits:
- Binary Merkle Tree
- JoinSplit
Binary Merkle Tree
A circuit to calculate the root of a binary Merkle tree using a provided proof-of-membership.
Ref: https://github.com/zk-kit/zk-kit.circom/tree/main/packages/binary-merkle-root
JoinSplit
The JoinSplit circuit allows a user to privately spend existing notes and create new notes without revealing the notes themselves.
At a high level, the circuit proves that:
- The spender owns the input notes
- The input notes exist in the Merkle tree
- Each input note is spent exactly once
- Output notes are well-formed
- No value is created out of thin air
Constraints
1. Ownership of Input Notes
For each input note, the circuit verifies that the prover controls it.
This is done by:
- Recomputing the note commitment
- Verifying ownership of the stealth address using the prover’s viewing key
- Ensuring the viewing key is correctly derived from the spending key
2. Membership in the Merkle Tree
Each input note must exist in the onchain note commitment tree.
The circuit:
- Recomputes the note commitment
- Verifies a Merkle proof against a public Merkle root
- Ensures the provided root matches one of the accepted roots
This proves the note exists without revealing which leaf it is.
3. Nullifier Correctness (Double-Spend Prevention)
For every input note, the circuit checks that:
- The provided nullifier is correctly derived from the note and viewing key
Nullifiers are exposed publicly, ensuring:
- Each note can only be spent once
- The note itself remains private
4. Value Constraints and Balance
The circuit enforces conservation of value:
- All input and output values are range-checked
- The sum of output values must be less than or equal to the sum of input values
- Any excess becomes
publicSpend
sum(outputs) ≤ sum(inputs)
publicSpend = sum(inputs) − sum(outputs)
5. Output Note Creation
For each output note, the circuit:
- Generates a deterministic nonce
- Assigns ownership to the recipient’s canonical address
- Computes a new note commitment
Output note commitments are public outputs of the circuit, while:
- The recipient
- The value
- The sender
remain private.
6. Assets
All input and output notes are constrained to use the same:
- Encoded asset address
- Encoded asset ID
This prevents mixing different assets within a single JoinSplit.
7. Signature Verification
Finally, the circuit verifies a signature made with the spending key over the public operation input.
This ensures the JoinSplit was explicitly authorized by the key holder.
Public Inputs
operation: Signed operation identifiermerkleTreeRoots[]: Valid Merkle roots for input notesnullifiers[]: Nullifiers for spent notesoutputNoteCommitments[]: Commitments for newly created notespublicSpend: Net value exiting the shielded pool
Private Inputs
- Viewing key and spending key
- Input note data (values, nonces, owners)
- Merkle proofs for input notes
- Output values and recipient canonical addresses
- Signature authorizing the operation