← All posts

June 8, 2026 · Permissionless Technologies

How a zero-knowledge proof hides everything but the truth

A Berlin chocolatier wants to buy equity in his Swiss supplier. His business depends on every wallet he owns being public. Zero-knowledge proofs are how he buys without telling the world. Part 2 of Privacy Pool 101.

privacy-pooleducationprivacy-pool-101UPPzero-knowledgezk-proofcredentialscompliance
// privacy-pool-101.md// part 02 of 09How a ZK proofhides everythingbut the truth.// statement.// witness.// proof.permissionless-technologies.comUPP

You are tokenising real equity on chain. Or you run KYC for a regulated registrar under MiFID 2. You have spent months solving compliance. Passports verified. Sanctions screened. Audit trail kept for seven years.

You think your KYC is covered.

It is not. The way traditional KYC works does not translate to a public ledger. Every signature, every wallet attestation, every "this user is verified" claim becomes a permanent public record. Anyone reading the chain can build the KYC list you never meant to publish.

The rest of this post follows one customer through one transaction, to show exactly what leaks, and how a zero-knowledge proof closes the leak without weakening the compliance you already have.

Meet Lukas.

Prefer to watch? A short walkthrough of Lukas's transaction. Captions in English, German, Spanish, and Chinese.

1. The story

Lukas Brehm runs a small chocolatier in Berlin Kreuzberg. He started six years ago with pralines for a single café down the street. Today he also supplies couverture-based desserts to the Berlin fintech scene: office cafés, founder dinners, the catering for Solana hackathons. That fintech customer base is why every wallet he uses is published on his website and on every invoice. His customers expect the known address to be active.

Lukas, a man in his mid-thirties with round wire-frame glasses and a light beard, working a slab of dark couverture on a marble counter in his Berlin chocolate workshop. Stainless steel surfaces and labelled jars (COUVERTURE 70% ECUADOR, PRALINE BASE, GIANDUJA, COCOA NIBS) line the back wall. Warm late-afternoon window light.

Lukas in his Kreuzberg workshop, working a slab of dark couverture.

Maria, from part one, ships herbs and dried fruit. Lukas, the story of part two, makes chocolate. They live in the same world. Both depend on suppliers they would rather competitors did not learn about. Maria pays hers. Lukas wants to own a piece of his.

The supplier. A Swiss family-run artisan chocolate maker called Maison Flüh. Two generations, sixty years old, supplies high-end chocolatiers across Europe. Lukas has bought from them for five years. He knows their warehouse. He has met their second-generation owner. His best praline lines use their dark couverture.

The offer. Maison Flüh is doing a tokenized equity raise. The factory is incorporated in Switzerland. The actual sale is run by a regulated registrar in Austria, a B2B compliance shop called BlockReg, whose KYC product is BlockRegKYC. Lukas's supplier rep mentions the offering on a call. Lukas wants in. The amount he is willing to put down is twenty thousand euros. Large for him, modest for the raise.

The second-generation owner of Maison Flüh standing on the factory floor of his Swiss chocolate workshop, holding up a paper stock certificate for the tokenized equity raise. Copper tempering kettles, wooden moulds, and stacks of dark chocolate slabs fill the background. Warm industrial light.

Maison Flüh's second-generation owner, on the factory floor, with the certificate behind the tokens Lukas wants to buy.

The compliance path is fine. Lukas runs a clean business. He has no problem doing KYC with BlockReg. He uploads his passport, his Gewerbeschein (German trade licence), his proof of address. BlockReg marks him verified.

Now the on-chain part. The minting contract for the equity tokens needs to know Lukas is KYC'd. The straightforward way: BlockReg's backend signs an attestation that says "the wallet at 0xLUKAS is KYC'd, verified at this timestamp, by BlockReg." The contract verifies the signature, mints the equity, ships the tokens to 0xLUKAS.

