Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7bc6642
feat: add Keccak256CompressedTranscript for Cardano/Plutus compatibility
perturbing Jun 6, 2026
54cf7a6
feat: add transcript option to PLONK prover and verifier
perturbing Jun 6, 2026
9c10964
feat: add transcript option to FFLONK prover and verifier
perturbing Jun 6, 2026
0075266
feat: wire --transcript option to CLI and export transcript classes
perturbing Jun 6, 2026
dbf365c
test: add Cardano transcript tests for PLONK and FFLONK
perturbing Jun 6, 2026
c71157c
feat: add Cardano/Plutus proof and verification key export commands
perturbing Jun 7, 2026
14c7c6c
fix: compute FFLONK cubic root of unity curve-agnostically
perturbing Jun 7, 2026
ef06a6e
build: rebuild bundles with Keccak256CompressedTranscript
perturbing Jun 7, 2026
ac9b4a6
fix: restore bn128 FFLONK w3 computation, add bls12381 branch
perturbing Jul 21, 2026
b02dce2
refactor: drop shadowing Fr parameter from getOmegaCubicRoot
perturbing Jul 21, 2026
0bc098a
refactor: extract shared ZCash point compression helpers
perturbing Jul 21, 2026
d76a31f
fix: reject non-bls12381 curves in Cardano export commands
perturbing Jul 21, 2026
3db7366
fix: reject unknown transcript option values
perturbing Jul 21, 2026
a8ee709
refactor: collapse duplicate export-cardano-proof CLI handlers
perturbing Jul 21, 2026
e36bd30
build: rebuild bundles
perturbing Jul 21, 2026
e0425ea
fix: accept absent --transcript option from the CLI
perturbing Aug 5, 2026
cbf7e7a
test: pin ZCash point compression to IETF generator vectors
perturbing Aug 5, 2026
b066503
fix: reject non-bls12381 curves in the compressed transcript
perturbing Aug 5, 2026
85a52d8
refactor: derive compression sign flag from a half-field comparison
perturbing Aug 5, 2026
9243c16
refactor: share transcript implementation between variants
perturbing Aug 5, 2026
0aae369
fix: reject unknown curves in FFLONK w3 computation
perturbing Aug 5, 2026
d0d4197
fix: assert getOmegaCubicRoot cubes to the domain root
perturbing Aug 5, 2026
75df902
feat: include protocol and curve in Cardano proof export
perturbing Aug 5, 2026
8045204
chore: tidy Cardano export helpers and new-file headers
perturbing Aug 5, 2026
901c320
docs: document --transcript option and Cardano export commands
perturbing Aug 5, 2026
3d5e2fe
build: rebuild bundles
perturbing Aug 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
719 changes: 598 additions & 121 deletions build/browser.esm.js

Large diffs are not rendered by default.

625 changes: 551 additions & 74 deletions build/cli.cjs

Large diffs are not rendered by default.

564 changes: 498 additions & 66 deletions build/main.cjs

Large diffs are not rendered by default.

738 changes: 609 additions & 129 deletions build/snarkjs.js

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions build/snarkjs.min.js

Large diffs are not rendered by default.

74 changes: 62 additions & 12 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import * as plonk from "./src/plonk.js";
import * as fflonk from "./src/fflonk.js";
import * as wtns from "./src/wtns.js";
import * as curves from "./src/curves.js";
import exportCardanoProof from "./src/export_cardano_proof.js";
import path from "path";
import bfj from "bfj";

