Skip to main content
This guide shows how to create, compile, and test a simple Circom scheme and verify a ZK-proof using the zk-SNARK Groth16 protocol.
This guide is also applicable to circuits written in the Noname language, since the export-ton-verifier library integrates with snarkjs, which in turn integrates with the Noname language.Other examples can be found here.

Prerequisites

Project setup

  1. Create a new project using Blueprint:
    npm create ton@latest ZkSimple
    cd ZkSimple
    
  2. Install libraries for working with ZK-proofs:
    npm install snarkjs @types/snarkjs
    
  3. Install the verifier export utility for TON:
    npm install export-ton-verifier@latest
    
This utility exports verifier contracts for FunC, Tolk, and Tact.

Create the Circom circuit

Create the directory circuits/Multiplier and the file Multiplier.circom:
mkdir -p circuits/Multiplier
cd circuits/Multiplier
pragma circom 2.2.2;

template Multiplier() {
  signal input a;
  signal input b;

  signal output c;

  c <== a*b;
}

component main = Multiplier();
This circuit proves knowledge of two numbers a and b, whose product is equal to the public output c, without revealing a and b themselves.

Compile

Run in circuits/Multiplier:
circom Multiplier.circom --r1cs --wasm --sym --prime bls12381
After compilation, the following files will appear:
  • Multiplier.r1cs — circuit constraints (R1CS)
  • Multiplier.sym — symbolic signal map
  • Multiplier.wasm — artifact for generating proof
Check constraints:
snarkjs r1cs info Multiplier.r1cs
Output example:
[INFO]  snarkJS: Curve: bls12-381
[INFO]  snarkJS: # of Wires: 4
[INFO]  snarkJS: # of Constraints: 1
[INFO]  snarkJS: # of Private Inputs: 2
[INFO]  snarkJS: # of Public Inputs: 0
[INFO]  snarkJS: # of Outputs: 1
snarkjs supports the alt_bn128 and bls12-381 curves. Ethereum uses alt_bn128, but bls12-381 is used for TON, so it is the one chosen in this guide.

Trusted setup (Groth16)

The trusted setup is a one-time ceremony that generates the proving and verification keys for a circuit. It’s called “trusted” because if the setup parameters are compromised, proofs could be forged. For production use, participate in a multi-party trusted setup ceremony. For local development and testing, a simplified single-party setup is sufficient. For local tests, perform a simplified trusted setup ceremony. The “power of tau” parameter (10) has to be chosen
  • as low as possible, because it affects execution time;
  • high enough, because the more constraints in the scheme, the higher the parameter required.
# first phase
snarkjs powersoftau new bls12-381 10 pot10_0000.ptau -v
snarkjs powersoftau contribute pot10_0000.ptau pot10_0001.ptau --name="First contribution" -v -e="some random text"

# second phase (depends on the compiled scheme)
snarkjs powersoftau prepare phase2 pot10_0001.ptau pot10_final.ptau -v
snarkjs groth16 setup Multiplier.r1cs pot10_final.ptau Multiplier_0000.zkey
snarkjs zkey contribute Multiplier_0000.zkey Multiplier_final.zkey --name="1st Contributor" -v -e="some random text"

# export verification key
snarkjs zkey export verificationkey Multiplier_final.zkey verification_key.json
Clear up unnecessary artifacts:
rm pot10_0000.ptau pot10_0001.ptau pot10_final.ptau Multiplier_0000.zkey

Export the verifier contract

# export FunC contract (default)
npx export-ton-verifier ./circuits/Multiplier/Multiplier_final.zkey ./contracts/verifier_multiplier.fc

# export Tolk contract
npx export-ton-verifier ./circuits/Multiplier/Multiplier_final.zkey ./contracts/verifier_multiplier.tolk --tolk

# export Tact contract
npx export-ton-verifier ./circuits/Multiplier/Multiplier_final.zkey ./contracts/verifier_multiplier.tact --tact
For FunC and Tolk, wrappers must be generated manually:
npx export-ton-verifier import-wrapper ./wrappers/Verifier.ts --force
This command generates a TypeScript wrapper file that provides type-safe methods to interact with the verifier contract.

Testing and verification

In tests/ZkSimple.spec.ts:
import * as snarkjs from 'snarkjs';
import path from 'path';
import { dictFromInputList, groth16CompressProof } from 'export-ton-verifier';

// for Tact (After running `npx blueprint build --all`)
import { Verifier } from '../build/Verifier_tact/tact_Verifier';

// for FunC and Tolk
import { Verifier } from '../wrappers/Verifier';
Local verification:
const wasmPath = path.join(__dirname, '../circuits/Multiplier', 'Multiplier.wasm');
const zkeyPath = path.join(__dirname, '../circuits/Multiplier', 'Multiplier_final.zkey');
const verificationKey = require('../circuits/Multiplier/verification_key.json');