This is fine for Lukas, except for two things.

  1. His wallet 0xLUKAS is now public on a KYC list. Anyone scraping the chain can build a list of every wallet BlockReg has signed for. Lukas's address is on it.
  2. The mint transaction is public. Anyone watching can see the wallet that bakes pralines for Bitwala's office party just bought equity in Maison Flüh. That tells the world who his supplier is.

The second leak is the bad one. The first is the structural one.

The naive workaround. Lukas thinks: I will use a new wallet. He nearly does. Then he stops. Funding the new wallet has to come from somewhere: his business wallet, his KYC'd CEX account, or a fresh on-ramp that itself KYCs him. Maison Flüh will pay a stablecoin dividend every quarter; when he spends it, the trail forms. A single wallet UI prompt asking him to sign an "I own both addresses" message will collapse the disguise. And he is buying a position he plans to hold for ten years. Two wallets that must stay perfectly uncorrelated for a decade is a perpetual chore, not a solution.

The trap is not bad opsec. The trap is you have to be perfect forever.

// the discipline tax: a "fresh" wallet, over time0xLUKASknown wallet0xFRESHsupposed to be privatefundinglink #1"just €100to get started"dividendlink #2spends quarterlypayout to businessownership siglink #3"prove you ownboth wallets"gas refilllink #4"out of ETH,refill quickly"In theory, keeping two wallets uncorrelated is easy. In practice, very hard over a decade or more.

He closes his laptop. Opens a new tab. Searches private equity purchase on chain. The answers come back with phrases he half-knows: zero-knowledge proof, credential, selective disclosure.

This is what one of those is.

2. Where each piece comes from

Lukas's search leads him to a name. Zero-knowledge proof. The math is from 1985, from a paper by Goldwasser, Micali, and Rackoff. The use case is older:

How to convince someone that a statement is true without showing them why.

Modern privacy pools turn this into a recipe. Four actors. Each does one job. They do not need to trust each other.

// meet the castREGISTRARBlockRegAustrian registrar// KYC, signs credentialsBUYERLukas BrehmBerlin chocolatier// the buyer in this storyCONTRACTblock #nblock #n+1block #n+2Privacy Poolsmart contract on Ethereum// verifies, mints, emitsPUBLICObservereveryone watching the chain// scrapers, rivals, analytics

BlockReg, the regulated registrar. They KYC Lukas once, off-chain. They hand him a signed credential. That credential never goes on-chain. It is a piece of paper, digital, that sits in his wallet.

Lukas and his wallet. His wallet holds the credential. When Lukas wants to buy equity, his wallet runs a small program on his laptop. That program reads the credential, reads the offer, and produces a short cryptographic object called a proof. Nothing about the credential leaves his machine. Only the proof leaves.

The Privacy Pool. A smart contract on Ethereum. It does not know Lukas. It does not see his credential. It receives the proof, runs a verification function, and returns a yes or a no. If yes, it mints the Maison Flüh equity into the pool, to a new identity that only Lukas can see.

The Observer. Everyone else watching the chain. Block explorers, scrapers, his competitors, the analytics firms that build wallet profiles for a living. They see the transaction land. They see events emitted. They learn nothing about Lukas, his credential, or what he bought.

Here is what flows between them.

// who talks to whom, and who sees whatREGISTRARBlockReg// runs KYC// issues credentialsBUYERLukas + wallet// holds credential// generates proofCONTRACTPrivacy Pool// verifies proof// mints commitmentPUBLICObserver// scrapers, rivals,// analytics firmscredentialoff-chain, privateproof πon-chain, no identityeventspublic, anonymous⊘ cannot link events to Lukas⊘ cannot link events to BlockReg's KYC listEach arrow carries the bare minimum the next actor needs. Nothing more.

That is the rest of this post. The next section unpacks the wallet's superpower: a proof that says true without saying why. The section after that walks through the credential. Then we sit on Lukas's shoulder as he generates the proof and watch what each piece does. The swimlane below maps the data flow step by step.

