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}