# ZeroJ — full documentation (for AI ingestion) > Every page of the ZeroJ documentation site concatenated into one Markdown file. zerojVersion: 0.1.0-pre12 Site: https://zeroj.dev Repository: https://github.com/bloxbean/zeroj Source revision: 1734f1731b1ebda07c70fd537b395f5845871cf6 ## Key facts - ZeroJ is a Java-first zero-knowledge proof toolkit for Cardano: define circuits in Java, prove with a pure-Java prover, verify in Java (off-chain) or on Cardano (Plutus V3 via JuLC). - Status: experimental research software, not externally audited, not for production or value-bearing/mainnet use. "Beta" components are feature-complete and correctness-tested but not audited. - **Groth16 on BLS12-381 is the supported path for the current release.** PlonK (prover, verifier and on-chain validators) is experimental: make no correctness claims about it and do not choose it by default. BN254 is legacy and disabled by default. - Maven group and Java packages are `org.zeroj` (from 0.1.0-pre12). Current version: 0.1.0-pre12. Import the BOM `org.zeroj:zeroj-bom-core:0.1.0-pre12`; opt-in modules (zeroj-verifier-plonk, zeroj-bbs, zeroj-mpf-poseidon, zeroj-jmt-poseidon) need explicit versions. Releases up to 0.1.0-pre11 used `com.bloxbean.cardano`. - Java 25+. Nothing beyond a JDK is required for the default path (no Rust, Node.js, native toolchain or external CLIs); blst acceleration and snarkjs/circom interop are optional. `zeroj-verifier-groth16` carries the blst-java JNI jar for its native verifier, and `VerifierRegistry.withServiceLoader()` lists that verifier first: construct `Groth16BLS12381PureJavaVerifier` explicitly for a pure-Java path. - Write application circuits with annotations: `@ZKCircuit`, `@Prove`, `@Public`/`@Secret`, symbolic types `ZkField`, `ZkBool`, `ZkUInt` (always with `@UInt(bits = N)`), `ZkArray`/`ZkBits`/`ZkBytes` (with `@FixedSize`). The annotation processor generates a `Circuit` companion. Never use Java `if`, `&&`, `||` on secret values — use `ZkBool.and/or/not/select`. - For Cardano circuits hash with Poseidon using explicit BLS12-381 parameters (`PoseidonParamsBLS12_381T3.INSTANCE`). MiMC and the no-params Poseidon overload are BN254-oriented; do not use them for Cardano. - Single-party trusted setup (`PowersOfTauBLS381.generate`, `Groth16Keys.setupInMemory`) is for development and tests only and requires `-Dzeroj.allowInsecureTrustedSetup=true`. Production keys come from a multi-party ceremony (snarkjs `.zkey`, imported into ZeroJ). - Every proof is freshly blinded; there is no public deterministic/unblinded prove API. - A valid proof is not authorization. On-chain, the reusable `Groth16BLS12381Verifier` only checks the math; real validators compose `Groth16BLS12381Lib`, bind the proof to the spend and recipient using values computed from ScriptContext, prevent replay with nullifiers or state, and enforce business policy. Do not lock funds with `Groth16BLS12381TxOutRefBindingVerifier`: it reads its spend binding from the guarded UTxO's own datum, which no real UTxO can satisfy. - An honest witness passing does not make a circuit sound: test invalid witnesses for every constraint. ## Table of contents - [Start here → What is ZeroJ?](https://zeroj.dev/start/overview/) - [Start here → Installation](https://zeroj.dev/start/installation/) - [Start here → Quickstart: your first proof](https://zeroj.dev/start/quickstart/) - [Start here → Status & maturity](https://zeroj.dev/start/status/) - [Learn zero-knowledge (concepts) → Zero-knowledge in plain English](https://zeroj.dev/learn/zero-knowledge-basics/) - [Learn zero-knowledge (concepts) → Circuits, constraints & witnesses](https://zeroj.dev/learn/circuits-and-witnesses/) - [Learn zero-knowledge (concepts) → Groth16, PlonK & BBS](https://zeroj.dev/learn/proof-systems/) - [Learn zero-knowledge (concepts) → Trusted setup, explained](https://zeroj.dev/learn/trusted-setup/) - [Learn zero-knowledge (concepts) → ZK on Cardano](https://zeroj.dev/learn/zk-on-cardano/) - [Learn zero-knowledge (concepts) → Glossary](https://zeroj.dev/learn/glossary/) - [Tutorials → Prove you're over 18](https://zeroj.dev/tutorials/age-check/) - [Tutorials → Private allowlist with a Merkle tree](https://zeroj.dev/tutorials/private-allowlist/) - [Tutorials → Verify your proof on Cardano](https://zeroj.dev/tutorials/verify-on-cardano/) - [Tutorials → Bring circom & snarkjs circuits](https://zeroj.dev/tutorials/snarkjs-interop/) - [Guides: circuits → Write circuits with annotations](https://zeroj.dev/guides/circuits/annotations/) - [Guides: circuits → CircuitSpec & the Signal DSL](https://zeroj.dev/guides/circuits/circuit-dsl/) - [Guides: circuits → Gadget library](https://zeroj.dev/guides/circuits/gadgets/) - [Guides: circuits → Test your circuits for soundness](https://zeroj.dev/guides/circuits/testing-circuits/) - [Guides: proving → Prove with Groth16](https://zeroj.dev/guides/proving/groth16/) - [Guides: proving → Run a trusted setup ceremony](https://zeroj.dev/guides/proving/trusted-setup-ceremony/) - [Guides: proving → Performance & large circuits](https://zeroj.dev/guides/proving/performance/) - [Guides: proving → Prove with PlonK](https://zeroj.dev/guides/proving/plonk/) - [Guides: verifying → Verify proofs in Java](https://zeroj.dev/guides/verifying/off-chain/) - [Guides: verifying → Verify proofs on Cardano](https://zeroj.dev/guides/verifying/on-chain/) - [Guides: verifying → Secure your ZK application](https://zeroj.dev/guides/verifying/application-security/) - [Guides: credentials & state → Selective disclosure with BBS](https://zeroj.dev/guides/credentials/bbs/) - [Guides: credentials & state → Large authenticated state (Poseidon MPF/JMT)](https://zeroj.dev/guides/credentials/authenticated-state/) - [Use cases → Use cases](https://zeroj.dev/use-cases/overview/) - [Use cases → Private voting](https://zeroj.dev/use-cases/private-voting/) - [Use cases → Proof of reserves](https://zeroj.dev/use-cases/proof-of-reserves/) - [Use cases → Age & KYC eligibility](https://zeroj.dev/use-cases/age-and-kyc/) - [Use cases → Private NFT ownership](https://zeroj.dev/use-cases/nft-ownership/) - [Use cases → One claim per person](https://zeroj.dev/use-cases/sybil-resistant-airdrop/) - [Use cases → Selective disclosure & reusable KYC](https://zeroj.dev/use-cases/selective-disclosure/) - [Use cases → Digital product passport](https://zeroj.dev/use-cases/digital-product-passport/) - [Use cases → Prove you own a Cardano account](https://zeroj.dev/use-cases/account-recovery/) - [Use cases → Private token transfers](https://zeroj.dev/use-cases/private-payments/) - [Reference → Modules](https://zeroj.dev/reference/modules/) - [Reference → API cheat sheet](https://zeroj.dev/reference/api-cheatsheet/) - [Reference → Configuration](https://zeroj.dev/reference/configuration/) - [Reference → FAQ & troubleshooting](https://zeroj.dev/reference/faq/) - [Reference → Migration notes](https://zeroj.dev/reference/migration/) - [Build with AI → Build with AI](https://zeroj.dev/ai/) - [Build with AI → AI Starter Pack](https://zeroj.dev/ai/starter-pack/) --- ## What is ZeroJ? Source: https://zeroj.dev/start/overview/ > ZeroJ is a Java-first zero-knowledge proof toolkit for Cardano. Learn what it does, how the pieces fit, and where to start. **ZeroJ is a Java-first zero-knowledge proof toolkit for Cardano.** You define circuits in Java, generate proofs with a pure-Java Groth16 prover on the BLS12-381 curve, and verify them off-chain in Java or on-chain in a Cardano Plutus V3 validator written with JuLC. It also includes BBS signatures for credentials where the holder chooses which attributes to reveal. You don't need Rust, Go, Node.js, or native libraries to get from a circuit to a verified proof. Everything on that path is plain Java on the JVM. ### The problem zero-knowledge solves A lot of software has to answer questions like "is this person over 18?", "does this account hold at least 10,000 ADA?", or "is this wallet on the allowlist?". The usual way to answer them is to hand over the raw data (a birth date, a balance, an address) and let someone else check it. That leaks far more than the answer. A **zero-knowledge proof** lets you prove the answer without handing over the data. The prover runs a computation on private inputs and produces a small proof. The verifier checks the proof against the public inputs and learns one thing: the statement is true. They learn nothing about the private inputs. ```text Without ZK: "Here is my birth date: 1990-04-12." → verifier learns the date With ZK: "Here is a proof that my age ≥ 18." → verifier learns only "true" ``` On a blockchain, this matters twice over. Everything on-chain is public forever, so zero-knowledge proofs are one of the few ways to build dApps that check private facts, such as votes, balances, credentials, or key ownership, without publishing them. New to the idea? [Zero-knowledge in plain English](https://zeroj.dev/learn/zero-knowledge-basics/) explains it from scratch in about five minutes. ### Why Java? Most ZK tooling lives in Rust, Go, C++, or JavaScript, often with a custom circuit language on top. ZeroJ brings the whole workflow to the JVM: - **Pure Java on the default path.** Circuit compilation, witness generation, proving, and verification with `Groth16BLS12381PureJavaVerifier` run in pure Java, with nothing to install beyond a JDK. Native `blst` acceleration is optional. (The verifier module also carries the `blst-java` binding for its native verifier; see [Verify proofs in Java](https://zeroj.dev/guides/verifying/off-chain/).) - **Circuits are Java code.** You write circuits with annotations and symbolic types (`ZkField`, `ZkBool`, `ZkUInt`), so your IDE, refactoring tools, and unit tests all work. - **GraalVM friendly.** ZeroJ targets Java 25, and several modules ship GraalVM native-image metadata. - **Fits the Java Cardano stack.** It works alongside [Cardano Client Lib](https://github.com/bloxbean/cardano-client-lib) for building transactions, [JuLC](https://github.com/bloxbean/julc) for writing Plutus V3 validators in Java, and [Yaci DevKit](https://github.com/bloxbean/yaci-devkit) for a local Cardano devnet. ### How the pieces fit Every ZeroJ application follows the same pipeline. Only the circuit is yours to design; the rest is library calls. ```text 1. Circuit A Java class annotated with @ZKCircuit └─ compile ─► R1CS constraints (the rules a valid answer must satisfy) 2. Witness Public inputs + secret inputs └─ calculate ─► a value for every wire in the circuit (stays private) 3. Setup R1CS ─► proving key + verification key (once per circuit; real keys come from a multi-party ceremony) 4. Prove proving key + witness ─► proof (192 bytes for Groth16 on BLS12-381) 5. Verify verification key + proof + public inputs ─► true / false off-chain (pure Java, in your backend or client) 6. Verify the same check inside a Plutus V3 validator (JuLC), on-chain plus the application rules you add around it ``` Step 3 deserves a warning up front. The quick, in-process setup that ZeroJ offers for development is **insecure by design**: the process that runs it could forge proofs. ZeroJ disables it unless you pass `-Dzeroj.allowInsecureTrustedSetup=true`. Real deployments use keys from a multi-party ceremony. [Trusted setup, explained](https://zeroj.dev/learn/trusted-setup/) covers why. ### Building blocks | Area | What you get | Modules | |------|--------------|---------| | Circuit authoring | `@ZKCircuit` symbolic annotations (recommended), the `CircuitSpec` DSL, and an inline lambda DSL for quick experiments | `zeroj-circuit-annotation-api`, `zeroj-circuit-annotation-processor`, `zeroj-circuit-dsl` | | Gadget library | Poseidon (with explicit BLS12-381 parameters), Merkle membership, comparators and range checks, binary decomposition, multiplexers; in-circuit Blake2b, SHA-512, HMAC-SHA512, Ed25519, BIP32-Ed25519 and CIP-1852 key derivation | `zeroj-circuit-lib` | | Provers | Pure-Java Groth16 on BLS12-381, with a streaming setup and `mmap`-able proving keys for large circuits; optional blst-accelerated backend | `zeroj-crypto`, `zeroj-crypto-blst` | | Off-chain verification | Pure-Java Groth16 verifier, proof envelopes, codecs, and a pluggable verifier SPI | `zeroj-verifier-groth16`, `zeroj-api`, `zeroj-codec`, `zeroj-backend-spi` | | On-chain verification | A reusable Plutus V3 Groth16 verifier and an on-chain library for your own validators, compiled with JuLC | `zeroj-onchain-julc` | | Credentials | BBS signatures (IRTF CFRG draft-10): sign, verify, and selective-disclosure proofs | `zeroj-bbs` (opt-in) | | snarkjs interop | Import `.ptau` and `.zkey` files, export snarkjs-compatible JSON, and contribute to Groth16 ceremonies with the `zeroj-ceremony` CLI | `zeroj-crypto`, `zeroj-codec`, `zeroj-tools` | | Authenticated state | Poseidon Merkle Patricia Forestry and Jellyfish Merkle Tree circuits (experimental) | `zeroj-mpf-poseidon`, `zeroj-jmt-poseidon` (opt-in) | PlonK proving and verification also exist in ZeroJ, but they are **experimental**, both off-chain and on-chain. Groth16 on BLS12-381 is the focus of the current release and the default everywhere in these docs. See [Groth16, PlonK & BBS](https://zeroj.dev/learn/proof-systems/) for how they differ. ### A taste of the code This is a complete circuit. It proves "I know a secret `b` such that `a × b = product`" without revealing `b`: ```java title="SecretMultiplier.java" @ZKCircuit(name = "secret-multiplier", version = 1) public class SecretMultiplier { @Prove ZkBool prove(ZkContext zk, @Public ZkField a, @Public ZkField product, @Secret ZkField b) { return a.mul(b).isEqual(product); } } ``` At compile time, the annotation processor generates a `SecretMultiplierCircuit` companion class that you use to compile the circuit, calculate witnesses, and prove. The [Quickstart](https://zeroj.dev/start/quickstart/) takes this circuit all the way to a verified proof. ### Who it's for - **Java and Kotlin developers** who want to add privacy features, such as age checks, allowlists, sealed bids, or private votes, without leaving the JVM or learning a new circuit language first. - **Cardano builders** who want dApps that check private facts on-chain, using Plutus V3's native BLS12-381 support. - **ZK engineers and researchers** who want a readable pure-Java prover, snarkjs-compatible artifacts, and independent verification paths. > **Caution: Experimental research software** > > ZeroJ is experimental and **has not been externally audited**. Don't use it to protect real > value or on mainnet. The on-chain Groth16 verifier is labeled for testnet use only, and > in-process trusted setup is for development only. The "Beta" label means feature-complete and > correctness-tested, not audited. See [Status & maturity](https://zeroj.dev/start/status/) for component-by-component > detail. ### Where to go next **New to zero-knowledge?** Start with [Zero-knowledge in plain English](https://zeroj.dev/learn/zero-knowledge-basics/), then [Circuits, constraints & witnesses](https://zeroj.dev/learn/circuits-and-witnesses/). The Learn ZK section takes about 20 minutes and assumes no cryptography background. **Want to see code running?** [Install ZeroJ](https://zeroj.dev/start/installation/), then follow the [Quickstart](https://zeroj.dev/start/quickstart/) to prove and verify your first statement in pure Java. **Building on Cardano?** Read [ZK on Cardano](https://zeroj.dev/learn/zk-on-cardano/), then follow [Verify your proof on Cardano](https://zeroj.dev/tutorials/verify-on-cardano/) to run a proof through a Plutus V3 validator on Yaci DevKit. Browse the [use cases](https://zeroj.dev/use-cases/overview/) for complete designs, including private voting, proof of reserves, and selective disclosure. --- ## Installation Source: https://zeroj.dev/start/installation/ > Add ZeroJ to a Gradle or Maven project with the BOM, wire up the annotation processor, and enable the dev-only trusted setup for local runs. ZeroJ is a set of Maven artifacts under the group `org.zeroj`. A single BOM, `org.zeroj:zeroj-bom-core`, keeps the core module versions in sync. This page gets a Gradle or Maven project ready for the [Quickstart](https://zeroj.dev/start/quickstart/). ### Prerequisites | Requirement | Version | Notes | |-------------|---------|-------| | Java | 25 or newer | GraalVM is recommended if you want native images | | Build tool | Gradle or Maven | ZeroJ itself builds with Gradle 9.2; use a Gradle version that runs on Java 25 | The simplest way to install Java 25 is [SDKMAN!](https://sdkman.io/): ```bash sdk install java 25.0.2-graal sdk use java 25.0.2-graal ``` The default path of circuit, witness, prove, and verify needs **nothing installed beyond a JDK**: no native toolchain and no external CLIs. Everything below the "Optional tools" heading is only for specific scenarios. > **Note: About the blst jar on your classpath** > > `zeroj-verifier-groth16` depends on `zeroj-blst`, which brings the `blst-java` JNI binding (a jar > with prebuilt native code) for the module's optional native verifier. The pure-Java prover and > `Groth16BLS12381PureJavaVerifier` never load it. If you discover verifiers with > `VerifierRegistry.withServiceLoader()`, note that the native `Groth16BLS12381Verifier` is listed > first; construct `Groth16BLS12381PureJavaVerifier` explicitly when you want a pure-Java path. ### Add ZeroJ to your build The dependencies below are everything the [Quickstart](https://zeroj.dev/start/quickstart/) needs: annotation-based circuits, the gadget library, the pure-Java Groth16 prover, and the pure-Java verifier. **Gradle (Groovy)** ```groovy title="build.gradle" plugins { id 'java' } java { toolchain { languageVersion = JavaLanguageVersion.of(25) } } repositories { mavenCentral() } dependencies { // One BOM for all core ZeroJ modules implementation platform('org.zeroj:zeroj-bom-core:0.1.0-pre12') // The BOM must also apply to the annotation processor path annotationProcessor platform('org.zeroj:zeroj-bom-core:0.1.0-pre12') // Circuits: @ZKCircuit annotations + the generated *Circuit companions implementation 'org.zeroj:zeroj-circuit-annotation-api' annotationProcessor 'org.zeroj:zeroj-circuit-annotation-processor' // Gadgets (Poseidon, Merkle, comparators, ...) implementation 'org.zeroj:zeroj-circuit-lib' // Pure-Java Groth16 prover + setup implementation 'org.zeroj:zeroj-crypto' // Pure-Java verification: verifier, envelopes, snarkjs JSON codec implementation 'org.zeroj:zeroj-verifier-groth16' implementation 'org.zeroj:zeroj-codec' } ``` **Gradle (Kotlin)** ```kotlin title="build.gradle.kts" plugins { java } java { toolchain { languageVersion = JavaLanguageVersion.of(25) } } repositories { mavenCentral() } dependencies { // One BOM for all core ZeroJ modules implementation(platform("org.zeroj:zeroj-bom-core:0.1.0-pre12")) // The BOM must also apply to the annotation processor path annotationProcessor(platform("org.zeroj:zeroj-bom-core:0.1.0-pre12")) // Circuits: @ZKCircuit annotations + the generated *Circuit companions implementation("org.zeroj:zeroj-circuit-annotation-api") annotationProcessor("org.zeroj:zeroj-circuit-annotation-processor") // Gadgets (Poseidon, Merkle, comparators, ...) implementation("org.zeroj:zeroj-circuit-lib") // Pure-Java Groth16 prover + setup implementation("org.zeroj:zeroj-crypto") // Pure-Java verification: verifier, envelopes, snarkjs JSON codec implementation("org.zeroj:zeroj-verifier-groth16") implementation("org.zeroj:zeroj-codec") } ``` **Maven** ```xml title="pom.xml" 25 0.1.0-pre12 org.zeroj zeroj-bom-core ${zeroj.version} pom import org.zeroj zeroj-circuit-annotation-api org.zeroj zeroj-circuit-lib org.zeroj zeroj-crypto org.zeroj zeroj-verifier-groth16 org.zeroj zeroj-codec org.apache.maven.plugins maven-compiler-plugin 3.14.0 org.zeroj zeroj-circuit-annotation-processor ${zeroj.version} ``` > **Note: Why the BOM appears twice in Gradle** > > Gradle resolves the `annotationProcessor` configuration separately from `implementation`, so > a platform declared only on `implementation` doesn't reach the processor. Without the second > `platform(...)` line, `zeroj-circuit-annotation-processor` has no version and resolution fails. > Maven's `annotationProcessorPaths` has the same gap, which is why the processor path above names > `${zeroj.version}` explicitly. > > If you write circuits in test sources, add the same two lines to `testAnnotationProcessor` > as well. `zeroj-codec` is listed explicitly because the verifier doesn't expose it transitively on your compile classpath, and the Quickstart uses its `SnarkjsJsonCodec` class directly. ### Which modules do I need? All modules in this table are managed by `zeroj-bom-core`, so you declare them without a version. | I want to… | Add… | |------------|------| | Write circuits with `@ZKCircuit` annotations | `zeroj-circuit-annotation-api` + `zeroj-circuit-annotation-processor` (processor path) | | Write circuits with the `CircuitSpec` or inline DSL | `zeroj-circuit-dsl` | | Use Poseidon, Merkle proofs, comparators, Blake2b, SHA-512, Ed25519, CIP-1852 gadgets | `zeroj-circuit-lib` | | Run trusted setup and prove with Groth16 (pure Java) | `zeroj-crypto` | | Speed up Groth16 proving with native `blst` | `zeroj-crypto-blst` (opt-in; see [below](#native-blst-acceleration)) | | Verify Groth16 proofs off-chain | `zeroj-verifier-groth16` + `zeroj-codec` | | Work with proof envelopes and verification results | `zeroj-api` (comes in transitively with most modules) | | Route proofs through pluggable verifier backends | `zeroj-backend-spi` | | Verify proofs on-chain in a Plutus V3 validator | `zeroj-onchain-julc` plus JuLC and Cardano Client Lib (see [below](#on-chain-verification-dependencies)) | | Contribute to a Groth16 ceremony from your own code (`ZkeyContributor`) | `zeroj-tools` (also home of the `zeroj-ceremony` CLI) | | Use BLS12-381 field, curve, and pairing primitives directly | `zeroj-bls12381` | For a description of every module, see [Modules](https://zeroj.dev/reference/modules/). ### Opt-in modules outside the BOM Four published modules are deliberately **outside** `zeroj-bom-core`, so they never slip into a dependency graph by accident. Give each one an explicit version: **Gradle (Groovy)** ```groovy dependencies { implementation 'org.zeroj:zeroj-bbs:0.1.0-pre12' // BBS selective-disclosure credentials implementation 'org.zeroj:zeroj-mpf-poseidon:0.1.0-pre12' // Poseidon MPF authenticated state (experimental) implementation 'org.zeroj:zeroj-jmt-poseidon:0.1.0-pre12' // Poseidon JMT authenticated state (experimental) implementation 'org.zeroj:zeroj-verifier-plonk:0.1.0-pre12' // PlonK verification (experimental) } ``` **Gradle (Kotlin)** ```kotlin dependencies { implementation("org.zeroj:zeroj-bbs:0.1.0-pre12") // BBS selective-disclosure credentials implementation("org.zeroj:zeroj-mpf-poseidon:0.1.0-pre12") // Poseidon MPF authenticated state (experimental) implementation("org.zeroj:zeroj-jmt-poseidon:0.1.0-pre12") // Poseidon JMT authenticated state (experimental) implementation("org.zeroj:zeroj-verifier-plonk:0.1.0-pre12") // PlonK verification (experimental) } ``` **Maven** ```xml org.zeroj zeroj-bbs ${zeroj.version} ``` > **Caution: PlonK is experimental** > > ZeroJ's PlonK support, including pure-Java proving, `zeroj-verifier-plonk`, and the on-chain > validators, is experimental. It isn't a recommended alternative to Groth16. Use it only for > evaluation. See [Status & maturity](https://zeroj.dev/start/status/). ### On-chain verification dependencies To compile Plutus V3 validators in Java you also need [JuLC](https://github.com/bloxbean/julc), and to build and submit transactions you need Cardano Client Lib. Both keep their own `com.bloxbean.cardano` group and have their own versions. This block mirrors the builds in [zeroj-usecases](https://github.com/bloxbean/zeroj-usecases): ```groovy title="build.gradle (on-chain additions)" dependencies { implementation 'org.zeroj:zeroj-onchain-julc' // Put ZeroJ's on-chain libraries on the processor path so JuLC can compile validators that use them annotationProcessor 'org.zeroj:zeroj-onchain-julc' // JuLC: write Plutus V3 validators in Java implementation "com.bloxbean.cardano:julc-stdlib:0.1.0-pre16" annotationProcessor "com.bloxbean.cardano:julc-annotation-processor:0.1.0-pre16" implementation "com.bloxbean.cardano:julc-cardano-client-lib:0.1.0-pre16" runtimeOnly "com.bloxbean.cardano:julc-vm-java:0.1.0-pre16" // Cardano Client Lib: build and submit transactions implementation "com.bloxbean.cardano:cardano-client-lib:0.8.0-pre5" implementation "com.bloxbean.cardano:cardano-client-backend-blockfrost:0.8.0-pre5" } ``` The zeroj-usecases builds also fork `javac` with `--enable-native-access=ALL-UNNAMED` for the JuLC annotation processor on Java 25: ```groovy compileJava { options.fork = true options.forkOptions.jvmArgs = ['--enable-native-access=ALL-UNNAMED'] } ``` [Verify your proof on Cardano](https://zeroj.dev/tutorials/verify-on-cardano/) walks through a complete on-chain setup. ### Enable the development trusted setup Groth16 needs a trusted setup before you can prove anything. For local experiments ZeroJ can run a quick single-party setup in-process (`PowersOfTauBLS381.generate`, `Groth16Keys.setupInMemory`, `Groth16SetupBLS381.setup`). That setup is **insecure by design**: the process knows the secret randomness (the "toxic waste") and could forge proofs. So ZeroJ refuses to run it unless you opt in, and the call fails with an `IllegalStateException` that explains why. To opt in for local runs and tests, set the system property `zeroj.allowInsecureTrustedSetup` to `true` or the environment variable `ZEROJ_ALLOW_INSECURE_TRUSTED_SETUP=true`. In code, the property name is available as `TrustedSetupPolicy.ALLOW_INSECURE_TRUSTED_SETUP_PROPERTY`. **Gradle (Groovy)** ```groovy title="build.gradle" plugins { id 'application' // only if you use the run task } application { mainClass = 'com.example.Main' } // Dev/test only: allows the insecure single-party setup tasks.named('test') { systemProperty 'zeroj.allowInsecureTrustedSetup', 'true' } tasks.named('run') { systemProperty 'zeroj.allowInsecureTrustedSetup', 'true' } ``` **Gradle (Kotlin)** ```kotlin title="build.gradle.kts" plugins { application // only if you use the run task } application { mainClass = "com.example.Main" } // Dev/test only: allows the insecure single-party setup tasks.test { systemProperty("zeroj.allowInsecureTrustedSetup", "true") } tasks.named("run") { systemProperty("zeroj.allowInsecureTrustedSetup", "true") } ``` **Maven** ```xml title="pom.xml" org.apache.maven.plugins maven-surefire-plugin true org.codehaus.mojo exec-maven-plugin 3.5.0 com.example.Main zeroj.allowInsecureTrustedSetup true ``` **Plain java** ```bash java -Dzeroj.allowInsecureTrustedSetup=true -cp app.jar com.example.Main # or, for any launcher: export ZEROJ_ALLOW_INSECURE_TRUSTED_SETUP=true ``` > **Caution: Development keys only** > > Keys from the in-process setup must never protect anything of value. Keep the flag in test and > local run configurations only, never in production launch scripts. Real deployments import > proving and verification keys from a multi-party ceremony (a snarkjs `.zkey`). See > [Trusted setup, explained](https://zeroj.dev/learn/trusted-setup/) and > [Running a trusted setup ceremony](https://zeroj.dev/guides/proving/trusted-setup-ceremony/). ### Snapshots Snapshot builds are published on demand to the Maven Central snapshot repository. Their versions embed the short Git commit they were built from, in the form `--SNAPSHOT`. Snapshots aren't published for every commit, so check the repository for the exact version you want. Snapshots are development builds, so prefer a release for anything you share with others. **Gradle (Groovy)** ```groovy repositories { mavenCentral() maven { url = uri('https://central.sonatype.com/repository/maven-snapshots') mavenContent { snapshotsOnly() } } } ``` **Gradle (Kotlin)** ```kotlin repositories { mavenCentral() maven { url = uri("https://central.sonatype.com/repository/maven-snapshots") mavenContent { snapshotsOnly() } } } ``` **Maven** ```xml central-snapshots https://central.sonatype.com/repository/maven-snapshots false true ``` > **Note: Upgrading from 0.1.0-pre11 or earlier?** > > Up to `0.1.0-pre11`, ZeroJ was published as `com.bloxbean.cardano:zeroj-*` with packages under > `com.bloxbean.cardano.zeroj.*`. From `0.1.0-pre12` the group is `org.zeroj` and packages start with > `org.zeroj`. Artifact IDs, class names, and proof/key bytes are unchanged. Cardano Client Lib and > JuLC keep their `com.bloxbean.cardano` coordinates. See [Migration](https://zeroj.dev/reference/migration/). ### Optional tools None of these are needed for the pure-Java path. | Tool | Version | When you need it | |------|---------|------------------| | [Yaci DevKit](https://github.com/bloxbean/yaci-devkit) | latest | Running on-chain verification against a local Cardano devnet | | circom | 2.x | Compiling existing circom circuits you want to prove with ZeroJ | | snarkjs (Node.js) | 0.7.x (ZeroJ's interop CI pins 0.7.6) | Interop only: MPC ceremony tooling, or cross-checking ZeroJ proofs with snarkjs | Install them so they're on your `PATH`. ZeroJ's own interop tests find snarkjs through the `SNARKJS_BIN` environment variable, common npm locations, or `PATH`. See [Bring circom & snarkjs circuits](https://zeroj.dev/tutorials/snarkjs-interop/). ### Native blst acceleration `zeroj-crypto-blst` plugs the native [blst](https://github.com/supranational/blst) library into the Groth16 prover through Java's Foreign Function & Memory API. It produces bit-identical proofs and is purely a performance option; since the large-circuit memory work, the pure-Java prover matches it at large sizes, so measure before you adopt it. The JVM needs native access at runtime: ```bash java --enable-native-access=ALL-UNNAMED ... ``` See [Performance](https://zeroj.dev/guides/proving/performance/) for when it helps. ### GraalVM native image ZeroJ's pure-Java path doesn't call into JNI, so it suits GraalVM. (The `blst-java` jar that `zeroj-verifier-groth16` brings along is only used by its native verifier; test your image with the verifier you actually use.) Several modules ship native-image metadata under `META-INF/native-image/org.zeroj//`, which `native-image` picks up from the classpath automatically: `zeroj-api`, `zeroj-codec`, `zeroj-backend-spi`, `zeroj-verifier-groth16`, `zeroj-verifier-plonk`, `zeroj-bls12381`, `zeroj-blst`, `zeroj-bbs`, and `zeroj-onchain-julc`. The `zeroj-ceremony` CLI is itself distributed as a native binary. Build and test your own native image as part of your pipeline. If `native-image` reports missing reflection or resource configuration for your application classes, the GraalVM tracing agent is the usual way to generate it. ### Next steps - [Quickstart: your first proof](https://zeroj.dev/start/quickstart/) - [Zero-knowledge in plain English](https://zeroj.dev/learn/zero-knowledge-basics/), if the concepts are new - [Configuration reference](https://zeroj.dev/reference/configuration/) for all system properties and flags --- ## Quickstart: your first proof Source: https://zeroj.dev/start/quickstart/ > In about ten minutes, prove you know a secret factor without revealing it, then verify the proof in pure Java. Circuit, witness, setup, prove, verify. Here is the claim you'll prove: *"I know a number `b` such that `3 × b = 33`."* You'll convince a verifier that it's true without ever telling it `b`. It's a toy statement (anyone can divide 33 by 3), but it walks the whole path a real application uses: circuit, witness, trusted setup, proof, and verification. Real statements hide secrets that can't be guessed, such as the preimage of a hash. Everything runs in plain Java, with no native libraries and no external tools. **What you'll build:** a small Java program that defines a circuit, proves it with Groth16 on the BLS12-381 curve, verifies the proof, and shows that a tampered claim is rejected. **What you'll learn:** - how a `@ZKCircuit` class becomes a circuit - what a witness is, and where the secret goes - why Groth16 needs a trusted setup, and why the one here is for development only - what a verifier sees, and what it never sees **Note: Prerequisites:** Java 25 and a Gradle version that runs on it (we tested Gradle 9.2), or Maven. See [Installation](https://zeroj.dev/start/installation/) if you need to set these up. No zero-knowledge background is needed; unfamiliar terms are defined in the [Glossary](https://zeroj.dev/learn/glossary/). ### Build it 1. **Create the project.** Make an empty directory with this layout. You'll write the two Java files in the next steps. - zeroj-quickstart/ - settings.gradle - build.gradle - src/main/java/com/example/quickstart/ - SecretMultiplier.java - Main.java Add the build files. The BOM (`zeroj-bom-core`) pins every ZeroJ module to one version. It appears twice because Gradle resolves the annotation processor path separately. **Gradle** ```groovy title="settings.gradle" rootProject.name = 'zeroj-quickstart' ``` ```groovy title="build.gradle" plugins { id 'application' } repositories { mavenCentral() } java { toolchain { languageVersion = JavaLanguageVersion.of(25) } } dependencies { implementation platform('org.zeroj:zeroj-bom-core:0.1.0-pre12') annotationProcessor platform('org.zeroj:zeroj-bom-core:0.1.0-pre12') // @ZKCircuit annotations + the processor that generates SecretMultiplierCircuit implementation 'org.zeroj:zeroj-circuit-annotation-api' annotationProcessor 'org.zeroj:zeroj-circuit-annotation-processor' implementation 'org.zeroj:zeroj-circuit-dsl' // R1CS compiler + witness calculator implementation 'org.zeroj:zeroj-crypto' // pure-Java Groth16 setup + prover implementation 'org.zeroj:zeroj-codec' // snarkjs JSON parsing implementation 'org.zeroj:zeroj-verifier-groth16' // pure-Java Groth16 verifier } application { mainClass = 'com.example.quickstart.Main' // Dev-only: allows the in-process, single-party trusted setup used in Main. applicationDefaultJvmArgs = ['-Dzeroj.allowInsecureTrustedSetup=true'] } ``` **Maven** With Maven, use this `pom.xml` instead of the two Gradle files. ```xml title="pom.xml" 4.0.0 com.example zeroj-quickstart 1.0-SNAPSHOT 0.1.0-pre12 25 UTF-8 org.zeroj zeroj-bom-core ${zeroj.version} pom import org.zeroj zeroj-circuit-annotation-api org.zeroj zeroj-circuit-dsl org.zeroj zeroj-crypto org.zeroj zeroj-codec org.zeroj zeroj-verifier-groth16 org.apache.maven.plugins maven-compiler-plugin 3.14.0 org.zeroj zeroj-circuit-annotation-processor ${zeroj.version} org.codehaus.mojo exec-maven-plugin 3.5.0 com.example.quickstart.Main ``` The `-Dzeroj.allowInsecureTrustedSetup=true` flag matters. Without it, ZeroJ refuses to run the quick in-process setup this tutorial uses, for reasons you'll see in step 3 of `Main`. 2. **Write the circuit.** A circuit is the statement you want to prove, written as rules the prover's numbers must satisfy. This one says "`a` times `b` equals `product`". ```java title="src/main/java/com/example/quickstart/SecretMultiplier.java" package com.example.quickstart; import org.zeroj.circuit.annotation.Prove; import org.zeroj.circuit.annotation.Public; import org.zeroj.circuit.annotation.Secret; import org.zeroj.circuit.annotation.ZKCircuit; import org.zeroj.circuit.annotation.ZkBool; import org.zeroj.circuit.annotation.ZkContext; import org.zeroj.circuit.annotation.ZkField; /** "I know a secret b such that a × b = product." */ @ZKCircuit(name = "secret-multiplier", version = 1) public class SecretMultiplier { @Prove ZkBool prove(ZkContext zk, @Public ZkField a, @Public ZkField product, @Secret ZkField b) { return a.mul(b).isEqual(product); } } ``` - `@Public` values are shared with the verifier. `@Secret` values stay with the prover. - `ZkField` is a number in the circuit's finite field, not a Java `int`. Its methods (`mul`, `isEqual`, ...) record constraints rather than computing a result right away. - `@Prove` returns a `ZkBool`, and ZeroJ requires it to be true for any valid proof. At compile time the annotation processor reads this class and generates `SecretMultiplierCircuit`, a companion with `build()`, a typed `inputs()` builder, and more. You'll find it under `build/generated/sources/annotationProcessor/` (Gradle) or `target/generated-sources/annotations/` (Maven). 3. **Write the program.** It compiles the circuit, builds the witness, runs a development setup, proves, verifies, and then tries to cheat. ```java title="src/main/java/com/example/quickstart/Main.java" package com.example.quickstart; import org.zeroj.api.CircuitId; import org.zeroj.api.CurveId; import org.zeroj.api.ProofSystemId; import org.zeroj.api.VerificationMaterial; import org.zeroj.api.VerificationResult; import org.zeroj.codec.SnarkjsJsonCodec; import org.zeroj.crypto.groth16.Groth16Keys; import org.zeroj.crypto.groth16.Groth16ProofBLS381; import org.zeroj.crypto.setup.PowersOfTauBLS381; import org.zeroj.crypto.snarkjs.SnarkjsGroth16Json; import org.zeroj.verifier.groth16.bls12381.Groth16BLS12381PureJavaVerifier; import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.util.Arrays; public class Main { public static void main(String[] args) { // 1. Build the circuit (generated from SecretMultiplier) and compile it to R1CS. var circuit = SecretMultiplierCircuit.build(); var r1cs = circuit.compileR1CS(CurveId.BLS12_381); System.out.println("Constraints: " + r1cs.numConstraints() + ", public inputs: " + r1cs.numPublicInputs()); // 2. Fill in the inputs. Only the prover ever knows b. var inputs = SecretMultiplierCircuit.inputs() .a(3) .product(33) .b(11); // the secret BigInteger[] witness = inputs.calculateWitness(circuit, CurveId.BLS12_381); // 3. DEV-ONLY trusted setup: this process knows the toxic waste (tau). BigInteger tau = PowersOfTauBLS381.generate(4).tauScalar(); try (var keys = Groth16Keys.setupInMemory( r1cs.constraints(), r1cs.numWires(), r1cs.numPublicInputs(), tau)) { // 4. Prove. Groth16ProofBLS381 proof = keys.prove(witness, r1cs.constraints()); // 5. Export the three artifacts a verifier needs (snarkjs-compatible JSON). BigInteger[] publicInputs = Arrays.copyOfRange(witness, 1, 1 + r1cs.numPublicInputs()); String vkJson = SnarkjsGroth16Json.verificationKeyJson(keys); String proofJson = SnarkjsGroth16Json.proofJson(proof); String publicJson = SnarkjsGroth16Json.publicJson(publicInputs); System.out.println("Public inputs (a, product): " + inputs.publicValues()); // 6. Verify. The verifier sees the VK, the proof and the public inputs — never b. VerificationResult ok = verify(vkJson, proofJson, publicJson); System.out.println("Proof valid? " + ok.proofValid()); // 7. Tamper with a public input: claim the product is 34 instead of 33. String tampered = SnarkjsGroth16Json.publicJson( new BigInteger[]{BigInteger.valueOf(3), BigInteger.valueOf(34)}); VerificationResult bad = verify(vkJson, proofJson, tampered); System.out.println("Tampered proof valid? " + bad.proofValid() + " (" + bad.message().orElse("") + ")"); } } static VerificationResult verify(String vkJson, String proofJson, String publicJson) { CircuitId id = SecretMultiplierCircuit.circuitId(); var envelope = SnarkjsJsonCodec.toEnvelopeFromJson(proofJson, vkJson, publicJson, id); var material = VerificationMaterial.of(vkJson.getBytes(StandardCharsets.UTF_8), ProofSystemId.GROTH16, CurveId.BLS12_381, id); return new Groth16BLS12381PureJavaVerifier().verify(envelope, material); } } ``` 4. **Run it.** **Gradle** ```bash gradle run # or ./gradlew run if your project has a wrapper ``` **Maven** ```bash mvn -q compile exec:java -Dzeroj.allowInsecureTrustedSetup=true ``` You should see this (build-tool chatter trimmed): ```text Constraints: 6, public inputs: 2 WARNING: Single-party Powers of Tau generation (BLS12-381) — for DEVELOPMENT and TESTING only. Use MPC ceremony outputs (Hermez, Zcash PoT) for production. WARNING: Single-party Groth16 Phase 2 setup (BLS12-381) — for DEVELOPMENT and TESTING only. Use snarkjs multi-party ceremony for production. Public inputs (a, product): [3, 33] Proof valid? true Tampered proof valid? false (Groth16 BLS12-381 pairing check failed) ``` The two `WARNING` lines are ZeroJ reminding you that this setup is for development only. ### What the program does 1. **Compile.** `build()` turns your class into a circuit, and `compileR1CS` flattens it into R1CS: a list of small equations of the form "(something) × (something) = (something)". The one line `a.mul(b).isEqual(product)` becomes 6 of them, because `isEqual` needs a few helper values of its own. 2. **Witness.** The typed `inputs()` builder names every input, so you can't mix up the order. `calculateWitness` then fills in every wire of the circuit, including `b` and the helpers, and checks each constraint on the way. The result is the witness: a `BigInteger[]` whose element `0` is always `1`, followed by the public inputs in declaration order. 3. **Setup.** Groth16 needs keys made from secret randomness called toxic waste (`tau` here). Whoever knows it can forge proofs. That's fine on your laptop and unacceptable anywhere else. 4. **Prove.** `keys.prove` produces a proof: three elliptic-curve points (192 bytes when compressed), whatever the size of the circuit. Proving is randomized, so calling it twice gives two different proofs that both verify. 5. **Export.** The verification key, proof, and public inputs are written in the JSON layout snarkjs uses, so other tools can read them too. 6. **Verify.** The verifier checks a pairing equation over the proof, the key, and the public inputs `[3, 33]`. It never receives `b`. 7. **Tamper.** The same proof with `product = 34` fails. A proof is bound to its exact public inputs. ### What just happened? | You did | The idea behind it | Read more | |---------|--------------------|-----------| | Wrote `a × b = product` as constraints | A circuit is a statement expressed as arithmetic rules | [Circuits, constraints & witnesses](https://zeroj.dev/learn/circuits-and-witnesses/) | | Computed a witness containing `b` | The witness is the prover's secret solution to those rules | [Circuits, constraints & witnesses](https://zeroj.dev/learn/circuits-and-witnesses/) | | Ran `PowersOfTauBLS381` and `setupInMemory` | Groth16 keys come from a one-time setup whose randomness must be destroyed | [Trusted setup, explained](https://zeroj.dev/learn/trusted-setup/) | | Called `keys.prove` | Groth16 makes a short proof that the prover knows a valid witness | [Groth16, PlonK & BBS](https://zeroj.dev/learn/proof-systems/) | | Verified without `b` | The verifier learns the statement is true, and nothing else | [Zero-knowledge in plain English](https://zeroj.dev/learn/zero-knowledge-basics/) | One detail is worth noticing. `VerificationResult` has both `proofValid()` and `accepted()`. The verifier only checked the math, so `proofValid()` is `true` while `accepted()` stays `false`: whether a valid proof should *do* anything, such as grant access or release funds, is a decision for your application. > **Caution: Development setup only** > > The keys from `PowersOfTauBLS381.generate(...)` and `Groth16Keys.setupInMemory(...)` are made by > one process that knows the toxic waste, so anyone holding it could forge proofs. That's why > the setup is disabled unless you pass `-Dzeroj.allowInsecureTrustedSetup=true`. Never use such > keys to protect anything of value. Real deployments use keys from a multi-party ceremony > (a snarkjs `.zkey`), which ZeroJ imports. See > [Running a trusted setup ceremony](https://zeroj.dev/guides/proving/trusted-setup-ceremony/). ### Try this - Change the secret to `.b(12)`. `calculateWitness` throws `ArithmeticException: Constraint violation: ...`, because `3 × 12` isn't `33`. A cheater could skip the witness calculator, but a proof built from a witness that breaks a constraint doesn't verify. [Prove you're over 18](https://zeroj.dev/tutorials/age-check/) shows this in action. - Remove `.b(11)` entirely. You get `IllegalArgumentException: Missing secret input: b`. - Inside the `try` block, call `keys.prove(...)` a second time and compare the two `proofJson` strings. They differ, yet both verify against the same key. ### Next steps - [Prove you're over 18](https://zeroj.dev/tutorials/age-check/): range proofs, invalid witnesses, and keeping the verifier separate from the prover. - [Verify your proof on Cardano](https://zeroj.dev/tutorials/verify-on-cardano/): run this same proof through a Plutus V3 validator. - [Annotations guide](https://zeroj.dev/guides/circuits/annotations/): everything `@ZKCircuit` can do. - [Groth16 guide](https://zeroj.dev/guides/proving/groth16/): key stores, large circuits, and production keys. --- ## Status & maturity Source: https://zeroj.dev/start/status/ > What ZeroJ's Beta, Experimental, Disabled and Assurance-only labels mean, the full support matrix, and what "not for production" means in practice. ZeroJ is **experimental research software**. Some parts are feature-complete and heavily tested; others are opt-in experiments. None of it has been externally audited. This page tells you exactly where each component stands, so you can decide what is reasonable to build on and what to treat as a prototype. As the repository README states, ZeroJ is generated using AI, with human-assisted design, testing, and verification. That's one more reason to treat every component as unaudited until an external review says otherwise. ### What the labels mean | Label | Meaning in ZeroJ | |-------|------------------| | **Beta** | Feature-complete and correctness-tested. The repository has more than 3,500 tests, and the full Groth16 flow is verified end-to-end on-chain against Yaci DevKit. **Not externally audited, and not for value-bearing or mainnet use.** | | **Beta, testnet only** | Beta, and additionally limited to test networks. Nothing of value should depend on it. | | **Beta with caveat** | Beta, with a specific known limitation you must design around. The caveat is named in the matrix. | | **Beta, opt-in** | Beta, but you have to add it explicitly; it's never pulled in by default. | | **Experimental** | Opt-in, may change, and may have known limitations. Use for evaluation and research, not as a foundation. | | **Disabled by default** | Legacy code kept for explicit experiments. It refuses to run unless you set a flag. | | **Assurance only** | Independent implementations used to cross-check ZeroJ in CI. Never published and never a runtime option. | "Beta" is a statement about completeness and testing. It is **not** a security claim. ### Support matrix The matrix below restates the project's support matrix. Groth16 on BLS12-381 is the focus of the current release and the default in every recommendation. #### Circuits and core | Area | Modules | Status | |------|---------|--------| | Core proof model, codecs, verifier SPI and orchestrator | `zeroj-api`, `zeroj-codec`, `zeroj-backend-spi` | **Beta** | | Circuit definition: DSL, symbolic annotations, gadgets | `zeroj-circuit-dsl`, `zeroj-circuit-annotation-api`, `zeroj-circuit-annotation-processor`, `zeroj-circuit-lib` | **Beta**, with per-gadget status (see [Gadgets](https://zeroj.dev/guides/circuits/gadgets/)) | | BLS12-381 pure-Java primitives | `zeroj-bls12381` | **Beta**, verification-grade | #### Groth16 (the default) | Area | Modules | Status | |------|---------|--------| | Groth16 BLS12-381: pure-Java prove and verify | `zeroj-crypto`, `zeroj-verifier-groth16` | **Beta**. Production keys require an external snarkjs MPC ceremony; the in-repo setup is dev-only and flag-gated. | | Groth16 BLS12-381: on-chain (JuLC / Plutus V3) | `zeroj-onchain-julc` | **Beta, testnet only**, not value-bearing. Real validators must bind the proof to `ScriptContext` (see [Verify proofs on Cardano](https://zeroj.dev/guides/verifying/on-chain/#bind-the-proof-to-the-spend)). | | blst native acceleration | `zeroj-blst`, `zeroj-crypto-blst` | **Beta, opt-in**. FFM binding, `libblst` built from source (pinned v0.3.15). | #### PlonK | Area | Modules | Status | |------|---------|--------| | PlonK BLS12-381: pure-Java prove and verify, `.ptau`/`.zkey` import | `zeroj-crypto`, `zeroj-verifier-plonk` | **Experimental** | | PlonK BLS12-381: on-chain (JuLC / Plutus V3) | `zeroj-onchain-julc` | **Experimental**. Labeled testnet trials only. | > **Caution: PlonK is experimental everywhere** > > Treat ZeroJ's PlonK support, off-chain and on-chain, as experimental. It isn't a recommended > alternative to Groth16, and these docs make no correctness or readiness claims for it. #### Credentials | Area | Modules | Status | |------|---------|--------| | BBS (IRTF CFRG draft-10): verification | `zeroj-bbs` | **Beta**. The spec is an IRTF draft, not yet an RFC. | | BBS: issuance and proof generation | `zeroj-bbs` | **Beta with caveat**. The default pure-Java provider is not constant-time; prefer the blst provider for issuer keys. | #### Authenticated state | Area | Modules | Status | |------|---------|--------| | Poseidon authenticated state (MPF, JMT) | `zeroj-mpf-poseidon`, `zeroj-jmt-poseidon` | **Experimental**. High-volume paths are benchmarked (5M-entry local end-to-end runs passed), but production ceremonies, external review, and Yaci/public-network gates remain open. | #### Legacy and assurance | Area | Modules | Status | |------|---------|--------| | BN254 (Groth16 and PlonK, off-chain) | legacy classes | **Disabled by default.** Requires `-Dzeroj.allowLegacyBn254=true`. BN254 is not a Cardano curve. | | BLS12-381 and BBS WASM differential providers | `assurance/zeroj-bls12381-wasm`, `assurance/zeroj-bbs-wasm` | **Assurance only**. Independent zkcrypto and zkryptium oracles, outside the default build and never published. | ### What "not for production" means in practice "Don't use this in production" is easy to say and easy to ignore. Here is what it means concretely for ZeroJ today: - **No external audit.** No third party has reviewed the provers, verifiers, circuits, gadgets, or on-chain validators. Bugs that tests didn't anticipate may exist, including soundness bugs that would let someone forge a proof. - **Development setup is not a ceremony.** The in-process trusted setup (`PowersOfTauBLS381.generate`, `Groth16Keys.setupInMemory`, `Groth16SetupBLS381.setup`) knows its own toxic waste and can forge proofs. It only runs with `-Dzeroj.allowInsecureTrustedSetup=true`. Anything beyond local testing needs keys from a multi-party ceremony. See [Trusted setup, explained](https://zeroj.dev/learn/trusted-setup/). - **On-chain means testnet.** The on-chain Groth16 verifier is tested on Yaci DevKit and is labeled testnet-only. Don't lock real ADA or tokens behind it. - **A valid proof isn't authorization.** The reusable on-chain verifiers only check the math. Your validator must still prevent replay, bind the proof to the transaction, track nullifiers, and enforce who may do what. See [Application security](https://zeroj.dev/guides/verifying/application-security/). - **No constant-time guarantee for Java code.** The BBS pure-Java provider uses fixed-schedule arithmetic for secret scalars, but that is not a full JVM constant-time guarantee. For BBS issuer keys, select the native blst provider. ZeroJ doesn't claim constant-time behavior for its Java code. - **PlonK is experimental,** off-chain and on-chain. - **APIs and coordinates can still change.** ZeroJ is pre-1.0. The move to `org.zeroj` in `0.1.0-pre12` is one example (see [Migration](https://zeroj.dev/reference/migration/)). What you *can* reasonably do today: learn ZK, prototype applications, run the demos, evaluate designs, benchmark, and run testnet experiments that protect nothing of value. ### How ZeroJ gathers evidence The project tries to avoid the classic trap of a library only ever agreeing with itself. Where possible, expected values come from somewhere other than the code under test. | Kind of evidence | Examples in ZeroJ | |------------------|-------------------| | Unit and regression tests | More than 3,500 tests across modules, including invalid-witness, proof-tampering, wrong-public-input, and public-input-order tests | | Official test vectors | IETF RFC 9380 hash-to-curve vectors for BLS12-381; the official CFRG BBS draft-10 fixtures (SHA-256 and SHAKE-256 ciphersuites) | | Differential tests against independent implementations | zkcrypto BLS12-381 and zkryptium BBS compiled to WASM and run as oracles in a dedicated assurance CI job; Poseidon checked against published circomlibjs vectors (BN254) and an independent SageMath implementation of the Poseidon paper spec (BLS12-381) | | Cross-provider equivalence | The same BBS vectors run against the pure-Java, blst, and WASM providers; the blst prover backend is tested for bit-identical Groth16 output against the pure-Java prover | | Interop with other toolchains | A CI job where the pinned snarkjs 0.7.6 CLI verifies ZeroJ-produced proofs and keys, ZeroJ verifies live snarkjs proofs, and tampered proofs must be rejected; PlonK transcript checks against fixtures generated by gnark v0.14.0 | | End-to-end on-chain runs | Groth16 proofs generated in pure Java and verified by a Plutus V3 validator in lock-and-unlock transactions on Yaci DevKit | > **Caution: Passing tests is not security** > > Tests, vectors, differential checks, and a successful on-chain run all raise confidence that ZeroJ > computes what it intends to compute. None of them proves the absence of soundness bugs, > side channels, or protocol-level mistakes in your application. Only careful design and external > review can move a component toward production. This page will change when that happens, not > before. ### Further reading Production gates and remediation status are tracked in [ADR-0026](https://github.com/bloxbean/zeroj/blob/main/docs/adr/0026-production-readiness-review-and-remediation-plan.md). The module split between the stable BOM, opt-in modules, and assurance providers is described in [ADR-0044](https://github.com/bloxbean/zeroj/blob/main/docs/adr/0044-focused-module-surface-and-optional-provider-isolation.md). ### Next steps - [Installation](https://zeroj.dev/start/installation/) - [Quickstart: your first proof](https://zeroj.dev/start/quickstart/) - [Application security](https://zeroj.dev/guides/verifying/application-security/) - [Modules reference](https://zeroj.dev/reference/modules/) --- ## Zero-knowledge in plain English Source: https://zeroj.dev/learn/zero-knowledge-basics/ > What a zero-knowledge proof is, what it convinces you of, what it can't do, and a mental model that maps it onto ordinary Java code. A zero-knowledge proof lets one party convince another that a statement is true without revealing *why* it's true. This page builds the intuition you need before touching any code. There's no math beyond arithmetic, and it takes about five minutes. ### Two roles: prover and verifier Every zero-knowledge system has two roles: - The **prover** knows a secret and wants to convince someone of a fact about it. - The **verifier** wants to be convinced, but should learn nothing about the secret. ```text Prover Verifier ┌──────────────┐ ┌──────────────┐ │ secret: 42 │ ── proof ──────────────► │ accepts or │ │ public: 18 │ ── public inputs (18) ─► │ rejects │ └──────────────┘ └──────────────┘ the secret (42) never leaves the prover ``` In ZeroJ the prover is usually your application or a user's wallet running Java. The verifier is a backend service (verifying in Java) or a Cardano validator (verifying on-chain). ### An analogy: the colour-blind friend You have two balls that look identical to your friend, who is colour-blind. You claim one is red and one is green. How do you convince your friend without telling them which is which? 1. Your friend takes both balls and hides them behind their back. 2. They either swap them or don't, secretly, and show them to you again. 3. You say "swapped" or "not swapped". If the balls really are different colours, you answer correctly every time. If they're secretly the same colour, you can only guess, and you'll be right half the time. After 20 rounds, the chance of a lucky cheater getting every answer right is about one in a million. Look at what your friend learned: that the balls differ. They never learned which one is red. That's the whole idea. The proof convinces without revealing. Real ZK systems replace the balls with math, but the shape is the same. ### What a proof convinces you of A zero-knowledge proof convinces the verifier of a statement of this form: > "I know secret values that, together with these public values, satisfy these rules." The **rules** are fixed in advance and agreed by both sides. In ZeroJ they're a *circuit* you write in Java. The **public values** are visible to everyone. The **secret values** stay with the prover. For example: - *Rules:* `age ≥ threshold`. *Public:* `threshold = 18`. *Secret:* `age = 42`. - *Rules:* `hash(password) = H`. *Public:* `H`. *Secret:* `password`. ### The three properties Every proof system worth using guarantees three things. Here they are in plain terms. | Property | In plain English | In the ball game | |----------|------------------|------------------| | **Completeness** | If the statement is true and the prover is honest, the verifier accepts. | You really can tell the colours apart, so you always answer correctly. | | **Soundness** | If the statement is false, a cheating prover can't make the verifier accept, except with negligible probability. | With identical balls, you can't keep guessing right. | | **Zero-knowledge** | The verifier learns nothing except that the statement is true. | Your friend never finds out which ball is red. | Soundness is the property attackers target. In practice, the most common soundness failures come from **circuits that don't enforce what their authors intended**, not from broken cryptography. [Circuits, constraints & witnesses](https://zeroj.dev/learn/circuits-and-witnesses/) shows how that happens. ### Interactive vs non-interactive The ball game is **interactive**: the verifier has to be there, flipping coins, round after round. That doesn't work for a blockchain, where a validator can't chat with you. **Non-interactive** proofs fix this. The prover produces a single proof object that anyone can check later, with no conversation. The verifier's random challenges come from somewhere both sides can compute instead: - a hash of everything said so far (the *Fiat–Shamir* technique, used by PlonK and BBS), or - a structured setup created in advance (the *trusted setup*, which Groth16 relies on). The proofs ZeroJ generates by default are **zk-SNARKs**: | Letter | Stands for | Meaning | |--------|------------|---------| | S | Succinct | The proof is tiny and quick to check, no matter how big the computation was. A Groth16 proof on BLS12-381 is **192 bytes**. | | N | Non-interactive | One message from prover to verifier. | | AR | ARgument | Soundness holds against computationally bounded cheaters. That's the standard assumption for real-world cryptography. | | K | of Knowledge | The prover must actually *know* a valid secret, not just show that one exists. | ### Public inputs vs secret inputs Deciding what's public and what's secret is the first design decision in any ZK application. | You want to prove… | Secret inputs | Public inputs | |--------------------|---------------|---------------| | I'm at least 18 | your age or birth date | the threshold (18), and usually a commitment or issuer signature that ties your age to something real | | My balance is at least X | your balance | X, and a commitment to your balance | | I'm on the allowlist | your leaf in a Merkle tree and the path to the root | the Merkle root of the list | | I know the password | the password | its hash | | I own this Cardano address | your root key | the address's payment key hash | The verifier sees the public inputs in full. Everything on the secret side is covered by the zero-knowledge property. ### What zero-knowledge does *not* do ZK is powerful, but it's easy to expect too much from it. **It doesn't make data true.** A proof shows that *some* secret satisfies the rules, not that the secret matches reality. If a user can type in any age they like, "I proved I'm over 18" means nothing. Real systems bind secrets to something trustworthy, such as an issuer's signature, a commitment published earlier, or on-chain state. Garbage in, proof of garbage out. **It doesn't authorize anything.** A valid proof is just a fact. Anyone who sees a proof can copy it and submit it again, possibly in their own transaction. Your application has to bind the proof to its context and prevent replay, for example with nullifiers or by including the spender's details in the public inputs. [ZK on Cardano](https://zeroj.dev/learn/zk-on-cardano/) covers this in depth. **It doesn't hide public inputs.** Anything you mark public is visible to the verifier, and on-chain it's visible to everyone, forever. **It doesn't hide low-entropy secrets behind a bare hash.** If the public input is `hash(age)`, anyone can hash 0 through 150 and find your age. Mix in a random salt (a *blinding factor*) when you commit to small values. **It doesn't hide metadata.** Who submitted the proof, when, from which address, which fee they paid, and how often they act can all leak information. ZK hides the witness, not the envelope around it. ### A mental model for Java developers If you remember one thing from this page, make it this. A **circuit** is like a pure function that returns `true` or `false`, whose parameters are split into public and secret: ```java // Conceptually (not ZeroJ code), a circuit is a predicate over public and secret values: static boolean check(int threshold /* public */, int age /* secret */) { return age >= threshold; } ``` A **proof** is a small certificate, 192 bytes for Groth16, that says: > "I ran `check` on these public inputs with *some* secret, and it returned `true`." The verifier never learns the secret, and never has to re-run `check`. Verification cost stays roughly the same whether the circuit has ten constraints or ten million. The prover does the heavy lifting, from well under a second for small circuits to minutes for circuits with millions of constraints. The analogy breaks in one important place. A circuit isn't executed like Java; it's a set of **equations** that the secret values must satisfy. That's why you can't use `if` or `&&` on secrets, and why forgetting an equation creates a security hole. [The next page](https://zeroj.dev/learn/circuits-and-witnesses/) explains what that means. ### Next steps - [Circuits, constraints & witnesses](https://zeroj.dev/learn/circuits-and-witnesses/): how the rules are actually written - [Groth16, PlonK & BBS](https://zeroj.dev/learn/proof-systems/): the proof systems ZeroJ implements - [Glossary](https://zeroj.dev/learn/glossary/): quick definitions of every term used here --- ## Circuits, constraints & witnesses Source: https://zeroj.dev/learn/circuits-and-witnesses/ > How ZK circuits really work (equations over a finite field, R1CS, witnesses), why Java control flow doesn't apply, and how under-constrained circuits break. In [Zero-knowledge in plain English](https://zeroj.dev/learn/zero-knowledge-basics/) we treated a circuit as a function that returns `true` or `false`. That's a useful first picture, but it isn't how circuits work, and the gap between the two is where most ZK security bugs come from. This page closes that gap. ### A circuit is a system of equations A ZK circuit doesn't *run*. It's a list of equations, called **constraints**, over a set of variables, called **wires**. A proof shows that the prover knows values for all the wires that make every equation true at once. Here's the statement "I know `x` such that `x³ + x + 5 = 35`", written as constraints: ```text v1 = x · x constraint 1 v2 = v1 · x constraint 2 out = v2 + x + 5 constraint 3 ``` With `x = 3`, the wires are `v1 = 9`, `v2 = 27`, `out = 35`, and all three equations hold. The verifier never sees `x`, `v1`, or `v2`. It only learns that some assignment satisfies every constraint with `out = 35`. Notice the shape: each constraint does at most **one multiplication**. That's not a style choice. It's the format the proof system understands. ### R1CS: one multiplication per constraint Groth16 consumes circuits in **R1CS** form (rank-1 constraint system). Every constraint looks like: ```text (linear combination A) × (linear combination B) = (linear combination C) ``` A *linear combination* is a sum of wires times constants, like `v2 + x + 5`. Additions are free; they fold into the linear combinations. Multiplications of two wires each cost one constraint. The simplest possible circuit, "I know `b` such that `a · b = product`", is a single constraint: ```text (a) × (b) = (product) ``` When people say a circuit has "54 constraints" or "19 million constraints", they're counting these rows. More constraints mean a bigger proving key and a slower prover. Verification cost doesn't change: Groth16 verification depends on the number of public inputs, not the number of constraints. ### Field arithmetic: numbers that wrap around Circuit wires don't hold Java `int`s or `long`s. They hold **field elements**, which are whole numbers modulo a large prime. For BLS12-381, the curve ZeroJ uses for Cardano, that prime is a 255-bit number called `r`: ```text r = 52435875175126190479447740508185965837690552500527637822603658699938581184513 ``` Think of it as a clock with `r` hours. Arithmetic works like normal until you pass `r`, then it wraps around. That has consequences Java developers don't expect: | Expression | In Java | In the field | |------------|------------------|--------------| | `3 - 5` | `-2` | `r - 2`, a huge positive number. There are no negative numbers. | | `1 / 2` | `0` | `(r + 1) / 2`, the number that gives 1 when doubled. There are no fractions or decimals, only inverses. | | `x > 5` | a comparison | Not defined. Field elements have no built-in order. | | `(r - 1) + 1` | `r` (using `BigInteger`) | `0` | The last two rows are the dangerous ones. To prove "`age ≥ 18`", a circuit can't just compare. It has to prove the numbers are small, by splitting them into bits, and only then compare the bits. Proving that a value fits in `n` bits is called a **range check**, and forgetting one is the most common way to break a circuit. ### The witness: every wire, filled in The **witness** is the complete list of values for every wire in the circuit: the public inputs, the secret inputs, and every intermediate value. For the `x³ + x + 5` example it's `[1, 35, 3, 9, 27]`. In ZeroJ, `calculateWitness(...)` produces it as a `BigInteger[]` with a fixed layout: ```text index 0 the constant 1 (every R1CS uses a "one" wire for constants) index 1..n the n public inputs, in the circuit's declared order index n+1.. secret inputs and intermediate wires ``` The witness contains your secrets, so it **never leaves the prover**. The prover turns it into a proof, and only the proof and the public inputs travel. ### Constraints describe; they don't execute Because a circuit is a set of equations and not a program, ordinary Java control flow doesn't translate. **No `if` on secrets.** An `if` in your Java code runs once, while the circuit is being *built*, before any secret exists. It can't depend on a secret value. Instead, you compute both branches and use a constraint to pick one: ```java // In Java you'd write: fee == (isMember ? 0 : 5) ZkField expected = isMember.select(zk.constant(0), zk.constant(5)); return expected.isEqual(fee); ``` Under the hood, `select` is the equation `result = isMember · (0 − 5) + 5`. It only works as a choice if `isMember` is exactly 0 or 1. **No `&&`, `||`, `!` on secrets.** Same reason. Use `ZkBool.and(...)`, `or(...)`, and `not()`, which emit the equivalent equations. **Booleans need their own constraint.** In a field, a "boolean" is just a number. A value is only 0 or 1 if a constraint says so: `b · (b − 1) = 0`. In the `select` above, a cheater who sets `isMember = 2` would get `expected = 2 · (−5) + 5`, which is not a fee anyone intended. ZeroJ's `ZkBool` adds the boolean constraint for you, so `isMember = 2` is rejected. ### The under-constrained circuit bug Here's the most important idea on this page: > A circuit only enforces the equations you actually wrote down, not the ones you meant. If an intended rule has no constraint behind it, a cheater can pick any value that satisfies the remaining equations, and the proof will verify. This is called an **under-constrained circuit**, and it's the most common serious bug class in ZK applications. Here's a concrete, runnable example. The idea: "prove `age ≥ threshold` by showing `age = threshold + diff`, where `diff` is a non-negative secret". ```java title="BuggyAgeCheck.java" // BUG: nothing says `diff` is small and non-negative. @ZKCircuit(name = "buggy-age-check", version = 1) public class BuggyAgeCheck { @Prove ZkBool prove(@Secret ZkField age, @Secret ZkField diff, @Public ZkField threshold) { return age.isEqual(threshold.add(diff)); } } ``` An honest 25-year-old uses `diff = 7`, and everything looks fine. Tests with honest inputs pass. Now a 16-year-old cheats: ```text age = 16 threshold = 18 diff = r − 2 ("−2" in the field) threshold + diff = 18 + (r − 2) = r + 16 ≡ 16 = age ✓ the equation holds ``` ZeroJ's own witness calculator accepts these inputs, and the resulting proof verifies. The circuit said "equal", and the values *are* equal in the field. It never said "`diff` is a small, non-negative number". The fix is to state the missing rule. In ZeroJ, `ZkUInt` with `@UInt(bits = N)` adds range constraints that force each value into `0 … 2ᴺ − 1`: ```java title="FixedAgeCheck.java" @ZKCircuit(name = "fixed-age-check", version = 1) public class FixedAgeCheck { @Prove ZkBool prove(@Secret @UInt(bits = 8) ZkUInt age, @Secret @UInt(bits = 8) ZkUInt diff, @Public @UInt(bits = 8) ZkUInt threshold) { return age.isEqual(threshold.add(diff)); } } ``` Now `diff = r − 2` doesn't fit in 8 bits, so no valid witness exists. The fixed circuit has 56 constraints instead of 5, and those extra constraints are exactly the rules that were missing. In a real circuit you'd simply write `age.gte(threshold)`, which does the same range-checked comparison for you. ### How ZeroJ helps ZeroJ can't design your circuit for you, but it removes the most common traps: - **Symbolic types.** You write circuits with `ZkField`, `ZkBool`, `ZkUInt`, and friends instead of Java primitives. You can't accidentally branch on a secret, because a `ZkBool` isn't a `boolean`. - **Range checks by declaration.** `@UInt(bits = N)` on a `ZkUInt` input adds its range constraints when the value is created. Comparisons like `gte` and `lt` require range-checked operands. - **Constrained booleans.** Every `ZkBool` is constrained to 0 or 1. - **A gadget library.** Poseidon hashing, Merkle membership, comparators, bit decomposition, and more are written once and reused. For Cardano circuits, use Poseidon with explicit BLS12-381 parameters (`PoseidonParamsBLS12_381T3.INSTANCE`). See [Gadgets](https://zeroj.dev/guides/circuits/gadgets/). - **Witness checking.** `calculateWitness(...)` checks every constraint and throws an `ArithmeticException` when one fails, which makes invalid-witness tests easy to write. A small annotated circuit, and a test that it rejects a false claim, looks like this: ```java title="AgeCheck.java" @ZKCircuit(name = "age-check", version = 1) public class AgeCheck { @Prove ZkBool prove(@Secret @UInt(bits = 8) ZkUInt age, @Public @UInt(bits = 8) ZkUInt threshold) { return age.gte(threshold); } } ``` ```java title="AgeCheckTest.java" var circuit = AgeCheckCircuit.build(); // generated by the annotation processor // Honest witness: 25 ≥ 18 var ok = AgeCheckCircuit.inputs().age(25).threshold(18); circuit.calculateWitness(ok.toWitnessMap(), CurveId.BLS12_381); // Invalid witnesses must be rejected var tooYoung = AgeCheckCircuit.inputs().age(16).threshold(18); assertThrows(ArithmeticException.class, () -> circuit.calculateWitness(tooYoung.toWitnessMap(), CurveId.BLS12_381)); var outOfRange = AgeCheckCircuit.inputs().age(300).threshold(18); // doesn't fit in 8 bits assertThrows(ArithmeticException.class, () -> circuit.calculateWitness(outOfRange.toWitnessMap(), CurveId.BLS12_381)); ``` > **Caution: Honest tests don't prove soundness** > > A circuit that produces the right answer for honest inputs can still be under-constrained. For > every rule your circuit is supposed to enforce, write a test that breaks that rule and check it's > rejected. Keep in mind that `calculateWitness` fills in intermediate wires honestly, while a real > attacker can choose *every* wire value. So also review each intermediate value and ask: "what > stops a prover from putting something else here?" > [Testing circuits](https://zeroj.dev/guides/circuits/testing-circuits/) goes deeper. ### Next steps - [Groth16, PlonK & BBS](https://zeroj.dev/learn/proof-systems/): what happens to your constraints next - [Testing circuits](https://zeroj.dev/guides/circuits/testing-circuits/): invalid-witness and adversarial testing in practice - [Writing circuits with annotations](https://zeroj.dev/guides/circuits/annotations/): the full annotation API --- ## Groth16, PlonK & BBS Source: https://zeroj.dev/learn/proof-systems/ > The three proof systems in ZeroJ, how their setup, proof size and verification cost differ, and why Cardano uses the BLS12-381 curve. ZeroJ implements three cryptographic systems. Two of them, Groth16 and PlonK, prove statements about circuits. The third, BBS, is a signature scheme for credentials. They solve different problems, and in ZeroJ they have very different maturity. This page explains each one, compares them side by side, and explains why everything runs on the BLS12-381 curve. **Short version:** use **Groth16 on BLS12-381** for circuits. It's the focus of the current release and the default throughout these docs. Use **BBS** when you need issuer-signed credentials with selective disclosure. Treat ZeroJ's **PlonK** support as experimental. ### Groth16: the default Groth16 (Jens Groth, 2016) is the most widely deployed zk-SNARK. It turns an R1CS circuit into proofs that are tiny and cheap to verify. - **Proof:** three elliptic-curve points: two in the group G1 and one in G2. On BLS12-381 that's 48 + 96 + 48 = **192 bytes** in compressed form, whatever the size of the circuit. (The same proof as snarkjs-style JSON text is about 1 KB.) - **Verification:** a single *pairing check*, plus one elliptic-curve multiplication per public input. On Cardano, ZeroJ's reusable verifier does four Miller loops and one final check using Plutus V3 builtins. In ZeroJ's JuLC VM tests, a proof with two public inputs used about 2.8 billion CPU units, against a per-transaction limit of 10 billion. - **Setup:** Groth16 needs a **trusted setup for each circuit**. A universal "powers of tau" phase is reused, and a circuit-specific phase 2 runs for every circuit and every change to it. See [Trusted setup, explained](https://zeroj.dev/learn/trusted-setup/). The per-circuit setup is Groth16's main cost. In exchange you get the smallest proofs and the cheapest verification of the three, which is exactly what you want when a blockchain validator pays for every CPU cycle. In ZeroJ, Groth16 on BLS12-381 is **Beta** off-chain and **Beta, testnet only** on-chain. You get a pure-Java prover, a pure-Java verifier, a reusable Plutus V3 verifier, import of snarkjs `.zkey` ceremony keys, and export to snarkjs-compatible JSON. ### PlonK: universal setup (experimental in ZeroJ) PlonK (Gabizon, Williamson, and Ciobotaru, 2019) uses a different circuit format, built from gates and wiring permutations instead of R1CS, and a polynomial commitment scheme called **KZG**. - **Setup:** one **universal** trusted setup (a KZG structured reference string, or SRS) serves every circuit up to a maximum size. Each circuit still needs preprocessing, but that step is public and deterministic, so changing a circuit doesn't need a new ceremony. - **Proof:** larger than Groth16, about 650 bytes (roughly 3.5 times as large). - **Verification:** two pairings plus more scalar multiplications than Groth16. ZeroJ's experimental on-chain verifier measured about 4.8 billion CPU units for one public input. - **Non-interactivity** comes from Fiat–Shamir: the verifier's challenges are derived by hashing the proof transcript, so transcript encoding details are security-critical. > **Caution: PlonK is experimental in ZeroJ** > > ZeroJ includes pure-Java PlonK proving and verification and experimental on-chain validators. > All of it is **experimental**, off-chain and on-chain. These docs make no correctness or > readiness claims for it, and it isn't a recommended alternative to Groth16. Use it only for > evaluation and research. See [PlonK](https://zeroj.dev/guides/proving/plonk/) and [Status & maturity](https://zeroj.dev/start/status/). ### BBS: signatures with selective disclosure BBS works differently from the other two. It isn't a circuit proof system; it's a **signature scheme** with a built-in zero-knowledge proof. The workflow has three parties: ```text Issuer Holder Verifier │ signs attributes │ │ │ [name, birth date, │ │ │ country, KYC level, …] │ │ ├─────── signature ───────►│ │ │ │ derives a proof revealing only │ │ │ [country, KYC level] │ │ ├──────── presentation ─────────────►│ │ │ │ checks against the │ │ │ issuer's public key ``` - The **issuer** signs a list of messages (attributes) with one signature. - The **holder** derives a *presentation*: a zero-knowledge proof that they hold a valid signature over all the attributes, revealing only the ones they choose. - The **verifier** checks the presentation against the issuer's public key. Hidden attributes stay hidden, and each presentation is freshly randomized, so two presentations of the same credential aren't linkable by their bytes. **No trusted setup.** The issuer just generates a key pair. **Proof size** grows with what you hide: a presentation proof is 272 bytes plus 32 bytes per hidden attribute. **Limitation:** BBS can reveal or hide attributes, but it can't prove a *predicate* about a hidden attribute, such as "birth date is before 2007". For predicates, use a circuit (Groth16), possibly alongside BBS. ZeroJ implements the IRTF CFRG draft `draft-irtf-cfrg-bbs-signatures-10` with both of its BLS12-381 ciphersuites (SHA-256 and SHAKE-256), and tests against the draft's official fixtures. The draft isn't an RFC yet, so the scheme itself may still change. In ZeroJ, BBS verification is **Beta**, and issuance and proof generation are **Beta with a caveat**: the default pure-Java provider isn't constant-time, so prefer the blst provider for issuer keys. There is also a fixed-profile on-chain presentation verifier. See [BBS credentials](https://zeroj.dev/guides/credentials/bbs/). ### Side by side | | Groth16 | PlonK | BBS | |---|---------|-------|-----| | What it proves | Any statement you express as an R1CS circuit | Any statement you express as a PlonK circuit | "I hold an issuer-signed credential; here are the attributes I choose to reveal" | | Setup | Trusted, **per circuit** (universal phase 1 + circuit-specific phase 2) | Trusted, **universal** SRS, reused across circuits | **None** beyond the issuer's key pair | | Proof size (BLS12-381) | 192 bytes | about 650 bytes | 272 bytes + 32 per hidden attribute | | Verification cost | Lowest: one pairing check + one scalar multiplication per public input | Higher: two pairings + more scalar multiplications | Pairing-based; grows with the number of attributes | | Changing the circuit | New phase-2 ceremony | Recompute public preprocessing | Not applicable | | Status in ZeroJ | **Beta** off-chain; **Beta, testnet only** on-chain | **Experimental**, off-chain and on-chain | Verification **Beta**; issuance **Beta with caveat**; on-chain verifier is fixed-profile | | When to use | **Default** for every circuit, especially on Cardano | Evaluation and research only | Credentials with selective disclosure | ### Why BLS12-381 and not BN254? Pairing-based proof systems need a **pairing-friendly elliptic curve**. Two curves dominate the ecosystem: - **BN254** (also called alt_bn128) is what Ethereum's precompiles support, so much existing tooling, including default circom and snarkjs setups, targets it. - **BLS12-381** was designed later with a higher security margin. It's used by Zcash, Ethereum's consensus layer, and many signature schemes. For Cardano the choice is simple. Plutus V3, introduced in the Conway era, added **native BLS12-381 builtins**, specified in CIP-0381: curve point operations, Miller loops, and a final pairing check. There are no BN254 builtins, so a BN254 proof can't be verified on-chain at any reasonable cost. That's why ZeroJ uses BLS12-381 everywhere by default. Circuits compile over the BLS12-381 scalar field (`CurveId.BLS12_381`), and hash gadgets must use BLS12-381 parameters, such as Poseidon with `PoseidonParamsBLS12_381T3.INSTANCE`. ZeroJ still contains legacy BN254 code, but it's **disabled by default** and needs `-Dzeroj.allowLegacyBn254=true`. Use it only for off-chain experiments. > **Note: Bringing circom circuits** > > circom and snarkjs default to BN254. To use a circom circuit with ZeroJ and Cardano, compile and > set it up for BLS12-381 (for example, `circom circuit.circom --r1cs --wasm -p bls12381`). See > [Bring circom & snarkjs circuits](https://zeroj.dev/tutorials/snarkjs-interop/). ### Which should I use? - **Proving a statement about private data?** Groth16 on BLS12-381. Plan for a proper ceremony before anything real depends on it. - **Issuing credentials that holders reveal piece by piece?** BBS. - **Need a predicate over a credential attribute?** A Groth16 circuit, possibly combined with BBS or another way of binding the attribute to an issuer. - **Curious about universal setups?** Try PlonK for research, and keep it away from anything that matters. ### Next steps - [Trusted setup, explained](https://zeroj.dev/learn/trusted-setup/) - [ZK on Cardano](https://zeroj.dev/learn/zk-on-cardano/) - [Groth16 proving guide](https://zeroj.dev/guides/proving/groth16/) - [BBS credentials](https://zeroj.dev/guides/credentials/bbs/) --- ## Trusted setup, explained Source: https://zeroj.dev/learn/trusted-setup/ > What a trusted setup is, why its "toxic waste" can forge proofs, how multi-party ceremonies remove the risk, and what ZeroJ provides for dev and production. Before anyone can prove or verify a Groth16 statement, somebody has to generate the keys: a **proving key** for provers and a **verification key** for verifiers. That generation step is called the **trusted setup**, and it's the part of ZK most likely to be done wrong in a real deployment. This page explains what's being trusted, why, and how to do it properly. ### The idea in one analogy Imagine a factory that makes tamper-evident seals. To set up the production line, the factory casts a unique mould. Seals made from the mould are public, and anyone can inspect them. Once the line is set up, the mould is supposed to be smashed. If someone secretly keeps the mould, they can stamp out seals that pass every inspection while sealing nothing at all. A trusted setup works the same way. Generating the keys requires secret random numbers. The best known is called **tau (τ)**, and Groth16 adds a few companions of its own. The public keys are derived from these secrets, and afterwards the secrets must be destroyed. Anyone who keeps them can **forge proofs**: create proofs that verify for statements that are false. That's why these secrets are nicknamed **toxic waste**. Two properties make this worse than it sounds: - **Forgeries are undetectable.** A forged proof is mathematically indistinguishable from an honest one. No verifier, on-chain or off-chain, can tell them apart. - **The damage is silent and permanent.** If the toxic waste leaks, every proof ever verified with those keys becomes suspect, and you'd never know it happened. Knowing the toxic waste doesn't, by itself, reveal anyone's secret inputs from their proofs. The risk is to **soundness**: fake proofs, such as an under-18 user "proving" they're over 18, or someone unlocking funds without a valid witness. ### Powers of tau What the setup actually publishes is a long list of elliptic-curve points built from powers of τ: ```text [1]G1, [τ]G1, [τ²]G1, [τ³]G1, …, [τⁿ]G1 [1]G2, [τ]G2, … ``` `[x]G1` means "the curve's generator point G1, multiplied by x". Because of how elliptic curves work, you can publish `[τ]G1` without revealing τ: going backwards is the hard problem the whole curve's security rests on. This list is called the **powers of tau**, or a **structured reference string (SRS)**. It's stored in a `.ptau` file. Its length sets the maximum circuit size it can support; a setup with 2²⁰ powers supports circuits up to roughly that many constraints. ### Phase 1 and phase 2 Groth16 setup happens in two phases: ```text Phase 1 (universal) Powers of tau for BLS12-381, up to size 2ⁿ once per curve + size ─ reusable by every circuit that fits │ ▼ Phase 2 (circuit-specific) Powers of tau + your compiled circuit (R1CS) once per circuit ─ produces this circuit's proving key + verification key (a snarkjs .zkey file) ``` - **Phase 1** doesn't depend on any circuit. You can reuse a large public ceremony. ZeroJ's ceremony runbook, for example, uses an existing attested BLS12-381 ceremony as its primary phase-1 source. - **Phase 2** is specific to one circuit and introduces **its own toxic waste**. Any change to the circuit, such as a new constraint, a different number or order of public inputs, or a changed circuit parameter, means a new phase 2 and new keys. Changing input *values* never does; a new user or a new proof uses the same keys. ### Multi-party ceremonies: one honest participant is enough If one party generates τ, everyone has to trust that party. A **multi-party computation (MPC) ceremony** removes that single point of trust: ```text key_0 ──► Alice mixes in her randomness ──► key_1 key_1 ──► Bob mixes in his randomness ──► key_2 key_2 ──► Carol mixes in her randomness ──► key_3 key_3 ──► public random beacon ──► key_final ``` Each participant takes the previous file, mixes in fresh secret randomness, publishes the result, and destroys their randomness. The final secret is effectively the product of every participant's contribution. To forge proofs you would need **all** of those contributions. So the keys are safe as long as **at least one** participant was honest and really destroyed their contribution. This is the "1-of-N" trust model. Each contribution is publicly checkable. Anyone can re-verify the whole chain, for example with `snarkjs zkey verify`, without trusting the coordinator. A final **random beacon**, a public value nobody can predict in advance such as a future block hash, closes the ceremony so the last contributor can't bias the result. ### PlonK's universal setup PlonK only needs phase 1. One trusted SRS serves every PlonK circuit up to its size, and each circuit's keys are derived from it with a public, deterministic computation, with no new toxic waste per circuit. The SRS still has to come from a trustworthy ceremony, and if its τ leaks, every circuit using it is affected at once. ZeroJ's PlonK support is experimental; see [Groth16, PlonK & BBS](https://zeroj.dev/learn/proof-systems/). ### What ZeroJ provides | Need | What ZeroJ offers | |------|-------------------| | Fast local setup for tests and demos | `PowersOfTauBLS381.generate(...)`, `Groth16Keys.setupInMemory(...)`, `Groth16SetupBLS381.setup(...)`: single-party, in-process, **insecure by design**, and disabled unless you opt in | | Using a real phase 1 | `PtauImporterBLS381` imports snarkjs `.ptau` files | | Using real Groth16 keys | `ZkeyImporterBLS381` imports a ceremony `.zkey`; `Groth16PkStore` and `ZkeyPkStoreImporter` stream large keys into an `mmap`-loaded store | | Running or joining a ceremony | The `zeroj-ceremony` CLI (`export-r1cs`, `contribute`, `finalize`) and the `ZkeyContributor` library in `zeroj-tools`, producing snarkjs-compatible contributions | | Independent verification | Contributions made with ZeroJ verify with `snarkjs zkey verify`, so nobody has to trust ZeroJ's own tool | #### The development opt-in The in-process setup refuses to run unless you explicitly allow it: ```text JVM system property: -Dzeroj.allowInsecureTrustedSetup=true or environment: ZEROJ_ALLOW_INSECURE_TRUSTED_SETUP=true ``` Without the opt-in, the call throws an `IllegalStateException` explaining that the generator knows the toxic waste and can forge proofs. [Installation](https://zeroj.dev/start/installation/) shows how to set the flag for Gradle and Maven test and run tasks. #### The production path For anything beyond local testing, the flow is: ```text 1. Compile your circuit and export it: zeroj-ceremony export-r1cs → circuit.r1cs 2. Start phase 2 from a trusted .ptau: snarkjs groth16 setup → key_0000.zkey 3. Contributors mix in randomness: zeroj-ceremony contribute (or snarkjs zkey contribute) 4. Close with a pre-announced beacon, and let anyone re-check: snarkjs zkey verify 5. Import the final key into ZeroJ: zeroj-ceremony finalize → proving-key store 6. Pin the verification key (and, on Cardano, the resulting script hash). ``` [Running a trusted setup ceremony](https://zeroj.dev/guides/proving/trusted-setup-ceremony/) walks through every step. > **Danger: Never protect real value with development keys** > > Keys from `PowersOfTauBLS381.generate`, `Groth16Keys.setupInMemory`, or `Groth16SetupBLS381.setup` > were made by a process that knew the toxic waste. Anyone with access to that process, its memory, > or any file that captured the secret could forge proofs, for example to unlock every UTxO guarded > by your validator. > Use them only in tests and local demos. Real deployments need keys from a multi-party ceremony, > with every artifact hash pinned and published. ### What needs a new setup? | Change | New setup? | |--------|-----------| | New witness values, new users, new proofs | No | | Different public input *values* | No | | Any change to the constraints | **Yes**, a new phase 2 (Groth16) | | Different number or order of public inputs | **Yes** | | A different circuit parameter (for example Merkle depth) | **Yes**. It's a different circuit. | | Circuit grows beyond the SRS size | **Yes**, and a larger phase 1 too | Setup is expensive, so run it once per circuit version and cache the results. Never re-run it per proof. ### Next steps - [ZK on Cardano](https://zeroj.dev/learn/zk-on-cardano/) - [Running a trusted setup ceremony](https://zeroj.dev/guides/proving/trusted-setup-ceremony/) - [Groth16 proving guide](https://zeroj.dev/guides/proving/groth16/) Design notes: [ADR-0031, Groth16 MPC trusted-setup ceremony](https://github.com/bloxbean/zeroj/blob/main/docs/adr/0031-groth16-mpc-trusted-setup-ceremony.md). --- ## ZK on Cardano Source: https://zeroj.dev/learn/zk-on-cardano/ > How Cardano verifies ZK proofs with Plutus V3 BLS12-381 builtins, how proofs map onto eUTxO, what it costs, and why a valid proof is not authorization. Cardano can check a Groth16 proof inside a smart contract. This page explains how that works, how a proof fits into Cardano's eUTxO model, what verification costs, and the most important lesson of all: **a validator that only checks the proof is not secure.** ### What Cardano gives you: BLS12-381 builtins Verifying a pairing-based proof means doing elliptic-curve math, which would be far too expensive to write by hand in a smart contract. Plutus V3, introduced in the Conway era, added **native BLS12-381 builtins**, specified in CIP-0381. They cover curve point arithmetic, compression and decompression, hashing to the curve, and the two halves of a pairing check: the **Miller loop** and the **final verification**. That's why ZeroJ targets BLS12-381: it's the only pairing-friendly curve Cardano can verify on-chain. (See [Groth16, PlonK & BBS](https://zeroj.dev/learn/proof-systems/) for the comparison with BN254.) ZeroJ's on-chain Groth16 verifier is Java code compiled to Plutus V3 by [JuLC](https://github.com/bloxbean/julc). When it runs, it: 1. reads the public inputs, 2. decompresses the proof points (A, C in G1; B in G2), 3. combines the public inputs with the verification key: one scalar multiplication per input, 4. runs four Miller loops and one final check to confirm the Groth16 pairing equation. ### How a proof fits into eUTxO On Cardano, funds and state live in **UTxOs** (unspent transaction outputs). A UTxO locked at a script address can only be spent by a transaction that the script, called a **validator**, approves. The validator sees three things: the UTxO's **datum**, the spender's **redeemer**, and the **ScriptContext**, a description of the whole transaction. ZK verification maps onto that model naturally: | eUTxO piece | Role in ZK verification | |-------------|-------------------------| | Validator script | The verifier. The **verification key is baked in** as script parameters at deploy time, so each VK produces a different script hash and address. | | Datum | The **public inputs**, in ZeroJ's reusable verifier. Your own validator may instead derive them from the transaction or from on-chain state. | | Redeemer | The **proof**: A, B, and C as compressed BLS12-381 points, 192 bytes in total. | | ScriptContext | The transaction being validated. This is where **your application rules** live. | The typical flow: ```text Lock Alice pays ADA to the verifier script address, with the public inputs as datum. Spend Bob builds a transaction that spends that UTxO. Redeemer = his proof. The validator checks the proof against the datum using Plutus V3 BLS12-381 builtins (and, in a real application, checks the transaction against its policy). Valid → the transaction succeeds. Invalid → the whole transaction fails. ``` [Verify your proof on Cardano](https://zeroj.dev/tutorials/verify-on-cardano/) runs exactly this flow on a local Yaci DevKit network. ### What verification costs Every Plutus script execution has a budget of CPU and memory units, with a per-transaction maximum. Groth16 is the cheapest pairing-based option because its verification work is fixed, apart from one extra scalar multiplication per public input. In ZeroJ's JuLC VM tests, the reusable Groth16 verifier, run as a spending validator with a full `ScriptContext`, measured: | Public inputs | CPU units | Memory units | |---------------|-----------|--------------| | 1 | about 2.63 billion | about 178,000 | | 2 | about 2.82 billion | about 212,000 | | 3 | about 3.02 billion | about 246,000 | That's roughly a quarter to a third of the per-transaction CPU limit of 10 billion units under current protocol parameters, leaving room for your own validation logic. Each extra public input adds about 0.2 billion CPU units, and transaction fees scale with the units used, so keep public inputs to what the validator actually needs. Measure your own validator too: its policy checks add cost, and a JuLC compiler upgrade can shift the numbers. `zeroj-onchain-julc` includes two planning helpers: `ScriptBudgetEstimator` estimates CPU and memory for a proof system, curve, and public-input count, and `OnChainFeasibility` reports which combinations are practical on Plutus V3. Script size matters too, since the verification key lives in the script. Deploying the validator once as a **reference script** (CIP-0033) avoids attaching it to every transaction. ### Verifying off-chain instead You don't always need the chain to check the math. If your application only needs a **tamper-evident record**, you can verify the proof in Java (in a backend, an indexer, or a client), then record a hash of the proof and its public inputs, or a new state root, in transaction metadata using Cardano Client Lib. | | On-chain verification | Off-chain verification + anchoring | |---|------------------------|-------------------------------------| | Who checks the proof | Every Cardano node, as part of consensus | Your verifier, plus anyone who re-checks later | | Can an invalid proof move funds? | No; the transaction fails | The chain doesn't check the proof at all, so your off-chain logic must | | Cost | Plutus execution units, roughly 2.6 to 3 billion CPU units per proof | Only metadata bytes | | Good for | Unlocking funds, minting, state transitions enforced by the ledger | Audit trails, attestations, batch results, off-chain protocols | ZeroJ's current release doesn't ship an anchoring helper. You write the metadata yourself with Cardano Client Lib, choosing what to commit to. ### Proof validity is not authorization This is the part that separates a demo from a real application. A reusable verifier such as `Groth16BLS12381Verifier` (the on-chain validator in `zeroj-onchain-julc`) checks exactly one thing: > *Some* witness satisfies *this* circuit for *these* public inputs. It does **not** check who is spending, where the money goes, whether this proof was used before, or whether the public inputs match your application's current state. On its own, it's safe for tests and demos, and nothing else. Here's what goes wrong if you deploy it as-is: - **Replay.** Proofs are public once they're on-chain. If two UTxOs sit at the same script with the same datum, one proof unlocks both. Anyone can copy it. - **Front-running.** Your unlock transaction is visible before it's confirmed. Someone can copy your proof into their own transaction that pays *them*, because nothing in the proof names the recipient. - **Double use.** A voter proves "I'm on the voter list" and votes twice. Both proofs are valid. - **Stale or wrong context.** A proof of membership in an old Merkle root still verifies, unless the validator checks the root against current state. The defenses all have the same shape: **bind the proof's public inputs to the transaction and the application state, then have the validator enforce that binding.** **Bind to the UTxO being spent.** Make the first public input a value derived from the output being spent: ```text first public input = blake2b_256( spentTxId ‖ spentOutputIndex as 32 bytes ) mod r ``` (`r` is the BLS12-381 scalar field order.) The prover computes this value for the UTxO it is about to spend and proves with it, and the validator recomputes it from `ScriptContext`. A proof made for one UTxO is then useless for any other. The validator must compute the value itself rather than read it from the locked UTxO's datum, because a datum can't contain a hash of its own transaction. ZeroJ's `Groth16BLS12381TxOutRefBindingVerifier` demonstrates the check but reads it from the datum, so treat it as a reference, not a lock. [Verify your proof on Cardano](https://zeroj.dev/tutorials/verify-on-cardano/) builds a validator that does this correctly. **Bind to the recipient and outputs.** Make the recipient's key hash, or a commitment to the intended outputs, a public input, and have the validator check the transaction pays exactly that. A copied proof then only ever pays the original recipient. **Use nullifiers for one-time actions.** A nullifier is a public value derived from the prover's secret, for example `Poseidon(secret, electionId)`. It's always the same for the same secret and scope, but reveals nothing about the secret. The validator records used nullifiers in on-chain state and rejects repeats, which stops double voting and double claiming. ZeroJ doesn't provide a generic nullifier registry; your application designs and stores it. The [private allowlist tutorial](https://zeroj.dev/tutorials/private-allowlist/) shows a nullifier in practice. **Check public inputs against state.** If the proof says "I'm in the tree with root R", the validator must confirm that R is the current root, for example from a datum or reference input. **Enforce everything else through ScriptContext.** Required signatures, validity intervals, continuing outputs, value conservation, and minting rules are ordinary validator logic. ZK doesn't replace any of it. In practice you write your own validator: compose ZeroJ's on-chain library (`Groth16BLS12381Lib`) for the proof check and add your policy around it. [On-chain verification](https://zeroj.dev/guides/verifying/on-chain/) shows how. > **Danger: Testnet only** > > ZeroJ's on-chain Groth16 verifier is **Beta, testnet only**, and not externally audited. Don't > lock real value behind it. Even after an audit, a validator is only as safe as its application > policy and its trusted setup. ### Next steps - [Quickstart: your first proof](https://zeroj.dev/start/quickstart/): if you haven't run any code yet, start here - [Verify your proof on Cardano](https://zeroj.dev/tutorials/verify-on-cardano/): hands-on with Yaci DevKit - [On-chain verification](https://zeroj.dev/guides/verifying/on-chain/): building your own validator - [Application security](https://zeroj.dev/guides/verifying/application-security/): replay, nullifiers, and binding in depth - [Private voting](https://zeroj.dev/use-cases/private-voting/): a complete design that puts these ideas together --- ## Glossary Source: https://zeroj.dev/learn/glossary/ > Short, plain-English definitions of the zero-knowledge, cryptography and Cardano terms used throughout the ZeroJ docs. Quick definitions of the terms you'll meet in these docs, in alphabetical order. Each entry links to the page that explains the idea properly. ### A–B **Arithmetic circuit.** A computation expressed as additions and multiplications over a finite field, written as a list of constraints. It's what "circuit" means in ZK; there are no wires or gates in the electronic sense. See [Circuits, constraints & witnesses](https://zeroj.dev/learn/circuits-and-witnesses/). **BBS.** A signature scheme where an issuer signs a list of attributes and the holder later proves possession of the signature while revealing only chosen attributes. ZeroJ implements IRTF CFRG draft-10. See [Groth16, PlonK & BBS](https://zeroj.dev/learn/proof-systems/) and [BBS credentials](https://zeroj.dev/guides/credentials/bbs/). **Blinding.** Randomness mixed into a proof or commitment so it reveals nothing about the secret. ZeroJ's Groth16 prover always blinds proofs, so two proofs of the same statement look unrelated. In commitments, a random salt stops low-entropy values like an age from being guessed. See [Zero-knowledge in plain English](https://zeroj.dev/learn/zero-knowledge-basics/). **BLS12-381.** The pairing-friendly elliptic curve ZeroJ uses by default, and the only one Cardano can verify on-chain, through Plutus V3 builtins. See [Groth16, PlonK & BBS](https://zeroj.dev/learn/proof-systems/). **BN254.** Another pairing-friendly curve, popular on Ethereum. It isn't a Cardano curve. ZeroJ's BN254 code is legacy and disabled by default (`-Dzeroj.allowLegacyBn254=true`). See [Groth16, PlonK & BBS](https://zeroj.dev/learn/proof-systems/). ### C **CIP-0381.** The Cardano Improvement Proposal that added BLS12-381 curve operations and pairings to Plutus. It's what makes on-chain proof verification affordable. See [ZK on Cardano](https://zeroj.dev/learn/zk-on-cardano/). **Circuit.** The rules a proof is about: a fixed set of constraints over public and secret inputs. In ZeroJ you write circuits in Java, usually as a class annotated with `@ZKCircuit`. See [Circuits, constraints & witnesses](https://zeroj.dev/learn/circuits-and-witnesses/). **Commitment.** A value that binds you to a secret without revealing it, like a sealed envelope. In circuits it's often a hash such as Poseidon(value, salt). You can later prove statements about the committed value. See [Zero-knowledge in plain English](https://zeroj.dev/learn/zero-knowledge-basics/). **Completeness.** The guarantee that an honest prover with a true statement always convinces the verifier. See [Zero-knowledge in plain English](https://zeroj.dev/learn/zero-knowledge-basics/). **Constraint.** One equation in a circuit. In R1CS, each constraint has the form A × B = C, where A, B, and C are sums of wires times constants. See [Circuits, constraints & witnesses](https://zeroj.dev/learn/circuits-and-witnesses/). **CRS / SRS.** Common (or structured) reference string: public parameters produced by a trusted setup and used by both prover and verifier. The powers of tau are an SRS. See [Trusted setup, explained](https://zeroj.dev/learn/trusted-setup/). **Curve point (G1, G2).** An element of one of the elliptic-curve groups used by pairing-based proofs. On BLS12-381, a compressed G1 point is 48 bytes and a G2 point is 96 bytes. A Groth16 proof is two G1 points and one G2 point. See [Groth16, PlonK & BBS](https://zeroj.dev/learn/proof-systems/). ### D–F **Datum.** Data attached to a UTxO locked at a script address. ZeroJ's reusable on-chain verifier reads the public inputs from it. See [ZK on Cardano](https://zeroj.dev/learn/zk-on-cardano/). **eUTxO.** Cardano's extended unspent-transaction-output model: funds and state live in outputs that validators guard, with a datum, a redeemer, and a ScriptContext. See [ZK on Cardano](https://zeroj.dev/learn/zk-on-cardano/). **Field element.** A whole number modulo a large prime. For BLS12-381 the circuit field's prime, `r`, is 255 bits. Field arithmetic wraps around and has no negatives or fractions. See [Circuits, constraints & witnesses](https://zeroj.dev/learn/circuits-and-witnesses/). **Fiat–Shamir.** A technique that makes an interactive proof non-interactive by deriving the verifier's challenges from a hash of the transcript. PlonK and BBS use it, so their transcript encoding is security-critical. See [Zero-knowledge in plain English](https://zeroj.dev/learn/zero-knowledge-basics/). ### G–K **Gadget.** A reusable piece of circuit, such as a Poseidon hash, a Merkle membership check, or a comparator. ZeroJ's gadgets live in `zeroj-circuit-lib`. See [Gadgets](https://zeroj.dev/guides/circuits/gadgets/). **Groth16.** The zk-SNARK ZeroJ uses by default: 192-byte proofs on BLS12-381, the cheapest verification, and a trusted setup per circuit. See [Groth16, PlonK & BBS](https://zeroj.dev/learn/proof-systems/). **JuLC.** A compiler that turns Java code into Plutus V3 validators for Cardano. ZeroJ's on-chain verifiers are written with it. See [ZK on Cardano](https://zeroj.dev/learn/zk-on-cardano/). **KZG.** The Kate–Zaverucha–Goldberg polynomial commitment scheme. It lets a prover commit to a polynomial and later prove its value at a point. PlonK uses it with a universal SRS. See [Groth16, PlonK & BBS](https://zeroj.dev/learn/proof-systems/). ### M–N **Merkle tree.** A tree of hashes whose single root commits to an entire list. A Merkle proof shows an item is in the list using one sibling hash per level. In a circuit, the item and its path can stay secret while only the root is public. See [Private allowlist](https://zeroj.dev/tutorials/private-allowlist/). **MPC ceremony.** A multi-party trusted setup where each participant mixes in secret randomness and destroys it. The resulting keys are safe as long as at least one participant was honest. See [Trusted setup, explained](https://zeroj.dev/learn/trusted-setup/). **Nullifier.** A public value derived from a secret and a scope, for example Poseidon(secret, electionId). It's the same every time the same secret is used in the same scope, so a validator can reject repeats without learning the secret. See [ZK on Cardano](https://zeroj.dev/learn/zk-on-cardano/). ### P **Pairing.** A special function that maps a G1 point and a G2 point to a third group, such that e(aP, bQ) = e(P, Q)ᵃᵇ. It lets a verifier check multiplication relationships between hidden values. Groth16, PlonK, and BBS verification all rely on pairings. See [Groth16, PlonK & BBS](https://zeroj.dev/learn/proof-systems/). **PlonK.** A zk-SNARK with a universal trusted setup, based on KZG commitments. In ZeroJ it's **experimental**, off-chain and on-chain. See [Groth16, PlonK & BBS](https://zeroj.dev/learn/proof-systems/). **Poseidon.** A hash function designed to be cheap inside circuits, needing far fewer constraints than SHA-256. For Cardano circuits, always pass explicit BLS12-381 parameters: `PoseidonParamsBLS12_381T3.INSTANCE`. See [Gadgets](https://zeroj.dev/guides/circuits/gadgets/). **Powers of tau.** The universal phase-1 setup output: curve points built from powers of a secret τ. It's stored in a `.ptau` file and reusable by every circuit up to its size. See [Trusted setup, explained](https://zeroj.dev/learn/trusted-setup/). **Proof.** The short object a prover sends to convince a verifier. A Groth16 proof on BLS12-381 is 192 bytes. See [Zero-knowledge in plain English](https://zeroj.dev/learn/zero-knowledge-basics/). **Proof envelope.** ZeroJ's container for a proof together with its proof system, curve, circuit ID, public inputs, and a verification-key reference (`ZkProofEnvelope`). See [Off-chain verification](https://zeroj.dev/guides/verifying/off-chain/). **Proving key.** The key a prover uses to create proofs for one circuit. It isn't secret, but it must come from a trustworthy setup. For very large circuits it can be many gigabytes, so ZeroJ can `mmap` it from disk. See [Trusted setup, explained](https://zeroj.dev/learn/trusted-setup/). **Public input.** A value both prover and verifier see, such as a threshold, a Merkle root, or a hash. On-chain, public inputs are visible to everyone. See [Zero-knowledge in plain English](https://zeroj.dev/learn/zero-knowledge-basics/). ### R **R1CS.** Rank-1 constraint system: the circuit format Groth16 uses, where every constraint is (linear combination) × (linear combination) = (linear combination). See [Circuits, constraints & witnesses](https://zeroj.dev/learn/circuits-and-witnesses/). **Range check.** Constraints proving a value fits in N bits, usually by decomposing it into boolean bits. Without range checks, field wrap-around lets cheaters pass comparisons. In ZeroJ, `@UInt(bits = N)` adds them. See [Circuits, constraints & witnesses](https://zeroj.dev/learn/circuits-and-witnesses/). **Redeemer.** Data supplied by the transaction that spends a script UTxO. In ZK validators it carries the proof. See [ZK on Cardano](https://zeroj.dev/learn/zk-on-cardano/). ### S **ScriptContext.** The validator's view of the whole transaction: inputs, outputs, signatures, validity range, and more. Binding proofs to it is what prevents replay and theft. See [ZK on Cardano](https://zeroj.dev/learn/zk-on-cardano/). **Secret input (private input).** A value only the prover knows. It's part of the witness and never leaves the prover. ZeroJ marks these with `@Secret`. See [Zero-knowledge in plain English](https://zeroj.dev/learn/zero-knowledge-basics/). **Selective disclosure.** Revealing some attributes of a signed credential while proving the rest are validly signed but hidden. BBS provides it natively. See [Selective disclosure](https://zeroj.dev/use-cases/selective-disclosure/). **SNARK.** Succinct Non-interactive ARgument of Knowledge: a short proof, checked quickly, sent in one message, showing the prover knows a valid witness. A zk-SNARK is also zero-knowledge. See [Zero-knowledge in plain English](https://zeroj.dev/learn/zero-knowledge-basics/). **snarkjs / circom.** A widely used JavaScript proving toolkit and its companion circuit language. ZeroJ imports their `.ptau`, `.zkey`, and `.r1cs` artifacts and exports snarkjs-compatible JSON. See [Bring circom & snarkjs circuits](https://zeroj.dev/tutorials/snarkjs-interop/). **Soundness.** The guarantee that a false statement can't be proven, except with negligible probability. Under-constrained circuits and leaked toxic waste both break it. See [Zero-knowledge in plain English](https://zeroj.dev/learn/zero-knowledge-basics/). ### T–U **Toxic waste.** The secret randomness used in a trusted setup. Anyone who keeps it can forge proofs. See [Trusted setup, explained](https://zeroj.dev/learn/trusted-setup/). **Trusted setup.** The one-time generation of proving and verification keys from secret randomness. ZeroJ's in-process setup is development-only and needs `-Dzeroj.allowInsecureTrustedSetup=true`; production keys come from an MPC ceremony. See [Trusted setup, explained](https://zeroj.dev/learn/trusted-setup/). **Under-constrained circuit.** A circuit missing a constraint its author intended, so a cheater can choose values that satisfy the remaining equations and produce a valid proof of a false statement. This is the most common serious ZK bug. See [Circuits, constraints & witnesses](https://zeroj.dev/learn/circuits-and-witnesses/). **UTxO.** An unspent transaction output: value, and optionally a datum, sitting at an address until a transaction spends it. On Cardano, funds and application state live in UTxOs. See [ZK on Cardano](https://zeroj.dev/learn/zk-on-cardano/). ### V–Z **Validator.** A Plutus script that decides whether a transaction may spend a UTxO locked at its address. ZeroJ ships reusable verifier validators and a library for writing your own. See [ZK on Cardano](https://zeroj.dev/learn/zk-on-cardano/). **Verification key.** The small public key used to check proofs for one circuit. For Groth16 on BLS12-381 with one public input it's 432 bytes compressed. On Cardano it's baked into the validator as script parameters. See [Trusted setup, explained](https://zeroj.dev/learn/trusted-setup/). **Wire.** A variable in a circuit. The witness gives every wire a value; wire 0 is always the constant 1. See [Circuits, constraints & witnesses](https://zeroj.dev/learn/circuits-and-witnesses/). **Witness.** Every wire value in a circuit for one proof: public inputs, secret inputs, and all intermediate values. It contains your secrets and never leaves the prover. See [Circuits, constraints & witnesses](https://zeroj.dev/learn/circuits-and-witnesses/). **Zero-knowledge.** The guarantee that a proof reveals nothing beyond the truth of the statement. See [Zero-knowledge in plain English](https://zeroj.dev/learn/zero-knowledge-basics/). **`.zkey` / `.ptau`.** snarkjs file formats. A `.ptau` holds powers of tau (phase 1); a `.zkey` holds one circuit's proving and verification keys after phase 2, including its ceremony history. See [Trusted setup, explained](https://zeroj.dev/learn/trusted-setup/). --- ## Prove you're over 18 Source: https://zeroj.dev/tutorials/age-check/ > Build a range proof with ZkUInt, see why bit widths matter, test an invalid witness, and verify with nothing but a key, a proof and public inputs. A website wants to know you're an adult. Today you'd show an ID card and hand over your name, birth date and address to answer a yes/no question. In this tutorial you'll answer only the question: you prove *"my age is at least 18"* while your age stays on your machine. **What you'll build:** an `AgeCheck` circuit, a prover that sets up keys once and reuses them, and a separate `AgeVerifier` that holds only a verification key and its own policy. **What you'll learn:** - how `ZkUInt` and `@UInt(bits = ...)` make comparisons safe in a finite field - what happens when the witness is invalid, and why constraints, not your Java code, are the real gatekeeper - how public inputs are ordered, and why a verifier should build them itself - how to persist keys so setup runs once - what an age proof does *not* prove on its own **Note: Prerequisites:** Java 25 and Gradle (see [Installation](https://zeroj.dev/start/installation/)). The [Quickstart](https://zeroj.dev/start/quickstart/) introduces the basic flow; this page is self-contained, but it moves faster. ### Build it 1. **Create the project.** - zeroj-age-check/ - settings.gradle - build.gradle - src/main/java/com/example/agecheck/ - AgeCheck.java - AgeVerifier.java - Main.java ```groovy title="settings.gradle" rootProject.name = 'zeroj-age-check' ``` ```groovy title="build.gradle" plugins { id 'application' } repositories { mavenCentral() } java { toolchain { languageVersion = JavaLanguageVersion.of(25) } } dependencies { implementation platform('org.zeroj:zeroj-bom-core:0.1.0-pre12') annotationProcessor platform('org.zeroj:zeroj-bom-core:0.1.0-pre12') implementation 'org.zeroj:zeroj-circuit-annotation-api' annotationProcessor 'org.zeroj:zeroj-circuit-annotation-processor' implementation 'org.zeroj:zeroj-circuit-dsl' implementation 'org.zeroj:zeroj-crypto' implementation 'org.zeroj:zeroj-codec' implementation 'org.zeroj:zeroj-verifier-groth16' } application { mainClass = 'com.example.agecheck.Main' // Dev-only: allows the in-process, single-party trusted setup. applicationDefaultJvmArgs = ['-Dzeroj.allowInsecureTrustedSetup=true'] } ``` 2. **Write the circuit.** The age is secret, the threshold is public, and both are 8-bit unsigned integers. ```java title="src/main/java/com/example/agecheck/AgeCheck.java" package com.example.agecheck; import org.zeroj.circuit.annotation.Prove; import org.zeroj.circuit.annotation.Public; import org.zeroj.circuit.annotation.Secret; import org.zeroj.circuit.annotation.UInt; import org.zeroj.circuit.annotation.ZKCircuit; import org.zeroj.circuit.annotation.ZkBool; import org.zeroj.circuit.annotation.ZkUInt; /** "My secret age is at least the public threshold." */ @ZKCircuit(name = "age-check", version = 1) public class AgeCheck { @Prove ZkBool prove(@Public @UInt(bits = 8) ZkUInt threshold, @Secret @UInt(bits = 8) ZkUInt age) { return age.gte(threshold); } } ``` `@UInt(bits = 8)` isn't decoration. It adds constraints proving each value fits in 8 bits (0 to 255), and `gte` relies on that. The section after the steps explains why. 3. **Write the verifier.** This class is what a website would run. It holds a trusted verification key and a policy, receives a proof plus public inputs, and never sees an age. ```java title="src/main/java/com/example/agecheck/AgeVerifier.java" package com.example.agecheck; import org.zeroj.api.CurveId; import org.zeroj.api.ProofSystemId; import org.zeroj.api.PublicInputs; import org.zeroj.api.VerificationMaterial; import org.zeroj.codec.SnarkjsJsonCodec; import org.zeroj.verifier.groth16.bls12381.Groth16BLS12381PureJavaVerifier; import java.nio.charset.StandardCharsets; /** * The verifier's side. It holds a trusted verification key and its own policy * ("adults only: threshold = 18"). It receives a proof and public inputs from * the prover — and never sees the age. */ public class AgeVerifier { private static final int REQUIRED_AGE = 18; private final String trustedVkJson; public AgeVerifier(String trustedVkJson) { this.trustedVkJson = trustedVkJson; } public boolean accept(String proofJson, String publicJson) { // 1. Policy: the statement must be "age >= 18", not a threshold the prover picked. PublicInputs expected = AgeCheckCircuit.inputs().threshold(REQUIRED_AGE).toPublicInputs(); PublicInputs claimed = SnarkjsJsonCodec.parsePublicInputs(publicJson); if (!expected.equals(claimed)) { System.out.println(" rejected: statement " + claimed.values() + " is not the required " + expected.values()); return false; } // 2. Math: is the proof valid for this VK and these public inputs? var circuitId = AgeCheckCircuit.circuitId(); var envelope = SnarkjsJsonCodec.toEnvelopeFromJson(proofJson, trustedVkJson, publicJson, circuitId); var material = VerificationMaterial.of(trustedVkJson.getBytes(StandardCharsets.UTF_8), ProofSystemId.GROTH16, CurveId.BLS12_381, circuitId); var result = new Groth16BLS12381PureJavaVerifier().verify(envelope, material); if (!result.proofValid()) { System.out.println(" rejected: " + result.message().orElse("invalid proof")); } return result.proofValid(); } } ``` 4. **Write the prover program.** It runs setup once and saves the keys, then plays three characters: an adult, a 16-year-old, and a 17-year-old who proves something true but useless. ```java title="src/main/java/com/example/agecheck/Main.java" package com.example.agecheck; import org.zeroj.api.CurveId; import org.zeroj.crypto.groth16.Groth16Keys; import org.zeroj.crypto.groth16.Groth16PkStore; import org.zeroj.crypto.setup.PowersOfTauBLS381; import org.zeroj.crypto.snarkjs.SnarkjsGroth16Json; import java.io.IOException; import java.math.BigInteger; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; public class Main { static final Path KEY_DIR = Path.of("build/age-check-keys"); static final Path VK_FILE = Path.of("build/age-check-vk.json"); public static void main(String[] args) throws IOException { var circuit = AgeCheckCircuit.build(); var r1cs = circuit.compileR1CS(CurveId.BLS12_381); System.out.println("Public inputs, in order: " + AgeCheckCircuit.schema().publicInputs().names()); System.out.println("Constraints: " + r1cs.numConstraints()); // --- One-time setup (DEV-ONLY), then reuse the keys on every later run --------------- if (!Groth16PkStore.exists(KEY_DIR)) { BigInteger tau = PowersOfTauBLS381.generate(6).tauScalar(); try (var keys = Groth16Keys.setupToStore(r1cs.flat(), r1cs.numWires(), r1cs.numPublicInputs(), tau, KEY_DIR, true)) { Files.writeString(VK_FILE, SnarkjsGroth16Json.verificationKeyJson(keys)); } System.out.println("Setup done; keys saved to " + KEY_DIR); } var verifier = new AgeVerifier(Files.readString(VK_FILE)); try (var keys = Groth16Keys.load(KEY_DIR)) { // --- Prover: a 25-year-old proves "age >= 18" ------------------------------------ var inputs = AgeCheckCircuit.inputs().age(25).threshold(18); BigInteger[] witness = inputs.calculateWitness(circuit, CurveId.BLS12_381); var proof = keys.prove(witness, r1cs.constraints()); String proofJson = SnarkjsGroth16Json.proofJson(proof); String publicJson = SnarkjsGroth16Json.publicJson( inputs.publicValues().toArray(BigInteger[]::new)); System.out.println("Public inputs sent to the verifier: " + inputs.publicValues()); System.out.println("Adult accepted? " + verifier.accept(proofJson, publicJson)); // --- A 16-year-old cannot even build a witness ------------------------------------ try { AgeCheckCircuit.inputs().age(16).threshold(18) .calculateWitness(circuit, CurveId.BLS12_381); System.out.println("16-year-old produced a witness?!"); } catch (ArithmeticException e) { System.out.println("16-year-old: witness rejected (" + e.getMessage() + ")"); } // --- A true statement about the wrong threshold is still rejected ---------------- var teen = AgeCheckCircuit.inputs().age(17).threshold(16); var teenProof = keys.prove(teen.calculateWitness(circuit, CurveId.BLS12_381), r1cs.constraints()); String teenPublic = SnarkjsGroth16Json.publicJson( teen.publicValues().toArray(BigInteger[]::new)); System.out.println("Proof of 'age >= 16' accepted? " + verifier.accept(SnarkjsGroth16Json.proofJson(teenProof), teenPublic)); } } } ``` 5. **Run it** with `gradle run` (or `./gradlew run`). The first run does the setup: ```text Public inputs, in order: [threshold] Constraints: 54 WARNING: Single-party Powers of Tau generation (BLS12-381) — for DEVELOPMENT and TESTING only. Use MPC ceremony outputs (Hermez, Zcash PoT) for production. WARNING: Single-party Groth16 Phase 2 setup (BLS12-381, streaming) — for DEVELOPMENT and TESTING only. Use snarkjs multi-party ceremony for production. Setup done; keys saved to build/age-check-keys Public inputs sent to the verifier: [18] Adult accepted? true 16-year-old: witness rejected (Constraint violation: w112=0 != w113=1) rejected: statement [16] is not the required [18] Proof of 'age >= 16' accepted? false ``` Run it again: the `WARNING` and `Setup done` lines disappear, because the keys are loaded from `build/age-check-keys`. ### Why `@UInt(bits = 8)` matters Circuits don't compute with Java `int`s. Every value is an element of a *finite field*: the integers modulo a large prime `r` (for BLS12-381, `r` is a 255-bit number). Arithmetic wraps around at `r`, so there are no negative numbers and no natural "less than": ```text 16 - 18 = r - 2 = 52435875175126190479447740508185965837690552500527637822603658699938581184511 ``` That's why `ZkField` has no `gte` method at all. To compare, a circuit has to prove that both values are small, which it does by splitting them into bits and checking each bit is 0 or 1. `@UInt(bits = 8)` adds exactly those constraints when the input is created. After that, `gte` can compare two numbers known to lie in 0 to 255, where "greater or equal" means what you expect. You can watch the wrap-around get caught. In the [Try this](#try-this) variant below, a birth year *after* the current year makes `currentYear.sub(birthYear)` wrap to a huge number, and the 16-bit range check rejects it: ```text ArithmeticException: Constraint violation: w234=65533 != w169=52435875175126190479447740508185965837690552500527637822603658699938581184509 ``` That long number is `2026 − 2030` in the field, which is `r − 4`. > **Danger: Unconstrained ranges are a classic ZK bug** > > If a value is compared or subtracted without a range constraint, a dishonest prover can pick a > field element that "wraps" and satisfies the equations. The honest test cases still pass, so > nothing looks wrong. Give every `ZkUInt` an explicit `@UInt(bits = ...)` that matches its real > domain, and test values just outside it. ### The invalid witness, and who really enforces the rule For the 16-year-old, `calculateWitness` throws `ArithmeticException: Constraint violation` before any proof exists. It's useful, but it isn't a security boundary: the witness calculator is ordinary Java running on the prover's own machine, and a cheater can skip it and hand-craft a witness. What stops the cheater is the constraints. Groth16 is designed so that a proof verifies only if *every* constraint holds (assuming nobody kept the setup's toxic waste). We tried it: take the adult's valid witness, overwrite the age with 16 (element `2`, right after the constant `1` and the public `threshold`), prove anyway, and the verifier answers `Groth16 BLS12-381 pairing check failed`. That's why an honest run proves nothing about soundness. A circuit that forgets a constraint still works perfectly for honest users. Always test inputs that must fail, like this page does. [Testing circuits](https://zeroj.dev/guides/circuits/testing-circuits/) goes deeper. ### Public inputs: order and ownership Groth16 public inputs are a plain list of numbers, and order matters. The generated schema fixes it: public inputs come in declaration order, which you can print with `AgeCheckCircuit.schema().publicInputs().names()`. The generated `Inputs` class gives you the values in that order through `publicValues()` (a `List`) or `toPublicInputs()` (a typed `PublicInputs`). Notice where the verifier gets its public inputs from. It *builds* the expected list from its own policy with `AgeCheckCircuit.inputs().threshold(18).toPublicInputs()` and compares it with what the prover sent. The third scenario shows why: the 17-year-old's proof of `age >= 16` is perfectly valid, because the statement is true. A verifier that only asked "is this proof valid?" would have let them in. ### Reusing keys Setup is the slow, sensitive part, so you do it once per circuit: - `Groth16Keys.setupToStore(...)` streams the proving key into a directory (`sparse = true` stores it compactly), and `Groth16Keys.load(dir)` memory-maps it on later runs. Small circuits can also use `setupInMemory`, as the Quickstart does. - The verification key is tiny and public. The prover program writes it to `build/age-check-vk.json`, and the verifier only ever reads that file. - Keys belong to one exact circuit. Change the circuit and you must delete `build/age-check-keys` and run setup again. (`gradle clean` also deletes it.) > **Caution: Development setup only** > > These keys come from a single-party setup that knows its toxic waste, which is why it needs > `-Dzeroj.allowInsecureTrustedSetup=true`. Anyone with that secret can forge an "I'm over 18" > proof. Production keys come from a multi-party ceremony; see > [Running a trusted setup ceremony](https://zeroj.dev/guides/proving/trusted-setup-ceremony/). ### What this proof does not prove The circuit proves that the prover knows *a* number of at least 18. It doesn't prove that the number is *their age*. Right now anyone can type `.age(25)` and pass. To make an age check meaningful, the age has to come from someone the verifier trusts, such as a government or a KYC provider. Typically the issuer signs or commits to the user's attributes, and the circuit proves the age inside that signed credential is at least 18. Alternatively, BBS credentials let the holder reveal selected attributes of a signed credential directly. See [Age & KYC checks](https://zeroj.dev/use-cases/age-and-kyc/) and [BBS credentials](https://zeroj.dev/guides/credentials/bbs/). ### Try this Add a version that works from a birth year, so the verifier supplies the current year: ```java title="src/main/java/com/example/agecheck/BirthYearCheck.java" package com.example.agecheck; import org.zeroj.circuit.annotation.Prove; import org.zeroj.circuit.annotation.Public; import org.zeroj.circuit.annotation.Secret; import org.zeroj.circuit.annotation.UInt; import org.zeroj.circuit.annotation.ZKCircuit; import org.zeroj.circuit.annotation.ZkBool; import org.zeroj.circuit.annotation.ZkUInt; @ZKCircuit(name = "birth-year-check", version = 1) public class BirthYearCheck { @Prove ZkBool prove(@Public @UInt(bits = 16) ZkUInt currentYear, @Public @UInt(bits = 8) ZkUInt minAge, @Secret @UInt(bits = 16) ZkUInt birthYear) { ZkUInt age = currentYear.sub(birthYear); // range-checked: can't wrap below zero return age.gte(minAge); } } ``` Build witnesses with `BirthYearCheckCircuit.inputs().currentYear(2026).minAge(18).birthYear(...)`. A birth year of 2000 works, 2010 fails the comparison, and 2030 fails the range check shown above. `ZkUInt.sub` range-checks its result at the wider of the two widths, which is what catches the wrap. (Counting whole years is only approximate; a real service would compare full dates.) ### Next steps - [Private allowlist with a Merkle tree](https://zeroj.dev/tutorials/private-allowlist/): prove membership without revealing which member you are, once per event. - [Testing circuits](https://zeroj.dev/guides/circuits/testing-circuits/): systematic invalid-witness tests. - [Age & KYC checks](https://zeroj.dev/use-cases/age-and-kyc/): binding the age to a trusted issuer. --- ## Private allowlist with a Merkle tree Source: https://zeroj.dev/tutorials/private-allowlist/ > Prove you're on a list without saying who you are, using a Poseidon Merkle tree, then add a nullifier so each member can act only once per event. An event has a guest list. At the door you want to prove *"I'm on the list"* without saying *which* guest you are, and the door wants to make sure nobody gets in twice. Those two goals seem to conflict: how can the door spot a repeat visitor it can't identify? The answer is a **nullifier**, and it's the same trick behind private voting and sybil-resistant airdrops. **What you'll build:** an allowlist of members stored as a Poseidon Merkle tree, a circuit that proves membership plus a per-event nullifier, and an `EventGate` that admits each member at most once per event. **What you'll learn:** - how a Merkle tree turns "I'm one of these N people" into a small proof - why Cardano circuits use Poseidon with explicit BLS12-381 parameters - how to compute the same hashes off-circuit so the prover can build its witness - what a nullifier is, and what the *verifier* must do with it - how to check that a non-member really is rejected **Note: Prerequisites:** Java 25 and Gradle (see [Installation](https://zeroj.dev/start/installation/)). It helps to have done [Prove you're over 18](https://zeroj.dev/tutorials/age-check/), but this page stands on its own. ### The idea in one picture Each member holds two random secrets, a `nullifierKey` and a `trapdoor`. They give the organizer only a hash of them, the **leaf**. The organizer puts all leaves into a Merkle tree and publishes the **root**. ```text root (public) / \ h01 h23 / \ / \ h0 h1 h2 h3 h = Poseidon(left, right) / \ / \ / \ / \ L0 L1 L2 L3 L4 L5 L6 L7 ... leaf = Poseidon(nullifierKey, trapdoor) ``` To prove membership, a member shows (in zero knowledge) that their leaf, hashed together with its **siblings** on the way up, reproduces the published root. The **path bits** say whether the node is a left or right child at each level. The verifier sees only the root. For each event the member also publishes `nullifier = Poseidon(nullifierKey, eventId)`. It's the same value every time the same member proves for the same event, so the gate can refuse repeats. For a different event it's a different, unrelated-looking number, so members can't be tracked across events. Why two secrets? If the leaf were `Poseidon(nullifierKey, 0)`, an organizer could announce `eventId = 0`, and every nullifier would equal its owner's leaf, revealing who is who. Hashing the leaf with a separate secret `trapdoor` rules that out: the organizer would have to guess the trapdoor. ### Build it 1. **Create the project.** This one also needs `zeroj-circuit-lib` for the Poseidon and Merkle gadgets. - zeroj-private-allowlist/ - settings.gradle - build.gradle - src/main/java/com/example/allowlist/ - AllowlistMembership.java - PoseidonMerkleTree.java - EventGate.java - Main.java ```groovy title="settings.gradle" rootProject.name = 'zeroj-private-allowlist' ``` ```groovy title="build.gradle" plugins { id 'application' } repositories { mavenCentral() } java { toolchain { languageVersion = JavaLanguageVersion.of(25) } } dependencies { implementation platform('org.zeroj:zeroj-bom-core:0.1.0-pre12') annotationProcessor platform('org.zeroj:zeroj-bom-core:0.1.0-pre12') implementation 'org.zeroj:zeroj-circuit-annotation-api' annotationProcessor 'org.zeroj:zeroj-circuit-annotation-processor' implementation 'org.zeroj:zeroj-circuit-dsl' implementation 'org.zeroj:zeroj-circuit-lib' // Poseidon + Merkle gadgets implementation 'org.zeroj:zeroj-crypto' implementation 'org.zeroj:zeroj-codec' implementation 'org.zeroj:zeroj-verifier-groth16' } application { mainClass = 'com.example.allowlist.Main' // Dev-only: allows the in-process, single-party trusted setup. applicationDefaultJvmArgs = ['-Dzeroj.allowInsecureTrustedSetup=true'] } ``` 2. **Write the circuit.** The tree depth is a `@CircuitParam`, so one class can produce circuits for different list sizes. This tutorial uses depth 4, room for 16 members. ```java title="src/main/java/com/example/allowlist/AllowlistMembership.java" package com.example.allowlist; import org.zeroj.circuit.annotation.CircuitParam; import org.zeroj.circuit.annotation.FixedSize; import org.zeroj.circuit.annotation.Prove; import org.zeroj.circuit.annotation.Public; import org.zeroj.circuit.annotation.Secret; import org.zeroj.circuit.annotation.ZKCircuit; import org.zeroj.circuit.annotation.ZkArray; import org.zeroj.circuit.annotation.ZkBool; import org.zeroj.circuit.annotation.ZkContext; import org.zeroj.circuit.annotation.ZkField; import org.zeroj.circuit.lib.poseidon.PoseidonParams; import org.zeroj.circuit.lib.poseidon.PoseidonParamsBLS12_381T3; import org.zeroj.circuit.lib.zk.ZkMerkle; import org.zeroj.circuit.lib.zk.ZkPoseidon; /** * "I own one of the leaves under this root, and this nullifier is the one * that leaf gets for this event" — without saying which leaf. */ @ZKCircuit(name = "private-allowlist", nameTemplate = "private-allowlist-d{depth}", version = 1) public class AllowlistMembership { private static final PoseidonParams POSEIDON = PoseidonParamsBLS12_381T3.INSTANCE; public AllowlistMembership(@CircuitParam("depth") int depth) { } @Prove ZkBool prove(ZkContext zk, @Public ZkField root, @Public ZkField eventId, @Public ZkField nullifier, @Secret ZkField nullifierKey, @Secret ZkField trapdoor, @Secret @FixedSize(param = "depth") ZkArray siblings, @Secret @FixedSize(param = "depth") ZkArray pathBits) { // The leaf is a commitment to the member's two secrets. ZkField leaf = ZkPoseidon.hash(zk, POSEIDON, nullifierKey, trapdoor); ZkBool onTheList = ZkMerkle.isMemberPoseidon(zk, POSEIDON, leaf, root, siblings, pathBits); // One nullifier per (member, event): same member + same event = same nullifier. ZkBool nullifierMatches = ZkPoseidon.hash(zk, POSEIDON, nullifierKey, eventId).isEqual(nullifier); return onTheList.and(nullifierMatches); } } ``` Notice what's secret: the member's two secrets, and the whole path (siblings and path bits). Revealing the path would reveal the position in the tree, which is the member's identity. Each `ZkBool` path bit is constrained to be 0 or 1. **Caution: Always pass the BLS12-381 Poseidon parameters:** `ZkPoseidon`, `ZkMerkle.*Poseidon` and the off-circuit `PoseidonHash` all take a `PoseidonParams` argument. For Cardano (BLS12-381) pass `PoseidonParamsBLS12_381T3.INSTANCE` explicitly. The no-params Poseidon overloads and MiMC are aimed at BN254 and aren't Cardano defaults. 3. **Build the tree off-circuit.** The prover needs the root, the siblings and the path bits as ordinary numbers. `PoseidonHash.hash` is ZeroJ's off-circuit Poseidon: with the same parameters it computes exactly what the in-circuit gadget constrains. The tree itself is a few lines on top. ```java title="src/main/java/com/example/allowlist/PoseidonMerkleTree.java" package com.example.allowlist; import org.zeroj.circuit.lib.poseidon.PoseidonHash; import org.zeroj.circuit.lib.poseidon.PoseidonParamsBLS12_381T3; import java.math.BigInteger; import java.util.ArrayList; import java.util.List; /** * A tiny fixed-depth binary Merkle tree, hashed off-circuit with the same * Poseidon parameters the circuit uses. Empty slots hold 0. */ public class PoseidonMerkleTree { private final List> levels = new ArrayList<>(); // levels.get(0) = leaves public PoseidonMerkleTree(int depth, List leaves) { int width = 1 << depth; if (leaves.size() > width) { throw new IllegalArgumentException("too many leaves for depth " + depth); } var level = new ArrayList<>(leaves); while (level.size() < width) { level.add(BigInteger.ZERO); } levels.add(level); for (int d = 0; d < depth; d++) { var parents = new ArrayList(); for (int i = 0; i < level.size(); i += 2) { parents.add(hash(level.get(i), level.get(i + 1))); } levels.add(parents); level = parents; } } public BigInteger root() { return levels.getLast().getFirst(); } /** Sibling hashes from the leaf level up to (not including) the root. */ public List siblings(int index) { var out = new ArrayList(); for (int d = 0; d < levels.size() - 1; d++) { out.add(levels.get(d).get(index ^ 1)); index >>= 1; } return out; } /** 0 = our node is the left child at that level, 1 = it is the right child. */ public List pathBits(int index) { var out = new ArrayList(); for (int d = 0; d < levels.size() - 1; d++) { out.add(BigInteger.valueOf(index & 1)); index >>= 1; } return out; } public static BigInteger hash(BigInteger left, BigInteger right) { return PoseidonHash.hash(PoseidonParamsBLS12_381T3.INSTANCE, left, right); } } ``` The path-bit convention matches the gadget: at a level with bit `0` the current node is hashed as `hash(current, sibling)`, with bit `1` as `hash(sibling, current)`. 4. **Write the gate.** This is the verifier. Its job is more than checking the math, and the order of its checks matters. ```java title="src/main/java/com/example/allowlist/EventGate.java" package com.example.allowlist; import org.zeroj.api.CircuitId; import org.zeroj.api.CurveId; import org.zeroj.api.ProofSystemId; import org.zeroj.api.PublicInputs; import org.zeroj.api.VerificationMaterial; import org.zeroj.codec.SnarkjsJsonCodec; import org.zeroj.verifier.groth16.bls12381.Groth16BLS12381PureJavaVerifier; import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.util.HashSet; import java.util.Set; /** The verifier side: admits each allowlisted member at most once per event. */ public class EventGate { private final String vkJson; private final BigInteger trustedRoot; private final BigInteger eventId; private final Set spentNullifiers = new HashSet<>(); // a database table in real life public EventGate(String vkJson, BigInteger trustedRoot, BigInteger eventId) { this.vkJson = vkJson; this.trustedRoot = trustedRoot; this.eventId = eventId; } public synchronized boolean admit(String proofJson, String publicJson) { PublicInputs pub = SnarkjsJsonCodec.parsePublicInputs(publicJson); if (pub.size() != 3) { return false; } // Order comes from the circuit schema: [root, eventId, nullifier]. BigInteger root = pub.get(0); BigInteger event = pub.get(1); BigInteger nullifier = pub.get(2); if (!root.equals(trustedRoot) || !event.equals(eventId)) { return false; // a proof about some other list or some other event } if (spentNullifiers.contains(nullifier)) { return false; // this member already used their one admission } CircuitId id = AllowlistMembershipCircuit.circuitId(Main.DEPTH); var envelope = SnarkjsJsonCodec.toEnvelopeFromJson(proofJson, vkJson, publicJson, id); var material = VerificationMaterial.of(vkJson.getBytes(StandardCharsets.UTF_8), ProofSystemId.GROTH16, CurveId.BLS12_381, id); if (!new Groth16BLS12381PureJavaVerifier().verify(envelope, material).proofValid()) { return false; } spentNullifiers.add(nullifier); // record only after the proof checks out return true; } } ``` 5. **Write the program.** Five members register, member #2 proves membership, tries to come back, and an outsider tries to get in. ```java title="src/main/java/com/example/allowlist/Main.java" package com.example.allowlist; import org.zeroj.api.CurveId; import org.zeroj.circuit.FieldConfig; import org.zeroj.crypto.groth16.Groth16Keys; import org.zeroj.crypto.setup.PowersOfTauBLS381; import org.zeroj.crypto.snarkjs.SnarkjsGroth16Json; import java.math.BigInteger; import java.security.SecureRandom; import java.util.ArrayList; import java.util.List; public class Main { static final int DEPTH = 4; // up to 2^4 = 16 members static final BigInteger R = FieldConfig.BLS12_381.prime(); static final SecureRandom RNG = new SecureRandom(); /** A member's identity: two random secrets. Only the leaf (a hash) is ever published. */ record Member(BigInteger nullifierKey, BigInteger trapdoor) { static Member create() { return new Member(randomScalar(), randomScalar()); } BigInteger leaf() { return PoseidonMerkleTree.hash(nullifierKey, trapdoor); } BigInteger nullifierFor(BigInteger eventId) { return PoseidonMerkleTree.hash(nullifierKey, eventId); } } public static void main(String[] args) { // 1. Members register a leaf (commitment). The organiser builds the tree and publishes the root. List members = new ArrayList<>(); for (int i = 0; i < 5; i++) { members.add(Member.create()); } var tree = new PoseidonMerkleTree(DEPTH, members.stream().map(Member::leaf).toList()); System.out.println("Allowlist root: " + tree.root()); // 2. Compile the circuit for depth 4 and run a DEV-ONLY setup. var circuit = AllowlistMembershipCircuit.build(DEPTH); var r1cs = circuit.compileR1CS(CurveId.BLS12_381); System.out.println("Constraints: " + r1cs.numConstraints() + ", public inputs: " + AllowlistMembershipCircuit.schema(DEPTH).publicInputs().names()); BigInteger tau = PowersOfTauBLS381.generate(11).tauScalar(); try (var keys = Groth16Keys.setupInMemory( r1cs.constraints(), r1cs.numWires(), r1cs.numPublicInputs(), tau)) { BigInteger event1 = BigInteger.valueOf(2026_09_01); var gate = new EventGate(SnarkjsGroth16Json.verificationKeyJson(keys), tree.root(), event1); // 3. Member #2 proves "I'm on the list" for event 1 — without revealing which member. int me = 2; Member member = members.get(me); var inputs = AllowlistMembershipCircuit.inputs(DEPTH) .root(tree.root()) .eventId(event1) .nullifier(member.nullifierFor(event1)) .nullifierKey(member.nullifierKey()) .trapdoor(member.trapdoor()) .siblings(tree.siblings(me)) .pathBits(tree.pathBits(me)); BigInteger[] witness = inputs.calculateWitness(circuit, CurveId.BLS12_381); var proof = keys.prove(witness, r1cs.constraints()); String proofJson = SnarkjsGroth16Json.proofJson(proof); String publicJson = SnarkjsGroth16Json.publicJson(inputs.publicValues().toArray(BigInteger[]::new)); System.out.println("First use admitted? " + gate.admit(proofJson, publicJson)); System.out.println("Replay admitted? " + gate.admit(proofJson, publicJson)); // 4. The same member at another event gets an unrelated nullifier. BigInteger event2 = BigInteger.valueOf(2026_10_01); System.out.println("Nullifier, event 1: " + member.nullifierFor(event1)); System.out.println("Nullifier, event 2: " + member.nullifierFor(event2)); // 5. Someone who is not on the list cannot build a witness. Member outsider = Member.create(); try { AllowlistMembershipCircuit.inputs(DEPTH) .root(tree.root()) .eventId(event1) .nullifier(outsider.nullifierFor(event1)) .nullifierKey(outsider.nullifierKey()) .trapdoor(outsider.trapdoor()) .siblings(tree.siblings(me)) // borrow a real path: still not a member .pathBits(tree.pathBits(me)) .calculateWitness(circuit, CurveId.BLS12_381); System.out.println("Outsider produced a witness?!"); } catch (ArithmeticException e) { System.out.println("Outsider rejected: " + e.getMessage()); } } } static BigInteger randomScalar() { return new BigInteger(R.bitLength() + 64, RNG).mod(R); } } ``` 6. **Run it** with `gradle run` (or `./gradlew run`). It takes a few seconds; the secrets are random, so your root and nullifiers will differ: ```text Allowlist root: 32221980021819000464522828998816528831645266903374788066867346656485094859322 Constraints: 1466, public inputs: [root, eventId, nullifier] WARNING: Single-party Powers of Tau generation (BLS12-381) — for DEVELOPMENT and TESTING only. Use MPC ceremony outputs (Hermez, Zcash PoT) for production. WARNING: Single-party Groth16 Phase 2 setup (BLS12-381) — for DEVELOPMENT and TESTING only. Use snarkjs multi-party ceremony for production. First use admitted? true Replay admitted? false Nullifier, event 1: 31954755462251752238431364037933139093392902278911937153828149776072209289058 Nullifier, event 2: 46302969935255831668131597490214309035567725663443764852945242753187584366618 Outsider rejected: Constraint violation: w8757=0 != w8758=1 ``` ### What each piece guarantees | Piece | Guarantee | Who enforces it | |-------|-----------|-----------------| | Merkle membership | The prover knows secrets whose leaf is under `root` | The circuit | | Nullifier formula | `nullifier` really is `Poseidon(nullifierKey, eventId)` for *that* leaf's key | The circuit | | Right list, right event | `root` and `eventId` are the ones this gate cares about | `EventGate` (your code) | | Once per event | The same `nullifier` is never accepted twice | `EventGate` (your code) | | Nothing about who | The verifier learns root, event and nullifier, not the leaf or its position | Zero knowledge | The circuit guarantees the nullifier is honest; it can't remember which nullifiers were used. That's state, and state belongs to the verifier. If `EventGate` forgot the `spentNullifiers` check, the same proof would get in again and again, even though every proof is valid. About the outsider: they borrowed a real member's siblings and path bits, but their own leaf hashes to a different root, so the membership check fails. Try other variations, such as a member's leaf with a wrong path bit or a real member's proof submitted with a different `eventId` in the public inputs. Each must be rejected, either at witness time or by the gate. ### Nullifiers on Cardano On-chain, "remember every spent nullifier" has to live in the ledger, and the circuit stays the same. The [private voting use case](https://zeroj.dev/use-cases/private-voting/) compares the usual patterns: | Pattern | How duplicates are refused | Trade-off | |---------|----------------------------|-----------| | One registry UTxO holding a list | The validator checks the list and appends | Simple, but a single datum only fits a small list and every use contends for it | | One token per nullifier | Mint a token named after the nullifier | Needs an extra mechanism to prevent re-minting the same name | | Nullifier Merkle root | Each use also proves the nullifier was inserted | Constant on-chain size, needs an off-chain tree service | | Sorted linked list of UTxOs | Insertion between two neighbours proves the nullifier is new | Fully on-chain, costs a little locked ADA per entry | Whatever the pattern, the validator must check the nullifier *and* verify the proof in the same transaction, and it must check the root and event against trusted state, just as `EventGate` does. [Verify your proof on Cardano](https://zeroj.dev/tutorials/verify-on-cardano/) shows the proof side, and [Application security](https://zeroj.dev/guides/verifying/application-security/) covers the rest. > **Caution: Bind the proof to the action** > > In this tutorial the proof says "some member, event 1", but not *what* the admission is for. > If admission pays out something, whoever submits a copied proof first gets it, and the real > member is then refused as a replay. Real designs add the action, such as a recipient address, > as a public input that takes part in the circuit's constraints, so a copied proof is useless > for anything else. ### Try this - Change `DEPTH` to 8 (256 members). The circuit grows from 1,466 to 2,442 constraints, about 244 per level, and its circuit ID changes from `private-allowlist-d4--depth-1:4` to `private-allowlist-d8--depth-1:8`: a different depth is a different circuit with its own keys. The setup call can stay as it is: Groth16 setup uses only the tau scalar from `PowersOfTauBLS381.generate(...)` and sizes its own domain from the constraint count. - Remove the `spentNullifiers` check and watch the replay get in. - Give a member a proof for event 2 and submit it to the event 1 gate. ### Next steps - [Verify your proof on Cardano](https://zeroj.dev/tutorials/verify-on-cardano/): the on-chain side, including replay protection. - [Sybil-resistant airdrop](https://zeroj.dev/use-cases/sybil-resistant-airdrop/) and [Private voting](https://zeroj.dev/use-cases/private-voting/): nullifiers in complete applications. - [Gadgets](https://zeroj.dev/guides/circuits/gadgets/): the Poseidon and Merkle gadgets, and which curves they support. - [Authenticated state](https://zeroj.dev/guides/credentials/authenticated-state/): Poseidon-rooted trees for large, frequently updated sets. --- ## Verify your proof on Cardano Source: https://zeroj.dev/tutorials/verify-on-cardano/ > Lock test ADA behind a Groth16 verifier on a local Yaci DevKit devnet, unlock it with a proof, and learn why a bare verifier must never guard real value. In the [Quickstart](https://zeroj.dev/start/quickstart/) a Java method checked your proof. Here, every Cardano node checks it. You'll take the same `3 × b = 33` proof, lock some test ADA at a Plutus V3 script that verifies Groth16 proofs, and unlock it by presenting the proof. Then comes the most important part of this page: why that script, on its own, must never guard real value. **What you'll build:** a Java program that proves, packages the proof for Plutus, derives the verifier's script address, locks 5 ADA there, and unlocks it with the proof on a local devnet. Then a custom validator, compiled from Java with JuLC, that binds each proof to the UTxO it spends. **What you'll learn:** - how a Groth16 proof maps onto Cardano's eUTxO model: script parameters, datum, redeemer - how `ProverToCardano` and `JulcScriptLoader` turn keys and proofs into Plutus data - how to lock and unlock with Cardano Client Lib against Yaci DevKit - why a valid proof is not authorization, and what binding a proof to a spend looks like **Note: Prerequisites:** - Java 25 and Gradle (see [Installation](https://zeroj.dev/start/installation/)). - [Yaci DevKit](https://github.com/bloxbean/yaci-devkit), a local Cardano devnet. Follow its README to install it, then start a node: ```bash devkit start # then, at the yaci-cli prompt: create-node -o --start ``` This tutorial expects DevKit's defaults: the Blockfrost-compatible API at `http://localhost:8080/api/v1/` and the admin API (which includes a faucet) at `http://localhost:10000`. ### How a proof lives on Cardano Cardano doesn't have contract storage the way account-based chains do. Value sits in **UTxOs**, and a UTxO locked at a **script address** can only be spent if that script approves the spending transaction. The Groth16 flow fits this model neatly: | Groth16 piece | Where it goes on Cardano | Why | |---------------|--------------------------|-----| | Verification key | Baked into the script as **parameters** | The key becomes part of the script, so a different key gives a different script hash and address | | Public inputs | The locked UTxO's **datum** | Fixed when the ADA is locked: this is the statement that must be proven | | Proof | The spending transaction's **redeemer** | Supplied by whoever wants to spend | | Verification | Plutus V3's built-in BLS12-381 operations | The pairing check runs inside the validator | ZeroJ ships the validator: `Groth16BLS12381Verifier`, a Plutus V3 spending script written in Java with [JuLC](https://github.com/bloxbean/julc) and precompiled into `zeroj-onchain-julc`. (It lives in `org.zeroj.onchain.julc.groth16.validator`. Don't confuse it with the off-chain verifier of the same simple name in `org.zeroj.verifier.groth16.bls12381`.) It accepts any number of public inputs. See [ZK on Cardano](https://zeroj.dev/learn/zk-on-cardano/) for the bigger picture. ### Build it 1. **Create the project.** The last four files come later, in [Bind the proof to the UTxO it spends](#bind-the-proof-to-the-utxo-it-spends). - zeroj-verify-on-cardano/ - settings.gradle - build.gradle - src/main/java/com/example/onchain/ - SecretMultiplier.java - Main.java - BoundSecretMultiplier.java - SpendBoundGroth16Verifier.java - SpendBinding.java - BoundMain.java ```groovy title="settings.gradle" rootProject.name = 'zeroj-verify-on-cardano' ``` ```groovy title="build.gradle" plugins { id 'application' } repositories { mavenCentral() } java { toolchain { languageVersion = JavaLanguageVersion.of(25) } } dependencies { implementation platform('org.zeroj:zeroj-bom-core:0.1.0-pre12') annotationProcessor platform('org.zeroj:zeroj-bom-core:0.1.0-pre12') implementation 'org.zeroj:zeroj-circuit-annotation-api' annotationProcessor 'org.zeroj:zeroj-circuit-annotation-processor' implementation 'org.zeroj:zeroj-circuit-dsl' implementation 'org.zeroj:zeroj-crypto' implementation 'org.zeroj:zeroj-onchain-julc' // Plutus V3 verifiers + ProverToCardano implementation 'com.bloxbean.cardano:julc-cardano-client-lib:0.1.0-pre16' // JulcScriptLoader implementation 'com.bloxbean.cardano:cardano-client-lib:0.8.0-pre5' implementation 'com.bloxbean.cardano:cardano-client-backend-blockfrost:0.8.0-pre5' // Only for compiling your own validator (SpendBoundGroth16Verifier) to Plutus V3: implementation 'com.bloxbean.cardano:julc-stdlib:0.1.0-pre16' annotationProcessor 'com.bloxbean.cardano:julc-annotation-processor:0.1.0-pre16' annotationProcessor 'org.zeroj:zeroj-onchain-julc' // lets JuLC find Groth16BLS12381Lib's source } application { mainClass = 'com.example.onchain.Main' // Dev-only: allows the in-process, single-party trusted setup. applicationDefaultJvmArgs = ['-Dzeroj.allowInsecureTrustedSetup=true'] } // `gradle runBound` runs the spend-bound flow (BoundMain) from the security section. tasks.register('runBound', JavaExec) { classpath = sourceSets.main.runtimeClasspath mainClass = 'com.example.onchain.BoundMain' jvmArgs '-Dzeroj.allowInsecureTrustedSetup=true' } ``` JuLC and Cardano Client Lib keep their own `com.bloxbean.cardano` group and versions. The bare verifier in `Main` is precompiled inside `zeroj-onchain-julc`, so `Main` only *loads* it. The three lines marked "Only for compiling your own validator" are for the spend-bound validator later on this page: they run the JuLC compiler inside `javac`. 2. **Add the circuit.** It's the Quickstart circuit, in this project's package. ```java title="src/main/java/com/example/onchain/SecretMultiplier.java" package com.example.onchain; import org.zeroj.circuit.annotation.Prove; import org.zeroj.circuit.annotation.Public; import org.zeroj.circuit.annotation.Secret; import org.zeroj.circuit.annotation.ZKCircuit; import org.zeroj.circuit.annotation.ZkBool; import org.zeroj.circuit.annotation.ZkContext; import org.zeroj.circuit.annotation.ZkField; /** "I know a secret b such that a × b = product." */ @ZKCircuit(name = "secret-multiplier", version = 1) public class SecretMultiplier { @Prove ZkBool prove(ZkContext zk, @Public ZkField a, @Public ZkField product, @Secret ZkField b) { return a.mul(b).isEqual(product); } } ``` 3. **Write the program.** `proveAndPackage` does everything off-chain; `lockThenUnlock` talks to the chain. ```java title="src/main/java/com/example/onchain/Main.java" package com.example.onchain; import com.bloxbean.cardano.client.account.Account; import com.bloxbean.cardano.client.address.AddressProvider; import com.bloxbean.cardano.client.api.model.Amount; import com.bloxbean.cardano.client.backend.api.BackendService; import com.bloxbean.cardano.client.backend.blockfrost.service.BFBackendService; import com.bloxbean.cardano.client.common.model.Networks; import com.bloxbean.cardano.client.function.helper.SignerProviders; import com.bloxbean.cardano.client.plutus.spec.BigIntPlutusData; import com.bloxbean.cardano.client.plutus.spec.BytesPlutusData; import com.bloxbean.cardano.client.plutus.spec.ConstrPlutusData; import com.bloxbean.cardano.client.plutus.spec.ListPlutusData; import com.bloxbean.cardano.client.plutus.spec.PlutusData; import com.bloxbean.cardano.client.plutus.spec.PlutusV3Script; import com.bloxbean.cardano.client.quicktx.QuickTxBuilder; import com.bloxbean.cardano.client.quicktx.ScriptTx; import com.bloxbean.cardano.client.quicktx.Tx; import com.bloxbean.cardano.julc.clientlib.JulcScriptLoader; import org.zeroj.api.CurveId; import org.zeroj.crypto.groth16.Groth16Keys; import org.zeroj.crypto.setup.PowersOfTauBLS381; import org.zeroj.onchain.julc.groth16.codec.ProverToCardano; import org.zeroj.onchain.julc.groth16.codec.SnarkjsToCardano; import org.zeroj.onchain.julc.groth16.validator.Groth16BLS12381Verifier; import java.math.BigInteger; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { static final String YACI_API = "http://localhost:8080/api/v1/"; // Blockfrost-compatible static final String YACI_ADMIN = "http://localhost:10000"; // DevKit admin API (faucet) /** Everything the chain needs: the script, its address, the datum and the redeemer. */ record OnChainProof(PlutusV3Script script, String scriptAddress, PlutusData datum, PlutusData redeemer) {} public static void main(String[] args) throws Exception { OnChainProof onChain = proveAndPackage(); System.out.println("Verifier script address: " + onChain.scriptAddress()); // Connect to Yaci DevKit and fund a throwaway devnet account. BackendService backend = new BFBackendService(YACI_API, "not-needed-for-yaci"); Account alice = new Account(Networks.testnet()); topUp(alice.baseAddress(), 100); lockThenUnlock(new QuickTxBuilder(backend), alice, onChain); } static OnChainProof proveAndPackage() { // 1. Prove "I know b such that 3 × b = 33" — exactly as in the quickstart. var circuit = SecretMultiplierCircuit.build(); var r1cs = circuit.compileR1CS(CurveId.BLS12_381); var inputs = SecretMultiplierCircuit.inputs().a(3).product(33).b(11); BigInteger[] witness = inputs.calculateWitness(circuit, CurveId.BLS12_381); BigInteger tau = PowersOfTauBLS381.generate(4).tauScalar(); // DEV-ONLY setup try (var keys = Groth16Keys.setupInMemory( r1cs.constraints(), r1cs.numWires(), r1cs.numPublicInputs(), tau)) { var proof = keys.prove(witness, r1cs.constraints()); // 2. Compress VK and proof into the byte format Plutus' BLS12-381 builtins expect. SnarkjsToCardano.VkCompressed vk = ProverToCardano.compressVk(keys); SnarkjsToCardano.ProofCompressed p = ProverToCardano.compressProof(proof); // 3. Bake the VK into the reusable verifier script. A different VK gives a different script. PlutusV3Script script = verifierScript(vk); String address = AddressProvider.getEntAddress(script, Networks.testnet()).toBech32(); // 4. Datum = public inputs in schema order; redeemer = the proof. PlutusData datum = publicInputsDatum(inputs.publicValues().toArray(BigInteger[]::new)); PlutusData redeemer = ConstrPlutusData.builder() .alternative(0) .data(ListPlutusData.of( new BytesPlutusData(p.piA()), new BytesPlutusData(p.piB()), new BytesPlutusData(p.piC()))) .build(); return new OnChainProof(script, address, datum, redeemer); } } static void lockThenUnlock(QuickTxBuilder quickTx, Account alice, OnChainProof onChain) { // 5. Lock 5 ADA at the script address, with the public inputs as inline datum. var lock = new Tx() .payToContract(onChain.scriptAddress(), Amount.ada(5), onChain.datum()) .from(alice.baseAddress()); var locked = quickTx.compose(lock) .withSigner(SignerProviders.signerFrom(alice)) .completeAndWait(System.out::println); if (!locked.isSuccessful()) { throw new IllegalStateException("Lock failed: " + locked.getResponse()); } String lockTxHash = locked.getValue(); System.out.println("Locked 5 ADA in tx " + lockTxHash); // 6. Unlock: spend that UTxO with the proof as redeemer. Every node runs the verifier. var unlock = new ScriptTx() .collectFrom(onChain.scriptAddress(), utxo -> utxo.getTxHash().equals(lockTxHash), onChain.redeemer()) .payToAddress(alice.baseAddress(), Amount.ada(4.5)) .attachSpendingValidator(onChain.script()); var unlocked = quickTx.compose(unlock) .withSigner(SignerProviders.signerFrom(alice)) .feePayer(alice.baseAddress()) .collateralPayer(alice.baseAddress()) .completeAndWait(System.out::println); System.out.println(unlocked.isSuccessful() ? "Proof verified on-chain. Unlock tx " + unlocked.getValue() : "Unlock failed: " + unlocked.getResponse()); } /** Groth16BLS12381Verifier parameters, in order: alpha, beta, gamma, delta, IC list. */ static PlutusV3Script verifierScript(SnarkjsToCardano.VkCompressed vk) { var ic = ListPlutusData.of(); for (byte[] point : vk.ic()) { ic.add(new BytesPlutusData(point)); } return JulcScriptLoader.load(Groth16BLS12381Verifier.class, new BytesPlutusData(vk.alpha()), new BytesPlutusData(vk.beta()), new BytesPlutusData(vk.gamma()), new BytesPlutusData(vk.delta()), ic); } static PlutusData publicInputsDatum(BigInteger[] publicInputs) { var list = ListPlutusData.of(); for (BigInteger value : publicInputs) { list.add(BigIntPlutusData.of(value)); } return list; } /** Yaci DevKit's faucet (devnet only). */ static void topUp(String address, int ada) throws Exception { var request = HttpRequest.newBuilder(URI.create(YACI_ADMIN + "/local-cluster/api/addresses/topup")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString( "{\"address\":\"" + address + "\",\"adaAmount\":" + ada + "}")) .build(); var response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() != 200) { throw new IllegalStateException("Top-up failed: " + response.body()); } Thread.sleep(2000); // give the devnet a moment to include the faucet transaction } } ``` 4. **Run it** with DevKit's node running: `gradle run` (or `./gradlew run`). You should see output like this (hashes shortened; there may also be `[PENDING]` lines while the devnet produces blocks): ```text Verifier script address: addr_test1w... [SUBMITTED] Tx: 1d1054a6... [CONFIRMED] Tx: 1d1054a6... Locked 5 ADA in tx 1d1054a6... [SUBMITTED] Tx: 3d1f3a28... [CONFIRMED] Tx: 3d1f3a28... Proof verified on-chain. Unlock tx 3d1f3a28... ``` If the unlock fails, the most common causes are a DevKit node that isn't running, and a datum whose public inputs don't match the proof. Cardano Client Lib evaluates the script while building the transaction, so a wrong proof or datum fails before anything is submitted. If the error mentions `blst` or a failure to evaluate script cost, the DevKit build itself can't evaluate Plutus' BLS12-381 builtins (ZeroJ's own end-to-end test skips in that case); check the Yaci DevKit release notes for a build that supports them. ### What each piece does - **`ProverToCardano.compressVk` / `compressProof`** convert ZeroJ's curve points into the compressed encodings Plutus' BLS12-381 builtins take: 48 bytes per G1 point, 96 per G2 point. The proof becomes `piA`, `piB`, `piC`. (For proofs and keys that came from snarkjs, `SnarkjsToCardano` produces the same shapes from JSON.) - **`JulcScriptLoader.load(Groth16BLS12381Verifier.class, ...)`** takes the precompiled validator and applies its parameters in declaration order: `vkAlpha`, `vkBeta`, `vkGamma`, `vkDelta`, then `vkIc`, the list of IC points (one more than the number of public inputs). The result is a `PlutusV3Script`. Change the key and you get a different script hash and a different address. - **`AddressProvider.getEntAddress`** derives the script's enterprise address (no staking part). - **The datum** is a plain list of integers, `[3, 33]`, in the circuit's public-input order. - **The redeemer** is constructor 0 with the three compressed proof points. - **`ScriptTx`** spends the script UTxO. Recent Cardano Client Lib versions mark `ScriptTx` as deprecated in favour of the same operations on `Tx`; this tutorial uses `ScriptTx` because it's the path ZeroJ's end-to-end tests run against Yaci DevKit. Verifying this two-input proof inside the validator costs roughly 2.8 billion CPU steps and 212,000 memory units, measured in the JuLC VM. More public inputs cost more, because each one adds a scalar multiplication. ### A valid proof is not authorization > **Danger: Never protect real value with the bare verifier** > > `Groth16BLS12381Verifier` answers exactly one question: *is this a valid proof for the public > inputs in the datum?* It does not look at who is spending, where the ADA goes, or which UTxO > is being spent. So **anyone who sees a valid proof can reuse it** to spend UTxOs locked at this > script, and send the ADA wherever they like. Here's the attack. When you submit the unlock transaction, your proof becomes public in its redeemer. If another UTxO is locked at the same address with the same datum, anyone can copy that redeemer and spend it, sending the ADA wherever they like. They can even copy a proof from the mempool and race your own transaction. We checked this: the same redeemer that unlocks the first UTxO also unlocks a second one locked with the same datum. Nothing in the statement `3 × b = 33` mentions a transaction, a recipient or a UTxO, so nothing stops the replay. A real validator has to bind the proof to its context. The rest of this section binds it to the UTxO being spent. #### Bind the proof to the UTxO it spends The idea: make the UTxO part of the statement. Hash the out-ref of the UTxO being spent into a field element, ```text spendRef = blake2b_256(txId || outputIndex as 32-byte big-endian) mod r ``` make `spendRef` the circuit's first public input, and have the validator compute the same value from the `ScriptContext` of the spending transaction. A proof made for one UTxO then fails for every other UTxO, because the validator feeds it a different `spendRef`. Where `spendRef` comes from matters. The datum can't carry it: a UTxO's out-ref only exists once the transaction that creates it exists, and that transaction's id is a hash over the output, datum included. So the datum holds only the application's inputs, and the order of operations is **lock first, then prove**: ```text lock datum = [a, product] creates UTxO txId#index prove public inputs = [spendRef(txId#index), a, product] unlock validator: spendRef from ScriptContext, verify proof against [spendRef] ++ datum ``` **Caution: Why not Groth16BLS12381TxOutRefBindingVerifier?:** ZeroJ ships `Groth16BLS12381TxOutRefBindingVerifier`, which performs the same check but expects `spendRef` *inside the datum* of the UTxO it protects. That would need a datum containing a hash of the very transaction that creates it, a hash fixed point nobody can find in practice, so the bundled class can't guard real funds as it stands. It's useful as a reference for the check itself; the validator below computes `spendRef` instead of reading it. 1. **The circuit.** It's the Quickstart statement with `spendRef` added as the first public input. ```java title="src/main/java/com/example/onchain/BoundSecretMultiplier.java" package com.example.onchain; import org.zeroj.circuit.annotation.Prove; import org.zeroj.circuit.annotation.Public; import org.zeroj.circuit.annotation.Secret; import org.zeroj.circuit.annotation.ZKCircuit; import org.zeroj.circuit.annotation.ZkBool; import org.zeroj.circuit.annotation.ZkContext; import org.zeroj.circuit.annotation.ZkField; /** The quickstart statement, plus a first public input that names the UTxO being spent. */ @ZKCircuit(name = "bound-secret-multiplier", version = 1) public class BoundSecretMultiplier { @Prove ZkBool prove(ZkContext zk, @Public ZkField spendRef, // must be the FIRST public input @Public ZkField a, @Public ZkField product, @Secret ZkField b) { spendRef.mul(b); // puts spendRef into a real constraint so the proof commits to it return a.mul(b).isEqual(product); } } ``` The `spendRef.mul(b)` line matters. A public input that takes part in no constraint isn't bound by the proof at all, so ZeroJ's setup refuses such a circuit with `IllegalArgumentException: R1CS public wire 1 ... is not referenced by any constraint`. 2. **The validator.** A JuLC spending validator that composes `Groth16BLS12381Lib`, ZeroJ's reusable on-chain Groth16 check. It computes `spendRef` for the UTxO it is validating, prepends it to the datum's list, and verifies the proof against the result. ```java title="src/main/java/com/example/onchain/SpendBoundGroth16Verifier.java" package com.example.onchain; import com.bloxbean.cardano.julc.core.PlutusData; import com.bloxbean.cardano.julc.ledger.ScriptContext; import com.bloxbean.cardano.julc.ledger.ScriptInfo; import com.bloxbean.cardano.julc.ledger.TxOutRef; import com.bloxbean.cardano.julc.stdlib.Builtins; import com.bloxbean.cardano.julc.stdlib.annotation.Entrypoint; import com.bloxbean.cardano.julc.stdlib.annotation.Param; import com.bloxbean.cardano.julc.stdlib.annotation.SpendingValidator; import org.zeroj.onchain.julc.groth16.lib.Groth16BLS12381Lib; import java.math.BigInteger; /** * Groth16 verifier whose statement is bound to the UTxO being spent. * *

The datum holds only the application's public inputs, e.g. [a, product]. The validator * computes spendRef = blake2b_256(txId || outputIndex as 32 bytes) mod r for the UTxO it is * validating and prepends it, so the proof is checked against [spendRef, a, product]. A proof * made for one UTxO therefore fails for every other UTxO.

*/ @SpendingValidator public class SpendBoundGroth16Verifier { @Param static byte[] vkAlpha; // G1 compressed, 48 bytes @Param static byte[] vkBeta; // G2 compressed, 96 bytes @Param static byte[] vkGamma; // G2 compressed, 96 bytes @Param static byte[] vkDelta; // G2 compressed, 96 bytes @Param static PlutusData vkIc; // list of G1 compressed IC points record Groth16Proof(byte[] piA, byte[] piB, byte[] piC) {} @Entrypoint public static boolean validate(PlutusData datum, Groth16Proof proof, ScriptContext ctx) { PlutusData publicInputs = Builtins.listData( Builtins.mkCons(Builtins.iData(spendRef(ctx)), Builtins.unListData(datum))); return Groth16BLS12381Lib.verify(publicInputs, proof.piA(), proof.piB(), proof.piC(), vkAlpha, vkBeta, vkGamma, vkDelta, vkIc); } /** blake2b_256(txId || outputIndex as 32-byte big-endian) mod r; -1 (always rejected) if not a spend. */ private static BigInteger spendRef(ScriptContext ctx) { ScriptInfo scriptInfo = ctx.scriptInfo(); if (scriptInfo instanceof ScriptInfo.SpendingScript spendingScript) { TxOutRef txOutRef = spendingScript.txOutRef(); byte[] indexBytes = Builtins.integerToByteString(true, 32, txOutRef.index()); byte[] preimage = Builtins.appendByteString(txOutRef.txId().hash(), indexBytes); return Builtins.byteStringToInteger(true, Builtins.blake2b_256(preimage)).mod(fr()); } else { return BigInteger.valueOf(-1); } } /** The BLS12-381 scalar field order r. */ private static BigInteger fr() { BigInteger base = BigInteger.valueOf(1000000000000000000L); return BigInteger.valueOf(52435L).multiply(base) .add(BigInteger.valueOf(875175126190479447L)).multiply(base) .add(BigInteger.valueOf(740508185965837690L)).multiply(base) .add(BigInteger.valueOf(552500527637822603L)).multiply(base) .add(BigInteger.valueOf(658699938581184513L)); } } ``` This is Java, but it never runs on the JVM. When you build, the JuLC annotation processor compiles it to a Plutus V3 script and writes `META-INF/plutus/SpendBoundGroth16Verifier.plutus.json` next to your classes; `JulcScriptLoader` loads it from there. That's what the three "Only for compiling your own validator" lines in `build.gradle` are for. The `annotationProcessor 'org.zeroj:zeroj-onchain-julc'` line puts the source of `Groth16BLS12381Lib` where JuLC can find it; without it the build fails with `Plutus compilation error: Undefined variable: Groth16BLS12381Lib`. A few details keep the check tight. `Groth16BLS12381Lib.verify` rejects the proof unless the list has exactly one entry per public input and every entry lies in `[0, r)`, so a datum that already contains a `spendRef`, or a non-spending context (where `spendRef` is `-1`), fails. `fr()` builds `r` from `long` pieces, the same way ZeroJ's own validators do. 3. **The prover-side hash.** The prover computes the same `spendRef` off-chain with Cardano Client Lib's Blake2b: ```java title="src/main/java/com/example/onchain/SpendBinding.java" package com.example.onchain; import com.bloxbean.cardano.client.crypto.Blake2bUtil; import com.bloxbean.cardano.client.util.HexUtil; import org.zeroj.circuit.FieldConfig; import java.math.BigInteger; import java.nio.ByteBuffer; public final class SpendBinding { private static final BigInteger R = FieldConfig.BLS12_381.prime(); /** * blake2b_256(txId || outputIndex as 32-byte big-endian) mod r — the value * SpendBoundGroth16Verifier computes on-chain from the ScriptContext. */ public static BigInteger spendRef(String txHash, int outputIndex) { byte[] preimage = ByteBuffer.allocate(64) .put(HexUtil.decodeHexString(txHash)) // 32-byte transaction id .putInt(60, outputIndex) // index, left-padded to 32 bytes .array(); return new BigInteger(1, Blake2bUtil.blake2bHash256(preimage)).mod(R); } private SpendBinding() { } } ``` 4. **The flow.** `BoundMain` sets up keys (so the script address exists), locks 5 ADA with datum `[3, 33]`, reads the new UTxO's out-ref, proves for it, and unlocks. Then it locks a second UTxO with the same datum and tries the same proof on it. ```java title="src/main/java/com/example/onchain/BoundMain.java" package com.example.onchain; import com.bloxbean.cardano.client.account.Account; import com.bloxbean.cardano.client.address.AddressProvider; import com.bloxbean.cardano.client.api.UtxoSupplier; import com.bloxbean.cardano.client.api.model.Amount; import com.bloxbean.cardano.client.api.model.Utxo; import com.bloxbean.cardano.client.backend.api.BackendService; import com.bloxbean.cardano.client.backend.api.DefaultUtxoSupplier; import com.bloxbean.cardano.client.backend.blockfrost.service.BFBackendService; import com.bloxbean.cardano.client.common.model.Networks; import com.bloxbean.cardano.client.function.helper.SignerProviders; import com.bloxbean.cardano.client.plutus.spec.BytesPlutusData; import com.bloxbean.cardano.client.plutus.spec.ConstrPlutusData; import com.bloxbean.cardano.client.plutus.spec.ListPlutusData; import com.bloxbean.cardano.client.plutus.spec.PlutusData; import com.bloxbean.cardano.client.plutus.spec.PlutusV3Script; import com.bloxbean.cardano.client.quicktx.QuickTxBuilder; import com.bloxbean.cardano.client.quicktx.ScriptTx; import com.bloxbean.cardano.client.quicktx.Tx; import com.bloxbean.cardano.julc.clientlib.JulcScriptLoader; import org.zeroj.api.CurveId; import org.zeroj.crypto.groth16.Groth16Keys; import org.zeroj.crypto.setup.PowersOfTauBLS381; import org.zeroj.onchain.julc.groth16.codec.ProverToCardano; import org.zeroj.onchain.julc.groth16.codec.SnarkjsToCardano; import java.math.BigInteger; /** Lock first, then prove for that exact UTxO, then unlock. A copied proof can't spend another UTxO. */ public class BoundMain { public static void main(String[] args) throws Exception { BackendService backend = new BFBackendService(Main.YACI_API, "not-needed-for-yaci"); Account alice = new Account(Networks.testnet()); Main.topUp(alice.baseAddress(), 100); run(new QuickTxBuilder(backend), new DefaultUtxoSupplier(backend.getUtxoService()), alice); } static void run(QuickTxBuilder quickTx, UtxoSupplier utxos, Account alice) { // 1. Circuit and DEV-ONLY setup. The keys, and so the script address, exist before any proof. var circuit = BoundSecretMultiplierCircuit.build(); var r1cs = circuit.compileR1CS(CurveId.BLS12_381); BigInteger tau = PowersOfTauBLS381.generate(4).tauScalar(); try (var keys = Groth16Keys.setupInMemory( r1cs.constraints(), r1cs.numWires(), r1cs.numPublicInputs(), tau)) { PlutusV3Script script = spendBoundScript(ProverToCardano.compressVk(keys)); String scriptAddress = AddressProvider.getEntAddress(script, Networks.testnet()).toBech32(); // 2. Lock with the application's public inputs only: datum = [a, product]. PlutusData datum = Main.publicInputsDatum(new BigInteger[]{BigInteger.valueOf(3), BigInteger.valueOf(33)}); Utxo locked = lock(quickTx, utxos, alice, scriptAddress, datum); // 3. The out-ref now exists. Compute spendRef for it and prove [spendRef, a, product]. BigInteger spendRef = SpendBinding.spendRef(locked.getTxHash(), locked.getOutputIndex()); var inputs = BoundSecretMultiplierCircuit.inputs().spendRef(spendRef).a(3).product(33).b(11); var proof = keys.prove(inputs.calculateWitness(circuit, CurveId.BLS12_381), r1cs.constraints()); PlutusData redeemer = redeemer(ProverToCardano.compressProof(proof)); // 4. Unlock that UTxO with its proof. System.out.println("Unlock the bound UTxO: " + unlock(quickTx, alice, script, locked, redeemer)); // 5. Replay: lock another UTxO with the same datum and try the same proof on it. Utxo other = lock(quickTx, utxos, alice, scriptAddress, datum); System.out.println("Replay on another UTxO: " + unlock(quickTx, alice, script, other, redeemer)); } } static Utxo lock(QuickTxBuilder quickTx, UtxoSupplier utxos, Account alice, String scriptAddress, PlutusData datum) { var tx = new Tx() .payToContract(scriptAddress, Amount.ada(5), datum) .from(alice.baseAddress()); var result = quickTx.compose(tx) .withSigner(SignerProviders.signerFrom(alice)) .completeAndWait(System.out::println); if (!result.isSuccessful()) { throw new IllegalStateException("Lock failed: " + result.getResponse()); } String txHash = result.getValue(); return utxos.getAll(scriptAddress).stream() .filter(utxo -> utxo.getTxHash().equals(txHash)) .findFirst() .orElseThrow(); } static String unlock(QuickTxBuilder quickTx, Account alice, PlutusV3Script script, Utxo utxo, PlutusData redeemer) { var tx = new ScriptTx() .collectFrom(utxo, redeemer) .payToAddress(alice.baseAddress(), Amount.ada(4.5)) .attachSpendingValidator(script); try { var result = quickTx.compose(tx) .withSigner(SignerProviders.signerFrom(alice)) .feePayer(alice.baseAddress()) .collateralPayer(alice.baseAddress()) .completeAndWait(System.out::println); return result.isSuccessful() ? "unlocked in tx " + result.getValue() : "failed: " + result.getResponse(); } catch (RuntimeException e) { return "rejected (" + e.getMessage() + ")"; } } /** SpendBoundGroth16Verifier parameters, in order: alpha, beta, gamma, delta, IC list. */ static PlutusV3Script spendBoundScript(SnarkjsToCardano.VkCompressed vk) { var ic = ListPlutusData.of(); for (byte[] point : vk.ic()) { ic.add(new BytesPlutusData(point)); } return JulcScriptLoader.load(SpendBoundGroth16Verifier.class, new BytesPlutusData(vk.alpha()), new BytesPlutusData(vk.beta()), new BytesPlutusData(vk.gamma()), new BytesPlutusData(vk.delta()), ic); } static PlutusData redeemer(SnarkjsToCardano.ProofCompressed p) { return ConstrPlutusData.builder() .alternative(0) .data(ListPlutusData.of( new BytesPlutusData(p.piA()), new BytesPlutusData(p.piB()), new BytesPlutusData(p.piC()))) .build(); } } ``` 5. **Run it** with DevKit running: `gradle runBound` (or `./gradlew runBound`). Expect something like this (hashes shortened; there may be `[PENDING]` lines, and the exact rejection text may vary): ```text [SUBMITTED] Tx: d050405f... [CONFIRMED] Tx: d050405f... [SUBMITTED] Tx: 05355fba... [CONFIRMED] Tx: 05355fba... Unlock the bound UTxO: unlocked in tx 05355fba... [SUBMITTED] Tx: 4f303bfb... [CONFIRMED] Tx: 4f303bfb... Replay on another UTxO: rejected (Error while evaluating script cost) ``` The second UTxO is still spendable, just not with the first UTxO's proof: a fresh proof made for *its* out-ref unlocks it. Checking the proof costs about 3.0 billion CPU steps and 263,000 memory units in the JuLC VM, a little more than the bare verifier because of the extra public input and the hash. Test a validator like this with failing cases as well as the happy path. For this one we checked, in the JuLC VM, that each of these is rejected: the same proof on another UTxO, a tampered datum (`[3, 34]`), a proof made for a different `spendRef`, a datum that already includes `spendRef`, and a proof with its points swapped. #### What else to bind Binding to a UTxO is one ingredient. It stops a proof from being reused on *other* UTxOs, but not on the one it was made for: someone who copies the redeemer from your pending unlock transaction can submit their own transaction that spends the same UTxO to their address, and whichever lands first wins. Before a ZK validator guards anything of value, it typically also needs: - **Output policy and recipient binding**: put the recipient in the statement, as a public input that takes part in a constraint, and have the validator check that an output pays them. A copied proof then can't redirect the funds, which is what stops front-running. - **Replay protection** that fits the application: a UTxO binding like the one above, or [nullifiers](https://zeroj.dev/tutorials/private-allowlist/) recorded on-chain. - **Authorization**: whose signature is required, and whose proof counts. - **State binding**: which roots, epochs or deadlines the public inputs must match. [On-chain verification](https://zeroj.dev/guides/verifying/on-chain/) covers the validators and libraries in detail. > **Caution: Development keys only** > > This tutorial's verification key comes from the in-process development setup, whose toxic > waste the program knew. Anyone holding it could forge proofs for any public inputs. On-chain > keys for anything beyond a devnet must come from a multi-party ceremony; see > [Running a trusted setup ceremony](https://zeroj.dev/guides/proving/trusted-setup-ceremony/). ZeroJ's on-chain > Groth16 path is rated **Beta, testnet only**: not externally audited and not for value-bearing > use. See [Status & maturity](https://zeroj.dev/start/status/). ### Try this - Change the datum to `[3, 34]` before locking. The lock succeeds (the chain doesn't check datums), but the unlock fails while the transaction is being built, because the validator rejects the proof for those inputs. - Print `JulcScriptLoader.scriptHash(Groth16BLS12381Verifier.class, ...)` with the same parameters and compare it with the address: the address is built from that hash. - Run the program twice. Each run makes new development keys, so each run has a different script address. ### Next steps - [On-chain verification](https://zeroj.dev/guides/verifying/on-chain/): the validator family, `Groth16BLS12381Lib`, and budgets. - [Application security](https://zeroj.dev/guides/verifying/application-security/): binding proofs to context, replay protection and authorization. - [Bring circom & snarkjs circuits](https://zeroj.dev/tutorials/snarkjs-interop/): prove circuits written for the snarkjs toolchain with ZeroJ. - [Private voting](https://zeroj.dev/use-cases/private-voting/): nullifiers and validators in a complete application. --- ## Bring circom & snarkjs circuits Source: https://zeroj.dev/tutorials/snarkjs-interop/ > Prove a circom circuit with ZeroJ's pure-Java prover using snarkjs keys, and hand ZeroJ proofs to snarkjs. Groth16 on BLS12-381, both directions. Maybe you already have circuits written in [circom](https://docs.circom.io/), or keys from a snarkjs trusted-setup ceremony. You don't have to rewrite anything to use ZeroJ. Groth16 on BLS12-381 speaks the same formats in both directions: ```text circom + snarkjs ZeroJ (pure Java) ---------------- ----------------- .zkey (proving key) ──── import ────► prove without Node.js .wtns (witness) ──── import ────► verification_key.json ◄─── same JSON ───► verify snarkjs or ZeroJ proofs proof.json, public.json ◄─── same JSON ───► export for `snarkjs groth16 verify` ``` **What you'll build:** a tiny circom circuit set up with snarkjs, proved by ZeroJ's pure-Java prover; and a ZeroJ circuit whose proof snarkjs verifies. **What you'll learn:** - how to compile circom for BLS12-381 and run a snarkjs Groth16 setup - how to import `.zkey` and `.wtns` files and prove in Java - how snarkjs orders public signals - how to export ZeroJ keys and proofs as snarkjs JSON **Note: Prerequisites:** - Java 25 and Gradle (see [Installation](https://zeroj.dev/start/installation/)). - Node.js, [circom](https://docs.circom.io/getting-started/installation/) 2.x and snarkjs on your `PATH` (`npm install -g snarkjs`). ZeroJ's interoperability CI pins **snarkjs 0.7.6**; this page was checked with snarkjs 0.7.6 and circom 2.2.3. ### Set up the Java project Both directions share one small project. `Main` picks the direction from its first argument, and the other Java files follow in the two sections below; create all of them before the first run. - zeroj-snarkjs-interop/ - settings.gradle - build.gradle - circuit/ - multiplier.circom - input.json - src/main/java/com/example/interop/ - Main.java - FromSnarkjs.java - ToSnarkjs.java - SecretMultiplier.java ```groovy title="settings.gradle" rootProject.name = 'zeroj-snarkjs-interop' ``` ```groovy title="build.gradle" plugins { id 'application' } repositories { mavenCentral() } java { toolchain { languageVersion = JavaLanguageVersion.of(25) } } dependencies { implementation platform('org.zeroj:zeroj-bom-core:0.1.0-pre12') annotationProcessor platform('org.zeroj:zeroj-bom-core:0.1.0-pre12') implementation 'org.zeroj:zeroj-circuit-annotation-api' annotationProcessor 'org.zeroj:zeroj-circuit-annotation-processor' implementation 'org.zeroj:zeroj-circuit-dsl' implementation 'org.zeroj:zeroj-crypto' implementation 'org.zeroj:zeroj-codec' implementation 'org.zeroj:zeroj-verifier-groth16' } application { mainClass = 'com.example.interop.Main' // Dev-only: needed by the ZeroJ -> snarkjs direction, which runs an in-process setup. applicationDefaultJvmArgs = ['-Dzeroj.allowInsecureTrustedSetup=true'] } ``` ```java title="src/main/java/com/example/interop/Main.java" package com.example.interop; import org.zeroj.api.CircuitId; import org.zeroj.api.CurveId; import org.zeroj.api.ProofSystemId; import org.zeroj.api.VerificationMaterial; import org.zeroj.codec.SnarkjsJsonCodec; import org.zeroj.verifier.groth16.bls12381.Groth16BLS12381PureJavaVerifier; import java.nio.charset.StandardCharsets; import java.nio.file.Path; public class Main { public static void main(String[] args) throws Exception { Path dir = Path.of(args[1]); switch (args[0]) { case "from-snarkjs" -> FromSnarkjs.run(dir); case "to-snarkjs" -> ToSnarkjs.run(dir); default -> throw new IllegalArgumentException("usage: from-snarkjs|to-snarkjs "); } } /** Pure-Java Groth16 BLS12-381 verification of snarkjs-format JSON. */ static boolean verify(String vkJson, String proofJson, String publicJson) { var id = new CircuitId("multiplier"); var envelope = SnarkjsJsonCodec.toEnvelopeFromJson(proofJson, vkJson, publicJson, id); var material = VerificationMaterial.of(vkJson.getBytes(StandardCharsets.UTF_8), ProofSystemId.GROTH16, CurveId.BLS12_381, id); return new Groth16BLS12381PureJavaVerifier().verify(envelope, material).proofValid(); } } ``` ### Direction 1: circom and snarkjs in, ZeroJ proves 1. **Write the circom circuit** and its input in the `circuit/` directory. It's the same statement as the [Quickstart](https://zeroj.dev/start/quickstart/): *"I know `b` such that `a × b = c`."* ```text title="circuit/multiplier.circom" pragma circom 2.0.0; // "I know a secret b such that a * b = c." a and c are public. template Multiplier() { signal input a; signal input b; signal output c; c <== a * b; } component main {public [a]} = Multiplier(); ``` ```json title="circuit/input.json" { "a": "3", "b": "11" } ``` 2. **Compile for BLS12-381 and compute the witness.** circom targets BN254 by default; Cardano needs `--prime bls12381`. ```bash cd circuit circom multiplier.circom --r1cs --wasm --sym --prime bls12381 node multiplier_js/generate_witness.js multiplier_js/multiplier.wasm input.json witness.wtns ``` `witness.wtns` contains every signal, including the secret `b`. Treat it like a private key: don't commit it or ship it anywhere. 3. **Run a Groth16 setup with snarkjs.** Phase 1 is the universal "powers of tau", phase 2 is specific to this circuit. Each `contribute` mixes in randomness. ```bash # Phase 1 (dev-only, tiny): 2^8 constraints is plenty here snarkjs powersoftau new bls12-381 8 pot_0000.ptau snarkjs powersoftau contribute pot_0000.ptau pot_0001.ptau --name="dev contribution" -e="some random text" snarkjs powersoftau prepare phase2 pot_0001.ptau pot_final.ptau # Phase 2: circuit-specific keys snarkjs groth16 setup multiplier.r1cs pot_final.ptau multiplier_0000.zkey snarkjs zkey contribute multiplier_0000.zkey multiplier.zkey --name="dev contribution" -e="more random text" snarkjs zkey export verificationkey multiplier.zkey verification_key.json ``` For comparison later, also let snarkjs make its own proof: ```bash snarkjs groth16 prove multiplier.zkey witness.wtns proof.json public.json snarkjs groth16 verify verification_key.json public.json proof.json cd .. ``` The last command prints `[INFO] snarkJS: OK!`. **Caution: A one-person ceremony is still a dev setup:** This ceremony has a single contributor, you, so you could have kept the toxic waste. It's fine for learning. Production keys need a real multi-party ceremony with independent contributors and a public transcript; see [Running a trusted setup ceremony](https://zeroj.dev/guides/proving/trusted-setup-ceremony/). 4. **Prove in Java.** `ZkeyImporterBLS381` reads the snarkjs proving key (with the circuit's constraints, which a `.zkey` carries) and the witness. After that no Node.js is involved. ```java title="src/main/java/com/example/interop/FromSnarkjs.java" package com.example.interop; import org.zeroj.codec.SnarkjsJsonCodec; import org.zeroj.crypto.groth16.Groth16ProverBLS381; import org.zeroj.crypto.groth16.ZkeyImporterBLS381; import org.zeroj.crypto.snarkjs.SnarkjsGroth16Json; import java.io.IOException; import java.math.BigInteger; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; /** circom + snarkjs artifacts in, pure-Java proof out. */ public class FromSnarkjs { static void run(Path dir) throws IOException { // 1. Load what circom and snarkjs produced. var zkey = ZkeyImporterBLS381.importZkeyFull(Files.readAllBytes(dir.resolve("multiplier.zkey"))); BigInteger[] witness; try (var in = Files.newInputStream(dir.resolve("witness.wtns"))) { witness = ZkeyImporterBLS381.importWtns(in); } String vkJson = Files.readString(dir.resolve("verification_key.json")); int nPublic = SnarkjsJsonCodec.parseVerificationKey(vkJson).nPublic(); // 2. Prove with the pure-Java prover. No Node.js involved from here on. var proof = Groth16ProverBLS381.prove( zkey.provingKey(), witness, zkey.constraints(), zkey.numWires()); BigInteger[] publicInputs = Arrays.copyOfRange(witness, 1, 1 + nPublic); String proofJson = SnarkjsGroth16Json.proofJson(proof); String publicJson = SnarkjsGroth16Json.publicJson(publicInputs); Files.writeString(dir.resolve("zeroj-proof.json"), proofJson); Files.writeString(dir.resolve("zeroj-public.json"), publicJson); System.out.println("Public signals (snarkjs order): " + Arrays.toString(publicInputs)); // 3. Verify ZeroJ's proof against snarkjs' own verification key. System.out.println("ZeroJ proof + snarkjs VK -> " + Main.verify(vkJson, proofJson, publicJson)); // 4. Verify the proof snarkjs generated, with ZeroJ's verifier. String snarkjsProof = Files.readString(dir.resolve("proof.json")); String snarkjsPublic = Files.readString(dir.resolve("public.json")); System.out.println("snarkjs proof + ZeroJ verifier -> " + Main.verify(vkJson, snarkjsProof, snarkjsPublic)); } } ``` 5. **Run it** from the project directory. `Main` also refers to `ToSnarkjs`, so add the two files from [Direction 2](#direction-2-zeroj-proves-snarkjs-verifies) first: ```bash gradle run --args='from-snarkjs circuit' ``` ```text Public signals (snarkjs order): [33, 3] ZeroJ proof + snarkjs VK -> true snarkjs proof + ZeroJ verifier -> true ``` Now close the loop and let snarkjs judge ZeroJ's proof: ```bash snarkjs groth16 verify circuit/verification_key.json circuit/zeroj-public.json circuit/zeroj-proof.json ``` ```text [INFO] snarkJS: OK! ``` #### Public signal order Look at the public signals: `[33, 3]`, not `[3, 33]`. snarkjs lists a circuit's **outputs first**, then its public inputs, in declaration order. Here `c` is an output and `a` a public input. In the witness they sit right after the constant `1`, which is why `FromSnarkjs` takes elements `1` to `nPublic`. Anything that consumes these values, such as a Cardano datum, must use exactly this order. #### Importing keys you care about - **Pin the key.** A `.zkey` from a ceremony is a security-critical input. Use the `ZkeyImporterBLS381.importZkeyFull(bytes, expectedSha256)` overload so the import fails unless the file matches the hash published with the ceremony transcript. - **Large circuits.** `importZkeyFull` loads the whole key into the heap. For big keys, `ZkeyPkStoreImporter.importToPkStore(zkeyFile, dir)` converts the `.zkey` once into a memory-mapped key store that you open with `Groth16Keys.load(dir)`. The [Groth16 guide](https://zeroj.dev/guides/proving/groth16/) shows that flow, including the extra argument that snarkjs-made keys need at prove time. - **The witness must match the key.** A `.zkey` is made for one exact constraint system, so the witness must come from that same circuit, here circom's generated witness calculator. Rewriting the circuit in ZeroJ's DSL gives a different constraint system, which needs its own setup. ### Direction 2: ZeroJ proves, snarkjs verifies Any ZeroJ Groth16 proof can be exported in snarkjs' JSON format. `SnarkjsGroth16Json` writes the same bytes snarkjs 0.7.6 writes, so any tool that reads snarkjs files can check ZeroJ proofs. 1. **Add the Quickstart circuit** to the project. ```java title="src/main/java/com/example/interop/SecretMultiplier.java" package com.example.interop; import org.zeroj.circuit.annotation.Prove; import org.zeroj.circuit.annotation.Public; import org.zeroj.circuit.annotation.Secret; import org.zeroj.circuit.annotation.ZKCircuit; import org.zeroj.circuit.annotation.ZkBool; import org.zeroj.circuit.annotation.ZkContext; import org.zeroj.circuit.annotation.ZkField; /** "I know a secret b such that a × b = product." */ @ZKCircuit(name = "secret-multiplier", version = 1) public class SecretMultiplier { @Prove ZkBool prove(ZkContext zk, @Public ZkField a, @Public ZkField product, @Secret ZkField b) { return a.mul(b).isEqual(product); } } ``` 2. **Prove and export.** `SnarkjsGroth16Json` produces the three files snarkjs expects. ```java title="src/main/java/com/example/interop/ToSnarkjs.java" package com.example.interop; import org.zeroj.api.CurveId; import org.zeroj.crypto.groth16.Groth16Keys; import org.zeroj.crypto.setup.PowersOfTauBLS381; import org.zeroj.crypto.snarkjs.SnarkjsGroth16Json; import java.io.IOException; import java.math.BigInteger; import java.nio.file.Files; import java.nio.file.Path; /** A ZeroJ circuit, proved in Java, exported in exactly the files snarkjs expects. */ public class ToSnarkjs { static void run(Path dir) throws IOException { var circuit = SecretMultiplierCircuit.build(); var r1cs = circuit.compileR1CS(CurveId.BLS12_381); var inputs = SecretMultiplierCircuit.inputs().a(3).product(33).b(11); BigInteger[] witness = inputs.calculateWitness(circuit, CurveId.BLS12_381); BigInteger tau = PowersOfTauBLS381.generate(4).tauScalar(); // DEV-ONLY setup try (var keys = Groth16Keys.setupInMemory( r1cs.constraints(), r1cs.numWires(), r1cs.numPublicInputs(), tau)) { var proof = keys.prove(witness, r1cs.constraints()); Files.createDirectories(dir); Files.writeString(dir.resolve("verification_key.json"), SnarkjsGroth16Json.verificationKeyJson(keys)); Files.writeString(dir.resolve("proof.json"), SnarkjsGroth16Json.proofJson(proof)); Files.writeString(dir.resolve("public.json"), SnarkjsGroth16Json.publicJson(inputs.publicValues().toArray(BigInteger[]::new))); } System.out.println("Wrote verification_key.json, proof.json, public.json to " + dir); } } ``` 3. **Run it, then verify with snarkjs.** ```bash gradle run --args='to-snarkjs zeroj-out' cd zeroj-out snarkjs groth16 verify verification_key.json public.json proof.json ``` ```text [INFO] snarkJS: OK! ``` 4. **Tamper and check again.** Claim the product is 34: ```bash echo '["3","34"]' > tampered-public.json snarkjs groth16 verify verification_key.json tampered-public.json proof.json ``` ```text [ERROR] snarkJS: Invalid proof ``` This direction uses ZeroJ's in-process setup, which is **development-only** (hence the `-Dzeroj.allowInsecureTrustedSetup=true` flag in `build.gradle`). Exporting its key to snarkjs doesn't make it any more trustworthy. For keys you'll rely on, run a multi-party ceremony and import the resulting `.zkey`, as in Direction 1. > **Note: What about PlonK?** > > ZeroJ also has an exporter for snarkjs-format PlonK files, but PlonK support is > **experimental** in ZeroJ. This tutorial, and the current release, focus on Groth16. ### Where the formats meet Cardano snarkjs artifacts can go on-chain too. `SnarkjsToCardano` in `zeroj-onchain-julc` converts `verification_key.json` and `proof.json` into the compressed parameters and redeemer that the Plutus V3 Groth16 verifier takes, the same shapes that [Verify your proof on Cardano](https://zeroj.dev/tutorials/verify-on-cardano/) builds from a ZeroJ proof. The public inputs go into the datum in snarkjs' order, outputs first. The warnings on that page about binding proofs to their context apply here unchanged. ### Try this - Change `input.json` to `{ "a": "3", "b": "12" }`, regenerate the witness, and prove again in Java. The proof is valid, for the public signals `[36, 3]`: circom computed `c` for you. To reject a wrong product, `c` would have to be a public *input* that the circuit constrains, not an output. - Edit `circuit/zeroj-public.json` to `["33","4"]` and run the `snarkjs groth16 verify` command from Direction 1 again. It prints `Invalid proof`. ### Next steps - [Groth16 guide](https://zeroj.dev/guides/proving/groth16/): key stores and proving with imported ceremony keys. - [Running a trusted setup ceremony](https://zeroj.dev/guides/proving/trusted-setup-ceremony/): replace the dev setup with a real one. - [Off-chain verification](https://zeroj.dev/guides/verifying/off-chain/): verifier backends and envelopes. - [Verify your proof on Cardano](https://zeroj.dev/tutorials/verify-on-cardano/): take a Groth16 proof on-chain. --- ## Write circuits with annotations Source: https://zeroj.dev/guides/circuits/annotations/ > Author ZeroJ circuits as annotated Java classes, and use the generated companion to build, fill inputs, compute witnesses, and wire proofs. Annotations are the recommended way to write ZeroJ circuits. You write an ordinary Java class, mark its inputs `@Public` or `@Secret`, and put the rules in one `@Prove` method. At compile time, the annotation processor generates a companion class (`MyCircuit` gives you `MyCircuitCircuit`) that builds a normal ZeroJ circuit, describes its inputs, and fills witnesses for you. Nothing about proving changes. The companion's `build()` returns the same `CircuitBuilder` that the lower-level DSL produces, so everything in [Prove with Groth16](https://zeroj.dev/guides/proving/groth16/) applies unchanged. ### Set up the processor You need `zeroj-circuit-annotation-api` on the compile classpath, `zeroj-circuit-annotation-processor` on the annotation processor path, and usually `zeroj-circuit-lib` for gadgets such as Poseidon: ```groovy title="build.gradle" dependencies { implementation platform('org.zeroj:zeroj-bom-core:0.1.0-pre12') annotationProcessor platform('org.zeroj:zeroj-bom-core:0.1.0-pre12') implementation 'org.zeroj:zeroj-circuit-annotation-api' annotationProcessor 'org.zeroj:zeroj-circuit-annotation-processor' implementation 'org.zeroj:zeroj-circuit-lib' } ``` Maven and Kotlin DSL versions are on [Installation](https://zeroj.dev/start/installation/). If your circuits live in test sources, use `testAnnotationProcessor` too. ### Anatomy of an annotated circuit This sealed-bid circuit proves "my hidden bid matches the public commitment, and it is at least the reserve price" without revealing the bid: ```java title="SealedBid.java" import org.zeroj.circuit.annotation.Prove; import org.zeroj.circuit.annotation.Public; import org.zeroj.circuit.annotation.Secret; import org.zeroj.circuit.annotation.UInt; import org.zeroj.circuit.annotation.ZKCircuit; import org.zeroj.circuit.annotation.ZkBool; import org.zeroj.circuit.annotation.ZkContext; import org.zeroj.circuit.annotation.ZkField; import org.zeroj.circuit.annotation.ZkUInt; import org.zeroj.circuit.lib.poseidon.PoseidonParamsBLS12_381T3; import org.zeroj.circuit.lib.zk.ZkPoseidon; @ZKCircuit(name = "sealed-bid", version = 1) public class SealedBid { @Prove ZkBool prove( ZkContext zk, @Public ZkField bidCommitment, @Public @UInt(bits = 64) ZkUInt reservePrice, @Secret @UInt(bits = 64) ZkUInt bidAmount, @Secret ZkField salt) { var commitmentMatches = ZkPoseidon.hash( zk, PoseidonParamsBLS12_381T3.INSTANCE, bidAmount.asField(), salt) .isEqual(bidCommitment); return commitmentMatches.and(bidAmount.gte(reservePrice)); } } ``` A few things to notice: - The method body doesn't compute an answer. It *describes constraints*. `ZkField`, `ZkUInt` and `ZkBool` are symbolic values: wires in a circuit, not numbers. - Returning a `ZkBool` makes the generated code assert that it is true. A witness that makes it false can't be proven. - `ZkContext` is optional. Declare it when you call gadgets that need the circuit context. - For Cardano, pass `PoseidonParamsBLS12_381T3.INSTANCE` to every Poseidon call. See [Gadget library](https://zeroj.dev/guides/circuits/gadgets/). ### Annotations | Annotation | Where | What it does | |------------|-------|--------------| | `@ZKCircuit(name, nameTemplate, version)` | class | Marks the circuit. `name` defaults to the class name, `version` defaults to 1 and must be positive, `nameTemplate` is for parameterized circuits. | | `@Prove` | method | The single method that defines the constraints. Exactly one per class, not `private`. Returns `ZkBool` or `void`. | | `@Public(name = "...")` / `@Secret(name = "...")` | field or parameter | Visibility. Exactly one is required on every input. `name` overrides the generated input name. | | `@UInt(bits = N)` | `ZkUInt` input (or arrays of it) | Declares the width, 1 to 253. Required on every `ZkUInt` input. | | `@FixedSize(value)` / `@FixedSize(param = "...")` | `ZkArray`, `ZkBits`, `ZkBytes` | Fixed length, as a literal or a `@CircuitParam` name. Add `inner` / `innerParam` for `ZkArray>`. | | `@CircuitParam("name")` | constructor parameter | A build-time value that changes the circuit's shape (depth, size, mode). | | `@Order(n)` | field | Overrides declaration order in field style. Values must be unique per visibility. | | `@FieldElement` | `ZkField` input | Optional marker that documents "raw field element". The processor only checks it sits on a `ZkField`. | ### Symbolic types | Type | Input constraints added | Key operations | |------|-------------------------|----------------| | `ZkField` | none | `add`, `sub`, `mul`, `div`, `isEqual`, `assertEqual` | | `ZkBool` | value is 0 or 1 | `and`, `or`, `xor`, `not`, `select(a, b)`, `isEqual`, `assertTrue`, `assertFalse`, `asField` | | `ZkUInt` | value < 2^bits (range check) | `add`, `sub`, `mul`, `lt`, `lte`, `gt`, `gte`, `inRange`, `isEqual`, `assertEqual`, `asField`, `bits` | | `ZkArray` | per element, by element type | `get(i)`, `size()`, `values()` | | `ZkBits` | each element boolean | `get(i)`, `size()`, `isEqual`, `assertEqual` | | `ZkBytes` | each element 8 bits | `get(i)` (a `ZkUInt`), `size()`, `isEqual`, `assertEqual` | Semantics worth knowing: - **`ZkField` arithmetic wraps modulo the circuit's field prime** (the BLS12-381 scalar field for Cardano). Use `ZkUInt` for anything that behaves like an amount, a count, or an age. - **`ZkUInt` keeps you honest about widths.** `add` and `mul` widen the result (`add` of two 64-bit values is 65 bits, `mul` sums the widths), and fail at circuit-build time if the result would exceed 253 bits. `sub` range-constrains its result, so a negative difference makes the witness unsatisfiable instead of wrapping. Comparisons need widths below 253. - **`ZkContext`** gives you constants: `zk.constant(18)` or `zk.constant(BigInteger)` return a `ZkField`. ### Field style and parameter style You can declare inputs as fields or as `@Prove` parameters. Pick one per class; mixing them is a compile error. ```java title="Field style" @ZKCircuit(name = "range-proof") public class RangeProof { @Secret @UInt(bits = 16) ZkUInt secret; @Public @UInt(bits = 16) ZkUInt lo; @Public @UInt(bits = 16) ZkUInt hi; @Prove ZkBool inRange() { return secret.gte(lo).and(secret.lte(hi)); } } ``` ```java title="Parameter style" @ZKCircuit(name = "age-verification") public class AgeVerification { @Prove ZkBool prove(@Secret @UInt(bits = 8) ZkUInt age, @Public @UInt(bits = 8) ZkUInt threshold) { return age.gte(threshold); } } ``` Field style is concise. Parameter style keeps every dependency in the method signature and is required for `static` `@Prove` methods. Symbolic input fields must be neither `private` nor `final`, and a non-static circuit needs a visible no-argument constructor unless it has a `@CircuitParam` constructor. When there is no natural boolean result, declare `void` and assert explicitly: ```java @Prove void prove(ZkContext zk, @Secret @UInt(bits = 16) ZkUInt value, @Secret @UInt(bits = 16) ZkUInt blinding, @Public ZkField expectedU, @Public ZkField expectedV) { ZkPedersen.commit(zk, value, blinding, 16) .assertAffineEquals(zk, expectedU, expectedV); } ``` ### Input order and names The order of public inputs is part of your verification key's contract, so know how it's decided: 1. All public inputs come before all secret inputs. 2. Within each group, inputs keep declaration order. In field style, `@Order` values come first (ascending), then unannotated fields in declaration order. 3. Array inputs are flattened with a singular base name: `siblings` becomes `sibling_0`, `sibling_1`, and so on. A matrix `measurements` becomes `measurement_0_0`, `measurement_0_1`, row-major. The generated `schema().publicInputs().names()` is the source of truth. Assert it in a test (see [Test your circuits](https://zeroj.dev/guides/circuits/testing-circuits/)) so a refactor can't silently reorder your public inputs. ### Parameterized circuits Constructor parameters annotated `@CircuitParam` make the circuit a template. Supported types are primitive or boxed integral, boolean and char types, `String`, `BigInteger`, and enums. Integer parameters can size arrays through `@FixedSize(param = ...)`: ```java @ZKCircuit(name = "merkle-bls12-381", nameTemplate = "merkle-bls-d{depth}") public class MerkleMembership { public MerkleMembership(@CircuitParam("depth") int depth) { } @Prove ZkBool prove(ZkContext zk, @Secret ZkField leaf, @Public ZkField root, @Secret @FixedSize(param = "depth") ZkArray siblings, @Secret @FixedSize(param = "depth") ZkArray pathBits) { return ZkMerkle.isMemberPoseidon(zk, PoseidonParamsBLS12_381T3.INSTANCE, leaf, root, siblings, pathBits); } } ``` Every generated method then takes the parameters: ```java var circuit = MerkleMembershipCircuit.build(20); var inputs = MerkleMembershipCircuit.inputs(20); var schema = MerkleMembershipCircuit.schema(20); ``` The generated circuit name is the rendered template plus a canonical parameter suffix, for example `merkle-bls-d2--depth-1:2` for depth 2. The suffix prevents two parameter sets from ever sharing a name. > **Caution: One parameter set, one key** > > Each parameter set is a different circuit with a different constraint system. It needs its own > proving key, verification key, and, for real deployments, its own ceremony. Track the circuit > name, `@ZKCircuit(version)`, and parameters together in your key registry. > `metadata().envelopeMetadata()` carries all three. #### Rectangular matrices Two-dimensional inputs are supported when both dimensions are fixed: ```java @Secret @UInt(bits = 16) @FixedSize(param = "rows", innerParam = "cols") ZkArray> measurements ``` The input builder takes a `List>` and rejects ragged rows. Deeper nesting isn't supported; flatten it to parallel arrays. ### The generated companion For a class `X`, the processor generates `XCircuit` with these static members. For parameterized circuits, the methods marked with ✱ take the `@CircuitParam` values as arguments. | Member | Returns | Use it to | |--------|---------|-----------| | `CIRCUIT_NAME`, `CIRCUIT_VERSION`, `CIRCUIT_NAME_TEMPLATE` | constants | Identify the circuit (the template constant exists only with `nameTemplate`) | | One `String` constant per input, such as `BID_AMOUNT` | constants | Refer to input names without string literals | | `build(...)` ✱ | `CircuitBuilder` | Compile (`compileR1CS`) and compute witnesses | | `schema(...)` ✱ | `ZkCircuitSchema` | Inspect names, order, widths, and dimensions | | `inputs(...)` ✱ | `XCircuit.Inputs` | Fill input values fluently | | `circuitId(...)` ✱ | `CircuitId` | Label envelopes and key registries | | `metadata(...)` ✱ | `ZkCircuitMetadata` | Name, version, and parameters for envelopes | | `calculateWitness(circuit, inputs, curve)` | `BigInteger[]` | Compute the full witness | | `publicInputs(inputs)` | `List` | Public values in schema order | | `publicInputValues(inputs)` | `PublicInputs` | The same, as the envelope type | | `proofEnvelopeBuilder(circuit, proofSystem, curve, proofBytes, inputs, vkRef)` | `ZkProofEnvelope.Builder` | Wrap proof bytes with the right circuit ID, public inputs, and metadata | `Inputs` has one setter per input. Scalars accept `BigInteger` or `long`. Arrays accept `(index, value)` or a whole `List`, and matrices accept `(row, col, value)` or a list of rows. It also offers `toWitnessMap()`, `publicValues()`, `toPublicInputs()`, `calculateWitness(circuit, curve)`, and `schema()`. Putting it together: ```java var circuit = SealedBidCircuit.build(); BigInteger bid = BigInteger.valueOf(100); BigInteger salt = new BigInteger("88001"); BigInteger commitment = PoseidonHash.hash(PoseidonParamsBLS12_381T3.INSTANCE, bid, salt); var inputs = SealedBidCircuit.inputs() .bidCommitment(commitment) .reservePrice(75) .bidAmount(bid) .salt(salt); var r1cs = circuit.compileR1CS(CurveId.BLS12_381); BigInteger[] witness = inputs.calculateWitness(circuit, CurveId.BLS12_381); // witness[0] == 1 List publicValues = inputs.publicValues(); // [commitment, 75] ``` `PoseidonHash` (in `org.zeroj.circuit.lib.poseidon`) computes the same hash outside the circuit, which is how you produce public values such as commitments. Use a large random salt in real code. From here, `r1cs` and `witness` go straight into [Groth16 proving](https://zeroj.dev/guides/proving/groth16/). After you have a proof, `proofEnvelopeBuilder(...)` checks that the circuit you pass has the name the inputs were generated for, then builds an envelope with the generated `CircuitId`, the public inputs in schema order, and the circuit metadata. ### Authoring rules These rules are what keep an annotated circuit sound: - **No Java control flow on circuit values.** `ZkBool` is not a Java `boolean`, so `if`, `&&` and `||` over secrets don't compile. That's deliberate: a circuit can't branch. Use `and`, `or`, `not`, and `select(ifTrue, ifFalse)`, which evaluate both sides and pick one with a constraint. - **Java loops over shape values are fine.** Loops over `@CircuitParam` sizes or constants just unroll into more constraints. - **A `ZkBool` you compute and then drop constrains nothing.** In a `void` method, call `assertTrue()`, `assertEqual(...)`, or an asserting gadget, or return the `ZkBool`. - **Range-check everything that is really a number.** Use `ZkUInt` with the tightest honest `@UInt` width, and never compare raw `ZkField` values as if they were integers. - **Use explicit BLS12-381 Poseidon parameters.** The no-parameter overloads are BN254 oriented, and the circuit refuses to compile for BLS12-381 if the fields don't match. - **Test with invalid witnesses.** An honest witness that passes proves very little. See [Test your circuits for soundness](https://zeroj.dev/guides/circuits/testing-circuits/). When the symbolic types can't express something, drop to `CircuitSpec` and the `Signal` API. See [CircuitSpec & the Signal DSL](https://zeroj.dev/guides/circuits/circuit-dsl/). ### Current limits - Nested (inner) `@ZKCircuit` classes, `private` `@Prove` methods, and `private` or `final` input fields aren't supported. - `static` `@Prove` methods must use parameter style. - `@CircuitParam` belongs on constructor parameters, not on `@Prove` parameters, and a class can have only one `@CircuitParam` constructor. - `ZkArray` elements must be `ZkField`, `ZkBool`, `ZkUInt`, or one nested `ZkArray` of those. - `ZkBits` and `ZkBytes` store one constrained field element per bit or byte. Packed encodings aren't available yet, and symbolic bitwise operations on `ZkBits` are limited. - `ZkMiMC` and `ZkMerkle.HashType.MIMC` are BN254-only, so they aren't usable for Cardano circuits. ### Next steps - [Gadget library](https://zeroj.dev/guides/circuits/gadgets/): Poseidon, Merkle, comparators, Jubjub, and more - [Test your circuits for soundness](https://zeroj.dev/guides/circuits/testing-circuits/) - [Prove with Groth16](https://zeroj.dev/guides/proving/groth16/) --- ## CircuitSpec & the Signal DSL Source: https://zeroj.dev/guides/circuits/circuit-dsl/ > Write circuits directly with CircuitSpec, Signal and the inline CircuitAPI DSL, compile them, compute witnesses, and use hints without breaking soundness. Annotated circuits are built on a lower layer that you can use directly: `CircuitBuilder`, the object-oriented `Signal` API, and the functional `CircuitAPI`. This page covers when to drop down to that layer, how to use it, and how prover *hints* work without making your circuit forgeable. Everything here lives in `zeroj-circuit-dsl` (package `org.zeroj.circuit`). Gadgets such as Poseidon and comparators come from `zeroj-circuit-lib`. ### Which layer should you use? | Style | Entry point | Use it when | |-------|-------------|-------------| | Annotations | `@ZKCircuit` + generated `*Circuit` | New application circuits. See [Write circuits with annotations](https://zeroj.dev/guides/circuits/annotations/). | | `CircuitSpec` + `Signal` | `CircuitBuilder.defineSignals(spec)` | You want a reusable circuit class without the annotation processor, need `Signal`-level gadgets that have no `Zk*` adapter, or are porting a circom-style circuit. | | Inline `CircuitAPI` | `CircuitBuilder.define(api -> ...)` | Small tests, quick experiments, and building gadgets on raw `Variable`s. | All three produce the same kind of `CircuitBuilder`, so compiling, witness calculation, and proving are identical from there on. ### CircuitSpec with the Signal API A `CircuitSpec` is a class with one method, `define(SignalBuilder c)`. You declare the input *layout* on the `CircuitBuilder`, then fetch the same names inside `define`: ```java title="HashCommitmentCircuit.java" import org.zeroj.circuit.CircuitBuilder; import org.zeroj.circuit.CircuitSpec; import org.zeroj.circuit.Signal; import org.zeroj.circuit.SignalBuilder; import org.zeroj.circuit.lib.SignalPoseidon; import org.zeroj.circuit.lib.poseidon.PoseidonParamsBLS12_381T3; public class HashCommitmentCircuit implements CircuitSpec { @Override public void define(SignalBuilder c) { Signal secret = c.privateInput("secret"); Signal salt = c.privateInput("salt"); Signal commitment = c.publicOutput("commitment"); Signal hash = SignalPoseidon.hash(c, PoseidonParamsBLS12_381T3.INSTANCE, secret, salt); c.assertEqual(hash, commitment); } public static CircuitBuilder build() { return CircuitBuilder.create("hash-commitment") .publicVar("commitment") .secretVar("secret") .secretVar("salt") .defineSignals(new HashCommitmentCircuit()); } } ``` The rules that trip people up: - **Names must match.** `c.publicInput("x")` / `c.publicOutput("x")` require an earlier `.publicVar("x")`, and `c.privateInput("y")` requires `.secretVar("y")`. An unknown name, or a name requested with the wrong visibility, throws `IllegalArgumentException` while the circuit is being defined. - **`publicOutput` is just a public input.** The name documents intent. The prover still supplies the value, and only your constraints make it correct. - **Declaration order is public-input order.** Wire 0 is the constant `1`, then every `publicVar` in declaration order, then every `secretVar`. Verifiers receive public inputs in exactly this order. - **Define once.** `define` / `defineSignals` build the constraint graph and freeze that definition. A symbolic value that escapes the block and later tries to add constraints fails loudly instead of silently constraining nothing. Constructor parameters replace circom template parameters: a `MerkleCircuit(int depth)` can loop `depth` times in `define` and declare `sibling_0 ... sibling_{depth-1}` in its `build(depth)`. ### The inline lambda DSL For tests and experiments, define the constraints in place with the functional `CircuitAPI`, where values are `Variable`s and operations are methods on `api`: ```java var circuit = CircuitBuilder.create("multiplier") .publicVar("c").secretVar("a").secretVar("b") .define(api -> api.assertEqual(api.mul(api.var("a"), api.var("b")), api.var("c"))); ``` `defineSignals` also takes a lambda, so you can write Signal-style code inline, and even mix in the symbolic annotation types: ```java var circuit = CircuitBuilder.create("range") .publicVar("threshold") .secretVar("age") .defineSignals(c -> { var age = ZkUInt.secret(c, "age", 8); // adds the 8-bit range check var threshold = ZkUInt.publicInput(c, "threshold", 8); age.gte(threshold).assertTrue(); }); ``` ### Key Signal operations | Operation | Meaning | R1CS cost | |-----------|---------|-----------| | `a.add(b)`, `a.sub(b)`, `a.neg()`, `a.add(5)` | Field addition and subtraction | Free (linear combination) | | `a.mul(b)` | Field multiplication | 1 constraint | | `a.mul(5)` | Multiply by a constant | Free (the compiler folds constant factors into a linear combination) | | `a.inv()`, `a.div(b)` | Inverse, division | Adds `a·a⁻¹ = 1`, so zero has no inverse and makes the witness unsatisfiable | | `a.toBinary(n)` | Decompose into `n` bits, LSB first | About `n` booleanity checks plus one recomposition | | `a.and(b)`, `a.or(b)`, `a.xor(b)`, `a.not()` | Boolean logic | Inputs must already be boolean | | `a.isZero()`, `a.isEqual(b)` | Returns 1 or 0 | A few constraints (uses advice, see below) | | `a.lessThan(b, n)` | 1 if `a < b` as `n`-bit integers | O(n); range-checks both operands | | `cond.select(x, y)` | `cond ? x : y`, with `cond` boolean | Small constant | | `a.assertBoolean()`, `a.assertInRange(n)` | Constrain to {0,1} or to `[0, 2ⁿ)` | 1, about `n + 1` | | `c.assertEqual(a, b)`, `c.assertNotEqual(a, b)` | Equality constraints | Small constant | | `c.constant(v)`, `c.fromBinary(bits)`, `c.arrayAccess(arr, idx)` | Constants, recomposition, MUX lookup | Free, free, O(length) | The golden rule of R1CS cost: additions are free, multiplications are not. Reuse intermediate results instead of recomputing them, and prefer `select` over any "branching". `isEqual`, `lessThan` and friends *return* a 0/1 signal; they don't assert anything. Feed the result into `c.assertEqual(result, c.constant(1))` or another constraint, or it constrains nothing. ### Compile ```java var circuit = HashCommitmentCircuit.build(); R1CSConstraintSystem r1cs = circuit.compileR1CS(CurveId.BLS12_381); // Groth16 r1cs.numConstraints(); r1cs.numWires(); r1cs.numPublicInputs(); r1cs.constraints(); // List r1cs.flat(); // packed CSR form (R1CSFlat) for large circuits byte[] iden3 = R1CSSerializer.serialize(r1cs); // .r1cs file for snarkjs tooling ``` | Method | Produces | Notes | |--------|----------|-------| | `compileR1CS(curve)` | `R1CSConstraintSystem` | The Groth16 input. Use `CurveId.BLS12_381` for Cardano. | | `compileR1CSWithDiagnostics(curve)` | `R1CSCompiler.CompilationResult` | Same compilation plus diagnostics. Use it in build tooling to catch changes in the R1CS shape before you generate keys. | | `compilePlonK(curve)` | `PlonKConstraintSystem` | Experimental. See [Prove with PlonK](https://zeroj.dev/guides/proving/plonk/). | Gadgets that depend on field constants (Poseidon, MiMC) record the field they need. If you compile or compute a witness for a different curve, `CircuitBuilder` throws `IllegalStateException` instead of producing a circuit with mismatched constants. > **Caution: The R1CS is the key's identity** > > A Groth16 proving key belongs to one exact constraint system. Any change to the circuit, > including gadget parameters, array sizes, or public-input order, needs a new key (and, for real > deployments, a new ceremony). ### Compute a witness ```java BigInteger[] witness = circuit.calculateWitness(Map.of( "commitment", List.of(commitment), "secret", List.of(secret), "salt", List.of(salt)), CurveId.BLS12_381); // witness[0] = 1, witness[1..numPublic] = public inputs, then secrets, then intermediate wires ``` The witness calculator evaluates every gate in order and checks every equality assertion. Expect these failures: | Exception | Cause | |-----------|-------| | `ArithmeticException` | A constraint is violated (`Constraint violation: ...`) | | `IllegalArgumentException` | A declared input is missing from the map | | `IllegalStateException` | The curve doesn't match the gadgets' field | Input values are reduced modulo the field prime before use, so `-1` becomes `p - 1`. Range-check anything that must be a small integer. For very large circuits, `calculateWitnessFlat` and `calculateWitnessFlatChunked` return packed limbs instead of millions of `BigInteger`s; see [Performance & large circuits](https://zeroj.dev/guides/proving/performance/). ### Hints: prover advice, and how to keep it sound Some values are expensive to *compute* with constraints but cheap to *check*. An inverse is the classic example: computing `a⁻¹` in-circuit is costly, but checking `a · x = 1` costs one multiplication. A **hint** (also called advice) is a value the witness calculator computes outside the constraint system and hands to the circuit as a new wire. Here's the catch: **a hinted wire is unconstrained.** A malicious prover doesn't run your witness calculator. They pick every wire value themselves. The only thing that stops them putting any number in a hinted wire is the constraints you add around it. Soundness lives entirely in those constraints. You already use hints through built-ins that pin their own advice: | Operation | Advice | Constraint that pins it | |-----------|--------|--------------------------| | `inv(a)` | `x = a⁻¹` | `a · x = 1` | | `isZero(a)` | `r` (the result) and `x` (an inverse) | `a · x = 1 − r` and `a · r = 0` | | `toBinary(a, n)` | the bits | each bit is boolean, and the bits recompose to `a` | For advanced gadgets, `CircuitAPI.hintN(kind, params, numOutputs, inputs)` requests multi-output advice from a fixed, enumerated set of trusted-core kinds (`Gate.HintKind`: `MUL_MOD_REDUCE` and `INV_MOD`, used for non-native Ed25519 field arithmetic). There is deliberately no way to plug in arbitrary advice lambdas. `hintN` creates **no constraints**: the caller must add all of them. The rules ZeroJ applies to its own hinted gadgets are the ones to follow in yours: 1. **Pin every hinted value.** For each advice wire, write down which constraints force it to one correct value, or show that a different value can't change any output. 2. **Range-check limbs and quotients.** Advice split into limbs must have each limb range-checked, or a prover can overflow the field. 3. **Check integer identities over the integers.** For non-native arithmetic, verify `a·b − q·p − r = 0` limb-wise with range-bounded carries, not merely modulo the native field. Checking only modulo the native field lets a prover forge by adding a multiple of the field modulus. 4. **Test with mutated advice.** Take an honest witness, change each hint output (+1, −1, plus the modulus), and assert the circuit rejects it. An accepted mutation is a forgery. The next page shows how: [Test your circuits for soundness](https://zeroj.dev/guides/circuits/testing-circuits/). ZeroJ's own hint-based Ed25519 multiplication (`Fe25519.USE_HINT_MUL`) ships **off by default** and gated on an external audit. The simpler hint-based inverse (`Fe25519.USE_HINT_INVERSE`) is on by default because its check reuses the deterministic multiplication. ### Tips - Minimize multiplications; additions are free. - Decompose to bits once and reuse the result. `ZkUInt.decomposition()` and `CircuitAPI.decompose(...)` return an owned `BitDecomposition` that gadgets can reuse, instead of range-checking the same wire twice. - Use Poseidon with `PoseidonParamsBLS12_381T3.INSTANCE` as your hash. Bit-oriented hashes such as SHA-512 cost around a hundred thousand constraints per block. - Keep public inputs few and bound to something meaningful. Every public wire must appear in some constraint, or Groth16 setup refuses the relation (see [Prove with Groth16](https://zeroj.dev/guides/proving/groth16/#relation-validation)). Design notes: [ADR-0010 (the Java circuit DSL)](https://github.com/bloxbean/zeroj/blob/main/docs/adr/0010-java-circuit-dsl.md), [ADR-0028 (DSL optimization and hint soundness)](https://github.com/bloxbean/zeroj/blob/main/docs/adr/0028-dsl-optimization-and-hint-soundness.md). ### Next steps - [Gadget library](https://zeroj.dev/guides/circuits/gadgets/) - [Test your circuits for soundness](https://zeroj.dev/guides/circuits/testing-circuits/) - [Prove with Groth16](https://zeroj.dev/guides/proving/groth16/) --- ## Gadget library Source: https://zeroj.dev/guides/circuits/gadgets/ > The circuit building blocks in zeroj-circuit-lib (hashes, Merkle proofs, ranges, Jubjub, and Cardano key derivation), with status and cost. A *gadget* is a reusable piece of circuit: a hash, a Merkle path check, a comparison. ZeroJ's gadgets live in `zeroj-circuit-lib`. Reusing them saves you from re-deriving constraint systems that already have tests, but each one has a field it works in, a cost, and a maturity status. This page catalogs them and ends with rules for choosing gadgets for Cardano. ```groovy implementation 'org.zeroj:zeroj-circuit-lib' // version from zeroj-bom-core ``` ### Three API flavors Most gadgets come in up to three shapes, one per [authoring style](https://zeroj.dev/guides/circuits/circuit-dsl/#which-layer-should-you-use): | Flavor | Package | Example | Used from | |--------|---------|---------|-----------| | `Zk*` adapters | `org.zeroj.circuit.lib.zk` | `ZkPoseidon.hash(zk, params, a, b)` | Annotated circuits | | `Signal*` helpers | `org.zeroj.circuit.lib` | `SignalPoseidon.hash(c, params, a, b)` | `CircuitSpec` / `defineSignals` | | `CircuitAPI` gadgets | `org.zeroj.circuit.lib` | `Poseidon.hash(api, params, a, b)` | Inline `define(api -> ...)` | The `Zk*` adapters delegate to the same underlying gadgets and reject values that belong to a different circuit. ### Hashing: Poseidon Poseidon is the hash to use inside Cardano circuits. It's designed for arithmetic circuits: a two-input hash compiles to roughly 240 R1CS constraints on BLS12-381 with the current compiler, while bit-oriented hashes such as SHA-512 cost around a hundred thousand per block. **Always pass BLS12-381 parameters explicitly.** The no-parameter overloads use BN254 constants for backward compatibility. If you mix them with a BLS12-381 compile, `CircuitBuilder` refuses to compile, but the explicit form keeps the intent obvious. ```java // Two inputs (in-circuit) ZkField h = ZkPoseidon.hash(zk, PoseidonParamsBLS12_381T3.INSTANCE, left, right); // N inputs, folded pairwise (in-circuit) ZkField c = ZkPoseidonN.hash(zk, PoseidonParamsBLS12_381T3.INSTANCE, owner, assetId, nonce); // The same values off-circuit, for commitments and expected public inputs BigInteger h2 = PoseidonHash.hash(PoseidonParamsBLS12_381T3.INSTANCE, a, b); BigInteger c2 = PoseidonHash.hashN(PoseidonParamsBLS12_381T3.INSTANCE, owner, asset, nonce); ``` `PoseidonN` / `ZkPoseidonN` is a left fold of the two-input hash, not a wider Poseidon permutation, so its outputs differ from a native `t = N + 1` Poseidon. `ZkPoseidonN` has no no-parameter overload at all. `PoseidonHash` is host-side code: it computes values, it doesn't constrain anything. **MiMC is BN254-only.** `MiMC`, `SignalMiMC`, `ZkMiMC` and `MiMCSponge` require the BN254 field and refuse to compile for BLS12-381. Treat them as legacy, off-chain gadgets. ### Merkle membership `ZkMerkle` proves that a leaf sits under a public root. For Cardano, use the params-aware Poseidon helpers: ```java @ZKCircuit(name = "allowlist", nameTemplate = "allowlist-d{depth}") public class Allowlist { public Allowlist(@CircuitParam("depth") int depth) { } @Prove ZkBool prove(ZkContext zk, @Secret ZkField leaf, @Public ZkField root, @Secret @FixedSize(param = "depth") ZkArray siblings, @Secret @FixedSize(param = "depth") ZkArray pathBits) { return ZkMerkle.isMemberPoseidon(zk, PoseidonParamsBLS12_381T3.INSTANCE, leaf, root, siblings, pathBits); } } ``` `isMemberPoseidon` returns a `ZkBool`, `verifyPoseidon` asserts directly, and `computeRootPoseidon` returns the root for further use. Path bits are constrained boolean: bit `0` means the current node is the left child (`hash(current, sibling)`), and `1` means it's the right child (`hash(sibling, current)`). Build the matching root off-circuit with the same convention: ```java BigInteger current = leaf; for (int i = 0; i < siblings.size(); i++) { current = pathBits.get(i).signum() == 0 ? PoseidonHash.hash(PoseidonParamsBLS12_381T3.INSTANCE, current, siblings.get(i)) : PoseidonHash.hash(PoseidonParamsBLS12_381T3.INSTANCE, siblings.get(i), current); } ``` `ZkMerkle.HashType.MIMC` and the enum `HashType.POSEIDON` path are BN254-oriented conveniences. Avoid them for Cardano. For large, updatable state (millions of entries, inclusion and non-inclusion, updates), see the experimental Poseidon MPF/JMT modules in [Authenticated state](https://zeroj.dev/guides/credentials/authenticated-state/). ### Comparisons and ranges For annotated circuits, `ZkUInt` is the comparator: `@UInt(bits = N)` adds the range check, and `lt`, `lte`, `gt`, `gte`, and `inRange(lo, hi)` compare. At the Signal level, use `SignalComparators` (`lessThan`, `lessOrEqual`, `greaterThan`, `greaterOrEqual`, `inRange`, `min`, `max`), or `Comparators` for raw `Variable`s. ```java Signal ok = SignalComparators.greaterOrEqual(c, balance, threshold, 64); c.assertEqual(ok, c.constant(1)); ``` A comparison over `n` bits range-checks **both** operands to `n` bits, and rejects a constant operand that doesn't fit when the circuit is defined. Widths must stay below 253 bits. Size `n` to the real domain of your values; an oversized width costs constraints, and an undersized one makes honest witnesses fail. ### Bits, selection and aliasing | Need | Use | |------|-----| | Decompose or recompose bits | `SignalBinary.num2Bits` / `bits2Num`, `Binary.*`; `ZkBits` for fixed bit-vector inputs | | Bitwise logic | `SignalBinary.bitAnd/bitOr/bitXor`, `rotateLeft` (a free re-indexing) | | Choose between values | `ZkBool.select(a, b)`; `Mux.mux1`, `Mux.mux2` | | Dynamic array lookup | `Mux.arrayAccess(api, array, index)` or `SignalBuilder.arrayAccess` (cost grows with length) | | Canonical field representation | `AliasCheck.check(c, value, nBits)`, a decomposition that proves `value < 2^nBits` | ### Jubjub, Pedersen and EdDSA Jubjub is an elliptic curve defined over the BLS12-381 scalar field, so its arithmetic is cheap inside BLS12-381 circuits. These gadgets require `CurveId.BLS12_381`. Their in-circuit verification side is marked ready pending external review; the off-circuit secret operations have tighter restrictions (see the status table). **Bind every prover-supplied point.** `ZkJubjubPoint.witnessAffine(zk, u, v)` asserts the curve equation and fixes the extended coordinates (5 constraints). Without it, a prover can inject an off-curve point such as `(1, 1)`. Neither `witnessAffine` nor `assertWellFormed()` proves prime-order subgroup membership; that is a separate, much more expensive check. **Pedersen commitments.** `ZkPedersen.commit(zk, value, blinding, scalarBits)` commits two `ZkUInt` scalars and returns a `ZkJubjubPoint`, and `verifyOpening(...)` checks an opening. Both scalars are constrained to canonical values below the subgroup order. Two 252-bit scalars cost 3,020 constraints. Range-limit business amounts separately. **EdDSA-Jubjub verification** comes in two named entry points, because whether the public key needs an in-circuit subgroup check depends on your protocol: | Entry point | Use when | Approx. constraints | |-------------|----------|--------------------| | `ZkEdDSAJubjub.verifyStrict(...)` | The public key is secret or chosen by the prover | ~14,500 | | `ZkEdDSAJubjub.verifyWithRegisteredKey(...)` | The public key is a public input or constant (the DSL enforces this) | ~8,962 | Both reject small-order keys, including the identity. With `verifyWithRegisteredKey`, binding the public key to a subgroup-checked registry entry is your verifier's job. Both also need two reduction witnesses, computed off-circuit with `ZkEdDSAJubjub.witnessComputeKReduction(signature.r(), publicKey, message)`: ```java @Prove void prove(ZkContext zk, @Public ZkField pkU, @Public ZkField pkV, @Public ZkField msg, @Public ZkField rU, @Public ZkField rV, @Public @UInt(bits = 252) ZkUInt s, @Secret @UInt(bits = 252) ZkUInt kModL, @Secret @UInt(bits = 4) ZkUInt kQuotient) { ZkEdDSAJubjub.verifyWithRegisteredKey(zk, pkU, pkV, msg, rU, rV, s, kModL, kQuotient); } ``` > **Danger: Off-circuit signing and commitment generation** > > `EdDSAJubjub.sign` and `PedersenCommitment.commit` run secret-dependent, variable-time Java > `BigInteger` arithmetic. They are approved only for local, offline, or isolated use, not for > value-bearing issuance on shared or network-reachable machines. The in-circuit gadgets are not > affected. The normative scheme is > [jubjub-eddsa-v1](https://github.com/bloxbean/zeroj/blob/main/docs/specs/jubjub-eddsa-v1.md). ### Real-world crypto: Cardano key derivation These gadgets reproduce standard wallet primitives *inside* a circuit, so a proof can show "I know the root key behind this Cardano address" without revealing it. They're bit-oriented and independent of the circuit field, so they run on BLS12-381 Groth16, but they are **large**. | Gadget | Adapter | What it's for | Measured size | |--------|---------|---------------|---------------| | BLAKE2b (RFC 7693) | `ZkBlake2b.hash224` / `hash256` / `hash` | Cardano key hashes (blake2b-224) | 76,832 constraints per block | | SHA-512 (FIPS 180-4) | `ZkSha512.hash` | Building block for HMAC and BIP32 | ~109,000 constraints per block | | HMAC-SHA512 (RFC 2104) | `ZkHmacSha512.hmac` | BIP32-Ed25519 child derivation | ~454,000 constraints at the BIP32 input shape | | GF(2²⁵⁵−19) field, Ed25519 points | `Fe25519`, `Ed25519Point` (no `Zk*` adapter) | Non-native Ed25519 arithmetic, fixed-base scalar multiplication | Building blocks | | BIP32-Ed25519 | `Bip32Ed25519` (no `Zk*` adapter) | Hardened and soft child-key derivation, Icarus style | Building block | | CIP-1852 derivation | `ZkCip1852.paymentKeyHash`, `leafKeyHash` | Root key → `m/1852'/1815'/account'/role/index` → 28-byte payment key hash | On the order of 19 million constraints for the full path | Sizes are ZeroJ's own measurements from the gadget design work ([ADR-0027](https://github.com/bloxbean/zeroj/blob/main/docs/adr/0027-real-world-crypto-gadgets-sha512-hmac-blake2b-ed25519.md), [ADR-0028](https://github.com/bloxbean/zeroj/blob/main/docs/adr/0028-dsl-optimization-and-hint-soundness.md)) and may change as the gadgets are optimized. `ZkCip1852.paymentKeyHash` has overloads that take the account, role, and index as Java constants or as `ZkBytes` circuit inputs; with all three as inputs, one circuit and one setup cover every address of a root key. A proof of this size needs the large-circuit proving path in [Performance & large circuits](https://zeroj.dev/guides/proving/performance/). See [Account recovery](https://zeroj.dev/use-cases/account-recovery/) for the application built on it. ### Status at a glance "Ready" below means what the library documents: the gadget can be used in a circuit compiled for BLS12-381, proved with Groth16, and checked by ZeroJ's reusable Plutus V3 verifier. It is not a claim of audit, and a reusable verifier checks only the math, not your application's authorization or replay rules. | Gadget | Field | Cardano status (from the library's table) | |--------|-------|-------------------------------------------| | Field arithmetic, `ZkBool`, `ZkUInt`, arrays and matrices | Any | Ready on BLS12-381 Groth16 | | `ZkBits`, `ZkBytes` | Any | Ready for binding and equality | | Binary decomposition, comparators, selection | Any | Ready on BLS12-381 Groth16 | | Poseidon T3, folded Poseidon N | BN254 default; BLS12-381 with explicit params | Ready with `PoseidonParamsBLS12_381T3.INSTANCE` | | Merkle membership | Hash-dependent | Ready with the params-aware Poseidon helpers | | MiMC, MiMC sponge | BN254 only | Not Cardano-ready | | Jubjub point arithmetic | BLS12-381 only | Ready for algebraic and public-data use, pending external review | | Pedersen commitment (in-circuit) | BLS12-381 only | Ready, pending external review | | Pedersen commitment (off-circuit generation) | Jubjub | Offline or isolated use only | | EdDSA-Jubjub | BLS12-381 only | Verification ready pending external review; legacy signing offline only | | BLAKE2b, CIP-1852 derivation | Field-agnostic | Ready on BLS12-381 Groth16 | | SHA-512, HMAC-SHA512, BIP32-Ed25519 | Field-agnostic | Ready as building blocks | | Poseidon MPF/JMT authenticated state | BLS12-381 Poseidon profile | Experimental (separate modules) | The authoritative, detailed table is in the [`zeroj-circuit-lib` README](https://github.com/bloxbean/zeroj/blob/main/zeroj-circuit-lib/README.md#gadget-status). ### Choosing a gadget for Cardano 1. **Compile for `CurveId.BLS12_381` and prove with Groth16.** That is the verified on-chain path. 2. **Hash with Poseidon and explicit `PoseidonParamsBLS12_381T3.INSTANCE`.** Never MiMC, never the no-parameter overloads. 3. **Use `ZkMerkle.*Poseidon(...)` for membership.** The `HashType` enum paths are BN254-oriented. 4. **Use `ZkUInt` for every quantity,** with the tightest honest width. 5. **Bind every witness-supplied curve point** with `witnessAffine`, and pick the EdDSA entry point that matches who controls the key. 6. **Reach for SHA-512, HMAC, BLAKE2b, or Ed25519 only when a protocol demands them.** They cost orders of magnitude more than Poseidon. 7. **Match off-circuit and in-circuit hashing exactly.** Same parameters, same argument order, same path-bit convention, or honest proofs will fail. ### Next steps - [Test your circuits for soundness](https://zeroj.dev/guides/circuits/testing-circuits/) - [Private allowlist with a Merkle tree](https://zeroj.dev/tutorials/private-allowlist/) - [Prove with Groth16](https://zeroj.dev/guides/proving/groth16/) --- ## Test your circuits for soundness Source: https://zeroj.dev/guides/circuits/testing-circuits/ > Why honest-witness tests aren't enough, a soundness checklist, invalid-witness and proof-tampering JUnit tests, and differential testing. A circuit that produces the right answer for honest inputs can still be broken. The dangerous bug in zero-knowledge code is the **under-constrained circuit**: a missing constraint that lets a dishonest prover build a valid proof for a false statement. Nothing crashes and every honest test stays green. This is the most common class of serious ZK bug, so this page is about testing what the circuit *rejects*, not only what it accepts. ### Why honest tests prove so little Your tests use ZeroJ's witness calculator, which computes every intermediate wire honestly. A real attacker doesn't. They choose all the wire values themselves and only need the constraint rows to hold. So two different questions matter: 1. **Does my witness generator reject bad inputs?** `calculateWitness` answers this. It throws `ArithmeticException` when an assertion fails. 2. **Do my constraints reject every false statement?** This is soundness, and it's what the verifier actually enforces. You probe it with *adversarial* witnesses: values chosen to satisfy the equations you wrote while breaking the statement you meant. A classic example. The intended statement is "I know a non-trivial factorization of `n`": ```java var circuit = CircuitBuilder.create("factor") .publicVar("n").secretVar("a").secretVar("b") .define(api -> api.assertEqual(api.mul(api.var("a"), api.var("b")), api.var("n"))); ``` Honest tests pass with `a = 3, b = 11, n = 33`. But `a = 1, b = 33` also satisfies the circuit, and so does `a = p − 1, b = p − 33` (that is, `−1 · −33`), because field arithmetic wraps. The circuit needs `a` and `b` range-checked and different from 1. Only a test that *tries* those witnesses finds the bug. ### The checklist For every circuit, test: - [ ] **Schema order.** Public and secret input names, in order. Public-input order is part of the verifier contract. - [ ] **A valid witness.** The honest case computes without error. - [ ] **One invalid witness per intended rule.** For each thing the circuit should enforce, build a witness that breaks only that rule and assert it's rejected. - [ ] **Boundaries.** For every range and comparison: the limit itself, limit ± 1, zero, the maximum of the declared width, one past it, and a "negative" value (which becomes `p − x`). - [ ] **Field wraparound.** Values near the field prime, and products that could wrap. - [ ] **Public-input extraction.** `inputs.publicValues()` returns the values you expect, in order. - [ ] **Target compilation.** It compiles for `CurveId.BLS12_381`, and a curve mismatch is refused. - [ ] **Proof tampering.** A real proof fails with a changed public input, with public inputs in a different order, and under another circuit's verification key. - [ ] **Hints and advice.** Every prover-supplied value is pinned: mutate it and expect rejection. ### A complete JUnit 5 example This tests the `SealedBid` circuit from [Write circuits with annotations](https://zeroj.dev/guides/circuits/annotations/#anatomy-of-an-annotated-circuit): the public commitment must match `Poseidon(bidAmount, salt)`, and `bidAmount` (64 bits) must be at least `reservePrice`. The proof test needs the dev-only setup flag `zeroj.allowInsecureTrustedSetup=true` in your test JVM (see [Installation](https://zeroj.dev/start/installation/#enable-the-development-trusted-setup)). ```java title="SealedBidCircuitTest.java" import org.junit.jupiter.api.Test; import org.zeroj.api.CircuitId; import org.zeroj.api.CurveId; import org.zeroj.api.ProofSystemId; import org.zeroj.api.VerificationMaterial; import org.zeroj.circuit.CircuitBuilder; import org.zeroj.circuit.lib.poseidon.PoseidonHash; import org.zeroj.circuit.lib.poseidon.PoseidonParamsBLS12_381T3; import org.zeroj.codec.SnarkjsJsonCodec; import org.zeroj.crypto.groth16.Groth16Keys; import org.zeroj.crypto.setup.PowersOfTauBLS381; import org.zeroj.crypto.snarkjs.SnarkjsGroth16Json; import org.zeroj.verifier.groth16.bls12381.Groth16BLS12381PureJavaVerifier; import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.util.List; import static org.junit.jupiter.api.Assertions.*; class SealedBidCircuitTest { private static final BigInteger RESERVE = BigInteger.valueOf(75); private static final BigInteger SALT = new BigInteger("88001"); private static final BigInteger MAX_U64 = BigInteger.ONE.shiftLeft(64).subtract(BigInteger.ONE); private final CircuitBuilder circuit = SealedBidCircuit.build(); private static BigInteger commit(BigInteger bid) { return PoseidonHash.hash(PoseidonParamsBLS12_381T3.INSTANCE, bid, SALT); } private static SealedBidCircuit.Inputs bid(BigInteger amount, BigInteger commitment) { return SealedBidCircuit.inputs() .bidCommitment(commitment) .reservePrice(RESERVE) .bidAmount(amount) .salt(SALT); } private void assertAccepted(SealedBidCircuit.Inputs in) { assertDoesNotThrow(() -> in.calculateWitness(circuit, CurveId.BLS12_381)); } private void assertRejected(SealedBidCircuit.Inputs in) { assertThrows(ArithmeticException.class, () -> in.calculateWitness(circuit, CurveId.BLS12_381)); } @Test void schemaOrderIsPinned() { var schema = SealedBidCircuit.schema(); assertEquals(List.of("bidCommitment", "reservePrice"), schema.publicInputs().names()); assertEquals(List.of("bidAmount", "salt"), schema.secretInputs().names()); assertEquals(64, schema.input("bidAmount").bits()); } @Test void honestBidIsAcceptedAndPublicValuesAreOrdered() { var in = bid(BigInteger.valueOf(100), commit(BigInteger.valueOf(100))); assertAccepted(in); assertEquals(List.of(commit(BigInteger.valueOf(100)), RESERVE), in.publicValues()); } @Test void eachRuleIsEnforcedOnItsOwn() { // Rule 1: the commitment must match. Valid bid, wrong commitment. assertRejected(bid(BigInteger.valueOf(100), BigInteger.ONE)); // Rule 2: the bid must reach the reserve. Matching commitment, low bid. assertRejected(bid(BigInteger.valueOf(50), commit(BigInteger.valueOf(50)))); } @Test void boundaries() { assertAccepted(bid(RESERVE, commit(RESERVE))); // equal: ok BigInteger justBelow = RESERVE.subtract(BigInteger.ONE); assertRejected(bid(justBelow, commit(justBelow))); // reserve - 1 assertAccepted(bid(MAX_U64, commit(MAX_U64))); // widest 64-bit value BigInteger tooWide = MAX_U64.add(BigInteger.ONE); assertRejected(bid(tooWide, commit(tooWide))); // 2^64: range check BigInteger minusOne = BigInteger.ONE.negate(); // becomes p - 1 assertRejected(bid(minusOne, commit(minusOne))); } @Test void compilesOnlyForTheIntendedCurve() { var r1cs = circuit.compileR1CS(CurveId.BLS12_381); assertEquals(2, r1cs.numPublicInputs()); assertThrows(IllegalStateException.class, () -> circuit.compileR1CS(CurveId.BN254)); } @Test void proofFailsWithTamperedPublicInputs() { var r1cs = circuit.compileR1CS(CurveId.BLS12_381); var in = bid(BigInteger.valueOf(100), commit(BigInteger.valueOf(100))); BigInteger[] witness = in.calculateWitness(circuit, CurveId.BLS12_381); // DEV/TEST ONLY: single-party setup, needs -Dzeroj.allowInsecureTrustedSetup=true BigInteger tau = PowersOfTauBLS381.generate(4).tauScalar(); try (var keys = Groth16Keys.setupInMemory( r1cs.constraints(), r1cs.numWires(), r1cs.numPublicInputs(), tau)) { var proof = keys.prove(witness, r1cs.constraints()); String vkJson = SnarkjsGroth16Json.verificationKeyJson(keys); String proofJson = SnarkjsGroth16Json.proofJson(proof); BigInteger[] pub = in.publicValues().toArray(BigInteger[]::new); assertTrue(verifies(proofJson, vkJson, pub)); BigInteger[] lowerReserve = pub.clone(); lowerReserve[1] = lowerReserve[1].subtract(BigInteger.ONE); assertFalse(verifies(proofJson, vkJson, lowerReserve)); BigInteger[] swapped = {pub[1], pub[0]}; assertFalse(verifies(proofJson, vkJson, swapped)); } } private static boolean verifies(String proofJson, String vkJson, BigInteger[] publicInputs) { var id = new CircuitId("sealed-bid"); var envelope = SnarkjsJsonCodec.toEnvelopeFromJson( proofJson, vkJson, SnarkjsGroth16Json.publicJson(publicInputs), id); var material = VerificationMaterial.of( vkJson.getBytes(StandardCharsets.UTF_8), ProofSystemId.GROTH16, CurveId.BLS12_381, id); return new Groth16BLS12381PureJavaVerifier().verify(envelope, material).proofValid(); } } ``` Besides `ArithmeticException` for a violated constraint, expect `IllegalArgumentException` for a missing input or an input array of the wrong length, and `IllegalStateException` when the curve doesn't match the gadgets' field. Assert the specific type so a test can't pass for the wrong reason. > **Note: Test the rule, not the hash** > > In `eachRuleIsEnforcedOnItsOwn`, each invalid case breaks exactly one rule and keeps everything > else honest (the low bid still has a *matching* commitment). If you broke both at once, a > missing reserve check would hide behind the commitment check and the test would still pass. ### Simulating a malicious prover `calculateWitness` computes intermediate wires and hints itself, so you can't tamper with them through the input map. Two techniques reach past it. **Promote the advice to an input.** To test a gadget that consumes prover advice, build a test circuit where the advice values are secret inputs. Then the "attacker" chooses them, and `calculateWitness` only checks your constraints. ZeroJ tests its own hint-based Ed25519 arithmetic this way: honest quotient and remainder limbs must be accepted, and every limb changed by ±1, or a remainder pushed past the modulus, must be rejected. **Check rows against a tampered witness.** A compiled R1CS is just rows of `(A·w) × (B·w) = (C·w)`, and its wire indices match the witness array. A tiny checker lets you test any hand-made witness: ```java static boolean satisfies(R1CSConstraintSystem r1cs, BigInteger[] w) { BigInteger p = r1cs.prime(); for (R1CSConstraint row : r1cs.constraints()) { BigInteger a = dot(row.a(), w, p), b = dot(row.b(), w, p), c = dot(row.c(), w, p); if (a.multiply(b).subtract(c).mod(p).signum() != 0) return false; } return true; } static BigInteger dot(Map terms, BigInteger[] w, BigInteger p) { BigInteger sum = BigInteger.ZERO; for (var term : terms.entrySet()) sum = sum.add(term.getValue().multiply(w[term.getKey()])); return sum.mod(p); } ``` Start from an honest witness (`satisfies` must be true), change a value that *should* be pinned, such as a public input (`w[1]`) or the output of a gadget you wrote, and assert `satisfies` becomes false. An accepted change is a lead worth chasing. Note that some wires legitimately don't appear in any row: the compiler inlines linear expressions, and some advice is genuinely free (the inverse inside `isZero` when the input is zero). A mutation that stays accepted only matters if it changes something the statement depends on. ### Property-based and randomized tests Soundness bugs hide in corners, so generate many cases. A `@ParameterizedTest` or a loop over random values works well: for random bids and reserves, assert that acceptance exactly equals `bid >= reserve`, and that every witness for a false statement is rejected. A property-testing library such as jqwik can shrink failures to minimal cases. Aim for both directions: *every true statement proves* (completeness) and *no false statement proves* (soundness). ### Differential testing Don't derive expected values only from the code under test: - **Independent references for off-circuit values.** ZeroJ's own gadgets are validated this way: SHA-512 and HMAC against the JDK's `MessageDigest` and `Mac`, BLAKE2b and key derivation against Cardano Client Lib, Ed25519 against BouncyCastle. Use known-answer vectors from standards where they exist. - **snarkjs as an independent verifier.** Export a ZeroJ proof with `SnarkjsGroth16Json.verificationKeyJson`, `proofJson` and `publicJson`, then run `snarkjs groth16 verify verification_key.json public.json proof.json`. ZeroJ's own CI does this in both directions against a pinned snarkjs. - **circom for cross-implementation checks.** If a circom version of your circuit exists, compare public outputs for the same inputs. See [Bring circom & snarkjs circuits](https://zeroj.dev/tutorials/snarkjs-interop/). ### Beyond the circuit A sound circuit and a valid proof still don't make an application secure. A proof can be replayed in another transaction, or used by someone it wasn't meant for, unless your validator binds it to the transaction context and checks nullifiers and authorization. Test those paths too; see [Application security](https://zeroj.dev/guides/verifying/application-security/). ### Next steps - [Gadget library](https://zeroj.dev/guides/circuits/gadgets/) - [Prove with Groth16](https://zeroj.dev/guides/proving/groth16/) - [Application security](https://zeroj.dev/guides/verifying/application-security/) --- ## Prove with Groth16 Source: https://zeroj.dev/guides/proving/groth16/ > Set up keys, generate Groth16 proofs on BLS12-381, scale to millions of constraints, and export proofs and keys for verifiers. Groth16 on BLS12-381 is ZeroJ's primary proof system and **the focus of the current release**. It's the path used in every tutorial, the one verified end to end on-chain against Yaci DevKit, and the one Cardano verifies with Plutus V3's built-in BLS12-381 operations. Proofs are small (three curve points) and cheap to verify. The trade-off is a trusted setup per circuit. Like the rest of ZeroJ, this path is research software. Its status is **Beta**: feature-complete and correctness-tested, but not externally audited and not for value-bearing or mainnet use. See [Status & maturity](https://zeroj.dev/start/status/). This guide covers the `zeroj-crypto` API: where keys live, how to prove, how to scale up, what the fail-closed checks mean, and how to hand proofs to verifiers. ```text circuit ──compileR1CS(BLS12_381)──▶ R1CS ──setup / import──▶ Groth16Keys │ │ └──calculateWitness──▶ witness ─────────────▶ keys.prove(...) ──▶ proof ──▶ verifier ``` ### Choose where the proving key lives `Groth16Keys` is the front door: one handle for the key material wherever it lives, and one `prove` that works the same against all of them. You make one decision, at setup time: | Key home | Get it with | When | Memory | |----------|-------------|------|--------| | Heap | `Groth16Keys.setupInMemory(...)` | Tests and small circuits | Whole key on the heap | | Key store, sparse | `Groth16Keys.setupToStore(..., true)` | Large local circuits (recommended store format) | Streamed setup; the key is memory-mapped, so it uses page cache, not heap | | Key store, dense | `Groth16Keys.setupToStore(..., false)` | Interchange with older tools | Same profile, larger files | | Imported ceremony key | `ZkeyPkStoreImporter.importToPkStore(...)`, then `Groth16Keys.load(dir)` | Anything beyond local testing | Memory-mapped at prove time | `Groth16Keys` is `AutoCloseable`. Use try-with-resources so store-backed keys are unmapped. > **Caution: The first three rows are development keys** > > `setupInMemory` and `setupToStore` run a **single-party** setup: your process knows the secret > randomness and could forge proofs. ZeroJ refuses to run them unless you opt in with > `-Dzeroj.allowInsecureTrustedSetup=true` (or `ZEROJ_ALLOW_INSECURE_TRUSTED_SETUP=true`); without > it they throw `IllegalStateException`. Keys that protect anything real come from a multi-party > ceremony. See [Run a trusted setup ceremony](https://zeroj.dev/guides/proving/trusted-setup-ceremony/). ### Flow 1: small circuits, keys in memory The examples use the `SealedBid` circuit and its filled-in `inputs` from [Write circuits with annotations](https://zeroj.dev/guides/circuits/annotations/#the-generated-companion). Any `CircuitBuilder` works the same way. ```java var circuit = SealedBidCircuit.build(); var r1cs = circuit.compileR1CS(CurveId.BLS12_381); BigInteger[] witness = inputs.calculateWitness(circuit, CurveId.BLS12_381); // witness[0] == 1 // DEV/TEST ONLY: single-party setup. Groth16 setup uses only the tau scalar. BigInteger tau = PowersOfTauBLS381.generate(4).tauScalar(); try (var keys = Groth16Keys.setupInMemory( r1cs.constraints(), r1cs.numWires(), r1cs.numPublicInputs(), tau)) { Groth16ProofBLS381 proof = keys.prove(witness, r1cs.constraints()); } ``` Nothing touches disk. This is fine up to a few hundred thousand constraints. ### Flow 2: bigger circuits, keys on disk `setupToStore` streams every proving-key point straight into memory-mapped files, so the key is never fully on the heap. Pass the packed constraints (`r1cs.flat()`) and a directory: ```java Path keysDir = Path.of("keys/sealed-bid-v1"); // DEV/TEST ONLY try (var keys = Groth16Keys.setupToStore( r1cs.flat(), r1cs.numWires(), r1cs.numPublicInputs(), tau, keysDir, true)) { Groth16ProofBLS381 proof = keys.prove(witness, r1cs.constraints()); } // Every later run reopens the bundle. Sparse or dense is detected from the manifest. try (var keys = Groth16Keys.load(keysDir)) { Groth16ProofBLS381 proof = keys.prove(witness, r1cs.constraints()); } ``` `sparse = true` stores points at infinity as a single bit each, which makes the on-disk key much smaller for large circuits. ### Flow 3: keys from a ceremony For real deployments, the proving key comes from a snarkjs multi-party ceremony `.zkey`. Import it once into the same store layout: ```java ZkeyPkStoreImporter.importToPkStore(Path.of("circuit_final.zkey"), keysDir); // streaming, multi-GB safe ``` snarkjs appends one public-input binding row per public signal (plus one for the constant wire) after your circuit's rows, so you must tell the prover about them: ```java int numPublic = r1cs.numPublicInputs(); try (var keys = Groth16Keys.load(keysDir)) { // List form: append snarkjs's binding rows to your compiled constraints var proof = keys.prove(witness, ZkeyPkStoreImporter.snarkjsConstraints(r1cs.constraints(), numPublic)); // Packed form: pass the number of binding rows instead (0 for locally generated keys) var proof2 = keys.prove(ProverBackend.PURE_JAVA, FlatScalars.pack(witness, witness.length), r1cs.flat(), numPublic + 1); } ``` The ceremony must have been run on **exactly** the R1CS you compile, exported with `zeroj-ceremony export-r1cs` or `R1CSSerializer.serialize(r1cs)`. For small keys (up to 128 MB) there is also an in-memory importer that can pin the file's SHA-256 before parsing: `ZkeyImporterBLS381.importZkeyFull(bytes, expectedSha256)`. It returns the proving key and the constraints for `Groth16ProverBLS381.prove(...)`. Circom circuits follow the same import path; see [Bring circom & snarkjs circuits](https://zeroj.dev/tutorials/snarkjs-interop/). ### The packed prove path The list form boxes every coefficient and witness value as a `BigInteger`. At millions of constraints, use the packed overload with `R1CSFlat` constraints and `FlatScalars` witness values: ```java FlatScalars w = FlatScalars.pack(witness, witness.length); Groth16ProofBLS381 proof = keys.prove(ProverBackend.PURE_JAVA, w, r1cs.flat(), /* bindingRows */ 0); ``` `FlatScalars.packConsuming(witness, n)` does the same but nulls out the `BigInteger[]` as it goes, so the boxed values can be garbage-collected early. `ProverBackend.PURE_JAVA` is the default, multi-core pure-Java backend; the opt-in native backend is covered in [Performance & large circuits](https://zeroj.dev/guides/proving/performance/). ### Groth16Pipeline for very large circuits `Groth16Pipeline` packages the orchestration ZeroJ uses for its ~19-million-constraint account-ownership circuit: a constraint cache written during setup, witness generation *before* the constraints are memory-mapped (so the two memory peaks never overlap), and a circuit fingerprint that fails fast if a key bundle doesn't match the circuit. You supply two things: how to compile, and how to compute the witness. ```java Supplier compile = () -> { var cs = SealedBidCircuit.build().compileR1CS(CurveId.BLS12_381); return new Groth16Pipeline.Compiled( cs.flat(), cs.numConstraints(), cs.numWires(), cs.numPublicInputs()); }; Supplier computeWitness = () -> { var c = SealedBidCircuit.build(); // released when the lambda returns BigInteger[] w = inputs.calculateWitness(c, CurveId.BLS12_381); return FlatScalars.packConsuming(w, w.length); }; // Setup (DEV/TEST ONLY): sparse store + r1cs.bin cache, bound to the circuit fingerprint var compiled = compile.get(); String fingerprint = compiled.fingerprint(); // record this with the bundle var setup = Groth16Pipeline.setup(compiled, tau, keysDir, true); String vkJson = SnarkjsGroth16Json.verificationKeyJson(setup); // export the VK from the result compiled = null; // let the compiled circuit go // Prove: compiles only if the cache is missing or stale try (var keys = Groth16Keys.load(keysDir)) { Groth16ProofBLS381 proof = Groth16Pipeline.prove(keys, keysDir.resolve(Groth16Pipeline.R1CS_CACHE), fingerprint, compile, computeWitness, /* bindingRows */ 0, ProverBackend.PURE_JAVA); } ``` The fingerprint has the form `c-w-p-r`: the dimensions plus a hash of the exact relation. A mismatch throws `IllegalStateException` before any proving work. For an imported ceremony key, bind the fingerprint to the store once with `Groth16PkStore.bindCircuitFingerprint(keysDir, fingerprint)`, and pass `numPublic + 1` binding rows. `Groth16Pipeline.estimateProvePhaseHeapBytes(numWires, domain)` gives a lower bound on prove-phase heap for preflight checks; witness generation can need more, so measure your own circuit. An optional `Groth16Pipeline.Progress` listener reports stages for CLIs. ### Relation validation Every setup and prove entry point checks the relation's shape before doing any work, and fails closed with an exception instead of proceeding: | Error | Meaning | |-------|---------| | `numWires` / `numPublic` out of range | Need `numWires >= 1` and `0 <= numPublic < numWires` | | A wire index outside `[0, numWires)` | A constraint references a wire that doesn't exist | | Malformed CSR offsets or coefficients | The packed `R1CSFlat` is corrupt | | `witness length (…) must match numWires (…)` | The witness wasn't computed for this circuit | | `witness[0] must be 1` | The constant wire is missing or wrong | | A public wire with no nonzero coefficient | See the binding rule below | **Every public wire must be bound.** Native setup requires each wire `0..numPublic`, including the constant wire 0, to appear with a nonzero coefficient in at least one constraint row. A public input that appears in no row would produce a verification-key entry at the point at infinity, which every ZeroJ verifier (pure Java, blst, on-chain) rejects, and it would leave that public input unbound by the proof. Circuits built with the DSL bind the constant wire through their assertions. If you have a public input your circuit doesn't use, either drop it or make it take part in a real multiplication with another wire, for example `recipient.mul(secret)`. A constant multiplication such as `p * 1` is folded into a linear combination by the compiler and binds nothing. snarkjs binds unused public signals itself, so a circuit can set up under snarkjs and still be refused by native setup; imported ceremony keys are unaffected. In the negligible case that the sampled randomness cancels a bound wire, setup aborts with an `IllegalStateException` before writing anything; run it again. These exceptions mean the relation, the dimensions, or the witness doesn't describe the circuit the key was made for. Fix the caller; don't catch and retry. ### Every proof is freshly randomized Groth16 proofs include two random blinding scalars. Every ZeroJ prove call draws them from `SecureRandom`, and nothing lets you fix, seed, or omit them. That's what makes the proof zero-knowledge: an unblinded proof is a deterministic function of the key and the witness, and a low-entropy secret could be recovered by trying candidates. Consequences: - Two proofs of the same statement are different bytes. Don't compare proofs for equality. - There is **no deterministic or unblinded prove** in any published ZeroJ artifact. To debug, keep the proof you got. ### Export and hand off proofs Proofs and keys travel as snarkjs-compatible JSON. The exporters in `org.zeroj.crypto.snarkjs` write exactly what snarkjs 0.7.6 writes, and refuse non-canonical input (points at infinity, off-curve points, out-of-range scalars): ```java BigInteger[] publicInputs = Arrays.copyOfRange(witness, 1, 1 + r1cs.numPublicInputs()); String vkJson = SnarkjsGroth16Json.verificationKeyJson(keys); // also accepts a SetupResult String proofJson = SnarkjsGroth16Json.proofJson(proof); String publicJson = SnarkjsGroth16Json.publicJson(publicInputs); ``` From there: - **Off-chain:** wrap the JSON in an envelope with `SnarkjsJsonCodec.toEnvelopeFromJson(...)` (or the generated `proofEnvelopeBuilder(...)` for annotated circuits) and verify with `Groth16BLS12381PureJavaVerifier`. See [Verify off-chain](https://zeroj.dev/guides/verifying/off-chain/). - **With snarkjs:** `snarkjs groth16 verify verification_key.json public.json proof.json`. - **On-chain:** `ProverToCardano.compressVk(keys)` and `ProverToCardano.compressProof(proof)` (in `zeroj-onchain-julc`) produce the compressed points a Plutus V3 validator consumes. See [Verify on-chain](https://zeroj.dev/guides/verifying/on-chain/). > **Danger: A valid proof is not authorization** > > A reusable verifier such as the on-chain `Groth16BLS12381Verifier` validator (in > `zeroj-onchain-julc`) only checks the math. Anyone who sees a proof can resubmit it. Real validators must bind the proof to the transaction's `ScriptContext`, > prevent replay (for example by binding a public input to the spent UTxO, or with nullifiers), and > enforce authorization and business rules. See > [Application security](https://zeroj.dev/guides/verifying/application-security/). ### The expert layer `Groth16Keys` and `Groth16Pipeline` delegate to public lower-level seams. You rarely need them, but they exist for memory-tuned pipelines: | Entry point | Purpose | |-------------|---------| | `Groth16SetupBLS381.setup(...)` / `setupToStore(...)` | In-heap and streaming setup (dev only) | | `Groth16PkStore.load/save` | The key-store format itself | | `Groth16ProverBLS381.computeHFlat(...)` + `proveWithHCoeffs(...)` | Compute H, drop the constraints, then run the MSMs | | `Groth16ProverBLS381.proveWithReaders(...)` | Prove from key readers without the handle | Design notes: [ADR-0036 (API facade and pipeline)](https://github.com/bloxbean/zeroj/blob/main/docs/adr/0036-groth16-api-facade-and-pipeline.md), [ADR-0045 (public-wire binding)](https://github.com/bloxbean/zeroj/blob/main/docs/adr/0045-groth16-infinity-ic-profile-and-public-wire-binding.md), [ADR-0046 (no unblinded prove)](https://github.com/bloxbean/zeroj/blob/main/docs/adr/0046-groth16-unblinded-proving-test-boundary.md). ### Next steps - [Run a trusted setup ceremony](https://zeroj.dev/guides/proving/trusted-setup-ceremony/) - [Performance & large circuits](https://zeroj.dev/guides/proving/performance/) - [Verify off-chain](https://zeroj.dev/guides/verifying/off-chain/) - [Verify on-chain](https://zeroj.dev/guides/verifying/on-chain/) --- ## Run a trusted setup ceremony Source: https://zeroj.dev/guides/proving/trusted-setup-ceremony/ > Run a multi-party Groth16 ceremony with snarkjs and the zeroj-ceremony tool, import the result into ZeroJ, and know what to check before trusting one. Every Groth16 circuit needs a trusted setup, and whoever knows the setup's secret randomness (the "toxic waste") can forge proofs. A **multi-party computation (MPC) ceremony** spreads that randomness across many independent contributors. The result is sound as long as **at least one** contributor was honest and destroyed their secret. This page walks through a ceremony with snarkjs and ZeroJ's `zeroj-ceremony` tool, from circuit freeze to proving with the final key. New to the idea? Read [Trusted setup, explained](https://zeroj.dev/learn/trusted-setup/) first. ### When you need one You need a ceremony for **any real deployment of a Groth16 circuit**: anything beyond local development, tests, and throwaway demos. ZeroJ's in-process setup (`Groth16Keys.setupInMemory`, `setupToStore`, `Groth16SetupBLS381.setup`) is single-party by design and only runs behind the `zeroj.allowInsecureTrustedSetup` opt-in. > **Danger: Never protect value with a development key** > > A single-party setup knows the toxic waste. Anyone with access to that process, its memory, or > its logs could forge proofs for your circuit. Keys that guard anything real must come from a > ceremony whose transcript you've verified. ### How the pieces fit ```text Phase 1 (universal, reusable) powers of tau (.ptau) for BLS12-381, up to 2^N constraints │ verify, then "prepare phase2" Phase 2 (one per circuit) circuit.r1cs ──snarkjs groth16 setup──▶ key_0000.zkey │ contributor 1, 2, 3 ... (zeroj-ceremony or snarkjs, any mix) │ public random beacon ▼ key_final.zkey ──snarkjs zkey verify──▶ transcript checked │ ZeroJ zeroj-ceremony finalize ──▶ proving-key store ──▶ prove ``` Every artifact stays in the snarkjs `.zkey` format, and **the independent check is always `snarkjs zkey verify`**, which re-checks every contribution, including ones made with ZeroJ's tool. The tool that checks the ceremony is never the tool ZeroJ wrote. ### Before you start: freeze the circuit A ceremony binds to one exact R1CS. Any later change (a gadget, an array size, the order of public inputs, a compiler change that alters the constraint shape) needs a new ceremony. Before starting: - Finalize and review the circuit, including invalid-witness tests ([Test your circuits](https://zeroj.dev/guides/circuits/testing-circuits/)). - Add the public inputs your validator will use for replay binding. You can't add them later. - Tag the source commit and record the compiler's constraint count. You can rehearse the machinery on a throwaway circuit at any time (see [Rehearse first](#rehearse-first)). ### Phase 1: the powers of tau Phase 1 is universal: one prepared `.ptau` serves every circuit up to its size, on the same curve. It must be **BLS12-381**. Two options: **Reuse an attested ceremony.** ZeroJ's runbook points to Filecoin's BLS12-381 powers of tau (2²⁷). Whatever the source, verify it and prepare it yourself: ```bash snarkjs powersoftau verify pot_imported.ptau snarkjs powersoftau truncate pot_imported.ptau snarkjs powersoftau prepare phase2 pot25.ptau pot25_final.ptau ``` **Run your own.** Contributors take turns, then a beacon closes it: ```bash snarkjs powersoftau new bls12-381 25 pot_0000.ptau snarkjs powersoftau contribute pot_0000.ptau pot_0001.ptau --name="" -v # ... more contributors ... snarkjs powersoftau beacon pot_.ptau pot_beacon.ptau 10 -n="final beacon" snarkjs powersoftau prepare phase2 pot_beacon.ptau pot25_final.ptau ``` At 2²⁵ these steps are heavy (the runbook budgets hours per phase-1 contribution and far longer for `prepare phase2`), but you do them once. Publish the prepared file and its verify output. ### Phase 2: key genesis (coordinator) Export your frozen circuit's R1CS with the ZeroJ tool, create the initial key with snarkjs, and publish both hashes **before** contributions begin: ```bash zeroj-ceremony export-r1cs \ --circuit com.example.OwnershipProof \ --circuit-jar my-circuits.jar \ --out ownership.r1cs snarkjs groth16 setup ownership.r1cs pot25_final.ptau key_0000.zkey shasum -a 256 ownership.r1cs key_0000.zkey ``` `--circuit` takes the `@ZKCircuit` class or its generated `*Circuit` companion (any class with a static `build()` that returns a `CircuitBuilder`). `export-r1cs` compiles for BLS12-381 and loads the class reflectively, so run it with the fat jar on a JVM rather than the native binary. From code, `R1CSSerializer.serialize(r1cs)` produces the same iden3 `.r1cs` bytes. ### Contributors Contributors need the `zeroj-ceremony` tool (or snarkjs; both produce compatible contributions). **Get the tool.** When a ZeroJ release publishes the ceremony distributables, they're attached to the [GitHub release](https://github.com/bloxbean/zeroj/releases): a fat jar (`zeroj-ceremony--all.jar`, needs Java 25) and native zips for linux-x86_64, linux-arm64, macos-arm64, and windows-x86_64 (no Java needed). Or build it from a ZeroJ checkout: ```bash ./gradlew :zeroj-tools:fatJar # zeroj-tools/build/libs/zeroj-ceremony--all.jar ./gradlew :zeroj-tools:nativeDistZip # needs a GraalVM JDK; zeroj-tools/build/distributions/ ``` **Contribute.** Check what you received against the coordinator's published hash, contribute, and send the result back: ```bash shasum -a 256 key_0007.zkey # must match the published hash zeroj-ceremony contribute --in key_0007.zkey --out key_0008.zkey --name "Alice / Example Org" shasum -a 256 key_0008.zkey ``` | Option | Required | Meaning | |--------|----------|---------| | `--in ` | yes | The `.zkey` you received | | `--out ` | yes | The `.zkey` you send back | | `--name ` | no | Your name in the public transcript (default `zeroj contributor`) | The tool draws your secret from the OS secure random source, uses it once, and never writes it anywhere. It prints a **Contribution Hash**; copy it into your attestation. The equivalent snarkjs command is `snarkjs zkey contribute key_0007.zkey key_0008.zkey --name="..."`. ZeroJ's documentation reports its contributor at roughly 0.9 hours versus 2.5 to 3 hours for snarkjs on a 19M-constraint key (about 30 GB); budget about twice the `.zkey` size in free disk. **Publish an attestation**, for example as a gist or a pull request to the ceremony's transcript repository: ```text Ceremony: , contribution #8 Who: , Received: key_0007.zkey sha256=<...> Produced: key_0008.zkey sha256=<...> Contribution Hash: Machine: I confirm the entropy was generated fresh and destroyed after use. ``` After contributing there's nothing left to keep secret. What matters is that nobody observed the machine during the contribution. To embed contributions in your own service or wallet, `zeroj-tools` exposes the same step as a library call: `ZkeyContributor.contribute(in, out, name)` returns the contribution hash. ### Close the ceremony (coordinator) ```bash # 1. Beacon: announce the source BEFORE the last contribution lands, # e.g. "the hash of Bitcoin block N" or "drand round R" for a future N or R snarkjs zkey beacon key_0012.zkey key_final.zkey 10 -n="final beacon" # 2. The independent check anyone can re-run snarkjs zkey verify ownership.r1cs pot25_final.ptau key_final.zkey # 3. The verification key for your verifiers and validators snarkjs zkey export verificationkey key_final.zkey verification_key.json # 4. Convert to a ZeroJ proving-key store (streaming, multi-GB safe) zeroj-ceremony finalize --zkey key_final.zkey --pk-store ./ownership-pk ``` `finalize` is `ZkeyPkStoreImporter.importToPkStore(...)` behind a CLI, and you can call that method directly instead. The importer validates the key's curve, field, and dimensions, and that every verification-key point is on the curve and not the point at infinity. ### Prove with the ceremony key The imported store works like any other key bundle. Remember that snarkjs appends one binding row per public input (plus one for the constant wire), so pass them to the prover: ```java int numPublic = r1cs.numPublicInputs(); try (var keys = Groth16Keys.load(Path.of("ownership-pk"))) { var proof = keys.prove(witness, ZkeyPkStoreImporter.snarkjsConstraints(r1cs.constraints(), numPublic)); } ``` For the packed path and `Groth16Pipeline`, pass `numPublic + 1` as the binding-row count. See [Prove with Groth16](https://zeroj.dev/guides/proving/groth16/#flow-3-keys-from-a-ceremony). Before going live, generate a test proof and verify it off-chain and on-chain against the exported verification key. ### Pin artifact hashes Treat the ceremony outputs as pinned artifacts, and check their hashes where you load them: - Record the SHA-256 of `ownership.r1cs`, `key_final.zkey`, `verification_key.json`, and the prepared `.ptau` in your repository or release notes. - The in-memory importers accept an expected hash and refuse a mismatch before parsing: `ZkeyImporterBLS381.importZkeyFull(bytes, expectedSha256)` (keys up to 128 MB) and `PtauImporterBLS381.importPtau(input, maxPoints, expectedSha256)`. - `ZkeyPkStoreImporter.importToPkStore` has no hash parameter, so check the file's SHA-256 yourself before importing it. - Pin the verification key your on-chain validator is built with, and treat any change as a new deployment. ### Publish the transcript In a public repository, publish: - the `.r1cs`, its hash, and the exact circuit source commit it was built from - the prepared `.ptau` source, hash, and verify output - every intermediate `.zkey` hash and every contributor attestation - the beacon announcement and the beacon value - the `snarkjs zkey verify` output and the final key and verification-key hashes Coordinator checklist, from ZeroJ's runbook: - [ ] Circuit frozen (reviewed, replay-binding public inputs added) and tagged in git - [ ] Prepared `.ptau` acquired, and its verify output published - [ ] `.r1cs` and `key_0000.zkey` hashes published before contributions - [ ] At least 3 contributors from independent organizations, attestations published - [ ] Beacon source announced in advance, applied, and published - [ ] `zkey verify` passes; final key and verification-key hashes published - [ ] `finalize` run; a test proof verified off-chain and on-chain ### What to check before trusting a ceremony If you're a user or auditor of someone else's deployment, you don't have to trust the coordinator. Check: 1. **The circuit.** Rebuild the `.r1cs` from the published source commit and compare hashes, so you know the key binds to the circuit you reviewed. 2. **The transcript.** Run `snarkjs zkey verify ` yourself. 3. **The contributors.** Several independent, identifiable people or organizations, each with a published attestation whose hashes chain from one key to the next. 4. **The beacon.** It was announced before the last contribution and matches the public value. 5. **The deployed key.** The verification key in the validator or service matches the one exported from the verified final key. If any link is missing, treat the setup as untrusted. ### Rehearse first ZeroJ ships a rehearsal script that runs this whole flow on a tiny circuit, mixing ZeroJ and snarkjs contributions, a beacon, `zkey verify`, and `finalize`: [`docs/ceremony/rehearsal.sh`](https://github.com/bloxbean/zeroj/blob/main/docs/ceremony/rehearsal.sh). It needs snarkjs on your `PATH`, Java 25, and a built ZeroJ checkout. Further reading: [ceremony user guide](https://github.com/bloxbean/zeroj/blob/main/docs/ceremony/USER-GUIDE.md), [coordinator runbook](https://github.com/bloxbean/zeroj/blob/main/docs/ceremony/OPTION-A-RUNBOOK.md), [ADR-0031](https://github.com/bloxbean/zeroj/blob/main/docs/adr/0031-groth16-mpc-trusted-setup-ceremony.md). ### Next steps - [Prove with Groth16](https://zeroj.dev/guides/proving/groth16/) - [Verify on-chain](https://zeroj.dev/guides/verifying/on-chain/) - [Application security](https://zeroj.dev/guides/verifying/application-security/) --- ## Performance & large circuits Source: https://zeroj.dev/guides/proving/performance/ > How ZeroJ's pure-Java Groth16 prover handles millions of constraints, how to size memory, when the optional blst backend helps, and JVM and GraalVM notes. Small circuits need no tuning. This page is for the other end: circuits with millions of constraints, where memory is the constraint that bites first. All of it is about **Groth16 on BLS12-381**, the focus of the current release. Verification cost doesn't grow with the circuit (a few pairings plus one scalar multiplication per public input), so everything here concerns proving and setup. ### Where the cost goes | Phase | What happens | What it needs | |-------|--------------|---------------| | Compile | Build the circuit graph, emit R1CS rows | Heap proportional to the circuit graph | | Witness | Evaluate every gate | Heap for the graph plus one value per wire | | Setup (dev) or import (ceremony) | Produce the proving key: several points per wire | Disk for the key; heap if you keep the key in memory | | Prove: H | FFTs over the constraint domain | Heap for a few domain-sized arrays | | Prove: MSMs | Multi-scalar multiplications over the key | Reads the whole key once; CPU-bound | ### How the pure-Java prover stays lean The default prover in `zeroj-crypto` has no native dependencies. Several design choices keep large circuits within ordinary machines: - **Allocation-lean arithmetic.** Field elements and curve points are packed `long[]` limbs in Montgomery form rather than objects, so the hot loops allocate very little. - **Memory-mapped key stores.** A store-backed proving key is memory-mapped (through the Java FFM API), so its points live in the OS page cache instead of the Java heap. The *sparse* store format stores points at infinity as one bit each. - **Streaming setup.** `setupToStore` writes every key point straight to the mapped files, so the key is never fully resident. - **Packed constraints and witnesses.** `R1CSFlat` stores the matrices in CSR form, and `FlatScalars` stores the witness as packed limbs instead of millions of `BigInteger`s. - **Ordered peaks.** `Groth16Pipeline` generates the witness *before* it maps the cached constraints, and drops the constraints before the MSMs, so the big memory peaks never overlap. - **Parallel MSMs.** `ProverBackend.PURE_JAVA` splits large MSMs across cores; the result is bit-identical to the single-threaded `ProverBackend.PURE_JAVA_SERIAL`. ### The scale reference: a 19M-constraint circuit ZeroJ's largest real circuit proves Cardano account ownership in-circuit: CIP-1852 key derivation from a root key to a payment key hash. It has 19,075,097 constraints, about 43.7 million wires, and an FFT domain of 2²⁵. It drove the memory work, and ZeroJ's design records report these measurements: | Measurement | Result | Context | |-------------|--------|---------| | Dev setup, before the setup work | ~90 GB heap, ~47 min | 23 GB key bundle, in-heap setup | | Setup → prove → verify, after | Setup 9.6 min, prove 161 s, verify 0.17 s | One Docker container capped at 16 GB total memory, `-Xmx8g`, sparse key store | | Sparse vs dense key store | 9.3 GB vs 24.2 GB of point files | Same circuit | | Prove heap floor | About 7 GB | Set by witness generation for this circuit, not by proving | > **Note: Read these as one data point** > > These are single-machine measurements from ZeroJ's design records > ([ADR-0033](https://github.com/bloxbean/zeroj/blob/main/docs/adr/0033-prover-memory-reduction.md), > [ADR-0034](https://github.com/bloxbean/zeroj/blob/main/docs/adr/0034-frontend-memory-reduction.md), > [ADR-0035](https://github.com/bloxbean/zeroj/blob/main/docs/adr/0035-setup-memory-time-reduction.md)), > taken in July 2026 on one circuit. Your circuit shape, hardware, disk, and JVM will change them. > Measure your own circuit before you size machines for it. ### Heap versus page cache With a store-backed key, memory splits into two budgets: - **Java heap (`-Xmx`)** holds the circuit graph during compile and witness generation, and the FFT buffers during proving. It doesn't hold the key. - **Page cache** holds the memory-mapped key files. The OS manages it: with spare RAM the key stays cached and proving is fast; under pressure it pages key data in from disk, which is slower but still works. That's how the 19M circuit fits in a 16 GB cap with an 8 GB heap. So don't give the heap all your RAM. Size `-Xmx` to your measured heap floor plus headroom, and leave the rest for page cache. `Groth16Pipeline.estimateProvePhaseHeapBytes(numWires, domain)` gives a lower bound for the prove phase only; witness generation for your circuit can need more. Keep the heap below about 32 GB where you can, because larger heaps turn off compressed object pointers and inflate every object. ### Recipe for millions of constraints 1. **Compile once to the packed form**: `compileR1CS(CurveId.BLS12_381)` and use `r1cs.flat()`. 2. **Put the key in a store**: `Groth16Keys.setupToStore(..., true)` for development keys, or `ZkeyPkStoreImporter.importToPkStore(...)` for a ceremony key. Never `setupInMemory` at this size. 3. **Prove through `Groth16Pipeline`**, so the `r1cs.bin` constraint cache skips recompiling on later runs and the memory peaks stay ordered. See [Groth16Pipeline](https://zeroj.dev/guides/proving/groth16/#groth16pipeline-for-very-large-circuits). 4. **Release the circuit graph before packing the witness**, then pack with `FlatScalars.packConsuming(...)`. 5. **Measure the witness peak.** `calculateWitnessFlat` / `calculateWitnessFlatChunked` avoid boxing, which helps typical circuits. In a bit-heavy circuit most wires hold 0 or 1 and share the same `BigInteger` objects, so the boxed witness plus `packConsuming` used less memory for the 19M circuit. Try both. 6. **Run it as its own JVM process** with explicit flags, rather than inside a build tool's default test JVM, which usually has a small heap. ### The optional blst backend `zeroj-crypto-blst` plugs the native [blst](https://github.com/supranational/blst) library's multi-scalar multiplication into the prover through the FFM API. It's purely a performance option: proofs are bit-identical to pure Java, and cross-provider equivalence is tested. ```groovy implementation 'org.zeroj:zeroj-crypto-blst' // brings zeroj-crypto and zeroj-blst; version from the BOM ``` ```java import org.zeroj.cryptoblst.BlstProverBackend; try (var keys = Groth16Keys.load(keysDir)) { var proof = keys.prove(BlstProverBackend.create(), witness, r1cs.constraints()); } ``` `BlstProverBackend.create()` is multi-core; `createSerial()` makes one native call per MSM. The JVM needs native access: ```bash java --enable-native-access=ALL-UNNAMED -Xmx8g -jar prover.jar ``` What to know before you adopt it: - **Measure first.** blst measured about 5× faster than the pure-Java prover of the time on 2¹²–2¹⁶ benchmark circuits. After the later memory and FFT work, the pure-Java prover matches it at the 19M scale, so the win depends on your circuit size. - **It uses native memory outside `-Xmx`.** At the 19M scale, blst's native MSM buffers took several GB on top of the heap (about 8.4 GB during the G2 MSM alone), and the process was killed under a 16 GB cap. Use it on large-memory machines only. - **Supply chain.** `libblst` is built from source at pinned tag v0.3.15 and bundled in `zeroj-blst` for Linux (x86-64 and AArch64), macOS on Apple silicon, and Windows. Intel Macs fall back to pure Java. ### JVM flags | Flag | When | |------|------| | `-Xmx` | Always, for large circuits. Size to your measured floor plus headroom. | | `--enable-native-access=ALL-UNNAMED` | When you use `zeroj-crypto-blst` or `zeroj-blst` | | `-Dzeroj.allowInsecureTrustedSetup=true` | Only for development setup; never in production launch scripts | ### GraalVM native image The pure-Java prover has no JNI, so it can be compiled with `native-image`. For a long, CPU-bound prove, though, native image is a *deployment* choice, not a speed lever: a warmed-up JIT matches or beats it. Native image pays off for startup time and footprint, which matter most for short-lived verifiers and CLIs. `zeroj-blst` ships native-image configuration that enables native access and bundles the `libblst` binaries as image resources. Depending on your GraalVM version, the build may also need the experimental Foreign API support options noted in that configuration. ZeroJ's own `zeroj-ceremony` CLI is built as a native binary with `--no-fallback --enable-native-access=ALL-UNNAMED`, and its contribute and finalize commands (streaming, memory-mapped key processing) run in that binary. Build and test your own image in CI rather than assuming. ### Benchmarks Both benchmarks are opt-in Gradle tasks in the ZeroJ repository, heavy by design: ```bash # Pure-Java Groth16 prover scale benchmark (circuit sizes as log2) ./gradlew :zeroj-crypto:benchmark -Dzeroj.bench.logs=12,14,16 -PbenchHeap=10g # blst MSM and full-prove speedup versus pure Java ./gradlew :zeroj-crypto-blst:blstBench ``` Recorded end-to-end benchmark reports, including five-million-entry Poseidon MPF and JMT state runs, are in [docs/benchmarks](https://github.com/bloxbean/zeroj/tree/main/docs/benchmarks). Further reading: [ADR-0029 (prover performance and blst)](https://github.com/bloxbean/zeroj/blob/main/docs/adr/0029-blst-accelerated-groth16-prover.md), [alternate prover backends](https://github.com/bloxbean/zeroj/blob/main/docs/alternate-prover-backends.md). ### Next steps - [Prove with Groth16](https://zeroj.dev/guides/proving/groth16/) - [Run a trusted setup ceremony](https://zeroj.dev/guides/proving/trusted-setup-ceremony/) - [Account recovery](https://zeroj.dev/use-cases/account-recovery/), the 19M-constraint circuit in context --- ## Prove with PlonK Source: https://zeroj.dev/guides/proving/plonk/ > What ZeroJ's experimental PlonK support contains today, how to try it off-chain, and its known gaps. Groth16 is the supported path. > **Caution: Experimental, with no correctness claims** > > ZeroJ's PlonK support is **experimental everywhere**: pure-Java proving, off-chain verification, > `.zkey` import, and the on-chain validators. ZeroJ makes no claim that it is correct, sound, or > ready for use. **Groth16 on BLS12-381 is the supported path for the current release**; use > [Prove with Groth16](https://zeroj.dev/guides/proving/groth16/) for anything you build on. Use PlonK only to > evaluate or experiment, and never where value is at stake. PlonK uses a *universal* setup: one powers-of-tau reference string (SRS) serves every circuit up to its size, instead of a ceremony per circuit. ZeroJ has a pure-Java implementation over BLS12-381. This page describes what exists and how to try it, briefly and without recommending it. ### What exists | Piece | API | Module | |-------|-----|--------| | Compile a circuit | `circuit.compilePlonK(CurveId.BLS12_381)` → `PlonKConstraintSystem` | `zeroj-circuit-dsl` | | Development SRS | `PowersOfTauBLS381.generate(power)` (needs the insecure-setup flag) | `zeroj-crypto` | | Import a `.ptau` SRS | `PtauImporterBLS381.importPtau(input, maxPoints, expectedSha256)` | `zeroj-crypto` | | Setup | `PlonKSetupBLS381.setup(...)` → `PlonKProvingKeyBLS381` | `zeroj-crypto` | | Prove | `PlonKProverBLS381.prove(...)`; `proveCardano(...)` and `proveCardanoMpi(...)` for the on-chain profiles | `zeroj-crypto` | | Import a snarkjs PlonK `.zkey` | `PlonKZkeyImporterBLS381.importZkey(input, expectedSha256)` | `zeroj-crypto` | | Export snarkjs JSON | `SnarkjsPlonkJson.proofJson`, `verificationKeyJson`, `publicJson` | `zeroj-crypto` | | Verify off-chain | `PlonkBLS12381Verifier` | `zeroj-verifier-plonk` | | Verify on-chain | `PlonkBLS12381Verifier`, `PlonkBLS12381MultiInputVerifier`, `PlonkBLS12381MultiInputParamVerifier` (JuLC validators) | `zeroj-onchain-julc` | `zeroj-verifier-plonk` is deliberately **outside** `zeroj-bom-core`, so declare its version explicitly: ```groovy implementation 'org.zeroj:zeroj-verifier-plonk:0.1.0-pre12' ``` BN254 PlonK exists only as a legacy path and is disabled unless you start the JVM with `-Dzeroj.allowLegacyBn254=true`. It isn't a Cardano curve. ### Known gaps - **Blinding.** The PlonK prover uses 9 blinding scalars, while the PlonK paper and snarkjs use 11 (the quotient-split blinding factors are missing). This is tracked as an open issue affecting the zero-knowledge margin. - **snarkjs PlonK keys.** `PlonKZkeyImporterBLS381` reads the header, selectors, permutations and SRS, but not the wire-map sections, so ZeroJ can't prove a snarkjs-arithmetized circuit under an imported PlonK `.zkey`. - **No large-circuit work.** The memory and speed work that makes Groth16 practical at millions of constraints hasn't been applied to PlonK. - **Not audited,** and on-chain use is limited to bounded profiles (below). ### Try it off-chain The flow has more steps than Groth16 because you assign the three PlonK wire columns yourself. This mirrors the shape used in ZeroJ's own interop tests: ```java var circuit = CircuitBuilder.create("multiplier") .publicVar("c").secretVar("a").secretVar("b") .define(api -> api.assertEqual(api.mul(api.var("a"), api.var("b")), api.var("c"))); BigInteger[] witness = circuit.calculateWitness(Map.of( "c", List.of(BigInteger.valueOf(33)), "a", List.of(BigInteger.valueOf(3)), "b", List.of(BigInteger.valueOf(11))), CurveId.BLS12_381); PlonKConstraintSystem plonk = circuit.compilePlonK(CurveId.BLS12_381); // DEV/TEST ONLY: single-party SRS, needs -Dzeroj.allowInsecureTrustedSetup=true var srs = PowersOfTauBLS381.generate(8); int numGates = plonk.numGates(); BigInteger[][] selectors = new BigInteger[numGates][]; for (int i = 0; i < numGates; i++) { var row = plonk.gateRows().get(i); selectors[i] = new BigInteger[]{row.qL(), row.qR(), row.qO(), row.qM(), row.qC()}; } PlonKProvingKeyBLS381 pk = PlonKSetupBLS381.setup(numGates, plonk.numPublicInputs(), selectors, plonk.sigmaA(), plonk.sigmaB(), plonk.sigmaC(), plonk.numWires(), srs); BigInteger[] ext = plonk.extendWitness(witness); int n = pk.domainSize(); MontFr381[] wireA = new MontFr381[n], wireB = new MontFr381[n], wireC = new MontFr381[n]; for (int i = 0; i < n; i++) { if (i < numGates) { var row = plonk.gateRows().get(i); wireA[i] = MontFr381.fromBigInteger(ext[row.wireA()]); wireB[i] = MontFr381.fromBigInteger(ext[row.wireB()]); wireC[i] = MontFr381.fromBigInteger(ext[row.wireC()]); } else { wireA[i] = wireB[i] = wireC[i] = MontFr381.ZERO; } } BigInteger[] publicInputs = Arrays.copyOfRange(witness, 1, 1 + plonk.numPublicInputs()); PlonKProofBLS381 proof = PlonKProverBLS381.prove(pk, wireA, wireB, wireC, publicInputs); ``` Every prove call draws fresh blinding scalars from `SecureRandom`; an overload accepts your own `SecureRandom` instance. For an SRS from a public ceremony instead of the development generator, import a BLS12-381 `.ptau` with `PtauImporterBLS381.importPtau(...)`, pinning its SHA-256. Verify through the snarkjs JSON form: ```java String vkJson = SnarkjsPlonkJson.verificationKeyJson(pk); String proofJson = SnarkjsPlonkJson.proofJson(proof); String publicJson = SnarkjsPlonkJson.publicJson(publicInputs); var id = new CircuitId("multiplier"); byte[] vkBytes = vkJson.getBytes(StandardCharsets.UTF_8); var material = VerificationMaterial.of(vkBytes, ProofSystemId.PLONK, CurveId.BLS12_381, id, CanonicalHash.sha256(vkBytes)); // optional pin: the verifier checks the VK hash matches var envelope = SnarkjsPlonkCodec.toEnvelopeFromJson(proofJson, vkJson, publicJson, id); boolean ok = new PlonkBLS12381Verifier().verify(envelope, material).proofValid(); ``` The verifier accepts structured snarkjs/ZeroJ PlonK JSON. It doesn't accept gnark's binary PlonK proof format. ### Independent test vectors ZeroJ checks its PlonK transcript against artifacts it didn't produce: - BLS12-381 PlonK vectors generated by **gnark v0.14.0**, an independent implementation, in `zeroj-test-vectors` (`test-vectors/plonk-bls12381/`). `GnarkTranscriptCompatTest` checks ZeroJ's Fiat-Shamir transcript against them. The pinned generator lives in `assurance/gnark-fixtures` so the vectors stay reproducible. - A structured snarkjs PlonK vector (`test-vectors/snarkjs-plonk-bls12381/`) pins the JSON exporters byte for byte, and the interop suite checks proofs and keys in both directions against a pinned snarkjs CLI. Passing these vectors is evidence of compatibility on the tested cases. It is not a correctness or soundness guarantee. ### On-chain status The JuLC validators perform the KZG pairing check with Plutus V3's BLS12-381 built-ins, for two bounded profiles: | Validator | Profile | Prove with | |-----------|---------|------------| | `PlonkBLS12381Verifier` | Exactly one public input | `PlonKProverBLS381.proveCardano(...)` | | `PlonkBLS12381MultiInputVerifier` | 1 to 8 public inputs, supplied in the datum | `PlonKProverBLS381.proveCardanoMpi(...)` | | `PlonkBLS12381MultiInputParamVerifier` | 1 to 8 public inputs, pinned at script-application time | `PlonKProverBLS381.proveCardanoMpi(...)` | The Cardano profiles hash compressed curve points into the transcript, so proofs from the plain `prove(...)` won't verify on-chain. These validators are experimental and suitable at most for labeled, non-value-bearing testnet trials. As with Groth16, a valid proof is not authorization: a real validator must bind `ScriptContext`, prevent replay, and enforce its own rules. See [Verify on-chain](https://zeroj.dev/guides/verifying/on-chain/). Further reading: [plonk-support.md](https://github.com/bloxbean/zeroj/blob/main/docs/plonk-support.md), [ADR-0024 (PlonK release gates and multi-public-input profile)](https://github.com/bloxbean/zeroj/blob/main/docs/adr/0024-plonk-release-gates-and-multi-public-input-profile.md). ### Next steps - [Prove with Groth16](https://zeroj.dev/guides/proving/groth16/), the supported path - [Groth16, PlonK & BBS](https://zeroj.dev/learn/proof-systems/) - [Status & maturity](https://zeroj.dev/start/status/) --- ## Verify proofs in Java Source: https://zeroj.dev/guides/verifying/off-chain/ > Verify Groth16 proofs off-chain with ZeroJ's proof model, verifier backends and registries, and build a service that only trusts keys you pinned. Verification is the cheap half of zero-knowledge: a prover may spend seconds or minutes building a proof, but checking it takes milliseconds and needs no secrets. ZeroJ's off-chain verifiers are plain Java objects. You hand them a proof, its public inputs, and a verification key (VK), and they tell you whether the math holds. This page covers the proof model, the verifier backends, the registries that route proofs to them, what the verifiers check at the trust boundary, and how to wrap all of it in a service that only accepts proofs for circuits and keys you have pinned. > **Note: Math, not permission** > > A valid proof says *"someone knows a witness that satisfies this circuit for these public > inputs."* It does not say the caller is allowed to do anything. Replay protection, nullifiers and > business rules are yours to add. See [Secure your ZK application](https://zeroj.dev/guides/verifying/application-security/). ### Add the dependencies The Groth16 verifiers live in `zeroj-verifier-groth16`, which puts `zeroj-backend-spi` (the SPI and registries) and `zeroj-api` (the proof model) on your compile classpath. Add `zeroj-codec` yourself for the snarkjs JSON parser, CBOR and hashing helpers. ```groovy title="build.gradle" dependencies { implementation platform('org.zeroj:zeroj-bom-core:0.1.0-pre12') implementation 'org.zeroj:zeroj-verifier-groth16' implementation 'org.zeroj:zeroj-codec' // SnarkjsJsonCodec, CborEnvelopeCodec, CanonicalHash } ``` ### The proof model Everything in `org.zeroj.api` is immutable, fails fast on missing or blank values, and copies byte arrays on the way in and out. | Type | What it holds | |---|---| | `ZkProofEnvelope` | Proof bytes plus everything needed to check them: `proofSystem()`, `curve()`, `circuitId()`, `publicInputs()`, `vkRef()`, and optional `proofFormat()`, `metadata()`, `domainTag()`. Built with `ZkProofEnvelope.builder()`. | | `PublicInputs` | An ordered `List` of field elements (`new PublicInputs(values)`, `size()`, `get(i)`). Order must match the circuit's public inputs. | | `VerificationKeyRef` | A sealed interface: `ByHash(byte[] sha256)` (exactly 32 bytes) or `ById(String id)`. | | `VerificationMaterial` | The VK bytes plus `ProofSystemId`, `CurveId`, `CircuitId` and an optional pre-computed `vkHash`. Create with `VerificationMaterial.of(...)`. | | `CircuitId` | A non-blank string naming the circuit, e.g. `new CircuitId("age-check")`. | | `ProofSystemId` | `GROTH16`, `PLONK`, `FFLONK`, `HALO2`, `BBS`. Shipped verifiers cover `GROTH16`, `BBS` and (experimentally) `PLONK`; the others are identifiers only. | | `CurveId` | `BLS12_381` (Cardano's pairing curve), `PALLAS`, and legacy `BN254`. | For Groth16, the proof bytes and VK bytes are **snarkjs JSON** (`proof.json` and `verification_key.json`). That is the format ZeroJ's exporter writes and snarkjs reads, so the same three files work in both tools. See [Bring circom & snarkjs circuits](https://zeroj.dev/tutorials/snarkjs-interop/). ### Verify a Groth16 proof `SnarkjsJsonCodec.toEnvelopeFromJson` parses the three snarkjs files, checks that they agree with each other (same protocol and curve, public-input count equal to the VK's `nPublic`), and returns an envelope whose `vkRef` is `ByHash(SHA-256 of the exact VK JSON bytes)`. ```java title="VerifyOnce.java" import org.zeroj.api.CircuitId; import org.zeroj.api.CurveId; import org.zeroj.api.ProofSystemId; import org.zeroj.api.VerificationMaterial; import org.zeroj.api.VerificationResult; import org.zeroj.api.ZkProofEnvelope; import org.zeroj.codec.SnarkjsJsonCodec; import org.zeroj.verifier.groth16.bls12381.Groth16BLS12381PureJavaVerifier; import java.nio.charset.StandardCharsets; CircuitId circuitId = new CircuitId("multiplier"); // proofJson and publicJson come from the prover; vkJson comes from YOUR trusted storage. ZkProofEnvelope envelope = SnarkjsJsonCodec.toEnvelopeFromJson(proofJson, vkJson, publicJson, circuitId); VerificationMaterial material = VerificationMaterial.of( vkJson.getBytes(StandardCharsets.UTF_8), ProofSystemId.GROTH16, CurveId.BLS12_381, circuitId); VerificationResult result = new Groth16BLS12381PureJavaVerifier().verify(envelope, material); if (result.proofValid()) { // the pairing check passed — now apply your own policy } ``` `toEnvelopeFromJson` throws `CodecException` (a `RuntimeException`) for malformed or inconsistent JSON, so catch it when the input comes from outside your process. > **Caution: The VK is never the prover's to choose** > > Load the verification key from storage you control. If you verify against a VK the prover sent > you, they can send a key for a different, trivial circuit. ### Read the result `VerificationResult` is a record that keeps *cryptographic* validity separate from *policy* validity. | Accessor | Meaning | |---|---| | `proofValid()` | The cryptographic check passed. **This is the field a verifier backend sets.** | | `protocolValid()` | `Optional`: your policy checks passed (empty when none ran). | | `accepted()` | `true` only when both the proof and the policy are valid. | | `reasonCode()` | `Optional` explaining a rejection. | | `message()` | `Optional` human-readable detail. | On success a backend returns `VerificationResult.cryptoValid()`, so `proofValid()` is `true` but `accepted()` is **`false`**: no policy has been evaluated yet. Your service decides acceptance, for example by returning `VerificationResult.ok()` after its own checks pass, or `VerificationResult.policyRejected(ReasonCode.USED_NULLIFIER, "...")` when they fail. The `ReasonCode` enum covers both sides: | Code | Typically set by | |---|---| | `INVALID_PROOF` | A backend: the pairing check failed, or a point was malformed | | `INVALID_PUBLIC_INPUTS` | A backend: wrong count, or a value outside `[0, r)` | | `INTERNAL_ERROR` | A backend: an unexpected exception, e.g. unparseable proof bytes | | `UNKNOWN_VERIFICATION_KEY` | The orchestrator: the envelope's `vkRef` isn't registered | | `UNSUPPORTED_PROOF_SYSTEM` | The orchestrator (no backend registered) or a backend given the wrong proof system | | `VK_MISMATCH`, `UNSUPPORTED_CURVE`, `MALFORMED_ENVELOPE` | Consistency checks in the PlonK and BBS backends, and in your own gate | | `UNKNOWN_CIRCUIT`, `RETIRED_CIRCUIT`, `STALE_STATE_ROOT`, `DUPLICATE_NONCE`, `UNAUTHORIZED_SUBMITTER`, `USED_NULLIFIER` | Your policy layer | ### Choose a backend | Backend class | Module | Descriptor name | Notes | |---|---|---|---| | `Groth16BLS12381PureJavaVerifier` | `zeroj-verifier-groth16` | `groth16-bls12381-java` | Pure Java, no native code. The portable default. | | `Groth16BLS12381Verifier` | `zeroj-verifier-groth16` | `groth16-bls12381-blst` | Same checks, pairing via the native blst library (the `blst-java` JNI binding that `zeroj-blst` brings in). | | `PlonkBLS12381Verifier` | `zeroj-verifier-plonk` (opt-in) | — | **Experimental.** See [PlonK](https://zeroj.dev/guides/proving/plonk/). | | `BbsZkVerifier` | `zeroj-bbs` (opt-in) | `bbs-bls12381-java` | Verifies BBS presentations wrapped in an envelope. See [BBS](https://zeroj.dev/guides/credentials/bbs/). | > **Caution: Two classes named `Groth16BLS12381Verifier`** > > `org.zeroj.verifier.groth16.bls12381.Groth16BLS12381Verifier` is the **off-chain** blst-backed > verifier on this page. `org.zeroj.onchain.julc.groth16.validator.Groth16BLS12381Verifier` is the > **on-chain** Plutus validator. Check your imports. > **Caution: PlonK is experimental** > > The PlonK verifier is an experimental, opt-in path. Groth16 on BLS12-381 is the supported focus of > the current release; don't treat PlonK verification as production evidence. The legacy BN254 verifiers are not registered with `ServiceLoader` and refuse to run unless you start the JVM with `-Dzeroj.allowLegacyBn254=true`. BN254 is not a Cardano curve; ignore it unless you are running old off-chain experiments. ### Route proofs with registries When a service handles more than one circuit or proof system, let the orchestrator pick the key and the backend. - **`VerifierRegistry`** (`org.zeroj.verifier.core`) holds backends. `VerifierRegistry.empty()` plus `register(...)` gives you an explicit list; `VerifierRegistry.withServiceLoader()` discovers every `ZkVerifier` on the classpath. - **`VerificationKeyRegistry`** (`org.zeroj.backend.spi`) resolves a `VerificationKeyRef` to `VerificationMaterial`. `InMemoryVerificationKeyRegistry` stores each registered key under its SHA-256 (computed from the VK bytes if you didn't supply one) **and** under its circuit ID. - **`VerifierOrchestrator`** resolves the envelope's `vkRef`, finds a backend for its proof system and curve, and delegates. ```java import org.zeroj.backend.spi.InMemoryVerificationKeyRegistry; import org.zeroj.backend.spi.VerificationKeyRegistry; import org.zeroj.verifier.core.VerifierOrchestrator; import org.zeroj.verifier.core.VerifierRegistry; VerificationKeyRegistry keys = new InMemoryVerificationKeyRegistry(); keys.register(VerificationMaterial.of( vkJson.getBytes(StandardCharsets.UTF_8), ProofSystemId.GROTH16, CurveId.BLS12_381, circuitId)); VerifierRegistry backends = VerifierRegistry.empty(); backends.register(new Groth16BLS12381PureJavaVerifier()); var orchestrator = new VerifierOrchestrator(backends, keys); VerificationResult result = orchestrator.verify(envelope); // UNKNOWN_VERIFICATION_KEY if not registered ``` `VerifierRegistry.find` returns the *first* backend that supports a proof-system/curve pair. With `withServiceLoader()`, `zeroj-verifier-groth16` lists the blst-backed verifier before the pure-Java one, so discovery picks blst. Register backends explicitly when you need to know which one runs, and when you build a GraalVM native image. The orchestrator resolves whatever `vkRef` the envelope carries. Every key in the registry is therefore reachable by any caller, including through `ById` lookups by circuit ID. Treat the registry as an allowlist and put only keys you intend to accept in it, or use the pinned-gate pattern below. ### What the verifiers enforce The Groth16 backends fail closed on malformed input before any pairing work: - **Bounded, strict JSON.** The snarkjs codec caps documents at 8 MiB, rejects duplicate keys, and accepts only canonical decimal strings (no sign, no leading zeros). - **Public inputs.** The count must equal the VK's `nPublic`, and every value must be a canonical scalar in `[0, r)`. A value like `x + r` is rejected with `INVALID_PUBLIC_INPUTS`, never silently reduced. - **Points.** Every proof and VK point, including every `IC` entry, must have coordinates in `[0, p)`, lie on the curve, be in the prime-order subgroup, and **not be the point at infinity**. An infinity `IC` entry would leave a public input unbound, so ZeroJ's setup refuses to create one and every verifier rejects it. What the Groth16 backends do **not** check: that `envelope.circuitId()` or `envelope.vkRef()` matches the `VerificationMaterial` you passed. They verify the proof against whatever key you give them. (The experimental PlonK verifier does compare them and returns `VK_MISMATCH`.) Do that comparison yourself, as the next section shows. ### Build a verification service A verification endpoint should trust nothing in the envelope except the proof and public inputs. Pin each circuit's VK by hash in configuration, look it up by the circuit *your endpoint expects*, and compare the envelope against it before verifying. ```java title="ProofGate.java" import org.zeroj.api.CircuitId; import org.zeroj.api.CurveId; import org.zeroj.api.ProofSystemId; import org.zeroj.api.VerificationKeyRef; import org.zeroj.api.VerificationMaterial; import org.zeroj.api.VerificationResult; import org.zeroj.api.VerificationResult.ReasonCode; import org.zeroj.api.ZkProofEnvelope; import org.zeroj.codec.CanonicalHash; import org.zeroj.verifier.groth16.bls12381.Groth16BLS12381PureJavaVerifier; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import java.util.HexFormat; import java.util.Map; /** Verifies only proofs for circuits and verification keys this service has pinned. */ public final class ProofGate { private final Groth16BLS12381PureJavaVerifier verifier = new Groth16BLS12381PureJavaVerifier(); private final Map pinned; // "age-check@1" -> material public ProofGate(Map pinned) { this.pinned = Map.copyOf(pinned); } /** Load a VK you control and refuse it unless it matches the SHA-256 pinned in config. */ public static VerificationMaterial pin(Path vkJson, CircuitId circuitId, String expectedSha256Hex) throws Exception { byte[] vkBytes = Files.readAllBytes(vkJson); byte[] actual = CanonicalHash.sha256(vkBytes); if (!Arrays.equals(actual, HexFormat.of().parseHex(expectedSha256Hex))) { throw new IllegalStateException("verification key does not match pinned hash: " + vkJson); } return VerificationMaterial.of(vkBytes, ProofSystemId.GROTH16, CurveId.BLS12_381, circuitId, actual); } /** {@code expected} comes from your endpoint or business flow, never from the envelope. */ public VerificationResult verify(String expected, ZkProofEnvelope envelope) { VerificationMaterial material = pinned.get(expected); if (material == null) { return VerificationResult.error(ReasonCode.UNKNOWN_CIRCUIT, "circuit not allowlisted: " + expected); } if (envelope.proofSystem() != material.proofSystemId() || envelope.curve() != material.curveId() || !envelope.circuitId().equals(material.circuitId())) { return VerificationResult.error(ReasonCode.VK_MISMATCH, "envelope does not match " + expected); } if (!(envelope.vkRef() instanceof VerificationKeyRef.ByHash ref) || !Arrays.equals(ref.hash(), material.vkHash().orElseThrow())) { return VerificationResult.error(ReasonCode.VK_MISMATCH, "unexpected verification key"); } return verifier.verify(envelope, material); // cryptographic check only } } ``` Then layer policy on top. Here the circuit exposes a nullifier as its second public input: ```java VerificationResult crypto = gate.verify("age-check@1", envelope); if (!crypto.proofValid()) { return crypto; } BigInteger nullifier = envelope.publicInputs().get(1); // your circuit's nullifier position if (!usedNullifiers.add(nullifier)) { // durable, atomic store in production return VerificationResult.policyRejected(ReasonCode.USED_NULLIFIER, "nullifier already used"); } return VerificationResult.ok(); ``` A few rules that keep this honest: - **Version your allowlist keys.** `@ZKCircuit(name = "age-check", version = 2)` produces the same `CircuitId` (`"age-check"`) as version 1; the version travels separately in envelope metadata. Key your pinned map by name *and* version, and give each version its own VK hash. - **The VK hash is byte-exact.** `toEnvelopeFromJson` hashes the raw VK JSON. Re-formatting the file (whitespace, key order) changes the hash, so pin the exact bytes you ship. - **Retire keys deliberately.** When a circuit changes, remove the old entry (or answer `RETIRED_CIRCUIT`) instead of leaving every historical key live. ### Move envelopes between services `zeroj-codec` gives you two helpers for transport and content addressing: ```java import org.zeroj.codec.CanonicalHash; import org.zeroj.codec.CborEnvelopeCodec; byte[] cbor = CborEnvelopeCodec.encode(envelope); // integer-keyed CBOR map ZkProofEnvelope decoded = CborEnvelopeCodec.decode(cbor); // bounded, typed CodecException on bad input byte[] id = CanonicalHash.hash(envelope); // SHA-256 over the core fields ``` The CBOR encoding carries the core fields only: version, proof system, curve, circuit ID, proof bytes, public inputs, and the VK reference. Optional fields such as `metadata()` (where annotated circuits put their version) and `proofFormat()` are **not** carried, and `CanonicalHash` ignores them too. If your policy depends on them, send them another way, or rely on the pinned VK hash, which already distinguishes circuit versions. ### Write a custom backend A backend implements `ZkVerifier` from `zeroj-backend-spi`: a `descriptor()` naming the proof system and curve it handles, and a `verify(envelope, material)` that returns a crypto-only result. ```java import org.zeroj.api.CurveId; import org.zeroj.api.ProofSystemId; import org.zeroj.api.VerificationMaterial; import org.zeroj.api.VerificationResult; import org.zeroj.api.ZkProofEnvelope; import org.zeroj.backend.spi.BackendDescriptor; import org.zeroj.backend.spi.ZkVerifier; public final class MyVerifier implements ZkVerifier { @Override public BackendDescriptor descriptor() { return new BackendDescriptor(ProofSystemId.GROTH16, CurveId.BLS12_381, "my-verifier"); } @Override public VerificationResult verify(ZkProofEnvelope envelope, VerificationMaterial material) { try { // 1. parse and validate every input: encodings, ranges, curve/subgroup, infinity // 2. run the verification equation return checkProof(envelope, material) ? VerificationResult.cryptoValid() : VerificationResult.proofInvalid("verification equation failed"); } catch (IllegalArgumentException e) { return VerificationResult.proofInvalid("malformed proof or key: " + e.getMessage()); } } private boolean checkProof(ZkProofEnvelope envelope, VerificationMaterial material) { throw new UnsupportedOperationException("your verification logic"); // fail closed until written } } ``` To make it discoverable, list its fully qualified name in `META-INF/services/org.zeroj.backend.spi.ZkVerifier`. Match the built-in backends' validation: canonical encodings, range checks, curve and subgroup membership, and the infinity rules above. A backend with weaker checks silently weakens every service that discovers it. ### Next steps - [Verify proofs on Cardano](https://zeroj.dev/guides/verifying/on-chain/) - [Secure your ZK application](https://zeroj.dev/guides/verifying/application-security/) - [Bring circom & snarkjs circuits](https://zeroj.dev/tutorials/snarkjs-interop/) - [API cheat sheet](https://zeroj.dev/reference/api-cheatsheet/) --- ## Verify proofs on Cardano Source: https://zeroj.dev/guides/verifying/on-chain/ > Run Groth16 verification inside a Plutus V3 validator with zeroj-onchain-julc, bind proofs to the spend, and plan budgets and reference scripts. Cardano's Plutus V3 ledger has native BLS12-381 builtins (CIP-0381): point decompression, group operations, Miller loops and a final pairing check. That is everything a Groth16 verifier needs, so a smart contract can check a zero-knowledge proof itself. ZeroJ's `zeroj-onchain-julc` module ships that verifier as Java source compiled to Plutus by JuLC (a Java-to-Plutus compiler), plus the off-chain codecs that turn a ZeroJ or snarkjs proof into the bytes the validator expects. This guide explains the pieces and how to compose them. For a step-by-step walkthrough on a local devnet, follow [Verify your proof on Cardano](https://zeroj.dev/tutorials/verify-on-cardano/). > **Danger: A valid proof is not authorization** > > The reusable verifiers check the math and nothing else. A real validator must also bind the proof > to the transaction (`ScriptContext`), prevent replay, enforce who gets paid, and apply your business > rules. ZeroJ's on-chain Groth16 path is **Beta, testnet only**: correctness-tested and verified > end-to-end on Yaci DevKit, but not audited and not for value-bearing or mainnet use. ### What's in the module | Component | Package (`org.zeroj.onchain.julc.…`) | Status | Use it for | |---|---|---|---| | `Groth16BLS12381Verifier` | `groth16.validator` | Working, crypto-only | Learning, tests, and as the reference shape. Never protects value alone. | | `Groth16BLS12381TxOutRefBindingVerifier` | `groth16.validator` | Reference example | Shows the check that binds the first public input to the spent output. It reads that value from the guarded UTxO's datum, so it can't lock real funds as-is ([why](#bind-the-proof-to-the-spend)). | | `Groth16BLS12381Lib` | `groth16.lib` | Working `@OnchainLibrary` | Composing Groth16 verification into **your own** validator. | | `ProverToCardano`, `SnarkjsToCardano` | `groth16.codec` | Off-chain helpers | Compressing ZeroJ or snarkjs proofs/VKs into validator parameters and redeemers. | | `Groth16AuthenticatedStateTransitionValidator` and its script factory | `groth16.validator`, `groth16.codec` | Experimental | Poseidon MPF/JMT state transitions. See [Large authenticated state](https://zeroj.dev/guides/credentials/authenticated-state/). | | `BbsProofVerify`, `BbsHashToScalar` | `bbs.lib` | Working libraries, fixed profile | On-chain BBS selective disclosure. See [BBS](https://zeroj.dev/guides/credentials/bbs/). | | `PlonkBLS12381Lib`, `PlonkBLS12381Verifier`, `PlonkBLS12381MultiInputVerifier`, `PlonkBLS12381MultiInputParamVerifier`, `PlonKProverToCardano` | `plonk.*` | **Experimental** | Labeled testnet trials only. | | `ScriptBudgetEstimator`, `OnChainFeasibility` | `analysis` | Planning helpers | Rough budget estimates and a proof-system/curve feasibility matrix. | | `ReferenceScriptDeployer` | `deployment` | Config helper | Describing CIP-0033 reference-script deployment patterns. Does not submit transactions. | > **Caution: Name clash** > > `org.zeroj.onchain.julc.groth16.validator.Groth16BLS12381Verifier` (on-chain validator) and > `org.zeroj.verifier.groth16.bls12381.Groth16BLS12381Verifier` (off-chain blst verifier) share a > simple name. Import the one you mean. ### Where each piece of the proof goes The generic Groth16 validator splits the statement across the three places a Cardano spend can carry data: | Data | Where | Why | |---|---|---| | Verification key (`vkAlpha`, `vkBeta`, `vkGamma`, `vkDelta`, `vkIc`) | Script **parameters**, applied at load time | The VK becomes part of the script hash, so the script address itself pins the key. | | Public inputs | **Datum**: a list of integers in VK order | Fixed when the UTxO is locked. | | Proof (`piA`, `piB`, `piC`) | **Redeemer**: `Constr 0 [piA, piB, piC]` of compressed points | Supplied by whoever spends. | `vkIc` holds one compressed G1 point per public input plus one (`IC[0]`). The validator checks the datum length matches. Inside `Groth16BLS12381Lib.verify`, every public input must be in `[0, r)`, and every proof and VK point must be a canonical compressed encoding (48 bytes for G1, 96 for G2) that is not the point at infinity. Only then does it fold the public inputs into `vk_x` and run the pairing check with the Plutus builtins. ### Load the generic verifier Compress the VK and proof off-chain, then apply the VK as parameters with JuLC's `JulcScriptLoader`. This mirrors ZeroJ's Yaci DevKit end-to-end test. ```java title="LoadVerifier.java" import com.bloxbean.cardano.client.address.AddressProvider; import com.bloxbean.cardano.client.common.model.Networks; import com.bloxbean.cardano.client.plutus.spec.BigIntPlutusData; import com.bloxbean.cardano.client.plutus.spec.BytesPlutusData; import com.bloxbean.cardano.client.plutus.spec.ConstrPlutusData; import com.bloxbean.cardano.client.plutus.spec.ListPlutusData; import com.bloxbean.cardano.julc.clientlib.JulcScriptLoader; import org.zeroj.onchain.julc.groth16.codec.ProverToCardano; import org.zeroj.onchain.julc.groth16.validator.Groth16BLS12381Verifier; // keys: a Groth16Keys handle; proof: keys.prove(...); witness[1..numPublic] are the public inputs var vk = ProverToCardano.compressVk(keys); var compressedProof = ProverToCardano.compressProof(proof); var ic = ListPlutusData.of(); for (byte[] point : vk.ic()) { ic.add(new BytesPlutusData(point)); } var script = JulcScriptLoader.load(Groth16BLS12381Verifier.class, new BytesPlutusData(vk.alpha()), new BytesPlutusData(vk.beta()), new BytesPlutusData(vk.gamma()), new BytesPlutusData(vk.delta()), ic); String scriptAddress = AddressProvider.getEntAddress(script, Networks.testnet()).toBech32(); var datum = ListPlutusData.of(BigIntPlutusData.of(witness[1])); // one entry per public input var redeemer = ConstrPlutusData.builder() .alternative(0) .data(ListPlutusData.of( new BytesPlutusData(compressedProof.piA()), new BytesPlutusData(compressedProof.piB()), new BytesPlutusData(compressedProof.piC()))) .build(); ``` Lock funds at `scriptAddress` with the datum, then spend that UTxO with the redeemer and the script attached (or referenced), using Cardano Client Lib's `QuickTxBuilder`. If your proof and key came from snarkjs, use `SnarkjsToCardano.parseVk(vkJson)`, `SnarkjsToCardano.parseProof(proofJson)` and `SnarkjsToCardano.parsePublicInputs(publicJson)` instead. They return the same `VkCompressed` and `ProofCompressed` records. Dependencies for the off-chain side: ```groovy title="build.gradle" dependencies { implementation platform('org.zeroj:zeroj-bom-core:0.1.0-pre12') implementation 'org.zeroj:zeroj-onchain-julc' implementation "com.bloxbean.cardano:julc-cardano-client-lib:0.1.0-pre16" // JulcScriptLoader implementation "com.bloxbean.cardano:cardano-client-lib:0.8.0-pre5" runtimeOnly "com.bloxbean.cardano:julc-vm-java:0.1.0-pre16" } ``` ### Why the generic verifier is not enough `Groth16BLS12381Verifier.validate` ignores its `ScriptContext` argument. Three consequences follow: 1. **Replay across UTxOs.** If two UTxOs carry the same datum, one proof unlocks both. 2. **Front-running.** A proof is public the moment its transaction hits the mempool. Anyone can copy the redeemer into their own transaction that spends the same UTxO and pays themselves. 3. **No business rules.** Nothing checks signers, outputs, deadlines, or nullifiers. #### Bind the proof to the spend The standard fix for the first problem is to make public input 0 depend on the UTxO being spent: `spendRef = blake2b_256(spentTxId || outputIndex) mod r`, with the index encoded as 32 big-endian bytes. The circuit exposes `spendRef` as its first public input, the prover computes it for the UTxO it is about to spend, and the validator recomputes it from `ScriptContext`. A proof made for one UTxO then fails for every other one. It does **not** stop front-running: a watcher can still copy the redeemer into a transaction that spends the same UTxO and pays itself, so bind the beneficiary too. `Groth16BLS12381TxOutRefBindingVerifier` demonstrates this check, and its own documentation calls it an example policy. Don't use it to lock real funds as-is. > **Caution: The bundled binding verifier can't guard a real UTxO** > > `Groth16BLS12381TxOutRefBindingVerifier` takes **all** public inputs, including `spendRef`, from > the datum of the UTxO it protects. That UTxO's out-ref only exists once the locking transaction > exists, and the transaction id is a hash over the output that carries the datum. A datum can't > contain a hash of its own transaction, so no real UTxO can satisfy it. It only passes in VM tests > with synthetic contexts. In your own validator, compute `spendRef` from `ScriptContext` and feed it > into the proof check yourself, keeping only the application's public inputs in the datum or > redeemer. [Verify your proof on Cardano](https://zeroj.dev/tutorials/verify-on-cardano/) builds such a validator > step by step. The fix for all three problems is a custom validator that ties the statement to the transaction. ### Write a custom validator Define your own spending validator, keep the proof record local (JuLC decodes records per validator), and call `Groth16BLS12381Lib.verify` alongside your own checks. The validator below binds the proof to the UTxO being spent the right way: the datum holds only the application's public inputs, and the validator computes `spendRef` from `ScriptContext` and prepends it before checking the proof. It is the validator built and tested in [Verify your proof on Cardano](https://zeroj.dev/tutorials/verify-on-cardano/), where it unlocks a real lock transaction on an in-memory ledger and rejects the same proof replayed against a second UTxO (about 3.02 billion CPU steps and 263,000 memory units for two application inputs). ```java title="SpendBoundGroth16Verifier.java" package com.example.onchain; import com.bloxbean.cardano.julc.core.PlutusData; import com.bloxbean.cardano.julc.ledger.ScriptContext; import com.bloxbean.cardano.julc.ledger.ScriptInfo; import com.bloxbean.cardano.julc.ledger.TxOutRef; import com.bloxbean.cardano.julc.stdlib.Builtins; import com.bloxbean.cardano.julc.stdlib.annotation.Entrypoint; import com.bloxbean.cardano.julc.stdlib.annotation.Param; import com.bloxbean.cardano.julc.stdlib.annotation.SpendingValidator; import org.zeroj.onchain.julc.groth16.lib.Groth16BLS12381Lib; import java.math.BigInteger; /** * Groth16 verifier whose statement is bound to the UTxO being spent. * *

The datum holds only the application's public inputs, e.g. [a, product]. The validator * computes spendRef = blake2b_256(txId || outputIndex as 32 bytes) mod r for the UTxO it is * validating and prepends it, so the proof is checked against [spendRef, a, product]. A proof * made for one UTxO therefore fails for every other UTxO.

*/ @SpendingValidator public class SpendBoundGroth16Verifier { @Param static byte[] vkAlpha; // G1 compressed, 48 bytes @Param static byte[] vkBeta; // G2 compressed, 96 bytes @Param static byte[] vkGamma; // G2 compressed, 96 bytes @Param static byte[] vkDelta; // G2 compressed, 96 bytes @Param static PlutusData vkIc; // list of G1 compressed IC points record Groth16Proof(byte[] piA, byte[] piB, byte[] piC) {} @Entrypoint public static boolean validate(PlutusData datum, Groth16Proof proof, ScriptContext ctx) { PlutusData publicInputs = Builtins.listData( Builtins.mkCons(Builtins.iData(spendRef(ctx)), Builtins.unListData(datum))); return Groth16BLS12381Lib.verify(publicInputs, proof.piA(), proof.piB(), proof.piC(), vkAlpha, vkBeta, vkGamma, vkDelta, vkIc); } /** blake2b_256(txId || outputIndex as 32-byte big-endian) mod r; -1 (always rejected) if not a spend. */ private static BigInteger spendRef(ScriptContext ctx) { ScriptInfo scriptInfo = ctx.scriptInfo(); if (scriptInfo instanceof ScriptInfo.SpendingScript spendingScript) { TxOutRef txOutRef = spendingScript.txOutRef(); byte[] indexBytes = Builtins.integerToByteString(true, 32, txOutRef.index()); byte[] preimage = Builtins.appendByteString(txOutRef.txId().hash(), indexBytes); return Builtins.byteStringToInteger(true, Builtins.blake2b_256(preimage)).mod(fr()); } else { return BigInteger.valueOf(-1); } } /** The BLS12-381 scalar field order r. */ private static BigInteger fr() { BigInteger base = BigInteger.valueOf(1000000000000000000L); return BigInteger.valueOf(52435L).multiply(base) .add(BigInteger.valueOf(875175126190479447L)).multiply(base) .add(BigInteger.valueOf(740508185965837690L)).multiply(base) .add(BigInteger.valueOf(552500527637822603L)).multiply(base) .add(BigInteger.valueOf(658699938581184513L)); } } ``` The circuit must declare `spendRef` as its **first** public input and use it in a real constraint, and the prover computes it for the out-ref it is about to spend. The tutorial shows the matching circuit and the off-chain `blake2b_256` computation. Unlike the bundled `Groth16BLS12381TxOutRefBindingVerifier`, nothing here requires a datum to contain a hash of its own transaction. A spend binding alone doesn't stop front-running on the same UTxO. Add whatever else your application needs: - **Bind the beneficiary.** Put the recipient's key hash in the statement (or the datum) and check that an output pays them, so a copied proof can't redirect funds. The BBS claim validator in [zeroj-usecases](https://github.com/bloxbean/zeroj-usecases) (`reusable-kyc`) does exactly this. - **Enforce single use.** Spending a UTxO is already one-time. For "one action per person", add a nullifier and record it on-chain. See [Secure your ZK application](https://zeroj.dev/guides/verifying/application-security/#nullifiers). - **Check signers, validity ranges, minted tokens and continuing outputs** as your protocol requires. To compile a validator that calls `Groth16BLS12381Lib`, add ZeroJ's on-chain module and the JuLC annotation processor to your build. The runnable apps in zeroj-usecases use: ```groovy title="build.gradle" dependencies { implementation "org.zeroj:zeroj-onchain-julc:0.1.0-pre12" annotationProcessor "org.zeroj:zeroj-onchain-julc:0.1.0-pre12" implementation "com.bloxbean.cardano:julc-stdlib:0.1.0-pre16" annotationProcessor "com.bloxbean.cardano:julc-annotation-processor:0.1.0-pre16" testImplementation "com.bloxbean.cardano:julc-testkit:0.1.0-pre16" // run validators in the JuLC VM testRuntimeOnly "com.bloxbean.cardano:julc-vm-java:0.1.0-pre16" } ``` The `annotationProcessor "org.zeroj:zeroj-onchain-julc"` line is required: JuLC compiles library code such as `Groth16BLS12381Lib` from the sources that module ships under `META-INF/plutus-sources`, and without it compilation fails with `Plutus compilation error: Undefined variable: Groth16BLS12381Lib`. If you use the BOM with versionless coordinates, add `annotationProcessor platform('org.zeroj:zeroj-bom-core:0.1.0-pre12')` as well. Only the JuLC testkit and VM lines are for running validators locally; loading a compiled script and submitting transactions needs just JuLC's `julc-cardano-client-lib` and Cardano Client Lib. Test the validator in the JuLC VM with positive cases **and** negative ones: wrong public input, tampered proof, a different spent UTxO, an output paying someone else. Then run it on [Yaci DevKit](https://zeroj.dev/tutorials/verify-on-cardano/) before any public testnet. ### Budgets and script size A Groth16 check is four Miller loops and one final verification, plus one G1 scalar multiplication and addition per public input. ZeroJ's JuLC VM tests measured the generic `Groth16BLS12381Verifier`, evaluated as a spending validator with a full `ScriptContext`, at **2,627,770,348 CPU steps and 177,749 memory units** for one public input, about 2.82 billion CPU steps and 212,000 memory units for two, and about 3.02 billion and 246,000 for three. Your own validator's policy checks come on top, and a JuLC upgrade can shift these numbers. For planning, `ScriptBudgetEstimator` gives estimates built from BLS12-381 builtin costs (they leave out the rest of the validator, so measured budgets come out higher), and `OnChainFeasibility` reports what ZeroJ considers workable: ```java import org.zeroj.api.CurveId; import org.zeroj.api.ProofSystemId; import org.zeroj.onchain.julc.analysis.OnChainFeasibility; import org.zeroj.onchain.julc.analysis.ScriptBudgetEstimator; long cpu = ScriptBudgetEstimator.estimateCpu(ProofSystemId.GROTH16, CurveId.BLS12_381, 3); var entry = OnChainFeasibility.lookup(ProofSystemId.GROTH16, CurveId.BLS12_381); // status WORKING boolean bn254 = OnChainFeasibility.isFeasible(ProofSystemId.GROTH16, CurveId.BN254); // false ``` Estimates are not measurements. Before relying on a validator, measure it in the JuLC VM with a representative transaction and compare against the target network's current per-transaction execution limits. Keep public inputs few: each one costs a scalar multiplication on-chain. #### Reference scripts (CIP-0033) Attaching the full validator, with its VK parameters applied, to every spending transaction makes each one larger and more expensive. Instead, publish the script once in a UTxO as a **reference script** and point spends at it. That shrinks transactions and fees; the execution budget is unchanged. `ReferenceScriptDeployer.DeploymentConfig` records the three patterns ZeroJ describes (`VK_IN_SCRIPT`, `REFERENCE_SCRIPT_DATUM_VK`, `VK_HASH_COMMITMENT`); your transaction builder does the actual work. If you ever supply the VK through a datum or redeemer instead of script parameters, the validator must check it against a hash it already trusts. An unauthenticated VK lets the spender pick the circuit. ### JuLC version coupling The compiled UPLC, and therefore the **script hash and address**, is produced by the JuLC compiler. ZeroJ 0.1.0-pre12 builds against JuLC 0.1.0-pre16. Treat a JuLC upgrade as a new script: recompute the hash, re-measure budgets, and plan how funds locked at the old address move. ZeroJ's authenticated-state release tooling binds the compiler version into each release identity for exactly this reason. Renaming Java packages alone does not change the hash: the `org.zeroj` namespace move left script hashes identical. See [Migration notes](https://zeroj.dev/reference/migration/). ### PlonK and BBS on-chain > **Caution: PlonK on-chain is experimental** > > The PlonK validators implement a KZG pairing check for their supported profiles, but they are > experimental, opt-in, unaudited, and meant for labeled testnet trials only. ZeroJ makes no > correctness claim for them. Use Groth16. For completeness: `PlonkBLS12381Verifier` handles one public input, `PlonkBLS12381MultiInputVerifier` takes 1–8 public inputs from the datum, and `PlonkBLS12381MultiInputParamVerifier` takes them as script parameters, so the statement values are pinned by the script hash. `ScriptBudgetEstimator` records roughly 4.8 billion CPU steps for the one-input profile. `BbsProofVerify` verifies a BBS selective-disclosure presentation natively. It is unrolled for one fixed shape: a **5-message credential disclosing indexes 2 and 3**, measured at about 2.44 billion CPU steps and 183,509 memory units. Other shapes need a different unrolling. The off-chain half is `BbsToCardano` in `zeroj-bbs`; the [BBS guide](https://zeroj.dev/guides/credentials/bbs/) shows both. ### Next steps - [Verify your proof on Cardano](https://zeroj.dev/tutorials/verify-on-cardano/): the step-by-step tutorial - [Secure your ZK application](https://zeroj.dev/guides/verifying/application-security/) - [ZK on Cardano](https://zeroj.dev/learn/zk-on-cardano/) - [Verify proofs in Java](https://zeroj.dev/guides/verifying/off-chain/) --- ## Secure your ZK application Source: https://zeroj.dev/guides/verifying/application-security/ > A practical threat-model checklist for ZeroJ builders — authorization, replay, nullifiers, trusted inputs, setup, circuit soundness and secrets. A zero-knowledge proof answers one narrow question: *does the prover know private values that satisfy this circuit, for these public inputs, under this verification key?* Every other property your application needs (who may act, how often, with what money, based on whose data) is something **you** must design, bind and check. This page walks through the mistakes that turn a correct proof into a broken application, and ends with a checklist you can copy into a design review. It applies to off-chain services and Cardano validators alike. > **Caution: Research software** > > ZeroJ is experimental and not externally audited. Nothing on this page makes it safe for > value-bearing or mainnet use. It helps you avoid the application-level bugs that no library can > prevent for you. ### Start with a threat model Before writing a circuit, sort every value into one of three buckets: | Bucket | Examples | Rule | |---|---|---| | **Untrusted** | The proof, public inputs, envelope fields (`circuitId`, `vkRef`, metadata), BBS presentations, datums anyone can create, redeemers | Validate, and bind to context, before acting on them. | | **Secret** | The witness, issuer signing keys, BBS key material, wallet seeds, trusted-setup toxic waste | Minimize lifetime, never log or persist, keep off shared machines. | | **Trusted** | Verification keys *you* pinned, issuer public keys *you* distribute, your own state (nullifier sets, roots) | Load from storage you control. Never accept these from the prover. | ### Proof validity is not authorization ZeroJ's verifiers return `proofValid() == true` when the math checks out. They do not know who sent the proof, whether it was used before, or whether the action it justifies is allowed. On Cardano, the reusable `Groth16BLS12381Verifier` validator doesn't even look at the transaction. Keep the two decisions visibly separate in code. Off-chain, a verifier backend returns `VerificationResult.cryptoValid()`, with `accepted() == false`. Only your policy layer should produce `VerificationResult.ok()`. See [Verify proofs in Java](https://zeroj.dev/guides/verifying/off-chain/#build-a-verification-service). ### Replay and front-running A proof is a public, copyable object. Anyone who sees it (in a log, an API response, or the Cardano mempool) can submit it again unless the statement ties it to one specific use. - **Bind to the spend.** On Cardano, make a public input depend on the UTxO being spent, for example `spendRef = blake2b_256(spentTxId || outputIndex) mod r` as public input 0. Your validator must compute that value from `ScriptContext`, not read it from the datum of the UTxO it guards: a datum can't contain a hash of its own transaction. The bundled `Groth16BLS12381TxOutRefBindingVerifier` reads it from the datum, so it demonstrates the check but can't lock real funds as-is. See [Verify proofs on Cardano](https://zeroj.dev/guides/verifying/on-chain/#bind-the-proof-to-the-spend). - **Bind to the beneficiary.** A proof that is bound only to a UTxO can still be front-run: a watcher copies the redeemer into a transaction that pays *them*. Put the recipient in the statement and check the outputs on-chain. - **Bind to a session off-chain.** For API verification, include a server-issued, random, single-use nonce in the statement and consume it on first use. - **BBS presentation headers.** A BBS presentation signs over its presentation header (`ph`). Off-chain, the verifier picks `ph` as a fresh random challenge and accepts it once. On-chain, derive the expected `ph` from `ScriptContext`, e.g. `blake2b_256(txId || index || recipientPkh)`. `BbsService.verifyPresentation` reads `ph` from the presentation itself, so **you** must compare it with the value you expect. See [Selective disclosure with BBS](https://zeroj.dev/guides/credentials/bbs/). ### Nullifiers When the rule is "each member may act once", such as one vote per election or one claim per airdrop, use a **nullifier**: a public value that is the same every time the same member acts in the same scope, but reveals nothing about who they are. ```text nullifier = Poseidon(memberSecret, scopeId) // scopeId: election, airdrop, epoch… ``` The circuit must prove all of these at once: 1. `memberSecret` belongs to an eligible member, e.g. `Poseidon(memberSecret)` is a leaf under the published membership Merkle root; 2. `nullifier` was computed from **that same** secret and the public `scopeId`; 3. whatever else the action requires. If step 1 and step 2 use different secrets, a member can mint unlimited fresh nullifiers. Different scopes give unlinkable nullifiers, so a member's votes in two elections can't be connected. For Cardano circuits, compute Poseidon with the explicit BLS12-381 parameters, `PoseidonParamsBLS12_381T3.INSTANCE`. Then **store and check** spent nullifiers: | Where | How | Trade-off | |---|---|---| | Off-chain database | Unique index on the nullifier; insert atomically in the same transaction as the action | Simple and fast; you are trusted to enforce it | | On-chain state UTxO | One registry UTxO whose datum lists nullifiers | Trustless, but sequential and limited by datum size | | On-chain token per nullifier | Mint a token named by the nullifier | Parallel, but must still prevent duplicate names | | On-chain sorted linked list | One UTxO per nullifier; insertion proves the gap | Trustless and concurrent; locks some ADA per entry | | On-chain Merkle root | Keep only a root; prove insertion in a second circuit | Constant size; needs an off-chain tree service | ZeroJ's [private voting design](https://zeroj.dev/use-cases/private-voting/) compares these in detail. The [private allowlist tutorial](https://zeroj.dev/tutorials/private-allowlist/) builds a membership proof with a nullifier. ### Bind public inputs to their meaning A public input is just a number. The verifier checks the proof *for that number*. It doesn't know the number is meant to be "the current epoch" or "this pool's address". - **Check every public input against the real context** at verification time: amounts against the transaction value, addresses against outputs, epochs and deadlines against the validity range, roots against your stored state. - **Keep the order fixed.** Public inputs are positional. Document the order next to the circuit, and test that swapping two inputs makes verification fail. - **Separate domains.** Include a scope, network or contract-instance identifier in hashes and nullifiers, so a proof for one deployment is useless in another. ### Trust your data sources A circuit proves a computation over its inputs. It cannot tell whether a secret input is *true*. If a prover can type their own balance, age or KYC status into the witness, the proof only shows they can type. Anchor every fact to something the verifier already trusts: - an **issuer signature** over the attributes (a BBS credential, or a signature checked in-circuit); - a **commitment** published by a trusted party or on-chain, such as a Merkle root of balances or of allowlisted members, that the circuit opens; - a **key the user provably controls**, as in the account-recovery use case, which derives a Cardano key inside the circuit. Then ask who can change that anchor, and how the verifier learns about updates and revocations. ### Trusted setup, key pinning and versions - **Groth16 keys come from a ceremony.** ZeroJ's in-process setup (`Groth16Keys.setupInMemory`, `PowersOfTauBLS381.generate`, `Groth16SetupBLS381.setup`) knows the toxic waste and can forge proofs. It refuses to run unless you opt in, and it is for development only. Production keys come from a multi-party ceremony (snarkjs `.zkey`, imported). See [Trusted setup, explained](https://zeroj.dev/learn/trusted-setup/) and the [ceremony guide](https://zeroj.dev/guides/proving/trusted-setup-ceremony/). - **Pin the VK.** Off-chain, compare the VK's SHA-256 against a value in your configuration. On-chain, the VK is a script parameter, so the script hash pins it. - **Version circuits.** Any change to a circuit, even a "harmless" refactor, can change its constraints and therefore its keys. Give each version its own ID, VK hash and (on-chain) script, and retire old versions explicitly. - **Record provenance.** Keep the circuit source revision, compiler version, R1CS hash, ceremony transcript and VK hash together, so anyone can check the chain from code to key. ### Under-constrained circuits The most common ZK bug is a circuit that *computes* the right answer for honest inputs but doesn't *constrain* it. A dishonest prover then finds other witness values that also satisfy the constraints. Passing tests with honest witnesses tells you nothing about this. - **Write invalid-witness tests.** For every rule, craft a witness that breaks it and check that witness generation fails, or that a proof built from it doesn't verify. See [Testing circuits](https://zeroj.dev/guides/circuits/testing-circuits/). - **Range-check numbers.** Inputs are field elements. ZeroJ's witness calculator reduces each input modulo the field order, so `-1` becomes a 255-bit number. Comparisons like "age ≥ 18" are only meaningful with explicit bit-width constraints. - **Constrain booleans and outputs.** A "flag" must be constrained to 0 or 1, and every output you rely on must be tied to the inputs by constraints, not just assigned. - **Don't trust hints.** Values computed outside the constraint system (inverses, quotients) must be checked by constraints afterwards. - **Prefer the library gadgets** and read their status in the [gadget guide](https://zeroj.dev/guides/circuits/gadgets/). ### Secrets in Java - **`BigInteger` is not constant-time**, and ZeroJ does not claim that witness generation or the pure-Java prover are constant-time. Run proving on hardware you control, not next to untrusted tenants. - **BBS issuer keys.** The default pure-Java BBS provider routes secret scalar multiplications through fixed-schedule code, but it is *not* a full JVM constant-time guarantee. For issuer keys and high-value signing, select the blst provider with `BbsService.withBlsProvider(BbsCiphersuite.BLS12381_SHA256, BlstBls12381Provider.createDefault())`. - **Short witness lifetime.** The witness (`BigInteger[]`) contains every secret. Don't log it, cache it or serialize it. snarkjs `.wtns` files hold the same secrets, so keep them off disk or on tmpfs and delete them. JVM objects can't be reliably wiped, so keep secrets in a short-lived process where you can. - **Randomness.** Use `SecureRandom` for key material, nonces and challenges. BBS key generation needs at least 32 bytes of key material. ### Metadata leaks A perfect proof can still leak through everything around it: - **Public inputs and revealed attributes** are public. Combinations of "harmless" disclosed fields (birth year + postcode + employer) can identify a person. - **On-chain linkability.** Funding a claim from a known wallet, reusing addresses, or paying fees from the same UTxO set links actions together, whatever the proof hides. - **Amounts and timing.** Exact amounts and the timing between issuance, proving and submission can correlate users. Batch, round, or delay where it matters. - **Scope reuse.** Reusing one nullifier scope across unrelated actions makes them linkable. ### Development flags never reach production Two JVM switches exist for development only: | Flag | What it unlocks | |---|---| | `-Dzeroj.allowInsecureTrustedSetup=true` / `ZEROJ_ALLOW_INSECURE_TRUSTED_SETUP=true` | Single-party trusted setup whose creator can forge proofs | | `-Dzeroj.allowLegacyBn254=true` / `ZEROJ_ALLOW_LEGACY_BN254=true` | Legacy BN254 proving/verification (not a Cardano curve) | Make production refuse to start with either one set: ```java import org.zeroj.api.LegacyCurvePolicy; import org.zeroj.api.TrustedSetupPolicy; if (TrustedSetupPolicy.insecureTrustedSetupEnabled() || LegacyCurvePolicy.legacyBn254Enabled()) { throw new IllegalStateException("ZeroJ development flags are enabled; refusing to start"); } ``` See [Configuration](https://zeroj.dev/reference/configuration/) for every switch. ### The checklist Copy this into your design review. Every "no" or "not sure" is work to do before real users arrive. ```text title="zk-app-review-checklist.md" ## Statement - [ ] Every intended rule is a constraint; outputs, booleans and ranges are constrained - [ ] Invalid-witness tests exist for every rule (not just honest-witness tests) - [ ] Public vs secret inputs are correct; public-input order is documented and tested - [ ] Secret facts are anchored to an issuer signature, commitment or key the verifier trusts ## Binding & replay - [ ] The statement is bound to its context (UTxO / tx / session nonce / scope / network) - [ ] The beneficiary is bound, so a copied proof can't redirect value (front-running) - [ ] Nullifier = H(secret, scope) uses the SAME secret as the membership proof - [ ] Spent nullifiers are stored and checked atomically (on-chain state or unique DB index) - [ ] BBS: ph is verifier-chosen (or derived from ScriptContext) and compared before accepting ## Keys & setup - [ ] Groth16 keys come from a multi-party ceremony; provenance recorded - [ ] VK hashes pinned in config; circuit IDs + versions allowlisted; old versions retired - [ ] The verifier never accepts a VK, circuit ID or VK reference from the prover - [ ] zeroj.allowInsecureTrustedSetup / zeroj.allowLegacyBn254 are rejected at startup ## Cardano - [ ] Validator binds ScriptContext: spent input, outputs, signers, validity range, minting - [ ] Budget measured in the JuLC VM and on a devnet against current protocol limits - [ ] Script hash recorded; JuLC upgrades treated as a new script ## Secrets & privacy - [ ] Witness data is short-lived, never logged/persisted; .wtns files kept off disk - [ ] Issuer keys use the blst BBS provider; SecureRandom for keys, nonces, challenges - [ ] Disclosed attributes, public inputs, amounts, timing and addresses reviewed for linkability ## Evidence - [ ] Independent test vectors / differential checks where they exist - [ ] External review planned before any value-bearing use ``` ### Next steps - [Verify proofs on Cardano](https://zeroj.dev/guides/verifying/on-chain/) - [Testing circuits](https://zeroj.dev/guides/circuits/testing-circuits/) - [Private voting use case](https://zeroj.dev/use-cases/private-voting/) - [Configuration](https://zeroj.dev/reference/configuration/) --- ## Selective disclosure with BBS Source: https://zeroj.dev/guides/credentials/bbs/ > Issue BBS credentials, derive presentations that reveal only chosen attributes, verify them in Java or on Cardano, and bind them against replay. BBS is a signature scheme built for credentials. An **issuer** signs a list of attributes once. The **holder** can then show any subset of those attributes to a **verifier**, together with a zero-knowledge proof that the issuer signed the full set. The hidden attributes stay hidden, and two presentations of the same credential can't be linked to each other by the proof alone. Think of a passport where you can black out any lines you like before showing it, and the border officer can still check the government's stamp. ZeroJ implements BBS in the opt-in `zeroj-bbs` module, following the IRTF CFRG draft **`draft-irtf-cfrg-bbs-signatures-10`**. It is a draft, not yet an RFC. ### BBS or a circuit? | You want to… | Use | |---|---| | Reveal some signed attributes exactly as issued ("country = NZ") and hide the rest | **BBS**: no circuit, no trusted setup | | Prove a *predicate* over a hidden value ("age ≥ 18", "balance > 1000") | A **Groth16 circuit** (see [Prove you're over 18](https://zeroj.dev/tutorials/age-check/)) | | Prove membership in a set without revealing which member | A circuit with a Merkle proof ([private allowlist](https://zeroj.dev/tutorials/private-allowlist/)) | | Verify on Cardano with a flexible statement | Groth16. On-chain BBS supports one fixed disclosure shape (see below). | BBS reveals values; it doesn't compute on them. If the verifier only needs a yes/no answer about a hidden attribute, you want a circuit. ### Status | Operation | Status | |---|---| | Verification (`Verify`, `ProofVerify`) | **Beta**: vector-tested, not audited, spec is an IRTF draft | | Issuance and presentation (`KeyGen`, `Sign`, `ProofGen`) | **Beta with caveat**: the default pure-Java provider is not constant-time; prefer the blst provider for issuer keys | Both BLS12-381 ciphersuites from the draft are implemented: `BLS12381_SHA256` (the default, `BBS_BLS12381G1_XMD:SHA-256_SSWU_RO_`) and `BLS12381_SHAKE256`. The test suite runs the draft's official fixture vectors for both, across the pure-Java and blst providers. ### Add the dependency `zeroj-bbs` is published but sits **outside** `zeroj-bom-core`, so give it an explicit version: ```groovy title="build.gradle" dependencies { implementation platform('org.zeroj:zeroj-bom-core:0.1.0-pre12') implementation 'org.zeroj:zeroj-bbs:0.1.0-pre12' // opt-in, explicit version implementation 'org.zeroj:zeroj-blst' // optional: native provider for issuers } ``` ```xml title="pom.xml" org.zeroj zeroj-bbs 0.1.0-pre12 ``` ### Issue, present, verify This complete program plays all three roles. Each role would normally be a different party. ```java title="BbsQuickstart.java" import org.zeroj.bbs.BbsCiphersuite; import org.zeroj.bbs.BbsKeyPair; import org.zeroj.bbs.BbsPresentation; import org.zeroj.bbs.BbsPresentationCodec; import org.zeroj.bbs.BbsPublicKey; import org.zeroj.bbs.BbsRevealedMessage; import org.zeroj.bbs.BbsService; import org.zeroj.bbs.BbsSignature; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.Arrays; import java.util.List; public class BbsQuickstart { public static void main(String[] args) { BbsService bbs = BbsService.pureJava(); // draft-10, BLS12-381 SHA-256 ciphersuite // ---- Issuer: create a key pair (key material is secret, at least 32 bytes) ---- byte[] keyMaterial = new byte[32]; new SecureRandom().nextBytes(keyMaterial); BbsKeyPair issuer = bbs.keyPair(keyMaterial, utf8("acme-kyc-issuer-2026")); // ---- Issuer: sign five attributes under a credential header ---- List attributes = List.of( utf8("name:Alice Liddell"), // 0 utf8("dob:1990-04-01"), // 1 utf8("country:NZ"), // 2 utf8("kycLevel:verified"), // 3 utf8("docId:P1234567")); // 4 byte[] header = utf8("acme-kyc-credential-v1"); BbsSignature signature = bbs.sign(issuer.secretKey(), issuer.publicKey(), attributes, header); // ---- Holder: check the credential on receipt ---- boolean credentialOk = bbs.verify(issuer.publicKey(), signature, attributes, header); // ---- Verifier: issue a fresh, single-use challenge ---- byte[] challenge = new byte[32]; new SecureRandom().nextBytes(challenge); // ---- Holder: reveal only country (2) and kycLevel (3), bound to the challenge ---- BbsPresentation presentation = bbs.derivePresentation( issuer.publicKey(), signature, attributes, header, challenge, new int[]{2, 3}); byte[] wire = BbsPresentationCodec.encode(presentation); // send this to the verifier // ---- Verifier: decode, check the bindings, then the proof ---- BbsPresentation received = BbsPresentationCodec.decode(wire); BbsPublicKey trustedIssuer = new BbsPublicKey(issuer.publicKey().bytes(), BbsCiphersuite.BLS12381_SHA256); boolean freshAndBound = Arrays.equals(received.presentationHeader(), challenge) && Arrays.equals(received.header(), header); boolean proofOk = bbs.verifyPresentation(trustedIssuer, received); System.out.println("credentialOk=" + credentialOk + " freshAndBound=" + freshAndBound + " proofOk=" + proofOk); for (BbsRevealedMessage m : received.revealedMessages()) { System.out.println(" revealed[" + m.index() + "] = " + new String(m.message(), StandardCharsets.UTF_8)); } } private static byte[] utf8(String s) { return s.getBytes(StandardCharsets.UTF_8); } } ``` Output: ```text credentialOk=true freshAndBound=true proofOk=true revealed[2] = country:NZ revealed[3] = kycLevel:verified ``` A few API details worth knowing: - **Argument order.** `sign(secretKey, publicKey, messages, header)` and `verify(publicKey, signature, messages, header)` put messages before the header. Overloads with the draft's order, `(…, header, messages)`, also exist. - **Disclosed indexes** are zero-based and must be strictly ascending: `new int[]{2, 3}`, not `{3, 2}`. - **Every presentation is fresh.** `derivePresentation` draws new randomness from `SecureRandom` each time, so repeated presentations don't share proof bytes. - **Tampering fails.** Change a revealed message, the header or `ph`, and `verifyPresentation` returns `false`. - **Distribute the issuer key as bytes.** `publicKey.bytes()` on the issuer side, `new BbsPublicKey(bytes, BbsCiphersuite.BLS12381_SHA256)` on the verifier side. Verifiers must get it from a source they trust, not from the holder. ### Headers and presentation headers BBS has two "extra data" fields, and they do different jobs: | | Set by | Signed/proved over | Use it for | |---|---|---|---| | **header** | Issuer, at signing | The signature and every presentation | Credential type, schema version, issuer context. Verifiers should expect a specific value. | | **presentation header (`ph`)** | Holder, at presentation, usually from the verifier's challenge | That one presentation | Binding a presentation to one session or transaction, which prevents replay | `verifyPresentation(publicKey, presentation)` takes both values **from the presentation itself**. It proves the holder used *some* `ph`; it does not know which one you expected. Always compare `presentation.presentationHeader()` with the challenge you issued and consume the challenge, and compare `presentation.header()` with the credential type you accept. Without that, a captured presentation can be replayed forever. On Cardano there is no interactive challenge, so derive `ph` from the transaction instead. For example, `blake2b_256(spentTxId || outputIndex || recipientPkh)`, recomputed by the validator from `ScriptContext`. Then a presentation only works for the one UTxO and recipient it was made for. ### Encode attributes carefully Revealed messages are raw bytes. The proof says "the issuer signed these bytes at these positions", nothing more. - **Fix the schema.** Agree on what each index means (0 = name, 2 = country, …) and have verifiers check the index of every revealed message, not just its value. - **Make values unambiguous.** Prefix values with the attribute name, or use a canonical encoding, so `"NZ"` in one position can't be confused with another field. - **Remember what BBS doesn't do.** Expiry, revocation, holder binding (proving the presenter is the person the credential was issued to), and issuer trust are application policy. ### Choose a provider `BbsService.pureJava()` needs no native code. Its secret-scalar operations go through fixed-schedule code, but it is **not** a full JVM constant-time guarantee. For issuer keys, and anywhere high-value signing keys or proof randomness live, select the native blst provider. The API stays the same: ```java import org.zeroj.bbs.BbsCiphersuite; import org.zeroj.bbs.BbsService; import org.zeroj.blst.BlstBls12381Provider; BbsService issuerService = BbsService.withBlsProvider( BbsCiphersuite.BLS12381_SHA256, BlstBls12381Provider.createDefault()); ``` Signatures and presentations produced with either provider verify with the other: the key pair derived from the same key material is identical. ### Verify through the ZeroJ SPI If your service already routes proofs through ZeroJ's verifier registry, `BbsZkVerifier` (registered with `ServiceLoader`, descriptor name `bbs-bls12381-java`) verifies a presentation carried in a `ZkProofEnvelope`: - `proofSystem` = `ProofSystemId.BBS`, `curve` = `CurveId.BLS12_381`; - proof bytes = `BbsPresentationCodec.encode(presentation)`, proof format `bbs-cfrg-draft10-presentation-cbor-v1`; - `VerificationMaterial` VK bytes = the issuer's public key bytes. Like every ZeroJ backend, it checks cryptography only. The `ph`/header comparison above is still yours. See [Verify proofs in Java](https://zeroj.dev/guides/verifying/off-chain/). ### Verify on Cardano `zeroj-onchain-julc` includes `BbsProofVerify`, a native Plutus V3 implementation of BBS `ProofVerify`, and `BbsToCardano` in `zeroj-bbs` prepares its inputs off-chain: ```java import org.zeroj.bbs.cardano.BbsToCardano; var params = BbsToCardano.verifierParams(issuer.publicKey(), header, attributes.size()); // validator @Params var proof = BbsToCardano.onChainProof(presentation); // redeemer fields ``` `verifierParams` bakes the issuer key, the generators and the header-dependent domain into the validator's parameters, so the script hash pins both the issuer and the credential header. > **Caution: Fixed profile** > > `BbsProofVerify.verify` is unrolled for **one shape: a 5-message credential disclosing indexes 2 > and 3** (hiding 0, 1 and 4). Plutus has no cheap dynamic loop, so other shapes need a different > unrolling. It uses the SHA-256 ciphersuite and was measured at about 2.44 billion CPU steps and > 183,509 memory units in the JuLC VM. On-chain BBS is an opt-in, unaudited path for testnets. Your validator composes `BbsProofVerify` with its own policy: check that the disclosed values match what the datum requires, that `ph` equals the value recomputed from `ScriptContext`, and that the payout goes to the intended recipient. The `reusable-kyc` app in [zeroj-usecases](https://github.com/bloxbean/zeroj-usecases) is a complete worked example. See [Verify proofs on Cardano](https://zeroj.dev/guides/verifying/on-chain/) for the general validator pattern. ### Next steps - [Selective disclosure use case](https://zeroj.dev/use-cases/selective-disclosure/) - [Age & KYC use case](https://zeroj.dev/use-cases/age-and-kyc/) - [Secure your ZK application](https://zeroj.dev/guides/verifying/application-security/) - [Groth16, PlonK & BBS](https://zeroj.dev/learn/proof-systems/) --- ## Large authenticated state (Poseidon MPF/JMT) Source: https://zeroj.dev/guides/credentials/authenticated-state/ > Keep millions of entries off-chain, publish one Poseidon root, and prove single-key reads and updates with small Groth16 circuits. Experimental. Many private applications revolve around a big table: a registry of members, credential status flags, account balances, or the state of a rollup-like app. You can't put millions of entries on Cardano, and you can't put them in a circuit. You can, however, keep them in an authenticated tree off-chain, publish only the tree's **root**, and prove statements about one entry at a time. ZeroJ's `zeroj-mpf-poseidon` and `zeroj-jmt-poseidon` modules do exactly that. They adapt Cardano Client Lib's Merkle Patricia Forestry (MPF) and Jellyfish Merkle Tree (JMT) to a Poseidon hash over BLS12-381, so the same tree the service stores can be opened inside a Groth16 circuit. > **Caution: Experimental** > > Both modules are **experimental**: opt-in, outside the stable BOM, and not externally reviewed. > The 5-million-entry runs described below used deliberately insecure local setups, and the > production gates listed at the end are still open. Don't protect value with them. ### How it works ```text OFF-CHAIN (your service) RocksDB MPF/JMT, millions of entries │ read one native proof path for key K ▼ strict verification + normalization ──► bounded, canonical witness │ ▼ operation-specific circuit ──► Groth16 prover ──► 192-byte proof │ ON-CHAIN (Cardano) ▼ state UTxO datum: old root ──► validator: verify proof over (old root, new root) and check the continuing state UTxO carries new root ``` The circuit never sees the database. It sees one path, padded to a fixed maximum length, and the root(s) as public inputs. Proving cost depends on that path bound, not on how many entries the tree holds. ### Operation-specific circuits Each operation has its own narrow circuit, its own R1CS hash and its own keys. That keeps the statement explicit and the constraint count small. There is no generic "do anything" circuit whose behaviour a prover could steer. | Statement | MPF | JMT | Public inputs | |---|:---:|:---:|---| | Inclusion: key/value is in the tree | ✓ | ✓ | `root` | | Non-inclusion: the path ends empty | ✓ | ✓ | `root` | | Non-inclusion: a different key occupies the path | ✓ | ✓ | `root` | | Value update | ✓ | ✓ | `oldRoot`, `newRoot` | | Insert at an empty spot | ✓ | ✓ | `oldRoot`, `newRoot` | | Insert beside a different leaf | ✓ | ✓ | `oldRoot`, `newRoot` | | Tombstone update | — | ✓ | `oldRoot`, `newRoot`, tombstone hash | | Physical delete, multiproofs, batch updates | deferred | deferred | — | The circuits are exposed as `PoseidonMpfCircuitTemplates` / `PoseidonJmtCircuitTemplates` factory methods (`inclusion(maxSteps)`, `valueUpdate(maxSteps)`, …) and as `ZkMpf*` / `ZkJmt*` gadgets. ### MPF or JMT? | | Poseidon MPF | Poseidon JMT | |---|---|---| | Structure | Radix-16 Patricia trie with compressed paths | Versioned Jellyfish Merkle Tree | | Best for | Membership/read-heavy state, compact native proofs, existing MPF data | Update-heavy state that needs version history, rollback and pruning | | Measured complete profile at 5M entries | S9 (≤ 9 branch steps) | S12 (≤ 12 levels) | | Circuit size at that profile | 56,635 constraints | 14,057 constraints | | Median Groth16 prove | 4.173 s | 2.901 s | | Native (private) proof size | 805–939 B sampled | 2,744–3,161 B sampled | | Deletion | No physical delete yet | Tombstones only; a tombstone is **not** proof of absence | Both produce the same 192-byte compressed Groth16 proof and a 432-byte compressed VK for one public input, so on-chain cost is the same: ZeroJ measured **2,627,770,348 CPU steps and 177,749 memory units** in the JuLC VM. The JMT's larger native proof is private prover input and never reaches the chain. The two root profiles (`zeroj-poseidon-mpf-v1`, `zeroj-poseidon-jmt-v1`) are incompatible with each other and with classic Blake2b MPF/JMT roots, including Aiken MPF roots. ### Pick a path bound A circuit covers paths up to a fixed length ("S9" = 9 steps). Choosing it is a data question: - **Measure the depth** of your real tree. In the 5M MPF run, S8 covered all but 218 entries; S9 covered all of them. In the 5M JMT run, S8 covered 99.74 % and S12 covered everything. - **Plan the overflow.** Approve two or more exact profiles, route each request to the smallest one that fits, and reject (or send to an approved fallback) anything deeper. Never silently truncate or pad a path outside the canonical rules. - **Stop key grinding.** Poseidon keeps honest paths roughly balanced, but a user who can choose keys freely can try to create deep paths. Derive keys in a way callers can't grind, or use a fixed-depth design. ### Prove an inclusion (MPF) Witness factories first verify the native CCL proof strictly, then normalize it into circuit inputs. They reject malformed proofs and paths deeper than the bound before any proving work. ```java title="MpfInclusion.java" import org.zeroj.api.CurveId; import org.zeroj.circuit.CircuitBuilder; import org.zeroj.circuit.annotation.ZkInputMap; import org.zeroj.merkle.mpf.poseidon.ccl.PoseidonMpfTrie; import org.zeroj.merkle.mpf.poseidon.circuit.PoseidonMpfCircuitTemplates; import org.zeroj.merkle.mpf.poseidon.profile.PoseidonMpfHash; import org.zeroj.merkle.mpf.poseidon.profile.PoseidonMpfValueCommitment; import org.zeroj.merkle.mpf.poseidon.witness.PoseidonMpfBranchWitness; import java.math.BigInteger; PoseidonMpfTrie trie = PoseidonMpfTrie.inMemory(); // or PoseidonMpfTrie.create(nodeStore, root) trie.put(keyBytes, valueBytes); byte[] root = trie.getRootHash(); byte[] proofWire = trie.getProofWire(keyBytes).orElseThrow(); int maxBranches = 8; PoseidonMpfBranchWitness witness = PoseidonMpfBranchWitness.inclusion( root, keyBytes, valueBytes, proofWire, maxBranches); // throws on an invalid/too-deep proof ZkInputMap inputs = new ZkInputMap() .put(PoseidonMpfCircuitTemplates.ROOT, PoseidonMpfHash.fieldFromDigestBytes(root)) .put(PoseidonMpfCircuitTemplates.VALUE, PoseidonMpfValueCommitment.field(valueBytes)); witness.putInto(inputs); CircuitBuilder circuit = PoseidonMpfCircuitTemplates.inclusion(maxBranches); BigInteger[] circuitWitness = circuit.calculateWitness(inputs.toWitnessMap(), CurveId.BLS12_381); // …then compile, set up (ceremony keys in production) and prove as for any Groth16 circuit ``` The JMT flow has the same shape: `PoseidonJmtTree`, `PoseidonJmtInclusionWitness.create(...)`, `PoseidonJmtCircuitTemplates.inclusion(maxLevels)`. ### What the proof does and doesn't say - A root-only inclusion proof says "the prover knows *some* entry under this root", and nothing public about which one. It doesn't bind a particular key to a user. Add the key, owner, nullifier, version or transaction fields your application needs in your own circuit. - JMT version numbers are storage coordinates, not authenticated state. Bind `{chain point, version, root}` in your application, with one logical writer. - A JMT tombstone is still an included value. Never present it as non-inclusion. ### The state-transition validator `zeroj-onchain-julc` includes a representative Cardano validator, `Groth16AuthenticatedStateTransitionValidator`, also experimental. It reads the old root from the state-token UTxO being spent and the new root from the single continuing state-token UTxO. It enforces a version increment, the authorized signer, value and token conservation and no minting, then verifies one operation-specific Groth16 proof over the two roots. Its release tooling (`Groth16AuthenticatedStateTransitionScriptFactory`) binds the exact circuit manifest, R1CS and VK identities, validator-template digest, JuLC compiler profile, script hash, network, state token, signer and a one-shot genesis attestation, and it refuses benchmark bundles on mainnet. The state token's minting policy is outside Groth16: you must independently ensure it mints exactly one token, once. ### Add the dependency Both modules are published outside `zeroj-bom-core`, so pin the version. RocksDB is **not** a dependency. The persistent load and benchmark tools live in the unpublished `benchmarks/` projects (`-PincludeBenchmarks`). ```groovy title="build.gradle" dependencies { implementation 'org.zeroj:zeroj-mpf-poseidon:0.1.0-pre12' // or zeroj-jmt-poseidon } ``` ### Gates that remain open The local tests cover golden vectors, CCL compatibility, malicious witness mutations, cross-operation and cross-structure replay, real value transitions and JuLC VM evaluation. They show the implementation is internally consistent and scales on the measured machine. They do **not** show production assurance. Before protecting value, ZeroJ's own guide requires: 1. freezing every deployed operation and profile, with published manifest, R1CS hash, VK identity, Poseidon fingerprint, compiler version and validator-template hash; 2. independent circuit and cryptographic review; 3. a reviewed Groth16 ceremony for each exact circuit; 4. full state-token transactions validated on Yaci DevKit and a public network under current protocol parameters; 5. an enforced one-shot, supply-of-one state-token policy; 6. operational procedures for backup, restore, compaction, retention, rollback and rebuild; 7. monitoring of proof depth, prover latency and memory, and rejected over-bound requests. ### Further reading - 5M benchmark reports: [MPF](https://github.com/bloxbean/zeroj/blob/main/docs/benchmarks/poseidon-mpf-5m-2026-08-02.md), [JMT](https://github.com/bloxbean/zeroj/blob/main/docs/benchmarks/poseidon-jmt-5m-2026-08-03.md) - [Practical large-state guide](https://github.com/bloxbean/zeroj/blob/main/docs/merkle/practical-large-state-guide.md) and the [authenticated-state v1 specification](https://github.com/bloxbean/zeroj/blob/main/docs/merkle/poseidon-authenticated-state-v1.md) - Design notes: [ADR-0042](https://github.com/bloxbean/zeroj/blob/main/docs/adr/0042-operation-specific-poseidon-mpf-and-jmt-circuits.md) ### Next steps - [Private allowlist with a Merkle tree](https://zeroj.dev/tutorials/private-allowlist/) - [Verify proofs on Cardano](https://zeroj.dev/guides/verifying/on-chain/) - [Proving performance](https://zeroj.dev/guides/proving/performance/) --- ## Use cases Source: https://zeroj.dev/use-cases/overview/ > What zero-knowledge proofs unlock on Cardano — nine worked use cases, the building block behind each, and runnable demos. A zero-knowledge proof lets you prove a fact without revealing the data behind it. "I'm over 18" without a birth date. "I'm on the voter list" without saying which voter. "Our reserves cover every deposit" without publishing a single balance. A Cardano validator can check that kind of proof in one transaction. The secret inputs never leave the prover's machine. This section walks through what that makes possible. Each page explains the real-world problem, the statement being proven, what stays private, how the proof reaches the chain, and the security questions you still have to answer. Most pages also link a runnable demo. > **Caution: Research software** > > ZeroJ is experimental and has not been externally audited. The demos run against a local > [Yaci DevKit](https://github.com/bloxbean/yaci-devkit) devnet with development-only trusted setups. > Treat them as blueprints to learn from, not as systems for real identities, votes or funds. See > [Status & maturity](https://zeroj.dev/start/status/). ### Identity & compliance - [Age & KYC eligibility](https://zeroj.dev/use-cases/age-and-kyc/): Prove you're old enough and live in an approved country, using an issuer-signed credential, without revealing either value. - [Selective disclosure & reusable KYC](https://zeroj.dev/use-cases/selective-disclosure/): Do KYC once, then show each service only the attributes or predicates it needs, with Groth16 predicates or BBS verified on-chain. ### DeFi & finance - [Proof of reserves](https://zeroj.dev/use-cases/proof-of-reserves/): A custodian proves reserves cover all customer balances without publishing any balance. - [Private token transfers](https://zeroj.dev/use-cases/private-payments/): A design walkthrough of a shielded pool: deposit, then withdraw to a fresh address that can't be linked to the deposit. ### Governance & communities - [Private voting](https://zeroj.dev/use-cases/private-voting/): An eligible member votes exactly once, and nobody can tell which member cast which ballot. - [One claim per person](https://zeroj.dev/use-cases/sybil-resistant-airdrop/): A Sybil-resistant airdrop: a personhood credential claims once per epoch, and the payout is bound to a recipient. ### Ownership & assets - [Private NFT ownership](https://zeroj.dev/use-cases/nft-ownership/): Prove you hold an NFT from a collection for token-gated access without revealing your wallet. - [Prove you own a Cardano account](https://zeroj.dev/use-cases/account-recovery/): Prove you know the root key behind an address, without revealing the seed, so a refund can reach the real owner after a hack. ### Supply chain - [Digital product passport](https://zeroj.dev/use-cases/digital-product-passport/): Prove a product meets carbon, recycled-content and origin rules without exposing supplier data. ### Pick the right tool Almost every use case is built from a small set of patterns. Start from what you need to prove: | You need to… | Approach | ZeroJ building blocks | |---|---|---| | Prove a predicate over a hidden value (age ≥ 18, carbon ≤ 50 kg, reserves ≥ liabilities) | Groth16 circuit | `ZkUInt` with `@UInt(bits = …)` and `gte` / `lte` comparisons | | Reveal some issuer-signed attributes and hide the rest | BBS selective disclosure (no circuit, no trusted setup) | `BbsService` in `zeroj-bbs`; `BbsProofVerify` for on-chain checks | | Prove you're in a set without saying which member | Merkle tree + Poseidon | `ZkMerkle.verifyProofPoseidon` with `PoseidonParamsBLS12_381T3.INSTANCE` | | Allow one action per person, credential or NFT | Nullifier | `ZkPoseidon.hash(zk, PoseidonParamsBLS12_381T3.INSTANCE, secret, contextId)`, stored on-chain (sorted list, spent UTxO) | | Rely on a fact someone else attested | Issuer signature checked inside the circuit | `ZkEdDSAJubjub.verifyWithRegisteredKey` | | Stop a copied proof from paying someone else | Bind the recipient or the spent UTxO into the public inputs | recipient public input; a spend reference computed from `ScriptContext` in your validator | | Prove facts about large private state (millions of entries) | Poseidon MPF / JMT (**experimental**) | `zeroj-mpf-poseidon`, `zeroj-jmt-poseidon` | Groth16 on BLS12-381 is the default proof system for everything above. Circuits use Poseidon with explicit BLS12-381 parameters. For the concepts behind these patterns, read [Circuits, constraints & witnesses](https://zeroj.dev/learn/circuits-and-witnesses/) and [ZK on Cardano](https://zeroj.dev/learn/zk-on-cardano/). > **Note: A proof is not a permission slip** > > A valid proof only shows that the math checks out. Every validator in these use cases also has to > bind the proof to its `ScriptContext` (who gets paid, which UTxO is spent), prevent replay and > enforce its own business rules. [Application security](https://zeroj.dev/guides/verifying/application-security/) > covers this in depth. ### Run the demos The [zeroj-usecases](https://github.com/bloxbean/zeroj-usecases) repository has complete Spring Boot apps for most pages here. Each one covers the circuit, proof generation, a Cardano transaction and on-chain verification, with a small web UI. You need Docker (with Compose v2) and a Yaci DevKit devnet running on your machine: ```bash # 1. Start a local Cardano devnet (outside the demos repo) devkit start # then, at the yaci-cli prompt: # create-node -o --start # 2. Run a demo end to end git clone https://github.com/bloxbean/zeroj-usecases cd zeroj-usecases ./demo.sh proof-of-reserves --run # 3. Stop it (Yaci keeps running) ./demo.sh proof-of-reserves --stop ``` `demo.sh` tops up a devnet-only demo wallet, starts the app, opens its UI and, with `--run`, runs the happy path. The demo names are `proof-of-reserves`, `identity-kyc`, `nft-ownership`, `voting`, `airdrop`, `dpp`, `selective-disclosure` and `reusable-kyc`. The account-ownership demo ships separately as a desktop app and CLI; see [its page](https://zeroj.dev/use-cases/account-recovery/#try-it). The demos use a single-party development trusted setup (they run with `-Dzeroj.allowInsecureTrustedSetup=true`, and cached setup files are generated the same way). Real deployments need keys from a multi-party ceremony; see [Trusted setup, explained](https://zeroj.dev/learn/trusted-setup/). ### Ideas to explore These ideas are **not implemented as demos**. They show how the same building blocks carry over to new problems. - **Private credit score for lending.** A borrower proves "score ≥ 700" from a credit bureau's signed credential. That needs an in-circuit issuer signature (`ZkEdDSAJubjub`), a `ZkUInt.gte` range check and a public input bound to the loan UTxO. BBS is an option if revealing a score band is acceptable. - **Sealed-bid auctions.** Bidders publish `Poseidon(bid, salt)` during bidding, then prove their bid clears the reserve price without revealing it. ZeroJ's integration tests include sealed-bid example circuits: an annotation-style [`AnnotatedSealedBid`](https://github.com/bloxbean/zeroj/blob/main/zeroj-integration-tests/src/test/java/org/zeroj/examples/annotation/AnnotatedSealedBid.java) and a DSL version with a Yaci DevKit on-chain test. That's a starting point, not a full auction protocol. - **Anonymous feedback and whistleblowing.** An employee proves membership in a Merkle tree of staff keys and publishes one nullifier per topic. The report's hash is bound as a public input, so every report is from a real member and nobody can flood a topic. The shape is the [private voting](https://zeroj.dev/use-cases/private-voting/) circuit. - **Payroll or treasury solvency.** A DAO proves that a batch of private salaries or grants sums to no more than the treasury balance, and that each payment falls within an approved band. That uses `ZkUInt` sums and comparisons plus a Poseidon commitment to the batch, as in [proof of reserves](https://zeroj.dev/use-cases/proof-of-reserves/). ### Next steps - New to ZK? Start with [Zero-knowledge in plain English](https://zeroj.dev/learn/zero-knowledge-basics/). - Build the core patterns yourself: [Prove you're over 18](https://zeroj.dev/tutorials/age-check/) and [Private allowlist with a Merkle tree](https://zeroj.dev/tutorials/private-allowlist/). - Take a proof on-chain: [Verify your proof on Cardano](https://zeroj.dev/tutorials/verify-on-cardano/). --- ## Private voting Source: https://zeroj.dev/use-cases/private-voting/ > Let each eligible member vote exactly once without revealing which member cast which ballot, using Merkle membership, nullifiers and Groth16 on Cardano. DAOs, community treasuries and project councils increasingly vote on-chain, and on a public ledger every ballot is tied to an address. That invites vote buying ("show me your transaction and I'll pay you"), social pressure from peers and employers, and whales watching the count before they decide. What you want is a vote where eligibility and one-person-one-vote can be checked by anyone, but the link between a person and their ballot cannot. ### The zero-knowledge idea **The voter proves "I'm on the eligible-voter list, this is my one nullifier for this election, and this commitment records a valid yes/no vote", without revealing who they are.** Three values make this work: - **Voter list.** Each voter's public key is `Poseidon(secretKey, 0)`. The organizer puts all public keys into a Merkle tree and publishes only the root. - **Nullifier.** `Poseidon(secretKey, electionId)`. It's deterministic, so the same voter in the same election always produces the same value, and a second vote is caught. A different `electionId` gives an unrelated nullifier, so a voter's ballots can't be linked across elections. - **Commitment.** `Poseidon(vote, nullifier)`: the ballot record used for the tally. The circuit ties all three to one secret key. A voter who switches keys to get a fresh nullifier isn't in the tree, and a voter who keeps their key gets the same nullifier again. > **Note: Anonymity, not ballot secrecy** > > The demo hides **who** voted. It does not hide the **choice**: because the nullifier is public and > the vote is 0 or 1, anyone can try both values against the commitment. That's how the demo tallies. > Keeping choices secret until the count needs a salted commitment plus a reveal phase, or a > homomorphic or MACI-style tally. The design notes linked below compare these approaches. ### What stays private, what's public | Input | Visibility | Why | |---|---|---| | `secretKey` | Secret | The voter's identity; it derives both the public key and the nullifier | | Merkle path (`siblings`, `pathBits`) | Secret | The path would reveal the voter's position in the list | | `vote` | Secret (witness) | Constrained to 0 or 1; see the note above on decodability | | `electionId` | Public | Scopes the nullifier to this election | | `voterRoot` | Public | The published list of eligible voters | | `nullifier` | Public | Recorded on-chain to block a second vote | | `commitment` | Public | The ballot record the tally reads | ### How it works 1. **Register.** The organizer collects each voter's public key `Poseidon(secretKey, 0)`, builds the Merkle tree and publishes `voterRoot` and `electionId`. 2. **Prove.** The voter computes their Merkle path, nullifier and commitment and generates a Groth16 proof with ZeroJ's pure-Java prover. In a real deployment this runs on the voter's own device, so the secret key never leaves it. The demo proves server-side for convenience. 3. **Submit.** A transaction mints one token whose name is the nullifier. The minting policy verifies the proof on-chain, and the nullifier is inserted into an on-chain sorted list. 4. **Reject repeats.** A second vote from the same key has the same nullifier. The list already contains it, so the insert fails. 5. **Tally.** Anyone reads the commitments on-chain and counts them. ```text voter (off-chain) Cardano ───────────────── ─────── secretKey, vote, Merkle path ──prove──▶ tx: redeemer = proof + 4 public inputs ├─ minting policy: Groth16 check, mint 1 token named └─ list validator: insert between sorted neighbours ``` ### The circuit This is lightly simplified from the demo's `PrivateVoteProof`: constructor validation is trimmed and the statements are reordered for reading. ```java title="PrivateVoteProof.java" import org.zeroj.circuit.annotation.*; import org.zeroj.circuit.lib.poseidon.PoseidonParams; import org.zeroj.circuit.lib.poseidon.PoseidonParamsBLS12_381T3; import org.zeroj.circuit.lib.zk.ZkMerkle; import org.zeroj.circuit.lib.zk.ZkPoseidon; @ZKCircuit(name = "private-vote", nameTemplate = "private-vote-d{treeDepth}-bls-poseidon", version = 1) public class PrivateVoteProof { private static final PoseidonParams POSEIDON = PoseidonParamsBLS12_381T3.INSTANCE; public PrivateVoteProof(@CircuitParam("treeDepth") int treeDepth) {} @Prove ZkBool prove(ZkContext zk, @Public ZkField electionId, @Public ZkField voterRoot, @Public ZkField nullifier, @Public ZkField commitment, @Secret ZkBool vote, // ZkBool: constrained to 0 or 1 @Secret ZkField secretKey, @Secret @FixedSize(param = "treeDepth") ZkArray siblings, @Secret @FixedSize(param = "treeDepth") ZkArray pathBits) { var publicKey = ZkPoseidon.hash(zk, POSEIDON, secretKey, zk.constant(0)); ZkMerkle.verifyProofPoseidon(zk, POSEIDON, publicKey, voterRoot, siblings, pathBits); var computedNullifier = ZkPoseidon.hash(zk, POSEIDON, secretKey, electionId); var computedCommitment = ZkPoseidon.hash(zk, POSEIDON, vote.asField(), nullifier); return computedNullifier.isEqual(nullifier) .and(computedCommitment.isEqual(commitment)); } } ``` The annotation processor generates a `PrivateVoteProofCircuit` companion that you compile to R1CS and prove with Groth16. Before you trust a circuit like this, test it with invalid witnesses: a key that isn't in the tree, a nullifier from another election, a vote of 2. It should reject every one. See [Testing circuits](https://zeroj.dev/guides/circuits/testing-circuits/). ### On Cardano The demo uses two Plutus V3 scripts written in Java with JuLC: - **`VoteZkMintingPolicy`** composes `Groth16BLS12381Lib.verifyFour(...)` from `zeroj-onchain-julc`. The verification key is baked in as script parameters. The redeemer carries the three proof points plus the four public inputs. The policy also requires exactly one token to be minted and its name to equal the nullifier. - **`VoteListValidator`** keeps nullifiers in a **sorted linked list**, one UTxO per nullifier. Inserting between two neighbours proves the new nullifier wasn't there before. Votes that land in different parts of the list touch different UTxOs, so they don't contend. Each node locks a small min-UTxO deposit that can be reclaimed after the election. A valid proof is not authorization. The proof only says "some eligible voter produced this nullifier for *this* `electionId` and `voterRoot`". For simplicity the demo policy takes both values from the redeemer. A real deployment must pin them to the election's state, as script parameters or via a reference input, or a voter could prove membership in a tree they built themselves. See [On-chain verification](https://zeroj.dev/guides/verifying/on-chain/). ### Security considerations - **The voter list is a trust point.** Whoever builds the tree decides who can vote. Publish the leaves so members can check their inclusion and spot keys that shouldn't be there. - **Pin the election's public inputs** (`electionId`, `voterRoot`) on-chain, as described above. - **Watch transaction-level metadata.** In the demo a service wallet submits and pays for every vote. If voters pay fees from their own wallets, the fee input links them to their ballot. Use a relayer and be mindful of timing. - **No coercion resistance.** A voter can hand their secret key to a vote buyer, who recomputes the nullifier and checks the commitment. Anti-collusion designs such as MACI address this. This demo doesn't. - **Key theft is vote theft.** Anyone who learns a voter's secret key can vote in their place. - **Trusted setup.** The demo uses a single-party development setup. A real election needs keys from a multi-party ceremony ([Trusted setup, explained](https://zeroj.dev/learn/trusted-setup/)). ### Try it The demo lives in [`private-voting`](https://github.com/bloxbean/zeroj-usecases/tree/main/private-voting). With Yaci DevKit running (see [Run the demos](https://zeroj.dev/use-cases/overview/#run-the-demos)): ```bash ./demo.sh voting --run ``` At startup the app creates an election with five funded test voters. With `--run`, `voter1` votes yes and `voter2` votes no. Each vote is proven in pure Java and verified on-chain, and then the tally is printed. To see double-vote protection, vote again as `voter1` from the UI, or run: ```bash curl -X POST http://localhost:8086/api/vote \ -H "Content-Type: application/json" -d '{"voterLabel":"voter1","vote":0}' ``` The second vote is rejected because `voter1`'s nullifier is already in the on-chain list. Design notes: [Private voting — detailed design](https://github.com/bloxbean/zeroj/blob/main/docs/usecases/private-voting.md) (nullifier registries compared, batch rollups, Hydra, MACI). ### Related - [Private allowlist with a Merkle tree](https://zeroj.dev/tutorials/private-allowlist/): build membership and a nullifier yourself, step by step - [One claim per person](https://zeroj.dev/use-cases/sybil-resistant-airdrop/): the same nullifier idea with a personhood credential - [Application security](https://zeroj.dev/guides/verifying/application-security/): what the validator must check beyond the proof - [Gadgets](https://zeroj.dev/guides/circuits/gadgets/): `ZkMerkle`, `ZkPoseidon` and friends --- ## Proof of reserves Source: https://zeroj.dev/use-cases/proof-of-reserves/ > A custodian proves its reserves cover every customer balance without publishing any balance, and each customer can check they were counted. When an exchange or custodian fails, customers find out too late that the money wasn't there. The usual remedies are weak. An auditor's report asks you to trust the auditor. Publishing every account balance destroys customer privacy. A plain Merkle proof lets you see your neighbours' balances. Stablecoin issuers, bridges, lending protocols and DAO treasuries face the same question: *can you prove you're solvent without opening your books?* ### The zero-knowledge idea **The custodian proves "the balances committed in this published liabilities root are all non-negative, add up to exactly this total, and that total is covered by our reserves", without revealing any balance.** Each customer account becomes a leaf `Poseidon(accountId, balance)` in a Merkle tree, and the root is published. The circuit rebuilds the whole tree from the private balances, so the root commits to exactly the numbers being summed. The custodian can't sum one set of balances and publish another. Every balance is a 64-bit unsigned integer, and range checks rule out a negative "balance" that would quietly shrink the total. Customers close the loop. Each gets the Merkle path for their own leaf and checks that it hashes up to the published root. If the custodian left someone out or understated a balance, that customer can tell. > **Caution: Reserves are claimed, not proven** > > In this design `totalReserves` is a public input the custodian supplies. The circuit proves > liabilities ≤ that number. It does not prove the custodian controls the funds. On Cardano you can > check reserves held at known addresses directly; off-chain reserves need an attestation or oracle > you trust. ### What stays private, what's public | Input | Visibility | Why | |---|---|---| | `accountIds` | Secret | Customer identities stay private | | `balances` | Secret | Individual balances; each is range-checked to 64 bits | | `totalReserves` | Public | The custodian's claimed reserves, which must be checked independently | | `liabilitiesRoot` | Public | Commits to every leaf; customers check their inclusion against it | | `totalLiabilities` | Public | The exact sum of all balances | | `isSolvent` | Public | Proven equal to `totalReserves ≥ totalLiabilities`; the validator requires 1 | ### How it works 1. **Snapshot.** The custodian takes the customer balances at a point in time and builds the Poseidon Merkle tree. 2. **Prove.** It generates a Groth16 proof over the private balances with the four public inputs above. 3. **Attest on-chain.** It locks an attestation UTxO whose datum holds the public inputs. Spending it with the proof runs the verifier on-chain. An insolvent claim can't pass. 4. **Customers verify.** Each customer receives their Merkle path and checks their leaf against `liabilitiesRoot`, off-chain, whenever they like. 5. **Repeat.** A fresh proof every period keeps the attestation current. ```text custodian (private) public ─────────────────── ────── 16 × (accountId, balance) ─prove─▶ datum: [totalReserves, liabilitiesRoot, totalLiabilities, isSolvent=1] redeemer: Groth16 proof customer: my leaf + Merkle path ───▶ hashes up to liabilitiesRoot? ✓ ``` ### The circuit This is simplified from the demo's `SolvencyProof`: constructor validation is trimmed, and the demo requires `numLeaves == 1 << treeDepth`. ```java title="SolvencyProof.java" import org.zeroj.circuit.annotation.*; import org.zeroj.circuit.lib.poseidon.PoseidonParams; import org.zeroj.circuit.lib.poseidon.PoseidonParamsBLS12_381T3; import org.zeroj.circuit.lib.zk.ZkPoseidon; @ZKCircuit(name = "solvency-proof", nameTemplate = "solvency-proof-d{treeDepth}-n{numLeaves}-bls-poseidon", version = 1) public class SolvencyProof { private static final PoseidonParams POSEIDON = PoseidonParamsBLS12_381T3.INSTANCE; private final int treeDepth; private final int numLeaves; public SolvencyProof(@CircuitParam("treeDepth") int treeDepth, @CircuitParam("numLeaves") int numLeaves) { this.treeDepth = treeDepth; this.numLeaves = numLeaves; } @Prove ZkBool prove(ZkContext zk, @Public @UInt(bits = 64) ZkUInt totalReserves, @Public ZkField liabilitiesRoot, @Public @UInt(bits = 64) ZkUInt totalLiabilities, @Public ZkBool isSolvent, @Secret @FixedSize(param = "numLeaves") ZkArray accountIds, @Secret @UInt(bits = 64) @FixedSize(param = "numLeaves") ZkArray balances) { // Leaves and running sum over the private balances ZkUInt sum = balances.get(0); ZkField[] level = new ZkField[numLeaves]; for (int i = 0; i < numLeaves; i++) { level[i] = ZkPoseidon.hash(zk, POSEIDON, accountIds.get(i), balances.get(i).asField()); if (i > 0) sum = sum.add(balances.get(i)); } // Rebuild the Merkle root from the same leaves for (int d = 0; d < treeDepth; d++) { ZkField[] next = new ZkField[level.length / 2]; for (int i = 0; i < next.length; i++) { next[i] = ZkPoseidon.hash(zk, POSEIDON, level[2 * i], level[2 * i + 1]); } level = next; } return level[0].isEqual(liabilitiesRoot) .and(sum.asField().isEqual(totalLiabilities.asField())) .and(isSolvent.isEqual(totalReserves.gte(totalLiabilities))); } } ``` The circuit covers a fixed batch. The demo defaults to depth 4, which is 16 accounts. A real book of accounts needs a much larger circuit or a batching scheme, and proving cost grows with it. See [Performance](https://zeroj.dev/guides/proving/performance/). ### On Cardano The demo's `ReserveAttestationValidator` is a Plutus V3 spending validator written with JuLC. Its datum is the list `[totalReserves, liabilitiesRoot, totalLiabilities, isSolvent]`, its redeemer is the proof `(piA, piB, piC)`, and it calls `Groth16BLS12381Lib.verify(datum, …)` from `zeroj-onchain-julc` with the verification key baked in as script parameters. It also requires `isSolvent == 1`, so an honest proof of insolvency is rejected. A valid proof is not authorization. This demo validator checks the math and the solvency flag, nothing else. A real attestation also has to bind who is attesting (the custodian's signature), when (a period or slot as a public input, checked against the transaction's validity range, so an old proof can't be replayed) and where the reserves figure comes from. See [Application security](https://zeroj.dev/guides/verifying/application-security/). > **Caution: Experimental PlonK variant** > > An experimental PlonK variant of a reserve statement lives in > [`examples/minimal-circuits/plonk/proof-of-reserves`](https://github.com/bloxbean/zeroj-usecases/tree/main/examples/minimal-circuits/plonk/proof-of-reserves). > PlonK is experimental in ZeroJ and not a recommended path; use Groth16. ### Security considerations - **Reserves need their own evidence.** Check on-chain holdings directly, or rely on an attestation you trust. Watch for reserves borrowed just for the snapshot. Proving over several periods, or at unpredictable times, makes that harder. - **Omissions are only caught by customers.** The proof covers the accounts in the tree. If few customers check their inclusion, a missing account can go unnoticed. Make checking easy. - **Freshness and replay.** Put the snapshot period in the public inputs and enforce it on-chain. - **Salt the leaves.** An inclusion path contains neighbouring leaf hashes. If account IDs are guessable and balances fall in a small range, `Poseidon(accountId, balance)` can be brute-forced. A per-account random salt in the leaf prevents that. - **Trusted setup.** The demo uses a single-party development setup. Production keys come from a multi-party ceremony ([Trusted setup, explained](https://zeroj.dev/learn/trusted-setup/)). ### Try it The demo lives in [`proof-of-reserves`](https://github.com/bloxbean/zeroj-usecases/tree/main/proof-of-reserves). With Yaci DevKit running (see [Run the demos](https://zeroj.dev/use-cases/overview/#run-the-demos)): ```bash ./demo.sh proof-of-reserves --run ``` `--run` builds the liabilities tree over the demo accounts and proves solvency against 10,000 ADA of declared reserves. The proof is verified on-chain. In the UI you can add accounts, try a reserve figure below total liabilities to see the attestation rejected and verify a single account's inclusion. The first start generates a development setup cache, which can take a few minutes. Design notes: [Proof of reserves — detailed design](https://github.com/bloxbean/zeroj/blob/main/docs/usecases/proof-of-reserves.md) (Merkle sum trees, stake pool pledge, stablecoin and bridge variants). ### Related - [Prove you're over 18](https://zeroj.dev/tutorials/age-check/): the range-check pattern on a single value - [Annotations](https://zeroj.dev/guides/circuits/annotations/): `@UInt`, `@FixedSize` and `@CircuitParam` - [Verify your proof on Cardano](https://zeroj.dev/tutorials/verify-on-cardano/): lock and unlock with a proof on Yaci DevKit - [Performance](https://zeroj.dev/guides/proving/performance/): sizing larger circuits --- ## Age & KYC eligibility Source: https://zeroj.dev/use-cases/age-and-kyc/ > Prove you're old enough and live in an approved country, using an issuer-signed credential, while the validator learns only "eligible". Regulated DeFi, age-gated content and jurisdiction checks all ask the same thing: *are you allowed to use this?* Today the answer usually means uploading a passport to every app, which stores it and can leak it. Credentials fix half of that problem. A KYC provider checks you once and signs your attributes. Zero knowledge fixes the other half: you prove the signed attributes satisfy the rule, and the app never sees them. ### The zero-knowledge idea **The holder proves "a registered issuer signed my age and country, my age meets the minimum, and my country is on the approved list". The only public outputs are the issuer's key, the policy and the result.** The issuer signs `Poseidon(age, country)` with EdDSA over Jubjub. Jubjub is a curve defined over the BLS12-381 scalar field, so checking its signatures inside a BLS12-381 circuit is comparatively cheap. The circuit then does three things: 1. verifies the issuer's signature on the hidden attributes; 2. compares `age ≥ minAge` as 8-bit integers; 3. proves `country` is a leaf of the approved-country Merkle tree, whose root is public. The demo circuit is about 11,000 constraints. The on-chain check costs the same whatever the circuit contains, because it's a Groth16 pairing check over a handful of public inputs. ### What stays private, what's public | Input | Visibility | Why | |---|---|---| | `age` | Secret | 8-bit integer, range-checked; only the comparison result matters | | `country` | Secret | Numeric country code; only membership in the approved set matters | | Issuer signature (`sigRU`, `sigRV`, `sigS`) and helper witnesses (`kModL`, `kQuotient`) | Secret | Revealing the signature would make every presentation linkable | | Merkle path (`siblings`, `pathBits`) | Secret | The path would reveal which country | | `pkU`, `pkV` | Public | The issuer's Jubjub public key; the validator pins it | | `minAge` | Public | The policy threshold; pinned | | `countryRoot` | Public | The approved-country set; pinned | | `eligible` | Public | Proven equal to `age ≥ minAge`; the validator requires 1 | ### How it works 1. **Issue.** The KYC provider verifies the person out of band, signs `Poseidon(age, country)` and hands the holder their attributes plus the signature. The issuer keeps its secret key. 2. **Deploy the gate.** The protocol compiles a validator with the verification key, the issuer's public key, `minAge` and `countryRoot` as script parameters. Changing the policy produces a new script address. 3. **Prove.** The holder generates a Groth16 proof on their own device. The demo does this server-side for convenience. 4. **Spend.** The transaction's redeemer carries the proof plus the five public inputs. The validator checks the issuer, the policy, `eligible == 1` and the proof. 5. **Reuse.** No nullifier is involved. The same credential can prove eligibility again and again, which suits ongoing access. ```text issuer ──sign(Poseidon(age, country))──▶ holder ──Groth16 proof──▶ validator (off-chain, once) (local) params: VK, issuer key, minAge, countryRoot checks: registered issuer + policy + eligible==1 + pairing ``` ### The circuit This is lightly simplified from the demo's `CredentialProof`: constructor validation is trimmed and the steps are reordered for reading. ```java title="CredentialProof.java" import org.zeroj.circuit.annotation.*; import org.zeroj.circuit.lib.poseidon.PoseidonParams; import org.zeroj.circuit.lib.poseidon.PoseidonParamsBLS12_381T3; import org.zeroj.circuit.lib.zk.ZkEdDSAJubjub; import org.zeroj.circuit.lib.zk.ZkMerkle; import org.zeroj.circuit.lib.zk.ZkPoseidon; @ZKCircuit(name = "credential-verify-eddsa", nameTemplate = "credential-verify-eddsa-d{countryTreeDepth}-bls-poseidon", version = 1) public class CredentialProof { private static final PoseidonParams POSEIDON = PoseidonParamsBLS12_381T3.INSTANCE; public CredentialProof(@CircuitParam("countryTreeDepth") int countryTreeDepth) {} @Prove ZkBool prove(ZkContext zk, @Public ZkField pkU, @Public ZkField pkV, @Public @UInt(bits = 8) ZkUInt minAge, @Public ZkField countryRoot, @Public ZkBool eligible, @Secret @UInt(bits = 8) ZkUInt age, @Secret ZkField country, @Secret ZkField sigRU, @Secret ZkField sigRV, @Secret @UInt(bits = 252) ZkUInt sigS, @Secret @UInt(bits = 252) ZkUInt kModL, @Secret @UInt(bits = 4) ZkUInt kQuotient, @Secret @FixedSize(param = "countryTreeDepth") ZkArray siblings, @Secret @FixedSize(param = "countryTreeDepth") ZkArray pathBits) { // 1. The issuer signed these exact attributes var claims = ZkPoseidon.hash(zk, POSEIDON, age.asField(), country); ZkEdDSAJubjub.verifyWithRegisteredKey(zk, pkU, pkV, claims, sigRU, sigRV, sigS, kModL, kQuotient); // 2. The country is in the approved set ZkMerkle.verifyProofPoseidon(zk, POSEIDON, country, countryRoot, siblings, pathBits); // 3. The age meets the minimum return eligible.isEqual(age.gte(minAge)); } } ``` `verifyWithRegisteredKey` is the right entry point here because the issuer key is a public input that the verifier pins. If the prover could choose the key, you'd need `verifyStrict`, which also checks the key's subgroup membership inside the circuit. The name reminds you that "is this a trusted issuer?" is the validator's job. > **Tip: Sign a birth year, not an age** > > An age goes stale the day after it's signed. The > [selective-disclosure demo](https://zeroj.dev/use-cases/selective-disclosure/) signs `dobYear` and proves > `dobYear ≤ currentYear − 21`, with `currentYear` pinned by the validator. ### On Cardano The demo's `CredentialGatedValidator` is a JuLC spending validator. It composes `Groth16BLS12381Lib.verify(…)` and takes the verification key, the issuer's key coordinates and the policy values (`minAge`, `countryRoot`) as parameters. It accepts a spend only if the redeemer's public inputs match the registered issuer and policy, `eligible == 1`, and the pairing check passes. A valid proof is not authorization. This gate is stateless, so the proof isn't tied to the person spending. Anyone who sees an unlock transaction can copy its proof. In the demo each locked UTxO can be spent only once, but a real protocol should bind the proof to its context: add the recipient or the spent `TxOutRef` as a bound public input that the validator recomputes from `ScriptContext`, or use a per-epoch nullifier when access should be rate-limited. See [Application security](https://zeroj.dev/guides/verifying/application-security/). ### Security considerations - **You trust the issuer.** A dishonest or compromised issuer can sign anything. Plan for key rotation and revocation: short-lived credentials, or a periodically refreshed validity tree the circuit checks. - **Keep issuance offline.** The demo signs with `EdDSAJubjub.signCompatibilityOffline`. ZeroJ's Jubjub signing isn't constant-time and is meant for offline, isolated use. Don't expose it as a network signing service. - **Credentials can be shared.** A proof shows knowledge of a credential, not identity. Holder binding helps: include the holder's key in the signed message and prove control of it. - **Replay** of stateless proofs, as described above. - **Linkability outside the proof.** The same wallet, fee payer or timing can still link two presentations. - **Trusted setup.** The demo uses a single-party development setup; production needs an MPC ceremony ([Trusted setup, explained](https://zeroj.dev/learn/trusted-setup/)). ### Try it The demo lives in [`identity-kyc`](https://github.com/bloxbean/zeroj-usecases/tree/main/identity-kyc). With Yaci DevKit running (see [Run the demos](https://zeroj.dev/use-cases/overview/#run-the-demos)): ```bash ./demo.sh identity-kyc --run ``` `--run` locks 5 ADA at the credential-gated script, proves that Alice (25, USA) is eligible and unlocks the funds with that proof on-chain. The UI lists five test users. Charlie is 16 and Diana's country isn't on the approved list, so neither can produce a proof that unlocks the gate. Design notes: [ZK identity & credentials — detailed design](https://github.com/bloxbean/zeroj/blob/main/docs/usecases/identity-and-credentials.md) (credential lifecycle, revocation options, issuer registries). ### Related - [Prove you're over 18](https://zeroj.dev/tutorials/age-check/): build the range check yourself - [Selective disclosure & reusable KYC](https://zeroj.dev/use-cases/selective-disclosure/): many predicates from one credential, or BBS - [Gadgets](https://zeroj.dev/guides/circuits/gadgets/): `ZkEdDSAJubjub`, `ZkMerkle`, `ZkPoseidon` - [Testing circuits](https://zeroj.dev/guides/circuits/testing-circuits/): write the invalid-witness tests --- ## Private NFT ownership Source: https://zeroj.dev/use-cases/nft-ownership/ > Prove you hold an NFT from a collection for one-time token-gated access without connecting your wallet, plus the nullifier trade-off that decides privacy. On Cardano, owning an NFT means a UTxO holding it sits at your address. To prove ownership, you usually connect your wallet or point at that UTxO, and the verifier (plus anyone watching) learns your address, your balance, every other token you hold and your transaction history. That's a steep price for a concert ticket, a holders-only chat or a DAO forum. Zero knowledge lets you prove "I'm a holder" and nothing more. ### The zero-knowledge idea **The holder proves "I'm in the collection's current ownership snapshot, holding a token from it, and this is the one-time nullifier for that token in this context", without revealing their wallet.** Live UTxOs change with every transaction, so the proof is made against a **snapshot**. An indexer records who holds which token and builds a Merkle tree. The demo's leaf is `Poseidon(ownerHash, tokenName)`, where `ownerHash = Poseidon(secretKey, 0)` is derived from a secret only the holder knows. The root is published, and the holder proves their leaf is in the tree without saying which leaf. The **nullifier** is `Poseidon(tokenName, contextId)`, where `contextId` names the event or campaign. The same NFT in the same context always gives the same nullifier, so each NFT gets in once. If the NFT changes hands, the new holder can't use it a second time in that context either. That choice has a privacy cost, explained in the caution after the circuit. ### What stays private, what's public | Input | Visibility | Why | |---|---|---| | `secretKey` | Secret | The holder's secret; `ownerHash` is derived from it | | `tokenName` | Secret | Which NFT (see the caveat below) | | Merkle path (`siblings`, `pathBits`) | Secret | The path would reveal the leaf's position | | `snapshotRoot` | Public | The ownership snapshot the proof is checked against | | `contextId` | Public | The event or campaign that scopes the nullifier | | `isOwner` | Public | Must be 1 | | `nullifier` | Public | Minted as a token name and stored on-chain so it can't be reused | ### How it works 1. **Snapshot.** The indexer scans holders of the collection's policy, builds the Merkle tree of `(ownerHash, tokenName)` leaves and publishes the root. (In the demo, holders register with the service; see the security notes on what a real snapshot must check.) 2. **Prove.** The holder generates a Groth16 proof with ZeroJ's pure-Java prover. 3. **Access.** A transaction mints one token named after the nullifier. The minting policy verifies the proof on-chain, and the nullifier is inserted into an on-chain sorted list. 4. **Reuse is rejected.** The same NFT in the same context produces the same nullifier, and the list already contains it. ```text indexer: holders of policy P ─▶ Merkle tree ─▶ snapshotRoot (published) holder: secretKey, tokenName, path ─prove─▶ tx: mint 1 × , insert into sorted list ``` ### The circuit This is the demo's `NFTOwnershipProof`, with constructor validation trimmed. ```java title="NFTOwnershipProof.java" import org.zeroj.circuit.annotation.*; import org.zeroj.circuit.lib.poseidon.PoseidonParams; import org.zeroj.circuit.lib.poseidon.PoseidonParamsBLS12_381T3; import org.zeroj.circuit.lib.zk.ZkMerkle; import org.zeroj.circuit.lib.zk.ZkPoseidon; @ZKCircuit(name = "nft-ownership", nameTemplate = "nft-ownership-d{treeDepth}-bls-poseidon", version = 1) public class NFTOwnershipProof { private static final PoseidonParams POSEIDON = PoseidonParamsBLS12_381T3.INSTANCE; public NFTOwnershipProof(@CircuitParam("treeDepth") int treeDepth) {} @Prove ZkBool prove(ZkContext zk, @Public ZkField snapshotRoot, @Public ZkField contextId, @Public ZkBool isOwner, @Public ZkField nullifier, @Secret ZkField secretKey, @Secret ZkField tokenName, @Secret @FixedSize(param = "treeDepth") ZkArray siblings, @Secret @FixedSize(param = "treeDepth") ZkArray pathBits) { var ownerHash = ZkPoseidon.hash(zk, POSEIDON, secretKey, zk.constant(0)); var leaf = ZkPoseidon.hash(zk, POSEIDON, ownerHash, tokenName); ZkMerkle.verifyProofPoseidon(zk, POSEIDON, leaf, snapshotRoot, siblings, pathBits); var computedNullifier = ZkPoseidon.hash(zk, POSEIDON, tokenName, contextId); return isOwner.and(computedNullifier.isEqual(nullifier)); } } ``` The demo builds a depth-10 tree, which holds up to 1,024 holders. At that depth the circuit compiles to a few thousand constraints. Each extra level doubles the tree's capacity (and the potential anonymity set) for one more Poseidon hash. > **Caution: This nullifier can reveal the NFT, and the wallet** > > Token names in a collection are public and usually few. Anyone can compute > `Poseidon(tokenName, contextId)` for every token and match the published nullifier. That reveals > *which* NFT was used. Because an NFT's holder address is public on Cardano, it usually reveals > *which wallet* too. For wallet-level privacy, derive the nullifier from a holder secret instead, > such as `Poseidon(secretKey, contextId)`. The token then stays hidden, but the one-use limit > applies per holder secret rather than per NFT, so an NFT that changes hands could be used again. > Choose deliberately. ### On Cardano The demo pairs two JuLC scripts: - **`ZkProofMintingPolicy`** composes `Groth16BLS12381Lib.verifyFour(...)` with the verification key baked in as parameters. The redeemer carries the proof plus `snapshotRoot`, `contextId`, `isOwner` and `nullifier`. The policy requires `isOwner == 1` and exactly one minted token whose name equals the nullifier. - **`NullifierListValidator`** stores nullifiers as a sorted linked list, one UTxO per entry. An insert proves the new value isn't already present, and unrelated accesses touch different nodes. A valid proof is not authorization. The demo policy takes `snapshotRoot` and `contextId` from the redeemer. A real gate must pin them to the current snapshot and event, for example by reading a snapshot UTxO as a reference input. Otherwise a prover could build a tree of their own. See [On-chain verification](https://zeroj.dev/guides/verifying/on-chain/). ### Security considerations - **The snapshot is a trust point.** The indexer decides which leaves exist. In production it must confirm that each registered `ownerHash` really belongs to the wallet holding the token, for example with a wallet signature at registration. Publish the snapshot data so anyone can rebuild the root. - **Staleness.** A holder who sells after the snapshot can still prove against it until the next one. Shorter snapshot intervals narrow the window. - **Pin the public inputs** (`snapshotRoot`, `contextId`) on-chain, as above. - **Nullifier design leaks information** (see the caveat). Also consider fee payer and timing linkability when holders submit their own transactions. - **Trusted setup.** The demo uses a single-party development setup; production needs an MPC ceremony ([Trusted setup, explained](https://zeroj.dev/learn/trusted-setup/)). ### Try it The demo lives in [`nft-ownership`](https://github.com/bloxbean/zeroj-usecases/tree/main/nft-ownership). With Yaci DevKit running (see [Run the demos](https://zeroj.dev/use-cases/overview/#run-the-demos)): ```bash ./demo.sh nft-ownership --run ``` `--run` registers a holder, builds the snapshot, generates a proof and submits the access transaction. The proof is verified on-chain and the nullifier lands in the sorted list. Submit the same nullifier again from the UI and the service refuses it, because the nullifier already exists on-chain. The UI also lets you mint test NFTs on the devnet. Design notes: [Private NFT ownership — detailed design](https://github.com/bloxbean/zeroj/blob/main/docs/usecases/private-nft-ownership.md) (snapshot vs. on-chain registries, threshold "whale" proofs, shielded transfers). ### Related - [Private allowlist with a Merkle tree](https://zeroj.dev/tutorials/private-allowlist/): the same membership and nullifier pattern, step by step - [Private voting](https://zeroj.dev/use-cases/private-voting/): the same sorted-list nullifier registry - [Authenticated state](https://zeroj.dev/guides/credentials/authenticated-state/): larger, updatable registries (experimental) - [Application security](https://zeroj.dev/guides/verifying/application-security/) --- ## One claim per person Source: https://zeroj.dev/use-cases/sybil-resistant-airdrop/ > A Sybil-resistant airdrop where each personhood credential claims once per epoch, anonymously, with the payout bound to a chosen recipient. Airdrops, faucets and community rewards get farmed. One person spins up a thousand wallets and claims a thousand times. Checking identity at claim time stops that, but then every claimant hands over personal data and every claim is linked to a person. What you want is **one claim per real human per period**, where the distributor learns only that *some* eligible person claimed. ### The zero-knowledge idea **The claimant proves "a personhood issuer signed my credential, and this nullifier is the one my credential produces for this epoch", and binds the payout to a recipient, without revealing the credential.** A personhood issuer checks, out of band, that each human gets exactly one credential. That's the part zero knowledge can't do for you. The issuer signs `Poseidon(personhoodId, 0)` with EdDSA over Jubjub. From then on: - **Nullifier = `Poseidon(personhoodId, epoch)`.** It's the same for every claim in an epoch, so repeats are caught. A new epoch gives an unrelated value, so claims can't be linked across epochs. - **Recipient binding.** The payout address is a public input the proof commits to. Someone who copies a proof from the mempool can't redirect the payout without invalidating it. This is the same building block behind Semaphore-style signals and "one-per-human" claim tokens. ### What stays private, what's public | Input | Visibility | Why | |---|---|---| | `personhoodId` | Secret | The credential's unique ID; revealing it would link every claim | | Issuer signature (`sigRU`, `sigRV`, `sigS`) and helpers (`kModL`, `kQuotient`) | Secret | Revealing the signature would also link claims | | `pkU`, `pkV` | Public | The issuer's Jubjub key; pinned by the minting policy | | `epoch` | Public | The claim period; pinned by the policy | | `nullifier` | Public | One per credential per epoch; becomes the minted receipt's asset name | | `recipient` | Public | The payout destination, committed to by the proof | | `eligible` | Public | Must be 1 | ### How it works 1. **Enrol.** The issuer verifies the person is unique, generates `personhoodId`, signs it and delivers the credential privately. 2. **Claim.** The holder proves with the current `epoch` and the recipient they choose. 3. **Mint a receipt.** The faucet minting policy verifies the proof and mints one "claim NFT" whose asset name is the nullifier. The claim pays out ADA. 4. **Repeat in the same epoch?** Same credential and same epoch give the same nullifier, so the claim is refused. 5. **Next epoch.** A fresh nullifier space opens and everyone can claim again. ```text issuer ──sig(Poseidon(personhoodId, 0))──▶ holder holder ──proof(epoch, recipient)─────────▶ faucet policy: issuer ✓ epoch ✓ eligible ✓ pairing ✓ mint 1 × receipt, pay recipient ``` ### The circuit This is the demo's `PersonhoodAirdropProof`, unchanged apart from imports: ```java title="PersonhoodAirdropProof.java" import org.zeroj.circuit.annotation.*; import org.zeroj.circuit.lib.poseidon.PoseidonParams; import org.zeroj.circuit.lib.poseidon.PoseidonParamsBLS12_381T3; import org.zeroj.circuit.lib.zk.ZkEdDSAJubjub; import org.zeroj.circuit.lib.zk.ZkPoseidon; @ZKCircuit(name = "personhood-airdrop", version = 1) public class PersonhoodAirdropProof { private static final PoseidonParams POSEIDON = PoseidonParamsBLS12_381T3.INSTANCE; @Prove ZkBool prove(ZkContext zk, @Public ZkField pkU, @Public ZkField pkV, @Public ZkField epoch, @Public ZkField nullifier, @Public ZkField recipient, @Public ZkBool eligible, @Secret ZkField personhoodId, @Secret ZkField sigRU, @Secret ZkField sigRV, @Secret @UInt(bits = 252) ZkUInt sigS, @Secret @UInt(bits = 252) ZkUInt kModL, @Secret @UInt(bits = 4) ZkUInt kQuotient) { var claimsMsg = ZkPoseidon.hash(zk, POSEIDON, personhoodId, zk.constant(0)); ZkEdDSAJubjub.verifyWithRegisteredKey( zk, pkU, pkV, claimsMsg, sigRU, sigRV, sigS, kModL, kQuotient); // Bind the public recipient into a real (non-degenerate) R1CS row. recipient.mul(personhoodId); var computedNullifier = ZkPoseidon.hash(zk, POSEIDON, personhoodId, epoch); return eligible.and(computedNullifier.isEqual(nullifier)); } } ``` The line `recipient.mul(personhoodId)` deserves a closer look. The circuit doesn't *compute* anything with `recipient`, but a Groth16 proof only commits to a public input that appears in at least one constraint. Multiplying two variables emits a real constraint row. A constant multiplication such as `recipient * 1` gets folded away and wouldn't bind anything. ZeroJ's native Groth16 setup refuses a relation with an unbound public input rather than silently producing a key that ignores it. ### On Cardano The demo's `FaucetMintingPolicy` is a JuLC minting policy parameterized by the verification key, the issuer's key and the epoch. It reads the six public inputs from the claim output's inline datum, checks the issuer and epoch against its parameters, requires `eligible == 1`, requires exactly one minted token whose asset name equals the nullifier, and verifies the proof with `Groth16BLS12381Lib.verify(...)`. A valid proof is not authorization, and this demo shows two gaps you'd close before real use: - **Double claims are blocked off-chain.** The service keeps an in-memory set of used nullifiers. Cardano lets a policy mint another unit under the same asset name, so the receipt NFT alone doesn't prevent a second claim. A production faucet needs on-chain uniqueness: a sorted-list registry like the [voting](https://zeroj.dev/use-cases/private-voting/) and [NFT](https://zeroj.dev/use-cases/nft-ownership/) demos, or a state-thread token carrying the used-nullifier set. - **The payout isn't checked against `recipient`.** The proof commits to the recipient, but the demo policy doesn't verify that an output actually pays it. The [account-recovery validator](https://zeroj.dev/use-cases/account-recovery/#on-cardano) shows how to enforce this. ### Security considerations - **Uniqueness lives at the issuer.** ZK enforces "one claim per credential". It can't tell whether a person holds two credentials. Sybil resistance is only as good as the issuer's enrolment checks. - **Stolen credentials claim once per epoch.** If a `personhoodId` and signature leak, the thief can claim. Plan for revocation. - **On-chain nullifier state and recipient enforcement,** as above. - **Epoch source.** The demo reads the epoch from configuration and pins it into the policy, so a new epoch means a new policy ID. Tie it to chain time for a long-running faucet. - **Keep issuance offline.** The demo signs with `EdDSAJubjub.signCompatibilityOffline`, which is meant for offline, isolated use, not a network signing service. - **Trusted setup.** The demo uses a single-party development setup; production needs an MPC ceremony ([Trusted setup, explained](https://zeroj.dev/learn/trusted-setup/)). ### Try it The demo lives in [`personhood-airdrop`](https://github.com/bloxbean/zeroj-usecases/tree/main/personhood-airdrop). Its [tutorial](https://github.com/bloxbean/zeroj-usecases/blob/main/personhood-airdrop/SYBIL_AIRDROP_TUTORIAL.md) walks through the design. With Yaci DevKit running (see [Run the demos](https://zeroj.dev/use-cases/overview/#run-the-demos)): ```bash ./demo.sh airdrop --run ``` `--run` has Alice claim for the current epoch (proof generated, receipt minted on-chain, ADA paid out) and prints the faucet status. Claim for Alice again from the UI and the service refuses it because the nullifier is the same. Bob's claim succeeds because his credential produces a different nullifier. ### Related - [Private voting](https://zeroj.dev/use-cases/private-voting/): nullifiers stored in an on-chain sorted list - [Age & KYC eligibility](https://zeroj.dev/use-cases/age-and-kyc/): the same in-circuit issuer signature, without rate limiting - [Circuits, constraints & witnesses](https://zeroj.dev/learn/circuits-and-witnesses/): why unconstrained inputs are dangerous - [Application security](https://zeroj.dev/guides/verifying/application-security/) --- ## Selective disclosure & reusable KYC Source: https://zeroj.dev/use-cases/selective-disclosure/ > Do KYC once, then show each service only what it asks for, as Groth16 predicates over hidden fields or BBS presentations verified on Cardano. Every exchange, lending app and marketplace runs its own KYC, and each one keeps a copy of your documents. A signed credential lets you do KYC once. But if you then show the whole credential everywhere, you've only moved the problem. Selective disclosure is the missing piece. A library asks "over 21 and a resident?", a healthcare portal asks "a doctor over 30?", a DeFi app asks "KYC-verified and from an allowed country?", and each one learns *only* its answer. ZeroJ supports two complementary ways to do this, and each has a runnable demo. ### The zero-knowledge idea **One issuer-signed credential; each verifier learns only the answer to its own question.** | | Groth16 predicates (`selective-disclosure` demo) | BBS selective disclosure (`reusable-kyc` demo) | |---|---|---| | Verifier learns | A computed yes/no, such as `dobYear ≤ currentYear − 21` | Chosen attribute values, such as `country = USA` | | Computes over hidden values? | Yes: ranges, Merkle membership, any circuit | No: each attribute is either revealed or hidden | | Signature | EdDSA-Jubjub over a Poseidon hash of all fields, checked in-circuit | BBS (IRTF CFRG draft-10) over all attributes | | Circuit and trusted setup | One circuit and one setup per predicate | None | | On-chain | `Groth16BLS12381Lib` | `BbsProofVerify` (fixed profile, see below) | Use predicates when the verifier needs a fact computed from a value it must not see. Use BBS when revealing the value itself is fine and you want no circuits and no trusted setup. The two combine well: reveal some attributes with BBS, and prove a range over another with a small Groth16 circuit. ### What stays private, what's public **Groth16 "senior doctor" predicate:** | Input | Visibility | Why | |---|---|---| | `dobYear`, `country`, `roleId`, `salaryBracket`, `nameHash` | Secret | The whole credential stays hidden | | Issuer signature and helper witnesses | Secret | Revealing them would link presentations | | `pkU`, `pkV` | Public | The issuer's key; pinned by the validator | | `currentYear` | Public | Pinned by the validator, so the prover can't pick a convenient year | | `eligible` | Public | The predicate's result; the validator requires 1 | **BBS reusable KYC:** | Input | Visibility | Why | |---|---|---| | `givenName`, `dob`, `docHash` | Hidden | Proven signed but never shown | | `country`, `kycLevel` | Revealed | Exactly what this verifier's policy needs | | Issuer public key, credential `header` | Public | Identify the issuer and schema | | Presentation header (challenge) | Public | Makes the presentation single-use | ### How it works **Groth16 predicates:** 1. The issuer signs `Poseidon(dobYear, country, roleId, salaryBracket, nameHash)` once. 2. For each verifier, the holder runs that verifier's predicate circuit. Every circuit recomputes the same claims hash, checks the same signature, then asserts its own predicate. 3. Each gate is a spending validator pinned to its own verification key, issuer key and policy values. **BBS:** 1. The issuer signs five attributes as one BBS credential. 2. The verifier sends a fresh random challenge. 3. The holder derives a presentation that reveals only the requested attributes, bound to that challenge. 4. The verifier checks the proof, that the challenge is one it issued and hasn't seen before, and its policy. On-chain, a validator can do the same natively. ### The circuit This Groth16 predicate is from the demo's `SeniorDoctorProof`. The `AdultResidentProof` next to it reuses the same first half and adds a Merkle country check. ```java title="SeniorDoctorProof.java" import org.zeroj.circuit.annotation.*; import org.zeroj.circuit.lib.poseidon.PoseidonParams; import org.zeroj.circuit.lib.poseidon.PoseidonParamsBLS12_381T3; import org.zeroj.circuit.lib.zk.ZkEdDSAJubjub; import org.zeroj.circuit.lib.zk.ZkPoseidonN; @ZKCircuit(name = "senior-doctor", version = 1) public class SeniorDoctorProof { private static final PoseidonParams POSEIDON = PoseidonParamsBLS12_381T3.INSTANCE; private static final int MIN_AGE = 30; private static final long DOCTOR_ROLE_ID = 1001L; @Prove ZkBool prove(ZkContext zk, @Public ZkField pkU, @Public ZkField pkV, @Public @UInt(bits = 16) ZkUInt currentYear, @Public ZkBool eligible, @Secret @UInt(bits = 16) ZkUInt dobYear, @Secret @UInt(bits = 16) ZkUInt country, @Secret ZkField roleId, @Secret @UInt(bits = 8) ZkUInt salaryBracket, @Secret ZkField nameHash, @Secret ZkField sigRU, @Secret ZkField sigRV, @Secret @UInt(bits = 252) ZkUInt sigS, @Secret @UInt(bits = 252) ZkUInt kModL, @Secret @UInt(bits = 4) ZkUInt kQuotient) { // Every predicate recomputes the same signed message over all five fields var claims = ZkPoseidonN.hash(zk, POSEIDON, dobYear.asField(), country.asField(), roleId, salaryBracket.asField(), nameHash); ZkEdDSAJubjub.verifyWithRegisteredKey(zk, pkU, pkV, claims, sigRU, sigRV, sigS, kModL, kQuotient); // The predicate: role == doctor AND born no later than currentYear - 30 var maxDobYear = ZkUInt.wrap(zk, currentYear.asField().sub(zk.constant(MIN_AGE)).signal(), 16); var roleOk = roleId.isEqual(zk.constant(DOCTOR_ROLE_ID)); return roleOk.and(eligible.isEqual(dobYear.lte(maxDobYear))); } } ``` #### The BBS flow With BBS there's no circuit at all. This is the `zeroj-bbs` API as the reusable-KYC demo uses it, condensed: ```java import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.List; import org.zeroj.bbs.BbsKeyPair; import org.zeroj.bbs.BbsPresentation; import org.zeroj.bbs.BbsRevealedMessage; import org.zeroj.bbs.BbsService; import org.zeroj.bbs.BbsSignature; var bbs = BbsService.pureJava(); // Issuer: sign all five attributes as one credential (index = position in the list) BbsKeyPair issuer = bbs.keyPair(keyMaterial, keyInfo); // keyMaterial: >= 32 secret random bytes List attributes = List.of(givenName, dob, country, kycLevel, docHash); BbsSignature signature = bbs.sign(issuer.secretKey(), issuer.publicKey(), attributes, header); // Verifier: a fresh, single-use challenge. The verifier picks it, never the holder. byte[] challenge = new byte[32]; new SecureRandom().nextBytes(challenge); // Holder: reveal country (2) and kycLevel (3); indexes must be strictly ascending BbsPresentation presentation = bbs.derivePresentation( issuer.publicKey(), signature, attributes, header, challenge, new int[] {2, 3}); // Verifier: check the proof (and that `challenge` is unused), then apply its policy boolean valid = bbs.verifyPresentation(issuer.publicKey(), presentation); for (BbsRevealedMessage m : presentation.revealedMessages()) { System.out.println(m.index() + " -> " + new String(m.message(), StandardCharsets.UTF_8)); } ``` `verifyPresentation` checks cryptography only. Issuer trust, schema, expiry, revocation and your disclosure policy remain application logic. See the [BBS guide](https://zeroj.dev/guides/credentials/bbs/). ### On Cardano **Groth16 gates.** The demo's `AdultResidentValidator` and `SeniorDoctorValidator` are JuLC spending validators built on `Groth16BLS12381Lib`. The issuer key, `currentYear` and (for the adult gate) the approved-country root are script parameters. Changing any of them deploys a new script, so a caller can't substitute weaker public inputs. **BBS natively on-chain.** `BbsProofVerify` in `zeroj-onchain-julc` runs BBS `ProofVerify` inside Plutus V3. `BbsToCardano` in `zeroj-bbs` turns the issuer key into validator parameters and a presentation into redeemer fields. The demo's `BbsKycClaimValidator` locks a voucher UTxO and releases it only if four things hold: - the presentation verifies on the ledger; - it discloses exactly the required values; - it pays the voucher's recipient; - its presentation header equals `blake2b_256(voucherTxId ‖ I2OSP(index, 8) ‖ recipientPkh)`, which the validator recomputes itself rather than trusting the claimer. Spending the voucher is the nullifier. The demo measured about 2.4×10⁹ CPU units for the check, within Cardano's per-transaction limit. The current `BbsProofVerify` profile is fixed to a five-attribute credential disclosing indexes 2 and 3; other shapes need a different unrolling. In both cases a valid proof is not authorization. The payout, recipient and replay checks above carry the authorization; the proof alone doesn't. See [Application security](https://zeroj.dev/guides/verifying/application-security/). ### Security considerations - **Replay.** A BBS presentation stays valid forever. It's single-use only if the relying party chooses the challenge (off-chain) or derives it from the UTxO being spent (on-chain). The Groth16 demo proofs carry no nonce, so add a session or UTxO binding when the proof isn't consumed together with a UTxO. - **Revealed values can identify you.** A rare combination of disclosed attributes may be enough to re-identify someone. Reveal the minimum. - **Credential theft and sharing.** Whoever holds the credential can present it. Bind it to the holder's key when that matters. - **Issuer keys.** For BBS, the default pure-Java provider isn't constant-time. Prefer the blst provider (`BbsService.withBlsProvider(...)`) for issuer keys. The Jubjub demo issuer (`signCompatibilityOffline`) is for offline, isolated use only. - **Maturity.** BBS follows an IRTF draft, not yet an RFC. The Groth16 demos use a single-party development setup ([Trusted setup, explained](https://zeroj.dev/learn/trusted-setup/)). ### Try it Both demos are in [zeroj-usecases](https://github.com/bloxbean/zeroj-usecases). With Yaci DevKit running (see [Run the demos](https://zeroj.dev/use-cases/overview/#run-the-demos)): ```bash # Groth16 predicates from one signed credential ./demo.sh selective-disclosure --run # BBS: reveal a subset of attributes, verified natively on-chain ./demo.sh reusable-kyc --run ``` - [`selective-disclosure`](https://github.com/bloxbean/zeroj-usecases/tree/main/selective-disclosure): `--run` locks ADA at the "adult resident" and "senior doctor" gates, then has Bob prove each predicate and unlock both on-chain. In the UI, Alice passes only the adult gate and Charlie passes neither. - [`reusable-kyc`](https://github.com/bloxbean/zeroj-usecases/tree/main/reusable-kyc): `--run` issues a credential, gets a verifier challenge, presents only `country` and `kycLevel`, then claims a voucher on-chain. The ledger verifies the BBS proof. The UI lets you pick which attributes to reveal. ### Related - [BBS credentials guide](https://zeroj.dev/guides/credentials/bbs/) - [Groth16, PlonK & BBS](https://zeroj.dev/learn/proof-systems/): when to reach for which - [Age & KYC eligibility](https://zeroj.dev/use-cases/age-and-kyc/): a single predicate from a minimal credential - [Application security](https://zeroj.dev/guides/verifying/application-security/) --- ## Digital product passport Source: https://zeroj.dev/use-cases/digital-product-passport/ > Prove a product meets carbon, recycled-content and origin thresholds without exposing the supplier data behind them, anchored on Cardano. The EU's Ecodesign for Sustainable Products Regulation (ESPR) introduces Digital Product Passports: verifiable records of a regulated product's materials, carbon footprint, recycled content and origin. Regulators, recyclers and consumers need to trust those claims. But the exact numbers behind them are commercially sensitive. A precise carbon figure reveals manufacturing efficiency, origin data reveals suppliers, and inspection records reveal defect rates. The tension is real: *prove it, but don't publish it.* ### The zero-knowledge idea **The manufacturer proves "an auditor committed to this product's measurement, and it meets the threshold" (for example carbon ≤ 50 kg CO₂e, or recycled content ≥ 30 %) without revealing the measurement.** The pattern is the same for every claim: - An **auditor** measures the product and commits to `(productId, measurement)`. - The **manufacturer** proves, in zero knowledge, that the committed measurement passes the public threshold. - The **proof is tied to one product**, because `productId` is inside the committed hash. - **Anyone** can check the proof. Nobody learns the number. The demo applies this to carbon (≤), recycled content (≥), manufacturing origin (Merkle membership in an approved-country set) and an ordered chain of inspections. It covers two scenarios: EV batteries with a passport NFT per product, and textiles with one per batch. ### What stays private, what's public | Input | Visibility | Why | |---|---|---| | `measurement` | Secret | The exact carbon figure or recycled percentage: the competitive data | | `auditorSecret` | Secret | Opens the auditor's commitment | | `productId` | Public | Ties the proof to one product or batch | | `threshold` | Public | The regulatory or labelling limit | | `auditorHash` | Public | The auditor's commitment to `(productId, measurement)` | | `isCompliant` | Public | Proven equal to the threshold comparison; the validator requires 1 | ### How it works 1. **Register.** Products and batches go into a registry. The demo keeps it in a Merkle Patricia Forestry trie hashed with Poseidon, so its root stays circuit-friendly. 2. **Audit.** The auditor measures and publishes a commitment per product and claim. 3. **Prove.** The manufacturer generates one Groth16 proof per claim. 4. **Mint the passport.** A minting policy checks the manufacturer's signature and the compliance proof, then mints exactly one passport token. The public inputs sit in the output's inline datum. 5. **Check.** Anyone can read the passport and its public inputs on-chain. A non-compliant product can't mint one. ```text auditor ──commit(productId, measurement)──▶ auditorHash (public) manufacturer ──prove(measurement ≤ threshold)──▶ mint DPP token datum: [productId, threshold, auditorHash, isCompliant=1] ``` ### The circuit The DPP demo deliberately uses the lower-level `CircuitSpec` style instead of annotations. This sketch is simplified from its `ComplianceThresholdCircuit`, keeping only the "less than or equal" variant used for carbon: ```java title="CarbonThresholdCircuit.java" import org.zeroj.circuit.CircuitBuilder; import org.zeroj.circuit.CircuitSpec; import org.zeroj.circuit.Signal; import org.zeroj.circuit.SignalBuilder; import org.zeroj.circuit.lib.SignalComparators; import org.zeroj.circuit.lib.SignalPoseidon; import org.zeroj.circuit.lib.poseidon.PoseidonParams; import org.zeroj.circuit.lib.poseidon.PoseidonParamsBLS12_381T3; public class CarbonThresholdCircuit implements CircuitSpec { private static final PoseidonParams POSEIDON = PoseidonParamsBLS12_381T3.INSTANCE; @Override public void define(SignalBuilder c) { Signal measurement = c.privateInput("measurement"); // secret: kg CO2e Signal auditorSecret = c.privateInput("auditorSecret"); Signal productId = c.publicInput("productId"); Signal threshold = c.publicInput("threshold"); Signal auditorHash = c.publicInput("auditorHash"); Signal isCompliant = c.publicOutput("isCompliant"); // 1. This is the measurement the auditor committed to, for this product Signal claims = SignalPoseidon.hash(c, POSEIDON, productId, measurement); c.assertEqual(SignalPoseidon.hash(c, POSEIDON, auditorSecret, claims), auditorHash); // 2. measurement <= threshold (16-bit; the comparator range-checks both sides) c.assertEqual(isCompliant, SignalComparators.lessOrEqual(c, measurement, threshold, 16)); } public static CircuitBuilder build() { return CircuitBuilder.create("compliance-lte") .publicVar("productId").publicVar("threshold") .publicVar("auditorHash").publicVar("isCompliant") .secretVar("measurement").secretVar("auditorSecret") .defineSignals(new CarbonThresholdCircuit()); } } ``` Each threshold circuit is small, a few hundred constraints. The rest of the flow is the same as any other circuit. Compile to R1CS, compute the witness and prove with Groth16. See the [Circuit DSL guide](https://zeroj.dev/guides/circuits/circuit-dsl/). > **Caution: A commitment is not a signature** > > In the demo, one service plays both auditor and manufacturer, and the "auditor commitment" is a > Poseidon hash keyed by a secret the prover also knows. Anyone who knows that secret can commit to > any number. A real system needs the auditor to **sign** the measurement, with the signature > verified in-circuit (as the [KYC demos](https://zeroj.dev/use-cases/age-and-kyc/) do with EdDSA-Jubjub). Or it needs > `auditorHash` published by a registered auditor key that the validator checks. ### On Cardano The demo uses two JuLC scripts built on `Groth16BLS12381Lib.verify(...)`: - **`DppMintingPolicy`** mints a passport token only if the manufacturer signed the transaction, exactly one token is minted, the compliance proof over the first output's datum `[productId, threshold, auditorHash, isCompliant]` verifies, and `isCompliant == 1`. - **`DppComplianceValidator`** is a compliance-gated spending validator: lock ADA, then unlock it with a proof of compliance. A valid proof is not authorization. The manufacturer-signature check is a real `ScriptContext` binding, but a production passport also has to tie `auditorHash` to an accredited auditor, pin thresholds to the regulation that applies (not to a value the manufacturer picks) and bind `productId` to the token being minted. The DPP state machine and an auditor registry held as a reference input are covered in the design notes below. ### Security considerations - **Auditor trust.** ZK proves the audited value passes the threshold, not that the audit was honest. Use independent auditors, spot checks and accountability, the same trust model as today's certification. - **Bind every claim to the product and an attestation.** An origin proof that only shows "some country code is in the approved set" proves nothing about *this* product. Each claim needs the product ID and an attested value inside its constraints, and every public input must appear in a constraint. - **Thresholds and policy** belong to the verifier: pin them as script parameters or read them from a registry. - **Linkability of batches.** Per-batch passports reveal batch sizes and timing. Decide what you're willing to publish. - **Trusted setup.** The demo uses a single-party development setup; production needs an MPC ceremony ([Trusted setup, explained](https://zeroj.dev/learn/trusted-setup/)). ### Try it The demo lives in [`digital-product-passport`](https://github.com/bloxbean/zeroj-usecases/tree/main/digital-product-passport). With Yaci DevKit running (see [Run the demos](https://zeroj.dev/use-cases/overview/#run-the-demos)): ```bash ./demo.sh dpp --run ``` `--run` generates the compliance proofs for battery `BAT-SN001`, mints its passport token on-chain and prints the registry status. In the UI, try `BAT-SN003` (65 kg carbon, 20 % recycled) or textile batch `TEX-B2024-003`, which is made outside the approved set. Both come back "NOT COMPLIANT". Design notes: [Digital product passport — detailed design](https://github.com/bloxbean/zeroj/blob/main/docs/usecases/digital-product-passport.md) (multi-party supply chains, lifecycle state machine, comparison with the Cardano Foundation blueprint). ### Related - [Circuit DSL](https://zeroj.dev/guides/circuits/circuit-dsl/): the `CircuitSpec` style used here - [Authenticated state](https://zeroj.dev/guides/credentials/authenticated-state/): Poseidon MPF/JMT registries (experimental) - [Testing circuits](https://zeroj.dev/guides/circuits/testing-circuits/): prove your constraints reject bad witnesses - [Application security](https://zeroj.dev/guides/verifying/application-security/) --- ## Prove you own a Cardano account Source: https://zeroj.dev/use-cases/account-recovery/ > Prove you know the wallet root key behind a Cardano address, without revealing the seed or signing anything, so a refund can reach the real owner. Picture a wallet hack in which attackers obtain users' address-level signing keys and drain the funds. An operator such as an exchange or wallet provider wants to refund the victims. Now it has a hard problem. The attacker can show up and claim too, and a signature from the leaked keys proves nothing. The genuine owner still has their 24-word recovery phrase, but handing that to anyone would be a disaster. And a refund submitted on-chain can be copied from the mempool and redirected. This page explains why zero knowledge is the tool that resolves this, and what ZeroJ's largest circuit proves. ### Why a signature isn't enough Cardano wallets derive keys along the CIP-1852 path `m / 1852' / 1815' / account' / role / index`. The first three steps are **hardened**, the last two are **soft**, and the two kinds behave very differently: - **Soft derivation can be run backwards.** A child private key is the parent key plus an offset anyone can compute from the parent's extended public key (xpub). One leaked address key plus the account xpub, which some wallets keep on their servers, gives up the **account** private key, and with it every address in the account. - **So everything from the address up to the account is potentially compromised.** A signature under any of those keys can be produced by the attacker too. - **Hardened derivation is a one-way wall.** It feeds the parent *private* key into the derivation, so the attack can't climb from the account to the **root key**. Only the real owner has that. The one claim an attacker can't fake is therefore *"I know the root key this address descends from"*. You can't reveal the root key. And a verifier can't check the root-to-address link from public data, because the hardened steps need the private key. That combination is exactly what a zero-knowledge proof handles. ### The zero-knowledge idea **The owner proves "I know a wallet root key that derives, along the real CIP-1852 path, to this address's payment key hash, and I authorise a payout to this recipient". Only the key hash and the recipient are revealed.** The circuit takes the root extended private key as a secret witness and replays the whole derivation inside the proof. That means three hardened and two soft BIP32-Ed25519 steps (HMAC-SHA512 plus Ed25519 arithmetic emulated in the BLS12-381 field), the leaf public key and the Blake2b-224 key hash. It then checks that the result equals the public payment key hash. ### What stays private, what's public | Input | Visibility | Why | |---|---|---| | `rootKL`, `rootKR`, `rootChainCode` | Secret | The root extended private key: the wallet's master secret | | `account`, `role`, `index` | Secret | The derivation path stays private; `pkh` already pins the address | | `recipientBytes` | Secret witness | Constrained to pack exactly to the public `recipient` | | `pkh` | Public | The address's payment key hash, already public on-chain | | `recipient` | Public | The payout's payment key hash, bound so a copied proof can't be redirected | ### How it works 1. **Vouchers.** The operator locks a refund voucher UTxO for each affected account. Its datum holds `pkh` and `refundAmount`. 2. **Prove.** The owner runs the desktop app or CLI, enters the recovery phrase at a hidden prompt and chooses a recipient address. The seed is used in memory only. It's never written to disk or sent anywhere. 3. **Verify.** Anyone can check the proof off-chain in under a second. On-chain, a claim transaction spends the voucher. 4. **One claim.** A UTxO can only be spent once, so each voucher pays out once. Anyone, such as a fee sponsor, may submit the claim, and the funds still go to the bound recipient. ### The circuit This is the demo's `OwnershipProof`, unchanged apart from imports and comments: ```java title="OwnershipProof.java" import org.zeroj.circuit.annotation.*; import org.zeroj.circuit.lib.zk.ZkCip1852; @ZKCircuit(name = "account-ownership-proof", version = 4) public class OwnershipProof { @Prove void prove(ZkContext zk, @Secret @FixedSize(32) ZkBytes rootKL, @Secret @FixedSize(32) ZkBytes rootKR, @Secret @FixedSize(32) ZkBytes rootChainCode, @Secret @FixedSize(4) ZkBytes account, @Secret @FixedSize(4) ZkBytes role, @Secret @FixedSize(4) ZkBytes index, @Secret @FixedSize(28) ZkBytes recipientBytes, @Public ZkField pkh, @Public ZkField recipient) { // Root key + full CIP-1852 path -> 28-byte payment key hash, pinned to the public pkh ZkBytes derived = ZkCip1852.paymentKeyHash(zk, rootKL, rootKR, rootChainCode, account, role, index); pack(zk, derived).assertEqual(pkh); // Bind the payout: the proof commits to the recipient's packed key hash pack(zk, recipientBytes).assertEqual(recipient); } // Big-endian packing of a 28-byte hash into one field element (Horner's rule) private static ZkField pack(ZkContext zk, ZkBytes bytes) { ZkField acc = bytes.get(0).asField(); for (int i = 1; i < bytes.size(); i++) { acc = acc.mul(zk.constant(256L)).add(bytes.get(i).asField()); } return acc; } } ``` The file is short, but the circuit behind it is large. `ZkCip1852.paymentKeyHash` composes ZeroJ's in-circuit Blake2b, SHA-512, HMAC-SHA512, Ed25519 and BIP32 gadgets. The composed derivation is validated byte-for-byte against Cardano Client Lib's HD-wallet derivation. Packing each 28-byte hash into one field element keeps the proof at **two** public inputs, which is what makes the on-chain check affordable. #### Size and cost (documented measurements) | Metric | Measured | |---|---| | Circuit size | About 19 million constraints (19,075,097 in the measured runs) | | Local dev setup | About 6 min on a 12-core, 128 GB machine; about 9.6 GB key bundle on disk | | Prove | About 1.5 min on that machine; about 2.6 min on an ordinary 16 GB machine | | Verify off-chain | Under 1 s | | Verify on-chain | About 2.8×10⁹ CPU steps, under the 10×10⁹ per-transaction limit | These figures come from the demo's CLI documentation and ZeroJ's prover-memory work, and they depend on hardware. Proving a circuit this size in commodity memory relies on the memory-mapped proving key and the streaming setup; see [Performance](https://zeroj.dev/guides/proving/performance/). ### On Cardano The demo's `OwnershipProofValidator` is a JuLC spending validator parameterized with the verification key. On a claim it checks all of the following: 1. **The proof verifies** over `[pkh, recipient]`, where `pkh` comes from the voucher's datum. 2. **The recipient is actually paid.** Some transaction output's payment credential equals `recipient`, and its value is at least `refundAmount`. Checking only that such an output exists would let a front-runner pay a token amount and skim the rest. 3. **It's the right voucher.** The datum's `pkh` equals the proof's `pkh`. This is a good example of "a valid proof is not authorization". The pairing check alone would let anyone with a copied proof spend the voucher to any address. The recipient and amount checks bind the proof to the transaction. See [Application security](https://zeroj.dev/guides/verifying/application-security/). ### Security considerations - **The whole argument assumes the root seed didn't leak.** If the attacker has the recovery phrase, owner and attacker are cryptographically identical, and no scheme can tell them apart. - **Trusted setup is critical here.** Whoever knows the setup randomness can forge ownership proofs. A locally generated key bundle is single-party and for testing only. A refund program needs a multi-party phase-2 ceremony (run externally with snarkjs and imported); see the [ceremony guide](https://zeroj.dev/guides/proving/trusted-setup-ceremony/). - **Confirm the recipient.** A production refund program should also confirm the payout address through its own authenticated channel. - **Protect the seed on the proving machine.** The tools read it at a hidden prompt and keep it in memory only, but the machine itself must be trusted. - **Review status.** The derivation gadgets are validated against an independent implementation but haven't been externally audited. > **Danger: Experimental** > > This is research-grade software. Don't use it to move real value or to run a live refund program > without an MPC ceremony and external review. ### Try it The demo lives in [`account-ownership`](https://github.com/bloxbean/zeroj-usecases/tree/main/account-ownership). It doesn't use `demo.sh`. Instead it ships a desktop app and a CLI (installers and a Java zip on the repo's [releases page](https://github.com/bloxbean/zeroj-usecases/releases)). From source: ```bash cd account-ownership ./gradlew :ui:run ``` The CLI flow is `setup` (a local, development-only key bundle, which needs `--i-understand-insecure`), then `prove --recipient `, then `verify`, or `verify --onchain` against a local Yaci DevKit. Allow about 10 GB of free disk and use a machine with at least 16 GB of RAM. Test with the public BIP-39 test mnemonic or a throwaway wallet, never a real recovery phrase. ### Related - [Performance](https://zeroj.dev/guides/proving/performance/): how multi-million-constraint circuits fit in commodity memory - [Gadgets](https://zeroj.dev/guides/circuits/gadgets/): Blake2b, SHA-512, HMAC, Ed25519, BIP32 and CIP-1852 in-circuit - [Trusted setup ceremony](https://zeroj.dev/guides/proving/trusted-setup-ceremony/): producing production keys - [ZK on Cardano](https://zeroj.dev/learn/zk-on-cardano/) --- ## Private token transfers Source: https://zeroj.dev/use-cases/private-payments/ > A design walkthrough of a shielded pool on Cardano. Deposit, then withdraw to a fresh address that nobody can link to the deposit. Design only, no demo. Every Cardano transaction shows who paid, who got paid, how much and when. That's fine for a lot of things, but not for payroll, supplier payments, donations to sensitive causes or a treasury that doesn't want to broadcast its strategy. Using fresh addresses doesn't help much, because change outputs and timing let chain analysis stitch them back together. A **shielded pool** breaks the link: many people deposit the same amount, and each later withdraws to a new address without revealing which deposit was theirs. > **Note: Design-level page: there is no runnable demo** > > Unlike the other use cases, private transfers aren't implemented in > [zeroj-usecases](https://github.com/bloxbean/zeroj-usecases). This page describes a design built > from ZeroJ's building blocks. The circuit below is a design sketch, not a tested application. > Privacy pools also carry legal and compliance questions that are outside the scope of these docs. ### The zero-knowledge idea **The withdrawer proves "I know the secret behind *one of* the deposits in this pool, and this is its one-time spend tag", without revealing which deposit.** - **Deposit.** Pick two random values, `secret` and `nullifier`. Compute the note commitment `commitment = Poseidon(secret, nullifier)` and deposit a fixed amount (say 100 ADA) along with it. The commitment joins the pool's Merkle tree. - **Withdraw.** Later, possibly from a brand-new address, prove the commitment is somewhere in the tree and reveal `nullifierHash = Poseidon(nullifier, 0)`. The pool records the hash so the same note can't be withdrawn twice. Nobody learns which leaf was spent. Your anonymity set is every deposit of the same denomination. ### What stays private, what's public | Input | Visibility | Why | |---|---|---| | `secret` | Secret | Proves you own the note; only the depositor (or whoever they share it with) knows it | | `nullifier` | Secret | Only its hash is revealed, so the spend tag can't be matched to the deposit | | Merkle path (`siblings`, `pathBits`) | Secret | The path would reveal which deposit is being spent | | `merkleRoot` | Public | A recent root of the pool's commitment tree | | `nullifierHash` | Public | Recorded on-chain to block a second withdrawal of the same note | | `recipient` | Public | Where the funds go, bound into the proof so it can't be redirected | The design notes also add a public `relayerFee`, bound the same way as `recipient`. ### How it works 1. **Deposit.** Alice sends 100 ADA and her commitment to the pool. Her address is visible, but her secret and nullifier stay with her. 2. **Grow the tree.** The pool absorbs deposits into its commitment tree. On Cardano a single shared root UTxO would serialize every deposit. The design notes therefore recommend collecting deposits as individual UTxOs and folding them into the tree in periodic batches. 3. **Wait.** Time and more deposits grow the anonymity set. Withdrawing right after depositing gives most of it away. 4. **Withdraw through a relayer.** A fresh address has no ADA to pay fees, so a relayer submits the transaction and takes a fee that the proof also binds. 5. **The pool validator** verifies the proof, checks the root is one it has published, checks that `nullifierHash` is new (for example in a sorted linked list, as in the [voting demo](https://zeroj.dev/use-cases/private-voting/)), records it and pays the recipient. ```text Alice ──100 ADA + commitment──▶ pool (public: Alice → pool) ... other deposits, time passes ... relayer ──proof + nullifierHash──▶ pool ──100 ADA──▶ fresh address (public: pool → ???) ``` ### The circuit This is a design sketch in the annotation style, adapted from the withdrawal circuit in the design notes with BLS12-381 Poseidon and an explicit recipient binding. It isn't taken from a runnable demo. ```java title="PoolWithdrawal.java" import org.zeroj.circuit.annotation.*; import org.zeroj.circuit.lib.poseidon.PoseidonParams; import org.zeroj.circuit.lib.poseidon.PoseidonParamsBLS12_381T3; import org.zeroj.circuit.lib.zk.ZkMerkle; import org.zeroj.circuit.lib.zk.ZkPoseidon; @ZKCircuit(name = "pool-withdrawal", nameTemplate = "pool-withdrawal-d{treeDepth}", version = 1) public class PoolWithdrawal { private static final PoseidonParams POSEIDON = PoseidonParamsBLS12_381T3.INSTANCE; public PoolWithdrawal(@CircuitParam("treeDepth") int treeDepth) {} @Prove ZkBool prove(ZkContext zk, @Public ZkField merkleRoot, @Public ZkField nullifierHash, @Public ZkField recipient, @Secret ZkField secret, @Secret ZkField nullifier, @Secret @FixedSize(param = "treeDepth") ZkArray siblings, @Secret @FixedSize(param = "treeDepth") ZkArray pathBits) { // 1. My note is one of the pool's deposits var commitment = ZkPoseidon.hash(zk, POSEIDON, secret, nullifier); ZkMerkle.verifyProofPoseidon(zk, POSEIDON, commitment, merkleRoot, siblings, pathBits); // 2. Bind the recipient: a variable x variable product emits a real constraint row recipient.mul(nullifier); // 3. The public spend tag belongs to this note return ZkPoseidon.hash(zk, POSEIDON, nullifier, zk.constant(0)).isEqual(nullifierHash); } } ``` Step 2 is the part people forget. A public input that appears in no constraint isn't committed to by a Groth16 proof, so anyone could swap the recipient. Multiplying it by a secret variable creates a real constraint. The [airdrop demo](https://zeroj.dev/use-cases/sybil-resistant-airdrop/#the-circuit) uses the same technique, and ZeroJ's native Groth16 setup refuses a relation that leaves a public input unbound. Before building on a sketch like this, write invalid-witness tests: a note that isn't in the tree, a wrong `nullifierHash`, and a proof checked against a changed recipient. All of them must fail. See [Testing circuits](https://zeroj.dev/guides/circuits/testing-circuits/). ### On Cardano Verification would use `Groth16BLS12381Lib` from `zeroj-onchain-julc` inside a custom pool validator, as every demo on this site does. The proof is the easy part. The validator carries most of the security: - **Known roots only.** Accept `merkleRoot` only if it's the current root or a recent one the pool published. Otherwise a withdrawer can prove membership in a tree they made up. - **Nullifier registry.** Store each `nullifierHash` so it can't be reused. A sorted linked list gives trustless, concurrent inserts. - **Pay exactly as bound.** Require an output to `recipient` for the denomination minus `relayerFee`, and the fee to the relayer, and nothing else leaving the pool. - **Value accounting.** The pool must never release more than it holds for a denomination. A valid proof is not authorization. Every one of these checks lives in the validator, not the circuit. See [Application security](https://zeroj.dev/guides/verifying/application-security/). ### Security considerations - **Anonymity set size.** With 5 deposits you're 1 in 5. Privacy grows only as the pool is used. - **Timing and amount fingerprinting.** Withdrawing soon after depositing, or using unusual amounts, links you. Fixed denominations help; variable amounts need a more complex circuit. - **Relayer trust.** A relayer can refuse to submit (censor) but can't redirect funds if the recipient and fee are bound. Use more than one. - **Note secrets.** Whoever holds `secret` and `nullifier` can withdraw. Losing them loses the funds. - **Compliance.** "Privacy pools" designs let users prove their deposit belongs to an approved association set. The design notes sketch this. Get legal advice before running anything like it. - **Trusted setup and review.** A pool holding value would need a multi-party ceremony ([Trusted setup, explained](https://zeroj.dev/learn/trusted-setup/)) and an external audit, neither of which exists for this design. ### Try it There's no demo to run. To experiment with the building blocks: - Work through [Private allowlist with a Merkle tree](https://zeroj.dev/tutorials/private-allowlist/). It's the same membership-plus-nullifier core, and it's runnable. - Run the [voting demo](https://zeroj.dev/use-cases/private-voting/#try-it) to see a sorted-list nullifier registry on Yaci DevKit. - Read the design notes: [Private token transfer — detailed design](https://github.com/bloxbean/zeroj/blob/main/docs/usecases/private-token-transfer.md) (UTxO patterns for commitments, batch deposits, relayers, multi-asset pools). Its code samples are older sketches. Prefer the BLS12-381 Poseidon parameters and explicit recipient binding shown above. ### Related - [Private voting](https://zeroj.dev/use-cases/private-voting/): nullifiers and a sorted-list registry, running end to end - [Gadgets](https://zeroj.dev/guides/circuits/gadgets/): `ZkMerkle`, `ZkPoseidon` - [On-chain verification](https://zeroj.dev/guides/verifying/on-chain/) - [ZK on Cardano](https://zeroj.dev/learn/zk-on-cardano/) --- ## Modules Source: https://zeroj.dev/reference/modules/ > Every published ZeroJ artifact with its coordinates, purpose, BOM membership, maturity and main entry-point classes, plus the unpublished support projects. ZeroJ is split into small modules so you only pull in what you use. All published artifacts share the Maven group **`org.zeroj`**, and every Java package starts with `org.zeroj`. (Releases up to `0.1.0-pre11` used `com.bloxbean.cardano`; see [Migration notes](https://zeroj.dev/reference/migration/).) There are three kinds of published module: - **Core** modules are version-aligned by the BOM `org.zeroj:zeroj-bom-core`. Import the BOM once and omit versions. - **Opt-in** modules are published but deliberately left out of the BOM. Declare them with an explicit version. "Opt-in" means they're not in the default dependency graph and have their own maturity; it doesn't mean untested. - **Support, assurance and benchmark** projects are never published. Maturity labels come from the support matrix on [Status & maturity](https://zeroj.dev/start/status/). "Beta" means feature-complete and correctness-tested but **not externally audited** and not for value-bearing or mainnet use. ### Using the BOM ```groovy title="build.gradle" dependencies { implementation platform('org.zeroj:zeroj-bom-core:0.1.0-pre12') implementation 'org.zeroj:zeroj-circuit-dsl' // core: version from the BOM implementation 'org.zeroj:zeroj-crypto' implementation 'org.zeroj:zeroj-verifier-groth16' implementation 'org.zeroj:zeroj-bbs:0.1.0-pre12' // opt-in: explicit version } ``` ```xml title="pom.xml" org.zeroj zeroj-bom-core 0.1.0-pre12 pom import ``` ### Core modules (in `zeroj-bom-core`) | Artifact | Purpose | Maturity | Start with | |---|---|---|---| | `zeroj-api` | Proof model and shared policy types. No dependencies. | Beta | `ZkProofEnvelope`, `PublicInputs`, `VerificationKeyRef`, `VerificationMaterial`, `VerificationResult`, `CircuitId`, `CurveId`, `ProofSystemId`, `TrustedSetupPolicy`, `LegacyCurvePolicy` | | `zeroj-codec` | snarkjs JSON parsing, CBOR envelopes, canonical hashing | Beta | `SnarkjsJsonCodec`, `CborEnvelopeCodec`, `CanonicalHash`, `EnvelopeValidator` | | `zeroj-backend-spi` | Verifier SPI, key registry, routing | Beta | `ZkVerifier`, `BackendDescriptor`, `VerificationKeyRegistry`, `InMemoryVerificationKeyRegistry`, `VerifierRegistry`, `VerifierOrchestrator` | | `zeroj-verifier-groth16` | Groth16 BLS12-381 verification | Beta | `Groth16BLS12381PureJavaVerifier` (pure Java), `Groth16BLS12381Verifier` (blst-backed) | | `zeroj-bls12381` | Pure-Java BLS12-381 fields, curves and pairing | Beta (verification-grade) | `BLS12381Pairing`, `G1Point`, `G2Point`, `Bls12381Provider` | | `zeroj-blst` | Native BLS12-381 via [blst](https://github.com/supranational/blst): FFM MSM binding with a source-built, bundled `libblst`, plus pairing | Beta, opt-in native code | `BlstBls12381Provider`, `BlstPairing`, `ffm.BlstFfm` | | `zeroj-crypto` | The pure-Java prover: Groth16 setup, proving, key stores, ceremony import, snarkjs export | Beta (Groth16); PlonK classes are experimental | `Groth16Keys`, `Groth16Pipeline`, `Groth16ProverBLS381`, `Groth16SetupBLS381`, `PowersOfTauBLS381`, `ZkeyImporterBLS381`, `ZkeyPkStoreImporter`, `SnarkjsGroth16Json` | | `zeroj-crypto-blst` | Opt-in blst MSM backend for the Groth16 prover; bit-identical proofs | Beta, opt-in | `BlstProverBackend` | | `zeroj-circuit-dsl` | Java circuit DSL; compiles to R1CS | Beta | `CircuitBuilder`, `CircuitSpec`, `SignalBuilder`, `Signal` | | `zeroj-circuit-lib` | Gadget library: Poseidon, Merkle, comparators, binary, mux, Blake2b/SHA-512/HMAC, Ed25519/BIP32/CIP-1852… | Beta, per-gadget status | `PoseidonParamsBLS12_381T3`, see [Gadgets](https://zeroj.dev/guides/circuits/gadgets/) | | `zeroj-circuit-annotation-api` | `@ZKCircuit` symbolic annotations and `Zk*` types | Beta | `@ZKCircuit`, `@Prove`, `@Public`, `@Secret`, `ZkField`, `ZkBool`, `ZkContext`, `ZkInputMap` | | `zeroj-circuit-annotation-processor` | Annotation processor that generates a `Circuit` companion class | Beta | Add as `annotationProcessor` | | `zeroj-onchain-julc` | Plutus V3 validators and codecs, compiled with JuLC | Groth16: **Beta, testnet only**. PlonK validators: **Experimental**. BBS on-chain libraries: working for one fixed disclosure profile, not separately rated in the support matrix | `groth16.validator.Groth16BLS12381Verifier`, `Groth16BLS12381TxOutRefBindingVerifier` (reference example only), `Groth16BLS12381Lib`, `ProverToCardano`, `SnarkjsToCardano`, `ScriptBudgetEstimator` | | `zeroj-tools` | Ceremony tooling and the `zeroj-ceremony` CLI (snarkjs-compatible phase-2 contributions) | Not separately rated; the ceremony design record is still *Proposed* | `ZkeyContributor`, `SnarkjsHashToG2`, `CeremonyCli` | | `zeroj-bom-core` | The BOM for all of the above | — | `platform('org.zeroj:zeroj-bom-core:…')` | > **Note: Two `Groth16BLS12381Verifier` classes** > > `org.zeroj.verifier.groth16.bls12381.Groth16BLS12381Verifier` (off-chain, blst-backed) and > `org.zeroj.onchain.julc.groth16.validator.Groth16BLS12381Verifier` (on-chain validator) share a > simple name. Import by package. ### Opt-in modules (explicit version) | Artifact | Purpose | Maturity | Start with | |---|---|---|---| | `zeroj-verifier-plonk` | PlonK BLS12-381 verification (structured snarkjs/ZeroJ JSON) | **Experimental** | `PlonkBLS12381Verifier` | | `zeroj-bbs` | CFRG BBS draft-10 signatures and selective disclosure | Verification: Beta. Issuance: Beta with caveat (use the blst provider for issuer keys) | `BbsService`, `BbsPresentationCodec`, `BbsToCardano`, `BbsZkVerifier` | | `zeroj-mpf-poseidon` | Poseidon-rooted MPF adapter over Cardano Client Lib plus operation-specific circuits | **Experimental** | `PoseidonMpfTrie`, `PoseidonMpfCircuitTemplates`, `PoseidonMpfBranchWitness` | | `zeroj-jmt-poseidon` | Poseidon-rooted JMT profile over Cardano Client Lib plus operation-specific circuits | **Experimental** | `PoseidonJmtTree`, `PoseidonJmtCircuitTemplates`, `PoseidonJmtInclusionWitness` | > **Caution: PlonK is experimental** > > Everything PlonK in ZeroJ (the prover classes in `zeroj-crypto`, `zeroj-verifier-plonk`, and the > on-chain PlonK validators) is experimental and opt-in. Groth16 on BLS12-381 is the focus of the > current release. Legacy BN254 classes still exist in some modules for old off-chain experiments. They are disabled unless you set `-Dzeroj.allowLegacyBn254=true`, are not registered with `ServiceLoader`, and BN254 is not a Cardano curve. ### What depends on what You rarely need to list every module. Typical sets: | You want to… | Declare | |---|---| | Write and prove circuits | `zeroj-circuit-dsl` (+ `zeroj-circuit-lib`), `zeroj-crypto`; for annotations also `zeroj-circuit-annotation-api` and `annotationProcessor 'org.zeroj:zeroj-circuit-annotation-processor'` | | Verify proofs in a service | `zeroj-verifier-groth16`, `zeroj-codec` | | Verify on Cardano | `zeroj-onchain-julc` (+ JuLC's `julc-cardano-client-lib` to load scripts) | | Faster proving on big machines | add `zeroj-crypto-blst` and benchmark first | | Credentials | `zeroj-bbs` (explicit version), optionally `zeroj-blst` | ### Never published These live in the repository for testing, assurance and benchmarking. You can't depend on them. | Project | What it is | How to build | |---|---|---| | `zeroj-test-vectors` | Shared fixtures: pre-generated proofs and VKs | Default build | | `zeroj-integration-tests` | Cross-module regressions: Groth16 end-to-end, tampering, invalid witness, snarkjs interop, JuLC VM and Yaci DevKit | `./gradlew :zeroj-integration-tests:test`, `:e2eTest` | | `assurance/zeroj-bls12381-wasm` | zkcrypto BLS12-381 compiled to WASM, an independent differential oracle (needs Rust) | `-PincludeAssurance` | | `assurance/zeroj-bbs-wasm` | zkryptium BBS compiled to WASM, an independent differential oracle (needs Rust) | `-PincludeAssurance` | | `assurance/gnark-fixtures` | Pinned gnark fixture generator for the committed PlonK vectors (needs Go) | `make` (not a Gradle project) | | `benchmarks/zeroj-mpf-poseidon-load` | RocksDB load, proof and Cardano-artifact benchmark tool for MPF | `-PincludeBenchmarks` | | `benchmarks/zeroj-jmt-poseidon-load` | Durable, versioned JMT load and benchmark tool | `-PincludeBenchmarks` | See [Configuration](https://zeroj.dev/reference/configuration/#gradle-flags-for-contributors) for the build flags. ### Next steps - [Installation](https://zeroj.dev/start/installation/) - [API cheat sheet](https://zeroj.dev/reference/api-cheatsheet/) - [Status & maturity](https://zeroj.dev/start/status/) --- ## API cheat sheet Source: https://zeroj.dev/reference/api-cheatsheet/ > One page of the ZeroJ calls you use most, grouped by task, each with the class's package, from circuit definition to on-chain verification and BBS. The calls you reach for most, grouped by task. Every class lives under `org.zeroj` unless the package says otherwise. For explanations, follow the links in each section. ### Define a circuit See [Annotations](https://zeroj.dev/guides/circuits/annotations/) and [Circuit DSL](https://zeroj.dev/guides/circuits/circuit-dsl/). | Call | Class (package) | |---|---| | `@ZKCircuit(name = "secret-multiplier", version = 1)` on a class | `ZKCircuit` (`org.zeroj.circuit.annotation`) | | `@Prove ZkBool prove(ZkContext zk, @Public ZkField a, @Public ZkField product, @Secret ZkField b)` | `Prove`, `Public`, `Secret`, `ZkBool`, `ZkField`, `ZkContext` (`org.zeroj.circuit.annotation`) | | `a.mul(b).isEqual(product)` · `a.add(b)` | `ZkField` (`org.zeroj.circuit.annotation`) | | `SecretMultiplierCircuit.build()` (generated companion) | returns `CircuitBuilder` (`org.zeroj.circuit`) | | `CircuitBuilder.create("mul").publicVar("c").secretVar("a").secretVar("b").define(api -> api.assertEqual(api.mul(api.var("a"), api.var("b")), api.var("c")))` | `CircuitBuilder` (`org.zeroj.circuit`) | | `CircuitBuilder.create(name).publicVar(...).secretVar(...).defineSignals(new MySpec())`, with `MySpec implements CircuitSpec { void define(SignalBuilder c) }` | `CircuitSpec`, `SignalBuilder`, `Signal` (`org.zeroj.circuit`) | | `ZkPoseidon.hash(zk, PoseidonParamsBLS12_381T3.INSTANCE, x, y)` | `ZkPoseidon` (`org.zeroj.circuit.lib.zk`), `PoseidonParamsBLS12_381T3` (`org.zeroj.circuit.lib.poseidon`) | | `SignalPoseidon.hash(c, PoseidonParamsBLS12_381T3.INSTANCE, x, y)` | `SignalPoseidon` (`org.zeroj.circuit.lib`) | | `PoseidonHash.hash(PoseidonParamsBLS12_381T3.INSTANCE, a, b)` (off-circuit, same hash) | `PoseidonHash` (`org.zeroj.circuit.lib.poseidon`) | For Cardano circuits, always pass the BLS12-381 Poseidon parameters explicitly. The overloads without parameters and MiMC are oriented to legacy BN254 use. ### Compile and compute the witness | Call | Class (package) | |---|---| | `var r1cs = circuit.compileR1CS(CurveId.BLS12_381)` | `CircuitBuilder` (`org.zeroj.circuit`), `CurveId` (`org.zeroj.api`) | | `r1cs.constraints()` · `r1cs.flat()` · `r1cs.numWires()` · `r1cs.numPublicInputs()` · `r1cs.numConstraints()` | `R1CSConstraintSystem` (`org.zeroj.circuit.r1cs`) | | `BigInteger[] w = circuit.calculateWitness(Map.of("a", List.of(BigInteger.valueOf(3)), …), CurveId.BLS12_381)` | `CircuitBuilder` (`org.zeroj.circuit`) | | `SecretMultiplierCircuit.inputs().a(3).product(33).b(11)` then `.toWitnessMap()` or `.calculateWitness(circuit, CurveId.BLS12_381)` | generated `…Circuit.Inputs` | | `new ZkInputMap().put("root", value).toWitnessMap()` | `ZkInputMap` (`org.zeroj.circuit.annotation`) | | Public inputs: `Arrays.copyOfRange(w, 1, 1 + r1cs.numPublicInputs())` (`w[0]` is always 1) | — | An unsatisfied constraint throws `ArithmeticException("Constraint violation: …")`; a missing input throws `IllegalArgumentException("Missing public input: …")` or `("Missing secret input: …")`. ### Trusted setup > **Caution: Dev/test only** > > The single-party calls below know the toxic waste and can forge proofs. They need > `-Dzeroj.allowInsecureTrustedSetup=true`. Production keys come from an MPC ceremony. See > [Trusted setup ceremony](https://zeroj.dev/guides/proving/trusted-setup-ceremony/). | Call | Class (package) | |---|---| | `BigInteger tau = PowersOfTauBLS381.generate(power).tauScalar()` (dev) | `PowersOfTauBLS381` (`org.zeroj.crypto.setup`) | | `try (var keys = Groth16Keys.setupInMemory(r1cs.constraints(), r1cs.numWires(), r1cs.numPublicInputs(), tau)) { … }` (dev, small circuits) | `Groth16Keys` (`org.zeroj.crypto.groth16`) | | `Groth16Keys.setupToStore(r1cs.flat(), numWires, numPublic, tau, keysDir, true)` (dev, large circuits, mmap'd store) | `Groth16Keys` (`org.zeroj.crypto.groth16`) | | `ZkeyPkStoreImporter.importToPkStore(zkeyPath, keysDir)` (production: import a ceremony `.zkey` once) | `ZkeyPkStoreImporter` (`org.zeroj.crypto.groth16`) | | `try (var keys = Groth16Keys.load(keysDir)) { … }` | `Groth16Keys` (`org.zeroj.crypto.groth16`) | | `var zkey = ZkeyImporterBLS381.importZkeyFull(zkeyBytes)` · `ZkeyImporterBLS381.importWtns(inputStream)` | `ZkeyImporterBLS381` (`org.zeroj.crypto.groth16`) | | `TrustedSetupPolicy.insecureTrustedSetupEnabled()` | `TrustedSetupPolicy` (`org.zeroj.api`) | ### Prove (Groth16) | Call | Class (package) | |---|---| | `Groth16ProofBLS381 proof = keys.prove(witness, r1cs.constraints())` | `Groth16Keys`, `Groth16ProofBLS381` (`org.zeroj.crypto.groth16`) | | `keys.prove(BlstProverBackend.create(), witness, r1cs.constraints())` (opt-in native MSM) | `BlstProverBackend` (`org.zeroj.cryptoblst`, module `zeroj-crypto-blst`) | | `Groth16ProverBLS381.prove(zkey.provingKey(), witness, zkey.constraints(), zkey.numWires())` | `Groth16ProverBLS381` (`org.zeroj.crypto.groth16`) | | Very large circuits: `Groth16Pipeline.setup(...)` / `Groth16Pipeline.prove(...)` | `Groth16Pipeline` (`org.zeroj.crypto.groth16`), see [Performance](https://zeroj.dev/guides/proving/performance/) | Every proof is blinded with fresh randomness; there is no public deterministic prove API. ### Export snarkjs JSON | Call | Class (package) | |---|---| | `SnarkjsGroth16Json.verificationKeyJson(keys)` | `SnarkjsGroth16Json` (`org.zeroj.crypto.snarkjs`) | | `SnarkjsGroth16Json.proofJson(proof)` | `SnarkjsGroth16Json` (`org.zeroj.crypto.snarkjs`) | | `SnarkjsGroth16Json.publicJson(publicInputs)` (a `BigInteger[]`) | `SnarkjsGroth16Json` (`org.zeroj.crypto.snarkjs`) | Output is byte-identical to what snarkjs 0.7.6 writes, so `snarkjs groth16 verify` accepts it. ### Verify off-chain See [Verify proofs in Java](https://zeroj.dev/guides/verifying/off-chain/). | Call | Class (package) | |---|---| | `SnarkjsJsonCodec.toEnvelopeFromJson(proofJson, vkJson, publicJson, new CircuitId("mul"))` | `SnarkjsJsonCodec` (`org.zeroj.codec`), `CircuitId` (`org.zeroj.api`) | | `VerificationMaterial.of(vkJson.getBytes(UTF_8), ProofSystemId.GROTH16, CurveId.BLS12_381, circuitId)` | `VerificationMaterial`, `ProofSystemId` (`org.zeroj.api`) | | `new Groth16BLS12381PureJavaVerifier().verify(envelope, material).proofValid()` | `Groth16BLS12381PureJavaVerifier` (`org.zeroj.verifier.groth16.bls12381`) | | `VerifierRegistry.empty()` + `register(verifier)` · `VerifierRegistry.withServiceLoader()` | `VerifierRegistry` (`org.zeroj.verifier.core`) | | `new InMemoryVerificationKeyRegistry()` + `register(material)` | `InMemoryVerificationKeyRegistry` (`org.zeroj.backend.spi`) | | `new VerifierOrchestrator(backends, keys).verify(envelope)` | `VerifierOrchestrator` (`org.zeroj.verifier.core`) | | `VerificationResult.ok()` · `policyRejected(ReasonCode.USED_NULLIFIER, msg)` | `VerificationResult` (`org.zeroj.api`) | | `CborEnvelopeCodec.encode(envelope)` · `decode(bytes)` · `CanonicalHash.sha256(vkBytes)` | `CborEnvelopeCodec`, `CanonicalHash` (`org.zeroj.codec`) | A backend's success result has `proofValid() == true` and `accepted() == false`; acceptance is your policy's decision. ### Compress for Cardano | Call | Class (package) | |---|---| | `ProverToCardano.compressVk(keys)` → `VkCompressed(alpha, beta, gamma, delta, ic)` | `ProverToCardano` (`org.zeroj.onchain.julc.groth16.codec`) | | `ProverToCardano.compressProof(proof)` → `ProofCompressed(piA, piB, piC)` | `ProverToCardano` (`org.zeroj.onchain.julc.groth16.codec`) | | `SnarkjsToCardano.parseVk(vkJson)` · `parseProof(proofJson)` · `parsePublicInputs(publicJson)` | `SnarkjsToCardano` (`org.zeroj.onchain.julc.groth16.codec`) | ### Load and use the on-chain verifier See [Verify proofs on Cardano](https://zeroj.dev/guides/verifying/on-chain/). A valid proof is not authorization: bind the proof to the transaction in your own validator. | Call | Class (package) | |---|---| | `JulcScriptLoader.load(Groth16BLS12381Verifier.class, new BytesPlutusData(vk.alpha()), …, icList)` | `JulcScriptLoader` (`com.bloxbean.cardano.julc.clientlib`, artifact `julc-cardano-client-lib`); `Groth16BLS12381Verifier` (`org.zeroj.onchain.julc.groth16.validator`) | | `AddressProvider.getEntAddress(script, Networks.testnet()).toBech32()` | Cardano Client Lib (`com.bloxbean.cardano.client.address`) | | Datum: `ListPlutusData.of(BigIntPlutusData.of(pub0), …)` · Redeemer: `Constr 0 [piA, piB, piC]` | Cardano Client Lib (`com.bloxbean.cardano.client.plutus.spec`) | | In your validator: `Groth16BLS12381Lib.verify(datum, piA, piB, piC, vkAlpha, vkBeta, vkGamma, vkDelta, vkIc)` | `Groth16BLS12381Lib` (`org.zeroj.onchain.julc.groth16.lib`) | | Spend-binding reference example (reads the bound value from the datum, so it can't lock real funds as-is): `Groth16BLS12381TxOutRefBindingVerifier` | `org.zeroj.onchain.julc.groth16.validator` | | `ScriptBudgetEstimator.estimateCpu(ProofSystemId.GROTH16, CurveId.BLS12_381, n)` · `OnChainFeasibility.lookup(...)` | `org.zeroj.onchain.julc.analysis` | ### BBS credentials `zeroj-bbs` is opt-in: declare it with an explicit version. See [Selective disclosure with BBS](https://zeroj.dev/guides/credentials/bbs/). | Call | Class (package) | |---|---| | `BbsService bbs = BbsService.pureJava()` | `BbsService` (`org.zeroj.bbs`) | | `BbsService.withBlsProvider(BbsCiphersuite.BLS12381_SHA256, BlstBls12381Provider.createDefault())` (issuers) | `BbsCiphersuite` (`org.zeroj.bbs`), `BlstBls12381Provider` (`org.zeroj.blst`) | | `BbsKeyPair kp = bbs.keyPair(keyMaterial32Bytes, keyInfo)` | `BbsKeyPair` (`org.zeroj.bbs`) | | `BbsSignature sig = bbs.sign(kp.secretKey(), kp.publicKey(), messages, header)` | `BbsSignature` (`org.zeroj.bbs`) | | `bbs.verify(kp.publicKey(), sig, messages, header)` | `BbsService` (`org.zeroj.bbs`) | | `BbsPresentation p = bbs.derivePresentation(pk, sig, messages, header, presentationHeader, new int[]{2, 3})` | `BbsPresentation` (`org.zeroj.bbs`) | | `bbs.verifyPresentation(pk, p)`, then compare `p.presentationHeader()` / `p.header()` with what you expect | `BbsService` (`org.zeroj.bbs`) | | `BbsPresentationCodec.encode(p)` · `decode(bytes)` | `BbsPresentationCodec` (`org.zeroj.bbs`) | | `new BbsPublicKey(bytes, BbsCiphersuite.BLS12381_SHA256)` · `pk.bytes()` | `BbsPublicKey` (`org.zeroj.bbs`) | | `BbsToCardano.verifierParams(pk, header, messageCount)` · `BbsToCardano.onChainProof(p)` | `BbsToCardano` (`org.zeroj.bbs.cardano`) | ### PlonK (experimental) > **Caution: Experimental** > > PlonK proving and verification are experimental, opt-in paths. Groth16 is the focus of the current > release. See [PlonK](https://zeroj.dev/guides/proving/plonk/) before using them. | Class | Package | |---|---| | `PlonKSetupBLS381`, `PlonKProverBLS381`, `PtauImporterBLS381` | `org.zeroj.crypto.plonk` | | `PlonkBLS12381Verifier` (module `zeroj-verifier-plonk`) | `org.zeroj.verifier.plonk` | | `PlonkBLS12381Verifier`, `PlonkBLS12381MultiInputVerifier`, `PlonkBLS12381MultiInputParamVerifier` (on-chain) | `org.zeroj.onchain.julc.plonk.validator` | ### Next steps - [Modules](https://zeroj.dev/reference/modules/) - [Configuration](https://zeroj.dev/reference/configuration/) - [Quickstart](https://zeroj.dev/start/quickstart/) --- ## Configuration Source: https://zeroj.dev/reference/configuration/ > Every ZeroJ system property, environment variable, native-library requirement, external tool and contributor Gradle flag, and which ones are dev-only. ZeroJ has very little configuration, on purpose. At runtime the library reads **three** `zeroj.*` system properties and **two** `ZEROJ_*` environment variables. Everything else is ordinary Java: the dependencies you declare and the objects you construct. This page lists all of it, plus the native-library and build details you may run into. ### Runtime switches | System property | Environment variable | Default | Dev-only? | Effect | |---|---|---|---|---| | `zeroj.allowInsecureTrustedSetup` | `ZEROJ_ALLOW_INSECURE_TRUSTED_SETUP` | off | **Yes** | Allows single-party trusted setup | | `zeroj.allowLegacyBn254` | `ZEROJ_ALLOW_LEGACY_BN254` | off | **Yes** | Allows the legacy BN254 proving and verification classes | | `zeroj.jubjub.debugSecretSubgroupChecks` | — | off | **Yes** (diagnostic) | Adds a subgroup assertion inside the off-circuit Jubjub secret-scalar helpers | A switch is on when the system property is `true` (via `Boolean.getBoolean`), or when the environment variable equals `true` (case-insensitive). Either one is enough. #### `zeroj.allowInsecureTrustedSetup` Groth16 needs a trusted setup whose secret ("toxic waste") must be destroyed. ZeroJ's in-process setup generates that secret itself, so whoever runs it could forge proofs. The following refuse to run unless the switch is on: - `PowersOfTauBLS381.generate(...)` - `Groth16SetupBLS381.setup(...)` and `Groth16SetupBLS381.setupToStore(...)` - `Groth16Keys.setupInMemory(...)` and `Groth16Keys.setupToStore(...)`, which call the above - the SRS cache, and the legacy BN254 setup classes `PowersOfTau` and `Groth16Setup` (which also need the BN254 switch below) Without it you get: ```text java.lang.IllegalStateException: Single-party trusted setup is disabled by default because the generator knows toxic waste and can forge proofs. Production deployments must use imported MPC ceremony artifacts and pinned artifact hashes. For local development or tests only, start the JVM with -Dzeroj.allowInsecureTrustedSetup=true. ``` Turn it on for tests and local experiments only: ```bash java -Dzeroj.allowInsecureTrustedSetup=true -jar my-dev-app.jar # or ZEROJ_ALLOW_INSECURE_TRUSTED_SETUP=true ./gradlew run ``` ```groovy title="build.gradle" tasks.withType(Test).configureEach { systemProperty 'zeroj.allowInsecureTrustedSetup', 'true' // tests only } ``` Production keys come from a multi-party ceremony and are imported (`ZkeyPkStoreImporter`, `ZkeyImporterBLS381`); importing and proving with them needs no switch. The constants are `TrustedSetupPolicy.ALLOW_INSECURE_TRUSTED_SETUP_PROPERTY` and `TrustedSetupPolicy.ALLOW_INSECURE_TRUSTED_SETUP_ENV`, and `TrustedSetupPolicy.insecureTrustedSetupEnabled()` tells you whether it is on. #### `zeroj.allowLegacyBn254` BN254 is not a Cardano curve. Plutus has no BN254 builtins. ZeroJ keeps a few BN254 classes for old off-chain experiments, disabled by default and not registered with `ServiceLoader`. Without the switch they fail with: ```text java.lang.IllegalStateException: BN254 is disabled by default because ZeroJ targets Cardano production flows and Cardano only supports BLS12-381 on-chain. For legacy off-chain experiments, start the JVM with -Dzeroj.allowLegacyBn254=true. ``` The constants are `LegacyCurvePolicy.ALLOW_LEGACY_BN254_PROPERTY` / `ALLOW_LEGACY_BN254_ENV`, and `LegacyCurvePolicy.legacyBn254Enabled()` reports the state. Use BLS12-381 instead. #### `zeroj.jubjub.debugSecretSubgroupChecks` A diagnostic for the off-circuit Jubjub helpers in `zeroj-circuit-lib` (`EdDSAJubjub`, `PedersenCommitment`). When set, their blinded secret-scalar multiplication first asserts that the point is in the prime-order subgroup and throws `IllegalStateException` otherwise. Leave it unset unless you are debugging those helpers. > **Danger: Fail closed in production** > > Make production services refuse to start if a development switch is on: > `if (TrustedSetupPolicy.insecureTrustedSetupEnabled() || LegacyCurvePolicy.legacyBn254Enabled()) throw …`. > Remember that the environment variables work too: check the deployment environment, not just the > command line. ### Native blst library The default path is pure Java. Native code enters only if you add `zeroj-blst` or a module that uses it: | Where blst is used | Binding | Pulled in by | |---|---|---| | Groth16 prover MSM (`BlstProverBackend`) | Java 25 FFM, with a `libblst` built from source (pinned v0.3.15) and bundled in the `zeroj-blst` jar | `zeroj-crypto-blst` | | Pairing for `Groth16BLS12381Verifier` (off-chain) and `BlstBls12381Provider` (BBS) | JNI via `foundation.icon:blst-java` | `zeroj-verifier-groth16`, `zeroj-blst` | How the FFM library is found: - There is **no path setting**. `BlstFfm` picks `/native///libblst.` from the jar (`os` is `linux`, `mac` or `windows`; `arch` is `amd64` or `aarch64`, with `x86_64` on macOS), copies it to a temp file named `libblst-zeroj-*`, and loads it. The JVM needs a writable temp directory. - Release builds rebuild the bundled binaries from source for each supported platform. macOS is **arm64-only**: Intel Macs can't use the blst prover backend but work normally on the pure-Java default. - If your platform has no bundled binary, the first blst MSM call fails with an error containing `Bundled libblst not found for this platform`. Stay on the pure-Java path. - FFM downcalls need native access enabled: ```bash java --enable-native-access=ALL-UNNAMED -cp … my.App ``` GraalVM native-image metadata for `zeroj-blst` ships under `META-INF/native-image/org.zeroj/zeroj-blst/`. ### External tools: circom and snarkjs The published ZeroJ libraries **never launch external processes**. Nothing at runtime looks for `circom` or `snarkjs`. You run those tools yourself, from your `PATH`, and hand ZeroJ their output files (`.r1cs`, `.zkey`, `.wtns`, `*.json`): ```bash # circom 2.x: build it from source as described at https://docs.circom.io/getting-started/installation/ npm install -g snarkjs # ZeroJ's interop tests pin snarkjs 0.7.6 circom circuit.circom --r1cs --wasm --sym -p bls12381 # BLS12-381, not the default BN254 ``` ZeroJ's own test suite finds snarkjs through the `SNARKJS_BIN` environment variable, then a few common npm install locations, then `PATH`. See [Bring circom & snarkjs circuits](https://zeroj.dev/tutorials/snarkjs-interop/). ### Gradle flags for contributors These only matter when you build the ZeroJ repository itself. Use the Gradle wrapper. | Command or flag | What it does | |---|---| | `./gradlew build` | Default pure-Java build: no Go, Rust, Node.js, WASM toolchain or RocksDB needed | | `-PincludeAssurance` | Adds the WASM differential oracles (`zeroj-bls12381-wasm`, `zeroj-bbs-wasm`; need Rust + the `wasm32-unknown-unknown` target). Also makes `:zeroj-bbs:test` run the official BBS vectors through the WASM provider. | | `-PincludeBenchmarks` | Adds the MPF/JMT RocksDB load and benchmark tools | | `-PrequireSnarkjs` | Makes the snarkjs interop suites in `zeroj-integration-tests` **fail** instead of skip when snarkjs 0.7.6 is missing (sets `zeroj.assurance.requireSnarkjs=true`) | | `./gradlew :zeroj-integration-tests:e2eTest` | Runs `@Tag("e2e")` tests: Yaci DevKit on-chain flows and snarkjs proving. They skip gracefully when Yaci DevKit (`localhost:8080`) or snarkjs is absent. | | `./gradlew verifyDefaultModuleSurface` | Checks the stable module graph has no edge into assurance, benchmark or removed modules | ```bash ./gradlew -PincludeAssurance :zeroj-bls12381-wasm:test :zeroj-bbs-wasm:test :zeroj-bbs:test ./gradlew -PincludeBenchmarks :zeroj-mpf-poseidon-load:build :zeroj-jmt-poseidon-load:build ./gradlew -PrequireSnarkjs :zeroj-integration-tests:test ``` Inside the repository, every test JVM already runs with `--enable-native-access=ALL-UNNAMED` and `zeroj.allowInsecureTrustedSetup=true` (and `zeroj-crypto`'s tests also set `zeroj.allowLegacyBn254`). That is test scaffolding. Don't copy it into application run configurations. Opt-in benchmark tasks such as `:zeroj-crypto:benchmark`, `:zeroj-crypto-blst:blstBench` and `:zeroj-circuit-lib:heavyGadgetTest` set their own internal `zeroj.bench` / `zeroj.heavy` properties. They are heavy and never part of `test`. ### Next steps - [Installation](https://zeroj.dev/start/installation/) - [Secure your ZK application](https://zeroj.dev/guides/verifying/application-security/#development-flags-never-reach-production) - [FAQ & troubleshooting](https://zeroj.dev/reference/faq/) --- ## FAQ & troubleshooting Source: https://zeroj.dev/reference/faq/ > Straight answers about ZeroJ's maturity, proof systems, tooling and performance, plus fixes for the errors you are most likely to hit. ### General #### Is ZeroJ production-ready? No. ZeroJ is experimental research software. It has **not** been externally audited and is not for production, value-bearing or mainnet use. Its most mature path, Groth16 on BLS12-381, is labelled **Beta**: feature-complete and correctness-tested (3,500+ tests, with the full flow verified end-to-end on-chain against Yaci DevKit), and on-chain verification is **testnet only**. Other areas are experimental. See [Status & maturity](https://zeroj.dev/start/status/). #### Which proof system should I use? **Groth16 on BLS12-381.** It is the focus of the current release, the default in every guide and tutorial, and the only proof system with a Beta on-chain verifier. Proofs are small (192 bytes compressed) and cheap to verify on Cardano. Its one cost is a per-circuit trusted setup; see [Trusted setup, explained](https://zeroj.dev/learn/trusted-setup/). PlonK exists in ZeroJ but is **experimental** everywhere, off-chain and on-chain. Don't choose it for new work. If you only need to reveal some signed attributes (not prove predicates over hidden ones), look at [BBS](https://zeroj.dev/guides/credentials/bbs/), which needs no circuit and no setup. #### Why BLS12-381 and not BN254? Cardano's Plutus V3 has built-in BLS12-381 operations (CIP-0381), so a validator can run the pairing check. There are no BN254 builtins, so BN254 proofs can't be verified on Cardano. ZeroJ keeps some BN254 classes for old off-chain experiments, disabled by default. #### Do I need Rust, Node.js or native libraries? No. The default path (circuit DSL, witness generation, trusted setup for development, proving, verification and on-chain codecs) is pure Java 25, and there is nothing to install beyond a JDK. - **Native code that is on the classpath anyway:** `zeroj-verifier-groth16` depends on `zeroj-blst`, which brings the `blst-java` JNI jar for the module's native `Groth16BLS12381Verifier`. `Groth16BLS12381PureJavaVerifier` never loads it, but `VerifierRegistry.withServiceLoader()` lists the native verifier first, so construct the pure-Java verifier explicitly if you want to stay pure Java. - **Optional native code:** `zeroj-blst` / `zeroj-crypto-blst` bundle a source-built `libblst` for a faster prover MSM. Opt in only after benchmarking; at large sizes the pure-Java prover matches it. - **Optional tools:** circom (Rust) and snarkjs (Node.js) only if you bring circom circuits or run a snarkjs ceremony. ZeroJ never launches them itself. - **Contributors only:** Rust, Go and RocksDB for the opt-in assurance and benchmark projects. #### Can I use my circom circuits? Yes, for Groth16 on BLS12-381. Compile with `circom circuit.circom --r1cs --wasm --sym -p bls12381` (circom's default prime is BN254), run the snarkjs setup or ceremony on the BLS12-381 curve, then either verify snarkjs proofs in Java with `SnarkjsJsonCodec` or import the `.zkey` and prove in Java with `ZkeyImporterBLS381` / `ZkeyPkStoreImporter`. See [Bring circom & snarkjs circuits](https://zeroj.dev/tutorials/snarkjs-interop/). #### How big and fast are proofs? Numbers ZeroJ has measured and published (they depend on hardware and JVM): | What | Measured | |---|---| | Groth16 proof, compressed for Cardano | 192 bytes | | Groth16 VK, compressed, one public input | 432 bytes | | Off-chain verification in the JVM | about 115 ms (113–119 ms across runs) | | Proving circuits of about 10k–74k constraints | about 2.9–4.6 s median | | On-chain verification, generic verifier, one public input (JuLC VM, full `ScriptContext`) | 2,627,770,348 CPU steps, 177,749 memory units | The proving, off-chain verification and on-chain figures come from the Poseidon MPF/JMT benchmarks. Each extra public input adds about 0.2 billion CPU steps on-chain, and your validator's own checks add more; see [Verify proofs on Cardano](https://zeroj.dev/guides/verifying/on-chain/#budgets-and-script-size). Circuits with millions of constraints can be proved within commodity memory using the store-backed keys; see [Proving performance](https://zeroj.dev/guides/proving/performance/). #### Does a valid proof mean the action is allowed? No. A proof only shows that someone knows a witness for the circuit and public inputs. Replay protection, nullifiers, authorization and business rules are yours to build. See [Secure your ZK application](https://zeroj.dev/guides/verifying/application-security/). #### Why is `accepted()` false when `proofValid()` is true? Verifier backends only check cryptography. They return `VerificationResult.cryptoValid()`, where `protocolValid()` is empty and `accepted()` is `false`. Your policy layer decides acceptance, for example by returning `VerificationResult.ok()`. Check `proofValid()` for the crypto result. ### Errors and fixes #### "Single-party trusted setup is disabled by default…" ```text java.lang.IllegalStateException: Single-party trusted setup is disabled by default because the generator knows toxic waste and can forge proofs. … ``` You called a development setup (`PowersOfTauBLS381.generate`, `Groth16Keys.setupInMemory`, `Groth16SetupBLS381.setup`, …) without opting in. For tests and local experiments, start the JVM with `-Dzeroj.allowInsecureTrustedSetup=true` (or set `ZEROJ_ALLOW_INSECURE_TRUSTED_SETUP=true`). For anything real, don't flip the switch: run a multi-party ceremony and import its `.zkey`. See [Trusted setup ceremony](https://zeroj.dev/guides/proving/trusted-setup-ceremony/). #### "R1CS … references wire … outside [0, …)" or "witness length … must match numWires" Every setup and prove entry point validates the relation's shape before doing any work: ```text IllegalArgumentException: R1CS A row 3 references wire 9 outside [0, 8) IllegalArgumentException: witness length (7) must match numWires (8) IllegalArgumentException: witness[0] must be 1 ``` The constraints, `numWires`/`numPublic`, or witness you passed don't describe the circuit the key was made for. Usually you mixed artifacts from two compilations, or passed a hand-built witness. Recompile, and pass `r1cs.constraints()`, `r1cs.numWires()` and the witness from `calculateWitness` for the same circuit. Don't catch and retry. #### "R1CS public wire … is not referenced by any constraint" ```text IllegalArgumentException: R1CS public wire 2 (public input 2 of 3) is not referenced by any constraint: IC[2] would be the point at infinity and the public input would be unbound by the verification equation (ADR-0045). Constrain the input in the circuit or remove it from the public inputs. ``` A public input that no constraint uses would leave it unbound: the proof would verify for *any* value. ZeroJ's setup refuses such a relation. Constrain the input, or drop it from the public inputs. The related message `R1CS constant wire 0 (ONE) is not referenced by any constraint` means the relation has no constant term. DSL circuits get one from `assertEqual`. A hand-written relation needs a row that references wire 0, such as `1 * 1 = 1`. #### Witness calculation throws "Constraint violation" ```text ArithmeticException: Constraint violation: … ``` Your inputs don't satisfy the circuit, which is the circuit doing its job (an under-age input to an age check, say). Fix the inputs, or catch the exception and report "can't prove this". Related messages: `Missing public input: ` and `Missing secret input: ` mean a key is missing from the input map. Inputs are reduced modulo the BLS12-381 scalar field before evaluation, so a negative or oversized number becomes a different field element instead of an error. Range limits must be constraints in the circuit. #### OutOfMemoryError on a large circuit `Groth16Keys.setupInMemory` keeps the proving key on the heap, which is fine up to a few hundred thousand constraints. Beyond that: - set up with `Groth16Keys.setupToStore(r1cs.flat(), numWires, numPublic, tau, keysDir, true)` and reopen with `Groth16Keys.load(keysDir)`, which memory-maps the key; - import ceremony keys with `ZkeyPkStoreImporter.importToPkStore(zkeyPath, keysDir)`; - for multi-million-constraint circuits, use `Groth16Pipeline`, which orders compile, witness and MSM work to keep the peak low; - give the JVM enough `-Xmx` and run the prover as its own process, not inside a request thread. See [Proving performance](https://zeroj.dev/guides/proving/performance/). #### "BN254 is disabled by default…" ```text java.lang.IllegalStateException: BN254 is disabled by default because ZeroJ targets Cardano production flows and Cardano only supports BLS12-381 on-chain. … ``` You called a legacy BN254 class, such as the curve-less `ZkeyImporter`, `Groth16Prover` or `PtauImporter`, often because the artifacts were BN254 (snarkjs's and circom's default curve). Use the BLS12-381 classes (`ZkeyImporterBLS381`, `Groth16ProverBLS381`, `Groth16Keys`, …) with BLS12-381 artifacts and `CurveId.BLS12_381`. Set `-Dzeroj.allowLegacyBn254=true` only for old off-chain experiments. #### snarkjs and ZeroJ disagree about a proof Check, in order: 1. **Curve.** Every artifact must be BLS12-381 (`"curve": "bls12381"` in the JSON). BN254 files won't cross over. 2. **Same key.** Verify against the exact `verification_key.json` from the same setup or ceremony as the proving key. 3. **Public-input order.** `public.json` must list wires `1..numPublic` in circuit order. 4. **Use the exporter.** `SnarkjsGroth16Json.verificationKeyJson/proofJson/publicJson` write exactly what snarkjs 0.7.6 writes, including the `vk_alphabeta_12` pairing value. Don't hand-build the JSON. 5. **Unused public signals.** snarkjs quietly binds unused public signals during its setup, while ZeroJ's native setup refuses them (see above). Keys imported from a snarkjs ceremony are unaffected. ZeroJ's interop tests pin snarkjs 0.7.6. See [Bring circom & snarkjs circuits](https://zeroj.dev/tutorials/snarkjs-interop/). #### On-chain: execution budget exceeded - Measure first: run the validator in the JuLC VM, and use `ScriptBudgetEstimator` for rough numbers (it counts only the BLS12-381 builtins, so real validators cost more). The generic one-input Groth16 verifier measured about 2.63 billion CPU steps. - Each public input costs an extra scalar multiplication on-chain. Keep the count low. - Keep other heavy logic out of the same script execution, and compare with the network's *current* protocol parameters. - Reference scripts (CIP-0033) cut transaction size and fees, not execution units. If the transaction builder's cost evaluation fails with errors mentioning `supranational.blst` or an `ExceptionInInitializerError`, the evaluator couldn't load the native blst library on that machine. This is an evaluator environment problem, not a proof problem; ZeroJ's own Yaci DevKit tests skip in that situation. #### My script hash changed after upgrading JuLC Expected. The script bytes, and so the hash and address, come from the JuLC compiler. A compiler upgrade can change them even when your Java source didn't change. Treat it as a new script: re-measure budgets, publish the new hash, and plan how funds at the old address move. Renaming Java packages alone does not change the hash. See [Verify proofs on Cardano](https://zeroj.dev/guides/verifying/on-chain/#julc-version-coupling). #### The verifier registry picks the blst verifier `VerifierRegistry.withServiceLoader()` discovers the blst-backed `Groth16BLS12381Verifier` before the pure-Java one, and `find` returns the first match. Build the registry explicitly with `VerifierRegistry.empty()` and `register(new Groth16BLS12381PureJavaVerifier())` when you want a specific backend. ### Platform #### Does ZeroJ work with GraalVM native image? The pure-Java path is designed to be GraalVM-compatible, and modules that need it ship native-image metadata under `META-INF/native-image/org.zeroj//`. Build and test your own image, and keep these in mind: - register verifier backends explicitly rather than relying on `ServiceLoader` discovery; - the blst FFM path needs `--enable-native-access=ALL-UNNAMED` and a bundled `libblst` for your platform; - the `zeroj-ceremony` CLI is published as native binaries. Its `contribute` and `finalize` commands are native-friendly, while `export-r1cs` loads circuit classes reflectively and is usually run on a JVM. #### Can I use Maven or Kotlin? Yes. ZeroJ is a set of ordinary Maven Central artifacts under `org.zeroj`. Import `zeroj-bom-core` in `` for Maven; any JVM language can call the APIs. See [Installation](https://zeroj.dev/start/installation/). ### Security #### Are my secrets safe in the JVM? ZeroJ doesn't claim constant-time behaviour for witness generation or the pure-Java prover (they use `BigInteger`). Prove on hardware you control. For BBS issuer keys, use the blst provider. Keep witnesses and `.wtns` files short-lived and out of logs. See [Secure your ZK application](https://zeroj.dev/guides/verifying/application-security/#secrets-in-java). #### How do I report a security issue? The repository has no `SECURITY.md` or published private disclosure address yet. Please don't post exploit details in a public issue. Check the **Security** tab of [bloxbean/zeroj](https://github.com/bloxbean/zeroj) for private vulnerability reporting. If it isn't available, open a short issue asking the maintainers for a private contact, without technical details. ### Next steps - [Status & maturity](https://zeroj.dev/start/status/) - [Configuration](https://zeroj.dev/reference/configuration/) - [Secure your ZK application](https://zeroj.dev/guides/verifying/application-security/) --- ## Migration notes Source: https://zeroj.dev/reference/migration/ > Move from com.bloxbean.cardano to the org.zeroj namespace, and from the pre-cleanup module set to today's focused module surface. ZeroJ is pre-1.0 and its coordinates have changed twice. This page covers both changes. Neither one changes cryptography, proof bytes, keys, transcripts or public-input order, and neither upgrades any maturity claim. ### The `org.zeroj` namespace Starting with **`0.1.0-pre12`**, ZeroJ moved from the `com.bloxbean.cardano` Maven group and the `com.bloxbean.cardano.zeroj.*` package root to **`org.zeroj`** for both. Every class keeps its simple name and behaviour. There is **no compatibility shim**: no type aliases, and no `com.bloxbean.cardano:zeroj-*` artifacts after `0.1.0-pre11`. If you can't migrate yet, stay on `0.1.0-pre11`, which remains on Maven Central unchanged. #### Two rules 1. **Group:** `com.bloxbean.cardano:zeroj-…` → `org.zeroj:zeroj-…`. Artifact ids don't change. 2. **Packages:** `com.bloxbean.cardano.zeroj.` → `org.zeroj.`. Sub-packages don't change. So `com.bloxbean.cardano.zeroj.crypto.groth16.Groth16ProverBLS381` becomes `org.zeroj.crypto.groth16.Groth16ProverBLS381`. For most projects that is three `sed` passes over your own sources and build files: ```bash # packages (source, and any FQN in config, resources or docs) grep -rl 'com\.bloxbean\.cardano\.zeroj' . | xargs sed -i '' 's#com\.bloxbean\.cardano\.zeroj#org.zeroj#g' # slash-separated paths (architecture tests, scripts, docs) grep -rl 'com/bloxbean/cardano/zeroj' . | xargs sed -i '' 's#com/bloxbean/cardano/zeroj#org/zeroj#g' # Maven coordinates grep -rl 'com\.bloxbean\.cardano:zeroj' . | xargs sed -i '' 's#com\.bloxbean\.cardano:zeroj#org.zeroj:zeroj#g' ``` On GNU `sed` (Linux), drop the `''` after `-i`. > **Danger: Don't blanket-replace `com.bloxbean.cardano`** > > It still owns ZeroJ's dependencies, which have **not** moved: Cardano Client Lib > (`com.bloxbean.cardano:cardano-client-*`), JuLC (`com.bloxbean.cardano.julc.*`, group > `com.bloxbean.cardano`) and VDS (`com.bloxbean.cardano.vds.*`). Anchor every replacement on > `zeroj`, as the commands above do. If **your own** packages live under `com.bloxbean.cardano.zeroj.*` (the zeroj-usecases apps do, for example), rule 2 renames them too, which also means moving source directories. Decide that deliberately. To keep your packages, anchor the replacement on the ZeroJ sub-packages you actually import instead of the bare prefix. #### Coordinates | Before (≤ `0.1.0-pre11`) | After (≥ `0.1.0-pre12`) | |---|---| | `com.bloxbean.cardano:zeroj-bom-core` | `org.zeroj:zeroj-bom-core` | | `com.bloxbean.cardano:zeroj-api` | `org.zeroj:zeroj-api` | | `com.bloxbean.cardano:zeroj-codec` | `org.zeroj:zeroj-codec` | | `com.bloxbean.cardano:zeroj-backend-spi` | `org.zeroj:zeroj-backend-spi` | | `com.bloxbean.cardano:zeroj-verifier-groth16` | `org.zeroj:zeroj-verifier-groth16` | | `com.bloxbean.cardano:zeroj-verifier-plonk` | `org.zeroj:zeroj-verifier-plonk` | | `com.bloxbean.cardano:zeroj-bls12381` | `org.zeroj:zeroj-bls12381` | | `com.bloxbean.cardano:zeroj-blst` | `org.zeroj:zeroj-blst` | | `com.bloxbean.cardano:zeroj-crypto` | `org.zeroj:zeroj-crypto` | | `com.bloxbean.cardano:zeroj-crypto-blst` | `org.zeroj:zeroj-crypto-blst` | | `com.bloxbean.cardano:zeroj-circuit-dsl` | `org.zeroj:zeroj-circuit-dsl` | | `com.bloxbean.cardano:zeroj-circuit-lib` | `org.zeroj:zeroj-circuit-lib` | | `com.bloxbean.cardano:zeroj-circuit-annotation-api` | `org.zeroj:zeroj-circuit-annotation-api` | | `com.bloxbean.cardano:zeroj-circuit-annotation-processor` | `org.zeroj:zeroj-circuit-annotation-processor` | | `com.bloxbean.cardano:zeroj-onchain-julc` | `org.zeroj:zeroj-onchain-julc` | | `com.bloxbean.cardano:zeroj-tools` | `org.zeroj:zeroj-tools` | | `com.bloxbean.cardano:zeroj-bbs` | `org.zeroj:zeroj-bbs` | | `com.bloxbean.cardano:zeroj-mpf-poseidon` | `org.zeroj:zeroj-mpf-poseidon` | | `com.bloxbean.cardano:zeroj-jmt-poseidon` | `org.zeroj:zeroj-jmt-poseidon` | Modules removed by the earlier cleanup (below) are **not** reissued under `org.zeroj`. A minimal build after the move: ```groovy title="build.gradle" dependencies { implementation platform("org.zeroj:zeroj-bom-core:0.1.0-pre12") implementation 'org.zeroj:zeroj-circuit-dsl' // version from the BOM implementation 'org.zeroj:zeroj-crypto' implementation 'org.zeroj:zeroj-verifier-groth16' // opt-in artifacts stay outside the BOM and carry their own version implementation 'org.zeroj:zeroj-bbs:0.1.0-pre12' } ``` #### Easy to miss - **`META-INF/services` files** have no extension, so filters by file type skip them. If you register your own `ZkVerifier`, rename the provider file from `com.bloxbean.cardano.zeroj.backend.spi.ZkVerifier` to `org.zeroj.backend.spi.ZkVerifier`, and update its contents if your own packages moved. A half-done rename **compiles cleanly** and gives a verifier registry that finds nothing at runtime. - **GraalVM native-image config** directories are resolved from the Maven group. If you ship your own config for ZeroJ types, move it to `META-INF/native-image/org.zeroj/…` and update class names inside `reflect-config.json` / `resource-config.json`. Stale config is skipped **silently**. - **Fully qualified names in strings**: `Class.forName`, logging configuration, `--initialize-at-build-time=` arguments, Gradle test filters, `Main-Class` manifest attributes and `-cp … ` in scripts. - **Slash-separated paths** in architecture tests, resource lookups and shell scripts. - **The ceremony CLI class** is now `org.zeroj.ceremony.CeremonyCli`. The `zeroj-ceremony` command name, behaviour, transcript bytes and release asset names are unchanged. #### What does not change - Proof bytes, verification keys, proving keys and every serialized artifact. None encode a Java package name, so keys and proofs from `0.1.0-pre11` verify unchanged. - Circuit constraint systems and their fingerprints, transcripts, domain separators and public-input order. - Artifact ids, the module graph and the core/opt-in split. - **On-chain script hashes.** JuLC doesn't carry the Java package name into compiled code; ZeroJ measured an identical script hash for its state-transition validator before and after the rename. Your own validators keep their hash too, as long as nothing else about them changes (including the JuLC version). ### The focused module surface An earlier pre-release narrowed ZeroJ to its Java-first product path. If you come from a release that still had the modules below, here is where things went. (This change happened while ZeroJ still used `com.bloxbean.cardano`. The replacements now live at `org.zeroj:`.) | Removed module | What to do | |---|---| | `zeroj-verifier-core` | Depend on `zeroj-backend-spi`. `VerifierRegistry` and `VerifierOrchestrator` moved there and kept their `verifier.core` package. | | `zeroj-prover-spi` | No replacement. Use the concrete `zeroj-crypto` APIs (`Groth16Keys`, `Groth16Pipeline`, `Groth16ProverBLS381`, …) or `zeroj-crypto-blst`. | | `zeroj-prover-gnark` | No runtime replacement. Use `zeroj-crypto`, optionally with `zeroj-crypto-blst`. | | `zeroj-verifier-halo2`, `zeroj-prover-wasm` | No replacement. Pin the last release that had them if you depend on them. | | `zeroj-ceremony` | Use `zeroj-tools`; `CeremonyCli` moved there, and the `zeroj-ceremony` command is unchanged. | | `zeroj-cardano`, `zeroj-ccl` | Use Cardano Client Lib directly in your application. The proof-anchor helpers were reference code. | | `zeroj-patterns` | Use application-specific policies (see [zeroj-usecases](https://github.com/bloxbean/zeroj-usecases)). ZeroJ provides no generic authorization, replay or nullifier guarantee. | | `zeroj-bom-all` | Use `zeroj-bom-core` plus explicitly versioned opt-in modules. | | `zeroj-bls12381-wasm`, `zeroj-bbs-wasm` | No longer runtime products; they are unpublished assurance projects built with `-PincludeAssurance`. | The core modules (in the BOM) are now `zeroj-api`, `zeroj-codec`, `zeroj-backend-spi`, `zeroj-verifier-groth16`, `zeroj-bls12381`, `zeroj-blst`, `zeroj-crypto`, `zeroj-crypto-blst`, `zeroj-circuit-dsl`, `zeroj-circuit-lib`, `zeroj-circuit-annotation-api`, `zeroj-circuit-annotation-processor`, `zeroj-onchain-julc` and `zeroj-tools`. The opt-in modules, published with an explicit version, are `zeroj-verifier-plonk`, `zeroj-bbs`, `zeroj-mpf-poseidon` and `zeroj-jmt-poseidon`. See [Modules](https://zeroj.dev/reference/modules/). The default build of the repository is pure Java. It needs no Go, Rust, Node.js, WASM toolchain or RocksDB. ### Further reading Design notes: [ADR-0048](https://github.com/bloxbean/zeroj/blob/main/docs/adr/0048-org-zeroj-namespace-and-central-portal-publishing.md) and [ADR-0044](https://github.com/bloxbean/zeroj/blob/main/docs/adr/0044-focused-module-surface-and-optional-provider-isolation.md). Full migration texts: [namespace](https://github.com/bloxbean/zeroj/blob/main/docs/migration/0048-org-zeroj-namespace.md), [module cleanup](https://github.com/bloxbean/zeroj/blob/main/docs/migration/0044-module-cleanup.md). ### Next steps - [Modules](https://zeroj.dev/reference/modules/) - [Installation](https://zeroj.dev/start/installation/) --- ## Build with AI Source: https://zeroj.dev/ai/ > Give Claude Code, Cursor, Codex, Copilot or any chat assistant accurate, versioned ZeroJ context — llms.txt, Markdown twins, a starter pack and an API catalog. ZeroJ is young, so AI models have little or no training data about it. Without context, an assistant will happily invent classes, reach for BN254 or MiMC, skip range checks, or treat a dev-only trusted setup as production-ready. This site ships everything an agent needs to get ZeroJ right on the first try. ### TL;DR Point your assistant at the **AI Starter Pack** — or at `llms.txt`, and let it pull what it needs. | Artifact | Use it when | | --- | --- | | [`/ai/starter-pack.md`](https://zeroj.dev/ai/starter-pack.md) | **Start here.** One file with the rules, idioms, anti-patterns, error→fix table, canonical code and a generated circuit API catalog. Save it as `CLAUDE.md`, `AGENTS.md` or a Cursor rule. | | [`/llms.txt`](https://zeroj.dev/llms.txt) | A compact index of the whole site ([llmstxt.org](https://llmstxt.org/) convention) with the key facts an agent must not get wrong. | | [`/llms-full.txt`](https://zeroj.dev/llms-full.txt) | Every documentation page concatenated into one Markdown file, for tools that ingest a single URL. | | [`/ai/catalog.json`](https://zeroj.dev/ai/catalog.json) | Machine-readable catalog of the symbolic circuit API — annotations, `Zk*` types and gadget adapters — extracted from the Java sources at build time. | | [`/ai/manifest.json`](https://zeroj.dev/ai/manifest.json) | ZeroJ, JuLC and Cardano Client Lib versions, the source revision, and a SHA-256 for every exported file. | | `/ai/pages/.md` | A Markdown twin of every page. Use **View Markdown** or **Copy page for AI** under any page title. | All of these are regenerated on every build from the same sources as the HTML pages, so they never drift from what you read here. ### Set up your tool #### Claude Code Save the starter pack as your project's `CLAUDE.md` (Claude Code reads it at the start of every session): ```bash curl -o CLAUDE.md https://zeroj.dev/ai/starter-pack.md ``` Already have a `CLAUDE.md`? Keep the pack beside it and import it: ```bash curl -o docs/zeroj-starter-pack.md https://zeroj.dev/ai/starter-pack.md echo '@docs/zeroj-starter-pack.md' >> CLAUDE.md ``` #### Codex, Jules, Aider and other `AGENTS.md` tools ```bash curl -o AGENTS.md https://zeroj.dev/ai/starter-pack.md ``` #### Cursor ```bash mkdir -p .cursor/rules curl -o .cursor/rules/zeroj.mdc https://zeroj.dev/ai/starter-pack.md ``` #### GitHub Copilot ```bash mkdir -p .github curl -o .github/copilot-instructions.md https://zeroj.dev/ai/starter-pack.md ``` #### Continue Add the full docs as a URL context provider in your Continue config: ```json { "contextProviders": [ { "name": "url", "params": { "url": "https://zeroj.dev/llms-full.txt" } } ] } ``` #### ChatGPT, Claude.ai and other chat assistants Start the conversation with: ```text I'm building with ZeroJ, a Java zero-knowledge proof toolkit for Cardano. Before writing any code, read https://zeroj.dev/llms.txt and https://zeroj.dev/ai/starter-pack.md and follow them strictly: Groth16 on BLS12-381, annotation-style circuits (@ZKCircuit), Poseidon with PoseidonParamsBLS12_381T3.INSTANCE, dev-only trusted setup behind -Dzeroj.allowInsecureTrustedSetup=true, and no APIs that are not in the docs or the circuit API catalog. ``` If the assistant can't browse, paste the starter pack (or use **Copy page for AI** on the pages you need). > **Tip: Pin the version** > > The starter pack starts with a comment naming the ZeroJ version it was generated for. When you upgrade ZeroJ, refresh the file so your agent learns the new APIs. ### Prompts that work well Be specific about the statement, what's secret, what's public, and ask for tests that try to cheat: ```text Using ZeroJ, write a @ZKCircuit proving that a secret balance (64-bit) is at least a public threshold. Add JUnit 5 tests with one valid witness and at least two invalid ones (balance below threshold, balance that overflows 64 bits). Then prove with Groth16Keys and verify with Groth16BLS12381PureJavaVerifier. Use only APIs from the starter pack. ``` ```text Review this ZeroJ circuit for soundness: list every relation the application relies on and point to the constraint that enforces it. Flag any value that is computed but never constrained, any ZkUInt without @UInt(bits), and any Java control flow over secret values. ``` ```text I verify this proof on Cardano with ZeroJ's on-chain Groth16BLS12381Verifier validator (org.zeroj.onchain.julc.groth16.validator). Explain how a third party could replay it, and change the design so the proof is bound to the spent UTxO and each user can claim only once. ``` ### Review what the agent writes AI assistants make the same mistakes in ZK code that humans do — just faster. Before you trust generated code: - **Constraints, not just outputs.** A circuit that returns the right answer for honest inputs can still accept a cheater. Insist on invalid-witness tests. See [Test your circuits for soundness](https://zeroj.dev/guides/circuits/testing-circuits/). - **Ranges.** Every `ZkUInt` needs `@UInt(bits = N)`; field arithmetic wraps around. - **Curves and hashes.** BLS12-381 and Poseidon with explicit BLS12-381 parameters for anything headed to Cardano. - **Setup.** `-Dzeroj.allowInsecureTrustedSetup=true` belongs in tests and local demos only. Real keys come from a [ceremony](https://zeroj.dev/guides/proving/trusted-setup-ceremony/). - **On-chain policy.** A valid proof is not authorization. Check replay protection, nullifiers and `ScriptContext` binding against [Secure your ZK application](https://zeroj.dev/guides/verifying/application-security/). - **PlonK.** It is experimental in ZeroJ. If an agent picks PlonK, ask why — Groth16 is the supported path. ### What this site does not provide These are static context files. There is no hosted MCP server, no remote prover and no signing service. Your proofs, witnesses and keys never leave your machine unless your own code sends them somewhere. ### Help improve the context If your agent keeps getting a ZeroJ pattern wrong, [open an issue](https://github.com/bloxbean/zeroj/issues) with the prompt and the bad output. The fix usually belongs in the starter pack, so the next agent doesn't repeat it. --- ## AI Starter Pack Source: https://zeroj.dev/ai/starter-pack/ > Everything an AI coding agent needs to write correct ZeroJ code on the first try. Ingest it before generating circuits, proofs or validators. > **AI agents: read this whole document before generating ZeroJ code.** It condenses the rules, idioms, and failure modes of ZeroJ into one file. Humans looking for a tutorial should start at the [quickstart](https://zeroj.dev/start/quickstart/) instead. > > Save it as `CLAUDE.md`, `AGENTS.md`, `.cursor/rules/zeroj.mdc` or `.github/copilot-instructions.md`: > `curl -o CLAUDE.md https://zeroj.dev/ai/starter-pack.md`. The downloadable Markdown version also contains a circuit API catalog generated from the Java sources ([JSON](https://zeroj.dev/ai/catalog.json)). ### 1. What ZeroJ is ZeroJ is a **Java-first zero-knowledge proof toolkit for Cardano** (Java 25, Maven group `org.zeroj`, version `0.1.0-pre12`). - **Define** circuits in Java — annotation style (`@ZKCircuit`, recommended) or the `CircuitSpec`/`SignalBuilder` DSL. - **Prove** with a pure-Java **Groth16** prover on **BLS12-381** (no native libraries; optional blst backend). - **Verify** off-chain in any JVM (`zeroj-verifier-groth16`) and on-chain in Cardano **Plutus V3** validators compiled from Java by **JuLC** (`zeroj-onchain-julc`, JuLC `0.1.0-pre16`). - Also: BBS selective-disclosure credentials (`zeroj-bbs`), snarkjs/circom interop, Poseidon-rooted authenticated state (experimental). **Status — say this honestly in anything you write for users:** ZeroJ is experimental research software. It is not externally audited and must not protect real value on mainnet. "Beta" means feature-complete and correctness-tested, not audited. **Scope for the current release:** Groth16 on BLS12-381 is the supported path. **PlonK is experimental** (prover, verifier and validators): never choose it by default and never describe it as correct or production-ready. **BN254 is legacy and disabled by default** — never use it for Cardano. ### 2. Project setup ```groovy title="build.gradle" plugins { id 'java' id 'application' } java { toolchain { languageVersion = JavaLanguageVersion.of(25) } } repositories { mavenCentral() } dependencies { implementation platform('org.zeroj:zeroj-bom-core:0.1.0-pre12') annotationProcessor platform('org.zeroj:zeroj-bom-core:0.1.0-pre12') implementation 'org.zeroj:zeroj-circuit-annotation-api' annotationProcessor 'org.zeroj:zeroj-circuit-annotation-processor' implementation 'org.zeroj:zeroj-circuit-dsl' implementation 'org.zeroj:zeroj-circuit-lib' // Poseidon, Merkle, gadgets implementation 'org.zeroj:zeroj-crypto' // prover, setup, snarkjs JSON export implementation 'org.zeroj:zeroj-codec' // snarkjs JSON parsing implementation 'org.zeroj:zeroj-verifier-groth16' // pure-Java verifier // implementation 'org.zeroj:zeroj-onchain-julc' // Cardano validators + codecs } // Dev/test only: allows the single-party trusted setup used in examples. application { applicationDefaultJvmArgs = ['-Dzeroj.allowInsecureTrustedSetup=true'] } tasks.withType(Test).configureEach { systemProperty 'zeroj.allowInsecureTrustedSetup', 'true' } ``` - The BOM pins the core modules. Opt-in modules are outside the BOM and need an explicit version: `zeroj-verifier-plonk`, `zeroj-bbs`, `zeroj-mpf-poseidon`, `zeroj-jmt-poseidon`. - Releases up to `0.1.0-pre11` used group `com.bloxbean.cardano` and packages `com.bloxbean.cardano.zeroj.*`. From `0.1.0-pre12` both are `org.zeroj`. Do **not** rename other `com.bloxbean.cardano` dependencies (Cardano Client Lib, JuLC). - Details: [Installation](https://zeroj.dev/start/installation/). ### 3. The canonical flow (copy this shape) ```java title="SecretMultiplier.java" import org.zeroj.circuit.annotation.*; @ZKCircuit(name = "secret-multiplier", version = 1) public class SecretMultiplier { @Prove ZkBool prove(ZkContext zk, @Public ZkField a, @Public ZkField product, @Secret ZkField b) { return a.mul(b).isEqual(product); } } ``` ```java title="Main.java" import org.zeroj.api.CircuitId; import org.zeroj.api.CurveId; import org.zeroj.api.ProofSystemId; import org.zeroj.api.VerificationMaterial; import org.zeroj.codec.SnarkjsJsonCodec; import org.zeroj.crypto.groth16.Groth16Keys; import org.zeroj.crypto.groth16.Groth16ProofBLS381; import org.zeroj.crypto.setup.PowersOfTauBLS381; import org.zeroj.crypto.snarkjs.SnarkjsGroth16Json; import org.zeroj.verifier.groth16.bls12381.Groth16BLS12381PureJavaVerifier; import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.util.Arrays; public class Main { public static void main(String[] args) { // 1. Circuit + witness (generated companion: SecretMultiplierCircuit) var circuit = SecretMultiplierCircuit.build(); var r1cs = circuit.compileR1CS(CurveId.BLS12_381); var inputs = SecretMultiplierCircuit.inputs().a(3).product(33).b(11); BigInteger[] witness = inputs.calculateWitness(circuit, CurveId.BLS12_381); // 2. DEV-ONLY trusted setup (needs -Dzeroj.allowInsecureTrustedSetup=true) BigInteger tau = PowersOfTauBLS381.generate(4).tauScalar(); try (var keys = Groth16Keys.setupInMemory( r1cs.constraints(), r1cs.numWires(), r1cs.numPublicInputs(), tau)) { // 3. Prove (fresh randomness every call) Groth16ProofBLS381 proof = keys.prove(witness, r1cs.constraints()); // 4. Export snarkjs-compatible JSON and verify with the pure-Java verifier BigInteger[] pub = Arrays.copyOfRange(witness, 1, 1 + r1cs.numPublicInputs()); String vkJson = SnarkjsGroth16Json.verificationKeyJson(keys); String proofJson = SnarkjsGroth16Json.proofJson(proof); String publicJson = SnarkjsGroth16Json.publicJson(pub); CircuitId id = SecretMultiplierCircuit.circuitId(); var envelope = SnarkjsJsonCodec.toEnvelopeFromJson(proofJson, vkJson, publicJson, id); var material = VerificationMaterial.of(vkJson.getBytes(StandardCharsets.UTF_8), ProofSystemId.GROTH16, CurveId.BLS12_381, id); boolean valid = new Groth16BLS12381PureJavaVerifier().verify(envelope, material).proofValid(); System.out.println("valid = " + valid); } } } ``` Facts about this flow (it is the [quickstart](https://zeroj.dev/start/quickstart/) program, compiled and run against the published artifacts): - The witness is a `BigInteger[]`; `witness[0]` is the constant `1`, followed by the public inputs in schema order, then private wires. `Arrays.copyOfRange(witness, 1, 1 + numPublicInputs)` is exactly what the verifier needs; `inputs.publicValues()` gives the same values as a `List`. - For Groth16 only `tauScalar()` is used and the setup sizes its own domain from the constraint count, so `PowersOfTauBLS381.generate(4)` (the minimum; allowed range 4–32) is enough for a dev key. - `VerificationResult.proofValid()` is the cryptographic result. `accepted()` also requires policy validity and stays `false` when you call a verifier directly — check `proofValid()` and apply your own policy. - `Groth16Keys` is `AutoCloseable` — always use try-with-resources. - Proofs are freshly blinded on every `prove` call. There is **no** deterministic or unblinded prove API; do not try to create one. - Proof size: 192 bytes compressed (G1 48 + G2 96 + G1 48). ### 4. Writing circuits (annotation style) Rules that make or break soundness: 1. Proof code uses **symbolic types only**: `ZkField`, `ZkBool`, `ZkUInt`, `ZkArray`, `ZkBits`, `ZkBytes` (package `org.zeroj.circuit.annotation`). 2. `@Prove` returns `ZkBool` (or uses explicit assertion methods). Never return a Java `boolean`. 3. **Never use Java `if`, `?:`, `&&`, `||`, `!`, loops with data-dependent bounds, or `==` on symbolic values.** Circuits describe relations; they do not execute. Use `ZkBool.and(..)`, `or(..)`, `not()`, `select(..)`. 4. Every `ZkUInt` input carries `@UInt(bits = N)` — this emits the range constraints. Without it, field arithmetic wraps around modulo a 255-bit prime and "age − 18" can be huge instead of negative. 5. Every `ZkArray`, `ZkBits`, `ZkBytes` carries `@FixedSize(n)` or `@FixedSize(param = "name")`. Shapes are fixed at compile time. 6. Values that change the circuit's shape (tree depth, array length) are constructor parameters annotated `@CircuitParam("name")`. A different parameter value is a different circuit with different keys. 7. Inputs are either **fields** of the class (field style) or **parameters** of the `@Prove` method (parameter style). Static `@Prove` methods must use parameter style. No private `@Prove` methods, no private field inputs, no nested `@ZKCircuit` classes. 8. Add a `ZkContext zk` parameter when a gadget needs it (Poseidon, Merkle, Pedersen…). ```java @ZKCircuit(name = "range-proof", version = 1) public class RangeProof { @Secret @UInt(bits = 16) ZkUInt secret; @Public @UInt(bits = 16) ZkUInt lo; @Public @UInt(bits = 16) ZkUInt hi; @Prove ZkBool inRange() { return secret.gte(lo).and(secret.lte(hi)); } } ``` ```java @ZKCircuit(name = "merkle-bls12-381", nameTemplate = "merkle-bls-d{depth}") public class MerkleMembership { public MerkleMembership(@CircuitParam("depth") int depth) { } @Prove ZkBool prove(ZkContext zk, @Secret ZkField leaf, @Public ZkField root, @Secret @FixedSize(param = "depth") ZkArray siblings, @Secret @FixedSize(param = "depth") ZkArray pathBits) { return ZkMerkle.isMemberPoseidon(zk, PoseidonParamsBLS12_381T3.INSTANCE, leaf, root, siblings, pathBits); } } // MerkleMembershipCircuit.build(32); MerkleMembershipCircuit.inputs(32) ``` The annotation processor generates `Circuit` with: `build(...)`, `schema(...)`, `inputs(...)` (a typed builder with one method per input, plus `toWitnessMap()`, `publicValues()`, `toPublicInputs()`, `calculateWitness(circuit, curve)`), `calculateWitness(circuit, inputs, curve)`, `circuitId()`, `metadata()` and `proofEnvelopeBuilder(...)`. Parameterized circuits take their `@CircuitParam` values in `build(...)`, `inputs(...)`, `schema(...)`, `circuitId(...)` and `metadata(...)`. #### Hashing and Merkle trees for Cardano - Poseidon **with explicit BLS12-381 parameters**: `ZkPoseidon.hash(zk, PoseidonParamsBLS12_381T3.INSTANCE, left, right)`, `ZkPoseidonN.hash(zk, PoseidonParamsBLS12_381T3.INSTANCE, x, y, z)`. - Merkle: `ZkMerkle.isMemberPoseidon`, `verifyPoseidon`, `computeRootPoseidon` with the same params. - **Never** for Cardano: `ZkMiMC` (BN254-only), the no-params Poseidon overload, `ZkMerkle.HashType.MIMC` or `HashType.POSEIDON`. - Heavier gadgets exist for real-world cryptography: `ZkSha512`, `ZkHmacSha512`, `ZkBlake2b`, `ZkCip1852` (BIP32-Ed25519 / CIP-1852 derivation), `ZkPedersen`, `ZkJubjubPoint`, `ZkEdDSAJubjub`. Bind prover-supplied Jubjub points with `ZkJubjubPoint.witnessAffine(zk, u, v)`. See [Gadget library](https://zeroj.dev/guides/circuits/gadgets/). #### Lower-level DSL When annotations don't fit, implement `CircuitSpec` and use `SignalBuilder`/`Signal`: ```java public class SecretMultiplierSpec implements CircuitSpec { @Override public void define(SignalBuilder c) { Signal a = c.publicInput("a"); Signal b = c.privateInput("b"); Signal product = c.publicOutput("product"); c.assertEqual(a.mul(b), product); } public static CircuitBuilder build() { return CircuitBuilder.create("secret-multiplier") .publicVar("a").publicVar("product").secretVar("b") .defineSignals(new SecretMultiplierSpec()); } } // witness: circuit.calculateWitness(Map.of("a", List.of(BigInteger.valueOf(3)), ...), CurveId.BLS12_381) ``` ### 5. Testing circuits (always generate these) For every circuit, generate JUnit tests for: (1) a valid witness that proves and verifies; (2) **at least one invalid witness per constraint** — witness calculation throws (an `ArithmeticException` with `Constraint violation: …` for a failed equality, or an exception from the range decomposition) or the proof fails to verify; (3) boundary values for every `@UInt` range; (4) a **tampered public input** that must fail verification; (5) public-input order via `inputs.publicValues()`. A circuit that only passes honest tests may still be under-constrained. See [Test your circuits for soundness](https://zeroj.dev/guides/circuits/testing-circuits/). ### 6. Proving at scale and with real keys | Situation | API | | --- | --- | | Tests, small circuits | `Groth16Keys.setupInMemory(constraints, numWires, numPublic, tau)` (dev-only) | | Large circuits (100k+ constraints) | `Groth16Keys.setupToStore(flat, numWires, numPublic, tau, dir, true)` then `Groth16Keys.load(dir)`; `Groth16Pipeline` for compile caching | | Production keys | Run a multi-party snarkjs ceremony; import once with `ZkeyPkStoreImporter.importToPkStore(zkeyPath, keysDir)`; prove from `Groth16Keys.load(keysDir)` with `keys.prove(witness, ZkeyPkStoreImporter.snarkjsConstraints(r1cs.constraints(), numPublic))` (snarkjs appends `numPublic + 1` binding rows), or pass `numPublic + 1` binding rows to the packed `prove` / `Groth16Pipeline` | | Optional native speed-up | `zeroj-crypto-blst` + `ProverBackend` selection; bit-identical proofs | Setup validates the relation and fails closed: every public wire (and the constant wire 0) must be referenced by some constraint, otherwise `IllegalArgumentException: R1CS public wire N … is not referenced by any constraint`. Fix the circuit; do not catch and retry. Details: [Prove with Groth16](https://zeroj.dev/guides/proving/groth16/), [Performance](https://zeroj.dev/guides/proving/performance/), [Trusted setup ceremony](https://zeroj.dev/guides/proving/trusted-setup-ceremony/). ### 7. Verifying on Cardano ```java import com.bloxbean.cardano.julc.clientlib.JulcScriptLoader; import org.zeroj.onchain.julc.groth16.codec.ProverToCardano; import org.zeroj.onchain.julc.groth16.validator.Groth16BLS12381Verifier; var vk = ProverToCardano.compressVk(keys); // alpha, beta, gamma, delta, ic var ic = ListPlutusData.of(); vk.ic().forEach(p -> ic.add(new BytesPlutusData(p))); var script = JulcScriptLoader.load(Groth16BLS12381Verifier.class, new BytesPlutusData(vk.alpha()), new BytesPlutusData(vk.beta()), new BytesPlutusData(vk.gamma()), new BytesPlutusData(vk.delta()), ic); var p = ProverToCardano.compressProof(proof); // piA (48 B), piB (96 B), piC (48 B) // datum = ListPlutusData of BigIntPlutusData public inputs, in order // redeemer = ConstrPlutusData(0, [piA, piB, piC]) as BytesPlutusData ``` - The VK is baked into the script as parameters (so the script hash commits to the VK); public inputs go in the datum; the proof goes in the redeemer. - `Groth16BLS12381Verifier` is **crypto-only**: anyone who sees a proof can replay it. For anything beyond a demo, write your own validator that composes `Groth16BLS12381Lib.verify(...)` and enforces `ScriptContext` policy: bind a public input to the spend (e.g. `spendRef = blake2b_256(txId ‖ outputIndex as 32 bytes) mod r`, **computed by the validator from `ScriptContext`** and prepended to the application's public inputs), bind the recipient, track nullifiers, and check authorization. - Do **not** lock funds with `Groth16BLS12381TxOutRefBindingVerifier`: it reads `spendRef` from the datum of the UTxO it guards, and a datum can't contain a hash of its own transaction, so no real UTxO can be spent through it. It is a reference for the check only. - Upgrading JuLC changes the compiled script bytes and therefore the script hash/address. - Step by step: [Verify your proof on Cardano](https://zeroj.dev/tutorials/verify-on-cardano/). Reference: [Verify proofs on Cardano](https://zeroj.dev/guides/verifying/on-chain/). ### 8. Anti-patterns → do this instead | ❌ Don't | ✅ Do | | --- | --- | | `if (age.gte(t)) …` or `a && b` on symbolic values | `age.gte(t)`, `x.and(y)`, `cond.select(...)` | | `ZkUInt` without `@UInt(bits = N)` | Always declare the bit width | | `ZkMiMC`, no-params Poseidon, BN254, `-Dzeroj.allowLegacyBn254=true` for Cardano | Poseidon with `PoseidonParamsBLS12_381T3.INSTANCE`, BLS12-381 | | Recommending PlonK because it has a universal setup | Groth16; mention PlonK only as experimental | | `PowersOfTauBLS381.generate` / `setupInMemory` in production | Multi-party ceremony `.zkey` imported with `ZkeyPkStoreImporter` | | Setting `zeroj.allowInsecureTrustedSetup` in production config | Only in tests, local demos and CI | | Inventing a seeded/deterministic prover for reproducibility | Store the proof you produced | | Bare `Groth16BLS12381Verifier` (or the datum-based `Groth16BLS12381TxOutRefBindingVerifier`) guarding funds | A custom validator on `Groth16BLS12381Lib` that computes the spend binding from `ScriptContext`, binds the recipient, and enforces policy | | Trusting a prover-supplied fact ("my age is 27") | Have an issuer sign or commit to it and verify that inside the circuit or with BBS | | Testing only the happy path | Invalid-witness and tampered-public-input tests for every constraint | | Claiming "secure", "audited", "production-ready" | "Experimental, not externally audited" | ### 9. Errors and fixes | Error | Cause → fix | | --- | --- | | `IllegalStateException: Single-party trusted setup is disabled by default…` | Dev setup without opt-in → add `-Dzeroj.allowInsecureTrustedSetup=true` (tests/demos only) or use ceremony keys. | | `IllegalStateException: BN254 is disabled by default…` | Legacy curve used → switch to `CurveId.BLS12_381`. | | `ArithmeticException: Constraint violation: …` during witness calculation | The inputs don't satisfy the circuit (expected for invalid-witness tests) or the inputs are wrong. | | `IllegalArgumentException: Missing public input: x` / `Missing secret input: x` | Witness map is missing a named input → use the generated `inputs()` builder. | | `IllegalArgumentException: R1CS public wire N … is not referenced by any constraint` | A public input is never constrained → constrain it or remove it from the public inputs. | | `IllegalArgumentException: Power must be in [4, 32]…` | Pick a `PowersOfTauBLS381.generate` power in range (4 is enough for a Groth16 dev key). | | `verify(...).proofValid()` is `false` | Wrong public-input order or values, a different VK, or a tampered proof. Take public inputs from `witness[1..numPublicInputs]`. | | `accepted()` is `false` although the proof is valid | Expected when calling a verifier directly: `accepted()` also needs policy validity. Check `proofValid()`. | | `OutOfMemoryError` on large circuits | Use `setupToStore` + `Groth16Keys.load` (mmap'd keys) and `Groth16Pipeline`. | | On-chain script fails / budget exceeded | Check datum = public inputs in order, redeemer = `Constr 0 [piA, piB, piC]`, VK params match; see the on-chain guide for budgets. | ### 10. Security checklist for generated code Before handing code to a user, confirm: - [ ] Every relation the application depends on is constrained; no computed value is left unconstrained. - [ ] Every integer input is range-checked; booleans are constrained booleans (`ZkBool`). - [ ] Public vs secret assignment matches the privacy goal; public-input order is taken from the generated schema. - [ ] Hashes are Poseidon with BLS12-381 params; the curve is BLS12-381; the proof system is Groth16. - [ ] Dev trusted setup is confined to tests/demos and labelled as such. - [ ] On-chain: proof bound to the spend (a `spendRef` computed from `ScriptContext`, not read from the datum) and to the recipient, nullifiers or state prevent double use, `ScriptContext` policy enforced. - [ ] Inputs that come from outside (proof JSON, VK files) are parsed with ZeroJ codecs, which validate encodings; VKs are pinned by hash or ID. - [ ] Invalid-witness and tamper tests exist. - [ ] The text you give the user says ZeroJ is experimental and not externally audited. ### 11. Circuit API catalog The downloadable version of this pack ([`/ai/starter-pack.md`](https://zeroj.dev/ai/starter-pack.md)) contains the full, generated list of annotations, symbolic types and gadget adapters with their exact public signatures, extracted from the Java sources at build time. It's also available as [JSON](https://zeroj.dev/ai/catalog.json). If a method is not in the catalog or in these docs, assume it does not exist. *Generated from the Java sources at build time — the same data as [/ai/catalog.json](https://zeroj.dev/ai/catalog.json).* #### Annotations (`org.zeroj.circuit.annotation`, module `org.zeroj:zeroj-circuit-annotation-api`) ##### ZKCircuit *Marks a Java class as a ZeroJ circuit source for annotation processing.* - `String name() default ""` - `String nameTemplate() default ""` - `int version() default 1` ##### Prove *Marks the method that defines circuit constraints.* _(marker annotation — no elements)_ ##### Public *Marks a symbolic value as a public circuit input.* - `String name() default ""` ##### Secret *Marks a symbolic value as a secret circuit input.* - `String name() default ""` ##### UInt *Declares the bit width for an unsigned symbolic integer.* - `int bits()` ##### FixedSize *Declares a fixed size for symbolic arrays and byte-like values.* - `int value() default -1` - `String param() default ""` - `int inner() default -1` - `String innerParam() default ""` ##### CircuitParam *Marks a build-time value that changes circuit shape.* - `String value() default ""` ##### FieldElement *Explicit marker for a raw field-element symbolic value.* _(marker annotation — no elements)_ ##### Order *Explicit field ordering override for field-style annotated circuits.* - `int value()` #### Symbolic types (`org.zeroj.circuit.annotation`, module `org.zeroj:zeroj-circuit-annotation-api`) ##### ZkContext *Minimal context wrapper around SignalBuilder for symbolic values and future gadget adapters.* - `SignalBuilder builder()` - `void requireSignal(Signal signal)` - `ZkField constant(long value)` - `ZkField constant(BigInteger value)` - `ZkField field(Signal signal)` ##### ZkField *Symbolic raw field element backed by one Signal.* - `static ZkField publicInput(SignalBuilder builder, String name)` - `static ZkField secret(SignalBuilder builder, String name)` - `static ZkField wrap(ZkContext context, Signal signal)` - `static ZkField wrap(SignalBuilder builder, Signal signal)` - `ZkField add(ZkField other)` - `ZkField sub(ZkField other)` - `ZkField mul(ZkField other)` - `ZkField div(ZkField other)` - `ZkBool isEqual(ZkField other)` - `void assertEqual(ZkField other)` - `Signal signal()` - `List signals()` - `void assertWellFormed()` ##### ZkBool *Symbolic boolean backed by one constrained bit signal.* - `static ZkBool publicInput(SignalBuilder builder, String name)` - `static ZkBool secret(SignalBuilder builder, String name)` - `static ZkBool wrap(ZkContext context, Signal signal)` - `static ZkBool wrap(SignalBuilder builder, Signal signal)` - `ZkBool and(ZkBool other)` - `ZkBool or(ZkBool other)` - `ZkBool xor(ZkBool other)` - `ZkBool not()` - `ZkField select(ZkField ifTrue, ZkField ifFalse)` - `ZkBool select(ZkBool ifTrue, ZkBool ifFalse)` - `ZkUInt select(ZkUInt ifTrue, ZkUInt ifFalse)` - `ZkBool isEqual(ZkBool other)` - `void assertTrue()` - `void assertFalse()` - `void assertEqual(ZkBool other)` - `ZkField asField()` - `Signal signal()` - `List signals()` - `void assertWellFormed()` ##### ZkUInt *Symbolic unsigned integer backed by one field element and an explicit bit width.* > Declare inputs with @UInt(bits = N); construction adds range constraints. - `static ZkUInt publicInput(SignalBuilder builder, String name, int bits)` - `static ZkUInt secret(SignalBuilder builder, String name, int bits)` - `static ZkUInt wrap(ZkContext context, Signal signal, int bits)` - `static ZkUInt wrap(SignalBuilder builder, Signal signal, int bits)` - `int bits()` - `ZkUInt add(ZkUInt other)` - `ZkUInt sub(ZkUInt other)` - `ZkUInt mul(ZkUInt other)` - `ZkBool lt(ZkUInt other)` - `ZkBool lte(ZkUInt other)` - `ZkBool gt(ZkUInt other)` - `ZkBool gte(ZkUInt other)` - `ZkBool isEqual(ZkUInt other)` - `ZkBool inRange(ZkUInt lo, ZkUInt hi)` - `void assertInRange()` - `void assertEqual(ZkUInt other)` - `ZkField asField()` - `Signal signal()` - `List signals()` - `void assertWellFormed()` - `BitDecomposition decomposition()` — This value's binary decomposition at its declared width, proving value Minted on first use and cached, so the range constraints are emitted once no matter how many gadgets ask. ##### ZkArray *Fixed-size symbolic array.* - `static ZkArray publicFields(SignalBuilder builder, String baseName, int size)` - `static ZkArray secretFields(SignalBuilder builder, String baseName, int size)` - `static ZkArray publicBools(SignalBuilder builder, String baseName, int size)` - `static ZkArray secretBools(SignalBuilder builder, String baseName, int size)` - `static ZkArray publicUInts(SignalBuilder builder, String baseName, int size, int bits)` - `static ZkArray secretUInts(SignalBuilder builder, String baseName, int size, int bits)` - `static ZkArray> publicFieldMatrix(SignalBuilder builder, String baseName, int outerSize, int innerSize)` - `static ZkArray> secretFieldMatrix(SignalBuilder builder, String baseName, int outerSize, int innerSize)` - `static ZkArray> publicBoolMatrix(SignalBuilder builder, String baseName, int outerSize, int innerSize)` - `static ZkArray> secretBoolMatrix(SignalBuilder builder, String baseName, int outerSize, int innerSize)` - `static ZkArray> publicUIntMatrix(SignalBuilder builder, String baseName, int outerSize, int innerSize, int bits)` - `static ZkArray> secretUIntMatrix(SignalBuilder builder, String baseName, int outerSize, int innerSize, int bits)` - `static ZkArray bind(SignalBuilder builder, String baseName, int size, ElementFactory factory)` — Bind a custom fixed-size array. - `int size()` - `T get(int index)` - `List values()` - `List signals()` - `void assertWellFormed()` ##### ZkBits *Fixed-size symbolic bit vector backed by constrained ZkBool values.* - `static ZkBits publicInput(SignalBuilder builder, String baseName, int size)` - `static ZkBits secret(SignalBuilder builder, String baseName, int size)` - `int size()` - `ZkBool get(int index)` - `List values()` - `ZkBool isEqual(ZkBits other)` - `void assertEqual(ZkBits other)` - `List signals()` - `void assertWellFormed()` ##### ZkBytes *Fixed-size symbolic byte vector backed by 8-bit ZkUInt values.* - `static ZkBytes publicInput(SignalBuilder builder, String baseName, int size)` - `static ZkBytes secret(SignalBuilder builder, String baseName, int size)` - `int size()` - `ZkUInt get(int index)` - `List values()` - `ZkBool isEqual(ZkBytes other)` - `void assertEqual(ZkBytes other)` - `List signals()` - `void assertWellFormed()` #### Gadget adapters (`org.zeroj.circuit.lib.zk`, module `org.zeroj:zeroj-circuit-lib`) ##### ZkPoseidon *Symbolic Poseidon adapter for annotation-based circuits.* > For Cardano pass PoseidonParamsBLS12_381T3.INSTANCE explicitly. - `static ZkField hash(ZkContext zk, PoseidonParams params, ZkField left, ZkField right)` - `static ZkField hash(ZkContext zk, ZkField left, ZkField right)` ##### ZkPoseidonN *Symbolic variable-arity Poseidon adapter for annotation-based circuits.* > For Cardano pass PoseidonParamsBLS12_381T3.INSTANCE explicitly. - `static ZkField hash(ZkContext zk, PoseidonParams params, ZkField... inputs)` — Hash one or more symbolic field elements using folded two-input Poseidon under the supplied parameters. ##### ZkMerkle *Symbolic fixed-depth Merkle helpers for annotation-based circuits.* > For Cardano use the *Poseidon methods with PoseidonParamsBLS12_381T3.INSTANCE; HashType.MIMC and no-params POSEIDON are BN254/off-chain paths. - `static ZkField computeRoot(ZkContext zk, ZkField leaf, ZkArray siblings, ZkArray pathBits, HashType hashType)` - `static ZkField computeRoot(ZkContext zk, ZkField leaf, ZkArray siblings, ZkArray pathBits, HashFn hashFn)` - `static ZkField computeRootPoseidon(ZkContext zk, PoseidonParams params, ZkField leaf, ZkArray siblings, ZkArray pathBits)` - `static void verify(ZkContext zk, ZkField leaf, ZkField root, ZkArray siblings, ZkArray pathBits, HashType hashType)` - `static void verifyProof(ZkContext zk, ZkField leaf, ZkField root, ZkArray siblings, ZkArray pathBits, HashType hashType)` - `static void verifyPoseidon(ZkContext zk, PoseidonParams params, ZkField leaf, ZkField root, ZkArray siblings, ZkArray pathBits)` - `static void verifyProofPoseidon(ZkContext zk, PoseidonParams params, ZkField leaf, ZkField root, ZkArray siblings, ZkArray pathBits)` - `static void verify(ZkContext zk, ZkField leaf, ZkField root, ZkArray siblings, ZkArray pathBits, HashFn hashFn)` - `static void verifyProof(ZkContext zk, ZkField leaf, ZkField root, ZkArray siblings, ZkArray pathBits, HashFn hashFn)` - `static ZkBool isMember(ZkContext zk, ZkField leaf, ZkField root, ZkArray siblings, ZkArray pathBits, HashType hashType)` - `static ZkBool isMemberPoseidon(ZkContext zk, PoseidonParams params, ZkField leaf, ZkField root, ZkArray siblings, ZkArray pathBits)` - `static ZkBool isMember(ZkContext zk, ZkField leaf, ZkField root, ZkArray siblings, ZkArray pathBits, HashFn hashFn)` ##### ZkSha512 *Symbolic SHA-512 adapter for annotation-based (@ZKCircuit) circuits.* - `static ZkBytes hash(ZkContext zk, ZkBytes message)` — SHA-512 of message → 64-byte digest. ##### ZkHmacSha512 *Symbolic HMAC-SHA512 adapter for annotation-based (@ZKCircuit) circuits.* - `static ZkBytes hmac(ZkContext zk, ZkBytes key, ZkBytes message)` — HMAC-SHA512 of message under key → 64-byte MAC. ##### ZkBlake2b *Symbolic BLAKE2b adapter for annotation-based (@ZKCircuit) circuits.* - `static ZkBytes hash224(ZkContext zk, ZkBytes message)` — blake2b-224 of message → 28-byte digest (Cardano key hash). - `static ZkBytes hash256(ZkContext zk, ZkBytes message)` — blake2b-256 of message → 32-byte digest. - `static ZkBytes hash(ZkContext zk, ZkBytes message, int outLenBytes)` — blake2b with an explicit output length in [1,64] bytes. ##### ZkCip1852 *Symbolic CIP-1852 / BIP32-Ed25519 derivation adapter for annotation-based (@ZKCircuit) circuits.* - `static ZkBytes paymentKeyHash(ZkContext zk, ZkBytes rootKL, ZkBytes rootKR, ZkBytes rootChainCode, long account, long role, long index)` — Payment key hash of m/1852'/1815'/account'/role/index derived from the root extended key. - `static ZkBytes paymentKeyHash(ZkContext zk, ZkBytes rootKL, ZkBytes rootKR, ZkBytes rootChainCode, long account, ZkBytes role, ZkBytes index)` — #paymentKeyHash(ZkContext, ZkBytes, ZkBytes, ZkBytes, long, long, long) with the two soft path components as circuit inputs: role and index are 4-byte little-endian ZkBytes (typically @Secret — the public pkh already binds the statement, so - `static ZkBytes paymentKeyHash(ZkContext zk, ZkBytes rootKL, ZkBytes rootKR, ZkBytes rootChainCode, ZkBytes account, ZkBytes role, ZkBytes index)` — Fully path-parameterised variant: account, role and index are all circuit inputs (4-byte little-endian ZkBytes, values < 2^31; the account is the plain number — hardening is applied in-circuit). - `static ZkBytes leafKeyHash(ZkContext zk, ZkBytes leafKL)` — Payment key hash of a leaf key: blake2b224(encode(kL·B)). ##### ZkPedersen *Symbolic Pedersen commitment adapter for annotation-based circuits.* - `static ZkJubjubPoint commit(ZkContext zk, ZkUInt value, ZkUInt blinding)` - `static ZkJubjubPoint commit(ZkContext zk, ZkUInt value, ZkUInt blinding, int scalarBits)` — Commits to value with blinding. - `static ZkJubjubPoint commitBits(ZkContext zk, ZkBits valueBits, ZkBits blindingBits)` — Commits using LSB-first scalar bit vectors. - `static void verifyOpening(ZkContext zk, ZkJubjubPoint commitment, ZkUInt value, ZkUInt blinding, int scalarBits)` - `static void verifyOpening(ZkContext zk, ZkJubjubPoint commitment, ZkUInt value, ZkUInt blinding)` ##### ZkJubjubPoint *Symbolic Jubjub point backed by extended-coordinate field values.* - `static ZkJubjubPoint witnessAffine(ZkContext zk, ZkField u, ZkField v)` — Binds a prover-supplied point given by its affine coordinates, emitting every constraint needed to make it a usable curve point: the affine curve equation v² − u² == 1 + d·u²·v², z = 1 (so z != 0 holds by construction and the representation - `static ZkJubjubPoint fromTrustedAffine(ZkContext zk, ZkField u, ZkField v)` — thing as an affine point a circuit may trust, because the caller never sees the prover's witness. - `static ZkJubjubPoint constant(ZkContext zk, JubjubPoint point)` — Wraps a compile-time point as circuit constants. - `ZkField u()` - `ZkField v()` - `ZkField z()` - `ZkField t()` - `ZkJubjubPoint add(ZkContext zk, ZkJubjubPoint other)` - `ZkJubjubPoint doubled(ZkContext zk)` - `static ZkJubjubPoint select(ZkContext zk, ZkBool condition, ZkJubjubPoint ifTrue, ZkJubjubPoint ifFalse)` - `void assertEqual(ZkContext zk, ZkJubjubPoint other)` - `ZkBool isEqual(ZkContext zk, ZkJubjubPoint other)` - `ZkBool isIdentity(ZkContext zk)` - `void assertNotIdentity(ZkContext zk)` - `void assertAffineEquals(ZkContext zk, ZkField affineU, ZkField affineV)` - `List signals()` - `void assertWellFormed()` — Asserts that this point is a well-formed projective curve point: V² − U² == Z² + d·T², T·Z == U·V, and Z != 0. ##### ZkEdDSAJubjub *Symbolic EdDSA-Jubjub verification adapter for annotation-based circuits.* - `static void verifyStrict(ZkContext zk, ZkField publicKeyU, ZkField publicKeyV, ZkField message, ZkField rU, ZkField rV, ZkUInt s, ZkUInt kModL, ZkUInt kQuotient)` — Verifies with an in-circuit prime-order subgroup check on pk. - `static void verifyWithRegisteredKey(ZkContext zk, ZkField publicKeyU, ZkField publicKeyV, ZkField message, ZkField rU, ZkField rV, ZkUInt s, ZkUInt kModL, ZkUInt kQuotient)` — Verifies where pk is a public input or circuit constant. - `static KReduction witnessComputeKReduction(JubjubPoint rPoint, JubjubPoint publicKey, BigInteger message)` — Computes the (kModL, kQuotient) witnesses the verification relation requires. ##### ZkMiMC *Symbolic MiMC adapter for annotation-based circuits.* > BN254-only. Do not use for Cardano (BLS12-381) circuits. - `static ZkField hash(ZkContext zk, ZkField left, ZkField right)` ### 12. Where to look next - Concepts: [Zero-knowledge in plain English](https://zeroj.dev/learn/zero-knowledge-basics/), [Circuits, constraints & witnesses](https://zeroj.dev/learn/circuits-and-witnesses/) - Guides: [Annotations](https://zeroj.dev/guides/circuits/annotations/), [Gadgets](https://zeroj.dev/guides/circuits/gadgets/), [Groth16](https://zeroj.dev/guides/proving/groth16/), [Verify in Java](https://zeroj.dev/guides/verifying/off-chain/), [Secure your ZK application](https://zeroj.dev/guides/verifying/application-security/) - Reference: [API cheat sheet](https://zeroj.dev/reference/api-cheatsheet/), [Configuration](https://zeroj.dev/reference/configuration/), [FAQ](https://zeroj.dev/reference/faq/) - Runnable end-to-end demos: https://github.com/bloxbean/zeroj-usecases - Source: https://github.com/bloxbean/zeroj