Expand Down Expand Up @@ -253,6 +254,30 @@ const commands = [
alias: ["zkesc", "generatecall -pub|public -p|proof"],
action: zkeyExportSolidityCalldata
},
{
cmd: "zkey export cardano-verificationkey [circuit_final.zkey] [cardano_vk.json]",
description: "Exports a Cardano/Plutus verification key with ZCash-compressed BLS12-381 points",
alias: ["zkecv"],
action: zkeyExportCardanoVKey
},
{
cmd: "groth16 export-cardano-proof [proof.json] [cardano_proof.json]",
description: "Converts a Groth16 proof to Cardano/Plutus format (compressed BLS12-381 points)",
alias: ["g16ecp"],
action: exportCardanoProofCmd
},
{
cmd: "plonk export-cardano-proof [proof.json] [cardano_proof.json]",
description: "Converts a PLONK proof to Cardano/Plutus format (compressed BLS12-381 points)",
alias: ["pkecp"],
action: exportCardanoProofCmd
},
{
cmd: "fflonk export-cardano-proof [proof.json] [cardano_proof.json]",
description: "Converts a FFLONK proof to Cardano/Plutus format (compressed BLS12-381 points)",
alias: ["ffecp"],
action: exportCardanoProofCmd
},
{
cmd: "groth16 setup [circuit.r1cs] [powersoftau.ptau] [circuit_0000.zkey]",
description: "Creates an initial groth16 pkey file with zero contributions",
Expand Down Expand Up @@ -291,21 +316,21 @@ const commands = [
cmd: "plonk prove [circuit.zkey] [witness.wtns] [proof.json] [public.json]",
description: "Generates a PLONK Proof from witness",
alias: ["pkp"],
options: "-verbose|v -protocol",
options: "-verbose|v -protocol -transcript",
Comment thread
perturbing marked this conversation as resolved.
action: plonkProve
},
{
cmd: "plonk fullprove [input.json] [circuit.wasm] [circuit.zkey] [proof.json] [public.json]",
description: "Generates a PLONK Proof from input",
alias: ["pkf"],
options: "-verbose|v -protocol",
options: "-verbose|v -protocol -transcript",
action: plonkFullProve
},
{
cmd: "plonk verify [verification_key.json] [public.json] [proof.json]",
description: "Verify a PLONK Proof",
alias: ["pkv"],
options: "-verbose|v",
options: "-verbose|v -transcript",
action: plonkVerify
},
{
Expand All @@ -319,21 +344,21 @@ const commands = [
cmd: "fflonk prove [circuit.zkey] [witness.wtns] [proof.json] [public.json]",
description: "BETA version. Generates a FFLONK Proof from witness",
alias: ["ffp"],
options: "-verbose|v -protocol",
options: "-verbose|v -protocol -transcript",
action: fflonkProve
},
{
cmd: "fflonk fullprove [witness.json] [circuit.wasm] [circuit.zkey] [proof.json] [public.json]",
description: "BETA version. Generates a witness and the FFLONK Proof in the same command",
alias: ["fff"],
options: "-verbose|v -protocol",
options: "-verbose|v -protocol -transcript",
action: fflonkFullProve
},
{
cmd: "fflonk verify [verification_key.json] [public.json] [proof.json]",
description: "BETA version. Verify a FFLONK Proof",
alias: ["ffv"],
options: "-verbose|v",
options: "-verbose|v -transcript",
action: fflonkVerify
},
{
Expand Down Expand Up @@ -1141,7 +1166,7 @@ async function plonkProve(params, options) {

if (options.verbose) Logger.setLogLevel("DEBUG");

const {proof, publicSignals} = await plonk.prove(zkeyName, witnessName, logger);
const {proof, publicSignals} = await plonk.prove(zkeyName, witnessName, logger, { transcript: options.transcript });

await bfj.write(proofName, stringifyBigInts(proof), {space: 1});
await bfj.write(publicName, stringifyBigInts(publicSignals), {space: 1});
Expand All @@ -1163,7 +1188,7 @@ async function plonkFullProve(params, options) {

const input = JSON.parse(await fs.promises.readFile(inputName, "utf8"));

const {proof, publicSignals} = await plonk.fullProve(input, wasmName, zkeyName, logger);
const {proof, publicSignals} = await plonk.fullProve(input, wasmName, zkeyName, logger, undefined, { transcript: options.transcript });

await bfj.write(proofName, stringifyBigInts(proof), {space: 1});
await bfj.write(publicName, stringifyBigInts(publicSignals), {space: 1});
Expand All @@ -1185,7 +1210,7 @@ async function plonkVerify(params, options) {

if (options.verbose) Logger.setLogLevel("DEBUG");

const isValid = await plonk.verify(verificationKey, pub, proof, logger);
const isValid = await plonk.verify(verificationKey, pub, proof, logger, { transcript: options.transcript });

if (isValid) {
return 0;
Expand Down Expand Up @@ -1214,7 +1239,7 @@ async function fflonkProve(params, options) {

if (options.verbose) Logger.setLogLevel("DEBUG");

const {proof, publicSignals} = await fflonk.prove(zkeyFilename, witnessFilename, logger);
const {proof, publicSignals} = await fflonk.prove(zkeyFilename, witnessFilename, logger, { transcript: options.transcript });

if(undefined !== proofFilename && undefined !== publicInputsFilename) {
// Write the proof and the publig signals in each file
Expand All @@ -1237,7 +1262,7 @@ async function fflonkFullProve(params, options) {

const input = JSON.parse(await fs.promises.readFile(witnessInputsFilename, "utf8"));

const {proof, publicSignals} = await fflonk.fullProve(input, wasmFilename, zkeyFilename, logger);
const {proof, publicSignals} = await fflonk.fullProve(input, wasmFilename, zkeyFilename, logger, undefined, { transcript: options.transcript });

// Write the proof and the publig signals in each file
await bfj.write(proofFilename, stringifyBigInts(proof), {space: 1});
Expand All @@ -1257,11 +1282,36 @@ async function fflonkVerify(params, options) {
const publicInputs = JSON.parse(fs.readFileSync(publicInputsFilename, "utf8"));
const proof = JSON.parse(fs.readFileSync(proofFilename, "utf8"));

const isValid = await fflonk.verify(vkey, publicInputs, proof, logger);
const isValid = await fflonk.verify(vkey, publicInputs, proof, logger, { transcript: options.transcript });

return isValid ? 0 : 1;
}

async function zkeyExportCardanoVKey(params, options) {
const zKeyFileName = params[0] || "circuit_final.zkey";
const vKeyFilename = params[1] || "cardano_vk.json";

if (options.verbose) Logger.setLogLevel("DEBUG");

const vKey = await zkey.exportCardanoVerificationKey(zKeyFileName, logger);
await bfj.write(vKeyFilename, vKey, {space: 1});

return 0;
}

// Shared handler for the per-protocol export-cardano-proof commands:
// exportCardanoProof dispatches on proof.protocol internally.
async function exportCardanoProofCmd(params) {
const proofFilename = params[0] || "proof.json";
const outFilename = params[1] || "cardano_proof.json";

const proof = JSON.parse(fs.readFileSync(proofFilename, "utf8"));
const cardanoProof = await exportCardanoProof(proof);
await bfj.write(outFilename, cardanoProof, {space: 1});

return 0;
}

async function fileInfo(params) {
const filename = params[0];
const extension = filename.split(".").pop();
Expand Down
3 changes: 3 additions & 0 deletions main.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ export * as zKey from "./src/zkey.js";
export * as plonk from "./src/plonk.js";
export * as fflonk from "./src/fflonk.js";
export * as curves from "./src/curves.js";
export { Keccak256Transcript } from "./src/Keccak256Transcript.js";
export { Keccak256CompressedTranscript } from "./src/Keccak256CompressedTranscript.js";
export { default as exportCardanoProof } from "./src/export_cardano_proof.js";
78 changes: 78 additions & 0 deletions src/Keccak256CompressedTranscript.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
Copyright 2022 iden3 association.

This file is part of snarkjs.

snarkjs is a free software: you can redistribute it and/or
modify it under the terms of the GNU General Public License as published by the
Free Software Foundation, either version 3 of the License, or (at your option)
any later version.

snarkjs is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
more details.

You should have received a copy of the GNU General Public License along with
snarkjs. If not, see <https://www.gnu.org/licenses/>.
*/

// Fiat-Shamir transcript using Keccak-256 with compressed G1 points (ZCash/IETF flag format).
// Use this instead of Keccak256Transcript when the on-chain verifier operates on compressed
// BLS12-381 points, e.g. Cardano/Plutus via CIP-0381 + CIP-0101.

import {Scalar} from "ffjavascript";
import {keccak_256} from "@noble/hashes/sha3";
import {compressG1} from "./point_compress.js";

const POLYNOMIAL = 0;
const SCALAR = 1;

export class Keccak256CompressedTranscript {
constructor(curve) {
this.G1 = curve.G1;
this.Fr = curve.Fr;

this.reset();
}

reset() {
this.data = [];
}

addPolCommitment(polynomialCommitment) {
this.data.push({type: POLYNOMIAL, data: polynomialCommitment});
}

addScalar(scalar) {
this.data.push({type: SCALAR, data: scalar});
}

getChallenge() {
if (0 === this.data.length) {
throw new Error("Keccak256CompressedTranscript: No data to generate a transcript");
}

let nPolynomials = 0;
let nScalars = 0;

this.data.forEach(element => POLYNOMIAL === element.type ? nPolynomials++ : nScalars++);

// Compressed G1 points are G1.F.n8 bytes (vs 2*n8 for uncompressed).
let buffer = new Uint8Array(nScalars * this.Fr.n8 + nPolynomials * this.G1.F.n8);
let offset = 0;

for (let i = 0; i < this.data.length; i++) {
if (POLYNOMIAL === this.data[i].type) {
buffer.set(compressG1(this.G1, this.data[i].data), offset);
offset += this.G1.F.n8;
} else {
this.Fr.toRprBE(buffer, offset, this.data[i].data);
offset += this.Fr.n8;
}
}

const value = Scalar.fromRprBE(keccak_256(buffer));
return this.Fr.e(value);
}
}
121 changes: 121 additions & 0 deletions src/export_cardano_proof.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
Copyright 2018 0KIMS association.

This file is part of snarkJS.

snarkJS is a free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

snarkJS is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
License for more details.

You should have received a copy of the GNU General Public License
along with snarkJS. If not, see <https://www.gnu.org/licenses/>.
*/

// Converts a snarkjs proof.json (projective/affine [x, y, z] coordinates) into
// a Cardano/Plutus-friendly JSON where all G1 points are 48-byte ZCash/IETF
// compressed hex strings and all G2 points are 96-byte compressed hex strings.
// Scalar field evaluations (eval_* / ql / qr / ...) are left as decimal strings.

import {getCurveFromName} from "./curves.js";
import {compressG1Hex, compressG2Hex} from "./point_compress.js";
import {utils} from "ffjavascript";

const {unstringifyBigInts} = utils;

// ---------- public API ----------

export default async function exportCardanoProof(_proof) {
const proof = unstringifyBigInts(_proof);

const curve = await getCurveFromName(proof.curve);
Comment thread
perturbing marked this conversation as resolved.

// The ZCash compressed encoding stores flags in the three high bits of the
// first byte, which only works when the base field leaves them free (as the
// 381-bit bls12381 field does in its 48-byte serialization). On other
// curves the flags would overwrite x-coordinate data.
if (curve.name !== "bls12381") {
throw new Error(`exportCardanoProof: only bls12381 proofs are supported, got '${curve.name}'`);
}

if (proof.protocol === "groth16") {
return groth16CardanoProof(curve, proof);
} else if (proof.protocol === "plonk") {
return plonkCardanoProof(curve, proof);
} else if (proof.protocol === "fflonk") {
return fflonkCardanoProof(curve, proof);
} else {
throw new Error(`exportCardanoProof: unknown protocol '${proof.protocol}'`);
}
}

// ---------- per-protocol converters ----------

function g1FromObj(curve, obj) {
return curve.G1.fromObject(obj);
}

function g2FromObj(curve, obj) {
return curve.G2.fromObject(obj);
}

function groth16CardanoProof(curve, proof) {
return {
pi_a: compressG1Hex(curve.G1, g1FromObj(curve, proof.pi_a)),
pi_b: compressG2Hex(curve.G2, g2FromObj(curve, proof.pi_b)),
pi_c: compressG1Hex(curve.G1, g1FromObj(curve, proof.pi_c)),
};
}

// PLONK proof.json uses a flat layout:
// A, B, C, Z, T1, T2, T3, Wxi, Wxiw → G1 points as [x, y, "1"]
// eval_a, eval_b, eval_c, eval_s1, eval_s2, eval_zw → Fr scalars (decimal strings)
function plonkCardanoProof(curve, proof) {
const g1 = (key) => compressG1Hex(curve.G1, g1FromObj(curve, proof[key]));
return {
A: g1("A"),
B: g1("B"),
C: g1("C"),
Z: g1("Z"),
T1: g1("T1"),
T2: g1("T2"),
T3: g1("T3"),
Wxi: g1("Wxi"),
Wxiw: g1("Wxiw"),
// Scalar evaluations: keep as decimal strings (already stringified by caller)
eval_a: String(proof.eval_a),
eval_b: String(proof.eval_b),
eval_c: String(proof.eval_c),
eval_s1: String(proof.eval_s1),
eval_s2: String(proof.eval_s2),
eval_zw: String(proof.eval_zw),
};
}

// FFLONK proof.json uses a nested layout:
// polynomials: { C1, C2, W1, W2 } → G1 points
// evaluations: { ql, qr, qm, qo, qc, s1, s2, s3, a, b, c, z, zw, t1w, t2w, inv } → Fr scalars
function fflonkCardanoProof(curve, proof) {
const poly = proof.polynomials;
const eval_ = proof.evaluations;
const g1 = (p) => compressG1Hex(curve.G1, g1FromObj(curve, p));
const sc = (v) => String(v);
return {
c1: g1(poly.C1),
c2: g1(poly.C2),
w1: g1(poly.W1),
w2: g1(poly.W2),
ql: sc(eval_.ql), qr: sc(eval_.qr), qm: sc(eval_.qm),
qo: sc(eval_.qo), qc: sc(eval_.qc),
s1: sc(eval_.s1), s2: sc(eval_.s2), s3: sc(eval_.s3),
a: sc(eval_.a), b: sc(eval_.b), c: sc(eval_.c),
z: sc(eval_.z), zw: sc(eval_.zw),
t1w: sc(eval_.t1w), t2w: sc(eval_.t2w),
inv: sc(eval_.inv),
};
}
Loading