auth.gno
7.51 Kb · 199 lines
1package nsdata
2
3import (
4 "chain"
5
6 "gno.land/r/nym-mnsrehearsal001/nsnft/v2"
7)
8
9// isFresh reports whether a name's profile data belongs to the token's
10// CURRENT ownership epoch. See Name.epoch and freshen.
11func isFresh(n *Name, tid string) bool { return freshAt(n, nsnft.Epoch(tid)) }
12
13// freshAt is isFresh with the ledger's answer passed in. See freshenTo.
14func freshAt(n *Name, epoch int64) bool { return n.epoch == epoch }
15
16// ownsNow reports whether `addr` holds the token right now, per the ledger.
17func ownsNow(addr address, tid string) bool {
18 return addr != "" && nsnft.OwnerOf(tid) == addr
19}
20
21// isValidNow / isReclaimable are pure integer math over `termLength` and
22// `gracePeriod`, both of which are settable — so the lifecycle rules
23// themselves can be retuned after deployment. All times are unix seconds.
24func isValidNow(expires, now int64) bool {
25 return now < expires
26}
27
28// isReclaimable compares the elapsed time with the grace period instead of
29// adding the grace period to the expiry. The sum wrapped: an expiry of
30// math.MaxInt64 — the natural "never expires" value — read as valid AND
31// reclaimable at once, and a stranger could take the name (audit L-2). A
32// name that has not expired is never reclaimable, whatever grace says.
33func isReclaimable(expires, now int64) bool {
34 if now < expires {
35 return false
36 }
37 return now-expires >= gracePeriod
38}
39
40// streakOf derives how many whole terms a holder has actually lived
41// through, replacing the counter that used to be stored on the record.
42//
43// The stored version counted RENEWAL CALLS, so it measured transactions
44// rather than time: prepaying five years bumped it five times on a name
45// registered that morning, and an earlier version could be run up
46// arbitrarily by calling renew in a loop within one block. Deriving it
47// from the registration date makes it unbuyable by construction — the
48// only way to increase it is for time to pass — and it resets on its own
49// when a lapsed name is reclaimed, because a reclaim writes a fresh
50// record with a new `registered`.
51//
52// termLength is settable and could legitimately be zero, which would
53// otherwise divide by zero in a realm that cannot be redeployed.
54func streakOf(registered, now int64) int64 {
55 if termLength <= 0 || now <= registered {
56 return 0
57 }
58 return (now - registered) / termLength
59}
60
61// assertIsTrustedLogic is the gate on every mutation function below.
62// cur.Previous().Address() resolves to the CALLING realm's own address
63// when invoked via cross(cur) from another realm — same idiom already
64// used for assertIsAdmin, just checking a different address. A stray
65// call from anywhere else (a different realm, a direct user tx trying
66// to call these directly) is rejected.
67func assertIsTrustedLogic(cur realm) {
68 // The pause check lives here rather than in each mutation so it can
69 // never be forgotten on a new one: every write in the realm passes
70 // through this gate.
71 if paused {
72 panic("nsdata: registry is paused")
73 }
74 // A WALLET IS NEVER LOGIC, even if somebody trusted one (audit L-4).
75 // The trust set takes any address, and a wallet granted by mistake —
76 // or "temporarily", for a migration — could call every write here
77 // directly, policy-free. A run script carries its signer's address, so
78 // it is refused too.
79 if prev := cur.Previous(); prev.IsUser() || prev.IsEphemeral() {
80 panic("nsdata: trusted logic is a deployed realm, never a wallet or a run script")
81 }
82 if trustedLogicSet.Get(cur.Previous().Address().String()) == nil {
83 panic("nsdata: restricted to a trusted logic realm")
84 }
85}
86
87// -- key encodings --
88//
89// Two DIFFERENT orderings, deliberately, and it matters which is which:
90//
91// storage key : domain + "*" + label (nameTreeKey)
92// token ID : label + "*" + domain (nameTokenID)
93//
94// The storage key is DOMAIN-first so that every per-domain query is a
95// RANGED scan over a contiguous key span instead of a full-tree scan
96// with a filter. Before this, ListNames/CountNames walked every name in
97// the registry to answer a question about one domain, which made the
98// gnoweb home page O(domains × names) — a permanent, unfixable DoS in an
99// immutable realm once the registry grew. Since this realm is deployed
100// exactly once, the key layout is a forever decision; it is made here.
101//
102// The token ID stays label-first because that is the user-facing
103// identity (`alice*meme`), it is what the NFT exposes to every external
104// tool, and it is what `primary` stores for reverse resolution.
105//
106// Neither a label nor a domain may contain "*" (enforced by
107// validateLabel in the logic realm), so both encodings split
108// unambiguously on the first "*".
109
110func nameTreeKey(label, domain string) string { return domain + "*" + label }
111
112func nameTokenID(label, domain string) string { return label + "*" + domain }
113
114// There is no domainTokenID any more: domains are records, not tokens.
115// See MIGRATION.md, second attempt, rule 7.
116
117// domainRange returns the [start, end) key bounds covering exactly the
118// names stored under `domain`. '+' (0x2B) is the next byte after '*'
119// (0x2A), so `domain+"+"` sorts immediately past every `domain+"*"+...`
120// key while still sorting before any longer domain (e.g. "meme+" falls
121// between "meme*zzz" and "memes*a").
122func domainRange(domain string) (start, end string) {
123 return domain + "*", domain + "+"
124}
125
126// accountKey is the per-address store's key: the address, a colon, the
127// field. A bech32 address never contains ":" and a field may not (see
128// assertAccountKey), so the split is unambiguous, and every field of one
129// address sorts into one span.
130func accountKey(owner address, field string) string { return owner.String() + ":" + field }
131
132// accountRange bounds exactly one address's fields. ';' (0x3B) is the byte
133// after ':' (0x3A), the same trick domainRange plays with '*' and '+'.
134func accountRange(owner address) (start, end string) {
135 s := owner.String()
136 return s + ":", s + ";"
137}
138
139// splitTokenID splits a `label*domain` TOKEN ID. Do not use it on a
140// storage key — see the ordering note above.
141func splitTokenID(tid string) (label, domain string) {
142 for i := 0; i < len(tid); i++ {
143 if tid[i] == '*' {
144 return tid[:i], tid[i+1:]
145 }
146 }
147 return tid, ""
148}
149
150// -- lookups --
151
152func mustGetDomain(label string) *Domain {
153 v := domains.Get(label)
154 if v == nil {
155 panic("nsdata: unknown domain: " + label)
156 }
157 return v.(*Domain)
158}
159
160func getDomain(label string) (*Domain, bool) {
161 v := domains.Get(label)
162 if v == nil {
163 return nil, false
164 }
165 return v.(*Domain), true
166}
167
168func mustGetName(label, domain string) *Name {
169 v := names.Get(nameTreeKey(label, domain))
170 if v == nil {
171 panic("nsdata: unknown name: " + nameTokenID(label, domain))
172 }
173 return v.(*Name)
174}
175
176func getName(label, domain string) (*Name, bool) {
177 v := names.Get(nameTreeKey(label, domain))
178 if v == nil {
179 return nil, false
180 }
181 return v.(*Name), true
182}
183
184// clearPrimaryIfPointingAt drops `owner`'s reverse-resolution pointer
185// when (and only when) it still points at `tid`. Called on every
186// ownership change — transfer and reclaim.
187//
188// Without this, `primary` was a write-only tree: it had a Set path and
189// no Remove path anywhere in the realm, so selling or losing a name left
190// the previous holder permanently reverse-resolving to a name somebody
191// else now owns. Reverse resolution asserting ownership that is no
192// longer true is the one thing a name service must never do.
193func clearPrimaryIfPointingAt(owner address, tid string) {
194 k := owner.String()
195 if v := primary.Get(k); v != nil && v.(string) == tid {
196 primary.Remove(k)
197 chain.Emit("PrimaryCleared", "owner", k, "tid", tid, "auto", "true")
198 }
199}