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

Offscreen API #303

Merged
merged 35 commits into from
Dec 24, 2023
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
ccc9f6c
nitpicking changes from offscreen branch + referencing tx approval wo…
TalDerei Dec 14, 2023
76c9b38
web-worker support using native web-worker javascript API
TalDerei Dec 14, 2023
f3ecff6
more idiomatic naming
TalDerei Dec 14, 2023
4c1d7f5
parallel action builds wip
TalDerei Dec 14, 2023
40e01e1
destructuring data objects passed to workers
TalDerei Dec 14, 2023
c478af9
spawn multiple workers and parallel tx building
TalDerei Dec 15, 2023
c6507f2
using promises paradigm
TalDerei Dec 15, 2023
226ef25
handle response and closing offscreen document
TalDerei Dec 15, 2023
010b6b9
remove unneccessary route
TalDerei Dec 15, 2023
cf065a3
encountering encoding error in build_parallel
TalDerei Dec 16, 2023
a87f5c5
working parallel builds
TalDerei Dec 17, 2023
92f7481
linting + fix build to pass CI
TalDerei Dec 17, 2023
f054df7
commit lock file
TalDerei Dec 17, 2023
4299a30
Merge branch 'main' into offscreen-api
TalDerei Dec 17, 2023
e3fa146
Merge branch 'main' into offscreen-api
TalDerei Dec 17, 2023
484cba4
attempt to pass CI
TalDerei Dec 17, 2023
65d6183
linting + L's comments
TalDerei Dec 18, 2023
07d9468
removed serial build method
TalDerei Dec 18, 2023
5e933cb
gabe's comments + refactor
TalDerei Dec 19, 2023
732bd30
minor lint
TalDerei Dec 19, 2023
5971d9f
addressing more comments
TalDerei Dec 19, 2023
dec1c51
remove console log
TalDerei Dec 19, 2023
5fff99c
Offscreen API Refactor (#318)
TalDerei Dec 22, 2023
0743e85
Merge branch 'main' into offscreen-api
TalDerei Dec 22, 2023
bdf5fd4
fix pnpm build
TalDerei Dec 22, 2023
8e4db5b
partially address L's comments
TalDerei Dec 22, 2023
00e441e
minimal action schema and validate
turbocrime Dec 23, 2023
1715399
message types wip
turbocrime Dec 23, 2023
8e592a3
partially addressing gabe's comments + fix tx building with proper se…
TalDerei Dec 23, 2023
0a24405
use jsonified + linting
TalDerei Dec 23, 2023
9584318
address more comments
TalDerei Dec 23, 2023
874164f
minor fix
TalDerei Dec 23, 2023
92958ea
lifecycle wip
turbocrime Dec 23, 2023
b4b2ce1
address more gabe's comments
TalDerei Dec 24, 2023
eac19d5
minor file name mod
TalDerei Dec 24, 2023
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
2 changes: 1 addition & 1 deletion apps/extension/public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"matches": ["<all_urls>"]
}
],
"permissions": ["storage", "unlimitedStorage"],
"permissions": ["storage", "unlimitedStorage", "offscreen"],
"host_permissions": ["<all_urls>"],
"content_security_policy": {
"extension_pages": "object-src 'self'; script-src 'self' 'wasm-unsafe-eval'"
Expand Down
12 changes: 12 additions & 0 deletions apps/extension/public/offscreen.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en" translate="no">
<head>
<meta charset="UTF-8" />
<meta content="notranslate" name="google" />
<title>Penumbra Wallet Offscreen</title>
</head>
<body>
<div id="root"></div>
<script src="js/offscreen.js"></script>
</body>
</html>
80 changes: 80 additions & 0 deletions apps/extension/src/offscreen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type { JsonValue } from '@bufbuild/protobuf';
import { OffscreenRequest } from '@penumbra-zone/types/src/internal-msg/offscreen-types';

export const isOffscreenRequest = (req: unknown): req is OffscreenRequest =>
req != null &&
typeof req === 'object' &&
'type' in req &&
typeof req.type === 'string' &&
req.type === 'BUILD_ACTION';

export const offscreenMessageHandler = (
req: unknown,
_: chrome.runtime.MessageSender,
sendResponse: (x: JsonValue) => void,
) => {
if (!isOffscreenRequest(req)) return false;

buildActionHandler(req, sendResponse);
return true;
};

chrome.runtime.onMessage.addListener(offscreenMessageHandler);

export const buildActionHandler = (
jsonReq: OffscreenRequest,
responder: (r: JsonValue) => void,
) => {
// Destructure the data object to get individual fields
const { transactionPlan, witness, fullViewingKey, length } = jsonReq.request;

// Spawn web workers
const workerPromises: Promise<JsonValue>[] = [];
for (let i = 0; i < length; i++) {
workerPromises.push(spawnWorker(transactionPlan, witness, fullViewingKey, i));
}
TalDerei marked this conversation as resolved.
Show resolved Hide resolved

// Wait for promises to resolve and construct response format
void Promise.all(workerPromises).then(batchActions => {
responder({
type: 'BUILD_ACTION',
data: batchActions,
});
TalDerei marked this conversation as resolved.
Show resolved Hide resolved
});
};

const spawnWorker = (
transactionPlan: JsonValue,
witness: JsonValue,
fullViewingKey: string,
actionId: number,
): Promise<JsonValue> => {
return new Promise((resolve, reject) => {
const worker = new Worker(new URL('./wasm-task.ts', import.meta.url));

const onWorkerMessage = (e: MessageEvent) => {
resolve(e.data as JsonValue);
worker.removeEventListener('message', onWorkerMessage);
worker.removeEventListener('message', onWorkerError);
worker.terminate();
};

const onWorkerError = (error: unknown) => {
reject(error);
worker.removeEventListener('message', onWorkerMessage);
worker.removeEventListener('message', onWorkerError);
worker.terminate();
};

worker.addEventListener('message', onWorkerMessage);
worker.addEventListener('error', onWorkerError);

// Send data to web worker
worker.postMessage({
transactionPlan,
witness,
fullViewingKey,
actionId,
});
TalDerei marked this conversation as resolved.
Show resolved Hide resolved
});
};
7 changes: 6 additions & 1 deletion apps/extension/src/service-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ const penumbraMessageHandler = (
message: unknown,
_sender: chrome.runtime.MessageSender,
sendResponse: (response: unknown) => void,
) => (isStdRequest(message) ? stdRouter(message, sendResponse, existingServices) : null);
) => {
if (!isStdRequest(message)) return;
stdRouter(message, sendResponse, existingServices);
return true;
};

