Zero Knowledge Circuits

The ZK logic is using a LeanIMT merkle tree, as its one of the few audited merkle tree's recommended by ZK-kit.

All circuits are written in Circom, if you are new to circom, we recommend you go throw this tutorials:
https://learn.0xparc.org/materials/circom/learning-group-1/intro-zkp/

Source code:

https://codeberg.org/KusamaShield/Solidity_helpers/src/branch/main/contracts/new_circuits

Circuits explaination

A ZK-SNARK circuit system built with Circom 2.1 and Groth16 on BN254. It enables private withdrawals from an on-chain commitment pool with UTXO-style commitment refresh for transaction unlinkability.

Overview

A user deposits funds by publishing a commitment (a Poseidon hash encoding value, asset type, nullifier, and secret) into an on-chain Merkle tree. To withdraw, the user generates a zero-knowledge proof that they know the preimage of some commitment in the tree — without revealing which one. The spent commitment is invalidated via a nullifier hash, and a new "change" commitment is inserted back into the tree.

Circuit Architecture

withdraw.circom          ← top-level circuit (orchestrator)
├── commitment.circom    ← three-layer Poseidon commitment scheme
│   └── poseidon_bn254.circom  ← Poseidon hash wrapper (BN254)
└── merkle_tree.circom   ← LeanIMT variable-depth inclusion proof
    └── poseidon_bn254.circom

poseidon_bn254.circom — Hash Primitive

A parameterized wrapper around circomlib's Poseidon hash. Poseidon is a ZK-friendly hash function — far cheaper inside an arithmetic circuit than SHA-256 or Keccak.

PoseidonBN254(n): signal input in[n] → signal output out

commitment.circom — Three-Layer Commitment

Constructs a commitment in three layers, each serving a distinct purpose:

LayerComputationPurpose
1nullifierHash = Poseidon(nullifier)Published on-chain to prevent double-spending
2precommitment = Poseidon(nullifier, secret)Binds the nullifier to a secret only the owner knows
3commitment = Poseidon(value, asset, precommitment)The Merkle tree leaf, encoding value and asset type

Separating the nullifier hash from the secret allows the nullifier to be revealed (marking a commitment as spent) without leaking the secret or the commitment's position in the tree.

merkle_tree.circom — LeanIMT Inclusion Proof

Implements a Lean Incremental Merkle Tree inclusion proof with variable depth (up to maxDepth = 254).

At each tree level:

  • The leafIndex bits determine left/right child ordering.
  • If a sibling is zero (empty subtree), the node propagates unchanged — this is the "lean" optimization that avoids hashing against placeholder nodes.
  • If a sibling is non-zero, the ordered pair is hashed with Poseidon.

The computed root is compared against the public root input. This approach is more constraint-efficient than a fixed-depth tree because most levels in a sparse tree have zero siblings.

withdraw.circom — Main Circuit

The top-level Withdraw(254) template orchestrates the full proof in eight steps:

StepOperationDescription
1Compute existing commitmentHashes (existingValue, asset, existingNullifier, existingSecret) through the three-layer scheme
2Output nullifier hashPublished on-chain so the contract can reject double-spends
3Merkle inclusion proofVerifies the existing commitment is in the tree against the public root
4128-bit range checksConstrains both withdrawnValue and remainingValue to 128 bits (prevents underflow/overflow)
5Nullifier uniquenessAsserts existingNullifier != newNullifier to prevent nullifier reuse
6Compute new commitmentHashes (remainingValue, asset, newNullifier, newSecret) — the "change" UTXO
7Output new commitmentInserted into the on-chain tree by the contract
8Replay protectionConstrains context into the proof (e.g., chain ID or tx hash) to prevent cross-chain/cross-tx replay

Signals

Public inputs:

  • withdrawnValue — amount being withdrawn
  • root — current Merkle tree root
  • treeDepth — actual depth of the tree
  • context — replay protection binding (chain ID, tx hash, etc.)

Public outputs:

  • existingNullifierHash — marks the old commitment as spent
  • newCommitmentHash — the change commitment to insert into the tree

Private inputs:

  • asset — asset identifier
  • existingValue, existingNullifier, existingSecret — preimage of the spent commitment
  • newNullifier, newSecret — fresh randomness for the change commitment
  • siblings[254], leafIndex — Merkle proof path

Privacy Properties

  • Sender privacy — the Merkle path is private, so the proof doesn't reveal which commitment is being spent.
  • Transaction unlinkability — each withdrawal creates a fresh commitment with a new nullifier and secret, preventing linkage of sequential transactions by the same user.
  • Double-spend prevention — the nullifier hash is published on-chain; the contract rejects any previously-seen nullifier hash.
  • Value integrity — 128-bit range checks ensure no one can create value from nothing or withdraw more than they deposited.
  • Replay protection — the context binding prevents proof reuse across different chains or transactions.