arnaucube
arnaucube/circom_testerPublic Self-hosted repository
first version of wasm tester
alrubio alberu04@ucm.es
Wed, 06 Oct 2021 11:40:45 +0200
commit

d7151baf8115809d259f4916b5a0858a61c84298

parent

28bac7659fb836f666c909acabd91b6ef7aca2ba

3 files changed, 478 insertions(+), 0 deletions(-)

jump to
A wasm/tester.js

@@ -0,0 +1,198 @@

+const chai = require("chai"); +const assert = chai.assert; + +const fs = require("fs"); +var tmp = require("tmp-promise"); +const path = require("path"); + +const util = require("util"); +const exec = util.promisify(require("child_process").exec); + +const loadR1cs = require("r1csfile").load; +const ZqField = require("ffjavascript").ZqField; + +module.exports = wasm_tester; + +async function wasm_tester(circomInput, _options) { + + assert(await compiler_above_version("2.0.0"),"Wrong compiler version. Must be at least 2.0.0"); + + tmp.setGracefulCleanup(); + + const dir = await tmp.dir({prefix: "circom_", unsafeCleanup: true }); + + //console.log(dir.path); + + const baseName = path.basename(circomInput, ".circom"); + const options = Object.assign({}, _options); + + options.wasm = true; + + options.sym = true; + options.json = options.json || false; // costraints in json format + options.r1cs = true; + options.output = dir.path; + + await compile(circomInput, options); + + const utils = require("./utils"); + const WitnessCalculator = require("./witness_calculator"); + + const wasm = await fs.promises.readFile(path.join(dir.path, baseName+"_js/"+ baseName + ".wasm")); + + const wc = await WitnessCalculator(wasm); + + return new WasmTester(dir, baseName, wc); +} + +async function compile (fileName, options) { + var flags = "--wasm "; + if (options.sym) flags += "--sym "; + if (options.r1cs) flags += "--r1cs "; + if (options.json) flags += "--json "; + if (options.output) flags += "--output " + options.output + " "; + console.log(circom + flags + fileName); + + await exec("circom " + flags + fileName); +} + +class WasmTester { + + constructor(dir, baseName, witnessCalculator) { + this.dir=dir; + this.baseName = baseName; + this.witnessCalculator = witnessCalculator; + } + + async release() { + await this.dir.cleanup(); + } + + async calculateWitness(input, sanityCheck) { + return await this.witnessCalculator.calculateWitness(input, sanityCheck); + } + + async loadSymbols() { + if (this.symbols) return; + this.symbols = {}; + const symsStr = await fs.promises.readFile( + path.join(this.dir.path, 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 loadR1cs(path.join(this.dir.path, this.baseName + ".r1cs"),true, false); + self.F = new ZqField(r1cs.prime); + 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") { + assert(false, "Output variable not defined: "+ prefix); + } + const ba = actualOut[self.symbols[prefix].varIdx].toString(); + const be = eOut.toString(); + assert.strictEqual(ba, be, prefix); + } + } + } + + async getDecoratedOutput(witness) { + const self = this; + const lines = []; + if (!self.symbols) await self.loadSymbols(); + for (let n in self.symbols) { + let v; + if (utils.isDefined(witness[self.symbols[n].varIdx])) { + v = witness[self.symbols[n].varIdx].toString(); + } else { + v = "undefined"; + } + lines.push(`${n} --> ${v}`); + } + return lines.join("\n"); + } + + async checkConstraints(witness) { + const self = this; + if (!self.constraints) await self.loadConstraints(); + for (let i=0; i<self.constraints.length; i++) { + checkConstraint(self.constraints[i]); + } + + function checkConstraint(constraint) { + + const F = self.F; + 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) { + const F = self.F; + let v = F.zero; + for (let w in lc) { + v = F.add( + v, + F.mul( lc[w], witness[w] ) + ); + } + return v; + } + } + +} + +function version_to_list ( v ) { + return v.split(".").map(function(x) { + return 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) { + let output = await exec('circom --version').toString(); + let compiler_version = version_to_list(output.slice(output.search(/\d/),-1)); + vlist = version_to_list(v); + return check_versions ( compiler_version, vlist ); +} +
A wasm/utils.js

@@ -0,0 +1,53 @@

+const fnv = require("fnv-plus"); + +module.exports.fnvHash = fnvHash; +module.exports.toArray32 = toArray32; +module.exports.fromArray32 = fromArray32; +module.exports.flatArray = flatArray; + +function toArray32(s,size) { + const res = []; //new Uint32Array(size); //has no unshift + let rem = BigInt(s); + const radix = BigInt(0x100000000); + while (rem) { + res.unshift( Number(rem % radix)); + rem = rem / radix; + } + if (size) { + var i = size - res.length; + while (i>0) { + res.unshift(0); + i--; + } + } + return res; +} + +function fromArray32(arr) { //returns a BigInt + var res = BigInt(0); + const radix = BigInt(0x100000000); + for (let i = 0; i<arr.length; i++) { + res = res*radix + BigInt(arr[i]); + } + return res; +} + +function flatArray(a) { + var res = []; + fillArray(res, a); + return res; + + function fillArray(res, a) { + if (Array.isArray(a)) { + for (let i=0; i<a.length; i++) { + fillArray(res, a[i]); + } + } else { + res.push(a); + } + } +} + +function fnvHash(str) { + return fnv.hash(str, 64).hex(); +}
A wasm/witness_calculator.js

@@ -0,0 +1,227 @@

+const utils = require("./utils"); + +module.exports = async function builder(code, options) { + + options = options || {}; + + const wasmModule = await WebAssembly.compile(code); + + let wc; + + + const instance = await WebAssembly.instantiate(wasmModule, { + runtime: { + exceptionHandler : function(code) { + let errStr; + if (code == 1) { + errStr= "Signal not found. "; + } else if (code == 2) { + errStr= "Too many signals set. "; + } else if (code == 3) { + errStr= "Signal already set. "; + } else if (code == 4) { + errStr= "Assert Failed. "; + } else if (code == 5) { + errStr= "Not enough memory. "; + } else { + errStr= "Unknown error\n"; + } + // get error message from wasm + errStr += getMessage(); + throw new Error(errStr); + }, + showSharedRWMemory: function() { + printSharedRWMemory (); + } + + } + }); + + const sanityCheck = + options +// options && +// ( +// options.sanityCheck || +// options.logGetSignal || +// options.logSetSignal || +// options.logStartComponent || +// options.logFinishComponent +// ); + + + wc = new WitnessCalculator(instance, sanityCheck); + return wc; + + function getMessage() { + var message = ""; + var c = instance.exports.getMessageChar(); + while ( c != 0 ) { + message += String.fromCharCode(c); + c = instance.exports.getMessageChar(); + } + return message; + } + + function printSharedRWMemory () { + const shared_rw_memory_size = instance.exports.getFieldNumLen32(); + const arr = new Uint32Array(shared_rw_memory_size); + for (let j=0; j<shared_rw_memory_size; j++) { + arr[shared_rw_memory_size-1-j] = instance.exports.readSharedRWMemory(j); + } + console.log(utils.fromArray32(arr)); + } + +}; + +class WitnessCalculator { + constructor(instance, sanityCheck) { + this.instance = instance; + + this.version = this.instance.exports.getVersion(); + this.n32 = this.instance.exports.getFieldNumLen32(); + +// Not needed +// this.instance.exports.getRawPrime(); +// const arr = new Array(this.n32); +// for (let i=0; i<this.n32; i++) { +// arr[this.n32-1-i] = this.instance.exports.readSharedRWMemory(i); +// } +// this.prime = utils.fromArray32(arr); + + this.witnessSize = this.instance.exports.getWitnessSize(); + + this.sanityCheck = sanityCheck; + } + + async _doCalculateWitness(input, sanityCheck) { + //input is assumed to be a map from signals to arrays of bigints + this.instance.exports.init((this.sanityCheck || sanityCheck) ? 1 : 0); + const keys = Object.keys(input); + keys.forEach( (k) => { + const h = utils.fnvHash(k); + const hMSB = parseInt(h.slice(0,8), 16); + const hLSB = parseInt(h.slice(8,16), 16); + const fArr = utils.flatArray(input[k]); + for (let i=0; i<fArr.length; i++) { + const arrFr = utils.toArray32(fArr[i],this.n32) + for (let j=0; j<this.n32; j++) { + this.instance.exports.writeSharedRWMemory(j,arrFr[this.n32-1-j]); + } + try { + this.instance.exports.setInputSignal(hMSB, hLSB,i); + } catch (err) { + // console.log(`After adding signal ${i} of ${k}`) + throw new Error(err); + } + } + + }); + } + + async calculateWitness(input, sanityCheck) { + + const w = []; + + await this._doCalculateWitness(input, sanityCheck); + + for (let i=0; i<this.witnessSize; i++) { + this.instance.exports.getWitness(i); + const arr = new Uint32Array(this.n32); + for (let j=0; j<this.n32; j++) { + arr[this.n32-1-j] = this.instance.exports.readSharedRWMemory(j); + } + w.push(utils.fromArray32(arr)); + } + + return w; + } + + + async calculateBinWitness(input, sanityCheck) { + + const buff32 = new Uint32Array(this.witnessSize*this.n32); + const buff = new Uint8Array( buff32.buffer); + await this._doCalculateWitness(input, sanityCheck); + + for (let i=0; i<this.witnessSize; i++) { + this.instance.exports.getWitness(i); + const pos = i*this.n32; + for (let j=0; j<this.n32; j++) { + buff32[pos+j] = this.instance.exports.readSharedRWMemory(j); + } + } + + return buff; + } + + + async calculateWTNSBin(input, sanityCheck) { + + const buff32 = new Uint32Array(this.witnessSize*this.n32+this.n32+11); + const buff = new Uint8Array( buff32.buffer); + await this._doCalculateWitness(input, sanityCheck); + + //"wtns" + buff[0] = "w".charCodeAt(0) + buff[1] = "t".charCodeAt(0) + buff[2] = "n".charCodeAt(0) + buff[3] = "s".charCodeAt(0) + + //version 2 + buff32[1] = 2; + + //number of sections: 2 + buff32[2] = 2; + + //id section 1 + buff32[3] = 1; + + const n8 = this.n32*4; + //id section 1 length in 64bytes + const idSection1length = 8 + n8; + const idSection1lengthHex = idSection1length.toString(16); + buff32[4] = parseInt(idSection1lengthHex.slice(0,8), 16); + buff32[5] = parseInt(idSection1lengthHex.slice(8,16), 16); + + //this.n32 + buff32[6] = n8; + + //prime number + this.instance.exports.getRawPrime(); + + var pos = 7; + for (let j=0; j<this.n32; j++) { + buff32[pos+j] = this.instance.exports.readSharedRWMemory(j); + } + pos += this.n32; + + // witness size + buff32[pos] = this.witnessSize; + pos++; + + //id section 2 + buff32[pos] = 2; + pos++; + + // section 2 length + const idSection2length = n8*this.witnessSize; + const idSection2lengthHex = idSection2length.toString(16); + buff32[pos] = parseInt(idSection2lengthHex.slice(0,8), 16); + buff32[pos+1] = parseInt(idSection2lengthHex.slice(8,16), 16); + + pos += 2; + for (let i=0; i<this.witnessSize; i++) { + this.instance.exports.getWitness(i); + for (let j=0; j<this.n32; j++) { + buff32[pos+j] = this.instance.exports.readSharedRWMemory(j); + } + pos += this.n32; + } + + return buff; + } + +} + + +