@btc-vision/bitcoin 7.0.0-beta.0 → 7.0.0-beta.1

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.
Files changed (116) hide show
  1. package/README.md +112 -13
  2. package/benchmark-compare/BENCHMARK.md +74 -59
  3. package/benchmark-compare/compare.bench.ts +249 -96
  4. package/benchmark-compare/harness.ts +23 -25
  5. package/benchmark-compare/package.json +1 -0
  6. package/browser/address.d.ts +4 -4
  7. package/browser/address.d.ts.map +1 -1
  8. package/browser/chunks/{psbt-parallel-B-dfm5GZ.js → psbt-parallel-jZ6QcCnM.js} +3128 -2731
  9. package/browser/index.d.ts +1 -1
  10. package/browser/index.d.ts.map +1 -1
  11. package/browser/index.js +603 -585
  12. package/browser/io/base58check.d.ts +1 -25
  13. package/browser/io/base58check.d.ts.map +1 -1
  14. package/browser/io/base64.d.ts.map +1 -1
  15. package/browser/networks.d.ts +1 -0
  16. package/browser/networks.d.ts.map +1 -1
  17. package/browser/payments/bip341.d.ts +17 -0
  18. package/browser/payments/bip341.d.ts.map +1 -1
  19. package/browser/payments/index.d.ts +3 -2
  20. package/browser/payments/index.d.ts.map +1 -1
  21. package/browser/payments/p2mr.d.ts +169 -0
  22. package/browser/payments/p2mr.d.ts.map +1 -0
  23. package/browser/payments/types.d.ts +11 -1
  24. package/browser/payments/types.d.ts.map +1 -1
  25. package/browser/psbt/bip371.d.ts +30 -0
  26. package/browser/psbt/bip371.d.ts.map +1 -1
  27. package/browser/psbt/psbtutils.d.ts +1 -0
  28. package/browser/psbt/psbtutils.d.ts.map +1 -1
  29. package/browser/psbt.d.ts.map +1 -1
  30. package/browser/workers/index.js +9 -9
  31. package/build/address.d.ts +4 -4
  32. package/build/address.d.ts.map +1 -1
  33. package/build/address.js +11 -1
  34. package/build/address.js.map +1 -1
  35. package/build/index.d.ts +1 -1
  36. package/build/index.d.ts.map +1 -1
  37. package/build/index.js.map +1 -1
  38. package/build/io/base58check.d.ts +1 -25
  39. package/build/io/base58check.d.ts.map +1 -1
  40. package/build/io/base58check.js +1 -31
  41. package/build/io/base58check.js.map +1 -1
  42. package/build/io/base64.d.ts.map +1 -1
  43. package/build/io/base64.js +3 -0
  44. package/build/io/base64.js.map +1 -1
  45. package/build/networks.d.ts +1 -0
  46. package/build/networks.d.ts.map +1 -1
  47. package/build/networks.js +12 -0
  48. package/build/networks.js.map +1 -1
  49. package/build/payments/bip341.d.ts +17 -0
  50. package/build/payments/bip341.d.ts.map +1 -1
  51. package/build/payments/bip341.js +32 -1
  52. package/build/payments/bip341.js.map +1 -1
  53. package/build/payments/index.d.ts +3 -2
  54. package/build/payments/index.d.ts.map +1 -1
  55. package/build/payments/index.js +2 -1
  56. package/build/payments/index.js.map +1 -1
  57. package/build/payments/p2mr.d.ts +178 -0
  58. package/build/payments/p2mr.d.ts.map +1 -0
  59. package/build/payments/p2mr.js +555 -0
  60. package/build/payments/p2mr.js.map +1 -0
  61. package/build/payments/types.d.ts +11 -1
  62. package/build/payments/types.d.ts.map +1 -1
  63. package/build/payments/types.js +1 -0
  64. package/build/payments/types.js.map +1 -1
  65. package/build/psbt/bip371.d.ts +30 -0
  66. package/build/psbt/bip371.d.ts.map +1 -1
  67. package/build/psbt/bip371.js +80 -15
  68. package/build/psbt/bip371.js.map +1 -1
  69. package/build/psbt/psbtutils.d.ts +1 -0
  70. package/build/psbt/psbtutils.d.ts.map +1 -1
  71. package/build/psbt/psbtutils.js +2 -0
  72. package/build/psbt/psbtutils.js.map +1 -1
  73. package/build/psbt.d.ts.map +1 -1
  74. package/build/psbt.js +3 -2
  75. package/build/psbt.js.map +1 -1
  76. package/build/pubkey.js +1 -1
  77. package/build/pubkey.js.map +1 -1
  78. package/build/tsconfig.build.tsbuildinfo +1 -1
  79. package/documentation/README.md +122 -0
  80. package/documentation/address.md +820 -0
  81. package/documentation/block.md +679 -0
  82. package/documentation/crypto.md +461 -0
  83. package/documentation/ecc.md +584 -0
  84. package/documentation/errors.md +656 -0
  85. package/documentation/io.md +942 -0
  86. package/documentation/networks.md +625 -0
  87. package/documentation/p2mr.md +380 -0
  88. package/documentation/payments.md +1485 -0
  89. package/documentation/psbt.md +1400 -0
  90. package/documentation/script.md +730 -0
  91. package/documentation/taproot.md +670 -0
  92. package/documentation/transaction.md +943 -0
  93. package/documentation/types.md +587 -0
  94. package/documentation/workers.md +1007 -0
  95. package/eslint.config.js +3 -0
  96. package/package.json +17 -14
  97. package/src/address.ts +22 -10
  98. package/src/index.ts +1 -0
  99. package/src/io/base58check.ts +1 -35
  100. package/src/io/base64.ts +5 -0
  101. package/src/networks.ts +13 -0
  102. package/src/payments/bip341.ts +36 -1
  103. package/src/payments/index.ts +4 -0
  104. package/src/payments/p2mr.ts +660 -0
  105. package/src/payments/types.ts +12 -0
  106. package/src/psbt/bip371.ts +84 -13
  107. package/src/psbt/psbtutils.ts +2 -0
  108. package/src/psbt.ts +4 -2
  109. package/src/pubkey.ts +1 -1
  110. package/test/bitcoin.core.spec.ts +1 -1
  111. package/test/fixtures/p2mr.json +270 -0
  112. package/test/integration/taproot.spec.ts +7 -3
  113. package/test/opnetTestnet.spec.ts +302 -0
  114. package/test/payments.spec.ts +3 -1
  115. package/test/psbt.spec.ts +297 -2
  116. package/test/tsconfig.json +2 -2
