argus/test/tld-enforcement.test.mjs
Local Dev b4fc73f0fa BNS: a transaction with more than one protocol message carries none
Resolvers took the last message in a transaction. Wherever one party fixes
a message and another assembles the transaction - the delivery escrow pins
the registration at output 1 - a later output could replace it and every
resolver would read that one. Counting instead of choosing removes the
question: one message, or the transaction says nothing.

Same rule in all three resolvers. The Android parser now recognises the
same set of operations as the JavaScript one, since what counts as a
message decides which transactions are void. No transaction on chipnet
carried more than one.

Three older suites built registrations that never created their
certificate's category; their fixtures now do, as the sole-certificate
rule requires.
2026-10-04 16:53:58 +02:00

146 lines
7.5 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: [{ txid: CAT_X, vout: 0 }], vout: [beaconVout(), opreturnVout("TREG", "x", { policy: "open" }), mintVout(CAT_X, "x")] });
txs.set("tEarly", { txid: "tEarly", vin: [{ txid: CAT_EARLY, vout: 0 }], vout: [beaconVout(), opreturnVout("REG", "early.x", {}), mintVout(CAT_EARLY, "early.x")] });
txs.set("tLate", { txid: "tLate", vin: [{ txid: CAT_LATE, vout: 0 }], vout: [beaconVout(), opreturnVout("REG", "late.x", {}), mintVout(CAT_LATE, "late.x")] });
txs.set("tOrphan", { txid: "tOrphan", vin: [{ txid: CAT_ORPHAN, vout: 0 }], vout: [beaconVout(), opreturnVout("REG", "orphan.zz",{}), mintVout(CAT_ORPHAN, "orphan.zz")] });
txs.set("tMempool", { txid: "tMempool", vin: [{ txid: CAT_MEMPOOL, vout: 0 }], 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: [{ txid: CAT_Y, vout: 0 }], vout: [beaconVout(), opreturnVout("TREG", "y", {}), mintVout(CAT_Y, "y")] });
txs.set("tNameUnderY", { txid: "tNameUnderY", vin: [{ txid: CAT_N, vout: 0 }], 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);