circom_tester
Version:
Tools for testing circom circuits.
395 lines (347 loc) • 14.2 kB
JavaScript
const fs = require("fs");
const path = require("path");
const { F1Field } = require("ffjavascript");
const { readR1cs } = require("r1csfile");
const util = require("util");
const tmp = require("tmp-promise");
const { createHash } = require('crypto');
const exec = util.promisify(require("child_process").exec);
async function parseOptionsAndCompile(circomInput, options) {
const compilerVersion = await compiler_above_version("2.0.0");
if (!compilerVersion) {
throw new Error("Wrong compiler version. Must be at least 2.0.0");
}
let baseName = path.basename(circomInput, ".circom");
options.sym = true;
options.baseName = baseName;
options.json = options.json || false; // constraints in json format
options.r1cs = true;
options.compile = (typeof options.recompile === 'undefined') ? true : options.recompile; // by default compile
// if output is not set, create a temporary directory
if (typeof options.output === 'undefined') {
tmp.setGracefulCleanup();
const dir = await tmp.dir({prefix: "circom_", unsafeCleanup: true});
options.output = dir.path;
} else {
try {
await fs.promises.access(options.output);
} catch (err) {
if (!options.compile) {
throw new Error("Cannot set recompile to false if the output path does not exist");
}
await fs.promises.mkdir(options.output, {recursive: true});
}
}
// read contents of circomInput
const circomCode = await fs.promises.readFile(circomInput, "utf8");
// check if circomCode has main component instantiated
const mainRegex = /^\s*component\s+main(\s|=)/m;
const mainDefined = mainRegex.test(circomCode);
// if main is not defined, get all template names
const templateNames = [];
const pragmaLines = [];
if (!mainDefined) {
// get all pragma lines to put in temporary circom file
const pragmaRegex = /^\s*pragma\s+.*$/gm;
let pragmaMatches;
while ((pragmaMatches = pragmaRegex.exec(circomCode)) !== null) {
pragmaLines.push(pragmaMatches[0]);
}
// get all template names
const templateRegex = /^\s*template\s+([a-zA-Z0-9_$]+)\s*\(/gm;
let templateMatches;
while ((templateMatches = templateRegex.exec(circomCode)) !== null) {
templateNames.push(templateMatches[1]);
}
}
// check for conflicting options
if (mainDefined) {
if (options.templateName || options.templateParams || options.templatePublicSignals) {
throw new Error("Cannot set template name, params and public signals if main is already defined");
}
}
// when main is not defined, create temporary circom file using specified template as a main component
if (!mainDefined) {
// if templateName is not provided, try to autodetect it
if (!options.templateName) {
// if file has only one template, use it as main component
if (templateNames.length === 1) {
options.templateName = templateNames[0];
} else {
throw new Error("No main component defined and template name to generate one is not provided");
}
}
// define main component with templateName
let params = "";
if (options.templateParams) {
params = options.templateParams.map((p) => p.toString()).join(", ");
}
// define public signals if any
let publicSignals = "";
if (options.templatePublicSignals) {
publicSignals = "{public [" + options.templatePublicSignals.map((p) => p.toString()).join(", ") + "]}";
}
// define main component
const mainComponent = `component main ${publicSignals} = ${options.templateName}(${params});`;
// include original circom file
const includes = "include \"" + baseName + ".circom\";";
const tmpCircomCodeLines = [...pragmaLines, includes, mainComponent];
const tmpCircomCode = tmpCircomCodeLines.join("\n");
// calculate the hash of the main component using sha256
const h = createHash("sha256");
const hash = h.update(Buffer.from(tmpCircomCode, "utf8")).digest('hex');
// and use it as a suffix for the temporary directory name inside the output path
let suffix = hash.substring(0, 8);
const tmpCircomPath = path.join(options.output, baseName + "_" + options.templateName + "_" + suffix);
// create the directory if it doesn't exist
await fs.promises.mkdir(tmpCircomPath, {recursive: true});
// add the original circom file directory to the include path
const includePath = path.dirname(path.resolve(circomInput));
if (options.include) {
if (Array.isArray(options.include)) {
options.include.push(includePath);
} else {
options.include = [options.include, includePath];
}
} else {
options.include = [includePath];
}
// override baseName, output path and circomInput
baseName = "circuit";
options.baseName = baseName;
options.output = tmpCircomPath;
circomInput = path.join(tmpCircomPath, baseName + ".circom");
// write the generated circom code to the temporary file circuit.circom
await fs.promises.writeFile(circomInput, tmpCircomCode, "utf8");
}
// compile flag is set by default, unless overwritten by recompile=false option
if (options.compile) {
await compile(baseName, circomInput, options);
} else {
const jsPath = path.join(options.output, baseName + "_js");
try {
await fs.promises.access(jsPath);
} catch (err) {
throw new Error("Cannot set recompile to false if the " + jsPath + " folder does not exist")
}
}
return options;
}
async function compile(baseName, fileName, options) {
let flags = "";
if (options.include) {
if (Array.isArray(options.include)) {
for (let i = 0; i < options.include.length; i++) {
flags += "-l " + options.include[i] + " ";
}
} else {
flags += "-l " + options.include + " ";
}
}
if (options.c) flags += "--c ";
if (options.wasm) flags += "--wasm ";
if (options.sym) flags += "--sym ";
if (options.r1cs) flags += "--r1cs ";
if (options.json) flags += "--json ";
if (options.output) flags += "--output " + options.output + " ";
if (options.prime) flags += "--prime " + options.prime + " ";
if (options.O === 0) flags += "--O0 ";
if (options.O === 1) flags += "--O1 ";
if (options.O === 2) flags += "--O2 ";
if (options.O2round) flags += "--O2round " + options.O2round + " ";
if (options.verbose) flags += "--verbose ";
if (options.inspect) flags += "--inspect ";
if (options.simplification_substitution) flags += "--simplification_substitution ";
if (options.no_asm) flags += "--no_asm ";
if (options.no_init) flags += "--no_init ";
try {
let b = await exec("circom " + flags + fileName);
if (options.verbose) {
console.log(b.stdout);
}
if (b.stderr) {
console.error(b.stderr);
}
} catch (e) {
throw new Error("circom compiler error: " + e);
}
if (options.c) {
const c_folder = path.join(options.output, baseName + "_cpp/")
let b = await exec("make -C " + c_folder);
if (b.stderr) {
console.error(b.stderr);
}
}
}
class BaseTester {
constructor(dir, baseName) {
this.dir = dir;
this.baseName = baseName;
}
async release() {
await this.dir.cleanup();
}
async loadSymbols() {
if (this.symbols) return;
this.symbols = {};
const symsStr = await fs.promises.readFile(
path.join(this.dir, this.baseName + ".sym"),
"utf8"
);
const lines = symsStr.split("\n");
for (let i = 0; i < lines.length; i++) {
const arr = lines[i].split(",");
if (arr.length !== 4) continue;
this.symbols[arr[3]] = {
labelIdx: Number(arr[0]),
varIdx: Number(arr[1]),
componentIdx: Number(arr[2]),
};
}
}
async loadConstraints() {
const self = this;
if (this.constraints) return;
const r1cs = await readR1cs(path.join(this.dir, this.baseName + ".r1cs"), {
loadConstraints: true,
loadMap: false,
getFieldFromPrime: (p, singlethread) => new F1Field(p)
});
self.F = r1cs.F;
self.nVars = r1cs.nVars;
self.constraints = r1cs.constraints;
}
async assertOut(actualOut, expectedOut) {
const self = this;
if (!self.symbols) await self.loadSymbols();
checkObject("main", expectedOut);
function checkObject(prefix, eOut) {
if (Array.isArray(eOut)) {
for (let i = 0; i < eOut.length; i++) {
checkObject(prefix + "[" + i + "]", eOut[i]);
}
} else if (typeof eOut === "object" && eOut.constructor.name === "Object") {
for (let k in eOut) {
checkObject(prefix + "." + k, eOut[k]);
}
} else {
if (typeof self.symbols[prefix] === "undefined") {
throw new Error("Output variable not defined: " + prefix);
}
const ba = actualOut[self.symbols[prefix].varIdx].toString();
const be = eOut.toString();
if (ba !== be) {
throw new Error(`Assertion failed for ${prefix}: expected ${be}, got ${ba}`);
}
}
}
}
async getDecoratedOutput(witness) {
const self = this;
const lines = [];
if (!self.symbols) await self.loadSymbols();
for (let n in self.symbols) {
let v;
if (witness[self.symbols[n].varIdx] !== undefined) {
v = witness[self.symbols[n].varIdx].toString();
} else {
v = "undefined";
}
lines.push(`${n} --> ${v}`);
}
return lines.join("\n");
}
async getOutput(witness, output, templateName = "main") {
const self = this;
if (!self.symbols) await self.loadSymbols();
let prefix = "main";
if (templateName !== "main") {
const regex = new RegExp(`^.*${templateName}[^.]*\.[^.]+$`);
Object.keys(self.symbols).find((k) => {
if (regex.test(k)) {
prefix = k.replace(/\.[^.]+$/, "");
return true;
}
});
}
return get_by_prefix(prefix, output);
function get_by_prefix(prefix, out) {
if (typeof out === "object" && out.constructor.name === "Object") {
return Object.fromEntries(
Object.entries(out).map(([k, v]) => [
k,
get_by_prefix(`${prefix}.${k}`, v),
])
);
} else if (Array.isArray(out)) {
if (out.length === 1) {
return get_by_prefix(prefix, out[0]);
} else if (out.length === 0 || out.length > 2) {
throw new Error(`Invalid output format: ${prefix} ${out}`);
}
return Array.from({ length: out[0] }, (_, i) =>
get_by_prefix(`${prefix}[${i}]`, out[1])
);
} else {
if (out === 1) {
const name = `${prefix}`;
if (typeof self.symbols[name] == "undefined") {
throw new Error(`Output variable not defined: ${name}`);
}
return witness[self.symbols[name].varIdx];
} else {
return Array.from({ length: out }, (_, i) => {
const name = `${prefix}[${i}]`;
if (typeof self.symbols[name] == "undefined") {
throw new Error(`Output variable not defined: ${name}`);
}
return witness[self.symbols[name].varIdx];
});
}
}
}
}
async checkConstraints(witness) {
const self = this;
if (!self.constraints) await self.loadConstraints();
const F = self.F;
for (let i = 0; i < self.constraints.length; i++) {
const constraint = self.constraints[i];
const a = evalLC(constraint[0]);
const b = evalLC(constraint[1]);
const c = evalLC(constraint[2]);
if (!F.isZero(F.sub(F.mul(a, b), c))) {
throw new Error("Constraint doesn't match");
}
}
function evalLC(lc) {
const F = self.F;
let v = F.zero;
for (let w in lc) {
v = F.add(
v,
F.mul(lc[w], F.e(witness[w]))
);
}
return v;
}
}
}
function version_to_list(v) {
return v.split(".").map((x) => parseInt(x, 10));
}
function check_versions(v1, v2) {
//check if v1 is newer than or equal to v2
for (let i = 0; i < v2.length; i++) {
if (v1[i] > v2[i]) return true;
if (v1[i] < v2[i]) return false;
}
return true;
}
async function compiler_above_version(v) {
const output = (await exec("circom --version")).stdout;
const compiler_version = version_to_list(output.slice(output.search(/\d/), -1));
const vlist = version_to_list(v);
return check_versions(compiler_version, vlist);
}
module.exports = { BaseTester, version_to_list, check_versions, compile, compiler_above_version, parseOptionsAndCompile };