Розробка застосунків Zero-Knowledge Proof
Більшість проектів, що звертаються за підтримкою ZKP, стикаються з однією з двох проблем: або потрібно довести факт без розкриття даних (вік, баланс, належність до множини), або потрібно перемістити важкі обчислення поза блокчейн з верифікацією в мережі. Це різні завдання з різними наборами інструментів, і плутанина між ними — перша і найдорожча помилка на старті.
Вибір proof-системи: це не академічне питання
Вибір між Groth16, PLONK, STARK, Halo2 та FRI визначає все: розмір proof, час генерування, наявність trusted setup, вартість верифікації в мережі.
Порівняння основних систем
| Система | Trusted setup | Розмір proof | Вартість верифікації (EVM) | Час прувера | Рекурсія |
|---|---|---|---|---|---|
| Groth16 | Так (per-circuit) | ~200 байт | ~270k газу | Швидкий | Складна |
| PLONK (KZG) | Так (універсальний) | ~800 байт | ~400k газу | Середній | Простіша |
| PLONK (IPA) | Ні | ~1.5KB | Дороговартісна | Повільний | Хороша |
| STARK | Ні | 40–200KB | Дуже дорога в EVM | Повільний | Відмінна |
| Halo2 | Ні | ~1–5KB | Не рідний | Середній | Вбудована |
Groth16 — вибір для production-систем з фіксованою схемою та мінімальними вимогами до газу. Використовують: Tornado Cash (колишній), Zcash Sapling, більшість zkSNARK мостів. Недолік: кожна зміна circuit вимагає нової церемонії.
PLONK з KZG — de facto стандарт для zkRollup-подібних систем. Gnosis, zkSync Lite, Polygon Hermez використовують варіанти PLONK. Універсальний trusted setup (Powers of Tau) переважно використовується — церемонія не потрібна для кожної схеми.
STARKs — вибір для завдань без trusted setup та що вимагають рекурсії: StarkNet, Cairo VM. Величезний розмір proof — верифікація в EVM не практична, потребує окремого верифікаційного контракту або L3 підходу.
Halo2 — використовується Zcash Orchard, Scroll. Не вимагає trusted setup, вбудована рекурсія. Інструменти менш зрілі, менша екосистема, але активно розвиваються.
Для більшості практичних завдань (приватне голосування, proof of membership, zkKYC, перевірка віку) — Groth16 через circom/snarkjs або PLONK через gnark/noir — правильна стартова точка.
Розробка circuits: де сидять справжні баги
Circom та його підводні каміння
Circom — DSL для написання arithmetic circuits. Circuit компілюється в R1CS (Rank-1 Constraint System), потім через snarkjs або rapidsnark генерується proof.
Базова схема для доказу знання preimage хешу:
pragma circom 2.1.4;
include "circomlib/circuits/poseidon.circom";
include "circomlib/circuits/comparators.circom";
template ProveBalance() {
signal input balance; // private
signal input salt; // private
signal input commitment; // public (зберігається in-chain)
signal input threshold; // public
// Доводимо: hash(balance, salt) == commitment
component hasher = Poseidon(2);
hasher.inputs[0] <== balance;
hasher.inputs[1] <== salt;
hasher.out === commitment;
// Доводимо: balance >= threshold (без розкриття балансу)
component gte = GreaterEqThan(64);
gte.in[0] <== balance;
gte.in[1] <== threshold;
gte.out === 1;
}
component main {public [commitment, threshold]} = ProveBalance();
Критична уразливість: недостатньо обмежені сигнали. Це найпоширеніший клас багів у ZK circuits. Якщо сигнал використовується в обчисленні, але немає достатнього числа обмежень — прувер може передати довільне значення, і верифікатор прийме proof.
Приклад уразливого коду:
// УРАЗЛИВО: немає обмеження, що out це bit
template IsZero() {
signal input in;
signal output out;
signal inv;
inv <-- in != 0 ? 1/in : 0;
out <-- in == 0 ? 1 : 0;
// ЗАБУЛИ: in * out === 0 та (in * inv - 1 + out) === 0
}
Верифікатор приймає будь-який out, тому що немає обмежень, пов'язаних out з in.
Переповнення в field arithmetic. Circom працює у простому полі p = 21888242871839275222246405745257275088548364400416034343698204186575808495617. Будь-яка операція виконується за модулем p. Якщо вхідні дані з реального світу (вік, timestamp), діапазон безпечний. Але при множенні великих чисел потрібна явна перевірка діапазону через Num2Bits:
component rangeCheck = Num2Bits(64);
rangeCheck.in <== balance;
// Тепер баланс гарантовано < 2^64
Gnark (Go) для більш складних схем
Коли схема занадто складна для circom (рекурсивні докази, BLS signature verification, zkEVM-подібні компоненти) — використовуйте gnark:
type Circuit struct {
PreImage frontend.Variable `gnark:",secret"`
Hash frontend.Variable `gnark:",public"`
}
func (c *Circuit) Define(api frontend.API) error {
mimc, err := mimc.NewMiMC(api)
if err != nil {
return err
}
mimc.Write(c.PreImage)
result := mimc.Sum()
api.AssertIsEqual(result, c.Hash)
return nil
}
gnark у 10–30 разів швидший за snarkjs у часі прувера для еквівалентних схем. Для production з реальними користувачами це важливо: генерація proof у браузері через WASM займає 3–15 секунд на помірній Groth16 схемі, у Go сервері — 0.1–1 секунду.
Верифікація in-chain
Solidity верифікатор генерується автоматично — snarkjs робить це через snarkjs zkey export solidityverifier. Але production контракт потребує адаптації:
contract BalanceProofVerifier {
IGroth16Verifier public immutable verifier;
// Зберігання nullifier для захисту від replay
mapping(bytes32 => bool) public usedNullifiers;
function verifyAndExecute(
uint[2] calldata a,
uint[2][2] calldata b,
uint[2] calldata c,
uint[2] calldata publicInputs // [commitment, threshold]
) external {
bytes32 nullifier = keccak256(abi.encodePacked(a, b, c));
require(!usedNullifiers[nullifier], "Proof already used");
require(verifier.verifyProof(a, b, c, publicInputs), "Invalid proof");
usedNullifiers[nullifier] = true;
// ... основна логіка
}
}
Nullifier pattern — обов'язковий для proof of membership та будь-яких систем, де один proof не повинен використовуватися двічі. Nullifier = детермінований хеш від секретного вводу, який неможливо пов'язати з ідентичністю, але можна перевірити на унікальність.
Вартість газу для Groth16 верифікації — близько 270k газу. На Ethereum mainnet при 20 gwei це ~$2–5 за верифікацію. Для систем з високою частотою — деплой на L2 (Arbitrum, Base) зменшує вартість в 10–50 разів.
Інфраструктура для генерування proof
Генерування на клієнті (браузер)
Придатне для: операцій на рівні гаманця, окремих доказів. Використовує WebAssembly збірку snarkjs:
import { groth16 } from "snarkjs";
const { proof, publicSignals } = await groth16.fullProve(
{ balance: "5000", salt: randomSalt, commitment: onChainCommitment, threshold: "1000" },
"/circuits/balance_proof.wasm",
"/circuits/balance_proof_final.zkey"
);
.zkey файли для складних схем важать 10–500MB — це проблема для браузера. Рішення: розбити на частини (chunked zkey) або використовувати потокове завантаження.
Генерування на сервері (proving service)
Для схем з великою кількістю обмежень (>1M) — клієнт не впорається. Архітектура:
Клієнт → API (створення завдання) → Queue (Bull/RabbitMQ) → Prover Worker → S3 (proof) → Webhook
Prover worker — Go сервіс з gnark або Rust з bellman/arkworks. Горизонтальне масштабування: кожен worker незалежний, завдання ідемпотентні.
Для zkEVM-рівневих схем (мільярди обмежень) — GPU proving через CUDA. Прискорення 100–1000x в порівнянні з CPU. Провайдери: Ingonyama, Ulvetanna.
Trusted Setup Ceremony
Для Groth16 — обов'язкова. Процес:
- Універсальна Powers of Tau (беремо готову від Hermez/EthSnarks — це публічно верифіковані параметри до певного розміру)
- Phase 2 церемонія специфічна для вашої схеми: кожен учасник додає свою випадковість
- Final beacon — публічне джерело випадковості (Bitcoin block hash)
Якщо хоча б один учасник чесний — параметри безпечні. Для production проектів: мінімум 10–20 учасників, публічна верифікація трансцепту.
Часові рамки та обсяг робіт
| Фаза | Зміст | Тривалість |
|---|---|---|
| Специфікація схеми | Формалізація завдання, вибір proof-системи, проектування public/private inputs | 1 тиждень |
| Розробка circuit | Написання circom/gnark/noir, unit тести обмежень | 2–4 тижні |
| Аудит circuits | Пошук under-constrained сигналів, перевірка soundness | 1–2 тижні |
| Верифікаційний контракт | Solidity верифікатор + nullifier логіка + інтеграція з протоколом | 1–2 тижні |
| Інфраструктура прувера | WASM збірка або серверний прувер, API | 1–2 тижні |
| Trusted setup | Організація церемонії (якщо Groth16/PLONK-KZG) | 1 тиждень |
Всього для типового ZKP застосунку (proof of membership, zkKYC, приватні транзакції): 6–12 тижнів від специфікації до mainnet. Складні zkRollup-подібні системи — 6–18 місяців для команди.







