export-ton-verifier
Version:
Tool for generating a Groth16 verifier code for the TON blockchain from SnarkJS .zkey files.
295 lines (279 loc) • 10.3 kB
JavaScript
// src/cli.js
import fs2 from "fs";
import fsp from "fs/promises";
import path from "path";
import { fileURLToPath } from "url";
// src/generateVerifier.js
import fs from "fs/promises";
import ejs from "ejs";
import { zKey } from "snarkjs";
import { utils } from "ffjavascript";
// src/utils.js
import { buildBn128, buildBls12381 } from "ffjavascript";
async function getCurveFromName(name, options) {
let curve;
let singleThread = options && options.singleThread;
const normName = normalizeName(name);
if (["BN128", "BN254", "ALTBN128"].indexOf(normName) >= 0) {
curve = await buildBn128(singleThread);
} else if (["BLS12381"].indexOf(normName) >= 0) {
curve = await buildBls12381(singleThread);
} else {
throw new Error(`Curve not supported: ${name}`);
}
return curve;
function normalizeName(n) {
return n.toUpperCase().match(/[A-Za-z0-9]+/g).join("");
}
}
function toHexString(byteArray) {
return Array.from(
byteArray,
(byte) => ("0" + (byte & 255).toString(16)).slice(-2)
).join("");
}
function g1Compressed(curve, p1Raw) {
const p1 = curve.G1.fromObject(p1Raw);
const buff = new Uint8Array(48);
curve.G1.toRprCompressed(buff, 0, p1);
if (buff[0] & 128) buff[0] |= 32;
buff[0] |= 128;
return toHexString(buff);
}
function g2Compressed(curve, p2Raw) {
const p2 = curve.G2.fromObject(p2Raw);
const buff = new Uint8Array(96);
curve.G2.toRprCompressed(buff, 0, p2);
if (buff[0] & 128) buff[0] |= 32;
buff[0] |= 128;
return toHexString(buff);
}
// src/generateVerifier.js
var { unstringifyBigInts } = utils;
async function loadVerificationKey(inputPath, { forceJson = false, logger = console } = {}) {
const lower = inputPath.toLowerCase();
if (forceJson || lower.endsWith(".json")) {
logger.log("\u{1F4E6} Loading verification key from JSON...");
const raw = await fs.readFile(inputPath, "utf8");
const vkRaw2 = JSON.parse(raw);
if (!vkRaw2.protocol)
throw new Error("verification_key.json: missing 'protocol'.");
if (!vkRaw2.curve)
throw new Error("verification_key.json: missing 'curve'.");
if (vkRaw2.protocol !== "groth16") {
throw new Error(`Only Groth16 is supported (got '${vkRaw2.protocol}').`);
}
return vkRaw2;
}
logger.log("\u{1F4E6} Loading verification key from .zkey...");
const vkRaw = await zKey.exportVerificationKey(inputPath, logger);
if (vkRaw.protocol !== "groth16") {
throw new Error(`Only Groth16 is supported (got '${vkRaw.protocol}').`);
}
return vkRaw;
}
async function generateVerifier(inputPath, templatePath, outputPath, opts = {}) {
const logger = console;
const vkRaw = await loadVerificationKey(inputPath, {
forceJson: opts.forceJson,
logger
});
const vk = unstringifyBigInts(vkRaw);
const curve = await getCurveFromName(vkRaw.curve);
try {
logger.log("\u{1F504} Compressing points...");
const data = {
vk_alpha_1: g1Compressed(curve, vk.vk_alpha_1),
vk_beta_2: g2Compressed(curve, vk.vk_beta_2),
vk_gamma_2: g2Compressed(curve, vk.vk_gamma_2),
vk_delta_2: g2Compressed(curve, vk.vk_delta_2),
IC: vk.IC.map((x) => g1Compressed(curve, x)),
// nPublic можно взять из vkRaw.nPublic, но точнее считать по длине IC:
nPublic: vk.IC.length - 1,
publicInputKeyLen: 32,
protocol: vkRaw.protocol,
curve: vkRaw.curve
};
logger.log("\u{1F4C4} Rendering template...");
const template = await fs.readFile(templatePath, "utf8");
const rendered = ejs.render(template, data);
logger.log(`\u{1F4BE} Saving file: ${outputPath}`);
await fs.writeFile(outputPath, rendered, "utf8");
logger.log("\u2705 Done.");
} finally {
if (curve && typeof curve.terminate === "function") {
await curve.terminate();
}
}
}
// src/cli.js
var __filename = fileURLToPath(import.meta.url);
var __dirname = path.dirname(__filename);
function printHelp() {
console.log(`
Usage:
export-ton-verifier <inputPath> <outputPath> [--func | --tolk | --tact] [--vk] [--wrapper-dest <destPath>] [--force]
export-ton-verifier import-wrapper <destPath> [--force]
Description:
1) Generates a Groth16 verifier for the TON blockchain from:
- Circom .zkey file, or
- verification_key.json (snarkjs/gnark style)
2) (Optional) Can also copy a TypeScript wrapper template (templates/Verifier.ts)
to your project. By default, the wrapper is NOT imported anywhere; you decide its destination.
Arguments:
inputPath Path to the .zkey OR verification_key.json
outputPath Path to save the generated verifier file
Subcommands:
import-wrapper Copies templates/Verifier.ts to <destPath> (file or directory).
If <destPath> is a directory, the file will be saved as <destPath>/Verifier.ts.
Options:
-h, --help Show this help message and exit
--vk Force treat <inputPath> as verification_key.json (skip .zkey export)
--func Use FunC template (templates/func_verifier.ejs) [default]
--tolk Use Tolk template (templates/tolk_verifier.ejs)
--tact Use Tact template (templates/tact_verifier.ejs)
--wrapper-dest <destPath> Copy the TypeScript wrapper (templates/Verifier.ts) to <destPath> after generation
--force Overwrite existing file when used with 'import-wrapper' or '--wrapper-dest'
Examples:
# From .zkey, FunC by default:
npx export-ton-verifier ./circuits/verifier.zkey ./verifier.fc
# From verification_key.json (auto-detected by .json):
npx export-ton-verifier ./circuits/verification_key.json ./verifier.fc
# Force JSON mode (even if extension is not .json):
npx export-ton-verifier ./vk.txt ./verifier.tolk --tolk --vk
# Generate Tact verifier:
npx export-ton-verifier ./circuits/verifier.zkey ./verifier.tact --tact
# Generate and also drop the TypeScript wrapper:
npx export-ton-verifier ./circuits/verification_key.json ./verifier.fc --func --wrapper-dest ./wrappers/ --force
# Only copy the TypeScript wrapper:
npx export-ton-verifier import-wrapper ./wrappers/Verifier.ts --force
`);
}
function fileExists(filePath) {
try {
return fs2.existsSync(filePath);
} catch {
return false;
}
}
async function ensureDir(dir) {
await fsp.mkdir(dir, { recursive: true });
}
async function copyWrapper(wrapperSrc, destPath, { force = false } = {}) {
let stat = null;
try {
stat = await fsp.stat(destPath);
} catch {
}
let finalDest = destPath;
if (stat == null ? void 0 : stat.isDirectory()) {
finalDest = path.join(destPath, "Verifier.ts");
} else {
const parent = path.dirname(destPath);
await ensureDir(parent);
}
const exists = fileExists(finalDest);
if (exists && !force) {
throw new Error(
`Wrapper already exists: ${finalDest}. Use --force to overwrite.`
);
}
const content = await fsp.readFile(wrapperSrc);
await fsp.writeFile(finalDest, content);
console.log(`\u2705 Wrapper copied to: ${finalDest}`);
}
function parseTemplateFlag(args) {
const allowed = ["--func", "--tolk", "--tact"];
const chosen = allowed.filter((f) => args.includes(f));
if (chosen.length > 1) {
console.error(
"\u274C You cannot use more than one of --func, --tolk, --tact at the same time."
);
process.exit(1);
}
if (chosen.length === 0) return "func";
return chosen[0].slice(2);
}
function templateFileFor(lang) {
switch (lang) {
case "func":
return "func_verifier.ejs";
case "tolk":
return "tolk_verifier.ejs";
case "tact":
return "tact_verifier.ejs";
default:
console.error(`\u274C Unknown template language: ${lang}`);
process.exit(1);
}
}
async function main() {
const args = process.argv.slice(2);
if (args.includes("--help") || args.includes("-h") || args.length === 0) {
printHelp();
process.exit(0);
}
if (args[0] === "import-wrapper") {
const destPath = args[1];
if (!destPath) {
console.error("\u274C Missing <destPath> for 'import-wrapper'.");
printHelp();
process.exit(1);
}
const force2 = args.includes("--force");
const wrapperSrc = path.join(__dirname, "./templates/Verifier.ts");
if (!fileExists(wrapperSrc)) {
console.error(`\u274C Wrapper template not found: ${wrapperSrc}`);
process.exit(1);
}
try {
await copyWrapper(wrapperSrc, destPath, { force: force2 });
process.exit(0);
} catch (err) {
console.error("\u274C Error:", err.message || err);
process.exit(1);
}
}
const positional = args.filter((a) => !a.startsWith("--"));
const flags = args.filter((a) => a.startsWith("--"));
if (positional.length < 2) {
console.error("\u274C Missing required arguments.");
printHelp();
process.exit(1);
}
const [inputPath, outputPath] = positional;
const lang = parseTemplateFlag(flags);
const templateFilename = templateFileFor(lang);
const templatePath = path.join(__dirname, `../templates/${templateFilename}`);
const wrapperDestIndex = flags.indexOf("--wrapper-dest");
const force = flags.includes("--force");
const wrapperDest = wrapperDestIndex >= 0 ? args[args.indexOf("--wrapper-dest") + 1] : null;
if (!fileExists(inputPath)) {
console.error(`\u274C Input file not found: ${inputPath}`);
process.exit(1);
}
if (!fileExists(templatePath)) {
console.error(`\u274C Template file not found: ${templatePath}`);
console.error(
` Make sure ${templateFilename} exists in ./templates (relative to this CLI).`
);
process.exit(1);
}
const forceJson = flags.includes("--vk");
try {
await generateVerifier(inputPath, templatePath, outputPath, { forceJson });
if (wrapperDest) {
const wrapperSrc = path.join(__dirname, "./templates/Verifier.ts");
if (!fileExists(wrapperSrc)) {
console.error(`\u274C Wrapper template not found: ${wrapperSrc}`);
process.exit(1);
}
await copyWrapper(wrapperSrc, wrapperDest, { force });
}
} catch (err) {
console.error("\u274C Error:", (err == null ? void 0 : err.message) ?? err);
process.exit(1);
}
}
main();