// The two integrity properties: search reveals nothing, and holdings cannot hide. // // Offline — fake clients only. Both properties are about what the software makes // impossible, so they have to be provable without a network. import test from "node:test"; import assert from "node:assert/strict"; import { NameIndex, buildFromCache } from "../src/lib/name-index.js"; import { verifyPortfolio, renderPortfolio, heldNames, PORTFOLIO_STATUS } from "../src/lib/portfolio.js"; import { addressToLockingBytecode } from "../src/lib/chain.js"; const OPERATOR = "bchtest:qzew3dqcsj0guwe5y3uxf9q2xs5ajgy9lca4z093cu"; const OTHER = "bchtest:qqmcn373plhjrvs5mrswuvevdvepxum8x58jhawqxs"; const utf8hex = (s) => [...new TextEncoder().encode(s)].map((b) => b.toString(16).padStart(2, "0")).join(""); /** A REG transaction the resolver will accept. */ function regTx(txid, name, category) { const payload = "BNS1" + JSON.stringify({ o: "REG", n: name, r: {} }); const hex = [...new TextEncoder().encode(payload)].map((b) => b.toString(16).padStart(2, "0")).join(""); const len = (hex.length / 2).toString(16).padStart(2, "0"); return { txid, vin: [], vout: [ { n: 0, scriptPubKey: { hex: `6a4c${len}${hex}` } }, { n: 1, scriptPubKey: { hex: "76a914" + "11".repeat(20) + "88ac" }, tokenData: { category, amount: "0", nft: { capability: "none", commitment: utf8hex(name) } } }, ], }; } /** Electrum stand-in: scripted history, transactions and UTXOs. */ function fakeClient({ history = [], txs = {}, utxos = {} } = {}) { const calls = []; return { calls, async call(method, params) { calls.push({ method, params }); if (method === "blockchain.scripthash.get_history") return history; if (method === "blockchain.transaction.get") { const tx = txs[params[0]]; if (!tx) throw new Error("unknown tx"); return tx; } if (method === "blockchain.scripthash.listunspent") { // Map scripthash back to whichever address we were given UTXOs for. for (const [addr, list] of Object.entries(utxos)) { const sh = shOf(addr); if (sh === params[0]) return list; } return []; } if (method === "server.version") return ["fake", "1.4.2"]; return null; }, close() {}, }; } // scripthash helper mirroring chain.js let _sha; function shOf(address) { if (!_sha) { // lazily to keep the import list short _sha = require("@bitauth/libauth"); } const bytes = addressToLockingBytecode(address); return _sha.binToHex(_sha.sha256.hash(bytes).slice().reverse()); } const CAT_A = "aa".repeat(32); const CAT_B = "bb".repeat(32); const TX_A = "1a".repeat(32); const TX_B = "1b".repeat(32); // ---------- search reveals nothing ---------- test("after one sync, lookups touch the network zero times", async () => { const client = fakeClient({ history: [{ tx_hash: TX_A, height: 100 }], txs: { [TX_A]: regTx(TX_A, "alice", CAT_A) }, }); const idx = new NameIndex(); await idx.sync(client); const afterSync = client.calls.length; // The property: searching cannot be observed, because there is nothing to observe. for (const n of ["alice.bch", "bob.bch", "verysecretplan.bch", "acquisition-target.bch"]) { idx.lookup(n); } assert.equal(client.calls.length, afterSync, "a lookup must not produce a single request"); assert.equal(idx.lookup("alice.bch")?.name, "alice.bch"); assert.equal(idx.lookup("bob.bch"), null); }); test("lookup before sync fails loudly rather than silently answering wrong", async () => { const idx = new NameIndex(); assert.throws(() => idx.lookup("alice.bch"), /call sync\(\) before lookup\(\)/); }); test("a second sync fetches only what is new", async () => { const txs = { [TX_A]: regTx(TX_A, "alice", CAT_A) }; const client = fakeClient({ history: [{ tx_hash: TX_A, height: 100 }], txs }); const idx = new NameIndex(); const first = await idx.sync(client); assert.equal(first.fetched, 1); // A new registration appears. txs[TX_B] = regTx(TX_B, "bob", CAT_B); const client2 = fakeClient({ history: [{ tx_hash: TX_A, height: 100 }, { tx_hash: TX_B, height: 101 }], txs, }); const second = await idx.sync(client2); assert.equal(second.fetched, 1, "only the unseen transaction is fetched"); assert.equal(second.cached, 1, "the already-known one is reused"); assert.equal(idx.lookup("bob.bch")?.name, "bob.bch"); assert.equal(idx.lookup("alice.bch")?.name, "alice.bch", "the old entry survives"); }); test("the cache survives a reload through storage", async () => { const store = new Map(); const storage = { getItem: (k) => store.get(k) ?? null, setItem: (k, v) => store.set(k, v), removeItem: (k) => store.delete(k), }; const client = fakeClient({ history: [{ tx_hash: TX_A, height: 100 }], txs: { [TX_A]: regTx(TX_A, "alice", CAT_A) }, }); const a = new NameIndex({ storage }); await a.sync(client); const b = new NameIndex({ storage }); assert.equal(b.load(), true, "a fresh instance picks up the cache"); const fresh = fakeClient({ history: [{ tx_hash: TX_A, height: 100 }], txs: { [TX_A]: regTx(TX_A, "alice", CAT_A) }, }); const r = await b.sync(fresh); assert.equal(r.fetched, 0, "nothing refetched after a reload"); assert.equal(b.lookup("alice.bch")?.name, "alice.bch"); }); test("a shrinking history drops the cache instead of merging disagreeing views", async () => { const idx = new NameIndex(); await idx.sync(fakeClient({ history: [{ tx_hash: TX_A, height: 100 }, { tx_hash: TX_B, height: 101 }], txs: { [TX_A]: regTx(TX_A, "alice", CAT_A), [TX_B]: regTx(TX_B, "bob", CAT_B) }, })); assert.equal(idx.size, 2); // A reorg, or a server with less data. Refetch rather than trust a merge. const shrunk = fakeClient({ history: [{ tx_hash: TX_A, height: 100 }], txs: { [TX_A]: regTx(TX_A, "alice", CAT_A) }, }); const r = await idx.sync(shrunk); assert.equal(r.fetched, 1, "everything is refetched"); assert.equal(idx.size, 1); }); test("a pool's union history is preferred over a single server's view", async () => { // The pool defends against a server hiding a registration; the index must use // it when available rather than silently taking one server's word. let unionUsed = false; const pool = { async unionHistory() { unionUsed = true; return { history: [{ tx_hash: TX_A, height: 100 }] }; }, async call(method, params) { if (method === "blockchain.transaction.get") return regTx(TX_A, "alice", CAT_A); return null; }, }; const idx = new NameIndex(); await idx.sync(pool); assert.equal(unionUsed, true, "unionHistory must be used when the client offers it"); assert.equal(idx.lookup("alice.bch")?.name, "alice.bch"); }); test("index rules are not reimplemented — buildFromCache defers to the resolver", async () => { const history = [{ tx_hash: TX_A, height: 100 }]; const txs = new Map([[TX_A, regTx(TX_A, "alice", CAT_A)]]); const built = await buildFromCache(history, txs); assert.ok(built instanceof Map); assert.equal(built.get("alice.bch")?.category, CAT_A); }); // ---------- holdings cannot hide ---------- const tokenUtxo = (txid, category, name) => ({ tx_hash: txid, tx_pos: 1, value: 1000, height: 100, token_data: { category, amount: "0", nft: { capability: "none", commitment: utf8hex(name) } }, }); function portfolioClient({ regs, utxosByAddress }) { const history = regs.map((r) => ({ tx_hash: r.txid, height: r.height ?? 100 })); const txs = Object.fromEntries(regs.map((r) => [r.txid, regTx(r.txid, r.name, r.category)])); return fakeClient({ history, txs, utxos: utxosByAddress }); } test("declared and held reconciles cleanly", async () => { const client = portfolioClient({ regs: [{ txid: TX_A, name: "alice", category: CAT_A }], utxosByAddress: { [OPERATOR]: [tokenUtxo(TX_A, CAT_A, "alice")] }, }); const report = await verifyPortfolio(client, { addresses: [OPERATOR], manifest: [{ name: "alice.bch", acquiredAt: "2026-07-01", askingSats: 5_000_000n }], }); assert.equal(report.complete, true); assert.equal(report.accurate, true); assert.equal(report.entries[0].status, PORTFOLIO_STATUS.ok); assert.equal(report.entries[0].forSale, true); }); test("a name held but NOT declared is reported — the disqualifying case", async () => { const client = portfolioClient({ regs: [ { txid: TX_A, name: "alice", category: CAT_A }, { txid: TX_B, name: "bob", category: CAT_B }, ], utxosByAddress: { [OPERATOR]: [tokenUtxo(TX_A, CAT_A, "alice"), tokenUtxo(TX_B, CAT_B, "bob")], }, }); const report = await verifyPortfolio(client, { addresses: [OPERATOR], manifest: [{ name: "alice.bch" }], // bob quietly omitted }); assert.equal(report.complete, false, "an undeclared holding must not pass"); assert.equal(report.undisclosed.length, 1); assert.equal(report.undisclosed[0].name, "bob.bch"); assert.equal(report.undisclosed[0].status, PORTFOLIO_STATUS.undisclosed); }); test("publishing is refused while a holding is undeclared", async () => { const client = portfolioClient({ regs: [ { txid: TX_A, name: "alice", category: CAT_A }, { txid: TX_B, name: "bob", category: CAT_B }, ], utxosByAddress: { [OPERATOR]: [tokenUtxo(TX_A, CAT_A, "alice"), tokenUtxo(TX_B, CAT_B, "bob")], }, }); const report = await verifyPortfolio(client, { addresses: [OPERATOR], manifest: [{ name: "alice.bch" }] }); assert.throws( () => renderPortfolio(report), (e) => e.code === "UNDISCLOSED_HOLDINGS" && /bob\.bch/.test(e.message), "a page that silently omits a holding is worse than no page", ); }); test("declared but not held is flagged separately from undisclosed", async () => { const client = portfolioClient({ regs: [{ txid: TX_A, name: "alice", category: CAT_A }], utxosByAddress: { [OPERATOR]: [] }, // sold, or never held }); const report = await verifyPortfolio(client, { addresses: [OPERATOR], manifest: [{ name: "alice.bch" }] }); assert.equal(report.accurate, false, "a stale manifest entry is not accurate"); assert.equal(report.complete, true, "but nothing is hidden, which is the stronger property"); assert.equal(report.entries[0].status, PORTFOLIO_STATUS.missing); // Stale is embarrassing, not disqualifying — publishing is still allowed. assert.doesNotThrow(() => renderPortfolio(report)); }); test("someone else's names are not counted as ours", async () => { const client = portfolioClient({ regs: [{ txid: TX_B, name: "bob", category: CAT_B }], utxosByAddress: { [OTHER]: [tokenUtxo(TX_B, CAT_B, "bob")] }, }); const held = await heldNames(client, [OPERATOR]); assert.deepEqual(held, [], "only the addresses we were asked about count"); }); test("the published form carries the txid so a reader can check it themselves", async () => { const client = portfolioClient({ regs: [{ txid: TX_A, name: "alice", category: CAT_A }], utxosByAddress: { [OPERATOR]: [tokenUtxo(TX_A, CAT_A, "alice")] }, }); const report = await verifyPortfolio(client, { addresses: [OPERATOR], manifest: [{ name: "alice.bch", acquiredAt: "2026-07-01", paidSats: 13_000n }], }); const page = renderPortfolio(report, { network: "chipnet" }); assert.equal(page.complete, true); assert.equal(page.names[0].registeredTxid, TX_A, "verifiable, not merely asserted"); assert.equal(page.names[0].paidSats, "13000"); assert.equal(page.network, "chipnet"); });