@bsv/sdk 2.0.12 → 2.0.13

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 (77) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/auth/clients/__tests__/AuthFetch.additional.test.js +827 -0
  3. package/dist/cjs/src/auth/clients/__tests__/AuthFetch.additional.test.js.map +1 -0
  4. package/dist/cjs/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js +654 -0
  5. package/dist/cjs/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js.map +1 -0
  6. package/dist/cjs/src/transaction/MerklePath.js +132 -0
  7. package/dist/cjs/src/transaction/MerklePath.js.map +1 -1
  8. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  9. package/dist/esm/src/auth/clients/__tests__/AuthFetch.additional.test.js +825 -0
  10. package/dist/esm/src/auth/clients/__tests__/AuthFetch.additional.test.js.map +1 -0
  11. package/dist/esm/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js +619 -0
  12. package/dist/esm/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js.map +1 -0
  13. package/dist/esm/src/transaction/MerklePath.js +132 -0
  14. package/dist/esm/src/transaction/MerklePath.js.map +1 -1
  15. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  16. package/dist/types/src/auth/clients/__tests__/AuthFetch.additional.test.d.ts +21 -0
  17. package/dist/types/src/auth/clients/__tests__/AuthFetch.additional.test.d.ts.map +1 -0
  18. package/dist/types/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.d.ts +2 -0
  19. package/dist/types/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.d.ts.map +1 -0
  20. package/dist/types/src/transaction/MerklePath.d.ts +27 -0
  21. package/dist/types/src/transaction/MerklePath.d.ts.map +1 -1
  22. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  23. package/dist/umd/bundle.js +1 -1
  24. package/dist/umd/bundle.js.map +1 -1
  25. package/docs/reference/storage.md +1 -1
  26. package/docs/reference/transaction.md +40 -0
  27. package/package.json +1 -1
  28. package/src/auth/clients/__tests__/AuthFetch.additional.test.ts +1131 -0
  29. package/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.ts +770 -0
  30. package/src/compat/__tests/Mnemonic.additional.test.ts +64 -0
  31. package/src/identity/__tests/IdentityClient.additional.test.ts +767 -0
  32. package/src/kvstore/__tests/LocalKVStore.additional.test.ts +611 -0
  33. package/src/kvstore/__tests/kvStoreInterpreter.test.ts +327 -0
  34. package/src/overlay-tools/__tests/HostReputationTracker.additional.test.ts +561 -0
  35. package/src/overlay-tools/__tests/LookupResolver.additional.test.ts +612 -0
  36. package/src/overlay-tools/__tests/withDoubleSpendRetry.test.ts +278 -0
  37. package/src/primitives/__tests/BigNumber.additional.test.ts +79 -0
  38. package/src/primitives/__tests/Curve.additional.test.ts +208 -0
  39. package/src/primitives/__tests/ECDSA.additional.test.ts +122 -0
  40. package/src/primitives/__tests/Hash.additional.test.ts +59 -0
  41. package/src/primitives/__tests/JacobianPoint.test.ts +308 -0
  42. package/src/primitives/__tests/Point.additional.test.ts +503 -0
  43. package/src/primitives/__tests/PublicKey.additional.test.ts +383 -0
  44. package/src/primitives/__tests/Random.additional.test.ts +262 -0
  45. package/src/primitives/__tests/Signature.test.ts +333 -0
  46. package/src/primitives/__tests/TransactionSignature.additional.test.ts +241 -0
  47. package/src/registry/__tests/RegistryClient.additional.test.ts +750 -0
  48. package/src/remittance/__tests/BasicBRC29.additional.test.ts +657 -0
  49. package/src/remittance/__tests/RemittanceManager.additional.test.ts +1272 -0
  50. package/src/script/__tests/LockingUnlockingScript.test.ts +79 -0
  51. package/src/script/__tests/Script.additional.test.ts +100 -0
  52. package/src/script/__tests/ScriptEvaluationError.test.ts +98 -0
  53. package/src/script/__tests/Spend.additional.test.ts +837 -0
  54. package/src/script/templates/__tests/RPuzzle.test.ts +134 -0
  55. package/src/transaction/MerklePath.ts +155 -0
  56. package/src/transaction/__tests/BeefParty.additional.test.ts +22 -0
  57. package/src/transaction/__tests/Broadcaster.test.ts +159 -0
  58. package/src/transaction/__tests/MerklePath.bench.test.ts +105 -0
  59. package/src/transaction/__tests/MerklePath.test.ts +80 -0
  60. package/src/transaction/__tests/Transaction.additional.test.ts +225 -0
  61. package/src/transaction/broadcasters/__tests/ARC.additional.test.ts +585 -0
  62. package/src/transaction/broadcasters/__tests/Teranode.test.ts +349 -0
  63. package/src/transaction/chaintrackers/__tests/BlockHeadersService.test.ts +253 -0
  64. package/src/transaction/chaintrackers/__tests/DefaultChainTracker.test.ts +44 -0
  65. package/src/transaction/chaintrackers/__tests/WhatsOnChain.additional.test.ts +193 -0
  66. package/src/transaction/fee-models/__tests/SatoshisPerKilobyte.test.ts +262 -0
  67. package/src/transaction/http/__tests/BinaryFetchClient.test.ts +212 -0
  68. package/src/transaction/http/__tests/DefaultHttpClient.additional.test.ts +192 -0
  69. package/src/transaction/http/__tests/DefaultHttpClient.test.ts +71 -0
  70. package/src/wallet/__tests/ProtoWallet.additional.test.ts +134 -0
  71. package/src/wallet/__tests/WERR.test.ts +212 -0
  72. package/src/wallet/__tests/WalletClient.additional.test.ts +699 -0
  73. package/src/wallet/__tests/WalletClient.substrate.test.ts +759 -0
  74. package/src/wallet/__tests/WalletError.test.ts +290 -0
  75. package/src/wallet/__tests/validationHelpers.test.ts +1218 -0
  76. package/src/wallet/substrates/__tests/HTTPWalletJSON.test.ts +496 -0
  77. package/src/wallet/substrates/__tests/HTTPWalletWire.test.ts +273 -0
