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.

flags.gno

27.43 Kb · 692 lines
  1package hlmailbox
  2
  3import (
  4	"chain"
  5	"encoding/hex"
  6	"strconv"
  7	"strings"
  8	"time"
  9
 10	"gno.land/p/nt/avl/v0"
 11	"gno.land/p/g1zdaz6sypk5tdsjc2fzsqrrn4ttn8qzmnl4waqp/hlroute/v0"
 12)
 13
 14// The guardians and the watch keys (bridge/PLAN.md §5.1, §5.3): six numbered seats — 1 to 4 the guardians (3 and 4
 15// the outsiders'), 5 and 6 the operator's two monitors' watch keys — each holding one gno address, and three pause
 16// flags per seat per route: sends (refuses the route's TransferRemote), credits (refuses its Process) and payouts
 17// (refuses its Claim). A function of a route is refused while any seat's flag for it stands. A seat sets and clears
 18// its own flags; GovDAO's unpause clears every seat's flag for the functions it names, and a seat removed or
 19// replaced loses its flags. A guardian's payouts flag may carry the message ids it suspects — a list, or a range of
 20// credits (hlroute.Range: "heights:<from>-<to>", or "since:<from>" for the verifier's credits from that height on,
 21// so one flag covers a flood larger than a list) — which is what unlocks GovDAO's at-once void of those claims; a
 22// watch key's never does.
 23//
 24// The meters (§5.4) start empty after every unpause, or removal, that clears a STANDING payouts flag: this realm
 25// counts those events per route (the payouts epoch) and the route realms read the count.
 26
 27const (
 28	seatCount  = 6
 29	guardians  = 4
 30	FnSends    = "sends"
 31	FnCredits  = "credits"
 32	FnPayouts  = "payouts"
 33	maxFlagIDs = 256 // ids a payouts flag may name
 34
 35	// The launch seats (§5.3, §11.9): PLACEHOLDERS until the four people and the two monitors have keys — seat 1
 36	// is Jae's cold key, the rest derived test addresses — replaced in the deployed source the launch vote reads.
 37	launchSeat1 = address("g1562mcsvwrd2hm3nj7and8ggexdqd3hnag20cmv")
 38	launchSeat2 = address("g16l5nrlrp9pd8rdld7vzylrthfusv30apeq0n6z")
 39	launchSeat3 = address("g1acuz6nhl9hyka2a4t9t965kmd8t355hy7ukur5")
 40	launchSeat4 = address("g1d6nckzj9n8udw8apwrxfg929p8zm330d664ucy")
 41	launchSeat5 = address("g1m8d47hlnmp77rpkamxphlx9q6jfvejt89j7hfj")
 42	launchSeat6 = address("g1gdudkuvqdxwfncvmualymvyufdj8u7m893czuv")
 43)
 44
 45var (
 46	seats         [seatCount + 1]address // by seat number; 0 unused
 47	routes        avl.Tree               // pkgpath → *routeRec
 48	flags         avl.Tree               // flagKey(route, fn, seat) → *flag
 49	successors    avl.Tree               // pkgpath → the route it announced for (SymbolGNOT or SymbolBUBBLE): every realm that announced itself as a route's possible successor (R25, AQ5)
 50	droppedRoutes avl.Tree               // pkgpath → the predecessor whose re-point dropped it (R35): the tombstone of a successor that is a route no more, cleared if a later migrate registers it afresh
 51)
 52
 53// The routes a successor announces for (AQ5): a migrate names only a successor announced for its own route, so a
 54// slip never sends the GNOT lock to a realm whose code moves BUBBLE, or the other way round.
 55const (
 56	SymbolGNOT   = "GNOT"
 57	SymbolBUBBLE = "BUBBLE"
 58)
 59
 60// routeRec is one route realm's record here: its payouts epoch, its place in the chain of migrations (AQ1), the
 61// kind 6 that enrolled it on Solana (AR1), whether it pulled page 0 of its predecessor's migration, and the
 62// successors its own re-points dropped (R35).
 63type routeRec struct {
 64	epoch       int64    // bumps whenever an unpause or a removal clears a standing payouts flag
 65	epochAt     int64    // unix time of the last bump
 66	predecessor string   // the route whose migrate registered this one; "" for a launch route
 67	successor   string   // the route this one's migrate registered; "" until it migrated
 68	enrolled    int64    // the block height r/hlgov last dispatched a kind 6 enrolling this route as the token's router on Solana (NoteEnrolled); 0 if never
 69	enrolledAt  int64    // unix time of that dispatch
 70	pulled      bool     // this successor pulled page 0 of its predecessor's migration, as the predecessor's Pull noted it (NotePulled, R35): the lock moved, the migration is never re-pointed, kind 6 may enrol it
 71	dropped     []string // the successors this route's re-points dropped (R35): each a route no more, in nobody's chain; the line's live last realm takes a message addressed to one, once its import has ended
 72}
 73
 74// maxChain bounds the walk along a route's predecessors (Process): a chain never cycles, since AddRoute refuses a
 75// path that is a route already, and a bridge sees a handful of migrations in its life.
 76const maxChain = 16
 77
 78// flag is one seat's standing flag on one function of one route.
 79type flag struct {
 80	seat   int
 81	at     int64         // unix time it was set, or last re-asserted
 82	ids    []string      // a guardian's payouts flag: the message ids it suspects (none for the other flags) …
 83	ranged bool          // … or the range of credits it suspects
 84	rng    hlroute.Range //
 85}
 86
 87func initSeats() {
 88	seats[1], seats[2], seats[3], seats[4], seats[5], seats[6] = launchSeat1, launchSeat2, launchSeat3, launchSeat4, launchSeat5, launchSeat6
 89}
 90
 91func flagKey(route, fn string, seat int) string { return route + "|" + fn + "|" + strconv.Itoa(seat) }
 92func flagPrefix(route, fn string) string    { return route + "|" + fn + "|" }
 93
 94func isFn(fn string) bool { return fn == FnSends || fn == FnCredits || fn == FnPayouts }
 95
 96func now() int64 { return time.Now().Unix() }
 97
 98// ------------------------------------------------------------- reading --
 99
