Skip to content

GO-7567: 0.7.2 keep names reserved, never remove cache records - #62

Merged
requilence merged 13 commits into
mainfrom
fix/ns-names-reserved
Oct 8, 2026
Merged

requilence merged 13 commits into
mainfrom
fix/ns-names-reserved

Conversation

@requilence

Copy link
Copy Markdown
Contributor

Linear: GO-7567 (hotfix for 0.7.1). Design: doc/names-reserved-0.7.2.md.

Problem

v0.7.1 made the name cache free names that lapsed on chain (tombstones). In prod this:

  • wiped the owner, AnyID, space and expiry of every tombstoned record (the tombstone is a ReplaceOne of a bare observation), and made those names available to anyone;
  • contradicted the product decision that a name stays reserved to the identity that registered it, even after it lapses (moving a name is a support action);
  • exhausted the provider's daily quota: lookups of expired/lapsed names and the periodic expiry/lapse/daily re-reads drove chain reads on both nodes.

Summary

  • A lapse never removes. A lapsed read updates the record in place (lapsed: true, expiry, block), keeping owner/AnyID/space; no enrichment reads. "Not registered" never removes a cached record (not-on-chain, bounded retries). No tombstones are written; existing removed records are served as taken. Finality/removal machinery and -purge-tombstones are gone.
  • Owner data is never wiped. Incomplete reads carry the stored owner fields over (only for the same AnyID when just the owner is missing); a maintenance pass never replaces a complete record with an incomplete one.
  • Chain reads only for our own operations. Background refresh only for post-operation re-reads (+5/+30 min) and incomplete records; per-record exponential backoff (1 min .. 24 h), cut by a new operation; records due only under the 0.7.1 rules are rescheduled without a chain read; one read per operation poll burst.
  • Reservation enforced on registration. AdminNameRegisterSigned rejects a name cached for another (or no) identity (ErrNameReserved), fail closed; same owner allowed. Advisory check on GetDataNameRegister*.
  • Maintenance: -restore-tombstones -restore-from <mongoexport jsonl> (restores owners of 0.7.1 tombstones; reports missing / conflict / owner-differs), -release-name <name> (support transfer), -refresh-cache -rpc-url <url> (one-off pass on another provider, chain id checked; no re-reads scheduled for the live nodes; lapsed / not-on-chain / unnormalizable counters).
  • Kept from 0.7.1: GO-7482, canon + unique name index, ordered transactional writes, replica set requirement, post-operation re-reads, -dedupe-cache.

Rollout (README "Name cache")

  1. Deploy 0.7.2 on both ns nodes (no 0.7.1 node left running).
  2. -restore-tombstones -restore-from ns-cache-20261007.jsonl (export taken before 0.7.1), dry run then -refresh-apply.
  3. Backup ns.cache, then -refresh-cache -rpc-url <alchemy> -refresh-interval 2s, dry run then -refresh-apply.

Rollback: 0.6.9 (serves every record as taken). Never 0.7.1.

Test plan

  • go build ./..., gofmt -l clean; go vet / errcheck+revive: no new findings vs v0.7.1
  • go test -p 1 ./... on a replica set: all pass except contracts TestNormalizeEnsip1 (fails locally on v0.7.1 too, passes in CI)
  • Mutation checks on lapse/owner carry-over, removed-as-taken, reservation, scan rescheduling, maintenance re-reads, restore owner-differs, backoff cut
  • Two review rounds (3 lenses each: cache correctness, data safety/migration, prod load/rollout); round 2: no critical/high findings
  • Stage1: deploy, restore dry run, refresh dry run

A name stays reserved to the identity that registered it. 0.7.1 turned a
lapsed name into a tombstone with a ReplaceOne of a bare observation, which
wiped its owner fields, and re-read the chain on lookups and on a time
schedule, which exhausted the provider quota.

- a lapse is stored on the record in place (lapsed, expiry, registry owner,
  block); the owner fields, AnyID, space ID and canon stay, no enrichment
  reads. a lapsed name that is not cached is not written
- "never registered" never removes: the cached record stays (errNotOnChain,
  a settled refresh); the finalized-block confirmation and every tombstone
  write are gone
