@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
@@ -1,17 +1,23 @@
1
1
  /**
2
- * Comprehensive benchmark: @btc-vision/bitcoin vs bitcoinjs-lib
2
+ * Comprehensive benchmark comparing three Bitcoin transaction libraries:
3
+ * - `@btc-vision/bitcoin` (this fork) with Noble and tiny-secp256k1 backends
4
+ * - `bitcoinjs-lib` v7.0.1 (official)
5
+ * - `@scure/btc-signer` (scure)
3
6
  *
4
- * Run:
5
- * cd benchmark-compare && npm run bench
6
- * cd benchmark-compare && npm run bench:gc # with GC control
7
+ * @example
8
+ * ```bash
9
+ * cd benchmark-compare && npm run bench
10
+ * cd benchmark-compare && npm run bench:gc # with GC control
11
+ * ```
7
12
  *
13
+ * @remarks
8
14
  * Scenarios:
9
- * 1. Library Initialization (cold-start)
10
- * 2. PSBT Creation (varying input counts)
11
- * 3. P2WPKH Signing
12
- * 4. P2TR Taproot Signing
13
- * 5. End-to-End Lifecycle
14
- * 6. Parallel Signing (fork-only)
15
+ * 1. Library Initialization (cold-start)
16
+ * 2. PSBT Creation (varying input counts)
17
+ * 3. P2WPKH Signing
18
+ * 4. P2TR Taproot Signing
19
+ * 5. End-to-End Lifecycle
20
+ * 6. Parallel Signing (fork-only)
15
21
  */
16
22
 
17
23
  import { execSync } from 'child_process';
@@ -29,12 +35,13 @@ import {
29
35
  printForkOnly,
30
36
  } from './harness.js';
31
37
 
32
- // ── Official bitcoinjs-lib ──────────────────────────────────────────────────
33
38
  import * as bitcoin from 'bitcoinjs-lib';
34
39
  import ECPairFactory from 'ecpair';
35
40
  import * as tinysecp from 'tiny-secp256k1';
36
41
 
