Skip to content

Commit 7078919

Browse files
committed
Draft: validation in worker thread
1 parent 924ecf2 commit 7078919

File tree

8 files changed

+205
-37
lines changed

8 files changed

+205
-37
lines changed

packages/sdk/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
"script": "./dist/streamr-sdk.web.min.js",
1414
"browser": {
1515
"./src/utils/persistence/ServerPersistence.ts": "./src/utils/persistence/BrowserPersistence.mts",
16-
"./dist/src/utils/persistence/ServerPersistence.js": "./dist/src/utils/persistence/BrowserPersistence.mjs"
16+
"./dist/src/utils/persistence/ServerPersistence.js": "./dist/src/utils/persistence/BrowserPersistence.mjs",
17+
"./src/signature/ServerSignatureValidation.ts": "./src/signature/BrowserSignatureValidation.mts",
18+
"./dist/src/signature/ServerSignatureValidation.js": "./dist/src/signature/BrowserSignatureValidation.mjs"
1719
},
1820
"exports": {
1921
"default": {
@@ -93,6 +95,7 @@
9395
"#IMPORTANT": "babel-runtime must be in dependencies, not devDependencies",
9496
"dependencies": {
9597
"@babel/runtime": "^7.28.4",
98+
"comlink": "^4.4.2",
9699
"@babel/runtime-corejs3": "^7.28.4",
97100
"@noble/post-quantum": "^0.4.1",
98101
"@protobuf-ts/runtime": "^2.8.2",
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* Browser implementation of signature validation using Web Worker.
3+
* This offloads CPU-intensive cryptographic operations to a separate thread.
4+
*/
5+
import * as Comlink from 'comlink'
6+
import { SignatureValidationContext } from './SignatureValidationContext.js'
7+
import { SignatureValidationResult } from './signatureValidation.js'
8+
import type { SignatureValidationWorkerApi } from './SignatureValidationWorker.js'
9+
import { StreamMessage } from '../protocol/StreamMessage.js'
10+
11+
export default class BrowserSignatureValidation implements SignatureValidationContext {
12+
private worker: Worker | null = null
13+
private workerApi: Comlink.Remote<SignatureValidationWorkerApi> | null = null
14+
15+
private ensureWorker(): Comlink.Remote<SignatureValidationWorkerApi> {
16+
if (!this.workerApi) {
17+
// Webpack 5 handles this pattern automatically, creating a separate chunk for the worker
18+
this.worker = new Worker(
19+
/* webpackChunkName: "signature-worker" */
20+
new URL('./SignatureValidationWorker.js', import.meta.url)
21+
)
22+
this.workerApi = Comlink.wrap<SignatureValidationWorkerApi>(this.worker)
23+
}
24+
return this.workerApi
25+
}
26+
27+
async validateSignature(message: StreamMessage): Promise<SignatureValidationResult> {
28+
return this.ensureWorker().validateSignature(message)
29+
}
30+
31+
destroy(): void {
32+
if (this.worker) {
33+
this.worker.terminate()
34+
this.worker = null
35+
}
36+
this.workerApi = null
37+
}
38+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Node.js implementation of signature validation.
3+
* Runs on the main thread (worker threads can be added later if needed).
4+
*/
5+
import { StreamMessage } from '../protocol/StreamMessage'
6+
import { SignatureValidationContext } from './SignatureValidationContext'
7+
import { SignatureValidationResult, validateSignatureData } from './signatureValidation'
8+
9+
export default class ServerSignatureValidation implements SignatureValidationContext {
10+
11+
async validateSignature(message: StreamMessage): Promise<SignatureValidationResult> {
12+
return validateSignatureData(message)
13+
}
14+
15+
// eslint-disable-next-line class-methods-use-this
16+
destroy(): void {
17+
// No-op for server implementation
18+
}
19+
}
20+
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Interface for signature validation backend.
3+
* Browser implementation uses a Web Worker, Node.js runs on main thread.
4+
*/
5+
import { StreamMessage } from '../protocol/StreamMessage'
6+
import { SignatureValidationResult } from './signatureValidation'
7+
8+
export interface SignatureValidationContext {
9+
validateSignature(message: StreamMessage): Promise<SignatureValidationResult>
10+
destroy(): void
11+
}
12+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Web Worker for signature validation.
3+
* This worker handles CPU-intensive cryptographic operations off the main thread.
4+
*/
5+
import * as Comlink from 'comlink'
6+
import { validateSignatureData, SignatureValidationResult } from './signatureValidation'
7+
import { StreamMessage } from '../protocol/StreamMessage'
8+
9+
const workerApi = {
10+
validateSignature: async (data: StreamMessage): Promise<SignatureValidationResult> => {
11+
return validateSignatureData(data)
12+
}
13+
}
14+
15+
export type SignatureValidationWorkerApi = typeof workerApi
16+
17+
Comlink.expose(workerApi)
18+
Lines changed: 39 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,33 @@
1-
import { toEthereumAddress, toUserIdRaw, SigningUtil } from '@streamr/utils'
1+
import { toEthereumAddress } from '@streamr/utils'
22
import { Lifecycle, scoped } from 'tsyringe'
33
import { ERC1271ContractFacade } from '../contracts/ERC1271ContractFacade'
4+
import { DestroySignal } from '../DestroySignal'
45
import { StreamMessage } from '../protocol/StreamMessage'
56
import { StreamrClientError } from '../StreamrClientError'
6-
import { createLegacySignaturePayload } from './createLegacySignaturePayload'
77
import { createSignaturePayload } from './createSignaturePayload'
8+
import { SignatureValidationContext } from './SignatureValidationContext'
9+
// This import will be swapped to BrowserSignatureValidation.mts in browser builds
10+
import SignatureValidation from './ServerSignatureValidation'
811
import { SignatureType } from '@streamr/trackerless-network'
9-
import { IDENTITY_MAPPING } from '../identity/IdentityMapping'
10-
11-
// Lookup structure SignatureType -> SigningUtil
12-
const signingUtilBySignatureType: Record<number, SigningUtil> = Object.fromEntries(
13-
IDENTITY_MAPPING.map((idMapping) => [idMapping.signatureType, SigningUtil.getInstance(idMapping.keyType)])
14-
)
15-
16-
const evmSigner = SigningUtil.getInstance('ECDSA_SECP256K1_EVM')
1712

1813
@scoped(Lifecycle.ContainerScoped)
1914
export class SignatureValidator {
2015
private readonly erc1271ContractFacade: ERC1271ContractFacade
16+
private validationContext: SignatureValidationContext | undefined
2117

22-
constructor(erc1271ContractFacade: ERC1271ContractFacade) {
18+
constructor(
19+
erc1271ContractFacade: ERC1271ContractFacade,
20+
destroySignal: DestroySignal
21+
) {
2322
this.erc1271ContractFacade = erc1271ContractFacade
23+
destroySignal.onDestroy.listen(() => this.destroy())
24+
}
25+
26+
private getValidationContext(): SignatureValidationContext {
27+
if (!this.validationContext) {
28+
this.validationContext = new SignatureValidation()
29+
}
30+
return this.validationContext
2431
}
2532

2633
/**
@@ -41,36 +48,33 @@ export class SignatureValidator {
4148
}
4249

4350
private async validate(streamMessage: StreamMessage): Promise<boolean> {
44-
const signingUtil = signingUtilBySignatureType[streamMessage.signatureType]
45-
46-
// Common case
47-
if (signingUtil) {
48-
return signingUtil.verifySignature(
49-
toUserIdRaw(streamMessage.getPublisherId()),
50-
createSignaturePayload(streamMessage),
51-
streamMessage.signature
52-
)
53-
}
54-
55-
// Special handling: different payload computation, same SigningUtil
56-
if (streamMessage.signatureType === SignatureType.ECDSA_SECP256K1_LEGACY) {
57-
return evmSigner.verifySignature(
58-
// publisherId is hex encoded Ethereum address string
59-
toUserIdRaw(streamMessage.getPublisherId()),
60-
createLegacySignaturePayload(streamMessage),
61-
streamMessage.signature
62-
)
63-
}
64-
65-
// Special handling: check signature with ERC-1271 contract facade
6651
if (streamMessage.signatureType === SignatureType.ERC_1271) {
6752
return this.erc1271ContractFacade.isValidSignature(
68-
toEthereumAddress(streamMessage.getPublisherId()),
53+
toEthereumAddress(streamMessage.messageId.publisherId),
6954
createSignaturePayload(streamMessage),
7055
streamMessage.signature
7156
)
7257
}
58+
const result = await this.getValidationContext().validateSignature(streamMessage)
59+
switch (result.type) {
60+
case 'valid':
61+
return true
62+
case 'invalid':
63+
return false
64+
case 'error':
65+
throw new Error(result.message)
66+
default:
67+
throw new Error(`Unknown signature validation result type: ${result.type}`)
68+
}
69+
}
7370

74-
throw new Error(`Cannot validate message signature, unsupported signatureType: "${streamMessage.signatureType}"`)
71+
/**
72+
* Cleanup worker resources when the validator is no longer needed.
73+
*/
74+
destroy(): void {
75+
if (this.validationContext) {
76+
this.validationContext.destroy()
77+
this.validationContext = undefined
78+
}
7579
}
7680
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* Core signature validation logic - shared between worker and main thread implementations.
3+
* This file contains pure cryptographic validation functions without any network dependencies.
4+
*/
5+
import { SigningUtil, toUserIdRaw } from '@streamr/utils'
6+
import { SignatureType } from '@streamr/trackerless-network'
7+
import { IDENTITY_MAPPING } from '../identity/IdentityMapping'
8+
import { createSignaturePayload } from './createSignaturePayload'
9+
import { createLegacySignaturePayload } from './createLegacySignaturePayload'
10+
import { StreamMessage } from '../protocol/StreamMessage'
11+
12+
// Lookup structure SignatureType -> SigningUtil
13+
const signingUtilBySignatureType: Record<number, SigningUtil> = Object.fromEntries(
14+
IDENTITY_MAPPING.map((idMapping) => [idMapping.signatureType, SigningUtil.getInstance(idMapping.keyType)])
15+
)
16+
17+
const evmSigner = SigningUtil.getInstance('ECDSA_SECP256K1_EVM')
18+
19+
/**
20+
* Result of signature validation
21+
*/
22+
export type SignatureValidationResult =
23+
| { type: 'valid' }
24+
| { type: 'invalid' }
25+
| { type: 'requires_erc1271' }
26+
| { type: 'error'; message: string }
27+
28+
/**
29+
* Validate signature using extracted data.
30+
* This is the core validation logic that can be run in a worker.
31+
*/
32+
export async function validateSignatureData(message: StreamMessage): Promise<SignatureValidationResult> {
33+
try {
34+
const signingUtil = signingUtilBySignatureType[message.signatureType]
35+
// Common case: standard signature types
36+
if (signingUtil) {
37+
const payload = createSignaturePayload({
38+
messageId: message.messageId,
39+
content: message.content,
40+
messageType: message.messageType,
41+
prevMsgRef: message.prevMsgRef,
42+
newGroupKey: message.newGroupKey,
43+
})
44+
const isValid = await signingUtil.verifySignature(
45+
toUserIdRaw(message.messageId.publisherId),
46+
payload,
47+
message.signature
48+
)
49+
return isValid ? { type: 'valid' } : { type: 'invalid' }
50+
}
51+
// Special handling: legacy signature type
52+
if (message.signatureType === SignatureType.ECDSA_SECP256K1_LEGACY) {
53+
const payload = createLegacySignaturePayload({
54+
messageId: message.messageId,
55+
content: message.content,
56+
encryptionType: message.encryptionType,
57+
prevMsgRef: message.prevMsgRef,
58+
newGroupKey: message.newGroupKey,
59+
})
60+
const isValid = await evmSigner.verifySignature(
61+
toUserIdRaw(message.messageId.publisherId),
62+
payload,
63+
message.signature
64+
)
65+
return isValid ? { type: 'valid' } : { type: 'invalid' }
66+
}
67+
return { type: 'error', message: `Unsupported signatureType: "${message.signatureType}"` }
68+
} catch (err) {
69+
return { type: 'error', message: String(err) }
70+
}
71+
}
72+

packages/sdk/webpack.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ module.exports = (env, argv) => {
6767
GIT_BRANCH: gitRevisionPlugin.branch(),
6868
}),
6969
new webpack.optimize.LimitChunkCountPlugin({
70-
maxChunks: 1
70+
// Allow 2 chunks: main bundle + signature validation worker
71+
maxChunks: 2
7172
})
7273
],
7374
performance: {

0 commit comments

Comments
 (0)