zones.gno
25.97 Kb · 594 lines
1// untrusted-render: slugs, chain ids, URLs and peer addresses are charset-checked
2// at write time by p/moul/zones; titles, descriptions, labels and reasons are a
3// caller's free text and go through ui.Inline or ui.Cell at every call site.
4
5// Package zones is a curated registry of gno.land networks: mainnet, the
6// testnets, staging chains, anybody's gnodev. Each zone carries what a node or a
7// wallet needs to join it (chain id, RPC, gnoweb, genesis) plus a growing list of
8// endpoints: RPCs, seeds and peers, indexers, faucets, explorers.
9//
10// Curation is the point. Anybody may propose a zone and register endpoints on
11// an approved one (on a pending one, its proposer or a curator; a zone's own
12// main RPC and gnoweb, only them); a curator approves or rejects a proposal,
13// retires a zone that stopped running, and verifies or flags an endpoint. The
14// latest decision on each is recorded with who made it and when, and a rejection, a retirement, a flag or an edit to an
15// approved zone must also say why, on the zone's page. Nothing is hidden while
16// it waits: proposals and unverified endpoints are listed, and labelled.
17//
18// This realm only manages the information and answers questions about it. The
19// read helpers (GetZone, ListZones, GetEndpoint, ListEndpoints, ListAddresses,
20// IsCurator, Curators, IsInvited, Invited) take plain values so they work from
21// `gnokey query vm/qeval` as well as from another realm, and turning them into a
22// node's config.toml is a separate realm's job.
23//
24// The model, its validation and its state machine are p/moul/zones. This realm
25// owns only who may write.
26package zones
27
28import (
29 "chain/runtime"
30 "strconv"
31
32 "gno.land/p/moul/addrset/v1"
33 "gno.land/p/moul/zones/v0"
34)
35
36// Admin is the first curator. More are invited with AddCurator and become
37// curators when they accept.
38const Admin address = "g1manfred47kzduec920z88wfr64ylksmdcedlf5"
39
40// ReviewWindow is how many blocks must pass before the author of a change may
41// change it again, a rate bound rather than a review deadline (100 blocks is
42// minutes): after a zone was proposed or last edited (by anybody) before its
43// proposer may edit or withdraw it, and after an endpoint was registered before
44// its registrant may withdraw it. Every edit bumps the revision a curator's
45// decision must name, and a withdrawal plus a fresh proposal or registration
46// does the same with a new revision or id, so without a wait a proposer or a
47// registrant acting every block would keep their entry out of every curator's
48// reach. Curators are not limited.
49const ReviewWindow = 100
50
51// MaxCurators bounds curators and open invitations together. The curator list
52// renders on the index page, and only curators can grow it, but a bound costs
53// nothing and an unbounded list on a public page is a page someone can break.
54const MaxCurators = 16
55
56var (
57 reg = zones.NewRegistry()
58 curators addrset.Set
59 invited addrset.Set // invited by a curator, not yet accepted
60 inviter = map[address]address{} // invitee -> the curator who invited them; lookups only
61)
62
63func init() {
64 curators.Add(Admin)
65 seed()
66}
67
68// ---- proposing and curating zones
69
70// ProposeZone files a new zone for review. Anybody may; it is listed as a
71// proposal until a curator approves or rejects it. The review queue's caps
72// (zones.MaxPending, zones.MaxPendingPerProposer) do not stop a curator, who
73// also has the registry's last zones.ReservedForReviewers places, so neither a
74// flood nor a full registry locks out the people who clear it.
75//
76// kind is mainnet, testnet, devnet or local. gnowebURL and genesisURL may be
77// empty; rpcURL may not, and is <scheme>://<host>[:<port>] with no path, which
78// is what gnokey -remote takes.
79func ProposeZone(cur realm, slug, chainID, title, description, kind, gnowebURL, rpcURL, genesisURL string) {
80 who := caller(cur)
81 in := info(chainID, title, description, kind, gnowebURL, rpcURL, genesisURL)
82 if curators.Has(who) {
83 // A flood that fills the review queue must not lock out the people
84 // who clear it.
85 must(reg.ProposeExempt(who, runtime.ChainHeight(), zones.TrimSpaces(slug), in))
86 return
87 }
88 must(reg.Propose(who, runtime.ChainHeight(), zones.TrimSpaces(slug), in))
89}
90
91// EditZone replaces a zone's Info, every field but its slug and status. A
92// curator may edit any pending or approved zone; the proposer may edit their
93// own while it is pending. revision is the zone's Revision the edit was written
94// against: it fails if the zone changed in between, its content or its status.
95// Every edit bumps the revision, so a decision prepared against the old one
96// fails too. A proposer edits only ReviewWindow blocks after the zone was
97// proposed or last edited, and an edit that changes nothing is refused. On a
98// pending zone the reason must be empty; on an approved one it is required and
99// replaces the review on record, so the page names who changed the values, and
100// why. A new chain id sends the zone's verified endpoints back to unverified.
101// Leaving the local kind drops every endpoint on a private host, and is refused
102// while one carries a curator's ruling (a verified one its registrant may
103// withdraw first); the proposer's edit only drops endpoints that are theirs and
104// that they could withdraw on their own (RemoveEndpoint), as for RemoveZone. A
105// new main RPC or gnoweb a stranger (on an approved zone, its proposer too)
106// already lists under its kind would make
107// that listing the zone's own: on a curator's edit it is dropped if nobody
108// ruled on it, and refuses the edit if a curator did; a proposer's edit
109// refuses rather than drop what is not theirs; and a flagged listing refuses
110// the edit whoever holds it. The registry validates the edit before it drops
111// or resets anything, and a refusal reverts the edit with it. A rejected or
112// retired zone cannot be edited.
113func EditZone(cur realm, slug string, revision int64, chainID, title, description, kind, gnowebURL, rpcURL, genesisURL, reason string) {
114 who := caller(cur)
115 slug = zones.TrimSpaces(slug)
116 z := mustZone(slug)
117 in := info(chainID, title, description, kind, gnowebURL, rpcURL, genesisURL)
118 if !curators.Has(who) {
119 if z.Status != zones.Pending || z.Proposer != who {
120 panic("zones: only a curator, or the proposer while it is pending, may edit " + slug)
121 }
122 assertReviewWindow(slug, z)
123 if z.Kind == zones.Local && in.Kind != zones.Local {
124 // The edit would drop these; dropping is removing, under the
125 // rules a removal by the proposer has.
126 for _, e := range reg.PrivateEndpoints(slug) {
127 if e.Registrant != who {
128 panic("zones: leaving local would drop endpoint #" + strconv.FormatInt(e.ID, 10) +
129 ", which somebody else registered; a curator must remove it first")
130 }
131 assertWithdrawable(e)
132 }
133 }
134 }
135 must(reg.Edit(slug, revision, in, who, runtime.ChainHeight(), reason))
136 // The reservation RegisterEndpoint keeps holds across an edit too: a new
137 // main RPC or gnoweb already listed under its kind by a stranger would
138 // make that listing, and its verdict, the zone's own. Checked only for a
139 // URL the edit changed, after the edit, which has validated everything: a
140 // refusal here reverts the edit with it, and an edit that cannot pass never
141 // gets this far. On a curator's edit a stranger's listing nobody ruled on
142 // is dropped, so listing a zone's likely next URL cannot hold its move
143 // off; one a curator ruled on refuses the edit, and a curator removes it
144 // first. A proposer's edit drops nothing that is not theirs.
145 for _, own := range []struct {
146 kind zones.EndpointKind
147 url, was string
148 }{{zones.RPC, in.RPCURL, z.RPCURL}, {zones.Gnoweb, in.GnowebURL, z.GnowebURL}} {
149 if own.url == "" || (own.was != "" && zones.Canonical(own.kind, own.url) == zones.Canonical(own.kind, own.was)) {
150 continue
151 }
152 e, ok := reg.EndpointByAddress(slug, own.kind, own.url)
153 if !ok {
154 continue
155 }
156 if curators.Has(e.Registrant) || (z.Status == zones.Pending && e.Registrant == z.Proposer) {
157 // Theirs to hold, but a flagged one would mark the zone's own URL
158 // the moment it becomes one.
159 if e.Status == zones.Flagged {
160 panic("zones: " + own.url + " is listed under " + string(own.kind) + " as endpoint #" + strconv.FormatInt(e.ID, 10) +
161 ", flagged; a curator removes it or rules again before it becomes " + slug + "'s own")
162 }
163 continue
164 }
165 if !curators.Has(who) {
166 // A proposer's edit (a pending zone) drops nothing that is not
167 // theirs: the only other registrant there is a former curator.
168 panic("zones: " + own.url + " is listed under " + string(own.kind) + " by " + e.Registrant.String() +
169 " (endpoint #" + strconv.FormatInt(e.ID, 10) + "); a curator removes it before it becomes " + slug + "'s own")
170 }
171 if e.ReviewedBy != "" || e.Reason != "" { // a ruling, a reset's reason
172 panic("zones: " + own.url + " is listed under " + string(own.kind) + " by " + e.Registrant.String() +
173 " (endpoint #" + strconv.FormatInt(e.ID, 10) + "), with a curator's ruling; a curator removes it before it becomes " + slug + "'s own")
174 }
175 must(reg.RemoveEndpoint(e.ID, e.Revision))
176 }
177}
178
179// ApproveZone makes a zone official. revision is the zone's Revision as you
180// read it (the zone page's Approve link carries it): if the zone changed since,
181// its content or its status, the approval fails and you read it again. The
182// reason is optional.
183func ApproveZone(cur realm, slug string, revision int64, reason string) {
184 review(cur, slug, zones.Approved, revision, reason)
185}
186
187// RejectZone turns a proposal down. The reason is required, and public.
188// revision is the zone's Revision you read, as for ApproveZone.
189func RejectZone(cur realm, slug string, revision int64, reason string) {
190 review(cur, slug, zones.Rejected, revision, reason)
191}
192
193// RetireZone marks an official zone as no longer running, and sends its
194// verified endpoints back to unverified. The reason is required: it is what an
195// operator still holding the chain id will read. revision is the zone's
196// Revision you read.
197func RetireZone(cur realm, slug string, revision int64, reason string) {
198 review(cur, slug, zones.Retired, revision, reason)
199}
200
201// RemoveZone deletes a pending or rejected zone and its endpoints. revision is
202// the zone's Revision you read: a removal meant for one proposal fails on
203// another proposed again under the same slug. A curator may remove any pending
204// or rejected zone. The proposer may withdraw their own only while it is
205// pending, ReviewWindow blocks after it was proposed or last edited
206// (so withdrawing and proposing again cannot dodge the edit wait), while every
207// endpoint on it is theirs (the deposit is refunded to whoever signs the
208// removal, so removing somebody else's endpoints would collect what they paid)
209// and each one is one they could withdraw on its own (RemoveEndpoint):
210// removing the zone must not wipe a curator's flag or unverify, or skip an endpoint's
211// own wait. A rejected zone is the curators' record: only a curator removes
212// it, or newer rejections push it out, and the rejection's deposit, paid by the
213// curator, is not the proposer's to collect. A zone that was ever official is
214// never removed this way: an approved one is retired, and a retired one is kept
215// until newer retirements push it out.
216func RemoveZone(cur realm, slug string, revision int64) {
217 who := caller(cur)
218 slug = zones.TrimSpaces(slug)
219 z := mustZone(slug)
220 if !curators.Has(who) {
221 if z.Proposer != who || z.Status != zones.Pending {
222 panic("zones: only a curator, or the proposer while it is pending, may remove " + slug)
223 }
224 assertReviewWindow(slug, z)
225 // From the index counts first: nothing is read to answer it.
226 if _, total := reg.Count(slug); reg.OwnerCount(slug, who) != total {
227 panic("zones: " + slug + " carries endpoints somebody else registered; they must be removed first")
228 }
229 // Every endpoint is the proposer's: at most MaxEndpointsPerZone (a curator
230 // who proposed it may have registered past the per-address cap).
231 for _, e := range reg.Endpoints(zones.EndpointFilter{Zone: slug}) {
232 assertWithdrawable(e)
233 }
234 }
235 must(reg.RemoveZone(slug, revision))
236}
237
238// ---- registering and curating endpoints
239
240// RegisterEndpoint lists an endpoint on a zone and returns its id. On an
241// approved zone anybody may, except the zone's own main RPC under rpc and
242// gnoweb under gnoweb, which only a curator lists (or the proposer while the
243// zone is pending: every proposer right ends at approval); on a
244// pending one only the proposer or a curator, so a stranger cannot pin a
245// proposal the proposer then cannot withdraw. It shows as unverified until a
246// curator checks it. A curator also registers past the review queue's caps
247// and into the zones.ReservedForReviewers places a gated registration may
248// not take, never past zones.MaxEndpointsPerZone.
249//
250// kind is rpc, gnoweb, seed, peer, indexer, faucet or explorer. addr is a
251// URL, or <node id>@<host>:<port> for a seed or a peer. label is optional:
252// who runs it, in your words.
253func RegisterEndpoint(cur realm, slug, kind, addr, label string) int64 {
254 who := caller(cur)
255 slug = zones.TrimSpaces(slug)
256 z := mustZone(slug)
257 if z.Status == zones.Pending && z.Proposer != who && !curators.Has(who) {
258 panic("zones: " + slug + " is pending; only its proposer or a curator may register on it")
259 }
260 k, err := zones.ParseEndpointKind(kind)
261 must(err)
262 addr = zones.TrimSpaces(addr) // compared as the registry will store it
263 // The zone's own main RPC and gnoweb, listed under their own kind, carry
264 // the verdict its page shows on them, so only a curator lists them, or the
265 // proposer while the zone is pending (every proposer right ends at
266 // approval): a stranger's listing, once flagged for its label, would mark
267 // the zone's own URL.
268 if !curators.Has(who) && !(z.Status == zones.Pending && z.Proposer == who) &&
269 ((k == zones.RPC && zones.Canonical(k, addr) == zones.Canonical(k, z.RPCURL)) ||
270 (k == zones.Gnoweb && z.GnowebURL != "" && zones.Canonical(k, addr) == zones.Canonical(k, z.GnowebURL))) {
271 panic("zones: that is " + slug + "'s own " + string(k) + "; only a curator, or its proposer while it is pending, lists it")
272 }
273 register := reg.Register
274 if curators.Has(who) {
275 register = reg.RegisterExempt // past the review queue, as ProposeZone
276 }
277 id, err := register(who, runtime.ChainHeight(), slug, k, addr, label)
278 must(err)
279 return id
280}
281
282// VerifyEndpoint marks an endpoint as checked against its zone. revision is
283// the endpoint's revision as read (the endpoint table's revision column), and
284// zoneRevision the zone's (the zone page shows it): the verdict fails if
285// either changed since, another curator's verdict on the endpoint, or any
286// edit or status change of the zone (what is verified is that it answers for
287// this zone's chain id). The reason is optional. Each verdict may be given
288// again with a new reason, to restate it.
289func VerifyEndpoint(cur realm, id, zoneRevision, revision int64, reason string) {
290 reviewEndpoint(cur, id, zones.Verified, zoneRevision, revision, reason)
291}
292
293// FlagEndpoint tells readers not to use an endpoint. The reason is required.
294// revision is the endpoint's, as for VerifyEndpoint; a flag is not bound to
295// the zone, so editing the zone cannot hold off a warning.
296func FlagEndpoint(cur realm, id, revision int64, reason string) {
297 reviewEndpoint(cur, id, zones.Flagged, 0, revision, reason)
298}
299
300// UnverifyEndpoint puts an endpoint back to unverified, for one that changed
301// hands or needs checking again. revision is as for FlagEndpoint.
302func UnverifyEndpoint(cur realm, id, revision int64, reason string) {
303 reviewEndpoint(cur, id, zones.Unverified, 0, revision, reason)
304}
305
306// RemoveEndpoint deletes an endpoint. revision is its revision as read: the
307// removal fails if a verdict landed since, rather than delete one nobody saw. A
308// curator may remove any. Its registrant may remove it unless a curator flagged
309// or unverified it (a flag is a warning, an unverify carries why, and removing
310// and registering it again would wipe either; a verified one the registrant may
311// take down, since listing it again starts it unverified, and so one a reset
312// sent back to unverified, since the reset says the zone changed, not it), only while its zone is pending or approved (a rejected
313// or retired zone is a record, endpoints and all), and only ReviewWindow blocks
314// after registering it (removing and registering again every block would give
315// it a new id faster than a curator could flag the old one).
316func RemoveEndpoint(cur realm, id, revision int64) {
317 who := caller(cur)
318 e, ok := reg.Endpoint(id)
319 if !ok {
320 panic("zones: no endpoint #" + strconv.FormatInt(id, 10))
321 }
322 if !curators.Has(who) {
323 if e.Registrant != who {
324 panic("zones: only a curator or its registrant may remove endpoint #" + strconv.FormatInt(id, 10))
325 }
326 if z := mustZone(e.Zone); z.Status != zones.Pending && z.Status != zones.Approved {
327 panic("zones: " + z.Slug + " is " + string(z.Status) + ", a record; only a curator may remove its endpoints")
328 }
329 assertWithdrawable(e)
330 }
331 must(reg.RemoveEndpoint(id, revision))
332}
333
334// ClearUnreviewed removes, in one call, every endpoint on a zone that is
335// zones.Endpoint.Clearable (nobody ruled on it, and no reviewer registered it
336// past the caps), and returns how many. There are never more than
337// zones.MaxUnverifiedPerZone: clearable endpoints are a part of the review
338// queue its gate holds there, and no reset makes one. It is the answer to a
339// flood that cycles: registrants who withdraw and register again each review
340// window keep the queue full, and one removal per transaction lets them refill
341// it before a curator is done. throughRevision bounds it to what the curator
342// read: an endpoint registered after it (a later Revision) is kept. The
343// deposits go to the curator who signs. At most zones.MaxEndpointsPerZone are
344// read.
345func ClearUnreviewed(cur realm, slug string, throughRevision int64) int {
346 who := caller(cur)
347 assertCurator(who)
348 slug = zones.TrimSpaces(slug)
349 mustZone(slug)
350 n := 0
351 for _, e := range reg.Endpoints(zones.EndpointFilter{Zone: slug, Status: zones.Unverified}) {
352 if e.Clearable() && e.Revision <= throughRevision {
353 must(reg.RemoveEndpoint(e.ID, e.Revision))
354 n++
355 }
356 }
357 return n
358}
359
360// assertWithdrawable refuses a non-curator's withdrawal of an endpoint a
361// curator flagged or unverified, or one registered less than ReviewWindow
362// blocks ago. Shared by RemoveEndpoint, the proposer's RemoveZone and the
363// proposer's edit off local, so none can do what another refuses.
364func assertWithdrawable(e zones.Endpoint) {
365 id := strconv.FormatInt(e.ID, 10)
366 // A verification guards nothing a withdrawal could erase: the registrant
367 // takes down a node that is going away, and listing it again starts it
368 // unverified. A flag, or an unverify a curator wrote (a reviewer or a
369 // reason), is a warning, and removing and registering it again would wipe
370 // it. A reset is not: it reaches only a verified endpoint and says the zone
371 // changed, not the endpoint, so it leaves the withdrawal a verified one had.
372 if e.Status == zones.Flagged || (e.Status == zones.Unverified && (e.ReviewedBy != "" || e.Reason != "") && !zones.IsReset(e)) {
373 panic("zones: a curator ruled on endpoint #" + id + "; only a curator may remove it now")
374 }
375 if wait := e.RegisteredAt + ReviewWindow - runtime.ChainHeight(); wait > 0 {
376 panic("zones: endpoint #" + id + " was registered at block " +
377 strconv.FormatInt(e.RegisteredAt, 10) + "; it may be withdrawn in " +
378 strconv.FormatInt(wait, 10) + " blocks, so curators can review it")
379 }
380}
381
382// ---- curators
383
384// AddCurator invites another address to curate. Only a curator may. The
385// address becomes a curator when it calls AcceptCurator itself.
386//
387// Two steps, deliberately: a curator set is the one thing a mistake here can
388// lose for good. With a one-step add, a sole curator who invites a mistyped
389// address and then steps down leaves a registry nobody controls; accepting is
390// the proof that somebody holds the key. An invitation dies with its inviter:
391// removing a curator withdraws every invitation they sent.
392func AddCurator(cur realm, addr address) {
393 who := caller(cur)
394 assertCurator(who)
395 if !zones.ValidAddress(addr) {
396 panic("zones: not a valid lowercase address: " + addr.String())
397 }
398 if curators.Has(addr) {
399 panic("zones: " + addr.String() + " is already a curator")
400 }
401 if invited.Has(addr) {
402 panic("zones: " + addr.String() + " is already invited")
403 }
404 if curators.Size()+invited.Size() >= MaxCurators {
405 panic("zones: " + strconv.Itoa(MaxCurators) + " curators and invitations already; remove one first")
406 }
407 invited.Add(addr)
408 inviter[addr] = who
409}
410
411// AcceptCurator makes the caller a curator, if a curator invited it.
412func AcceptCurator(cur realm) {
413 who := caller(cur)
414 if !invited.Has(who) {
415 panic("zones: " + who.String() + " has no curator invitation")
416 }
417 invited.Remove(who)
418 delete(inviter, who)
419 curators.Add(who)
420}
421
422// RemoveCurator revokes a curator, along with every invitation they sent, or
423// withdraws one invitation. Only a curator may, and the last curator cannot be
424// removed: a registry nobody can curate can never retire a dead zone.
425//
426// Curators are equals: any one may remove any other, the admin included. That
427// is the trust a curator set is, and it is why there are few of them.
428func RemoveCurator(cur realm, addr address) {
429 assertCurator(caller(cur))
430 if invited.Remove(addr) {
431 delete(inviter, addr)
432 return
433 }
434 if !curators.Has(addr) {
435 panic("zones: " + addr.String() + " is not a curator")
436 }
437 if curators.Size() == 1 {
438 panic("zones: " + addr.String() + " is the last curator")
439 }
440 curators.Remove(addr)
441 for _, a := range Invited() {
442 if inviter[a] == addr {
443 invited.Remove(a)
444 delete(inviter, a)
445 }
446 }
447}
448
449// ---- reads: plain arguments, so `gnokey query vm/qeval` can call them
450
451// GetZone returns the zone under slug, and whether there is one.
452func GetZone(slug string) (zones.Zone, bool) {
453 return reg.Zone(zones.TrimSpaces(slug))
454}
455
456// ListZones returns the zones with that status and kind, in the order they
457// were proposed. "" matches any; "approved" is the official list.
458func ListZones(status, kind string) []zones.Zone {
459 st, err := zones.ParseStatus(status)
460 must(err)
461 k, err := zones.ParseKind(kind)
462 must(err)
463 return reg.Zones(zones.ZoneFilter{Status: st, Kind: k})
464}
465
466// GetEndpoint returns the endpoint with that id, and whether there is one.
467func GetEndpoint(id int64) (zones.Endpoint, bool) {
468 return reg.Endpoint(id)
469}
470
471// ListEndpoints returns a zone's endpoints of that kind and verification,
472// oldest first. "" matches any kind or verdict, so ("onyx", "", "") is
473// everything on onyx and ("onyx", "peer", "verified") is what a cautious node
474// should dial. The zone is required: one zone is at most MaxEndpointsPerZone
475// rows, every zone together is a response no node should be asked for.
476func ListEndpoints(slug, kind, status string) []zones.Endpoint {
477 return reg.Endpoints(endpointFilter(slug, kind, status))
478}
479
480// ListAddresses is ListEndpoints reduced to the addresses, which is what a
481// config file wants: ListAddresses("onyx", "peer", "verified").
482func ListAddresses(slug, kind, status string) []string {
483 es := reg.Endpoints(endpointFilter(slug, kind, status))
484 out := make([]string, 0, len(es))
485 for _, e := range es {
486 out = append(out, e.Address)
487 }
488 return out
489}
490
491// IsInvited reports whether addr holds a curator invitation it has not
492// accepted yet.
493func IsInvited(addr address) bool { return invited.Has(addr) }
494
495// IsCurator reports whether addr may curate.
496func IsCurator(addr address) bool { return curators.Has(addr) }
497
498// Curators returns every curator, in address order.
499func Curators() []address { return members(&curators) }
500
501// Invited returns every open curator invitation, in address order.
502func Invited() []address { return members(&invited) }
503
504func members(set *addrset.Set) []address {
505 out := make([]address, 0, set.Size())
506 set.IterateByOffset(0, set.Size(), func(a address) bool {
507 out = append(out, a)
508 return false
509 })
510 return out
511}
512
513// ---- helpers
514
515func review(cur realm, slug string, to zones.Status, revision int64, reason string) {
516 who := caller(cur)
517 assertCurator(who)
518 must(reg.ReviewZone(zones.TrimSpaces(slug), to, revision, who, runtime.ChainHeight(), reason))
519}
520
521func reviewEndpoint(cur realm, id int64, to zones.Verification, zoneRevision, revision int64, reason string) {
522 who := caller(cur)
523 assertCurator(who)
524 must(reg.ReviewEndpoint(id, to, zoneRevision, revision, who, runtime.ChainHeight(), reason))
525}
526
527func endpointFilter(slug, kind, status string) zones.EndpointFilter {
528 slug = zones.TrimSpaces(slug)
529 if slug == "" {
530 panic("zones: name a zone; ListZones lists them")
531 }
532 k, err := zones.ParseEndpointKind(kind)
533 must(err)
534 v, err := zones.ParseVerification(status)
535 must(err)
536 return zones.EndpointFilter{Zone: slug, Kind: k, Status: v}
537}
538
539func info(chainID, title, description, kind, gnowebURL, rpcURL, genesisURL string) zones.Info {
540 k, err := zones.ParseKind(kind)
541 must(err)
542 return zones.Info{
543 ChainID: zones.TrimSpaces(chainID),
544 Title: zones.TrimSpaces(title),
545 Description: zones.TrimSpaces(description),
546 Kind: k,
547 GnowebURL: zones.TrimSpaces(gnowebURL),
548 RPCURL: zones.TrimSpaces(rpcURL),
549 GenesisURL: zones.TrimSpaces(genesisURL),
550 }
551}
552
553// caller is the one place this realm decides who is acting: the realm token is
554// checked before it is walked, because an unchecked token is not a caller.
555func caller(cur realm) address {
556 if !cur.IsCurrent() {
557 panic("zones: spoofed realm")
558 }
559 return cur.Previous().Address()
560}
561
562// assertReviewWindow refuses a proposer's edit or withdrawal of a pending zone
563// sooner than ReviewWindow blocks after it was proposed or last edited,
564// by anybody.
565func assertReviewWindow(slug string, z zones.Zone) {
566 last := z.ProposedAt
567 if z.EditedAt > last {
568 last = z.EditedAt
569 }
570 if wait := last + ReviewWindow - runtime.ChainHeight(); wait > 0 {
571 panic("zones: " + slug + " was changed at block " + strconv.FormatInt(last, 10) +
572 "; its proposer may act on it again in " + strconv.FormatInt(wait, 10) + " blocks, so curators can review it")
573 }
574}
575
576func assertCurator(who address) {
577 if !curators.Has(who) {
578 panic("zones: " + who.String() + " is not a curator")
579 }
580}
581
582func mustZone(slug string) zones.Zone {
583 z, ok := reg.Zone(slug)
584 if !ok {
585 panic("zones: no zone " + strconv.Quote(slug))
586 }
587 return z
588}
589
590func must(err error) {
591 if err != nil {
592 panic(err.Error())
593 }
594}