From 53428a605fae31ad821e036bf6d7d9b666251ba6 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Sun, 27 Sep 2026 21:18:37 +0200 Subject: [PATCH] bns: one network table instead of chipnet constants copied into fifteen places MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every client hard-coded the chipnet beacons, address prefix and electrum servers on its own: resolver, registrar config, wallets, gateway, indexer, mirror scripts, the browser bundle and the mobile Java. A mainnet launch would have meant finding all of them and hoping none was missed. The table now lives in resolver-web.js, the one file every client already shares, so it stays a single-file drop-in. BNS_NETWORK selects the record; unset means chipnet, so nothing changes today: the live index resolves the same 60 names and 20 TLDs, the 67 offline tests pass, and the dashboard, market and studio load the same values through BNS.NETWORK. The mainnet record carries the verified public servers, the prefix and its own Sia bucket, but its beacons, start height and operator address are deliberately null: requireBeacons() refuses to scan until they are pinned in the order ROADMAP-MAINNET.md §6 requires. Bns.java reads a generated BnsNetwork.java so the phone cannot drift from the desktop clients. NETWORK-CONFIG.md records what reads the table and what a launch still pins. --- src/config.js | 11 +- src/gateway/public-gateway.mjs | 43 +++--- src/generate-network-constants.mjs | 61 +++++++++ src/indexer/bns-indexer.js | 20 ++- src/indexer/electrum-source.js | 8 +- src/lib/chain.js | 9 +- src/lib/connect/wallet-session.js | 3 +- src/lib/electrum.js | 27 ++-- src/lib/gen-wallets.js | 12 +- src/lib/market-usd.js | 5 +- src/lib/orders.js | 9 +- src/lib/portfolio.js | 3 +- src/lib/register-tx.js | 11 +- src/lib/registrar.js | 6 +- src/lib/resolver-web.d.ts | 44 +++++- src/lib/resolver-web.js | 212 +++++++++++++++++++++-------- src/lib/sign-message.js | 5 +- src/lib/wallet-web.js | 11 +- src/lib/wallet.js | 10 +- src/publish-name-mirror.mjs | 12 +- src/publish-tld-mirror.mjs | 5 +- src/registrar-config.js | 29 ++-- src/snapshot-name-to-file.mjs | 3 +- src/web/register-entry.js | 4 +- 24 files changed, 382 insertions(+), 181 deletions(-) create mode 100644 src/generate-network-constants.mjs diff --git a/src/config.js b/src/config.js index 13de3e7..6346fba 100644 --- a/src/config.js +++ b/src/config.js @@ -1,9 +1,8 @@ -export const BEACON = "bchtest:qp8njaa9a3ahg7259s5qajnce526n0rttsq8qwx4jl"; -// TLD-certificate beacon (Decentralized.DNS/DESIGN-tld-registry.md §3). -// Distinct from the name BEACON so a client that only wants the TLD list can -// walk one address instead of every name-registration transaction. Chipnet. -// Scripthash (electrum encoding): c85db10e106606273422b2733b2a6fb8b35a4d0e7dbfb20cf59bdb0833c008b8 -export const TLD_BEACON = "bchtest:qzc4c76vtw9nd49g6x8nmvc0jz62uvk4dc7hmpmerh"; +import { NETWORK } from "./lib/resolver-web.js"; +// Beacons of the active network (BNS_NETWORK; chipnet unless set). The table +// itself lives in lib/resolver-web.js — see the note there. +export const BEACON = NETWORK.beacon; +export const TLD_BEACON = NETWORK.tldBeacon; export const PREFIX = "BNS1"; export const TLD = ".bch"; export const BEACON_DUST_SAT = 600; diff --git a/src/gateway/public-gateway.mjs b/src/gateway/public-gateway.mjs index cbc29a1..a5e1bfd 100644 --- a/src/gateway/public-gateway.mjs +++ b/src/gateway/public-gateway.mjs @@ -28,7 +28,16 @@ import WebSocket from "ws"; // resolver-web.js lives at ../lib/, not alongside this file — the previous // `./resolver-web.js` import was broken (file was moved into lib/ but this // import wasn't updated). Fix while touching the file. -import { resolveName, buildIndex, normalizeName, connectAny, CHIPNET_ELECTRUM, TLD_BEACON_SCRIPTHASH } from "./resolver-web.js"; +import { resolveName, buildIndex, normalizeName, connectAny, NETWORK, requireBeacons } from "./resolver-web.js"; +// Everything chain-specific comes from the network table (BNS_NETWORK env; +// chipnet unless set). Refuse to start on a network whose beacons are unpinned. +requireBeacons(NETWORK); +const TLD_BEACON_SCRIPTHASH = NETWORK.tldBeaconScripthash; +// Public electrum servers that answer wallet queries (listunspent). Our own +// beacon-only indexers are filtered out by the table's beaconOnly pattern. +const FULL_ELECTRUM = NETWORK.electrum.filter((s) => !NETWORK.beaconOnly.test(s.url)); +// Sia bucket holding operator-provided hosting: //… +const HOSTING_BUCKET = NETWORK.hostingBucket; import { pickRecord, isSubdomainOf } from "./record-picker.js"; import { buildTldSnapshot } from "./tld-index.js"; import { verifyListing } from "./market-tx.js"; @@ -43,10 +52,7 @@ import { readFileSync as readFileSyncOracle } from "node:fs"; let _tldSnap = { at: 0, snap: null }; async function getTldSnapshot() { if (_tldSnap.snap && Date.now() - _tldSnap.at < 30000) return _tldSnap.snap; - const BNS_INDEXER_URLS = /silentmode\.st\/electrum|coinspectrum\.duckdns\.org:50011/; - const candidates = CHIPNET_ELECTRUM - .map((s) => (typeof s === "string" ? { url: s } : s)) - .filter((s) => !BNS_INDEXER_URLS.test(s.url)); + const candidates = FULL_ELECTRUM; let el; for (const s of candidates) { try { el = await connectAny([s], WebSocket); break; } catch { /* try next */ } @@ -131,7 +137,7 @@ function recoverSigner(sigB64, msg, likeAddress) { const digest = sha256.hash(utf8ToBin(msg)); const pub = secp256k1.recoverPublicKeyCompressed(sigBytes.slice(1), (sigBytes[0] - 27) & 3, digest); if (typeof pub === "string") return null; - const prefix = String(likeAddress || "bchtest:").split(":")[0] || "bchtest"; + const prefix = String(likeAddress || NETWORK.prefix + ":").split(":")[0] || NETWORK.prefix; const enc = encodeCashAddress({ prefix, type: "p2pkh", payload: ripemd160.hash(sha256.hash(pub)) }); return typeof enc === "string" ? enc : (enc?.address ?? null); } catch { return null; } @@ -139,8 +145,7 @@ function recoverSigner(sigB64, msg, likeAddress) { // One full-electrum call (beacon-only indexers don't serve listunspent). async function withFullElectrum(fn) { - const BNS_INDEXER_URLS = /silentmode\.st\/electrum|coinspectrum\.duckdns\.org:50011/; - const candidates = CHIPNET_ELECTRUM.map((s) => (typeof s === "string" ? { url: s } : s)).filter((s) => !BNS_INDEXER_URLS.test(s.url)); + const candidates = FULL_ELECTRUM; let el = null, lastErr = null; for (const s of candidates) { try { el = await connectAny([s], WebSocket); break; } catch (e) { lastErr = e; } } if (!el) throw new Error("no full electrum reachable: " + String(lastErr?.message || lastErr)); @@ -987,12 +992,8 @@ const server = http.createServer(async (req, res) => { if (scripthashes.length > 64) { res.writeHead(400); res.end(JSON.stringify({ error: "at most 64 scripthashes per request" })); return; } - // Skip beacon-only indexers (silentmode.st/electrum on 443, our own - // bns-indexer on :50011) — they don't implement listunspent. - const BNS_INDEXER_URLS = /silentmode\.st\/electrum|coinspectrum\.duckdns\.org:50011/; - const candidates = CHIPNET_ELECTRUM - .map((s) => (typeof s === "string" ? { url: s } : s)) - .filter((s) => !BNS_INDEXER_URLS.test(s.url)); + // Skip beacon-only indexers — they don't implement listunspent. + const candidates = FULL_ELECTRUM; let el, lastErr; for (const s of candidates) { try { el = await connectAny([s], WebSocket); break; } @@ -1218,7 +1219,7 @@ const server = http.createServer(async (req, res) => { // prefix as the on-chain owner address (chipnet = "bchtest", // mainnet = "bitcoincash"). The prefix comes from the owner // string itself so we don't have to hardcode chain. - const ownerPrefix = String(entry.owner).split(":")[0] || "bchtest"; + const ownerPrefix = String(entry.owner).split(":")[0] || NETWORK.prefix; const pubkeyHash = ripemd160.hash(sha256.hash(recovered)); const _enc = encodeCashAddress({ prefix: ownerPrefix, type: "p2pkh", payload: pubkeyHash }); // libauth returns { address } in newer versions and a bare string in @@ -1487,7 +1488,7 @@ const server = http.createServer(async (req, res) => { catch (e) { res.setHeader("content-type", "application/json"); res.writeHead(400); res.end(JSON.stringify({ error: e.message })); return; } const entry = (await getIndex()).get(name); if (!entry) { res.setHeader("content-type", "application/json"); res.writeHead(404); res.end(JSON.stringify({ error: `"${name}" not registered` })); return; } - const prefix = `bns/${name}/`; + const prefix = `${HOSTING_BUCKET}/${name}/`; // GET ...?src=live — a READ-ONLY view of wherever the name's on-chain // records say the site lives right now: a CLI-published bucket such as // bns/