- Prerequisites
- Step 1: Define the Circuit (Java DSL)
- Step 2: Compile and Calculate Witness (Pure Java)
- Step 3: Generate Proof (Pure Java)
- Step 4: Verify Off-Chain (Pure Java)
- Step 5: Deploy On-Chain Verifier (Julc / Plutus V3)
- Step 6: Lock Funds with Public Inputs as Datum
- Step 7: Unlock with ZK Proof as Redeemer
- What Happens On-Chain
- End-to-End Flow Summary
- Running the Examples
- Prover Options
- Curves and On-Chain Feasibility
This guide walks through the complete ZeroJ flow: define a ZK circuit in Java, generate a proof, verify it off-chain, and execute on-chain verification on Cardano via Yaci DevKit.
We'll build a private multiplier circuit where the prover shows they know a secret factor b such that a * b = c, while a and c remain public. This small circuit matches the reusable Groth16 on-chain verifier, which currently accepts two public inputs.
| Tool | Version | Purpose |
|---|---|---|
| Java | 25+ (GraalVM) | sdk use java 25.0.2-graal |
| Yaci DevKit | latest | Local Cardano devnet (only for on-chain steps) |
The gnark native library is only needed if you want to generate proofs using the optional in-process gnark FFM prover. The main flow in this guide uses the pure Java prover.
If you want to use the gnark prover, you need Go 1.21+ to build the native library once:
cd zeroj-prover-gnark/gnark-wrapper && make buildStart Yaci DevKit (only for on-chain Steps 5-7):
yaci-cli:default> create-node -o --startThe circuit proves: "I know a secret b such that a * b == c."
// PrivateMultiplierCircuit.java
public class PrivateMultiplierCircuit implements CircuitSpec {
@Override
public void define(SignalBuilder c) {
Signal a = c.publicInput("a");
Signal b = c.privateInput("b");
Signal cOut = c.publicOutput("c");
c.assertEqual(a.mul(b), cOut);
}
public static CircuitBuilder build() {
return CircuitBuilder.create("private-multiplier")
.publicVar("a")
.publicVar("c")
.secretVar("b")
.defineSignals(new PrivateMultiplierCircuit());
}
}Key points:
secretVar-- only the prover knows this value (b)publicVar-- visible to the verifier (aandc)CircuitSpeckeeps the circuit reusable and testable
var circuit = PrivateMultiplierCircuit.build();
// Compile circuit to R1CS (Groth16 constraint system)
var r1cs = circuit.compileR1CS(CurveId.BLS12_381);
// Calculate witness with concrete inputs
BigInteger a = BigInteger.valueOf(3);
BigInteger b = BigInteger.valueOf(11);
BigInteger c = BigInteger.valueOf(33);
var witness = circuit.calculateWitness(Map.of(
"a", List.of(a),
"b", List.of(b),
"c", List.of(c)
), CurveId.BLS12_381);This is all pure Java -- no external tools needed.
var constraints = r1cs.constraints();
// Local test setup only: toxic waste is known.
var srs = PowersOfTauBLS381.generate(4);
var setupResult = Groth16SetupBLS381.setup(
constraints, r1cs.numWires(), r1cs.numPublicInputs(), srs.tauScalar());
var proof = Groth16ProverBLS381.prove(
setupResult.provingKey(), witness, constraints, r1cs.numWires());For setup beyond local tests, import MPC-generated .zkey artifacts instead of using PowersOfTauBLS381.generate(...).
BigInteger[] publicInputs = new BigInteger[r1cs.numPublicInputs()];
for (int i = 0; i < publicInputs.length; i++) {
publicInputs[i] = witness[i + 1];
}
boolean ok = verifyGroth16Pairing(proof, setupResult, publicInputs);
assert ok;The complete helper is shown in PureJavaProverYaciE2ETest; it computes the Groth16 vk_x linear combination and runs the BLS12-381 pairing equation.
ZeroJ includes reusable Plutus V3 validators compiled from Java via Julc. The VK is baked into the script at deploy time:
import com.bloxbean.cardano.zeroj.onchain.julc.groth16.codec.ProverToCardano;
import com.bloxbean.cardano.zeroj.onchain.julc.groth16.validator.Groth16BLS12381Verifier;
// Compress proof + VK for on-chain BLS format
var compressedVk = ProverToCardano.compressVk(setupResult);
var compressedProof = ProverToCardano.compressProof(proof);
var vkIcData = ListPlutusData.of();
for (byte[] ic : compressedVk.ic()) {
vkIcData.add(new BytesPlutusData(ic));
}
// Compile Julc validator with VK parameters
var script = JulcScriptLoader.load(Groth16BLS12381Verifier.class,
new BytesPlutusData(compressedVk.alpha()),
new BytesPlutusData(compressedVk.beta()),
new BytesPlutusData(compressedVk.gamma()),
new BytesPlutusData(compressedVk.delta()),
vkIcData
);
var scriptAddr = AddressProvider.getEntAddress(script, Networks.testnet()).toBech32();// Datum = public inputs that the on-chain verifier checks: [a, c]
var datum = ListPlutusData.of(
BigIntPlutusData.of(a),
BigIntPlutusData.of(c)
);
// Lock 5 ADA at the script address
var lockTx = new Tx()
.payToContract(scriptAddr, Amount.ada(5), datum)
.from(sender.baseAddress());
var lockResult = new QuickTxBuilder(backend)
.compose(lockTx)
.withSigner(SignerProviders.signerFrom(sender))
.complete();// Redeemer = the Groth16 proof (piA, piB, piC compressed BLS points)
var redeemer = ConstrPlutusData.builder()
.alternative(0)
.data(ListPlutusData.of(
new BytesPlutusData(compressedProof.piA()),
new BytesPlutusData(compressedProof.piB()),
new BytesPlutusData(compressedProof.piC())
))
.build();
// Spend the script UTXO -- triggers on-chain ZK verification
var unlockTx = new ScriptTx()
.collectFrom(scriptUtxo, redeemer)
.payToAddress(sender.baseAddress(), Amount.ada(4.5))
.attachSpendingValidator(script);
var unlockResult = new QuickTxBuilder(backend)
.compose(unlockTx)
.withSigner(SignerProviders.signerFrom(sender))
.feePayer(sender.baseAddress())
.collateralPayer(sender.baseAddress())
.complete();
// Transaction succeeds = proof verified on-chain by Plutus V3!The Groth16BLS12381Verifier Plutus V3 script executes:
- Extract public inputs from datum:
[a, c] - Decompress proof points (piA, piB, piC) from BLS12-381 compressed bytes
- Compute
vk_x = ic[0] + pub[0] * ic[1] + ... + pub[n-1] * ic[n] - Verify pairing equation:
e(piA, piB) == e(alpha, beta) * e(vk_x, gamma) * e(piC, delta) - Return
Trueif pairing check passes -- UTXO unlocked
PrivateMultiplierCircuit.java (define in Java DSL)
|
compileR1CS() (pure Java → R1CS binary)
|
calculateWitness() (pure Java → BigInteger[])
|
pure Java prove (Groth16ProverBLS381)
|
Java verify (pure Java pairing check)
|
Julc compile (VK baked → Plutus V3 script)
|
Lock ADA at script (datum = public inputs)
|
Unlock with proof (redeemer = piA, piB, piC)
|
Plutus V3 executes (BLS12-381 pairing check)
|
Transaction succeeds (proof verified on Cardano!)
The zeroj-examples module contains complete working examples:
# Off-chain: DSL circuit → prove → Java verify
./gradlew :zeroj-examples:test
# On-chain: full flow on Yaci DevKit (requires running Yaci)
./gradlew :zeroj-examples:e2eTestPure Java prover (zero external tools):
| Example | Circuit | Prove | Verify | On-Chain |
|---|---|---|---|---|
SealedBidPureJavaE2ETest |
Sealed bid (497 constraints) | Pure Java | Pairing | No |
AnonymousVotingPureJavaE2ETest |
Anonymous voting (367 constraints) | Pure Java | Pairing | No |
BalanceThresholdPureJavaE2ETest |
Balance threshold (132 constraints) | Pure Java | Pairing | No |
PureJavaProverYaciE2ETest |
Multiplier | Pure Java | Yaci DevKit | Yes |
CircomToOnChainE2ETest |
Circom multiplier | Pure Java | Julc VM | Yes |
ParameterizedCircuitE2ETest |
Hash chain, Merkle, multi-commit | Pure Java | Pairing | No |
FFM/CLI provers (native dependencies):
| Example | Circuit | Prove | Verify | On-Chain |
|---|---|---|---|---|
SealedBidE2ETest |
Sealed bid auction | snarkjs | Pure Java | No |
SealedBidGnarkE2ETest |
Sealed bid auction | gnark FFM | Pure Java | No |
SealedBidOnChainE2ETest |
Sealed bid auction | Pre-generated | Julc/Plutus V3 | Yes (Yaci) |
AnonymousVotingE2ETest |
Anonymous voting | snarkjs | Pure Java | No |
BalanceThresholdE2ETest |
Balance threshold | snarkjs | Pure Java | No |
- Sealed Bid Auction -- prove bid >= reserve without revealing bid amount
- Anonymous Voting -- prove vote is 0/1 with a MiMC commitment in the BN254/off-chain reference flow. For Cardano/BLS12-381 circuits, use Poseidon with explicit BLS12-381 parameters.
- Balance Threshold -- prove balance >= threshold without revealing exact balance
See the examples README for detailed descriptions of each flow.
| Prover | Proof System | Curve | External Deps | Notes |
|---|---|---|---|---|
| Pure Java | Groth16 + PlonK | BLS12-381, BN254 | None | Recommended default path |
| gnark FFM | Groth16 + PlonK | BLS12-381, BN254 | Go native lib | Optional native backend |
| snarkjs CLI | Groth16 + PlonK | BLS12-381, BN254 | Node.js + snarkjs | External CLI workflow |
Pure Java is the recommended prover for the core Cardano path -- zero native dependencies and covered by end-to-end on-chain tests. See the Pure Java Prover Guide for the complete pipeline.
For native or CLI alternatives, see Alternate Prover Backends.
| Curve | Off-Chain | On-Chain (Plutus V3) | Notes |
|---|---|---|---|
| BLS12-381 | Groth16 + PlonK | Groth16 supported; PlonK prototype | Plutus V3 has native BLS builtins |
| BN254 | Groth16 + PlonK | Not feasible | No Plutus BN254 builtins |
For on-chain verification, always use BLS12-381.