Skip to main content

ZK Circuits

Shroud Network uses two Circom 2.x circuits compiled to Groth16 proofs.

Transfer Circuit — PrivateTransfer

The transfer circuit proves that a user owns a note, that the note exists in the Merkle tree, and that the output notes have the same total value — all without revealing any private data.

Public inputs (4 signals, visible on-chain)

SignalDescription
merkle_rootMerkle tree root being proven against
nullifier_hashNullifier of consumed input note
new_commitment_1Output note commitment for recipient
new_commitment_2Output note commitment for change

Private inputs (prover only)

  • Input note: amount_in, blinding_in, secret, nullifier_preimage, owner_private_key, leaf_index
  • Merkle path: merkle_path[20], path_indices[20]
  • Output note 1: amount_out_1, blinding_out_1, secret_out_1, nullifier_preimage_out_1, owner_pk_out_1
  • Output note 2: amount_out_2, blinding_out_2, secret_out_2, nullifier_preimage_out_2, owner_pk_out_2

Constraint groups

GroupConstraintsWhat it verifies
Ownership~700Derive public key from private key (EscalarMulFix)
Input Pedersen~1,000-1,400C_in = amount * G + blinding * H
Note commitment~250commitment = Poseidon(C.x, C.y, secret, nullifier_preimage, pk.x)
Merkle proof~5,00020-level Poseidon hash path from leaf to root
Nullifier~250nullifier = Poseidon(nullifier_preimage, secret, leaf_index)
Amount conservation1amount_in === amount_out_1 + amount_out_2
Blinding conservation1blinding_in === blinding_out_1 + blinding_out_2
Range proofs~384Both outputs fit in 64 bits (Num2Bits)
Output Pedersen~1,000-1,400Both output Pedersen commitments are correct
Output commitments~500Both output note commitments are correct

Total: ~25,133 non-linear constraints. Proof generation takes under 1 second.

Balance check (in-circuit)

The Pedersen balance check (C_in == C_out_1 + C_out_2) is verified inside the circuit using BabyAdd — a Baby Jubjub point addition at ~6 constraints. Pedersen coordinates never appear as public inputs or on-chain calldata.

Withdraw Circuit — PrivateWithdraw

Similar structure but with key differences:

Public inputs (4 signals)

SignalDescription
merkle_rootMerkle tree root
nullifier_hashNullifier of consumed note
amountWithdrawal amount (public — needed to release ERC20)
change_commitmentChange note commitment (0 if full withdrawal)

Total: ~20,858 non-linear constraints.

Circuit dependencies (circomlib)

  • poseidon.circom — Poseidon hash
  • babyjub.circom — Baby Jubjub point operations
  • escalarmulfix.circom — Fixed-base scalar multiplication
  • bitify.circom — Num2Bits for range proofs
  • mux1.circom — Multiplexer for Merkle path selection

Compilation

# Compile circuits
circom circuits/transfer.circom --r1cs --wasm --sym \
--output circuits/build/transfer -l circuits/node_modules

circom circuits/withdraw.circom --r1cs --wasm --sym \
--output circuits/build/withdraw -l circuits/node_modules