Skip to content

Prove with PlonK

View Markdown

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.

PieceAPIModule
Compile a circuitcircuit.compilePlonK(CurveId.BLS12_381) → PlonKConstraintSystemzeroj-circuit-dsl
Development SRSPowersOfTauBLS381.generate(power) (needs the insecure-setup flag)zeroj-crypto
Import a .ptau SRSPtauImporterBLS381.importPtau(input, maxPoints, expectedSha256)zeroj-crypto
SetupPlonKSetupBLS381.setup(...) → PlonKProvingKeyBLS381zeroj-crypto
ProvePlonKProverBLS381.prove(...); proveCardano(...) and proveCardanoMpi(...) for the on-chain profileszeroj-crypto
Import a snarkjs PlonK .zkeyPlonKZkeyImporterBLS381.importZkey(input, expectedSha256)zeroj-crypto
Export snarkjs JSONSnarkjsPlonkJson.proofJson, verificationKeyJson, publicJsonzeroj-crypto
Verify off-chainPlonkBLS12381Verifierzeroj-verifier-plonk
Verify on-chainPlonkBLS12381Verifier, PlonkBLS12381MultiInputVerifier, PlonkBLS12381MultiInputParamVerifier (JuLC validators)zeroj-onchain-julc

zeroj-verifier-plonk is deliberately outside zeroj-bom-core, so declare its version explicitly:

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.

  • 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).

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:

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:

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.

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.

The JuLC validators perform the KZG pairing check with Plutus V3’s BLS12-381 built-ins, for two bounded profiles:

ValidatorProfileProve with
PlonkBLS12381VerifierExactly one public inputPlonKProverBLS381.proveCardano(...)
PlonkBLS12381MultiInputVerifier1 to 8 public inputs, supplied in the datumPlonKProverBLS381.proveCardanoMpi(...)
PlonkBLS12381MultiInputParamVerifier1 to 8 public inputs, pinned at script-application timePlonKProverBLS381.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.

Further reading: plonk-support.md, ADR-0024 (PlonK release gates and multi-public-input profile).