Skip to Content
How it worksThe pool model

The pool model

zx402 keeps every deposit in one place: a single pool output on chain, guarded by one validator. Money enters by deposit and leaves by proof. This page explains the parts.

One UTXO holds the pool

Cardano has no accounts. Every coin sits in an unspent output, a UTXO. The pool is one UTXO at the pool script address. It carries all the deposited ADA, a token that marks it as the real pool, and a datum with the state of the pool.

Every pool action spends this UTXO and creates it again with a new datum. So the pool changes about once per block, and actions queue behind each other. Deposits are different. They sit in their own small UTXOs until the pool absorbs them, so many agents can deposit in the same block.

The pool datum holds five fields.

FieldWhat it is
rootsThe last 16 roots of the state tree. A proof may use any of them, so a payment is not stale when the tree grows while the agent proves
sizeHow many leaves the state tree holds
queueUp to 8 commitments waiting for the next Insert. Change notes from payments land here
nullifier_rootThe root of a trie that holds every spent nullifier hash
fees_accruedProtocol fees that an admin can collect

Notes

A deposit becomes a note. A note is four numbers: value, label, nullifier and secret. The owner keeps the nullifier and the secret. The chain sees only hashes.

precommitment = H2(nullifier, secret) commitment = H3(value, label, precommitment) nullifierHash = H1(nullifier)

H1, H2 and H3 are Poseidon hashes over the BLS12-381 scalar field. The commitment is the leaf in the state tree. The nullifier hash is published when the note is spent, so a note can be spent once. The label names the deposit the note came from. A change note keeps the label of its parent, so one deposit can pay many times and still has one label.

The chain derives the label itself when it absorbs a deposit. The label is a hash of the pool ID, the deposit transaction, the output index and the depositor’s refund key. That last part matters: the depositor can always prove ownership of a label by signing with the refund key, which gives the public exit.

The state tree

The commitments live in a Merkle tree of depth 32, hashed with Poseidon. The chain never computes Poseidon. It is too expensive in Plutus. Instead a service called the crank computes the new tree off chain and proves the update with the insert circuit. The validator checks the proof, which is cheap, and accepts the new root.

The tree only grows. A spent note stays in the tree. What stops a second spend is the nullifier hash.

The nullifier trie

Spent nullifier hashes live in a Merkle Patricia trie. The pool datum holds only its root. Every Settle and every Ragequit comes with a proof that the nullifier hash is absent from the trie, and the validator inserts it and computes the new root. The same proof of absence serves as the proof of insertion, which keeps the on-chain work small.

Approval

A deposit is not spendable in private until an approval service has added it to the approved set. That service, called the ASP, keeps its own Merkle tree of approved labels and publishes its root in a separate output. A private payment proves membership in that tree too, so only approved deposits pay in private. This is the Privacy Pools idea: a pool whose members all passed the same check, so honest money is not mixed with money nobody wants.

The approved set never blocks an exit. Ragequit proves membership in the state tree only, and the refund key signs. A depositor whom the ASP refuses can always take the money back in public.

Four actions

ActionWho sends itWhat it does
InsertThe crankAbsorbs waiting deposits and queued change notes into the state tree, with an insert proof
SettleThe relayer, for an agentSpends one note in private. Pays the payouts, keeps the change as a new note, with a spend proof
RagequitThe depositorSpends one note in public to the refund key, with a ragequit proof and a signature
CollectFeesAn adminMoves accrued protocol fees to the treasury

The contracts page explains the checks of each action.

Other outputs around the pool

OutputPurpose
Deposit outputsOne per deposit, at the deposit script address, with the precommitment and the refund key in the datum. Insert absorbs them. The depositor can refund one that was never absorbed
Config outputAdmin keys, limits and fee rates. Read by every action as a reference input
ASP outputThe root of the approved set and the operators who may update it
Pool NFTOne token on each of the three state outputs, so a validator can tell the real one from a copy

What makes this private

The spend proof reveals the nullifier hash, the new commitment of the change, the withdrawn amount and the roots. It does not reveal which leaf was spent or which label it carried. Everyone who deposited into the pool and was approved is a possible payer. The more deposits the pool holds, the less a payment says. The privacy page lists what every party can still learn.