PathrockNetwork Gno Explorer
HomeBlocksTransactionsTokensRealmsPackagesValidatorsAnalytics

PathrockNetwork Gno Explorer — an independent explorer for Gno.land Mainnet (gnoland-1), operated by PathrockNetwork. Not an official Gno.land service.

gnowebarchive RPC

gno.land/r/sys/users

Realm
Open in gnoweb ↗

Overview

Kind
Realm (renderable)
Name
users
Namespace
sys
Files
12 (README)(gnomod.toml)
Exported functions
21
Module
gno.land/r/sys/users
gno
0.9

Files (12)

  • README.mdmarkdown
  • gnomod.tomltoml
  • admin.gnogno
  • api.gnogno
  • canonical.gnogno
  • errors.gnogno
  • render.gnogno
  • store.gnogno
  • users.gnogno
  • crossrealm_test.gnogno
  • store_test.gnogno
  • users_test.gnogno
  • store_test.gnogno
    1package users23import (4	"chain"5	"testing"67	"gno.land/p/nt/bptree/v0"8

    Functions

    • 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.

    Rendered

    RenderedRawgnoweb ↗

    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/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/v0
    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}
    570