chrome.runtime.onMessage.addListener(penumbraMessageHandler);

/*
Expand Down
58 changes: 58 additions & 0 deletions apps/extension/src/wasm-task.ts
TalDerei marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {
Action,
TransactionPlan,
WitnessData,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/transaction/v1alpha1/transaction_pb';
import { JsonValue } from '@bufbuild/protobuf';

interface WasmTaskInput {
transactionPlan: JsonValue;
witness: JsonValue;
fullViewingKey: string;
actionId: number;
}
TalDerei marked this conversation as resolved.
Show resolved Hide resolved

const workerListener = ({ data }: { data: WasmTaskInput }) => {
const {
transactionPlan: transactionPlanJson,
witness: witnessJson,
fullViewingKey,
actionId,
} = data;

// Deserialize payload
const transactionPlan = TransactionPlan.fromJson(transactionPlanJson);
const witness = WitnessData.fromJson(witnessJson);

void executeWorker(transactionPlan, witness, fullViewingKey, actionId).then(action => {
self.postMessage(action);
});
TalDerei marked this conversation as resolved.
Show resolved Hide resolved
};

self.addEventListener('message', workerListener);

async function executeWorker(
transactionPlan: TransactionPlan,
witness: WitnessData,
fullViewingKey: string,
actionId: number,
): Promise<Action> {
TalDerei marked this conversation as resolved.
Show resolved Hide resolved
console.log('web worker running...');
TalDerei marked this conversation as resolved.
Show resolved Hide resolved

// Dynamically load wasm module
const penumbraWasmModule = await import('@penumbra-zone/wasm-ts');

// Conditionally read proving keys from disk and load keys into WASM binary
const actionKey = transactionPlan.actions[actionId]?.action.case;
await penumbraWasmModule.loadProvingKey(actionKey!);
TalDerei marked this conversation as resolved.
Show resolved Hide resolved

// Build action according to specification in `TransactionPlan`
const action = penumbraWasmModule.buildActionParallel(
transactionPlan,
witness,
fullViewingKey,
actionId,
);

return action;
TalDerei marked this conversation as resolved.
Show resolved Hide resolved
}
1 change: 1 addition & 0 deletions apps/extension/webpack/webpack.common.js
turbocrime marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ module.exports = {
'service-worker': path.join(srcDir, 'service-worker.ts'),
'injected-connection-manager': path.join(srcDir, 'injected-connection-manager.ts'),
'injected-penumbra-global': path.join(srcDir, 'injected-penumbra-global.ts'),
offscreen: path.join(srcDir, 'offscreen.ts'),
},
output: {
path: path.join(__dirname, '../dist/js'),
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"test": "turbo run test",
"clean": "turbo clean",
"format": "prettier --write .",
"format-check": "prettier --check ."
"format-check": "prettier --check .",
"all-check": "pnpm install && pnpm format && pnpm format-check && pnpm lint && pnpm test && pnpm build"
},
"dependencies": {
"@buf/cosmos_ibc.bufbuild_es": "1.5.1-20231211183134-b93a64f9fb08.1",
Expand Down
47 changes: 47 additions & 0 deletions packages/router/src/grpc/offscreen-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { WitnessAndBuildRequest } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1alpha1/view_pb';
import { WitnessData } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/transaction/v1alpha1/transaction_pb';
import { InternalRequest, InternalResponse } from '@penumbra-zone/types/src/internal-msg/shared';
import {
ActionBuildMessage,
OffscreenMessage,
} from '@penumbra-zone/types/src/internal-msg/offscreen-types';

const OFFSCREEN_DOCUMENT_PATH = '/offscreen.html';

export const offscreenClient = {
buildAction: (
arg: WitnessAndBuildRequest,
witness: WitnessData,
fullViewingKey: string,
): Promise<InternalResponse<ActionBuildMessage>> =>
sendOffscreenMessage<ActionBuildMessage>({
type: 'BUILD_ACTION',
request: {
transactionPlan: arg.transactionPlan!.toJson(),
witness: witness.toJson(),
fullViewingKey,
length: arg.transactionPlan!.actions.length,
},
}),
};

export const sendOffscreenMessage = async <T extends OffscreenMessage>(
req: InternalRequest<T>,
): Promise<InternalResponse<ActionBuildMessage>> => {
await chrome.offscreen.createDocument({
TalDerei marked this conversation as resolved.
Show resolved Hide resolved
url: OFFSCREEN_DOCUMENT_PATH,
reasons: [chrome.offscreen.Reason.WORKERS],
justification: 'Manages Penumbra transaction WASM workers',
});

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
turbocrime marked this conversation as resolved.
Show resolved Hide resolved
const result = (await chrome.runtime.sendMessage({
...req,
})) as InternalResponse<ActionBuildMessage>;
if ('error' in result) throw new Error('failed to build action');

// Close offscreen document
await chrome.offscreen.closeDocument();
TalDerei marked this conversation as resolved.
Show resolved Hide resolved

return result;
};
10 changes: 7 additions & 3 deletions packages/router/src/grpc/view-protocol-server/witness-build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1alpha1/view_pb';
import { ViewReqMessage } from './router';
import { ServicesInterface } from '@penumbra-zone/types';
import { build, witness } from '@penumbra-zone/wasm-ts';
import { buildParallel, witness } from '@penumbra-zone/wasm-ts';
import { offscreenClient } from '../offscreen-client';

export const isWitnessBuildRequest = (msg: ViewReqMessage): msg is WitnessAndBuildRequest => {
return msg.getType().typeName === WitnessAndBuildRequest.typeName;
Expand All @@ -25,8 +26,11 @@ export const handleWitnessBuildReq = async (

const witnessData = witness(req.transactionPlan, sct);

const transaction = await build(
fullViewingKey,
const batchActions = await offscreenClient.buildAction(req, witnessData, fullViewingKey);
if ('error' in batchActions) throw new Error('failed to build action');

const transaction = buildParallel(
batchActions.data,
req.transactionPlan,
witnessData,
req.authorizationData,
Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/indexed-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export interface PenumbraDb extends DBSchema {
// @ts-expect-error Meant to be a marker to indicate it's json serialized.
// Protobuf values often need to be as they are json-deserialized in the wasm crate.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type Jsonified<T> = JsonValue;
export type Jsonified<T> = JsonValue;
TalDerei marked this conversation as resolved.
Show resolved Hide resolved

export type Tables = Record<string, StoreNames<PenumbraDb>>;

Expand Down
16 changes: 16 additions & 0 deletions packages/types/src/internal-msg/offscreen-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Action } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/transaction/v1alpha1/transaction_pb';
import { InternalMessage, InternalRequest, InternalResponse } from './shared';
import { JsonValue } from '@bufbuild/protobuf';

export type ActionBuildMessage = InternalMessage<'BUILD_ACTION', OffscreenRequestPayload, Action[]>;

export type OffscreenMessage = ActionBuildMessage;
export type OffscreenRequest = InternalRequest<OffscreenMessage>;
export type OffscreenResponse = InternalResponse<ActionBuildMessage>;

interface OffscreenRequestPayload {
transactionPlan: JsonValue;
witness: JsonValue;
fullViewingKey: string;
length: number;
}
16 changes: 16 additions & 0 deletions packages/types/src/transaction/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,19 @@ export const WasmBuildSchema = z.object({
transactionParameters: z.unknown(),
}),
});

export const WasmBuildActionSchema = z.object({
action: z.object({
body: z.object({
balanceCommitment: InnerBase64Schema,
notePayload: z.object({
encryptedNote: InnerBase64Schema,
ephemeralKey: Base64StringSchema,
noteCommitment: InnerBase64Schema,
}),
ovkWrappedKey: Base64StringSchema,
wrappedMemoKey: Base64StringSchema,
}),
proof: InnerBase64Schema,
}),
});
1 change: 1 addition & 0 deletions packages/wasm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './src/sct';
export * from './src/transaction';
export * from './src/build';
export * from './src/address';
export * from './src/utils';
39 changes: 30 additions & 9 deletions packages/wasm/src/build.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
import {
Action,
AuthorizationData,
Transaction,
TransactionPlan,
WitnessData,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/transaction/v1alpha1/transaction_pb';
import { JsonValue } from '@bufbuild/protobuf';
import {
StateCommitmentTree,
validateSchema,
WasmAuthorizeSchema,
WasmBuildSchema,
WasmWitnessDataSchema,
} from '@penumbra-zone/types';
import { authorize, build as wasmBuild, witness as wasmWitness } from '@penumbra-zone/wasm-bundler';
import { loadProvingKeys } from '../src/utils';
import {
authorize,
build_parallel as buildTxParallel,
build_action as buildAction,
witness as wasmWitness,
} from '@penumbra-zone/wasm-bundler';

export const authorizePlan = (spendKey: string, txPlan: TransactionPlan): AuthorizationData => {
const result = validateSchema(WasmAuthorizeSchema, authorize(spendKey, txPlan.toJson()));
Expand All @@ -24,17 +30,32 @@ export const witness = (txPlan: TransactionPlan, sct: StateCommitmentTree): Witn
return WitnessData.fromJsonString(JSON.stringify(result));
};

export const build = async (
fullViewingKey: string,
export const buildParallel = (
batchActions: Action[],
txPlan: TransactionPlan,
witnessData: WitnessData,
authData: AuthorizationData,
): Promise<Transaction> => {
await loadProvingKeys();

): Transaction => {
const result = validateSchema(
WasmBuildSchema,
wasmBuild(fullViewingKey, txPlan.toJson(), witnessData.toJson(), authData.toJson()),
buildTxParallel(batchActions, txPlan.toJson(), witnessData.toJson(), authData.toJson()),
);
return Transaction.fromJsonString(JSON.stringify(result));

return Transaction.fromJson(result as JsonValue);
};

export const buildActionParallel = (
txPlan: TransactionPlan,
witnessData: WitnessData,
fullViewingKey: string,
actionId: number,
): Action => {
const result = buildAction(
txPlan.toJson(),
txPlan.actions[actionId]?.toJson(),
fullViewingKey,
witnessData.toJson(),
) as Action;
TalDerei marked this conversation as resolved.
Show resolved Hide resolved

return result;
};
Loading
Loading