Documentation

About StellarCred

Zero-knowledge credential infrastructure on Stellar. Prove facts about yourself without the data ever touching the chain.

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.

Core guarantee: A verifier reading 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

01Issue

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.

02Prove

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.

03Verify

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 Complete
identity verified
Attribute
None (random secret proves liveness)
Kept private
The secret value
ageAge Verified
age ≥ 18
Attribute
Date of birth (stored as days since epoch)
Kept private
Exact date of birth
incomeAccredited Investor
income > $200k
Attribute
Annual income in USD
Kept private
Exact income figure
jurisdictionJurisdiction Eligible
country not restricted
Attribute
ISO 3166-1 numeric country code
Kept private
Exact country code
fundsProof of Funds
balance > $10,000
Attribute
Account balance from Plaid (verified by bank, never stored)
Kept private
Exact balance figure

ZK 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.

The demo issuer’s signing key lives only in the Next.js server process — never in the browser. In production, each issuer would hold their own secret key in a hardware security module or secrets manager.

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

Stays on device
Raw attribute (age, income, country)
Random salt
Full credential JSON
Issuer signature
Witness bytes
Goes on-chain
Proof bytes (~14 KB)
Public inputs (commitment + issuer pubkey)
Holder address
Credential type (symbol)
Expiry timestamp
The commitment that appears in the public inputs is a hash — it reveals nothing about the underlying value without the salt. Even if the chain is public, an observer learns only that some holder with some credential passed verification at a given time.

Toolchain

Noir1.0.0-beta.9Circuit language & compiler
Barretenberg0.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 client
Next.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/*/vk

Deploy contracts

stellar keys generate --global deployer --network testnet --fund
SOURCE=deployer ./scripts/deploy.sh
# Copy the printed NEXT_PUBLIC_* vars into frontend/.env.local

Get started

The fastest way to see StellarCred in action on testnet — no local build needed:

  1. Install Freighter wallet and switch it to Testnet
  2. Fund your address via Stellar Friendbot (friendbot.stellar.org/?addr=YOUR_ADDRESS)
  3. Click Verify in the nav, connect your wallet, and request a KYC credential
  4. Go to Wallet, click Generate proof — the browser proves in ~10 seconds
  5. Click Submit to Stellar, approve in Freighter, and check the Apps page to see your eligibility update live