Search Apps Documentation Source Content File Folder Download Copy Actions Download 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}