- 0.7.1 tombstones (removed) are served as taken; the alias check counts them
- lookups hand only refresh_needed records to the background; repair_at is
  due only for refresh_needed records and re-reads; the scan reschedules
  records due under the 0.7.1 rules without a chain read
- exponential per-record failure backoff (refresh_failures, 1 min doubled,
  at most 24 h), reset by a successful write
- maintenance: -release-name (support transfer), -restore-tombstones
  -restore-from (owners from a mongoexport), -rpc-url; -refresh-cache counts
  lapsed / not-on-chain; -purge-tombstones removed
AdminNameRegisterSigned looks the canonical name up in the cache before it
sends the operation (cache.CheckReservation, on IsNameAvailable: any record,
lapsed, removed or under another spelling). A record of another identity or
without an owner is rejected (ErrNameReserved), the same identity is allowed
(a re-registration after a lapse, the payment node's renew-past-grace
fallback), a cache read error rejects (fail closed). GetDataNameRegister and
GetDataNameRegisterForSpace check the same before building calldata for a
client-signed registration. Renewals are not checked.
An incomplete read (and a name reserved without a registry owner) carries
over from the stored record what it could not read: the owner, its wallet,
AnyID and space ID. The record stays incomplete and marked until a complete
read. In 0.7.2 the identity of a name only changes through our operations or
-release-name, so a stored owner is never a guess.
The first scans after the upgrade reschedule thousands of records that are due
only under the 0.7.1 rules. They no longer fill the batch: the scan pages on
by (repair_at, _id), at most 500 reschedules per round, so a due re-read or
incomplete record is still read in the same round.
A lagging provider must not consume an operation's re-reads. A cached record
that the chain says was never registered keeps its due re-reads and backs off
like a failed refresh (1, 2, 4... minutes, never every lease); after 6 such
reads in a row (past the +30 min re-read) they are done and the record is
left to an operator.
…ion poll burst

- refreshAfterOperation stores the operation's re-reads itself when another
  refresh holds the record, so they survive a skipped or timed-out request
  schedule (bounded write in the worker, the poll never waits)
- a replacing write keeps a short hold (refresh_next_at = read + lease):
  repeated GetOperation polls of a cached name no longer cost one chain
  refresh each
…unnormalizable

-restore-tombstones:
- name_expires is the later of the export's and the tombstone's
- names with records of different owners in the export are not restored,
  listed as conflict:
- skipped= counts records that were no longer removed at the write
  (tombstones = restored + skipped + missing + conflicts)
- owner-differs: lists live records whose owner differs from the export's
  (for support; nothing is written); exit 1 on missing/conflict/owner-differs
- a restore with name_expires 0 is logged

-release-name deletes only the record as printed (_id, observed_at,
owner_any_address); ErrRecordChanged if it changed in between.

-refresh-cache counts names that can not be normalized as unnormalizable=,
not as failed.
At startup of a maintenance run, eth_chainId of -rpc-url is compared with
accountAbstraction.chainID of the config (or, without one, with the chain id
of contracts.gethUrl); a mismatch or a failed read refuses to run. Errors
carry the provider host only, never the URL.
… marks, safer maintenance

- an operation (its re-reads) cuts a failure backoff to refreshLease and
  resets refresh_failures, so a renewal shows soon after a provider outage
- unreadOwner carries the owner over only for the same AnyID
- a not-on-chain record past the retries is retired: its mark is cleared too
- -restore-tombstones: a lapsed restore clears refresh_needed,
  refresh_failures and stale re-reads (no live chain read); an unknown expiry
  (0) gets refresh_needed instead of lapsed
- a maintenance run never replaces a complete record with an incomplete
  read: it counts as failed
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

New Coverage 58.8% of statements
Patch Coverage 88.8% of changed statements (443/499)

Coverage provided by https://github.com/seriousben/go-patch-cover-action

@requilence
requilence merged commit 3d1c531 into main Oct 8, 2026
8 checks passed
@requilence
requilence deleted the fix/ns-names-reserved branch October 8, 2026 13:40
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 8, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant