Skip to content

Commit 13b24bc

Browse files
authored
docs: add guide “zkProofs on Tact” (#3479)
- Added new Markdown page: cookbook/zk-proofs-on-tact.mdx - Guide explains zkJetton — a minimal Jetton contract with hidden balances - Covers Circom circuits (registration, mint, transfer) with Groth16 - Includes trusted setup, verifier export for Tact, and testing flow - Provides useful references for zk integration in TON - Sorted `cspell-list.txt` alphabetically for consistency
1 parent c4ed3e5 commit 13b24bc

File tree

3 files changed

+314
-4
lines changed

3 files changed

+314
-4
lines changed

docs/astro.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ export default defineConfig({
260260
collapsed: true,
261261
autogenerate: { directory: 'cookbook/dexes' }
262262
},
263+
{ slug: 'cookbook/zk-proofs-on-tact' },
263264
],
264265
},
265266
{
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
---
2+
title: zkProofs on Tact
3+
description: "A step-by-step guide on integrating zero-knowledge proofs into Tact smart contracts, using zkJetton as an example with hidden balances, Circom circuits, and Groth16 verifiers."
4+
---
5+
6+
7+
## Introduction
8+
9+
This guide shows how to create, compile, and test Circom circuits and verify **ZK-proofs** in the **TON** blockchain using the **Tact** language and the **zk-SNARK Groth16** protocol.
10+
11+
It demonstrates how to use the **[zkJetton](https://github.com/zkTokenTip/zkJetton)** repository to create a Jetton token in the **TON** blockchain, where user balances are hidden using homomorphic encryption and zero-knowledge proofs.
12+
13+
The zkJetton project combines the Jetton standard with ZK-proof verification inside **Tact** smart contracts. `zkJetton` is based on the pipeline **Circom → snarkjs → export-ton-verifier → Tact**, similar to the examples from [zk-ton-example](https://github.com/zkTokenTip/zk-ton-example).
14+
15+
:::note
16+
This guide is also applicable to circuits written in [Noname](https://github.com/zksecurity/noname), since the `export-ton-verifier` library integrates with `snarkjs`, which in turn supports Noname.
17+
18+
You can also use [gnark](https://github.com/Consensys/gnark) circuits by importing a verification key compatible with `snarkjs`.
19+
:::
20+
21+
### Disclaimer
22+
23+
This repository uses a simplified version of Jetton written in Tact. The code has not been audited, contains potential vulnerabilities, and requires significant improvements. It is implemented solely for educational purposes.
24+
25+
---
26+
27+
## What this guide covers
28+
29+
- How zkJetton is designed.
30+
- Setting up the environment.
31+
- How to work with Circom.
32+
- Exporting zk-verifiers for Tact.
33+
- Testing zkJetton.
34+
35+
---
36+
37+
## Prerequisites
38+
39+
- **Node.js** and **npm** installed.
40+
- **circom** and **snarkjs** installed.
41+
- Basic familiarity with TON, Tact, and the Blueprint toolkit.
42+
- Basic knowledge of Jetton and Tact.
43+
44+
---
45+
46+
## Homomorphic encryption
47+
48+
This project uses additively homomorphic **Paillier** encryption for private balances. It allows performing **addition** operations directly on encrypted data without decryption:
49+
50+
- $Enc(m_1) \cdot Enc(m_2) \bmod n^2 = Enc(m_1 + m_2)$
51+
52+
This aligns perfectly with Jetton logic: deposit/withdrawal = adding/subtracting to the hidden balance.
53+
54+
---
55+
56+
## Project setup
57+
58+
1. Create a new project with Blueprint.
59+
2. Install libraries for ZK-proof handling:
60+
61+
```bash
62+
npm install snarkjs @types/snarkjs
63+
```
64+
65+
3. Install the verifier export utility for TON:
66+
67+
```bash
68+
npm install export-ton-verifier@latest
69+
```
70+
71+
This tool exports verifier contracts for Tact, FunC, and Tolk.
72+
73+
Or simply clone the current project:
74+
75+
```sh
76+
git clone https://github.com/zkTokenTip/zkJetton.git
77+
cd zkJetton
78+
npm install
79+
```
80+
81+
---
82+
83+
## zkJetton architecture
84+
85+
The token consists of several contracts:
86+
87+
1. `ZkJettonMinter` — similar to `JettonMinter`. The main token contract. Allows minting tokens (`Mint`). Inherits from `trait MintVerifier`.
88+
2. `ZkJettonWallet` — similar to `JettonWallet`. Created for each user upon registration. Allows transferring tokens (`ZkJettonTransfer`) and receiving them (`ZkJettonTransferInternal`). Inherits from `trait RegistrationVerifier` and `trait TransferVerifier`.
89+
90+
---
91+
92+
## Circom
93+
94+
In the `circuits` directory, you will find `circom` circuits that can be compiled as follows:
95+
96+
```bash
97+
cd circuits
98+
99+
circom registration.circom --r1cs --wasm --sym --prime bls12381
100+
circom mint.circom --r1cs --wasm --sym --prime bls12381
101+
circom transfer.circom --r1cs --wasm --sym --prime bls12381
102+
```
103+
104+
Compilation produces:
105+
- `.r1cs` — circuit constraints (R1CS)
106+
- `.sym` — signal mapping
107+
- `.wasm` — artifact for proof generation
108+
109+
:::note
110+
`snarkjs` supports both **altbn-128** and **bls12-381** curves. Ethereum uses altbn-128, but TON uses **bls12-381**, which is why this guide uses it.
111+
:::
112+
113+
### Circuits
114+
115+
#### Registration circuit
116+
The first step is registering in the token contract and assigning the user keys that will be used for encrypting balances.
117+
118+
The template for fast modular exponentiation is imported first:
119+
120+
```circom
121+
include "binpower.circom";
122+
```
123+
124+
This template is then used to encrypt an initial balance of zero, required for proving that the encrypted balance indeed equals zero.
125+
126+
#### Mint circuit
127+
The second step is minting tokens to the user.
128+
129+
After registration, the user’s public key and encrypted balance are stored in the token contract.
130+
131+
The circuit checks that the minted amount is encrypted with the recipient’s public key. This is important because otherwise, when adding the encrypted balance and transfer amount, the decrypted result could be invalid (e.g., instead of 10 tokens, the user might get 10,000).
132+
133+
#### Transfer circuit
134+
The third step is transferring tokens from one user to another.
135+
136+
The circuit checks that:
137+
1. The transfer amount does not exceed the decrypted user balance.
138+
2. The encrypted transfer amounts for sender and recipient are correctly encrypted with their respective public keys and are valid.
139+
140+
---
141+
142+
### Trusted setup (Groth16)
143+
144+
After writing and compiling circuits, the next step is running a simplified trusted setup ceremony. Example for the registration circuit (similar for others):
145+
146+
```bash
147+
snarkjs powersoftau new bls12-381 10 pot10_0000.ptau -v
148+
snarkjs powersoftau contribute pot10_0000.ptau pot10_0001.ptau --name="First contribution" -v -e="some random text"
149+
snarkjs powersoftau prepare phase2 pot10_0001.ptau pot10_final.ptau -v
150+
snarkjs groth16 setup registration.r1cs pot10_final.ptau registration_0000.zkey
151+
snarkjs zkey contribute registration_0000.zkey registration_final.zkey --name="1st Contributor Name" -v -e="some random text"
152+
153+
# export verification key
154+
snarkjs zkey export verificationkey registration_final.zkey verification_key.json
155+
```
156+
157+
The parameter (`10`) affects execution time — larger circuits require higher values.
158+
159+
---
160+
161+
## Exporting verifier contracts
162+
163+
After the trusted setup ceremony, verifier contracts can be exported for Tact:
164+
165+
```sh
166+
npx export-ton-verifier ./circuits/registration/registration_final.zkey ./contracts/verifiers/verifier_registration.tact --tact
167+
```
168+
169+
This generates a contract template that accepts a proof and verifies it:
170+
171+
```tact
172+
receive(msg: Verify) {
173+
let res = self.groth16Verify(msg.piA, msg.piB, msg.piC, msg.pubInputs);
174+
}
175+
```
176+
177+
For quick checks, you can use the `verify` get-method.
178+
179+
Possible integration approaches:
180+
1. Turn the contract into a `trait` and inherit from it.
181+
2. Extend the generated contract with business logic.
182+
3. Use a two-step flow:
183+
- User → Verifier (proof check)
184+
- Verifier → Main contract (execute logic if verified)
185+
186+
The example here uses the `trait` approach as the most convenient.
187+
188+
---
189+
190+
## Testing and proof verification
191+
192+
Testing is split into two stages:
193+
1. Verifier testing (`Verifiers.spec.ts`)
194+
2. Token testing (`zkJetton.spec.ts`)
195+
196+
Helper functions in the `common` directory simplify proof generation.
197+
198+
### Preparing for testing
199+
For example, registration:
200+
201+
```ts
202+
import paillierBigint from 'paillier-bigint';
203+
import * as snarkjs from 'snarkjs';
204+
import path from 'path';
205+
206+
import { dictFromInputList, groth16CompressProof } from 'export-ton-verifier';
207+
208+
const wasmPath = path.join(__dirname, '../../circuits/registration/registration_js', 'registration.wasm');
209+
const zkeyPath = path.join(__dirname, '../../circuits/registration', 'registration_final.zkey');
210+
```
211+
212+
Imports used:
213+
1. `paillier-bigint` — implementation of the homomorphic cryptosystem.
214+
2. `export-ton-verifier` — helper functions:
215+
- `dictFromInputList` — converts input array into a `Dictionary`.
216+
- `groth16CompressProof` — prepares the proof for sending to the contract.
217+
3. `path` — file path handling.
218+
219+
### Generating a proof
220+
Proofs can be generated with one line:
221+
222+
```js
223+
await snarkjs.groth16.fullProve(getRegistrationData(keys), wasmPath, zkeyPath);
224+
```
225+
226+
The function `getRegistrationData(keys)` generates input values for proof creation:
227+
228+
```js
229+
export function getRegistrationData(keys: paillierBigint.KeyPair) {
230+
const balance = initBalance; // 0
231+
const rand_r = getRandomBigInt(keys.publicKey.n);
232+
const encryptedBalance = keys.publicKey.encrypt(balance, rand_r);
233+
const pubKey = [keys.publicKey.g, rand_r, keys.publicKey.n];
234+
235+
return { encryptedBalance, balance, pubKey };
236+
}
237+
```
238+
239+
Which corresponds to the `circom` circuit:
240+
241+
```
242+
signal input encryptedBalance;
243+
signal input balance;
244+
// public key: g, rand r, n
245+
signal input pubKey[3];
246+
```
247+
248+
The generated proof is then prepared for contract submission:
249+
250+
```js
251+
const { proof, publicSignals } = await createRegistrationProof(keys);
252+
253+
const { pi_a, pi_b, pi_c, pubInputs } = await groth16CompressProof(proof, publicSignals);
254+
```
255+
256+
### Proof verification
257+
For testing, proofs can be verified locally:
258+
259+
```js
260+
const verificationKey = require('../circuits/verification_key.json');
261+
262+
const ok = await snarkjs.groth16.verify(verificationKey, publicSignals, proof);
263+
```
264+
265+
Or sent to the contract:
266+
267+
```js
268+
await zkJettonWallet.getVerifyRegistration(
269+
beginCell().storeBuffer(pi_a).endCell().asSlice(),
270+
beginCell().storeBuffer(pi_b).endCell().asSlice(),
271+
beginCell().storeBuffer(pi_c).endCell().asSlice(),
272+
dictFromInputList(pubInputs),
273+
),
274+
```
275+
276+
---
277+
278+
## Conclusion
279+
280+
`zkJetton` is a working example of a minimal Jetton token with private balances.
281+
It demonstrates:
282+
- how to integrate zk-proofs into TON,
283+
- how to protect user data,
284+
- how to use Circom circuits with Tact contracts.
285+
286+
This can serve as a foundation for building private DeFi protocols, DAOs, or payment systems in TON.
287+
288+
---
289+
290+
## Useful links
291+
292+
- Token repository with hidden balances: [zkJetton](https://github.com/zkTokenTip/zkJetton)
293+
- Example repository: [zk-ton-example](https://github.com/zkTokenTip/zk-ton-example/)
294+
- Verifier export library: [export-ton-verifier](https://github.com/mysteryon88/export-ton-verifier)
295+
- Circom: [docs.circom.io](https://docs.circom.io/)
296+
- SnarkJS: [iden3/snarkjs](https://github.com/iden3/snarkjs)

0 commit comments

Comments
 (0)