Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}