@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,302 @@
1
+ import assert from 'assert';
2
+ import { describe, it, beforeEach } from 'vitest';
3
+ import * as ecc from 'tiny-secp256k1';
4
+ import * as networks from '../src/networks.js';
5
+ import * as address from '../src/address.js';
6
+ import { fromBech32 } from '../src/bech32utils.js';
7
+ import { p2pkh } from '../src/payments/p2pkh.js';
8
+ import { p2wpkh } from '../src/payments/p2wpkh.js';
9
+ import { p2wsh } from '../src/payments/p2wsh.js';
10
+ import { p2tr } from '../src/payments/p2tr.js';
11
+ import type { EccLib } from '../src/index.js';
12
+ import { initEccLib } from '../src/index.js';
13
+ import { fromHex, toHex } from '../src/io/index.js';
14
+ import type { PublicKey, XOnlyPublicKey } from '../src/types.js';
15
+ import { toBytes32 } from '../src/types.js';
16
+
17
+ // A fixed compressed public key for deterministic tests
18
+ const PUBKEY = fromHex(
19
+ '030000000000000000000000000000000000000000000000000000000000000001',
20
+ ) as PublicKey;
21
+ // x-only form (drop the 03 prefix)
22
+ const XONLY_PUBKEY = PUBKEY.subarray(1) as XOnlyPublicKey;
23
+
24
+ describe('opnetTestnet network', () => {
25
+ beforeEach(() => {
26
+ initEccLib(ecc as unknown as EccLib);
27
+ });
28
+
29
+ describe('network constant', () => {
30
+ it('is exported and has correct bech32 prefix', () => {
31
+ assert.strictEqual(networks.opnetTestnet.bech32, 'opt');
32
+ });
33
+
34
+ it('has correct bech32Opnet prefix', () => {
35
+ assert.strictEqual(networks.opnetTestnet.bech32Opnet, 'opt');
36
+ });
37
+
38
+ it('bech32 and bech32Opnet are identical', () => {
39
+ assert.strictEqual(
40
+ networks.opnetTestnet.bech32,
41
+ networks.opnetTestnet.bech32Opnet,
42
+ );
43
+ });
44
+
45
+ it('has correct messagePrefix', () => {
46
+ assert.strictEqual(
47
+ networks.opnetTestnet.messagePrefix,
48
+ '\x18Bitcoin Signed Message:\n',
49
+ );
50
+ });
51
+
52
+ it('shares version bytes with testnet', () => {
53
+ assert.strictEqual(
54
+ networks.opnetTestnet.pubKeyHash,
55
+ networks.testnet.pubKeyHash,
56
+ );
57
+ assert.strictEqual(
58
+ networks.opnetTestnet.scriptHash,
59
+ networks.testnet.scriptHash,
60
+ );
61
+ assert.strictEqual(networks.opnetTestnet.wif, networks.testnet.wif);
62
+ });
63
+
64
+ it('shares bip32 values with testnet', () => {
65
+ assert.deepStrictEqual(
66
+ networks.opnetTestnet.bip32,
67
+ networks.testnet.bip32,
68
+ );
69
+ });
70
+
71
+ it('has different bech32 prefix than testnet', () => {
72
+ assert.notStrictEqual(
73
+ networks.opnetTestnet.bech32,
74
+ networks.testnet.bech32,
75
+ );
76
+ });
77
+
78
+ it('is accessible via the networks namespace', () => {
79
+ assert.ok(
80
+ (networks as Record<string, unknown>)['opnetTestnet'] !==
81
+ undefined,
82
+ );
83
+ });
84
+ });
85
+
86
+ describe('P2PKH (base58)', () => {
87
+ it('produces addresses starting with m or n (same as testnet)', () => {
88
+ const payment = p2pkh({
89
+ pubkey: PUBKEY,
90
+ network: networks.opnetTestnet,
91
+ });
92
+ assert.ok(payment.address);
93
+ assert.ok(
94
+ payment.address.startsWith('m') ||
95
+ payment.address.startsWith('n'),
96
+ `Expected address starting with m or n, got ${payment.address}`,
97
+ );
98
+ });
99
+
100
+ it('produces same address as testnet for same pubkey', () => {
101
+ const opnetAddr = p2pkh({
102
+ pubkey: PUBKEY,
103
+ network: networks.opnetTestnet,
104
+ }).address;
105
+ const testnetAddr = p2pkh({
106
+ pubkey: PUBKEY,
107
+ network: networks.testnet,
108
+ }).address;
109
+ assert.strictEqual(opnetAddr, testnetAddr);
110
+ });
111
+ });
112
+
113
+ describe('P2WPKH (segwit v0)', () => {
114
+ it('produces addresses starting with opt1q', () => {
115
+ const payment = p2wpkh({
116
+ pubkey: PUBKEY,
117
+ network: networks.opnetTestnet,
118
+ });
119
+ assert.ok(payment.address);
120
+ assert.ok(
121
+ payment.address.startsWith('opt1q'),
122
+ `Expected address starting with opt1q, got ${payment.address}`,
123
+ );
124
+ });
125
+
126
+ it('differs from testnet P2WPKH address (tb1q vs opt1q)', () => {
127
+ const opnetAddr = p2wpkh({
128
+ pubkey: PUBKEY,
129
+ network: networks.opnetTestnet,
130
+ }).address;
131
+ const testnetAddr = p2wpkh({
132
+ pubkey: PUBKEY,
133
+ network: networks.testnet,
134
+ }).address;
135
+ assert.ok(opnetAddr);
136
+ assert.ok(testnetAddr);
137
+ assert.ok(opnetAddr.startsWith('opt1q'));
138
+ assert.ok(testnetAddr.startsWith('tb1q'));
139
+ assert.notStrictEqual(opnetAddr, testnetAddr);
140
+ });
141
+
142
+ it('round-trips through toOutputScript and fromOutputScript', () => {
143
+ const payment = p2wpkh({
144
+ pubkey: PUBKEY,
145
+ network: networks.opnetTestnet,
146
+ });
147
+ const script = address.toOutputScript(
148
+ payment.address!,
149
+ networks.opnetTestnet,
150
+ );
151
+ const recovered = address.fromOutputScript(
152
+ script,
153
+ networks.opnetTestnet,
154
+ );
155
+ assert.strictEqual(recovered, payment.address);
156
+ });
157
+ });
158
+
159
+ describe('P2WSH (segwit v0)', () => {
160
+ it('produces addresses starting with opt1q', () => {
161
+ const hash = toBytes32(
162
+ fromHex(
163
+ 'ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5',
164
+ ),
165
+ );
166
+ const payment = p2wsh({
167
+ hash,
168
+ network: networks.opnetTestnet,
169
+ });
170
+ assert.ok(payment.address);
171
+ assert.ok(
172
+ payment.address.startsWith('opt1q'),
173
+ `Expected address starting with opt1q, got ${payment.address}`,
174
+ );
175
+ });
176
+ });
177
+
178
+ describe('P2TR (segwit v1, taproot)', () => {
179
+ it('produces addresses starting with opt1p', () => {
180
+ const payment = p2tr({
181
+ pubkey: XONLY_PUBKEY,
182
+ network: networks.opnetTestnet,
183
+ });
184
+ assert.ok(payment.address);
185
+ assert.ok(
186
+ payment.address.startsWith('opt1p'),
187
+ `Expected address starting with opt1p, got ${payment.address}`,
188
+ );
189
+ });
190
+
191
+ it('differs from testnet P2TR address (tb1p vs opt1p)', () => {
192
+ const opnetAddr = p2tr({
193
+ pubkey: XONLY_PUBKEY,
194
+ network: networks.opnetTestnet,
195
+ }).address;
196
+ const testnetAddr = p2tr({
197
+ pubkey: XONLY_PUBKEY,
198
+ network: networks.testnet,
199
+ }).address;
200
+ assert.ok(opnetAddr);
201
+ assert.ok(testnetAddr);
202
+ assert.ok(opnetAddr.startsWith('opt1p'));
203
+ assert.ok(testnetAddr.startsWith('tb1p'));
204
+ });
205
+
206
+ it('round-trips through toOutputScript and fromOutputScript', () => {
207
+ const payment = p2tr({
208
+ pubkey: XONLY_PUBKEY,
209
+ network: networks.opnetTestnet,
210
+ });
211
+ const script = address.toOutputScript(
212
+ payment.address!,
213
+ networks.opnetTestnet,
214
+ );
215
+ const recovered = address.fromOutputScript(
216
+ script,
217
+ networks.opnetTestnet,
218
+ );
219
+ assert.strictEqual(recovered, payment.address);
220
+ });
221
+ });
222
+
223
+ describe('bech32 encode/decode', () => {
224
+ it('toBech32 encodes with opt prefix for segwit v0', () => {
225
+ const hash = fromHex(
226
+ 'ea6d525c0c955d90d3dbd29a81ef8bfb79003727',
227
+ );
228
+ const encoded = address.toBech32(hash, 0, 'opt');
229
+ assert.ok(encoded.startsWith('opt1q'));
230
+ });
231
+
232
+ it('fromBech32 decodes opt-prefixed address', () => {
233
+ const hash = fromHex(
234
+ 'ea6d525c0c955d90d3dbd29a81ef8bfb79003727',
235
+ );
236
+ const encoded = address.toBech32(hash, 0, 'opt');
237
+ const decoded = fromBech32(encoded);
238
+ assert.strictEqual(decoded.prefix, 'opt');
239
+ assert.strictEqual(decoded.version, 0);
240
+ assert.strictEqual(toHex(decoded.data), toHex(hash));
241
+ });
242
+ });
243
+
244
+ describe('toOutputScript prefix validation', () => {
245
+ it('accepts opt-prefixed address for opnetTestnet', () => {
246
+ const payment = p2wpkh({
247
+ pubkey: PUBKEY,
248
+ network: networks.opnetTestnet,
249
+ });
250
+ assert.doesNotThrow(() => {
251
+ address.toOutputScript(
252
+ payment.address!,
253
+ networks.opnetTestnet,
254
+ );
255
+ });
256
+ });
257
+
258
+ it('rejects tb-prefixed address for opnetTestnet', () => {
259
+ const testnetPayment = p2wpkh({
260
+ pubkey: PUBKEY,
261
+ network: networks.testnet,
262
+ });
263
+ assert.throws(() => {
264
+ address.toOutputScript(
265
+ testnetPayment.address!,
266
+ networks.opnetTestnet,
267
+ );
268
+ }, /has an invalid prefix/);
269
+ });
270
+
271
+ it('accepts opt-prefixed address for testnet (opt is testnet bech32Opnet)', () => {
272
+ // testnet has bech32Opnet: 'opt', so opt-prefixed addresses are valid
273
+ const opnetPayment = p2wpkh({
274
+ pubkey: PUBKEY,
275
+ network: networks.opnetTestnet,
276
+ });
277
+ const script = address.toOutputScript(
278
+ opnetPayment.address!,
279
+ networks.testnet,
280
+ );
281
+ // Should produce the same output script regardless of which network decodes it
282
+ const scriptFromOpnet = address.toOutputScript(
283
+ opnetPayment.address!,
284
+ networks.opnetTestnet,
285
+ );
286
+ assert.deepStrictEqual(script, scriptFromOpnet);
287
+ });
288
+
289
+ it('rejects bc-prefixed address for opnetTestnet', () => {
290
+ const mainnetPayment = p2wpkh({
291
+ pubkey: PUBKEY,
292
+ network: networks.bitcoin,
293
+ });
294
+ assert.throws(() => {
295
+ address.toOutputScript(
296
+ mainnetPayment.address!,
297
+ networks.opnetTestnet,
298
+ );
299
+ }, /has an invalid prefix/);
300
+ });
301
+ });
302
+ });
@@ -19,6 +19,7 @@ import * as p2shModule from '../src/payments/p2sh.js';
19
19
  import * as p2wpkhModule from '../src/payments/p2wpkh.js';
