Real-Time Zero-Knowledge Proof-of-Reserves & Solvency Oracle for Stellar Issuers
β
Real Groth16 (BN254) proof verified on Stellar testnet.
Reproduce with npm run prove:demo β oracle CDXROOACFGK7FIOMNRO22O25O5YIMSHA3DKEIQXUUWHR74QGVGKXXSOY; a fresh snarkjs proof makes attest_reserves return true on-chain, and tampered inputs are rejected.
Honest status: the hosted web app is a demo sandbox (local crypto simulations for UX); the load-bearing ZK is the prove:demo pipeline plus the deployed contract.
Stablecoin issuers (USDC, EURC) hold billions in custodian reserves. In the wake of historical collapses, trust in stablecoins is at an all-time low. To restore trust, issuers publish Proof-of-Reserves (PoR). However:
- Privacy Leakage: Standard PoR unmasks individual customer balances and total company assets to competitors and the public.
- Centralization: Standard audits are retrospective and require trusting third-party accounting firms.
Crisp solves this by leveraging Zero-Knowledge Merkle-Sum Tree circuits. It allows Stellar stablecoin issuers to continuously prove that their off-chain bank reserves exceed their total liabilities (
- β‘ Real-Time Audits: Instant balance scraping and proof generation.
- π Zero Leakage: All customer balances are blinded with private salts.
- π Customer Inclusion Checks: Customers verify their balance was included in the solvency pool in under 1 second client-side.
Solvency Attestation Workflow:
- Connect Freighter Wallet inside the Issuer Dashboard.
- Submit reserve balances
$\rightarrow$ off-chain engine scrapes balances & generates ZK proof.- Verify Groth16 on-chain via Soroban
$\rightarrow$ update public solvency state dynamically.- Customer verifies balance inclusion locally using their private salt.
sequenceDiagram
autonumber
actor Issuer as Stablecoin Issuer
participant Horizon as Stellar Horizon API
participant Client as Crisp Client Panel
participant Supabase as Supabase Database
participant Soroban as Stellar Soroban (Verifier)
Issuer->>Client: Trigger Solvency Attestation
Client->>Horizon: Pull all token holder balances
Horizon-->>Client: Return balances list
Client->>Client: Construct Merkle-Sum Tree & ZK Proof
Client->>Soroban: Submit tx: attest_reserves(proof, root, total_L, reserve_R)
Soroban->>Soroban: Run Groth16 verification via BN254 pairing host fn (Protocol 25/26)
Soroban-->>Client: Attestation Event logged on-chain
Client->>Supabase: Write attestation metadata and historical logs
graph TD
Circuits[circom circuits/*.circom] -->|circom compiler| R1CS[circuit.r1cs]
Circuits -->|circom compiler| WASM[circuit.wasm]
R1CS -->|snarkjs setup| VKey[verification_key.json]
R1CS -->|snarkjs setup| ProvingKey[circuit_final.zkey]
WASM -->|snarkjs generate witness| Witness[witness.wtns]
Witness -->|snarkjs prove| Proof[proof.json]
Witness -->|snarkjs prove| Public[public.json]
ProvingKey -->|snarkjs prove| Proof
ProvingKey -->|snarkjs prove| Public
Proof -->| Freighter / Client Submit | Soroban[Stellar Soroban Contract]
Public -->| Freighter / Client Submit | Soroban
VKey -->| Rust / cargo build | Soroban
| Layer | Technology | Rationale |
|---|---|---|
| Frontend | Next.js 16 (App Router), React 19 | Standard high-performance UI. |
| ZK Circuits | Circom (Groth16) | Standard low-level constraint ZK engine. |
| Smart Contract | Rust / Soroban SDK | Deployed on Stellar Testnet, calls native cryptographic host functions. |
| Database | Supabase (PostgreSQL) | Caches historical reports and inclusion path proofs. |
- Native BN254 Pairing Check (
env.crypto().bn254().pairing_check()) β Protocol 25/26: The load-bearing primitive βattest_reservesruns the full Groth16 pairing equation on-chain with Stellar's native BN254 host functions, so solvency is verified by the ledger itself instead of trusted off-chain. Performing this in raw WASM would exhaust Soroban's CPU budget. - Native BN254 G1 Operations (
bn254().g1_mul/g1_add): The verifier folds the public inputs (liabilities root, total liabilities, reserve threshold) into the VK commitmentvk_xusing native scalar-multiply and point-add (the MSM that Protocol 26 accelerates). - In-Circuit Poseidon Merkle-Sum Tree (Circom, bn128): Poseidon hashing runs inside the off-chain circuit (compiled over bn128, matching the on-chain BN254 field), so the entire liabilities tree collapses into one constant-size proof. On-chain we verify a single pairing β independent of account count.
- Horizon Accounts Indexing API: Natively tracks and indexes all token balances and trustlines, allowing our balance scraper to pull the liability database in seconds without running custom indexer nodes.
- Stellar Contract Events (
env.events().publish()): Publishes historical solvency roots and telemetry to the ledger, which our Next.js dashboard indexes in real-time, preventing high storage fee overheads.
Smart contracts target the wasm32v1-none compilation target (using cargo build --target wasm32v1-none or equivalent Soroban build parameters) under Rust 1.82+ to ensure compatibility with Stellar's Protocol 25/26 BN254 EC pairing host functions.
- Oracle Contract:
CDXROOACFGK7FIOMNRO22O25O5YIMSHA3DKEIQXUUWHR74QGVGKXXSOY
Maintains trusted solvency verifications and allowlisted providers:
initialize(env: Env, admin: Bytes): Initialize contract with admin identity.set_verification_key(env: Env, alpha: Bytes, beta: Bytes, gamma: Bytes, delta: Bytes, ic: Vec<Bytes>): Set Groth16 verification key points for pairing check.add_provider(env: Env, provider: Address): Authorizes a trusted balance provider address.attest_reserves(env: Env, proof: Bytes, kyc_root: BytesN<32>, total_liabilities: u128, reserves_threshold: u128, issuer_ax: BytesN<32>, issuer_ay: BytesN<32>) -> bool: Verify solvency using Groth16 verification against public inputs[kyc_root, total_liabilities, reserves_threshold, issuer_ax, issuer_ay]. Prevents duplicate attestation of the same root and updates the solvency attestation report.get_attestation(env: Env) -> AttestationReport: Retrieve the latest verified attestation report details.
Honest status: the v3 multi-issuer batch aggregation ships as a separate, dedicated contract with its own batch verification key.
circuits/aggregator.circomis compiled and proven,attest_batch_v3runs a real batch pairing check (no longer a structural-only stub), and it is verified on Stellar testnet β reproducible vianpm run prove:demo:batch. It is not wired into the hosted demo web app, which exercises the v1 oracle only.
-
attest_batch_v3(...)[v3, shipped] β Multi-issuer aggregated solvency: a single batch Groth16 proof over N issuers (min 2), the system-wide invariant$R_{total} \ge L_{total}$ , per-issuer root registration, and batch replay protection, against a dedicated batch VK on testnet contractCANW4N5YTB4UYDM4MO5WK5SUHGPLJBXG3FQATLZQ5QKAQ2A57TXQ2DL2. Reproduce:npm run prove:demo:batch.
- Node.js β₯ 20
- Cargo / Rust (to run smart contract test suites)
- Clone the repository:
git clone https://github.com/edycutjong/crisp.git
- Install dependencies:
cd crisp npm install - Configure environment variables:
Note: In development and test environments, if Supabase keys are not populated, the application will automatically fall back to an in-memory database pre-seeded with mock profiles.
cp .env.example .env.local
- Run Next.js dashboard locally:
npm run dev
Quality
# ββ Code Quality ββββββββββββββββββββββββββββ
npm run lint # ESLint source checks
npm run lint:fix # ESLint source checks with auto-fixes
npm run format:check # Prettier code format checks
npm run typecheck # TypeScript static compiler check
npm run test # Run cryptographic protocol tests
npm run test:coverage # Run cryptographic protocol tests with coverage
# ββ E2E & Performance βββββββββββββββββββββββ
npm run e2e # Playwright E2E tests (demo-mode)
npm run e2e:ui # Playwright interactive E2E UI
npm run lighthouse # Lighthouse CI metrics audit
# ββ Security & Auditing βββββββββββββββββββββ
make security-scan # Vulnerability audit + License compliance audit| Layer | Tool | Status |
|---|---|---|
| Code Quality | ESLint + TypeScript + Prettier | β |
| Unit Testing (JS) | Custom runner (100+ assertions) | β |
| Unit Testing (Rust) | Cargo test (Soroban smart contract) | β |
| E2E Testing | Playwright (3 suites, responsive, solvency) | β |
| Security (SAST) | CodeQL | β |
| Security (SCA) | Dependabot + npm audit | β |
| Secret Scanning | TruffleHog | β |
| Performance | Lighthouse CI | β |
dorahacks-stellarzh-crisp/
βββ .github/ # GitHub Actions (CI, Dependabot, CodeQL)
βββ circuits/ # ZK solvency constraint circuits (Circom)
βββ contracts/ # Soroban CrispOracle contract (Rust)
βββ db/ # Supabase schema definitions
βββ docs/ # DX logs, security audits, and visuals
βββ e2e/ # Playwright E2E tests
βββ public/ # Static files, icons, and OG cards
βββ scripts/ # Seeding, testing, and benchmark scripts
βββ src/ # React components, pages, and API routes
βββ Makefile # Testing and scanning CLI automation
βββ README.md # You are here
On-chain CPU instruction costs and memory consumption measured using soroban-sdk testutils:
| Operation | CPU Instructions | Memory Bytes | % of Limit |
|---|---|---|---|
| BN254 G1 Add / Mul (per op) | ~14,488 | 0 | <0.02% |
Groth16 Verify β full BN254 pairing check (attest_reserves) |
~22,450,000 | ~120,400 | ~22.4% |
Reproduce with the contract's Cargo test suite (cargo test -- --nocapture), which prints the Soroban budget() CPU/memory for the real on-chain BN254 pairing check. Poseidon runs in-circuit (off-chain) and has no on-chain instruction cost.
- Phase 1: Core Groth16 solvency circuit implementation (Circom)
- Phase 2: Soroban
attest_reservescontract with native BN254 pairing check - Phase 3: Merkle sum tree client-side library and browser proving
- Phase 4: Freighter wallet integration and Next.js dashboard
- Phase 5: Registered-oracle reserve attestation (v2) with Ed25519 signature verification β shipped & verified on-chain.
set_oracle_keyregisters an authorized reserve-oracle Ed25519 key;verify_oracle_sig/attest_reserves_v2verify the oracle's signature overreserves_threshold β kyc_rootagainst that registered key (closing the prior "caller supplies its own key" gap) on testnet contractCBBO72ROVZVAC2KWYZOEN6PH2GAGFFIFFDO35FV5PGM3QWDEN4EO45PU. Reproduce:npm run prove:demo:oracle(registered-key sig βtrue, tampered β rejected). Covered bytest_verify_oracle_sig_*+test_attest_reserves_v2_with_registered_oracle. Production transport: the oracle key would be operated by a TLSNotary notary that witnesses the custodian balance β that TLS-transcript layer is out of scope and not implemented. - Phase 6: Batch solvency attestation for multi-issuer aggregation (v3) β shipped & verified on-chain. Real
aggregator.circomGroth16 circuit (per-issuer solvency + system-wide conservation + Poseidon batch-root commitment over N=4 issuers) β BN254 proof β on-chainverify_batch_proof/attest_batch_v3against a dedicated batch VK on testnet contractCANW4N5YTB4UYDM4MO5WK5SUHGPLJBXG3FQATLZQ5QKAQ2A57TXQ2DL2. Reproduce:npm run prove:demo:batch(real proof βtrue, tampered inputs βfalse). Covered by contract unit teststest_batch_attestation_v3_*. - Phase 7: Hosted/decentralized prover network (e.g. Sindri) for production-grade proving β blocked on external infra: requires a third-party proving account + API key, not available in this environment. The
prove:demo:batch/prove:demopipelines are the integration point; plugging a remote prover in is a credentialed config change, not new protocol work. Not deployed β left honest rather than stubbed.
- GitHub Repository: https://github.com/edycutjong/crisp
- Live App URL: https://crisp.edycu.dev
- Pitch Deck: https://crisp.edycu.dev/pitch.html
MIT Β© 2026 Edy Cu
Built for the Stellar Hacks: Real-World ZK Hackathon. Thank you to the Stellar Development Foundation for Protocol 25/26 cryptographic host primitives.
