Start with the failure, because the failure is the whole reason this exists. A wallet provider gets popped, and what leaks isn't seed phrases. It's the leaf signing scalars, the derived keys that actually sign transactions. If you've never thought hard about hierarchical deterministic wallets, here's the part that matters. The leaf key is the one with authority. It signs. And once it's public the account is anyone's. You can't rotate it, you can't revoke it, you can't email support. The key is the password, and the password is on the internet now, forever.
So the question I care about is whether the rightful owner can get their money out without touching the key the attacker now holds. If recovery needs that key, congratulations, you've built a footrace, and the attacker has a head start. Recovery has to lean on something the attacker does not have.
That something is the master secret. Picture the wallet as a family tree. The seed phrase is the root, and every key below it is a child, derived by a one-way step. You can walk down from a parent to a child, but you can never climb back up from a child to its parent. The leak exposed some leaves near the bottom. The owner still holds the root, in their seed phrase. The attacker holds a stolen leaf and no way up the tree. So if you make recovery mean "prove you know the root this exposed leaf descends from," the attacker is simply out. He can't make that proof. Not because we forbid it, but because the mathematics will not let him.
The catch is the word "prove." You obviously can't hand the master key to a contract; the moment it is on-chain it is public, and you have handed the attacker the very thing you were protecting. You need to prove you know it without showing it. That is a zero-knowledge proof, and it is the entire game.
The relation
Here is exactly what the proof establishes. I know a master extended private key. Run it down the standard Cardano path, CIP-1852 over BIP32-Ed25519, and you arrive at a leaf scalar kL. Multiply the base point by it and compress, A = compress(kL·B), and you have the leaf public key. Hash that, C = Blake2b-224(A), and you have the credential, the exact bytes sitting inside the compromised address. That credential is one leaf in an eligibility snapshot, a fixed Merkle tree whose root the contract already holds, so checking membership is cheap. And every piece of it folds into a single number:
pub = Fr(Blake2b-256(domain_sep ‖ scriptHash ‖ snapshot_version ‖ root ‖ C ‖ entitlement ‖ D ‖ role))That one field element is the only public input to the proof. The master key, the path, the Merkle siblings, the entitlement, all of it stays private witness. The verifier never sees any of it. It checks the proof against pub, and the contract rebuilds pub from data it already trusts.
Why the destination lives inside the hash
See the D in that hash? That is the destination, where the recovered money is allowed to land, and it is baked into the proof on purpose. This is the answer to the attack everyone thinks of first. Someone watching the mempool sees your proof arrive, copies it, and tries to swap in his own address. He can't. The proof is welded to the destination it was built for. Change D and pub changes, and the proof is dead on arrival. That is why I can publish a recovery proof in the clear and not care who reads it. The only address it will ever pay is the one I chose when I made it.
The same hash pins the entitlement, the snapshot version, and the script it is aimed at. A proof is good for one claim against one deployment and nothing else. It is not a coupon you can reuse.
The circuit
The statement turns into an arithmetic circuit, one long list of constraints that must all hold together. This one has 3,450,403 of them. Where do three and a half million constraints come from? Hashing, mostly. The derivation is a chain of HMAC-SHA512 calls. The credential is a Blake2b hash. Walking the Merkle tree is fourteen more Blake2b hashes. The destination binding is one more. Every hash is thousands of constraints, and there are a lot of hashes.
We did not invent any of this. The circuit is built from gadgets that were each audited on their own, the derivation chain, the curve arithmetic, the Blake2b core, and the work was wiring proven parts together so that "correctly" actually holds. That word is doing the heavy lifting, and it is exactly where this kind of system goes wrong.
One piece earns a mention: the entitlement. A real wallet does not just hold ADA, it holds native tokens, and the proof has to commit to the precise bundle so the contract pays back what was there, to the token. The encoding is fixed at up to eight assets, 553 bytes, sorted and padded, so the bytes are deterministic and the circuit and the contract never disagree about what they are looking at. The whole thing sits at K=22, the size bucket the proving system rounds up to.
Verifying it on-chain
The point was always to check the proof on Cardano itself, not on some server I ask you to trust. Cardano gives you the BLS12-381 pairing operations as Plutus built-ins under CIP-381, the elliptic-curve machinery a Groth16 verifier runs on, callable straight from a contract. So a Plutus V3 validator can do the verification with its own hands.
There is a wrinkle anyone building this will smack into. The circuit uses lookup tables to make hashing cheap. The moment the proving system sees lookups, it quietly staples an extra object onto the proof, a Pedersen commitment in the BSB22 scheme, and a textbook Groth16 verifier has no idea it is there. So the on-chain verifier has to reproduce the proving library's full procedure, that commitment included. In plain terms: pull the extra group elements out of the verifying key and the proof, run a pairing check that the prover actually knows what it committed to, turn the commitment point into a field element with the exact recipe the library uses, expand_message_xmd over SHA-256, byte for byte, fold that into the public-input term,
vk_x = K0 + pub·K1 + e_cmt·K2 + commitmentand run the final pairing. One byte off in that challenge hash and nothing matches, so "byte for byte" is the literal spec, not a figure of speech.
What the contract actually checks
Verifying the proof is necessary, not the finish line. The full claim does more. The contract rebuilds pub from context it already trusts, the credential and entitlement in the custody datum, the root and version and script hash in its own parameters, plus the destination and role from the redeemer. It checks the proof against that rebuilt value, so a prover cannot smuggle in their own public input. It checks the payout equals the committed entitlement, to the token. It checks the money goes to D and nowhere else. And it refuses to pay itself. Money leaves custody through this claim path or through governance, and through nothing else.
It ran on a real chain
A Plutus V3 contract on Cardano's preview testnet verified a real 3,450,403-constraint proof and moved funds because of it. Flip one byte of the proof and resubmit, and the node rejects it, at the precise step where the bad byte becomes an invalid curve point. So the verifier is not rubber-stamping; it says yes to a real proof and no to a doctored one. The whole verification fit in roughly a third of a transaction's compute budget, which is to say it is cheap enough to actually use.
Then the full thing, end to end. Lock five test ADA in custody. Spin up a clean wallet that has never held a cent. Prove the circuit with that wallet as the destination, and claim. The contract verified the proof, matched the entitlement, matched the destination, and paid out. Five ADA landed in a fresh UTxO at an address that had been empty its whole short life, put there by nothing but a proof that I knew a master secret I never typed into anything connected to the chain. The verification is transaction 7f7f41e0…; the full claim is ababcda3…. Both are sitting on the preview chain right now with real fees and confirmations.
Trusted setup, and why I'm not losing sleep over it
People hear "trusted setup" and reach for the panic button. I get the reflex. But look at what you are already trusting. The funds sit in a contract that a three-of-five governance set can move whenever they agree to, by design, so that same group can patch a flaw or migrate the pot. You have already handed five people the keys to everything. A setup ceremony asks for far less than that.
The math needs exactly one honest participant. One person, anywhere in the line, who generates a little randomness, stirs it into the parameters, and throws their copy away. If a single contributor in the whole crowd does that honestly, nobody can forge a proof, not the other contributors, not anyone. So you run the ceremony with the governance set, plus the white-hat who swept the funds, plus a dozen strangers who have never met, and you finish on a public coin-flip nobody owns, like a future block hash. For the setup to be poisoned, every last one of them has to collude and lie. That is a heavier ask than the one you already swallowed when you let three of those five move the money.
Here is how I would actually run it. Phase one is universal and circuit-independent, and you do not run it yourself; you take an existing Perpetual Powers of Tau, which already has hundreds of contributors and covers a circuit this size. Phase two is specific to this circuit, and that is the ceremony you run: the governance signers, the white-hat, and unrelated outside contributors, sequenced by a coordinator, every contribution published so anyone can check the final parameters came from the contributions claimed, and a public randomness beacon as the last word. This was never the research problem. Zcash ran one. So did Tornado, and Aztec, and the Ethereum people ran one with a hundred and forty thousand contributors. It is a Tuesday.
What's actually left
So strip the cryptography down to the claim: someone who lost a wallet but still holds the master seed can pull their exact funds to a fresh address by handing the chain a proof, and the chain will check it and pay, without the leaked key ever being touched. That works, on a real network, today.
The honest gaps live upstream of the setup.
There is a race. Cutting the attacker out only holds once the at-risk funds are in the custody contract, and getting them there is itself a transaction the attacker, who has the leaked key, is trying to beat. The eligibility snapshot has to be taken before the draining starts, not after. The cryptography here assumes it is standing on the far side of that race; it does not win the race for you. That is the real engineering problem, and it is a timing-and-operations problem, not a proving-system problem.
And someone has to build the eligibility list, and you are trusting them to build it honestly. Who is owed what is a human judgment before it is ever a Merkle leaf. That is governance, not math, and it is the part I would want the most eyes on.
So: a working spine, and the load-bearing question answered. Can a node verify one of these proofs and move real funds on the strength of it? It can. I watched it.