micro-zk-proofs
Version:
Create & verify zero-knowledge SNARK proofs in parallel, using noble cryptography
556 lines • 22.5 kB
JavaScript
/**
* The code is only used if you plan to run **legacy circom-js programs**. It is unused in WASM.
* Minimal witness program executor for circom programs, based on websnark/wasmsnark/snarkjs.
* Unsafe: it uses eval, better to be used inside worker threads.
* Depends on **monkey-patched BigInt** prototypes due to how circom programs are serialized.
* We only patch prototypes before execution. After finishing, patches are reverted.
* @module
*/
import { invert, pow } from '@noble/curves/abstract/modular.js';
import { bn254 as nobleBn254 } from '@noble/curves/bn254.js';
import { bitMask } from '@noble/curves/utils.js';
import * as P from 'micro-packed';
import {} from "./index.js";
function monkeyPatchBigInt() {
const methods = {
// Equality
eq: (a, b) => a === b,
neq: (a, b) => a !== b,
greaterOrEquals: (a, b) => a >= b,
greater: (a, b) => a > b,
gt: (a, b) => a > b,
lesserOrEquals: (a, b) => a <= b,
lesser: (a, b) => a < b,
lt: (a, b) => a < b,
// Basic math
sub: (a, b) => a - b,
add: (a, b) => a + b,
mul: (a, b) => a * b,
div: (a, b) => a / b,
mod: (a, b) => a % b,
// Fields
inverse: (n, modulo) => invert(n, modulo),
modPow: (a, power, modulo) => pow(a, power, modulo),
// Binary
and: (a, b) => a & b,
shr: (a, b) => a >> BigInt(b),
};
let patched = false;
let orig = {};
const proto = BigInt.prototype;
return {
patch() {
if (patched)
throw new Error('bigint: already patched');
for (const name in methods) {
// Preserve descriptors: callers may have accessors or own undefined-valued properties here.
orig[name] = Object.getOwnPropertyDescriptor(proto, name);
Object.defineProperty(proto, name, {
configurable: true,
enumerable: orig[name]?.enumerable || false,
value: function (...args) {
return methods[name](this, ...args);
},
writable: true,
});
}
patched = true;
},
restore() {
if (!patched)
throw new Error('bigint: not patched');
for (const name in methods) {
if (!orig[name])
delete proto[name];
else
Object.defineProperty(proto, name, orig[name]);
}
orig = {};
patched = false;
},
};
}
const selectorStr = (lst) => lst.map((i) => `[${i}]`).join('');
const signalStr = (name, selectors) => name + selectorStr(selectors);
// Apply selectors
const select = (a, selectors) => {
for (const s of selectors)
a = a[s];
return a;
};
/**
* Builds a witness generator for a legacy circom-js circuit JSON.
* @param circJson - Circom circuit JSON artifact.
* @returns Function that executes the circuit and returns the witness.
* @example
* Build a witness runner from a circom JSON circuit artifact.
* ```ts
* import { generateWitness } from 'micro-zk-proofs/witness.js';
* // Addition circuit: witness output is one, a + b, b, a.
* const circuitJson = {
* nVars: 4,
* nInputs: 2,
* nOutputs: 1,
* nSignals: 4,
* templates: {
* Main: `function(ctx) {
* ctx.setSignal(
* "out",
* [],
* bigInt(ctx.getSignal("a", [])).add(bigInt(ctx.getSignal("b", []))).mod(__P__)
* );
* }`,
* },
* functions: {},
* components: [{ name: 'main', params: {}, template: 'Main', inputSignals: 2 }],
* signals: [
* { names: ['one'], triggerComponents: [] },
* { names: ['main.out'], triggerComponents: [] },
* { names: ['main.b'], triggerComponents: [0] },
* { names: ['main.a'], triggerComponents: [0] },
* ],
* signalName2Idx: { one: 0, 'main.out': 1, 'main.b': 2, 'main.a': 3 },
* };
* const witness = generateWitness(circuitJson)({ a: '33', b: '34' });
* // [1n, 67n, 34n, 33n]
* ```
*/
export function generateWitness(circJson) {
const P = nobleBn254.fields.Fr.ORDER;
const MASK = bitMask(nobleBn254.fields.Fr.BITS);
const signals = circJson.signals;
const components = circJson.components;
const templates = {};
// Bind P & MASK directly into templates/functions, so we see dependency
for (let t in circJson.templates) {
templates[t] = new Function('bigInt', '__P__', '__MASK__', 'return ' + circJson.templates[t])(BigInt, P, MASK);
}
const functions = {};
for (let f in circJson.functions) {
functions[f] = {
params: circJson.functions[f].params,
func: new Function('bigInt', '__P__', '__MASK__', 'return ' + circJson.functions[f].func)(BigInt, P, MASK),
};
}
function inputIdx(i) {
if (i >= circJson.nInputs)
throw new Error('Accessing an invalid input: ' + i);
// Witness slot 0 is the constant one, so declared inputs start after the output slots.
return circJson.nOutputs + 1 + i;
}
function getSignalIdx(name) {
if (circJson.signalName2Idx[name] !== undefined)
return circJson.signalName2Idx[name];
// signalNames() also queries raw witness indices when building error messages.
if (!isNaN(name))
return Number(name);
throw new Error('Invalid signal identifier: ' + name);
}
const signalNames = (i) => signals[getSignalIdx(i)].names.join(', ');
const patcher = monkeyPatchBigInt();
return function (input) {
const witness = new Array(circJson.nSignals);
let currentComponent;
let scopes = []; // scope stack
const notInitSignals = {};
function inScope(newScope, cb) {
const oldScope = scopes;
scopes = [scopes[0], newScope];
const res = cb();
scopes = oldScope;
return res;
}
function triggerComponent(c) {
notInitSignals[c]--;
const oldComponent = currentComponent;
currentComponent = components[c].name;
const template = components[c].template;
const newScope = {};
for (let p in components[c].params)
newScope[p] = components[c].params[p];
inScope(newScope, () => templates[template](ctx));
currentComponent = oldComponent;
}
function setSignalFullName(fullName, value) {
const sId = getSignalIdx(fullName);
let firstInit = false;
if (witness[sId] === undefined)
firstInit = true;
witness[sId] = BigInt(value);
const callComponents = [];
for (let i = 0; i < signals[sId].triggerComponents.length; i++) {
var idCmp = signals[sId].triggerComponents[i];
if (firstInit)
notInitSignals[idCmp]--;
callComponents.push(idCmp);
}
callComponents.map((c) => {
if (notInitSignals[c] == 0)
triggerComponent(c);
});
return witness[sId];
}
function getSignalFullName(name) {
const id = getSignalIdx(name);
if (witness[id] === undefined)
throw new Error('Signal not initialized: ' + name);
return witness[id];
}
const cName = (name) => (name == 'one' ? 'one' : currentComponent + '.' + name);
// Minimal API that used inside evaluated code
const ctx = {
// Pins
setPin(compName, compSel, sigName, sigSel, value) {
const name = signalStr(cName(compName), compSel) + '.' + signalStr(sigName, sigSel);
setSignalFullName(name, value);
},
getPin(compName, componentSels, sigName, sigSel) {
const name = signalStr(cName(compName), componentSels) + '.' + signalStr(sigName, sigSel);
return getSignalFullName(name);
},
// Vars
setVar(name, sels, value) {
const scope = scopes[scopes.length - 1];
if (sels.length == 0) {
scope[name] = value;
}
else {
if (scope[name] === undefined)
scope[name] = [];
let cur = scope[name];
for (let i = 0; i < sels.length - 1; i++) {
if (cur[sels[i]] === undefined)
cur[sels[i]] = [];
cur = cur[sels[i]];
}
cur[sels[sels.length - 1]] = value;
}
return value;
},
getVar(name, sels) {
for (let i = scopes.length - 1; i >= 0; i--)
if (scopes[i][name] !== undefined)
return select(scopes[i][name], sels);
throw new Error('Variable not defined: ' + name);
},
// Signals
setSignal(name, sels, value) {
setSignalFullName(signalStr(currentComponent ? currentComponent + '.' + name : name, sels), value);
},
getSignal(name, sels) {
return getSignalFullName(signalStr(cName(name), sels));
},
// Utils
callFunction(name, params) {
const newScope = {};
for (let p = 0; p < functions[name].params.length; p++)
newScope[functions[name].params[p]] = params[p];
return inScope(newScope, () => functions[name].func(ctx));
},
assert(a, b, errStr = '') {
a = BigInt(a);
b = BigInt(b);
if (a === b)
return;
throw new Error(`Constraint doesn't match ${currentComponent}: ${errStr} -> ${a} != ${b}`);
},
};
patcher.patch();
try {
// Processing
for (const c in components)
notInitSignals[c] = components[c].inputSignals;
ctx.setSignal('one', [], BigInt(1));
for (let c in notInitSignals)
if (notInitSignals[c] == 0)
triggerComponent(c);
// Circuit JSON inputs are own fields; prototypes may carry unrelated app metadata.
for (const s of Object.keys(input)) {
currentComponent = 'main';
const stack = [{ selectors: [], values: input[s] }];
while (stack.length) {
const { selectors, values } = stack.pop();
if (!Array.isArray(values)) {
if (values === undefined)
throw new Error('Signal not defined:' + s);
ctx.setSignal(s, selectors, BigInt(values));
continue;
}
for (let j = values.length - 1; j >= 0; j--) {
stack.push({ selectors: [...selectors, `${j}`], values: values[j] });
}
}
}
for (let i = 0; i < circJson.nInputs; i++) {
const idx = inputIdx(i);
if (witness[idx] === undefined)
throw new Error('Input Signal not assigned: ' + signalNames(idx));
}
for (let i = 0; i < witness.length; i++)
if (witness[i] === undefined)
throw new Error('Signal not assigned: ' + signalNames(i));
return witness.slice(0, circJson.nVars);
}
finally {
patcher.restore();
}
};
}
/**
* Binary coders and parsers for Circom2 artifacts.
* @param curve - Curve pair used for field sizing and point decoding.
* @returns R1CS, witness, and zkey coders plus parse helpers.
* @example
* Build the coders once, then use them to parse and encode Circom2 artifacts.
* ```ts
* const { bn254 } = await import('@noble/curves/bn254.js');
* const coders = getCoders(bn254);
* const bytes = coders.binWitness.encode([1n, 2n]);
* coders.binWitness.decode(bytes);
* ```
*/
export const getCoders = (curve) => {
const field = curve.fields.Fr;
// NOTE: we need to pass field here, even if bigints are variable size, they are fixed to field bytes!
const fieldBytes = field.BYTES;
const fieldCoder = P.bigint(fieldBytes, true, false);
const Header = P.struct({
prime: P.prefix(P.U32LE, fieldCoder), // TODO: verify that exactly same as field.ORDER?
nWires: P.U32LE, // Total Number of wires including ONE signal (Index 0).
nPubOut: P.U32LE, // Total Number of wires public output wires. They should be starting at idx 1
nPubIn: P.U32LE, // Total Number of wires public input wires. They should be starting just after the public output
nPrvIn: P.U32LE, // Total Number of wires private input wires. They should be starting just after the public inputs
nLables: P.U64LE, // Total Number of wires private input wires. They should be starting just after the public inputs
mConstraints: P.U32LE, // Total Number of constraints
});
const constraintDict = {
encode: (from) => {
if (!Array.isArray(from))
throw new Error('array expected');
const to = {};
for (const item of from) {
if (!Array.isArray(item) || item.length !== 2)
throw new Error(`array of two elements expected`);
const [key, value] = item;
if (Object.prototype.hasOwnProperty.call(to, key))
throw new Error(`key(${key}) appears twice in constraint`);
to[key] = value;
}
return to;
},
decode: (to) => {
if (to === null || typeof to !== 'object' || Array.isArray(to))
throw new Error(`expected constraint object, got ${to}`);
return Object.entries(to).map(([key, value]) => {
// Object.entries() stringifies numeric R1CS signal ids; U32LE needs the number back.
if (!/^(0|[1-9][0-9]*)$/.test(key))
throw new Error(`expected uint32 constraint key, got ${key}`);
const n = Number(key);
if (!Number.isSafeInteger(n) || n < 0 || n > 0xffffffff)
throw new Error(`expected uint32 constraint key, got ${key}`);
return [n, value];
});
},
};
const Constraint = P.apply(P.array(P.U32LE, P.tuple([P.U32LE, fieldCoder])), constraintDict);
// A*B-C = 0
const Constraints = P.array(null, P.tuple([Constraint, Constraint, Constraint]));
const WireMap = P.array(null, P.U64LE);
// prefix() emits JS byte lengths, while Circom section headers serialize them as u64.
const sectionLen = P.apply(P.U64LE, P.coders.numberBigint);
const section = (inner) => P.prefix(sectionLen, inner);
const empty = P.bytes(null);
const R1CSSection = P.mappedTag(P.U32LE, {
header: [0x01, section(Header)],
constraint: [0x02, section(Constraints)],
wire2label: [0x03, section(WireMap)],
// not implemented: ultra-plonk
customGatesList: [0x04, section(empty)],
customGatesApplication: [0x05, section(empty)],
});
const R1CS = P.struct({
magic: P.magic(P.string(4), 'r1cs'),
version: P.U32LE,
sections: P.array(P.U32LE, R1CSSection),
});
const binWitness = P.array(null, fieldCoder);
const WTNSHeader = P.struct({
prime: P.prefix(P.U32LE, fieldCoder),
size: P.U32LE,
});
const WTNSSection = P.mappedTag(P.U32LE, {
header: [0x01, section(WTNSHeader)],
witness: [0x02, section(P.array(null, fieldCoder))],
});
const WTNS = P.struct({
magic: P.magic(P.string(4), 'wtns'),
version: P.U32LE,
sections: P.array(P.U32LE, WTNSSection),
});
const G1 = P.tuple([fieldCoder, fieldCoder]);
const G2 = P.tuple([fieldCoder, fieldCoder, fieldCoder, fieldCoder]);
const ZKeyHeader = P.map(P.U32LE, {
groth16: 1,
});
const ZKeyHeaderGroth = P.struct({
n8q: P.U32LE,
q: fieldCoder,
n8r: P.U32LE,
r: fieldCoder,
nVars: P.U32LE,
nPublic: P.U32LE,
domainSize: P.U32LE,
vk_alpha_1: G1,
vk_beta_1: G1,
vk_beta_2: G2,
vk_gamma_2: G2,
vk_delta_1: G1,
vk_delta_2: G2,
});
const ZKeyCoeff = P.struct({
matrix: P.U32LE,
constraint: P.U32LE,
signal: P.U32LE,
value: fieldCoder,
});
const ZKeySection = P.mappedTag(P.U32LE, {
header: [1, section(ZKeyHeader)],
headerGroth: [2, section(ZKeyHeaderGroth)],
IC: [3, section(P.array(null, G1))],
ccoefs: [4, section(P.array(P.U32LE, ZKeyCoeff))],
A: [5, section(P.array(null, G1))],
B1: [6, section(P.array(null, G1))],
B2: [7, section(P.array(null, G2))],
C: [8, section(P.array(null, G1))],
hExps: [9, section(P.array(null, G1))],
Contributions: [10, section(P.bytes(null))],
});
const ZKeyRaw = P.struct({
magic: P.magic(P.string(4), 'zkey'),
version: P.U32LE,
sections: P.array(P.U32LE, ZKeySection),
});
const getCircuitInfo = (bytes) => {
const data = R1CS.decode(bytes);
const constraints = data.sections.find((i) => i.TAG === 'constraint');
if (!constraints)
throw new Error('R1CS: cannot find constraints');
const header = data.sections.find((i) => i.TAG === 'header');
if (!header)
throw new Error('R1CS: cannot find header');
if (header.data.prime !== field.ORDER)
throw new Error('R1CS: wrong field order');
return {
nVars: header.data.nWires,
nPubInputs: header.data.nPubIn,
nOutputs: header.data.nPubOut,
constraints: constraints.data,
};
};
function parseZKey(zkey) {
const { Fr, Fp } = curve.fields;
// Montgomery encoding of field elements
const fieldFromMont = (f, is1) => {
const Rr = f.pow(BigInt(2), BigInt(f.BYTES * 8));
const RRi = f.inv(Rr);
const RRi2 = f.mul(RRi, RRi);
// G1/G2 coordinates carry one Montgomery factor; coefficient field elements need two.
return (x) => f.mul(x, is1 ? RRi : RRi2);
};
const is0 = (x) => x === BigInt(0);
const convFr2 = fieldFromMont(Fr, false);
const convFp = fieldFromMont(Fp, true);
const convG1 = ([x, y]) => is0(x) && is0(y) ? [BigInt(0), BigInt(1), BigInt(0)] : [convFp(x), convFp(y), BigInt(1)];
// [ [ 0n, 0n ], [ 0n, 0n ], [ 1n, 0n ] ], -> [ [ 0n, 0n ], [ 1n, 0n ], [ 0n, 0n ] ],
const convG2 = ([xc0, xc1, yc0, yc1]) => is0(xc0) && is0(xc1) && is0(yc0) && is0(yc1)
? [
[BigInt(0), BigInt(0)],
[BigInt(1), BigInt(0)],
[BigInt(0), BigInt(0)],
]
: [
[convFp(xc0), convFp(xc1)],
[convFp(yc0), convFp(yc1)],
[BigInt(1), BigInt(0)],
];
const data = ZKeyRaw.decode(zkey);
function getByTag(sections, tag) {
const v = sections.find((i) => i.TAG === tag);
if (!v)
throw new Error('ZKey: cannot find ' + String(tag));
return v.data;
}
function collect(sections, ks) {
const out = {};
for (const k of ks)
out[k] = getByTag(sections, k);
return out;
}
const res = collect(data.sections, [
'header',
'headerGroth',
'IC',
'ccoefs',
'A',
'B1',
'B2',
'C',
'hExps',
]);
// Same format as verification key
const json = {
protocol: res.header,
...res.headerGroth,
vk_alpha_1: convG1(res.headerGroth.vk_alpha_1),
vk_beta_1: convG1(res.headerGroth.vk_beta_1),
vk_delta_1: convG1(res.headerGroth.vk_delta_1),
vk_beta_2: convG2(res.headerGroth.vk_beta_2),
vk_delta_2: convG2(res.headerGroth.vk_delta_2),
vk_gamma_2: convG2(res.headerGroth.vk_gamma_2),
power: Math.log2(res.headerGroth.domainSize),
IC: res.IC.map(convG1),
ccoefs: res.ccoefs.map((i) => ({ ...i, value: convFr2(i.value) })),
A: res.A.map(convG1),
B1: res.B1.map(convG1),
B2: res.B2.map(convG2),
// snarkjs zkeys omit the leading zero C-query entries for public signals.
C: new Array(res.headerGroth.nPublic + 1).fill(null).concat(res.C.map(convG1)),
hExps: res.hExps.map(convG1),
};
// Our format (old snarkjs compat)
const pkey = {
protocol: 'groth',
nVars: json.nVars,
nPublic: json.nPublic,
domainSize: json.domainSize,
domainBits: json.power,
// Polynominals (instead polsA/polsB/polsC)
ccoefs: json.ccoefs, // changed
//
A: json.A,
B1: json.B1,
B2: json.B2,
C: json.C,
//
vk_alfa_1: json.vk_alpha_1,
vk_beta_1: json.vk_beta_1,
vk_delta_1: json.vk_delta_1,
vk_beta_2: json.vk_beta_2,
vk_delta_2: json.vk_delta_2,
//
hExps: json.hExps,
};
const vkey = {
protocol: 'groth',
nPublic: json.nPublic,
IC: json.IC,
vk_alfa_1: json.vk_alpha_1,
vk_beta_2: json.vk_beta_2,
vk_gamma_2: json.vk_gamma_2,
vk_delta_2: json.vk_delta_2,
};
return { json, pkey, vkey };
}
return { R1CS, binWitness, WTNS, getCircuitInfo, ZKeyRaw, parseZKey };
};
//# sourceMappingURL=witness.js.map