const input = { a: '342', b: '1245' };

const { proof, publicSignals } = await snarkjs.groth16.fullProve(input, wasmPath, zkeyPath);
const okLocal = await snarkjs.groth16.verify(verificationKey, publicSignals, proof);
On-chain verification:
const { pi_a, pi_b, pi_c, pubInputs } = await groth16CompressProof(proof, publicSignals);

// Quick check via get-method: verifies the proof locally without changing blockchain state.
expect(await verifier.getVerify({ pi_a, pi_b, pi_c, pubInputs })).toBe(true);

// Send the proof to the contract in a message
// The contract will run verification; handling the result/flow is up to the developer using this template.
await verifier.sendVerify(deployer.getSender(), { pi_a, pi_b, pi_c, pubInputs, value: toNano('0.15') });
Build the contracts before running tests. For Tact contracts, run npx blueprint build --all first. For FunC/Tolk, ensure the wrappers are generated.

Other Languages

This tutorial follows the path Circom → snarkjsexport-ton-verifier → TON. The same workflow applies to other stacks — the key requirement is to obtain a proof and a verification key in snarkjs format. In the example repository — zk-ton-examples — there are already templates for noname, gnark, and arkworks: proofs can be generated in any of these stacks, then converted into snarkjs format and verified both locally and on-chain in the same way.
Two utilities are available that help convert proofs and verification keys into a format compatible with snarkjs:
The idea is always the same: generate proof.json and verification_key.json in snarkjs format, then use export-ton-verifier and perform verification in TON.

Arkworks (Rust)

Use the arkworks library to generate the proof and verification key, then convert them into snarkjs format with ark-snarkjs.
  1. Set up an Arkworks project:
cargo init
cargo add ark-bls12-381 ark-ff ark-groth16 ark-r1cs-std ark-relations ark-snark ark-snarkjs ark-std [email protected]
The packages listed above are the core dependencies needed for most arkworks circuits. Depending on the specific circuit implementation, additional packages may be required.
  1. Write the circuit in Rust. Implement the circuit logic using arkworks primitives, similar to how a Circom circuit would be written. Learn how to write constraints in arkworks by following the Arkworks R1CS tutorial. A working example of a simple multiplication circuit can be found in the zk-ton-examples repository.
  2. Compile, generate proof, and perform trusted setup following the same workflow as in the Circom section above.
  3. Export the proof and verification key to JSON using ark-snarkjs:
use ark_snarkjs::{export_proof, export_vk};
use ark_bls12_381::{Bls12_381, Fr};

let _ = export_proof::<Bls12_381, _>(&proof, &public_inputs, "json/proof.json");
let _ = export_vk::<Bls12_381, _>(
    &params.vk,
    public_inputs.len(),
    "json/verification_key.json",
);
The directory and files will be created automatically.
  1. Export the verifier contract:
# FunC contract
npx export-ton-verifier ./circuits/Arkworks/MulCircuit/json/verification_key.json ./contracts/verifier_ark.fc

# Tact contract
npx export-ton-verifier ./circuits/Arkworks/MulCircuit/json/verification_key.json ./contracts/verifier_ark.tact --tact

# Tolk contract
npx export-ton-verifier ./circuits/Arkworks/MulCircuit/json/verification_key.json ./contracts/verifier_ark.tolk --tolk

gnark (Go)

Use the gnark library to generate the proof and verification key, then convert them into snarkjs format with gnark-to-snarkjs.
  1. Set up a gnark project. You can find an example circuit in the gnark repository. A working example of a cubic circuit can be found in the zk-ton-examples repository.
  2. Add gnark-to-snarkjs as a dependency:
go get github.com/mysteryon88/gnark-to-snarkjs@latest
  1. Export the proof and verification key:
{
  proof_out, _ := os.Create("proof.json")
  defer proof_out.Close()
  _ = gnarktosnarkjs.ExportProof(proof, []string{"35"}, proof_out)
}
{
  out, _ := os.Create("verification_key.json")
  defer out.Close()
  _ = gnarktosnarkjs.ExportVerifyingKey(vk, out)
}
  1. Export the verifier contract:
# Tact contract
npx export-ton-verifier ./circuits/cubic-gnark/verification_key.json ./contracts/verifier_cubic.tact --tact

# FunC contract
npx export-ton-verifier ./circuits/cubic-gnark/verification_key.json ./contracts/verifier_cubic.fc

# Tolk contract
npx export-ton-verifier ./circuits/cubic-gnark/verification_key.json ./contracts/verifier_cubic.tolk --tolk

Conclusion

This guide demonstrates a minimal example: circuit → trusted setup → verifier export → verification in TON. This workflow can be extended to support more complex circuits and real-world applications.