trac-msb 0.2.26 → 0.2.28

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/README.md CHANGED
@@ -37,6 +37,40 @@ cd main_settlement_bus
37
37
  npm install
38
38
  ```
39
39
 
40
+ ## HyperDHT LAN compatibility (0.2.28)
41
+
42
+ HyperDHT 6.29.x calls `bogon.isReserved` when connecting peers through a relay on
43
+ one host or LAN. Bogon 1.1.0 lacks that export; 1.2.0 first supplies it. This
44
+ release requires `bogon ^1.2.0` and checks the Bogon actually resolved by every
45
+ reachable HyperDHT copy during `postinstall`. This applies to regular peers as
46
+ well as validators.
47
+
48
+ A package lockfile or `overrides` in a dependency does not protect an application's
49
+ installation. Applications should update their own lockfiles and run
50
+ `node node_modules/trac-msb/scripts/check-hyperdht-bogon.cjs` after installation.
51
+ If lifecycle scripts are disabled, also run
52
+ `node node_modules/trac-msb/scripts/hypercore-stallfix/apply.mjs` and then repeat
53
+ it with `--check`. Both checks must succeed before starting the application.
54
+ The check reports an incompatible nested Bogon instead of modifying dependencies.
55
+
56
+ From this repository, run `npm run check:dependencies` and
57
+ `npm run test:hyperdht-bogon`. The regression uses only a local bootstrap and
58
+ throwaway keys, binds to loopback ports 59100–59199, and does not join a Trac
59
+ network. Start the application with `npm start` (there is no `prod` script).
60
+
61
+ ## Upgrading from releases started with `npm run prod`
62
+
63
+ Early 0.2.x releases started with `npm run prod --store=<name>` (through `pear run`). Current releases have no
64
+ `prod` script; start a mainnet node with:
65
+
66
+ ```sh
67
+ npm run start -- --stores-directory stores/<name> --network mainnet
68
+ ```
69
+
70
+ The node reads its key from `stores/<name>/db/keypair.json`. A store created by an older release may live in a
71
+ different location, in which case the key file is not found. Copy that store's `db/keypair.json` to the new path
72
+ before the first start, or restore the wallet from its mnemonic.
73
+
40
74
  ## Post-install checklist
41
75
 
42
76
  Before running tests, install bare globally:
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "trac-msb",
3
3
  "main": "msb.mjs",
4
- "version": "0.2.26",
4
+ "version": "0.2.28",
5
5
  "pear": {
6
6
  "name": "trac-msb",
7
7
  "type": "terminal"
@@ -13,10 +13,13 @@
13
13
  "NOTICE",
14
14
  "rpc/",
15
15
  "SECURITY.md",
16
+ "scripts/check-hyperdht-bogon.cjs",
17
+ "scripts/hypercore-stallfix/",
16
18
  "scripts/pear-runner.js",
17
19
  "src/"
18
20
  ],
19
21
  "scripts": {
22
+ "postinstall": "node scripts/check-hyperdht-bogon.cjs && node scripts/hypercore-stallfix/apply.mjs",
20
23
  "start": "NODE_OPTIONS='--max-old-space-size=4096' node scripts/pear-runner.js",
21
24
  "rpc": "NODE_OPTIONS='--max-old-space-size=4096' node scripts/pear-runner.js --rpc --host ${npm_config_host} --port ${npm_config_port}",
22
25
  "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,12 +31,15 @@
28
31
  "docs:check": "node scripts/docs/docs-checker.js",
29
32
  "test:acceptance": "node --experimental-vm-modules node_modules/jest/bin/jest.js --testTimeout=200000 tests/acceptance/v1/rpc.test.mjs --runInBand",
30
33
  "test:integration": "brittle-node -t 1200000 tests/integration/integration.test.js",
34
+ "test:unit:prepare": "node scripts/hypercore-stallfix/apply.mjs && node scripts/hypercore-stallfix/apply.mjs --check",
31
35
  "test:unit:node": "brittle-node -t 60000 tests/unit/unit.test.js",
32
36
  "test:unit:bare": "brittle-bare -t 60000 tests/unit/unit.test.js",
33
37
  "test:unit:pear": "BRITTLE='-t 60000' brittle-pear tests/unit/unit.test.js",
34
38
  "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",
36
- "test:unit:node:cov": "brittle-node -c -t 60000 tests/unit/unit.test.js"
39
+ "test:unit:all": "(npm run test:unit:prepare && npm run test:hyperdht-bogon && npm run test:unit:node && npm run test:unit:bare && npm run test:pear-runner) || exit 1",
40
+ "test:unit:node:cov": "brittle-node -c -t 60000 tests/unit/unit.test.js",
41
+ "check:dependencies": "node scripts/check-hyperdht-bogon.cjs",
42
+ "test:hyperdht-bogon": "node --test tests/hyperdht-bogon-check.test.cjs && node tests/hyperdht-lan-repro.cjs"
37
43
  },
38
44
  "dependencies": {
39
45
  "autobase": "7.20.1",
@@ -45,6 +51,7 @@
45
51
  "bare-tty": "5.0.2",
46
52
  "bare-utils": "1.5.1",
47
53
  "bech32": "2.0.0",
54
+ "bogon": "^1.2.0",
48
55
  "compact-encoding": "2.18.0",
49
56
  "corestore": "7.5.0",
50
57
  "crypto": "npm:bare-node-crypto",
@@ -55,8 +62,8 @@
55
62
  "hypercore": "11.18.3",
56
63
  "hypercore-crypto": "3.6.1",
57
64
  "hyperdht": "6.29.6",
58
- "hyperswarm": "4.14.2",
59
65
  "hyperschema": "1.17.1",
66
+ "hyperswarm": "4.14.2",
60
67
  "lodash": "^4.18.1",
61
68
  "p-queue": "^9.1.0",
62
69
  "pear-runtime": "1.3.1",
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,50 @@
1
+ #!/usr/bin/env node
2
+ // Check the actual Bogon seen by each reachable HyperDHT, including nested copies.
3
+ // A dependency's overrides and package-lock.json do not constrain its consumers.
4
+ const fs = require('node:fs')
5
+ const path = require('node:path')
6
+ const { createRequire } = require('node:module')
7
+
8
+ function check(root = path.resolve(__dirname, '..')) {
9
+ const pending = [path.join(root, 'package.json')]
10
+ const seen = new Set()
11
+ let checked = 0
12
+ while (pending.length) {
13
+ const manifest = pending.pop()
14
+ if (seen.has(manifest)) continue
15
+ seen.add(manifest)
16
+ const pkg = JSON.parse(fs.readFileSync(manifest, 'utf8'))
17
+ const requireFrom = createRequire(manifest)
18
+ if (pkg.name === 'hyperdht') {
19
+ checked++
20
+ const [major, minor] = pkg.version.split('.').map(Number)
21
+ if (major < 6 || (major === 6 && minor < 29)) continue
22
+ const bogon = requireFrom('bogon')
23
+ if (typeof bogon.isReserved !== 'function' || typeof bogon.isBogon !== 'function') {
24
+ throw new Error(`HyperDHT ${pkg.version} resolves incompatible Bogon at ${requireFrom.resolve('bogon')}. Update the consumer lockfile to bogon >=1.2.0 and reinstall; same-host or LAN peer connections can otherwise crash.`)
25
+ }
26
+ }
27
+ for (const name of Object.keys({ ...pkg.dependencies, ...pkg.optionalDependencies })) {
28
+ // Resolve physical manifests even when package exports hide package.json.
29
+ const paths = requireFrom.resolve.paths(`${name}/package.json`) || []
30
+ const resolved = paths.map(dir => path.join(dir, name, 'package.json')).find(file => fs.existsSync(file))
31
+ if (!resolved) {
32
+ if (pkg.optionalDependencies?.[name]) continue
33
+ throw new Error(`Missing dependency ${name} required by ${pkg.name}`)
34
+ }
35
+ pending.push(fs.realpathSync(resolved))
36
+ }
37
+ }
38
+ if (checked === 0) throw new Error('No HyperDHT found; dependency installation is incomplete.')
39
+ return checked
40
+ }
41
+
42
+ module.exports = { check }
43
+ if (require.main === module) {
44
+ try {
45
+ console.log(`HyperDHT/Bogon compatibility: ${check()} copies checked`)
46
+ } catch (error) {
47
+ console.error(error.message)
48
+ process.exitCode = 1
49
+ }
50
+ }
@@ -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
+ }