Stellar Hacks: Real-World ZK — Track: Real-World Use Cases, Compliance & Identity
zkCred lets anyone cryptographically prove their wallet holds at least a minimum balance — without revealing their wallet address, exact balance, or transaction history. Auditors get a simple TRUE / FALSE. Everything else stays private.
- The Problem
- The Solution
- How It Works (User Flow)
- Architecture
- Zero-Knowledge Circuit Explained
- Soroban Smart Contract Explained
- Technology Stack
- Project Structure
- Setup & Installation
- Running the Full Demo
- Why This Wins the Hackathon
- Team
When a Web3 user needs to prove liquidity in the real world — to rent an apartment, get supplier credit, or satisfy a visa requirement — they face a binary, broken choice:
| They must show... | What the other party actually sees |
|---|---|
| Their public wallet address | Real-time balance of every token they hold |
| Complete transaction history going back years | |
| All counterparties they have ever interacted with | |
| Enough information to target them for phishing or physical theft |
Sharing a wallet address is the Web3 equivalent of handing over your full bank statement, tax returns, and spending history — forever. There is currently no privacy-respecting way to prove Stellar wallet solvency.
zkCred uses Zero-Knowledge Proofs (ZKPs) to let a user prove a mathematical claim — "my balance is at least $5,000 USDC" — without revealing any data beyond that one binary fact.
| Traditional Method | zkCred (Zero-Knowledge) |
|---|---|
| Hand over public wallet address | Wallet address stays completely private |
| Auditor sees exact balance ($54,231.50) | Auditor sees only TRUE (Balance ≥ $5,000) |
| Auditor sees all past transactions | Auditor sees zero history |
| High risk of targeted attacks | Zero risk of targeted attacks |
| Privacy destroyed permanently | Nothing is exposed, ever |
The proof is a short string of bytes. It can be emailed, pasted into a form, or embedded in a document. It carries zero identifying information. Verifying it on-chain costs a fraction of a cent.
PROVER (User) VERIFIER (Auditor / Landlord)
───────────────────────────────── ──────────────────────────────────
1. Connect Freighter wallet
2. Enter threshold (e.g. $5,000)
3. zkCred reads balance via
Stellar Horizon API
4. Noir circuit runs LOCALLY:
circuit(balance=42318,
threshold=5000) → π
5. Receives proof string: 6. Receives proof string from user
"zkp_v1.abc123..." ──────▶ 7. Pastes into zkCred Auditor Portal
8. Portal calls Soroban contract:
verify_proof(π, threshold, asset)
9. Contract uses BN254 host functions
to check the pairing equation
10. Returns: TRUE ✅ or FALSE ❌
Key guarantee: Steps 3–4 happen entirely in the user's browser. The real balance is never sent to any server.
┌─────────────────────────────────────────────────────────────────────────────┐
│ zkCred System │
│ │
│ ┌──────────────────────────┐ ┌──────────────────────────────────┐ │
│ │ ZONE B — Frontend │ │ ZONE A — Backend │ │
│ │ (Next.js / React) │ │ │ │
│ │ │ │ ┌──────────────────────────┐ │ │
│ │ ┌─────────────────────┐ │ │ │ Noir Circuit (main.nr) │ │ │
│ │ │ Prover Dashboard │ │ │ │ │ │ │
│ │ │ ───────────────── │ │ │ │ fn main( │ │ │
│ │ │ 1. Connect wallet │ │ │ │ balance: u64, │ │ │
│ │ │ 2. Pick asset │ │ calls │ │ threshold: pub u64, │ │ │
│ │ │ 3. Set threshold │──┼───────▶│ │ asset_id: pub Field, │ │ │
│ │ │ 4. Generate proof │ │ │ │ nonce: pub Field, │ │ │
│ │ │ 5. Copy proof str │ │ │ │ ) { │ │ │
│ │ └─────────────────────┘ │ │ │ assert(balance >= │ │ │
│ │ │ │ │ threshold); │ │ │
│ │ ┌─────────────────────┐ │ │ │ } │ │ │
│ │ │ Auditor Portal │ │ │ └──────────┬───────────────┘ │ │
│ │ │ ───────────────── │ │ │ │ compiles to │ │
│ │ │ 1. Paste proof │ │ │ ▼ │ │
│ │ │ 2. Click Verify │ │ │ ┌──────────────────────────┐ │ │
│ │ │ 3. See TRUE/FALSE │ │ │ │ Groth16 Verifier WASM │ │ │
│ │ └──────────┬──────────┘ │ │ │ (Rust / Soroban) │ │ │
│ │ │ │ │ │ │ │ │
│ │ │ invokes │ │ │ verify_proof(π, inputs) │ │ │
│ └─────────────┼─────────────┘ │ │ → BN254 pairing check │ │ │
│ │ │ │ → true / false │ │ │
│ │ │ └──────────────────────────┘ │ │
│ │ └──────────────────────────────────┘ │
│ │ │ │
│ │ deploys to │ │
│ └─────────────────────────────────────▼──────────────────┐ │
│ │ │
│ STELLAR TESTNET │ │
│ ┌───────────────────────────────┐ │ │
│ │ Soroban Smart Contract │ │ │
│ │ (ZkCredVerifier) │ │ │
│ │ │ │ │
│ │ Storage: │ │ │
│ │ VerificationKey (instance) │ │ │
│ │ UsedNonces (temporary) │ │ │
│ │ │ │ │
│ │ BN254 host functions: │ │ │
│ │ bn254_g1_mul() │ │ │
│ │ bn254_g1_add() │ │ │
│ │ bn254_pairing_check() │ │ │
│ └───────────────────────────────┘ │ │
└──────────────────────────────────────────────────────────────────────────┘
The circuit lives in Backend/circuits/src/main.nr. Here is what it does, in plain English:
Think of it like a locked box with a window. The prover puts their secret (the balance) inside the box, locks it, and the box performs a calculation. Through the window, the verifier can see the result of the calculation (TRUE/FALSE) but cannot see inside the box.
PRIVATE (only the prover knows):
balance = 42_318_960_000 ← actual balance in micro-USDC ($42,318.96)
This never leaves the prover's device.
PUBLIC (visible to the verifier, embedded in the proof):
threshold = 5_000_000_000 ← $5,000 minimum to prove
asset_id = 0x14f0d1... ← identifies "USDC" (prevents asset-swap attacks)
nonce = 0x3a7c9f... ← unique random value (prevents proof reuse)
fn main(balance: u64, threshold: pub u64, asset_id: pub Field, nonce: pub Field) {
// Constraint 1: The hidden balance is at least the public threshold
assert(balance >= threshold);
// Constraint 2: Prevent overflow tricks
assert(balance <= 1_000_000_000_000_000);
}When nargo prove runs, it generates a cryptographic proof π that:
- These constraints are satisfied, AND
- The prover knows a valid
balancethat satisfies them
Without knowing balance at all.
The security comes from the BN254 elliptic curve. The proof is a set of curve points (A, B, C) that satisfy a pairing equation. Finding a valid (A, B, C) without knowing the private input is computationally equivalent to solving the Discrete Logarithm Problem on a 254-bit elliptic curve — which would take longer than the age of the universe with all current computing power combined.
The contract lives in Backend/contracts/verifier/src/lib.rs. Here is what it does:
Verification Key (VK) — stored permanently in instance storage
├── alpha_g1 : [α]₁ — a G1 curve point (64 bytes)
├── beta_g2 : [β]₂ — a G2 curve point (128 bytes)
├── gamma_g2 : [γ]₂ — a G2 curve point (128 bytes)
├── delta_g2 : [δ]₂ — a G2 curve point (128 bytes)
└── ic[0..3] : Input Commitments — 4 G1 points binding the public inputs
Used Nonces — stored temporarily (expires in ~150 days)
└── nonce → true (prevents proof replay)
The VK is derived directly from the Noir circuit source code. If the circuit changes, the VK changes, and the old contract can no longer verify proofs from the new circuit. This immutability is a security feature.
Step 1 — Replay check
Is this nonce already in storage? → return false immediately
Step 2 — Prepare the Public Input Point
The three public inputs are combined into one G1 point using the Input Commitments:
vk_x = IC[0]
+ threshold × IC[1]
+ asset_id × IC[2]
+ nonce × IC[3]
This uses bn254_g1_mul() and bn254_g1_add() — Stellar's native BN254 host functions running at near-zero cost.
Step 3 — Groth16 Pairing Check
The core verification equation is:
e(A, B) · e(−α, β) · e(−vk_x, γ) · e(−C, δ) == 1
Where e(·, ·) is the BN254 bilinear pairing. This equation has the magical property that it is:
- Efficient to check (one call to
bn254_pairing_check()) - Impossible to satisfy without a valid proof for the original circuit
- Trustless — no oracle, no trusted party, just math
Step 4 — Record the nonce and return
If the check passes, the nonce is stored to block replay, and true is returned.
| Function | Who calls it | What it does |
|---|---|---|
initialize(admin, vk) |
Deployer (once) | Sets the VK on-chain |
update_vk(vk) |
Admin only | Updates VK if circuit changes |
verify_proof(proof, inputs) |
Anyone | Returns TRUE / FALSE |
get_vk() |
Anyone | Read the current VK (transparency) |
is_nonce_used(nonce) |
Anyone | Check if a nonce was already consumed |
| Layer | Technology | Why |
|---|---|---|
| ZK Circuit | Noir Lang | Rust-like syntax, compiles to Barretenberg backend, generates BN254-compatible proofs |
| Proving Backend | Barretenberg (bb) | The official backend for Noir; generates Groth16 / UltraHonk proofs on BN254 |
| Smart Contract | Rust + Soroban SDK | Compiled to WASM, deployed on Stellar |
| On-Chain Crypto | Stellar BN254 Host Functions (Protocol 25+) | Native pairing checks at extremely low cost |
| Blockchain | Stellar Testnet → Mainnet | Fast, cheap, fintech-focused; 5s block time, ~$0.00001 tx fees |
| Wallet | Freighter | Standard Stellar browser extension wallet |
| Frontend | React + TypeScript + Vite + Tailwind CSS | Modern, fast, type-safe UI |
zkCred/
│
├── README.md ← You are here
│
├── Frontend/ ← Zone B: React UI
│ ├── src/
│ │ ├── Landing.tsx ← Main app (Home, Prover, Auditor views)
│ │ ├── App.tsx
│ │ └── index.css
│ ├── index.html
│ ├── package.json
│ └── vite.config.ts
│
└── Backend/ ← Zone A: ZK + Smart Contract
│
├── circuits/ ← Noir ZK Circuit
│ ├── Nargo.toml ← Noir package manifest
│ ├── Prover.toml ← Input values for proof generation
│ └── src/
│ └── main.nr ← ★ THE ZK CIRCUIT (balance >= threshold)
│
├── contracts/ ← Soroban Rust Smart Contract
│ ├── Cargo.toml ← Workspace manifest
│ └── verifier/
│ ├── Cargo.toml ← Contract dependencies
│ └── src/
│ └── lib.rs ← ★ THE VERIFIER CONTRACT (BN254 pairing)
│
└── scripts/ ← Step-by-step automation
├── 1_compile_circuit.sh ← nargo compile + bb write_vk
├── 2_generate_proof.sh ← nargo execute + bb prove
├── 3_deploy_contract.sh ← cargo build → stellar contract deploy
└── 4_verify_proof.sh ← stellar contract invoke verify_proof
You need three toolchains installed. On macOS/Linux:
# Install noirup (Noir version manager)
curl -L https://raw.githubusercontent.com/noir-lang/noirup/main/install | bash
source ~/.bashrc # or restart terminal
# Install the latest Noir compiler
noirup
# Verify
nargo --version
# Expected: nargo version = 0.33.x# Install bbup (Barretenberg version manager)
curl -L https://raw.githubusercontent.com/AztecProtocol/aztec-packages/master/barretenberg/bbup/install | bash
source ~/.bashrc
# Install the latest bb
bbup
# Verify
bb --version# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/env
# Add WebAssembly compilation target
rustup target add wasm32-unknown-unknown
# Install Soroban CLI (Stellar's contract tool)
cargo install --locked soroban-cli
# Verify
stellar --version
# Create and fund a Testnet wallet
stellar keys generate deployer --network testnet
stellar keys fund deployer --network testnetcd Frontend
npm installFollow these steps in order. Each step builds on the last.
cd Backend
chmod +x scripts/*.sh
./scripts/1_compile_circuit.shOutput: circuits/target/balance_threshold.json (circuit bytecode) and circuits/target/vk (verification key bytes)
Open Backend/circuits/Prover.toml and set your values:
# Your actual balance (PRIVATE — stays on your machine)
balance = "42318960000" # $42,318.96 USDC in micro-units (6 decimals)
# What you want to prove (PUBLIC — auditor sees this)
threshold = "5000000000" # $5,000 USDC minimum
# The asset being proven
asset_id = "0x14f0d1c0b67fb52e8b8e81e73ff31b3a98ec7a7d2c3f0bc4e9e4c8a3d6f5b2e"
# A fresh random nonce (generate a new one each time)
nonce = "0x3a7c9f2d5e1b4a8c6d0f3e7b2a5d9c1f4e8b3d6a0c7f2e5b9d4a1c8f3e6b0d"Then run:
./scripts/2_generate_proof.shOutput: A proof string starting with zkp_v1. — this is what the prover sends to the auditor.
./scripts/3_deploy_contract.shOutput: A Stellar contract address like CDFGT...X3KV. Copy this into Frontend/src/config.ts.
./scripts/4_verify_proof.sh \
"zkp_v1.abc123..." \ # The proof string from Step 2
"5000000000" \ # The threshold
"0x14f0d1..." \ # The asset_id
"0x3a7c9f..." # The nonceOutput:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ VERIFIED — Balance EXCEEDS threshold
The prover holds ≥ $5,000 USDC. Wallet remains private.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
cd Frontend
npm run devVisit http://localhost:5173 — the UI connects to your deployed contract automatically.
The Stellar Development Foundation explicitly requested "Real-World ZK" applications with compliance, identity, and institutional settlement use cases. zkCred hits every criterion:
- Protocol 25/26 BN254 Host Functions — We use
bn254_g1_mul,bn254_g1_add, andbn254_pairing_checkinside the Soroban verifier. This is the exact feature the SDF built for this kind of application. - Soroban Smart Contracts — The verifier is fully on-chain, trustless, and permissionless.
- Noir ZK Language — Purpose-built for zero-knowledge proofs, modern Rust-like syntax.
| Market | Use Case | How zkCred Helps |
|---|---|---|
| Real Estate | Apartment rental applications | Prove $10K liquid without sharing bank statements |
| B2B Commerce | Net-30 supplier credit | Prove solvency without sharing financial records |
| Immigration | Visa proof-of-funds requirement | Prove $5K without revealing wallet to government |
| DeFi | Under-collateralized lending | ZK-native credit score based on provable holdings |
| Freelancing | Platform trust/verification | Prove freelancer solvency to enterprise clients |
- Chainlink / Oracles — Still expose wallet addresses; just relay the same data
- Traditional KYC — Requires sharing identity documents; defeats privacy
- Privacy Coins — Illegal in many jurisdictions; don't prove enough (you need to prove minimum, not hide everything)
- zkCred — Minimal disclosure. You prove only what you need to prove.
zkCred was built for the Stellar Hacks: Real-World ZK hackathon by [Mide_xol].
- Zone A (Backend / ZK): Noir circuit design, Soroban contract, BN254 verification
- Zone B (Frontend / UX): React dashboard, Freighter integration, Auditor portal
- Zone C (Documentation): Architecture write-up, demo video, README
MIT License — see LICENSE for details.
Built on Stellar. Proven with Noir. Verified on-chain. Private by design.