Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
lispc committed Mar 25, 2021
0 parents commit 1519f4b
Show file tree
Hide file tree
Showing 13 changed files with 3,272 additions and 0 deletions.
19 changes: 19 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module.exports = {
parser: '@typescript-eslint/parser', // Specifies the ESLint parser
extends: [
'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin
'prettier', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
'plugin:prettier/recommended', // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
],
parserOptions: {
ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
sourceType: 'module', // Allows for the use of imports
},
rules: {
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
},
};
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v15.11.0
4 changes: 4 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
**/node_modules
dist/
helper.ts/rescue_wasm/

6 changes: 6 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"trailingComma": "all",
"singleQuote": true,
"printWidth": 140,
"arrowParens": "avoid"
}
34 changes: 34 additions & 0 deletions cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env node
import { parepareCircuitDir, testCircuitDir } from './index';
import * as path from 'path';
import * as fs from 'fs';
import { Command } from 'commander';

async function main() {
try {
const program = new Command();

program
.command('compile <circuit_dir>')
.description('compile a circom circuit dir')
.option('-f, --force_recompile', 'ignore compiled files', false)
.option('-v, --verbose', 'print verbose log', true)
.action(async (circuit_dir, options) => {
console.log({ circuit_dir, options });
await parepareCircuitDir(circuit_dir, { alwaysRecompile: option.force_recompile, verbose: option.verbose });
});

program
.command('test <circuit_dir>')
.description('test a circom circuit with given inputs/outputs')
.action(async (circuit_dir, options) => {
await testCircuitDir(circuit_dir);
});

await program.parseAsync(process.argv);
} catch (error) {
console.error('Error:', error);
process.exit(1);
}
}
main();
288 changes: 288 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
const chai = require('chai');
const assert = chai.assert;
import * as fs from 'fs';
import * as path from 'path';
import * as shelljs from 'shelljs';
import * as tmp from 'tmp-promise';
import * as circom from 'circom';
const loadR1cs = require('r1csfile').load;
const { ZqField, utils: ffutils } = require('ffjavascript');

const print_info = false;
const primeStr = '21888242871839275222246405745257275088548364400416034343698204186575808495617';

// This is an opinionated tester.
// Rather than allowing setting data path flexibly,
// It assumes some standard folder structure and file name
// , like circuit.circom input.json witness.json etc

// TOOD: type
async function checkConstraints(F, constraints, witness) {
if (!constraints) {
throw new Error('empty constraints');
}
for (let i = 0; i < constraints.length; i++) {
checkConstraint(constraints[i]);
}

function checkConstraint(constraint) {
const a = evalLC(constraint[0]);
const b = evalLC(constraint[1]);
const c = evalLC(constraint[2]);

assert(F.isZero(F.sub(F.mul(a, b), c)), "Constraint doesn't match");
}

function evalLC(lc) {
let v = F.zero;
for (let w in lc) {
v = F.add(v, F.mul(lc[w], BigInt(witness[w])));
}
return v;
}
}

// TOOD: type
async function assertOut(symbols, actualOut, expectedOut) {
if (!symbols) {
throw new Error('empty symbols');
}

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 symbols[prefix] == 'undefined') {
assert(false, 'Output variable not defined: ' + prefix);
}
const ba = actualOut[symbols[prefix].varIdx].toString();
const be = eOut.toString();
assert.strictEqual(ba, be, prefix);
}
}
}

async function generateMainTestCircom(fileName: string, { src, main }) {
if (src == '' || main == '') {
throw new Error('invalid component ' + src + ' ' + main);
}
let srcCode = `include "${src}";
component main = ${main};`;
fs.writeFileSync(fileName, srcCode, 'utf8');
}

async function writeJsonWithBigint(path: string, obj: Object) {
let text = JSON.stringify(
obj,
(key, value) => (typeof value === 'bigint' ? value.toString() : value), // return everything else unchanged
2,
);
// maybe another implementation?:
// let text = JSON.stringify(ffutils.stringifyBigInts(obj)));
fs.writeFileSync(path, text, 'utf8');
}

async function readSymbols(path: string) {
let symbols = {};

const symsStr = await fs.promises.readFile(path, 'utf8');
const lines = symsStr.split('\n');
for (let i = 0; i < lines.length; i++) {
const arr = lines[i].split(',');
if (arr.length != 4) continue;
symbols[arr[3]] = {
labelIdx: Number(arr[0]),
varIdx: Number(arr[1]),
componentIdx: Number(arr[2]),
};
}
return symbols;
}

