ProofMatch Docs
Everything a collaborator needs to understand, run, and contribute to ProofMatch — a zero-knowledge proof-of-funds system on Stellar.
Introduction
ProofMatch generates a zero-knowledge proof that a Stellar wallet holds at least a given balance — verified on-chain by a Soroban smart contract — without ever revealing the actual balance. A user shares a single link; anyone who opens it sees one verified fact ("holds ≥ X") and nothing else.
Beyond a point-in-time solvency proof, ProofMatch includes a second circuit that proves a wallet held ≥ X for 7 consecutive days, defeating the borrow-prove-return gaming a single snapshot proof is vulnerable to. Every proof's commitment is permanently anchored on-chain via a dedicated Soroban registry contract.
Architecture
ProofMatch is split into three repos, each with a focused responsibility:
Circom circuits (Groth16 over BLS12-381), trusted setup artifacts, and the proof-formatting scripts that convert snarkjs output into Soroban's expected hex layout.
The Soroban verifier contract and the on-chain proof registry (Rust). Deployed and live on Stellar testnet.
The web app (React + TypeScript frontend, Node/Express backend), REST API, and embeddable SDK. Vendors the compiled circuit artifacts and calls the deployed contracts.
How they connect: circuits are compiled once and vendored into proofmatch-app; the app generates proofs off-chain and submits them to the verifier contract, then anchors a commitment in the registry contract — both via the Stellar CLI, serialized through a single queue so concurrent proofs never race the deployer account's sequence number.
How solvency proofs work
The circuit (proof_of_funds.circom) enforces one constraint: balance ≥ threshold. threshold is public; balance is a private witness never sent anywhere.
Why the ZK is load-bearing, not decorative
If the balance doesn't meet the threshold, no valid witness or proof can be generated at all — the proving step fails outright. There is no "false" result to fake; a proof existing is itself the guarantee.
Two ways to generate the proof
- Server-side — the app fetches the wallet's balance from Horizon and proves server-side. Fast, but the server briefly sees the real balance in memory.
- Privacy mode (browser-side) — the proof is generated entirely in a Web Worker running snarkjs. The balance never leaves the user's device; only the 192-byte proof is sent to the server.
How 7-day stability proofs work
A single snapshot proof can be gamed: borrow funds right before proving, return them right after. The stability circuit closes this gap by proving balance ≥ threshold held true on all 7 of the last 7 days — a chain of comparators where every day must pass.
Daily balances are reconstructed by walking the account's Horizon effects history backwards from its current balance to each of the last 7 UTC-midnight boundaries. All 7 values stay private witnesses; only the pass/fail result and threshold are public.
Wallet connect & ownership verification
Wallets connect via Freighter. By default, a proof shows that some wallet holds the funds — not that the person sharing the link controls it. Optional SEP-53 signed-message ownership verification closes that gap: the server issues a single-use challenge, Freighter signs it with the account's key, and the server verifies the signature against the claimed account before marking the proof ownershipVerified: true.
On-chain contracts (Stellar testnet)
Verifies Groth16 proofs on-chain using Stellar's native BLS12-381 host functions. Exports verify_proof(vk, proof, pub_signals) → bool.
Permanently anchors each proof's commitment. Exports store, exists, and get.
REST API
Full endpoint reference (request/response shapes, the embeddable widget, and the SDK) lives in the API Reference. The core flow, in short:
Contributing
All three repos are open source (MIT) with scoped issues ready to pick up — spanning security hardening, new circuit features, contract improvements, testing, and CI/CD. Every PR is functionally tested before merging (we run the real code — spin up the server, submit real testnet transactions — not just review the diff), and we aim to review promptly.
Issues labeled good first issue are self-contained and low-risk. Higher-context issues (circuit changes, contract security) have more detail in their description — read the linked file/function before starting.
Known limitations
- Wallet ownership isn't proven by default. Anyone can generate a proof for any public address unless SEP-53 ownership verification is used.
- Single-contribution trusted setup. Both circuits' Groth16 ceremonies have one contribution — fine for a hackathon, not production.
- Balance witness trusts the server's Horizon fetch rather than an in-circuit signature check — blocked today on regenerating Poseidon round constants for BLS12-381 (circomlib hardcodes BN254).
- Registry has no access control — anyone can write a commitment, including squatting a proof ID before the legitimate one lands.
FAQ
Is this running on mainnet?
No — testnet only. Contracts, wallets, and all demo flows use Stellar testnet.
Can I self-host this?
Yes — see DEPLOY.md in proofmatch-app for the Fly.io deployment used for the live demo, plus a Cloudflare-tunnel option for quick local sharing.
Where do I start as a new contributor?
Pick an open issue labeled good first issue in whichever repo matches your interest (circuits/ZK, contracts/Rust, or app/TypeScript), read its linked code pointers, and open a PR against that repo's main.