Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

types.gno

11.56 Kb · 269 lines
  1// Package nsdata is the registry — the records behind every name.
  2//
  3// IT IS REPLACEABLE NOW, and that changes what every comment below means.
  4//
  5// Until this generation the vault held ownership as well as records, which
  6// made it the one realm that could never be redeployed: a "new nsdata"
  7// was an empty registry and everybody's names with it. So everything here
  8// was written as if it would ship forever — no constants, no policy, no
  9// display code — and the comments say "can never be replaced" throughout.
 10//
 11// Ownership has moved to nsnft, the small frozen token realm. This realm
 12// keeps records — dates, the frozen bit, TTL, profile data, reverse
 13// resolution, pricing globals, domains — and reads every owner from the
 14// ledger rather than storing one. Replacing it is a new package plus one
 15// Trust call on nsnft; no token moves, no holder notices. The integration
 16// address the outside world keeps is nsnft's, not this one's.
 17//
 18// The two old rules still hold, for a better reason than before:
 19//
 20//  1. NOTHING IS A CONSTANT. Term length, grace period, size caps — all
 21//     settable. Not because a mistake would be permanent any more, but
 22//     because a registry that needs a redeploy to change a number is a
 23//     registry that gets redeployed carelessly.
 24//
 25//  2. THE REGISTRY STORES, IT DOES NOT DECIDE. Pricing math, validation,
 26//     what a profile field means — all of it lives in nslogic. Anything
 27//     this realm "knows" is a rule that needs a migration to change.
 28//
 29// Rule 2 is why Domain and Name carry an `extra` tree instead of a
 30// growing list of typed fields.
 31package nsdata
 32
 33import (
 34	"time"
 35
 36	"gno.land/p/nt/avl/v0"
 37)
 38
 39// Timestamps are unix SECONDS, not time.Time, and that is the single
 40// biggest storage decision in this file. Measured on three throwaway
 41// realms (MAINNET.md §3d-i): two time.Time fields were 70% of a record,
 42// because each carries an unused nanosecond field and a *Location
 43// pointer for a value that is always UTC. Switching to int64 takes a
 44// real 7,385-byte domain record to 6,246 bytes on its own, and turns
 45// every lifecycle comparison into integer arithmetic.
 46//
 47// Nothing here stores a label. The AVL key already holds it — `label`
 48// for a domain, `domain*label` for a name — so a copy inside the record
 49// was paying storage forever to duplicate the thing used to find it.
 50type Domain struct {
 51	owner      address
 52	registered int64 // unix seconds
 53	expires    int64 // unix seconds
 54	frozen     bool
 55
 56	// How long a resolver may cache an answer about this record, in
 57	// seconds, where 0 MEANS NEVER CACHE. Copied deliberately from the
 58	// ENS registry, which stores exactly this field with exactly this
 59	// rule, so integrators already know what to do with it.
 60	//
 61	// It exists because a name can be sold at any moment, and a cached
 62	// resolution then sends funds to the previous owner. A blanket
 63	// policy would be wrong for somebody receiving payments and
 64	// wastefully strict for somebody using a name as a handle, so the
 65	// person who knows the risk sets it. Typed rather than an `extra`
 66	// key because an avl entry costs an order of magnitude more than
 67	// eight bytes, and because integrators read it.
 68	ttl int64
 69
 70	// Pricing, and anything a future logic realm needs. Not typed
 71	// fields, because pricing is policy and policy belongs in the
 72	// swappable half — the vault only has to remember the values.
 73	extra avl.Tree
 74}
 75
 76/*
 77Name has NO OWNER FIELD, and that absence is the whole design.
 78
 79The first attempt at moving ownership into a token realm kept a copy of
 80the owner here, and every bug it had followed from the copy: renewal
 81checked the copy and refused the real owner after a sale, snapshots
 82recorded the copy and would have handed sold names back to their sellers
 83on restore. There is one owner and it lives in nsnft. Every read asks it.
 84
 85It also has no isNFT flag. Every name is a token; a free-tier name is a
 86token with a hold on it (lockBitFree). Four separate places used to read
 87"has no token" as "is free", and deleting the flag deletes that class of
 88bug rather than fixing four instances of it.
 89*/
 90type Name struct {
 91	registered int64
 92	expires    int64
 93	frozen     bool
 94	ttl        int64
 95
 96	/* THE OWNERSHIP EPOCH this record's profile data belongs to.
 97
 98	   nsnft moves a token's epoch on every mint and transfer. When the two
 99	   disagree, the extras and TTL here were written for a previous holder
100	   — possibly through a sale on a marketplace this realm never hears
101	   about — and they are treated as absent on read and destroyed before
102	   the next write. See freshen in write.gno. */
103	epoch int64
104
105	/* WHEN THE CURRENT HOLDING BEGAN, as far as this registry has seen.
106
107	   Unix seconds, stamped together with `epoch`: at registration, at the
108	   paid upgrade, at a restore, and at the first write after a transfer
109	   (freshen). Meaningful only while `epoch` matches the ledger. A sale
110	   that settles on nsnft moves the epoch without telling this realm, so
111	   a buyer's clock starts at their first write here and never earlier:
112	   it can understate how long somebody has held a name, never overstate
113	   it. The paid upgrade restarts it, because a held free-tier name and a
114	   sellable one are not the same holding.
115
116	   A fact, not a policy: nslogic decides what it is good for (a
117	   referral code earns only once its holder has kept a paid name for
118	   thirty days). See GetNameHeldSince. */
119	since int64
120
121	/* WHO A RESTORE WROTE THIS RECORD FOR, or empty if no restore did.
122
123	   nslogic.RestoreNameField replays profile data — pay addresses among
124	   it — and may do so only onto a record a restore created, and only
125	   while the ledger still says the snapshot's owner holds it. Without
126	   this, a name sold between the snapshot and the restore had the
127	   seller's pay addresses written onto the buyer's name, and any name a
128	   user registered during the restore window could have its pay fields
129	   rewritten with the admin key alone. */
130	restoredFor address
131
132	// Profile fields, cross-chain addresses, management-key hashes,
133	// referrer — see the package comment. Only meaningful while `epoch`
134	// matches the ledger.
135	extra avl.Tree
136}
137
138/*
139THE REGISTRY'S HOLD BITS on a token in nsnft. nsnft stores them and
140
141	never interprets them; the vocabulary is this realm's.
142
143	Separate bits so the two holds cannot undo each other: unfreezing a
144	free-tier name must not make it sellable, and upgrading a frozen name
145	must not unfreeze it.
146*/
147const (
148	lockBitFree   = 0 // free tier, not yet paid for: not sellable
149	lockBitFrozen = 1 // frozen by an admin: not movable while disputed
150)
151
152// There is deliberately no `streak` field. It used to be stored and
153// incremented on renewal, which breaks the moment multi-year
154// registration exists: prepaying five years bumps it five times, so a
155// name minted this morning would render "5 years held". A streak is
156// meant to say how long somebody stuck around, and buying time is not
157// the same as spending it. It is now derived from `registered` — see
158// streakOf in auth.gno — which is always truthful, cannot be bought,
159// resets naturally on reclaim, and removes a field instead of adding
160// one.
161
162var (
163	domains avl.Tree
164	names   avl.Tree
165	primary avl.Tree
166
167	// Package-level extension slot, same reasoning as Domain/Name.extra:
168	// somewhere to put global CONFIGURATION nobody has thought of yet
169	// without needing a new package var. Not per-wallet state: that is
170	// `accounts`, below.
171	globals avl.Tree
172
173	/* PER-ADDRESS STATE, in a tree of its own.
174
175	   Credits for a free registration, and per-wallet counters such as
176	   referrals, are about an ADDRESS, and there was nowhere to put them.
177	   globals caps a key at 32 bytes and an address alone is 40, and its
178	   entry cap was shared with every name's and domain's extras, so each
179	   new wallet would have competed with the price list for the same 24
180	   slots. An audit found the referral counter had never once been
181	   written, for exactly that reason, and that every referred
182	   registration reverted because of it.
183
184	   Keyed address + ":" + field (see accountKey), so one wallet's fields
185	   are one contiguous span. Bounded three ways — field name, value and
186	   fields per address — so a wallet's footprint is small whatever a
187	   logic realm writes. Written only by trusted logic, like every record;
188	   what a field MEANS is the logic realm's business. */
189	accounts avl.Tree
190
191	admin address // the vault door — see governance.gno for the full role model
192
193	// The set of realms currently allowed to call mutations. A set, not a
194	// single address, because the project will plausibly grow more than
195	// one writer (a free-tier realm, a custodial relayer) and a single
196	// slot would force them all into one realm.
197	trustedLogicSet avl.Tree
198
199	// -- config: every value below is settable, none is a const --
200	treasury address
201
202	// Seconds, matching the record timestamps. These were time.Duration,
203	// which meant every lifecycle comparison converted, and every read
204	// through vm/qeval printed a nanosecond count that each caller had to
205	// know to divide.
206	termLength  int64 = 365 * 24 * 60 * 60
207	gracePeriod int64 = 90 * 24 * 60 * 60
208
209	labelPattern = `^[a-z0-9]+(-[a-z0-9]+)*$`
210	minLabelLen  = 1
211	maxLabelLen  = 63
212
213	// Bounds on the extension slots. Storage written here is permanent
214	// and paid for by the realm, so unbounded strings are griefing.
215	maxExtraKeyLen   = 32
216	maxExtraValueLen = 512
217	maxExtraEntries  = 24
218
219	// globals' OWN entry cap. It used to be maxExtraEntries, shared with
220	// every record, and the logic realm's fixed keys alone (prices, rate,
221	// fee, free tier, eight premium pages, eight block-list chunks,
222	// referral rules, restore state) number more than 24 — so raising it
223	// meant loosening every name and domain by the same factor.
224	maxGlobalEntries = 64
225
226	// Bounds on the per-address store. Small on purpose: a field is a
227	// counter or a short tuple, and these are paid for by whoever's
228	// transaction writes them.
229	maxAccountKeyLen   = 16
230	maxAccountValueLen = 128
231	maxAccountEntries  = 8
232
233	// Upper bound on how many name records a single DeleteDomainRecord
234	// call will destroy. Deletion is unbounded work over a domain that
235	// may hold any number of names, and an unbounded loop in a realm
236	// that cannot be redeployed is a domain that can never be deleted
237	// at all once it grows past one block's gas. Settable, because the
238	// right number depends on chain parameters nobody knows yet.
239	maxDeletePerCall = 100
240
241	// Prefix for TokenURI. Points at whichever view realm currently
242	// renders the artwork; settable so the art can be replaced without
243	// touching this realm. See nft.gno.
244	tokenURIBase = "/r/demo/nsview:token/"
245)
246
247func init(cur realm) {
248	deployer := cur.Previous().Address()
249	admin = deployer
250	recovery = deployer
251	treasury = deployer
252
253	// THE REGISTRY ARRIVES CONFIGURED. Domains, prices, referral tiers and
254	// lifecycle settings are baked into genesis_data.gno and written here,
255	// so a fresh chain is usable the moment this package lands — no trust
256	// grant and no admin call needed, because this realm owns the state
257	// it is writing. See genesis.gno.
258	applyGenesis(deployer, time.Now().Unix())
259	// The trusted-logic set starts EMPTY and timelockDelay starts at
260	// zero: deploy nsdata, deploy nslogic, then propose+execute the
261	// grant. No mutation is callable until that happens.
262	//
263	// FIRST OPERATIONAL STEP AFTER DEPLOY, before anything of value
264	// exists here: move `recovery` to a key held separately from
265	// `admin` (ideally cold, ideally a multisig), then raise
266	// timelockDelay. Until that is done both roles are the same key and
267	// the whole recovery design is inert — it protects against one key
268	// being lost or stolen, not against one key being both.
269}