v0 source pure
Package groups provides Groups containing a base address set plus named Roles, each with their own member set and met...
View source
groups
A Group is a set of addresses (the base set) plus any number of named
Roles, each with its own member set and optional metadata. One Group
per DAO, per board, per permissions instance — whatever your realm manages.
Group
├── base set: the plain members (guests, users, council — you decide)
└── roles
├── "admin": member set + meta
└── "moderator": member set + meta
Quick start
1import "gno.land/p/nt/groups/v0"
2
3var group = groups.NewGroup()
4
5func init() {
6 // Base members.
7 group.Add(address("g1alice..."))
8 group.Add(address("g1bob..."))
9
10 // A role with its own members.
11 admins, _ := group.AddRole("admin")
12 admins.Members().Add(address("g1carol..."))
13}
Three kinds of operations
Every membership operation belongs to exactly one family, so a call site always says which semantic it means — checking the base set and checking "anywhere in the group" are different questions with different methods.
| Family | Methods | Looks at |
|---|---|---|
| Base set | Add, Remove, Has, Size, Iterate |
base set only |
| Role registry | AddRole, GetRole, HasRole, RemoveRole, RoleCount, IterateRoles |
the named roles |
| Aggregated | HasAny, TotalSize, IterateAll, RemoveFromAll |
base + every role, deduplicated |
| Aggregated | RolesContaining |
every role — base membership is not a role |
(NewGroup and the Readonly() views sit outside the families; views are
covered below.)
So with alice in the base set only and dave in the "council" role only:
1group.Has(alice) // true — alice is a base member
2group.Has(dave) // false — Has never consults roles
3group.HasAny(dave) // true — dave is somewhere in the group
4group.RolesContaining(dave) // ["council"]
An address may appear in the base set and several roles at once;
TotalSize and IterateAll count and yield it once. All iterators take
offset, count for pagination, and the callback returns true to stop.
RemoveRole discards only the role itself — its members stay wherever
else they appear. RemoveFromAll is the opposite: it purges one address
from the base set and every role.
Sharing across realms: readonly views
A *Group or *Role is a mutable handle: anyone holding it can change
your data (method calls run with the allocating realm's storage authority).
Readonly() returns a view that structurally cannot mutate — no mutator
methods exist on it at all.
Three rules at realm boundaries:
- Never accept a
*Group/*Rolefrom an untrusted caller. - Never return a
*Group/*Roleto one — returngroup.Readonly()(a*ReadonlyGroup) orrole.Readonly()instead. - Never trust a readonly view someone else hands you: it is a live window onto their data, which they can change between your reads.
The meta slot
Role.SetMeta(meta any) stores arbitrary per-role data — permission bits,
a description, a quorum. Store value types only (strings, ints, value
structs/slices). Do not store pointers to types with mutator methods (such
as *avl.Tree or *addrset.Set): Meta() returns the value as-is, so a
reader holding a readonly view could call those mutators on it.
See doc.gno for the precise security model, and
filetests/z_readme_filetest.gno for this README as a running example.
Package groups provides Groups containing a base address set plus named Roles, each with their own member set and metadata.
A Group is the top-level container — one per DAO, one per permissions instance, etc. A Role is a named subset within a Group with arbitrary per-role metadata.
The API separates three concerns explicitly, so each call site picks the right semantic:
- base-only operations: Add, Remove, Has, Size, Iterate;
- role registry operations: AddRole, GetRole, HasRole, RemoveRole, RoleCount, IterateRoles;
- aggregated operations across base + all roles: HasAny, TotalSize, IterateAll, RolesContaining, RemoveFromAll.
Security model
A Group, and the *Role values it hands out, are meant to be allocated and held by the consuming realm. Three rules apply at realm boundaries:
-
Do not ACCEPT a *Group or *Role from an external/untrusted caller — subsequent mutations would route to the allocating (attacker) realm's authority, and a poisoned Group could cause DoS or unexpected state.
-
Do not RETURN a *Group or *Role from any method or function callable by untrusted realms. Return *ReadonlyGroup or *ReadonlyRole instead. Exposing a mutable handle is exactly as dangerous as accepting one.
-
Do not TRUST a *ReadonlyGroup or *ReadonlyRole received from an untrusted caller. A readonly view is a live handle over its creator's data, not a snapshot: the sender controls the contents and can mutate them between reads. Base authorization and accounting decisions only on views derived from a Group you allocated yourself.
The Readonly() views are the only safe handles to cross a realm boundary — safe to hand out, per rule 3 not blindly safe to consume.
Metadata: do not store mutable pointers
Each Role has a free-form "meta any" slot. Meta() returns the stored value as-is, so a pointer stored in meta can be retrieved by an untrusted reader holding a Readonly() view. A direct field write through that pointer is still blocked by the realm-ownership gate, but invoking a MUTATOR METHOD on it (or passing it into a function that mutates by argument) runs under whatever realm allocated it (borrow rule #2) and commits the write. This includes common /p/ types such as *addrset.Set and *avl.Tree — they are mutable pointers, not "just data". Therefore store only:
- value types (ints, strings, value structs/slices with NO internal pointer reaching a mutator-bearing type), or
- a wrapper whose only exported methods are read-only and which holds no externally-mutable pointer.
Readonly views
Group and Role each expose a Readonly() method returning a typed read-only view (ReadonlyGroup, ReadonlyRole; role member sets surface as *addrset.ReadonlySet). The views are concrete structs with unexported fields and only read-side exported methods, so cross-package callers cannot mutate through them.
1
1
4
type Group
structGroup is a container with a base address set plus a registry of named Roles. The zero value is not usable; construct with NewGroup.
Security
A Group, and the *Role values it hands out, are meant to be allocated and held by the consuming realm. Three rules apply at realm boundaries:
-
Do not ACCEPT a *Group or *Role from an external/untrusted caller — subsequent mutations would route to the allocating (attacker) realm's authority, and a poisoned Group could cause DoS or unexpected state.
-
Do not RETURN a *Group or *Role from any method or function callable by untrusted realms. Return *ReadonlyGroup or *ReadonlyRole instead.
-
Do not TRUST a *ReadonlyGroup or *ReadonlyRole received from an untrusted caller — it is a live handle over the sender's data, not a snapshot; the contents are attacker-controlled and can change between reads.
Both directions matter: exposing a *Group to attacker code is as dangerous as accepting one. The Readonly() views are the only safe handles to cross a realm boundary.
Methods on Group
func Add
method on GroupAdd inserts addr into the base set. Returns true if newly added.
func AddRole
method on GroupAddRole registers a new empty role. Returns ErrEmptyName if name is empty, ErrRoleExists if a role of that name already exists.
func GetRole
method on GroupGetRole returns the mutable role if it exists.
SECURITY: the returned *Role exposes mutators (Members().Add/Remove, SetMeta). Do not pass it to untrusted callers — use GetRole on a *ReadonlyGroup for cross-realm exposure.
func Has
method on GroupHas reports whether addr is in the base set. It does NOT consult roles; use HasAny for an aggregated check.
func HasAny
method on GroupHasAny reports whether addr is in the base set OR in any role.
func HasRole
method on GroupHasRole reports whether a role with the given name exists.
func Iterate
method on GroupIterate walks the base set (only) in sorted order, starting at offset. fn returns true to stop; Iterate returns true if stopped early.
func IterateAll
method on GroupIterateAll walks every distinct address across base + all roles, deduplicated. Order: base first (in addrset order), then roles in name order, skipping addresses already yielded. offset and count apply to the deduplicated output, not the pre-dedup items; a negative offset counts as zero. fn returns true to stop; IterateAll returns true if stopped early.
The walk is live: do not mutate the group (base set, member sets, or the role registry) from within fn — registry mutation mid-iteration can panic and abort the transaction. Collect addresses first, mutate after IterateAll returns.
Implementation note: dedup tracks seen addresses in an internal addrset (O(N) memory in the addresses scanned); scanning stops as soon as the requested window has been served. For paginating a large Group without dedup, use Iterate (base only) or IterateRoles.
func IterateRoles
method on GroupIterateRoles walks roles in lexicographic name order, starting at offset and visiting up to count roles. The callback receives a *ReadonlyRole — deliberately not a *Role, so that plumbing an untrusted callback into the iteration cannot escalate into role mutation under this realm's authority. To mutate, capture names during iteration and revisit via GetRole from a trusted context after iteration returns; registry mutation (AddRole, RemoveRole) mid-iteration can panic and abort the transaction. fn returns true to stop; IterateRoles returns true if stopped early.
func Readonly
method on GroupReadonly returns a read-only view of the group.
func Remove
method on GroupRemove deletes addr from the base set. Returns true if it was present.
func RemoveFromAll
method on GroupRemoveFromAll removes addr from the base set and from every role. Returns true if it was removed from at least one location.
func RemoveRole
method on GroupRemoveRole removes the named role and its membership records. Members of the removed role are NOT removed from the base set or from any other role; only this role's own data is discarded. Returns false if no such role exists.
func RoleCount
method on GroupRoleCount returns the number of registered roles.
func RolesContaining
method on GroupRolesContaining returns the names of all roles containing addr, in lexicographic name order. The base set is not consulted (base membership is not a "role"). Returns nil if addr is in no roles.
func Size
method on GroupSize returns the number of addresses in the base set only.
func TotalSize
method on GroupTotalSize returns the count of distinct addresses across the base set and all roles, deduplicated. A caller may place the same address in base and in multiple roles; TotalSize counts it once.
Implementation note: dedup tracks seen addresses in an internal addrset, costing O(N) memory in the total membership.
type ReadonlyGroup
structReadonlyGroup is a read-only view of a Group. Every method mirrors the read-side of Group; mutators are absent. It holds the *Group in an unexported field, so cross-package callers cannot mutate through it.
Methods on ReadonlyGroup
func GetRole
method on ReadonlyGroupGetRole returns a read-only view of the named role if it exists.
func Has
method on ReadonlyGroupHas reports whether addr is in the base set (roles not consulted).
func HasAny
method on ReadonlyGroupHasAny reports whether addr is in the base set OR in any role.
func HasRole
method on ReadonlyGroupHasRole reports whether a role with the given name exists.
func Iterate
method on ReadonlyGroup1func (rg ReadonlyGroup) Iterate(offset, count int, fn func(addr address) bool) (stopped bool)Iterate walks the base set (only); see Group.Iterate.
func IterateAll
method on ReadonlyGroup1func (rg ReadonlyGroup) IterateAll(offset, count int, fn func(addr address) bool) (stopped bool)IterateAll walks every distinct address across base + all roles; see Group.IterateAll.
func IterateRoles
method on ReadonlyGroup1func (rg ReadonlyGroup) IterateRoles(offset, count int, fn func(*ReadonlyRole) bool) (stopped bool)IterateRoles walks roles in name order; see Group.IterateRoles.
func RoleCount
method on ReadonlyGroupRoleCount returns the number of registered roles.
func RolesContaining
method on ReadonlyGroupRolesContaining returns the names of all roles containing addr, in name order; see Group.RolesContaining.
func Size
method on ReadonlyGroupSize returns the number of addresses in the base set only.
func TotalSize
method on ReadonlyGroupTotalSize returns the deduplicated count across base + all roles.
type ReadonlyRole
structReadonlyRole is a read-only view of a Role. It exposes only read-side methods and holds the *Role in an unexported field, so cross-package callers cannot mutate the role through this type.
Methods on ReadonlyRole
func Members
method on ReadonlyRoleMembers returns a read-only view of the role's member set.
func Meta
method on ReadonlyRoleMeta returns the role's metadata slot.
NOTE: a mutable pointer stored in meta is NOT protected by this readonly view — the pointee remains mutable by anyone who retrieves it. See the package doc.
func Name
method on ReadonlyRoleName returns the role's name.
type Role
structRole is a named bucket of addresses with optional metadata.
A Role is always owned by a parent Group; the only way to obtain a *Role is Group.AddRole or Group.GetRole. See the Group doc for the realm- boundary rules that govern passing *Role values around.
Methods on Role
func Members
method on RoleMembers returns a mutable reference to the role's member set; mutations through the returned pointer affect the role.
SECURITY: the returned *addrset.Set is mutable. Do not expose it to untrusted callers — use Role.Readonly().Members() for a cross-realm-safe view.
func Meta
method on RoleMeta returns the role's metadata slot. See the package doc for the rule against storing mutable pointers in meta.
func Name
method on RoleName returns the role's registry name.
func Readonly
method on RoleReadonly returns a read-only view of the role.
func SetMeta
method on RoleSetMeta sets the role's metadata slot. Passing nil clears it.
SECURITY: do NOT store a pointer whose type has a mutator method (this includes common /p/ types like *addrset.Set or *avl.Tree) if untrusted realms may hold a Readonly() view of this Group. Meta() returns the stored value as-is, so a foreign reader can invoke that method and borrow rule #2 commits the write under this (the allocating) realm's authority. A direct field write through the pointer is still blocked by the realm-ownership gate — the leak is specifically mutator-method dispatch. Prefer value types with no internal pointers. See the package doc.