A collection of runnable, end-to-end examples that show how to build zero-knowledge (ZK) applications on Cardano in pure Java β no circom, no snarkjs, no native toolchains.
Every demo is built with ZeroJ β a pure-Java zero-knowledge toolkit for Cardano (Groth16 proofs over the BLS12-381 curve, Poseidon hashing, a circuit DSL, and on-chain verifiers compiled to Plutus V3 via Julc).
New to zero-knowledge proofs? Jump to What is a zero-knowledge proof? and Glossary first, then come back to Quick Start.
- What is a zero-knowledge proof?
- The examples at a glance
- Prerequisites
- One-time setup
- Quick Start
- Two kinds of demo
- The examples in detail
- Building everything at once
- Glossary (plain English)
- Troubleshooting
- Learn more
A zero-knowledge proof lets you prove a statement is true without revealing why it is true.
Classic examples you'll see in this repo:
- "I am over 18 and from an approved country" β without revealing your age or country (identity-kyc).
- "I own an NFT from this collection" β without revealing which wallet or which NFT (nft-ownership).
- "Our exchange holds enough reserves to cover all customer balances" β without revealing any individual balance (proof-of-reserves).
On Cardano, the proof is verified on-chain by a Plutus V3 script using the built-in BLS12-381 pairing operations, so a smart contract can gate funds or mint tokens based only on "the proof is valid."
ZeroJ does all of this in Java: you write the circuit (the statement to prove) as Java code, generate the proof on a plain JVM, and the matching on-chain verifier is generated for you.
There are 12 examples, in two styles.
These run against a local Cardano devnet (Yaci DevKit), build and submit real transactions, and ship with a browser UI.
| # | Demo | What you prove | Port | On-chain pattern |
|---|---|---|---|---|
| 1 | identity-kyc | Age β₯ 18 AND country approved, without revealing them | 8087 | Spending validator (reusable proof) |
| 2 | nft-ownership | I own an NFT, without revealing my wallet | 8085 | Nullifier in a sorted linked list (one-time) |
| 3 | private-voting | I'm an eligible voter, without revealing identity/vote | 8086 | Nullifier list + vote commitments |
| 4 | proof-of-reserves | Reserves β₯ liabilities, without revealing balances | 8089 | Merkle Sum Tree + attestation UTXO |
| 5 | digital-product-passport | Product is compliant (carbon, recycled %, origin) | 8088 | Poseidon MPF trie + spending validator |
| 6 | personhood-airdrop | I'm a unique human, claim once per epoch | 8086 | Nullifier as NFT asset name (sybil resistance) |
| 7 | selective-disclosure | Many predicates from one signed credential | 8085 | Two predicate-gated validators |
Ports 8085/8086 are reused across demos β run one demo at a time, or change the port in that demo's
src/main/resources/application.yml.
These are smaller, focused examples of ZeroJ's annotation-based circuit DSL β you describe the circuit with Java annotations (@ZkBool, @ZkUInt, @FixedSize, β¦) and a companion circuit class is generated. They run with ./gradlew run and print results to the console; great for understanding circuits in isolation.
| # | Demo | What it teaches |
|---|---|---|
| 8 | annotated-private-voting | Voting circuit via symbolic annotations (registry root, vote commitment, nullifier) |
| 9 | annotated-compliance-credential | Selective-disclosure credential gate with ZkUInt/ZkBool constraints |
| 10 | annotated-proof-of-reserves | Parameterized fixed-depth Merkle circuit with @CircuitParam/@FixedSize |
| 11 | annotated-batch-threshold-matrix | Nested ZkArray<ZkArray<β¦>> matrix, row-major flattening |
| 12 | zk-mpf-private-registry | Private membership in a Poseidon MPF trie (witness-level demo) |
| Requirement | Version | How to get it |
|---|---|---|
| Java (GraalVM) | 25 | sdk install java 25.0.2-graal then sdk use java 25.0.2-graal (via SDKMAN) |
| Yaci DevKit | Latest | github.com/bloxbean/yaci-devkit β only needed for the full-stack demos (1β7) |
| Node.js | 18+ | Optional β only if you want to rebuild a Svelte frontend yourself |
| ZeroJ | 0.1.0-pre3 |
Pulled from Maven; see note on local ZeroJ below |
You do not need Node.js to run the demos β each ships with a pre-built frontend bundled in the JAR.
These steps apply to the full-stack demos (1β7). The annotation demos (8β12) need only Java.
1. Start a local Cardano devnet (Yaci DevKit):
yaci-cli devkit startThis gives you a local Cardano network at http://localhost:8080 with an admin API at http://localhost:10000.
2. Select Java 25:
sdk use java 25.0.2-graal3. Fund the demo's admin wallet (the demos share one hardcoded test address):
curl -X POST http://localhost:10000/local-cluster/api/addresses/topup \
-H "Content-Type: application/json" \
-d '{"address":"addr_test1qryvgass5dsrf2kxl3vgfz76uhp83kv5lagzcp29tcana68ca5aqa6swlq6llfamln09tal7n5kvt4275ckwedpt4v7q48uhex","adaAmount":10000}'The mnemonic and address are for local testing only β never use them on a real network.
Run your first full-stack demo (identity-kyc) end to end:
# Prerequisites: yaci-devkit running, Java 25 selected, admin wallet funded (see above)
cd identity-kyc
# Build (skip tests for speed)
./gradlew clean build -x test
# Run
java --enable-native-access=ALL-UNNAMED -jar build/libs/identity-kyc-0.1.0-SNAPSHOT.jarFirst startup takes ~30β60s (it compiles the ZK circuit and runs a dev trusted setup). When you see Started β¦Application, open:
Click through the UI, or drive it from the command line:
# Verify Alice (eligible: age 25, USA) β generates a real ZK proof
curl -X POST http://localhost:8087/api/credential/verify \
-H "Content-Type: application/json" -d '{"name":"Alice"}'
# Verify Charlie (not eligible: under 18)
curl -X POST http://localhost:8087/api/credential/verify \
-H "Content-Type: application/json" -d '{"name":"Charlie"}'The proof reveals only eligible: YES/NO β never the age or country.
For your first command-line (annotation) demo, no devnet is needed:
cd annotated-compliance-credential
./gradlew run| Full-stack (1β7) | Annotation circuits (8β12) | |
|---|---|---|
| Needs Yaci DevKit | β Yes | β No |
| How to run | Build a JAR, then java -jar β¦ |
./gradlew run |
| UI | Browser (Svelte) | Console output |
| Submits Cardano txs | β Yes | β No (witness/circuit only) |
| Good for | Seeing the full ZK + on-chain flow | Learning the circuit DSL |
Why java -jar instead of ./gradlew bootRun for the full-stack demos?
ZeroJ uses the native BLST library for BLS12-381, which needs the --enable-native-access=ALL-UNNAMED JVM flag. Running the built JAR is the reliable way to pass it. (./gradlew bootRun also works for some demos but the JAR form is recommended.)
Each demo has its own README/tutorial with full API references and architecture notes β linked below. What follows is a beginner-oriented summary and the minimal run steps.
π Full README Β· π EdDSA/Jubjub tutorial
Prove you meet KYC requirements (age β₯ 18 AND country in an approved list) without revealing your age, country, or identity. A KYC provider issues a Poseidon-signed credential; you generate a proof; ADA locked at a Plutus V3 script is unlockable only with a valid proof. The proof is reusable (no nullifier) β appropriate for ongoing DeFi access. Ships with 5 test users (some eligible, some not).
cd identity-kyc
./gradlew clean build -x test
java --enable-native-access=ALL-UNNAMED -jar build/libs/identity-kyc-0.1.0-SNAPSHOT.jar
# β http://localhost:8087π Full README Β· π Design notes
Prove you own an NFT from a collection without revealing your wallet address or which NFT. The app mints NFTs on the devnet, builds a Merkle snapshot of holders, and you prove membership. Access is one-time: a nullifier is inserted into an on-chain sorted linked list so the same proof can't be used twice.
cd nft-ownership
./gradlew clean build -x test
java --enable-native-access=ALL-UNNAMED -jar build/libs/nft-ownership-0.1.0-SNAPSHOT.jar
# β http://localhost:8085π Full README
Cast a YES/NO vote in a DAO election without revealing who you are or how you voted. Eligibility is proven against a voter Merkle tree; a nullifier (Poseidon(secret, electionId)) prevents double-voting; vote commitments on-chain are decoded at tally time. Auto-creates 5 funded test voters at startup.
cd private-voting
./gradlew clean build -x test
java --enable-native-access=ALL-UNNAMED -jar build/libs/private-voting-0.1.0-SNAPSHOT.jar
# β http://localhost:8086π Full README
Prove an exchange holds reserves β₯ total customer liabilities without revealing any individual balance. Customer balances form a Merkle Sum Tree (each leaf Poseidon(accountId, balance), each node carries a running sum); the circuit proves all balances are non-negative, the root matches, the sum equals declared liabilities, and reserves cover them. An attestation UTXO records the result on-chain.
cd proof-of-reserves
./gradlew clean build -x test
java --enable-native-access=ALL-UNNAMED -jar build/libs/proof-of-reserves-0.1.0-SNAPSHOT.jar
# β http://localhost:8089π Full README
Prove a product complies with EU ESPR rules (carbon footprint, recycled content, manufacturing origin, inspections passed) without revealing sensitive supply-chain data. Products are stored in a Poseidon Merkle Patricia Forestry (MPF) trie (persistent RocksDB storage, ZK-verifiable root). Two scenarios β EV Battery (per-product) and Textile (per-batch) β including non-compliant negative cases.
cd digital-product-passport
./gradlew clean build -x test
java --enable-native-access=ALL-UNNAMED -jar build/libs/digital-product-passport-0.1.0-SNAPSHOT.jar
# β http://localhost:8088π Full tutorial
A one-claim-per-human faucet. You prove possession of an issuer-signed personhood credential (EdDSA over Jubjub, verified in-circuit) and publish a deterministic nullifier Poseidon(personhoodId, epoch). The faucet mints one NFT per claim whose asset name is the nullifier β a second claim in the same epoch reproduces the same nullifier and the mint fails. This is the building block behind Semaphore / Worldcoin / Tornado-style sybil resistance.
cd personhood-airdrop
./gradlew bootRun
# β http://localhost:8086First boot runs Powers of Tau (~3 min) + Phase-2 setup (~4 min) for the ~20k-constraint circuit, then caches to
./data/for sub-second subsequent boots.
π Full tutorial
One issuer-signed rich credential (dobYear, country, roleId, salaryBracket, nameHash) proves different predicates to different DApps with no linkability between them. Example: prove "adult resident" to a Library gate and "senior doctor" to a Healthcare gate, from the same signature. Each predicate is an independent Groth16 proof against its own Plutus V3 validator. This is the shape of W3C Verifiable Credential selective disclosure.
cd selective-disclosure
./gradlew bootRun
# β http://localhost:8085π README
The voting circuit written purely with ZeroJ symbolic annotations. The voter proves voteChoice is boolean (ZkBool), the voter secret is in a BLS12-381 Poseidon registry Merkle root, and the public vote commitment / nullifier were correctly derived. The generated PrivateVoteProofCircuit exposes build, schema, inputs, publicInputs, calculateWitness.
cd annotated-private-voting
./gradlew test # run the circuit tests
./gradlew run # run the demoπ README
A selective-disclosure credential gate via annotations. Proves age β₯ public minimum, country equals required code, sanctions flag is true, and a Poseidon commitment binds the attributes + salt. Shows how ZkUInt range constraints and ZkBool boolean constraints are injected by the symbolic type factories while the source still reads like plain domain code.
cd annotated-compliance-credential
./gradlew test && ./gradlew runπ README
A proof-of-reserves slice using @CircuitParam and @FixedSize(param = "depth"), so the same Java source builds Merkle circuits of different depths. Proves a private liability leaf is in the public liabilities root, assets β₯ claimed liabilities, and the account balance is covered. Uses explicit BLS12-381 Poseidon parameters aligned with the Cardano Groth16 path.
cd annotated-proof-of-reserves
./gradlew test && ./gradlew runπ README
A grouped compliance check over a nested symbolic array β a private ZkArray<ZkArray<ZkUInt>> matrix of measurements proven to be β€ a public per-column maximum. Demonstrates how nested arrays flatten row-major into witness names (measurement_0_0, measurement_0_1, β¦).
cd annotated-batch-threshold-matrix
./gradlew test && ./gradlew runπ README
A private-membership circuit over a Cardano Client Lib MPF registry using a Poseidon hash profile (so the root is ZK-verifiable, unlike the default Blake2b MPF). It's a witness-level demo: it builds the registry, derives MPF witness arrays, and evaluates the BLS12-381 circuit. The public verifier sees only registryRoot and keyPathNullifier. A full Groth16/Yaci flow is deferred until MPF circuit cost is reduced.
cd zk-mpf-private-registry
./gradlew test && ./gradlew runFrom the repository root, a Gradle aggregator wires up every example:
# Build all examples (with tests)
./gradlew buildAllUsecases
# Build all, skipping tests (faster)
./gradlew buildAllUsecasesNoTests
# Test all / clean all
./gradlew testAllUsecases
./gradlew cleanAllUsecasesThere's also a convenience script that builds a subset sequentially:
./build-all.sh # default: build
./build-all.sh build -x test # pass any gradle args throughThe examples depend on ZeroJ 0.1.0-pre3 from Maven. To test against a ZeroJ build from source, publish it to your local Maven repository and pass the version through:
# In the zeroj checkout
cd ../zeroj
./gradlew publishToMavenLocal
# Then build a usecase with that version
cd ../zeroj-usecases
./gradlew buildAllUsecasesNoTests -PzerojVersion=0.1.0-pre3Each module reads zerojVersion (a Gradle property / env var with a sensible default), so -PzerojVersion=β¦ is forwarded to every example.
| Term | What it means here |
|---|---|
| Zero-knowledge proof (ZKP) | A proof that a statement is true that reveals nothing else. |
| Groth16 | A specific, compact, fast-to-verify ZKP scheme. Proofs are tiny and verification is constant-time (3 pairings). |
| BLS12-381 | The elliptic curve the proofs use. Cardano (Plutus V3) has built-in operations for it, so proofs can be checked on-chain. |
| Circuit | The statement to prove, expressed as arithmetic constraints. In ZeroJ you write it in Java. |
| Constraint count | Roughly the "size" of a circuit; bigger = slower proving. These demos range from ~1,000 to ~20,000 constraints. |
| Trusted setup / Powers of Tau | A one-time ceremony producing parameters for proving/verifying. These demos use a dev-only single-party setup β production needs a multi-party ceremony. |
| Poseidon | A hash function designed to be cheap inside ZK circuits (unlike SHA/Blake2b). Used for commitments, Merkle trees, and nullifiers. |
| Nullifier | A deterministic, one-way tag (e.g. Poseidon(secret, context)) published with a proof. It reveals nothing about the secret but lets the chain reject reuse ("already voted / already claimed"). |
| Merkle tree / root | A tree of hashes whose single root commits to a whole set. A proof can show "X is in the set" against just the root. |
| Merkle Sum Tree | A Merkle tree where each node also carries a sum β used to prove a total (e.g. total liabilities). |
| MPF (Merkle Patricia Forestry) | A persistent key/value trie with a verifiable root. The DPP and registry demos use a Poseidon MPF so the root works inside a circuit. |
| Plutus V3 | Cardano's current smart-contract language version; includes the BLS12-381 builtins that verify these proofs on-chain. |
| Julc | The ZeroJ tool that compiles on-chain verifier logic written in Java to Plutus V3. |
| Yaci DevKit | A one-command local Cardano devnet for development and testing. |
| EdDSA over Jubjub | A signature scheme that can be verified efficiently inside a BLS12-381 circuit β used to check issuer-signed credentials in zero knowledge. |
| Symptom | Fix |
|---|---|
UnsatisfiedLinkError / BLST native errors |
Run the JAR with --enable-native-access=ALL-UNNAMED and make sure you're on Java 25 GraalVM (sdk use java 25.0.2-graal). |
| Transaction / topup fails, "address has no funds" | Make sure yaci-cli devkit start is running and you ran the admin top-up curl. |
| Port already in use (8085/8086 collisions) | Run only one demo at a time, or change server.port in that demo's src/main/resources/application.yml. |
| Startup takes a long time | Expected β circuit compilation + dev trusted setup runs on first boot (30sβ8min depending on circuit size). Setups are cached to ./data/ for later boots. |
Could not resolve com.bloxbean.cardano:zeroj-* |
Publish ZeroJ locally (./gradlew publishToMavenLocal in the zeroj repo) and/or pass -PzerojVersion=β¦. See Using a local ZeroJ build. |
- ZeroJ β the pure-Java ZK toolkit powering these demos: https://github.com/bloxbean/zeroj
- Yaci DevKit β local Cardano devnet: https://github.com/bloxbean/yaci-devkit
- cardano-client-lib β the Java Cardano client used to build transactions: https://github.com/bloxbean/cardano-client-lib
- Background docs in this repo:
- docs/JUBJUB_ON_CARDANO.md β verifying Jubjub/EdDSA signatures inside a BLS12-381 circuit
- docs/PRESENTATION.md β overview presentation
Project conventions and the shared tech-stack/NFR matrix live in CLAUDE.md.