@smartledger/bsv 9.15.0 → 9.16.1

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,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()
@@ -30,10 +30,14 @@ const BufferWriter = bsv.encoding.BufferWriter
30
30
 
31
31
  const rawVectors = require('../test/data/bitcoin-sv/script_tests.json')
32
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.
33
+ // Mapped by name rather than by value, and the reason is no longer the one this comment used
34
+ // to give. It said this library puts MONOLITH and MAGNETIC on 1<<18 and 1<<19, colliding with
35
+ // the node's SCRIPT_GENESIS and SCRIPT_UTXO_AFTER_GENESIS. That collision was real once and was
36
+ // removed: they moved to 1<<11 and 1<<12, which the node leaves unassigned. Bits 18 and 19 now
37
+ // mean the same thing on both sides.
38
+ //
39
+ // Name mapping is still right, for a better reason: the node has no MONOLITH or MAGNETIC flag
40
+ // at all, so there is no value to map to. A row names a rule; only the name is portable.
37
41
  const FLAG_MAP = {
38
42
  NONE: 'SCRIPT_VERIFY_NONE',
39
43
  P2SH: 'SCRIPT_VERIFY_P2SH',
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 = '9.15.0'
3
+ module.exports = '9.16.1'