ZK proofs & circuits
Every private action in Cowl is a zero-knowledge proof: math that convinces the chain a state change is valid without revealing what changed. This is what makes a shielded balance both private and verifiable.
What a proof asserts
- You own the notes you are spending.
- Inputs equal outputs — no value is created or destroyed.
- Each spent note is nullified exactly once, so nothing double-spends.
- The trade or transfer follows the pool’s rules.
None of the amounts, assets, or addresses appear on-chain — only the proof and the resulting commitments and nullifiers.
Commitments & nullifiers
| Primitive | What the chain sees |
|---|---|
| Commitment | A hash that hides a note’s amount, asset, and owner |
| Nullifier | A one-time marker that a note was spent — unlinkable to the note |
| Merkle root | A snapshot proving your note is in the set, without saying which one |
The circuits
Two circuits carry the whole protocol, written in Noir and compiled to Solidity verifiers that live on chain.
| Circuit | What it proves |
|---|---|
shield | A deposit of a stated public amount becomes exactly one valid note |
spend | A join-split: two input notes are yours and unspent, two output notes are well formed, and value is conserved across them — covering private send, unshield, merge and trade with one circuit |
Notes are hashed with Poseidon2 over BN254 and proven with UltraHonk. The commitment tree is fixed-depth, and every leaf insertion is proven inside the circuit rather than recomputed by the contract — the pool holds a root and never hashes a thing. Verification on chain is a constant-cost check whatever the proof is about.
Proving is always yours
Every proof is built on your own machine: in a web worker in the app, in-process in the CLI. There is no remote prover and no proving service, because handing a witness to somebody else is handing them everything the proof exists to hide. A transfer proof takes around 2.3 seconds and a shield proof under one.
A relayer only submits the finished proof and pays gas. It never sees a witness.
Proven load-bearing
The failure mode that empties a pool is not a circuit that rejects a good proof — it is one that accepts a bad one. So the circuits are tested against adversarial witnesses, not just valid ones, and every constraint is checked for whether it actually holds anything up: 17 deleted one at a time, 17 caught. Public inputs are pinned against what the contract reads, on all 14 of spend and 6 of shield.