@smartledger/bsv 9.15.0 → 9.16.0

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.
@@ -0,0 +1,422 @@
1
+ 'use strict'
2
+ // Emit cross-implementation vectors for the two blind spots in the node's script_tests.json.
3
+ // Everything is raw bytes so another implementation needs nothing from this library.
4
+ const bsv = require('..')
5
+ const I = bsv.Script.Interpreter
6
+ const Sig = bsv.crypto.Signature
7
+ const Sighash = bsv.Transaction.Sighash
8
+ const BN = bsv.crypto.BN
9
+ const Script = bsv.Script
10
+
11
+ const key = bsv.PrivateKey.fromBuffer(Buffer.alloc(32, 0x22))
12
+ const addr = key.toAddress()
13
+ const SATS = 100000
14
+ const lock = bsv.Script.buildPublicKeyHashOut(addr)
15
+
16
+ // Every flag bitcoin-sv v1.2.2 defines, by bit, from src/script/script_flags.h. The earlier
17
+ // version of this table listed only the flags each vector "meant", so `flags` and `flagsHex`
18
+ // disagreed — bits 2, 4, 11 and 12 were set in the hex and absent from the names. A session
19
+ // running these against two SDKs hit exactly that: nulldummy-v1 read as OK by name and as a
20
+ // rejection by hex. Two readings of one vector must not give two answers.
21
+ const NODE_FLAGS = {
22
+ 0: 'P2SH',
23
+ 1: 'STRICTENC',
24
+ 2: 'DERSIG',
25
+ 3: 'LOW_S',
26
+ 4: 'NULLDUMMY',
27
+ 5: 'SIGPUSHONLY',
28
+ 6: 'MINIMALDATA',
29
+ 7: 'DISCOURAGE_UPGRADABLE_NOPS',
30
+ 8: 'CLEANSTACK',
31
+ 9: 'CHECKLOCKTIMEVERIFY',
32
+ 10: 'CHECKSEQUENCEVERIFY',
33
+ 13: 'MINIMALIF',
34
+ 14: 'NULLFAIL',
35
+ 15: 'COMPRESSED_PUBKEYTYPE',
36
+ 16: 'SIGHASH_FORKID',
37
+ 18: 'GENESIS',
38
+ 19: 'UTXO_AFTER_GENESIS',
39
+ 20: 'CHRONICLE',
40
+ 21: 'UTXO_AFTER_CHRONICLE'
41
+ }
42
+
43
+ // Bits this library defines that the node does NOT. The node has no MONOLITH or MAGNETIC flag:
44
+ // those opcodes were restored on BSV in 2018 and are simply enabled, and its IsOpcodeDisabled
45
+ // disables only OP_2MUL and OP_2DIV, and only before Chronicle. Neither bit is a sighash or
46
+ // BIP143 flag. Verified: dropping both changes the verdict on none of these vectors, so another
47
+ // implementation should ignore them and use nodeFlagsHex.
48
+ const LIBRARY_ONLY_FLAGS = { 11: 'ENABLE_MONOLITH_OPCODES', 12: 'ENABLE_MAGNETIC_OPCODES' }
49
+
50
+ function splitFlags (f) {
51
+ const node = []; const libraryOnly = []; const unknown = []
52
+ let nodeHex = 0
53
+ for (let bit = 0; bit < 32; bit++) {
54
+ if ((f & (1 << bit)) === 0) continue
55
+ if (NODE_FLAGS[bit]) { node.push(NODE_FLAGS[bit]); nodeHex |= (1 << bit) } else if (LIBRARY_ONLY_FLAGS[bit]) { libraryOnly.push(LIBRARY_ONLY_FLAGS[bit]) } else { unknown.push('bit' + bit) }
56
+ }
57
+ if (unknown.length) throw new Error('unmapped flag bits: ' + unknown.join(',') + ' in 0x' + f.toString(16))
58
+ return { node: node.sort(), libraryOnly: libraryOnly.sort(), nodeHex }
59
+ }
60
+
61
+ function signedSpend (hashType, signFlags, version) {
62
+ const utxo = new bsv.Transaction.UnspentOutput({
63
+ txId: 'a'.repeat(64), outputIndex: 0, script: lock, satoshis: SATS
64
+ })
65
+ const tx = new bsv.Transaction().from(utxo).to(addr, SATS - 500)
66
+ tx.version = version
67
+ const sig = Sighash.sign(tx, key, hashType, 0, lock, new BN(SATS), signFlags)
68
+ const unlock = new bsv.Script().add(sig.toTxFormat()).add(key.toPublicKey().toBuffer())
69
+ tx.inputs[0].setScript(unlock)
70
+ return { tx, unlock }
71
+ }
72
+
73
+ function rawSpend (unlockAsm, lockAsm, version) {
74
+ const lk = bsv.Script.fromASM(lockAsm)
75
+ const utxo = new bsv.Transaction.UnspentOutput({
76
+ txId: 'b'.repeat(64), outputIndex: 0, script: lk, satoshis: SATS
77
+ })
78
+ const tx = new bsv.Transaction().from(utxo).to(addr, SATS - 500)
79
+ tx.version = version
80
+ const unlock = bsv.Script.fromASM(unlockAsm)
81
+ tx.inputs[0].setScript(unlock)
82
+ return { tx, unlock, lk }
83
+ }
84
+
85
+ // Result names this library reports more precisely than the node does, and the node code each
86
+ // stands for. Taken from tools/sv-vector-harness.js, where the same table scores the corpus.
87
+ // The node renders every verdict through its own enum -> name table (FormatScriptError,
88
+ // src/test/script_tests.cpp), so the corpus's expected column uses THOSE names, not the enum
89
+ // suffixes: SCRIPT_ERR_MUST_USE_FORKID is called MISSING_FORKID and SCRIPT_ERR_SIG_NULLFAIL is
90
+ // called NULLFAIL. Vendored beside the vectors so a label cannot be invented. One row here said
91
+ // MUST_USE_FORKID, the enum suffix, while the other twenty used table names; the Rust session
92
+ // running these caught it.
93
+ const NODE_ERROR_NAMES = require('../test/data/bitcoin-sv/script-error-names.json')
94
+
95
+ // Our errstr mapped to what the node calls the same error. A rename, not a narrowing, which is
96
+ // why it is a separate table from NARROWER below.
97
+ const NODE_SHORT_NAMES = {}
98
+ Object.keys(NODE_ERROR_NAMES).forEach(function (enumSuffix) {
99
+ if (enumSuffix !== NODE_ERROR_NAMES[enumSuffix]) {
100
+ NODE_SHORT_NAMES[enumSuffix] = NODE_ERROR_NAMES[enumSuffix]
101
+ }
102
+ })
103
+
104
+ const NARROWER = {
105
+ EVAL_FALSE_IN_STACK: 'EVAL_FALSE',
106
+ EVAL_FALSE_NO_RESULT: 'EVAL_FALSE',
107
+ EVAL_FALSE_NO_P2SH_STACK: 'EVAL_FALSE',
108
+ EVAL_FALSE_IN_P2SH_STACK: 'EVAL_FALSE',
109
+ SIG_DER_INVALID_FORMAT: 'SIG_DER',
110
+ SIG_DER_HIGH_S: 'SIG_HIGH_S',
111
+ INVALID_OPERAND_SIZE: 'OPERAND_SIZE',
112
+ INVALID_SPLIT_RANGE: 'SPLIT_RANGE'
113
+ }
114
+
115
+ const MAIN = I.mainnetFlags()
116
+ const vectors = []
117
+
118
+ function record (o) {
119
+ const interp = new I()
120
+ let got
121
+ try {
122
+ got = interp.verify(o.unlock, o.lockScript, o.tx, 0, o.flags, new BN(SATS))
123
+ ? 'OK' : interp.errstr.replace(/^SCRIPT_ERR_/, '')
124
+ } catch (e) { got = 'THREW: ' + e.message }
125
+ vectors.push({
126
+ id: o.id,
127
+ blindSpot: o.blindSpot,
128
+ comment: o.comment,
129
+ nodeExpects: o.nodeExpects,
130
+ regressionClass: o.regressionClass,
131
+ txVersion: o.tx.version,
132
+ // nodeFlags / nodeFlagsHex are what another implementation should use: they agree with each
133
+ // other exactly, and contain only bits the node defines.
134
+ nodeFlags: splitFlags(o.flags).node,
135
+ nodeFlagsHex: '0x' + splitFlags(o.flags).nodeHex.toString(16),
136
+ libraryOnlyFlags: splitFlags(o.flags).libraryOnly,
137
+ flagsHexAsRun: '0x' + o.flags.toString(16),
138
+ prevoutScriptHex: o.lockScript.toBuffer().toString('hex'),
139
+ prevoutSatoshis: SATS,
140
+ spendingTxHex: o.tx.uncheckedSerialize(),
141
+ scriptSigHex: o.unlock.toBuffer().toString('hex'),
142
+ smartledgerBsv_9_15_0: got,
143
+ // Our name is sometimes NARROWER than the node's, which is not a disagreement. The node
144
+ // reports one EVAL_FALSE for four distinct situations; we say which. Anyone diffing
145
+ // verdicts should resolve through `narrowerNames` below before calling it a mismatch.
146
+ agreesWithNode: got === o.nodeExpects || NARROWER[got] === o.nodeExpects ||
147
+ NODE_SHORT_NAMES[got] === o.nodeExpects
148
+ })
149
+ }
150
+
151
+ // ---- Blind spot 1: no row pairs LOW_S with a hash-type expectation ----
152
+ let s = signedSpend(Sig.SIGHASH_ALL, 0, 1)
153
+ record({ id: 'lowS-masks-must-use-forkid',
154
+ blindSpot: 'LOW_S never paired with a hash-type expectation',
155
+ comment: 'Signature carries no FORKID bit and is signed over the legacy digest, so it verifies. FORKID is required by the flags. Accepted by @smartledger/bsv <= 9.14.0 because LOW_S short-circuited the STRICTENC block.',
156
+ nodeExpects: 'MISSING_FORKID',
157
+ regressionClass: 'false-accept in <= 9.14.0',
158
+ tx: s.tx,
159
+ unlock: s.unlock,
160
+ lockScript: lock,
161
+ flags: MAIN })
162
+
163
+ s = signedSpend(0x60 | Sig.SIGHASH_FORKID, I.SCRIPT_ENABLE_SIGHASH_FORKID, 1)
164
+ record({ id: 'lowS-masks-sig-hashtype',
165
+ blindSpot: 'LOW_S never paired with a hash-type expectation',
166
+ comment: 'Base sighash type 0x60 is not one of the defined types. Accepted by <= 9.14.0 for the same reason.',
167
+ nodeExpects: 'SIG_HASHTYPE',
168
+ regressionClass: 'false-accept in <= 9.14.0',
169
+ tx: s.tx,
170
+ unlock: s.unlock,
171
+ lockScript: lock,
172
+ flags: MAIN })
173
+
174
+ const NO_CHRON = MAIN & ~(I.SCRIPT_CHRONICLE | I.SCRIPT_ENABLE_CHRONICLE | I.SCRIPT_UTXO_AFTER_CHRONICLE)
175
+ s = signedSpend(Sig.SIGHASH_ALL | Sig.SIGHASH_FORKID | Sig.SIGHASH_CHRONICLE,
176
+ I.SCRIPT_ENABLE_SIGHASH_FORKID | I.SCRIPT_CHRONICLE, 1)
177
+ record({ id: 'lowS-masks-illegal-chronicle',
178
+ blindSpot: 'LOW_S never paired with a hash-type expectation',
179
+ comment: 'Type byte sets the 0x20 Chronicle bit while Chronicle does not apply. Refusing this is what lets the digest be selected by the bit alone.',
180
+ nodeExpects: 'ILLEGAL_CHRONICLE',
181
+ regressionClass: 'false-accept in <= 9.14.0',
182
+ tx: s.tx,
183
+ unlock: s.unlock,
184
+ lockScript: lock,
185
+ flags: NO_CHRON })
186
+
187
+ // ---- Blind spot 2: every row carries transaction version 1 ----
188
+ const CH = I.SCRIPT_VERIFY_P2SH | I.SCRIPT_GENESIS | I.SCRIPT_UTXO_AFTER_GENESIS |
189
+ I.SCRIPT_CHRONICLE | I.SCRIPT_UTXO_AFTER_CHRONICLE
190
+
191
+ for (const v of [1, 2]) {
192
+ let r = rawSpend('', 'OP_2 OP_IF OP_1 OP_ENDIF', v)
193
+ record({ id: 'minimalif-v' + v,
194
+ blindSpot: 'every row carries transaction version 1',
195
+ comment: 'Non-minimal OP_IF operand under Chronicle. Version > 1 opts into malleability, so the node stops enforcing MINIMALIF.',
196
+ nodeExpects: v > 1 ? 'OK' : 'MINIMALIF',
197
+ regressionClass: v > 1 ? 'false-reject in <= 9.14.0' : 'agreed before and after',
198
+ tx: r.tx,
199
+ unlock: r.unlock,
200
+ lockScript: r.lk,
201
+ flags: CH | I.SCRIPT_VERIFY_MINIMALIF })
202
+
203
+ r = rawSpend('OP_1 OP_1', 'OP_NOP', v)
204
+ record({ id: 'cleanstack-v' + v,
205
+ blindSpot: 'every row carries transaction version 1',
206
+ comment: 'Two items left on the stack under Chronicle. Clean stack was only ever policy, so the node ties it to the version.',
207
+ nodeExpects: v > 1 ? 'OK' : 'CLEANSTACK',
208
+ regressionClass: v > 1 ? 'false-reject in <= 9.14.0' : 'agreed before and after',
209
+ tx: r.tx,
210
+ unlock: r.unlock,
211
+ lockScript: r.lk,
212
+ flags: CH | I.SCRIPT_VERIFY_CLEANSTACK })
213
+
214
+ r = rawSpend('OP_1 OP_NOP', 'OP_NOP', v)
215
+ record({ id: 'sigpushonly-v' + v,
216
+ blindSpot: 'every row carries transaction version 1',
217
+ comment: 'Non-push scriptSig under Chronicle with SIGPUSHONLY set.',
218
+ nodeExpects: v > 1 ? 'OK' : 'SIG_PUSHONLY',
219
+ regressionClass: v > 1 ? 'false-reject in <= 9.14.0' : 'agreed before and after',
220
+ tx: r.tx,
221
+ unlock: r.unlock,
222
+ lockScript: r.lk,
223
+ flags: CH | I.SCRIPT_VERIFY_SIGPUSHONLY })
224
+ }
225
+
226
+ // Like rawSpend, but takes built Script objects rather than ASM.
227
+ function rawSpendScripts (unlock, lk, version) {
228
+ const utxo = new bsv.Transaction.UnspentOutput({
229
+ txId: 'c'.repeat(64), outputIndex: 0, script: lk, satoshis: SATS
230
+ })
231
+ const tx = new bsv.Transaction().from(utxo).to(addr, SATS - 500)
232
+ tx.version = version
233
+ tx.inputs[0].setScript(unlock)
234
+ return { tx, unlock, lk }
235
+ }
236
+
237
+ // ---- The remaining EnforceNonMalleability sites, so all seven are covered ----
238
+ // interpreter.cpp 285 SIG_HIGH_S, 433 requireMinimal, 801 MINIMALIF, 1493 + 1643 SIG_NULLFAIL,
239
+ // 1666 SIG_NULLDUMMY, 2441 CLEANSTACK, plus SIGPUSHONLY in VerifyScript.
240
+ const pubBuf = key.toPublicKey().toBuffer()
241
+ const FORKID_ALL = Sig.SIGHASH_ALL | Sig.SIGHASH_FORKID
242
+
243
+ for (const v of [1, 2]) {
244
+ // LOW_S. A VALID high-S signature: (r, s) and (r, N-s) both verify, so negating s keeps the
245
+ // signature good and only the low-S policy objects. At version > 1 it is accepted and the
246
+ // script then succeeds on its merits.
247
+ const utxo0 = new bsv.Transaction.UnspentOutput({
248
+ txId: 'd'.repeat(64), outputIndex: 0, script: lock, satoshis: SATS
249
+ })
250
+ const t = new bsv.Transaction().from(utxo0).to(addr, SATS - 500)
251
+ t.version = v
252
+ const good = Sighash.sign(t, key, FORKID_ALL, 0, lock, new BN(SATS),
253
+ I.SCRIPT_ENABLE_SIGHASH_FORKID)
254
+ const N = bsv.crypto.Point.getN()
255
+ const highS = new Sig(good.r, N.sub(good.s))
256
+ highS.nhashtype = FORKID_ALL
257
+ const unlockHigh = new bsv.Script().add(highS.toTxFormat()).add(pubBuf)
258
+ t.inputs[0].setScript(unlockHigh)
259
+ record({ id: 'lowS-highS-v' + v,
260
+ blindSpot: 'every row carries transaction version 1',
261
+ comment: 'A valid but high-S signature under Chronicle. Negating s leaves ECDSA verification passing, so only the LOW_S policy objects — and version > 1 exempts the transaction from it.',
262
+ nodeExpects: v > 1 ? 'OK' : 'SIG_HIGH_S',
263
+ regressionClass: v > 1 ? 'false-reject in <= 9.14.0' : 'agreed before and after',
264
+ tx: t,
265
+ unlock: unlockHigh,
266
+ lockScript: lock,
267
+ flags: CH | I.SCRIPT_VERIFY_LOW_S | I.SCRIPT_ENABLE_SIGHASH_FORKID })
268
+
269
+ // MINIMALDATA: OP_PUSHDATA1 pushing a single byte, where a direct push would do.
270
+ let r = rawSpendScripts(Script.fromBuffer(Buffer.from('4c0101', 'hex')),
271
+ Script.fromASM('OP_1'), v)
272
+ record({ id: 'minimaldata-v' + v,
273
+ blindSpot: 'every row carries transaction version 1',
274
+ comment: 'Non-minimal push under Chronicle. requireMinimal is VerifyMinimalData(flags) AND EnforceNonMalleability(flags, version), so version > 1 switches it off.',
275
+ nodeExpects: v > 1 ? 'OK' : 'MINIMALDATA',
276
+ regressionClass: v > 1 ? 'false-reject in <= 9.14.0' : 'agreed before and after',
277
+ tx: r.tx,
278
+ unlock: r.unlock,
279
+ lockScript: r.lk,
280
+ flags: CH | I.SCRIPT_VERIFY_MINIMALDATA })
281
+
282
+ // NULLFAIL: a non-empty but invalid signature on a CHECKSIG that fails. OP_NOT turns the
283
+ // false result true, so version > 1 gives OK rather than merely a different error.
284
+ const bogus = Buffer.concat([Buffer.from('3006020101020101', 'hex'), Buffer.from([FORKID_ALL])])
285
+ r = rawSpendScripts(new bsv.Script().add(bogus).add(pubBuf),
286
+ Script.fromASM('OP_CHECKSIG OP_NOT'), v)
287
+ record({ id: 'nullfail-v' + v,
288
+ blindSpot: 'every row carries transaction version 1',
289
+ comment: 'CHECKSIG fails with a non-empty signature. Under Chronicle version > 1 the node stops requiring the signature be empty on failure.',
290
+ nodeExpects: v > 1 ? 'OK' : 'NULLFAIL',
291
+ regressionClass: v > 1 ? 'false-reject in <= 9.14.0' : 'agreed before and after',
292
+ tx: r.tx,
293
+ unlock: r.unlock,
294
+ lockScript: r.lk,
295
+ flags: CH | I.SCRIPT_VERIFY_NULLFAIL | I.SCRIPT_ENABLE_SIGHASH_FORKID })
296
+
297
+ // NULLDUMMY: CHECKMULTISIG's extra argument non-empty.
298
+ r = rawSpendScripts(new bsv.Script().add(Buffer.from([0x01])).add(Buffer.alloc(0)),
299
+ new bsv.Script().add('OP_1').add(pubBuf).add('OP_1').add('OP_CHECKMULTISIG').add('OP_NOT'), v)
300
+ record({ id: 'nulldummy-v' + v,
301
+ blindSpot: 'every row carries transaction version 1',
302
+ comment: 'The argument CHECKMULTISIG consumes without checking is non-empty. Under Chronicle version > 1 the node stops requiring it be zero.',
303
+ nodeExpects: v > 1 ? 'OK' : 'SIG_NULLDUMMY',
304
+ regressionClass: v > 1 ? 'false-reject in <= 9.14.0' : 'agreed before and after',
305
+ tx: r.tx,
306
+ unlock: r.unlock,
307
+ lockScript: r.lk,
308
+ flags: CH | I.SCRIPT_VERIFY_NULLDUMMY | I.SCRIPT_ENABLE_SIGHASH_FORKID })
309
+ }
310
+
311
+ // ---- Blind spot 3: P2SH after Genesis. In the corpus, but only reachable by an
312
+ // implementation that actually replays script_tests.json rather than its own fixtures.
313
+ const Hash = bsv.crypto.Hash
314
+ const BAD_REDEEM = bsv.Script.fromASM('OP_0') // a redeem script that FAILS
315
+ const BAD_HEX = BAD_REDEEM.toBuffer().toString('hex')
316
+ const BAD_OUT = 'OP_HASH160 ' + Hash.sha256ripemd160(BAD_REDEEM.toBuffer()).toString('hex') + ' OP_EQUAL'
317
+
318
+ for (const after of [false, true]) {
319
+ const f = I.SCRIPT_VERIFY_P2SH | (after ? (I.SCRIPT_GENESIS | I.SCRIPT_UTXO_AFTER_GENESIS) : 0)
320
+ const r = rawSpend(BAD_HEX, BAD_OUT, 1)
321
+ record({
322
+ id: 'p2sh-' + (after ? 'after' : 'before') + '-genesis-failing-redeem',
323
+ blindSpot: 'P2SH was removed at Genesis; only the reference corpus covers this',
324
+ comment: after
325
+ ? 'Genesis removed P2SH. The output is an ordinary script, so the hash-and-equal IS the whole check and the pushed bytes are data, never code. An implementation that still evaluates the redeem script rejects a spend the network accepts.'
326
+ : 'Before Genesis the redeem script runs, and this one leaves false on the stack. An implementation that skips P2SH evaluation here accepts a spend the network rejects.',
327
+ nodeExpects: after ? 'OK' : 'EVAL_FALSE',
328
+ regressionClass: after ? 'false-reject in <= 9.14.0' : 'agreed before and after',
329
+ tx: r.tx,
330
+ unlock: r.unlock,
331
+ lockScript: r.lk,
332
+ flags: f
333
+ })
334
+ }
335
+
336
+ // ---- Blind spot 4: restored Chronicle opcodes gate on the UTXO's era, not a local opt-in.
337
+ // An implementation that treats 0xb3-0xb5 as unconditional NOPs consumes nothing and lets an
338
+ // out-of-range argument through silently. That is a false accept.
339
+ for (const after of [false, true]) {
340
+ const f = I.SCRIPT_VERIFY_P2SH | I.SCRIPT_GENESIS | I.SCRIPT_UTXO_AFTER_GENESIS |
341
+ (after ? I.SCRIPT_UTXO_AFTER_CHRONICLE : 0)
342
+ // OP_LEFT with a length past the end of the operand.
343
+ const r = rawSpend('aabbcc OP_5', 'OP_LEFT', 1)
344
+ record({
345
+ id: 'op-left-out-of-range-' + (after ? 'after' : 'before') + '-chronicle',
346
+ blindSpot: 'restored opcodes follow the OUTPUT era, so a local opt-in flag reads as a NOP',
347
+ comment: after
348
+ ? 'Post-Chronicle UTXO: OP_LEFT is a real opcode and a length past the end is an error. An implementation treating 0xb3 as a NOP consumes nothing, leaves the operands, and accepts.'
349
+ : 'Pre-Chronicle UTXO: the byte is OP_NOP4, so it does nothing and the operands stay on the stack. Top of stack is 5, which is true, so the script succeeds.',
350
+ nodeExpects: after ? 'INVALID_NUMBER_RANGE' : 'OK',
351
+ regressionClass: after ? 'false-accept in <= 9.14.0' : 'agreed before and after',
352
+ tx: r.tx,
353
+ unlock: r.unlock,
354
+ lockScript: r.lk,
355
+ flags: f
356
+ })
357
+ }
358
+
359
+ // ---- Blind spot 5: a harness that injects opcode-enable flags the node does not define.
360
+ // The corpus has 77 rows for MUL, LSHIFT, RSHIFT and INVERT, but both this library's harness and
361
+ // @smartledger/bsv-core's granted MONOLITH and MAGNETIC to every row, so a gate keyed on those
362
+ // bits never fired. Published @smartledger/bsv 9.15.0 and bsv-core <= 1.0.1 refused all four
363
+ // opcodes without the flag, scoring 1483/1483 throughout. These vectors carry node flags only,
364
+ // with no library-only bits, so they test the node's rule rather than ours.
365
+ const MAGNETIC_SCRIPT = 'OP_1 OP_1 OP_LSHIFT OP_2 OP_EQUALVERIFY OP_2 OP_1 OP_RSHIFT OP_1 OP_EQUALVERIFY ' +
366
+ 'ff OP_INVERT 00 OP_EQUALVERIFY OP_2 OP_3 OP_MUL OP_6 OP_EQUAL'
367
+ const AFTER_GENESIS = I.SCRIPT_VERIFY_P2SH | I.SCRIPT_GENESIS | I.SCRIPT_UTXO_AFTER_GENESIS
368
+ for (const after of [false, true]) {
369
+ const r = rawSpend('', MAGNETIC_SCRIPT, 1)
370
+ record({
371
+ id: 'magnetic-opcodes-' + (after ? 'after' : 'before') + '-genesis',
372
+ blindSpot: 'harnesses grant opcode-enable flags the node does not define, so a gate keyed on them never fires',
373
+ comment: 'LSHIFT, RSHIFT, INVERT and MUL, each checked by value. The node has no flag for them: IsOpcodeDisabled disables only OP_2MUL and OP_2DIV. An implementation that gates these four on a local opt-in rejects a spend the network accepts.',
374
+ nodeExpects: 'OK',
375
+ regressionClass: 'false-reject in published 9.15.0',
376
+ tx: r.tx,
377
+ unlock: r.unlock,
378
+ lockScript: r.lk,
379
+ flags: after ? AFTER_GENESIS : I.SCRIPT_VERIFY_P2SH
380
+ })
381
+ }
382
+
383
+ // Before Genesis a disabled opcode fails even in a branch that does not run, so a gate that
384
+ // marks MUL as disabled rejects this script although MUL is never executed. OP_2MUL in the same
385
+ // position is the control: it IS disabled, so an implementation that removed the Magnetic gate
386
+ // by un-disabling everything accepts it, and that is a false accept.
387
+ for (const op of ['OP_MUL', 'OP_2MUL']) {
388
+ const r = rawSpend('', 'OP_0 OP_IF ' + op + ' OP_ENDIF OP_1', 1)
389
+ const disabled = op === 'OP_2MUL'
390
+ record({
391
+ id: (disabled ? 'disabled-2mul' : 'magnetic-mul') + '-unexecuted-before-genesis',
392
+ blindSpot: 'harnesses grant opcode-enable flags the node does not define, so a gate keyed on them never fires',
393
+ comment: disabled
394
+ ? 'OP_2MUL in a branch that does not run, before Genesis. It is still disabled, and before Genesis a disabled opcode fails whether or not it executes. An implementation that dropped the Magnetic gate by enabling every opcode accepts this.'
395
+ : 'OP_MUL in a branch that does not run, before Genesis. OP_MUL is not disabled, so the branch is skipped and the script succeeds. An implementation that treats MUL as disabled rejects it even though it never runs.',
396
+ nodeExpects: disabled ? 'DISABLED_OPCODE' : 'OK',
397
+ regressionClass: disabled ? 'agreed before and after' : 'false-reject in published 9.15.0',
398
+ tx: r.tx,
399
+ unlock: r.unlock,
400
+ lockScript: r.lk,
401
+ flags: I.SCRIPT_VERIFY_P2SH
402
+ })
403
+ }
404
+
405
+ console.log(JSON.stringify({
406
+ authoritative: 'nodeFlagsHex — it equals the set named in nodeFlags exactly. flagsHexAsRun is what this library was invoked with and additionally carries libraryOnlyFlags, which the node does not define and which change none of these verdicts.',
407
+ nodeErrorNames: NODE_ERROR_NAMES,
408
+ nodeShortNames: NODE_SHORT_NAMES,
409
+ nodeFlagBits: NODE_FLAGS,
410
+ libraryOnlyFlagBits: LIBRARY_ONLY_FLAGS,
411
+ narrowerNames: NARROWER,
412
+ foundInThirdPartyImplementations: 'These vectors found a false accept in the official ' +
413
+ 'bsv-blockchain/go-sdk v1.7.0, filed as https://github.com/bsv-blockchain/go-sdk/issues/373 ' +
414
+ '(checkHashTypeEncoding returns before the MUST_USE_FORKID check, so a signature with no ' +
415
+ 'FORKID bit is accepted under STRICTENC + EnableSighashForkID where the node rejects it). ' +
416
+ 'They also surfaced five unguarded EnforceNonMalleability sites in an independent Rust ' +
417
+ 'implementation. @bsv/sdk 2.8.11 agreed on all of them.',
418
+ note: 'Cross-implementation vectors for five defect classes in bitcoin-sv v1.2.2 script_tests.json. Every row of that corpus carries transaction version 1, and no row pairs SCRIPT_VERIFY_LOW_S with a hash-type expectation, so neither defect class below is reachable by replaying it. Flag names are the node\'s. nodeExpects is the verdict derived from bitcoin-sv v1.2.2 source, not from a live node.',
419
+ source: '@smartledger/bsv 9.15.0',
420
+ generated: new Date().toISOString().slice(0, 10),
421
+ vectors
422
+ }, null, 2))
@@ -7,7 +7,7 @@
7
7
  * in SmartLedger-BSV v3.0.2 after successful fixes.
