Skip to main content

ZK Circuits

There are two major components to the ZK Circuits:

  1. Binary Merkle Tree
  2. 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 identifier
  • merkleTreeRoots[]: Valid Merkle roots for input notes
  • nullifiers[]: Nullifiers for spent notes
  • outputNoteCommitments[]: Commitments for newly created notes
  • publicSpend: 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