store.gno
8.98 Kb · 309 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 _, taken := canonicalStore.Get(canonical); taken {
100 return ErrCanonicalCollision
101 }
102 }
103
104 raw, ok := addressStore.Get(address_XXX.String())
105 if ok {
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 // At genesis (height 0), allow any caller to register users.
139 // After genesis, only whitelisted controllers can register.
140 if runtime.ChainHeight() > 0 && !controllers.Has(cur.Previous().Address()) {
141 return NewErrNotWhitelisted(0, cur.Previous())
142 }
143
144 return registerUser(cur, name, address_XXX, false)
145}
146
147// RegisterUserIgnoreCanonical is the bypass path: same controller-
148// whitelist gate, but ErrCanonicalCollision is suppressed. The canonical
149// store is still written; a prior entry with the same canonical key is
150// silently overwritten (decision #14, later-wins). Use sparingly — names
151// registered here can canonical-collide with existing ones, weakening
152// confusable protection for everyone.
153func RegisterUserIgnoreCanonical(cur realm, name string, address_XXX address) error {
154 if runtime.ChainHeight() > 0 && !controllers.Has(cur.Previous().Address()) {
155 return NewErrNotWhitelisted(0, cur.Previous())
156 }
157
158 return registerUser(cur, name, address_XXX, true)
159}
160
161// updateName adds a name that is associated with a specific address without
162// checking controllers. The ignoreCanonical flag suppresses
163// ErrCanonicalCollision; the canonical store is written either way (decision
164// #14: later-wins on bypass).
165//
166// All previous names are preserved and resolvable.
167// The new name is the default value returned for address lookups.
168func (u *UserData) updateName(newName string, ignoreCanonical bool) error {
169 // IsDeleted handles both branches: nil receiver (user never existed)
170 // AND a non-nil receiver whose .deleted is true (a controller cached
171 // the *UserData pointer before the user was deleted by a separate
172 // controller or governance proposal). Without the deleted-flag branch,
173 // nameStore.Set(newName, u) would insert an alias pointing at a
174 // deleted user — Has(newName) returns true forever but Resolve(newName)
175 // returns nil (Resolve* APIs filter deleted), so the name is squatted
176 // with no recovery path. (audit finding #3)
177 if u.IsDeleted() {
178 return ErrUserNotExistOrDeleted
179 }
180
181 // Validate name
182 if err := validateName(newName); err != nil {
183 return err
184 }
185
186 // Check if the requested Alias is already taken (exact-string match)
187 if nameStore.Has(newName) {
188 return ErrNameTaken
189 }
190
191 canonical := Canonicalize(newName)
192 if !ignoreCanonical {
193 // No self-collision filter (decision #15): even the user's OWN
194 // prior canonical claim blocks the rename. Prevents accumulating
195 // confusable aliases of one's own name through free renames. The
196 // only path to a self-confusable rename is DAO governance via
197 // ProposeUpdateName.
198 if _, taken := canonicalStore.Get(canonical); taken {
199 return ErrCanonicalCollision
200 }
201 }
202
203 u.username = newName
204 nameStore.Set(newName, u)
205 canonicalStore.Set(canonical, newName)
206
207 chain.Emit(UpdateNameEvent,
208 "alias", newName,
209 "address", u.addr.String(),
210 )
211 return nil
212}
213
214// UpdateName adds a name that is associated with a specific address.
215// Enforces canonical-collision detection.
216// All previous names are preserved and resolvable.
217// The new name is the default value returned for address lookups.
218//
219// rlm is the cur of the caller's enclosing crossing function (passed as
220// data via the `_ int, rlm realm` non-crossing form). rlm.Address() is
221// the calling realm against which we authorize.
222func (u *UserData) UpdateName(_ int, rlm realm, newName string) error {
223 if !rlm.IsCurrent() {
224 return ErrInvalidRealm
225 }
226 if u.IsDeleted() {
227 return ErrUserNotExistOrDeleted
228 }
229
230 // Validate caller
231 if !controllers.Has(rlm.Address()) {
232 return NewErrNotWhitelisted(0, rlm)
233 }
234
235 return u.updateName(newName, false)
236}
237
238// UpdateNameIgnoreCanonical is the bypass path: same controller-
239// whitelist gate, but ErrCanonicalCollision is suppressed. The canonical
240// store is still written; a prior entry with the same canonical key is
241// silently overwritten (decision #14, later-wins).
242func (u *UserData) UpdateNameIgnoreCanonical(_ int, rlm realm, newName string) error {
243 if !rlm.IsCurrent() {
244 return ErrInvalidRealm
245 }
246 if u.IsDeleted() {
247 return ErrUserNotExistOrDeleted
248 }
249
250 if !controllers.Has(rlm.Address()) {
251 return NewErrNotWhitelisted(0, rlm)
252 }
253
254 return u.updateName(newName, true)
255}
256
257// delete marks a user and all their aliases as deleted without checking controllers.
258func (u *UserData) delete() error {
259 if u.IsDeleted() {
260 return ErrUserNotExistOrDeleted
261 }
262
263 u.deleted = true
264
265 chain.Emit(DeleteUserEvent, "address", u.addr.String())
266 return nil
267}
268
269// Delete marks a user and all their aliases as deleted.
270// rlm is the cur of the caller's enclosing crossing function; see UpdateName.
271func (u *UserData) Delete(_ int, rlm realm) error {
272 if !rlm.IsCurrent() {
273 return ErrInvalidRealm
274 }
275 if u.IsDeleted() {
276 return ErrUserNotExistOrDeleted
277 }
278
279 // Validate caller
280 if !controllers.Has(rlm.Address()) {
281 return NewErrNotWhitelisted(0, rlm)
282 }
283
284 return u.delete()
285}
286
287// Validate validates username and address passed in
288// Most of the validation is done in the controllers
289// This provides more flexibility down the line
290func validateName(username string) error {
291 if username == "" {
292 return ErrEmptyUsername
293 }
294
295 if len(username) > maxNameLen {
296 return ErrInvalidUsername
297 }
298
299 if !reName.MatchString(username) {
300 return ErrInvalidUsername
301 }
302
303 // Check if the username can be decoded or looks like a valid address
304 if address(username).IsValid() || reAddressLookalike.MatchString(username) {
305 return ErrNameLikeAddress
306 }
307
308 return nil
309}