2026-07-29 13:54:34 +02:00
|
|
|
// 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";
|
2026-10-04 16:13:04 +02:00
|
|
|
import { reduceBlocklist, soleCertificate } from "./resolver-web.js";
|
2026-07-29 13:54:34 +02:00
|
|
|
|
|
|
|
|
// ---------- 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)
|
2026-10-04 02:32:59 +02:00
|
|
|
// RPT / BLK / UBK — blocklist report, block and unblock
|
|
|
|
|
// (PROTOCOL-ADDENDUM-blocklist.md); an RPT's `n` is an array.
|
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
|
|
|
// KEEP IN LOCKSTEP with resolver-web.js parseOpReturnHex — memory rule.
|
2026-10-04 02:32:59 +02:00
|
|
|
if (!KNOWN_OPS.has(p.o)) return null;
|
|
|
|
|
if (typeof p.n === "string") return p;
|
|
|
|
|
if (p.o === "RPT" && Array.isArray(p.n) && p.n.every((x) => typeof x === "string")) return p;
|
2026-07-29 13:54:34 +02:00
|
|
|
} catch { /* not ours */ }
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-04 16:53:58 +02:00
|
|
|
// One message per transaction: more than one parseable BNS payload means none.
|
|
|
|
|
// KEEP IN LOCKSTEP with resolver-web.js onlyPayload (the reasoning is there).
|
|
|
|
|
export function onlyPayload(vouts) {
|
|
|
|
|
let found = null;
|
|
|
|
|
for (const v of vouts || []) {
|
|
|
|
|
const p = parseOpReturn(v?.scriptPubKey?.hex);
|
|
|
|
|
if (!p) continue;
|
|
|
|
|
if (found) return null;
|
|
|
|
|
found = p;
|
|
|
|
|
}
|
|
|
|
|
return found;
|
|
|
|
|
}
|
|
|
|
|
|
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.
|
2026-10-04 02:32:59 +02:00
|
|
|
export const KNOWN_OPS = new Set(["REG", "UPD", "REL", "TREG", "TUPD", "RPT", "BLK", "UBK"]);
|
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
|
|
|
|
2026-07-29 13:54:34 +02:00
|
|
|
// ---------- 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);
|
2026-10-04 16:53:58 +02:00
|
|
|
const payload = onlyPayload(tx.vout);
|
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
|
|
|
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;
|
2026-10-04 16:13:04 +02:00
|
|
|
if (!soleCertificate(tx, mint)) continue; // one definition, in resolver-web.js
|
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.
|
2026-10-04 01:39:15 +02:00
|
|
|
// ---- REL: voluntary release (Decentralized.DNS/PROTOCOL-ADDENDUM-release.md) ----
|
|
|
|
|
// KEEP IN LOCKSTEP with resolver-web.js burnsCertificate / mayReRegister —
|
|
|
|
|
// Argus/test/release.test.mjs runs both over the same fixtures.
|
|
|
|
|
function spentOutput(vin, txs) {
|
|
|
|
|
const prev = vin && vin.txid ? txs.get(vin.txid) : null;
|
|
|
|
|
if (!prev || !Array.isArray(prev.vout)) return null;
|
|
|
|
|
return prev.vout.find((o) => o && o.n === vin.vout) ?? prev.vout[vin.vout] ?? null;
|
|
|
|
|
}
|
|
|
|
|
// Valid release: no output carries the name's category, and one input spends
|
|
|
|
|
// an output of an earlier beacon transaction that carried it as an NFT.
|
|
|
|
|
export function burnsCertificate(tx, entry, txs) {
|
|
|
|
|
if (!tx || !entry || !entry.category) return false;
|
|
|
|
|
if ((tx.vout || []).some((v) => v.tokenData && v.tokenData.category === entry.category)) return false;
|
|
|
|
|
return (tx.vin || []).some((vin) => {
|
|
|
|
|
const out = spentOutput(vin, txs);
|
|
|
|
|
return !!(out && out.tokenData && out.tokenData.nft && out.tokenData.category === entry.category);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
// One-block gap: a released name is registrable again only by a REG in a later
|
|
|
|
|
// block than a CONFIRMED release (an unconfirmed REG qualifies).
|
|
|
|
|
export function mayReRegister(releasedAt, height) {
|
|
|
|
|
if (releasedAt === undefined) return true;
|
|
|
|
|
if (!(releasedAt > 0)) return false;
|
|
|
|
|
return !(height > 0) || height > releasedAt;
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
2026-07-29 13:54:34 +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();
|
2026-10-04 01:39:15 +02:00
|
|
|
const released = new Map(); // name -> height of its release
|
2026-07-29 13:54:34 +02:00
|
|
|
|
|
|
|
|
for (const { tx_hash, height } of orderByDependency(history.filter((h) => txs.has(h.tx_hash)), txs)) {
|
|
|
|
|
const tx = txs.get(tx_hash);
|
|
|
|
|
|
2026-10-04 16:53:58 +02:00
|
|
|
const payload = onlyPayload(tx.vout);
|
2026-07-29 13:54:34 +02:00
|
|
|
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
|
2026-10-04 01:39:15 +02:00
|
|
|
if (!mayReRegister(released.get(name), height)) continue; // one-block gap after a release
|
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;
|
|
|
|
|
}
|
2026-07-29 13:54:34 +02:00
|
|
|
// 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;
|
2026-10-04 16:13:04 +02:00
|
|
|
// the certificate must be the only token of its category (resolver-web.js)
|
|
|
|
|
if (!soleCertificate(tx, mint)) continue;
|
2026-07-29 13:54:34 +02:00
|
|
|
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);
|
2026-10-04 01:39:15 +02:00
|
|
|
released.delete(name);
|
|
|
|
|
} else if (payload.o === "REL") {
|
|
|
|
|
const entry = names.get(name);
|
|
|
|
|
if (!entry) continue;
|
|
|
|
|
if (!burnsCertificate(tx, entry, txs)) continue;
|
|
|
|
|
names.delete(name);
|
|
|
|
|
byCategory.delete(entry.category);
|
|
|
|
|
released.set(name, height);
|
2026-07-29 13:54:34 +02:00
|
|
|
} 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-04 01:39:15 +02:00
|
|
|
Object.defineProperty(names, "released", { value: released, enumerable: false });
|
2026-07-29 13:54:34 +02:00
|
|
|
return names;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-04 02:32:59 +02:00
|
|
|
// ---- blocklists (PROTOCOL-ADDENDUM-blocklist.md) ----
|
|
|
|
|
// The rules live in ONE place, resolver-web.js reduceBlocklist; this is only
|
|
|
|
|
// the mainnet-js-provider way of feeding it a list's history.
|
|
|
|
|
export async function buildBlocklist(provider, { address, source = null }) {
|
|
|
|
|
const history = await provider.getHistory(address);
|
|
|
|
|
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 */ }
|
|
|
|
|
}
|
|
|
|
|
return reduceBlocklist({ history, txs, address, source });
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-29 13:54:34 +02:00
|
|
|
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 };
|
|
|
|
|
}
|