Cosmic Bull

Pure package on pearl-1

permbook

gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/permbook

pure packageprimitiveaccess-control

RenderedSourceCall builder

curated

Named permissions granted to and revoked from addresses, O(log P + log H) membership. Each consumer allocates its own Book with its own Limits, so capacity contention across applications is structurally impossible.

Identity

Import pathgno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/permbook
Kindpure package (/p/)
Chainpearl-1
Namespaceg1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3
Realm addressnone — A /p/ package is not a realm: it holds no state, custodies no coins and is never a transaction sender. The pkgPath derivation would still produce a value; recording one would name nothing.

Provenance

chain-attested
Deployed at height612,427
Deploy transaction87866042519c18702b5e434c35284e70cfa5a8075c967a43d90382c2f225ccfb look it up on the RPC
Deployerg1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3
Gas used34,757,728
Storage21,065 bytes, deposit 2106500ugnot
Files on chaingnomod.toml permbook.gno
Deployed bytespermbook.gno — 18,208 bytes
sha25635abf3906050b8e1873f999c90f6dad79eda8f40024012fec21b677cadd9fc56

Do not take the hash above on trust. $download returns the bytes pearl-1 is actually running; this command fetches them and prints their digest, which should equal the one in the table:

curl -sS 'https://pearl.testnets.gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/permbook$download&file=permbook.gno' | shasum -a 256

Expected: 35abf3906050b8e1873f999c90f6dad79eda8f40024012fec21b677cadd9fc56 — 18,208 bytes. This was checked for all 21 packages while building this site's architecture record; every one matched. Use curl: pearl's edge answers Python's default user-agent with HTTP 403.

API

chain-derived 3 exported functions, 2 types, 18 methods.

This is a /p/ package: you import it, you do not call it in a transaction. gnoweb's $help shows only exported top-level functions, so the types and methods below do not appear there at all — which is why this reference exists.

Overview

Package permbook is a bounded, authorization-carrying facade over gno.land/p/nt/groups/v0. It lets a realm define named permissions, grant them to addresses, revoke them, and ask "does this address currently hold this permission" without iterating anything.

It holds no coins, imports no banker, stores no callbacks, and owns no package-level state. The importing realm allocates a *Book and keeps it private.

What this package adds, and what it does not

It adds exactly four things to groups, and deliberately nothing else:

  1. an ADMIN bound to the book, with a two-step handoff;
  2. BOUNDS on permission count, holders per permission, and name shape;
  3. permission semantics instead of membership semantics — no base set, no metadata slot, and an empty permission is pruned rather than kept;
  4. a cross-realm surface that is safe by CONSTRUCTION — the *groups.Group is unexported and no method returns a mutable handle to it.

Everything else — the B+-tree registry, the member sets, ordered iteration, the readonly views — comes from groups. This package writes no data-structure code.

The query

if !book.Has("withdraw", who) { panic("not authorized") }

Has costs two independent B+-tree descents: O(log P) to find the permission, O(log H) to find the holder. It is NOT a function of how many permissions `who` already holds. That matters: every other named-permission implementation surveyed answers this by iterating the subject's permissions, which makes the most-privileged address the most expensive to check — the wrong asymptotic for an authorization check on a hot path.

Authorization

Mutators take (_ int, rlm realm) and identify the principal as rlm.Previous().Address(), after asserting rlm.IsCurrent(). The consuming realm threads its own cur:

func Grant(cur realm, perm string, addr address) { if err := book.Grant(0, cur, perm, addr); err != nil { panic(err) } }

IsCurrent() rejects a stale or stashed realm value, so a hostile realm cannot replay an old cur to impersonate the admin. This is the same shape as p/nt/ownable/v0 and p/nt/ownable/v0/exts/authorizable.

Lifecycle of a permission

A permission comes into existence on its first grant and ceases to exist when its last holder is revoked. There is no create step and no reserved name. Has returns false either way, so the distinction is invisible to a caller and the book does not accumulate empty buckets.

(absent) ──Grant──> held by 1..MaxHoldersPerPermission ──Revoke last──> (absent) │ └── DropPermission ──> (absent)

Admin handoff is two-step

NominateAdmin records a nominee; only AcceptAdmin, called by that nominee, moves the admin. A one-step transfer to a well-formed-but-unowned address is permanently fatal — address.IsValid only checks bech32 form, so a mistyped address passes validation and leaves a book nobody can ever grant or revoke on again. This is the Y4 finding from the audit of Cosmic Bull's own r/permission_registry, carried forward.

