// Keep a public, read-only RAW name-beacon snapshot on the CDN. // // Theseus warm-starts its name index from a raw snapshot — the beacon's // history plus the verbose transactions — and rebuilds the index from that // evidence itself (buildIndexFromSnapshot), so the operator is not trusted // for the interpretation. The only published copy lived on the Sia S3 // endpoint, which needs SigV4 credentials for every read: every Theseus // launch got a 403 and the copy was never refreshed anyway (it was written // by publish-name-mirror.mjs, by hand, last on 2026-08-31). // // This writes the same snapshot format to a plain static file served by the // dl vhost — public, view-only, no credentials, no wallet. Each run fetches // the beacon history over electrum and only the transactions the previous // file doesn't already hold, so a run is one history call plus whatever is // new. Runs on the VPS from a systemd timer (VPS/SilentMode/cdn-name-snapshot.*), // beside cdn-name-index.mjs, which publishes the interpreted index. // // Two public, read-only copies, on two storage backends: // 1. --out the dl vhost's disk (https://dl.silentmode.st/bns-name-snapshot.json) // 2. --sia the Sia bucket, served by the gateway // (https://navigate.st/bns/silentmode.bch/bns-name-snapshot.json) // The Sia copy is uploaded whenever its last uploaded root (kept beside --out // as .sia-root) differs from the current one, so a failed upload heals // on the next run. Credentials come from the environment (AK / SK, as in // /etc/s3d/sync.env) and an S3 endpoint (--s3-endpoint). // // node cdn-name-snapshot.mjs [--out /opt/silent-mode/dl/bns-name-snapshot.json] // [--sia s3://bns/silentmode/bns-name-snapshot.json --s3-endpoint http://127.0.0.1:8500] import { readFileSync, writeFileSync, renameSync, existsSync } from "node:fs"; import { spawnSync } from "node:child_process"; import WebSocket from "ws"; // The gateway runtime on the VPS is flat (/opt/bns-gateway/*.js); the repo // keeps the libraries under lib/. async function load(file) { for (const p of [`./${file}`, `./lib/${file}`]) { try { return await import(new URL(p, import.meta.url)); } catch (e) { if (e?.code !== "ERR_MODULE_NOT_FOUND") throw e; } } throw new Error(`${file} not found next to this script or in lib/`); } const R = await load("resolver-web.js"); const { buildNameSnapshot } = await load("snapshot-name.js"); const args = process.argv.slice(2); const opt = (k, d) => { const i = args.indexOf(k); return i >= 0 && args[i + 1] ? args[i + 1] : d; }; const OUT = opt("--out", "/opt/silent-mode/dl/bns-name-snapshot.json"); const SIA = opt("--sia", ""); const S3_ENDPOINT = opt("--s3-endpoint", "http://127.0.0.1:8500"); const MIN_NAMES = 10; // fewer is a broken answer, not the registry // Second copy: push the file at OUT to Sia unless that root is already there. function mirrorToSia(root) { if (!SIA) return; const marker = OUT + ".sia-root"; let done = ""; try { done = readFileSync(marker, "utf8").trim(); } catch {} if (done === root) return; const env = { ...process.env }; if (env.AK && !env.AWS_ACCESS_KEY_ID) env.AWS_ACCESS_KEY_ID = env.AK; if (env.SK && !env.AWS_SECRET_ACCESS_KEY) env.AWS_SECRET_ACCESS_KEY = env.SK; const r = spawnSync("aws", ["--endpoint-url", S3_ENDPOINT, "s3", "cp", OUT, SIA, "--content-type", "application/json", "--only-show-errors"], { env, encoding: "utf8" }); if (r.status !== 0) { console.error(`sia mirror failed (${r.status}): ${(r.stderr || r.error?.message || "").trim()}`); process.exitCode = 1; return; } writeFileSync(marker, root); console.log(`mirrored to ${SIA} (root ${root.slice(0, 12)})`); } let prev = null; try { if (existsSync(OUT)) prev = JSON.parse(readFileSync(OUT, "utf8")); } catch { prev = null; } const prevTxs = (prev && prev.beacon === R.BEACON_SCRIPTHASH && prev.txs) || {}; const el = await R.connectElectrum({ WebSocket, directIP: true }); let history, txs = {}, fetched = 0; try { history = await el.call("blockchain.scripthash.get_history", [R.BEACON_SCRIPTHASH]); if (!Array.isArray(history)) throw new Error("electrum returned no history"); for (const h of history) { if (prevTxs[h.tx_hash]) { txs[h.tx_hash] = prevTxs[h.tx_hash]; continue; } txs[h.tx_hash] = await el.call("blockchain.transaction.get", [h.tx_hash, true]); fetched++; } } finally { try { el.close(); } catch {} } // A server answering with a shorter history than we already published is // lagging (or lying); keep the published copy rather than go backwards. if (prev && Array.isArray(prev.history) && history.length < prev.history.length) { console.log(`electrum history shorter than published (${history.length} < ${prev.history.length}) — not writing`); process.exit(0); } const heights = history.map((h) => Number(h.height) || 0).filter((h) => h > 0); const snapshot = buildNameSnapshot({ beacon: R.BEACON_SCRIPTHASH, history: history.map((h) => ({ tx_hash: h.tx_hash, height: h.height })), txs, asOfHeight: heights.length ? Math.max(...heights) : null, }); const names = R.buildIndexFromSnapshot({ snapshot }).size; if (names < MIN_NAMES) throw new Error(`snapshot resolves to only ${names} names — not writing`); if (prev && prev.root === snapshot.root) { console.log(`unchanged (${names} names, root ${snapshot.root.slice(0, 12)})`); } else { writeFileSync(OUT + ".tmp", JSON.stringify(snapshot)); renameSync(OUT + ".tmp", OUT); console.log(`wrote ${OUT}: ${history.length} beacon txs (${fetched} fetched), ${names} names, height ${snapshot.asOfHeight ?? "?"}, root ${snapshot.root.slice(0, 12)}`); } mirrorToSia(snapshot.root);