@bsv/sdk 2.0.9 → 2.0.10

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 (37) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/auth/certificates/Certificate.js +1 -1
  3. package/dist/cjs/src/auth/certificates/Certificate.js.map +1 -1
  4. package/dist/cjs/src/primitives/BigNumber.js +4 -5
  5. package/dist/cjs/src/primitives/BigNumber.js.map +1 -1
  6. package/dist/cjs/src/script/OP.js +22 -16
  7. package/dist/cjs/src/script/OP.js.map +1 -1
  8. package/dist/cjs/src/script/Spend.js +21 -78
  9. package/dist/cjs/src/script/Spend.js.map +1 -1
  10. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  11. package/dist/esm/src/auth/certificates/Certificate.js +1 -1
  12. package/dist/esm/src/auth/certificates/Certificate.js.map +1 -1
  13. package/dist/esm/src/primitives/BigNumber.js +4 -5
  14. package/dist/esm/src/primitives/BigNumber.js.map +1 -1
  15. package/dist/esm/src/script/OP.js +22 -16
  16. package/dist/esm/src/script/OP.js.map +1 -1
  17. package/dist/esm/src/script/Spend.js +21 -78
  18. package/dist/esm/src/script/Spend.js.map +1 -1
  19. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  20. package/dist/types/src/primitives/BigNumber.d.ts.map +1 -1
  21. package/dist/types/src/script/OP.d.ts +9 -7
  22. package/dist/types/src/script/OP.d.ts.map +1 -1
  23. package/dist/types/src/script/Spend.d.ts.map +1 -1
  24. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  25. package/dist/umd/bundle.js +3 -3
  26. package/dist/umd/bundle.js.map +1 -1
  27. package/docs/reference/script.md +76 -0
  28. package/docs/reference/transaction.md +63 -6
  29. package/docs/reference/wallet.md +285 -1094
  30. package/package.json +1 -1
  31. package/src/auth/certificates/Certificate.ts +1 -1
  32. package/src/primitives/BigNumber.ts +4 -5
  33. package/src/primitives/__tests/BigNumber.constructor.test.ts +3 -3
  34. package/src/script/OP.ts +21 -16
  35. package/src/script/Spend.ts +21 -28
  36. package/src/script/__tests/Chronicle.test.ts +39 -103
  37. package/src/script/__tests/ChronicleOpcodes.test.ts +548 -0
