The zero-knowledge circuits for Shielded Protocol. Written in Circom 2, compiled to Groth16 proofs on the BN254 elliptic curve.
A zero-knowledge circuit is a program that can prove a statement is true without revealing the inputs used to prove it.
In Shielded Protocol, the key statement is:
"I know a secret that corresponds to a commitment already in the Merkle tree, and I have not spent it before."
Proving this on-chain lets the smart contract release funds to the recipient — without ever learning who deposited them or which commitment is being spent.
| Property | Value |
|---|---|
| Proving system | Groth16 |
| Elliptic curve | BN254 (alt_bn128) |
| Hash function | Poseidon (ZK-friendly, ~200× cheaper than SHA-256 in constraints) |
| Merkle tree depth | 20 (supports 2²⁰ = 1,048,576 notes) |
| Proof size | 3 elliptic curve points (~256 bytes) — constant regardless of tree size |
| On-chain verification | O(1) via Stellar BN254 host functions |
Used when a user wants to withdraw funds. Proves all three things simultaneously:
- Membership: The note's commitment exists in the Merkle tree (via a Merkle inclusion proof)
- Ownership: The prover knows the secret that created the commitment
- Non-reuse: The nullifier has not been used before (checked on-chain against a nullifier set)
Private inputs (never revealed):
| Input | Description |
|---|---|
secret |
31-byte random scalar generated at deposit time |
amount |
Token amount in the note |
tokenId |
Identifier for the token type |
pathElements[20] |
Sibling hashes along the Merkle path |
pathIndices[20] |
Left/right flags for each Merkle level |
Public inputs (visible on-chain):
| Input | Description |
|---|---|
root |
Merkle tree root at proof time |
nullifierHash |
Deterministic hash of the secret — marks the note as spent |
recipient |
Address that receives the withdrawn funds |
relayer |
Optional relayer address (for gas-less withdrawals) |
fee |
Optional relayer fee |
Used at deposit time to verify the commitment is correctly formed.
| Inputs | Description |
|---|---|
secret (private) |
The randomly generated note secret |
amount (private) |
Amount being deposited |
tokenId (private) |
Token identifier |
Outputs commitment = Poseidon(secret, amount, tokenId), which is inserted into the on-chain Merkle tree.
A standalone Merkle inclusion circuit reused by withdraw.circom. Proves that a leaf (commitment) is included in a tree with a given root, given the path of sibling hashes.
Depth: 20 levels → 20 Poseidon2 hashes → ~24,000 constraints.
Derives the nullifier from the secret: nullifier = Poseidon(secret, 1).
This is deterministic (same secret always gives the same nullifier) but not reversible — you cannot recover the secret from the nullifier. This lets the on-chain contract check for double-spending without being able to link the nullifier back to a commitment.
| Circuit | Constraints | Bottleneck |
|---|---|---|
withdraw (depth 20) |
~26,000 | 20 × Poseidon2 for Merkle path |
merkle_proof (depth 20) |
~24,000 | Same — Merkle path hashing |
deposit |
~200 | Single Poseidon3 |
nullifier_hash |
~100 | Single Poseidon2 |
Lower constraints = faster proof generation. See the constraint reduction issue for planned optimizations.
User's browser (private) On-chain (public)
──────────────────────────────────────────────────────────────
secret, amount, tokenId
│
▼
commitment = Poseidon(secret, amount, tokenId)
│
▼
Merkle path (fetched from chain)
│
▼
[ witness generation ]
│
▼
[ Groth16 proof ] ─────────────────────────► proof (256 bytes)
nullifierHash
root
recipient
│
▼
groth16-verifier contract
│
✓ proof valid
✓ root matches current tree
✓ nullifier not yet spent
│
▼
→ release funds to recipient
Prerequisites: Node.js 20+, circom 2.x, snarkjs
# Install global tools
npm install -g circom snarkjs
# Clone and install
git clone https://github.com/Shielded-Protocol/shielded-circuits
cd shielded-circuits
npm install
# Compile all circuits to R1CS + WASM
npm run compile
# Run tests (generates proofs and verifies them)
npm testcircuits/
├── withdraw.circom ← Main withdrawal proof (~26K constraints)
├── deposit.circom ← Commitment generation (~200 constraints)
├── merkle_proof.circom ← Merkle inclusion helper (~24K constraints)
├── nullifier_hash.circom ← Nullifier derivation (~100 constraints)
├── poseidon.circom ← Poseidon hash primitive
└── lib/ ← Shared circuit utilities
Groth16 requires a one-time trusted setup (also called a "powers of tau" ceremony). The setup produces two keys:
- Proving key — used by the prover (browser) to generate proofs
- Verifying key — used by the on-chain verifier to check proofs
Shielded Protocol uses the Hermez Powers of Tau ceremony for the universal phase 1, and performs circuit-specific phase 2 setup.
If you are doing a fresh deployment, run
npm run setupto perform the phase 2 ceremony. For production, use a multi-party computation (MPC) ceremony.
- Keeping your
secretprivate is the only security requirement. The circuit itself does not need to be trusted. - The verifying key embedded in the on-chain contract must match the proving key used to generate proofs.
- Poseidon parameters used here:
t=3for commitments,t=2for nullifiers and Merkle hashing.
See CONTRIBUTING.md.
Browse Wave-ready issues →
Good first issues are labeled layer:circuits and difficulty:easy.
| Repo | Description |
|---|---|
| shielded-contracts | On-chain Groth16 verifier that consumes these proofs |
| shielded-sdk | TypeScript SDK that calls these circuits from the browser |
| shielded-app | Frontend UI |
MIT