trac-msb 0.2.24 → 0.2.27

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "trac-msb",
3
3
  "main": "msb.mjs",
4
- "version": "0.2.24",
4
+ "version": "0.2.27",
5
5
  "pear": {
6
6
  "name": "trac-msb",
7
7
  "type": "terminal"
@@ -13,10 +13,12 @@
13
13
  "NOTICE",
14
14
  "rpc/",
15
15
  "SECURITY.md",
16
+ "scripts/hypercore-stallfix/",
16
17
  "scripts/pear-runner.js",
17
18
  "src/"
18
19
  ],
19
20
  "scripts": {
21
+ "postinstall": "node scripts/hypercore-stallfix/apply.mjs",
20
22
  "start": "NODE_OPTIONS='--max-old-space-size=4096' node scripts/pear-runner.js",
21
23
  "rpc": "NODE_OPTIONS='--max-old-space-size=4096' node scripts/pear-runner.js --rpc --host ${npm_config_host} --port ${npm_config_port}",
22
24
  "env": "if [ -f .env ]; then set -a; . ./.env; set +a; fi; NODE_OPTIONS='--max-old-space-size=4096' node scripts/pear-runner.js --stores-directory ${STORES_DIRECTORY:-stores} --network ${NETWORK:-mainnet}",
@@ -28,11 +30,12 @@
28
30
  "docs:check": "node scripts/docs/docs-checker.js",
29
31
  "test:acceptance": "node --experimental-vm-modules node_modules/jest/bin/jest.js --testTimeout=200000 tests/acceptance/v1/rpc.test.mjs --runInBand",
30
32
  "test:integration": "brittle-node -t 1200000 tests/integration/integration.test.js",
33
+ "test:unit:prepare": "node scripts/hypercore-stallfix/apply.mjs && node scripts/hypercore-stallfix/apply.mjs --check",
31
34
  "test:unit:node": "brittle-node -t 60000 tests/unit/unit.test.js",
32
35
  "test:unit:bare": "brittle-bare -t 60000 tests/unit/unit.test.js",
33
36
  "test:unit:pear": "BRITTLE='-t 60000' brittle-pear tests/unit/unit.test.js",
34
37
  "test:pear-runner": "node --test tests/runner/pear-runner.test.js",
35
- "test:unit:all": "(npm run test:unit:node && npm run test:unit:bare && npm run test:pear-runner) || exit 1",
38
+ "test:unit:all": "(npm run test:unit:prepare && npm run test:unit:node && npm run test:unit:bare && npm run test:pear-runner) || exit 1",
36
39
  "test:unit:node:cov": "brittle-node -c -t 60000 tests/unit/unit.test.js"
37
40
  },
38
41
  "dependencies": {
package/rpc/handlers.js CHANGED
@@ -24,6 +24,15 @@ export async function handleHealth({ msbInstance, respond }) {
24
24
  }
25
25
  }
26
26
 