// where each piece comes fromBLOCKREGLUKAS + WALLETPRIVACY POOLOBSERVERSTEP 1 · OFF-CHAINIssue credentialcredential// kyc · eu · retail · 2027STEP 2Holds credentialSTEP 3 · LOCALGenerate proofprivate inputs (witness):credentialnew notesigningKeysaltsprove()output:πSTEP 4 · ON-CHAINVerify proofverify(π, public)STEP 5Mint equityto new commitment// Maison Flüh equity → poolSTEP 6 · PUBLICSees events onlyverify(π, ...) ← relayerEquityCommitmentInsertedTransfer → poolCredentialNullifierConsumed// no identity. no link.// 0xLUKAS untouched.issuesusesπ + public inputsemitsFour actors. One signs once off-chain. One does the work. One checks and mints. One watches and learns nothing.

3.1 What a zero-knowledge proof actually is

That superpower has a name and a recipe. The recipe is forty years old, and easier than the name makes it sound.

Picture this. You and a friend sit at a small table. There are two billiard balls in front of you. One is red. One is teal. Your friend has been colorblind since birth. You can tell the balls apart at a glance. To them, both balls look the same shade of grey.

You want to prove to your friend that the two balls are different colors. You do not want to tell them which one is red.

Here is the game.

// the colorblind friendyou (the prover)// i can tell them apart"swapped." or "not swapped.""did i swap them?"friend (the verifier)?// i can't see the colorsAfter enough rounds, the verifier is convinced. They still don't know which color is which.

Your friend takes both balls and puts them behind their back. They flip a private coin in their head: heads they swap the balls between their hands, tails they leave them. They bring both hands out and show you a ball in each hand. You look. You can see colors, so you know whether the positions changed since the last view. You say swapped or not swapped. Your friend writes it down.

You play another round. And another.

If the two balls really are different colors, you will get every round right. You can always see whether they moved. If the two balls were the same color, the best you could do is guess. Half the time right, half the time wrong. After ten rounds without a miss, your friend is reasonably convinced. After twenty, the chance you got that lucky by guessing is roughly one in a million. After fifty, it is vanishing.

Your friend leaves the table convinced the two balls are different colors. They never learned which one is red and which is teal. They learned the fact. They did not learn the secret.

You did not need to trust your friend. They did not need to trust you. The game itself produced the conviction. The math of "lucky enough times in a row" did the work.

The game is not what Lukas's wallet runs. The recipe is.

// the three properties every zk proof hasCompletenessif true, accepted.// no false rejections×Soundnessif false, rejected.// no false acceptancesZero-knowledgeverifier learns nothing else.// no leakageEvery real ZK proof system delivers all three. The cost is computational.

Every zero-knowledge proof, including the one Lukas's wallet is about to generate, satisfies the same three properties.

Completeness. If the statement is true, an honest prover can always convince an honest verifier. In our game, the two balls really are different colors, so you always pass. In Lukas's case, he really is KYC'd, so his proof will always verify.

Soundness. If the statement is false, no prover can fool the verifier, except with vanishing probability. In our game, if the balls were the same color, you would slip up after a few rounds. In Lukas's case, anyone trying to fake a credential without actually having one cannot produce a passing proof.

Zero-knowledge. The verifier learns nothing beyond the truth of the statement itself. In our game, your friend leaves knowing the balls differ but not which is which. In Lukas's case, the Privacy Pool contract leaves knowing he is KYC'd but not which wallet is his, not which credential he holds, not his name.

That is the recipe. Notice that the game with your colorblind friend needs many rounds. Each round is one challenge and one answer. Lukas's wallet does not have time for fifty rounds with the blockchain. Real ZK proof systems collapse all of those rounds into a single message that the wallet sends once. Because there is no back-and-forth, this is called a non-interactive proof.

The message is also small. A few hundred bytes, sometimes a few kilobytes, no matter how complicated the statement is. The word for short in this context is succinct, and it is the S in SNARK and in STARK. Succinct means the proof stays short even when the thing it proves is huge.

The math that makes non-interactive and succinct work together is heavy. Elliptic-curve pairings, polynomial commitments, arithmetic circuits, a 1986 transform by Amos Fiat and Adi Shamir. We do not need any of it to follow what Lukas's wallet does. A later part of this series unpacks it. For now we trust the cryptography and look at how he uses it.