Consumer contract — the parts this package CANNOT enforce

  1. THREAD A LIVE cur. rlm.IsCurrent() proves the realm value came from a live crossing frame, and rlm.Previous().Address() is then the principal that crossed into your realm. A consumer that wraps a permbook mutator in a NON-crossing exported helper resolves its importer's caller instead of its importer — Class-2 designation forgery, in the consumer. Call permbook mutators from your own crossing entrypoints, passing that entrypoint's own cur.
  2. DO NOT LEAK THE *Book. No method here returns a mutable handle, but a consumer that exports its *Book — or returns it from a function reachable by another realm — hands out every mutator on it, and borrow rule #2 commits those writes under YOUR realm's authority. Keep it in an unexported package-level variable.
  3. Has ANSWERS ABOUT AN ADDRESS, NOT ABOUT YOUR CALLER. It performs no authentication. Derive the address from your own crossing entrypoint's cur.Previous().Address() and pass it in.
  4. CHOOSE LIMITS DELIBERATELY. They are fixed for the life of a Book. A consumer that needs different bounds later must allocate a second Book; nothing here migrates state between them.

Note on the groups base set

A groups.Group carries a base address set alongside its named roles. This package never writes to it, so it is provably empty for any Book, and "in the base set" can never mean "holds a permission".

Imports

Constants and variables

Ceilings on what Limits may be configured to. They bound worst-case gas and storage for any Book, however the consumer configures it.

MaxPermissionsCeiling is the one that bounds GAS rather than merely storage. Three operations walk every permission in the book at O(P log H) — Permissions, HasAny, and RevokeAll — and this ceiling is what bounds them. Everything else is logarithmic or paginated; see each method's own doc for its cost, which is authoritative.

In particular HasAny is NOT a cheap variant of Has. Has is two tree descents; HasAny is a full walk of the registry. Do not put HasAny on a hot authorization path or inside a per-item render loop.

These ceilings are not the objection this package raises against a shared registry. A shared registry's caps are rivalrous and unraisable because one immutable realm holds every tenant's state; here a consumer that needs more simply allocates another Book in its own realm, at no cost to anyone else.

const (
	MaxPermissionsCeiling = 256
	MaxHoldersCeiling     = 10000
	MaxNameLenCeiling     = 64
)

Default limits, used by NewDefault.

const (
	DefaultMaxPermissions          = 64
	DefaultMaxHoldersPerPermission = 1024
	DefaultMaxNameLen              = 32
)
var (
	ErrUnauthorized      = errors.New("permbook: caller is not the admin")
	ErrNotLiveRealm      = errors.New("permbook: rlm is not the caller's live cur")
	ErrInvalidAddress    = errors.New("permbook: invalid address")
	ErrInvalidName       = errors.New("permbook: permission name must be 1..MaxNameLen chars, lowercase alphanumeric and underscore only")
	ErrInvalidLimits     = errors.New("permbook: limits must be positive and within the package ceilings")
	ErrAlreadyGranted    = errors.New("permbook: address already holds this permission")
	ErrNotGranted        = errors.New("permbook: address does not hold this permission")
	ErrPermissionLimit   = errors.New("permbook: permission limit reached for this book")
	ErrHolderLimit       = errors.New("permbook: holder limit reached for this permission")
	ErrNoPendingAdmin    = errors.New("permbook: no pending admin nomination")
	ErrNotPendingAdmin   = errors.New("permbook: caller is not the pending admin")
	ErrSameAdmin         = errors.New("permbook: nominee is already the admin")
	ErrUnknownPermission = errors.New("permbook: no such permission")
)

Types

type Book

type Book struct {
	g            *groups.Group
	admin        address
	pendingAdmin address
	lim          Limits
}

Book is a bounded set of named permissions with an admin. The zero value is not usable; construct with New or NewDefault.

SECURITY: keep a *Book in an unexported variable. It is the capability. Every mutator on it is gated on the admin, but a realm that receives the pointer itself can invoke those mutators, and borrow rule #2 commits the writes under the ALLOCATING realm's authority.

Book.AcceptAdmin

func (b *Book) AcceptAdmin(_ int, rlm realm) error

AcceptAdmin completes a pending handoff. Only the nominee may call it.

Book.Admin

func (b *Book) Admin() address

Admin returns the book's current admin.

Book.CancelNomination

func (b *Book) CancelNomination(_ int, rlm realm) error

CancelNomination withdraws a pending nomination. Admin only.

Book.DropPermission

func (b *Book) DropPermission(_ int, rlm realm, perm string) error

DropPermission removes a permission and every grant of it. Admin only.

Cost is O(log P): groups discards the whole member set with the role and does not walk it, so this is safe for a permission with many holders.

Book.Grant

func (b *Book) Grant(_ int, rlm realm, perm string, addr address) error

Grant gives addr the named permission. Admin only.

The permission is created if it does not exist. Granting a permission the address already holds returns ErrAlreadyGranted rather than silently succeeding, so a consumer cannot mistake a no-op for a state change.

Book.Has

func (b *Book) Has(perm string, addr address) bool

