Pure package on pearl-1
permbook
gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/permbook
pure packageprimitiveaccess-control
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 path | gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/permbook |
|---|---|
| Kind | pure package (/p/) |
| Chain | pearl-1 |
| Namespace | g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3 |
| Realm address | none — 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 height | 612,427 |
|---|---|
| Deploy transaction | 87866042519c18702b5e434c35284e70cfa5a8075c967a43d90382c2f225ccfb look it up on the RPC |
| Deployer | g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3 |
| Gas used | 34,757,728 |
| Storage | 21,065 bytes, deposit 2106500ugnot |
| Files on chain | gnomod.toml permbook.gno |
| Deployed bytes | permbook.gno — 18,208 bytes |
| sha256 | 35abf3906050b8e1873f999c90f6dad79eda8f40024012fec21b677cadd9fc56 |
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 256Expected: 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:
- an ADMIN bound to the book, with a two-step handoff;
- BOUNDS on permission count, holders per permission, and name shape;
- permission semantics instead of membership semantics — no base set, no metadata slot, and an empty permission is pruned rather than kept;
- 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
- 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.
- 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.
- 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.
- 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
errorsgno.land/p/nt/groups/v0
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 field | Type | Doc |
|---|---|---|
MaxPermissions | int | 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. |
MaxHoldersPerPermission | int | MaxHoldersPerPermission bounds how many addresses may hold any one permission. No operation iterates holders unpaginated, so this bounds storage rather than gas. |
MaxNameLen | int | MaxNameLen 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| Imports | errors, gno.land/p/nt/groups/v0 |
|---|---|
| First-party dependencies | none |
| Used by | permbook_demo |
Known limitations
Recorded by the people who built and deployed it. This list is deliberately not empty where honesty costs something.
curated- One consumer on chain (permbook_demo); its reusability is a design argument, not a demonstrated fact.
Source and records
| Source file | pearl/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 bytes | yes — byte-identical |
| Registered in | gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/service_registry as permbook (type library) |
| Records | catalog/primitives.md#permbookpearl/DEPLOYMENT.md |