UNPKG

export-ton-verifier

Version:

Tool for generating Groth16 and PLONK verifier code for the TON blockchain from SnarkJS .zkey or verification key JSON files.

428 lines (401 loc) 15.1 kB
#!/usr/bin/env node import { proofToMessageBoc } from "./chunk-OYYC4SR3.js"; import { formatDoctorReport, inspectVerifierInput } from "./chunk-K5M5T7RZ.js"; import "./chunk-SFKUOXLN.js"; import { generateVerifier } from "./chunk-4RQ5LN5R.js"; import "./chunk-LXOLCHR7.js"; import "./chunk-6TDHKTNH.js"; import "./chunk-5MKK56I7.js"; import "./chunk-TB4DFXWQ.js"; import "./chunk-MCE4ZZVW.js"; import "./chunk-IELVU6ER.js"; import "./chunk-I7S4EEHA.js"; import "./chunk-4I2ZWZKN.js"; // src/cli.js import path3 from "path"; import { fileURLToPath } from "url"; // src/cli/commands.js import fsp2 from "fs/promises"; import path2 from "path"; // src/cli/errors.js var CliUsageError = class extends Error { constructor(message, { showHelp = false } = {}) { super(message); this.name = "CliUsageError"; this.showHelp = showHelp; } }; // src/cli/help.js var HELP_TEXT = ` Usage: export-ton-verifier <inputPath> <outputPath> [--func | --tolk | --tact] [--contract-name <name>] [--wrapper-dest <destPath>] [--groth16 | --plonk] [--force] export-ton-verifier doctor <inputPath> [--func | --tolk | --tact] [--json] export-ton-verifier proof-to-message <proof.json> <public.json> [--func | --tolk] [--groth16 | --plonk] [--format base64|hex] [--output <file>] export-ton-verifier import-wrapper <destPath> [--func | --tolk] [--groth16 | --plonk] [--force] Description: 1) Generates a TON verifier smart contract from .zkey, snarkjs JSON, gnark, or arkworks artifacts. Protocol (Groth16/PLONK) is auto-detected from the input artifact. 2) Diagnoses verifier artifacts before generation with the 'doctor' subcommand. 3) Converts proof/public JSON into supported internal message body BOCs. 4) (Optional) Can also copy a TypeScript wrapper template to your project. Wrapper selection supports language- and protocol-specific files. Notes: - Tolk is the default for Groth16 and PLONK verifier generation. Use --func to generate FunC output. - If language is Tact (--tact), wrapper copy is skipped even if --wrapper-dest is provided. Arguments: inputPath Path to .zkey, snarkjs verification_key.json, gnark JSON/.bin, or arkworks JSON outputPath Path to save the generated verifier file Subcommands: doctor Reports input format, protocol, curve, public input count, template support, and TVM limits. proof-to-message Converts proof/public JSON into a base64 or hex internal message body BOC. import-wrapper Copies a language-specific wrapper to <destPath> (file or directory). If <destPath> is a directory, the file will be saved as <destPath>/Verifier.ts. Requires one of --groth16 or --plonk. Options: -h, --help Show this help message and exit --func Use FunC language template for the verifier --tolk Use Tolk language template for the verifier [default] --tact Use Tact language template for the verifier --contract-name <name> Wrap the generated Tolk verifier in a named struct receiver --wrapper-dest <destPath> After generation, copy a language-specific TypeScript wrapper to <destPath> --groth16 Protocol hint for wrapper selection (wrapper only; verifier protocol is auto-detected) --plonk Protocol hint for wrapper selection (wrapper only; verifier protocol is auto-detected) --json Print machine-readable JSON for 'doctor' --format <base64|hex> Encoding for 'proof-to-message' output [default: base64] --output <file> Write 'proof-to-message' output to a file --force Overwrite existing file when used with 'import-wrapper' or '--wrapper-dest' Examples: # Generate Tolk verifier from .zkey (default language for Groth16 and PLONK) npx export-ton-verifier ./circuits/verifier.zkey ./verifier.tolk # Generate from verification_key.json npx export-ton-verifier ./circuits/verification_key.json ./verifier.tolk # Generate from native BLS12-381 gnark JSON or binary npx export-ton-verifier ./circuits/verification_key_gnark.json ./verifier.tolk npx export-ton-verifier ./circuits/verification_key.bin ./verifier.tolk # Generate from native BLS12-381 arkworks compressed JSON/bundle npx export-ton-verifier ./circuits/groth16_artifacts.json ./verifier.tolk # Generate FunC verifier (requires --func) npx export-ton-verifier ./circuits/verifier.zkey ./verifier.fc --func # Generate Tact verifier npx export-ton-verifier ./circuits/verifier.zkey ./verifier.tact --tact # Generate and also drop a wrapper (auto-detect protocol from the input artifact) npx export-ton-verifier ./circuits/verification_key.json ./verifier.tolk --wrapper-dest ./wrappers/ --force # Generate a named Tolk verifier receiver with getter verify_MultiplierVerifier npx export-ton-verifier ./circuits/verifier.zkey ./verifier.tolk --contract-name multiplierVerifier # Diagnose generation support npx export-ton-verifier doctor ./circuits/verification_key.json --tolk npx export-ton-verifier doctor ./circuits/verification_key.json --json # Build internal message body BOC from proof/public JSON npx export-ton-verifier proof-to-message ./circuits/proof.json ./circuits/public.json --groth16 --tolk npx export-ton-verifier proof-to-message ./circuits/proof.json ./circuits/public.json --plonk --tolk --format hex --output ./body.hex # Copy the Tolk Groth16 wrapper (default wrapper language) npx export-ton-verifier import-wrapper ./wrappers/ --groth16 --force # Copy the PLONK TypeScript wrapper with the default language selection npx export-ton-verifier import-wrapper ./wrappers/ --plonk --force # Copy the PLONK TypeScript wrapper for FunC npx export-ton-verifier import-wrapper ./wrappers/ --plonk --func --force `; function printHelp(stdout = process.stdout) { stdout.write(`${HELP_TEXT} `); } // src/cli/options.js function parseLangFlag(args) { const allowed = ["--func", "--tolk", "--tact"]; const chosen = allowed.filter((flag) => args.includes(flag)); if (chosen.length > 1) { throw new CliUsageError("Use only one of --func, --tolk, --tact."); } if (chosen.length === 0) return "tolk"; return chosen[0].slice(2); } function parseProtocolFlag(args, { required = false } = {}) { const isGroth = args.includes("--groth16"); const isPlonk = args.includes("--plonk"); if (isGroth && isPlonk) { throw new CliUsageError("Use only one of --groth16 or --plonk."); } if (!isGroth && !isPlonk) { if (required) { throw new CliUsageError("Missing protocol flag: use --groth16 or --plonk."); } return null; } return isGroth ? "groth16" : "plonk"; } function getFlagValue(args, flag) { const idx = args.indexOf(flag); if (idx === -1) return null; const value = args[idx + 1]; if (!value || value.startsWith("--")) { throw new CliUsageError(`Missing value for ${flag}.`); } return value; } function positionalArgs(args, { valueFlags = [] } = {}) { const flagsWithValues = new Set(valueFlags); const positional = []; for (let idx = 0; idx < args.length; idx += 1) { const arg = args[idx]; if (arg.startsWith("--")) { if (flagsWithValues.has(arg) && args[idx + 1] && !args[idx + 1].startsWith("--")) { idx += 1; } continue; } positional.push(arg); } return positional; } // src/cli/wrappers.js import fs from "fs"; import fsp from "fs/promises"; import path from "path"; function fileExists(p) { try { return fs.existsSync(p); } catch { return false; } } async function ensureDir(dir) { await fsp.mkdir(dir, { recursive: true }); } async function copyWrapper(wrapperSrc, destPath, { force = false, stdout = process.stdout } = {}) { if (!destPath) throw new CliUsageError("Missing <destPath> for wrapper copy."); let stat = null; try { stat = await fsp.stat(destPath); } catch { } let finalDest = destPath; if ((stat == null ? void 0 : stat.isDirectory()) || hasTrailingPathSeparator(destPath)) { await ensureDir(destPath); finalDest = path.join(destPath, "Verifier.ts"); } else { await ensureDir(path.dirname(destPath)); } if (fileExists(finalDest) && !force) { throw new Error( `Wrapper already exists: ${finalDest}. Use --force to overwrite.` ); } const content = await fsp.readFile(wrapperSrc); await fsp.writeFile(finalDest, content); stdout.write(`Wrapper copied to: ${finalDest} `); } function hasTrailingPathSeparator(destPath) { return /[\\/]$/.test(destPath); } function wrapperTemplateCandidates(lang, protocol) { switch (protocol) { case "groth16": case "plonk": return [`Verifier_${lang}_${protocol}.ts`, `Verifier_${protocol}.ts`]; default: throw new CliUsageError(`Unknown protocol for wrapper: ${protocol}`); } } function resolveWrapperTemplate(templatesDir, lang, protocol) { if (lang === "tact") { throw new CliUsageError("TypeScript wrappers are not available for Tact."); } for (const file of wrapperTemplateCandidates(lang, protocol)) { const fullPath = path.join(templatesDir, file); if (fileExists(fullPath)) { return fullPath; } } throw new CliUsageError( `Wrapper template not found for language '${lang}' and protocol '${protocol}'.` ); } // src/cli/commands.js function writeLine(stream, message = "") { stream.write(`${message} `); } function stdoutLogger(stdout) { return { log(message) { writeLine(stdout, message); } }; } function normalizeEnv(env) { return { stdout: env.stdout ?? process.stdout, stderr: env.stderr ?? process.stderr, templatesDir: env.templatesDir }; } function writeUsageError(err, ctx) { writeLine(ctx.stderr, `Error: ${err.message}`); if (err.showHelp) { printHelp(ctx.stdout); } } async function runDoctorCommand(args, ctx) { const [inputPath] = positionalArgs(args.slice(1)); const json = args.includes("--json"); if (!inputPath || inputPath.startsWith("--")) { const message = "Missing <inputPath> for 'doctor'."; if (json) { writeLine(ctx.stdout, JSON.stringify({ ok: false, error: message }, null, 2)); } else { writeLine(ctx.stderr, `Error: ${message}`); printHelp(ctx.stdout); } return 1; } try { const report = await inspectVerifierInput(inputPath, { lang: parseLangFlag(args), templatesDir: ctx.templatesDir }); if (json) { writeLine(ctx.stdout, JSON.stringify(report, null, 2)); } else { ctx.stdout.write(formatDoctorReport(report)); } return report.ok ? 0 : 1; } catch (err) { const message = (err == null ? void 0 : err.message) || String(err); if (json) { writeLine(ctx.stdout, JSON.stringify({ ok: false, error: message }, null, 2)); } else { writeLine(ctx.stderr, `Error: ${message}`); } return 1; } } async function runProofToMessageCommand(args, ctx) { const [proofPath, publicPath] = positionalArgs(args.slice(1), { valueFlags: ["--format", "--output"] }); if (!proofPath || proofPath.startsWith("--") || !publicPath || publicPath.startsWith("--")) { writeLine( ctx.stderr, "Error: Missing <proof.json> or <public.json> for 'proof-to-message'." ); printHelp(ctx.stdout); return 1; } try { const proof = JSON.parse(await fsp2.readFile(proofPath, "utf8")); const publicSignals = JSON.parse(await fsp2.readFile(publicPath, "utf8")); const format = getFlagValue(args, "--format") || "base64"; const outputPath = getFlagValue(args, "--output"); const encoded = await proofToMessageBoc({ proof, publicSignals, protocol: parseProtocolFlag(args, { required: false }), lang: parseLangFlag(args), format }); if (outputPath) { await ensureDir(path2.dirname(outputPath)); await fsp2.writeFile(outputPath, encoded, "utf8"); } else { writeLine(ctx.stdout, encoded); } return 0; } catch (err) { writeLine(ctx.stderr, `Error: ${(err == null ? void 0 : err.message) || err}`); return 1; } } async function runImportWrapperCommand(args, ctx) { const [destPath] = positionalArgs(args.slice(1)); if (!destPath || destPath.startsWith("--")) { throw new CliUsageError("Missing <destPath> for 'import-wrapper'.", { showHelp: true }); } const protocol = parseProtocolFlag(args, { required: true }); const lang = parseLangFlag(args); const force = args.includes("--force"); const wrapperSrc = resolveWrapperTemplate(ctx.templatesDir, lang, protocol); await copyWrapper(wrapperSrc, destPath, { force, stdout: ctx.stdout }); return 0; } async function runGenerateCommand(args, ctx) { const positional = positionalArgs(args, { valueFlags: ["--contract-name", "--wrapper-dest"] }); if (positional.length < 2) { throw new CliUsageError("Missing required arguments.", { showHelp: true }); } const [inputPath, outputPath] = positional; const lang = parseLangFlag(args); const contractName = getFlagValue(args, "--contract-name"); const wrapperDest = getFlagValue(args, "--wrapper-dest"); const force = args.includes("--force"); if (!fileExists(inputPath)) { throw new Error(`Input file not found: ${inputPath}`); } const detectedProtocol = await generateVerifier(inputPath, outputPath, { lang, templatesDir: ctx.templatesDir, contractName, logger: stdoutLogger(ctx.stdout) }); if (wrapperDest) { if (lang === "tact") { writeLine( ctx.stdout, "Tact selected - wrapper copy is skipped even though --wrapper-dest was provided." ); } else { const protocol = parseProtocolFlag(args, { required: false }) || detectedProtocol; const wrapperSrc = resolveWrapperTemplate(ctx.templatesDir, lang, protocol); await copyWrapper(wrapperSrc, wrapperDest, { force, stdout: ctx.stdout }); } } return 0; } async function runCli(args, env = {}) { const ctx = normalizeEnv(env); try { if (args.includes("--help") || args.includes("-h") || args.length === 0) { printHelp(ctx.stdout); return 0; } if (args.includes("--vk")) { throw new CliUsageError("--vk is no longer supported. Use a .json file directly."); } switch (args[0]) { case "doctor": return await runDoctorCommand(args, ctx); case "proof-to-message": return await runProofToMessageCommand(args, ctx); case "import-wrapper": return await runImportWrapperCommand(args, ctx); default: return await runGenerateCommand(args, ctx); } } catch (err) { if (err instanceof CliUsageError) { writeUsageError(err, ctx); } else { writeLine(ctx.stderr, `Error: ${(err == null ? void 0 : err.message) || err}`); } return 1; } } // src/cli.js var __dirname = path3.dirname(fileURLToPath(import.meta.url)); var status = await runCli(process.argv.slice(2), { templatesDir: path3.join(__dirname, "../templates") }); process.exit(status);