Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

registry.gno

45.74 Kb · 1220 lines
   1package zones
   2
   3import (
   4	"crypto/sha256"
   5	"errors"
   6	"strconv"
   7
   8	"gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/kit/index/v0"
   9	"gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/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) }