@smartledger/bsv 8.1.0 → 8.2.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,304 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Shared harness for the SV Node script vectors in test/data/bitcoin-sv.
5
+ *
6
+ * These are the reference node's own test data, copied verbatim, and they are
7
+ * the specification this interpreter is measured against. The corpus in
8
+ * test/data/bitcoind is Bitcoin Core's and encodes rules BSV abandoned at
9
+ * Genesis in 2020; it is not a consensus reference for this library.
10
+ *
11
+ * Used by tools/sv-vector-report.js (which reports) and
12
+ * test/consensus/sv-script-vectors.js (which ratchets), so the progress report
13
+ * and the regression gate cannot disagree about what passes.
14
+ *
15
+ * On eras. The node decides most rules by the era of the *output being spent*,
16
+ * carried in SCRIPT_UTXO_AFTER_GENESIS and SCRIPT_UTXO_AFTER_CHRONICLE, and a
17
+ * few by the era of the *spending transaction*, carried in SCRIPT_GENESIS and
18
+ * SCRIPT_CHRONICLE.
19
+ *
20
+ * Era flags are passed through as the node's own, which is what the rows name.
21
+ */
22
+
23
+ const bsv = require('..')
24
+ const Script = bsv.Script
25
+ const Opcode = bsv.Opcode
26
+ const Interpreter = bsv.Script.Interpreter
27
+ const Transaction = bsv.Transaction
28
+ const BN = bsv.crypto.BN
29
+ const BufferWriter = bsv.encoding.BufferWriter
30
+
31
+ const rawVectors = require('../test/data/bitcoin-sv/script_tests.json')
32
+
33
+ // Mapped by name rather than by value. This library assigns MONOLITH and
34
+ // MAGNETIC to 1<<18 and 1<<19, which are the bits the node uses for
35
+ // SCRIPT_GENESIS and SCRIPT_UTXO_AFTER_GENESIS, so mapping by value would
36
+ // quietly mean something else.
37
+ const FLAG_MAP = {
38
+ NONE: 'SCRIPT_VERIFY_NONE',
39
+ P2SH: 'SCRIPT_VERIFY_P2SH',
40
+ STRICTENC: 'SCRIPT_VERIFY_STRICTENC',
41
+ DERSIG: 'SCRIPT_VERIFY_DERSIG',
42
+ LOW_S: 'SCRIPT_VERIFY_LOW_S',
43
+ NULLDUMMY: 'SCRIPT_VERIFY_NULLDUMMY',
44
+ SIGPUSHONLY: 'SCRIPT_VERIFY_SIGPUSHONLY',
45
+ MINIMALDATA: 'SCRIPT_VERIFY_MINIMALDATA',
46
+ DISCOURAGE_UPGRADABLE_NOPS: 'SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_NOPS',
47
+ CLEANSTACK: 'SCRIPT_VERIFY_CLEANSTACK',
48
+ CHECKLOCKTIMEVERIFY: 'SCRIPT_VERIFY_CHECKLOCKTIMEVERIFY',
49
+ CHECKSEQUENCEVERIFY: 'SCRIPT_VERIFY_CHECKSEQUENCEVERIFY',
50
+ MINIMALIF: 'SCRIPT_VERIFY_MINIMALIF',
51
+ NULLFAIL: 'SCRIPT_VERIFY_NULLFAIL',
52
+ COMPRESSED_PUBKEYTYPE: 'SCRIPT_VERIFY_COMPRESSED_PUBKEYTYPE',
53
+ SIGHASH_FORKID: 'SCRIPT_ENABLE_SIGHASH_FORKID',
54
+ REPLAY_PROTECTION: 'SCRIPT_ENABLE_REPLAY_PROTECTION',
55
+ MONOLITH_OPCODES: 'SCRIPT_ENABLE_MONOLITH_OPCODES',
56
+ MAGNETIC_OPCODES: 'SCRIPT_ENABLE_MAGNETIC_OPCODES'
57
+ }
58
+
59
+ // The corpus never names MONOLITH or MAGNETIC, because the node has no such
60
+ // flags — those opcodes were restored on BSV in 2018 and are simply enabled.
61
+ // This library still gates them, so they are enabled for every row; otherwise
62
+ // the report is dominated by that difference rather than by consensus.
63
+ // Requiring them is itself a divergence, counted separately rather than here.
64
+ const OPCODE_BASELINE =
65
+ Interpreter.SCRIPT_ENABLE_MONOLITH_OPCODES | Interpreter.SCRIPT_ENABLE_MAGNETIC_OPCODES
66
+
67
+ /**
68
+ * Result codes this library reports more narrowly than the node does, and the
69
+ * node code each stands for.
70
+ *
71
+ * Matching the node's accept/reject outcome is not the same as failing for the
72
+ * node's reason, and the gap between the two hid real bugs: a div-by-zero
73
+ * guard comparing a BN against the number 0, so that it never fired and bn.js
74
+ * asserted instead; script number decoding throwing past the evaluator into a
75
+ * generic catch; a truncated PUSHDATA doing the same. All three left the
76
+ * script failing, just not in the way the node fails it, so the outcome check
77
+ * stayed green through every one.
78
+ *
79
+ * This is a short statement of intent rather than a list of exempted vectors,
80
+ * and it ratchets both ways — an unlisted mismatch fails, and so does an alias
81
+ * no vector produces any more. Anything added here should be a name that says
82
+ * *more* than the node's, never a different failure wearing its label.
83
+ */
84
+ const ERROR_CODE_ALIASES = {
85
+ EVAL_FALSE_IN_STACK: 'EVAL_FALSE',
86
+ EVAL_FALSE_NO_RESULT: 'EVAL_FALSE',
87
+ EVAL_FALSE_NO_P2SH_STACK: 'EVAL_FALSE',
88
+ EVAL_FALSE_IN_P2SH_STACK: 'EVAL_FALSE',
89
+ SIG_DER_INVALID_FORMAT: 'SIG_DER',
90
+ SIG_DER_HIGH_S: 'SIG_HIGH_S',
91
+ INVALID_OPERAND_SIZE: 'OPERAND_SIZE',
92
+ INVALID_SPLIT_RANGE: 'SPLIT_RANGE'
93
+ }
94
+
95
+ /** The bare result code, without the prefix or any appended detail. */
96
+ function errorCode (errstr) {
97
+ return String(errstr || '').replace(/^SCRIPT_ERR_/, '').split(':')[0].trim()
98
+ }
99
+
100
+ /** Does what we reported match the node's `expected` code, or alias to it? */
101
+ function errorCodeMatches (expected, errstr) {
102
+ const got = errorCode(errstr)
103
+ return got === expected || ERROR_CODE_ALIASES[got] === expected
104
+ }
105
+
106
+ /** Parse a bitcoind-format script string, e.g. "0x47 0x3044... CHECKSIG". */
107
+ function fromBitcoindString (str) {
108
+ const bw = new BufferWriter()
109
+ for (const token of String(str).split(' ')) {
110
+ if (token === '') continue
111
+ if (token[0] === '0' && token[1] === 'x') {
112
+ bw.write(Buffer.from(token.slice(2), 'hex'))
113
+ } else if (token[0] === "'") {
114
+ bw.write(Script().add(Buffer.from(token.slice(1, token.length - 1))).toBuffer())
115
+ } else if (typeof Opcode['OP_' + token] !== 'undefined') {
116
+ bw.writeUInt8(Opcode['OP_' + token])
117
+ } else if (typeof Opcode[token] === 'number') {
118
+ bw.writeUInt8(Opcode[token])
119
+ } else if (!isNaN(parseInt(token))) {
120
+ bw.write(Script().add(new BN(token).toScriptNumBuffer()).toBuffer())
121
+ } else {
122
+ const err = new Error('unknown script token: ' + token)
123
+ err.token = token
124
+ throw err
125
+ }
126
+ }
127
+ return Script.fromBuffer(bw.concat())
128
+ }
129
+
130
+ /**
131
+ * Row layout, from script_json_test in the node's src/test/script_tests.cpp:
132
+ *
133
+ * [ [nValue]?, txnVersion, scriptSig, scriptPubKey, flags, expected, comment? ]
134
+ *
135
+ * nValue appears only where a row needs an amount and is array-wrapped in
136
+ * whole coins. txnVersion is the spending transaction's version, which
137
+ * Chronicle uses to gate its malleability relaxations — it is not padding and
138
+ * it is not an amount.
139
+ */
140
+ function parseRow (row) {
141
+ let pos = 0
142
+ let value = 0
143
+ if (Array.isArray(row[0])) {
144
+ value = Math.round(parseFloat(row[0][0]) * 1e8)
145
+ pos++
146
+ }
147
+ if (row.length < 4 + pos) return null
148
+ return {
149
+ version: parseInt(row[pos], 10),
150
+ scriptSig: row[pos + 1],
151
+ scriptPubKey: row[pos + 2],
152
+ flagStr: row[pos + 3],
153
+ expected: row[pos + 4],
154
+ satoshis: value,
155
+ comment: row.slice(pos + 5).join(' ')
156
+ }
157
+ }
158
+
159
+ function classifyFlags (flagStr) {
160
+ let flags = OPCODE_BASELINE
161
+ const unknown = []
162
+ for (const raw of String(flagStr).split(',')) {
163
+ const name = raw.trim()
164
+ if (!name) continue
165
+ if (FLAG_MAP[name] !== undefined) {
166
+ flags |= Interpreter[FLAG_MAP[name]]
167
+ } else if (name === 'GENESIS') {
168
+ flags |= Interpreter.SCRIPT_GENESIS
169
+ } else if (name === 'UTXO_AFTER_GENESIS') {
170
+ flags |= Interpreter.SCRIPT_UTXO_AFTER_GENESIS
171
+ } else if (name === 'UTXO_AFTER_CHRONICLE') {
172
+ flags |= Interpreter.SCRIPT_UTXO_AFTER_CHRONICLE
173
+ } else {
174
+ unknown.push(name)
175
+ }
176
+ }
177
+ return { flags, unknown }
178
+ }
179
+
180
+ /**
181
+ * Build and verify the crediting/spending transaction pair, mirroring
182
+ * BuildCreditingTransaction and BuildSpendingTransaction in the node's
183
+ * src/test/script_tests.cpp. The crediting transaction is always version 1;
184
+ * only the spending transaction carries the version under test.
185
+ */
186
+ function evaluate (row, flags) {
187
+ const scriptSig = fromBitcoindString(row.scriptSig)
188
+ const scriptPubKey = fromBitcoindString(row.scriptPubKey)
189
+
190
+ const credtx = new Transaction()
191
+ credtx.version = 1
192
+ credtx.nLockTime = 0
193
+ credtx.uncheckedAddInput(new Transaction.Input({
194
+ prevTxId: '0'.repeat(64),
195
+ outputIndex: 0xffffffff,
196
+ sequenceNumber: 0xffffffff,
197
+ script: Script('OP_0 OP_0')
198
+ }))
199
+ credtx.addOutput(new Transaction.Output({ script: scriptPubKey, satoshis: row.satoshis }))
200
+
201
+ const spendtx = new Transaction()
202
+ spendtx.version = row.version
203
+ spendtx.nLockTime = 0
204
+ spendtx.uncheckedAddInput(new Transaction.Input({
205
+ prevTxId: credtx.id.toString('hex'),
206
+ outputIndex: 0,
207
+ sequenceNumber: 0xffffffff,
208
+ script: scriptSig
209
+ }))
210
+ spendtx.addOutput(new Transaction.Output({ script: Script(), satoshis: row.satoshis }))
211
+
212
+ const interp = new Interpreter()
213
+ const ok = interp.verify(scriptSig, scriptPubKey, spendtx, 0, flags, new BN(row.satoshis))
214
+ return { ok, errstr: interp.errstr }
215
+ }
216
+
217
+ /** A stable id for a row, so a known-failure list survives reordering. */
218
+ function vectorId (raw) {
219
+ return require('crypto').createHash('sha256')
220
+ .update(JSON.stringify(raw)).digest('hex').slice(0, 12)
221
+ }
222
+
223
+ /**
224
+ * Run every vector. Each result is
225
+ *
226
+ * { id, row, passed, reason, direction, gotErrstr }
227
+ *
228
+ * `direction` is 'accept' where this library accepted a script the node
229
+ * rejects — the direction that can cost money — and 'reject' for the converse.
230
+ */
231
+ function runAll () {
232
+ const results = []
233
+ for (const raw of rawVectors) {
234
+ if (!Array.isArray(raw) || raw.length < 4) continue
235
+ const row = parseRow(raw)
236
+ if (row === null) continue
237
+
238
+ const cls = classifyFlags(row.flagStr)
239
+ const expectOk = row.expected === 'OK'
240
+
241
+ let got = null
242
+ let thrown = null
243
+ try {
244
+ got = evaluate(row, cls.flags)
245
+ } catch (e) {
246
+ thrown = e
247
+ }
248
+
249
+ let reason = null
250
+ let direction = null
251
+ if (thrown !== null) {
252
+ reason = 'threw: ' + String(thrown.message).slice(0, 70)
253
+ direction = 'reject'
254
+ } else if (got.ok !== expectOk) {
255
+ if (expectOk) {
256
+ reason = 'expected OK, rejected with ' + got.errstr
257
+ direction = 'reject'
258
+ } else {
259
+ reason = 'expected ' + row.expected + ', but ACCEPTED'
260
+ direction = 'accept'
261
+ }
262
+ }
263
+
264
+ // Only meaningful where the node rejects and so does this library: there
265
+ // is no code to compare on a row the node accepts, and a row whose outcome
266
+ // already disagrees is reported as that rather than twice.
267
+ const comparable = !expectOk && reason === null
268
+ results.push({
269
+ id: vectorId(raw),
270
+ row,
271
+ unknownFlags: cls.unknown,
272
+ gotErrstr: got ? got.errstr : null,
273
+ passed: reason === null,
274
+ reason,
275
+ direction,
276
+ expectedCode: comparable ? row.expected : null,
277
+ gotCode: comparable ? errorCode(got.errstr) : null,
278
+ codeMatches: comparable ? errorCodeMatches(row.expected, got.errstr) : null
279
+ })
280
+ }
281
+ return results
282
+ }
283
+
284
+ /** A one-line description of a row, for failure output. */
285
+ function describe (row) {
286
+ return String(row.scriptSig).slice(0, 40) + ' | ' +
287
+ String(row.scriptPubKey).slice(0, 40) +
288
+ ' [' + row.flagStr + ']' +
289
+ (row.comment ? ' // ' + row.comment.slice(0, 50) : '')
290
+ }
291
+
292
+ module.exports = {
293
+ runAll,
294
+ evaluate,
295
+ describe,
296
+ parseRow,
297
+ vectorId,
298
+ classifyFlags,
299
+ fromBitcoindString,
300
+ errorCode,
301
+ errorCodeMatches,
302
+ ERROR_CODE_ALIASES,
303
+ FLAG_MAP
304
+ }
@@ -0,0 +1,88 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * How far this interpreter is from the reference node.
5
+ *
6
+ * npm run vectors:sv
7
+ * npm run vectors:sv -- --verbose
8
+ *
9
+ * False accepts are listed first and separately. Accepting a script the
10
+ * network rejects is the direction that can cost money; rejecting one it
11
+ * accepts only costs a transaction.
12
+ */
13
+
14
+ const harness = require('./sv-vector-harness')
15
+
16
+ const verbose = process.argv.indexOf('--verbose') !== -1
17
+ const results = harness.runAll()
18
+
19
+ const failing = results.filter(r => !r.passed)
20
+ const accepts = failing.filter(r => r.direction === 'accept')
21
+ const rejects = failing.filter(r => r.direction === 'reject')
22
+ const passing = results.length - failing.length
23
+
24
+ console.log()
25
+ console.log('=== SV Node v1.2.0 script vectors ===')
26
+ console.log()
27
+ console.log('vectors run :', results.length)
28
+ console.log('passing :', passing,
29
+ ' (' + (100 * passing / results.length).toFixed(1) + '%)')
30
+ console.log('failing :', failing.length,
31
+ ' (' + (100 * failing.length / results.length).toFixed(1) + '%)')
32
+ console.log()
33
+ console.log('--- direction ---')
34
+ console.log(' false accepts (we accept, node rejects) :', accepts.length,
35
+ accepts.length ? ' <- the direction that can cost money' : '')
36
+ console.log(' false rejects (we reject, node accepts) :', rejects.length)
37
+
38
+ const comparable = results.filter(r => r.codeMatches !== null)
39
+ const wrongCode = comparable.filter(r => !r.codeMatches)
40
+ const aliased = comparable.filter(r => r.codeMatches && r.gotCode !== r.expectedCode)
41
+ console.log()
42
+ console.log('--- result codes, on the', comparable.length, 'vectors the node rejects ---')
43
+ console.log(' exact match :',
44
+ comparable.length - wrongCode.length - aliased.length)
45
+ console.log(' matched via a documented narrower name :', aliased.length)
46
+ console.log(' wrong reason :', wrongCode.length,
47
+ wrongCode.length ? ' <- the outcome is right, the reason is not' : '')
48
+
49
+ if (wrongCode.length) {
50
+ const byCode = {}
51
+ wrongCode.forEach(function (r) {
52
+ const key = r.expectedCode + ' <- ' + (r.gotCode || '(none)')
53
+ byCode[key] = (byCode[key] || 0) + 1
54
+ })
55
+ Object.keys(byCode).sort((a, b) => byCode[b] - byCode[a])
56
+ .forEach(k => console.log(String(byCode[k]).padStart(5), k))
57
+ }
58
+
59
+ const byReason = {}
60
+ failing.forEach(function (r) {
61
+ const key = String(r.reason).replace(/vector.*/, '').slice(0, 62)
62
+ byReason[key] = (byReason[key] || 0) + 1
63
+ })
64
+ console.log()
65
+ console.log('--- failure reasons ---')
66
+ Object.keys(byReason).sort((a, b) => byReason[b] - byReason[a]).slice(0, 16)
67
+ .forEach(k => console.log(String(byReason[k]).padStart(5), k))
68
+
69
+ if (accepts.length) {
70
+ console.log()
71
+ console.log('--- false accepts ---')
72
+ accepts.slice(0, verbose ? accepts.length : 10).forEach(function (r) {
73
+ console.log(' ' + r.id + ' ' + harness.describe(r.row))
74
+ })
75
+ if (!verbose && accepts.length > 10) {
76
+ console.log(' ... and ' + (accepts.length - 10) + ' more; pass --verbose')
77
+ }
78
+ }
79
+
80
+ if (verbose) {
81
+ console.log()
82
+ console.log('--- every failing vector ---')
83
+ failing.forEach(function (r) {
84
+ console.log(' ' + r.id + ' ' + r.reason)
85
+ console.log(' ' + harness.describe(r.row))
86
+ })
87
+ }
88
+ console.log()
package/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  'use strict'
2
2
  // GENERATED by scripts/sync-version.js on `npm version` — do not edit.
3
- module.exports = '8.1.0'
3
+ module.exports = '8.2.0'