bns: REL — an owner can release a name, and resolvers notice a burned certificate
A burned certificate used to leave a name in the index forever: listed, resolvable with its last records, and impossible to update or re-register. REL is the declared burn. A transaction that spends the certificate from a beacon transaction, recreates no output of its category and carries the REL payload removes the name from the index. The proof is read from an input because a burn leaves no output to read it from. Resolvers hold only the beacon history, so the spent output must belong to a beacon transaction; a certificate moved by a plain transfer is anchored with an UPD first. That keeps the rule a pure function of the snapshot every client already has. A released name can be registered again only in a later block than the confirmed release. Same-block re-registration would be decided by txid order between a release and a snipe sitting in the mempool together. This is the first step of ROADMAP-MAINNET.md §9: it puts burn detection in the resolvers on its own, before any covenant depends on it. Specified in PROTOCOL-ADDENDUM-release.md; shared resolver and its Ariadne copy here.
This commit is contained in:
parent
c7c66e1557
commit
e185ca43a0
3 changed files with 80 additions and 5 deletions
23
PROTOCOL.md
23
PROTOCOL.md
|
|
@ -28,7 +28,16 @@ server is the only infrastructure a resolver needs. No fork, no sidechain.
|
|||
2. Carries `OP_RETURN "BNS1{...}"` with `{"o":"UPD","n":<name>,"r":<newRecords>}`.
|
||||
3. Pays 600 sat to the beacon.
|
||||
|
||||
**REL** — one tx that:
|
||||
1. Spends the name's certificate and does **not** recreate it (no output carries
|
||||
the category): the certificate is burned.
|
||||
2. Carries `OP_RETURN "BNS1{...}"` with `{"o":"REL","n":<name>}`.
|
||||
3. Pays 600 sat to the beacon.
|
||||
|
||||
The name returns to the pool. Specified in `PROTOCOL-ADDENDUM-release.md`.
|
||||
|
||||
Transferring a name = sending the certificate to a new owner (no beacon tx needed).
|
||||
A certificate moved that way must be anchored by an `UPD` before it can be released.
|
||||
|
||||
## Records (JSON, keep total payload ≤ 200 bytes)
|
||||
|
||||
|
|
@ -55,9 +64,14 @@ Transferring a name = sending the certificate to a new owner (no beacon tx neede
|
|||
from. Independent transactions keep the `txid` tiebreak. *(See "Ordering"
|
||||
below — this rule is load-bearing, added 2026-07-26.)*
|
||||
3. `REG`: valid if the same tx mints a certificate whose commitment matches the name
|
||||
and the name is not yet taken. **First valid REG wins.**
|
||||
and the name is not yet taken. **First valid REG wins.** If the name was
|
||||
released (rule 5), the `REG` must be in a later block than the confirmed
|
||||
release — the one-block gap.
|
||||
4. `UPD`: valid if some output of the tx carries the name's category.
|
||||
5. Names: `[a-z0-9-]{1,32}`, rendered as `<name>.bch`.
|
||||
5. `REL`: valid if the name is registered, no output of the tx carries the
|
||||
name's category, and one input spends an output of an earlier beacon
|
||||
transaction that carries the certificate. The name leaves the index.
|
||||
6. Names: `[a-z0-9-]{1,32}`, rendered as `<name>.bch`.
|
||||
|
||||
### Ordering (why rule 2 exists)
|
||||
|
||||
|
|
@ -89,7 +103,8 @@ between them is a consensus split between clients.
|
|||
causally — see "Ordering" above.
|
||||
- The beacon is a keyed address (prototype convenience); production would use a
|
||||
keyless constant script so no one can be pressured over it.
|
||||
- No expiry/renewal: names live forever once minted (Namecoin used ~8-month
|
||||
renewals to garbage-collect dead names).
|
||||
- No expiry/renewal: names live until their owner releases them (`REL`);
|
||||
an abandoned name still lives forever (Namecoin used ~8-month renewals to
|
||||
garbage-collect dead names — the covenant registry is that step).
|
||||
- Front-running: a miner/mempool observer could see a REG and race it
|
||||
(Namecoin's two-step `name_new` commit-reveal solved this; omitted here).
|
||||
|
|
|
|||
4
src/lib/resolver-web.d.ts
vendored
4
src/lib/resolver-web.d.ts
vendored
|
|
@ -57,6 +57,10 @@ export interface BnsNetwork {
|
|||
accountPath: string;
|
||||
bchnRpcPort: number;
|
||||
}
|
||||
/** REL: does `tx` burn `entry`'s certificate, spending it from a beacon transaction in `txs`? */
|
||||
export function burnsCertificate(tx: any, entry: { category: string }, txs: Map<string, any>): boolean;
|
||||
/** One-block gap after a release. `releasedAt` undefined = never released. */
|
||||
export function mayReRegister(releasedAt: number | undefined, height: number): boolean;
|
||||
export const NETWORKS: { chipnet: BnsNetwork; mainnet: BnsNetwork };
|
||||
export const DEFAULT_NETWORK_ID: "chipnet";
|
||||
export function networkIdFromEnv(): string;
|
||||
|
|
|
|||
|
|
@ -297,6 +297,8 @@ function parseOpReturnHex(hex) {
|
|||
const p = JSON.parse(data.slice(PREFIX.length));
|
||||
// Recognised opcodes:
|
||||
// REG / UPD — name registrations & updates on the name beacon
|
||||
// REL — voluntary release of a name (burns its certificate)
|
||||
// (Decentralized.DNS/PROTOCOL-ADDENDUM-release.md)
|
||||
// TREG / TUPD — TLD registrations & updates on the TLD beacon
|
||||
// (Decentralized.DNS/DESIGN-tld-registry.md §3)
|
||||
// KEEP IN LOCKSTEP with bns.js parseOpReturn — memory rule.
|
||||
|
|
@ -307,7 +309,7 @@ function parseOpReturnHex(hex) {
|
|||
|
||||
// The set of payload opcodes this codec recognises. New opcodes go here AND in
|
||||
// bns.js — divergence is a consensus split.
|
||||
export const KNOWN_OPS = new Set(["REG", "UPD", "TREG", "TUPD"]);
|
||||
export const KNOWN_OPS = new Set(["REG", "UPD", "REL", "TREG", "TUPD"]);
|
||||
|
||||
// ---- ordering ----
|
||||
|
||||
|
|
@ -480,6 +482,38 @@ function ownerFromVout(vout) {
|
|||
return null;
|
||||
}
|
||||
|
||||
// ---- REL: voluntary release (Decentralized.DNS/PROTOCOL-ADDENDUM-release.md) ----
|
||||
// The output a vin spends, looked up among the beacon transactions the index is
|
||||
// built from. null when the parent is not one of them — the resolver holds the
|
||||
// beacon history and nothing else, so the rule is stated over that set.
|
||||
function spentOutput(vin, txs) {
|
||||
const prev = vin && vin.txid ? txs.get(vin.txid) : null;
|
||||
if (!prev || !Array.isArray(prev.vout)) return null;
|
||||
return prev.vout.find((o) => o && o.n === vin.vout) ?? prev.vout[vin.vout] ?? null;
|
||||
}
|
||||
// A release is valid when the transaction burns the name's certificate: no
|
||||
// output carries the category, and one input spends an output of an earlier
|
||||
// beacon transaction that carried it as an NFT. `txs` is the Map of beacon
|
||||
// transactions (txid -> verbose tx). KEEP IN LOCKSTEP with bns.js.
|
||||
export function burnsCertificate(tx, entry, txs) {
|
||||
if (!tx || !entry || !entry.category) return false;
|
||||
if ((tx.vout || []).some((v) => v.tokenData && v.tokenData.category === entry.category)) return false;
|
||||
return (tx.vin || []).some((vin) => {
|
||||
const out = spentOutput(vin, txs);
|
||||
return !!(out && out.tokenData && out.tokenData.nft && out.tokenData.category === entry.category);
|
||||
});
|
||||
}
|
||||
// The one-block gap. `releasedAt` is the height of the name's release
|
||||
// (undefined = never released; <= 0 = release still unconfirmed). A released
|
||||
// name can be registered again only by a REG in a later block than a confirmed
|
||||
// release; an unconfirmed REG qualifies because it cannot be mined into a block
|
||||
// that already exists. KEEP IN LOCKSTEP with bns.js.
|
||||
export function mayReRegister(releasedAt, height) {
|
||||
if (releasedAt === undefined) return true;
|
||||
if (!(releasedAt > 0)) return false;
|
||||
return !(height > 0) || height > releasedAt;
|
||||
}
|
||||
|
||||
export async function buildIndex({ network = NETWORK, electrum = getNetwork(network).electrum, WebSocket: WS, client, directIP = false, enforceTld = false, cosignGate = true } = {}) {
|
||||
const net = requireBeacons(network);
|
||||
const el = client ?? (await connectAny(electrum, WS, { directIP }));
|
||||
|
|
@ -523,6 +557,7 @@ export async function buildIndex({ network = NETWORK, electrum = getNetwork(netw
|
|||
}
|
||||
|
||||
const names = new Map();
|
||||
const released = new Map(); // name -> height of its release (see mayReRegister)
|
||||
for (const { tx_hash, height } of orderByDependency(history.filter((h) => txs.has(h.tx_hash)), txs)) {
|
||||
const tx = txs.get(tx_hash);
|
||||
const vouts = tx.vout || [];
|
||||
|
|
@ -536,6 +571,7 @@ export async function buildIndex({ network = NETWORK, electrum = getNetwork(netw
|
|||
|
||||
if (payload.o === "REG") {
|
||||
if (names.has(key)) continue; // first valid REG wins
|
||||
if (!mayReRegister(released.get(key), height)) continue; // one-block gap after a release
|
||||
// TLD gate — see §4 of the design. Only applies to REG; UPDs modify
|
||||
// names already admitted, so no gate needed there.
|
||||
if (enforceTld && !tldAdmits(tldOfKey(key), height, tldMap)) continue;
|
||||
|
|
@ -558,6 +594,13 @@ export async function buildIndex({ network = NETWORK, electrum = getNetwork(netw
|
|||
// Updated in the UPD branch below on every ownership-touching tx.
|
||||
owner: ownerFromVout(mint),
|
||||
});
|
||||
released.delete(key);
|
||||
} else if (payload.o === "REL") {
|
||||
const entry = names.get(key);
|
||||
if (!entry) continue;
|
||||
if (!burnsCertificate(tx, entry, txs)) continue;
|
||||
names.delete(key);
|
||||
released.set(key, height);
|
||||
} else if (payload.o === "UPD") {
|
||||
const entry = names.get(key);
|
||||
if (!entry) continue;
|
||||
|
|
@ -577,6 +620,9 @@ export async function buildIndex({ network = NETWORK, electrum = getNetwork(netw
|
|||
// working. A caller that cares (the registrar, before taking money) can ask
|
||||
// whether the servers agreed on what exists.
|
||||
if (integrity) Object.defineProperty(names, "integrity", { value: integrity, enumerable: false });
|
||||
// name -> release height, for names currently free because they were
|
||||
// released (the registrar reads it to explain a pending release).
|
||||
Object.defineProperty(names, "released", { value: released, enumerable: false });
|
||||
return names;
|
||||
} finally {
|
||||
if (!client) el.close();
|
||||
|
|
@ -605,6 +651,7 @@ export function buildIndexFromSnapshot({ snapshot, enforceTld = false, tldMap =
|
|||
const txMap = snapshot.txs instanceof Map ? snapshot.txs : new Map(Object.entries(snapshot.txs || {}));
|
||||
const history = [...snapshot.history].sort(byHeightThenTxid);
|
||||
const names = new Map();
|
||||
const released = new Map();
|
||||
for (const { tx_hash, height } of orderByDependency(history.filter((h) => txMap.has(h.tx_hash)), txMap)) {
|
||||
const tx = txMap.get(tx_hash);
|
||||
const vouts = tx.vout || [];
|
||||
|
|
@ -616,6 +663,7 @@ export function buildIndexFromSnapshot({ snapshot, enforceTld = false, tldMap =
|
|||
const key = canonicalKey(rawName);
|
||||
if (payload.o === "REG") {
|
||||
if (names.has(key)) continue;
|
||||
if (!mayReRegister(released.get(key), height)) continue;
|
||||
if (enforceTld && !tldAdmits(tldOfKey(key), height, tldMap)) continue;
|
||||
const commitHex = strToHex(rawName);
|
||||
const mint = vouts.find((v) => v.tokenData?.nft && v.tokenData.nft.commitment === commitHex);
|
||||
|
|
@ -625,6 +673,13 @@ export function buildIndexFromSnapshot({ snapshot, enforceTld = false, tldMap =
|
|||
txid: tx_hash, height, updatedTxid: null,
|
||||
owner: ownerFromVout(mint),
|
||||
});
|
||||
released.delete(key);
|
||||
} else if (payload.o === "REL") {
|
||||
const entry = names.get(key);
|
||||
if (!entry) continue;
|
||||
if (!burnsCertificate(tx, entry, txMap)) continue;
|
||||
names.delete(key);
|
||||
released.set(key, height);
|
||||
} else if (payload.o === "UPD") {
|
||||
const entry = names.get(key);
|
||||
if (!entry) continue;
|
||||
|
|
@ -635,6 +690,7 @@ export function buildIndexFromSnapshot({ snapshot, enforceTld = false, tldMap =
|
|||
entry.owner = ownerFromVout(proving) ?? entry.owner;
|
||||
}
|
||||
}
|
||||
Object.defineProperty(names, "released", { value: released, enumerable: false });
|
||||
return names;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue