// Resolvers reading covenant-held names. // (Decentralized.DNS/PROTOCOL-ADDENDUM-covenant-names.md) // // A covenant-held name's owner and expiry are hashed into its address. The // transaction says what they are; the resolver checks the claim against the // address, decides whether an entry was paid for, and stops resolving a name // whose grace period has passed. // // Every transaction here is built by the real builders and, where it spends // the covenant, accepted by the VM first — then turned into the verbose form // an electrum server returns and fed to all three JavaScript reducers, which // must agree. import test from "node:test"; import { writeFileSync } from "node:fs"; import assert from "node:assert/strict"; import * as L from "@bitauth/libauth"; import { NETWORKS, buildIndex, buildIndexFromSnapshot, covenantLockHex, covenantTierIndex, covenantClaim, covenantPhase, pkhToAddress, addressToLockHex, historyHeight, } from "../src/lib/resolver-web.js"; import * as legacy from "../src/lib/bns.js"; import { covenantTable } from "../src/generate-covenant-table.mjs"; import { registryTerms, registryLock, commitBoxLock, commitHashFor, covenantClaimOf, registryAnnouncement, buildRegistryEnterTx, buildRegistryRenewTx, buildRegistryTransferTx, buildRegistryTakeoverTx, buildRegistryReleaseTx, } from "../src/lib/registry-covenant.js"; import { TIERS, tierForName, BLOCKS_PER_YEAR, GRACE_BLOCKS } from "../src/lib/registry-lifecycle.js"; import { lockToAddress } from "../src/lib/covenant.js"; import { encodePayload, opReturnScript } from "../src/lib/register-tx.js"; const { hexToBin, binToHex, hash160, secp256k1, encodeLockingBytecodeP2pkh, generatePrivateKey } = L; const NET = NETWORKS.chipnet; const COV = NET.covenant; const VM = L.createVirtualMachineBch2025(); // ---------- cast ---------- const person = () => { const priv = generatePrivateKey(() => crypto.getRandomValues(new Uint8Array(32))); const pub = secp256k1.derivePublicKeyCompressed(priv); const pkh = hash160(pub); const lock = encodeLockingBytecodeP2pkh(pkh); return { priv, pub, pkh: binToHex(pkh), lock, lockHex: binToHex(lock), address: lockToAddress(lock, NET.prefix, false) }; }; const owner = person(), buyer = person(), taker = person(), stranger = person(); const keyring = (...people) => (lockHex) => { const p = people.find((x) => x.lockHex === lockHex); return p ? { privateKey: p.priv, publicKey: p.pub } : null; }; const KEYS = keyring(owner, buyer, taker, stranger); const fundLock = hexToBin(COV.fundLock); const beaconLock = hexToBin(addressToLockHex(NET.beacon)); const termsFor = (name) => registryTerms({ tier: tierForName(name), fundLock, stewardPk: hexToBin(COV.steward) }); let coinSeq = 100; const coin = (who, sats = 50_000_000n) => ({ txid: (coinSeq++).toString(16).padStart(64, "0"), vout: 0, satoshis: sats, lockingBytecode: who.lock }); // ---------- libauth transaction -> what an electrum server returns ---------- const verbose = (built) => ({ txid: built.txid, vin: built.transaction.inputs.map((i) => ({ txid: binToHex(i.outpointTransactionHash), vout: i.outpointIndex })), vout: built.transaction.outputs.map((o, n) => { const hex = binToHex(o.lockingBytecode); let address; try { address = lockToAddress(o.lockingBytecode, NET.prefix, false); } catch { address = undefined; } return { n, value: Number(o.valueSatoshis) / 1e8, scriptPubKey: { hex, ...(address ? { address } : {}) }, ...(o.token ? { tokenData: { category: binToHex(o.token.category), amount: String(o.token.amount ?? 0n), ...(o.token.nft ? { nft: { capability: o.token.nft.capability, commitment: binToHex(o.token.nft.commitment) } } : {}) } } : {}), }; }), }); const vmAccepts = (built, what) => assert.equal(VM.verify({ transaction: built.transaction, sourceOutputs: built.sourceOutputs }), true, `${what}: the VM must accept it`); const nextCov = (built, token) => ({ txid: built.txid, vout: 0, satoshis: built.transaction.outputs[0].valueSatoshis, lockingBytecode: built.transaction.outputs[0].lockingBytecode, token }); // A plain registration, hand-made (no contract involved). let catSeq = 1; function registration(name, who, records = { u: "https://first.example" }) { const category = (catSeq++).toString(16).padStart(2, "0").repeat(32); const token = { category, amount: 0n, nft: { capability: "none", commitment: binToHex(new TextEncoder().encode(name)) } }; const txid = "f" + (catSeq).toString(16).padStart(63, "0"); const tx = { txid, vin: [{ txid: category, vout: 0 }], vout: [ { n: 0, value: 0.00001, scriptPubKey: { hex: who.lockHex, address: who.address }, tokenData: { category, amount: "0", nft: { capability: "none", commitment: token.nft.commitment } } }, { n: 1, value: 0, scriptPubKey: { hex: binToHex(opReturnScript(encodePayload("REG", name, records))) } }, { n: 2, value: 0.000006, scriptPubKey: { hex: binToHex(beaconLock), address: NET.beacon } }, ], }; return { tx, cert: { txid, vout: 0, satoshis: 1000n, lockingBytecode: who.lock, token } }; } const announce = (name, records) => ({ name, records, beaconLock }); // BNS_DUMP=: write every reduction as a vector, so the Android reducer // can be run against the same histories on the desktop (see the memory note). const DUMPED = []; const dump = (history, txs, height, idx) => { if (!process.env.BNS_DUMP) return; DUMPED.push({ history, txs, height, expect: [...idx].map(([k, e]) => ({ name: k, category: e.category, txid: e.txid, records: e.records, ownerPkh: e.covenant?.ownerPkh ?? null, expiry: e.covenant?.expiry ?? 0, status: e.status ?? null })) }); }; process.on("exit", () => { if (process.env.BNS_DUMP && DUMPED.length) writeFileSync(process.env.BNS_DUMP, JSON.stringify({ vectors: DUMPED })); }); // ---------- the three reducers, which must agree ---------- async function reduce(entries, height) { const history = entries.map(([tx, h]) => ({ tx_hash: tx.txid, height: h })); const txs = Object.fromEntries(entries.map(([tx]) => [tx.txid, tx])); const snap = buildIndexFromSnapshot({ snapshot: { history, txs }, height }); const el = { call: async (method, [a]) => method === "blockchain.scripthash.get_history" ? history.map((x) => ({ ...x })) : method === "blockchain.headers.subscribe" ? { height } : txs[a], }; const live = await buildIndex({ client: el, cosignGate: false }); const old = await legacy.buildIndex({ getHistory: async () => history.map((x) => ({ ...x })), getRawTransactionObject: async (id) => txs[id] }, { cosignGate: false, height }); const shape = (idx) => [...idx].map(([k, e]) => JSON.stringify([k, e.records, e.covenant ?? null, e.status ?? null, e.expiry ?? null, e.lapsedRecords ?? null])).sort(); assert.deepEqual(shape(live), shape(snap), "buildIndex and buildIndexFromSnapshot disagree"); assert.deepEqual(shape(old), shape(snap), "bns.js and resolver-web.js disagree"); assert.equal(snap.height, height); dump(history, txs, height, snap); return snap; } // ---------- the table ---------- test("the network table holds the contract's code, byte for byte", () => { assert.deepEqual(covenantTable({ fundLock, stewardPk: hexToBin(COV.steward) }), COV, "regenerate: node src/generate-covenant-table.mjs"); for (const t of TIERS) { const terms = registryTerms({ tier: t.tier, fundLock, stewardPk: hexToBin(COV.steward) }); const state = { ownerPkh: owner.pkh, expiry: 123456 + t.tier }; assert.equal(covenantLockHex(COV, t.tier - 1, state.ownerPkh, state.expiry), binToHex(registryLock(terms, state)), `tier ${t.tier}`); } for (const name of ["a.x", "ab.x", "abc.x", "abcd.x", "abcde.x", "abcdefg.x", "abcdefgh.x", "a-much-longer-label.test", "bare"]) { assert.equal(covenantTierIndex(name, COV) + 1, tierForName(name), name); } assert.equal(NETWORKS.mainnet.covenant, null, "mainnet is not pinned"); }); test("pkhToAddress is the inverse of addressToLockHex", () => { for (const p of [owner, buyer, taker]) { assert.equal(pkhToAddress(p.pkh, NET.prefix), p.address); assert.equal(addressToLockHex(pkhToAddress(p.pkh, NET.prefix)), p.lockHex); } assert.equal(pkhToAddress(owner.pkh, "bitcoincash"), lockToAddress(owner.lock, "bitcoincash", false)); }); // ---------- entering ---------- const NAME = "holdme.x"; const H_REG = 1000, TIP = 1999, H_ENTER = 2000; function entered({ name = NAME, who = owner, years = 1, height = TIP, records = { u: "https://held.example" } } = {}) { const reg = registration(name, who); const terms = termsFor(name); const enter = buildRegistryEnterTx({ cert: reg.cert, terms, name, records, ownerPkh: who.pkh, height, years, beaconLock, coins: [coin(who)], changeLock: who.lock, keyFor: KEYS }); return { reg, terms, enter, cov: nextCov(enter, reg.cert.token), state: enter.state }; } test("a paid entry makes the name covenant-held: owner and expiry come from the verified claim", async () => { const { reg, enter, state } = entered(); const idx = await reduce([[reg.tx, H_REG], [verbose(enter), H_ENTER]], H_ENTER); const e = idx.get(NAME); assert.deepEqual(e.covenant, { ownerPkh: owner.pkh, expiry: TIP + BLOCKS_PER_YEAR, tier: 3, version: 1 }); assert.equal(e.owner, owner.address, "the owner is the key in the state, not the covenant's address"); assert.deepEqual(e.records, { u: "https://held.example" }); assert.equal(e.status, "active"); assert.equal(e.expiry, state.expiry); assert.equal(e.idleEnd, state.expiry + GRACE_BLOCKS); }); test("the lifecycle: resolves while active and idle, not once open — and stays taken", async () => { const { reg, enter, state } = entered(); const at = async (h) => (await reduce([[reg.tx, H_REG], [verbose(enter), H_ENTER]], h)).get(NAME); assert.equal((await at(state.expiry - 1)).status, "active"); const idle = await at(state.expiry); assert.equal(idle.status, "idle"); assert.deepEqual(idle.records, { u: "https://held.example" }, "the owner keeps the records through the grace period"); assert.equal((await at(state.expiry + GRACE_BLOCKS - 1)).status, "idle"); const open = await at(state.expiry + GRACE_BLOCKS); assert.equal(open.status, "open"); assert.deepEqual(open.records, {}, "a lapsed name resolves to nothing"); assert.deepEqual(open.lapsedRecords, { u: "https://held.example" }); // Its certificate still exists: a fresh registration of the same name is refused. const again = registration(NAME, stranger, { u: "https://squatter.example" }); const idx = await reduce([[reg.tx, H_REG], [verbose(enter), H_ENTER], [again.tx, state.expiry + GRACE_BLOCKS + 5]], state.expiry + GRACE_BLOCKS + 10); assert.equal(idx.get(NAME).category, reg.cert.token.category); assert.deepEqual(idx.get(NAME).records, {}); assert.deepEqual(covenantPhase(open.covenant, state.expiry + GRACE_BLOCKS), { status: "open", expiry: state.expiry, idleEnd: state.expiry + GRACE_BLOCKS }); assert.equal(covenantPhase(null, 5), null); }); test("an entry that was not paid for leaves an ordinary name", async () => { const plain = async (enter, height, why) => { const idx = await reduce([[enter.reg.tx, H_REG], [enter.tx, height]], Math.max(height, H_ENTER)); const e = idx.get(enter.name ?? NAME); assert.equal(e.covenant, undefined, why); assert.equal(e.status, undefined, why); assert.deepEqual(e.records, { u: "https://held.example" }, `${why}: it is still a valid update of an ordinary name`); return e; }; const base = entered(); const fundIndex = verbose(base.enter).vout.findIndex((v) => v.scriptPubKey.hex === COV.fundLock && !v.tokenData); assert.equal(fundIndex, 1); // one satoshi short const short = verbose(base.enter); short.vout[1].value = (COV.tiers[2].renewalSats - 1) / 1e8; await plain({ reg: base.reg, tx: short }, H_ENTER, "one satoshi short of a year"); // paid to somebody else const elsewhere = verbose(base.enter); elsewhere.vout[1].scriptPubKey = { hex: stranger.lockHex, address: stranger.address }; await plain({ reg: base.reg, tx: elsewhere }, H_ENTER, "paid to another address"); // a token output to the fund is not a payment const tokenPay = verbose(base.enter); tokenPay.vout[1].tokenData = { category: "ee".repeat(32), amount: "5" }; await plain({ reg: base.reg, tx: tokenPay }, H_ENTER, "the fund output carries a token"); // one year paid, two claimed const far = entered({ height: TIP + BLOCKS_PER_YEAR }); await plain({ reg: far.reg, tx: verbose(far.enter) }, H_ENTER, "expiry further out than was paid for"); // not in a block yet const e = await plain({ reg: base.reg, tx: verbose(base.enter) }, 0, "unconfirmed"); assert.notEqual(e.owner, owner.address, "until it confirms, the holder is just the address the certificate sits at"); // two years paid, two claimed: fine const two = entered({ years: 2 }); const idx = await reduce([[two.reg.tx, H_REG], [verbose(two.enter), H_ENTER]], H_ENTER); assert.equal(idx.get(NAME).covenant.expiry, TIP + 2 * BLOCKS_PER_YEAR); }); test("a claim that does not hash to the address is not a claim", async () => { const base = entered(); const holder = verbose(base.enter).vout[0]; const good = covenantClaimOf(base.state); assert.ok(covenantClaim({ c: good }, holder, NAME)); const bad = [ { ...good, o: buyer.pkh }, { ...good, e: good.e + 1 }, { ...good, e: String(good.e) }, { ...good, o: good.o.toUpperCase() }, { o: good.o }, { e: good.e }, null, "x", { ...good, e: 500000000 }, { ...good, e: 0 }, { ...good, e: good.e + 0.5 }, ]; for (const c of bad) assert.equal(covenantClaim({ c }, holder, NAME), null, JSON.stringify(c)); // the right state under another tier's prices is another address assert.equal(covenantClaim({ c: good }, holder, "ab.x"), null); assert.equal(covenantClaim({ c: good }, holder, "a-long-label.x"), null); // a network with no covenant pinned knows no covenant-held names assert.equal(covenantClaim({ c: good }, holder, NAME, NETWORKS.mainnet), null); // In the index: a short name entered at the cheap tier's address stays ordinary. const reg = registration("ab.x", owner); const cheap = registryTerms({ tier: 4, fundLock, stewardPk: hexToBin(COV.steward) }); const enter = buildRegistryEnterTx({ cert: reg.cert, terms: cheap, name: "ab.x", records: { u: "https://held.example" }, ownerPkh: owner.pkh, height: TIP, beaconLock, coins: [coin(owner)], changeLock: owner.lock, keyFor: KEYS }); const idx = await reduce([[reg.tx, H_REG], [verbose(enter), H_ENTER]], H_ENTER); assert.equal(idx.get("ab.x").covenant, undefined); }); // ---------- inside ---------- test("inside the covenant: updates, transfers and renewals are read from announced states", async () => { const { reg, terms, enter, cov, state } = entered(); const chain = [[reg.tx, H_REG], [verbose(enter), H_ENTER]]; // records update: a transfer to the same key const upd = buildRegistryTransferTx({ cov, terms, state, newOwnerPkh: owner.pkh, announce: announce(NAME, { u: "https://second.example" }), coins: [coin(owner)], changeLock: owner.lock, keyFor: KEYS }); vmAccepts(upd, "records update"); chain.push([verbose(upd), 2100]); let e = (await reduce(chain, 2100)).get(NAME); assert.deepEqual(e.records, { u: "https://second.example" }); assert.equal(e.owner, owner.address); // transfer to the buyer const xfer = buildRegistryTransferTx({ cov: nextCov(upd, cov.token), terms, state: upd.state, newOwnerPkh: buyer.pkh, announce: announce(NAME, { u: "https://buyer.example" }), coins: [coin(owner)], changeLock: owner.lock, keyFor: KEYS }); vmAccepts(xfer, "transfer"); chain.push([verbose(xfer), 2200]); e = (await reduce(chain, 2200)).get(NAME); assert.equal(e.owner, buyer.address); assert.equal(e.covenant.expiry, state.expiry, "a transfer does not move the expiry"); // renewal by the buyer const renew = buildRegistryRenewTx({ cov: nextCov(xfer, cov.token), terms, state: xfer.state, years: 2, announce: announce(NAME, { u: "https://buyer.example" }), coins: [coin(buyer)], changeLock: buyer.lock, keyFor: KEYS }); vmAccepts(renew, "renew"); chain.push([verbose(renew), 2300]); e = (await reduce(chain, state.expiry + GRACE_BLOCKS + 1)).get(NAME); assert.equal(e.covenant.expiry, state.expiry + 2 * BLOCKS_PER_YEAR); assert.equal(e.status, "active", "the renewal was announced, so the name did not lapse"); // an unannounced renewal is valid on chain and invisible here: the name lapses in the index const quiet = (await reduce(chain.slice(0, -1), state.expiry + GRACE_BLOCKS + 1)).get(NAME); assert.equal(quiet.status, "open"); assert.deepEqual(quiet.records, {}); }); test("once inside, a transaction without a verified state changes nothing", async () => { const { reg, terms, enter, cov, state } = entered(); const base = [[reg.tx, H_REG], [verbose(enter), H_ENTER]]; const real = buildRegistryTransferTx({ cov, terms, state, newOwnerPkh: owner.pkh, announce: announce(NAME, { u: "https://hijack.example" }), coins: [coin(owner)], changeLock: owner.lock, keyFor: KEYS }); const payloadAt = verbose(real).vout.findIndex((v) => v.scriptPubKey.hex.startsWith("6a")); const withPayload = (payload) => { const t = verbose(real); t.vout[payloadAt].scriptPubKey.hex = binToHex(opReturnScript(payload)); return t; }; const cases = [ ["no claim at all — the way back to a name that never expires", encodePayload("UPD", NAME, { u: "https://hijack.example" })], ["a claim for another owner", encodePayload("UPD", NAME, { u: "https://hijack.example" }, { o: stranger.pkh, e: state.expiry })], ["a claim for a later expiry", encodePayload("UPD", NAME, { u: "https://hijack.example" }, { o: owner.pkh, e: state.expiry + BLOCKS_PER_YEAR })], ]; for (const [what, payload] of cases) { const e = (await reduce([...base, [withPayload(payload), 2100]], 2100)).get(NAME); assert.deepEqual(e.records, { u: "https://held.example" }, what); assert.equal(e.covenant.expiry, state.expiry, what); assert.equal(e.owner, owner.address, what); } // and the honest announcement of the same transaction is accepted assert.deepEqual((await reduce([...base, [verbose(real), 2100]], 2100)).get(NAME).records, { u: "https://hijack.example" }); }); // ---------- leaving ---------- test("a takeover brings a lapsed name back, for its new owner", async () => { const { reg, terms, enter, cov, state } = entered(); const openAt = state.expiry + GRACE_BLOCKS + 100; const salt = crypto.getRandomValues(new Uint8Array(32)); const box = { txid: "b0".repeat(32), vout: 0, satoshis: 1000n, lockingBytecode: commitBoxLock({ ownerPkh: taker.pkh, commitHash: commitHashFor(NAME, salt) }) }; const take = buildRegistryTakeoverTx({ cov, terms, state, name: NAME, height: openAt, newOwnerPkh: taker.pkh, salt, box, announce: announce(NAME, { u: "https://taker.example" }), coins: [coin(taker)], changeLock: taker.lock, keyFor: KEYS }); vmAccepts(take, "takeover"); const before = (await reduce([[reg.tx, H_REG], [verbose(enter), H_ENTER]], openAt)).get(NAME); assert.deepEqual(before.records, {}); const after = (await reduce([[reg.tx, H_REG], [verbose(enter), H_ENTER], [verbose(take), openAt + 1]], openAt + 1)).get(NAME); assert.equal(after.owner, taker.address); assert.deepEqual(after.records, { u: "https://taker.example" }); assert.equal(after.status, "active"); assert.equal(after.covenant.expiry, openAt + BLOCKS_PER_YEAR); assert.equal(after.category, reg.cert.token.category, "the same certificate: a takeover is not a new registration"); }); test("a release from the covenant frees the name like any other", async () => { const { reg, terms, enter, cov, state } = entered(); const rel = buildRegistryReleaseTx({ cov, terms, state, announce: announce(NAME), coins: [coin(owner)], changeLock: owner.lock, keyFor: KEYS }); vmAccepts(rel, "release"); const chain = [[reg.tx, H_REG], [verbose(enter), H_ENTER], [verbose(rel), 2100]]; const idx = await reduce(chain, 2100); assert.equal(idx.has(NAME), false); assert.equal(idx.released.get(NAME), 2100); const again = registration(NAME, stranger, { u: "https://next.example" }); assert.equal((await reduce([...chain, [again.tx, 2100]], 2101)).has(NAME), false, "not in the release's own block"); assert.deepEqual((await reduce([...chain, [again.tx, 2101]], 2101)).get(NAME).records, { u: "https://next.example" }); }); // ---------- registering straight in ---------- test("a registration can go straight into the covenant, on the same terms as an entry", async () => { const name = "straight.x"; const terms = termsFor(name); const state = { ownerPkh: owner.pkh, expiry: TIP + BLOCKS_PER_YEAR }; const make = (paid) => { const r = registration(name, owner, { u: "https://straight.example" }); r.tx.vout[0].scriptPubKey = { hex: binToHex(registryLock(terms, state)) }; r.tx.vout[1].scriptPubKey.hex = binToHex(registryAnnouncement({ name, records: { u: "https://straight.example" }, beaconLock }, state, "REG")[0].lockingBytecode); if (paid) r.tx.vout.push({ n: 3, value: COV.tiers[covenantTierIndex(name, COV)].renewalSats / 1e8, scriptPubKey: { hex: COV.fundLock } }); return r.tx; }; const held = (await reduce([[make(true), H_ENTER]], H_ENTER)).get(name); assert.equal(held.covenant.expiry, state.expiry); assert.equal(held.owner, owner.address); const unpaid = (await reduce([[make(false), H_ENTER]], H_ENTER)).get(name); assert.equal(unpaid.covenant, undefined, "registered, but an ordinary name"); assert.deepEqual(unpaid.records, { u: "https://straight.example" }); }); // ---------- which height ---------- test("expiry is judged at the tip when known, else at the newest block in the history — never ahead of the chain", async () => { const { reg, enter, state } = entered(); const history = [{ tx_hash: reg.tx.txid, height: H_REG }, { tx_hash: enter.txid, height: H_ENTER }]; const txs = { [reg.tx.txid]: reg.tx, [enter.txid]: verbose(enter) }; assert.equal(historyHeight(history), H_ENTER); assert.equal(historyHeight([{ height: 0 }, { height: -1 }]), 0); const noTip = buildIndexFromSnapshot({ snapshot: { history, txs } }); assert.equal(noTip.height, H_ENTER); assert.equal(noTip.get(NAME).status, "active"); const stamped = buildIndexFromSnapshot({ snapshot: { history, txs, height: state.expiry + GRACE_BLOCKS } }); assert.equal(stamped.get(NAME).status, "open"); // a server that will not say the tip: fall back to the history const el = { call: async (m, [a]) => { if (m === "blockchain.scripthash.get_history") return history.map((x) => ({ ...x })); if (m === "blockchain.headers.subscribe") throw new Error("unsupported"); return txs[a]; } }; const live = await buildIndex({ client: el, cosignGate: false }); assert.equal(live.height, H_ENTER); // an explicit height wins assert.equal((await buildIndex({ client: el, cosignGate: false, height: state.expiry })).get(NAME).status, "idle"); }); test("ordinary names are untouched: no status, no expiry, same entry as before", async () => { const reg = registration("plain.x", owner); const e = (await reduce([[reg.tx, H_REG]], 10_000_000)).get("plain.x"); assert.deepEqual(Object.keys(e).sort(), ["category", "height", "name", "owner", "records", "txid", "updatedTxid"]); });