@reclaimprotocol/zk-symmetric-crypto
Version:
JS Wrappers for Various ZK Snark Circuits
149 lines (148 loc) • 5.67 kB
JavaScript
import { Base64 } from 'js-base64';
const BIN_PATH = '../../bin/gnark';
let globalGnarkLib;
// golang uses different arch names
// for some archs -- so this map corrects the name
const ARCH_MAP = {
'x64': 'x86_64',
};
const INIT_ALGS = {};
async function loadGnarkLib() {
const koffiMod = await import('koffi')
.catch(() => undefined);
if (!koffiMod) {
throw new Error('Koffi not available, cannot use gnark');
}
const { join, dirname } = await import('path');
const __dirname = dirname(import.meta.url.replace('file://', ''));
const { default: koffi } = koffiMod;
koffi.reset(); //otherwise tests will fail
// define object GoSlice to map to:
// C type struct { void *data; GoInt len; GoInt cap; }
const GoSlice = koffi.struct('GoSlice', {
data: 'void *',
len: 'longlong',
cap: 'longlong'
});
const ProveReturn = koffi.struct('ProveReturn', {
r0: 'void *',
r1: 'longlong',
});
const LibReturn = koffi.struct('LibReturn', {
r0: 'void *',
r1: 'longlong',
});
const arch = ARCH_MAP[process.arch] || process.arch;
const platform = process.platform;
const libVerifyPath = join(__dirname, `${BIN_PATH}/${platform}-${arch}-libverify.so`);
const libProvePath = join(__dirname, `${BIN_PATH}/${platform}-${arch}-libprove.so`);
try {
const libVerify = koffi.load(libVerifyPath);
const libProve = koffi.load(libProvePath);
return {
verify: libVerify.func('Verify', 'unsigned char', [GoSlice]),
free: libProve.func('Free', 'void', ['void *']),
vfree: libVerify.func('VFree', 'void', ['void *']), //free in verify library
prove: libProve.func('Prove', ProveReturn, [GoSlice]),
initAlgorithm: libProve.func('InitAlgorithm', 'unsigned char', ['unsigned char', GoSlice, GoSlice]),
generateThresholdKeys: libVerify.func('GenerateThresholdKeys', LibReturn, [GoSlice]),
oprfEvaluate: libVerify.func('OPRFEvaluate', LibReturn, [GoSlice]),
generateOPRFRequest: libProve.func('GenerateOPRFRequestData', LibReturn, [GoSlice]),
toprfFinalize: libProve.func('TOPRFFinalize', LibReturn, [GoSlice]),
koffi
};
}
catch (err) {
const message = err instanceof Error ? err.message : String(err ?? '');
if (message.includes('not a mach-o')) {
throw new Error(`Gnark library not compatible with OS/arch (${platform}/${arch})`);
}
else if (message.toLowerCase().includes('no such file')) {
throw new Error(`Gnark library not built for OS/arch (${platform}/${arch})`);
}
throw err;
}
}
export async function initGnarkAlgorithm(id, fileExt, fetcher, logger) {
globalGnarkLib ??= loadGnarkLib();
const lib = await globalGnarkLib;
if (INIT_ALGS[id]) {
return lib;
}
const [pk, r1cs] = await Promise.all([
fetcher.fetch('gnark', `pk.${fileExt}`, logger),
fetcher.fetch('gnark', `r1cs.${fileExt}`, logger),
]);
const f1 = { data: pk, len: pk.length, cap: pk.length };
const f2 = { data: r1cs, len: r1cs.length, cap: r1cs.length };
await lib.initAlgorithm(id, f1, f2);
INIT_ALGS[id] = true;
return lib;
}
export function strToUint8Array(str) {
return new TextEncoder().encode(str);
}
export function serialiseGnarkWitness(cipher, input) {
const json = generateGnarkWitness(cipher, input);
return strToUint8Array(JSON.stringify(json));
}
export function generateGnarkWitness(cipher, input) {
// input is bits, we convert them back to bytes
return {
cipher: cipher + ('toprf' in input ? '-toprf' : ''),
key: 'key' in input
? Base64.fromUint8Array(input.key)
: undefined,
ciphertext: 'out' in input && input.out?.length
? Base64.fromUint8Array(input.out)
: undefined,
blocks: input.noncesAndCounters.map(n => ({
nonce: Base64.fromUint8Array(n.nonce),
counter: n.counter,
boundary: n.boundary || null
})),
input: Base64.fromUint8Array(input.in),
toprf: generateTOPRFParams()
};
function generateTOPRFParams() {
if (!('toprf' in input)) {
return {};
}
const { locations, domainSeparator, output, responses } = input.toprf;
return {
locations,
domainSeparator: Base64
.fromUint8Array(strToUint8Array(domainSeparator)),
output: Base64.fromUint8Array(output),
responses: responses.map(mapResponse),
mask: 'mask' in input
? Base64.fromUint8Array(input.mask)
: ''
};
}
}
function mapResponse({ publicKeyShare, evaluated, c, r }) {
return {
publicKeyShare: Base64.fromUint8Array(publicKeyShare),
evaluated: Base64.fromUint8Array(evaluated),
c: Base64.fromUint8Array(c),
r: Base64.fromUint8Array(r),
};
}
export function executeGnarkFn(fn, jsonInput) {
const wtns = {
data: typeof jsonInput === 'string'
? Buffer.from(jsonInput)
: jsonInput,
len: jsonInput.length,
cap: jsonInput.length
};
return fn(wtns);
}
export async function executeGnarkFnAndGetJson(fn, jsonInput) {
const { free, koffi } = await globalGnarkLib;
const res = executeGnarkFn(fn, jsonInput);
const proof = Buffer.from(koffi.decode(res.r0, 'unsigned char', res.r1)).toString();
free(res.r0); // Avoid memory leak!
return JSON.parse(proof);
}