3.2 The credential: what BlockReg hands Lukas

Lukas now knows what a ZK proof is. The next step is what it proves about. The piece BlockReg gives him is called a credential, but a credential alone is not enough. The pool also needs to know which credentials count. That information lives in two places.

BlockReg keeps an association set off-chain. It is a Merkle tree. Each leaf is one KYC'd subject, hashed so the tree contains no names or wallets in plain form. Adding Lukas means adding one new leaf. Removing him means removing that leaf and rebuilding the tree.

The root of that tree, a single 32-byte hash, gets posted on-chain to a tiny contract called the ASP (Association Set Provider). The root changes every time the set changes. The root is the only piece of this system anyone on chain ever sees.

// the association set: tree off-chain, root on-chainOFF-CHAIN · BlockReg's association setROOT0xab12...c5dfh_01h_23subj 0subj 1Lukassubj 3// 4 leaves shown. real trees can hold thousands.post root on chainON-CHAIN · ASP CONTRACTassociationRoot:0xab12...c5df// 32 bytes.// same size for 4 leaves// or 4 million.// reveals no member identities.

What Lukas gets from BlockReg is a credential: the data he needs to reconstruct his own leaf and prove it is in the current tree. That data has four parts:

  • The attributes BlockReg verified (kyc, eu, retail).
  • A wallet hash that ties Lukas's leaf to his wallet, without putting the wallet on chain.
  • A secret salt that only Lukas knows. Without the salt, no one can recompute his leaf, even if they know everything else.
  • A Merkle path: the list of sibling hashes from his leaf up to the root.

Lukas's wallet keeps all four. None of it ever crosses the chain boundary.

// the credential primitiveCREDENTIAL// signed once, used many timesissuerBlockReg.TLDsubject0x???? (hidden)attributeskyc, eu, retailexpiry2027-12-31// wallet pairs this card with a secret salt// + the merkle path to BlockReg's current rootThe credential card is what BlockReg signs. The wallet keeps it next to the salt and path.

Think of a theatre with a paid-up members list. The theatre keeps the full list at the box office. At the entrance, they post a single fingerprint of the list: a short string that changes whenever the list does. When Lukas walks up, he proves "my name is somewhere on the list that matches today's fingerprint," and the door lets him in. He never says which row. Tomorrow the theatre updates the list and posts a new fingerprint. Yesterday's fingerprint stops being useful.

A Merkle tree is the same idea, made mathematical. The leaves are the members. The root is the fingerprint. Lukas's proof is a sequence of hashes that climbs from his leaf to the root and matches.

Contrast this with the naive approach. BlockReg could sign a fresh attestation for each purchase: "wallet 0xLUKAS, kyc-verified, valid for this one mint." That is one ticket per show. Every attestation hits the chain, every attestation names a wallet. The KYC list builds itself by accident. The Merkle approach posts one root per update, and nothing about individual members ever appears on chain.

Three things make this work for a privacy pool.

Membership is provable in zero knowledge. Lukas proves his leaf is in the tree with the current root, without revealing which leaf is his. This is what 3.1 was for. The proof is short. The verifier only needs the root.

The on-chain footprint is fixed. One 32-byte root, no matter how many subjects are in the tree. Adding Lukas costs zero extra storage on chain. The ASP contract stays the same size whether the tree holds four leaves or four million.

Revocation is just a new root. BlockReg removes Lukas's leaf, recomputes the root, posts the new one. Old proofs against old roots stop verifying. Lukas asks for a fresh path and continues.

For the rest of this post, the credential is the secret half that lives in Lukas's wallet. The root is the public half that lives on chain. The ZK proof is the bridge between them. It carries nothing about Lukas's wallet, his leaf, or his salt.

3.3 Lukas's proof: what goes where

The credential is the secret half. The root is the public half. The proof is what crosses between them, and its job is to keep the two halves separate.

A ZK proof has exactly two kinds of inputs.

