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_test.gno

19.92 Kb · 569 lines
  1package users
  2
  3import (
  4	"chain"
  5	"testing"
  6
  7	"gno.land/p/nt/bptree/v0"
  8	"gno.land/p/nt/testutils/v0"
  9	"gno.land/p/nt/uassert/v0"
 10	"gno.land/p/nt/urequire/v0"
 11)
 12
 13var (
 14	alice     = "alice"
 15	aliceAddr = testutils.TestAddress(alice)
 16	bob       = "bob"
 17	bobAddr   = testutils.TestAddress(bob)
 18
 19	whitelistedCallerAddr = chain.PackageAddress(initControllerPath)
 20)
 21
 22func TestRegister(cur realm, t *testing.T) {
 23	testing.SetRealm(testing.NewCodeRealm(initControllerPath))
 24
 25	t.Run("valid_registration", func(t *testing.T) {
 26		urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
 27
 28		res, isLatest := ResolveName(alice)
 29		uassert.Equal(t, aliceAddr, res.Addr())
 30		uassert.True(t, isLatest)
 31
 32		res = ResolveAddress(aliceAddr)
 33		uassert.Equal(t, alice, res.Name())
 34	})
 35
 36	t.Run("invalid_inputs", func(t *testing.T) {
 37		cleanStore(t)
 38
 39		uassert.ErrorContains(t, RegisterUser(cross(cur), "", aliceAddr), ErrEmptyUsername.Error())
 40		uassert.ErrorContains(t, RegisterUser(cross(cur), alice, ""), ErrInvalidAddress.Error())
 41		uassert.ErrorContains(t, RegisterUser(cross(cur), alice, "invalidaddress"), ErrInvalidAddress.Error())
 42
 43		uassert.ErrorContains(t, RegisterUser(cross(cur), "username with a space", aliceAddr), ErrInvalidUsername.Error())
 44		uassert.ErrorContains(t,
 45			RegisterUser(cross(cur), "verylongusernameverylongusernameverylongusernameverylongusername1", aliceAddr),
 46			ErrInvalidUsername.Error())
 47		uassert.ErrorContains(t, RegisterUser(cross(cur), "namewith^&()", aliceAddr), ErrInvalidUsername.Error())
 48
 49		// Lowercase-only enforcement (closes case-confusable squatting via
 50		// Alice/alice/ALICE registering as distinct names — see TO_REVIEW
 51		// adversarial review). reName mirrors gno's mempackage Re_name shape.
 52		uassert.ErrorContains(t, RegisterUser(cross(cur), "Alice", aliceAddr), ErrInvalidUsername.Error())
 53		uassert.ErrorContains(t, RegisterUser(cross(cur), "ALICE", aliceAddr), ErrInvalidUsername.Error())
 54		uassert.ErrorContains(t, RegisterUser(cross(cur), "aLice", aliceAddr), ErrInvalidUsername.Error())
 55
 56		// Must start with a lowercase letter. Names starting with a digit,
 57		// underscore, or hyphen are rejected.
 58		uassert.ErrorContains(t, RegisterUser(cross(cur), "1alice", aliceAddr), ErrInvalidUsername.Error())
 59		uassert.ErrorContains(t, RegisterUser(cross(cur), "9", aliceAddr), ErrInvalidUsername.Error())
 60		uassert.ErrorContains(t, RegisterUser(cross(cur), "_alice", aliceAddr), ErrInvalidUsername.Error())
 61		uassert.ErrorContains(t, RegisterUser(cross(cur), "-alice", aliceAddr), ErrInvalidUsername.Error())
 62
 63		// Cannot end with a separator either (the new mempackage-aligned
 64		// regex requires the final char to be in [a-z0-9]).
 65		uassert.ErrorContains(t, RegisterUser(cross(cur), "alice_", aliceAddr), ErrInvalidUsername.Error())
 66		uassert.ErrorContains(t, RegisterUser(cross(cur), "alice-", aliceAddr), ErrInvalidUsername.Error())
 67
 68		// Cannot have consecutive separators in the middle either.
 69		// Each separator MUST be followed by at least one alphanumeric.
 70		uassert.ErrorContains(t, RegisterUser(cross(cur), "alice--bob", aliceAddr), ErrInvalidUsername.Error())
 71		uassert.ErrorContains(t, RegisterUser(cross(cur), "alice__bob", aliceAddr), ErrInvalidUsername.Error())
 72		uassert.ErrorContains(t, RegisterUser(cross(cur), "alice-_bob", aliceAddr), ErrInvalidUsername.Error())
 73		uassert.ErrorContains(t, RegisterUser(cross(cur), "alice_-bob", aliceAddr), ErrInvalidUsername.Error())
 74
 75		// Length cap of exactly 64 — boundary cases.
 76		exactly65 := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" // 65 chars
 77		uassert.ErrorContains(t, RegisterUser(cross(cur), exactly65, aliceAddr), ErrInvalidUsername.Error())
 78	})
 79
 80	t.Run("valid_edge_cases", func(t *testing.T) {
 81		// Names valid under the mempackage-aligned reName: starts with a
 82		// letter, may contain hyphens or underscores in the middle, ends
 83		// in [a-z0-9]. The hyphen support is what enables the namereg/v1
 84		// `nym-...` prefix shape.
 85		cleanStore(t)
 86		urequire.NoError(t, RegisterUser(cross(cur),
 87			"nym-alice123",
 88			testutils.TestAddress("u_nym_alice")))
 89
 90		cleanStore(t)
 91		urequire.NoError(t, RegisterUser(cross(cur),
 92			"a_b_c",
 93			testutils.TestAddress("u_underscore")))
 94
 95		cleanStore(t)
 96		urequire.NoError(t, RegisterUser(cross(cur),
 97			"a",
 98			testutils.TestAddress("u_singlechar")))
 99
100		cleanStore(t)
101		// 64 chars exactly (length cap is inclusive).
102		exactly64 := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
103		urequire.NoError(t, RegisterUser(cross(cur),
104			exactly64,
105			testutils.TestAddress("u_64")))
106	})
107
108	t.Run("addr_already_registered", func(t *testing.T) {
109		cleanStore(t)
110
111		urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
112
113		// Try registering again
114		uassert.ErrorContains(t, RegisterUser(cross(cur), "othername", aliceAddr), ErrAlreadyHasName.Error())
115	})
116
117	t.Run("name_taken", func(t *testing.T) {
118		cleanStore(t)
119
120		urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
121
122		// Try registering alice's name with bob's address
123		uassert.ErrorContains(t, RegisterUser(cross(cur), alice, bobAddr), ErrNameTaken.Error())
124	})
125
126	t.Run("user_deleted", func(t *testing.T) {
127		cleanStore(t)
128
129		urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
130		data := ResolveAddress(aliceAddr)
131		urequire.NoError(t, data.Delete(0, cur))
132
133		// Try re-registering after deletion
134		uassert.ErrorContains(t, RegisterUser(cross(cur), "newname", aliceAddr), ErrDeletedUser.Error())
135	})
136
137	t.Run("address_lookalike", func(t *testing.T) {
138		cleanStore(t)
139
140		// Address as username
141		uassert.ErrorContains(t, RegisterUser(cross(cur), "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", aliceAddr), ErrNameLikeAddress.Error())
142		// Beginning of address as username
143		uassert.ErrorContains(t, RegisterUser(cross(cur), "g1jg8mtutu9khhfwc4nxmu", aliceAddr), ErrNameLikeAddress.Error())
144		uassert.NoError(t, RegisterUser(cross(cur), "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5longerthananaddress", aliceAddr))
145	})
146}
147
148func TestUpdateName(cur realm, t *testing.T) {
149	testing.SetRealm(testing.NewCodeRealm(initControllerPath))
150
151	t.Run("valid_direct_alias", func(t *testing.T) {
152		cleanStore(t)
153
154		urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
155		data := ResolveAddress(aliceAddr)
156		{
157			testing.SetOriginCaller(whitelistedCallerAddr)
158			uassert.NoError(t, data.UpdateName(0, cur, "alice1"))
159			testing.SetRealm(testing.NewCodeRealm("gno.land/r/sys/users"))
160		}
161	})
162
163	t.Run("valid_double_alias", func(t *testing.T) {
164		cleanStore(t)
165
166		urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
167		data := ResolveAddress(aliceAddr)
168		{
169			testing.SetOriginCaller(whitelistedCallerAddr)
170			uassert.NoError(t, data.UpdateName(0, cur, "alice2"))
171			uassert.NoError(t, data.UpdateName(0, cur, "alice3"))
172			testing.SetRealm(testing.NewCodeRealm("gno.land/r/sys/users"))
173		}
174		uassert.Equal(t, ResolveAddress(aliceAddr).username, "alice3")
175	})
176
177	t.Run("name_taken", func(t *testing.T) {
178		cleanStore(t)
179
180		urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
181
182		data := ResolveAddress(aliceAddr)
183		uassert.Error(t, data.UpdateName(0, cur, alice), ErrNameTaken.Error())
184	})
185
186	t.Run("alias_before_name", func(t *testing.T) {
187		cleanStore(t)
188		data := ResolveAddress(aliceAddr) // not registered
189
190		uassert.ErrorContains(t, data.UpdateName(0, cur, alice), ErrUserNotExistOrDeleted.Error())
191	})
192
193	t.Run("alias_after_delete", func(t *testing.T) {
194		cleanStore(t)
195
196		urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
197		data := ResolveAddress(aliceAddr)
198		{
199			urequire.NoError(t, data.Delete(0, cur))
200			testing.SetRealm(testing.NewCodeRealm("gno.land/r/sys/users"))
201		}
202
203		data = ResolveAddress(aliceAddr)
204		{
205			uassert.ErrorContains(t, data.UpdateName(0, cur, "newalice"), ErrUserNotExistOrDeleted.Error())
206			testing.SetRealm(testing.NewCodeRealm("gno.land/r/sys/users"))
207		}
208	})
209
210	// Audit finding #3: a controller holding a cached *UserData pointer to a
211	// user that gets deleted between resolve and update must not be able to
212	// insert a new alias. The alias_after_delete test above re-resolves and
213	// gets nil, so it only exercises the u==nil branch. This test holds the
214	// pointer across the delete and verifies the deleted-flag branch.
215	t.Run("alias_with_cached_pointer_after_delete", func(t *testing.T) {
216		cleanStore(t)
217
218		urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
219		// Cache the pointer BEFORE deletion (simulating a controller that
220		// resolved earlier and held the reference).
221		cached := ResolveAddress(aliceAddr)
222		urequire.NotEqual(t, nil, cached)
223
224		// Delete via a fresh resolve.
225		data := ResolveAddress(aliceAddr)
226		{
227			urequire.NoError(t, data.Delete(0, cur))
228			testing.SetRealm(testing.NewCodeRealm("gno.land/r/sys/users"))
229		}
230
231		// The cached pointer is non-nil but its .deleted is now true.
232		urequire.NotEqual(t, nil, cached)
233		uassert.True(t, cached.IsDeleted())
234
235		// Attempting UpdateName on the cached pointer must reject — without
236		// this check, "squattedname" would be inserted into nameStore
237		// pointing at the deleted user, becoming permanently unresolvable
238		// AND unregisterable.
239		{
240			testing.SetOriginCaller(whitelistedCallerAddr)
241			uassert.ErrorContains(t, cached.UpdateName(0, cur, "squattedname"), ErrUserNotExistOrDeleted.Error())
242			testing.SetRealm(testing.NewCodeRealm("gno.land/r/sys/users"))
243		}
244
245		// Confirm the squat didn't happen: nameStore should NOT have it.
246		uassert.False(t, nameStore.Has("squattedname"))
247	})
248
249	t.Run("invalid_inputs", func(t *testing.T) {
250		cleanStore(t)
251
252		urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
253		data := ResolveAddress(aliceAddr)
254		{
255			testing.SetOriginCaller(whitelistedCallerAddr)
256			uassert.ErrorContains(t, data.UpdateName(0, cur, ""), ErrEmptyUsername.Error())
257			uassert.ErrorContains(t, data.UpdateName(0, cur, "username with a space"), ErrInvalidUsername.Error())
258			uassert.ErrorContains(t,
259				data.UpdateName(0, cur, "verylongusernameverylongusernameverylongusernameverylongusername1"),
260				ErrInvalidUsername.Error())
261			uassert.ErrorContains(t, data.UpdateName(0, cur, "namewith^&()"), ErrInvalidUsername.Error())
262			testing.SetRealm(testing.NewCodeRealm("gno.land/r/sys/users"))
263		}
264	})
265
266	t.Run("address_lookalike", func(t *testing.T) {
267		cleanStore(t)
268
269		urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
270		data := ResolveAddress(aliceAddr)
271
272		{
273			// Address as username
274			uassert.ErrorContains(t, data.UpdateName(0, cur, "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), ErrNameLikeAddress.Error())
275			// Beginning of address as username
276			uassert.ErrorContains(t, data.UpdateName(0, cur, "g1jg8mtutu9khhfwc4nxmu"), ErrNameLikeAddress.Error())
277			uassert.NoError(t, data.UpdateName(0, cur, "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5longerthananaddress"))
278			testing.SetRealm(testing.NewCodeRealm("gno.land/r/sys/users"))
279		}
280	})
281}
282
283func TestDelete(cur realm, t *testing.T) {
284	testing.SetRealm(testing.NewCodeRealm(initControllerPath))
285
286	t.Run("non_existent_user", func(t *testing.T) {
287		cleanStore(t)
288
289		data := ResolveAddress(testutils.TestAddress("unregistered"))
290		uassert.ErrorContains(t, data.Delete(0, cur), ErrUserNotExistOrDeleted.Error())
291	})
292
293	t.Run("double_delete", func(t *testing.T) {
294		cleanStore(t)
295
296		urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
297		data := ResolveAddress(aliceAddr)
298		urequire.NoError(t, data.Delete(0, cur))
299		data = ResolveAddress(aliceAddr)
300		uassert.ErrorContains(t, data.Delete(0, cur), ErrUserNotExistOrDeleted.Error())
301	})
302
303	t.Run("valid_delete", func(t *testing.T) {
304		cleanStore(t)
305
306		urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
307		data := ResolveAddress(aliceAddr)
308		uassert.NoError(t, data.Delete(0, cur))
309
310		resolved1, _ := ResolveName(alice)
311		uassert.Equal(t, nil, resolved1)
312		uassert.Equal(t, nil, ResolveAddress(aliceAddr))
313	})
314}
315
316func TestRegisterNotWhitelisted(cur realm, t *testing.T) {
317	t.Run("register_not_whitelisted", func(t *testing.T) {
318		uassert.ErrorContains(t, RegisterUser(cross(cur), alice, aliceAddr), "does not exist in whitelist")
319	})
320}
321
322func TestCanonicalize(cur realm, t *testing.T) {
323	cases := []struct {
324		in, want string
325	}{
326		// Single-rule cases.
327		{"l", "i"}, {"i", "i"}, {"1", "i"},
328		{"0", "o"}, {"o", "o"},
329		{"-", ""}, {".", ""}, {"_", ""},
330
331		// Identity / pass-through.
332		{"abc", "abc"}, {"xyz", "xyz"}, {"123", "i23"},
333
334		// Open Nym Tier examples from the design doc.
335		{"nym-vital1k123", "nymvitaiiki23"},
336		{"nym-vitalik123", "nymvitaiiki23"},
337		{"nym-vital1k999", "nymvitaiik999"},
338
339		// Same stem, different digit suffix → distinct canonicals.
340		{"nym-foolbar000", "nymfooibarooo"},
341		{"nym-foolbar001", "nymfooibarooi"},
342
343		// Three-way separator equivalence.
344		{"xyz-com", "xyzcom"},
345		{"xyz_com", "xyzcom"},
346		{"xyz.com", "xyzcom"},
347
348		// Idempotency: applying twice equals applying once.
349		{"nymvitaiiki23", "nymvitaiiki23"},
350
351		// Empty input.
352		{"", ""},
353	}
354	for _, tc := range cases {
355		got := Canonicalize(tc.in)
356		uassert.Equal(t, tc.want, got)
357	}
358}
359
360func TestCanonicalize_NonASCIIPassthrough(cur realm, t *testing.T) {
361	// ASCII-only contract: the function passes non-ASCII bytes through
362	// unchanged. Locks the documented contract — registration paths
363	// reject non-ASCII upstream via validateName, so this code path is
364	// only reachable through direct callers from other realms.
365	uassert.Equal(t, "café", Canonicalize("café"))
366	uassert.Equal(t, "naïve", Canonicalize("naïve"))
367}
368
369func TestRegisterUser_CanonicalCollision(cur realm, t *testing.T) {
370	testing.SetRealm(testing.NewCodeRealm(initControllerPath))
371
372	t.Run("non_bypass_blocks", func(t *testing.T) {
373		cleanStore(t)
374
375		urequire.NoError(t, RegisterUser(cross(cur), "vitalik", aliceAddr))
376		// Different exact name, same canonical form → ErrCanonicalCollision.
377		uassert.ErrorContains(t,
378			RegisterUser(cross(cur), "vital1k", bobAddr),
379			ErrCanonicalCollision.Error(),
380		)
381	})
382
383	t.Run("non_bypass_blocks_with_separator_strip", func(t *testing.T) {
384		cleanStore(t)
385
386		urequire.NoError(t, RegisterUser(cross(cur), "xyz-com", aliceAddr))
387		uassert.ErrorContains(t,
388			RegisterUser(cross(cur), "xyz_com", bobAddr),
389			ErrCanonicalCollision.Error(),
390		)
391	})
392
393	t.Run("non_bypass_allows_different_digit_suffix", func(t *testing.T) {
394		cleanStore(t)
395
396		// Same alpha stem, different digits-after-canonicalization → no collision.
397		urequire.NoError(t, RegisterUser(cross(cur), "nym-foolbar000", aliceAddr))
398		urequire.NoError(t, RegisterUser(cross(cur), "nym-foolbar999",
399			testutils.TestAddress("foolbar999")))
400	})
401
402	t.Run("exact_name_taken_takes_precedence", func(t *testing.T) {
403		cleanStore(t)
404
405		urequire.NoError(t, RegisterUser(cross(cur), "vitalik", aliceAddr))
406		// Same exact name → ErrNameTaken (NOT ErrCanonicalCollision).
407		// The nameStore.Has check precedes the canonical check.
408		uassert.ErrorContains(t,
409			RegisterUser(cross(cur), "vitalik", bobAddr),
410			ErrNameTaken.Error(),
411		)
412	})
413}
414
415func TestRegisterUserIgnoreCanonical(cur realm, t *testing.T) {
416	testing.SetRealm(testing.NewCodeRealm(initControllerPath))
417
418	t.Run("bypass_succeeds_on_canonical_collision", func(t *testing.T) {
419		cleanStore(t)
420
421		urequire.NoError(t, RegisterUser(cross(cur), "vitalik", aliceAddr))
422		// Bypass succeeds even though canonical collides.
423		urequire.NoError(t, RegisterUserIgnoreCanonical(cross(cur), "vital1k", bobAddr))
424
425		// Both names resolve.
426		res, _ := ResolveName("vitalik")
427		uassert.Equal(t, aliceAddr, res.Addr())
428		res, _ = ResolveName("vital1k")
429		uassert.Equal(t, bobAddr, res.Addr())
430	})
431
432	t.Run("bypass_later_wins_overwrite", func(t *testing.T) {
433		cleanStore(t)
434
435		// First registration writes canonical entry.
436		urequire.NoError(t, RegisterUser(cross(cur), "vitalik", aliceAddr))
437		existing, taken := IsCanonicalTaken("vital1k")
438		urequire.True(t, taken)
439		uassert.Equal(t, "vitalik", existing)
440
441		// Bypass write overwrites the canonical entry (decision #14).
442		urequire.NoError(t, RegisterUserIgnoreCanonical(cross(cur), "vital1k", bobAddr))
443		existing, taken = IsCanonicalTaken("vitalik")
444		urequire.True(t, taken)
445		uassert.Equal(t, "vital1k", existing)
446	})
447
448	t.Run("bypass_still_blocks_exact_taken", func(t *testing.T) {
449		cleanStore(t)
450
451		urequire.NoError(t, RegisterUser(cross(cur), "vitalik", aliceAddr))
452		// Same exact name still blocked, even via the bypass path.
453		uassert.ErrorContains(t,
454			RegisterUserIgnoreCanonical(cross(cur), "vitalik", bobAddr),
455			ErrNameTaken.Error(),
456		)
457	})
458}
459
460func TestUpdateName_CanonicalCollision(cur realm, t *testing.T) {
461	testing.SetRealm(testing.NewCodeRealm(initControllerPath))
462
463	t.Run("self_collision_blocks_per_decision_15", func(t *testing.T) {
464		cleanStore(t)
465
466		// Alice registers vital1k. Canonical store maps "vitaiik" → "vital1k".
467		urequire.NoError(t, RegisterUser(cross(cur), "vital1k", aliceAddr))
468		data := ResolveAddress(aliceAddr)
469
470		// Alice attempts to rename to a confusable variant of HER OWN name.
471		// Decision #15: blocked. No self-collision exception in the
472		// non-bypass path.
473		testing.SetOriginCaller(whitelistedCallerAddr)
474		uassert.ErrorContains(t,
475			data.UpdateName(0, cur, "vitalik"),
476			ErrCanonicalCollision.Error(),
477		)
478		testing.SetRealm(testing.NewCodeRealm("gno.land/r/sys/users"))
479	})
480
481	t.Run("different_user_collision_blocks", func(t *testing.T) {
482		cleanStore(t)
483
484		urequire.NoError(t, RegisterUser(cross(cur), "vitalik", aliceAddr))
485		urequire.NoError(t, RegisterUser(cross(cur), "bob", bobAddr))
486
487		data := ResolveAddress(bobAddr)
488		testing.SetOriginCaller(whitelistedCallerAddr)
489		uassert.ErrorContains(t,
490			data.UpdateName(0, cur, "vital1k"),
491			ErrCanonicalCollision.Error(),
492		)
493		testing.SetRealm(testing.NewCodeRealm("gno.land/r/sys/users"))
494	})
495
496	t.Run("bypass_allows_self_collision_rename", func(t *testing.T) {
497		cleanStore(t)
498
499		urequire.NoError(t, RegisterUser(cross(cur), "vital1k", aliceAddr))
500		data := ResolveAddress(aliceAddr)
501
502		// Bypass path allows the rename (DAO grant scenario).
503		testing.SetOriginCaller(whitelistedCallerAddr)
504		urequire.NoError(t, data.UpdateNameIgnoreCanonical(0, cur, "vitalik"))
505		testing.SetRealm(testing.NewCodeRealm("gno.land/r/sys/users"))
506
507		// Both names point to alice; latest is "vitalik".
508		uassert.Equal(t, "vitalik", ResolveAddress(aliceAddr).Name())
509		// Old name still resolves to alice (decision #4: keep old entry).
510		res, isLatest := ResolveName("vital1k")
511		uassert.Equal(t, aliceAddr, res.Addr())
512		uassert.False(t, isLatest)
513	})
514}
515
516func TestCanonicalEntry_Persists_After_Delete(cur realm, t *testing.T) {
517	// Decision #5: Delete does NOT remove the canonical entry. Mirrors
518	// the existing tombstone behavior (anti-revival policy).
519	testing.SetRealm(testing.NewCodeRealm(initControllerPath))
520	cleanStore(t)
521
522	urequire.NoError(t, RegisterUser(cross(cur), "vitalik", aliceAddr))
523	data := ResolveAddress(aliceAddr)
524	urequire.NoError(t, data.Delete(0, cur))
525
526	// Canonical entry retained.
527	existing, taken := IsCanonicalTaken("vitalik")
528	urequire.True(t, taken)
529	uassert.Equal(t, "vitalik", existing)
530
531	// A different user CANNOT register a confusable variant — the deleted
532	// user's canonical claim still stands.
533	uassert.ErrorContains(t,
534		RegisterUser(cross(cur), "vital1k", bobAddr),
535		ErrCanonicalCollision.Error(),
536	)
537}
538
539func TestCanonicalEntry_Persists_After_UpdateName(cur realm, t *testing.T) {
540	// Decision #4: UpdateName keeps the OLD canonical entry. Mirrors the
541	// existing nameStore alias retention (anti-rename-squat policy).
542	testing.SetRealm(testing.NewCodeRealm(initControllerPath))
543	cleanStore(t)
544
545	urequire.NoError(t, RegisterUser(cross(cur), "alice", aliceAddr))
546	data := ResolveAddress(aliceAddr)
547	testing.SetOriginCaller(whitelistedCallerAddr)
548	urequire.NoError(t, data.UpdateName(0, cur, "alice2"))
549	testing.SetRealm(testing.NewCodeRealm("gno.land/r/sys/users"))
550
551	// Both old and new canonical entries exist.
552	existing, taken := IsCanonicalTaken("alice")
553	urequire.True(t, taken)
554	uassert.Equal(t, "alice", existing)
555
556	existing, taken = IsCanonicalTaken("alice2")
557	urequire.True(t, taken)
558	uassert.Equal(t, "alice2", existing)
559}
560
561// cleanStore should not be needed, as vm store should be reset after each test.
562// Reference: https://github.com/gnolang/gno/issues/1982
563func cleanStore(t *testing.T) {
564	t.Helper()
565
566	nameStore = bptree.NewBPTree32()
567	addressStore = bptree.NewBPTree32()
568	canonicalStore = bptree.NewBPTree32()
569}