Compliance model

Privé is built on the Privacy Pools idea: privacy that a user can, if they choose, back with a proof that their funds come from a screened set. Privacy is live. So is the first stage of the compliance layer: a recomputable approved-set root and verifiable receipts. What is not live is the second stage, where the withdraw circuit itself enforces membership. This page states exactly where that line is.

Status, stated plainly. Privacy is Live on Robinhood Chain testnet (chain id 46630). Screening and the approved-set root are Live as a recomputable attestation (stage A). In-circuit enforcement is Planned (stage B). This distinction is the important one: you can now export a compliance receipt that anyone can verify, but the withdraw circuit still does not check association-set membership, so a withdrawal on its own is not proof of screening. What proves it is the receipt, attached deliberately.

Why a private rail is not a mixer

A mixer breaks the link between a deposit and a withdrawal and stops there. Everyone inside the anonymity set is equal, including whoever deposited stolen funds an hour ago. That is why honest users get tainted by association and why mixers end up sanctioned.

Privacy Pools, proposed by Buterin, Illum, Nadler, Schär and Soleimani, adds one idea: alongside the pool’s state tree of all deposits, publish an association set, a subset of deposits that satisfy a stated policy. A withdrawal then proves two things at once: that it owns a note in the pool, and that this note’s deposit belongs to the association set. The user is not identified. They only demonstrate that their funds are in the clean subset. That is the “proof of innocence”: a membership proof, not a disclosure.

Stage A of this design is now running on testnet. Stage B, the part that makes the withdrawal itself carry the membership proof, is not. The rest of this page is precise about which is which.

What the deployed circuit actually enforces

The circuit at packages/circuits/circuits/withdraw.circom says so in its own header comment:

// Partial withdrawal proof (single state-tree membership; NO ASP ...).
// Adapted from 0xbow/privacy-pools-core withdraw.circom (Apache-2.0),
// ASP membership removed.

Concretely, a Groth16 proof accepted by the deployed WithdrawVerifier constrains: the existing commitment is recomputed from its value, label, nullifier and secret; that commitment is a member of the LeanIMT state tree, which fixes stateRoot; the withdrawn amount and the remaining amount are range-checked to 128 bits; and the change note uses a fresh nullifier. The six public signals are [newCommitmentHash, existingNullifierHash, withdrawnValue, stateRoot, stateTreeDepth, context]. There is no association-set root among them, and none is checked.

On chain, PrivacyPool.withdraw rejects a zero withdrawal, a context mismatch, a zero recipient, a fee at or above the withdrawn value, an unknown state root, and an invalid proof. It then spends the nullifier and inserts the change note. Those are the only checks. See Contracts for the full surface.

The association set

The pool already emits everything an association set needs. Every deposit emits Deposited(depositor, commitment, label, value, precommitment), where label is derived from the pool scope and a monotonic nonce, and the depositor address is public. Screening operates on those public deposit records, never on the private notes.

Stage A: recomputable off-chain attestation Live

An Association Set Provider reads the pool’s public Deposited events, screens each depositor address against a published policy, and builds a Merkle tree over the approved labels. The root of that tree is the approved-set root. A policy hash pins which policy version a given root was produced under. This runs today: GET /api/asp serves the current root and members, and the app shows each of your notes as clean, pending, or not in the set. Because it is derived only from public data, you do not have to use our endpoint at all: you can rebuild the identical root yourself.

The property that matters: there is no signing key. Chain events are public and the policy is public, so the construction is deterministic and anyone can rebuild the same root and get the same answer. An ASP that lies is caught by anyone who reruns it. A withdrawal can then be accompanied by a compliance receipt containing the label, the root, the policy hash and a Merkle inclusion proof, which a counterparty verifies without learning the note secret, the nullifier, or the user’s other notes.

Stage A is an attestation, not enforcement. It tells you whether a note is in the clean set. It does not stop a withdrawal that is not. Being honest about that distinction is the entire point of this page.

Stage B: in-circuit enforcement

Enforcement means the withdraw circuit itself carries a second membership proof, against the association-set root, and refuses to produce a valid proof otherwise, with the pool checking that root on chain. That is what the upstream 0xbow circuit does and what we removed for the testnet build.

Adding it back is not a configuration flag. It requires:

  • a new circuit with an ASP membership constraint and new public signals,
  • a newly compiled and redeployed Solidity verifier,
  • a fresh Groth16 trusted setup ceremony for the new circuit,
  • a pool upgrade that stores and validates the approved-set root,
  • governance of who publishes that root and how a bad root is contested.

Because every one of those touches the security core, Stage B is deliberately bundled with the mainnet audit rather than rushed onto testnet.

CapabilityStatusWhere it lives
Shielded deposit, note held in browserLivePrivacyPool.deposit
Groth16 withdrawal proof, verified on chainLiveWithdrawVerifier
Double-spend prevention via nullifiersLivePrivacyPool.withdraw
Gasless relayer, fee bound into the proof contextLive/api/relay
Deposit screening against a public policyLiveASP, stage A · /api/asp
Approved-set root and compliance receiptsLiveASP, stage A · recomputable
Proof of innocence enforced in the circuitPlannedStage B · new circuit, verifier, ceremony

What this means for you today

On Robinhood Chain testnet, Privé is private, and screenable by attestation, but not yet enforced in-protocol. The unlinkability between sender and receiver is real and cryptographic. The approved-set root is real and recomputable. What is not yet real is the circuit refusing an unscreened withdrawal.

  • You can now produce a compliance receipt (stage A) for a note whose deposit is in the approved set, and a counterparty can verify it without learning your secrets. But a withdrawal on its own is not that proof, because the circuit does not enforce membership yet. Attach the receipt deliberately when you need it.
  • Everything on testnet is worthless. The USDG here is a mock token with an open public mint behind the faucet, so its provenance carries no meaning at all and no screening result over it would either.
  • The contracts are unaudited and there is no mainnet deployment. See Security and risk.
  • Note secrets stay in your browser. If you lose them, the funds are unrecoverable, so use the encrypted backup.

The narrow, honest claim: Privé keeps payments private, publishes a recomputable approved-set root, and can issue a verifiable receipt, while in-circuit enforcement is still ahead of us. For the mechanics of what is live, see How it works.

Further reading: Blockchain Privacy and Regulatory Compliance: Towards a Practical Equilibrium, the paper this model is based on.