The witness is the secret part. It is what only Lukas's wallet knows. The wallet feeds it into the prover, the prover uses it to build the proof, then the wallet throws it away. The witness never crosses the chain boundary.

The public inputs are the named values the chain expects. Every verifier sees them. Anyone reading the transaction can read them. They are what the proof commits to making checkable.

The swimlane below shows where each piece lives, lane by lane. Hover any underlined term in the paragraphs to highlight it.

// lukas's proof, lane by lane1. Lukas (private)2. Wallet (computation)3. Proof + public inputs4. Chain (verifier)credentialsigned by BlockRegsigningKeyderived from seednullifierSaltfresh per txblindingfresh random// never leaves walletcheck credential signaturecompute commitmentcompute nullifierprove(witness, public)// all of the above, sealed inside// all computation localπcommitmentnullifierrevRootblockreg.pubkeyequity.contract// what lands on chainverify(π, public)// pass / failinsertCommitment(c)recordNullifier(n)equity.mintTo(pool)// public ledger updatesOnly lane 1 stays in Lukas's wallet. Everything else lives on chain.
Hover any underlined term below to highlight its place in the swimlane.

The witness is what only Lukas's wallet knows. His credential card with the attributes BlockReg verified. The secret salt that mixes into his leaf hash. His Merkle path: the sibling hashes between his leaf and the root. A fresh signing key for the new equity note the pool is about to mint to him. None of it ever leaves his wallet.

Inside the wallet, the prover circuit takes those private inputs and runs four steps. It recomputes his leaf from the credential and salt. It walks the Merkle path up and confirms the result matches BlockReg's current association root. It computes a fresh nullifier from his secrets, so this credential cannot be used twice for the same action. It binds the new commitment to his fresh signing key. The output is a single proof artifact of around 250 bytes.

Travelling on chain alongside the proof are the public inputs. BlockReg's current association root. The nullifier Lukas's wallet just computed. The new commitment that gets inserted into the privacy pool. The equity contract address that names what is being minted. These four values, plus the proof, are everything the chain receives.

The chain runs verify(π, public). The contract checks that the association root matches the one in the ASP. That the nullifier has not been seen before. That the proof itself is mathematically sound under those public inputs. If all three pass, the pool inserts the new commitment, records the nullifier, and the equity contract mints to a fresh commitment owner inside the pool. Lukas's known wallet does not appear in this transaction at all.

The trick that makes all of this private is the anonymity set. BlockReg's association set is every subject they have KYC'd: Lukas, plus everyone else. The proof says "one of these leaves was used." It does not say which one. If the set has a thousand subjects, the chain knows the action was taken by one of a thousand people, and no more.

This is what ASP (Association Set Provider) means in practice. The ASP defines the anonymity set. Bigger tree, stronger privacy. BlockReg keeps the set wide and the root current.

Section 3.4 walks through what an observer actually sees when this transaction lands. Then it flips the view: same scene, same transaction, but seen by a regulator with an audit key. That is the one perspective in which Lukas's name comes back.

3.4 What the public actually sees

The pool just confirmed Lukas's transaction. Time to look at what actually landed.

Two views. Same transaction. Same block. Different keys, different visibilities.

In the public view, anyone reading the chain sees this: a commitment was inserted into the privacy pool, a nullifier was recorded, the equity contract minted twenty thousand euros of Maison Flüh tokens to the pool itself. The transaction was paid for by a relayer. No KYC list grew by one entry. Lukas's known wallet, 0xLUKAS, is untouched.

Both of Act 1's two leaks are gone.

The first leak. "His wallet 0xLUKAS is now public on a KYC list." Look at the call data: no wallet address went through BlockReg, no signature from BlockReg appears on chain. The only BlockReg-related thing on chain is the association root, which is a 32-byte hash that says nothing about any individual member.

The second leak. "The mint transaction is public." Look at the events: yes, a commitment was inserted, yes, Maison Flüh equity was minted to the pool. But no one watching can tell which subject in BlockReg's tree initiated this. The proof only says one of these leaves was used.

