permission_registry
gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/permission_registry on onyx-1 testnet
realmapplicationaccess-controllive on onyx-1
Register a resource and control who may do what with it.
Access rules live in one shared place with a clear owner, so a realm can check a caller's permission instead of hard-coding addresses.
In detail curated
MULTI-TENANT NAMED-PERMISSION REGISTRY -- resource -> permission -> holder, with an admin per resource, two-step admin transfer under nominee consent, pruning of emptied permissions, and a 90-day name reservation that preserves the original creator's reclaim right. Has(resource, permission, holder) is the read consuming realms call. NO CUSTODY BY CONSTRUCTION: no banker, no payable path, and rejectStraySend(cur) on all 8 entrypoints, so the realm address reads exists:false / coins "0". THIS IS NOT A NEW APPLICATION AND THE DEPLOY'S OBJECTIVE IS NARROW. The same bytes were audited (1 RED / 6 YELLOW, all resolved before broadcast), remediated and deployed to pearl-1 on 2026-09-21 at height 604615, tx a30c0942...; pearl-1 then retired on 2026-09-30 with all four hostnames NXDOMAIN, so that record is historical -- frozen, unverifiable and not re-derivable (section 7c). This broadcast re-establishes QUERYABLE live behavioural evidence for exactly these bytes on a chain that answers. The port is zero-substitution: the source has no first-party imports and no address literal, so only gnomod.toml's module line differs between the chain copies, and permission_registry.gno is BYTE-IDENTICAL to the pearl-1 and mainnet trees at sha256 603fec57... Byte-match PASSED on onyx-1 (raw-RPC vm/qfile, 6 of 6 checks). Selected under section 8a, which delegates the TESTNET selection and nothing downstream; the gnoland-1 selection remains the operator's.
Use it
A realm holds state. Reading it is a free query; changing it is a transaction you sign yourself.
Read it — no key, no account, no GNOT
gnokey query vm/qeval \
--data 'gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/permission_registry.ListResources()' \
--remote https://rpc.onyx.testnets.gno.land:443ListResources is read-only, so this is a query rather than a transaction: it costs nothing, signs nothing and needs no funded account. Every exported function is listed under the API below; swap the name and the arguments to read any of the others.
Change its state
Every state change is a transaction you sign with your own key — this site never asks for one and holds none. You do not have to hand-write the transaction: gnoweb generates the exact gnokey maketx call command for any of this realm's 7 crossing functions, with the argument names filled in.
Read the known limitations further down this page first. They record what was not verified on chain, which is usually the part that matters.
Import it from another realm
import "gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/permission_registry"The path is the address: there is no package manager, nothing to add to a manifest and no version to pin, so the package you compile against is the one the chain runs. It differs per chain because the namespace in it is the deploying account's.
Deployments
onyx-1 testnet onyx-1
RenderedSourceCall builderState
chain-attested| Import path | gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/permission_registry |
|---|---|
| Namespace | g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3 |
| Deployed at height | 686,107 |
| Deploy transaction | 915e3fb96a3145b4831e2d3eaa80baa489da0c8b78ca210fa3141a4964da52fc look it up on the RPC |
| Deployer | g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3 |
| Gas used | 25,626,543 |
| Storage | not attested per package on this chain — see the limitations below |
| Files on chain | gnomod.toml permission_registry.gno |
| Deployed bytes | permission_registry.gno — 18,070 bytes |
| sha256 | 603fec571a55d051353e101c64c0490a5268124abda8d88ac703b9bbf26fb763 |
| Realm address | g1x9tatmqcdlas2w6k724mrs2zcgh3awf3s5wkhk |
Do not take the hash above on trust. $download returns the bytes onyx-1 is actually running; this command fetches them and prints their digest, which should equal the one in the table:
curl -sS 'https://onyx.testnets.gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/permission_registry$download&file=permission_registry.gno' | shasum -a 256Expected: 603fec571a55d051353e101c64c0490a5268124abda8d88ac703b9bbf26fb763 — 18,070 bytes. Use curl: the gno.land edges answer Python's default user-agent with HTTP 403.
API
chain-derived 13 exported functions, 1 type.
Every function below deep-links to gnoweb's call builder, which generates a ready-to-run gnokey maketx call for it:
AcceptAdminCancelAdminTransferCreateResourceDeleteResourceGetAdminGetPendingAdminGetPermissionsGrantHasListResourcesRenderRevokeTransferAdmin
Overview
This package carries no package doc comment on chain, so there is nothing for vm/qdoc to return and gnoweb's $help Overview is empty. Deployed bytes are immutable, so this cannot be repaired in place — see [an unpublished record] §2.4.
Imports
chain/runtime/unsafesortstrconvstringstime
Constants and variables
const (
// MaxResources bounds total registry state. Raised from the upstream
// 200 as part of the R1 remediation: with a per-admin quota now
// carrying the anti-monopoly duty, the global cap is a pure state
// bound rather than the sole defense against namespace exhaustion.
MaxResources = 1000
// MaxResourcesPerAdmin bounds how many resources one address may hold
// at once. R1 (audit 2026-09-21): the upstream design had only a
// global cap on a permissionless shared registry, so one unprivileged
// key could occupy every slot for ~200 cheap transactions and
// permanently deny the registry to every other tenant. DeleteResource
// is admin-only, so the squat was irreversible.
MaxResourcesPerAdmin = 20
MaxPermissionsPerResource = 50
MaxHoldersPerPermission = 200
MaxNameLen = 64
// ReservationPeriod is how long a deleted resource name stays
// reserved. Finite (re-audit 2026-09-02): eternal tombstones let an
// attacker cycle create/delete to lock the namespace forever.
ReservationPeriod = int64(90 * 24 * 3600) // 90 days
// Render bounds (Y3, audit 2026-09-21). Render walks the whole
// registry and is reachable by any viewer through gnoweb and
// vm/qrender, so its cost is borne by third parties rather than by
// whoever grew the state. Uncapped, the declared limits allowed
// 1000*50*200 rendered holder entries. The full data stays available
// through ListResources / GetPermissions / Has, which are bounded per
// call by construction.
MaxRenderResources = 20
MaxRenderPermissions = 8
MaxRenderHolders = 10
)
Types
type Reservation
type Reservation struct {
Admin address
Creator address
Expires time.Time
}
Reservation holds a deleted resource name for its former admin AND its original creator, and expires.
Functions
AcceptAdmin
func AcceptAdmin(cur realm, resourceName string)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asAcceptAdmin(cross(cur), ...).
AcceptAdmin completes a pending handoff; only the nominee may call it. The nominee's quota is checked HERE — at consent time — so a nomination can never push an account past MaxResourcesPerAdmin without that account agreeing to carry the resource.
CancelAdminTransfer
func CancelAdminTransfer(cur realm, resourceName string)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asCancelAdminTransfer(cross(cur), ...).
CancelAdminTransfer withdraws a pending nomination. Only the current admin can call this.
CreateResource
func CreateResource(cur realm, resourceName string)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asCreateResource(cross(cur), ...).
CreateResource registers a new named resource. The caller becomes its admin and is the only address that can grant or revoke permissions on it. A deleted resource name stays reserved for its former admin and its original creator until the reservation expires.
Each address may administer at most MaxResourcesPerAdmin resources at once, and the registry holds at most MaxResources in total.
DeleteResource
func DeleteResource(cur realm, resourceName string)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asDeleteResource(cross(cur), ...).
DeleteResource removes a resource and every permission under it. Only the resource admin can call this. The name stays reserved for the caller and for the original creator: nobody else can re-create it and inherit its consumers until the reservation expires.
GetAdmin
func GetAdmin(resourceName string) string
GetAdmin returns the admin address of a resource.
GetPendingAdmin
func GetPendingAdmin(resourceName string) string
GetPendingAdmin returns the nominated admin awaiting acceptance for a resource, or "none".
GetPermissions
func GetPermissions(resourceName string, addr address) string
GetPermissions returns all permission names granted to addr on a resource, as a comma-separated string. Returns "none" if the address has no permissions.
Grant
func Grant(cur realm, resourceName, permission string, addr address)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asGrant(cross(cur), ...).
Grant gives an address a named permission on a resource. Only the resource admin can call this. Panics if the permission is already granted to avoid silent no-ops.
Has
func Has(resourceName, permission string, addr address) bool
Has returns true if addr holds the named permission on the resource. Returns false (never panics) for unknown resources or permissions.
INTEGRATOR CONTRACT (Y7): Has takes the subject address explicitly and performs NO caller authentication — it answers "does this address hold this permission", not "may my caller do this". A consuming realm must derive addr from its own crossing entrypoint's cur.Previous().Address() and pass it in. Deriving it inside a non-crossing helper via unsafe.PreviousRealm() resolves the consumer's own caller's caller and is a Class-2 designation-forgery bug in the consumer.
ListResources
func ListResources() string
ListResources returns all registered resource names as a comma-separated string in registration order. Returns "none" if no resources exist.
Render
func Render(path string) string
Render returns a markdown overview. Never panics. Output is bounded by MaxRenderResources / MaxRenderPermissions / MaxRenderHolders (Y3); truncated sections name the query to use for complete data.
Revoke
func Revoke(cur realm, resourceName, permission string, addr address)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asRevoke(cross(cur), ...).
Revoke removes a permission from an address. Only the resource admin can call this. Panics if the permission was not granted. A permission left with no holders is pruned from the resource's permission list.
TransferAdmin
func TransferAdmin(cur realm, resourceName string, newAdmin address)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asTransferAdmin(cross(cur), ...).
TransferAdmin nominates a new admin for a resource. Only the current admin can call this, and the handoff does NOT take effect until the nominee calls AcceptAdmin.
Y4 (audit 2026-09-21): the upstream one-step transfer made a well-formed-but-unowned destination permanently fatal. address.IsValid only checks bech32 form, so a mistyped address passed the check and left the resource with an admin nobody controls — it could never again be granted on, revoked from, transferred or deleted, and its slot was lost from both the global cap and the former admin's quota forever. Nomination is reversible; only the nominee's consent is final.
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 | chain/runtime/unsafe, sort, strconv, strings, time |
|---|---|
| First-party dependencies | none |
Known limitations
Recorded by the people who built and deployed it. This list is deliberately not empty where honesty costs something.
curated- NO LIVE LIFECYCLE HAS BEEN EXERCISED ON onyx-1 YET. What is proven live on this chain is the DEPLOY and the BYTES, not the behaviour under transactions: the post-broadcast behavioural probe is 14 of 14 AGREE over raw-RPC vm/qeval, bracketed against the same instrument reading 14 ABSENT before the broadcast, but every one of those 14 is a PURE VIEW OR A CONSTANT. vm/qeval discards writes, so a view proves the bytes answer and does NOT prove a write commits. The full live lifecycle -- CreateResource, Grant/Revoke, the two-step admin transfer with nominee consent, the squat/reclaim sequence, and the 7 adversarial rejections -- was exercised on PEARL-1 and that record is historical, frozen and not re-derivable (section 7c). Re-establishing it on onyx-1 is the remaining work and is not claimed here.
- THE 14 PROBE CASES ARE SOURCE-SPECIFIC BY CONSTRUCTION, WHICH IS WHAT MAKES THEM EVIDENCE ABOUT THESE BYTES RATHER THAN ABOUT ANY PERMISSION REGISTRY. They assert the R1 remediation constants (MaxResourcesPerAdmin 20, which does not exist upstream at all, and MaxResources 1000 raised from the upstream 200), the Y3 render bounds (MaxRenderResources 20, MaxRenderPermissions 8, MaxRenderHolders 10, where upstream Render is uncapped), the FINITE ReservationPeriod 7776000 (90*24*3600; the upstream tombstone was eternal), and the exact return strings of THIS source ('not found', 'none', false). ListResources() == "none" is also the freshness claim: a non-empty answer would mean somebody had already written to the realm. TWO CONTROLS RAN FIRST and the report is withheld if either misbehaves -- a positive control extracting a live sibling's declared const (budgets.MaxLiveGrants == 4096) and an ABSENT control on a path that cannot exist, which must read /vm.InvalidPkgPathError rather than collapse into DISAGREE.
- THE BEHAVIOURAL PROBE AND THE BYTE-MATCH ARE RAW-RPC AND CARRY NO SECTION 9a STAMP. Re-measured on this build (gnomcp #107, 0.12.3+pkgdir-107.867cc0e) against CLAUDE.md section 9a's own published control reads: gno_eval, gno_packages and gno_render return structuredContent {chain_id_bound: true} with NO PAYLOAD AT ALL, so the evidence-grade read set is gno_read and gno_account alone. The 9a citation for these bytes is therefore a STAMPED gno_read full=true, which returned the whole 18,070-byte source beside chain_id_bound: true; the vm/qfile byte-match and the vm/qeval probe are strong evidence and ALSO unstamped, and are cited as raw-RPC.
- gnomod.toml IS NOT BYTE-EQUAL TO THE COMMITTED FILE AND MUST NOT BE, because the chain AUTHORS an [addpkg] stanza into it at deploy time: 210 bytes on chain against 95 committed, the difference being creator, height and max_deposit. The byte-match therefore asserts a RELATION for this file rather than equality -- stanza present, prefix byte-equal to the commit, creator == the signer, height == 686107, and the stanza is the ONLY difference (the non-vacuity check) -- which is strictly STRONGER than equality and additionally pins the height and creator from the chain's own bytes. The file is NOT excluded from the denominator; dropping it would be the shrinking-denominator fail-open (AL10). My first instrument reported MISMATCH here and the defect was its SCOPE, not the payload.
- CAPS ARE GLOBAL, SHARED AND UNRAISABLE ON AN IMMUTABLE REALM -- MaxResources 1000, MaxResourcesPerAdmin 20, MaxPermissionsPerResource 50, MaxHoldersPerPermission 200, MaxNameLen 64. The caps are read live and correct, and they cannot be changed after addpkg. p/permbook exists because of this. The bounds themselves are not exercised live on either chain -- each needs hundreds of funded transactions -- and are correlated to the committed suite: TestCreate_GlobalCap, TestCreate_PerAdminCap, TestGrant_PermissionCap and the holder-cap test, 25 test functions in all in permission_registry_test.gno.
- EXPIRED-TOMBSTONE GROWTH WAS ASSESSED AND DELIBERATELY NOT CHANGED, graded INFORMATIONAL with the reasoning recorded: `retired` is never iterated and the cost falls on the caller's own storage deposit. It was NOT downgraded in order to reach a deployment. The Y2 class is separate and is recorded against spendbook, not here.
- THE DEPENDENCY CLOSURE IS STDLIB-ONLY, SO THE EQUIVALENT VERDICT MEANS 'NO CHAIN DEPENDENCIES AT ALL' RATHER THAN 'A CLOSURE WAS VERIFIED' (section 7a). Gate B compared 0 dependencies and its chain_closure_digest is e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -- the sha256 of EMPTY BYTES, which is what zero compared dependencies looks like and must not be read as a digest of something. Gate A, run with --include-tests, compared 4 (p/nt/testutils/v0, p/nt/uassert/v0, p/nt/ufmt/v0, p/onbloc/diff/v0) and is EQUIVALENT at digest 435f2eed16b15b3d6371dbff434b3e87bca7aee346454aa85b01e1a922d2ac1a. The STDLIB RESIDUAL is uncovered on both: the five imports here -- chain/runtime/unsafe, sort, strconv, strings, time -- execute from the node binary and are not compared by the closure gate.
- Sybil namespace exhaustion is irreversible past the R1 per-admin quota: ~50 keys x 20 creates fill MaxResources=1000 forever (DeleteResource admin-only). The audit also verified SEC7's three uncaught render terms are confined to the display layer -- none reaches an authorization or funds path, and this realm has no funds path at all. Audit 2026-11-04, disclosed per the operator's disclose-and-queue-successor rule; details and severity in [an unpublished record].
Source
| Source file | onyx/r/permission_registry/permission_registry.gno at commit c2e10d17ffbf in the project repository (not public — the digest command above is the check that needs no repository) |
|---|---|
| Matches the deployed bytes | yes — byte-identical |
| Upstream repository | https://github.com/SillyZir/permission_registry |
| Deployed from commit | e3d6aeaf3571ec35c2af68c5fbb756480b76ed03 |