-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 1519f4b
Showing
13 changed files
with
3,272 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
node_modules/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
v15.11.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
**/node_modules | ||
dist/ | ||
helper.ts/rescue_wasm/ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"trailingComma": "all", | ||
"singleQuote": true, | ||
"printWidth": 140, | ||
"arrowParens": "avoid" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
Oops, something went wrong.