Has reports whether addr currently holds the named permission.

Two B+-tree descents, O(log P + log H). Independent of how many other permissions addr holds. Never panics; unknown permissions report false.

Book.HasAny

func (b *Book) HasAny(addr address) bool

HasAny reports whether addr holds any permission at all. O(P log H).

Book.HolderCount

func (b *Book) HolderCount(perm string) int

HolderCount returns how many addresses hold the named permission, or 0 if it does not exist.

Book.Holders

func (b *Book) Holders(perm string, offset, count int) []address

Holders returns up to count holders of the named permission, in sorted order, starting at offset. Paginated for the same reason as PermissionNames. Returns nil for an unknown permission.

Book.IsAdmin

func (b *Book) IsAdmin(addr address) bool

IsAdmin reports whether addr is the book's admin.

Book.Limits

func (b *Book) Limits() Limits

Limits returns the book's fixed limits.

Book.NominateAdmin

func (b *Book) NominateAdmin(_ int, rlm realm, nominee address) error

NominateAdmin records a nominee for the admin role. Admin only. The handoff does NOT take effect until the nominee calls AcceptAdmin, and a nomination may be withdrawn with CancelNomination until then.

Book.PendingAdmin

func (b *Book) PendingAdmin() address

PendingAdmin returns the nominated-but-not-yet-accepted admin, or the empty address if there is no pending nomination.

Book.PermissionCount

func (b *Book) PermissionCount() int

PermissionCount returns how many distinct permissions currently exist.

Book.PermissionNames

func (b *Book) PermissionNames(offset, count int) []string

PermissionNames returns up to count permission names in lexicographic order, starting at offset. Paginated so the caller, not the book, chooses how much work a single call does.

Book.Permissions

func (b *Book) Permissions(addr address) []string

Permissions returns the names addr holds, in lexicographic order, or nil. O(P log H) — bounded by MaxPermissions.

Book.Revoke

func (b *Book) Revoke(_ int, rlm realm, perm string, addr address) error

Revoke removes the named permission from addr. Admin only.

Revoking the last holder removes the permission itself, freeing its slot against MaxPermissions. Has reports false either way, so this is invisible to a caller and keeps the book from accumulating empty buckets.

Book.RevokeAll

func (b *Book) RevokeAll(_ int, rlm realm, addr address) (int, error)

RevokeAll removes addr from every permission in the book and reports how many were removed. Admin only.

Cost is O(P log H) in the book's permission count — bounded by MaxPermissions, which is why that limit has a ceiling.

Implementation note: the permission names are collected FIRST, into a value slice, and the registry is mutated only after that walk returns. groups documents that mutating the role registry mid-iteration can panic and abort the transaction.

type Limits

type Limits struct {
	// MaxPermissions bounds how many distinct permission names may exist at
	// once. Because Permissions and RevokeAll walk all of them, this bounds
	// gas, not just storage.
	MaxPermissions int

	// MaxHoldersPerPermission bounds how many addresses may hold any one
	// permission. No operation iterates holders unpaginated, so this bounds
	// storage rather than gas.
	MaxHoldersPerPermission int

	// MaxNameLen bounds permission-name length.
	MaxNameLen int
}

Limits are fixed at construction and never change for the life of a Book.

Exported fieldTypeDoc
MaxPermissionsintMaxPermissions bounds how many distinct permission names may exist at once. Because Permissions and RevokeAll walk all of them, this bounds gas, not just storage.
MaxHoldersPerPermissionintMaxHoldersPerPermission bounds how many addresses may hold any one permission. No operation iterates holders unpaginated, so this bounds storage rather than gas.
MaxNameLenintMaxNameLen bounds permission-name length.

Functions

DefaultLimits

func DefaultLimits() Limits

DefaultLimits returns the limits used by NewDefault.

New

func New(admin address, lim Limits) (*Book, error)

New constructs an empty Book owned by admin, with explicit limits.

NewDefault

func NewDefault(admin address) (*Book, error)

NewDefault constructs an empty Book owned by admin, with DefaultLimits.


Doc text is reproduced as vm/qdoc returns it. The node markdown-escapes doc comments, so a bracket or angle bracket may carry a backslash the committed source does not have. The source itself is at source and in this repository.

Dependencies

chain-attested
Importserrors, gno.land/p/nt/groups/v0
First-party dependenciesnone
Used bypermbook_demo

Known limitations

Recorded by the people who built and deployed it. This list is deliberately not empty where honesty costs something.

curated

Source and records

Source filepearl/p/permbook/permbook.gno at commit 6a510c665a53 in the project repository (not public — the digest command above is the check that needs no repository)
Matches the deployed bytesyes — byte-identical
Registered ingno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/service_registry as permbook (type library)
Recordscatalog/primitives.md#permbook
pearl/DEPLOYMENT.md