UNPKG

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
#!/usr/bin/env node // 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();