UNPKG

o1js

Version:

TypeScript framework for zk-SNARKs and zkApps

188 lines (162 loc) 5.86 kB
import plonkWasm from '../../../web_bindings/plonk_wasm.js'; import { workerSpec } from './worker-spec.js'; import { srcFromFunctionModule, inlineWorker, waitForMessage } from './worker-helpers.js'; import o1jsWebSrc from 'string:../../../web_bindings/o1js_web.bc.js'; import { WithThreadPool, workers } from '../../../lib/proof-system/workers.js'; export { initializeBindings, withThreadPool, wasm }; let wasm; /** * @type {Promise<Worker>} */ let workerPromise; /** * @type {number | undefined} */ let numWorkers = undefined; async function initializeBindings() { wasm = plonkWasm(); globalThis.plonk_wasm = wasm; let init = wasm.default; const memory = allocateWasmMemoryForUserAgent(navigator.userAgent); await init(undefined, memory); let module = init.__wbindgen_wasm_module; // we have two approaches to run the .bc.js code after its dependencies are ready, without fetching an additional script: // 1. wrap it inside a function and just include that function in the bundle // this could be nice and simple, but breaks because the .bc.js code uses `(function(){return this}())` to access `window` // (probably as a cross-platform way to get the global object before globalThis existed) // that obsolete hack doesn't work here because inside an ES module, this === undefined instead of this === window // it seems to work when we patch the source code (replace IIFEs with `window`) // 2. include the code as string and eval it: // (this works because it breaks out of strict mode) new Function(o1jsWebSrc)(); workerPromise = new Promise((resolve) => { setTimeout(async () => { let worker = inlineWorker(srcFromFunctionModule(mainWorker)); await workerCall(worker, 'start', { memory, module }); overrideBindings(globalThis.plonk_wasm, worker); resolve(worker); }, 0); }); } async function initThreadPool() { if (workerPromise === undefined) throw Error('need to initialize worker first'); let worker = await workerPromise; numWorkers ??= Math.max(1, workers.numWorkers ?? (navigator.hardwareConcurrency ?? 1) - 1); await workerCall(worker, 'initThreadPool', numWorkers); } async function exitThreadPool() { if (workerPromise === undefined) throw Error('need to initialize worker first'); let worker = await workerPromise; await workerCall(worker, 'exitThreadPool'); } const withThreadPool = WithThreadPool({ initThreadPool, exitThreadPool }); async function mainWorker() { const wasm = plonkWasm(); let init = wasm.default; let spec = workerSpec(wasm); let isInitialized = false; let data = await waitForMessage(self, 'start'); let { module, memory } = data.message; onMessage(self, 'run', ({ name, args, u32_ptr }) => { let functionSpec = spec[name]; let specArgs = functionSpec.args; let resArgs = args; for (let i = 0, l = specArgs.length; i < l; i++) { let specArg = specArgs[i]; if (specArg && specArg.__wrap) { // Class info got lost on transfer, rebuild it. resArgs[i] = specArg.__wrap(args[i].__wbg_ptr); } else { resArgs[i] = args[i]; } } let res = wasm[name].apply(wasm, resArgs); if (functionSpec.res && functionSpec.res.__wrap) { res = res.__wbg_ptr; } else if (functionSpec.res && functionSpec.res.there) { res = functionSpec.res.there(res); } /* Here be undefined behavior dragons. */ wasm.set_u32_ptr(u32_ptr, res); /*postMessage(res);*/ }); workerExport(self, { async initThreadPool(numWorkers) { if (!isInitialized) { isInitialized = true; await wasm.initThreadPool(numWorkers); } }, async exitThreadPool() { if (isInitialized) { isInitialized = false; await wasm.exitThreadPool(); } }, }); await init(module, memory); postMessage({ type: data.id }); } mainWorker.deps = [plonkWasm, workerSpec, workerExport, onMessage, waitForMessage]; function overrideBindings(plonk_wasm, worker) { let spec = workerSpec(plonk_wasm); for (let key in spec) { plonk_wasm[key] = (...args) => { if (spec[key].disabled) throw Error(`Wasm method '${key}' is disabled on the web.`); let u32_ptr = wasm.create_zero_u32_ptr(); worker.postMessage({ type: 'run', message: { name: key, args, u32_ptr }, }); /* Here be undefined behavior dragons. */ let res = wasm.wait_until_non_zero(u32_ptr); wasm.free_u32_ptr(u32_ptr); let res_spec = spec[key].res; if (res_spec && res_spec.__wrap) { return spec[key].res.__wrap(res); } else if (res_spec && res_spec.back) { return res_spec.back(res); } else { return res; } }; } } // helpers for main thread <-> worker communication function onMessage(worker, type, onMsg) { worker.addEventListener('message', function ({ data }) { if (data?.type !== type) return; onMsg(data.message); }); } function workerExport(worker, exportObject) { for (let key in exportObject) { worker.addEventListener('message', async function ({ data }) { if (data?.type !== key) return; let result = await exportObject[key](data.message); postMessage({ type: data.id, result }); }); } } async function workerCall(worker, type, message) { let id = Math.random(); let promise = waitForMessage(worker, id); worker.postMessage({ type, id, message }); return (await promise).result; } function allocateWasmMemoryForUserAgent(userAgent) { const isIOSDevice = /iPad|iPhone|iPod/.test(userAgent); if (isIOSDevice) { return new WebAssembly.Memory({ initial: 19, maximum: 16384, // 1 GiB shared: true, }); } else { return new WebAssembly.Memory({ initial: 19, maximum: 65536, // 4 GiB shared: true, }); } }