Разработка Zero-Knowledge Proof приложений
Большинство проектов, которые приходят с запросом на ZKP, столкнулись с одной из двух проблем: либо нужно доказать факт без раскрытия данных (возраст, баланс, принадлежность к множеству), либо нужно перенести тяжёлые вычисления off-chain с верификацией on-chain. Это разные задачи с разными инструментальными стеками, и путать их — первая и самая дорогостоящая ошибка на старте.
Выбор proof-системы: это не академический вопрос
Выбор между Groth16, PLONK, STARK, Halo2 и FRI определяет всё: размер proof, время генерации, наличие trusted setup, стоимость верификации on-chain.
Сравнение основных систем
| Система | Trusted setup | Proof size | Verification cost (EVM) | Prover time | Рекурсия |
|---|---|---|---|---|---|
| Groth16 | Да (per-circuit) | ~200 bytes | ~270k gas | Быстрый | Сложная |
| PLONK (KZG) | Да (универсальный) | ~800 bytes | ~400k gas | Средний | Проще |
| PLONK (IPA) | Нет | ~1.5KB | Дороже | Медленный | Хорошая |
| STARK | Нет | 40–200KB | Очень дорого в EVM | Медленный | Отличная |
| Halo2 | Нет | ~1–5KB | Не нативный | Средний | Встроена |
Groth16 — выбор для production систем с фиксированной схемой и требованиями к минимальному газу. Используют: Tornado Cash (был), Zcash Sapling, большинство zkSNARK мостов. Минус: каждое изменение circuit требует новой ceremony.
PLONK с KZG — де-факто стандарт для zkRollup-подобных систем. Gnosis, zkSync Lite, Polygon Hermez используют варианты PLONK. Универсальный trusted setup (Powers of Tau) переиспользуется — не нужна ceremony под каждую схему.
STARKs — выбор для задач, где нет trusted setup и нужна рекурсия: StarkNet, Cairo VM. Огромный proof размер — верификация в EVM нативно нецелесообразна, нужен отдельный verifier контракт или L3-подход.
Halo2 — используют Zcash Orchard, Scroll. Не требует trusted setup, встроенная рекурсия. Tooling менее зрелый, экосистема меньше, но активно развивается.
Для большинства практических задач (приватное голосование, proof of membership, zkKYC, age verification) — Groth16 через circom/snarkjs или PLONK через gnark/noir — правильная отправная точка.
Разработка circuits: где сидят настоящие баги
Circom и его подводные камни
Circom — DSL для написания arithmetic circuits. Circuit компилируется в R1CS (Rank-1 Constraint System), затем через snarkjs или rapidsnark генерируется proof.
Базовая схема для доказательства знания preim хэша:
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 (хранится on-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. Если сигнал используется в вычислении, но не имеет достаточного количества constraints — прувер может передать произвольное значение, и верификатор примет proof.
Пример уязвимого кода:
// УЯЗВИМО: нет constraint что 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, потому что нет constraints, связывающих out с in.
Overflow в field arithmetic. Circom работает в простом поле p = 21888242871839275222246405745257275088548364400416034343698204186575808495617. Любая операция выполняется по модулю p. Если входные данные — числа из реального мира (возраст, timestamp) — диапазон безопасен. Но при умножении больших чисел нужен явный range check через 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–30x быстрее snarkjs в prover time для одинаковых схем. Для production с реальными пользователями это важно: генерация proof в браузере через WASM занимает 3–15 секунд на Groth16 схеме умеренной сложности, в Go сервере — 0.1–1 секунду.
On-chain верификация
Solidity verifier генерируется автоматически — 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 = детерминированный хэш от секретного ввода, который невозможно связать с идентичностью, но можно проверить на уникальность.
Gas cost верификации Groth16 — около 270k gas. На Ethereum mainnet при 20 gwei это ~$2–5 за верификацию. Для систем с высокой частотой — деплой на L2 (Arbitrum, Base) снижает стоимость в 10–50x.
Инфраструктура для proof generation
Клиентская генерация (браузер)
Подходит для: wallet-уровневых операций, единичных доказательств. Используется 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) или использовать streaming загрузку.
Серверная генерация (proving service)
Для схем с большим числом constraints (>1M) — клиент не справится. Архитектура:
Client → API (создание задачи) → Queue (Bull/RabbitMQ) → Prover Worker → S3 (proof) → Webhook
Prover worker — Go сервис с gnark или Rust с bellman/arkworks. Горизонтальное масштабирование: каждый worker независим, задачи идемпотентны.
Для zkEVM-уровневых схем (миллиарды constraints) — GPU proving через CUDA. Ускорение в 100–1000x по сравнению с CPU. Провайдеры: Ingonyama, Ulvetanna.
Trusted Setup Ceremony
Для Groth16 — обязательна. Процесс:
- Универсальный Powers of Tau (берём готовый от Hermez/EthSnarks — это публично верифицированные параметры до определённого размера)
- Phase 2 ceremony специфична для вашей схемы: каждый участник добавляет свою рандомность
- Final beacon — публичный рандомный источник (Bitcoin block hash)
Если хотя бы один участник честен — параметры безопасны. Для production проектов: минимум 10–20 участников, публичная верификация транскрипта.
Сроки и объём работ
| Фаза | Содержание | Срок |
|---|---|---|
| Спецификация схемы | Формализация задачи, выбор proof-системы, проектирование public/private inputs | 1 нед |
| Circuit разработка | Написание circom/gnark/noir, unit тесты constraints | 2–4 нед |
| Аудит circuits | Поиск under-constrained сигналов, проверка soundness | 1–2 нед |
| Verifier контракт | Solidity verifier + nullifier логика + интеграция с основным протоколом | 1–2 нед |
| Prover инфраструктура | WASM сборка или серверный прувер, API | 1–2 нед |
| Trusted setup | Организация ceremony (если Groth16/PLONK-KZG) | 1 нед |
Итого для типичного ZKP приложения (proof of membership, zkKYC, приватные транзакции): 6–12 недель от спецификации до mainnet. Сложные zkRollup-подобные системы — 6–18 месяцев команды.







