Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}