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).
146 lines
7.3 KiB
JavaScript
146 lines
7.3 KiB
JavaScript
// Tests for the TLD-registry enforcement gate in bns.js buildIndex.
|
|
//
|
|
// Two parts:
|
|
// 1) Unit — synthetic mock provider, fast and deterministic. Confirms the
|
|
// admits/rejects rule and mempool-friendly behaviour.
|
|
// 2) Chipnet integration — live provider. Since we just seeded the TLD
|
|
// beacon at the tip, EVERY existing chipnet name predates its TLD's
|
|
// TREG. So enforceTld:true should reduce the index dramatically compared
|
|
// to enforceTld:false. That is the design's "no grandfathering" rule
|
|
// producing exactly the effect the doc predicts (§2.2).
|
|
|
|
import { buildIndex, registerName, registerTld, encodePayload } from "../src/lib/bns.js";
|
|
import { BEACON, TLD_BEACON } from "../src/config.js";
|
|
import { loadWallet } from "../src/lib/wallet.js";
|
|
|
|
let failures = 0;
|
|
const ok = (c, m) => { console.log((c ? " ✅ " : " ❌ ") + m); if (!c) failures++; };
|
|
|
|
// ---------- (1) unit tests with a synthetic mock provider ----------
|
|
console.log("1) unit: enforcement gate on synthetic fixtures");
|
|
|
|
// A tx factory that produces the shape provider.getRawTransactionObject returns.
|
|
// The important fields for the indexer: vin[], vout[].scriptPubKey.hex,
|
|
// vout[].tokenData?.{category, nft.commitment}.
|
|
function opreturnVout(op, name, records) {
|
|
const buf = encodePayload(op, name, records);
|
|
return { scriptPubKey: { hex: "6a" + buf.length.toString(16).padStart(2, "0") + buf.toString("hex") } };
|
|
}
|
|
function mintVout(category, commitLabel) {
|
|
return {
|
|
scriptPubKey: { hex: "76a914" + "aa".repeat(20) + "88ac" },
|
|
tokenData: { category, amount: "0", nft: { capability: "none", commitment: Buffer.from(commitLabel, "utf8").toString("hex") } },
|
|
};
|
|
}
|
|
function beaconVout() { return { scriptPubKey: { hex: "76a91400aa" + "bb".repeat(18) + "88ac" } }; }
|
|
|
|
// Build a mock provider whose getHistory(address) returns different histories
|
|
// per address, and getRawTransactionObject returns the queued txs.
|
|
function mockProvider({ nameHistory, tldHistory, txs }) {
|
|
return {
|
|
async getHistory(addr) {
|
|
if (addr === BEACON) return nameHistory;
|
|
if (addr === TLD_BEACON) return tldHistory;
|
|
return [];
|
|
},
|
|
async getRawTransactionObject(txid) {
|
|
const tx = txs.get(txid);
|
|
if (!tx) throw new Error("unknown tx " + txid);
|
|
return tx;
|
|
},
|
|
};
|
|
}
|
|
|
|
// Fixture: TLD `x` registered at height 100.
|
|
// Names:
|
|
// `late` — regged at 150 under `.x` (should be ADMITTED under enforcement)
|
|
// `early` — regged at 50 under `.x` (predates TLD → REJECTED under enforcement)
|
|
// `orphan` — regged at 200 under `.zz` (no TLD cert → REJECTED under enforcement)
|
|
// `mempoolName` — regged at 0 (mempool) under `.x` (TLD confirmed → ADMITTED, mempool-friendly)
|
|
{
|
|
const CAT_X = "aa".repeat(32);
|
|
const CAT_LATE = "cc".repeat(32);
|
|
const CAT_EARLY = "dd".repeat(32);
|
|
const CAT_ORPHAN = "ee".repeat(32);
|
|
const CAT_MEMPOOL = "ff".repeat(32);
|
|
|
|
const txs = new Map();
|
|
txs.set("tX", { txid: "tX", vin: [], vout: [beaconVout(), opreturnVout("TREG", "x", { policy: "open" }), mintVout(CAT_X, "x")] });
|
|
txs.set("tEarly", { txid: "tEarly", vin: [], vout: [beaconVout(), opreturnVout("REG", "early.x", {}), mintVout(CAT_EARLY, "early.x")] });
|
|
txs.set("tLate", { txid: "tLate", vin: [], vout: [beaconVout(), opreturnVout("REG", "late.x", {}), mintVout(CAT_LATE, "late.x")] });
|
|
txs.set("tOrphan", { txid: "tOrphan", vin: [], vout: [beaconVout(), opreturnVout("REG", "orphan.zz",{}), mintVout(CAT_ORPHAN, "orphan.zz")] });
|
|
txs.set("tMempool", { txid: "tMempool", vin: [], vout: [beaconVout(), opreturnVout("REG", "mempool.x",{}), mintVout(CAT_MEMPOOL, "mempool.x")] });
|
|
|
|
const provider = mockProvider({
|
|
nameHistory: [
|
|
{ tx_hash: "tEarly", height: 50 },
|
|
{ tx_hash: "tLate", height: 150 },
|
|
{ tx_hash: "tOrphan", height: 200 },
|
|
{ tx_hash: "tMempool", height: 0 },
|
|
],
|
|
tldHistory: [{ tx_hash: "tX", height: 100 }],
|
|
txs,
|
|
});
|
|
|
|
const noGate = await buildIndex(provider);
|
|
ok(noGate.size === 4, `enforceTld:false admits all 4 name REGs (got ${noGate.size})`);
|
|
ok(noGate.has("early.x") && noGate.has("late.x") && noGate.has("orphan.zz") && noGate.has("mempool.x"), "no-gate: every name present");
|
|
|
|
const withGate = await buildIndex(provider, { enforceTld: true });
|
|
ok(!withGate.has("early.x"), "gate: early.x REJECTED (predates TLD)");
|
|
ok(withGate.has("late.x"), "gate: late.x ADMITTED (post-TLD)");
|
|
ok(!withGate.has("orphan.zz"), "gate: orphan.zz REJECTED (no TLD cert)");
|
|
ok(withGate.has("mempool.x"), "gate: mempool.x ADMITTED (mempool-friendly rule)");
|
|
ok(withGate.size === 2, `gate: exactly 2 admitted (got ${withGate.size})`);
|
|
}
|
|
|
|
// Fixture: TLD `y` registered in MEMPOOL (height 0). A mempool name under `.y`
|
|
// is admitted (both in mempool); a confirmed name under `.y` before the TLD
|
|
// confirms is also admitted (mempool-friendly: we cannot decide time-order
|
|
// across mempool + confirmed edge). This is the deliberate simplification.
|
|
{
|
|
const CAT_Y = "11".repeat(32);
|
|
const CAT_N = "22".repeat(32);
|
|
const txs = new Map();
|
|
txs.set("tYmempool", { txid: "tYmempool", vin: [], vout: [beaconVout(), opreturnVout("TREG", "y", {}), mintVout(CAT_Y, "y")] });
|
|
txs.set("tNameUnderY", { txid: "tNameUnderY", vin: [], vout: [beaconVout(), opreturnVout("REG", "name.y", {}), mintVout(CAT_N, "name.y")] });
|
|
|
|
const provider = mockProvider({
|
|
nameHistory: [{ tx_hash: "tNameUnderY", height: 0 }],
|
|
tldHistory: [{ tx_hash: "tYmempool", height: 0 }],
|
|
txs,
|
|
});
|
|
|
|
const withGate = await buildIndex(provider, { enforceTld: true });
|
|
ok(withGate.has("name.y"), "gate: both-in-mempool → admitted");
|
|
}
|
|
|
|
// ---------- (2) chipnet integration ----------
|
|
// Real provider. All existing chipnet names predate the just-seeded TLD TREGs
|
|
// (in mempool at time of writing), so enforceTld:true should return a MUCH
|
|
// smaller index than enforceTld:false.
|
|
console.log("\n2) chipnet integration (live provider)");
|
|
try {
|
|
const wallet = await loadWallet("main");
|
|
const noGate = await buildIndex(wallet.provider);
|
|
const withGate = await buildIndex(wallet.provider, { enforceTld: true });
|
|
console.log(` index sizes: no-gate=${noGate.size}, with-gate=${withGate.size}`);
|
|
ok(noGate.size > 0, "no-gate index has entries");
|
|
ok(withGate.size <= noGate.size, "gate never admits MORE than no-gate");
|
|
const droppedTlds = new Set();
|
|
for (const [name] of noGate) if (!withGate.has(name)) {
|
|
const i = name.lastIndexOf("."); droppedTlds.add(i < 0 ? "bch" : name.slice(i + 1));
|
|
}
|
|
console.log(` TLDs whose names got dropped: ${[...droppedTlds].sort().join(", ") || "(none)"}`);
|
|
// If the TLD TREGs have not confirmed yet (mempool), the mempool-friendly
|
|
// rule admits chipnet names that share the mempool state; once confirmed,
|
|
// no chipnet name predating them is admitted. Either way: gate should have
|
|
// dropped SOMETHING, because there are chipnet names under TLDs we did not
|
|
// seed (`.de`, `.dgb`, `.silentmode`).
|
|
ok(droppedTlds.size > 0, "gate drops at least one TLD's worth of names (chipnet had names under unseeded TLDs)");
|
|
} catch (e) {
|
|
console.log(" ⚠️ chipnet section skipped (network / wallet unavailable):", e.message);
|
|
}
|
|
|
|
console.log(failures === 0 ? "\n🎉 ALL PASSED — enforcement gate works" : `\n💥 ${failures} check(s) failed`);
|
|
process.exit(failures === 0 ? 0 : 1);
|