function parepareCircuitDir(circuitDirName, { alwaysRecompile = false, verbose = false } = {}) {
// console.log('compiling dir', circuitDirName);
const circuitFilePath = path.join(circuitDirName, 'circuit.circom');
const r1csFilepath = path.join(circuitDirName, 'circuit.r1cs');
const symFilepath = path.join(circuitDirName, 'circuit.sym');
const binaryFilePath = path.join(circuitDirName, 'circuit');
if (alwaysRecompile || !fs.existsSync(binaryFilePath)) {
if (verbose) {
console.log('compile', circuitDirName);
}
compileNativeBinary({ circuitDirName, r1csFilepath, circuitFilePath, symFilepath, binaryFilePath });
} else {
if (verbose) {
console.log('skip compiling binary ', binaryFilePath);
}
}
return { circuitFilePath, r1csFilepath, symFilepath, binaryFilePath };
}

function compileNativeBinary({ circuitDirName, r1csFilepath, circuitFilePath, symFilepath, binaryFilePath }) {
const circomRuntimePath = path.join(__dirname, '..', '..', 'node_modules', 'circom_runtime');
const snarkjsPath = path.join(__dirname, '..', '..', 'node_modules', 'snarkjs', 'build', 'cli.cjs');
const ffiasmPath = path.join(__dirname, '..', '..', 'node_modules', 'ffiasm');
const circomcliPath = path.join(__dirname, '..', '..', 'node_modules', 'circom', 'cli.js');
const cFilepath = path.join(circuitDirName, 'circuit.c');

var cmd: string;
cmd = `cp ${circomRuntimePath}/c/*.cpp ${circuitDirName}`;
shelljs.exec(cmd);
cmd = `cp ${circomRuntimePath}/c/*.hpp ${circuitDirName}`;
shelljs.exec(cmd);
cmd = `node ${ffiasmPath}/src/buildzqfield.js -q ${primeStr} -n Fr`;
shelljs.exec(cmd, { cwd: circuitDirName });
//cmd = `mv fr.asm fr.cpp fr.hpp ${circuitDirName}`;
//shelljs.exec(cmd);
if (process.platform === 'darwin') {
cmd = `nasm -fmacho64 --prefix _ ${circuitDirName}/fr.asm`;
} else if (process.platform === 'linux') {
cmd = `nasm -felf64 ${circuitDirName}/fr.asm`;
} else throw 'Unsupported platform';
shelljs.exec(cmd);
cmd = `NODE_OPTIONS=--max-old-space-size=8192 node --stack-size=65500 ${circomcliPath} ${circuitFilePath} -r ${r1csFilepath} -c ${cFilepath} -s ${symFilepath}`;
shelljs.exec(cmd);
if (print_info) {
cmd = `NODE_OPTIONS=--max-old-space-size=8192 node ${snarkjsPath} r1cs info ${r1csFilepath}`;
shelljs.exec(cmd);
// cmd = `NODE_OPTIONS=--max-old-space-size=8192 node ${snarkjsPath} r1cs print ${r1csFilepath} ${symFilepath}`;
// shelljs.exec(cmd);
}
if (process.platform === 'darwin') {
cmd = `g++ ${circuitDirName}/main.cpp ${circuitDirName}/calcwit.cpp ${circuitDirName}/utils.cpp ${circuitDirName}/fr.cpp ${circuitDirName}/fr.o ${cFilepath} -o ${binaryFilePath} -lgmp -std=c++11 -O3 -DSANITY_CHECK`;
if (process.arch === 'arm64') {
cmd = 'arch -x86_64 ' + cmd;
} else {
//cmd = cmd + ' -fopenmp';
}
} else if (process.platform === 'linux') {
cmd = `g++ -pthread ${circuitDirName}/main.cpp ${circuitDirName}/calcwit.cpp ${circuitDirName}/utils.cpp ${circuitDirName}/fr.cpp ${circuitDirName}/fr.o ${cFilepath} -o ${binaryFilePath} -lgmp -std=c++11 -O3 -fopenmp -DSANITY_CHECK`;
} else throw 'Unsupported platform';
shelljs.exec(cmd);
}

