@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.
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/src/auth/certificates/Certificate.js +1 -1
- package/dist/cjs/src/auth/certificates/Certificate.js.map +1 -1
- package/dist/cjs/src/primitives/BigNumber.js +4 -5
- package/dist/cjs/src/primitives/BigNumber.js.map +1 -1
- package/dist/cjs/src/script/OP.js +22 -16
- package/dist/cjs/src/script/OP.js.map +1 -1
- package/dist/cjs/src/script/Spend.js +21 -78
- package/dist/cjs/src/script/Spend.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/auth/certificates/Certificate.js +1 -1
- package/dist/esm/src/auth/certificates/Certificate.js.map +1 -1
- package/dist/esm/src/primitives/BigNumber.js +4 -5
- package/dist/esm/src/primitives/BigNumber.js.map +1 -1
- package/dist/esm/src/script/OP.js +22 -16
- package/dist/esm/src/script/OP.js.map +1 -1
- package/dist/esm/src/script/Spend.js +21 -78
- package/dist/esm/src/script/Spend.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/primitives/BigNumber.d.ts.map +1 -1
- package/dist/types/src/script/OP.d.ts +9 -7
- package/dist/types/src/script/OP.d.ts.map +1 -1
- package/dist/types/src/script/Spend.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +3 -3
- package/dist/umd/bundle.js.map +1 -1
- package/docs/reference/script.md +76 -0
- package/docs/reference/transaction.md +63 -6
- package/docs/reference/wallet.md +285 -1094
- package/package.json +1 -1
- package/src/auth/certificates/Certificate.ts +1 -1
- package/src/primitives/BigNumber.ts +4 -5
- package/src/primitives/__tests/BigNumber.constructor.test.ts +3 -3
- package/src/script/OP.ts +21 -16
- package/src/script/Spend.ts +21 -28
- package/src/script/__tests/Chronicle.test.ts +39 -103
- 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
|
+
})
|