@btc-vision/bitcoin 7.0.0-alpha.3 → 7.0.0-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +139 -12
- package/browser/chunks/index.browser-CUaPaKyS.js +10742 -0
- package/browser/env.d.ts +13 -0
- package/browser/env.d.ts.map +1 -0
- package/browser/index.d.ts +1 -1
- package/browser/index.d.ts.map +1 -1
- package/browser/index.js +1894 -12851
- package/browser/io/index.d.ts +0 -1
- package/browser/io/index.d.ts.map +1 -1
- package/browser/types.d.ts.map +1 -1
- package/browser/workers/WorkerSigningPool.d.ts +6 -0
- package/browser/workers/WorkerSigningPool.d.ts.map +1 -1
- package/browser/workers/WorkerSigningPool.node.d.ts +6 -0
- package/browser/workers/WorkerSigningPool.node.d.ts.map +1 -1
- package/browser/workers/WorkerSigningPool.sequential.d.ts +69 -0
- package/browser/workers/WorkerSigningPool.sequential.d.ts.map +1 -0
- package/browser/workers/WorkerSigningPool.worklet.d.ts +64 -0
- package/browser/workers/WorkerSigningPool.worklet.d.ts.map +1 -0
- package/browser/workers/index.browser.d.ts +16 -0
- package/browser/workers/index.browser.d.ts.map +1 -0
- package/browser/workers/index.d.ts +4 -17
- package/browser/workers/index.d.ts.map +1 -1
- package/browser/workers/index.js +21 -0
- package/browser/workers/index.node.d.ts +12 -19
- package/browser/workers/index.node.d.ts.map +1 -1
- package/browser/workers/index.react-native.d.ts +28 -0
- package/browser/workers/index.react-native.d.ts.map +1 -0
- package/browser/workers/index.shared.d.ts +15 -0
- package/browser/workers/index.shared.d.ts.map +1 -0
- package/browser/workers/psbt-parallel.d.ts +2 -3
- package/browser/workers/psbt-parallel.d.ts.map +1 -1
- package/browser/workers/types.d.ts +17 -0
- package/browser/workers/types.d.ts.map +1 -1
- package/build/env.d.ts +13 -0
- package/build/env.d.ts.map +1 -0
- package/build/env.js +198 -0
- package/build/env.js.map +1 -0
- package/build/index.d.ts +2 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -1
- package/build/index.js.map +1 -1
- package/build/io/index.d.ts +0 -1
- package/build/io/index.d.ts.map +1 -1
- package/build/io/index.js +0 -2
- package/build/io/index.js.map +1 -1
- package/build/tsconfig.build.tsbuildinfo +1 -1
- package/build/types.d.ts.map +1 -1
- package/build/types.js +2 -16
- package/build/types.js.map +1 -1
- package/build/workers/WorkerSigningPool.d.ts +6 -0
- package/build/workers/WorkerSigningPool.d.ts.map +1 -1
- package/build/workers/WorkerSigningPool.js +8 -0
- package/build/workers/WorkerSigningPool.js.map +1 -1
- package/build/workers/WorkerSigningPool.node.d.ts +6 -0
- package/build/workers/WorkerSigningPool.node.d.ts.map +1 -1
- package/build/workers/WorkerSigningPool.node.js +9 -2
- package/build/workers/WorkerSigningPool.node.js.map +1 -1
- package/build/workers/WorkerSigningPool.sequential.d.ts +78 -0
- package/build/workers/WorkerSigningPool.sequential.d.ts.map +1 -0
- package/build/workers/WorkerSigningPool.sequential.js +160 -0
- package/build/workers/WorkerSigningPool.sequential.js.map +1 -0
- package/build/workers/WorkerSigningPool.worklet.d.ts +79 -0
- package/build/workers/WorkerSigningPool.worklet.d.ts.map +1 -0
- package/build/workers/WorkerSigningPool.worklet.js +388 -0
- package/build/workers/WorkerSigningPool.worklet.js.map +1 -0
- package/build/workers/index.browser.d.ts +24 -0
- package/build/workers/index.browser.d.ts.map +1 -0
- package/build/workers/index.browser.js +30 -0
- package/build/workers/index.browser.js.map +1 -0
- package/build/workers/index.d.ts +6 -18
- package/build/workers/index.d.ts.map +1 -1
- package/build/workers/index.js +12 -11
- package/build/workers/index.js.map +1 -1
- package/build/workers/index.node.d.ts +17 -3
- package/build/workers/index.node.d.ts.map +1 -1
- package/build/workers/index.node.js +23 -4
- package/build/workers/index.node.js.map +1 -1
- package/build/workers/index.react-native.d.ts +28 -0
- package/build/workers/index.react-native.d.ts.map +1 -0
- package/build/workers/index.react-native.js +67 -0
- package/build/workers/index.react-native.js.map +1 -0
- package/build/workers/index.shared.d.ts +15 -0
- package/build/workers/index.shared.d.ts.map +1 -0
- package/build/workers/index.shared.js +20 -0
- package/build/workers/index.shared.js.map +1 -0
- package/build/workers/psbt-parallel.d.ts +2 -3
- package/build/workers/psbt-parallel.d.ts.map +1 -1
- package/build/workers/psbt-parallel.js +4 -4
- package/build/workers/psbt-parallel.js.map +1 -1
- package/build/workers/types.d.ts +17 -0
- package/build/workers/types.d.ts.map +1 -1
- package/package.json +19 -1
- package/src/env.ts +237 -0
- package/src/index.ts +1 -2
- package/src/io/index.ts +0 -3
- package/src/types.ts +4 -27
- package/src/workers/WorkerSigningPool.node.ts +10 -2
- package/src/workers/WorkerSigningPool.sequential.ts +190 -0
- package/src/workers/WorkerSigningPool.ts +9 -0
- package/src/workers/WorkerSigningPool.worklet.ts +519 -0
- package/src/workers/index.browser.ts +34 -0
- package/src/workers/index.node.ts +28 -5
- package/src/workers/index.react-native.ts +110 -0
- package/src/workers/index.shared.ts +58 -0
- package/src/workers/index.ts +15 -64
- package/src/workers/psbt-parallel.ts +8 -8
- package/src/workers/types.ts +21 -0
- package/test/env.spec.ts +418 -0
- package/test/workers-pool.spec.ts +43 -0
- package/test/workers-sequential.spec.ts +669 -0
- package/test/workers-worklet.spec.ts +500 -0
- package/vite.config.browser.ts +31 -6
- package/browser/io/MemoryPool.d.ts +0 -220
- package/browser/io/MemoryPool.d.ts.map +0 -1
- package/build/io/MemoryPool.d.ts +0 -220
- package/build/io/MemoryPool.d.ts.map +0 -1
- package/build/io/MemoryPool.js +0 -309
- package/build/io/MemoryPool.js.map +0 -1
- package/src/io/MemoryPool.ts +0 -343
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type {
|
|
3
|
+
ParallelSignerKeyPair,
|
|
4
|
+
SigningTask,
|
|
5
|
+
} from '../src/workers/types.js';
|
|
6
|
+
import { SignatureType } from '../src/workers/types.js';
|
|
7
|
+
import {
|
|
8
|
+
createNobleBackend,
|
|
9
|
+
createPrivateKey,
|
|
10
|
+
createMessageHash,
|
|
11
|
+
createPublicKey,
|
|
12
|
+
createXOnlyPublicKey,
|
|
13
|
+
createSignature,
|
|
14
|
+
createSchnorrSignature,
|
|
15
|
+
} from '@btc-vision/ecpair';
|
|
16
|
+
|
|
17
|
+
const nobleBackend = createNobleBackend();
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Returns a valid secp256k1 private key (scalar = 1).
|
|
21
|
+
*/
|
|
22
|
+
function makeTestPrivateKey(): Uint8Array {
|
|
23
|
+
const key = new Uint8Array(32);
|
|
24
|
+
key[31] = 0x01;
|
|
25
|
+
return key;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Derives the compressed public key for the test private key. */
|
|
29
|
+
function getTestPublicKey(): Uint8Array {
|
|
30
|
+
return nobleBackend.pointFromScalar(createPrivateKey(makeTestPrivateKey()))!;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// --- Mock react-native-worklets ---
|
|
34
|
+
//
|
|
35
|
+
// In Node.js tests, we can't use real worklet runtimes.
|
|
36
|
+
// Instead, we simulate them: `createWorkletRuntime` returns a named object,
|
|
37
|
+
// and `runOnRuntime` simply calls the function in the current JS context.
|
|
38
|
+
//
|
|
39
|
+
// The ECC bundle eval during initialize() sets `globalThis.nobleBundle`,
|
|
40
|
+
// which subsequent signing calls then read. We let it persist on globalThis
|
|
41
|
+
// for the duration of each test.
|
|
42
|
+
|
|
43
|
+
interface MockRuntime {
|
|
44
|
+
name: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function createMockWorkletRuntime(name: string): MockRuntime {
|
|
48
|
+
return { name };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function mockRunOnRuntime<T>(
|
|
52
|
+
_runtime: MockRuntime,
|
|
53
|
+
fn: () => T,
|
|
54
|
+
): Promise<T> {
|
|
55
|
+
// Just execute the function in current context.
|
|
56
|
+
// The ECC bundle eval will set globalThis.nobleBundle which persists.
|
|
57
|
+
return fn();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
vi.mock('react-native-worklets', () => ({
|
|
61
|
+
createWorkletRuntime: createMockWorkletRuntime,
|
|
62
|
+
runOnRuntime: mockRunOnRuntime,
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
describe('WorkletSigningPool', () => {
|
|
66
|
+
let WorkletSigningPool: typeof import('../src/workers/WorkerSigningPool.worklet.js').WorkletSigningPool;
|
|
67
|
+
|
|
68
|
+
beforeEach(async () => {
|
|
69
|
+
const mod = await import('../src/workers/WorkerSigningPool.worklet.js');
|
|
70
|
+
WorkletSigningPool = mod.WorkletSigningPool;
|
|
71
|
+
WorkletSigningPool.resetInstance();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
afterEach(() => {
|
|
75
|
+
WorkletSigningPool.resetInstance();
|
|
76
|
+
// Clean up nobleBundle from globalThis
|
|
77
|
+
delete (globalThis as Record<string, unknown>)['nobleBundle'];
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('Singleton Pattern', () => {
|
|
81
|
+
it('should return same instance on multiple calls', () => {
|
|
82
|
+
const pool1 = WorkletSigningPool.getInstance();
|
|
83
|
+
const pool2 = WorkletSigningPool.getInstance();
|
|
84
|
+
expect(pool1).toBe(pool2);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should create new instance after reset', () => {
|
|
88
|
+
const pool1 = WorkletSigningPool.getInstance();
|
|
89
|
+
WorkletSigningPool.resetInstance();
|
|
90
|
+
const pool2 = WorkletSigningPool.getInstance();
|
|
91
|
+
expect(pool1).not.toBe(pool2);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should accept config on first call', () => {
|
|
95
|
+
const pool = WorkletSigningPool.getInstance({ workerCount: 2 });
|
|
96
|
+
expect(pool).toBeDefined();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('Pool Properties (before init)', () => {
|
|
101
|
+
it('should report workerCount as 0 before init', () => {
|
|
102
|
+
const pool = WorkletSigningPool.getInstance();
|
|
103
|
+
expect(pool.workerCount).toBe(0);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should report busyWorkerCount as 0', () => {
|
|
107
|
+
const pool = WorkletSigningPool.getInstance();
|
|
108
|
+
expect(pool.busyWorkerCount).toBe(0);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should report isPreservingWorkers as false by default', () => {
|
|
112
|
+
const pool = WorkletSigningPool.getInstance();
|
|
113
|
+
expect(pool.isPreservingWorkers).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('Lifecycle', () => {
|
|
118
|
+
it('should initialize and create runtimes', async () => {
|
|
119
|
+
const pool = WorkletSigningPool.getInstance({ workerCount: 2 });
|
|
120
|
+
await pool.initialize();
|
|
121
|
+
expect(pool.workerCount).toBe(2);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should be idempotent on double initialize', async () => {
|
|
125
|
+
const pool = WorkletSigningPool.getInstance({ workerCount: 2 });
|
|
126
|
+
await pool.initialize();
|
|
127
|
+
await pool.initialize();
|
|
128
|
+
expect(pool.workerCount).toBe(2);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should shutdown and clear runtimes', async () => {
|
|
132
|
+
const pool = WorkletSigningPool.getInstance({ workerCount: 2 });
|
|
133
|
+
await pool.initialize();
|
|
134
|
+
expect(pool.workerCount).toBe(2);
|
|
135
|
+
await pool.shutdown();
|
|
136
|
+
expect(pool.workerCount).toBe(0);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should support preserveWorkers / releaseWorkers', () => {
|
|
140
|
+
const pool = WorkletSigningPool.getInstance();
|
|
141
|
+
expect(pool.isPreservingWorkers).toBe(false);
|
|
142
|
+
pool.preserveWorkers();
|
|
143
|
+
expect(pool.isPreservingWorkers).toBe(true);
|
|
144
|
+
pool.releaseWorkers();
|
|
145
|
+
expect(pool.isPreservingWorkers).toBe(false);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should have Symbol.asyncDispose', () => {
|
|
149
|
+
const pool = WorkletSigningPool.getInstance();
|
|
150
|
+
expect(typeof pool[Symbol.asyncDispose]).toBe('function');
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('signBatch', () => {
|
|
155
|
+
it('should return success for empty task array', async () => {
|
|
156
|
+
const pool = WorkletSigningPool.getInstance({ workerCount: 2 });
|
|
157
|
+
pool.preserveWorkers();
|
|
158
|
+
|
|
159
|
+
const keyPair: ParallelSignerKeyPair = {
|
|
160
|
+
publicKey: getTestPublicKey(),
|
|
161
|
+
getPrivateKey: makeTestPrivateKey,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const result = await pool.signBatch([], keyPair);
|
|
165
|
+
|
|
166
|
+
expect(result.success).toBe(true);
|
|
167
|
+
expect(result.signatures.size).toBe(0);
|
|
168
|
+
expect(result.errors.size).toBe(0);
|
|
169
|
+
expect(result.durationMs).toBeGreaterThanOrEqual(0);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should produce valid ECDSA signatures', async () => {
|
|
173
|
+
const pool = WorkletSigningPool.getInstance({ workerCount: 1 });
|
|
174
|
+
pool.preserveWorkers();
|
|
175
|
+
await pool.initialize();
|
|
176
|
+
|
|
177
|
+
const hash = new Uint8Array(32).fill(0x11);
|
|
178
|
+
const pubKey = getTestPublicKey();
|
|
179
|
+
|
|
180
|
+
const keyPair: ParallelSignerKeyPair = {
|
|
181
|
+
publicKey: pubKey,
|
|
182
|
+
getPrivateKey: makeTestPrivateKey,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const tasks: SigningTask[] = [
|
|
186
|
+
{
|
|
187
|
+
taskId: 'ecdsa-0',
|
|
188
|
+
inputIndex: 0,
|
|
189
|
+
hash,
|
|
190
|
+
signatureType: SignatureType.ECDSA,
|
|
191
|
+
sighashType: 0x01,
|
|
192
|
+
},
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
const result = await pool.signBatch(tasks, keyPair);
|
|
196
|
+
|
|
197
|
+
expect(result.success).toBe(true);
|
|
198
|
+
expect(result.signatures.size).toBe(1);
|
|
199
|
+
|
|
200
|
+
const sig = result.signatures.get(0)!;
|
|
201
|
+
expect(sig.type).toBe('result');
|
|
202
|
+
expect(sig.taskId).toBe('ecdsa-0');
|
|
203
|
+
expect(sig.inputIndex).toBe(0);
|
|
204
|
+
expect(sig.signatureType).toBe(SignatureType.ECDSA);
|
|
205
|
+
expect(sig.signature).toHaveLength(64);
|
|
206
|
+
|
|
207
|
+
// Verify signature
|
|
208
|
+
const valid = nobleBackend.verify(
|
|
209
|
+
createMessageHash(hash),
|
|
210
|
+
createPublicKey(pubKey),
|
|
211
|
+
createSignature(sig.signature),
|
|
212
|
+
);
|
|
213
|
+
expect(valid).toBe(true);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should produce valid Schnorr signatures', async () => {
|
|
217
|
+
const pool = WorkletSigningPool.getInstance({ workerCount: 1 });
|
|
218
|
+
pool.preserveWorkers();
|
|
219
|
+
await pool.initialize();
|
|
220
|
+
|
|
221
|
+
const hash = new Uint8Array(32).fill(0x22);
|
|
222
|
+
const pubKey = getTestPublicKey();
|
|
223
|
+
|
|
224
|
+
const keyPair: ParallelSignerKeyPair = {
|
|
225
|
+
publicKey: pubKey,
|
|
226
|
+
getPrivateKey: makeTestPrivateKey,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const tasks: SigningTask[] = [
|
|
230
|
+
{
|
|
231
|
+
taskId: 'schnorr-0',
|
|
232
|
+
inputIndex: 0,
|
|
233
|
+
hash,
|
|
234
|
+
signatureType: SignatureType.Schnorr,
|
|
235
|
+
sighashType: 0x00,
|
|
236
|
+
},
|
|
237
|
+
];
|
|
238
|
+
|
|
239
|
+
const result = await pool.signBatch(tasks, keyPair);
|
|
240
|
+
|
|
241
|
+
expect(result.success).toBe(true);
|
|
242
|
+
expect(result.signatures.size).toBe(1);
|
|
243
|
+
|
|
244
|
+
const sig = result.signatures.get(0)!;
|
|
245
|
+
expect(sig.signatureType).toBe(SignatureType.Schnorr);
|
|
246
|
+
expect(sig.signature).toHaveLength(64);
|
|
247
|
+
|
|
248
|
+
// Verify Schnorr signature
|
|
249
|
+
const valid = nobleBackend.verifySchnorr!(
|
|
250
|
+
createMessageHash(hash),
|
|
251
|
+
createXOnlyPublicKey(pubKey.subarray(1, 33)),
|
|
252
|
+
createSchnorrSignature(sig.signature),
|
|
253
|
+
);
|
|
254
|
+
expect(valid).toBe(true);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should handle mixed ECDSA and Schnorr tasks', async () => {
|
|
258
|
+
const pool = WorkletSigningPool.getInstance({ workerCount: 2 });
|
|
259
|
+
pool.preserveWorkers();
|
|
260
|
+
await pool.initialize();
|
|
261
|
+
|
|
262
|
+
const keyPair: ParallelSignerKeyPair = {
|
|
263
|
+
publicKey: getTestPublicKey(),
|
|
264
|
+
getPrivateKey: makeTestPrivateKey,
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const tasks: SigningTask[] = [
|
|
268
|
+
{
|
|
269
|
+
taskId: 'ecdsa-0',
|
|
270
|
+
inputIndex: 0,
|
|
271
|
+
hash: new Uint8Array(32).fill(0x11),
|
|
272
|
+
signatureType: SignatureType.ECDSA,
|
|
273
|
+
sighashType: 0x01,
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
taskId: 'schnorr-1',
|
|
277
|
+
inputIndex: 1,
|
|
278
|
+
hash: new Uint8Array(32).fill(0x22),
|
|
279
|
+
signatureType: SignatureType.Schnorr,
|
|
280
|
+
sighashType: 0x00,
|
|
281
|
+
},
|
|
282
|
+
];
|
|
283
|
+
|
|
284
|
+
const result = await pool.signBatch(tasks, keyPair);
|
|
285
|
+
|
|
286
|
+
expect(result.success).toBe(true);
|
|
287
|
+
expect(result.signatures.size).toBe(2);
|
|
288
|
+
expect(result.signatures.get(0)?.signatureType).toBe(SignatureType.ECDSA);
|
|
289
|
+
expect(result.signatures.get(1)?.signatureType).toBe(SignatureType.Schnorr);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('should include leafHash for Taproot script-path', async () => {
|
|
293
|
+
const pool = WorkletSigningPool.getInstance({ workerCount: 1 });
|
|
294
|
+
pool.preserveWorkers();
|
|
295
|
+
await pool.initialize();
|
|
296
|
+
|
|
297
|
+
const leafHash = new Uint8Array(32).fill(0xcd);
|
|
298
|
+
|
|
299
|
+
const keyPair: ParallelSignerKeyPair = {
|
|
300
|
+
publicKey: getTestPublicKey(),
|
|
301
|
+
getPrivateKey: makeTestPrivateKey,
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const tasks: SigningTask[] = [
|
|
305
|
+
{
|
|
306
|
+
taskId: 'script-path-0',
|
|
307
|
+
inputIndex: 0,
|
|
308
|
+
hash: new Uint8Array(32).fill(0x33),
|
|
309
|
+
signatureType: SignatureType.Schnorr,
|
|
310
|
+
sighashType: 0x00,
|
|
311
|
+
leafHash,
|
|
312
|
+
},
|
|
313
|
+
];
|
|
314
|
+
|
|
315
|
+
const result = await pool.signBatch(tasks, keyPair);
|
|
316
|
+
|
|
317
|
+
expect(result.success).toBe(true);
|
|
318
|
+
expect(result.signatures.get(0)?.leafHash).toBeDefined();
|
|
319
|
+
expect(result.signatures.get(0)?.leafHash).toEqual(leafHash);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('should handle multiple inputs', async () => {
|
|
323
|
+
const pool = WorkletSigningPool.getInstance({ workerCount: 2 });
|
|
324
|
+
pool.preserveWorkers();
|
|
325
|
+
await pool.initialize();
|
|
326
|
+
|
|
327
|
+
const keyPair: ParallelSignerKeyPair = {
|
|
328
|
+
publicKey: getTestPublicKey(),
|
|
329
|
+
getPrivateKey: makeTestPrivateKey,
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const tasks: SigningTask[] = [];
|
|
333
|
+
for (let i = 0; i < 10; i++) {
|
|
334
|
+
tasks.push({
|
|
335
|
+
taskId: `task-${i}`,
|
|
336
|
+
inputIndex: i,
|
|
337
|
+
hash: new Uint8Array(32).fill(i + 1),
|
|
338
|
+
signatureType: i % 2 === 0 ? SignatureType.ECDSA : SignatureType.Schnorr,
|
|
339
|
+
sighashType: i % 2 === 0 ? 0x01 : 0x00,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const result = await pool.signBatch(tasks, keyPair);
|
|
344
|
+
|
|
345
|
+
expect(result.success).toBe(true);
|
|
346
|
+
expect(result.signatures.size).toBe(10);
|
|
347
|
+
expect(result.errors.size).toBe(0);
|
|
348
|
+
|
|
349
|
+
for (let i = 0; i < 10; i++) {
|
|
350
|
+
expect(result.signatures.has(i)).toBe(true);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
describe('Key Security', () => {
|
|
356
|
+
it('should zero private key in main thread after signing', async () => {
|
|
357
|
+
const pool = WorkletSigningPool.getInstance({ workerCount: 1 });
|
|
358
|
+
pool.preserveWorkers();
|
|
359
|
+
await pool.initialize();
|
|
360
|
+
|
|
361
|
+
let capturedKey: Uint8Array | null = null;
|
|
362
|
+
const keyPair: ParallelSignerKeyPair = {
|
|
363
|
+
publicKey: getTestPublicKey(),
|
|
364
|
+
getPrivateKey: () => {
|
|
365
|
+
capturedKey = makeTestPrivateKey();
|
|
366
|
+
return capturedKey;
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const tasks: SigningTask[] = [
|
|
371
|
+
{
|
|
372
|
+
taskId: 'task-0',
|
|
373
|
+
inputIndex: 0,
|
|
374
|
+
hash: new Uint8Array(32).fill(0x11),
|
|
375
|
+
signatureType: SignatureType.ECDSA,
|
|
376
|
+
sighashType: 0x01,
|
|
377
|
+
},
|
|
378
|
+
];
|
|
379
|
+
|
|
380
|
+
await pool.signBatch(tasks, keyPair);
|
|
381
|
+
|
|
382
|
+
expect(capturedKey).not.toBeNull();
|
|
383
|
+
expect(capturedKey!.every((b: number) => b === 0)).toBe(true);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it('should zero private key even when signing fails', async () => {
|
|
387
|
+
const pool = WorkletSigningPool.getInstance({ workerCount: 1 });
|
|
388
|
+
pool.preserveWorkers();
|
|
389
|
+
await pool.initialize();
|
|
390
|
+
|
|
391
|
+
let capturedKey: Uint8Array | null = null;
|
|
392
|
+
const keyPair: ParallelSignerKeyPair = {
|
|
393
|
+
publicKey: new Uint8Array(33).fill(0x02),
|
|
394
|
+
getPrivateKey: () => {
|
|
395
|
+
// Zero key is invalid for secp256k1
|
|
396
|
+
capturedKey = new Uint8Array(32);
|
|
397
|
+
return capturedKey;
|
|
398
|
+
},
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const tasks: SigningTask[] = [
|
|
402
|
+
{
|
|
403
|
+
taskId: 'task-0',
|
|
404
|
+
inputIndex: 0,
|
|
405
|
+
hash: new Uint8Array(32).fill(0x11),
|
|
406
|
+
signatureType: SignatureType.ECDSA,
|
|
407
|
+
sighashType: 0x01,
|
|
408
|
+
},
|
|
409
|
+
];
|
|
410
|
+
|
|
411
|
+
await pool.signBatch(tasks, keyPair);
|
|
412
|
+
|
|
413
|
+
expect(capturedKey).not.toBeNull();
|
|
414
|
+
expect(capturedKey!.every((b: number) => b === 0)).toBe(true);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('should call getPrivateKey once per batch', async () => {
|
|
418
|
+
const pool = WorkletSigningPool.getInstance({ workerCount: 2 });
|
|
419
|
+
pool.preserveWorkers();
|
|
420
|
+
await pool.initialize();
|
|
421
|
+
|
|
422
|
+
let callCount = 0;
|
|
423
|
+
const keyPair: ParallelSignerKeyPair = {
|
|
424
|
+
publicKey: getTestPublicKey(),
|
|
425
|
+
getPrivateKey: () => {
|
|
426
|
+
callCount++;
|
|
427
|
+
return makeTestPrivateKey();
|
|
428
|
+
},
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
const tasks: SigningTask[] = [
|
|
432
|
+
{
|
|
433
|
+
taskId: 'task-0',
|
|
434
|
+
inputIndex: 0,
|
|
435
|
+
hash: new Uint8Array(32).fill(0x11),
|
|
436
|
+
signatureType: SignatureType.ECDSA,
|
|
437
|
+
sighashType: 0x01,
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
taskId: 'task-1',
|
|
441
|
+
inputIndex: 1,
|
|
442
|
+
hash: new Uint8Array(32).fill(0x22),
|
|
443
|
+
signatureType: SignatureType.Schnorr,
|
|
444
|
+
sighashType: 0x00,
|
|
445
|
+
},
|
|
446
|
+
];
|
|
447
|
+
|
|
448
|
+
await pool.signBatch(tasks, keyPair);
|
|
449
|
+
|
|
450
|
+
expect(callCount).toBe(1);
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
describe('Symbol.asyncDispose', () => {
|
|
455
|
+
it('should be safe to call dispose multiple times', async () => {
|
|
456
|
+
const pool = WorkletSigningPool.getInstance({ workerCount: 1 });
|
|
457
|
+
await pool.initialize();
|
|
458
|
+
await pool[Symbol.asyncDispose]();
|
|
459
|
+
await pool[Symbol.asyncDispose]();
|
|
460
|
+
// Should not throw
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
describe('React Native index worklet exports', () => {
|
|
466
|
+
it('should export WorkletSigningPool', async () => {
|
|
467
|
+
const mod = await import('../src/workers/index.react-native.js');
|
|
468
|
+
expect(mod.WorkletSigningPool).toBeDefined();
|
|
469
|
+
expect(typeof mod.WorkletSigningPool.getInstance).toBe('function');
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('should still export SequentialSigningPool', async () => {
|
|
473
|
+
const mod = await import('../src/workers/index.react-native.js');
|
|
474
|
+
expect(mod.SequentialSigningPool).toBeDefined();
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('should still export WorkerSigningPool as SequentialSigningPool alias', async () => {
|
|
478
|
+
const mod = await import('../src/workers/index.react-native.js');
|
|
479
|
+
expect(mod.WorkerSigningPool).toBe(mod.SequentialSigningPool);
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
describe('createSigningPool fallback', () => {
|
|
484
|
+
it('should fall back to SequentialSigningPool when worklets import fails', async () => {
|
|
485
|
+
// We can't easily un-mock react-native-worklets within the same test file
|
|
486
|
+
// since vi.mock is hoisted. Instead, test that createSigningPool catches
|
|
487
|
+
// WorkletSigningPool.initialize() failures by verifying the fallback path
|
|
488
|
+
// exists in the module and that SequentialSigningPool is functional.
|
|
489
|
+
const { SequentialSigningPool } = await import('../src/workers/WorkerSigningPool.sequential.js');
|
|
490
|
+
SequentialSigningPool.resetInstance();
|
|
491
|
+
|
|
492
|
+
const pool = SequentialSigningPool.getInstance();
|
|
493
|
+
await pool.initialize();
|
|
494
|
+
|
|
495
|
+
expect(pool.workerCount).toBe(0);
|
|
496
|
+
expect(pool.isPreservingWorkers).toBe(false);
|
|
497
|
+
|
|
498
|
+
SequentialSigningPool.resetInstance();
|
|
499
|
+
});
|
|
500
|
+
});
|
package/vite.config.browser.ts
CHANGED
|
@@ -1,8 +1,32 @@
|
|
|
1
1
|
import { resolve } from 'path';
|
|
2
|
-
import { defineConfig } from 'vite';
|
|
2
|
+
import { defineConfig, type Plugin } from 'vite';
|
|
3
3
|
import { nodePolyfills } from 'vite-plugin-node-polyfills';
|
|
4
4
|
import dts from 'vite-plugin-dts';
|
|
5
5
|
|
|
6
|
+
const BROWSER_WORKERS_INDEX = resolve(__dirname, 'src/workers/index.browser.ts');
|
|
7
|
+
|
|
8
|
+
// Redirect all imports of workers/index to the browser-specific entry
|
|
9
|
+
// which never references Node.js modules (worker_threads, os, etc.)
|
|
10
|
+
function browserWorkersRedirect(): Plugin {
|
|
11
|
+
const workersIndex = resolve(__dirname, 'src/workers/index.ts');
|
|
12
|
+
return {
|
|
13
|
+
name: 'browser-workers-redirect',
|
|
14
|
+
enforce: 'pre',
|
|
15
|
+
resolveId(source, importer) {
|
|
16
|
+
if (!importer) return null;
|
|
17
|
+
// Match resolved path to workers/index.ts (from relative ./workers/index.js imports)
|
|
18
|
+
if (source === './workers/index.js' || source === './workers/index.ts') {
|
|
19
|
+
const resolvedDir = resolve(importer, '..');
|
|
20
|
+
const resolved = resolve(resolvedDir, 'workers/index.ts');
|
|
21
|
+
if (resolved === workersIndex) {
|
|
22
|
+
return BROWSER_WORKERS_INDEX;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
6
30
|
export default defineConfig({
|
|
7
31
|
build: {
|
|
8
32
|
outDir: 'browser',
|
|
@@ -10,16 +34,16 @@ export default defineConfig({
|
|
|
10
34
|
target: 'esnext',
|
|
11
35
|
minify: 'esbuild',
|
|
12
36
|
lib: {
|
|
13
|
-
entry:
|
|
37
|
+
entry: {
|
|
38
|
+
index: resolve(__dirname, 'src/index.ts'),
|
|
39
|
+
'workers/index': BROWSER_WORKERS_INDEX,
|
|
40
|
+
},
|
|
14
41
|
formats: ['es'],
|
|
15
|
-
fileName: () =>
|
|
42
|
+
fileName: (_format, entryName) => `${entryName}.js`,
|
|
16
43
|
},
|
|
17
44
|
rollupOptions: {
|
|
18
|
-
external: [/WorkerSigningPool\.node/],
|
|
19
45
|
output: {
|
|
20
46
|
chunkFileNames: 'chunks/[name]-[hash].js',
|
|
21
|
-
// Tree-shaking enabled - no manual chunks needed
|
|
22
|
-
// This allows bundlers to only include what's actually used
|
|
23
47
|
},
|
|
24
48
|
},
|
|
25
49
|
},
|
|
@@ -36,6 +60,7 @@ export default defineConfig({
|
|
|
36
60
|
global: 'globalThis',
|
|
37
61
|
},
|
|
38
62
|
plugins: [
|
|
63
|
+
browserWorkersRedirect(),
|
|
39
64
|
nodePolyfills({
|
|
40
65
|
globals: {
|
|
41
66
|
Buffer: true,
|