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

v0 source pure

Package groups provides Groups containing a base address set plus named Roles, each with their own member set and met...

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

  1. Never accept a *Group/*Role from an untrusted caller.
  2. Never return a *Group/*Role to one — return group.Readonly() (a *ReadonlyGroup) or role.Readonly() instead.
  3. 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.

Overview

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:

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

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

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

Variables 1

Functions 1

func NewGroup

1func NewGroup() *Group
source

NewGroup constructs an empty group.

Types 4

type Group

struct
1type Group struct {
2	base  *addrset.Set
3	roles *bptree.BPTree // name -> *Role
4}
source

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

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

  2. Do not RETURN a *Group or *Role from any method or function callable by untrusted realms. Return *ReadonlyGroup or *ReadonlyRole instead.

  3. 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 Group
1func (g *Group) Add(addr address) (added bool)
source

Add inserts addr into the base set. Returns true if newly added.

func AddRole

method on Group
1func (g *Group) AddRole(name string) (*Role, error)
source

AddRole registers a new empty role. Returns ErrEmptyName if name is empty, ErrRoleExists if a role of that name already exists.

func GetRole

method on Group
1func (g *Group) GetRole(name string) (r *Role, found bool)
source

GetRole 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 Group
1func (g *Group) Has(addr address) bool
source

Has reports whether addr is in the base set. It does NOT consult roles; use HasAny for an aggregated check.

func HasAny

method on Group
1func (g *Group) HasAny(addr address) bool
source

HasAny reports whether addr is in the base set OR in any role.

func HasRole

method on Group
1func (g *Group) HasRole(name string) bool
source

HasRole reports whether a role with the given name exists.

func Iterate

method on Group
1func (g *Group) Iterate(offset, count int, fn func(addr address) bool) (stopped bool)
source

Iterate 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 Group
1func (g *Group) IterateAll(offset, count int, fn func(addr address) bool) (stopped bool)
source

IterateAll 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 Group
1func (g *Group) IterateRoles(offset, count int, fn func(*ReadonlyRole) bool) (stopped bool)
source

IterateRoles 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 Group
1func (g *Group) Readonly() *ReadonlyGroup
source

Readonly returns a read-only view of the group.

func Remove

method on Group
1func (g *Group) Remove(addr address) (removed bool)
source

Remove deletes addr from the base set. Returns true if it was present.

func RemoveFromAll

method on Group
1func (g *Group) RemoveFromAll(addr address) (removed bool)
source

RemoveFromAll 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 Group
1func (g *Group) RemoveRole(name string) (removed bool)
source

RemoveRole 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 Group
1func (g *Group) RoleCount() int
source

RoleCount returns the number of registered roles.

func RolesContaining

method on Group
1func (g *Group) RolesContaining(addr address) []string
source

RolesContaining 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 Group
1func (g *Group) Size() int
source

Size returns the number of addresses in the base set only.

func TotalSize

method on Group
1func (g *Group) TotalSize() int
source

TotalSize 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

struct
1type ReadonlyGroup struct {
2	group *Group
3}
source

ReadonlyGroup 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 ReadonlyGroup
1func (rg ReadonlyGroup) GetRole(name string) (rr *ReadonlyRole, found bool)
source

GetRole returns a read-only view of the named role if it exists.

func Has

method on ReadonlyGroup
1func (rg ReadonlyGroup) Has(addr address) bool
source

Has reports whether addr is in the base set (roles not consulted).

func HasAny

method on ReadonlyGroup
1func (rg ReadonlyGroup) HasAny(addr address) bool
source

HasAny reports whether addr is in the base set OR in any role.

func HasRole

method on ReadonlyGroup
1func (rg ReadonlyGroup) HasRole(name string) bool
source

HasRole reports whether a role with the given name exists.

func Iterate

method on ReadonlyGroup
1func (rg ReadonlyGroup) Iterate(offset, count int, fn func(addr address) bool) (stopped bool)
source

Iterate walks the base set (only); see Group.Iterate.

func IterateAll

method on ReadonlyGroup
1func (rg ReadonlyGroup) IterateAll(offset, count int, fn func(addr address) bool) (stopped bool)
source

IterateAll walks every distinct address across base + all roles; see Group.IterateAll.

func IterateRoles

method on ReadonlyGroup
1func (rg ReadonlyGroup) IterateRoles(offset, count int, fn func(*ReadonlyRole) bool) (stopped bool)
source

IterateRoles walks roles in name order; see Group.IterateRoles.

func RoleCount

method on ReadonlyGroup
1func (rg ReadonlyGroup) RoleCount() int
source

RoleCount returns the number of registered roles.

func RolesContaining

method on ReadonlyGroup
1func (rg ReadonlyGroup) RolesContaining(addr address) []string
source

RolesContaining returns the names of all roles containing addr, in name order; see Group.RolesContaining.

func Size

method on ReadonlyGroup
1func (rg ReadonlyGroup) Size() int
source

Size returns the number of addresses in the base set only.

func TotalSize

method on ReadonlyGroup
1func (rg ReadonlyGroup) TotalSize() int
source

TotalSize returns the deduplicated count across base + all roles.

type ReadonlyRole

struct
1type ReadonlyRole struct {
2	role *Role
3}
source

ReadonlyRole 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 ReadonlyRole
1func (rr ReadonlyRole) Members() *addrset.ReadonlySet
source

Members returns a read-only view of the role's member set.

func Meta

method on ReadonlyRole
1func (rr ReadonlyRole) Meta() any
source

Meta 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 ReadonlyRole
1func (rr ReadonlyRole) Name() string
source

Name returns the role's name.

type Role

struct
1type Role struct {
2	name    string
3	members *addrset.Set
4	meta    any
5}
source

Role 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 Role
1func (r *Role) Members() *addrset.Set
source

Members 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 Role
1func (r *Role) Meta() any
source

Meta returns the role's metadata slot. See the package doc for the rule against storing mutable pointers in meta.

func Name

method on Role
1func (r *Role) Name() string
source

Name returns the role's registry name.

func Readonly

method on Role
1func (r *Role) Readonly() *ReadonlyRole
source

Readonly returns a read-only view of the role.

func SetMeta

method on Role
1func (r *Role) SetMeta(meta any)
source

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

Imports 3

Source Files 8