From afd13196c226980a9ac6906c5f4b05c0fbc72f6b Mon Sep 17 00:00:00 2001 From: turbocrime Date: Wed, 4 Sep 2024 22:05:35 -0700 Subject: [PATCH 1/5] services use DOM lib, make keys pathable, remove 'internal message' from types --- packages/keys/action-keys.json | 8 - packages/keys/package.json | 19 ++- packages/keys/src/index.ts | 10 ++ packages/keys/tsconfig.json | 13 ++ packages/services/package.json | 5 +- packages/services/src/offscreen-client.ts | 102 ------------ .../view-service/util/build-action-worker.ts | 91 +++++++++++ .../src/view-service/util/build-tx.ts | 68 +++----- .../src/view-service/util/parallel.ts | 89 +++++++++++ packages/services/tsconfig.json | 3 +- .../types/src/internal-msg/chrome-error.ts | 7 - packages/types/src/internal-msg/offscreen.ts | 49 ------ packages/types/src/internal-msg/shared.ts | 22 --- pnpm-lock.yaml | 149 ++---------------- 14 files changed, 261 insertions(+), 374 deletions(-) delete mode 100644 packages/keys/action-keys.json create mode 100644 packages/keys/src/index.ts create mode 100644 packages/keys/tsconfig.json delete mode 100644 packages/services/src/offscreen-client.ts create mode 100644 packages/services/src/view-service/util/build-action-worker.ts create mode 100644 packages/services/src/view-service/util/parallel.ts delete mode 100644 packages/types/src/internal-msg/chrome-error.ts delete mode 100644 packages/types/src/internal-msg/offscreen.ts delete mode 100644 packages/types/src/internal-msg/shared.ts diff --git a/packages/keys/action-keys.json b/packages/keys/action-keys.json deleted file mode 100644 index c3ceb8d9a5..0000000000 --- a/packages/keys/action-keys.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "delegatorVote": "delegator_vote_pk.bin", - "output": "output_pk.bin", - "spend": "spend_pk.bin", - "swap": "swap_pk.bin", - "swapClaim": "swapclaim_pk.bin", - "undelegateClaim": "convert_pk.bin" -} diff --git a/packages/keys/package.json b/packages/keys/package.json index d2a33b3b02..1352ee1d2e 100644 --- a/packages/keys/package.json +++ b/packages/keys/package.json @@ -5,19 +5,30 @@ "description": "Tool to download proving keys for Penumbra", "type": "module", "scripts": { - "clean": "rm -rf penumbra-zone-*.tgz", - "dev:pack": "$npm_execpath pack", + "build": "tsc --build --verbose", + "clean": "rm -rfv dist *.tsbuildinfo package penumbra-zone-*.tgz", + "clean:keys": "rm -rfv keys", + "dev:pack": "tsc-watch --onSuccess \"$npm_execpath pack\"", "prepare": "./download-keys ./keys" }, "files": [ - "action-keys.json", + "dist", "download-keys", "keys/*_pk.bin" ], "exports": { - ".": "./action-keys.json", + ".": "./src/index.ts", "./*_pk.bin": "./keys/*_pk.bin" }, + "publishConfig": { + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./*_pk.bin": "./keys/*_pk.bin" + } + }, "bin": { "penumbra-download-keys": "./download-keys" } diff --git a/packages/keys/src/index.ts b/packages/keys/src/index.ts new file mode 100644 index 0000000000..9a9024b735 --- /dev/null +++ b/packages/keys/src/index.ts @@ -0,0 +1,10 @@ +const keyPaths = { + delegatorVote: new URL('../keys/delegator_vote_pk.bin', import.meta.url), + output: new URL('../keys/output_pk.bin', import.meta.url), + spend: new URL('../keys/spend_pk.bin', import.meta.url), + swap: new URL('../keys/swap_pk.bin', import.meta.url), + swapClaim: new URL('../keys/swapclaim_pk.bin', import.meta.url), + undelegateClaim: new URL('../keys/convert_pk.bin', import.meta.url), +} as const; + +export default keyPaths; diff --git a/packages/keys/tsconfig.json b/packages/keys/tsconfig.json new file mode 100644 index 0000000000..d7621332ac --- /dev/null +++ b/packages/keys/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "exactOptionalPropertyTypes": false, + "composite": true, + "module": "Node16", + "outDir": "dist", + "preserveWatchOutput": true, + "rootDir": "src", + "target": "ESNext" + }, + "extends": "@tsconfig/strictest/tsconfig.json", + "include": ["src"] +} diff --git a/packages/services/package.json b/packages/services/package.json index 0a6160f70b..ddea6fb3e4 100644 --- a/packages/services/package.json +++ b/packages/services/package.json @@ -40,13 +40,15 @@ "@penumbra-zone/bech32m": "workspace:*", "@penumbra-zone/crypto-web": "workspace:*", "@penumbra-zone/getters": "workspace:*", + "@penumbra-zone/keys": "workspace:*", "@penumbra-zone/protobuf": "workspace:*", "@penumbra-zone/query": "workspace:*", "@penumbra-zone/storage": "workspace:*", "@penumbra-zone/transport-dom": "workspace:*", "@penumbra-zone/types": "workspace:*", "@penumbra-zone/wasm": "workspace:*", - "@types/chrome": "^0.0.268" + "@types/chrome": "^0.0.268", + "import-meta-resolve": "^4.1.0" }, "peerDependencies": { "@bufbuild/protobuf": "^1.10.0", @@ -54,6 +56,7 @@ "@penumbra-zone/bech32m": "workspace:*", "@penumbra-zone/crypto-web": "workspace:*", "@penumbra-zone/getters": "workspace:*", + "@penumbra-zone/keys": "workspace:*", "@penumbra-zone/protobuf": "workspace:*", "@penumbra-zone/query": "workspace:*", "@penumbra-zone/storage": "workspace:*", diff --git a/packages/services/src/offscreen-client.ts b/packages/services/src/offscreen-client.ts deleted file mode 100644 index 2ae62986e6..0000000000 --- a/packages/services/src/offscreen-client.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { FullViewingKey } from '@penumbra-zone/protobuf/penumbra/core/keys/v1/keys_pb'; -import { - Action, - TransactionPlan, - WitnessData, -} from '@penumbra-zone/protobuf/penumbra/core/transaction/v1/transaction_pb'; -import { ConnectError } from '@connectrpc/connect'; -import { errorFromJson } from '@connectrpc/connect/protocol-connect'; -import { ActionBuildMessage, OffscreenMessage } from '@penumbra-zone/types/internal-msg/offscreen'; -import { InternalRequest, InternalResponse } from '@penumbra-zone/types/internal-msg/shared'; -import type { Jsonified } from '@penumbra-zone/types/jsonified'; - -const OFFSCREEN_DOCUMENT_PATH = '/offscreen.html'; - -let active = 0; - -const activateOffscreen = async () => { - const noOffscreen = chrome.runtime - .getContexts({ - contextTypes: [chrome.runtime.ContextType.OFFSCREEN_DOCUMENT], - }) - .then(offscreenContexts => !offscreenContexts.length); - - if (!active++ || (await noOffscreen)) { - await chrome.offscreen - .createDocument({ - url: chrome.runtime.getURL(OFFSCREEN_DOCUMENT_PATH), - reasons: [chrome.offscreen.Reason.WORKERS], - justification: 'Manages Penumbra transaction WASM workers', - }) - .catch((e: unknown) => { - // the offscreen window might have been created since we checked - // TODO: other failures? - console.warn('Failed to create offscreen window', e); - }); - } -}; - -/** - * Decrement and close if there is no remaining activity. - */ -const releaseOffscreen = async () => { - if (!--active) { - await chrome.offscreen.closeDocument(); - } -}; - -const sendOffscreenMessage = async (req: InternalRequest) => - chrome.runtime.sendMessage, InternalResponse>(req).then(res => { - if ('error' in res) { - throw errorFromJson(res.error, undefined, ConnectError.from(res)); - } - return res.data; - }); - -/** - * Build actions in parallel, in an offscreen window where we can run wasm. - * @param cancel Promise that rejects if the build should be cancelled, usually auth denial. - * @returns An independently-promised list of action build results. - */ -const buildActions = ( - transactionPlan: TransactionPlan, - witness: WitnessData, - fullViewingKey: FullViewingKey, - cancel: PromiseLike, -): Promise[] => { - const activation = activateOffscreen(); - - // this json serialization involves a lot of binary -> base64 which is slow, - // so just do it once and reuse - const partialRequest = { - transactionPlan: transactionPlan.toJson() as Jsonified, - witness: witness.toJson() as Jsonified, - fullViewingKey: fullViewingKey.toJson() as Jsonified, - }; - - const buildTasks = transactionPlan.actions.map(async (_, actionPlanIndex) => { - const buildReq: InternalRequest = { - type: 'BUILD_ACTION', - request: { - ...partialRequest, - actionPlanIndex, - }, - }; - - // wait for offscreen to finish standing up - await activation; - - const buildRes = await sendOffscreenMessage(buildReq); - return Action.fromJson(buildRes); - }); - - void Promise.race([Promise.all(buildTasks), cancel]) - // suppress 'unhandled promise' logs - real failures are already conveyed by the individual promises. - .catch() - // this build is done with offscreen. it may shut down - .finally(() => void releaseOffscreen()); - - return buildTasks; -}; - -export const offscreenClient = { buildActions }; diff --git a/packages/services/src/view-service/util/build-action-worker.ts b/packages/services/src/view-service/util/build-action-worker.ts new file mode 100644 index 0000000000..33e2357579 --- /dev/null +++ b/packages/services/src/view-service/util/build-action-worker.ts @@ -0,0 +1,91 @@ +import { + ActionPlan, + TransactionPlan, + WitnessData, +} from '@penumbra-zone/protobuf/penumbra/core/transaction/v1/transaction_pb'; +import type { JsonObject, JsonValue } from '@bufbuild/protobuf'; +import { FullViewingKey } from '@penumbra-zone/protobuf/penumbra/core/keys/v1/keys_pb'; +import keyPaths from '@penumbra-zone/keys'; + +export interface WorkerBuildAction { + transactionPlan: JsonObject; + witness: JsonObject; + fullViewingKey: JsonObject; + actionPlanIndex: number; +} + +interface ExecuteWorkerParams { + transactionPlan: TransactionPlan; + witness: WitnessData; + fullViewingKey: FullViewingKey; + actionPlanIndex: number; +} + +// necessary to propagate errors that occur in promises +// see: https://stackoverflow.com/questions/39992417/how-to-bubble-a-web-worker-error-in-a-promise-via-worker-onerror +globalThis.addEventListener( + 'unhandledrejection', + event => { + // the event object has two special properties: + // event.promise - the promise that generated the error + // event.reason - the unhandled error object + throw event.reason; + }, + { once: true }, +); + +const workerListener = ({ data }: MessageEvent) => { + console.debug('workerListener', data); + const { + transactionPlan: transactionPlanJson, + witness: witnessJson, + fullViewingKey: fullViewingKeyJson, + actionPlanIndex, + } = data; + + // Deserialize payload + const transactionPlan = TransactionPlan.fromJson(transactionPlanJson); + const witness = WitnessData.fromJson(witnessJson); + const fullViewingKey = FullViewingKey.fromJson(fullViewingKeyJson); + + void executeWorker({ transactionPlan, witness, fullViewingKey, actionPlanIndex }).then( + jsonAction => { + console.debug('built action', jsonAction); + globalThis.postMessage(jsonAction); + }, + ); +}; + +globalThis.addEventListener('message', workerListener, { once: true }); + +const executeWorker = async ({ + transactionPlan, + witness, + fullViewingKey, + actionPlanIndex, +}: ExecuteWorkerParams): Promise => { + console.debug('executeWorker', transactionPlan, witness, fullViewingKey, actionPlanIndex); + // Dynamically load wasm module + const penumbraWasmModule = await import('@penumbra-zone/wasm/build'); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- asdf + const actionType = transactionPlan.actions[actionPlanIndex]!.action.case!; + + type KeyPaths = Partial, URL>>; + const keyPath = (keyPaths as KeyPaths)[actionType]; + + console.debug('build-action-worker using keyPath', keyPath); + + // Build action according to specification in `TransactionPlan` + const action = await penumbraWasmModule.buildActionParallel( + transactionPlan, + witness, + fullViewingKey, + actionPlanIndex, + keyPath?.href, + ); + + console.debug('built action', action.toJson()); + + return action.toJson(); +}; diff --git a/packages/services/src/view-service/util/build-tx.ts b/packages/services/src/view-service/util/build-tx.ts index 78430653e4..923ae2632b 100644 --- a/packages/services/src/view-service/util/build-tx.ts +++ b/packages/services/src/view-service/util/build-tx.ts @@ -5,40 +5,44 @@ import { WitnessData, } from '@penumbra-zone/protobuf/penumbra/core/transaction/v1/transaction_pb'; import { buildParallel } from '@penumbra-zone/wasm/build'; -import { offscreenClient } from '../../offscreen-client.js'; +import { launchActionWorkers, taskProgress } from './parallel.js'; import { AuthorizeAndBuildResponse, WitnessAndBuildResponse, } from '@penumbra-zone/protobuf/penumbra/view/v1/view_pb'; import { PartialMessage } from '@bufbuild/protobuf'; import { ConnectError } from '@connectrpc/connect'; - import { FullViewingKey } from '@penumbra-zone/protobuf/penumbra/core/keys/v1/keys_pb'; export const optimisticBuild = async function* ( transactionPlan: TransactionPlan, witnessData: WitnessData, - authorizationRequest: PromiseLike, + authorizationRequest: Promise, fvk: FullViewingKey, -) { - // a promise that rejects if auth denies. raced with build tasks to cancel. - // if we raced auth directly, approval would complete the race. - const cancel = new Promise( - (_, reject) => - void Promise.resolve(authorizationRequest).catch((r: unknown) => - reject(ConnectError.from(r)), - ), - ); - - // kick off the parallel actions build - const offscreenTasks = offscreenClient.buildActions(transactionPlan, witnessData, fvk, cancel); - - // status updates - yield* progressStream(offscreenTasks, cancel); +): AsyncGenerator> { + const ac = new AbortController(); + void authorizationRequest.catch((r: unknown) => ac.abort(ConnectError.from(r))); + + // kick off the workers + const actionBuilds = launchActionWorkers(transactionPlan, witnessData, fvk, ac.signal); + + // yield status updates as builds complete + for await (const progress of taskProgress( + actionBuilds, + ac.signal, + 1, // offset to represent the final step + )) { + yield { + status: { + case: 'buildProgress', + value: { progress }, + }, + }; + } - // final build is synchronous + // collect everything and execute final step const transaction: Transaction = buildParallel( - await Promise.all(offscreenTasks), + await Promise.all(actionBuilds), transactionPlan, witnessData, await authorizationRequest, @@ -49,27 +53,5 @@ export const optimisticBuild = async function* ( case: 'complete', value: { transaction }, }, - // TODO: satisfies type parameter? - } satisfies PartialMessage; -}; - -const progressStream = async function* (tasks: PromiseLike[], cancel: PromiseLike) { - // deliberately not a 'map' - tasks and promises have no direct relationship. - const tasksRemaining = Array.from(tasks, () => Promise.withResolvers()); - - // tasksRemaining will be consumed in order, as tasks complete in any order. - tasks.forEach(task => void task.then(() => tasksRemaining.shift()?.resolve())); - - // yield status when any task resolves the next 'remaining' promise - while (tasksRemaining.length) { - await Promise.race([cancel, tasksRemaining[0]?.promise]); - yield { - status: { - case: 'buildProgress', - // +1 to represent the final build step, which we aren't handling here - value: { progress: (tasks.length - tasksRemaining.length) / (tasks.length + 1) }, - }, - // TODO: satisfies type parameter? - } satisfies PartialMessage; - } + }; }; diff --git a/packages/services/src/view-service/util/parallel.ts b/packages/services/src/view-service/util/parallel.ts new file mode 100644 index 0000000000..6463c3b313 --- /dev/null +++ b/packages/services/src/view-service/util/parallel.ts @@ -0,0 +1,89 @@ +import { JsonObject, JsonValue } from '@bufbuild/protobuf'; +import { FullViewingKey } from '@penumbra-zone/protobuf/penumbra/core/keys/v1/keys_pb'; +import { + Action, + TransactionPlan, + WitnessData, +} from '@penumbra-zone/protobuf/penumbra/core/transaction/v1/transaction_pb'; +import { WorkerBuildAction } from './build-action-worker.js'; +import { ConnectError } from '@connectrpc/connect'; + +/** + * Build actions in parallel by launching a worker for each action in the plan. + * @returns An individually-promised list of build results. + */ +export const launchActionWorkers = ( + transactionPlan: TransactionPlan, + witness: WitnessData, + fullViewingKey: FullViewingKey, + signal: AbortSignal, +): Promise[] => { + const partialRequest: Omit = { + transactionPlan: transactionPlan.toJson() as JsonObject, + witness: witness.toJson() as JsonObject, + fullViewingKey: fullViewingKey.toJson() as JsonObject, + }; + + const workerOutputs = transactionPlan.actions.map(async (_, actionPlanIndex) => { + const buildReq: WorkerBuildAction = { + ...partialRequest, + actionPlanIndex, + }; + + const actionWorker = new Worker(new URL('./build-action-worker.js', import.meta.url)); + + const buildRes = await new Promise((resolve, reject) => { + signal.addEventListener('abort', () => reject(ConnectError.from(signal.reason))); + signal.throwIfAborted(); + + actionWorker.onmessage = evt => { + console.debug('actionWorker.onmessage', evt.data); + resolve(Action.fromJson(evt.data as JsonValue)); + }; + actionWorker.onerror = evt => { + console.debug('actionWorker.onerror', evt.error); + reject(evt.error as Error); + }; + actionWorker.onmessageerror = evt => { + console.debug('actionWorker.onmessageerror', evt.data); + reject(new Error('Message Error', { cause: evt.data })); + }; + + actionWorker.postMessage(buildReq); + }).finally(() => { + console.debug('actionWorker finally'); + actionWorker.terminate(); + }); + + return buildRes; + }); + + return workerOutputs; +}; + +/** Generic progress tracking for some set of parallel tasks. Yields a fraction + * from 0 to 1 representing (completed / tasks.length). The offset parameter + * represents some number of other tasks known by the caller. */ +export const taskProgress = async function* ( + tasks: Promise[], + signal: AbortSignal, + offsetTotal = 0, +): AsyncGenerator { + const cancel = new Promise((_, reject) => { + signal.addEventListener('abort', () => reject(ConnectError.from(signal.reason))); + }); + + // tasksRemaining is deliberately not a 'map' - tasks and promises have no + // direct relationship. tasksRemaining will be consumed in order, as tasks + // complete in any order. + const tasksRemaining = Array.from(tasks, () => Promise.withResolvers()); + tasks.forEach(task => void task.then(() => tasksRemaining.shift()?.resolve())); + + // yield status when any task resolves the next 'remaining' promise + while (tasksRemaining.length) { + await Promise.race([cancel, tasksRemaining[0]?.promise]); + // +offset to represent some portion of the task known by the caller + const completed = tasks.length - tasksRemaining.length; + yield completed / (tasks.length + offsetTotal); + } +}; diff --git a/packages/services/tsconfig.json b/packages/services/tsconfig.json index d7621332ac..8ff2e131f3 100644 --- a/packages/services/tsconfig.json +++ b/packages/services/tsconfig.json @@ -6,7 +6,8 @@ "outDir": "dist", "preserveWatchOutput": true, "rootDir": "src", - "target": "ESNext" + "target": "ESNext", + "lib": ["ESNext", "DOM"] }, "extends": "@tsconfig/strictest/tsconfig.json", "include": ["src"] diff --git a/packages/types/src/internal-msg/chrome-error.ts b/packages/types/src/internal-msg/chrome-error.ts deleted file mode 100644 index f1bd268e87..0000000000 --- a/packages/types/src/internal-msg/chrome-error.ts +++ /dev/null @@ -1,7 +0,0 @@ -type ChromeResponderDroppedError = Error & { - message: 'A listener indicated an asynchronous response by returning true, but the message channel closed before a response was received'; -}; -export const isChromeResponderDroppedError = (e: unknown): e is ChromeResponderDroppedError => - e instanceof Error && - e.message === - 'A listener indicated an asynchronous response by returning true, but the message channel closed before a response was received'; diff --git a/packages/types/src/internal-msg/offscreen.ts b/packages/types/src/internal-msg/offscreen.ts deleted file mode 100644 index 40b938254d..0000000000 --- a/packages/types/src/internal-msg/offscreen.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { - Action, - TransactionPlan, - WitnessData, -} from '@penumbra-zone/protobuf/penumbra/core/transaction/v1/transaction_pb'; -import type { Jsonified } from '../jsonified.js'; -import type { InternalMessage, InternalRequest, InternalResponse } from './shared.js'; -import { FullViewingKey } from '@penumbra-zone/protobuf/penumbra/core/keys/v1/keys_pb'; - -export type ActionBuildMessage = InternalMessage< - 'BUILD_ACTION', - ActionBuildRequest, - ActionBuildResponse ->; - -export type OffscreenMessage = ActionBuildMessage; -export type OffscreenRequest = InternalRequest; -export type OffscreenResponse = InternalResponse; - -export interface ActionBuildRequest { - transactionPlan: Jsonified; - witness: Jsonified; - fullViewingKey: Jsonified; - actionPlanIndex: number; -} -export type ActionBuildResponse = Jsonified; - -export const isActionBuildRequest = (req: unknown): req is ActionBuildRequest => - req != null && - typeof req === 'object' && - 'transactionPlan' in req && - req.transactionPlan != null && - typeof req.transactionPlan === 'object' && - 'actions' in req.transactionPlan && - Array.isArray(req.transactionPlan.actions) && - 'witness' in req && - req.witness != null && - typeof req.witness === 'object' && - 'fullViewingKey' in req && - typeof req.fullViewingKey === 'object' && - 'actionPlanIndex' in req && - typeof req.actionPlanIndex === 'number'; - -export const isOffscreenRequest = (req: unknown): req is OffscreenRequest => - req != null && - typeof req === 'object' && - 'type' in req && - typeof req.type === 'string' && - req.type === 'BUILD_ACTION'; diff --git a/packages/types/src/internal-msg/shared.ts b/packages/types/src/internal-msg/shared.ts deleted file mode 100644 index 31d5e8b41e..0000000000 --- a/packages/types/src/internal-msg/shared.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { JsonValue } from '@bufbuild/protobuf'; - -export interface InternalMessage { - type: Type; - request: Req; - response: Res; -} - -export interface InternalRequest> { - type: M['type']; - request: M['request']; -} - -export type InternalResponse> = - | { - type: M['type']; - data: M['response']; - } - | { - type: M['type']; - error: JsonValue; - }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 51369f5039..b8d3643619 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,7 +37,7 @@ importers: version: link:packages/tailwind-config '@storybook/react-vite': specifier: 8.1.1 - version: 8.1.1(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.18.1)(typescript@5.5.3)(vite@5.3.3(@types/node@20.14.10)(terser@5.31.1)) + version: 8.1.1(prettier@3.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.18.1)(typescript@5.5.3)(vite@5.3.3(@types/node@20.14.10)(terser@5.31.1)) '@testing-library/jest-dom': specifier: ^6.4.5 version: 6.4.6(vitest@1.6.0(@types/node@20.14.10)(@vitest/browser@1.6.0)(jsdom@24.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.31.1)) @@ -498,6 +498,9 @@ importers: '@penumbra-zone/getters': specifier: workspace:* version: link:../getters + '@penumbra-zone/keys': + specifier: workspace:* + version: link:../keys '@penumbra-zone/protobuf': specifier: workspace:* version: link:../protobuf @@ -519,6 +522,9 @@ importers: '@types/chrome': specifier: ^0.0.268 version: 0.0.268 + import-meta-resolve: + specifier: ^4.1.0 + version: 4.1.0 packages/storage: dependencies: @@ -7481,6 +7487,9 @@ packages: resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} engines: {node: '>=8'} + import-meta-resolve@4.1.0: + resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -16411,33 +16420,6 @@ snapshots: - prettier - supports-color - '@storybook/builder-vite@8.1.1(prettier@3.3.3)(typescript@5.5.3)(vite@5.3.3(@types/node@20.14.10)(terser@5.31.1))': - dependencies: - '@storybook/channels': 8.1.1 - '@storybook/client-logger': 8.1.1 - '@storybook/core-common': 8.1.1(prettier@3.3.3) - '@storybook/core-events': 8.1.1 - '@storybook/csf-plugin': 8.1.1 - '@storybook/node-logger': 8.1.1 - '@storybook/preview': 8.1.1 - '@storybook/preview-api': 8.1.1 - '@storybook/types': 8.1.1 - '@types/find-cache-dir': 3.2.1 - browser-assert: 1.2.1 - es-module-lexer: 1.5.4 - express: 4.19.2 - find-cache-dir: 3.3.2 - fs-extra: 11.2.0 - magic-string: 0.30.10 - ts-dedent: 2.2.0 - vite: 5.3.3(@types/node@20.14.10)(terser@5.31.1) - optionalDependencies: - typescript: 5.5.3 - transitivePeerDependencies: - - encoding - - prettier - - supports-color - '@storybook/channels@8.1.1': dependencies: '@storybook/client-logger': 8.1.1 @@ -16584,43 +16566,6 @@ snapshots: - encoding - supports-color - '@storybook/core-common@8.1.1(prettier@3.3.3)': - dependencies: - '@storybook/core-events': 8.1.1 - '@storybook/csf-tools': 8.1.1 - '@storybook/node-logger': 8.1.1 - '@storybook/types': 8.1.1 - '@yarnpkg/fslib': 2.10.3 - '@yarnpkg/libzip': 2.3.0 - chalk: 4.1.2 - cross-spawn: 7.0.3 - esbuild: 0.20.2 - esbuild-register: 3.5.0(esbuild@0.20.2) - execa: 5.1.1 - file-system-cache: 2.3.0 - find-cache-dir: 3.3.2 - find-up: 5.0.0 - fs-extra: 11.2.0 - glob: 10.4.5 - handlebars: 4.7.8 - lazy-universal-dotenv: 4.0.0 - node-fetch: 2.7.0 - picomatch: 2.3.1 - pkg-dir: 5.0.0 - prettier-fallback: prettier@3.3.3 - pretty-hrtime: 1.0.3 - resolve-from: 5.0.0 - semver: 7.6.2 - tempy: 1.0.1 - tiny-invariant: 1.3.3 - ts-dedent: 2.2.0 - util: 0.12.5 - optionalDependencies: - prettier: 3.3.3 - transitivePeerDependencies: - - encoding - - supports-color - '@storybook/core-common@8.1.11(prettier@3.3.2)': dependencies: '@storybook/core-events': 8.1.11 @@ -16791,21 +16736,6 @@ snapshots: - prettier - supports-color - '@storybook/docs-tools@8.1.1(prettier@3.3.3)': - dependencies: - '@storybook/core-common': 8.1.1(prettier@3.3.3) - '@storybook/core-events': 8.1.1 - '@storybook/preview-api': 8.1.1 - '@storybook/types': 8.1.1 - '@types/doctrine': 0.0.3 - assert: 2.1.0 - doctrine: 3.0.0 - lodash: 4.17.21 - transitivePeerDependencies: - - encoding - - prettier - - supports-color - '@storybook/docs-tools@8.1.11(prettier@3.3.2)': dependencies: '@storybook/core-common': 8.1.11(prettier@3.3.2) @@ -16944,31 +16874,6 @@ snapshots: - typescript - vite-plugin-glimmerx - '@storybook/react-vite@8.1.1(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.18.1)(typescript@5.5.3)(vite@5.3.3(@types/node@20.14.10)(terser@5.31.1))': - dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.1(typescript@5.5.3)(vite@5.3.3(@types/node@20.14.10)(terser@5.31.1)) - '@rollup/pluginutils': 5.1.0(rollup@4.18.1) - '@storybook/builder-vite': 8.1.1(prettier@3.3.3)(typescript@5.5.3)(vite@5.3.3(@types/node@20.14.10)(terser@5.31.1)) - '@storybook/node-logger': 8.1.1 - '@storybook/react': 8.1.1(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.3) - '@storybook/types': 8.1.1 - find-up: 5.0.0 - magic-string: 0.30.10 - react: 18.3.1 - react-docgen: 7.0.3 - react-dom: 18.3.1(react@18.3.1) - resolve: 1.22.8 - tsconfig-paths: 4.2.0 - vite: 5.3.3(@types/node@20.14.10)(terser@5.31.1) - transitivePeerDependencies: - - '@preact/preset-vite' - - encoding - - prettier - - rollup - - supports-color - - typescript - - vite-plugin-glimmerx - '@storybook/react@8.1.1(prettier@3.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.3)': dependencies: '@storybook/client-logger': 8.1.1 @@ -17001,38 +16906,6 @@ snapshots: - prettier - supports-color - '@storybook/react@8.1.1(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.3)': - dependencies: - '@storybook/client-logger': 8.1.1 - '@storybook/docs-tools': 8.1.1(prettier@3.3.3) - '@storybook/global': 5.0.0 - '@storybook/preview-api': 8.1.1 - '@storybook/react-dom-shim': 8.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/types': 8.1.1 - '@types/escodegen': 0.0.6 - '@types/estree': 0.0.51 - '@types/node': 18.19.39 - acorn: 7.4.1 - acorn-jsx: 5.3.2(acorn@7.4.1) - acorn-walk: 7.2.0 - escodegen: 2.1.0 - html-tags: 3.3.1 - lodash: 4.17.21 - prop-types: 15.8.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-element-to-jsx-string: 15.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - semver: 7.6.2 - ts-dedent: 2.2.0 - type-fest: 2.19.0 - util-deprecate: 1.0.2 - optionalDependencies: - typescript: 5.5.3 - transitivePeerDependencies: - - encoding - - prettier - - supports-color - '@storybook/react@8.1.11(prettier@3.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.3)': dependencies: '@storybook/client-logger': 8.1.11 @@ -20833,6 +20706,8 @@ snapshots: import-lazy@4.0.0: {} + import-meta-resolve@4.1.0: {} + imurmurhash@0.1.4: {} indent-string@4.0.0: {} From 5937a4a4542193eaa4d314a94f77388f49ec31d5 Mon Sep 17 00:00:00 2001 From: turbocrime Date: Thu, 5 Sep 2024 02:32:22 -0700 Subject: [PATCH 2/5] wip --- .../view-service/util/build-action-worker.ts | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/packages/services/src/view-service/util/build-action-worker.ts b/packages/services/src/view-service/util/build-action-worker.ts index 33e2357579..5d7213518d 100644 --- a/packages/services/src/view-service/util/build-action-worker.ts +++ b/packages/services/src/view-service/util/build-action-worker.ts @@ -1,12 +1,16 @@ import { - ActionPlan, + type Action, + type ActionPlan, TransactionPlan, WitnessData, } from '@penumbra-zone/protobuf/penumbra/core/transaction/v1/transaction_pb'; -import type { JsonObject, JsonValue } from '@bufbuild/protobuf'; +import type { JsonObject } from '@bufbuild/protobuf'; import { FullViewingKey } from '@penumbra-zone/protobuf/penumbra/core/keys/v1/keys_pb'; import keyPaths from '@penumbra-zone/keys'; +console.log('build-action-worker loaded'); +console.log('keyPaths', keyPaths); + export interface WorkerBuildAction { transactionPlan: JsonObject; witness: JsonObject; @@ -35,7 +39,7 @@ globalThis.addEventListener( ); const workerListener = ({ data }: MessageEvent) => { - console.debug('workerListener', data); + console.debug('build-action-worker workerListener', data); const { transactionPlan: transactionPlanJson, witness: witnessJson, @@ -48,11 +52,10 @@ const workerListener = ({ data }: MessageEvent) => { const witness = WitnessData.fromJson(witnessJson); const fullViewingKey = FullViewingKey.fromJson(fullViewingKeyJson); - void executeWorker({ transactionPlan, witness, fullViewingKey, actionPlanIndex }).then( - jsonAction => { - console.debug('built action', jsonAction); - globalThis.postMessage(jsonAction); - }, + console.debug('executing...'); + + void executeWorker({ transactionPlan, witness, fullViewingKey, actionPlanIndex }).then(action => + globalThis.postMessage(action.toJson()), ); }; @@ -63,8 +66,14 @@ const executeWorker = async ({ witness, fullViewingKey, actionPlanIndex, -}: ExecuteWorkerParams): Promise => { - console.debug('executeWorker', transactionPlan, witness, fullViewingKey, actionPlanIndex); +}: ExecuteWorkerParams): Promise => { + console.debug( + 'build-action-worker executeWorker', + transactionPlan, + witness, + fullViewingKey, + actionPlanIndex, + ); // Dynamically load wasm module const penumbraWasmModule = await import('@penumbra-zone/wasm/build'); @@ -74,7 +83,7 @@ const executeWorker = async ({ type KeyPaths = Partial, URL>>; const keyPath = (keyPaths as KeyPaths)[actionType]; - console.debug('build-action-worker using keyPath', keyPath); + console.debug('build-action-worker using keyPath', String(keyPath)); // Build action according to specification in `TransactionPlan` const action = await penumbraWasmModule.buildActionParallel( @@ -85,7 +94,7 @@ const executeWorker = async ({ keyPath?.href, ); - console.debug('built action', action.toJson()); + console.debug('built action', action); - return action.toJson(); + return action; }; From e027d0b5c34f310021342cdac46ae10760a9ba3d Mon Sep 17 00:00:00 2001 From: turbocrime Date: Thu, 5 Sep 2024 16:44:34 -0700 Subject: [PATCH 3/5] wip --- .../view-service/util/build-action-worker.ts | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/packages/services/src/view-service/util/build-action-worker.ts b/packages/services/src/view-service/util/build-action-worker.ts index 5d7213518d..17e2546798 100644 --- a/packages/services/src/view-service/util/build-action-worker.ts +++ b/packages/services/src/view-service/util/build-action-worker.ts @@ -27,19 +27,14 @@ interface ExecuteWorkerParams { // necessary to propagate errors that occur in promises // see: https://stackoverflow.com/questions/39992417/how-to-bubble-a-web-worker-error-in-a-promise-via-worker-onerror -globalThis.addEventListener( - 'unhandledrejection', - event => { - // the event object has two special properties: - // event.promise - the promise that generated the error - // event.reason - the unhandled error object - throw event.reason; - }, - { once: true }, -); +onunhandledrejection = function (this, event) { + console.debug('build-action-worker unhandledrejection', this, event); + throw event.reason; +}; -const workerListener = ({ data }: MessageEvent) => { - console.debug('build-action-worker workerListener', data); +onmessage = function (this, event) { + console.debug('build-action-worker onmessage', this, event); + const { data } = event as MessageEvent; const { transactionPlan: transactionPlanJson, witness: witnessJson, @@ -54,14 +49,12 @@ const workerListener = ({ data }: MessageEvent) => { console.debug('executing...'); - void executeWorker({ transactionPlan, witness, fullViewingKey, actionPlanIndex }).then(action => - globalThis.postMessage(action.toJson()), + void buildAction({ transactionPlan, witness, fullViewingKey, actionPlanIndex }).then(action => + postMessage(action.toJson()), ); }; -globalThis.addEventListener('message', workerListener, { once: true }); - -const executeWorker = async ({ +const buildAction = async ({ transactionPlan, witness, fullViewingKey, @@ -96,5 +89,6 @@ const executeWorker = async ({ console.debug('built action', action); + debugger; return action; }; From 9598f4b6a4f1a82d7554cd5790f1d86f937e87b1 Mon Sep 17 00:00:00 2001 From: turbocrime Date: Thu, 5 Sep 2024 19:14:08 -0700 Subject: [PATCH 4/5] working --- .../src/view-service/util/build-action-worker.ts | 9 +++++---- packages/services/src/view-service/util/parallel.ts | 4 ++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/services/src/view-service/util/build-action-worker.ts b/packages/services/src/view-service/util/build-action-worker.ts index 17e2546798..8da0a5875d 100644 --- a/packages/services/src/view-service/util/build-action-worker.ts +++ b/packages/services/src/view-service/util/build-action-worker.ts @@ -49,9 +49,11 @@ onmessage = function (this, event) { console.debug('executing...'); - void buildAction({ transactionPlan, witness, fullViewingKey, actionPlanIndex }).then(action => - postMessage(action.toJson()), - ); + void (async () => { + const action = await buildAction({ transactionPlan, witness, fullViewingKey, actionPlanIndex }); + console.log('built action!!!', action); + postMessage(action.toJson()); + })(); }; const buildAction = async ({ @@ -89,6 +91,5 @@ const buildAction = async ({ console.debug('built action', action); - debugger; return action; }; diff --git a/packages/services/src/view-service/util/parallel.ts b/packages/services/src/view-service/util/parallel.ts index 6463c3b313..e230f7daf4 100644 --- a/packages/services/src/view-service/util/parallel.ts +++ b/packages/services/src/view-service/util/parallel.ts @@ -30,7 +30,11 @@ export const launchActionWorkers = ( actionPlanIndex, }; + console.log('launching action worker'); const actionWorker = new Worker(new URL('./build-action-worker.js', import.meta.url)); + actionWorker.addEventListener('message', (...p) => console.log('parallel message', ...p)); + actionWorker.addEventListener('error', (...p) => console.log('parallel error', ...p)); + console.log('launched action worker'); const buildRes = await new Promise((resolve, reject) => { signal.addEventListener('abort', () => reject(ConnectError.from(signal.reason))); From fb2c9d2474fd62be7e8f7f3d65867d1b86bdc1b0 Mon Sep 17 00:00:00 2001 From: turbocrime Date: Fri, 6 Sep 2024 10:13:03 -0700 Subject: [PATCH 5/5] asdf --- .../src/view-service/util/parallel.ts | 24 ++++--------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/packages/services/src/view-service/util/parallel.ts b/packages/services/src/view-service/util/parallel.ts index e230f7daf4..5aee9ce8f0 100644 --- a/packages/services/src/view-service/util/parallel.ts +++ b/packages/services/src/view-service/util/parallel.ts @@ -30,34 +30,18 @@ export const launchActionWorkers = ( actionPlanIndex, }; - console.log('launching action worker'); const actionWorker = new Worker(new URL('./build-action-worker.js', import.meta.url)); - actionWorker.addEventListener('message', (...p) => console.log('parallel message', ...p)); - actionWorker.addEventListener('error', (...p) => console.log('parallel error', ...p)); - console.log('launched action worker'); const buildRes = await new Promise((resolve, reject) => { signal.addEventListener('abort', () => reject(ConnectError.from(signal.reason))); signal.throwIfAborted(); - actionWorker.onmessage = evt => { - console.debug('actionWorker.onmessage', evt.data); - resolve(Action.fromJson(evt.data as JsonValue)); - }; - actionWorker.onerror = evt => { - console.debug('actionWorker.onerror', evt.error); - reject(evt.error as Error); - }; - actionWorker.onmessageerror = evt => { - console.debug('actionWorker.onmessageerror', evt.data); - reject(new Error('Message Error', { cause: evt.data })); - }; + actionWorker.onmessage = evt => resolve(Action.fromJson(evt.data as JsonValue)); + actionWorker.onerror = evt => reject(evt.error as Error); + actionWorker.onmessageerror = evt => reject(new Error('Message Error', { cause: evt.data })); actionWorker.postMessage(buildReq); - }).finally(() => { - console.debug('actionWorker finally'); - actionWorker.terminate(); - }); + }).finally(() => actionWorker.terminate()); return buildRes; });