@@ -0,0 +1,837 @@
1
+ import Spend from '../../script/Spend'
2
+ import LockingScript from '../../script/LockingScript'
3
+ import UnlockingScript from '../../script/UnlockingScript'
4
+ import BigNumber from '../../primitives/BigNumber'
5
+ import OP from '../../script/OP'
6
+ import ScriptChunk from '../../script/ScriptChunk'
7
+ import PrivateKey from '../../primitives/PrivateKey'
8
+ import PublicKey from '../../primitives/PublicKey'
9
+ import Transaction from '../../transaction/Transaction'
10
+ import P2PKH from '../../script/templates/P2PKH'
11
+
12
+ const ZERO_TXID = '0'.repeat(64)
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Helpers
16
+ // ---------------------------------------------------------------------------
17
+
18
+ function makeSpend (
19
+ lockingChunks: ScriptChunk[],
20
+ unlockingChunks: ScriptChunk[] = [],
21
+ opts: { transactionVersion?: number, memoryLimit?: number, isRelaxed?: boolean } = {}
22
+ ): Spend {
23
+ return new Spend({
24
+ sourceTXID: ZERO_TXID,
25
+ sourceOutputIndex: 0,
26
+ sourceSatoshis: 1,
27
+ lockingScript: new LockingScript(lockingChunks),
28
+ transactionVersion: opts.transactionVersion ?? 1,
29
+ otherInputs: [],
30
+ outputs: [],
31
+ inputIndex: 0,
32
+ unlockingScript: new UnlockingScript(unlockingChunks),
33
+ inputSequence: 0xffffffff,
34
+ lockTime: 0,
35
+ memoryLimit: opts.memoryLimit,
36
+ isRelaxed: opts.isRelaxed
37
+ })
38
+ }
39
+
40
+ /** Minimal-push helper that respects script number semantics. */
41
+ function pushChunk (data: number[]): ScriptChunk {
42
+ if (data.length === 0) return { op: OP.OP_0 }
43
+ if (data.length === 1 && data[0] >= 1 && data[0] <= 16) {
44
+ return { op: OP.OP_1 + (data[0] - 1) }
45
+ }
46
+ if (data.length === 1 && data[0] === 0x81) return { op: OP.OP_1NEGATE }
47
+ if (data.length <= 75) return { op: data.length, data }
48
+ if (data.length <= 255) return { op: OP.OP_PUSHDATA1, data }
49
+ if (data.length <= 65535) return { op: OP.OP_PUSHDATA2, data }
50
+ return { op: OP.OP_PUSHDATA4, data }
51
+ }
52
+
53
+ /** scriptnum bytes for an integer. */
54
+ function scriptNum (n: number): number[] {
55
+ return new BigNumber(n).toScriptNum()
56
+ }
57
+
58
+ /** Fast way to make a Spend in relaxed mode with a locking-script-only test. */
59
+ function makeLocking (
60
+ lockingChunks: ScriptChunk[],
61
+ opts: { memoryLimit?: number } = {}
62
+ ): Spend {
63
+ return makeSpend(lockingChunks, [], { isRelaxed: true, memoryLimit: opts.memoryLimit })
64
+ }
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Memory limit checks (lines 238, 246, 383-384, 387-388)
68
+ // ---------------------------------------------------------------------------
69
+ describe('Spend – memory limit enforcement', () => {
70
+ it('step() throws when stackMem already exceeds memoryLimit', () => {
71
+ const spend = makeLocking([{ op: OP.OP_1 }], { memoryLimit: 0 })
72
+ spend.context = 'LockingScript'
73
+ spend.stackMem = 1 // artificially exceeded
74
+ expect(() => spend.step()).toThrow('Stack memory usage has exceeded')
75
+ })
76
+
77
+ it('step() throws when altStackMem already exceeds memoryLimit', () => {
78
+ const spend = makeLocking([{ op: OP.OP_1 }], { memoryLimit: 0 })
79
+ spend.context = 'LockingScript'
80
+ spend.altStackMem = 1
81
+ expect(() => spend.step()).toThrow('Alt stack memory usage has exceeded')
82
+ })
83
+
84
+ it('pushStack throws when additional bytes would exceed memoryLimit', () => {
85
+ // OP_1 calls pushStackCopy with a 1-byte item; memoryLimit=0 so 0+1>0
86
+ const spend = makeLocking([{ op: OP.OP_1 }], { memoryLimit: 0 })
87
+ spend.context = 'LockingScript'
88
+ expect(() => spend.step()).toThrow('Stack memory usage has exceeded')
89
+ })
90
+
91
+ it('pushAltStack throws when additional bytes would exceed memoryLimit', () => {
92
+ const spend = makeLocking([{ op: OP.OP_TOALTSTACK }], { memoryLimit: 0 })
93
+ spend.context = 'LockingScript'
94
+ // Pre-populate stack without going through ensureStackMem
95
+ spend.stack = [[1]]
96
+ spend.stackMem = 0 // bypass step() initial guard (0 > 0 = false)
97
+ expect(() => spend.step()).toThrow('Alt stack memory usage has exceeded')
98
+ })
99
+ })
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // step() structural edge-cases (lines 401, 407, 417)
103
+ // ---------------------------------------------------------------------------
104
+ describe('Spend – step() structural edge-cases', () => {
105
+ it('step() returns false when locking script is exhausted', () => {
106
+ // Empty locking script: after unlocking script finishes, step() returns false
107
+ const spend = makeSpend([], [pushChunk([1])], { isRelaxed: true })
108
+ expect(spend.step()).toBe(true) // process unlocking push
109
+ expect(spend.step()).toBe(false) // locking is empty → return false
110
+ })
111
+
112
+ it('step() throws for non-minimal push in strict mode', () => {
113
+ // data=[0x01] should be pushed with OP_1 (0x51), not op=1
114
+ const nonMinimal: ScriptChunk = { op: 1, data: [0x01] }
115
+ const spend = makeSpend([nonMinimal], [], { memoryLimit: 1000 })
116
+ spend.context = 'LockingScript'
117
+ expect(() => spend.step()).toThrow('not minimally-encoded')
118
+ })
119
+
120
+ it('step() processes non-minimal push when isRelaxed=true', () => {
121
+ const nonMinimal: ScriptChunk = { op: 1, data: [0x01] }
122
+ const spend = makeSpend([nonMinimal], [], { isRelaxed: true, memoryLimit: 1000 })
123
+ spend.context = 'LockingScript'
124
+ expect(spend.step()).toBe(true)
125
+ expect(spend.stack[0]).toEqual([0x01])
126
+ })
127
+ })
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // validate() error paths (lines 1103, 1118, 1125, 1132, 1136)
131
+ // ---------------------------------------------------------------------------
132
+ describe('Spend – validate() error paths', () => {
133
+ it('throws when unlocking script is not push-only in strict mode', () => {
134
+ // OP_DROP is not a push opcode
135
+ const spend = makeSpend(
136
+ [{ op: OP.OP_1 }],
137
+ [{ op: OP.OP_DROP }]
138
+ )
139
+ expect(() => spend.validate()).toThrow('Unlocking scripts can only contain push operations')
140
+ })
141
+
142
+ it('throws when OP_IF is not closed with OP_ENDIF', () => {
143
+ // OP_1 OP_IF OP_1 ← no OP_ENDIF
144
+ const spend = makeLocking([
145
+ { op: OP.OP_1 },
146
+ { op: OP.OP_IF },
147
+ { op: OP.OP_1 }
148
+ ])
149
+ expect(() => spend.validate()).toThrow('OP_IF')
150
+ })
151
+
152
+ it('throws clean-stack violation when more than one item left', () => {
153
+ // Non-relaxed mode: exactly 1 item is required
154
+ const spend = makeSpend([{ op: OP.OP_1 }, { op: OP.OP_1 }])
155
+ expect(() => spend.validate()).toThrow('clean stack')
156
+ })
157
+
158
+ it('throws when stack is empty after execution', () => {
159
+ // OP_DROP leaves stack empty in relaxed mode
160
+ const spend = makeLocking([
161
+ { op: OP.OP_1 },
162
+ { op: OP.OP_DROP }
163
+ ])
164
+ expect(() => spend.validate()).toThrow('stack is empty')
165
+ })
166
+
167
+ it('throws when top stack item is falsy', () => {
168
+ const spend = makeLocking([{ op: OP.OP_0 }])
169
+ expect(() => spend.validate()).toThrow('top stack element must be truthy')
170
+ })
171
+
172
+ it('castToBool returns false for negative-zero sentinel [0x80]', () => {
173
+ // Push [0x80] (negative zero) – should be falsy
174
+ const spend = makeLocking([pushChunk([0x80])])
175
+ expect(() => spend.validate()).toThrow('top stack element must be truthy')
176
+ })
177
+ })
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // isChunkMinimalPushHelper – data.length 256-65535 path (line 97-98)
181
+ // ---------------------------------------------------------------------------
182
+ describe('Spend – isChunkMinimalPushHelper 256-65535 byte data', () => {
183
+ it('rejects PUSHDATA4 for 256-byte data (should use PUSHDATA2)', () => {
184
+ const data = new Array(256).fill(0x42)
185
+ // Using OP_PUSHDATA4 for 256-byte data is non-minimal (should be PUSHDATA2)
186
+ const badPush: ScriptChunk = { op: OP.OP_PUSHDATA4, data }
187
+ const spend = makeSpend([badPush], [], { memoryLimit: 10000000 })
188
+ spend.context = 'LockingScript'
189
+ expect(() => spend.step()).toThrow('not minimally-encoded')
190
+ })
191
+
192
+ it('accepts PUSHDATA2 for 256-byte data', () => {
193
+ const data = new Array(256).fill(0x42)
194
+ const goodPush: ScriptChunk = { op: OP.OP_PUSHDATA2, data }
195
+ const spend = makeSpend([goodPush, { op: OP.OP_DROP }, { op: OP.OP_1 }], [], { memoryLimit: 10000000 })
196
+ expect(spend.validate()).toBe(true)
197
+ })
198
+ })
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // checkPublicKeyEncoding paths (lines 328-353)
202
+ // ---------------------------------------------------------------------------
203
+ describe('Spend – checkPublicKeyEncoding error paths', () => {
204
+ function checksigSpend (pubkeyBytes: number[], sigBytes: number[]): Spend {
205
+ return makeLocking([
206
+ pushChunk(sigBytes),
207
+ pushChunk(pubkeyBytes),
208
+ { op: OP.OP_CHECKSIG }
209
+ ])
210
+ }
211
+
212
+ it('throws when pubkey is empty', () => {
213
+ expect(() => checksigSpend([], []).validate()).toThrow('Public key is empty')
214
+ })
215
+
216
+ it('throws when pubkey is too short (< 33 bytes)', () => {
217
+ const shortKey = new Array(32).fill(0x02)
218
+ expect(() => checksigSpend(shortKey, []).validate()).toThrow('too short')
219
+ })
220
+
221
+ it('throws when 0x04 pubkey is not 65 bytes', () => {
222
+ const wrongLen = [0x04, ...new Array(33).fill(0x00)]
223
+ expect(() => checksigSpend(wrongLen, []).validate()).toThrow('non-compressed public key must be 65 bytes')
224
+ })
225
+
226
+ it('throws when 0x02 pubkey is not 33 bytes', () => {
227
+ const wrongLen = [0x02, ...new Array(34).fill(0x00)]
228
+ expect(() => checksigSpend(wrongLen, []).validate()).toThrow('compressed public key must be 33 bytes')
229
+ })
230
+
231
+ it('throws when 0x03 pubkey is not 33 bytes', () => {
232
+ const wrongLen = [0x03, ...new Array(34).fill(0x00)]
233
+ expect(() => checksigSpend(wrongLen, []).validate()).toThrow('compressed public key must be 33 bytes')
234
+ })
235
+
236
+ it('throws when pubkey prefix is unknown', () => {
237
+ const unknown = new Array(33).fill(0x00)
238
+ unknown[0] = 0x05
239
+ expect(() => checksigSpend(unknown, []).validate()).toThrow('unknown format')
240
+ })
241
+
242
+ it('throws when pubkey prefix is 0x04 but coordinates are invalid', () => {
243
+ // 65-byte 0x04 with all zeros – valid length but invalid curve point
244
+ const bad = [0x04, ...new Array(64).fill(0x00)]
245
+ expect(() => checksigSpend(bad, []).validate()).toThrow()
246
+ })
247
+ })
248
+
249
+ // ---------------------------------------------------------------------------
250
+ // checkSignatureEncoding paths (lines 310-321)
251
+ // ---------------------------------------------------------------------------
252
+ describe('Spend – checkSignatureEncoding error paths', () => {
253
+ it('throws when sig has invalid DER format (wrong first byte)', () => {
254
+ const privKey = PrivateKey.fromRandom()
255
+ const pubKey = PublicKey.fromPrivateKey(privKey)
256
+ const badSig = [0x31, 0x06, 0x02, 0x01, 0x01, 0x02, 0x01, 0x01, 0x01]
257
+ const spend = makeLocking([
258
+ pushChunk(badSig),
259
+ pushChunk(pubKey.toDER() as number[]),
260
+ { op: OP.OP_CHECKSIG }
261
+ ])
262
+ expect(() => spend.validate()).toThrow('signature format is invalid')
263
+ })
264
+
265
+ it('CHECKSIG with empty sig returns false result without error', () => {
266
+ const privKey = PrivateKey.fromRandom()
267
+ const pubKey = PublicKey.fromPrivateKey(privKey)
268
+ // empty sig → fSuccess stays false → pushes [] but still truthy check fails
269
+ const spend = makeLocking([
270
+ { op: OP.OP_0 }, // empty sig
271
+ pushChunk(pubKey.toDER() as number[]),
272
+ { op: OP.OP_CHECKSIG } // pushes [] (false)
273
+ // Stack is [[]] which is falsy → validate throws
274
+ ])
275
+ expect(() => spend.validate()).toThrow('top stack element must be truthy')
276
+ })
277
+
278
+ it('CHECKSIGVERIFY throws when sig is empty (fSuccess = false)', () => {
279
+ const privKey = PrivateKey.fromRandom()
280
+ const pubKey = PublicKey.fromPrivateKey(privKey)
281
+ const spend = makeLocking([
282
+ { op: OP.OP_0 }, // empty sig
283
+ pushChunk(pubKey.toDER() as number[]),
284
+ { op: OP.OP_CHECKSIGVERIFY },
285
+ { op: OP.OP_1 }
286
+ ])
287
+ expect(() => spend.validate()).toThrow('OP_CHECKSIGVERIFY requires')
288
+ })
289
+
290
+ it('CHECKSIGVERIFY succeeds and pops result for valid sig', async () => {
291
+ const privKey = PrivateKey.fromRandom()
292
+ const pubKey = PublicKey.fromPrivateKey(privKey)
293
+ const p2pkh = new P2PKH()
294
+ const hash = pubKey.toHash()
295
+ const lockingScript = p2pkh.lock(hash)
296
+ const sourceTx = new Transaction(1, [], [{ lockingScript, satoshis: 1 }], 0)
297
+ const spendTx = new Transaction(
298
+ 1,
299
+ [{ sourceTransaction: sourceTx, sourceOutputIndex: 0, sequence: 0xffffffff }],
300
+ [],
301
+ 0
302
+ )
303
+ const unlockingScript = await p2pkh.unlock(privKey).sign(spendTx, 0)
304
+ const spend = new Spend({
305
+ sourceTXID: sourceTx.id('hex'),
306
+ sourceOutputIndex: 0,
307
+ sourceSatoshis: 1,
308
+ lockingScript,
309
+ transactionVersion: 1,
310
+ otherInputs: [],
311
+ outputs: [],
312
+ inputIndex: 0,
313
+ unlockingScript,
314
+ inputSequence: 0xffffffff,
315
+ lockTime: 0
316
+ })
317
+ expect(spend.validate()).toBe(true)
318
+ })
319
+ })
320
+
321
+ // ---------------------------------------------------------------------------
322
+ // OP_CODESEPARATOR (lines 861-862)
323
+ // ---------------------------------------------------------------------------
324
+ describe('Spend – OP_CODESEPARATOR', () => {
325
+ it('sets lastCodeSeparator to current programCounter', () => {
326
+ const spend = makeLocking([
327
+ { op: OP.OP_CODESEPARATOR },
328
+ { op: OP.OP_1 }
329
+ ])
330
+ expect(spend.validate()).toBe(true)
331
+ // lastCodeSeparator was set during execution
332
+ })
333
+ })
334
+
335
+ // ---------------------------------------------------------------------------
336
+ // OP_VER (lines 432-435)
337
+ // ---------------------------------------------------------------------------
338
+ describe('Spend – OP_VER', () => {
339
+ it('pushes transaction version as 4-byte LE', () => {
340
+ // transactionVersion=2, LE=[2,0,0,0]; compare with that value
341
+ const spend = makeLocking([
342
+ { op: OP.OP_VER },
343
+ pushChunk([2, 0, 0, 0]),
344
+ { op: OP.OP_EQUAL }
345
+ ], { memoryLimit: 100000 })
346
+ // isRelaxed already set, transactionVersion defaults to 1 via makeLocking
347
+ // Redo with version=2
348
+ const spend2 = makeSpend([
349
+ { op: OP.OP_VER },
350
+ pushChunk([2, 0, 0, 0]),
351
+ { op: OP.OP_EQUAL }
352
+ ], [], { isRelaxed: true, transactionVersion: 2, memoryLimit: 100000 })
353
+ expect(spend2.validate()).toBe(true)
354
+ })
355
+ })
356
+
357
+ // ---------------------------------------------------------------------------
358
+ // OP_SUBSTR (lines 436-449)
359
+ // ---------------------------------------------------------------------------
360
+ describe('Spend – OP_SUBSTR', () => {
361
+ it('extracts a substring from a buffer', () => {
362
+ // "hello" starting at offset 1, length 3 → "ell"
363
+ const hello = [0x68, 0x65, 0x6c, 0x6c, 0x6f]
364
+ const ell = [0x65, 0x6c, 0x6c]
365
+ const spend = makeSpend([
366
+ pushChunk(hello),
367
+ pushChunk(scriptNum(1)),
368
+ pushChunk(scriptNum(3)),
369
+ { op: OP.OP_SUBSTR },
370
+ pushChunk(ell),
371
+ { op: OP.OP_EQUAL }
372
+ ], [], { isRelaxed: true, memoryLimit: 100000 })
373
+ expect(spend.validate()).toBe(true)
374
+ })
375
+
376
+ it('throws when OP_SUBSTR offset is out of range', () => {
377
+ const buf = [0x01, 0x02, 0x03]
378
+ const spend = makeSpend([
379
+ pushChunk(buf),
380
+ pushChunk(scriptNum(5)), // offset >= size → error
381
+ pushChunk(scriptNum(1)),
382
+ { op: OP.OP_SUBSTR },
383
+ { op: OP.OP_1 }
384
+ ], [], { isRelaxed: true, memoryLimit: 100000 })
385
+ expect(() => spend.validate()).toThrow('OP_SUBSTR')
386
+ })
387
+ })
388
+
389
+ // ---------------------------------------------------------------------------
390
+ // OP_LEFT / OP_RIGHT (lines 450-474)
391
+ // ---------------------------------------------------------------------------
392
+ describe('Spend – OP_LEFT / OP_RIGHT', () => {
393
+ it('OP_LEFT extracts the first N bytes', () => {
394
+ const buf = [0x01, 0x02, 0x03, 0x04]
395
+ const spend = makeSpend([
396
+ pushChunk(buf),
397
+ pushChunk(scriptNum(2)),
398
+ { op: OP.OP_LEFT },
399
+ pushChunk([0x01, 0x02]),
400
+ { op: OP.OP_EQUAL }
401
+ ], [], { isRelaxed: true, memoryLimit: 100000 })
402
+ expect(spend.validate()).toBe(true)
403
+ })
404
+
405
+ it('OP_LEFT throws when len is out of range', () => {
406
+ const buf = [0x01, 0x02]
407
+ const spend = makeSpend([
408
+ pushChunk(buf),
409
+ pushChunk(scriptNum(5)), // len > size → error
410
+ { op: OP.OP_LEFT },
411
+ { op: OP.OP_1 }
412
+ ], [], { isRelaxed: true, memoryLimit: 100000 })
413
+ expect(() => spend.validate()).toThrow('OP_LEFT')
414
+ })
415
+
416
+ it('OP_RIGHT extracts the last N bytes', () => {
417
+ const buf = [0x01, 0x02, 0x03, 0x04]
418
+ const spend = makeSpend([
419
+ pushChunk(buf),
420
+ pushChunk(scriptNum(2)),
421
+ { op: OP.OP_RIGHT },
422
+ pushChunk([0x03, 0x04]),
423
+ { op: OP.OP_EQUAL }
424
+ ], [], { isRelaxed: true, memoryLimit: 100000 })
425
+ expect(spend.validate()).toBe(true)
426
+ })
427
+
428
+ it('OP_RIGHT throws when len is out of range', () => {
429
+ const buf = [0x01, 0x02]
430
+ const spend = makeSpend([
431
+ pushChunk(buf),
432
+ pushChunk(scriptNum(5)),
433
+ { op: OP.OP_RIGHT },
434
+ { op: OP.OP_1 }
435
+ ], [], { isRelaxed: true, memoryLimit: 100000 })
436
+ expect(() => spend.validate()).toThrow('OP_RIGHT')
437
+ })
438
+ })
439
+
440
+ // ---------------------------------------------------------------------------
441
+ // OP_LSHIFTNUM / OP_RSHIFTNUM (lines 476-501)
442
+ // ---------------------------------------------------------------------------
443
+ describe('Spend – OP_LSHIFTNUM / OP_RSHIFTNUM', () => {
444
+ it('OP_LSHIFTNUM shifts a number left by N bits', () => {
445
+ // 1 << 3 = 8
446
+ const spend = makeSpend([
447
+ pushChunk(scriptNum(1)),
448
+ pushChunk(scriptNum(3)),
449
+ { op: OP.OP_LSHIFTNUM },
450
+ pushChunk(scriptNum(8)),
451
+ { op: OP.OP_NUMEQUAL }
452
+ ], [], { isRelaxed: true, memoryLimit: 100000 })
453
+ expect(spend.validate()).toBe(true)
454
+ })
455
+
456
+ it('OP_LSHIFTNUM throws when shift bits are negative', () => {
457
+ const spend = makeSpend([
458
+ pushChunk(scriptNum(1)),
459
+ pushChunk(scriptNum(-1)),
460
+ { op: OP.OP_LSHIFTNUM },
461
+ { op: OP.OP_1 }
462
+ ], [], { isRelaxed: true, memoryLimit: 100000 })
463
+ expect(() => spend.validate()).toThrow('OP_LSHIFTNUM bits to shift must not be negative')
464
+ })
465
+
466
+ it('OP_RSHIFTNUM shifts a positive number right', () => {
467
+ // 8 >> 2 = 2
468
+ const spend = makeSpend([
469
+ pushChunk(scriptNum(8)),
470
+ pushChunk(scriptNum(2)),
471
+ { op: OP.OP_RSHIFTNUM },
472
+ pushChunk(scriptNum(2)),
473
+ { op: OP.OP_NUMEQUAL }
474
+ ], [], { isRelaxed: true, memoryLimit: 100000 })
475
+ expect(spend.validate()).toBe(true)
476
+ })
477
+
478
+ it('OP_RSHIFTNUM shifts a negative number right (sign-preserving)', () => {
479
+ // -8 >> 2 = -2
480
+ const spend = makeSpend([
481
+ pushChunk(scriptNum(-8)),
482
+ pushChunk(scriptNum(2)),
483
+ { op: OP.OP_RSHIFTNUM },
484
+ pushChunk(scriptNum(-2)),
485
+ { op: OP.OP_NUMEQUAL }
486
+ ], [], { isRelaxed: true, memoryLimit: 100000 })
487
+ expect(spend.validate()).toBe(true)
488
+ })
489
+
490
+ it('OP_RSHIFTNUM throws when shift bits are negative', () => {
491
+ const spend = makeSpend([
492
+ pushChunk(scriptNum(8)),
493
+ pushChunk(scriptNum(-1)),
494
+ { op: OP.OP_RSHIFTNUM },
495
+ { op: OP.OP_1 }
496
+ ], [], { isRelaxed: true, memoryLimit: 100000 })
497
+ expect(() => spend.validate()).toThrow('OP_RSHIFTNUM bits to shift must not be negative')
498
+ })
499
+ })
500
+
501
+ // ---------------------------------------------------------------------------
502
+ // OP_1NEGATE (line 504-505)
503
+ // ---------------------------------------------------------------------------
504
+ describe('Spend – OP_1NEGATE', () => {
505
+ it('pushes -1 onto the stack', () => {
506
+ const spend = makeSpend([
507
+ { op: OP.OP_1NEGATE },
508
+ pushChunk(scriptNum(-1)),
509
+ { op: OP.OP_NUMEQUAL }
510
+ ], [], { isRelaxed: true, memoryLimit: 100000 })
511
+ expect(spend.validate()).toBe(true)
512
+ })
513
+ })
514
+
515
+ // ---------------------------------------------------------------------------
516
+ // OP_VERIF / OP_VERNOTIF (lines 531-544)
517
+ // ---------------------------------------------------------------------------
518
+ describe('Spend – OP_VERIF / OP_VERNOTIF', () => {
519
+ it('OP_VERIF: matching 4-byte LE version makes inner block execute', () => {
520
+ // version=2 → LE = [2,0,0,0]; VERIF match → ifStack=true → inner push executes
521
+ const spend = makeSpend([
522
+ pushChunk([2, 0, 0, 0]),
523
+ { op: OP.OP_VERIF },
524
+ { op: OP.OP_1 },
525
+ { op: OP.OP_ENDIF }
526
+ ], [], { isRelaxed: true, transactionVersion: 2, memoryLimit: 100000 })
527
+ expect(spend.validate()).toBe(true)
528
+ })
529
+
530
+ it('OP_VERIF: non-matching version skips inner block', () => {
531
+ // version=2, push [3,0,0,0] → no match → ifStack=false → inner push skipped
532
+ const spend = makeSpend([
533
+ pushChunk([3, 0, 0, 0]),
534
+ { op: OP.OP_VERIF },
535
+ { op: OP.OP_1 },
536
+ { op: OP.OP_ENDIF },
537
+ { op: OP.OP_1 } // fallback truthy result
538
+ ], [], { isRelaxed: true, transactionVersion: 2, memoryLimit: 100000 })
539
+ expect(spend.validate()).toBe(true)
540
+ })
541
+
542
+ it('OP_VERIF: non-4-byte value never matches', () => {
543
+ const spend = makeSpend([
544
+ pushChunk([1]), // 1-byte value → never 4-byte match
545
+ { op: OP.OP_VERIF },
546
+ { op: OP.OP_1 },
547
+ { op: OP.OP_ENDIF },
548
+ { op: OP.OP_1 }
549
+ ], [], { isRelaxed: true, transactionVersion: 1, memoryLimit: 100000 })
550
+ expect(spend.validate()).toBe(true)
551
+ })
552
+
553
+ it('OP_VERNOTIF: matching version skips inner block (negated)', () => {
554
+ // version=2, [2,0,0,0] matches → VERNOTIF negates → ifStack=false → block skipped
555
+ const spend = makeSpend([
556
+ pushChunk([2, 0, 0, 0]),
557
+ { op: OP.OP_VERNOTIF },
558
+ { op: OP.OP_1 },
559
+ { op: OP.OP_ENDIF },
560
+ { op: OP.OP_1 }
561
+ ], [], { isRelaxed: true, transactionVersion: 2, memoryLimit: 100000 })
562
+ expect(spend.validate()).toBe(true)
563
+ })
564
+ })
565
+
566
+ // ---------------------------------------------------------------------------
567
+ // OP_IFDUP (lines 628-633)
568
+ // ---------------------------------------------------------------------------
569
+ describe('Spend – OP_IFDUP', () => {
570
+ it('duplicates top item when truthy', () => {
571
+ const spend = makeSpend([
572
+ { op: OP.OP_1 },
573
+ { op: OP.OP_IFDUP },
574
+ { op: OP.OP_DROP } // consume duplicate
575
+ // stack: [1] → truthy → validate ok
576
+ ], [], { isRelaxed: true, memoryLimit: 100000 })
577
+ expect(spend.validate()).toBe(true)
578
+ })
579
+
580
+ it('does NOT duplicate top item when falsy', () => {
581
+ // [0x00] is falsy → IFDUP leaves stack unchanged → stack=[0x00] → falsy → throws
582
+ const spend = makeSpend([
583
+ pushChunk([0x00]),
584
+ { op: OP.OP_IFDUP }
585
+ ], [], { isRelaxed: true, memoryLimit: 100000 })
586
+ expect(() => spend.validate()).toThrow('top stack element must be truthy')
587
+ })
588
+ })
589
+
590
+ // ---------------------------------------------------------------------------
591
+ // OP_AND / OP_OR / OP_XOR / OP_INVERT (lines 704-726)
592
+ // ---------------------------------------------------------------------------
593
+ describe('Spend – bitwise opcodes', () => {
594
+ it('OP_AND performs bitwise AND', () => {
595
+ const spend = makeSpend([
596
+ pushChunk([0xff]),
597
+ pushChunk([0x0f]),
598
+ { op: OP.OP_AND },
599
+ pushChunk([0x0f]),
600
+ { op: OP.OP_EQUAL }
601
+ ], [], { isRelaxed: true, memoryLimit: 100000 })
602
+ expect(spend.validate()).toBe(true)
603
+ })
604
+
605
+ it('OP_AND throws when operands differ in length', () => {
606
+ const spend = makeSpend([
607
+ pushChunk([0x01, 0x02]),
608
+ pushChunk([0x01]),
609
+ { op: OP.OP_AND },
610
+ { op: OP.OP_1 }
611
+ ], [], { isRelaxed: true, memoryLimit: 100000 })
612
+ expect(() => spend.validate()).toThrow('OP_AND requires the top two stack items to be the same size')
613
+ })
614
+
615
+ it('OP_OR performs bitwise OR', () => {
616
+ const spend = makeSpend([
617
+ pushChunk([0xf0]),
618
+ pushChunk([0x0f]),
619
+ { op: OP.OP_OR },
620
+ pushChunk([0xff]),
621
+ { op: OP.OP_EQUAL }
622
+ ], [], { isRelaxed: true, memoryLimit: 100000 })
623
+ expect(spend.validate()).toBe(true)
624
+ })
625
+
626
+ it('OP_XOR performs bitwise XOR', () => {
627
+ const spend = makeSpend([
628
+ pushChunk([0xff]),
629
+ pushChunk([0xf0]),
630
+ { op: OP.OP_XOR },
631
+ pushChunk([0x0f]),
632
+ { op: OP.OP_EQUAL }
633
+ ], [], { isRelaxed: true, memoryLimit: 100000 })
634
+ expect(spend.validate()).toBe(true)
635
+ })
636
+
637
+ it('OP_INVERT performs bitwise NOT', () => {
638
+ const spend = makeSpend([
639
+ pushChunk([0xff]),
640
+ { op: OP.OP_INVERT },
641
+ pushChunk([0x00]),
642
+ { op: OP.OP_EQUAL }
643
+ ], [], { isRelaxed: true, memoryLimit: 100000 })
644
+ expect(spend.validate()).toBe(true)
645
+ })
646
+ })
647
+
648
+ // ---------------------------------------------------------------------------
649
+ // OP_2MUL / OP_2DIV (lines 775-776)
650
+ // ---------------------------------------------------------------------------
651
+ describe('Spend – OP_2MUL / OP_2DIV', () => {
652
+ it('OP_2MUL doubles the top stack value', () => {
653
+ const spend = makeSpend([
654
+ pushChunk(scriptNum(7)),
655
+ { op: OP.OP_2MUL },
656
+ pushChunk(scriptNum(14)),
657
+ { op: OP.OP_NUMEQUAL }
658
+ ], [], { isRelaxed: true, memoryLimit: 100000 })
659
+ expect(spend.validate()).toBe(true)
660
+ })
661
+
662
+ it('OP_2DIV halves the top stack value', () => {
663
+ const spend = makeSpend([
664
+ pushChunk(scriptNum(8)),
665
+ { op: OP.OP_2DIV },
666
+ pushChunk(scriptNum(4)),
667
+ { op: OP.OP_NUMEQUAL }
668
+ ], [], { isRelaxed: true, memoryLimit: 100000 })
669
+ expect(spend.validate()).toBe(true)
670
+ })
671
+ })
672
+
673
+ // ---------------------------------------------------------------------------
674
+ // OP_CHECKMULTISIG error paths (lines 902, 908, 916, 922, 929)
675
+ // ---------------------------------------------------------------------------
676
+ describe('Spend – OP_CHECKMULTISIG error paths', () => {
677
+ it('throws when stack is empty (no nKeys item)', () => {
678
+ const spend = makeLocking([{ op: OP.OP_CHECKMULTISIG }])
679
+ expect(() => spend.validate()).toThrow('requires at least 1 item')
680
+ })
681
+
682
+ it('throws when nKeys is negative', () => {
683
+ const spend = makeLocking([
684
+ pushChunk(scriptNum(-1)),
685
+ { op: OP.OP_CHECKMULTISIG }
686
+ ])
687
+ expect(() => spend.validate()).toThrow('key count between 0')
688
+ })
689
+
690
+ it('throws when stack is too small for the declared keys', () => {
691
+ // nKeys=2 but only the nKeys element and 1 key are available
692
+ const spend = makeLocking([
693
+ pushChunk([0x01]), // one dummy key
694
+ pushChunk(scriptNum(2)), // nKeys=2
695
+ { op: OP.OP_CHECKMULTISIG }
696
+ ])
697
+ expect(() => spend.validate()).toThrow('stack too small for nKeys and keys')
698
+ })
699
+
700
+ it('throws when nSigs > nKeys', () => {
701
+ const dummyKey = [0x02, ...new Array(32).fill(0x00)] // 33-byte invalid key (will fail later)
702
+ // nSigs=3 but nKeys=1
703
+ const spend = makeLocking([
704
+ pushChunk([0x00]), // dummy
705
+ pushChunk(scriptNum(3)), // nSigs
706
+ pushChunk(dummyKey), // key 1
707
+ pushChunk(scriptNum(1)), // nKeys=1
708
+ { op: OP.OP_CHECKMULTISIG }
709
+ ])
710
+ expect(() => spend.validate()).toThrow('number of signatures to be no greater than the number of keys')
711
+ })
712
+
713
+ it('throws when stack is too small for the declared sigs', () => {
714
+ const dummyKey = [0x02, ...new Array(32).fill(0x00)]
715
+ // nKeys=1, nSigs=1 but no sig or dummy on stack
716
+ const spend = makeLocking([
717
+ pushChunk(dummyKey), // key 1
718
+ pushChunk(scriptNum(1)), // nKeys=1
719
+ { op: OP.OP_CHECKMULTISIG }
720
+ // nSigs would be next but stack ran out
721
+ ])
722
+ expect(() => spend.validate()).toThrow()
723
+ })
724
+
725
+ it('throws when non-empty dummy is present in strict mode', () => {
726
+ const privKey = PrivateKey.fromRandom()
727
+ const pubKey = PublicKey.fromPrivateKey(privKey)
728
+ // 0 of 0 multisig with non-empty dummy → NULLDUMMY violation
729
+ const spend = makeSpend([
730
+ pushChunk([0x01]), // non-empty dummy (violates SCRIPT_VERIFY_NULLDUMMY)
731
+ pushChunk(scriptNum(0)), // nSigs=0
732
+ pushChunk(scriptNum(0)), // nKeys=0
733
+ { op: OP.OP_CHECKMULTISIG }
734
+ ])
735
+ expect(() => spend.validate()).toThrow('dummy')
736
+ })
737
+
738
+ it('succeeds for 0-of-0 multisig with empty dummy', () => {
739
+ const spend = makeSpend([
740
+ { op: OP.OP_0 }, // empty dummy
741
+ { op: OP.OP_0 }, // nSigs=0
742
+ { op: OP.OP_0 }, // nKeys=0
743
+ { op: OP.OP_CHECKMULTISIG }
744
+ ])
745
+ expect(spend.validate()).toBe(true)
746
+ })
747
+ })
748
+
749
+ // ---------------------------------------------------------------------------
750
+ // OP_NUM2BIN (lines 1029-1068)
751
+ // ---------------------------------------------------------------------------
752
+ describe('Spend – OP_NUM2BIN', () => {
753
+ it('throws when the requested size is too small for the value', () => {
754
+ // 256 needs 2 bytes; requesting 1 byte → error
755
+ const spend = makeSpend([
756
+ pushChunk(scriptNum(256)),
757
+ pushChunk(scriptNum(1)), // size = 1 → too small
758
+ { op: OP.OP_NUM2BIN },
759
+ { op: OP.OP_1 }
760
+ ], [], { isRelaxed: true, memoryLimit: 100000 })
761
+ expect(() => spend.validate()).toThrow('OP_NUM2BIN requires that the size')
762
+ })
763
+
764
+ it('pads a positive number to the requested size', () => {
765
+ // value=5, size=4 → [0x05, 0x00, 0x00, 0x00]
766
+ const spend = makeSpend([
767
+ pushChunk(scriptNum(5)),
768
+ pushChunk(scriptNum(4)),
769
+ { op: OP.OP_NUM2BIN },
770
+ pushChunk([0x05, 0x00, 0x00, 0x00]),
771
+ { op: OP.OP_EQUAL }
772
+ ], [], { isRelaxed: true, memoryLimit: 100000 })
773
+ expect(spend.validate()).toBe(true)
774
+ })
775
+
776
+ it('pads a negative number to the requested size (sign bit preserved)', () => {
777
+ // value=-5 (scriptnum: [0x85]), size=4 → [0x05, 0x00, 0x00, 0x80]
778
+ const spend = makeSpend([
779
+ pushChunk(scriptNum(-5)),
780
+ pushChunk(scriptNum(4)),
781
+ { op: OP.OP_NUM2BIN },
782
+ pushChunk([0x05, 0x00, 0x00, 0x80]),
783
+ { op: OP.OP_EQUAL }
784
+ ], [], { isRelaxed: true, memoryLimit: 100000 })
785
+ expect(spend.validate()).toBe(true)
786
+ })
787
+
788
+ it('does not pad when rawnum length equals requested size', () => {
789
+ // value=256 (scriptnum [0x00, 0x01]), size=2 → [0x00, 0x01]
790
+ const spend = makeSpend([
791
+ pushChunk(scriptNum(256)),
792
+ pushChunk(scriptNum(2)),
793
+ { op: OP.OP_NUM2BIN },
794
+ pushChunk([0x00, 0x01]),
795
+ { op: OP.OP_EQUAL }
796
+ ], [], { isRelaxed: true, memoryLimit: 100000 })
797
+ expect(spend.validate()).toBe(true)
798
+ })
799
+ })
800
+
801
+ // ---------------------------------------------------------------------------
802
+ // OP_BIN2NUM (line 1070-1078)
803
+ // ---------------------------------------------------------------------------
804
+ describe('Spend – OP_BIN2NUM', () => {
805
+ it('converts binary to minimal scriptnum', () => {
806
+ // [0x05, 0x00, 0x00, 0x00] → 5
807
+ const spend = makeSpend([
808
+ pushChunk([0x05, 0x00, 0x00, 0x00]),
809
+ { op: OP.OP_BIN2NUM },
810
+ pushChunk(scriptNum(5)),
811
+ { op: OP.OP_NUMEQUAL }
812
+ ], [], { isRelaxed: true, memoryLimit: 100000 })
813
+ expect(spend.validate()).toBe(true)
814
+ })
815
+
816
+ it('converts padded negative binary to minimal scriptnum', () => {
817
+ // [0x05, 0x00, 0x00, 0x80] → -5
818
+ const spend = makeSpend([
819
+ pushChunk([0x05, 0x00, 0x00, 0x80]),
820
+ { op: OP.OP_BIN2NUM },
821
+ pushChunk(scriptNum(-5)),
822
+ { op: OP.OP_NUMEQUAL }
823
+ ], [], { isRelaxed: true, memoryLimit: 100000 })
824
+ expect(spend.validate()).toBe(true)
825
+ })
826
+ })
827
+
828
+ // ---------------------------------------------------------------------------
829
+ // Default opcode path – invalid opcode (line 1082)
830
+ // ---------------------------------------------------------------------------
831
+ describe('Spend – default opcode (invalid)', () => {
832
+ it('throws for an invalid opcode value', () => {
833
+ // 0xff is not a valid opcode in BSV
834
+ const spend = makeLocking([{ op: 0xff }])
835
+ expect(() => spend.validate()).toThrow('Invalid opcode')
836
+ })
837
+ })