registry.gno
45.67 Kb · 1220 lines
1package zones
2
3import (
4 "crypto/sha256"
5 "errors"
6 "strconv"
7
8 "gno.land/p/moul/kit/index/v0"
9 "gno.land/p/moul/kit/store/v0"
10 "gno.land/p/nt/bptree/v0"
11)
12
13// Bounds on what the registry holds.
14//
15// The LIVE registry, pending and approved zones, has a hard cap. Rejected and
16// retired zones are kept for the record but do not count against it: each has
17// a cap of its own, and when it is reached the zone that has been in that state
18// longest is dropped to make room (by when it entered the state, not when it
19// was proposed, so a long-lived network retired today is not the next to go).
20// So nothing a proposer or a curator does, and no amount of time, can fill the
21// registry for good: a cap that only ever fills is a lifetime cap, and on an
22// immutable path that is a brick.
23//
24// The per-address caps make one address cheap to ignore. Neither stops a flood
25// from many addresses, because an address is not an identity, so admission to
26// the review queue has a gate of its own (MaxPending zones, MaxUnverifiedPerZone
27// endpoints awaiting a verdict), well under the hard caps: a flood fills the
28// queue and never crowds out an approved zone or a verified endpoint. A
29// flagged endpoint has its verdict and leaves the queue, so curators keep a
30// warning instead of having to delete it to make room. The gate is checked
31// where something enters (Propose, Register); a review or a reset can push a
32// count past it, and never fails for it. ProposeExempt and RegisterExempt skip
33// it, and the per-address caps, for the reviewers a holder trusts: a full
34// queue must not lock out the people who clear it. The queue clears through
35// approval, verification, a flag, rejection, RemoveZone, RemoveEndpoint, an
36// edit leaving the local kind, and eviction. Flagged endpoints still count
37// toward MaxEndpointsPerZone: a flood a curator flags rather than removes can
38// fill the places a gated registration may use (all but the last
39// ReservedForReviewers), and removing it is the answer. Each entry costs its sender a
40// storage deposit, refunded to whoever signs the transaction that
41// frees it (on a chain where ugnot is not transfer-locked; on one that is, the
42// refund goes to the storage fee collector), so a flooder who withdraws first
43// gets it back: a bond, not a fee.
44const (
45 MaxZones = 256 // pending and approved zones together
46 MaxPending = 64 // zones awaiting review, all proposers together
47 MaxPendingPerProposer = 4 // open proposals one address may have at once
48 MaxRejected = 64 // rejected zones kept for the record
49 MaxRetired = 128 // retired zones kept for the record
50 MaxEndpointsPerZone = 128
51 MaxUnverifiedPerZone = 64 // endpoints on one zone still waiting for a verdict (flagged ones have theirs)
52 MaxEndpointsPerAddress = 16 // endpoints one address may register on one zone
53 // MaxDropPerEdit bounds how many endpoints one edit off the local kind
54 // drops, and so its gas: a removal rewrites up to about 45 later values in
55 // two B+ tree leaves, and a caller can lay ids out so every removal does,
56 // which at 128 measured 2.6B, close to a 3B block, and at 64 measures
57 // 2.02B even with a chain-id reset spread across leaves and both own URLs
58 // moved in the same edit. More waits for removals.
59 MaxDropPerEdit = 64
60 // ReservedForReviewers is how much of each hard cap (MaxZones live,
61 // MaxEndpointsPerZone) only the exempt calls may use: strangers who keep
62 // a cap full refill it the block after a curator clears it, and a cap a
63 // curator cannot reach is one they cannot act under.
64 ReservedForReviewers = 16
65)
66
67// The reasons an endpoint carries when its zone changed under it and sent it
68// back to unverified. A reset caused by an edit to a pending zone by its own
69// proposer records nobody, because that is not a review: ReviewedBy is empty
70// and Reason says why. Every other reset (a rejection, a retirement, anybody
71// else's edit, any edit to an approved zone) records whoever caused it.
72const (
73 ChainIDChanged = "the zone's chain id changed; verify again"
74 ZoneRetired = "the zone was retired; verify again if it returns"
75 ZoneRejected = "the zone was rejected; verify again if it is approved"
76)
77
78// IsReset reports whether an unverified endpoint is unverified only because its
79// zone changed under it: its reason is one of the three above. A reset reaches
80// only a verified endpoint, so it says nothing against the endpoint itself.
81func IsReset(e Endpoint) bool {
82 return e.Status == Unverified && (e.Reason == ChainIDChanged || e.Reason == ZoneRetired || e.Reason == ZoneRejected)
83}
84
85// sequences are the registry-wide counters: the last Revision handed out (so
86// none is reused) and the last status-entry sequence (the eviction order).
87type sequences struct{ rev, seq uint64 }
88
89// fanout is the B+ tree fanout for every container this package builds itself:
90// 32, not the 128 EFFECTIVE_GNO.md measured cheapest in memory. Every value in
91// a B+ tree leaf is a boxed object of its own, and removing (or inserting) a
92// key shifts every later value in the leaf, each shift a store write. Measured
93// on a node, one transaction per step (gno master 3cc494ec4): removing the
94// first key of a fanout-128 leaf filled with 120 cost 15.8M gas, the last 5.1M,
95// about 90k gas per entry shifted for a scalar value (the counters, the unique
96// ids) and about 230k for a pointer (the zone and endpoint tables, kit/index);
97// at fanout 32 each costs the same per shift, on a leaf a quarter the size. Endpoints are removed and their dedup keys inserted at
98// arbitrary positions, so the smaller leaf is worth its larger node overhead
99// (671 B per entry against 592).
100const fanout = 32
101
102// Registry holds the zones and their endpoints. The zero value is not usable:
103// call [NewRegistry]. All its state is behind pointers, so a copy of the value
104// is the same registry, not a second one sharing half of it.
105//
106// Every write touches its record and all of that record's indexes in one
107// method. The index writes cannot fail as written: every key is validated
108// non-empty and every unique key is checked free before the first write. If a
109// later change made one fail, the method returns the error and the holding
110// realm's abort undoes the half-applied write; that abort, not this method, is
111// the atomicity guarantee.
112type Registry struct {
113 // The two sequences live behind a pointer like every container here, so a
114 // copied Registry value shares them: with them inline, r2 := *r1 would
115 // share every zone but count revisions on its own, and hand out a revision
116 // r1 had already used, which a stale decision would then pass.
117 ids *sequences
118
119 zones *table
120 bySlug *unique // slug -> zone id
121 byStatus *index.Index // status -> zone ids, so a page reads only its rows
122 entered *unique // status|entry sequence -> zone id: who has been in a state longest
123 byProposer *counter // proposer -> how many PENDING zones they have
124 approved *index.Index // chain id -> ids of APPROVED zones naming it
125
126 endpoints *table
127 byAddress *unique // slug|kind|hash(canonical address) -> endpoint id
128 byZone *index.Index // slug -> endpoint ids
129 byKind *index.Index // slug|kind -> endpoint ids, so a page reads only its rows
130 byOwner *counter // slug|registrant -> how many endpoints they registered
131 unchecked *index.Index // slug -> ids of endpoints that are not Verified
132 flagged *counter // slug -> how many of those are Flagged
133 fresh *counter // slug -> how many endpoints are Clearable
134 private *index.Index // slug -> ids of endpoints on a private host (a local zone's only)
135}
136
137// NewRegistry returns an empty registry.
138func NewRegistry() *Registry {
139 return &Registry{
140 ids: &sequences{},
141 zones: newTable(),
142 bySlug: newUnique(),
143 byStatus: index.New(),
144 entered: newUnique(),
145 byProposer: newCounter(),
146 approved: index.New(),
147 endpoints: newTable(),
148 byAddress: newUnique(),
149 byZone: index.New(),
150 byKind: index.New(),
151 byOwner: newCounter(),
152 flagged: newCounter(),
153 fresh: newCounter(),
154 unchecked: index.New(),
155 private: index.New(),
156 }
157}
158
159// Propose files a new zone, Pending, under a slug nobody holds.
160func (r *Registry) Propose(by address, at int64, slug string, in Info) error {
161 return r.propose(by, at, slug, in, true)
162}
163
164// ProposeExempt is Propose without the admission gates (MaxPending and
165// MaxPendingPerProposer), for the reviewers a holding realm trusts: a flood
166// that fills the queue would otherwise lock out the people who clear it. It
167// also takes the last ReservedForReviewers places of the live registry, which
168// Propose may not; MaxZones itself still holds.
169func (r *Registry) ProposeExempt(by address, at int64, slug string, in Info) error {
170 return r.propose(by, at, slug, in, false)
171}
172
173func (r *Registry) propose(by address, at int64, slug string, in Info, gated bool) error {
174 in = trimInfo(in)
175 if err := ValidateSlug(slug); err != nil {
176 return err
177 }
178 if err := ValidateInfo(in); err != nil {
179 return err
180 }
181 if !ValidAddress(by) {
182 return errors.New("zones: the proposer is not a valid lowercase address")
183 }
184 if r.bySlug.has(slug) {
185 return errors.New("zones: the slug " + strconv.Quote(slug) + " is taken")
186 }
187 if r.Live() >= MaxZones {
188 return errors.New("zones: the registry is full at " + strconv.Itoa(MaxZones) + " pending and approved zones")
189 }
190 if gated && r.Live() >= MaxZones-ReservedForReviewers {
191 return errors.New("zones: the registry's last " + strconv.Itoa(ReservedForReviewers) + " places are kept for curators")
192 }
193 if gated && r.byStatus.Count(string(Pending)) >= MaxPending {
194 return errors.New("zones: " + strconv.Itoa(MaxPending) + " proposals are already waiting for review; try again once a curator has cleared some")
195 }
196 if n := r.byProposer.count(by.String()); gated && n >= MaxPendingPerProposer {
197 return errors.New("zones: " + by.String() + " already has " + strconv.Itoa(n) +
198 " pending proposals, the limit is " + strconv.Itoa(MaxPendingPerProposer))
199 }
200 z := &Zone{Slug: slug, Proposer: by, ProposedAt: at}
201 z.setInfo(in)
202 id := r.zones.add(z)
203 r.bySlug.set(slug, id)
204 r.byProposer.inc(by.String())
205 return r.enter(z, id, Pending)
206}
207
208// enter files a zone under a status, and bumps its Revision: a decision
209// prepared against the old status must fail too, or an approval opened before
210// a colleague's rejection would quietly reverse it. Only the two states that
211// evict read the entry order, so only they write it. The caller has already
212// taken the zone out of its old status.
213func (r *Registry) enter(z *Zone, id store.ID, to Status) error {
214 r.ids.seq++
215 r.ids.rev++
216 z.Status, z.Entered, z.Revision = to, int64(r.ids.seq), int64(r.ids.rev)
217 if evicts(to) {
218 r.entered.set(enteredKey(to, z.Entered), id)
219 }
220 return r.byStatus.Add(string(to), id)
221}
222
223func evicts(st Status) bool { return st == Rejected || st == Retired }
224
225// leave takes a zone out of its current status.
226func (r *Registry) leave(z *Zone, id store.ID) {
227 r.byStatus.Remove(string(z.Status), id)
228 if evicts(z.Status) {
229 r.entered.remove(enteredKey(z.Status, z.Entered))
230 }
231 if z.Status == Pending {
232 r.byProposer.dec(z.Proposer.String())
233 }
234 if z.Status == Approved {
235 r.approved.Remove(z.ChainID, id)
236 }
237}
238
239func enteredKey(st Status, seq int64) string { return string(st) + "|" + store.ID(seq).Key() }
240
241// Edit replaces a zone's Info: every field but the slug and the status.
242//
243// Only a pending or an approved zone can be edited. A rejected one is removed
244// and proposed again, or approved first; a retired one is a record, and its
245// retirement reason is the thing it is kept for.
246//
247// Every edit bumps the zone's Revision and records who made it and when. On a
248// pending zone that is all, and a reason is refused rather than silently
249// dropped: nobody has reviewed anything yet. On an approved zone an edit is a
250// curator decision of its own: the reviewed values changed, so the review on
251// record is replaced by this one, with a reason required.
252//
253// When the chain id changes, whatever the status, every endpoint verified
254// against the old one goes back to unverified: what was verified was that it
255// answered for a chain this zone no longer names. A reset caused by the
256// proposer's own edit to a pending zone records no reviewer; any other records
257// the editor. An edit that changes nothing is refused, and leaving the local
258// kind removes every endpoint on a private or special-use host (IsPrivateHost),
259// at most MaxDropPerEdit in one edit, and is refused while one of them carries
260// a curator's ruling.
261//
262// An edit names the revision it was written against, like an approval does, so
263// two editors working from the same reading cannot silently undo each other.
264func (r *Registry) Edit(slug string, revision int64, in Info, by address, at int64, reason string) error {
265 z, zid, err := r.zone(slug)
266 if err != nil {
267 return err
268 }
269 if err := stale(z, revision); err != nil {
270 return err
271 }
272 if z.Status != Pending && z.Status != Approved {
273 return errors.New("zones: " + slug + " is " + string(z.Status) + "; only a pending or approved zone can be edited")
274 }
275 in = trimInfo(in)
276 if err := ValidateInfo(in); err != nil {
277 return err
278 }
279 if !ValidAddress(by) {
280 return errors.New("zones: the editor is not a valid lowercase address")
281 }
282 reason = TrimSpaces(reason)
283 if err := ValidateReason(reason); err != nil {
284 return err
285 }
286 switch {
287 case z.Status == Pending && reason != "":
288 return errors.New("zones: " + slug + " is pending, and an edit before review takes no reason")
289 case z.Status == Approved && !HasVisible(reason):
290 return errors.New("zones: " + slug + " is approved, so an edit needs a reason")
291 }
292 // An edit that changes nothing would still bump the revision, so refusing
293 // it is what stops a revision being bumped for its own sake.
294 if in == z.Info() {
295 return errors.New("zones: that edit changes nothing on " + slug)
296 }
297 // Leaving the local kind drops the private hosts only a local zone may
298 // list, whatever their verdict: they name nothing on the zone it becomes.
299 // Dropped in the edit itself, not refused until somebody removes them,
300 // because anybody may register one again between that removal and this
301 // edit. Only those endpoints are read, from their own index. One a
302 // curator ruled on (a verdict, or a reset that left a reason) is a
303 // record, maybe one the editor never saw: the edit fails while one is
304 // listed, and a curator removes it first, naming its revision. Only a
305 // curator can rule, so nobody else can block the edit this way.
306 if z.Kind == Local && in.Kind != Local {
307 drop := r.private.Lookup(slug)
308 if len(drop) > MaxDropPerEdit {
309 return errors.New("zones: leaving local would drop " + strconv.Itoa(len(drop)) + " endpoints, more than " +
310 strconv.Itoa(MaxDropPerEdit) + " in one edit; remove some first")
311 }
312 for _, eid := range drop {
313 v, _ := r.endpoints.get(eid)
314 // Every verdict names its reviewer and a reset leaves a reason, so
315 // these two say "ruled on".
316 if e := v.(*Endpoint); e.ReviewedBy != "" || e.Reason != "" {
317 who := "a curator ruled on; a curator must remove it first"
318 if e.Status == Verified {
319 who = "is verified; its registrant or a curator must remove it first"
320 }
321 return errors.New("zones: leaving local would drop endpoint #" + strconv.FormatInt(e.ID, 10) + ", which " + who)
322 }
323 }
324 for _, eid := range drop {
325 r.removeEndpoint(eid)
326 }
327 }
328 if in.ChainID != z.ChainID {
329 // The proposer is not a reviewer; anybody else editing (a curator, on a
330 // pending zone or an approved one) is, and is named on the reset.
331 resetBy := address("")
332 if z.Status == Approved || by != z.Proposer {
333 resetBy = by
334 }
335 if z.Status == Approved {
336 r.approved.Remove(z.ChainID, zid)
337 if err := r.approved.Add(in.ChainID, zid); err != nil {
338 return err
339 }
340 }
341 if err := r.unverifyAll(slug, resetBy, at, ChainIDChanged); err != nil {
342 return err
343 }
344 }
345 if z.Status == Approved {
346 z.ReviewedBy, z.ReviewedAt, z.Reason = by, at, reason
347 }
348 r.ids.rev++
349 z.Revision = int64(r.ids.rev)
350 z.EditedBy, z.EditedAt = by, at
351 z.setInfo(in)
352 return nil
353}
354
355// ReviewZone records a curator's decision on a zone.
356//
357// The transitions are the curation policy, and they are deliberately few:
358//
359// approve from pending, rejected or retired reason optional
360// reject from pending reason REQUIRED
361// retire from approved reason REQUIRED
362//
363// and a decision restated: the zone's present status again, with a new
364// visible reason, which records the new reviewer and bumps the revision and
365// does nothing else (no new entry in the state, no eviction, no reset). It is
366// the only way to correct a reason.
367//
368// Every decision names the zone's Revision the curator read, and fails if the
369// zone changed since: otherwise a proposer's edit landing just before an
370// approval would become official under the curator's name, and a rejection's
371// public reason would describe text the curator never saw.
372//
373// Rejecting or retiring sends every verified endpoint back to unverified: a
374// rejected zone was never vouched for, and a retired network's peers may be
375// somebody else's hosts by the time anyone reads them. Each state keeps at most
376// MaxRejected or MaxRetired zones; the next one in drops the zone that entered
377// that state first, with its endpoints. Every transition bumps the zone's
378// Revision, so a decision prepared against the old status fails.
379//
380// Nothing goes back to pending: a rejected zone is approved after all, removed,
381// or pushed out by newer rejections. An official zone is never rejected
382// after the fact, it is retired, so the record of it having been official
383// survives.
384func (r *Registry) ReviewZone(slug string, to Status, revision int64, by address, at int64, reason string) error {
385 z, id, err := r.zone(slug)
386 if err != nil {
387 return err
388 }
389 if !ValidAddress(by) {
390 return errors.New("zones: the reviewer is not a valid lowercase address")
391 }
392 reason = TrimSpaces(reason)
393 if err := ValidateReason(reason); err != nil {
394 return err
395 }
396 if err := stale(z, revision); err != nil {
397 return err
398 }
399 if z.Status == to {
400 // A decision is restated with a new reason, the only way to correct
401 // one: a rejected or retired zone is not editable, and an edit to an
402 // approved zone must change something besides its reason.
403 if (to == Approved || to == Rejected || to == Retired) && HasVisible(reason) && reason != z.Reason {
404 r.ids.rev++
405 z.Revision = int64(r.ids.rev)
406 z.ReviewedBy, z.ReviewedAt, z.Reason = by, at, reason
407 return nil
408 }
409 return errors.New("zones: " + slug + " is already " + string(to))
410 }
411 switch to {
412 case Approved:
413 if z.Status != Pending && r.Live() >= MaxZones {
414 return errors.New("zones: the registry is full at " + strconv.Itoa(MaxZones) + " pending and approved zones")
415 }
416 case Rejected:
417 if z.Status == Approved {
418 return errors.New("zones: " + slug + " is approved; retire it instead of rejecting it")
419 }
420 if z.Status != Pending {
421 return errors.New("zones: only a pending zone can be rejected; " + slug + " is " + string(z.Status))
422 }
423 case Retired:
424 if z.Status != Approved {
425 return errors.New("zones: only an approved zone can be retired; " + slug + " is " + string(z.Status))
426 }
427 default:
428 return errors.New("zones: a review cannot set a zone to " + strconv.Quote(string(to)))
429 }
430 if (to == Rejected || to == Retired) && !HasVisible(reason) {
431 return errors.New("zones: " + string(to) + " needs a reason")
432 }
433 switch to {
434 case Rejected:
435 if err := r.unverifyAll(slug, by, at, ZoneRejected); err != nil {
436 return err
437 }
438 r.makeRoom(Rejected, MaxRejected)
439 case Retired:
440 if err := r.unverifyAll(slug, by, at, ZoneRetired); err != nil {
441 return err
442 }
443 r.makeRoom(Retired, MaxRetired)
444 }
445 r.leave(z, id)
446 if to == Approved {
447 if err := r.approved.Add(z.ChainID, id); err != nil {
448 return err
449 }
450 }
451 if err := r.enter(z, id, to); err != nil {
452 return err
453 }
454 z.ReviewedBy, z.ReviewedAt, z.Reason = by, at, reason
455 return nil
456}
457
458// stale refuses a decision written against a revision the zone has moved past.
459func stale(z *Zone, revision int64) error {
460 if revision != z.Revision {
461 return errors.New("zones: " + z.Slug + " changed since you read it: it is at revision " +
462 strconv.FormatInt(z.Revision, 10) + ", you acted on revision " + strconv.FormatInt(revision, 10) +
463 "; read it again")
464 }
465 return nil
466}
467
468// makeRoom drops the zone that has been in that state longest, when the state
469// is at its cap, so the one about to enter fits.
470func (r *Registry) makeRoom(status Status, max int) {
471 for r.byStatus.Count(string(status)) >= max {
472 var oldest store.ID
473 r.entered.tree.Iterate(string(status)+"|", string(status)+"}", func(_ string, v any) bool {
474 oldest = v.(store.ID)
475 return true
476 })
477 v, _ := r.zones.get(oldest)
478 r.removeZone(v.(*Zone), oldest)
479 }
480}
481
482// RemoveZone deletes a pending or rejected zone and every endpoint registered
483// on it, freeing their storage deposit to whoever signs the removal. It names
484// the revision it was decided on, so a removal meant for one proposal cannot
485// land on another proposed again under the same slug. A zone that has ever
486// been official cannot be removed this way: an approved one is retired, and a
487// retired one is kept until MaxRetired newer retirements push it out, so
488// whoever still holds its chain id can find out what happened to it.
489func (r *Registry) RemoveZone(slug string, revision int64) error {
490 z, id, err := r.zone(slug)
491 if err != nil {
492 return err
493 }
494 if err := stale(z, revision); err != nil {
495 return err
496 }
497 if z.Status == Approved {
498 return errors.New("zones: " + slug + " is approved; retire it instead of removing it")
499 }
500 if z.Status == Retired {
501 return errors.New("zones: " + slug + " is retired, and a retired zone is kept on record")
502 }
503 r.removeZone(z, id)
504 return nil
505}
506
507// removeZone unwinds a zone, its endpoints and every index. The per-zone index
508// keys go in one RemoveKey each rather than an id at a time, which would copy
509// the bucket once per endpoint.
510func (r *Registry) removeZone(z *Zone, id store.ID) {
511 slug := z.Slug
512 for _, eid := range r.byZone.Lookup(slug) {
513 v, ok := r.endpoints.remove(eid)
514 if !ok {
515 continue
516 }
517 r.byOwner.dec(ownerKey(slug, v.(*Endpoint).Registrant))
518 }
519 // The zone's dedup keys are one contiguous range (a slug is [a-z0-9-], all
520 // below "|"), so they are dropped without hashing each address again.
521 keys := []string{}
522 r.byAddress.tree.Iterate(slug+"|", slug+"}", func(k string, _ any) bool {
523 keys = append(keys, k)
524 return false
525 })
526 for _, k := range keys {
527 r.byAddress.remove(k)
528 }
529 r.flagged.drop(slug)
530 r.fresh.drop(slug)
531 r.byZone.RemoveKey(slug)
532 r.unchecked.RemoveKey(slug)
533 r.private.RemoveKey(slug)
534 for _, k := range EndpointKinds() {
535 r.byKind.RemoveKey(kindKey(slug, k))
536 }
537 r.leave(z, id)
538 r.bySlug.remove(slug)
539 r.zones.remove(id)
540}
541
542// Register adds an endpoint to a zone, Unverified, and returns its id.
543//
544// A zone takes endpoints while it is pending or approved, and a rejected or
545// retired zone takes none. Who may register on a pending zone is the holding
546// realm's call; a URL is stored with its scheme and host lowercased and
547// without an empty tail (trimEmptyTail), a peer lowercased whole without its
548// host's terminal dot, so what is listed is what dials (Canonical, which also drops default ports and reads tcp as http, is
549// what they are compared in, not what is stored).
550func (r *Registry) Register(by address, at int64, slug string, kind EndpointKind, addr, label string) (int64, error) {
551 return r.register(by, at, slug, kind, addr, label, true)
552}
553
554// RegisterExempt is Register without the admission gates
555// (MaxUnverifiedPerZone and MaxEndpointsPerAddress), for the reviewers a
556// holding realm trusts, as ProposeExempt is, and also takes a zone's last
557// ReservedForReviewers places; the hard cap, MaxEndpointsPerZone, still holds.
558func (r *Registry) RegisterExempt(by address, at int64, slug string, kind EndpointKind, addr, label string) (int64, error) {
559 return r.register(by, at, slug, kind, addr, label, false)
560}
561
562func (r *Registry) register(by address, at int64, slug string, kind EndpointKind, addr, label string, gated bool) (int64, error) {
563 z, _, err := r.zone(slug)
564 if err != nil {
565 return 0, err
566 }
567 if z.Status != Pending && z.Status != Approved {
568 return 0, errors.New("zones: " + slug + " is " + string(z.Status) + " and takes no endpoints")
569 }
570 addr = TrimSpaces(addr)
571 label = TrimSpaces(label)
572 if kind != Seed && kind != Peer {
573 // Validated as it will be stored, so every rule judges the string
574 // that dials, length included.
575 addr = trimEmptyTail(lowerAuthority(addr))
576 }
577 if err := ValidateEndpoint(kind, addr); err != nil {
578 return 0, err
579 }
580 if kind == Seed || kind == Peer {
581 addr = Canonical(kind, addr) // lowercase, no terminal dot: what tm2 dials
582 }
583 private := IsPrivateHost(HostOf(kind, addr))
584 if z.Kind != Local && private {
585 return 0, errors.New("zones: " + slug + " is a " + string(z.Kind) + " zone, and " + addr +
586 " names a private or special-use host; only a local zone lists those")
587 }
588 if err := ValidateLabel(label); err != nil {
589 return 0, err
590 }
591 if !ValidAddress(by) {
592 return 0, errors.New("zones: the registrant is not a valid lowercase address")
593 }
594 akey := addressKey(slug, kind, addr)
595 if r.byAddress.has(akey) {
596 return 0, errors.New("zones: " + slug + " already lists that " + string(kind) + ": " + addr)
597 }
598 if n := r.byZone.Count(slug); n >= MaxEndpointsPerZone {
599 return 0, errors.New("zones: " + slug + " is full at " + strconv.Itoa(MaxEndpointsPerZone) + " endpoints")
600 } else if gated && n >= MaxEndpointsPerZone-ReservedForReviewers {
601 return 0, errors.New("zones: " + slug + "'s last " + strconv.Itoa(ReservedForReviewers) + " endpoint places are kept for curators")
602 }
603 if n := r.Awaiting(slug); gated && n >= MaxUnverifiedPerZone {
604 return 0, errors.New("zones: " + slug + " already has " + strconv.Itoa(n) +
605 " endpoints waiting for review; try again once a curator has cleared some")
606 }
607 okey := ownerKey(slug, by)
608 if n := r.byOwner.count(okey); gated && n >= MaxEndpointsPerAddress {
609 return 0, errors.New("zones: " + by.String() + " already registered " + strconv.Itoa(n) +
610 " endpoints on " + slug + ", the limit is " + strconv.Itoa(MaxEndpointsPerAddress))
611 }
612 e := &Endpoint{Zone: slug, Kind: kind, Address: addr, Label: label, Registrant: by, RegisteredAt: at, Status: Unverified, Exempt: !gated}
613 id := r.endpoints.add(e)
614 e.ID = int64(id)
615 r.ids.rev++
616 e.Revision = int64(r.ids.rev)
617 r.byAddress.set(akey, id)
618 if err := r.byZone.Add(slug, id); err != nil {
619 return 0, err
620 }
621 if err := r.byKind.Add(kindKey(slug, kind), id); err != nil {
622 return 0, err
623 }
624 r.byOwner.inc(okey)
625 if gated {
626 r.fresh.inc(slug)
627 }
628 if err := r.unchecked.Add(slug, id); err != nil {
629 return 0, err
630 }
631 if private {
632 if err := r.private.Add(slug, id); err != nil {
633 return 0, err
634 }
635 }
636 return e.ID, nil
637}
638
639// ReviewEndpoint records a curator's verdict on an endpoint. Any verdict may
640// follow any other, and the same one again with a new reason (so a typo is
641// corrected without passing through a state its registrant may remove it
642// from), with these rules:
643//
644// - every verdict names the endpoint's Revision as read, and fails if the
645// endpoint changed since: a verdict landing after another curator's,
646// unseen, would silently reverse it;
647// - flagging needs a reason, because "do not use this" with no why is not
648// something an operator can act on;
649// - a verification also names the zone's Revision it was checked against,
650// and fails if the zone changed since: what is verified is that the
651// endpoint answers for THIS zone's chain id, and a proposer could otherwise
652// switch it while the verdict is in flight. A flag or an unverify does
653// not, so an edit to the zone cannot hold off a warning;
654// - only an endpoint of a pending or approved zone can be verified.
655func (r *Registry) ReviewEndpoint(id int64, to Verification, zoneRevision, revision int64, by address, at int64, reason string) error {
656 e, err := r.endpoint(id)
657 if err != nil {
658 return err
659 }
660 if !ValidAddress(by) {
661 return errors.New("zones: the reviewer is not a valid lowercase address")
662 }
663 reason = TrimSpaces(reason)
664 if err := ValidateReason(reason); err != nil {
665 return err
666 }
667 switch to {
668 case Unverified, Verified, Flagged:
669 case "":
670 return errors.New("zones: a review needs a verdict")
671 default:
672 // Exact values only: ParseVerification trims, and a " verified " stored
673 // as written would match no filter and count as unverified forever.
674 return errors.New("zones: unknown verification " + strconv.Quote(string(to)) + ", want verified, unverified or flagged")
675 }
676 if e.Status == to && (reason == e.Reason || !HasVisible(reason)) {
677 return errors.New("zones: endpoint #" + strconv.FormatInt(id, 10) + " is already " + string(to) +
678 "; restating it needs a new reason")
679 }
680 if to == Flagged && !HasVisible(reason) {
681 return errors.New("zones: flagging an endpoint needs a reason")
682 }
683 z, _, err := r.zone(e.Zone)
684 if err != nil {
685 return err
686 }
687 if revision != e.Revision {
688 return errors.New("zones: endpoint #" + strconv.FormatInt(id, 10) + " changed since you checked it: it is at revision " +
689 strconv.FormatInt(e.Revision, 10) + ", you acted on revision " + strconv.FormatInt(revision, 10))
690 }
691 if to == Verified {
692 if z.Status != Pending && z.Status != Approved {
693 return errors.New("zones: " + z.Slug + " is " + string(z.Status) + "; nothing on it can be verified")
694 }
695 if zoneRevision != z.Revision {
696 return errors.New("zones: " + z.Slug + " changed since you checked it against it: it is at revision " +
697 strconv.FormatInt(z.Revision, 10) + ", you verified against revision " + strconv.FormatInt(zoneRevision, 10))
698 }
699 }
700 sid := store.ID(id)
701 if to == Verified {
702 r.unchecked.Remove(e.Zone, sid)
703 } else if e.Status == Verified {
704 if err := r.unchecked.Add(e.Zone, sid); err != nil {
705 return err
706 }
707 }
708 if e.Status == Flagged {
709 r.flagged.dec(e.Zone)
710 }
711 if to == Flagged {
712 r.flagged.inc(e.Zone)
713 }
714 if e.Clearable() {
715 r.fresh.dec(e.Zone) // its first verdict: somebody has ruled on it now
716 }
717 e.Status = to
718 e.ReviewedBy, e.ReviewedAt, e.Reason = by, at, reason
719 r.ids.rev++
720 e.Revision = int64(r.ids.rev)
721 return nil
722}
723
724// unverifyAll sends every verified endpoint of a zone back to unverified with
725// that reason, and by as the reviewer ("" when the proposer's own edit caused
726// it). Flagged ones keep their flag. Only the verified ones are read: their
727// ids are the zone's ids minus the unchecked ones, both kept ascending, so one
728// merge pass finds them without loading the rest.
729func (r *Registry) unverifyAll(slug string, by address, at int64, reason string) error {
730 all := r.byZone.Lookup(slug)
731 skip := r.unchecked.Lookup(slug)
732 j := 0
733 for _, id := range all {
734 for j < len(skip) && skip[j] < id {
735 j++
736 }
737 if j < len(skip) && skip[j] == id {
738 continue
739 }
740 v, ok := r.endpoints.get(id)
741 if !ok {
742 continue
743 }
744 e := v.(*Endpoint)
745 if err := r.unchecked.Add(slug, id); err != nil {
746 return err
747 }
748 e.Status = Unverified
749 e.ReviewedBy, e.ReviewedAt, e.Reason = by, at, reason
750 r.ids.rev++
751 e.Revision = int64(r.ids.rev)
752 }
753 return nil
754}
755
756// RemoveEndpoint deletes an endpoint, freeing its storage deposit to whoever
757// signs the removal. revision is the endpoint's Revision as read: a removal
758// fails if a verdict landed since, rather than delete one nobody saw. Who may
759// is the holding realm's call.
760func (r *Registry) RemoveEndpoint(id, revision int64) error {
761 e, err := r.endpoint(id)
762 if err != nil {
763 return err
764 }
765 if revision != e.Revision {
766 return errors.New("zones: endpoint #" + strconv.FormatInt(id, 10) + " changed since you read it: it is at revision " +
767 strconv.FormatInt(e.Revision, 10) + ", you acted on revision " + strconv.FormatInt(revision, 10))
768 }
769 r.removeEndpoint(store.ID(id))
770 return nil
771}
772
773// removeEndpoint unwinds the record and all eight of its indexes and counters
774// together.
775func (r *Registry) removeEndpoint(id store.ID) {
776 v, ok := r.endpoints.remove(id)
777 if !ok {
778 return
779 }
780 e := v.(*Endpoint)
781 r.byAddress.remove(addressKey(e.Zone, e.Kind, e.Address))
782 r.byZone.Remove(e.Zone, id)
783 r.byKind.Remove(kindKey(e.Zone, e.Kind), id)
784 r.byOwner.dec(ownerKey(e.Zone, e.Registrant))
785 r.unchecked.Remove(e.Zone, id)
786 r.private.Remove(e.Zone, id)
787 if e.Status == Flagged {
788 r.flagged.dec(e.Zone)
789 }
790 if e.Clearable() {
791 r.fresh.dec(e.Zone)
792 }
793}
794
795// Revision is the last revision handed out, to a zone or an endpoint. Anything
796// that changes after a read gets a later one.
797func (r *Registry) Revision() int64 { return int64(r.ids.rev) }
798
799// PrivateEndpoints returns a zone's endpoints on a private host, the ones
800// leaving the local kind drops. Only those are read.
801func (r *Registry) PrivateEndpoints(slug string) []Endpoint {
802 out := []Endpoint{}
803 for _, id := range r.private.Lookup(slug) {
804 if v, ok := r.endpoints.get(id); ok {
805 out = append(out, *v.(*Endpoint))
806 }
807 }
808 return out
809}
810
811// Zone returns a copy of the zone under slug.
812func (r *Registry) Zone(slug string) (Zone, bool) {
813 z, _, err := r.zone(slug)
814 if err != nil {
815 return Zone{}, false
816 }
817 return *z, true
818}
819
820// Endpoint returns a copy of the endpoint with that id.
821func (r *Registry) Endpoint(id int64) (Endpoint, bool) {
822 e, err := r.endpoint(id)
823 if err != nil {
824 return Endpoint{}, false
825 }
826 return *e, true
827}
828
829// EndpointByAddress returns a copy of the endpoint a zone lists under that kind
830// and address, compared in [Canonical] form.
831func (r *Registry) EndpointByAddress(slug string, kind EndpointKind, addr string) (Endpoint, bool) {
832 id, ok := r.byAddress.get(addressKey(slug, kind, addr))
833 if !ok {
834 return Endpoint{}, false
835 }
836 v, ok := r.endpoints.get(id)
837 if !ok {
838 return Endpoint{}, false
839 }
840 return *v.(*Endpoint), true
841}
842
843// ZoneFilter selects zones. A zero field matches anything.
844type ZoneFilter struct {
845 Status Status
846 Kind Kind
847}
848
849// Match reports whether z passes the filter.
850func (f ZoneFilter) Match(z Zone) bool {
851 return (f.Status == "" || z.Status == f.Status) && (f.Kind == "" || z.Kind == f.Kind)
852}
853
854// Zones returns copies of the zones passing f, in the order they were
855// proposed. With a status set it reads that status's zones only.
856//
857// The result, like Endpoints', is capped at its length. A slice handed to
858// another realm is readonly there: an append that fits in spare capacity
859// writes into it and aborts, one that does not copies and succeeds. Uncapped,
860// an importer's append would pass or abort by how many rows the filter
861// dropped, which strangers decide by registering.
862func (r *Registry) Zones(f ZoneFilter) []Zone {
863 if f.Status != "" {
864 ids := r.byStatus.Lookup(string(f.Status))
865 out := make([]Zone, 0, len(ids))
866 for _, id := range ids {
867 v, _ := r.zones.get(id)
868 if z := v.(*Zone); f.Match(*z) {
869 out = append(out, *z)
870 }
871 }
872 return out[:len(out):len(out)]
873 }
874 out := make([]Zone, 0, r.zones.len())
875 r.zones.each(func(v any) {
876 if z := v.(*Zone); f.Match(*z) {
877 out = append(out, *z)
878 }
879 })
880 return out[:len(out):len(out)]
881}
882
883// ZoneCount returns how many zones have that status ("" for all), without
884// reading any.
885func (r *Registry) ZoneCount(status Status) int {
886 if status == "" {
887 return r.zones.len()
888 }
889 return r.byStatus.Count(string(status))
890}
891
892// Live returns how many zones are pending or approved: what MaxZones bounds.
893func (r *Registry) Live() int {
894 return r.byStatus.Count(string(Pending)) + r.byStatus.Count(string(Approved))
895}
896
897// ZonePage returns page (1-based) of the zones with that status ("" for every
898// status), size per page, in proposal order. A page past the end is empty.
899//
900// It reads the zones ON the page only; the status's id list (one object, at
901// most MaxZones ids) is read whole and sliced.
902func (r *Registry) ZonePage(status Status, page, size int) []Zone {
903 if status == "" {
904 n := r.zones.len()
905 if page < 1 || size < 1 || page-1 > n/size {
906 return []Zone{}
907 }
908 if size > n {
909 size = n
910 }
911 out := make([]Zone, 0, size)
912 //gnovet:ignore page-offset-overflow bounded by page-1 > n/size at the top
913 r.zones.page((page-1)*size, size, func(v any) { out = append(out, *v.(*Zone)) })
914 return out
915 }
916 ids := pageOf(r.byStatus.Lookup(string(status)), page, size)
917 out := make([]Zone, 0, len(ids))
918 for _, id := range ids {
919 v, _ := r.zones.get(id)
920 out = append(out, *v.(*Zone))
921 }
922 return out
923}
924
925// ApprovedByChainID returns the approved zones naming that chain id, in
926// proposal order. Usually one, but chain ids are not unique across networks
927// (every gnodev is "dev"), so it is a list. Only approved zones are indexed, so
928// proposals naming a real chain id cost a reader of this nothing.
929func (r *Registry) ApprovedByChainID(chainID string) []Zone {
930 ids := r.approved.Lookup(chainID)
931 out := make([]Zone, 0, len(ids))
932 for _, id := range ids {
933 v, _ := r.zones.get(id)
934 out = append(out, *v.(*Zone))
935 }
936 return out
937}
938
939// SoleApproved returns the approved zone naming that chain id when there is
940// exactly one, which is the only case a reader can be pointed at it without a
941// guess. It counts first and reads one record at most.
942func (r *Registry) SoleApproved(chainID string) (Zone, bool) {
943 if r.approved.Count(chainID) != 1 {
944 return Zone{}, false
945 }
946 id, _ := r.approved.First(chainID)
947 v, _ := r.zones.get(id)
948 return *v.(*Zone), true
949}
950
951// EndpointFilter selects endpoints. A zero field matches anything.
952type EndpointFilter struct {
953 Zone string
954 Kind EndpointKind
955 Status Verification
956 Registrant address
957}
958
959// Match reports whether e passes the filter.
960func (f EndpointFilter) Match(e Endpoint) bool {
961 return (f.Zone == "" || e.Zone == f.Zone) &&
962 (f.Kind == "" || e.Kind == f.Kind) &&
963 (f.Status == "" || e.Status == f.Status) &&
964 (f.Registrant == "" || e.Registrant == f.Registrant)
965}
966
967// Endpoints returns copies of the endpoints passing f, oldest first. With a
968// zone set it reads that zone's ids only (that kind's, with a kind set), so it
969// costs at most MaxEndpointsPerZone records. Without one it reads every
970// endpoint in the registry: bound it yourself.
971func (r *Registry) Endpoints(f EndpointFilter) []Endpoint {
972 if f.Zone != "" {
973 var ids []store.ID
974 if f.Kind != "" {
975 ids = r.byKind.Lookup(kindKey(f.Zone, f.Kind))
976 } else {
977 ids = r.byZone.Lookup(f.Zone)
978 }
979 out := make([]Endpoint, 0, len(ids))
980 for _, id := range ids {
981 v, _ := r.endpoints.get(id)
982 if e := v.(*Endpoint); f.Match(*e) {
983 out = append(out, *e)
984 }
985 }
986 return out[:len(out):len(out)]
987 }
988 out := make([]Endpoint, 0, r.endpoints.len())
989 r.endpoints.each(func(v any) {
990 if e := v.(*Endpoint); f.Match(*e) {
991 out = append(out, *e)
992 }
993 })
994 return out[:len(out):len(out)]
995}
996
997// Count returns how many endpoints a zone has, and how many of them are
998// verified, from the index counts: no endpoint is read.
999func (r *Registry) Count(slug string) (verified, total int) {
1000 total = r.byZone.Count(slug)
1001 return total - r.unchecked.Count(slug), total
1002}
1003
1004// EndpointCount returns how many endpoints of that kind a zone has; "" is
1005// every kind. No endpoint is read.
1006func (r *Registry) EndpointCount(slug string, kind EndpointKind) int {
1007 if kind == "" {
1008 return r.byZone.Count(slug)
1009 }
1010 return r.byKind.Count(kindKey(slug, kind))
1011}
1012
1013// Awaiting returns how many of a zone's endpoints are waiting for a verdict,
1014// what MaxUnverifiedPerZone gates. No endpoint is read.
1015func (r *Registry) Awaiting(slug string) int {
1016 return r.unchecked.Count(slug) - r.flagged.count(slug)
1017}
1018
1019// Clearable returns how many of a zone's endpoints are Clearable, what a bulk
1020// clear could remove. No endpoint is read.
1021func (r *Registry) Clearable(slug string) int { return r.fresh.count(slug) }
1022
1023// OwnerCount returns how many endpoints that address registered on a zone. No
1024// endpoint is read.
1025func (r *Registry) OwnerCount(slug string, who address) int {
1026 return r.byOwner.count(ownerKey(slug, who))
1027}
1028
1029// EndpointPage returns page (1-based) of a zone's endpoints of that kind, ""
1030// for every kind, size per page, oldest first. Like [Registry.ZonePage] it reads
1031// the records on the page only, and the bucket's id list (at most
1032// MaxEndpointsPerZone ids) whole.
1033func (r *Registry) EndpointPage(slug string, kind EndpointKind, page, size int) []Endpoint {
1034 var ids []store.ID
1035 if kind != "" {
1036 ids = r.byKind.Lookup(kindKey(slug, kind))
1037 } else {
1038 ids = r.byZone.Lookup(slug)
1039 }
1040 ids = pageOf(ids, page, size)
1041 out := make([]Endpoint, 0, len(ids))
1042 for _, id := range ids {
1043 v, _ := r.endpoints.get(id)
1044 out = append(out, *v.(*Endpoint))
1045 }
1046 return out
1047}
1048
1049// pageOf slices ids to one page, 1-based. Out of range is empty, never a
1050// panic: page and size usually come from a reader's query string.
1051func pageOf(ids []store.ID, page, size int) []store.ID {
1052 // The division comes first so a page number near MaxInt cannot overflow
1053 // the multiplication into a small positive offset.
1054 if page < 1 || size < 1 || page-1 > len(ids)/size {
1055 return nil
1056 }
1057 //gnovet:ignore page-offset-overflow bounded by page-1 > len(ids)/size above
1058 start := (page - 1) * size
1059 if start >= len(ids) {
1060 return nil
1061 }
1062 end := len(ids)
1063 if len(ids)-start > size {
1064 end = start + size
1065 }
1066 return ids[start:end]
1067}
1068
1069// Len returns how many zones the registry holds, whatever their status.
1070func (r *Registry) Len() int { return r.zones.len() }
1071
1072func (r *Registry) zone(slug string) (*Zone, store.ID, error) {
1073 id, ok := r.bySlug.get(slug)
1074 if !ok {
1075 return nil, 0, errors.New("zones: no zone " + strconv.Quote(slug))
1076 }
1077 v, _ := r.zones.get(id)
1078 return v.(*Zone), id, nil
1079}
1080
1081func (r *Registry) endpoint(id int64) (*Endpoint, error) {
1082 if id <= 0 {
1083 return nil, errors.New("zones: no endpoint #" + strconv.FormatInt(id, 10))
1084 }
1085 v, ok := r.endpoints.get(store.ID(id))
1086 if !ok {
1087 return nil, errors.New("zones: no endpoint #" + strconv.FormatInt(id, 10))
1088 }
1089 return v.(*Endpoint), nil
1090}
1091
1092// addressKey is the dedup key: the zone, the kind, and a hash of the address
1093// in [Canonical] form. Hashed so the address part of the key is 16 bytes
1094// whatever the URL's length,
1095// instead of a second copy of up to MaxURLLen bytes the record already holds,
1096// and kept raw rather than hex: it is a key, never shown. Truncated to 128
1097// bits, which no accidental collision reaches and which only lets a deliberate
1098// one refuse its own registration. It is the last field, so a byte that happens
1099// to be "|" cannot be mistaken for a separator.
1100func addressKey(slug string, kind EndpointKind, addr string) string {
1101 sum := sha256.Sum256([]byte(Canonical(kind, addr)))
1102 return slug + "|" + string(kind) + "|" + string(sum[:16])
1103}
1104
1105func kindKey(slug string, kind EndpointKind) string { return slug + "|" + string(kind) }
1106
1107func ownerKey(slug string, who address) string { return slug + "|" + who.String() }
1108
1109// trimInfo normalizes what a caller typed before it is validated: spaces
1110// around every field, and each URL's scheme and host lowercased in ASCII
1111// (both are case-insensitive, gno's link sanitizer is not, so HTTPS:// would
1112// render as a dead link), and a gnoweb or genesis URL's empty tail dropped
1113// (trimEmptyTail), as an address bar writes it. Nothing else is rewritten:
1114// what fails validation is refused, not repaired, and the main RPC with an
1115// empty tail is refused.
1116func trimInfo(in Info) Info {
1117 in.ChainID = TrimSpaces(in.ChainID)
1118 in.Title = TrimSpaces(in.Title)
1119 in.Description = TrimSpaces(in.Description)
1120 in.Kind = Kind(TrimSpaces(string(in.Kind)))
1121 in.GnowebURL = trimEmptyTail(lowerAuthority(TrimSpaces(in.GnowebURL)))
1122 in.RPCURL = lowerAuthority(TrimSpaces(in.RPCURL))
1123 in.GenesisURL = trimEmptyTail(lowerAuthority(TrimSpaces(in.GenesisURL)))
1124 return in
1125}
1126
1127// table is numbered records in a B+ tree: kit/store's shape (an
1128// id that is never reused, keyed by its seqid encoding so the tree iterates in
1129// id order) on a B+ tree, the keyed, ordered container EFFECTIVE_GNO.md
1130// recommends for iteration and pagination. kit/store/v0 sits on an avl, which
1131// it measured at 2,029 B per entry against a B+ tree's 592 to 671, and about
1132// six times the gas per entry iterated, and on an immutable path that choice
1133// is permanent.
1134type table struct {
1135 tree *bptree.BPTree
1136 last uint64
1137}
1138
1139func newTable() *table { return &table{tree: bptree.NewBPTreeN(fanout)} }
1140
1141func (t *table) add(v any) store.ID {
1142 t.last++
1143 id := store.ID(t.last)
1144 t.tree.Set(id.Key(), v)
1145 return id
1146}
1147
1148func (t *table) get(id store.ID) (any, bool) {
1149 v := t.tree.Get(id.Key())
1150 return v, v != nil
1151}
1152
1153func (t *table) remove(id store.ID) (any, bool) { return t.tree.Remove(id.Key()) }
1154
1155func (t *table) len() int { return t.tree.Size() }
1156
1157func (t *table) each(fn func(v any)) {
1158 t.tree.IterateByOffset(0, t.tree.Size(), func(_ string, v any) bool {
1159 fn(v)
1160 return false
1161 })
1162}
1163
1164func (t *table) page(offset, count int, fn func(v any)) {
1165 t.tree.IterateByOffset(offset, count, func(_ string, v any) bool {
1166 fn(v)
1167 return false
1168 })
1169}
1170
1171// unique is a key -> id map in a B+ tree that holds the id itself. kit/index
1172// stores every key as three objects (a box, an entry struct, an ids array), and
1173// for a key that only ever has one id two of them say nothing.
1174type unique struct{ tree *bptree.BPTree }
1175
1176func newUnique() *unique { return &unique{tree: bptree.NewBPTreeN(fanout)} }
1177
1178func (u *unique) has(key string) bool { return u.tree.Has(key) }
1179
1180func (u *unique) get(key string) (store.ID, bool) {
1181 v := u.tree.Get(key)
1182 if v == nil {
1183 return 0, false
1184 }
1185 return v.(store.ID), true
1186}
1187
1188func (u *unique) set(key string, id store.ID) { u.tree.Set(key, id) }
1189
1190func (u *unique) remove(key string) { u.tree.Remove(key) }
1191
1192// counter is a key -> count map in a B+ tree, one object per key. What it
1193// answers (pending proposals per proposer and endpoints per registrant per
1194// zone for the caps, flagged endpoints per zone for the admission gate,
1195// clearable endpoints per zone for the bulk clear) only
1196// ever asks how many, so keeping the ids would be two more objects per key,
1197// written and deleted for nothing.
1198type counter struct{ tree *bptree.BPTree }
1199
1200func newCounter() *counter { return &counter{tree: bptree.NewBPTreeN(fanout)} }
1201
1202func (c *counter) count(key string) int {
1203 v := c.tree.Get(key)
1204 if v == nil {
1205 return 0
1206 }
1207 return v.(int)
1208}
1209
1210func (c *counter) inc(key string) { c.tree.Set(key, c.count(key)+1) }
1211
1212func (c *counter) dec(key string) {
1213 if n := c.count(key) - 1; n > 0 {
1214 c.tree.Set(key, n)
1215 } else {
1216 c.tree.Remove(key)
1217 }
1218}
1219
1220func (c *counter) drop(key string) { c.tree.Remove(key) }