Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

remove offscreen and parallelize transaction building #1769

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

95 changes: 95 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,95 @@
import {
type Action,
type ActionPlan,
TransactionPlan,
WitnessData,
} from '@penumbra-zone/protobuf/penumbra/core/transaction/v1/transaction_pb';
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');

Check failure on line 11 in packages/services/src/view-service/util/build-action-worker.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected console statement
console.log('keyPaths', keyPaths);

Check failure on line 12 in packages/services/src/view-service/util/build-action-worker.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected console statement

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
onunhandledrejection = function (this, event) {
console.debug('build-action-worker unhandledrejection', this, event);
throw event.reason;
};

onmessage = function (this, event) {
console.debug('build-action-worker onmessage', this, event);
const { data } = event as MessageEvent<WorkerBuildAction>;
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);

console.debug('executing...');

void (async () => {
const action = await buildAction({ transactionPlan, witness, fullViewingKey, actionPlanIndex });
console.log('built action!!!', action);

Check failure on line 54 in packages/services/src/view-service/util/build-action-worker.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected console statement
postMessage(action.toJson());
})();
};

const buildAction = async ({
transactionPlan,
witness,
fullViewingKey,
actionPlanIndex,
}: ExecuteWorkerParams): Promise<Action> => {
console.debug(
'build-action-worker 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', String(keyPath));

// Build action according to specification in `TransactionPlan`
const action = await penumbraWasmModule.buildActionParallel(
transactionPlan,
witness,
fullViewingKey,
actionPlanIndex,
keyPath?.href,
);

console.debug('built action', action);

return action;
};
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
Loading