Transaction

3621595A77A636…36208E7814E5

Block 592,217 · index 0 · indexed

Summary

Hash
3621595A77A6364002C5E59D2B6F35D473B8E4CBAD384CA1216536208E7814E5
Block
592,217
Size
167761 bytes
Gas used
204,442,321 / 447,193,200
Fee
1341579ugnot
Status
success

Messages

#1AddPackagegno.land/r/moul/zones/v017 arguments
Attached funds
67000000ugnot

Arguments · 17

  1. #1zones
  2. #2README.md
  3. #3# zones 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`](../../../p/moul/zones). This realm owns only who may write, and the pages. ## It ships with four zones 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 | ## Read it from a node 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. ```sh gnokey query vm/qeval -remote https://rpc.gno.land \ -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. ## Write to it | 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. ## Pages 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. <!-- BEGIN GNOCONTRACTS FOOTER (generated by `make readmes`; do not edit below) --> --- Part of **[moul/gno-contracts](https://github.com/moul/gno-contracts)** — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage. **On mainnet:** [![deployment status](https://gnoscope.com/_badges/shield/status/r/moul/zones/v0?network=mainnet)](https://gnoscope.com/realm/r/moul/zones/v0) [![transactions](https://gnoscope.com/_badges/shield/txs/r/moul/zones/v0?network=mainnet)](https://gnoscope.com/realm/r/moul/zones/v0) [![unique callers](https://gnoscope.com/_badges/shield/users/r/moul/zones/v0?network=mainnet)](https://gnoscope.com/realm/r/moul/zones/v0) [![deployed revision](https://gnoscope.com/_badges/shield/version/r/moul/zones/v0?network=mainnet)](https://gnoscope.com/realm/r/moul/zones/v0) **Dependency graph:** ![gno.land/r/moul/zones/v0 dependency graph](https://raw.githubusercontent.com/moul/gno-contracts/main/_assets/gno.land/r/moul/zones/v0/deps.png) > ⚠️ **Disclaimer:** provided as-is, without warranty; not security-audited. Full disclaimer: [DISCLAIMER](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md). <!-- END GNOCONTRACTS FOOTER -->
  4. #4gnomod.toml
  5. #5module = "gno.land/r/moul/zones/v0" gno = "0.9" # public: the registry is meant to be read by other realms (a config generator for node operators first), and a private realm cannot be imported
  6. #6render.gno
  7. #7package zones import ( "chain/runtime" "strconv" "strings" "gno.land/p/moul/kit/ui/v0" "gno.land/p/moul/md/v0" "gno.land/p/moul/realmpath/v0" "gno.land/p/moul/zones/v0" ) // realmURL is this realm as gnoweb serves it. Absolute, because the path ends in // /v0 and a relative link would resolve against the parent directory. Not // derived at runtime: a Render has no cur, and CurrentRealm there is the caller. // TestRealmURLMatchesTheModule pins it to the module line. const realmURL = "/r/moul/zones/v0" // pkgPath is this realm's package path, for the action links. const pkgPath = "gno.land" + realmURL // action is a call link to a function of THIS realm, named explicitly. // // Not ui.Action: that resolves its target through unsafe.CurrentRealm, which in // a borrowed Render is the caller, so a realm embedding this page would get // links that call its own functions. filetests/z_borrowed_render_filetest.gno // pins it from a caller realm, the only place the difference shows. func action(title, fn string, args ...string) string { return ui.ActionIn(pkgPath, title, fn, args...) } // 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. const PageSize = 25 // 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=. func Render(path string) string { req := realmpath.Parse(path) // Exact paths only: a page that also answers zone/x/approved-by-gno-core // lends its content to whatever the extra segment claims. parts := len(req.PathParts()) switch req.PathPart(0) { case "": if parts <= 1 { return renderIndex(req) } case "zone": if parts == 2 { return renderZone(req.PathPart(1), req) } case "proposals": if parts == 1 { return renderProposals(req) } } return notFound("No such page.") } func renderIndex(req *realmpath.Request) string { status, heading := zones.Approved, "Official" switch req.Query.Get("status") { case "", "approved": case "retired": status, heading = zones.Retired, "Retired" default: return notFound("No such list.") } query := "" if status == zones.Retired { query = "status=retired&" } table, pages := zoneTable(status, req, "") links := []string{} if status == zones.Retired { links = append(links, md.Link("official zones", realmURL)) } else if n := reg.ZoneCount(zones.Retired); n > 0 { links = append(links, md.Link(strconv.Itoa(n)+" retired zone(s)", realmURL+":?status=retired")) } links = append(links, md.Link(strconv.Itoa(reg.ZoneCount(zones.Pending))+" proposal(s) waiting for review", realmURL+":proposals")) // Offered only while it can succeed: a link certain to fail is a dead end, // and the note says which cap is full (not what would free it: that // depends on who acts, and what frees one cap can fill another). A // full review queue still takes a curator's proposal, so the link stays, // labelled for them. propose := func(title string) string { return action(title, "ProposeZone", "slug", "", "chainID", "", "title", "", "description", "", "kind", "testnet", "gnowebURL", "", "rpcURL", "", "genesisURL", "") } switch { case reg.Live() >= zones.MaxZones: links = append(links, "the registry is full at "+strconv.Itoa(zones.MaxZones)+" live zones") case reg.Live() >= zones.MaxZones-zones.ReservedForReviewers: links = append(links, "the registry's last "+strconv.Itoa(zones.ReservedForReviewers)+" places are kept for curators", propose("Propose a zone (curators only)")) case reg.ZoneCount(zones.Pending) >= zones.MaxPending: links = append(links, "the review queue is full at "+strconv.Itoa(zones.MaxPending)+" proposals", propose("Propose a zone (curators only, while full)")) default: links = append(links, propose("Propose a zone")) } return ui.Join("\n", md.H1("Zones"), para("A curated registry of gno.land networks and the endpoints that reach them. "+ "Anybody can propose a zone, and register endpoints on an official one; a curator approves, "+ "rejects or retires a zone and verifies or flags its endpoints, and every rejection, "+ "retirement and flag says why, in public."), md.H2(heading), table, pager("", query, pageNum(req, pages), pages), para(strings.Join(links, " · ")), md.H2("Read it from a node"), para("Every list is a plain function, so a node operator or a script can ask for it directly. "+ "An empty kind or status matches anything."), md.CodeBlock(queryCommand(`ListAddresses("`+exampleSlug()+`", "peer", "verified")`)), md.BulletList([]string{ md.InlineCode(`ListZones(status, kind)`) + ": `approved` is the official list", md.InlineCode(`GetZone(slug)`), md.InlineCode(`ListEndpoints(slug, kind, status)`) + ": kind is rpc, gnoweb, seed, peer, indexer, faucet or explorer", md.InlineCode(`ListAddresses(slug, kind, status)`) + ": the same, reduced to what a config file wants", }), para("Curated by "+curatorList()+"."), ) } // zoneTable renders one page of the zones with that status, and returns how // many pages there are. func zoneTable(status zones.Status, req *realmpath.Request, empty string) (string, int) { pages := pageCount(reg.ZoneCount(status)) var t *ui.Table switch status { case zones.Approved: t = ui.NewTable("zone", "chain id", "kind", "rpc", "endpoints") case zones.Pending: t = ui.NewTable("zone", "chain id", "kind", "proposed by", "review") case zones.Rejected: t = ui.NewTable("zone", "chain id", "proposed by", "why", "") default: t = ui.NewTable("zone", "chain id", "proposed by", "why") } for _, z := range reg.ZonePage(status, pageNum(req, pages), PageSize) { switch status { case zones.Approved: verified, total := reg.Count(z.Slug) t.Row(zoneLink(z), md.InlineCode(z.ChainID), string(z.Kind), md.InlineCode(z.RPCURL)+rpcFlag(z), strconv.Itoa(verified)+" verified of "+strconv.Itoa(total)) case zones.Pending: // Review on the zone page, which shows what is being approved; the // Approve link there carries the revision it was rendered from. t.Row(zoneLink(z), md.InlineCode(z.ChainID), string(z.Kind), ui.AddrFull(z.Proposer), md.Link("review", realmURL+":zone/"+z.Slug)) case zones.Rejected: t.Row(zoneLink(z), md.InlineCode(z.ChainID), ui.AddrFull(z.Proposer), cell(z.Reason), action("Remove", "RemoveZone", "slug", z.Slug, "revision", revStr(z))) default: t.Row(zoneLink(z), md.InlineCode(z.ChainID), ui.AddrFull(z.Proposer), cell(z.Reason)) } } if empty == "" { empty = "No " + string(status) + " zone yet." } return t.OrEmpty(empty), pages } func renderZone(slug string, req *realmpath.Request) string { z, ok := reg.Zone(slug) if !ok { return notFound("No such zone.") } kind := zones.EndpointKind(req.Query.Get("kind")) if kind != "" && !isKind(kind) { return notFound("No such endpoint kind.") } facts := []string{ md.Bold("chain id") + ": " + md.InlineCode(z.ChainID), md.Bold("kind") + ": " + string(z.Kind), md.Bold("rpc") + ": " + md.InlineCode(z.RPCURL) + rpcFlag(z), } if z.GnowebURL != "" { facts = append(facts, md.Bold("gnoweb")+": "+urlFact(z, zones.Gnoweb, z.GnowebURL)) } if z.GenesisURL != "" { facts = append(facts, md.Bold("genesis")+": "+urlFact(z, "", z.GenesisURL)) } // Addresses in full, here and on every page: an 8+4 shortening is about 50 // bits, within reach of a vanity grinder who wants to look like a curator. facts = append(facts, md.Bold("proposed")+" by "+ui.AddrFull(z.Proposer)+" at block "+strconv.FormatInt(z.ProposedAt, 10)) if z.EditedBy != "" { facts = append(facts, md.Bold("edited")+" by "+ui.AddrFull(z.EditedBy)+" at block "+strconv.FormatInt(z.EditedAt, 10)) } facts = append(facts, md.Bold("revision")+": "+strconv.FormatInt(z.Revision, 10)) if z.Reviewed() { facts = append(facts, md.Bold("last reviewed")+" "+reviewText(z.ReviewedBy, z.ReviewedAt, z.Reason)) } parts := []string{ md.H1(prose(z.Title)), para(statusBadge(z.Status) + " · " + md.InlineCode(z.Slug)), } if z.Description != "" { parts = append(parts, para(prose(z.Description))) } parts = append(parts, md.BulletList(facts), md.H2("Endpoints")) // One link per kind the zone has, from the index counts: no endpoint is // read to draw them. base := realmURL + ":zone/" + z.Slug kinds := []string{} if all := reg.EndpointCount(z.Slug, ""); all > 0 { kinds = append(kinds, kindLink("all", base, all, kind == "")) for _, k := range zones.EndpointKinds() { if n := reg.EndpointCount(z.Slug, k); n > 0 { kinds = append(kinds, kindLink(string(k), base+"?kind="+string(k), n, kind == k)) } } parts = append(parts, para(strings.Join(kinds, " · "))) } pages := pageCount(reg.EndpointCount(z.Slug, kind)) // The revision column is what a verdict or a removal names; a verification // also names the zone's revision, shown above. t := ui.NewTable("#", "revision", "kind", "address", "label", "status", "registered by") for _, e := range reg.EndpointPage(z.Slug, kind, pageNum(req, pages), PageSize) { t.Row( strconv.FormatInt(e.ID, 10), strconv.FormatInt(e.Revision, 10), string(e.Kind), md.InlineCode(e.Address), cell(e.Label), endpointStatus(e), ui.AddrFull(e.Registrant), ) } query := "" if kind != "" { query = "kind=" + string(kind) + "&" } empty := "No endpoint registered yet." if kind != "" { empty = "No " + string(kind) + " endpoint registered yet." } parts = append(parts, t.OrEmpty(empty), pager("zone/"+z.Slug, query, pageNum(req, pages), pages)) actions := []string{} open := z.Status == zones.Approved || z.Status == zones.Pending switch { case open && reg.EndpointCount(z.Slug, "") >= zones.MaxEndpointsPerZone: actions = append(actions, "endpoints are full at "+strconv.Itoa(zones.MaxEndpointsPerZone)) case open && reg.EndpointCount(z.Slug, "") >= zones.MaxEndpointsPerZone-zones.ReservedForReviewers: actions = append(actions, "the last "+strconv.Itoa(zones.ReservedForReviewers)+" endpoint places are kept for curators", action("Register an endpoint (curators only)", "RegisterEndpoint", "slug", z.Slug, "kind", "rpc", "addr", "", "label", "")) case open && reg.Awaiting(z.Slug) >= zones.MaxUnverifiedPerZone: actions = append(actions, "the review queue is full at "+strconv.Itoa(zones.MaxUnverifiedPerZone)+" endpoints awaiting review", action("Register an endpoint (curators only, while full)", "RegisterEndpoint", "slug", z.Slug, "kind", "rpc", "addr", "", "label", "")) case z.Status == zones.Approved: actions = append(actions, action("Register an endpoint", "RegisterEndpoint", "slug", z.Slug, "kind", "rpc", "addr", "", "label", "")) case z.Status == zones.Pending && !curators.Has(z.Proposer) && reg.OwnerCount(z.Slug, z.Proposer) >= zones.MaxEndpointsPerAddress: // On a pending zone the proposer is the one gated registrant. actions = append(actions, "the proposer has registered "+strconv.Itoa(zones.MaxEndpointsPerAddress)+" endpoints here, the limit per address", action("Register an endpoint (curators only)", "RegisterEndpoint", "slug", z.Slug, "kind", "rpc", "addr", "", "label", "")) case z.Status == zones.Pending: actions = append(actions, action("Register an endpoint (proposer or curator)", "RegisterEndpoint", "slug", z.Slug, "kind", "rpc", "addr", "", "label", "")) } // A flood clears in one call, on any zone the call takes (a rejected or // retired zone's records are a curator's to remove too). The call reaches // every page and every kind, so it is offered only on the unfiltered view, // only while it would clear something (the clearable count, no endpoint // read), labelled with its real scope, and bounded in time: an endpoint // registered after this page was rendered has a later revision and is kept. if n := reg.Clearable(z.Slug); kind == "" && n > 0 { actions = append(actions, action("Clear "+strconv.Itoa(n)+" never-reviewed endpoint(s) registered through the review queue, all pages and kinds (curators only)", "ClearUnreviewed", "slug", z.Slug, "throughRevision", strconv.FormatInt(reg.Revision(), 10))) } // Every decision carries the revision this page was rendered from, so acting // on what you read here fails if the zone changed after you read it. rev := revStr(z) approve := action("Approve revision "+rev, "ApproveZone", "slug", z.Slug, "revision", rev, "reason", "") remove := action("Remove", "RemoveZone", "slug", z.Slug, "revision", rev) switch z.Status { case zones.Pending: actions = append(actions, approve, action("Reject", "RejectZone", "slug", z.Slug, "revision", rev, "reason", ""), remove) if reg.ZoneCount(zones.Rejected) >= zones.MaxRejected { actions = append(actions, "rejecting it drops the zone rejected longest ago, endpoints and all") } case zones.Approved: actions = append(actions, action("Retire", "RetireZone", "slug", z.Slug, "revision", rev, "reason", "")) if reg.ZoneCount(zones.Retired) >= zones.MaxRetired { actions = append(actions, "retiring it drops the zone retired longest ago, endpoints and all") } case zones.Rejected, zones.Retired: // Back into the live registry only while it has room. if reg.Live() < zones.MaxZones { actions = append(actions, approve) } else { actions = append(actions, "approving it waits for a free place: the registry is full at "+strconv.Itoa(zones.MaxZones)+" live zones") } if z.Status == zones.Rejected { actions = append(actions, remove) } } parts = append(parts, para(strings.Join(actions, " · ")), md.CodeBlock(queryCommand(`GetZone("`+z.Slug+`")`)), para(md.Link("All zones", realmURL)), ) return ui.Join("\n", parts...) } func renderProposals(req *realmpath.Request) string { status, heading, other := zones.Pending, "Pending", "" switch req.Query.Get("status") { case "", "pending": if n := reg.ZoneCount(zones.Rejected); n > 0 { other = md.Link(strconv.Itoa(n)+" rejected", realmURL+":proposals?status=rejected") } case "rejected": status, heading = zones.Rejected, "Rejected" other = md.Link("pending", realmURL+":proposals") default: return notFound("No such list.") } query := "" if status == zones.Rejected { query = "status=rejected&" } empty := "Nothing waiting for review." if status == zones.Rejected { empty = "Nothing rejected." } table, pages := zoneTable(status, req, empty) links := []string{} if other != "" { links = append(links, other) } links = append(links, md.Link("All zones", realmURL)) return ui.Join("\n", md.H1("Proposals"), para("Zones somebody proposed and no curator has approved. Only an approved zone is official."), md.H2(heading), table, pager("proposals", query, pageNum(req, pages), pages), para(strings.Join(links, " · ")), ) } // pageCount is how many pages n rows make, at least one. func pageCount(n int) int { pages := n / PageSize if n%PageSize != 0 { pages++ } if pages < 1 { pages = 1 } return pages } // pageNum reads ?page= and clamps it into 1..pages: it is a reader's input, // and ?page=-1 must not render a footer saying "page -1 of 2". func pageNum(req *realmpath.Request, pages int) int { page := 1 raw := req.Query.Get("page") if n, err := strconv.Atoi(raw); err == nil { page = n } else if digits := strings.TrimPrefix(raw, "+"); digits != "" && strings.Trim(digits, "0123456789") == "" { // All digits and still unparseable: a positive number past int. It is // past the last page too, so it lands there, as any big number does. page = pages } if page < 1 { page = 1 } if page > pages { page = pages } return page } // pager links the neighbouring pages of a list, keeping the list's other query // parameters (query ends in "&" when not empty). One page needs no pager. func pager(path, query string, page, pages int) string { if pages < 2 { return "" } link := func(n int) string { return realmURL + ":" + path + "?" + query + "page=" + strconv.Itoa(n) } out := "" if page > 1 { out += md.Link("previous", link(page-1)) + " · " } out += "page " + strconv.Itoa(page) + " of " + strconv.Itoa(pages) if page < pages { out += " · " + md.Link("next", link(page+1)) } return para(out) } func kindLink(label, url string, n int, current bool) string { text := label + " (" + strconv.Itoa(n) + ")" if current { return md.Bold(text) } return md.Link(text, url) } func notFound(msg string) string { return md.H1("Not found") + ui.Empty(msg) } // queryCommand is the gnokey line that evaluates call against this realm. // // The remote is looked up in the registry itself, by the chain id this realm is // running on, so the command a reader copies points at the chain they are // reading. A chain the registry does not list gets no -remote, rather than a // guessed one, and so does a chain id two approved zones share (every gnodev // is "dev"): picking one would be the same guess. // // A reader pastes this into a shell, so the remote must never carry shell // syntax. Two layers make sure: the main RPC is validated down to // <scheme>://<host>[:<port>], host [A-Za-z0-9.-] and port digits, which leaves // nothing a shell reads as syntax; and it is single-quoted anyway, a quote being // the one character no URL here can contain, so a later, wider validator does // not reopen it. The call is built from charset-checked slugs and literals. func queryCommand(call string) string { remote := "" if z, ok := reg.SoleApproved(runtime.ChainID()); ok && !rpcFlagged(z) { // Not one the same page flags: printing a remote the endpoint table says // not to use would contradict the page. remote = " -remote '" + z.RPCURL + "'" } return "gnokey query vm/qeval" + remote + " -data '" + pkgPath + "." + call + "'" } // exampleSlug is the zone the index's example asks for peers of: the one // serving the chain this page is read on, when there is exactly one and it // lists a peer, else onyx (one zone and an index count read). func exampleSlug() string { if z, ok := reg.SoleApproved(runtime.ChainID()); ok && reg.EndpointCount(z.Slug, zones.Peer) > 0 { return z.Slug } return "onyx" } // para is one paragraph as a Join part. Not md.Paragraph, whose trailing blank // line plus Join's separator makes two in a row, which an example cannot pin. func para(s string) string { return s + "\n" } // zoneLink is a zone's title linking to its page, for a TABLE cell. Built by // hand rather than with md.Link, whose text escape is for prose and leaves a // pipe literal, so a title like "Alice|official" would open a column in every // zone table. ui.Cell escapes the pipe too; the destination is a constant plus // a slug checked to [a-z0-9-] at write time. func zoneLink(z zones.Zone) string { return "[" + cell(z.Title) + "](" + realmURL + ":zone/" + z.Slug + ")" } // urlFact shows a zone's URL as its own text, never behind a label, so a // reader sees where it goes: a proposal's "genesis.json" link could otherwise // open anything, a pre-filled transaction form included, right under the // Approve button. Only an approved zone's URLs are links; a proposal's are // code, to copy and check, not to click. // // A URL the zone also lists, under the kind it is shown as, as a flagged // endpoint is code and marked, never a link: the page does not offer what its // own endpoint table says not to use. kind is what the URL is shown as, "" // for the genesis URL, which no endpoint kind lists (flaggedAs). func urlFact(z zones.Zone, kind zones.EndpointKind, url string) string { if kind != "" && flaggedAs(z, kind, url) { return md.InlineCode(url) + " ⚠️ flagged" } if z.Status == zones.Approved { return md.Link(url, url) } return md.InlineCode(url) } // prose and cell escape a caller's free text for a sentence and a table cell, // plus what ui.Inline and ui.Cell leave alone: gnoweb turns an @name, and a // bare g1 address after a space or at the start, into a user-profile link with // an icon, so "run by @gnocore" or "run by g1…" would vouch for an account the // registrant chose. func prose(s string) string { return escapeMentions(ui.Inline(s)) } func cell(s string) string { return escapeMentions(ui.Cell(s)) } // escapeMentions backslash-escapes every @, and writes the 1 of every g1 as a // character reference. It runs after ui.Inline or ui.Cell, which escape every // & the caller typed, so the only reference in the output is this one; goldmark // decodes it back to "1" after the mention parser has looked and not matched. // // One pass, and none at all when there is nothing to escape: it runs on every // free-text field of every page. func escapeMentions(s string) string { if !strings.Contains(s, "@") && !strings.Contains(s, "g1") { return s } // Copy the runs between matches in one piece each: a byte at a time costs a // native call per byte, every page, on text a stranger chose. var b strings.Builder n, last := len(s), 0 for i := 0; i < n; i++ { switch { case s[i] == '@': b.WriteString(s[last:i]) b.WriteString("\\@") last = i + 1 case s[i] == 'g' && i+1 < n && s[i+1] == '1': b.WriteString(s[last:i]) b.WriteString("g&#49;") i++ last = i + 1 } } b.WriteString(s[last:]) return b.String() } // rpcFlag marks a zone's main RPC when the endpoint table flags it, so the // table, the zone page and the printed command never disagree. func rpcFlag(z zones.Zone) string { if rpcFlagged(z) { return " ⚠️ flagged" } return "" } func rpcFlagged(z zones.Zone) bool { return flaggedAs(z, zones.RPC, z.RPCURL) } // flaggedAs reports whether a URL the page shows as a kind (the main RPC as // rpc, the gnoweb URL as gnoweb) is listed under that kind and flagged. Only // that kind's verdict counts. A listing under another kind is anybody's to // make on an approved zone, so letting its flag mark the zone's own URL would // let a stranger's mis-kinded entry, flagged as mis-kinded, mark an official // URL; a curator who means the zone's URL flags it under its own kind. The // lookup compares in Canonical form, which reads an rpc tcp:// as the http:// // gnokey dials, so either spelling finds the other. func flaggedAs(z zones.Zone, shown zones.EndpointKind, url string) bool { e, ok := reg.EndpointByAddress(z.Slug, shown, url) return ok && e.Status == zones.Flagged } func revStr(z zones.Zone) string { return strconv.FormatInt(z.Revision, 10) } func isKind(k zones.EndpointKind) bool { for _, x := range zones.EndpointKinds() { if k == x { return true } } return false } func statusBadge(s zones.Status) string { switch s { case zones.Approved: return "✅ " + md.Bold("official") case zones.Pending: return "⏳ " + md.Bold("pending review") case zones.Rejected: return "❌ " + md.Bold("rejected") case zones.Retired: return "⏹️ " + md.Bold("retired") } return string(s) } func endpointStatus(e zones.Endpoint) string { s := "" switch e.Status { case zones.Verified: s = "✅ verified" case zones.Flagged: s = "⚠️ flagged" default: s = "unverified" } if e.Reason != "" { s += ": " + cell(e.Reason) } if e.ReviewedBy != "" { s += " (" + ui.AddrFull(e.ReviewedBy) + ", block " + strconv.FormatInt(e.ReviewedAt, 10) + ")" } return s } func reviewText(by address, at int64, reason string) string { s := "by " + ui.AddrFull(by) + " at block " + strconv.FormatInt(at, 10) if reason != "" { s += ": " + prose(reason) } return s } func curatorList() string { out := []string{} for _, a := range Curators() { out = append(out, ui.AddrFull(a)) } return strings.Join(out, ", ") }
  8. #8render_example_test.gno
  9. #9package zones // ExampleRender pins the index: the four seeded zones, official. func ExampleRender() { reset() print(Render("")) // Output: // # Zones // // A curated registry of gno.land networks and the endpoints that reach them. Anybody can propose a zone, and register endpoints on an official one; a curator approves, rejects or retires a zone and verifies or flags its endpoints, and every rejection, retirement and flag says why, in public. // // ## Official // // | zone | chain id | kind | rpc | endpoints | // | --- | --- | --- | --- | --- | // | [gno\.land mainnet](/r/moul/zones/v0:zone/mainnet) | `gnoland-1` | mainnet | `https://rpc.gno.land` | 6 verified of 6 | // | [Onyx testnet](/r/moul/zones/v0:zone/onyx) | `onyx-1` | testnet | `https://rpc.onyx.testnets.gno.land` | 7 verified of 7 | // | [gno\.land staging](/r/moul/zones/v0:zone/staging) | `staging` | devnet | `https://rpc.staging.gno.land` ⚠️ flagged | 0 verified of 2 | // | [moul's gnodev staging](/r/moul/zones/v0:zone/moul-staging) | `moulstaging-1` | devnet | `https://rpc.gno-staging.moul.p2p.team` | 3 verified of 3 | // // [0 proposal\(s\) waiting for review](/r/moul/zones/v0:proposals) · [Propose a zone](/r/moul/zones/v0$help&func=ProposeZone&chainID=&description=&genesisURL=&gnowebURL=&kind=testnet&rpcURL=&slug=&title=) // // ## Read it from a node // // Every list is a plain function, so a node operator or a script can ask for it directly. An empty kind or status matches anything. // // ``` // gnokey query vm/qeval -data 'gno.land/r/moul/zones/v0.ListAddresses("onyx", "peer", "verified")' // ``` // // - `ListZones(status, kind)`: `approved` is the official list // - `GetZone(slug)` // - `ListEndpoints(slug, kind, status)`: kind is rpc, gnoweb, seed, peer, indexer, faucet or explorer // - `ListAddresses(slug, kind, status)`: the same, reduced to what a config file wants // // Curated by `g1manfred47kzduec920z88wfr64ylksmdcedlf5`. } // ExampleRender_zone pins one zone's page: its facts, a link per endpoint kind, // and its endpoints in id order. func ExampleRender_zone() { reset() print(Render("zone/onyx")) // Output: // # Onyx testnet // // ✅ **official** · `onyx` // // The gno\.land testnet, launched 2026\-09\-28: mainnet's code, upgraded whenever mainnet is\. // // - **chain id**: `onyx-1` // - **kind**: testnet // - **rpc**: `https://rpc.onyx.testnets.gno.land` // - **gnoweb**: [https://onyx\.testnets\.gno\.land](https://onyx.testnets.gno.land) // - **genesis**: [https://github\.com/gnolang/gno/releases/download/chain/onyx/genesis\.json](https://github.com/gnolang/gno/releases/download/chain/onyx/genesis.json) // - **proposed** by `g1manfred47kzduec920z88wfr64ylksmdcedlf5` at block 123 // - **revision**: 16 // - **last reviewed** by `g1manfred47kzduec920z88wfr64ylksmdcedlf5` at block 123: seeded at deploy; probed 2026\-10\-01 // // ## Endpoints // // **all (7)** · [rpc \(1\)](/r/moul/zones/v0:zone/onyx?kind=rpc) · [gnoweb \(1\)](/r/moul/zones/v0:zone/onyx?kind=gnoweb) · [peer \(2\)](/r/moul/zones/v0:zone/onyx?kind=peer) · [indexer \(1\)](/r/moul/zones/v0:zone/onyx?kind=indexer) · [faucet \(1\)](/r/moul/zones/v0:zone/onyx?kind=faucet) · [explorer \(1\)](/r/moul/zones/v0:zone/onyx?kind=explorer) // // | # | revision | kind | address | label | status | registered by | // | --- | --- | --- | --- | --- | --- | --- | // | 7 | 18 | rpc | `https://rpc.onyx.testnets.gno.land` | gno core | ✅ verified: seeded at deploy; probed 2026\-10\-01 (`g1manfred47kzduec920z88wfr64ylksmdcedlf5`, block 123) | `g1manfred47kzduec920z88wfr64ylksmdcedlf5` | // | 8 | 20 | gnoweb | `https://onyx.testnets.gno.land` | gno core | ✅ verified: seeded at deploy; probed 2026\-10\-01 (`g1manfred47kzduec920z88wfr64ylksmdcedlf5`, block 123) | `g1manfred47kzduec920z88wfr64ylksmdcedlf5` | // | 9 | 22 | peer | `g1x5mlj5ava0dw9vkf4j6admjlzswm6f06p44krn@seed-1.onyx.testnets.gno.land:26656` | seed\-1, gno core | ✅ verified: seeded at deploy; probed 2026\-10\-01 (`g1manfred47kzduec920z88wfr64ylksmdcedlf5`, block 123) | `g1manfred47kzduec920z88wfr64ylksmdcedlf5` | // | 10 | 24 | peer | `g1grq5zswt0dlwwe7clr4359w70k2ewgse0gcwck@seed-2.onyx.testnets.gno.land:26656` | seed\-2, gno core | ✅ verified: seeded at deploy; probed 2026\-10\-01 (`g1manfred47kzduec920z88wfr64ylksmdcedlf5`, block 123) | `g1manfred47kzduec920z88wfr64ylksmdcedlf5` | // | 11 | 26 | indexer | `https://indexer.onyx.testnets.gno.land/graphql/query` | gno core tx\-indexer | ✅ verified: seeded at deploy; probed 2026\-10\-01 (`g1manfred47kzduec920z88wfr64ylksmdcedlf5`, block 123) | `g1manfred47kzduec920z88wfr64ylksmdcedlf5` | // | 12 | 28 | faucet | `https://onyx.testnets.gno.land/faucet` | gno core | ✅ verified: seeded at deploy; probed 2026\-10\-01 (`g1manfred47kzduec920z88wfr64ylksmdcedlf5`, block 123) | `g1manfred47kzduec920z88wfr64ylksmdcedlf5` | // | 13 | 30 | explorer | `https://gnoscope.com/?network=onyx` | gnoscope | ✅ verified: seeded at deploy; probed 2026\-10\-01 (`g1manfred47kzduec920z88wfr64ylksmdcedlf5`, block 123) | `g1manfred47kzduec920z88wfr64ylksmdcedlf5` | // // [Register an endpoint](/r/moul/zones/v0$help&func=RegisterEndpoint&addr=&kind=rpc&label=&slug=onyx) · [Retire](/r/moul/zones/v0$help&func=RetireZone&reason=&revision=16&slug=onyx) // // ``` // gnokey query vm/qeval -data 'gno.land/r/moul/zones/v0.GetZone("onyx")' // ``` // // [All zones](/r/moul/zones/v0) } // ExampleRender_proposals pins the proposals page with one pending and one // rejected zone, written straight to the registry: an Example has no realm // token to cross with. func ExampleRender_proposals() { reset() must(reg.Propose(alice, 200, "alice-dev", info("alicedev-1", "Alice's devnet", "", "devnet", "", "https://rpc.alice.example.com", ""))) must(reg.Propose(bob, 201, "fake-mainnet", info("gnoland-1", "Mainnet (faster!)", "", "mainnet", "", "https://rpc.fake.example.com", ""))) must(reg.ReviewZone("fake-mainnet", "rejected", zrev("fake-mainnet"), Admin, 202, "not the mainnet RPC")) print(Render("proposals")) // Output: // # Proposals // // Zones somebody proposed and no curator has approved. Only an approved zone is official. // // ## Pending // // | zone | chain id | kind | proposed by | review | // | --- | --- | --- | --- | --- | // | [Alice's devnet](/r/moul/zones/v0:zone/alice-dev) | `alicedev-1` | devnet | `g1v9kxjcm9ta047h6lta047h6lta047h6lzd40gh` | [review](/r/moul/zones/v0:zone/alice-dev) | // // [1 rejected](/r/moul/zones/v0:proposals?status=rejected) · [All zones](/r/moul/zones/v0) } // ExampleRender_rejected pins the rejected list, which carries the reasons. func ExampleRender_rejected() { reset() must(reg.Propose(bob, 201, "fake-mainnet", info("gnoland-1", "Mainnet (faster!)", "", "mainnet", "", "https://rpc.fake.example.com", ""))) must(reg.ReviewZone("fake-mainnet", "rejected", zrev("fake-mainnet"), Admin, 202, "not the mainnet RPC")) print(Render("proposals?status=rejected")) // Output: // # Proposals // // Zones somebody proposed and no curator has approved. Only an approved zone is official. // // ## Rejected // // | zone | chain id | proposed by | why | | // | --- | --- | --- | --- | --- | // | [Mainnet \(faster\!\)](/r/moul/zones/v0:zone/fake-mainnet) | `gnoland-1` | `g1vfhkyh6lta047h6lta047h6lta047h6l03vdhu` | not the mainnet RPC | [Remove](/r/moul/zones/v0$help&func=RemoveZone&revision=46&slug=fake-mainnet) | // // [pending](/r/moul/zones/v0:proposals) · [All zones](/r/moul/zones/v0) }
  10. #10seed.gno
  11. #11package zones import ( "chain/runtime" "gno.land/p/moul/zones/v0" ) // seedReason is the review every seeded zone and endpoint carries, so a reader // can tell a deploy-time entry from one a curator reviewed later, and knows how // old the check is. const seedReason = "seeded at deploy; probed 2026-10-01" // seedZone is one zone the realm starts with, approved, plus the endpoints it // starts with. Every seeded endpoint carries the probe's verdict, which is per // zone: verified with seedReason when the zone's endpoints answered, flagged // with failed, the observed failure, when they did not. Flagged rather than left unverified, because "probed and down" // and "nobody looked" are different facts and only a review can say which. type seedZone struct { slug string info zones.Info failed string // empty when every endpoint answered the probe endpoints [][3]string // kind, address, label } // seeds are the four zones this registry opens with. Every address below was // probed on 2026-10-01: each RPC's /status answered with the chain id given // here, except staging, whose RPC answered "temporarily unavailable" and whose // gnoweb answered 503, which is why its endpoints are seeded flagged with that. The // peers are the persistent_peers each network's VALIDATOR.md publishes in the // gnolang/gno monorepo, under misc/deployments. // // A function, not a package-level var: a realm's package variables are its // database, so a var here would keep every seed stored a second time, forever, // beside the registry copy seed() makes of it. func seeds() []seedZone { return []seedZone{ { slug: "mainnet", info: zones.Info{ ChainID: "gnoland-1", Title: "gno.land mainnet", Description: "The gno.land mainnet, launched 2026-09-12.", Kind: zones.Mainnet, GnowebURL: "https://gno.land", RPCURL: "https://rpc.gno.land", GenesisURL: "https://github.com/gnolang/gno/releases/download/chain/mainnet/genesis.json", }, endpoints: [][3]string{ {"rpc", "https://rpc.gno.land", "gno core"}, {"gnoweb", "https://gno.land", "gno core"}, {"peer", "g15rcv5yqef3kvnmueqvkyw8y05sd40jz9p3n5su@seed-1.gno.land:26656", "seed-1, gno core"}, {"peer", "g1ck2yeyvvnpl92237gcea0z68jx07a4nnyvuaan@seed-2.gno.land:26656", "seed-2, gno core"}, {"indexer", "https://indexer.gno.land/graphql/query", "gno core tx-indexer"}, {"explorer", "https://gnoscope.com/?network=mainnet", "gnoscope"}, }, }, { slug: "onyx", info: zones.Info{ ChainID: "onyx-1", Title: "Onyx testnet", Description: "The gno.land testnet, launched 2026-09-28: mainnet's code, upgraded whenever mainnet is.", Kind: zones.Testnet, GnowebURL: "https://onyx.testnets.gno.land", RPCURL: "https://rpc.onyx.testnets.gno.land", GenesisURL: "https://github.com/gnolang/gno/releases/download/chain/onyx/genesis.json", }, endpoints: [][3]string{ {"rpc", "https://rpc.onyx.testnets.gno.land", "gno core"}, {"gnoweb", "https://onyx.testnets.gno.land", "gno core"}, {"peer", "g1x5mlj5ava0dw9vkf4j6admjlzswm6f06p44krn@seed-1.onyx.testnets.gno.land:26656", "seed-1, gno core"}, {"peer", "g1grq5zswt0dlwwe7clr4359w70k2ewgse0gcwck@seed-2.onyx.testnets.gno.land:26656", "seed-2, gno core"}, {"indexer", "https://indexer.onyx.testnets.gno.land/graphql/query", "gno core tx-indexer"}, {"faucet", "https://onyx.testnets.gno.land/faucet", "gno core"}, {"explorer", "https://gnoscope.com/?network=onyx", "gnoscope"}, }, }, { slug: "staging", info: zones.Info{ ChainID: "staging", Title: "gno.land staging", Description: "gno core's staging network.", Kind: zones.Devnet, GnowebURL: "https://staging.gno.land", RPCURL: "https://rpc.staging.gno.land", }, failed: "did not answer when probed 2026-10-01: the RPC said temporarily unavailable, gnoweb 503", endpoints: [][3]string{ {"rpc", "https://rpc.staging.gno.land", "gno core"}, {"gnoweb", "https://staging.gno.land", "gno core"}, }, }, { slug: "moul-staging", info: zones.Info{ ChainID: "moulstaging-1", Title: "moul's gnodev staging", Description: "One gnodev node in staging mode, run by moul: a single validator, no consensus, worthless GNOT. Good for trying a deploy.", Kind: zones.Devnet, GnowebURL: "https://gno-staging.moul.p2p.team", RPCURL: "https://rpc.gno-staging.moul.p2p.team", }, endpoints: [][3]string{ {"rpc", "https://rpc.gno-staging.moul.p2p.team", "moul"}, {"gnoweb", "https://gno-staging.moul.p2p.team", "moul"}, {"faucet", "https://gno-staging.moul.p2p.team/faucet", "moul"}, }, }, } } // seed files every seed through the same Registry methods a caller's // transaction goes through (the gated ones, so a seeded endpoint is not // Exempt; each is ruled on at once, so none is clearable), so a seed that // would fail validation fails the deploy instead of shipping a zone no later // edit could reproduce. func seed() { at := runtime.ChainHeight() for _, s := range seeds() { must(reg.Propose(Admin, at, s.slug, s.info)) must(reg.ReviewZone(s.slug, zones.Approved, mustZone(s.slug).Revision, Admin, at, seedReason)) for _, e := range s.endpoints { k, err := zones.ParseEndpointKind(e[0]) must(err) id, err := reg.Register(Admin, at, s.slug, k, e[1], e[2]) must(err) ep, _ := reg.Endpoint(id) zrev := mustZone(s.slug).Revision // the approval moved it if s.failed == "" { must(reg.ReviewEndpoint(id, zones.Verified, zrev, ep.Revision, Admin, at, seedReason)) } else { must(reg.ReviewEndpoint(id, zones.Flagged, zrev, ep.Revision, Admin, at, s.failed)) } } } }
  12. #12z_borrowed_render_filetest.gno
  13. #13// PKGPATH: gno.land/r/test/main // // Renders the registry from ANOTHER realm, which is the case a unit test in // the package cannot reach: there, CurrentRealm is the zones realm itself. // Here Render is borrowed, CurrentRealm is gno.land/r/test/main, and every // action link must still call the zones realm, not the one embedding it. package main import ( "strings" zones "gno.land/r/moul/zones/v0" ) func main() { page := zones.Render("") + zones.Render("zone/onyx") println(strings.Contains(page, "/r/moul/zones/v0$help&func=ProposeZone")) println(strings.Contains(page, "/r/moul/zones/v0$help&func=RegisterEndpoint")) println(strings.Contains(page, "/r/test/main$help")) } // Output: // true // true // false
  14. #14zones.gno
  15. #15// untrusted-render: slugs, chain ids, URLs and peer addresses are charset-checked // at write time by p/moul/zones; titles, descriptions, labels and reasons are a // caller's free text and go through ui.Inline or ui.Cell at every call site. // 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. package zones import ( "chain/runtime" "strconv" "gno.land/p/moul/addrset/v1" "gno.land/p/moul/zones/v0" ) // Admin is the first curator. More are invited with AddCurator and become // curators when they accept. const Admin address = "g1manfred47kzduec920z88wfr64ylksmdcedlf5" // 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. const ReviewWindow = 100 // 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. const MaxCurators = 16 var ( reg = zones.NewRegistry() curators addrset.Set invited addrset.Set // invited by a curator, not yet accepted inviter = map[address]address{} // invitee -> the curator who invited them; lookups only ) func init() { curators.Add(Admin) seed() } // ---- proposing and curating zones // 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. func ProposeZone(cur realm, slug, chainID, title, description, kind, gnowebURL, rpcURL, genesisURL string) { who := caller(cur) in := info(chainID, title, description, kind, gnowebURL, rpcURL, genesisURL) if curators.Has(who) { // A flood that fills the review queue must not lock out the people // who clear it. must(reg.ProposeExempt(who, runtime.ChainHeight(), zones.TrimSpaces(slug), in)) return } must(reg.Propose(who, runtime.ChainHeight(), zones.TrimSpaces(slug), in)) } // 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. func EditZone(cur realm, slug string, revision int64, chainID, title, description, kind, gnowebURL, rpcURL, genesisURL, reason string) { who := caller(cur) slug = zones.TrimSpaces(slug) z := mustZone(slug) in := info(chainID, title, description, kind, gnowebURL, rpcURL, genesisURL) if !curators.Has(who) { if z.Status != zones.Pending || z.Proposer != who { panic("zones: only a curator, or the proposer while it is pending, may edit " + slug) } assertReviewWindow(slug, z) if z.Kind == zones.Local && in.Kind != zones.Local { // The edit would drop these; dropping is removing, under the // rules a removal by the proposer has. for _, e := range reg.PrivateEndpoints(slug) { if e.Registrant != who { panic("zones: leaving local would drop endpoint #" + strconv.FormatInt(e.ID, 10) + ", which somebody else registered; a curator must remove it first") } assertWithdrawable(e) } } } must(reg.Edit(slug, revision, in, who, runtime.ChainHeight(), reason)) // The reservation RegisterEndpoint keeps holds across an edit too: a new // main RPC or gnoweb already listed under its kind by a stranger would // make that listing, and its verdict, the zone's own. Checked only for a // URL the edit changed, after the edit, which has validated everything: a // refusal here reverts the edit with it, and an edit that cannot pass never // gets this far. On a curator's edit a stranger's listing nobody ruled on // is dropped, so listing a zone's likely next URL cannot hold its move // off; one a curator ruled on refuses the edit, and a curator removes it // first. A proposer's edit drops nothing that is not theirs. for _, own := range []struct { kind zones.EndpointKind url, was string }{{zones.RPC, in.RPCURL, z.RPCURL}, {zones.Gnoweb, in.GnowebURL, z.GnowebURL}} { if own.url == "" || (own.was != "" && zones.Canonical(own.kind, own.url) == zones.Canonical(own.kind, own.was)) { continue } e, ok := reg.EndpointByAddress(slug, own.kind, own.url) if !ok { continue } if curators.Has(e.Registrant) || (z.Status == zones.Pending && e.Registrant == z.Proposer) { // Theirs to hold, but a flagged one would mark the zone's own URL // the moment it becomes one. if e.Status == zones.Flagged { panic("zones: " + own.url + " is listed under " + string(own.kind) + " as endpoint #" + strconv.FormatInt(e.ID, 10) + ", flagged; a curator removes it or rules again before it becomes " + slug + "'s own") } continue } if !curators.Has(who) { // A proposer's edit (a pending zone) drops nothing that is not // theirs: the only other registrant there is a former curator. panic("zones: " + own.url + " is listed under " + string(own.kind) + " by " + e.Registrant.String() + " (endpoint #" + strconv.FormatInt(e.ID, 10) + "); a curator removes it before it becomes " + slug + "'s own") } if e.ReviewedBy != "" || e.Reason != "" { // a ruling, a reset's reason panic("zones: " + own.url + " is listed under " + string(own.kind) + " by " + e.Registrant.String() + " (endpoint #" + strconv.FormatInt(e.ID, 10) + "), with a curator's ruling; a curator removes it before it becomes " + slug + "'s own") } must(reg.RemoveEndpoint(e.ID, e.Revision)) } } // 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. func ApproveZone(cur realm, slug string, revision int64, reason string) { review(cur, slug, zones.Approved, revision, reason) } // RejectZone turns a proposal down. The reason is required, and public. // revision is the zone's Revision you read, as for ApproveZone. func RejectZone(cur realm, slug string, revision int64, reason string) { review(cur, slug, zones.Rejected, revision, reason) } // 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. func RetireZone(cur realm, slug string, revision int64, reason string) { review(cur, slug, zones.Retired, revision, reason) } // 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. func RemoveZone(cur realm, slug string, revision int64) { who := caller(cur) slug = zones.TrimSpaces(slug) z := mustZone(slug) if !curators.Has(who) { if z.Proposer != who || z.Status != zones.Pending { panic("zones: only a curator, or the proposer while it is pending, may remove " + slug) } assertReviewWindow(slug, z) // From the index counts first: nothing is read to answer it. if _, total := reg.Count(slug); reg.OwnerCount(slug, who) != total { panic("zones: " + slug + " carries endpoints somebody else registered; they must be removed first") } // Every endpoint is the proposer's: at most MaxEndpointsPerZone (a curator // who proposed it may have registered past the per-address cap). for _, e := range reg.Endpoints(zones.EndpointFilter{Zone: slug}) { assertWithdrawable(e) } } must(reg.RemoveZone(slug, revision)) } // ---- registering and curating endpoints // 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. func RegisterEndpoint(cur realm, slug, kind, addr, label string) int64 { who := caller(cur) slug = zones.TrimSpaces(slug) z := mustZone(slug) if z.Status == zones.Pending && z.Proposer != who && !curators.Has(who) { panic("zones: " + slug + " is pending; only its proposer or a curator may register on it") } k, err := zones.ParseEndpointKind(kind) must(err) addr = zones.TrimSpaces(addr) // compared as the registry will store it // The zone's own main RPC and gnoweb, listed under their own kind, carry // the verdict its page shows on them, so only a curator lists them, or the // proposer while the zone is pending (every proposer right ends at // approval): a stranger's listing, once flagged for its label, would mark // the zone's own URL. if !curators.Has(who) && !(z.Status == zones.Pending && z.Proposer == who) && ((k == zones.RPC && zones.Canonical(k, addr) == zones.Canonical(k, z.RPCURL)) || (k == zones.Gnoweb && z.GnowebURL != "" && zones.Canonical(k, addr) == zones.Canonical(k, z.GnowebURL))) { panic("zones: that is " + slug + "'s own " + string(k) + "; only a curator, or its proposer while it is pending, lists it") } register := reg.Register if curators.Has(who) { register = reg.RegisterExempt // past the review queue, as ProposeZone } id, err := register(who, runtime.ChainHeight(), slug, k, addr, label) must(err) return id } // 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. func VerifyEndpoint(cur realm, id, zoneRevision, revision int64, reason string) { reviewEndpoint(cur, id, zones.Verified, zoneRevision, revision, reason) } // 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. func FlagEndpoint(cur realm, id, revision int64, reason string) { reviewEndpoint(cur, id, zones.Flagged, 0, revision, reason) } // UnverifyEndpoint puts an endpoint back to unverified, for one that changed // hands or needs checking again. revision is as for FlagEndpoint. func UnverifyEndpoint(cur realm, id, revision int64, reason string) { reviewEndpoint(cur, id, zones.Unverified, 0, revision, reason) } // 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). func RemoveEndpoint(cur realm, id, revision int64) { who := caller(cur) e, ok := reg.Endpoint(id) if !ok { panic("zones: no endpoint #" + strconv.FormatInt(id, 10)) } if !curators.Has(who) { if e.Registrant != who { panic("zones: only a curator or its registrant may remove endpoint #" + strconv.FormatInt(id, 10)) } if z := mustZone(e.Zone); z.Status != zones.Pending && z.Status != zones.Approved { panic("zones: " + z.Slug + " is " + string(z.Status) + ", a record; only a curator may remove its endpoints") } assertWithdrawable(e) } must(reg.RemoveEndpoint(id, revision)) } // 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. func ClearUnreviewed(cur realm, slug string, throughRevision int64) int { who := caller(cur) assertCurator(who) slug = zones.TrimSpaces(slug) mustZone(slug) n := 0 for _, e := range reg.Endpoints(zones.EndpointFilter{Zone: slug, Status: zones.Unverified}) { if e.Clearable() && e.Revision <= throughRevision { must(reg.RemoveEndpoint(e.ID, e.Revision)) n++ } } return n } // assertWithdrawable refuses a non-curator's withdrawal of an endpoint a // curator flagged or unverified, or one registered less than ReviewWindow // blocks ago. Shared by RemoveEndpoint, the proposer's RemoveZone and the // proposer's edit off local, so none can do what another refuses. func assertWithdrawable(e zones.Endpoint) { id := strconv.FormatInt(e.ID, 10) // A verification guards nothing a withdrawal could erase: the registrant // takes down a node that is going away, and listing it again starts it // unverified. A flag, or an unverify a curator wrote (a reviewer or a // reason), is a warning, and removing and registering it again would wipe // it. A reset is not: it reaches only a verified endpoint and says the zone // changed, not the endpoint, so it leaves the withdrawal a verified one had. if e.Status == zones.Flagged || (e.Status == zones.Unverified && (e.ReviewedBy != "" || e.Reason != "") && !zones.IsReset(e)) { panic("zones: a curator ruled on endpoint #" + id + "; only a curator may remove it now") } if wait := e.RegisteredAt + ReviewWindow - runtime.ChainHeight(); wait > 0 { panic("zones: endpoint #" + id + " was registered at block " + strconv.FormatInt(e.RegisteredAt, 10) + "; it may be withdrawn in " + strconv.FormatInt(wait, 10) + " blocks, so curators can review it") } } // ---- curators // 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. func AddCurator(cur realm, addr address) { who := caller(cur) assertCurator(who) if !zones.ValidAddress(addr) { panic("zones: not a valid lowercase address: " + addr.String()) } if curators.Has(addr) { panic("zones: " + addr.String() + " is already a curator") } if invited.Has(addr) { panic("zones: " + addr.String() + " is already invited") } if curators.Size()+invited.Size() >= MaxCurators { panic("zones: " + strconv.Itoa(MaxCurators) + " curators and invitations already; remove one first") } invited.Add(addr) inviter[addr] = who } // AcceptCurator makes the caller a curator, if a curator invited it. func AcceptCurator(cur realm) { who := caller(cur) if !invited.Has(who) { panic("zones: " + who.String() + " has no curator invitation") } invited.Remove(who) delete(inviter, who) curators.Add(who) } // 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. func RemoveCurator(cur realm, addr address) { assertCurator(caller(cur)) if invited.Remove(addr) { delete(inviter, addr) return } if !curators.Has(addr) { panic("zones: " + addr.String() + " is not a curator") } if curators.Size() == 1 { panic("zones: " + addr.String() + " is the last curator") } curators.Remove(addr) for _, a := range Invited() { if inviter[a] == addr { invited.Remove(a) delete(inviter, a) } } } // ---- reads: plain arguments, so `gnokey query vm/qeval` can call them // GetZone returns the zone under slug, and whether there is one. func GetZone(slug string) (zones.Zone, bool) { return reg.Zone(zones.TrimSpaces(slug)) } // ListZones returns the zones with that status and kind, in the order they // were proposed. "" matches any; "approved" is the official list. func ListZones(status, kind string) []zones.Zone { st, err := zones.ParseStatus(status) must(err) k, err := zones.ParseKind(kind) must(err) return reg.Zones(zones.ZoneFilter{Status: st, Kind: k}) } // GetEndpoint returns the endpoint with that id, and whether there is one. func GetEndpoint(id int64) (zones.Endpoint, bool) { return reg.Endpoint(id) } // 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. func ListEndpoints(slug, kind, status string) []zones.Endpoint { return reg.Endpoints(endpointFilter(slug, kind, status)) } // ListAddresses is ListEndpoints reduced to the addresses, which is what a // config file wants: ListAddresses("onyx", "peer", "verified"). func ListAddresses(slug, kind, status string) []string { es := reg.Endpoints(endpointFilter(slug, kind, status)) out := make([]string, 0, len(es)) for _, e := range es { out = append(out, e.Address) } return out } // IsInvited reports whether addr holds a curator invitation it has not // accepted yet. func IsInvited(addr address) bool { return invited.Has(addr) } // IsCurator reports whether addr may curate. func IsCurator(addr address) bool { return curators.Has(addr) } // Curators returns every curator, in address order. func Curators() []address { return members(&curators) } // Invited returns every open curator invitation, in address order. func Invited() []address { return members(&invited) } func members(set *addrset.Set) []address { out := make([]address, 0, set.Size()) set.IterateByOffset(0, set.Size(), func(a address) bool { out = append(out, a) return false }) return out } // ---- helpers func review(cur realm, slug string, to zones.Status, revision int64, reason string) { who := caller(cur) assertCurator(who) must(reg.ReviewZone(zones.TrimSpaces(slug), to, revision, who, runtime.ChainHeight(), reason)) } func reviewEndpoint(cur realm, id int64, to zones.Verification, zoneRevision, revision int64, reason string) { who := caller(cur) assertCurator(who) must(reg.ReviewEndpoint(id, to, zoneRevision, revision, who, runtime.ChainHeight(), reason)) } func endpointFilter(slug, kind, status string) zones.EndpointFilter { slug = zones.TrimSpaces(slug) if slug == "" { panic("zones: name a zone; ListZones lists them") } k, err := zones.ParseEndpointKind(kind) must(err) v, err := zones.ParseVerification(status) must(err) return zones.EndpointFilter{Zone: slug, Kind: k, Status: v} } func info(chainID, title, description, kind, gnowebURL, rpcURL, genesisURL string) zones.Info { k, err := zones.ParseKind(kind) must(err) return zones.Info{ ChainID: zones.TrimSpaces(chainID), Title: zones.TrimSpaces(title), Description: zones.TrimSpaces(description), Kind: k, GnowebURL: zones.TrimSpaces(gnowebURL), RPCURL: zones.TrimSpaces(rpcURL), GenesisURL: zones.TrimSpaces(genesisURL), } } // caller is the one place this realm decides who is acting: the realm token is // checked before it is walked, because an unchecked token is not a caller. func caller(cur realm) address { if !cur.IsCurrent() { panic("zones: spoofed realm") } return cur.Previous().Address() } // assertReviewWindow refuses a proposer's edit or withdrawal of a pending zone // sooner than ReviewWindow blocks after it was proposed or last edited, // by anybody. func assertReviewWindow(slug string, z zones.Zone) { last := z.ProposedAt if z.EditedAt > last { last = z.EditedAt } if wait := last + ReviewWindow - runtime.ChainHeight(); wait > 0 { panic("zones: " + slug + " was changed at block " + strconv.FormatInt(last, 10) + "; its proposer may act on it again in " + strconv.FormatInt(wait, 10) + " blocks, so curators can review it") } } func assertCurator(who address) { if !curators.Has(who) { panic("zones: " + who.String() + " is not a curator") } } func mustZone(slug string) zones.Zone { z, ok := reg.Zone(slug) if !ok { panic("zones: no zone " + strconv.Quote(slug)) } return z } func must(err error) { if err != nil { panic(err.Error()) } }
  16. #16zones_test.gno
  17. #17package zones import ( "chain/runtime" "strconv" "strings" "testing" "gno.land/p/moul/addrset/v1" "gno.land/p/moul/zones/v0" "gno.land/p/nt/testutils/v0" "gno.land/p/nt/uassert/v0" "gno.land/p/nt/urequire/v0" ) var ( alice = testutils.TestAddress("alice") bob = testutils.TestAddress("bob") carol = testutils.TestAddress("carol") ) const nodeID = "g15rcv5yqef3kvnmueqvkyw8y05sd40jz9p3n5su" // reset puts the realm back to its init() state. // // Realm globals persist for the whole test binary and examples run after every // Test, so without this an ExampleRender's pinned output depends silently on // which tests ran before it. func reset() { reg = zones.NewRegistry() curators = addrset.Set{} invited = addrset.Set{} inviter = map[address]address{} curators.Add(Admin) seed() } func slugs(zs []zones.Zone) string { out := []string{} for _, z := range zs { out = append(out, z.Slug) } return strings.Join(out, " ") } func TestRealmURLMatchesTheModule(cur realm, t *testing.T) { uassert.Equal(t, "gno.land"+realmURL, cur.PkgPath()) } func TestSeeded(t *testing.T) { reset() uassert.Equal(t, "mainnet onyx staging moul-staging", slugs(ListZones("approved", ""))) uassert.Equal(t, "", slugs(ListZones("pending", ""))) uassert.Equal(t, "onyx", slugs(ListZones("", "testnet"))) uassert.Equal(t, "staging moul-staging", slugs(ListZones("approved", "devnet"))) z, ok := GetZone("onyx") urequire.True(t, ok) uassert.Equal(t, "onyx-1", z.ChainID) uassert.Equal(t, Admin.String(), z.ReviewedBy.String()) uassert.Equal(t, seedReason, z.Reason) peers := ListAddresses("onyx", "peer", "verified") uassert.Equal(t, 2, len(peers)) uassert.Equal(t, "g1x5mlj5ava0dw9vkf4j6admjlzswm6f06p44krn@seed-1.onyx.testnets.gno.land:26656", peers[0]) uassert.Equal(t, "https://rpc.gno.land", strings.Join(ListAddresses("mainnet", "rpc", ""), ",")) // staging did not answer when the seed was written, so nothing on it is // verified, but it is all listed. uassert.Equal(t, 0, len(ListEndpoints("staging", "", "verified"))) flagged := ListEndpoints("staging", "", "flagged") uassert.Equal(t, 2, len(flagged)) uassert.True(t, strings.Contains(flagged[0].Reason, "probed 2026-10-01")) uassert.Equal(t, Admin.String(), flagged[0].ReviewedBy.String()) // Every seed endpoint is filed under its own zone, and only there. n := 0 for _, s := range seeds() { uassert.Equal(t, len(s.endpoints), len(ListEndpoints(s.slug, "", "")), s.slug) for _, e := range ListEndpoints(s.slug, "", "") { uassert.Equal(t, s.slug, e.Zone) } n += len(s.endpoints) } uassert.Equal(t, n, len(reg.Endpoints(zones.EndpointFilter{}))) } func TestReadsRefuseUnknownFilters(cur realm, t *testing.T) { reset() // The reads cross nothing, so a refusal is a plain panic, not an abort. uassert.PanicsContains(t, cur, "unknown status", func() { ListZones("official", "") }) uassert.PanicsContains(t, cur, "unknown kind", func() { ListZones("", "betanet") }) uassert.PanicsContains(t, cur, "unknown endpoint kind", func() { ListEndpoints("onyx", "grpc", "") }) uassert.PanicsContains(t, cur, "unknown verification", func() { ListAddresses("onyx", "", "trusted") }) // Every zone at once is not a read a node should be asked for. uassert.PanicsContains(t, cur, "name a zone", func() { ListEndpoints("", "", "") }) uassert.PanicsContains(t, cur, "name a zone", func() { ListAddresses(" ", "rpc", "") }) _, ok := GetZone("nope") uassert.False(t, ok) _, ok = GetEndpoint(9999) uassert.False(t, ok) } func TestProposeThenCurate(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(alice)) ProposeZone(cross(cur), "alice-dev", "alicedev-1", "Alice's devnet", "for trying things", "devnet", "", "https://rpc.alice.example.com", "") z, ok := GetZone("alice-dev") urequire.True(t, ok) uassert.Equal(t, string(zones.Pending), string(z.Status)) uassert.Equal(t, alice.String(), z.Proposer.String()) uassert.Equal(t, "alice-dev", slugs(ListZones("pending", ""))) // The proposer fills in an endpoint and edits the zone (its gnoweb URL) // while pending. id := RegisterEndpoint(cross(cur), "alice-dev", "peer", nodeID+"@alice.example.com:26656", "alice") // A proposer waits between their own edits: every edit bumps the revision a // curator's decision must name, and curators need a window to make one. uassert.AbortsContains(t, cur, "may act on it again in", func() { EditZone(cross(cur), "alice-dev", zrev("alice-dev"), "alicedev-1", "Alice's devnet", "for trying things", "devnet", "https://alice.example.com", "https://rpc.alice.example.com", "", "") }) testing.SkipHeights(ReviewWindow) EditZone(cross(cur), "alice-dev", zrev("alice-dev"), "alicedev-1", "Alice's devnet", "for trying things", "devnet", "https://alice.example.com", "https://rpc.alice.example.com", "", "") uassert.AbortsContains(t, cur, "may act on it again in", func() { EditZone(cross(cur), "alice-dev", zrev("alice-dev"), "alicedev-1", "Alice's devnet", "for trying more", "devnet", "https://alice.example.com", "https://rpc.alice.example.com", "", "") }) z, _ = GetZone("alice-dev") uassert.Equal(t, "https://alice.example.com", z.GnowebURL) uassert.False(t, z.Reviewed()) // a pending edit is just an edit // Nobody but a curator approves, and bob cannot touch alice's proposal. uassert.AbortsContains(t, cur, "is not a curator", func() { ApproveZone(cross(cur), "alice-dev", zrev("alice-dev"), "") }) testing.SetRealm(testing.NewUserRealm(bob)) uassert.AbortsContains(t, cur, "only a curator, or the proposer", func() { EditZone(cross(cur), "alice-dev", zrev("alice-dev"), "x", "x", "", "devnet", "", "https://x.example.com", "", "") }) uassert.AbortsContains(t, cur, "only a curator, or the proposer while it is pending", func() { RemoveZone(cross(cur), "alice-dev", zrev("alice-dev")) }) uassert.AbortsContains(t, cur, "only a curator or its registrant", func() { RemoveEndpoint(cross(cur), id, erev(id)) }) uassert.AbortsContains(t, cur, "is not a curator", func() { VerifyEndpoint(cross(cur), id, zr(id), erev(id), "") }) testing.SetRealm(testing.NewUserRealm(Admin)) ApproveZone(cross(cur), "alice-dev", zrev("alice-dev"), "checked the node answers") VerifyEndpoint(cross(cur), id, zr(id), erev(id), "") z, _ = GetZone("alice-dev") uassert.Equal(t, string(zones.Approved), string(z.Status)) uassert.Equal(t, "checked the node answers", z.Reason) uassert.Equal(t, 1, len(ListAddresses("alice-dev", "peer", "verified"))) // Approved, it is the curator's to edit now, not the proposer's. testing.SetRealm(testing.NewUserRealm(alice)) uassert.AbortsContains(t, cur, "only a curator, or the proposer while it is pending", func() { EditZone(cross(cur), "alice-dev", zrev("alice-dev"), "alicedev-1", "renamed", "", "devnet", "", "https://rpc.alice.example.com", "", "x") }) // A curator's edit to a reviewed zone replaces the review on record, and // needs a reason, so the page never shows new values under the old review. testing.SetRealm(testing.NewUserRealm(Admin)) uassert.AbortsContains(t, cur, "an edit needs a reason", func() { EditZone(cross(cur), "alice-dev", zrev("alice-dev"), "alicedev-1", "Alice's devnet", "", "devnet", "", "https://rpc2.alice.example.com", "", "") }) EditZone(cross(cur), "alice-dev", zrev("alice-dev"), "alicedev-1", "Alice's devnet", "", "devnet", "", "https://rpc2.alice.example.com", "", "rpc moved") z, _ = GetZone("alice-dev") uassert.Equal(t, "https://rpc2.alice.example.com", z.RPCURL) uassert.Equal(t, "rpc moved", z.Reason) uassert.Equal(t, string(zones.Approved), string(z.Status)) testing.SetRealm(testing.NewUserRealm(alice)) // Nor hers to remove; and not even a curator removes an approved zone: it is // retired instead. uassert.AbortsContains(t, cur, "while it is pending, may remove", func() { RemoveZone(cross(cur), "alice-dev", zrev("alice-dev")) }) testing.SetRealm(testing.NewUserRealm(Admin)) uassert.AbortsContains(t, cur, "retire it instead", func() { RemoveZone(cross(cur), "alice-dev", zrev("alice-dev")) }) uassert.AbortsContains(t, cur, "needs a reason", func() { RetireZone(cross(cur), "alice-dev", zrev("alice-dev"), "") }) RetireZone(cross(cur), "alice-dev", zrev("alice-dev"), "alice turned it off") z, _ = GetZone("alice-dev") uassert.Equal(t, string(zones.Retired), string(z.Status)) _, err := reg.Register(bob, 1, "alice-dev", zones.RPC, "https://late.example.com", "") uassert.ErrorContains(t, err, "takes no endpoints") // Retired, it is a record the curators keep: the proposer cannot erase it. testing.SetRealm(testing.NewUserRealm(alice)) uassert.AbortsContains(t, cur, "while it is pending, may remove", func() { RemoveZone(cross(cur), "alice-dev", zrev("alice-dev")) }) // Nor can a curator: a retired zone is kept on record. testing.SetRealm(testing.NewUserRealm(Admin)) uassert.AbortsContains(t, cur, "kept on record", func() { RemoveZone(cross(cur), "alice-dev", zrev("alice-dev")) }) _, ok = GetZone("alice-dev") uassert.True(t, ok) } // The printed command is pasted into a shell. The main RPC cannot carry shell // syntax at all; the quote is defense in depth. func TestQueryCommandQuotesTheRemote(t *testing.T) { reset() // Two layers. The main RPC cannot carry shell syntax at all: its host is // [A-Za-z0-9.-], its port digits, and a path, query or fragment is refused. // And the printed remote is single-quoted anyway, in case a later version // widens the validator. z, _ := GetZone("moul-staging") in := zones.Info{ChainID: runtime.ChainID(), Title: z.Title, Kind: z.Kind, RPCURL: "https://rpc.example.com/;id;"} uassert.ErrorContains(t, reg.Edit("moul-staging", zrev("moul-staging"), in, Admin, 1, "test"), "no path") in.RPCURL = "https://rpc.example.com:443" must(reg.Edit("moul-staging", zrev("moul-staging"), in, Admin, 1, "test")) got := queryCommand(`GetZone("onyx")`) uassert.Equal(t, `gnokey query vm/qeval -remote 'https://rpc.example.com:443' -data 'gno.land/r/moul/zones/v0.GetZone("onyx")'`, got) // Two approved zones on the same chain id: no -remote, rather than a guess. must(reg.Edit("staging", zrev("staging"), zones.Info{ChainID: runtime.ChainID(), Title: "S", Kind: zones.Devnet, RPCURL: "https://other.example.com"}, Admin, 1, "test")) uassert.Equal(t, `gnokey query vm/qeval -data 'gno.land/r/moul/zones/v0.GetZone("onyx")'`, queryCommand(`GetZone("onyx")`)) uassert.ErrorContains(t, zones.ValidateEndpoint(zones.RPC, "https://rpc.example.com/'x"), "contains") } func TestRejectAndWithdraw(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(bob)) ProposeZone(cross(cur), "fake-mainnet", "gnoland-1", "Mainnet (faster!)", "", "mainnet", "", "https://rpc.fake.example.com", "") RegisterEndpoint(cross(cur), "fake-mainnet", "rpc", "https://bob.example.com", "") // A stranger cannot pin a pending proposal with an endpoint of their own. testing.SetRealm(testing.NewUserRealm(alice)) uassert.AbortsContains(t, cur, "only its proposer or a curator may register", func() { RegisterEndpoint(cross(cur), "fake-mainnet", "rpc", "https://alice.example.com", "") }) // A curator can. testing.SetRealm(testing.NewUserRealm(Admin)) theirs := RegisterEndpoint(cross(cur), "fake-mainnet", "rpc", "https://curator.example.com", "") uassert.AbortsContains(t, cur, "needs a reason", func() { RejectZone(cross(cur), "fake-mainnet", zrev("fake-mainnet"), " ") }) RejectZone(cross(cur), "fake-mainnet", zrev("fake-mainnet"), "not the mainnet RPC") z, _ := GetZone("fake-mainnet") uassert.Equal(t, string(zones.Rejected), string(z.Status)) uassert.Equal(t, "fake-mainnet", slugs(ListZones("rejected", ""))) uassert.Equal(t, "mainnet onyx staging moul-staging", slugs(ListZones("approved", ""))) // A rejected zone is the curators' record: its proposer cannot take it down // (and so cannot collect the deposit the curator paid to record the // rejection). A curator can. testing.SetRealm(testing.NewUserRealm(bob)) uassert.AbortsContains(t, cur, "while it is pending, may remove", func() { RemoveZone(cross(cur), "fake-mainnet", zrev("fake-mainnet")) }) testing.SetRealm(testing.NewUserRealm(Admin)) RemoveEndpoint(cross(cur), theirs, erev(theirs)) RemoveZone(cross(cur), "fake-mainnet", zrev("fake-mainnet")) _, ok := GetZone("fake-mainnet") uassert.False(t, ok) // A proposer withdrawing a PENDING zone waits out the review window, and cannot // while it carries somebody else's endpoint: the refund for it, which they // paid, would go to the proposer who signs. testing.SetRealm(testing.NewUserRealm(bob)) ProposeZone(cross(cur), "bob-again", "bob-1", "Bob again", "", "devnet", "", "https://rpc.bob.example.com", "") uassert.AbortsContains(t, cur, "may act on it again in", func() { RemoveZone(cross(cur), "bob-again", zrev("bob-again")) }) testing.SetRealm(testing.NewUserRealm(Admin)) RegisterEndpoint(cross(cur), "bob-again", "rpc", "https://curator2.example.com", "") testing.SkipHeights(ReviewWindow) testing.SetRealm(testing.NewUserRealm(bob)) uassert.AbortsContains(t, cur, "must be removed first", func() { RemoveZone(cross(cur), "bob-again", zrev("bob-again")) }) } // A registrant withdraws their endpoint only after the review window: removing and // registering again every block would give it a new id faster than a curator // could flag the old one. func TestRegistrantWithdrawalWaits(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(bob)) id := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://cycle.example.com", "") uassert.AbortsContains(t, cur, "may be withdrawn in", func() { RemoveEndpoint(cross(cur), id, erev(id)) }) testing.SkipHeights(ReviewWindow) RemoveEndpoint(cross(cur), id, erev(id)) _, ok := GetEndpoint(id) uassert.False(t, ok) } func TestEndpointLifecycle(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(bob)) id := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://onyx-rpc.bob.example.com", "bob's node") e, ok := GetEndpoint(id) urequire.True(t, ok) uassert.Equal(t, string(zones.Unverified), string(e.Status)) uassert.Equal(t, bob.String(), e.Registrant.String()) // Unverified is listed, and a verified-only read leaves it out. uassert.Equal(t, 2, len(ListAddresses("onyx", "rpc", ""))) uassert.Equal(t, 1, len(ListAddresses("onyx", "rpc", "verified"))) uassert.AbortsContains(t, cur, "already lists that rpc", func() { RegisterEndpoint(cross(cur), "onyx", "rpc", "https://ONYX-rpc.bob.example.com", "") }) uassert.AbortsContains(t, cur, "unknown endpoint kind", func() { RegisterEndpoint(cross(cur), "onyx", "grpc", "https://x.example.com", "") }) uassert.AbortsContains(t, cur, "no zone", func() { RegisterEndpoint(cross(cur), "nope", "rpc", "https://x.example.com", "") }) testing.SetRealm(testing.NewUserRealm(Admin)) uassert.AbortsContains(t, cur, "needs a reason", func() { FlagEndpoint(cross(cur), id, erev(id), "") }) FlagEndpoint(cross(cur), id, erev(id), "answers for the wrong chain id") e, _ = GetEndpoint(id) uassert.Equal(t, string(zones.Flagged), string(e.Status)) uassert.Equal(t, 1, len(ListEndpoints("onyx", "rpc", "flagged"))) UnverifyEndpoint(cross(cur), id, erev(id), "bob fixed it") VerifyEndpoint(cross(cur), id, zr(id), erev(id), "") uassert.Equal(t, 2, len(ListAddresses("onyx", "rpc", "verified"))) // Flagged, it is a warning: its registrant cannot take it down (and so // cannot wipe it by registering it again). testing.SetRealm(testing.NewUserRealm(Admin)) FlagEndpoint(cross(cur), id, erev(id), "serves the wrong chain") testing.SetRealm(testing.NewUserRealm(bob)) uassert.AbortsContains(t, cur, "a curator ruled on", func() { RemoveEndpoint(cross(cur), id, erev(id)) }) // Unverified by a curator, it carries why: still not the registrant's to // take down, even after the review window, or removing and registering it // again would wipe the warning. A curator may. testing.SetRealm(testing.NewUserRealm(Admin)) UnverifyEndpoint(cross(cur), id, erev(id), "key was sold; re-checking") testing.SetRealm(testing.NewUserRealm(bob)) testing.SkipHeights(ReviewWindow) uassert.AbortsContains(t, cur, "a curator ruled on", func() { RemoveEndpoint(cross(cur), id, erev(id)) }) testing.SetRealm(testing.NewUserRealm(Admin)) RemoveEndpoint(cross(cur), id, erev(id)) _, ok = GetEndpoint(id) uassert.False(t, ok) uassert.AbortsContains(t, cur, "no endpoint", func() { RemoveEndpoint(cross(cur), id, erev(id)) }) } func TestCurators(cur realm, t *testing.T) { reset() uassert.True(t, IsCurator(Admin)) uassert.False(t, IsCurator(alice)) testing.SetRealm(testing.NewUserRealm(alice)) uassert.AbortsContains(t, cur, "is not a curator", func() { AddCurator(cross(cur), alice) }) testing.SetRealm(testing.NewUserRealm(Admin)) uassert.AbortsContains(t, cur, "is the last curator", func() { RemoveCurator(cross(cur), Admin) }) AddCurator(cross(cur), alice) uassert.AbortsContains(t, cur, "already invited", func() { AddCurator(cross(cur), alice) }) // An invitation is not a seat: until alice accepts, the admin is still the // last curator, so a mistyped invite cannot strand the registry. uassert.True(t, IsInvited(alice)) uassert.False(t, IsCurator(alice)) uassert.Equal(t, 1, len(Curators())) uassert.AbortsContains(t, cur, "is the last curator", func() { RemoveCurator(cross(cur), Admin) }) // Withdrawing an invitation works, and a withdrawn one cannot be accepted. AddCurator(cross(cur), bob) RemoveCurator(cross(cur), bob) testing.SetRealm(testing.NewUserRealm(bob)) uassert.AbortsContains(t, cur, "has no curator invitation", func() { AcceptCurator(cross(cur)) }) testing.SetRealm(testing.NewUserRealm(alice)) AcceptCurator(cross(cur)) uassert.True(t, IsCurator(alice)) uassert.False(t, IsInvited(alice)) testing.SetRealm(testing.NewUserRealm(Admin)) uassert.AbortsContains(t, cur, "already a curator", func() { AddCurator(cross(cur), alice) }) uassert.Equal(t, 2, len(Curators())) // A curator added by the admin curates on equal terms, the admin included. testing.SetRealm(testing.NewUserRealm(alice)) ProposeZone(cross(cur), "alice-dev", "alicedev-1", "Alice's devnet", "", "devnet", "", "https://rpc.alice.example.com", "") ApproveZone(cross(cur), "alice-dev", zrev("alice-dev"), "") RemoveCurator(cross(cur), Admin) uassert.False(t, IsCurator(Admin)) uassert.AbortsContains(t, cur, "is not a curator", func() { RemoveCurator(cross(cur), bob) }) } // Every caller string a Render interpolates is escaped: a title that is a link, // a reason that would open a table column. The shapes that can carry markup at // all (slug, chain id, URL) are refused at write time instead, so they never // reach a Render. func TestRenderEscapesCallerText(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(alice)) ProposeZone(cross(cur), "evil", "evil-1", "[click](https://evil.example.com)", "**bold** | col", "devnet", "", "https://rpc.evil.example.com", "") RegisterEndpoint(cross(cur), "evil", "rpc", "https://rpc2.evil.example.com", "a|b") page := Render("zone/evil") uassert.False(t, strings.Contains(page, "[click](https://evil.example.com)"), "a title renders as text, not a link") uassert.False(t, strings.Contains(page, "**bold**"), "a description renders as text, not markup") uassert.True(t, strings.Contains(page, `a\|b`), "a label cannot open a table column") // A pipe in a title cannot open a column in the zone tables. ProposeZone(cross(cur), "evil-pipe", "evil-9", "Alice|official", "", "devnet", "", "https://rpc.pipe.example.com", "") uassert.True(t, strings.Contains(Render("proposals"), `[Alice\|official](/r/moul/zones/v0:zone/evil-pipe)`)) // A scheme and host are stored lowercased, so the link is live (the // sanitizer gno's md.Link uses accepts lowercase http and https only) and // every later lowercasing is free; the path keeps its case. testing.SetRealm(testing.NewUserRealm(bob)) ProposeZone(cross(cur), "upper", "upper-1", "Upper", "", "devnet", "HTTPS://Upper.example.com/Web", "HTTPS://rpc.upper.example.com", "") z, _ := GetZone("upper") uassert.Equal(t, "https://upper.example.com/Web", z.GnowebURL) uassert.Equal(t, "https://rpc.upper.example.com", z.RPCURL) // A proposal's URL is shown as code, to copy and check, not as a link. uassert.True(t, strings.Contains(Render("zone/upper"), "`https://upper.example.com/Web`")) uassert.False(t, strings.Contains(Render("zone/upper"), "](https://upper.example.com/Web)")) id := RegisterEndpoint(cross(cur), "upper", "faucet", "HTTP://f.upper.example.com", "") e, _ := GetEndpoint(id) uassert.Equal(t, "http://f.upper.example.com", e.Address) testing.SetRealm(testing.NewUserRealm(alice)) uassert.AbortsContains(t, cur, "contains", func() { ProposeZone(cross(cur), "evil2", "evil-2", "x", "", "devnet", "", "https://rpc.evil.example.com/)[x](y", "") }) uassert.AbortsContains(t, cur, "slug", func() { ProposeZone(cross(cur), "evil|3", "evil-3", "x", "", "devnet", "", "https://rpc.evil.example.com", "") }) } func TestRenderPagesAnswer(t *testing.T) { reset() uassert.True(t, strings.Contains(Render("zone/nope"), "No such zone.")) uassert.True(t, strings.Contains(Render("nope"), "No such page.")) uassert.True(t, strings.Contains(Render("proposals"), "Nothing waiting for review.")) uassert.True(t, strings.Contains(Render("zone/staging"), "⚠️ flagged: did not answer when probed 2026\\-10\\-01")) } // Every list is paginated and clamps a reader's ?page=, which is what keeps a // full registry inside vm/qrender's gas. What a page reads is measured // separately; this test pins only the paging. func TestRenderPaginates(t *testing.T) { reset() for i := 0; i < 2*PageSize; i++ { slug := "z" + strconv.Itoa(i) must(reg.Propose(alice, 1, slug, info("c-"+strconv.Itoa(i), "Zone "+strconv.Itoa(i), "", "devnet", "", "https://r"+strconv.Itoa(i)+".example.com", ""))) must(reg.ReviewZone(slug, zones.Approved, zrev(slug), Admin, 2, "")) } // 4 seeded + 50 = 54 approved, so 3 pages of 25. first := Render("") uassert.True(t, strings.Contains(first, "page 1 of 3")) uassert.True(t, strings.Contains(first, "Zone 20")) uassert.False(t, strings.Contains(first, "Zone 21"), "row 26 is on page 2") last := Render("?page=3") uassert.True(t, strings.Contains(last, "Zone 49")) uassert.False(t, strings.Contains(last, "Zone 45]")) uassert.True(t, strings.Contains(last, "page 3 of 3")) uassert.Equal(t, first, Render("?page=-1")) uassert.Equal(t, last, Render("?page=99999999999999999")) uassert.Equal(t, last, Render("?page=999999999999999999999999999999"), "past int is past the end, not page 1") uassert.Equal(t, last, Render("?page=%2B999999999999999999999999999999"), "a + sign does not change that") uassert.True(t, strings.Contains(Render("?page=%2B2"), "page 2 of 3")) uassert.Equal(t, first, Render("?page=-999999999999999999999999999999")) uassert.Equal(t, first, Render("?page=nope")) uassert.True(t, strings.Contains(Render("?status=nope"), "No such list.")) for i := 0; i < PageSize+3; i++ { who := testutils.TestAddress("reg" + strconv.Itoa(i/10)) _, err := reg.Register(who, 3, "z0", zones.RPC, "https://e"+strconv.Itoa(i)+".example.com", "") must(err) } _, err := reg.Register(alice, 3, "z0", zones.Peer, nodeID+"@p.example.com:26656", "") must(err) zp := Render("zone/z0") uassert.True(t, strings.Contains(zp, "page 1 of 2")) uassert.True(t, strings.Contains(zp, "**all (29)**")) uassert.True(t, strings.Contains(zp, `[rpc \(28\)](/r/moul/zones/v0:zone/z0?kind=rpc)`)) peers := Render("zone/z0?kind=peer") uassert.True(t, strings.Contains(peers, "p.example.com")) uassert.False(t, strings.Contains(peers, "e0.example.com")) uassert.False(t, strings.Contains(peers, "page 1 of"), "one page needs no pager") uassert.True(t, strings.Contains(Render("zone/z0?kind=rpc&page=2"), "[previous](/r/moul/zones/v0:zone/z0?kind=rpc&page=1)")) uassert.True(t, strings.Contains(Render("zone/z0?kind=grpc"), "No such endpoint kind.")) } // zrev is a zone's current revision, what a curator who just read it approves. func zrev(slug string) int64 { z, _ := GetZone(slug) return z.Revision } // erev is an endpoint's revision, what a verdict or a removal names. func erev(id int64) int64 { e, ok := GetEndpoint(id) if !ok { return 0 } return e.Revision } // zr is the revision of an endpoint's zone, what a verification also names. func zr(id int64) int64 { e, ok := GetEndpoint(id) if !ok { return 0 } return zrev(e.Zone) } // An invitation dies with its inviter, can be listed, and the set is bounded. func TestCuratorInvitations(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(Admin)) upper := address(strings.ToUpper(alice.String())) uassert.AbortsContains(t, cur, "lowercase", func() { AddCurator(cross(cur), upper) }) AddCurator(cross(cur), alice) testing.SetRealm(testing.NewUserRealm(alice)) AcceptCurator(cross(cur)) // alice invites bob, then is removed: bob's invitation goes with her. AddCurator(cross(cur), bob) uassert.Equal(t, bob.String(), Invited()[0].String()) testing.SetRealm(testing.NewUserRealm(Admin)) RemoveCurator(cross(cur), alice) uassert.False(t, IsInvited(bob)) uassert.Equal(t, 0, len(Invited())) testing.SetRealm(testing.NewUserRealm(bob)) uassert.AbortsContains(t, cur, "has no curator invitation", func() { AcceptCurator(cross(cur)) }) // Curators and open invitations together stop at MaxCurators. testing.SetRealm(testing.NewUserRealm(Admin)) for i := 1; i < MaxCurators; i++ { AddCurator(cross(cur), testutils.TestAddress("c"+strconv.Itoa(i))) } uassert.AbortsContains(t, cur, "remove one first", func() { AddCurator(cross(cur), bob) }) RemoveCurator(cross(cur), testutils.TestAddress("c1")) AddCurator(cross(cur), bob) } func TestRenderRoutesAreExact(t *testing.T) { reset() uassert.True(t, strings.Contains(Render("zone/onyx/approved-by-gno-core"), "No such page.")) uassert.True(t, strings.Contains(Render("proposals/x"), "No such page.")) uassert.True(t, strings.Contains(Render("zone"), "No such page.")) uassert.True(t, strings.Contains(Render("zone/onyx?kind=%20rpc"), "No such endpoint kind.")) uassert.True(t, strings.Contains(Render("zone/onyx?kind=seed"), "No seed endpoint registered yet.")) uassert.True(t, strings.Contains(Render("zone/onyx"), "Approve revision") == false, "an approved zone offers no Approve") } // gnoweb turns an @name into a user-profile link with an icon; free text // carries a backslash before every @ so a label cannot vouch for an account. func TestMentionsAreEscaped(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(Admin)) RegisterEndpoint(cross(cur), "onyx", "rpc", "https://m.example.com", "run by @gnocore") page := Render("zone/onyx?kind=rpc") uassert.True(t, strings.Contains(page, `run by \@gnocore`)) uassert.False(t, strings.Contains(page, "run by @gnocore")) } // The printed -remote is never one the same page flags. func TestQueryCommandSkipsAFlaggedRemote(cur realm, t *testing.T) { reset() z, _ := GetZone("moul-staging") in := zones.Info{ChainID: runtime.ChainID(), Title: z.Title, Kind: z.Kind, RPCURL: z.RPCURL, GnowebURL: z.GnowebURL} must(reg.Edit("moul-staging", zrev("moul-staging"), in, Admin, 1, "test")) uassert.True(t, strings.Contains(queryCommand("X()"), "-remote 'https://rpc.gno-staging.moul.p2p.team'")) e, ok := reg.EndpointByAddress("moul-staging", zones.RPC, z.RPCURL) urequire.True(t, ok) must(reg.ReviewEndpoint(e.ID, zones.Flagged, 0, erev(e.ID), Admin, 2, "down")) uassert.False(t, strings.Contains(queryCommand("X()"), "-remote")) } // A proposer cannot wipe a curator's verdict by removing the zone and // proposing it again. func TestProposerCannotRemoveOverAVerdict(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(bob)) ProposeZone(cross(cur), "bobs", "bobs-1", "Bob's", "", "devnet", "", "https://rpc.bobs.example.com", "") id := RegisterEndpoint(cross(cur), "bobs", "peer", nodeID+"@p.bobs.example.com:26656", "") testing.SetRealm(testing.NewUserRealm(Admin)) FlagEndpoint(cross(cur), id, erev(id), "malicious peer") testing.SetRealm(testing.NewUserRealm(bob)) testing.SkipHeights(ReviewWindow) uassert.AbortsContains(t, cur, "a curator ruled on", func() { RemoveZone(cross(cur), "bobs", zrev("bobs")) }) } // A bare g1 address in free text would become a profile link: its 1 is // written as a character reference so the mention parser does not match. func TestBareAddressesAreNotMentions(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(Admin)) RegisterEndpoint(cross(cur), "onyx", "rpc", "https://g.example.com", "run by "+Admin.String()) page := Render("zone/onyx?kind=rpc") uassert.False(t, strings.Contains(page, "run by g1manfred")) uassert.True(t, strings.Contains(page, "run by g&#49;manfred")) } // The main RPC is marked where it is shown when its endpoint is flagged, so the // official table and the zone page agree (TestQueryCommandSkipsAFlaggedRemote // covers the printed command). func TestFlaggedMainRPCIsMarked(t *testing.T) { reset() uassert.True(t, strings.Contains(Render(""), "`https://rpc.staging.gno.land` ⚠️ flagged")) uassert.True(t, strings.Contains(Render("zone/staging"), "`https://rpc.staging.gno.land` ⚠️ flagged")) uassert.False(t, strings.Contains(Render(""), "`https://rpc.gno.land` ⚠️")) } // A flagged gnoweb URL is shown as code and marked, never as a link the page's // own endpoint table says not to use. func TestFlaggedGnowebIsNotALink(t *testing.T) { reset() page := Render("zone/staging") uassert.True(t, strings.Contains(page, "`https://staging.gno.land` ⚠️ flagged")) uassert.False(t, strings.Contains(page, "](https://staging.gno.land)")) } // A curator's flag on an rpc endpoint under the tcp:// spelling still marks a // main RPC written as http://, and the reverse: gnokey dials them alike. func TestFlaggedRPCMatchesAcrossTcpAndHttp(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(Admin)) z, _ := GetZone("onyx") id := RegisterEndpoint(cross(cur), "onyx", "rpc", "tcp://rpc.tcp.example.com:26657", "") FlagEndpoint(cross(cur), id, erev(id), "down") in := zones.Info{ChainID: z.ChainID, Title: z.Title, Description: z.Description, Kind: z.Kind, GnowebURL: z.GnowebURL, RPCURL: "http://rpc.tcp.example.com:26657", GenesisURL: z.GenesisURL} must(reg.Edit("onyx", zrev("onyx"), in, Admin, 1, "moved")) uassert.True(t, strings.Contains(Render("zone/onyx"), "`http://rpc.tcp.example.com:26657` ⚠️ flagged")) id = RegisterEndpoint(cross(cur), "onyx", "rpc", "http://rpc.http.example.com:26657", "") FlagEndpoint(cross(cur), id, erev(id), "down") in.RPCURL = "tcp://rpc.http.example.com:26657" must(reg.Edit("onyx", zrev("onyx"), in, Admin, 2, "moved back")) uassert.True(t, strings.Contains(Render("zone/onyx"), "`tcp://rpc.http.example.com:26657` ⚠️ flagged")) } // A page offers an action only while somebody's call can succeed: with the // review queue full, Propose stays for curators only; with the live registry // full, Propose and a rejected zone's Approve give way to a note. func TestFullQueuesSayWhoMayStillAct(t *testing.T) { reset() in := func(i int) zones.Info { return zones.Info{ChainID: "fill-" + strconv.Itoa(i), Title: "Fill", Kind: zones.Testnet, RPCURL: "https://rpc" + strconv.Itoa(i) + ".example.com"} } slug := func(i int) string { return "fill-" + strconv.Itoa(i) } who := func(i int) address { return testutils.TestAddress("p" + strconv.Itoa(i/zones.MaxPendingPerProposer)) } must(reg.Propose(alice, 1, "nope", in(-1))) must(reg.ReviewZone("nope", zones.Rejected, zrev("nope"), Admin, 1, "no")) uassert.True(t, strings.Contains(Render("zone/nope"), "Approve revision")) for i := 0; i < zones.MaxPending; i++ { must(reg.Propose(who(i), 2, slug(i), in(i))) } // The queue is full for everybody but curators: the note says so, and the // link stays, labelled for them. uassert.True(t, strings.Contains(Render(""), "the review queue is full at 64 proposals")) uassert.True(t, strings.Contains(Render(""), "curators only, while full")) // Approve the queue away, then keep proposing and approving until the live // registry is full: no proposal is waiting, so Propose is hidden by the // live cap alone. for i := 0; i < zones.MaxPending; i++ { must(reg.ReviewZone(slug(i), zones.Approved, zrev(slug(i)), Admin, 3, "")) } uassert.True(t, strings.Contains(Render(""), "func=ProposeZone")) for i := zones.MaxPending; reg.Live() < zones.MaxZones; i++ { must(reg.ProposeExempt(who(i), 4, slug(i), in(i))) must(reg.ReviewZone(slug(i), zones.Approved, zrev(slug(i)), Admin, 4, "")) } uassert.Equal(t, 0, reg.ZoneCount(zones.Pending)) uassert.False(t, strings.Contains(Render("zone/nope"), "Approve revision")) uassert.True(t, strings.Contains(Render("zone/nope"), "approving it waits for a free place")) uassert.False(t, strings.Contains(Render(""), "func=ProposeZone"), "the live cap binds curators too") uassert.True(t, strings.Contains(Render(""), "the registry is full at 256 live zones")) } // Withdrawing the zone is no way round an endpoint's own review window: the // proposer removing it the block an endpoint appears would delete it before a // curator could see it. func TestRemoveZoneWaitsForItsEndpoints(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(bob)) ProposeZone(cross(cur), "bobs", "bobs-1", "Bob's", "", "devnet", "", "https://rpc.bobs.example.com", "") testing.SkipHeights(ReviewWindow) RegisterEndpoint(cross(cur), "bobs", "rpc", "https://fresh.bobs.example.com", "") uassert.AbortsContains(t, cur, "may be withdrawn in", func() { RemoveZone(cross(cur), "bobs", zrev("bobs")) }) testing.SkipHeights(ReviewWindow) RemoveZone(cross(cur), "bobs", zrev("bobs")) _, ok := GetZone("bobs") uassert.False(t, ok) } // A rejected zone is a record, endpoints and all: its registrant cannot strip // the evidence a rejection cites. func TestRejectedZoneKeepsItsEndpoints(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(bob)) ProposeZone(cross(cur), "fake", "fake-1", "Fake", "", "devnet", "", "https://rpc.fake.example.com", "") id := RegisterEndpoint(cross(cur), "fake", "rpc", "https://phish.example.com", "") testing.SetRealm(testing.NewUserRealm(Admin)) RejectZone(cross(cur), "fake", zrev("fake"), "its rpc is a phishing clone") testing.SetRealm(testing.NewUserRealm(bob)) testing.SkipHeights(ReviewWindow) uassert.AbortsContains(t, cur, "is rejected, a record", func() { RemoveEndpoint(cross(cur), id, erev(id)) }) } // Curators pass the admission gates: a flood that fills the review queue does // not lock out the people who clear it. The hard caps still hold. func TestCuratorsPassTheAdmissionGates(cur realm, t *testing.T) { reset() in := func(i int) zones.Info { return zones.Info{ChainID: "f-" + strconv.Itoa(i), Title: "F", Kind: zones.Testnet, RPCURL: "https://rpc" + strconv.Itoa(i) + ".example.com"} } for i := 0; i < zones.MaxPending; i++ { must(reg.Propose(testutils.TestAddress("p"+strconv.Itoa(i/zones.MaxPendingPerProposer)), 1, "f-"+strconv.Itoa(i), in(i))) } testing.SetRealm(testing.NewUserRealm(bob)) uassert.AbortsContains(t, cur, "waiting for review", func() { ProposeZone(cross(cur), "bobs", "bobs-1", "Bob's", "", "devnet", "", "https://rpc.bobs.example.com", "") }) testing.SetRealm(testing.NewUserRealm(Admin)) ProposeZone(cross(cur), "admins", "admins-1", "Admin's", "", "devnet", "", "https://rpc.admins.example.com", "") for i := 0; i < zones.MaxPendingPerProposer; i++ { ProposeZone(cross(cur), "a-"+strconv.Itoa(i), "a-"+strconv.Itoa(i), "A", "", "devnet", "", "https://rpc.a"+strconv.Itoa(i)+".example.com", "") } for i := 0; i < zones.MaxUnverifiedPerZone; i++ { _, err := reg.Register(testutils.TestAddress("r"+strconv.Itoa(i/zones.MaxEndpointsPerAddress)), 2, "onyx", zones.RPC, "https://n"+strconv.Itoa(i)+".example.com", "") must(err) } testing.SetRealm(testing.NewUserRealm(bob)) uassert.AbortsContains(t, cur, "waiting for review", func() { RegisterEndpoint(cross(cur), "onyx", "rpc", "https://bob.example.com", "") }) testing.SetRealm(testing.NewUserRealm(Admin)) for i := 0; reg.EndpointCount("onyx", "") < zones.MaxEndpointsPerZone; i++ { RegisterEndpoint(cross(cur), "onyx", "rpc", "https://c"+strconv.Itoa(i)+".example.com", "") } uassert.AbortsContains(t, cur, "is full at", func() { RegisterEndpoint(cross(cur), "onyx", "rpc", "https://over.example.com", "") }) } // A flag can be restated with a new reason, so correcting one never passes // through a state its registrant could remove it from. func TestAFlagsReasonCanBeAmended(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(bob)) id := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://typo.example.com", "") testing.SetRealm(testing.NewUserRealm(Admin)) FlagEndpoint(cross(cur), id, erev(id), "srves the wrong chian") FlagEndpoint(cross(cur), id, erev(id), "serves the wrong chain") e, _ := GetEndpoint(id) uassert.Equal(t, "serves the wrong chain", e.Reason) uassert.AbortsContains(t, cur, "already flagged", func() { FlagEndpoint(cross(cur), id, erev(id), "serves the wrong chain") }) uassert.Equal(t, 0, reg.Awaiting("onyx"), "restating a flag does not count it twice") } // Only a flag under the kind a URL is shown as marks it: a listing under // another kind is anybody's to make, so its flag never marks the zone's own // URL. The genesis URL, which no kind lists, is never marked. func TestOnlyTheShownKindsFlagMarks(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(bob)) z, _ := GetZone("mainnet") misKinded := []int64{ RegisterEndpoint(cross(cur), "mainnet", "explorer", z.RPCURL, ""), RegisterEndpoint(cross(cur), "mainnet", "faucet", z.GnowebURL, ""), RegisterEndpoint(cross(cur), "mainnet", "explorer", z.GenesisURL, ""), } testing.SetRealm(testing.NewUserRealm(Admin)) for _, id := range misKinded { FlagEndpoint(cross(cur), id, erev(id), "not that kind") } page := Render("zone/mainnet") uassert.False(t, strings.Contains(page, "` ⚠️ flagged"), "no fact is marked by another kind's flag") uassert.True(t, strings.Contains(page, "](https://gno.land)")) uassert.False(t, strings.Contains(Render(""), "`"+z.RPCURL+"` ⚠️")) // Its own kind's flag does mark it. e, ok := reg.EndpointByAddress("mainnet", zones.Gnoweb, z.GnowebURL) urequire.True(t, ok) FlagEndpoint(cross(cur), e.ID, erev(e.ID), "a phishing clone took the name") page = Render("zone/mainnet") uassert.True(t, strings.Contains(page, "`https://gno.land` ⚠️ flagged")) uassert.False(t, strings.Contains(page, "](https://gno.land)")) } // The note in place of an action says which cap is full, and nothing more: // advice on what frees it could name the zone the note is shown on. func TestFullCapsSayWhichIsFull(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(Admin)) for i := 0; reg.EndpointCount("onyx", "") < zones.MaxEndpointsPerZone; i++ { id := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://c"+strconv.Itoa(i)+".example.com", "") FlagEndpoint(cross(cur), id, erev(id), "spam") } page := Render("zone/onyx") uassert.True(t, strings.Contains(page, "endpoints are full at 128")) uassert.False(t, strings.Contains(page, "func=RegisterEndpoint")) for i := 0; reg.EndpointCount("mainnet", "") < zones.MaxUnverifiedPerZone+8; i++ { RegisterEndpoint(cross(cur), "mainnet", "rpc", "https://m"+strconv.Itoa(i)+".example.com", "") } uassert.True(t, reg.Awaiting("mainnet") >= zones.MaxUnverifiedPerZone) page = Render("zone/mainnet") uassert.True(t, strings.Contains(page, "the review queue is full at 64 endpoints awaiting review")) uassert.True(t, strings.Contains(page, "curators only, while full")) } // An edited zone names its editor in full, as it does its proposer. func TestEditorIsShownInFull(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(Admin)) z, _ := GetZone("onyx") EditZone(cross(cur), "onyx", z.Revision, z.ChainID, "Onyx, edited", z.Description, string(z.Kind), z.GnowebURL, z.RPCURL, z.GenesisURL, "a better title") uassert.True(t, strings.Contains(Render("zone/onyx"), "**edited** by `"+Admin.String()+"`")) } // A curator clears a cycling flood in one call; whatever a curator ruled on // stays. func TestClearUnreviewed(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(bob)) spam := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://spam1.example.com", "") RegisterEndpoint(cross(cur), "onyx", "rpc", "https://spam2.example.com", "") ruled := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://ruled.example.com", "") uassert.AbortsContains(t, cur, "is not a curator", func() { ClearUnreviewed(cross(cur), "onyx", reg.Revision()) }) testing.SetRealm(testing.NewUserRealm(Admin)) UnverifyEndpoint(cross(cur), ruled, erev(ruled), "looking into it") uassert.Equal(t, 2, ClearUnreviewed(cross(cur), "onyx", reg.Revision())) _, ok := GetEndpoint(spam) uassert.False(t, ok) _, ok = GetEndpoint(ruled) uassert.True(t, ok, "a curator's ruling is kept") uassert.Equal(t, 1, reg.OwnerCount("onyx", bob)) uassert.Equal(t, 0, ClearUnreviewed(cross(cur), "onyx", reg.Revision())) } // The review windows are the full ReviewWindow, a block short is refused, and // the zone's runs from its last edit by anybody. func TestReviewWindowsAreExact(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(bob)) ProposeZone(cross(cur), "bobs", "bobs-1", "Bob's", "", "devnet", "", "https://rpc.bobs.example.com", "") id := RegisterEndpoint(cross(cur), "bobs", "rpc", "https://a.bobs.example.com", "") testing.SkipHeights(ReviewWindow - 1) uassert.AbortsContains(t, cur, "may be withdrawn in 1 blocks", func() { RemoveEndpoint(cross(cur), id, erev(id)) }) uassert.AbortsContains(t, cur, "may act on it again in 1 blocks", func() { RemoveZone(cross(cur), "bobs", zrev("bobs")) }) testing.SkipHeights(1) // A curator's edit restarts the proposer's window: it runs from the last // edit by anybody. testing.SetRealm(testing.NewUserRealm(Admin)) z, _ := GetZone("bobs") EditZone(cross(cur), "bobs", z.Revision, z.ChainID, "Bob's, retitled", z.Description, string(z.Kind), z.GnowebURL, z.RPCURL, z.GenesisURL, "") testing.SetRealm(testing.NewUserRealm(bob)) uassert.AbortsContains(t, cur, "may act on it again in", func() { RemoveZone(cross(cur), "bobs", zrev("bobs")) }) } // A proposer's edit off local drops private endpoints only under the rules a // removal by the proposer has: never somebody else's, never one a curator // ruled on. func TestProposerLeavingLocalMeetsTheWithdrawalRules(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(bob)) ProposeZone(cross(cur), "lab", "lab-1", "Lab", "", "local", "", "http://127.0.0.1:26657", "") mine := RegisterEndpoint(cross(cur), "lab", "rpc", "http://10.1.2.3:26657", "") leave := func() { z, _ := GetZone("lab") EditZone(cross(cur), "lab", z.Revision, z.ChainID, z.Title, z.Description, "devnet", "", "https://rpc.lab.example.com", "", "") } testing.SetRealm(testing.NewUserRealm(Admin)) theirs := RegisterEndpoint(cross(cur), "lab", "rpc", "http://10.9.9.9:26657", "") testing.SetRealm(testing.NewUserRealm(bob)) testing.SkipHeights(ReviewWindow) uassert.AbortsContains(t, cur, "which somebody else registered", leave) testing.SetRealm(testing.NewUserRealm(Admin)) RemoveEndpoint(cross(cur), theirs, erev(theirs)) FlagEndpoint(cross(cur), mine, erev(mine), "a trap") testing.SetRealm(testing.NewUserRealm(bob)) uassert.AbortsContains(t, cur, "a curator ruled on", leave) testing.SetRealm(testing.NewUserRealm(Admin)) UnverifyEndpoint(cross(cur), mine, erev(mine), "") RemoveEndpoint(cross(cur), mine, erev(mine)) testing.SetRealm(testing.NewUserRealm(bob)) fresh := RegisterEndpoint(cross(cur), "lab", "rpc", "http://10.4.4.4:26657", "") // Dropping it is withdrawing it, so it waits out its own review window. uassert.AbortsContains(t, cur, "may be withdrawn in", leave) testing.SkipHeights(ReviewWindow) leave() _, ok := GetEndpoint(fresh) uassert.False(t, ok, "the proposer's own, never ruled on, is dropped") } // A reset reaches only a verified endpoint, which its registrant could // withdraw, and it says the zone changed, not the endpoint: so it leaves that // withdrawal in place, after a chain-id edit and after a retirement undone. func TestAResetLeavesTheWithdrawal(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(bob)) ProposeZone(cross(cur), "bobs", "bobs-1", "Bob's", "", "devnet", "", "https://rpc.bobs.example.com", "") id := RegisterEndpoint(cross(cur), "bobs", "rpc", "https://a.bobs.example.com", "") id2 := RegisterEndpoint(cross(cur), "bobs", "rpc", "https://b.bobs.example.com", "") testing.SetRealm(testing.NewUserRealm(Admin)) VerifyEndpoint(cross(cur), id, zr(id), erev(id), "answers bobs-1") VerifyEndpoint(cross(cur), id2, zr(id2), erev(id2), "answers bobs-1") testing.SetRealm(testing.NewUserRealm(bob)) testing.SkipHeights(ReviewWindow) z, _ := GetZone("bobs") EditZone(cross(cur), "bobs", z.Revision, "bobs-2", z.Title, z.Description, string(z.Kind), z.GnowebURL, z.RPCURL, z.GenesisURL, "") e, _ := GetEndpoint(id) uassert.Equal(t, "", e.ReviewedBy.String()) uassert.Equal(t, zones.ChainIDChanged, e.Reason) RemoveEndpoint(cross(cur), id, erev(id)) _, ok := GetEndpoint(id) uassert.False(t, ok, "a chain-id reset leaves the withdrawal") testing.SetRealm(testing.NewUserRealm(Admin)) VerifyEndpoint(cross(cur), id2, zr(id2), erev(id2), "answers bobs-2") ApproveZone(cross(cur), "bobs", zr(id2), "") RetireZone(cross(cur), "bobs", zr(id2), "shut down") ApproveZone(cross(cur), "bobs", zr(id2), "back") e, _ = GetEndpoint(id2) uassert.Equal(t, zones.ZoneRetired, e.Reason) testing.SetRealm(testing.NewUserRealm(bob)) RemoveEndpoint(cross(cur), id2, erev(id2)) _, ok = GetEndpoint(id2) uassert.False(t, ok, "a retirement's reset leaves it too") // A curator's own unverify is still a warning. id3 := RegisterEndpoint(cross(cur), "bobs", "rpc", "https://c.bobs.example.com", "") testing.SetRealm(testing.NewUserRealm(Admin)) UnverifyEndpoint(cross(cur), id3, erev(id3), "answers a different chain") testing.SetRealm(testing.NewUserRealm(bob)) testing.SkipHeights(ReviewWindow) uassert.AbortsContains(t, cur, "a curator ruled on", func() { RemoveEndpoint(cross(cur), id3, erev(id3)) }) } // The zone's own main RPC and gnoweb, under their own kind, are its proposer's // or a curator's to list: a stranger's listing, flagged for its label, would // mark the zone's own URL. func TestTheZonesOwnURLsAreReserved(cur realm, t *testing.T) { reset() z, _ := GetZone("onyx") reg.RemoveEndpoint(mustRPC(t, "onyx", z.RPCURL)) ge, ok := reg.EndpointByAddress("onyx", zones.Gnoweb, z.GnowebURL) urequire.True(t, ok) reg.RemoveEndpoint(ge.ID, ge.Revision) testing.SetRealm(testing.NewUserRealm(bob)) for _, a := range []string{strings.ToUpper(z.RPCURL), " " + z.RPCURL, z.RPCURL + "\t"} { uassert.AbortsContains(t, cur, "onyx's own rpc", func() { RegisterEndpoint(cross(cur), "onyx", "rpc", a, "moved: use mine") }) } uassert.AbortsContains(t, cur, "onyx's own gnoweb", func() { RegisterEndpoint(cross(cur), "onyx", "gnoweb", z.GnowebURL+"\t", "") }) // Under another kind it is anybody's, and its flag marks nothing. RegisterEndpoint(cross(cur), "onyx", "explorer", z.RPCURL, "") testing.SetRealm(testing.NewUserRealm(Admin)) RegisterEndpoint(cross(cur), "onyx", "rpc", z.RPCURL, "gno core") } // mustRPC returns the id and revision of the rpc endpoint listing url. func mustRPC(t *testing.T, slug, url string) (int64, int64) { e, ok := reg.EndpointByAddress(slug, zones.RPC, url) urequire.True(t, ok, url) return e.ID, e.Revision } // ClearUnreviewed takes only what the curator read: nothing registered after // the revision they name, nothing a curator registered, nothing a curator // ruled on (an unverify with no reason included), nothing a reset left. func TestClearUnreviewedIsBoundToWhatWasRead(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(Admin)) uassert.AbortsContains(t, cur, "no zone", func() { ClearUnreviewed(cross(cur), "nope", reg.Revision()) }) mine := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://curators-own.example.com", "") testing.SetRealm(testing.NewUserRealm(bob)) spam := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://spam.example.com", "") ruled := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://ruled.example.com", "") testing.SetRealm(testing.NewUserRealm(Admin)) FlagEndpoint(cross(cur), ruled, erev(ruled), "checking") UnverifyEndpoint(cross(cur), ruled, erev(ruled), "") read := reg.Revision() testing.SetRealm(testing.NewUserRealm(alice)) late := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://late.example.com", "alice's node") testing.SetRealm(testing.NewUserRealm(Admin)) uassert.Equal(t, 1, ClearUnreviewed(cross(cur), "onyx", read)) for id, kept := range map[int64]bool{mine: true, spam: false, ruled: true, late: true} { _, ok := GetEndpoint(id) uassert.Equal(t, kept, ok, strconv.FormatInt(id, 10)) } uassert.True(t, strings.Contains(Render("zone/onyx"), "func=ClearUnreviewed")) } // A removal names the endpoint's revision: one written before a colleague's // verdict fails rather than delete it unseen. A verified endpoint is its // registrant's to take down; listing it again starts it unverified. func TestRemovalIsBoundAndAVerifiedListingIsWithdrawable(cur realm, t *testing.T) { reset() uassert.Equal(t, int64(100), int64(ReviewWindow)) testing.SetRealm(testing.NewUserRealm(bob)) id := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://bobs-node.example.com", "") read := erev(id) testing.SetRealm(testing.NewUserRealm(Admin)) VerifyEndpoint(cross(cur), id, zr(id), erev(id), "answers onyx-1") uassert.AbortsContains(t, cur, "changed since you read it", func() { RemoveEndpoint(cross(cur), id, read) }) testing.SetRealm(testing.NewUserRealm(bob)) testing.SkipHeights(ReviewWindow) RemoveEndpoint(cross(cur), id, erev(id)) _, ok := GetEndpoint(id) uassert.False(t, ok) } // A proposer leaving local keeps every public endpoint, a curator's included: // the withdrawal rules apply only to what the edit drops. func TestLeavingLocalKeepsOthersPublicEndpoints(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(bob)) ProposeZone(cross(cur), "lab", "lab-1", "Lab", "", "local", "", "http://127.0.0.1:26657", "") testing.SetRealm(testing.NewUserRealm(Admin)) pub := RegisterEndpoint(cross(cur), "lab", "rpc", "https://rpc.lab.example.com", "") testing.SetRealm(testing.NewUserRealm(bob)) testing.SkipHeights(ReviewWindow) z, _ := GetZone("lab") EditZone(cross(cur), "lab", z.Revision, z.ChainID, z.Title, z.Description, "devnet", "", "https://rpc.lab.example.com", "", "") _, ok := GetEndpoint(pub) uassert.True(t, ok) } // The index's example falls back to onyx when this chain's zone lists no // peer, rather than print a query certain to return nothing. func TestTheIndexExampleFallsBack(t *testing.T) { reset() z, _ := GetZone("moul-staging") in := zones.Info{ChainID: runtime.ChainID(), Title: z.Title, Kind: z.Kind, RPCURL: z.RPCURL, GnowebURL: z.GnowebURL} must(reg.Edit("moul-staging", zrev("moul-staging"), in, Admin, 1, "on this chain")) uassert.Equal(t, 0, reg.EndpointCount("moul-staging", zones.Peer)) uassert.True(t, strings.Contains(Render(""), `ListAddresses("onyx", "peer", "verified")`)) } // ClearUnreviewed keeps an endpoint a reset left with a reason: a curator // verified it, and the proposer's chain-id edit undid that. func TestClearKeepsAResetEndpoint(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(bob)) ProposeZone(cross(cur), "bobs", "bobs-1", "Bob's", "", "devnet", "", "https://rpc.bobs.example.com", "") id := RegisterEndpoint(cross(cur), "bobs", "rpc", "https://a.bobs.example.com", "") testing.SetRealm(testing.NewUserRealm(Admin)) VerifyEndpoint(cross(cur), id, zr(id), erev(id), "") testing.SetRealm(testing.NewUserRealm(bob)) testing.SkipHeights(ReviewWindow) z, _ := GetZone("bobs") EditZone(cross(cur), "bobs", z.Revision, "bobs-2", z.Title, z.Description, string(z.Kind), z.GnowebURL, z.RPCURL, z.GenesisURL, "") testing.SetRealm(testing.NewUserRealm(Admin)) uassert.Equal(t, 0, ClearUnreviewed(cross(cur), "bobs", reg.Revision())) } // The reservation exempts any curator, and the proposer while the zone is // pending, each alone: every proposer right ends at approval. func TestTheReservationsExemptions(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(bob)) ProposeZone(cross(cur), "bobs", "bobs-1", "Bob's", "", "devnet", "https://gnoweb.bobs.example.com", "https://rpc.bobs.example.com", "") RegisterEndpoint(cross(cur), "bobs", "rpc", "https://rpc.bobs.example.com", "mine") // the proposer, pending testing.SetRealm(testing.NewUserRealm(Admin)) ApproveZone(cross(cur), "bobs", zrev("bobs"), "") testing.SetRealm(testing.NewUserRealm(bob)) uassert.AbortsContains(t, cur, "only a curator, or its proposer while it is pending", func() { RegisterEndpoint(cross(cur), "bobs", "gnoweb", "https://gnoweb.bobs.example.com", "mine too") }) testing.SetRealm(testing.NewUserRealm(Admin)) RegisterEndpoint(cross(cur), "bobs", "gnoweb", "https://gnoweb.bobs.example.com", "") // a curator, not the proposer } // On an approved zone the proposer's listing of a new own URL is anybody's // listing: a curator's edit drops it if nobody ruled on it, rather than adopt // it as the zone's own. func TestApprovedProposersListingIsNotAdopted(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(bob)) ProposeZone(cross(cur), "bobs", "bobs-1", "Bob's", "", "devnet", "", "https://rpc.bobs.example.com", "") testing.SetRealm(testing.NewUserRealm(Admin)) ApproveZone(cross(cur), "bobs", zrev("bobs"), "") testing.SetRealm(testing.NewUserRealm(bob)) id := RegisterEndpoint(cross(cur), "bobs", "rpc", "https://rpc2.bobs.example.com", "official next") testing.SetRealm(testing.NewUserRealm(Admin)) z, _ := GetZone("bobs") EditZone(cross(cur), "bobs", z.Revision, z.ChainID, z.Title, z.Description, string(z.Kind), z.GnowebURL, "https://rpc2.bobs.example.com", z.GenesisURL, "moved") _, ok := GetEndpoint(id) uassert.False(t, ok, "the proposer's unruled listing is dropped, not adopted") } // An edit cannot make a stranger's listing the zone's own. One nobody ruled on // is dropped in the edit, so pre-listing a zone's next URL cannot hold its // move off; one a curator ruled on refuses the edit until a curator removes // it, however the URL is padded. Each case starts fresh: a test does not roll // back an aborted call's writes, a transaction does. func TestAnEditCannotAdoptAStrangersListing(cur realm, t *testing.T) { setup := func() zones.Zone { reset() _, err := reg.Register(bob, 1, "onyx", zones.RPC, "https://rpc2.onyx.example.com", "official onyx rpc") must(err) for _, l := range [][2]string{{"gnoweb", "https://web2.onyx.example.com"}, {"rpc", "https://rpc3.onyx.example.com"}} { k, _ := zones.ParseEndpointKind(l[0]) id, err := reg.Register(bob, 1, "onyx", k, l[1], "official onyx") must(err) must(reg.ReviewEndpoint(id, zones.Flagged, 0, erev(id), Admin, 1, "not run by the onyx team")) } z, _ := GetZone("onyx") return z } testing.SetRealm(testing.NewUserRealm(Admin)) edit := func(z zones.Zone, gnoweb, rpc string) func() { return func() { EditZone(cross(cur), "onyx", zrev("onyx"), z.ChainID, z.Title+".", z.Description, string(z.Kind), gnoweb, rpc, z.GenesisURL, "moved") } } for _, c := range [][2]string{{"https://web2.onyx.example.com", ""}, {" https://web2.onyx.example.com\t", ""}, {"", " https://rpc3.onyx.example.com"}} { z := setup() gw, rpc := c[0], c[1] if gw == "" { gw = z.GnowebURL } if rpc == "" { rpc = z.RPCURL } uassert.AbortsContains(t, cur, "with a curator's ruling", edit(z, gw, rpc)) } z := setup() squat, ok := reg.EndpointByAddress("onyx", zones.RPC, "https://rpc2.onyx.example.com") urequire.True(t, ok) edit(z, z.GnowebURL, "https://rpc2.onyx.example.com")() _, ok = GetEndpoint(squat.ID) uassert.False(t, ok, "the unruled squat is dropped in the edit") z, _ = GetZone("onyx") uassert.Equal(t, "https://rpc2.onyx.example.com", z.RPCURL) } // A listing of the zone's current URL by a curator since removed does not // block an edit that leaves the URL alone. func TestAnExCuratorsListingDoesNotFreezeTheZone(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(Admin)) AddCurator(cross(cur), carol) testing.SetRealm(testing.NewUserRealm(carol)) AcceptCurator(cross(cur)) testing.SetRealm(testing.NewUserRealm(bob)) ProposeZone(cross(cur), "bobs", "bobs-1", "Bob's", "", "devnet", "", "https://rpc.bobs.example.com", "") testing.SetRealm(testing.NewUserRealm(Admin)) ApproveZone(cross(cur), "bobs", zrev("bobs"), "") testing.SetRealm(testing.NewUserRealm(carol)) listed := RegisterEndpoint(cross(cur), "bobs", "rpc", "https://rpc.bobs.example.com", "") testing.SetRealm(testing.NewUserRealm(Admin)) RemoveCurator(cross(cur), carol) z, _ := GetZone("bobs") EditZone(cross(cur), "bobs", z.Revision, z.ChainID, "Bob's, retitled", z.Description, string(z.Kind), z.GnowebURL, z.RPCURL, z.GenesisURL, "a better title") _, ok := GetEndpoint(listed) uassert.True(t, ok, "an edit that leaves the URL alone touches no listing of it") } // The Clear link reaches every page and kind, so it is offered only on the // unfiltered view of a zone that takes endpoints, bound to the revision the // page was rendered at; the call clears an endpoint registered exactly at the // revision it names. func TestTheClearLinksScope(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(bob)) id := RegisterEndpoint(cross(cur), "onyx", "faucet", "https://spam.example.com", "") at := strconv.FormatInt(reg.Revision(), 10) uassert.True(t, strings.Contains(Render("zone/onyx"), "throughRevision="+at)) uassert.False(t, strings.Contains(Render("zone/onyx?kind=rpc"), "func=ClearUnreviewed")) testing.SetRealm(testing.NewUserRealm(Admin)) uassert.Equal(t, 1, ClearUnreviewed(cross(cur), "onyx", erev(id))) testing.SetRealm(testing.NewUserRealm(bob)) RegisterEndpoint(cross(cur), "onyx", "faucet", "https://spam2.example.com", "") testing.SetRealm(testing.NewUserRealm(Admin)) RetireZone(cross(cur), "onyx", zrev("onyx"), "shut down") uassert.Equal(t, 1, reg.Clearable("onyx")) uassert.True(t, strings.Contains(Render("zone/onyx"), "func=ClearUnreviewed"), "a retired zone's flood is a curator's to clear") } // A registrant cannot withdraw an endpoint a curator unverified, even with no // reason: the reviewer is the ruling. func TestAReasonlessUnverifyIsARuling(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(bob)) id := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://bob.example.com", "") testing.SetRealm(testing.NewUserRealm(Admin)) FlagEndpoint(cross(cur), id, erev(id), "checking") UnverifyEndpoint(cross(cur), id, erev(id), "") e, _ := GetEndpoint(id) uassert.Equal(t, "", e.Reason) testing.SetRealm(testing.NewUserRealm(bob)) testing.SkipHeights(ReviewWindow) uassert.AbortsContains(t, cur, "a curator ruled on", func() { RemoveEndpoint(cross(cur), id, erev(id)) }) } // With no listing of its own kind at all, a flag under another kind still // does not mark the zone's URL: there is no fallback. func TestNoCrossKindFallback(cur realm, t *testing.T) { reset() z, _ := GetZone("mainnet") reg.RemoveEndpoint(mustRPC(t, "mainnet", z.RPCURL)) testing.SetRealm(testing.NewUserRealm(bob)) id := RegisterEndpoint(cross(cur), "mainnet", "explorer", z.RPCURL, "") testing.SetRealm(testing.NewUserRealm(Admin)) FlagEndpoint(cross(cur), id, erev(id), "not an explorer") uassert.False(t, strings.Contains(Render("zone/mainnet"), "`"+z.RPCURL+"` ⚠️")) } // Inputs are trimmed where a caller types them, the edit-time reservation // exempts the proposer and trims too, and the documented realm bounds hold. func TestRealmTrimsAndBounds(cur realm, t *testing.T) { reset() uassert.Equal(t, 16, MaxCurators) _, ok := GetZone(" onyx ") uassert.True(t, ok) testing.SetRealm(testing.NewUserRealm(bob)) ProposeZone(cross(cur), " bobs ", "bobs-1", "Bob's", "", "devnet", "", "https://rpc.bobs.example.com", "") _, ok = GetZone("bobs") uassert.True(t, ok) id := RegisterEndpoint(cross(cur), " bobs ", "rpc", "https://rpc2.bobs.example.com", "") testing.SetRealm(testing.NewUserRealm(Admin)) VerifyEndpoint(cross(cur), id, zr(id), erev(id), "") uassert.Equal(t, reg.Revision(), erev(id), "the last revision handed out is this verdict's") // The proposer's own listing of the new URL is no stranger's. testing.SetRealm(testing.NewUserRealm(bob)) testing.SkipHeights(ReviewWindow) z, _ := GetZone("bobs") EditZone(cross(cur), "bobs", z.Revision, z.ChainID, z.Title, z.Description, string(z.Kind), z.GnowebURL, " https://rpc2.bobs.example.com ", z.GenesisURL, "") _, ok = GetEndpoint(id) uassert.True(t, ok, "the proposer's listing stays") } // The list routes and their links: the approved alias, the retired list and // its link from the index, the pending list, a retired or rejected zone's // URLs shown as code, the review-queue note at exactly the gate, and the // pending zone's Register label. func TestRoutesNotesAndLabels(cur realm, t *testing.T) { reset() uassert.Equal(t, Render(""), Render("?status=approved")) testing.SetRealm(testing.NewUserRealm(Admin)) z, _ := GetZone("onyx") RetireZone(cross(cur), "onyx", zrev("onyx"), "shut down") uassert.True(t, strings.Contains(Render(""), "1 retired zone")) page := Render("?status=retired") uassert.True(t, strings.Contains(page, "Onyx")) uassert.False(t, strings.Contains(Render("zone/onyx"), "]("+z.GnowebURL+")"), "a retired zone's URLs are code") uassert.True(t, strings.Contains(Render("zone/onyx"), "`"+z.GnowebURL+"`")) testing.SetRealm(testing.NewUserRealm(bob)) ProposeZone(cross(cur), "bobs", "bobs-1", "Bob's", "", "devnet", "https://web.bobs.example.com", "https://rpc.bobs.example.com", "") uassert.True(t, strings.Contains(Render("proposals"), "bobs")) uassert.True(t, strings.Contains(Render("zone/bobs"), "proposer or curator")) testing.SetRealm(testing.NewUserRealm(Admin)) RejectZone(cross(cur), "bobs", zrev("bobs"), "no") uassert.False(t, strings.Contains(Render("zone/bobs"), "](https://web.bobs.example.com)"), "a rejected zone's URLs are code") for i := 0; reg.Awaiting("mainnet") < zones.MaxUnverifiedPerZone; i++ { _, err := reg.Register(testutils.TestAddress("q"+strconv.Itoa(i/zones.MaxEndpointsPerAddress)), 1, "mainnet", zones.RPC, "https://q"+strconv.Itoa(i)+".example.com", "") must(err) } uassert.Equal(t, zones.MaxUnverifiedPerZone, reg.Awaiting("mainnet")) uassert.True(t, strings.Contains(Render("zone/mainnet"), "the review queue is full at 64 endpoints awaiting review")) } // The Clear link shows only while something is clearable, and the admission // gate bounds how much that can be. func TestClearIsOfferedOnlyWhenItClears(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(Admin)) RegisterEndpoint(cross(cur), "onyx", "rpc", "https://curators.example.com", "") uassert.False(t, strings.Contains(Render("zone/onyx"), "func=ClearUnreviewed"), "a curator's own listing is not clearable") for i := 0; reg.Clearable("mainnet") <= zones.MaxUnverifiedPerZone; i++ { _, err := reg.Register(testutils.TestAddress("c"+strconv.Itoa(i/zones.MaxEndpointsPerAddress)), 1, "mainnet", zones.RPC, "https://c"+strconv.Itoa(i)+".example.com", "") if err != nil { // The admission gate holds the queue at 64: this is as full as a flood gets. break } } n := reg.Clearable("mainnet") uassert.True(t, strings.Contains(Render("zone/mainnet"), "Clear "+strconv.Itoa(n)+" never")) uassert.True(t, n <= zones.MaxUnverifiedPerZone) uassert.Equal(t, n, ClearUnreviewed(cross(cur), "mainnet", reg.Revision())) uassert.Equal(t, 0, reg.Clearable("mainnet")) uassert.False(t, strings.Contains(Render("zone/mainnet"), "func=ClearUnreviewed")) } // Where a decision evicts a record, the page says so beside it. func TestTheEvictionNote(cur realm, t *testing.T) { reset() for i := 0; reg.ZoneCount(zones.Rejected) < zones.MaxRejected; i++ { slug := "rj" + strconv.Itoa(i) must(reg.ProposeExempt(Admin, 1, slug, zones.Info{ChainID: slug, Title: "R", Kind: zones.Testnet, RPCURL: "https://rpc" + strconv.Itoa(i) + ".example.com"})) must(reg.ReviewZone(slug, zones.Rejected, zrev(slug), Admin, 1, "no")) } must(reg.ProposeExempt(Admin, 2, "next", zones.Info{ChainID: "next", Title: "N", Kind: zones.Testnet, RPCURL: "https://rpcn.example.com"})) uassert.True(t, strings.Contains(Render("zone/next"), "rejecting it drops the zone rejected longest ago")) uassert.False(t, strings.Contains(Render("zone/onyx"), "retiring it drops")) } // The last places of each hard cap are curators': strangers who keep a cap // full cannot lock out the people who would act under it, and the page says // so with a link for them. func TestTheHardCapsKeepPlacesForCurators(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(Admin)) for i := 0; reg.EndpointCount("onyx", "") < zones.MaxEndpointsPerZone-zones.ReservedForReviewers; i++ { id := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://v"+strconv.Itoa(i)+".example.com", "") VerifyEndpoint(cross(cur), id, zr(id), erev(id), "") } testing.SetRealm(testing.NewUserRealm(bob)) uassert.AbortsContains(t, cur, "kept for curators", func() { RegisterEndpoint(cross(cur), "onyx", "rpc", "https://bob.example.com", "") }) uassert.True(t, strings.Contains(Render("zone/onyx"), "endpoint places are kept for curators")) testing.SetRealm(testing.NewUserRealm(Admin)) RegisterEndpoint(cross(cur), "onyx", "rpc", "https://curator.example.com", "") for i := 0; reg.Live() < zones.MaxZones-zones.ReservedForReviewers; i++ { s := "l" + strconv.Itoa(i) must(reg.ProposeExempt(Admin, 1, s, zones.Info{ChainID: s, Title: "L", Kind: zones.Testnet, RPCURL: "https://rpcl" + strconv.Itoa(i) + ".example.com"})) must(reg.ReviewZone(s, zones.Approved, zrev(s), Admin, 1, "")) } testing.SetRealm(testing.NewUserRealm(bob)) uassert.AbortsContains(t, cur, "kept for curators", func() { ProposeZone(cross(cur), "bobs", "bobs-1", "Bob's", "", "devnet", "", "https://rpc.bobs.example.com", "") }) uassert.True(t, strings.Contains(Render(""), "places are kept for curators")) testing.SetRealm(testing.NewUserRealm(Admin)) ProposeZone(cross(cur), "admins", "admins-1", "A", "", "devnet", "", "https://rpc.admins.example.com", "") } // Slugs are trimmed, each zone state offers its links, and reasons and titles // are escaped in every cell. func TestRealmTrimsLinksAndEscapes(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(Admin)) // Every slug a caller types is trimmed. RetireZone(cross(cur), " onyx ", zrev("onyx"), "shut down") ApproveZone(cross(cur), " onyx ", zrev("onyx"), "") testing.SetRealm(testing.NewUserRealm(bob)) ProposeZone(cross(cur), "bobs", "bobs-1", "Bob's | best @moul "+Admin.String(), "run by @moul "+Admin.String(), "devnet", "", "https://rpc.bobs.example.com", "") id := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://spam.example.com", "") testing.SetRealm(testing.NewUserRealm(Admin)) uassert.Equal(t, 1, ClearUnreviewed(cross(cur), " onyx ", reg.Revision())) _, ok := GetEndpoint(id) uassert.False(t, ok) // A pending zone offers Approve and Remove; its title and description // have no live mention. page := Render("zone/bobs") uassert.True(t, strings.Contains(page, "func=ApproveZone")) uassert.True(t, strings.Contains(page, "func=RemoveZone")) uassert.True(t, strings.Contains(page, "pending review")) uassert.False(t, strings.Contains(page, " @moul")) uassert.False(t, strings.Contains(page, " "+Admin.String())) // A rejected zone offers Remove, its reason cell escapes a pipe and has // no live mention. RejectZone(cross(cur), " bobs ", zrev("bobs"), "a | b, see @moul") page = Render("zone/bobs") uassert.True(t, strings.Contains(page, "func=RemoveZone")) uassert.True(t, strings.Contains(page, "rejected")) uassert.False(t, strings.Contains(page, " @moul")) uassert.True(t, strings.Contains(Render("proposals?status=rejected"), `a \| b`)) uassert.True(t, strings.Contains(Render("proposals?status=nope"), "No such list.")) // An endpoint reason with a pipe stays in its cell. e := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://r.example.com", "") FlagEndpoint(cross(cur), e, erev(e), "x | y") uassert.True(t, strings.Contains(Render("zone/onyx?kind=rpc"), `x \| y`)) // A retired zone offers Approve, and its reason cell escapes a pipe. RetireZone(cross(cur), "mainnet", zrev("mainnet"), "c | d") uassert.True(t, strings.Contains(Render("zone/mainnet"), "func=ApproveZone")) uassert.True(t, strings.Contains(Render("zone/mainnet"), "retired")) uassert.True(t, strings.Contains(Render("?status=retired"), `c \| d`)) } // The index's example asks this chain's zone for peers when it lists one, not // the onyx fallback. func TestTheIndexExampleUsesThisChainsZone(t *testing.T) { reset() m, _ := GetZone("mainnet") in := zones.Info{ChainID: runtime.ChainID(), Title: m.Title, Kind: m.Kind, RPCURL: m.RPCURL, GnowebURL: m.GnowebURL} must(reg.Edit("mainnet", zrev("mainnet"), in, Admin, 1, "on this chain")) uassert.True(t, reg.EndpointCount("mainnet", zones.Peer) > 0) uassert.True(t, strings.Contains(Render(""), `ListAddresses("mainnet", "peer", "verified")`)) } // Retiring a zone with the retired list full says it evicts the oldest. func TestTheRetireEvictionNote(t *testing.T) { reset() for i := 0; reg.ZoneCount(zones.Retired) < zones.MaxRetired; i++ { s := "rt" + strconv.Itoa(i) must(reg.ProposeExempt(Admin, 1, s, zones.Info{ChainID: s, Title: "R", Kind: zones.Testnet, RPCURL: "https://rpcr" + strconv.Itoa(i) + ".example.com"})) must(reg.ReviewZone(s, zones.Approved, zrev(s), Admin, 1, "")) must(reg.ReviewZone(s, zones.Retired, zrev(s), Admin, 1, "gone")) } uassert.True(t, strings.Contains(Render("zone/onyx"), "retiring it drops the zone retired longest ago")) } // An edit is validated before it acts: one that cannot pass removes nothing // on its way to failing. A former curator's unruled listing of a new URL is // dropped like a stranger's: only a ruling refuses. func TestAnEditValidatesBeforeItRemoves(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(bob)) ws := RegisterEndpoint(cross(cur), "onyx", "rpc", "wss://rpc9.onyx.example.com/websocket", "") testing.SetRealm(testing.NewUserRealm(Admin)) FlagEndpoint(cross(cur), ws, erev(ws), "not ours") z, _ := GetZone("onyx") edit := func(rpc string) func() { return func() { EditZone(cross(cur), "onyx", zrev("onyx"), z.ChainID, z.Title+".", z.Description, string(z.Kind), z.GnowebURL, rpc, z.GenesisURL, "moved") } } uassert.AbortsContains(t, cur, "rpc url", edit("wss://rpc9.onyx.example.com/websocket")) _, ok := GetEndpoint(ws) uassert.True(t, ok, "a failing edit removed nothing") AddCurator(cross(cur), carol) testing.SetRealm(testing.NewUserRealm(carol)) AcceptCurator(cross(cur)) old := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://next.onyx.example.com", "") testing.SetRealm(testing.NewUserRealm(Admin)) RemoveCurator(cross(cur), carol) edit("https://next.onyx.example.com")() _, ok = GetEndpoint(old) uassert.False(t, ok, "an unruled former curator's listing is dropped") } // No tail a registration trims gets a stranger past the own-URL reservation. func TestTheReservationHoldsWhateverTheTail(cur realm, t *testing.T) { reset() z, _ := GetZone("onyx") reg.RemoveEndpoint(mustRPC(t, "onyx", z.RPCURL)) testing.SetRealm(testing.NewUserRealm(bob)) for _, tail := range []string{"/", "?", "/?"} { uassert.AbortsContains(t, cur, "onyx's own rpc", func() { RegisterEndpoint(cross(cur), "onyx", "rpc", z.RPCURL+tail, "moved: use mine") }) } // A tail that is a real query is not the zone's URL, and is stored keyed // as it was typed ("/??" is the same endpoint as "??"). for _, tail := range []string{"??", "?/"} { id := RegisterEndpoint(cross(cur), "onyx", "rpc", z.RPCURL+tail, "") e, _ := GetEndpoint(id) uassert.True(t, zones.Canonical(zones.RPC, e.Address) != zones.Canonical(zones.RPC, z.RPCURL), tail) } uassert.AbortsContains(t, cur, "already lists", func() { RegisterEndpoint(cross(cur), "onyx", "rpc", z.RPCURL+"/??", "") }) } // Curator removal, invitation order, rulings on a stranger's listing, the // genesis URL, the status badges and the decision links. func TestMoreRealmRules(cur realm, t *testing.T) { reset() // Only a curator removes a curator. testing.SetRealm(testing.NewUserRealm(bob)) uassert.AbortsContains(t, cur, "is not a curator", func() { RemoveCurator(cross(cur), Admin) }) // Curators and invitations list in address order. testing.SetRealm(testing.NewUserRealm(Admin)) AddCurator(cross(cur), carol) AddCurator(cross(cur), alice) inv := Invited() for i := 1; i < len(inv); i++ { uassert.True(t, inv[i-1].String() < inv[i].String()) } // A verdict with no reason on a stranger's listing of the new URL is a // ruling: the edit refuses. testing.SetRealm(testing.NewUserRealm(bob)) id := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://next.onyx.example.com", "") testing.SetRealm(testing.NewUserRealm(Admin)) VerifyEndpoint(cross(cur), id, zr(id), erev(id), "") z, _ := GetZone("onyx") uassert.AbortsContains(t, cur, "with a curator's ruling", func() { EditZone(cross(cur), "onyx", zrev("onyx"), z.ChainID, z.Title+".", z.Description, string(z.Kind), z.GnowebURL, "https://next.onyx.example.com", z.GenesisURL, "moved") }) reset() // a test does not roll back the aborted edit's writes; a transaction does z, _ = GetZone("onyx") // The genesis URL is never marked, whatever kind lists it. g := RegisterEndpoint(cross(cur), "onyx", "gnoweb", z.GenesisURL, "") FlagEndpoint(cross(cur), g, erev(g), "not a gnoweb") uassert.False(t, strings.Contains(Render("zone/onyx"), "`"+z.GenesisURL+"` ⚠️")) // Status badges, exactly. uassert.True(t, strings.Contains(Render("zone/onyx"), "✅ **official**")) testing.SetRealm(testing.NewUserRealm(bob)) ProposeZone(cross(cur), "bobs", "bobs-1", "Bob's", "", "devnet", "", "https://rpc.bobs.example.com", "") uassert.True(t, strings.Contains(Render("zone/bobs"), "⏳ **pending review**")) // The pending zone's decision links carry its revision. rv := strconv.FormatInt(zrev("bobs"), 10) page := Render("zone/bobs") for _, fn := range []string{"ApproveZone", "RejectZone", "RemoveZone"} { uassert.True(t, regexpLinkCarries(page, fn, rv), fn) } testing.SetRealm(testing.NewUserRealm(Admin)) RejectZone(cross(cur), "bobs", zrev("bobs"), "no") uassert.True(t, strings.Contains(Render("zone/bobs"), "❌ **rejected**")) RetireZone(cross(cur), "onyx", zrev("onyx"), "gone") uassert.True(t, strings.Contains(Render("zone/onyx"), "⏹️ **retired**")) } // Paging clamps a reader's page number and shows every row. func TestPagingEdges(t *testing.T) { reset() for i := 0; reg.EndpointCount("mainnet", "") < PageSize+1; i++ { _, err := reg.RegisterExempt(Admin, 1, "mainnet", zones.Indexer, "https://p"+strconv.Itoa(i)+".example.com", "") must(err) } all := reg.Endpoints(zones.EndpointFilter{Zone: "mainnet"}) last := all[len(all)-1].Address uassert.True(t, strings.Contains(Render("zone/mainnet?page=2"), last), "the 26th row is on page 2") uassert.Equal(t, Render("zone/mainnet?page=1"), Render("zone/mainnet?page=0")) uassert.Equal(t, Render("zone/mainnet?page=2"), Render("zone/mainnet?page=3")) } // A proposer's edit drops nothing that is not theirs: a former curator's // listing on their pending zone refuses the edit instead of being dropped. func TestAProposersEditDropsNothingNotTheirs(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(Admin)) AddCurator(cross(cur), carol) testing.SetRealm(testing.NewUserRealm(carol)) AcceptCurator(cross(cur)) testing.SetRealm(testing.NewUserRealm(bob)) ProposeZone(cross(cur), "bobs", "bobs-1", "Bob's", "", "devnet", "", "https://rpc.bobs.example.com", "") testing.SetRealm(testing.NewUserRealm(carol)) id := RegisterEndpoint(cross(cur), "bobs", "rpc", "https://b.example.com", "") testing.SetRealm(testing.NewUserRealm(Admin)) RemoveCurator(cross(cur), carol) testing.SetRealm(testing.NewUserRealm(bob)) testing.SkipHeights(ReviewWindow) z, _ := GetZone("bobs") uassert.AbortsContains(t, cur, "a curator removes it before", func() { EditZone(cross(cur), "bobs", z.Revision, z.ChainID, z.Title, z.Description, string(z.Kind), "", "https://b.example.com", "", "") }) _, ok := GetEndpoint(id) uassert.True(t, ok) } // regexpLinkCarries reports whether a $help link to fn carries revision rv. func regexpLinkCarries(page, fn, rv string) bool { for _, part := range strings.Split(page, "$help&") { end := strings.IndexAny(part, ")") if end < 0 { continue } link := part[:end] if strings.Contains(link, "func="+fn) && strings.Contains(link, "revision="+rv) { return true } } return false } // A flagged listing of a zone's new URL refuses the edit whoever holds it and // whoever edits: it would mark the zone's own URL. Each case starts fresh: a // test does not roll back an aborted call's writes. func TestAFlaggedOwnListingRefusesTheEdit(cur realm, t *testing.T) { for _, tc := range []struct { name string approve bool holder, editor address kind, url, want string }{ {"the proposer's, on a pending zone, by the proposer", false, bob, bob, "rpc", "https://rpc2.bobs.example.com", "flagged; a curator removes it"}, {"a curator's, under rpc, by a curator", true, Admin, Admin, "rpc", "https://rpc2.bobs.example.com", "flagged; a curator removes it"}, {"a curator's, under gnoweb, by a curator", true, Admin, Admin, "gnoweb", "https://web2.bobs.example.com", "flagged; a curator removes it"}, {"the proposer's, on an approved zone, by a curator", true, bob, Admin, "rpc", "https://rpc2.bobs.example.com", "a curator removes it before it becomes"}, } { reset() testing.SetRealm(testing.NewUserRealm(bob)) ProposeZone(cross(cur), "bobs", "bobs-1", "Bob's", "", "devnet", "https://web.bobs.example.com", "https://rpc.bobs.example.com", "") if tc.approve { testing.SetRealm(testing.NewUserRealm(Admin)) ApproveZone(cross(cur), "bobs", zrev("bobs"), "") } k, _ := zones.ParseEndpointKind(tc.kind) id, err := reg.Register(tc.holder, 1, "bobs", k, tc.url, "spam") must(err) must(reg.ReviewEndpoint(id, zones.Flagged, 0, erev(id), Admin, 1, "spam label")) testing.SkipHeights(ReviewWindow) testing.SetRealm(testing.NewUserRealm(tc.editor)) z, _ := GetZone("bobs") web, rpc, reason := z.GnowebURL, z.RPCURL, "" if tc.kind == "gnoweb" { web = tc.url } else { rpc = tc.url } if tc.approve { reason = "moved" } uassert.AbortsContains(t, cur, tc.want, func() { EditZone(cross(cur), "bobs", z.Revision, z.ChainID, z.Title, z.Description, string(z.Kind), web, rpc, z.GenesisURL, reason) }, tc.name) } } // Paging keeps a list's status, and the next link shows on the page before // the last; a registrant cannot withdraw from a retired zone. func TestListPagingAndRetiredRecords(cur realm, t *testing.T) { reset() for i := 0; reg.ZoneCount(zones.Retired) < 2*PageSize+1; i++ { s := "pr" + strconv.Itoa(i) must(reg.ProposeExempt(Admin, 1, s, zones.Info{ChainID: s, Title: "P", Kind: zones.Testnet, RPCURL: "https://rpcp" + strconv.Itoa(i) + ".example.com"})) must(reg.ReviewZone(s, zones.Approved, zrev(s), Admin, 1, "")) must(reg.ReviewZone(s, zones.Retired, zrev(s), Admin, 1, "gone")) } page := Render("?status=retired&page=2") uassert.True(t, strings.Contains(page, "status=retired&page=3"), "page 2 of 3 links to page 3, in the same list") uassert.True(t, strings.Contains(page, "status=retired&page=1")) testing.SetRealm(testing.NewUserRealm(bob)) id := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://bob.example.com", "") testing.SetRealm(testing.NewUserRealm(Admin)) RetireZone(cross(cur), "onyx", zrev("onyx"), "gone") testing.SetRealm(testing.NewUserRealm(bob)) testing.SkipHeights(ReviewWindow) uassert.AbortsContains(t, cur, "a record", func() { RemoveEndpoint(cross(cur), id, erev(id)) }) uassert.False(t, strings.Contains(Render("proposals"), "0 rejected"), "no link to an empty rejected list") uassert.Equal(t, Render("proposals"), Render("proposals?status=pending")) } // The reserve notes come before the queue notes when both caps are full, and // every link a non-curator's call would fail on says who it is for. func TestReserveNotesAreLabelledAndComeFirst(cur realm, t *testing.T) { reset() for i := 0; reg.Live() < zones.MaxZones-zones.ReservedForReviewers-zones.MaxPending; i++ { s := "ap" + strconv.Itoa(i) must(reg.ProposeExempt(Admin, 1, s, zones.Info{ChainID: s, Title: "A", Kind: zones.Testnet, RPCURL: "https://rpca" + strconv.Itoa(i) + ".example.com"})) must(reg.ReviewZone(s, zones.Approved, zrev(s), Admin, 1, "")) } for i := 0; reg.ZoneCount(zones.Pending) < zones.MaxPending; i++ { s := "pp" + strconv.Itoa(i) must(reg.ProposeExempt(Admin, 1, s, zones.Info{ChainID: s, Title: "P", Kind: zones.Testnet, RPCURL: "https://rpcq" + strconv.Itoa(i) + ".example.com"})) } uassert.Equal(t, zones.MaxZones-zones.ReservedForReviewers, reg.Live()) page := Render("") uassert.True(t, strings.Contains(page, "the registry's last 16 places are kept for curators")) uassert.True(t, strings.Contains(page, `Propose a zone \(curators only\)]`)) uassert.False(t, strings.Contains(page, "the review queue is full"), "the reserve binds first") reset() for i := 0; reg.Awaiting("onyx") < zones.MaxUnverifiedPerZone; i++ { _, err := reg.RegisterExempt(Admin, 1, "onyx", zones.RPC, "https://u"+strconv.Itoa(i)+".example.com", "") must(err) } for i := 0; reg.EndpointCount("onyx", "") < zones.MaxEndpointsPerZone-zones.ReservedForReviewers; i++ { id, err := reg.RegisterExempt(Admin, 1, "onyx", zones.RPC, "https://f"+strconv.Itoa(i)+".example.com", "") must(err) must(reg.ReviewEndpoint(id, zones.Flagged, 0, erev(id), Admin, 1, "spam")) } page = Render("zone/onyx") uassert.True(t, strings.Contains(page, "the last 16 endpoint places are kept for curators")) uassert.True(t, strings.Contains(page, `Register an endpoint \(curators only\)]`)) uassert.False(t, strings.Contains(page, "the review queue is full"), "the reserve binds first") reset() testing.SetRealm(testing.NewUserRealm(bob)) RegisterEndpoint(cross(cur), "onyx", "rpc", "https://bob.example.com", "") uassert.True(t, strings.Contains(Render("zone/onyx"), `all pages and kinds \(curators only\)]`)) } // A rejected or retired zone takes no endpoints: no Register link and no cap // note, but a curator still clears its never-reviewed endpoints. func TestARecordZoneOffersNoRegistration(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(bob)) RegisterEndpoint(cross(cur), "onyx", "rpc", "https://bob.example.com", "") for i := 0; reg.EndpointCount("onyx", "") < zones.MaxEndpointsPerZone; i++ { id, err := reg.RegisterExempt(Admin, 1, "onyx", zones.RPC, "https://f"+strconv.Itoa(i)+".example.com", "") must(err) must(reg.ReviewEndpoint(id, zones.Flagged, 0, erev(id), Admin, 1, "spam")) } must(reg.ReviewZone("onyx", zones.Retired, zrev("onyx"), Admin, 1, "gone")) page := Render("zone/onyx") uassert.False(t, strings.Contains(page, "func=RegisterEndpoint")) uassert.False(t, strings.Contains(page, "endpoints are full")) uassert.True(t, strings.Contains(page, "func=ClearUnreviewed"), "the record's flood is still a curator's to clear") } // On a pending zone the proposer is the one gated registrant, so once they // reach the per-address cap the link is for curators. func TestThePendingRegisterLinkFollowsTheProposersCap(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(bob)) ProposeZone(cross(cur), "bobs", "bobs-1", "Bob's", "", "devnet", "", "https://rpc.bobs.example.com", "") uassert.True(t, strings.Contains(Render("zone/bobs"), `Register an endpoint \(proposer or curator\)]`)) for i := 0; i < zones.MaxEndpointsPerAddress; i++ { RegisterEndpoint(cross(cur), "bobs", "rpc", "https://b"+strconv.Itoa(i)+".bobs.example.com", "") } page := Render("zone/bobs") uassert.True(t, strings.Contains(page, "the proposer has registered 16 endpoints here")) uassert.True(t, strings.Contains(page, `Register an endpoint \(curators only\)]`)) } // The rejected list's pager stays in the rejected list. func TestTheRejectedPagerKeepsItsList(t *testing.T) { reset() for i := 0; reg.ZoneCount(zones.Rejected) < PageSize+1; i++ { s := "rj" + strconv.Itoa(i) must(reg.ProposeExempt(Admin, 1, s, zones.Info{ChainID: s, Title: "R", Kind: zones.Testnet, RPCURL: "https://rpcr" + strconv.Itoa(i) + ".example.com"})) must(reg.ReviewZone(s, zones.Rejected, zrev(s), Admin, 1, "no")) } uassert.True(t, strings.Contains(Render("proposals?status=rejected"), "status=rejected&page=2")) } // A reason on an endpoint is free text: a mention in it stays text. func TestAnEndpointReasonCannotMention(t *testing.T) { reset() id, err := reg.Register(bob, 1, "onyx", zones.RPC, "https://bob.example.com", "") must(err) must(reg.ReviewEndpoint(id, zones.Flagged, 0, erev(id), Admin, 1, "ask @alice")) page := Render("zone/onyx?kind=rpc") uassert.True(t, strings.Contains(page, `ask \@alice`)) uassert.False(t, strings.Contains(page, "ask @alice")) } // An edit onto a listing a curator ruled on is refused whatever the ruling // left: a reasonless unverify, or a reset's reason with nobody recorded. func TestAnEditRefusesEveryRuling(cur realm, t *testing.T) { // A curator's reasonless unverify. reset() id, err := reg.Register(bob, 1, "onyx", zones.RPC, "https://rpc2.onyx.example.com", "") must(err) must(reg.ReviewEndpoint(id, zones.Verified, zrev("onyx"), erev(id), Admin, 1, "")) must(reg.ReviewEndpoint(id, zones.Unverified, 0, erev(id), Admin, 1, "")) testing.SetRealm(testing.NewUserRealm(Admin)) z, _ := GetZone("onyx") uassert.AbortsContains(t, cur, "with a curator's ruling", func() { EditZone(cross(cur), "onyx", z.Revision, z.ChainID, z.Title, z.Description, string(z.Kind), z.GnowebURL, "https://rpc2.onyx.example.com", z.GenesisURL, "moved") }) // A proposer's chain-id reset of a former curator's listing: no reviewer, // a reason. reset() testing.SetRealm(testing.NewUserRealm(bob)) ProposeZone(cross(cur), "bobs", "bobs-1", "Bob's", "", "devnet", "", "https://rpc.bobs.example.com", "") id, err = reg.RegisterExempt(carol, 1, "bobs", zones.RPC, "https://rpc2.bobs.example.com", "") must(err) must(reg.ReviewEndpoint(id, zones.Verified, zrev("bobs"), erev(id), Admin, 1, "")) testing.SkipHeights(ReviewWindow) z, _ = GetZone("bobs") EditZone(cross(cur), "bobs", z.Revision, "bobs-2", z.Title, z.Description, string(z.Kind), z.GnowebURL, z.RPCURL, z.GenesisURL, "") e, _ := GetEndpoint(id) uassert.Equal(t, "", e.ReviewedBy.String()) testing.SetRealm(testing.NewUserRealm(Admin)) z, _ = GetZone("bobs") uassert.AbortsContains(t, cur, "with a curator's ruling", func() { EditZone(cross(cur), "bobs", z.Revision, z.ChainID, z.Title, z.Description, string(z.Kind), z.GnowebURL, "https://rpc2.bobs.example.com", z.GenesisURL, "") }) } // Respelling the zone's own URL is no change to it: the edit does not look at // listings of it, ruled or not. func TestRespellingTheOwnURLIsNoMove(cur realm, t *testing.T) { reset() z, _ := GetZone("onyx") reg.RemoveEndpoint(mustRPC(t, "onyx", z.RPCURL)) id, err := reg.Register(bob, 1, "onyx", zones.RPC, z.RPCURL, "") must(err) must(reg.ReviewEndpoint(id, zones.Verified, zrev("onyx"), erev(id), Admin, 1, "")) testing.SetRealm(testing.NewUserRealm(Admin)) z, _ = GetZone("onyx") EditZone(cross(cur), "onyx", z.Revision, z.ChainID, "Onyx, retitled", z.Description, string(z.Kind), z.GnowebURL, z.RPCURL+":443", z.GenesisURL, "retitled") _, ok := GetEndpoint(id) uassert.True(t, ok) } // The slug is trimmed on every write, EditZone and RemoveZone included. func TestEveryWriteTrimsTheSlug(cur realm, t *testing.T) { reset() testing.SetRealm(testing.NewUserRealm(Admin)) ProposeZone(cross(cur), "tz", "tz-1", "T", "", "devnet", "", "https://rpc.tz.example.com", "") z, _ := GetZone("tz") EditZone(cross(cur), " tz\t", z.Revision, z.ChainID, "T, edited", z.Description, string(z.Kind), z.GnowebURL, z.RPCURL, z.GenesisURL, "") z, _ = GetZone("tz") uassert.Equal(t, "T, edited", z.Title) RemoveZone(cross(cur), "\ttz ", z.Revision) _, ok := GetZone("tz") uassert.False(t, ok) } // The seeded endpoints went through the gated registration, so none is // Exempt; and a zone with no endpoints draws no kind bar. func TestSeedsAreGatedAndAnEmptyZoneHasNoKindBar(cur realm, t *testing.T) { reset() for _, slug := range []string{"mainnet", "onyx", "staging", "moul-staging"} { for _, e := range ListEndpoints(slug, "", "") { uassert.False(t, e.Exempt, slug+" #"+strconv.FormatInt(e.ID, 10)) } } testing.SetRealm(testing.NewUserRealm(Admin)) ProposeZone(cross(cur), "tz", "tz-1", "T", "", "devnet", "", "https://rpc.tz.example.com", "") uassert.False(t, strings.Contains(Render("zone/tz"), "all (0)")) }

Result log

msg:0,success:true,log:,events:[]

← Back to block 592,217