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

users source realm

Readme 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:

  1. The controller whitelist is empty (it can't be populated until after it exists).
  2. System realms (r/sys/users/init, r/sys/namereg/v1, etc.) need to pre-seed users and add themselves as controllers.
  3. Any realm whose init() runs at genesis can therefore call RegisterUser without 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.Bootstrap adds its own package address, and r/sys/namereg/v1/init.gno likewise auto-whitelists gno.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.Sprint used instead of Sprintf in controller-swap proposal description (governance-vote readability).
  • #6: Add+Remove proposal silently no-ops if add fails on an already-listed controller — voted-on swap doesn't actually swap.
  • #7: AddControllerAtGenesis shares 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.

Constants 1

Variables 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)
source

Functions 20

func AddControllerAtGenesis

crossing Action
1func AddControllerAtGenesis(_ realm, addr address)
source

AddControllerAtGenesis allows adding a controller during chain genesis (height 0). This is mostly useful for testing.

func Canonicalize

Action
1func Canonicalize(name string) string
source

Canonicalize 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 IsCanonicalTaken

Action
1func IsCanonicalTaken(name string) (existing string, taken bool)
source

IsCanonicalTaken 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

Action
1func IsController(addr address) bool
source

IsController 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

Action
1func IsNameTaken(name string) bool
source

IsNameTaken 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 Action
1func ProposeControllerAdditionAndRemoval(cur realm, toAdd, toRemove address) dao.ProposalRequest
source

ProposeControllerAdditionAndRemoval allows GovDAO to add a new caller and remove an old caller in the same proposal.

func ProposeControllerRemoval

crossing Action
1func ProposeControllerRemoval(cur realm, addr address) dao.ProposalRequest
source

ProposeControllerRemoval allows GovDAO to add a whitelisted caller

func ProposeDeleteUser

crossing Action
1func ProposeDeleteUser(cur realm, addr address) dao.ProposalRequest
source

ProposeDeleteUser allows GovDAO to delete a user without checking controllers

func ProposeNewController

crossing Action
1func ProposeNewController(cur realm, addr address) dao.ProposalRequest
source

ProposeNewController allows GovDAO to add a whitelisted caller

func ProposeRegisterUser

crossing Action
1func ProposeRegisterUser(cur realm, name string, addr address) dao.ProposalRequest
source

ProposeRegisterUser 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 Action
1func ProposeUpdateName(cur realm, addr address, newName string) dao.ProposalRequest
source

ProposeUpdateName 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 Action
1func RegisterUser(cur realm, name string, address_XXX address) error
source

RegisterUser 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 Action
1func RegisterUserIgnoreCanonical(cur realm, name string, address_XXX address) error
source

RegisterUserIgnoreCanonical 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 NewErrNotWhitelisted

Action
1func NewErrNotWhitelisted(_ int, caller realm) ErrNotWhitelisted
source

NewErrNotWhitelisted 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

Action
1func ResolveAddress(addr address) *UserData
source

ResolveAddress returns the latest UserData of a specific user by address

func ResolveAny

Action
1func ResolveAny(input string) (*UserData, bool)
source

ResolveAny 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

Action
1func ResolveName(name string) (data *UserData, isCurrent bool)
source

ResolveName returns the latest UserData of a specific user by name or alias

Types 2

type ErrNotWhitelisted

struct
1type ErrNotWhitelisted struct {
2	Caller string // "CodeRealm{ <addr>, <pkgPath> }" or "UserRealm{ <addr> }" — failed the whitelist check
3}
source

ErrNotWhitelisted 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).

Methods on ErrNotWhitelisted

func Error

method on ErrNotWhitelisted
1func (e ErrNotWhitelisted) Error() string
source

type UserData

struct
1type UserData struct {
2	addr     address
3	username string // contains the latest name of a user
4	deleted  bool
5}
source

Methods on UserData

func Addr

method on UserData
1func (u UserData) Addr() address
source

func Delete

method on UserData
1func (u *UserData) Delete(_ int, rlm realm) error
source

Delete 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 UserData
1func (u *UserData) IsDeleted() bool
source

IsDeleted 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:

Example
1if u.IsDeleted() {
2    return ErrUserNotExistOrDeleted
3}

func Name

method on UserData
1func (u UserData) Name() string
source

func UpdateName

method on UserData
1func (u *UserData) UpdateName(_ int, rlm realm, newName string) error
source

UpdateName 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 UserData
1func (u *UserData) UpdateNameIgnoreCanonical(_ int, rlm realm, newName string) error
source

UpdateNameIgnoreCanonical 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).

Imports 10

Source Files 12