argus/src/generate-covenant-table.mjs
Local Dev 9404c024b7 BNS: resolvers read covenant-held names
A name whose certificate sits in the registry covenant has an owner and an
expiry that are hashed into its address, not written on the chain. Until
now a resolver saw only "held by some script address", forever.

The transaction that moves the certificate now says the state (`c` in the
payload) and the resolver checks it against the address, using the
covenant's code from the network table. Nobody is trusted about who owns
such a name or until when.

Entering is the one step script cannot police - a certificate can be sent
to a covenant address with any expiry - so resolvers set the price: the
entering transaction pays the fund for whole years and may claim no more
than it paid for. Once a name is inside, only a verified state speaks for
it; otherwise an owner could announce their way back to a name that never
expires.

A name past its grace period stays taken but resolves to nothing, judged
at the chain tip (the indexer now answers what that is) or, failing that,
at the newest block in the beacon history, so a stale index lapses a name
late and never early.

Same rules in the gateway, the Ariadne resolver and the Android resolver;
the builders announce every move, and there is one to put an existing
name into the covenant. Chipnet's fund and steward are the operator key.
2026-10-04 18:08:00 +02:00

75 lines
3.4 KiB
JavaScript

// Prints the `covenant` block of a network record in src/lib/resolver-web.js.
//
// The resolver is a dependency-free single file: it cannot compile a contract
// or import the artifact. What it needs to recognise a covenant-held name is
// the covenant's CODE — every byte of the redeem script after the 26 state
// bytes — so that code is written into the network table, and this script is
// where it comes from. test/covenant-names.test.mjs fails if the table and the
// contract ever disagree.
//
// node src/generate-covenant-table.mjs --fund <cashaddr> --steward <33-byte pubkey hex>
//
// CODE = HEAD(tier) ‖ TAIL. Only the three prices differ between tiers, and
// they are pushed first, so the table stores one short head per tier and the
// long tail once.
import { binToHex, cashAddressToLockingBytecode, flattenBinArray } from "@bitauth/libauth";
import { registryTerms, registryCode, REGISTRY_VERSION } from "./lib/registry-covenant.js";
import { pushInt } from "./lib/covenant.js";
import { TIERS } from "./lib/registry-lifecycle.js";
/** { version, fundLock, steward, blocksPerYear, graceBlocks, tiers: [{ maxLabel, renewalSats, head }], tail } */
export function covenantTable({ fundLock, stewardPk }) {
let tail = null;
let shared = null;
const tiers = TIERS.map((t) => {
const terms = registryTerms({ tier: t.tier, fundLock, stewardPk });
const code = binToHex(registryCode(terms));
const head = binToHex(flattenBinArray([
pushInt(terms.ownerPercentIdle), pushInt(terms.blocksPerYear), pushInt(terms.graceBlocks),
pushInt(terms.floorSats), pushInt(terms.premiumSats), pushInt(terms.renewalSats),
]));
if (!code.startsWith(head)) throw new Error("the code does not start with the tier's price pushes");
const rest = code.slice(head.length);
if (tail !== null && rest !== tail) throw new Error("tiers differ in more than their prices");
tail = rest;
shared = terms;
return { maxLabel: Number.isFinite(t.maxLength) ? t.maxLength : 255, renewalSats: Number(terms.renewalSats), head };
});
return {
version: REGISTRY_VERSION,
fundLock: binToHex(shared.fundLock),
steward: binToHex(shared.stewardPk),
blocksPerYear: shared.blocksPerYear,
graceBlocks: shared.graceBlocks,
tiers,
tail,
};
}
const isMain = process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/\\/g, "/").split("/").pop());
if (isMain) {
const arg = (name) => { const i = process.argv.indexOf(`--${name}`); return i > 0 ? process.argv[i + 1] : null; };
const fund = arg("fund"), steward = arg("steward");
if (!fund || !steward) {
console.error("usage: node src/generate-covenant-table.mjs --fund <cashaddr> --steward <33-byte pubkey hex>");
process.exit(1);
}
const decoded = cashAddressToLockingBytecode(fund);
if (typeof decoded === "string") { console.error(decoded); process.exit(1); }
const t = covenantTable({ fundLock: decoded.bytecode, stewardPk: steward });
const lines = [
" covenant: {",
` version: ${t.version},`,
` fundLock: "${t.fundLock}",`,
` steward: "${t.steward}",`,
` blocksPerYear: ${t.blocksPerYear},`,
` graceBlocks: ${t.graceBlocks},`,
" tiers: [",
...t.tiers.map((x) => ` { maxLabel: ${x.maxLabel}, renewalSats: ${x.renewalSats}, head: "${x.head}" },`),
" ],",
` tail: "${t.tail}",`,
" },",
];
console.log(lines.join("\n"));
}