const MaxCurators
MaxCurators bounds curators and open invitations together. The curator list renders on the index page, and only curators can grow it, but a bound costs nothing and an unbounded list on a public page is a page someone can break.
Package zones is a curated registry of gno.land networks: mainnet, the testnets, staging chains, anybody's gnodev. Ea...
A curated registry of gno.land networks and the endpoints that reach them: mainnet, the testnets, staging chains, anybody's gnodev. Anybody proposes a zone; a curator approves, rejects or retires it, and verifies or flags its endpoints. The latest decision on each is recorded with who made it and when, and a rejection, a retirement, a flag or an edit to an approved zone must also say why, on the zone's page. Nothing is hidden while it waits: proposals and unverified endpoints are listed, and labelled.
The model, validation and state machine are p/moul/zones.
This realm owns only who may write, and the pages.
All approved at deploy, every address probed on 2026-10-01. Peers are the
persistent_peers each network's VALIDATOR.md publishes in the gnolang/gno
monorepo.
| slug | chain id | endpoints |
|---|---|---|
mainnet |
gnoland-1 |
rpc, gnoweb, 2 peers, indexer, explorer, all verified |
onyx |
onyx-1 |
rpc, gnoweb, 2 peers, indexer, faucet, explorer, all verified |
staging |
staging |
rpc, gnoweb, flagged: neither answered when seeded, and the flag says what each returned |
moul-staging |
moulstaging-1 |
rpc, gnoweb, faucet, all verified |
The reads take plain values, so they work from gnokey query vm/qeval and from
another realm alike. An empty kind or status matches anything; a slug is
required wherever one is asked for.
1gnokey query vm/qeval -remote https://rpc.gno.land \
2 -data 'gno.land/r/moul/zones/v0.ListAddresses("onyx", "peer", "verified")'
| function | answers |
|---|---|
ListZones(status, kind) |
the zones, in the order they were proposed; approved is the official list |
GetZone(slug) |
one zone, and whether it exists |
ListEndpoints(slug, kind, status) |
one zone's endpoints, oldest first |
ListAddresses(slug, kind, status) |
the same, reduced to the address strings a config file wants |
GetEndpoint(id) |
one endpoint, and whether it exists |
IsCurator(addr), Curators() |
who curates |
IsInvited(addr), Invited() |
who has an open invitation to curate |
Turning these into a node's config.toml is deliberately not this realm's job.
It is public (gnomod.toml says why) so that a separate realm can import it and
do that. Two things an importer must know: a slice these return is read-only in
the importing realm, elements included (copy it before sorting or changing it;
a struct returned on its own is already the caller's; a slice the importer
keeps in its own state stays this realm's object, so copy it before storing it
too), and a kind or status the
p/moul/zones Parse functions do not accept, or a blank slug, panics, which no recover in the
caller catches, so check one taken from a query string with them first.
| function | who |
|---|---|
ProposeZone(slug, chainID, title, description, kind, gnowebURL, rpcURL, genesisURL) |
anybody; a curator also when the review queue is full, and into the registry's last 16 places |
EditZone(slug, revision, chainID, ..., reason) |
a curator, or the proposer while pending. Before review the reason must be empty; on an approved zone it is required and replaces the review on record. Every edit bumps the zone's revision, and a new chain id sends its verified endpoints back to unverified. Leaving local drops every endpoint on a private host, at most 64 in one edit (more, remove some first), and is refused while one carries a curator's ruling (a curator removes it first; a verified one its registrant may withdraw); the proposer's edit only drops endpoints that are theirs and that they could withdraw on their own. A rejected or retired zone is not editable |
ApproveZone(slug, revision, reason) |
a curator |
RejectZone(slug, revision, reason), RetireZone(slug, revision, reason) |
a curator, reason required; the zone's verified endpoints go back to unverified. Rejecting a rejected zone, or retiring a retired one, with a new reason restates it; so does approving an approved one |
RemoveZone(slug, revision) |
a curator, on a pending or rejected zone; or its proposer, on a pending one, 100 blocks after it was proposed or last edited, while every endpoint on it is theirs and one they could withdraw on its own (below). One that was ever official is not removable; a retired one is kept until 128 newer retirements push it out |
RegisterEndpoint(slug, kind, addr, label) |
anybody on an approved zone; on a pending one, its proposer or a curator. The zone's own main RPC under rpc and gnoweb under gnoweb only a curator lists, or the proposer while the zone is pending (every proposer right ends at approval), since the page marks those URLs by that listing's verdict. An edit cannot make a stranger's listing the zone's own either: when it changes the main RPC or gnoweb to a URL a stranger (on an approved zone, the proposer too) lists under that kind, on a curator's edit the listing is dropped if nobody ruled on it (so pre-listing a zone's next URL cannot hold its move off) and the edit is refused if a curator did; a proposer's edit refuses rather than drop what is not theirs, and a flagged listing refuses the edit whoever holds it. A curator also registers past the review queue and the 16-per-address cap, and into the last 16 of the zone's 128 places, which a registration through the queue may not take |
VerifyEndpoint(id, zoneRevision, revision, reason) |
a curator, naming the endpoint's revision (the endpoint table's column) and the zone's (the zone page's), both as read |
FlagEndpoint(id, revision, reason), UnverifyEndpoint(id, revision, reason) |
a curator, naming the endpoint's revision as read. A flag needs a reason; any verdict given again with a new reason restates it |
ClearUnreviewed(slug, throughRevision) |
a curator: removes, in one call, every endpoint on the zone that nobody ruled on and that was registered through the review queue, not past it by a curator (never more than the 64 the queue holds), up to the revision named, on every page and every kind (the link on the zone's unfiltered page carries the revision it was rendered at, so nothing registered after is touched). For a flood that withdraws and registers again faster than one removal at a time |
RemoveEndpoint(id, revision) |
a curator; or its registrant, on a pending or approved zone, 100 blocks after registering it, unless a curator flagged or unverified it (a reset, which a chain-id edit, a rejection or a retirement makes, leaves the withdrawal a verified one had). Naming the revision means a removal fails if a verdict landed since. A verified endpoint its registrant may take down; a flagged or curator-unverified one is a warning, and everything on a rejected or retired zone is a record: only a curator removes those |
AddCurator(addr) |
a curator; it is an invitation, at most 16 curators and invitations together |
AcceptCurator() |
the invited address, to take up the invitation |
RemoveCurator(addr) |
a curator; it withdraws an invitation, or removes a curator along with every invitation they sent. The last curator cannot be removed |
Every decision on a zone (edit, approve, reject, retire, remove) takes its
revision, the one you read (the zone page shows it, and its action links carry
it): if the zone changed since, its content or its status, the call fails and
you read it again. A verdict on an endpoint, and its removal, take the
endpoint's own revision (the endpoint table's column), so they fail if another
curator ruled on it since; a verification also takes the zone's. The two are
named apart: one number combined from two reads could pair a stale value with a
fresh one. The $help links fill a revision in; an edit, a verdict, a
restated decision and an endpoint's removal have no link, so copy them from the
zone page. A zone's
revision covers its own fields and status, not its endpoints' verdicts. A
proposer edits or withdraws a pending zone only 100 blocks (ReviewWindow)
after it was proposed or last edited, by anybody, and a registrant withdraws an
endpoint only 100 blocks after registering it: a rate bound (100 blocks is
minutes), so neither can change an entry every block.
Curators are equals: any one may remove any other, the admin included. That is the trust a curator set is, and it is why there are few of them. A removed curator keeps what they registered, a listing of a zone's own URL included, until a curator removes it.
A storage deposit is refunded to whoever signs the transaction that frees it (on a chain with transfers locked, it goes to the storage fee collector). That is why a proposer cannot remove a zone carrying somebody else's endpoints, and why a curator's removal collects what the remover did not pay.
Every list is 25 rows a page (?page=), and a page reads the records it shows
plus a bounded handful of lookups (each row's main RPC verdict, the flags on a
zone's own URLs, the zone serving the current chain for the printed command,
the curators), never a whole list: vm/qrender is
gas-metered, and a Render that outgrows it stops answering.
Paths are exact, up to slashes at either end; anything else is Not found.
| path | shows |
|---|---|
| (root) | the official zones; ?status=retired for the retired ones |
zone/<slug> |
one zone: its facts and revision, its last review, its endpoints; ?kind=peer narrows them |
proposals |
pending proposals; ?status=rejected for rejected ones, with the reason |
An approved zone's gnoweb and genesis URLs are links; any other zone's show as
code, to copy and check. A main RPC or gnoweb URL the zone also lists, under
that same kind, as a flagged endpoint is code and marked wherever it is shown,
and the printed gnokey line leaves out a main RPC so marked. Only that kind's
flag counts: a listing under another kind is anybody's to make, so its flag
never marks the zone's own URL, and under its own kind only a curator lists it
(or the proposer, while pending). RPCs and endpoint addresses are always code. An action link
is shown only when the call could pass the caps, and where a cap is full a note
says which cap is full; a full review queue still takes a curator's call,
so its link stays, labelled for curators, and a zone with endpoints awaiting
review offers curators, on its unfiltered page and only while something is
clearable, a Clear link that says how many it clears and that it reaches all
pages and kinds, bound to the revision the page was rendered at. Where a
Rejection or a Retirement would evict the oldest record of its state, the page
says so beside it. Addresses are shown in full everywhere,
never shortened: an 8+4 shortening is within reach of a vanity grinder who
wants to look like a curator. Free text has @ and bare g1 addresses
neutralised, so a label cannot turn into a profile link.
Part of moul/gno-contracts — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage.
Dependency graph:

