You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

589 lines
20 KiB

/*
Copyright 2018 0KIMS association.
This file is part of circom (Zero Knowledge Circuit Compiler).
circom is a free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
circom is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
License for more details.
You should have received a copy of the GNU General Public License
along with circom. If not, see <https://www.gnu.org/licenses/>.
*/
const Scalar = require("ffjavascript").Scalar;
const sONE = 0;
const build = require("./build");
const BuilderC = require("../ports/c/builder.js");
const BuilderWasm = require("../ports/wasm/builder.js");
const constructionPhase = require("./construction_phase");
const Ctx = require("./ctx");
const utils = require("./utils");
const buildR1cs = require("./r1csfile").buildR1cs;
const BigArray = require("./bigarray");
const buildSyms = require("./buildsyms");
const {performance} = require("perf_hooks");
module.exports = compile;
const measures = {};
function ms2String(v) {
v = Math.floor(v);
const ms = v % 1000;
v = Math.floor(v/1000);
const secs = v % 60;
v = Math.floor(v/60);
const mins = v % 60;
v = Math.floor(v/60);
const hours = v % 24;
const days = Math.floor(v/24);
let S = "";
if (days) S = S + days + "D ";
if ((S!="")||(hours)) S = S + hours.toString().padStart(2, "0") + ":";
if ((S!="")||(mins)) S = S + mins.toString().padStart(2, "0") + ":";
if ((S!="")||(secs)) S = S + secs.toString().padStart(2, "0");
S+=".";
S = S + ms.toString().padStart(3, "0");
return S;
}
async function compile(srcFile, options) {
options.prime = options.prime || Scalar.fromString("21888242871839275222246405745257275088548364400416034343698204186575808495617");
if (!options) {
options = {};
}
if (typeof options.reduceConstraints === "undefined") {
options.reduceConstraints = true;
}
const ctx = new Ctx(options.prime);
ctx.verbose= options.verbose || false;
ctx.mainComponent = options.mainComponent || "main";
ctx.newThreadTemplates = options.newThreadTemplates;
measures.constructionPhase = -performance.now();
constructionPhase(ctx, srcFile);
measures.constructionPhase += performance.now();
if (ctx.verbose) console.log("NConstraints Before: "+ctx.constraints.length);
if (ctx.verbose) console.log("NSignals Before: "+ctx.signals.length);
if (ctx.error) {
throw(ctx.error);
}
if (ctx.getComponentIdx(ctx.mainComponent)<0) {
throw new Error("A main component must be defined");
}
if (ctx.verbose) console.log("Reduce Constants");
measures.reduceConstants = -performance.now();
reduceConstants(ctx);
measures.reduceConstants += performance.now();
if (options.reduceConstraints) {
if (ctx.verbose) console.log("Reduce Constraints");
// Repeat while reductions are performed
/*
let oldNConstrains = -1;
while (ctx.constraints.length != oldNConstrains) {
if (ctx.verbose) console.log("Reducing constraints: "+ctx.constraints.length);
oldNConstrains = ctx.constraints.length;
reduceConstrains(ctx);
}
*/
measures.reduceConstraints = -performance.now();
await reduceConstrains(ctx);
measures.reduceConstraints += performance.now();
}
if (ctx.verbose) console.log("NConstraints After: "+ctx.constraints.length);
if (ctx.verbose) console.log("Classify Signals");
measures.classifySignals = -performance.now();
classifySignals(ctx);
measures.classifySignals += performance.now();
measures.generateWitnessNames = -performance.now();
generateWitnessNames(ctx);
measures.generateWitnessNames += performance.now();
if (ctx.error) {
throw(ctx.error);
}
if (options.r1csFileName) {
measures.generateR1cs = -performance.now();
await buildR1cs(ctx, options.r1csFileName);
measures.generateR1cs += performance.now();
}
if (ctx.error) throw(ctx.error);
delete ctx.constraints; // Liberate memory.
if (options.cSourceFile) {
if (ctx.verbose) console.log("Generating c...");
measures.generateC = -performance.now();
ctx.builder = new BuilderC(options.prime, ctx.verbose);
build(ctx);
await ctx.builder.build(options.cSourceFile, options.dataFile);
measures.generateC += performance.now();
}
if (ctx.error) throw(ctx.error);
if ((options.wasmFile)||(options.watFile)) {
if (ctx.verbose) console.log("Generating wasm...");
measures.generateWasm = -performance.now();
ctx.builder = new BuilderWasm(options.prime);
build(ctx);
if (options.wasmFile) {
await ctx.builder.build(options.wasmFile, "wasm");
}
if (options.watFile) {
await ctx.builder.build(options.watFile, "wat");
}
measures.generateWasm += performance.now();
}
// const mainCode = gen(ctx,ast);
if (ctx.error) throw(ctx.error);
if (options.symWriteStream) {
measures.generateSyms = -performance.now();
const rdStream = buildSyms(ctx);
rdStream.pipe(options.symWriteStream);
measures.generateSyms += performance.now();
await new Promise(fulfill => options.symWriteStream.on("finish", fulfill));
}
// const def = buildCircuitDef(ctx, mainCode);
if (ctx.verbose) {
for (let [mStr, mValue] of Object.entries(measures)) {
console.log(mStr + ": " + ms2String(mValue));
}
}
}
function classifySignals(ctx) {
const ERROR = 0xFFFF;
function priorize(t1, t2) {
if ((t1 == ERROR) || (t2==ERROR)) return ERROR;
if ((t1 == ctx.stONE) || (t2 == ctx.stONE)) return ctx.stONE;
if ((t1 == ctx.stOUTPUT) || (t2 == ctx.stOUTPUT)) return ctx.stOUTPUT;
if ((t1 == ctx.stPUBINPUT) || (t2 == ctx.stPUBINPUT)) return ctx.stPUBINPUT;
if ((t1 == ctx.stPRVINPUT) || (t2 == ctx.stPRVINPUT)) return ctx.stPRVINPUT;
if ((t1 == ctx.stINTERNAL) || (t2 == ctx.stINTERNAL)) return ctx.stINTERNAL;
if ((t1 == ctx.stCONSTANT) || (t2 == ctx.stCONSTANT)) return ctx.stCONSTANT;
if ((t1 == ctx.stDISCARDED) || (t2 == ctx.stDISCARDED)) return ctx.stDISCARDED;
if (t1!=t2) return ERROR;
return t1;
}
for (let i=0; i<ctx.constraints.length; i++) {
if ((ctx.verbose)&&(i%100000 == 0)) console.log(`marking as internal: ${i}/${ctx.constraints.length}`);
const c = ctx.constraints[i];
for (let s in c.a.coefs) ctx.signals[s].c = ctx.stINTERNAL;
for (let s in c.b.coefs) ctx.signals[s].c = ctx.stINTERNAL;
for (let s in c.c.coefs) ctx.signals[s].c = ctx.stINTERNAL;
}
// First classify the signals
for (let s=0; s<ctx.signals.length; s++) {
if ((ctx.verbose)&&(s%100000 == 0)) console.log(`classify signals: ${s}/${ctx.signals.length}`);
const signal = ctx.signals[s];
let tAll = ctx.stDISCARDED;
let lSignal = signal;
let end = false;
while (!end) {
let t = lSignal.c || ctx.stDISCARDED;
if (s == 0) {
t = ctx.stONE;
} else if (lSignal.o & ctx.MAIN) {
if (lSignal.o & ctx.IN) {
if (lSignal.o & ctx.PRV) {
t = ctx.stPRVINPUT;
} else {
t = ctx.stPUBINPUT;
}
} else if (lSignal.o & ctx.OUT) {
t = ctx.stOUTPUT;
}
} else if (utils.isDefined(lSignal.v)) {
t = ctx.stCONSTANT;
}
tAll = priorize(t,tAll);
if (lSignal.e>=0) {
lSignal = ctx.signals[lSignal.e];
} else {
end=true;
}
}
if (tAll == ERROR) {
throw new Error("Incompatible types in signal: " + s);
}
lSignal.c = tAll;
}
}
function generateWitnessNames(ctx) {
const totals = {};
totals[ctx.stONE] = 0;
totals[ctx.stOUTPUT] = 0;
totals[ctx.stPUBINPUT] = 0;
totals[ctx.stPRVINPUT] = 0;
totals[ctx.stINTERNAL] = 0;
totals[ctx.stDISCARDED] = 0;
totals[ctx.stCONSTANT] = 0;
const ids = {};
// First classify the signals
for (let s=0; s<ctx.signals.length; s++) {
if ((ctx.verbose)&&(s%10000 == 0)) console.log("generate witness (counting): ", s);
const signal = ctx.signals[s];
let lSignal = signal;
while (lSignal.e>=0) lSignal = ctx.signals[lSignal.e];
if (!( lSignal.o & ctx.COUNTED) ) {
lSignal.o |= ctx.COUNTED;
totals[lSignal.c] ++;
}
}
ids[ctx.stONE] = 0;
ids[ctx.stOUTPUT] = 1;
ids[ctx.stPUBINPUT] = ids[ctx.stOUTPUT] + totals[ctx.stOUTPUT];
ids[ctx.stPRVINPUT] = ids[ctx.stPUBINPUT] + totals[ctx.stPUBINPUT];
ids[ctx.stINTERNAL] = ids[ctx.stPRVINPUT] + totals[ctx.stPRVINPUT];
ids[ctx.stDISCARDED] = ids[ctx.stINTERNAL] + totals[ctx.stINTERNAL];
ids[ctx.stCONSTANT] = ids[ctx.stDISCARDED] + totals[ctx.stDISCARDED];
const nSignals = ids[ctx.stCONSTANT] + totals[ctx.stCONSTANT];
for (let s=0; s<ctx.signals.length; s++) {
if ((ctx.verbose)&&(s%10000 == 0)) console.log("seting id: ", s);
const signal = ctx.signals[s];
let lSignal = signal;
while (lSignal.e>=0) {
lSignal = ctx.signals[lSignal.e];
}
if ( typeof(lSignal.id) === "undefined" ) {
lSignal.id = ids[lSignal.c] ++;
}
signal.id = lSignal.id;
}
ctx.totals = totals;
}
function reduceConstants(ctx) {
const newConstraints = new BigArray();
for (let i=0; i<ctx.constraints.length; i++) {
if ((ctx.verbose)&&(i%10000 == 0)) console.log("reducing constants: ", i);
const c = ctx.lc.canonize(ctx, ctx.constraints[i]);
if (!ctx.lc.isZero(c)) {
newConstraints.push(c);
}
delete ctx.constraints[i];
}
ctx.constraints = newConstraints;
}
async function reduceConstrains(ctx) {
const sig2constraint = new BigArray();
let removedSignals = new BigArray();
let nRemoved;
let lIdx;
let possibleConstraints = new BigArray(ctx.constraints.length);
let nextPossibleConstraints;
for (let i=0; i<ctx.constraints.length; i++) {
if ((ctx.verbose)&&(i%100000 == 0)) console.log(`indexing constraints: ${i}/${ctx.constraints.length}`);
const insertedSig = { 0: true}; // Do not insert one.
const c = ctx.constraints[i];
for (let s in c.a.coefs) {
if (!insertedSig[s]) {
if (!sig2constraint[s]) sig2constraint[s] = {};
sig2constraint[s][i] = true;
insertedSig[s] = true;
}
}
for (let s in c.b.coefs) {
if (!insertedSig[s]) {
if (!sig2constraint[s]) sig2constraint[s] = {};
sig2constraint[s][i] = true;
insertedSig[s] = true;
}
}
for (let s in c.c.coefs) {
if (!insertedSig[s]) {
if (!sig2constraint[s]) sig2constraint[s] = {};
sig2constraint[s][i] = true;
insertedSig[s] = true;
}
}
possibleConstraints[i] = ctx.constraints.length - i -1;
}
let totalRemoved = 0;
while (possibleConstraints.length >0) {
if (possibleConstraints.length>1<<20) {
nextPossibleConstraints = new BigArray();
} else {
nextPossibleConstraints = {};
}
removedSignals = {};
nRemoved = 0;
lIdx = {};
for (let i=0;i<possibleConstraints.length;i++) {
if ((ctx.verbose)&&(i%10000 == 0)) {
await Promise.resolve();
console.log(`reducing constraints: ${i}/${possibleConstraints.length} reduced: ${nRemoved}`);
}
const c = ctx.constraints[possibleConstraints[i]];
if (!c) continue;
// Limit of number of lelements removed per step
if (nRemoved>5000000) {
nextPossibleConstraints[possibleConstraints[i]] = true;
continue;
}
// Swap a and b if b has more variables.
if (Object.keys(c.b).length > Object.keys(c.a).length) {
const aux = c.a;
c.a=c.b;
c.b=aux;
}
// Mov to C if possible.
if (isConstant(c.a)) {
const ct = {t: "N", v: c.a.coefs[sONE]};
c.c = ctx.lc.add(ctx.lc.mul(c.b, ct), c.c);
c.a = { t: "LC", coefs: {} };
c.b = { t: "LC", coefs: {} };
}
if (isConstant(c.b)) {
const ct = {t: "N", v: c.b.coefs[sONE]};
c.c = ctx.lc.add(ctx.lc.mul(c.a, ct), c.c);
c.a = { t: "LC", coefs: {} };
c.b = { t: "LC", coefs: {} };
}
if (ctx.lc.isZero(c.a) || ctx.lc.isZero(c.b)) {
const freeC = substituteRemoved(c.c);
const isolatedSignal = getFirstInternalSignal(ctx, freeC);
if (isolatedSignal) {
removedSignals[isolatedSignal] = isolateSignal(freeC, isolatedSignal);
if (lIdx[isolatedSignal]) {
const sigs = Object.keys(lIdx[isolatedSignal]);
for (let k=0; k<sigs.length; k++) {
const s = sigs[k];
const oldLC = removedSignals[s];
removedSignals[s] = substitute(removedSignals[s], isolatedSignal, removedSignals[isolatedSignal]);
if (oldLC !== removedSignals[s]) addTolIdx(removedSignals[s], s);
}
}
addTolIdx(removedSignals[isolatedSignal], isolatedSignal);
ctx.constraints[possibleConstraints[i]] = null;
nRemoved ++;
delete lIdx[isolatedSignal];
const cts = Object.keys(sig2constraint[isolatedSignal]);
for (let k=0; k<cts.length; k++) {
if (ctx.constraints[cts[k]]) nextPossibleConstraints[cts[k]] = true;
}
}
}
}
if (nextPossibleConstraints.getKeys) {
nextPossibleConstraints = nextPossibleConstraints.getKeys();
} else {
nextPossibleConstraints = Object.keys(nextPossibleConstraints);
}
for (let i=0; i<nextPossibleConstraints.length;i++) {
if ((ctx.verbose)&&(i%10000 == 0)) {
await Promise.resolve();
console.log(`substituting constraints: ${i}/${nextPossibleConstraints.length}`);
}
const c = ctx.constraints[nextPossibleConstraints[i]];
if (c) {
const nc = {
a: substituteRemoved(c.a, nextPossibleConstraints[i]),
b: substituteRemoved(c.b, nextPossibleConstraints[i]),
c: substituteRemoved(c.c, nextPossibleConstraints[i])
};
if (ctx.lc.isZero(nc)) {
delete ctx.constraints[nextPossibleConstraints[i]];
} else {
ctx.constraints[nextPossibleConstraints[i]] = nc;
}
}
}
const removedSignalsList = Object.keys(removedSignals);
for (let i=0; i<removedSignalsList.length; i++) {
if ((ctx.verbose )&&(i%100000 == 0)) console.log(`removing signals: ${i}/${removedSignalsList.length}`);
const s = removedSignalsList[i];
let lSignal = ctx.signals[s];
while (lSignal.e>=0) {
lSignal = ctx.signals[lSignal.e];
}
sig2constraint[s] = null;
}
/*
possibleConstraints = new BigArray();
// Reverse
for (let i=0; i<nextPossibleConstraints.length; i++) {
possibleConstraints[i] = nextPossibleConstraints[nextPossibleConstraints.length -1 -i];
}
*/
possibleConstraints = nextPossibleConstraints;
totalRemoved += nRemoved;
if (ctx.verbose) console.log(`Removed: ${totalRemoved} TotalConstraints: ${ctx.constraints.length}` );
}
let o=0;
for (let i=0; i<ctx.constraints.length;i++) {
if ((ctx.verbose)&&(i%100000 == 0)) console.log(`reordering constraints: ${i}/${ctx.constraints.length}`);
if (ctx.constraints[i]) {
if (!ctx.lc.isZero(ctx.constraints[i])) {
ctx.constraints[o] = ctx.constraints[i];
o++;
}
}
}
ctx.constraints.length = o;
function getFirstInternalSignal(ctx, l) {
for (let k in l.coefs) {
k = Number(k);
const signal = ctx.signals[k];
if ( ( ((signal.o & ctx.MAIN) == 0)
||( ((signal.o & ctx.IN) == 0)
&&((signal.o & ctx.OUT) == 0)))
&&((signal.o & ctx.ONE) ==0)
&&(!ctx.F.isZero(l.coefs[k]))
&&(!removedSignals[k])) return k;
}
return null;
}
function isolateSignal(lc, s) {
const eq = {
t: "LC",
coefs: {}
};
const invCoef = ctx.F.inv(lc.coefs[s]);
for (const k in lc.coefs) {
if (k != s) {
const v = ctx.F.mul( ctx.F.neg(lc.coefs[k]), invCoef);
if (!ctx.F.isZero(v)) {
eq.coefs[k] = v;
}
}
}
return eq;
}
function substituteRemoved(lc, idxConstraint) {
const newLc = ctx.lc._clone(lc);
for (let k in lc.coefs) {
if (removedSignals[k]) {
delete newLc.coefs[k];
for (let k2 in removedSignals[k].coefs) {
const newP = ctx.F.mul(removedSignals[k].coefs[k2], lc.coefs[k]);
if (!ctx.F.isZero(newP)) {
if (newLc.coefs[k2]) {
newLc.coefs[k2] = ctx.F.add(newLc.coefs[k2], newP);
if (ctx.F.isZero(newLc.coefs[k2])) delete newLc.coefs[k2];
} else {
newLc.coefs[k2] = newP;
}
}
if ((typeof idxConstraint != "undefined")&&(k2!=0)) {
if (!sig2constraint[k2]) sig2constraint[k2] = {};
sig2constraint[k2][idxConstraint] = true;
}
}
}
}
return newLc;
}
function substitute(lc, s, eq) {
if (!lc.coefs[s]) return lc;
const newLc = ctx.lc._clone(lc);
delete newLc.coefs[s];
for (let k2 in eq.coefs) {
const newP = ctx.F.mul(eq.coefs[k2], lc.coefs[s]);
if (!ctx.F.isZero(newP)) {
if (newLc.coefs[k2]) {
newLc.coefs[k2] = ctx.F.add(newLc.coefs[k2], newP);
if (ctx.F.isZero(newLc.coefs[k2])) delete newLc.coefs[k2];
} else {
newLc.coefs[k2] = newP;
}
}
}
return newLc;
}
function isConstant(l) {
for (let k in l.coefs) {
if ((k != sONE) && (!ctx.F.isZero(l.coefs[k]))) return false;
}
if (!l.coefs[sONE] || ctx.F.isZero(l.coefs[sONE])) return false;
return true;
}
function addTolIdx(lc, newS) {
const sigs = Object.keys(lc.coefs);
for (let k=0; k<sigs.length; k++) {
const s = sigs[k];
if (s) {
if (!lIdx[s]) lIdx[s] = {};
lIdx[s][newS] = true;
}
}
}
}