37
- // ── Fork (@btc-vision/bitcoin) ─────────────────────────────────────────────
42
+ import * as scureBtc from '@scure/btc-signer';
43
+ import { secp256k1 } from '@noble/curves/secp256k1.js';
44
+
38
45
  import {
39
46
  Psbt as ForkPsbt,
40
47
  initEccLib,
@@ -50,49 +57,52 @@ import {
50
57
  ECPairSigner,
51
58
  } from '@btc-vision/ecpair';
52
59
 
53
- // ─────────────────────────────────────────────────────────────────────────────
54
- // Initialization helpers
55
- // ─────────────────────────────────────────────────────────────────────────────
56
-
60
+ /** Initializes the official bitcoinjs-lib ECC backend. */
57
61
  function initOfficial(): void {
58
62
  bitcoin.initEccLib(tinysecp);
59
63
  }
60
64
 
65
+ /** Initializes the fork ECC backend with Noble (pure JS). */
61
66
  function initForkNoble(): void {
62
67
  initEccLib(createNobleBackend());
63
68
  }
64
69
 
70
+ /** Initializes the fork ECC backend with tiny-secp256k1 (WASM). */
65
71
  function initForkTiny(): void {
66
72
  initEccLib(createLegacyBackend(tinysecp));
67
73
  }
68
74
 
69
- // ─────────────────────────────────────────────────────────────────────────────
70
- // Shared key material (deterministic from same seed for fairness)
71
- // ─────────────────────────────────────────────────────────────────────────────
72
-
75
+ /**
76
+ * Deterministic seed shared across all libraries for fair comparison.
77
+ * All key pairs are derived from this same 32-byte seed.
78
+ */
73
79
  const SEED = Buffer.from(
74
80
  'e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35',
75
81
  'hex',
76
82
  );
77
83
 
78
- // Official ECPair
79
84
  const officialECPair = ECPairFactory(tinysecp);
80
85
  const officialKeyPair = officialECPair.fromPrivateKey(SEED);
81
86
 
82
- // Fork Noble key pair
83
87
  const nobleBackend = createNobleBackend();
84
88
  const forkNobleKeyPair = ECPairSigner.fromPrivateKey(nobleBackend, SEED);
85
89
 
86
- // Fork tiny-secp256k1 key pair
87
90
  const tinyBackend = createLegacyBackend(tinysecp);
88
91
  const forkTinyKeyPair = ECPairSigner.fromPrivateKey(tinyBackend, SEED);
89
92
 
90
- // Tweaked signers for P2TR (key-path spend)
91
- // Must be created after ECC init — we'll create them lazily
93
+ const scurePrivKey = new Uint8Array(SEED);
94
+ const scurePubKey = secp256k1.getPublicKey(scurePrivKey, true);
95
+ const scureXOnlyPubKey = scurePubKey.slice(1);
96
+
97
+ /**
98
+ * Lazily-initialized tweaked signers for P2TR key-path spends.
99
+ * Must be created after ECC init, so they're built on first access.
100
+ */
92
101
  let _forkNobleTweaked: ReturnType<typeof forkNobleKeyPair.tweak> | null = null;
93
102
  let _forkTinyTweaked: ReturnType<typeof forkTinyKeyPair.tweak> | null = null;
94
103
  let _officialTweaked: ReturnType<typeof officialKeyPair.tweak> | null = null;
95
104
 
105
+ /** @returns The fork Noble tweaked signer for Taproot key-path spends. */
96
106
  function getForkNobleTweakedSigner() {
97
107
  if (!_forkNobleTweaked) {
98
108
  const xonly = toXOnly(forkNobleKeyPair.publicKey);
@@ -101,6 +111,7 @@ function getForkNobleTweakedSigner() {
101
111
  return _forkNobleTweaked;
102
112
  }
103
113
 
114
+ /** @returns The fork tiny-secp256k1 tweaked signer for Taproot key-path spends. */
104
115
  function getForkTinyTweakedSigner() {
105
116
  if (!_forkTinyTweaked) {
106
117
  const xonly = toXOnly(forkTinyKeyPair.publicKey);
@@ -109,6 +120,7 @@ function getForkTinyTweakedSigner() {
109
120
  return _forkTinyTweaked;
110
121
  }
111
122
 
123
+ /** @returns The official tweaked signer for Taproot key-path spends. */
112
124
  function getOfficialTweakedSigner() {
113
125
  if (!_officialTweaked) {
114
126
  const xonly = bitcoin.toXOnly(officialKeyPair.publicKey);
@@ -117,10 +129,7 @@ function getOfficialTweakedSigner() {
117
129
  return _officialTweaked;
118
130
  }
119
131
 
120
- // ─────────────────────────────────────────────────────────────────────────────
121
- // Helper: build a fake prev tx output script for P2WPKH
122
- // ─────────────────────────────────────────────────────────────────────────────
123
-
132
+ /** Builds a P2WPKH witness UTXO for the official library. */
124
133
  function makeWitnessUtxoOfficial(pubkey: Uint8Array): { script: Uint8Array; value: bigint } {
125
134
  const p2wpkh = bitcoin.payments.p2wpkh({ pubkey });
126
135
  return {
@@ -129,6 +138,7 @@ function makeWitnessUtxoOfficial(pubkey: Uint8Array): { script: Uint8Array; valu
129
138
  };
130
139
  }
131
140
 
141
+ /** Builds a P2WPKH witness UTXO for the fork library. */
132
142
  function makeWitnessUtxoFork(pubkey: Uint8Array): { script: Uint8Array; value: bigint } {
133
143
  const p2wpkh = forkPayments.p2wpkh({ pubkey });
134
144
  return {
@@ -137,7 +147,13 @@ function makeWitnessUtxoFork(pubkey: Uint8Array): { script: Uint8Array; value: b
137
147
  };
138
148
  }
139
149
 
140
- // Helper: build a fake prev tx output for P2TR
150
+ /** Builds a P2WPKH witness UTXO for `@scure/btc-signer`. */
151
+ function makeWitnessUtxoScure(pubkey: Uint8Array): { script: Uint8Array; amount: bigint } {
152
+ const p2wpkh = scureBtc.p2wpkh(pubkey);
153
+ return { script: p2wpkh.script, amount: 100_000n };
154
+ }
155
+
156
+ /** Builds a P2TR (Taproot) witness UTXO for the official library. */
141
157
  function makeTaprootWitnessUtxoOfficial(pubkey: Uint8Array): { script: Uint8Array; value: bigint } {
142
158
  const xonly = bitcoin.toXOnly(pubkey);
143
159
  const p2tr = bitcoin.payments.p2tr({ internalPubkey: xonly });
@@ -147,6 +163,7 @@ function makeTaprootWitnessUtxoOfficial(pubkey: Uint8Array): { script: Uint8Arra
147
163
  };
148
164
  }
149
165
 
166
+ /** Builds a P2TR (Taproot) witness UTXO for the fork library. */
150
167
  function makeTaprootWitnessUtxoFork(pubkey: Uint8Array): { script: Uint8Array; value: bigint } {
151
168
  const xonly = toXOnly(pubkey);
152
169
  const p2tr = forkPayments.p2tr({ internalPubkey: xonly });
@@ -156,23 +173,31 @@ function makeTaprootWitnessUtxoFork(pubkey: Uint8Array): { script: Uint8Array; v
156
173
  };
157
174
  }
158
175
 
159
- // ─────────────────────────────────────────────────────────────────────────────
160
- // Helper: random tx hash
161
- // ─────────────────────────────────────────────────────────────────────────────
176
+ /** Builds a P2TR (Taproot) witness UTXO for `@scure/btc-signer`. */
177
+ function makeTaprootWitnessUtxoScure(xOnlyPubkey: Uint8Array): { script: Uint8Array; amount: bigint } {
178
+ const p2tr = scureBtc.p2tr(xOnlyPubkey);
179
+ return { script: p2tr.script, amount: 100_000n };
180
+ }
162
181
 
182
+ /** @returns A random 32-byte transaction hash for benchmark inputs. */
163
183
  function randomTxHash(): Buffer {
164
184
  return randomBytes(32);
165
185
  }
166
186
 
167
- // ─────────────────────────────────────────────────────────────────────────────
168
- // Scenario 1: Library Initialization (subprocess isolation)
169
- // ─────────────────────────────────────────────────────────────────────────────
170
-
187
+ /**
188
+ * Scenario 1: Measures cold-start initialization time for each library
189
+ * by spawning isolated Node.js subprocesses per iteration.
190
+ */
171
191
  async function scenarioInit(): Promise<ComparisonRow[]> {
172
192
  console.log('\n--- Scenario 1: Library Initialization (cold-start) ---\n');
173
193
 
174
194
  const iterations = 5;
175
195
 
196
+ /**
197
+ * Spawns a subprocess to measure cold-start import + init time.
198
+ * @param label - Display name for the result.
199
+ * @param scriptBody - ESM script body to execute in a child process.
200
+ */
176
201
  function measureColdStart(label: string, scriptBody: string): BenchResult {
177
202
  const scriptPath = join(__dirname, `_bench_init_${Date.now()}.mjs`);
178
203
  writeFileSync(scriptPath, scriptBody, 'utf-8');
@@ -232,6 +257,18 @@ console.log((performance.now() - t0).toFixed(4));`,
232
257
  );
233
258
  console.log(` Fork (tiny-secp256k1): ${fmt(forkTinyInit.median)}`);
234
259
 
260
+ const scureInit = measureColdStart(
261
+ 'Scure (btc-signer)',
262
+ `const t0 = performance.now();
263
+ import * as btc from '@scure/btc-signer';
264
+ import { secp256k1 } from '@noble/curves/secp256k1.js';
265
+ const privKey = new Uint8Array(32).fill(1);
266
+ const pubKey = secp256k1.getPublicKey(privKey, true);
267
+ btc.p2wpkh(pubKey);
268
+ console.log((performance.now() - t0).toFixed(4));`,
269
+ );
270
+ console.log(` Scure (btc-signer): ${fmt(scureInit.median)}`);
271
+
235
272
  const officialInit = measureColdStart(
236
273
  'Official (tiny-secp256k1)',
237
274
  `const t0 = performance.now();
@@ -249,18 +286,18 @@ console.log((performance.now() - t0).toFixed(4));`,
249
286
  detail: 'cold-start',
250
287
  forkNoble: forkNobleInit,
251
288
  fork: forkTinyInit,
289
+ scure: scureInit,
252
290
  official: officialInit,
253
291
  }];
254
292
  }
255
293
 
256
- // ─────────────────────────────────────────────────────────────────────────────
257
- // Scenario 2: PSBT Creation
258
- // ─────────────────────────────────────────────────────────────────────────────
259
-
294
+ /**
295
+ * Scenario 2: Measures PSBT/transaction creation time (addInput + addOutput)
296
+ * without signing, across varying input counts (10-500).
297
+ */
260
298
  async function scenarioPsbtCreation(): Promise<ComparisonRow[]> {
261
299
  console.log('\n--- Scenario 2: PSBT Creation ---\n');
262
300
 
263
- // Make sure both ECC libs are initialized
264
301
  initOfficial();
265
302
  initForkNoble();
266
303
 
@@ -269,8 +306,6 @@ async function scenarioPsbtCreation(): Promise<ComparisonRow[]> {
269
306
 
270
307
  for (const numInputs of inputCounts) {
271
308
  const iters = numInputs >= 500 ? 5 : numInputs >= 250 ? 10 : 30;
272
-
273
- // Pre-generate hashes
274
309
  const hashes = Array.from({ length: numInputs }, () => randomTxHash());
275
310
 
276
311
  const forkResult = await measure(
@@ -298,6 +333,31 @@ async function scenarioPsbtCreation(): Promise<ComparisonRow[]> {
298
333
  { iterations: iters },
299
334
  );
300
335
 
336
+ const scureResult = await measure(
337
+ `Scure Tx ${numInputs}in`,
338
+ () => {
339
+ const witnessUtxo = makeWitnessUtxoScure(scurePubKey);
340
+ const tx = new scureBtc.Transaction();
341
+ for (let i = 0; i < numInputs; i++) {
342
+ tx.addInput({
343
+ txid: hashes[i]!,
344
+ index: 0,
345
+ witnessUtxo: {
346
+ script: witnessUtxo.script,
347
+ amount: witnessUtxo.amount,
348
+ },
349
+ });
350
+ }
351
+ for (let j = 0; j < 5; j++) {
352
+ tx.addOutput({
353
+ script: witnessUtxo.script,
354
+ amount: 10_000n,
355
+ });
356
+ }
357
+ },
358
+ { iterations: iters },
359
+ );
360
+
301
361
  const officialResult = await measure(
302
362
  `Official PSBT ${numInputs}in`,
303
363
  () => {
@@ -321,7 +381,7 @@ async function scenarioPsbtCreation(): Promise<ComparisonRow[]> {
321
381
  );
322
382
 
323
383
  console.log(
324
- ` ${String(numInputs).padStart(4)} inputs: Fork=${fmt(forkResult.median)}, Official=${fmt(officialResult.median)}`,
384
+ ` ${String(numInputs).padStart(4)} inputs: Fork=${fmt(forkResult.median)}, Scure=${fmt(scureResult.median)}, Official=${fmt(officialResult.median)}`,
325
385
  );
326
386
 
327
387
  rows.push({
@@ -329,6 +389,7 @@ async function scenarioPsbtCreation(): Promise<ComparisonRow[]> {
329
389
  detail: `${numInputs} inputs`,
330
390
  forkNoble: forkResult,
331
391
  fork: null,
392
+ scure: scureResult,
332
393
  official: officialResult,
333
394
  });
334
395
  }
@@ -336,10 +397,10 @@ async function scenarioPsbtCreation(): Promise<ComparisonRow[]> {
336
397
  return rows;
337
398
  }
338
399
 
339
- // ─────────────────────────────────────────────────────────────────────────────
340
- // Scenario 3: P2WPKH Signing
341
- // ─────────────────────────────────────────────────────────────────────────────
342
-
400
+ /**
401
+ * Scenario 3: Measures P2WPKH (SegWit v0) transaction creation + signing
402
+ * across varying input counts (10-500).
403
+ */
343
404
  async function scenarioP2wpkhSigning(): Promise<ComparisonRow[]> {
344
405
  console.log('\n--- Scenario 3: P2WPKH Signing ---\n');
345
406
 
@@ -352,7 +413,6 @@ async function scenarioP2wpkhSigning(): Promise<ComparisonRow[]> {
352
413
  const iters = numInputs >= 500 ? 5 : numInputs >= 250 ? 10 : 30;
353
414
  const hashes = Array.from({ length: numInputs }, () => randomTxHash());
354
415
 
355
- // --- Fork with Noble ---
356
416
  initForkNoble();
357
417
  const forkNobleResult = await measure(
358
418
  `Fork Noble P2WPKH ${numInputs}`,
@@ -380,7 +440,6 @@ async function scenarioP2wpkhSigning(): Promise<ComparisonRow[]> {
380
440
  { iterations: iters },
381
441
  );
382
442
 
383
- // --- Fork with tiny-secp256k1 ---
384
443
  initForkTiny();
385
444
  const forkTinyResult = await measure(
386
445
  `Fork tiny P2WPKH ${numInputs}`,
@@ -408,7 +467,32 @@ async function scenarioP2wpkhSigning(): Promise<ComparisonRow[]> {
408
467
  { iterations: iters },
409
468
  );
410
469
 
411
- // --- Official with tiny-secp256k1 ---
470
+ const scureResult = await measure(
471
+ `Scure P2WPKH ${numInputs}`,
472
+ () => {
473
+ const witnessUtxo = makeWitnessUtxoScure(scurePubKey);
474
+ const tx = new scureBtc.Transaction();
475
+ for (let i = 0; i < numInputs; i++) {
476
+ tx.addInput({
477
+ txid: hashes[i]!,
478
+ index: 0,
479
+ witnessUtxo: {
480
+ script: witnessUtxo.script,
481
+ amount: witnessUtxo.amount,
482
+ },
483
+ });
484
+ }
485
+ for (let j = 0; j < 5; j++) {
486
+ tx.addOutput({
487
+ script: witnessUtxo.script,
488
+ amount: 10_000n,
489
+ });
490
+ }
491
+ tx.sign(scurePrivKey);
492
+ },
493
+ { iterations: iters },
494
+ );
495
+
412
496
  initOfficial();
413
497
  const officialResult = await measure(
414
498
  `Official P2WPKH ${numInputs}`,
@@ -434,7 +518,7 @@ async function scenarioP2wpkhSigning(): Promise<ComparisonRow[]> {
434
518
  );
435
519
 
436
520
  console.log(
437
- ` ${String(numInputs).padStart(4)} inputs: Noble=${fmt(forkNobleResult.median)}, tiny=${fmt(forkTinyResult.median)}, Official=${fmt(officialResult.median)}`,
521
+ ` ${String(numInputs).padStart(4)} inputs: Noble=${fmt(forkNobleResult.median)}, tiny=${fmt(forkTinyResult.median)}, Scure=${fmt(scureResult.median)}, Official=${fmt(officialResult.median)}`,
438
522
  );
439
523
 
440
524
  rows.push({
@@ -442,6 +526,7 @@ async function scenarioP2wpkhSigning(): Promise<ComparisonRow[]> {
442
526
  detail: `${numInputs} inputs`,
443
527
  forkNoble: forkNobleResult,
444
528
  fork: forkTinyResult,
529
+ scure: scureResult,
445
530
  official: officialResult,
446
531
  });
447
532
  }
@@ -449,10 +534,10 @@ async function scenarioP2wpkhSigning(): Promise<ComparisonRow[]> {
449
534
  return rows;
450
535
  }
451
536
 
452
- // ─────────────────────────────────────────────────────────────────────────────
453
- // Scenario 4: P2TR Taproot Signing
454
- // ─────────────────────────────────────────────────────────────────────────────
455
-
537
+ /**
538
+ * Scenario 4: Measures P2TR Taproot (Schnorr, SegWit v1) transaction creation + signing
539
+ * across varying input counts (10-500).
540
+ */
456
541
  async function scenarioP2trSigning(): Promise<ComparisonRow[]> {
457
542
  console.log('\n--- Scenario 4: P2TR Taproot Signing ---\n');
458
543
 
@@ -463,7 +548,6 @@ async function scenarioP2trSigning(): Promise<ComparisonRow[]> {
463
548
  const iters = numInputs >= 500 ? 5 : numInputs >= 250 ? 10 : 30;
464
549
  const hashes = Array.from({ length: numInputs }, () => randomTxHash());
465
550
 
466
- // --- Fork with Noble ---
467
551
  initForkNoble();
468
552
  const nobleTweaked = getForkNobleTweakedSigner();
469
553
  const forkNobleXOnly = toXOnly(forkNobleKeyPair.publicKey);
@@ -494,7 +578,6 @@ async function scenarioP2trSigning(): Promise<ComparisonRow[]> {
494
578
  { iterations: iters },
495
579
  );
496
580
 
497
- // --- Fork with tiny-secp256k1 ---
498
581
  initForkTiny();
499
582
  const tinyTweaked = getForkTinyTweakedSigner();
500
583
  const forkTinyXOnly = toXOnly(forkTinyKeyPair.publicKey);
@@ -525,7 +608,33 @@ async function scenarioP2trSigning(): Promise<ComparisonRow[]> {
525
608
  { iterations: iters },
526
609
  );
527
610
 
528
- // --- Official with tiny-secp256k1 ---
611
+ const scureResult = await measure(
612
+ `Scure P2TR ${numInputs}`,
613
+ () => {
614
+ const witnessUtxo = makeTaprootWitnessUtxoScure(scureXOnlyPubKey);
615
+ const tx = new scureBtc.Transaction();
616
+ for (let i = 0; i < numInputs; i++) {
617
+ tx.addInput({
618
+ txid: hashes[i]!,
619
+ index: 0,
620
+ witnessUtxo: {
621
+ script: witnessUtxo.script,
622
+ amount: witnessUtxo.amount,
623
+ },
624
+ tapInternalKey: scureXOnlyPubKey,
625
+ });
626
+ }
627
+ for (let j = 0; j < 5; j++) {
628
+ tx.addOutput({
629
+ script: witnessUtxo.script,
630
+ amount: 10_000n,
631
+ });
632
+ }
633
+ tx.sign(scurePrivKey);
634
+ },
635
+ { iterations: iters },
636
+ );
637
+
529
638
  initOfficial();
530
639
  const officialTweaked = getOfficialTweakedSigner();
531
640
  const xonlyOfficial = bitcoin.toXOnly(officialKeyPair.publicKey);
@@ -554,7 +663,7 @@ async function scenarioP2trSigning(): Promise<ComparisonRow[]> {
554
663
  );
555
664
 
556
665
  console.log(
557
- ` ${String(numInputs).padStart(4)} inputs: Noble=${fmt(forkNobleResult.median)}, tiny=${fmt(forkTinyResult.median)}, Official=${fmt(officialResult.median)}`,
666
+ ` ${String(numInputs).padStart(4)} inputs: Noble=${fmt(forkNobleResult.median)}, tiny=${fmt(forkTinyResult.median)}, Scure=${fmt(scureResult.median)}, Official=${fmt(officialResult.median)}`,
558
667
  );
559
668
 
560
669
  rows.push({
@@ -562,6 +671,7 @@ async function scenarioP2trSigning(): Promise<ComparisonRow[]> {
562
671
  detail: `${numInputs} inputs`,
563
672
  forkNoble: forkNobleResult,
564
673
  fork: forkTinyResult,
674
+ scure: scureResult,
565
675
  official: officialResult,
566
676
  });
567
677
  }
@@ -569,10 +679,11 @@ async function scenarioP2trSigning(): Promise<ComparisonRow[]> {
569
679
  return rows;
570
680
  }
571
681
 
572
- // ─────────────────────────────────────────────────────────────────────────────
573
- // Scenario 5: End-to-End Lifecycle
574
- // ─────────────────────────────────────────────────────────────────────────────
575
-
682
+ /**
683
+ * Scenario 5: Measures the full end-to-end lifecycle with 100 inputs:
684
+ * create PSBT/tx, add inputs/outputs, sign, finalize, extract, serialize to hex.
685
+ * Tests both P2WPKH and P2TR for all libraries.
686
+ */
576
687
  async function scenarioEndToEnd(): Promise<ComparisonRow[]> {
577
688
  console.log('\n--- Scenario 5: End-to-End Lifecycle (100 inputs) ---\n');
578
689
 
@@ -580,7 +691,6 @@ async function scenarioEndToEnd(): Promise<ComparisonRow[]> {
580
691
  const iters = 20;
581
692
  const hashes = Array.from({ length: numInputs }, () => randomTxHash());
582
693
 
583
- // --- Fork Noble (P2WPKH) ---
584
694
  initForkNoble();
585
695
  const forkNobleE2E = await measure(
586
696
  'Fork Noble E2E P2WPKH',
@@ -611,7 +721,6 @@ async function scenarioEndToEnd(): Promise<ComparisonRow[]> {
611
721
  { iterations: iters },
612
722
  );
613
723
 
614
- // --- Fork tiny-secp256k1 (P2WPKH) ---
615
724
  initForkTiny();
616
725
  const forkTinyE2E = await measure(
617
726
  'Fork tiny E2E P2WPKH',
@@ -642,7 +751,34 @@ async function scenarioEndToEnd(): Promise<ComparisonRow[]> {
642
751
  { iterations: iters },
643
752
  );
644
753
 
645
- // --- Official (P2WPKH) ---
754
+ const scureE2E = await measure(
755
+ 'Scure E2E P2WPKH',
756
+ () => {
757
+ const witnessUtxo = makeWitnessUtxoScure(scurePubKey);
758
+ const tx = new scureBtc.Transaction();
759
+ for (let i = 0; i < numInputs; i++) {
760
+ tx.addInput({
761
+ txid: hashes[i]!,
762
+ index: 0,
763
+ witnessUtxo: {
764
+ script: witnessUtxo.script,
765
+ amount: witnessUtxo.amount,
766
+ },
767
+ });
768
+ }
769
+ for (let j = 0; j < 5; j++) {
770
+ tx.addOutput({
771
+ script: witnessUtxo.script,
772
+ amount: 10_000n,
773
+ });
774
+ }
775
+ tx.sign(scurePrivKey);
776
+ tx.finalize();
777
+ tx.hex;
778
+ },
779
+ { iterations: iters },
780
+ );
781
+
646
782
  initOfficial();
647
783
  const officialE2E = await measure(
648
784
  'Official E2E P2WPKH',
@@ -670,9 +806,8 @@ async function scenarioEndToEnd(): Promise<ComparisonRow[]> {
670
806
  { iterations: iters },
671
807
  );
672
808
 
673
- console.log(` P2WPKH 100in: Noble=${fmt(forkNobleE2E.median)}, tiny=${fmt(forkTinyE2E.median)}, Official=${fmt(officialE2E.median)}`);
809
+ console.log(` P2WPKH 100in: Noble=${fmt(forkNobleE2E.median)}, tiny=${fmt(forkTinyE2E.median)}, Scure=${fmt(scureE2E.median)}, Official=${fmt(officialE2E.median)}`);
674
810
 
675
- // --- Fork Noble (P2TR) ---
676
811
  initForkNoble();
677
812
  const e2eNobleTweaked = getForkNobleTweakedSigner();
678
813
  const e2eNobleXOnly = toXOnly(forkNobleKeyPair.publicKey);
@@ -706,7 +841,6 @@ async function scenarioEndToEnd(): Promise<ComparisonRow[]> {
706
841
  { iterations: iters },
707
842
  );
708
843
 
709
- // --- Fork tiny-secp256k1 (P2TR) ---
710
844
  initForkTiny();
711
845
  const e2eTinyTweaked = getForkTinyTweakedSigner();
712
846
  const e2eTinyXOnly = toXOnly(forkTinyKeyPair.publicKey);
@@ -740,7 +874,35 @@ async function scenarioEndToEnd(): Promise<ComparisonRow[]> {
740
874
  { iterations: iters },
741
875
  );
742
876
 
743
- // --- Official (P2TR) ---
877
+ const scureE2ETr = await measure(
878
+ 'Scure E2E P2TR',
879
+ () => {
880
+ const witnessUtxo = makeTaprootWitnessUtxoScure(scureXOnlyPubKey);
881
+ const tx = new scureBtc.Transaction();
882
+ for (let i = 0; i < numInputs; i++) {
883
+ tx.addInput({
884
+ txid: hashes[i]!,
885
+ index: 0,
886
+ witnessUtxo: {
887
+ script: witnessUtxo.script,
888
+ amount: witnessUtxo.amount,
889
+ },
890
+ tapInternalKey: scureXOnlyPubKey,
891
+ });
892
+ }
893
+ for (let j = 0; j < 5; j++) {
894
+ tx.addOutput({
895
+ script: witnessUtxo.script,
896
+ amount: 10_000n,
897
+ });
898
+ }
899
+ tx.sign(scurePrivKey);
900
+ tx.finalize();
901
+ tx.hex;
902
+ },
903
+ { iterations: iters },
904
+ );
905
+
744
906
  initOfficial();
745
907
  const e2eOfficialTweaked = getOfficialTweakedSigner();
746
908
  const xonly = bitcoin.toXOnly(officialKeyPair.publicKey);
@@ -771,7 +933,7 @@ async function scenarioEndToEnd(): Promise<ComparisonRow[]> {
771
933
  { iterations: iters },
772
934
  );
773
935
 
774
- console.log(` P2TR 100in: Noble=${fmt(forkNobleE2ETr.median)}, tiny=${fmt(forkTinyE2ETr.median)}, Official=${fmt(officialE2ETr.median)}`);
936
+ console.log(` P2TR 100in: Noble=${fmt(forkNobleE2ETr.median)}, tiny=${fmt(forkTinyE2ETr.median)}, Scure=${fmt(scureE2ETr.median)}, Official=${fmt(officialE2ETr.median)}`);
775
937
 
776
938
  return [
777
939
  {
@@ -779,6 +941,7 @@ async function scenarioEndToEnd(): Promise<ComparisonRow[]> {
779
941
  detail: '100 inputs',
780
942
  forkNoble: forkNobleE2E,
781
943
  fork: forkTinyE2E,
944
+ scure: scureE2E,
782
945
  official: officialE2E,
783
946
  },
784
947
  {
@@ -786,15 +949,16 @@ async function scenarioEndToEnd(): Promise<ComparisonRow[]> {
786
949
  detail: '100 inputs',
787
950
  forkNoble: forkNobleE2ETr,
788
951
  fork: forkTinyE2ETr,
952
+ scure: scureE2ETr,
789
953
  official: officialE2ETr,
790
954
  },
791
955
  ];
792
956
  }
793
957
 
794
- // ─────────────────────────────────────────────────────────────────────────────
795
- // Scenario 6: Parallel Signing (fork-only)
796
- // ─────────────────────────────────────────────────────────────────────────────
797
-
958
+ /**
959
+ * Scenario 6: Measures fork-exclusive parallel signing using `NodeWorkerSigningPool`
960
+ * with 4 worker threads. Compares sequential fork, parallel fork, and official sequential.
961
+ */
798
962
  async function scenarioParallelSigning(): Promise<{
799
963
  rows: { scenario: string; detail: string; result: BenchResult }[];
800
964
  results: Record<string, BenchResult>;
@@ -814,7 +978,6 @@ async function scenarioParallelSigning(): Promise<{
814
978
  const iters = numInputs >= 500 ? 5 : 10;
815
979
  const hashes = Array.from({ length: numInputs }, () => randomTxHash());
816
980
 
817
- // --- Fork Sequential ---
818
981
  initForkNoble();
819
982
  const seqResult = await measure(
820
983
  `Fork Sequential ${numInputs}`,
@@ -842,13 +1005,11 @@ async function scenarioParallelSigning(): Promise<{
842
1005
  { iterations: iters, warmup: 2 },
843
1006
  );
844
1007
 
845
- // --- Fork Parallel (4 workers) ---
846
1008
  NodeWorkerSigningPool.resetInstance();
847
1009
  const pool = NodeWorkerSigningPool.getInstance({ workerCount: 4 });
848
1010
  await pool.initialize();
849
1011
  pool.preserveWorkers();
850
1012
 
851
- // Warmup the pool
852
1013
  {
853
1014
  const warmupPsbt = new ForkPsbt({ network: forkNetworks.bitcoin });
854
1015
  const witnessUtxo = makeWitnessUtxoFork(forkNobleKeyPair.publicKey);
@@ -897,7 +1058,6 @@ async function scenarioParallelSigning(): Promise<{
897
1058
 
898
1059
  await pool.shutdown();
899
1060
 
900
- // --- Official Sequential (for comparison) ---
901
1061
  initOfficial();
902
1062
  const officialSeqResult = await measure(
903
1063
  `Official Sequential ${numInputs}`,
@@ -942,17 +1102,13 @@ async function scenarioParallelSigning(): Promise<{
942
1102
  return { rows, results };
943
1103
  }
944
1104
 
945
- // ─────────────────────────────────────────────────────────────────────────────
946
- // Main
947
- // ─────────────────────────────────────────────────────────────────────────────
948
-
949
- // dirname workaround for ESM
950
1105
  const __filename = fileURLToPath(import.meta.url);
951
1106
  const __dirname = dirname(__filename);
952
1107
 
1108
+ /** Runs all benchmark scenarios and prints comparison tables + JSON summary. */
953
1109
  async function main(): Promise<void> {
954
1110
  console.log('='.repeat(80));
955
- console.log(' @btc-vision/bitcoin vs bitcoinjs-lib — Benchmark Comparison');
1111
+ console.log(' @btc-vision/bitcoin vs bitcoinjs-lib vs @scure/btc-signer — Benchmark Comparison');
956
1112
  console.log(` Node ${process.version} | ${process.platform} ${process.arch}`);
957
1113
  console.log(` GC control: ${typeof globalThis.gc === 'function' ? 'YES' : 'NO (run with --expose-gc for best results)'}`);
958
1114
  console.log('='.repeat(80));
@@ -960,59 +1116,56 @@ async function main(): Promise<void> {
960
1116
  const allRows: ComparisonRow[] = [];
961
1117
  const allResults: Record<string, BenchResult> = {};
962
1118
 
963
- // Scenario 1: Initialization
964
1119
  const initRows = await scenarioInit();
965
1120
  allRows.push(...initRows);
966
1121
  for (const r of initRows) {
967
1122
  if (r.forkNoble) allResults[`init_fork_noble`] = r.forkNoble;
968
1123
  if (r.fork) allResults[`init_fork_tiny`] = r.fork;
1124
+ if (r.scure) allResults[`init_scure`] = r.scure;
969
1125
  if (r.official) allResults[`init_official`] = r.official;
970
1126
  }
971
1127
 
972
- // Scenario 2: PSBT Creation
973
1128
  const createRows = await scenarioPsbtCreation();
974
1129
  allRows.push(...createRows);
975
1130
  for (const r of createRows) {
976
1131
  if (r.forkNoble) allResults[`create_fork_${r.detail}`] = r.forkNoble;
1132
+ if (r.scure) allResults[`create_scure_${r.detail}`] = r.scure;
977
1133
  if (r.official) allResults[`create_official_${r.detail}`] = r.official;
978
1134
  }
979
1135
 
980
- // Scenario 3: P2WPKH Signing
981
1136
  const wpkhRows = await scenarioP2wpkhSigning();
982
1137
  allRows.push(...wpkhRows);
983
1138
  for (const r of wpkhRows) {
984
1139
  if (r.forkNoble) allResults[`wpkh_noble_${r.detail}`] = r.forkNoble;
985
1140
  if (r.fork) allResults[`wpkh_tiny_${r.detail}`] = r.fork;
1141
+ if (r.scure) allResults[`wpkh_scure_${r.detail}`] = r.scure;
986
1142
  if (r.official) allResults[`wpkh_official_${r.detail}`] = r.official;
987
1143
  }
988
1144
 
989
- // Scenario 4: P2TR Signing
990
1145
  const trRows = await scenarioP2trSigning();
991
1146
  allRows.push(...trRows);
992
1147
  for (const r of trRows) {
993
1148
  if (r.forkNoble) allResults[`tr_noble_${r.detail}`] = r.forkNoble;
994
1149
  if (r.fork) allResults[`tr_tiny_${r.detail}`] = r.fork;
1150
+ if (r.scure) allResults[`tr_scure_${r.detail}`] = r.scure;
995
1151
  if (r.official) allResults[`tr_official_${r.detail}`] = r.official;
996
1152
  }
997
1153
 
998
- // Scenario 5: End-to-End
999
1154
  const e2eRows = await scenarioEndToEnd();
1000
1155
  allRows.push(...e2eRows);
1001
1156
  for (const r of e2eRows) {
1002
1157
  if (r.forkNoble) allResults[`e2e_noble_${r.detail}_${r.scenario}`] = r.forkNoble;
1003
1158
  if (r.fork) allResults[`e2e_tiny_${r.detail}_${r.scenario}`] = r.fork;
1159
+ if (r.scure) allResults[`e2e_scure_${r.detail}_${r.scenario}`] = r.scure;
1004
1160
  if (r.official) allResults[`e2e_official_${r.detail}_${r.scenario}`] = r.official;
1005
1161
  }
1006
1162
 
1007
- // Scenario 6: Parallel Signing
1008
1163
  const parallelData = await scenarioParallelSigning();
1009
1164
  Object.assign(allResults, parallelData.results);
1010
1165
 
1011
- // ── Print summary tables ──
1012
- printComparison('Summary: @btc-vision/bitcoin vs bitcoinjs-lib', allRows);
1166
+ printComparison('Summary: @btc-vision/bitcoin vs bitcoinjs-lib vs @scure/btc-signer', allRows);
1013
1167
  printForkOnly('Parallel Signing (fork-exclusive)', parallelData.rows);
1014
1168
 
1015
- // ── JSON summary ──
1016
1169
  const summary = buildSummary(allResults);
1017
1170
  console.log('\n--- JSON Summary ---');
1018
1171
  console.log(JSON.stringify(summary, null, 2));