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}