Privacy-first zk credential attestation on Citrea: batch proofs, instant verification.
What it is (one-liner)
A privacy-preserving credential attestation and verification system: issuers batch-commit records to a Bitcoin-secured chain (Citrea) and anyone can verify an individual claim with a zero-knowledge proof—without exposing personal data.
The Problem
Credentials (degrees, certificates, employment letters) are easy to forge and hard to verify globally.
Verifiers (employers, platforms) either trust PDFs/emails or must contact the issuer directly—slow, manual, and privacy-leaking.
On-chain storage of PII is unacceptable; off-chain databases are centralized and reversible.
What ProofGuard Solves
Authenticity: The issuer proves “this record was part of our official batch” using zk-SNARKs.
Privacy: Verifiers get a binary result (Valid/Invalid) without seeing raw personal data.
Global Verifiability: Anyone can independently verify against an immutable on-chain commitment (Merkle root).
Cost Efficiency: Batch attestation compresses 100s/1000s of records into a single on-chain commitment.
Core Concepts (plain English)
Merkle Tree: Off-chain, we hash each credential into a leaf. Leaves form a Merkle tree; its root uniquely commits to the entire batch.
zk-SNARK (Groth16): A tiny proof that a specific leaf (the student’s credential) exists in the committed tree and satisfies the circuit’s rules—without revealing the underlying data.
On-chain Contracts:
ProofGuardRegistry: Stores Merkle tree state (via incremental insert). Emits current root after each batch/insert; exposes getters.
ProofGuardCoreVerifier: Stateless verifier that checks Groth16 proofs (generated off-chain) via Ethereum precompiles (pairing checks).
Citrea (EVM on Bitcoin): We post the root/updates to Citrea, leveraging Bitcoin security while using EVM tooling (Remix, MetaMask, ethers).
Stakeholders & Journeys
Onboarding: Connect SIS (student information system) to ProofGuard via API or simple upload.
Batch Attestation:
Normalize records (stable JSON) → encrypt each record’s payload (so on-chain storage is never raw PII).
Compute the leaf for each record (consistent hash inputs that match the circuit).
Insert leaves into the on-chain Merkle tree (or call a batch function), which emits a new root.
(Optional) Generate a proof template or pre-compute per-record proofs if the flow needs it.
Result: One or a few on-chain transactions represent a large set of credentials, time-stamped and immutable.
Gets a Claim Bundle: { registry, verifier, root, publicInputs[], proof }.
publicInputs[] typically includes the Merkle root + disclosed statement hashes.
proof is the Groth16 proof generated from the student’s encrypted record and the tree path.
Can share the claim as a QR code or short JSON.
Opens the public dApp:
Pasts/scans the Claim Bundle.
dApp fetches getCurrentMerkleRoot() from ProofGuardRegistry.
Compares provided root vs on-chain root (or verifies against a specific historical root).
Calls ProofGuardCoreVerifier.verifyProof(a,b,c,input) via eth_call.
Sees Valid / Invalid. No PII is exposed at any point.
Repo Components (minimal logic approach)
Contracts
ProofGuardCoreVerifier.sol: Groth16 verifier (IC points hard-coded; generated from the circuit).
ProofGuardRegistry.sol: Ownable + Incremental Merkle Tree (zk-kit library). Functions to store encrypted records and insert leaves; exposes root and leaves.
Circuits
Define the exact leaf hash and public inputs layout (e.g., Poseidon hash of selective fields).
Set depth (e.g., 16) to match the on-chain registry’s TREE_DEPTH.
claimsN (or equivalent) must match your schema so inputs/constraints line up.
Off-chain Tooling
A small batch uploader (CSV/JSON → canonicalize → encrypt → leaf → on-chain insert).
A prover that, for a given holder, computes the Merkle path and generates the Groth16 proof.
A claim packer that exports the Claim Bundle JSON + QR.
Data Model & Privacy
On-chain: Only encrypted strings (ciphertexts), Merkle leaves, events, and roots.
Off-chain: The cleartext record lives with the holder/issuer; proofs and public inputs reveal only what is necessary.
No PII on-chain: All raw data is encrypted before touching the registry.
Selective Disclosure: Circuits can be designed to prove properties (e.g., “degree=BS, year≥2024”) without revealing the entire record.
API/UX Surface (example)
Issuer Admin UI
Upload CSV/JSON → map fields to schema → “Generate leaves” → “Attest batch”
Shows tx status, final root, and per-record status.
Student Portal
“Create Claim” → choose which attributes to disclose → download JSON/QR claim bundle.
Verifier dApp
Textarea / QR scanner → “Verify” → instant Valid/Invalid.
Trust & Threat Model
Trust: You trust the issuer’s policy (they control what is attested) but not their storage; proof + chain root prevents tampering.
Tamper Resistance: Once a root is on-chain, issuer cannot later modify past records without changing the root.
Privacy: Verifier learns only the validity of the claimed statement; no raw data leakage.
Key Management: Issuer/holder encryption keys must be handled securely; losing keys can make claims unverifiable or undecryptable.
Performance & Cost
Batching drastically reduces on-chain cost (one/few transactions per N records).
Groth16 verification gas is low and deterministic; proofs are tiny (a few hundred bytes).
Citrea: Native coin (cBTC) for gas; EVM RPC compatibility streamlines tooling.
Constraints & Assumptions
Circuit/Contract Parity: Merkle depth, leaf hash function, and public input ordering must match exactly across:
the circom circuit,
the generated verifier,
the off-chain leaf builder,
the on-chain registry.
Updates/Revocations:
Easiest pattern: append new leaf representing revocation or updated status and verify against latest root.
A dedicated revocation list circuit can be added later (roadmap).
Availability:
Encrypted data is on-chain (durable), but decryption depends on holder/issuer keys.
Off-chain re-generation of proofs is required when root changes (e.g., periodic batch inserts).
ChatGPT said:
Smart contracts in Solidity: ProofGuardRegistry (incremental Merkle tree via zk-kit) + ProofGuardCoreVerifier (Groth16 verifier with hard-coded VK IC points).
OpenZeppelin (Ownable, ReentrancyGuard) for access + safety; deployed on Citrea testnet (Bitcoin-secured EVM).
Circuits in circom; proofs generated with snarkjs; public inputs include Merkle root + selective claim hashes.
Off-chain Node/TypeScript batch uploader: canonicalizes JSON, encrypts payloads, computes Poseidon leaves, and submits inserts.
Encryption (ECIES/NaCl style): on-chain only stores ciphertext; PII never appears in clear.
Frontend dApp (Next.js/React + ethers.js): paste/scan a “claim bundle” (a,b,c + inputs + root), auto-reads on-chain root, calls verifyProof via eth_call.
Minimal gas by batching many records into one on-chain commitment (root), with per-record proofs verified client-side.
“Hacky but handy”: a single attestBatch wrapper (loop in-contract) to compress UX into one tx without changing semantics.
Tooling lint/format/test: pnpm, TypeScript, eslint/prettier, and a tiny prover CLI for local proof generation.
Partner value: Citrea gives Bitcoin-level security with EVM tooling, so Remix/MetaMask/ethers “just work” while anchoring to BTC.

