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
JavaScript
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
};