Skip to content

Commit

Permalink
Downloading anon circuits using webworkers (#121)
Browse files Browse the repository at this point in the history
closes #123
  • Loading branch information
nigeon authored Dec 14, 2023
1 parent 92d1adb commit 46dd6f2
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 5 deletions.
45 changes: 41 additions & 4 deletions packages/react-providers/src/election/use-election-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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<ICircuit, ICircuitWorkerRequest>(workerInstance)

const fetchAnonCircuits = useCallback(async () => {
const hasOverwriteEnabled =
typeof election !== 'undefined' &&
typeof election.voteType.maxVoteOverwrites !== 'undefined' &&
Expand All @@ -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(() => {
Expand Down
95 changes: 95 additions & 0 deletions packages/react-providers/src/worker/circuitWorkerScript.ts
Original file line number Diff line number Diff line change
@@ -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<ICircuit> {}

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<ICircuitWorkerRequest>) => {
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)
}
})
}
37 changes: 37 additions & 0 deletions packages/react-providers/src/worker/useWebWorker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useCallback, useEffect, useState } from 'react'

export interface IBaseWorkerResponse<T> {
result: T
error?: any
}

export const useWebWorker = <TResult, TWorkerPayload>(worker: Worker) => {
const [running, setRunning] = useState(false)
const [error, setError] = useState<any>()
const [result, setResult] = useState<TResult>()

const startProcessing = useCallback(
(data: TWorkerPayload) => {
setRunning(true)
worker.postMessage(data)
},
[worker]
)

useEffect(() => {
const onMessage = (event: MessageEvent<IBaseWorkerResponse<TResult>>) => {
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,
}
}
7 changes: 7 additions & 0 deletions packages/react-providers/src/worker/webWorker.ts
Original file line number Diff line number Diff line change
@@ -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)
}
10 changes: 9 additions & 1 deletion setup-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 46dd6f2

Please sign in to comment.