@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.
- package/README.md +112 -13
- package/benchmark-compare/BENCHMARK.md +74 -59
- package/benchmark-compare/compare.bench.ts +249 -96
- package/benchmark-compare/harness.ts +23 -25
- package/benchmark-compare/package.json +1 -0
- package/browser/address.d.ts +4 -4
- package/browser/address.d.ts.map +1 -1
- package/browser/chunks/{psbt-parallel-B-dfm5GZ.js → psbt-parallel-jZ6QcCnM.js} +3128 -2731
- package/browser/index.d.ts +1 -1
- package/browser/index.d.ts.map +1 -1
- package/browser/index.js +603 -585
- package/browser/io/base58check.d.ts +1 -25
- package/browser/io/base58check.d.ts.map +1 -1
- package/browser/io/base64.d.ts.map +1 -1
- package/browser/networks.d.ts +1 -0
- package/browser/networks.d.ts.map +1 -1
- package/browser/payments/bip341.d.ts +17 -0
- package/browser/payments/bip341.d.ts.map +1 -1
- package/browser/payments/index.d.ts +3 -2
- package/browser/payments/index.d.ts.map +1 -1
- package/browser/payments/p2mr.d.ts +169 -0
- package/browser/payments/p2mr.d.ts.map +1 -0
- package/browser/payments/types.d.ts +11 -1
- package/browser/payments/types.d.ts.map +1 -1
- package/browser/psbt/bip371.d.ts +30 -0
- package/browser/psbt/bip371.d.ts.map +1 -1
- package/browser/psbt/psbtutils.d.ts +1 -0
- package/browser/psbt/psbtutils.d.ts.map +1 -1
- package/browser/psbt.d.ts.map +1 -1
- package/browser/workers/index.js +9 -9
- package/build/address.d.ts +4 -4
- package/build/address.d.ts.map +1 -1
- package/build/address.js +11 -1
- package/build/address.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js.map +1 -1
- package/build/io/base58check.d.ts +1 -25
- package/build/io/base58check.d.ts.map +1 -1
- package/build/io/base58check.js +1 -31
- package/build/io/base58check.js.map +1 -1
- package/build/io/base64.d.ts.map +1 -1
- package/build/io/base64.js +3 -0
- package/build/io/base64.js.map +1 -1
- package/build/networks.d.ts +1 -0
- package/build/networks.d.ts.map +1 -1
- package/build/networks.js +12 -0
- package/build/networks.js.map +1 -1
- package/build/payments/bip341.d.ts +17 -0
- package/build/payments/bip341.d.ts.map +1 -1
- package/build/payments/bip341.js +32 -1
- package/build/payments/bip341.js.map +1 -1
- package/build/payments/index.d.ts +3 -2
- package/build/payments/index.d.ts.map +1 -1
- package/build/payments/index.js +2 -1
- package/build/payments/index.js.map +1 -1
- package/build/payments/p2mr.d.ts +178 -0
- package/build/payments/p2mr.d.ts.map +1 -0
- package/build/payments/p2mr.js +555 -0
- package/build/payments/p2mr.js.map +1 -0
- package/build/payments/types.d.ts +11 -1
- package/build/payments/types.d.ts.map +1 -1
- package/build/payments/types.js +1 -0
- package/build/payments/types.js.map +1 -1
- package/build/psbt/bip371.d.ts +30 -0
- package/build/psbt/bip371.d.ts.map +1 -1
- package/build/psbt/bip371.js +80 -15
- package/build/psbt/bip371.js.map +1 -1
- package/build/psbt/psbtutils.d.ts +1 -0
- package/build/psbt/psbtutils.d.ts.map +1 -1
- package/build/psbt/psbtutils.js +2 -0
- package/build/psbt/psbtutils.js.map +1 -1
- package/build/psbt.d.ts.map +1 -1
- package/build/psbt.js +3 -2
- package/build/psbt.js.map +1 -1
- package/build/pubkey.js +1 -1
- package/build/pubkey.js.map +1 -1
- package/build/tsconfig.build.tsbuildinfo +1 -1
- package/documentation/README.md +122 -0
- package/documentation/address.md +820 -0
- package/documentation/block.md +679 -0
- package/documentation/crypto.md +461 -0
- package/documentation/ecc.md +584 -0
- package/documentation/errors.md +656 -0
- package/documentation/io.md +942 -0
- package/documentation/networks.md +625 -0
- package/documentation/p2mr.md +380 -0
- package/documentation/payments.md +1485 -0
- package/documentation/psbt.md +1400 -0
- package/documentation/script.md +730 -0
- package/documentation/taproot.md +670 -0
- package/documentation/transaction.md +943 -0
- package/documentation/types.md +587 -0
- package/documentation/workers.md +1007 -0
- package/eslint.config.js +3 -0
- package/package.json +17 -14
- package/src/address.ts +22 -10
- package/src/index.ts +1 -0
- package/src/io/base58check.ts +1 -35
- package/src/io/base64.ts +5 -0
- package/src/networks.ts +13 -0
- package/src/payments/bip341.ts +36 -1
- package/src/payments/index.ts +4 -0
- package/src/payments/p2mr.ts +660 -0
- package/src/payments/types.ts +12 -0
- package/src/psbt/bip371.ts +84 -13
- package/src/psbt/psbtutils.ts +2 -0
- package/src/psbt.ts +4 -2
- package/src/pubkey.ts +1 -1
- package/test/bitcoin.core.spec.ts +1 -1
- package/test/fixtures/p2mr.json +270 -0
- package/test/integration/taproot.spec.ts +7 -3
- package/test/opnetTestnet.spec.ts +302 -0
- package/test/payments.spec.ts +3 -1
- package/test/psbt.spec.ts +297 -2
- 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.
|