Lukas's chocolatier-in-Berlin-Kreuzberg world, his public wallet, his café customers, his fintech catering route: none of it is visible from this transaction. The chain learned that a KYC'd European retail subject bought Maison Flüh equity for €20,000. It learned no more.

Now flip the toggle below.

// the moment lukas's transaction landsEXPLORER · tx 0xab12...c5dfSENDER?0x???...???(relayer, not Lukas)// chain sees an unrelated addressCALLDATAverify(π, public)events emitted:EquityCommitmentInsertedCredentialNullifierConsumedTransfer → pool// all you can see.// no identity.PRIVACY POOLnew commitment0x????...????// owner unknownnew nullifier0x????...????// credential unknownMaison Flüh equity→ minted to a commitment inside this pool// owner: ???// amount: ???// indistinguishable from any other buyer// inside this poolPUBLIC VIEW: events on chain. No identity. No link to Lukas.
Same data on chain. Toggle the view to see what the audit key reveals.

Inside the pool, every commitment is encrypted to a small set of viewing keys. BlockReg holds one by regulatory mandate. Lukas's own wallet holds another. With either key, the same transaction reads completely differently: the new commitment is Lukas Brehm's, the credential consumed is his, the amount is twenty thousand euros, the asset is Maison Flüh equity.

That is the design point of UPC's compliance layer. Privacy is not the absence of visibility. Privacy is programmable visibility, with the keys spread across exactly the parties who need them, and no one else.

4. Going deeper: Lukas's transaction, one step at a time

Going deeper. This section is for the technically curious. If the story is enough, skip to section 5.

Lukas's transaction in full detail. Eight steps, with sizes and times.

  1. Wallet opens the credential. Lukas's credential lives in his wallet's encrypted storage. The wallet decrypts it locally. The credential contains the attributes BlockReg verified (kyc, eu, retail), the wallet-bound salt, and the leaf index that says which row of the tree is his.

  2. Wallet fetches the latest Merkle path. BlockReg publishes the full tree contents through a public API (the leaves are hashed, so this leaks nothing). The wallet pulls the siblings between Lukas's leaf and the current root. About thirty hashes for a million-leaf tree, around one kilobyte total.

  3. Wallet rolls fresh secrets. A signing key for the new commitment (32 bytes). A nullifier salt (32 bytes). A blinding factor (32 bytes). All from a CSPRNG.

  4. Wallet reconstructs the leaf and walks the path. leaf = Poseidon31(walletHash, attrs, salt). The output is a 32-byte field element. The wallet hashes its way up the path, sibling by sibling, and confirms the climbing result equals BlockReg's current associationRoot from the ASP contract.

  5. Wallet builds the new commitment and the nullifier. commitment = Poseidon31(signingKey, equity, amount, blinding). nullifier = Poseidon31(credential, action_id). The action_id binds the nullifier to minting Maison Flüh equity in this pool, so the same credential can later be used for a different action without colliding.

  6. Wallet runs the prover. This is the slow step. About 2 to 30 seconds on a modern laptop, depending on the proof system. The output is a single proof of around 250 bytes for SNARKs, around 10 kilobytes for STARKs.

  7. Lukas's wallet submits through a relayer. The proof and the four public inputs go to a relayer service that pays the gas. The relayer sees only the public inputs (no wallet identity in them) and broadcasts the transaction. Lukas's known wallet, 0xLUKAS, never appears on chain.

  8. Chain verifies and finalises. verify(π, public) runs in around 250,000 gas for SNARKs, around one million gas for STARKs. If it passes, the pool inserts the new commitment, records the nullifier, calls equity.mintTo(pool), and emits the events the observer in 3.4 saw.

Total: about half a minute on the wallet side, about a second on chain. Lukas owns Maison Flüh equity inside the pool. The chain has no record of his name.

