Kind Realm (renderable)
Name users
Namespace sys
Exported functions 21
Module gno.land/r/sys/users
gno 0.9 store.gno gno
⧉
1 package users 2 3 import ( 4 "chain" 5 "chain/runtime" 6 "regexp" 7 8 AddControllerAtGenesis (interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, addr string)
Canonicalize (name string) string
Controllers () [].uverse.address
GetReadonlyAddrStore () *gno.land/p/nt/bptree/rotree/v0.ReadOnlyTree
GetReadOnlyNameStore () *gno.land/p/nt/bptree/rotree/v0.ReadOnlyTree
IsCanonicalTaken (name string) (existing string, taken bool)
IsController (addr string) bool
IsNameTaken (name string) bool
NewErrNotWhitelisted (int, caller interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}) struct{Caller string}
ProposeControllerAdditionAndRemoval (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, toAdd string, toRemove string) struct{title string; description string; executor gno.land/r/gov/dao.Executor; filter gno.land/r/gov/dao.Filter}
ProposeControllerRemoval (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, addr string) struct{title string; description string; executor gno.land/r/gov/dao.Executor; filter gno.land/r/gov/dao.Filter}
ProposeDeleteUser (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, addr string) struct{title string; description string; executor gno.land/r/gov/dao.Executor; filter gno.land/r/gov/dao.Filter}
ProposeNewController (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, addr string) struct{title string; description string; executor gno.land/r/gov/dao.Executor; filter gno.land/r/gov/dao.Filter}
ProposeRegisterUser (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, name string, addr string) struct{title string; description string; executor gno.land/r/gov/dao.Executor; filter gno.land/r/gov/dao.Filter}
ProposeUpdateName (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, addr string, newName string) struct{title string; description string; executor gno.land/r/gov/dao.Executor; filter gno.land/r/gov/dao.Filter}
RegisterUser (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, name string, address_XXX string) interface {Error func() string}
RegisterUserIgnoreCanonical (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, name string, address_XXX string) interface {Error func() string}
Render (string) string
ResolveAddress (addr string) *gno.land/r/sys/users.UserData
ResolveAny (input string) (*gno.land/r/sys/users.UserData, bool)
ResolveName (name string) (data *gno.land/r/sys/users.UserData, isCurrent bool)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.
"gno.land/p/nt/bptree/v0"
9 "gno.land/p/nt/ufmt/v0"
10 )
11
12 var (
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
30 const maxNameLen = 64
31
32 const (
33 RegisterUserEvent = "Registered"
34 UpdateNameEvent = "Updated"
35 DeleteUserEvent = "Deleted"
36 )
37
38 type UserData struct {
39 addr address
40 username string // contains the latest name of a user
41 deleted bool
42 }
43
44 func (u UserData) Name() string {
45 return u.username
46 }
47
48 func (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 // }
61 func (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
70 func (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).
81 func 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.
137 func 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.
158 func 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.
178 func (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.
232 func (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).
252 func (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.
268 func (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.
281 func (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
300 func 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 }
320