Skip to content

aliyudotdev/proofmatch-app

 
 

Repository files navigation

ProofMatch — ZK Proof of Funds on Stellar

Prove you can pay. Without showing what you have.

🌍 Live app proofmatch-stellar.fly.dev — real Groth16 proofs, real Stellar testnet transactions
🎥 Demo video youtu.be/jBYiM3C75dM
𝕏 Follow @zkproofmatch
📖 Docs proofmatch-stellar.fly.dev/docs.html — architecture, how proofs work, contributing guide
📚 API docs proofmatch-stellar.fly.dev/api.html
🏆 Leaderboard proofmatch-stellar.fly.dev/leaderboard.html

Built for the Stellar ZK Hackathon (DoraHacks).

The problem

Every day, people are asked to prove they have enough money — to rent an apartment, close a P2P trade, qualify for a loan, join a token-gated community. The only way to do it today is to hand over everything: bank statements, account numbers, transaction history. In crypto it's worse — sharing a wallet address to prove one balance exposes your entire on-chain financial life, forever.

The person asking never needed the number. They needed one bit: "is it enough?"

What ProofMatch does

ProofMatch turns "show me your balance" into a single shareable link. You pick a threshold tier (≥ 10, ≥ 100, ≥ 1K … XLM or USDC); a zero-knowledge proof is generated that your Stellar wallet clears it; a Soroban smart contract verifies the proof on-chain; you share …/v/abc123. Whoever opens it sees one verified fact — "holds ≥ 1K XLM" — and nothing else. Not the balance, not the history, not even (unless you opt in) which wallet.

  • Solvency proof — "holds ≥ X right now." Groth16 over BLS12-381, verified on-chain. The balance stays a private witness.
  • 7-Day Streak proof — "held ≥ X every day for 7 consecutive days." A second circuit (973 constraints) proves all 7 daily balances clear the threshold, defeating borrow-prove-return gaming of snapshot proofs.
  • Privacy mode — the proof is generated in your browser (snarkjs in a Web Worker); your balance never reaches the server at all. Only the 192-byte proof is transmitted.
  • Multi-wallet aggregation — prove combined holdings across 2–3 wallets.
  • Wallet ownership verification — with Freighter connected, the wallet signs a single-use SEP-53 challenge, so the link also proves the presenter controls the wallet, not just that it exists.
  • On-chain proof registry — every proof commitment is permanently anchored in a second Soroban contract, so links can be independently corroborated on-chain.
  • Embeddable — a 3-line widget (ProofMatch.verify({...})), an npm SDK (@stellar-zk/proofmatch in sdk/), and a REST API for integrators.
  • Shareable verification pages with OG cards, QR codes, a public leaderboard, and a local ZK trust score.

How it uses ZK

The core statement is a Groth16 zk-SNARK over BLS12-381:

I know a private balance such that balance ≥ threshold, where threshold is public.

  • The circuit (proof_of_funds.circom in proofmatch-circuits) enforces the comparison as a hard constraint — if the balance doesn't meet the threshold, no valid witness exists and proving fails by construction. There is no "false" result to forge.
  • Both inputs are range-checked to 64 bits via Num2Bits(64) (193 constraints total), so values ≥ 2⁶⁴ are rejected at the constraint level.
  • The proof (192 bytes: π = A, B, C) is verified on-chain by a Soroban contract using Stellar's native bls12_381_multi_pairing_check host function — the verifier learns exactly one bit.
  • BLS12-381 (not the more common BN254) was chosen specifically because Soroban ships native pairing host functions for it — the same path Stellar's own engineering used for Stellar Private Payments (~41M CPU instructions per pairing check against a 100M budget).

Why Groth16, not Noir? The hackathon's Noir resource states on-chain UltraHonk verification is "right on the borderline of CPU instruction limits" and requires a quickstart image with budget limits disabled — not deployable to real testnet today. Groth16/BLS12-381 runs comfortably within real network limits.

The ProofMatch trio

Repo Role
proofmatch-circuits Circom circuits (Groth16 / BLS12-381) + trusted setup + proof formatting
proofmatch-contracts Soroban verifier + registry contracts deployed on Stellar testnet
proofmatch-app (this) Web app, REST API, and SDK — the user-facing product

