UNPKG

circomkit

Version:

A Circom testing & development environment

1,337 lines (1,323 loc) 46.7 kB
import * as snarkjs from 'snarkjs'; import { wasm, c } from 'circom_tester'; import { createWriteStream, readFileSync, openSync, readSync, existsSync, mkdirSync, writeFileSync, rmSync, renameSync } from 'fs'; import { rm, readFile, writeFile } from 'fs/promises'; import { randomBytes } from 'crypto'; import loglevel from 'loglevel'; import { get } from 'https'; import { AssertionError } from 'node:assert'; import path from 'node:path'; import fs from 'node:fs'; import { exec } from 'child_process'; const PTAU_URL_BASE = "https://storage.googleapis.com/zkevm/ptau"; function getPtauName(n) { const p = Math.ceil(Math.log2(n)); let id = ""; if (p < 8) { id = "_08"; } else if (p < 10) { id = `_0${p}`; } else if (p < 28) { id = `_${p}`; } else if (p === 28) { id = ""; } else { throw new Error("No PTAU for that many constraints!"); } return `powersOfTau28_hez_final${id}.ptau`; } function downloadPtau(ptauName, ptauDir) { const ptauPath = `${ptauDir}/${ptauName}`; const file = createWriteStream(ptauPath); return new Promise((resolve) => { get(`${PTAU_URL_BASE}/${ptauName}`, (response) => { response.pipe(file); file.on("finish", () => { file.close(); resolve(ptauPath); }); }); }); } var __async$4 = (__this, __arguments, generator) => { return new Promise((resolve, reject) => { var fulfilled = (value) => { try { step(generator.next(value)); } catch (e) { reject(e); } }; var rejected = (value) => { try { step(generator.throw(value)); } catch (e) { reject(e); } }; var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected); step((generator = generator.apply(__this, __arguments)).next()); }); }; class WitnessTester { constructor(circomTester) { this.circomTester = circomTester; } /** Assert that constraints are valid for a given witness. */ expectConstraintPass(witness) { return __async$4(this, null, function* () { return this.circomTester.checkConstraints(witness); }); } /** * Assert that constraints are NOT valid for a given witness. * This is useful to test if a fake witness (a witness from a * dishonest prover) can still be valid, which would indicate * that there are soundness errors in the circuit. */ expectConstraintFail(witness) { return __async$4(this, null, function* () { yield this.expectConstraintPass(witness).then( () => { throw new AssertionError({ message: "Expected constraints to not match." }); }, (err) => { if (err.message !== "Constraint doesn't match") { throw new AssertionError({ message: err.message }); } } ); }); } /** Compute witness given the input signals. */ calculateWitness(input) { return __async$4(this, null, function* () { return this.circomTester.calculateWitness(input, false); }); } /** Returns the number of constraints. */ getConstraintCount() { return __async$4(this, null, function* () { if (this.constraints === void 0) { yield this.loadConstraints(); } const numConstraints = this.constraints.length; return numConstraints; }); } /** Asserts that the circuit has enough constraints. * * By default, this function checks if there **at least** `expected` many constraints in the circuit. * If `exact` option is set to `true`, it will also check if the number of constraints is exactly equal to * the `expected` amount. * * If first check fails, it means the circuit is under-constrained. If the second check fails, it means * the circuit is over-constrained. */ expectConstraintCount(expected, exact) { return __async$4(this, null, function* () { const count = yield this.getConstraintCount(); if (count < expected) { throw new AssertionError({ message: "Circuit is under-constrained", expected, actual: count }); } if (exact && count !== expected) { throw new AssertionError({ message: "Circuit is over-constrained", expected, actual: count }); } }); } /** Expect a witness computation to fail in the circuit. * * See [here](https://github.com/iden3/circom/blob/master/code_producers/src/wasm_elements/common/witness_calculator.js#L21) * for the list of errors that may occur during witness calculation. * Most of the time, you will be expecting an assertion error. * * @returns the error message. */ expectFail(input) { return __async$4(this, null, function* () { return yield this.calculateWitness(input).then( () => { throw new AssertionError({ message: "Expected witness calculation to fail." }); }, (err) => { const errorMessage = err.message; const isExpectedError = [ "Error: Assert Failed.", // a constraint failure (most common) "Not enough values for input signal", // few inputs than expected for a signal "Too many values for input signal", // more inputs than expected for a signal "Not all inputs have been set." // few inputs than expected for many signals ].some((msg) => errorMessage.startsWith(msg)); if (!isExpectedError) throw err; return errorMessage; } ); }); } /** Expect an input to pass assertions and match the output. * * If `output` is omitted, it will only check for constraints to pass. */ expectPass(input, output) { return __async$4(this, null, function* () { const witness = yield this.calculateWitness(input); yield this.expectConstraintPass(witness); if (output) { yield this.assertOut(witness, output); } }); } /** * Computes the witness. * This is a shorthand for calculating the witness and calling {@link readWitnessSignals} on the result. */ compute(input, signals) { return __async$4(this, null, function* () { const witness = yield this.calculateWitness(input); yield this.expectConstraintPass(witness); return yield this.readWitnessSignals(witness, signals); }); } /** * Override witness value to try and fake a proof. If the circuit has soundness problems (i.e. * some signals are not constrained correctly), then you may be able to create a fake witness by * overriding specific values, and pass the constraints check. * * The symbol names must be given in full form, not just as the signal is named in the circuit code. In * general a symbol name looks something like: * * - `main.signal` * - `main.component.signal` * - `main.component.signal[n][m]` * * You will likely call `expectConstraintPass` on the resulting fake witness to see if it can indeed fool * a verifier. * @see {@link expectConstraintPass} */ editWitness(witness, symbolValues) { return __async$4(this, null, function* () { yield this.loadSymbols(); const fakeWitness = witness.slice(); for (const symbolName in symbolValues) { const symbolInfo = this.symbols[symbolName]; if (symbolInfo === void 0) { throw new Error("Invalid symbol name: " + symbolName); } fakeWitness[symbolInfo.varIdx] = symbolValues[symbolName]; } return fakeWitness; }); } /** Read symbol values from a witness. */ readWitness(witness, symbols) { return __async$4(this, null, function* () { yield this.loadSpecificSymbols(symbols); const ans = {}; for (const symbolName of symbols) { const symbolInfo = this.symbols[symbolName]; if (symbolInfo === void 0) { throw new Error("Invalid symbol name: " + symbolName); } ans[symbolName] = witness[symbolInfo.varIdx]; } return ans; }); } /** * Read signals from a witness. * * This is not the same as {@link readWitness} in the sense that the entire value represented by a signal * will be returned here. For example, instead of reading `main.out[0], main.out[1], main.out[2]` with `readWitness`, * you can simply query `out` in this function and an object with `{out: [...]}` will be returned. * * To read signals within a component, simply refer to them as `component.signal`. In other words, omit the `main.` prefix * and array dimensions. */ readWitnessSignals(witness, signals) { return __async$4(this, null, function* () { yield this.loadSymbols(); const entries = []; function dotCount(s) { return s.split(".").length; } for (const signal of signals) { const signalDotCount = dotCount(signal) + 1; const signalLength = signal.length + 5; const symbolNames = Object.keys(this.symbols).filter( (s) => s.startsWith(`main.${signal}`) && signalDotCount === dotCount(s) ); const matchedSymbols = symbolNames.filter((s) => { const i = s.indexOf("["); if (i === -1) { return s.length === signalLength; } else { return s.slice(0, i).length === signalLength; } }); if (matchedSymbols.length === 0) { throw new Error("No symbols matched for signal: " + signal); } else if (matchedSymbols.length === 1) { entries.push([signal, witness[this.symbols[matchedSymbols[0]].varIdx]]); } else { let processDepth2 = function(d) { const acc = []; if (d === dims.length - 1) { for (let i = 0; i < dims[d]; i++) { acc.push(witness[idx++]); } } else { for (let i = 0; i < dims[d]; i++) { acc.push(processDepth2(d + 1)); } } return acc; }; let idx = this.symbols[matchedSymbols[0]].varIdx; const splits = matchedSymbols.at(-1).split("["); const dims = splits.slice(1).map((dim) => parseInt(dim.slice(0, -1)) + 1); entries.push([signal, processDepth2(0)]); } } return Object.fromEntries(entries); }); } /** * Assert the output of a given witness. * @param actualOut expected witness * @param expectedOut computed output signals */ assertOut(actualOut, expectedOut) { return this.circomTester.assertOut(actualOut, expectedOut); } /** Loads the list of R1CS constraints to `this.constraints`. */ loadConstraints() { return __async$4(this, null, function* () { yield this.circomTester.loadConstraints(); this.constraints = this.circomTester.constraints; }); } /** * Loads the symbols in a dictionary at `this.symbols` * Symbols are stored under the .sym file * * Each line has 4 comma-separated values: * * 1. symbol name * 2. label index * 3. variable index * 4. component index */ loadSymbols() { return __async$4(this, null, function* () { yield this.circomTester.loadSymbols(); this.symbols = this.circomTester.symbols; }); } /** Faster alternative to this.circomTester.loadSymbols(), * stops once all the requested symbols are found */ loadSpecificSymbols(symbolsRequested) { return __async$4(this, null, function* () { if (!this.symbols) this.symbols = {}; const symbolsToFind = new Set(symbolsRequested.filter((s) => { var _a; return !((_a = this.symbols) == null ? void 0 : _a[s]); })); if (symbolsToFind.size === 0) return; const symbolsFile = yield fs.promises.readFile( path.join(this.circomTester.dir, this.circomTester.baseName + ".sym"), "utf8" ); for (const line of symbolsFile.split("\n")) { if (symbolsToFind.size === 0) return; const [labelIdx, varIdx, componentIdx, symbolName] = line.split(","); if (!symbolName || !symbolsToFind.has(symbolName)) continue; this.symbols[symbolName] = { labelIdx: Number(labelIdx), varIdx: Number(varIdx), componentIdx: Number(componentIdx) }; symbolsToFind.delete(symbolName); } }); } /** * @deprecated this is buggy right now * @param witness witness */ getDecoratedOutput(witness) { return this.circomTester.getDecoratedOutput(witness); } /** * Cleanup directory, should probably be called upon test completion (?) * @deprecated this is buggy right now */ release() { return this.circomTester.release(); } } var __async$3 = (__this, __arguments, generator) => { return new Promise((resolve, reject) => { var fulfilled = (value) => { try { step(generator.next(value)); } catch (e) { reject(e); } }; var rejected = (value) => { try { step(generator.throw(value)); } catch (e) { reject(e); } }; var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected); step((generator = generator.apply(__this, __arguments)).next()); }); }; class ProofTester { constructor(wasmPath, pkeyPath, vkeyPath, protocol) { this.wasmPath = wasmPath; this.pkeyPath = pkeyPath; this.vkeyPath = vkeyPath; this.protocol = protocol; this.verificationKey = JSON.parse(readFileSync(vkeyPath).toString()); if (this.verificationKey.protocol !== protocol) { throw new Error("Protocol mismatch."); } } prove(input) { return __async$3(this, null, function* () { return snarkjs[this.protocol].fullProve(input, this.wasmPath, this.pkeyPath, void 0); }); } verify(proof, publicSignals) { return __async$3(this, null, function* () { return yield snarkjs[this.protocol].verify( this.verificationKey, publicSignals, proof ); }); } expectPass(proof, publicSignals) { return __async$3(this, null, function* () { const ok = yield this.verify(proof, publicSignals); if (!ok) { throw new AssertionError({ message: "Expected proof to be verified.", expected: true, actual: false }); } }); } expectFail(proof, publicSignals) { return __async$3(this, null, function* () { const ok = yield this.verify(proof, publicSignals); if (ok) { throw new AssertionError({ message: "Expected proof to be not verified.", expected: false, actual: true }); } }); } } ({ bn128: BigInt("21888242871839275222246405745257275088548364400416034343698204186575808495617"), bls12381: BigInt("52435875175126190479447740508185965837690552500527637822603658699938581184513"), goldilocks: BigInt("18446744069414584321"), grumpkin: BigInt("21888242871839275222246405745257275088696311157297823662689037894645226208583"), pallas: BigInt("28948022309329048855892746252171976963363056481941560715954676764349967630337"), vesta: BigInt("28948022309329048855892746252171976963363056481941647379679742748393362948097"), secq256r1: BigInt("115792089210356248762697446949407573530086143415290314195533631308867097853951") }); const primeToName = { "21888242871839275222246405745257275088548364400416034343698204186575808495617": "bn128", "0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001": "bn128", "52435875175126190479447740508185965837690552500527637822603658699938581184513": "bls12381", "0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001": "bls12381", "18446744069414584321": "goldilocks", "0xffffffff00000001": "goldilocks", "21888242871839275222246405745257275088696311157297823662689037894645226208583": "grumpkin", "0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47": "grumpkin", "28948022309329048855892746252171976963363056481941560715954676764349967630337": "pallas", "0x40000000000000000000000000000000224698fc094cf91b992d30ed00000001": "pallas", "28948022309329048855892746252171976963363056481941647379679742748393362948097": "vesta", "0x40000000000000000000000000000000224698fc0994a8dd8c46eb2100000001": "vesta", "115792089210356248762697446949407573530086143415290314195533631308867097853951": "secq256r1", "0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff": "secq256r1" }; function prettyStringify(obj) { return JSON.stringify(obj, void 0, 2); } const PROTOCOLS = ["groth16", "plonk", "fflonk"]; const PRIMES = ["bn128", "bls12381", "goldilocks", "grumpkin", "pallas", "vesta", "secq256r1"]; const DEFAULT = Object.seal({ // general settings protocol: "groth16", prime: "bn128", version: "2.1.0", // directories & paths circuits: "./circuits.json", dirPtau: "./ptau", dirCircuits: "./circuits", dirInputs: "./inputs", dirBuild: "./build", circomPath: "circom", // compiler-specific optimization: void 0, inspect: true, include: ["./node_modules"], cWitness: false, wasmWitness: true, // groth16 phase-2 settings groth16numContributions: 1, groth16askForEntropy: false, // solidity & calldata prettyCalldata: false, // logger logLevel: "INFO", verbose: true }); var __async$2 = (__this, __arguments, generator) => { return new Promise((resolve, reject) => { var fulfilled = (value) => { try { step(generator.next(value)); } catch (e) { reject(e); } }; var rejected = (value) => { try { step(generator.throw(value)); } catch (e) { reject(e); } }; var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected); step((generator = generator.apply(__this, __arguments)).next()); }); }; function readR1CSInfo(r1csPath) { return __async$2(this, null, function* () { let pointer = 0; const r1csInfoType = { wires: 0, constraints: 0, privateInputs: 0, publicInputs: 0, publicOutputs: 0, useCustomGates: false, labels: 0, prime: BigInt(0), primeName: "" }; const fd = openSync(r1csPath, "r"); const numberOfSections = readBytesFromFile(fd, 0, 4, 8); pointer = 12; for (let i = Number(numberOfSections); i >= 0; i--) { const sectionType = Number(readBytesFromFile(fd, 0, 4, pointer)); pointer += 4; const sectionSize = Number(readBytesFromFile(fd, 0, 8, pointer)); pointer += 8; switch (sectionType) { // header section. case 1: pointer += 4; r1csInfoType.prime = readBytesFromFile(fd, 0, 32, pointer).toString(); pointer += 32; r1csInfoType.wires = Number(readBytesFromFile(fd, 0, 4, pointer)); pointer += 4; r1csInfoType.publicOutputs = Number(readBytesFromFile(fd, 0, 4, pointer)); pointer += 4; r1csInfoType.publicInputs = Number(readBytesFromFile(fd, 0, 4, pointer)); pointer += 4; r1csInfoType.privateInputs = Number(readBytesFromFile(fd, 0, 4, pointer)); pointer += 4; r1csInfoType.labels = Number(readBytesFromFile(fd, 0, 8, pointer)); pointer += 8; r1csInfoType.constraints = Number(readBytesFromFile(fd, 0, 4, pointer)); pointer += 4; break; // custom gates list section (plonk only). case 4: r1csInfoType.useCustomGates = Number(readBytesFromFile(fd, 0, 4, pointer)) > 0; pointer += Number(sectionSize); break; default: pointer += Number(sectionSize); break; } } r1csInfoType.primeName = primeToName[r1csInfoType.prime.toString()]; return r1csInfoType; }); } function readBytesFromFile(fd, offset, length, position) { const buffer = Buffer.alloc(length); readSync(fd, buffer, offset, length, position); return BigInt(`0x${buffer.reverse().toString("hex")}`); } var __defProp$1 = Object.defineProperty; var __getOwnPropSymbols$1 = Object.getOwnPropertySymbols; var __hasOwnProp$1 = Object.prototype.hasOwnProperty; var __propIsEnum$1 = Object.prototype.propertyIsEnumerable; var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __spreadValues$1 = (a, b) => { for (var prop in b || (b = {})) if (__hasOwnProp$1.call(b, prop)) __defNormalProp$1(a, prop, b[prop]); if (__getOwnPropSymbols$1) for (var prop of __getOwnPropSymbols$1(b)) { if (__propIsEnum$1.call(b, prop)) __defNormalProp$1(a, prop, b[prop]); } return a; }; var __async$1 = (__this, __arguments, generator) => { return new Promise((resolve, reject) => { var fulfilled = (value) => { try { step(generator.next(value)); } catch (e) { reject(e); } }; var rejected = (value) => { try { step(generator.throw(value)); } catch (e) { reject(e); } }; var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected); step((generator = generator.apply(__this, __arguments)).next()); }); }; function compileCircuit(config, targetPath, outDir) { return __async$1(this, null, function* () { mkdirSync(outDir, { recursive: true }); let flags = `--sym --r1cs -p ${config.prime} -o ${outDir}`; if (config.include.length > 0) flags += " " + config.include.map((path) => `-l ${path}`).join(" "); if (config.verbose) flags += " --verbose"; if (config.inspect) flags += " --inspect"; if (config.cWitness) flags += " --c"; if (config.wasmWitness) flags += " --wasm"; if (typeof config.optimization === "number") { if (config.optimization > 2) { flags += ` --O2round ${config.optimization}`; } else { flags += ` --O${config.optimization}`; } } try { const result = yield new Promise((resolve, reject) => { exec(`${config.circomPath} ${flags} ${targetPath}`, (error, stdout, stderr) => { if (error === null) { resolve({ stdout, stderr }); } else { reject(error); } }); }); return __spreadValues$1({}, result); } catch (e) { throw new Error("Compiler error:\n" + e); } }); } function instantiateCircuit(config, targetDir, targetPath) { const circuitCode = makeCircuit(config); if (!existsSync(targetDir)) { mkdirSync(targetDir, { recursive: true }); } writeFileSync(targetPath, circuitCode); } function makeCircuit(config) { const { version, usesCustomTemplates, file, pubs, template, params } = config; return `// auto-generated by circomkit pragma circom ${version}; ${usesCustomTemplates ? "pragma custom_templates;\n" : ""} include "../${file}.circom"; component main${pubs.length === 0 ? "" : " {public[" + pubs.join(", ") + "]}"} = ${template}(${params.join(", ")}); `; } function getCalldata(proof, pubs, pretty = false) { const pubsCalldata = publicSignalsCalldata(pubs, pretty); let proofCalldata; switch (proof.protocol) { case "groth16": proofCalldata = groth16Calldata(proof, pretty); break; case "plonk": proofCalldata = plonkCalldata(proof, pretty); break; case "fflonk": proofCalldata = fflonkCalldata(proof, pretty); break; default: throw "Unknown protocol:" + proof.protocol; } return ` ${proofCalldata} ${pubsCalldata} `; } function publicSignalsCalldata(pubs, pretty) { const pubs256 = valuesToPaddedUint256s(pubs); if (pretty) { return `uint[${pubs.length}] memory pubs = [ ${pubs256.join(",\n ")} ];`; } else { return `[${pubs256.map((s) => `"${s}"`).join(",")}]`; } } function fflonkCalldata(proof, pretty) { const vals = valuesToPaddedUint256s([ proof.polynomials.C1[0], proof.polynomials.C1[1], proof.polynomials.C2[0], proof.polynomials.C2[1], proof.polynomials.W1[0], proof.polynomials.W1[1], proof.polynomials.W2[0], proof.polynomials.W2[1], proof.evaluations.ql, proof.evaluations.qr, proof.evaluations.qm, proof.evaluations.qo, proof.evaluations.qc, proof.evaluations.s1, proof.evaluations.s2, proof.evaluations.s3, proof.evaluations.a, proof.evaluations.b, proof.evaluations.c, proof.evaluations.z, proof.evaluations.zw, proof.evaluations.t1w, proof.evaluations.t2w, proof.evaluations.inv ]); if (pretty) { return `uint256[24] memory proof = [ ${vals.join(",\n ")} ];`; } else { return `[${withQuotes(vals).join(",")}]`; } } function plonkCalldata(proof, pretty = false) { const vals = valuesToPaddedUint256s([ proof.A[0], proof.A[1], proof.B[0], proof.B[1], proof.C[0], proof.C[1], proof.Z[0], proof.Z[1], proof.T1[0], proof.T1[1], proof.T2[0], proof.T2[1], proof.T3[0], proof.T3[1], proof.Wxi[0], proof.Wxi[1], proof.Wxiw[0], proof.Wxiw[1], proof.eval_a, proof.eval_b, proof.eval_c, proof.eval_s1, proof.eval_s2, proof.eval_zw ]); if (pretty) { return `uint[24] memory proof = [ ${vals.join(",\n ")} ];`; } else { return `[${withQuotes(vals).join(",")}]`; } } function groth16Calldata(proof, pretty) { const pA = valuesToPaddedUint256s([proof.pi_a[0], proof.pi_a[1]]); const pB0 = valuesToPaddedUint256s([proof.pi_b[0][1], proof.pi_b[0][0]]); const pB1 = valuesToPaddedUint256s([proof.pi_b[1][1], proof.pi_b[1][0]]); const pC = valuesToPaddedUint256s([proof.pi_c[0], proof.pi_c[1]]); if (pretty) { return [ `uint[2] memory pA = [ ${pA.join(",\n ")} ];`, `uint[2][2] memory pB = [ [ ${pB0.join(",\n ")} ], [ ${pB1.join(",\n ")} ] ];`, `uint[2] memory pC = [ ${pC.join(",\n ")} ];` ].join("\n"); } else { return [ `[${withQuotes(pA).join(", ")}]`, `[[${withQuotes(pB0).join(", ")}], [${withQuotes(pB1).join(", ")}]]`, `[${withQuotes(pC).join(", ")}]` ].join("\n"); } } function valuesToPaddedUint256s(values) { return values.map((hexStr) => { const ans = "0x" + BigInt(hexStr).toString(16).padStart(64, "0"); if (ans.length !== 66) throw new Error("uint256 overflow: " + hexStr); return ans; }); } function withQuotes(vals) { return vals.map((val) => `"${val}"`); } class CircomkitPath { constructor(config) { this.config = config; } /** * Computes a path that requires a circuit name. * * @param circuit The name of the circuit. * @param kind The kind of file to compute the path for. */ ofCircuit(circuit, kind) { const dir = `${this.config.dirBuild}/${circuit}`; switch (kind) { case "dir": return dir; case "main": return `${this.config.dirCircuits}/main/${circuit}.circom`; case "r1cs": return `${dir}/${circuit}.r1cs`; case "sym": return `${dir}/${circuit}.sym`; case "wasm": return `${dir}/${circuit}_js/${circuit}.wasm`; case "pkey": return `${dir}/${this.config.protocol}_pkey.zkey`; case "vkey": return `${dir}/${this.config.protocol}_vkey.json`; case "sol": return `${dir}/${this.config.protocol}_verifier.sol`; default: throw new Error("Invalid kind: " + kind); } } /** * Computes a path that requires a circuit and an input name. * * @param circuit The name of the circuit. * @param input The name of the input. * @param kind The kind of file to compute the path for. */ ofCircuitWithInput(circuit, input, kind) { const dir = `${this.config.dirBuild}/${circuit}/${input}`; switch (kind) { case "dir": return dir; case "wtns": return `${dir}/witness.wtns`; case "pubs": return `${dir}/public.json`; case "proof": return `${dir}/${this.config.protocol}_proof.json`; case "in": return `${this.config.dirInputs}/${circuit}/${input}.json`; default: throw new Error("Invalid type: " + kind); } } /** * Given a PTAU name, returns the relative path. * * @param ptauName The name of the PTAU file, e.g. `powersOfTau28_hez_final_08.ptau`. */ ofPtau(ptauName) { return `${this.config.dirPtau}/${ptauName}`; } /** * Given a circuit & id name, returns the relative path of the phase-2 PTAU. * * This is used in particular by Groth16's circuit-specific setup phase. * * @param circuit The name of the circuit. * @param id The id of the zKey. */ ofZkey(circuit, id) { return `${this.config.dirBuild}/${circuit}/${circuit}_${id}.zkey`; } } var __defProp = Object.defineProperty; var __defProps = Object.defineProperties; var __getOwnPropDescs = Object.getOwnPropertyDescriptors; var __getOwnPropSymbols = Object.getOwnPropertySymbols; var __hasOwnProp = Object.prototype.hasOwnProperty; var __propIsEnum = Object.prototype.propertyIsEnumerable; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __spreadValues = (a, b) => { for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]); if (__getOwnPropSymbols) for (var prop of __getOwnPropSymbols(b)) { if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]); } return a; }; var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b)); var __async = (__this, __arguments, generator) => { return new Promise((resolve, reject) => { var fulfilled = (value) => { try { step(generator.next(value)); } catch (e) { reject(e); } }; var rejected = (value) => { try { step(generator.throw(value)); } catch (e) { reject(e); } }; var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected); step((generator = generator.apply(__this, __arguments)).next()); }); }; class Circomkit { constructor(overrides = {}) { const config = __spreadValues(__spreadValues({}, DEFAULT), overrides); this.config = JSON.parse(JSON.stringify(config)); this.log = loglevel.getLogger("Circomkit"); this.log.setLevel(this.config.logLevel); this.snarkjsLogger = this.config.verbose ? this.log : void 0; if (!PRIMES.includes(this.config.prime)) { throw new Error("Invalid prime in configuration."); } if (!PROTOCOLS.includes(this.config.protocol)) { throw new Error("Invalid protocol in configuration."); } if (this.config.optimization !== void 0 && typeof this.config.optimization !== "number") { throw new Error("Invalid optimization level."); } if (typeof this.config.optimization === "number" && this.config.optimization < 0) { this.log.warn("Optimization level must be at least 0, setting it to 0."); this.config.optimization = 0; } if (this.config.protocol === "plonk" && this.config.optimization !== 1) { this.log.warn( "Optimization level for PLONK must be 1.\n", "See: https://docs.circom.io/circom-language/circom-insight/simplification/" ); this.config.optimization = 1; } this.path = new CircomkitPath(this.config); } /** Returns the contents of `circuits.json`. */ readCircuits() { return JSON.parse(readFileSync(this.config.circuits, "utf-8")); } /** Returns a single circuit config from `circuits.json`. */ readCircuitConfig(circuit) { const circuits = this.readCircuits(); if (!(circuit in circuits)) { throw new Error("No such circuit in " + this.config.circuits); } return circuits[circuit]; } /** Clear build files and the `main` component of a circuit. */ clear(circuit) { return __async(this, null, function* () { yield Promise.all([ rm(this.path.ofCircuit(circuit, "dir"), { recursive: true, force: true }), rm(this.path.ofCircuit(circuit, "main"), { force: true }) ]); }); } /** Export a verification key (vKey) from a proving key (zKey). */ vkey(circuit, pkeyPath) { return __async(this, null, function* () { const vkeyPath = this.path.ofCircuit(circuit, "vkey"); if (pkeyPath === void 0) { pkeyPath = this.path.ofCircuit(circuit, "pkey"); } if (!existsSync(pkeyPath)) { throw new Error("There must be a prover key for this circuit to extract a verification key."); } const vkey = yield snarkjs.zKey.exportVerificationKey(pkeyPath, this.snarkjsLogger); writeFileSync(vkeyPath, prettyStringify(vkey)); return vkeyPath; }); } /** Returns circuit information. */ info(circuit) { return __async(this, null, function* () { return yield readR1CSInfo(this.path.ofCircuit(circuit, "r1cs")); }); } /** Downloads the phase-1 setup PTAU file for a circuit based on it's number of constraints. * * The downloaded PTAU files can be seen at [SnarkJS docs](https://github.com/iden3/snarkjs#7-prepare-phase-2). * Note that this may take a while if the circuit is large and thus a larger PTAU is needed. * * This function only works when the used prime is `bn128`. * * @returns path of the downloaded PTAU file */ ptau(circuit) { return __async(this, null, function* () { const { constraints } = yield this.info(circuit); const ptauName = getPtauName(constraints); const ptauPath = this.path.ofPtau(ptauName); if (existsSync(ptauPath)) { return ptauPath; } else { if (this.config.prime !== "bn128") { throw new Error("Auto-downloading PTAU only allowed for bn128 at the moment."); } mkdirSync(this.config.dirPtau, { recursive: true }); this.log.info(`Downloading ${ptauName}, this may take a while.`); return yield downloadPtau(ptauName, this.config.dirPtau); } }); } /** Compile the circuit. * * A circuit configuration can be passed optionally; if not, the * config will be read from `circuits.json` at the working directory. * * @returns path of the build directory */ compile(circuit, config) { return __async(this, null, function* () { const targetPath = this.instantiate(circuit, config); this.log.debug("Main component created at: " + targetPath); const outDir = this.path.ofCircuit(circuit, "dir"); const { stdout, stderr } = yield compileCircuit(this.config, targetPath, outDir); if (this.config.verbose) { this.log.info(stdout); } if (stderr) { this.log.error(stderr); } return outDir; }); } /** Exports a solidity contract for the verifier. * @returns path of the exported Solidity contract */ contract(circuit) { return __async(this, null, function* () { const pkey = this.path.ofCircuit(circuit, "pkey"); const template = readFileSync(`./node_modules/snarkjs/templates/verifier_${this.config.protocol}.sol.ejs`, "utf-8"); const contractCode = yield snarkjs.zKey.exportSolidityVerifier( pkey, { [this.config.protocol]: template }, this.snarkjsLogger ); const contractPath = this.path.ofCircuit(circuit, "sol"); writeFileSync(contractPath, contractCode); return contractPath; }); } /** Export calldata to call a Verifier contract. * * @returns calldata */ calldata(circuit, input, pretty) { return __async(this, null, function* () { const pubs = JSON.parse( yield readFile(this.path.ofCircuitWithInput(circuit, input, "pubs"), "utf-8") ); const proof = JSON.parse( yield readFile(this.path.ofCircuitWithInput(circuit, input, "proof"), "utf-8") ); const res = getCalldata(proof, pubs, pretty != null ? pretty : this.config.prettyCalldata); return res; }); } /** Instantiate the `main` component. * * If `circuitConfig` argument is omitted, this function will look for it at `circuits.json` * in the working directory, and throw an error if no entry is found for the circuit. * * When config is read from file, `dir` defaults to `main`, otherwise `dir` defaults to `test`. * This is done to make it so that when CLI is used circuits are created under `main`, and when * we use Circomkit programmatically (e.g. during testing) circuits are created under `test` * unless specified otherwise. * * @returns path of the created main component */ instantiate(circuit, circuitConfig) { var _a; if (!circuitConfig) { const circuitConfigFile = this.readCircuitConfig(circuit); circuitConfig = __spreadProps(__spreadValues({}, circuitConfigFile), { dir: circuitConfigFile.dir || "main", version: circuitConfigFile.version || this.config.version }); } const circomSource = readFileSync(`${this.config.dirCircuits}/${circuitConfig.file}.circom`, "utf8"); const usesCustomTemplates = (_a = circuitConfig.usesCustomTemplates) != null ? _a : circomSource.replace(/\/\/.*|\/\*[\s\S]*?\*\//g, "").includes("pragma custom_templates;"); const directory = circuitConfig.dir || "test"; const filePrefixMatches = directory.match(/\//g); let file = circuitConfig.file; if (filePrefixMatches !== null) { file = "../".repeat(filePrefixMatches.length) + file; } const config = { file, template: circuitConfig.template, version: circuitConfig.version || "2.0.0", dir: directory, pubs: circuitConfig.pubs || [], params: circuitConfig.params || [], usesCustomTemplates }; const targetDir = `${this.config.dirCircuits}/${directory}`; const targetPath = `${targetDir}/${circuit}.circom`; instantiateCircuit(config, targetDir, targetPath); return targetPath; } /** Generate a proof. * * If `data` is not passed, the input data will be read from `inputs/<circuit>/<input>.json`. * * @returns path of the directory where public signals and proof are created */ prove(circuit, input, data) { return __async(this, null, function* () { const wasmPath = this.path.ofCircuit(circuit, "wasm"); if (!existsSync(wasmPath)) { this.log.warn("WASM file does not exist, creating it now..."); yield this.compile(circuit); } const pkeyPath = this.path.ofCircuit(circuit, "pkey"); if (!existsSync(pkeyPath)) { this.log.warn("Prover key does not exist, creating it now..."); yield this.setup(circuit); } const jsonInput = data != null ? data : JSON.parse(readFileSync(this.path.ofCircuitWithInput(circuit, input, "in"), "utf-8")); const { proof, publicSignals } = yield snarkjs[this.config.protocol].fullProve( jsonInput, wasmPath, pkeyPath, this.snarkjsLogger ); const dir = this.path.ofCircuitWithInput(circuit, input, "dir"); mkdirSync(dir, { recursive: true }); yield Promise.all([ writeFile(this.path.ofCircuitWithInput(circuit, input, "pubs"), prettyStringify(publicSignals)), writeFile(this.path.ofCircuitWithInput(circuit, input, "proof"), prettyStringify(proof)) ]); return dir; }); } /** Commence a circuit-specific setup. * * If `ptauPath` argument is omitted, this function will try to automatically download it. * See the {@link ptau} method for more information about this. * * @returns path of the verifier key and prover key */ setup(circuit, ptauPath) { return __async(this, null, function* () { const r1csPath = this.path.ofCircuit(circuit, "r1cs"); const pkeyPath = this.path.ofCircuit(circuit, "pkey"); const vkeyPath = this.path.ofCircuit(circuit, "vkey"); if (!existsSync(r1csPath)) { this.log.warn("R1CS does not exist, creating it now."); yield this.compile(circuit); } if (ptauPath === void 0) { if (this.config.prime !== "bn128") { throw new Error("Can not download PTAU file when using a prime field other than bn128"); } ptauPath = yield this.ptau(circuit); } else if (!existsSync(ptauPath)) { this.log.warn("PTAU path was given but no PTAU exists there, downloading it anyways."); ptauPath = yield this.ptau(circuit); } this.log.info("Beginning setup phase!"); if (this.config.protocol === "groth16") { let curZkey = this.path.ofZkey(circuit, 0); yield snarkjs.zKey.newZKey(r1csPath, ptauPath, curZkey, this.snarkjsLogger); for (let contrib = 1; contrib <= this.config.groth16numContributions; contrib++) { const nextZkey = this.path.ofZkey(circuit, contrib); this.log.info(`Making contribution: ${contrib}`); yield snarkjs.zKey.contribute( curZkey, nextZkey, `${circuit}_${contrib}`, this.config.groth16askForEntropy ? void 0 : randomBytes(32), // entropy this.snarkjsLogger ); rmSync(curZkey); curZkey = nextZkey; } renameSync(curZkey, pkeyPath); } else { yield snarkjs[this.config.protocol].setup(r1csPath, ptauPath, pkeyPath, this.snarkjsLogger); } const vkey = yield snarkjs.zKey.exportVerificationKey(pkeyPath, this.snarkjsLogger); writeFileSync(vkeyPath, prettyStringify(vkey)); return { verifierKeyPath: vkeyPath, proverKeyPath: pkeyPath }; }); } /** Verify a proof for some public signals. * @returns `true` if verification is successful, `false` otherwise. */ verify(circuit, input) { return __async(this, null, function* () { const [vkey, pubs, proof] = (yield Promise.all( [ this.path.ofCircuit(circuit, "vkey"), this.path.ofCircuitWithInput(circuit, input, "pubs"), this.path.ofCircuitWithInput(circuit, input, "proof") ].map((path) => readFile(path, "utf-8")) )).map((content) => JSON.parse(content)); return yield snarkjs[this.config.protocol].verify(vkey, pubs, proof, this.snarkjsLogger); }); } /** Calculates the witness for the given circuit and input. * * If `data` is not passed, the input data will be read from `inputs/<circuit>/<input>.json`. * * @returns path of the created witness */ witness(circuit, input, data) { return __async(this, null, function* () { const wasmPath = this.path.ofCircuit(circuit, "wasm"); if (!existsSync(wasmPath)) { this.log.warn("WASM file does not exist, creating it now..."); yield this.compile(circuit); } const wtnsPath = this.path.ofCircuitWithInput(circuit, input, "wtns"); const outDir = this.path.ofCircuitWithInput(circuit, input, "dir"); const jsonInput = data != null ? data : JSON.parse(readFileSync(this.path.ofCircuitWithInput(circuit, input, "in"), "utf-8")); mkdirSync(outDir, { recursive: true }); yield snarkjs.wtns.calculate(jsonInput, wasmPath, wtnsPath); return wtnsPath; }); } /** Exports a JSON input file for some circuit with the given object. * * This is useful for testing real circuits, or creating an input programmatically. * Overwrites an existing input. * * @returns path of the created input file */ input(circuit, input, data) { const inputPath = this.path.ofCircuitWithInput(circuit, input, "in"); if (existsSync(inputPath)) { this.log.warn("Input file exists already, overwriting it."); } writeFileSync(inputPath, prettyStringify(data)); return inputPath; } json(target, circuit, input) { return __async(this, null, function* () { let json; let path; switch (target) { // R1CS case "r1cs": { path = this.path.ofCircuit(circuit, "r1cs"); json = yield snarkjs.r1cs.exportJson(path, void 0); break; } // Prover key case "zkey": { if (this.config.protocol !== "groth16") { throw new Error("Exporting zKey to JSON is only supported for Groth16 at the moment."); } path = this.path.ofCircuit(circuit, "pkey"); json = yield snarkjs.zKey.exportJson(path); break; } // Witness case "wtns": { if (!input) throw new Error("Expected input"); path = this.path.ofCircuitWithInput(circuit, input, "wtns"); json = yield snarkjs.wtns.exportJson(path); break; } default: throw new Error("Unknown export target: " + target); } return { json, path: path + ".json" }; }); } /** Compiles the circuit and returns a witness tester instance. */ WitnessTester(circuit, circuitConfig, tester = "wasm") { return __async(this, null, function* () { var _a, _b; (_a = circuitConfig.dir) != null ? _a : circuitConfig.dir = "test"; const targetPath = this.instantiate(circuit, circuitConfig); const circomWasmTester = yield (tester === "wasm" ? wasm : c)(targetPath, { output: void 0, // this makes tests to be created under /tmp prime: this.config.prime, verbose: this.config.verbose, O: typeof this.config.optimization === "number" ? Math.min(this.config.optimization, 1) : void 0, // tester doesnt have O2 json: false, include: this.config.include, wasm: true, sym: true, recompile: (_b = circuitConfig.recompile) != null ? _b : true }); return new WitnessTester(circomWasmTester); }); } /** Returns a proof tester. */ ProofTester(circuit, protocol) { return __async(this, null, function* () { const wasmPath = this.path.ofCircuit(circuit, "wasm"); const pkeyPath = this.path.ofCircuit(circuit, "pkey"); const vkeyPath = this.path.ofCircuit(circuit, "vkey"); const missingPaths = [wasmPath, pkeyPath, vkeyPath].filter((p) => !existsSync(p)); if (missingPaths.length !== 0) { throw new Error("Missing files: " + missingPaths.join(", ")); } return new ProofTester(wasmPath, pkeyPath, vkeyPath, protocol); }); } } export { Circomkit as C, prettyStringify as p };