Overview
StellarCred is a zero-knowledge credential system built on Stellar. It lets a trusted issuer sign claims about a holder — KYC status, age, income, jurisdiction — and lets that holder later prove those claims on-chain without revealing the underlying data.
Every proof is generated locally in the holder's browser using the UltraHonk proof system (Noir 1.0.0-beta.9 / Barretenberg 0.87.0). The Stellar chain stores only a compact verification record — no personal data touches the ledger.
ProofRegistry.is_verified learns that a holder satisfies a claim, and nothing else. Not their name, date of birth, income figure, or country — only the boolean result.How it works
The issuer calls POST /api/issue (server-side) with the holder's wallet address and the relevant attribute (e.g. date of birth). The server computes a Poseidon2 commitment over (value, salt), signs the commitment with a secp256k1 demo key, and returns the full credential JSON. The credential is stored in the holder'slocalStorage — never on a server.
The holder clicks Generate proof. Their credential inputs are sent to POST /api/witness, which executes the Noir circuit server-side and returns the witness bytes. The browser then loads /bb/index.js (the pre-built Barretenberg browser bundle) and calls UltraHonkBackend.generateProof(witness) — pure client-side WASM proving. The proof is ~14 KB.
The holder submits the proof to ProofRegistry.submit_proof via a Freighter-signed Stellar transaction. The registry checks the issuer is trusted via IssuerRegistry, verifies the on-chain public key matches the one in the proof's public inputs, and forwards to CredentialVerifier which runs the BN254 UltraHonk verifier as a Soroban host function. If all pass, a record (verified_at, expiry) is written to persistent storage. Any protocol can then call ProofRegistry.is_verified — a free, read-only simulation.
Credential types
Each credential type has a dedicated Noir circuit. The circuit proves the claim using the commitment, the issuer's signature, and optional public parameters — all without revealing the underlying attribute.
kycKYC CompleteageAge VerifiedincomeAccredited InvestorjurisdictionJurisdiction EligiblefundsProof of FundsZK proof system
UltraHonk
StellarCred uses the UltraHonk proving system from the Aztec Barretenberg library. UltraHonk is a PLONK-family argument over the BN254 elliptic curve with a Keccak transcript, matching the on-chain verifier exposed as a Soroban host function in Stellar Protocol 22.
Proofs are generated in the browser via the pre-built Barretenberg WASM bundle (served from /bb/index.js), avoiding any dependency on webpack for the heavy proving machinery.
Poseidon2 commitment
The credential stores a Poseidon2 hash of the attribute value and a random 248-bit salt:
commitment = Poseidon2([value, salt])Poseidon2 is ZK-friendly — it is cheap to prove inside a Noir circuit while being collision-resistant. The salt prevents dictionary attacks on the commitment even for low-entropy values like country codes.
secp256k1 issuer signature
After computing the commitment, the issuer signs it with a secp256k1 ECDSA key using prehash: false — Noir uses the raw 32-byte commitment as the message digest directly. Each Noir circuit verifies this signature inside the proof, binding the claim to a specific registered issuer public key.
Smart contracts
Four Soroban contracts are deployed on Stellar testnet. They are wired at deploy time and communicate through typed contract clients (no shared library dependencies between them).
IssuerRegistryStores trusted issuer addresses, their secp256k1 public keys, and which credential types each is authorised to issue. Admins call register_issuer; anyone can read is_valid_issuer and get_issuer_pubkey.CredentialVerifierHolds a verification key (VK) per credential type. submit_proof calls the BN254 UltraHonk host function with the VK and the provided proof + public inputs. Returns bool.ProofRegistryThe public API for downstream protocols. Calls IssuerRegistry to check trust, verifies the public key in the proof's public inputs matches the registered key, calls CredentialVerifier, and writes (holder, type) → (verified_at, expiry) to persistent storage.GatedPoolDemo protocol. Reads ProofRegistry.is_verified(holder, kyc|age|income) before allowing a deposit. Shows how any protocol can gate actions on ZK proofs without owning the verification logic.Public-input layout
Each circuit outputs the following public inputs (32 bytes per field, big-endian):
field 0 commitment (Poseidon2 hash)
fields 1–32 issuer_x (secp256k1 X, one byte per field in low byte)
fields 33–64 issuer_y (secp256k1 Y, one byte per field in low byte)ProofRegistry extracts fields 1–64 and compares them byte-by-byte against the registered issuer key, preventing a holder from substituting an attacker-controlled key.
Privacy model
Toolchain
Noir1.0.0-beta.9Circuit language & compilerBarretenberg0.87.0UltraHonk prover / verifier (bb CLI + bb.js)Stellar CLI26+Contract deploy & invoke (Protocol 22 / BN254 host fns)soroban-sdk22Rust contract framework@stellar/stellar-sdk13.3.0TypeScript clientNext.js14.2Frontend framework (App Router)Build circuits
# From repo root
cd circuits
bash scripts/build.sh # compiles all 5 circuits + commit helper
# outputs *.json to frontend/public/circuits/
# outputs VKs to fixtures/*/vkDeploy contracts
stellar keys generate --global deployer --network testnet --fund
SOURCE=deployer ./scripts/deploy.sh
# Copy the printed NEXT_PUBLIC_* vars into frontend/.env.localGet started
The fastest way to see StellarCred in action on testnet — no local build needed:
- Install Freighter wallet and switch it to Testnet
- Fund your address via Stellar Friendbot (
friendbot.stellar.org/?addr=YOUR_ADDRESS) - Click Verify in the nav, connect your wallet, and request a KYC credential
- Go to Wallet, click Generate proof — the browser proves in ~10 seconds
- Click Submit to Stellar, approve in Freighter, and check the Apps page to see your eligibility update live