store.gno
9.38 Kb · 319 lines
1package users
2
3import (
4 "chain"
5 "chain/runtime"
6 "regexp"
7
8 "gno.land/p/nt/bptree/v0"
9 "gno.land/p/nt/ufmt/v0"
10)
11
12var (
13 nameStore = bptree.NewBPTree32() // name/aliases > *UserData
14 addressStore = bptree.NewBPTree32() // address > *UserData
15
16 reAddressLookalike = regexp.MustCompile(`^g1[a-z0-9]{20,38}$`)
17
18 // reName mirrors gno's package-name shape (gnovm/pkg/gnolang/mempackage.go
19 // `Re_name`): start with a lowercase letter, optional alphanumeric body,
20 // then any number of (separator + alphanumeric run) — so single hyphens
21 // or underscores are allowed BETWEEN alphanumerics, but consecutive
22 // separators (`--`, `__`, `-_`, `_-`) are rejected, and so are leading
23 // or trailing separators. Lowercase-only — closes the case-confusable
24 // squatting concern (Alice vs alice were two distinct names under the
25 // previous case-preserving regex). Length cap of 64 enforced separately
26 // in validateName.
27 reName = regexp.MustCompile(`^[a-z][a-z0-9]*([_-][a-z0-9]+)*$`)
28)
29
30const maxNameLen = 64
31
32const (
33 RegisterUserEvent = "Registered"
34 UpdateNameEvent = "Updated"
35 DeleteUserEvent = "Deleted"
36)
37
38type UserData struct {
39 addr address
40 username string // contains the latest name of a user
41 deleted bool
42}
43
44func (u UserData) Name() string {
45 return u.username
46}
47
48func (u UserData) Addr() address {
49 return u.addr
50}
51
52// IsDeleted reports whether this user record is missing or marked deleted.
53// A nil receiver returns true — "the user does not exist" is semantically
54// indistinguishable from "the user was deleted" for callers that need to
55// gate further state changes. This lets call sites collapse the nil check
56// and the deleted check into a single guard:
57//
58// if u.IsDeleted() {
59// return ErrUserNotExistOrDeleted
60// }
61func (u *UserData) IsDeleted() bool {
62 if u == nil {
63 return true
64 }
65 return u.deleted
66}
67
68// RenderLink provides a render link to the user page on gnoweb
69// `linkText` is optional
70func (u UserData) RenderLink(linkText string) string {
71 if linkText == "" {
72 return ufmt.Sprintf("[@%s](/u/%s)", u.username, u.username)
73 }
74
75 return ufmt.Sprintf("[%s](/u/%s)", linkText, u.username)
76}
77
78// registerUser adds a new user to the system without checking controllers.
79// The ignoreCanonical flag suppresses ErrCanonicalCollision; the canonical
80// store is written either way (decision #14: later-wins on bypass).
81func registerUser(cur realm, name string, address_XXX address, ignoreCanonical bool) error {
82 // Validate name
83 if err := validateName(name); err != nil {
84 return err
85 }
86
87 // Validate address
88 if !address_XXX.IsValid() {
89 return ErrInvalidAddress
90 }
91
92 // Check if name is taken (exact-string match precedes canonical check)
93 if nameStore.Has(name) {
94 return ErrNameTaken
95 }
96
97 canonical := Canonicalize(name)
98 if !ignoreCanonical {
99 if canonicalStore.Has(canonical) {
100 return ErrCanonicalCollision
101 }
102 }
103
104 raw := addressStore.Get(address_XXX.String())
105 if raw != nil {
106 // Cannot re-register after deletion
107 if raw.(*UserData).IsDeleted() {
108 return ErrDeletedUser
109 }
110
111 // For a second name, use UpdateName
112 return ErrAlreadyHasName
113 }
114
115 // Create UserData
116 data := &UserData{
117 addr: address_XXX,
118 username: name,
119 deleted: false,
120 }
121
122 // Set corresponding stores
123 nameStore.Set(name, data)
124 addressStore.Set(address_XXX.String(), data)
125 canonicalStore.Set(canonical, name)
126
127 chain.Emit(RegisterUserEvent,
128 "name", name,
129 "address", address_XXX.String(),
130 )
131 return nil
132}
133
134// RegisterUser adds a new user to the system. Enforces canonical-
135// collision detection: a name whose Canonicalize-form matches a prior
136// registration returns ErrCanonicalCollision.
137func RegisterUser(cur realm, name string, address_XXX address) error {
138 // IsCurrent before Previous, as UpdateName/Delete below already do. The
139 // file was inconsistent: those three checked it, these two did not.
140 if !cur.IsCurrent() {
141 return NewErrNotWhitelisted(0, cur.Previous())
142 }
143 // At genesis (height 0), allow any caller to register users.
144 // After genesis, only whitelisted controllers can register.
145 if runtime.ChainHeight() > 0 && !controllers.Has(cur.Previous().Address()) {
146 return NewErrNotWhitelisted(0, cur.Previous())
147 }
148
149 return registerUser(cur, name, address_XXX, false)
150}
151
152// RegisterUserIgnoreCanonical is the bypass path: same controller-
153// whitelist gate, but ErrCanonicalCollision is suppressed. The canonical
154// store is still written; a prior entry with the same canonical key is
155// silently overwritten (decision #14, later-wins). Use sparingly — names
156// registered here can canonical-collide with existing ones, weakening
157// confusable protection for everyone.
158func RegisterUserIgnoreCanonical(cur realm, name string, address_XXX address) error {
159 // IsCurrent before Previous, as UpdateName/Delete below already do. The
160 // file was inconsistent: those three checked it, these two did not.
161 if !cur.IsCurrent() {
162 return NewErrNotWhitelisted(0, cur.Previous())
163 }
164 if runtime.ChainHeight() > 0 && !controllers.Has(cur.Previous().Address()) {
165 return NewErrNotWhitelisted(0, cur.Previous())
166 }
167
168 return registerUser(cur, name, address_XXX, true)
169}
170
171// updateName adds a name that is associated with a specific address without
172// checking controllers. The ignoreCanonical flag suppresses
173// ErrCanonicalCollision; the canonical store is written either way (decision
174// #14: later-wins on bypass).
175//
176// All previous names are preserved and resolvable.
177// The new name is the default value returned for address lookups.
178func (u *UserData) updateName(newName string, ignoreCanonical bool) error {
179 // IsDeleted handles both branches: nil receiver (user never existed)
180 // AND a non-nil receiver whose .deleted is true (a controller cached
181 // the *UserData pointer before the user was deleted by a separate
182 // controller or governance proposal). Without the deleted-flag branch,
183 // nameStore.Set(newName, u) would insert an alias pointing at a
184 // deleted user — Has(newName) returns true forever but Resolve(newName)
185 // returns nil (Resolve* APIs filter deleted), so the name is squatted
186 // with no recovery path. (audit finding #3)
187 if u.IsDeleted() {
188 return ErrUserNotExistOrDeleted
189 }
190
191 // Validate name
192 if err := validateName(newName); err != nil {
193 return err
194 }
195
196 // Check if the requested Alias is already taken (exact-string match)
197 if nameStore.Has(newName) {
198 return ErrNameTaken
199 }
200
201 canonical := Canonicalize(newName)
202 if !ignoreCanonical {
203 // No self-collision filter (decision #15): even the user's OWN
204 // prior canonical claim blocks the rename. Prevents accumulating
205 // confusable aliases of one's own name through free renames. The
206 // only path to a self-confusable rename is DAO governance via
207 // ProposeUpdateName.
208 if canonicalStore.Has(canonical) {
209 return ErrCanonicalCollision
210 }
211 }
212
213 u.username = newName
214 nameStore.Set(newName, u)
215 canonicalStore.Set(canonical, newName)
216
217 chain.Emit(UpdateNameEvent,
218 "alias", newName,
219 "address", u.addr.String(),
220 )
221 return nil
222}
223
224// UpdateName adds a name that is associated with a specific address.
225// Enforces canonical-collision detection.
226// All previous names are preserved and resolvable.
227// The new name is the default value returned for address lookups.
228//
229// rlm is the cur of the caller's enclosing crossing function (passed as
230// data via the `_ int, rlm realm` non-crossing form). rlm.Address() is
231// the calling realm against which we authorize.
232func (u *UserData) UpdateName(_ int, rlm realm, newName string) error {
233 if !rlm.IsCurrent() {
234 return ErrInvalidRealm
235 }
236 if u.IsDeleted() {
237 return ErrUserNotExistOrDeleted
238 }
239
240 // Validate caller
241 if !controllers.Has(rlm.Address()) {
242 return NewErrNotWhitelisted(0, rlm)
243 }
244
245 return u.updateName(newName, false)
246}
247
248// UpdateNameIgnoreCanonical is the bypass path: same controller-
249// whitelist gate, but ErrCanonicalCollision is suppressed. The canonical
250// store is still written; a prior entry with the same canonical key is
251// silently overwritten (decision #14, later-wins).
252func (u *UserData) UpdateNameIgnoreCanonical(_ int, rlm realm, newName string) error {
253 if !rlm.IsCurrent() {
254 return ErrInvalidRealm
255 }
256 if u.IsDeleted() {
257 return ErrUserNotExistOrDeleted
258 }
259
260 if !controllers.Has(rlm.Address()) {
261 return NewErrNotWhitelisted(0, rlm)
262 }
263
264 return u.updateName(newName, true)
265}
266
267// delete marks a user and all their aliases as deleted without checking controllers.
268func (u *UserData) delete() error {
269 if u.IsDeleted() {
270 return ErrUserNotExistOrDeleted
271 }
272
273 u.deleted = true
274
275 chain.Emit(DeleteUserEvent, "address", u.addr.String())
276 return nil
277}
278
279// Delete marks a user and all their aliases as deleted.
280// rlm is the cur of the caller's enclosing crossing function; see UpdateName.
281func (u *UserData) Delete(_ int, rlm realm) error {
282 if !rlm.IsCurrent() {
283 return ErrInvalidRealm
284 }
285 if u.IsDeleted() {
286 return ErrUserNotExistOrDeleted
287 }
288
289 // Validate caller
290 if !controllers.Has(rlm.Address()) {
291 return NewErrNotWhitelisted(0, rlm)
292 }
293
294 return u.delete()
295}
296
297// Validate validates username and address passed in
298// Most of the validation is done in the controllers
299// This provides more flexibility down the line
300func validateName(username string) error {
301 if username == "" {
302 return ErrEmptyUsername
303 }
304
305 if len(username) > maxNameLen {
306 return ErrInvalidUsername
307 }
308
309 if !reName.MatchString(username) {
310 return ErrInvalidUsername
311 }
312
313 // Check if the username can be decoded or looks like a valid address
314 if address(username).IsValid() || reAddressLookalike.MatchString(username) {
315 return ErrNameLikeAddress
316 }
317
318 return nil
319}