argus/src/lib/bns.js

373 lines
15 KiB
JavaScript
Raw Normal View History

// BNS1 core: payload codec, chain indexer/resolver, and tx builders.
import { OpReturnData, TokenSendRequest, NFTCapability } from "mainnet-js";
BNS: per-TLD on-chain registry, multi-beacon indexer, enforcement gate Per-TLD certificates on a distinct TLD beacon supersede the legacy list-in-one-NFT model. TLD label is the NFT commitment; TREG/TUPD payloads on the TLD beacon mirror REG/UPD on the name beacon. Chipnet TLD beacon: bchtest:qzc4c76vtw9nd49g6x8nmvc0jz62uvk4dc7hmpmerh scripthash c85db10e106606273422b2733b2a6fb8b35a4d0e7dbfb20cf59bdb0833c008b8 * config.js, resolver-web.{js,d.ts} — TLD_BEACON + TLD_BEACON_SCRIPTHASH * bns.js — KNOWN_OPS { REG, UPD, TREG, TUPD }, registerTld/updateTld/normalizeTld * bns.js + resolver-web.js — buildIndex({enforceTld:false}) drops name REGs whose TLD lacks a live TLD cert at their block height (mempool-friendly). Lockstep across both files. * lib/tld-index.js — buildTldSnapshot({history,txs}) → deterministic root over canonicalised tlds[]. Pure, importable, and stable across insertion order. * indexer/bns-indexer.js — BeaconIndex is multi-beacon; ws handler dispatches by scripthash; new HTTP endpoint GET /tld-list.json + /health behind BNS_HTTP_PORT; state file v1 → v2 migrates in fromJSON. * publish-tld-mirror.mjs — S3 (floating + immutable content-addressed) plus NIP-33 kind 30078 d:bns-tld-list to Nostr relays, signed via nostr-tools (libauth's Schnorr differs from BIP-340 and relays reject it). * seed-tld-beacon.mjs — idempotent chipnet seed. Current set on the beacon: bch p2p bit nav test x asm neo gt sc sia com dex cex nt (15). `.com` is held by the operator: cert on-chain, excluded from public registrar UI. Design + docs: * Decentralized.DNS/DESIGN-tld-registry.md — schema, enforcement, operator-countersig gate, USD-tiered pricing floors, fee splits, tracker/mirror mechanics, migration/grandfathering. * INSTRUCTIONS.md §3 rewritten for the 4-step flow (TREG → UI → deploy → republish snapshot). * site/tlds/ — public docs page listing current TLDs + verification paths. * site/sirius/register.html — dropdown removed; parallel search across every TLD in the TLDS array, sorted available → taken → not-on-registry. Config: * ariadne.config.json tlds → [bch, p2p, bit, nav, test, x] (CA constraints; on-chain registry is now the authoritative source, config is fallback). * setup/install.ps1 — reads TLD list via fetchTldList (per-TLD registry) with fallback to fetchBcnrTlds (legacy tls.bch list-NFT) then config. Tests: * test/tld-index.test.mjs — 30 self-contained checks (multi-beacon dispatch, state migration, ws routing, snapshot shape + root recomputability). * test/tld-enforcement.test.mjs — synthetic + live chipnet integration (with-gate correctly drops all pre-seed names).
2026-08-29 17:11:02 +02:00
import { BEACON, TLD_BEACON, PREFIX, BEACON_DUST_SAT, NAME_RE } from "../config.js";
// ---------- payload codec ----------
const LABEL_RE = /^[a-z0-9-]{1,32}$/;
const TLD_RE = /^[a-z0-9-]{1,16}$/;
const RAWNAME_RE = /^[a-z0-9-]{1,32}(\.[a-z0-9-]{1,16})?$/;
// On-chain name for a registration: bare "<label>" for .bch (backward compat),
// or "<label>.<tld>" for any other TLD. The rightmost label is the registry.
export function normalizeName(input) {
const parts = String(input).toLowerCase().trim().replace(/\.$/, "").split(".").filter(Boolean);
let label, tld;
if (parts.length === 1) { label = parts[0]; tld = "bch"; }
else { tld = parts[parts.length - 1]; label = parts[parts.length - 2]; }
if (!LABEL_RE.test(label) || !TLD_RE.test(tld)) {
throw new Error(`invalid name "${input}" (allowed: <label>.<tld>, [a-z0-9-])`);
}
return tld === "bch" ? label : `${label}.${tld}`;
}
export function encodePayload(op, name, records) {
const json = JSON.stringify({ o: op, n: name, r: records });
const buf = Buffer.from(PREFIX + json, "utf8");
if (buf.length > 200) throw new Error(`records too large (${buf.length} > 200 bytes)`);
return buf;
}
// Parse a raw output script hex; return payload object or null.
export function parseOpReturn(scriptHex) {
if (!scriptHex?.startsWith("6a")) return null; // OP_RETURN
let i = 2;
const pushes = [];
while (i < scriptHex.length) {
let op = parseInt(scriptHex.slice(i, i + 2), 16);
i += 2;
let len;
if (op <= 0x4b) len = op;
else if (op === 0x4c) { len = parseInt(scriptHex.slice(i, i + 2), 16); i += 2; }
else if (op === 0x4d) { len = parseInt(scriptHex.slice(i + 2, i + 4) + scriptHex.slice(i, i + 2), 16); i += 4; }
else return null;
pushes.push(Buffer.from(scriptHex.slice(i, i + len * 2), "hex"));
i += len * 2;
}
const data = pushes[0]?.toString("utf8") ?? "";
if (!data.startsWith(PREFIX)) return null;
try {
const p = JSON.parse(data.slice(PREFIX.length));
BNS: per-TLD on-chain registry, multi-beacon indexer, enforcement gate Per-TLD certificates on a distinct TLD beacon supersede the legacy list-in-one-NFT model. TLD label is the NFT commitment; TREG/TUPD payloads on the TLD beacon mirror REG/UPD on the name beacon. Chipnet TLD beacon: bchtest:qzc4c76vtw9nd49g6x8nmvc0jz62uvk4dc7hmpmerh scripthash c85db10e106606273422b2733b2a6fb8b35a4d0e7dbfb20cf59bdb0833c008b8 * config.js, resolver-web.{js,d.ts} — TLD_BEACON + TLD_BEACON_SCRIPTHASH * bns.js — KNOWN_OPS { REG, UPD, TREG, TUPD }, registerTld/updateTld/normalizeTld * bns.js + resolver-web.js — buildIndex({enforceTld:false}) drops name REGs whose TLD lacks a live TLD cert at their block height (mempool-friendly). Lockstep across both files. * lib/tld-index.js — buildTldSnapshot({history,txs}) → deterministic root over canonicalised tlds[]. Pure, importable, and stable across insertion order. * indexer/bns-indexer.js — BeaconIndex is multi-beacon; ws handler dispatches by scripthash; new HTTP endpoint GET /tld-list.json + /health behind BNS_HTTP_PORT; state file v1 → v2 migrates in fromJSON. * publish-tld-mirror.mjs — S3 (floating + immutable content-addressed) plus NIP-33 kind 30078 d:bns-tld-list to Nostr relays, signed via nostr-tools (libauth's Schnorr differs from BIP-340 and relays reject it). * seed-tld-beacon.mjs — idempotent chipnet seed. Current set on the beacon: bch p2p bit nav test x asm neo gt sc sia com dex cex nt (15). `.com` is held by the operator: cert on-chain, excluded from public registrar UI. Design + docs: * Decentralized.DNS/DESIGN-tld-registry.md — schema, enforcement, operator-countersig gate, USD-tiered pricing floors, fee splits, tracker/mirror mechanics, migration/grandfathering. * INSTRUCTIONS.md §3 rewritten for the 4-step flow (TREG → UI → deploy → republish snapshot). * site/tlds/ — public docs page listing current TLDs + verification paths. * site/sirius/register.html — dropdown removed; parallel search across every TLD in the TLDS array, sorted available → taken → not-on-registry. Config: * ariadne.config.json tlds → [bch, p2p, bit, nav, test, x] (CA constraints; on-chain registry is now the authoritative source, config is fallback). * setup/install.ps1 — reads TLD list via fetchTldList (per-TLD registry) with fallback to fetchBcnrTlds (legacy tls.bch list-NFT) then config. Tests: * test/tld-index.test.mjs — 30 self-contained checks (multi-beacon dispatch, state migration, ws routing, snapshot shape + root recomputability). * test/tld-enforcement.test.mjs — synthetic + live chipnet integration (with-gate correctly drops all pre-seed names).
2026-08-29 17:11:02 +02:00
// Recognised opcodes:
// REG / UPD — name registrations & updates on the name beacon
// TREG / TUPD — TLD registrations & updates on the TLD beacon
// (Decentralized.DNS/DESIGN-tld-registry.md §3)
// KEEP IN LOCKSTEP with resolver-web.js parseOpReturnHex — memory rule.
if (KNOWN_OPS.has(p.o) && typeof p.n === "string") return p;
} catch { /* not ours */ }
return null;
}
BNS: per-TLD on-chain registry, multi-beacon indexer, enforcement gate Per-TLD certificates on a distinct TLD beacon supersede the legacy list-in-one-NFT model. TLD label is the NFT commitment; TREG/TUPD payloads on the TLD beacon mirror REG/UPD on the name beacon. Chipnet TLD beacon: bchtest:qzc4c76vtw9nd49g6x8nmvc0jz62uvk4dc7hmpmerh scripthash c85db10e106606273422b2733b2a6fb8b35a4d0e7dbfb20cf59bdb0833c008b8 * config.js, resolver-web.{js,d.ts} — TLD_BEACON + TLD_BEACON_SCRIPTHASH * bns.js — KNOWN_OPS { REG, UPD, TREG, TUPD }, registerTld/updateTld/normalizeTld * bns.js + resolver-web.js — buildIndex({enforceTld:false}) drops name REGs whose TLD lacks a live TLD cert at their block height (mempool-friendly). Lockstep across both files. * lib/tld-index.js — buildTldSnapshot({history,txs}) → deterministic root over canonicalised tlds[]. Pure, importable, and stable across insertion order. * indexer/bns-indexer.js — BeaconIndex is multi-beacon; ws handler dispatches by scripthash; new HTTP endpoint GET /tld-list.json + /health behind BNS_HTTP_PORT; state file v1 → v2 migrates in fromJSON. * publish-tld-mirror.mjs — S3 (floating + immutable content-addressed) plus NIP-33 kind 30078 d:bns-tld-list to Nostr relays, signed via nostr-tools (libauth's Schnorr differs from BIP-340 and relays reject it). * seed-tld-beacon.mjs — idempotent chipnet seed. Current set on the beacon: bch p2p bit nav test x asm neo gt sc sia com dex cex nt (15). `.com` is held by the operator: cert on-chain, excluded from public registrar UI. Design + docs: * Decentralized.DNS/DESIGN-tld-registry.md — schema, enforcement, operator-countersig gate, USD-tiered pricing floors, fee splits, tracker/mirror mechanics, migration/grandfathering. * INSTRUCTIONS.md §3 rewritten for the 4-step flow (TREG → UI → deploy → republish snapshot). * site/tlds/ — public docs page listing current TLDs + verification paths. * site/sirius/register.html — dropdown removed; parallel search across every TLD in the TLDS array, sorted available → taken → not-on-registry. Config: * ariadne.config.json tlds → [bch, p2p, bit, nav, test, x] (CA constraints; on-chain registry is now the authoritative source, config is fallback). * setup/install.ps1 — reads TLD list via fetchTldList (per-TLD registry) with fallback to fetchBcnrTlds (legacy tls.bch list-NFT) then config. Tests: * test/tld-index.test.mjs — 30 self-contained checks (multi-beacon dispatch, state migration, ws routing, snapshot shape + root recomputability). * test/tld-enforcement.test.mjs — synthetic + live chipnet integration (with-gate correctly drops all pre-seed names).
2026-08-29 17:11:02 +02:00
// The set of payload opcodes this codec recognises. New opcodes go here AND in
// resolver-web.js — divergence is a consensus split.
export const KNOWN_OPS = new Set(["REG", "UPD", "TREG", "TUPD"]);
// ---------- indexer / resolver ----------
// (height, txid) order; unconfirmed (height<=0) last.
function byHeightThenTxid(a, b) {
const ha = a.height > 0 ? a.height : Infinity;
const hb = b.height > 0 ? b.height : Infinity;
return ha - hb || a.tx_hash.localeCompare(b.tx_hash);
}
// Never index a transaction before one it spends from — otherwise an UPD that
// happens to sort below its own REG is evaluated against an index that does not
// yet contain the name, and is dropped. Keeps the txid tiebreak for independent
// transactions. Mirrors resolver-web.js `orderByDependency` — change both.
function orderByDependency(history, txs) {
const inSet = new Set(history.map((h) => h.tx_hash));
const deps = new Map(
history.map((h) => {
const spent = new Set();
for (const vin of txs.get(h.tx_hash)?.vin ?? []) {
if (vin.txid && vin.txid !== h.tx_hash && inSet.has(vin.txid)) spent.add(vin.txid);
}
return [h.tx_hash, spent];
}),
);
const emitted = new Set();
const ordered = [];
const pending = [...history];
while (pending.length) {
let i = pending.findIndex((h) => [...deps.get(h.tx_hash)].every((d) => emitted.has(d)));
if (i < 0) i = 0; // unreachable on a real chain; guarantees termination
const [next] = pending.splice(i, 1);
emitted.add(next.tx_hash);
ordered.push(next);
}
return ordered;
}
BNS: per-TLD on-chain registry, multi-beacon indexer, enforcement gate Per-TLD certificates on a distinct TLD beacon supersede the legacy list-in-one-NFT model. TLD label is the NFT commitment; TREG/TUPD payloads on the TLD beacon mirror REG/UPD on the name beacon. Chipnet TLD beacon: bchtest:qzc4c76vtw9nd49g6x8nmvc0jz62uvk4dc7hmpmerh scripthash c85db10e106606273422b2733b2a6fb8b35a4d0e7dbfb20cf59bdb0833c008b8 * config.js, resolver-web.{js,d.ts} — TLD_BEACON + TLD_BEACON_SCRIPTHASH * bns.js — KNOWN_OPS { REG, UPD, TREG, TUPD }, registerTld/updateTld/normalizeTld * bns.js + resolver-web.js — buildIndex({enforceTld:false}) drops name REGs whose TLD lacks a live TLD cert at their block height (mempool-friendly). Lockstep across both files. * lib/tld-index.js — buildTldSnapshot({history,txs}) → deterministic root over canonicalised tlds[]. Pure, importable, and stable across insertion order. * indexer/bns-indexer.js — BeaconIndex is multi-beacon; ws handler dispatches by scripthash; new HTTP endpoint GET /tld-list.json + /health behind BNS_HTTP_PORT; state file v1 → v2 migrates in fromJSON. * publish-tld-mirror.mjs — S3 (floating + immutable content-addressed) plus NIP-33 kind 30078 d:bns-tld-list to Nostr relays, signed via nostr-tools (libauth's Schnorr differs from BIP-340 and relays reject it). * seed-tld-beacon.mjs — idempotent chipnet seed. Current set on the beacon: bch p2p bit nav test x asm neo gt sc sia com dex cex nt (15). `.com` is held by the operator: cert on-chain, excluded from public registrar UI. Design + docs: * Decentralized.DNS/DESIGN-tld-registry.md — schema, enforcement, operator-countersig gate, USD-tiered pricing floors, fee splits, tracker/mirror mechanics, migration/grandfathering. * INSTRUCTIONS.md §3 rewritten for the 4-step flow (TREG → UI → deploy → republish snapshot). * site/tlds/ — public docs page listing current TLDs + verification paths. * site/sirius/register.html — dropdown removed; parallel search across every TLD in the TLDS array, sorted available → taken → not-on-registry. Config: * ariadne.config.json tlds → [bch, p2p, bit, nav, test, x] (CA constraints; on-chain registry is now the authoritative source, config is fallback). * setup/install.ps1 — reads TLD list via fetchTldList (per-TLD registry) with fallback to fetchBcnrTlds (legacy tls.bch list-NFT) then config. Tests: * test/tld-index.test.mjs — 30 self-contained checks (multi-beacon dispatch, state migration, ws routing, snapshot shape + root recomputability). * test/tld-enforcement.test.mjs — synthetic + live chipnet integration (with-gate correctly drops all pre-seed names).
2026-08-29 17:11:02 +02:00
// Extract the TLD of a raw on-chain name: "hello" -> "bch" (legacy .bch),
// "foo.x" -> "x". Same rule as normalizeName's TLD inference.
function tldOf(rawName) {
const i = rawName.lastIndexOf(".");
return i < 0 ? "bch" : rawName.slice(i + 1);
}
// Walk the TLD-beacon history to a Map<tld, {category, regHeight, regTxid}>.
// Same first-TREG-wins / commitment-must-match rules as buildIndex uses for
// names — kept inline (rather than importing tld-index.js) to avoid the
// circular import bns.js -> tld-index.js -> bns.js. Any change here must
// match the equivalent block in resolver-web.js (memory rule).
async function buildTldMap(provider) {
const history = await provider.getHistory(TLD_BEACON);
history.sort(byHeightThenTxid);
const txs = new Map();
for (const { tx_hash } of history) {
try { txs.set(tx_hash, await provider.getRawTransactionObject(tx_hash)); }
catch { /* skip */ }
}
const tlds = new Map();
for (const { tx_hash, height } of orderByDependency(history.filter((h) => txs.has(h.tx_hash)), txs)) {
const tx = txs.get(tx_hash);
let payload = null;
for (const vout of tx.vout) payload = parseOpReturn(vout.scriptPubKey?.hex) ?? payload;
if (!payload) continue;
if (typeof payload.n !== "string" || !/^[a-z0-9-]{1,16}$/.test(payload.n)) continue;
const tld = payload.n;
if (payload.o === "TREG") {
if (tlds.has(tld)) continue; // first valid TREG wins
const commitment = Buffer.from(tld, "utf8").toString("hex");
const mint = tx.vout.find((v) => v.tokenData?.nft?.commitment === commitment);
if (!mint) continue;
feat(bns): name marketplace and TLD-owner co-sign rule Two gaps the owner panel left open. First, a hidden TLD was only a UI gate: anyone could still broadcast a REG under it and every indexer admitted it. Second, there was no way to sell a name without trusting the other side. Co-sign rule (consensus, applied in lockstep by bns.js and resolver-web.js): a REG under a TLD whose records at that height say policy "cosign" or hidden 1 is indexed only if the transaction carries the TLD's own certificate. The certificate can only be spent by the owner's key and is re-issued to them in the same transaction, so it is a co-signature nobody can forge and nothing is consumed. The TLD map now keeps the TUPD timeline so policy is evaluated at the REG height. Owners register under their private TLDs with the certificate added from their own wallet; third parties under a "cosign" TLD build the full transaction, sign their inputs and queue it at /api/cosign, where the owner approves it from the dashboard (signCosignRequest refuses to sign unless the certificate returns to the same locking script). Marketplace: a listing is the seller's certificate input plus a price output signed SIGHASH_SINGLE|ANYONECANPAY, stored by the gateway as a bulletin board (/api/market, verified against the on-chain owner and pruned when the certificate moves). The buyer completes it in one transaction, so the seller is paid exactly when the name moves. Cancelling also spends the certificate once so the offer is void. Site: market.html, Sell sub-tab and Pending approvals in the portal, Market link in nav and footer, six dictionaries extended, cache tags bumped. Verified on chipnet: cosigned.sc registered by a throwaway wallet through the queue with the .sc certificate back at the owner; aloevera.test listed and delisted through the API. Ariadne's resolver-web.js copy and the mobile Bns.java port still need the co-sign rule; until then they admit REGs this index rejects.
2026-09-17 04:04:22 +02:00
tlds.set(tld, {
category: mint.tokenData.category, regHeight: height, regTxid: tx_hash,
records: payload.r ?? {},
updates: [{ height, txid: tx_hash, records: payload.r ?? {} }],
});
} else if (payload.o === "TUPD") {
// Authoritative only if the tx re-issues the TLD NFT (same as tld-index.js).
const t = tlds.get(tld);
if (!t) continue;
if (!tx.vout.some((v) => v.tokenData && v.tokenData.category === t.category)) continue;
t.records = payload.r ?? {};
t.updates.push({ height, txid: tx_hash, records: t.records });
BNS: per-TLD on-chain registry, multi-beacon indexer, enforcement gate Per-TLD certificates on a distinct TLD beacon supersede the legacy list-in-one-NFT model. TLD label is the NFT commitment; TREG/TUPD payloads on the TLD beacon mirror REG/UPD on the name beacon. Chipnet TLD beacon: bchtest:qzc4c76vtw9nd49g6x8nmvc0jz62uvk4dc7hmpmerh scripthash c85db10e106606273422b2733b2a6fb8b35a4d0e7dbfb20cf59bdb0833c008b8 * config.js, resolver-web.{js,d.ts} — TLD_BEACON + TLD_BEACON_SCRIPTHASH * bns.js — KNOWN_OPS { REG, UPD, TREG, TUPD }, registerTld/updateTld/normalizeTld * bns.js + resolver-web.js — buildIndex({enforceTld:false}) drops name REGs whose TLD lacks a live TLD cert at their block height (mempool-friendly). Lockstep across both files. * lib/tld-index.js — buildTldSnapshot({history,txs}) → deterministic root over canonicalised tlds[]. Pure, importable, and stable across insertion order. * indexer/bns-indexer.js — BeaconIndex is multi-beacon; ws handler dispatches by scripthash; new HTTP endpoint GET /tld-list.json + /health behind BNS_HTTP_PORT; state file v1 → v2 migrates in fromJSON. * publish-tld-mirror.mjs — S3 (floating + immutable content-addressed) plus NIP-33 kind 30078 d:bns-tld-list to Nostr relays, signed via nostr-tools (libauth's Schnorr differs from BIP-340 and relays reject it). * seed-tld-beacon.mjs — idempotent chipnet seed. Current set on the beacon: bch p2p bit nav test x asm neo gt sc sia com dex cex nt (15). `.com` is held by the operator: cert on-chain, excluded from public registrar UI. Design + docs: * Decentralized.DNS/DESIGN-tld-registry.md — schema, enforcement, operator-countersig gate, USD-tiered pricing floors, fee splits, tracker/mirror mechanics, migration/grandfathering. * INSTRUCTIONS.md §3 rewritten for the 4-step flow (TREG → UI → deploy → republish snapshot). * site/tlds/ — public docs page listing current TLDs + verification paths. * site/sirius/register.html — dropdown removed; parallel search across every TLD in the TLDS array, sorted available → taken → not-on-registry. Config: * ariadne.config.json tlds → [bch, p2p, bit, nav, test, x] (CA constraints; on-chain registry is now the authoritative source, config is fallback). * setup/install.ps1 — reads TLD list via fetchTldList (per-TLD registry) with fallback to fetchBcnrTlds (legacy tls.bch list-NFT) then config. Tests: * test/tld-index.test.mjs — 30 self-contained checks (multi-beacon dispatch, state migration, ws routing, snapshot shape + root recomputability). * test/tld-enforcement.test.mjs — synthetic + live chipnet integration (with-gate correctly drops all pre-seed names).
2026-08-29 17:11:02 +02:00
}
}
return tlds;
}
feat(bns): name marketplace and TLD-owner co-sign rule Two gaps the owner panel left open. First, a hidden TLD was only a UI gate: anyone could still broadcast a REG under it and every indexer admitted it. Second, there was no way to sell a name without trusting the other side. Co-sign rule (consensus, applied in lockstep by bns.js and resolver-web.js): a REG under a TLD whose records at that height say policy "cosign" or hidden 1 is indexed only if the transaction carries the TLD's own certificate. The certificate can only be spent by the owner's key and is re-issued to them in the same transaction, so it is a co-signature nobody can forge and nothing is consumed. The TLD map now keeps the TUPD timeline so policy is evaluated at the REG height. Owners register under their private TLDs with the certificate added from their own wallet; third parties under a "cosign" TLD build the full transaction, sign their inputs and queue it at /api/cosign, where the owner approves it from the dashboard (signCosignRequest refuses to sign unless the certificate returns to the same locking script). Marketplace: a listing is the seller's certificate input plus a price output signed SIGHASH_SINGLE|ANYONECANPAY, stored by the gateway as a bulletin board (/api/market, verified against the on-chain owner and pruned when the certificate moves). The buyer completes it in one transaction, so the seller is paid exactly when the name moves. Cancelling also spends the certificate once so the offer is void. Site: market.html, Sell sub-tab and Pending approvals in the portal, Market link in nav and footer, six dictionaries extended, cache tags bumped. Verified on chipnet: cosigned.sc registered by a throwaway wallet through the queue with the .sc certificate back at the owner; aloevera.test listed and delisted through the API. Ariadne's resolver-web.js copy and the mobile Bns.java port still need the co-sign rule; until then they admit REGs this index rejects.
2026-09-17 04:04:22 +02:00
// Policy in force at `height` (0 = mempool → latest). KEEP IN LOCKSTEP with
// resolver-web.js tldRecordsAt / cosignRequired / carriesTldCertificate.
export function tldRecordsAt(tldEntry, height) {
if (!tldEntry) return {};
const ups = tldEntry.updates || [];
if (!ups.length) return tldEntry.records || {};
let rec = {};
for (const u of ups) {
if (height > 0 && (u.height <= 0 || u.height > height)) continue;
rec = u.records || {};
}
return rec;
}
// Co-sign rule: a REG under a TLD that is private (`hidden: 1`) or has
// `policy: "cosign"` at that height is admitted only if the same transaction
// carries the TLD's certificate in an output — proof the TLD owner signed.
export function cosignRequired(tldEntry, height) {
const r = tldRecordsAt(tldEntry, height);
return r.policy === "cosign" || r.hidden === 1 || r.hidden === true || r.hidden === "1";
}
export function carriesTldCertificate(vouts, tldEntry, tld) {
const commitment = Buffer.from(tld, "utf8").toString("hex");
return (vouts || []).some((v) => v.tokenData && v.tokenData.category === tldEntry.category && v.tokenData.nft?.commitment === commitment);
}
BNS: per-TLD on-chain registry, multi-beacon indexer, enforcement gate Per-TLD certificates on a distinct TLD beacon supersede the legacy list-in-one-NFT model. TLD label is the NFT commitment; TREG/TUPD payloads on the TLD beacon mirror REG/UPD on the name beacon. Chipnet TLD beacon: bchtest:qzc4c76vtw9nd49g6x8nmvc0jz62uvk4dc7hmpmerh scripthash c85db10e106606273422b2733b2a6fb8b35a4d0e7dbfb20cf59bdb0833c008b8 * config.js, resolver-web.{js,d.ts} — TLD_BEACON + TLD_BEACON_SCRIPTHASH * bns.js — KNOWN_OPS { REG, UPD, TREG, TUPD }, registerTld/updateTld/normalizeTld * bns.js + resolver-web.js — buildIndex({enforceTld:false}) drops name REGs whose TLD lacks a live TLD cert at their block height (mempool-friendly). Lockstep across both files. * lib/tld-index.js — buildTldSnapshot({history,txs}) → deterministic root over canonicalised tlds[]. Pure, importable, and stable across insertion order. * indexer/bns-indexer.js — BeaconIndex is multi-beacon; ws handler dispatches by scripthash; new HTTP endpoint GET /tld-list.json + /health behind BNS_HTTP_PORT; state file v1 → v2 migrates in fromJSON. * publish-tld-mirror.mjs — S3 (floating + immutable content-addressed) plus NIP-33 kind 30078 d:bns-tld-list to Nostr relays, signed via nostr-tools (libauth's Schnorr differs from BIP-340 and relays reject it). * seed-tld-beacon.mjs — idempotent chipnet seed. Current set on the beacon: bch p2p bit nav test x asm neo gt sc sia com dex cex nt (15). `.com` is held by the operator: cert on-chain, excluded from public registrar UI. Design + docs: * Decentralized.DNS/DESIGN-tld-registry.md — schema, enforcement, operator-countersig gate, USD-tiered pricing floors, fee splits, tracker/mirror mechanics, migration/grandfathering. * INSTRUCTIONS.md §3 rewritten for the 4-step flow (TREG → UI → deploy → republish snapshot). * site/tlds/ — public docs page listing current TLDs + verification paths. * site/sirius/register.html — dropdown removed; parallel search across every TLD in the TLDS array, sorted available → taken → not-on-registry. Config: * ariadne.config.json tlds → [bch, p2p, bit, nav, test, x] (CA constraints; on-chain registry is now the authoritative source, config is fallback). * setup/install.ps1 — reads TLD list via fetchTldList (per-TLD registry) with fallback to fetchBcnrTlds (legacy tls.bch list-NFT) then config. Tests: * test/tld-index.test.mjs — 30 self-contained checks (multi-beacon dispatch, state migration, ws routing, snapshot shape + root recomputability). * test/tld-enforcement.test.mjs — synthetic + live chipnet integration (with-gate correctly drops all pre-seed names).
2026-08-29 17:11:02 +02:00
// The gate the design specifies (Decentralized.DNS/DESIGN-tld-registry.md §4):
// a name REG is admitted iff its TLD has a live TLD certificate on the TLD
// beacon at a height ≤ the REG's height. Mempool-friendly: if either side is
// unconfirmed, admit — the beacon is the source of truth for "does this TLD
// exist now", and time-ordering across mempool entries is fuzzy anyway.
function tldAdmits(tld, regHeight, tldMap) {
const t = tldMap.get(tld);
if (!t) return false;
if (t.regHeight > 0 && regHeight > 0) return t.regHeight <= regHeight;
return true;
}
// Builds { name -> { name, category, records, txid, height, updatedTxid } }.
//
// `enforceTld: true` turns the TLD registry into a consensus gate — see §4 of
// the design doc. Off by default while the mainnet migration and grandfathering
// story lands; a chipnet deployment can flip it on immediately to prove the
// path end-to-end.
feat(bns): name marketplace and TLD-owner co-sign rule Two gaps the owner panel left open. First, a hidden TLD was only a UI gate: anyone could still broadcast a REG under it and every indexer admitted it. Second, there was no way to sell a name without trusting the other side. Co-sign rule (consensus, applied in lockstep by bns.js and resolver-web.js): a REG under a TLD whose records at that height say policy "cosign" or hidden 1 is indexed only if the transaction carries the TLD's own certificate. The certificate can only be spent by the owner's key and is re-issued to them in the same transaction, so it is a co-signature nobody can forge and nothing is consumed. The TLD map now keeps the TUPD timeline so policy is evaluated at the REG height. Owners register under their private TLDs with the certificate added from their own wallet; third parties under a "cosign" TLD build the full transaction, sign their inputs and queue it at /api/cosign, where the owner approves it from the dashboard (signCosignRequest refuses to sign unless the certificate returns to the same locking script). Marketplace: a listing is the seller's certificate input plus a price output signed SIGHASH_SINGLE|ANYONECANPAY, stored by the gateway as a bulletin board (/api/market, verified against the on-chain owner and pruned when the certificate moves). The buyer completes it in one transaction, so the seller is paid exactly when the name moves. Cancelling also spends the certificate once so the offer is void. Site: market.html, Sell sub-tab and Pending approvals in the portal, Market link in nav and footer, six dictionaries extended, cache tags bumped. Verified on chipnet: cosigned.sc registered by a throwaway wallet through the queue with the .sc certificate back at the owner; aloevera.test listed and delisted through the API. Ariadne's resolver-web.js copy and the mobile Bns.java port still need the co-sign rule; until then they admit REGs this index rejects.
2026-09-17 04:04:22 +02:00
export async function buildIndex(provider, { enforceTld = false, cosignGate = true } = {}) {
// Fetch the TLD map first — needed for the existence gate when enforcing
// and always for the co-sign rule. Both fetches share `provider`.
let tldMap = null;
if (enforceTld || cosignGate) {
try { tldMap = await buildTldMap(provider); }
catch (e) { if (enforceTld) throw e; tldMap = null; }
}
BNS: per-TLD on-chain registry, multi-beacon indexer, enforcement gate Per-TLD certificates on a distinct TLD beacon supersede the legacy list-in-one-NFT model. TLD label is the NFT commitment; TREG/TUPD payloads on the TLD beacon mirror REG/UPD on the name beacon. Chipnet TLD beacon: bchtest:qzc4c76vtw9nd49g6x8nmvc0jz62uvk4dc7hmpmerh scripthash c85db10e106606273422b2733b2a6fb8b35a4d0e7dbfb20cf59bdb0833c008b8 * config.js, resolver-web.{js,d.ts} — TLD_BEACON + TLD_BEACON_SCRIPTHASH * bns.js — KNOWN_OPS { REG, UPD, TREG, TUPD }, registerTld/updateTld/normalizeTld * bns.js + resolver-web.js — buildIndex({enforceTld:false}) drops name REGs whose TLD lacks a live TLD cert at their block height (mempool-friendly). Lockstep across both files. * lib/tld-index.js — buildTldSnapshot({history,txs}) → deterministic root over canonicalised tlds[]. Pure, importable, and stable across insertion order. * indexer/bns-indexer.js — BeaconIndex is multi-beacon; ws handler dispatches by scripthash; new HTTP endpoint GET /tld-list.json + /health behind BNS_HTTP_PORT; state file v1 → v2 migrates in fromJSON. * publish-tld-mirror.mjs — S3 (floating + immutable content-addressed) plus NIP-33 kind 30078 d:bns-tld-list to Nostr relays, signed via nostr-tools (libauth's Schnorr differs from BIP-340 and relays reject it). * seed-tld-beacon.mjs — idempotent chipnet seed. Current set on the beacon: bch p2p bit nav test x asm neo gt sc sia com dex cex nt (15). `.com` is held by the operator: cert on-chain, excluded from public registrar UI. Design + docs: * Decentralized.DNS/DESIGN-tld-registry.md — schema, enforcement, operator-countersig gate, USD-tiered pricing floors, fee splits, tracker/mirror mechanics, migration/grandfathering. * INSTRUCTIONS.md §3 rewritten for the 4-step flow (TREG → UI → deploy → republish snapshot). * site/tlds/ — public docs page listing current TLDs + verification paths. * site/sirius/register.html — dropdown removed; parallel search across every TLD in the TLDS array, sorted available → taken → not-on-registry. Config: * ariadne.config.json tlds → [bch, p2p, bit, nav, test, x] (CA constraints; on-chain registry is now the authoritative source, config is fallback). * setup/install.ps1 — reads TLD list via fetchTldList (per-TLD registry) with fallback to fetchBcnrTlds (legacy tls.bch list-NFT) then config. Tests: * test/tld-index.test.mjs — 30 self-contained checks (multi-beacon dispatch, state migration, ws routing, snapshot shape + root recomputability). * test/tld-enforcement.test.mjs — synthetic + live chipnet integration (with-gate correctly drops all pre-seed names).
2026-08-29 17:11:02 +02:00
const history = await provider.getHistory(BEACON);
history.sort(byHeightThenTxid);
const txs = new Map();
for (const { tx_hash } of history) {
try { txs.set(tx_hash, await provider.getRawTransactionObject(tx_hash)); }
catch { /* unreadable tx — skip it entirely */ }
}
const names = new Map();
const byCategory = new Map();
for (const { tx_hash, height } of orderByDependency(history.filter((h) => txs.has(h.tx_hash)), txs)) {
const tx = txs.get(tx_hash);
let payload = null;
for (const vout of tx.vout) {
payload = parseOpReturn(vout.scriptPubKey?.hex) ?? payload;
}
if (!payload) continue;
const name = payload.n;
if (!RAWNAME_RE.test(name)) continue;
if (payload.o === "REG") {
if (names.has(name)) continue; // first valid REG wins
BNS: per-TLD on-chain registry, multi-beacon indexer, enforcement gate Per-TLD certificates on a distinct TLD beacon supersede the legacy list-in-one-NFT model. TLD label is the NFT commitment; TREG/TUPD payloads on the TLD beacon mirror REG/UPD on the name beacon. Chipnet TLD beacon: bchtest:qzc4c76vtw9nd49g6x8nmvc0jz62uvk4dc7hmpmerh scripthash c85db10e106606273422b2733b2a6fb8b35a4d0e7dbfb20cf59bdb0833c008b8 * config.js, resolver-web.{js,d.ts} — TLD_BEACON + TLD_BEACON_SCRIPTHASH * bns.js — KNOWN_OPS { REG, UPD, TREG, TUPD }, registerTld/updateTld/normalizeTld * bns.js + resolver-web.js — buildIndex({enforceTld:false}) drops name REGs whose TLD lacks a live TLD cert at their block height (mempool-friendly). Lockstep across both files. * lib/tld-index.js — buildTldSnapshot({history,txs}) → deterministic root over canonicalised tlds[]. Pure, importable, and stable across insertion order. * indexer/bns-indexer.js — BeaconIndex is multi-beacon; ws handler dispatches by scripthash; new HTTP endpoint GET /tld-list.json + /health behind BNS_HTTP_PORT; state file v1 → v2 migrates in fromJSON. * publish-tld-mirror.mjs — S3 (floating + immutable content-addressed) plus NIP-33 kind 30078 d:bns-tld-list to Nostr relays, signed via nostr-tools (libauth's Schnorr differs from BIP-340 and relays reject it). * seed-tld-beacon.mjs — idempotent chipnet seed. Current set on the beacon: bch p2p bit nav test x asm neo gt sc sia com dex cex nt (15). `.com` is held by the operator: cert on-chain, excluded from public registrar UI. Design + docs: * Decentralized.DNS/DESIGN-tld-registry.md — schema, enforcement, operator-countersig gate, USD-tiered pricing floors, fee splits, tracker/mirror mechanics, migration/grandfathering. * INSTRUCTIONS.md §3 rewritten for the 4-step flow (TREG → UI → deploy → republish snapshot). * site/tlds/ — public docs page listing current TLDs + verification paths. * site/sirius/register.html — dropdown removed; parallel search across every TLD in the TLDS array, sorted available → taken → not-on-registry. Config: * ariadne.config.json tlds → [bch, p2p, bit, nav, test, x] (CA constraints; on-chain registry is now the authoritative source, config is fallback). * setup/install.ps1 — reads TLD list via fetchTldList (per-TLD registry) with fallback to fetchBcnrTlds (legacy tls.bch list-NFT) then config. Tests: * test/tld-index.test.mjs — 30 self-contained checks (multi-beacon dispatch, state migration, ws routing, snapshot shape + root recomputability). * test/tld-enforcement.test.mjs — synthetic + live chipnet integration (with-gate correctly drops all pre-seed names).
2026-08-29 17:11:02 +02:00
// TLD gate — see design §4 / tldAdmits above. Only applies to REG; UPDs
// are transitions on names already admitted, so no gate needed there.
if (enforceTld && !tldAdmits(tldOf(name), height, tldMap)) continue;
feat(bns): name marketplace and TLD-owner co-sign rule Two gaps the owner panel left open. First, a hidden TLD was only a UI gate: anyone could still broadcast a REG under it and every indexer admitted it. Second, there was no way to sell a name without trusting the other side. Co-sign rule (consensus, applied in lockstep by bns.js and resolver-web.js): a REG under a TLD whose records at that height say policy "cosign" or hidden 1 is indexed only if the transaction carries the TLD's own certificate. The certificate can only be spent by the owner's key and is re-issued to them in the same transaction, so it is a co-signature nobody can forge and nothing is consumed. The TLD map now keeps the TUPD timeline so policy is evaluated at the REG height. Owners register under their private TLDs with the certificate added from their own wallet; third parties under a "cosign" TLD build the full transaction, sign their inputs and queue it at /api/cosign, where the owner approves it from the dashboard (signCosignRequest refuses to sign unless the certificate returns to the same locking script). Marketplace: a listing is the seller's certificate input plus a price output signed SIGHASH_SINGLE|ANYONECANPAY, stored by the gateway as a bulletin board (/api/market, verified against the on-chain owner and pruned when the certificate moves). The buyer completes it in one transaction, so the seller is paid exactly when the name moves. Cancelling also spends the certificate once so the offer is void. Site: market.html, Sell sub-tab and Pending approvals in the portal, Market link in nav and footer, six dictionaries extended, cache tags bumped. Verified on chipnet: cosigned.sc registered by a throwaway wallet through the queue with the .sc certificate back at the owner; aloevera.test listed and delisted through the API. Ariadne's resolver-web.js copy and the mobile Bns.java port still need the co-sign rule; until then they admit REGs this index rejects.
2026-09-17 04:04:22 +02:00
if (cosignGate && tldMap) {
const tldEntry = tldMap.get(tldOf(name));
if (tldEntry && cosignRequired(tldEntry, height) && !carriesTldCertificate(tx.vout, tldEntry, tldOf(name))) continue;
}
// the same tx must mint a certificate whose commitment is the name
const commitment = Buffer.from(name, "utf8").toString("hex");
const mint = tx.vout.find((v) => v.tokenData?.nft?.commitment === commitment);
if (!mint) continue;
const entry = {
name, category: mint.tokenData.category, records: payload.r ?? {},
txid: tx_hash, height, updatedTxid: null,
};
names.set(name, entry);
byCategory.set(entry.category, entry);
} else if (payload.o === "UPD") {
const entry = names.get(name);
if (!entry) continue;
// ownership proof: some output carries the name's category
// (consensus: impossible unless an input held that category)
const proves = tx.vout.some((v) => v.tokenData?.category === entry.category);
if (!proves) continue;
entry.records = payload.r ?? {};
entry.updatedTxid = tx_hash;
}
}
return names;
}
export async function resolveName(provider, input) {
const name = normalizeName(input);
const index = await buildIndex(provider);
return index.get(name) ?? null;
}
// ---------- tx builders ----------
// CashTokens genesis must spend an outpoint with index 0; self-send to make one if needed.
async function ensureGenesisInput(wallet) {
const utxos = await wallet.getUtxos();
if (utxos.some((u) => u.vout === 0 && !u.token)) return;
await wallet.send([{ cashaddr: wallet.cashaddr, value: 10000n, unit: "sat" }]);
}
export async function registerName(wallet, input, records) {
const name = normalizeName(input);
const index = await buildIndex(wallet.provider);
if (index.has(name)) throw new Error(`"${name}${'.bch'}" is already registered`);
await ensureGenesisInput(wallet);
const payload = encodePayload("REG", name, records);
const result = await wallet.tokenGenesis(
{
cashaddr: wallet.tokenaddr,
nft: {
capability: NFTCapability.none,
commitment: Buffer.from(name, "utf8").toString("hex"),
},
value: 1000n,
},
[
OpReturnData.fromUint8Array(payload),
{ cashaddr: BEACON, value: BEACON_DUST_SAT, unit: "sat" },
],
);
return { name, txid: result.txId, category: result.tokenIds?.[0] };
}
export async function updateName(wallet, input, records) {
const name = normalizeName(input);
const index = await buildIndex(wallet.provider);
const entry = index.get(name);
if (!entry) throw new Error(`"${name}.bch" is not registered`);
const payload = encodePayload("UPD", name, records);
const result = await wallet.send([
new TokenSendRequest({
cashaddr: wallet.tokenaddr,
category: entry.category,
nft: {
capability: NFTCapability.none,
commitment: Buffer.from(name, "utf8").toString("hex"),
},
}),
OpReturnData.fromUint8Array(payload),
{ cashaddr: BEACON, value: BEACON_DUST_SAT, unit: "sat" },
]);
return { name, txid: result.txId, category: entry.category };
}
BNS: per-TLD on-chain registry, multi-beacon indexer, enforcement gate Per-TLD certificates on a distinct TLD beacon supersede the legacy list-in-one-NFT model. TLD label is the NFT commitment; TREG/TUPD payloads on the TLD beacon mirror REG/UPD on the name beacon. Chipnet TLD beacon: bchtest:qzc4c76vtw9nd49g6x8nmvc0jz62uvk4dc7hmpmerh scripthash c85db10e106606273422b2733b2a6fb8b35a4d0e7dbfb20cf59bdb0833c008b8 * config.js, resolver-web.{js,d.ts} — TLD_BEACON + TLD_BEACON_SCRIPTHASH * bns.js — KNOWN_OPS { REG, UPD, TREG, TUPD }, registerTld/updateTld/normalizeTld * bns.js + resolver-web.js — buildIndex({enforceTld:false}) drops name REGs whose TLD lacks a live TLD cert at their block height (mempool-friendly). Lockstep across both files. * lib/tld-index.js — buildTldSnapshot({history,txs}) → deterministic root over canonicalised tlds[]. Pure, importable, and stable across insertion order. * indexer/bns-indexer.js — BeaconIndex is multi-beacon; ws handler dispatches by scripthash; new HTTP endpoint GET /tld-list.json + /health behind BNS_HTTP_PORT; state file v1 → v2 migrates in fromJSON. * publish-tld-mirror.mjs — S3 (floating + immutable content-addressed) plus NIP-33 kind 30078 d:bns-tld-list to Nostr relays, signed via nostr-tools (libauth's Schnorr differs from BIP-340 and relays reject it). * seed-tld-beacon.mjs — idempotent chipnet seed. Current set on the beacon: bch p2p bit nav test x asm neo gt sc sia com dex cex nt (15). `.com` is held by the operator: cert on-chain, excluded from public registrar UI. Design + docs: * Decentralized.DNS/DESIGN-tld-registry.md — schema, enforcement, operator-countersig gate, USD-tiered pricing floors, fee splits, tracker/mirror mechanics, migration/grandfathering. * INSTRUCTIONS.md §3 rewritten for the 4-step flow (TREG → UI → deploy → republish snapshot). * site/tlds/ — public docs page listing current TLDs + verification paths. * site/sirius/register.html — dropdown removed; parallel search across every TLD in the TLDS array, sorted available → taken → not-on-registry. Config: * ariadne.config.json tlds → [bch, p2p, bit, nav, test, x] (CA constraints; on-chain registry is now the authoritative source, config is fallback). * setup/install.ps1 — reads TLD list via fetchTldList (per-TLD registry) with fallback to fetchBcnrTlds (legacy tls.bch list-NFT) then config. Tests: * test/tld-index.test.mjs — 30 self-contained checks (multi-beacon dispatch, state migration, ws routing, snapshot shape + root recomputability). * test/tld-enforcement.test.mjs — synthetic + live chipnet integration (with-gate correctly drops all pre-seed names).
2026-08-29 17:11:02 +02:00
// ---------- TLD certificate tx builders ----------
// Mirror registerName / updateName but for the per-TLD registry
// (Decentralized.DNS/DESIGN-tld-registry.md §3):
// • payload opcode is TREG/TUPD
// • the NFT commitment is the TLD label (e.g. "x") — not a name
// • dust flows to TLD_BEACON, not the name BEACON
// Ownership + spend rules are otherwise identical to the name path, so the
// signing / covenant / renewal work carries over unchanged.
export function normalizeTld(input) {
const tld = String(input).toLowerCase().trim().replace(/^\.+/, "").replace(/\.+$/, "");
if (!TLD_RE.test(tld)) throw new Error(`invalid TLD "${input}" (allowed: [a-z0-9-]{1,16})`);
return tld;
}
export async function registerTld(wallet, input, records = {}) {
const tld = normalizeTld(input);
await ensureGenesisInput(wallet);
const payload = encodePayload("TREG", tld, records);
const result = await wallet.tokenGenesis(
{
cashaddr: wallet.tokenaddr,
nft: {
capability: NFTCapability.none,
commitment: Buffer.from(tld, "utf8").toString("hex"),
},
value: 1000n,
},
[
OpReturnData.fromUint8Array(payload),
{ cashaddr: TLD_BEACON, value: BEACON_DUST_SAT, unit: "sat" },
],
);
return { tld, txid: result.txId, category: result.tokenIds?.[0] };
}
export async function updateTld(wallet, input, records, category) {
const tld = normalizeTld(input);
if (!category) throw new Error("updateTld requires the TLD certificate's category id");
const payload = encodePayload("TUPD", tld, records);
const result = await wallet.send([
new TokenSendRequest({
cashaddr: wallet.tokenaddr,
category,
nft: {
capability: NFTCapability.none,
commitment: Buffer.from(tld, "utf8").toString("hex"),
},
}),
OpReturnData.fromUint8Array(payload),
{ cashaddr: TLD_BEACON, value: BEACON_DUST_SAT, unit: "sat" },
]);
return { tld, txid: result.txId, category };
}