This repository accompanies the snarkjs pull request that upstreams Cardano compatibility across all three proof systems (Groth16, PLONK, FFLONK). It demonstrates the full end-to-end workflow — from a Circom circuit through snarkjs proof generation to an on-chain Aiken verifier running on Cardano (Plutus v3, BLS12-381).
The goal is to give Cardano developers a concrete, auditable reference: what snarkjs produces, what the Cardano-specific export commands do, and how the resulting proof data maps to an on-chain verifier.
circom/ Circom circuits and witness inputs
mul.circom c = a · b, public [b] — used by Groth16 and PLONK
mul2.circom c = a · b², public [c, b] — used by FFLONK
mul.input.json a=3, b=5
mul2.input.json a=3, b=4
test-vectors/ Committed proof artifacts (reproducible via e2e scripts)
pot8_final.ptau Powers-of-tau for circuits up to 2⁸ constraints
groth16/ cardano_vk.json, cardano_proof.json, public.json
plonk/
fflonk/
scripts/ End-to-end shell scripts + Aiken test literal generators
e2e_groth16.sh
e2e_plonk.sh
e2e_fflonk.sh
gen_groth16_test.js
gen_plonk_test.js
gen_fflonk_test.js
lib/ Aiken verifier libraries
groth16/ verifier.ak, types.ak, verifier.test.ak
plonk/
fflonk/
The easiest way to get all tools pinned to the right versions is Nix:
nix develop # provides circom, snarkjs (Cardano fork), aiken, node, jqNote: the
flake.nixcurrently points the snarkjs input at a local path while the PR is in review. Once merged, it will point to the upstreamiden3/snarkjsrepository.
Without Nix, you need:
- circom ≥ 2.0
- snarkjs built from the Cardano-compat branch (
perturbing/cardano-compat-nix) - aiken v1.1.19
- Node.js ≥ 18
Then install the JS dependencies used by the generator scripts:
npm installThe snarkjs Cardano branch adds the following commands and flags on top of the standard CLI. Everything else (circuit compilation, witness generation, trusted setup) is unchanged.
Applies to plonk prove and fflonk prove. Switches the Fiat-Shamir hash
function from the default (Poseidon / SHA-256) to keccak-256 with compressed
point encoding.
Cardano has a native keccak-256 builtin, making it cheap to verify on-chain. Poseidon is not available as a builtin, so using the default transcript would require an extremely expensive software implementation.
snarkjs plonk prove --transcript keccak256-compressed \
circuit.zkey witness.wtns proof.json public.json
snarkjs fflonk prove --transcript keccak256-compressed \
circuit.zkey witness.wtns proof.json public.jsonGroth16 has no Fiat-Shamir transcript (it is a non-interactive argument from a structured reference string), so no flag is needed.
Exports the verification key in Cardano-friendly JSON: G1 points as compressed 48-byte hex strings, G2 points as compressed 96-byte hex strings, and scalar field elements as decimal integers. Works for all three proof systems.
snarkjs zkey export cardano-verificationkey circuit.zkey cardano_vk.jsonRe-encodes a Groth16 proof (originally in affine, uncompressed Montgomery form) into compressed G1/G2 hex strings suitable for the Cardano BLS12-381 builtins.
snarkjs groth16 export-cardano-proof proof.json cardano_proof.jsonSame re-encoding for PLONK and FFLONK. These proofs additionally contain scalar
field evaluations (already integers) and the batch-inverse hint inv (see
below).
snarkjs plonk export-cardano-proof proof.json cardano_proof.json
snarkjs fflonk export-cardano-proof proof.json cardano_proof.jsonEach script compiles the circuit, generates a witness, runs the trusted setup, proves, exports the Cardano artefacts, and regenerates the Aiken test file:
bash scripts/e2e_groth16.sh
bash scripts/e2e_plonk.sh
bash scripts/e2e_fflonk.shAfter any of these, run the Aiken test suite to confirm the verifier still accepts the fresh proof:
aiken checkThe committed test-vectors/ directory contains the artefacts produced by
these scripts, so aiken check works out of the box without running the e2e
scripts first.
All G1 elements are 48-byte compressed byte arrays; G2 elements are 96 bytes.
The Plutus bls12_381_G1_uncompress / bls12_381_G2_uncompress builtins
decompress them on-chain.
Challenges are derived by hashing compressed curve points concatenated with scalar evaluations, all encoded as big-endian 32-byte integers. The verifier recomputes these challenges on-chain using Cardano's native keccak-256 builtin, so the transcript must match exactly what the prover used.
On-chain modular inversion (computing 1/x mod p) currently requires
implementing a software modular exponentiation loop, which is prohibitively
expensive in Plutus execution units.
To avoid this, the prover supplies a single scalar inv defined as:
inv = 1 / ∏ dᵢ (mod p)
where {dᵢ} is the list of denominators the verifier needs to invert. The
verifier checks inv · ∏ dᵢ ≡ 1 (mod p) (one cheap multiplication) and then
uses the Montgomery batch-inverse trick to recover each 1/dᵢ individually
with only multiplications — no modular exponentiation.
For FFLONK this covers 23 denominators (for n_public = 2); for PLONK it
covers n_public + 1. In both cases the hint is a single 32-byte scalar,
making proof size constant in n_public.
Future work: Aiken PR #1310 adds a modular exponentiation builtin. Once that lands, the
invfield can be dropped from the proof and computed on-chain directly, removing the need for any prover-side hint.
The final KZG check uses Cardano's native pairing builtins:
bls12_381_millerLoop and bls12_381_finalVerify. Note that
bls12_381_finalVerify(ml₁, ml₂) checks equality of the two Miller-loop
outputs (i.e. e(A,B) == e(C,D)), not that their product equals one. The
verifier equations are written accordingly.
aiken checkExpected output (all passing):
PASS verify_fflonk_valid [mem: 1.98 M, cpu: 2.39 B]
PASS verify_fflonk_wrong_pub_input [mem: 1.47 M, cpu: 0.99 B]
PASS verify_fflonk_tampered_c1 [mem: 1.54 M, cpu: 1.03 B]
PASS verify_groth16_valid [mem: 37.2 K, cpu: 2.25 B]
PASS verify_groth16_wrong_pub_input [mem: 37.8 K, cpu: 2.25 B]
PASS verify_groth16_tampered_pi_a [mem: 41.7 K, cpu: 2.25 B]
PASS verify_plonk_valid [mem: 454.2 K, cpu: 3.51 B]
PASS verify_plonk_wrong_pub_input [mem: 356.9 K, cpu: 3.49 B]
PASS verify_plonk_tampered_commitment [mem: 402.6 K, cpu: 3.51 B]