20
20
  import * as p2wshModule from '../src/payments/p2wsh.js';
21
21
  import * as p2trModule from '../src/payments/p2tr.js';
22
+ import * as p2mrModule from '../src/payments/p2mr.js';
22
23
 
23
24
  const paymentModules: Record<string, { [key: string]: PaymentCreator }> = {
24
25
  embed: embedModule as any,
@@ -29,12 +30,13 @@ const paymentModules: Record<string, { [key: string]: PaymentCreator }> = {
29
30
  p2wpkh: p2wpkhModule as any,
30
31
  p2wsh: p2wshModule as any,
31
32
  p2tr: p2trModule as any,
33
+ p2mr: p2mrModule as any,
32
34
  };
33
35
 
34
36
  // Initialize ECC library at module load time
35
37
  initEccLib(ecc as unknown as EccLib);
36
38
 
37
- ['embed', 'p2ms', 'p2pk', 'p2pkh', 'p2sh', 'p2wpkh', 'p2wsh', 'p2tr'].forEach((p) => {
39
+ ['embed', 'p2ms', 'p2pk', 'p2pkh', 'p2sh', 'p2wpkh', 'p2wsh', 'p2tr', 'p2mr'].forEach((p) => {
38
40
  describe(p, () => {
39
41
  // Ensure ECC library is initialized before each test
40
42
  beforeEach(() => {
package/test/psbt.spec.ts CHANGED
@@ -5,8 +5,8 @@ import * as crypto from 'crypto';
5
5
  import { beforeEach, describe, it } from 'vitest';
6
6
 
7
7
  import { convertScriptTree } from './payments.utils.js';
8
- import { LEAF_VERSION_TAPSCRIPT } from '../src/payments/bip341.js';
9
- import { tapTreeFromList, tapTreeToList } from '../src/psbt/bip371.js';
8
+ import { LEAF_VERSION_TAPSCRIPT, tapleafHash } from '../src/payments/bip341.js';
9
+ import { isTaprootInput, isP2MRInput, tapTreeFromList, tapTreeToList } from '../src/psbt/bip371.js';
10
10
  import type { Bytes32, EccLib, MessageHash, PrivateKey, PublicKey, Satoshi, Script, Signature, Taptree, } from '../src/types.js';
11
11
  import type { HDSigner, Signer, SignerAsync, ValidateSigFunction } from '../src/index.js';
12
12
  import { initEccLib, networks, payments, Psbt } from '../src/index.js';
@@ -1535,4 +1535,299 @@ describe(`Psbt`, () => {
1535
1535
  assert.strictEqual(fresh.value, originalValue);
1536
1536
  });
1537
1537
  });
1538
+
1539
+ describe('P2MR (BIP 360) PSBT spending', () => {
1540
+ // P2MR (Pay-to-Merkle-Root) is SegWit v2, script-path only (no key-path).
1541
+ // These tests verify the full PSBT sign -> finalize -> extract flow for P2MR inputs.
1542
+
1543
+ const LEAF_VERSION = 0xc0;
1544
+ const OP_CHECKSIG = 0xac; // 172
1545
+ const OP_PUSHBYTES_32 = 0x20; // 32
1546
+
1547
+ function buildLeafScript(xOnlyPubkey: Uint8Array): Script {
1548
+ // <32-byte x-only pubkey> OP_CHECKSIG
1549
+ const script = new Uint8Array(34);
1550
+ script[0] = OP_PUSHBYTES_32;
1551
+ script.set(xOnlyPubkey, 1);
1552
+ script[33] = OP_CHECKSIG;
1553
+ return script as Script;
1554
+ }
1555
+
1556
+ function toXOnly(pubkey: Uint8Array): Uint8Array {
1557
+ return pubkey.length === 32 ? pubkey : pubkey.subarray(1, 33);
1558
+ }
1559
+
1560
+ it('detects P2MR inputs via isTaprootInput', () => {
1561
+ initEccLib(ecc as unknown as EccLib);
1562
+ const keyPair = ECPair.makeRandom();
1563
+ const xonly = toXOnly(keyPair.publicKey);
1564
+ const leafScript = buildLeafScript(xonly);
1565
+ const scriptTree = { output: leafScript, version: LEAF_VERSION };
1566
+
1567
+ const p2mrPayment = payments.p2mr({ scriptTree });
1568
+ assert(p2mrPayment.output, 'P2MR output should be defined');
1569
+
1570
+ const psbt = new Psbt();
1571
+ psbt.addInput({
1572
+ hash: '0000000000000000000000000000000000000000000000000000000000000000',
1573
+ index: 0,
1574
+ witnessUtxo: {
1575
+ value: 100_000n as Satoshi,
1576
+ script: p2mrPayment.output as Script,
1577
+ },
1578
+ });
1579
+
1580
+ const input = psbt.data.inputs[0];
1581
+ assert(input, 'Input should exist');
1582
+
1583
+ assert(isTaprootInput(input), 'P2MR input should be detected as taproot-style');
1584
+ assert(isP2MRInput(input), 'P2MR input should be detected as P2MR');
1585
+ });
1586
+
1587
+ it('signs and finalizes a single-leaf P2MR script-path spend', () => {
1588
+ initEccLib(ecc as unknown as EccLib);
1589
+ const keyPair = ECPair.makeRandom();
1590
+ const xonly = toXOnly(keyPair.publicKey);
1591
+ const leafScript = buildLeafScript(xonly);
1592
+
1593
+ const scriptTree = { output: leafScript, version: LEAF_VERSION };
1594
+ const p2mrPayment = payments.p2mr({
1595
+ scriptTree,
1596
+ redeem: { output: leafScript, redeemVersion: LEAF_VERSION },
1597
+ });
1598
+
1599
+ assert(p2mrPayment.output, 'P2MR output should be defined');
1600
+ assert(p2mrPayment.witness, 'P2MR witness should be defined');
1601
+ assert(p2mrPayment.hash, 'P2MR hash should be defined');
1602
+
1603
+ // Verify the output is SegWit v2 (OP_2 <32-byte hash>)
1604
+ assert.strictEqual(p2mrPayment.output[0], 0x52); // OP_2
1605
+ assert.strictEqual(p2mrPayment.output[1], 0x20); // PUSHBYTES_32
1606
+ assert.strictEqual(p2mrPayment.output.length, 34);
1607
+
1608
+ // Build the P2MR control block: [leafVersion | 0x01] (no merkle path for single leaf)
1609
+ const controlBlock = new Uint8Array([LEAF_VERSION | 0x01]);
1610
+
1611
+ // Compute the tapleaf hash
1612
+ const leafHash = tapleafHash({ output: leafScript, version: LEAF_VERSION });
1613
+
1614
+ // Create PSBT
1615
+ const psbt = new Psbt();
1616
+ psbt.addInput({
1617
+ hash: '0000000000000000000000000000000000000000000000000000000000000001',
1618
+ index: 0,
1619
+ witnessUtxo: {
1620
+ value: 100_000n as Satoshi,
1621
+ script: p2mrPayment.output as Script,
1622
+ },
1623
+ tapLeafScript: [
1624
+ {
1625
+ leafVersion: LEAF_VERSION,
1626
+ script: leafScript,
1627
+ controlBlock,
1628
+ },
1629
+ ],
1630
+ tapMerkleRoot: p2mrPayment.hash as Uint8Array,
1631
+ });
1632
+
1633
+ // Add a dummy output
1634
+ psbt.addOutput({
1635
+ address: 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq',
1636
+ value: 90_000n as Satoshi,
1637
+ });
1638
+
1639
+ // Sign the taproot input (routes to script-path signing for P2MR)
1640
+ psbt.signTaprootInput(0, keyPair, leafHash);
1641
+
1642
+ // Finalize
1643
+ psbt.finalizeInput(0);
1644
+
1645
+ // Extract
1646
+ const tx = psbt.extractTransaction(true);
1647
+ assert(tx, 'Transaction should be extractable');
1648
+
1649
+ // Verify witness structure: [signature, leaf_script, control_block]
1650
+ const witness = tx.ins[0]!.witness;
1651
+ assert.strictEqual(witness.length, 3, 'P2MR witness should have 3 elements');
1652
+
1653
+ // First element: Schnorr signature (64 or 65 bytes)
1654
+ assert(
1655
+ witness[0]!.length === 64 || witness[0]!.length === 65,
1656
+ `Signature should be 64 or 65 bytes, got ${witness[0]!.length}`,
1657
+ );
1658
+
1659
+ // Second element: leaf script
1660
+ assert(
1661
+ equals(witness[1]!, leafScript),
1662
+ 'Second witness element should be the leaf script',
1663
+ );
1664
+
1665
+ // Third element: P2MR control block (1 byte for single leaf, no merkle path)
1666
+ assert.strictEqual(
1667
+ witness[2]!.length,
1668
+ 1,
1669
+ 'Control block should be 1 byte for single-leaf P2MR',
1670
+ );
1671
+ assert.strictEqual(
1672
+ witness[2]![0],
1673
+ LEAF_VERSION | 0x01,
1674
+ 'Control byte should have parity bit 1',
1675
+ );
1676
+ });
1677
+
1678
+ it('signs and finalizes a two-leaf P2MR script-path spend', () => {
1679
+ initEccLib(ecc as unknown as EccLib);
1680
+ const keyPair1 = ECPair.makeRandom();
1681
+ const keyPair2 = ECPair.makeRandom();
1682
+ const xonly1 = toXOnly(keyPair1.publicKey);
1683
+ const xonly2 = toXOnly(keyPair2.publicKey);
1684
+
1685
+ const leafScriptA = buildLeafScript(xonly1);
1686
+ const leafScriptB = buildLeafScript(xonly2);
1687
+
1688
+ const scriptTree: [{ output: Script; version: number }, { output: Script; version: number }] = [
1689
+ { output: leafScriptA, version: LEAF_VERSION },
1690
+ { output: leafScriptB, version: LEAF_VERSION },
1691
+ ];
1692
+
1693
+ // Create P2MR payment to get the hash
1694
+ const p2mrPayment = payments.p2mr({
1695
+ scriptTree,
1696
+ redeem: { output: leafScriptB, redeemVersion: LEAF_VERSION },
1697
+ });
1698
+
1699
+ assert(p2mrPayment.output, 'P2MR output should be defined');
1700
+ assert(p2mrPayment.hash, 'P2MR hash should be defined');
1701
+ assert(p2mrPayment.witness, 'P2MR witness (with control block) should be defined');
1702
+
1703
+ // The witness from p2mr() gives us the correct control block for leafB
1704
+ // witness = [leafScript, controlBlock]
1705
+ const controlBlockB = p2mrPayment.witness![1]!;
1706
+ assert(controlBlockB.length === 33, 'Control block for leaf B in 2-leaf tree should be 1 + 32 bytes');
1707
+
1708
+ const leafHashB = tapleafHash({ output: leafScriptB, version: LEAF_VERSION });
1709
+
1710
+ // Create PSBT
1711
+ const psbt = new Psbt();
1712
+ psbt.addInput({
1713
+ hash: '0000000000000000000000000000000000000000000000000000000000000002',
1714
+ index: 0,
1715
+ witnessUtxo: {
1716
+ value: 100_000n as Satoshi,
1717
+ script: p2mrPayment.output as Script,
1718
+ },
1719
+ tapLeafScript: [
1720
+ {
1721
+ leafVersion: LEAF_VERSION,
1722
+ script: leafScriptB,
1723
+ controlBlock: controlBlockB,
1724
+ },
1725
+ ],
1726
+ tapMerkleRoot: p2mrPayment.hash as Uint8Array,
1727
+ });
1728
+
1729
+ psbt.addOutput({
1730
+ address: 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq',
1731
+ value: 90_000n as Satoshi,
1732
+ });
1733
+
1734
+ // Sign with keyPair2 (matches leafScriptB)
1735
+ psbt.signTaprootInput(0, keyPair2, leafHashB);
1736
+
1737
+ // Finalize
1738
+ psbt.finalizeInput(0);
1739
+
1740
+ // Extract
1741
+ const tx = psbt.extractTransaction(true);
1742
+ assert(tx, 'Transaction should be extractable');
1743
+
1744
+ // Verify witness structure: [signature, leaf_script, control_block]
1745
+ const witness = tx.ins[0]!.witness;
1746
+ assert.strictEqual(witness.length, 3, 'P2MR witness should have 3 elements');
1747
+
1748
+ // Control block: 1 + 32 bytes (control byte + one merkle path element)
1749
+ assert.strictEqual(witness[2]!.length, 33, 'Control block should be 33 bytes for 2-leaf tree');
1750
+ assert.strictEqual(
1751
+ witness[2]![0],
1752
+ LEAF_VERSION | 0x01,
1753
+ 'Control byte should have parity bit 1',
1754
+ );
1755
+ });
1756
+
1757
+ it('validates signatures of a P2MR input', () => {
1758
+ initEccLib(ecc as unknown as EccLib);
1759
+ const keyPair = ECPair.makeRandom();
1760
+ const xonly = toXOnly(keyPair.publicKey);
1761
+ const leafScript = buildLeafScript(xonly);
1762
+
1763
+ const scriptTree = { output: leafScript, version: LEAF_VERSION };
1764
+ const p2mrPayment = payments.p2mr({
1765
+ scriptTree,
1766
+ redeem: { output: leafScript, redeemVersion: LEAF_VERSION },
1767
+ });
1768
+
1769
+ const controlBlock = new Uint8Array([LEAF_VERSION | 0x01]);
1770
+
1771
+ const leafHash = tapleafHash({ output: leafScript, version: LEAF_VERSION });
1772
+
1773
+ const psbt = new Psbt();
1774
+ psbt.addInput({
1775
+ hash: '0000000000000000000000000000000000000000000000000000000000000003',
1776
+ index: 0,
1777
+ witnessUtxo: {
1778
+ value: 100_000n as Satoshi,
1779
+ script: p2mrPayment.output as Script,
1780
+ },
1781
+ tapLeafScript: [
1782
+ {
1783
+ leafVersion: LEAF_VERSION,
1784
+ script: leafScript,
1785
+ controlBlock,
1786
+ },
1787
+ ],
1788
+ tapMerkleRoot: p2mrPayment.hash as Uint8Array,
1789
+ });
1790
+
1791
+ psbt.addOutput({
1792
+ address: 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq',
1793
+ value: 90_000n as Satoshi,
1794
+ });
1795
+
1796
+ psbt.signTaprootInput(0, keyPair, leafHash);
1797
+
1798
+ // Validate signatures
1799
+ const isValid = psbt.validateSignaturesOfInput(0, schnorrValidator);
1800
+ assert(isValid, 'P2MR signature should be valid');
1801
+ });
1802
+
1803
+ it('rejects key-path finalization for P2MR inputs', () => {
1804
+ initEccLib(ecc as unknown as EccLib);
1805
+ const keyPair = ECPair.makeRandom();
1806
+ const xonly = toXOnly(keyPair.publicKey);
1807
+ const leafScript = buildLeafScript(xonly);
1808
+
1809
+ const scriptTree = { output: leafScript, version: LEAF_VERSION };
1810
+ const p2mrPayment = payments.p2mr({ scriptTree });
1811
+
1812
+ const psbt = new Psbt();
1813
+ psbt.addInput({
1814
+ hash: '0000000000000000000000000000000000000000000000000000000000000004',
1815
+ index: 0,
1816
+ witnessUtxo: {
1817
+ value: 100_000n as Satoshi,
1818
+ script: p2mrPayment.output as Script,
1819
+ },
1820
+ });
1821
+
1822
+ psbt.addOutput({
1823
+ address: 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq',
1824
+ value: 90_000n as Satoshi,
1825
+ });
1826
+
1827
+ // Without tapLeafScript, finalization should fail (no script-path data)
1828
+ assert.throws(() => {
1829
+ psbt.finalizeInput(0);
1830
+ }, /Can not finalize taproot input #0/);
1831
+ });
1832
+ });
1538
1833
  });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "target": "ESNext",
4
- "module": "nodenext",
4
+ "module": "ESNext",
5
5
  "moduleResolution": "nodenext",
6
6
  "outDir": "../",
7
7
  "declaration": false,
@@ -9,7 +9,7 @@
9
9
  "rootDir": "../",
10
10
  "rootDirs": ["../src", "../types"],
11
11
  "types": ["node"],
12
- "lib": ["ES2021"],
12
+ "lib": ["ESNext"],
13
13
  "resolvePackageJsonImports": true,
14
14
  "allowJs": false,
15
15
  "resolveJsonModule": true,