// lukas's transaction, with numbers1. Lukas (private)2. Wallet (computation)3. Proof + public inputs4. Chain (verifier)credentialleaf preimage from BlockRegmerklePathsiblings to rootsigningKeycontrols the new notenullifierSaltfresh per txblindingfresh random// never leaves wallet1. leaf = Poseidon31(walletHash, attrs, salt)2. verify path → associationRoot3. commitment = Poseidon31(...)4. nullifier = Poseidon31(...)5. prove(witness, public)// ~2-30s on M2 laptop// all computation localπ~250 B SNARK / ~10 KB STARKcommitment32 Bnullifier32 BassociationRoot32 Bequity.contract20 B// what lands on chainverify(π, public)// ~250K gas SNARK// ~1M gas STARKinsertCommitment(c)recordNullifier(n)equity.mintTo(pool)// public ledger updatesSame shape as the section 3.3 swimlane. With numbers.

5. What this means for you

Lukas owns Maison Flüh equity. The shares sit inside a privacy pool, registered to a commitment only his wallet can spend. He paid twenty thousand euros for them. His business wallet, 0xLUKAS, is untouched. The Berlin fintech scene that pays him to cater their hackathons has no idea what he just bought.

The four actors did their jobs.

BlockReg KYC'd him once and added him to their association set. They never saw his transaction.

Lukas's wallet kept his credential off-chain, generated a proof, and threw away the secrets. It never sent his credential anywhere.

The privacy pool checked a proof against the latest root. It minted the equity to a fresh commitment. It learned nothing about who made the call.

The Observer saw a generic transaction land, a commitment inserted, a nullifier consumed. They saw hashes. They learned nothing else.

This post did not cover three things. The math inside the prover. The hash function inside the leaves, which is part 4 of this series. The encryption that lets BlockReg and Lukas (and only them) read the audit layer, which is part 8.

For now, Lukas owns his equity. That is enough.

6. Where UPC fits

The piece of Lukas's stack you can take and reuse today is the compliance layer. We have been calling it the association set and the ASP contract. In code, that piece is an open-source package called UPC, short for Universal Private Compliance.

UPC's whole job is to answer one question: "is this address in the registrar's tree?" It ships the tooling to run the off-chain Merkle tree, the on-chain ASP registry contract, and the ZK circuit that proves membership without revealing which leaf. If you re-read sections 3.2 and 3.3, the parts that talk about BlockReg's tree, the 32-byte root in the ASP, and the leaf-plus-path proof are the parts UPC delivers.

UPC does not do the rest of the post. The new commitment Lukas creates, the nullifier that prevents reuse, the per-note encryption that powers the audit view: those live in the privacy pool that uses UPC. In our stack that pool is called UPP. The two compose. Either can be replaced.

Three decisions shaped UPC.

Policy-neutral. UPC does not know what "approved" means. It does not pick a KYC provider, a sanctions list, an attestation schema, or a jurisdiction. The registrar runs the policy they need; UPC runs the tree and the verifier.

Pluggable. The hash function, the tree depth, the attestation source, and the membership gate are all interfaces. A registrar on one stack plugs in one verifier. A registrar on a different stack plugs in another. The wire format stays the same.

Standalone. UPC ships as a TypeScript SDK and a small set of Solidity contracts. It is not bound to any one pool. Use it under UPP. Use it under your own pool. Use it under a competitor's pool. The on-chain root has the same shape.

UPC is an open-source preview, not a 1.0. The code is on GitHub. If your team is building tokenised securities, compliant DeFi, or any system that wants regulated participants without a public KYC list, we want to hear what is missing.

Stay tuned for Part 3: How agents pay without leaking your business. When it ships we will edit this footer to link to it.


Privacy Pool 101: nine short posts.

  01. What is a privacy pool?                ← see part 1
  02. How a ZK proof hides everything        ← you are here
  03. How agents pay (u402)                  coming soon
  04. Picking a ZK-friendly hash             coming soon
  05. The audit finding that made us redo it coming soon
  06. Wrapping a STARK in a SNARK            coming soon
  07. Why our Merkle tree got eight times wider coming soon
  08. What you must encrypt                  coming soon
  09. Compliance without surveillance        coming soon