100// Seat is the address in a seat (1–4 guardians, 5–6 watch keys); "" if the seat is empty.
101func Seat(n int) address {
102	if n < 1 || n > seatCount {
103		return ""
104	}
105	return seats[n]
106}
107
108// SeatOf is the seat an address holds, 0 if none.
109func SeatOf(a address) int {
110	if a == "" {
111		return 0
112	}
113	for i := 1; i <= seatCount; i++ {
114		if seats[i] == a {
115			return i
116		}
117	}
118	return 0
119}
120
121// IsGuardianSeat is whether a seat number is a guardian's (1–4) rather than a watch key's (5–6).
122func IsGuardianSeat(n int) bool { return n >= 1 && n <= guardians }
123
124// IsRoute is whether a pkgpath is one of the route realms this mailbox keeps flags for.
125func IsRoute(path string) bool { return routes.Has(path) }
126
127// Routes lists the route realms' pkgpaths.
128func Routes() []string {
129	var out []string
130	routes.Iterate("", "", func(k string, _ any) bool {
131		out = append(out, k)
132		return false
133	})
134	return out
135}
136
137// DroppedRoutes lists the dropped successors' pkgpaths (R35's tombstones; Dropped names each one's predecessor):
138// routes no more, whose addresses the routes still refuse as a send's recipient and sweep at a credit (AT6), as a
139// registered route's — a holder may have pasted one as a deposit address while its page printed it as the lock's
140// holder to be, and a claim credited to it would be paid into a realm no counter of the line holds. Bounded by the
141// re-points a bridge sees in its life, as Routes is by its migrations.
142func DroppedRoutes() []string {
143	var out []string
144	droppedRoutes.Iterate("", "", func(k string, _ any) bool {
145		out = append(out, k)
146		return false
147	})
148	return out
149}
150
151// Paused is what the routes ask: whether any seat's flag for fn stands on route.
152func Paused(route, fn string) bool {
153	found := false
154	flags.Iterate(flagPrefix(route, fn), flagPrefix(route, fn)+"~", func(_ string, _ any) bool {
155		found = true
156		return true
157	})
158	return found
159}
160
161// Flagged is whether some guardian's standing payouts flag on route names this claim — by its message id, or by a
162// range covering the height it was credited at (a "since" range only if the verifier credited it): the test that
163// makes GovDAO's void of that claim at once (§5.3). A watch key's flag never counts.
164func Flagged(route, id string, height int64, verifier bool) bool {
165	id = cleanID(id)
166	found := false
167	flags.Iterate(flagPrefix(route, FnPayouts), flagPrefix(route, FnPayouts)+"~", func(_ string, v any) bool {
168		f := v.(*flag)
169		if !IsGuardianSeat(f.seat) {
170			return false
171		}
172		if f.ranged && f.rng.Covers(height, verifier) {
173			found = true
174			return true
175		}
176		for _, x := range f.ids {
177			if x == id {
178				found = true
179				return true
180			}
181		}
182		return false
183	})
184	return found
185}
186
187// PayoutsEpoch is how many times an unpause or a removal cleared a standing payouts flag on route, and when the
188// last one did: the routes empty their meters when it changes (§5.4).
189func PayoutsEpoch(route string) (epoch, at int64) {
190	v := routes.Get(route)
191	if v == nil {
192		return 0, 0
193	}
194	r := v.(*routeRec)
195	return r.epoch, r.epochAt
196}
197
198// Flags lists the standing flags of a route as "fn seat@time[ids…]" lines.
199func Flags(route string) []string {
200	var out []string
201	flags.Iterate(route+"|", route+"|~", func(k string, v any) bool {
202		f := v.(*flag)
203		fn := strings.Split(k, "|")[1]
204		s := fn + " by seat " + strconv.Itoa(f.seat) + " (" + string(seats[f.seat]) + ") at " + strconv.FormatInt(f.at, 10)
205		if f.ranged {
206			s += " naming " + f.rng.String()
207		} else if len(f.ids) > 0 {
208			s += " naming " + strings.Join(f.ids, ",")
209		}
210		out = append(out, s)
211		return false
212	})
213	return out
214}
215
216// ------------------------------------------------------------- flagging --
217
218func mustBeRoute(cur realm) string {
219	if !cur.IsCurrent() {
220		panic("hlmailbox: that realm value is not live; pass your own cur")
221	}
222	path := cur.Previous().PkgPath()
223	if !routes.Has(path) {
224		panic("hlmailbox: only a route realm, not " + path)
225	}
226	return path
227}
228
229// parseIDs reads what a payouts flag names: a range (hlroute.ParseRange's forms), or a comma-separated list of
230// message ids, at most maxFlagIDs; "" names nothing.
231func parseIDs(s string) (ids []string, rng hlroute.Range, ranged bool) {
232	if strings.TrimSpace(s) == "" {
233		return nil, rng, false
234	}
235	if r, ok := hlroute.ParseRange(s); ok {
236		return nil, r, true
237	}
238	parts := strings.Split(s, ",")
239	if len(parts) > maxFlagIDs {
240		panic("hlmailbox: more than " + strconv.Itoa(maxFlagIDs) + " ids")
241	}
242	out := make([]string, 0, len(parts))
243	for _, p := range parts {
244		id := cleanID(p)
245		if b, err := hex.DecodeString(id); err != nil || len(b) != 32 {
246			panic("hlmailbox: not a 32-byte message id in hex: " + id)
247		}
248		dup := false
249		for _, x := range out {
250			if x == id {
251				dup = true
252			}
253		}
254		if !dup {
255			out = append(out, id)
256		}
257	}
258	return out, rng, false
259}
260
261// SetFlag sets, or re-asserts, the caller's flag on one function of a route: fn is sends, credits or payouts. ids
262// is what a guardian's payouts flag suspects — a comma-separated list of message ids, or a range of credits
263// ("heights:<from>-<to>" inclusive, or "since:<from>" for every credit the verifier makes from that height on) —
264// "" otherwise; a re-assertion replaces it. A watch key's flag never names ids or a range.
265func SetFlag(cur realm, route, fn, ids string) {
266	who := caller(cur)
267	seat := SeatOf(who)
268	if seat == 0 {
269		panic("hlmailbox: not a guardian or a watch key: " + string(who))
270	}
271	if !routes.Has(route) {
272		panic("hlmailbox: no route " + route)
273	}
274	if !isFn(fn) {
275		panic("hlmailbox: the function must be sends, credits or payouts, not " + fn)
276	}
277	list, rng, ranged := parseIDs(ids)
278	if (len(list) > 0 || ranged) && (fn != FnPayouts || !IsGuardianSeat(seat)) {
279		panic("hlmailbox: only a guardian's payouts flag names ids, or a range")
280	}
281	flags.Set(flagKey(route, fn, seat), &flag{seat: seat, at: now(), ids: list, ranged: ranged, rng: rng})
282	named := strconv.Itoa(len(list))
283	if ranged {
284		named = rng.String()
285	}
286	chain.Emit("Flag", "route", route, "fn", fn, "seat", strconv.Itoa(seat), "ids", named)
287}
288
289// ClearFlag clears the caller's own flag on one function of a route. It resets no meter.
290func ClearFlag(cur realm, route, fn string) {
291	who := caller(cur)
292	seat := SeatOf(who)
293	if seat == 0 {
294		panic("hlmailbox: not a guardian or a watch key: " + string(who))
295	}
296	if _, ok := flags.Remove(flagKey(route, fn, seat)); !ok {
297		panic("hlmailbox: no such flag of yours")
298	}
299	chain.Emit("Unflag", "route", route, "fn", fn, "seat", strconv.Itoa(seat))
300}
301
302// ClearFlags clears every seat's flag for fn on the calling route — the void builder's andUnpause option (§5.3),
303// which reopens payouts in the same closure as the void. It reports whether any flag was cleared.
304func ClearFlags(cur realm, fn string) bool {
305	path := mustBeRoute(cur)
306	if !isFn(fn) {
307		panic("hlmailbox: the function must be sends, credits or payouts, not " + fn)
308	}
309	n, _ := clearFns(path, []string{fn}, now())
310	return n > 0
311}
312
313// VacateSeat removes the key in a seat if holder still has it, its flags with it (a payouts flag among them
314// restarting the route's meters): the void builder's unpause option retires keys in the same closure as the void
315// (§5.3). Only a route realm may call it. It reports whether the seat was vacated: a seat whose occupant changed
316// since the proposal named it, or one emptied since, is left as it is (R17), so a replacement that landed in
317// between is never undone by a removal that meant the key before it.
318func VacateSeat(cur realm, seat int, holder address) bool {
319	mustBeRoute(cur)
320	if seat < 1 || seat > seatCount {
321		panic("hlmailbox: the seat must be 1 to 6")
322	}
323	if seats[seat] == "" || seats[seat] != holder {
324		return false
325	}
326	clearSeat(seat, now())
327	seats[seat] = ""
328	chain.Emit("SeatSet", "seat", strconv.Itoa(seat), "address", "")
329	return true
330}
331
332// AddRoute registers a successor route realm (a route's migrate names it), so flags can be set on it too, and
333// records the succession both ways (AQ1): the caller's `successor`, the new route's `predecessor`, which is what
334// lets the successor take a message addressed to the predecessor (Process) — the burns name the predecessor until
335// kind 6 enrolls the successor. The path is a deployed realm's: a sub-identity's (`host#subpath`) is refused, since
336// the route powers are keyed by exact pkgpath (mustBeRoute) and such a path would hand them to a token its host
337// mints for free; a path that is a route already is refused (a chain never cycles). A route that migrated already
338// re-points its migration (AR2): its migrate decision names another successor while the first pulled nothing —
339// the route's own state refuses once page 0 moved, and a first successor that migrated on holds the lock, so it
340// is refused here too — and the first's registration is dropped with the flags copied to it, the new path taking
341// its place with the flags standing on the route now.
342func AddRoute(cur realm, path string) {
343	from := mustBeRoute(cur)
344	if !strings.HasPrefix(path, "gno.land/r/") || strings.Contains(path, "#") {
345		panic("hlmailbox: a route is a realm under gno.land/r/, not " + path)
346	}
347	if routes.Has(path) {
348		panic("hlmailbox: " + path + " is a route already")
349	}
350	rec := routes.Get(from).(*routeRec)
351	if old := rec.successor; old != "" {
352		if s := Successor(old); s != "" {
353			panic("hlmailbox: " + from + " migrated to " + old + ", which migrated on to " + s + " already: the migration is under way and cannot be re-pointed")
354		}
355		if Pulled(old) {
356			panic("hlmailbox: " + from + " migrated to " + old + ", which pulled page 0 already: the migration is under way and cannot be re-pointed")
357		}
358		dropRoute(from, rec, old)
359	}
360	rec.successor = path
361	undrop(path)
362	routes.Set(path, &routeRec{predecessor: from})
363	n := copyFlags(from, path)
364	chain.Emit("RouteAdded", "pkgpath", path, "predecessor", from, "flags", strconv.Itoa(n))
365}
366
367// dropRoute forgets a successor's registration (AR2: its predecessor re-pointed the migration before it pulled
368// anything): the record and every flag standing on its path. It is a route no more — nothing addressed to its
369// predecessor is its to take, no route power is its — but it is remembered (R35): on its predecessor's record and
370// as a tombstone, since Solana's token may still name it the router until the next kind 6 and a burn addressed to
371// it must be the line's last realm's to take, never its own. A later migrate may register it afresh (undrop).
372func dropRoute(from string, rec *routeRec, path string) {
373	routes.Remove(path)
374	var keys []string
375	flags.Iterate(path+"|", path+"|~", func(k string, _ any) bool {
376		keys = append(keys, k)
377		return false
378	})
379	for _, k := range keys {
380		flags.Remove(k)
381	}
382	rec.dropped = append(rec.dropped, path)
383	droppedRoutes.Set(path, from)
384	chain.Emit("RouteDropped", "pkgpath", path, "predecessor", from, "flags", strconv.Itoa(len(keys)))
385}
386
387// undrop clears a path's tombstone when a migrate registers it afresh (R35): the migration re-pointed back to it
388// while the newer successor pulled nothing, or a later route of the line named it. It is then a route like any
389// other, with nothing pulled.
390func undrop(path string) {
391	v := droppedRoutes.Get(path)
392	if v == nil {
393		return
394	}
395	by := v.(string)
396	droppedRoutes.Remove(path)
397	if r := routes.Get(by); r != nil {
398		rec := r.(*routeRec)
399		var kept []string
400		for _, p := range rec.dropped {
401			if p != path {
402				kept = append(kept, p)
403			}
404		}
405		rec.dropped = kept
406	}
407	chain.Emit("RouteUndropped", "pkgpath", path, "predecessor", by)
408}
409
410// Dropped is the predecessor whose re-point dropped the realm at path from the line (R35) — a route no more, in
411// nobody's chain, that neither sends (Dispatch refuses it) nor takes (Process refuses it) — or "" for a route, a
412// realm never registered, or one registered afresh since.
413func Dropped(path string) string {
414	v := droppedRoutes.Get(path)
415	if v == nil {
416		return ""
417	}
418	return v.(string)
419}
420
421// Pulled is whether the successor at path pulled page 0 of its predecessor's migration, as the predecessor's Pull
422// noted it (NotePulled): the lock and the counters moved, so the migration is never re-pointed and a kind 6 may
423// enrol it (R35). False for a launch route, a successor that pulled nothing, or no route.
424func Pulled(path string) bool {
425	v := routes.Get(path)
426	if v == nil {
427		return false
428	}
429	return v.(*routeRec).pulled
430}
431
432// NotePulled is the predecessor's note, from its Pull, that the successor its migrate registered has pulled page 0
433// (R35): the lock and the counters moved with it, so the migration is under way — the route's own state refuses a
434// re-point from here on, and AddRoute refuses one too — and r/hlgov's kind 6 may enrol the successor: before it a
435// kind 6 could name a route a re-point then drops, leaving Solana's token minting for a realm in nobody's chain.
436// Only the route whose successor it is notes it; a second note (a second Pull of page 0) changes nothing.
437func NotePulled(cur realm, path string) {
438	from := mustBeRoute(cur)
439	rec := routes.Get(from).(*routeRec)
440	if rec.successor != path {
441		panic("hlmailbox: " + path + " is not " + from + "'s successor")
442	}
443	sv := routes.Get(path).(*routeRec)
444	if !sv.pulled {
445		sv.pulled = true
446		chain.Emit("RoutePulled", "pkgpath", path, "predecessor", from)
447	}
448}
449
450// copyFlags gives the route at `to` a copy of every flag standing on `from` — the seat, the time it was set, the
451// ids or the range it names — keyed by the new path (R27): the pause follows the lock and the claims to the
452// successor, whose Claim and Process ask their own path's flags, and clears there as any flag does — by the
453// seat, by GovDAO's unpause, or with the seat's removal. It reports how many were copied.
454func copyFlags(from, to string) (n int) {
455	var keys []string
456	var fs []*flag
457	flags.Iterate(from+"|", from+"|~", func(k string, v any) bool {
458		keys = append(keys, k)
459		fs = append(fs, v.(*flag))
460		return false
461	})
462	for i, k := range keys {
463		f := fs[i]
464		fn := strings.Split(k, "|")[1]
465		flags.Set(flagKey(to, fn, f.seat), &flag{seat: f.seat, at: f.at, ids: append([]string(nil), f.ids...), ranged: f.ranged, rng: f.rng})
466		n++
467	}
468	return n
469}
470
471// Predecessor is the route whose migrate registered path as its successor; "" for a launch route, or no route.
472func Predecessor(path string) string {
473	v := routes.Get(path)
474	if v == nil {
475		return ""
476	}
477	return v.(*routeRec).predecessor
478}
479
480// Successor is the route path's migrate registered; "" while it never migrated, or for no route.
481func Successor(path string) string {
482	v := routes.Get(path)
483	if v == nil {
484		return ""
485	}
486	return v.(*routeRec).successor
487}
488
489// Enrolled is the block height and unix time r/hlgov last dispatched a kind 6 enrolling the route at path as the
490// token's router on Solana (NoteEnrolled); 0, 0 if never, or for no route. A route's refund builder waits 7 days
491// after it (AR1).
492func Enrolled(path string) (height, at int64) {
493	v := routes.Get(path)
494	if v == nil {
495		return 0, 0
496	}
497	r := v.(*routeRec)
498	return r.enrolled, r.enrolledAt
499}
500
501// mustBeHlgov is the caller check of what r/hlgov alone does here: its exact pkgpath, read from the live cur.
502func mustBeHlgov(cur realm) {
503	if !cur.IsCurrent() {
504		panic("hlmailbox: that realm value is not live; pass your own cur")
505	}
506	if p := cur.Previous().PkgPath(); p != hlgovPath {
507		panic("hlmailbox: only " + hlgovPath + " notes an enrolment, not " + p)
508	}
509}
510
511// NoteEnrolled is r/hlgov's note, from its kind-6 closure, that it dispatched a decision enrolling the route at
512// path as the token's router on Solana at this block height (AR1): the route's refund builder and its apply read
513// it (Enrolled) and refuse a refund until the note is 14 days old (R37: a week for the delivery, a week for
514// Solana's own clock), since a refunded send stays deliverable on Solana until the predecessor's router is
515// replaced and a refund that lands first lets one stranger's Reemit and delivery mint it twice. The latest note
516// counts: a later kind 6 for the route restarts the wait; a cancelled one clears it (ClearEnrolled). Necessary,
517// not sufficient — the realm cannot see Solana; hlwatch reads the token's enrolled router there and alarms a
518// refund proposal whose successor the token has not enrolled.
519func NoteEnrolled(cur realm, path string, height int64) {
520	mustBeHlgov(cur)
521	v := routes.Get(path)
522	if v == nil {
523		panic("hlmailbox: no route " + path)
524	}
525	if height < 1 {
526		panic("hlmailbox: not a block height: " + strconv.FormatInt(height, 10))
527	}
528	r := v.(*routeRec)
529	r.enrolled, r.enrolledAt = height, now()
530	chain.Emit("RouteEnrolled", "pkgpath", path, "height", strconv.FormatInt(height, 10))
531}
532
533// ClearEnrolled is r/hlgov's word, from its cancel closure, that the kind 6 whose note stands on the route at path
534// was cancelled (R37, AS2): the note goes, so the route's refund gate reads "no kind 6" again and its clock
535// restarts with a re-proposal — before, a cancelled, lost or held kind 6 left the note standing and the gate
536// opened on the realm's clock alone while the predecessor was still the enrolled router. r/hlgov's exact pkgpath
537// alone, for a route; a route with no note is left as it is.
538func ClearEnrolled(cur realm, path string) {
539	mustBeHlgov(cur)
540	v := routes.Get(path)
541	if v == nil {
542		panic("hlmailbox: no route " + path)
543	}
544	r := v.(*routeRec)
545	if r.enrolled == 0 {
546		return
547	}
548	r.enrolled, r.enrolledAt = 0, 0
549	chain.Emit("RouteEnrolmentCleared", "pkgpath", path)
550}
551
552// successorOf is whether the route at path succeeds the route whose address, as 32 bytes, is recipient — along the
553// chain of migrations, at any distance (AQ1): a burn addressed to a migrated route is taken by the successor that
554// holds its lock. A successor a predecessor's re-point dropped counts as that predecessor's (R35): a burn made
555// while Solana still named it the router is the line's live last realm's to take, never its own. A caller that is
556// no route, or whose chain never reaches the recipient, is not.
557func successorOf(path string, recipient [32]byte) bool {
558	for i := 0; i < maxChain && path != ""; i++ {
559		v := routes.Get(path)
560		if v == nil {
561			return false
562		}
563		path = v.(*routeRec).predecessor
564		if path == "" {
565			return false
566		}
567		if mustBytes32(chain.PackageAddress(path)) == recipient {
568			return true
569		}
570		if pv := routes.Get(path); pv != nil {
571			for _, d := range pv.(*routeRec).dropped {
572				if mustBytes32(chain.PackageAddress(d)) == recipient {
573					return true
574				}
575			}
576		}
577	}
578	return false
579}
580
581// AnnounceSuccessor is how a realm offers itself as a route's successor (R25): called once from that realm's own
582// code (its init, usually), naming the route it succeeds — GNOT or BUBBLE (AQ5: a migrate names only a successor
583// announced for its own route) — it is kept under the caller's exact pkgpath, which a route's migrate builder and
584// its closure require of the path they name — so the lock never goes to a path with no realm behind it (a typo,
585// or an undeployed path whose namespace owner could deploy anything there and pull the lock). Only an /r/ realm,
586// with its own cur: a sub-realm identity (host#subpath, a token a realm mints for free) is refused as in
587// RegisterVerifier, so every entry costs a package deployment and the listing (render.gno) is bounded. Nothing is
588// ever withdrawn; GovDAO reads the successor's source before it votes, as before.
589func AnnounceSuccessor(cur realm, symbol string) {
590	if !cur.IsCurrent() {
591		panic("hlmailbox: that realm value is not live; pass your own cur")
592	}
593	path := cur.Previous().PkgPath()
594	if !strings.HasPrefix(path, "gno.land/r/") {
595		panic("hlmailbox: only a realm under gno.land/r/ announces itself as a successor, not " + path)
596	}
597	if cur.Previous().Subpath() != "" {
598		panic("hlmailbox: a realm announces itself with its own cur, not a sub-identity (" + path + ")")
599	}
600	if symbol != SymbolGNOT && symbol != SymbolBUBBLE {
601		panic("hlmailbox: the route a successor announces for is GNOT or BUBBLE, not " + symbol)
602	}
603	if successors.Has(path) {
604		panic("hlmailbox: " + path + " announced itself already")
605	}
606	successors.Set(path, symbol)
607	chain.Emit("SuccessorAnnounced", "pkgpath", path, "route", symbol)
608}
609
610// SuccessorAnnounced is whether a realm at this pkgpath announced itself as a possible successor: what a route's
611// migrate builder and its closure require of the path they name (R25).
612func SuccessorAnnounced(path string) bool { return successors.Has(path) }
613
614// SuccessorFor is the route — GNOT or BUBBLE — a realm at this pkgpath announced itself as a successor of (AQ5);
615// "" if it never announced itself. A route's migrate builder and its closure refuse another route's successor.
616func SuccessorFor(path string) string {
617	v := successors.Get(path)
618	if v == nil {
619		return ""
620	}
621	return v.(string)
622}
623
624// RouteSymbol is the token a realm's route is for (R36): the launch paths' compiled symbols; for every other
625// realm the route it announced itself a successor of (SuccessorFor — a registered successor always announced,
626// AQ5); "" for one that never did. r/hlgov's kind 6 compares it with the token it names, so a slip can never
627// enrol one token's route as the other's router.
628func RouteSymbol(path string) string {
629	switch path {
630	case launchRouteGNOT:
631		return SymbolGNOT
632	case launchRouteBUBBLE:
633		return SymbolBUBBLE
634	}
635	return SuccessorFor(path)
636}
637
638// clearFns removes every seat's flag for the named functions on a route; payouts says whether a standing payouts
639// flag went with them, in which case the route's meters restart (the epoch bumps).
640func clearFns(path string, fns []string, t int64) (n int, payouts bool) {
641	for _, fn := range fns {
642		var keys []string
643		flags.Iterate(flagPrefix(path, fn), flagPrefix(path, fn)+"~", func(k string, _ any) bool {
644			keys = append(keys, k)
645			return false
646		})
647		for _, k := range keys {
648			flags.Remove(k)
649			n++
650			if fn == FnPayouts {
651				payouts = true
652			}
653		}
654	}
655	if payouts {
656		bump(path, t)
657	}
658	return n, payouts
659}
660
661// clearSeat removes every flag a seat holds, on every route, bumping the epoch of each route where a payouts
662// flag stood.
663func clearSeat(seat int, t int64) (n int) {
664	suffix := "|" + strconv.Itoa(seat)
665	var keys []string
666	flags.Iterate("", "", func(k string, v any) bool {
667		if strings.HasSuffix(k, suffix) && v.(*flag).seat == seat {
668			keys = append(keys, k)
669		}
670		return false
671	})
672	for _, k := range keys {
673		flags.Remove(k)
674		n++
675		parts := strings.Split(k, "|")
676		if parts[1] == FnPayouts {
677			bump(parts[0], t)
678		}
679	}
680	return n
681}
682
683func bump(path string, t int64) {
684	v := routes.Get(path)
685	if v == nil {
686		return
687	}
688	r := v.(*routeRec)
689	r.epoch++
690	r.epochAt = t
691	chain.Emit("PayoutsReset", "route", path, "epoch", strconv.FormatInt(r.epoch, 10))
692}