Proving You Own a Wallet Without Showing It: A zk-SNARK Primer
Usman Ghani · April 12, 2026 · 2 min read
Cryptography · Blockchain · zk-SNARKs
I spent the better part of a month building a zero-knowledge wallet verifier — a small Solidity and JavaScript prototype that lets a user prove they own a wallet (and that it holds at least X tokens) without revealing the wallet address, balance, or private key. This post is the explanation I wish I'd had when I started.
The setup
The classic on-chain identity check works like this: you sign a message with your private key, the contract recovers your public address from the signature, and it looks up your balance. Simple, battle-tested — and deeply leaky. Anyone who watches the chain learns your address, your holdings, your trading history, and every protocol you've touched.
What we actually want is a way to say:
"I am a person who controls some wallet that satisfies condition C."
Without saying which wallet.
Enter zk-SNARKs
A SNARK (Succinct Non-interactive ARgument of Knowledge) lets a prover convince a verifier that they know a witness w satisfying some predicate P(x, w) — where x is public and w is private — and the verifier learns nothing about w beyond the fact that the predicate holds.
Concretely with Groth16 (the variant I used):
- The predicate is compiled into an arithmetic circuit.
- A trusted setup produces a proving key and a verifying key.
- The prover runs the circuit with their secret inputs and produces a ~200-byte proof.
- The verifier checks the proof against the public inputs in constant time (~3 elliptic curve pairings).
The verifier doesn't learn the witness. It only learns: the prover knew one.
What the circuit actually checks
For wallet verification, the predicate roughly says:
Given:
public: challenge_hash, balance_threshold, merkle_root
private: address, signature, balance, merkle_path
Assert:
1. ECDSA.verify(address, challenge_hash, signature) == true
2. balance >= balance_threshold
3. MerkleVerify(address, balance, merkle_path, merkle_root) == true
The Merkle root is a public commitment to a snapshot of all wallet balances (think of it as a compressed view of the chain state). The proof asserts: somewhere in this snapshot, there's a wallet I control whose balance clears the threshold.
The verifier never sees the address. They never see the balance. They just learn: "yes, this person passes."
What I got wrong on the first try
ECDSA inside a SNARK is brutal. The secp256k1 curve isn't SNARK-friendly, and constraint counts explode quickly. My first circuit ran for about 40 minutes per proof on a laptop — completely unusable. The fix was to switch the in-circuit signature scheme to EdDSA over a SNARK-friendly curve and bridge to ECDSA at the application layer.
Lesson: the cost of a SNARK is set by the worst primitive you put inside it. Pick your gadgets carefully.
Where this is going
I think the long-term shape of on-chain identity is something like: most interactions happen through proofs, not addresses. Your wallet is a private object. The chain only learns predicates you choose to disclose. The fact that this is technically achievable today, with reasonable performance, on commodity hardware — that still surprises me every time I run the verifier.
If you want to play with the code, the prototype is on my GitHub. It's rough, but it works.