8
8
  */
9
9
 
10
- const bsv = require('./index.js');
10
+ const bsv = require('../index.js');
11
11
 
12
12
  console.log('🔬 SmartLedger-BSV v3.0.2 - Signature Verification Test');
13
13
  console.log('======================================================\n');
@@ -9,7 +9,7 @@
9
9
  * Usage: node opcode_map.js
10
10
  */
11
11
 
12
- const bsv = require('./index.js');
12
+ const bsv = require('../index.js');
13
13
  const { Opcode, Script } = bsv;
14
14
  const Interpreter = bsv.Script.Interpreter;
15
15
 
@@ -40,11 +40,18 @@ const OPCODE_CATEGORIES = {
40
40
  ]
41
41
  },
42
42
 
43
+ // Every flagValue below is read from the Interpreter rather than written out. They used to
44
+ // be literals, and two of the three were wrong: MONOLITH said 1<<18 and MAGNETIC 1<<19, which
45
+ // are SCRIPT_GENESIS and SCRIPT_UTXO_AFTER_GENESIS. A caller OR-ing the value it was handed to
46
+ // "enable opcodes" would instead have declared the output post-Genesis and lifted every
47
+ // consensus limit. They were right when written and went stale when the flags moved to 1<<11
48
+ // and 1<<12; a second copy of a constant is the bug, not the digits.
49
+
43
50
  // Monolith opcodes (May 2018 upgrade)