27
+ export async function handleReplication({ msbInstance, respond }) {
28
+ try {
29
+ if (!msbInstance?.opened || typeof msbInstance.replicationStatus !== 'function') throw new Error("RPC_OFFLINE");
30
+ respond(200, msbInstance.replicationStatus());
31
+ } catch {
32
+ respond(503, { error: "Could not connect to RPC server" });
33
+ }
34
+ }
35
+
27
36
  export async function handleBalance({ req, respond, msbInstance }) {
28
37
  const url = buildRequestUrl(req);
29
38
  const parts = url.pathname.split("/").filter(Boolean);
package/rpc/routes/v1.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  handleHealth,
3
+ handleReplication,
3
4
  handleBalance,
4
5
  handleTxv,
5
6
  handleFee,
@@ -15,6 +16,7 @@ import {
15
16
 
16
17
  export const v1Routes = [
17
18
  { method: 'GET', path: '/health', handler: handleHealth },
19
+ { method: 'GET', path: '/replication', handler: handleReplication },
18
20
  { method: 'GET', path: '/balance', handler: handleBalance },
19
21
  { method: 'GET', path: '/txv', handler: handleTxv },
20
22
  { method: 'GET', path: '/fee', handler: handleFee },
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env node
2
+ // OPENMAYHEM-STALLFIX: install-time Hypercore patch (identical copy in trac-msb and trac-peer, run from postinstall).
3
+ //
4
+ // Why: Hypercore block requests have no age limit, and a responder that has paused a peer drops that peer's block
5
+ // requests silently. A node whose needed request sits on such a peer never advances until the connection closes.
6
+ // The patch (one exact-text patch set per supported Hypercore version, see hypercore-<version>.mjs here):
7
+ // - cancels an in-flight request after 30 s on the wire, re-queues it for another peer and gives the silent peer a
8
+ // cool-down (30 s doubling to 4 min); one unref'd sweep per replicator, bounded work per sweep;
9
+ // - backports upstream 8cf8ad9 (#896) fix 2 (a stale block-queue entry no longer consumes a peer update);
10
+ // - answers NOT_AVAILABLE (max 16 per minute per peer, silent beyond) to a peer we paused, instead of silence.
11
+ // - backports upstream c24f37a (#911): never attach a stream that closed while the core was opening (v2).
12
+ // No wire-format change: patched and unpatched nodes interoperate.
13
+ //
14
+ // What this script does: finds every Hypercore copy this package loads (its own resolution plus the copies that
15
+ // autobase, corestore, trac-msb and trac-peer resolve from here, hoisted or nested), and for each copy:
16
+ // - supported version, file hash = original -> applies the patch set (each original text must occur exactly once),
17
+ // checks the result hash, writes atomically (tmp + rename);
18
+ // - supported version, file hash = patched -> "already patched" (idempotent; safe when both packages run it);
19
+ // - supported version, file hash = released v1 -> upgrades only the marker and #911 guard, checks the result hash;
20
+ // - supported version, any other content -> exit 1 with a clear message (nothing is written);
21
+ // - unsupported version -> loud warning, no change.
22
+ // Usage: node scripts/hypercore-stallfix/apply.mjs [--check] [--quiet]
23
+ // --check only report; exit 1 if a supported copy is not patched.
24
+ import fs from 'node:fs'
25
+ import path from 'node:path'
26
+ import crypto from 'node:crypto'
27
+ import { fileURLToPath, pathToFileURL } from 'node:url'
28
+
29
+ const here = path.dirname(fileURLToPath(import.meta.url))
30
+ const PATCH_SETS = ['hypercore-11.18.3.mjs', 'hypercore-11.8.3.mjs']
31
+ const FOLLOW = ['autobase', 'corestore', 'trac-msb', 'trac-peer']
32
+ const MAX_DEPTH = 3
33
+
34
+ export async function loadPatchSets() {
35
+ const sets = new Map()
36
+ for (const name of PATCH_SETS) {
37
+ const mod = await import(pathToFileURL(path.join(here, name)).href)
38
+ sets.set(mod.default.version, mod.default)
39
+ }
40
+ return sets
41
+ }
42
+
43
+ function sha256(text) {
44
+ return crypto.createHash('sha256').update(text).digest('hex')
45
+ }
46
+
47
+ function findPackageDir(name, fromDir) {
48
+ let dir = fromDir
49
+ for (;;) {
50
+ const candidate = path.join(dir, 'node_modules', name)
51
+ if (fs.existsSync(path.join(candidate, 'package.json'))) return fs.realpathSync(candidate)
52
+ const parent = path.dirname(dir)
53
+ if (parent === dir) return null
54
+ dir = parent
55
+ }
56
+ }
57
+
58
+ // Every Hypercore copy loaded from packageDir: bounded walk over FOLLOW packages, deduplicated by real path.
59
+ export function findHypercoreCopies(packageDir) {
60
+ const copies = new Map()
61
+ const seen = new Set()
62
+ let level = [fs.realpathSync(packageDir)]
63
+ for (let depth = 0; depth <= MAX_DEPTH && level.length > 0; depth++) {
64
+ const next = []
65
+ for (const dir of level) {
66
+ if (seen.has(dir)) continue
67
+ seen.add(dir)
68
+ const hc = findPackageDir('hypercore', dir)
69
+ if (hc !== null && !copies.has(hc)) copies.set(hc, path.relative(packageDir, dir) || '.')
70
+ for (const name of FOLLOW) {
71
+ const d = findPackageDir(name, dir)
72
+ if (d !== null && !seen.has(d)) next.push(d)
73
+ }
74
+ }
75
+ level = next
76
+ }
77
+ return [...copies.entries()].map(([dir, from]) => ({ dir, from }))
78
+ }
79
+
80
+ export function applyPatchSet(text, set) {
81
+ let out = text
82
+ for (const [original, replacement] of set.edits) {
83
+ const parts = out.split(original)
84
+ if (parts.length !== 2) {
85
+ throw new Error('expected original text exactly once, found ' + (parts.length - 1) + ': ' + JSON.stringify(original.slice(0, 80)))
86
+ }
87
+ out = parts[0] + replacement + parts[1]
88
+ }
89
+ return out
90
+ }
91
+
92
+ // Reverse of applyPatchSet (tests and forensics): restores the original text from a patched file.
93
+ export function unapplyPatchSet(text, set) {
94
+ let out = text
95
+ for (let i = set.edits.length - 1; i >= 0; i--) {
96
+ const [original, replacement] = set.edits[i]
97
+ const parts = out.split(replacement)
98
+ if (parts.length !== 2) throw new Error('expected patched text exactly once, found ' + (parts.length - 1))
99
+ out = parts[0] + original + parts[1]
100
+ }
101
+ return out
102
+ }
103
+
104
+ export function sha256Hex(text) {
105
+ return sha256(text)
106
+ }
107
+
108
+ export function inspectCopy(copy, sets) {
109
+ const pkg = JSON.parse(fs.readFileSync(path.join(copy.dir, 'package.json'), 'utf8'))
110
+ const set = sets.get(pkg.version) || null
111
+ const result = { dir: copy.dir, from: copy.from, version: pkg.version, supported: set !== null, state: null, sha256: null }
112
+ if (set === null) {
113
+ result.state = 'unsupported-version'
114
+ return result
115
+ }
116
+ const file = path.join(copy.dir, set.file)
117
+ const text = fs.readFileSync(file, 'utf8')
118
+ // keep the inspected text: patching must not re-read the file, a parallel run may have replaced it meanwhile
119
+ Object.defineProperty(result, 'text', { value: text, enumerable: false })
120
+ result.sha256 = sha256(text)
121
+ if (result.sha256 === set.patchedSha256) result.state = 'patched'
122
+ else if (result.sha256 === set.originalSha256) result.state = 'original'
123
+ else if (result.sha256 === set.previousPatchedSha256) result.state = 'previous-patched'
124
+ else if (text.includes('OPENMAYHEM-STALLFIX')) result.state = 'other-stallfix-revision'
125
+ else result.state = 'mismatch'
126
+ return result
127
+ }
128
+
129
+ export async function run({ packageDir, check = false, quiet = false, log = console.log, warn = console.error } = {}) {
130
+ const sets = await loadPatchSets()
131
+ const copies = findHypercoreCopies(packageDir)
132
+ const name = JSON.parse(fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8')).name
133
+ const tag = '[hypercore-stallfix ' + name + ']'
134
+ let failed = false
135
+
136
+ if (copies.length === 0) warn(tag + ' WARNING: no hypercore copy found from ' + packageDir + ' (nothing patched)')
137
+
138
+ const results = []
139
+ for (const copy of copies) {
140
+ const r = inspectCopy(copy, sets)
141
+ const where = r.dir + ' (via ' + r.from + ')'
142
+ if (r.state === 'unsupported-version') {
143
+ warn(tag + ' WARNING: hypercore ' + r.version + ' at ' + where + ' is not a supported version (' + [...sets.keys()].join(', ') + '); NOT patched')
144
+ } else if (r.state === 'patched') {
145
+ if (!quiet) log(tag + ' hypercore ' + r.version + ' at ' + where + ': already patched (sha256 ' + r.sha256 + ')')
146
+ } else if ((r.state === 'original' || r.state === 'previous-patched') && check) {
147
+ warn(tag + ' hypercore ' + r.version + ' at ' + where + ': NOT patched to v2' + (r.state === 'previous-patched' ? ' (released v1 needs upgrade)' : ''))
148
+ failed = true
149
+ } else if (r.state === 'original' || r.state === 'previous-patched') {
150
+ const set = sets.get(r.version)
151
+ const file = path.join(r.dir, set.file)
152
+ const upgrading = r.state === 'previous-patched'
153
+ let patched
154
+ try {
155
+ patched = applyPatchSet(r.text, upgrading ? { edits: set.upgradeEdits } : set)
156
+ } catch (err) {
157
+ warn(tag + ' ERROR: hypercore ' + r.version + ' at ' + where + ': ' + err.message + '; nothing written')
158
+ failed = true
159
+ results.push(r)
160
+ continue
161
+ }
162
+ if (sha256(patched) !== set.patchedSha256) {
163
+ warn(tag + ' ERROR: hypercore ' + r.version + ' at ' + where + ': patched result has an unexpected hash; nothing written')
164
+ failed = true
165
+ results.push(r)
166
+ continue
167
+ }
168
+ // atomic: a parallel run (trac-msb and trac-peer postinstall) writes the same bytes, rename keeps one
169
+ const tmp = file + '.stallfix-' + process.pid + '.tmp'
170
+ fs.writeFileSync(tmp, patched)
171
+ fs.renameSync(tmp, file)
172
+ r.state = 'patched'
173
+ r.sha256 = set.patchedSha256
174
+ log(tag + ' hypercore ' + r.version + ' at ' + where + ': ' + (upgrading ? 'UPGRADED v1 -> v2' : 'PATCHED v2') + ' (sha256 ' + r.sha256 + ')')
175
+ } else {
176
+ const set = sets.get(r.version)
177
+ warn(tag + ' ERROR: hypercore ' + r.version + ' at ' + where + ' has unexpected ' + set.file + ' content (sha256 ' + r.sha256 +
178
+ (r.state === 'other-stallfix-revision' ? ', patched by a different stallfix revision' : '') +
179
+ '; expected original ' + set.originalSha256 + ', released v1 ' + set.previousPatchedSha256 + ' or patched v2 ' + set.patchedSha256 + '). Refusing to patch.')
180
+ failed = true
181
+ }
182
+ results.push(r)
183
+ }
184
+ return { ok: !failed, results }
185
+ }
186
+
187
+ if (process.argv[1] && fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
188
+ const args = process.argv.slice(2)
189
+ const packageDir = path.resolve(here, '..', '..')
190
+ run({ packageDir, check: args.includes('--check'), quiet: args.includes('--quiet') }).then(({ ok }) => {
191
+ if (!ok) {
192
+ console.error('[hypercore-stallfix] FAILED: see the messages above. The Hypercore replication stall fix is not in place.')
193
+ process.exit(1)
194
+ }
195
+ }, (err) => {
196
+ console.error('[hypercore-stallfix] FAILED:', err && err.stack ? err.stack : err)
197
+ process.exit(1)
198
+ })
199
+ }