class CircuitTester {
circuit: any;
component: any;
name: string;
circuitDirName: string;
r1csFilepath: string;
symFilepath: string;
binaryFilePath: string;
r1cs: any;
symbols: any;
alwaysRecompile: boolean;
verbose: boolean;
writeExpectedOutput: boolean;
constructor(name, { alwaysRecompile = true, verbose = false } = {}) {
this.name = name;
// we can specify cached files to avoid compiling every time
this.alwaysRecompile = alwaysRecompile;
this.verbose = verbose;
}

async compileAndload(circuitDirName) {
this.circuitDirName = path.resolve(circuitDirName);
const { r1csFilepath, symFilepath, binaryFilePath } = parepareCircuitDir(this.circuitDirName, {
alwaysRecompile: this.alwaysRecompile,
verbose: this.verbose,
});
this.binaryFilePath = binaryFilePath;

this.r1cs = await loadR1cs(r1csFilepath, true, false);
this.symbols = await readSymbols(symFilepath);
}

generateWitness(inputFilePath, witnessFilePath) {
var cmd: string;
if (witnessFilePath == '') {
witnessFilePath = path.join(path.dirname(inputFilePath), 'witness.json');
}
if (this.binaryFilePath == '' || !fs.existsSync(this.binaryFilePath)) {
throw new Error('invalid bin ' + this.binaryFilePath);
}
// gen witness
cmd = `${this.binaryFilePath} ${inputFilePath} ${witnessFilePath}`;
const genWtnsOut = shelljs.exec(cmd);
if (genWtnsOut.stderr || genWtnsOut.code != 0) {
console.error(genWtnsOut.stderr);
throw new Error('Could not generate witness');
}
return witnessFilePath;
}

async checkWitness(witness, expectedOutputJson) {
const F = new ZqField(this.r1cs.prime);
// const nVars = r1cs.nVars;
const constraints = this.r1cs.constraints;
await checkConstraints(F, constraints, witness);
// assert output
await assertOut(this.symbols, witness, expectedOutputJson);
return true;
}

async checkInputOutputFile(inputFilePath, expectedOutputFile) {
const outputJsonFilePath = this.generateWitness(inputFilePath, '');
const witness = JSON.parse(fs.readFileSync(outputJsonFilePath).toString());
const expectedOutputJson = JSON.parse(fs.readFileSync(expectedOutputFile).toString());
return this.checkWitness(witness, expectedOutputJson);
}
async checkInputOutput(input, expectedOutputJson) {
const inputFilePath = path.join(this.circuitDirName, 'input.json');
await writeJsonWithBigint(inputFilePath, input);
const outputJsonFilePath = this.generateWitness(inputFilePath, '');
const witness = JSON.parse(fs.readFileSync(outputJsonFilePath).toString());
return this.checkWitness(witness, expectedOutputJson);
}
}

async function testCircuitDir(circuitDir) {
let tester = new CircuitTester(path.basename(circuitDir), { alwaysRecompile: false, verbose: true });
await tester.compileAndload(circuitDir);
for (const testCaseName of fs.readdirSync(path.join(circuitDir, 'data'))) {
const dataDir = path.join(circuitDir, 'data', testCaseName);
console.log('\ntest', dataDir);
await tester.checkInputOutputFile(path.join(dataDir, 'input.json'), path.join(dataDir, 'output.json'));
}
}

async function writeCircuitIntoDir(circuitDir, component) {
fs.mkdirSync(circuitDir, { recursive: true });
const circuitFilePath = path.join(circuitDir, 'circuit.circom');
await generateMainTestCircom(circuitFilePath, component);
}

async function writeInputOutputIntoDir(dataDir, input, output) {
fs.mkdirSync(dataDir, { recursive: true });
const inputFilePath = path.join(dataDir, 'input.json');
await writeJsonWithBigint(inputFilePath, input);
const outputFilePath = path.join(dataDir, 'output.json');
await writeJsonWithBigint(outputFilePath, output);
}

async function testWithInputOutput(input, output, component, name, circuitDir = '') {
if (circuitDir == '') {
// create a tmp dir for test
const targetDir = tmp.dirSync({ prefix: `tmp-${name}-circuit` });
circuitDir = targetDir.name;
} else {
circuitDir = path.resolve(circuitDir);
//fs.mkdirSync(circuitDir, { recursive: true });
}
// write input/output/circuit into the dir
await writeCircuitIntoDir(circuitDir, component);
await writeInputOutputIntoDir(path.join(circuitDir, 'data', name), input, output);
// test the dir
await testCircuitDir(circuitDir);
console.log('test', name, 'done\n');
}

export { testWithInputOutput, CircuitTester, writeCircuitIntoDir, writeInputOutputIntoDir, parepareCircuitDir, testCircuitDir };
Loading

0 comments on commit 1519f4b

Please sign in to comment.