@@ -0,0 +1,548 @@
1
+ import Script from '../Script'
2
+ import Spend from '../Spend'
3
+ import LockingScript from '../LockingScript'
4
+ import UnlockingScript from '../UnlockingScript'
5
+ import ScriptChunk from '../ScriptChunk'
6
+ import OP from '../OP'
7
+
8
+ /**
9
+ * Chronicle upgrade opcode tests.
10
+ *
11
+ * Based on the bitcoin-sv node v1.2.0 functional test suite:
12
+ * https://github.com/bitcoin-sv/bitcoin-sv/tree/172c8fa38cce30cf4df0327b33c7418ea6289de8/test/functional/chronicle_upgrade_tests
13
+ *
14
+ * Covers:
15
+ * - opcodes.py → restored/new opcodes (OP_VER, OP_VERIF, OP_VERNOTIF, OP_SUBSTR, OP_LEFT, OP_RIGHT, OP_2MUL, OP_2DIV, OP_LSHIFTNUM, OP_RSHIFTNUM)
16
+ * - script_num_size.py → enlarged script number limits post-Chronicle
17
+ * - Undefined opcodes → 0xba+ must error (no longer NOPs)
18
+ */
19
+
20
+ const ZERO_TXID = '0'.repeat(64)
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Helpers
24
+ // ---------------------------------------------------------------------------
25
+
26
+ /** Build a ScriptChunk that pushes arbitrary bytes. */
27
+ function pushChunk (bytes: number[]): ScriptChunk {
28
+ if (bytes.length === 0) return { op: OP.OP_0, data: [] }
29
+ if (bytes.length === 1) {
30
+ if (bytes[0] >= 1 && bytes[0] <= 16) return { op: OP.OP_1 + (bytes[0] - 1) }
31
+ if (bytes[0] === 0x81) return { op: OP.OP_1NEGATE }
32
+ }
33
+ let op: number
34
+ if (bytes.length < OP.OP_PUSHDATA1) op = bytes.length
35
+ else if (bytes.length < 256) op = OP.OP_PUSHDATA1
36
+ else if (bytes.length < 65536) op = OP.OP_PUSHDATA2
37
+ else op = OP.OP_PUSHDATA4
38
+ return { op, data: bytes.slice() }
39
+ }
40
+
41
+ /** Create a Spend that evaluates a locking script (with optional unlocking pushes). */
42
+ function createSpend (
43
+ lockingScript: LockingScript,
44
+ unlockingPushes: number[][] = [],
45
+ txVersion: number = 1
46
+ ): Spend {
47
+ return new Spend({
48
+ sourceTXID: ZERO_TXID,
49
+ sourceOutputIndex: 0,
50
+ sourceSatoshis: 1,
51
+ lockingScript,
52
+ transactionVersion: txVersion,
53
+ otherInputs: [],
54
+ outputs: [],
55
+ inputIndex: 0,
56
+ unlockingScript: new UnlockingScript(unlockingPushes.map(pushChunk)),
57
+ inputSequence: 0xffffffff,
58
+ lockTime: 0
59
+ })
60
+ }
61
+
62
+ /** Create a Spend from ASM string for the locking script. */
63
+ function createSpendFromAsm (
64
+ lockingAsm: string,
65
+ unlockingPushes: number[][] = [],
66
+ txVersion: number = 1
67
+ ): Spend {
68
+ const parsed = Script.fromASM(lockingAsm)
69
+ const ls = new LockingScript(parsed.chunks.map(c => ({
70
+ op: c.op,
71
+ data: Array.isArray(c.data) ? c.data.slice() : undefined
72
+ })))
73
+ return createSpend(ls, unlockingPushes, txVersion)
74
+ }
75
+
76
+ /** Build a locking script from a mixture of opcodes and byte-array pushes. */
77
+ function buildLockingScript (items: Array<number | number[]>): LockingScript {
78
+ const chunks: ScriptChunk[] = items.map(item => {
79
+ if (typeof item === 'number') return { op: item }
80
+ return pushChunk(item)
81
+ })
82
+ return new LockingScript(chunks)
83
+ }
84
+
85
+ /** Encode a string to its byte array. */
86
+ function strBytes (s: string): number[] {
87
+ return Array.from(Buffer.from(s, 'ascii'))
88
+ }
89
+
90
+ /** 4-byte little-endian encoding of a 32-bit integer (matching node's to_le). */
91
+ function le4 (v: number): number[] {
92
+ return [v & 0xff, (v >>> 8) & 0xff, (v >>> 16) & 0xff, (v >>> 24) & 0xff]
93
+ }
94
+
95
+ /** Assert that a locking script built from items validates successfully. */
96
+ function expectValid (items: Array<number | number[]>, txVersion: number = 1): void {
97
+ const spend = createSpend(buildLockingScript(items), [], txVersion)
98
+ expect(spend.validate()).toBe(true)
99
+ }
100
+
101
+ /** Assert that a locking script built from items throws on validation. */
102
+ function expectInvalid (items: Array<number | number[]>, txVersion: number = 1): void {
103
+ const spend = createSpend(buildLockingScript(items), [], txVersion)
104
+ expect(() => spend.validate()).toThrow()
105
+ }
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // Tests
109
+ // ---------------------------------------------------------------------------
110
+
111
+ describe('Chronicle Opcode Tests (based on bitcoin-sv node v1.2.0 test suite)', () => {
112
+
113
+ // ==========================================================================
114
+ // opcodes.py — OP_VER
115
+ // ==========================================================================
116
+ describe('OP_VER', () => {
117
+ it('pushes the transaction version (1) as 4-byte LE, then DROP + TRUE succeeds', () => {
118
+ // CScript([OP_VER, OP_DROP, OP_TRUE])
119
+ expectValid([OP.OP_VER, OP.OP_DROP, OP.OP_TRUE])
120
+ })
121
+
122
+ it('pushes tx version 2 correctly as 4-byte LE', () => {
123
+ // OP_VER should push [0x02, 0x00, 0x00, 0x00] for version 2
124
+ expectValid([OP.OP_VER, le4(2), OP.OP_EQUAL], 2)
125
+ })
126
+
127
+ it('pushes tx version 1 as [0x01, 0x00, 0x00, 0x00]', () => {
128
+ expectValid([OP.OP_VER, le4(1), OP.OP_EQUAL])
129
+ })
130
+
131
+ it('OP_VER with version 0xFF00 encodes correctly', () => {
132
+ const ver = 0xFF00
133
+ expectValid([OP.OP_VER, le4(ver), OP.OP_EQUAL], ver)
134
+ })
135
+ })
136
+
137
+ // ==========================================================================
138
+ // opcodes.py — OP_VERIF / OP_VERNOTIF
139
+ // ==========================================================================
140
+ describe('OP_VERIF', () => {
141
+ it('branches to TRUE when stack top matches tx version as 4-byte LE', () => {
142
+ // CScript([b'\x01\x00\x00\x00', OP_VERIF, OP_TRUE, OP_ELSE, OP_FALSE, OP_ENDIF])
143
+ expectValid([le4(1), OP.OP_VERIF, OP.OP_TRUE, OP.OP_ELSE, OP.OP_FALSE, OP.OP_ENDIF])
144
+ })
145
+
146
+ it('branches to ELSE when stack top does NOT match tx version', () => {
147
+ // Push version 2 encoding but tx version is 1 → VERIF is false → goes to ELSE
148
+ expectValid([le4(2), OP.OP_VERIF, OP.OP_FALSE, OP.OP_ELSE, OP.OP_TRUE, OP.OP_ENDIF])
149
+ })
150
+
151
+ it('only matches exactly 4-byte items (3-byte push fails match)', () => {
152
+ // Node v1.2.0: only matches when stack item is exactly 4 bytes
153
+ // 3 bytes — should NOT match version 1
154
+ expectValid([[0x01, 0x00, 0x00], OP.OP_VERIF, OP.OP_FALSE, OP.OP_ELSE, OP.OP_TRUE, OP.OP_ENDIF])
155
+ })
156
+
157
+ it('only matches exactly 4-byte items (5-byte push fails match)', () => {
158
+ expectValid([[0x01, 0x00, 0x00, 0x00, 0x00], OP.OP_VERIF, OP.OP_FALSE, OP.OP_ELSE, OP.OP_TRUE, OP.OP_ENDIF])
159
+ })
160
+
161
+ it('requires at least one item on the stack', () => {
162
+ expectInvalid([OP.OP_VERIF, OP.OP_TRUE, OP.OP_ELSE, OP.OP_FALSE, OP.OP_ENDIF])
163
+ })
164
+ })
165
+
166
+ describe('OP_VERNOTIF', () => {
167
+ it('branches to TRUE when stack top does NOT match tx version', () => {
168
+ // CScript([b'\x01\xFF\x00\x00', OP_VERNOTIF, OP_TRUE, OP_ELSE, OP_FALSE, OP_ENDIF])
169
+ // Version 0x00FF01 != version 1 → VERNOTIF negates → true → goes to OP_TRUE
170
+ expectValid([[0x01, 0xFF, 0x00, 0x00], OP.OP_VERNOTIF, OP.OP_TRUE, OP.OP_ELSE, OP.OP_FALSE, OP.OP_ENDIF])
171
+ })
172
+
173
+ it('branches to ELSE when stack top matches tx version', () => {
174
+ expectValid([le4(1), OP.OP_VERNOTIF, OP.OP_FALSE, OP.OP_ELSE, OP.OP_TRUE, OP.OP_ENDIF])
175
+ })
176
+
177
+ it('non-4-byte items always evaluate as not-matching (so VERNOTIF → true)', () => {
178
+ // 1 byte — won't match, so NOT(false) = true
179
+ expectValid([[0x01], OP.OP_VERNOTIF, OP.OP_TRUE, OP.OP_ELSE, OP.OP_FALSE, OP.OP_ENDIF])
180
+ })
181
+ })
182
+
183
+ // ==========================================================================
184
+ // opcodes.py — OP_SUBSTR (restored Chronicle opcode, 0xb3)
185
+ // ==========================================================================
186
+ describe('OP_SUBSTR', () => {
187
+ it("extracts 'oWorl' from 'HelloWorld' at offset 4, length 5", () => {
188
+ // CScript([b'HelloWorld', OP_4, OP_5, OP_SUBSTR, b'oWorl', OP_EQUAL])
189
+ expectValid([strBytes('HelloWorld'), OP.OP_4, OP.OP_5, OP.OP_SUBSTR, strBytes('oWorl'), OP.OP_EQUAL])
190
+ })
191
+
192
+ it('extracts full string with offset 0 and length = size', () => {
193
+ expectValid([strBytes('ABC'), OP.OP_0, OP.OP_3, OP.OP_SUBSTR, strBytes('ABC'), OP.OP_EQUAL])
194
+ })
195
+
196
+ it('extracts single char from beginning', () => {
197
+ expectValid([strBytes('Hello'), OP.OP_0, OP.OP_1, OP.OP_SUBSTR, strBytes('H'), OP.OP_EQUAL])
198
+ })
199
+
200
+ it('fails when offset is out of range', () => {
201
+ // offset 5, but string is only 2 bytes
202
+ expectInvalid([strBytes('Hi'), OP.OP_5, OP.OP_1, OP.OP_SUBSTR])
203
+ })
204
+
205
+ it('fails when length exceeds available bytes from offset', () => {
206
+ // offset 1, length 5, but only 1 byte remaining
207
+ expectInvalid([strBytes('Hi'), OP.OP_1, OP.OP_5, OP.OP_SUBSTR])
208
+ })
209
+
210
+ it('requires at least 3 stack items', () => {
211
+ expectInvalid([OP.OP_1, OP.OP_SUBSTR])
212
+ })
213
+ })
214
+
215
+ // ==========================================================================
216
+ // opcodes.py — OP_LEFT (restored Chronicle opcode, 0xb4)
217
+ // ==========================================================================
218
+ describe('OP_LEFT', () => {
219
+ it("extracts 'Hello' from 'HelloWorld' (left 5 bytes)", () => {
220
+ // CScript([b'HelloWorld', OP_5, OP_LEFT, b'Hello', OP_EQUAL])
221
+ expectValid([strBytes('HelloWorld'), OP.OP_5, OP.OP_LEFT, strBytes('Hello'), OP.OP_EQUAL])
222
+ })
223
+
224
+ it('left 0 bytes returns empty', () => {
225
+ expectValid([strBytes('Hello'), OP.OP_0, OP.OP_LEFT, OP.OP_0, OP.OP_EQUAL])
226
+ })
227
+
228
+ it('left full length returns the whole string', () => {
229
+ expectValid([strBytes('Hello'), OP.OP_5, OP.OP_LEFT, strBytes('Hello'), OP.OP_EQUAL])
230
+ })
231
+
232
+ it('fails when length exceeds string size', () => {
233
+ expectInvalid([strBytes('Hi'), OP.OP_5, OP.OP_LEFT])
234
+ })
235
+ })
236
+
237
+ // ==========================================================================
238
+ // opcodes.py — OP_RIGHT (restored Chronicle opcode, 0xb5)
239
+ // ==========================================================================
240
+ describe('OP_RIGHT', () => {
241
+ it("extracts 'World' from 'HelloWorld' (right 5 bytes)", () => {
242
+ // CScript([b'HelloWorld', OP_5, OP_RIGHT, b'World', OP_EQUAL])
243
+ expectValid([strBytes('HelloWorld'), OP.OP_5, OP.OP_RIGHT, strBytes('World'), OP.OP_EQUAL])
244
+ })
245
+
246
+ it('right 0 bytes returns empty', () => {
247
+ expectValid([strBytes('Hello'), OP.OP_0, OP.OP_RIGHT, OP.OP_0, OP.OP_EQUAL])
248
+ })
249
+
250
+ it('right full length returns the whole string', () => {
251
+ expectValid([strBytes('Hello'), OP.OP_5, OP.OP_RIGHT, strBytes('Hello'), OP.OP_EQUAL])
252
+ })
253
+
254
+ it('right 1 byte returns last char', () => {
255
+ expectValid([strBytes('Hello'), OP.OP_1, OP.OP_RIGHT, strBytes('o'), OP.OP_EQUAL])
256
+ })
257
+
258
+ it('fails when length exceeds string size', () => {
259
+ expectInvalid([strBytes('Hi'), OP.OP_5, OP.OP_RIGHT])
260
+ })
261
+ })
262
+
263
+ // ==========================================================================
264
+ // opcodes.py — OP_2MUL (restored Chronicle opcode, 0x8d)
265
+ // ==========================================================================
266
+ describe('OP_2MUL', () => {
267
+ it('1 * 2 = 2', () => {
268
+ // CScript([OP_1, OP_2MUL, OP_2, OP_EQUAL])
269
+ expectValid([OP.OP_1, OP.OP_2MUL, OP.OP_2, OP.OP_EQUAL])
270
+ })
271
+
272
+ it('0 * 2 = 0', () => {
273
+ expectValid([OP.OP_0, OP.OP_2MUL, OP.OP_0, OP.OP_EQUAL])
274
+ })
275
+
276
+ it('8 * 2 = 16', () => {
277
+ expectValid([OP.OP_8, OP.OP_2MUL, OP.OP_16, OP.OP_EQUAL])
278
+ })
279
+
280
+ it('-1 * 2 = -2', () => {
281
+ // -2 in script num is [0x82]
282
+ expectValid([OP.OP_1NEGATE, OP.OP_2MUL, [0x82], OP.OP_EQUAL])
283
+ })
284
+ })
285
+
286
+ // ==========================================================================
287
+ // opcodes.py — OP_2DIV (restored Chronicle opcode, 0x8e)
288
+ // ==========================================================================
289
+ describe('OP_2DIV', () => {
290
+ it('2 / 2 = 1', () => {
291
+ // CScript([OP_2, OP_2DIV, OP_1, OP_EQUAL])
292
+ expectValid([OP.OP_2, OP.OP_2DIV, OP.OP_1, OP.OP_EQUAL])
293
+ })
294
+
295
+ it('16 / 2 = 8', () => {
296
+ expectValid([OP.OP_16, OP.OP_2DIV, OP.OP_8, OP.OP_EQUAL])
297
+ })
298
+
299
+ it('1 / 2 = 0 (integer division)', () => {
300
+ expectValid([OP.OP_1, OP.OP_2DIV, OP.OP_0, OP.OP_EQUAL])
301
+ })
302
+
303
+ it('0 / 2 = 0', () => {
304
+ expectValid([OP.OP_0, OP.OP_2DIV, OP.OP_0, OP.OP_EQUAL])
305
+ })
306
+ })
307
+
308
+ // ==========================================================================
309
+ // opcodes.py — OP_LSHIFTNUM (restored Chronicle opcode, 0xb6)
310
+ // ==========================================================================
311
+ describe('OP_LSHIFTNUM', () => {
312
+ it('1 << 2 = 4', () => {
313
+ // CScript([OP_1, OP_2, OP_LSHIFTNUM, OP_4, OP_EQUAL])
314
+ expectValid([OP.OP_1, OP.OP_2, OP.OP_LSHIFTNUM, OP.OP_4, OP.OP_EQUAL])
315
+ })
316
+
317
+ it('1 << 1 = 2', () => {
318
+ const spend = createSpendFromAsm('OP_1 OP_1 OP_LSHIFTNUM OP_2 OP_EQUAL')
319
+ expect(spend.validate()).toBe(true)
320
+ })
321
+
322
+ it('1 << 8 produces a 2-byte result', () => {
323
+ const spend = createSpendFromAsm('OP_1 OP_8 OP_LSHIFTNUM 0001 OP_EQUAL')
324
+ expect(spend.validate()).toBe(true)
325
+ })
326
+ })
327
+
328
+ // ==========================================================================
329
+ // opcodes.py — OP_RSHIFTNUM (restored Chronicle opcode, 0xb7)
330
+ // ==========================================================================
331
+ describe('OP_RSHIFTNUM', () => {
332
+ it('16 >> 2 = 4', () => {
333
+ // CScript([OP_16, OP_2, OP_RSHIFTNUM, OP_4, OP_EQUAL])
334
+ expectValid([OP.OP_16, OP.OP_2, OP.OP_RSHIFTNUM, OP.OP_4, OP.OP_EQUAL])
335
+ })
336
+
337
+ it('4 >> 2 = 1', () => {
338
+ const spend = createSpendFromAsm('OP_4 OP_2 OP_RSHIFTNUM OP_1 OP_EQUAL')
339
+ expect(spend.validate()).toBe(true)
340
+ })
341
+
342
+ it('2 >> 1 = 1', () => {
343
+ const spend = createSpendFromAsm('OP_2 OP_1 OP_RSHIFTNUM OP_1 OP_EQUAL')
344
+ expect(spend.validate()).toBe(true)
345
+ })
346
+ })
347
+
348
+ // ==========================================================================
349
+ // All opcodes together — full post-Chronicle validation
350
+ // Mirrors the CHRONICLE_ACTIVATION / POST_CHRONICLE test sets from opcodes.py
351
+ // ==========================================================================
352
+ describe('Post-Chronicle opcode activation (all should succeed)', () => {
353
+ const tests: Array<{ name: string, ls: LockingScript, txVersion?: number }> = [
354
+ {
355
+ name: 'OP_VER OP_DROP OP_TRUE',
356
+ ls: buildLockingScript([OP.OP_VER, OP.OP_DROP, OP.OP_TRUE])
357
+ },
358
+ {
359
+ name: 'OP_VERIF with matching version (v1)',
360
+ ls: buildLockingScript([le4(1), OP.OP_VERIF, OP.OP_TRUE, OP.OP_ELSE, OP.OP_FALSE, OP.OP_ENDIF])
361
+ },
362
+ {
363
+ name: 'OP_VERNOTIF with non-matching version',
364
+ ls: buildLockingScript([[0x01, 0xFF, 0x00, 0x00], OP.OP_VERNOTIF, OP.OP_TRUE, OP.OP_ELSE, OP.OP_FALSE, OP.OP_ENDIF])
365
+ },
366
+ {
367
+ name: "OP_SUBSTR: 'oWorl' from 'HelloWorld'",
368
+ ls: buildLockingScript([strBytes('HelloWorld'), OP.OP_4, OP.OP_5, OP.OP_SUBSTR, strBytes('oWorl'), OP.OP_EQUAL])
369
+ },
370
+ {
371
+ name: "OP_LEFT: 'Hello' from 'HelloWorld'",
372
+ ls: buildLockingScript([strBytes('HelloWorld'), OP.OP_5, OP.OP_LEFT, strBytes('Hello'), OP.OP_EQUAL])
373
+ },
374
+ {
375
+ name: "OP_RIGHT: 'World' from 'HelloWorld'",
376
+ ls: buildLockingScript([strBytes('HelloWorld'), OP.OP_5, OP.OP_RIGHT, strBytes('World'), OP.OP_EQUAL])
377
+ },
378
+ {
379
+ name: 'OP_2MUL: 1 * 2 = 2',
380
+ ls: buildLockingScript([OP.OP_1, OP.OP_2MUL, OP.OP_2, OP.OP_EQUAL])
381
+ },
382
+ {
383
+ name: 'OP_2DIV: 2 / 2 = 1',
384
+ ls: buildLockingScript([OP.OP_2, OP.OP_2DIV, OP.OP_1, OP.OP_EQUAL])
385
+ },
386
+ {
387
+ name: 'OP_LSHIFTNUM: 1 << 2 = 4',
388
+ ls: buildLockingScript([OP.OP_1, OP.OP_2, OP.OP_LSHIFTNUM, OP.OP_4, OP.OP_EQUAL])
389
+ },
390
+ {
391
+ name: 'OP_RSHIFTNUM: 16 >> 2 = 4',
392
+ ls: buildLockingScript([OP.OP_16, OP.OP_2, OP.OP_RSHIFTNUM, OP.OP_4, OP.OP_EQUAL])
393
+ }
394
+ ]
395
+
396
+ for (const t of tests) {
397
+ it(t.name, () => {
398
+ const spend = createSpend(t.ls, [], t.txVersion ?? 1)
399
+ expect(spend.validate()).toBe(true)
400
+ })
401
+ }
402
+ })
403
+
404
+ // ==========================================================================
405
+ // Undefined opcodes >= 0xba must now fail (no longer treated as NOPs)
406
+ // Per node v1.2.0: opcodes >= FIRST_UNDEFINED_OP_VALUE (0xba) return SCRIPT_ERR_BAD_OPCODE
407
+ // ==========================================================================
408
+ describe('Undefined opcodes (>= 0xba) must fail', () => {
409
+ const undefinedOpcodes = [
410
+ { name: 'OP_NOP11 (0xba)', value: 0xba },
411
+ { name: 'OP_NOP12 (0xbb)', value: 0xbb },
412
+ { name: 'OP_NOP16 (0xbf)', value: 0xbf },
413
+ { name: 'OP_NOP20 (0xc3)', value: 0xc3 },
414
+ { name: 'OP_NOP32 (0xcf)', value: 0xcf },
415
+ { name: 'OP_NOP50 (0xe1)', value: 0xe1 },
416
+ { name: 'OP_NOP73 (0xf8)', value: 0xf8 },
417
+ { name: '0xf9 (OP_SMALLDATA)', value: 0xf9 },
418
+ { name: 'OP_INVALIDOPCODE (0xff)', value: 0xff }
419
+ ]
420
+
421
+ for (const { name, value } of undefinedOpcodes) {
422
+ it(`${name} should error when executed`, () => {
423
+ // Build a script that executes the undefined opcode followed by OP_TRUE
424
+ const ls = new LockingScript([{ op: value }, { op: OP.OP_TRUE }])
425
+ const spend = createSpend(ls)
426
+ expect(() => spend.validate()).toThrow()
427
+ })
428
+ }
429
+ })
430
+
431
+ // ==========================================================================
432
+ // Valid NOPs that should still work (OP_NOP1, OP_NOP2/CLTV, OP_NOP3/CSV, OP_NOP9, OP_NOP10)
433
+ // ==========================================================================
434
+ describe('Valid NOPs still work', () => {
435
+ const validNops = [
436
+ { name: 'OP_NOP1 (0xb0)', value: OP.OP_NOP1 },
437
+ { name: 'OP_CHECKLOCKTIMEVERIFY/OP_NOP2 (0xb1)', value: OP.OP_CHECKLOCKTIMEVERIFY },
438
+ { name: 'OP_CHECKSEQUENCEVERIFY/OP_NOP3 (0xb2)', value: OP.OP_CHECKSEQUENCEVERIFY },
439
+ { name: 'OP_NOP9 (0xb8)', value: OP.OP_NOP9 },
440
+ { name: 'OP_NOP10 (0xb9)', value: OP.OP_NOP10 }
441
+ ]
442
+
443
+ for (const { name, value } of validNops) {
444
+ it(`${name} acts as NOP and script succeeds`, () => {
445
+ const ls = new LockingScript([{ op: value }, { op: OP.OP_TRUE }])
446
+ const spend = createSpend(ls)
447
+ expect(spend.validate()).toBe(true)
448
+ })
449
+ }
450
+ })
451
+
452
+ // ==========================================================================
453
+ // script_num_size.py — Larger script numbers work post-Chronicle
454
+ // ==========================================================================
455
+ describe('Script number size (post-Chronicle)', () => {
456
+ it('a script number up to old genesis limit (750000 bytes) executes OP_1ADD successfully', () => {
457
+ // A large script number followed by OP_1ADD OP_DROP OP_TRUE
458
+ // Use a modest size that proves the limit is > 4 bytes (pre-genesis limit)
459
+ const bigNum = new Array(8).fill(42) // 8-byte script number
460
+ expectValid([bigNum, OP.OP_1ADD, OP.OP_DROP, OP.OP_TRUE])
461
+ })
462
+
463
+ it('OP_MUL works with numbers up to the genesis script num size', () => {
464
+ // Use OP_DUP OP_MUL to square a number, verifying arithmetic on larger nums
465
+ expectValid([OP.OP_3, OP.OP_DUP, OP.OP_MUL, OP.OP_9, OP.OP_EQUAL])
466
+ })
467
+ })
468
+
469
+ // ==========================================================================
470
+ // OP_RIGHT fix verification — the PR fixed buf.slice(size - len, len) → buf.slice(size - len)
471
+ // ==========================================================================
472
+ describe('OP_RIGHT slice fix', () => {
473
+ it('correctly returns last N bytes for various inputs', () => {
474
+ const testCases = [
475
+ { input: [1, 2, 3, 4, 5], len: 3, expected: [3, 4, 5] },
476
+ { input: [0xAA, 0xBB, 0xCC, 0xDD], len: 2, expected: [0xCC, 0xDD] },
477
+ { input: [0xFF], len: 1, expected: [0xFF] },
478
+ { input: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], len: 1, expected: [10] }
479
+ ]
480
+
481
+ for (const { input, len, expected } of testCases) {
482
+ const lenOpcode = OP.OP_1 + (len - 1) // len is always 1-10 in our test cases
483
+ expectValid([input, lenOpcode, OP.OP_RIGHT, expected, OP.OP_EQUAL])
484
+ }
485
+ })
486
+ })
487
+
488
+ // ==========================================================================
489
+ // OP_VER encoding fix — the PR changed from script-number to 4-byte LE
490
+ // ==========================================================================
491
+ describe('OP_VER encoding (4-byte LE, not script num)', () => {
492
+ it('version 1 pushes exactly [0x01, 0x00, 0x00, 0x00] (not script num [0x01])', () => {
493
+ // If OP_VER used script num, it would push [0x01] (1 byte).
494
+ // With the fix it pushes 4 bytes. Check by testing SIZE.
495
+ expectValid([
496
+ OP.OP_VER,
497
+ OP.OP_SIZE, // push size of top element
498
+ OP.OP_4, // expected: 4 bytes
499
+ OP.OP_EQUALVERIFY,
500
+ le4(1), // verify actual value
501
+ OP.OP_EQUAL
502
+ ])
503
+ })
504
+
505
+ it('version 256 pushes [0x00, 0x01, 0x00, 0x00]', () => {
506
+ expectValid([OP.OP_VER, [0x00, 0x01, 0x00, 0x00], OP.OP_EQUAL], 256)
507
+ })
508
+ })
509
+
510
+ // ==========================================================================
511
+ // Combined OP_LEFT + OP_RIGHT = OP_SPLIT equivalent
512
+ // ==========================================================================
513
+ describe('OP_LEFT + OP_RIGHT composition', () => {
514
+ it('LEFT + RIGHT can reconstruct the original string', () => {
515
+ // Take 'HelloWorld', LEFT 5 → 'Hello', original RIGHT 5 → 'World', CAT → 'HelloWorld'
516
+ expectValid([
517
+ strBytes('HelloWorld'),
518
+ OP.OP_DUP,
519
+ OP.OP_5,
520
+ OP.OP_LEFT, // stack: 'HelloWorld', 'Hello'
521
+ OP.OP_SWAP,
522
+ OP.OP_5,
523
+ OP.OP_RIGHT, // stack: 'Hello', 'World'
524
+ OP.OP_CAT, // stack: 'HelloWorld'
525
+ strBytes('HelloWorld'),
526
+ OP.OP_EQUAL
527
+ ])
528
+ })
529
+ })
530
+
531
+ // ==========================================================================
532
+ // Edge cases for OP_VERIF/OP_VERNOTIF with different version numbers
533
+ // ==========================================================================
534
+ describe('OP_VERIF/OP_VERNOTIF edge cases', () => {
535
+ it('OP_VERIF with tx version 2 matches [0x02, 0x00, 0x00, 0x00]', () => {
536
+ expectValid([le4(2), OP.OP_VERIF, OP.OP_TRUE, OP.OP_ELSE, OP.OP_FALSE, OP.OP_ENDIF], 2)
537
+ })
538
+
539
+ it('OP_VERIF with empty stack item (0 bytes) does not match any version', () => {
540
+ // OP_0 pushes empty array
541
+ expectValid([OP.OP_0, OP.OP_VERIF, OP.OP_FALSE, OP.OP_ELSE, OP.OP_TRUE, OP.OP_ENDIF])
542
+ })
543
+
544
+ it('OP_VERNOTIF with tx version 2: matching value goes to ELSE', () => {
545
+ expectValid([le4(2), OP.OP_VERNOTIF, OP.OP_FALSE, OP.OP_ELSE, OP.OP_TRUE, OP.OP_ENDIF], 2)
546
+ })
547
+ })
548
+ })