# Build a Privacy-Preserving Badge System on Starknet **TL;DR:** Clone the repo, install Noir/Barretenberg/Garaga, run `./zk-badges/generate-proof.sh --amount 15000 --threshold 1000 --donor-secret "mysecret123" --tier 1`, then call `sncast` with the generated calldata to mint a badge on Starknet Sepolia. That's the full loop: local proof → on-chain claim → badge minted. ![image](https://hackmd.io/_uploads/rJxbxjS6ZWe.png) You donated $150 to a sensitive cause. You want to tell the world "yes, I donated enough" without exposing the exact number - because blockchains never forget. This guide shows how to keep that promise: build a Noir circuit, wire up a Garaga verifier, and use Starknet to mint a badge that certifies the tier while hiding the amount. ## Why this matters (and why you might not care) Here's the tension: **Legacy blockchains are transparent by default.** Donate on-chain and everyone sees your exact amount, wallet balance, and - even worse - the record is permanent. Traditional charities hide that info, but they become the centralized gatekeeper. Zero-knowledge lets you have it both ways: prove you met the bar, keep the number private, and rely on math instead of trust. **But does this actually matter?** Probably not for most people. If you're donating to mainstream causes, privacy isn't urgent. But if you're in a place where certain causes are politically sensitive - LGBTQ+ organizations, opposition groups, whistleblower funds - or if you just hate the idea of your giving history being permanent public record, yes. It matters. The argument: blockchains claim to be trustless. But transparency without privacy is just surveillance in a ledger. ZK proofs let you have one without the other. ## What you'll build By the end, you'll have all of the following: • A **Noir circuit** that generates ZK proofs (the math) • Two **smart contracts** live on Starknet Sepolia (the blockchain) • A **local proof pipeline** you control entirely (no external API required) • The ability to **mint privacy badges** (the payoff) **See it live right now in Sepolia:** • [UltraKeccakHonkVerifier](https://sepolia.voyager.online/contract/0x022b20fef3764d09293c5b377bc399ae7490e60665797ec6654d478d74212669) • [DonationBadge](https://sepolia.voyager.online/contract/0x077ca6f2ee4624e51ed6ea6d5ca292889ca7437a0c887bf0d63f055f42ad7010) These contracts are *already deployed*. They're verifying proofs right now. You'll understand how by the end. ## How it works (the zk magic) ### Step 1: Make a commitment You hash two things together: • Your donation amount ($150) • A secret string (like "mysecretkey123") This creates one number, the **commitment**. Think of it as a locked box. Anyone can see the box exists, but nobody can see what's inside. ``` Commitment = Hash(DonationAmount, DonorSecret) ``` ### Step 2: Generate a proof You run a prover. It takes your inputs and generates a blob of numbers, about 8 KB, that proves two things: 1. "I know what's inside that locked box" 2. "The donation amount inside is at least $100" **Crucially:** The proof doesn't reveal the amount. It just proves the claim. ### Step 3: Verify on-chain A smart contract runs a verifier. The verifier checks the proof and asks: "Is this proof valid?" If yes, the contract mints you a badge. It never learns your actual donation amount. It only knows: "This proof is valid, which means someone donated at least $100". ``` Verifier: "Proof is valid?" → Contract: "Yes" → Badge: 🏅 Minted! ``` That's it. The magic is that verification is *fast* and *public*, but proof generation is *private*. ## The proof pipeline **Data flows through three tools in sequence - and each one has a single job.** ```mermaid %%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#375a7f', 'primaryTextColor': '#ffffff', 'primaryBorderColor': '#375a7f', 'lineColor': '#5bc0de', 'secondaryColor': '#f8f9fa', 'secondaryTextColor': '#000000', 'tertiaryColor': '#ffffff', 'tertiaryTextColor': '#000000', 'tertiaryBorderColor': '#5bc0de'}}}%% flowchart LR subgraph Inputs["📥 Inputs"] TOML["Your Secrets<br/>amount, secret<br/>threshold"] end subgraph Pipeline["🔧 Pipeline"] NOIR["Noir<br/>Write constraints"] BB["Barretenberg<br/>Generate proof"] GARAGA["Garaga<br/>Make it Starknet-ready"] end subgraph Output["📤 Output"] FELT["Calldata<br/>→ Starknet"] end TOML --> NOIR --> BB --> GARAGA --> FELT ``` **Left to right:** Your secrets go in. Each tool does exactly one job. Swap any piece (Noir version, prover backend, Garaga flavor) and the pipeline still works. ## The circuit logic This tiny circuit is only doing two things, but they're the reasons your badge means anything at all: ### Check 1: commitment verification **The question:** Does Poseidon(amount, secret) equal the public commitment? **Why it matters:** This binds you to a specific donation amount without revealing it. If you later claim you donated $500 instead of $150, the hash won't match. The proof fails. **The mechanism:** You computed the commitment long ago. You published it. Now you're trying to prove something about that commitment. The circuit verifies that your current computation matches what you promised earlier, with no room for adjustment afterward. ### Check 2: threshold verification **The question:** Is the donation amount ≥ $100? **Why it matters:** This is the actual qualification. You want to prove you gave enough. The circuit checks the number. **The mechanism:** The private value `donation_amount` is compared against the public value `threshold`. If amount is too low, the constraint fails. The proof stops. ## The complete verification process ```mermaid %%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#375a7f', 'primaryTextColor': '#ffffff', 'primaryBorderColor': '#375a7f', 'lineColor': '#5bc0de', 'secondaryColor': '#f8f9fa', 'secondaryTextColor': '#000000', 'tertiaryColor': '#ffffff', 'tertiaryTextColor': '#000000', 'tertiaryBorderColor': '#5bc0de'}}}%% flowchart TD subgraph Public["🌐 Public (Everyone sees)"] T[threshold: $100] C[commitment: 0x2a5f...] end subgraph Private["🔒 Private (Only you know)"] DA[amount: $150] DS[secret: 'xyz'] end DA --> HASH["Poseidon Hash"] DS --> HASH HASH --> CHECK1{hash ==<br/>commitment?} C --> CHECK1 CHECK1 -->|Yes| CHECK2{amount >=<br/>threshold?} T --> CHECK2 DA --> CHECK2 CHECK2 -->|Yes| VALID["✅ Valid"] CHECK1 -->|No| INVALID["❌ Invalid"] CHECK2 -->|No| INVALID style Public fill:#f8f9fa,stroke:#375a7f,stroke-width:2px,color:#000000 style Private fill:#375a7f,stroke:#375a7f,stroke-width:2px,color:#ffffff style VALID fill:#4ade80,stroke:#22c55e,stroke-width:2px,color:#ffffff style INVALID fill:#f87171,stroke:#ef4444,stroke-width:2px,color:#ffffff ``` The two checks execute in order: 1. **Private Input Collection:** Prover loads the donation amount and secret phrase. Nobody else knows these values. 2. **Hashing:** Poseidon combines amount and secret into a single hash. This is the commitment. Think of it as a cryptographic fingerprint. Change even one bit of input, and the output changes completely. 3. **Check 1 - commitment match:** The circuit compares the computed hash against the public commitment. They must match exactly. If they don't, the proof fails immediately. No second check. No partial credit. 4. **Check 2 - threshold:** The circuit compares the donation amount against the threshold. If amount ≥ threshold, the check passes. If amount < threshold, the check fails. 5. **Result:** If both checks pass, the proof is valid. The verifier learns only that the constraint is satisfied. The verifier doesn't learn the amount. The verifier doesn't learn the secret. ## Quick start (GitHub Codespaces) Launch Codespaces → run the install snippet → you're ready. Prefer local? Jump to "Toolchain Installation." ### Option 1: GitHub Codespaces (recommended) 1. Go to your repository on GitHub 2. Click **Code** → **Codespaces** → **Create codespace on main** 3. Wait 2-3 minutes for the environment to build 4. In the terminal, run: ```bash # Install Noir curl -L https://noirup.dev | bash source ~/.bashrc noirup --version 1.0.0-beta.1 # Install Barretenberg curl -L https://bbup.dev | bash source ~/.bashrc bbup --version 0.67.0 # Install Garaga (Python) pip install garaga==0.15.5 # Install Bun (JS runtime) curl -fsSL https://bun.sh/install | bash source ~/.bashrc # Install project dependencies bun install # Start the API server bun run api ``` Done. Your proof API is running on `http://localhost:3001`. ### Option 2: Local setup Skip to "Toolchain Installation" below, then come back here. ## Getting testnet STRK Badge claims cost ~0.001 STRK each. You need testnet funds. ### Pick a faucet | Faucet | URL | Request | |--------|-----|---------| | **Official** | https://faucet.starknet.io/ | 0.1 STRK | | **Alchemy** | https://www.alchemy.com/faucets/starknet-sepolia | 0.1 STRK (requires account) | ## Toolchain installation (if not using Codespaces) ### Why these specific versions? Versions matter in cryptography. Mixing versions produces incompatible proofs. These versions work together: | Tool | Version | Why | |------|---------|-----| | **Noir** | 1.0.0-beta.1 | Circuit language (compiles your logic) | | **Barretenberg** | 0.67.0 | Proof prover (generates the proof) | | **Garaga** | 0.15.5 | Verifier generator (makes Cairo code) | | **Scarb** | 2.9.2 | Cairo build tool (compiles contracts) | ### macOS ```bash # Noir curl -L https://noirup.dev | bash source ~/.zshrc noirup --version 1.0.0-beta.1 # Barretenberg (may fail on ARM64 - use Codespaces if it does) curl -L https://bbup.dev | bash source ~/.zshrc bbup --version 0.67.0 # Garaga (requires Python 3.10) brew install python@3.10 python3.10 -m pip install garaga==0.15.5 # Scarb curl --proto '=https' --tlsv1.2 -sSf https://docs.swmansion.com/scarb/install.sh | sh -s -- -v 2.9.2 # Starknet Foundry curl -L https://raw.githubusercontent.com/foundry-rs/starknet-foundry/master/scripts/install.sh | sh snfoundryup # Bun curl -fsSL https://bun.sh/install | bash source ~/.zshrc ``` ### Linux (Ubuntu 22.04+) ```bash # Same as macOS - just use `source ~/.bashrc` instead of `~/.zshrc` ``` ### Verify everything installed ```bash #!/bin/bash echo "=== Verifying ZK Toolchain ===" nargo --version | grep -q "1.0.0-beta.1" && echo "✅ Noir" || echo "❌ Noir" bb --version | grep -q "0.67.0" && echo "✅ Barretenberg" || echo "❌ Barretenberg" garaga --version | grep -q "0.15.5" && echo "✅ Garaga" || echo "❌ Garaga" scarb --version | grep -q "2.9.2" && echo "✅ Scarb" || echo "❌ Scarb" bun --version && echo "✅ Bun" || echo "❌ Bun" ``` If all green, you're done. If any red, reinstall that tool. > **Docker?** Not yet. Noir + bb are still too brittle inside containers, so stick with Codespaces or a native setup. ## Environment setup Create `.env.local` (add to `.gitignore`): ```bash # Starknet Account (NEVER COMMIT!) STARKNET_ACCOUNT_ADDRESS=0x... STARKNET_PRIVATE_KEY=0x... # RPC Endpoints SEPOLIA_RPC_URL=... # Proof API (for local testing) PROOF_API_URL=http://localhost:3001/api/generate-proof ``` ## The Noir circuit Time to open the editor. This tiny function is the whole promise - read it slowly before you run anything. ### File: `zk-badges/donation_badge/src/main.nr` ```noir use std::hash::poseidon::bn254::hash_2; fn main( // Public inputs (everyone sees these) threshold: pub u64, commitment: pub Field, // Private inputs (only the prover knows) donation_amount: u64, donor_secret: Field ) { // Constraint 1: Verify the commitment matches let computed_commitment = hash_2([ donation_amount as Field, donor_secret ]); assert(computed_commitment == commitment); // Constraint 2: Verify donation meets threshold assert(donation_amount >= threshold); } ``` ### What this does The `pub` keyword means "public." Everyone sees `threshold` and `commitment`. But `donation_amount` and `donor_secret`? Those are private. They're used to *generate* the proof but never appear in it. The two assertions are the constraints. If either fails, the proof fails. - **First:** "Does my commitment hash match the public commitment?" (Binds you to your claimed amount) - **Second:** "Is my amount at least the threshold?" (Proves you qualify) Both must pass. That's the whole circuit. ### Config: `zk-badges/donation_badge/Nargo.toml` ```toml [package] name = "donation_badge" type = "bin" authors = ["Your Name"] compiler_version = ">=1.0.0-beta.1" ``` The `>=1.0.0-beta.1` allows newer versions, but this tutorial was tested with exactly `1.0.0-beta.1`. Other versions may produce incompatible proofs. ### Test the circuit Create `zk-badges/donation_badge/src/main.test.nr`: ```noir use crate::main; use std::hash::poseidon::bn254::hash_2; #[test] fn test_valid_donation() { let donor_secret = 0x123abc as Field; let donation_amount = 15000 as u64; let threshold = 1000 as u64; let commitment = hash_2([donation_amount as Field, donor_secret]); // Should pass - amount meets threshold main(threshold, commitment, donation_amount, donor_secret); } #[test(should_fail)] fn test_below_threshold() { let donor_secret = 0x123abc as Field; let donation_amount = 500 as u64; let threshold = 1000 as u64; let commitment = hash_2([donation_amount as Field, donor_secret]); // Should fail - amount is below threshold main(threshold, commitment, donation_amount, donor_secret); } ``` Run tests: ```bash cd zk-badges/donation_badge nargo test ``` ### Compile ```bash cd zk-badges/donation_badge nargo compile ``` Output: `target/donation_badge.json` (your circuit in ACIR format) ## Generating proofs Now you have a circuit. Let's generate a proof. ### Step 1: Compute the commitment The circuit will check that your commitment matches `Hash(amount, secret)`. You need to compute this locally first. Use `zk-badges/computecommitment.js` (ES modules): ```javascript import { buildPoseidon } from 'circomlibjs'; async function computeCommitment(donationAmount, donorSecret) { const poseidon = await buildPoseidon(); let secretBigInt; if (donorSecret.startsWith('0x')) { secretBigInt = BigInt(donorSecret); } else { // Convert string secret to hex first const encoder = new TextEncoder(); const bytes = encoder.encode(donorSecret); secretBigInt = BigInt('0x' + Buffer.from(bytes).toString('hex')); } const hash = poseidon([BigInt(donationAmount), secretBigInt]); const commitment = poseidon.F.toObject(hash); const commitmentHex = '0x' + commitment.toString(16); return { commitment, commitmentHex }; } // Usage const [, , amount, secret] = process.argv; computeCommitment(BigInt(amount), secret).then(({ commitmentHex }) => { console.log(commitmentHex); }); ``` Run it: ```bash cd zk-badges node computecommitment.js 15000 "mysecret123" # Output: 0x1abc...def ``` Save that output. ### Step 2: Create `Prover.toml` Create `zk-badges/donation_badge/Prover.toml`: ```toml threshold = "1000" # $10 in cents commitment = "0x1abc...def" # From step 1 above (hex format) donation_amount = "15000" # $150 in cents (private!) donor_secret = "0xabc123def456" # Hex-encoded secret (private!) ``` > **⚠️ Important:** If you're using a string secret like `"mysecretkey123"`, convert it to hex using the `computecommitment.js` script first. The script handles the conversion. Then use the hex output in Prover.toml. ### Step 3: Generate witness ```bash cd zk-badges/donation_badge nargo execute witness ``` Output: `target/witness.gz` (your witness, compressed) ### Step 4: Generate proof This is where the crypto happens. Takes 30-60 seconds: ```bash bb prove --scheme ultrakeccakhonk \ --bytecode target/donation_badge.json \ --witness target/witness.gz \ --output target/proof bb write_vk --scheme ultrakeccakhonk \ --bytecode target/donation_badge.json \ --output target/vk ``` Output: - `target/proof` - Your proof (binary) - `target/vk` - Verification key (tells verifiers how to check) ### Step 5: Generate Starknet calldata ```bash garaga calldata \ --system ultrakeccakhonk \ --vk target/vk \ --proof target/proof \ --format starkli \ > ../calldata.txt ``` Output: `zk-badges/calldata.txt` (array of numbers for Starknet) ### Automated script Doing all this manually is tedious. Use `zk-badges/generate-proof.sh`: ```bash #!/bin/bash set -e AMOUNT=1000 THRESHOLD=1000 SECRET="hunter2" TIER=1 while [[ $# -gt 0 ]]; do case $1 in --amount) AMOUNT="$2"; shift 2 ;; --threshold) THRESHOLD="$2"; shift 2 ;; --donor-secret) SECRET="$2"; shift 2 ;; --tier) TIER="$2"; shift 2 ;; *) echo "Unknown option: $1"; exit 1 ;; esac done cd "$(dirname "$0")/donation_badge" echo "🔐 Computing commitment..." COMMITMENT=$(node ../computecommitment.js $AMOUNT "$SECRET") echo "📝 Commitment: $COMMITMENT" echo "📝 Creating Prover.toml..." cat > Prover.toml << EOF threshold = "$THRESHOLD" commitment = "$COMMITMENT" donation_amount = "$AMOUNT" donor_secret = "$SECRET" EOF echo "⚙️ Compiling circuit..." nargo compile echo "🔍 Executing witness..." nargo execute witness echo "🔒 Generating proof (30-60 seconds)..." bb prove --scheme ultrakeccakhonk \ --bytecode target/donation_badge.json \ --witness target/witness.gz \ --output target/proof echo "🔑 Generating verification key..." bb write_vk --scheme ultrakeccakhonk \ --bytecode target/donation_badge.json \ --output target/vk echo "📦 Generating calldata..." garaga calldata \ --system ultrakeccakhonk \ --vk target/vk \ --proof target/proof \ --format starkli \ > ../calldata.txt echo "✅ Done! Calldata: zk-badges/calldata.txt" ``` Use it: ```bash cd zk-badges chmod +x generate-proof.sh ./generate-proof.sh --amount 15000 --threshold 1000 --donor-secret "mysecret123" --tier 1 ``` ## The Cairo verifier contract Garaga handles the math translation; you focus on policy (no replays, only tier upgrades, maintaining accurate counts). ### Generate the verifier ```bash cd donation_badge_verifier garaga gen --system ultrakeccakhonk \ --vk ../zk-badges/donation_badge/target/vk \ --project-name donation_badge_verifier ``` This creates: - `src/honk_verifier.cairo` - Auto-generated verifier (don't edit) - `src/honk_verifier_circuits.cairo` - Circuit-specific code - `src/honk_verifier_constants.cairo` - Verification key constants ### The badge contract You write this part. It's where the business logic lives. Create `src/badge_contract.cairo`: ```cairo #[starknet::contract] mod DonationBadge { use starknet::ContractAddress; use starknet::get_caller_address; use super::honk_verifier::IUltraKeccakHonkVerifierDispatcher; use super::honk_verifier::IUltraKeccakHonkVerifierDispatcherTrait; #[storage] struct Storage { verifier_address: ContractAddress, user_badges: LegacyMap<(ContractAddress, u8), bool>, user_max_tier: LegacyMap<ContractAddress, u8>, used_commitments: LegacyMap<u256, bool>, badge_counts: (u64, u64, u64), } #[constructor] fn constructor(ref self: ContractState, verifier: ContractAddress) { self.verifier_address.write(verifier); } #[external(v0)] fn claim_badge( ref self: ContractState, full_proof_with_hints: Span<felt252>, threshold: u256, donation_commitment: u256, badge_tier: u8 ) -> bool { let caller = get_caller_address(); // Prevent replay attacks assert(!self.used_commitments.read(donation_commitment), 'Commitment used'); // Can only upgrade tiers let current_tier = self.user_max_tier.read(caller); assert(badge_tier > current_tier, 'Must upgrade tier'); // Verify the proof let verifier = IUltraKeccakHonkVerifierDispatcher { contract_address: self.verifier_address.read() }; let is_valid = verifier.verify_ultra_keccak_honk_proof(full_proof_with_hints); assert(is_valid, 'Invalid proof'); // Mark commitment used self.used_commitments.write(donation_commitment, true); // Update badge self.user_badges.write((caller, badge_tier), true); self.user_max_tier.write(caller, badge_tier); // Update counts let (bronze, silver, gold) = self.badge_counts.read(); if badge_tier == 1 { self.badge_counts.write((bronze + 1, silver, gold)); } else if badge_tier == 2 { self.badge_counts.write((bronze, silver + 1, gold)); } else if badge_tier == 3 { self.badge_counts.write((bronze, silver, gold + 1)); } true } #[external(v0)] fn get_badge_tier(self: @ContractState, address: ContractAddress) -> u8 { self.user_max_tier.read(address) } #[external(v0)] fn has_badge(self: @ContractState, address: ContractAddress, tier: u8) -> bool { self.user_badges.read((address, tier)) } #[external(v0)] fn is_commitment_used(self: @ContractState, commitment: u256) -> bool { self.used_commitments.read(commitment) } #[external(v0)] fn get_badge_counts(self: @ContractState) -> (u64, u64, u64) { self.badge_counts.read() } } ``` ### Build ```bash cd donation_badge_verifier scarb build ``` Output: Two compiled contracts, ready to deploy. ## Deployed contracts (Sepolia) The system is already live. These are real contracts on Starknet Sepolia: | Contract | Address | |----------|---------| | **UltraKeccakHonkVerifier** | [`0x022b20...2669`](https://sepolia.voyager.online/contract/0x022b20fef3764d09293c5b377bc399ae7490e60665797ec6654d478d74212669) | | **DonationBadge** | [`0x077ca6...7010`](https://sepolia.voyager.online/contract/0x077ca6f2ee4624e51ed6ea6d5ca292889ca7437a0c887bf0d63f055f42ad7010) | ### If you want to deploy your own ```bash cd donation_badge_verifier # Declare verifier sncast --profile sepolia declare \ --contract target/release/donation_badge_verifier_UltraKeccakHonkVerifier.contract_class.json # Deploy verifier sncast --profile sepolia deploy --class-hash 0x0452... # Declare badge contract sncast --profile sepolia declare \ --contract target/release/donation_badge_verifier_DonationBadge.contract_class.json # Deploy badge contract (pass verifier address) sncast --profile sepolia deploy --class-hash 0x04be... \ --constructor-calldata 0x022b20fef3764d09293c5b377bc399ae7490e60665797ec6654d478d74212669 ``` ## What could go wrong - **Version mismatch:** You run Noir 1.0.0 but accidentally installed 1.0.1. Proof fails. Proofs are version-dependent. - **Prover timeout:** Takes 90 seconds instead of 60 on slower hardware. This isn't a bug, it's crypto. - **Lost secret:** You generate a proof, then lose the donor_secret. You can't prove it again with the same commitment. Backup your `.env`. - **You reveal the secret later:** Someone doxes you and finds out you donated $5K, not $100. The commitment was public, so now everyone knows. Choose secrets accordingly. - **Starknet RPC goes down:** Your proof is safe (it's local), but you can't claim the badge that moment. RPC providers are (usually) resilient, but it's not guaranteed. ## End-to-end flow (manual) Once `zk-badges/generate-proof.sh` finishes, the proof artifacts are already on disk. All that's left: 1. **Claim the badge** ```bash sncast --profile sepolia invoke \ --contract-address 0x077ca6f2ee4624e51ed6ea6d5ca292889ca7437a0c887bf0d63f055f42ad7010 \ --function claim_badge \ --calldata [calldata_from_script] 1000 0xCOMMITMENT 1 ``` Replace `[calldata_from_script]` with the array in `zk-badges/calldata.txt` and `0xCOMMITMENT` with the Poseidon hash printed by the script. 2. **Verify the tier** ```bash sncast --profile sepolia call \ --contract-address 0x077ca6f2ee4624e51ed6ea6d5ca292889ca7437a0c887bf0d63f055f42ad7010 \ --function get_badge_tier \ --calldata 0xYOUR_WALLET_ADDRESS ``` Expect `1`, `2`, or `3` depending on the tier you just claimed. That's the loop: secrets stay local, only the proof hits the blockchain. ### The full flow visualized ```mermaid %%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#375a7f', 'primaryTextColor': '#ffffff', 'actorTextColor': '#000000', 'actorBkg': '#f8f9fa', 'actorBorder': '#375a7f', 'signalColor': '#5bc0de', 'signalTextColor': '#000000'}}}%% sequenceDiagram autonumber participant User as 👤 User participant API as ⚙️ Proof API participant ZK as 🔐 Noir/BB/Garaga participant Badge as 📜 Badge Contract participant Verifier as ✅ Verifier User->>API: {amount, secret, threshold, tier} rect rgba(55,90,127,0.1) Note over API,ZK: Proof generation typically takes 30-60 seconds API->>ZK: nargo → bb → garaga ZK-->>API: calldata[] end API-->>User: {calldata, commitment} User->>Badge: claim_badge(calldata, threshold, commitment, tier) Badge->>Verifier: verify_proof(calldata) Verifier-->>Badge: true ✓ Badge-->>User: 🏅 Badge Minted! ``` User sends secrets → API generates proof → user claims badge on-chain → verifier checks proof → badge minted. ## Common errors | Error | Fix | |-------|-----| | `error: could not satisfy constraint` | `donation_amount < threshold` - increase amount | | `WASM module not found` | macOS ARM64 issue - use Codespaces | | `Invalid proof` on-chain | Version mismatch - verify `bb --version = 0.67.0` | | `Commitment already used` | Proof replay - use new `donor_secret` | | `Must upgrade tier` | Already have this tier - claim higher tier instead | | `garaga: command not found` | `pip install garaga==0.15.5` | | `nargo: command not found` | `curl -L https://noirup.dev \| bash` | | Transaction fails | Check you're on Sepolia, not mainnet | | Amounts below threshold rejected | Remember: amounts in cents ($10 = 1000, $100 = 10000) | ## Resources - [Noir Docs](https://noir-lang.org/docs) - [Barretenberg](https://github.com/AztecProtocol/aztec-packages/tree/master/barretenberg) - [Garaga](https://github.com/keep-starknet-strange/garaga) - [Starknet Foundry](https://foundry-rs.github.io/starknet-foundry/) ## Next steps: plug into Tongo This badge system is half the Starknet privacy story. The repo also contains Tongo's private donation flow (encrypted STRK/USDC balances, rollover, withdraw). Stitching them together on mainnet is the logical next experiment: fund privately via Tongo, then mint a public badge proving you crossed the threshold. It's the same ethos - math beats trust - but now it spans both the money movement and the public proof.