diff --git a/src/lib/bns.js b/src/lib/bns.js index d7a0851..0127bdb 100644 --- a/src/lib/bns.js +++ b/src/lib/bns.js @@ -296,6 +296,7 @@ export async function buildIndex(provider, { enforceTld = false, cosignGate = tr const entry = { name, category: mint.tokenData.category, records: payload.r ?? {}, txid: tx_hash, height, updatedTxid: null, + ...(won.commit ? { commit: { height: won.commit.height, txid: won.commit.txid } } : {}), }; names.set(name, entry); byCategory.set(entry.category, entry); diff --git a/src/lib/registrar.js b/src/lib/registrar.js index 4c2d7ad..844af12 100644 --- a/src/lib/registrar.js +++ b/src/lib/registrar.js @@ -109,11 +109,21 @@ export async function checkAvailability(client, name) { // (PROTOCOL-ADDENDUM-release.md, the one-block gap). const releasedAt = index.released?.get(key); const releasePending = releasedAt !== undefined && !(releasedAt > 0); + // A registration can still be displaced, for contestBlocks after it is + // mined, by one whose commit is older (PROTOCOL-ADDENDUM-commit-reveal.md). + // `contest` says whether that window is open and what a challenger must beat. + const contestBlocks = NETWORK.commitReveal?.contestBlocks ?? 0; + const contest = entry ? { + open: !(entry.height > 0) || (index.height ?? 0) + 1 <= entry.height + contestBlocks, + finalAt: entry.height > 0 ? entry.height + contestBlocks + 1 : null, + rank: entry.commit ? [entry.commit.height, entry.commit.txid] : [entry.height > 0 ? entry.height : Infinity, entry.txid], + } : null; return { name: key, rawName: raw, available: !entry && !releasePending, entry, + contest, releasedAt: releasedAt ?? null, releasePending, reason: entry ? "registered" : releasePending ? "release-pending" : null, @@ -332,19 +342,27 @@ export async function registerWithBuiltInWallet(client, { commit = null, onProgress = () => {}, }) { + let committed = null; if (commit) { if (normalizeName(commit.name) !== normalizeName(name)) throw new Error("this commit was made for another name"); if (commit.lock !== binToHex(addressToLockingBytecode(wallet.address))) throw new Error("this commit was made for another owner"); - const state = await commitState(client, commit); + const state = committed = await commitState(client, commit); if (!state.mature) throw new Error(state.expired ? "this commit is too old to reveal — commit again" : "this commit is not in a block yet — the registration can be sent after the next block"); } onProgress("checking-availability"); const availability = await checkAvailability(client, name); - if (!availability.available) throw new Error(availability.releasePending - ? `"${availability.name}" was just released — it can be registered once that release is in a block` - : `"${availability.name}" is already registered`); + if (!availability.available) { + // Somebody registered it first — perhaps by copying this very name out of + // the mempool. An older commit still takes it while the contest is open. + const c = availability.contest; + const older = committed && c && (committed.height < c.rank[0] || (committed.height === c.rank[0] && commit.txid < c.rank[1])); + if (!(c?.open && older)) throw new Error(availability.releasePending + ? `"${availability.name}" was just released — it can be registered once that release is in a block` + : `"${availability.name}" is already registered`); + onProgress("contesting", { incumbent: availability.entry.txid, finalAt: c.finalAt }); + } onProgress("loading-coins"); let utxos = await getUtxosForAddresses(client, wallet.watchedAddresses); diff --git a/src/lib/resolver-web.js b/src/lib/resolver-web.js index f628c5c..ccce46a 100644 --- a/src/lib/resolver-web.js +++ b/src/lib/resolver-web.js @@ -811,6 +811,8 @@ export async function buildIndex({ network = NETWORK, electrum = getNetwork(netw names.set(key, { name: key, category: mint.tokenData.category, records: payload.r ?? {}, txid: tx_hash, height, updatedTxid: null, + // the commit this registration revealed, when it had one + ...(won.commit ? { commit: { height: won.commit.height, txid: won.commit.txid } } : {}), // The mint output's scriptPubKey.address is the initial owner — // needed for the signed-records-manifest write path (POST // /api/records/) so the gateway can reject a signed @@ -918,6 +920,7 @@ export function buildIndexFromSnapshot({ snapshot, enforceTld = false, tldMap = names.set(key, { name: key, category: mint.tokenData.category, records: payload.r ?? {}, txid: tx_hash, height, updatedTxid: null, + ...(won.commit ? { commit: { height: won.commit.height, txid: won.commit.txid } } : {}), owner: ownerFromVout(mint), }); // registered straight into the registry covenant?