Skip to content

cardano-scaling/snarkjs-circom-aiken

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

snarkjs × Cardano — on-chain verifiers for Groth16, PLONK, and FFLONK

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.


Repository layout

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/

Prerequisites

The easiest way to get all tools pinned to the right versions is Nix:

nix develop   # provides circom, snarkjs (Cardano fork), aiken, node, jq

Note: the flake.nix currently points the snarkjs input at a local path while the PR is in review. Once merged, it will point to the upstream iden3/snarkjs repository.

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 install

Cardano-specific snarkjs commands

The 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.

--transcript keccak256-compressed

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.json

Groth16 has no Fiat-Shamir transcript (it is a non-interactive argument from a structured reference string), so no flag is needed.

zkey export cardano-verificationkey

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.json

groth16 export-cardano-proof

Re-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.json

plonk export-cardano-proof / fflonk export-cardano-proof

Same 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.json

Running end-to-end

Each 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.sh

After any of these, run the Aiken test suite to confirm the verifier still accepts the fresh proof:

aiken check

The 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.


On-chain verifier design notes

Point encoding

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.

Fiat-Shamir challenges (PLONK and FFLONK)

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.

Batch inverse hint (inv)

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 inv field can be dropped from the proof and computed on-chain directly, removing the need for any prover-side hint.

Pairing check

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.


Running the tests

aiken check

Expected 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]

About

The minimal onchain implementation of ZK verifiers in Aiken that are generated by snarkjs and circom.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors