func AccountCount
ActionAccountCount is how many per-address fields exist, so a snapshot of the store can be checked for completeness. A separate read rather than a fifth value from Sizes, whose shape the snapshot tools already parse.
Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.
Package nsdata is the registry — the records behind every name.
Package nsdata is the registry — the records behind every name.
IT IS REPLACEABLE NOW, and that changes what every comment below means.
Until this generation the vault held ownership as well as records, which made it the one realm that could never be redeployed: a "new nsdata" was an empty registry and everybody's names with it. So everything here was written as if it would ship forever — no constants, no policy, no display code — and the comments say "can never be replaced" throughout.
Ownership has moved to nsnft, the small frozen token realm. This realm keeps records — dates, the frozen bit, TTL, profile data, reverse resolution, pricing globals, domains — and reads every owner from the ledger rather than storing one. Replacing it is a new package plus one Trust call on nsnft; no token moves, no holder notices. The integration address the outside world keeps is nsnft's, not this one's.
The two old rules still hold, for a better reason than before:
NOTHING IS A CONSTANT. Term length, grace period, size caps — all settable. Not because a mistake would be permanent any more, but because a registry that needs a redeploy to change a number is a registry that gets redeployed carelessly.
THE REGISTRY STORES, IT DOES NOT DECIDE. Pricing math, validation, what a profile field means — all of it lives in nslogic. Anything this realm "knows" is a rule that needs a migration to change.
Rule 2 is why Domain and Name carry an `extra` tree instead of a growing list of typed fields.
AccountCount is how many per-address fields exist, so a snapshot of the store can be checked for completeness. A separate read rather than a fifth value from Sizes, whose shape the snapshot tools already parse.
ActionReady reports whether an action could be consumed right now.
BalanceOf counts every token an address holds — including free-tier tokens, which are held rather than absent. That is a real change from the previous generation, where a free name had no token and counted for nothing; nsvote gates on this, so free-tier holders now have a vote.
CancelAction withdraws one pending action. Admin or recovery: the admin changing its mind, or recovery vetoing a key it no longer trusts.
CancelAllActions withdraws every pending action at once — the incident call, when nobody wants to read a list before acting. From recovery it is a veto: the admin proposes no action for one timelock.
CancelPending cancels the pending change of one kind. A proposal with a protectorRole can be cancelled only by that role, so a compromised key cannot veto its own replacement.
ClearGuardian stands the pause capability down.
"Removing a power is never the dangerous direction" was the old reasoning here, and it is wrong in one specific way: removing this power restores the state in which SetGuardian is admin-callable. SetGuardian demands the RECOVERY key whenever a guardian is already set, and says why — a stolen admin key must not be able to disarm the emergency stop. ClearGuardian then SetGuardian was a two-transaction admin-only replacement of a live guardian, which defeats exactly that rule.
So standing down a LIVE guardian now takes the same key that replacing one takes. Clearing when there is no guardian stays open to either role, because then there is nothing to defeat.
The practical stakes are small — assertCanPause admits admin, recovery and guardian, so an operator holding recovery keeps Pause and RevokeTrustedLogic either way. It is fixed because this realm can never be redeployed, and a documented authorization rule that a two-call sequence walks around is not something to leave in permanent code.
ConsumeAction spends a matured action. Trusted logic only, so it is also gated by the pause: a paused registry executes nothing, timelocked or not.
It refuses an action proposed by anybody but the CURRENT admin. Rotation already drops them all; this is the second lock on the same door, because a matured proposal is a loaded instruction and the vault should not rely on one rule to unload it.
DeleteDomainRecord destroys a domain and the names beneath it: it burns every token, drops every reverse-resolution pointer into them, removes the records, and finally removes the domain itself.
WHY IT EXISTS. Retiring a domain after a brand complaint, recovering the storage deposit on a domain that never sold, and clearing out fad domains nobody renewed. A logic realm cannot provide any of this, because the vault owns the trees and exposed no delete path — the only Remove call for a domain lived inside RegisterDomainRecord as the evict-and-replace step of re-registration.
IT DELIBERATELY WORKS ON A DOMAIN WITH LIVE NAMES UNDER IT. The case this exists for is a demand to remove a domain entirely, where waiting for names to lapse is not an option. Compensating the holders is handled off chain and is deliberately not a contract concern.
WHERE THIS DEPARTS FROM THE SPEC, stated plainly because it is a real difference: MAINNET.md §3d asks for all of it "in one call". It is bounded instead, deleting at most maxDeletePerCall names per call and reporting whether it finished. An unbounded loop over a tree that may hold any number of names is a transaction that stops fitting in a block once a domain gets popular — which would mean the domains most likely to attract a complaint are exactly the ones that could never be deleted, in a realm that can never be fixed. The caller repeats until `done`; for any domain of realistic size that is one call, so the no-preconditions requirement is preserved. What is NOT preserved is atomicity: a multi-call deletion is visible half-done between calls. Freeze the domain first if that matters, so nothing new lands under it meanwhile. Not Pause(): a paused registry deletes nothing at all, since this passes the same trust gate as every write.
Calling it on a domain that does not exist is a no-op reporting done, so a repeat loop terminates cleanly rather than panicking on the call that follows the last one.
Disarm deletes the claim window, the granter and the keeper at once. Admin, recovery or guardian, as a direct wallet call; it works while the registry is paused and with every logic realm revoked.
EvictionPending reports a pending rotation of the admin that recovery proposed while the two roles are separate keys: recovery replacing an admin it no longer trusts. See the note above pendingOp.
ExecutePending applies a matured change. Callable by anyone on purpose: execution must not depend on a particular key still being available, since key unavailability is one of the things being recovered from. It only applies what was already proposed and survived its window.
ExportAccounts pages through the per-address store as raw keys `<address>:<field>`; pair with GetAccount for each value. Field names are comma-free by construction (see assertAccountKey).
ExportDomainExtraKeys / ExportNameExtraKeys list a record's extension keys so a snapshot can capture data this realm has no schema for. Keys are comma-free by construction (see assertExtraKey), so a CSV listing is unambiguous.
ExportDomains pages through every domain label.
ExportGlobalKeys lists the package-level extension slot's keys; pair with GetGlobal for values.
A previous holder's keys are not exported. A snapshot that captured them would carry the seller's payment addresses into a restore as the buyer's.
ExportNames pages through every name in the registry, as raw storage keys (`domain*label`). Pass an empty domainLabel to walk the whole tree, or a domain to walk just that domain's span.
ExportPrimary pages through the reverse-resolution table as owner addresses; pair with GetPrimary to read each value.
FreshenNameRecord applies freshen and writes nothing else: the way a holder who bought a name starts its clock (Name.since) without having to invent a profile edit. For the current holder, freshen destroys only data every read already hides.
GetAccount reads one field of an address's own record, or "" when it has none. See `accounts` in types.gno.
GetAction reports a pending action: when it matures, when it expires, and who proposed it. All zero when nothing by that id is pending.
1func GetDomainInfo(label string) (owner address, registered, expires, streak int64, frozen bool)GetDomainInfo / GetNameInfo return a whole record in one call, so a view realm can render a page without six round trips. Timestamps come back as unix SECONDS. They used to be time.Time, which vm/qeval rendered using gno's internal epoch, so every consumer had to know to subtract 62135596800 — our own frontend did exactly that. Returning unix seconds stops leaking that offset into every integration, and it is what the records now hold anyway.
`streak` is computed from the registration date rather than read from a stored field. See streakOf.
GetNameExtra answers EMPTY for data written in a previous ownership epoch.
This is the read half of the lazy wipe. A sale that settles on nsnft — through our marketplace or anybody else's — never calls this realm, so nothing here can clear the seller's payment addresses at the moment it happens. What this CAN do is refuse to repeat them: the instant the ledger's epoch moves, every extra on the name stops answering, for every reader, before anybody has written anything.
The write half, which destroys the stale data rather than hiding it, is freshen in write.gno. Both halves are needed: hiding alone is what the first attempt shipped, and the buyer's first edit brought the seller's Bitcoin address back as the buyer's.
GetNameHeldSince is when the current holder's holding began, in unix seconds — or 0 when the ledger has moved on since this registry last saw the record (a sale it has not yet observed). It can understate a holding, never overstate one. See Name.since.
1func GetNameInfo(label, domainLabel string) (owner address, registered, expires, streak int64, frozen, paid bool)GetNameInfo keeps its six-value shape so every existing reader — nslogic, nsview, nsskin, nsvote, the site, both snapshot tools — goes on parsing it unchanged. Two of the six are now answered differently:
1owner is read from nsnft, never stored here. The snapshot tools record
2 whatever this returns, so they now record the real owner — the
3 restore-hands-sold-names-back bug the first attempt had cannot
4 happen, because there is no stale copy for them to read.
5
6paid replaces isNFT and means exactly one thing: the name is NOT
7 held as an un-upgraded free-tier record. It is deliberately not
8 "transferable" — a frozen paid name is locked too, and reading
9 the lock would have made a frozen paid name look free, which is
10 precisely the proxy bug that let free renewals renew paid names.
GetNameTTL / GetDomainTTL report how long a resolver may cache an answer about this record, in seconds, where 0 MEANS NEVER CACHE — the ENS registry's rule, deliberately, so integrators need not read anything of ours to handle it correctly. A TTL written for a previous holder is not this holder's cache policy. A stale one reads as 0 — never cache — which is the safe answer during exactly the window after a sale when a cached answer pays the seller.
1func GetPendingDetail(kind string) (addr address, seconds int64, executableAt time.Time, protectorRole string, by address)GetPendingDetail is GetPending with the rest of the proposal: the delay a "delay" proposal would install, in seconds — which GetPending could not show, so "set the delay to zero" and "raise it" looked identical — and who proposed it.
GetPrimary is a raw pointer lookup. It reports what an address last SELECTED, not what it still owns — callers presenting this as reverse resolution must re-check ownership (see nslogic.ResolveAddress).
GrantVoided reports whether a grant made at unix second `at` falls inside a void recovery declared.
GrantVoids lists the void windows, "from-to" comma-separated, oldest first.
IsApprovedFor is the question a marketplace actually has: may this operator move this token, by either route?
IsNamePaid reports that a name is not held as an un-upgraded free-tier record. See GetNameInfo on why this is not "transferable".
ListActions pages through pending action ids, ONE PER LINE: an id may contain commas (a price rule does), never a newline.
ListNames and CountNames scan only the key span belonging to `domainLabel` (see domainRange in auth.gno), costing O(names in that domain) rather than O(every name in the registry).
Pause halts every registry mutation, timelocked actions included. Reads are unaffected, so names stay resolvable while writes are frozen.
It deliberately does NOT gate ExecutePending: governance must keep working during an incident — recovery's counter-rotation above all.
A pause also drops a pending admin-proposed unpause, so the guardian bot can hold the registry paused against a stolen admin key simply by pausing again whenever it sees UnpauseProposed.
ProposeAction starts the clock on one admin action. Proposing an id already pending restarts its clock: the latest proposal is the one that must survive its window.
ProposeAdminTransfer starts an admin rotation. When recovery initiates it, only recovery may cancel it — the admin being replaced gets no veto over their own replacement.
ProposeRecoveryTransfer is the mirror image: when admin initiates it, only admin may cancel it, so a compromised recovery key cannot block its own replacement either.
ProposeTimelockDelay is bounded on BOTH sides. An unbounded delay would let a single execution stamp every future proposal with an unreachable maturity, permanently disabling governance in an unredeployable realm.
ProposeUnpause is the admin's way back from a pause: a proposal like any grant of power, executable by anyone once it matures.
QuarantineUntil is the unix second giveaways may run again, 0 for none.
Quarantined reports whether a quarantine holds right now.
ReassignDomainRecord moves a domain to a new owner and leaves its configuration exactly where it was.
This is the durable fix nslogic.ReassignDomain's comment was waiting for. The old path moved a domain through the name-transfer machinery, whose wipe exists to clear a SELLER'S payment addresses — and so it silently reopened closed domains, dropped their pricing to the default and released their term caps, during the key-compromise recovery it exists for. A domain's extras are its settings, not a previous holder's data, and there is no previous holder: domains never reach a third party.
TTL resets, because it is the one field that is a caching promise made by whoever held the record.
1func RegisterNameRecord(cur realm, label, domainLabel string, owner address, now int64, paid bool) errorRegisterNameRecord creates `label*domain` and mints its token in nsnft.
`paid` false registers a FREE-TIER name: the token is minted with the free hold already set, so there is never an instant in which it could be approved and sold before a lock arrived. There is no longer any such thing as a name without a token — see Name.
RECLAIM burns the previous holder's token first. The owner is read from the ledger, never from the record, because the record has no owner.
AN ORPHAN TOKEN IS REFUSED. A token that exists with no record behind it means a registry swap is mid-migration or something has gone wrong; either way, registering over it would mint a second claim to a name somebody already holds. RestoreNameRecord is the door for adopting one.
The real human-facing view moved OUT of this realm into `nsview` (2026-08-21), after an adversarial review found three separate defects in the old Render() — unescaped profile-field injection into the markdown, an O(domains × names) home page any user could inflate into a permanent DoS, and links that 404'd — none of which could ever be fixed, because this realm is deployed once and never replaced.
The lesson generalised: "it's only display code, it has no authorization surface" was the wrong test for what belongs in an immutable realm. The right test is whether a bug in it would be permanent. Display code changes far more often than storage schemas do, so it belongs in something swappable.
What remains here is deliberately a fixed string: no user-authored data, no iteration over state, nothing that can be wrong later.
1func RestoreNameRecord(cur realm, label, domainLabel string, owner address, registered int64, paid bool) errorRestoreNameRecord is the migration door: records move, tokens do not.
A registry swapped in behind the same nsnft finds tokens that already exist. This writes the record for one and ADOPTS its token, or mints one when the ledger has none. It never moves a token: a snapshot must never take a name from its current holder.
WHEN THE LEDGER DISAGREES WITH THE SNAPSHOT, IT WINS. This used to refuse, and an audit showed where refusing leads: a name sold between the snapshot and the restore panics the batch, gets dropped to finish the rest, and the window seals — leaving the buyer's token with no record behind it, unable to be renewed, resolved or registered, forever. So the record is written for whoever holds the token now, and the snapshot's profile data is NOT replayable onto it (restoredFor stays empty), because it was the seller's.
Gated like every write, and nslogic only reaches it through the restore window, which is one-shot.
RevokeTrustedLogic is INSTANT and available to the guardian, because cutting off a misbehaving logic realm is the emergency action that must never wait on a timelock.
SetAccountLimitsRecord bounds the per-address store. Lowering a cap never deletes anything; it only refuses the next write that exceeds it.
SetAccountRecord writes one field of an address's own record: a credit balance, a counter, whatever a logic realm keeps per wallet. An empty value deletes the field, so its storage can be reclaimed.
Bounded like the extras, with caps of its own: the field name, the value, and how many fields one address may hold. The fields-per-address cap is what keeps any single wallet's footprint small, whoever pays for it.
SetActionWindow sets how long a matured action stays executable, between an hour and ninety days.
SetDomainExtraRecord writes one arbitrary key on a domain — pricing, or whatever a future logic realm needs. An empty value deletes the key, so storage can be reclaimed.
SetDomainTTLRecord sets how long a resolver may cache answers about this domain, in seconds, where 0 means never cache. See Domain.ttl.
SetGlobalEntryLimitRecord sets how many keys globals may hold, apart from the per-record extras cap. See maxGlobalEntries.
SetGuardian takes the RECOVERY key, to seat a guardian as much as to replace one.
Replacing took recovery from the start: otherwise the first move of anyone who steals the admin key is to point the guardian at themselves, disarming the emergency stop before you can use it. Seating an EMPTY seat used to be the admin's too, and that was the same hole: a stolen admin key seated its own bot, and after its eviction that bot could still pause and revoke, removable only by recovery (re-audit 2026-10-16, L-4). The deploy seats the guardian while its key still holds recovery.
SetMaxDeletePerCallRecord tunes how much work one DeleteDomainRecord call will do. See maxDeletePerCall in types.gno.
SetMaxTimelockDelay raises or lowers the ceiling on timelockDelay. Bounded by absoluteMaxTimelock, and it cannot be set below the delay currently in force.
SetNameExpiryRecord sets an expiry directly. Backs the admin free-extension lever (SPEC.md §19) and any future term mechanic; the logic realm decides who may call it.
SetNameExtraRecord writes one arbitrary key on a name. This is where profile fields, cross-chain addresses, management-key hashes and tier markers live. An empty value deletes the key.
SetNameFrozen sets the record's frozen bit AND the matching hold on the token, in the same call.
The record bit alone used to be the whole mechanism, and it bound nothing outside our own transfer wrappers — a disputed name could be moved on any marketplace that called the token directly. The hold in nsnft binds every path there is.
SetNameTTLRecord sets how long a resolver may cache answers about this name, in seconds, where 0 means never cache. See Domain.ttl for why this exists and why the owner rather than the registry sets it.
SetQuarantine holds every giveaway shut until `until` (unix seconds), at most maxQuarantine ahead; 0 lifts it. Recovery only.
SetReservedGlobalRecord is the restore machinery's own door to the keys above. Same trust gate; separate name, so a general-purpose setter can never reach them by accident.
SetSuccessor records where this registry moved, if it ever has to be abandoned. Pair it with Pause() so the retired registry cannot drift away from the snapshot its successor was built from.
The lifecycle setters refuse the two values that would break the lifecycle for everybody: a term of zero or less, and a negative grace period, which made live names reclaimable early (audit L-2). nslogic bounds both too; a future logic realm need not.
Sizes let an operator verify a snapshot is complete rather than trusting that pagination terminated correctly.
TokenURI returns a pointer at whichever view realm renders the art. nsnft has its own URI base for external marketplaces; both are set to the same view realm at wiring time.
Unpause restarts the registry at once, and only RECOVERY may do that.
It used to be open to the admin too, which made a pause useless against the key most likely to be stolen: the admin could unpause in the same multi-message transaction as whatever it did next, so a pause stopped only writers that did not hold admin. An audit showed it. The admin can still unpause — ProposeUnpause, then the timelock — so a false alarm costs a delay rather than the cold key; recovery can cancel that proposal, and any new pause drops it. The guardian is excluded as before: a hot key that can stop the system must not also be able to restart it.
UpgradeFreeRecord lifts the free-tier hold — the paid upgrade.
Preserves the dates: this is not a re-registration.
`newOwner` exists because the common case is somebody who decided to sell and, in the same moment, wants the name in a wallet they control rather than the key they were given. That move is a burn and a mint here, because a held token cannot be transferred by design.
THE PROFILE DOES NOT GO WITH IT, and it used to. The move re-stamped the old data to the new epoch and gave the recipient a reverse pointer — and `newOwner` can be ANY address. An audit spelled out the attack: fill a free name's profile with your own pay addresses, pay list price to upgrade it into a victim's wallet, and the victim's name now asks people to pay you, their address reverse-resolves to it, and their own later edits do not clear it because the epoch is already current. So the data is left in the previous epoch, where every read hides it and the next write destroys it, and the recipient gets no pointer they never chose. A person moving their own name to a new wallet re-enters their profile once; that is the price of the move not being usable against somebody else.
Refused for a frozen name. Upgrading must never be the way a disputed name gets out of its hold.
VoidGrantsSince voids every grant made at or after `since`, up to now. Recovery only. `since` may be 0 (everything so far) and may not be in the future. The window joins the ones already in force; past maxGrantVoids the two oldest merge, which can only void more, never less.
1type Domain struct {
2 owner address
3 registered int64 // unix seconds
4 expires int64 // unix seconds
5 frozen bool
6
7 // How long a resolver may cache an answer about this record, in
8 // seconds, where 0 MEANS NEVER CACHE. Copied deliberately from the
9 // ENS registry, which stores exactly this field with exactly this
10 // rule, so integrators already know what to do with it.
11 //
12 // It exists because a name can be sold at any moment, and a cached
13 // resolution then sends funds to the previous owner. A blanket
14 // policy would be wrong for somebody receiving payments and
15 // wastefully strict for somebody using a name as a handle, so the
16 // person who knows the risk sets it. Typed rather than an `extra`
17 // key because an avl entry costs an order of magnitude more than
18 // eight bytes, and because integrators read it.
19 ttl int64
20
21 // Pricing, and anything a future logic realm needs. Not typed
22 // fields, because pricing is policy and policy belongs in the
23 // swappable half — the vault only has to remember the values.
24 extra avl.Tree
25}Timestamps are unix SECONDS, not time.Time, and that is the single biggest storage decision in this file. Measured on three throwaway realms (MAINNET.md §3d-i): two time.Time fields were 70% of a record, because each carries an unused nanosecond field and a *Location pointer for a value that is always UTC. Switching to int64 takes a real 7,385-byte domain record to 6,246 bytes on its own, and turns every lifecycle comparison into integer arithmetic.
Nothing here stores a label. The AVL key already holds it — `label` for a domain, `domain*label` for a name — so a copy inside the record was paying storage forever to duplicate the thing used to find it.
1type Name struct {
2 registered int64
3 expires int64
4 frozen bool
5 ttl int64
6
7 /* THE OWNERSHIP EPOCH this record's profile data belongs to.
8
9 nsnft moves a token's epoch on every mint and transfer. When the two
10 disagree, the extras and TTL here were written for a previous holder
11 — possibly through a sale on a marketplace this realm never hears
12 about — and they are treated as absent on read and destroyed before
13 the next write. See freshen in write.gno. */
14 epoch int64
15
16 /* WHEN THE CURRENT HOLDING BEGAN, as far as this registry has seen.
17
18 Unix seconds, stamped together with `epoch`: at registration, at the
19 paid upgrade, at a restore, and at the first write after a transfer
20 (freshen). Meaningful only while `epoch` matches the ledger. A sale
21 that settles on nsnft moves the epoch without telling this realm, so
22 a buyer's clock starts at their first write here and never earlier:
23 it can understate how long somebody has held a name, never overstate
24 it. The paid upgrade restarts it, because a held free-tier name and a
25 sellable one are not the same holding.
26
27 A fact, not a policy: nslogic decides what it is good for (a
28 referral code earns only once its holder has kept a paid name for
29 thirty days). See GetNameHeldSince. */
30 since int64
31
32 /* WHO A RESTORE WROTE THIS RECORD FOR, or empty if no restore did.
33
34 nslogic.RestoreNameField replays profile data — pay addresses among
35 it — and may do so only onto a record a restore created, and only
36 while the ledger still says the snapshot's owner holds it. Without
37 this, a name sold between the snapshot and the restore had the
38 seller's pay addresses written onto the buyer's name, and any name a
39 user registered during the restore window could have its pay fields
40 rewritten with the admin key alone. */
41 restoredFor address
42
43 // Profile fields, cross-chain addresses, management-key hashes,
44 // referrer — see the package comment. Only meaningful while `epoch`
45 // matches the ledger.
46 extra avl.Tree
47}Name has NO OWNER FIELD, and that absence is the whole design.
The first attempt at moving ownership into a token realm kept a copy of the owner here, and every bug it had followed from the copy: renewal checked the copy and refused the real owner after a sale, snapshots recorded the copy and would have handed sold names back to their sellers on restore. There is one owner and it lives in nsnft. Every read asks it.
It also has no isNFT flag. Every name is a token; a free-tier name is a token with a hold on it (lockBitFree). Four separate places used to read "has no token" as "is free", and deleting the flag deletes that class of bug rather than fixing four instances of it.