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.

617 lines (608 loc) 19.5 kB
import { normalizePlonkVerificationKey, zkeyExportPlonkVerificationKey } from "./chunk-5MKK56I7.js"; // src/logger.js function createLogger({ logger = null, quiet = false } = {}) { if (quiet || logger === false || logger == null) { return { log() { } }; } if (typeof logger === "function") { return { log: logger }; } if (typeof logger.log === "function") { return { log: logger.log.bind(logger) }; } throw new Error( "logger must be a function, an object with log(message), false, null, or undefined." ); } // src/formats/snarkjs.js import { zKey } from "snarkjs"; var SNARKJS_JSON_SOURCE = "snarkjs-json"; var SNARKJS_ZKEY_SOURCE = "snarkjs-zkey"; var GROTH16_REQUIRED_FIELDS = [ "vk_alpha_1", "vk_beta_2", "vk_gamma_2", "vk_delta_2" ]; var PLONK_REQUIRED_FIELDS = [ "nPublic", "power", "k1", "k2", "Qm", "Ql", "Qr", "Qo", "Qc", "S1", "S2", "S3", "X_2" ]; function normalizeSnarkjsVerificationKey(vkRaw) { if (!(vkRaw == null ? void 0 : vkRaw.protocol)) { throw new Error("verification_key: missing 'protocol'."); } if (!vkRaw.curve) { throw new Error("verification_key: missing 'curve'."); } ensureSupportedProtocol(vkRaw.protocol, "JSON"); validateSnarkjsVerificationKeyShape(vkRaw); return vkRaw; } async function loadSnarkjsZkeyVerificationKey(inputPath, { logger = null } = {}) { var _a; (_a = logger == null ? void 0 : logger.log) == null ? void 0 : _a.call(logger, "Loading verification key from .zkey..."); const vkRaw = await zKey.exportVerificationKey(inputPath); ensureSupportedProtocol(vkRaw.protocol, ".zkey"); return vkRaw; } async function loadSnarkjsPlonkTemplateVerificationKey(verifierInput) { if (verifierInput.sourceFormat === SNARKJS_ZKEY_SOURCE) { return zkeyExportPlonkVerificationKey(verifierInput.inputPath); } return normalizePlonkVerificationKey(verifierInput.verificationKey); } function ensureSupportedProtocol(protocol, source) { if (protocol !== "groth16" && protocol !== "plonk") { throw new Error( `Only Groth16 and PLONK are supported from ${source} (got '${protocol}').` ); } } function validateSnarkjsVerificationKeyShape(vkRaw) { if (vkRaw.protocol === "groth16") { requireFields(vkRaw, GROTH16_REQUIRED_FIELDS); if (!Array.isArray(vkRaw.IC) || vkRaw.IC.length === 0) { throw new Error("verification_key: 'IC' must be a non-empty array."); } return; } requireFields(vkRaw, PLONK_REQUIRED_FIELDS); } function requireFields(value, fields) { for (const field of fields) { if (value[field] === void 0 || value[field] === null) { throw new Error(`verification_key: missing '${field}'.`); } } } // src/formats/gnark.js import fs2 from "fs/promises"; // src/formats/common.js import fs from "fs/promises"; import { buildBls12381 } from "ffjavascript"; var BLS12381_CURVE_NAME = "bls12381"; function normalizeBls12381CurveName(raw, field = "curve") { if (typeof raw !== "string" || raw.trim() === "") { throw new Error(`${field}: BLS12-381 curve metadata is required.`); } const normalized = raw.toLowerCase().replace(/[^a-z0-9]/g, ""); if (normalized !== "bls12381") { throw new Error( `${field}: only BLS12-381 artifacts are supported for gnark/arkworks inputs (got '${raw}').` ); } return BLS12381_CURVE_NAME; } function decimalString(value, field) { if (typeof value === "bigint") return value.toString(); if (typeof value === "number" && Number.isInteger(value)) return String(value); if (typeof value === "string") { const trimmed = value.trim(); if (/^[0-9]+$/.test(trimmed)) return trimmed; } throw new Error(`${field} must be a decimal string or integer.`); } function requireField(value, field) { if (value === void 0 || value === null) { throw new Error(`verification key: missing '${field}'.`); } return value; } function g1Point(x, y, field) { return [ decimalString(x, `${field}.X`), decimalString(y, `${field}.Y`), "1" ]; } function g2Point(x0, x1, y0, y1, field) { return [ [ decimalString(x0, `${field}.X.A0`), decimalString(x1, `${field}.X.A1`) ], [ decimalString(y0, `${field}.Y.A0`), decimalString(y1, `${field}.Y.A1`) ], ["1", "0"] ]; } function makeGroth16VerificationKey({ curve = BLS12381_CURVE_NAME, vk_alpha_1, vk_beta_2, vk_gamma_2, vk_delta_2, IC }) { if (!Array.isArray(IC) || IC.length === 0) { throw new Error("verification key: IC/G1.K must contain at least one point."); } return { protocol: "groth16", curve, vk_alpha_1, vk_beta_2, vk_gamma_2, vk_delta_2, IC, nPublic: IC.length - 1 }; } async function readJson(path, label) { const content = await fs.readFile(path, "utf8"); try { return JSON.parse(content); } catch (err) { throw new Error(`invalid ${label} JSON in ${path}: ${err.message}`); } } function decodeHex(raw, field) { if (typeof raw !== "string") { throw new Error(`${field} must be a hex string.`); } const hex = raw.trim().replace(/^0x/i, ""); if (hex.length === 0) throw new Error(`${field} must not be empty.`); if (hex.length % 2 !== 0) { throw new Error(`${field} has odd hex length.`); } if (!/^[0-9a-fA-F]+$/.test(hex)) { throw new Error(`${field} must be a hex string.`); } return Uint8Array.from(Buffer.from(hex, "hex")); } async function withBls12381(run) { const curve = await buildBls12381(); try { return await run(curve); } finally { if (curve && typeof curve.terminate === "function") { await curve.terminate(); } } } function decodeBls12381G1(curve, bytes, field) { const point = decodeBls12381Point(curve.G1, bytes, 48, field); return curvePointToObjectArray(curve.G1, point); } function decodeBls12381G2(curve, bytes, field) { const point = decodeBls12381Point(curve.G2, bytes, 96, field); return curvePointToObjectArray(curve.G2, point); } function curvePointToObjectArray(curveGroup, point) { return stringifyBigIntsDeep(curveGroup.toObject(point)); } function stringifyBigIntsDeep(value) { if (typeof value === "bigint") return value.toString(); if (Array.isArray(value)) return value.map(stringifyBigIntsDeep); return value; } function decodeBls12381Point(group, bytes, compressedSize, field) { const first = bytes[0]; if (first === void 0) { throw new Error(`${field}: empty point encoding.`); } const flag = first & 224; const compressed = flag === 128 || flag === 160 || flag === 192; const uncompressed = flag === 0 || flag === 64; if (!compressed && !uncompressed) { throw new Error( `${field}: unsupported gnark/arkworks point flag 0x${flag.toString(16)}.` ); } if (compressed) { if (bytes.length !== compressedSize) { throw new Error( `${field}: expected ${compressedSize} compressed bytes, got ${bytes.length}.` ); } if (flag === 192) return group.zero; return group.fromRprCompressed(toFfjavascriptCompressed(bytes), 0); } if (bytes.length !== compressedSize * 2) { throw new Error( `${field}: expected ${compressedSize * 2} uncompressed bytes, got ${bytes.length}.` ); } if (flag === 64) return group.zero; return group.fromRprUncompressed(bytes, 0); } function toFfjavascriptCompressed(bytes) { const converted = Uint8Array.from(bytes); const sign = converted[0] & 32; converted[0] &= 31; if (sign) converted[0] |= 128; return converted; } var BinaryReader = class { constructor(bytes, sourceName) { this.bytes = bytes instanceof Uint8Array ? bytes : Uint8Array.from(bytes); this.sourceName = sourceName; this.offset = 0; } readBytes(len, field) { const end = this.offset + len; if (end > this.bytes.length) { throw new Error( `${this.sourceName} ended while reading ${field}: need ${len} bytes at offset ${this.offset}, remaining ${this.bytes.length - this.offset}.` ); } const slice = this.bytes.slice(this.offset, end); this.offset = end; return slice; } peekByte(field) { const value = this.bytes[this.offset]; if (value === void 0) { throw new Error(`${this.sourceName} ended before reading ${field}.`); } return value; } readU32BE(field) { const bytes = this.readBytes(4, field); return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32( 0, false ); } readU64BE(field) { const bytes = this.readBytes(8, field); return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getBigUint64( 0, false ); } readU64LE(field) { const bytes = this.readBytes(8, field); return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getBigUint64( 0, true ); } finish(label = "artifact") { if (this.offset !== this.bytes.length) { throw new Error( `${this.sourceName} has ${this.bytes.length - this.offset} trailing bytes after ${label}.` ); } } }; // src/formats/gnark.js var BLS_FIELD_BYTES = 48; var GNARK_FLAG_MASK = 224; var GNARK_COMPRESSED_FLAGS = /* @__PURE__ */ new Set([128, 160, 192]); var GNARK_UNCOMPRESSED_FLAGS = /* @__PURE__ */ new Set([0, 64]); async function normalizeGnarkJsonVerificationKey(value) { const g1 = requireField(value == null ? void 0 : value.G1, "G1"); const g2 = requireField(value == null ? void 0 : value.G2, "G2"); ensureNoVerificationKeyCommitments(value); const k = requireField(g1.K, "G1.K"); if (!Array.isArray(k)) { throw new Error("verification key: G1.K must be an array."); } const verificationKey = makeGroth16VerificationKey({ curve: BLS12381_CURVE_NAME, vk_alpha_1: g1FromJson(requireField(g1.Alpha, "G1.Alpha"), "G1.Alpha"), vk_beta_2: g2FromJson(requireField(g2.Beta, "G2.Beta"), "G2.Beta"), vk_gamma_2: g2FromJson(requireField(g2.Gamma, "G2.Gamma"), "G2.Gamma"), vk_delta_2: g2FromJson(requireField(g2.Delta, "G2.Delta"), "G2.Delta"), IC: k.map((point, idx) => g1FromJson(point, `G1.K[${idx}]`)) }); await assertBls12381VerificationKeyPoints(verificationKey); return verificationKey; } async function loadGnarkBinaryVerificationKey(path) { const bytes = await fs2.readFile(path); return normalizeGnarkBinaryVerificationKey(Uint8Array.from(bytes), path); } async function normalizeGnarkBinaryVerificationKey(bytes, sourceName = "gnark binary VK") { return withBls12381((curve) => { const reader = new BinaryReader(bytes, sourceName); const alpha = readG1(reader, curve, "G1.Alpha"); readG1(reader, curve, "G1.Beta"); const beta = readG2(reader, curve, "G2.Beta"); const gamma = readG2(reader, curve, "G2.Gamma"); readG1(reader, curve, "G1.Delta"); const delta = readG2(reader, curve, "G2.Delta"); const icLen = reader.readU32BE("G1.K length"); const IC = []; for (let idx = 0; idx < icLen; idx += 1) { IC.push(readG1(reader, curve, `G1.K[${idx}]`)); } const publicAndCommitmentCommitted = readU64SliceSlice( reader, "PublicAndCommitmentCommitted" ); const commitmentKeysLen = reader.readU32BE("CommitmentKeys length"); if (publicAndCommitmentCommitted.length !== 0 || commitmentKeysLen !== 0) { throw new Error( "gnark commitment keys are not supported by TON verifier generation." ); } reader.finish("gnark verification key"); return makeGroth16VerificationKey({ curve: BLS12381_CURVE_NAME, vk_alpha_1: alpha, vk_beta_2: beta, vk_gamma_2: gamma, vk_delta_2: delta, IC }); }); } function ensureNoVerificationKeyCommitments(value) { const commitmentKeys = value == null ? void 0 : value.CommitmentKeys; const publicAndCommitmentCommitted = value == null ? void 0 : value.PublicAndCommitmentCommitted; if (!isEmptyCommitmentValue(commitmentKeys) || !isEmptyCommitmentValue(publicAndCommitmentCommitted)) { throw new Error( "gnark commitment keys are not supported by TON verifier generation." ); } } function isEmptyCommitmentValue(value) { return value === void 0 || value === null || Array.isArray(value) && value.length === 0; } function g1FromJson(point, field) { return g1Point( requireField(point == null ? void 0 : point.X, `${field}.X`), requireField(point == null ? void 0 : point.Y, `${field}.Y`), field ); } function g2FromJson(point, field) { var _a, _b, _c, _d; return g2Point( requireField((_a = point == null ? void 0 : point.X) == null ? void 0 : _a.A0, `${field}.X.A0`), requireField((_b = point == null ? void 0 : point.X) == null ? void 0 : _b.A1, `${field}.X.A1`), requireField((_c = point == null ? void 0 : point.Y) == null ? void 0 : _c.A0, `${field}.Y.A0`), requireField((_d = point == null ? void 0 : point.Y) == null ? void 0 : _d.A1, `${field}.Y.A1`), field ); } async function assertBls12381VerificationKeyPoints(vk) { await withBls12381((curve) => { assertBls12381Point(curve.G1, vk.vk_alpha_1, "G1.Alpha"); assertBls12381Point(curve.G2, vk.vk_beta_2, "G2.Beta"); assertBls12381Point(curve.G2, vk.vk_gamma_2, "G2.Gamma"); assertBls12381Point(curve.G2, vk.vk_delta_2, "G2.Delta"); vk.IC.forEach((point, idx) => { assertBls12381Point(curve.G1, point, `G1.K[${idx}]`); }); }); } function assertBls12381Point(group, point, field) { let parsed; try { parsed = group.fromObject(bigintPoint(point)); } catch (err) { throw new Error( `${field}: expected a valid BLS12-381 point (${(err == null ? void 0 : err.message) || err}).` ); } if (!group.isValid(parsed)) { throw new Error(`${field}: point is not on BLS12-381.`); } } function bigintPoint(value) { if (Array.isArray(value)) return value.map(bigintPoint); if (typeof value === "bigint") return value; return BigInt(value); } function readG1(reader, curve, field) { const bytes = reader.readBytes(pointLength(reader, BLS_FIELD_BYTES, field), field); return decodeBls12381G1(curve, bytes, field); } function readG2(reader, curve, field) { const bytes = reader.readBytes( pointLength(reader, BLS_FIELD_BYTES * 2, field), field ); return decodeBls12381G2(curve, bytes, field); } function pointLength(reader, compressedSize, field) { const flag = reader.peekByte(field) & GNARK_FLAG_MASK; if (GNARK_COMPRESSED_FLAGS.has(flag)) return compressedSize; if (GNARK_UNCOMPRESSED_FLAGS.has(flag)) return compressedSize * 2; throw new Error( `${field}: unsupported gnark point flag 0x${flag.toString(16).padStart(2, "0")}.` ); } function readU64SliceSlice(reader, field) { const outerLen = reader.readU32BE(`${field} length`); const outer = []; for (let outerIdx = 0; outerIdx < outerLen; outerIdx += 1) { const innerLen = reader.readU32BE(`${field}[${outerIdx}] length`); const inner = []; for (let innerIdx = 0; innerIdx < innerLen; innerIdx += 1) { inner.push(reader.readU64BE(`${field}[${outerIdx}][${innerIdx}]`)); } outer.push(inner); } return outer; } // src/formats/arkworks.js var G1_COMPRESSED_BYTES = 48; var G2_COMPRESSED_BYTES = 96; async function normalizeArkworksVerificationKey(value) { const curve = parseCurve(value); const vkHex = firstString(value, ["vk", "verification_key", "verifying_key"], "vk"); return decodeArkworksVerificationKey(vkHex, curve); } async function decodeArkworksVerificationKey(raw, curveName = BLS12381_CURVE_NAME) { normalizeBls12381CurveName(curveName, "curve"); const bytes = decodeHex(raw, "vk"); return withBls12381((curve) => { const reader = new BinaryReader(bytes, "arkworks verification key"); const vk_alpha_1 = decodeBls12381G1( curve, reader.readBytes(G1_COMPRESSED_BYTES, "vk.alpha_g1"), "vk.alpha_g1" ); const vk_beta_2 = decodeBls12381G2( curve, reader.readBytes(G2_COMPRESSED_BYTES, "vk.beta_g2"), "vk.beta_g2" ); const vk_gamma_2 = decodeBls12381G2( curve, reader.readBytes(G2_COMPRESSED_BYTES, "vk.gamma_g2"), "vk.gamma_g2" ); const vk_delta_2 = decodeBls12381G2( curve, reader.readBytes(G2_COMPRESSED_BYTES, "vk.delta_g2"), "vk.delta_g2" ); const icLen = Number(reader.readU64LE("vk.gamma_abc_g1 length")); if (!Number.isSafeInteger(icLen)) { throw new Error("vk.gamma_abc_g1 length is too large."); } const IC = []; for (let idx = 0; idx < icLen; idx += 1) { IC.push( decodeBls12381G1( curve, reader.readBytes(G1_COMPRESSED_BYTES, `vk.gamma_abc_g1[${idx}]`), `vk.gamma_abc_g1[${idx}]` ) ); } reader.finish("arkworks verification key"); return makeGroth16VerificationKey({ curve: BLS12381_CURVE_NAME, vk_alpha_1, vk_beta_2, vk_gamma_2, vk_delta_2, IC }); }); } function parseCurve(value) { const curve = value == null ? void 0 : value.curve; if (!curve) { throw new Error("arkworks input requires curve metadata."); } return normalizeBls12381CurveName(curve, "curve"); } function firstString(value, keys, label) { for (const key of keys) { const candidate = value == null ? void 0 : value[key]; if (typeof candidate === "string") return candidate; } throw new Error(`arkworks input requires ${label} hex field.`); } // src/formats/index.js var GNARK_JSON_SOURCE = "gnark-json"; var GNARK_BINARY_SOURCE = "gnark-bin"; var ARKWORKS_JSON_SOURCE = "arkworks-json"; async function loadVerificationKey(inputPath, { logger = null, quiet = false } = {}) { const log = createLogger({ logger, quiet }); const nativeInput = await loadNativeVerificationKeyInput(inputPath); if (nativeInput) return nativeInput; return makeVerifierInput( inputPath, SNARKJS_ZKEY_SOURCE, await loadSnarkjsZkeyVerificationKey(inputPath, { logger: log }) ); } async function loadPlonkTemplateVerificationKey(verifierInput) { return loadSnarkjsPlonkTemplateVerificationKey(verifierInput); } async function loadNativeVerificationKeyInput(inputPath) { const lower = inputPath.toLowerCase(); if (lower.endsWith(".json")) { return loadJsonVerificationKey(inputPath); } if (lower.endsWith(".bin")) { return makeVerifierInput( inputPath, GNARK_BINARY_SOURCE, await loadGnarkBinaryVerificationKey(inputPath) ); } return null; } async function loadJsonVerificationKey(inputPath) { const raw = await readJson(inputPath, "verification key"); const errors = []; try { return makeVerifierInput( inputPath, SNARKJS_JSON_SOURCE, normalizeSnarkjsVerificationKey(raw) ); } catch (err) { errors.push(`snarkjs JSON: ${err.message}`); } try { return makeVerifierInput( inputPath, GNARK_JSON_SOURCE, await normalizeGnarkJsonVerificationKey(raw) ); } catch (err) { errors.push(`gnark JSON: ${err.message}`); } try { return makeVerifierInput( inputPath, ARKWORKS_JSON_SOURCE, await normalizeArkworksVerificationKey(raw) ); } catch (err) { errors.push(`arkworks JSON: ${err.message}`); } throw new Error( `could not auto-detect verification key JSON format: ${errors.join("; ")}` ); } function makeVerifierInput(inputPath, sourceFormat, verificationKey) { return { inputPath, sourceFormat, verificationKey }; } export { createLogger, loadVerificationKey, loadPlonkTemplateVerificationKey };