// The dashboard's covenant actions, end to end against a simulated chain. // (src/lib/covenant-registrar.js) // // A tiny electrum stand-in keeps a UTXO set and a beacon history. Every // transaction the library broadcasts is checked by the VM against the coins // it spends, applied to the UTXO set, and then read back through the // resolver — so each step proves both that the chain would accept it and // that resolvers see what the owner meant. import test from "node:test"; import assert from "node:assert/strict"; import * as L from "@bitauth/libauth"; import { NETWORKS, buildIndexFromSnapshot, addressToScripthash, addressToLockHex } from "../src/lib/resolver-web.js"; import { BuiltInWallet } from "../src/lib/wallet-web.js"; import { buildRegistrationTx } from "../src/lib/register-tx.js"; import { signTransaction } from "../src/lib/sign-tx.js"; import { addressToLockingBytecode, getUtxosForAddresses } from "../src/lib/chain.js"; import { lockToAddress } from "../src/lib/covenant.js"; import { BLOCKS_PER_YEAR, GRACE_BLOCKS } from "../src/lib/registry-lifecycle.js"; import { covenantQuote, covenantLifecycle, findCovenantUtxo, enterCovenantWithBuiltInWallet, renewCovenantNameWithBuiltInWallet, setCovenantRecordsWithBuiltInWallet, transferCovenantNameWithBuiltInWallet, releaseCovenantNameWithBuiltInWallet, } from "../src/lib/covenant-registrar.js"; const { hexToBin, binToHex, decodeTransaction, hash256, sha256 } = L; const NET = NETWORKS.chipnet; const VM = L.createVirtualMachineBch2025(); // ---------- a chain small enough to read ---------- function chain(startHeight = 5000) { const utxos = new Map(); // "txid:vout" -> { lockHex, satoshis, token } const txs = {}; // txid -> verbose const history = []; // beacon history const beaconHex = addressToLockHex(NET.beacon); let height = startHeight; const scripthashOf = (lockHex) => binToHex(sha256.hash(hexToBin(lockHex)).slice().reverse()); const fund = (address, satoshis, vout = 0) => { const txid = binToHex(crypto.getRandomValues(new Uint8Array(32))); utxos.set(`${txid}:${vout}`, { lockHex: binToHex(addressToLockingBytecode(address)), satoshis: BigInt(satoshis), token: null }); return txid; }; // Check with the VM, spend, create, and record if it pays the beacon. function accept(hex) { const tx = decodeTransaction(hexToBin(hex)); assert.notEqual(typeof tx, "string", "the transaction must decode"); const sourceOutputs = tx.inputs.map((i) => { const u = utxos.get(`${binToHex(i.outpointTransactionHash)}:${i.outpointIndex}`); assert.ok(u, "spends a coin that exists"); return { lockingBytecode: hexToBin(u.lockHex), valueSatoshis: u.satoshis, ...(u.token ? { token: u.token } : {}) }; }); const verdict = VM.verify({ transaction: tx, sourceOutputs }); assert.equal(verdict, true, `the VM must accept it: ${verdict}`); const txid = binToHex(hash256(hexToBin(hex)).slice().reverse()); for (const i of tx.inputs) utxos.delete(`${binToHex(i.outpointTransactionHash)}:${i.outpointIndex}`); tx.outputs.forEach((o, n) => { if (o.lockingBytecode[0] !== 0x6a) utxos.set(`${txid}:${n}`, { lockHex: binToHex(o.lockingBytecode), satoshis: o.valueSatoshis, token: o.token ?? null }); }); const verbose = { txid, vin: tx.inputs.map((i) => ({ txid: binToHex(i.outpointTransactionHash), vout: i.outpointIndex })), vout: tx.outputs.map((o, n) => { let address; try { address = lockToAddress(o.lockingBytecode, NET.prefix, false); } catch {} return { n, value: Number(o.valueSatoshis) / 1e8, scriptPubKey: { hex: binToHex(o.lockingBytecode), ...(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) } } : {}) } } : {}) }; }), }; txs[txid] = verbose; height += 1; // every transaction gets its own block here if (tx.outputs.some((o) => binToHex(o.lockingBytecode) === beaconHex)) history.push({ tx_hash: txid, height }); return txid; } const client = { call: async (method, params) => { if (method === "blockchain.headers.subscribe") return { height }; if (method === "blockchain.transaction.broadcast") return accept(params[0]); if (method === "blockchain.scripthash.listunspent") { return [...utxos].filter(([, u]) => scripthashOf(u.lockHex) === params[0]).map(([k, u]) => ({ tx_hash: k.split(":")[0], tx_pos: Number(k.split(":")[1]), value: Number(u.satoshis), height: 1, ...(u.token ? { token_data: { category: binToHex(u.token.category), amount: String(u.token.amount ?? 0n), ...(u.token.nft ? { nft: { capability: u.token.nft.capability, commitment: binToHex(u.token.nft.commitment) } } : {}) } } : {}), })); } throw new Error("unexpected call " + method); }, }; return { client, fund, accept, get height() { return height; }, mine: (n) => { height += n; }, index: () => buildIndexFromSnapshot({ snapshot: { history: history.map((h) => ({ ...h })), txs }, height }), balance: (address) => [...utxos.values()].filter((u) => u.lockHex === binToHex(addressToLockingBytecode(address)) && !u.token).reduce((a, u) => a + u.satoshis, 0n), }; } const WORDS = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; const OTHER = "legal winner thank year wave sausage worth useful legal winner thank yellow"; const NAME = "tenant.x"; async function registered() { const c = chain(); const owner = await BuiltInWallet.fromMnemonic(WORDS, NET.prefix); const buyer = await BuiltInWallet.fromMnemonic(OTHER, NET.prefix); c.fund(owner.address, 50_000_000n); c.fund(owner.address, 50_000_000n); c.fund(buyer.address, 50_000_000n); const built = buildRegistrationTx({ name: NAME, ownerAddress: owner.address, records: { u: "https://first.example" }, utxos: await getUtxosForAddresses(c.client, owner.watchedAddresses), addressToLockingBytecode }); c.accept(signTransaction(built.transaction, built.sourceOutputs, (h) => owner.keyFor(h)).hex); return { c, owner, buyer }; } const entryOf = (c) => c.index().get(NAME); test("the whole life of a name in covenant custody, through the dashboard's functions", async () => { const { c, owner, buyer } = await registered(); let e = entryOf(c); assert.equal(e.owner, owner.address); assert.equal(e.covenant, undefined); const quote = covenantQuote(NAME); assert.deepEqual([quote.tier, quote.yearSats], [3, 200000n]); // --- enter --- const steps = []; const before = c.balance(NET.operatorAddress); const entered = await enterCovenantWithBuiltInWallet(c.client, { wallet: owner, entry: e, years: 1, onProgress: (s) => steps.push(s) }); assert.deepEqual(steps, ["loading-coins", "building", "broadcasting", "entered"]); assert.equal(c.balance(NET.operatorAddress) - before, quote.yearSats, "one year paid to the fund"); e = entryOf(c); assert.equal(e.owner, owner.address, "still listed under the owner's key, though the certificate left the wallet"); assert.equal(e.covenant.expiry, entered.state.expiry); assert.equal(e.status, "active"); assert.deepEqual(e.records, { u: "https://first.example" }, "the records came along"); assert.deepEqual(covenantLifecycle(e, c.height), { status: "active", expiry: e.covenant.expiry, idleEnd: e.covenant.expiry + GRACE_BLOCKS, blocksLeft: e.covenant.expiry - c.height }); await assert.rejects(() => enterCovenantWithBuiltInWallet(c.client, { wallet: owner, entry: e }), /already held by the covenant/); // --- records --- await setCovenantRecordsWithBuiltInWallet(c.client, { wallet: owner, entry: e, records: { s3: "bns/tenant.x/" } }); e = entryOf(c); assert.deepEqual(e.records, { s3: "bns/tenant.x/" }); assert.equal(e.owner, owner.address); // --- renew, two years --- const exp0 = e.covenant.expiry; await renewCovenantNameWithBuiltInWallet(c.client, { wallet: owner, entry: e, years: 2 }); e = entryOf(c); assert.equal(e.covenant.expiry, exp0 + 2 * BLOCKS_PER_YEAR); assert.deepEqual(e.records, { s3: "bns/tenant.x/" }, "a renewal keeps the records"); // --- a stale view of the name is refused, not guessed at --- const stale = { ...e, covenant: { ...e.covenant, expiry: exp0 } }; await assert.rejects(() => setCovenantRecordsWithBuiltInWallet(c.client, { wallet: owner, entry: stale, records: {} }), /not at the covenant address the registry last saw/); // --- transfer --- await assert.rejects(() => transferCovenantNameWithBuiltInWallet(c.client, { wallet: buyer, entry: e, toAddress: buyer.address }), /does not hold the owner key/); const scriptAddress = lockToAddress((await findCovenantUtxo(c.client, e)).cov.lockingBytecode, NET.prefix); await assert.rejects(() => transferCovenantNameWithBuiltInWallet(c.client, { wallet: owner, entry: e, toAddress: scriptAddress }), /ordinary address/); await transferCovenantNameWithBuiltInWallet(c.client, { wallet: owner, entry: e, toAddress: buyer.address }); e = entryOf(c); assert.equal(e.owner, buyer.address); assert.equal(e.covenant.expiry, exp0 + 2 * BLOCKS_PER_YEAR, "the expiry travels with the name"); await assert.rejects(() => setCovenantRecordsWithBuiltInWallet(c.client, { wallet: owner, entry: e, records: {} }), /does not hold the owner key/); // --- the new owner lets it expire, then reclaims it in the grace period --- c.mine(e.covenant.expiry - c.height + 10); e = entryOf(c); assert.equal(e.status, "idle"); await renewCovenantNameWithBuiltInWallet(c.client, { wallet: buyer, entry: e, years: 1 }); e = entryOf(c); assert.equal(e.status, "active"); // --- past the grace period it resolves to nothing; a renewal still brings it back, with its records --- c.mine(e.covenant.expiry + GRACE_BLOCKS - c.height + 1); e = entryOf(c); assert.equal(e.status, "open"); assert.deepEqual(e.records, {}); await renewCovenantNameWithBuiltInWallet(c.client, { wallet: buyer, entry: e, years: 2 }); e = entryOf(c); assert.equal(e.status, "active"); assert.deepEqual(e.records, { s3: "bns/tenant.x/" }, "the lapsed records are republished by the renewal"); // --- release --- await releaseCovenantNameWithBuiltInWallet(c.client, { wallet: buyer, entry: e }); assert.equal(c.index().has(NAME), false); assert.ok(c.index().released.has(NAME)); }); test("what the gateway's holdings match on: the owner key's scripthash", async () => { const { c, owner } = await registered(); await enterCovenantWithBuiltInWallet(c.client, { wallet: owner, entry: entryOf(c) }); const e = entryOf(c); assert.ok(owner.watchedAddresses.map((a) => addressToScripthash(a)).includes(addressToScripthash(e.owner))); });