Skip to content

Commit

Permalink
services use DOM lib, make keys pathable, remove 'internal message' f…
Browse files Browse the repository at this point in the history
…rom types
  • Loading branch information
turbocrime committed Sep 5, 2024
1 parent ad64968 commit 3027e44
Show file tree
Hide file tree
Showing 14 changed files with 261 additions and 374 deletions.
8 changes: 0 additions & 8 deletions packages/keys/action-keys.json

This file was deleted.

19 changes: 15 additions & 4 deletions packages/keys/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
10 changes: 10 additions & 0 deletions packages/keys/src/index.ts
Original file line number Diff line number Diff line change
@@ -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;
13 changes: 13 additions & 0 deletions packages/keys/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"]
}
5 changes: 4 additions & 1 deletion packages/services/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,23 @@
"@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",
"@connectrpc/connect": "^1.4.0",
"@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:*",
Expand Down
102 changes: 0 additions & 102 deletions packages/services/src/offscreen-client.ts

This file was deleted.

91 changes: 91 additions & 0 deletions packages/services/src/view-service/util/build-action-worker.ts
Original file line number Diff line number Diff line change
@@ -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<WorkerBuildAction>) => {
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<JsonValue> => {
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<Record<NonNullable<ActionPlan['action']['case']>, 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();
};
68 changes: 25 additions & 43 deletions packages/services/src/view-service/util/build-tx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AuthorizationData>,
authorizationRequest: Promise<AuthorizationData>,
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<never>(
(_, 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<PartialMessage<AuthorizeAndBuildResponse | WitnessAndBuildResponse>> {
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,
Expand All @@ -49,27 +53,5 @@ export const optimisticBuild = async function* (
case: 'complete',
value: { transaction },
},
// TODO: satisfies type parameter?
} satisfies PartialMessage<AuthorizeAndBuildResponse | WitnessAndBuildResponse>;
};

const progressStream = async function* <T>(tasks: PromiseLike<T>[], cancel: PromiseLike<never>) {
// deliberately not a 'map' - tasks and promises have no direct relationship.
const tasksRemaining = Array.from(tasks, () => Promise.withResolvers<void>());

// 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<AuthorizeAndBuildResponse | WitnessAndBuildResponse>;
}
};
};
Loading

0 comments on commit 3027e44

Please sign in to comment.