From 46dd6f2ec2844579c8a5432635216d6c961af808 Mon Sep 17 00:00:00 2001 From: Aleix Date: Thu, 14 Dec 2023 17:48:21 +0100 Subject: [PATCH] Downloading anon circuits using webworkers (#121) closes #123 --- .../src/election/use-election-provider.ts | 45 ++++++++- .../src/worker/circuitWorkerScript.ts | 95 +++++++++++++++++++ .../src/worker/useWebWorker.ts | 37 ++++++++ .../react-providers/src/worker/webWorker.ts | 7 ++ setup-tests.ts | 10 +- 5 files changed, 189 insertions(+), 5 deletions(-) create mode 100644 packages/react-providers/src/worker/circuitWorkerScript.ts create mode 100644 packages/react-providers/src/worker/useWebWorker.ts create mode 100644 packages/react-providers/src/worker/webWorker.ts diff --git a/packages/react-providers/src/election/use-election-provider.ts b/packages/react-providers/src/election/use-election-provider.ts index 5122b89c..01095a10 100644 --- a/packages/react-providers/src/election/use-election-provider.ts +++ b/packages/react-providers/src/election/use-election-provider.ts @@ -7,9 +7,13 @@ import { PublishedElection, Vote, } from '@vocdoni/sdk' -import { ComponentType, useCallback, useEffect } from 'react' +import { ComponentType, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useClient } from '../client' import { useElectionReducer } from './use-election-reducer' +import { ChainAPI } from '@vocdoni/sdk' +import { createWebWorker } from '../worker/webWorker' +import { useWebWorker } from '../worker/useWebWorker' +import worker, { ICircuit, ICircuitWorkerRequest } from '../worker/circuitWorkerScript' export type ElectionProviderProps = { id?: string @@ -40,6 +44,9 @@ export const useElectionProvider = ({ loaded, sik: { password, signature }, } = state + const [anonCircuitsFetched, setAnonCircuitsFetched] = useState(false) + + const isAnonCircuitsFetching = useRef(false) const fetchElection = useCallback( async (id: string) => { @@ -87,7 +94,10 @@ export const useElectionProvider = ({ } }, [actions, client, election, password, signature]) - const fetchAnonCircuits = useCallback(() => { + const workerInstance = useMemo(() => createWebWorker(worker), []) + const { result: circuits, startProcessing } = useWebWorker(workerInstance) + + const fetchAnonCircuits = useCallback(async () => { const hasOverwriteEnabled = typeof election !== 'undefined' && typeof election.voteType.maxVoteOverwrites !== 'undefined' && @@ -96,16 +106,43 @@ export const useElectionProvider = ({ const votable = state.isAbleToVote || (hasOverwriteEnabled && state.isInCensus && state.voted) if (votable && election?.census.type === CensusType.ANONYMOUS) { - client.anonymousService.fetchCircuits() + const chainCircuits = await ChainAPI.circuits(client.url) + const circuits = { + zKeyURI: chainCircuits.uri + '/' + chainCircuits.circuitPath + '/' + chainCircuits.zKeyFilename, + zKeyHash: chainCircuits.zKeyHash, + vKeyURI: chainCircuits.uri + '/' + chainCircuits.circuitPath + '/' + chainCircuits.vKeyFilename, + vKeyHash: chainCircuits.vKeyHash, + wasmURI: chainCircuits.uri + '/' + chainCircuits.circuitPath + '/' + chainCircuits.wasmFilename, + wasmHash: chainCircuits.wasmHash, + } + startProcessing({ circuits }) } }, [election, state.isAbleToVote, state.isInCensus, state.voted, client.anonymousService]) // pre-fetches circuits needed for voting in anonymous elections useEffect(() => { - if (!fetchCensus || !election || loading.census || !client.wallet) return + if ( + !fetchCensus || + !election || + loading.census || + !client.wallet || + anonCircuitsFetched || + isAnonCircuitsFetching.current + ) + return + isAnonCircuitsFetching.current = true fetchAnonCircuits() }, [fetchAnonCircuits, client.wallet, election, loading.census, fetchCensus]) + // sets circuits in the anonymous service + useEffect(() => { + ;(async () => { + if (!circuits) return + setAnonCircuitsFetched(true) + client.anonymousService.setCircuits(circuits) + })() + }, [circuits]) + // CSP OAuth flow // As vote setting and voting token are async, we need to wait for both to be set useEffect(() => { diff --git a/packages/react-providers/src/worker/circuitWorkerScript.ts b/packages/react-providers/src/worker/circuitWorkerScript.ts new file mode 100644 index 00000000..d0a40e38 --- /dev/null +++ b/packages/react-providers/src/worker/circuitWorkerScript.ts @@ -0,0 +1,95 @@ +/* eslint-disable no-restricted-globals */ +/* eslint-disable import/no-anonymous-default-export */ + +import { IBaseWorkerResponse } from './useWebWorker' + +export interface ICircuit { + zKeyData: Uint8Array + zKeyHash: string + zKeyURI: string + vKeyData: Uint8Array + vKeyHash: string + vKeyURI: string + wasmData: Uint8Array + wasmHash: string + wasmURI: string +} + +export interface ICircuitWorkerRequest { + circuits: { + zKeyURI: string + zKeyHash: string + vKeyURI: string + vKeyHash: string + wasmURI: string + wasmHash: string + } +} + +export interface IWorkerResponse extends IBaseWorkerResponse {} + +export const fetchDataInChunks = async (url: string) => { + return fetch(url) + .then((res) => res.arrayBuffer()) + .then((res) => new Uint8Array(res)) +} + +export default () => { + self.addEventListener('message', async (e: MessageEvent) => { + try { + const { circuits } = e.data + + async function fetchDataInChunks(uri: string) { + const response = await fetch(uri) + const reader = response.body?.getReader() as ReadableStreamDefaultReader + const contentLength = +(response.headers?.get('Content-Length') ?? 0) + + const chunks = [] + let receivedLength = 0 + + while (true) { + const { done, value } = await reader.read() + + if (done) break + + chunks.push(value) + receivedLength += value.length + + // Check for content length, break if all content received + if (contentLength && receivedLength >= contentLength) break + } + + const concatenatedArray = new Uint8Array(receivedLength) + let offset = 0 + + for (const chunk of chunks) { + concatenatedArray.set(chunk, offset) + offset += chunk.length + } + + return concatenatedArray + } + + const [zKeyData, vKeyData, wasmData] = await Promise.all([ + fetchDataInChunks(circuits.zKeyURI), + fetchDataInChunks(circuits.vKeyURI), + fetchDataInChunks(circuits.wasmURI), + ]) + + const circuitsData: ICircuit = { + zKeyData, + zKeyURI: circuits.zKeyURI, + zKeyHash: circuits.zKeyHash, + vKeyData, + vKeyURI: circuits.vKeyURI, + vKeyHash: circuits.vKeyHash, + wasmData, + wasmURI: circuits.wasmURI, + wasmHash: circuits.wasmHash, + } + return postMessage({ result: circuitsData } as IWorkerResponse) + } catch (error) { + return postMessage({ error } as IWorkerResponse) + } + }) +} diff --git a/packages/react-providers/src/worker/useWebWorker.ts b/packages/react-providers/src/worker/useWebWorker.ts new file mode 100644 index 00000000..eb34d364 --- /dev/null +++ b/packages/react-providers/src/worker/useWebWorker.ts @@ -0,0 +1,37 @@ +import { useCallback, useEffect, useState } from 'react' + +export interface IBaseWorkerResponse { + result: T + error?: any +} + +export const useWebWorker = (worker: Worker) => { + const [running, setRunning] = useState(false) + const [error, setError] = useState() + const [result, setResult] = useState() + + const startProcessing = useCallback( + (data: TWorkerPayload) => { + setRunning(true) + worker.postMessage(data) + }, + [worker] + ) + + useEffect(() => { + const onMessage = (event: MessageEvent>) => { + setRunning(false) + setError(event.data.error) + setResult(event.data.result) + } + worker.addEventListener('message', onMessage) + return () => worker.removeEventListener('message', onMessage) + }, [worker]) + + return { + startProcessing, + running, + error, + result, + } +} diff --git a/packages/react-providers/src/worker/webWorker.ts b/packages/react-providers/src/worker/webWorker.ts new file mode 100644 index 00000000..09d67bb7 --- /dev/null +++ b/packages/react-providers/src/worker/webWorker.ts @@ -0,0 +1,7 @@ +export const createWebWorker = (worker: any) => { + const code = worker.toString() + const blob = new Blob(['(' + code + ')()']) + const workerURL = (window as any).MockedWindowURL || window.URL || window.webkitURL + const blobURL = workerURL.createObjectURL(blob) + return new Worker(blobURL) +} diff --git a/setup-tests.ts b/setup-tests.ts index 2b801822..b7959183 100644 --- a/setup-tests.ts +++ b/setup-tests.ts @@ -25,10 +25,18 @@ class Worker { postMessage(msg) { this.onmessage(msg) } + addEventListener() {} + removeEventListener() {} } // required due to SDK dependency -Object.defineProperty(window, 'Worker', Worker) +Object.defineProperty(window, 'Worker', { value: Worker }) +Object.defineProperty(window, 'MockedWindowURL', { + value: { + createObjectURL: () => 'blob:mocked', + revokeObjectURL: () => {}, + }, +}) // required by any react component (almost all of them) global.React = React