⚠️ Disclaimer: provided as-is, without warranty; not security-audited. Full disclaimer: DISCLAIMER.
Package zones is a curated registry of gno.land networks: mainnet, the testnets, staging chains, anybody's gnodev. Each zone carries what a node or a wallet needs to join it (chain id, RPC, gnoweb, genesis) plus a growing list of endpoints: RPCs, seeds and peers, indexers, faucets, explorers.
Curation is the point. Anybody may propose a zone and register endpoints on an approved one (on a pending one, its proposer or a curator; a zone's own main RPC and gnoweb, only them); a curator approves or rejects a proposal, retires a zone that stopped running, and verifies or flags an endpoint. The latest decision on each is recorded with who made it and when, and a rejection, a retirement, a flag or an edit to an approved zone must also say why, on the zone's page. Nothing is hidden while it waits: proposals and unverified endpoints are listed, and labelled.
This realm only manages the information and answers questions about it. The read helpers (GetZone, ListZones, GetEndpoint, ListEndpoints, ListAddresses, IsCurator, Curators, IsInvited, Invited) take plain values so they work from `gnokey query vm/qeval` as well as from another realm, and turning them into a node's config.toml is a separate realm's job.
The model, its validation and its state machine are p/moul/zones. This realm owns only who may write.
MaxCurators bounds curators and open invitations together. The curator list renders on the index page, and only curators can grow it, but a bound costs nothing and an unbounded list on a public page is a page someone can break.
PageSize is how many rows any table here shows at once. Every list is paginated and every page reads only its own records, because vm/qrender is gas-metered (3B per query) and a Render that outgrows it stops answering instead of slowing down. At the caps a page reads its 25 records, one lookup per URL it may mark flagged (each row's main RPC on the index; the zone's RPC and gnoweb on its page), the zone serving this chain for the printed command, the curators, and a few index nodes. Escaping free text is what dominates: about 97k gas a character on a node, so a page of full-length fields costs on the order of a billion, still under the ceiling.
ReviewWindow is how many blocks must pass before the author of a change may change it again, a rate bound rather than a review deadline (100 blocks is minutes): after a zone was proposed or last edited (by anybody) before its proposer may edit or withdraw it, and after an endpoint was registered before its registrant may withdraw it. Every edit bumps the revision a curator's decision must name, and a withdrawal plus a fresh proposal or registration does the same with a new revision or id, so without a wait a proposer or a registrant acting every block would keep their entry out of every curator's reach. Curators are not limited.
AcceptCurator makes the caller a curator, if a curator invited it.
AddCurator invites another address to curate. Only a curator may. The address becomes a curator when it calls AcceptCurator itself.
Two steps, deliberately: a curator set is the one thing a mistake here can lose for good. With a one-step add, a sole curator who invites a mistyped address and then steps down leaves a registry nobody controls; accepting is the proof that somebody holds the key. An invitation dies with its inviter: removing a curator withdraws every invitation they sent.
ApproveZone makes a zone official. revision is the zone's Revision as you read it (the zone page's Approve link carries it): if the zone changed since, its content or its status, the approval fails and you read it again. The reason is optional.
ClearUnreviewed removes, in one call, every endpoint on a zone that is zones.Endpoint.Clearable (nobody ruled on it, and no reviewer registered it past the caps), and returns how many. There are never more than zones.MaxUnverifiedPerZone: clearable endpoints are a part of the review queue its gate holds there, and no reset makes one. It is the answer to a flood that cycles: registrants who withdraw and register again each review window keep the queue full, and one removal per transaction lets them refill it before a curator is done. throughRevision bounds it to what the curator read: an endpoint registered after it (a later Revision) is kept. The deposits go to the curator who signs. At most zones.MaxEndpointsPerZone are read.
1func EditZone(cur realm, slug string, revision int64, chainID, title, description, kind, gnowebURL, rpcURL, genesisURL, reason string)EditZone replaces a zone's Info, every field but its slug and status. A curator may edit any pending or approved zone; the proposer may edit their own while it is pending. revision is the zone's Revision the edit was written against: it fails if the zone changed in between, its content or its status. Every edit bumps the revision, so a decision prepared against the old one fails too. A proposer edits only ReviewWindow blocks after the zone was proposed or last edited, and an edit that changes nothing is refused. On a pending zone the reason must be empty; on an approved one it is required and replaces the review on record, so the page names who changed the values, and why. A new chain id sends the zone's verified endpoints back to unverified. Leaving the local kind drops every endpoint on a private host, and is refused while one carries a curator's ruling (a verified one its registrant may withdraw first); the proposer's edit only drops endpoints that are theirs and that they could withdraw on their own (RemoveEndpoint), as for RemoveZone. A new main RPC or gnoweb a stranger (on an approved zone, its proposer too) already lists under its kind would make that listing the zone's own: on a curator's edit it is dropped if nobody ruled on it, and refuses the edit if a curator did; a proposer's edit refuses rather than drop what is not theirs; and a flagged listing refuses the edit whoever holds it. The registry validates the edit before it drops or resets anything, and a refusal reverts the edit with it. A rejected or retired zone cannot be edited.
FlagEndpoint tells readers not to use an endpoint. The reason is required. revision is the endpoint's, as for VerifyEndpoint; a flag is not bound to the zone, so editing the zone cannot hold off a warning.
GetEndpoint returns the endpoint with that id, and whether there is one.
GetZone returns the zone under slug, and whether there is one.
IsCurator reports whether addr may curate.
IsInvited reports whether addr holds a curator invitation it has not accepted yet.
ListAddresses is ListEndpoints reduced to the addresses, which is what a config file wants: ListAddresses("onyx", "peer", "verified").
ListEndpoints returns a zone's endpoints of that kind and verification, oldest first. "" matches any kind or verdict, so ("onyx", "", "") is everything on onyx and ("onyx", "peer", "verified") is what a cautious node should dial. The zone is required: one zone is at most MaxEndpointsPerZone rows, every zone together is a response no node should be asked for.
ListZones returns the zones with that status and kind, in the order they were proposed. "" matches any; "approved" is the official list.
1func ProposeZone(cur realm, slug, chainID, title, description, kind, gnowebURL, rpcURL, genesisURL string)ProposeZone files a new zone for review. Anybody may; it is listed as a proposal until a curator approves or rejects it. The review queue's caps (zones.MaxPending, zones.MaxPendingPerProposer) do not stop a curator, who also has the registry's last zones.ReservedForReviewers places, so neither a flood nor a full registry locks out the people who clear it.
kind is mainnet, testnet, devnet or local. gnowebURL and genesisURL may be empty; rpcURL may not, and is <scheme>://<host>[:<port>] with no path, which is what gnokey -remote takes.
RegisterEndpoint lists an endpoint on a zone and returns its id. On an approved zone anybody may, except the zone's own main RPC under rpc and gnoweb under gnoweb, which only a curator lists (or the proposer while the zone is pending: every proposer right ends at approval); on a pending one only the proposer or a curator, so a stranger cannot pin a proposal the proposer then cannot withdraw. It shows as unverified until a curator checks it. A curator also registers past the review queue's caps and into the zones.ReservedForReviewers places a gated registration may not take, never past zones.MaxEndpointsPerZone.
kind is rpc, gnoweb, seed, peer, indexer, faucet or explorer. addr is a URL, or <node id>@<host>:<port> for a seed or a peer. label is optional: who runs it, in your words.
RejectZone turns a proposal down. The reason is required, and public. revision is the zone's Revision you read, as for ApproveZone.
RemoveCurator revokes a curator, along with every invitation they sent, or withdraws one invitation. Only a curator may, and the last curator cannot be removed: a registry nobody can curate can never retire a dead zone.
Curators are equals: any one may remove any other, the admin included. That is the trust a curator set is, and it is why there are few of them.
RemoveEndpoint deletes an endpoint. revision is its revision as read: the removal fails if a verdict landed since, rather than delete one nobody saw. A curator may remove any. Its registrant may remove it unless a curator flagged or unverified it (a flag is a warning, an unverify carries why, and removing and registering it again would wipe either; a verified one the registrant may take down, since listing it again starts it unverified, and so one a reset sent back to unverified, since the reset says the zone changed, not it), only while its zone is pending or approved (a rejected or retired zone is a record, endpoints and all), and only ReviewWindow blocks after registering it (removing and registering again every block would give it a new id faster than a curator could flag the old one).
RemoveZone deletes a pending or rejected zone and its endpoints. revision is the zone's Revision you read: a removal meant for one proposal fails on another proposed again under the same slug. A curator may remove any pending or rejected zone. The proposer may withdraw their own only while it is pending, ReviewWindow blocks after it was proposed or last edited (so withdrawing and proposing again cannot dodge the edit wait), while every endpoint on it is theirs (the deposit is refunded to whoever signs the removal, so removing somebody else's endpoints would collect what they paid) and each one is one they could withdraw on its own (RemoveEndpoint): removing the zone must not wipe a curator's flag or unverify, or skip an endpoint's own wait. A rejected zone is the curators' record: only a curator removes it, or newer rejections push it out, and the rejection's deposit, paid by the curator, is not the proposer's to collect. A zone that was ever official is never removed this way: an approved one is retired, and a retired one is kept until newer retirements push it out.
Render is the whole public surface, three pages: the zone list (official, or retired with ?status=retired), one zone (?kind= narrows its endpoints), and the proposals (pending, or rejected with ?status=rejected). Every list takes ?page=.
RetireZone marks an official zone as no longer running, and sends its verified endpoints back to unverified. The reason is required: it is what an operator still holding the chain id will read. revision is the zone's Revision you read.
UnverifyEndpoint puts an endpoint back to unverified, for one that changed hands or needs checking again. revision is as for FlagEndpoint.
VerifyEndpoint marks an endpoint as checked against its zone. revision is the endpoint's revision as read (the endpoint table's revision column), and zoneRevision the zone's (the zone page shows it): the verdict fails if either changed since, another curator's verdict on the endpoint, or any edit or status change of the zone (what is verified is that it answers for this zone's chain id). The reason is optional. Each verdict may be given again with a new reason, to restate it.