// The name registry covenant, run in libauth's BCH virtual machine. // (contracts/name-registry.cash, contracts/commit-box.cash) // // No network, no coins. Every path is built with the library and executed by // the VM, then broken one rule at a time: each negative case re-signs the // tampered transaction, so it fails because the CONTRACT refuses it and not // because a signature went stale. // // The prices are checked against registry-lifecycle.js to the satoshi. That // file is the reference; if the two ever disagree the contract is wrong. // // This is a draft contract and these tests are not a review. They show the // paths behave as designed, not that no other path exists. import test from "node:test"; import assert from "node:assert/strict"; import * as L from "@bitauth/libauth"; import { registryTerms, registryRedeem, registryLock, registryCode, registryState, parseRegistryState, STATE_BYTES, commitBoxLock, commitBoxRedeem, commitHashFor, takeoverQuote, signMigrationApproval, migratedLock, registryPhase, COMMIT_SALT_BYTES, buildRegistryRenewTx, buildRegistryTransferTx, buildRegistryTakeoverTx, buildRegistryReleaseTx, buildRegistryMigrateTx, } from "../src/lib/registry-covenant.js"; import { BLOCKS_PER_YEAR, GRACE_BLOCKS, PHASE, takeoverPriceSats, splitTakeover, renewalPriceSats, phaseAt, } from "../src/lib/registry-lifecycle.js"; import { unlockScript, assemble, p2sh32Lock, selectorsOf, PUBKEY, SIG } from "../src/lib/covenant.js"; import { NAME_REGISTRY } from "../src/lib/name-registry-artifact.js"; const { hexToBin, binToHex, hash160, secp256k1, encodeLockingBytecodeP2pkh, generatePrivateKey, flattenBinArray } = L; // Run under the rules live today and under the next upgrade's. const VMS = [["BCH 2025", L.createVirtualMachineBch2025()], ["BCH 2026", L.createVirtualMachineBch2026()]]; const verdict = (built) => VMS.map(([label, vm]) => [label, vm.verify({ transaction: built.transaction, sourceOutputs: built.sourceOutputs })]); const accepts = (built, what) => { for (const [label, r] of verdict(built)) assert.equal(r, true, `${what} — ${label}: ${r}`); }; // A negative case must be refused by the VIRTUAL MACHINE. If the library // throws before a transaction exists, the contract was never asked, and the // case proves nothing about it — so those are counted, and the last test // pins exactly which cases are allowed to end that way. const refusedByLibrary = []; let refusedByVm = 0; const rejects = (make, what) => { let built; try { built = make(); } catch (e) { refusedByLibrary.push(what); return; } for (const [label, r] of verdict(built)) assert.notEqual(r, true, `${what} — ${label} accepted it`); refusedByVm++; }; // ---------- 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) }; }; const owner = person(), taker = person(), buyer = person(), steward = person(), fund = 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 fakeTxid = (n) => n.toString(16).padStart(64, "0"); const coin = (id, who, sats = 500_000_000n) => ({ txid: fakeTxid(id), vout: 0, satoshis: sats, lockingBytecode: who.lock }); const TIER = 3; const terms = registryTerms({ tier: TIER, fundLock: fund.lock, stewardPk: steward.pub }); const NAME = "covenant.x"; const CATEGORY = "ab".repeat(32); const EXPIRY = 900_000 + BLOCKS_PER_YEAR; const IDLE_END = EXPIRY + GRACE_BLOCKS; const state0 = { ownerPkh: owner.pkh, expiry: EXPIRY }; const certToken = (name = NAME) => ({ category: CATEGORY, amount: 0n, nft: { capability: "none", commitment: binToHex(new TextEncoder().encode(name)) } }); const covUtxo = (state = state0, name = NAME) => ({ txid: fakeTxid(1), vout: 0, satoshis: 1000n, lockingBytecode: registryLock(terms, state), token: certToken(name) }); const SALT = hexToBin("00112233445566778899aabbccddeeff".repeat(2)); // 32 bytes: the contract requires exactly that const boxUtxo = (who = taker, name = NAME, salt = SALT) => ({ txid: fakeTxid(7), vout: 0, satoshis: 1000n, lockingBytecode: commitBoxLock({ ownerPkh: who.pkh, commitHash: commitHashFor(name, salt) }) }); const outLockHex = (built, i) => binToHex(built.transaction.outputs[i].lockingBytecode); const setOut = (i, patch) => (tx) => ({ ...tx, outputs: tx.outputs.map((o, k) => (k === i ? { ...o, ...patch(o) } : o)) }); // ---------- the address is the state ---------- test("state prefix: 26 bytes the contract can swap, and nothing else moves", () => { const redeem = registryRedeem(terms, state0); assert.equal(binToHex(redeem.slice(0, 1)), "14"); assert.equal(binToHex(redeem.slice(21, 22)), "04"); assert.deepEqual(parseRegistryState(redeem), state0); assert.equal(binToHex(redeem.slice(STATE_BYTES)), binToHex(registryCode(terms)), "CODE is identical for every name in the tier"); const other = registryRedeem(terms, { ownerPkh: taker.pkh, expiry: EXPIRY + 5 }); assert.equal(binToHex(other.slice(STATE_BYTES)), binToHex(redeem.slice(STATE_BYTES))); assert.notEqual(binToHex(registryLock(terms, state0)), binToHex(registryLock(terms, { ...state0, expiry: EXPIRY + 1 })), "a different expiry is a different address"); assert.throws(() => registryState({ ownerPkh: "00", expiry: 1 })); }); // ---------- renew ---------- const renew = (o = {}) => buildRegistryRenewTx({ cov: covUtxo(), terms, state: state0, years: 1, coins: [coin(2, owner)], changeLock: owner.lock, keyFor: keyring(owner), ...o }); test("renew: the owner extends the name and pays the fund the tier's price", () => { for (const years of [1, 3]) { const built = renew({ years }); accepts(built, `renew ${years}y`); assert.equal(outLockHex(built, 0), binToHex(registryLock(terms, { ownerPkh: owner.pkh, expiry: EXPIRY + years * BLOCKS_PER_YEAR }))); assert.equal(built.transaction.outputs[1].valueSatoshis, renewalPriceSats(TIER, years), "the price is the reference model's"); assert.equal(binToHex(built.transaction.outputs[1].lockingBytecode), fund.lockHex); assert.deepEqual(built.state, { ownerPkh: owner.pkh, expiry: EXPIRY + years * BLOCKS_PER_YEAR }); } }); test("renew: an expired name renews the same way (reclaim), from its old expiry", () => { // No height is involved at all: the owner can reclaim for as long as nobody has taken the name. accepts(renew({ years: 2 }), "reclaim"); }); test("renew is refused when a rule is broken", () => { rejects(() => buildRegistryRenewTx({ cov: covUtxo(), terms, state: state0, coins: [coin(2, stranger)], changeLock: stranger.lock, keyFor: (h) => keyring(stranger)(h) ?? { privateKey: stranger.priv, publicKey: stranger.pub } }), "a stranger's signature"); rejects(() => renew({ tamper: setOut(1, (o) => ({ valueSatoshis: o.valueSatoshis - 1n })) }), "one satoshi short"); rejects(() => renew({ tamper: setOut(1, () => ({ lockingBytecode: owner.lock })) }), "the payment goes to the owner instead of the fund"); rejects(() => renew({ years: 1, tamper: setOut(0, () => ({ lockingBytecode: registryLock(terms, { ownerPkh: owner.pkh, expiry: EXPIRY + 2 * BLOCKS_PER_YEAR }) })) }), "two years claimed, one paid"); rejects(() => renew({ tamper: setOut(0, () => ({ lockingBytecode: owner.lock })) }), "the certificate leaves the covenant"); rejects(() => renew({ tamper: setOut(0, (o) => ({ token: { ...o.token, nft: { ...o.token.nft, commitment: new TextEncoder().encode("other.x") } } })) }), "the certificate's name is changed"); rejects(() => renew({ tamper: setOut(0, () => ({ token: undefined })) }), "the certificate is dropped"); rejects(() => renew({ tamper: setOut(0, () => ({ valueSatoshis: 999n })) }), "the certificate's dust is skimmed"); rejects(() => renew({ years: 0 }), "zero years"); }); // ---------- transfer ---------- const transfer = (o = {}) => buildRegistryTransferTx({ cov: covUtxo(), terms, state: state0, newOwnerPkh: buyer.pkh, coins: [coin(2, owner)], changeLock: owner.lock, keyFor: keyring(owner), ...o }); test("transfer: the owner hands the name over; expiry is untouched", () => { const built = transfer(); accepts(built, "transfer"); assert.equal(outLockHex(built, 0), binToHex(registryLock(terms, { ownerPkh: buyer.pkh, expiry: EXPIRY }))); accepts(transfer({ newOwnerPkh: owner.pkh }), "transfer to self — the carrier for a records update"); }); test("transfer cannot be used to walk the certificate out, or by anyone else", () => { rejects(() => transfer({ tamper: setOut(0, () => ({ lockingBytecode: buyer.lock })) }), "certificate sent to a plain address"); rejects(() => transfer({ tamper: setOut(0, () => ({ lockingBytecode: registryLock(terms, { ownerPkh: buyer.pkh, expiry: EXPIRY + BLOCKS_PER_YEAR }) })) }), "a free year smuggled into a transfer"); rejects(() => buildRegistryTransferTx({ cov: covUtxo(), terms, state: state0, newOwnerPkh: stranger.pkh, coins: [coin(2, stranger)], changeLock: stranger.lock, keyFor: () => ({ privateKey: stranger.priv, publicKey: stranger.pub }) }), "a stranger signs"); }); // ---------- takeover ---------- const takeover = (height, o = {}) => buildRegistryTakeoverTx({ cov: covUtxo(), terms, state: state0, name: NAME, height, newOwnerPkh: taker.pkh, salt: SALT, box: boxUtxo(), coins: [coin(3, taker, 50_000_000_000n)], changeLock: taker.lock, keyFor: keyring(taker), ...o }); test("takeover price and split equal the reference model at every point of the glide", () => { const heights = [EXPIRY, EXPIRY + 1, EXPIRY + Math.floor(GRACE_BLOCKS / 3), EXPIRY + Math.floor(GRACE_BLOCKS / 2), IDLE_END - 1, IDLE_END, IDLE_END + 1, IDLE_END + 5 * BLOCKS_PER_YEAR]; for (const height of heights) { const model = takeoverPriceSats({ height, expiryHeight: EXPIRY, idleEndHeight: IDLE_END, tier: TIER }); const split = splitTakeover({ amountSats: model, phase: phaseAt({ height, expiryHeight: EXPIRY, idleEndHeight: IDLE_END }) }); const quote = takeoverQuote(terms, state0, height); assert.equal(quote.priceSats, model, `price at ${height}`); assert.equal(quote.ownerSats, split.ownerSats, `owner share at ${height}`); assert.equal(quote.fundSats, split.fundSats, `fund share at ${height}`); const built = takeover(height); accepts(built, `takeover at ${height}`); assert.equal(built.transaction.outputs[1].valueSatoshis, split.fundSats); assert.equal(outLockHex(built, 0), binToHex(registryLock(terms, { ownerPkh: taker.pkh, expiry: height + BLOCKS_PER_YEAR })), "one fresh year from the takeover height"); rejects(() => takeover(height, { tamper: setOut(1, (o) => ({ valueSatoshis: o.valueSatoshis - 1n })) }), `fund underpaid by one satoshi at ${height}`); if (split.ownerSats > 0n) { assert.equal(built.transaction.outputs[2].valueSatoshis, split.ownerSats); assert.equal(outLockHex(built, 2), owner.lockHex, "the previous owner is paid at their key"); rejects(() => takeover(height, { tamper: setOut(2, (o) => ({ valueSatoshis: o.valueSatoshis - 1n })) }), `previous owner underpaid at ${height}`); rejects(() => takeover(height, { tamper: setOut(2, () => ({ lockingBytecode: taker.lock })) }), `previous owner's share kept by the taker at ${height}`); } else { assert.equal(registryPhase(terms, state0, height), PHASE.open, "no owner share only once the grace period is over"); } } }); test("an ACTIVE name cannot be taken at any price", () => { assert.equal(takeoverQuote(terms, state0, EXPIRY - 1), null); assert.throws(() => takeover(EXPIRY - 1), /ACTIVE/); // Build a valid takeover, then claim an earlier height: the locktime check must refuse it. rejects(() => takeover(EXPIRY, { tamper: (tx) => ({ ...tx, locktime: EXPIRY - 1 }) }), "locktime before expiry"); rejects(() => takeover(EXPIRY, { tamper: (tx) => ({ ...tx, locktime: 0 }) }), "no locktime"); rejects(() => takeover(EXPIRY, { tamper: (tx) => ({ ...tx, locktime: 1_800_000_000 }) }), "a timestamp in place of a height"); }); test("a taker cannot claim a later height than the locktime to pay less", () => { // Price for IDLE_END (the floor) with a transaction that says it is only at EXPIRY. const floorQuote = takeoverQuote(terms, state0, IDLE_END); rejects(() => takeover(EXPIRY, { tamper: (tx) => ({ ...tx, outputs: tx.outputs.map((o, i) => (i === 1 ? { ...o, valueSatoshis: floorQuote.fundSats } : o)) }) }), "floor price paid during grace"); rejects(() => takeover(EXPIRY, { tamper: setOut(0, () => ({ lockingBytecode: registryLock(terms, { ownerPkh: taker.pkh, expiry: EXPIRY + 3 * BLOCKS_PER_YEAR }) })) }), "three years claimed for the price of one"); }); test("takeover needs the taker's own matured commit for this name", () => { accepts(takeover(EXPIRY + 10), "with the commit"); rejects(() => takeover(EXPIRY + 10, { box: { ...boxUtxo(), lockingBytecode: taker.lock } }), "input 1 is a plain coin, not a commit box"); rejects(() => takeover(EXPIRY + 10, { box: boxUtxo(taker, "other.x") }), "a commit made for another name"); rejects(() => takeover(EXPIRY + 10, { box: boxUtxo(taker, NAME, hexToBin("ff".repeat(32))) }), "a commit made with another salt"); rejects(() => takeover(EXPIRY + 10, { box: boxUtxo(stranger) }), "someone else's commit"); rejects(() => takeover(EXPIRY + 10, { tamper: (tx) => ({ ...tx, inputs: tx.inputs.map((inp, i) => (i === 1 ? { ...inp, sequenceNumber: 0 } : inp)) }) }), "a commit spent in the block it was made in"); rejects(() => takeover(EXPIRY + 10, { tamper: setOut(0, () => ({ lockingBytecode: registryLock(terms, { ownerPkh: stranger.pkh, expiry: EXPIRY + 10 + BLOCKS_PER_YEAR }) })) }), "the name goes to a key the commit was not made for"); }); test("takeover keeps the certificate in the covenant", () => { rejects(() => takeover(IDLE_END, { tamper: setOut(0, () => ({ lockingBytecode: taker.lock })) }), "certificate sent to the taker's wallet"); rejects(() => takeover(IDLE_END, { tamper: setOut(0, () => ({ token: undefined })) }), "certificate dropped"); }); // ---------- release ---------- const release = (o = {}) => buildRegistryReleaseTx({ cov: covUtxo(), terms, state: state0, coins: [coin(2, owner)], changeLock: owner.lock, keyFor: keyring(owner), ...o }); test("release destroys the certificate; it cannot be used to take it out", () => { const built = release(); accepts(built, "release"); assert.ok(built.transaction.outputs.every((o) => !o.token)); accepts(release({ extra: [{ lockingBytecode: Uint8Array.of(0x6a, 0x01, 0x00), valueSatoshis: 0n }, { lockingBytecode: fund.lock, valueSatoshis: 600n }] }), "release with a REL payload and beacon dust"); for (const i of [0, 1, 2]) { rejects(() => release({ extra: [{ lockingBytecode: Uint8Array.of(0x6a, 0x01, 0x00), valueSatoshis: 0n }, { lockingBytecode: fund.lock, valueSatoshis: 600n }], tamper: setOut(i, () => ({ lockingBytecode: owner.lock, valueSatoshis: 1000n, token: { category: hexToBin(CATEGORY), amount: 0n, nft: { capability: "none", commitment: new TextEncoder().encode(NAME) } } })) }), `certificate recreated in output ${i}`); } rejects(() => release({ tamper: (tx) => ({ ...tx, outputs: [...tx.outputs.slice(0, 1), ...Array(4).fill({ lockingBytecode: owner.lock, valueSatoshis: 546n })] }) }), "five outputs: one would be unchecked"); rejects(() => buildRegistryReleaseTx({ cov: covUtxo(), terms, state: state0, coins: [coin(2, stranger)], changeLock: stranger.lock, keyFor: () => ({ privateKey: stranger.priv, publicKey: stranger.pub }) }), "a stranger releases someone's name"); }); // ---------- migrate ---------- // A stand-in for a newer version's CODE. It runs behind the 26 state bytes // (two pushes), which it drops; then it is spendable by the owner's key. const NEW_CODE = flattenBinArray([Uint8Array.of(0x6d), Uint8Array.of(0x21), owner.pub, Uint8Array.of(0xac)]); // OP_2DROP OP_CHECKSIG const approval = signMigrationApproval(steward.priv, terms, NEW_CODE); const migrate = (o = {}) => buildRegistryMigrateTx({ cov: covUtxo(), terms, state: state0, newCode: NEW_CODE, approval, coins: [coin(2, owner)], changeLock: owner.lock, keyFor: keyring(owner), ...o }); test("migrate: owner AND steward — neither can move a name alone, and the state carries over", () => { const built = migrate(); accepts(built, "migrate"); assert.equal(outLockHex(built, 0), binToHex(migratedLock(state0, NEW_CODE)), "same owner, same expiry, in front of the approved code"); rejects(() => migrate({ approval: signMigrationApproval(stranger.priv, terms, NEW_CODE) }), "code approved by someone who is not the steward"); rejects(() => migrate({ newCode: flattenBinArray([NEW_CODE, Uint8Array.of(0x00)]) }), "code the steward did not sign"); rejects(() => migrate({ approval: signMigrationApproval(steward.priv, terms, Uint8Array.of(0x51)) }), "an approval for different code"); rejects(() => buildRegistryMigrateTx({ cov: covUtxo(), terms, state: state0, newCode: NEW_CODE, approval, coins: [coin(2, stranger)], changeLock: stranger.lock, keyFor: () => ({ privateKey: stranger.priv, publicKey: stranger.pub }) }), "the steward's approval without the owner"); rejects(() => migrate({ tamper: setOut(0, () => ({ lockingBytecode: owner.lock })) }), "certificate sent to a plain address under cover of a migration"); rejects(() => migrate({ tamper: setOut(0, () => ({ token: undefined })) }), "certificate dropped"); }); test("migrate: the owner chooses nothing — no script in front of the code, no new expiry, no new owner", () => { // The first draft let the owner supply the bytes placed in front of the // approved code. An owner could put their own signature check there and a // push opcode that swallowed the approved code as data: the certificate left // the covenant under any approval the steward had ever made. The contract // now rebuilds the state itself, so every one of these targets is refused. const push = (b) => (b.length <= 75 ? flattenBinArray([Uint8Array.of(b.length), b]) : flattenBinArray([Uint8Array.of(0x4c, b.length), b])); const swallowed = flattenBinArray([Uint8Array.of(0x21), owner.pub, Uint8Array.of(0xad), push(NEW_CODE)]); // OP_CHECKSIGVERIFY PUSH(approved code) rejects(() => migrate({ tamper: setOut(0, () => ({ lockingBytecode: p2sh32Lock(swallowed) })) }), "the approved code wrapped as data behind the owner's own signature check"); rejects(() => migrate({ tamper: setOut(0, () => ({ lockingBytecode: migratedLock({ ownerPkh: owner.pkh, expiry: 0x7ffffffe }, NEW_CODE) })) }), "a migration that also grants itself forty thousand years"); rejects(() => migrate({ tamper: setOut(0, () => ({ lockingBytecode: migratedLock({ ownerPkh: buyer.pkh, expiry: EXPIRY }, NEW_CODE) })) }), "a migration that also changes the owner"); }); test("migrate: an approval is for one covenant and is worthless in another", () => { // Same steward, same new code — but the approval was made for tier 4's covenant. const otherTerms = registryTerms({ tier: 4, fundLock: fund.lock, stewardPk: steward.pub }); rejects(() => migrate({ approval: signMigrationApproval(steward.priv, otherTerms, NEW_CODE) }), "an approval made for another tier's covenant"); const otherFund = registryTerms({ tier: TIER, fundLock: stranger.lock, stewardPk: steward.pub }); rejects(() => migrate({ approval: signMigrationApproval(steward.priv, otherFund, NEW_CODE) }), "an approval made for a covenant with another fund (another network's, say)"); }); // ---------- bounds the review asked for ---------- test("renew cannot push the expiry past the range of block heights", () => { const nearEnd = { ownerPkh: owner.pkh, expiry: 500_000_000 - BLOCKS_PER_YEAR }; const cov = covUtxo(nearEnd); // one more year would be exactly 500,000,000: read as a timestamp, never takeable again rejects(() => buildRegistryRenewTx({ cov, terms, state: nearEnd, years: 1, coins: [coin(2, owner)], changeLock: owner.lock, keyFor: keyring(owner) }), "expiry of exactly 500,000,000"); const justUnder = { ownerPkh: owner.pkh, expiry: 500_000_000 - BLOCKS_PER_YEAR - 1 }; accepts(buildRegistryRenewTx({ cov: covUtxo(justUnder), terms, state: justUnder, years: 1, coins: [coin(2, owner)], changeLock: owner.lock, keyFor: keyring(owner) }), "expiry of 499,999,999"); }); test("a commit is for one name: the salt has a fixed size, so nothing can slide between name and salt", () => { assert.equal(COMMIT_SALT_BYTES, 32); assert.throws(() => commitHashFor(NAME, hexToBin("00".repeat(16))), /exactly 32 bytes/); // The ambiguity, with real bytes: hash256("covenant" ‖ ".x" ‖ S) is one hash, // whether you call the name "covenant" with salt ".x"‖S or "covenant.x" with salt S. // Build a takeover of "covenant.x" from a box made as if for a 34-byte salt on a shorter name, // by hand (the library refuses to), and confirm the CONTRACT refuses the odd-sized salt. const longSalt = flattenBinArray([new TextEncoder().encode("xx"), SALT]); // 34 bytes const { hash256 } = L; const commitHash = hash256(flattenBinArray([new TextEncoder().encode(NAME), longSalt])); const boxRedeem = commitBoxRedeem({ ownerPkh: taker.pkh, commitHash }); const box = { txid: fakeTxid(7), vout: 0, satoshis: 1000n, lockingBytecode: p2sh32Lock(boxRedeem) }; const height = IDLE_END, quote = takeoverQuote(terms, state0, height); const redeem = registryRedeem(terms, state0); const FN = selectorsOf(NAME_REGISTRY); const takerKey = { privateKey: taker.priv, publicKey: taker.pub }; const cov = covUtxo(); rejects(() => assemble({ special: [ { utxo: cov, unlock: () => unlockScript({ selector: FN.takeover, redeem, args: [hexToBin(taker.pkh), longSalt] }) }, { utxo: box, sequence: 1, unlock: (transaction, sourceOutputs, inputIndex, final) => unlockScript({ redeem: boxRedeem, transaction, sourceOutputs, inputIndex, key: final ? takerKey : null, args: [PUBKEY, SIG] }) }, ], outputs: [ { lockingBytecode: registryLock(terms, { ownerPkh: taker.pkh, expiry: quote.newExpiry }), valueSatoshis: 1000n, token: { amount: 0n, category: hexToBin(CATEGORY), nft: { capability: "none", commitment: new TextEncoder().encode(NAME) } } }, { lockingBytecode: fund.lock, valueSatoshis: quote.fundSats }, ], coins: [coin(3, taker, 50_000_000_000n)], changeLock: taker.lock, keyFor: keyring(taker), locktime: height, }), "a takeover with a 34-byte salt, everything else in order"); }); // ---------- a life ---------- test("a name's life: renew, sell, lapse, be taken over, be renewed by its new owner", () => { let state = state0; let cov = covUtxo(state); const step = (built, next) => { accepts(built, "life"); cov = { txid: built.txid, vout: 0, satoshis: built.transaction.outputs[0].valueSatoshis, lockingBytecode: built.transaction.outputs[0].lockingBytecode, token: certToken() }; state = next; }; let b = buildRegistryRenewTx({ cov, terms, state, years: 2, coins: [coin(20, owner)], changeLock: owner.lock, keyFor: keyring(owner) }); step(b, b.state); b = buildRegistryTransferTx({ cov, terms, state, newOwnerPkh: buyer.pkh, coins: [coin(21, owner)], changeLock: owner.lock, keyFor: keyring(owner) }); step(b, b.state); assert.equal(state.expiry, EXPIRY + 2 * BLOCKS_PER_YEAR); rejects(() => buildRegistryRenewTx({ cov, terms, state, coins: [coin(22, owner)], changeLock: owner.lock, keyFor: keyring(owner, buyer) === null ? null : (h) => (h === owner.lockHex ? { privateKey: owner.priv, publicKey: owner.pub } : h === buyer.lockHex ? { privateKey: owner.priv, publicKey: owner.pub } : null) }), "the old owner cannot renew a name they sold"); const height = state.expiry + 100; // the buyer lets it lapse b = buildRegistryTakeoverTx({ cov, terms, state, name: NAME, height, newOwnerPkh: taker.pkh, salt: SALT, box: boxUtxo(), coins: [coin(23, taker, 50_000_000_000n)], changeLock: taker.lock, keyFor: keyring(taker) }); assert.equal(binToHex(b.transaction.outputs[2].lockingBytecode), buyer.lockHex, "the buy-out goes to whoever owned it when it lapsed"); step(b, b.state); assert.equal(state.ownerPkh, taker.pkh); b = buildRegistryRenewTx({ cov, terms, state, coins: [coin(24, taker)], changeLock: taker.lock, keyFor: keyring(taker) }); step(b, b.state); assert.equal(state.expiry, height + 2 * BLOCKS_PER_YEAR); }); // ---------- the negatives are real ---------- test("every negative case was refused by the contract itself, not by the library", () => { assert.deepEqual(refusedByLibrary.sort(), ["zero years"], "only these may be stopped before the VM runs"); assert.ok(refusedByVm >= 54, `expected the VM to refuse at least 54 tampered transactions, it refused ${refusedByVm}`); });