KASPA

← Zero-Knowledge on L1·Design docPrototype · TN10 only

A shielded pool on Kaspa L1

Groth16-gated privacy for a covenant stablecoin — the design, and the live testnet-10 benchmarks behind it. Published from our research notes; the full source opens with the kUSD framework release.

Goal

A trustless privacy pool: deposits go into a commitment set; a withdrawal proves “I own an un-spent deposit” without revealing which one. Unlike a coinjoin — where compute mass caps k-anonymity at roughly 3, since each participant is a ~5.4 KB covenant reveal — a ZK pool's anonymity set is the entire commitment tree. The Merkle membership proof lives inside the circuit, off-chain, so on-chain cost is flat per withdrawal.

Why Groth16

Kaspa consensus ships two ZK-verify precompiles behind one opcode (OpZkPrecompile): Groth16 over BN254 (tag 0x20) and RISC Zero succinct receipts (tag 0x21). We use Groth16 for the pool: roughly half the compute mass (~294k vs ~474k measured), 128-byte proofs instead of ~222 KB, and it's the standard mixer construction. The tradeoff is a per-circuit trusted setup. Measured costs for both are on the ZK overview.

The live benchmark that de-risked it

A benchmark covenant bakes the Groth16 verifying key into its locking script and is spent by revealing proof + public inputs in the unlocking script — verified entirely on-chain, no signature.

  • First Groth16 verify accepted live on TN10: 018a73cc1325
  • Measured cost: 15,500,893 script units → ~294k compute mass (the 14M ZK verify plus the VK/proof/input pushes and P2SH overhead). A first attempt budgeting only the 14M verify cost was rejected with used=15,500,893 > limit=14,009,999 — the budget must cover the whole script, a real-world detail the docs don't tell you.
  • That leaves ~206k mass headroom under the 500k per-tx limit for the pool covenant's own logic — enough, as the live cycle below proves.

The VK is the committed circuit; proof + inputs are the per-withdrawal witness.

Architecture

Deposit — cheap, no proof*

The user publishes commitment = H(secret, nullifierSecret); the pool covenant appends it to a Merkle tree and advances the root in its state. Fixed denomination. (*The built prototype proves the root transition with a small deposit circuit — see the progress log.)

Withdraw — one Groth16 proof

The circuit proves “I know (secret, nullifierSecret) whose commitment is in the tree at root R, and nullifier = H(nullifierSecret)”, exposing {R, nullifier, recipient}. The covenant checks R is a known root, the nullifier is unspent, and pays the recipient.

Economics: one proof per transaction (two don't fit under the mass limit), ~0.3 KAS per withdrawal, anonymity set = the whole tree. Deposits are cheap and frequent; withdrawals are the metered operation.

Progress — every step proven live before the next

  1. 0. Groth16 0x20 proven live (fixture)

    The precompile verifies the arkworks BN254 test fixture on-chain at ~294k mass.

  2. 1. Our own circuit verifies

    A minimal circuit we author end-to-end — trusted setup, prove, serialize in the exact compressed BN254 format the precompile deserializes — verifies live. This cleared the serialization risk: an arbitrary circuit of ours verifies on-chain.

  3. 2. The real withdrawal statement verifies

    Commitment = Poseidon(nullifier, secret) is a leaf in a depth-20 Merkle tree at a public root; nullifierHash is revealed; the recipient is bound. Same ~294k mass as the trivial circuit — Groth16 verification is constant-size, so tree depth (the anonymity set) is free on-chain.

  4. 3. ZK becomes a language primitive

    A verifyGroth16(vk, proof, …inputs) builtin added to the Silverscript covenant compiler, lowering to the exact opcode sequence proven above — so the stateful pool covenant is written in the high-level language. (Compiler change is local, not upstreamed.)

  5. 4. The pool covenant runs, with double-spend prevention

    zkpool.sil: a singleton covenant holding pooled funds, with the deposit-tree root AND a nullifier-tree root in state. Deposits prove the root transition; withdrawals prove membership + nullifier non-membership + spent-marking in one 5-input proof. A replayed withdrawal is rejected — the covenant feeds the advanced nullifier root, the stale proof fails.

  6. 5. Full cycle live under consensus

    Genesis → deposit → withdraw on TN10, the withdrawal (5-input verify + full covenant reveal + logic) accepted within a 20M script-unit budget (≈380k mass), comfortably under the 500k limit.

  7. 6. A real anonymity set

    Three deposits grow one commitment tree; a withdrawal proves ownership of one of the three without revealing which. Deposit roots chain across all transactions. Anonymity grows with deposits at zero extra on-chain cost.

Caveats (read before excitement)

  • Poseidon parameters are generated per-run — prototype-grade. A deployment needs committed, audited constants.
  • Groth16 needs a real multi-party trusted-setup ceremony per circuit; ours is a dev setup.
  • The compiler builtin is local to our toolchain checkout, not upstream Silverscript.
  • Testnet-10 only. Unaudited. Not for real value.