@@ -0,0 +1,1007 @@
1
+ # Workers - Parallel Signing System
2
+
3
+ The workers module provides secure parallel signature computation for PSBT signing using platform-native threading. Private keys are isolated per-worker and zeroed immediately after signing. The system supports Node.js (`worker_threads`), browsers (Web Workers), React Native (parallel via `react-native-worklets` with sequential fallback), and automatic runtime detection.
4
+
5
+ ## Overview
6
+
7
+ | Property | Value |
8
+ |----------|-------|
9
+ | Import path | `@btc-vision/bitcoin/workers` |
10
+ | Node.js backend | `worker_threads` (`NodeWorkerSigningPool`) |
11
+ | Browser backend | Web Workers via Blob URL (`WorkerSigningPool`) |
12
+ | React Native backend | Worklet runtimes (`WorkletSigningPool`) with sequential main-thread fallback (`SequentialSigningPool`) |
13
+ | Supported signatures | ECDSA (secp256k1), Schnorr (BIP340) |
14
+ | Singleton pattern | Yes (per pool class, via `getInstance()`) |
15
+ | Disposable support | `Symbol.dispose` and `Symbol.asyncDispose` |
16
+
17
+ ### Key Security Properties
18
+
19
+ | Guarantee | Mechanism |
20
+ |-----------|-----------|
21
+ | Key isolation | Keys are cloned via `postMessage`, never shared via `SharedArrayBuffer` |
22
+ | Key zeroing (worker) | `secureZero()` double-writes zeros immediately after signing |
23
+ | Key zeroing (main thread) | `privateKey.fill(0)` in `finally` block after batch completes |
24
+ | Key hold timeout | Worker is terminated if signing exceeds `maxKeyHoldTimeMs` (default 5s) |
25
+ | No key persistence | Keys are not stored; obtained per-batch via `getPrivateKey()` |
26
+
27
+ ---
28
+
29
+ ## SignatureType Enum
30
+
31
+ Discriminates between the two signature algorithms supported by the signing workers.
32
+
33
+ ```typescript
34
+ const SignatureType = {
35
+ /** ECDSA signature (secp256k1) */
36
+ ECDSA: 0,
37
+ /** Schnorr signature (BIP340) */
38
+ Schnorr: 1,
39
+ } as const;
40
+
41
+ type SignatureType = (typeof SignatureType)[keyof typeof SignatureType];
42
+ ```
43
+
44
+ | Value | Numeric | Use case |
45
+ |-------|---------|----------|
46
+ | `SignatureType.ECDSA` | `0` | Legacy, SegWit (P2PKH, P2SH, P2WPKH, P2WSH) |
47
+ | `SignatureType.Schnorr` | `1` | Taproot key-path and script-path (P2TR) |
48
+
49
+ ---
50
+
51
+ ## SigningTask Interface
52
+
53
+ Represents a single signing operation to be dispatched to a worker.
54
+
55
+ ```typescript
56
+ interface SigningTask {
57
+ /** Unique task identifier */
58
+ readonly taskId: string;
59
+ /** PSBT input index this task corresponds to */
60
+ readonly inputIndex: number;
61
+ /** 32-byte hash to sign */
62
+ readonly hash: Uint8Array;
63
+ /** Signature algorithm to use */
64
+ readonly signatureType: SignatureType;
65
+ /** Grind for low R value (ECDSA only) */
66
+ readonly lowR?: boolean | undefined;
67
+ /** Sighash type for signature encoding */
68
+ readonly sighashType: number;
69
+ /** Leaf hash for Taproot script-path spending */
70
+ readonly leafHash?: Uint8Array | undefined;
71
+ }
72
+ ```
73
+
74
+ Tasks are created by `prepareSigningTasks()` when working with PSBTs, or constructed manually for lower-level usage with `signBatch()`.
75
+
76
+ ---
77
+
78
+ ## ParallelSignerKeyPair Interface
79
+
80
+ The key pair interface required by the signing pool. Exposes the public key and a method to retrieve the raw private key bytes.
81
+
82
+ ```typescript
83
+ interface ParallelSignerKeyPair {
84
+ /** Public key (compressed 33 bytes or uncompressed 65 bytes) */
85
+ readonly publicKey: Uint8Array;
86
+
87
+ /**
88
+ * Get private key bytes (32 bytes).
89
+ *
90
+ * SECURITY WARNING: This exposes the raw private key.
91
+ * Called only when dispatching to worker. Key is cloned
92
+ * to worker via postMessage.
93
+ */
94
+ getPrivateKey(): Uint8Array;
95
+
96
+ /** Optional: Sign ECDSA directly (fallback if workers unavailable) */
97
+ sign?(hash: Uint8Array, lowR?: boolean): Uint8Array;
98
+
99
+ /** Optional: Sign Schnorr directly (fallback if workers unavailable) */
100
+ signSchnorr?(hash: Uint8Array): Uint8Array;
101
+ }
102
+ ```
103
+
104
+ The `sign()` and `signSchnorr()` methods are optional fallback methods. When workers are available, the pool uses `getPrivateKey()` and delegates signing to the worker thread.
105
+
106
+ ---
107
+
108
+ ## ParallelSigningResult Interface
109
+
110
+ Returned by `signBatch()` and `signPsbtParallel()` after all tasks complete.
111
+
112
+ ```typescript
113
+ interface ParallelSigningResult {
114
+ /** Whether all signatures were created successfully */
115
+ readonly success: boolean;
116
+ /** Signatures indexed by input index */
117
+ readonly signatures: ReadonlyMap<number, SigningResultMessage>;
118
+ /** Errors indexed by input index */
119
+ readonly errors: ReadonlyMap<number, string>;
120
+ /** Total time taken in milliseconds */
121
+ readonly durationMs: number;
122
+ }
123
+ ```
124
+
125
+ The `success` field is `true` only when `errors.size === 0`. Each entry in `signatures` contains the full `SigningResultMessage` with the raw signature bytes, public key, signature type, and optional leaf hash.
126
+
127
+ ---
128
+
129
+ ## WorkerPoolConfig Interface
130
+
131
+ Configuration for any signing pool implementation.
132
+
133
+ ```typescript
134
+ interface WorkerPoolConfig {
135
+ /**
136
+ * Number of workers to create.
137
+ * Default: navigator.hardwareConcurrency (browser) or os.cpus().length (Node.js)
138
+ */
139
+ readonly workerCount?: number;
140
+
141
+ /**
142
+ * Timeout per signing operation in milliseconds.
143
+ * Worker is terminated if exceeded (key safety measure).
144
+ * Default: 30000 (30 seconds)
145
+ */
146
+ readonly taskTimeoutMs?: number;
147
+
148
+ /**
149
+ * Maximum time a worker can hold a private key in milliseconds.
150
+ * Acts as a safety net - worker is terminated if signing takes too long.
151
+ * Default: 5000 (5 seconds)
152
+ */
153
+ readonly maxKeyHoldTimeMs?: number;
154
+
155
+ /**
156
+ * Whether to verify signatures after signing.
157
+ * Adds overhead but ensures correctness.
158
+ * Default: true
159
+ */
160
+ readonly verifySignatures?: boolean;
161
+
162
+ /**
163
+ * Whether to preserve workers between signing batches.
164
+ * true = workers stay alive (faster for multiple batches)
165
+ * false = workers terminated after each batch (more secure)
166
+ * Default: false
167
+ */
168
+ readonly preserveWorkers?: boolean;
169
+ }
170
+ ```
171
+
172
+ | Option | Default | Description |
173
+ |--------|---------|-------------|
174
+ | `workerCount` | CPU core count | Number of worker threads to spawn |
175
+ | `taskTimeoutMs` | `30000` | Maximum time for any single signing operation |
176
+ | `maxKeyHoldTimeMs` | `5000` | Safety limit on how long a worker may hold a private key |
177
+ | `verifySignatures` | `true` | Post-signing signature verification |
178
+ | `preserveWorkers` | `false` | Keep workers alive between batches |
179
+
180
+ The Node.js pool extends this with `NodeWorkerPoolConfig`:
181
+
182
+ ```typescript
183
+ interface NodeWorkerPoolConfig extends WorkerPoolConfig {
184
+ /**
185
+ * ECC library type for signing.
186
+ * Default: NodeEccLibrary.TinySecp256k1
187
+ */
188
+ readonly eccLibrary?: NodeEccLibrary;
189
+ }
190
+ ```
191
+
192
+ | ECC Library | Value | Size | Notes |
193
+ |-------------|-------|------|-------|
194
+ | `NodeEccLibrary.Noble` | `'noble'` | ~12KB | Pure JS `@noble/secp256k1` |
195
+ | `NodeEccLibrary.TinySecp256k1` | `'tiny-secp256k1'` | ~1.2MB | WASM-based, faster for batch operations |
196
+
197
+ > **Note:** The `NodeEccLibrary` enum is **not** re-exported from any index entry point (`index.ts`, `index.node.ts`, `index.browser.ts`, or `index.react-native.ts`). When specifying the ECC library in a `NodeWorkerPoolConfig`, use the string literal values `'noble'` or `'tiny-secp256k1'` directly. If you need the enum itself, import it directly from the worker module:
198
+ >
199
+ > ```typescript
200
+ > import { NodeEccLibrary } from '@btc-vision/bitcoin/src/workers/WorkerSigningPool.node.js';
201
+ > ```
202
+
203
+ ---
204
+
205
+ ## WorkerSigningPool Class (Browser)
206
+
207
+ Manages a pool of Web Workers for parallel signature computation. Uses the singleton pattern.
208
+
209
+ ```typescript
210
+ class WorkerSigningPool {
211
+ // Singleton access
212
+ static getInstance(config?: WorkerPoolConfig): WorkerSigningPool;
213
+ static resetInstance(): void;
214
+
215
+ // Properties
216
+ get workerCount(): number;
217
+ get idleWorkerCount(): number;
218
+ get busyWorkerCount(): number;
219
+ get isPreservingWorkers(): boolean;
220
+
221
+ // Lifecycle
222
+ initialize(): Promise<void>;
223
+ shutdown(): Promise<void>;
224
+
225
+ // Worker preservation
226
+ preserveWorkers(): void;
227
+ releaseWorkers(): void;
228
+
229
+ // Signing
230
+ signBatch(
231
+ tasks: readonly SigningTask[],
232
+ keyPair: ParallelSignerKeyPair,
233
+ ): Promise<ParallelSigningResult>;
234
+
235
+ // Disposable protocol
236
+ [Symbol.dispose](): void;
237
+ [Symbol.asyncDispose](): Promise<void>;
238
+ }
239
+ ```
240
+
241
+ ### getInstance()
242
+
243
+ Returns the singleton pool instance. Configuration is only applied on first call.
244
+
245
+ ```typescript
246
+ const pool = WorkerSigningPool.getInstance({ workerCount: 4 });
247
+ ```
248
+
249
+ ### initialize()
250
+
251
+ Creates worker threads and waits for all to signal readiness. Called automatically on the first `signBatch()` if not called explicitly.
252
+
253
+ ```typescript
254
+ await pool.initialize();
255
+ ```
256
+
257
+ Internally, this creates a Blob URL from the generated worker code (which bundles the `@noble/secp256k1` ECC library inline), spawns the configured number of workers, and sends each an `init` message. Each worker responds with a `ready` message. Worker initialization times out after 10 seconds.
258
+
259
+ ### signBatch()
260
+
261
+ Signs an array of tasks in parallel by distributing them across available workers using round-robin assignment. Returns a `ParallelSigningResult`.
262
+
263
+ ```typescript
264
+ const result = await pool.signBatch(tasks, keyPair);
265
+
266
+ if (result.success) {
267
+ for (const [inputIndex, sig] of result.signatures) {
268
+ console.log(`Input ${inputIndex} signed in ${result.durationMs}ms`);
269
+ }
270
+ }
271
+ ```
272
+
273
+ Internally, `signBatch()`:
274
+ 1. Calls `keyPair.getPrivateKey()` once to obtain the raw key.
275
+ 2. Distributes tasks into sub-batches, one per worker (round-robin).
276
+ 3. Sends each sub-batch as a `BatchSigningMessage` to its assigned worker via `postMessage` (the private key is cloned, not shared).
277
+ 4. Awaits all `BatchSigningResultMessage` responses.
278
+ 5. Zeros the private key in the main thread via `privateKey.fill(0)` in a `finally` block.
279
+ 6. If `preserveWorkers` is `false`, terminates all idle workers after the batch completes.
280
+
281
+ ### shutdown()
282
+
283
+ Terminates all workers by sending `shutdown` messages and awaiting acknowledgments. Falls back to forced `terminate()` after 1 second. Revokes the Blob URL.
284
+
285
+ ```typescript
286
+ await pool.shutdown();
287
+ ```
288
+
289
+ ### preserveWorkers() / releaseWorkers()
290
+
291
+ Controls whether workers are kept alive between signing batches.
292
+
293
+ ```typescript
294
+ pool.preserveWorkers(); // Workers stay alive after signBatch()
295
+ pool.releaseWorkers(); // Workers terminated after each signBatch()
296
+ ```
297
+
298
+ Preserving workers is faster for applications that sign multiple transactions. Releasing workers is more secure since no threads persist with loaded ECC code.
299
+
300
+ ---
301
+
302
+ ## NodeWorkerSigningPool Class (Node.js)
303
+
304
+ Node.js-specific signing pool using `worker_threads`. Same public API as `WorkerSigningPool` but uses Node.js threading primitives.
305
+
306
+ ```typescript
307
+ class NodeWorkerSigningPool {
308
+ static getInstance(config?: NodeWorkerPoolConfig): NodeWorkerSigningPool;
309
+ static resetInstance(): void;
310
+
311
+ get workerCount(): number;
312
+ get idleWorkerCount(): number;
313
+ get busyWorkerCount(): number;
314
+ get isPreservingWorkers(): boolean;
315
+
316
+ initialize(): Promise<void>;
317
+ shutdown(): Promise<void>;
318
+ preserveWorkers(): void;
319
+ releaseWorkers(): void;
320
+
321
+ signBatch(
322
+ tasks: readonly SigningTask[],
323
+ keyPair: ParallelSignerKeyPair,
324
+ ): Promise<ParallelSigningResult>;
325
+ }
326
+ ```
327
+
328
+ Key differences from the browser pool:
329
+ - Uses `new Worker(script, { eval: true })` with inline script code instead of Blob URLs.
330
+ - Supports choosing between `@noble/secp256k1` (pure JS) and `tiny-secp256k1` (WASM) via `NodeWorkerPoolConfig.eccLibrary`.
331
+ - Uses zero-copy `ArrayBuffer` transfer for private keys and hashes via the `transferList` parameter.
332
+ - Default `workerCount` is `os.cpus().length` instead of `navigator.hardwareConcurrency`.
333
+ - Can only be created in the main thread (throws if `isMainThread` is `false`).
334
+
335
+ ```typescript
336
+ import { NodeWorkerSigningPool } from '@btc-vision/bitcoin/workers';
337
+
338
+ const pool = NodeWorkerSigningPool.getInstance({
339
+ workerCount: 4,
340
+ eccLibrary: 'tiny-secp256k1',
341
+ });
342
+
343
+ await pool.initialize();
344
+ pool.preserveWorkers();
345
+
346
+ const result = await pool.signBatch(tasks, keyPair);
347
+
348
+ await pool.shutdown();
349
+ ```
350
+
351
+ ---
352
+
353
+ ## WorkletSigningPool Class (React Native)
354
+
355
+ React Native signing pool using `react-native-worklets` (Software Mansion v0.7+) for true parallel signing across multiple worklet runtimes. Each runtime gets its own ECC module instance via eval of the bundled `@noble/secp256k1` IIFE string. Uses the singleton pattern.
356
+
357
+ ```typescript
358
+ class WorkletSigningPool {
359
+ // Singleton access
360
+ static getInstance(config?: WorkerPoolConfig): WorkletSigningPool;
361
+ static resetInstance(): void;
362
+
363
+ // Properties
364
+ get workerCount(): number; // Number of active runtimes
365
+ get idleWorkerCount(): number; // Non-tainted runtimes
366
+ get busyWorkerCount(): number; // Always 0 outside of signBatch
367
+ get isPreservingWorkers(): boolean;
368
+
369
+ // Lifecycle
370
+ initialize(): Promise<void>;
371
+ shutdown(): Promise<void>;
372
+
373
+ // Worker preservation
374
+ preserveWorkers(): void;
375
+ releaseWorkers(): void;
376
+
377
+ // Signing
378
+ signBatch(
379
+ tasks: readonly SigningTask[],
380
+ keyPair: ParallelSignerKeyPair,
381
+ ): Promise<ParallelSigningResult>;
382
+
383
+ // Disposable protocol
384
+ [Symbol.dispose](): void;
385
+ [Symbol.asyncDispose](): Promise<void>;
386
+ }
387
+ ```
388
+
389
+ ### Security Architecture
390
+
391
+ - Private keys are cloned per-runtime (structuredClone semantics).
392
+ - Keys are zeroed inside the worklet AND in the main thread `finally` block.
393
+ - Tainted runtimes (those that exceeded the key hold timeout) are replaced, not reused.
394
+
395
+ ### initialize()
396
+
397
+ Dynamically imports `react-native-worklets`, creates N runtimes (default 4), and injects the ECC bundle into each via `new Function()` eval. Also feature-detects whether `Uint8Array` survives the worklet boundary; if not, data is encoded as `number[]` for transfer.
398
+
399
+ ```typescript
400
+ const pool = WorkletSigningPool.getInstance({ workerCount: 4 });
401
+ await pool.initialize();
402
+ ```
403
+
404
+ Throws if `react-native-worklets` is not installed or ECC bundle injection fails.
405
+
406
+ ### signBatch()
407
+
408
+ Signs an array of tasks in parallel by distributing them round-robin across available worklet runtimes. Uses `runOnRuntime()` to dispatch signing closures to each runtime, with a timeout guard (`maxKeyHoldTimeMs`) that taints and replaces runtimes that exceed the limit.
409
+
410
+ ```typescript
411
+ const result = await pool.signBatch(tasks, keyPair);
412
+
413
+ if (result.success) {
414
+ for (const [inputIndex, sig] of result.signatures) {
415
+ console.log(`Input ${inputIndex} signed`);
416
+ }
417
+ }
418
+ ```
419
+
420
+ ### shutdown()
421
+
422
+ Clears all runtime references for garbage collection. Worklet runtimes do not have an explicit `destroy()` API, so shutdown releases references and resets internal state.
423
+
424
+ ```typescript
425
+ await pool.shutdown();
426
+ ```
427
+
428
+ ### Usage Example
429
+
430
+ ```typescript
431
+ import { WorkletSigningPool } from '@btc-vision/bitcoin/workers';
432
+
433
+ const pool = WorkletSigningPool.getInstance({ workerCount: 4 });
434
+ await pool.initialize();
435
+ pool.preserveWorkers();
436
+
437
+ const result = await pool.signBatch(tasks, keyPair);
438
+ await pool.shutdown();
439
+ ```
440
+
441
+ Key differences from the browser `WorkerSigningPool`:
442
+ - Uses `react-native-worklets` runtimes instead of Web Workers.
443
+ - Uses `runOnRuntime()` which returns a Promise directly -- no `postMessage` protocol.
444
+ - Data may be encoded as `number[]` instead of `Uint8Array` if the worklet boundary does not support typed arrays.
445
+ - Default `workerCount` is 4 (not tied to hardware concurrency detection).
446
+ - Only available in React Native environments with `react-native-worklets` installed.
447
+
448
+ ---
449
+
450
+ ## SequentialSigningPool Class (React Native / Fallback)
451
+
452
+ Sequential signing pool for environments without worker support. Signs inputs one-by-one on the main thread using the ECC library from `EccContext`. Same API shape as `WorkerSigningPool` for transparent swapping.
453
+
454
+ > **Note:** `SequentialSigningPool` is only exported from the React Native entry point (`index.react-native.ts`). It is **not** available from the generic (`index.ts`), browser (`index.browser.ts`), or Node.js (`index.node.ts`) entry points. To use it outside React Native, import it directly:
455
+ >
456
+ > ```typescript
457
+ > import { SequentialSigningPool } from '@btc-vision/bitcoin/src/workers/WorkerSigningPool.sequential.js';
458
+ > ```
459
+
460
+ ```typescript
461
+ class SequentialSigningPool {
462
+ static getInstance(config?: WorkerPoolConfig): SequentialSigningPool;
463
+ static resetInstance(): void;
464
+
465
+ get workerCount(): number; // Always 0
466
+ get idleWorkerCount(): number; // Always 0
467
+ get busyWorkerCount(): number; // Always 0
468
+ get isPreservingWorkers(): boolean; // Always false
469
+
470
+ initialize(): Promise<void>; // No-op
471
+ shutdown(): Promise<void>; // No-op
472
+ preserveWorkers(): void; // No-op
473
+ releaseWorkers(): void; // No-op
474
+
475
+ signBatch(
476
+ tasks: readonly SigningTask[],
477
+ keyPair: ParallelSignerKeyPair,
478
+ ): Promise<ParallelSigningResult>;
479
+ }
480
+ ```
481
+
482
+ The `signBatch()` method iterates tasks sequentially, calling `EccContext.get().lib.sign()` or `EccContext.get().lib.signSchnorr()` for each task. Private keys are zeroed after the loop completes.
483
+
484
+ ```typescript
485
+ import { SequentialSigningPool } from '@btc-vision/bitcoin/workers'; // React Native entry point only
486
+
487
+ const pool = SequentialSigningPool.getInstance();
488
+ const result = await pool.signBatch(tasks, keyPair);
489
+ ```
490
+
491
+ ---
492
+
493
+ ## Platform Functions: createSigningPool() and detectRuntime()
494
+
495
+ ### detectRuntime()
496
+
497
+ Detects the current runtime environment.
498
+
499
+ ```typescript
500
+ function detectRuntime(): 'node' | 'browser' | 'react-native' | 'unknown';
501
+ ```
502
+
503
+ Detection logic (in order):
504
+ 1. `navigator.product === 'ReactNative'` returns `'react-native'`
505
+ 2. `process.versions.node` exists returns `'node'`
506
+ 3. `window` and `Worker` exist returns `'browser'`
507
+ 4. Otherwise returns `'unknown'`
508
+
509
+ Platform-specific entry points (`index.node.ts`, `index.browser.ts`, `index.react-native.ts`) hardcode the return value to avoid detection overhead.
510
+
511
+ ### createSigningPool()
512
+
513
+ Creates and initializes the appropriate signing pool for the current runtime.
514
+
515
+ ```typescript
516
+ async function createSigningPool(config?: WorkerPoolConfig): Promise<SigningPoolLike>;
517
+ ```
518
+
519
+ The behavior of `createSigningPool()` depends on which entry point is being used:
520
+
521
+ **Generic entry point (`index.ts`):**
522
+
523
+ | Runtime | Pool class created |
524
+ |---------|--------------------|
525
+ | `'node'` | `NodeWorkerSigningPool` |
526
+ | `'browser'` | `WorkerSigningPool` |
527
+ | `'react-native'` | `SequentialSigningPool` |
528
+ | `'unknown'` | Throws `Error('Unsupported runtime...')` |
529
+
530
+ **Platform-specific entry points:**
531
+
532
+ | Entry point | Pool class created |
533
+ |-------------|--------------------|
534
+ | `index.node.ts` | `NodeWorkerSigningPool` |
535
+ | `index.browser.ts` | `WorkerSigningPool` |
536
+ | `index.react-native.ts` | `WorkletSigningPool` (with fallback to `SequentialSigningPool` if `react-native-worklets` is not installed or initialization fails) |
537
+
538
+ > **Note:** Only the React Native platform-specific entry point (`index.react-native.ts`) attempts to use `WorkletSigningPool` for true parallel signing. The generic entry point (`index.ts`) always creates a `SequentialSigningPool` for React Native, since it does not attempt the worklet probe.
539
+
540
+ ```typescript
541
+ import { createSigningPool } from '@btc-vision/bitcoin/workers';
542
+
543
+ const pool = await createSigningPool({ workerCount: 4 });
544
+ pool.preserveWorkers();
545
+
546
+ // Use pool for signing...
547
+
548
+ await pool.shutdown();
549
+ ```
550
+
551
+ ---
552
+
553
+ ## getSigningPool() Helper
554
+
555
+ Convenience function that returns the browser `WorkerSigningPool` singleton without initialization.
556
+
557
+ ```typescript
558
+ function getSigningPool(config?: WorkerPoolConfig): WorkerSigningPool;
559
+ ```
560
+
561
+ ```typescript
562
+ import { getSigningPool } from '@btc-vision/bitcoin/workers';
563
+
564
+ const pool = getSigningPool({ workerCount: 4 });
565
+ ```
566
+
567
+ This is equivalent to calling `WorkerSigningPool.getInstance(config)`. The pool initializes lazily on the first `signBatch()` call.
568
+
569
+ ---
570
+
571
+ ## PSBT Parallel Signing
572
+
573
+ The `psbt-parallel.ts` module integrates the worker pool with the PSBT class for high-level parallel signing.
574
+
575
+ ### PsbtParallelKeyPair Interface
576
+
577
+ Extends `ParallelSignerKeyPair` with optional network information.
578
+
579
+ ```typescript
580
+ interface PsbtParallelKeyPair extends ParallelSignerKeyPair {
581
+ /** Network (optional, for validation) */
582
+ readonly network?: { messagePrefix: string };
583
+ }
584
+ ```
585
+
586
+ ### ParallelSignOptions Interface
587
+
588
+ Options for controlling parallel PSBT signing behavior.
589
+
590
+ ```typescript
591
+ interface ParallelSignOptions {
592
+ /** Sighash types allowed for signing.
593
+ * Default: [SIGHASH_ALL] for legacy, [SIGHASH_DEFAULT] for Taproot */
594
+ readonly sighashTypes?: readonly number[];
595
+
596
+ /** Tap leaf hash to sign (for Taproot script-path) */
597
+ readonly tapLeafHash?: Uint8Array;
598
+
599
+ /** Worker pool configuration (if pool not provided) */
600
+ readonly poolConfig?: WorkerPoolConfig;
601
+
602
+ /** Whether to use low-R signing for ECDSA. Default: false */
603
+ readonly lowR?: boolean;
604
+ }
605
+ ```
606
+
607
+ ### signPsbtParallel()
608
+
609
+ Main entry point for parallel PSBT signing. Analyzes inputs, creates signing tasks, dispatches them to workers, and applies the resulting signatures back to the PSBT.
610
+
611
+ ```typescript
612
+ async function signPsbtParallel(
613
+ psbt: Psbt,
614
+ keyPair: PsbtParallelKeyPair,
615
+ poolOrConfig?: SigningPoolLike | WorkerPoolConfig,
616
+ options?: ParallelSignOptions,
617
+ ): Promise<ParallelSigningResult>;
618
+ ```
619
+
620
+ The `poolOrConfig` parameter accepts either:
621
+ - An existing `SigningPoolLike` instance (recommended for multiple operations)
622
+ - A `WorkerPoolConfig` to create a temporary pool (pool is shut down after signing if not preserving workers)
623
+
624
+ > **WARNING: Browser-only pool creation when passing config.** When `poolOrConfig` is a `WorkerPoolConfig` (not a pool instance), `signPsbtParallel()` hardcodes `import('./WorkerSigningPool.js')` and always creates a browser `WorkerSigningPool`. This means:
625
+ >
626
+ > - **Node.js users MUST pass an already-created `NodeWorkerSigningPool` instance** as `poolOrConfig`. Passing a plain config object will fail or create the wrong pool type.
627
+ > - **React Native users MUST pass an already-created `WorkletSigningPool` or `SequentialSigningPool` instance.**
628
+ > - Only browser environments can safely pass a `WorkerPoolConfig` directly.
629
+ >
630
+ > ```typescript
631
+ > // CORRECT: Node.js usage -- pass a pool instance
632
+ > import { NodeWorkerSigningPool } from '@btc-vision/bitcoin/workers';
633
+ > const pool = NodeWorkerSigningPool.getInstance({ workerCount: 4 });
634
+ > const result = await signPsbtParallel(psbt, keyPair, pool);
635
+ >
636
+ > // CORRECT: React Native usage -- pass a pool instance
637
+ > import { WorkletSigningPool } from '@btc-vision/bitcoin/workers';
638
+ > const pool = WorkletSigningPool.getInstance({ workerCount: 4 });
639
+ > const result = await signPsbtParallel(psbt, keyPair, pool);
640
+ >
641
+ > // WRONG: Node.js usage -- passing config creates a browser pool!
642
+ > const result = await signPsbtParallel(psbt, keyPair, { workerCount: 4 }); // BUG
643
+ > ```
644
+
645
+ ```typescript
646
+ import { Psbt } from '@btc-vision/bitcoin';
647
+ import { signPsbtParallel, WorkerSigningPool } from '@btc-vision/bitcoin/workers';
648
+
649
+ // With an existing pool (recommended)
650
+ const pool = WorkerSigningPool.getInstance();
651
+ pool.preserveWorkers();
652
+
653
+ const result = await signPsbtParallel(psbt, keyPair, pool);
654
+
655
+ if (result.success) {
656
+ psbt.finalizeAllInputs();
657
+ const tx = psbt.extractTransaction();
658
+ }
659
+
660
+ // With inline configuration (browser only -- creates and destroys browser WorkerSigningPool)
661
+ const result = await signPsbtParallel(psbt, keyPair, { workerCount: 4 });
662
+ ```
663
+
664
+ Internally, `signPsbtParallel()`:
665
+ 1. If `poolOrConfig` is a `SigningPoolLike` instance (has `signBatch` method), uses it directly. Otherwise, imports `WorkerSigningPool` (browser) and creates a singleton instance with the provided config.
666
+ 2. Calls `prepareSigningTasks()` to extract signable inputs.
667
+ 3. Calls `pool.signBatch()` to sign all tasks in parallel.
668
+ 4. Calls `applySignaturesToPsbt()` to write signatures back into the PSBT.
669
+ 5. Shuts down the pool if it was created inline and not preserving workers.
670
+
671
+ ### prepareSigningTasks()
672
+
673
+ Analyzes a PSBT and creates `SigningTask` entries for each input that can be signed with the provided key pair.
674
+
675
+ ```typescript
676
+ function prepareSigningTasks(
677
+ psbt: Psbt,
678
+ keyPair: PsbtParallelKeyPair,
679
+ options?: ParallelSignOptions,
680
+ ): SigningTask[];
681
+ ```
682
+
683
+ > **Important: Only Taproot inputs are supported for parallel signing.** The `prepareLegacyTask()` internal function is a placeholder that always returns `null`. Legacy and SegWit inputs (P2PKH, P2SH, P2WPKH, P2WSH) are silently skipped by `prepareSigningTasks()` and will not be included in the returned task array. For legacy/SegWit inputs, use the standard `psbt.signInput()` or `psbt.signAllInputs()` methods instead.
684
+
685
+ For each PSBT input:
686
+ 1. Checks if the input contains the key pair's public key via `psbt.inputHasPubkey()`.
687
+ 2. If the input is Taproot (`isTaprootInput()`), calls `psbt.checkTaprootHashesForSig()` and creates Schnorr signing tasks. Each task is tagged with `signatureType: SignatureType.Schnorr` and may include a `leafHash` for script-path spending.
688
+ 3. If the input is legacy/SegWit, the implementation is a placeholder that returns `null` -- the input is skipped. Legacy/SegWit parallel signing is not yet implemented.
689
+
690
+ ```typescript
691
+ import { prepareSigningTasks } from '@btc-vision/bitcoin/workers';
692
+
693
+ const tasks = prepareSigningTasks(psbt, keyPair, {
694
+ sighashTypes: [Transaction.SIGHASH_ALL],
695
+ lowR: true,
696
+ });
697
+
698
+ // NOTE: tasks will only contain Taproot inputs.
699
+ // Legacy/SegWit inputs must be signed separately via psbt.signInput().
700
+ console.log(`Found ${tasks.length} Taproot inputs to sign in parallel`);
701
+ ```
702
+
703
+ ### applySignaturesToPsbt()
704
+
705
+ Writes parallel signing results back into the PSBT data structure.
706
+
707
+ ```typescript
708
+ function applySignaturesToPsbt(
709
+ psbt: Psbt,
710
+ result: ParallelSigningResult,
711
+ keyPair: PsbtParallelKeyPair,
712
+ ): void;
713
+ ```
714
+
715
+ For each successful signature in `result.signatures`:
716
+
717
+ | Signature type | PSBT field updated | Details |
718
+ |----------------|-------------------|---------|
719
+ | Schnorr + leafHash | `tapScriptSig` | Script-path: `{ pubkey: xOnlyPubkey, signature: serialized, leafHash }` |
720
+ | Schnorr (no leafHash) | `tapKeySig` | Key-path: serialized Taproot signature |
721
+ | ECDSA | `partialSig` | `{ pubkey, signature: DER-encoded with sighash }` |
722
+
723
+ ```typescript
724
+ import { applySignaturesToPsbt } from '@btc-vision/bitcoin/workers';
725
+
726
+ // After pool.signBatch() returns:
727
+ if (result.success) {
728
+ applySignaturesToPsbt(psbt, result, keyPair);
729
+ psbt.finalizeAllInputs();
730
+ }
731
+ ```
732
+
733
+ ---
734
+
735
+ ## Worker Internals
736
+
737
+ ### Worker Code Generation
738
+
739
+ The browser pool uses inline worker code generated at compile time. The `@noble/secp256k1` ECC library is bundled as a string constant (`ECC_BUNDLE`) and evaluated inside each worker via `new Function()`. No network requests are made.
740
+
741
+ ```typescript
742
+ // Generate worker code as a string
743
+ function generateWorkerCode(): string;
744
+
745
+ // Create a Blob URL for use with new Worker()
746
+ function createWorkerBlobUrl(): string;
747
+
748
+ // Revoke a previously created Blob URL
749
+ function revokeWorkerBlobUrl(url: string): void;
750
+ ```
751
+
752
+ ### Worker Message Protocol
753
+
754
+ Workers communicate with the main thread via structured messages. All messages include a `type` discriminator field.
755
+
756
+ **Messages to worker:**
757
+
758
+ | Type | Interface | Description |
759
+ |------|-----------|-------------|
760
+ | `'init'` | `WorkerInitMessage` | Initialize the ECC library |
761
+ | `'sign'` | `SigningTaskMessage` | Sign a single hash |
762
+ | `'signBatch'` | `BatchSigningMessage` | Sign multiple hashes with one private key |
763
+ | `'shutdown'` | `WorkerShutdownMessage` | Terminate the worker |
764
+
765
+ **Messages from worker:**
766
+
767
+ | Type | Interface | Description |
768
+ |------|-----------|-------------|
769
+ | `'ready'` | `WorkerReadyMessage` | Worker initialized and ready |
770
+ | `'result'` | `SigningResultMessage` | Single signing result |
771
+ | `'batchResult'` | `BatchSigningResultMessage` | Batch of signing results |
772
+ | `'error'` | `SigningErrorMessage` | Signing error |
773
+ | `'shutdown-ack'` | `WorkerShutdownAckMessage` | Shutdown acknowledged |
774
+
775
+ Type guards are provided for response discrimination:
776
+
777
+ ```typescript
778
+ function isSigningError(response: WorkerResponse): response is SigningErrorMessage;
779
+ function isSigningResult(response: WorkerResponse): response is SigningResultMessage;
780
+ function isBatchResult(response: WorkerResponse): response is BatchSigningResultMessage;
781
+ function isWorkerReady(response: WorkerResponse): response is WorkerReadyMessage;
782
+ ```
783
+
784
+ ### WorkerState Enum
785
+
786
+ Tracks the lifecycle of each pooled worker.
787
+
788
+ ```typescript
789
+ const WorkerState = {
790
+ Initializing: 0, // Worker is loading ECC library
791
+ Idle: 1, // Ready and waiting for tasks
792
+ Busy: 2, // Currently processing a signing batch
793
+ ShuttingDown: 3, // Shutdown message sent, awaiting ack
794
+ Terminated: 4, // Worker has been terminated
795
+ } as const;
796
+ ```
797
+
798
+ ### SigningPoolLike Interface
799
+
800
+ The minimum contract shared by all pool implementations. Extends both `Disposable` and `AsyncDisposable`.
801
+
802
+ ```typescript
803
+ interface SigningPoolLike extends Disposable, AsyncDisposable {
804
+ signBatch(
805
+ tasks: readonly SigningTask[],
806
+ keyPair: ParallelSignerKeyPair,
807
+ ): Promise<ParallelSigningResult>;
808
+ preserveWorkers(): void;
809
+ releaseWorkers(): void;
810
+ initialize(): Promise<void>;
811
+ shutdown(): Promise<void>;
812
+ readonly workerCount: number;
813
+ readonly idleWorkerCount: number;
814
+ readonly busyWorkerCount: number;
815
+ readonly isPreservingWorkers: boolean;
816
+ }
817
+ ```
818
+
819
+ ---
820
+
821
+ ## Platform Support
822
+
823
+ | Platform | Pool class | Threading | ECC library |
824
+ |----------|-----------|-----------|-------------|
825
+ | Node.js | `NodeWorkerSigningPool` | `worker_threads` (true parallelism) | `tiny-secp256k1` (WASM) or `@noble/secp256k1` (JS) |
826
+ | Browser | `WorkerSigningPool` | Web Workers via Blob URL | `@noble/secp256k1` bundled inline |
827
+ | React Native | `WorkletSigningPool` | `react-native-worklets` runtimes (true parallelism) | `@noble/secp256k1` bundled inline (eval'd per runtime) |
828
+ | React Native (fallback) | `SequentialSigningPool` | None (main thread) | `EccContext` library |
829
+
830
+ ### Entry Points
831
+
832
+ The library provides platform-specific entry points that avoid importing unused platform modules:
833
+
834
+ | Entry point | File | `detectRuntime()` | `createSigningPool()` |
835
+ |-------------|------|-------------------|----------------------|
836
+ | Generic | `index.ts` | Auto-detects | Dynamic import based on runtime (uses `SequentialSigningPool` for React Native) |
837
+ | Node.js | `index.node.ts` | Always `'node'` | Creates `NodeWorkerSigningPool` |
838
+ | Browser | `index.browser.ts` | Always `'browser'` | Creates `WorkerSigningPool` |
839
+ | React Native | `index.react-native.ts` | Always `'react-native'` | Tries `WorkletSigningPool`, falls back to `SequentialSigningPool` |
840
+
841
+ The generic entry point uses dynamic `import()` to load only the platform-relevant pool class, preventing bundler issues (e.g., browser bundlers will not encounter `worker_threads` imports).
842
+
843
+ ---
844
+
845
+ ## Complete Workflow Example
846
+
847
+ A full example of parallel PSBT signing from pool creation to transaction extraction.
848
+
849
+ ```typescript
850
+ import { Psbt, networks } from '@btc-vision/bitcoin';
851
+ import {
852
+ signPsbtParallel,
853
+ createSigningPool,
854
+ SignatureType,
855
+ type PsbtParallelKeyPair,
856
+ type SigningPoolLike,
857
+ } from '@btc-vision/bitcoin/workers';
858
+
859
+ // 1. Create a key pair that implements PsbtParallelKeyPair
860
+ const keyPair: PsbtParallelKeyPair = {
861
+ publicKey: myCompressedPublicKey, // Uint8Array, 33 bytes
862
+ getPrivateKey(): Uint8Array {
863
+ return myPrivateKeyBytes; // Uint8Array, 32 bytes
864
+ },
865
+ };
866
+
867
+ // 2. Create and configure the signing pool (auto-detects runtime)
868
+ const pool: SigningPoolLike = await createSigningPool({ workerCount: 4 });
869
+ pool.preserveWorkers(); // Keep workers alive for multiple operations
870
+
871
+ // 3. Build a PSBT
872
+ const psbt = new Psbt({ network: networks.bitcoin });
873
+
874
+ psbt.addInput({
875
+ hash: 'previous_txid...',
876
+ index: 0,
877
+ witnessUtxo: {
878
+ script: outputScript,
879
+ value: 100000n,
880
+ },
881
+ });
882
+
883
+ psbt.addOutput({
884
+ address: 'bc1q...',
885
+ value: 90000n,
886
+ });
887
+
888
+ // 4. Sign all matching Taproot inputs in parallel
889
+ // (Legacy/SegWit inputs are not supported -- sign those separately)
890
+ const result = await signPsbtParallel(psbt, keyPair, pool, {
891
+ sighashTypes: [0x01], // SIGHASH_ALL
892
+ lowR: true,
893
+ });
894
+
895
+ // 5. Check results
896
+ if (result.success) {
897
+ console.log(`Signed ${result.signatures.size} inputs in ${result.durationMs}ms`);
898
+
899
+ // 6. Finalize and extract
900
+ psbt.finalizeAllInputs();
901
+ const tx = psbt.extractTransaction();
902
+ console.log('Transaction hex:', tx.toHex());
903
+ } else {
904
+ for (const [inputIndex, error] of result.errors) {
905
+ console.error(`Input ${inputIndex} failed: ${error}`);
906
+ }
907
+ }
908
+
909
+ // 7. Shutdown pool when application is done
910
+ await pool.shutdown();
911
+ ```
912
+
913
+ ### Lower-Level Usage (Manual Tasks)
914
+
915
+ For more control, use `prepareSigningTasks()`, `signBatch()`, and `applySignaturesToPsbt()` individually.
916
+
917
+ ```typescript
918
+ import {
919
+ prepareSigningTasks,
920
+ applySignaturesToPsbt,
921
+ WorkerSigningPool,
922
+ } from '@btc-vision/bitcoin/workers';
923
+
924
+ const pool = WorkerSigningPool.getInstance({ workerCount: 8 });
925
+ await pool.initialize();
926
+ pool.preserveWorkers();
927
+
928
+ // Manually prepare tasks (only Taproot inputs will be included)
929
+ const tasks = prepareSigningTasks(psbt, keyPair, { lowR: true });
930
+ console.log(`Prepared ${tasks.length} signing tasks`);
931
+
932
+ // Sign in parallel
933
+ const result = await pool.signBatch(tasks, keyPair);
934
+
935
+ // Apply signatures manually
936
+ if (result.success) {
937
+ applySignaturesToPsbt(psbt, result, keyPair);
938
+ }
939
+
940
+ await pool.shutdown();
941
+ ```
942
+
943
+ ### Using `await using` for Automatic Cleanup
944
+
945
+ All pool classes implement `AsyncDisposable`, enabling automatic cleanup.
946
+
947
+ ```typescript
948
+ {
949
+ await using pool = await createSigningPool({ workerCount: 4 });
950
+ pool.preserveWorkers();
951
+
952
+ const result = await signPsbtParallel(psbt, keyPair, pool);
953
+ // Pool is automatically shut down when leaving the block
954
+ }
955
+ ```
956
+
957
+ ---
958
+
959
+ ## Performance Considerations
960
+
961
+ ### When Parallel Signing Helps
962
+
963
+ Parallel signing provides the most benefit when a PSBT has many inputs. The overhead of spawning workers and distributing tasks means that for a single input, sequential signing on the main thread is faster.
964
+
965
+ | Inputs | Recommended approach |
966
+ |--------|---------------------|
967
+ | 1-2 | Sequential signing (standard `psbt.signInput()`) |
968
+ | 3-10 | Parallel signing with 2-4 workers |
969
+ | 10-100+ | Parallel signing with workers equal to CPU core count |
970
+
971
+ ### Worker Preservation
972
+
973
+ Creating and initializing workers has a measurable startup cost (ECC library loading, message round-trips). For applications that sign multiple transactions:
974
+
975
+ ```typescript
976
+ // DO: Initialize once, sign many times
977
+ const pool = await createSigningPool();
978
+ pool.preserveWorkers();
979
+
980
+ for (const psbt of psbtsToSign) {
981
+ await signPsbtParallel(psbt, keyPair, pool);
982
+ }
983
+
984
+ await pool.shutdown();
985
+
986
+ // DON'T: Create and destroy for each transaction
987
+ for (const psbt of psbtsToSign) {
988
+ await signPsbtParallel(psbt, keyPair, { workerCount: 4 }); // Slow!
989
+ }
990
+ ```
991
+
992
+ ### Batch Signing Efficiency
993
+
994
+ The `signBatch` message type sends multiple tasks with a single private key to one worker. This is more efficient than sending individual `sign` messages because:
995
+ 1. The private key is transferred once instead of per-task.
996
+ 2. A single response message carries all results.
997
+ 3. The private key is zeroed once after all tasks complete.
998
+
999
+ Tasks are distributed across workers using round-robin assignment: `taskBatches[i % workerCount]`.
1000
+
1001
+ ### Node.js Zero-Copy Transfer
1002
+
1003
+ The `NodeWorkerSigningPool` uses `ArrayBuffer` transfer lists with `postMessage()` to avoid copying private keys and hashes between threads. The transferred buffers become detached in the main thread (inaccessible), which also serves as an additional security measure since the key bytes are no longer readable in the sending context.
1004
+
1005
+ ### Key Hold Time Safety
1006
+
1007
+ The `maxKeyHoldTimeMs` configuration (default 5 seconds) acts as a safety net. If a worker takes longer than this to complete a signing batch, it is forcefully terminated and replaced with a new worker. This prevents a stalled or compromised worker from holding key material indefinitely.