users source realm
View source
r/sys/users
The system realm that owns the (name → address, address → user) registry for
gno.land. It is intentionally minimal: it stores UserData records, exposes
resolve/update/delete primitives, and gates writes through a controller
whitelist managed by GovDAO (ProposeNewController /
ProposeControllerRemoval / ProposeControllerAdditionAndRemoval).
This realm does not define what a "name" is, what registration costs, or
whether names can be transferred. Those policies live in controller realms
that the DAO whitelists. See r/sys/namereg/v1 for one such controller, and
the examples/gno.land/r/sys/names realm for the related namespace verifier
that gates package deployment under gno.land/r/<namespace>/....
Trust boundary at genesis (height 0)
The whitelist check in RegisterUser (and the sibling
AddControllerAtGenesis) short-circuits at chain height 0:
1// store.gno
2if runtime.ChainHeight() > 0 && !controllers.Has(runtime.PreviousRealm().Address()) {
3 return NewErrNotWhitelisted()
4}
This is intentional, not a bug. Genesis is the bootstrap window where:
- The controller whitelist is empty (it can't be populated until after it exists).
- System realms (
r/sys/users/init,r/sys/namereg/v1, etc.) need to pre-seed users and add themselves as controllers. - Any realm whose
init()runs at genesis can therefore callRegisterUserwithout authorization.
The protection model is out-of-band trust: chain operators control which
realms ship in genesis (via the contents of examples/gno.land/r/...), and
those realms are vouched for at chain-binary build time. The realm code does
not — and intentionally does not try to — enforce who is "allowed" to
pre-register at height 0.
Audit reference
This bypass was flagged as audit finding #4 ("Genesis bypass — any caller can register at height 0"). After review, it is treated as WON'T FIX, working as intended:
- Removing the bypass breaks every legitimate genesis pre-registration use
case (including this realm's own bootstrap and
r/sys/namereg/v1's preregister loop of system names). - A hardcoded genesis-allowlist (a la "only
r/sys/*realms may bypass") shifts the trust to a literal in source — a chain upgrade is required to add a new genesis-bootstrap realm. This trades flexibility for the same amount of trust. - Path-prefix gating (e.g. "only
gno.land/r/sys/*") couples this realm to the namespace verifier remaining locked-down, an implicit dependency that makes future refactors fragile.
If chain operators want post-deployment auditing of who pre-registered what
at genesis, the RegisterUserEvent is emitted on every successful
registration regardless of height, and the source of each registration can be
recovered by walking genesis-block events alongside the examples/ tree.
Sibling bypass: AddControllerAtGenesis
The same height-0 trust model applies to AddControllerAtGenesis in
admin.gno:
1func AddControllerAtGenesis(_ realm, addr address) {
2 height := runtime.ChainHeight()
3 if height > 0 {
4 panic("AddControllerAtGenesis can only be called at genesis (height 0)")
5 }
6 if !addr.IsValid() {
7 panic(ErrInvalidAddress)
8 }
9 controllers.Add(addr)
10}
This was audit finding #7 ("AddControllerAtGenesis has no caller check"). It is the same intentional design as #4 and is likewise treated as WON'T FIX:
- Any realm whose
init()runs at genesis can whitelist any address as a controller, without authorization. - This is how the registry bootstraps itself:
r/sys/users/init.Bootstrapadds its own package address, andr/sys/namereg/v1/init.gnolikewise auto-whitelistsgno.land/r/sys/namereg/v1. Removing the bypass would break the bootstrap pattern. - After genesis (height > 0) the function hard-panics, so the privilege window is strictly one-time at chain birth.
- The trust model is identical: chain operators vouch for whatever realms
ship in
examples/at chain-binary build time.
If you need to add a new controller post-genesis, the supported path is a
GovDAO proposal via ProposeNewController — the same channel that rotates
every controller going forward.
What the audit DID flag that's worth fixing
- #5:
ufmt.Sprintused instead ofSprintfin controller-swap proposal description (governance-vote readability). - #6: Add+Remove proposal silently no-ops if
addfails on an already-listed controller — voted-on swap doesn't actually swap. - #7:
AddControllerAtGenesisshares the height-0 bypass; same trust model applies, same intentional design.
See NAMEREG_AUDIT.md for the full set and NAMEREG_TODO.md for tracked
"won't fix / accepted risk" items.
1
1
var ErrAlreadyWhitelisted, ErrWhitelistRemoveFailed, ErrNameTaken, ErrCanonicalCollision, ErrInvalidAddress, ErrEmptyUsername, ErrNameLikeAddress, ErrInvalidUsername, ErrAlreadyHasName, ErrDeletedUser, ErrUserNotExistOrDeleted, ErrInvalidRealm
1var (
2 ErrAlreadyWhitelisted = errors.New(prefix + "already whitelisted")
3 ErrWhitelistRemoveFailed = errors.New(prefix + "failed to remove address from whitelist")
4
5 ErrNameTaken = errors.New(prefix + "name/Alias already taken")
6 ErrCanonicalCollision = errors.New(prefix + "name collides with a confusable variant of an existing name")
7 ErrInvalidAddress = errors.New(prefix + "invalid address")
8
9 ErrEmptyUsername = errors.New(prefix + "empty username provided")
10 ErrNameLikeAddress = errors.New(prefix + "username resembles a gno.land address")
11 ErrInvalidUsername = errors.New(prefix + "username must match ^[a-z][a-z0-9]*([_-][a-z0-9]+)*$ (max 64 chars)")
12
13 ErrAlreadyHasName = errors.New(prefix + "username for this address already registered - try creating an Alias")
14 ErrDeletedUser = errors.New(prefix + "cannot register a new username after deleting")
15
16 ErrUserNotExistOrDeleted = errors.New(prefix + "this user does not exist or was deleted")
17
18 // ErrInvalidRealm is returned by controller-gated *UserData mutators
19 // when the supplied rlm is not the caller's live cur (i.e.
20 // rlm.IsCurrent() is false). Closes Class-2 designation forgery via
21 // a stored stale realm value whose .Address() resolves to a
22 // whitelisted controller. See docs/resources/gno-security.md.
23 ErrInvalidRealm = errors.New(prefix + "rlm is not the caller's live cur")
24)20
func AddControllerAtGenesis
crossing ActionAddControllerAtGenesis allows adding a controller during chain genesis (height 0). This is mostly useful for testing.
func Canonicalize
ActionCanonicalize returns the canonical form of a name. The substitutions collapse single-character visual confusables that arise across the allowed [a-z0-9] character set, and strip the three separators that can sneak between identical alphanumeric runs.
- {l, i, 1} → i
- {0, o} → o
- {-, ., _} → stripped
- all other characters unchanged
CONTRACT: stable. Future controllers that want to share this canonical namespace MUST use this exact function — do not roll your own. Adding new substitutions later is a breaking change because it would silently re-key existing entries in canonicalStore.
Input contract: ASCII-only. r/sys/users.validateName already rejects non-ASCII at the registration boundary, so by the time a name reaches Canonicalize through the standard write path it is guaranteed to be ASCII. Direct callers from other realms must honor this contract; non-ASCII bytes are passed through as-is and will produce undefined collision behavior.
Multi-char confusables (m↔rn, nn↔m, cl↔d) are NOT canonicalized. They require fixed-point substring substitution rounds, which is out of scope for the unified store.
Pure: no state access. Safe to call from anywhere.
func GetReadOnlyNameStore
ActionGetReadOnlyNameStore exposes the name store in readonly mode
func GetReadonlyAddrStore
ActionGetReadonlyAddrStore exposes the address store in readonly mode
func IsCanonicalTaken
ActionIsCanonicalTaken reports whether the given name's canonical form is already registered. Pass the raw name; canonicalization is applied internally. The first return is the original (non-canonical) name that owns the canonical key, for UX in collision messages.
When a bypass write (RegisterUserIgnoreCanonical or the bypass path through ProposeRegisterUser/ProposeUpdateName) overwrites a prior canonical entry, this returns the most-recently-written original.
func IsController
ActionIsController reports whether the given address is currently in the controller whitelist. Returns the same boolean that gating checks (RegisterUser, UpdateName, Delete) use internally — useful for off-chain monitoring and for governance proposals to inspect state before voting.
func IsNameTaken
ActionIsNameTaken reports whether the exact-string name exists in nameStore. Returns true for any name ever registered, including:
- active registrations
- tombstoned (deleted) users' names — Delete() sets `deleted=true` but does not remove the nameStore entry (anti-revival policy)
- old aliases from renames — UpdateName inserts the new name alongside the old; the old key stays (anti-rename-squat policy)
In short: IsNameTaken(name) answers "would RegisterUser(name, _) fail with ErrNameTaken?" — same answer for active, deleted, or aliased-away names. Pairs with IsCanonicalTaken (canonical-match) and ResolveName (active-current-user lookup with full UserData).
No canonicalization is applied. For controllers that want exact- match uniqueness without pulling in canonical-collision logic.
func ProposeControllerAdditionAndRemoval
crossing Action1func ProposeControllerAdditionAndRemoval(cur realm, toAdd, toRemove address) dao.ProposalRequestProposeControllerAdditionAndRemoval allows GovDAO to add a new caller and remove an old caller in the same proposal.
func ProposeControllerRemoval
crossing ActionProposeControllerRemoval allows GovDAO to add a whitelisted caller
func ProposeDeleteUser
crossing ActionProposeDeleteUser allows GovDAO to delete a user without checking controllers
func ProposeNewController
crossing ActionProposeNewController allows GovDAO to add a whitelisted caller
func ProposeRegisterUser
crossing ActionProposeRegisterUser allows GovDAO to register a name without checking controllers. The executor closure runs with ignoreCanonical=true (decision #3): DAO grants always bypass canonical-collision detection. Voters see any collision in the proposal description and can vote NO if unintended.
func ProposeUpdateName
crossing ActionProposeUpdateName allows GovDAO to update a name with an alias without checking controllers. Like ProposeRegisterUser, the executor runs with ignoreCanonical=true (decision #3).
func RegisterUser
crossing ActionRegisterUser adds a new user to the system. Enforces canonical- collision detection: a name whose Canonicalize-form matches a prior registration returns ErrCanonicalCollision.
func RegisterUserIgnoreCanonical
crossing ActionRegisterUserIgnoreCanonical is the bypass path: same controller- whitelist gate, but ErrCanonicalCollision is suppressed. The canonical store is still written; a prior entry with the same canonical key is silently overwritten (decision #14, later-wins). Use sparingly — names registered here can canonical-collide with existing ones, weakening confusable protection for everyone.
func Render
func NewErrNotWhitelisted
ActionNewErrNotWhitelisted constructs the error with the caller's realm identity captured as a string at construction time. The _ int discriminator keeps this non-crossing (a non-crossing function can't take a `realm`-named-`cur` first param, so we use the standard _ int, rlm realm shape).
func ResolveAddress
ActionResolveAddress returns the latest UserData of a specific user by address
func ResolveAny
ActionResolveAny tries to resolve any given string to *UserData If the input is not found in the registry in any form, nil is returned
func ResolveName
ActionResolveName returns the latest UserData of a specific user by name or alias
2
type ErrNotWhitelisted
structErrNotWhitelisted stores the failing caller's realm identity as a plain string so the error is a pure data record (no live realm values in its fields).
type UserData
structMethods on UserData
func Addr
method on UserDatafunc Delete
method on UserDataDelete marks a user and all their aliases as deleted. rlm is the cur of the caller's enclosing crossing function; see UpdateName.
func IsDeleted
method on UserDataIsDeleted reports whether this user record is missing or marked deleted. A nil receiver returns true — "the user does not exist" is semantically indistinguishable from "the user was deleted" for callers that need to gate further state changes. This lets call sites collapse the nil check and the deleted check into a single guard:
func Name
method on UserDatafunc RenderLink
method on UserDataRenderLink provides a render link to the user page on gnoweb `linkText` is optional
func UpdateName
method on UserDataUpdateName adds a name that is associated with a specific address. Enforces canonical-collision detection. All previous names are preserved and resolvable. The new name is the default value returned for address lookups.
rlm is the cur of the caller's enclosing crossing function (passed as data via the `_ int, rlm realm` non-crossing form). rlm.Address() is the calling realm against which we authorize.
func UpdateNameIgnoreCanonical
method on UserDataUpdateNameIgnoreCanonical is the bypass path: same controller- whitelist gate, but ErrCanonicalCollision is suppressed. The canonical store is still written; a prior entry with the same canonical key is silently overwritten (decision #14, later-wins).
10
- chain stdlib
- chain/runtime stdlib
- errors stdlib
- gno.land/p/moul/addrset package
- gno.land/p/nt/bptree/v0 package
- gno.land/p/nt/bptree/v0/rotree package
- gno.land/p/nt/ufmt/v0 package
- gno.land/r/gov/dao realm
- regexp stdlib
- strings stdlib