44
51
  MONOLITH: {
45
52
  description: 'Monolith opcodes - require SCRIPT_ENABLE_MONOLITH_OPCODES',
46
53
  flag: 'SCRIPT_ENABLE_MONOLITH_OPCODES',
47
- flagValue: (1 << 18),
54
+ flagValue: Interpreter.SCRIPT_ENABLE_MONOLITH_OPCODES,
48
55
  opcodes: [
49
56
  'OP_CAT', 'OP_SPLIT', 'OP_NUM2BIN', 'OP_BIN2NUM', 'OP_AND', 'OP_OR', 'OP_XOR', 'OP_DIV', 'OP_MOD'
50
57
  ]
@@ -54,7 +61,7 @@ const OPCODE_CATEGORIES = {
54
61
  MAGNETIC: {
55
62
  description: 'Magnetic opcodes - require SCRIPT_ENABLE_MAGNETIC_OPCODES',
56
63
  flag: 'SCRIPT_ENABLE_MAGNETIC_OPCODES',
57
- flagValue: (1 << 19),
64
+ flagValue: Interpreter.SCRIPT_ENABLE_MAGNETIC_OPCODES,
58
65
  opcodes: [
59
66
  'OP_INVERT', 'OP_MUL', 'OP_LSHIFT', 'OP_RSHIFT'
60
67
  ]
@@ -73,7 +80,7 @@ const OPCODE_CATEGORIES = {
73
80
  NOPS: {
74
81
  description: 'NOP opcodes - may be discouraged with SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_NOPS',
75
82
  flag: 'SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_NOPS',
76
- flagValue: (1 << 7),
83
+ flagValue: Interpreter.SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_NOPS,
77
84
  opcodes: [
78
85
  'OP_NOP2', 'OP_NOP3', 'OP_NOP4', 'OP_NOP5', 'OP_NOP6', 'OP_NOP7', 'OP_NOP8', 'OP_NOP9', 'OP_NOP10'
79
86
  ]
@@ -6,7 +6,7 @@
6
6
  * Uses only real UTXOs to create and broadcast a transaction
7
7
  */
8
8
 
9
- const bsv = require('./index.js');
9
+ const bsv = require('../index.js');
10
10
  const https = require('https');
11
11
 
12
12
  console.log('🚀 Simple Real BSV Transaction Test');
@@ -0,0 +1,174 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * What can this corpus NOT tell us?
5
+ *
6
+ * A 1483/1483 score says we agree with the corpus. It says nothing about any rule the corpus
7
+ * never asks about, and the percentage cannot distinguish the two. This reports the difference.
8
+ *
9
+ * The idea is the bsv-scale-protocol session's: tally the verdicts a corpus expects, diff
10
+ * against the verdicts the engine can emit, and name what is left over. Its Rust version is
11
+ * `verdict coverage`; this is the JavaScript one.
12
+ *
13
+ * It reports three axes, because verdict coverage alone would NOT have caught either defect
14
+ * this library shipped:
15
+ *
16
+ * 1. VERDICTS — codes the engine can emit that no row expects. A guard that is present,
17
+ * reachable, and never once exercised by the file we certify ourselves against.
18
+ *
19
+ * 2. FLAG PAIRS — the axis that actually mattered. `SIG_HASHTYPE` IS exercised, by 7 rows, so
20
+ * verdict coverage calls it covered. But no row sets LOW_S at the same time, and LOW_S is
21
+ * what masked it. A check can only be shadowed by another check that is also on, so the
22
+ * unit of coverage for masking is the PAIR, not the flag.
23
+ *
24
+ * 3. TRANSACTION VERSION — every row here carries version 1, so no row can reach Chronicle's
25
+ * malleability relaxations, which apply only above 1. A single column, constant across the
26
+ * whole corpus, silently removed seven rules from the test.
27
+ *
28
+ * Exits 0 always: this measures the corpus, it does not judge the engine.
29
+ */
30
+
31
+ const fs = require('fs')
32
+ const path = require('path')
33
+ const harness = require('./sv-vector-harness')
34
+
35
+ const rows = require('../test/data/bitcoin-sv/script_tests.json')
36
+
37
+ // Flags BSV mainnet requires today. A low row count on one of these is worse than a low count
38
+ // on a policy flag, which presence/absence alone cannot say.
39
+ const MANDATORY_ON_MAINNET = ['SIGHASH_FORKID', 'STRICTENC', 'UTXO_AFTER_GENESIS']
40
+
41
+ // Worse than untested: the row format has no name for these, so the case cannot be written as a
42
+ // row at all and no amount of adding vectors to this file will cover it. Found by the
43
+ // bsv-scale-protocol session, checking for a bare CHRONICLE token and finding none.
44
+ const CANNOT_EXPRESS = [
45
+ {
46
+ flag: 'CHRONICLE (block era, 1<<20)',
47
+ why: 'no row names it — 42 rows carry UTXO_AFTER_CHRONICLE, the OUTPUT era, and none ' +
48
+ 'carries the block era. ILLEGAL_CHRONICLE gates on the block era, so that verdict ' +
49
+ 'is unreachable from this file by construction. It needs a hand-built vector.'
50
+ }
51
+ ]
52
+
53
+ /** Every SCRIPT_ERR_* this engine can produce, read from the source that produces them. */
54
+ function emittableVerdicts () {
55
+ const files = ['../lib/script/interpreter.js', '../lib/script/script.js']
56
+ const found = new Set()
57
+ for (const f of files) {
58
+ const src = fs.readFileSync(path.join(__dirname, f), 'utf8')
59
+ for (const m of src.matchAll(/SCRIPT_ERR_([A-Z0-9_]+)/g)) found.add(m[1])
60
+ }
61
+ return found
62
+ }
63
+
64
+ function main () {
65
+ const parsed = []
66
+ for (const raw of rows) {
67
+ if (!Array.isArray(raw) || raw.length === 1) continue
68
+ const row = harness.parseRow(raw)
69
+ if (row) parsed.push(row)
70
+ }
71
+
72
+ // ---- 1. verdicts ----
73
+ const expected = new Set()
74
+ for (const r of parsed) if (r.expected && r.expected !== 'OK') expected.add(r.expected)
75
+ const emittable = emittableVerdicts()
76
+ const aliases = harness.ERROR_CODE_ALIASES
77
+ const unexercised = [...emittable].filter((v) => {
78
+ if (expected.has(v)) return false
79
+ // A narrower name is exercised when the node's broader name is.
80
+ return !(aliases[v] && expected.has(aliases[v]))
81
+ }).sort()
82
+
83
+ // ---- 2. flag pairs ----
84
+ // Names come from the rows themselves, not from a lookup table: the corpus states its own
85
+ // flags, and a table would only reintroduce the mismatch that made the shared vectors give
86
+ // two answers earlier today.
87
+ const setCount = {}
88
+ const pairCount = {}
89
+ for (const r of parsed) {
90
+ const on = String(r.flagStr).split(',').map((s) => s.trim()).filter(Boolean)
91
+ for (const a of on) {
92
+ setCount[a] = (setCount[a] || 0) + 1
93
+ for (const b of on) if (a < b) pairCount[a + '+' + b] = (pairCount[a + '+' + b] || 0) + 1
94
+ }
95
+ }
96
+ // Flags that can mask another check by returning before it.
97
+ const MASKERS = ['LOW_S', 'DERSIG', 'STRICTENC', 'MINIMALDATA', 'SIGPUSHONLY', 'CLEANSTACK']
98
+ // A pair can be absent for two very different reasons, and they call for different fixes.
99
+ // The bsv-scale-protocol session made this point: with LOW_S in one row of 1483, most
100
+ // "untested pairs" are really reporting the scarcity of a single flag, and reading them as
101
+ // 13 independent gaps invites 13 vectors when adding rows that set LOW_S at all is cheaper
102
+ // and covers most of them.
103
+ const SCARCE = 5
104
+ const missingPairs = []
105
+ const scarcePairs = []
106
+ const neverSet = []
107
+ for (const a of MASKERS) {
108
+ if (!setCount[a]) { neverSet.push(a); continue }
109
+ for (const b of MASKERS) {
110
+ if (a >= b || !setCount[b]) continue
111
+ if (pairCount[a + '+' + b]) continue
112
+ const entry = `${a} (${setCount[a]}) + ${b} (${setCount[b]})`
113
+ if (setCount[a] <= SCARCE || setCount[b] <= SCARCE) scarcePairs.push(entry)
114
+ else missingPairs.push(entry)
115
+ }
116
+ }
117
+
118
+ // ---- 3. transaction version ----
119
+ const byVersion = {}
120
+ for (const r of parsed) byVersion[r.version] = (byVersion[r.version] || 0) + 1
121
+
122
+ const out = []
123
+ out.push('SV Node script corpus — what it cannot tell us')
124
+ out.push('')
125
+ out.push(`rows : ${parsed.length}`)
126
+ out.push(`verdicts this engine emits: ${emittable.size}`)
127
+ out.push(`verdicts the corpus asks : ${expected.size}`)
128
+ out.push('')
129
+ out.push(`--- ${unexercised.length} rules no row in this corpus exercises ---`)
130
+ out.push('agreement here says nothing about them')
131
+ for (const v of unexercised) out.push(' ' + v)
132
+ out.push('')
133
+ out.push('--- how often each flag is set at all ---')
134
+ out.push('a flag that is mandatory in production and set in a handful of rows is a standing')
135
+ out.push('risk, not a gap to be closed once')
136
+ for (const f of Object.keys(setCount).sort((x, y) => setCount[x] - setCount[y])) {
137
+ const pct = ((setCount[f] / parsed.length) * 100).toFixed(1)
138
+ const note = MANDATORY_ON_MAINNET.includes(f) ? ' <- mandatory on BSV mainnet' : ''
139
+ out.push(` ${String(setCount[f]).padStart(5)} ${pct.padStart(5)}% ${f}${note}`)
140
+ }
141
+ out.push('')
142
+ if (neverSet.length) {
143
+ out.push('--- masking flags no row sets at all ---')
144
+ out.push('every pair containing one of these would read as a gap; they are excluded above')
145
+ for (const f of neverSet) out.push(' ' + f)
146
+ out.push('')
147
+ }
148
+ out.push('--- pairs never set together, though BOTH flags are common ---')
149
+ out.push('a check can only be shadowed by another check that is also on, so masking is')
150
+ out.push('covered per PAIR, not per flag. These are the real gaps.')
151
+ if (missingPairs.length === 0) out.push(' (none)')
152
+ for (const p of missingPairs) out.push(' ' + p)
153
+ out.push('')
154
+ out.push(`--- pairs absent because one flag is scarce (<= ${SCARCE} rows) ---`)
155
+ out.push('adding rows that set the scarce flag at all is cheaper than one vector per pair')
156
+ for (const p of scarcePairs) out.push(' ' + p)
157
+ out.push('')
158
+ out.push('--- rules this corpus format cannot express ---')
159
+ for (const f of CANNOT_EXPRESS) {
160
+ out.push(` ${f.flag}: ${f.why}`)
161
+ }
162
+ out.push('')
163
+ out.push('--- transaction version ---')
164
+ for (const v of Object.keys(byVersion).sort()) {
165
+ out.push(` version ${v}: ${byVersion[v]} rows`)
166
+ }
167
+ if (Object.keys(byVersion).length === 1) {
168
+ out.push(' ONE version only. Every rule gated on the transaction version is untested,')
169
+ out.push(' which on BSV means all seven of Chronicle\'s malleability relaxations.')
170
+ }
171
+ console.log(out.join('\n'))
172
+ }
173
+
174
+ main()