How they communicate:

  1. circuits → app: this repo vendors the compiled runtime artifacts (circuits/*_js/ witness generators, *_0001.zkey proving keys, verification keys) built from proofmatch-circuits.
  2. app → contracts: every proof is submitted to the deployed verifier contract (verify_proof), and its commitment is anchored in the registry contract (store) — both via the stellar CLI on testnet. Contract IDs are configurable via CONTRACT_ID / REGISTRY_CONTRACT_ID env vars.
  3. circuits → contracts: proof_to_cli_args.js (vendored in scripts/) converts snarkjs output into the BLS12-381 host-type hex format the verifier contract expects.

Architecture

Stellar Horizon API
       │  (real balance, never displayed)
       ▼
balance, threshold  →  Circom circuit  →  Groth16 proof (snarkjs, BLS12-381)
   (server-side, or in-browser Web Worker in Privacy mode)
                                                │
                                                ▼
                              Soroban verifier contract (Stellar testnet)
                              bls12_381_multi_pairing_check host function
                                                │
                                                ▼
                    registry contract (commitment anchored on-chain)
                                                │
                                                ▼
                                     /v/:id  — shareable proof link
  • Frontend web/frontend/ — React 19 + TypeScript (strict) + Tailwind v4 + shadcn/ui, built with Vite into web/public/ (generated output, not committed). Privacy-mode proving runs in a Web Worker (web/frontend/public/prove-worker.js) via snarkjs.
  • Backend web/server.js — a single Express server: serves the built frontend, exposes the REST API, fetches balances from Horizon, generates proofs server-side, and invokes the contracts via the stellar CLI.
  • Circuits — authored in proofmatch-circuits; compiled artifacts vendored under circuits/.
  • Contracts — the Groth16 verifier and proof registry live in proofmatch-contracts (verifier forked unmodified from Stellar's official soroban-examples/groth16_verifier).

Repo layout

web/frontend/          React + TS source (Vite project — this is what you edit)
web/frontend/public/   passthrough files copied verbatim into every build:
                       widget.js, prove-worker.js, leaderboard/verify/api pages
web/public/            build output served by Express (gitignored — run a build)
web/server.js          Express API + static server + /v/:id share-link renderer
circuits/              vendored proving artifacts from proofmatch-circuits
scripts/               proof_to_cli_args.js (snarkjs → Soroban hex format)
sdk/                   @stellar-zk/proofmatch npm package (embeddable SDK)

Running locally

Requirements: Node.js 22+, snarkjs, stellar-cli 27+, a stellar keys identity funded on testnet. (circom is only needed if you rebuild the circuits — see proofmatch-circuits.)

Build the frontend, then start the server

web/public/ is generated build output and is not committed — on a fresh clone you must build it once before the server will start:

npm ci                                  # server deps (express, stellar-sdk)
npm ci --prefix web/frontend            # frontend deps (react, vite, tailwind)
npm run build                           # vite build → web/public/
npm start                               # → http://localhost:3000

The server runs startup self-checks before binding: missing build output, an unwritable proof store, or malformed contract IDs refuse to start with an actionable report; a missing stellar/snarkjs CLI or unconfigured deployer identity starts in degraded mode (pages and share links work, proof generation doesn't) and is surfaced on /api/health. Probe an environment without starting the server:

node web/server.js --check              # human-readable report, exit 0/1
node web/server.js --check --json       # machine-readable (CI / healthchecks)

Real wallet mode — connect Freighter or enter any Stellar G… address, pick a tier (XLM or USDC). The balance is fetched live from Horizon, never displayed, and used only in-memory to generate a witness in a per-request temp directory. Hit "Use demo" to pre-fill the project's own funded testnet account and see a real proof in one click.

After a successful proof, a shareable /v/:id URL is returned. Proofs are stored for 1 year (the registry contract anchors the commitment on-chain with a matching TTL).

Frontend development

npm start                               # Express API on :3000 (keep running)
npm run dev:frontend                    # Vite dev server on :5173 with HMR,
                                        # proxying /api, /circuits, /v to :3000

Edit web/frontend/src/ with instant reload; run npm run build to refresh what :3000 serves. The Docker image builds the frontend itself in a multi-stage build, so fly deploy needs no local build step (see DEPLOY.md).

API

Health check

curl http://localhost:3000/api/health
# → { ok: true, checks: { horizon: "ok", proofsDir: "ok" }, uptimeSeconds: ... }

Verify a share link (for third-party integrators — no ZK stack required)

curl -X POST http://localhost:3000/api/verify-link \
  -H "Content-Type: application/json" \
  -d '{ "url": "http://localhost:3000/v/abc12345" }'
# → { ok: true, verified: true, threshold: "50000000", asset: "XLM",
#     txHash: "...", txExplorerUrl: "https://stellar.expert/..." }

Full endpoint reference: /api.html.

Embeddable widget

Add ZK proof verification to any website in three lines:

<script src="https://your-server/widget.js"></script>
<script>
  ProofMatch.verify({
    minAmount: 5,
    asset: "XLM",
    onVerified: (result) => {
      console.log("Verified!", result.displayAmount, result.shareUrl);
    },
    onFail: (reason) => console.log("Not verified:", reason),
  });
</script>

The widget opens a self-contained modal, runs the full proving pipeline against Stellar testnet, and returns the result via callback. CORS is enabled on all /api/* routes. Bundled apps can use the npm SDK in sdk/ instead.

Tests & rebuilding from source

  • Circuit demo, edge-case tests, recompilation, trusted setupproofmatch-circuits. After rebuilding, copy the new *_js/, *_0001.zkey, and verification-key artifacts into this repo's circuits/.
  • Contract tests and redeploymentproofmatch-contracts. Point this app at a new deployment via CONTRACT_ID / REGISTRY_CONTRACT_ID env vars.
  • Frontend type-check + build: npm run build (runs tsc --strict then vite build).

What's real vs. what's mocked

Real, end to end:

  • Groth16 proofs are actually generated (snarkjs) and actually verified on-chain by the deployed Soroban verifier — every proof is a real Stellar testnet transaction you can open on Stellar Expert.
  • Balances are fetched live from Horizon, never self-reported (outside the clearly-labeled demo tab).
  • Proof commitments are really anchored in the registry contract.
  • Privacy mode really proves in the browser — watch the network tab: only the proof leaves the page.
  • SEP-53 ownership signatures are really verified server-side (ed25519).

Mocked, simplified, or honestly unfinished:

  • "Demo numbers" tab is self-reported — it demonstrates the ZK mechanics only, and is labeled as such in the UI.
  • The circuit can't verify where the balance came from. It proves balance ≥ threshold for a private witness; the binding to a real Horizon balance is enforced by the server fetch (or your own browser in privacy mode), not cryptographically inside the circuit. A full fix — hashing a signed balance attestation in-circuit — is deliberate follow-up work: circomlib's Poseidon/EdDSA hardcode BN128 constants and can't run on BLS12-381 without regenerating them.
  • 7-day history is reconstructed, not snapshotted — daily balances come from walking Horizon effects backwards; it trusts Horizon's history and runs server-side.
  • Ownership verification is opt-in, not enforced — anyone can still generate a proof for a bare address; the verify page shows a clear warning when ownership wasn't attested.
  • Single-contribution trusted setup — fine for a hackathon, not production. A real deployment needs a multi-party ceremony.

Deployed contracts (Stellar testnet)

Groth16 verifier — checks the ZK proofs on-chain:

CB33TGWZROSRQRKFVZGZV74SKFGSDC6HOEN366PF2S2WD7ESRCHIR6VB

https://lab.stellar.org/r/testnet/contract/CB33TGWZROSRQRKFVZGZV74SKFGSDC6HOEN366PF2S2WD7ESRCHIR6VB

Proof registry — permanently anchors proof commitments (store / exists / get — source in proofmatch-contracts):

CCB7YWB42PZNXNCQVAUNX7LWYDEAAJ6YSLMCK6LPDDNMPMQEMDTMZGKL

https://lab.stellar.org/r/testnet/contract/CCB7YWB42PZNXNCQVAUNX7LWYDEAAJ6YSLMCK6LPDDNMPMQEMDTMZGKL

License

MIT — see LICENSE.

About

ProofMatch — prove you can pay without showing what you have. ZK proof-of-funds web app on Stellar

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages

  • TypeScript 39.1%
  • JavaScript 30.4%
  • HTML 28.9%
  • Other 1.6%