AI Starter Pack
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 instead.
Save it as
CLAUDE.md,AGENTS.md,.cursor/rules/zeroj.mdcor.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).
1. What ZeroJ is
Section titled “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 theCircuitSpec/SignalBuilderDSL. - 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, JuLC0.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
Section titled “2. Project setup”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-pre11used groupcom.bloxbean.cardanoand packagescom.bloxbean.cardano.zeroj.*. From0.1.0-pre12both areorg.zeroj. Do not rename othercom.bloxbean.cardanodependencies (Cardano Client Lib, JuLC). - Details: Installation.
3. The canonical flow (copy this shape)
Section titled “3. The canonical flow (copy this shape)”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); }}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 program, compiled and run against the published artifacts):
- The witness is a
BigInteger[];witness[0]is the constant1, 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 aList<BigInteger>. - For Groth16 only
tauScalar()is used and the setup sizes its own domain from the constraint count, soPowersOfTauBLS381.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 staysfalsewhen you call a verifier directly — checkproofValid()and apply your own policy.Groth16KeysisAutoCloseable— always use try-with-resources.- Proofs are freshly blinded on every
provecall. 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)
Section titled “4. Writing circuits (annotation style)”Rules that make or break soundness:
- Proof code uses symbolic types only:
ZkField,ZkBool,ZkUInt,ZkArray<T>,ZkBits,ZkBytes(packageorg.zeroj.circuit.annotation). @ProvereturnsZkBool(or uses explicit assertion methods). Never return a Javaboolean.- Never use Java
if,?:,&&,||,!, loops with data-dependent bounds, or==on symbolic values. Circuits describe relations; they do not execute. UseZkBool.and(..),or(..),not(),select(..). - Every
ZkUIntinput 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. - Every
ZkArray,ZkBits,ZkBytescarries@FixedSize(n)or@FixedSize(param = "name"). Shapes are fixed at compile time. - 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. - Inputs are either fields of the class (field style) or parameters of the
@Provemethod (parameter style). Static@Provemethods must use parameter style. No private@Provemethods, no private field inputs, no nested@ZKCircuitclasses. - Add a
ZkContext zkparameter when a gadget needs it (Poseidon, Merkle, Pedersen…).
@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)); }}@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<ZkField> siblings, @Secret @FixedSize(param = "depth") ZkArray<ZkBool> pathBits) { return ZkMerkle.isMemberPoseidon(zk, PoseidonParamsBLS12_381T3.INSTANCE, leaf, root, siblings, pathBits); }}// MerkleMembershipCircuit.build(32); MerkleMembershipCircuit.inputs(32)The annotation processor generates <ClassName>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
Section titled “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,computeRootPoseidonwith the same params. - Never for Cardano:
ZkMiMC(BN254-only), the no-params Poseidon overload,ZkMerkle.HashType.MIMCorHashType.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 withZkJubjubPoint.witnessAffine(zk, u, v). See Gadget library.
Lower-level DSL
Section titled “Lower-level DSL”When annotations don’t fit, implement CircuitSpec and use SignalBuilder/Signal:
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)
Section titled “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.
6. Proving at scale and with real keys
Section titled “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, Performance, Trusted setup ceremony.
7. Verifying on Cardano
Section titled “7. Verifying on Cardano”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, icvar 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.
Groth16BLS12381Verifieris crypto-only: anyone who sees a proof can replay it. For anything beyond a demo, write your own validator that composesGroth16BLS12381Lib.verify(...)and enforcesScriptContextpolicy: bind a public input to the spend (e.g.spendRef = blake2b_256(txId ‖ outputIndex as 32 bytes) mod r, computed by the validator fromScriptContextand prepended to the application’s public inputs), bind the recipient, track nullifiers, and check authorization.- Do not lock funds with
Groth16BLS12381TxOutRefBindingVerifier: it readsspendReffrom 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. Reference: Verify proofs on Cardano.
8. Anti-patterns → do this instead
Section titled “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
Section titled “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
Section titled “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
spendRefcomputed fromScriptContext, not read from the datum) and to the recipient, nullifiers or state prevent double use,ScriptContextpolicy 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
Section titled “11. Circuit API catalog”The downloadable version of this pack (/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. If a method is not in the catalog or in these docs, assume it does not exist.
12. Where to look next
Section titled “12. Where to look next”- Concepts: Zero-knowledge in plain English, Circuits, constraints & witnesses
- Guides: Annotations, Gadgets, Groth16, Verify in Java, Secure your ZK application
- Reference: API cheat sheet, Configuration, FAQ
- Runnable end-to-end demos: https://github.com/bloxbean/zeroj-usecases
- Source: https://github.com/bloxbean/zeroj